@@ -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 {
374443pub ( 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