Skip to content

Commit 78ed2a0

Browse files
committed
refactor(commit-preview): extract commit preview question wiring into helper
1 parent 8c0a489 commit 78ed2a0

File tree

2 files changed

+133
-114
lines changed

2 files changed

+133
-114
lines changed

commitizen/commands/commit.py

Lines changed: 12 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
import subprocess
77
import tempfile
88
from pathlib import Path
9-
from typing import TYPE_CHECKING, Any, TypedDict
9+
from typing import TYPE_CHECKING, TypedDict
1010

1111
import questionary
12-
from prompt_toolkit.application.current import get_app
1312

1413
from commitizen import factory, git, out
1514
from commitizen.cz.exceptions import CzException
@@ -26,16 +25,9 @@
2625
NothingToCommitError,
2726
)
2827
from commitizen.git import smart_open
29-
from commitizen.interactive_preview import (
30-
make_length_validator as make_length_validator_preview,
31-
)
32-
from commitizen.interactive_preview import (
33-
make_toolbar_content as make_toolbar_content_preview,
34-
)
28+
from commitizen.preview_questions import build_preview_questions
3529

3630
if TYPE_CHECKING:
37-
from collections.abc import Callable
38-
3931
from commitizen.config import BaseConfig
4032

4133

@@ -49,16 +41,12 @@ class CommitArgs(TypedDict, total=False):
4941
signoff: bool
5042
write_message_to_file: Path | None
5143
retry: bool
44+
preview: bool
5245

5346

5447
class Commit:
5548
"""Show prompt for the user to create a guided commit."""
5649

57-
# Questionary types for interactive preview hooks (length validator / toolbar),
58-
# based on questionary 2.0.1
59-
VALIDATABLE_TYPES = {"input", "text", "password", "path", "checkbox"}
60-
BOTTOM_TOOLBAR_TYPES = {"input", "text", "password", "confirm"}
61-
6250
def __init__(self, config: BaseConfig, arguments: CommitArgs) -> None:
6351
if not git.is_git_project():
6452
raise NotAGitProjectError()
@@ -85,100 +73,6 @@ def _read_backup_message(self) -> str | None:
8573
encoding=self.config.settings["encoding"]
8674
).strip()
8775

