Coverage for tests/test_loader.py: 96.91%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

326 statements  

1"""Tests for [the `loader` module][pytkdocs.loader].""" 

2 

3import os 

4import sys 

5from pathlib import Path 

6from typing import Set 

7 

8import pytest 

9from django.db.models.fields import CharField 

10from marshmallow import fields 

11 

12from pytkdocs.loader import Loader, get_object_tree 

13from tests import FIXTURES_DIR 

14 

15 

16def test_import_no_path(): 

17 """Raise error when getting tree for empty object name.""" 

18 with pytest.raises(ValueError): 

19 get_object_tree("") 

20 

21 

22def test_import_error(): 

23 """Raise error when getting tree for missing object.""" 

24 with pytest.raises(ImportError): 

25 get_object_tree("eeeeeeeeeeeeeeeeeee") 

26 

27 

28def test_can_find_class_real_path(): 

29 """Find real path of a class.""" 

30 leaf = get_object_tree("tests.fixtures.real_path.module_a.DefinedInModuleB") 

31 assert leaf.dotted_path == "tests.fixtures.real_path.module_b.DefinedInModuleB" 

32 

33 

34def test_can_find_class_method_real_path(): 

35 """Find real path of a class method.""" 

36 leaf = get_object_tree("tests.fixtures.real_path.module_a.DefinedInModuleB.method") 

37 assert leaf.dotted_path == "tests.fixtures.real_path.module_b.DefinedInModuleB.method" 

38 

39 

40def test_can_find_class_attribute_real_path(): 

41 """Find real path of a class attribute.""" 

42 leaf = get_object_tree("tests.fixtures.real_path.module_a.DefinedInModuleB.ATTRIBUTE") 

43 assert leaf.dotted_path == "tests.fixtures.real_path.module_b.DefinedInModuleB.ATTRIBUTE" 

44 

45 

46def test_cannot_find_module_attribute_real_path(): 

47 """Find real path of a module attribute.""" 

48 leaf = get_object_tree("tests.fixtures.real_path.module_a.ATTRIBUTE") 

49 assert leaf.dotted_path != "tests.fixtures.real_path.module_b.ATTRIBUTE" 

50 

51 

52def test_import_module_with_colon_path_syntax(): 

53 """Import a module using the "colon" path syntax.""" 

54 leaf = get_object_tree("tests.fixtures.the_package.the_module", new_path_syntax=True) 

55 

56 

57def test_import_attribute_with_colon_path_syntax(): 

58 """Import an attribute using the "colon" path syntax.""" 

59 leaf = get_object_tree("tests.fixtures.the_package.the_module:THE_ATTRIBUTE") 

60 

61 

62def test_import_nested_attribute_with_colon_path_syntax(): 

63 """Import a nested attribute using the "colon" path syntax.""" 

64 leaf = get_object_tree("tests.fixtures.the_package.the_module:TheClass.THE_ATTRIBUTE") 

65 

66 

67def test_fail_to_import_module_with_colon_path_syntax(): 

68 """Import a module using the "colon" path syntax.""" 

69 with pytest.raises(ImportError): 

70 get_object_tree("tests.fixtures.does_not_exist", new_path_syntax=True) 

71 

72 

73def test_fail_to_import_attribute_with_colon_path_syntax(): 

74 """Import an attribute using the "colon" path syntax.""" 

75 with pytest.raises(AttributeError) as error: 

76 leaf = get_object_tree("tests.fixtures.the_package.the_module:does_not_exist") 

77 

78 

79def test_fail_to_import_nested_attribute_with_colon_path_syntax(): 

80 """Import a nested attribute using the "colon" path syntax.""" 

81 with pytest.raises(AttributeError) as error: 

82 leaf = get_object_tree("tests.fixtures.the_package.the_module:TheClass.does_not_exist") 

83 

84 

85def test_fail_to_import_module_with_dot_path_syntax(): 

