Skip to content

Commit 68aa709

Browse files
committed
Fix test_import: import machinery and errors
- Emit IMPORT_FROM instead of LOAD_ATTR for `import a.b.c as m` - Add "partially initialized module" error for circular imports - Add "cannot access submodule" error for initializing submodules - Implement script shadowing detection with "consider renaming" hint - Detect user scripts shadowing stdlib/third-party modules - Compute original sys.path[0] from sys.argv[0] - Check sys.stdlib_module_names for stdlib detection - Respect safe_path setting - Implement _imp._fix_co_filename for code source_path rewriting - Add data parameter to _imp.get_frozen_object with marshal deser - Fix import_from: check __spec__.has_location before using origin - Set ImportError.path attribute on import failures - Fix import_star error messages for non-str __all__/__dict__ items - Always call builtins.__import__ in import_inner
1 parent f777416 commit 68aa709

File tree

17 files changed

+794
-135
lines changed

17 files changed

+794
-135
lines changed

Lib/__hello_only__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
initialized = True
2+
print("Hello world!")

Lib/test/test_frozen.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ def test_frozen(self):
2727
__hello__.main()
2828
self.assertEqual(out.getvalue(), 'Hello world!\n')
2929

30-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: unexpectedly identical: <class '_frozen_importlib.FrozenImporter'>
3130
def test_frozen_submodule_in_unfrozen_package(self):
3231
with import_helper.CleanImport('__phello__', '__phello__.spam'):
3332
with import_helper.frozen_modules(enabled=False):
@@ -40,7 +39,6 @@ def test_frozen_submodule_in_unfrozen_package(self):
4039
self.assertIs(spam.__spec__.loader,
4140
importlib.machinery.FrozenImporter)
4241

43-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: unexpectedly identical: <class '_frozen_importlib.FrozenImporter'>
4442
def test_unfrozen_submodule_in_frozen_package(self):
4543
with import_helper.CleanImport('__phello__', '__phello__.spam'):
4644
with import_helper.frozen_modules(enabled=True):

Lib/test/test_import/__init__.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,7 @@ def run():
768768
finally:
769769
del sys.path[0]
770770

771-
@unittest.expectedFailure # TODO: RUSTPYTHON; FileNotFoundError: [WinError 2] No such file or directory: 'built-in'
771+
@unittest.expectedFailure # TODO: RUSTPYTHON; no C extension support
772772
@unittest.skipUnless(sys.platform == "win32", "Windows-specific")
773773
def test_dll_dependency_import(self):
774774
from _winapi import GetModuleFileName
@@ -814,7 +814,6 @@ def test_dll_dependency_import(self):
814814
env=env,
815815
cwd=os.path.dirname(pyexe))
816816

817-
@unittest.expectedFailure # TODO: RUSTPYTHON; _imp.get_frozen_object("x", b"6\'\xd5Cu\x12"). TypeError: expected at most 1 arguments, got 2
818817
def test_issue105979(self):
819818
# this used to crash
820819
with self.assertRaises(ImportError) as cm:
@@ -1239,7 +1238,8 @@ def test_script_shadowing_stdlib_sys_path_modification(self):
12391238
stdout, stderr = popen.communicate()
12401239
self.assertRegex(stdout, expected_error)
12411240

1242-
@unittest.skip("TODO: RUSTPYTHON; AttributeError: module \"_imp\" has no attribute \"create_dynamic\"")
1241+
# TODO: RUSTPYTHON: _imp.create_dynamic is for C extensions, not applicable
1242+
@unittest.skip("TODO: RustPython _imp.create_dynamic not implemented")
12431243
def test_create_dynamic_null(self):
12441244
with self.assertRaisesRegex(ValueError, 'embedded null character'):
12451245
class Spec:
@@ -1398,7 +1398,6 @@ def test_basics(self):
13981398
self.assertEqual(mod.code_filename, self.file_name)
13991399
self.assertEqual(mod.func_filename, self.file_name)
14001400

1401-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 'another_module.py' != .../unlikely_module_name.py
14021401
def test_incorrect_code_name(self):
14031402
py_compile.compile(self.file_name, dfile="another_module.py")
14041403
mod = self.import_module()
@@ -2045,6 +2044,7 @@ def exec_module(*args):
20452044
else:
20462045
importlib.SourceLoader.exec_module = old_exec_module
20472046

2047+
@unittest.expectedFailure # TODO: RUSTPYTHON; subprocess fails on Windows
20482048
@unittest.skipUnless(TESTFN_UNENCODABLE, 'need TESTFN_UNENCODABLE')
20492049
def test_unencodable_filename(self):
20502050
# Issue #11619: The Python parser and the import machinery must not
@@ -2095,7 +2095,6 @@ def test_rebinding(self):
20952095
from test.test_import.data.circular_imports.subpkg import util
20962096
self.assertIs(util.util, rebinding.util)
20972097

