Skip to content

Commit dc0f0b7

Browse files
committed
Fix test_import: import machinery, circular imports, and script shadowing
- Emit IMPORT_FROM instead of LOAD_ATTR for `import a.b.c as m` (compile.rs) - Add "partially initialized module" error in module getattr for circular imports - Add "cannot access submodule" error for initializing submodules - Implement "consider renaming" script shadowing detection (module.rs, frame.rs) - Detect when a user script shadows a stdlib or third-party module - Compute original sys.path[0] from sys.argv[0] (like CPython's config->sys_path_0) - Check sys.stdlib_module_names for stdlib module detection - Respect safe_path setting - Implement _imp._fix_co_filename to rewrite code object source_path in-place - Add data parameter support to _imp.get_frozen_object with marshal deserialization - Fix import_from error messages: check __spec__.has_location before using origin - Set ImportError.path attribute on import failures - Fix import_star error messages for non-str items in __all__ and __dict__ - Always call builtins.__import__ (remove sys.modules cache bypass in import_inner)
1 parent 75137f7 commit dc0f0b7

File tree

7 files changed

+402
-109
lines changed

7 files changed

+402
-109
lines changed

Lib/test/test_import/__init__.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,6 @@ 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'
772771
@unittest.skipUnless(sys.platform == "win32", "Windows-specific")
773772
def test_dll_dependency_import(self):
774773
from _winapi import GetModuleFileName
@@ -814,7 +813,6 @@ def test_dll_dependency_import(self):
814813
env=env,
815814
cwd=os.path.dirname(pyexe))
816815

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

1242-
@unittest.skip("TODO: RUSTPYTHON; AttributeError: module \"_imp\" has no attribute \"create_dynamic\"")
1240+
# TODO: RUSTPYTHON: _imp.create_dynamic is for C extensions, not applicable
1241+
@unittest.skip("TODO: RustPython _imp.create_dynamic not implemented")
12431242
def test_create_dynamic_null(self):
12441243
with self.assertRaisesRegex(ValueError, 'embedded null character'):
12451244
class Spec:
@@ -1398,7 +1397,6 @@ def test_basics(self):
13981397
self.assertEqual(mod.code_filename, self.file_name)
13991398
self.assertEqual(mod.func_filename, self.file_name)
14001399

