Coverage for sygaldry / types.py: 83%
32 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 19:06 -0400
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 19:06 -0400
1from __future__ import annotations
3__author__ = "Rohan B. Dalton"
5import importlib
6from typing import Optional
8from .errors import ImportResolutionError
10RESERVED_KEYS = frozenset(
11 {
12 "_type",
13 "_func",
14 "_args",
15 "_kwargs",
16 "_instance",
17 "_ref",
18 "_deep",
19 "_include",
20 "_entries",
21 "_call",
22 }
23)
26def import_dotted_path(
27 dotted_path: str,
28 *,
29 file_path: str | None = None,
30 config_path: str | None = None,
31) -> object:
32 """
33 Import a dotted path and return the target object.
34 Resolves the longest importable module prefix and then walks remaining attributes.
35 Raising and ImportResolutionError with context if it fails.
37 :param dotted_path: Fully-qualified dotted import path.
38 :type dotted_path: str
39 :param file_path: Source config file path, if known.
40 :type file_path: str | None
41 :param config_path: Dotted config path for context.
42 :type config_path: str | None
43 :returns: Imported module attribute or callable.
44 :rtype: object
45 :raises ImportResolutionError: If the module or attribute cannot be imported.
46 """
47 if not dotted_path or not isinstance(dotted_path, str):
48 raise ImportResolutionError(
49 "Dotted path must be a non-empty string.",
50 file_path=file_path,
51 config_path=config_path,
52 )
54 parts = dotted_path.split(".")
55 module = None
56 module_path = None
58 for idx in range(len(parts), 0, -1): 58 ↛ 69line 58 didn't jump to line 69 because the loop on line 58 didn't complete
59 candidate = ".".join(parts[:idx])
60 try:
61 module = importlib.import_module(candidate)
62 except Exception:
63 continue
64 else:
65 module_path = candidate
66 attributes = parts[idx:]
67 break
69 if module is None or module_path is None: 69 ↛ 70line 69 didn't jump to line 70 because the condition on line 69 was never true
70 raise ImportResolutionError(
71 f"Failed to import module for dotted path '{dotted_path}'.",
72 file_path=file_path,
73 config_path=config_path,
74 )
75 else:
76 target: object = module
77 for attribute in attributes:
78 try:
79 target = getattr(target, attribute)
80 except AttributeError as exc:
81 raise ImportResolutionError(
82 f"Failed to resolve attribute '{attribute}' from '{module_path}' for dotted path '{dotted_path}'.",
83 file_path=file_path,
84 config_path=config_path,
85 ) from exc
87 return target
90if __name__ == "__main__": 90 ↛ 91line 90 didn't jump to line 91 because the condition on line 90 was never true
91 pass