88-
def _build_commit_questions(
89-
self,
90-
questions: list,
91-
preview_enabled: bool,
92-
max_preview_length: int,
93-
) -> list:
94-
"""Build the list of questions to ask; add toolbar/validate when preview enabled."""
95-
if not preview_enabled:
96-
return list(questions)
97-
98-
default_answers: dict[str, Any] = {
99-
q["name"]: q.get("default", "")
100-
for q in questions
101-
if isinstance(q.get("name"), str)
102-
}
103-
field_filters: dict[str, Any] = {
104-
q["name"]: q.get("filter")
105-
for q in questions
106-
if isinstance(q.get("name"), str)
107-
}
108-
answers_state: dict[str, Any] = {}
109-
110-
def _get_current_buffer_text() -> str:
111-
try:
112-
app = get_app()
113-
buffer = app.layout.current_buffer
114-
return buffer.text if buffer is not None else ""
115-
except Exception:
116-
return ""
117-
118-
def subject_builder(current_field: str, current_text: str) -> str:
119-
preview_answers: dict[str, Any] = default_answers.copy()
120-
preview_answers.update(answers_state)
121-
if current_field:
122-
field_filter = field_filters.get(current_field)
123-
if field_filter:
124-
try:
125-
preview_answers[current_field] = field_filter(current_text)
126-
except Exception:
127-
preview_answers[current_field] = current_text
128-
else:
129-
preview_answers[current_field] = current_text
130-
try:
131-
return self.cz.message(preview_answers).partition("\n")[0].strip()
132-
except Exception:
133-
return ""
134-
135-
def make_stateful_filter(
136-
name: str, original_filter: Callable[[str], Any] | None
137-
) -> Callable[[str], Any]:
138-
def _filter(raw: str) -> Any:
139-
value = original_filter(raw) if original_filter else raw
140-
answers_state[name] = value
141-
return value
142-
143-
return _filter
144-
145-
def make_toolbar(name: str) -> Callable[[], str]:
146-
def _toolbar() -> str:
147-
return make_toolbar_content_preview(
148-
subject_builder,
149-
name,
150-
_get_current_buffer_text(),
151-
max_length=max_preview_length,
152-
)
153-
154-
return _toolbar
155-
156-
def make_length_validator(name: str) -> Callable[[str], bool | str]:
157-
return make_length_validator_preview(
158-
subject_builder,
159-
name,
160-
max_length=max_preview_length,
161-
)
162-
163-
enhanced_questions: list[dict[str, object]] = []
164-
for q in questions:
165-
q_dict = dict(q)
166-
q_type = q_dict.get("type")
167-
name = q_dict.get("name")
168-
169-
if isinstance(name, str):
170-
original_filter = q_dict.get("filter")
171-
q_dict["filter"] = make_stateful_filter(name, original_filter)
172-
173-
if q_type in self.BOTTOM_TOOLBAR_TYPES:
174-
q_dict["bottom_toolbar"] = make_toolbar(name)
175-
176-
if q_type in self.VALIDATABLE_TYPES:
177-
q_dict["validate"] = make_length_validator(name)
178-
179-
enhanced_questions.append(q_dict)
180-
return enhanced_questions
181-
18276
def _get_message_by_prompt_commit_questions(self) -> str:
18377
questions = self.cz.questions()
18478
for question in (q for q in questions if q["type"] == "list"):
@@ -188,13 +82,17 @@ def _get_message_by_prompt_commit_questions(self) -> str:
18882
self.arguments.get("preview", False)
18983
or self.config.settings.get("preview", False)
19084
)
191-
max_preview_length = self.arguments.get(
192-
"message_length_limit",
193-
self.config.settings.get("message_length_limit", 0),
85+
max_preview_length = (
86+
self.arguments.get("message_length_limit")
87+
if self.arguments.get("message_length_limit") is not None
88+
else self.config.settings.get("message_length_limit", 0)
19489
)
19590

196-
questions_to_ask = self._build_commit_questions(
197-
questions, preview_enabled, max_preview_length
91+
questions_to_ask = build_preview_questions(
92+
self.cz,
93+
questions,
94+
enabled=preview_enabled,
95+
max_length=max_preview_length,
19896
)
19997

20098
try:

commitizen/preview_questions.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, Any
4+
5+
from prompt_toolkit.application.current import get_app
6+
7+
from commitizen.interactive_preview import (
8+
make_length_validator as make_length_validator_preview,
9+
)
10+
from commitizen.interactive_preview import (
11+
make_toolbar_content as make_toolbar_content_preview,
12+
)
13+
14+
if TYPE_CHECKING:
15+
from collections.abc import Callable
16+
17+
from commitizen.cz.base import BaseCommitizen
18+
from commitizen.question import CzQuestion
19+
20+
21+
# Questionary types for interactive preview hooks (length validator / toolbar),
22+
# based on questionary 2.0.1
23+
VALIDATABLE_TYPES = {"input", "text", "password", "path", "checkbox"}
24+
BOTTOM_TOOLBAR_TYPES = {"input", "text", "password", "confirm"}
25+
26+
27+
def build_preview_questions(
28+
cz: BaseCommitizen,
29+
questions: list[CzQuestion],
30+
*,
31+
enabled: bool,
32+
max_length: int,
33+
) -> list[CzQuestion]:
34+
"""Return questions enhanced with interactive preview, when enabled."""
35+
if not enabled:
36+
return questions
37+
38+
max_preview_length = max_length
39+
40+
default_answers: dict[str, Any] = {
41+
q["name"]: q.get("default", "")
42+
for q in questions
43+
if isinstance(q.get("name"), str)
44+
}
45+
field_filters: dict[str, Any] = {
46+
q["name"]: q.get("filter") for q in questions if isinstance(q.get("name"), str)
47+
}
48+
answers_state: dict[str, Any] = {}
49+
50+
def _get_current_buffer_text() -> str:
51+
try:
52+
app = get_app()
53+
buffer = app.layout.current_buffer
54+
return buffer.text if buffer is not None else ""
55+
except Exception:
56+
return ""
57+
58+
def subject_builder(current_field: str, current_text: str) -> str:
59+
preview_answers: dict[str, Any] = default_answers.copy()
60+
preview_answers.update(answers_state)
61+
if current_field:
62+
field_filter = field_filters.get(current_field)
63+
if field_filter:
64+
try:
65+
preview_answers[current_field] = field_filter(current_text)
66+
except Exception:
67+
preview_answers[current_field] = current_text
68+
else:
69+
preview_answers[current_field] = current_text
70+
try:
71+
return cz.message(preview_answers).partition("\n")[0].strip()
72+
except Exception:
73+
return ""
74+
75+
def make_stateful_filter(
76+
name: str, original_filter: Callable[[str], str] | None
77+
) -> Callable[[str], str]:
78+
def _filter(raw: str) -> str:
79+
value = original_filter(raw) if original_filter else raw
80+
answers_state[name] = value
81+
return value
82+
83+
return _filter
84+
85+
def make_toolbar(name: str) -> Callable[[], str]:
86+
def _toolbar() -> str:
87+
return make_toolbar_content_preview(
88+
subject_builder,
89+
name,
90+
_get_current_buffer_text(),
91+
max_length=max_preview_length,
92+
)
93+
94+
return _toolbar
95+
96+
def make_length_validator(name: str) -> Callable[[str], bool | str]:
97+
return make_length_validator_preview(
98+
subject_builder,
99+
name,
100+
max_length=max_preview_length,
101+
)
102+
103+
enhanced_questions: list[dict[str, object]] = []
104+
for q in questions:
105+
q_dict = q.copy()
106+
q_type = q_dict.get("type")
107+
name = q_dict.get("name")
108+
109+
if isinstance(name, str):
110+
original_filter = q_dict.get("filter")
111+
q_dict["filter"] = make_stateful_filter(name, original_filter)
112+
113+
if q_type in BOTTOM_TOOLBAR_TYPES:
114+
q_dict["bottom_toolbar"] = make_toolbar(name)
115+
116+
if q_type in VALIDATABLE_TYPES:
117+
q_dict["validate"] = make_length_validator(name)
118+
119+
enhanced_questions.append(q_dict)
120+
121+
return enhanced_questions

0 commit comments

Comments
 (0)