86 """Import a module using the "dot" path syntax.""" 

87 with pytest.raises(ImportError, match=r"possible causes"): 

88 get_object_tree("does_not_exist") 

89 

90 

91def test_fail_to_import_attribute_with_dot_path_syntax(): 

92 """Import an attribute using the "dot" path syntax.""" 

93 with pytest.raises(AttributeError) as error: 

94 leaf = get_object_tree("tests.fixtures.the_package.the_module.does_not_exist") 

95 

96 

97def test_fail_to_import_nested_attribute_with_dot_path_syntax(): 

98 """Import a nested attribute using the "dot" path syntax.""" 

99 with pytest.raises(AttributeError) as error: 

100 leaf = get_object_tree("tests.fixtures.the_package.the_module.TheClass.does_not_exist") 

101 

102 

103def test_inheriting_enum_Enum(): 

104 """Handle `enum.Enum` classes.""" 

105 """See details at [tests.fixtures.inheriting_enum_Enum][].""" 

106 loader = Loader() 

107 loader.get_object_documentation("tests.fixtures.inheriting_enum_Enum") 

108 assert not loader.errors 

109 

110 

111def test_inheriting_typing_NamedTuple(): 

112 """ 

113 Handle `typing.NamedTuple classes`. 

114 

115 See details at [tests.fixtures.inheriting_typing_NamedTuple][]. 

116 """ 

117 loader = Loader() 

118 loader.get_object_documentation("tests.fixtures.inheriting_typing_NamedTuple") 

119 assert len(loader.errors) == 0 

120 

121 

122def test_nested_class(): 

123 """Handle nested classes.""" 

124 loader = Loader() 

125 obj = loader.get_object_documentation("tests.fixtures.nested_class") 

126 assert obj.classes 

127 assert obj.classes[0].docstring == "Main docstring." 

128 assert obj.classes[0].classes 

129 assert obj.classes[0].classes[0].docstring == "Nested docstring." 

130 

131 

132def test_loading_deep_package(): 

133 """Handle deep nesting of packages.""" 

134 loader = Loader() 

135 obj = loader.get_object_documentation("tests.fixtures.pkg1.pkg2.pkg3.pkg4.pkg5") 

136 assert obj.docstring == "Hello from the abyss." 

137 assert obj.path == "tests.fixtures.pkg1.pkg2.pkg3.pkg4.pkg5" 

138 

139 

140def test_loading_package(): 

141 """Handle basic packages.""" 

142 loader = Loader() 

143 obj = loader.get_object_documentation("tests.fixtures.the_package") 

144 assert obj.docstring == "The package docstring." 

145 

146 

147def test_loading_namespace_package(): 

148 """Handle native namespace packages.""" 

149 loader = Loader() 

150 old_paths = list(sys.path) 

151 sys.path.append(str(Path(FIXTURES_DIR).resolve())) 

152 obj = loader.get_object_documentation("test_namespace.subspace") 

153 assert obj.docstring == "The subspace package docstring." 

154 assert obj.relative_file_path == f"subspace{os.sep}__init__.py" 

155 sys.path = old_paths 

156 

157 

158def test_loading_module(): 

159 """Handle single modules.""" 

160 loader = Loader() 

161 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module") 

162 assert obj.docstring == "The module docstring." 

163 

164 

165def test_loading_class(): 

166 """Handle classes.""" 

167 loader = Loader() 

168 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass") 

169 assert obj.docstring == "The class docstring." 

170 assert obj.bases == ["object"] 

171 

172 

173def test_loading_class_with_multiline_docstring_starting_on_first_line(): 

174 """Handle classes with multiline docstrings where the first line is next to the triple-quotes.""" 

175 loader = Loader() 

176 obj = loader.get_object_documentation("tests.fixtures.first_line_class_docstring.TheClass") 

177 assert obj.docstring == """The first line of the docstring.\n\nA bit more of the docstring.""" 

