Skip to content

Commit afe2c4d

Browse files
authored
3.12: add support for type params and type statements (#778)
1 parent 0727850 commit afe2c4d

File tree

4 files changed

+141
-22
lines changed

4 files changed

+141
-22
lines changed

.github/workflows/test.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,13 @@ jobs:
3333

3434
- name: Tox tests
3535
run: tox -e py
36+
# TODO: after flake8 6.1 include this in the main matrix
37+
py312:
38+
runs-on: ubuntu-latest
39+
steps:
40+
- uses: actions/checkout@v3
41+
- uses: actions/setup-python@v4
42+
with:
43+
python-version: '3.12-dev'
44+
- run: pip install --upgrade tox
45+
- run: tox -e py312

pyflakes/checker.py

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,10 @@ def unused_annotations(self):
577577
yield name, binding
578578

579579

580+
class TypeScope(Scope):
581+
pass
582+
583+
580584
class GeneratorScope(Scope):
581585
pass
582586

@@ -1039,15 +1043,20 @@ def handleNodeLoad(self, node, parent):
10391043
if not name:
10401044
return
10411045

1042-
in_generators = None
1046+
# only the following can access class scoped variables (since classes
1047+
# aren't really a scope)
1048+
# - direct accesses (not within a nested scope)
1049+
# - generators
1050+
# - type annotations (for generics, etc.)
1051+
can_access_class_vars = None
10431052
importStarred = None
10441053

10451054
# try enclosing function scopes and global scope
10461055
for scope in self.scopeStack[-1::-1]:
10471056
if isinstance(scope, ClassScope):
10481057
if name == '__class__':
10491058
return
1050-
elif in_generators is False:
1059+
elif can_access_class_vars is False:
10511060
# only generators used in a class scope can access the
10521061
# names of the class. this is skipped during the first
10531062
# iteration
@@ -1082,8 +1091,10 @@ def handleNodeLoad(self, node, parent):
10821091

10831092
importStarred = importStarred or scope.importStarred
10841093

1085-
if in_generators is not False:
1086-
in_generators = isinstance(scope, GeneratorScope)
1094+
if can_access_class_vars is not False:
1095+
can_access_class_vars = isinstance(
1096+
scope, (TypeScope, GeneratorScope),
1097+
)
10871098

10881099
if importStarred:
10891100
from_list = []
@@ -1310,6 +1321,10 @@ def handleStringAnnotation(self, s, node, ref_lineno, ref_col_offset, err):
13101321

13111322
self.handleNode(parsed_annotation, node)
13121323

1324+
def handle_annotation_always_deferred(self, annotation, parent):
1325+
fn = in_annotation(Checker.handleNode)
1326+
self.deferFunction(lambda: fn(self, annotation, parent))
1327+
13131328
@in_annotation
13141329
def handleAnnotation(self, annotation, node):
13151330
if (
@@ -1326,8 +1341,7 @@ def handleAnnotation(self, annotation, node):
13261341
messages.ForwardAnnotationSyntaxError,
13271342
))
13281343
elif self.annotationsFutureEnabled:
1329-
fn = in_annotation(Checker.handleNode)
1330-
self.deferFunction(lambda: fn(self, annotation, node))
1344+
self.handle_annotation_always_deferred(annotation, node)
13311345
else:
13321346
self.handleNode(annotation, node)
13331347

@@ -1902,7 +1916,10 @@ def YIELD(self, node):
19021916
def FUNCTIONDEF(self, node):
19031917
for deco in node.decorator_list:
19041918
self.handleNode(deco, node)
1905-
self.LAMBDA(node)
1919+
1920+
with self._type_param_scope(node):
1921+
self.LAMBDA(node)
1922+
19061923
self.addBinding(node, FunctionDefinition(node.name, node))
19071924
# doctest does not process doctest within a doctest,
19081925
# or in nested functions.
@@ -1951,7 +1968,10 @@ def LAMBDA(self, node):
19511968

19521969
def runFunction():
19531970
with self.in_scope(FunctionScope):
1954-
self.handleChildren(node, omit=['decorator_list', 'returns'])
1971+
self.handleChildren(
1972+
node,
1973+
omit=('decorator_list', 'returns', 'type_params'),
1974+
)
19551975

19561976
self.deferFunction(runFunction)
19571977

