Coverage for tests/test_loader.py: 96.23%

331 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-09 18:24 +0100

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

2 

3import os 

4import sys 

5from pathlib import Path 

6 

7import pytest 

8from django.db.models.fields import CharField 

9from marshmallow import fields 

10 

11from pytkdocs.loader import Loader, get_object_tree 

12from tests import FIXTURES_DIR 

13 

14 

15def test_import_no_path() -> None: 

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

17 with pytest.raises(ValueError): # noqa: PT011 

18 get_object_tree("") 

19 

20 

21def test_import_error() -> None: 

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

23 with pytest.raises(ImportError): 

24 get_object_tree("eeeeeeeeeeeeeeeeeee") 

25 

26 

27def test_can_find_class_real_path() -> None: 

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

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

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

31 

32 

33def test_can_find_class_method_real_path() -> None: 

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

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

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

37 

38 

39def test_can_find_class_attribute_real_path() -> None: 

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

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

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

43 

44 

45def test_cannot_find_module_attribute_real_path() -> None: 

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

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

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

49 

50 

51def test_import_module_with_colon_path_syntax() -> None: 

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

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

54 

55 

56def test_import_attribute_with_colon_path_syntax() -> None: 

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

58 get_object_tree("tests.fixtures.the_package.the_module:THE_ATTRIBUTE") 

59 

60 

61def test_import_nested_attribute_with_colon_path_syntax() -> None: 

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

63 get_object_tree("tests.fixtures.the_package.the_module:TheClass.THE_ATTRIBUTE") 

64 

65 

66def test_fail_to_import_module_with_colon_path_syntax() -> None: 

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

68 with pytest.raises(ImportError): 

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

70 

71 

72def test_fail_to_import_attribute_with_colon_path_syntax() -> None: 

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

74 with pytest.raises(AttributeError): 

75 get_object_tree("tests.fixtures.the_package.the_module:does_not_exist") 

76 

77 

78def test_fail_to_import_nested_attribute_with_colon_path_syntax() -> None: 

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

80 with pytest.raises(AttributeError): 

81 get_object_tree("tests.fixtures.the_package.the_module:TheClass.does_not_exist") 

82 

83 

84def test_fail_to_import_module_with_dot_path_syntax() -> None: 

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

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

87 get_object_tree("does_not_exist") 

88 

89 

90def test_fail_to_import_attribute_with_dot_path_syntax() -> None: 

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

92 with pytest.raises(AttributeError): 

93 get_object_tree("tests.fixtures.the_package.the_module.does_not_exist") 

94 

95 

96def test_fail_to_import_nested_attribute_with_dot_path_syntax() -> None: 

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

98 with pytest.raises(AttributeError): 

99 get_object_tree("tests.fixtures.the_package.the_module.TheClass.does_not_exist") 

100 

101 

102def test_inheriting_enum_Enum() -> None: # noqa: N802 

103 """Handle `enum.Enum` classes.""" 

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

105 loader = Loader() 

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

107 assert not loader.errors 

108 

109 

110def test_inheriting_typing_NamedTuple() -> None: # noqa: N802 

111 """Handle `typing.NamedTuple classes`. 

112 

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

114 """ 

115 loader = Loader() 

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

117 assert len(loader.errors) == 0 

118 

119 

120def test_nested_class() -> None: 

121 """Handle nested classes.""" 

122 loader = Loader() 

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

124 assert obj.classes 

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

126 assert obj.classes[0].classes 

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

128 

129 

130def test_loading_deep_package() -> None: 

131 """Handle deep nesting of packages.""" 

132 loader = Loader() 

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

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

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

136 

137 

138def test_loading_package() -> None: 

139 """Handle basic packages.""" 

140 loader = Loader() 

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

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

143 

144 

145def test_loading_namespace_package() -> None: 

146 """Handle native namespace packages.""" 

147 loader = Loader() 

148 old_paths = list(sys.path) 

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

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

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

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

153 sys.path = old_paths 

154 

155 

156def test_loading_module() -> None: 

157 """Handle single modules.""" 

158 loader = Loader() 

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

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

161 

162 

163def test_loading_class() -> None: 

164 """Handle classes.""" 

165 loader = Loader() 

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

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

168 assert obj.bases == ["object"] # type: ignore[attr-defined] 

169 

170 

171def test_loading_class_with_multiline_docstring_starting_on_first_line() -> None: 

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

173 loader = Loader() 

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

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

176 

177 

178def test_loading_dataclass() -> None: 

179 """Handle dataclasses.""" 

180 loader = Loader() 

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

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

183 assert len(obj.attributes) == 2 

184 name_attr = next(attr for attr in obj.attributes if attr.name == "name") 

185 assert name_attr.type is str 