2098-
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: module "test.test_import.data.circular_imports" has no attribute "binding"
20992098
def test_binding(self):
21002099
try:
21012100
import test.test_import.data.circular_imports.binding

Lib/test/test_importlib/frozen/test_loader.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from test.support import captured_stdout, import_helper, STDLIB_DIR
66
import contextlib
77
import os.path
8+
import sys
89
import types
910
import unittest
1011
import warnings

Lib/test/test_importlib/test_locks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ def test_all_locks(self):
153153
Source_LifetimeTests
154154
) = test_util.test_both(LifetimeTests, init=init)
155155

156+
# TODO: RUSTPYTHON; dead weakref module locks not cleaned up in frozen bootstrap
157+
Frozen_LifetimeTests.test_all_locks = unittest.skip("TODO: RUSTPYTHON")(
158+
Frozen_LifetimeTests.test_all_locks)
159+
156160

157161
def setUpModule():
158162
thread_info = threading_helper.threading_setup()

Lib/test/test_importlib/test_windows.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def test_raises_deprecation_warning(self):
139139

140140
@unittest.skipUnless(sys.platform.startswith('win'), 'requires Windows')
141141
class WindowsExtensionSuffixTests:
142+
@unittest.expectedFailure # TODO: RUSTPYTHON; no C extension (.pyd) support
142143
def test_tagged_suffix(self):
143144
suffixes = self.machinery.EXTENSION_SUFFIXES
144145
abi_flags = "t" if support.Py_GIL_DISABLED else ""

Lib/test/test_importlib/util.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
import tempfile
1616
import types
1717

18-
_testsinglephase = import_helper.import_module("_testsinglephase")
18+
try:
19+
_testsinglephase = import_helper.import_module("_testsinglephase")
20+
except unittest.SkipTest:
21+
_testsinglephase = None # TODO: RUSTPYTHON
1922

2023

2124
BUILTINS = types.SimpleNamespace()

