From ec9745307970677228a970f072792fadba5eda55 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 8 Jun 2025 16:54:27 +0800 Subject: [PATCH 01/31] ci(mypy): fix linter error --- commitizen/config/json_config.py | 2 +- commitizen/config/toml_config.py | 2 +- commitizen/config/yaml_config.py | 2 +- poetry.lock | 2 +- pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/commitizen/config/json_config.py b/commitizen/config/json_config.py index d413d73383..28fac25662 100644 --- a/commitizen/config/json_config.py +++ b/commitizen/config/json_config.py @@ -13,7 +13,7 @@ class JsonConfig(BaseConfig): def __init__(self, *, data: bytes | str, path: Path | str): super().__init__() self.is_empty_config = False - self.path = path # type: ignore + self.path = path self._parse_setting(data) def init_empty_config_content(self): diff --git a/commitizen/config/toml_config.py b/commitizen/config/toml_config.py index e2cfcc9340..a7050214ba 100644 --- a/commitizen/config/toml_config.py +++ b/commitizen/config/toml_config.py @@ -14,7 +14,7 @@ class TomlConfig(BaseConfig): def __init__(self, *, data: bytes | str, path: Path | str): super().__init__() self.is_empty_config = False - self.path = path # type: ignore + self.path = path self._parse_setting(data) def init_empty_config_content(self): diff --git a/commitizen/config/yaml_config.py b/commitizen/config/yaml_config.py index c5721c8d4b..2c384cf17d 100644 --- a/commitizen/config/yaml_config.py +++ b/commitizen/config/yaml_config.py @@ -14,7 +14,7 @@ class YAMLConfig(BaseConfig): def __init__(self, *, data: bytes | str, path: Path | str): super().__init__() self.is_empty_config = False - self.path = path # type: ignore + self.path = path self._parse_setting(data) def init_empty_config_content(self): diff --git a/poetry.lock b/poetry.lock index 6a31d292fe..cbe0fb0e3c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2014,4 +2014,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9,<4.0" -content-hash = "146cae2abe0cdb8a831b9ece0aa1ee98366e3e4e17ef734f0e7001c4eb7508b3" +content-hash = "6eea6e061a2ece897c2fb37b0b5116085057e6d97e1f22c0fcd4294fb0ad1dbf" diff --git a/pyproject.toml b/pyproject.toml index 144868e197..1a29ca6bf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,7 +121,7 @@ deprecated = "^1.2.13" [tool.poetry.group.linters.dependencies] ruff = ">=0.5.0,<0.10.0" pre-commit = ">=2.18,<5.0" -mypy = "^1.15.0" +mypy = "^1.16.0" types-deprecated = "^1.2.9.2" types-python-dateutil = "^2.8.19.13" types-PyYAML = ">=5.4.3,<7.0.0" From 304c98c205b86aca9c6727daf2df31467d8d6d94 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Thu, 29 May 2025 22:13:11 +0800 Subject: [PATCH 02/31] style(tags): improve types --- commitizen/tags.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/commitizen/tags.py b/commitizen/tags.py index c5f06884fe..4231e0053e 100644 --- a/commitizen/tags.py +++ b/commitizen/tags.py @@ -2,7 +2,7 @@ import re import warnings -from collections.abc import Sequence +from collections.abc import Iterable, Sequence from dataclasses import dataclass, field from functools import cached_property from string import Template @@ -89,14 +89,14 @@ class TagRules: merge_prereleases: bool = False @cached_property - def version_regexes(self) -> Sequence[re.Pattern]: + def version_regexes(self) -> list[re.Pattern]: """Regexes for all legit tag formats, current and legacy""" tag_formats = [self.tag_format, *self.legacy_tag_formats] regexes = (self._format_regex(p) for p in tag_formats) return [re.compile(r) for r in regexes] @cached_property - def ignored_regexes(self) -> Sequence[re.Pattern]: + def ignored_regexes(self) -> list[re.Pattern]: """Regexes for known but ignored tag formats""" regexes = (self._format_regex(p, star=True) for p in self.ignored_tag_formats) return [re.compile(r) for r in regexes] @@ -135,8 +135,8 @@ def is_ignored_tag(self, tag: str | GitTag) -> bool: return any(regex.match(tag) for regex in self.ignored_regexes) def get_version_tags( - self, tags: Sequence[GitTag], warn: bool = False - ) -> Sequence[GitTag]: + self, tags: Iterable[GitTag], warn: bool = False + ) -> list[GitTag]: """Filter in version tags and warn on unexpected tags""" return [tag for tag in tags if self.is_version_tag(tag, warn)] @@ -236,7 +236,7 @@ def normalize_tag( ) def find_tag_for( - self, tags: Sequence[GitTag], version: Version | str + self, tags: Iterable[GitTag], version: Version | str ) -> GitTag | None: """Find the first matching tag for a given version.""" version = self.scheme(version) if isinstance(version, str) else version From ebaa152058790be2df2dbe2fcb2a9e000c41d66e Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Thu, 29 May 2025 22:18:06 +0800 Subject: [PATCH 03/31] perf(tags): use set --- commitizen/tags.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commitizen/tags.py b/commitizen/tags.py index 4231e0053e..b19bb89e09 100644 --- a/commitizen/tags.py +++ b/commitizen/tags.py @@ -240,11 +240,11 @@ def find_tag_for( ) -> GitTag | None: """Find the first matching tag for a given version.""" version = self.scheme(version) if isinstance(version, str) else version - possible_tags = [ + possible_tags = set( self.normalize_tag(version, f) for f in (self.tag_format, *self.legacy_tag_formats) - ] - candidates = [t for t in tags if any(t.name == p for p in possible_tags)] + ) + candidates = [t for t in tags if t.name in possible_tags] if len(candidates) > 1: warnings.warn( UserWarning( From 76cc333355f8c4f70a675df83c41698ac2faa1a6 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Thu, 29 May 2025 23:20:33 +0800 Subject: [PATCH 04/31] refactor: fix mypy output and better type --- commitizen/changelog.py | 10 ++--- commitizen/cli.py | 43 ++++++++++++++++--- commitizen/cmd.py | 5 ++- commitizen/commands/changelog.py | 24 ++++++----- commitizen/commands/check.py | 10 +++-- commitizen/commands/commit.py | 14 +++--- commitizen/commands/info.py | 4 +- commitizen/commands/init.py | 8 ++-- commitizen/commands/list_cz.py | 4 +- commitizen/commands/version.py | 6 ++- commitizen/config/base_config.py | 2 +- .../conventional_commits.py | 4 +- commitizen/cz/utils.py | 5 ++- commitizen/exceptions.py | 7 +-- commitizen/git.py | 20 +++++---- commitizen/out.py | 7 +-- tests/test_git.py | 7 +++ 17 files changed, 117 insertions(+), 63 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index bac4d824f9..6526b4c540 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -29,7 +29,7 @@ import re from collections import OrderedDict, defaultdict -from collections.abc import Generator, Iterable, Mapping +from collections.abc import Generator, Iterable, Mapping, Sequence from dataclasses import dataclass from datetime import date from typing import TYPE_CHECKING, Any @@ -225,7 +225,7 @@ def render_changelog( tree: Iterable, loader: BaseLoader, template: str, - **kwargs, + **kwargs: Any, ) -> str: jinja_template = get_changelog_template(loader, template) changelog: str = jinja_template.render(tree=tree, **kwargs) @@ -282,7 +282,7 @@ def incremental_build( def get_smart_tag_range( - tags: list[GitTag], newest: str, oldest: str | None = None + tags: Sequence[GitTag], newest: str, oldest: str | None = None ) -> list[GitTag]: """Smart because it finds the N+1 tag. @@ -308,10 +308,10 @@ def get_smart_tag_range( def get_oldest_and_newest_rev( - tags: list[GitTag], + tags: Sequence[GitTag], version: str, rules: TagRules, -) -> tuple[str | None, str | None]: +) -> tuple[str | None, str]: """Find the tags for the given version. `version` may come in different formats: diff --git a/commitizen/cli.py b/commitizen/cli.py index deb5644d27..cea15f1a22 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -8,7 +8,7 @@ from functools import partial from pathlib import Path from types import TracebackType -from typing import Any +from typing import TYPE_CHECKING, Any import argcomplete from decli import cli @@ -50,7 +50,7 @@ def __call__( namespace: argparse.Namespace, kwarg: str | Sequence[Any] | None, option_string: str | None = None, - ): + ) -> None: if not isinstance(kwarg, str): return if "=" not in kwarg: @@ -550,8 +550,12 @@ def __call__( def commitizen_excepthook( - type, value, traceback, debug=False, no_raise: list[int] | None = None -): + type: type[BaseException], + value: BaseException, + traceback: TracebackType | None, + debug: bool = False, + no_raise: list[int] | None = None, +) -> None: traceback = traceback if isinstance(traceback, TracebackType) else None if not isinstance(value, CommitizenException): original_excepthook(type, value, traceback) @@ -581,7 +585,7 @@ def parse_no_raise(comma_separated_no_raise: str) -> list[int]: represents the exit code found in exceptions. """ no_raise_items: list[str] = comma_separated_no_raise.split(",") - no_raise_codes = [] + no_raise_codes: list[int] = [] for item in no_raise_items: if item.isdecimal(): no_raise_codes.append(int(item)) @@ -596,8 +600,33 @@ def parse_no_raise(comma_separated_no_raise: str) -> list[int]: return no_raise_codes -def main(): - parser = cli(data) +if TYPE_CHECKING: + + class Args(argparse.Namespace): + config: str | None = None + debug: bool = False + name: str | None = None + no_raise: str | None = None # comma-separated string, later parsed as list[int] + report: bool = False + project: bool = False + commitizen: bool = False + verbose: bool = False + func: type[ + commands.Init # init + | commands.Commit # commit (c) + | commands.ListCz # ls + | commands.Example # example + | commands.Info # info + | commands.Schema # schema + | commands.Bump # bump + | commands.Changelog # changelog (ch) + | commands.Check # check + | commands.Version # version + ] + + +def main() -> None: + parser: argparse.ArgumentParser = cli(data) argcomplete.autocomplete(parser) # Show help if no arg provided if len(sys.argv) == 1: diff --git a/commitizen/cmd.py b/commitizen/cmd.py index ba48ac7881..9d6b2f6d2a 100644 --- a/commitizen/cmd.py +++ b/commitizen/cmd.py @@ -1,6 +1,7 @@ import os import subprocess -from typing import NamedTuple +from collections.abc import Mapping +from typing import NamedTuple, Union from charset_normalizer import from_bytes @@ -28,7 +29,7 @@ def _try_decode(bytes_: bytes) -> str: raise CharacterSetDecodeError() from e -def run(cmd: str, env=None) -> Command: +def run(cmd: str, env: Union[Mapping[str, str], None] = None) -> Command: if env is not None: env = {**os.environ, **env} process = subprocess.Popen( diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 7bf644a934..181531dee2 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -2,10 +2,11 @@ import os import os.path -from collections.abc import Generator +from collections.abc import Generator, Iterable, Mapping from difflib import SequenceMatcher from operator import itemgetter from pathlib import Path +from typing import Any, cast from commitizen import changelog, defaults, factory, git, out from commitizen.changelog_formats import get_changelog_format @@ -28,7 +29,7 @@ class Changelog: """Generate a changelog based on the commit history.""" - def __init__(self, config: BaseConfig, args): + def __init__(self, config: BaseConfig, args: Mapping[str, Any]): if not git.is_git_project(): raise NotAGitProjectError() @@ -76,10 +77,11 @@ def __init__(self, config: BaseConfig, args): self.change_type_map = ( self.config.settings.get("change_type_map") or self.cz.change_type_map ) - self.change_type_order = ( + self.change_type_order = cast( + list[str], self.config.settings.get("change_type_order") or self.cz.change_type_order - or defaults.CHANGE_TYPE_ORDER + or defaults.CHANGE_TYPE_ORDER, ) self.rev_range = args.get("rev_range") self.tag_format: str = ( @@ -102,7 +104,7 @@ def __init__(self, config: BaseConfig, args): self.extras = args.get("extras") or {} self.export_template_to = args.get("export_template") - def _find_incremental_rev(self, latest_version: str, tags: list[GitTag]) -> str: + def _find_incremental_rev(self, latest_version: str, tags: Iterable[GitTag]) -> str: """Try to find the 'start_rev'. We use a similarity approach. We know how to parse the version from the markdown @@ -151,18 +153,18 @@ def write_changelog( changelog_file.write(changelog_out) - def export_template(self): + def export_template(self) -> None: tpl = changelog.get_changelog_template(self.cz.template_loader, self.template) - src = Path(tpl.filename) - Path(self.export_template_to).write_text(src.read_text()) + src = Path(tpl.filename) # type: ignore + Path(self.export_template_to).write_text(src.read_text()) # type: ignore - def __call__(self): + def __call__(self) -> None: commit_parser = self.cz.commit_parser changelog_pattern = self.cz.changelog_pattern start_rev = self.start_rev unreleased_version = self.unreleased_version changelog_meta = changelog.Metadata() - change_type_map: dict | None = self.change_type_map + change_type_map: dict[str, str] | None = self.change_type_map # type: ignore changelog_message_builder_hook: MessageBuilderHook | None = ( self.cz.changelog_message_builder_hook ) @@ -190,7 +192,7 @@ def __call__(self): changelog_meta = self.changelog_format.get_metadata(self.file_name) if changelog_meta.latest_version: start_rev = self._find_incremental_rev( - strip_local_version(changelog_meta.latest_version_tag), tags + strip_local_version(changelog_meta.latest_version_tag or ""), tags ) if self.rev_range: start_rev, end_rev = changelog.get_oldest_and_newest_rev( diff --git a/commitizen/commands/check.py b/commitizen/commands/check.py index 1e3b8464e1..6cf91bb926 100644 --- a/commitizen/commands/check.py +++ b/commitizen/commands/check.py @@ -17,7 +17,9 @@ class Check: """Check if the current commit msg matches the commitizen format.""" - def __init__(self, config: BaseConfig, arguments: dict[str, Any], cwd=os.getcwd()): + def __init__( + self, config: BaseConfig, arguments: dict[str, Any], cwd: str = os.getcwd() + ): """Initial check command. Args: @@ -48,7 +50,7 @@ def __init__(self, config: BaseConfig, arguments: dict[str, Any], cwd=os.getcwd( self.encoding = config.settings["encoding"] self.cz = factory.committer_factory(self.config) - def _valid_command_argument(self): + def _valid_command_argument(self) -> None: num_exclusive_args_provided = sum( arg is not None for arg in (self.commit_msg_file, self.commit_msg, self.rev_range) @@ -61,7 +63,7 @@ def _valid_command_argument(self): "See 'cz check -h' for more information" ) - def __call__(self): + def __call__(self) -> None: """Validate if commit messages follows the conventional pattern. Raises: @@ -97,7 +99,7 @@ def _get_commit_message(self) -> str | None: # Get commit message from file (--commit-msg-file) return commit_file.read() - def _get_commits(self): + def _get_commits(self) -> list[git.GitCommit]: if (msg := self._get_commit_message()) is not None: return [git.GitCommit(rev="", title="", body=self._filter_comments(msg))] diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index eedf77e079..3e7a850a05 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -5,6 +5,8 @@ import shutil import subprocess import tempfile +from pathlib import Path +from typing import Union, cast import questionary @@ -105,11 +107,13 @@ def _get_message(self) -> str: return self.read_backup_message() or self.prompt_commit_questions() return self.prompt_commit_questions() - def __call__(self): - extra_args: str = self.arguments.get("extra_cli_args", "") - dry_run: bool = self.arguments.get("dry_run") - write_message_to_file: bool = self.arguments.get("write_message_to_file") - signoff: bool = self.arguments.get("signoff") + def __call__(self) -> None: + extra_args = cast(str, self.arguments.get("extra_cli_args", "")) + dry_run = cast(bool, self.arguments.get("dry_run")) + write_message_to_file = cast( + Union[Path, None], self.arguments.get("write_message_to_file") + ) + signoff = cast(bool, self.arguments.get("signoff")) if self.arguments.get("all"): git.add("-u") diff --git a/commitizen/commands/info.py b/commitizen/commands/info.py index abd4197e7f..5b649ceb5d 100644 --- a/commitizen/commands/info.py +++ b/commitizen/commands/info.py @@ -5,9 +5,9 @@ class Info: """Show in depth explanation of your rules.""" - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, *args: object): self.config: BaseConfig = config self.cz = factory.committer_factory(self.config) - def __call__(self): + def __call__(self) -> None: out.write(self.cz.info()) diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index 0eb3d99d17..a7e1b56665 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -207,7 +207,7 @@ def _ask_tag(self) -> str: raise NoAnswersError("Tag is required!") return latest_tag - def _ask_tag_format(self, latest_tag) -> str: + def _ask_tag_format(self, latest_tag: str) -> str: is_correct_format = False if latest_tag.startswith("v"): tag_format = r"v$version" @@ -302,7 +302,7 @@ def _ask_update_changelog_on_bump(self) -> bool: ).unsafe_ask() return update_changelog_on_bump - def _exec_install_pre_commit_hook(self, hook_types: list[str]): + def _exec_install_pre_commit_hook(self, hook_types: list[str]) -> None: cmd_str = self._gen_pre_commit_cmd(hook_types) c = cmd.run(cmd_str) if c.return_code != 0: @@ -323,7 +323,7 @@ def _gen_pre_commit_cmd(self, hook_types: list[str]) -> str: ) return cmd_str - def _install_pre_commit_hook(self, hook_types: list[str] | None = None): + def _install_pre_commit_hook(self, hook_types: list[str] | None = None) -> None: pre_commit_config_filename = ".pre-commit-config.yaml" cz_hook_config = { "repo": "https://github.com/commitizen-tools/commitizen", @@ -369,6 +369,6 @@ def _install_pre_commit_hook(self, hook_types: list[str] | None = None): self._exec_install_pre_commit_hook(hook_types) out.write("commitizen pre-commit hook is now installed in your '.git'\n") - def _update_config_file(self, values: dict[str, Any]): + def _update_config_file(self, values: dict[str, Any]) -> None: for key, value in values.items(): self.config.set_key(key, value) diff --git a/commitizen/commands/list_cz.py b/commitizen/commands/list_cz.py index 99701865af..ff4e12ad44 100644 --- a/commitizen/commands/list_cz.py +++ b/commitizen/commands/list_cz.py @@ -6,8 +6,8 @@ class ListCz: """List currently installed rules.""" - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, *args: object): self.config: BaseConfig = config - def __call__(self): + def __call__(self) -> None: out.write("\n".join(registry.keys())) diff --git a/commitizen/commands/version.py b/commitizen/commands/version.py index 45d553c710..55d0a950a3 100644 --- a/commitizen/commands/version.py +++ b/commitizen/commands/version.py @@ -1,5 +1,7 @@ import platform import sys +from collections.abc import Mapping +from typing import Any from commitizen import out from commitizen.__version__ import __version__ @@ -10,13 +12,13 @@ class Version: """Get the version of the installed commitizen or the current project.""" - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, *args: Mapping[str, Any]): self.config: BaseConfig = config self.parameter = args[0] self.operating_system = platform.system() self.python_version = sys.version - def __call__(self): + def __call__(self) -> None: if self.parameter.get("report"): out.write(f"Commitizen Version: {__version__}") out.write(f"Python Version: {self.python_version}") diff --git a/commitizen/config/base_config.py b/commitizen/config/base_config.py index fd034412fe..587b72aee6 100644 --- a/commitizen/config/base_config.py +++ b/commitizen/config/base_config.py @@ -6,7 +6,7 @@ class BaseConfig: - def __init__(self): + def __init__(self) -> None: self._settings: Settings = DEFAULT_SETTINGS.copy() self.encoding = self.settings["encoding"] self._path: Path | None = None diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index 9a2b9016b7..3f62c66c97 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -9,7 +9,7 @@ __all__ = ["ConventionalCommitsCz"] -def parse_scope(text): +def parse_scope(text: str) -> str: if not text: return "" @@ -20,7 +20,7 @@ def parse_scope(text): return "-".join(scope) -def parse_subject(text): +def parse_subject(text: str) -> str: if isinstance(text, str): text = text.strip(".").strip() diff --git a/commitizen/cz/utils.py b/commitizen/cz/utils.py index cb79d65d1a..430bda2d43 100644 --- a/commitizen/cz/utils.py +++ b/commitizen/cz/utils.py @@ -1,6 +1,7 @@ import os import re import tempfile +from typing import Union from commitizen import git from commitizen.cz import exceptions @@ -8,13 +9,13 @@ _RE_LOCAL_VERSION = re.compile(r"\+.+") -def required_validator(answer, msg=None): +def required_validator(answer: str, msg: Union[str, None] = None) -> str: if not answer: raise exceptions.AnswerRequiredError(msg) return answer -def multiple_line_breaker(answer, sep="|"): +def multiple_line_breaker(answer: str, sep: str = "|") -> str: return "\n".join(line.strip() for line in answer.split(sep) if line) diff --git a/commitizen/exceptions.py b/commitizen/exceptions.py index 29733b624b..87efabe2ab 100644 --- a/commitizen/exceptions.py +++ b/commitizen/exceptions.py @@ -1,4 +1,5 @@ import enum +from typing import Any from commitizen import out @@ -40,7 +41,7 @@ class ExitCode(enum.IntEnum): class CommitizenException(Exception): - def __init__(self, *args, **kwargs): + def __init__(self, *args: str, **kwargs: Any): self.output_method = kwargs.get("output_method") or out.error self.exit_code: ExitCode = self.__class__.exit_code if args: @@ -50,14 +51,14 @@ def __init__(self, *args, **kwargs): else: self.message = "" - def __str__(self): + def __str__(self) -> str: return self.message class ExpectedExit(CommitizenException): exit_code = ExitCode.EXPECTED_EXIT - def __init__(self, *args, **kwargs): + def __init__(self, *args: str, **kwargs: Any): output_method = kwargs.get("output_method") or out.write kwargs["output_method"] = output_method super().__init__(*args, **kwargs) diff --git a/commitizen/git.py b/commitizen/git.py index ab2866ac48..48fa13ec52 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -46,15 +46,15 @@ class GitObject: name: str date: str - def __eq__(self, other) -> bool: + def __eq__(self, other: object) -> bool: return hasattr(other, "rev") and self.rev == other.rev class GitCommit(GitObject): def __init__( self, - rev, - title, + rev: str, + title: str, body: str = "", author: str = "", author_email: str = "", @@ -68,7 +68,7 @@ def __init__( self.parents = parents or [] @property - def message(self): + def message(self) -> str: return f"{self.title}\n\n{self.body}".strip() @classmethod @@ -127,23 +127,27 @@ def from_rev_and_commit(cls, rev_and_commit: str) -> GitCommit: parents=[p for p in parents.strip().split(" ") if p], ) - def __repr__(self): + def __repr__(self) -> str: return f"{self.title} ({self.rev})" class GitTag(GitObject): - def __init__(self, name, rev, date): + def __init__(self, name: str, rev: str, date: str): self.rev = rev.strip() self.name = name.strip() self._date = date.strip() - def __repr__(self): + def __repr__(self) -> str: return f"GitTag('{self.name}', '{self.rev}', '{self.date}')" @property - def date(self): + def date(self) -> str: return self._date + @date.setter + def date(self, value: str) -> None: + self._date = value + @classmethod def from_line(cls, line: str, inner_delimiter: str) -> GitTag: name, objectname, date, obj = line.split(inner_delimiter) diff --git a/commitizen/out.py b/commitizen/out.py index 40342e9de5..1bbfe4329d 100644 --- a/commitizen/out.py +++ b/commitizen/out.py @@ -1,5 +1,6 @@ import io import sys +from typing import Any from termcolor import colored @@ -8,12 +9,12 @@ sys.stdout.reconfigure(encoding="utf-8") -def write(value: str, *args) -> None: +def write(value: str, *args: object) -> None: """Intended to be used when value is multiline.""" print(value, *args) -def line(value: str, *args, **kwargs) -> None: +def line(value: str, *args: object, **kwargs: Any) -> None: """Wrapper in case I want to do something different later.""" print(value, *args, **kwargs) @@ -33,7 +34,7 @@ def info(value: str) -> None: line(message) -def diagnostic(value: str): +def diagnostic(value: str) -> None: line(value, file=sys.stderr) diff --git a/tests/test_git.py b/tests/test_git.py index 3fecaabafd..e242b3a2ae 100644 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -18,6 +18,13 @@ ) +@pytest.mark.parametrize("date", ["2020-01-21", "1970-01-01"]) +def test_git_tag_date(date: str): + git_tag = git.GitTag(rev="sha1-code", name="0.0.1", date="2025-05-30") + git_tag.date = date + assert git_tag.date == date + + def test_git_object_eq(): git_commit = git.GitCommit( rev="sha1-code", title="this is title", body="this is body" From 21eb968b235d0093ab1bd01b6cb3bec58b230d5d Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Fri, 30 May 2025 01:25:06 +0800 Subject: [PATCH 05/31] refactor(conventional_commits): remove unnecessary checks --- .../conventional_commits.py | 22 +++----- tests/test_cz_conventional_commits.py | 54 +++++++++---------- 2 files changed, 30 insertions(+), 46 deletions(-) diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index 3f62c66c97..9296375349 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -9,22 +9,12 @@ __all__ = ["ConventionalCommitsCz"] -def parse_scope(text: str) -> str: - if not text: - return "" - - scope = text.strip().split() - if len(scope) == 1: - return scope[0] - - return "-".join(scope) - +def _parse_scope(text: str) -> str: + return "-".join(text.strip().split()) -def parse_subject(text: str) -> str: - if isinstance(text, str): - text = text.strip(".").strip() - return required_validator(text, msg="Subject is required.") +def _parse_subject(text: str) -> str: + return required_validator(text.strip(".").strip(), msg="Subject is required.") class ConventionalCommitsCz(BaseCommitizen): @@ -113,12 +103,12 @@ def questions(self) -> Questions: "message": ( "What is the scope of this change? (class or file name): (press [enter] to skip)\n" ), - "filter": parse_scope, + "filter": _parse_scope, }, { "type": "input", "name": "subject", - "filter": parse_subject, + "filter": _parse_subject, "message": ( "Write a short and imperative summary of the code changes: (lower case and no period)\n" ), diff --git a/tests/test_cz_conventional_commits.py b/tests/test_cz_conventional_commits.py index 6d4e0f7435..b37a0f0116 100644 --- a/tests/test_cz_conventional_commits.py +++ b/tests/test_cz_conventional_commits.py @@ -2,48 +2,42 @@ from commitizen.cz.conventional_commits.conventional_commits import ( ConventionalCommitsCz, - parse_scope, - parse_subject, + _parse_scope, + _parse_subject, ) from commitizen.cz.exceptions import AnswerRequiredError -valid_scopes = ["", "simple", "dash-separated", "camelCaseUPPERCASE"] -scopes_transformations = [["with spaces", "with-spaces"], [None, ""]] - -valid_subjects = ["this is a normal text", "aword"] - -subjects_transformations = [["with dot.", "with dot"]] - -invalid_subjects = ["", " ", ".", " .", "", None] - - -def test_parse_scope_valid_values(): - for valid_scope in valid_scopes: - assert valid_scope == parse_scope(valid_scope) +@pytest.mark.parametrize( + "valid_scope", ["", "simple", "dash-separated", "camelCaseUPPERCASE"] +) +def test_parse_scope_valid_values(valid_scope): + assert valid_scope == _parse_scope(valid_scope) -def test_scopes_transformations(): - for scopes_transformation in scopes_transformations: - invalid_scope, transformed_scope = scopes_transformation - assert transformed_scope == parse_scope(invalid_scope) +@pytest.mark.parametrize( + "scopes_transformation", [["with spaces", "with-spaces"], ["", ""]] +) +def test_scopes_transformations(scopes_transformation): + invalid_scope, transformed_scope = scopes_transformation + assert transformed_scope == _parse_scope(invalid_scope) -def test_parse_subject_valid_values(): - for valid_subject in valid_subjects: - assert valid_subject == parse_subject(valid_subject) +@pytest.mark.parametrize("valid_subject", ["this is a normal text", "aword"]) +def test_parse_subject_valid_values(valid_subject): + assert valid_subject == _parse_subject(valid_subject) -def test_parse_subject_invalid_values(): - for valid_subject in invalid_subjects: - with pytest.raises(AnswerRequiredError): - parse_subject(valid_subject) +@pytest.mark.parametrize("invalid_subject", ["", " ", ".", " .", "\t\t."]) +def test_parse_subject_invalid_values(invalid_subject): + with pytest.raises(AnswerRequiredError): + _parse_subject(invalid_subject) -def test_subject_transformations(): - for subject_transformation in subjects_transformations: - invalid_subject, transformed_subject = subject_transformation - assert transformed_subject == parse_subject(invalid_subject) +@pytest.mark.parametrize("subject_transformation", [["with dot.", "with dot"]]) +def test_subject_transformations(subject_transformation): + invalid_subject, transformed_subject = subject_transformation + assert transformed_subject == _parse_subject(invalid_subject) def test_questions(config): From 652fd828c2b06e526e85c7030e13860bb5de7f86 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Fri, 30 May 2025 14:25:50 +0800 Subject: [PATCH 06/31] refactor: make methods protected, better type --- commitizen/commands/bump.py | 10 +++++----- commitizen/commands/changelog.py | 8 ++++---- commitizen/commands/commit.py | 10 +++++----- commitizen/commands/init.py | 4 ++-- commitizen/config/base_config.py | 3 +++ tests/commands/test_bump_command.py | 10 +++++----- 6 files changed, 24 insertions(+), 21 deletions(-) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index a2d98c2451..bfe49d4438 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -101,7 +101,7 @@ def __init__(self, config: BaseConfig, arguments: dict): ) self.extras = arguments["extras"] - def is_initial_tag( + def _is_initial_tag( self, current_tag: git.GitTag | None, is_yes: bool = False ) -> bool: """Check if reading the whole git tree up to HEAD is needed.""" @@ -118,7 +118,7 @@ def is_initial_tag( ) return bool(questionary.confirm("Is this the first tag created?").ask()) - def find_increment(self, commits: list[git.GitCommit]) -> Increment | None: + def _find_increment(self, commits: list[git.GitCommit]) -> Increment | None: # Update the bump map to ensure major version doesn't increment. is_major_version_zero: bool = self.bump_settings["major_version_zero"] # self.cz.bump_map = defaults.bump_map_major_version_zero @@ -135,7 +135,7 @@ def find_increment(self, commits: list[git.GitCommit]) -> Increment | None: ) return bump.find_increment(commits, regex=bump_pattern, increments_map=bump_map) - def __call__(self) -> None: # noqa: C901 + def __call__(self) -> None: """Steps executed to bump.""" provider = get_provider(self.config) @@ -227,7 +227,7 @@ def __call__(self) -> None: # noqa: C901 current_tag, "name", rules.normalize_tag(current_version) ) - is_initial = self.is_initial_tag(current_tag, is_yes) + is_initial = self._is_initial_tag(current_tag, is_yes) if manual_version: try: @@ -255,7 +255,7 @@ def __call__(self) -> None: # noqa: C901 "[NO_COMMITS_FOUND]\nNo new commits found." ) - increment = self.find_increment(commits) + increment = self._find_increment(commits) # It may happen that there are commits, but they are not eligible # for an increment, this generates a problem when using prerelease (#281) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 181531dee2..11f72cba18 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -136,7 +136,7 @@ def _find_incremental_rev(self, latest_version: str, tags: Iterable[GitTag]) -> raise NoRevisionError() return start_rev - def write_changelog( + def _write_changelog( self, changelog_out: str, lines: list[str], changelog_meta: changelog.Metadata ): with smart_open(self.file_name, "w", encoding=self.encoding) as changelog_file: @@ -153,7 +153,7 @@ def write_changelog( changelog_file.write(changelog_out) - def export_template(self) -> None: + def _export_template(self) -> None: tpl = changelog.get_changelog_template(self.cz.template_loader, self.template) src = Path(tpl.filename) # type: ignore Path(self.export_template_to).write_text(src.read_text()) # type: ignore @@ -173,7 +173,7 @@ def __call__(self) -> None: ) if self.export_template_to: - return self.export_template() + return self._export_template() if not changelog_pattern or not commit_parser: raise NoPatternMapError( @@ -240,4 +240,4 @@ def __call__(self) -> None: with open(self.file_name, encoding=self.encoding) as changelog_file: lines = changelog_file.readlines() - self.write_changelog(changelog_out, lines, changelog_meta) + self._write_changelog(changelog_out, lines, changelog_meta) diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 3e7a850a05..22ad642c43 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -41,7 +41,7 @@ def __init__(self, config: BaseConfig, arguments: dict): self.arguments = arguments self.temp_file: str = get_backup_file_path() - def read_backup_message(self) -> str | None: + def _read_backup_message(self) -> str | None: # Check the commit backup file exists if not os.path.isfile(self.temp_file): return None @@ -50,7 +50,7 @@ def read_backup_message(self) -> str | None: with open(self.temp_file, encoding=self.encoding) as f: return f.read().strip() - def prompt_commit_questions(self) -> str: + def _prompt_commit_questions(self) -> str: # Prompt user for the commit message cz = self.cz questions = cz.questions() @@ -96,7 +96,7 @@ def manual_edit(self, message: str) -> str: def _get_message(self) -> str: if self.arguments.get("retry"): - m = self.read_backup_message() + m = self._read_backup_message() if m is None: raise NoCommitBackupError() return m @@ -104,8 +104,8 @@ def _get_message(self) -> str: if self.config.settings.get("retry_after_failure") and not self.arguments.get( "no_retry" ): - return self.read_backup_message() or self.prompt_commit_questions() - return self.prompt_commit_questions() + return self._read_backup_message() or self._prompt_commit_questions() + return self._prompt_commit_questions() def __call__(self) -> None: extra_args = cast(str, self.arguments.get("extra_cli_args", "")) diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index a7e1b56665..e03103d4cb 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -85,7 +85,7 @@ def __init__(self, config: BaseConfig, *args): self.cz = factory.committer_factory(self.config) self.project_info = ProjectInfo() - def __call__(self): + def __call__(self) -> None: if self.config.path: out.line(f"Config file {self.config.path} already exists") return @@ -120,7 +120,7 @@ def __call__(self): self.config = JsonConfig(data="{}", path=config_path) elif "yaml" in config_path: self.config = YAMLConfig(data="", path=config_path) - values_to_add = {} + values_to_add: dict[str, Any] = {} values_to_add["name"] = cz_name values_to_add["tag_format"] = tag_format values_to_add["version_scheme"] = version_scheme diff --git a/commitizen/config/base_config.py b/commitizen/config/base_config.py index 587b72aee6..2c7bce206d 100644 --- a/commitizen/config/base_config.py +++ b/commitizen/config/base_config.py @@ -41,3 +41,6 @@ def update(self, data: Settings) -> None: def _parse_setting(self, data: bytes | str) -> None: raise NotImplementedError() + + def init_empty_config_content(self) -> None: + raise NotImplementedError() diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index 2af7bec121..41da985704 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -1692,7 +1692,7 @@ def test_bump_warn_but_dont_fail_on_invalid_tags( def test_is_initial_tag(mocker: MockFixture, tmp_commitizen_project): - """Test the is_initial_tag method behavior.""" + """Test the _is_initial_tag method behavior.""" # Create a commit but no tags create_file_and_commit("feat: initial commit") @@ -1725,15 +1725,15 @@ def test_is_initial_tag(mocker: MockFixture, tmp_commitizen_project): # Test case 1: No current tag, not yes mode mocker.patch("questionary.confirm", return_value=mocker.Mock(ask=lambda: True)) - assert bump_cmd.is_initial_tag(None, is_yes=False) is True + assert bump_cmd._is_initial_tag(None, is_yes=False) is True # Test case 2: No current tag, yes mode - assert bump_cmd.is_initial_tag(None, is_yes=True) is True + assert bump_cmd._is_initial_tag(None, is_yes=True) is True # Test case 3: Has current tag mock_tag = mocker.Mock() - assert bump_cmd.is_initial_tag(mock_tag, is_yes=False) is False + assert bump_cmd._is_initial_tag(mock_tag, is_yes=False) is False # Test case 4: No current tag, user denies mocker.patch("questionary.confirm", return_value=mocker.Mock(ask=lambda: False)) - assert bump_cmd.is_initial_tag(None, is_yes=False) is False + assert bump_cmd._is_initial_tag(None, is_yes=False) is False From afcf662503da656f24853ae7bb3f8c1f9d04654b Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Fri, 30 May 2025 14:29:35 +0800 Subject: [PATCH 07/31] style: remove unnecessary noqa --- commitizen/cz/conventional_commits/conventional_commits.py | 2 +- commitizen/cz/jira/jira.py | 2 +- tests/conftest.py | 2 +- tests/test_bump_find_increment.py | 4 ++-- tests/test_cz_conventional_commits.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/commitizen/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index 9296375349..e748b950c6 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -21,7 +21,7 @@ class ConventionalCommitsCz(BaseCommitizen): bump_pattern = defaults.BUMP_PATTERN bump_map = defaults.BUMP_MAP bump_map_major_version_zero = defaults.BUMP_MAP_MAJOR_VERSION_ZERO - commit_parser = r"^((?Pfeat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P[^()\r\n]*)\)|\()?(?P!)?|\w+!):\s(?P.*)?" # noqa + commit_parser = r"^((?Pfeat|fix|refactor|perf|BREAKING CHANGE)(?:\((?P[^()\r\n]*)\)|\()?(?P!)?|\w+!):\s(?P.*)?" change_type_map = { "feat": "Feat", "fix": "Fix", diff --git a/commitizen/cz/jira/jira.py b/commitizen/cz/jira/jira.py index f43de2177c..05e23e1690 100644 --- a/commitizen/cz/jira/jira.py +++ b/commitizen/cz/jira/jira.py @@ -67,7 +67,7 @@ def example(self) -> str: ) def schema(self) -> str: - return " # " # noqa + return " # " def schema_pattern(self) -> str: return r".*[A-Z]{2,}\-[0-9]+( #| .* #).+( #.+)*" diff --git a/tests/conftest.py b/tests/conftest.py index 60c586f2e6..1b49dcbfaa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -169,7 +169,7 @@ class SemverCommitizen(BaseCommitizen): "patch": "PATCH", } changelog_pattern = r"^(patch|minor|major)" - commit_parser = r"^(?Ppatch|minor|major)(?:\((?P[^()\r\n]*)\)|\()?:?\s(?P.+)" # noqa + commit_parser = r"^(?Ppatch|minor|major)(?:\((?P[^()\r\n]*)\)|\()?:?\s(?P.+)" change_type_map = { "major": "Breaking Changes", "minor": "Features", diff --git a/tests/test_bump_find_increment.py b/tests/test_bump_find_increment.py index ff24ff17a7..77e11c78c7 100644 --- a/tests/test_bump_find_increment.py +++ b/tests/test_bump_find_increment.py @@ -32,14 +32,14 @@ MAJOR_INCREMENTS_BREAKING_CHANGE_CC = [ "feat(cli): added version", "docs(README): motivation", - "BREAKING CHANGE: `extends` key in config file is now used for extending other config files", # noqa + "BREAKING CHANGE: `extends` key in config file is now used for extending other config files", "fix(setup.py): future is now required for every python version", ] MAJOR_INCREMENTS_BREAKING_CHANGE_ALT_CC = [ "feat(cli): added version", "docs(README): motivation", - "BREAKING-CHANGE: `extends` key in config file is now used for extending other config files", # noqa + "BREAKING-CHANGE: `extends` key in config file is now used for extending other config files", "fix(setup.py): future is now required for every python version", ] diff --git a/tests/test_cz_conventional_commits.py b/tests/test_cz_conventional_commits.py index b37a0f0116..c89ddf6ae5 100644 --- a/tests/test_cz_conventional_commits.py +++ b/tests/test_cz_conventional_commits.py @@ -83,7 +83,7 @@ def test_long_answer(config): message = conventional_commits.message(answers) assert ( message - == "fix(users): email pattern corrected\n\ncomplete content\n\ncloses #24" # noqa + == "fix(users): email pattern corrected\n\ncomplete content\n\ncloses #24" ) @@ -101,7 +101,7 @@ def test_breaking_change_in_footer(config): print(message) assert ( message - == "fix(users): email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users" # noqa + == "fix(users): email pattern corrected\n\ncomplete content\n\nBREAKING CHANGE: migrate by renaming user to users" ) From 8c0ff1b00567af049ad74377fbc86b46485a2799 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Fri, 30 May 2025 14:30:57 +0800 Subject: [PATCH 08/31] style: type untyped methods --- commitizen/changelog.py | 6 +++--- commitizen/commands/changelog.py | 2 +- commitizen/commands/example.py | 4 ++-- commitizen/commands/init.py | 2 +- commitizen/commands/schema.py | 4 ++-- commitizen/config/base_config.py | 12 +++++++++++- commitizen/config/json_config.py | 14 ++++++++++++-- commitizen/config/toml_config.py | 18 ++++++++++++++---- commitizen/config/yaml_config.py | 14 ++++++++++++-- commitizen/cz/base.py | 4 ++-- commitizen/git.py | 4 ++-- commitizen/hooks.py | 5 +++-- commitizen/providers/base_provider.py | 10 +++++----- commitizen/providers/cargo_provider.py | 2 +- commitizen/providers/commitizen_provider.py | 2 +- commitizen/providers/poetry_provider.py | 2 +- commitizen/providers/scm_provider.py | 2 +- 17 files changed, 74 insertions(+), 33 deletions(-) diff --git a/commitizen/changelog.py b/commitizen/changelog.py index 6526b4c540..ba6fbbc6b3 100644 --- a/commitizen/changelog.py +++ b/commitizen/changelog.py @@ -63,7 +63,7 @@ class Metadata: latest_version_position: int | None = None latest_version_tag: str | None = None - def __post_init__(self): + def __post_init__(self) -> None: if self.latest_version and not self.latest_version_tag: # Test syntactic sugar # latest version tag is optional if same as latest version @@ -169,8 +169,8 @@ def process_commit_message( commit: GitCommit, changes: dict[str | None, list], change_type_map: dict[str, str] | None = None, -): - message: dict = { +) -> None: + message: dict[str, Any] = { "sha1": commit.rev, "parents": commit.parents, "author": commit.author, diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 11f72cba18..5a1270dfd8 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -138,7 +138,7 @@ def _find_incremental_rev(self, latest_version: str, tags: Iterable[GitTag]) -> def _write_changelog( self, changelog_out: str, lines: list[str], changelog_meta: changelog.Metadata - ): + ) -> None: with smart_open(self.file_name, "w", encoding=self.encoding) as changelog_file: partial_changelog: str | None = None if self.incremental: diff --git a/commitizen/commands/example.py b/commitizen/commands/example.py index a28ad85f16..818e217530 100644 --- a/commitizen/commands/example.py +++ b/commitizen/commands/example.py @@ -5,9 +5,9 @@ class Example: """Show an example so people understands the rules.""" - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, *args: object): self.config: BaseConfig = config self.cz = factory.committer_factory(self.config) - def __call__(self): + def __call__(self) -> None: out.write(self.cz.example()) diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index e03103d4cb..ca6426b0bd 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -79,7 +79,7 @@ def is_pre_commit_installed(self) -> bool: class Init: - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, *args: object): self.config: BaseConfig = config self.encoding = config.settings["encoding"] self.cz = factory.committer_factory(self.config) diff --git a/commitizen/commands/schema.py b/commitizen/commands/schema.py index 4af5679cf5..5c77898d8a 100644 --- a/commitizen/commands/schema.py +++ b/commitizen/commands/schema.py @@ -5,9 +5,9 @@ class Schema: """Show structure of the rule.""" - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, *args: object): self.config: BaseConfig = config self.cz = factory.committer_factory(self.config) - def __call__(self): + def __call__(self) -> None: out.write(self.cz.schema()) diff --git a/commitizen/config/base_config.py b/commitizen/config/base_config.py index 2c7bce206d..10a26e3f0c 100644 --- a/commitizen/config/base_config.py +++ b/commitizen/config/base_config.py @@ -1,9 +1,19 @@ from __future__ import annotations from pathlib import Path +from typing import TYPE_CHECKING, Any from commitizen.defaults import DEFAULT_SETTINGS, Settings +if TYPE_CHECKING: + import sys + + # Self is Python 3.11+ but backported in typing-extensions + if sys.version_info < (3, 11): + from typing_extensions import Self + else: + from typing import Self + class BaseConfig: def __init__(self) -> None: @@ -28,7 +38,7 @@ def path(self, path: str | Path) -> None: """ self._path = Path(path) - def set_key(self, key, value): + def set_key(self, key: str, value: Any) -> Self: """Set or update a key in the conf. For now only strings are supported. diff --git a/commitizen/config/json_config.py b/commitizen/config/json_config.py index 28fac25662..83e0a928a0 100644 --- a/commitizen/config/json_config.py +++ b/commitizen/config/json_config.py @@ -2,12 +2,22 @@ import json from pathlib import Path +from typing import TYPE_CHECKING, Any from commitizen.exceptions import InvalidConfigurationError from commitizen.git import smart_open from .base_config import BaseConfig +if TYPE_CHECKING: + import sys + + # Self is Python 3.11+ but backported in typing-extensions + if sys.version_info < (3, 11): + from typing_extensions import Self + else: + from typing import Self + class JsonConfig(BaseConfig): def __init__(self, *, data: bytes | str, path: Path | str): @@ -16,11 +26,11 @@ def __init__(self, *, data: bytes | str, path: Path | str): self.path = path self._parse_setting(data) - def init_empty_config_content(self): + def init_empty_config_content(self) -> None: with smart_open(self.path, "a", encoding=self.encoding) as json_file: json.dump({"commitizen": {}}, json_file) - def set_key(self, key, value): + def set_key(self, key: str, value: Any) -> Self: """Set or update a key in the conf. For now only strings are supported. diff --git a/commitizen/config/toml_config.py b/commitizen/config/toml_config.py index a7050214ba..9bab994fd5 100644 --- a/commitizen/config/toml_config.py +++ b/commitizen/config/toml_config.py @@ -2,6 +2,7 @@ import os from pathlib import Path +from typing import TYPE_CHECKING, Any from tomlkit import exceptions, parse, table @@ -9,6 +10,15 @@ from .base_config import BaseConfig +if TYPE_CHECKING: + import sys + + # Self is Python 3.11+ but backported in typing-extensions + if sys.version_info < (3, 11): + from typing_extensions import Self + else: + from typing import Self + class TomlConfig(BaseConfig): def __init__(self, *, data: bytes | str, path: Path | str): @@ -17,7 +27,7 @@ def __init__(self, *, data: bytes | str, path: Path | str): self.path = path self._parse_setting(data) - def init_empty_config_content(self): + def init_empty_config_content(self) -> None: if os.path.isfile(self.path): with open(self.path, "rb") as input_toml_file: parser = parse(input_toml_file.read()) @@ -27,10 +37,10 @@ def init_empty_config_content(self): with open(self.path, "wb") as output_toml_file: if parser.get("tool") is None: parser["tool"] = table() - parser["tool"]["commitizen"] = table() + parser["tool"]["commitizen"] = table() # type: ignore output_toml_file.write(parser.as_string().encode(self.encoding)) - def set_key(self, key, value): + def set_key(self, key: str, value: Any) -> Self: """Set or update a key in the conf. For now only strings are supported. @@ -39,7 +49,7 @@ def set_key(self, key, value): with open(self.path, "rb") as f: parser = parse(f.read()) - parser["tool"]["commitizen"][key] = value + parser["tool"]["commitizen"][key] = value # type: ignore with open(self.path, "wb") as f: f.write(parser.as_string().encode(self.encoding)) return self diff --git a/commitizen/config/yaml_config.py b/commitizen/config/yaml_config.py index 2c384cf17d..8eac9bb785 100644 --- a/commitizen/config/yaml_config.py +++ b/commitizen/config/yaml_config.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from typing import TYPE_CHECKING, Any import yaml @@ -9,6 +10,15 @@ from .base_config import BaseConfig +if TYPE_CHECKING: + import sys + + # Self is Python 3.11+ but backported in typing-extensions + if sys.version_info < (3, 11): + from typing_extensions import Self + else: + from typing import Self + class YAMLConfig(BaseConfig): def __init__(self, *, data: bytes | str, path: Path | str): @@ -17,7 +27,7 @@ def __init__(self, *, data: bytes | str, path: Path | str): self.path = path self._parse_setting(data) - def init_empty_config_content(self): + def init_empty_config_content(self) -> None: with smart_open(self.path, "a", encoding=self.encoding) as json_file: yaml.dump({"commitizen": {}}, json_file, explicit_start=True) @@ -41,7 +51,7 @@ def _parse_setting(self, data: bytes | str) -> None: except (KeyError, TypeError): self.is_empty_config = True - def set_key(self, key, value): + def set_key(self, key: str, value: Any) -> Self: """Set or update a key in the conf. For now only strings are supported. diff --git a/commitizen/cz/base.py b/commitizen/cz/base.py index 43455a74ca..0bf1e4cb47 100644 --- a/commitizen/cz/base.py +++ b/commitizen/cz/base.py @@ -76,13 +76,13 @@ def message(self, answers: dict) -> str: """Format your git message.""" @property - def style(self): + def style(self) -> Style: return merge_styles( [ Style(BaseCommitizen.default_style_config), Style(self.config.settings["style"]), ] - ) + ) # type: ignore[return-value] def example(self) -> str: """Example of the commit message.""" diff --git a/commitizen/git.py b/commitizen/git.py index 48fa13ec52..54d57f69bb 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -215,7 +215,7 @@ def get_commits( ] -def get_filenames_in_commit(git_reference: str = ""): +def get_filenames_in_commit(git_reference: str = "") -> list[str]: """Get the list of files that were committed in the requested git reference. :param git_reference: a git reference as accepted by `git show`, default: the last commit @@ -312,7 +312,7 @@ def get_core_editor() -> str | None: return None -def smart_open(*args, **kwargs): +def smart_open(*args, **kwargs): # type: ignore[no-untyped-def,unused-ignore] """Open a file with the EOL style determined from Git.""" return open(*args, newline=EOLType.for_open(), **kwargs) diff --git a/commitizen/hooks.py b/commitizen/hooks.py index f5505d0e82..f60bd9b43e 100644 --- a/commitizen/hooks.py +++ b/commitizen/hooks.py @@ -1,12 +1,13 @@ from __future__ import annotations import os +from collections.abc import Mapping from commitizen import cmd, out from commitizen.exceptions import RunHookError -def run(hooks, _env_prefix="CZ_", **env): +def run(hooks: str | list[str], _env_prefix: str = "CZ_", **env: object) -> None: if isinstance(hooks, str): hooks = [hooks] @@ -24,7 +25,7 @@ def run(hooks, _env_prefix="CZ_", **env): raise RunHookError(f"Running hook '{hook}' failed") -def _format_env(prefix: str, env: dict[str, str]) -> dict[str, str]: +def _format_env(prefix: str, env: Mapping[str, object]) -> dict[str, str]: """_format_env() prefixes all given environment variables with the given prefix so it can be passed directly to cmd.run().""" penv = dict(os.environ) diff --git a/commitizen/providers/base_provider.py b/commitizen/providers/base_provider.py index 34048318e2..9c1e0f7a98 100644 --- a/commitizen/providers/base_provider.py +++ b/commitizen/providers/base_provider.py @@ -29,7 +29,7 @@ def get_version(self) -> str: """ @abstractmethod - def set_version(self, version: str): + def set_version(self, version: str) -> None: """ Set the new current version """ @@ -58,7 +58,7 @@ def get_version(self) -> str: document = json.loads(self.file.read_text()) return self.get(document) - def set_version(self, version: str): + def set_version(self, version: str) -> None: document = json.loads(self.file.read_text()) self.set(document, version) self.file.write_text(json.dumps(document, indent=self.indent) + "\n") @@ -66,7 +66,7 @@ def set_version(self, version: str): def get(self, document: dict[str, Any]) -> str: return document["version"] # type: ignore - def set(self, document: dict[str, Any], version: str): + def set(self, document: dict[str, Any], version: str) -> None: document["version"] = version @@ -79,7 +79,7 @@ def get_version(self) -> str: document = tomlkit.parse(self.file.read_text()) return self.get(document) - def set_version(self, version: str): + def set_version(self, version: str) -> None: document = tomlkit.parse(self.file.read_text()) self.set(document, version) self.file.write_text(tomlkit.dumps(document)) @@ -87,5 +87,5 @@ def set_version(self, version: str): def get(self, document: tomlkit.TOMLDocument) -> str: return document["project"]["version"] # type: ignore - def set(self, document: tomlkit.TOMLDocument, version: str): + def set(self, document: tomlkit.TOMLDocument, version: str) -> None: document["project"]["version"] = version # type: ignore diff --git a/commitizen/providers/cargo_provider.py b/commitizen/providers/cargo_provider.py index 2e73ff35a1..87e45cd71c 100644 --- a/commitizen/providers/cargo_provider.py +++ b/commitizen/providers/cargo_provider.py @@ -28,7 +28,7 @@ def get(self, document: tomlkit.TOMLDocument) -> str: ... return document["workspace"]["package"]["version"] # type: ignore - def set(self, document: tomlkit.TOMLDocument, version: str): + def set(self, document: tomlkit.TOMLDocument, version: str) -> None: try: document["workspace"]["package"]["version"] = version # type: ignore return diff --git a/commitizen/providers/commitizen_provider.py b/commitizen/providers/commitizen_provider.py index a1da25ff72..7ce177a604 100644 --- a/commitizen/providers/commitizen_provider.py +++ b/commitizen/providers/commitizen_provider.py @@ -11,5 +11,5 @@ class CommitizenProvider(VersionProvider): def get_version(self) -> str: return self.config.settings["version"] # type: ignore - def set_version(self, version: str): + def set_version(self, version: str) -> None: self.config.set_key("version", version) diff --git a/commitizen/providers/poetry_provider.py b/commitizen/providers/poetry_provider.py index 7aa28f56d9..1dd33f053e 100644 --- a/commitizen/providers/poetry_provider.py +++ b/commitizen/providers/poetry_provider.py @@ -15,5 +15,5 @@ class PoetryProvider(TomlProvider): def get(self, pyproject: tomlkit.TOMLDocument) -> str: return pyproject["tool"]["poetry"]["version"] # type: ignore - def set(self, pyproject: tomlkit.TOMLDocument, version: str): + def set(self, pyproject: tomlkit.TOMLDocument, version: str) -> None: pyproject["tool"]["poetry"]["version"] = version # type: ignore diff --git a/commitizen/providers/scm_provider.py b/commitizen/providers/scm_provider.py index cb575148cb..3085b16efa 100644 --- a/commitizen/providers/scm_provider.py +++ b/commitizen/providers/scm_provider.py @@ -23,6 +23,6 @@ def get_version(self) -> str: return "0.0.0" return str(versions[-1]) - def set_version(self, version: str): + def set_version(self, version: str) -> None: # Not necessary pass From eee01b08520797e6df756fde24e27c1ce9e36cde Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Fri, 30 May 2025 18:48:20 +0800 Subject: [PATCH 09/31] style: remove Union --- commitizen/cmd.py | 6 ++++-- commitizen/cz/utils.py | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/commitizen/cmd.py b/commitizen/cmd.py index 9d6b2f6d2a..3f13087233 100644 --- a/commitizen/cmd.py +++ b/commitizen/cmd.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import os import subprocess from collections.abc import Mapping -from typing import NamedTuple, Union +from typing import NamedTuple from charset_normalizer import from_bytes @@ -29,7 +31,7 @@ def _try_decode(bytes_: bytes) -> str: raise CharacterSetDecodeError() from e -def run(cmd: str, env: Union[Mapping[str, str], None] = None) -> Command: +def run(cmd: str, env: Mapping[str, str] | None = None) -> Command: if env is not None: env = {**os.environ, **env} process = subprocess.Popen( diff --git a/commitizen/cz/utils.py b/commitizen/cz/utils.py index 430bda2d43..a6f687226c 100644 --- a/commitizen/cz/utils.py +++ b/commitizen/cz/utils.py @@ -1,7 +1,6 @@ import os import re import tempfile -from typing import Union from commitizen import git from commitizen.cz import exceptions @@ -9,7 +8,7 @@ _RE_LOCAL_VERSION = re.compile(r"\+.+") -def required_validator(answer: str, msg: Union[str, None] = None) -> str: +def required_validator(answer: str, msg: object = None) -> str: if not answer: raise exceptions.AnswerRequiredError(msg) return answer From d60167b9a063dcad2c1743e94dd4328a6fbbce51 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Fri, 30 May 2025 19:00:05 +0800 Subject: [PATCH 10/31] style: better type --- commitizen/cli.py | 4 ++-- commitizen/providers/base_provider.py | 5 +++-- commitizen/providers/npm_provider.py | 5 +++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index cea15f1a22..11afb990b9 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -8,7 +8,7 @@ from functools import partial from pathlib import Path from types import TracebackType -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import argcomplete from decli import cli @@ -48,7 +48,7 @@ def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - kwarg: str | Sequence[Any] | None, + kwarg: str | Sequence[object] | None, option_string: str | None = None, ) -> None: if not isinstance(kwarg, str): diff --git a/commitizen/providers/base_provider.py b/commitizen/providers/base_provider.py index 9c1e0f7a98..7a7e1205b4 100644 --- a/commitizen/providers/base_provider.py +++ b/commitizen/providers/base_provider.py @@ -2,6 +2,7 @@ import json from abc import ABC, abstractmethod +from collections.abc import Mapping from pathlib import Path from typing import Any, ClassVar @@ -63,8 +64,8 @@ def set_version(self, version: str) -> None: self.set(document, version) self.file.write_text(json.dumps(document, indent=self.indent) + "\n") - def get(self, document: dict[str, Any]) -> str: - return document["version"] # type: ignore + def get(self, document: Mapping[str, str]) -> str: + return document["version"] def set(self, document: dict[str, Any], version: str) -> None: document["version"] = version diff --git a/commitizen/providers/npm_provider.py b/commitizen/providers/npm_provider.py index 12900ff7de..3125447250 100644 --- a/commitizen/providers/npm_provider.py +++ b/commitizen/providers/npm_provider.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +from collections.abc import Mapping from pathlib import Path from typing import Any, ClassVar @@ -58,8 +59,8 @@ def set_version(self, version: str) -> None: json.dumps(shrinkwrap_document, indent=self.indent) + "\n" ) - def get_package_version(self, document: dict[str, Any]) -> str: - return document["version"] # type: ignore + def get_package_version(self, document: Mapping[str, str]) -> str: + return document["version"] def set_package_version( self, document: dict[str, Any], version: str From 6350ff0dc833b437bbbc10c2c2e1d9a4c85fe66e Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Fri, 30 May 2025 22:12:30 +0800 Subject: [PATCH 11/31] style: add `-> None` to __init__ --- commitizen/changelog_formats/__init__.py | 2 +- commitizen/changelog_formats/base.py | 2 +- commitizen/commands/bump.py | 2 +- commitizen/commands/changelog.py | 2 +- commitizen/commands/check.py | 2 +- commitizen/commands/commit.py | 2 +- commitizen/commands/example.py | 2 +- commitizen/commands/info.py | 2 +- commitizen/commands/init.py | 2 +- commitizen/commands/list_cz.py | 2 +- commitizen/commands/schema.py | 2 +- commitizen/commands/version.py | 2 +- commitizen/config/json_config.py | 2 +- commitizen/config/toml_config.py | 2 +- commitizen/config/yaml_config.py | 2 +- commitizen/cz/customize/customize.py | 2 +- commitizen/exceptions.py | 4 ++-- commitizen/git.py | 4 ++-- commitizen/providers/base_provider.py | 2 +- commitizen/version_schemes.py | 2 +- 20 files changed, 22 insertions(+), 22 deletions(-) diff --git a/commitizen/changelog_formats/__init__.py b/commitizen/changelog_formats/__init__.py index 782bfb24cb..b7b3cac01d 100644 --- a/commitizen/changelog_formats/__init__.py +++ b/commitizen/changelog_formats/__init__.py @@ -25,7 +25,7 @@ class ChangelogFormat(Protocol): config: BaseConfig - def __init__(self, config: BaseConfig): + def __init__(self, config: BaseConfig) -> None: self.config = config @property diff --git a/commitizen/changelog_formats/base.py b/commitizen/changelog_formats/base.py index f69cf8f00f..cb5d385bf8 100644 --- a/commitizen/changelog_formats/base.py +++ b/commitizen/changelog_formats/base.py @@ -20,7 +20,7 @@ class BaseFormat(ChangelogFormat, metaclass=ABCMeta): extension: ClassVar[str] = "" alternative_extensions: ClassVar[set[str]] = set() - def __init__(self, config: BaseConfig): + def __init__(self, config: BaseConfig) -> None: # Constructor needs to be redefined because `Protocol` prevent instantiation by default # See: https://bugs.python.org/issue44807 self.config = config diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index bfe49d4438..e8016a0948 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -40,7 +40,7 @@ class Bump: """Show prompt for the user to create a guided commit.""" - def __init__(self, config: BaseConfig, arguments: dict): + def __init__(self, config: BaseConfig, arguments: dict) -> None: if not git.is_git_project(): raise NotAGitProjectError() diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 5a1270dfd8..59d799b037 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -29,7 +29,7 @@ class Changelog: """Generate a changelog based on the commit history.""" - def __init__(self, config: BaseConfig, args: Mapping[str, Any]): + def __init__(self, config: BaseConfig, args: Mapping[str, Any]) -> None: if not git.is_git_project(): raise NotAGitProjectError() diff --git a/commitizen/commands/check.py b/commitizen/commands/check.py index 6cf91bb926..82a8039fa3 100644 --- a/commitizen/commands/check.py +++ b/commitizen/commands/check.py @@ -19,7 +19,7 @@ class Check: def __init__( self, config: BaseConfig, arguments: dict[str, Any], cwd: str = os.getcwd() - ): + ) -> None: """Initial check command. Args: diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 22ad642c43..68095d0e0c 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -31,7 +31,7 @@ class Commit: """Show prompt for the user to create a guided commit.""" - def __init__(self, config: BaseConfig, arguments: dict): + def __init__(self, config: BaseConfig, arguments: dict) -> None: if not git.is_git_project(): raise NotAGitProjectError() diff --git a/commitizen/commands/example.py b/commitizen/commands/example.py index 818e217530..ba9f34adc4 100644 --- a/commitizen/commands/example.py +++ b/commitizen/commands/example.py @@ -5,7 +5,7 @@ class Example: """Show an example so people understands the rules.""" - def __init__(self, config: BaseConfig, *args: object): + def __init__(self, config: BaseConfig, *args: object) -> None: self.config: BaseConfig = config self.cz = factory.committer_factory(self.config) diff --git a/commitizen/commands/info.py b/commitizen/commands/info.py index 5b649ceb5d..5ea8227313 100644 --- a/commitizen/commands/info.py +++ b/commitizen/commands/info.py @@ -5,7 +5,7 @@ class Info: """Show in depth explanation of your rules.""" - def __init__(self, config: BaseConfig, *args: object): + def __init__(self, config: BaseConfig, *args: object) -> None: self.config: BaseConfig = config self.cz = factory.committer_factory(self.config) diff --git a/commitizen/commands/init.py b/commitizen/commands/init.py index ca6426b0bd..1207cd02ee 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -79,7 +79,7 @@ def is_pre_commit_installed(self) -> bool: class Init: - def __init__(self, config: BaseConfig, *args: object): + def __init__(self, config: BaseConfig, *args: object) -> None: self.config: BaseConfig = config self.encoding = config.settings["encoding"] self.cz = factory.committer_factory(self.config) diff --git a/commitizen/commands/list_cz.py b/commitizen/commands/list_cz.py index ff4e12ad44..412266f6c3 100644 --- a/commitizen/commands/list_cz.py +++ b/commitizen/commands/list_cz.py @@ -6,7 +6,7 @@ class ListCz: """List currently installed rules.""" - def __init__(self, config: BaseConfig, *args: object): + def __init__(self, config: BaseConfig, *args: object) -> None: self.config: BaseConfig = config def __call__(self) -> None: diff --git a/commitizen/commands/schema.py b/commitizen/commands/schema.py index 5c77898d8a..a7aeb53569 100644 --- a/commitizen/commands/schema.py +++ b/commitizen/commands/schema.py @@ -5,7 +5,7 @@ class Schema: """Show structure of the rule.""" - def __init__(self, config: BaseConfig, *args: object): + def __init__(self, config: BaseConfig, *args: object) -> None: self.config: BaseConfig = config self.cz = factory.committer_factory(self.config) diff --git a/commitizen/commands/version.py b/commitizen/commands/version.py index 55d0a950a3..ecf1f03129 100644 --- a/commitizen/commands/version.py +++ b/commitizen/commands/version.py @@ -12,7 +12,7 @@ class Version: """Get the version of the installed commitizen or the current project.""" - def __init__(self, config: BaseConfig, *args: Mapping[str, Any]): + def __init__(self, config: BaseConfig, *args: Mapping[str, Any]) -> None: self.config: BaseConfig = config self.parameter = args[0] self.operating_system = platform.system() diff --git a/commitizen/config/json_config.py b/commitizen/config/json_config.py index 83e0a928a0..be1f1c36b0 100644 --- a/commitizen/config/json_config.py +++ b/commitizen/config/json_config.py @@ -20,7 +20,7 @@ class JsonConfig(BaseConfig): - def __init__(self, *, data: bytes | str, path: Path | str): + def __init__(self, *, data: bytes | str, path: Path | str) -> None: super().__init__() self.is_empty_config = False self.path = path diff --git a/commitizen/config/toml_config.py b/commitizen/config/toml_config.py index 9bab994fd5..3571c9c882 100644 --- a/commitizen/config/toml_config.py +++ b/commitizen/config/toml_config.py @@ -21,7 +21,7 @@ class TomlConfig(BaseConfig): - def __init__(self, *, data: bytes | str, path: Path | str): + def __init__(self, *, data: bytes | str, path: Path | str) -> None: super().__init__() self.is_empty_config = False self.path = path diff --git a/commitizen/config/yaml_config.py b/commitizen/config/yaml_config.py index 8eac9bb785..f2a79e6937 100644 --- a/commitizen/config/yaml_config.py +++ b/commitizen/config/yaml_config.py @@ -21,7 +21,7 @@ class YAMLConfig(BaseConfig): - def __init__(self, *, data: bytes | str, path: Path | str): + def __init__(self, *, data: bytes | str, path: Path | str) -> None: super().__init__() self.is_empty_config = False self.path = path diff --git a/commitizen/cz/customize/customize.py b/commitizen/cz/customize/customize.py index 53ada4b2c0..8f844501ec 100644 --- a/commitizen/cz/customize/customize.py +++ b/commitizen/cz/customize/customize.py @@ -26,7 +26,7 @@ class CustomizeCommitsCz(BaseCommitizen): bump_map_major_version_zero = defaults.BUMP_MAP_MAJOR_VERSION_ZERO change_type_order = defaults.CHANGE_TYPE_ORDER - def __init__(self, config: BaseConfig): + def __init__(self, config: BaseConfig) -> None: super().__init__(config) if "customize" not in self.config.settings: diff --git a/commitizen/exceptions.py b/commitizen/exceptions.py index 87efabe2ab..8c0956be53 100644 --- a/commitizen/exceptions.py +++ b/commitizen/exceptions.py @@ -41,7 +41,7 @@ class ExitCode(enum.IntEnum): class CommitizenException(Exception): - def __init__(self, *args: str, **kwargs: Any): + def __init__(self, *args: str, **kwargs: Any) -> None: self.output_method = kwargs.get("output_method") or out.error self.exit_code: ExitCode = self.__class__.exit_code if args: @@ -58,7 +58,7 @@ def __str__(self) -> str: class ExpectedExit(CommitizenException): exit_code = ExitCode.EXPECTED_EXIT - def __init__(self, *args: str, **kwargs: Any): + def __init__(self, *args: str, **kwargs: Any) -> None: output_method = kwargs.get("output_method") or out.write kwargs["output_method"] = output_method super().__init__(*args, **kwargs) diff --git a/commitizen/git.py b/commitizen/git.py index 54d57f69bb..b8528e3f77 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -59,7 +59,7 @@ def __init__( author: str = "", author_email: str = "", parents: list[str] | None = None, - ): + ) -> None: self.rev = rev.strip() self.title = title.strip() self.body = body.strip() @@ -132,7 +132,7 @@ def __repr__(self) -> str: class GitTag(GitObject): - def __init__(self, name: str, rev: str, date: str): + def __init__(self, name: str, rev: str, date: str) -> None: self.rev = rev.strip() self.name = name.strip() self._date = date.strip() diff --git a/commitizen/providers/base_provider.py b/commitizen/providers/base_provider.py index 7a7e1205b4..27c3123416 100644 --- a/commitizen/providers/base_provider.py +++ b/commitizen/providers/base_provider.py @@ -20,7 +20,7 @@ class VersionProvider(ABC): config: BaseConfig - def __init__(self, config: BaseConfig): + def __init__(self, config: BaseConfig) -> None: self.config = config @abstractmethod diff --git a/commitizen/version_schemes.py b/commitizen/version_schemes.py index d5370b2c6c..094d455590 100644 --- a/commitizen/version_schemes.py +++ b/commitizen/version_schemes.py @@ -51,7 +51,7 @@ class VersionProtocol(Protocol): parser: ClassVar[re.Pattern] """Regex capturing this version scheme into a `version` group""" - def __init__(self, version: str): + def __init__(self, version: str) -> None: """ Initialize a version object from its string representation. From 4a6998604600bc623eef5ef04c042577853f4759 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Fri, 30 May 2025 22:35:44 +0800 Subject: [PATCH 12/31] build(ruff,mypy): more strict rules --- commitizen/git.py | 2 +- pyproject.toml | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/commitizen/git.py b/commitizen/git.py index b8528e3f77..fb59750eaf 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -312,7 +312,7 @@ def get_core_editor() -> str | None: return None -def smart_open(*args, **kwargs): # type: ignore[no-untyped-def,unused-ignore] +def smart_open(*args, **kwargs): # type: ignore[no-untyped-def,unused-ignore] # noqa: ANN201 """Open a file with the EOL style determined from Git.""" return open(*args, newline=EOLType.for_open(), **kwargs) diff --git a/pyproject.toml b/pyproject.toml index 1a29ca6bf9..9bb40b270f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -186,6 +186,9 @@ line-length = 88 [tool.ruff.lint] select = [ + # flake8-annotations + "ANN001", + "ANN2", # pycodestyle "E", # Pyflakes @@ -197,6 +200,9 @@ select = [ ] ignore = ["E501", "D1", "D415"] +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["ANN"] + [tool.ruff.lint.isort] known-first-party = ["commitizen", "tests"] @@ -204,7 +210,8 @@ known-first-party = ["commitizen", "tests"] convention = "google" [tool.mypy] -files = "commitizen" +files = ["commitizen", "tests"] +disallow_untyped_defs = true disallow_untyped_decorators = true disallow_subclassing_any = true warn_return_any = true @@ -212,6 +219,10 @@ warn_redundant_casts = true warn_unused_ignores = true warn_unused_configs = true +[[tool.mypy.overrides]] +module = "tests/*" +disallow_untyped_defs = false + [[tool.mypy.overrides]] module = "py.*" # Legacy pytest dependencies ignore_missing_imports = true @@ -228,14 +239,14 @@ poetry_command = "" [tool.poe.tasks] format.help = "Format the code" format.sequence = [ - { cmd = "ruff check --fix commitizen tests" }, - { cmd = "ruff format commitizen tests" }, + { cmd = "ruff check --fix" }, + { cmd = "ruff format" }, ] lint.help = "Lint the code" lint.sequence = [ - { cmd = "ruff check commitizen/ tests/ --fix" }, - { cmd = "mypy commitizen/ tests/" }, + { cmd = "ruff check" }, + { cmd = "mypy" }, ] check-commit.help = "Check the commit message" From 93c5b661145daebf663304b898659fd7e17bc0d0 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sat, 31 May 2025 18:20:41 +0800 Subject: [PATCH 13/31] fix(BaseConfig): mypy error --- commitizen/config/base_config.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/commitizen/config/base_config.py b/commitizen/config/base_config.py index 10a26e3f0c..59c0d16a06 100644 --- a/commitizen/config/base_config.py +++ b/commitizen/config/base_config.py @@ -26,16 +26,11 @@ def settings(self) -> Settings: return self._settings @property - def path(self) -> Path | None: - return self._path + def path(self) -> Path: + return self._path # type: ignore @path.setter def path(self, path: str | Path) -> None: - """ - mypy does not like this until 1.16 - See https://github.com/python/mypy/pull/18510 - TODO: remove "type: ignore" from the call sites when 1.16 is available - """ self._path = Path(path) def set_key(self, key: str, value: Any) -> Self: From f9df10c33285b131f78fb3639c7ff0ab9f21f9c6 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sat, 31 May 2025 22:04:10 +0800 Subject: [PATCH 14/31] build(mypy): remove disallow_untyped_defs because it's already done by ruff rules --- pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9bb40b270f..aa42a9eb13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -211,7 +211,6 @@ convention = "google" [tool.mypy] files = ["commitizen", "tests"] -disallow_untyped_defs = true disallow_untyped_decorators = true disallow_subclassing_any = true warn_return_any = true @@ -219,10 +218,6 @@ warn_redundant_casts = true warn_unused_ignores = true warn_unused_configs = true -[[tool.mypy.overrides]] -module = "tests/*" -disallow_untyped_defs = false - [[tool.mypy.overrides]] module = "py.*" # Legacy pytest dependencies ignore_missing_imports = true From b773bdcd1be2d28eedbee93a021dca6bdce49c6f Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sat, 31 May 2025 00:45:15 +0800 Subject: [PATCH 15/31] refactor(bump): TypedDict for bump argument --- commitizen/cli.py | 2 +- commitizen/commands/bump.py | 129 ++++++++++++++++------------ commitizen/defaults.py | 38 ++++---- tests/commands/test_bump_command.py | 2 +- 4 files changed, 97 insertions(+), 74 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index 11afb990b9..c11e1c6443 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -682,7 +682,7 @@ def main() -> None: ) sys.excepthook = no_raise_debug_excepthook - args.func(conf, arguments)() + args.func(conf, arguments)() # type: ignore if __name__ == "__main__": diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index e8016a0948..aa5d60ce1a 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -37,37 +37,65 @@ logger = getLogger("commitizen") +class BumpArguments(Settings, total=False): + allow_no_commit: bool | None + annotated_tag_message: str | None + annotated_tag: bool + build_metadata: str | None + changelog_to_stdout: bool + changelog: bool + check_consistency: bool + devrelease: int | None + dry_run: bool + file_name: str + files_only: bool | None + get_next: bool + git_output_to_stderr: bool + gpg_sign: bool + increment_mode: str + increment: Increment | None + local_version: bool + manual_version: str | None + no_verify: bool + prerelease: Prerelease | None + retry: bool + yes: bool + + class Bump: """Show prompt for the user to create a guided commit.""" - def __init__(self, config: BaseConfig, arguments: dict) -> None: + def __init__(self, config: BaseConfig, arguments: BumpArguments) -> None: if not git.is_git_project(): raise NotAGitProjectError() self.config: BaseConfig = config self.encoding = config.settings["encoding"] - self.arguments: dict = arguments - self.bump_settings: dict = { - **config.settings, - **{ - key: arguments[key] - for key in [ - "tag_format", - "prerelease", - "increment", - "increment_mode", - "bump_message", - "gpg_sign", - "annotated_tag", - "annotated_tag_message", - "major_version_zero", - "prerelease_offset", - "template", - "file_name", - ] - if arguments.get(key) is not None + self.arguments = arguments + self.bump_settings = cast( + BumpArguments, + { + **config.settings, + **{ + k: v + for k, v in { + "annotated_tag_message": arguments.get("annotated_tag_message"), + "annotated_tag": arguments.get("annotated_tag"), + "bump_message": arguments.get("bump_message"), + "file_name": arguments.get("file_name"), + "gpg_sign": arguments.get("gpg_sign"), + "increment_mode": arguments.get("increment_mode"), + "increment": arguments.get("increment"), + "major_version_zero": arguments.get("major_version_zero"), + "prerelease_offset": arguments.get("prerelease_offset"), + "prerelease": arguments.get("prerelease"), + "tag_format": arguments.get("tag_format"), + "template": arguments.get("template"), + }.items() + if v is not None + }, }, - } + ) self.cz = factory.committer_factory(self.config) self.changelog_flag = arguments["changelog"] self.changelog_config = self.config.settings.get("update_changelog_on_bump") @@ -120,11 +148,10 @@ def _is_initial_tag( def _find_increment(self, commits: list[git.GitCommit]) -> Increment | None: # Update the bump map to ensure major version doesn't increment. - is_major_version_zero: bool = self.bump_settings["major_version_zero"] # self.cz.bump_map = defaults.bump_map_major_version_zero bump_map = ( self.cz.bump_map_major_version_zero - if is_major_version_zero + if self.bump_settings["major_version_zero"] else self.cz.bump_map ) bump_pattern = self.cz.bump_pattern @@ -144,23 +171,14 @@ def __call__(self) -> None: except TypeError: raise NoVersionSpecifiedError() - bump_commit_message: str | None = self.bump_settings["bump_message"] - version_files: list[str] = self.bump_settings["version_files"] - major_version_zero: bool = self.bump_settings["major_version_zero"] - prerelease_offset: int = self.bump_settings["prerelease_offset"] - - dry_run: bool = self.arguments["dry_run"] - is_yes: bool = self.arguments["yes"] - increment: Increment | None = self.arguments["increment"] - prerelease: Prerelease | None = self.arguments["prerelease"] - devrelease: int | None = self.arguments["devrelease"] - is_files_only: bool | None = self.arguments["files_only"] - is_local_version: bool = self.arguments["local_version"] + increment = self.arguments["increment"] + prerelease = self.arguments["prerelease"] + devrelease = self.arguments["devrelease"] + is_local_version = self.arguments["local_version"] manual_version = self.arguments["manual_version"] build_metadata = self.arguments["build_metadata"] - increment_mode: str = self.arguments["increment_mode"] - get_next: bool = self.arguments["get_next"] - allow_no_commit: bool | None = self.arguments["allow_no_commit"] + get_next = self.arguments["get_next"] + allow_no_commit = self.arguments["allow_no_commit"] if manual_version: if increment: @@ -182,7 +200,7 @@ def __call__(self) -> None: "--build-metadata cannot be combined with MANUAL_VERSION" ) - if major_version_zero: + if self.bump_settings["major_version_zero"]: raise NotAllowed( "--major-version-zero cannot be combined with MANUAL_VERSION" ) @@ -190,7 +208,7 @@ def __call__(self) -> None: if get_next: raise NotAllowed("--get-next cannot be combined with MANUAL_VERSION") - if major_version_zero: + if self.bump_settings["major_version_zero"]: if not current_version.release[0] == 0: raise NotAllowed( f"--major-version-zero is meaningless for current version {current_version}" @@ -215,7 +233,7 @@ def __call__(self) -> None: else: # If user specified changelog_to_stdout, they probably want the # changelog to be generated as well, this is the most intuitive solution - self.changelog_flag = ( + self.changelog_flag = bool( self.changelog_flag or bool(self.changelog_to_stdout) or self.changelog_config @@ -227,7 +245,7 @@ def __call__(self) -> None: current_tag, "name", rules.normalize_tag(current_version) ) - is_initial = self._is_initial_tag(current_tag, is_yes) + is_initial = self._is_initial_tag(current_tag, self.arguments["yes"]) if manual_version: try: @@ -273,16 +291,16 @@ def __call__(self) -> None: new_version = current_version.bump( increment, prerelease=prerelease, - prerelease_offset=prerelease_offset, + prerelease_offset=self.bump_settings["prerelease_offset"], devrelease=devrelease, is_local_version=is_local_version, build_metadata=build_metadata, - exact_increment=increment_mode == "exact", + exact_increment=self.arguments["increment_mode"] == "exact", ) new_tag_version = rules.normalize_tag(new_version) message = bump.create_commit_message( - current_version, new_version, bump_commit_message + current_version, new_version, self.bump_settings["bump_message"] ) if get_next: @@ -314,6 +332,7 @@ def __call__(self) -> None: ) files: list[str] = [] + dry_run = self.arguments["dry_run"] if self.changelog_flag: args = { "unreleased_version": new_tag_version, @@ -342,7 +361,7 @@ def __call__(self) -> None: bump.update_version_in_files( str(current_version), str(new_version), - version_files, + self.bump_settings["version_files"], check_consistency=self.check_consistency, encoding=self.encoding, ) @@ -366,7 +385,7 @@ def __call__(self) -> None: else None, ) - if is_files_only: + if self.arguments["files_only"]: raise ExpectedExit() # FIXME: check if any changes have been staged @@ -395,11 +414,15 @@ def __call__(self) -> None: c = git.tag( new_tag_version, - signed=self.bump_settings.get("gpg_sign", False) - or bool(self.config.settings.get("gpg_sign", False)), - annotated=self.bump_settings.get("annotated_tag", False) - or bool(self.config.settings.get("annotated_tag", False)) - or bool(self.bump_settings.get("annotated_tag_message", False)), + signed=bool( + self.bump_settings.get("gpg_sign") + or self.config.settings.get("gpg_sign") + ), + annotated=bool( + self.bump_settings.get("annotated_tag") + or self.config.settings.get("annotated_tag") + or self.bump_settings.get("annotated_tag_message") + ), msg=self.bump_settings.get("annotated_tag_message", None), # TODO: also get from self.config.settings? ) diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 10d55b8621..2992a02b0c 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -29,36 +29,36 @@ class CzSettings(TypedDict, total=False): class Settings(TypedDict, total=False): - name: str - version: str | None - version_files: list[str] - version_provider: str | None - version_scheme: str | None - version_type: str | None - tag_format: str - legacy_tag_formats: Sequence[str] - ignored_tag_formats: Sequence[str] - bump_message: str | None - retry_after_failure: bool allow_abort: bool allowed_prefixes: list[str] + always_signoff: bool + bump_message: str | None changelog_file: str changelog_format: str | None changelog_incremental: bool - changelog_start_rev: str | None changelog_merge_prerelease: bool - update_changelog_on_bump: bool - use_shortcuts: bool - style: list[tuple[str, str]] + changelog_start_rev: str | None customize: CzSettings + encoding: str + extras: dict[str, Any] + ignored_tag_formats: Sequence[str] + legacy_tag_formats: Sequence[str] major_version_zero: bool - pre_bump_hooks: list[str] | None + name: str post_bump_hooks: list[str] | None + pre_bump_hooks: list[str] | None prerelease_offset: int - encoding: str - always_signoff: bool + retry_after_failure: bool + style: list[tuple[str, str]] + tag_format: str template: str | None - extras: dict[str, Any] + update_changelog_on_bump: bool + use_shortcuts: bool + version_files: list[str] + version_provider: str | None + version_scheme: str | None + version_type: str | None + version: str | None CONFIG_FILES: list[str] = [ diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index 41da985704..64b810e4de 100644 --- a/tests/commands/test_bump_command.py +++ b/tests/commands/test_bump_command.py @@ -1721,7 +1721,7 @@ def test_is_initial_tag(mocker: MockFixture, tmp_commitizen_project): "extras": None, } - bump_cmd = bump.Bump(config, arguments) + bump_cmd = bump.Bump(config, arguments) # type: ignore # Test case 1: No current tag, not yes mode mocker.patch("questionary.confirm", return_value=mocker.Mock(ask=lambda: True)) From b7af25b49a1ed2ee6b22fa05cadd4c90be5b9bd8 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 1 Jun 2025 00:57:10 +0800 Subject: [PATCH 16/31] refactor(changelog): type untyped arguments --- commitizen/commands/bump.py | 4 ++-- commitizen/commands/changelog.py | 38 +++++++++++++++++++++++--------- commitizen/defaults.py | 1 + 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index aa5d60ce1a..c656d00070 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -342,14 +342,14 @@ def __call__(self) -> None: "dry_run": dry_run, } if self.changelog_to_stdout: - changelog_cmd = Changelog(self.config, {**args, "dry_run": True}) + changelog_cmd = Changelog(self.config, {**args, "dry_run": True}) # type: ignore try: changelog_cmd() except DryRunExit: pass args["file_name"] = self.file_name - changelog_cmd = Changelog(self.config, args) + changelog_cmd = Changelog(self.config, args) # type: ignore changelog_cmd() files.append(changelog_cmd.file_name) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 59d799b037..6d150b03f9 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -2,11 +2,11 @@ import os import os.path -from collections.abc import Generator, Iterable, Mapping +from collections.abc import Generator, Iterable from difflib import SequenceMatcher from operator import itemgetter from pathlib import Path -from typing import Any, cast +from typing import Any, TypedDict, cast from commitizen import changelog, defaults, factory, git, out from commitizen.changelog_formats import get_changelog_format @@ -26,10 +26,28 @@ from commitizen.version_schemes import get_version_scheme +class ChangelogArgs(TypedDict, total=False): + change_type_map: dict[str, str] + change_type_order: list[str] + current_version: str + dry_run: bool + file_name: str + incremental: bool + merge_prerelease: bool + rev_range: str + start_rev: str + tag_format: str + unreleased_version: str | None + version_scheme: str + template: str + extras: dict[str, Any] + export_template: str + + class Changelog: """Generate a changelog based on the commit history.""" - def __init__(self, config: BaseConfig, args: Mapping[str, Any]) -> None: + def __init__(self, config: BaseConfig, args: ChangelogArgs) -> None: if not git.is_git_project(): raise NotAGitProjectError() @@ -59,17 +77,17 @@ def __init__(self, config: BaseConfig, args: Mapping[str, Any]) -> None: self.changelog_format = get_changelog_format(self.config, self.file_name) - self.incremental = args["incremental"] or self.config.settings.get( - "changelog_incremental" + self.incremental = bool( + args.get("incremental") or self.config.settings.get("changelog_incremental") ) - self.dry_run = args["dry_run"] + self.dry_run = bool(args.get("dry_run")) self.scheme = get_version_scheme( self.config.settings, args.get("version_scheme") ) current_version = ( - args.get("current_version", config.settings.get("version")) or "" + args.get("current_version") or self.config.settings.get("version") or "" ) self.current_version = self.scheme(current_version) if current_version else None @@ -84,9 +102,7 @@ def __init__(self, config: BaseConfig, args: Mapping[str, Any]) -> None: or defaults.CHANGE_TYPE_ORDER, ) self.rev_range = args.get("rev_range") - self.tag_format: str = ( - args.get("tag_format") or self.config.settings["tag_format"] - ) + self.tag_format = args.get("tag_format") or self.config.settings["tag_format"] self.tag_rules = TagRules( scheme=self.scheme, tag_format=self.tag_format, @@ -164,7 +180,7 @@ def __call__(self) -> None: start_rev = self.start_rev unreleased_version = self.unreleased_version changelog_meta = changelog.Metadata() - change_type_map: dict[str, str] | None = self.change_type_map # type: ignore + change_type_map: dict[str, str] | None = self.change_type_map changelog_message_builder_hook: MessageBuilderHook | None = ( self.cz.changelog_message_builder_hook ) diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 2992a02b0c..5ed75589f8 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -37,6 +37,7 @@ class Settings(TypedDict, total=False): changelog_format: str | None changelog_incremental: bool changelog_merge_prerelease: bool + change_type_map: dict[str, str] changelog_start_rev: str | None customize: CzSettings encoding: str From 9c5c4b84c45702c25950b878e53b6b64db98d0d5 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 1 Jun 2025 00:59:21 +0800 Subject: [PATCH 17/31] refactor(check): remove unused argument --- commitizen/commands/check.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/commitizen/commands/check.py b/commitizen/commands/check.py index 82a8039fa3..a293b6f855 100644 --- a/commitizen/commands/check.py +++ b/commitizen/commands/check.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os import re import sys from typing import Any @@ -17,9 +16,7 @@ class Check: """Check if the current commit msg matches the commitizen format.""" - def __init__( - self, config: BaseConfig, arguments: dict[str, Any], cwd: str = os.getcwd() - ) -> None: + def __init__(self, config: BaseConfig, arguments: dict[str, Any]) -> None: """Initial check command. Args: From bb022ad6f76154ee02c22c35596a202c672a883c Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 1 Jun 2025 01:01:08 +0800 Subject: [PATCH 18/31] style(bump): rename class for consistency --- commitizen/commands/bump.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index c656d00070..6468733fc0 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -37,7 +37,7 @@ logger = getLogger("commitizen") -class BumpArguments(Settings, total=False): +class BumpArgs(Settings, total=False): allow_no_commit: bool | None annotated_tag_message: str | None annotated_tag: bool @@ -65,7 +65,7 @@ class BumpArguments(Settings, total=False): class Bump: """Show prompt for the user to create a guided commit.""" - def __init__(self, config: BaseConfig, arguments: BumpArguments) -> None: + def __init__(self, config: BaseConfig, arguments: BumpArgs) -> None: if not git.is_git_project(): raise NotAGitProjectError() @@ -73,7 +73,7 @@ def __init__(self, config: BaseConfig, arguments: BumpArguments) -> None: self.encoding = config.settings["encoding"] self.arguments = arguments self.bump_settings = cast( - BumpArguments, + BumpArgs, { **config.settings, **{ From a6f4e9f62be6aa8374767c1e6cbd288a3765cce4 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 1 Jun 2025 01:08:03 +0800 Subject: [PATCH 19/31] refactor(check): type CheckArgs arguments --- commitizen/commands/check.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/commitizen/commands/check.py b/commitizen/commands/check.py index a293b6f855..ba0beeeb4e 100644 --- a/commitizen/commands/check.py +++ b/commitizen/commands/check.py @@ -2,7 +2,7 @@ import re import sys -from typing import Any +from typing import TypedDict from commitizen import factory, git, out from commitizen.config import BaseConfig @@ -13,10 +13,20 @@ ) +class CheckArgs(TypedDict, total=False): + commit_msg_file: str + commit_msg: str + rev_range: str + allow_abort: bool + message_length_limit: int + allowed_prefixes: list[str] + message: str + + class Check: """Check if the current commit msg matches the commitizen format.""" - def __init__(self, config: BaseConfig, arguments: dict[str, Any]) -> None: + def __init__(self, config: BaseConfig, arguments: CheckArgs) -> None: """Initial check command. Args: @@ -24,16 +34,15 @@ def __init__(self, config: BaseConfig, arguments: dict[str, Any]) -> None: arguments: All the flags provided by the user cwd: Current work directory """ - self.commit_msg_file: str | None = arguments.get("commit_msg_file") - self.commit_msg: str | None = arguments.get("message") - self.rev_range: str | None = arguments.get("rev_range") - self.allow_abort: bool = bool( + self.commit_msg_file = arguments.get("commit_msg_file") + self.commit_msg = arguments.get("message") + self.rev_range = arguments.get("rev_range") + self.allow_abort = bool( arguments.get("allow_abort", config.settings["allow_abort"]) ) - self.max_msg_length: int = arguments.get("message_length_limit", 0) + self.max_msg_length = arguments.get("message_length_limit", 0) # we need to distinguish between None and [], which is a valid value - allowed_prefixes = arguments.get("allowed_prefixes") self.allowed_prefixes: list[str] = ( allowed_prefixes From f45511fee40c42a679f10658738d2e73c366035d Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 1 Jun 2025 01:16:52 +0800 Subject: [PATCH 20/31] refactor(commit): type commit args --- commitizen/commands/commit.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/commitizen/commands/commit.py b/commitizen/commands/commit.py index 68095d0e0c..43feb272ba 100644 --- a/commitizen/commands/commit.py +++ b/commitizen/commands/commit.py @@ -6,7 +6,7 @@ import subprocess import tempfile from pathlib import Path -from typing import Union, cast +from typing import TypedDict import questionary @@ -28,10 +28,22 @@ from commitizen.git import smart_open +class CommitArgs(TypedDict, total=False): + all: bool + dry_run: bool + edit: bool + extra_cli_args: str + message_length_limit: int + no_retry: bool + signoff: bool + write_message_to_file: Path | None + retry: bool + + class Commit: """Show prompt for the user to create a guided commit.""" - def __init__(self, config: BaseConfig, arguments: dict) -> None: + def __init__(self, config: BaseConfig, arguments: CommitArgs) -> None: if not git.is_git_project(): raise NotAGitProjectError() @@ -69,7 +81,7 @@ def _prompt_commit_questions(self) -> str: message = cz.message(answers) message_len = len(message.partition("\n")[0].strip()) - message_length_limit: int = self.arguments.get("message_length_limit", 0) + message_length_limit = self.arguments.get("message_length_limit", 0) if 0 < message_length_limit < message_len: raise CommitMessageLengthExceededError( f"Length of commit message exceeds limit ({message_len}/{message_length_limit})" @@ -108,12 +120,10 @@ def _get_message(self) -> str: return self._prompt_commit_questions() def __call__(self) -> None: - extra_args = cast(str, self.arguments.get("extra_cli_args", "")) - dry_run = cast(bool, self.arguments.get("dry_run")) - write_message_to_file = cast( - Union[Path, None], self.arguments.get("write_message_to_file") - ) - signoff = cast(bool, self.arguments.get("signoff")) + extra_args = self.arguments.get("extra_cli_args", "") + dry_run = bool(self.arguments.get("dry_run")) + write_message_to_file = self.arguments.get("write_message_to_file") + signoff = bool(self.arguments.get("signoff")) if self.arguments.get("all"): git.add("-u") From bf4f0551d69aa7ca2aaad197eecd9ae384442501 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 1 Jun 2025 01:30:59 +0800 Subject: [PATCH 21/31] refactor(commands): remove unused args, type version command args --- commitizen/commands/version.py | 13 +++++++++---- tests/commands/test_version_command.py | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/commitizen/commands/version.py b/commitizen/commands/version.py index ecf1f03129..6b0aa331ad 100644 --- a/commitizen/commands/version.py +++ b/commitizen/commands/version.py @@ -1,7 +1,6 @@ import platform import sys -from collections.abc import Mapping -from typing import Any +from typing import TypedDict from commitizen import out from commitizen.__version__ import __version__ @@ -9,12 +8,18 @@ from commitizen.providers import get_provider +class VersionArgs(TypedDict, total=False): + report: bool + project: bool + verbose: bool + + class Version: """Get the version of the installed commitizen or the current project.""" - def __init__(self, config: BaseConfig, *args: Mapping[str, Any]) -> None: + def __init__(self, config: BaseConfig, arguments: VersionArgs) -> None: self.config: BaseConfig = config - self.parameter = args[0] + self.parameter = arguments self.operating_system = platform.system() self.python_version = sys.version diff --git a/tests/commands/test_version_command.py b/tests/commands/test_version_command.py index 927cf55f25..3dcbed168b 100644 --- a/tests/commands/test_version_command.py +++ b/tests/commands/test_version_command.py @@ -97,7 +97,6 @@ def test_version_use_version_provider( { "report": False, "project": project, - "commitizen": False, "verbose": not project, }, )() From 612d0b71011f1d9f1b777f9d8b325eff6df86d58 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 1 Jun 2025 01:38:22 +0800 Subject: [PATCH 22/31] style(cli): shorten arg type --- commitizen/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index c11e1c6443..2e8764911c 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -3,7 +3,6 @@ import argparse import logging import sys -from collections.abc import Sequence from copy import deepcopy from functools import partial from pathlib import Path @@ -48,7 +47,7 @@ def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - kwarg: str | Sequence[object] | None, + kwarg: object, option_string: str | None = None, ) -> None: if not isinstance(kwarg, str): From d5f9bfd503c40a2c1332e3836ba8b42700d42959 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 1 Jun 2025 01:41:39 +0800 Subject: [PATCH 23/31] docs(bump): comment on a stupid looking pattern --- commitizen/commands/bump.py | 1 + 1 file changed, 1 insertion(+) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 6468733fc0..80ed6b0fd9 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -79,6 +79,7 @@ def __init__(self, config: BaseConfig, arguments: BumpArgs) -> None: **{ k: v for k, v in { + # All these are for making mypy happy "annotated_tag_message": arguments.get("annotated_tag_message"), "annotated_tag": arguments.get("annotated_tag"), "bump_message": arguments.get("bump_message"), From d1efd89cb33f1e307d6e3119548e748f398e172c Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 1 Jun 2025 01:50:37 +0800 Subject: [PATCH 24/31] fix(Check): make parameters backward compatiable --- commitizen/commands/check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commitizen/commands/check.py b/commitizen/commands/check.py index ba0beeeb4e..8a7d0dd019 100644 --- a/commitizen/commands/check.py +++ b/commitizen/commands/check.py @@ -26,7 +26,7 @@ class CheckArgs(TypedDict, total=False): class Check: """Check if the current commit msg matches the commitizen format.""" - def __init__(self, config: BaseConfig, arguments: CheckArgs) -> None: + def __init__(self, config: BaseConfig, arguments: CheckArgs, *args: object) -> None: """Initial check command. Args: From 8ab16a458001b8f7b9ea04dfcfdd382f333e174f Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 1 Jun 2025 01:52:12 +0800 Subject: [PATCH 25/31] style(changelog): rename parameter for consistency --- commitizen/commands/changelog.py | 33 ++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 6d150b03f9..f0dd1c516c 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -47,13 +47,13 @@ class ChangelogArgs(TypedDict, total=False): class Changelog: """Generate a changelog based on the commit history.""" - def __init__(self, config: BaseConfig, args: ChangelogArgs) -> None: + def __init__(self, config: BaseConfig, arguments: ChangelogArgs) -> None: if not git.is_git_project(): raise NotAGitProjectError() self.config = config - changelog_file_name = args.get("file_name") or self.config.settings.get( + changelog_file_name = arguments.get("file_name") or self.config.settings.get( "changelog_file" ) if not isinstance(changelog_file_name, str): @@ -71,27 +71,30 @@ def __init__(self, config: BaseConfig, args: ChangelogArgs) -> None: self.encoding = self.config.settings["encoding"] self.cz = factory.committer_factory(self.config) - self.start_rev = args.get("start_rev") or self.config.settings.get( + self.start_rev = arguments.get("start_rev") or self.config.settings.get( "changelog_start_rev" ) self.changelog_format = get_changelog_format(self.config, self.file_name) self.incremental = bool( - args.get("incremental") or self.config.settings.get("changelog_incremental") + arguments.get("incremental") + or self.config.settings.get("changelog_incremental") ) - self.dry_run = bool(args.get("dry_run")) + self.dry_run = bool(arguments.get("dry_run")) self.scheme = get_version_scheme( - self.config.settings, args.get("version_scheme") + self.config.settings, arguments.get("version_scheme") ) current_version = ( - args.get("current_version") or self.config.settings.get("version") or "" + arguments.get("current_version") + or self.config.settings.get("version") + or "" ) self.current_version = self.scheme(current_version) if current_version else None - self.unreleased_version = args["unreleased_version"] + self.unreleased_version = arguments["unreleased_version"] self.change_type_map = ( self.config.settings.get("change_type_map") or self.cz.change_type_map ) @@ -101,24 +104,26 @@ def __init__(self, config: BaseConfig, args: ChangelogArgs) -> None: or self.cz.change_type_order or defaults.CHANGE_TYPE_ORDER, ) - self.rev_range = args.get("rev_range") - self.tag_format = args.get("tag_format") or self.config.settings["tag_format"] + self.rev_range = arguments.get("rev_range") + self.tag_format = ( + arguments.get("tag_format") or self.config.settings["tag_format"] + ) self.tag_rules = TagRules( scheme=self.scheme, tag_format=self.tag_format, legacy_tag_formats=self.config.settings["legacy_tag_formats"], ignored_tag_formats=self.config.settings["ignored_tag_formats"], - merge_prereleases=args.get("merge_prerelease") + merge_prereleases=arguments.get("merge_prerelease") or self.config.settings["changelog_merge_prerelease"], ) self.template = ( - args.get("template") + arguments.get("template") or self.config.settings.get("template") or self.changelog_format.template ) - self.extras = args.get("extras") or {} - self.export_template_to = args.get("export_template") + self.extras = arguments.get("extras") or {} + self.export_template_to = arguments.get("export_template") def _find_incremental_rev(self, latest_version: str, tags: Iterable[GitTag]) -> str: """Try to find the 'start_rev'. From bd7b80f87ad7b75cb8cb8ac1eb6944d38908c0a4 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 1 Jun 2025 01:55:38 +0800 Subject: [PATCH 26/31] refactor(bump): improve readability and still bypass mypy check --- commitizen/commands/bump.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 80ed6b0fd9..9c259ca63d 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -78,22 +78,21 @@ def __init__(self, config: BaseConfig, arguments: BumpArgs) -> None: **config.settings, **{ k: v - for k, v in { - # All these are for making mypy happy - "annotated_tag_message": arguments.get("annotated_tag_message"), - "annotated_tag": arguments.get("annotated_tag"), - "bump_message": arguments.get("bump_message"), - "file_name": arguments.get("file_name"), - "gpg_sign": arguments.get("gpg_sign"), - "increment_mode": arguments.get("increment_mode"), - "increment": arguments.get("increment"), - "major_version_zero": arguments.get("major_version_zero"), - "prerelease_offset": arguments.get("prerelease_offset"), - "prerelease": arguments.get("prerelease"), - "tag_format": arguments.get("tag_format"), - "template": arguments.get("template"), - }.items() - if v is not None + for k in ( + "annotated_tag_message", + "annotated_tag", + "bump_message", + "file_name", + "gpg_sign", + "increment_mode", + "increment", + "major_version_zero", + "prerelease_offset", + "prerelease", + "tag_format", + "template", + ) + if (v := arguments.get(k)) is not None }, }, ) From a35196d675899556b917d05382642bb4adc54028 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Sun, 1 Jun 2025 02:05:23 +0800 Subject: [PATCH 27/31] refactor: remove unnecessary bool() and remove Any type from TypedDict get --- commitizen/commands/bump.py | 8 ++------ commitizen/defaults.py | 4 +++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index 9c259ca63d..cec395599e 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -40,7 +40,6 @@ class BumpArgs(Settings, total=False): allow_no_commit: bool | None annotated_tag_message: str | None - annotated_tag: bool build_metadata: str | None changelog_to_stdout: bool changelog: bool @@ -51,7 +50,6 @@ class BumpArgs(Settings, total=False): files_only: bool | None get_next: bool git_output_to_stderr: bool - gpg_sign: bool increment_mode: str increment: Increment | None local_version: bool @@ -222,7 +220,7 @@ def __call__(self) -> None: if get_next: # if trying to use --get-next, we should not allow --changelog or --changelog-to-stdout - if self.changelog_flag or bool(self.changelog_to_stdout): + if self.changelog_flag or self.changelog_to_stdout: raise NotAllowed( "--changelog or --changelog-to-stdout is not allowed with --get-next" ) @@ -234,9 +232,7 @@ def __call__(self) -> None: # If user specified changelog_to_stdout, they probably want the # changelog to be generated as well, this is the most intuitive solution self.changelog_flag = bool( - self.changelog_flag - or bool(self.changelog_to_stdout) - or self.changelog_config + self.changelog_flag or self.changelog_to_stdout or self.changelog_config ) rules = TagRules.from_settings(cast(Settings, self.bump_settings)) diff --git a/commitizen/defaults.py b/commitizen/defaults.py index 5ed75589f8..2b57609094 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -32,16 +32,18 @@ class Settings(TypedDict, total=False): allow_abort: bool allowed_prefixes: list[str] always_signoff: bool + annotated_tag: bool bump_message: str | None + change_type_map: dict[str, str] changelog_file: str changelog_format: str | None changelog_incremental: bool changelog_merge_prerelease: bool - change_type_map: dict[str, str] changelog_start_rev: str | None customize: CzSettings encoding: str extras: dict[str, Any] + gpg_sign: bool ignored_tag_formats: Sequence[str] legacy_tag_formats: Sequence[str] major_version_zero: bool From d63285e5876e7fd0640ee5b05a93acc9d1505ac4 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Wed, 4 Jun 2025 23:24:59 +0800 Subject: [PATCH 28/31] style(changelog): add TODO to fixable type ignores --- commitizen/commands/changelog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index f0dd1c516c..8ca36bdb9a 100644 --- a/commitizen/commands/changelog.py +++ b/commitizen/commands/changelog.py @@ -176,6 +176,7 @@ def _write_changelog( def _export_template(self) -> None: tpl = changelog.get_changelog_template(self.cz.template_loader, self.template) + # TODO: fix the following type ignores src = Path(tpl.filename) # type: ignore Path(self.export_template_to).write_text(src.read_text()) # type: ignore From e4674632f712e8db39c4a8665cb6517494afa0ee Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Wed, 4 Jun 2025 23:26:37 +0800 Subject: [PATCH 29/31] style(cli): more specific type ignore --- commitizen/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index 2e8764911c..c7cef187bf 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -681,7 +681,7 @@ def main() -> None: ) sys.excepthook = no_raise_debug_excepthook - args.func(conf, arguments)() # type: ignore + args.func(conf, arguments)() # type: ignore[arg-type] if __name__ == "__main__": From d2e182108d54dc6e9e63788e01b585f6adebaf4e Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Wed, 4 Jun 2025 23:35:19 +0800 Subject: [PATCH 30/31] style(cli): rename kwarg to values --- commitizen/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index c7cef187bf..fcd9acf3e4 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -47,17 +47,17 @@ def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - kwarg: object, + values: object, option_string: str | None = None, ) -> None: - if not isinstance(kwarg, str): + if not isinstance(values, str): return - if "=" not in kwarg: + if "=" not in values: raise InvalidCommandArgumentError( f"Option {option_string} expect a key=value format" ) kwargs = getattr(namespace, self.dest, None) or {} - key, value = kwarg.split("=", 1) + key, value = values.split("=", 1) if not key: raise InvalidCommandArgumentError( f"Option {option_string} expect a key=value format" From 31194f7852597c4314bf936949b8970cc2c10822 Mon Sep 17 00:00:00 2001 From: Yu-Ting Hsiung Date: Fri, 6 Jun 2025 11:24:46 +0800 Subject: [PATCH 31/31] refactor(bump): use any to replace 'or' chain --- commitizen/cli.py | 2 +- commitizen/commands/bump.py | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/commitizen/cli.py b/commitizen/cli.py index fcd9acf3e4..4f4f3098db 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -681,7 +681,7 @@ def main() -> None: ) sys.excepthook = no_raise_debug_excepthook - args.func(conf, arguments)() # type: ignore[arg-type] + args.func(conf, arguments)() if __name__ == "__main__": diff --git a/commitizen/commands/bump.py b/commitizen/commands/bump.py index cec395599e..4f1ee48288 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -231,8 +231,8 @@ def __call__(self) -> None: else: # If user specified changelog_to_stdout, they probably want the # changelog to be generated as well, this is the most intuitive solution - self.changelog_flag = bool( - self.changelog_flag or self.changelog_to_stdout or self.changelog_config + self.changelog_flag = any( + (self.changelog_flag, self.changelog_to_stdout, self.changelog_config) ) rules = TagRules.from_settings(cast(Settings, self.bump_settings)) @@ -410,14 +410,18 @@ def __call__(self) -> None: c = git.tag( new_tag_version, - signed=bool( - self.bump_settings.get("gpg_sign") - or self.config.settings.get("gpg_sign") + signed=any( + ( + self.bump_settings.get("gpg_sign"), + self.config.settings.get("gpg_sign"), + ) ), - annotated=bool( - self.bump_settings.get("annotated_tag") - or self.config.settings.get("annotated_tag") - or self.bump_settings.get("annotated_tag_message") + annotated=any( + ( + self.bump_settings.get("annotated_tag"), + self.config.settings.get("annotated_tag"), + self.bump_settings.get("annotated_tag_message"), + ) ), msg=self.bump_settings.get("annotated_tag_message", None), # TODO: also get from self.config.settings?