Do not count attribute docstrings for WPS226 - #3806
Merged
Merged
Conversation
PEP 258 attribute docstrings are the strings placed right after an assignment. They document that assignment, so they cannot be moved into a constant any more than a regular docstring can. Extends `is_doc_string_value()` to accept a string statement that directly follows an `ast.Assign` or `ast.AnnAssign` in the same definition body, which also covers `self.attr = ...` in `__init__`. Closes wemake-services#3805
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3806 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 374 374
Lines 12682 12708 +26
Branches 875 879 +4
=========================================
+ Hits 12682 12708 +26 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
sobolevn
reviewed
Sep 11, 2026
sobolevn
left a comment
Member
There was a problem hiding this comment.
Please, also test type-aliases, see https://discuss.python.org/t/docstrings-for-type-aliases/108901 for the context
Member
|
🤔 |
Contributor
Author
|
Sorry, that was comment for the wrong PR. 🙈 |
sobolevn
reviewed
Sep 11, 2026
PEP 695 `type` statements are documented by a string placed right after them, the same way PEP 258 documents an assignment. Pyright, Pylint and Sphinx's `autotype` already read those strings as documentation. `X: TypeAlias = int` was already covered, since it is an `ast.AnnAssign`. This adds the `type` statement and tests both spellings. See https://discuss.python.org/t/docstrings-for-type-aliases/108901 Refs wemake-services#3805
…t_overused_string.py Co-authored-by: sobolevn <mail@sobolevn.me>
…t_overused_string.py Co-authored-by: sobolevn <mail@sobolevn.me>
…t_overused_string.py Co-authored-by: sobolevn <mail@sobolevn.me>
…t_overused_string.py Co-authored-by: sobolevn <mail@sobolevn.me>
Applying the review suggestions turned the negative fixture into four local assignments, which the previous rule wrongly exempted. Local variables are not attributes, so PEP 258 does not document them. A statement can now be documented by a following string when it is a `type` alias, or a single-target assignment to a module or class attribute, or to `self.some` inside a method. That keeps `x = y = 1`, `x, y = call()` and plain locals counted. The type alias check sits behind the empty-targets branch on purpose: a dedicated `isinstance` branch would never be taken below 3.12 and would drop branch coverage under 100% on the 3.10 and 3.11 CI legs. Refs wemake-services#3805
sobolevn
reviewed
Sep 11, 2026
`compat.functions.get_assign_targets` already returns targets without knowing the assign type, so the local reimplementation was redundant. It is not a type guard, it assumes the caller knows the node assigns at all, so narrowing on `AssignNodes` has to come first. That narrowing is also what keeps `count += 1` out: `get_assign_targets` accepts `ast.AugAssign`, but an augmented assignment defines no attribute. Refs wemake-services#3805
sobolevn
reviewed
Sep 12, 2026
`x.some = 1` defines an attribute of `x`. It belongs to whatever `x` is, not to the module, class or instance the statement sits in, so a string after it documents nothing. Inside functions the target now has to be `self.some`, which `is_special_attr` already decides for the rest of the codebase. Modules and classes define their attributes by plain names, so `ast.Name` is required there. That also covers `x.some = 1` at module level and `self.some.other = 1` in a method, which had the same flaw. The one case left is a plain function whose first argument is named `self`. Telling it apart from a method needs more than this node, and `is_self` and `is_special_attr` already accept it everywhere else. Refs wemake-services#3805
sobolevn
approved these changes
Sep 12, 2026
sobolevn
left a comment
Member
There was a problem hiding this comment.
Now this seems correct, thank you :)
Member
|
Please, don't forget about new rule request for |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I have made things!
WPS226still counted PEP 258 attribute docstrings after #3804. Four class attributes sharing one:Same reasoning as #3803. The string documents the assignment above it, so moving it into a named constant would stop it from being documentation.
is_doc_string_value()accepted exactly one position,body[0]of the nearest module, class or function. It now accepts a second, and the lookup moved into a helper:AssignNodesis the existing(ast.Assign, ast.AnnAssign)tuple, sofirst = 1,second: int = 2and a barethird: intall work. I leftast.AugAssignout, sincecount += 1followed by a string isn't documenting anything.The search stays inside the context's own body.
self.third = 3in__init__is covered, because the context there is the method. A string after an assignment nested inside anifstill counts, same as before, and so does one that follows anything other than an assignment. Both have tests.One thing that looks odd in the diff:
is_doc_string_value()now ends withcontext is not None and _is_doc_string_place(...)instead of an early return. Writing it asif context is None: return Falsedrops coverage below 100%, becauseget_context()returnsNoneonly for the module node itself, and that can never be a docstring statement.AI Policy
Checklist
CHANGELOG.mdRelated issues
Closes #3805
🙏 Please, if you or your company is finding wemake-python-styleguide valuable, help us sustain the project by sponsoring it transparently on https://opencollective.com/wemake-python-styleguide. As a thank you, your profile/company logo will be added to our main README which receives hundreds of unique visitors per day.