anntzer / defopt

Effortless argument parser
https://pypi.org/project/defopt/
MIT License
213 stars 11 forks source link

Feature Request: Make Optional[bool] still act as a flag #99

Closed Spectre5 closed 2 years ago

Spectre5 commented 2 years ago

Consider the code below. I'd like to have the argument skip still act as a normal boolean flag and then program would only get None if neither --skip nor --no-skip is provided. The idea here is that if the user explicitly sets this flag, then it should be followed. But if they don't set the flag explicitly (so it is None), then the CLI would have some logic to decide if the default value would be True or False, potentially depending on some other input, like loops in this dummy example.

from typing import Optional

import defopt

def test(*, loops: int = 5, skip: Optional[bool] = None):
    """This is a dummy function.

    Parameters
    ----------
    skip
        Skip printing something.
    """
    print(f'loops = {loops}')
    print(f'skip  = {skip}')
    _skip = skip if skip is not None else loops > 10
    if _skip:
        print(f'skipping loop printing')
    for i in range(loops):
        if not _skip:
            print(f'long loop iteration: {i+1}')

if __name__ == '__main__':
    defopt.run(test)

But this is now recognized as a variable input instead of a flag.

$ python dummy.py --help
usage: dummy.py [-h] [-l LOOPS] [-s SKIP]

This is a dummy function.

optional arguments:
  -h, --help            show this help message and exit
  -l LOOPS, --loops LOOPS
                        (default: 5)
  -s SKIP, --skip SKIP  Skip printing something.
                        (default: None)
nox > Session dummy was successful.

So specifying nothing works:

$ python dummy.py
loops = 5
skip  = None
long loop iteration: 1
long loop iteration: 2
long loop iteration: 3
long loop iteration: 4
long loop iteration: 5

Specifying 0 or 1 as the input works:

$ python dummy.py --skip 1
loops = 5
skip  = True
skipping loop printing
$ python dummy.py --skip 0
loops = 5
skip  = False
long loop iteration: 1
long loop iteration: 2
long loop iteration: 3
long loop iteration: 4
long loop iteration: 

But of course using just --skip alone (or --no-skip) doesn't work. Note also that, as far as I can tell, you can't manually specify the value of None either:

$ python dummy.py --skip None
usage: dummy.py [-h] [-l LOOPS] [-s SKIP]
dummy.py: error: argument -s/--skip: invalid typing.Optional[bool] value: 'None'

What I would like, is for this to be treated as a flag still. This would be a breaking change since now using the example of python dummy.py --skip 1 would no longer work as instead it would just be python dummy.py --skip or python dummy.py --no-skip. I personally can't imagine any scenario in which using --skip 1 and --skip 0 is better than --skip and --no-skip.

A PR to consider for this is incoming.

anntzer commented 2 years ago

Let's keep the discussion on #100; I agree with the general idea.