1010import fnmatch
1111from functools import partial
1212from importlib .machinery import ModuleSpec
13+ from importlib .machinery import PathFinder
1314import importlib .util
1415import itertools
1516import os
3738from _pytest .warning_types import PytestWarning
3839
3940
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
4145
46+ LOCK_TIMEOUT = 60 * 60 * 24 * 3
4247
4348_AnyPurePath = TypeVar ("_AnyPurePath" , bound = PurePath )
4449
@@ -611,13 +616,78 @@ def _import_module_using_spec(
611616 module_name : str , module_path : Path , module_location : Path , * , insert_modules : bool
612617) -> ModuleType | None :
613618 """
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.
616632
617633 :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/")
620664 """
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+
621691 # Checking with sys.meta_path first in case one of its hooks can import this module,
622692 # such as our own assertion-rewrite hook.
623693 for meta_importer in sys .meta_path :
@@ -627,36 +697,18 @@ def _import_module_using_spec(
627697 if spec_matches_module_path (spec , module_path ):
628698 break
629699 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+ )
631709
632710 if spec_matches_module_path (spec , module_path ):
633711 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-
660712 # Find spec and import this module.
661713 mod = importlib .util .module_from_spec (spec )
662714 sys .modules [module_name ] = mod
@@ -675,10 +727,21 @@ def _import_module_using_spec(
675727
676728def spec_matches_module_path (module_spec : ModuleSpec | None , module_path : Path ) -> bool :
677729 """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 :
679731 return False
680732
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
682745
683746
684747# Implement a special _is_same function on Windows which returns True if the two filenames
0 commit comments