diff --git a/CHANGELOG.md b/CHANGELOG.md index 90ae240a..2d113027 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [Unreleased](https://github.com/jshwi/docsig/compare/v0.46.0...HEAD) ------------------------------------------------------------------------ +### Added +- option for checking nested functions and classes + +### Fixed +- check indented functions and classes [0.46.0](https://github.com/jshwi/docsig/releases/tag/v0.46.0) - 2024-04-09 ------------------------------------------------------------------------ diff --git a/README.rst b/README.rst index 900c9a4e..819bada8 100644 --- a/README.rst +++ b/README.rst @@ -77,8 +77,8 @@ Commandline .. code-block:: console - usage: docsig [-h] [-V] [-l] [-c | -C] [-D] [-m] [-o] [-p] [-P] [-i] [-a] [-k] - [-n] [-S] [-v] [-s STR] [-d LIST] [-t LIST] [-e EXCLUDE] + usage: docsig [-h] [-V] [-l] [-c | -C] [-D] [-m] [-N] [-o] [-p] [-P] [-i] [-a] + [-k] [-n] [-S] [-v] [-s STR] [-d LIST] [-t LIST] [-e EXCLUDE] [path [path ...]] Check signature params for proper documentation @@ -96,6 +96,7 @@ Commandline -D, --check-dunders check dunder methods -m, --check-protected-class-methods check public methods belonging to protected classes + -N, --check-nested check nested functions and classes -o, --check-overridden check overridden methods -p, --check-protected check protected functions and classes -P, --check-property-returns check property return values diff --git a/docsig/_config.py b/docsig/_config.py index 314dd1fd..d8e03600 100644 --- a/docsig/_config.py +++ b/docsig/_config.py @@ -67,6 +67,12 @@ def _add_arguments(self) -> None: action="store_true", help="check public methods belonging to protected classes", ) + self.add_argument( + "-N", + "--check-nested", + action="store_true", + help="check nested functions and classes", + ) self.add_argument( "-o", "--check-overridden", diff --git a/docsig/_core.py b/docsig/_core.py index 5ff1b579..750fb785 100644 --- a/docsig/_core.py +++ b/docsig/_core.py @@ -51,6 +51,7 @@ def _run_check( # pylint: disable=too-many-arguments check_class: bool, check_class_constructor: bool, check_dunders: bool, + check_nested: bool, check_overridden: bool, check_protected: bool, check_property_returns: bool, @@ -79,6 +80,24 @@ def _run_check( # pylint: disable=too-many-arguments failures.append( _Failure(child, _FuncStr(child, no_ansi), report) ) + + if check_nested: + for func in child: + _run_check( + func, + child, + check_class, + check_class_constructor, + check_dunders, + check_nested, + check_overridden, + check_protected, + check_property_returns, + ignore_no_params, + no_ansi, + targets, + failures, + ) else: for func in child: _run_check( @@ -87,6 +106,7 @@ def _run_check( # pylint: disable=too-many-arguments check_class, check_class_constructor, check_dunders, + check_nested, check_overridden, check_protected, check_property_returns, @@ -107,6 +127,7 @@ def docsig( # pylint: disable=too-many-locals,too-many-arguments check_class_constructor: bool = False, check_dunders: bool = False, check_protected_class_methods: bool = False, + check_nested: bool = False, check_overridden: bool = False, check_protected: bool = False, check_property_returns: bool = False, @@ -138,6 +159,7 @@ def docsig( # pylint: disable=too-many-locals,too-many-arguments :param check_dunders: Check dunder methods :param check_protected_class_methods: Check public methods belonging to protected classes. + :param check_nested: Check nested functions and classes. :param check_overridden: Check overridden methods :param check_protected: Check protected functions and classes. :param check_property_returns: Run return checks on properties. @@ -186,6 +208,7 @@ def docsig( # pylint: disable=too-many-locals,too-many-arguments check_class, check_class_constructor, check_dunders, + check_nested, check_overridden, check_protected, check_property_returns, diff --git a/docsig/_main.py b/docsig/_main.py index b61002e1..6f547f4f 100644 --- a/docsig/_main.py +++ b/docsig/_main.py @@ -31,6 +31,7 @@ def main() -> str | int: check_protected_class_methods=( parser.args.check_protected_class_methods ), + check_nested=parser.args.check_nested, check_overridden=parser.args.check_overridden, check_protected=parser.args.check_protected, check_property_returns=parser.args.check_property_returns, diff --git a/docsig/_module.py b/docsig/_module.py index b15eef7c..a681550b 100644 --- a/docsig/_module.py +++ b/docsig/_module.py @@ -48,55 +48,89 @@ def __init__( # pylint: disable=too-many-arguments super().__init__() self._name = node.name self._path = f"{path}:" if path is not None else "" - overloads = [] + overloads: list[str] = [] returns = None + self._parse_ast( + node, + directives, + path, + ignore_args, + ignore_kwargs, + check_class_constructor, + overloads, + returns, + ) + + def _parse_ast( # pylint: disable=protected-access,too-many-arguments + self, + node, + directives, + path, + ignore_args, + ignore_kwargs, + check_class_constructor, + overloads, + returns, + ) -> None: parent_comments, parent_disabled = directives.get( node.lineno, ([], []) ) - for subnode in node.body: - comments, disabled = directives.get(subnode.lineno, ([], [])) - comments.extend(parent_comments) - disabled.extend(parent_disabled) - if isinstance(subnode, _ast.FunctionDef): - func = Function( - subnode, - comments, - directives, - disabled, - path, - ignore_args, - ignore_kwargs, - check_class_constructor, - ) - if func.isoverloaded: - overloads.append(func.name) - returns = func.signature.rettype - else: - if func.name in overloads: - subnode.returns = returns - # noinspection PyProtectedMember - func._signature._rettype = ( - returns - if isinstance(returns, str) - else func._signature._get_rettype(returns) - ) - # noinspection PyProtectedMember - func._signature._returns = ( - str(func._signature._rettype) != "None" + if hasattr(node, "body"): + for subnode in node.body: + comments, disabled = directives.get(subnode.lineno, ([], [])) + comments.extend(parent_comments) + disabled.extend(parent_disabled) + if isinstance(subnode, _ast.FunctionDef): + func = Function( + subnode, + comments, + directives, + disabled, + path, + ignore_args, + ignore_kwargs, + check_class_constructor, + ) + if func.isoverloaded: + overloads.append(func.name) + returns = func.signature.rettype + else: + if func.name in overloads: + subnode.returns = returns + # noinspection PyProtectedMember + func._signature._rettype = ( + returns + if isinstance(returns, str) + else func._signature._get_rettype(returns) + ) + # noinspection PyProtectedMember + func._signature._returns = ( + str(func._signature._rettype) != "None" + ) + + self.append(func) + elif isinstance(subnode, _ast.ClassDef): + self.append( + Parent( + subnode, + directives, + path, + ignore_args, + ignore_kwargs, + check_class_constructor, ) - - self.append(func) - elif isinstance(subnode, _ast.ClassDef): - self.append( - Parent( + ) + else: + self._parse_ast( subnode, directives, path, ignore_args, ignore_kwargs, check_class_constructor, + overloads, + returns, ) - ) @property def path(self) -> str: diff --git a/tests/__init__.py b/tests/__init__.py index 15c9a17c..1c293bde 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -50,6 +50,7 @@ def __call__(self, content: str, path: Path = ..., /) -> Path: long.check_dunders, long.check_property_returns, long.check_protected_class_methods, + long.check_nested, ) FAIL_CHECK_ARGS = tuple(f"f-{i[8:]}" for i in CHECK_ARGS) ENABLE = "enable" @@ -8603,3 +8604,323 @@ def order(self, sig: _Param, doc: _Param) -> None: @property def expected(self) -> str: return "" + + +@_templates.register +class _FFuncInIfStatementN(_BaseTemplate): + @property + def template(self) -> str: + return ''' +def my_function(argument: int = 42) -> int: + """ + Function that prints a message and returns the argument + 1 + + Parameters + ---------- + argument : int, optional + The input argument, by default 42 + + Returns + ------- + int + The input argument + 1 + """ + print("Hello from a function") + print(argument) + return argument + 1 + + +if True: + my_function(42) + def my_external_function(argument: int = 42) -> int: + print("Hello from an external function") + print(argument) + return argument + 42 +''' + + @property + def expected(self) -> str: + return """\ +def my_external_function(✖argument) -> ✖int: + ... + +E113: function is missing a docstring (function-doc-missing) + +""" + + +@_templates.register +class _FKlassInIfStatementN(_BaseTemplate): + @property + def template(self) -> str: + return ''' +if True: + class Klass: + """Class is OK.""" + def my_external_function(self, argument: int = 42) -> int: + print("Hello from an external function") + print(argument) + return argument + 42 +''' + + @property + def expected(self) -> str: + return """\ +def my_external_function(✖argument) -> ✖int: + ... + +E113: function is missing a docstring (function-doc-missing) + +""" + + +@_templates.register +class _FFuncInIfInIfStatementN(_BaseTemplate): + @property + def template(self) -> str: + return """ +if True: + if True: + def my_external_function(argument: int = 42) -> int: + print("Hello from an external function") + print(argument) + return argument + 42 +""" + + @property + def expected(self) -> str: + return """\ +def my_external_function(✖argument) -> ✖int: + ... + +E113: function is missing a docstring (function-doc-missing) + +""" + + +@_templates.register +class _FKlassNotMethodOkN(_BaseTemplate): + @property + def template(self) -> str: + return ''' +class Klass: + def __init__(self, this) -> None: + self.this = this + def my_external_function(self, argument: int = 42) -> int: + """This is a method. + + :param argument: An int. + :return: An int. + """ + print("Hello from an external function") + print(argument) + return argument + 42 +''' + + @property + def expected(self) -> str: + return """\ +class Klass: + ... + + def __init__(✖this) -> ✓None: + +E114: class is missing a docstring (class-doc-missing) +""" + + +@_templates.register +class _FFuncInForLoopN(_BaseTemplate): + @property + def template(self) -> str: + return """ +container = [] + +for argument in container: + def my_external_function(argument: int = 42) -> int: + print("Hello from an external function") + print(argument) + return argument + 42 +""" + + @property + def expected(self) -> str: + return """\ +def my_external_function(✖argument) -> ✖int: + ... + +E113: function is missing a docstring (function-doc-missing) + +""" + + +@_templates.register +class _FFuncInForLoopIfN(_BaseTemplate): + @property + def template(self) -> str: + return """ +container = [] + +for argument in container: + if argument > 0: + def my_external_function(argument: int = 42) -> int: + print("Hello from an external function") + print(argument) + return argument + 42 +""" + + @property + def expected(self) -> str: + return """\ +def my_external_function(✖argument) -> ✖int: + ... + +E113: function is missing a docstring (function-doc-missing) + +""" + + +@_templates.register +class _FNestedFuncN(_BaseTemplate): + @property + def template(self) -> str: + return ''' +def my_function(argument: int = 42) -> int: + """ + Function that prints a message and returns the argument + 1 + + Parameters + ---------- + argument : int, optional + The input argument, by default 42 + + Returns + ------- + int + The input argument + 1 + """ + print("Hello from a function") + print(argument) + def my_external_function(argument: int = 42) -> int: + print("Hello from an external function") + print(argument) + return argument + 42 + return argument + 1 +''' + + @property + def expected(self) -> str: + return """\ +def my_external_function(✖argument) -> ✖int: + ... + +E113: function is missing a docstring (function-doc-missing) + +""" + + +# starts with `M` for multi instead of `F` so we don't run +# `test_single_flag` with this as it needs `-N/--check-nested` and +# `-c/--check-class` to fail +@_templates.register +class _MNestedKlassNotMethodOkN(_BaseTemplate): + @property + def template(self) -> str: + return ''' +def my_function(argument: int = 42) -> int: + """ + Function that prints a message and returns the argument + 1 + + Parameters + ---------- + argument : int, optional + The input argument, by default 42 + + Returns + ------- + int + The input argument + 1 + """ + print("Hello from a function") + print(argument) + class Klass: + def __init__(self, this) -> None: + self.this = this + def my_external_function(self, argument: int = 42) -> int: + """This is a method. + + :param argument: An int. + :return: An int. + """ + print("Hello from an external function") + print(argument) + return argument + 42 + return argument + 1 +''' + + @property + def expected(self) -> str: + return f"""\ +{PATH}:19 in Klass +-------------------------- +class Klass: + ... + + def __init__(✖this) -> ✓None: + +E114: class is missing a docstring (class-doc-missing) + +""" + + +@_templates.register +class _MNestedKlassNotMethodNotN(_BaseTemplate): + @property + def template(self) -> str: + return ''' +def my_function(argument: int = 42) -> int: + """ + Function that prints a message and returns the argument + 1 + + Parameters + ---------- + argument : int, optional + The input argument, by default 42 + + Returns + ------- + int + The input argument + 1 + """ + print("Hello from a function") + print(argument) + class Klass: + def __init__(self, this) -> None: + self.this = this + def my_external_function(self, argument: int = 42) -> int: + print("Hello from an external function") + print(argument) + return argument + 42 + return argument + 1 +''' + + @property + def expected(self) -> str: + return f"""\ +{PATH}:19 in Klass +-------------------------- +class Klass: + ... + + def __init__(✖this) -> ✓None: + +E114: class is missing a docstring (class-doc-missing) + +{PATH}:21 in Klass +-------------------------- +def my_external_function(✖argument) -> ✖int: + ... + +E113: function is missing a docstring (function-doc-missing) + +""" diff --git a/whitelist.py b/whitelist.py index 643c5f9e..e4fd08b1 100644 --- a/whitelist.py +++ b/whitelist.py @@ -69,6 +69,10 @@ _FE112NI # unused class (tests/__init__.py) _FE112S # unused class (tests/__init__.py) _FE115NoSpaceS # unused class (tests/__init__.py) +_FFuncInForLoopIfN # unused class (tests/__init__.py) +_FFuncInForLoopN # unused class (tests/__init__.py) +_FFuncInIfInIfStatementN # unused class (tests/__init__.py) +_FFuncInIfStatementN # unused class (tests/__init__.py) _FFuncPropG # unused class (tests/__init__.py) _FFuncPropN # unused class (tests/__init__.py) _FFuncPropNI # unused class (tests/__init__.py) @@ -82,6 +86,8 @@ _FIncorrectDocS # unused class (tests/__init__.py) _FIncorrectDocSumS # unused class (tests/__init__.py) _FIssue36OffIndentN # unused class (tests/__init__.py) +_FKlassInIfStatementN # unused class (tests/__init__.py) +_FKlassNotMethodOkN # unused class (tests/__init__.py) _FKwargsOutOfOrderG # unused class (tests/__init__.py) _FKwargsOutOfOrderN # unused class (tests/__init__.py) _FKwargsOutOfOrderNI # unused class (tests/__init__.py) @@ -96,6 +102,7 @@ _FMethodWKwargsS # unused class (tests/__init__.py) _FMsgPoorIndentS # unused class (tests/__init__.py) _FMsgPoorIndentSRs # unused class (tests/__init__.py) +_FNestedFuncN # unused class (tests/__init__.py) _FNoDocClassS # unused class (tests/__init__.py) _FNoDocNoRetN # unused class (tests/__init__.py) _FNoDocNoRetS # unused class (tests/__init__.py) @@ -194,6 +201,8 @@ _MInvalidDirectiveOptions # unused class (tests/__init__.py) _MInvalidSingleDirectiveOptions # unused class (tests/__init__.py) _MInvalidSingleModuleDirectiveOptions # unused class (tests/__init__.py) +_MNestedKlassNotMethodNotN # unused class (tests/__init__.py) +_MNestedKlassNotMethodOkN # unused class (tests/__init__.py) _MPassBadInlineDirective # unused class (tests/__init__.py) _MPassBadModuleDirective # unused class (tests/__init__.py) _MPassCommentDisableModuleFirstS # unused class (tests/__init__.py)