Patchwork [4,of,4] demandimport: prefer loaded module over package attribute (issue5617)

login
register
mail settings
Submitter Yuya Nishihara
Date July 16, 2017, 2:41 p.m.
Message ID <6ab37703424f873c6583.1500216111@mimosa>
Download mbox | patch
Permalink /patch/22441/
State Accepted
Headers show

Comments

Yuya Nishihara - July 16, 2017, 2:41 p.m.
# HG changeset patch
# User Yuya Nishihara <yuya@tcha.org>
# Date 1500194319 -32400
#      Sun Jul 16 17:38:39 2017 +0900
# Node ID 6ab37703424f873c658309e9dfcc0ccb311ef655
# Parent  f74cef39afa518f5b7a3e5770b9988d24f99fc24
demandimport: prefer loaded module over package attribute (issue5617)

In general, the attribute of the same name is overwritten by executing an
import statement.

  import a.b
  print(a.b.c)  # 'c' of a/b/__init__.py
  from a.b.c import d
  print(a.b.c)  # a/b/c.py

However, this appears not true for the scenario described in the test case,
and surprisingly, "from a.b.c import d" works even if "a.b.c" is not a module.

This patch works around the problem by taking the right module from sys.modules
if available.
Augie Fackler - July 17, 2017, 2:07 p.m.
On Sun, Jul 16, 2017 at 11:41:51PM +0900, Yuya Nishihara wrote:
> # HG changeset patch
> # User Yuya Nishihara <yuya@tcha.org>
> # Date 1500194319 -32400
> #      Sun Jul 16 17:38:39 2017 +0900
> # Node ID 6ab37703424f873c658309e9dfcc0ccb311ef655
> # Parent  f74cef39afa518f5b7a3e5770b9988d24f99fc24
> demandimport: prefer loaded module over package attribute (issue5617)

queued, thanks

Patch

diff --git a/hgdemandimport/demandimportpy2.py b/hgdemandimport/demandimportpy2.py
--- a/hgdemandimport/demandimportpy2.py
+++ b/hgdemandimport/demandimportpy2.py
@@ -227,10 +227,14 @@  def _demandimport(name, globals=None, lo
             # recurse down the module chain, and return the leaf module
             mod = rootmod
             for comp in modname.split('.')[1:]:
-                if getattr(mod, comp, nothing) is nothing:
-                    setattr(mod, comp, _demandmod(comp, mod.__dict__,
-                                                  mod.__dict__, level=1))
-                mod = getattr(mod, comp)
+                obj = getattr(mod, comp, nothing)
+                if obj is nothing:
+                    obj = _demandmod(comp, mod.__dict__, mod.__dict__, level=1)
+                    setattr(mod, comp, obj)
+                elif mod.__name__ + '.' + comp in sys.modules:
+                    # prefer loaded module over attribute (issue5617)
+                    obj = sys.modules[mod.__name__ + '.' + comp]
+                mod = obj
             return mod
 
         if level >= 0:
diff --git a/tests/test-extension.t b/tests/test-extension.t
--- a/tests/test-extension.t
+++ b/tests/test-extension.t
@@ -337,6 +337,23 @@  importing with "absolute_import" feature
   > from .legacy import detail as legacydetail
   > EOF
 
+Setup package that re-exports an attribute of its submodule as the same
+name. This leaves 'shadowing.used' pointing to 'used.detail', but still
+the submodule 'used' should be somehow accessible. (issue5617)
+
+  $ mkdir -p $TESTTMP/extlibroot/shadowing
+  $ cat > $TESTTMP/extlibroot/shadowing/used.py <<EOF
+  > detail = "this is extlibroot.shadowing.used"
+  > EOF
+  $ cat > $TESTTMP/extlibroot/shadowing/proxied.py <<EOF
+  > from __future__ import absolute_import
+  > from extlibroot.shadowing.used import detail
+  > EOF
+  $ cat > $TESTTMP/extlibroot/shadowing/__init__.py <<EOF
+  > from __future__ import absolute_import
+  > from .used import detail as used
+  > EOF
+
 Setup extension local modules to be imported with "absolute_import"
 feature.
 
@@ -429,6 +446,7 @@  Setup main procedure of extension.
   > from extlibroot.lsub1.lsub2 import used as lused, unused as lunused
   > from extlibroot.lsub1.lsub2.called import func as lfunc
   > from extlibroot.recursedown import absdetail, legacydetail
+  > from extlibroot.shadowing import proxied
   > 
   > def uisetup(ui):
   >     result = []
@@ -436,6 +454,7 @@  Setup main procedure of extension.
   >     result.append(lfunc())
   >     result.append(absdetail)
   >     result.append(legacydetail)
+  >     result.append(proxied.detail)
   >     ui.write('LIB: %s\n' % '\nLIB: '.join(result))
   > EOF
 
@@ -446,6 +465,7 @@  Examine module importing.
   LIB: this is extlibroot.lsub1.lsub2.called.func()
   LIB: this is extlibroot.recursedown.abs.used
   LIB: this is extlibroot.recursedown.legacy.used
+  LIB: this is extlibroot.shadowing.used
   ABS: this is absextroot.xsub1.xsub2.used
   ABS: this is absextroot.xsub1.xsub2.called.func()
 
@@ -454,6 +474,7 @@  Examine module importing.
   LIB: this is extlibroot.lsub1.lsub2.called.func()
   LIB: this is extlibroot.recursedown.abs.used
   LIB: this is extlibroot.recursedown.legacy.used
+  LIB: this is extlibroot.shadowing.used
   REL: this is absextroot.xsub1.xsub2.used
   REL: this is absextroot.xsub1.xsub2.called.func()
   REL: this relimporter imports 'this is absextroot.relimportee'