186 age_attr = next(attr for attr in obj.attributes if attr.name == "age") 

187 assert age_attr.type is int 

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

189 assert "dataclass" in obj.properties 

190 

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

192 assert "dataclass" not in not_dataclass.properties 

193 

194 

195def test_loading_empty_dataclass() -> None: 

196 """Handle empty dataclasses.""" 

197 loader = Loader() 

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

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

200 assert len(obj.attributes) == 0 

201 assert "dataclass" in obj.properties 

202 

203 

204@pytest.mark.xfail(reason="Probable change in Pydantic since v1") 

205def test_loading_pydantic_model() -> None: 

206 """Handle Pydantic models.""" 

207 loader = Loader() 

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

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

210 assert "pydantic-model" in obj.properties 

211 name_attr = next(attr for attr in obj.attributes if attr.name == "name") 

212 assert name_attr.type is str 

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

214 assert "pydantic-field" in name_attr.properties 

215 age_attr = next(attr for attr in obj.attributes if attr.name == "age") 

216 assert age_attr.type is int 

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

218 assert "pydantic-field" in age_attr.properties 

219 labels_attr = next(attr for attr in obj.attributes if attr.name == "labels") 

220 assert labels_attr.type == set[str] 

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

222 assert "pydantic-field" in labels_attr.properties 

223 

224 

225def test_loading_django_model() -> None: 

226 """Handle Django models.""" 

227 loader = Loader() 

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

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

230 name_attr = next(attr for attr in obj.attributes if attr.name == "name") 

231 assert name_attr.type == CharField 

232 assert name_attr.docstring == "Name" 

233 

234 

235@pytest.mark.xfail( 

236 reason="Change in marshmallow: Passing field metadata as keyword arguments is deprecated. " 

237 "Use the explicit `metadata=...` argument instead.", 

238) 

239def test_loading_marshmallow_model() -> None: 

240 """Handle Marshmallow models.""" 

241 loader = Loader() 

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

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

244 assert "marshmallow-model" in obj.properties 

245 name_attr = next(attr for attr in obj.attributes if attr.name == "name") 

246 assert name_attr.type == fields.Str 

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

248 assert "marshmallow-field" in name_attr.properties 

249 assert "required" in name_attr.properties 

250 age_attr = next(attr for attr in obj.attributes if attr.name == "age") 

251 assert age_attr.type == fields.Int 

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

253 assert "marshmallow-field" in age_attr.properties 

254 

255 

256def test_loading_nested_class() -> None: 

257 """Select nested class.""" 

258 loader = Loader() 

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

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

261 

262 

263def test_loading_double_nested_class() -> None: 

264 """Select double-nested class.""" 

265 loader = Loader() 

266 obj = loader.get_object_documentation( 

267 "tests.fixtures.the_package.the_module.TheClass.TheNestedClass.TheDoubleNestedClass", 

268 ) 

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

270 

271 

272def test_loading_class_attribute() -> None: 

273 """Select class attribute.""" 

274 loader = Loader() 

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

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

277 

278 

279def test_loading_nested_class_attribute() -> None: 

280 """Select nested-class attribute.""" 

281 loader = Loader() 

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

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

284 

285 

286def test_loading_double_nested_class_attribute() -> None: 

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

288 loader = Loader() 

289 obj = loader.get_object_documentation( 

290 "tests.fixtures.the_package.the_module.TheClass.TheNestedClass.TheDoubleNestedClass.THE_ATTRIBUTE", 

291 ) 

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

293 

294 

295def test_loading_class_method() -> None: 

296 """Select class method.""" 

297 loader = Loader() 

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

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

300 

301 

302def test_loading_nested_class_method() -> None: 

303 """Select nested class method.""" 

304 loader = Loader() 

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

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

307 

308 

309def test_loading_double_nested_class_method() -> None: 

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

311 loader = Loader() 

312 obj = loader.get_object_documentation( 

313 "tests.fixtures.the_package.the_module.TheClass.TheNestedClass.TheDoubleNestedClass.the_method", 

314 ) 

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

316 

317 

318def test_loading_staticmethod() -> None: 

319 """Select static method.""" 

320 loader = Loader() 

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

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

323 

324 

325def test_loading_classmethod() -> None: 

326 """Select class method.""" 

327 loader = Loader() 

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

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

330 

331 

332def test_loading_property() -> None: 

333 """Select property.""" 

334 loader = Loader() 

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

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

337 

338 

339def test_loading_writable_property() -> None: 

340 """Select writable property.""" 

341 loader = Loader() 

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

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

344 

345 

346def test_loading_function() -> None: 

347 """Select function.""" 

348 loader = Loader() 

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

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

351 

352 

353def test_loading_attribute() -> None: 

354 """Select attribute.""" 

