diff --git a/src/core/IronPython/Hosting/PythonCommandLine.cs b/src/core/IronPython/Hosting/PythonCommandLine.cs
index ba4ba624f..6e8ed42dc 100644
--- a/src/core/IronPython/Hosting/PythonCommandLine.cs
+++ b/src/core/IronPython/Hosting/PythonCommandLine.cs
@@ -176,7 +176,7 @@ protected override void Initialize() {
// the first entry in sys.path. If running a script file, however, the CWD should not be added;
// instead, the script's containg folder should be added.
- string fullPath = "."; // this is a valid path resolving to current working dir. Pinky-swear.
+ string fullPath = ""; // the current working directory, importlib resolves it with os.getcwd()
if (Options.Command == null && Options.FileName != null) {
if (Options.FileName == "-") {
diff --git a/src/core/IronPython/Runtime/Importer.cs b/src/core/IronPython/Runtime/Importer.cs
index b36c40385..8a7984808 100644
--- a/src/core/IronPython/Runtime/Importer.cs
+++ b/src/core/IronPython/Runtime/Importer.cs
@@ -564,8 +564,9 @@ private static object ImportTopAbsolute(CodeContext/*!*/ context, string/*!*/ na
}
if (TryLoadMetaPathModule(context, name, null, out ret)) {
- // importlib's BuiltinImporter loads clr before ImportBuiltin gets a chance to
- if (ret == context.LanguageContext.ClrModule) {
+ // importlib's BuiltinImporter loads clr before ImportBuiltin gets a chance to,
+ // and ClrImporter loads .NET namespaces and types
+ if (ret is MemberTracker || ret is PythonType { IsSystemType: true } || ret == context.LanguageContext.ClrModule) {
context.ShowCls = true;
}
return ret;
@@ -842,7 +843,8 @@ private static object LoadFromDisk(CodeContext context, string name, string full
// default behavior
string pathname;
try {
- pathname = context.LanguageContext.DomainManager.Platform.CombinePaths(str, name);
+ // an empty path entry is the current working directory
+ pathname = context.LanguageContext.DomainManager.Platform.CombinePaths(str.Length == 0 ? "." : str, name);
} catch (ArgumentException) {
return null; // ignore invalid paths
}
@@ -934,5 +936,74 @@ internal static PythonModule LoadPackageFromSource(CodeContext/*!*/ context, str
Assert.NotNull(sourceCode, name, path);
return context.LanguageContext.CompileModule(path, name, sourceCode, ModuleOptions.Initialize | ModuleOptions.Optimized);
}
+
+#nullable enable
+
+ ///
+ /// sys.meta_path finder for .NET namespaces and types. It is added after importlib's PathFinder
+ /// so Python modules take precedence over .NET namespaces and types.
+ ///
+ [PythonType]
+ public sealed class ClrImporter {
+ private readonly object _moduleSpec;
+
+ internal ClrImporter(object moduleSpec) {
+ _moduleSpec = moduleSpec;
+ }
+
+ public object? find_spec(CodeContext/*!*/ context, [NotNone] string fullname, object? path = null, object? target = null) {
+ object? member = FindMember(context, fullname);
+ if (member is null) return null;
+
+ // we return "this" as the loader for the module, which uses the legacy load_module
+ // rather than exec_module since types and namespaces don't support setting
+ // the module attributes (__name__, etc.)
+ return PythonCalls.CallWithKeywordArgs(context, _moduleSpec, [fullname, this, member is NamespaceTracker], ["is_package"]);
+ }
+
+ public object? find_module(CodeContext/*!*/ context, [NotNone] string fullname, object? path = null) {
+ return FindMember(context, fullname) is null ? null : this;
+ }
+
+ public object load_module(CodeContext/*!*/ context, [NotNone] string fullname) {
+ var modules = context.LanguageContext.SystemStateModules;
+ if (modules.TryGetValue(fullname, out object? existing) && existing is not null) {
+ return existing;
+ }
+
+ object member = FindMember(context, fullname) ?? throw PythonOps.ImportError("No module named {0}", fullname);
+ modules[fullname] = member;
+ return member;
+ }
+
+ ///
+ /// Looks up a top-level namespace or type, or a child of a namespace that has been imported.
+ /// Returns the same object as attribute access on the namespace.
+ ///
+ private static object? FindMember(CodeContext/*!*/ context, string fullname) {
+ PythonContext pc = context.LanguageContext;
+ NamespaceTracker? parent;
+ string name;
+
+ int lastDot = fullname.LastIndexOf('.');
+ if (lastDot == -1) {
+ // names in the runtime's globals (e.g. COM type libraries) take precedence over namespaces
+ // and are left to the legacy importer
+ if (PythonOps.ScopeTryGetMember(context, pc.DomainManager.Globals, fullname, out _)) return null;
+ parent = pc.TopNamespace;
+ name = fullname;
+ } else {
+ parent = pc.SystemStateModules.TryGetValue(fullname.Substring(0, lastDot), out object? parentModule) ? parentModule as NamespaceTracker : null;
+ name = fullname.Substring(lastDot + 1);
+ }
+
+ if (parent is null) return null;
+
+ object member = NamespaceTrackerOps.GetCustomMember(context, parent, name);
+ return member == OperationFailed.Value ? null : member;
+ }
+ }
+
+#nullable restore
}
}
diff --git a/src/core/IronPython/Runtime/PythonContext.cs b/src/core/IronPython/Runtime/PythonContext.cs
index 434549350..0ccb25a07 100644
--- a/src/core/IronPython/Runtime/PythonContext.cs
+++ b/src/core/IronPython/Runtime/PythonContext.cs
@@ -306,6 +306,8 @@ void BootstrapImportLib() {
#if PYTHON_37_OR_GREATER
PythonOps.Invoke(SharedClsContext, _frozen_importlib, "_install_external_importers");
#endif
+ var moduleSpec = PythonOps.GetBoundAttr(SharedClsContext, _frozen_importlib, "ModuleSpec");
+ (GetSystemStateValue("meta_path") as PythonList)?.append(new Importer.ClrImporter(moduleSpec));
} catch { }
PythonModule LoadModuleFromResource(string name, string resourceName) {
diff --git a/tests/suite/test_imp.py b/tests/suite/test_imp.py
index 640a73a18..6fa4366fd 100644
--- a/tests/suite/test_imp.py
+++ b/tests/suite/test_imp.py
@@ -1064,13 +1064,83 @@ def test_import_clr_show_cls(self):
clr = sys.modules.pop('clr')
try:
g = {}
- exec("r1 = hasattr('abc', 'Split')\nimport clr\nr2 = hasattr('abc', 'Split')", g)
+ exec(compile("r1 = hasattr('abc', 'Split')\nimport clr\nr2 = hasattr('abc', 'Split')", '', 'exec', dont_inherit=True), g)
self.assertFalse(g['r1'])
self.assertTrue(g['r2'])
self.assertIs(g['clr'], clr)
finally:
sys.modules['clr'] = clr
+ @skipUnlessIronPython()
+ def test_clr_importer(self):
+ import importlib
+ import System
+ import System.Collections.Generic
+
+ names = [type(finder).__name__ if not isinstance(finder, type) else finder.__name__ for finder in sys.meta_path]
+ self.assertLess(names.index('PathFinder'), names.index('ClrImporter'))
+
+ # namespaces and types are imported as the same objects as attribute access gives
+ self.assertIs(importlib.import_module('System.Collections.Generic'), System.Collections.Generic)
+ self.assertIs(importlib.import_module('System.Math'), System.Math)
+ self.assertIs(importlib.import_module('.Generic', 'System.Collections'), System.Collections.Generic)
+ self.assertRaises(ImportError, importlib.import_module, 'System.Nonexistent')
+
+ # the first import of a namespace goes through ClrImporter
+ index = names.index('ClrImporter')
+ finder = sys.meta_path[index]
+ found = []
+ class Spy:
+ def find_module(self, fullname, path=None):
+ found.append(fullname)
+ return finder.find_module(fullname, path)
+
+ system = sys.modules.pop('System')
+ sys.meta_path[index] = Spy()
+ try:
+ g = {}
+ exec(compile("r1 = hasattr('abc', 'Split')\nimport System\nr2 = hasattr('abc', 'Split')", '', 'exec', dont_inherit=True), g)
+ self.assertEqual(found, ['System'])
+ self.assertFalse(g['r1'])
+ self.assertTrue(g['r2'])
+ self.assertIs(g['System'], system)
+ finally:
+ sys.meta_path[index] = finder
+ sys.modules['System'] = system
+
+ @skipUnlessIronPython()
+ def test_clr_importer_global_type(self):
+ # the first import of a type in the global namespace enables CLS members
+ import clr
+ from System.Reflection import AssemblyName, TypeAttributes
+ from System.Reflection.Emit import AssemblyBuilder, AssemblyBuilderAccess
+ ab = AssemblyBuilder.DefineDynamicAssembly(AssemblyName('ClrImporterGlobalType'), AssemblyBuilderAccess.Run)
+ ab.DefineDynamicModule('ClrImporterGlobalType').DefineType('ClrImporterGlobalType', TypeAttributes.Public).CreateType()
+ clr.AddReference(ab)
+
+ g = {}
+ exec(compile("r1 = hasattr('abc', 'Split')\nimport ClrImporterGlobalType\nr2 = hasattr('abc', 'Split')", '', 'exec', dont_inherit=True), g)
+ self.assertFalse(g['r1'])
+ self.assertTrue(g['r2'])
+ self.assertEqual(g['ClrImporterGlobalType'].__name__, 'ClrImporterGlobalType')
+
+ @skipUnlessIronPython()
+ def test_clr_importer_globals(self):
+ # the runtime's globals take precedence over namespaces
+ import clr
+ import System
+ runtime_globals = clr.GetCurrentRuntime().Globals
+ value = object()
+ system = sys.modules.pop('System')
+ runtime_globals.System = value
+ try:
+ g = {}
+ exec("import System", g)
+ self.assertIs(g['System'], value)
+ finally:
+ del runtime_globals.System
+ sys.modules['System'] = system
+
def test_get_frozen_object(self):
# frozen objects not supported, this always fails
self.assertRaises(ImportError, imp.get_frozen_object, 'foo')