178 

179 

180def test_loading_dataclass(): 

181 """Handle dataclasses.""" 

182 loader = Loader() 

183 obj = loader.get_object_documentation("tests.fixtures.dataclass.Person") 

184 assert obj.docstring == "Simple dataclass for a person's information" 

185 assert len(obj.attributes) == 2 

186 name_attr = next(attr for attr in obj.attributes if attr.name == "name") 186 ↛ exitline 186 didn't finish the generator expression on line 186

187 assert name_attr.type == str 

188 age_attr = next(attr for attr in obj.attributes if attr.name == "age") 188 ↛ exitline 188 didn't finish the generator expression on line 188

189 assert age_attr.type == int 

190 assert age_attr.docstring == "Field description." 

191 assert "dataclass" in obj.properties 

192 

193 not_dataclass = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.TheNestedClass") 

194 assert "dataclass" not in not_dataclass.properties 

195 

196 

197def test_loading_empty_dataclass(): 

198 """Handle empty dataclasses.""" 

199 loader = Loader() 

200 obj = loader.get_object_documentation("tests.fixtures.dataclass.Empty") 

201 assert obj.docstring == "A dataclass without any fields" 

202 assert len(obj.attributes) == 0 

203 assert "dataclass" in obj.properties 

204 

205 

206def test_loading_pydantic_model(): 

207 """Handle Pydantic models.""" 

208 loader = Loader() 

209 obj = loader.get_object_documentation("tests.fixtures.pydantic.Person") 

210 assert obj.docstring == "Simple Pydantic Model for a person's information" 

211 assert "pydantic-model" in obj.properties 

212 name_attr = next(attr for attr in obj.attributes if attr.name == "name") 212 ↛ exitline 212 didn't finish the generator expression on line 212

213 assert name_attr.type == str 

214 assert name_attr.docstring == "The person's name" 

215 assert "pydantic-field" in name_attr.properties 

216 age_attr = next(attr for attr in obj.attributes if attr.name == "age") 216 ↛ exitline 216 didn't finish the generator expression on line 216

217 assert age_attr.type == int 

218 assert age_attr.docstring == "The person's age which must be at minimum 18" 

219 assert "pydantic-field" in age_attr.properties 

220 labels_attr = next(attr for attr in obj.attributes if attr.name == "labels") 220 ↛ exitline 220 didn't finish the generator expression on line 220

221 assert labels_attr.type == Set[str] 

222 assert labels_attr.docstring == "Set of labels the person can be referred by" 

223 assert "pydantic-field" in labels_attr.properties 

224 

225 

226def test_loading_django_model(): 

227 """Handle Django models""" 

228 loader = Loader() 

229 obj = loader.get_object_documentation("tests.fixtures.django.Person") 

230 assert obj.docstring == "Simple Django Model for a person's information" 

231 name_attr = next(attr for attr in obj.attributes if attr.name == "name") 231 ↛ exitline 231 didn't finish the generator expression on line 231

232 assert name_attr.type == CharField 

233 assert name_attr.docstring == "Name" 

234 

235 

236def test_loading_marshmallow_model(): 

237 """Handle Marshmallow models.""" 

238 loader = Loader() 

239 obj = loader.get_object_documentation("tests.fixtures.marshmallow.Person") 

240 assert obj.docstring == "Simple Marshmallow Model for a person's information" 

241 assert "marshmallow-model" in obj.properties 

242 name_attr = next(attr for attr in obj.attributes if attr.name == "name") 242 ↛ exitline 242 didn't finish the generator expression on line 242

243 assert name_attr.type == fields.Str 

244 assert name_attr.docstring == "The person's name" 

245 assert "marshmallow-field" in name_attr.properties 

246 assert "required" in name_attr.properties 

247 age_attr = next(attr for attr in obj.attributes if attr.name == "age") 247 ↛ exitline 247 didn't finish the generator expression on line 247

