diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index ea1e214ea..2c344a9cd 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -368,7 +368,9 @@ jobs: - name: Setup | Install dependencies run: | python -m pip install --upgrade pip setuptools wheel - pip install -e .[dev,mypy] + pip install -e .[dev,mypy,test] + # needs test because we run mypy over the tests as well and without the dependencies + # mypy will throw import errors - name: Lint | Ruff Evaluation id: lint @@ -382,7 +384,7 @@ jobs: id: type-check if: ${{ always() && steps.lint.outcome != 'skipped' }} run: | - mypy --ignore-missing-imports src/ + mypy . - name: Format-Check | Ruff Evaluation id: format-check diff --git a/docs/conf.py b/docs/conf.py index 997a23911..f9e418002 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ apidoc_extra_args = ["-d", "3"] -def setup(app): # noqa: ARG001,ANN001,ANN201 +def setup(app): # type: ignore[no-untyped-def] # noqa: ARG001,ANN001,ANN201 pass diff --git a/pyproject.toml b/pyproject.toml index 9a43dc654..657725265 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,6 +84,7 @@ dev = [ mypy = [ "mypy == 1.13.0", "types-requests ~= 2.32.0", + "types-pyyaml ~= 6.0", ] @@ -152,7 +153,7 @@ commands = [testenv:mypy] deps = .[mypy] commands = - mypy src/ + mypy . [testenv:coverage] deps = coverage[toml] @@ -175,7 +176,6 @@ pretty = true error_summary = true follow_imports = "normal" enable_error_code = ["ignore-without-code"] -ignore_missing_imports = true # gitpython is very dynamic disallow_untyped_calls = true # warn_return_any = true strict_optional = true @@ -191,9 +191,15 @@ plugins = ["pydantic.mypy"] [[tool.mypy.overrides]] module = "tests.*" -allow_untyped_defs = true -allow_incomplete_defs = true -allow_untyped_calls = true +disallow_untyped_defs = false + +[[tool.mypy.overrides]] +module = "flatdict" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "shellingham" +ignore_missing_imports = true [tool.ruff] line-length = 88 diff --git a/scripts/bump_version_in_docs.py b/scripts/bump_version_in_docs.py index 155444f12..30874d345 100644 --- a/scripts/bump_version_in_docs.py +++ b/scripts/bump_version_in_docs.py @@ -11,12 +11,12 @@ def update_github_actions_example(filepath: Path, new_version: str) -> None: psr_regex = RegExp(r"(uses:.*python-semantic-release)@v\d+\.\d+\.\d+") - file_content_lines = filepath.read_text().splitlines() + file_content_lines: list[str] = filepath.read_text().splitlines() for regex in [psr_regex]: file_content_lines = list( map( - lambda line, regex=regex: regex.sub(r"\1@v" + new_version, line), + lambda line, regex=regex: regex.sub(r"\1@v" + new_version, line), # type: ignore[misc] file_content_lines, ) ) diff --git a/tests/conftest.py b/tests/conftest.py index 11041100d..858f9efd6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -448,9 +448,9 @@ def _get_md5_for_set_of_files(files: Sequence[Path | str]) -> str: @pytest.fixture(scope="session") def clean_os_environment() -> dict[str, str]: - return dict( # type: ignore + return dict( filter( - lambda k_v: k_v[1] is not None, + lambda k_v: k_v[1] is not None, # type: ignore[arg-type] { "PATH": os.getenv("PATH"), "HOME": os.getenv("HOME"), diff --git a/tests/e2e/cmd_config/test_generate_config.py b/tests/e2e/cmd_config/test_generate_config.py index c928827e6..cac6778bc 100644 --- a/tests/e2e/cmd_config/test_generate_config.py +++ b/tests/e2e/cmd_config/test_generate_config.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: from typing import Any - from tests.command_line.conftest import CliRunner + from click.testing import CliRunner @pytest.fixture diff --git a/tests/fixtures/git_repo.py b/tests/fixtures/git_repo.py index 294d6d499..5d30890d1 100644 --- a/tests/fixtures/git_repo.py +++ b/tests/fixtures/git_repo.py @@ -160,7 +160,7 @@ def __call__( self, build_definition: Sequence[RepoActions] ) -> RepoDefinition: ... - RepoDefinition: TypeAlias = dict[VersionStr, RepoVersionDef] + RepoDefinition: TypeAlias = dict[VersionStr, RepoVersionDef] # type: ignore[misc] # mypy is thoroughly confused """ A Type alias to define a repositories versions, commits, and changelog sections for a specific commit convention @@ -622,10 +622,10 @@ def _format_squash_commit_msg_github( pr_number: int, squashed_commits: list[CommitDef | str], ) -> str: - sq_cmts: list[str] = ( # type: ignore - squashed_commits - if not isinstance(squashed_commits[0], dict) - else [commit["msg"] for commit in squashed_commits] # type: ignore + sq_cmts: list[str] = ( + squashed_commits # type: ignore[assignment] + if len(squashed_commits) > 1 and not isinstance(squashed_commits[0], dict) + else [commit["msg"] for commit in squashed_commits] # type: ignore[index] ) return ( str.join( @@ -1015,9 +1015,9 @@ def expand_repo_construction_steps( *acc, *( reduce( - expand_repo_construction_steps, + expand_repo_construction_steps, # type: ignore[arg-type] step["details"]["pre_actions"], - [], # type: ignore[arg-type] + [], ) if "pre_actions" in step["details"] else [] @@ -1025,9 +1025,9 @@ def expand_repo_construction_steps( step, *( reduce( - expand_repo_construction_steps, + expand_repo_construction_steps, # type: ignore[arg-type] step["details"]["post_actions"], - [], # type: ignore[arg-type] + [], ) if "post_actions" in step["details"] else [] diff --git a/tests/unit/semantic_release/changelog/test_template.py b/tests/unit/semantic_release/changelog/test_template.py index 9e9b29bef..32c6ab4dc 100644 --- a/tests/unit/semantic_release/changelog/test_template.py +++ b/tests/unit/semantic_release/changelog/test_template.py @@ -49,9 +49,9 @@ "subjects", [("dogs", "cats"), ("stocks", "finance", "politics")] ) def test_template_env_configurable(format_map: dict[str, Any], subjects: tuple[str]): - template = EXAMPLE_TEMPLATE_FORMAT_STR.format_map(format_map) + template_as_str = EXAMPLE_TEMPLATE_FORMAT_STR.format_map(format_map) env = environment(**format_map) - template = env.from_string(template) + template = env.from_string(template_as_str) title = "important" newline = "\n" diff --git a/tests/unit/semantic_release/cli/test_config.py b/tests/unit/semantic_release/cli/test_config.py index 8adeaa7ef..369958290 100644 --- a/tests/unit/semantic_release/cli/test_config.py +++ b/tests/unit/semantic_release/cli/test_config.py @@ -40,7 +40,7 @@ from typing import Any from tests.fixtures.example_project import ExProjectDir, UpdatePyprojectTomlFn - from tests.fixtures.git_repo import BuildRepoFn + from tests.fixtures.git_repo import BuildRepoFn, CommitConvention @pytest.mark.parametrize( @@ -230,7 +230,7 @@ def test_load_valid_runtime_config( ], ) def test_load_valid_runtime_config_w_custom_parser( - commit_parser: str, + commit_parser: CommitConvention, build_configured_base_repo: BuildRepoFn, example_project_dir: ExProjectDir, example_pyproject_toml: Path, diff --git a/tests/unit/semantic_release/commit_parser/test_scipy.py b/tests/unit/semantic_release/commit_parser/test_scipy.py index 976348981..f160bc8d8 100644 --- a/tests/unit/semantic_release/commit_parser/test_scipy.py +++ b/tests/unit/semantic_release/commit_parser/test_scipy.py @@ -21,7 +21,7 @@ def test_valid_scipy_parsed_chore_commits( default_scipy_parser: ScipyCommitParser, make_commit_obj: MakeCommitObjFn, - scipy_chore_commit_parts: list[list[str]], + scipy_chore_commit_parts: list[tuple[str, str, list[str]]], scipy_chore_commits: list[str], ): expected_parts = scipy_chore_commit_parts @@ -34,7 +34,7 @@ def test_valid_scipy_parsed_chore_commits( subject, *[body.rstrip() for body in commit_bodies if body], ] - expected_brk_desc = [] + expected_brk_desc: list[str] = [] commit = make_commit_obj(full_commit_msg) result = default_scipy_parser.parse(commit) @@ -50,7 +50,7 @@ def test_valid_scipy_parsed_chore_commits( def test_valid_scipy_parsed_patch_commits( default_scipy_parser: ScipyCommitParser, make_commit_obj: MakeCommitObjFn, - scipy_patch_commit_parts: list[list[str]], + scipy_patch_commit_parts: list[tuple[str, str, list[str]]], scipy_patch_commits: list[str], ): expected_parts = scipy_patch_commit_parts @@ -63,7 +63,7 @@ def test_valid_scipy_parsed_patch_commits( subject, *[body.rstrip() for body in commit_bodies if body], ] - expected_brk_desc = [] + expected_brk_desc: list[str] = [] commit = make_commit_obj(full_commit_msg) result = default_scipy_parser.parse(commit) @@ -79,7 +79,7 @@ def test_valid_scipy_parsed_patch_commits( def test_valid_scipy_parsed_minor_commits( default_scipy_parser: ScipyCommitParser, make_commit_obj: MakeCommitObjFn, - scipy_minor_commit_parts: list[list[str]], + scipy_minor_commit_parts: list[tuple[str, str, list[str]]], scipy_minor_commits: list[str], ): expected_parts = scipy_minor_commit_parts @@ -92,7 +92,7 @@ def test_valid_scipy_parsed_minor_commits( subject, *[body for body in commit_bodies if body], ] - expected_brk_desc = [] + expected_brk_desc: list[str] = [] commit = make_commit_obj(full_commit_msg) result = default_scipy_parser.parse(commit) @@ -108,7 +108,7 @@ def test_valid_scipy_parsed_minor_commits( def test_valid_scipy_parsed_major_commits( default_scipy_parser: ScipyCommitParser, make_commit_obj: MakeCommitObjFn, - scipy_major_commit_parts: list[list[str]], + scipy_major_commit_parts: list[tuple[str, str, list[str]]], scipy_major_commits: list[str], ): expected_parts = scipy_major_commit_parts diff --git a/tests/unit/semantic_release/hvcs/test_gitlab.py b/tests/unit/semantic_release/hvcs/test_gitlab.py index 01d688eae..c4a0979fe 100644 --- a/tests/unit/semantic_release/hvcs/test_gitlab.py +++ b/tests/unit/semantic_release/hvcs/test_gitlab.py @@ -22,7 +22,6 @@ if TYPE_CHECKING: from typing import Generator -gitlab.Gitlab("") # instantiation necessary to discover gitlab ProjectManager # Note: there's nothing special about the value of these variables, # they're just constants for easier consistency with the faked objects @@ -30,119 +29,12 @@ A_BAD_TAG = "v2.1.1-rc.1" A_LOCKED_TAG = "v0.9.0" A_MISSING_TAG = "v1.0.0+missing" -AN_EXISTING_TAG = "v2.3.4+existing" # But note this is the only ref we're making a "fake" commit for, so # tests which need to query the remote for "a" ref, the exact sha for # which doesn't matter, all use this constant REF = "hashashash" -class _GitlabProject: - def __init__(self, status): - self.commits = {REF: self._Commit(status)} - self.tags = self._Tags() - self.releases = self._Releases() - - class _Commit: - def __init__(self, status): - self.statuses = self._Statuses(status) - - class _Statuses: - def __init__(self, status): - if status == "pending": - self.jobs = [ - { - "name": "good_job", - "status": "passed", - "allow_failure": False, - }, - { - "name": "slow_job", - "status": "pending", - "allow_failure": False, - }, - ] - elif status == "failure": - self.jobs = [ - { - "name": "good_job", - "status": "passed", - "allow_failure": False, - }, - {"name": "bad_job", "status": "failed", "allow_failure": False}, - ] - elif status == "allow_failure": - self.jobs = [ - { - "name": "notsobad_job", - "status": "failed", - "allow_failure": True, - }, - { - "name": "good_job2", - "status": "passed", - "allow_failure": False, - }, - ] - elif status == "success": - self.jobs = [ - { - "name": "good_job1", - "status": "passed", - "allow_failure": True, - }, - { - "name": "good_job2", - "status": "passed", - "allow_failure": False, - }, - ] - - def list(self): - return self.jobs - - class _Tags: - def __init__(self): - pass - - def get(self, tag): - if tag in (A_GOOD_TAG, AN_EXISTING_TAG): - return self._Tag() - if tag == A_LOCKED_TAG: - return self._Tag(locked=True) - raise gitlab.exceptions.GitlabGetError() - - class _Tag: - def __init__(self, locked=False): - self.locked = locked - - def set_release_description(self, _): - if self.locked: - raise gitlab.exceptions.GitlabUpdateError() - - class _Releases: - def __init__(self): - pass - - def create(self, input_): - if ( - input_["name"] - and input_["tag_name"] - and input_["tag_name"] in (A_GOOD_TAG, A_LOCKED_TAG) - ): - return self._Release() - raise gitlab.exceptions.GitlabCreateError() - - def update(self, tag, _): - if tag == A_MISSING_TAG: - raise gitlab.exceptions.GitlabUpdateError() - return self._Release() - - class _Release: - def __init__(self, locked=False): - pass - - @pytest.fixture def default_gl_project(example_git_https_url: str): return gitlab.Gitlab(url=example_git_https_url).projects.get( @@ -428,12 +320,10 @@ def test_create_release_fails_with_bad_tag( @pytest.mark.parametrize("tag", (A_GOOD_TAG, A_LOCKED_TAG)) -def test_update_release_succeeds( - default_gl_client: Gitlab, default_gl_project: gitlab.v4.objects.Project, tag: str -): - fake_release_obj = gitlab.v4.objects.ProjectReleaseManager(default_gl_project).get( - tag, lazy=True - ) +def test_update_release_succeeds(default_gl_client: Gitlab, tag: str): + fake_release_obj = gitlab.v4.objects.ProjectReleaseManager( + default_gl_client._client + ).get(tag, lazy=True) fake_release_obj._attrs["name"] = tag with mock.patch.object( diff --git a/tests/unit/semantic_release/version/test_algorithm.py b/tests/unit/semantic_release/version/test_algorithm.py index 9db7dd12f..c48d89e22 100644 --- a/tests/unit/semantic_release/version/test_algorithm.py +++ b/tests/unit/semantic_release/version/test_algorithm.py @@ -70,7 +70,7 @@ class TagReferenceOverride(TagReference): ) # Verify - assert expected_version == actual + assert expected_version == (actual or "") @pytest.mark.parametrize( diff --git a/tests/unit/semantic_release/version/test_translator.py b/tests/unit/semantic_release/version/test_translator.py index 01438b4c3..d3a6cf666 100644 --- a/tests/unit/semantic_release/version/test_translator.py +++ b/tests/unit/semantic_release/version/test_translator.py @@ -79,19 +79,16 @@ def test_translator_converts_versions_with_default_formatting_rules( tag_format=tag_format, prerelease_token=prerelease_token ) - assert translator.from_string(version_string) == Version.parse( + expected_version_obj = Version.parse( version_string, prerelease_token=translator.prerelease_token ) + expected_tag = tag_format.format(version=version_string) + actual_version_obj = translator.from_string(version_string) + actual_tag = translator.str_to_tag(version_string) # These are important assumptions for formatting into source files/tags/etc - assert str(translator.from_string(version_string)) == version_string - assert translator.str_to_tag(version_string) == tag_format.format( - version=version_string - ) - assert translator.from_tag( - tag_format.format(version=version_string) - ) == Version.parse(version_string, prerelease_token=translator.prerelease_token) - assert ( - str(translator.from_tag(translator.str_to_tag(version_string))) - == version_string - ) + assert version_string == str(actual_version_obj) + assert expected_version_obj == actual_version_obj + assert expected_tag == actual_tag + assert expected_version_obj == (translator.from_tag(expected_tag) or "") + assert version_string == str(translator.from_tag(actual_tag) or "") diff --git a/tests/util.py b/tests/util.py index 5d277209a..b49942171 100644 --- a/tests/util.py +++ b/tests/util.py @@ -11,12 +11,11 @@ from textwrap import indent from typing import TYPE_CHECKING, Tuple -from git import Repo +from git import Git, Repo from pydantic.dataclasses import dataclass from semantic_release.changelog.context import ChangelogMode, make_changelog_context from semantic_release.changelog.release_history import ReleaseHistory -from semantic_release.cli import config as cli_config_module from semantic_release.commit_parser._base import CommitParser, ParserOptions from semantic_release.commit_parser.token import ParsedCommit, ParseResult from semantic_release.enums import LevelBump @@ -28,10 +27,10 @@ from typing import Any, Callable, Generator, Iterable, TypeVar try: - from typing import TypeAlias - except ImportError: - # for python 3.8 and 3.9 + # Python 3.8 and 3.9 compatibility from typing_extensions import TypeAlias + except ImportError: + from typing import TypeAlias # type: ignore[attr-defined, no-redef] from unittest.mock import MagicMock @@ -43,7 +42,7 @@ _R = TypeVar("_R") - GitCommandWrapperType: TypeAlias = cli_config_module.Repo.GitCommandWrapperType + GitCommandWrapperType: TypeAlias = Git def get_func_qual_name(func: Callable) -> str: @@ -125,8 +124,8 @@ def on_read_only_error(_func, path, _exc_info): def dynamic_python_import(file_path: Path, module_name: str): spec = importlib.util.spec_from_file_location(module_name, str(file_path)) - module = importlib.util.module_from_spec(spec) # type: ignore - spec.loader.exec_module(module) # type: ignore + module = importlib.util.module_from_spec(spec) # type: ignore[arg-type] + spec.loader.exec_module(module) # type: ignore[union-attr] return module @@ -233,7 +232,7 @@ def prepare_mocked_git_command_wrapper_type( >>> mocked_push.assert_called_once() """ - class MockGitCommandWrapperType(cli_config_module.Repo.GitCommandWrapperType): + class MockGitCommandWrapperType(Git): def __getattr__(self, name: str) -> Any: try: return object.__getattribute__(self, f"mocked_{name}")