1401-
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 'another_module.py' != .../unlikely_module_name.py
14021400
def test_incorrect_code_name(self):
14031401
py_compile.compile(self.file_name, dfile="another_module.py")
14041402
mod = self.import_module()
@@ -2095,7 +2093,6 @@ def test_rebinding(self):
20952093
from test.test_import.data.circular_imports.subpkg import util
20962094
self.assertIs(util.util, rebinding.util)
20972095

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

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
@@ -2062,11 +2062,19 @@ impl Compiler {
20622062
let idx = self.name(&name.name);
20632063
emit!(self, Instruction::ImportName { idx });
20642064
if let Some(alias) = &name.asname {
2065-
for part in name.name.split('.').skip(1) {
2065+
let parts: Vec<&str> = name.name.split('.').skip(1).collect();
2066+
for (i, part) in parts.iter().enumerate() {
20662067
let idx = self.name(part);
2067-
self.emit_load_attr(idx);
2068+
emit!(self, Instruction::ImportFrom { idx });
2069+
if i < parts.len() - 1 {
2070+
emit!(self, Instruction::Swap { index: 2 });
2071+
emit!(self, Instruction::PopTop);
2072+
}
2073+
}
2074+
self.store_name(alias.as_str())?;
2075+
if !parts.is_empty() {
2076+
emit!(self, Instruction::PopTop);
20682077
}
2069-
self.store_name(alias.as_str())?
20702078
} else {
20712079
self.store_name(name.name.split('.').next().unwrap())?
20722080
}

crates/vm/src/builtins/module.rs

Lines changed: 218 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -149,20 +149,89 @@ impl Py<PyModule> {
149149
if let Ok(getattr) = self.dict().get_item(identifier!(vm, __getattr__), vm) {
150150
return getattr.call((name.to_owned(),), vm);
151151
}
152-
let module_name = if let Some(name) = self.name(vm) {
153-
format!(" '{name}'")
152+
let dict = self.dict();
153+
154+
// Get the raw __name__ object (may be a str subclass)
155+
let mod_name_obj = dict
156+
.get_item_opt(identifier!(vm, __name__), vm)
157+
.ok()
158+
.flatten();
159+
let mod_name_str = mod_name_obj
160+
.as_ref()
161+
.and_then(|n| n.downcast_ref::<PyStr>().map(|s| s.as_str().to_owned()));
162+
let mod_display = mod_name_str.as_deref().unwrap_or("<unknown module name>");
163+
164+
let spec = dict
165+
.get_item_opt(vm.ctx.intern_str("__spec__"), vm)
166+
.ok()
167+
.flatten()
168+
.filter(|s| !vm.is_none(s));
169+
170+
let origin = get_spec_file_origin(&spec, vm);
171+
172+
let is_possibly_shadowing = origin
173+
.as_ref()
174+
.map(|o| is_possibly_shadowing_path(o, vm))
175+
.unwrap_or(false);
176+
// Use the ORIGINAL __name__ object for stdlib check (may raise TypeError
177+
// if __name__ is an unhashable str subclass)
178+
let is_possibly_shadowing_stdlib = if is_possibly_shadowing {
179+
if let Some(ref mod_name) = mod_name_obj {
180+
is_stdlib_module_name(mod_name, vm)?
181+
} else {
182+
false
183+
}
154184
} else {
155-
"".to_owned()
185+
false
156186
};
157-
Err(vm.new_attribute_error(format!("module{module_name} has no attribute '{name}'")))
158-
}
159187

160-
fn name(&self, vm: &VirtualMachine) -> Option<PyStrRef> {
161-
let name = self
162-
.as_object()
163-
.generic_getattr_opt(identifier!(vm, __name__), None, vm)
164-
.unwrap_or_default()?;
165-
name.downcast::<PyStr>().ok()
188+
if is_possibly_shadowing_stdlib {
189+
let origin = origin.as_ref().unwrap();
190+
Err(vm.new_attribute_error(format!(
191+
"module '{mod_display}' has no attribute '{name}' \
192+
(consider renaming '{origin}' since it has the same \
193+
name as the standard library module named '{mod_display}' \
194+
and prevents importing that standard library module)"
195+
)))
196+
} else {
197+
let is_initializing = PyModule::is_initializing(&dict, vm);
198+
if is_initializing {
199+
if is_possibly_shadowing {
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}' if it has the same name \
204+
as a library you intended to import)"
205+
)))
206+
} else if let Some(ref origin) = origin {
207+
Err(vm.new_attribute_error(format!(
208+
"partially initialized module '{mod_display}' from '{origin}' \
209+
has no attribute '{name}' \
210+
(most likely due to a circular import)"
211+
)))
212+
} else {
213+
Err(vm.new_attribute_error(format!(
214+
"partially initialized module '{mod_display}' \
215+
has no attribute '{name}' \
216+
(most likely due to a circular import)"
217+
)))
218+
}
219+
} else {
220+
// Check for uninitialized submodule
221+
let submod_initializing =
222+
is_uninitialized_submodule(mod_name_str.as_ref(), name, vm);
223+
if submod_initializing {
224+
Err(vm.new_attribute_error(format!(
225+
"cannot access submodule '{name}' of module '{mod_display}' \
226+
(most likely due to a circular import)"
227+
)))
228+
} else {
229+
Err(vm.new_attribute_error(format!(
230+
"module '{mod_display}' has no attribute '{name}'"
231+
)))
232+
}
233+
}
234+
}
166235
}
167236

