Skip to content

Commit 96caf65

Browse files
committed
refactor: try to use ChainMap to chain settings
1 parent de24815 commit 96caf65

File tree

10 files changed

+170
-89
lines changed

10 files changed

+170
-89
lines changed

commitizen/commands/bump.py

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import warnings
44
from logging import getLogger
5-
from typing import TYPE_CHECKING, cast
5+
from typing import TYPE_CHECKING
66

77
import questionary
88

99
from commitizen import bump, factory, git, hooks, out
1010
from commitizen.changelog_formats import get_changelog_format
1111
from commitizen.commands.changelog import Changelog
12+
from commitizen.config.settings import ChainSettings
1213
from commitizen.defaults import Settings
1314
from commitizen.exceptions import (
1415
BumpCommitFailedError,
@@ -70,37 +71,11 @@ def __init__(self, config: BaseConfig, arguments: BumpArgs) -> None:
7071

7172
self.config: BaseConfig = config
7273
self.arguments = arguments
73-
self.bump_settings = cast(
74-
"BumpArgs",
75-
{
76-
**config.settings,
77-
**{
78-
k: v
79-
for k in (
80-
"annotated_tag_message",
81-
"annotated_tag",
82-
"bump_message",
83-
"file_name",
84-
"gpg_sign",
85-
"increment_mode",
86-
"increment",
87-
"major_version_zero",
88-
"prerelease_offset",
89-
"prerelease",
90-
"tag_format",
91-
"template",
92-
)
93-
if (v := arguments.get(k)) is not None
94-
},
95-
},
96-
)
74+
self.settings = ChainSettings(config.settings, arguments).load_settings()
9775
self.cz = factory.committer_factory(self.config)
9876
self.changelog_flag = arguments["changelog"]
9977
self.changelog_to_stdout = arguments["changelog_to_stdout"]
10078
self.git_output_to_stderr = arguments["git_output_to_stderr"]
101-
self.no_verify = arguments["no_verify"]
102-
self.check_consistency = arguments["check_consistency"]
103-
self.retry = arguments["retry"]
10479
self.pre_bump_hooks = self.config.settings["pre_bump_hooks"]
10580
self.post_bump_hooks = self.config.settings["post_bump_hooks"]
10681
deprecated_version_type = arguments.get("version_type")
@@ -148,7 +123,7 @@ def _find_increment(self, commits: list[git.GitCommit]) -> Increment | None:
148123
# self.cz.bump_map = defaults.bump_map_major_version_zero
149124
bump_map = (
150125
self.cz.bump_map_major_version_zero
151-
if self.bump_settings["major_version_zero"]
126+
if self.settings["major_version_zero"]
152127
else self.cz.bump_map
153128
)
154129
bump_pattern = self.cz.bump_pattern
@@ -230,7 +205,7 @@ def _resolve_increment_and_new_version(
230205
return increment, current_version.bump(
231206
increment,
232207
prerelease=self.arguments["prerelease"],
233-
prerelease_offset=self.bump_settings["prerelease_offset"],
208+
prerelease_offset=self.settings["prerelease_offset"],
234209
devrelease=self.arguments["devrelease"],
235210
is_local_version=self.arguments["local_version"],
236211
build_metadata=self.arguments["build_metadata"],
@@ -262,7 +237,7 @@ def __call__(self) -> None:
262237
)
263238
)
264239

265-
rules = TagRules.from_settings(cast("Settings", self.bump_settings))
240+
rules = TagRules.from_settings(self.settings)
266241
current_tag = rules.find_tag_for(git.get_tags(), current_version)
267242
current_tag_version = (
268243
current_tag.name if current_tag else rules.normalize_tag(current_version)
@@ -285,7 +260,7 @@ def __call__(self) -> None:
285260
raise DryRunExit()
286261

287262
message = bump.create_commit_message(
288-
current_version, new_version, self.bump_settings["bump_message"]
263+
current_version, new_version, self.settings["bump_message"]
289264
)
290265
# Report found information
291266
information = f"{message}\ntag to create: {new_tag_version}\n"
@@ -342,8 +317,8 @@ def __call__(self) -> None:
342317
bump.update_version_in_files(
343318
str(current_version),
344319
str(new_version),
345-
self.bump_settings["version_files"],
346-
check_consistency=self.check_consistency,
320+
self.settings["version_files"],
321+
check_consistency=self.arguments["check_consistency"],
347322
encoding=self.config.settings["encoding"],
348323
)
349324
)
@@ -372,7 +347,7 @@ def __call__(self) -> None:
372347
# FIXME: check if any changes have been staged
373348
git.add(*updated_files)
374349
c = git.commit(message, args=self._get_commit_args())
375-
if self.retry and c.return_code != 0 and self.changelog_flag:
350+
if self.arguments["retry"] and c.return_code != 0 and self.changelog_flag:
376351
# Maybe pre-commit reformatted some files? Retry once
377352
logger.debug("1st git.commit error: %s", c.err)
378353
logger.info("1st commit attempt failed; retrying once")
@@ -391,18 +366,18 @@ def __call__(self) -> None:
391366
new_tag_version,
392367
signed=any(
393368
(
394-
self.bump_settings.get("gpg_sign"),
395-
self.config.settings.get("gpg_sign"),
369+
self.settings.get("gpg_sign"),
370+
self.config.settings.get("gpg_sign"), # TODO: remove this
396371
)
397372
),
398373
annotated=any(
399374
(
400-
self.bump_settings.get("annotated_tag"),
401-
self.config.settings.get("annotated_tag"),
402-
self.bump_settings.get("annotated_tag_message"),
375+
self.settings.get("annotated_tag"),
376+
self.config.settings.get("annotated_tag"), # TODO: remove this
377+
self.settings.get("annotated_tag_message"),
403378
)
404379
),
405-
msg=self.bump_settings.get("annotated_tag_message", None),
380+
msg=self.settings.get("annotated_tag_message", None), # type: ignore[arg-type]
406381
# TODO: also get from self.config.settings?
407382
)
408383
if c.return_code != 0:
@@ -432,6 +407,6 @@ def __call__(self) -> None:
432407

