From 203145f9569ca915970f74e57d3b9648633b9acd Mon Sep 17 00:00:00 2001 From: Olga Pichuzhkina Date: Sat, 12 Sep 2026 10:20:27 +0300 Subject: [PATCH 1/5] Add WPS484: forbid strings that document nothing A string statement only documents something when it opens a module, class or function body, follows a module or class attribute, follows `self.some = ...` in a method, or follows a `type` alias. Anywhere else it does nothing at runtime while still reading like documentation. The check reuses `is_doc_string_value()`, the predicate WPS226 already uses to skip docstrings, so both rules share one definition of a documenting position instead of keeping two that can drift apart. Closes #3808 --- CHANGELOG.md | 5 + tests/fixtures/noqa/noqa.py | 3 + tests/test_checker/test_noqa.py | 1 + .../test_doc_string_placement.py | 151 ++++++++++++++++++ .../presets/types/tree.py | 1 + .../violations/best_practices.py | 43 +++++ .../visitors/ast/statements.py | 18 +++ 7 files changed, 222 insertions(+) create mode 100644 tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 930013323..3cdef4784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,11 @@ Semantic versioning in our case means: ## WIP +### Features + +- Adds `WPS484`: forbid strings that look like docstrings + but document nothing, #3808 + ### Bugfixes - Count overused str and bytes separately for `WPS226`, 3782 diff --git a/tests/fixtures/noqa/noqa.py b/tests/fixtures/noqa/noqa.py index 0393862d1..d37d5a83f 100644 --- a/tests/fixtures/noqa/noqa.py +++ b/tests/fixtures/noqa/noqa.py @@ -789,3 +789,6 @@ async def test_await_in_loop(): if not user in users: # noqa: WPS364 my_print('legacy not-in style') + +some_sequence.first = 1 +'Documents nothing.' # noqa: WPS484 diff --git a/tests/test_checker/test_noqa.py b/tests/test_checker/test_noqa.py index ff30b5f95..6f10be91f 100644 --- a/tests/test_checker/test_noqa.py +++ b/tests/test_checker/test_noqa.py @@ -258,6 +258,7 @@ 'WPS481': 10, 'WPS482': 0, # enabled only in python 3.15+ 'WPS483': 0, # enabled only in python 3.15+ + 'WPS484': 1, 'WPS500': 1, 'WPS501': 1, 'WPS502': 0, # disabled since 1.0.0 diff --git a/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py b/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py new file mode 100644 index 000000000..68970a155 --- /dev/null +++ b/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py @@ -0,0 +1,151 @@ +import pytest + +from wemake_python_styleguide.compat.constants import PY312 +from wemake_python_styleguide.violations.best_practices import ( + WrongDocStringPlacementViolation, +) +from wemake_python_styleguide.visitors.ast.statements import ( + DocStringPlacementVisitor, +) + +module_docstring = "'Docs.'" + +function_docstring = """ +def some(): + 'Docs.' +""" + +class_docstring = """ +class Some: + 'Docs.' +""" + +module_attribute = """ +first = 1 +'Docs.' +""" + +annotated_module_attribute = """ +first: int = 1 +'Docs.' +""" + +class_attribute = """ +class Some: + 'Class docs.' + + first = 1 + 'Docs.' +""" + +instance_attribute = """ +class Some: + def __init__(self): + 'Method docs.' + self.first = 1 + 'Docs.' +""" + +type_alias = pytest.param( + """ + type Some = int + 'Docs.' + """, + marks=pytest.mark.skipif( + not PY312, + reason='`type` aliases are only in Python 3.12+', + ), +) + +foreign_attribute = """ +some.first = 1 +'Docs.' +""" + +local_variable = """ +def some(): + 'Function docs.' + first = 1 + 'Docs.' +""" + +multiple_targets = """ +first = second = 1 +'Docs.' +""" + +inside_condition = """ +def some(arg): + 'Function docs.' + if arg: + 'Docs.' + return 1 + return 0 +""" + +after_call = """ +def some(): + 'Function docs.' + print(1) + 'Docs.' +""" + +after_doc_string = """ +def some(): + 'Function docs.' + 'Docs.' +""" + + +@pytest.mark.parametrize( + 'code', + [ + module_docstring, + function_docstring, + class_docstring, + module_attribute, + annotated_module_attribute, + class_attribute, + instance_attribute, + type_alias, + ], +) +def test_documenting_string( + assert_errors, + parse_ast_tree, + default_options, + code, +): + """Testing that strings documenting something are allowed.""" + tree = parse_ast_tree(code) + + visitor = DocStringPlacementVisitor(default_options, tree=tree) + visitor.run() + + assert_errors(visitor, []) + + +@pytest.mark.parametrize( + 'code', + [ + foreign_attribute, + local_variable, + multiple_targets, + inside_condition, + after_call, + after_doc_string, + ], +) +def test_string_documenting_nothing( + assert_errors, + parse_ast_tree, + default_options, + code, +): + """Testing that strings documenting nothing are forbidden.""" + tree = parse_ast_tree(code) + + visitor = DocStringPlacementVisitor(default_options, tree=tree) + visitor.run() + + assert_errors(visitor, [WrongDocStringPlacementViolation]) diff --git a/wemake_python_styleguide/presets/types/tree.py b/wemake_python_styleguide/presets/types/tree.py index 8cb87565d..ff44084bf 100644 --- a/wemake_python_styleguide/presets/types/tree.py +++ b/wemake_python_styleguide/presets/types/tree.py @@ -32,6 +32,7 @@ statements.WrongNamedKeywordVisitor, statements.AssignmentPatternsVisitor, statements.WrongMethodArgumentsVisitor, + statements.DocStringPlacementVisitor, keywords.WrongRaiseVisitor, keywords.WrongKeywordVisitor, keywords.WrongContextManagerVisitor, diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py index 2a316ef16..458166fc0 100644 --- a/wemake_python_styleguide/violations/best_practices.py +++ b/wemake_python_styleguide/violations/best_practices.py @@ -3117,3 +3117,46 @@ class ForbidMappingProxyTypeViolation(ASTViolation): 'Found a `types.MappingProxyType` usage, prefer `frozendict` on 3.15+' ) code = 483 + + +@final +class WrongDocStringPlacementViolation(ASTViolation): + """ + Forbid strings that look like docstrings, but document nothing. + + A string statement documents something only when it is placed: + + 1. as the first statement of a module, class, or function body + 2. after an assignment to a plain name in a module or a class + 3. after an assignment to ``self`` inside a method + 4. after a ``type`` alias on ``python3.12+`` + + Anywhere else it does nothing at runtime, + while still reading like documentation. + + Reasoning: + Such strings are dead code that looks alive. + A reader takes them for documentation of the line above, + while no tool will ever render them, + and the code they seem to describe can change without notice. + + Solution: + Move the string to a place where it documents something, + or turn it into a regular ``#`` comment. + + Example:: + + # Correct: + first = 1 + \"\"\"Documents ``first``.\"\"\" + + # Wrong: + some.first = 1 + \"\"\"Documents nothing, ``first`` belongs to ``some``.\"\"\" + + .. versionadded:: 1.9.0 + + """ + + error_template = 'Found a string that documents nothing' + code = 484 diff --git a/wemake_python_styleguide/visitors/ast/statements.py b/wemake_python_styleguide/visitors/ast/statements.py index cfa387e5f..890b76fbf 100644 --- a/wemake_python_styleguide/visitors/ast/statements.py +++ b/wemake_python_styleguide/visitors/ast/statements.py @@ -10,12 +10,14 @@ from wemake_python_styleguide.compat.nodes import TryStar from wemake_python_styleguide.logic.arguments import call_args from wemake_python_styleguide.logic.naming import name_nodes +from wemake_python_styleguide.logic.tree import strings from wemake_python_styleguide.logic.tree.collections import ( first, sequence_of_node, ) from wemake_python_styleguide.violations.best_practices import ( UnreachableCodeViolation, + WrongDocStringPlacementViolation, WrongNamedKeywordViolation, ) from wemake_python_styleguide.violations.consistency import ( @@ -358,3 +360,19 @@ def _check_tuple_arguments_types( if isinstance(arg, self._no_tuples_collections): self.add_violation(NotATupleArgumentViolation(node)) break + + +@final +class DocStringPlacementVisitor(BaseNodeVisitor): + """Restricts strings that look like docstrings, but document nothing.""" + + def visit_Expr(self, node: ast.Expr) -> None: + """Checks that a string statement documents something.""" + self._check_doc_string_place(node) + self.generic_visit(node) + + def _check_doc_string_place(self, node: ast.Expr) -> None: + if strings.is_doc_string(node) and not strings.is_doc_string_value( + node.value, + ): + self.add_violation(WrongDocStringPlacementViolation(node)) From 4ea8b94cc8f6459387cf6cf97e8318b7a4be2056 Mon Sep 17 00:00:00 2001 From: Olga Pichuzhkina Date: Sat, 12 Sep 2026 10:40:10 +0300 Subject: [PATCH 2/5] Only a constructor defines instance attributes to document `self.some = 1` in a regular method assigns to an instance that already exists, so a string after it documents nothing. Only `__init__` defines the attributes, which is what PEP 258 and Sphinx read. This narrows the shared predicate, so it moves `WPS226` as well: the same string after `self.some = 1` outside a constructor is counted again. That is the point of the two rules sharing one definition. Refs #3808 --- .../test_overuses/test_overused_string.py | 22 +++++++++++++++++++ .../test_doc_string_placement.py | 9 ++++++++ .../logic/tree/strings.py | 11 +++++++--- .../violations/best_practices.py | 2 +- .../violations/complexity.py | 4 +++- 5 files changed, 43 insertions(+), 5 deletions(-) diff --git a/tests/test_visitors/test_ast/test_complexity/test_overuses/test_overused_string.py b/tests/test_visitors/test_ast/test_complexity/test_overuses/test_overused_string.py index f6cde85e6..1bf6c807d 100644 --- a/tests/test_visitors/test_ast/test_complexity/test_overuses/test_overused_string.py +++ b/tests/test_visitors/test_ast/test_complexity/test_overuses/test_overused_string.py @@ -213,6 +213,27 @@ def fourth(): {0} """ +# Only a constructor defines instance attributes, other methods just +# assign to an object that is already made, there's nothing to document. +not_a_constructor_docstring = """ +class Some: + def first(self): + self.field = 1 + {0} + + def second(self): + self.field = 2 + {0} + + def third(self): + self.field = 3 + {0} + + def fourth(self): + self.field = 4 + {0} +""" + # `x.some = 1` defines an attribute of `x`, not of the module, the class, # or the instance we are in. So, there's nothing here to document. foreign_attribute_docstrings = """ @@ -502,6 +523,7 @@ def test_docstrings_not_counted( [ not_a_docstring, not_an_attribute_docstring, + not_a_constructor_docstring, foreign_attribute_docstrings, ], ) diff --git a/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py b/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py index 68970a155..217435359 100644 --- a/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py +++ b/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py @@ -62,6 +62,14 @@ def __init__(self): 'Docs.' """ +instance_attribute_outside_constructor = """ +class Some: + def method(self): + 'Method docs.' + self.first = 1 + 'Docs.' +""" + local_variable = """ def some(): 'Function docs.' @@ -129,6 +137,7 @@ def test_documenting_string( 'code', [ foreign_attribute, + instance_attribute_outside_constructor, local_variable, multiple_targets, inside_condition, diff --git a/wemake_python_styleguide/logic/tree/strings.py b/wemake_python_styleguide/logic/tree/strings.py index 9dad50953..f392365ab 100644 --- a/wemake_python_styleguide/logic/tree/strings.py +++ b/wemake_python_styleguide/logic/tree/strings.py @@ -4,6 +4,7 @@ from wemake_python_styleguide.compat import nodes from wemake_python_styleguide.compat.aliases import AssignNodes, FunctionNodes from wemake_python_styleguide.compat.functions import get_assign_targets +from wemake_python_styleguide.constants import INIT from wemake_python_styleguide.logic.nodes import get_context, get_parent from wemake_python_styleguide.logic.tree.attributes import is_special_attr from wemake_python_styleguide.logic.walk import get_closest_parent @@ -66,9 +67,13 @@ def _is_documented(statement: ast.stmt, context: ContextNodes) -> bool: return False target = targets[0] if isinstance(context, FunctionNodes): - # Locals are not attributes. Only `self.some = 1` defines one, - # while `x.some = 1` documents an attribute of some other object. - return isinstance(target, ast.Attribute) and is_special_attr(target) + # Only a constructor defines the instance attributes. Locals are + # not attributes, and `x.some = 1` belongs to some other object. + return ( + context.name == INIT + and isinstance(target, ast.Attribute) + and is_special_attr(target) + ) # Modules and classes define their attributes by plain names, # `x.some = 1` here belongs to `x`, not to this module or class. return isinstance(target, ast.Name) diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py index 458166fc0..c728a2000 100644 --- a/wemake_python_styleguide/violations/best_practices.py +++ b/wemake_python_styleguide/violations/best_practices.py @@ -3128,7 +3128,7 @@ class WrongDocStringPlacementViolation(ASTViolation): 1. as the first statement of a module, class, or function body 2. after an assignment to a plain name in a module or a class - 3. after an assignment to ``self`` inside a method + 3. after an assignment to ``self`` inside a constructor 4. after a ``type`` alias on ``python3.12+`` Anywhere else it does nothing at runtime, diff --git a/wemake_python_styleguide/violations/complexity.py b/wemake_python_styleguide/violations/complexity.py index 60e7eb844..f044ccddd 100644 --- a/wemake_python_styleguide/violations/complexity.py +++ b/wemake_python_styleguide/violations/complexity.py @@ -825,7 +825,9 @@ class OverusedStringViolation(MaybeASTViolation): This includes attribute docstrings from PEP 258 and type alias docstrings, the ones placed right after an attribute definition or a ``type`` statement. - Local variables are not attributes, so they are still counted. + Instance attributes are only defined in a constructor, + and local variables are not attributes at all, + so both of these are still counted. The violation points to the first occurrence of the overused string literal. From debf226e611a07be461dd035c8a7b398c65bc7a6 Mon Sep 17 00:00:00 2001 From: Olga Pichuzhkina Date: Sat, 12 Sep 2026 10:56:25 +0300 Subject: [PATCH 3/5] Update wemake_python_styleguide/violations/best_practices.py Co-authored-by: sobolevn --- wemake_python_styleguide/violations/best_practices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py index c728a2000..adbb96461 100644 --- a/wemake_python_styleguide/violations/best_practices.py +++ b/wemake_python_styleguide/violations/best_practices.py @@ -3148,7 +3148,7 @@ class WrongDocStringPlacementViolation(ASTViolation): # Correct: first = 1 - \"\"\"Documents ``first``.\"\"\" + """Documents ``first``.""" # Wrong: some.first = 1 From b05da1f50758fb05bf9dd7584fa430dbe0465314 Mon Sep 17 00:00:00 2001 From: Olga Pichuzhkina Date: Sat, 12 Sep 2026 11:07:07 +0300 Subject: [PATCH 4/5] Fix docstring syntax, support __new__, move the changelog entry Four review points plus the build: - The applied suggestion dropped the escaping on one example but kept `"""`, which closed the class docstring early and broke the parse. Both examples now use `'''`, which is what the suggestion asked for. - `__new__` defines attributes too, so both constructors are accepted. - The placement tests now run over `"Doc"`, `'Doc'`, `"""Doc"""` and `'''Doc'''`, and cover a `__new__` constructor. - Master released 1.8.1 while this branch was open, so the merge swept the entry into a released patch section. A new violation cannot ship in a patch anyway, so it starts a fresh WIP section. Refs #3808 --- CHANGELOG.md | 5 +- .../test_doc_string_placement.py | 63 ++++++++++++++----- .../logic/tree/strings.py | 7 ++- .../violations/best_practices.py | 4 +- 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 070a7af35..f1634c7bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,13 +17,16 @@ Semantic versioning in our case means: change the client facing API, change code conventions significantly, etc. -## 1.8.1 +## WIP ### Features - Adds `WPS484`: forbid strings that look like docstrings but document nothing, #3808 + +## 1.8.1 + ### Bugfixes - Count overused str and bytes separately for `WPS226`, #3782 diff --git a/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py b/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py index 217435359..0a7a531bd 100644 --- a/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py +++ b/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py @@ -8,26 +8,26 @@ DocStringPlacementVisitor, ) -module_docstring = "'Docs.'" +module_docstring = '{0}' function_docstring = """ def some(): - 'Docs.' + {0} """ class_docstring = """ class Some: - 'Docs.' + {0} """ module_attribute = """ first = 1 -'Docs.' +{0} """ annotated_module_attribute = """ first: int = 1 -'Docs.' +{0} """ class_attribute = """ @@ -35,7 +35,7 @@ class Some: 'Class docs.' first = 1 - 'Docs.' + {0} """ instance_attribute = """ @@ -43,13 +43,21 @@ class Some: def __init__(self): 'Method docs.' self.first = 1 - 'Docs.' + {0} +""" + +instance_attribute_in_new = """ +class Some: + def __new__(cls): + 'Method docs.' + cls.first = 1 + {0} """ type_alias = pytest.param( """ type Some = int - 'Docs.' + {0} """, marks=pytest.mark.skipif( not PY312, @@ -59,7 +67,7 @@ def __init__(self): foreign_attribute = """ some.first = 1 -'Docs.' +{0} """ instance_attribute_outside_constructor = """ @@ -67,26 +75,26 @@ class Some: def method(self): 'Method docs.' self.first = 1 - 'Docs.' + {0} """ local_variable = """ def some(): 'Function docs.' first = 1 - 'Docs.' + {0} """ multiple_targets = """ first = second = 1 -'Docs.' +{0} """ inside_condition = """ def some(arg): 'Function docs.' if arg: - 'Docs.' + {0} return 1 return 0 """ @@ -95,13 +103,13 @@ def some(arg): def some(): 'Function docs.' print(1) - 'Docs.' + {0} """ after_doc_string = """ def some(): 'Function docs.' - 'Docs.' + {0} """ @@ -115,17 +123,28 @@ def some(): annotated_module_attribute, class_attribute, instance_attribute, + instance_attribute_in_new, type_alias, ], ) +@pytest.mark.parametrize( + 'string_value', + [ + '"Doc"', + "'Doc'", + '"""Doc"""', + "'''Doc'''", + ], +) def test_documenting_string( assert_errors, parse_ast_tree, default_options, code, + string_value, ): """Testing that strings documenting something are allowed.""" - tree = parse_ast_tree(code) + tree = parse_ast_tree(code.format(string_value)) visitor = DocStringPlacementVisitor(default_options, tree=tree) visitor.run() @@ -145,14 +164,24 @@ def test_documenting_string( after_doc_string, ], ) +@pytest.mark.parametrize( + 'string_value', + [ + '"Doc"', + "'Doc'", + '"""Doc"""', + "'''Doc'''", + ], +) def test_string_documenting_nothing( assert_errors, parse_ast_tree, default_options, code, + string_value, ): """Testing that strings documenting nothing are forbidden.""" - tree = parse_ast_tree(code) + tree = parse_ast_tree(code.format(string_value)) visitor = DocStringPlacementVisitor(default_options, tree=tree) visitor.run() diff --git a/wemake_python_styleguide/logic/tree/strings.py b/wemake_python_styleguide/logic/tree/strings.py index f392365ab..8dd7cd4b0 100644 --- a/wemake_python_styleguide/logic/tree/strings.py +++ b/wemake_python_styleguide/logic/tree/strings.py @@ -1,15 +1,18 @@ import ast import itertools +from typing import Final from wemake_python_styleguide.compat import nodes from wemake_python_styleguide.compat.aliases import AssignNodes, FunctionNodes from wemake_python_styleguide.compat.functions import get_assign_targets -from wemake_python_styleguide.constants import INIT from wemake_python_styleguide.logic.nodes import get_context, get_parent from wemake_python_styleguide.logic.tree.attributes import is_special_attr from wemake_python_styleguide.logic.walk import get_closest_parent from wemake_python_styleguide.types import ContextNodes +#: Methods that define the instance attributes a docstring can document. +_ConstructorMethods: Final = frozenset(('__init__', '__new__')) + def is_doc_string(node: ast.AST) -> bool: """ @@ -70,7 +73,7 @@ def _is_documented(statement: ast.stmt, context: ContextNodes) -> bool: # Only a constructor defines the instance attributes. Locals are # not attributes, and `x.some = 1` belongs to some other object. return ( - context.name == INIT + context.name in _ConstructorMethods and isinstance(target, ast.Attribute) and is_special_attr(target) ) diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py index adbb96461..a3a239db3 100644 --- a/wemake_python_styleguide/violations/best_practices.py +++ b/wemake_python_styleguide/violations/best_practices.py @@ -3148,11 +3148,11 @@ class WrongDocStringPlacementViolation(ASTViolation): # Correct: first = 1 - """Documents ``first``.""" + '''Documents ``first``.''' # Wrong: some.first = 1 - \"\"\"Documents nothing, ``first`` belongs to ``some``.\"\"\" + '''Documents nothing, ``first`` belongs to ``some``.''' .. versionadded:: 1.9.0 From 663120ae3d37f224acfdbe8432e2fda804880aa9 Mon Sep 17 00:00:00 2001 From: Olga Pichuzhkina Date: Sat, 12 Sep 2026 11:14:52 +0300 Subject: [PATCH 5/5] Test dataclass attributes and docstrings placed before them Both already behaved correctly, so this is coverage plus docs, no change to the check itself. Dataclass fields are plain annotated class attributes, with or without a default, so they were already allowed. Now they say so explicitly. A string placed before an attribute is already reported, since the statement above it is not an assignment. Three shapes are covered: at module level, in a class body, and between two attributes. The example in the violation now shows this, as it is the common way to get it wrong. Refs #3808 --- .../test_doc_string_placement.py | 50 +++++++++++++++++++ .../violations/best_practices.py | 5 ++ 2 files changed, 55 insertions(+) diff --git a/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py b/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py index 0a7a531bd..f66939753 100644 --- a/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py +++ b/tests/test_visitors/test_ast/test_statements/test_doc_string_placement.py @@ -38,6 +38,24 @@ class Some: {0} """ +dataclass_attribute = """ +@dataclass +class Some: + 'Class docs.' + + first: int + {0} +""" + +dataclass_attribute_with_default = """ +@dataclass +class Some: + 'Class docs.' + + first: int = 0 + {0} +""" + instance_attribute = """ class Some: def __init__(self): @@ -70,6 +88,33 @@ def __new__(cls): {0} """ +# A docstring goes after the attribute it documents, never before it. +before_module_attribute = """ +'Module docs.' + +{0} +first = 1 +""" + +before_class_attribute = """ +class Some: + 'Class docs.' + + {0} + first: int +""" + +between_attributes = """ +class Some: + 'Class docs.' + + first = 1 + 'Documents first.' + + {0} + second = 2 +""" + instance_attribute_outside_constructor = """ class Some: def method(self): @@ -122,6 +167,8 @@ def some(): module_attribute, annotated_module_attribute, class_attribute, + dataclass_attribute, + dataclass_attribute_with_default, instance_attribute, instance_attribute_in_new, type_alias, @@ -156,6 +203,9 @@ def test_documenting_string( 'code', [ foreign_attribute, + before_module_attribute, + before_class_attribute, + between_attributes, instance_attribute_outside_constructor, local_variable, multiple_targets, diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py index a3a239db3..4a0544e9d 100644 --- a/wemake_python_styleguide/violations/best_practices.py +++ b/wemake_python_styleguide/violations/best_practices.py @@ -3143,6 +3143,7 @@ class WrongDocStringPlacementViolation(ASTViolation): Solution: Move the string to a place where it documents something, or turn it into a regular ``#`` comment. + A docstring goes after the attribute it documents, never before it. Example:: @@ -3150,6 +3151,10 @@ class WrongDocStringPlacementViolation(ASTViolation): first = 1 '''Documents ``first``.''' + # Wrong: + '''Documents nothing, a docstring goes after the attribute.''' + first = 1 + # Wrong: some.first = 1 '''Documents nothing, ``first`` belongs to ``some``.'''