Patchwork [1,of,2] import-checker: allow absolute imports of sub modules from local packages

login
register
mail settings
Submitter Yuya Nishihara
Date Dec. 7, 2015, 3 p.m.
Message ID <451851fe49b160239248.1449500408@mimosa>
Download mbox | patch
Permalink /patch/11902/
State Accepted
Headers show

Comments

Yuya Nishihara - Dec. 7, 2015, 3 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1449379099 -32400
#      Sun Dec 06 14:18:19 2015 +0900
# Node ID 451851fe49b160239248661e0ef9dc907529b51e
# Parent  4d72cf769f4f6e9acd767e6b3e25569983cb3bb5
import-checker: allow absolute imports of sub modules from local packages

Before this patch, import-checker.py didn't know if a name in ImportFrom
statement are module or not. Therefore, it complained the following example
did "direct symbol import from mercurial".

  # hgext/foo.py
  from mercurial import hg

This patch reuses the dict of local modules to filter out sub-module names.

Patch

diff --git a/contrib/import-checker.py b/contrib/import-checker.py
--- a/contrib/import-checker.py
+++ b/contrib/import-checker.py
@@ -321,7 +321,7 @@  def imported_modules(source, modulename,
                 # lookup
                 yield dottedpath
 
-def verify_import_convention(module, source):
+def verify_import_convention(module, source, localmods):
     """Verify imports match our established coding convention.
 
     We have 2 conventions: legacy and modern. The modern convention is in
@@ -334,11 +334,11 @@  def verify_import_convention(module, sou
     absolute = usingabsolute(root)
 
     if absolute:
-        return verify_modern_convention(module, root)
+        return verify_modern_convention(module, root, localmods)
     else:
         return verify_stdlib_on_own_line(root)
 
-def verify_modern_convention(module, root, root_col_offset=0):
+def verify_modern_convention(module, root, localmods, root_col_offset=0):
     """Verify a file conforms to the modern import convention rules.
 
     The rules of the modern convention are:
@@ -365,6 +365,7 @@  def verify_modern_convention(module, roo
       and readability problems. See `requirealias`.
     """
     topmodule = module.split('.')[0]
+    fromlocal = fromlocalfunc(module, localmods)
 
     # Whether a local/non-stdlib import has been performed.
     seenlocal = False
@@ -380,7 +381,7 @@  def verify_modern_convention(module, roo
             return (fmt % args, node.lineno)
         if newscope:
             # Check for local imports in function
-            for r in verify_modern_convention(module, node,
+            for r in verify_modern_convention(module, node, localmods,
                                               node.col_offset + 4):
                 yield r
         elif isinstance(node, ast.Import):
@@ -443,10 +444,18 @@  def verify_modern_convention(module, roo
             # Direct symbol import is only allowed from certain modules and
             # must occur before non-symbol imports.
             if node.module and node.col_offset == root_col_offset:
-                if fullname not in allowsymbolimports:
+                found = fromlocal(node.module, node.level)
+                if found and found[2]:  # node.module is a package
+                    prefix = found[0] + '.'
+                    symbols = [n.name for n in node.names
+                               if not fromlocal(prefix + n.name)]
+                else:
+                    symbols = [n.name for n in node.names]
+
+                if symbols and fullname not in allowsymbolimports:
                     yield msg('direct symbol import from %s', fullname)
 
-                if seennonsymbolrelative:
+                if symbols and seennonsymbolrelative:
                     yield msg('symbol import follows non-symbol import: %s',
                               fullname)
 
@@ -577,7 +586,7 @@  def main(argv):
         src = f.read()
         used_imports[modname] = sorted(
             imported_modules(src, modname, localmods, ignore_nested=True))
-        for error, lineno in verify_import_convention(modname, src):
+        for error, lineno in verify_import_convention(modname, src, localmods):
             any_errors = True
             print '%s:%d: %s' % (source_path, lineno, error)
         f.close()
diff --git a/tests/test-module-imports.t b/tests/test-module-imports.t
--- a/tests/test-module-imports.t
+++ b/tests/test-module-imports.t
@@ -85,6 +85,16 @@  Run additional tests for the import chec
   >     import testpackage.subpackage.levelpriority
   > EOF
 
+  $ cat > testpackage/importmodulefromsub.py << EOF
+  > from __future__ import absolute_import
+  > from .subpackage import foo  # not a "direct symbol import"
+  > EOF
+
+  $ cat > testpackage/importsymbolfromsub.py << EOF
+  > from __future__ import absolute_import
+  > from .subpackage import foo, nonmodule
+  > EOF
+
   $ cat > testpackage/sortedentries.py << EOF
   > from __future__ import absolute_import
   > from . import (
@@ -108,6 +118,7 @@  Run additional tests for the import chec
   testpackage/importfromalias.py:2: ui from testpackage must be "as" aliased to uimod
   testpackage/importfromrelative.py:2: import should be relative: testpackage.unsorted
   testpackage/importfromrelative.py:2: direct symbol import from testpackage.unsorted
+  testpackage/importsymbolfromsub.py:2: direct symbol import from testpackage.subpackage
   testpackage/latesymbolimport.py:3: symbol import follows non-symbol import: mercurial.node
   testpackage/multiple.py:2: multiple imported names: os, sys
   testpackage/multiplegroups.py:3: multiple "from . import" statements