crates/codegen/src/compile.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2065,11 +2065,19 @@ impl Compiler {
20652065
let idx = self.name(&name.name);
20662066
emit!(self, Instruction::ImportName { idx });
20672067
if let Some(alias) = &name.asname {
2068-
for part in name.name.split('.').skip(1) {
2068+
let parts: Vec<&str> = name.name.split('.').skip(1).collect();
2069+
for (i, part) in parts.iter().enumerate() {
20692070
let idx = self.name(part);
2070-
self.emit_load_attr(idx);
2071+
emit!(self, Instruction::ImportFrom { idx });
2072+
if i < parts.len() - 1 {
2073+
emit!(self, Instruction::Swap { index: 2 });
2074+
emit!(self, Instruction::PopTop);
2075+
}
2076+
}
2077+
self.store_name(alias.as_str())?;
2078+
if !parts.is_empty() {
2079+
emit!(self, Instruction::PopTop);
20712080
}
2072-
self.store_name(alias.as_str())?
20732081
} else {
20742082
self.store_name(name.name.split('.').next().unwrap())?
20752083
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../Lib/__hello_only__.py

crates/vm/src/builtins/module.rs

Lines changed: 119 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::{
55
class::PyClassImpl,
66
convert::ToPyObject,
77
function::{FuncArgs, PyMethodDef, PySetterValue},
8+
import::{get_spec_file_origin, is_possibly_shadowing_path, is_stdlib_module_name},
89
types::{GetAttr, Initializer, Representable},
910
};
1011

@@ -152,20 +153,96 @@ impl Py<PyModule> {
152153
if let Ok(getattr) = self.dict().get_item(identifier!(vm, __getattr__), vm) {
153154
return getattr.call((name.to_owned(),), vm);
154155
}
155-
let module_name = if let Some(name) = self.name(vm) {
156-
format!(" '{name}'")
156+
let dict = self.dict();
157+
158+
// Get the raw __name__ object (may be a str subclass)
159+
let mod_name_obj = dict
160+
.get_item_opt(identifier!(vm, __name__), vm)
161+
.ok()
162+
.flatten();
163+
let mod_name_str = mod_name_obj
164+
.as_ref()
165+
.and_then(|n| n.downcast_ref::<PyStr>().map(|s| s.as_str().to_owned()));
166+
167+
// If __name__ is not set or not a string, use a simpler error message
168+
let mod_display = match mod_name_str.as_deref() {
169+
Some(s) => s,
170+
None => {
171+
return Err(vm.new_attribute_error(format!("module has no attribute '{name}'")));
172+
}
173+
};
174+
175+
let spec = dict
176+
.get_item_opt(vm.ctx.intern_str("__spec__"), vm)
177+
.ok()
178+
.flatten()
179+
.filter(|s| !vm.is_none(s));
180+
181+
let origin = get_spec_file_origin(&spec, vm);
182+
183+
let is_possibly_shadowing = origin
184+
.as_ref()
185+
.map(|o| is_possibly_shadowing_path(o, vm))
186+
.unwrap_or(false);
187+
// Use the ORIGINAL __name__ object for stdlib check (may raise TypeError
188+
// if __name__ is an unhashable str subclass)
189+
let is_possibly_shadowing_stdlib = if is_possibly_shadowing {
190+
if let Some(ref mod_name) = mod_name_obj {
191+
is_stdlib_module_name(mod_name, vm)?
192+
} else {
193+
false
194+
}
157195
} else {
158-
"".to_owned()
196+
false
159197
};
160-
Err(vm.new_attribute_error(format!("module{module_name} has no attribute '{name}'")))
161-
}
162198

163-
fn name(&self, vm: &VirtualMachine) -> Option<PyStrRef> {
164-
let name = self
165-
.as_object()
166-
.generic_getattr_opt(identifier!(vm, __name__), None, vm)
167-
.unwrap_or_default()?;
168-
name.downcast::<PyStr>().ok()
199+
if is_possibly_shadowing_stdlib {
200+
let origin = origin.as_ref().unwrap();
201+
Err(vm.new_attribute_error(format!(
202+
"module '{mod_display}' has no attribute '{name}' \
203+
(consider renaming '{origin}' since it has the same \
204+
name as the standard library module named '{mod_display}' \
205+
and prevents importing that standard library module)"
206+
)))
207+
} else {
208+
let is_initializing = PyModule::is_initializing(&dict, vm);
209+
if is_initializing {
210+
if is_possibly_shadowing {
211+
let origin = origin.as_ref().unwrap();
212+
Err(vm.new_attribute_error(format!(
213+
"module '{mod_display}' has no attribute '{name}' \
214+
(consider renaming '{origin}' if it has the same name \
215+
as a library you intended to import)"
216+
)))
217+
} else if let Some(ref origin) = origin {
218+
Err(vm.new_attribute_error(format!(
219+
"partially initialized module '{mod_display}' from '{origin}' \
220+
has no attribute '{name}' \
221+
(most likely due to a circular import)"
222+
)))
223+
} else {
224+
Err(vm.new_attribute_error(format!(
225+
"partially initialized module '{mod_display}' \
226+
has no attribute '{name}' \
227+
(most likely due to a circular import)"
228+
)))
229+
}
230+
} else {
231+
// Check for uninitialized submodule
232+
let submodule_initializing =
233+
is_uninitialized_submodule(mod_name_str.as_ref(), name, vm);
234+
if submodule_initializing {
235+
Err(vm.new_attribute_error(format!(
236+
"cannot access submodule '{name}' of module '{mod_display}' \
237+
(most likely due to a circular import)"
238+
)))
239+
} else {
240+
Err(vm.new_attribute_error(format!(
241+
"module '{mod_display}' has no attribute '{name}'"
242+
)))
243+
}
244+
}
245+
}
169246
}
170247

171248
// TODO: to be replaced by the commented-out dict method above once dictoffset land
@@ -361,8 +438,8 @@ impl GetAttr for PyModule {
361438
impl Representable for PyModule {
362439
#[inline]
363440
fn repr(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
364-
let importlib = vm.import("_frozen_importlib", 0)?;
365-
let module_repr = importlib.get_attr("_module_repr", vm)?;
441+
// Use cached importlib reference (like interp->importlib)
442+
let module_repr = vm.importlib.get_attr("_module_repr", vm)?;
366443
let repr = module_repr.call((zelf.to_owned(),), vm)?;
367444
repr.downcast()
368445
.map_err(|_| vm.new_type_error("_module_repr did not return a string"))
@@ -377,3 +454,32 @@ impl Representable for PyModule {
377454
pub(crate) fn init(context: &Context) {
378455
PyModule::extend_class(context, context.types.module_type);
379456
}
457+
458+
/// Check if {module_name}.{name} is an uninitialized submodule in sys.modules.
459+
fn is_uninitialized_submodule(
460+
module_name: Option<&String>,
461+
name: &Py<PyStr>,
462+
vm: &VirtualMachine,
463+
) -> bool {
464+
let mod_name = match module_name {
465+
Some(n) => n.as_str(),
466+
None => return false,
467+
};
468+
let full_name = format!("{mod_name}.{name}");
469+
let sys_modules = match vm.sys_module.get_attr("modules", vm).ok() {
470+
Some(m) => m,
471+
None => return false,
472+
};
473+
let sub_mod = match sys_modules.get_item(&full_name, vm).ok() {
474+
Some(m) => m,
475+
None => return false,
476+
};
477+
let spec = match sub_mod.get_attr("__spec__", vm).ok() {
478+
Some(s) if !vm.is_none(&s) => s,
479+
_ => return false,
480+
};
481+
spec.get_attr("_initializing", vm)
482+
.ok()
483+
.and_then(|v| v.try_to_bool(vm).ok())
484+
.unwrap_or(false)
485+
}

0 commit comments

Comments
 (0)