From f50df5acac28e6809c74adda54cc748ef717cfab Mon Sep 17 00:00:00 2001 From: Dominik1123 <> Date: Wed, 11 Nov 2020 22:14:36 +0100 Subject: [PATCH] Don't warn for ignored parameters --- pyproject.toml | 2 +- src/click_inspect/decorators.py | 18 ++++++++---------- src/click_inspect/parser.py | 21 ++++++++++++++++++--- tests/test_decorators.py | 22 +++++++++++++++++++++- tests/test_parser.py | 9 +++++++++ 5 files changed, 57 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3214acd..2262cbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "click-inspect" -version = "0.3.1" +version = "0.3.2" description = "Add options to click commands based on inspecting functions" authors = ["Dominik1123"] license = "MIT" diff --git a/src/click_inspect/decorators.py b/src/click_inspect/decorators.py index 052d1b5..87edd83 100644 --- a/src/click_inspect/decorators.py +++ b/src/click_inspect/decorators.py @@ -3,7 +3,8 @@ import inspect from inspect import Parameter import sys -from typing import Any, Dict, Sequence, Set, Union, get_type_hints +from types import MappingProxyType +from typing import Any, Collection, Container, Mapping, Sequence, Union, get_type_hints try: from typing import get_args, get_origin # type: ignore except ImportError: # pragma: no cover @@ -23,10 +24,10 @@ def add_options_from(func, *, - names: Dict[str, Sequence[str]] = None, - include: Set[str] = None, - exclude: Set[str] = None, - custom: Dict[str, Dict[str, Any]] = None): + names: Mapping[str, Sequence[str]] = MappingProxyType({}), + include: Collection[str] = frozenset(), + exclude: Container[str] = frozenset(), + custom: Mapping[str, Mapping[str, Any]] = MappingProxyType({})): """Inspect `func` and add corresponding options to the decorated function. Args: @@ -48,11 +49,8 @@ def add_options_from(func, If `func` type hints contain standard collections as type hinting generics for Python < 3.9 (e.g. `list[int]`). """ - include = include or set() - names = names or {} - custom = custom or {} try: - p_doc = parse_docstring(func.__doc__ or '') + p_doc = parse_docstring(func, ignore=exclude) except UnsupportedDocstringStyle: p_doc = defaultdict(dict) try: @@ -69,7 +67,7 @@ def add_options_from(func, raise # pragma: no cover type_hints = {} all_parameters = inspect.signature(func).parameters - to_be_used = (include or all_parameters.keys()) - (exclude or set()) + to_be_used = {name for name in (include or all_parameters.keys()) if name not in exclude} parameters = [(name, parameter) for name, parameter in all_parameters.items() if name in to_be_used] diff --git a/src/click_inspect/parser.py b/src/click_inspect/parser.py index 195c141..00120d9 100644 --- a/src/click_inspect/parser.py +++ b/src/click_inspect/parser.py @@ -2,7 +2,7 @@ from collections import defaultdict import inspect -from typing import Any, DefaultDict, Dict +from typing import Any, Container, DefaultDict, Dict import warnings from sphinx.ext.napoleon import Config, GoogleDocstring, NumpyDocstring # type: ignore @@ -17,12 +17,25 @@ NUMPY_HEADER = 'Parameters\n----------' -def parse_docstring(obj) -> Dict[str, Dict[str, Any]]: - """Parse the given docstring or the given obj's docstring.""" +def parse_docstring(obj, *, ignore: Container[str] = frozenset()) -> Dict[str, Dict[str, Any]]: + """Parse the given docstring or the given obj's docstring. + + Args: + obj (function or str): Parse the docstring from the given object. + ignore (set): Ignore the type hint string of those parameters. + + Returns: + DefaultDict: Per parameter specification containing 'help' and 'type' (if provided). + + Raises: + UnsupportedDocstringStyle: If the given docstring contains no parameter section. + """ if isinstance(obj, str): doc, func = inspect.cleandoc(obj), None else: doc, func = inspect.getdoc(obj), obj # type: ignore + if doc is None: + return defaultdict(dict) if NUMPY_HEADER in doc: lines = NumpyDocstring(doc, config=CONFIG).lines() elif GOOGLE_HEADER in doc: @@ -37,6 +50,8 @@ def parse_docstring(obj) -> Dict[str, Dict[str, Any]]: name, parameters[name]['help'] = _find_name_and_remainder(line) elif line.startswith(':type'): name, type_string = _find_name_and_remainder(line) + if name in ignore: + continue try: parameters[name]['type'] = typstr_parse(type_string, func=func) except NameError as err: diff --git a/tests/test_decorators.py b/tests/test_decorators.py index d00f760..f0df7d0 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -289,7 +289,7 @@ def test_add_options_from_union_type_hint_via_docstring(union_type_hint_function test_add_options_from_union_type_hint(union_type_hint_function) -def test_add_option_from_nested_union_and_sequence(): +def test_add_options_from_nested_union_and_sequence(): def func(*, x: Union[List[int], str]): pass @click.command() @@ -300,6 +300,26 @@ def test(): pass assert test.params[0].type is click.INT +def test_add_options_from_no_type_warning_for_excluded_parameters(): + def func(*, x: int): # Use some valid type hint here to prevent further warnings. + """ + Args: + x (UnknownType): If 'x' gets excluded, no warning should be issued. + """ + + with pytest.warns(UserWarning) as warninfo: + @add_options_from(func) + def test(): pass + + assert len(warninfo) == 1 + + @click.command() + @add_options_from(func, exclude={'x'}) + def test(): pass + + assert len(test.params) == 0 + + @pytest.mark.skipif(sys.version_info >= (3, 9), reason='Starting with Python 3.9 get_type_hints works without raising TypeError.') def test_add_options_from_warn_on_standard_collections_as_typing_generics(): diff --git a/tests/test_parser.py b/tests/test_parser.py index 25481c9..232a9bf 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -122,6 +122,10 @@ def test_parse_docstring(doc_func_or_string): assert str(warninfo[0].message.args[0]).startswith("Type hint 'CustomType' cannot be resolved.") +def test_parse_docstring_no_warning_if_ignored(doc_func_or_string): + assert parse_docstring(doc_func_or_string, ignore={'a_b_c'}) + + def test_parse_docstring_raises(): with pytest.raises(UnsupportedDocstringStyle) as excinfo: parse_docstring('This docstring contains no parameters') @@ -145,3 +149,8 @@ def _f(): x (int and str): Type string is not supported. """ assert parse_docstring(_f) == {'x': {'help': 'Type string is not supported.'}} + + +def test_parse_docstring_return_empty_dict_if_no_doc(): + def test(): pass + assert parse_docstring(test) == {}