Skip to content

feat: add copier inspect subcommand#2575

Open
jannahopp wants to merge 1 commit intocopier-org:masterfrom
jannahopp:feat/show-questions
Open

feat: add copier inspect subcommand#2575
jannahopp wants to merge 1 commit intocopier-org:masterfrom
jannahopp:feat/show-questions

Conversation

@jannahopp
Copy link
Copy Markdown

@jannahopp jannahopp commented Mar 26, 2026

Why

When using Copier non-interactively (CI pipelines, agents, scripts), there is no way to discover which --data parameters a template expects without reading copier.yaml directly. This makes templates opaque to tooling that wants to call copier copy with the right --data flags.

What

Adds a new copier inspect <template> subcommand that prints template questions with their metadata without copying or modifying any files.

$ copier inspect gh:copier-org/autopretty
python (bool)  default: True
  help: Will your project use Python 3.6 or later?

poetry_local_hooks (bool)  default: False
  when: {{ python }}
  help: Do you prefer to run local hooks prefixed with `poetry run`?
...

Key behaviors:

  • Output formats: plain (default), json, yaml via --output-format
  • Questions with when: false (computed/derived values) are hidden in plain output but included in JSON/YAML with a computed: true marker
  • Conditional questions show raw when Jinja2 expressions
  • No --trust required (exits before any unsafe checks)
  • Supports --vcs-ref to inspect a specific template version

Design note

The original version of this PR added a --show-questions flag to copy/recopy/update. As pointed out in review, this was a poor fit: the flag performed an inspect action on mutation commands, requiring irrelevant arguments (like a destination path). A dedicated copier inspect subcommand — modeled after copier check-update — is a much better UX.

Test plan

  • copier inspect <template> prints questions to stdout
  • copier inspect --output-format json produces valid JSON with computed markers
  • copier inspect --output-format yaml produces valid YAML with computed markers
  • copier inspect --quiet suppresses output
  • Computed questions (when: false, when: 0, when: "false") hidden in plain output
  • Dict choices, Jinja expression choices, multiselect displayed correctly
  • Type inferred from default value when not specified
  • Empty template (no questions) produces clean empty output
  • Invalid template path gives clean error (exit 1)
  • Existing test suite unaffected

🤖 Generated with Claude Code

Copy link
Copy Markdown
Member

@sisp sisp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some reservations about the current change:

  • Why add this flag to the copy/recopy/update commands when it performs the exact same task? Conceptually, it isn't even related to any of those because it is an inspect action and not a (re)copy/update action.
  • The UX of using this flag is bad. For example, copier copy requires a destination path, but this path is irrelevant when using the flag, and it can't be omitted because the path is mandatory for normal use of the command.

