diff --git a/pyflakes/checker.py b/pyflakes/checker.py index 56fc3ca4..89c9d0a4 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -588,7 +588,7 @@ def __init__(self): self.returnValue = None # First non-empty return self.isGenerator = False # Detect a generator - def unusedAssignments(self): + def unused_assignments(self): """ Return a generator for the assignments which have not been used. """ @@ -600,6 +600,14 @@ def unusedAssignments(self): isinstance(binding, Assignment)): yield name, binding + def unused_annotations(self): + """ + Return a generator for the annotations which have not been used. + """ + for name, binding in self.items(): + if not binding.used and isinstance(binding, Annotation): + yield name, binding + class GeneratorScope(Scope): pass @@ -1156,6 +1164,7 @@ def handleNodeLoad(self, node): 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): @@ -2084,13 +2093,22 @@ def runFunction(): self.handleChildren(node, omit=['decorator_list', 'returns']) - def checkUnusedAssignments(): + def check_unused_assignments(): """ Check to see if any assignments have not been used. """ - for name, binding in self.scope.unusedAssignments(): + for name, binding in self.scope.unused_assignments(): self.report(messages.UnusedVariable, binding.source, name) - self.deferAssignment(checkUnusedAssignments) + + def check_unused_annotations(): + """ + Check to see if any annotations have not been used. + """ + for name, binding in self.scope.unused_annotations(): + self.report(messages.UnusedAnnotation, binding.source, name) + + self.deferAssignment(check_unused_assignments) + self.deferAssignment(check_unused_annotations) self.popScope() diff --git a/pyflakes/messages.py b/pyflakes/messages.py index 37c4432f..c2246cf4 100644 --- a/pyflakes/messages.py +++ b/pyflakes/messages.py @@ -156,6 +156,18 @@ def __init__(self, filename, loc, names): self.message_args = (names,) +class UnusedAnnotation(Message): + """ + Indicates that a variable has been explicitly annotated to but not actually + used. + """ + message = 'local variable %r is annotated but never used' + + def __init__(self, filename, loc, names): + Message.__init__(self, filename, loc) + self.message_args = (names,) + + class ReturnOutsideFunction(Message): """ Indicates a return statement outside of a function/method. diff --git a/pyflakes/test/test_type_annotations.py b/pyflakes/test/test_type_annotations.py index d881205a..2ad9f45d 100644 --- a/pyflakes/test/test_type_annotations.py +++ b/pyflakes/test/test_type_annotations.py @@ -174,7 +174,7 @@ class C: def f(): name: str age: int - ''') + ''', m.UnusedAnnotation, m.UnusedAnnotation) self.flakes(''' def f(): name: str = 'Bob' @@ -190,7 +190,7 @@ def f(): from typing import Any def f(): a: Any - ''') + ''', m.UnusedAnnotation) self.flakes(''' foo: not_a_real_type ''', m.UndefinedName) @@ -356,11 +356,10 @@ def test_unused_annotation(self): class Cls: y: int ''') - # TODO: this should print a UnusedVariable message self.flakes(''' def f(): x: int - ''') + ''', m.UnusedAnnotation) # This should only print one UnusedVariable message self.flakes(''' def f(): @@ -368,6 +367,12 @@ def f(): x = 3 ''', m.UnusedVariable) + def test_unassigned_annotation_is_undefined(self): + self.flakes(''' + name: str + print(name) + ''', m.UndefinedName) + def test_annotated_async_def(self): self.flakes(''' class c: pass