1313from contextvars import ContextVar
1414from dataclasses import field , replace
1515from filecmp import dircmp
16+ from fnmatch import fnmatchcase
1617from functools import cached_property , partial , wraps
1718from itertools import chain
1819from pathlib import Path , PurePath , PurePosixPath , PureWindowsPath
@@ -228,6 +229,10 @@ class Worker:
228229
229230 skip_tasks:
230231 When `True`, skip template tasks execution.
232+
233+ ask:
234+ List of question names to ask, even if they would be skipped by other
235+ options. Supports glob-style patterns.
231236 """
232237
233238 # NOTE: attributes are fully documented in [creating.md](../docs/creating.md)
@@ -253,6 +258,7 @@ class Worker:
253258 unsafe : bool = False
254259 skip_answered : bool = False
255260 skip_tasks : bool = False
261+ ask : Sequence [str ] = ()
256262
257263 answers : AnswersMap = field (default_factory = AnswersMap , init = False )
258264 _cleanup_hooks : list [Callable [[], None ]] = field (default_factory = list , init = False )
@@ -622,35 +628,36 @@ def _ask(self) -> None: # noqa: C901
622628 # value.
623629 if question .get_default () is MISSING :
624630 continue
625- if var_name in self .answers .init :
626- # Try to parse and validate (if the question has a validator)
627- # the answer value.
628- answer = question .parse_answer (self .answers .init [var_name ])
629- question .validate_answer (answer )
630- # At this point, the answer value is valid. Do not ask the
631- # question again, but set answer as the user's answer instead.
632- self .answers .user [var_name ] = answer
633- continue
634- # Skip a question when the user already answered it.
635- if self .skip_answered and var_name in self .answers .last :
636- continue
637631
638- # Display TUI and ask user interactively only without --defaults
639- try :
632+ if not any (fnmatchcase (var_name , ask_pattern ) for ask_pattern in self .ask ):
633+ # If the user didn't explicitly request the question be asked,
634+ # it may now be skipped by `--data`, `--skip-answered`, or `--defaults`.
635+ if var_name in self .answers .init :
636+ # Try to parse and validate (if the question has a validator)
637+ # the answer value.
638+ answer = question .parse_answer (self .answers .init [var_name ])
639+ question .validate_answer (answer )
640+ self .answers .user [var_name ] = answer
641+ continue
642+ if self .skip_answered and var_name in self .answers .last :
643+ continue
640644 if self .defaults :
641- new_answer = question .get_default ()
642- if new_answer is MISSING :
645+ answer = question .get_default ()
646+ if answer is MISSING :
643647 raise ValueError (f'Question "{ var_name } " is required' )
644- else :
645- try :
646- new_answer = unsafe_prompt (
647- [question .get_questionary_structure ()],
648- answers = {question .var_name : question .get_default ()},
649- )[question .var_name ]
650- except EOFError as err :
651- raise InteractiveSessionError (
652- "Use `--defaults` and/or `--data`/`--data-file`"
653- ) from err
648+ self .answers .user [var_name ] = answer
649+ continue
650+
651+ # Display TUI and ask user interactively only without --defaults
652+ try :
653+ new_answer = unsafe_prompt (
654+ [question .get_questionary_structure ()],
655+ answers = {question .var_name : question .get_default ()},
656+ )[question .var_name ]
657+ except EOFError as err :
658+ raise InteractiveSessionError (
659+ "Use `--defaults` and/or `--data`/`--data-file`"
660+ ) from err
654661 except KeyboardInterrupt as err :
655662 raise CopierAnswersInterrupt (
656663 self .answers , question , self .template
@@ -1385,6 +1392,7 @@ def _apply_update(self) -> None: # noqa: C901
13851392 # won't be included in the diff as deleted paths to prevent deletion.
13861393 # https://github.com/orgs/copier-org/discussions/2345
13871394 exclude = [* self .template .exclude , * self .exclude ],
1395+ ask = (),
13881396 ) as old_worker :
13891397 old_worker .run_copy ()
13901398 # Run pre-migration tasks
@@ -1457,6 +1465,7 @@ def _apply_update(self) -> None: # noqa: C901
14571465 src_path = self .subproject .template .url , # type: ignore[union-attr]
14581466 exclude = exclude_plus_removed ,
14591467 vcs_ref = self .resolved_vcs_ref ,
1468+ ask = (),
14601469 ) as new_worker :
14611470 new_worker .run_copy ()
14621471 with local .cwd (new_copy ):
@@ -1708,6 +1717,7 @@ def run_copy(
17081717 quiet : bool = False ,
17091718 unsafe : bool = False ,
17101719 skip_tasks : bool = False ,
1720+ ask : Sequence [str ] = (),
17111721) -> Worker :
17121722 """Copy a template to a destination, from zero."""
17131723 with Worker (
@@ -1736,6 +1746,7 @@ def run_copy(
17361746 quiet = quiet ,
17371747 unsafe = unsafe ,
17381748 skip_tasks = skip_tasks ,
1749+ ask = ask ,
17391750 ) as worker :
17401751 worker .run_copy ()
17411752 return worker
@@ -1760,6 +1771,7 @@ def run_recopy(
17601771 unsafe : bool = False ,
17611772 skip_answered : bool = False ,
17621773 skip_tasks : bool = False ,
1774+ ask : Sequence [str ] = (),
17631775) -> Worker :
17641776 """Update a subproject from its template, discarding subproject evolution."""
17651777 with Worker (
@@ -1788,6 +1800,7 @@ def run_recopy(
17881800 unsafe = unsafe ,
17891801 skip_answered = skip_answered ,
17901802 skip_tasks = skip_tasks ,
1803+ ask = ask ,
17911804 ) as worker :
17921805 worker .run_recopy ()
17931806 return worker
@@ -1814,6 +1827,7 @@ def run_update(
18141827 unsafe : bool = False ,
18151828 skip_answered : bool = False ,
18161829 skip_tasks : bool = False ,
1830+ ask : Sequence [str ] = (),
18171831) -> Worker :
18181832 """Update a subproject, from its template."""
18191833 with Worker (
@@ -1844,6 +1858,7 @@ def run_update(
18441858 unsafe = unsafe ,
18451859 skip_answered = skip_answered ,
18461860 skip_tasks = skip_tasks ,
1861+ ask = ask ,
18471862 ) as worker :
18481863 worker .run_update ()
18491864 return worker
0 commit comments