Skip to content

Commit

Permalink
feat(commit): implement questions 'filter' support with evaluations
Browse files Browse the repository at this point in the history
Supported APIs: Common Python, commitizen.cz.utils.* functions

Example YAML configurations:
---
commitizen:
  name: cz_customize
  customize:
    questions:
      - ...
      - type: input
        name: scope
        message: 'Scope of the change :'
        filter: 'lambda text: commitizen.cz.utils.required_validator(text, msg="! Error: Scope is required")'
        default: ''
      - type: input
        name: subject
        message: 'Title of the commit (starting in lower case and without period) :'
        filter: 'lambda text: commitizen.cz.utils.required_validator(text.strip(".").strip(), msg="! Error: Title is required")'
        default: ''
      - type: input
        name: body
        message: 'Additional contextual message (Empty to skip) :'
        default: 'Issue: #...'
        filter: 'commitizen.cz.utils.multiple_line_breaker'
---

Signed-off-by: Adrian DC <[email protected]>
  • Loading branch information
AdrianDC committed Aug 17, 2024
1 parent 7258073 commit 5011a91
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 5 deletions.
18 changes: 17 additions & 1 deletion commitizen/commands/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
from commitizen import factory, git, out
from commitizen.config import BaseConfig
from commitizen.cz.exceptions import CzException
from commitizen.cz.utils import get_backup_file_path
from commitizen.cz.utils import (
get_backup_file_path,
multiple_line_breaker,
)
from commitizen.exceptions import (
CommitError,
CommitMessageLengthExceededError,
Expand Down Expand Up @@ -52,6 +55,19 @@ def prompt_commit_questions(self) -> str:

for question in filter(lambda q: q["type"] == "list", questions):
question["use_shortcuts"] = self.config.settings["use_shortcuts"]

# Import allowed modules for 'filter'
global commitizen
import commitizen.cz.utils

for question in filter(
lambda q: isinstance(q.get("filter", None), str), questions
):
question_filter = [
multiple_line_breaker(question["filter"].replace("\\n", "\n"))
]
question["filter"] = eval("\n".join(question_filter))

try:
answers = questionary.prompt(questions, style=cz.style)
except ValueError as err:
Expand Down
2 changes: 1 addition & 1 deletion docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ commitizen:
| `message` | `str` | `None` | Detail description for the question. |
| `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. |
| `default` | `Any` | `None` | (OPTIONAL) The default value for this question. |
| `filter` | `str` | `None` | (Optional) Validator for user's answer. **(Work in Progress)** |
| `filter` | `str` | `None` | (OPTIONAL) Validator for user's answer. The string is evaluated into a Python function, either use `commitizen.cz.utils.*` or lambda functions like `lambda text: text.strip(".").strip()` |
[different-question-types]: https://github.com/tmbo/questionary#different-question-types

#### Shortcut keys
Expand Down
80 changes: 77 additions & 3 deletions tests/test_cz_customize.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from types import LambdaType

import pytest
from pytest_mock import MockFixture

from commitizen import cmd, commands
from commitizen.config import BaseConfig, JsonConfig, TomlConfig, YAMLConfig
from commitizen.cz.customize import CustomizeCommitsCz
from commitizen.cz.utils import multiple_line_breaker
from commitizen.exceptions import MissingCzCustomizeConfigError

TOML_STR = r"""
Expand Down Expand Up @@ -36,10 +41,17 @@
]
message = "Select the type of change you are committing"
[[tool.commitizen.customize.questions]]
type = "input"
name = "subject"
message = "Subject."
filter = "lambda text: commitizen.cz.utils.required_validator(text.strip(\".\").strip(), msg=\"! Error: Subject is required\")"
[[tool.commitizen.customize.questions]]
type = "input"
name = "message"
message = "Body."
filter = "commitizen.cz.utils.multiple_line_breaker"
[[tool.commitizen.customize.questions]]
type = "confirm"
Expand Down Expand Up @@ -89,10 +101,17 @@
],
"message": "Select the type of change you are committing"
},
{
"type": "input",
"name": "subject",
"message": "Subject.",
"filter": "lambda text: commitizen.cz.utils.required_validator(text.strip(\".\").strip(), msg=\"! Error: Subject is required\")"
},
{
"type": "input",
"name": "message",
"message": "Body."
"message": "Body.",
"filter": "commitizen.cz.utils.multiple_line_breaker"
},
{
"type": "confirm",
Expand Down Expand Up @@ -139,9 +158,14 @@
- value: bug fix
name: 'bug fix: A bug fix.'
message: Select the type of change you are committing
- type: input
name: subject
message: Subject.
filter: 'lambda text: commitizen.cz.utils.required_validator(text.strip(".").strip(), msg="! Error: Subject is required")'
- type: input
name: message
message: Body.
filter: 'commitizen.cz.utils.multiple_line_breaker'
- type: confirm
name: show_message
message: Do you want to add body message in commit?
Expand Down Expand Up @@ -330,6 +354,13 @@
"""


@pytest.fixture
def staging_is_clean(mocker: MockFixture, tmp_git_project):
is_staging_clean_mock = mocker.patch("commitizen.git.is_staging_clean")
is_staging_clean_mock.return_value = False
return tmp_git_project


@pytest.fixture(
params=[
TomlConfig(data=TOML_STR, path="not_exist.toml"),
Expand Down Expand Up @@ -437,7 +468,7 @@ def test_change_type_order_unicode(config_with_unicode):
]


def test_questions(config):
def test_questions_default(config):
cz = CustomizeCommitsCz(config)
questions = cz.questions()
expected_questions = [
Expand All @@ -450,7 +481,18 @@ def test_questions(config):
],
"message": "Select the type of change you are committing",
},
{"type": "input", "name": "message", "message": "Body."},
{
"type": "input",
"name": "subject",
"message": "Subject.",
"filter": 'lambda text: commitizen.cz.utils.required_validator(text.strip(".").strip(), msg="! Error: Subject is required")',
},
{
"type": "input",
"name": "message",
"message": "Body.",
"filter": "commitizen.cz.utils.multiple_line_breaker",
},
{
"type": "confirm",
"name": "show_message",
Expand All @@ -460,6 +502,38 @@ def test_questions(config):
assert list(questions) == expected_questions


@pytest.mark.usefixtures("staging_is_clean")
def test_questions_filter(config, mocker: MockFixture):
is_staging_clean_mock = mocker.patch("commitizen.git.is_staging_clean")
is_staging_clean_mock.return_value = False

prompt_mock = mocker.patch("questionary.prompt")
prompt_mock.return_value = {
"change_type": "feature",
"subject": "user created",
"message": "body of the commit",
"show_message": True,
}

commit_mock = mocker.patch("commitizen.git.commit")
commit_mock.return_value = cmd.Command("success", "", b"", b"", 0)

commands.Commit(config, {})()

prompts_questions = prompt_mock.call_args[0][0]
assert prompts_questions[0]["type"] == "list"
assert prompts_questions[0]["name"] == "change_type"
assert prompts_questions[0]["use_shortcuts"] is False
assert prompts_questions[1]["type"] == "input"
assert prompts_questions[1]["name"] == "subject"
assert type(prompts_questions[1]["filter"]) is LambdaType
assert prompts_questions[2]["type"] == "input"
assert prompts_questions[2]["name"] == "message"
assert prompts_questions[2]["filter"] == multiple_line_breaker
assert prompts_questions[3]["type"] == "confirm"
assert prompts_questions[3]["name"] == "show_message"


def test_questions_unicode(config_with_unicode):
cz = CustomizeCommitsCz(config_with_unicode)
questions = cz.questions()
Expand Down

0 comments on commit 5011a91

Please sign in to comment.