diff --git a/CHANGELOG.md b/CHANGELOG.md index ef0ce278..9d5afb42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [Unreleased](https://github.com/jshwi/docsig/compare/v0.32.0...HEAD) ------------------------------------------------------------------------ ### Added +- Add `-k/--ignore-kwargs` argument - Add `-a/--ignore-args` argument [0.32.0](https://github.com/jshwi/docsig/releases/tag/v0.32.0) - 2022-12-11 diff --git a/README.rst b/README.rst index 4b34f276..da34a8c1 100644 --- a/README.rst +++ b/README.rst @@ -42,8 +42,8 @@ Commandline .. code-block:: console - usage: docsig [-h] [-v] [-c] [-D] [-o] [-p] [-P] [-i] [-a] [-n] [-S] [-s STR] [-d LIST] - [-t LIST] + usage: docsig [-h] [-v] [-c] [-D] [-o] [-p] [-P] [-i] [-a] [-k] [-n] [-S] [-s STR] + [-d LIST] [-t LIST] [path [path ...]] Check signature params for proper documentation @@ -61,6 +61,7 @@ Commandline -P, --check-property-returns check property return values -i, --ignore-no-params ignore docstrings where parameters are not documented -a, --ignore-args ignore args prefixed with an asterisk + -k, --ignore-kwargs ignore kwargs prefixed with two asterisks -n, --no-ansi disable ansi output -S, --summary print a summarised report -s STR, --string STR string to parse instead of files diff --git a/docsig/_config.py b/docsig/_config.py index b1d44321..b462ba40 100644 --- a/docsig/_config.py +++ b/docsig/_config.py @@ -79,6 +79,12 @@ def _add_arguments(self) -> None: action="store_true", help="ignore args prefixed with an asterisk", ) + self.add_argument( + "-k", + "--ignore-kwargs", + action="store_true", + help="ignore kwargs prefixed with two asterisks", + ) self.add_argument( "-n", "--no-ansi", action="store_true", help="disable ansi output" ) diff --git a/docsig/_core.py b/docsig/_core.py index 91242bd7..ed37b044 100644 --- a/docsig/_core.py +++ b/docsig/_core.py @@ -56,6 +56,7 @@ def docsig( # pylint: disable=too-many-locals check_property_returns: bool = False, ignore_no_params: bool = False, ignore_args: bool = False, + ignore_kwargs: bool = False, no_ansi: bool = False, summary: bool = False, targets: list[str] | None = None, @@ -80,13 +81,19 @@ def docsig( # pylint: disable=too-many-locals :param ignore_no_params: Ignore docstrings where parameters are not documented :param ignore_args: Ignore args prefixed with an asterisk. + :param ignore_kwargs: Ignore kwargs prefixed with two asterisks. :param no_ansi: Disable ANSI output. :param summary: Print a summarised report. :param targets: List of errors to target. :param disable: List of errors to disable. :return: Exit status for whether test failed or not. """ - modules = _Modules(*path, string=string, ignore_args=ignore_args) + modules = _Modules( + *path, + string=string, + ignore_args=ignore_args, + ignore_kwargs=ignore_kwargs, + ) display = _Display(no_ansi) for module in modules: for top_level in module: diff --git a/docsig/_function.py b/docsig/_function.py index 17eddada..c347f83e 100644 --- a/docsig/_function.py +++ b/docsig/_function.py @@ -109,16 +109,23 @@ def __init__(self, string: str) -> None: class _Params(_MutableSequence[Param]): - def __init__(self, ignore_args: bool = False) -> None: + def __init__( + self, ignore_args: bool = False, ignore_kwargs: bool = False + ) -> None: super().__init__() self._ignore_args = ignore_args + self._ignore_kwargs = ignore_kwargs # pylint: disable=too-many-boolean-expressions def insert(self, index: int, value: Param) -> None: if not value.isprotected and ( value.kind == PARAM or (value.kind == ARG and not self._ignore_args) - or (value.kind == KEY and not any(i.kind == KEY for i in self)) + or ( + value.kind == KEY + and not self._ignore_kwargs + and not any(i.kind == KEY for i in self) + ) ): super().insert(index, value) @@ -143,8 +150,10 @@ def duplicated(self) -> bool: class _DocSig: - def __init__(self, ignore_args: bool = False) -> None: - self._args = _Params(ignore_args) + def __init__( + self, ignore_args: bool = False, ignore_kwargs: bool = False + ) -> None: + self._args = _Params(ignore_args, ignore_kwargs) self._returns = False @property @@ -166,8 +175,9 @@ def __init__( # pylint: disable=too-many-arguments ismethod: bool = False, isstaticmethod: bool = False, ignore_args: bool = False, + ignore_kwargs: bool = False, ) -> None: - super().__init__(ignore_args) + super().__init__(ignore_args, ignore_kwargs) if ismethod and not isstaticmethod and arguments.args: arguments.args.pop(0) @@ -218,9 +228,12 @@ def rettype(self) -> str | None: class _Docstring(_DocSig): def __init__( - self, node: _ast.Const | None = None, ignore_args: bool = False + self, + node: _ast.Const | None = None, + ignore_args: bool = False, + ignore_kwargs: bool = False, ) -> None: - super().__init__(ignore_args) + super().__init__(ignore_args, ignore_kwargs) self._string = None if node is not None: self._string = _RawDocstring(node.value) @@ -249,10 +262,14 @@ class Function: :param node: Function's abstract syntax tree. :param ignore_args: Ignore args prefixed with an asterisk. + :param ignore_kwargs: Ignore kwargs prefixed with two asterisks. """ def __init__( - self, node: _ast.FunctionDef, ignore_args: bool = False + self, + node: _ast.FunctionDef, + ignore_args: bool = False, + ignore_kwargs: bool = False, ) -> None: self._node = node self._parent = node.parent.frame() @@ -262,10 +279,11 @@ def __init__( self.ismethod, self.isstaticmethod, ignore_args, + ignore_kwargs, ) self._docstring = _Docstring( node.doc_node if not self.isinit else self._parent.doc_node, - ignore_args, + ignore_kwargs, ) def __len__(self) -> int: diff --git a/docsig/_main.py b/docsig/_main.py index bda20a5e..7458369e 100644 --- a/docsig/_main.py +++ b/docsig/_main.py @@ -26,6 +26,7 @@ def main() -> int: check_property_returns=parser.args.check_property_returns, ignore_no_params=parser.args.ignore_no_params, ignore_args=parser.args.ignore_args, + ignore_kwargs=parser.args.ignore_kwargs, no_ansi=parser.args.no_ansi, summary=parser.args.summary, targets=parser.args.target, diff --git a/docsig/_module.py b/docsig/_module.py index ac5cdfac..8840bce2 100644 --- a/docsig/_module.py +++ b/docsig/_module.py @@ -19,6 +19,7 @@ class Parent(_MutableSequence[_Function]): :param node: Parent's abstract syntax tree. :param path: Path to base path representation on. :param ignore_args: Ignore args prefixed with an asterisk. + :param ignore_kwargs: Ignore kwargs prefixed with two asterisks. """ def __init__( @@ -26,13 +27,14 @@ def __init__( node: _ast.Module | _ast.ClassDef, path: _Path | None = None, ignore_args: bool = False, + ignore_kwargs: bool = False, ) -> None: super().__init__() self._name = node.name self._path = f"{path}:" if path is not None else "" for subnode in node.body: if isinstance(subnode, _ast.FunctionDef): - self.append(_Function(subnode, ignore_args)) + self.append(_Function(subnode, ignore_args, ignore_kwargs)) @property def path(self) -> str: @@ -57,12 +59,13 @@ def __init__( node: _ast.Module, path: _Path | None = None, ignore_args: bool = False, + ignore_kwargs: bool = False, ) -> None: super().__init__() - self.append(Parent(node, path, ignore_args)) + self.append(Parent(node, path, ignore_args, ignore_kwargs)) for subnode in node.body: if isinstance(subnode, _ast.ClassDef): - self.append(Parent(subnode, path, ignore_args)) + self.append(Parent(subnode, path, ignore_args, ignore_kwargs)) class Modules(_MutableSequence[_Module]): @@ -76,6 +79,7 @@ class Modules(_MutableSequence[_Module]): :param paths: Path(s) to pase ``Module``(s) from. :param string: String to parse if provided. :param ignore_args: Ignore args prefixed with an asterisk. + :param ignore_kwargs: Ignore kwargs prefixed with two asterisks. """ def __init__( @@ -83,11 +87,19 @@ def __init__( *paths: _Path, string: str | None = None, ignore_args: bool = False, + ignore_kwargs: bool = False, ) -> None: super().__init__() self._ignore_args = ignore_args + self._ignore_kwargs = ignore_kwargs if string is not None: - self.append(_Module(_ast.parse(string), ignore_args=ignore_args)) + self.append( + _Module( + _ast.parse(string), + ignore_args=ignore_args, + ignore_kwargs=ignore_kwargs, + ) + ) else: for path in paths: self._populate(path) @@ -95,7 +107,12 @@ def __init__( def _populate(self, root: _Path) -> None: if root.is_file() and root.name.endswith(".py"): self.append( - _Module(_ast.parse(root.read_text()), root, self._ignore_args) + _Module( + _ast.parse(root.read_text()), + root, + self._ignore_args, + self._ignore_kwargs, + ) ) if root.is_dir(): diff --git a/tests/_test.py b/tests/_test.py index 645bc515..43918e99 100644 --- a/tests/_test.py +++ b/tests/_test.py @@ -845,3 +845,43 @@ def test_ignore_args( or name.startswith(PASS) and "w-args" in name ) + + +@pytest.mark.parametrize( + [NAME, TEMPLATE, "_"], + templates.registered.filtergroup(MULTI), + ids=templates.registered.filtergroup(MULTI).getids(), +) +def test_ignore_kwargs( + init_file: InitFileFixtureType, + main: MockMainType, + name: str, + template: str, + _: str, +) -> None: + """Test docstrings without kwargs don't fail wih ``-k``. + + Passing tests will fail and failing tests will pass, as tests which + generally pass will have kwargs documented, which shouldn't be with + this argument. + + :param init_file: Initialize a test file. + :param main: Mock ``main`` function. + :param name: Name of test. + :param template: Contents to write to file. + """ + file = init_file(template) + assert main( + long.check_class, + long.check_protected, + long.check_overridden, + long.check_dunders, + long.check_property_returns, + long.ignore_kwargs, + file.parent, + ) == int( + name.startswith(FAIL) + and "w-kwargs" not in name + or name.startswith(PASS) + and "w-kwargs" in name + )