Coverage for tests/test_extension.py: 100.00%

90 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-02-18 01:26 +0100

1"""Tests for the Griffe extension.""" 

2 

3import pytest 

4from griffe import DocstringSectionKind, Extensions, GriffeLoader, temporary_visited_package 

5 

6from griffe_typingdoc import TypingDocExtension 

7 

8typing_imports = ( 

9 "from typing import Annotated, Doc, Generator, Iterator, Name, NotRequired, Raises, TypedDict, Unpack, Warns" 

10) 

11warning_imports = "from warnings import deprecated" 

12 

13# NOTE: Important! The value in calls to `Doc` will be parsed as a Name expression 

14# if it is valid Python syntax for names. To make sure it is correctly parsed as a string, 

15# it must contain invalid syntax for names, such as a dot at the end. 

16# The alternative solution would be to add `from __future__ import annotations` 

17# at the beginning of each temporary visited module. 

18 

19 

20def test_extension_on_itself() -> None: 

21 """Load our own package using the extension, assert a parameters section is added to the parsed docstring.""" 

22 loader = GriffeLoader(extensions=Extensions(TypingDocExtension())) 

23 typingdoc = loader.load("griffe_typingdoc") 

24 sections = typingdoc["TypingDocExtension.on_function_instance"].docstring.parsed 

25 assert len(sections) == 2 

26 assert sections[1].kind is DocstringSectionKind.parameters 

27 assert sections[1].value[1].description == "The Griffe function just instantiated." 

28 

29 

30def test_attribute_doc() -> None: 

31 """Read documentation for attributes.""" 

32 with temporary_visited_package( 

33 "package", 

34 modules={"__init__.py": f"{typing_imports}\na: Annotated[str, Doc('Hello.')]"}, 

35 extensions=Extensions(TypingDocExtension()), 

36 ) as package: 

37 assert package["a"].docstring.value == "Hello." 

38 

39 

40def test_parameter_doc() -> None: 

41 """Read documentation for parameters.""" 

42 with temporary_visited_package( 

43 "package", 

44 modules={"__init__.py": f"{typing_imports}\ndef f(a: Annotated[str, Doc('Hello.')]): ..."}, 

45 extensions=Extensions(TypingDocExtension()), 

46 ) as package: 

47 assert package["f"].docstring.parsed[1].value[0].description == "Hello." 

48 

49 

50def test_other_parameter_doc() -> None: 

51 """Read documentation for other parameters, in unpack/typeddict annotations.""" 

52 with temporary_visited_package( 

53 "package", 

54 modules={ 

55 "__init__.py": f""" 

56 {typing_imports} 

57 class OtherParameters(TypedDict, total=False): 

58 param1: Annotated[NotRequired[str], Doc("Hello.")] 

59 

60 def f(**kwargs: Annotated[Unpack[OtherParameters], Doc("See other parameters.")]): 

61 ... 

62 """, 

63 }, 

64 extensions=Extensions(TypingDocExtension()), 

65 ) as package: 

66 assert package["f"].docstring.parsed[2].value[0].description == "Hello." 

67 

68 

69def test_iterator_doc() -> None: 

70 """Read documentation in iterator annotations.""" 

71 with temporary_visited_package( 

72 "package", 

73 modules={ 

74 "__init__.py": f""" 

75 {typing_imports} 

76 def f() -> Iterator[Annotated[int, Doc("Yielded hello.")]]: 

77 ... 

78 """, 

79 }, 

80 extensions=Extensions(TypingDocExtension()), 

81 ) as package: 

82 assert package["f"].docstring.parsed[1].value[0].description == "Yielded hello." 

83 

84 

85def test_generator_doc() -> None: 

86 """Read documentation in generator annotations.""" 

