diff --git a/commitizen/changelog.py b/commitizen/changelog.py index bac4d824f9..ba6fbbc6b3 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 @@ -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, @@ -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/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/cli.py b/commitizen/cli.py index deb5644d27..4f4f3098db 100644 --- a/commitizen/cli.py +++ b/commitizen/cli.py @@ -3,12 +3,11 @@ import argparse import logging import sys -from collections.abc import Sequence from copy import deepcopy from functools import partial from pathlib import Path from types import TracebackType -from typing import Any +from typing import TYPE_CHECKING import argcomplete from decli import cli @@ -48,17 +47,17 @@ def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - kwarg: str | Sequence[Any] | None, + values: object, option_string: str | None = None, - ): - if not isinstance(kwarg, str): + ) -> None: + 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" @@ -550,8 +549,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 +584,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 +599,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..3f13087233 100644 --- a/commitizen/cmd.py +++ b/commitizen/cmd.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import os import subprocess +from collections.abc import Mapping from typing import NamedTuple from charset_normalizer import from_bytes @@ -28,7 +31,7 @@ def _try_decode(bytes_: bytes) -> str: raise CharacterSetDecodeError() from e -def run(cmd: str, env=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/commands/bump.py b/commitizen/commands/bump.py index a2d98c2451..4f1ee48288 100644 --- a/commitizen/commands/bump.py +++ b/commitizen/commands/bump.py @@ -37,37 +37,63 @@ logger = getLogger("commitizen") +class BumpArgs(Settings, total=False): + allow_no_commit: bool | None + annotated_tag_message: str | None + 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 + 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): + def __init__(self, config: BaseConfig, arguments: BumpArgs) -> 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( + BumpArgs, + { + **config.settings, + **{ + k: v + 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 + }, }, - } + ) self.cz = factory.committer_factory(self.config) self.changelog_flag = arguments["changelog"] self.changelog_config = self.config.settings.get("update_changelog_on_bump") @@ -101,7 +127,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,13 +144,12 @@ 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 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 @@ -135,7 +160,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) @@ -144,23 +169,14 @@ def __call__(self) -> None: # noqa: C901 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 +198,7 @@ def __call__(self) -> None: # noqa: C901 "--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 +206,7 @@ def __call__(self) -> None: # noqa: C901 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}" @@ -204,7 +220,7 @@ def __call__(self) -> None: # noqa: C901 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" ) @@ -215,10 +231,8 @@ def __call__(self) -> None: # noqa: C901 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 - or bool(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)) @@ -227,7 +241,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, self.arguments["yes"]) if manual_version: try: @@ -255,7 +269,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) @@ -273,16 +287,16 @@ def __call__(self) -> None: # noqa: C901 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 +328,7 @@ def __call__(self) -> None: # noqa: C901 ) files: list[str] = [] + dry_run = self.arguments["dry_run"] if self.changelog_flag: args = { "unreleased_version": new_tag_version, @@ -323,14 +338,14 @@ def __call__(self) -> None: # noqa: C901 "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) @@ -342,7 +357,7 @@ def __call__(self) -> None: # noqa: C901 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 +381,7 @@ def __call__(self) -> None: # noqa: C901 else None, ) - if is_files_only: + if self.arguments["files_only"]: raise ExpectedExit() # FIXME: check if any changes have been staged @@ -395,11 +410,19 @@ def __call__(self) -> None: # noqa: C901 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=any( + ( + self.bump_settings.get("gpg_sign"), + self.config.settings.get("gpg_sign"), + ) + ), + 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? ) diff --git a/commitizen/commands/changelog.py b/commitizen/commands/changelog.py index 7bf644a934..8ca36bdb9a 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 from difflib import SequenceMatcher from operator import itemgetter from pathlib import Path +from typing import Any, TypedDict, cast from commitizen import changelog, defaults, factory, git, out from commitizen.changelog_formats import get_changelog_format @@ -25,16 +26,34 @@ 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): + 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): @@ -52,57 +71,61 @@ def __init__(self, config: BaseConfig, args): 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 = args["incremental"] or self.config.settings.get( - "changelog_incremental" + self.incremental = bool( + arguments.get("incremental") + or self.config.settings.get("changelog_incremental") ) - self.dry_run = args["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", 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 ) - 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 = ( - 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: 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 @@ -134,9 +157,9 @@ def _find_incremental_rev(self, latest_version: str, tags: list[GitTag]) -> str: raise NoRevisionError() return start_rev - def write_changelog( + 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: @@ -151,18 +174,19 @@ 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()) + # TODO: fix the following type ignores + 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 changelog_message_builder_hook: MessageBuilderHook | None = ( self.cz.changelog_message_builder_hook ) @@ -171,7 +195,7 @@ def __call__(self): ) if self.export_template_to: - return self.export_template() + return self._export_template() if not changelog_pattern or not commit_parser: raise NoPatternMapError( @@ -190,7 +214,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( @@ -238,4 +262,4 @@ def __call__(self): 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/check.py b/commitizen/commands/check.py index 1e3b8464e1..8a7d0dd019 100644 --- a/commitizen/commands/check.py +++ b/commitizen/commands/check.py @@ -1,9 +1,8 @@ from __future__ import annotations -import os import re import sys -from typing import Any +from typing import TypedDict from commitizen import factory, git, out from commitizen.config import BaseConfig @@ -14,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], cwd=os.getcwd()): + def __init__(self, config: BaseConfig, arguments: CheckArgs, *args: object) -> None: """Initial check command. Args: @@ -25,16 +34,15 @@ def __init__(self, config: BaseConfig, arguments: dict[str, Any], cwd=os.getcwd( 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 @@ -48,7 +56,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 +69,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 +105,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..43feb272ba 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 TypedDict import questionary @@ -26,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): + def __init__(self, config: BaseConfig, arguments: CommitArgs) -> None: if not git.is_git_project(): raise NotAGitProjectError() @@ -39,7 +53,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 @@ -48,7 +62,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() @@ -67,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})" @@ -94,7 +108,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 @@ -102,14 +116,14 @@ 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() - - 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") + return self._read_backup_message() or self._prompt_commit_questions() + return self._prompt_commit_questions() + + def __call__(self) -> None: + 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") diff --git a/commitizen/commands/example.py b/commitizen/commands/example.py index a28ad85f16..ba9f34adc4 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) -> None: 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/info.py b/commitizen/commands/info.py index abd4197e7f..5ea8227313 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) -> None: 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..1207cd02ee 100644 --- a/commitizen/commands/init.py +++ b/commitizen/commands/init.py @@ -79,13 +79,13 @@ def is_pre_commit_installed(self) -> bool: class Init: - def __init__(self, config: BaseConfig, *args): + def __init__(self, config: BaseConfig, *args: object) -> None: self.config: BaseConfig = config self.encoding = config.settings["encoding"] 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 @@ -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..412266f6c3 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) -> None: self.config: BaseConfig = config - def __call__(self): + def __call__(self) -> None: out.write("\n".join(registry.keys())) diff --git a/commitizen/commands/schema.py b/commitizen/commands/schema.py index 4af5679cf5..a7aeb53569 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) -> None: 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/commands/version.py b/commitizen/commands/version.py index 45d553c710..6b0aa331ad 100644 --- a/commitizen/commands/version.py +++ b/commitizen/commands/version.py @@ -1,5 +1,6 @@ import platform import sys +from typing import TypedDict from commitizen import out from commitizen.__version__ import __version__ @@ -7,16 +8,22 @@ 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): + 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 - 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..59c0d16a06 100644 --- a/commitizen/config/base_config.py +++ b/commitizen/config/base_config.py @@ -1,12 +1,22 @@ 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): + def __init__(self) -> None: self._settings: Settings = DEFAULT_SETTINGS.copy() self.encoding = self.settings["encoding"] self._path: Path | None = None @@ -16,19 +26,14 @@ 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, value): + def set_key(self, key: str, value: Any) -> Self: """Set or update a key in the conf. For now only strings are supported. @@ -41,3 +46,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/commitizen/config/json_config.py b/commitizen/config/json_config.py index d413d73383..be1f1c36b0 100644 --- a/commitizen/config/json_config.py +++ b/commitizen/config/json_config.py @@ -2,25 +2,35 @@ 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): + def __init__(self, *, data: bytes | str, path: Path | str) -> None: 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): + 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 e2cfcc9340..3571c9c882 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,15 +10,24 @@ 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): + def __init__(self, *, data: bytes | str, path: Path | str) -> None: 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): + 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 c5721c8d4b..f2a79e6937 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,15 +10,24 @@ 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): + def __init__(self, *, data: bytes | str, path: Path | str) -> None: 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): + 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/cz/conventional_commits/conventional_commits.py b/commitizen/cz/conventional_commits/conventional_commits.py index 9a2b9016b7..e748b950c6 100644 --- a/commitizen/cz/conventional_commits/conventional_commits.py +++ b/commitizen/cz/conventional_commits/conventional_commits.py @@ -9,29 +9,19 @@ __all__ = ["ConventionalCommitsCz"] -def parse_scope(text): - 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): - 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): 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", @@ -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/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/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/commitizen/cz/utils.py b/commitizen/cz/utils.py index cb79d65d1a..a6f687226c 100644 --- a/commitizen/cz/utils.py +++ b/commitizen/cz/utils.py @@ -8,13 +8,13 @@ _RE_LOCAL_VERSION = re.compile(r"\+.+") -def required_validator(answer, msg=None): +def required_validator(answer: str, msg: object = 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/defaults.py b/commitizen/defaults.py index 10d55b8621..2b57609094 100644 --- a/commitizen/defaults.py +++ b/commitizen/defaults.py @@ -29,36 +29,39 @@ 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 + annotated_tag: bool + bump_message: str | None + change_type_map: dict[str, str] 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] + gpg_sign: bool + 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/commitizen/exceptions.py b/commitizen/exceptions.py index 29733b624b..8c0956be53 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) -> None: 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) -> 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 ab2866ac48..fb59750eaf 100644 --- a/commitizen/git.py +++ b/commitizen/git.py @@ -46,20 +46,20 @@ 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 = "", parents: list[str] | None = None, - ): + ) -> None: self.rev = rev.strip() self.title = title.strip() self.body = body.strip() @@ -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) -> None: 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) @@ -211,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 @@ -308,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] # noqa: ANN201 """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/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/commitizen/providers/base_provider.py b/commitizen/providers/base_provider.py index 34048318e2..27c3123416 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 @@ -19,7 +20,7 @@ class VersionProvider(ABC): config: BaseConfig - def __init__(self, config: BaseConfig): + def __init__(self, config: BaseConfig) -> None: self.config = config @abstractmethod @@ -29,7 +30,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,15 +59,15 @@ 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") - 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): + def set(self, document: dict[str, Any], version: str) -> None: document["version"] = version @@ -79,7 +80,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 +88,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/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 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 diff --git a/commitizen/tags.py b/commitizen/tags.py index c5f06884fe..b19bb89e09 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,15 +236,15 @@ 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 - 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( 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. 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..aa42a9eb13 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" @@ -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,7 @@ known-first-party = ["commitizen", "tests"] convention = "google" [tool.mypy] -files = "commitizen" +files = ["commitizen", "tests"] disallow_untyped_decorators = true disallow_subclassing_any = true warn_return_any = true @@ -228,14 +234,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" diff --git a/tests/commands/test_bump_command.py b/tests/commands/test_bump_command.py index 2af7bec121..64b810e4de 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") @@ -1721,19 +1721,19 @@ 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)) - 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 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, }, )() 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 6d4e0f7435..c89ddf6ae5 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): @@ -89,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" ) @@ -107,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" ) 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"