Skip to content

Commit e1a556c

Browse files
authored
Use qualified module name from import statement during importing (#1079)
* Use qualified module name from import statement during importing * Add tests from gh936
1 parent 9322b72 commit e1a556c

7 files changed

Lines changed: 139 additions & 15 deletions

File tree

Src/IronPython/Runtime/Importer.cs

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ public static object ImportFrom(CodeContext/*!*/ context, object from, string na
8686

8787
if (scope.__dict__._storage.TryGetPath(out object path)) {
8888
if (path is PythonList listPath) {
89-
return ImportNestedModule(context, scope, new[] { name }, 0, listPath);
89+
return ImportNestedModule(context, scope, new ArraySegment<string>(new[] { name }), listPath, scope.GetName());
9090
}
9191
if (path is string stringPath) {
92-
return ImportNestedModule(context, scope, new[] { name }, 0, PythonList.FromArrayNoCopy(stringPath));
92+
return ImportNestedModule(context, scope, new ArraySegment<string>(new[] { name }), PythonList.FromArrayNoCopy(stringPath), scope.GetName());
9393
}
9494
}
9595
} else if (from is PythonType pt) {
@@ -112,25 +112,26 @@ public static object ImportFrom(CodeContext/*!*/ context, object from, string na
112112
throw PythonOps.ImportError("Cannot import name {0}", name);
113113
}
114114

115-
private static object ImportModuleFrom(CodeContext/*!*/ context, object from, string[] parts, int current) {
115+
private static object ImportModuleFrom(CodeContext/*!*/ context, object from, ArraySegment<string> parts, object root) {
116116
if (from is PythonModule scope) {
117117
if (scope.__dict__._storage.TryGetPath(out object path) || DynamicHelpers.GetPythonType(scope).TryGetMember(context, scope, "__path__", out path)) {
118118
if (path is PythonList listPath) {
119-
return ImportNestedModule(context, scope, parts, current, listPath);
119+
return ImportNestedModule(context, scope, parts, listPath, (root as PythonModule)?.GetName());
120120
}
121121
if (path is string stringPath) {
122-
return ImportNestedModule(context, scope, parts, current, PythonList.FromArrayNoCopy(stringPath));
122+
return ImportNestedModule(context, scope, parts, PythonList.FromArrayNoCopy(stringPath), (root as PythonModule)?.GetName());
123123
}
124124
}
125125
}
126126

127+
string name = parts.Array[parts.Offset + parts.Count - 1];
127128
if (from is NamespaceTracker ns) {
128-
if (ns.TryGetValue(parts[current], out object val)) {
129+
if (ns.TryGetValue(name, out object val)) {
129130
return MemberTrackerToPython(context, val);
130131
}
131132
}
132133

133-
throw PythonOps.ImportError("No module named {0}", parts[current]);
134+
throw PythonOps.ImportError("No module named {0}", name);
134135
}
135136

136137
/// <summary>
@@ -262,7 +263,7 @@ public static object ImportModule(CodeContext/*!*/ context, object globals, stri
262263
}
263264
} else if (i != 0) {
264265
// child module isn't loaded yet, import it.
265-
next = ImportModuleFrom(context, next, parts, i);
266+
next = ImportModuleFrom(context, next, new ArraySegment<string>(parts, 1, i), newmod);
266267
} else {
267268
// top-level module doesn't exist in sys.modules, probably
268269
// came from some weird meta path hook.
@@ -603,17 +604,21 @@ private static bool TryGetNestedModule(CodeContext/*!*/ context, PythonModule/*!
603604
}
604605

605606
private static object ImportNestedModule(CodeContext/*!*/ context, PythonModule/*!*/ module,
606-
string[] parts, int current, PythonList/*!*/ path) {
607+
ArraySegment<string> parts, PythonList/*!*/ path, string scopeModuleName) {
608+
Debug.Assert(parts.Array is not null);
609+
Debug.Assert(parts.Count > 0);
610+
607611
object ret;
608-
string name = parts[current];
609-
string fullName = CreateFullName(module.GetName() as string, name);
612+
int current = parts.Offset + parts.Count - 1;
613+
string name = parts.Array[current];
614+
string fullName = CreateFullName(scopeModuleName, parts);
610615

611616
if (TryGetExistingOrMetaPathModule(context, fullName, path, out ret)) {
612617
module.__dict__[name] = ret;
613618
return ret;
614619
}
615620

616-
if (TryGetNestedModule(context, module, parts, current, out ret)) {
621+
if (TryGetNestedModule(context, module, parts.Array, current, out ret)) {
617622
return ret;
618623
}
619624

@@ -762,11 +767,11 @@ private static bool IsReflected(object module) {
762767
|| module is BuiltinFunction;
763768
}
764769

765-
private static string CreateFullName(string/*!*/ baseName, string name) {
770+
private static string CreateFullName(string/*!*/ baseName, ArraySegment<string> parts) {
766771
if (baseName == null || baseName.Length == 0 || baseName == "__main__") {
767-
return name;
772+
return string.Join(".", parts);
768773
}
769-
return baseName + "." + name;
774+
return baseName + "." + string.Join(".", parts);
770775
}
771776

772777
#endregion

Tests/import_extant/_vendor/__init__.py

Whitespace-only changes.

Tests/import_extant/_vendor/packaging/__init__.py

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Infinity = None
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from ._structures import Infinity
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import sys
2+
3+
class VendorImporter:
4+
"""
5+
A PEP 302 meta path importer for finding optionally-vendored
6+
or otherwise naturally-installed packages from root_name.
7+
"""
8+
9+
def __init__(self, root_name, vendored_names=(), vendor_pkg=None):
10+
self.root_name = root_name
11+
self.vendored_names = set(vendored_names)
12+
self.vendor_pkg = vendor_pkg or root_name.replace('extern', '_vendor')
13+
14+
@property
15+
def search_path(self):
16+
"""
17+
Search first the vendor package then as a natural package.
18+
"""
19+
yield self.vendor_pkg + '.'
20+
yield ''
21+
22+
def find_module(self, fullname, path=None):
23+
"""
24+
Return self when fullname starts with root_name and the
25+
target module is one vendored through this importer.
26+
"""
27+
root, base, target = fullname.partition(self.root_name + '.')
28+
if root:
29+
return
30+
if not any(map(target.startswith, self.vendored_names)):
31+
return
32+
return self
33+
34+
def load_module(self, fullname):
35+
"""
36+
Iterate over the search path to locate and load fullname.
37+
"""
38+
root, base, target = fullname.partition(self.root_name + '.')
39+
for prefix in self.search_path:
40+
try:
41+
extant = prefix + target
42+
__import__(extant)
43+
mod = sys.modules[extant]
44+
sys.modules[fullname] = mod
45+
# mysterious hack:
46+
# Remove the reference to the extant package/module
47+
# on later Python versions to cause relative imports
48+
# in the vendor package to resolve the same modules
49+
# as those going through this importer.
50+
if sys.version_info > (3, 3):
51+
del sys.modules[extant]
52+
return mod
53+
except ImportError:
54+
pass
55+
else:
56+
raise ImportError(
57+
"The '{target}' package is required; "
58+
"normally this is bundled with this package so if you get "
59+
"this warning, consult the packager of your "
60+
"distribution.".format(**locals())
61+
)
62+
63+
def install(self):
64+
"""
65+
Install this importer into sys.meta_path if not already present.
66+
"""
67+
if self not in sys.meta_path:
68+
sys.meta_path.append(self)
69+
70+
71+
names = 'packaging',
72+
VendorImporter(__name__, names).install()

Tests/test_importext.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Licensed to the .NET Foundation under one or more agreements.
2+
# The .NET Foundation licenses this file to you under the Apache 2.0 License.
3+
# See the LICENSE file in the project root for more information.
4+
5+
import os
6+
import sys
7+
import unittest
8+
9+
from iptest import IronPythonTestCase, run_test
10+
11+
12+
class ImportExtant(IronPythonTestCase):
13+
def test_gh936(self):
14+
# https://github.com/IronLanguages/ironpython3/issues/936
15+
16+
extra_import_path = os.path.join(self.test_dir, "import_extant")
17+
sys.path.insert(0, extra_import_path)
18+
try:
19+
m = __import__('extern.packaging.version')
20+
self.assertEqual(m.__name__, "extern")
21+
self.assertEqual(m.__package__, "extern")
22+
self.assertTrue(hasattr(m, "VendorImporter"))
23+
self.assertTrue(hasattr(m, "packaging"))
24+
25+
self.assertEqual(m.packaging.__name__, "_vendor.packaging")
26+
self.assertEqual(m.packaging.__package__, "_vendor.packaging")
27+
28+
self.assertEqual(m.packaging.version.__name__, "extern.packaging.version")
29+
self.assertEqual(m.packaging.version.__package__, "extern.packaging")
30+
31+
self.assertIn("extern", sys.modules.keys())
32+
self.assertIs(m, sys.modules["extern"])
33+
34+
self.assertIn("extern.packaging", sys.modules.keys())
35+
self.assertIs(m.packaging, sys.modules["extern.packaging"])
36+
37+
self.assertIn("extern.packaging.version", sys.modules.keys())
38+
self.assertIs(m.packaging.version, sys.modules["extern.packaging.version"])
39+
40+
self.assertIsNone(m.packaging.version.Infinity)
41+
finally:
42+
sys.path.remove(extra_import_path)
43+
44+
run_test(__name__)
45+

0 commit comments

Comments
 (0)