Skip to content

Commit 0351f3b

Browse files
authored
Merge branch 'main' into pseudo-protocols
2 parents 06fc424 + 328f342 commit 0351f3b

File tree

12 files changed

+46
-215
lines changed

12 files changed

+46
-215
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ max-complexity = 12
3030
noqa-require-code = true
3131
per-file-ignores =
3232
*.py: B905, B907, B950, E203, E501, W503, W291, W293
33-
*.pyi: B, E301, E302, E305, E501, E701, E704, W503
33+
*.pyi: B, E301, E302, E305, E501, E701, E704, F821, W503

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ repos:
2626
hooks:
2727
- id: validate-pyproject
2828
- repo: https://github.com/python-jsonschema/check-jsonschema
29-
rev: 0.33.0
29+
rev: 0.33.2
3030
hooks:
3131
- id: check-github-workflows
3232
- repo: https://github.com/rhysd/actionlint

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
## Unreleased
44

5+
* **Breaking change:** Previously, flake8-pyi monkey patched flake8's F821
6+
(undefined name) check to avoid false positives in stub files. This monkey
7+
patch has been removed. Instead, we now recommend to disable F821 when running flake8
8+
on stub files.
9+
* Remove the now unnecessary `--no-pyi-aware-file-checker` option.
10+
511
New error codes:
12+
* Y068: Don't use `@override` in stub files.
613
* Y092: Pseudo-protocols should not be used as argument types.
714

815
## 5.5.0

ERRORCODES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ The following warnings are currently emitted by default:
8080
| <a id="Y064" href="#Y064">Y064</a> | Use simpler syntax to define final literal types. For example, use `x: Final = 42` instead of `x: Final[Literal[42]]`. | Style
8181
| <a id="Y065" href="#Y065">Y065</a> | Don't use bare `Incomplete` in argument and return annotations. Instead, leave them unannotated. Omitting an annotation entirely from a function will cause some type checkers to view the parameter or return type as "untyped"; this may result in stricter type-checking on code that makes use of the stubbed function. | Style
8282
| <a id="Y066" href="#Y066">Y066</a> | When using if/else with `sys.version_info`, put the code for new Python versions first. | Style
83-
| <a id="Y067" href="#Y067">Y067</a> | Don't use `Incomplete | None = None` in
84-
argument annotations. Instead, just use `=None`. | Style
83+
| <a id="Y067" href="#Y067">Y067</a> | Don't use `Incomplete \| None = None` in argument annotations. Instead, just use `=None`. | Style
84+
| <a id="Y068" href="#Y068">Y068</a> | Don't use `@override` in stub files. Problems with a function signature deviating from its superclass are inherited from the implementation, and other tools such as stubtest are better placed to recognize deviations between stubs and the implementation. | Understanding stubs
8585

8686
## Warnings disabled by default
8787

flake8_pyi/checker.py

Lines changed: 1 addition & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,18 @@
11
from __future__ import annotations
22

3-
import argparse
43
import ast
54
import logging
65
import re
76
from dataclasses import dataclass
8-
from typing import Any, ClassVar, Iterator, Literal
7+
from typing import ClassVar, Iterator
98

10-
from flake8 import checker
119
from flake8.options.manager import OptionManager
12-
from flake8.plugins.finder import LoadedPlugin
13-
from flake8.plugins.pyflakes import FlakesChecker
14-
from pyflakes.checker import ModuleScope
1510

1611
from . import errors, visitor
1712

1813
LOG = logging.getLogger("flake8.pyi")
1914

2015

21-
class PyflakesPreProcessor(ast.NodeTransformer):
22-
"""Transform AST prior to passing it to pyflakes.
23-
24-
This reduces false positives on recursive class definitions.
25-
"""
26-
27-
def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef:
28-
self.generic_visit(node)
29-
node.bases = [
30-
# Remove the subscript to prevent F821 errors from being emitted
31-
# for (valid) recursive definitions: Foo[Bar] --> Foo
32-
base.value if isinstance(base, ast.Subscript) else base
33-
for base in node.bases
34-
]
35-
return node
36-
37-
38-
class PyiAwareFlakesChecker(FlakesChecker):
39-
def __init__(self, tree: ast.AST, *args: Any, **kwargs: Any) -> None:
40-
super().__init__(PyflakesPreProcessor().visit(tree), *args, **kwargs)
41-
42-
@property
43-
def annotationsFutureEnabled(self) -> Literal[True]:
44-
"""Always allow forward references in `.pyi` files.
45-
46-
Pyflakes can already handle forward refs for annotations,
47-
but only via `from __future__ import annotations`.
48-
In a stub file, `from __future__ import annotations` is unnecessary,
49-
so we pretend to pyflakes that it's always present when linting a `.pyi` file.
50-
"""
51-
return True
52-
53-
@annotationsFutureEnabled.setter
54-
def annotationsFutureEnabled(self, value: bool) -> None:
55-
"""Does nothing, as we always want this property to be `True`."""
56-
57-
def ASSIGN(
58-
self, tree: ast.Assign, omit: str | tuple[str, ...] | None = None
59-
) -> None:
60-
"""Defer evaluation of assignments in the module scope.
61-
62-
This is a custom implementation of ASSIGN, originally derived from
63-
handleChildren() in pyflakes 1.3.0.
64-
65-
This reduces false positives for:
66-
- TypeVars bound or constrained to forward references
67-
- Assignments to forward references that are not explicitly
68-
demarcated as type aliases.
69-
"""
70-
if not isinstance(self.scope, ModuleScope):
71-
super().ASSIGN(tree)
72-
return
73-
74-
for target in tree.targets:
75-
self.handleNode(target, tree)
76-
77-
self.deferFunction(lambda: self.handleNode(tree.value, tree))
78-
79-
def handleNodeDelete(self, node: ast.AST) -> None:
80-
"""Null implementation.
81-
82-
Lets users use `del` in stubs to denote private names.
83-
"""
84-
return
85-
86-
87-
class PyiAwareFileChecker(checker.FileChecker):
88-
def run_check(self, plugin: LoadedPlugin, **kwargs: Any) -> Any:
89-
if plugin.obj is FlakesChecker:
90-
if self.filename == "-":
91-
filename = self.options.stdin_display_name
92-
else:
93-
filename = self.filename
94-
95-
if filename.endswith(".pyi"):
96-
LOG.info(
97-
f"Replacing FlakesChecker with PyiAwareFlakesChecker while "
98-
f"checking {filename!r}"
99-
)
100-
plugin = plugin._replace(obj=PyiAwareFlakesChecker)
101-
return super().run_check(plugin, **kwargs)
102-
103-
10416
_TYPE_COMMENT_REGEX = re.compile(r"#\s*type:\s*(?!\s?ignore)([^#]+)(\s*#.*?)?$")
10517

