Skip to content

Commit

Permalink
Support boolean flags
Browse files Browse the repository at this point in the history
  • Loading branch information
Dominik1123 committed Nov 10, 2020
1 parent 78f58d2 commit 51a08b3
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 21 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,23 @@ of `add_options_from` for more information. In the following some possibilities
@add_options_from(display_data, custom={'symbol': {'default': '#'}})
```

### Boolean flags

Boolean flags are supported via the `bool` type hint. The default behavior is to create an on-/off-option
as described in the [click docs](https://click.palletsprojects.com/en/7.x/options/#boolean-flags).
If this is undesired, it can be customized by using the `names` keyword parameter of `add_options_from`:

```python
foo: bool = True
# translates to
click.option('--foo/--no-foo', default=True)

# Use the following to create an on-/off-option:
add_options_from(my_func, names={'foo': ['--foo']})
# translates to
click.option('--foo', is_flag=True, default=True)
```

### Lists and tuples

`click-inspect` also supports sequences as type hints (e.g. `list[int]` or `tuple[int, str]`).
Expand Down Expand Up @@ -166,6 +183,6 @@ Args:
So `Union[int, str]` is equivalent to `int`.
Unions are also supported as part of the docstring via `int or str`.

## Docstring styles
### Docstring styles

`click-inspect` supports inspecting [reST-style](https://www.python.org/dev/peps/pep-0287/) docstrings, as well as [Google-](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) and [Numpy-style](https://numpydoc.readthedocs.io/en/latest/format.html) docstrings via [`sphinx.ext.napoleon`](https://github.com/sphinx-doc/sphinx/tree/master/sphinx/ext/napoleon).
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "click-inspect"
version = "0.2"
version = "0.3"
description = "Add options to click commands based on inspecting functions"
authors = ["Dominik1123"]
license = "MIT"
Expand Down
38 changes: 24 additions & 14 deletions src/click_inspect/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,6 @@ def _decorator(f):
if not condition:
continue

try:
opt_names = names[name]
except KeyError:
opt_names = f'--{name.replace("_", "-")}',

kwargs = {}
if 'help' in p_doc[name]:
kwargs['help'] = p_doc[name]['help']
Expand All @@ -97,20 +92,33 @@ def _decorator(f):
kwargs['default'] = parameter.default
else:
kwargs['required'] = True

try:
kwargs['type'] = custom[name]['type']
except KeyError:
try:
kwargs['type'] = custom[name]['type']
tp_hint = type_hints[name]
except KeyError:
try:
tp_hint = type_hints[name]
tp_hint = p_doc[name]['type']
except KeyError:
try:
tp_hint = p_doc[name]['type']
except KeyError:
tp_hint = None
tp_hint = None
if parameter.default is EMPTY:
warnings.warn(f'No type hint for parameter {name!r}')
if tp_hint is not None:
kwargs.update(_parse_type_hint_into_kwargs(tp_hint))
if tp_hint is not None:
kwargs.update(_parse_type_hint_into_kwargs(tp_hint))

kwargs.update(custom.get(name, {}))

try:
opt_names = names[name]
except KeyError:
opt_name = name.replace("_", "-")
if kwargs.get('is_flag', False):
opt_names = [f'--{opt_name}/--no-{opt_name}']
else:
opt_names = [f'--{opt_name}']

click.option(*opt_names, **kwargs)(f)
return f

Expand All @@ -119,7 +127,9 @@ def _decorator(f):

def _parse_type_hint_into_kwargs(tp_hint):
args, origin = get_args(tp_hint), get_origin(tp_hint)
if origin in (list, collections.abc.Sequence):
if tp_hint is bool:
return dict(is_flag=True)
elif origin in (list, collections.abc.Sequence):
return dict(multiple=True, type=args[0])
elif origin is tuple:
return dict(type=args)
Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

@pytest.fixture(scope='function')
def base_function():
def _f(a, b: int = 1, *, c: int, d: str = 'test'):
def _f(a, b: int = 1, *, c: int, d: str = 'test', e: bool = True):
"""Short description.
Long
Expand All @@ -16,6 +16,7 @@ def _f(a, b: int = 1, *, c: int, d: str = 'test'):
b (int): This one should be added.
c (int): This one should be added too.
d (str): And so should this one.
e (bool): Boolean flag.
Returns:
str: This is just a test.
Expand Down
72 changes: 68 additions & 4 deletions tests/test_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,40 @@ def test_add_options_from(base_function):
@add_options_from(base_function)
def test(): pass

assert len(test.params) == 3
assert len(test.params) == 4

assert test.params[0].name == 'b'
assert test.params[0].opts == ['--b']
assert test.params[0].secondary_opts == []
assert test.params[0].type is click.INT
assert test.params[0].default == 1
assert test.params[0].required is False
assert test.params[0].help == 'This one should be added.'

assert test.params[1].name == 'c'
assert test.params[1].opts == ['--c']
assert test.params[1].secondary_opts == []
assert test.params[1].type is click.INT
assert test.params[1].required is True
assert test.params[1].help == 'This one should be added too.'

assert test.params[2].name == 'd'
assert test.params[2].opts == ['--d']
assert test.params[2].secondary_opts == []
assert test.params[2].type is click.STRING
assert test.params[2].default == 'test'
assert test.params[2].required is False
assert test.params[2].help == 'And so should this one.'

assert test.params[3].name == 'e'
assert test.params[3].opts == ['--e']
assert test.params[3].secondary_opts == ['--no-e']
assert test.params[3].type is click.BOOL
assert test.params[3].is_flag is True
assert test.params[3].default is True
assert test.params[3].required is False
assert test.params[3].help == 'Boolean flag.'


def test_add_options_from_infer_types_from_docstring(base_function):
base_function.__annotations__ = {}
Expand All @@ -50,12 +62,14 @@ def test(): pass

assert test.params[0].name == 'a'
assert test.params[0].opts == ['--a']
assert test.params[0].secondary_opts == []
assert test.params[0].type is click.STRING
assert test.params[0].required is True
assert test.params[0].help == 'This parameter should be skipped.'

assert test.params[1].name == 'b'
assert test.params[1].opts == ['--b']
assert test.params[1].secondary_opts == []
assert test.params[1].type is click.INT
assert test.params[1].default == 1
assert test.params[1].required is False
Expand All @@ -67,42 +81,75 @@ def test_add_options_from_exclude(base_function):
@add_options_from(base_function, exclude={'b', 'c'})
def test(): pass

assert len(test.params) == 1
assert len(test.params) == 2

assert test.params[0].name == 'd'
assert test.params[0].opts == ['--d']
assert test.params[0].secondary_opts == []
assert test.params[0].type is click.STRING
assert test.params[0].default == 'test'
assert test.params[0].required is False
assert test.params[0].help == 'And so should this one.'

assert test.params[1].name == 'e'
assert test.params[1].opts == ['--e']
assert test.params[1].secondary_opts == ['--no-e']
assert test.params[1].type is click.BOOL
assert test.params[1].is_flag is True
assert test.params[1].default is True
assert test.params[1].required is False
assert test.params[1].help == 'Boolean flag.'


def test_add_options_from_names(base_function):
@click.command()
@add_options_from(base_function, names={'b': ['-b'], 'd': ['-test', '--d']})
def test(): pass

assert len(test.params) == 3
assert len(test.params) == 4

assert test.params[0].name == 'b'
assert test.params[0].opts == ['-b']
assert test.params[0].secondary_opts == []

assert test.params[1].name == 'c'
assert test.params[1].opts == ['--c']
assert test.params[1].secondary_opts == []

assert test.params[2].name == 'd'
assert test.params[2].opts == ['-test', '--d']
assert test.params[2].secondary_opts == []

assert test.params[3].name == 'e'
assert test.params[3].opts == ['--e']
assert test.params[3].secondary_opts == ['--no-e']


def test_add_options_from_custom(base_function):
@click.command()
@add_options_from(base_function, custom={'d': dict(default='custom_default')})
def test(): pass

assert len(test.params) == 3
assert len(test.params) == 4
assert test.params[2].default == 'custom_default'


def test_add_options_from_single_switch_boolean_flag(base_function):
@click.command()
@add_options_from(base_function, names={'e': ['--e']})
def test(): pass

assert len(test.params) == 4
assert test.params[3].name == 'e'
assert test.params[3].opts == ['--e']
assert test.params[3].secondary_opts == []
assert test.params[3].type is click.BOOL
assert test.params[3].is_flag is True
assert test.params[3].default is True
assert test.params[3].required is False
assert test.params[3].help == 'Boolean flag.'


def test_add_options_from_warn_if_no_type():
def func(*, a):
"""Test func.
Expand All @@ -119,6 +166,23 @@ def test(): pass
assert str(warninfo[0].message.args[0]) == "No type hint for parameter 'a'"


def test_add_options_from_no_warn_if_no_type_but_default():
def func(*, a = 1):
"""Test func.
Args:
a: Test parameter
"""

@click.command()
@add_options_from(func)
def test(): pass

assert len(test.params) == 1
assert test.params[0].type is click.INT
assert test.params[0].default == 1


def test_add_options_from_unsupported_docstring_style():
def func(*, a: int):
"""Test func.
Expand Down
1 change: 1 addition & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def test_parse_docstring_base_function(base_function):
'b': {'help': 'This one should be added.', 'type': int},
'c': {'help': 'This one should be added too.', 'type': int},
'd': {'help': 'And so should this one.', 'type': str},
'e': {'help': 'Boolean flag.', 'type': bool},
}


Expand Down

0 comments on commit 51a08b3

Please sign in to comment.