Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use pre-commit run prettier if prettier is not available #1983

Merged
merged 26 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fbb1759
use f-string instead of format
fabianegli Oct 29, 2022
b1882d8
use pre-commit installed Prettier if not already installed
fabianegli Oct 29, 2022
a9a9be9
ship and use the nf-core pre-commit config for linting
fabianegli Oct 29, 2022
f77c935
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Oct 29, 2022
5860858
fix typo in variable name
fabianegli Oct 29, 2022
0cca4ce
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 2, 2022
1e46a5d
simplify conditions and utilize lazy eval
fabianegli Nov 5, 2022
d46eec9
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 5, 2022
d648284
fix typo in variable name
fabianegli Nov 6, 2022
d143da6
remove unused argument
fabianegli Nov 7, 2022
dcb8ac0
only compute fix flags if needed
fabianegli Nov 7, 2022
cd1b05d
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 7, 2022
81b0ed2
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 11, 2022
9c95c5c
make running prettier return better error messages
fabianegli Nov 14, 2022
22819d9
add tests for prettier invocations
fabianegli Nov 14, 2022
d52d83d
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 14, 2022
5f4711e
remove unused imports
fabianegli Nov 14, 2022
b674a3b
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 15, 2022
4ad7812
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 15, 2022
6760188
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 15, 2022
1e76772
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 16, 2022
333c918
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 17, 2022
fe1ca2b
exclusiely run prettier from pre-commit hook
fabianegli Nov 17, 2022
0d52e0d
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 17, 2022
bb5ad2d
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 17, 2022
0ec2d1e
Merge branch 'dev' into pre-commit-run-prettier
fabianegli Nov 21, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ graft nf_core/module-template
graft nf_core/pipeline-template
graft nf_core/subworkflow-template
include requirements.txt
include .pre-commit-config.yaml
2 changes: 1 addition & 1 deletion nf_core/lint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def run_linting(
lint_obj._print_results(show_passed)
module_lint_obj._print_results(show_passed)
nf_core.lint_utils.print_joint_summary(lint_obj, module_lint_obj)
nf_core.lint_utils.print_fixes(lint_obj, module_lint_obj)
nf_core.lint_utils.print_fixes(lint_obj)

# Save results to Markdown file
if md_fn is not None:
Expand Down
72 changes: 59 additions & 13 deletions nf_core/lint_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import logging
import shutil
import subprocess
from pathlib import Path

import rich
from rich.console import Console
Expand Down Expand Up @@ -34,24 +36,25 @@ def print_joint_summary(lint_obj, module_lint_obj):
console.print(table)


def print_fixes(lint_obj, module_lint_obj):
def print_fixes(lint_obj):
"""Prints available and applied fixes"""

if len(lint_obj.could_fix):
fix_cmd = "nf-core lint {} --fix {}".format(
"" if lint_obj.wf_path == "." else f"--dir {lint_obj.wf_path}", " --fix ".join(lint_obj.could_fix)
)
if lint_obj.could_fix:
lint_dir = "" if lint_obj.wf_path == "." else f"--dir {lint_obj.wf_path}"
fix_flags = " ".join([f"--fix {file}" for file in lint_obj.could_fix])
console.print(
f"\nTip: Some of these linting errors can automatically be resolved with the following command:\n\n[blue] {fix_cmd}\n"
"\nTip: Some of these linting errors can automatically be resolved with the following command:"
f"\n\n[blue] nf-core lint {lint_dir} {fix_flags}\n"
)
if len(lint_obj.fix):
console.print(
"Automatic fixes applied. Please check with 'git diff' and revert any changes you do not want with 'git checkout <file>'."
"Automatic fixes applied. Please check with 'git diff' and revert "
"any changes you do not want with 'git checkout <file>'."
)


def run_prettier_on_file(file):
"""Runs Prettier on a file if Prettier is installed.
"""Run Prettier on a file.

Args:
file (Path | str): A file identifier as a string or pathlib.Path.
Expand All @@ -60,12 +63,55 @@ def run_prettier_on_file(file):
If Prettier is not installed, a warning is logged.
"""

if shutil.which("prettier"):
_run_prettier_on_file(file)
elif shutil.which("pre-commit"):
_run_pre_commit_prettier_on_file(file)
else:
log.warning(
"Neither Prettier nor the prettier pre-commit hook are available. At least one of them is required."
)


def _run_prettier_on_file(file):
"""Run natively installed Prettier on a file."""

try:
subprocess.run(
["prettier", "--write", file],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
capture_output=True,
check=True,
)
except subprocess.CalledProcessError as e:
if ": SyntaxError: " in e.stderr.decode():
raise ValueError(f"Can't format {file} because it has a synthax error.\n{e.stderr.decode()}") from e
raise ValueError(
"There was an error running the prettier pre-commit hook.\n"
f"STDOUT: {e.stdout.decode()}\nSTDERR: {e.stderr.decode()}"
) from e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote we either go all in on pre-commit for nf-core tools or not.

Copy link
Contributor Author

@fabianegli fabianegli Nov 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And include pre-commit as a dependency in tools?

This PR makes pre-commit a dependency. Installing the hooks is not necessary, they can be run directly. Installing them with pre-commit install just makes them run before any commit in the repo. So install is not (just) installing the tools but setting a git repo up to use them. pre-commit run will also install the hooks but not change any settings in any git repo. The beauty of pre-commit is that it takes care of everything based on the configuration file and because we have one we can just use it.

The way it is set up means the user doesn't need to handle any installation manually and neither is there a need for a separate nf-core lint setup command.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I'm following now. Still wondering, since it'll be installed, why allow the system prettier install? I get it's probably faster, less stuff installed, but more code for us to maintain.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am all for only using prettier via the pre-commit hook mechanism. The global Prettier would make the first invocation a bit quicker, because pre-commit has to pull the hook from the web the first time it runs. I also have some questions about the configuration for when prettier is invoked from a global install, because it might not see the same configs as the one from the pre-commit hooks. I'd have to look at this, too.
One more point for only using pre-commit prettier is that the code - both n production and testing - would become much simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am tempted to actually promote running pre-commit install by default. It would make the developer experience more platform independent, but it also installs pre-commit hooks in all repos nf-core is used in and this may or may not be desirable depending on the project.

Copy link
Contributor Author

@fabianegli fabianegli Nov 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because pre-commit has to pull the hook from the web the first time it runs.

And this also means internet access is necessary for the first time prettier is run. If a developer doesn't have internet access but prettier is installed on the machine it would help to keep the current setup, although it is more complex. (hitherto I remain ignorant about the issue of the configuration for Prettier native)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to move the discussion about the use of pre-commit install out of this PR because I see it as a separate issue that does not decrease the usefulness of this PR, but considerably complicates it and would drag the merge of this PR unnecessarily.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How/where does running pre-commit prettier get it's config? Although the primary use-case for tools is working with the public nf-core repos I am mindful of the fact that people do use them internally (selfish bias here too as I do) and wonder whether there are cases where running prettier (or using some fixed config) is undesirable?

If pre-commit prettier draws its config from the repo/global config files in the same way as running manually installed prettier does then this is probably less of an issue

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hooks take the configuration in the repository where they are run into account.



def _run_pre_commit_prettier_on_file(file):
"""Runs pre-commit hook prettier on a file if pre-commit is installed.

Args:
file (Path | str): A file identifier as a string or pathlib.Path.

Warns:
If Prettier is not installed, a warning is logged.
"""

nf_core_pre_commit_config = Path(nf_core.__file__).parent.parent / ".pre-commit-config.yaml"
try:
subprocess.run(
["pre-commit", "run", "--config", nf_core_pre_commit_config, "prettier", "--files", file],
capture_output=True,
check=True,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wondering if we can import pre-commit the package and call this manually? Or is using it as a subprocess more transparent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the manual and the subprocess version serve different purposes. The automated one should make sure all autogenerated file changes are lint-compliant and the manual invocations can help after manually changing files plus installing the pre-commit hooks will protect the git revision history from content that doesn't pass linting.

Copy link
Contributor Author

@fabianegli fabianegli Nov 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I answered the wrong question. The real one I don't know. Well, I kind of know. It's not the intended use of pre-commit. Not impossible, but a hack.

Copy link
Contributor Author

@fabianegli fabianegli Nov 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to know why I classify the alternative as a "hack", compare the complexity of the parameters in the run function wit the much simpler command line invocation.

Copy link
Contributor Author

@fabianegli fabianegli Nov 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sidenote: running a command in subprocess adds very little overhead (about 0.04 seconds on a 10 year old machine) and there's no state or computation that we already have when we invoke it that we can share with prettier to make it run faster anyway. I would say we can drop this line of inquiry.

except FileNotFoundError:
log.warning("Prettier is not installed. Please install it and run it on the pipeline to fix linting issues.")
except subprocess.CalledProcessError as e:
if ": SyntaxError: " in e.stdout.decode():
raise ValueError(f"Can't format {file} because it has a synthax error.\n{e.stdout.decode()}") from e
raise ValueError(
"There was an error running the prettier pre-commit hook.\n"
f"STDOUT: {e.stdout.decode()}\nSTDERR: {e.stderr.decode()}"
) from e
1 change: 0 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
black
isort
myst_parser
pre-commit
pytest-cov
pytest-datafiles
requests-mock
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ jinja2
jsonschema>=3.0
markdown>=3.3
packaging
pre-commit
prompt_toolkit>=3.0.3
pytest>=7.0.0
pytest-workflow>=1.6.0
Expand Down
98 changes: 98 additions & 0 deletions tests/test_lint_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import shutil

import git
import pytest

import nf_core.lint_utils

JSON_WITH_SYNTAX_ERROR = "{'a':1, 1}"
JSON_MALFORMED = "{'a':1}"
JSON_FORMATTED = '{ "a": 1 }\n'

WHICH_PRE_COMMIT = shutil.which("pre-commit")


@pytest.fixture()
def temp_git_repo(tmp_path_factory):
tmp_git_dir = tmp_path_factory.mktemp("tmp_git_dir")
repo = git.Repo.init(tmp_git_dir)
return tmp_git_dir, repo


@pytest.fixture(name="formatted_json")
def git_dir_with_json(temp_git_repo):
tmp_git_dir, repo = temp_git_repo
file = tmp_git_dir / "formatted.json"
with open(file, "w", encoding="utf-8") as f:
f.write(JSON_FORMATTED)
repo.git.add(file)
return file


@pytest.fixture(name="malformed_json")
def git_dir_with_json_malformed(temp_git_repo):
tmp_git_dir, repo = temp_git_repo
file = tmp_git_dir / "malformed.json"
with open(file, "w", encoding="utf-8") as f:
f.write(JSON_MALFORMED)
repo.git.add(file)
return file


@pytest.fixture(name="synthax_error_json")
def git_dir_with_json_syntax_error(temp_git_repo):
tmp_git_dir, repo = temp_git_repo
file = tmp_git_dir / "synthax-error.json"
with open(file, "w", encoding="utf-8") as f:
f.write(JSON_WITH_SYNTAX_ERROR)
repo.git.add(file)
return file


@pytest.fixture
def ensure_prettier_is_not_found(monkeypatch):
def dont_find_prettier(x):
if x == "pre-commit":
which_x = WHICH_PRE_COMMIT
elif x == "prettier":
which_x = None
else:
raise ValueError(f"This mock is only inteded to hide prettier form tests. {x}")
return which_x

monkeypatch.setattr("nf_core.lint_utils.shutil.which", dont_find_prettier)


@pytest.mark.skipif(shutil.which("prettier") is None, reason="Can't test prettier if it is not available.")
def test_run_prettier_on_formatted_file(formatted_json):
nf_core.lint_utils.run_prettier_on_file(formatted_json)
assert formatted_json.read_text() == JSON_FORMATTED


def test_run_pre_commit_prettier_on_formatted_file(formatted_json, ensure_prettier_is_not_found):
nf_core.lint_utils.run_prettier_on_file(formatted_json)
assert formatted_json.read_text() == JSON_FORMATTED


@pytest.mark.skipif(shutil.which("prettier") is None, reason="Can't test prettier if it is not available.")
def test_run_prettier_on_malformed_file(malformed_json):
nf_core.lint_utils.run_prettier_on_file(malformed_json)
assert malformed_json.read_text() == JSON_FORMATTED


def test_run_prettier_pre_commit_on_malformed_file(malformed_json, ensure_prettier_is_not_found):
nf_core.lint_utils.run_prettier_on_file(malformed_json)
assert malformed_json.read_text() == JSON_FORMATTED


@pytest.mark.skipif(shutil.which("prettier") is None, reason="Can't test prettier if it is not available.")
def test_run_prettier_on_synthax_error_file(synthax_error_json):
with pytest.raises(ValueError) as exc_info:
nf_core.lint_utils.run_prettier_on_file(synthax_error_json)
assert exc_info.value.args[0].startswith(f"Can't format {synthax_error_json} because it has a synthax error.")


def test_run_prettier_pre_commit_on_synthax_error_file(synthax_error_json, ensure_prettier_is_not_found):
with pytest.raises(ValueError) as exc_info:
nf_core.lint_utils.run_prettier_on_file(synthax_error_json)
assert exc_info.value.args[0].startswith(f"Can't format {synthax_error_json} because it has a synthax error.")