diff --git a/ni_python_styleguide/__init__.py b/ni_python_styleguide/__init__.py index 6a2d0ae5..b915f873 100644 --- a/ni_python_styleguide/__init__.py +++ b/ni_python_styleguide/__init__.py @@ -1 +1,7 @@ """NI's internal and external style rules enforcement tool for Python.""" + + +class _Flake8Error(Exception): + def __init__(self, *args: object) -> None: + super().__init__(*args) + self.code = args[0] diff --git a/ni_python_styleguide/_cli.py b/ni_python_styleguide/_cli.py index f36ffc7b..d2202933 100644 --- a/ni_python_styleguide/_cli.py +++ b/ni_python_styleguide/_cli.py @@ -1,10 +1,12 @@ import pathlib +import sys import click import toml from ni_python_styleguide import _acknowledge_existing_errors from ni_python_styleguide import _fix +from ni_python_styleguide import _Flake8Error from ni_python_styleguide import _lint @@ -126,14 +128,17 @@ def main(ctx, verbose, quiet, config, exclude, extend_exclude): @click.pass_obj def lint(obj, format, extend_ignore, file_or_dir): """Lint the file(s)/directory(s) given.""" # noqa: D4 - _lint.lint( - qs_or_vs=_qs_or_vs(obj["VERBOSITY"]), - exclude=obj["EXCLUDE"], - app_import_names=obj["APP_IMPORT_NAMES"], - format=format, - extend_ignore=extend_ignore, - file_or_dir=file_or_dir, - ) + try: + _lint.lint( + qs_or_vs=_qs_or_vs(obj["VERBOSITY"]), + exclude=obj["EXCLUDE"], + app_import_names=obj["APP_IMPORT_NAMES"], + format=format, + extend_ignore=extend_ignore, + file_or_dir=file_or_dir, + ) + except _Flake8Error: + sys.exit(-1) # exit without additional output @main.command() diff --git a/ni_python_styleguide/_lint.py b/ni_python_styleguide/_lint.py index e720a073..47f1d1cf 100644 --- a/ni_python_styleguide/_lint.py +++ b/ni_python_styleguide/_lint.py @@ -1,10 +1,11 @@ """Linting methods.""" import contextlib -from io import StringIO +import io import flake8.main.application from ni_python_styleguide import _config_constants +from ni_python_styleguide import _Flake8Error def lint(qs_or_vs, exclude, app_import_names, format, extend_ignore, file_or_dir): @@ -24,20 +25,19 @@ def lint(qs_or_vs, exclude, app_import_names, format, extend_ignore, file_or_dir *[str(p) for p in file_or_dir], ] app.run(list(filter(bool, args))) - app.exit() + if app.exit_code() != 0: + raise _Flake8Error(app.exit_code()) # Note: tried to use functools.wraps # - but VSCode did not properly identify the wrapped method's signature :( def get_lint_output(qs_or_vs, exclude, app_import_names, format, extend_ignore, file_or_dir) -> str: "Return the output from running the linter." - capture = StringIO() + capture = io.TextIOWrapper(io.BytesIO()) with contextlib.redirect_stdout(capture): try: lint(qs_or_vs, exclude, app_import_names, format, extend_ignore, file_or_dir) - except SystemExit as e: - if e.code in (True, 0): - pass # the flake8 app wants to always SystemExit :( - else: - raise - return capture.getvalue() + except _Flake8Error: + pass + capture.seek(0) + return capture.read() diff --git a/ni_python_styleguide/_utils/lint.py b/ni_python_styleguide/_utils/lint.py index 55b334cb..f53f47ac 100644 --- a/ni_python_styleguide/_utils/lint.py +++ b/ni_python_styleguide/_utils/lint.py @@ -7,14 +7,16 @@ def get_errors_to_process(exclude, app_import_names, extend_ignore, file_or_dir, excluded_errors): """Get lint errors to process.""" - lint_errors = _lint.get_lint_output( - format=None, - qs_or_vs=None, - exclude=exclude, - app_import_names=app_import_names, - extend_ignore=extend_ignore, - file_or_dir=file_or_dir, - ).splitlines() + lint_errors = sorted( + _lint.get_lint_output( + format=None, + qs_or_vs=None, + exclude=exclude, + app_import_names=app_import_names, + extend_ignore=extend_ignore, + file_or_dir=file_or_dir, + ).splitlines() + ) parsed_errors = map(parse, lint_errors) parsed_errors = list(filter(None, parsed_errors)) lint_errors_to_process = [error for error in parsed_errors if error.code not in excluded_errors] diff --git a/poetry.lock b/poetry.lock index 1fd6e7a5..6fbc5d2f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -68,17 +68,17 @@ test = ["pytest (>=6)"] [[package]] name = "flake8" -version = "3.9.2" +version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6.1" [package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" +importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" [[package]] name = "flake8-black" @@ -121,20 +121,19 @@ pycodestyle = "*" [[package]] name = "importlib-metadata" -version = "4.13.0" +version = "4.2.0" description = "Read metadata from Python packages" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.6" [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] [[package]] name = "iniconfig" @@ -160,11 +159,11 @@ requirements_deprecated_finder = ["pip-api", "pipreqs"] [[package]] name = "mccabe" -version = "0.6.1" +version = "0.7.0" description = "McCabe checker, plugin for flake8" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "mypy-extensions" @@ -230,11 +229,11 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pycodestyle" -version = "2.7.0" +version = "2.9.1" description = "Python style guide checker" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [[package]] name = "pydocstyle" @@ -252,11 +251,11 @@ toml = ["toml"] [[package]] name = "pyflakes" -version = "2.3.1" +version = "2.5.0" description = "passive checker of Python programs" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [[package]] name = "pytest" @@ -357,7 +356,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "882b940a473c07ef2abe87b6f916c63e12cae28ccc499cdd5ae22f45c8c96ebc" +content-hash = "bdb0222ad560bc83af4f1bd612b7db35a9201b10c93943d074ed5983299be972" [metadata.files] attrs = [ @@ -402,8 +401,8 @@ exceptiongroup = [ {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, ] flake8 = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, + {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, + {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, ] flake8-black = [ {file = "flake8-black-0.3.5.tar.gz", hash = "sha256:9e93252b1314a8eb3c2f55dec54a07239e502b12f57567f2c105f2202714b15e"}, @@ -418,8 +417,8 @@ flake8-import-order = [ {file = "flake8_import_order-0.18.2-py2.py3-none-any.whl", hash = "sha256:82ed59f1083b629b030ee9d3928d9e06b6213eb196fe745b3a7d4af2168130df"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"}, - {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"}, + {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, + {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -430,8 +429,8 @@ isort = [ {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -458,16 +457,16 @@ pluggy = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pycodestyle = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, + {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, + {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, ] pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, ] pyflakes = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, + {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, + {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, ] pytest = [ {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, diff --git a/pyproject.toml b/pyproject.toml index 25410cde..9685db2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "ni-python-styleguide" # The -alpha.0 here denotes a source based version # This is removed when released through the Publish-Package.yml GitHub action # Official PyPI releases follow Major.Minor.Patch -version = "0.2.0-alpha.0" +version = "0.3.0-alpha.0" description = "NI's internal and external Python linter rules and plugins" authors = ["NI "] readme = "README.md" # apply the repo readme to the package as well @@ -15,7 +15,7 @@ include = ["ni_python_styleguide/config.toml"] python = "^3.7" # Tools we aggregate -flake8 = "^3.8" # flake8 4.x broke the test +flake8 = "^5.0" black = ">=22.3, !=22.10.0" # https://github.com/psf/black/issues/3312 # Additional support libraries @@ -50,14 +50,9 @@ ni-python-styleguide = 'ni_python_styleguide._cli:main' [tool.black] line-length = 100 -exclude = ''' +extend-exclude = ''' ( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.venv - )/ - | /.*__snapshots/.*output\.py # exclude the simple snapshot outputs +/.*__snapshots/.*output\.py # exclude the simple snapshot outputs ) ''' diff --git a/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/function_signature_tests/output.py b/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/function_signature_tests/output.py index e82908ad..a9c2207b 100644 --- a/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/function_signature_tests/output.py +++ b/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/function_signature_tests/output.py @@ -70,7 +70,7 @@ def method_withBadName_with_parameters_on_multiple_lines(x, y): # noqa N802: fu return x + y -def method_withBadName_with_bad_params_on_single_line(myBadlyNamedParam, my_other_Bad_name): # noqa N802: function name 'method_withBadName_with_bad_params_on_single_line' should be lowercase (auto-generated noqa) # noqa N803: argument name 'myBadlyNamedParam' should be lowercase (auto-generated noqa) +def method_withBadName_with_bad_params_on_single_line(myBadlyNamedParam, my_other_Bad_name): # noqa N803: argument name 'myBadlyNamedParam' should be lowercase (auto-generated noqa) # noqa N802: function name 'method_withBadName_with_bad_params_on_single_line' should be lowercase (auto-generated noqa) """Provide parameters with bad names on single line.""" return myBadlyNamedParam + my_other_Bad_name @@ -90,7 +90,7 @@ def method_withBadName_with_bad_params_on_multiple_lines_2( # noqa N802: functi return myBadlyNamedParam + my_other_Bad_name -def method_withBadName_andParams(my_normal_param, myBadlyNamedParam, my_other_Bad_param): # noqa N802: function name 'method_withBadName_andParams' should be lowercase (auto-generated noqa) # noqa N803: argument name 'myBadlyNamedParam' should be lowercase (auto-generated noqa) +def method_withBadName_andParams(my_normal_param, myBadlyNamedParam, my_other_Bad_param): # noqa N803: argument name 'myBadlyNamedParam' should be lowercase (auto-generated noqa) # noqa N802: function name 'method_withBadName_andParams' should be lowercase (auto-generated noqa) """Provide example where black will want to split out result.""" return 5 + 7 diff --git a/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/import_line_tests/output.py b/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/import_line_tests/output.py index 52e065cb..ddfdd6ae 100644 --- a/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/import_line_tests/output.py +++ b/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/import_line_tests/output.py @@ -1,7 +1,7 @@ """example of a python file with linter errors. """ -import pathlib, glob # noqa F401: 'pathlib' imported but unused (auto-generated noqa) # noqa E401: multiple imports on one line (auto-generated noqa) +import pathlib, glob # noqa E401: multiple imports on one line (auto-generated noqa) # noqa F401: 'glob' imported but unused (auto-generated noqa) import os # noqa I100: Import statements are in the wrong order. 'import os' should be before 'import pathlib, glob' (auto-generated noqa) from os import path # noqa F401: 'os.path' imported but unused (auto-generated noqa) from os.path import * # noqa F403: 'from os.path import *' used; unable to detect undefined names (auto-generated noqa) diff --git a/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/import_line_tests/output__aggressive.py b/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/import_line_tests/output__aggressive.py index 11825179..577ab251 100644 --- a/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/import_line_tests/output__aggressive.py +++ b/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/import_line_tests/output__aggressive.py @@ -1,7 +1,7 @@ """example of a python file with linter errors. """ -import pathlib, glob # noqa F401: 'pathlib' imported but unused (auto-generated noqa) # noqa E401: multiple imports on one line (auto-generated noqa) +import pathlib, glob # noqa E401: multiple imports on one line (auto-generated noqa) # noqa F401: 'glob' imported but unused (auto-generated noqa) import os # noqa I100: Import statements are in the wrong order. 'import os' should be before 'import pathlib, glob' (auto-generated noqa) from os import path # noqa F401: 'os.path' imported but unused (auto-generated noqa) from os.path import * # noqa F403: 'from os.path import *' used; unable to detect undefined names (auto-generated noqa) diff --git a/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/unicode_in_files/output.py b/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/unicode_in_files/output.py index 163662e7..76444a4c 100644 --- a/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/unicode_in_files/output.py +++ b/tests/test_cli/acknowledge_existing_errors_test_cases__snapshots/unicode_in_files/output.py @@ -14,6 +14,6 @@ def problem_chars(self): return self._problem_chars -def method_withBadName_andParams(my_normal_param, myBadlyNamedParam, my_other_Bad_param): # noqa N802: function name 'method_withBadName_andParams' should be lowercase (auto-generated noqa) # noqa N803: argument name 'myBadlyNamedParam' should be lowercase (auto-generated noqa) +def method_withBadName_andParams(my_normal_param, myBadlyNamedParam, my_other_Bad_param): # noqa N803: argument name 'myBadlyNamedParam' should be lowercase (auto-generated noqa) # noqa N802: function name 'method_withBadName_andParams' should be lowercase (auto-generated noqa) """Provide example where black will want to split out result.""" return 5 + 7