433408
def _get_commit_args(self) -> str:
434409
commit_args = ["-a"]
435-
if self.no_verify:
410+
if self.arguments["no_verify"]:
436411
commit_args.append("--no-verify")
437412
return " ".join(commit_args)

commitizen/commands/check.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import TYPE_CHECKING, TypedDict
66

77
from commitizen import factory, git, out
8+
from commitizen.config.settings import ChainSettings
89
from commitizen.exceptions import (
910
InvalidCommandArgumentError,
1011
InvalidCommitMessageError,
@@ -20,7 +21,7 @@ class CheckArgs(TypedDict, total=False):
2021
commit_msg: str
2122
rev_range: str
2223
allow_abort: bool
23-
message_length_limit: int | None
24+
message_length_limit: int
2425
allowed_prefixes: list[str]
2526
message: str
2627
use_default_range: bool
@@ -37,25 +38,12 @@ def __init__(self, config: BaseConfig, arguments: CheckArgs, *args: object) -> N
3738
arguments: All the flags provided by the user
3839
cwd: Current work directory
3940
"""
41+
self.settings = ChainSettings(config.settings, arguments).load_settings()
4042
self.commit_msg_file = arguments.get("commit_msg_file")
4143
self.commit_msg = arguments.get("message")
4244
self.rev_range = arguments.get("rev_range")
43-
self.allow_abort = bool(
44-
arguments.get("allow_abort", config.settings["allow_abort"])
45-
)
4645

4746
self.use_default_range = bool(arguments.get("use_default_range"))
48-
self.max_msg_length = arguments.get(
49-
"message_length_limit", config.settings.get("message_length_limit", None)
50-
)
51-
52-
# we need to distinguish between None and [], which is a valid value
53-
allowed_prefixes = arguments.get("allowed_prefixes")
54-
self.allowed_prefixes: list[str] = (
55-
allowed_prefixes
56-
if allowed_prefixes is not None
57-
else config.settings["allowed_prefixes"]
58-
)
5947

6048
num_exclusive_args_provided = sum(
6149
arg is not None
@@ -97,9 +85,9 @@ def __call__(self) -> None:
9785
check := self.cz.validate_commit_message(
9886
commit_msg=commit.message,
9987
pattern=pattern,
100-
allow_abort=self.allow_abort,
101-
allowed_prefixes=self.allowed_prefixes,
102-
max_msg_length=self.max_msg_length,
88+
allow_abort=self.settings["allow_abort"],
89+
allowed_prefixes=self.settings["allowed_prefixes"],
90+
max_msg_length=self.settings["message_length_limit"] or 0,
10391
commit_hash=commit.rev,
10492
)
10593
).is_valid

commitizen/commands/commit.py

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import questionary
1111

1212
from commitizen import factory, git, out
13+
from commitizen.config.settings import ChainSettings
1314
from commitizen.cz.exceptions import CzException
1415
from commitizen.cz.utils import get_backup_file_path
1516
from commitizen.exceptions import (
@@ -36,7 +37,7 @@ class CommitArgs(TypedDict, total=False):
3637
dry_run: bool
3738
edit: bool
3839
extra_cli_args: str
39-
message_length_limit: int | None
40+
message_length_limit: int
4041
no_retry: bool
4142
signoff: bool
4243
write_message_to_file: Path | None
@@ -53,6 +54,7 @@ def __init__(self, config: BaseConfig, arguments: CommitArgs) -> None:
5354
self.config: BaseConfig = config
5455
self.cz = factory.committer_factory(self.config)
5556
self.arguments = arguments
57+
self.settings = ChainSettings(config.settings, arguments).load_settings()
5658
self.backup_file_path = get_backup_file_path()
5759

5860
def _read_backup_message(self) -> str | None:
@@ -61,16 +63,14 @@ def _read_backup_message(self) -> str | None:
6163
return None
6264

6365
# Read commit message from backup
64-
with open(
65-
self.backup_file_path, encoding=self.config.settings["encoding"]
66-
) as f:
66+
with open(self.backup_file_path, encoding=self.settings["encoding"]) as f:
6767
return f.read().strip()
6868

6969
def _get_message_by_prompt_commit_questions(self) -> str:
7070
# Prompt user for the commit message
7171
questions = self.cz.questions()
7272
for question in (q for q in questions if q["type"] == "list"):
73-
question["use_shortcuts"] = self.config.settings["use_shortcuts"]
73+
question["use_shortcuts"] = self.settings["use_shortcuts"]
7474
try:
7575
answers = questionary.prompt(questions, style=self.cz.style)
7676
except ValueError as err:
@@ -83,21 +83,16 @@ def _get_message_by_prompt_commit_questions(self) -> str:
8383
raise NoAnswersError()
8484

8585
message = self.cz.message(answers)
86-
if limit := self.arguments.get(
87-
"message_length_limit", self.config.settings.get("message_length_limit", 0)
88-
):
89-
self._validate_subject_length(message=message, length_limit=limit)
86+
if (length_limit := self.settings["message_length_limit"]) > 0:
87+
# By the contract, message_length_limit is set to 0 for no limit
88+
subject = message.partition("\n")[0].strip()
89+
if len(subject) > length_limit:
90+
raise CommitMessageLengthExceededError(
91+
f"Length of commit message exceeds limit ({len(subject)}/{length_limit}), subject: '{subject}'"
92+
)
9093

9194
return message
9295

93-
def _validate_subject_length(self, *, message: str, length_limit: int) -> None:
94-
# By the contract, message_length_limit is set to 0 for no limit
95-
subject = message.partition("\n")[0].strip()
96-
if len(subject) > length_limit:
97-
raise CommitMessageLengthExceededError(
98-
f"Length of commit message exceeds limit ({len(subject)}/{length_limit}), subject: '{subject}'"
99-
)
100-
10196
def manual_edit(self, message: str) -> str:
10297
editor = git.get_core_editor()
10398
if editor is None:
@@ -123,7 +118,7 @@ def _get_message(self) -> str:
123118
return commit_message
124119

125120
if (
126-
self.config.settings.get("retry_after_failure")
121+
self.settings.get("retry_after_failure")
127122
and not self.arguments.get("no_retry")
128123
and (backup_message := self._read_backup_message())
129124
):
@@ -158,14 +153,14 @@ def __call__(self) -> None:
158153

159154
if write_message_to_file:
160155
with smart_open(
161-
write_message_to_file, "w", encoding=self.config.settings["encoding"]
156+
write_message_to_file, "w", encoding=self.settings["encoding"]
162157
) as file:
163158
file.write(commit_message)
164159

165160
if dry_run:
166161
raise DryRunExit()
167162

168-
if self.config.settings["always_signoff"] or signoff:
163+
if self.settings["always_signoff"] or signoff:
169164
extra_args = f"{extra_args} -s".strip()
170165

171166
c = git.commit(commit_message, args=extra_args)
@@ -174,7 +169,7 @@ def __call__(self) -> None:
174169

175170
# Create commit backup
176171
with smart_open(
177-
self.backup_file_path, "w", encoding=self.config.settings["encoding"]
172+
self.backup_file_path, "w", encoding=self.settings["encoding"]
178173
) as f:
179174
f.write(commit_message)
180175

commitizen/config/settings.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from collections import ChainMap
2+
from collections.abc import Mapping
3+
from typing import Any, cast
4+
5+
from commitizen.defaults import DEFAULT_SETTINGS, Settings
6+
7+
8+
class ChainSettings:
9+
def __init__(
10+
self,
11+
config_file_settings: Mapping[str, Any],
12+
cli_settings: Mapping[str, Any] | None = None,
13+
) -> None:
14+
if cli_settings is None:
15+
cli_settings = {}
16+
self._chain_map: ChainMap[str, Any] = ChainMap[Any, Any](
17+
self._remove_none_values(cli_settings),
18+
self._remove_none_values(config_file_settings),
19+
DEFAULT_SETTINGS, # type: ignore[arg-type]
20+
)
21+
22+
def load_settings(self) -> Settings:
23+
return cast("Settings", dict(self._chain_map))
24+
25+
def _remove_none_values(self, settings: Mapping[str, Any]) -> dict[str, Any]:
26+
"""HACK: remove None values from settings to avoid incorrectly overriding settings."""
27+
return {k: v for k, v in settings.items() if v is not None}

commitizen/cz/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def validate_commit_message(
118118
pattern: re.Pattern[str],
119119
allow_abort: bool,
120120
allowed_prefixes: list[str],
121-
max_msg_length: int | None,
121+
max_msg_length: int,
122122
commit_hash: str,
123123
) -> ValidationResult:
124124
"""Validate commit message against the pattern."""
@@ -130,7 +130,7 @@ def validate_commit_message(
130130
if any(map(commit_msg.startswith, allowed_prefixes)):
131131
return ValidationResult(True, [])
132132

133-
if max_msg_length is not None:
133+
if max_msg_length > 0:
134134
msg_len = len(commit_msg.partition("\n")[0].strip())
135135
if msg_len > max_msg_length:
136136
# TODO: capitalize the first letter of the error message for consistency in v5

commitizen/defaults.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class Settings(TypedDict, total=False):
4848
ignored_tag_formats: Sequence[str]
4949
legacy_tag_formats: Sequence[str]
5050
major_version_zero: bool
51-
message_length_limit: int | None
51+
message_length_limit: int
5252
name: str
5353
post_bump_hooks: list[str] | None
5454
pre_bump_hooks: list[str] | None
@@ -114,7 +114,7 @@ class Settings(TypedDict, total=False):
114114
"template": None, # default provided by plugin
115115
"extras": {},
116116
"breaking_change_exclamation_in_title": False,
117-
"message_length_limit": None, # None for no limit
117+
"message_length_limit": 0, # 0 for no limit
118118
}
119119

120120
MAJOR = "MAJOR"

tests/commands/test_check_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ def test_check_command_cli_overrides_config_message_length_limit(
385385
):
386386
message = "fix(scope): some commit message"
387387
config.settings["message_length_limit"] = len(message) - 1
388-
for message_length_limit in [len(message) + 1, None]:
388+
for message_length_limit in [len(message) + 1, 0]:
389389
success_mock.reset_mock()
390390
commands.Check(
391391
config=config,

tests/commands/test_commit_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,5 +363,5 @@ def test_commit_command_with_config_message_length_limit(
363363
success_mock.assert_called_once()
364364

365365
success_mock.reset_mock()
366-
commands.Commit(config, {"message_length_limit": None})()
366+
commands.Commit(config, {"message_length_limit": 0})()
367367
success_mock.assert_called_once()

0 commit comments

Comments
 (0)