Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions commitizen/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from dataclasses import dataclass
from datetime import date
from itertools import chain
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast

from deprecated import deprecated
from jinja2 import (
Expand Down Expand Up @@ -192,10 +192,13 @@ def process_commit_message(

messages = [processed_msg] if isinstance(processed_msg, dict) else processed_msg
for msg in messages:
change_type = msg.pop("change_type", None)
if not isinstance(msg, dict):
continue
msg_dict = cast("dict[str, Any]", msg)
change_type = msg_dict.pop("change_type", None)
if change_type_map and change_type:
change_type = change_type_map.get(change_type, change_type)
ref_changes[change_type].append(msg)
ref_changes[change_type].append(msg_dict)


def generate_ordered_changelog_tree(
Expand Down Expand Up @@ -272,6 +275,7 @@ def incremental_build(
skip = False
if (
latest_version_position is None
or unreleased_end is None
or latest_version_position > unreleased_end
):
continue
Expand Down
10 changes: 6 additions & 4 deletions commitizen/changelog_formats/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,16 @@ def get_changelog_format(
:raises FormatUnknown: if a non-empty name is provided but cannot be found in the known formats
"""
name: str | None = config.settings.get("changelog_format")
format = (
name and KNOWN_CHANGELOG_FORMATS.get(name) or _guess_changelog_format(filename)
format_cls: type[ChangelogFormat] | None = (
KNOWN_CHANGELOG_FORMATS.get(name) if name else None
)
if format_cls is None:
format_cls = _guess_changelog_format(filename)

if not format:
if not format_cls:
raise ChangelogFormatUnknown(f"Unknown changelog format '{name}'")

return format(config)
return format_cls(config)


def _guess_changelog_format(filename: str | None) -> type[ChangelogFormat] | None:
Expand Down
2 changes: 1 addition & 1 deletion commitizen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ def main() -> None:
if args.no_raise:
sys.excepthook = partial(sys.excepthook, no_raise=parse_no_raise(args.no_raise))

args.func(conf, arguments)() # type: ignore[arg-type]
args.func(conf, arguments)() # type: ignore # noqa: PGH003


if __name__ == "__main__":
Expand Down
9 changes: 6 additions & 3 deletions commitizen/commands/bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from commitizen import bump, factory, git, hooks, out
from commitizen.changelog_formats import get_changelog_format
from commitizen.commands.changelog import Changelog
from commitizen.commands.changelog import Changelog, ChangelogArgs
from commitizen.defaults import Settings
from commitizen.exceptions import (
BumpCommitFailedError,
Expand Down Expand Up @@ -323,14 +323,17 @@ def __call__(self) -> None:
try:
Changelog(
self.config,
{**changelog_args, "dry_run": True}, # type: ignore[typeddict-item]
{**changelog_args, "dry_run": True},
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changelog expects a ChangelogArgs TypedDict, but here a plain dict created via unpacking is passed without a cast. This was previously suppressed with a type: ignore and may now fail ty check. Cast the argument to ChangelogArgs (as is already done for the later Changelog instantiation) to keep the type checker happy and make intent explicit.

Suggested change
{**changelog_args, "dry_run": True},
cast(
"ChangelogArgs",
{**changelog_args, "dry_run": True},
),

Copilot uses AI. Check for mistakes.
)()
except DryRunExit:
pass

changelog_cmd = Changelog(
self.config,
{**changelog_args, "file_name": self.file_name}, # type: ignore[typeddict-item]
cast(
"ChangelogArgs",
{**changelog_args, "file_name": self.file_name},
),
)
changelog_cmd()
changelog_file_name = changelog_cmd.file_name
Expand Down
2 changes: 1 addition & 1 deletion commitizen/config/base_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def settings(self) -> Settings:

@property
def path(self) -> Path:
return self._path # type: ignore[return-value]
return self._path # type: ignore # noqa: PGH003

@path.setter
def path(self, path: Path) -> None:
Expand Down
6 changes: 3 additions & 3 deletions commitizen/config/toml_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def init_empty_config_content(self) -> None:

if config_doc.get("tool") is None:
config_doc["tool"] = table()
config_doc["tool"]["commitizen"] = table() # type: ignore[index]
config_doc["tool"]["commitizen"] = table() # type: ignore # noqa: PGH003

with self.path.open("wb") as output_toml_file:
output_toml_file.write(
Expand All @@ -46,7 +46,7 @@ def init_empty_config_content(self) -> None:
def set_key(self, key: str, value: object) -> Self:
config_doc = parse(self.path.read_bytes())

config_doc["tool"]["commitizen"][key] = value # type: ignore[index]
config_doc["tool"]["commitizen"][key] = value # type: ignore # noqa: PGH003
self.path.write_bytes(config_doc.as_string().encode(self._settings["encoding"]))

return self
Expand All @@ -65,6 +65,6 @@ def _parse_setting(self, data: bytes | str) -> None:
raise InvalidConfigurationError(f"Failed to parse {self.path}: {e}")

try:
self.settings.update(doc["tool"]["commitizen"]) # type: ignore[index,typeddict-item] # TODO: fix this
self.settings.update(doc["tool"]["commitizen"]) # type: ignore # noqa: PGH003 # TODO: fix this
except exceptions.NonExistentKey:
pass
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def questions(self) -> list[CzQuestion]:
},
]

def message(self, answers: ConventionalCommitsAnswers) -> str: # type: ignore[override]
def message(self, answers: ConventionalCommitsAnswers) -> str: # type: ignore # noqa: PGH003
prefix = answers["prefix"]
scope = answers["scope"]
subject = answers["subject"]
Expand Down
6 changes: 4 additions & 2 deletions commitizen/cz/customize/customize.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ def __init__(self, config: BaseConfig) -> None:
setattr(self, attr_name, value)

def questions(self) -> list[CzQuestion]:
return self.custom_settings.get("questions", [{}]) # type: ignore[return-value]
return self.custom_settings.get("questions", [{}]) # type: ignore # noqa: PGH003

def message(self, answers: Mapping[str, Any]) -> str:
message_template = Template(self.custom_settings.get("message_template", ""))
if getattr(Template, "substitute", None):
return message_template.substitute(**answers) # type: ignore[attr-defined,no-any-return] # pragma: no cover # TODO: check if we can fix this
return message_template.substitute(
**answers
) # pragma: no cover # TODO: check if we can fix this
return message_template.render(**answers)
Comment on lines 56 to 61
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch calls message_template.substitute(...) but under TYPE_CHECKING the Template type is jinja2.Template, which doesn't define substitute. This will fail static type checking (and was previously suppressed). Consider narrowing the type (e.g., separate string.Template vs jinja2.Template) or reintroducing a targeted type: ignore[attr-defined] for this call.

Copilot uses AI. Check for mistakes.

def example(self) -> str:
Expand Down
2 changes: 1 addition & 1 deletion commitizen/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ def get_core_editor() -> str | None:
return None


def smart_open(*args, **kwargs): # type: ignore[no-untyped-def,unused-ignore] # noqa: ANN201
def smart_open(*args, **kwargs): # noqa: ANN201
"""Open a file with the EOL style determined from Git."""
return open(*args, newline=EOLType.for_open(), **kwargs)
Comment on lines +319 to 321
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smart_open lacks parameter type annotations; the # noqa: ANN201 only suppresses the missing return annotation, but ruff is configured to enforce argument annotations (ANN001/ANN2). Add a typed signature (or explicitly ignore the relevant ANN codes) so poe lint passes.

Copilot uses AI. Check for mistakes.

Expand Down
4 changes: 2 additions & 2 deletions commitizen/providers/base_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def set_version(self, version: str) -> None:
self.file.write_text(tomlkit.dumps(document), encoding=self._get_encoding())

def get(self, document: tomlkit.TOMLDocument) -> str:
return document["project"]["version"] # type: ignore[index,return-value]
return document["project"]["version"] # type: ignore # noqa: PGH003

def set(self, document: tomlkit.TOMLDocument, version: str) -> None:
document["project"]["version"] = version # type: ignore[index]
document["project"]["version"] = version # type: ignore # noqa: PGH003
6 changes: 3 additions & 3 deletions commitizen/providers/cargo_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ def set_lock_version(self, version: str) -> None:
assert isinstance(packages, AoT)

try:
cargo_package_name = cargo_toml_content["package"]["name"] # type: ignore[index]
cargo_package_name = cargo_toml_content["package"]["name"] # type: ignore # noqa: PGH003
if TYPE_CHECKING:
assert isinstance(cargo_package_name, str)
for i, package in enumerate(packages):
if package["name"] == cargo_package_name:
cargo_lock_content["package"][i]["version"] = version # type: ignore[index]
cargo_lock_content["package"][i]["version"] = version # type: ignore # noqa: PGH003
break
except NonExistentKey:
workspace = cargo_toml_content.get("workspace", {})
Expand Down Expand Up @@ -94,7 +94,7 @@ def set_lock_version(self, version: str) -> None:

for i, package in enumerate(packages):
if package["name"] in members_inheriting:
cargo_lock_content["package"][i]["version"] = version # type: ignore[index]
cargo_lock_content["package"][i]["version"] = version # type: ignore # noqa: PGH003

self.lock_file.write_text(
dumps(cargo_lock_content), encoding=self._get_encoding()
Expand Down
8 changes: 4 additions & 4 deletions commitizen/providers/poetry_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class PoetryProvider(TomlProvider):

filename = "pyproject.toml"

def get(self, pyproject: tomlkit.TOMLDocument) -> str:
return pyproject["tool"]["poetry"]["version"] # type: ignore[index,return-value]
def get(self, document: tomlkit.TOMLDocument) -> str:
return document["tool"]["poetry"]["version"] # type: ignore # noqa: PGH003

def set(self, pyproject: tomlkit.TOMLDocument, version: str) -> None:
pyproject["tool"]["poetry"]["version"] = version # type: ignore[index]
def set(self, document: tomlkit.TOMLDocument, version: str) -> None:
document["tool"]["poetry"]["version"] = version # type: ignore # noqa: PGH003
6 changes: 3 additions & 3 deletions commitizen/providers/uv_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,17 @@ def set_lock_version(self, version: str) -> None:
pyproject_toml_content = tomlkit.parse(
self.file.read_text(encoding=self._get_encoding())
)
project_name = pyproject_toml_content["project"]["name"] # type: ignore[index]
project_name = pyproject_toml_content["project"]["name"] # type: ignore # noqa: PGH003
normalized_project_name = canonicalize_name(str(project_name))

document = tomlkit.parse(
self.lock_file.read_text(encoding=self._get_encoding())
)

packages: tomlkit.items.AoT = document["package"] # type: ignore[assignment]
packages: tomlkit.items.AoT = document["package"] # type: ignore # noqa: PGH003
for i, package in enumerate(packages):
if package["name"] == normalized_project_name:
document["package"][i]["version"] = version # type: ignore[index]
document["package"][i]["version"] = version # type: ignore # noqa: PGH003
break
self.lock_file.write_text(
tomlkit.dumps(document), encoding=self._get_encoding()
Expand Down
8 changes: 4 additions & 4 deletions commitizen/version_schemes.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def scheme(self) -> VersionScheme:

@property
def prerelease(self) -> str | None:
# version.pre is needed for mypy check
# version.pre is needed for static type checking
if self.is_prerelease and self.pre:
return f"{self.pre[0]}{self.pre[1]}"
return None
Expand Down Expand Up @@ -253,7 +253,7 @@ def bump(

if self.local and is_local_version:
local_version = self.scheme(self.local).bump(increment)
return self.scheme(f"{self.public}+{local_version}") # type: ignore[return-value]
return self.scheme(f"{self.public}+{local_version}") # type: ignore # noqa: PGH003

base = self._get_increment_base(increment, exact_increment)
dev_version = self.generate_devrelease(devrelease)
Expand All @@ -270,7 +270,7 @@ def bump(
# TODO: post version
return self.scheme(
f"{base}{pre_version}{dev_version}{self.generate_build_metadata(build_metadata)}"
) # type: ignore[return-value]
) # type: ignore # noqa: PGH003

def _get_increment_base(
self, increment: Increment | None, exact_increment: bool
Expand Down Expand Up @@ -385,7 +385,7 @@ def _get_prerelease(self) -> str:
return ".".join(prerelease_parts)


DEFAULT_SCHEME: VersionScheme = Pep440
DEFAULT_SCHEME: VersionScheme = cast("VersionScheme", Pep440)

SCHEMES_ENTRYPOINT = "commitizen.scheme"
"""Schemes entrypoints group"""
Expand Down
6 changes: 3 additions & 3 deletions docs/contributing/contributing_tldr.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ uv sync --dev --frozen
# Make ruff happy
uv run poe format

# Check if ruff and mypy are happy
# Check if ruff and ty are happy
uv run poe lint

# Check if mypy is happy in python 3.10
mypy --python-version 3.10
# Check ty against Python 3.10 types (matches [tool.ty.environment] python-version)
uv run ty check --python-version 3.10

# Run tests in parallel.
pytest -n auto # This may take a while.
Expand Down
24 changes: 11 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,13 @@ test = [

linters = [
"ruff>=0.11.5",
"mypy>=1.16.0",
"types-deprecated>=1.2.9.2",
"types-python-dateutil>=2.8.19.13",
"types-PyYAML>=5.4.3",
"types-termcolor>=0.1.1",
"types-colorama>=0.4.15.20240311",
"prek>=0.2.28",
"ty>=0.0.29",
]

documentation = [
Expand Down Expand Up @@ -251,18 +251,16 @@ known-first-party = ["commitizen", "tests"]
[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.mypy]
files = ["commitizen", "tests", "scripts"]
disallow_untyped_decorators = true
disallow_subclassing_any = true
warn_return_any = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_unused_configs = true
[tool.ty.src]
include = ["commitizen", "tests", "scripts"]

[[tool.mypy.overrides]]
module = "py.*" # Legacy pytest dependencies
ignore_missing_imports = true
[tool.ty.environment]
# Match CI / minimum supported Python (see also: contributing_tldr ty check example)
python-version = "3.10"

[tool.ty.analysis]
# Legacy pytest dependencies (`py` namespace)
allowed-unresolved-imports = ["py.**"]

[tool.codespell]
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
Expand All @@ -278,7 +276,7 @@ format.help = "Format the code"
format.sequence = [{ cmd = "ruff check --fix" }, { cmd = "ruff format" }]

lint.help = "Lint the code"
lint.sequence = [{ cmd = "ruff check" }, { cmd = "mypy" }]
lint.sequence = [{ cmd = "ruff check" }, { cmd = "ty check" }]

check-commit.help = "Check the commit messages"
check-commit.cmd = "cz --no-raise 3 check --rev-range origin/master.."
Expand Down
5 changes: 4 additions & 1 deletion scripts/gen_cli_help_screenshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import subprocess
from itertools import chain
from pathlib import Path
from typing import Any, cast

from rich.console import Console

Expand All @@ -13,9 +14,11 @@ def gen_cli_help_screenshots() -> None:
images_root = Path(__file__).parent.parent / "docs" / "images" / "cli_help"
images_root.mkdir(parents=True, exist_ok=True)

cli_data = cast("dict[str, Any]", data)
commands_cfg = cast("list[dict[str, Any]]", cli_data["subcommands"]["commands"])
cz_commands = (
command["name"] if isinstance(command["name"], str) else command["name"][0]
for command in data["subcommands"]["commands"] # type: ignore[index]
for command in commands_cfg
)
for cmd in chain(
["cz --help"], (f"cz {cz_command} --help" for cz_command in cz_commands)
Expand Down
2 changes: 1 addition & 1 deletion tests/commands/test_bump_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -1396,7 +1396,7 @@ def test_is_initial_tag(mocker: MockFixture, tmp_commitizen_project, util: UtilF
"extras": None,
}

bump_cmd = bump.Bump(config, arguments) # type: ignore[arg-type]
bump_cmd = bump.Bump(config, arguments) # type: ignore # noqa: PGH003

# Test case 1: No current tag, not yes mode
mocker.patch("questionary.confirm", return_value=mocker.Mock(ask=lambda: True))
Expand Down
2 changes: 1 addition & 1 deletion tests/commands/test_changelog_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ def test_changelog_hook(
util.create_file_and_commit("refactor: is in changelog")
util.create_file_and_commit("Merge into master")

config.settings["change_type_order"] = ["Refactor", "Feat"] # type: ignore[typeddict-unknown-key]
config.settings["change_type_order"] = ["Refactor", "Feat"] # type: ignore # noqa: PGH003
changelog = Changelog(
config, {"unreleased_version": None, "incremental": True, "dry_run": dry_run}
)
Expand Down
2 changes: 1 addition & 1 deletion tests/commands/test_version_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def test_version_just_major_error_no_project(config, capsys, argument: str):
commands.Version(
config,
{
argument: True, # type: ignore[misc]
argument: True,
},
Comment on lines 152 to 156
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commands.Version expects VersionArgs (a TypedDict), but {argument: True} uses a dynamic key so the literal is inferred as a plain dict[str, bool] by type checkers. This used to be suppressed with a type: ignore; consider casting to VersionArgs (or branching on argument to build a literal with a static key) to avoid ty type errors.

Copilot uses AI. Check for mistakes.
)()
captured = capsys.readouterr()
Expand Down
Loading
Loading