Missing required command-line options are reported using post-transformation names

Open
#1,017 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
45/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Stale
Tech stack
python
Domain
cli

Research direction

Start by tracing the parser entry points shown in the debug output, especially invoke.parser.init, parse_argv, complete_context, and invoke.program.run. Reproduce the missing-argument error and verify that its reported names match the command-line option spelling, including hyphens where applicable.

Written by the indexing model from the issue text.

Description

I noticed that when a command-line "option" is required, but not given in the task invocation, the reported names use underscores instead of hyphens.

This isn't a huge deal, but it does present a bit of an impediment to a user responding to what is wrong. If I saw that error message without existing knowledge of invoke, I would think that I need to add e.g. --param_1 asdf to my command-line. A straightforward workaround would be to assign a default value, test for it, and raise an appropriate error from the body of the task.

It would also be helpful to have the - or -- prefix in this case, but I imagine this is a harder problem to solve since not all ways to enter the task are necessarily from the command-line? Anyway, I couldn't find an existing report for this, so I figured I'd file one.

Reproduction
$ cat tasks.py 
from invoke import task

@task
def dummy(ctx, param_1, param_2):
    print(f"Got {param_1=} {param_2=}")
$ invoke dummy --param-1 asdf --param-2 1234
Got param_1='asdf' param_2='1234'
$ invoke dummy
'dummy' did not receive required positional arguments: 'param_1', 'param_2'
click for `invoke --debug dummy` output
$ invoke --debug dummy
invoke.program.parse_collection: No default namespace provided, trying to load one from disk
invoke.loader.find: FilesystemLoader find starting at '/tmp'
invoke.loader.find: Found module: ModuleSpec(name='tasks', loader=<_frozen_importlib_external.SourceFileLoader object at 0x7c00493c8bb0>, origin='/tmp/tasks.py')
invoke.config._load_file: Didn't see any /tmp/invoke.yaml, skipping.
invoke.config._load_file: Didn't see any /tmp/invoke.yml, skipping.
invoke.config._load_file: Didn't see any /tmp/invoke.json, skipping.
invoke.config.merge: Merging config sources in order onto new empty _config...
invoke.config.merge: Defaults: {'run': {'asynchronous': False, 'disown': False, 'dry': False, 'echo': False, 'echo_stdin': None, 'encoding': None, 'env': {}, 'err_stream': None, 'fallback': True, 'hide': None, 'in_stream': None, 'out_stream': None, 'echo_format': '\x1b[1;37m{command}\x1b[0m', 'pty': False, 'replace_env': False, 'shell': '/bin/bash', 'warn': False, 'watchers': []}, 'runners': {'local': <class 'invoke.runners.Local'>}, 'sudo': {'password': None, 'prompt': '[sudo] password: ', 'user': None}, 'tasks': {'auto_dash_names': True, 'collection_name': 'tasks', 'dedupe': True, 'executor_class': None, 'ignore_unknown_help': False, 'search_root': None}, 'timeouts': {'command': None}}
invoke.config.merge: Collection-driven: {}
invoke.config._merge_file: System-wide config file (/etc/invoke.py): {}
invoke.config._merge_file: Per-user config file (/home/jgerity/.invoke.py): {}
invoke.config._merge_file: Per-project config file (/tmp/invoke.py): {}
invoke.config.merge: Environment variable config: {}
invoke.config._merge_file: Runtime config file has not been loaded yet, skipping
invoke.config.merge: Overrides: {}
invoke.config.merge: Modifications: {}
invoke.config.merge: Deletions: {}
invoke.parser.__init__: Adding <parser/Context 'dummy': {'param-1': <Argument: param_1 (p) *>, 'param-2': <Argument: param_2 (a) *>}>
invoke.program.parse_tasks: Parsing tasks against <Collection 'tasks': dummy>
invoke.parser.__init__: Initialized with context: <parser/Context: {'command-timeout': <Argument: command-timeout (T) [int]>, 'complete': <Argument: complete [bool]>, 'config': <Argument: config (f)>, 'debug': <Argument: debug (d) [bool]>, 'dry': <Argument: dry (R) [bool]>, 'echo': <Argument: echo (e) [bool]>, 'help': <Argument: help (h) ?>, 'hide': <Argument: hide>, 'list': <Argument: list (l) ?>, 'list-depth': <Argument: list-depth (D) [int]>, 'list-format': <Argument: list-format (F)>, 'print-completion-script': <Argument: print-completion-script>, 'prompt-for-sudo-password': <Argument: prompt-for-sudo-password [bool]>, 'pty': <Argument: pty (p) [bool]>, 'version': <Argument: version (V) [bool]>, 'warn-only': <Argument: warn-only (w) [bool]>, 'write-pyc': <Argument: write-pyc [bool]>, 'collection': <Argument: collection (c)>, 'no-dedupe': <Argument: no-dedupe [bool]>, 'search-root': <Argument: search-root (r)>}>
invoke.parser.__init__: Available contexts: {'dummy': <parser/Context 'dummy': {'param-1': <Argument: param_1 (p) *>, 'param-2': <Argument: param_2 (a) *>}>}
invoke.parser.complete_context: Wrapping up context None
invoke.parser.parse_argv: Starting argv: ['dummy']
invoke.parser.handle: Handling token: 'dummy'
invoke.parser.changing_state: ParseMachine: 'context' => 'context'
invoke.parser.complete_context: Wrapping up context None
invoke.parser.switch_to_context: Moving to context 'dummy'
invoke.parser.switch_to_context: Context args: {'param-1': <Argument: param_1 (p) *>, 'param-2': <Argument: param_2 (a) *>}
invoke.parser.switch_to_context: Context flags: {'--param-1': <Argument: param_1 (p) *>, '--param-2': <Argument: param_2 (a) *>}
invoke.parser.switch_to_context: Context inverse_flags: {}
invoke.parser.changing_state: ParseMachine: 'context' => 'end'
invoke.parser.complete_context: Wrapping up context 'dummy'
invoke.program.run: Received a possibly-skippable exception: ParseError("'dummy' did not receive required positional arguments: 'param_1', 'param_2'")
'dummy' did not receive required positional arguments: 'param_1', 'param_2'
Dominant language
Python
Stars
4.8k
Forks
412
PR merge metrics
No merged PRs in 30d

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from pyinvoke/invoke

All issues in pyinvoke/invoke

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.