Skip to content

Do not count attribute docstrings for WPS226 - #3806

Merged
sobolevn merged 10 commits into
wemake-services:masterfrom
vyhuholl:issue-3805
Sep 12, 2026
Merged

sobolevn merged 10 commits into
wemake-services:masterfrom
vyhuholl:issue-3805

Conversation

@vyhuholl

Copy link
Copy Markdown
Contributor

I have made things!

WPS226 still counted PEP 258 attribute docstrings after #3804. Four class attributes sharing one:

attr_doc.py:3:5: WPS226 Found string literal over-use: 'Some field docs.' 4 > 3

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:

def _is_doc_string_place(body, statement) -> bool:
    """Docstrings open a definition's body or follow an assignment."""
    if body[0] is statement:
        return True
    return any(
        current is statement and isinstance(previous, AssignNodes)
        for previous, current in itertools.pairwise(body)
    )

AssignNodes is the existing (ast.Assign, ast.AnnAssign) tuple, so first = 1, second: int = 2 and a bare third: int all work. I left ast.AugAssign out, since count += 1 followed by a string isn't documenting anything.

The search stays inside the context's own body. self.third = 3 in __init__ is covered, because the context there is the method. A string after an assignment nested inside an if still 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 with context is not None and _is_doc_string_place(...) instead of an early return. Writing it as if context is None: return False drops coverage below 100%, because get_context() returns None only for the module node itself, and that can never be a docstring statement.

AI Policy

  • I have read and agree to the AI Policy, removed any "Co-Authored-By" lines attributing coding agents, and manually reviewed the final result

Checklist

  • I have double checked that there are no unrelated changes in this pull request (old patches, accidental config files, etc)
  • I have created at least one test case for the changes I have made
  • I have updated the documentation for the changes I have made
  • I have added my changes to the CHANGELOG.md

Related 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.

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

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (b841755) to head (019a1ea).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sobolevn sobolevn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, also test type-aliases, see https://discuss.python.org/t/docstrings-for-type-aliases/108901 for the context

@sobolevn

Copy link
Copy Markdown
Member

🤔

@vyhuholl

Copy link
Copy Markdown
Contributor Author

Sorry, that was comment for the wrong PR. 🙈

vyhuholl and others added 6 commits September 12, 2026 00:33
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
Comment thread wemake_python_styleguide/logic/tree/strings.py Outdated
`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
Comment thread wemake_python_styleguide/logic/tree/strings.py Outdated
`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 sobolevn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now this seems correct, thank you :)

@sobolevn

Copy link
Copy Markdown
Member

Please, don't forget about new rule request for docstring positions :)

@sobolevn
sobolevn merged commit 88e8395 into wemake-services:master Sep 12, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WPS226 counts PEP 258 attribute docstrings

2 participants