Search
Search copied to clipboard
Use of keyword-only arguments in the CLI's `run` commands
Whenever we have positional and optional CLI arguments, how do we reflect this in the run command?
Option 1: make all arguments keyword only. E.g. my-command file_name --verbose would be implemented as
def run(*, file_name, verbose=True)
Option 2: turn positional CLI arguments to normal arguments, optional ones to keyword-only. E.g. my-command file_name --verbose would be implemented as
def run(file_name, *, verbose=True)
This issue emerged from the discussion posted by @jankrepl in https://github.com/BlueBrain/Search/pull/482#discussion_r736296577_
I would also say that there is Option 3: Not using * at all.
Option 1 comment
For the record, I was the one who introduced this logic. And let me just stress that it is actually composed of 2 ideas: "keyword only" and "without defaults" signatures.
Why "keyword only" signatures could be a good idea?
Making sure that the run function and the corresponding parser always stay in sync. Alternatively, we could write unit tests that check that the run commands and their respective parsers are in sync (as done in a recent PR using inspect).
Why "without defaults" signatures could be a good idea?
The goal is to force the developer to specify all the defaults in the parser. This way we don't really have to worry about maintaining 2 sets of defaults. Maybe this could also be checked using inspect.
Option 2 comment
One thing to note here is that parser.parse_args() is going to return a Namespace object that does not really store any information about what parameters were arguments and what parameters were options in the CLI. (I hope I am not wrong). Therefore at "call time" we never pass any positional arguments anyway. Additionally, the developer would be forced not only to make sure that the run and the parser have the same arguments but also that
- CLI options = Python keyword arguments
- CLI arguments = Python positional arguments
which is extra work with IMO no benefits.
Good point about maintaining/synching 2 sets of defaults. I agree it makes sens to have run without any defaults and let argparse manage this.
This being said, are there any other practical reasons to favour one of the three options mentioned so far?
run(file_name, *, verbose)run(*, file_name, verbose)run(file_name, verbose)
This being said, are there any other practical reasons to favour one of the three options mentioned so far?
run(file_name, *, verbose)run(*, file_name, verbose)run(file_name, verbose)
Maybe it doesn't really matter.
For me the most important things when it comes to the run function vs parser
- Avoid duplication of defaults
- Avoid duplication of docstrings=
help(unless you want the Python function to contain different kind of information) - Enforce somehow that
vars(parser.parse_args()).keys()is equal to the parameters ofrun(unit tests are fine)
I was trying to find a way to get a list of arguments from in instance of ArgumentParser, and I couldn't find a straightforward way... The tests using inspect was the best I could come up with...
I was trying to find a way to get a list of arguments from in instance of
ArgumentParser, and I couldn't find a straightforward way... The tests usinginspectwas the best I could come up with...
You mean without actually running parse_args, right? Hmm...good point, I don't know how to do it. However, there must be a way...