@@ -1969,19 +1989,22 @@ def CLASSDEF(self, node):
19691989
"""
19701990
for deco in node.decorator_list:
19711991
self.handleNode(deco, node)
1972-
for baseNode in node.bases:
1973-
self.handleNode(baseNode, node)
1974-
for keywordNode in node.keywords:
1975-
self.handleNode(keywordNode, node)
1976-
with self.in_scope(ClassScope):
1977-
# doctest does not process doctest within a doctest
1978-
# classes within classes are processed.
1979-
if (self.withDoctest and
1980-
not self._in_doctest() and
1981-
not isinstance(self.scope, FunctionScope)):
1982-
self.deferFunction(lambda: self.handleDoctests(node))
1983-
for stmt in node.body:
1984-
self.handleNode(stmt, node)
1992+
1993+
with self._type_param_scope(node):
1994+
for baseNode in node.bases:
1995+
self.handleNode(baseNode, node)
1996+
for keywordNode in node.keywords:
1997+
self.handleNode(keywordNode, node)
1998+
with self.in_scope(ClassScope):
1999+
# doctest does not process doctest within a doctest
2000+
# classes within classes are processed.
2001+
if (self.withDoctest and
2002+
not self._in_doctest() and
2003+
not isinstance(self.scope, FunctionScope)):
2004+
self.deferFunction(lambda: self.handleDoctests(node))
2005+
for stmt in node.body:
2006+
self.handleNode(stmt, node)
2007+
19852008
self.addBinding(node, ClassDefinition(node.name, node))
19862009

19872010
def AUGASSIGN(self, node):
@@ -2155,3 +2178,21 @@ def _match_target(self, node):
21552178
self.handleChildren(node)
21562179

21572180
MATCHAS = MATCHMAPPING = MATCHSTAR = _match_target
2181+
2182+
@contextlib.contextmanager
2183+
def _type_param_scope(self, node):
2184+
with contextlib.ExitStack() as ctx:
2185+
if sys.version_info >= (3, 12):
2186+
ctx.enter_context(self.in_scope(TypeScope))
2187+
for param in node.type_params:
2188+
self.handleNode(param, node)
2189+
yield
2190+
2191+
def TYPEVAR(self, node):
2192+
self.handleNodeStore(node)
2193+
self.handle_annotation_always_deferred(node.bound, node)
2194+
2195+
def TYPEALIAS(self, node):
2196+
self.handleNode(node.name, node)
2197+
with self._type_param_scope(node):
2198+
self.handle_annotation_always_deferred(node.value, node)

pyflakes/test/test_type_annotations.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,3 +713,70 @@ def f(*args: *Ts) -> None: ...
713713
714714
def g(x: Shape[*Ts]) -> Shape[*Ts]: ...
715715
""")
716+
717+
@skipIf(version_info < (3, 12), 'new in Python 3.12')
718+
def test_type_statements(self):
719+
self.flakes("""
720+
type ListOrSet[T] = list[T] | set[T]
721+
722+
def f(x: ListOrSet[str]) -> None: ...
723+
724+
type RecursiveType = int | list[RecursiveType]
725+
726+
type ForwardRef = int | C
727+
728+
type ForwardRefInBounds[T: C] = T
729+
730+
class C: pass
731+
""")
732+
733+
@skipIf(version_info < (3, 12), 'new in Python 3.12')
734+
def test_type_parameters_functions(self):
735+
self.flakes("""
736+
def f[T](t: T) -> T: return t
737+
738+
async def g[T](t: T) -> T: return t
739+
740+
def with_forward_ref[T: C](t: T) -> T: return t
741+
742+
def can_access_inside[T](t: T) -> T:
743+
print(T)
744+
return t
745+
746+
class C: pass
747+
""")
748+
749+
@skipIf(version_info < (3, 12), 'new in Python 3.12')
750+
def test_type_parameters_do_not_escape_function_scopes(self):
751+
self.flakes("""
752+
from x import g
753+
754+
@g(T) # not accessible in decorators
755+
def f[T](t: T) -> T: return t
756+
757+
T # not accessible afterwards
758+
""", m.UndefinedName, m.UndefinedName)
759+
760+
@skipIf(version_info < (3, 12), 'new in Python 3.12')
761+
def test_type_parameters_classes(self):
762+
self.flakes("""
763+
class C[T](list[T]): pass
764+
765+
class UsesForward[T: Forward](list[T]): pass
766+
767+
class Forward: pass
768+
769+
class WithinBody[T](list[T]):
770+
t = T
771+
""")
772+
773+
@skipIf(version_info < (3, 12), 'new in Python 3.12')
774+
def test_type_parameters_do_not_escape_class_scopes(self):
775+
self.flakes("""
776+
from x import g
777+
778+
@g(T) # not accessible in decorators
779+
class C[T](list[T]): pass
780+
781+
T # not accessible afterwards
782+
""", m.UndefinedName, m.UndefinedName)

tox.ini

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ setenv = PYFLAKES_ERROR_UNKNOWN=1
88
commands =
99
python --version --version
1010
python -m unittest discover pyflakes {posargs}
11-
flake8 pyflakes setup.py
11+
# TODO: remove factor selection after flake8 6.1
12+
!py312: flake8 pyflakes setup.py
1213

1314
[flake8]
1415
builtins = unicode

0 commit comments

Comments
 (0)