Skip to content

Commit 49c7dce

Browse files
authored
make format command (#221)
* setup ability to print diff with whitespace only changes highlighted * got it working * dog-food itself * remove un-needed import * don't cause confusion with stdlib * get tests working * format * make format produce something the linter is happy with * update readme * s/fix/format * add global exception handling/logging * rename this back * get import sorting to work the way we want * cleanup fix module * dog-food * silence formatting calls * make test output consistent * ni-python-styleguide format! * add better-diff as dep * switch over to using better_diff * pass down file contents instead of file handles * also specify the fromfile name * update snapshots * type-hint the extend_ignore all the way down * handle the correct types on extend_ignore * reduce diff * dog-food format * didn't use this
1 parent b3147f8 commit 49c7dce

File tree

19 files changed

+273
-85
lines changed

19 files changed

+273
-85
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@ application-import-names = "<app_name>"
6363

6464
### Formatting
6565

66-
`ni-python-styleguide` in the future will have a `format` command which we intend to fix as many lint issues as possible.
66+
`ni-python-styleguide` has a subcommand `format` which will run [black](https://pypi.org/project/black/) and [isort](https://pycqa.github.io/isort/).
6767

68-
Until then you'll want to set the following to get `black` formatting as the styleguide expects.
68+
If you wish to be able to invoke black directly, you'll want to set the following to get `black` formatting as the styleguide expects.
6969

7070
```toml
7171
# pyproject.toml

ni_python_styleguide/_acknowledge_existing_errors/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import pathlib
33
import re
4+
import typing
45
from collections import defaultdict
56

67
from ni_python_styleguide import _format, _utils
@@ -41,7 +42,12 @@ def _filter_suppresion_from_line(line: str):
4142

4243

4344
def acknowledge_lint_errors(
44-
exclude, app_import_names, extend_ignore, file_or_dir, *_, aggressive=False
45+
exclude,
46+
app_import_names,
47+
extend_ignore: typing.Optional[str],
48+
file_or_dir,
49+
*_,
50+
aggressive=False,
4551
):
4652
"""Adds a "noqa" comment for each of existing errors (unless excluded).
4753

ni_python_styleguide/_cli.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import pathlib
22
import sys
3+
import typing
34

45
import click
56
import toml
67

7-
from ni_python_styleguide import _acknowledge_existing_errors
8-
from ni_python_styleguide import _fix
9-
from ni_python_styleguide import _Flake8Error
10-
from ni_python_styleguide import _lint
8+
from ni_python_styleguide import _acknowledge_existing_errors, _fix, _Flake8Error, _lint
119

1210

1311
def _qs_or_vs(verbosity):
@@ -181,12 +179,30 @@ def acknowledge_existing_violations(obj, extend_ignore, file_or_dir, aggressive)
181179
help="Remove any existing acknowledgments, fix what can be fixed, and re-acknowledge remaining.",
182180
)
183181
@click.pass_obj
184-
def fix(obj, extend_ignore, file_or_dir, aggressive):
185-
"""Fix basic linter/formatting errors in file(s)/directory(s) given.""" # noqa: D4
182+
def fix(obj, extend_ignore: typing.Optional[str], file_or_dir, aggressive):
183+
"""Fix basic linter/formatting errors in file(s)/directory(s) given."""
186184
_fix.fix(
187185
exclude=obj["EXCLUDE"],
188186
app_import_names=obj["APP_IMPORT_NAMES"],
189187
extend_ignore=extend_ignore,
190188
file_or_dir=file_or_dir or [pathlib.Path.cwd()],
191189
aggressive=aggressive,
192190
)
191+
192+
193+
@main.command()
194+
@click.option("--diff", is_flag=True, help="Show a diff of the changes that would be made")
195+
@click.option("--check", is_flag=True, help="Error if files would be changed")
196+
@click.argument("file_or_dir", nargs=-1)
197+
@click.pass_obj
198+
def format(obj, file_or_dir, check: bool, diff: bool):
199+
"""Format the file(s)/directory(s) given."""
200+
_fix.fix(
201+
exclude=obj["EXCLUDE"],
202+
app_import_names=obj["APP_IMPORT_NAMES"],
203+
extend_ignore=None,
204+
file_or_dir=file_or_dir or [pathlib.Path.cwd()],
205+
aggressive=False,
206+
check=check,
207+
diff=diff,
208+
)

ni_python_styleguide/_fix.py

Lines changed: 56 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import logging
22
import pathlib
3+
import typing
34
from collections import defaultdict
45
from typing import Iterable
56

7+
import better_diff.unified_plus
68
import isort
79
import pathspec
810

@@ -12,59 +14,10 @@
1214
_format,
1315
_utils,
1416
)
17+
from ni_python_styleguide._utils import temp_file
1518

1619
_module_logger = logging.getLogger(__name__)
17-
18-
19-
def _split_imports_line(lines: str, *_, **__):
20-
r"""Splits multi-import lines to multiple lines.
21-
22-
>>> _split_imports_line("import os, collections\n")
23-
'import os\nimport collections\n'
24-
25-
>>> _split_imports_line("import os\n")
26-
'import os\n'
27-
28-
>>> _split_imports_line("from ni_python_styleguide import"
29-
... " _acknowledge_existing_errors, _format")
30-
'from ni_python_styleguide import _acknowledge_existing_errors\nfrom ni_python_styleguide import _format\n'
31-
32-
>>> _split_imports_line("from ni_python_styleguide import _acknowledge_existing_errors")
33-
'from ni_python_styleguide import _acknowledge_existing_errors\n'
34-
35-
>>> _split_imports_line("import os, collections\nimport pathlib")
36-
'import os\nimport collections\nimport pathlib\n'
37-
38-
>>> _split_imports_line("import os, collections\nimport pathlib, os")
39-
'import os\nimport collections\nimport pathlib\nimport os\n'
40-
41-
>>> _split_imports_line("\n")
42-
'\n'
43-
""" # noqa W505: long lines...
44-
result_parts = []
45-
for line in lines.splitlines(keepends=True):
46-
code_portion_of_line, *non_code = line.split("#", maxsplit=1)
47-
first, _, rest = code_portion_of_line.partition(",")
48-
if not all(
49-
[
50-
rest,
51-
"import " in code_portion_of_line,
52-
code_portion_of_line.strip().startswith("import ")
53-
or code_portion_of_line.strip().startswith("from "),
54-
]
55-
):
56-
result_parts.append(code_portion_of_line)
57-
continue
58-
prefix, first = " ".join(first.split()[:-1]), first.split()[-1]
59-
split_up = [first] + rest.split(",")
60-
result_parts.extend([prefix + " " + part.strip() for part in split_up])
61-
suffix = ""
62-
if non_code:
63-
suffix = "#" + "".join(non_code)
64-
result = "\n".join(result_parts) + suffix
65-
if result.strip():
66-
return result.rstrip() + "\n"
67-
return result
20+
_module_logger.addHandler(logging.NullHandler())
6821

6922

7023
def _sort_imports(file: pathlib.Path, app_import_names):
@@ -82,27 +35,38 @@ def _sort_imports(file: pathlib.Path, app_import_names):
8235

8336
def _format_imports(file: pathlib.Path, app_import_names: Iterable[str]) -> None:
8437
_sort_imports(file, app_import_names=app_import_names)
85-
_format.format(file)
38+
_format.format(file, "-q")
39+
40+
41+
def _posix_relative_if_under(file: pathlib.Path, base: pathlib.Path) -> str:
42+
file_resolved = file.resolve()
43+
base_resolved = base.resolve()
44+
if file_resolved.as_posix().startswith(base_resolved.as_posix()):
45+
return file_resolved.relative_to(base_resolved).as_posix()
46+
return file_resolved.as_posix()
8647

8748

8849
def fix(
8950
exclude: str,
9051
app_import_names: str,
91-
extend_ignore,
52+
extend_ignore: typing.Optional[str],
9253
file_or_dir,
9354
*_,
9455
aggressive=False,
56+
diff=False,
57+
check=False,
9558
):
9659
"""Fix basic linter errors and format."""
9760
file_or_dir = file_or_dir or ["."]
98-
extend_ignore = extend_ignore or ""
99-
exclude = exclude or ""
61+
if diff or check:
62+
if aggressive:
63+
raise Exception("Cannot use --aggressive with --diff or --check")
10064
if aggressive:
10165
glob_spec = pathspec.PathSpec.from_lines(
10266
"gitwildmatch",
10367
["*.py"]
10468
+ [f"!{exclude_}" for exclude_ in exclude.split(",") if exclude_]
105-
+ [f"!{ignore_}" for ignore_ in extend_ignore.split(",") if ignore_],
69+
+ [f"!{ignore_}" for ignore_ in (extend_ignore or "").split(",") if ignore_],
10670
)
10771
all_files = []
10872
for file_or_dir_ in file_or_dir:
@@ -130,25 +94,45 @@ def fix(
13094
lint_errors_by_file[pathlib.Path(error.file)].append(error)
13195

13296
failed_files = []
97+
make_changes = not (diff or check)
13398
for bad_file, errors_in_file in lint_errors_by_file.items():
13499
try:
135-
_format.format(bad_file)
136-
_format_imports(file=bad_file, app_import_names=app_import_names)
137-
remaining_lint_errors_in_file = _utils.lint.get_errors_to_process(
138-
exclude,
139-
app_import_names,
140-
extend_ignore,
141-
[bad_file],
142-
excluded_errors=[],
143-
)
144-
if remaining_lint_errors_in_file and aggressive:
145-
_acknowledge_existing_errors.acknowledge_lint_errors(
146-
exclude=exclude,
147-
app_import_names=app_import_names,
148-
extend_ignore=extend_ignore,
149-
aggressive=aggressive,
150-
file_or_dir=[bad_file],
100+
if make_changes:
101+
_format.format(bad_file, "-q")
102+
_format_imports(file=bad_file, app_import_names=app_import_names)
103+
remaining_lint_errors_in_file = _utils.lint.get_errors_to_process(
104+
exclude,
105+
app_import_names,
106+
extend_ignore,
107+
[bad_file],
108+
excluded_errors=[],
151109
)
110+
if remaining_lint_errors_in_file and aggressive:
111+
_acknowledge_existing_errors.acknowledge_lint_errors(
112+
exclude=exclude,
113+
app_import_names=app_import_names,
114+
extend_ignore=extend_ignore,
115+
aggressive=aggressive,
116+
file_or_dir=[bad_file],
117+
)
118+
else:
119+
with temp_file.multi_access_tempfile(suffix="__" + bad_file.name) as working_file:
120+
working_file.write_text(bad_file.read_text())
121+
_format.format(working_file, "-q")
122+
_format_imports(file=working_file, app_import_names=app_import_names)
123+
124+
diff_lines = better_diff.unified_plus.format_diff(
125+
bad_file.read_text(),
126+
working_file.read_text(),
127+
fromfile=f"{_posix_relative_if_under(bad_file, pathlib.Path.cwd())}",
128+
tofile=f"{_posix_relative_if_under(bad_file, pathlib.Path.cwd())}_formatted",
129+
)
130+
if diff:
131+
print(diff_lines)
132+
if check and diff_lines:
133+
print("Error: file would be changed:", str(bad_file))
134+
failed_files.append((bad_file, "File would be changed."))
135+
152136
except Exception as e:
153137
failed_files.append((bad_file, e))
154138
if failed_files:

ni_python_styleguide/_lint.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
import contextlib
44
import io
5+
import typing
56

67
import flake8.main.application
78

89
from ni_python_styleguide import _config_constants, _Flake8Error
910

1011

11-
def lint(qs_or_vs, exclude, app_import_names, format, extend_ignore, file_or_dir):
12+
def lint(
13+
qs_or_vs, exclude, app_import_names, format, extend_ignore: typing.Optional[str], file_or_dir
14+
):
1215
"""Run the linter."""
1316
app = flake8.main.application.Application()
1417
args = [
@@ -31,7 +34,9 @@ def lint(qs_or_vs, exclude, app_import_names, format, extend_ignore, file_or_dir
3134

3235
# Note: tried to use functools.wraps
3336
# - but VSCode did not properly identify the wrapped method's signature :(
34-
def get_lint_output(qs_or_vs, exclude, app_import_names, format, extend_ignore, file_or_dir) -> str:
37+
def get_lint_output(
38+
qs_or_vs, exclude, app_import_names, format, extend_ignore: typing.Optional[str], file_or_dir
39+
) -> str:
3540
"Return the output from running the linter."
3641
capture = io.TextIOWrapper(io.BytesIO())
3742
with contextlib.redirect_stdout(capture):

ni_python_styleguide/_utils/lint.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from ni_python_styleguide import _lint
66

77

8-
def get_errors_to_process(exclude, app_import_names, extend_ignore, file_or_dir, excluded_errors):
8+
def get_errors_to_process(
9+
exclude, app_import_names, extend_ignore: typing.Optional[str], file_or_dir, excluded_errors
10+
):
911
"""Get lint errors to process."""
1012
lint_errors = sorted(
1113
_lint.get_lint_output(

ni_python_styleguide/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ line-length = 100
88
[tool.isort]
99
profile = "black"
1010
combine_as_imports = true
11+
force_alphabetical_sort_within_sections = true

poetry.lock

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pycodestyle = [
2626
{version = "^2.11", python = "^3.12"},
2727
]
2828
black = ">=23.1"
29+
better-diff = "^0.1.3"
2930

3031
# Additional support libraries
3132
# These dependencies were selected because they are already used by black.

tests/test_cli/format_test_cases__snapshots/blank_file/input.py

Whitespace-only changes.

0 commit comments

Comments
 (0)