248 assert age_attr.type == fields.Int 

249 assert age_attr.docstring == "The person's age which must be at minimum 18" 

250 assert "marshmallow-field" in age_attr.properties 

251 

252 

253def test_loading_nested_class(): 

254 """Select nested class.""" 

255 loader = Loader() 

256 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.TheNestedClass") 

257 assert obj.docstring == "The nested class docstring." 

258 

259 

260def test_loading_double_nested_class(): 

261 """Select double-nested class.""" 

262 loader = Loader() 

263 obj = loader.get_object_documentation( 

264 "tests.fixtures.the_package.the_module.TheClass.TheNestedClass.TheDoubleNestedClass" 

265 ) 

266 assert obj.docstring == "The double nested class docstring." 

267 

268 

269def test_loading_class_attribute(): 

270 """Select class attribute.""" 

271 loader = Loader() 

272 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.THE_ATTRIBUTE") 

273 assert obj.docstring == "The attribute 0.1 docstring." 

274 

275 

276def test_loading_nested_class_attribute(): 

277 """Select nested-class attribute.""" 

278 loader = Loader() 

279 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.TheNestedClass.THE_ATTRIBUTE") 

280 assert obj.docstring == "The attribute 0.2 docstring." 

281 

282 

283def test_loading_double_nested_class_attribute(): 

284 """Select double-nested-class attribute.""" 

285 loader = Loader() 

286 obj = loader.get_object_documentation( 

287 "tests.fixtures.the_package.the_module.TheClass.TheNestedClass.TheDoubleNestedClass.THE_ATTRIBUTE" 

288 ) 

289 assert obj.docstring == "The attribute 0.3 docstring." 

290 

291 

292def test_loading_class_method(): 

293 """Select class method.""" 

294 loader = Loader() 

295 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_method") 

296 assert obj.docstring == "The method1 docstring." 

297 

298 

299def test_loading_nested_class_method(): 

300 """Select nested class method.""" 

301 loader = Loader() 

302 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.TheNestedClass.the_method") 

303 assert obj.docstring == "The method2 docstring." 

304 

305 

306def test_loading_double_nested_class_method(): 

307 """Select double-nested class method.""" 

308 loader = Loader() 

309 obj = loader.get_object_documentation( 

310 "tests.fixtures.the_package.the_module.TheClass.TheNestedClass.TheDoubleNestedClass.the_method" 

311 ) 

312 assert obj.docstring == "The method3 docstring." 

313 

314 

315def test_loading_staticmethod(): 

316 """Select static method.""" 

317 loader = Loader() 

318 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_static_method") 

319 assert obj.docstring == "The static method docstring." 

320 

321 

322def test_loading_classmethod(): 

323 """Select class method.""" 

324 loader = Loader() 

325 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_class_method") 

326 assert obj.docstring == "The class method docstring." 

327 

328 

329def test_loading_property(): 

330 """Select property.""" 

331 loader = Loader() 

332 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_property") 

333 assert obj.docstring == "The property docstring." 

334 

335 

336def test_loading_writable_property(): 

337 """Select writable property.""" 

338 loader = Loader() 

339 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_writable_property") 

340 assert obj.docstring == "The writable property getter docstring." 

341 

342 

343def test_loading_function(): 

344 """Select function.""" 

345 loader = Loader() 

346 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.the_function") 

347 assert obj.docstring == "The function docstring." 

348 

349 

350def test_loading_attribute(): 

351 """Select attribute.""" 

352 loader = Loader() 

353 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.THE_ATTRIBUTE") 

354 assert obj.docstring == "The attribute docstring." 

355 

356 

357def test_loading_explicit_members(): 

358 """Select members explicitly.""" 

359 loader = Loader() 

360 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module", members={"TheClass"}) 

361 assert len(obj.children) == 1 

