1import io 2import os 3import pathlib 4import re 5import shutil 6import sys 7from unittest.mock import patch, Mock, call 8 9import pytest 10import sphinx 11from sphinx.application import Sphinx 12import sphinx.util.logging 13 14from autoapi.mappers.python import ( 15 PythonModule, 16 PythonFunction, 17 PythonClass, 18 PythonData, 19 PythonMethod, 20) 21import autoapi.settings 22 23 24def rebuild(confoverrides=None, confdir=".", **kwargs): 25 app = Sphinx( 26 srcdir=".", 27 confdir=confdir, 28 outdir="_build/text", 29 doctreedir="_build/.doctrees", 30 buildername="text", 31 confoverrides=confoverrides, 32 **kwargs 33 ) 34 app.build() 35 36 37@pytest.fixture(scope="class") 38def builder(): 39 cwd = os.getcwd() 40 41 def build(test_dir, confoverrides=None, **kwargs): 42 os.chdir("tests/python/{0}".format(test_dir)) 43 rebuild(confoverrides=confoverrides, **kwargs) 44 45 yield build 46 47 try: 48 shutil.rmtree("_build") 49 finally: 50 os.chdir(cwd) 51 52 53class TestSimpleModule: 54 @pytest.fixture(autouse=True, scope="class") 55 def built(self, builder): 56 builder( 57 "pyexample", 58 warningiserror=True, 59 confoverrides={"suppress_warnings": ["app"]}, 60 ) 61 62 def test_integration(self): 63 self.check_integration("_build/text/autoapi/example/index.txt") 64 65 def test_manual_directives(self): 66 example_path = "_build/text/manualapi.txt" 67 # The manual directives should contain the same information 68 self.check_integration(example_path) 69 70 with io.open(example_path, encoding="utf8") as example_handle: 71 example_file = example_handle.read() 72 73 assert "@example.decorator_okay" in example_file 74 75 def check_integration(self, example_path): 76 with io.open(example_path, encoding="utf8") as example_handle: 77 example_file = example_handle.read() 78 79 assert "class example.Foo" in example_file 80 assert "class Meta" in example_file 81 assert "attr2" in example_file 82 assert "This is the docstring of an instance attribute." in example_file 83 assert "method_okay(self, foo=None, bar=None)" in example_file 84 assert "method_multiline(self, foo=None, bar=None, baz=None)" in example_file 85 assert "method_tricky(self, foo=None, bar=dict(foo=1, bar=2))" in example_file 86 87 # Are constructor arguments from the class docstring parsed? 88 assert "Set an attribute" in example_file 89 90 # "self" should not be included in constructor arguments 91 assert "Foo(self" not in example_file 92 93 # Overridden methods without their own docstring 94 # should inherit the parent's docstring 95 assert example_file.count("This method should parse okay") == 2 96 97 assert not os.path.exists("_build/text/autoapi/method_multiline") 98 99 # Inherited constructor docstrings should be included in a merged 100 # (autoapi_python_class_content="both") class docstring only once. 101 assert example_file.count("One __init__.") == 3 102 103 index_path = "_build/text/index.txt" 104 with io.open(index_path, encoding="utf8") as index_handle: 105 index_file = index_handle.read() 106 107 assert "API Reference" in index_file 108 109 assert "Foo" in index_file 110 assert "Meta" in index_file 111 112 def test_napoleon_integration_not_loaded(self, builder): 113 example_path = "_build/text/autoapi/example/index.txt" 114 with io.open(example_path, encoding="utf8") as example_handle: 115 example_file = example_handle.read() 116 117 # Check that docstrings are not transformed without napoleon loaded 118 assert "Args" in example_file 119 120 assert "Returns" in example_file 121 122 def test_show_inheritance(self, builder): 123 example_path = "_build/text/autoapi/example/index.txt" 124 with io.open(example_path, encoding="utf8") as example_handle: 125 example_file = example_handle.read() 126 127 assert "Bases:" in example_file 128 129 130class TestMovedConfPy(TestSimpleModule): 131 @pytest.fixture(autouse=True, scope="class") 132 def built(self, builder): 133 builder( 134 "pymovedconfpy", 135 confdir="confpy", 136 warningiserror=True, 137 confoverrides={"suppress_warnings": ["app"]}, 138 ) 139 140 141class TestSimpleModuleDifferentPrimaryDomain: 142 @pytest.fixture(autouse=True, scope="class") 143 def built(self, builder): 144 builder( 145 "pyexample", 146 warningiserror=True, 147 confoverrides={ 148 "autoapi_options": [ 149 "members", 150 "undoc-members", 151 "private-members", 152 "special-members", 153 "imported-members", 154 ], 155 "primary_domain": "cpp", 156 "suppress_warnings": ["app"], 157 }, 158 ) 159 160 def test_success(self): 161 pass 162 163 164class TestSimpleStubModule: 165 @pytest.fixture(autouse=True, scope="class") 166 def built(self, builder): 167 builder("pyiexample") 168 169 def test_integration(self): 170 example_path = "_build/text/autoapi/example/index.txt" 171 with io.open(example_path, encoding="utf8") as example_handle: 172 example_file = example_handle.read() 173 174 # Are pyi files preferred 175 assert "DoNotFindThis" not in example_file 176 177 assert "class example.Foo" in example_file 178 assert "class Meta" in example_file 179 assert "Another class var docstring" in example_file 180 assert "A class var without a value." in example_file 181 assert "method_okay(self, foo=None, bar=None)" in example_file 182 assert "method_multiline(self, foo=None, bar=None, baz=None)" in example_file 183 assert "method_without_docstring(self)" in example_file 184 185 # Are constructor arguments from the class docstring parsed? 186 assert "Set an attribute" in example_file 187 188 189class TestSimpleStubModuleNotPreferred: 190 @pytest.fixture(autouse=True, scope="class") 191 def built(self, builder): 192 builder("pyiexample2") 193 194 def test_integration(self): 195 example_path = "_build/text/autoapi/example/index.txt" 196 with io.open(example_path, encoding="utf8") as example_handle: 197 example_file = example_handle.read() 198 199 # Are py files preferred 200 assert "DoNotFindThis" not in example_file 201 202 assert "Foo" in example_file 203 204 205class TestPy3Module: 206 @pytest.fixture(autouse=True, scope="class") 207 def built(self, builder): 208 builder("py3example") 209 210 def test_integration(self): 211 example_path = "_build/text/autoapi/example/index.txt" 212 with io.open(example_path, encoding="utf8") as example_handle: 213 example_file = example_handle.read() 214 215 assert "Initialize self" not in example_file 216 assert "a new type" not in example_file 217 218 def test_annotations(self): 219 example_path = "_build/text/autoapi/example/index.txt" 220 with io.open(example_path, encoding="utf8") as example_handle: 221 example_file = example_handle.read() 222 223 assert "software = sphinx" in example_file 224 assert "code_snippet = Multiline-String" in example_file 225 226 assert "max_rating :int = 10" in example_file 227 assert "is_valid" in example_file 228 229 assert "ratings" in example_file 230 assert "List[int]" in example_file 231 232 assert "Dict[int, str]" in example_file 233 234 assert "start: int" in example_file 235 assert "Iterable[int]" in example_file 236 237 assert "List[Union[str, int]]" in example_file 238 239 assert "not_yet_a: A" in example_file 240 assert "imported: example2.B" in example_file 241 assert "-> example2.B" in example_file 242 243 assert "is_an_a" in example_file 244 assert "ClassVar" in example_file 245 246 assert "instance_var" in example_file 247 248 assert "global_a :A" in example_file 249 250 assert "my_method(self) -> str" in example_file 251 252 assert "class example.SomeMetaclass" in example_file 253 254 def test_overload(self): 255 example_path = "_build/text/autoapi/example/index.txt" 256 with io.open(example_path, encoding="utf8") as example_handle: 257 example_file = example_handle.read() 258 259 assert "overloaded_func(a: float" in example_file 260 assert "overloaded_func(a: str" in example_file 261 assert "overloaded_func(a: Union" not in example_file 262 assert "Overloaded function" in example_file 263 264 assert "overloaded_method(self, a: float" in example_file 265 assert "overloaded_method(self, a: str" in example_file 266 assert "overloaded_method(self, a: Union" not in example_file 267 assert "Overloaded method" in example_file 268 269 assert "overloaded_class_method(cls, a: float" in example_file 270 assert "overloaded_class_method(cls, a: str" in example_file 271 assert "overloaded_class_method(cls, a: Union" not in example_file 272 assert "Overloaded method" in example_file 273 274 assert "undoc_overloaded_func" in example_file 275 assert "undoc_overloaded_method" in example_file 276 277 assert "C(a: int" in example_file 278 assert "C(a: float" in example_file 279 assert "C(a: str" not in example_file 280 assert "C(self, a: int" not in example_file 281 assert "C(self, a: float" not in example_file 282 assert "C(self, a: str" not in example_file 283 284 def test_async(self): 285 example_path = "_build/text/autoapi/example/index.txt" 286 with io.open(example_path, encoding="utf8") as example_handle: 287 example_file = example_handle.read() 288 289 assert "async async_method" in example_file 290 assert "async example.async_function" in example_file 291 292 293def test_py3_hiding_undoc_overloaded_members(builder): 294 confoverrides = {"autoapi_options": ["members", "special-members"]} 295 builder("py3example", confoverrides=confoverrides) 296 297 example_path = "_build/text/autoapi/example/index.txt" 298 with io.open(example_path, encoding="utf8") as example_handle: 299 example_file = example_handle.read() 300 301 assert "overloaded_func" in example_file 302 assert "overloaded_method" in example_file 303 assert "undoc_overloaded_func" not in example_file 304 assert "undoc_overloaded_method" not in example_file 305 306 307class TestAnnotationCommentsModule: 308 @pytest.fixture(autouse=True, scope="class") 309 def built(self, builder): 310 builder("pyannotationcommentsexample") 311 312 def test_integration(self): 313 example_path = "_build/text/autoapi/example/index.txt" 314 with io.open(example_path, encoding="utf8") as example_handle: 315 example_file = example_handle.read() 316 317 assert "max_rating :int = 10" in example_file 318 319 assert "ratings" in example_file 320 assert "List[int]" in example_file 321 322 assert "Dict[int, str]" in example_file 323 324 # When astroid>2.2.5 325 # assert "start: int" in example_file 326 # assert "end: int" in example_file 327 assert "Iterable[int]" in example_file 328 329 assert "List[Union[str, int]]" in example_file 330 331 assert "not_yet_a: A" in example_file 332 assert "is_an_a" in example_file 333 assert "ClassVar" in example_file 334 335 assert "instance_var" in example_file 336 337 assert "global_a :A" in example_file 338 339 340@pytest.mark.skipif( 341 sys.version_info < (3, 8), reason="Positional only arguments need Python >=3.8" 342) 343class TestPositionalOnlyArgumentsModule: 344 @pytest.fixture(autouse=True, scope="class") 345 def built(self, builder): 346 builder("py38positionalparams") 347 348 def test_integration(self): 349 example_path = "_build/text/autoapi/example/index.txt" 350 with io.open(example_path, encoding="utf8") as example_handle: 351 example_file = example_handle.read() 352 353 assert "f_simple(a, b, /, c, d, *, e, f)" in example_file 354 355 assert ( 356 "f_comment(a: int, b: int, /, c: Optional[int], d: Optional[int], *, e: float, f: float)" 357 in example_file 358 ) 359 assert ( 360 "f_annotation(a: int, b: int, /, c: Optional[int], d: Optional[int], *, e: float, f: float)" 361 in example_file 362 ) 363 assert ( 364 "f_arg_comment(a: int, b: int, /, c: Optional[int], d: Optional[int], *, e: float, f: float)" 365 in example_file 366 ) 367 assert "f_no_cd(a: int, b: int, /, *, e: float, f: float)" in example_file 368 369 370def test_napoleon_integration_loaded(builder): 371 confoverrides = { 372 "extensions": ["autoapi.extension", "sphinx.ext.autodoc", "sphinx.ext.napoleon"] 373 } 374 375 builder("pyexample", confoverrides=confoverrides) 376 377 example_path = "_build/text/autoapi/example/index.txt" 378 with io.open(example_path, encoding="utf8") as example_handle: 379 example_file = example_handle.read() 380 381 assert "Parameters" in example_file 382 383 assert "Return type" in example_file 384 385 assert "Args" not in example_file 386 387 388class TestSimplePackage: 389 @pytest.fixture(autouse=True, scope="class") 390 def built(self, builder): 391 builder("pypackageexample") 392 393 def test_integration_with_package(self): 394 395 example_path = "_build/text/autoapi/example/index.txt" 396 with io.open(example_path, encoding="utf8") as example_handle: 397 example_file = example_handle.read() 398 399 assert "example.foo" in example_file 400 assert "example.module_level_method(foo, bar)" in example_file 401 402 example_foo_path = "_build/text/autoapi/example/foo/index.txt" 403 with io.open(example_foo_path, encoding="utf8") as example_foo_handle: 404 example_foo_file = example_foo_handle.read() 405 406 assert "class example.foo.Foo" in example_foo_file 407 assert "method_okay(self, foo=None, bar=None)" in example_foo_file 408 409 index_path = "_build/text/index.txt" 410 with io.open(index_path, encoding="utf8") as index_handle: 411 index_file = index_handle.read() 412 413 assert "API Reference" in index_file 414 assert "example.foo" in index_file 415 assert "Foo" in index_file 416 assert "module_level_method" in index_file 417 418 419def test_simple_no_false_warnings(builder, caplog): 420 logger = sphinx.util.logging.getLogger("autoapi") 421 logger.logger.addHandler(caplog.handler) 422 builder("pypackageexample") 423 424 assert "Cannot resolve" not in caplog.text 425 426 427def _test_class_content(builder, class_content): 428 confoverrides = {"autoapi_python_class_content": class_content} 429 430 builder("pyexample", confoverrides=confoverrides) 431 432 example_path = "_build/text/autoapi/example/index.txt" 433 with io.open(example_path, encoding="utf8") as example_handle: 434 example_file = example_handle.read() 435 436 if class_content == "init": 437 assert "Can we parse arguments" not in example_file 438 else: 439 assert "Can we parse arguments" in example_file 440 441 if class_content not in ("both", "init"): 442 assert "Constructor docstring" not in example_file 443 else: 444 assert "Constructor docstring" in example_file 445 446 447def test_class_class_content(builder): 448 _test_class_content(builder, "class") 449 450 451def test_both_class_content(builder): 452 _test_class_content(builder, "both") 453 454 455def test_init_class_content(builder): 456 _test_class_content(builder, "init") 457 458 459def test_hiding_private_members(builder): 460 confoverrides = {"autoapi_options": ["members", "undoc-members", "special-members"]} 461 builder("pypackageexample", confoverrides=confoverrides) 462 463 example_path = "_build/text/autoapi/example/index.txt" 464 with io.open(example_path, encoding="utf8") as example_handle: 465 example_file = example_handle.read() 466 467 assert "private" not in example_file 468 469 private_path = "_build/text/autoapi/example/_private_module/index.txt" 470 with io.open(private_path, encoding="utf8") as private_handle: 471 private_file = private_handle.read() 472 473 assert "public_method" in private_file 474 475 476def test_hiding_inheritance(builder): 477 confoverrides = {"autoapi_options": ["members", "undoc-members", "special-members"]} 478 builder("pyexample", confoverrides=confoverrides) 479 480 example_path = "_build/text/autoapi/example/index.txt" 481 with io.open(example_path, encoding="utf8") as example_handle: 482 example_file = example_handle.read() 483 484 assert "Bases:" not in example_file 485 486 487def test_hiding_imported_members(builder): 488 confoverrides = {"autoapi_options": ["members", "undoc-members"]} 489 builder("pypackagecomplex", confoverrides=confoverrides) 490 491 subpackage_path = "_build/text/autoapi/complex/subpackage/index.txt" 492 with io.open(subpackage_path, encoding="utf8") as subpackage_handle: 493 subpackage_file = subpackage_handle.read() 494 495 assert "Part of a public resolution chain." not in subpackage_file 496 497 package_path = "_build/text/autoapi/complex/index.txt" 498 with io.open(package_path, encoding="utf8") as package_handle: 499 package_file = package_handle.read() 500 501 assert "Part of a public resolution chain." not in package_file 502 503 submodule_path = "_build/text/autoapi/complex/subpackage/submodule/index.txt" 504 with io.open(submodule_path, encoding="utf8") as submodule_handle: 505 submodule_file = submodule_handle.read() 506 507 assert "A private function made public by import." not in submodule_file 508 509 510def test_inherited_members(builder): 511 confoverrides = { 512 "autoapi_options": ["members", "inherited-members", "undoc-members"] 513 } 514 builder("pyexample", confoverrides=confoverrides) 515 516 example_path = "_build/text/autoapi/example/index.txt" 517 with io.open(example_path, encoding="utf8") as example_handle: 518 example_file = example_handle.read() 519 520 assert "class example.Bar" in example_file 521 i = example_file.index("class example.Bar") 522 assert "method_okay" in example_file[i:] 523 524 525def test_skipping_members(builder): 526 builder("pyskipexample") 527 528 example_path = "_build/text/autoapi/example/index.txt" 529 with io.open(example_path, encoding="utf8") as example_handle: 530 example_file = example_handle.read() 531 532 assert "foo doc" not in example_file 533 assert "bar doc" not in example_file 534 assert "bar m doc" not in example_file 535 assert "baf doc" in example_file 536 assert "baf m doc" not in example_file 537 assert "baz doc" not in example_file 538 assert "not ignored" in example_file 539 540 541@pytest.mark.parametrize( 542 "value,order", 543 [ 544 ("bysource", [".Foo", ".decorator_okay", ".Bar"]), 545 ("alphabetical", [".Bar", ".Foo", ".decorator_okay"]), 546 ("groupwise", [".Bar", ".Foo", ".decorator_okay"]), 547 ], 548) 549def test_order_members(builder, value, order): 550 confoverrides = {"autoapi_member_order": value} 551 builder("pyexample", confoverrides=confoverrides) 552 553 example_path = "_build/text/autoapi/example/index.txt" 554 with io.open(example_path, encoding="utf8") as example_handle: 555 example_file = example_handle.read() 556 557 indexes = [example_file.index(name) for name in order] 558 assert indexes == sorted(indexes) 559 560 561class _CompareInstanceType: 562 def __init__(self, type_): 563 self.type = type_ 564 565 def __eq__(self, other): 566 return self.type is type(other) 567 568 def __repr__(self): 569 return "<expect type {}>".format(self.type.__name__) 570 571 572def test_skip_members_hook(builder): 573 emit_firstresult_patch = Mock(name="emit_firstresult_patch", return_value=False) 574 with patch("sphinx.application.Sphinx.emit_firstresult", emit_firstresult_patch): 575 builder("pyskipexample") 576 577 options = ["members", "undoc-members", "special-members"] 578 579 mock_calls = [ 580 call( 581 "autoapi-skip-member", 582 "module", 583 "example", 584 _CompareInstanceType(PythonModule), 585 False, 586 options, 587 ), 588 call( 589 "autoapi-skip-member", 590 "function", 591 "example.foo", 592 _CompareInstanceType(PythonFunction), 593 False, 594 options, 595 ), 596 call( 597 "autoapi-skip-member", 598 "class", 599 "example.Bar", 600 _CompareInstanceType(PythonClass), 601 False, 602 options, 603 ), 604 call( 605 "autoapi-skip-member", 606 "class", 607 "example.Baf", 608 _CompareInstanceType(PythonClass), 609 False, 610 options, 611 ), 612 call( 613 "autoapi-skip-member", 614 "data", 615 "example.baz", 616 _CompareInstanceType(PythonData), 617 False, 618 options, 619 ), 620 call( 621 "autoapi-skip-member", 622 "data", 623 "example.anchor", 624 _CompareInstanceType(PythonData), 625 False, 626 options, 627 ), 628 call( 629 "autoapi-skip-member", 630 "method", 631 "example.Bar.m", 632 _CompareInstanceType(PythonMethod), 633 False, 634 options, 635 ), 636 call( 637 "autoapi-skip-member", 638 "method", 639 "example.Baf.m", 640 _CompareInstanceType(PythonMethod), 641 False, 642 options, 643 ), 644 ] 645 for mock_call in mock_calls: 646 assert mock_call in emit_firstresult_patch.mock_calls 647 648 649class TestComplexPackage: 650 @pytest.fixture(autouse=True, scope="class") 651 def built(self, builder): 652 builder("pypackagecomplex") 653 654 def test_public_chain_resolves(self): 655 submodule_path = "_build/text/autoapi/complex/subpackage/submodule/index.txt" 656 with io.open(submodule_path, encoding="utf8") as submodule_handle: 657 submodule_file = submodule_handle.read() 658 659 assert "Part of a public resolution chain." in submodule_file 660 661 subpackage_path = "_build/text/autoapi/complex/subpackage/index.txt" 662 with io.open(subpackage_path, encoding="utf8") as subpackage_handle: 663 subpackage_file = subpackage_handle.read() 664 665 assert "Part of a public resolution chain." in subpackage_file 666 667 package_path = "_build/text/autoapi/complex/index.txt" 668 with io.open(package_path, encoding="utf8") as package_handle: 669 package_file = package_handle.read() 670 671 assert "Part of a public resolution chain." in package_file 672 673 def test_private_made_public(self): 674 submodule_path = "_build/text/autoapi/complex/subpackage/submodule/index.txt" 675 with io.open(submodule_path, encoding="utf8") as submodule_handle: 676 submodule_file = submodule_handle.read() 677 678 assert "A private function made public by import." in submodule_file 679 680 def test_multiple_import_locations(self): 681 submodule_path = "_build/text/autoapi/complex/subpackage/submodule/index.txt" 682 with io.open(submodule_path, encoding="utf8") as submodule_handle: 683 submodule_file = submodule_handle.read() 684 685 assert "A public function imported in multiple places." in submodule_file 686 687 subpackage_path = "_build/text/autoapi/complex/subpackage/index.txt" 688 with io.open(subpackage_path, encoding="utf8") as subpackage_handle: 689 subpackage_file = subpackage_handle.read() 690 691 assert "A public function imported in multiple places." in subpackage_file 692 693 package_path = "_build/text/autoapi/complex/index.txt" 694 with io.open(package_path, encoding="utf8") as package_handle: 695 package_file = package_handle.read() 696 697 assert "A public function imported in multiple places." in package_file 698 699 def test_simple_wildcard_imports(self): 700 wildcard_path = "_build/text/autoapi/complex/wildcard/index.txt" 701 with io.open(wildcard_path, encoding="utf8") as wildcard_handle: 702 wildcard_file = wildcard_handle.read() 703 704 assert "public_chain" in wildcard_file 705 assert "now_public_function" in wildcard_file 706 assert "public_multiple_imports" in wildcard_file 707 assert "module_level_method" in wildcard_file 708 709 def test_wildcard_chain(self): 710 wildcard_path = "_build/text/autoapi/complex/wildchain/index.txt" 711 with io.open(wildcard_path, encoding="utf8") as wildcard_handle: 712 wildcard_file = wildcard_handle.read() 713 714 assert "public_chain" in wildcard_file 715 assert "module_level_method" in wildcard_file 716 717 def test_wildcard_all_imports(self): 718 wildcard_path = "_build/text/autoapi/complex/wildall/index.txt" 719 with io.open(wildcard_path, encoding="utf8") as wildcard_handle: 720 wildcard_file = wildcard_handle.read() 721 722 assert "not_all" not in wildcard_file 723 assert "NotAllClass" not in wildcard_file 724 assert "does_not_exist" not in wildcard_file 725 assert "SimpleClass" in wildcard_file 726 assert "simple_function" in wildcard_file 727 assert "public_chain" in wildcard_file 728 assert "module_level_method" in wildcard_file 729 730 def test_no_imports_in_module_with_all(self): 731 foo_path = "_build/text/autoapi/complex/foo/index.txt" 732 with io.open(foo_path, encoding="utf8") as foo_handle: 733 foo_file = foo_handle.read() 734 735 assert "module_level_method" not in foo_file 736 737 def test_all_overrides_import_in_module_with_all(self): 738 foo_path = "_build/text/autoapi/complex/foo/index.txt" 739 with io.open(foo_path, encoding="utf8") as foo_handle: 740 foo_file = foo_handle.read() 741 742 assert "PublicClass" in foo_file 743 744 def test_parses_unicode_file(self): 745 foo_path = "_build/text/autoapi/complex/unicode_data/index.txt" 746 with io.open(foo_path, encoding="utf8") as foo_handle: 747 foo_file = foo_handle.read() 748 749 assert "unicode_str" in foo_file 750 751 752class TestComplexPackageParallel: 753 @pytest.fixture(autouse=True, scope="class") 754 def built(self, builder): 755 builder("pypackagecomplex", parallel=2) 756 757 def test_success(self): 758 pass 759 760 761def test_caching(builder): 762 mtimes = (0, 0) 763 764 def record_mtime(): 765 nonlocal mtimes 766 mtime = 0 767 for root, _, files in os.walk("_build/text/autoapi"): 768 for name in files: 769 this_mtime = os.path.getmtime(os.path.join(root, name)) 770 mtime = max(mtime, this_mtime) 771 772 mtimes = (*mtimes[1:], mtime) 773 774 builder("pypackagecomplex", confoverrides={"autoapi_keep_files": True}) 775 record_mtime() 776 777 rebuild(confoverrides={"autoapi_keep_files": True}) 778 record_mtime() 779 780 assert mtimes[1] == mtimes[0] 781 782 # Check that adding a file rebuilds the docs 783 extra_file = "complex/new.py" 784 with open(extra_file, "w") as out_f: 785 out_f.write("\n") 786 787 try: 788 rebuild(confoverrides={"autoapi_keep_files": True}) 789 finally: 790 os.remove(extra_file) 791 792 record_mtime() 793 assert mtimes[1] != mtimes[0] 794 795 # Removing a file also rebuilds the docs 796 rebuild(confoverrides={"autoapi_keep_files": True}) 797 record_mtime() 798 assert mtimes[1] != mtimes[0] 799 800 # Changing not keeping files always builds 801 rebuild() 802 record_mtime() 803 assert mtimes[1] != mtimes[0] 804 805 806class TestImplicitNamespacePackage: 807 @pytest.fixture(autouse=True, scope="class") 808 def built(self, builder): 809 builder("py3implicitnamespace") 810 811 def test_sibling_import_from_namespace(self): 812 example_path = "_build/text/autoapi/namespace/example/index.txt" 813 with io.open(example_path, encoding="utf8") as example_handle: 814 example_file = example_handle.read() 815 816 assert "namespace.example.first_method" in example_file 817 818 def test_sub_sibling_import_from_namespace(self): 819 example_path = "_build/text/autoapi/namespace/example/index.txt" 820 with io.open(example_path, encoding="utf8") as example_handle: 821 example_file = example_handle.read() 822 823 assert "namespace.example.second_sub_method" in example_file 824 825 826def test_custom_jinja_filters(builder, tmp_path): 827 py_templates = tmp_path / "python" 828 py_templates.mkdir() 829 orig_py_templates = pathlib.Path(autoapi.settings.TEMPLATE_DIR) / "python" 830 orig_template = (orig_py_templates / "class.rst").read_text() 831 (py_templates / "class.rst").write_text( 832 orig_template.replace("obj.docstring", "obj.docstring|prepare_docstring") 833 ) 834 835 confoverrides = { 836 "autoapi_prepare_jinja_env": ( 837 lambda jinja_env: jinja_env.filters.update( 838 { 839 "prepare_docstring": ( 840 lambda docstring: "This is using custom filters." 841 ) 842 } 843 ) 844 ), 845 "autoapi_template_dir": str(tmp_path), 846 } 847 builder("pyexample", confoverrides=confoverrides) 848 849 example_path = "_build/text/autoapi/example/index.txt" 850 with io.open(example_path, encoding="utf8") as example_handle: 851 example_file = example_handle.read() 852 853 assert "This is using custom filters." in example_file 854 855 856def test_string_module_attributes(builder): 857 """Test toggle for multi-line string attribute values (GitHub #267).""" 858 keep_rst = { 859 "autoapi_keep_files": True, 860 "autoapi_root": "_build/autoapi", # Preserve RST files under _build for cleanup 861 } 862 builder("py3example", confoverrides=keep_rst) 863 864 example_path = os.path.join(keep_rst["autoapi_root"], "example", "index.rst") 865 with io.open(example_path, encoding="utf8") as example_handle: 866 example_file = example_handle.read() 867 868 code_snippet_contents = [ 869 ".. py:data:: code_snippet", 870 " :annotation: = Multiline-String", 871 "", 872 " .. raw:: html", 873 "", 874 " <details><summary>Show Value</summary>", 875 "", 876 " .. code-block:: text", 877 " :linenos:", 878 "", 879 " ", # <--- Line array monstrosity to preserve these leading spaces 880 " # -*- coding: utf-8 -*-", 881 " from __future__ import absolute_import, division, print_function, unicode_literals", 882 " # from future.builtins.disabled import *", 883 " # from builtins import *", 884 "", 885 """ print("chunky o'block")""", 886 "", 887 "", 888 " .. raw:: html", 889 "", 890 " </details>", 891 ] 892 assert "\n".join(code_snippet_contents) in example_file 893 894 895class TestAutodocTypehintsPackage: 896 """Test integrations with the autodoc.typehints extension.""" 897 898 @pytest.fixture(autouse=True, scope="class") 899 def built(self, builder): 900 builder("pyautodoc_typehints") 901 902 def test_renders_typehint(self): 903 example_path = "_build/text/autoapi/example/index.txt" 904 with io.open(example_path, encoding="utf8") as example_handle: 905 example_file = example_handle.read() 906 907 assert "(*int*)" in example_file 908 909 def test_renders_typehint_in_second_module(self): 910 example2_path = "_build/text/autoapi/example2/index.txt" 911 with io.open(example2_path, encoding="utf8") as example2_handle: 912 example2_file = example2_handle.read() 913 914 assert "(*int*)" in example2_file 915