diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67f177d1..c62dd674 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,6 +26,16 @@ repos: - id: isort args: ["--profile", "black", "--filter-files", "--skip-gitignore"] + # pyupgrade is a tool to automatically upgrade Python syntax for newer versions + # It works on files in-place + - repo: https://github.com/asottile/pyupgrade + # Latest version for Python 3.7: 3.3.2 + # Latest version for Python 3.8: 3.8.0 + rev: v3.3.2 + hooks: + - id: pyupgrade + args: ["--py37-plus"] + # Black is a code style and formatter # It works on files in-place - repo: https://github.com/ambv/black diff --git a/ci_cd/tasks/api_reference_docs.py b/ci_cd/tasks/api_reference_docs.py index 937bfdce..ccac717a 100644 --- a/ci_cd/tasks/api_reference_docs.py +++ b/ci_cd/tasks/api_reference_docs.py @@ -18,6 +18,8 @@ from ci_cd.utils import Emoji if TYPE_CHECKING: # pragma: no cover + from typing import List + from invoke import Context, Result @@ -121,18 +123,18 @@ def create_api_reference_docs( # pylint: disable=too-many-locals,too-many-branc debug: bool = debug # type: ignore[no-redef] if not unwanted_folder: - unwanted_folder: list[str] = ["__pycache__"] # type: ignore[no-redef] + unwanted_folder: "List[str]" = ["__pycache__"] # type: ignore[no-redef] if not unwanted_file: - unwanted_file: list[str] = ["__init__.py"] # type: ignore[no-redef] + unwanted_file: "List[str]" = ["__init__.py"] # type: ignore[no-redef] if not full_docs_folder: - full_docs_folder: list[str] = [] # type: ignore[no-redef] + full_docs_folder: "List[str]" = [] # type: ignore[no-redef] if not full_docs_file: - full_docs_file: list[str] = [] # type: ignore[no-redef] + full_docs_file: "List[str]" = [] # type: ignore[no-redef] if not special_option: - special_option: list[str] = [] # type: ignore[no-redef] + special_option: "List[str]" = [] # type: ignore[no-redef] # Initialize user-given paths as pure POSIX paths - package_dir: list[PurePosixPath] = [PurePosixPath(_) for _ in package_dir] + package_dir: "List[PurePosixPath]" = [PurePosixPath(_) for _ in package_dir] root_repo_path = str(PurePosixPath(root_repo_path)) docs_folder: PurePosixPath = PurePosixPath(docs_folder) # type: ignore[no-redef] full_docs_folder = [Path(PurePosixPath(_)) for _ in full_docs_folder] @@ -161,7 +163,7 @@ def write_file(full_path: Path, content: str) -> None: root_repo_path = result.stdout.strip("\n") # type: ignore[no-redef] root_repo_path: Path = Path(root_repo_path).resolve() # type: ignore[no-redef] - package_dirs: list[Path] = [Path(root_repo_path / _) for _ in package_dir] + package_dirs: "List[Path]" = [Path(root_repo_path / _) for _ in package_dir] docs_api_ref_dir = Path(root_repo_path / docs_folder / "api_reference") LOGGER.debug( diff --git a/ci_cd/tasks/docs_index.py b/ci_cd/tasks/docs_index.py index df8ae09f..0803b318 100644 --- a/ci_cd/tasks/docs_index.py +++ b/ci_cd/tasks/docs_index.py @@ -12,6 +12,8 @@ from ci_cd.utils import Emoji if TYPE_CHECKING: # pragma: no cover + from typing import List + from invoke import Context, Result @@ -56,7 +58,7 @@ def create_docs_index( # pylint: disable=too-many-locals docs_folder: Path = Path(docs_folder) if not replacement: - replacement: list[str] = [] # type: ignore[no-redef] + replacement: "List[str]" = [] # type: ignore[no-redef] replacement.append(f"{docs_folder.name}/{replacement_separator}") if pre_commit and root_repo_path == ".": diff --git a/ci_cd/tasks/setver.py b/ci_cd/tasks/setver.py index cc3f7005..b9ae0671 100644 --- a/ci_cd/tasks/setver.py +++ b/ci_cd/tasks/setver.py @@ -13,6 +13,9 @@ from ci_cd.utils import Emoji, SemanticVersion, update_file +if TYPE_CHECKING: # pragma: no cover + from typing import List + # Get logger LOGGER = logging.getLogger(__name__) @@ -63,7 +66,7 @@ def setver( # pylint: disable=too-many-locals package_dir: str = package_dir # type: ignore[no-redef] 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: 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 test: bool = test # type: ignore[no-redef] fail_fast: bool = fail_fast # type: ignore[no-redef] @@ -100,7 +103,7 @@ def setver( # pylint: disable=too-many-locals ), ) else: - errors: list[str] = [] + errors: "List[str]" = [] for code_update in code_base_update: try: filepath, pattern, replacement = code_update.split( diff --git a/ci_cd/tasks/update_deps.py b/ci_cd/tasks/update_deps.py index 31440d04..d53c4c59 100644 --- a/ci_cd/tasks/update_deps.py +++ b/ci_cd/tasks/update_deps.py @@ -105,7 +105,7 @@ def update_deps( # pylint: disable=too-many-branches,too-many-locals,too-many-s ): """Update dependencies in specified Python package's `pyproject.toml`.""" if TYPE_CHECKING: # pragma: no cover - context: "Context" = context # type: ignore[no-redef] + context: Context = context # type: ignore[no-redef] root_repo_path: str = root_repo_path # type: ignore[no-redef] fail_fast: bool = fail_fast # type: ignore[no-redef] pre_commit: bool = pre_commit # type: ignore[no-redef] @@ -130,7 +130,7 @@ def update_deps( # pylint: disable=too-many-branches,too-many-locals,too-many-s if pre_commit and root_repo_path == ".": # Use git to determine repo root - result: "Result" = context.run("git rev-parse --show-toplevel", hide=True) + result: Result = context.run("git rev-parse --show-toplevel", hide=True) root_repo_path = result.stdout.strip("\n") pyproject_path = Path(root_repo_path).resolve() / "pyproject.toml" @@ -261,7 +261,7 @@ def update_deps( # pylint: disable=too-many-branches,too-many-locals,too-many-s LOGGER.debug("Min/max Python version from marker: %s", marker_py_version) # Check version from PyPI's online package index - out: "Result" = context.run( + out: Result = context.run( "pip index versions " f"--python-version {marker_py_version or py_version} " f"{parsed_requirement.name}", @@ -342,8 +342,8 @@ def update_deps( # pylint: disable=too-many-branches,too-many-locals,too-many-s # Apply ignore rules if parsed_requirement.name in ignore_rules or "*" in ignore_rules: - versions: "IgnoreVersions" = [] - update_types: "IgnoreUpdateTypes" = {} + versions: IgnoreVersions = [] + update_types: IgnoreUpdateTypes = {} if "*" in ignore_rules: versions, update_types = parse_ignore_rules(ignore_rules["*"]) diff --git a/ci_cd/utils/versions.py b/ci_cd/utils/versions.py index ef935bca..10e532d9 100644 --- a/ci_cd/utils/versions.py +++ b/ci_cd/utils/versions.py @@ -12,18 +12,19 @@ from ci_cd.exceptions import InputError, InputParserError, UnableToResolve if TYPE_CHECKING: # pragma: no cover - from typing import Any, Literal, Optional, Union + from typing import Any, Dict, List from packaging.requirements import Requirement + from typing_extensions import Literal - IgnoreEntry = dict[Literal["dependency-name", "versions", "update-types"], str] + IgnoreEntry = Dict[Literal["dependency-name", "versions", "update-types"], str] - IgnoreRules = dict[Literal["versions", "update-types"], list[str]] - IgnoreRulesCollection = dict[str, IgnoreRules] + IgnoreRules = Dict[Literal["versions", "update-types"], List[str]] + IgnoreRulesCollection = Dict[str, IgnoreRules] - IgnoreVersions = list[dict[Literal["operator", "version"], str]] - IgnoreUpdateTypes = dict[ - Literal["version-update"], list[Literal["major", "minor", "patch"]] + IgnoreVersions = List[Dict[Literal["operator", "version"], str]] + IgnoreUpdateTypes = Dict[ + Literal["version-update"], List[Literal["major", "minor", "patch"]] ] @@ -80,21 +81,21 @@ class SemanticVersion(str): @no_type_check def __new__( - cls, version: "Optional[str]" = None, **kwargs: "Union[str, int]" - ) -> "SemanticVersion": + cls, version: str | None = None, **kwargs: str | int + ) -> SemanticVersion: return super().__new__( cls, version if version else cls._build_version(**kwargs) ) def __init__( self, - version: "Optional[str]" = None, + version: str | None = None, *, - major: "Union[str, int]" = "", - minor: "Optional[Union[str, int]]" = None, - patch: "Optional[Union[str, int]]" = None, - pre_release: "Optional[str]" = None, - build: "Optional[str]" = None, + major: str | int = "", + minor: str | int | None = None, + patch: str | int | None = None, + pre_release: str | None = None, + build: str | None = None, ) -> None: if version is not None: if major or minor or patch or pre_release or build: @@ -119,11 +120,11 @@ def __init__( @classmethod def _build_version( cls, - major: "Optional[Union[str, int]]" = None, - minor: "Optional[Union[str, int]]" = None, - patch: "Optional[Union[str, int]]" = None, - pre_release: "Optional[str]" = None, - build: "Optional[str]" = None, + major: str | int | None = None, + minor: str | int | None = None, + patch: str | int | None = None, + pre_release: str | None = None, + build: str | None = None, ) -> str: """Build a version from the given parameters.""" if major is None: @@ -169,7 +170,7 @@ def patch(self) -> int: return self._patch @property - def pre_release(self) -> "Union[None, str]": + def pre_release(self) -> None | str: """The pre-release part of the version This is the part supplied after a minus (`-`), but before a plus (`+`). @@ -177,7 +178,7 @@ def pre_release(self) -> "Union[None, str]": return self._pre_release @property - def build(self) -> "Union[None, str]": + def build(self) -> None | str: """The build metadata part of the version. This is the part supplied at the end of the version, after a plus (`+`). @@ -196,7 +197,7 @@ def __repr__(self) -> str: """Return the string representation of the object.""" return repr(self.__str__()) - def _validate_other_type(self, other: "Any") -> "SemanticVersion": + def _validate_other_type(self, other: Any) -> SemanticVersion: """Initial check/validation of `other` before rich comparisons.""" not_implemented_exc = NotImplementedError( f"Rich comparison not implemented between {self.__class__.__name__} and " @@ -214,7 +215,7 @@ def _validate_other_type(self, other: "Any") -> "SemanticVersion": raise not_implemented_exc - def __lt__(self, other: "Any") -> bool: + def __lt__(self, other: Any) -> bool: """Less than (`<`) rich comparison.""" other_semver = self._validate_other_type(other) @@ -234,11 +235,11 @@ def __lt__(self, other: "Any") -> bool: return self.pre_release < other_semver.pre_release return False - def __le__(self, other: "Any") -> bool: + 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: Any) -> bool: """Equal to (`==`) rich comparison.""" other_semver = self._validate_other_type(other) @@ -249,19 +250,19 @@ def __eq__(self, other: "Any") -> bool: and self.pre_release == other_semver.pre_release ) - def __ne__(self, other: "Any") -> bool: + def __ne__(self, other: Any) -> bool: """Not equal to (`!=`) rich comparison.""" return not self.__eq__(other) - def __ge__(self, other: "Any") -> bool: + def __ge__(self, other: Any) -> bool: """Greater than or equal to (`>=`) rich comparison.""" return not self.__lt__(other) - def __gt__(self, other: "Any") -> bool: + def __gt__(self, other: Any) -> bool: """Greater than (`>`) rich comparison.""" return not self.__le__(other) - def next_version(self, version_part: str) -> "SemanticVersion": + def next_version(self, version_part: str) -> SemanticVersion: """Return the next version for the specified version part. Parameters: @@ -290,8 +291,8 @@ def next_version(self, version_part: str) -> "SemanticVersion": return self.__class__(next_version) def previous_version( - self, version_part: str, max_filler: "Optional[Union[str, int]]" = 99 - ) -> "SemanticVersion": + self, version_part: str, max_filler: str | int | None = 99 + ) -> SemanticVersion: """Return the previous version for the specified version part. Parameters: @@ -358,7 +359,7 @@ def shortened(self) -> str: return f"{self.major}.{self.minor}.{self.patch}" -def parse_ignore_entries(entries: list[str], separator: str) -> "IgnoreRulesCollection": +def parse_ignore_entries(entries: list[str], separator: str) -> IgnoreRulesCollection: """Parser for the `--ignore` option. The `--ignore` option values are given as key/value-pairs in the form: @@ -373,7 +374,7 @@ def parse_ignore_entries(entries: list[str], separator: str) -> "IgnoreRulesColl A parsed mapping of dependencies to ignore rules. """ - ignore_entries: "IgnoreRulesCollection" = {} + ignore_entries: IgnoreRulesCollection = {} for entry in entries: pairs = entry.split(separator, maxsplit=2) @@ -385,7 +386,7 @@ def parse_ignore_entries(entries: list[str], separator: str) -> "IgnoreRulesColl f"value: --ignore={entry!r}" ) - ignore_entry: "IgnoreEntry" = {} + ignore_entry: IgnoreEntry = {} for pair in pairs: match = re.match( r"^(?Pdependency-name|versions|update-types)=(?P.*)$", @@ -424,8 +425,8 @@ def parse_ignore_entries(entries: list[str], separator: str) -> "IgnoreRulesColl def parse_ignore_rules( - rules: "IgnoreRules", -) -> "tuple[IgnoreVersions, IgnoreUpdateTypes]": + rules: IgnoreRules, +) -> tuple[IgnoreVersions, IgnoreUpdateTypes]: """Parser for a specific set of ignore rules. Parameters: @@ -439,8 +440,8 @@ def parse_ignore_rules( # Ignore package altogether return [{"operator": ">=", "version": "0"}], {} - versions: "IgnoreVersions" = [] - update_types: "IgnoreUpdateTypes" = {} + versions: IgnoreVersions = [] + update_types: IgnoreUpdateTypes = {} if "versions" in rules: for versions_entry in rules["versions"]: @@ -477,7 +478,7 @@ def parse_ignore_rules( return versions, update_types -def create_ignore_rules(specifier_set: SpecifierSet) -> "IgnoreRules": +def create_ignore_rules(specifier_set: SpecifierSet) -> IgnoreRules: """Create ignore rules based on version specifier set. The only ignore rules needed are related to versions that should be explicitly @@ -494,7 +495,7 @@ def create_ignore_rules(specifier_set: SpecifierSet) -> "IgnoreRules": def _ignore_version_rules_semver( - latest: list[str], version_rules: "IgnoreVersions" + latest: list[str], version_rules: IgnoreVersions ) -> bool: # pragma: no cover """Determine whether to ignore package based on `versions` input. @@ -565,7 +566,7 @@ def _ignore_version_rules_semver( def _ignore_version_rules_specifier_set( - latest: list[str], version_rules: "IgnoreVersions" + latest: list[str], version_rules: IgnoreVersions ) -> bool: """Determine whether to ignore package based on `versions` input. @@ -586,7 +587,7 @@ def _ignore_version_rules_specifier_set( def _ignore_semver_rules( current: list[str], latest: list[str], - semver_rules: "IgnoreUpdateTypes", + semver_rules: IgnoreUpdateTypes, ) -> bool: """If ANY of the semver rules are True, ignore the version.""" if any( @@ -626,8 +627,8 @@ def _ignore_semver_rules( def ignore_version( current: list[str], latest: list[str], - version_rules: "IgnoreVersions", - semver_rules: "IgnoreUpdateTypes", + version_rules: IgnoreVersions, + semver_rules: IgnoreUpdateTypes, ) -> bool: """Determine whether the latest version can be ignored. @@ -664,13 +665,13 @@ def ignore_version( def regenerate_requirement( - requirement: "Requirement", + requirement: Requirement, *, - name: "Optional[str]" = None, - extras: "Optional[set[str]]" = None, - specifier: "Optional[Union[SpecifierSet, str]]" = None, - url: "Optional[str]" = None, - marker: "Optional[Union[Marker, str]]" = None, + name: str | None = None, + extras: set[str] | None = None, + specifier: SpecifierSet | str | None = None, + url: str | None = None, + marker: Marker | str | None = None, post_name_space: bool = False, ) -> str: """Regenerate a requirement string including the given parameters. @@ -722,7 +723,7 @@ def regenerate_requirement( def update_specifier_set( - latest_version: "Union[SemanticVersion, str]", current_specifier_set: SpecifierSet + latest_version: SemanticVersion | str, current_specifier_set: SpecifierSet ) -> SpecifierSet: """Update the specifier set to include the latest version.""" latest_version = SemanticVersion(latest_version) @@ -822,7 +823,7 @@ def update_specifier_set( # current specifier set is valid as is and already includes the latest version if updated_specifiers != [""]: # Otherwise, add updated specifier(s) to new specifier set - new_specifier_set |= set(Specifier(_) for _ in updated_specifiers) + new_specifier_set |= {Specifier(_) for _ in updated_specifiers} else: raise UnableToResolve( "Cannot resolve how to update specifier set to include latest version." @@ -858,7 +859,7 @@ def _semi_valid_python_version(version: SemanticVersion) -> bool: def get_min_max_py_version( # pylint: disable=too-many-branches,too-many-statements - requires_python: "Union[str, Marker]", + requires_python: str | Marker, ) -> str: """Get minimum or maximum Python version from `requires_python`. @@ -1007,7 +1008,7 @@ def get_min_max_py_version( # pylint: disable=too-many-branches,too-many-statem return py_version -def find_minimum_py_version(marker: "Marker", project_py_version: str) -> str: +def find_minimum_py_version(marker: Marker, project_py_version: str) -> str: """Find the minimum Python version from a marker.""" split_py_version = project_py_version.split(".") diff --git a/pyproject.toml b/pyproject.toml index 029b80ba..676f7e8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,8 @@ dependencies = [ "invoke ~=2.2", "packaging ~=23.2", "tomlkit ~=0.12.1", + "typing-extensions ~=4.7; python_version < '3.8'", + "typing-extensions ~=4.8; python_version >= '3.8'", ] [project.optional-dependencies] diff --git a/tests/tasks/test_update_deps.py b/tests/tasks/test_update_deps.py index 34903386..30f6a9bb 100644 --- a/tests/tasks/test_update_deps.py +++ b/tests/tasks/test_update_deps.py @@ -10,7 +10,7 @@ from pathlib import Path -def test_update_deps(tmp_path: "Path", caplog: pytest.LogCaptureFixture) -> None: +def test_update_deps(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None: """Check update_deps runs with defaults.""" import re @@ -317,7 +317,7 @@ def test_update_deps(tmp_path: "Path", caplog: pytest.LogCaptureFixture) -> None ], ) def test_ignore_rules_logic( - tmp_path: "Path", ignore_rules: list[str], expected_result: dict[str, str] + tmp_path: Path, ignore_rules: list[str], expected_result: dict[str, str] ) -> None: """Check the workflow of multiple interconnecting ignore rules are respected.""" import re @@ -402,7 +402,7 @@ def test_ignore_rules_logic( def test_python_version_marker( - tmp_path: "Path", caplog: pytest.LogCaptureFixture + tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: """Check the python version marker is respected.""" import re @@ -458,7 +458,7 @@ def test_python_version_marker( def test_no_warn_when_project_name( - tmp_path: "Path", caplog: pytest.LogCaptureFixture + tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: """Check no warning is emitted if a dependency is also the project name. diff --git a/tests/utils/test_versions.py b/tests/utils/test_versions.py index f608ea97..d384e911 100644 --- a/tests/utils/test_versions.py +++ b/tests/utils/test_versions.py @@ -200,14 +200,14 @@ def test_semanticversion_next_version_invalid() -> None: def _parametrize_ignore_version() -> ( - "dict[str, tuple[str, str, IgnoreVersions, IgnoreUpdateTypes, bool]]" + dict[str, tuple[str, str, IgnoreVersions, IgnoreUpdateTypes, bool]] ): """Utility function for `test_ignore_version()`. The parametrized inputs are created in this function in order to have more meaningful IDs in the runtime overview. """ - test_cases: "list[tuple[str, str, IgnoreVersions, IgnoreUpdateTypes, bool]]" = [ + test_cases: list[tuple[str, str, IgnoreVersions, IgnoreUpdateTypes, bool]] = [ ("1.1.1", "2.2.2", [{"operator": ">", "version": "2.2.2"}], {}, False), ("1.1.1", "2.2.2", [{"operator": ">", "version": "2.2"}], {}, True), ("1.1.1", "2.2.2", [{"operator": ">", "version": "2"}], {}, True), @@ -724,7 +724,7 @@ def _parametrize_ignore_version() -> ( ), ("1.1.1", "1.1.2", [], {}, True), ] - res: "dict[str, tuple[str, str, IgnoreVersions, IgnoreUpdateTypes, bool]]" = {} + res: dict[str, tuple[str, str, IgnoreVersions, IgnoreUpdateTypes, bool]] = {} for test_case in test_cases: if test_case[2] and test_case[3]: operator_version = ",".join( @@ -756,8 +756,8 @@ def _parametrize_ignore_version() -> ( def test_ignore_version( current: str, latest: str, - version_rules: "IgnoreVersions", - semver_rules: "IgnoreUpdateTypes", + version_rules: IgnoreVersions, + semver_rules: IgnoreUpdateTypes, expected_outcome: bool, ) -> None: """Check the expected ignore rules are resolved correctly.""" @@ -886,7 +886,7 @@ def test_ignore_version( def test_parse_ignore_entries( entries: list[str], separator: str, - expected_outcome: "dict[str, IgnoreEntry]", + expected_outcome: dict[str, IgnoreEntry], ) -> None: """Check the `--ignore` option values are parsed as expected.""" from ci_cd.utils.versions import parse_ignore_entries @@ -950,8 +950,8 @@ def test_parse_ignore_entries( ], ) def test_parse_ignore_rules( - rules: "IgnoreRules", - expected_outcome: "tuple[IgnoreVersions, IgnoreUpdateTypes]", + rules: IgnoreRules, + expected_outcome: tuple[IgnoreVersions, IgnoreUpdateTypes], ) -> None: """Check a specific set of ignore rules is parsed as expected.""" from ci_cd.utils.versions import parse_ignore_rules