From aafb312ed8a8b1e71386a1f92d4d6af673863592 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 4 May 2016 17:38:46 +0700 Subject: [PATCH] Fix TypeError when processing relative imports Fixes lp:1560134 aec68a784 added module names to error messages, however it caused a TypeError for relative imports that do not specify a module such as: from . import x This fixes the TypeError, and also adds the necessary leading dots for relative import error messages. Add tests for various types of relative imports. --- pyflakes/checker.py | 23 ++++++-- pyflakes/test/test_imports.py | 105 ++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 6 deletions(-) diff --git a/pyflakes/checker.py b/pyflakes/checker.py index 9545cab9..43acc69b 100644 --- a/pyflakes/checker.py +++ b/pyflakes/checker.py @@ -209,7 +209,12 @@ class ImportationFrom(Importation): def __init__(self, name, source, module, real_name=None): self.module = module self.real_name = real_name or name - full_name = module + '.' + self.real_name + + if module.endswith('.'): + full_name = module + self.real_name + else: + full_name = module + '.' + self.real_name + super(ImportationFrom, self).__init__(name, source, full_name) def __str__(self): @@ -244,7 +249,11 @@ def source_statement(self): return 'from ' + self.fullName + ' import *' def __str__(self): - return self.name + # When the module ends with a ., avoid the ambiguous '..*' + if self.fullName.endswith('.'): + return self.source_statement + else: + return self.name class FutureImportation(ImportationFrom): @@ -1142,6 +1151,8 @@ def IMPORTFROM(self, node): else: self.futuresAllowed = False + module = ('.' * node.level) + (node.module or '') + for alias in node.names: name = alias.asname or alias.name if node.module == '__future__': @@ -1153,15 +1164,15 @@ def IMPORTFROM(self, node): # Only Python 2, local import * is a SyntaxWarning if not PY2 and not isinstance(self.scope, ModuleScope): self.report(messages.ImportStarNotPermitted, - node, node.module) + node, module) continue self.scope.importStarred = True - self.report(messages.ImportStarUsed, node, node.module) - importation = StarImportation(node.module, node) + self.report(messages.ImportStarUsed, node, module) + importation = StarImportation(module, node) else: importation = ImportationFrom(name, node, - node.module, alias.name) + module, alias.name) self.addBinding(node, importation) def TRY(self, node): diff --git a/pyflakes/test/test_imports.py b/pyflakes/test/test_imports.py index 9cbd4d7c..f1bb5cde 100644 --- a/pyflakes/test/test_imports.py +++ b/pyflakes/test/test_imports.py @@ -40,6 +40,26 @@ def test_import_submodule_as_source_name(self): assert binding.source_statement == 'import a.b as a' assert str(binding) == 'a.b as a' + def test_importfrom_relative(self): + binding = ImportationFrom('a', None, '.', 'a') + assert binding.source_statement == 'from . import a' + assert str(binding) == '.a' + + def test_importfrom_relative_parent(self): + binding = ImportationFrom('a', None, '..', 'a') + assert binding.source_statement == 'from .. import a' + assert str(binding) == '..a' + + def test_importfrom_relative_with_module(self): + binding = ImportationFrom('b', None, '..a', 'b') + assert binding.source_statement == 'from ..a import b' + assert str(binding) == '..a.b' + + def test_importfrom_relative_with_module_as(self): + binding = ImportationFrom('c', None, '..a', 'b') + assert binding.source_statement == 'from ..a import b as c' + assert str(binding) == '..a.b as c' + def test_importfrom_member(self): binding = ImportationFrom('b', None, 'a', 'b') assert binding.source_statement == 'from a import b' @@ -65,6 +85,11 @@ def test_importfrom_star(self): assert binding.source_statement == 'from a.b import *' assert str(binding) == 'a.b.*' + def test_importfrom_star_relative(self): + binding = StarImportation('.b', None) + assert binding.source_statement == 'from .b import *' + assert str(binding) == '.b.*' + def test_importfrom_future(self): binding = FutureImportation('print_function', None, None) assert binding.source_statement == 'from __future__ import print_function' @@ -77,6 +102,29 @@ def test_unusedImport(self): self.flakes('import fu, bar', m.UnusedImport, m.UnusedImport) self.flakes('from baz import fu, bar', m.UnusedImport, m.UnusedImport) + def test_unusedImport_relative(self): + self.flakes('from . import fu', m.UnusedImport) + self.flakes('from . import fu as baz', m.UnusedImport) + self.flakes('from .. import fu', m.UnusedImport) + self.flakes('from ... import fu', m.UnusedImport) + self.flakes('from .. import fu as baz', m.UnusedImport) + self.flakes('from .bar import fu', m.UnusedImport) + self.flakes('from ..bar import fu', m.UnusedImport) + self.flakes('from ...bar import fu', m.UnusedImport) + self.flakes('from ...bar import fu as baz', m.UnusedImport) + + checker = self.flakes('from . import fu', m.UnusedImport) + + error = checker.messages[0] + assert error.message == '%r imported but unused' + assert error.message_args == ('.fu', ) + + checker = self.flakes('from . import fu as baz', m.UnusedImport) + + error = checker.messages[0] + assert error.message == '%r imported but unused' + assert error.message_args == ('.fu as baz', ) + def test_aliasedImport(self): self.flakes('import fu as FU, bar as FU', m.RedefinedWhileUnused, m.UnusedImport) @@ -94,6 +142,12 @@ def test_usedImport(self): self.flakes('from baz import fu; print(fu)') self.flakes('import fu; del fu') + def test_usedImport_relative(self): + self.flakes('from . import fu; assert fu') + self.flakes('from .bar import fu; assert fu') + self.flakes('from .. import fu; assert fu') + self.flakes('from ..bar import fu as baz; assert baz') + def test_redefinedWhileUnused(self): self.flakes('import fu; fu = 3', m.RedefinedWhileUnused) self.flakes('import fu; fu, bar = 3', m.RedefinedWhileUnused) @@ -687,6 +741,49 @@ def test_importStar(self): pass ''', m.ImportStarUsed, m.UnusedImport) + checker = self.flakes('from fu import *', + m.ImportStarUsed, m.UnusedImport) + + error = checker.messages[0] + assert error.message.startswith("'from %s import *' used; unable ") + assert error.message_args == ('fu', ) + + error = checker.messages[1] + assert error.message == '%r imported but unused' + assert error.message_args == ('fu.*', ) + + def test_importStar_relative(self): + """Use of import * from a relative import is reported.""" + self.flakes('from .fu import *', m.ImportStarUsed, m.UnusedImport) + self.flakes(''' + try: + from .fu import * + except: + pass + ''', m.ImportStarUsed, m.UnusedImport) + + checker = self.flakes('from .fu import *', + m.ImportStarUsed, m.UnusedImport) + + error = checker.messages[0] + assert error.message.startswith("'from %s import *' used; unable ") + assert error.message_args == ('.fu', ) + + error = checker.messages[1] + assert error.message == '%r imported but unused' + assert error.message_args == ('.fu.*', ) + + checker = self.flakes('from .. import *', + m.ImportStarUsed, m.UnusedImport) + + error = checker.messages[0] + assert error.message.startswith("'from %s import *' used; unable ") + assert error.message_args == ('..', ) + + error = checker.messages[1] + assert error.message == '%r imported but unused' + assert error.message_args == ('from .. import *', ) + @skipIf(version_info < (3,), 'import * below module level is a warning on Python 2') def test_localImportStar(self): @@ -700,6 +797,14 @@ class a: from fu import * ''', m.ImportStarNotPermitted) + checker = self.flakes(''' + class a: + from .. import * + ''', m.ImportStarNotPermitted) + error = checker.messages[0] + assert error.message == "'from %s import *' only allowed at module level" + assert error.message_args == ('..', ) + @skipIf(version_info > (3,), 'import * below module level is an error on Python 3') def test_importStarNested(self):