Coverage for tests / test_encoders.py: 87.10%

89 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-11 11:48 +0100

1"""Tests for the `encoders` module.""" 

2 

3from __future__ import annotations 

4 

5import json 

6import sys 

7 

8import pytest 

9from jsonschema import ValidationError, validate 

10 

11from griffe import ( 

12 Attribute, 

13 Class, 

14 Function, 

15 GriffeLoader, 

16 Kind, 

17 Module, 

18 Object, 

19 temporary_inspected_package, 

20 temporary_visited_module, 

21) 

22 

23 

24def test_minimal_data_is_enough() -> None: 

25 """Test serialization and de-serialization. 

26 

27 This is an end-to-end test that asserts 

28 we can load back a serialized tree and 

29 infer as much data as within the original tree. 

30 """ 

31 loader = GriffeLoader() 

32 module = loader.load("griffe") 

33 dump_options = {"indent": 2, "sort_keys": True} 

34 minimal = module.as_json(full=False, **dump_options) 

35 full = module.as_json(full=True, **dump_options) 

36 reloaded = Module.from_json(minimal) 

37 assert reloaded.as_json(full=False, **dump_options) == minimal 

38 assert reloaded.as_json(full=True, **dump_options) == full 

39 

40 # Also works (but will result in a different type hint). 

41 assert Object.from_json(minimal) 

42 

43 # Won't work if the JSON doesn't represent the type requested. 

44 with pytest.raises(TypeError, match="provided JSON object is not of type"): 

45 Function.from_json(minimal) 

46 

47 

48def test_namespace_packages() -> None: 

49 """Test support for namespace packages. 

50 

51 Namespace packages are a bit special as they have no `__init__.py` file. 

52 """ 

53 with temporary_inspected_package("namespace_package", init=False) as package: 

54 dump_options = {"indent": 2, "sort_keys": True} 

55 as_json = package.as_json(full=True, **dump_options) 

56 from_json = Object.from_json(as_json) 

57 assert from_json.as_json(full=True, **dump_options) == as_json 

58 

59 

60@pytest.mark.parametrize( 

61 "symbol", 

62 [ 

63 # Attribute. 

64 "_internal.loader.GriffeLoader.finder", 

65 # Function/method. 

66 "_internal.loader.GriffeLoader.load", 

67 # Class. 

68 "_internal.mixins.GetMembersMixin", 

69 # Module. 

70 "_internal.debug", 

71 ], 

72) 

73def test_minimal_light_data_is_enough(symbol: str) -> None: 

74 """Test serialization and de-serialization.""" 

75 loader = GriffeLoader() 

76 package = loader.load("griffe") 

77 obj = package[symbol] 

78 dump_options = {"indent": 2, "sort_keys": True} 

79 minimal = obj.as_json(full=False, **dump_options) 

80 full = obj.as_json(full=True, **dump_options) 

81 reloaded = { 

82 Kind.MODULE: Module, 

83 Kind.CLASS: Class, 

84 Kind.FUNCTION: Function, 

85 Kind.ATTRIBUTE: Attribute, 

86 }[obj.kind].from_json(minimal) 

87 reloaded.parent = obj.parent 

88 assert reloaded.as_json(full=False, **dump_options) == minimal 

89 assert reloaded.as_json(full=True, **dump_options) == full 

90 

91 

92# YORE: EOL 3.12: Remove block. 

93# YORE: EOL 3.11: Remove line. 

94@pytest.mark.skipif(sys.version_info < (3, 12), reason="Python less than 3.12 does not have PEP 695 generics") 

95def test_encoding_pep695_generics_without_defaults() -> None: 

96 """Test serialization and de-serialization of PEP 695 generics without defaults. 

97 

98 Defaults are only possible from Python 3.13 onwards. 

99 """ 

100 with temporary_visited_module( 

101 """ 

102 class Class[X: Exception]: pass 

103 def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass 

104 type TA[T: (int, str)] = dict[str, T] 

105 """, 

106 ) as module: 

107 minimal = module.as_json(full=False) 

108 full = module.as_json(full=True) 

109 reloaded = Module.from_json(minimal) 

110 assert reloaded.as_json(full=False) == minimal 

111 assert reloaded.as_json(full=True) == full 