10618

@@ -136,18 +48,5 @@ def run(self) -> Iterator[errors.Error]:
13648

13749
@staticmethod
13850
def add_options(parser: OptionManager) -> None:
139-
"""This is brittle, there's multiple levels of caching of defaults."""
14051
parser.parser.set_defaults(filename="*.py,*.pyi")
14152
parser.extend_default_ignore(errors.DISABLED_BY_DEFAULT)
142-
parser.add_option(
143-
"--no-pyi-aware-file-checker",
144-
default=False,
145-
action="store_true",
146-
parse_from_config=True,
147-
help="don't patch flake8 with .pyi-aware file checker",
148-
)
149-
150-
@staticmethod
151-
def parse_options(options: argparse.Namespace) -> None:
152-
if not options.no_pyi_aware_file_checker:
153-
checker.FileChecker = PyiAwareFileChecker

flake8_pyi/errors.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ class Error(NamedTuple):
127127
'put the code for new Python versions first, e.g. "{new_syntax}"'
128128
)
129129
Y067 = 'Y067 Use "=None" instead of "Incomplete | None = None"'
130+
Y068 = 'Y068 Do not use "@override" in stub files.'
131+
130132
Y090 = (
131133
'Y090 "{original}" means '
132134
'"a tuple of length 1, in which the sole element is of type {typ!r}". '

flake8_pyi/visitor.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ def _is_object(node: ast.AST | None, name: str, *, from_: Container[str]) -> boo
235235
)
236236
_is_Generic = partial(_is_object, name="Generic", from_=_TYPING_MODULES)
237237
_is_Unpack = partial(_is_object, name="Unpack", from_=_TYPING_MODULES)
238+
_is_override = partial(_is_object, name="override", from_=_TYPING_MODULES)
238239

239240

240241
def _is_union(node: ast.expr | None) -> TypeIs[ast.BinOp]:
@@ -2044,6 +2045,12 @@ def check_protocol_param_kinds(
20442045
pos_or_kw, errors.Y091.format(arg=pos_or_kw.arg, method=node.name)
20452046
)
20462047

2048+
def check_for_override(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
2049+
for deco in node.decorator_list:
2050+
if _is_override(deco):
2051+
self.error(deco, errors.Y068)
2052+
return
2053+
20472054
@staticmethod
20482055
def _is_positional_pre_570_argname(name: str) -> bool:
20492056
# https://peps.python.org/pep-0484/#positional-only-arguments
@@ -2106,6 +2113,7 @@ def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
21062113
self.check_self_typevars(node, decorator_names)
21072114
if self.enclosing_class_ctx.is_protocol_class:
21082115
self.check_protocol_param_kinds(node, decorator_names)
2116+
self.check_for_override(node)
21092117

21102118
def visit_arg(self, node: ast.arg) -> None:
21112119
if _is_NoReturn(node.annotation):

tests/forward_refs.pyi

Lines changed: 0 additions & 40 deletions
This file was deleted.

tests/forward_refs_annassign.pyi

Lines changed: 0 additions & 56 deletions
This file was deleted.

tests/override.pyi

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import typing
2+
import typing as t
3+
from typing import override, override as over
4+
5+
import typing_extensions
6+
7+
class Foo:
8+
def f(self) -> None: ...
9+
def g(self) -> None: ...
10+
def h(self) -> None: ...
11+
def j(self) -> None: ...
12+
def k(self) -> None: ...
13+
14+
class Bar(Foo):
15+
@override # Y068 Do not use "@override" in stub files.
16+
def f(self) -> None: ...
17+
@typing.override # Y068 Do not use "@override" in stub files.
18+
def g(self) -> None: ...
19+
@t.override # Ideally we'd catch this too, but the infrastructure is not in place.
20+
def h(self) -> None: ...
21+
@over # Ideally we'd catch this too, but the infrastructure is not in place.
22+
def j(self) -> None: ...
23+
@typing_extensions.override # Y068 Do not use "@override" in stub files.
24+
def k(self) -> None: ...

0 commit comments

Comments
 (0)