From cb8b11eb645e06e86714765c269c28ac47ac0314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Fri, 23 Oct 2026 22:38:22 -0400 Subject: [PATCH 1/3] Use '' on sys.path --- src/core/IronPython/Hosting/PythonCommandLine.cs | 2 +- src/core/IronPython/Runtime/Importer.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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..a14502c60 100644 --- a/src/core/IronPython/Runtime/Importer.cs +++ b/src/core/IronPython/Runtime/Importer.cs @@ -842,7 +842,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 } From fee8fca3778cf919e142301aa96895f4b95ff5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Thu, 22 Oct 2026 21:56:44 -0400 Subject: [PATCH 2/3] Add a sys.meta_path finder for .NET namespaces and types --- src/core/IronPython/Runtime/Importer.cs | 73 +++++++++++++++++++- src/core/IronPython/Runtime/PythonContext.cs | 2 + tests/suite/test_imp.py | 72 ++++++++++++++++++- 3 files changed, 144 insertions(+), 3 deletions(-) diff --git a/src/core/IronPython/Runtime/Importer.cs b/src/core/IronPython/Runtime/Importer.cs index a14502c60..7f5a861ae 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; @@ -935,5 +936,73 @@ 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; + + // load_module is used rather than exec_module since types 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') From 36d696795bf49a4c01d2b6b62936010e333e5bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lozier?= Date: Fri, 23 Oct 2026 23:28:15 -0400 Subject: [PATCH 3/3] Adjust comment --- src/core/IronPython/Runtime/Importer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/IronPython/Runtime/Importer.cs b/src/core/IronPython/Runtime/Importer.cs index 7f5a861ae..8a7984808 100644 --- a/src/core/IronPython/Runtime/Importer.cs +++ b/src/core/IronPython/Runtime/Importer.cs @@ -955,8 +955,9 @@ internal ClrImporter(object moduleSpec) { object? member = FindMember(context, fullname); if (member is null) return null; - // load_module is used rather than exec_module since types don't support setting the - // module attributes (__name__, etc.) + // 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"]); }