From e0ba211ab61001a14977c28b275d2be8c5eeac81 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 25 Oct 2019 08:21:25 +0100 Subject: [PATCH 1/4] fix --- pyflakes/checker.py | 18 +++++++++++------- pyflakes/messages.py | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/pyflakes/checker.py b/pyflakes/checker.py index c8ccf569..d79dad2c 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -568,7 +568,7 @@ def __init__(self): self.returnValue = None # First non-empty return self.isGenerator = False # Detect a generator - def unusedAssignments(self): + def unusedBindings(self): """ Return a generator for the assignments which have not been used. """ @@ -576,8 +576,7 @@ def unusedAssignments(self): if (not binding.used and name != '_' and # see issue #202 name not in self.globals and - not self.usesLocals and - isinstance(binding, Assignment)): + not self.usesLocals): yield name, binding @@ -1839,13 +1838,18 @@ def runFunction(): self.handleChildren(node, omit=['decorator_list', 'returns']) - def checkUnusedAssignments(): + def checkUnusedBindings(): """ Check to see if any assignments have not been used. """ - for name, binding in self.scope.unusedAssignments(): - self.report(messages.UnusedVariable, binding.source, name) - self.deferAssignment(checkUnusedAssignments) + for name, binding in self.scope.unusedBindings(): + if isinstance(binding, Assignment): + self.report(messages.UnusedVariable, binding.source, name) + elif isinstance(binding, ClassDefinition): + self.report(messages.UnusedClass, binding.source, name) + elif isinstance(binding, FunctionDefinition): + self.report(messages.UnusedFunction, binding.source, name) + self.deferAssignment(checkUnusedBindings) if PY2: def checkReturnWithArgumentInsideGenerator(): diff --git a/pyflakes/messages.py b/pyflakes/messages.py index cf67cf8b..21639514 100644 --- a/pyflakes/messages.py +++ b/pyflakes/messages.py @@ -168,6 +168,28 @@ def __init__(self, filename, loc, names): self.message_args = (names,) +class UnusedFunction(Message): + """ + Indicates that a function has been defined but not actually used. + """ + message = 'local function %r is defined but never used' + + def __init__(self, filename, loc, names): + Message.__init__(self, filename, loc) + self.message_args = (names,) + + +class UnusedClass(Message): + """ + Indicates that a class has been defined but not actually used. + """ + message = 'local class %r is defined but never used' + + def __init__(self, filename, loc, names): + Message.__init__(self, filename, loc) + self.message_args = (names,) + + class ReturnWithArgsInsideGenerator(Message): """ Indicates a return statement with arguments inside a generator. From 675783fb94ca5827bca5cf66350781b05b784681 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 25 Oct 2019 08:21:31 +0100 Subject: [PATCH 2/4] fix existing tests --- pyflakes/test/test_imports.py | 6 ++++++ pyflakes/test/test_other.py | 1 + pyflakes/test/test_type_annotations.py | 2 ++ pyflakes/test/test_undefined_names.py | 13 ++++++++----- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pyflakes/test/test_imports.py b/pyflakes/test/test_imports.py index 13e7beff..4b97f9cd 100644 --- a/pyflakes/test/test_imports.py +++ b/pyflakes/test/test_imports.py @@ -302,6 +302,8 @@ def bar(): def baz(): def fu(): pass + return fu + return baz ''', m.RedefinedWhileUnused, m.UnusedImport) def test_redefinedInNestedFunctionTwice(self): @@ -316,6 +318,8 @@ def bar(): def baz(): def fu(): pass + return fu + return baz ''', m.RedefinedWhileUnused, m.RedefinedWhileUnused, m.UnusedImport, m.UnusedImport) @@ -729,6 +733,7 @@ def a(): def b(): fu import fu + return b ''') def test_nestedClassAndFunctionScope(self): @@ -738,6 +743,7 @@ def a(): class b: def c(self): print(fu) + return b ''') def test_importStar(self): diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py index 282accb1..d6517bd3 100644 --- a/pyflakes/test/test_other.py +++ b/pyflakes/test/test_other.py @@ -1412,6 +1412,7 @@ def barMaker(): def bar(): def baz(): return foo + return baz return bar ''') diff --git a/pyflakes/test/test_type_annotations.py b/pyflakes/test/test_type_annotations.py index 289535d2..c2b3f32e 100644 --- a/pyflakes/test/test_type_annotations.py +++ b/pyflakes/test/test_type_annotations.py @@ -182,11 +182,13 @@ class C: def f(): class C: foo: not_a_real_type + return C ''', m.UndefinedName) self.flakes(''' def f(): class C: foo: not_a_real_type = None + return C ''', m.UndefinedName) self.flakes(''' from foo import Bar diff --git a/pyflakes/test/test_undefined_names.py b/pyflakes/test/test_undefined_names.py index c952cbb6..06d8efd4 100644 --- a/pyflakes/test/test_undefined_names.py +++ b/pyflakes/test/test_undefined_names.py @@ -453,6 +453,7 @@ def test_globalFromNestedScope(self): def b(): def c(): a + return c ''') def test_laterRedefinedGlobalFromNestedScope(self): @@ -482,6 +483,7 @@ def fun2(): a a = 2 return a + return fun2 ''', m.UndefinedLocal) def test_intermediateClassScopeIgnored(self): @@ -499,7 +501,7 @@ def h(self): a = x x = None print(x, a) - print(x) + print(x, g) ''', m.UndefinedLocal) def test_doubleNestingReportsClosestName(self): @@ -518,8 +520,8 @@ def c(): x x = 3 return x - return x - return x + return x, c + return x, b ''', m.UndefinedLocal).messages[0] # _DoctestMixin.flakes adds two lines preceding the code above. @@ -539,7 +541,7 @@ def fun2(): a a = 1 return a - return a + return a, fun2 ''', m.UndefinedLocal) def test_undefinedAugmentedAssignment(self): @@ -580,7 +582,7 @@ def f(): class C: bar = foo foo = 456 - return foo + return foo, C f() ''', m.UndefinedName) @@ -678,6 +680,7 @@ def func(a: note1, *args: note2, def func(): d = e = 42 def func(a: {1, d}) -> (lambda c: e): pass + return func ''') @skipIf(version_info < (3,), 'new in Python 3') From 8b920039d4cb902e63dba86f32689456e22d1c37 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Fri, 25 Oct 2019 08:26:21 +0100 Subject: [PATCH 3/4] custom tests --- pyflakes/test/test_other.py | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py index d6517bd3..0063622d 100644 --- a/pyflakes/test/test_other.py +++ b/pyflakes/test/test_other.py @@ -1767,6 +1767,60 @@ def test_assign_expr(self): ''') +class TestUnusedFunction(TestCase): + """ + Tests for warning about unused functions. + """ + + def test_unusedFunction(self): + """ + Warn when a function inside a function is defined but never used. + """ + self.flakes(''' + def a(): + def b(): + pass + ''', m.UnusedFunction) + + def test_unusedUnderscoreFunction(self): + """ + Don't warn when the magic "_" (underscore) name is unused. + See issue #202. + """ + self.flakes(''' + def a(): + def _(): + pass + ''') + + +class TestUnusedClass(TestCase): + """ + Tests for warning about unused classes. + """ + + def test_unusedClass(self): + """ + Warn when a class inside a function is defined but never used. + """ + self.flakes(''' + def a(): + class B: + pass + ''', m.UnusedClass) + + def test_unusedUnderscoreClass(self): + """ + Don't warn when the magic "_" (underscore) name is unused. + See issue #202. + """ + self.flakes(''' + def a(): + class _: + pass + ''') + + class TestStringFormatting(TestCase): @skipIf(version_info < (3, 6), 'new in Python 3.6') From a0e4eea8119f690c0f62219fd23e949e8bcbfdf6 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 26 Oct 2019 09:39:16 +0100 Subject: [PATCH 4/4] Ignore decorated functions and classes --- pyflakes/checker.py | 10 ++++++++-- pyflakes/test/test_other.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/pyflakes/checker.py b/pyflakes/checker.py index d79dad2c..28334d59 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -1845,9 +1845,15 @@ def checkUnusedBindings(): for name, binding in self.scope.unusedBindings(): if isinstance(binding, Assignment): self.report(messages.UnusedVariable, binding.source, name) - elif isinstance(binding, ClassDefinition): + elif ( + isinstance(binding, ClassDefinition) + and not binding.source.decorator_list + ): self.report(messages.UnusedClass, binding.source, name) - elif isinstance(binding, FunctionDefinition): + elif ( + isinstance(binding, FunctionDefinition) + and not binding.source.decorator_list + ): self.report(messages.UnusedFunction, binding.source, name) self.deferAssignment(checkUnusedBindings) diff --git a/pyflakes/test/test_other.py b/pyflakes/test/test_other.py index 0063622d..3300880f 100644 --- a/pyflakes/test/test_other.py +++ b/pyflakes/test/test_other.py @@ -1793,6 +1793,20 @@ def _(): pass ''') + def test_usedDecoratedFunction(self): + """ + Don't warn when the function is decorated because decorators can do + anything, like copy it into global state. + """ + self.flakes(''' + from somewhere import decorator + + def a(): + @decorator + def b(): + pass + ''') + class TestUnusedClass(TestCase): """ @@ -1820,6 +1834,20 @@ class _: pass ''') + def test_usedDecoratedClass(self): + """ + Don't warn when the class is decorated because decorators can do + anything, like copy it into global state. + """ + self.flakes(''' + from somewhere import decorator + + def a(): + @decorator + class B: + pass + ''') + class TestStringFormatting(TestCase):