@@ -522,22 +522,19 @@ def import_path(
522
522
raise ImportError (path )
523
523
524
524
if mode is ImportMode .importlib :
525
- # Obtain the canonical name of this path if we can resolve it to a python package,
526
- # and try to import it without changing sys.path.
527
- # If it works, we import it and return the module.
528
- _ , module_name = resolve_pkg_root_and_module_name (path )
525
+ # Try to import this module using the standard import mechanisms, but
526
+ # without touching sys.path.
529
527
try :
530
- importlib . import_module ( module_name )
531
- except ( ImportError , ImportWarning ):
532
- # Cannot be imported normally with the current sys.path, so fallback
533
- # to the more complex import-path mechanism.
528
+ pkg_root , module_name = resolve_pkg_root_and_module_name (
529
+ path , consider_ns_packages = True
530
+ )
531
+ except ValueError :
534
532
pass
535
533
else :
536
- # Double check that the imported module is the one we
537
- # were given, otherwise it is easy to import modules with common names like "test"
538
- # from another location.
539
- mod = sys .modules [module_name ]
540
- if mod .__file__ and Path (mod .__file__ ) == path :
534
+ mod = _import_module_using_spec (
535
+ module_name , path , pkg_root , insert_modules = False
536
+ )
537
+ if mod is not None :
541
538
return mod
542
539
543
540
# Could not import the module with the current sys.path, so we fall back
@@ -546,22 +543,17 @@ def import_path(
546
543
with contextlib .suppress (KeyError ):
547
544
return sys .modules [module_name ]
548
545
549
- for meta_importer in sys .meta_path :
550
- spec = meta_importer .find_spec (module_name , [str (path .parent )])
551
- if spec is not None :
552
- break
553
- else :
554
- spec = importlib .util .spec_from_file_location (module_name , str (path ))
555
-
556
- if spec is None :
546
+ mod = _import_module_using_spec (
547
+ module_name , path , path .parent , insert_modules = True
548
+ )
549
+ if mod is None :
557
550
raise ImportError (f"Can't find module { module_name } at location { path } " )
558
- mod = importlib .util .module_from_spec (spec )
559
- sys .modules [module_name ] = mod
560
- spec .loader .exec_module (mod ) # type: ignore[union-attr]
561
- insert_missing_modules (sys .modules , module_name )
562
551
return mod
563
552
564
- pkg_root , module_name = resolve_pkg_root_and_module_name (path )
553
+ try :
554
+ pkg_root , module_name = resolve_pkg_root_and_module_name (path )
555
+ except ValueError :
556
+ pkg_root , module_name = path .parent , path .stem
565
557
566
558
# Change sys.path permanently: restoring it at the end of this function would cause surprising
567
559
# problems because of delayed imports: for example, a conftest.py file imported by this function
@@ -603,6 +595,27 @@ def import_path(
603
595
return mod
604
596
605
597
598
+ def _import_module_using_spec (
599
+ module_name : str , module_path : Path , module_location : Path , * , insert_modules : bool
600
+ ) -> Optional [ModuleType ]:
601
+ """Tries to import a module by its canonical name, path to the .py file and its location."""
602
+ for meta_importer in sys .meta_path :
603
+ spec = meta_importer .find_spec (module_name , [str (module_location )])
604
+ if spec is not None :
605
+ break
606
+ else :
607
+ spec = importlib .util .spec_from_file_location (module_name , str (module_path ))
608
+ if spec is not None :
609
+ mod = importlib .util .module_from_spec (spec )
610
+ sys .modules [module_name ] = mod
611
+ spec .loader .exec_module (mod ) # type: ignore[union-attr]
612
+ if insert_modules :
613
+ insert_missing_modules (sys .modules , module_name )
614
+ return mod
615
+
616
+ return None
617
+
618
+
606
619
# Implement a special _is_same function on Windows which returns True if the two filenames
607
620
# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
608
621
if sys .platform .startswith ("win" ):
@@ -707,7 +720,9 @@ def resolve_package_path(path: Path) -> Optional[Path]:
707
720
return result
708
721
709
722
710
- def resolve_pkg_root_and_module_name (path : Path ) -> Tuple [Path , str ]:
723
+ def resolve_pkg_root_and_module_name (
724
+ path : Path , * , consider_ns_packages : bool = False
725
+ ) -> Tuple [Path , str ]:
711
726
"""
712
727
Return the path to the directory of the root package that contains the
713
728
given Python file, and its module name:
@@ -721,20 +736,30 @@ def resolve_pkg_root_and_module_name(path: Path) -> Tuple[Path, str]:
721
736
722
737
Passing the full path to `models.py` will yield Path("src") and "app.core.models".
723
738
724
- If the given path does not belong to a package (missing __init__.py files), returns
725
- just the parent path and the name of the file as module name.
739
+ If consider_ns_packages is True, then we additionally check if the top-level directory
740
+ without __init__.py is reachable from sys.path; if it is, it is then considered a namespace package:
741
+
742
+ https://packaging.python.org/en/latest/guides/packaging-namespace-packages
743
+
744
+ This is not the default because we need to analyze carefully if it is safe to assume this
745
+ for all imports.
746
+
747
+ Raises ValueError if the given path does not belong to a package (missing any __init__.py files).
726
748
"""
727
749
pkg_path = resolve_package_path (path )
728
750
if pkg_path is not None :
729
751
pkg_root = pkg_path .parent
752
+ # pkg_root.parent does not contain a __init__.py file, as per resolve_package_path,
753
+ # but if it is reachable from sys.argv, it should be considered a namespace package.
754
+ # https://packaging.python.org/en/latest/guides/packaging-namespace-packages/
755
+ if consider_ns_packages and str (pkg_root .parent ) in sys .path :
756
+ pkg_root = pkg_root .parent
730
757
names = list (path .with_suffix ("" ).relative_to (pkg_root ).parts )
731
758
if names [- 1 ] == "__init__" :
732
759
names .pop ()
733
760
module_name = "." .join (names )
734
- else :
735
- pkg_root = path .parent
736
- module_name = path .stem
737
- return pkg_root , module_name
761
+ return pkg_root , module_name
762
+ raise ValueError (f"Could not resolve for { path } " )
738
763
739
764
740
765
def scandir (
0 commit comments