From 7921a6db337774d85c16c73010ca91e0ce1953f0 Mon Sep 17 00:00:00 2001 From: Yuval Raz Date: Mon, 14 Sep 2020 00:03:27 +0300 Subject: [PATCH] Prevent D103 errors when the function is decorated with @overload and add D418 (#511) * Preventing the D103 error when the function is decorated with @overload. Added an is_overload method in the function class(parser.py). Added an if statement so that the D103 error will not trigger when decorated with @overload(checker.py) Added some tests to see that it's working correctly. * Preventing the D103 error when the function is decorated with @overload. Added an is_overload method in the function class(parser.py). Added an if statement so that the D103 error will not trigger when decorated with @overload(checker.py) Added some tests to see that it's working correctly. * Added an is_overload method in the function class(parser.py). Added an if statement so that the D103 error will not trigger when decorated with @overload(checker.py) Added some tests to see that it's working correctly. * Fixing overload test. * Fixing overload test. Running isort src/pydocstyle * Added D418 Error: Function decorated with @overload shouldn\'t contain a docstring. * Overloaded functions shouldn't have a definition. * Tests for D418 error: Functions decorated with @overload * Tests for D418 error: Functions decorated with @overload * Tests for D418 error: Functions decorated with @overload * Added Tests for nested_functions/methods that are decorated with @overload checker is also preventing the 102 error in methods that are decorated with @overload. (checker.py) Any suggestions on how to write those if statements more elegantly? I really don't like the nested if statement. * Added Tests for valid overloaded functions, valid overloaded Method and overloaded Methods with D418 Error. * Added Tests for valid overloaded nested functions. * release_notes.rst updated. * release_notes.rst updated. --- docs/release_notes.rst | 13 +++ src/pydocstyle/checker.py | 34 +++++-- src/pydocstyle/parser.py | 8 ++ src/pydocstyle/violations.py | 6 ++ src/tests/test_cases/test.py | 61 ++++++++++- src/tests/test_integration.py | 184 +++++++++++++++++++++++++++++++++- 6 files changed, 295 insertions(+), 11 deletions(-) diff --git a/docs/release_notes.rst b/docs/release_notes.rst index 265bf5fc..4e555552 100644 --- a/docs/release_notes.rst +++ b/docs/release_notes.rst @@ -15,11 +15,24 @@ Major Updates New Features * Add flag to disable `# noqa` comment processing in API (#485). +* Methods, Functions and Nested functions that have a docstring now throw D418 (#511). +* Methods decorated with @overload no longer reported as D102 (#511). +* Functions and nested functions decorated with @overload no longer reported as D103 (#511). Bug Fixes * Treat "package" as an imperative verb for D401 (#356). +5.1.2 - September 13th, 2020 +---------------------------- + +New Features + +* Methods, Functions and Nested functions that have a docstring now throw D418 (#511). +* Methods decorated with @overload no longer reported as D102. +* Functions and nested functions decorated with @overload no longer reported as D103. + + 5.1.1 - August 29th, 2020 --------------------------- diff --git a/src/pydocstyle/checker.py b/src/pydocstyle/checker.py index 834ef7b8..41e3f35f 100644 --- a/src/pydocstyle/checker.py +++ b/src/pydocstyle/checker.py @@ -2,8 +2,6 @@ import ast import string -import sys -import textwrap import tokenize as tk from collections import namedtuple from itertools import chain, takewhile @@ -204,17 +202,23 @@ def check_docstring_missing(self, definition, docstring): Module: violations.D100, Class: violations.D101, NestedClass: violations.D106, - Method: ( - lambda: violations.D105() - if definition.is_magic + Method: lambda: violations.D105() + if definition.is_magic + else ( + violations.D107() + if definition.is_init else ( - violations.D107() - if definition.is_init - else violations.D102() + violations.D102() + if not definition.is_overload + else None ) ), - Function: violations.D103, NestedFunction: violations.D103, + Function: ( + lambda: violations.D103() + if not definition.is_overload + else None + ), Package: violations.D104, } return codes[type(definition)]() @@ -544,6 +548,18 @@ def check_capitalized(self, function, docstring): if first_word != first_word.capitalize(): return violations.D403(first_word.capitalize(), first_word) + @check_for(Function) + def check_if_needed(self, function, docstring): + """D418: Function decorated with @overload shouldn't contain a docstring. + + Functions that are decorated with @overload are definitions, + and are for the benefit of the type checker only, + since they will be overwritten by the non-@overload-decorated definition. + + """ + if docstring and function.is_overload: + return violations.D418() + @check_for(Definition) def check_starts_with_this(self, function, docstring): """D404: First word of the docstring should not be `This`. diff --git a/src/pydocstyle/parser.py b/src/pydocstyle/parser.py index 12c0b16d..a6f1ed0b 100644 --- a/src/pydocstyle/parser.py +++ b/src/pydocstyle/parser.py @@ -210,6 +210,14 @@ def is_public(self): else: return not self.name.startswith('_') + @property + def is_overload(self): + """Return True iff the method decorated with overload.""" + for decorator in self.decorators: + if decorator.name == "overload": + return True + return False + @property def is_test(self): """Return True if this function is a test function/method. diff --git a/src/pydocstyle/violations.py b/src/pydocstyle/violations.py index 6bf3963d..eb2b6d4c 100644 --- a/src/pydocstyle/violations.py +++ b/src/pydocstyle/violations.py @@ -411,6 +411,11 @@ def to_rst(cls) -> str: 'argument(s) {0} are missing descriptions in {1!r} docstring', ) +D418 = D4xx.create_error( + 'D418', + 'Function/ Method decorated with @overload shouldn\'t contain a docstring', +) + class AttrDict(dict): def __getattr__(self, item: str) -> Any: @@ -441,6 +446,7 @@ def __getattr__(self, item: str) -> Any: 'D415', 'D416', 'D417', + 'D418', }, 'numpy': all_errors - { diff --git a/src/tests/test_cases/test.py b/src/tests/test_cases/test.py index 1072b81d..49fd471a 100644 --- a/src/tests/test_cases/test.py +++ b/src/tests/test_cases/test.py @@ -1,8 +1,8 @@ # No docstring, so we can test D100 from functools import wraps import os -import sys from .expected import Expectation +from typing import overload expectation = Expectation() @@ -25,6 +25,23 @@ def method(self=None): def _ok_since_private(self=None): pass + @overload + def overloaded_method(self, a: int) -> str: + ... + + @overload + def overloaded_method(self, a: str) -> str: + """Foo bar documentation.""" + ... + + def overloaded_method(a): + """Foo bar documentation.""" + return str(a) + + expect('overloaded_method', + "D418: Function/ Method decorated with @overload" + " shouldn't contain a docstring") + @expect('D102: Missing docstring in public method') def __new__(self=None): pass @@ -53,6 +70,48 @@ def nested(): '' +def function_with_nesting(): + """Foo bar documentation.""" + @overload + def nested_overloaded_func(a: int) -> str: + ... + + @overload + def nested_overloaded_func(a: str) -> str: + """Foo bar documentation.""" + ... + + def nested_overloaded_func(a): + """Foo bar documentation.""" + return str(a) + + +expect('nested_overloaded_func', + "D418: Function/ Method decorated with @overload" + " shouldn't contain a docstring") + + +@overload +def overloaded_func(a: int) -> str: + ... + + +@overload +def overloaded_func(a: str) -> str: + """Foo bar documentation.""" + ... + + +def overloaded_func(a): + """Foo bar documentation.""" + return str(a) + + +expect('overloaded_func', + "D418: Function/ Method decorated with @overload" + " shouldn't contain a docstring") + + @expect('D200: One-line docstring should fit on one line with quotes ' '(found 3)') @expect('D212: Multi-line docstring summary should start at the first line') diff --git a/src/tests/test_integration.py b/src/tests/test_integration.py index 7de3b755..22f57857 100644 --- a/src/tests/test_integration.py +++ b/src/tests/test_integration.py @@ -3,7 +3,6 @@ from collections import namedtuple import os -import sys import shlex import shutil import pytest @@ -502,6 +501,189 @@ def foo(): in err) +def test_overload_function(env): + """Functions decorated with @overload trigger D418 error.""" + with env.open('example.py', 'wt') as example: + example.write(textwrap.dedent('''\ + from typing import overload + + + @overload + def overloaded_func(a: int) -> str: + ... + + + @overload + def overloaded_func(a: str) -> str: + """Foo bar documentation.""" + ... + + + def overloaded_func(a): + """Foo bar documentation.""" + return str(a) + + ''')) + env.write_config(ignore="D100") + out, err, code = env.invoke() + assert code == 1 + assert 'D418' in out + assert 'D103' not in out + + +def test_overload_method(env): + """Methods decorated with @overload trigger D418 error.""" + with env.open('example.py', 'wt') as example: + example.write(textwrap.dedent('''\ + from typing import overload + + class ClassWithMethods: + @overload + def overloaded_method(a: int) -> str: + ... + + + @overload + def overloaded_method(a: str) -> str: + """Foo bar documentation.""" + ... + + + def overloaded_method(a): + """Foo bar documentation.""" + return str(a) + + ''')) + env.write_config(ignore="D100") + out, err, code = env.invoke() + assert code == 1 + assert 'D418' in out + assert 'D102' not in out + assert 'D103' not in out + + +def test_overload_method_valid(env): + """Valid case for overload decorated Methods. + + This shouldn't throw any errors. + """ + with env.open('example.py', 'wt') as example: + example.write(textwrap.dedent('''\ + from typing import overload + + class ClassWithMethods: + """Valid docstring in public Class.""" + + @overload + def overloaded_method(a: int) -> str: + ... + + + @overload + def overloaded_method(a: str) -> str: + ... + + + def overloaded_method(a): + """Foo bar documentation.""" + return str(a) + + ''')) + env.write_config(ignore="D100, D203") + out, err, code = env.invoke() + assert code == 0 + + +def test_overload_function_valid(env): + """Valid case for overload decorated functions. + + This shouldn't throw any errors. + """ + with env.open('example.py', 'wt') as example: + example.write(textwrap.dedent('''\ + from typing import overload + + + @overload + def overloaded_func(a: int) -> str: + ... + + + @overload + def overloaded_func(a: str) -> str: + ... + + + def overloaded_func(a): + """Foo bar documentation.""" + return str(a) + + ''')) + env.write_config(ignore="D100") + out, err, code = env.invoke() + assert code == 0 + + +def test_overload_nested_function(env): + """Nested functions decorated with @overload trigger D418 error.""" + with env.open('example.py', 'wt') as example: + example.write(textwrap.dedent('''\ + from typing import overload + + def function_with_nesting(): + """Valid docstring in public function.""" + @overload + def overloaded_func(a: int) -> str: + ... + + + @overload + def overloaded_func(a: str) -> str: + """Foo bar documentation.""" + ... + + + def overloaded_func(a): + """Foo bar documentation.""" + return str(a) + ''')) + env.write_config(ignore="D100") + out, err, code = env.invoke() + assert code == 1 + assert 'D418' in out + assert 'D103' not in out + + +def test_overload_nested_function_valid(env): + """Valid case for overload decorated nested functions. + + This shouldn't throw any errors. + """ + with env.open('example.py', 'wt') as example: + example.write(textwrap.dedent('''\ + from typing import overload + + def function_with_nesting(): + """Adding a docstring to a function.""" + @overload + def overloaded_func(a: int) -> str: + ... + + + @overload + def overloaded_func(a: str) -> str: + ... + + + def overloaded_func(a): + """Foo bar documentation.""" + return str(a) + ''')) + env.write_config(ignore="D100") + out, err, code = env.invoke() + assert code == 0 + + def test_conflicting_select_ignore_config(env): """Test that select and ignore are mutually exclusive.""" env.write_config(select="D100", ignore="D101")