87 with temporary_visited_package( 

88 "package", 

89 modules={ 

90 "__init__.py": f""" 

91 {typing_imports} 

92 def f() -> Generator[ 

93 Annotated[int, Doc("Yielded hello.")], 

94 Annotated[int, Doc("Received hello.")], 

95 Annotated[int, Doc("Returned hello.")], 

96 ]: 

97 ... 

98 """, 

99 }, 

100 extensions=Extensions(TypingDocExtension()), 

101 ) as package: 

102 sections = package["f"].docstring.parsed 

103 assert sections[1].value[0].description == "Yielded hello." 

104 assert sections[2].value[0].description == "Received hello." 

105 assert sections[3].value[0].description == "Returned hello." 

106 

107 

108def test_generator_tuples() -> None: 

109 """Read documentation in generator annotations (in tuples).""" 

110 with temporary_visited_package( 

111 "package", 

112 modules={ 

113 "__init__.py": f""" 

114 {typing_imports} 

115 def f() -> Generator[ 

116 tuple[ 

117 Annotated[int, Doc("First yielded.")], 

118 Annotated[float, Doc("Second yielded.")], 

119 ], 

120 tuple[ 

121 Annotated[int, Doc("First received.")], 

122 Annotated[float, Doc("Second received.")], 

123 ], 

124 tuple[ 

125 Annotated[int, Doc("First returned.")], 

126 Annotated[float, Doc("Second returned.")], 

127 ], 

128 ]: 

129 ... 

130 """, 

131 }, 

132 extensions=Extensions(TypingDocExtension()), 

133 ) as package: 

134 sections = package["f"].docstring.parsed 

135 assert sections[1].value[0].description == "First yielded." 

136 assert sections[1].value[1].description == "Second yielded." 

137 assert sections[2].value[0].description == "First received." 

138 assert sections[2].value[1].description == "Second received." 

139 assert sections[3].value[0].description == "First returned." 

140 assert sections[3].value[1].description == "Second returned." 

141 

142 

143def test_return_doc() -> None: 

144 """Read documentation for return value.""" 

145 with temporary_visited_package( 

146 "package", 

147 modules={"__init__.py": f"{typing_imports}\ndef f() -> Annotated[int, Doc('Hello.')]: ..."}, 

148 extensions=Extensions(TypingDocExtension()), 

149 ) as package: 

150 assert package["f"].docstring.parsed[1].value[0].description == "Hello." 

151 

152 

153def test_unpacking_typed_dict() -> None: 

154 """Unpack typed dicts, resolving them to their right location.""" 

155 with temporary_visited_package( 

156 "package", 

157 { 

158 "__init__.py": """ 

159 from typing import TypedDict 

160 from typing_extensions import Annotated, Doc, Unpack 

161 

162 from package import module 

163 

164 class Options(TypedDict): 

165 foo: Annotated[int, Doc("Foo's description.")] 

166 

167 class A: 

168 def __init__(self, **kwargs: Unpack[Options]) -> None: 

169 '''Init.''' 

170 self.options = kwargs 

171 

172 class B: 

173 def __init__(self, **kwargs: Unpack[module.Options]) -> None: 

174 '''Init.''' 

175 self.options = kwargs 

176 """, 

177 "module.py": """ 

178 from typing import TypedDict 

179 from typing_extensions import Annotated, Doc 

180 

181 class Options(TypedDict): 

182 bar: Annotated[str, Doc("Bar's description.")] 

183 """, 

184 }, 

185 extensions=Extensions(TypingDocExtension()), 

186 ) as package: 

187 sections = package["A.__init__"].docstring.parsed 

188 assert len(sections) == 2 

189 assert sections[0].kind is DocstringSectionKind.text 

190 assert sections[1].kind is DocstringSectionKind.other_parameters 

191 foo = sections[1].value[0] 

192 assert foo.name == "foo" 

193 assert foo.description == "Foo's description." 

194 assert str(foo.annotation).startswith("Annotated[int") 

195 

196 sections = package["B.__init__"].docstring.parsed 

197 assert len(sections) == 2 

198 assert sections[0].kind is DocstringSectionKind.text 

