Skip to content

Commit 314f3bf

Browse files
committed
feat(commit): implement questions 'filter' support with handlers
Supported APIs: - multiple_line_breaker - required_validator - required_validator_scope - required_validator_subject_strip - required_validator_title_strip Example YAML configurations: --- commitizen: name: cz_customize customize: questions: - ... - type: input name: scope message: 'Scope of the change :' filter: 'required_validator_scope' default: '' - type: input name: subject message: 'Title of the commit (starting in lower case and without period) :' filter: 'required_validator_subject_strip' default: '' - type: input name: body message: 'Additional contextual message (Empty to skip) :' default: 'Issue: #...' filter: 'multiple_line_breaker' --- Signed-off-by: Adrian DC <[email protected]>
1 parent 5e08775 commit 314f3bf

File tree

4 files changed

+185
-8
lines changed

4 files changed

+185
-8
lines changed

commitizen/commands/commit.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@
88
from commitizen import factory, git, out
99
from commitizen.config import BaseConfig
1010
from commitizen.cz.exceptions import CzException
11-
from commitizen.cz.utils import get_backup_file_path
11+
from commitizen.cz.utils import (
12+
get_backup_file_path,
13+
multiple_line_breaker,
14+
required_validator,
15+
required_validator_scope,
16+
required_validator_subject_strip,
17+
required_validator_title_strip,
18+
)
1219
from commitizen.exceptions import (
1320
CommitError,
1421
CommitMessageLengthExceededError,
@@ -52,6 +59,23 @@ def prompt_commit_questions(self) -> str:
5259

5360
for question in filter(lambda q: q["type"] == "list", questions):
5461
question["use_shortcuts"] = self.config.settings["use_shortcuts"]
62+
63+
for question in filter(
64+
lambda q: isinstance(q.get("filter", None), str), questions
65+
):
66+
if question["filter"] == "multiple_line_breaker":
67+
question["filter"] = multiple_line_breaker
68+
elif question["filter"] == "required_validator":
69+
question["filter"] = required_validator
70+
elif question["filter"] == "required_validator_scope":
71+
question["filter"] = required_validator_scope
72+
elif question["filter"] == "required_validator_subject_strip":
73+
question["filter"] = required_validator_subject_strip
74+
elif question["filter"] == "required_validator_title_strip":
75+
question["filter"] = required_validator_title_strip
76+
else:
77+
raise NotAllowed(f"Unknown value filter: {question['filter']}")
78+
5579
try:
5680
answers = questionary.prompt(questions, style=cz.style)
5781
except ValueError as err:

commitizen/cz/utils.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,34 @@
66
from commitizen.cz import exceptions
77

88

9-
def required_validator(answer, msg=None):
9+
def required_validator(answer: str, msg=None) -> str:
1010
if not answer:
1111
raise exceptions.AnswerRequiredError(msg)
1212
return answer
1313

1414

15-
def multiple_line_breaker(answer, sep="|"):
15+
def required_validator_scope(
16+
answer: str,
17+
msg: str = "! Error: Scope is required",
18+
) -> str:
19+
return required_validator(answer, msg)
20+
21+
22+
def required_validator_subject_strip(
23+
answer: str,
24+
msg: str = "! Error: Subject is required",
25+
) -> str:
26+
return required_validator(answer.strip(".").strip(), msg)
27+
28+
29+
def required_validator_title_strip(
30+
answer: str,
31+
msg: str = "! Error: Title is required",
32+
) -> str:
33+
return required_validator(answer.strip(".").strip(), msg)
34+
35+
36+
def multiple_line_breaker(answer: str, sep: str = "|") -> str:
1637
return "\n".join(line.strip() for line in answer.split(sep) if line)
1738

1839

docs/customization.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ commitizen:
175175
| `message` | `str` | `None` | Detail description for the question. |
176176
| `choices` | `list` | `None` | (OPTIONAL) The choices when `type = list`. Either use a list of values or a list of dictionaries with `name` and `value` keys. Keyboard shortcuts can be defined via `key`. See examples above. |
177177
| `default` | `Any` | `None` | (OPTIONAL) The default value for this question. |
178-
| `filter` | `str` | `None` | (Optional) Validator for user's answer. **(Work in Progress)** |
178+
| `filter` | `str` | `None` | (OPTIONAL) Validator for user's answer. The string is the name of a `commitizen.cz.utils.NAME(answer...)` function like `multiple_line_breaker` |
179179
[different-question-types]: https://github.com/tmbo/questionary#different-question-types
180180

181181
#### Shortcut keys

tests/test_cz_customize.py

+136-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import pytest
2+
from pytest_mock import MockFixture
23

4+
from commitizen import cmd, commands
35
from commitizen.config import BaseConfig, JsonConfig, TomlConfig, YAMLConfig
46
from commitizen.cz.customize import CustomizeCommitsCz
5-
from commitizen.exceptions import MissingCzCustomizeConfigError
7+
from commitizen.cz.utils import (
8+
multiple_line_breaker,
9+
required_validator,
10+
required_validator_scope,
11+
required_validator_subject_strip,
12+
required_validator_title_strip,
13+
)
14+
from commitizen.exceptions import MissingCzCustomizeConfigError, NotAllowed
615

716
TOML_STR = r"""
817
[tool.commitizen]
@@ -36,10 +45,17 @@
3645
]
3746
message = "Select the type of change you are committing"
3847
48+
[[tool.commitizen.customize.questions]]
49+
type = "input"
50+
name = "subject"
51+
message = "Subject."
52+
filter = "required_validator_subject_strip"
53+
3954
[[tool.commitizen.customize.questions]]
4055
type = "input"
4156
name = "message"
4257
message = "Body."
58+
filter = "multiple_line_breaker"
4359
4460
[[tool.commitizen.customize.questions]]
4561
type = "confirm"
@@ -89,10 +105,17 @@
89105
],
90106
"message": "Select the type of change you are committing"
91107
},
108+
{
109+
"type": "input",
110+
"name": "subject",
111+
"message": "Subject.",
112+
"filter": "required_validator_subject_strip"
113+
},
92114
{
93115
"type": "input",
94116
"name": "message",
95-
"message": "Body."
117+
"message": "Body.",
118+
"filter": "multiple_line_breaker"
96119
},
97120
{
98121
"type": "confirm",
@@ -139,9 +162,14 @@
139162
- value: bug fix
140163
name: 'bug fix: A bug fix.'
141164
message: Select the type of change you are committing
165+
- type: input
166+
name: subject
167+
message: Subject.
168+
filter: required_validator_subject_strip
142169
- type: input
143170
name: message
144171
message: Body.
172+
filter: multiple_line_breaker
145173
- type: confirm
146174
name: show_message
147175
message: Do you want to add body message in commit?
@@ -330,6 +358,13 @@
330358
"""
331359

332360

361+
@pytest.fixture
362+
def staging_is_clean(mocker: MockFixture, tmp_git_project):
363+
is_staging_clean_mock = mocker.patch("commitizen.git.is_staging_clean")
364+
is_staging_clean_mock.return_value = False
365+
return tmp_git_project
366+
367+
333368
@pytest.fixture(
334369
params=[
335370
TomlConfig(data=TOML_STR, path="not_exist.toml"),
@@ -346,6 +381,15 @@ def config(request):
346381
return request.param
347382

348383

384+
@pytest.fixture(
385+
params=[
386+
YAMLConfig(data=YAML_STR, path="not_exist.yaml"),
387+
]
388+
)
389+
def config_filters(request):
390+
return request.param
391+
392+
349393
@pytest.fixture(
350394
params=[
351395
TomlConfig(data=TOML_STR_INFO_PATH, path="not_exist.toml"),
@@ -437,7 +481,7 @@ def test_change_type_order_unicode(config_with_unicode):
437481
]
438482

439483

440-
def test_questions(config):
484+
def test_questions_default(config):
441485
cz = CustomizeCommitsCz(config)
442486
questions = cz.questions()
443487
expected_questions = [
@@ -450,7 +494,18 @@ def test_questions(config):
450494
],
451495
"message": "Select the type of change you are committing",
452496
},
453-
{"type": "input", "name": "message", "message": "Body."},
497+
{
498+
"type": "input",
499+
"name": "subject",
500+
"message": "Subject.",
501+
"filter": "required_validator_subject_strip",
502+
},
503+
{
504+
"type": "input",
505+
"name": "message",
506+
"message": "Body.",
507+
"filter": "multiple_line_breaker",
508+
},
454509
{
455510
"type": "confirm",
456511
"name": "show_message",
@@ -460,6 +515,83 @@ def test_questions(config):
460515
assert list(questions) == expected_questions
461516

462517

518+
@pytest.mark.usefixtures("staging_is_clean")
519+
def test_questions_filter_default(config, mocker: MockFixture):
520+
is_staging_clean_mock = mocker.patch("commitizen.git.is_staging_clean")
521+
is_staging_clean_mock.return_value = False
522+
523+
prompt_mock = mocker.patch("questionary.prompt")
524+
prompt_mock.return_value = {
525+
"change_type": "feature",
526+
"subject": "user created",
527+
"message": "body of the commit",
528+
"show_message": True,
529+
}
530+
531+
commit_mock = mocker.patch("commitizen.git.commit")
532+
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
533+
534+
commands.Commit(config, {})()
535+
536+
prompts_questions = prompt_mock.call_args[0][0]
537+
assert prompts_questions[0]["type"] == "list"
538+
assert prompts_questions[0]["name"] == "change_type"
539+
assert prompts_questions[0]["use_shortcuts"] is False
540+
assert prompts_questions[1]["type"] == "input"
541+
assert prompts_questions[1]["name"] == "subject"
542+
assert prompts_questions[1]["filter"] == required_validator_subject_strip
543+
assert prompts_questions[2]["type"] == "input"
544+
assert prompts_questions[2]["name"] == "message"
545+
assert prompts_questions[2]["filter"] == multiple_line_breaker
546+
assert prompts_questions[3]["type"] == "confirm"
547+
assert prompts_questions[3]["name"] == "show_message"
548+
549+
550+
@pytest.mark.usefixtures("staging_is_clean")
551+
def test_questions_filter_values(config_filters, mocker: MockFixture):
552+
is_staging_clean_mock = mocker.patch("commitizen.git.is_staging_clean")
553+
is_staging_clean_mock.return_value = False
554+
555+
prompt_mock = mocker.patch("questionary.prompt")
556+
prompt_mock.return_value = {
557+
"change_type": "feature",
558+
"subject": "user created",
559+
"message": "body of the commit",
560+
"show_message": True,
561+
}
562+
563+
commit_mock = mocker.patch("commitizen.git.commit")
564+
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)
565+
566+
commit_cmd = commands.Commit(config_filters, {})
567+
568+
assert isinstance(commit_cmd.cz, CustomizeCommitsCz)
569+
570+
for filter_desc in [
571+
("multiple_line_breaker", multiple_line_breaker),
572+
("required_validator", required_validator),
573+
("required_validator_scope", required_validator_scope),
574+
("required_validator_subject_strip", required_validator_subject_strip),
575+
("required_validator_title_strip", required_validator_title_strip),
576+
]:
577+
commit_cmd.cz.custom_settings["questions"][1]["filter"] = filter_desc[0] # type: ignore[index]
578+
commit_cmd()
579+
580+
assert filter_desc[1]("input")
581+
582+
prompts_questions = prompt_mock.call_args[0][0]
583+
assert prompts_questions[1]["filter"] == filter_desc[1]
584+
585+
for filter_name in [
586+
"",
587+
"faulty_value",
588+
]:
589+
commit_cmd.cz.custom_settings["questions"][1]["filter"] = filter_name # type: ignore[index]
590+
591+
with pytest.raises(NotAllowed):
592+
commit_cmd()
593+
594+
463595
def test_questions_unicode(config_with_unicode):
464596
cz = CustomizeCommitsCz(config_with_unicode)
465597
questions = cz.questions()

0 commit comments

Comments
 (0)