Coverage for pytest_jsonschema_snapshot/tools/name_maker.py: 70%

77 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-02 00:37 +0000

1from __future__ import annotations 

2 

3import inspect 

4import re 

5import types 

6from functools import partial 

7from typing import Callable, List, Optional, TypedDict 

8 

9# ──────────────────────────── Типы ──────────────────────────── 

10_Meta = TypedDict( 

11 "_Meta", 

12 { 

13 "package": str, 

14 "package_full": str, 

15 "path_parts": List[str], 

16 "class": Optional[str], 

17 "method": str, 

18 }, 

19) 

20 

21 

22# ──────────────────────────── Класс ─────────────────────────── 

23class NameMaker: 

24 """ 

25 Lightweight helper that converts a callable into a string identifier 

26 using a tiny placeholder-based template language (keeps backward 

27 compatibility with the original test-suite). 

28 

29 Supported placeholders 

30 ---------------------- 

31 

32 `{package}` – full module path (``tests.test_mod``) 

33 

34 `{package_full=SEP}` – same but with custom separator (default “.”) 

35 

36 `{path} / {path=SEP}` – module path *without* the first segment 

37 

38 `{class}` – class name or empty string 

39 

40 `{method}` – function / method name 

41 

42 `{class_method} / {...=SEP}` – ``Class{SEP}method`` or just ``method`` 

43 

44 Unknown placeholders collapse to an empty string. 

45 

46 After substitution: 

47 * “//”, “..”, “--” are collapsed to “/”, “.”, “-” respectively; 

48 * double underscores **are preserved** so ``__call__`` stays intact. 

49 """ 

50 

51 _RE_PLHDR = re.compile(r"\{([^{}]+)\}") 

52 

53 # ───────────────────────────── PUBLIC ────────────────────────────── 

54 @staticmethod 

55 def format(obj: Callable[..., object], rule: str) -> str: 

56 """ 

57 Render *rule* using metadata extracted from *obj*. 

58 """ 

59 meta: _Meta = NameMaker._meta(obj) 

60 

61 def _sub(match: re.Match[str]) -> str: # noqa: N802 

62 token: str | None = match.group(1) 

63 name: str = token.split("=", 1)[0] if token else "" 

64 joiner: str | None = token.split("=", 1)[1] if token and "=" in token else None 

65 return NameMaker._expand(name, joiner, meta) 

66 

67 out = NameMaker._RE_PLHDR.sub(_sub, rule) 

68 return NameMaker._collapse(out) 

69 

70 # ──────────────────────────── INTERNAL ──────────────────────────── 

71 # metadata ---------------------------------------------------------- 

72 @staticmethod 

73 def _unwrap(obj: Callable[..., object]) -> Callable[..., object]: 

74 """Strip functools.partial and @functools.wraps wrappers.""" 

75 while True: 

76 if isinstance(obj, partial): 

77 obj = obj.func 

78 continue 

79 if hasattr(obj, "__wrapped__"): 

80 obj = obj.__wrapped__ 

81 continue 

82 break 

83 return obj 

84 

85 @staticmethod 

86 def _meta(obj: Callable[..., object]) -> _Meta: 

87 """Return mapping used during placeholder substitution.""" 

88 obj = NameMaker._unwrap(obj) 

89 

90 # 1) built-in function (len, sum, …) 

91 if inspect.isbuiltin(obj) or isinstance(obj, types.BuiltinFunctionType): 

92 qualname = obj.__name__ 

93 module = obj.__module__ or "builtins" 

94 

95 # 2) callable instance (defines __call__) 

96 elif not (inspect.isfunction(obj) or inspect.ismethod(obj)): 

97 qualname = f"{obj.__class__.__qualname__}.__call__" 

98 module = obj.__class__.__module__ 

99 

100 # 3) regular function / bound or unbound method 

101 else: 

102 qualname = obj.__qualname__ 

103 module = obj.__module__ 

104 

105 parts: List[str] = qualname.split(".") 

106 cls: Optional[str] = None 

107 if len(parts) > 1 and parts[-2] != "<locals>": 

108 cls = parts[-2] 

109 method = parts[-1] 

110 

111 mod_parts = (module or "").split(".") 

112 return { 

113 "package": module, 

114 "package_full": module, 

115 "path_parts": mod_parts[1:] if len(mod_parts) > 1 else [], 

116 "class": cls, 

117 "method": method, 

118 } 

119 

120 # placeholders ------------------------------------------------------ 

121 @staticmethod 

122 def _expand(name: str, joiner: Optional[str], m: _Meta) -> str: 

123 if name == "package": 

124 return m["package"] 

125 if name == "package_full": 

126 sep = joiner if joiner is not None else "." 

127 return sep.join(m["package_full"].split(".")) 

128 if name == "path": 

129 if not m["path_parts"]: 

130 return "" 

131 sep = joiner if joiner is not None else "/" 

132 return sep.join(m["path_parts"]) 

133 if name == "class": 

134 return m["class"] or "" 

135 if name == "method": 

136 return m["method"] 

137 if name == "class_method": 

138 sep = joiner if joiner is not None else "." 

139 cls_name = m["class"] 

140 if cls_name: 

141 return sep.join([cls_name, m["method"]]) 

142 return m["method"] 

143 # unknown placeholder → empty 

144 return "" 

145 

146 # post-processing --------------------------------------------------- 

147 @staticmethod 

148 def _collapse(s: str) -> str: 

149 # collapse critical duplicates but keep double underscores 

150 s = re.sub(r"/{2,}", "/", s) # '//' → '/' 

151 s = re.sub(r"\.{2,}", ".", s) # '..' → '.' 

152 s = re.sub(r"-{2,}", "-", s) # '--' → '-' 

153 return s