Perhaps a new subcommand like copier inspect (inspired by, e.g., docker inspect <URL> is more suitable for this feature. In any case, we'll eventually need tests for this feature.

Do you think that printing the question specs is useful even when they are possibly heavily templated and when there are (possibly non-trivial chains of) conditional questions? Note also that when: false questions are computed/derived values for which no answer shall be provided by the user.

Thanks for disclosing the use of AI in the PR description.

@jannahopp jannahopp force-pushed the feat/show-questions branch 2 times, most recently from f279980 to 566552c Compare March 26, 2026 15:54
@jannahopp
Copy link
Copy Markdown
Author

Thanks for the thorough review — all very valid points!

Backstory on the --show-questions approach: The original plan was to make the feature more dynamic — evaluating when conditions using partial answers passed via --data, which is why it was placed on copy/recopy/update (to leverage their Worker/answer infrastructure). That dynamic approach turned out to be over-engineered, and after simplifying to static display we missed that the UX no longer justified living on those subcommands. Your suggestion of a dedicated subcommand was exactly the right call.

What changed:

  • Replaced --show-questions flag with a new copier inspect <template> subcommand (modeled after check-update)
  • Only requires the template source — no destination path, no --trust
  • Output goes to stdout (not stderr), supports --output-format plain|json|yaml
  • Questions with trivially false when (computed/derived values) are filtered from plain output, but included in JSON/YAML with a computed: true marker for programmatic consumers
  • Conditional questions (when with Jinja2 expressions) show the raw expression — the limitation that the actual question set depends on answers to earlier questions is documented in the subcommand's help text
  • Added tests (18 test cases covering all output formats, edge cases, and error handling)

@jannahopp jannahopp changed the title feat: add --show-questions flag to copy, recopy, and update feat: add copier inspect subcommand Mar 26, 2026
Add a new `copier inspect <template>` subcommand that prints template
questions with their metadata (type, default, choices, conditions, help
text) without copying or modifying any files.

Key behaviors:
- Output formats: plain (default), JSON, YAML via --output-format
- Questions with `when: false` (computed/derived values) are hidden
  in plain output but included in JSON/YAML with `computed: true`
- Conditional questions show raw `when` Jinja2 expressions
- No --trust required (no files created, no tasks executed)

Closes copier-org#2574

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jannahopp jannahopp force-pushed the feat/show-questions branch from 566552c to 31ef758 Compare March 26, 2026 16:04
Copy link
Copy Markdown
Member

@sisp sisp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether it would be better to simply print a YAML dump of the question specs instead of trying to make it human-readable which may have all kinds of edge cases (e.g., complex choices, conditional questions, conditionally required questions). WDYT?


A comment regarding use of AI: The code changes are clearly AI-generated, as you disclosed using Claude Code in the PR description – there is nothing wrong with it per se. But it is my strong impression that you didn't invest time into reviewing and validating the changes properly prior to pushing commits which means you're offloading this work to us which creates additional effort on our side, disrespecting our time we invest into this project. Also, please write PR comments in your own words. Thanks for your understanding.

str,
help="Git reference to checkout in `template_src`.",
)
quiet = cli.Flag(["-q", "--quiet"], help="Suppress status output")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the point of this subcommand when its output is suppressed? I think this flag makes no sense here and should be removed.

Comment on lines +41 to +44
secret_token:
type: str
secret: true
help: API token for deployment
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an incorrect question spec because Copier currently requires a default answer for secret questions:

  • secret: When true, it hides the prompt displaying asterisks (*****) and doesn't save the answer in the answers file. When true, a default value is required.

print(f" {label}: {choices}")
elif isinstance(choices, dict):
print(
f" {label}: {', '.join(str(c) for c in choices.keys())}"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Listing the choice keys when choices is a dict means that the "labels" are listed but not the values/answers. When passing answers via -d, --data or --data-file flag, the dict value must be passed.

This leads to a more fundamental question: How do you intend to print choice values for JSON/YAML questions when they are complex (multiline) objects?

Comment on lines +501 to +502
if default is MISSING:
parts.append("REQUIRED")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question can be conditionally required by rendering the special UNSET variable. See the example of the default field at https://copier.readthedocs.io/en/stable/configuring/#advanced-prompt-formatting:

database_engine:
    type: str
    help: Database engine
    choices:
        - postgres
        - mysql
        - other
    default: postgres

database_url:
    type: str
    help: Database URL
    default: >-
        {%- if database_engine == 'postgres' -%}
        postgresql://user:pass@localhost:5432/dbname
        {%- elif database_engine == 'mysql' -%}
        mysql://user:pass@localhost:3306/dbname
        {%- else -%}
        {{ UNSET }}
        {%- endif -%}
    # Simplified for illustration purposes
    validator: "{% if '://' not in database_url %}Invalid{% endif %}"

This means, printing REQUIRED according to this logic isn't guaranteed to be accurate.

Comment on lines +456 to +457
if isinstance(when, str) and "{{" in when:
return False
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Jinja variable start marker {{ can be configured in the template via

_envops:
  variable_start_string: "[["

so it is incorrect to hardcode it here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better and clearer to assert the full printed output for a given question spec in copier.yml rather than asserting substring matches. Instead of having one test template with many questions, a parametrized test with one question spec and one expected print output per test case would be preferable IMHO.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer keeping this function internal until there is real demand for performing this via Python API to minimize the public API surface.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants