Skip to content

Commit 0532189

Browse files
committed
Report each usage of star imports
Also detect unused star imports.
1 parent 4e264a1 commit 0532189

File tree

4 files changed

+82
-13
lines changed

4 files changed

+82
-13
lines changed

pyflakes/checker.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,17 @@ def redefines(self, other):
147147
return isinstance(other, Definition) and self.name == other.name
148148

149149

150+
class StarImportation(Importation):
151+
"""A binding created by an 'from x import *' statement."""
152+
153+
def __init__(self, name, source):
154+
super(StarImportation, self).__init__('*', source)
155+
# Each star importation needs a unique name, and
156+
# may not be the module name otherwise it will be deemed imported
157+
self.name = name + '.*'
158+
self.fullName = name
159+
160+
150161
class Argument(Binding):
151162
"""
152163
Represents binding a name as an argument.
@@ -358,17 +369,29 @@ def checkDeadScopes(self):
358369
if isinstance(scope, ClassScope):
359370
continue
360371

361-
if isinstance(scope.get('__all__'), ExportBinding):
362-
all_names = set(scope['__all__'].names)
372+
all_binding = scope.get('__all__')
373+
if all_binding and not isinstance(all_binding, ExportBinding):
374+
all_binding = None
375+
376+
if all_binding:
377+
all_names = set(all_binding.names)
378+
undefined = all_names.difference(scope)
379+
else:
380+
all_names = undefined = []
381+
382+
if undefined:
363383
if not scope.importStarred and \
364384
os.path.basename(self.filename) != '__init__.py':
365385
# Look for possible mistakes in the export list
366-
undefined = all_names.difference(scope)
367386
for name in undefined:
368387
self.report(messages.UndefinedExport,
369388
scope['__all__'].source, name)
370-
else:
371-
all_names = []
389+
390+
# mark all import '*' as used by the undefined in __all__
391+
if scope.importStarred:
392+
for binding in scope.values():
393+
if isinstance(binding, StarImportation):
394+
binding.used = all_binding
372395

373396
# Look for imported names that aren't used.
374397
for value in scope.values():
@@ -504,8 +527,24 @@ def handleNodeLoad(self, node):
504527
in_generators = isinstance(scope, GeneratorScope)
505528

506529
# look in the built-ins
507-
if importStarred or name in self.builtIns:
530+
if name in self.builtIns:
508531
return
532+
533+
if importStarred:
534+
from_list = []
535+
536+
for scope in self.scopeStack[-1::-1]:
537+
for binding in scope.values():
538+
if isinstance(binding, StarImportation):
539+
# mark '*' imports as used for each scope
540+
binding.used = (self.scope, node)
541+
from_list.append(binding.fullName)
542+
543+
# report * usage, with a list of possible sources
544+
from_list = ', '.join(sorted(from_list))
545+
self.report(messages.ImportStarUsage, node, name, from_list)
546+
return
547+
509548
if name == '__path__' and os.path.basename(self.filename) == '__init__.py':
510549
# the special name __path__ is valid only in packages
511550
return
@@ -976,17 +1015,19 @@ def IMPORTFROM(self, node):
9761015
self.futuresAllowed = False
9771016

9781017
for alias in node.names:
1018+
name = alias.asname or alias.name
9791019
if alias.name == '*':
9801020
# Only Python 2, local import * is a SyntaxWarning
9811021
if not PY2 and not isinstance(self.scope, ModuleScope):
9821022
self.report(messages.ImportStarNotPermitted,
9831023
node, node.module)
9841024
continue
1025+
9851026
self.scope.importStarred = True
9861027
self.report(messages.ImportStarUsed, node, node.module)
987-
continue
988-
name = alias.asname or alias.name
989-
importation = Importation(name, node)
1028+
importation = StarImportation(node.module, node)
1029+
else:
1030+
importation = Importation(name, node)
9901031
if node.module == '__future__':
9911032
importation.used = (self.scope, node)
9921033
self.addBinding(node, importation)

pyflakes/messages.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ def __init__(self, filename, loc, modname):
6565
self.message_args = (modname,)
6666

6767

68+
class ImportStarUsage(Message):
69+
message = "%s may be undefined, or defined from star imports: %s"
70+
71+
def __init__(self, filename, loc, name, from_list):
72+
Message.__init__(self, filename, loc)
73+
self.message_args = (name, from_list)
74+
75+
6876
class UndefinedName(Message):
6977
message = 'undefined name %r'
7078

pyflakes/test/test_imports.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -607,13 +607,13 @@ def c(self):
607607

608608
def test_importStar(self):
609609
"""Use of import * at module level is reported."""
610-
self.flakes('from fu import *', m.ImportStarUsed)
610+
self.flakes('from fu import *', m.ImportStarUsed, m.UnusedImport)
611611
self.flakes('''
612612
try:
613613
from fu import *
614614
except:
615615
pass
616-
''', m.ImportStarUsed)
616+
''', m.ImportStarUsed, m.UnusedImport)
617617

618618
@skipIf(version_info < (3,),
619619
'import * below module level is a warning on Python 2')
@@ -628,6 +628,17 @@ class a:
628628
from fu import *
629629
''', m.ImportStarNotPermitted)
630630

631+
@skipIf(version_info > (3,),
632+
'import * below module level is an error on Python 3')
633+
def test_importStarNested(self):
634+
"""All star imports are marked as used by an undefined variable."""
635+
self.flakes('''
636+
from fu import *
637+
def f():
638+
from bar import *
639+
x
640+
''', m.ImportStarUsed, m.ImportStarUsed, m.ImportStarUsage)
641+
631642
def test_packageImport(self):
632643
"""
633644
If a dotted name is imported and used, no warning is reported.
@@ -868,6 +879,14 @@ def test_importStarExported(self):
868879
__all__ = ["foo"]
869880
''', m.ImportStarUsed)
870881

882+
def test_importStarNotExported(self):
883+
"""Report unused import when not needed to satisfy __all__."""
884+
self.flakes('''
885+
from foolib import *
886+
a = 1
887+
__all__ = ['a']
888+
''', m.ImportStarUsed, m.UnusedImport)
889+
871890
def test_usedInGenExp(self):
872891
"""
873892
Using a global in a generator expression results in no warnings.

pyflakes/test/test_undefined_names.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ def test_magicGlobalsPath(self):
7171

7272
def test_globalImportStar(self):
7373
"""Can't find undefined names with import *."""
74-
self.flakes('from fu import *; bar', m.ImportStarUsed)
74+
self.flakes('from fu import *; bar',
75+
m.ImportStarUsed, m.ImportStarUsage)
7576

7677
@skipIf(version_info >= (3,), 'obsolete syntax')
7778
def test_localImportStar(self):
@@ -83,7 +84,7 @@ def test_localImportStar(self):
8384
def a():
8485
from fu import *
8586
bar
86-
''', m.ImportStarUsed, m.UndefinedName)
87+
''', m.ImportStarUsed, m.UndefinedName, m.UnusedImport)
8788

8889
@skipIf(version_info >= (3,), 'obsolete syntax')
8990
def test_unpackedParameter(self):

0 commit comments

Comments
 (0)