199 assert sections[1].kind is DocstringSectionKind.other_parameters 

200 bar = sections[1].value[0] 

201 assert bar.name == "bar" 

202 assert bar.description == "Bar's description." 

203 assert str(bar.annotation).startswith("Annotated[str") 

204 

205 

206@pytest.mark.parametrize( 

207 "annotation", 

208 ["int", "Annotated[int, '']"], 

209) 

210def test_ignore_unannotated_params(annotation: str) -> None: 

211 """Ignore parameters that are not annotated with `Doc`.""" 

212 with temporary_visited_package( 

213 "package", 

214 { 

215 "__init__.py": f"{typing_imports}\ndef f(a: {annotation}):\n '''Docstring.'''", 

216 }, 

217 extensions=Extensions(TypingDocExtension()), 

218 ) as package: 

219 sections = package["f"].docstring.parsed 

220 assert len(sections) == 1 

221 assert sections[0].kind is DocstringSectionKind.text 

222 

223 

224@pytest.mark.parametrize( 

225 "annotation", 

226 ["int", "Annotated[int, '']"], 

227) 

228def test_ignore_unannotated_other_params(annotation: str) -> None: 

229 """Ignore other parameters that are not annotated with `Doc`.""" 

230 with temporary_visited_package( 

231 "package", 

232 { 

233 "__init__.py": f""" 

234 {typing_imports} 

235 from typing import TypedDict 

236 class Kwargs(TypedDict): 

237 a: {annotation} 

238 def f(**kwargs: Unpack[Kwargs]): 

239 '''Docstring.''' 

240 """, 

241 }, 

242 extensions=Extensions(TypingDocExtension()), 

243 ) as package: 

244 sections = package["f"].docstring.parsed 

245 assert len(sections) == 1 

246 assert sections[0].kind is DocstringSectionKind.text 

247 

248 

249@pytest.mark.parametrize( 

250 "annotation", 

251 ["int", "Annotated[int, '']"], 

252) 

253def test_ignore_unannotated_returns(annotation: str) -> None: 

254 """Ignore return values that are not annotated with `Doc`.""" 

255 with temporary_visited_package( 

256 "package", 

257 { 

258 "__init__.py": f"{typing_imports}\ndef f() -> {annotation}:\n '''Docstring.'''", 

259 }, 

260 extensions=Extensions(TypingDocExtension()), 

261 ) as package: 

262 sections = package["f"].docstring.parsed 

263 assert len(sections) == 1 

264 assert sections[0].kind is DocstringSectionKind.text 

265 

266 

267@pytest.mark.parametrize( 

268 "annotation", 

269 ["int", "Annotated[int, '']"], 

270) 

271def test_ignore_unannotated_yields(annotation: str) -> None: 

272 """Ignore yields that are not annotated with `Doc`.""" 

273 with temporary_visited_package( 

274 "package", 

275 { 

276 "__init__.py": f"{typing_imports}\ndef f() -> Iterator[{annotation}]:\n '''Docstring.'''", 

277 }, 

278 extensions=Extensions(TypingDocExtension()), 

279 ) as package: 

280 sections = package["f"].docstring.parsed 

281 assert len(sections) == 1 

282 assert sections[0].kind is DocstringSectionKind.text 

283 

284 

285@pytest.mark.parametrize( 

286 "annotation", 

287 ["int", "Annotated[int, '']"], 

288) 

289def test_ignore_unannotated_receives(annotation: str) -> None: 

290 """Ignore receives that are not annotated with `Doc`.""" 

291 with temporary_visited_package( 

292 "package", 

293 { 

294 "__init__.py": f"{typing_imports}\ndef f() -> Generator[int, {annotation}, None]:\n '''Docstring.'''", 

295 }, 

296 extensions=Extensions(TypingDocExtension()), 

297 ) as package: 

298 sections = package["f"].docstring.parsed 

299 assert len(sections) == 1 

300 assert sections[0].kind is DocstringSectionKind.text