Skip to content

Commit

Permalink
3.12: add support for type params and type statements (#778)
Browse files Browse the repository at this point in the history
  • Loading branch information
asottile authored Jun 29, 2023
1 parent 0727850 commit afe2c4d
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 22 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,13 @@ jobs:

- name: Tox tests
run: tox -e py
# TODO: after flake8 6.1 include this in the main matrix
py312:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.12-dev'
- run: pip install --upgrade tox
- run: tox -e py312
83 changes: 62 additions & 21 deletions pyflakes/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,10 @@ def unused_annotations(self):
yield name, binding


class TypeScope(Scope):
pass


class GeneratorScope(Scope):
pass

Expand Down Expand Up @@ -1039,15 +1043,20 @@ def handleNodeLoad(self, node, parent):
if not name:
return

in_generators = None
# only the following can access class scoped variables (since classes
# aren't really a scope)
# - direct accesses (not within a nested scope)
# - generators
# - type annotations (for generics, etc.)
can_access_class_vars = None
importStarred = 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:
elif can_access_class_vars is False:
# only generators used in a class scope can access the
# names of the class. this is skipped during the first
# iteration
Expand Down Expand Up @@ -1082,8 +1091,10 @@ def handleNodeLoad(self, node, parent):

importStarred = importStarred or scope.importStarred

if in_generators is not False:
in_generators = isinstance(scope, GeneratorScope)
if can_access_class_vars is not False:
can_access_class_vars = isinstance(
scope, (TypeScope, GeneratorScope),
)

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

self.handleNode(parsed_annotation, node)

def handle_annotation_always_deferred(self, annotation, parent):
fn = in_annotation(Checker.handleNode)
self.deferFunction(lambda: fn(self, annotation, parent))

@in_annotation
def handleAnnotation(self, annotation, node):
if (
Expand All @@ -1326,8 +1341,7 @@ def handleAnnotation(self, annotation, node):
messages.ForwardAnnotationSyntaxError,
))
elif self.annotationsFutureEnabled:
fn = in_annotation(Checker.handleNode)
self.deferFunction(lambda: fn(self, annotation, node))
self.handle_annotation_always_deferred(annotation, node)
else:
self.handleNode(annotation, node)

Expand Down Expand Up @@ -1902,7 +1916,10 @@ def YIELD(self, node):
def FUNCTIONDEF(self, node):
for deco in node.decorator_list:
self.handleNode(deco, node)
self.LAMBDA(node)

with self._type_param_scope(node):
self.LAMBDA(node)

self.addBinding(node, FunctionDefinition(node.name, node))
# doctest does not process doctest within a doctest,
# or in nested functions.
Expand Down Expand Up @@ -1951,7 +1968,10 @@ def LAMBDA(self, node):

def runFunction():
with self.in_scope(FunctionScope):
self.handleChildren(node, omit=['decorator_list', 'returns'])
self.handleChildren(
node,
omit=('decorator_list', 'returns', 'type_params'),
)

self.deferFunction(runFunction)

Expand All @@ -1969,19 +1989,22 @@ def CLASSDEF(self, node):
"""
for deco in node.decorator_list:
self.handleNode(deco, node)
for baseNode in node.bases:
self.handleNode(baseNode, node)
for keywordNode in node.keywords:
self.handleNode(keywordNode, node)
with self.in_scope(ClassScope):
# doctest does not process doctest within a doctest
# classes within classes are processed.
if (self.withDoctest and
not self._in_doctest() and
not isinstance(self.scope, FunctionScope)):
self.deferFunction(lambda: self.handleDoctests(node))
for stmt in node.body:
self.handleNode(stmt, node)

with self._type_param_scope(node):
for baseNode in node.bases:
self.handleNode(baseNode, node)
for keywordNode in node.keywords:
self.handleNode(keywordNode, node)
with self.in_scope(ClassScope):
# doctest does not process doctest within a doctest
# classes within classes are processed.
if (self.withDoctest and
not self._in_doctest() and
not isinstance(self.scope, FunctionScope)):
self.deferFunction(lambda: self.handleDoctests(node))
for stmt in node.body:
self.handleNode(stmt, node)

self.addBinding(node, ClassDefinition(node.name, node))

def AUGASSIGN(self, node):
Expand Down Expand Up @@ -2155,3 +2178,21 @@ def _match_target(self, node):
self.handleChildren(node)

MATCHAS = MATCHMAPPING = MATCHSTAR = _match_target

@contextlib.contextmanager
def _type_param_scope(self, node):
with contextlib.ExitStack() as ctx:
if sys.version_info >= (3, 12):
ctx.enter_context(self.in_scope(TypeScope))
for param in node.type_params:
self.handleNode(param, node)
yield

def TYPEVAR(self, node):
self.handleNodeStore(node)
self.handle_annotation_always_deferred(node.bound, node)

def TYPEALIAS(self, node):
self.handleNode(node.name, node)
with self._type_param_scope(node):
self.handle_annotation_always_deferred(node.value, node)
67 changes: 67 additions & 0 deletions pyflakes/test/test_type_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,3 +713,70 @@ def f(*args: *Ts) -> None: ...
def g(x: Shape[*Ts]) -> Shape[*Ts]: ...
""")

@skipIf(version_info < (3, 12), 'new in Python 3.12')
def test_type_statements(self):
self.flakes("""
type ListOrSet[T] = list[T] | set[T]
def f(x: ListOrSet[str]) -> None: ...
type RecursiveType = int | list[RecursiveType]
type ForwardRef = int | C
type ForwardRefInBounds[T: C] = T
class C: pass
""")

@skipIf(version_info < (3, 12), 'new in Python 3.12')
def test_type_parameters_functions(self):
self.flakes("""
def f[T](t: T) -> T: return t
async def g[T](t: T) -> T: return t
def with_forward_ref[T: C](t: T) -> T: return t
def can_access_inside[T](t: T) -> T:
print(T)
return t
class C: pass
""")

@skipIf(version_info < (3, 12), 'new in Python 3.12')
def test_type_parameters_do_not_escape_function_scopes(self):
self.flakes("""
from x import g
@g(T) # not accessible in decorators
def f[T](t: T) -> T: return t
T # not accessible afterwards
""", m.UndefinedName, m.UndefinedName)

@skipIf(version_info < (3, 12), 'new in Python 3.12')
def test_type_parameters_classes(self):
self.flakes("""
class C[T](list[T]): pass
class UsesForward[T: Forward](list[T]): pass
class Forward: pass
class WithinBody[T](list[T]):
t = T
""")

@skipIf(version_info < (3, 12), 'new in Python 3.12')
def test_type_parameters_do_not_escape_class_scopes(self):
self.flakes("""
from x import g
@g(T) # not accessible in decorators
class C[T](list[T]): pass
T # not accessible afterwards
""", m.UndefinedName, m.UndefinedName)
3 changes: 2 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ setenv = PYFLAKES_ERROR_UNKNOWN=1
commands =
python --version --version
python -m unittest discover pyflakes {posargs}
flake8 pyflakes setup.py
# TODO: remove factor selection after flake8 6.1
!py312: flake8 pyflakes setup.py

[flake8]
builtins = unicode
Expand Down

0 comments on commit afe2c4d

Please sign in to comment.