112 

113 # Also works (but will result in a different type hint). 

114 assert Object.from_json(minimal) 

115 

116 # Won't work if the JSON doesn't represent the type requested. 

117 with pytest.raises(TypeError, match="provided JSON object is not of type"): 

118 Function.from_json(minimal) 

119 

120 

121# YORE: EOL 3.12: Remove line. 

122@pytest.mark.skipif(sys.version_info < (3, 13), reason="Python less than 3.13 does not have defaults in PEP 695 generics") # fmt: skip 

123def test_encoding_pep695_generics() -> None: 

124 """Test serialization and de-serialization of PEP 695 generics with defaults. 

125 

126 Defaults are only possible from Python 3.13 onwards. 

127 """ 

128 with temporary_visited_module( 

129 """ 

130 class Class[X: Exception = OSError]: pass 

131 def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass 

132 type TA[T: (int, str) = str] = dict[str, T] 

133 """, 

134 ) as module: 

135 minimal = module.as_json(full=False) 

136 full = module.as_json(full=True) 

137 reloaded = Module.from_json(minimal) 

138 assert reloaded.as_json(full=False) == minimal 

139 assert reloaded.as_json(full=True) == full 

140 

141 # Also works (but will result in a different type hint). 

142 assert Object.from_json(minimal) 

143 

144 # Won't work if the JSON doesn't represent the type requested. 

145 with pytest.raises(TypeError, match="provided JSON object is not of type"): 

146 Function.from_json(minimal) 

147 

148 

149# use this function in test_json_schema to ease schema debugging 

150def _validate(obj: dict, schema: dict) -> None: 

151 if "members" in obj: 

152 for member in obj["members"]: 

153 _validate(member, schema) 

154 

155 try: 

156 validate(obj, schema) 

157 except ValidationError: 

158 print(obj["path"]) # noqa: T201 

159 raise 

160 

161 

162def test_json_schema() -> None: 

163 """Assert that our serialized data matches our JSON schema.""" 

164 loader = GriffeLoader() 

165 module = loader.load("griffe") 

166 loader.resolve_aliases() 

167 data = json.loads(module.as_json(full=True)) 

168 with open("docs/schema.json", encoding="utf8") as f: # noqa: PTH123 

169 schema = json.load(f) 

170 validate(data, schema) 

171 

172 

173# YORE: EOL 3.12: Remove block. 

174# YORE: EOL 3.11: Remove line. 

175@pytest.mark.skipif(sys.version_info < (3, 12), reason="Python less than 3.12 does not have PEP 695 generics") 

176def test_json_schema_for_pep695_generics_without_defaults() -> None: 

177 """Assert that serialized PEP 695 generics without defaults match our JSON schema. 

178 

179 Defaults are only possible from Python 3.13 onwards. 

180 """ 

181 with temporary_visited_module( 

182 """ 

183 class Class[X: Exception]: pass 

184 def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass 

185 type TA[T: (int, str)] = dict[str, T] 

186 """, 

187 ) as module: 

188 data = json.loads(module.as_json(full=True)) 

189 with open("docs/schema.json", encoding="utf8") as f: # noqa: PTH123 

190 schema = json.load(f) 

191 validate(data, schema) 

192 

193 

194# YORE: EOL 3.12: Remove line. 

195@pytest.mark.skipif(sys.version_info < (3, 13), reason="Python less than 3.13 does not have defaults in PEP 695 generics") # fmt: skip 

196def test_json_schema_for_pep695_generics() -> None: 

197 """Assert that serialized PEP 695 generics with defaults match our JSON schema. 

198 

199 Defaults are only possible from Python 3.13 onwards. 

200 """ 

201 with temporary_visited_module( 

202 """ 

203 class Class[X: Exception = OSError]: pass 

204 def func[**P, T, *R](arg: T, *args: P.args, **kwargs: P.kwargs) -> tuple[*R]: pass 

205 type TA[T: (int, str) = str] = dict[str, T] 

206 """, 

207 ) as module: 

208 data = json.loads(module.as_json(full=True)) 

209 with open("docs/schema.json", encoding="utf8") as f: # noqa: PTH123 

210 schema = json.load(f) 

211 validate(data, schema)