Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
732cbcd
feat(copier/_user_data.py): add `questionary` `use_shorcuts` & `use_f…
RR5555 Jan 8, 2026
06c942f
docs(copier/_user_data.py): complement `Question` docstring to doc `u…
RR5555 Jan 8, 2026
75bcd23
test(copier/_user_data.py): add tests for `Question` `use_shortcuts` …
RR5555 Jan 8, 2026
8f951e4
Merge branch 'copier-org:master' into question_filter_shortcut
RR5555 Jan 8, 2026
08371bf
test(tests/): rename `test_shortcut.py` to `test_choices.py`
RR5555 Jan 11, 2026
13d341b
fix(copier/_user_data.py): fix attribute name of `Question` from `use…
RR5555 Jan 11, 2026
20d24f8
docs(Question): line-wrap the docstring to max length 88 for consistency
RR5555 Jan 11, 2026
f8a8cb1
test(tests/test_choices.py): remove useless elements
RR5555 Jan 11, 2026
f734a35
test(tests/test_choices.py): curate the test template
RR5555 Jan 11, 2026
f8c695e
docs(docs/configuring.md): add docs for `use_shortcuts` & `use_search…
RR5555 Jan 11, 2026
70e7e42
test(tests/test_choices.py): split tests between `use_shortcuts` & `u…
RR5555 Jan 12, 2026
4ba04f7
test(tests/test_choices): fix type hints for compliance with `test_ty…
RR5555 Jan 12, 2026
c1f6643
Merge branch 'copier-org:master' into question_filter_shortcut
RR5555 Mar 16, 2026
bcdf3d2
Merge branch 'copier-org:master' into question_filter_shortcut
RR5555 Mar 16, 2026
5b3e902
fix(Question): change from superseeding to `ValidationError` for mutu…
RR5555 Mar 16, 2026
928e117
docs(Question): improve & adapt docstring to mutual exclusiveness
RR5555 Mar 16, 2026
215cd0c
test(tests/test_choices.py): break down fixtures into clearer tests; …
RR5555 Mar 16, 2026
04cd33d
docs(docs/configuring.md): improve & simplify the docs relative to `u…
RR5555 Mar 16, 2026
db9794e
feat(mkdocs.yml): enable `pymdownx.keys` for pretty keyboard keys
RR5555 Mar 16, 2026
70b67b2
Merge remote-tracking branch 'upstream/master' into question_filter_s…
RR5555 May 3, 2026
5d0c9c7
refactor(Question): move `use_shortcuts` validation to a Pydantic Mod…
RR5555 May 3, 2026
ddddbea
docs(docs/configuring.md): switch "Supported keys" item order
RR5555 May 3, 2026
38a3954
docs(docs/configuring.md): disable `rumdl` for `pre` html tag blocks
RR5555 May 5, 2026
aa1c05a
docs(docs/configuring.md): disable `rumdl` for `pre` html tag blocks
RR5555 May 5, 2026
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
15 changes: 15 additions & 0 deletions copier/_user_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ class Question:
If it is a boolean, it is used directly. If it is a str, it is
converted to boolean using a parser similar to YAML, but only for
boolean values.

use_shortcuts:
Condition that, if `True`, will use `use_shortcuts` in `select` question, allowing for selection via automatically numbered shortcut. Will be deactivated if `use_filter_search` is `True`.

use_filter_search:
Condition that, if `True`, uses `use_search_filter` in `checkbox`/`select` question while deactivating `use_jk_keys`, allowing for selection via filtering.
Comment thread
RR5555 marked this conversation as resolved.
Outdated
"""

var_name: str
Expand All @@ -221,6 +227,8 @@ class Question:
type: str = Field(default="", validate_default=True)
validator: str = ""
when: str | bool = True
use_shortcuts: bool = False
use_filter_search: bool = False

@field_validator("var_name")
@classmethod
Expand Down Expand Up @@ -419,6 +427,13 @@ def _validate(answer: str) -> str | Literal[True]:
result["default"] = False
if self.choices:
questionary_type = "checkbox" if self.multiselect else "select"

if self.use_filter_search:
Comment thread
RR5555 marked this conversation as resolved.
Outdated
result["use_search_filter"] = True
result["use_jk_keys"] = False
elif self.use_shortcuts and questionary_type == "select":
result["use_shortcuts"] = True

choices = self._formatted_choices
# Select default choices for a multiselect question.
if self.multiselect and isinstance(
Expand Down
234 changes: 234 additions & 0 deletions tests/test_shortcut.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
from __future__ import annotations

from collections import deque
from textwrap import dedent

import pexpect
import pytest
from plumbum import local

from .helpers import (
BRACKET_ENVOPS,
BRACKET_ENVOPS_JSON,
COPIER_PATH,
SUFFIX_TMPL,
Keyboard,
Spawn,
build_file_tree,
expect_prompt,
)

BLK_START = BRACKET_ENVOPS["block_start_string"]
BLK_END = BRACKET_ENVOPS["block_end_string"]


@pytest.mark.parametrize(
"copier_file, input_select, result",
[
(
f"""\
_templates_suffix: {SUFFIX_TMPL}
_envops: {BRACKET_ENVOPS_JSON}
select:
type: str
help: Select one option only
use_shortcuts: true
default: first
choices:
one: first
two: second
three: third
checkbox:
type: str
help: Select any
multiselect: true
use_filter_search: true
choices:
one: first
two: second
three: third
four: fourth
""",
"3",
dedent(
"""\
select: "third"
checkbox: ["third"]
""",
),
),
(
f"""\
_templates_suffix: {SUFFIX_TMPL}
_envops: {BRACKET_ENVOPS_JSON}
select:
type: str
help: Select one option only
default: first
choices:
one: first
two: second
three: third
checkbox:
type: str
help: Select any
multiselect: true
choices:
one: first
two: second
three: third
four: fourth
""",
"3",
dedent(
"""\
select: "first"
checkbox: ["first"]
"""
),
),
(
f"""\
_templates_suffix: {SUFFIX_TMPL}
_envops: {BRACKET_ENVOPS_JSON}
select:
type: str
help: Select one option only
use_shortcuts: true
default: first
choices:
one: first
two: second
three: third
checkbox:
type: str
help: Select any
multiselect: true
choices:
one: first
two: second
three: third
four: fourth
""",
"3",
dedent(
"""\
select: "third"
checkbox: ["first"]
"""
),
),
(
f"""\
_templates_suffix: {SUFFIX_TMPL}
_envops: {BRACKET_ENVOPS_JSON}
select:
type: str
help: Select one option only
default: first
choices:
one: first
two: second
three: third
checkbox:
type: str
help: Select any
multiselect: true
use_filter_search: true
choices:
one: first
two: second
three: third
four: fourth
""",
"3",
dedent(
"""\
select: "first"
checkbox: ["third"]
"""
),
),
(
f"""\
_templates_suffix: {SUFFIX_TMPL}
_envops: {BRACKET_ENVOPS_JSON}
select:
type: str
help: Select one option only
use_filter_search: true
default: first
choices:
one: first
two: second
three: third
checkbox:
type: str
help: Select any
multiselect: true
choices:
one: first
two: second
three: third
four: fourth
""",
"tw",
dedent(
"""\
select: "second"
checkbox: ["first"]
"""
),
),
],
)
def test_shortcuts(
tmp_path_factory: pytest.TempPathFactory,
copier_file: str,
input_select: str,
result: str,
spawn: Spawn,
) -> None:
"""Test shortcuts."""
# tui = spawn(COPIER_PATH + ("copy", path, str(tmp_path)))

src, dst = map(tmp_path_factory.mktemp, ("src", "dst"))

with local.cwd(src):
build_file_tree(
{
"copier.yml": copier_file,
"results.txt.tmpl": """\
select: [[select|tojson]]
checkbox: [[checkbox|tojson]]
""",
}
)

tui = spawn(COPIER_PATH + ("copy", str(src), str(dst)))

expect_prompt(tui, "select", "str", help="Select one option only")
deque(
map(
tui.expect_exact,
[
"one",
"two",
"three",
],
)
)
tui.send(input_select)
tui.send(Keyboard.Enter)

expect_prompt(tui, "checkbox", "str", help="Select any")
deque(
map(
tui.expect_exact,
["one", "two", "three", "four"],
)
)
tui.send("th ")
tui.send(Keyboard.Enter)
tui.expect_exact(pexpect.EOF)
assert (dst / "results.txt").read_text() == result
Loading