diff --git a/changelog/+m.fixed.md b/changelog/+m.fixed.md new file mode 100644 index 0000000..c52e3af --- /dev/null +++ b/changelog/+m.fixed.md @@ -0,0 +1 @@ +Made merge conflict pre-commit hook always run diff --git a/changelog/+n.changed.md b/changelog/+n.changed.md new file mode 100644 index 0000000..d24813c --- /dev/null +++ b/changelog/+n.changed.md @@ -0,0 +1 @@ +Changed nox pre-commit hook to local hook, added support for recent nox versions diff --git a/changelog/+p.changed.md b/changelog/+p.changed.md new file mode 100644 index 0000000..dc7f12c --- /dev/null +++ b/changelog/+p.changed.md @@ -0,0 +1 @@ +Updated pre-commit hook versions diff --git a/changelog/+pinpylint.changed.md b/changelog/+pinpylint.changed.md new file mode 100644 index 0000000..ae9ff99 --- /dev/null +++ b/changelog/+pinpylint.changed.md @@ -0,0 +1 @@ +Pinned pylint version used for linting diff --git a/changelog/+pylintconf.changed.md b/changelog/+pylintconf.changed.md new file mode 100644 index 0000000..940a076 --- /dev/null +++ b/changelog/+pylintconf.changed.md @@ -0,0 +1 @@ +Updated pylint configuration diff --git a/changelog/+pylintpy.fixed.md b/changelog/+pylintpy.fixed.md new file mode 100644 index 0000000..b53e056 --- /dev/null +++ b/changelog/+pylintpy.fixed.md @@ -0,0 +1 @@ +Ensured pylint lints against the minimum required Python version diff --git a/changelog/+relaxpylint.added.md b/changelog/+relaxpylint.added.md new file mode 100644 index 0000000..c834a50 --- /dev/null +++ b/changelog/+relaxpylint.added.md @@ -0,0 +1 @@ +Added `relax_pylint` question to suppress some annoying messages, especially with legacy code diff --git a/changelog/+saltpylint.removed.md b/changelog/+saltpylint.removed.md new file mode 100644 index 0000000..9625563 --- /dev/null +++ b/changelog/+saltpylint.removed.md @@ -0,0 +1 @@ +Removed unused saltpylint dependency diff --git a/changelog/+sess.removed.md b/changelog/+sess.removed.md new file mode 100644 index 0000000..255033b --- /dev/null +++ b/changelog/+sess.removed.md @@ -0,0 +1 @@ +Removed unnecessary `docs-html` and `gen-api-docs` nox sessions diff --git a/changelog/+strictpylint.changed.md b/changelog/+strictpylint.changed.md new file mode 100644 index 0000000..0f58749 --- /dev/null +++ b/changelog/+strictpylint.changed.md @@ -0,0 +1 @@ +Increased default pylint strictness diff --git a/changelog/+uv.changed.md b/changelog/+uv.changed.md new file mode 100644 index 0000000..3e15828 --- /dev/null +++ b/changelog/+uv.changed.md @@ -0,0 +1 @@ +Switched nox venv backend to uv, which reduced the time for pre-commit linting and other nox sessions significantly diff --git a/copier.yml b/copier.yml index b9da989..a541728 100644 --- a/copier.yml +++ b/copier.yml @@ -248,6 +248,11 @@ copyright_begin: # Cannot use when: false to set this once automatically during # creation since the value is not recorded then. +relax_pylint: + type: bool + help: Suppress some Pylint messages that can cause noise or be difficult to solve with legacy code. + default: false + # =========================================== # | Computed values for less ugly templates | # =========================================== diff --git a/docs/ref/questions.md b/docs/ref/questions.md index d8371cf..3c9a5ef 100644 --- a/docs/ref/questions.md +++ b/docs/ref/questions.md @@ -198,3 +198,15 @@ A contact email for Code of Conduct complaints. The starting year of the copyright range. **Example**: `2024` + +:::{question} relax_pylint +::: +## `relax_pylint` +Suppress some Pylint messages that can cause noise (`consider-using-f-string`) +or be time-intense (`too-many-*`) or difficult +(`redefined-builtin`, `redefined-outer-name`) to solve with legacy code. + +Additionally suppresses `unused-argument` in the test suite, often caused +by requesting fixtures in the function signature instead of using +`@pytest.mark.usefixtures`. Note that the decorator does not work on +fixtures requesting other fixtures. diff --git a/project/.pre-commit-config.yaml.j2 b/project/.pre-commit-config.yaml.j2 index 0130883..0560f5d 100755 --- a/project/.pre-commit-config.yaml.j2 +++ b/project/.pre-commit-config.yaml.j2 @@ -4,13 +4,14 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - - id: check-merge-conflict # Check for files that contain merge conflict strings. - - id: trailing-whitespace # Trims trailing whitespace. + - id: check-merge-conflict # Check for files that contain merge conflict strings. + args: [--assume-in-merge] + - id: trailing-whitespace # Trim trailing whitespace. args: [--markdown-linebreak-ext=md] - - id: mixed-line-ending # Replaces or checks mixed line ending. + - id: mixed-line-ending # Ensure files use UNIX-style newlines only. args: [--fix=lf] - - id: end-of-file-fixer # Makes sure files end in a newline and only a newline. - - id: check-ast # Simply check whether files parse as valid python. + - id: end-of-file-fixer # Ensure files end with a newline. + - id: check-ast # Check whether files parse as valid Python. # ----- Formatting ----------------------------------------------------------------------------> - repo: https://github.com/saltstack/pre-commit-remove-import-headers @@ -23,7 +24,7 @@ repos: - id: check-cli-examples name: Check CLI examples on execution modules entry: python .pre-commit-hooks/check-cli-examples.py - language: system + language: python files: ^src/{{ namespaced_package_path }}/modules/.*\.py$ - repo: local @@ -31,7 +32,7 @@ repos: - id: check-docs name: Check rST doc files exist for modules/states entry: python .pre-commit-hooks/make-autodocs.py - language: system + language: python pass_filenames: false - repo: https://github.com/s0undt3ch/salt-rewrite @@ -55,7 +56,7 @@ repos: args: [--silent, -E, fix_docstrings] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.16.0 hooks: - id: pyupgrade name: Rewrite Code to be Py{{ python_requires[:2] | join(".") }}+ @@ -74,14 +75,14 @@ repos: exclude: src/{{ namespaced_package_path }}/(__init__|version).py - repo: https://github.com/psf/black - rev: 24.2.0 + rev: 24.8.0 hooks: - id: black args: [-l 100] exclude: src/{{ namespaced_package_path }}/version.py - repo: https://github.com/adamchainz/blacken-docs - rev: 1.16.0 + rev: 1.18.0 hooks: - id: blacken-docs args: [--skip-errors] @@ -92,7 +93,7 @@ repos: # ----- Security ------------------------------------------------------------------------------> - repo: https://github.com/PyCQA/bandit - rev: 1.7.8 + rev: 1.7.9 hooks: - id: bandit alias: bandit-salt @@ -100,7 +101,7 @@ repos: args: [--silent, -lll, --skip, B701] exclude: src/{{ namespaced_package_path }}/version.py - repo: https://github.com/PyCQA/bandit - rev: 1.7.8 + rev: 1.7.9 hooks: - id: bandit alias: bandit-tests @@ -110,31 +111,30 @@ repos: # <---- Security ------------------------------------------------------------------------------- # ----- Code Analysis -------------------------------------------------------------------------> - - repo: https://github.com/saltstack/mirrors-nox - rev: v2022.11.21 + + - repo: local hooks: - id: nox alias: lint-src name: Lint Source Code + language: python + entry: nox -e lint-code-pre-commit -- files: ^((setup|noxfile)|src/.*)\.py$ require_serial: true - args: - - -e - - lint-code-pre-commit - - -- + additional_dependencies: + - nox==2024.4.15 + - uv==0.4.0 # Makes this hook much faster - - repo: https://github.com/saltstack/mirrors-nox - rev: v2022.11.21 - hooks: - id: nox alias: lint-tests name: Lint Tests + language: python + entry: nox -e lint-tests-pre-commit -- files: ^tests/.*\.py$ require_serial: true - args: - - -e - - lint-tests-pre-commit - - -- + additional_dependencies: + - nox==2024.4.15 + - uv==0.4.0 # Makes this hook much faster {%- if "github.com" in source_url %} diff --git a/project/.pylintrc b/project/.pylintrc.j2 similarity index 88% rename from project/.pylintrc rename to project/.pylintrc.j2 index 5692f3b..40ad27d 100755 --- a/project/.pylintrc +++ b/project/.pylintrc.j2 @@ -1,3 +1,39 @@ +{%- set disable = [ + "duplicate-code", + "fixme", + "line-too-long", + "logging-fstring-interpolation", + "missing-class-docstring", + "missing-function-docstring", + "missing-module-docstring", + "protected-access", + "too-few-public-methods", + "ungrouped-imports", + "wrong-import-position", +] -%} + +{%- if relax_pylint -%} +{%- do disable.extend( + [ + "consider-using-f-string", + "inconsistent-return-statements", + "no-else-return", + "no-member", + "redefined-argument-from-local", + "redefined-builtin", + "redefined-outer-name", + "too-many-boolean-expressions", + "too-many-branches", + "too-many-instance-attributes", + "too-many-lines", + "too-many-locals", + "too-many-nested-blocks", + "too-many-return-statements", + "too-many-statements", + ] +) -%} +{%- endif -%} + [MAIN] # Analyse import fallback blocks. This can be used to support both Python 2 and @@ -39,7 +75,7 @@ extension-pkg-whitelist= fail-on= # Specify a score threshold under which the program will exit with error. -fail-under=10 +fail-under=10.0 # Interpret the stdin as a python script, whose filename needs to be passed as # the module_or_package argument. @@ -59,10 +95,11 @@ ignore-paths= # Emacs file locks ignore-patterns=^\.# -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. +# List of module names for which member attributes should not be checked and +# will not be imported (useful for modules/projects where namespaces are +# manipulated during runtime and thus existing member attributes cannot be +# deduced by static analysis). It supports qualified module names, as well as +# Unix pattern matching. ignored-modules= # Python code to execute, usually for sys.path manipulation such as @@ -86,9 +123,13 @@ load-plugins= # Pickle collected data for later comparisons. persistent=yes +# Resolve imports to .pyi stubs if available. May reduce no-member messages and +# increase not-an-iterable messages. +prefer-stubs=no + # Minimum Python version to use for version dependent checks. Will default to # the version used to run pylint. -py-version=3.10 +py-version={{ python_requires | join(".") }} # Discover python modules and packages in the file system subtree. recursive=no @@ -285,7 +326,7 @@ exclude-too-few-public-methods= ignored-parents= # Maximum number of arguments for function / method. -max-args=15 +max-args=35 # Maximum number of attributes for a class (see R0902). max-attributes=7 @@ -294,10 +335,10 @@ max-attributes=7 max-bool-expr=5 # Maximum number of branch for function / method body. -max-branches=12 +max-branches=48 # Maximum number of locals for function / method body. -max-locals=15 +max-locals=40 # Maximum number of parents for a class (see R0901). max-parents=7 @@ -309,7 +350,7 @@ max-public-methods=25 max-returns=6 # Maximum number of statements in function / method body. -max-statements=50 +max-statements=100 # Minimum number of public methods for a class (see R0903). min-public-methods=2 @@ -324,7 +365,7 @@ overgeneral-exceptions=builtins.BaseException,builtins.Exception [FORMAT] # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= +expected-line-ending-format=LF # Regexp for a line that is allowed to be longer than the limit. ignore-long-lines=^\s*(# )??$ @@ -337,10 +378,10 @@ indent-after-paren=4 indent-string=' ' # Maximum number of characters on a single line. -max-line-length=100 +max-line-length=120 # Maximum number of lines in a module. -max-module-lines=2000 +max-module-lines=3000 # Allow the body of a class to be on the same line as the declaration if body # contains single statement. @@ -421,43 +462,7 @@ confidence=HIGH, # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=R, - locally-disabled, - file-ignored, - unexpected-special-method-signature, - import-error, - no-member, - unsubscriptable-object, - blacklisted-name, - invalid-name, - missing-docstring, - empty-docstring, - unidiomatic-typecheck, - wrong-import-order, - ungrouped-imports, - wrong-import-position, - bad-mcs-method-argument, - bad-mcs-classmethod-argument, - line-too-long, - too-many-lines, - bad-continuation, - exec-used, - attribute-defined-outside-init, - protected-access, - reimported, - fixme, - global-statement, - unused-variable, - unused-argument, - redefined-outer-name, - redefined-builtin, - undefined-loop-variable, - logging-format-interpolation, - invalid-format-index, - line-too-long, - import-outside-toplevel, - deprecated-method, - keyword-arg-before-vararg, +disable={{ disable | sort | unique | join(",\n" + " " * 8) }} # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -495,6 +500,11 @@ max-nested-blocks=5 # printed. never-returning-functions=sys.exit,argparse.parse_error +# Let 'consider-using-join' be raised when the separator to join on would be +# non-empty (resulting in expected fixes of the type: ``"- " + " - +# ".join(items)``) +suggest-join-with-non-empty-separator=yes + [REPORTS] @@ -509,8 +519,9 @@ evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor # used to format the message information. See doc for all details. msg-template= -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. +# Set the output format. Available formats are: text, parseable, colorized, +# json2 (improved json format), json (old json format) and msvs (visual +# studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. #output-format= @@ -544,8 +555,8 @@ min-similarity-lines=4 # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 -# Spelling dictionary name. No available dictionaries : You need to install the -# system dependency for enchant to work.. +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work. spelling-dict= # List of comma separated words that should be considered directives if they @@ -633,27 +644,27 @@ signature-mutators= # List of additional names supposed to be defined in builtins. Remember that # you should avoid defining new builtins when possible. additional-builtins=__opts__, - __salt__, - __pillar__, - __grains__, - __context__, - __runner__, - __ret__, - __env__, - __low__, - __states__, - __lowstate__, - __running__, - __active_provider_name__, - __master_opts__, - __jid_event__, - __instance_id__, - __salt_system_encoding__, - __proxy__, - __serializers__, - __reg__, - __executors__, - __events__ + __salt__, + __pillar__, + __grains__, + __context__, + __runner__, + __ret__, + __env__, + __low__, + __states__, + __lowstate__, + __running__, + __active_provider_name__, + __master_opts__, + __jid_event__, + __instance_id__, + __salt_system_encoding__, + __proxy__, + __serializers__, + __reg__, + __executors__, + __events__ # Tells whether unused global variables should be treated as a violation. allow-global-unused-variables=yes diff --git a/project/noxfile.py.j2 b/project/noxfile.py.j2 index b197f7b..be673fa 100755 --- a/project/noxfile.py.j2 +++ b/project/noxfile.py.j2 @@ -1,10 +1,34 @@ -# pylint: disable=missing-module-docstring,import-error,protected-access,missing-function-docstring +{%- set pylint_tests_disable = [ + "I", + "redefined-outer-name", + "no-member", + "missing-module-docstring", + "missing-function-docstring", + "missing-class-docstring", + "attribute-defined-outside-init", + "inconsistent-return-statements", + "too-few-public-methods", +] -%} + +{%- if relax_pylint -%} +{#- + Fixtures whose values are not used should be requested via `@pytest.mark.usefixtures`. + This decorator does not work on fixture functions though. +-#} +{%- do pylint_tests_disable.extend( + [ + "unused-argument", + ] +) -%} +{%- endif -%} + import datetime import json import os import shutil import sys import tempfile +from importlib import metadata from pathlib import Path import nox @@ -16,6 +40,9 @@ from nox.virtualenv import VirtualEnv nox.options.reuse_existing_virtualenvs = True # Don't fail on missing interpreters nox.options.error_on_missing_interpreters = False +# Speed up all sessions by using uv if possible +if tuple(map(int, metadata.version("nox").split("."))) >= (2024, 3): + nox.options.default_venv_backend = "uv|virtualenv" # Python versions to test against PYTHON_VERSIONS = ("3" {%- for i in range(python_requires[1], max_python_minor + 1) %}, "3.{{ i }}"{%- endfor %}) @@ -84,14 +111,17 @@ def _install_requirements( install_extras=None, ): install_extras = install_extras or [] + no_progress = "--progress-bar=off" + if isinstance(session._runner.venv, VirtualEnv) and session._runner.venv.venv_backend == "uv": + no_progress = "--no-progress" if SKIP_REQUIREMENTS_INSTALL is False: # Always have the wheel package installed - session.install("--progress-bar=off", "wheel", silent=PIP_INSTALL_SILENT) + session.install(no_progress, "wheel", silent=PIP_INSTALL_SILENT) if install_coverage_requirements: - session.install("--progress-bar=off", COVERAGE_REQUIREMENT, silent=PIP_INSTALL_SILENT) + session.install(no_progress, COVERAGE_REQUIREMENT, silent=PIP_INSTALL_SILENT) if install_salt: - session.install("--progress-bar=off", SALT_REQUIREMENT, silent=PIP_INSTALL_SILENT) + session.install(no_progress, SALT_REQUIREMENT, silent=PIP_INSTALL_SILENT) if install_test_requirements: install_extras.append("tests") @@ -103,7 +133,7 @@ def _install_requirements( "EXTRA_REQUIREMENTS_INSTALL='%s'", EXTRA_REQUIREMENTS_INSTALL, ) - install_command = ["--progress-bar=off"] + install_command = [no_progress] install_command += [req.strip() for req in EXTRA_REQUIREMENTS_INSTALL.split()] session.install(*install_command, silent=PIP_INSTALL_SILENT) @@ -256,7 +286,7 @@ def _lint(session, rcfile, flags, paths, tee_output=True): install_salt=False, install_coverage_requirements=False, install_test_requirements=False, - install_extras=["dev", "tests"], + install_extras=["lint", "tests"], ) if tee_output: @@ -319,12 +349,25 @@ def _lint_pre_commit(session, rcfile, flags, paths): ) # Let's patch nox to make it run inside the pre-commit virtualenv - session._runner.venv = VirtualEnv( - os.environ["VIRTUAL_ENV"], - interpreter=session._runner.func.python, - reuse_existing=True, - venv=True, - ) + try: + # nox >= 2024.03.02 + # pylint: disable=unexpected-keyword-arg + venv = VirtualEnv( + os.environ["VIRTUAL_ENV"], + interpreter=session._runner.func.python, + reuse_existing=True, + venv_backend="venv", + ) + except TypeError: + # nox < 2024.03.02 + # pylint: disable=unexpected-keyword-arg + venv = VirtualEnv( + os.environ["VIRTUAL_ENV"], + interpreter=session._runner.func.python, + reuse_existing=True, + venv=True, + ) + session._runner.venv = venv _lint(session, rcfile, flags, paths, tee_output=False) @@ -356,7 +399,7 @@ def lint_tests(session): Run PyLint against the test suite. Set PYLINT_REPORT to a path to capture output. """ flags = [ - "--disable=I,redefined-outer-name,missing-function-docstring,no-member,missing-module-docstring" + "--disable={{ pylint_tests_disable | join(",") }}", ] if session.posargs: paths = session.posargs @@ -384,7 +427,7 @@ def lint_tests_pre_commit(session): Run PyLint against the code and the test suite. Set PYLINT_REPORT to a path to capture output. """ flags = [ - "--disable=I,redefined-outer-name,missing-function-docstring,no-member,missing-module-docstring", + "--disable={{ pylint_tests_disable | join(",") }}", ] if session.posargs: paths = session.posargs @@ -419,34 +462,6 @@ def docs(session): os.chdir(str(REPO_ROOT)) -@nox.session(name="docs-html", python="3") -@nox.parametrize("clean", [False, True]) -@nox.parametrize("include_api_docs", [False, True]) -def docs_html(session, clean, include_api_docs): - """ - Build Sphinx HTML Documentation - - TODO: Add option for `make linkcheck` and `make coverage` - calls via Sphinx. Ran into problems with two when - using Furo theme and latest Sphinx. - """ - _install_requirements( - session, - install_coverage_requirements=False, - install_test_requirements=False, - install_source=True, - install_extras=["docs"], - ) - if include_api_docs: - gen_api_docs(session) - build_dir = Path("docs", "_build", "html") - sphinxopts = "-Wn" - if clean: - sphinxopts += "E" - args = [sphinxopts, "--keep-going", "docs", str(build_dir)] - session.run("sphinx-build", *args, external=True) - - @nox.session(name="docs-dev", python="3") def docs_dev(session): """ @@ -515,30 +530,3 @@ def docs_crosslink_info(session): "python", "-m", "sphinx.ext.intersphinx", mapping_entry[0].rstrip("/") + "/objects.inv" ) os.chdir(str(REPO_ROOT)) - - -@nox.session(name="gen-api-docs", python="3") -def gen_api_docs(session): - """ - Generate API Docs - """ - _install_requirements( - session, - install_coverage_requirements=False, - install_test_requirements=False, - install_source=True, - install_extras=["docs"], - ) - try: - shutil.rmtree("docs/ref") - except FileNotFoundError: - pass - session.run( - "sphinx-apidoc", - "--implicit-namespaces", - "--module-first", - "-o", - "docs/ref/", - "src/{{ package_namespace }}", - "src/{{ namespaced_package_path }}/config/schemas", - ) diff --git a/project/pyproject.toml.j2 b/project/pyproject.toml.j2 index 693c627..6e98f7a 100644 --- a/project/pyproject.toml.j2 +++ b/project/pyproject.toml.j2 @@ -59,10 +59,8 @@ Tracker = "{{ tracker_url }}" [project.optional-dependencies] changelog = ["towncrier==22.12.0"] dev = [ - "nox", + "nox[uv]>=2024.3", "pre-commit>=2.4.0", - "pylint", - "saltpylint", ] docs = [ "sphinx", @@ -77,8 +75,7 @@ docs = [ ] docsauto = ["sphinx-autobuild"] lint = [ - "pylint", - "saltpylint", + "pylint==3.2.6", ] tests = [ "pytest>=7.2.0", diff --git a/project/src/{{ namespaced_package_path }}/{% if 'sdb' in loaders %}sdb{% endif %}/{{ package_name }}_mod.py.j2 b/project/src/{{ namespaced_package_path }}/{% if 'sdb' in loaders %}sdb{% endif %}/{{ package_name }}_mod.py.j2 index 4ade8af..aa6a35f 100644 --- a/project/src/{{ namespaced_package_path }}/{% if 'sdb' in loaders %}sdb{% endif %}/{{ package_name }}_mod.py.j2 +++ b/project/src/{{ namespaced_package_path }}/{% if 'sdb' in loaders %}sdb{% endif %}/{{ package_name }}_mod.py.j2 @@ -29,4 +29,5 @@ def get(key, profile=None): salt '*' sdb.get "sdb://{{ package_name }}/foo" """ - return key + profile = profile or {} + return profile.get(key, key) diff --git a/tests/helpers/pre_commit.py b/tests/helpers/pre_commit.py new file mode 100644 index 0000000..409696f --- /dev/null +++ b/tests/helpers/pre_commit.py @@ -0,0 +1,51 @@ +import re + +PRE_COMMIT_TEST_REGEX = re.compile( + r"^(?P[^\n]+?)\.{4,}.*(?PFailed|Passed|Skipped)$" +) + +NON_IDEMPOTENT_HOOKS = ( + "trim trailing whitespace", + "mixed line ending", + "fix end of files", + "Remove Python Import Header Comments", + "Check rST doc files exist for modules/states", + "Salt extensions docstrings auto-fixes", + "Rewrite the test suite", + "Rewrite Code to be Py3.", + "isort", + "black", + "blacken-docs", +) + + +def parse_pre_commit(data): + passing = [] + failing = {} + cur = None + for line in data.splitlines(): + if match := PRE_COMMIT_TEST_REGEX.match(line): + cur = None + if match.group("resolution") != "Failed": + passing.append(match.group("test")) + continue + cur = match.group("test") + failing[cur] = [] + continue + try: + failing[cur].append(line) + except KeyError: + # in case the parsing logic fails, let's not crash everything + continue + return passing, {test: "\n".join(output).strip() for test, output in failing.items()} + + +def check_pre_commit_rerun(data): + """ + Check if we can expect failing hooks to turn green during a rerun. + """ + _, failing = parse_pre_commit(data) + for hook in failing: + if hook.startswith(NON_IDEMPOTENT_HOOKS): + return True + return False diff --git a/tests/test_basics.py b/tests/test_basics.py index 9d00905..9eac499 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -2,6 +2,8 @@ from plumbum import ProcessExecutionError from plumbum import local +from tests.helpers.pre_commit import check_pre_commit_rerun +from tests.helpers.pre_commit import parse_pre_commit from tests.helpers.venv import ProjectVenv pytestmark = [ @@ -57,8 +59,14 @@ def _commit_with_pre_commit(venv, max_retry=3, message="initial commit"): except ProcessExecutionError as err: retry_count += 1 saved_err = err + if not check_pre_commit_rerun(err.stderr): + retry_count = max_retry + 1 else: - raise saved_err + passing, failing = parse_pre_commit(saved_err.stderr) + msg = f"pre-commit failure\nPassing: {', '.join(passing)}\nFailing: {', '.join(failing)}" + for hook, out in failing.items(): + msg += f"\n\n{hook}:\n{out}" + raise AssertionError(msg) # We need to test both org and enhanced workflows (with actionlint/shellcheck) @@ -94,7 +102,12 @@ def test_testsuite_works(project, project_venv): @pytest.mark.parametrize("no_saltext_namespace", (False, True), indirect=True) def test_docs_build_works(project, project_venv): with ProjectVenv(project) as venv, local.cwd(project): - _commit_with_pre_commit(venv, max_retry=3) + for check in (False, True): + venv.run( + venv.venv_python, + str(project / ".pre-commit-hooks" / "make-autodocs.py"), + check=check, + ) res = project_venv.run( str(project_venv.venv_python), "-m", "nox", "-e", "docs", check=False )