362 assert obj.children[0].name == "TheClass" 

363 

364 

365def test_loading_no_members(): 

366 """Select no members.""" 

367 loader = Loader() 

368 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module", members=False) 

369 assert not obj.children 

370 

371 

372def test_loading_with_filters(): 

373 """Select with filters.""" 

374 loader = Loader(filters=["!^[A-Z_]+$"]) 

375 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module") 

376 for child in obj.children: 

377 assert child.name != "THE_ATTRIBUTE" 

378 

379 

380def test_loading_with_filters_reselection(): 

381 """A filter can cancel a previous filter.""" 

382 loader = Loader(filters=["![A-Z_]", "[a-z]"]) 

383 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module") 

384 assert obj.classes 

385 assert obj.classes[0].name == "TheClass" 

386 

387 

388def test_loading_with_members_and_filters(): 

389 """Select members with filters.""" 

390 loader = Loader(filters=["!THE"]) 

391 obj = loader.get_object_documentation( 

392 "tests.fixtures.the_package.the_module", members={"THE_ATTRIBUTE", "TheClass"} 

393 ) 

394 assert obj.attributes 

395 assert obj.attributes[0].name == "THE_ATTRIBUTE" 

396 assert obj.classes 

397 assert obj.classes[0].name == "TheClass" 

398 assert not any(a.name == "THE_ATTRIBUTE" for a in obj.classes[0].attributes) 

399 

400 

401def test_loading_members_set_at_import_time(): 

402 """Select dynamic members.""" 

403 loader = Loader() 

404 obj = loader.get_object_documentation("tests.fixtures.dynamic_members") 

405 assert obj.functions 

406 assert len(obj.classes) == 1 

407 class_ = obj.classes[0] 

408 assert class_.methods 

409 

410 

411def test_loading_inherited_members(): 

412 """Select inherited members.""" 

413 loader = Loader(inherited_members=True) 

414 obj = loader.get_object_documentation("tests.fixtures.inherited_members.Child") 

415 for child_name in ("method1", "method2", "V1", "V2"): 

416 assert child_name in (child.name for child in obj.children) 416 ↛ exitline 416 didn't finish the generator expression on line 416

417 

418 

419def test_not_loading_inherited_members(): 

420 """Do not select inherited members.""" 

421 loader = Loader(inherited_members=False) 

422 obj = loader.get_object_documentation("tests.fixtures.inherited_members.Child") 

423 for child_name in ("method1", "V1"): 

424 assert child_name not in (child.name for child in obj.children) 

425 for child_name in ("method2", "V2"): 

426 assert child_name in (child.name for child in obj.children) 426 ↛ exitline 426 didn't finish the generator expression on line 426

427 

428 

429def test_loading_selected_inherited_members(): 

430 """Select specific members, some of them being inherited.""" 

431 loader = Loader(inherited_members=True) 

432 obj = loader.get_object_documentation("tests.fixtures.inherited_members.Child", members={"V1", "V2"}) 

433 for child_name in ("V1", "V2"): 

434 assert child_name in (child.name for child in obj.children) 434 ↛ exitline 434 didn't finish the generator expression on line 434

435 

436 

437def test_loading_pydantic_inherited_members(): 

438 """Select inherited members in Pydantic models.""" 

439 loader = Loader(inherited_members=True) 

440 obj = loader.get_object_documentation("tests.fixtures.inherited_members.ChildModel") 

441 for child_name in ("a", "b"): 

442 assert child_name in (child.name for child in obj.children) 442 ↛ exitline 442 didn't finish the generator expression on line 442

443 

444 

445def test_not_loading_pydantic_inherited_members(): 

446 """Do not select inherited members in Pydantic models.""" 

447 loader = Loader(inherited_members=False) 

448 obj = loader.get_object_documentation("tests.fixtures.inherited_members.ChildModel") 

449 assert "a" not in (child.name for child in obj.children) 