168237
// TODO: to be replaced by the commented-out dict method above once dictoffset land
@@ -374,3 +443,141 @@ impl Representable for PyModule {
374443
pub(crate) fn init(context: &Context) {
375444
PyModule::extend_class(context, context.types.module_type);
376445
}
446+
447+
/// Get origin path from a module spec, checking has_location first.
448+
pub(crate) fn get_spec_file_origin(
449+
spec: &Option<crate::PyObjectRef>,
450+
vm: &VirtualMachine,
451+
) -> Option<String> {
452+
let spec = spec.as_ref()?;
453+
let has_location = spec
454+
.get_attr("has_location", vm)
455+
.ok()
456+
.and_then(|v| v.try_to_bool(vm).ok())
457+
.unwrap_or(false);
458+
if !has_location {
459+
return None;
460+
}
461+
spec.get_attr("origin", vm).ok().and_then(|origin| {
462+
if vm.is_none(&origin) {
463+
None
464+
} else {
465+
origin
466+
.downcast_ref::<PyStr>()
467+
.map(|s| s.as_str().to_owned())
468+
}
469+
})
470+
}
471+
472+
/// Check if a module file possibly shadows another module of the same name.
473+
/// Compares the module's directory with the original sys.path[0] (derived from sys.argv[0]).
474+
pub(crate) fn is_possibly_shadowing_path(origin: &str, vm: &VirtualMachine) -> bool {
475+
use std::path::Path;
476+
477+
if vm.state.config.settings.safe_path {
478+
return false;
479+
}
480+
481+
let origin_path = Path::new(origin);
482+
let parent = match origin_path.parent() {
483+
Some(p) => p,
484+
None => return false,
485+
};
486+
// For packages (__init__.py), look one directory further up
487+
let root = if origin_path.file_name() == Some("__init__.py".as_ref()) {
488+
parent.parent().unwrap_or(Path::new(""))
489+
} else {
490+
parent
491+
};
492+
493+
// Compute original sys.path[0] from sys.argv[0] (the script path).
494+
// See: config->sys_path_0, which is set once
495+
// at initialization and never changes even if sys.path is modified.
496+
let sys_path_0 = (|| -> Option<String> {
497+
let argv = vm.sys_module.get_attr("argv", vm).ok()?;
498+
let argv0 = argv.get_item(&0usize, vm).ok()?;
499+
let argv0_str = argv0.downcast_ref::<PyStr>()?;
500+
let s = argv0_str.as_str();
501+
502+
// For -c and REPL, original sys.path[0] is ""
503+
if s == "-c" || s.is_empty() {
504+
return Some(String::new());
505+
}
506+
// For scripts, original sys.path[0] is dirname(argv[0])
507+
Some(
508+
Path::new(s)
509+
.parent()
510+
.and_then(|p| p.to_str())
511+
.unwrap_or("")
512+
.to_owned(),
513+
)
514+
})();
515+
516+
let sys_path_0 = match sys_path_0 {
517+
Some(p) => p,
518+
None => return false,
519+
};
520+
521+
let cmp_path = if sys_path_0.is_empty() {
522+
match std::env::current_dir() {
523+
Ok(d) => d.to_string_lossy().to_string(),
524+
Err(_) => return false,
525+
}
526+
} else {
527+
sys_path_0
528+
};
529+
530+
root.to_str() == Some(cmp_path.as_str())
531+
}
532+
533+
/// Check if a module name is in sys.stdlib_module_names.
534+
/// Takes the original __name__ object to preserve str subclass behavior.
535+
/// Propagates errors (e.g. TypeError for unhashable str subclass).
536+
pub(crate) fn is_stdlib_module_name(
537+
name: &crate::PyObjectRef,
538+
vm: &VirtualMachine,
539+
) -> crate::PyResult<bool> {
540+
let stdlib_names = match vm.sys_module.get_attr("stdlib_module_names", vm) {
541+
Ok(names) => names,
542+
Err(_) => return Ok(false),
543+
};
544+
// if (stdlib_modules && PyAnySet_Check(stdlib_modules))
545+
if !stdlib_names.class().fast_issubclass(vm.ctx.types.set_type)
546+
&& !stdlib_names
547+
.class()
548+
.fast_issubclass(vm.ctx.types.frozenset_type)
549+
{
550+
return Ok(false);
551+
}
552+
let result = vm.call_method(&stdlib_names, "__contains__", (name.clone(),))?;
553+
result.try_to_bool(vm)
554+
}
555+
556+
/// Check if {module_name}.{name} is an uninitialized submodule in sys.modules.
557+
fn is_uninitialized_submodule(
558+
module_name: Option<&String>,
559+
name: &Py<PyStr>,
560+
vm: &VirtualMachine,
561+
) -> bool {
562+
let mod_name = match module_name {
563+
Some(n) => n.as_str(),
564+
None => return false,
565+
};
566+
let full_name = format!("{mod_name}.{name}");
567+
let sys_modules = match vm.sys_module.get_attr("modules", vm).ok() {
568+
Some(m) => m,
569+
None => return false,
570+
};
571+
let submod = match sys_modules.get_item(&full_name, vm).ok() {
572+
Some(m) => m,
573+
None => return false,
574+
};
575+
let spec = match submod.get_attr("__spec__", vm).ok() {
576+
Some(s) if !vm.is_none(&s) => s,
577+
_ => return false,
578+
};
579+
spec.get_attr("_initializing", vm)
580+
.ok()
581+
.and_then(|v| v.try_to_bool(vm).ok())
582+
.unwrap_or(false)
583+
}

0 commit comments

Comments
 (0)