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 # >=