450 

451 

452def test_loading_wrapped_function(): 

453 """Load documentation for wrapped function, not wrapper.""" 

454 loader = Loader() 

455 obj = loader.get_object_documentation("tests.fixtures.wrapped_objects.my_function") 

456 assert obj.docstring == "My docstring." 

457 

458 

459def test_loading_module_wrapped_members(): 

460 """Load documentation for wrapped function, not wrapper.""" 

461 loader = Loader() 

462 obj = loader.get_object_documentation("tests.fixtures.wrapped_objects") 

463 assert obj.functions and obj.functions[0].docstring == "My docstring." 

464 assert obj.classes and obj.classes[0].methods and obj.classes[0].methods[0].docstring == "Hello!" 

465 

466 

467def test_unwrap_object_with_getattr_method_raising_exception(): 

468 """Try loading an object that defines a `__getattr__` method which raises an exception.""" 

469 loader = Loader() 

470 loader.get_object_documentation("tests.fixtures.unwrap_getattr_raises") 

471 

472 

473def test_loading_coroutine(): 

474 """Load documentation for a coroutine.""" 

475 loader = Loader() 

476 obj = loader.get_object_documentation("tests.fixtures.asyncio.coroutine_function") 

477 assert "async" in obj.properties 

478 

479 

480def test_loading_coroutine_method(): 

481 """Load documentation for a coroutine method.""" 

482 loader = Loader() 

483 obj = loader.get_object_documentation("tests.fixtures.asyncio.ClassContainingCoroutineMethod.coroutine_method") 

484 assert "async" in obj.properties 

485 

486 

487def test_loading_function_without_async_property(): 

488 """Load documentation for a function that is not a coroutine.""" 

489 loader = Loader() 

490 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.the_function") 

491 assert "async" not in obj.properties 

492 

493 

494def test_loading_method_without_async_property(): 

495 """Load documentation for a method that is not a coroutine.""" 

496 loader = Loader() 

497 obj = loader.get_object_documentation("tests.fixtures.the_package.the_module.TheClass.the_method") 

498 assert "async" not in obj.properties 

499 

500 

501def test_inherited_properties_docstrings(): 

502 """Load docstrings from parent class for inherited properties.""" 

503 loader = Loader(new_path_syntax=True) 

504 obj = loader.get_object_documentation("tests.fixtures.inherited_properties:SubClass.read_only") 

505 assert obj.docstring == "SuperClass.read_only docs" 

506 obj = loader.get_object_documentation("tests.fixtures.inherited_properties:SubClass.mutable") 

507 assert obj.docstring == "SuperClass.mutable getter docs" 

508 

509 

510def test_loading_cached_properties(): 

511 """Load cached properties.""" 

512 loader = Loader(new_path_syntax=True) 

513 obj = loader.get_object_documentation("tests.fixtures.cached_properties:C") 

514 assert len(obj.children) == 1 

515 assert obj.children[0].name == obj.children[0].docstring == "aaa" 

516 assert "cached" in obj.children[0].properties 

517 

518 

519def test_method_descriptor(): 

520 """Load a method descriptor.""" 

521 loader = Loader(new_path_syntax=True) 

522 obj = loader.get_object_documentation("tests.fixtures.method_descriptor:descriptor") 

523 assert obj.name == "descriptor" 

524 assert obj.signature 

525 assert len(obj.signature.parameters) == 2 

526 assert obj.docstring 

527 assert obj.category == "method" 

528 

529 

530def test_load_decorated_function(): 

531 """Load a decorated function.""" 

532 loader = Loader(new_path_syntax=True) 

533 obj = loader.get_object_documentation("tests.fixtures.decorated_function") 

534 assert [child.name for child in obj.children] == ["add", "sub"] 

535 for child in obj.children: 

536 assert child.category == "function" 

537 assert child.parent is child.root 

538 assert child.parent.name == "decorated_function"