355 loader = Loader() 

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

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

358 

359 

360def test_loading_explicit_members() -> None: 

361 """Select members explicitly.""" 

362 loader = Loader() 

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

364 assert len(obj.children) == 1 

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

366 

367 

368def test_loading_no_members() -> None: 

369 """Select no members.""" 

370 loader = Loader() 

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

372 assert not obj.children 

373 

374 

375def test_loading_with_filters() -> None: 

376 """Select with filters.""" 

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

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

379 for child in obj.children: 

380 assert child.name != "THE_ATTRIBUTE" 

381 

382 

383def test_loading_with_filters_reselection() -> None: 

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

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

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

387 assert obj.classes 

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

389 

390 

391def test_loading_with_members_and_filters() -> None: 

392 """Select members with filters.""" 

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

394 obj = loader.get_object_documentation( 

395 "tests.fixtures.the_package.the_module", 

396 members={"THE_ATTRIBUTE", "TheClass"}, 

397 ) 

398 assert obj.attributes 

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

400 assert obj.classes 

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

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

403 

404 

405def test_loading_members_set_at_import_time() -> None: 

406 """Select dynamic members.""" 

407 loader = Loader() 

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

409 assert obj.functions 

410 assert len(obj.classes) == 1 

411 class_ = obj.classes[0] 

412 assert class_.methods 

413 

414 

415def test_loading_inherited_members() -> None: 

416 """Select inherited members.""" 

417 loader = Loader(inherited_members=True) 

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

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

420 assert child_name in (child.name for child in obj.children) 

421 

422 

423def test_not_loading_inherited_members() -> None: 

424 """Do not select inherited members.""" 

425 loader = Loader(inherited_members=False) 

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

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

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

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

430 assert child_name in (child.name for child in obj.children) 

431 

432 

433def test_loading_selected_inherited_members() -> None: 

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

435 loader = Loader(inherited_members=True) 

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

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

438 assert child_name in (child.name for child in obj.children) 

439 

440 

441@pytest.mark.xfail(reason="Probable change in Pydantic since v1") 

442def test_loading_pydantic_inherited_members() -> None: 

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

444 loader = Loader(inherited_members=True) 

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

446 for child_name in ("a", "b"): 446 ↛ exitline 446 didn't return from function 'test_loading_pydantic_inherited_members' because the loop on line 446 didn't complete

447 assert child_name in (child.name for child in obj.children) 

448 

449 

450def test_not_loading_pydantic_inherited_members() -> None: 

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

452 loader = Loader(inherited_members=False) 

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

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

455 

456 

457def test_loading_wrapped_function() -> None: 

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

459 loader = Loader() 

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

461 assert obj.docstring == "My docstring." 

462 

463 

464def test_loading_module_wrapped_members() -> None: 

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

466 loader = Loader() 

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

468 assert obj.functions 

469 assert obj.functions[0].docstring == "My docstring." 

470 assert obj.classes 

471 assert obj.classes[0].methods 

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

473 

474 

475def test_unwrap_object_with_getattr_method_raising_exception() -> None: 

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

477 loader = Loader() 

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

479 

480 

481def test_loading_coroutine() -> None: 

482 """Load documentation for a coroutine.""" 

483 loader = Loader() 

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

485 assert "async" in obj.properties 

486 

487 

488def test_loading_coroutine_method() -> None: 

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

490 loader = Loader() 

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

492 assert "async" in obj.properties 

493 

494 

495def test_loading_function_without_async_property() -> None: 

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

497 loader = Loader() 

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

499 assert "async" not in obj.properties 

500 

501 

502def test_loading_method_without_async_property() -> None: 

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

504 loader = Loader() 

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

506 assert "async" not in obj.properties 

507 

508 

509def test_inherited_properties_docstrings() -> None: 

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

511 loader = Loader(new_path_syntax=True) 

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

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

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

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

516 

517 

518def test_loading_cached_properties() -> None: 

519 """Load cached properties.""" 

520 loader = Loader(new_path_syntax=True) 

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

522 assert len(obj.children) == 1 

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

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

525 

526 

527def test_method_descriptor() -> None: 

528 """Load a method descriptor.""" 

529 loader = Loader(new_path_syntax=True) 

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

531 assert obj.name == "descriptor" 

532 assert obj.signature # type: ignore[attr-defined] 

533 assert len(obj.signature.parameters) == 2 # type: ignore[attr-defined] 

534 assert obj.docstring 

535 assert obj.category == "method" 

536 

537 

538def test_load_decorated_function() -> None: 

539 """Load a decorated function.""" 

540 loader = Loader(new_path_syntax=True) 

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

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

543 for child in obj.children: 

544 assert child.category == "function" 

545 assert child.parent is child.root 

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