From e4b2cc99f06411cf1fdca33863bbf784c293a91c Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen <43357585+CasperWA@users.noreply.github.com> Date: Thu, 14 Dec 2023 23:06:59 +0100 Subject: [PATCH] Update to ruff (instead of pylint (and isort)) (#192) Remove pylint as a dependency. Remove all "# pylint: disable=..." statements. Remove pylint and isort as pre-commit hooks. Add ruff as a pre-commit hook. Avoid running pylint in the CI. Add (back) disable E501 statements. E501: Line too long. Add it for either lines with string-formatted type (which has to be on one line) or an f-string with a single variable (i.e., a single-line f-string part with an implemented logic for a variable that results in the line being too long). Add explicit rule set to ruff Extend arguments to ruff CLI, ensuring no unsafe fixes are made and any fixes are shown explicitly in the output. Add pyupgrade pre-commit hook Enforce importing __future__.annotations in all Python files. Base it on scientific-python and then update it to fit this package. --- .github/utils/run_hooks.py | 2 +- .github/workflows/_local_ci_tests.yml | 10 +- .../_local_ci_update_dependencies.yml | 1 - .pre-commit-config.yaml | 64 +++------- ci_cd/tasks/api_reference_docs.py | 9 +- ci_cd/tasks/docs_index.py | 2 +- ci_cd/tasks/setver.py | 18 ++- ci_cd/tasks/update_deps.py | 5 +- ci_cd/utils/console_printing.py | 10 +- ci_cd/utils/versions.py | 110 +++++++++++------- pyproject.toml | 48 ++++++-- tests/conftest.py | 2 +- tests/tasks/test_api_reference_docs.py | 8 +- tests/tasks/test_update_deps.py | 59 ++++------ tests/utils/test_versions.py | 17 ++- 15 files changed, 189 insertions(+), 176 deletions(-) diff --git a/.github/utils/run_hooks.py b/.github/utils/run_hooks.py index 4ddb248e..4f0c3161 100755 --- a/.github/utils/run_hooks.py +++ b/.github/utils/run_hooks.py @@ -59,5 +59,5 @@ def main(hook: str, options: list[str]) -> None: hook=sys.argv[1], options=sys.argv[2:] if len(sys.argv) > 2 else [], ) - except Exception as exc: # pylint: disable=broad-except + except Exception as exc: sys.exit(str(exc)) diff --git a/.github/workflows/_local_ci_tests.yml b/.github/workflows/_local_ci_tests.yml index 30e60cfc..1781eee6 100644 --- a/.github/workflows/_local_ci_tests.yml +++ b/.github/workflows/_local_ci_tests.yml @@ -15,13 +15,9 @@ jobs: # general install_extras: "[dev,docs,testing]" - # pre-commit - skip_pre-commit_hooks: pylint,pylint-tests - - # pylint - pylint_runs: | - --rcfile=pyproject.toml ci_cd - --rcfile=pyproject.toml --disable=import-outside-toplevel,redefined-outer-name tests + # pylint & safety + run_pylint: false + run_safety: true # build dist build_libs: flit diff --git a/.github/workflows/_local_ci_update_dependencies.yml b/.github/workflows/_local_ci_update_dependencies.yml index 8e8b40a2..e5601e76 100644 --- a/.github/workflows/_local_ci_update_dependencies.yml +++ b/.github/workflows/_local_ci_update_dependencies.yml @@ -20,6 +20,5 @@ jobs: extra_to_dos: "- [ ] Make sure the PR is **squash** merged, with a sensible commit message." update_pre-commit: true install_extras: "[dev]" - skip_pre-commit_hooks: "pylint,pylint-tests" secrets: PAT: ${{ secrets.RELEASE_PAT }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cdc87fdf..28c0223e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,19 +19,8 @@ repos: - id: trailing-whitespace args: [--markdown-linebreak-ext=md] - # isort is a tool to sort and group import statements in Python files - # It works on files in-place - - repo: https://github.com/timothycrosley/isort - rev: 5.12.0 - hooks: - - id: isort - args: - - "--profile=black" - - "--filter-files" - - "--skip-gitignore" - - "--add-import=from __future__ import annotations" - - # pyupgrade is a tool to automatically upgrade Python syntax for newer versions + # pyupgrade is a tool for automatically upgrading Python syntax for newer versions of + # the language # It works on files in-place - repo: https://github.com/asottile/pyupgrade # Latest version for Python 3.7: 3.3.2 @@ -39,7 +28,7 @@ repos: rev: v3.15.0 hooks: - id: pyupgrade - args: ["--py37-plus"] + args: [--py37-plus] # Black is a code style and formatter # It works on files in-place @@ -48,6 +37,22 @@ repos: hooks: - id: black + # ruff is a Python linter, incl. import sorter and formatter + # It works partly on files in-place + # More information can be found in its documentation: + # https://docs.astral.sh/ruff/ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.7 + hooks: + - id: ruff + # Fix what can be fixed in-place and exit with non-zero status if files were + # changed and/or there are rules violations. + args: + - "--fix" + - "--exit-non-zero-on-fix" + - "--show-fixes" + - "--no-unsafe-fixes" + # Bandit is a security linter # More information can be found in its documentation: # https://bandit.readthedocs.io/en/latest/ @@ -74,34 +79,3 @@ repos: - id: docs-api-reference args: - "--package-dir=ci_cd" - - - repo: local - hooks: - # pylint is a Python linter - # It is run through the local environment to ensure external packages can be - # imported without issue. - # For more information about pylint see its documentation at: - # https://pylint.pycqa.org/en/latest/ - - id: pylint - name: pylint - entry: pylint - args: ["--rcfile=pyproject.toml"] - language: python - types: [python] - require_serial: true - exclude: ^tests/.*$ - # pylint is a Python linter - # It is run through the local environment to ensure external packages can be - # imported without issue. - # For more information about pylint see its documentation at: - # https://pylint.pycqa.org/en/latest/ - - id: pylint-tests - name: pylint - tests - entry: pylint - args: - - "--rcfile=pyproject.toml" - - "--disable=import-outside-toplevel,redefined-outer-name" - language: python - types: [python] - require_serial: true - files: ^tests/.*$ diff --git a/ci_cd/tasks/api_reference_docs.py b/ci_cd/tasks/api_reference_docs.py index 6b4def54..6d96a10e 100644 --- a/ci_cd/tasks/api_reference_docs.py +++ b/ci_cd/tasks/api_reference_docs.py @@ -5,7 +5,6 @@ """ from __future__ import annotations -# pylint: disable=duplicate-code import logging import os import re @@ -97,7 +96,7 @@ "special_option", ], ) -def create_api_reference_docs( # pylint: disable=too-many-locals,too-many-branches,too-many-statements,line-too-long +def create_api_reference_docs( context, package_dir, pre_clean=False, @@ -312,7 +311,11 @@ def write_file(full_path: Path, content: str) -> None: f"{py_path_root}/{filename.stem}" if str(relpath) == "." or (str(relpath) == package.name and not single_package) - else f"{py_path_root}/{relpath if single_package else relpath.relative_to(package.name)}/{filename.stem}" + else ( + f"{py_path_root}/" + f"{relpath if single_package else relpath.relative_to(package.name)}/" # noqa: E501 + f"{filename.stem}" + ) ) # Replace OS specific path separators with forward slashes before diff --git a/ci_cd/tasks/docs_index.py b/ci_cd/tasks/docs_index.py index 3a1a1b02..5f2a208c 100644 --- a/ci_cd/tasks/docs_index.py +++ b/ci_cd/tasks/docs_index.py @@ -40,7 +40,7 @@ }, iterable=["replacement"], ) -def create_docs_index( # pylint: disable=too-many-locals +def create_docs_index( context, pre_commit=False, root_repo_path=".", diff --git a/ci_cd/tasks/setver.py b/ci_cd/tasks/setver.py index ef5d18f9..642a7373 100644 --- a/ci_cd/tasks/setver.py +++ b/ci_cd/tasks/setver.py @@ -50,7 +50,7 @@ }, iterable=["code_base_update"], ) -def setver( # pylint: disable=too-many-locals +def setver( _, package_dir, version, @@ -66,7 +66,7 @@ def setver( # pylint: disable=too-many-locals version: str = version # type: ignore[no-redef] root_repo_path: str = root_repo_path # type: ignore[no-redef] code_base_update: list[str] = code_base_update # type: ignore[no-redef] - code_base_update_separator: str = code_base_update_separator # type: ignore[no-redef] # pylint: disable=line-too-long + code_base_update_separator: str = code_base_update_separator # type: ignore[no-redef] test: bool = test # type: ignore[no-redef] fail_fast: bool = fail_fast # type: ignore[no-redef] @@ -120,9 +120,7 @@ def setver( # pylint: disable=too-many-locals ) filepath = Path( - filepath.format( - **{"package_dir": package_dir, "version": semantic_version} - ) + filepath.format(package_dir=package_dir, version=semantic_version) ).resolve() if not filepath.exists(): error_msg = ( @@ -143,9 +141,7 @@ def setver( # pylint: disable=too-many-locals filepath, pattern, replacement, - replacement.format( - **{"package_dir": package_dir, "version": semantic_version} - ), + replacement.format(package_dir=package_dir, version=semantic_version), ) if test: print( @@ -154,7 +150,7 @@ def setver( # pylint: disable=too-many-locals ) print( "replacement (handled): " - f"{replacement.format(**{'package_dir': package_dir, 'version': semantic_version})}" # pylint: disable=line-too-long + f"{replacement.format(package_dir=package_dir, version=semantic_version)}" # noqa: E501 ) try: @@ -163,7 +159,7 @@ def setver( # pylint: disable=too-many-locals ( pattern, replacement.format( - **{"package_dir": package_dir, "version": semantic_version} + package_dir=package_dir, version=semantic_version ), ), ) @@ -176,7 +172,7 @@ def setver( # pylint: disable=too-many-locals f"{Emoji.CROSS_MARK.value} Error: Could not update file {filepath}" f" according to the given input:\n\n pattern: {pattern}\n " "replacement: " - f"{replacement.format(**{'package_dir': package_dir, 'version': semantic_version})}" # pylint: disable=line-too-long + f"{replacement.format(package_dir=package_dir, version=semantic_version)}" # noqa: E501 ) print( diff --git a/ci_cd/tasks/update_deps.py b/ci_cd/tasks/update_deps.py index efa827a2..c30ad4f2 100644 --- a/ci_cd/tasks/update_deps.py +++ b/ci_cd/tasks/update_deps.py @@ -2,7 +2,6 @@ Update dependencies in a `pyproject.toml` file. """ -# pylint: disable=duplicate-code from __future__ import annotations import logging @@ -36,8 +35,6 @@ ) if TYPE_CHECKING: # pragma: no cover - from typing import Union - from invoke import Context, Result from ci_cd.utils.versions import IgnoreUpdateTypes, IgnoreVersions @@ -114,7 +111,7 @@ def _format_and_update_dependency( }, iterable=["ignore"], ) -def update_deps( # pylint: disable=too-many-branches,too-many-locals,too-many-statements +def update_deps( context, root_repo_path=".", fail_fast=False, diff --git a/ci_cd/utils/console_printing.py b/ci_cd/utils/console_printing.py index 6e5517d2..f2e97f8c 100644 --- a/ci_cd/utils/console_printing.py +++ b/ci_cd/utils/console_printing.py @@ -3,12 +3,16 @@ import platform from enum import Enum +from typing import TYPE_CHECKING + +if TYPE_CHECKING: # pragma: no cover + from typing_extensions import Self class Emoji(str, Enum): """Unicode strings for certain emojis.""" - def __new__(cls, value: str) -> Emoji: + def __new__(cls, value: str) -> Self: obj = str.__new__(cls, value) if platform.system() == "Windows": # Windows does not support unicode emojis, so we replace them with @@ -27,7 +31,7 @@ def __new__(cls, value: str) -> Emoji: class Color(str, Enum): """ANSI escape sequences for colors.""" - def __new__(cls, value: str) -> Color: + def __new__(cls, value: str) -> Self: obj = str.__new__(cls, value) obj._value_ = value return obj @@ -49,7 +53,7 @@ def write(self, text: str) -> str: class Formatting(str, Enum): """ANSI escape sequences for formatting.""" - def __new__(cls, value: str) -> Formatting: + def __new__(cls, value: str) -> Self: obj = str.__new__(cls, value) obj._value_ = value return obj diff --git a/ci_cd/utils/versions.py b/ci_cd/utils/versions.py index 7f880b45..f9ac7d87 100644 --- a/ci_cd/utils/versions.py +++ b/ci_cd/utils/versions.py @@ -1,11 +1,10 @@ """Handle versions.""" -# pylint: disable=too-many-lines from __future__ import annotations import logging import operator import re -from typing import TYPE_CHECKING, no_type_check +from typing import TYPE_CHECKING, NamedTuple, no_type_check from packaging.markers import Marker, default_environment from packaging.specifiers import InvalidSpecifier, Specifier, SpecifierSet @@ -17,7 +16,7 @@ from typing import Any, Dict, List from packaging.requirements import Requirement - from typing_extensions import Literal + from typing_extensions import Literal, Self IgnoreEntry = Dict[Literal["dependency-name", "versions", "update-types"], str] @@ -30,6 +29,24 @@ ] +PART_TO_LENGTH_MAPPING = { + "major": 1, + "minor": 2, + "patch": 3, +} +"""Mapping of version-style name to their number of version parts. + +E.g., a minor version has two parts, so the length is `2`. +""" + + +class IgnoreEntryPair(NamedTuple): + """A key/value-pair within an ignore entry.""" + + key: Literal["dependency-name", "versions", "update-types"] + value: str + + class SemanticVersion(str): """A semantic version. @@ -85,9 +102,7 @@ class SemanticVersion(str): https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string.""" @no_type_check - def __new__( - cls, version: str | Version | None = None, **kwargs: str | int - ) -> SemanticVersion: + def __new__(cls, version: str | Version | None = None, **kwargs: str | int) -> Self: return super().__new__( cls, str(version) if version else cls._build_version(**kwargs) ) @@ -321,7 +336,7 @@ def __le__(self, other: Any) -> bool: """Less than or equal to (`<=`) rich comparison.""" return self.__lt__(other) or self.__eq__(other) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: """Equal to (`==`) rich comparison.""" other_semver = self._validate_other_type(other) @@ -332,7 +347,7 @@ def __eq__(self, other: Any) -> bool: and self.pre_release == other_semver.pre_release ) - def __ne__(self, other: Any) -> bool: + def __ne__(self, other: object) -> bool: """Not equal to (`!=`) rich comparison.""" return not self.__eq__(other) @@ -472,14 +487,17 @@ def parse_ignore_entries(entries: list[str], separator: str) -> IgnoreRulesColle f"Could not parse ignore configuration: {pair!r} (part of the " f"ignore option: {entry!r})" ) - if match.group("key") in ignore_entry: + + parsed_pair = IgnoreEntryPair(**match.groupdict()) # type: ignore[arg-type] + + if parsed_pair.key in ignore_entry: raise InputParserError( "An ignore configuration can only be given once per option. The " - f"configuration key {match.group('key')!r} was found multiple " + f"configuration key {parsed_pair.key!r} was found multiple " f"times in the option {entry!r}" ) - ignore_entry[match.group("key")] = match.group("value").strip() # type: ignore[index] # pylint: disable=line-too-long + ignore_entry[parsed_pair.key] = parsed_pair.value.strip() if "dependency-name" not in ignore_entry: raise InputError( @@ -487,14 +505,17 @@ def parse_ignore_entries(entries: list[str], separator: str) -> IgnoreRulesColle f"configuration. Ignore option entry: {entry}" ) - dependency_name: str = ignore_entry.pop("dependency-name", "") + dependency_name = ignore_entry["dependency-name"] if dependency_name not in ignore_entries: ignore_entries[dependency_name] = { - key: [value] for key, value in ignore_entry.items() # type: ignore[misc] + key: [value] + for key, value in ignore_entry.items() + if key != "dependency-name" } else: for key, value in ignore_entry.items(): - ignore_entries[dependency_name][key].append(value) # type: ignore[index] + if key != "dependency-name": + ignore_entries[dependency_name][key].append(value) return ignore_entries @@ -547,7 +568,7 @@ def parse_ignore_rules( "'version-update:semver-patch'.\nUnparseable 'update-types' " f"value: {update_type_entry!r}" ) - update_types["version-update"].append(match.group("semver_part")) # type: ignore[arg-type] # pylint: disable=line-too-long + update_types["version-update"].append(match.group("semver_part")) # type: ignore[arg-type] return versions, update_types @@ -599,7 +620,7 @@ def _ignore_version_rules_semver( semver_latest, semver_version_rule ): decision_version_rule = True - elif "~=" == version_rule["operator"]: + elif version_rule["operator"] == "~=": # The '~=' operator is a special case, as it's not a direct comparison # operator, but rather a range operator. The '~=' operator is used to # specify a minimum version, but with some flexibility in the last part. @@ -672,19 +693,19 @@ def _ignore_semver_rules( f"'patch' (you gave {semver_rules['version-update']!r})." ) - if ( # pylint: disable=too-many-boolean-expressions + if ( ("major" in semver_rules["version-update"] and latest[0] != current[0]) or ( "minor" in semver_rules["version-update"] - and len(latest) >= 2 - and len(current) >= 2 + and len(latest) >= PART_TO_LENGTH_MAPPING["minor"] + and len(current) >= PART_TO_LENGTH_MAPPING["minor"] and latest[1] > current[1] and latest[0] == current[0] ) or ( "patch" in semver_rules["version-update"] - and len(latest) >= 3 - and len(current) >= 3 + and len(latest) >= PART_TO_LENGTH_MAPPING["patch"] + and len(current) >= PART_TO_LENGTH_MAPPING["patch"] and latest[2] > current[2] and latest[0] == current[0] and latest[1] == current[1] @@ -777,7 +798,7 @@ def regenerate_requirement( str(_) for _ in sorted( specifier or requirement.specifier, - key=lambda spec: spec.operator, # type: ignore[attr-defined] + key=lambda spec: spec.operator, reverse=True, ) ) @@ -793,7 +814,7 @@ def regenerate_requirement( return updated_dependency -def update_specifier_set( # pylint: disable=too-many-statements,too-many-branches +def update_specifier_set( latest_version: SemanticVersion | Version | str, current_specifier_set: SpecifierSet ) -> SpecifierSet: """Update the specifier set to include the latest version.""" @@ -889,13 +910,13 @@ def update_specifier_set( # pylint: disable=too-many-statements,too-many-branch # Up only the last version segment of the latest version according to # what version segments are defined in the specifier version. - if len(split_specifier_version) == 1: + if len(split_specifier_version) == PART_TO_LENGTH_MAPPING["major"]: updated_version += str(latest_version.next_version("major").major) - elif len(split_specifier_version) == 2: + elif len(split_specifier_version) == PART_TO_LENGTH_MAPPING["minor"]: updated_version += ".".join( latest_version.next_version("minor").split(".")[:2] ) - elif len(split_specifier_version) == 3: + elif len(split_specifier_version) == PART_TO_LENGTH_MAPPING["patch"]: updated_version += latest_version.next_version("patch") else: raise UnableToResolve( @@ -929,7 +950,7 @@ def update_specifier_set( # pylint: disable=too-many-statements,too-many-branch # < next major version up from latest_version updated_specifiers.append( - f"<{epoch}{latest_version.next_version('major').major}" + f"<{epoch}{latest_version.next_version('major').major!s}" ) else: # Keep the ~= operator, but update to include the latest version as @@ -976,7 +997,7 @@ def _semi_valid_python_version(version: SemanticVersion) -> bool: f"Invalid Python major version: {version.major}. Expected 1, 2, or 3." ) - if version.minor not in range(0, 12 + 1) or version.patch not in range(0, 18 + 1): + if version.minor not in range(12 + 1) or version.patch not in range(18 + 1): # Either: # Not a valid Python minor version (0, 1, 2, ..., 12) # Not a valid Python patch version (0, 1, 2, ..., 18) @@ -984,7 +1005,7 @@ def _semi_valid_python_version(version: SemanticVersion) -> bool: return True -def get_min_max_py_version( # pylint: disable=too-many-branches,too-many-statements +def get_min_max_py_version( requires_python: str | Marker, ) -> str: """Get minimum or maximum Python version from `requires_python`. @@ -1052,13 +1073,13 @@ def get_min_max_py_version( # pylint: disable=too-many-branches,too-many-statem split_version = specifier.version.split(".") parsed_version = SemanticVersion(specifier.version) - if len(split_version) == 1: + if len(split_version) == PART_TO_LENGTH_MAPPING["major"]: py_version = str(parsed_version.next_version("major").major) - elif len(split_version) == 2: + elif len(split_version) == PART_TO_LENGTH_MAPPING["minor"]: py_version = ".".join( parsed_version.next_version("minor").split(".")[:2] ) - elif len(split_version) == 3: + elif len(split_version) == PART_TO_LENGTH_MAPPING["patch"]: py_version = str(parsed_version.next_version("patch")) break @@ -1106,26 +1127,31 @@ def get_min_max_py_version( # pylint: disable=too-many-branches,too-many-statem split_py_version = py_version.split(".") parsed_py_version = SemanticVersion(py_version) + # See the _semi_valid_python_version() function for these values + largest_value_for_a_patch_part = 18 + largest_value_for_a_minor_part = 12 + largest_value_for_a_major_part = 3 + largest_value_for_any_part = max( + largest_value_for_a_patch_part, + largest_value_for_a_minor_part, + largest_value_for_a_major_part, + ) + while ( not _semi_valid_python_version(parsed_py_version) or py_version not in specifier_set ): if min_or_max == "min": - if ( # Largest value for a Python version patch part - parsed_py_version.patch >= 18 - ): + if parsed_py_version.patch >= largest_value_for_a_patch_part: parsed_py_version = parsed_py_version.next_version("minor") - elif ( # Largest value for a Python version minor part - parsed_py_version.minor >= 12 - ): + elif parsed_py_version.minor >= largest_value_for_a_minor_part: parsed_py_version = parsed_py_version.next_version("major") else: parsed_py_version = parsed_py_version.next_version("patch") else: parsed_py_version = parsed_py_version.previous_version( length_to_part_mapping[len(split_py_version)], - # Largest value for any part of a Python version (patch) - max_filler=18, + max_filler=largest_value_for_any_part, ) py_version = parsed_py_version.shortened() @@ -1139,9 +1165,9 @@ def find_minimum_py_version(marker: Marker, project_py_version: str) -> str: split_py_version = project_py_version.split(".") def _next_version(_version: SemanticVersion) -> SemanticVersion: - if len(split_py_version) == 1: + if len(split_py_version) == PART_TO_LENGTH_MAPPING["major"]: return _version.next_version("major") - if len(split_py_version) == 2: + if len(split_py_version) == PART_TO_LENGTH_MAPPING["minor"]: return _version.next_version("minor") return _version.next_version("patch") diff --git a/pyproject.toml b/pyproject.toml index c8c227c0..95a1d211 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,8 +56,6 @@ testing = [ dev = [ "pre-commit ~=2.21; python_version < '3.8'", "pre-commit ~=3.5; python_version >= '3.8'", - "pylint ~=2.13; python_version < '3.8'", - "pylint ~=3.0; python_version >= '3.8'", "ci-cd[docs,testing]", ] @@ -80,14 +78,46 @@ show_error_codes = true allow_redefinition = true check_untyped_defs = true -[tool.pylint.messages_control] -max-line-length = 90 -disable = [] -max-args = 15 -max-branches = 18 -max-returns = 10 - [tool.pytest.ini_options] minversion = "7.4" addopts = ["-rs", "--cov=ci_cd", "--cov-report=term-missing:skip-covered"] filterwarnings = ["error"] + +[tool.ruff.lint] +extend-select = [ + "E", # pycodestyle + "F", # Pyflakes + "B", # flake8-bugbear + "I", # isort + "BLE", # flake8-blind-except + "ARG", # flake8-unused-arguments + "C4", # flake8-comprehensions + "ICN", # flake8-import-conventions + "G", # flake8-logging-format + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "RET", # flake8-return + "RUF", # Ruff-specific + "SIM", # flake8-simplify + "YTT", # flake8-2020 + "EXE", # flake8-executable + "PYI", # flake8-pyi +] +ignore = [ + "PLR", # Design related pylint codes + "PLW0127", # pylint: Self-assignment of variables +] + +# Import __future__.annotations for all Python files. +isort.required-imports = ["from __future__ import annotations"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = [ + "BLE", # flake8-blind-except +] +".github/**" = [ + "BLE", # flake8-blind-except +] diff --git a/tests/conftest.py b/tests/conftest.py index c5b86ba9..0013ea1c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ @pytest.fixture(autouse=True) -def clear_loggers() -> None: +def _clear_loggers() -> None: """Remove handlers from all loggers""" import logging diff --git a/tests/tasks/test_api_reference_docs.py b/tests/tasks/test_api_reference_docs.py index fd5e7ef8..bcd3193b 100644 --- a/tests/tasks/test_api_reference_docs.py +++ b/tests/tasks/test_api_reference_docs.py @@ -1,5 +1,4 @@ """Test `ci_cd.tasks.api_reference_docs`.""" -# pylint: disable=too-many-locals from __future__ import annotations from typing import TYPE_CHECKING @@ -495,7 +494,7 @@ def test_larger_package(tmp_path: Path) -> None: package_dir / "module" / "submodule", package_dir / "second_module", ] - for destination in [package_dir] + new_submodules: + for destination in [package_dir, *new_submodules]: shutil.copytree( src=Path(__file__).resolve().parent.parent.parent / "ci_cd", dst=destination, @@ -741,7 +740,10 @@ def test_larger_multi_packages(tmp_path: Path) -> None: ) == 'title: "tasks"\n' assert (package_dir / "tasks" / "api_reference_docs.md").read_text( encoding="utf8" - ) == f"# api_reference_docs\n\n::: {package_dir.name}.tasks.api_reference_docs\n" + ) == ( + "# api_reference_docs\n\n::: " + f"{package_dir.name}.tasks.api_reference_docs\n" + ) assert (package_dir / "tasks" / "docs_index.md").read_text( encoding="utf8" ) == f"# docs_index\n\n::: {package_dir.name}.tasks.docs_index\n" diff --git a/tests/tasks/test_update_deps.py b/tests/tasks/test_update_deps.py index 3114daf8..d4c36862 100644 --- a/tests/tasks/test_update_deps.py +++ b/tests/tasks/test_update_deps.py @@ -1,5 +1,4 @@ """Test `ci_cd.tasks.update_deps()`.""" -# pylint: disable=too-many-locals,too-many-lines from __future__ import annotations from typing import TYPE_CHECKING @@ -92,24 +91,22 @@ def test_update_deps(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: context = MockContext( run={ - **{ - re.compile(r".*invoke$"): "invoke (1.7.1.post1)\n", - re.compile(r".*tomlkit$"): "tomlkit (1.0.0)", - re.compile(r".*mike$"): "mike (1!1.1.1)", - re.compile(r".*pytest$"): "pytest (7.1.0)", - re.compile(r".*pytest-cov$"): "pytest-cov (3.1.5)", - re.compile(r".*pre-commit$"): "pre-commit (2.21.5)", - re.compile(r".*pylint$"): "pylint (2.14.2)", - re.compile(r".* A$"): "A (1.2.3)", - re.compile(r".*A.B-C_D$"): "A.B-C_D (1.2.3)", - re.compile(r".*aa$"): "aa (1.2.3)", - re.compile(r".*name$"): "name (1.2.3)", - re.compile(r".*test-pkg$"): "test-pkg (1!2.3)", - re.compile(r".*epoch$"): "epoch (2!2.0.4.post1)", - re.compile(r".*epoch1$"): "epoch1 (1!1.0.0)", - re.compile(r".*epoch2$"): "epoch2 (1!2.1.0)", - re.compile(r".*epoch3$"): "epoch3 (1!1.1.0.post1)", - }, + re.compile(r".*invoke$"): "invoke (1.7.1.post1)\n", + re.compile(r".*tomlkit$"): "tomlkit (1.0.0)", + re.compile(r".*mike$"): "mike (1!1.1.1)", + re.compile(r".*pytest$"): "pytest (7.1.0)", + re.compile(r".*pytest-cov$"): "pytest-cov (3.1.5)", + re.compile(r".*pre-commit$"): "pre-commit (2.21.5)", + re.compile(r".*pylint$"): "pylint (2.14.2)", + re.compile(r".* A$"): "A (1.2.3)", + re.compile(r".*A.B-C_D$"): "A.B-C_D (1.2.3)", + re.compile(r".*aa$"): "aa (1.2.3)", + re.compile(r".*name$"): "name (1.2.3)", + re.compile(r".*test-pkg$"): "test-pkg (1!2.3)", + re.compile(r".*epoch$"): "epoch (2!2.0.4.post1)", + re.compile(r".*epoch1$"): "epoch1 (1!1.0.0)", + re.compile(r".*epoch2$"): "epoch2 (1!2.1.0)", + re.compile(r".*epoch3$"): "epoch3 (1!1.1.0.post1)", **{re.compile(rf".*name{i}$"): f"name{i} (3.2.1)" for i in range(1, 12)}, } ) @@ -696,7 +693,7 @@ def test_missing_project_package_name(tmp_path: Path) -> None: @pytest.mark.parametrize( - "dependency,optional_dependency,fail_fast", + ("dependency", "optional_dependency", "fail_fast"), [ ("(pytest)", "", False), ("", "(pytest)", False), @@ -744,7 +741,7 @@ def test_invalid_requirement( # hence we'd expect the error to be raised for that one. raise_msg = ( f"^{re.escape(Emoji.CROSS_MARK.value)} " - f"{re.escape(error_msg(log_msg.format(bad_dependency=dependency))[:-len(Color.RESET.value)])}.*" # pylint: disable=line-too-long + f"{re.escape(error_msg(log_msg.format(bad_dependency=dependency))[:-len(Color.RESET.value)])}.*" ) else: raise_msg = r".*Errors occurred! See printed statements above\.$" @@ -793,7 +790,7 @@ def test_invalid_requirement( ), captured_stderr -@pytest.mark.parametrize("fail_fast", [True, False], ids=["fail_fast", "no fail_fast"]) +@pytest.mark.parametrize("fail_fast", [True, False]) def test_non_parseable_pip_index_versions( tmp_path: Path, fail_fast: bool, @@ -896,9 +893,7 @@ def test_no_dependency_updates_available( assert pyproject_file.read_text(encoding="utf8") == pyproject_file_data -@pytest.mark.parametrize( - "pre_commit", [True, False], ids=["pre-commit", "no pre-commit"] -) +@pytest.mark.parametrize("pre_commit", [True, False]) def test_pre_commit(tmp_path: Path, pre_commit: bool) -> None: """Check pre-commit toggle.""" import re @@ -981,7 +976,7 @@ def test_pre_commit(tmp_path: Path, pre_commit: bool) -> None: ) -@pytest.mark.parametrize("fail_fast", [True, False], ids=["fail_fast", "no fail_fast"]) +@pytest.mark.parametrize("fail_fast", [True, False]) def test_unresolvable_specifier_set( tmp_path: Path, fail_fast: bool, @@ -1052,16 +1047,8 @@ def test_unresolvable_specifier_set( assert terminal_msg.search(capsys.readouterr().err) is not None, terminal_msg -@pytest.mark.parametrize( - ["skip_unnormalized_python_package_names", "fail_fast"], - [(True, True), (False, False), (False, True), (True, False)], - ids=[ - "skip_unnormalized_python_package_names, fail_fast", - "no skip_unnormalized_python_package_names, no fail_fast", - "no skip_unnormalized_python_package_names, fail_fast", - "skip_unnormalized_python_package_names, no fail_fast", - ], -) +@pytest.mark.parametrize("skip_unnormalized_python_package_names", [True, False]) +@pytest.mark.parametrize("fail_fast", [True, False]) def test_skip_unnormalized_python_package_names( tmp_path: Path, skip_unnormalized_python_package_names: bool, diff --git a/tests/utils/test_versions.py b/tests/utils/test_versions.py index 36124ea6..201642bb 100644 --- a/tests/utils/test_versions.py +++ b/tests/utils/test_versions.py @@ -1,5 +1,4 @@ """Tests for utils/versions.py""" -# pylint: disable=too-many-lines from __future__ import annotations from typing import TYPE_CHECKING @@ -141,9 +140,9 @@ def test_semanticversion_invalid() -> None: ] for input_, exc_msg in invalid_inputs: with pytest.raises(ValueError, match=exc_msg): - SemanticVersion( # pylint: disable=expression-not-assigned - **input_ - ) if isinstance(input_, dict) else SemanticVersion(input_) + SemanticVersion(**input_) if isinstance(input_, dict) else SemanticVersion( + input_ + ) def test_semanticversion_invalid_comparisons() -> None: @@ -252,7 +251,7 @@ def test_semanticversion_python_version( if isinstance(version_, Version) or ( isinstance(version_, str) and re.match( - SemanticVersion._semver_regex, # pylint: disable=protected-access + SemanticVersion._semver_regex, version_, ) is None @@ -273,9 +272,9 @@ def test_semanticversion_python_version( f"{Version(version_) if isinstance(version_, str) else version_}" ) - assert ( - repr(semver) - == f"SemanticVersion({str(semver.as_python_version(shortened=False))!r})" + assert repr(semver) == ( + "SemanticVersion(" + f"{str(semver.as_python_version(shortened=False))!r})" ) else: # The version is parsed as a regular semantic version, where the 'local' @@ -1098,7 +1097,7 @@ def test_ignore_version_fails() -> None: @pytest.mark.parametrize( - ["requires_python", "expected_outcome"], + ("requires_python", "expected_outcome"), [ # Minimum operators # >=