1from __future__ import annotations
2
3import os
4import sys
5from importlib import import_module
6from types import ModuleType
7from typing import Any
8
9
10def cached_import(module_path: str, class_name: str) -> Any:
11 # Check whether module is loaded and fully initialized.
12 if not (
13 (module := sys.modules.get(module_path))
14 and (spec := getattr(module, "__spec__", None))
15 and getattr(spec, "_initializing", False) is False
16 ):
17 module = import_module(module_path)
18 return getattr(module, class_name)
19
20
21def import_string(dotted_path: str) -> Any:
22 """
23 Import a dotted module path and return the attribute/class designated by the
24 last name in the path. Raise ImportError if the import failed.
25 """
26 try:
27 module_path, class_name = dotted_path.rsplit(".", 1)
28 except ValueError as err:
29 raise ImportError(f"{dotted_path} doesn't look like a module path") from err
30
31 try:
32 return cached_import(module_path, class_name)
33 except AttributeError as err:
34 raise ImportError(
35 f'Module "{module_path}" does not define a "{class_name}" attribute/class'
36 ) from err
37
38
39def module_dir(module: ModuleType) -> str:
40 """
41 Find the name of the directory that contains a module, if possible.
42
43 Raise ValueError otherwise, e.g. for namespace packages that are split
44 over several directories.
45 """
46 # Convert to list because __path__ may not support indexing.
47 paths = list(getattr(module, "__path__", []))
48 if len(paths) == 1:
49 return paths[0]
50 else:
51 filename = getattr(module, "__file__", None)
52 if filename is not None:
53 return os.path.dirname(filename)
54 raise ValueError(f"Cannot determine directory containing {module}")