/
check.py
189 lines (171 loc) · 7.35 KB
/
check.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
from __future__ import annotations
import ast
import os
from ast import AST
from functools import cache
from typing import Type
import pyflakes
from pyflakes import checker
from pyflakes.checker import (
Annotation,
Builtin,
ClassScope,
DetectClassScopedMagic,
GeneratorScope,
Importation,
StarImportation,
getNodeName,
)
from pyflakes.messages import UnusedImport
from ..error_handling.failure import FailableMixIn
__all__ = ["Checker"]
class Checker(FailableMixIn, checker.Checker):
verbose: bool
escalate: bool
target_cls: bool
target_func: bool
funcdefs: list[AST]
classdefs: list[AST]
alldefs: list[AST]
imports: list[tuple[AST, Importation | Type[Importation]]]
import_uses: dict[str, list[tuple[AST, Importation | Type[Importation]]]]
def __init__(self, *args, **kwargs):
self.code = kwargs.pop("code")
self.verbose = kwargs.pop("verbose", False)
self.escalate = kwargs.pop("escalate", False)
self.target_cls = kwargs.pop("cls_defs", False)
self.target_func = kwargs.pop("func_defs", False)
self.funcdefs = []
self.classdefs = []
self.alldefs = []
self.imports = []
self.import_uses = {}
super().__init__(*args, **kwargs)
@property
def target_all(self) -> bool:
return not (self.target_cls or self.target_func)
@property
def target_defs(self) -> list[AST]:
"""Expand to classdefs or either in future"""
if self.target_all:
return self.alldefs
else:
return self.classdefs if self.target_cls else self.funcdefs
def handleNode(self, node: AST | None, parent: AST) -> None:
"""Subclass override"""
super().handleNode(node=node, parent=parent)
if node is not None:
setattr(node, "depth", self.get_ancestors(node, count=True))
def CLASSDEF(self, node: AST) -> None:
"""Subclass override"""
super().CLASSDEF(node=node)
self.classdefs.append(node)
self.alldefs.append(node)
def FUNCTIONDEF(self, node: AST) -> None:
"""Subclass override"""
super().FUNCTIONDEF(node=node)
self.funcdefs.append(node)
self.alldefs.append(node)
def addBinding(self, node: AST, value) -> None:
super().addBinding(node=node, value=value)
if isinstance(value, checker.Importation):
self.imports.append(value)
def describe_node(self, node: AST) -> None:
line_range = f"{node.lineno}-{node.end_lineno}"
info = f"{type(node).__name__} {node.name!r} ({line_range=})"
if self.verbose:
print(f"{info} depth={node.depth} -> {node.returns.id}")
def get_ancestors(self, node: AST, count: bool = False) -> int | list[AST]:
ancestors = []
while parent := self.getParent(node):
if count and hasattr(node, "depth"):
return node.depth + len(ancestors)
ancestors.append(parent)
if parent is self.root:
return len(ancestors) if count else ancestors
node = parent
@cache
def unused_imports(self) -> list[UnusedImport]:
"""
Import strings (in `m.message_args[0]` for each `UnusedImport` message `m`):
- "import a( as o)" -> "a( as o)"
- "from a import b( as o)" -> "a.b( as o)"
- "from a.b import c( as o)" -> "a.b.c( as o)"
- "from . import a( as o)" -> ".a( as o)"
- "from .a import b( as o)" -> ".a.b( as o)"
- "from .a.b import c( as o)" -> ".a.b.c( as o)"
"""
imps = [m for m in self.messages if isinstance(m, UnusedImport)]
return imps
def handleNodeLoad(self, node):
used_set = [] # Note: not used, but would help if re-assignment is an issue
# https://github.com/PyCQA/pyflakes/blob/
# 853cce91634cbddff01cc16313b5467be1e95c54/pyflakes/checker.py#L1073-L1094
# ------------- start of copied part (L1073-L1094)
if not (name := getNodeName(node)):
return
in_generators, importStarred = None, None
# try enclosing function scopes and global scope
for scope in self.scopeStack[-1::-1]:
if isinstance(scope, ClassScope):
if name == "__class__":
return
elif in_generators is False:
# only generators used in a class scope can access the names of the
# class. this is skipped during the first iteration
continue
binding = scope.get(name, None)
if isinstance(binding, Annotation) and not self._in_postponed_annotation:
scope[name].used = True
continue
if name == "print" and isinstance(binding, Builtin):
parent = self.getParent(node)
if isinstance(parent, ast.BinOp) and isinstance(parent.op, ast.RShift):
self.report(pyflakes.messages.InvalidPrintSyntax, node)
try:
scope[name].used = (self.scope, node)
# if the name of SubImportation is same as alias of other Importation
# and the alias is used, SubImportation also should be marked as used.
n = scope[name]
self.import_uses.setdefault(name, [])
self.import_uses[name].append((self.scope, node))
used_set.append(name)
if isinstance(n, Importation) and n._has_alias():
try:
scope[n.fullName].used = (self.scope, node)
self.import_uses.setdefault(n.fullName, [])
self.import_uses[n.fullName].append((self.scope, node))
used_set.append(n.fullName)
except KeyError:
pass
except KeyError:
pass
else:
return
importStarred = importStarred or scope.importStarred
if in_generators is not False:
in_generators = isinstance(scope, GeneratorScope)
if importStarred:
from_list = []
for scope in self.scopeStack[-1::-1]:
for binding in scope.values():
if isinstance(binding, StarImportation):
# mark '*' imports as used for each scope
binding.used = (self.scope, node)
self.import_uses.setdefault(binding.fullName, [])
self.import_uses[binding.fullName].append((self.scope, node))
used_set.append(binding.fullName)
from_list.append(binding.fullName)
# report * usage, with a list of possible sources
from_list = ", ".join(sorted(from_list))
self.report(pyflakes.messages.ImportStarUsage, node, name, from_list)
return
if name == "__path__" and os.path.basename(self.filename) == "__init__.py":
# the special name __path__ is valid only in packages
return
if name in DetectClassScopedMagic.names and isinstance(self.scope, ClassScope):
return
# protected with a NameError handler?
if "NameError" not in self.exceptHandlers[-1]:
self.report(pyflakes.messages.UndefinedName, node, name)
# super().handleNodeLoad(node)