10
10
import fnmatch
11
11
from functools import partial
12
12
from importlib .machinery import ModuleSpec
13
+ from importlib .machinery import PathFinder
13
14
import importlib .util
14
15
import itertools
15
16
import os
37
38
from _pytest .warning_types import PytestWarning
38
39
39
40
40
- LOCK_TIMEOUT = 60 * 60 * 24 * 3
41
+ if sys .version_info < (3 , 11 ):
42
+ from importlib ._bootstrap_external import _NamespaceLoader as NamespaceLoader
43
+ else :
44
+ from importlib .machinery import NamespaceLoader
41
45
46
+ LOCK_TIMEOUT = 60 * 60 * 24 * 3
42
47
43
48
_AnyPurePath = TypeVar ("_AnyPurePath" , bound = PurePath )
44
49
@@ -611,13 +616,78 @@ def _import_module_using_spec(
611
616
module_name : str , module_path : Path , module_location : Path , * , insert_modules : bool
612
617
) -> ModuleType | None :
613
618
"""
614
- Tries to import a module by its canonical name, path to the .py file, and its
615
- parent location.
619
+ Tries to import a module by its canonical name, path, and its parent location.
620
+
621
+ :param module_name:
622
+ The expected module name, will become the key of `sys.modules`.
623
+
624
+ :param module_path:
625
+ The file path of the module, for example `/foo/bar/test_demo.py`.
626
+ If module is a package, pass the path to the `__init__.py` of the package.
627
+ If module is a namespace package, pass directory path.
628
+
629
+ :param module_location:
630
+ The parent location of the module.
631
+ If module is a package, pass the directory containing the `__init__.py` file.
616
632
617
633
:param insert_modules:
618
- If True, will call insert_missing_modules to create empty intermediate modules
619
- for made-up module names (when importing test files not reachable from sys.path).
634
+ If True, will call `insert_missing_modules` to create empty intermediate modules
635
+ with made-up module names (when importing test files not reachable from `sys.path`).
636
+
637
+ Example 1 of parent_module_*:
638
+
639
+ module_name: "a.b.c.demo"
640
+ module_path: Path("a/b/c/demo.py")
641
+ module_location: Path("a/b/c/")
642
+ if "a.b.c" is package ("a/b/c/__init__.py" exists), then
643
+ parent_module_name: "a.b.c"
644
+ parent_module_path: Path("a/b/c/__init__.py")
645
+ parent_module_location: Path("a/b/c/")
646
+ else:
647
+ parent_module_name: "a.b.c"
648
+ parent_module_path: Path("a/b/c")
649
+ parent_module_location: Path("a/b/")
650
+
651
+ Example 2 of parent_module_*:
652
+
653
+ module_name: "a.b.c"
654
+ module_path: Path("a/b/c/__init__.py")
655
+ module_location: Path("a/b/c/")
656
+ if "a.b" is package ("a/b/__init__.py" exists), then
657
+ parent_module_name: "a.b"
658
+ parent_module_path: Path("a/b/__init__.py")
659
+ parent_module_location: Path("a/b/")
660
+ else:
661
+ parent_module_name: "a.b"
662
+ parent_module_path: Path("a/b/")
663
+ parent_module_location: Path("a/")
620
664
"""
665
+ # Attempt to import the parent module, seems is our responsibility:
666
+ # https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311
667
+ parent_module_name , _ , name = module_name .rpartition ("." )
668
+ parent_module : ModuleType | None = None
669
+ if parent_module_name :
670
+ parent_module = sys .modules .get (parent_module_name )
671
+ if parent_module is None :
672
+ # Get parent_location based on location, get parent_path based on path.
673
+ if module_path .name == "__init__.py" :
674
+ # If the current module is in a package,
675
+ # need to leave the package first and then enter the parent module.
676
+ parent_module_path = module_path .parent .parent
677
+ else :
678
+ parent_module_path = module_path .parent
679
+
680
+ if (parent_module_path / "__init__.py" ).is_file ():
681
+ # If the parent module is a package, loading by __init__.py file.
682
+ parent_module_path = parent_module_path / "__init__.py"
683
+
684
+ parent_module = _import_module_using_spec (
685
+ parent_module_name ,
686
+ parent_module_path ,
687
+ parent_module_path .parent ,
688
+ insert_modules = insert_modules ,
689
+ )
690
+
621
691
# Checking with sys.meta_path first in case one of its hooks can import this module,
622
692
# such as our own assertion-rewrite hook.
623
693
for meta_importer in sys .meta_path :
@@ -627,36 +697,18 @@ def _import_module_using_spec(
627
697
if spec_matches_module_path (spec , module_path ):
628
698
break
629
699
else :
630
- spec = importlib .util .spec_from_file_location (module_name , str (module_path ))
700
+ loader = None
701
+ if module_path .is_dir ():
702
+ # The `spec_from_file_location` matches a loader based on the file extension by default.
703
+ # For a namespace package, need to manually specify a loader.
704
+ loader = NamespaceLoader (name , module_path , PathFinder ())
705
+
706
+ spec = importlib .util .spec_from_file_location (
707
+ module_name , str (module_path ), loader = loader
708
+ )
631
709
632
710
if spec_matches_module_path (spec , module_path ):
633
711
assert spec is not None
634
- # Attempt to import the parent module, seems is our responsibility:
635
- # https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311
636
- parent_module_name , _ , name = module_name .rpartition ("." )
637
- parent_module : ModuleType | None = None
638
- if parent_module_name :
639
- parent_module = sys .modules .get (parent_module_name )
640
- if parent_module is None :
641
- # Find the directory of this module's parent.
642
- parent_dir = (
643
- module_path .parent .parent
644
- if module_path .name == "__init__.py"
645
- else module_path .parent
646
- )
647
- # Consider the parent module path as its __init__.py file, if it has one.
648
- parent_module_path = (
649
- parent_dir / "__init__.py"
650
- if (parent_dir / "__init__.py" ).is_file ()
651
- else parent_dir
652
- )
653
- parent_module = _import_module_using_spec (
654
- parent_module_name ,
655
- parent_module_path ,
656
- parent_dir ,
657
- insert_modules = insert_modules ,
658
- )
659
-
660
712
# Find spec and import this module.
661
713
mod = importlib .util .module_from_spec (spec )
662
714
sys .modules [module_name ] = mod
@@ -675,10 +727,21 @@ def _import_module_using_spec(
675
727
676
728
def spec_matches_module_path (module_spec : ModuleSpec | None , module_path : Path ) -> bool :
677
729
"""Return true if the given ModuleSpec can be used to import the given module path."""
678
- if module_spec is None or module_spec . origin is None :
730
+ if module_spec is None :
679
731
return False
680
732
681
- return Path (module_spec .origin ) == module_path
733
+ if module_spec .origin :
734
+ return Path (module_spec .origin ) == module_path
735
+
736
+ # Compare the path with the `module_spec.submodule_Search_Locations` in case
737
+ # the module is part of a namespace package.
738
+ # https://docs.python.org/3/library/importlib.html#importlib.machinery.ModuleSpec.submodule_search_locations
739
+ if module_spec .submodule_search_locations : # can be None.
740
+ for path in module_spec .submodule_search_locations :
741
+ if Path (path ) == module_path :
742
+ return True
743
+
744
+ return False
682
745
683
746
684
747
# Implement a special _is_same function on Windows which returns True if the two filenames
0 commit comments