Coverage for sygaldry / types.py: 83%

32 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-10 13:02 -0400

1from __future__ import annotations 

2 

3__author__ = "Rohan B. Dalton" 

4 

5import importlib 

6from typing import Optional 

7 

8from .errors import ImportResolutionError 

9 

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) 

24 

25 

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. 

36 

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 ) 

53 

54 parts = dotted_path.split(".") 

55 module = None 

56 module_path = None 

57 

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 

68 

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 

86 

87 return target 

88 

89 

90if __name__ == "__main__": 90 ↛ 91line 90 didn't jump to line 91 because the condition on line 90 was never true

91 pass