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