Skip to content

Commit aec68a7

Browse files
committed
Importation classes with imported name and alias
In order to solve many corner cases related to imports, more information is needed about each import. This change creates two new classes: - SubmoduleImportation - ImportationFrom And adds an optional parameter full_name to the super class Importation. Functionally, this change only improves existing error messages to report the full imported name where previously an error would include only the import alias.
1 parent cddd729 commit aec68a7

File tree

2 files changed

+183
-10
lines changed

2 files changed

+183
-10
lines changed

pyflakes/checker.py

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,83 @@ class Importation(Definition):
136136
@type fullName: C{str}
137137
"""
138138

139-
def __init__(self, name, source):
140-
self.fullName = name
139+
def __init__(self, name, source, full_name=None):
140+
self.fullName = full_name or name
141141
self.redefined = []
142-
name = name.split('.')[0]
143142
super(Importation, self).__init__(name, source)
144143

144+
def redefines(self, other):
145+
return isinstance(other, Definition) and self.name == other.name
146+
147+
def _has_alias(self):
148+
"""Return whether importation needs an as clause."""
149+
return not self.fullName.split('.')[-1] == self.name
150+
151+
@property
152+
def source_statement(self):
153+
"""Generate a source statement equivalent to the import."""
154+
if self._has_alias():
155+
return 'import %s as %s' % (self.fullName, self.name)
156+
else:
157+
return 'import %s' % self.fullName
158+
159+
def __str__(self):
160+
"""Return import full name with alias."""
161+
if self._has_alias():
162+
return self.fullName + ' as ' + self.name
163+
else:
164+
return self.fullName
165+
166+
167+
class SubmoduleImportation(Importation):
168+
169+
def __init__(self, name, source):
170+
# A dot should only appear in the name when it is a submodule import
171+
# without an 'as' clause, which is a special type of import where the
172+
# root module is implicitly imported, and the submodules are also
173+
# accessible because Python does not restrict which attributes of the
174+
# root module may be used.
175+
assert '.' in name and (not source or isinstance(source, ast.Import))
176+
package_name = name.split('.')[0]
177+
super(SubmoduleImportation, self).__init__(package_name, source)
178+
self.fullName = name
179+
145180
def redefines(self, other):
146181
if isinstance(other, Importation):
147182
return self.fullName == other.fullName
148-
return isinstance(other, Definition) and self.name == other.name
183+
return super(SubmoduleImportation, self).redefines(other)
184+
185+
def __str__(self):
186+
return self.fullName
187+
188+
@property
189+
def source_statement(self):
190+
return 'import ' + self.fullName
191+
192+
193+
class ImportationFrom(Importation):
194+
195+
def __init__(self, name, source, module, real_name=None):
196+
self.module = module
197+
self.real_name = real_name or name
198+
full_name = module + '.' + self.real_name
199+
super(ImportationFrom, self).__init__(name, source, full_name)
200+
201+
def __str__(self):
202+
"""Return import full name with alias."""
203+
if self.real_name != self.name:
204+
return self.fullName + ' as ' + self.name
205+
else:
206+
return self.fullName
207+
208+
@property
209+
def source_statement(self):
210+
if self.real_name != self.name:
211+
return 'from %s import %s as %s' % (self.module,
212+
self.real_name,
213+
self.name)
214+
else:
215+
return 'from %s import %s' % (self.module, self.name)
149216

150217

151218
class StarImportation(Importation):
@@ -158,16 +225,23 @@ def __init__(self, name, source):
158225
self.name = name + '.*'
159226
self.fullName = name
160227

228+
@property
229+
def source_statement(self):
230+
return 'from ' + self.fullName + ' import *'
161231

162-
class FutureImportation(Importation):
232+
def __str__(self):
233+
return self.name
234+
235+
236+
class FutureImportation(ImportationFrom):
163237
"""
164238
A binding created by a from `__future__` import statement.
165239
166240
`__future__` imports are implicitly used.
167241
"""
168242

169243
def __init__(self, name, source, scope):
170-
super(FutureImportation, self).__init__(name, source)
244+
super(FutureImportation, self).__init__(name, source, '__future__')
171245
self.used = (scope, source)
172246

173247

@@ -430,7 +504,7 @@ def checkDeadScopes(self):
430504
used = value.used or value.name in all_names
431505
if not used:
432506
messg = messages.UnusedImport
433-
self.report(messg, value.source, value.name)
507+
self.report(messg, value.source, str(value))
434508
for node in value.redefined:
435509
if isinstance(self.getParent(node), ast.For):
436510
messg = messages.ImportShadowedByLoopVar
@@ -1039,8 +1113,11 @@ def TUPLE(self, node):
10391113

10401114
def IMPORT(self, node):
10411115
for alias in node.names:
1042-
name = alias.asname or alias.name
1043-
importation = Importation(name, node)
1116+
if '.' in alias.name and not alias.asname:
1117+
importation = SubmoduleImportation(alias.name, node)
1118+
else:
1119+
name = alias.asname or alias.name
1120+
importation = Importation(name, node, alias.name)
10441121
self.addBinding(node, importation)
10451122

10461123
def IMPORTFROM(self, node):
@@ -1069,7 +1146,8 @@ def IMPORTFROM(self, node):
10691146
self.report(messages.ImportStarUsed, node, node.module)
10701147
importation = StarImportation(node.module, node)
10711148
else:
1072-
importation = Importation(name, node)
1149+
importation = ImportationFrom(name, node,
1150+
node.module, alias.name)
10731151
self.addBinding(node, importation)
10741152

10751153
def TRY(self, node):

pyflakes/test/test_imports.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,75 @@
22
from sys import version_info
33

44
from pyflakes import messages as m
5+
from pyflakes.checker import (
6+
FutureImportation,
7+
Importation,
8+
ImportationFrom,
9+
StarImportation,
10+
SubmoduleImportation,
11+
)
512
from pyflakes.test.harness import TestCase, skip, skipIf
613

714

15+
class TestImportationObject(TestCase):
16+
17+
def test_import_basic(self):
18+
binding = Importation('a', None, 'a')
19+
assert binding.source_statement == 'import a'
20+
assert str(binding) == 'a'
21+
22+
def test_import_as(self):
23+
binding = Importation('c', None, 'a')
24+
assert binding.source_statement == 'import a as c'
25+
assert str(binding) == 'a as c'
26+
27+
def test_import_submodule(self):
28+
binding = SubmoduleImportation('a.b', None)
29+
assert binding.source_statement == 'import a.b'
30+
assert str(binding) == 'a.b'
31+
32+
def test_import_submodule_as(self):
33+
# A submodule import with an as clause is not a SubmoduleImportation
34+
binding = Importation('c', None, 'a.b')
35+
assert binding.source_statement == 'import a.b as c'
36+
assert str(binding) == 'a.b as c'
37+
38+
def test_import_submodule_as_source_name(self):
39+
binding = Importation('a', None, 'a.b')
40+
assert binding.source_statement == 'import a.b as a'
41+
assert str(binding) == 'a.b as a'
42+
43+
def test_importfrom_member(self):
44+
binding = ImportationFrom('b', None, 'a', 'b')
45+
assert binding.source_statement == 'from a import b'
46+
assert str(binding) == 'a.b'
47+
48+
def test_importfrom_submodule_member(self):
49+
binding = ImportationFrom('c', None, 'a.b', 'c')
50+
assert binding.source_statement == 'from a.b import c'
51+
assert str(binding) == 'a.b.c'
52+
53+
def test_importfrom_member_as(self):
54+
binding = ImportationFrom('c', None, 'a', 'b')
55+
assert binding.source_statement == 'from a import b as c'
56+
assert str(binding) == 'a.b as c'
57+
58+
def test_importfrom_submodule_member_as(self):
59+
binding = ImportationFrom('d', None, 'a.b', 'c')
60+
assert binding.source_statement == 'from a.b import c as d'
61+
assert str(binding) == 'a.b.c as d'
62+
63+
def test_importfrom_star(self):
64+
binding = StarImportation('a.b', None)
65+
assert binding.source_statement == 'from a.b import *'
66+
assert str(binding) == 'a.b.*'
67+
68+
def test_importfrom_future(self):
69+
binding = FutureImportation('print_function', None, None)
70+
assert binding.source_statement == 'from __future__ import print_function'
71+
assert str(binding) == '__future__.print_function'
72+
73+
874
class Test(TestCase):
975

1076
def test_unusedImport(self):
@@ -17,6 +83,12 @@ def test_aliasedImport(self):
1783
self.flakes('from moo import fu as FU, bar as FU',
1884
m.RedefinedWhileUnused, m.UnusedImport)
1985

86+
def test_aliasedImportShadowModule(self):
87+
"""Imported aliases can shadow the source of the import."""
88+
self.flakes('from moo import fu as moo; moo')
89+
self.flakes('import fu as fu; fu')
90+
self.flakes('import fu.bar as fu; fu')
91+
2092
def test_usedImport(self):
2193
self.flakes('import fu; print(fu)')
2294
self.flakes('from baz import fu; print(fu)')
@@ -685,6 +757,29 @@ def test_differentSubmoduleImport(self):
685757
fu.bar, fu.baz
686758
''')
687759

760+
def test_used_package_with_submodule_import(self):
761+
"""
762+
Usage of package marks submodule imports as used.
763+
"""
764+
self.flakes('''
765+
import fu
766+
import fu.bar
767+
fu.x
768+
''')
769+
770+
def test_unused_package_with_submodule_import(self):
771+
"""
772+
When a package and its submodule are imported, only report once.
773+
"""
774+
checker = self.flakes('''
775+
import fu
776+
import fu.bar
777+
''', m.UnusedImport)
778+
error = checker.messages[0]
779+
assert error.message == '%r imported but unused'
780+
assert error.message_args == ('fu.bar', )
781+
assert error.lineno == 5 if self.withDoctest else 3
782+
688783
def test_assignRHSFirst(self):
689784
self.flakes('import fu; fu = fu')
690785
self.flakes('import fu; fu, bar = fu')

0 commit comments

Comments
 (0)