diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-05 16:20:58 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-05 16:20:58 +0000 |
commit | ffcb4b87846b4e4a2d9eee8df4b7ec40365878b8 (patch) | |
tree | 3c64877dd20ad1141111c77b3463e95686002b39 /tests | |
parent | Adding debian version 7.2.6-8. (diff) | |
download | sphinx-ffcb4b87846b4e4a2d9eee8df4b7ec40365878b8.tar.xz sphinx-ffcb4b87846b4e4a2d9eee8df4b7ec40365878b8.zip |
Merging upstream version 7.3.7.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests')
202 files changed, 7800 insertions, 5394 deletions
diff --git a/tests/conftest.py b/tests/conftest.py index 1b909bd..1c8d525 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,25 @@ +from __future__ import annotations + import os +import sys from pathlib import Path +from typing import TYPE_CHECKING import docutils import pytest import sphinx import sphinx.locale +import sphinx.pycode +from sphinx.testing.util import _clean_up_global_state + +if TYPE_CHECKING: + from collections.abc import Iterator -def _init_console(locale_dir=sphinx.locale._LOCALE_DIR, catalog='sphinx'): +def _init_console( + locale_dir: str | None = sphinx.locale._LOCALE_DIR, catalog: str = 'sphinx', +) -> tuple[sphinx.locale.NullTranslations, bool]: """Monkeypatch ``init_console`` to skip its action. Some tests rely on warning messages in English. We don't want @@ -20,7 +31,7 @@ def _init_console(locale_dir=sphinx.locale._LOCALE_DIR, catalog='sphinx'): sphinx.locale.init_console = _init_console -pytest_plugins = 'sphinx.testing.fixtures' +pytest_plugins = ['sphinx.testing.fixtures'] # Exclude 'roots' dirs for pytest test collector collect_ignore = ['roots'] @@ -29,12 +40,21 @@ os.environ['SPHINX_AUTODOC_RELOAD_MODULES'] = '1' @pytest.fixture(scope='session') -def rootdir(): - return Path(__file__).parent.absolute() / 'roots' +def rootdir() -> Path: + return Path(__file__).parent.resolve() / 'roots' -def pytest_report_header(config): +def pytest_report_header(config: pytest.Config) -> str: header = f"libraries: Sphinx-{sphinx.__display_version__}, docutils-{docutils.__version__}" if hasattr(config, '_tmp_path_factory'): header += f"\nbase tmp_path: {config._tmp_path_factory.getbasetemp()}" return header + + +@pytest.fixture(autouse=True) +def _cleanup_docutils() -> Iterator[None]: + saved_path = sys.path + yield # run the test + sys.path[:] = saved_path + + _clean_up_global_state() diff --git a/tests/js/searchtools.js b/tests/js/searchtools.js index c9e0c43..4f9984d 100644 --- a/tests/js/searchtools.js +++ b/tests/js/searchtools.js @@ -21,7 +21,59 @@ describe('Basic html theme search', function() { "<no title>", "", null, - 2, + 5, + "index.rst" + ]]; + expect(Search.performTermsSearch(searchterms, excluded, terms, titleterms)).toEqual(hits); + }); + + it('should be able to search for multiple terms', function() { + index = { + alltitles: { + 'Main Page': [[0, 'main-page']], + }, + docnames:["index"], + filenames:["index.rst"], + terms:{main:0, page:0}, + titles:["Main Page"], + titleterms:{ main:0, page:0 } + } + Search.setIndex(index); + + searchterms = ['main', 'page']; + excluded = []; + terms = index.terms; + titleterms = index.titleterms; + hits = [[ + 'index', + 'Main Page', + '', + null, + 15, + 'index.rst']]; + expect(Search.performTermsSearch(searchterms, excluded, terms, titleterms)).toEqual(hits); + }); + + it('should partially-match "sphinx" when in title index', function() { + index = { + docnames:["index"], + filenames:["index.rst"], + terms:{'useful': 0, 'utilities': 0}, + titles:["sphinx_utils module"], + titleterms:{'sphinx_utils': 0} + } + Search.setIndex(index); + searchterms = ['sphinx']; + excluded = []; + terms = index.terms; + titleterms = index.titleterms; + + hits = [[ + "index", + "sphinx_utils module", + "", + null, + 7, "index.rst" ]]; expect(Search.performTermsSearch(searchterms, excluded, terms, titleterms)).toEqual(hits); @@ -31,6 +83,51 @@ describe('Basic html theme search', function() { }); +describe("htmlToText", function() { + + const testHTML = `<html> + <body> + <script src="directory/filename.js"></script> + <div class="body" role="main"> + <script> + console.log('dynamic'); + </script> + <style> + div.body p.centered { + text-align: center; + margin-top: 25px; + } + </style> + <!-- main content --> + <section id="getting-started"> + <h1>Getting Started</h1> + <p>Some text</p> + </section> + <section id="other-section"> + <h1>Other Section</h1> + <p>Other text</p> + </section> + <section id="yet-another-section"> + <h1>Yet Another Section</h1> + <p>More text</p> + </section> + </div> + </body> + </html>`; + + it("basic case", () => { + expect(Search.htmlToText(testHTML).trim().split(/\s+/)).toEqual([ + 'Getting', 'Started', 'Some', 'text', + 'Other', 'Section', 'Other', 'text', + 'Yet', 'Another', 'Section', 'More', 'text' + ]); + }); + + it("will start reading from the anchor", () => { + expect(Search.htmlToText(testHTML, '#other-section').trim().split(/\s+/)).toEqual(['Other', 'Section', 'Other', 'text']); + }); +}); + // This is regression test for https://github.com/sphinx-doc/sphinx/issues/3150 describe('splitQuery regression tests', () => { diff --git a/tests/roots/test-changes/base.rst b/tests/roots/test-changes/base.rst index a1b2839..81d90e6 100644 --- a/tests/roots/test-changes/base.rst +++ b/tests/roots/test-changes/base.rst @@ -10,6 +10,9 @@ Version markup .. deprecated:: 0.6 Boring stuff. +.. versionremoved:: 0.6 + Goodbye boring stuff. + .. versionadded:: 1.2 First paragraph of versionadded. diff --git a/tests/roots/test-changes/conf.py b/tests/roots/test-changes/conf.py index c3b2169..29bea6a 100644 --- a/tests/roots/test-changes/conf.py +++ b/tests/roots/test-changes/conf.py @@ -1,4 +1,4 @@ project = 'Sphinx ChangesBuilder tests' -copyright = '2007-2023 by the Sphinx team, see AUTHORS' +copyright = '2007-2024 by the Sphinx team, see AUTHORS' version = '0.6' release = '0.6alpha1' diff --git a/tests/roots/test-domain-cpp/operator-lookup.rst b/tests/roots/test-domain-cpp/operator-lookup.rst new file mode 100644 index 0000000..671b91b --- /dev/null +++ b/tests/roots/test-domain-cpp/operator-lookup.rst @@ -0,0 +1,28 @@ +When doing name resolution there are 4 different idenOrOps: + +- identifier +- built-in operator +- user-defined literal +- type conversion + +.. cpp:function:: int g() +.. cpp:function:: int operator+(int, int) +.. cpp:function:: int operator""_lit() + +.. cpp:class:: B + + .. cpp:function:: operator int() + + Functions that can't be found: + + - :cpp:func:`int h()` + - :cpp:func:`int operator+(bool, bool)` + - :cpp:func:`int operator""_udl()` + - :cpp:func:`operator bool()` + + Functions that should be found: + + - :cpp:func:`int g()` + - :cpp:func:`int operator+(int, int)` + - :cpp:func:`int operator""_lit()` + - :cpp:func:`operator int()` diff --git a/tests/roots/test-domain-py/module.rst b/tests/roots/test-domain-py/module.rst index 4a28068..70098f6 100644 --- a/tests/roots/test-domain-py/module.rst +++ b/tests/roots/test-domain-py/module.rst @@ -58,3 +58,9 @@ module .. py:module:: object .. py:function:: sum() + +.. py:data:: test + :type: typing.Literal[2] + +.. py:data:: test2 + :type: typing.Literal[-2] diff --git a/tests/roots/test-ext-autodoc/target/enums.py b/tests/roots/test-ext-autodoc/target/enums.py index c69455f..6b27316 100644 --- a/tests/roots/test-ext-autodoc/target/enums.py +++ b/tests/roots/test-ext-autodoc/target/enums.py @@ -1,10 +1,46 @@ +# ruff: NoQA: D403, PIE796 import enum +from typing import final + + +class MemberType: + """Custom data type with a simple API.""" + + # this mangled attribute will never be shown on subclasses + # even if :inherited-members: and :private-members: are set + __slots__ = ('__data',) + + def __new__(cls, value): + self = object.__new__(cls) + self.__data = value + return self + + def __str__(self): + """inherited""" + return self.__data + + def __repr__(self): + return repr(self.__data) + + def __reduce__(self): + # data types must be pickable, otherwise enum classes using this data + # type will be forced to be non-pickable and have their __module__ set + # to '<unknown>' instead of, for instance, '__main__' + return self.__class__, (self.__data,) + + @final + @property + def dtype(self): + """docstring""" + return 'str' + + def isupper(self): + """inherited""" + return self.__data.isupper() class EnumCls(enum.Enum): - """ - this is enum class - """ + """this is enum class""" #: doc for val1 val1 = 12 @@ -15,9 +51,194 @@ class EnumCls(enum.Enum): def say_hello(self): """a method says hello to you.""" - pass @classmethod def say_goodbye(cls): """a classmethod says good-bye to you.""" - pass + + +class EnumClassWithDataType(MemberType, enum.Enum): + """this is enum class""" + + x = 'x' + + def say_hello(self): + """docstring""" + + @classmethod + def say_goodbye(cls): + """docstring""" + + +class ToUpperCase: # not inheriting from enum.Enum + @property + def value(self): # bypass enum.Enum.value + """uppercased""" + return str(self._value_).upper() # type: ignore[attr-defined] + + +class Greeter: + def say_hello(self): + """inherited""" + + @classmethod + def say_goodbye(cls): + """inherited""" + + +class EnumClassWithMixinType(ToUpperCase, enum.Enum): + """this is enum class""" + + x = 'x' + + def say_hello(self): + """docstring""" + + @classmethod + def say_goodbye(cls): + """docstring""" + + +class EnumClassWithMixinTypeInherit(Greeter, ToUpperCase, enum.Enum): + """this is enum class""" + + x = 'x' + + +class Overridden(enum.Enum): + def override(self): + """inherited""" + return 1 + + +class EnumClassWithMixinEnumType(Greeter, Overridden, enum.Enum): + """this is enum class""" + + x = 'x' + + def override(self): + """overridden""" + return 2 + + +class EnumClassWithMixinAndDataType(Greeter, ToUpperCase, MemberType, enum.Enum): + """this is enum class""" + + x = 'x' + + def say_hello(self): + """overridden""" + + @classmethod + def say_goodbye(cls): + """overridden""" + + def isupper(self): + """overridden""" + return False + + def __str__(self): + """overridden""" + return super().__str__() + + +class _ParentEnum(Greeter, Overridden, enum.Enum): + """docstring""" + + +class EnumClassWithParentEnum(ToUpperCase, MemberType, _ParentEnum, enum.Enum): + """this is enum class""" + + x = 'x' + + def isupper(self): + """overridden""" + return False + + def __str__(self): + """overridden""" + return super().__str__() + + +class _SunderMissingInNonEnumMixin: + @classmethod + def _missing_(cls, value): + """inherited""" + return super()._missing_(value) # type: ignore[misc] + + +class _SunderMissingInEnumMixin(enum.Enum): + @classmethod + def _missing_(cls, value): + """inherited""" + return super()._missing_(value) + + +class _SunderMissingInDataType(MemberType): + @classmethod + def _missing_(cls, value): + """inherited""" + return super()._missing_(value) # type: ignore[misc] + + +class EnumSunderMissingInNonEnumMixin(_SunderMissingInNonEnumMixin, enum.Enum): + """this is enum class""" + + +class EnumSunderMissingInEnumMixin(_SunderMissingInEnumMixin, enum.Enum): + """this is enum class""" + + +class EnumSunderMissingInDataType(_SunderMissingInDataType, enum.Enum): + """this is enum class""" + + +class EnumSunderMissingInClass(enum.Enum): + """this is enum class""" + + @classmethod + def _missing_(cls, value): + """docstring""" + return super()._missing_(value) + + +class _NamePropertyInNonEnumMixin: + @property + def name(self): + """inherited""" + return super().name # type: ignore[misc] + + +class _NamePropertyInEnumMixin(enum.Enum): + @property + def name(self): + """inherited""" + return super().name + + +class _NamePropertyInDataType(MemberType): + @property + def name(self): + """inherited""" + return super().name # type: ignore[misc] + + +class EnumNamePropertyInNonEnumMixin(_NamePropertyInNonEnumMixin, enum.Enum): + """this is enum class""" + + +class EnumNamePropertyInEnumMixin(_NamePropertyInEnumMixin, enum.Enum): + """this is enum class""" + + +class EnumNamePropertyInDataType(_NamePropertyInDataType, enum.Enum): + """this is enum class""" + + +class EnumNamePropertyInClass(enum.Enum): + """this is enum class""" + + @property + def name(self): + """docstring""" + return super().name diff --git a/tests/roots/test-ext-autodoc/target/functions.py b/tests/roots/test-ext-autodoc/target/functions.py index b62aa70..0265fb3 100644 --- a/tests/roots/test-ext-autodoc/target/functions.py +++ b/tests/roots/test-ext-autodoc/target/functions.py @@ -17,3 +17,6 @@ partial_coroutinefunc = partial(coroutinefunc) builtin_func = print partial_builtin_func = partial(print) + +def slice_arg_func(arg: 'float64[:, :]'): + pass diff --git a/tests/roots/test-ext-autodoc/target/inherited_annotations.py b/tests/roots/test-ext-autodoc/target/inherited_annotations.py new file mode 100644 index 0000000..3ae58a8 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/inherited_annotations.py @@ -0,0 +1,17 @@ +""" + Test case for #11387 corner case involving inherited + members with type annotations on python 3.9 and earlier +""" + +class HasTypeAnnotatedMember: + inherit_me: int + """Inherited""" + +class NoTypeAnnotation(HasTypeAnnotatedMember): + a = 1 + """Local""" + +class NoTypeAnnotation2(HasTypeAnnotatedMember): + a = 1 + """Local""" + diff --git a/tests/roots/test-ext-autodoc/target/singledispatchmethod_classmethod.py b/tests/roots/test-ext-autodoc/target/singledispatchmethod_classmethod.py new file mode 100644 index 0000000..039fada --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/singledispatchmethod_classmethod.py @@ -0,0 +1,31 @@ +from functools import singledispatchmethod + + +class Foo: + """docstring""" + + @singledispatchmethod + @classmethod + def class_meth(cls, arg, kwarg=None): + """A class method for general use.""" + pass + + @class_meth.register(int) + @class_meth.register(float) + @classmethod + def _class_meth_int(cls, arg, kwarg=None): + """A class method for numbers.""" + pass + + @class_meth.register(str) + @classmethod + def _class_meth_str(cls, arg, kwarg=None): + """A class method for str.""" + pass + + @class_meth.register + @classmethod + def _class_meth_dict(cls, arg: dict, kwarg=None): + """A class method for dict.""" + # This function tests for specifying type through annotations + pass diff --git a/tests/roots/test-ext-doctest-skipif/conf.py b/tests/roots/test-ext-doctest-skipif/conf.py index 6f54982..cd8f3eb 100644 --- a/tests/roots/test-ext-doctest-skipif/conf.py +++ b/tests/roots/test-ext-doctest-skipif/conf.py @@ -6,7 +6,7 @@ source_suffix = '.txt' exclude_patterns = ['_build'] doctest_global_setup = ''' -from tests.test_ext_doctest import record +from tests.test_extensions.test_ext_doctest import record record('doctest_global_setup', 'body', True) ''' diff --git a/tests/roots/test-ext-doctest/doctest.txt b/tests/roots/test-ext-doctest/doctest.txt index 04780cf..0adcf74 100644 --- a/tests/roots/test-ext-doctest/doctest.txt +++ b/tests/roots/test-ext-doctest/doctest.txt @@ -139,7 +139,7 @@ Special directives .. testcleanup:: * - from tests import test_ext_doctest + from tests.test_extensions import test_ext_doctest test_ext_doctest.cleanup_call() non-ASCII result diff --git a/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py b/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py index 43368de..c092860 100644 --- a/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py +++ b/tests/roots/test-ext-imgmockconverter/mocksvgconverter.py @@ -8,9 +8,9 @@ from sphinx.transforms.post_transforms.images import ImageConverter if False: # For type annotation - from typing import Any, Dict # NOQA + from typing import Any, Dict # NoQA - from sphinx.application import Sphinx # NOQA + from sphinx.application import Sphinx # NoQA class MyConverter(ImageConverter): conversion_rules = [ diff --git a/tests/roots/test-ext-intersphinx-role/index.rst b/tests/roots/test-ext-intersphinx-role/index.rst index 58edb7a..63bccf0 100644 --- a/tests/roots/test-ext-intersphinx-role/index.rst +++ b/tests/roots/test-ext-intersphinx-role/index.rst @@ -39,6 +39,10 @@ - a class with explicit non-existing inventory, which also has upper-case in name: :external+invNope:cpp:class:`foo::Bar` +- An object type being mistakenly used instead of a role name: + + - :external+inv:c:function:`CFunc` + - :external+inv:function:`CFunc` - explicit title: :external:cpp:type:`FoonsTitle <foons>` diff --git a/tests/roots/test-ext-math-include/conf.py b/tests/roots/test-ext-math-include/conf.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/roots/test-ext-math-include/conf.py diff --git a/tests/roots/test-ext-math-include/included.rst b/tests/roots/test-ext-math-include/included.rst new file mode 100644 index 0000000..7fb746e --- /dev/null +++ b/tests/roots/test-ext-math-include/included.rst @@ -0,0 +1,6 @@ +Title +===== + +Some file including some maths. + +.. include:: math.rst
\ No newline at end of file diff --git a/tests/roots/test-ext-math-include/index.rst b/tests/roots/test-ext-math-include/index.rst new file mode 100644 index 0000000..cc95338 --- /dev/null +++ b/tests/roots/test-ext-math-include/index.rst @@ -0,0 +1,7 @@ +Test Math +========= + +.. toctree:: + :numbered: 1 + + included diff --git a/tests/roots/test-ext-math-include/math.rst b/tests/roots/test-ext-math-include/math.rst new file mode 100644 index 0000000..a4266d0 --- /dev/null +++ b/tests/roots/test-ext-math-include/math.rst @@ -0,0 +1,4 @@ +:math:`1 + 1 = 2` +================= + +Lorem ipsum.
\ No newline at end of file diff --git a/tests/roots/test-ext-napoleon-paramtype/conf.py b/tests/roots/test-ext-napoleon-paramtype/conf.py new file mode 100644 index 0000000..34e2274 --- /dev/null +++ b/tests/roots/test-ext-napoleon-paramtype/conf.py @@ -0,0 +1,15 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath('.')) +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.intersphinx' +] + +# Python inventory is manually created in the test +# in order to avoid creating a real HTTP connection +intersphinx_mapping = {} +intersphinx_cache_limit = 0 +intersphinx_disabled_reftypes = []
\ No newline at end of file diff --git a/tests/roots/test-ext-napoleon-paramtype/index.rst b/tests/roots/test-ext-napoleon-paramtype/index.rst new file mode 100644 index 0000000..5540897 --- /dev/null +++ b/tests/roots/test-ext-napoleon-paramtype/index.rst @@ -0,0 +1,8 @@ +test-ext-napoleon +================= + +.. automodule:: pkg.bar + :members: + +.. automodule:: pkg.foo + :members: diff --git a/tests/roots/test-ext-napoleon-paramtype/pkg/__init__.py b/tests/roots/test-ext-napoleon-paramtype/pkg/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/roots/test-ext-napoleon-paramtype/pkg/__init__.py diff --git a/tests/roots/test-ext-napoleon-paramtype/pkg/bar.py b/tests/roots/test-ext-napoleon-paramtype/pkg/bar.py new file mode 100644 index 0000000..e1ae794 --- /dev/null +++ b/tests/roots/test-ext-napoleon-paramtype/pkg/bar.py @@ -0,0 +1,10 @@ +class Bar: + """The bar.""" + def list(self) -> None: + """A list method.""" + + @staticmethod + def int() -> float: + """An int method.""" + return 1.0 + diff --git a/tests/roots/test-ext-napoleon-paramtype/pkg/foo.py b/tests/roots/test-ext-napoleon-paramtype/pkg/foo.py new file mode 100644 index 0000000..6979f9e --- /dev/null +++ b/tests/roots/test-ext-napoleon-paramtype/pkg/foo.py @@ -0,0 +1,27 @@ +class Foo: + """The foo.""" + def do( + self, + *, + keyword_paramtype, + keyword_kwtype, + kwarg_paramtype, + kwarg_kwtype, + kwparam_paramtype, + kwparam_kwtype, + ): + """Some method. + + :keyword keyword_paramtype: some param + :paramtype keyword_paramtype: list[int] + :keyword keyword_kwtype: some param + :kwtype keyword_kwtype: list[int] + :kwarg kwarg_paramtype: some param + :paramtype kwarg_paramtype: list[int] + :kwarg kwarg_kwtype: some param + :kwtype kwarg_kwtype: list[int] + :kwparam kwparam_paramtype: some param + :paramtype kwparam_paramtype: list[int] + :kwparam kwparam_kwtype: some param + :kwtype kwparam_kwtype: list[int] + """ diff --git a/tests/roots/test-ext-viewcode/conf.py b/tests/roots/test-ext-viewcode/conf.py index 5e07214..d6de1d9 100644 --- a/tests/roots/test-ext-viewcode/conf.py +++ b/tests/roots/test-ext-viewcode/conf.py @@ -15,10 +15,10 @@ if 'test_linkcode' in tags: def linkcode_resolve(domain, info): if domain == 'py': fn = info['module'].replace('.', '/') - return "http://foobar/source/%s.py" % fn + return "https://foobar/source/%s.py" % fn elif domain == "js": - return "http://foobar/js/" + info['fullname'] + return "https://foobar/js/" + info['fullname'] elif domain in ("c", "cpp"): - return f"http://foobar/{domain}/{''.join(info['names'])}" + return f"https://foobar/{domain}/{''.join(info['names'])}" else: raise AssertionError() diff --git a/tests/roots/test-footnotes/index.rst b/tests/roots/test-footnotes/index.rst index f2c5d0e..e4490ed 100644 --- a/tests/roots/test-footnotes/index.rst +++ b/tests/roots/test-footnotes/index.rst @@ -31,10 +31,10 @@ The section with a reference to [AuthorYear]_ * First footnote: [#]_ * Second footnote: [1]_ -* `Sphinx <http://sphinx-doc.org/>`_ +* `Sphinx <https://sphinx-doc.org/>`_ * Third footnote: [#]_ * Fourth footnote: [#named]_ -* `URL including tilde <http://sphinx-doc.org/~test/>`_ +* `URL including tilde <https://sphinx-doc.org/~test/>`_ * GitHub Page: `https://github.com/sphinx-doc/sphinx <https://github.com/sphinx-doc/sphinx>`_ * Mailing list: `sphinx-dev@googlegroups.com <mailto:sphinx-dev@googlegroups.com>`_ @@ -49,13 +49,13 @@ The section with a reference to [#]_ .. [#] Footnote in section -`URL in term <http://sphinx-doc.org/>`_ +`URL in term <https://sphinx-doc.org/>`_ Description Description Description ... Footnote in term [#]_ Description Description Description ... - `Term in deflist <http://sphinx-doc.org/>`_ + `Term in deflist <https://sphinx-doc.org/>`_ Description2 .. [#] Footnote in term diff --git a/tests/roots/test-images/index.rst b/tests/roots/test-images/index.rst index 14a2987..9b9aac1 100644 --- a/tests/roots/test-images/index.rst +++ b/tests/roots/test-images/index.rst @@ -23,7 +23,7 @@ test-image :target: https://www.python.org/ .. a remote image -.. image:: https://www.python.org/static/img/python-logo.png +.. image:: http://localhost:7777/sphinx.png .. non-exist remote image -.. image:: https://www.google.com/NOT_EXIST.PNG +.. image:: http://localhost:7777/NOT_EXIST.PNG diff --git a/tests/roots/test-intl/definition_terms.txt b/tests/roots/test-intl/definition_terms.txt index 4c56288..19d4fcb 100644 --- a/tests/roots/test-intl/definition_terms.txt +++ b/tests/roots/test-intl/definition_terms.txt @@ -6,7 +6,7 @@ i18n with definition terms Some term The corresponding definition -Some *term* `with link <http://sphinx-doc.org/>`__ +Some *term* `with link <https://sphinx-doc.org/>`__ The corresponding definition #2 Some **term** with : classifier1 : classifier2 diff --git a/tests/roots/test-intl/external_links.txt b/tests/roots/test-intl/external_links.txt index 1cecbee..2d0c063 100644 --- a/tests/roots/test-intl/external_links.txt +++ b/tests/roots/test-intl/external_links.txt @@ -8,12 +8,12 @@ External link to Python_. Internal link to `i18n with external links`_. -Inline link by `Sphinx Site <http://sphinx-doc.org>`_. +Inline link by `Sphinx Site <https://sphinx-doc.org>`_. Unnamed link__. -.. _Python: http://python.org/index.html -.. __: http://google.com +.. _Python: https://python.org/index.html +.. __: https://google.com link target swapped translation @@ -21,7 +21,7 @@ link target swapped translation link to external1_ and external2_. -link to `Sphinx Site <http://sphinx-doc.org>`_ and `Python Site <http://python.org>`_. +link to `Sphinx Site <https://sphinx-doc.org>`_ and `Python Site <https://python.org>`_. .. _external1: https://www.google.com/external1 .. _external2: https://www.google.com/external2 @@ -30,6 +30,6 @@ link to `Sphinx Site <http://sphinx-doc.org>`_ and `Python Site <http://python.o Multiple references in the same line ===================================== -Link to `Sphinx Site <http://sphinx-doc.org>`_, `Python Site <http://python.org>`_, Python_, Unnamed__ and `i18n with external links`_. +Link to `Sphinx Site <https://sphinx-doc.org>`_, `Python Site <https://python.org>`_, Python_, Unnamed__ and `i18n with external links`_. -.. __: http://google.com +.. __: https://google.com diff --git a/tests/roots/test-intl/raw.txt b/tests/roots/test-intl/raw.txt index fe77f6c..1825941 100644 --- a/tests/roots/test-intl/raw.txt +++ b/tests/roots/test-intl/raw.txt @@ -4,5 +4,5 @@ Raw .. raw:: html - <iframe src="http://sphinx-doc.org"></iframe> + <iframe src="https://sphinx-doc.org"></iframe> diff --git a/tests/roots/test-intl/refs.txt b/tests/roots/test-intl/refs.txt index 4a094b2..ab52ef1 100644 --- a/tests/roots/test-intl/refs.txt +++ b/tests/roots/test-intl/refs.txt @@ -6,7 +6,7 @@ Translation Tips .. _download Sphinx: https://pypi.org/project/Sphinx/ .. _Docutils site: https://docutils.sourceforge.io/ -.. _Sphinx site: http://sphinx-doc.org/ +.. _Sphinx site: https://sphinx-doc.org/ A-1. Here's how you can `download Sphinx`_. diff --git a/tests/roots/test-intl/refs_inconsistency.txt b/tests/roots/test-intl/refs_inconsistency.txt index b16623a..4840597 100644 --- a/tests/roots/test-intl/refs_inconsistency.txt +++ b/tests/roots/test-intl/refs_inconsistency.txt @@ -10,4 +10,4 @@ i18n with refs inconsistency .. [#] This is a auto numbered footnote. .. [ref2] This is a citation. .. [100] This is a numbered footnote. -.. _reference: http://www.example.com +.. _reference: https://www.example.com diff --git a/tests/roots/test-intl/versionchange.txt b/tests/roots/test-intl/versionchange.txt index 4c57e14..7645342 100644 --- a/tests/roots/test-intl/versionchange.txt +++ b/tests/roots/test-intl/versionchange.txt @@ -14,3 +14,5 @@ i18n with versionchange .. versionchanged:: 1.0 This is the *first* paragraph of versionchanged. + +.. versionremoved:: 1.0 This is the *first* paragraph of versionremoved. diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po b/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po index 1752dd6..a19c9d1 100644 --- a/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po +++ b/tests/roots/test-intl/xx/LC_MESSAGES/definition_terms.po @@ -25,8 +25,8 @@ msgstr "SOME TERM" msgid "The corresponding definition"
msgstr "THE CORRESPONDING DEFINITION"
-msgid "Some *term* `with link <http://sphinx-doc.org/>`__"
-msgstr "SOME *TERM* `WITH LINK <http://sphinx-doc.org/>`__"
+msgid "Some *term* `with link <https://sphinx-doc.org/>`__"
+msgstr "SOME *TERM* `WITH LINK <https://sphinx-doc.org/>`__"
msgid "The corresponding definition #2"
msgstr "THE CORRESPONDING DEFINITION #2"
diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/external_links.po b/tests/roots/test-intl/xx/LC_MESSAGES/external_links.po index 8c53abb..345dc95 100644 --- a/tests/roots/test-intl/xx/LC_MESSAGES/external_links.po +++ b/tests/roots/test-intl/xx/LC_MESSAGES/external_links.po @@ -25,8 +25,8 @@ msgstr "EXTERNAL LINK TO Python_." msgid "Internal link to `i18n with external links`_." msgstr "`EXTERNAL LINKS`_ IS INTERNAL LINK." -msgid "Inline link by `Sphinx Site <http://sphinx-doc.org>`_." -msgstr "INLINE LINK BY `THE SPHINX SITE <http://sphinx-doc.org>`_." +msgid "Inline link by `Sphinx Site <https://sphinx-doc.org>`_." +msgstr "INLINE LINK BY `THE SPHINX SITE <https://sphinx-doc.org>`_." msgid "Unnamed link__." msgstr "UNNAMED LINK__." @@ -37,11 +37,11 @@ msgstr "LINK TARGET SWAPPED TRANSLATION" msgid "link to external1_ and external2_." msgstr "LINK TO external2_ AND external1_." -msgid "link to `Sphinx Site <http://sphinx-doc.org>`_ and `Python Site <http://python.org>`_." -msgstr "LINK TO `THE PYTHON SITE <http://python.org>`_ AND `THE SPHINX SITE <http://sphinx-doc.org>`_." +msgid "link to `Sphinx Site <https://sphinx-doc.org>`_ and `Python Site <https://python.org>`_." +msgstr "LINK TO `THE PYTHON SITE <https://python.org>`_ AND `THE SPHINX SITE <https://sphinx-doc.org>`_." msgid "Multiple references in the same line" msgstr "MULTIPLE REFERENCES IN THE SAME LINE" -msgid "Link to `Sphinx Site <http://sphinx-doc.org>`_, `Python Site <http://python.org>`_, Python_, Unnamed__ and `i18n with external links`_." -msgstr "LINK TO `EXTERNAL LINKS`_, Python_, `THE SPHINX SITE <http://sphinx-doc.org>`_, UNNAMED__ AND `THE PYTHON SITE <http://python.org>`_." +msgid "Link to `Sphinx Site <https://sphinx-doc.org>`_, `Python Site <https://python.org>`_, Python_, Unnamed__ and `i18n with external links`_." +msgstr "LINK TO `EXTERNAL LINKS`_, Python_, `THE SPHINX SITE <https://sphinx-doc.org>`_, UNNAMED__ AND `THE PYTHON SITE <https://python.org>`_." diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/raw.po b/tests/roots/test-intl/xx/LC_MESSAGES/raw.po index f2e8893..303f46b 100644 --- a/tests/roots/test-intl/xx/LC_MESSAGES/raw.po +++ b/tests/roots/test-intl/xx/LC_MESSAGES/raw.po @@ -16,6 +16,6 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -msgid "<iframe src=\"http://sphinx-doc.org\"></iframe>" -msgstr "<iframe src=\"HTTP://SPHINX-DOC.ORG\"></iframe>" +msgid "<iframe src=\"https://sphinx-doc.org\"></iframe>" +msgstr "<iframe src=\"HTTPS://SPHINX-DOC.ORG\"></iframe>" diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po b/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po index 5a8df38..b1d7865 100644 --- a/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po +++ b/tests/roots/test-intl/xx/LC_MESSAGES/versionchange.po @@ -31,3 +31,5 @@ msgstr "THIS IS THE *FIRST* PARAGRAPH OF VERSIONADDED." msgid "This is the *first* paragraph of versionchanged." msgstr "THIS IS THE *FIRST* PARAGRAPH OF VERSIONCHANGED." +msgid "This is the *first* paragraph of versionremoved." +msgstr "THIS IS THE *FIRST* PARAGRAPH OF VERSIONREMOVED." diff --git a/tests/roots/test-linkcheck-anchors-ignore-for-url/conf.py b/tests/roots/test-linkcheck-anchors-ignore-for-url/conf.py index 0005bfa..3fc3628 100644 --- a/tests/roots/test-linkcheck-anchors-ignore-for-url/conf.py +++ b/tests/roots/test-linkcheck-anchors-ignore-for-url/conf.py @@ -1,3 +1,3 @@ exclude_patterns = ['_build'] linkcheck_anchors = True -linkcheck_timeout = 0.05 +linkcheck_timeout = 0.25 diff --git a/tests/roots/test-linkcheck-anchors-ignore/conf.py b/tests/roots/test-linkcheck-anchors-ignore/conf.py index 0005bfa..3fc3628 100644 --- a/tests/roots/test-linkcheck-anchors-ignore/conf.py +++ b/tests/roots/test-linkcheck-anchors-ignore/conf.py @@ -1,3 +1,3 @@ exclude_patterns = ['_build'] linkcheck_anchors = True -linkcheck_timeout = 0.05 +linkcheck_timeout = 0.25 diff --git a/tests/roots/test-linkcheck-documents_exclude/conf.py b/tests/roots/test-linkcheck-documents_exclude/conf.py index 52388f9..39fcdcb 100644 --- a/tests/roots/test-linkcheck-documents_exclude/conf.py +++ b/tests/roots/test-linkcheck-documents_exclude/conf.py @@ -3,4 +3,4 @@ linkcheck_exclude_documents = [ '^broken_link$', 'br[0-9]ken_link', ] -linkcheck_timeout = 0.05 +linkcheck_timeout = 0.25 diff --git a/tests/roots/test-linkcheck-localserver-anchor/conf.py b/tests/roots/test-linkcheck-localserver-anchor/conf.py index 0005bfa..3fc3628 100644 --- a/tests/roots/test-linkcheck-localserver-anchor/conf.py +++ b/tests/roots/test-linkcheck-localserver-anchor/conf.py @@ -1,3 +1,3 @@ exclude_patterns = ['_build'] linkcheck_anchors = True -linkcheck_timeout = 0.05 +linkcheck_timeout = 0.25 diff --git a/tests/roots/test-linkcheck-localserver-https/conf.py b/tests/roots/test-linkcheck-localserver-https/conf.py index a2ce01e..85cbe6f 100644 --- a/tests/roots/test-linkcheck-localserver-https/conf.py +++ b/tests/roots/test-linkcheck-localserver-https/conf.py @@ -1,2 +1,2 @@ exclude_patterns = ['_build'] -linkcheck_timeout = 0.05 +linkcheck_timeout = 0.25 diff --git a/tests/roots/test-linkcheck-localserver-warn-redirects/conf.py b/tests/roots/test-linkcheck-localserver-warn-redirects/conf.py index a2ce01e..85cbe6f 100644 --- a/tests/roots/test-linkcheck-localserver-warn-redirects/conf.py +++ b/tests/roots/test-linkcheck-localserver-warn-redirects/conf.py @@ -1,2 +1,2 @@ exclude_patterns = ['_build'] -linkcheck_timeout = 0.05 +linkcheck_timeout = 0.25 diff --git a/tests/roots/test-linkcheck-localserver/conf.py b/tests/roots/test-linkcheck-localserver/conf.py index a2ce01e..85cbe6f 100644 --- a/tests/roots/test-linkcheck-localserver/conf.py +++ b/tests/roots/test-linkcheck-localserver/conf.py @@ -1,2 +1,2 @@ exclude_patterns = ['_build'] -linkcheck_timeout = 0.05 +linkcheck_timeout = 0.25 diff --git a/tests/roots/test-linkcheck-raw-node/conf.py b/tests/roots/test-linkcheck-raw-node/conf.py index a2ce01e..85cbe6f 100644 --- a/tests/roots/test-linkcheck-raw-node/conf.py +++ b/tests/roots/test-linkcheck-raw-node/conf.py @@ -1,2 +1,2 @@ exclude_patterns = ['_build'] -linkcheck_timeout = 0.05 +linkcheck_timeout = 0.25 diff --git a/tests/roots/test-linkcheck-raw-node/index.rst b/tests/roots/test-linkcheck-raw-node/index.rst deleted file mode 100644 index 76e26b5..0000000 --- a/tests/roots/test-linkcheck-raw-node/index.rst +++ /dev/null @@ -1,2 +0,0 @@ -.. raw:: html - :url: http://localhost:7777/ diff --git a/tests/roots/test-linkcheck-too-many-retries/conf.py b/tests/roots/test-linkcheck-too-many-retries/conf.py index 0005bfa..3fc3628 100644 --- a/tests/roots/test-linkcheck-too-many-retries/conf.py +++ b/tests/roots/test-linkcheck-too-many-retries/conf.py @@ -1,3 +1,3 @@ exclude_patterns = ['_build'] linkcheck_anchors = True -linkcheck_timeout = 0.05 +linkcheck_timeout = 0.25 diff --git a/tests/roots/test-linkcheck/conf.py b/tests/roots/test-linkcheck/conf.py index 6ddb41a..7cb6a0d 100644 --- a/tests/roots/test-linkcheck/conf.py +++ b/tests/roots/test-linkcheck/conf.py @@ -1,4 +1,4 @@ root_doc = 'links' exclude_patterns = ['_build'] linkcheck_anchors = True -linkcheck_timeout = 0.05 +linkcheck_timeout = 0.25 diff --git a/tests/roots/test-manpage_url/index.rst b/tests/roots/test-manpage_url/index.rst index 50d3b04..6761f97 100644 --- a/tests/roots/test-manpage_url/index.rst +++ b/tests/roots/test-manpage_url/index.rst @@ -1,3 +1,7 @@ - * :manpage:`man(1)` - * :manpage:`ls.1` - * :manpage:`sphinx` +The :manpage:`cp(1)` +-------------------- +* :manpage:`man(1)` +* :manpage:`ls.1` +* :manpage:`sphinx` +* :manpage:`mailx(1) <bsd-mailx/mailx.1>` +* :manpage:`!man(1)` diff --git a/tests/roots/test-need-escaped/index.rst b/tests/roots/test-need-escaped/index.rst index 9ef74e0..29a24fa 100644 --- a/tests/roots/test-need-escaped/index.rst +++ b/tests/roots/test-need-escaped/index.rst @@ -15,7 +15,7 @@ Contents: foo bar - http://sphinx-doc.org/ + https://sphinx-doc.org/ baz qux diff --git a/tests/roots/test-roles-download/index.rst b/tests/roots/test-roles-download/index.rst index cdb075e..9d2c622 100644 --- a/tests/roots/test-roles-download/index.rst +++ b/tests/roots/test-roles-download/index.rst @@ -4,4 +4,4 @@ test-roles-download * :download:`dummy.dat` * :download:`another/dummy.dat` * :download:`not_found.dat` -* :download:`Sphinx logo <http://www.sphinx-doc.org/en/master/_static/sphinxheader.png>` +* :download:`Sphinx logo <https://www.sphinx-doc.org/en/master/_static/sphinx-logo.svg>` diff --git a/tests/roots/test-root/conf.py b/tests/roots/test-root/conf.py index 154d4d1..a14ffaf 100644 --- a/tests/roots/test-root/conf.py +++ b/tests/roots/test-root/conf.py @@ -114,8 +114,8 @@ latex_elements = { coverage_c_path = ['special/*.h'] coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'} -extlinks = {'issue': ('http://bugs.python.org/issue%s', 'issue %s'), - 'pyurl': ('http://python.org/%s', None)} +extlinks = {'issue': ('https://bugs.python.org/issue%s', 'issue %s'), + 'pyurl': ('https://python.org/%s', None)} # modify tags from conf.py tags.add('confpytag') diff --git a/tests/roots/test-root/images.txt b/tests/roots/test-root/images.txt index 1dc591a..5a096dc 100644 --- a/tests/roots/test-root/images.txt +++ b/tests/roots/test-root/images.txt @@ -12,9 +12,6 @@ Sphinx image handling .. an image with unspecified extension .. image:: img.* -.. a non-local image URI -.. image:: https://www.python.org/static/img/python-logo.png - .. an image with subdir and unspecified extension .. image:: subdir/simg.* diff --git a/tests/roots/test-root/index.txt b/tests/roots/test-root/index.txt index e39c958..6a37668 100644 --- a/tests/roots/test-root/index.txt +++ b/tests/roots/test-root/index.txt @@ -28,9 +28,9 @@ Contents: lists otherext - http://sphinx-doc.org/ - Latest reference <http://sphinx-doc.org/latest/> - Python <http://python.org/> + https://sphinx-doc.org/ + Latest reference <https://sphinx-doc.org/latest/> + Python <https://python.org/> Indices and tables ================== diff --git a/tests/roots/test-root/markup.txt b/tests/roots/test-root/markup.txt index b59a652..ff677eb 100644 --- a/tests/roots/test-root/markup.txt +++ b/tests/roots/test-root/markup.txt @@ -274,6 +274,9 @@ Version markup .. deprecated:: 0.6 Boring stuff. +.. versionremoved:: 0.6 + Goodbye boring stuff. + .. versionadded:: 1.2 First paragraph of versionadded. @@ -308,7 +311,7 @@ Reference lookup underscore: [Ref_1]_ .. seealso:: something, something else, something more - `Google <http://www.google.com>`_ + `Google <https://www.google.com>`_ For everything. .. hlist:: diff --git a/tests/roots/test-toctree/index.rst b/tests/roots/test-toctree/index.rst index adf1b84..d56f2f6 100644 --- a/tests/roots/test-toctree/index.rst +++ b/tests/roots/test-toctree/index.rst @@ -15,7 +15,7 @@ Contents: foo bar - http://sphinx-doc.org/ + https://sphinx-doc.org/ self .. only:: html @@ -44,8 +44,8 @@ This used to crash: .. toctree:: :hidden: - Latest reference <http://sphinx-doc.org/latest/> - Python <http://python.org/> + Latest reference <https://sphinx-doc.org/latest/> + Python <https://python.org/> Indices and tables ================== diff --git a/tests/roots/test-versioning/insert_beginning.txt b/tests/roots/test-versioning/insert_beginning.txt index 57102a7..9b6e723 100644 --- a/tests/roots/test-versioning/insert_beginning.txt +++ b/tests/roots/test-versioning/insert_beginning.txt @@ -1,7 +1,7 @@ Versioning test text ==================== -Apperantly inserting a paragraph at the beginning of a document caused +Apparently inserting a paragraph at the beginning of a document caused problems earlier so this document should be used to test that. So the thing is I need some kind of text - not the lorem ipsum stuff, that diff --git a/tests/test_addnodes.py b/tests/test_addnodes.py index 184a696..aa99343 100644 --- a/tests/test_addnodes.py +++ b/tests/test_addnodes.py @@ -2,13 +2,18 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import pytest from sphinx import addnodes +if TYPE_CHECKING: + from collections.abc import Iterator + @pytest.fixture() -def sig_elements() -> set[type[addnodes.desc_sig_element]]: +def sig_elements() -> Iterator[set[type[addnodes.desc_sig_element]]]: """Fixture returning the current ``addnodes.SIG_ELEMENTS`` set.""" original = addnodes.SIG_ELEMENTS.copy() # safe copy of the current nodes yield {*addnodes.SIG_ELEMENTS} # temporary value to use during tests @@ -17,7 +22,6 @@ def sig_elements() -> set[type[addnodes.desc_sig_element]]: def test_desc_sig_element_nodes(sig_elements): """Test the registration of ``desc_sig_element`` subclasses.""" - # expected desc_sig_* node classes (must be declared *after* reloading # the module since otherwise the objects are not the correct ones) EXPECTED_SIG_ELEMENTS = { diff --git a/tests/test_application.py b/tests/test_application.py index a0fe268..1fc49d6 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,9 +1,11 @@ """Test the Sphinx class.""" +from __future__ import annotations import shutil import sys from io import StringIO from pathlib import Path +from typing import TYPE_CHECKING from unittest.mock import Mock import pytest @@ -11,11 +13,19 @@ from docutils import nodes import sphinx.application from sphinx.errors import ExtensionError -from sphinx.testing.util import SphinxTestApp, strip_escseq +from sphinx.testing.util import SphinxTestApp from sphinx.util import logging +from sphinx.util.console import strip_colors +if TYPE_CHECKING: + import os -def test_instantiation(tmp_path_factory, rootdir: str, monkeypatch): + +def test_instantiation( + tmp_path_factory: pytest.TempPathFactory, + rootdir: str | os.PathLike[str] | None, + monkeypatch: pytest.MonkeyPatch, +) -> None: # Given src_dir = tmp_path_factory.getbasetemp() / 'root' @@ -70,13 +80,13 @@ def test_emit_with_nonascii_name_node(app, status, warning): def test_extensions(app, status, warning): app.setup_extension('shutil') - warning = strip_escseq(warning.getvalue()) + warning = strip_colors(warning.getvalue()) assert "extension 'shutil' has no setup() function" in warning def test_extension_in_blacklist(app, status, warning): app.setup_extension('sphinxjp.themecore') - msg = strip_escseq(warning.getvalue()) + msg = strip_colors(warning.getvalue()) assert msg.startswith("WARNING: the extension 'sphinxjp.themecore' was") diff --git a/tests/test_build_html.py b/tests/test_build_html.py deleted file mode 100644 index 07f101d..0000000 --- a/tests/test_build_html.py +++ /dev/null @@ -1,1841 +0,0 @@ -"""Test the HTML builder and check output against XPath.""" - -import hashlib -import os -import posixpath -import re -from itertools import chain, cycle -from pathlib import Path -from unittest.mock import ANY, call, patch - -import pytest -from html5lib import HTMLParser - -import sphinx.builders.html -from sphinx.builders.html import validate_html_extra_path, validate_html_static_path -from sphinx.builders.html._assets import _file_checksum -from sphinx.errors import ConfigError, ThemeError -from sphinx.testing.util import strip_escseq -from sphinx.util.inventory import InventoryFile - -FIGURE_CAPTION = ".//figure/figcaption/p" - - -ENV_WARNINGS = """\ -%(root)s/autodoc_fodder.py:docstring of autodoc_fodder.MarkupError:\\d+: \ -WARNING: Explicit markup ends without a blank line; unexpected unindent. -%(root)s/index.rst:\\d+: WARNING: Encoding 'utf-8-sig' used for reading included \ -file '%(root)s/wrongenc.inc' seems to be wrong, try giving an :encoding: option -%(root)s/index.rst:\\d+: WARNING: invalid single index entry '' -%(root)s/index.rst:\\d+: WARNING: image file not readable: foo.png -%(root)s/index.rst:\\d+: WARNING: download file not readable: %(root)s/nonexisting.png -%(root)s/undecodable.rst:\\d+: WARNING: undecodable source characters, replacing \ -with "\\?": b?'here: >>>(\\\\|/)xbb<<<((\\\\|/)r)?' -""" - -HTML_WARNINGS = ENV_WARNINGS + """\ -%(root)s/index.rst:\\d+: WARNING: unknown option: '&option' -%(root)s/index.rst:\\d+: WARNING: citation not found: missing -%(root)s/index.rst:\\d+: WARNING: a suitable image for html builder not found: foo.\\* -%(root)s/index.rst:\\d+: WARNING: Lexing literal_block ".*" as "c" resulted in an error at token: ".*". Retrying in relaxed mode. -""" - - -etree_cache = {} - - -@pytest.fixture(scope='module') -def cached_etree_parse(): - def parse(fname): - if fname in etree_cache: - return etree_cache[fname] - with (fname).open('rb') as fp: - etree = HTMLParser(namespaceHTMLElements=False).parse(fp) - etree_cache.clear() - etree_cache[fname] = etree - return etree - yield parse - etree_cache.clear() - - -def flat_dict(d): - return chain.from_iterable( - [ - zip(cycle([fname]), values) - for fname, values in d.items() - ], - ) - - -def tail_check(check): - rex = re.compile(check) - - def checker(nodes): - for node in nodes: - if node.tail and rex.search(node.tail): - return True - msg = f'{check!r} not found in tail of any nodes {nodes}' - raise AssertionError(msg) - return checker - - -def check_xpath(etree, fname, path, check, be_found=True): - nodes = list(etree.findall(path)) - if check is None: - assert nodes == [], ('found any nodes matching xpath ' - '%r in file %s' % (path, fname)) - return - else: - assert nodes != [], ('did not find any node matching xpath ' - '%r in file %s' % (path, fname)) - if callable(check): - check(nodes) - elif not check: - # only check for node presence - pass - else: - def get_text(node): - if node.text is not None: - # the node has only one text - return node.text - else: - # the node has tags and text; gather texts just under the node - return ''.join(n.tail or '' for n in node) - - rex = re.compile(check) - if be_found: - if any(rex.search(get_text(node)) for node in nodes): - return - else: - if all(not rex.search(get_text(node)) for node in nodes): - return - - raise AssertionError('%r not found in any node matching ' - 'path %s in %s: %r' % (check, path, fname, - [node.text for node in nodes])) - - -@pytest.mark.sphinx('html', testroot='warnings') -def test_html_warnings(app, warning): - app.build() - html_warnings = strip_escseq(re.sub(re.escape(os.sep) + '{1,2}', '/', warning.getvalue())) - html_warnings_exp = HTML_WARNINGS % { - 'root': re.escape(app.srcdir.as_posix())} - assert re.match(html_warnings_exp + '$', html_warnings), \ - "Warnings don't match:\n" + \ - '--- Expected (regex):\n' + html_warnings_exp + \ - '--- Got:\n' + html_warnings - - -def test_html4_error(make_app, tmp_path): - (tmp_path / 'conf.py').write_text('', encoding='utf-8') - with pytest.raises( - ConfigError, - match='HTML 4 is no longer supported by Sphinx', - ): - make_app( - buildername='html', - srcdir=tmp_path, - confoverrides={'html4_writer': True}, - ) - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'images.html': [ - (".//img[@src='_images/img.png']", ''), - (".//img[@src='_images/img1.png']", ''), - (".//img[@src='_images/simg.png']", ''), - (".//img[@src='_images/svgimg.svg']", ''), - (".//a[@href='_sources/images.txt']", ''), - ], - 'subdir/images.html': [ - (".//img[@src='../_images/img1.png']", ''), - (".//img[@src='../_images/rimg.png']", ''), - ], - 'subdir/includes.html': [ - (".//a[@class='reference download internal']", ''), - (".//img[@src='../_images/img.png']", ''), - (".//p", 'This is an include file.'), - (".//pre/span", 'line 1'), - (".//pre/span", 'line 2'), - ], - 'includes.html': [ - (".//pre", 'Max Strauß'), - (".//a[@class='reference download internal']", ''), - (".//pre/span", '"quotes"'), - (".//pre/span", "'included'"), - (".//pre/span[@class='s2']", 'üöä'), - (".//div[@class='inc-pyobj1 highlight-text notranslate']//pre", - r'^class Foo:\n pass\n\s*$'), - (".//div[@class='inc-pyobj2 highlight-text notranslate']//pre", - r'^ def baz\(\):\n pass\n\s*$'), - (".//div[@class='inc-lines highlight-text notranslate']//pre", - r'^class Foo:\n pass\nclass Bar:\n$'), - (".//div[@class='inc-startend highlight-text notranslate']//pre", - '^foo = "Including Unicode characters: üöä"\\n$'), - (".//div[@class='inc-preappend highlight-text notranslate']//pre", - r'(?m)^START CODE$'), - (".//div[@class='inc-pyobj-dedent highlight-python notranslate']//span", - r'def'), - (".//div[@class='inc-tab3 highlight-text notranslate']//pre", - r'-| |-'), - (".//div[@class='inc-tab8 highlight-python notranslate']//pre/span", - r'-| |-'), - ], - 'autodoc.html': [ - (".//dl[@class='py class']/dt[@id='autodoc_target.Class']", ''), - (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'\*\*'), - (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'kwds'), - (".//dd/p", r'Return spam\.'), - ], - 'extapi.html': [ - (".//strong", 'from class: Bar'), - ], - 'markup.html': [ - (".//title", 'set by title directive'), - (".//p/em", 'Section author: Georg Brandl'), - (".//p/em", 'Module author: Georg Brandl'), - # created by the meta directive - (".//meta[@name='author'][@content='Me']", ''), - (".//meta[@name='keywords'][@content='docs, sphinx']", ''), - # a label created by ``.. _label:`` - (".//div[@id='label']", ''), - # code with standard code blocks - (".//pre", '^some code$'), - # an option list - (".//span[@class='option']", '--help'), - # admonitions - (".//p[@class='admonition-title']", 'My Admonition'), - (".//div[@class='admonition note']/p", 'Note text.'), - (".//div[@class='admonition warning']/p", 'Warning text.'), - # inline markup - (".//li/p/strong", r'^command\\n$'), - (".//li/p/strong", r'^program\\n$'), - (".//li/p/em", r'^dfn\\n$'), - (".//li/p/kbd", r'^kbd\\n$'), - (".//li/p/span", 'File \N{TRIANGULAR BULLET} Close'), - (".//li/p/code/span[@class='pre']", '^a/$'), - (".//li/p/code/em/span[@class='pre']", '^varpart$'), - (".//li/p/code/em/span[@class='pre']", '^i$'), - (".//a[@href='https://peps.python.org/pep-0008/']" - "[@class='pep reference external']/strong", 'PEP 8'), - (".//a[@href='https://peps.python.org/pep-0008/']" - "[@class='pep reference external']/strong", - 'Python Enhancement Proposal #8'), - (".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']" - "[@class='rfc reference external']/strong", 'RFC 1'), - (".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']" - "[@class='rfc reference external']/strong", 'Request for Comments #1'), - (".//a[@href='objects.html#envvar-HOME']" - "[@class='reference internal']/code/span[@class='pre']", 'HOME'), - (".//a[@href='#with']" - "[@class='reference internal']/code/span[@class='pre']", '^with$'), - (".//a[@href='#grammar-token-try_stmt']" - "[@class='reference internal']/code/span", '^statement$'), - (".//a[@href='#some-label'][@class='reference internal']/span", '^here$'), - (".//a[@href='#some-label'][@class='reference internal']/span", '^there$'), - (".//a[@href='subdir/includes.html']" - "[@class='reference internal']/span", 'Including in subdir'), - (".//a[@href='objects.html#cmdoption-python-c']" - "[@class='reference internal']/code/span[@class='pre']", '-c'), - # abbreviations - (".//abbr[@title='abbreviation']", '^abbr$'), - # version stuff - (".//div[@class='versionadded']/p/span", 'New in version 0.6: '), - (".//div[@class='versionadded']/p/span", - tail_check('First paragraph of versionadded')), - (".//div[@class='versionchanged']/p/span", - tail_check('First paragraph of versionchanged')), - (".//div[@class='versionchanged']/p", - 'Second paragraph of versionchanged'), - # footnote reference - (".//a[@class='footnote-reference brackets']", r'1'), - # created by reference lookup - (".//a[@href='index.html#ref1']", ''), - # ``seealso`` directive - (".//div/p[@class='admonition-title']", 'See also'), - # a ``hlist`` directive - (".//table[@class='hlist']/tbody/tr/td/ul/li/p", '^This$'), - # a ``centered`` directive - (".//p[@class='centered']/strong", 'LICENSE'), - # a glossary - (".//dl/dt[@id='term-boson']", 'boson'), - (".//dl/dt[@id='term-boson']/a", '¶'), - # a production list - (".//pre/strong", 'try_stmt'), - (".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'), - # tests for ``only`` directive - (".//p", 'A global substitution!'), - (".//p", 'In HTML.'), - (".//p", 'In both.'), - (".//p", 'Always present'), - # tests for ``any`` role - (".//a[@href='#with']/span", 'headings'), - (".//a[@href='objects.html#func_without_body']/code/span", 'objects'), - # tests for numeric labels - (".//a[@href='#id1'][@class='reference internal']/span", 'Testing various markup'), - # tests for smartypants - (".//li/p", 'Smart “quotes” in English ‘text’.'), - (".//li/p", 'Smart — long and – short dashes.'), - (".//li/p", 'Ellipsis…'), - (".//li/p/code/span[@class='pre']", 'foo--"bar"...'), - (".//p", 'Этот «абзац» должен использовать „русские“ кавычки.'), - (".//p", 'Il dit : « C’est “super” ! »'), - ], - 'objects.html': [ - (".//dt[@id='mod.Cls.meth1']", ''), - (".//dt[@id='errmod.Error']", ''), - (".//dt/span[@class='sig-name descname']/span[@class='pre']", r'long\(parameter,'), - (".//dt/span[@class='sig-name descname']/span[@class='pre']", r'list\)'), - (".//dt/span[@class='sig-name descname']/span[@class='pre']", 'another'), - (".//dt/span[@class='sig-name descname']/span[@class='pre']", 'one'), - (".//a[@href='#mod.Cls'][@class='reference internal']", ''), - (".//dl[@class='std userdesc']", ''), - (".//dt[@id='userdesc-myobj']", ''), - (".//a[@href='#userdesc-myobj'][@class='reference internal']", ''), - # docfields - (".//a[@class='reference internal'][@href='#TimeInt']/em", 'TimeInt'), - (".//a[@class='reference internal'][@href='#Time']", 'Time'), - (".//a[@class='reference internal'][@href='#errmod.Error']/strong", 'Error'), - # C references - (".//span[@class='pre']", 'CFunction()'), - (".//a[@href='#c.Sphinx_DoSomething']", ''), - (".//a[@href='#c.SphinxStruct.member']", ''), - (".//a[@href='#c.SPHINX_USE_PYTHON']", ''), - (".//a[@href='#c.SphinxType']", ''), - (".//a[@href='#c.sphinx_global']", ''), - # test global TOC created by toctree() - (".//ul[@class='current']/li[@class='toctree-l1 current']/a[@href='#']", - 'Testing object descriptions'), - (".//li[@class='toctree-l1']/a[@href='markup.html']", - 'Testing various markup'), - # test unknown field names - (".//dt[@class='field-odd']", 'Field_name'), - (".//dt[@class='field-even']", 'Field_name all lower'), - (".//dt[@class='field-odd']", 'FIELD_NAME'), - (".//dt[@class='field-even']", 'FIELD_NAME ALL CAPS'), - (".//dt[@class='field-odd']", 'Field_Name'), - (".//dt[@class='field-even']", 'Field_Name All Word Caps'), - (".//dt[@class='field-odd']", 'Field_name'), - (".//dt[@class='field-even']", 'Field_name First word cap'), - (".//dt[@class='field-odd']", 'FIELd_name'), - (".//dt[@class='field-even']", 'FIELd_name PARTial caps'), - # custom sidebar - (".//h4", 'Custom sidebar'), - # docfields - (".//dd[@class='field-odd']/p/strong", '^moo$'), - (".//dd[@class='field-odd']/p/strong", tail_check(r'\(Moo\) .* Moo')), - (".//dd[@class='field-odd']/ul/li/p/strong", '^hour$'), - (".//dd[@class='field-odd']/ul/li/p/em", '^DuplicateType$'), - (".//dd[@class='field-odd']/ul/li/p/em", tail_check(r'.* Some parameter')), - # others - (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span", - 'perl'), - (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span", - '\\+p'), - (".//a[@class='reference internal'][@href='#cmdoption-perl-ObjC']/code/span", - '--ObjC\\+\\+'), - (".//a[@class='reference internal'][@href='#cmdoption-perl-plugin.option']/code/span", - '--plugin.option'), - (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-create-auth-token']" - "/code/span", - 'create-auth-token'), - (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-arg']/code/span", - 'arg'), - (".//a[@class='reference internal'][@href='#cmdoption-perl-j']/code/span", - '-j'), - (".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span", - 'hg'), - (".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span", - 'commit'), - (".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span", - 'git'), - (".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span", - 'commit'), - (".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span", - '-p'), - ], - 'index.html': [ - (".//meta[@name='hc'][@content='hcval']", ''), - (".//meta[@name='hc_co'][@content='hcval_co']", ''), - (".//li[@class='toctree-l1']/a", 'Testing various markup'), - (".//li[@class='toctree-l2']/a", 'Inline markup'), - (".//title", 'Sphinx <Tests>'), - (".//div[@class='footer']", 'copyright text credits'), - (".//a[@href='http://python.org/']" - "[@class='reference external']", ''), - (".//li/p/a[@href='genindex.html']/span", 'Index'), - (".//li/p/a[@href='py-modindex.html']/span", 'Module Index'), - # custom sidebar only for contents - (".//h4", 'Contents sidebar'), - # custom JavaScript - (".//script[@src='file://moo.js']", ''), - # URL in contents - (".//a[@class='reference external'][@href='http://sphinx-doc.org/']", - 'http://sphinx-doc.org/'), - (".//a[@class='reference external'][@href='http://sphinx-doc.org/latest/']", - 'Latest reference'), - # Indirect hyperlink targets across files - (".//a[@href='markup.html#some-label'][@class='reference internal']/span", - '^indirect hyperref$'), - ], - 'bom.html': [ - (".//title", " File with UTF-8 BOM"), - ], - 'extensions.html': [ - (".//a[@href='http://python.org/dev/']", "http://python.org/dev/"), - (".//a[@href='http://bugs.python.org/issue1000']", "issue 1000"), - (".//a[@href='http://bugs.python.org/issue1042']", "explicit caption"), - ], - 'genindex.html': [ - # index entries - (".//a/strong", "Main"), - (".//a/strong", "[1]"), - (".//a/strong", "Other"), - (".//a", "entry"), - (".//li/a", "double"), - ], - 'otherext.html': [ - (".//h1", "Generated section"), - (".//a[@href='_sources/otherext.foo.txt']", ''), - ], -})) -@pytest.mark.sphinx('html', tags=['testtag'], - confoverrides={'html_context.hckey_co': 'hcval_co'}) -@pytest.mark.test_params(shared_result='test_build_html_output') -def test_html5_output(app, cached_etree_parse, fname, expect): - app.build() - print(app.outdir / fname) - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'index.html': [ - (".//div[@class='citation']/span", r'Ref1'), - (".//div[@class='citation']/span", r'Ref_1'), - ], - 'footnote.html': [ - (".//a[@class='footnote-reference brackets'][@href='#id9'][@id='id1']", r"1"), - (".//a[@class='footnote-reference brackets'][@href='#id10'][@id='id2']", r"2"), - (".//a[@class='footnote-reference brackets'][@href='#foo'][@id='id3']", r"3"), - (".//a[@class='reference internal'][@href='#bar'][@id='id4']/span", r"\[bar\]"), - (".//a[@class='reference internal'][@href='#baz-qux'][@id='id5']/span", r"\[baz_qux\]"), - (".//a[@class='footnote-reference brackets'][@href='#id11'][@id='id6']", r"4"), - (".//a[@class='footnote-reference brackets'][@href='#id12'][@id='id7']", r"5"), - (".//aside[@class='footnote brackets']/span/a[@href='#id1']", r"1"), - (".//aside[@class='footnote brackets']/span/a[@href='#id2']", r"2"), - (".//aside[@class='footnote brackets']/span/a[@href='#id3']", r"3"), - (".//div[@class='citation']/span/a[@href='#id4']", r"bar"), - (".//div[@class='citation']/span/a[@href='#id5']", r"baz_qux"), - (".//aside[@class='footnote brackets']/span/a[@href='#id6']", r"4"), - (".//aside[@class='footnote brackets']/span/a[@href='#id7']", r"5"), - (".//aside[@class='footnote brackets']/span/a[@href='#id8']", r"6"), - ], -})) -@pytest.mark.sphinx('html') -@pytest.mark.test_params(shared_result='test_build_html_output_docutils18') -def test_docutils_output(app, cached_etree_parse, fname, expect): - app.build() - print(app.outdir / fname) - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.sphinx('html', parallel=2) -def test_html_parallel(app): - app.build() - - -@pytest.mark.sphinx('html') -@pytest.mark.test_params(shared_result='test_build_html_output') -def test_html_download(app): - app.build() - - # subdir/includes.html - result = (app.outdir / 'subdir' / 'includes.html').read_text(encoding='utf8') - pattern = ('<a class="reference download internal" download="" ' - 'href="../(_downloads/.*/img.png)">') - matched = re.search(pattern, result) - assert matched - assert (app.outdir / matched.group(1)).exists() - filename = matched.group(1) - - # includes.html - result = (app.outdir / 'includes.html').read_text(encoding='utf8') - pattern = ('<a class="reference download internal" download="" ' - 'href="(_downloads/.*/img.png)">') - matched = re.search(pattern, result) - assert matched - assert (app.outdir / matched.group(1)).exists() - assert matched.group(1) == filename - - pattern = ('<a class="reference download internal" download="" ' - 'href="(_downloads/.*/)(file_with_special_%23_chars.xyz)">') - matched = re.search(pattern, result) - assert matched - assert (app.outdir / matched.group(1) / "file_with_special_#_chars.xyz").exists() - - -@pytest.mark.sphinx('html', testroot='roles-download') -def test_html_download_role(app, status, warning): - app.build() - digest = hashlib.md5(b'dummy.dat', usedforsecurity=False).hexdigest() - assert (app.outdir / '_downloads' / digest / 'dummy.dat').exists() - digest_another = hashlib.md5(b'another/dummy.dat', usedforsecurity=False).hexdigest() - assert (app.outdir / '_downloads' / digest_another / 'dummy.dat').exists() - - content = (app.outdir / 'index.html').read_text(encoding='utf8') - assert (('<li><p><a class="reference download internal" download="" ' - 'href="_downloads/%s/dummy.dat">' - '<code class="xref download docutils literal notranslate">' - '<span class="pre">dummy.dat</span></code></a></p></li>' % digest) - in content) - assert (('<li><p><a class="reference download internal" download="" ' - 'href="_downloads/%s/dummy.dat">' - '<code class="xref download docutils literal notranslate">' - '<span class="pre">another/dummy.dat</span></code></a></p></li>' % - digest_another) in content) - assert ('<li><p><code class="xref download docutils literal notranslate">' - '<span class="pre">not_found.dat</span></code></p></li>' in content) - assert ('<li><p><a class="reference download external" download="" ' - 'href="http://www.sphinx-doc.org/en/master/_static/sphinxheader.png">' - '<code class="xref download docutils literal notranslate">' - '<span class="pre">Sphinx</span> <span class="pre">logo</span>' - '</code></a></p></li>' in content) - - -@pytest.mark.sphinx('html', testroot='build-html-translator') -def test_html_translator(app): - app.build() - assert app.builder.docwriter.visitor.depart_with_node == 10 - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'index.html': [ - (".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True), - (".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True), - (".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False), - (".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False), - ], - 'foo.html': [ - (".//h1", 'Foo', True), - (".//h2", 'Foo A', True), - (".//h3", 'Foo A1', True), - (".//h2", 'Foo B', True), - (".//h3", 'Foo B1', True), - - (".//h1//span[@class='section-number']", '1. ', True), - (".//h2//span[@class='section-number']", '1.1. ', True), - (".//h3//span[@class='section-number']", '1.1.1. ', True), - (".//h2//span[@class='section-number']", '1.2. ', True), - (".//h3//span[@class='section-number']", '1.2.1. ', True), - - (".//div[@class='sphinxsidebarwrapper']//li/a", '1.1. Foo A', True), - (".//div[@class='sphinxsidebarwrapper']//li/a", '1.1.1. Foo A1', True), - (".//div[@class='sphinxsidebarwrapper']//li/a", '1.2. Foo B', True), - (".//div[@class='sphinxsidebarwrapper']//li/a", '1.2.1. Foo B1', True), - ], - 'bar.html': [ - (".//h1", 'Bar', True), - (".//h2", 'Bar A', True), - (".//h2", 'Bar B', True), - (".//h3", 'Bar B1', True), - (".//h1//span[@class='section-number']", '2. ', True), - (".//h2//span[@class='section-number']", '2.1. ', True), - (".//h2//span[@class='section-number']", '2.2. ', True), - (".//h3//span[@class='section-number']", '2.2.1. ', True), - (".//div[@class='sphinxsidebarwrapper']//li/a", '2. Bar', True), - (".//div[@class='sphinxsidebarwrapper']//li/a", '2.1. Bar A', True), - (".//div[@class='sphinxsidebarwrapper']//li/a", '2.2. Bar B', True), - (".//div[@class='sphinxsidebarwrapper']//li/a", '2.2.1. Bar B1', False), - ], - 'baz.html': [ - (".//h1", 'Baz A', True), - (".//h1//span[@class='section-number']", '2.1.1. ', True), - ], -})) -@pytest.mark.sphinx('html', testroot='tocdepth') -@pytest.mark.test_params(shared_result='test_build_html_tocdepth') -def test_tocdepth(app, cached_etree_parse, fname, expect): - app.build() - # issue #1251 - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'index.html': [ - (".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True), - (".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True), - (".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False), - (".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False), - - # index.rst - (".//h1", 'test-tocdepth', True), - - # foo.rst - (".//h2", 'Foo', True), - (".//h3", 'Foo A', True), - (".//h4", 'Foo A1', True), - (".//h3", 'Foo B', True), - (".//h4", 'Foo B1', True), - (".//h2//span[@class='section-number']", '1. ', True), - (".//h3//span[@class='section-number']", '1.1. ', True), - (".//h4//span[@class='section-number']", '1.1.1. ', True), - (".//h3//span[@class='section-number']", '1.2. ', True), - (".//h4//span[@class='section-number']", '1.2.1. ', True), - - # bar.rst - (".//h2", 'Bar', True), - (".//h3", 'Bar A', True), - (".//h3", 'Bar B', True), - (".//h4", 'Bar B1', True), - (".//h2//span[@class='section-number']", '2. ', True), - (".//h3//span[@class='section-number']", '2.1. ', True), - (".//h3//span[@class='section-number']", '2.2. ', True), - (".//h4//span[@class='section-number']", '2.2.1. ', True), - - # baz.rst - (".//h4", 'Baz A', True), - (".//h4//span[@class='section-number']", '2.1.1. ', True), - ], -})) -@pytest.mark.sphinx('singlehtml', testroot='tocdepth') -@pytest.mark.test_params(shared_result='test_build_html_tocdepth') -def test_tocdepth_singlehtml(app, cached_etree_parse, fname, expect): - app.build() - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.sphinx('html', testroot='numfig') -@pytest.mark.test_params(shared_result='test_build_html_numfig') -def test_numfig_disabled_warn(app, warning): - app.build() - warnings = warning.getvalue() - assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' in warnings - assert 'index.rst:56: WARNING: invalid numfig_format: invalid' not in warnings - assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' not in warnings - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'index.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", None, True), - (".//table/caption/span[@class='caption-number']", None, True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", None, True), - (".//li/p/code/span", '^fig1$', True), - (".//li/p/code/span", '^Figure%s$', True), - (".//li/p/code/span", '^table-1$', True), - (".//li/p/code/span", '^Table:%s$', True), - (".//li/p/code/span", '^CODE_1$', True), - (".//li/p/code/span", '^Code-%s$', True), - (".//li/p/a/span", '^Section 1$', True), - (".//li/p/a/span", '^Section 2.1$', True), - (".//li/p/code/span", '^Fig.{number}$', True), - (".//li/p/a/span", '^Sect.1 Foo$', True), - ], - 'foo.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", None, True), - (".//table/caption/span[@class='caption-number']", None, True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", None, True), - ], - 'bar.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", None, True), - (".//table/caption/span[@class='caption-number']", None, True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", None, True), - ], - 'baz.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", None, True), - (".//table/caption/span[@class='caption-number']", None, True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", None, True), - ], -})) -@pytest.mark.sphinx('html', testroot='numfig') -@pytest.mark.test_params(shared_result='test_build_html_numfig') -def test_numfig_disabled(app, cached_etree_parse, fname, expect): - app.build() - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.sphinx( - 'html', testroot='numfig', - srcdir='test_numfig_without_numbered_toctree_warn', - confoverrides={'numfig': True}) -def test_numfig_without_numbered_toctree_warn(app, warning): - app.build() - # remove :numbered: option - index = (app.srcdir / 'index.rst').read_text(encoding='utf8') - index = re.sub(':numbered:.*', '', index) - (app.srcdir / 'index.rst').write_text(index, encoding='utf8') - app.build() - - warnings = warning.getvalue() - assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings - assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings - assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings - assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'index.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 9 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 10 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 9 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 10 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 9 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 10 $', True), - (".//li/p/a/span", '^Fig. 9$', True), - (".//li/p/a/span", '^Figure6$', True), - (".//li/p/a/span", '^Table 9$', True), - (".//li/p/a/span", '^Table:6$', True), - (".//li/p/a/span", '^Listing 9$', True), - (".//li/p/a/span", '^Code-6$', True), - (".//li/p/code/span", '^foo$', True), - (".//li/p/code/span", '^bar_a$', True), - (".//li/p/a/span", '^Fig.9 should be Fig.1$', True), - (".//li/p/code/span", '^Sect.{number}$', True), - ], - 'foo.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 3 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 4 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 3 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 4 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 3 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 4 $', True), - ], - 'bar.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 5 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 7 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 8 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 5 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 7 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 8 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 5 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 7 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 8 $', True), - ], - 'baz.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 6 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 6 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 6 $', True), - ], -})) -@pytest.mark.sphinx( - 'html', testroot='numfig', - srcdir='test_numfig_without_numbered_toctree', - confoverrides={'numfig': True}) -def test_numfig_without_numbered_toctree(app, cached_etree_parse, fname, expect): - # remove :numbered: option - index = (app.srcdir / 'index.rst').read_text(encoding='utf8') - index = re.sub(':numbered:.*', '', index) - (app.srcdir / 'index.rst').write_text(index, encoding='utf8') - - if not os.listdir(app.outdir): - app.build() - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True}) -@pytest.mark.test_params(shared_result='test_build_html_numfig_on') -def test_numfig_with_numbered_toctree_warn(app, warning): - app.build() - warnings = warning.getvalue() - assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings - assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings - assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings - assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'index.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2 $', True), - (".//li/p/a/span", '^Fig. 1$', True), - (".//li/p/a/span", '^Figure2.2$', True), - (".//li/p/a/span", '^Table 1$', True), - (".//li/p/a/span", '^Table:2.2$', True), - (".//li/p/a/span", '^Listing 1$', True), - (".//li/p/a/span", '^Code-2.2$', True), - (".//li/p/a/span", '^Section.1$', True), - (".//li/p/a/span", '^Section.2.1$', True), - (".//li/p/a/span", '^Fig.1 should be Fig.1$', True), - (".//li/p/a/span", '^Sect.1 Foo$', True), - ], - 'foo.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.3 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.4 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1.1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1.2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1.3 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1.4 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1.1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1.2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1.3 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1.4 $', True), - ], - 'bar.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.3 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.4 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2.1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2.3 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2.4 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2.1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2.3 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2.4 $', True), - ], - 'baz.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2.2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2.2 $', True), - ], -})) -@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True}) -@pytest.mark.test_params(shared_result='test_build_html_numfig_on') -def test_numfig_with_numbered_toctree(app, cached_etree_parse, fname, expect): - app.build() - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.sphinx('html', testroot='numfig', confoverrides={ - 'numfig': True, - 'numfig_format': {'figure': 'Figure:%s', - 'table': 'Tab_%s', - 'code-block': 'Code-%s', - 'section': 'SECTION-%s'}}) -@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn') -def test_numfig_with_prefix_warn(app, warning): - app.build() - warnings = warning.getvalue() - assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings - assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings - assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings - assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'index.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Tab_1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Tab_2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Code-1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Code-2 $', True), - (".//li/p/a/span", '^Figure:1$', True), - (".//li/p/a/span", '^Figure2.2$', True), - (".//li/p/a/span", '^Tab_1$', True), - (".//li/p/a/span", '^Table:2.2$', True), - (".//li/p/a/span", '^Code-1$', True), - (".//li/p/a/span", '^Code-2.2$', True), - (".//li/p/a/span", '^SECTION-1$', True), - (".//li/p/a/span", '^SECTION-2.1$', True), - (".//li/p/a/span", '^Fig.1 should be Fig.1$', True), - (".//li/p/a/span", '^Sect.1 Foo$', True), - ], - 'foo.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.2 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.3 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.4 $', True), - (".//table/caption/span[@class='caption-number']", - '^Tab_1.1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Tab_1.2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Tab_1.3 $', True), - (".//table/caption/span[@class='caption-number']", - '^Tab_1.4 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Code-1.1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Code-1.2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Code-1.3 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Code-1.4 $', True), - ], - 'bar.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.3 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.4 $', True), - (".//table/caption/span[@class='caption-number']", - '^Tab_2.1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Tab_2.3 $', True), - (".//table/caption/span[@class='caption-number']", - '^Tab_2.4 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Code-2.1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Code-2.3 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Code-2.4 $', True), - ], - 'baz.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Tab_2.2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Code-2.2 $', True), - ], -})) -@pytest.mark.sphinx('html', testroot='numfig', - confoverrides={'numfig': True, - 'numfig_format': {'figure': 'Figure:%s', - 'table': 'Tab_%s', - 'code-block': 'Code-%s', - 'section': 'SECTION-%s'}}) -@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn') -def test_numfig_with_prefix(app, cached_etree_parse, fname, expect): - app.build() - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.sphinx('html', testroot='numfig', - confoverrides={'numfig': True, 'numfig_secnum_depth': 2}) -@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2') -def test_numfig_with_secnum_depth_warn(app, warning): - app.build() - warnings = warning.getvalue() - assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings - assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings - assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings - assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'index.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2 $', True), - (".//li/p/a/span", '^Fig. 1$', True), - (".//li/p/a/span", '^Figure2.1.2$', True), - (".//li/p/a/span", '^Table 1$', True), - (".//li/p/a/span", '^Table:2.1.2$', True), - (".//li/p/a/span", '^Listing 1$', True), - (".//li/p/a/span", '^Code-2.1.2$', True), - (".//li/p/a/span", '^Section.1$', True), - (".//li/p/a/span", '^Section.2.1$', True), - (".//li/p/a/span", '^Fig.1 should be Fig.1$', True), - (".//li/p/a/span", '^Sect.1 Foo$', True), - ], - 'foo.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1.1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1.2 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2.1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1.1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1.1.1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1.1.2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1.2.1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1.1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1.1.1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1.1.2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1.2.1 $', True), - ], - 'bar.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.3 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2.1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2.1.1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2.1.3 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2.2.1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2.1.1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2.1.3 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2.2.1 $', True), - ], - 'baz.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2.1.2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2.1.2 $', True), - ], -})) -@pytest.mark.sphinx('html', testroot='numfig', - confoverrides={'numfig': True, - 'numfig_secnum_depth': 2}) -@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2') -def test_numfig_with_secnum_depth(app, cached_etree_parse, fname, expect): - app.build() - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'index.html': [ - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2 $', True), - (".//li/p/a/span", '^Fig. 1$', True), - (".//li/p/a/span", '^Figure2.2$', True), - (".//li/p/a/span", '^Table 1$', True), - (".//li/p/a/span", '^Table:2.2$', True), - (".//li/p/a/span", '^Listing 1$', True), - (".//li/p/a/span", '^Code-2.2$', True), - (".//li/p/a/span", '^Section.1$', True), - (".//li/p/a/span", '^Section.2.1$', True), - (".//li/p/a/span", '^Fig.1 should be Fig.1$', True), - (".//li/p/a/span", '^Sect.1 Foo$', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.3 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.4 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1.1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1.2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1.3 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 1.4 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1.1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1.2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1.3 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 1.4 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.3 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.4 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2.1 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2.3 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2.4 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2.1 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2.3 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2.4 $', True), - (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2 $', True), - (".//table/caption/span[@class='caption-number']", - '^Table 2.2 $', True), - (".//div[@class='code-block-caption']/" - "span[@class='caption-number']", '^Listing 2.2 $', True), - ], -})) -@pytest.mark.sphinx('singlehtml', testroot='numfig', confoverrides={'numfig': True}) -@pytest.mark.test_params(shared_result='test_build_html_numfig_on') -def test_numfig_with_singlehtml(app, cached_etree_parse, fname, expect): - app.build() - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'index.html': [ - (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 1", True), - (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 2", True), - (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 3", True), - (".//div//span[@class='caption-number']", "No.1 ", True), - (".//div//span[@class='caption-number']", "No.2 ", True), - (".//li/p/a/span", 'Fig. 1', True), - (".//li/p/a/span", 'Fig. 2', True), - (".//li/p/a/span", 'Fig. 3', True), - (".//li/p/a/span", 'No.1', True), - (".//li/p/a/span", 'No.2', True), - ], -})) -@pytest.mark.sphinx('html', testroot='add_enumerable_node', - srcdir='test_enumerable_node') -def test_enumerable_node(app, cached_etree_parse, fname, expect): - app.build() - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.sphinx('html', testroot='html_assets') -def test_html_assets(app): - app.builder.build_all() - - # exclude_path and its family - assert not (app.outdir / 'static' / 'index.html').exists() - assert not (app.outdir / 'extra' / 'index.html').exists() - - # html_static_path - assert not (app.outdir / '_static' / '.htaccess').exists() - assert not (app.outdir / '_static' / '.htpasswd').exists() - assert (app.outdir / '_static' / 'API.html').exists() - assert (app.outdir / '_static' / 'API.html').read_text(encoding='utf8') == 'Sphinx-1.4.4' - assert (app.outdir / '_static' / 'css' / 'style.css').exists() - assert (app.outdir / '_static' / 'js' / 'custom.js').exists() - assert (app.outdir / '_static' / 'rimg.png').exists() - assert not (app.outdir / '_static' / '_build' / 'index.html').exists() - assert (app.outdir / '_static' / 'background.png').exists() - assert not (app.outdir / '_static' / 'subdir' / '.htaccess').exists() - assert not (app.outdir / '_static' / 'subdir' / '.htpasswd').exists() - - # html_extra_path - assert (app.outdir / '.htaccess').exists() - assert not (app.outdir / '.htpasswd').exists() - assert (app.outdir / 'API.html_t').exists() - assert (app.outdir / 'css/style.css').exists() - assert (app.outdir / 'rimg.png').exists() - assert not (app.outdir / '_build' / 'index.html').exists() - assert (app.outdir / 'background.png').exists() - assert (app.outdir / 'subdir' / '.htaccess').exists() - assert not (app.outdir / 'subdir' / '.htpasswd').exists() - - # html_css_files - content = (app.outdir / 'index.html').read_text(encoding='utf8') - assert '<link rel="stylesheet" type="text/css" href="_static/css/style.css" />' in content - assert ('<link media="print" rel="stylesheet" title="title" type="text/css" ' - 'href="https://example.com/custom.css" />' in content) - - # html_js_files - assert '<script src="_static/js/custom.js"></script>' in content - assert ('<script async="async" src="https://example.com/script.js">' - '</script>' in content) - - -@pytest.mark.sphinx('html', testroot='html_assets') -def test_assets_order(app, monkeypatch): - monkeypatch.setattr(sphinx.builders.html, '_file_checksum', lambda o, f: '') - - app.add_css_file('normal.css') - app.add_css_file('early.css', priority=100) - app.add_css_file('late.css', priority=750) - app.add_css_file('lazy.css', priority=900) - app.add_js_file('normal.js') - app.add_js_file('early.js', priority=100) - app.add_js_file('late.js', priority=750) - app.add_js_file('lazy.js', priority=900) - - app.builder.build_all() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - - # css_files - expected = [ - '_static/early.css', - '_static/pygments.css', - '_static/alabaster.css', - 'https://example.com/custom.css', - '_static/normal.css', - '_static/late.css', - '_static/css/style.css', - '_static/lazy.css', - ] - pattern = '.*'.join(f'href="{re.escape(f)}"' for f in expected) - assert re.search(pattern, content, re.DOTALL), content - - # js_files - expected = [ - '_static/early.js', - '_static/doctools.js', - '_static/sphinx_highlight.js', - 'https://example.com/script.js', - '_static/normal.js', - '_static/late.js', - '_static/js/custom.js', - '_static/lazy.js', - ] - pattern = '.*'.join(f'src="{re.escape(f)}"' for f in expected) - assert re.search(pattern, content, re.DOTALL), content - - -@pytest.mark.sphinx('html', testroot='html_file_checksum') -def test_file_checksum(app): - app.add_css_file('stylesheet-a.css') - app.add_css_file('stylesheet-b.css') - app.add_css_file('https://example.com/custom.css') - app.add_js_file('script.js') - app.add_js_file('empty.js') - app.add_js_file('https://example.com/script.js') - - app.builder.build_all() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - - # checksum for local files - assert '<link rel="stylesheet" type="text/css" href="_static/stylesheet-a.css?v=e575b6df" />' in content - assert '<link rel="stylesheet" type="text/css" href="_static/stylesheet-b.css?v=a2d5cc0f" />' in content - assert '<script src="_static/script.js?v=48278d48"></script>' in content - - # empty files have no checksum - assert '<script src="_static/empty.js"></script>' in content - - # no checksum for hyperlinks - assert '<link rel="stylesheet" type="text/css" href="https://example.com/custom.css" />' in content - assert '<script src="https://example.com/script.js"></script>' in content - - -def test_file_checksum_query_string(): - with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'): - _file_checksum(Path(), 'with_query_string.css?dead_parrots=1') - - with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'): - _file_checksum(Path(), 'with_query_string.js?dead_parrots=1') - - with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'): - _file_checksum(Path.cwd(), '_static/with_query_string.css?dead_parrots=1') - - with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'): - _file_checksum(Path.cwd(), '_static/with_query_string.js?dead_parrots=1') - - -@pytest.mark.sphinx('html', testroot='html_assets') -def test_javscript_loading_method(app): - app.add_js_file('normal.js') - app.add_js_file('early.js', loading_method='async') - app.add_js_file('late.js', loading_method='defer') - - app.builder.build_all() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - - assert '<script src="_static/normal.js"></script>' in content - assert '<script async="async" src="_static/early.js"></script>' in content - assert '<script defer="defer" src="_static/late.js"></script>' in content - - -@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_copy_source': False}) -def test_html_copy_source(app): - app.builder.build_all() - assert not (app.outdir / '_sources' / 'index.rst.txt').exists() - - -@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.txt'}) -def test_html_sourcelink_suffix(app): - app.builder.build_all() - assert (app.outdir / '_sources' / 'index.rst.txt').exists() - - -@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.rst'}) -def test_html_sourcelink_suffix_same(app): - app.builder.build_all() - assert (app.outdir / '_sources' / 'index.rst').exists() - - -@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': ''}) -def test_html_sourcelink_suffix_empty(app): - app.builder.build_all() - assert (app.outdir / '_sources' / 'index.rst').exists() - - -@pytest.mark.sphinx('html', testroot='html_entity') -def test_html_entity(app): - app.builder.build_all() - valid_entities = {'amp', 'lt', 'gt', 'quot', 'apos'} - content = (app.outdir / 'index.html').read_text(encoding='utf8') - for entity in re.findall(r'&([a-z]+);', content, re.M): - assert entity not in valid_entities - - -@pytest.mark.sphinx('html', testroot='basic') -def test_html_inventory(app): - app.builder.build_all() - - with app.outdir.joinpath('objects.inv').open('rb') as f: - invdata = InventoryFile.load(f, 'https://www.google.com', posixpath.join) - - assert set(invdata.keys()) == {'std:label', 'std:doc'} - assert set(invdata['std:label'].keys()) == {'modindex', - 'py-modindex', - 'genindex', - 'search'} - assert invdata['std:label']['modindex'] == ('Python', - '', - 'https://www.google.com/py-modindex.html', - 'Module Index') - assert invdata['std:label']['py-modindex'] == ('Python', - '', - 'https://www.google.com/py-modindex.html', - 'Python Module Index') - assert invdata['std:label']['genindex'] == ('Python', - '', - 'https://www.google.com/genindex.html', - 'Index') - assert invdata['std:label']['search'] == ('Python', - '', - 'https://www.google.com/search.html', - 'Search Page') - assert set(invdata['std:doc'].keys()) == {'index'} - assert invdata['std:doc']['index'] == ('Python', - '', - 'https://www.google.com/index.html', - 'The basic Sphinx documentation for testing') - - -@pytest.mark.sphinx('html', testroot='images', confoverrides={'html_sourcelink_suffix': ''}) -def test_html_anchor_for_figure(app): - app.builder.build_all() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ('<figcaption>\n<p><span class="caption-text">The caption of pic</span>' - '<a class="headerlink" href="#id1" title="Link to this image">¶</a></p>\n</figcaption>' - in content) - - -@pytest.mark.sphinx('html', testroot='directives-raw') -def test_html_raw_directive(app, status, warning): - app.builder.build_all() - result = (app.outdir / 'index.html').read_text(encoding='utf8') - - # standard case - assert 'standalone raw directive (HTML)' in result - assert 'standalone raw directive (LaTeX)' not in result - - # with substitution - assert '<p>HTML: abc def ghi</p>' in result - assert '<p>LaTeX: abc ghi</p>' in result - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'index.html': [ - (".//link[@href='_static/persistent.css']" - "[@rel='stylesheet']", '', True), - (".//link[@href='_static/default.css']" - "[@rel='stylesheet']" - "[@title='Default']", '', True), - (".//link[@href='_static/alternate1.css']" - "[@rel='alternate stylesheet']" - "[@title='Alternate']", '', True), - (".//link[@href='_static/alternate2.css']" - "[@rel='alternate stylesheet']", '', True), - (".//link[@href='_static/more_persistent.css']" - "[@rel='stylesheet']", '', True), - (".//link[@href='_static/more_default.css']" - "[@rel='stylesheet']" - "[@title='Default']", '', True), - (".//link[@href='_static/more_alternate1.css']" - "[@rel='alternate stylesheet']" - "[@title='Alternate']", '', True), - (".//link[@href='_static/more_alternate2.css']" - "[@rel='alternate stylesheet']", '', True), - ], -})) -@pytest.mark.sphinx('html', testroot='stylesheets') -def test_alternate_stylesheets(app, cached_etree_parse, fname, expect): - app.build() - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.sphinx('html', testroot='html_style') -def test_html_style(app, status, warning): - app.build() - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert '<link rel="stylesheet" type="text/css" href="_static/default.css" />' in result - assert ('<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />' - not in result) - - -@pytest.mark.sphinx('html', testroot='images') -def test_html_remote_images(app, status, warning): - app.builder.build_all() - - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ('<img alt="https://www.python.org/static/img/python-logo.png" ' - 'src="https://www.python.org/static/img/python-logo.png" />' in result) - assert not (app.outdir / 'python-logo.png').exists() - - -@pytest.mark.sphinx('html', testroot='image-escape') -def test_html_encoded_image(app, status, warning): - app.builder.build_all() - - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ('<img alt="_images/img_%231.png" src="_images/img_%231.png" />' in result) - assert (app.outdir / '_images/img_#1.png').exists() - - -@pytest.mark.sphinx('html', testroot='remote-logo') -def test_html_remote_logo(app, status, warning): - app.builder.build_all() - - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ('<img class="logo" src="https://www.python.org/static/img/python-logo.png" alt="Logo"/>' in result) - assert ('<link rel="icon" href="https://www.python.org/static/favicon.ico"/>' in result) - assert not (app.outdir / 'python-logo.png').exists() - - -@pytest.mark.sphinx('html', testroot='local-logo') -def test_html_local_logo(app, status, warning): - app.builder.build_all() - - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ('<img class="logo" src="_static/img.png" alt="Logo"/>' in result) - assert (app.outdir / '_static/img.png').exists() - - -@pytest.mark.sphinx('html', testroot='basic') -def test_html_sidebar(app, status, warning): - ctx = {} - - # default for alabaster - app.builder.build_all() - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ('<div class="sphinxsidebar" role="navigation" ' - 'aria-label="main navigation">' in result) - assert '<h1 class="logo"><a href="#">Python</a></h1>' in result - assert '<h3>Navigation</h3>' in result - assert '<h3>Related Topics</h3>' in result - assert '<h3 id="searchlabel">Quick search</h3>' in result - - app.builder.add_sidebars('index', ctx) - assert ctx['sidebars'] == ['about.html', 'navigation.html', 'relations.html', - 'searchbox.html', 'donate.html'] - - # only relations.html - app.config.html_sidebars = {'**': ['relations.html']} - app.builder.build_all() - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ('<div class="sphinxsidebar" role="navigation" ' - 'aria-label="main navigation">' in result) - assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result - assert '<h3>Navigation</h3>' not in result - assert '<h3>Related Topics</h3>' in result - assert '<h3 id="searchlabel">Quick search</h3>' not in result - - app.builder.add_sidebars('index', ctx) - assert ctx['sidebars'] == ['relations.html'] - - # no sidebars - app.config.html_sidebars = {'**': []} - app.builder.build_all() - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ('<div class="sphinxsidebar" role="navigation" ' - 'aria-label="main navigation">' not in result) - assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result - assert '<h3>Navigation</h3>' not in result - assert '<h3>Related Topics</h3>' not in result - assert '<h3 id="searchlabel">Quick search</h3>' not in result - - app.builder.add_sidebars('index', ctx) - assert ctx['sidebars'] == [] - - -@pytest.mark.parametrize(("fname", "expect"), flat_dict({ - 'index.html': [(".//em/a[@href='https://example.com/man.1']", "", True), - (".//em/a[@href='https://example.com/ls.1']", "", True), - (".//em/a[@href='https://example.com/sphinx.']", "", True)], - -})) -@pytest.mark.sphinx('html', testroot='manpage_url', confoverrides={ - 'manpages_url': 'https://example.com/{page}.{section}'}) -@pytest.mark.test_params(shared_result='test_build_html_manpage_url') -def test_html_manpage(app, cached_etree_parse, fname, expect): - app.build() - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@pytest.mark.sphinx('html', testroot='toctree-glob', - confoverrides={'html_baseurl': 'https://example.com/'}) -def test_html_baseurl(app, status, warning): - app.build() - - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert '<link rel="canonical" href="https://example.com/index.html" />' in result - - result = (app.outdir / 'qux' / 'index.html').read_text(encoding='utf8') - assert '<link rel="canonical" href="https://example.com/qux/index.html" />' in result - - -@pytest.mark.sphinx('html', testroot='toctree-glob', - confoverrides={'html_baseurl': 'https://example.com/subdir', - 'html_file_suffix': '.htm'}) -def test_html_baseurl_and_html_file_suffix(app, status, warning): - app.build() - - result = (app.outdir / 'index.htm').read_text(encoding='utf8') - assert '<link rel="canonical" href="https://example.com/subdir/index.htm" />' in result - - result = (app.outdir / 'qux' / 'index.htm').read_text(encoding='utf8') - assert '<link rel="canonical" href="https://example.com/subdir/qux/index.htm" />' in result - - -@pytest.mark.sphinx('html', testroot='basic') -def test_default_html_math_renderer(app, status, warning): - assert app.builder.math_renderer_name == 'mathjax' - - -@pytest.mark.sphinx('html', testroot='basic', - confoverrides={'extensions': ['sphinx.ext.mathjax']}) -def test_html_math_renderer_is_mathjax(app, status, warning): - assert app.builder.math_renderer_name == 'mathjax' - - -@pytest.mark.sphinx('html', testroot='basic', - confoverrides={'extensions': ['sphinx.ext.imgmath']}) -def test_html_math_renderer_is_imgmath(app, status, warning): - assert app.builder.math_renderer_name == 'imgmath' - - -@pytest.mark.sphinx('html', testroot='basic', - confoverrides={'extensions': ['sphinxcontrib.jsmath', - 'sphinx.ext.imgmath']}) -def test_html_math_renderer_is_duplicated(make_app, app_params): - args, kwargs = app_params - with pytest.raises( - ConfigError, - match='Many math_renderers are registered. But no math_renderer is selected.', - ): - make_app(*args, **kwargs) - - -@pytest.mark.sphinx('html', testroot='basic', - confoverrides={'extensions': ['sphinx.ext.imgmath', - 'sphinx.ext.mathjax']}) -def test_html_math_renderer_is_duplicated2(app, status, warning): - # case of both mathjax and another math_renderer is loaded - assert app.builder.math_renderer_name == 'imgmath' # The another one is chosen - - -@pytest.mark.sphinx('html', testroot='basic', - confoverrides={'extensions': ['sphinxcontrib.jsmath', - 'sphinx.ext.imgmath'], - 'html_math_renderer': 'imgmath'}) -def test_html_math_renderer_is_chosen(app, status, warning): - assert app.builder.math_renderer_name == 'imgmath' - - -@pytest.mark.sphinx('html', testroot='basic', - confoverrides={'extensions': ['sphinxcontrib.jsmath', - 'sphinx.ext.mathjax'], - 'html_math_renderer': 'imgmath'}) -def test_html_math_renderer_is_mismatched(make_app, app_params): - args, kwargs = app_params - with pytest.raises(ConfigError, match="Unknown math_renderer 'imgmath' is given."): - make_app(*args, **kwargs) - - -@pytest.mark.sphinx('html', testroot='basic') -def test_html_pygments_style_default(app): - style = app.builder.highlighter.formatter_args.get('style') - assert style.__name__ == 'Alabaster' - - -@pytest.mark.sphinx('html', testroot='basic', - confoverrides={'pygments_style': 'sphinx'}) -def test_html_pygments_style_manually(app): - style = app.builder.highlighter.formatter_args.get('style') - assert style.__name__ == 'SphinxStyle' - - -@pytest.mark.sphinx('html', testroot='basic', - confoverrides={'html_theme': 'classic'}) -def test_html_pygments_for_classic_theme(app): - style = app.builder.highlighter.formatter_args.get('style') - assert style.__name__ == 'SphinxStyle' - - -@pytest.mark.sphinx('html', testroot='basic') -def test_html_dark_pygments_style_default(app): - assert app.builder.dark_highlighter is None - - -@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_extra_path') -def test_validate_html_extra_path(app): - (app.confdir / '_static').mkdir(parents=True, exist_ok=True) - app.config.html_extra_path = [ - '/path/to/not_found', # not found - '_static', - app.outdir, # outdir - app.outdir / '_static', # inside outdir - ] - validate_html_extra_path(app, app.config) - assert app.config.html_extra_path == ['_static'] - - -@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_static_path') -def test_validate_html_static_path(app): - (app.confdir / '_static').mkdir(parents=True, exist_ok=True) - app.config.html_static_path = [ - '/path/to/not_found', # not found - '_static', - app.outdir, # outdir - app.outdir / '_static', # inside outdir - ] - validate_html_static_path(app, app.config) - assert app.config.html_static_path == ['_static'] - - -@pytest.mark.sphinx(testroot='html_scaled_image_link') -def test_html_scaled_image_link(app): - app.build() - context = (app.outdir / 'index.html').read_text(encoding='utf8') - - # no scaled parameters - assert re.search('\n<img alt="_images/img.png" src="_images/img.png" />', context) - - # scaled_image_link - assert re.search('\n<a class="reference internal image-reference" href="_images/img.png">' - '<img alt="_images/img.png" src="_images/img.png" style="[^"]+" /></a>', - context) - - # no-scaled-link class disables the feature - assert re.search('\n<img alt="_images/img.png" class="no-scaled-link"' - ' src="_images/img.png" style="[^"]+" />', - context) - - -@pytest.mark.sphinx('html', testroot='reST-code-block', - confoverrides={'html_codeblock_linenos_style': 'table'}) -def test_html_codeblock_linenos_style_table(app): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - - assert ('<div class="linenodiv"><pre><span class="normal">1</span>\n' - '<span class="normal">2</span>\n' - '<span class="normal">3</span>\n' - '<span class="normal">4</span></pre></div>') in content - - -@pytest.mark.sphinx('html', testroot='reST-code-block', - confoverrides={'html_codeblock_linenos_style': 'inline'}) -def test_html_codeblock_linenos_style_inline(app): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - - assert '<span class="linenos">1</span>' in content - - -@pytest.mark.sphinx('html', testroot='highlight_options') -def test_highlight_options(app): - subject = app.builder.highlighter - with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight: - app.build() - - call_args = highlight.call_args_list - assert len(call_args) == 3 - assert call_args[0] == call(ANY, 'default', force=False, linenos=False, - location=ANY, opts={'default_option': True}) - assert call_args[1] == call(ANY, 'python', force=False, linenos=False, - location=ANY, opts={'python_option': True}) - assert call_args[2] == call(ANY, 'java', force=False, linenos=False, - location=ANY, opts={}) - - -@pytest.mark.sphinx('html', testroot='highlight_options', - confoverrides={'highlight_options': {'default_option': True}}) -def test_highlight_options_old(app): - subject = app.builder.highlighter - with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight: - app.build() - - call_args = highlight.call_args_list - assert len(call_args) == 3 - assert call_args[0] == call(ANY, 'default', force=False, linenos=False, - location=ANY, opts={'default_option': True}) - assert call_args[1] == call(ANY, 'python', force=False, linenos=False, - location=ANY, opts={}) - assert call_args[2] == call(ANY, 'java', force=False, linenos=False, - location=ANY, opts={}) - - -@pytest.mark.sphinx('html', testroot='basic', - confoverrides={'html_permalinks': False}) -def test_html_permalink_disable(app): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - - assert '<h1>The basic Sphinx documentation for testing</h1>' in content - - -@pytest.mark.sphinx('html', testroot='basic', - confoverrides={'html_permalinks_icon': '<span>[PERMALINK]</span>'}) -def test_html_permalink_icon(app): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - - assert ('<h1>The basic Sphinx documentation for testing<a class="headerlink" ' - 'href="#the-basic-sphinx-documentation-for-testing" ' - 'title="Link to this heading"><span>[PERMALINK]</span></a></h1>' in content) - - -@pytest.mark.sphinx('html', testroot='html_signaturereturn_icon') -def test_html_signaturereturn_icon(app): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - - assert ('<span class="sig-return-icon">→</span>' in content) - - -@pytest.mark.sphinx('html', testroot='reST-code-role') -def test_html_code_role(app): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - - common_content = ( - '<span class="k">def</span> <span class="nf">foo</span>' - '<span class="p">(</span>' - '<span class="mi">1</span> ' - '<span class="o">+</span> ' - '<span class="mi">2</span> ' - '<span class="o">+</span> ' - '<span class="kc">None</span> ' - '<span class="o">+</span> ' - '<span class="s2">"abc"</span>' - '<span class="p">):</span> ' - '<span class="k">pass</span>') - assert ('<p>Inline <code class="code highlight python docutils literal highlight-python">' + - common_content + '</code> code block</p>') in content - assert ('<div class="highlight-python notranslate">' + - '<div class="highlight"><pre><span></span>' + - common_content) in content - - -@pytest.mark.sphinx('html', testroot='root', - confoverrides={'option_emphasise_placeholders': True}) -def test_option_emphasise_placeholders(app, status, warning): - app.build() - content = (app.outdir / 'objects.html').read_text(encoding='utf8') - assert '<em><span class="pre">TYPE</span></em>' in content - assert '{TYPE}' not in content - assert ('<em><span class="pre">WHERE</span></em>' - '<span class="pre">-</span>' - '<em><span class="pre">COUNT</span></em>' in content) - assert '<span class="pre">{{value}}</span>' in content - assert ('<span class="pre">--plugin.option</span></span>' - '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Link to this definition">¶</a></dt>') in content - - -@pytest.mark.sphinx('html', testroot='root') -def test_option_emphasise_placeholders_default(app, status, warning): - app.build() - content = (app.outdir / 'objects.html').read_text(encoding='utf8') - assert '<span class="pre">={TYPE}</span>' in content - assert '<span class="pre">={WHERE}-{COUNT}</span></span>' in content - assert '<span class="pre">{client_name}</span>' in content - assert ('<span class="pre">--plugin.option</span></span>' - '<span class="sig-prename descclassname"></span>' - '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Link to this definition">¶</a></dt>') in content - - -@pytest.mark.sphinx('html', testroot='root') -def test_option_reference_with_value(app, status, warning): - app.build() - content = (app.outdir / 'objects.html').read_text(encoding='utf-8') - assert ('<span class="pre">-mapi</span></span><span class="sig-prename descclassname">' - '</span><a class="headerlink" href="#cmdoption-git-commit-mapi"') in content - assert 'first option <a class="reference internal" href="#cmdoption-git-commit-mapi">' in content - assert ('<a class="reference internal" href="#cmdoption-git-commit-mapi">' - '<code class="xref std std-option docutils literal notranslate"><span class="pre">-mapi[=xxx]</span></code></a>') in content - assert '<span class="pre">-mapi</span> <span class="pre">with_space</span>' in content - - -@pytest.mark.sphinx('html', testroot='theming') -def test_theme_options(app, status, warning): - app.build() - - result = (app.outdir / '_static' / 'documentation_options.js').read_text(encoding='utf8') - assert 'NAVIGATION_WITH_KEYS: false' in result - assert 'ENABLE_SEARCH_SHORTCUTS: true' in result - - -@pytest.mark.sphinx('html', testroot='theming', - confoverrides={'html_theme_options.navigation_with_keys': True, - 'html_theme_options.enable_search_shortcuts': False}) -def test_theme_options_with_override(app, status, warning): - app.build() - - result = (app.outdir / '_static' / 'documentation_options.js').read_text(encoding='utf8') - assert 'NAVIGATION_WITH_KEYS: true' in result - assert 'ENABLE_SEARCH_SHORTCUTS: false' in result - - -@pytest.mark.sphinx('html', testroot='build-html-theme-having-multiple-stylesheets') -def test_theme_having_multiple_stylesheets(app): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf-8') - - assert '<link rel="stylesheet" type="text/css" href="_static/mytheme.css" />' in content - assert '<link rel="stylesheet" type="text/css" href="_static/extra.css" />' in content - - -@pytest.mark.sphinx('html', testroot='images') -def test_copy_images(app, status, warning): - app.build() - - images_dir = Path(app.outdir) / '_images' - images = {image.name for image in images_dir.rglob('*')} - assert images == { - 'img.png', - 'rimg.png', - 'rimg1.png', - 'svgimg.svg', - 'testimäge.png', - } diff --git a/tests/test_builders/__init__.py b/tests/test_builders/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_builders/__init__.py diff --git a/tests/test_builders/conftest.py b/tests/test_builders/conftest.py new file mode 100644 index 0000000..1203d5d --- /dev/null +++ b/tests/test_builders/conftest.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from sphinx.testing.util import etree_parse + +if TYPE_CHECKING: + from collections.abc import Callable, Iterator + from pathlib import Path + from xml.etree.ElementTree import ElementTree + +_etree_cache: dict[Path, ElementTree] = {} + + +def _parse(path: Path) -> ElementTree: + if path in _etree_cache: + return _etree_cache[path] + + _etree_cache[path] = tree = etree_parse(path) + return tree + + +@pytest.fixture(scope='package') +def cached_etree_parse() -> Iterator[Callable[[Path], ElementTree]]: + yield _parse + _etree_cache.clear() diff --git a/tests/test_build.py b/tests/test_builders/test_build.py index ed4bc43..3f6d12c 100644 --- a/tests/test_build.py +++ b/tests/test_builders/test_build.py @@ -2,13 +2,17 @@ import os import shutil +from contextlib import contextmanager from unittest import mock import pytest from docutils import nodes +from sphinx.cmd.build import build_main from sphinx.errors import SphinxError +from tests.utils import TESTS_ROOT + def request_session_head(url, **kwargs): response = mock.Mock() @@ -60,12 +64,12 @@ def test_root_doc_not_found(tmp_path, make_app): app = make_app('dummy', srcdir=tmp_path) with pytest.raises(SphinxError): - app.builder.build_all() # no index.rst + app.build(force_all=True) # no index.rst @pytest.mark.sphinx(buildername='text', testroot='circular') def test_circular_toctree(app, status, warning): - app.builder.build_all() + app.build(force_all=True) warnings = warning.getvalue() assert ( 'circular toctree references detected, ignoring: ' @@ -77,7 +81,7 @@ def test_circular_toctree(app, status, warning): @pytest.mark.sphinx(buildername='text', testroot='numbered-circular') def test_numbered_circular_toctree(app, status, warning): - app.builder.build_all() + app.build(force_all=True) warnings = warning.getvalue() assert ( 'circular toctree references detected, ignoring: ' @@ -89,7 +93,7 @@ def test_numbered_circular_toctree(app, status, warning): @pytest.mark.sphinx(buildername='dummy', testroot='images') def test_image_glob(app, status, warning): - app.builder.build_all() + app.build(force_all=True) # index.rst doctree = app.env.get_doctree('index') @@ -133,3 +137,29 @@ def test_image_glob(app, status, warning): assert doctree[0][3][0]['candidates'] == {'application/pdf': 'subdir/svgimg.pdf', 'image/svg+xml': 'subdir/svgimg.svg'} assert doctree[0][3][0]['uri'] == 'subdir/svgimg.*' + + +@contextmanager +def force_colors(): + forcecolor = os.environ.get('FORCE_COLOR', None) + + try: + os.environ['FORCE_COLOR'] = '1' + yield + finally: + if forcecolor is None: + os.environ.pop('FORCE_COLOR', None) + else: + os.environ['FORCE_COLOR'] = forcecolor + + +def test_log_no_ansi_colors(tmp_path): + with force_colors(): + wfile = tmp_path / 'warnings.txt' + srcdir = TESTS_ROOT / 'roots' / 'test-nitpicky-warnings' + argv = list(map(str, ['-b', 'html', srcdir, tmp_path, '-n', '-w', wfile])) + retcode = build_main(argv) + assert retcode == 0 + + content = wfile.read_text(encoding='utf8') + assert '\x1b[91m' not in content diff --git a/tests/test_build_changes.py b/tests/test_builders/test_build_changes.py index b340c8d..b537b87 100644 --- a/tests/test_build_changes.py +++ b/tests/test_builders/test_build_changes.py @@ -9,7 +9,7 @@ def test_build(app): # TODO: Use better checking of html content htmltext = (app.outdir / 'changes.html').read_text(encoding='utf8') - assert 'New in version 0.6: Some funny stuff.' in htmltext + assert 'Added in version 0.6: Some funny stuff.' in htmltext assert 'Changed in version 0.6: Even more funny stuff.' in htmltext assert 'Deprecated since version 0.6: Boring stuff.' in htmltext diff --git a/tests/test_build_dirhtml.py b/tests/test_builders/test_build_dirhtml.py index dc5ab86..dc5ab86 100644 --- a/tests/test_build_dirhtml.py +++ b/tests/test_builders/test_build_dirhtml.py diff --git a/tests/test_build_epub.py b/tests/test_builders/test_build_epub.py index 7f5b815..6829f22 100644 --- a/tests/test_build_epub.py +++ b/tests/test_builders/test_build_epub.py @@ -22,6 +22,7 @@ def runnable(command): class EPUBElementTree: """Test helper for content.opf and toc.ncx""" + namespaces = { 'idpf': 'http://www.idpf.org/2007/opf', 'dc': 'http://purl.org/dc/elements/1.1/', @@ -60,7 +61,7 @@ class EPUBElementTree: @pytest.mark.sphinx('epub', testroot='basic') def test_build_epub(app): - app.builder.build_all() + app.build(force_all=True) assert (app.outdir / 'mimetype').read_text(encoding='utf8') == 'application/epub+zip' assert (app.outdir / 'META-INF' / 'container.xml').exists() @@ -277,7 +278,7 @@ def test_escaped_toc(app): @pytest.mark.sphinx('epub', testroot='basic') def test_epub_writing_mode(app): # horizontal (default) - app.builder.build_all() + app.build(force_all=True) # horizontal / page-progression-direction opf = EPUBElementTree.fromstring((app.outdir / 'content.opf').read_text(encoding='utf8')) @@ -323,7 +324,7 @@ def test_epub_anchor_id(app): @pytest.mark.sphinx('epub', testroot='html_assets') def test_epub_assets(app): - app.builder.build_all() + app.build(force_all=True) # epub_sytlesheets (same as html_css_files) content = (app.outdir / 'index.xhtml').read_text(encoding='utf8') @@ -336,7 +337,7 @@ def test_epub_assets(app): @pytest.mark.sphinx('epub', testroot='html_assets', confoverrides={'epub_css_files': ['css/epub.css']}) def test_epub_css_files(app): - app.builder.build_all() + app.build(force_all=True) # epub_css_files content = (app.outdir / 'index.xhtml').read_text(encoding='utf8') @@ -361,13 +362,13 @@ def test_html_download_role(app, status, warning): '<span class="pre">not_found.dat</span></code></p></li>' in content) assert ('<li><p><code class="xref download docutils literal notranslate">' '<span class="pre">Sphinx</span> <span class="pre">logo</span></code>' - '<span class="link-target"> [http://www.sphinx-doc.org/en/master' - '/_static/sphinxheader.png]</span></p></li>' in content) + '<span class="link-target"> [https://www.sphinx-doc.org/en/master' + '/_static/sphinx-logo.svg]</span></p></li>' in content) @pytest.mark.sphinx('epub', testroot='toctree-duplicated') def test_duplicated_toctree_entry(app, status, warning): - app.builder.build_all() + app.build(force_all=True) assert 'WARNING: duplicated ToC entry found: foo.xhtml' in warning.getvalue() @@ -377,16 +378,21 @@ def test_duplicated_toctree_entry(app, status, warning): def test_run_epubcheck(app): app.build() + if not runnable(['java', '-version']): + pytest.skip("Unable to run Java; skipping test") + epubcheck = os.environ.get('EPUBCHECK_PATH', '/usr/share/java/epubcheck.jar') - if runnable(['java', '-version']) and os.path.exists(epubcheck): - try: - subprocess.run(['java', '-jar', epubcheck, app.outdir / 'SphinxTests.epub'], - capture_output=True, check=True) - except CalledProcessError as exc: - print(exc.stdout.decode('utf-8')) - print(exc.stderr.decode('utf-8')) - msg = f'epubcheck exited with return code {exc.returncode}' - raise AssertionError(msg) from exc + if not os.path.exists(epubcheck): + pytest.skip("Could not find epubcheck; skipping test") + + try: + subprocess.run(['java', '-jar', epubcheck, app.outdir / 'SphinxTests.epub'], + capture_output=True, check=True) + except CalledProcessError as exc: + print(exc.stdout.decode('utf-8')) + print(exc.stderr.decode('utf-8')) + msg = f'epubcheck exited with return code {exc.returncode}' + raise AssertionError(msg) from exc def test_xml_name_pattern_check(): diff --git a/tests/test_build_gettext.py b/tests/test_builders/test_build_gettext.py index 6d9154e..ddc6d30 100644 --- a/tests/test_build_gettext.py +++ b/tests/test_builders/test_build_gettext.py @@ -47,7 +47,7 @@ def test_Catalog_duplicated_message(): @pytest.mark.sphinx('gettext', srcdir='root-gettext') def test_build_gettext(app): # Generic build; should fail only when the builder is horribly broken. - app.builder.build_all() + app.build(force_all=True) # Do messages end up in the correct location? # top-level documents end up in a message catalog @@ -62,7 +62,7 @@ def test_build_gettext(app): @pytest.mark.sphinx('gettext', srcdir='root-gettext') def test_msgfmt(app): - app.builder.build_all() + app.build(force_all=True) (app.outdir / 'en' / 'LC_MESSAGES').mkdir(parents=True, exist_ok=True) with chdir(app.outdir): @@ -102,7 +102,7 @@ def test_msgfmt(app): confoverrides={'gettext_compact': False}) def test_gettext_index_entries(app): # regression test for #976 - app.builder.build(['index_entries']) + app.build(filenames=[app.srcdir / 'index_entries.txt']) pot = (app.outdir / 'index_entries.pot').read_text(encoding='utf8') msg_ids = list(filter(None, map(msgid_getter, pot.splitlines()))) @@ -131,7 +131,7 @@ def test_gettext_index_entries(app): def test_gettext_disable_index_entries(app): # regression test for #976 app.env._pickled_doctree_cache.clear() # clear cache - app.builder.build(['index_entries']) + app.build(filenames=[app.srcdir / 'index_entries.txt']) pot = (app.outdir / 'index_entries.pot').read_text(encoding='utf8') msg_ids = list(filter(None, map(msgid_getter, pot.splitlines()))) @@ -147,7 +147,7 @@ def test_gettext_disable_index_entries(app): @pytest.mark.sphinx('gettext', testroot='intl', srcdir='gettext') def test_gettext_template(app): - app.builder.build_all() + app.build(force_all=True) assert (app.outdir / 'sphinx.pot').is_file() @@ -158,7 +158,7 @@ def test_gettext_template(app): @pytest.mark.sphinx('gettext', testroot='gettext-template') def test_gettext_template_msgid_order_in_sphinxpot(app): - app.builder.build_all() + app.build(force_all=True) assert (app.outdir / 'sphinx.pot').is_file() result = (app.outdir / 'sphinx.pot').read_text(encoding='utf8') @@ -175,7 +175,7 @@ def test_gettext_template_msgid_order_in_sphinxpot(app): 'gettext', srcdir='root-gettext', confoverrides={'gettext_compact': 'documentation'}) def test_build_single_pot(app): - app.builder.build_all() + app.build(force_all=True) assert (app.outdir / 'documentation.pot').is_file() @@ -196,7 +196,7 @@ def test_build_single_pot(app): confoverrides={'gettext_compact': False, 'gettext_additional_targets': ['image']}) def test_gettext_prolog_epilog_substitution(app): - app.builder.build_all() + app.build(force_all=True) assert (app.outdir / 'prolog_epilog_substitution.pot').is_file() pot = (app.outdir / 'prolog_epilog_substitution.pot').read_text(encoding='utf8') @@ -223,7 +223,7 @@ def test_gettext_prolog_epilog_substitution(app): 'gettext_additional_targets': ['image']}) def test_gettext_prolog_epilog_substitution_excluded(app): # regression test for #9428 - app.builder.build_all() + app.build(force_all=True) assert (app.outdir / 'prolog_epilog_substitution_excluded.pot').is_file() pot = (app.outdir / 'prolog_epilog_substitution_excluded.pot').read_text(encoding='utf8') diff --git a/tests/test_builders/test_build_html.py b/tests/test_builders/test_build_html.py new file mode 100644 index 0000000..1fa3ba4 --- /dev/null +++ b/tests/test_builders/test_build_html.py @@ -0,0 +1,378 @@ +"""Test the HTML builder and check output against XPath.""" + +import os +import posixpath +import re + +import pytest + +from sphinx.builders.html import validate_html_extra_path, validate_html_static_path +from sphinx.deprecation import RemovedInSphinx80Warning +from sphinx.errors import ConfigError +from sphinx.util.console import strip_colors +from sphinx.util.inventory import InventoryFile + +from tests.test_builders.xpath_data import FIGURE_CAPTION +from tests.test_builders.xpath_util import check_xpath + + +def test_html4_error(make_app, tmp_path): + (tmp_path / 'conf.py').write_text('', encoding='utf-8') + with pytest.raises( + ConfigError, + match='HTML 4 is no longer supported by Sphinx', + ): + make_app( + buildername='html', + srcdir=tmp_path, + confoverrides={'html4_writer': True}, + ) + + +@pytest.mark.parametrize(("fname", "path", "check"), [ + ('index.html', ".//div[@class='citation']/span", r'Ref1'), + ('index.html', ".//div[@class='citation']/span", r'Ref_1'), + + ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id9'][@id='id1']", r"1"), + ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id10'][@id='id2']", r"2"), + ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#foo'][@id='id3']", r"3"), + ('footnote.html', ".//a[@class='reference internal'][@href='#bar'][@id='id4']/span", r"\[bar\]"), + ('footnote.html', ".//a[@class='reference internal'][@href='#baz-qux'][@id='id5']/span", r"\[baz_qux\]"), + ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id11'][@id='id6']", r"4"), + ('footnote.html', ".//a[@class='footnote-reference brackets'][@href='#id12'][@id='id7']", r"5"), + ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id1']", r"1"), + ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id2']", r"2"), + ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id3']", r"3"), + ('footnote.html', ".//div[@class='citation']/span/a[@href='#id4']", r"bar"), + ('footnote.html', ".//div[@class='citation']/span/a[@href='#id5']", r"baz_qux"), + ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id6']", r"4"), + ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id7']", r"5"), + ('footnote.html', ".//aside[@class='footnote brackets']/span/a[@href='#id8']", r"6"), +]) +@pytest.mark.sphinx('html') +@pytest.mark.test_params(shared_result='test_build_html_output_docutils18') +def test_docutils_output(app, cached_etree_parse, fname, path, check): + app.build() + check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check) + + +@pytest.mark.sphinx('html', parallel=2) +def test_html_parallel(app): + app.build() + + +@pytest.mark.sphinx('html', testroot='build-html-translator') +def test_html_translator(app): + app.build() + assert app.builder.docwriter.visitor.depart_with_node == 10 + + +@pytest.mark.parametrize("expect", [ + (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 1", True), + (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 2", True), + (FIGURE_CAPTION + "//span[@class='caption-number']", "Fig. 3", True), + (".//div//span[@class='caption-number']", "No.1 ", True), + (".//div//span[@class='caption-number']", "No.2 ", True), + (".//li/p/a/span", 'Fig. 1', True), + (".//li/p/a/span", 'Fig. 2', True), + (".//li/p/a/span", 'Fig. 3', True), + (".//li/p/a/span", 'No.1', True), + (".//li/p/a/span", 'No.2', True), +]) +@pytest.mark.sphinx('html', testroot='add_enumerable_node', + srcdir='test_enumerable_node') +def test_enumerable_node(app, cached_etree_parse, expect): + app.build() + check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect) + + +@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_copy_source': False}) +def test_html_copy_source(app): + app.build(force_all=True) + assert not (app.outdir / '_sources' / 'index.rst.txt').exists() + + +@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.txt'}) +def test_html_sourcelink_suffix(app): + app.build(force_all=True) + assert (app.outdir / '_sources' / 'index.rst.txt').exists() + + +@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': '.rst'}) +def test_html_sourcelink_suffix_same(app): + app.build(force_all=True) + assert (app.outdir / '_sources' / 'index.rst').exists() + + +@pytest.mark.sphinx('html', testroot='basic', confoverrides={'html_sourcelink_suffix': ''}) +def test_html_sourcelink_suffix_empty(app): + app.build(force_all=True) + assert (app.outdir / '_sources' / 'index.rst').exists() + + +@pytest.mark.sphinx('html', testroot='html_entity') +def test_html_entity(app): + app.build(force_all=True) + valid_entities = {'amp', 'lt', 'gt', 'quot', 'apos'} + content = (app.outdir / 'index.html').read_text(encoding='utf8') + for entity in re.findall(r'&([a-z]+);', content, re.MULTILINE): + assert entity not in valid_entities + + +@pytest.mark.sphinx('html', testroot='basic') +def test_html_inventory(app): + app.build(force_all=True) + + with app.outdir.joinpath('objects.inv').open('rb') as f: + invdata = InventoryFile.load(f, 'https://www.google.com', posixpath.join) + + assert set(invdata.keys()) == {'std:label', 'std:doc'} + assert set(invdata['std:label'].keys()) == {'modindex', + 'py-modindex', + 'genindex', + 'search'} + assert invdata['std:label']['modindex'] == ('Python', + '', + 'https://www.google.com/py-modindex.html', + 'Module Index') + assert invdata['std:label']['py-modindex'] == ('Python', + '', + 'https://www.google.com/py-modindex.html', + 'Python Module Index') + assert invdata['std:label']['genindex'] == ('Python', + '', + 'https://www.google.com/genindex.html', + 'Index') + assert invdata['std:label']['search'] == ('Python', + '', + 'https://www.google.com/search.html', + 'Search Page') + assert set(invdata['std:doc'].keys()) == {'index'} + assert invdata['std:doc']['index'] == ('Python', + '', + 'https://www.google.com/index.html', + 'The basic Sphinx documentation for testing') + + +@pytest.mark.sphinx('html', testroot='images', confoverrides={'html_sourcelink_suffix': ''}) +def test_html_anchor_for_figure(app): + app.build(force_all=True) + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<figcaption>\n<p><span class="caption-text">The caption of pic</span>' + '<a class="headerlink" href="#id1" title="Link to this image">¶</a></p>\n</figcaption>' + in content) + + +@pytest.mark.sphinx('html', testroot='directives-raw') +def test_html_raw_directive(app, status, warning): + app.build(force_all=True) + result = (app.outdir / 'index.html').read_text(encoding='utf8') + + # standard case + assert 'standalone raw directive (HTML)' in result + assert 'standalone raw directive (LaTeX)' not in result + + # with substitution + assert '<p>HTML: abc def ghi</p>' in result + assert '<p>LaTeX: abc ghi</p>' in result + + +@pytest.mark.parametrize("expect", [ + (".//link[@href='_static/persistent.css']" + "[@rel='stylesheet']", '', True), + (".//link[@href='_static/default.css']" + "[@rel='stylesheet']" + "[@title='Default']", '', True), + (".//link[@href='_static/alternate1.css']" + "[@rel='alternate stylesheet']" + "[@title='Alternate']", '', True), + (".//link[@href='_static/alternate2.css']" + "[@rel='alternate stylesheet']", '', True), + (".//link[@href='_static/more_persistent.css']" + "[@rel='stylesheet']", '', True), + (".//link[@href='_static/more_default.css']" + "[@rel='stylesheet']" + "[@title='Default']", '', True), + (".//link[@href='_static/more_alternate1.css']" + "[@rel='alternate stylesheet']" + "[@title='Alternate']", '', True), + (".//link[@href='_static/more_alternate2.css']" + "[@rel='alternate stylesheet']", '', True), +]) +@pytest.mark.sphinx('html', testroot='stylesheets') +def test_alternate_stylesheets(app, cached_etree_parse, expect): + app.build() + check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect) + + +@pytest.mark.sphinx('html', testroot='html_style') +def test_html_style(app, status, warning): + app.build() + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert '<link rel="stylesheet" type="text/css" href="_static/default.css" />' in result + assert ('<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />' + not in result) + + +@pytest.mark.sphinx('html', testroot='basic') +def test_html_sidebar(app, status, warning): + ctx = {} + + # default for alabaster + app.build(force_all=True) + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<div class="sphinxsidebar" role="navigation" ' + 'aria-label="main navigation">' in result) + assert '<h1 class="logo"><a href="#">Python</a></h1>' in result + assert '<h3>Navigation</h3>' in result + assert '<h3>Related Topics</h3>' in result + assert '<h3 id="searchlabel">Quick search</h3>' in result + + app.builder.add_sidebars('index', ctx) + assert ctx['sidebars'] == ['about.html', 'navigation.html', 'relations.html', + 'searchbox.html', 'donate.html'] + + # only relations.html + app.config.html_sidebars = {'**': ['relations.html']} + app.build(force_all=True) + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<div class="sphinxsidebar" role="navigation" ' + 'aria-label="main navigation">' in result) + assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result + assert '<h3>Navigation</h3>' not in result + assert '<h3>Related Topics</h3>' in result + assert '<h3 id="searchlabel">Quick search</h3>' not in result + + app.builder.add_sidebars('index', ctx) + assert ctx['sidebars'] == ['relations.html'] + + # no sidebars + app.config.html_sidebars = {'**': []} + app.build(force_all=True) + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<div class="sphinxsidebar" role="navigation" ' + 'aria-label="main navigation">' not in result) + assert '<h1 class="logo"><a href="#">Python</a></h1>' not in result + assert '<h3>Navigation</h3>' not in result + assert '<h3>Related Topics</h3>' not in result + assert '<h3 id="searchlabel">Quick search</h3>' not in result + + app.builder.add_sidebars('index', ctx) + assert ctx['sidebars'] == [] + + +@pytest.mark.parametrize(("fname", "expect"), [ + ('index.html', (".//h1/em/a[@href='https://example.com/cp.1']", '', True)), + ('index.html', (".//em/a[@href='https://example.com/man.1']", '', True)), + ('index.html', (".//em/a[@href='https://example.com/ls.1']", '', True)), + ('index.html', (".//em/a[@href='https://example.com/sphinx.']", '', True)), +]) +@pytest.mark.sphinx('html', testroot='manpage_url', confoverrides={ + 'manpages_url': 'https://example.com/{page}.{section}'}) +@pytest.mark.test_params(shared_result='test_build_html_manpage_url') +def test_html_manpage(app, cached_etree_parse, fname, expect): + app.build() + check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) + + +@pytest.mark.sphinx('html', testroot='toctree-glob', + confoverrides={'html_baseurl': 'https://example.com/'}) +def test_html_baseurl(app, status, warning): + app.build() + + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert '<link rel="canonical" href="https://example.com/index.html" />' in result + + result = (app.outdir / 'qux' / 'index.html').read_text(encoding='utf8') + assert '<link rel="canonical" href="https://example.com/qux/index.html" />' in result + + +@pytest.mark.sphinx('html', testroot='toctree-glob', + confoverrides={'html_baseurl': 'https://example.com/subdir', + 'html_file_suffix': '.htm'}) +def test_html_baseurl_and_html_file_suffix(app, status, warning): + app.build() + + result = (app.outdir / 'index.htm').read_text(encoding='utf8') + assert '<link rel="canonical" href="https://example.com/subdir/index.htm" />' in result + + result = (app.outdir / 'qux' / 'index.htm').read_text(encoding='utf8') + assert '<link rel="canonical" href="https://example.com/subdir/qux/index.htm" />' in result + + +@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_extra_path') +def test_validate_html_extra_path(app): + (app.confdir / '_static').mkdir(parents=True, exist_ok=True) + app.config.html_extra_path = [ + '/path/to/not_found', # not found + '_static', + app.outdir, # outdir + app.outdir / '_static', # inside outdir + ] + with pytest.warns(RemovedInSphinx80Warning, match='Use "pathlib.Path" or "os.fspath" instead'): + validate_html_extra_path(app, app.config) + assert app.config.html_extra_path == ['_static'] + + +@pytest.mark.sphinx(testroot='basic', srcdir='validate_html_static_path') +def test_validate_html_static_path(app): + (app.confdir / '_static').mkdir(parents=True, exist_ok=True) + app.config.html_static_path = [ + '/path/to/not_found', # not found + '_static', + app.outdir, # outdir + app.outdir / '_static', # inside outdir + ] + with pytest.warns(RemovedInSphinx80Warning, match='Use "pathlib.Path" or "os.fspath" instead'): + validate_html_static_path(app, app.config) + assert app.config.html_static_path == ['_static'] + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'html_permalinks': False}) +def test_html_permalink_disable(app): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + + assert '<h1>The basic Sphinx documentation for testing</h1>' in content + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'html_permalinks_icon': '<span>[PERMALINK]</span>'}) +def test_html_permalink_icon(app): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + + assert ('<h1>The basic Sphinx documentation for testing<a class="headerlink" ' + 'href="#the-basic-sphinx-documentation-for-testing" ' + 'title="Link to this heading"><span>[PERMALINK]</span></a></h1>' in content) + + +@pytest.mark.sphinx('html', testroot='html_signaturereturn_icon') +def test_html_signaturereturn_icon(app): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + + assert ('<span class="sig-return-icon">→</span>' in content) + + +@pytest.mark.sphinx('html', testroot='root', srcdir=os.urandom(4).hex()) +def test_html_remove_sources_before_write_gh_issue_10786(app, warning): + # see: https://github.com/sphinx-doc/sphinx/issues/10786 + target = app.srcdir / 'img.png' + + def handler(app): + assert target.exists() + target.unlink() + return [] + + app.connect('html-collect-pages', handler) + assert target.exists() + app.build() + assert not target.exists() + + ws = strip_colors(warning.getvalue()).splitlines() + assert len(ws) >= 1 + + file = os.fsdecode(target) + assert f'WARNING: cannot copy image file {file!r}: {file!s} does not exist' == ws[-1] diff --git a/tests/test_builders/test_build_html_5_output.py b/tests/test_builders/test_build_html_5_output.py new file mode 100644 index 0000000..ece6f49 --- /dev/null +++ b/tests/test_builders/test_build_html_5_output.py @@ -0,0 +1,276 @@ +"""Test the HTML builder and check output against XPath.""" + +import re + +import pytest + +from tests.test_builders.xpath_util import check_xpath + + +def tail_check(check): + rex = re.compile(check) + + def checker(nodes): + for node in nodes: + if node.tail and rex.search(node.tail): + return True + msg = f'{check!r} not found in tail of any nodes {nodes}' + raise AssertionError(msg) + return checker + + +@pytest.mark.parametrize(("fname", "path", "check"), [ + ('images.html', ".//img[@src='_images/img.png']", ''), + ('images.html', ".//img[@src='_images/img1.png']", ''), + ('images.html', ".//img[@src='_images/simg.png']", ''), + ('images.html', ".//img[@src='_images/svgimg.svg']", ''), + ('images.html', ".//a[@href='_sources/images.txt']", ''), + + ('subdir/images.html', ".//img[@src='../_images/img1.png']", ''), + ('subdir/images.html', ".//img[@src='../_images/rimg.png']", ''), + + ('subdir/includes.html', ".//a[@class='reference download internal']", ''), + ('subdir/includes.html', ".//img[@src='../_images/img.png']", ''), + ('subdir/includes.html', ".//p", 'This is an include file.'), + ('subdir/includes.html', ".//pre/span", 'line 1'), + ('subdir/includes.html', ".//pre/span", 'line 2'), + + ('includes.html', ".//pre", 'Max Strauß'), + ('includes.html', ".//a[@class='reference download internal']", ''), + ('includes.html', ".//pre/span", '"quotes"'), + ('includes.html', ".//pre/span", "'included'"), + ('includes.html', ".//pre/span[@class='s2']", 'üöä'), + ('includes.html', ".//div[@class='inc-pyobj1 highlight-text notranslate']//pre", + r'^class Foo:\n pass\n\s*$'), + ('includes.html', ".//div[@class='inc-pyobj2 highlight-text notranslate']//pre", + r'^ def baz\(\):\n pass\n\s*$'), + ('includes.html', ".//div[@class='inc-lines highlight-text notranslate']//pre", + r'^class Foo:\n pass\nclass Bar:\n$'), + ('includes.html', ".//div[@class='inc-startend highlight-text notranslate']//pre", + '^foo = "Including Unicode characters: üöä"\\n$'), + ('includes.html', ".//div[@class='inc-preappend highlight-text notranslate']//pre", + r'(?m)^START CODE$'), + ('includes.html', ".//div[@class='inc-pyobj-dedent highlight-python notranslate']//span", + r'def'), + ('includes.html', ".//div[@class='inc-tab3 highlight-text notranslate']//pre", + r'-| |-'), + ('includes.html', ".//div[@class='inc-tab8 highlight-python notranslate']//pre/span", + r'-| |-'), + + ('autodoc.html', ".//dl[@class='py class']/dt[@id='autodoc_target.Class']", ''), + ('autodoc.html', ".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'\*\*'), + ('autodoc.html', ".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'kwds'), + ('autodoc.html', ".//dd/p", r'Return spam\.'), + + ('extapi.html', ".//strong", 'from class: Bar'), + + ('markup.html', ".//title", 'set by title directive'), + ('markup.html', ".//p/em", 'Section author: Georg Brandl'), + ('markup.html', ".//p/em", 'Module author: Georg Brandl'), + # created by the meta directive + ('markup.html', ".//meta[@name='author'][@content='Me']", ''), + ('markup.html', ".//meta[@name='keywords'][@content='docs, sphinx']", ''), + # a label created by ``.. _label:`` + ('markup.html', ".//div[@id='label']", ''), + # code with standard code blocks + ('markup.html', ".//pre", '^some code$'), + # an option list + ('markup.html', ".//span[@class='option']", '--help'), + # admonitions + ('markup.html', ".//p[@class='admonition-title']", 'My Admonition'), + ('markup.html', ".//div[@class='admonition note']/p", 'Note text.'), + ('markup.html', ".//div[@class='admonition warning']/p", 'Warning text.'), + # inline markup + ('markup.html', ".//li/p/strong", r'^command\\n$'), + ('markup.html', ".//li/p/strong", r'^program\\n$'), + ('markup.html', ".//li/p/em", r'^dfn\\n$'), + ('markup.html', ".//li/p/kbd", r'^kbd\\n$'), + ('markup.html', ".//li/p/span", 'File \N{TRIANGULAR BULLET} Close'), + ('markup.html', ".//li/p/code/span[@class='pre']", '^a/$'), + ('markup.html', ".//li/p/code/em/span[@class='pre']", '^varpart$'), + ('markup.html', ".//li/p/code/em/span[@class='pre']", '^i$'), + ('markup.html', ".//a[@href='https://peps.python.org/pep-0008/']" + "[@class='pep reference external']/strong", 'PEP 8'), + ('markup.html', ".//a[@href='https://peps.python.org/pep-0008/']" + "[@class='pep reference external']/strong", + 'Python Enhancement Proposal #8'), + ('markup.html', ".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']" + "[@class='rfc reference external']/strong", 'RFC 1'), + ('markup.html', ".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']" + "[@class='rfc reference external']/strong", 'Request for Comments #1'), + ('markup.html', ".//a[@href='objects.html#envvar-HOME']" + "[@class='reference internal']/code/span[@class='pre']", 'HOME'), + ('markup.html', ".//a[@href='#with']" + "[@class='reference internal']/code/span[@class='pre']", '^with$'), + ('markup.html', ".//a[@href='#grammar-token-try_stmt']" + "[@class='reference internal']/code/span", '^statement$'), + ('markup.html', ".//a[@href='#some-label'][@class='reference internal']/span", '^here$'), + ('markup.html', ".//a[@href='#some-label'][@class='reference internal']/span", '^there$'), + ('markup.html', ".//a[@href='subdir/includes.html']" + "[@class='reference internal']/span", 'Including in subdir'), + ('markup.html', ".//a[@href='objects.html#cmdoption-python-c']" + "[@class='reference internal']/code/span[@class='pre']", '-c'), + # abbreviations + ('markup.html', ".//abbr[@title='abbreviation']", '^abbr$'), + # version stuff + ('markup.html', ".//div[@class='versionadded']/p/span", 'Added in version 0.6: '), + ('markup.html', ".//div[@class='versionadded']/p/span", + tail_check('First paragraph of versionadded')), + ('markup.html', ".//div[@class='versionchanged']/p/span", + tail_check('First paragraph of versionchanged')), + ('markup.html', ".//div[@class='versionchanged']/p", + 'Second paragraph of versionchanged'), + ('markup.html', ".//div[@class='versionremoved']/p/span", 'Removed in version 0.6: '), + # footnote reference + ('markup.html', ".//a[@class='footnote-reference brackets']", r'1'), + # created by reference lookup + ('markup.html', ".//a[@href='index.html#ref1']", ''), + # ``seealso`` directive + ('markup.html', ".//div/p[@class='admonition-title']", 'See also'), + # a ``hlist`` directive + ('markup.html', ".//table[@class='hlist']/tr/td/ul/li/p", '^This$'), + # a ``centered`` directive + ('markup.html', ".//p[@class='centered']/strong", 'LICENSE'), + # a glossary + ('markup.html', ".//dl/dt[@id='term-boson']", 'boson'), + ('markup.html', ".//dl/dt[@id='term-boson']/a", '¶'), + # a production list + ('markup.html', ".//pre/strong", 'try_stmt'), + ('markup.html', ".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'), + # tests for ``only`` directive + ('markup.html', ".//p", 'A global substitution!'), + ('markup.html', ".//p", 'In HTML.'), + ('markup.html', ".//p", 'In both.'), + ('markup.html', ".//p", 'Always present'), + # tests for ``any`` role + ('markup.html', ".//a[@href='#with']/span", 'headings'), + ('markup.html', ".//a[@href='objects.html#func_without_body']/code/span", 'objects'), + # tests for numeric labels + ('markup.html', ".//a[@href='#id1'][@class='reference internal']/span", 'Testing various markup'), + # tests for smartypants + ('markup.html', ".//li/p", 'Smart “quotes” in English ‘text’.'), + ('markup.html', ".//li/p", 'Smart — long and – short dashes.'), + ('markup.html', ".//li/p", 'Ellipsis…'), + ('markup.html', ".//li/p/code/span[@class='pre']", 'foo--"bar"...'), + ('markup.html', ".//p", 'Этот «абзац» должен использовать „русские“ кавычки.'), + ('markup.html', ".//p", 'Il dit : « C’est “super” ! »'), + + ('objects.html', ".//dt[@id='mod.Cls.meth1']", ''), + ('objects.html', ".//dt[@id='errmod.Error']", ''), + ('objects.html', ".//dt/span[@class='sig-name descname']/span[@class='pre']", r'long\(parameter,'), + ('objects.html', ".//dt/span[@class='sig-name descname']/span[@class='pre']", r'list\)'), + ('objects.html', ".//dt/span[@class='sig-name descname']/span[@class='pre']", 'another'), + ('objects.html', ".//dt/span[@class='sig-name descname']/span[@class='pre']", 'one'), + ('objects.html', ".//a[@href='#mod.Cls'][@class='reference internal']", ''), + ('objects.html', ".//dl[@class='std userdesc']", ''), + ('objects.html', ".//dt[@id='userdesc-myobj']", ''), + ('objects.html', ".//a[@href='#userdesc-myobj'][@class='reference internal']", ''), + # docfields + ('objects.html', ".//a[@class='reference internal'][@href='#TimeInt']/em", 'TimeInt'), + ('objects.html', ".//a[@class='reference internal'][@href='#Time']", 'Time'), + ('objects.html', ".//a[@class='reference internal'][@href='#errmod.Error']/strong", 'Error'), + # C references + ('objects.html', ".//span[@class='pre']", 'CFunction()'), + ('objects.html', ".//a[@href='#c.Sphinx_DoSomething']", ''), + ('objects.html', ".//a[@href='#c.SphinxStruct.member']", ''), + ('objects.html', ".//a[@href='#c.SPHINX_USE_PYTHON']", ''), + ('objects.html', ".//a[@href='#c.SphinxType']", ''), + ('objects.html', ".//a[@href='#c.sphinx_global']", ''), + # test global TOC created by toctree() + ('objects.html', ".//ul[@class='current']/li[@class='toctree-l1 current']/a[@href='#']", + 'Testing object descriptions'), + ('objects.html', ".//li[@class='toctree-l1']/a[@href='markup.html']", + 'Testing various markup'), + # test unknown field names + ('objects.html', ".//dt[@class='field-odd']", 'Field_name'), + ('objects.html', ".//dt[@class='field-even']", 'Field_name all lower'), + ('objects.html', ".//dt[@class='field-odd']", 'FIELD_NAME'), + ('objects.html', ".//dt[@class='field-even']", 'FIELD_NAME ALL CAPS'), + ('objects.html', ".//dt[@class='field-odd']", 'Field_Name'), + ('objects.html', ".//dt[@class='field-even']", 'Field_Name All Word Caps'), + # ('objects.html', ".//dt[@class='field-odd']", 'Field_name'), (duplicate) + ('objects.html', ".//dt[@class='field-even']", 'Field_name First word cap'), + ('objects.html', ".//dt[@class='field-odd']", 'FIELd_name'), + ('objects.html', ".//dt[@class='field-even']", 'FIELd_name PARTial caps'), + # custom sidebar + ('objects.html', ".//h4", 'Custom sidebar'), + # docfields + ('objects.html', ".//dd[@class='field-odd']/p/strong", '^moo$'), + ('objects.html', ".//dd[@class='field-odd']/p/strong", tail_check(r'\(Moo\) .* Moo')), + ('objects.html', ".//dd[@class='field-odd']/ul/li/p/strong", '^hour$'), + ('objects.html', ".//dd[@class='field-odd']/ul/li/p/em", '^DuplicateType$'), + ('objects.html', ".//dd[@class='field-odd']/ul/li/p/em", tail_check(r'.* Some parameter')), + # others + ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span", + 'perl'), + ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span", + '\\+p'), + ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-ObjC']/code/span", + '--ObjC\\+\\+'), + ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-plugin.option']/code/span", + '--plugin.option'), + ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-arg-create-auth-token']" + "/code/span", + 'create-auth-token'), + ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-arg-arg']/code/span", + 'arg'), + ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-perl-j']/code/span", + '-j'), + ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span", + 'hg'), + ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-hg-arg-commit']/code/span", + 'commit'), + ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span", + 'git'), + ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span", + 'commit'), + ('objects.html', ".//a[@class='reference internal'][@href='#cmdoption-git-commit-p']/code/span", + '-p'), + + ('index.html', ".//meta[@name='hc'][@content='hcval']", ''), + ('index.html', ".//meta[@name='hc_co'][@content='hcval_co']", ''), + ('index.html', ".//li[@class='toctree-l1']/a", 'Testing various markup'), + ('index.html', ".//li[@class='toctree-l2']/a", 'Inline markup'), + ('index.html', ".//title", 'Sphinx <Tests>'), + ('index.html', ".//div[@class='footer']", 'copyright text credits'), + ('index.html', ".//a[@href='https://python.org/']" + "[@class='reference external']", ''), + ('index.html', ".//li/p/a[@href='genindex.html']/span", 'Index'), + ('index.html', ".//li/p/a[@href='py-modindex.html']/span", 'Module Index'), + # custom sidebar only for contents + ('index.html', ".//h4", 'Contents sidebar'), + # custom JavaScript + ('index.html', ".//script[@src='file://moo.js']", ''), + # URL in contents + ('index.html', ".//a[@class='reference external'][@href='https://sphinx-doc.org/']", + 'https://sphinx-doc.org/'), + ('index.html', ".//a[@class='reference external'][@href='https://sphinx-doc.org/latest/']", + 'Latest reference'), + # Indirect hyperlink targets across files + ('index.html', ".//a[@href='markup.html#some-label'][@class='reference internal']/span", + '^indirect hyperref$'), + + ('bom.html', ".//title", " File with UTF-8 BOM"), + + ('extensions.html', ".//a[@href='https://python.org/dev/']", "https://python.org/dev/"), + ('extensions.html', ".//a[@href='https://bugs.python.org/issue1000']", "issue 1000"), + ('extensions.html', ".//a[@href='https://bugs.python.org/issue1042']", "explicit caption"), + + # index entries + ('genindex.html', ".//a/strong", "Main"), + ('genindex.html', ".//a/strong", "[1]"), + ('genindex.html', ".//a/strong", "Other"), + ('genindex.html', ".//a", "entry"), + ('genindex.html', ".//li/a", "double"), + + ('otherext.html', ".//h1", "Generated section"), + ('otherext.html', ".//a[@href='_sources/otherext.foo.txt']", ''), + + ('search.html', ".//meta[@name='robots'][@content='noindex']", ''), +]) +@pytest.mark.sphinx('html', tags=['testtag'], + confoverrides={'html_context.hckey_co': 'hcval_co'}) +@pytest.mark.test_params(shared_result='test_build_html_output') +def test_html5_output(app, cached_etree_parse, fname, path, check): + app.build() + check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check) diff --git a/tests/test_builders/test_build_html_assets.py b/tests/test_builders/test_build_html_assets.py new file mode 100644 index 0000000..fc7a987 --- /dev/null +++ b/tests/test_builders/test_build_html_assets.py @@ -0,0 +1,152 @@ +"""Test the HTML builder and check output against XPath.""" + +import re +from pathlib import Path + +import pytest + +import sphinx.builders.html +from sphinx.builders.html._assets import _file_checksum +from sphinx.errors import ThemeError + + +@pytest.mark.sphinx('html', testroot='html_assets') +def test_html_assets(app): + app.build(force_all=True) + + # exclude_path and its family + assert not (app.outdir / 'static' / 'index.html').exists() + assert not (app.outdir / 'extra' / 'index.html').exists() + + # html_static_path + assert not (app.outdir / '_static' / '.htaccess').exists() + assert not (app.outdir / '_static' / '.htpasswd').exists() + assert (app.outdir / '_static' / 'API.html').exists() + assert (app.outdir / '_static' / 'API.html').read_text(encoding='utf8') == 'Sphinx-1.4.4' + assert (app.outdir / '_static' / 'css' / 'style.css').exists() + assert (app.outdir / '_static' / 'js' / 'custom.js').exists() + assert (app.outdir / '_static' / 'rimg.png').exists() + assert not (app.outdir / '_static' / '_build' / 'index.html').exists() + assert (app.outdir / '_static' / 'background.png').exists() + assert not (app.outdir / '_static' / 'subdir' / '.htaccess').exists() + assert not (app.outdir / '_static' / 'subdir' / '.htpasswd').exists() + + # html_extra_path + assert (app.outdir / '.htaccess').exists() + assert not (app.outdir / '.htpasswd').exists() + assert (app.outdir / 'API.html_t').exists() + assert (app.outdir / 'css/style.css').exists() + assert (app.outdir / 'rimg.png').exists() + assert not (app.outdir / '_build' / 'index.html').exists() + assert (app.outdir / 'background.png').exists() + assert (app.outdir / 'subdir' / '.htaccess').exists() + assert not (app.outdir / 'subdir' / '.htpasswd').exists() + + # html_css_files + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert '<link rel="stylesheet" type="text/css" href="_static/css/style.css" />' in content + assert ('<link media="print" rel="stylesheet" title="title" type="text/css" ' + 'href="https://example.com/custom.css" />' in content) + + # html_js_files + assert '<script src="_static/js/custom.js"></script>' in content + assert ('<script async="async" src="https://example.com/script.js">' + '</script>' in content) + + +@pytest.mark.sphinx('html', testroot='html_assets') +def test_assets_order(app, monkeypatch): + monkeypatch.setattr(sphinx.builders.html, '_file_checksum', lambda o, f: '') + + app.add_css_file('normal.css') + app.add_css_file('early.css', priority=100) + app.add_css_file('late.css', priority=750) + app.add_css_file('lazy.css', priority=900) + app.add_js_file('normal.js') + app.add_js_file('early.js', priority=100) + app.add_js_file('late.js', priority=750) + app.add_js_file('lazy.js', priority=900) + + app.build(force_all=True) + content = (app.outdir / 'index.html').read_text(encoding='utf8') + + # css_files + expected = [ + '_static/early.css', + '_static/pygments.css', + '_static/alabaster.css', + 'https://example.com/custom.css', + '_static/normal.css', + '_static/late.css', + '_static/css/style.css', + '_static/lazy.css', + ] + pattern = '.*'.join(f'href="{re.escape(f)}"' for f in expected) + assert re.search(pattern, content, re.DOTALL), content + + # js_files + expected = [ + '_static/early.js', + '_static/doctools.js', + '_static/sphinx_highlight.js', + 'https://example.com/script.js', + '_static/normal.js', + '_static/late.js', + '_static/js/custom.js', + '_static/lazy.js', + ] + pattern = '.*'.join(f'src="{re.escape(f)}"' for f in expected) + assert re.search(pattern, content, re.DOTALL), content + + +@pytest.mark.sphinx('html', testroot='html_file_checksum') +def test_file_checksum(app): + app.add_css_file('stylesheet-a.css') + app.add_css_file('stylesheet-b.css') + app.add_css_file('https://example.com/custom.css') + app.add_js_file('script.js') + app.add_js_file('empty.js') + app.add_js_file('https://example.com/script.js') + + app.build(force_all=True) + content = (app.outdir / 'index.html').read_text(encoding='utf8') + + # checksum for local files + assert '<link rel="stylesheet" type="text/css" href="_static/stylesheet-a.css?v=e575b6df" />' in content + assert '<link rel="stylesheet" type="text/css" href="_static/stylesheet-b.css?v=a2d5cc0f" />' in content + assert '<script src="_static/script.js?v=48278d48"></script>' in content + + # empty files have no checksum + assert '<script src="_static/empty.js"></script>' in content + + # no checksum for hyperlinks + assert '<link rel="stylesheet" type="text/css" href="https://example.com/custom.css" />' in content + assert '<script src="https://example.com/script.js"></script>' in content + + +def test_file_checksum_query_string(): + with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'): + _file_checksum(Path(), 'with_query_string.css?dead_parrots=1') + + with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'): + _file_checksum(Path(), 'with_query_string.js?dead_parrots=1') + + with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'): + _file_checksum(Path.cwd(), '_static/with_query_string.css?dead_parrots=1') + + with pytest.raises(ThemeError, match='Local asset file paths must not contain query strings'): + _file_checksum(Path.cwd(), '_static/with_query_string.js?dead_parrots=1') + + +@pytest.mark.sphinx('html', testroot='html_assets') +def test_javscript_loading_method(app): + app.add_js_file('normal.js') + app.add_js_file('early.js', loading_method='async') + app.add_js_file('late.js', loading_method='defer') + + app.build(force_all=True) + content = (app.outdir / 'index.html').read_text(encoding='utf8') + + assert '<script src="_static/normal.js"></script>' in content + assert '<script async="async" src="_static/early.js"></script>' in content + assert '<script defer="defer" src="_static/late.js"></script>' in content diff --git a/tests/test_builders/test_build_html_code.py b/tests/test_builders/test_build_html_code.py new file mode 100644 index 0000000..f07eb97 --- /dev/null +++ b/tests/test_builders/test_build_html_code.py @@ -0,0 +1,46 @@ +import pytest + + +@pytest.mark.sphinx('html', testroot='reST-code-block', + confoverrides={'html_codeblock_linenos_style': 'table'}) +def test_html_codeblock_linenos_style_table(app): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + + assert ('<div class="linenodiv"><pre><span class="normal">1</span>\n' + '<span class="normal">2</span>\n' + '<span class="normal">3</span>\n' + '<span class="normal">4</span></pre></div>') in content + + +@pytest.mark.sphinx('html', testroot='reST-code-block', + confoverrides={'html_codeblock_linenos_style': 'inline'}) +def test_html_codeblock_linenos_style_inline(app): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + + assert '<span class="linenos">1</span>' in content + + +@pytest.mark.sphinx('html', testroot='reST-code-role') +def test_html_code_role(app): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + + common_content = ( + '<span class="k">def</span> <span class="nf">foo</span>' + '<span class="p">(</span>' + '<span class="mi">1</span> ' + '<span class="o">+</span> ' + '<span class="mi">2</span> ' + '<span class="o">+</span> ' + '<span class="kc">None</span> ' + '<span class="o">+</span> ' + '<span class="s2">"abc"</span>' + '<span class="p">):</span> ' + '<span class="k">pass</span>') + assert ('<p>Inline <code class="code highlight python docutils literal highlight-python">' + + common_content + '</code> code block</p>') in content + assert ('<div class="highlight-python notranslate">' + + '<div class="highlight"><pre><span></span>' + + common_content) in content diff --git a/tests/test_builders/test_build_html_download.py b/tests/test_builders/test_build_html_download.py new file mode 100644 index 0000000..1201c66 --- /dev/null +++ b/tests/test_builders/test_build_html_download.py @@ -0,0 +1,62 @@ +import hashlib +import re + +import pytest + + +@pytest.mark.sphinx('html') +@pytest.mark.test_params(shared_result='test_build_html_output') +def test_html_download(app): + app.build() + + # subdir/includes.html + result = (app.outdir / 'subdir' / 'includes.html').read_text(encoding='utf8') + pattern = ('<a class="reference download internal" download="" ' + 'href="../(_downloads/.*/img.png)">') + matched = re.search(pattern, result) + assert matched + assert (app.outdir / matched.group(1)).exists() + filename = matched.group(1) + + # includes.html + result = (app.outdir / 'includes.html').read_text(encoding='utf8') + pattern = ('<a class="reference download internal" download="" ' + 'href="(_downloads/.*/img.png)">') + matched = re.search(pattern, result) + assert matched + assert (app.outdir / matched.group(1)).exists() + assert matched.group(1) == filename + + pattern = ('<a class="reference download internal" download="" ' + 'href="(_downloads/.*/)(file_with_special_%23_chars.xyz)">') + matched = re.search(pattern, result) + assert matched + assert (app.outdir / matched.group(1) / "file_with_special_#_chars.xyz").exists() + + +@pytest.mark.sphinx('html', testroot='roles-download') +def test_html_download_role(app, status, warning): + app.build() + digest = hashlib.md5(b'dummy.dat', usedforsecurity=False).hexdigest() + assert (app.outdir / '_downloads' / digest / 'dummy.dat').exists() + digest_another = hashlib.md5(b'another/dummy.dat', usedforsecurity=False).hexdigest() + assert (app.outdir / '_downloads' / digest_another / 'dummy.dat').exists() + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert (('<li><p><a class="reference download internal" download="" ' + 'href="_downloads/%s/dummy.dat">' + '<code class="xref download docutils literal notranslate">' + '<span class="pre">dummy.dat</span></code></a></p></li>' % digest) + in content) + assert (('<li><p><a class="reference download internal" download="" ' + 'href="_downloads/%s/dummy.dat">' + '<code class="xref download docutils literal notranslate">' + '<span class="pre">another/dummy.dat</span></code></a></p></li>' % + digest_another) in content) + assert ('<li><p><code class="xref download docutils literal notranslate">' + '<span class="pre">not_found.dat</span></code></p></li>' in content) + assert ('<li><p><a class="reference download external" download="" ' + 'href="https://www.sphinx-doc.org/en/master/_static/sphinx-logo.svg">' + '<code class="xref download docutils literal notranslate">' + '<span class="pre">Sphinx</span> <span class="pre">logo</span>' + '</code></a></p></li>' in content) diff --git a/tests/test_builders/test_build_html_highlight.py b/tests/test_builders/test_build_html_highlight.py new file mode 100644 index 0000000..aee1ece --- /dev/null +++ b/tests/test_builders/test_build_html_highlight.py @@ -0,0 +1,61 @@ +from unittest.mock import ANY, call, patch + +import pytest + + +@pytest.mark.sphinx('html', testroot='basic') +def test_html_pygments_style_default(app): + style = app.builder.highlighter.formatter_args.get('style') + assert style.__name__ == 'Alabaster' + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'pygments_style': 'sphinx'}) +def test_html_pygments_style_manually(app): + style = app.builder.highlighter.formatter_args.get('style') + assert style.__name__ == 'SphinxStyle' + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'html_theme': 'classic'}) +def test_html_pygments_for_classic_theme(app): + style = app.builder.highlighter.formatter_args.get('style') + assert style.__name__ == 'SphinxStyle' + + +@pytest.mark.sphinx('html', testroot='basic') +def test_html_dark_pygments_style_default(app): + assert app.builder.dark_highlighter is None + + +@pytest.mark.sphinx('html', testroot='highlight_options') +def test_highlight_options(app): + subject = app.builder.highlighter + with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight: + app.build() + + call_args = highlight.call_args_list + assert len(call_args) == 3 + assert call_args[0] == call(ANY, 'default', force=False, linenos=False, + location=ANY, opts={'default_option': True}) + assert call_args[1] == call(ANY, 'python', force=False, linenos=False, + location=ANY, opts={'python_option': True}) + assert call_args[2] == call(ANY, 'java', force=False, linenos=False, + location=ANY, opts={}) + + +@pytest.mark.sphinx('html', testroot='highlight_options', + confoverrides={'highlight_options': {'default_option': True}}) +def test_highlight_options_old(app): + subject = app.builder.highlighter + with patch.object(subject, 'highlight_block', wraps=subject.highlight_block) as highlight: + app.build() + + call_args = highlight.call_args_list + assert len(call_args) == 3 + assert call_args[0] == call(ANY, 'default', force=False, linenos=False, + location=ANY, opts={'default_option': True}) + assert call_args[1] == call(ANY, 'python', force=False, linenos=False, + location=ANY, opts={}) + assert call_args[2] == call(ANY, 'java', force=False, linenos=False, + location=ANY, opts={}) diff --git a/tests/test_builders/test_build_html_image.py b/tests/test_builders/test_build_html_image.py new file mode 100644 index 0000000..08ed618 --- /dev/null +++ b/tests/test_builders/test_build_html_image.py @@ -0,0 +1,80 @@ +import re +from pathlib import Path + +import docutils +import pytest + + +@pytest.mark.sphinx('html', testroot='images') +def test_html_remote_images(app, status, warning): + app.build(force_all=True) + + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<img alt="http://localhost:7777/sphinx.png" ' + 'src="http://localhost:7777/sphinx.png" />' in result) + assert not (app.outdir / 'sphinx.png').exists() + + +@pytest.mark.sphinx('html', testroot='image-escape') +def test_html_encoded_image(app, status, warning): + app.build(force_all=True) + + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<img alt="_images/img_%231.png" src="_images/img_%231.png" />' in result) + assert (app.outdir / '_images/img_#1.png').exists() + + +@pytest.mark.sphinx('html', testroot='remote-logo') +def test_html_remote_logo(app, status, warning): + app.build(force_all=True) + + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<img class="logo" src="https://www.python.org/static/img/python-logo.png" alt="Logo"/>' in result) + assert ('<link rel="icon" href="https://www.python.org/static/favicon.ico"/>' in result) + assert not (app.outdir / 'python-logo.png').exists() + + +@pytest.mark.sphinx('html', testroot='local-logo') +def test_html_local_logo(app, status, warning): + app.build(force_all=True) + + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<img class="logo" src="_static/img.png" alt="Logo"/>' in result) + assert (app.outdir / '_static/img.png').exists() + + +@pytest.mark.sphinx(testroot='html_scaled_image_link') +def test_html_scaled_image_link(app): + app.build() + context = (app.outdir / 'index.html').read_text(encoding='utf8') + + # no scaled parameters + assert re.search('\n<img alt="_images/img.png" src="_images/img.png" />', context) + + # scaled_image_link + # Docutils 0.21 adds a newline before the closing </a> tag + closing_space = "\n" if docutils.__version_info__[:2] >= (0, 21) else "" + assert re.search('\n<a class="reference internal image-reference" href="_images/img.png">' + '<img alt="_images/img.png" src="_images/img.png" style="[^"]+" />' + f'{closing_space}</a>', + context) + + # no-scaled-link class disables the feature + assert re.search('\n<img alt="_images/img.png" class="no-scaled-link"' + ' src="_images/img.png" style="[^"]+" />', + context) + + +@pytest.mark.sphinx('html', testroot='images') +def test_copy_images(app, status, warning): + app.build() + + images_dir = Path(app.outdir) / '_images' + images = {image.name for image in images_dir.rglob('*')} + assert images == { + 'img.png', + 'rimg.png', + 'rimg1.png', + 'svgimg.svg', + 'testimäge.png', + } diff --git a/tests/test_builders/test_build_html_maths.py b/tests/test_builders/test_build_html_maths.py new file mode 100644 index 0000000..900846b --- /dev/null +++ b/tests/test_builders/test_build_html_maths.py @@ -0,0 +1,58 @@ +import pytest + +from sphinx.errors import ConfigError + + +@pytest.mark.sphinx('html', testroot='basic') +def test_default_html_math_renderer(app, status, warning): + assert app.builder.math_renderer_name == 'mathjax' + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_html_math_renderer_is_mathjax(app, status, warning): + assert app.builder.math_renderer_name == 'mathjax' + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'extensions': ['sphinx.ext.imgmath']}) +def test_html_math_renderer_is_imgmath(app, status, warning): + assert app.builder.math_renderer_name == 'imgmath' + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'extensions': ['sphinxcontrib.jsmath', + 'sphinx.ext.imgmath']}) +def test_html_math_renderer_is_duplicated(make_app, app_params): + args, kwargs = app_params + with pytest.raises( + ConfigError, + match='Many math_renderers are registered. But no math_renderer is selected.', + ): + make_app(*args, **kwargs) + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'extensions': ['sphinx.ext.imgmath', + 'sphinx.ext.mathjax']}) +def test_html_math_renderer_is_duplicated2(app, status, warning): + # case of both mathjax and another math_renderer is loaded + assert app.builder.math_renderer_name == 'imgmath' # The another one is chosen + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'extensions': ['sphinxcontrib.jsmath', + 'sphinx.ext.imgmath'], + 'html_math_renderer': 'imgmath'}) +def test_html_math_renderer_is_chosen(app, status, warning): + assert app.builder.math_renderer_name == 'imgmath' + + +@pytest.mark.sphinx('html', testroot='basic', + confoverrides={'extensions': ['sphinxcontrib.jsmath', + 'sphinx.ext.mathjax'], + 'html_math_renderer': 'imgmath'}) +def test_html_math_renderer_is_mismatched(make_app, app_params): + args, kwargs = app_params + with pytest.raises(ConfigError, match="Unknown math_renderer 'imgmath' is given."): + make_app(*args, **kwargs) diff --git a/tests/test_builders/test_build_html_numfig.py b/tests/test_builders/test_build_html_numfig.py new file mode 100644 index 0000000..62e68cb --- /dev/null +++ b/tests/test_builders/test_build_html_numfig.py @@ -0,0 +1,487 @@ +"""Test the HTML builder and check output against XPath.""" + +import os +import re + +import pytest + +from tests.test_builders.xpath_data import FIGURE_CAPTION +from tests.test_builders.xpath_util import check_xpath + + +@pytest.mark.sphinx('html', testroot='numfig') +@pytest.mark.test_params(shared_result='test_build_html_numfig') +def test_numfig_disabled_warn(app, warning): + app.build() + warnings = warning.getvalue() + assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' in warnings + assert 'index.rst:56: WARNING: invalid numfig_format: invalid' not in warnings + assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' not in warnings + + +@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [ + ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", None, True), + ('index.html', ".//table/caption/span[@class='caption-number']", None, True), + ('index.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", None, True), + ('index.html', ".//li/p/code/span", '^fig1$', True), + ('index.html', ".//li/p/code/span", '^Figure%s$', True), + ('index.html', ".//li/p/code/span", '^table-1$', True), + ('index.html', ".//li/p/code/span", '^Table:%s$', True), + ('index.html', ".//li/p/code/span", '^CODE_1$', True), + ('index.html', ".//li/p/code/span", '^Code-%s$', True), + ('index.html', ".//li/p/a/span", '^Section 1$', True), + ('index.html', ".//li/p/a/span", '^Section 2.1$', True), + ('index.html', ".//li/p/code/span", '^Fig.{number}$', True), + ('index.html', ".//li/p/a/span", '^Sect.1 Foo$', True), + + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", None, True), + ('foo.html', ".//table/caption/span[@class='caption-number']", None, True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", None, True), + + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", None, True), + ('bar.html', ".//table/caption/span[@class='caption-number']", None, True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", None, True), + + ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", None, True), + ('baz.html', ".//table/caption/span[@class='caption-number']", None, True), + ('baz.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", None, True), +]) +@pytest.mark.sphinx('html', testroot='numfig') +@pytest.mark.test_params(shared_result='test_build_html_numfig') +def test_numfig_disabled(app, cached_etree_parse, fname, path, check, be_found): + app.build() + check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found) + + +@pytest.mark.sphinx( + 'html', testroot='numfig', + srcdir='test_numfig_without_numbered_toctree_warn', + confoverrides={'numfig': True}) +def test_numfig_without_numbered_toctree_warn(app, warning): + app.build() + # remove :numbered: option + index = (app.srcdir / 'index.rst').read_text(encoding='utf8') + index = re.sub(':numbered:.*', '', index) + (app.srcdir / 'index.rst').write_text(index, encoding='utf8') + app.build() + + warnings = warning.getvalue() + assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings + assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings + assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings + assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings + + +@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [ + ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 9 $', True), + ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 10 $', True), + ('index.html', ".//table/caption/span[@class='caption-number']", + '^Table 9 $', True), + ('index.html', ".//table/caption/span[@class='caption-number']", + '^Table 10 $', True), + ('index.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 9 $', True), + ('index.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 10 $', True), + ('index.html', ".//li/p/a/span", '^Fig. 9$', True), + ('index.html', ".//li/p/a/span", '^Figure6$', True), + ('index.html', ".//li/p/a/span", '^Table 9$', True), + ('index.html', ".//li/p/a/span", '^Table:6$', True), + ('index.html', ".//li/p/a/span", '^Listing 9$', True), + ('index.html', ".//li/p/a/span", '^Code-6$', True), + ('index.html', ".//li/p/code/span", '^foo$', True), + ('index.html', ".//li/p/code/span", '^bar_a$', True), + ('index.html', ".//li/p/a/span", '^Fig.9 should be Fig.1$', True), + ('index.html', ".//li/p/code/span", '^Sect.{number}$', True), + + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True), + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True), + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 3 $', True), + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 4 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Table 1 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Table 2 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Table 3 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Table 4 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 3 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 4 $', True), + + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 5 $', True), + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 7 $', True), + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 8 $', True), + ('bar.html', ".//table/caption/span[@class='caption-number']", + '^Table 5 $', True), + ('bar.html', ".//table/caption/span[@class='caption-number']", + '^Table 7 $', True), + ('bar.html', ".//table/caption/span[@class='caption-number']", + '^Table 8 $', True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 5 $', True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 7 $', True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 8 $', True), + + ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 6 $', True), + ('baz.html', ".//table/caption/span[@class='caption-number']", + '^Table 6 $', True), + ('baz.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 6 $', True), +]) +@pytest.mark.sphinx( + 'html', testroot='numfig', + srcdir='test_numfig_without_numbered_toctree', + confoverrides={'numfig': True}) +def test_numfig_without_numbered_toctree(app, cached_etree_parse, fname, path, check, be_found): + # remove :numbered: option + index = (app.srcdir / 'index.rst').read_text(encoding='utf8') + index = re.sub(':numbered:.*', '', index) + (app.srcdir / 'index.rst').write_text(index, encoding='utf8') + + if not os.listdir(app.outdir): + app.build() + check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found) + + +@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True}) +@pytest.mark.test_params(shared_result='test_build_html_numfig_on') +def test_numfig_with_numbered_toctree_warn(app, warning): + app.build() + warnings = warning.getvalue() + assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings + assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings + assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings + assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings + + +@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [ + ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True), + ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True), + ('index.html', ".//table/caption/span[@class='caption-number']", + '^Table 1 $', True), + ('index.html', ".//table/caption/span[@class='caption-number']", + '^Table 2 $', True), + ('index.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1 $', True), + ('index.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2 $', True), + ('index.html', ".//li/p/a/span", '^Fig. 1$', True), + ('index.html', ".//li/p/a/span", '^Figure2.2$', True), + ('index.html', ".//li/p/a/span", '^Table 1$', True), + ('index.html', ".//li/p/a/span", '^Table:2.2$', True), + ('index.html', ".//li/p/a/span", '^Listing 1$', True), + ('index.html', ".//li/p/a/span", '^Code-2.2$', True), + ('index.html', ".//li/p/a/span", '^Section.1$', True), + ('index.html', ".//li/p/a/span", '^Section.2.1$', True), + ('index.html', ".//li/p/a/span", '^Fig.1 should be Fig.1$', True), + ('index.html', ".//li/p/a/span", '^Sect.1 Foo$', True), + + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True), + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2 $', True), + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.3 $', True), + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.4 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Table 1.1 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Table 1.2 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Table 1.3 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Table 1.4 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1.1 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1.2 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1.3 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1.4 $', True), + + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1 $', True), + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.3 $', True), + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.4 $', True), + ('bar.html', ".//table/caption/span[@class='caption-number']", + '^Table 2.1 $', True), + ('bar.html', ".//table/caption/span[@class='caption-number']", + '^Table 2.3 $', True), + ('bar.html', ".//table/caption/span[@class='caption-number']", + '^Table 2.4 $', True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2.1 $', True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2.3 $', True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2.4 $', True), + + ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2 $', True), + ('baz.html', ".//table/caption/span[@class='caption-number']", + '^Table 2.2 $', True), + ('baz.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2.2 $', True), +]) +@pytest.mark.sphinx('html', testroot='numfig', confoverrides={'numfig': True}) +@pytest.mark.test_params(shared_result='test_build_html_numfig_on') +def test_numfig_with_numbered_toctree(app, cached_etree_parse, fname, path, check, be_found): + app.build() + check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found) + + +@pytest.mark.sphinx('html', testroot='numfig', confoverrides={ + 'numfig': True, + 'numfig_format': {'figure': 'Figure:%s', + 'table': 'Tab_%s', + 'code-block': 'Code-%s', + 'section': 'SECTION-%s'}}) +@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn') +def test_numfig_with_prefix_warn(app, warning): + app.build() + warnings = warning.getvalue() + assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings + assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings + assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings + assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings + + +@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [ + ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1 $', True), + ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2 $', True), + ('index.html', ".//table/caption/span[@class='caption-number']", + '^Tab_1 $', True), + ('index.html', ".//table/caption/span[@class='caption-number']", + '^Tab_2 $', True), + ('index.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Code-1 $', True), + ('index.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Code-2 $', True), + ('index.html', ".//li/p/a/span", '^Figure:1$', True), + ('index.html', ".//li/p/a/span", '^Figure2.2$', True), + ('index.html', ".//li/p/a/span", '^Tab_1$', True), + ('index.html', ".//li/p/a/span", '^Table:2.2$', True), + ('index.html', ".//li/p/a/span", '^Code-1$', True), + ('index.html', ".//li/p/a/span", '^Code-2.2$', True), + ('index.html', ".//li/p/a/span", '^SECTION-1$', True), + ('index.html', ".//li/p/a/span", '^SECTION-2.1$', True), + ('index.html', ".//li/p/a/span", '^Fig.1 should be Fig.1$', True), + ('index.html', ".//li/p/a/span", '^Sect.1 Foo$', True), + + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.1 $', True), + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.2 $', True), + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.3 $', True), + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:1.4 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Tab_1.1 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Tab_1.2 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Tab_1.3 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Tab_1.4 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Code-1.1 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Code-1.2 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Code-1.3 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Code-1.4 $', True), + + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.1 $', True), + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.3 $', True), + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.4 $', True), + ('bar.html', ".//table/caption/span[@class='caption-number']", + '^Tab_2.1 $', True), + ('bar.html', ".//table/caption/span[@class='caption-number']", + '^Tab_2.3 $', True), + ('bar.html', ".//table/caption/span[@class='caption-number']", + '^Tab_2.4 $', True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Code-2.1 $', True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Code-2.3 $', True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Code-2.4 $', True), + + ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Figure:2.2 $', True), + ('baz.html', ".//table/caption/span[@class='caption-number']", + '^Tab_2.2 $', True), + ('baz.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Code-2.2 $', True), +]) +@pytest.mark.sphinx('html', testroot='numfig', + confoverrides={'numfig': True, + 'numfig_format': {'figure': 'Figure:%s', + 'table': 'Tab_%s', + 'code-block': 'Code-%s', + 'section': 'SECTION-%s'}}) +@pytest.mark.test_params(shared_result='test_build_html_numfig_format_warn') +def test_numfig_with_prefix(app, cached_etree_parse, fname, path, check, be_found): + app.build() + check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found) + + +@pytest.mark.sphinx('html', testroot='numfig', + confoverrides={'numfig': True, 'numfig_secnum_depth': 2}) +@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2') +def test_numfig_with_secnum_depth_warn(app, warning): + app.build() + warnings = warning.getvalue() + assert 'index.rst:47: WARNING: numfig is disabled. :numref: is ignored.' not in warnings + assert 'index.rst:55: WARNING: Failed to create a cross reference. Any number is not assigned: index' in warnings + assert 'index.rst:56: WARNING: invalid numfig_format: invalid' in warnings + assert 'index.rst:57: WARNING: invalid numfig_format: Fig %s %s' in warnings + + +@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [ + ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True), + ('index.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True), + ('index.html', ".//table/caption/span[@class='caption-number']", + '^Table 1 $', True), + ('index.html', ".//table/caption/span[@class='caption-number']", + '^Table 2 $', True), + ('index.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1 $', True), + ('index.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2 $', True), + ('index.html', ".//li/p/a/span", '^Fig. 1$', True), + ('index.html', ".//li/p/a/span", '^Figure2.1.2$', True), + ('index.html', ".//li/p/a/span", '^Table 1$', True), + ('index.html', ".//li/p/a/span", '^Table:2.1.2$', True), + ('index.html', ".//li/p/a/span", '^Listing 1$', True), + ('index.html', ".//li/p/a/span", '^Code-2.1.2$', True), + ('index.html', ".//li/p/a/span", '^Section.1$', True), + ('index.html', ".//li/p/a/span", '^Section.2.1$', True), + ('index.html', ".//li/p/a/span", '^Fig.1 should be Fig.1$', True), + ('index.html', ".//li/p/a/span", '^Sect.1 Foo$', True), + + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True), + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1.1 $', True), + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1.2 $', True), + ('foo.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2.1 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Table 1.1 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Table 1.1.1 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Table 1.1.2 $', True), + ('foo.html', ".//table/caption/span[@class='caption-number']", + '^Table 1.2.1 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1.1 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1.1.1 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1.1.2 $', True), + ('foo.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1.2.1 $', True), + + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.1 $', True), + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.3 $', True), + ('bar.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2.1 $', True), + ('bar.html', ".//table/caption/span[@class='caption-number']", + '^Table 2.1.1 $', True), + ('bar.html', ".//table/caption/span[@class='caption-number']", + '^Table 2.1.3 $', True), + ('bar.html', ".//table/caption/span[@class='caption-number']", + '^Table 2.2.1 $', True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2.1.1 $', True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2.1.3 $', True), + ('bar.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2.2.1 $', True), + + ('baz.html', FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1.2 $', True), + ('baz.html', ".//table/caption/span[@class='caption-number']", + '^Table 2.1.2 $', True), + ('baz.html', ".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2.1.2 $', True), +]) +@pytest.mark.sphinx('html', testroot='numfig', + confoverrides={'numfig': True, + 'numfig_secnum_depth': 2}) +@pytest.mark.test_params(shared_result='test_build_html_numfig_depth_2') +def test_numfig_with_secnum_depth(app, cached_etree_parse, fname, path, check, be_found): + app.build() + check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found) + + +@pytest.mark.parametrize("expect", [ + (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1 $', True), + (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2 $', True), + (".//table/caption/span[@class='caption-number']", + '^Table 1 $', True), + (".//table/caption/span[@class='caption-number']", + '^Table 2 $', True), + (".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1 $', True), + (".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2 $', True), + (".//li/p/a/span", '^Fig. 1$', True), + (".//li/p/a/span", '^Figure2.2$', True), + (".//li/p/a/span", '^Table 1$', True), + (".//li/p/a/span", '^Table:2.2$', True), + (".//li/p/a/span", '^Listing 1$', True), + (".//li/p/a/span", '^Code-2.2$', True), + (".//li/p/a/span", '^Section.1$', True), + (".//li/p/a/span", '^Section.2.1$', True), + (".//li/p/a/span", '^Fig.1 should be Fig.1$', True), + (".//li/p/a/span", '^Sect.1 Foo$', True), + (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.1 $', True), + (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.2 $', True), + (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.3 $', True), + (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 1.4 $', True), + (".//table/caption/span[@class='caption-number']", + '^Table 1.1 $', True), + (".//table/caption/span[@class='caption-number']", + '^Table 1.2 $', True), + (".//table/caption/span[@class='caption-number']", + '^Table 1.3 $', True), + (".//table/caption/span[@class='caption-number']", + '^Table 1.4 $', True), + (".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1.1 $', True), + (".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1.2 $', True), + (".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1.3 $', True), + (".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 1.4 $', True), + (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.1 $', True), + (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.3 $', True), + (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.4 $', True), + (".//table/caption/span[@class='caption-number']", + '^Table 2.1 $', True), + (".//table/caption/span[@class='caption-number']", + '^Table 2.3 $', True), + (".//table/caption/span[@class='caption-number']", + '^Table 2.4 $', True), + (".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2.1 $', True), + (".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2.3 $', True), + (".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2.4 $', True), + (FIGURE_CAPTION + "/span[@class='caption-number']", '^Fig. 2.2 $', True), + (".//table/caption/span[@class='caption-number']", + '^Table 2.2 $', True), + (".//div[@class='code-block-caption']/" + "span[@class='caption-number']", '^Listing 2.2 $', True), +]) +@pytest.mark.sphinx('singlehtml', testroot='numfig', confoverrides={'numfig': True}) +@pytest.mark.test_params(shared_result='test_build_html_numfig_on') +def test_numfig_with_singlehtml(app, cached_etree_parse, expect): + app.build() + check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect) diff --git a/tests/test_builders/test_build_html_tocdepth.py b/tests/test_builders/test_build_html_tocdepth.py new file mode 100644 index 0000000..4d99995 --- /dev/null +++ b/tests/test_builders/test_build_html_tocdepth.py @@ -0,0 +1,94 @@ +"""Test the HTML builder and check output against XPath.""" + +import pytest + +from tests.test_builders.xpath_util import check_xpath + + +@pytest.mark.parametrize(("fname", "path", "check", "be_found"), [ + ('index.html', ".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True), + ('index.html', ".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True), + ('index.html', ".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False), + ('index.html', ".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False), + + ('foo.html', ".//h1", 'Foo', True), + ('foo.html', ".//h2", 'Foo A', True), + ('foo.html', ".//h3", 'Foo A1', True), + ('foo.html', ".//h2", 'Foo B', True), + ('foo.html', ".//h3", 'Foo B1', True), + + ('foo.html', ".//h1//span[@class='section-number']", '1. ', True), + ('foo.html', ".//h2//span[@class='section-number']", '1.1. ', True), + ('foo.html', ".//h3//span[@class='section-number']", '1.1.1. ', True), + ('foo.html', ".//h2//span[@class='section-number']", '1.2. ', True), + ('foo.html', ".//h3//span[@class='section-number']", '1.2.1. ', True), + + ('foo.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '1.1. Foo A', True), + ('foo.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '1.1.1. Foo A1', True), + ('foo.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '1.2. Foo B', True), + ('foo.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '1.2.1. Foo B1', True), + + ('bar.html', ".//h1", 'Bar', True), + ('bar.html', ".//h2", 'Bar A', True), + ('bar.html', ".//h2", 'Bar B', True), + ('bar.html', ".//h3", 'Bar B1', True), + ('bar.html', ".//h1//span[@class='section-number']", '2. ', True), + ('bar.html', ".//h2//span[@class='section-number']", '2.1. ', True), + ('bar.html', ".//h2//span[@class='section-number']", '2.2. ', True), + ('bar.html', ".//h3//span[@class='section-number']", '2.2.1. ', True), + ('bar.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '2. Bar', True), + ('bar.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '2.1. Bar A', True), + ('bar.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '2.2. Bar B', True), + ('bar.html', ".//div[@class='sphinxsidebarwrapper']//li/a", '2.2.1. Bar B1', False), + + ('baz.html', ".//h1", 'Baz A', True), + ('baz.html', ".//h1//span[@class='section-number']", '2.1.1. ', True), +]) +@pytest.mark.sphinx('html', testroot='tocdepth') +@pytest.mark.test_params(shared_result='test_build_html_tocdepth') +def test_tocdepth(app, cached_etree_parse, fname, path, check, be_found): + app.build() + # issue #1251 + check_xpath(cached_etree_parse(app.outdir / fname), fname, path, check, be_found) + + +@pytest.mark.parametrize("expect", [ + (".//li[@class='toctree-l3']/a", '1.1.1. Foo A1', True), + (".//li[@class='toctree-l3']/a", '1.2.1. Foo B1', True), + (".//li[@class='toctree-l3']/a", '2.1.1. Bar A1', False), + (".//li[@class='toctree-l3']/a", '2.2.1. Bar B1', False), + + # index.rst + (".//h1", 'test-tocdepth', True), + + # foo.rst + (".//h2", 'Foo', True), + (".//h3", 'Foo A', True), + (".//h4", 'Foo A1', True), + (".//h3", 'Foo B', True), + (".//h4", 'Foo B1', True), + (".//h2//span[@class='section-number']", '1. ', True), + (".//h3//span[@class='section-number']", '1.1. ', True), + (".//h4//span[@class='section-number']", '1.1.1. ', True), + (".//h3//span[@class='section-number']", '1.2. ', True), + (".//h4//span[@class='section-number']", '1.2.1. ', True), + + # bar.rst + (".//h2", 'Bar', True), + (".//h3", 'Bar A', True), + (".//h3", 'Bar B', True), + (".//h4", 'Bar B1', True), + (".//h2//span[@class='section-number']", '2. ', True), + (".//h3//span[@class='section-number']", '2.1. ', True), + (".//h3//span[@class='section-number']", '2.2. ', True), + (".//h4//span[@class='section-number']", '2.2.1. ', True), + + # baz.rst + (".//h4", 'Baz A', True), + (".//h4//span[@class='section-number']", '2.1.1. ', True), +]) +@pytest.mark.sphinx('singlehtml', testroot='tocdepth') +@pytest.mark.test_params(shared_result='test_build_html_tocdepth') +def test_tocdepth_singlehtml(app, cached_etree_parse, expect): + app.build() + check_xpath(cached_etree_parse(app.outdir / 'index.html'), 'index.html', *expect) diff --git a/tests/test_build_latex.py b/tests/test_builders/test_build_latex.py index e37a97e..0776c74 100644 --- a/tests/test_build_latex.py +++ b/tests/test_builders/test_build_latex.py @@ -1,9 +1,9 @@ """Test the build process with LaTeX builder with the test root.""" +import http.server import os import re import subprocess -from itertools import chain, product from pathlib import Path from shutil import copyfile from subprocess import CalledProcessError @@ -15,36 +15,26 @@ from sphinx.config import Config from sphinx.errors import SphinxError from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping from sphinx.ext.intersphinx import setup as intersphinx_setup -from sphinx.testing.util import strip_escseq from sphinx.util.osutil import ensuredir from sphinx.writers.latex import LaTeXTranslator -from .test_build_html import ENV_WARNINGS +from tests.utils import http_server try: from contextlib import chdir except ImportError: from sphinx.util.osutil import _chdir as chdir -LATEX_ENGINES = ['pdflatex', 'lualatex', 'xelatex'] -DOCCLASSES = ['manual', 'howto'] STYLEFILES = ['article.cls', 'fancyhdr.sty', 'titlesec.sty', 'amsmath.sty', 'framed.sty', 'color.sty', 'fancyvrb.sty', 'fncychap.sty', 'geometry.sty', 'kvoptions.sty', 'hyperref.sty', 'booktabs.sty'] -LATEX_WARNINGS = ENV_WARNINGS + """\ -%(root)s/index.rst:\\d+: WARNING: unknown option: '&option' -%(root)s/index.rst:\\d+: WARNING: citation not found: missing -%(root)s/index.rst:\\d+: WARNING: a suitable image for latex builder not found: foo.\\* -%(root)s/index.rst:\\d+: WARNING: Lexing literal_block ".*" as "c" resulted in an error at token: ".*". Retrying in relaxed mode. -""" - # only run latex if all needed packages are there def kpsetest(*filenames): try: - subprocess.run(['kpsewhich'] + list(filenames), capture_output=True, check=True) + subprocess.run(['kpsewhich', *list(filenames)], capture_output=True, check=True) return True except (OSError, CalledProcessError): return False # command not found or exit with non-zero @@ -64,7 +54,7 @@ def compile_latex_document(app, filename='python.tex', docclass='manual'): args = [app.config.latex_engine, '--halt-on-error', '--interaction=nonstopmode', - '-output-directory=%s' % latex_outputdir, + f'-output-directory={latex_outputdir}', filename] subprocess.run(args, capture_output=True, check=True) except OSError as exc: # most likely the latex executable was not found @@ -92,6 +82,28 @@ def skip_if_stylefiles_notfound(testfunc): return testfunc +class RemoteImageHandler(http.server.BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def do_GET(self): + content, content_type = None, None + if self.path == "/sphinx.png": + with open("tests/roots/test-local-logo/images/img.png", "rb") as f: + content = f.read() + content_type = "image/png" + + if content: + self.send_response(200, "OK") + self.send_header("Content-Length", str(len(content))) + self.send_header("Content-Type", content_type) + self.end_headers() + self.wfile.write(content) + else: + self.send_response(404, "Not Found") + self.send_header("Content-Length", "0") + self.end_headers() + + @skip_if_requested @skip_if_stylefiles_notfound @pytest.mark.parametrize( @@ -99,13 +111,17 @@ def skip_if_stylefiles_notfound(testfunc): # Only running test with `python_maximum_signature_line_length` not None with last # LaTeX engine to reduce testing time, as if this configuration does not fail with # one engine, it's almost impossible it would fail with another. - chain( - product(LATEX_ENGINES[:-1], DOCCLASSES, [None]), - product([LATEX_ENGINES[-1]], DOCCLASSES, [1]), - ), + [ + ('pdflatex', 'manual', None), + ('pdflatex', 'howto', None), + ('lualatex', 'manual', None), + ('lualatex', 'howto', None), + ('xelatex', 'manual', 1), + ('xelatex', 'howto', 1), + ], ) @pytest.mark.sphinx('latex', freshenv=True) -def test_build_latex_doc(app, status, warning, engine, docclass, python_maximum_signature_line_length): +def test_build_latex_doc(app, engine, docclass, python_maximum_signature_line_length): app.config.python_maximum_signature_line_length = python_maximum_signature_line_length app.config.intersphinx_mapping = { 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), @@ -121,7 +137,8 @@ def test_build_latex_doc(app, status, warning, engine, docclass, python_maximum_ load_mappings(app) app.builder.init() LaTeXTranslator.ignore_missing_images = True - app.builder.build_all() + with http_server(RemoteImageHandler): + app.build(force_all=True) # file from latex_additional_files assert (app.outdir / 'svgimg.svg').is_file() @@ -131,7 +148,7 @@ def test_build_latex_doc(app, status, warning, engine, docclass, python_maximum_ @pytest.mark.sphinx('latex') def test_writer(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'sphinxtests.tex').read_text(encoding='utf8') assert ('\\begin{sphinxfigure-in-table}\n\\centering\n\\capstart\n' @@ -165,7 +182,7 @@ def test_writer(app, status, warning): '\\sphinxAtStartPar\n' 'something, something else, something more\n' '\\begin{description}\n' - '\\sphinxlineitem{\\sphinxhref{http://www.google.com}{Google}}\n' + '\\sphinxlineitem{\\sphinxhref{https://www.google.com}{Google}}\n' '\\sphinxAtStartPar\n' 'For everything.\n' '\n' @@ -173,22 +190,9 @@ def test_writer(app, status, warning): '\n\n\\end{sphinxseealso}\n\n' in result) -@pytest.mark.sphinx('latex', testroot='warnings', freshenv=True) -def test_latex_warnings(app, status, warning): - app.builder.build_all() - - warnings = strip_escseq(re.sub(re.escape(os.sep) + '{1,2}', '/', warning.getvalue())) - warnings_exp = LATEX_WARNINGS % { - 'root': re.escape(app.srcdir.as_posix())} - assert re.match(warnings_exp + '$', warnings), \ - "Warnings don't match:\n" + \ - '--- Expected (regex):\n' + warnings_exp + \ - '--- Got:\n' + warnings - - @pytest.mark.sphinx('latex', testroot='basic') def test_latex_basic(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'test.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -203,7 +207,7 @@ def test_latex_basic(app, status, warning): 'latex_documents': [('index', 'test.tex', 'title', 'author', 'manual')], }) def test_latex_basic_manual(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'test.tex').read_text(encoding='utf8') print(result) assert r'\def\sphinxdocclass{report}' in result @@ -215,7 +219,7 @@ def test_latex_basic_manual(app, status, warning): 'latex_documents': [('index', 'test.tex', 'title', 'author', 'howto')], }) def test_latex_basic_howto(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'test.tex').read_text(encoding='utf8') print(result) assert r'\def\sphinxdocclass{article}' in result @@ -228,7 +232,7 @@ def test_latex_basic_howto(app, status, warning): 'latex_documents': [('index', 'test.tex', 'title', 'author', 'manual')], }) def test_latex_basic_manual_ja(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'test.tex').read_text(encoding='utf8') print(result) assert r'\def\sphinxdocclass{ujbook}' in result @@ -241,7 +245,7 @@ def test_latex_basic_manual_ja(app, status, warning): 'latex_documents': [('index', 'test.tex', 'title', 'author', 'howto')], }) def test_latex_basic_howto_ja(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'test.tex').read_text(encoding='utf8') print(result) assert r'\def\sphinxdocclass{ujreport}' in result @@ -250,7 +254,7 @@ def test_latex_basic_howto_ja(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-theme') def test_latex_theme(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) assert r'\def\sphinxdocclass{book}' in result @@ -261,7 +265,7 @@ def test_latex_theme(app, status, warning): confoverrides={'latex_elements': {'papersize': 'b5paper', 'pointsize': '9pt'}}) def test_latex_theme_papersize(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) assert r'\def\sphinxdocclass{book}' in result @@ -272,7 +276,7 @@ def test_latex_theme_papersize(app, status, warning): confoverrides={'latex_theme_options': {'papersize': 'b5paper', 'pointsize': '9pt'}}) def test_latex_theme_options(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) assert r'\def\sphinxdocclass{book}' in result @@ -281,7 +285,7 @@ def test_latex_theme_options(app, status, warning): @pytest.mark.sphinx('latex', testroot='basic', confoverrides={'language': 'zh'}) def test_latex_additional_settings_for_language_code(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'test.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -291,7 +295,7 @@ def test_latex_additional_settings_for_language_code(app, status, warning): @pytest.mark.sphinx('latex', testroot='basic', confoverrides={'language': 'el'}) def test_latex_additional_settings_for_greek(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'test.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -302,7 +306,7 @@ def test_latex_additional_settings_for_greek(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-title') def test_latex_title_after_admonitions(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'test.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -313,7 +317,7 @@ def test_latex_title_after_admonitions(app, status, warning): @pytest.mark.sphinx('latex', testroot='basic', confoverrides={'release': '1.0_0'}) def test_latex_release(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'test.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -325,7 +329,7 @@ def test_latex_release(app, status, warning): @pytest.mark.sphinx('latex', testroot='numfig', confoverrides={'numfig': True}) def test_numref(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -367,7 +371,7 @@ def test_numref(app, status, warning): 'code-block': 'Code-%s', 'section': 'SECTION-%s'}}) def test_numref_with_prefix1(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -415,7 +419,7 @@ def test_numref_with_prefix1(app, status, warning): 'code-block': 'Code-%s | ', 'section': 'SECTION_%s_'}}) def test_numref_with_prefix2(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -491,7 +495,7 @@ def test_numref_with_language_ja(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-numfig') def test_latex_obey_numfig_is_false(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'SphinxManual.tex').read_text(encoding='utf8') assert '\\usepackage{sphinx}' in result @@ -504,7 +508,7 @@ def test_latex_obey_numfig_is_false(app, status, warning): 'latex', testroot='latex-numfig', confoverrides={'numfig': True, 'numfig_secnum_depth': 0}) def test_latex_obey_numfig_secnum_depth_is_zero(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'SphinxManual.tex').read_text(encoding='utf8') assert '\\usepackage[,nonumfigreset,mathnumfig]{sphinx}' in result @@ -517,7 +521,7 @@ def test_latex_obey_numfig_secnum_depth_is_zero(app, status, warning): 'latex', testroot='latex-numfig', confoverrides={'numfig': True, 'numfig_secnum_depth': 2}) def test_latex_obey_numfig_secnum_depth_is_two(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'SphinxManual.tex').read_text(encoding='utf8') assert '\\usepackage[,numfigreset=2,mathnumfig]{sphinx}' in result @@ -530,7 +534,7 @@ def test_latex_obey_numfig_secnum_depth_is_two(app, status, warning): 'latex', testroot='latex-numfig', confoverrides={'numfig': True, 'math_numfig': False}) def test_latex_obey_numfig_but_math_numfig_false(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'SphinxManual.tex').read_text(encoding='utf8') assert '\\usepackage[,numfigreset=1]{sphinx}' in result @@ -543,7 +547,7 @@ def test_latex_obey_numfig_but_math_numfig_false(app, status, warning): def test_latex_add_latex_package(app, status, warning): app.add_latex_package('foo') app.add_latex_package('bar', 'baz') - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'test.tex').read_text(encoding='utf8') assert '\\usepackage{foo}' in result assert '\\usepackage[baz]{bar}' in result @@ -551,7 +555,7 @@ def test_latex_add_latex_package(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-babel') def test_babel_with_no_language_settings(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -576,7 +580,7 @@ def test_babel_with_no_language_settings(app, status, warning): 'latex', testroot='latex-babel', confoverrides={'language': 'de'}) def test_babel_with_language_de(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -601,7 +605,7 @@ def test_babel_with_language_de(app, status, warning): 'latex', testroot='latex-babel', confoverrides={'language': 'ru'}) def test_babel_with_language_ru(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -626,7 +630,7 @@ def test_babel_with_language_ru(app, status, warning): 'latex', testroot='latex-babel', confoverrides={'language': 'tr'}) def test_babel_with_language_tr(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -651,7 +655,7 @@ def test_babel_with_language_tr(app, status, warning): 'latex', testroot='latex-babel', confoverrides={'language': 'ja'}) def test_babel_with_language_ja(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -675,7 +679,7 @@ def test_babel_with_language_ja(app, status, warning): 'latex', testroot='latex-babel', confoverrides={'language': 'unknown'}) def test_babel_with_unknown_language(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -702,7 +706,7 @@ def test_babel_with_unknown_language(app, status, warning): 'latex', testroot='latex-babel', confoverrides={'language': 'de', 'latex_engine': 'lualatex'}) def test_polyglossia_with_language_de(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -728,7 +732,7 @@ def test_polyglossia_with_language_de(app, status, warning): 'latex', testroot='latex-babel', confoverrides={'language': 'de-1901', 'latex_engine': 'lualatex'}) def test_polyglossia_with_language_de_1901(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -752,7 +756,7 @@ def test_polyglossia_with_language_de_1901(app, status, warning): @pytest.mark.sphinx('latex') def test_footnote(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'sphinxtests.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -781,7 +785,7 @@ def test_footnote(app, status, warning): @pytest.mark.sphinx('latex', testroot='footnotes') def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -821,7 +825,7 @@ def test_reference_in_caption_and_codeblock_in_footnote(app, status, warning): @pytest.mark.sphinx('latex', testroot='footnotes') def test_footnote_referred_multiple_times(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -843,7 +847,7 @@ def test_footnote_referred_multiple_times(app, status, warning): 'latex', testroot='footnotes', confoverrides={'latex_show_urls': 'inline'}) def test_latex_show_urls_is_inline(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -866,29 +870,29 @@ def test_latex_show_urls_is_inline(app, status, warning): assert ('Second footnote: %\n' '\\begin{footnote}[1]\\sphinxAtStartFootnote\n' 'Second\n%\n\\end{footnote}\n') in result - assert '\\sphinxhref{http://sphinx-doc.org/}{Sphinx} (http://sphinx\\sphinxhyphen{}doc.org/)' in result + assert '\\sphinxhref{https://sphinx-doc.org/}{Sphinx} (https://sphinx\\sphinxhyphen{}doc.org/)' in result assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n' 'Third \\sphinxfootnotemark[4]\n%\n\\end{footnote}%\n' '\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n' 'Footnote inside footnote\n%\n\\end{footnotetext}\\ignorespaces') in result assert ('Fourth footnote: %\n\\begin{footnote}[5]\\sphinxAtStartFootnote\n' 'Fourth\n%\n\\end{footnote}\n') in result - assert ('\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde} ' - '(http://sphinx\\sphinxhyphen{}doc.org/\\textasciitilde{}test/)') in result - assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{URL in term} ' - '(http://sphinx\\sphinxhyphen{}doc.org/)}\n' + assert ('\\sphinxhref{https://sphinx-doc.org/~test/}{URL including tilde} ' + '(https://sphinx\\sphinxhyphen{}doc.org/\\textasciitilde{}test/)') in result + assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{URL in term} ' + '(https://sphinx\\sphinxhyphen{}doc.org/)}\n' '\\sphinxAtStartPar\nDescription' in result) assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[7]}%\n' '\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' in result) - assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{URL in term} ' - '(http://sphinx\\sphinxhyphen{}doc.org/)}\n' + assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{URL in term} ' + '(https://sphinx\\sphinxhyphen{}doc.org/)}\n' '\\sphinxAtStartPar\nDescription' in result) assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[7]}%\n' '\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result - assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist} ' - '(http://sphinx\\sphinxhyphen{}doc.org/)}' + assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{Term in deflist} ' + '(https://sphinx\\sphinxhyphen{}doc.org/)}' '\n\\sphinxAtStartPar\nDescription') in result assert '\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}' @@ -900,7 +904,7 @@ def test_latex_show_urls_is_inline(app, status, warning): 'latex', testroot='footnotes', confoverrides={'latex_show_urls': 'footnote'}) def test_latex_show_urls_is_footnote(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -922,9 +926,9 @@ def test_latex_show_urls_is_footnote(app, status, warning): assert ('Second footnote: %\n' '\\begin{footnote}[1]\\sphinxAtStartFootnote\n' 'Second\n%\n\\end{footnote}') in result - assert ('\\sphinxhref{http://sphinx-doc.org/}{Sphinx}' + assert ('\\sphinxhref{https://sphinx-doc.org/}{Sphinx}' '%\n\\begin{footnote}[4]\\sphinxAtStartFootnote\n' - '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n\\end{footnote}') in result + '\\sphinxnolinkurl{https://sphinx-doc.org/}\n%\n\\end{footnote}') in result assert ('Third footnote: %\n\\begin{footnote}[6]\\sphinxAtStartFootnote\n' 'Third \\sphinxfootnotemark[7]\n%\n\\end{footnote}%\n' '\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' @@ -932,25 +936,25 @@ def test_latex_show_urls_is_footnote(app, status, warning): '\\end{footnotetext}\\ignorespaces') in result assert ('Fourth footnote: %\n\\begin{footnote}[8]\\sphinxAtStartFootnote\n' 'Fourth\n%\n\\end{footnote}\n') in result - assert ('\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde}' + assert ('\\sphinxhref{https://sphinx-doc.org/~test/}{URL including tilde}' '%\n\\begin{footnote}[5]\\sphinxAtStartFootnote\n' - '\\sphinxnolinkurl{http://sphinx-doc.org/~test/}\n%\n\\end{footnote}') in result - assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}' + '\\sphinxnolinkurl{https://sphinx-doc.org/~test/}\n%\n\\end{footnote}') in result + assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}' '{URL in term}\\sphinxfootnotemark[10]}%\n' '\\begin{footnotetext}[10]' '\\sphinxAtStartFootnote\n' - '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n' + '\\sphinxnolinkurl{https://sphinx-doc.org/}\n%\n' '\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[12]}%\n' '\\begin{footnotetext}[12]' '\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result - assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist}' + assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{Term in deflist}' '\\sphinxfootnotemark[11]}%\n' '\\begin{footnotetext}[11]' '\\sphinxAtStartFootnote\n' - '\\sphinxnolinkurl{http://sphinx-doc.org/}\n%\n' + '\\sphinxnolinkurl{https://sphinx-doc.org/}\n%\n' '\\end{footnotetext}\\ignorespaces \n\\sphinxAtStartPar\nDescription') in result assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result) assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}' @@ -962,7 +966,7 @@ def test_latex_show_urls_is_footnote(app, status, warning): 'latex', testroot='footnotes', confoverrides={'latex_show_urls': 'no'}) def test_latex_show_urls_is_no(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -984,21 +988,21 @@ def test_latex_show_urls_is_no(app, status, warning): assert ('Second footnote: %\n' '\\begin{footnote}[1]\\sphinxAtStartFootnote\n' 'Second\n%\n\\end{footnote}') in result - assert '\\sphinxhref{http://sphinx-doc.org/}{Sphinx}' in result + assert '\\sphinxhref{https://sphinx-doc.org/}{Sphinx}' in result assert ('Third footnote: %\n\\begin{footnote}[3]\\sphinxAtStartFootnote\n' 'Third \\sphinxfootnotemark[4]\n%\n\\end{footnote}%\n' '\\begin{footnotetext}[4]\\sphinxAtStartFootnote\n' 'Footnote inside footnote\n%\n\\end{footnotetext}\\ignorespaces') in result assert ('Fourth footnote: %\n\\begin{footnote}[5]\\sphinxAtStartFootnote\n' 'Fourth\n%\n\\end{footnote}\n') in result - assert '\\sphinxhref{http://sphinx-doc.org/~test/}{URL including tilde}' in result - assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{URL in term}}\n' + assert '\\sphinxhref{https://sphinx-doc.org/~test/}{URL including tilde}' in result + assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{URL in term}}\n' '\\sphinxAtStartPar\nDescription') in result assert ('\\sphinxlineitem{Footnote in term \\sphinxfootnotemark[7]}%\n' '\\begin{footnotetext}[7]\\sphinxAtStartFootnote\n' 'Footnote in term\n%\n\\end{footnotetext}\\ignorespaces ' '\n\\sphinxAtStartPar\nDescription') in result - assert ('\\sphinxlineitem{\\sphinxhref{http://sphinx-doc.org/}{Term in deflist}}' + assert ('\\sphinxlineitem{\\sphinxhref{https://sphinx-doc.org/}{Term in deflist}}' '\n\\sphinxAtStartPar\nDescription') in result assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result) assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}' @@ -1009,7 +1013,7 @@ def test_latex_show_urls_is_no(app, status, warning): @pytest.mark.sphinx( 'latex', testroot='footnotes', confoverrides={'latex_show_urls': 'footnote', - 'rst_prolog': '.. |URL| replace:: `text <http://www.example.com/>`__'}) + 'rst_prolog': '.. |URL| replace:: `text <https://www.example.com/>`__'}) def test_latex_show_urls_footnote_and_substitutions(app, status, warning): # hyperlinks in substitutions should not effect to make footnotes (refs: #4784) test_latex_show_urls_is_footnote(app, status, warning) @@ -1017,7 +1021,7 @@ def test_latex_show_urls_footnote_and_substitutions(app, status, warning): @pytest.mark.sphinx('latex', testroot='image-in-section') def test_image_in_section(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1035,12 +1039,12 @@ def test_image_in_section(app, status, warning): confoverrides={'latex_logo': 'notfound.jpg'}) def test_latex_logo_if_not_found(app, status, warning): with pytest.raises(SphinxError): - app.builder.build_all() + app.build(force_all=True) @pytest.mark.sphinx('latex', testroot='toctree-maxdepth') def test_toctree_maxdepth_manual(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1057,7 +1061,7 @@ def test_toctree_maxdepth_manual(app, status, warning): 'Georg Brandl', 'howto'), ]}) def test_toctree_maxdepth_howto(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1071,7 +1075,7 @@ def test_toctree_maxdepth_howto(app, status, warning): 'latex', testroot='toctree-maxdepth', confoverrides={'root_doc': 'foo'}) def test_toctree_not_found(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1085,7 +1089,7 @@ def test_toctree_not_found(app, status, warning): 'latex', testroot='toctree-maxdepth', confoverrides={'root_doc': 'bar'}) def test_toctree_without_maxdepth(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1098,7 +1102,7 @@ def test_toctree_without_maxdepth(app, status, warning): 'latex', testroot='toctree-maxdepth', confoverrides={'root_doc': 'qux'}) def test_toctree_with_deeper_maxdepth(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1111,7 +1115,7 @@ def test_toctree_with_deeper_maxdepth(app, status, warning): 'latex', testroot='toctree-maxdepth', confoverrides={'latex_toplevel_sectioning': None}) def test_latex_toplevel_sectioning_is_None(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1123,7 +1127,7 @@ def test_latex_toplevel_sectioning_is_None(app, status, warning): 'latex', testroot='toctree-maxdepth', confoverrides={'latex_toplevel_sectioning': 'part'}) def test_latex_toplevel_sectioning_is_part(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1141,7 +1145,7 @@ def test_latex_toplevel_sectioning_is_part(app, status, warning): 'Georg Brandl', 'howto'), ]}) def test_latex_toplevel_sectioning_is_part_with_howto(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1155,7 +1159,7 @@ def test_latex_toplevel_sectioning_is_part_with_howto(app, status, warning): 'latex', testroot='toctree-maxdepth', confoverrides={'latex_toplevel_sectioning': 'chapter'}) def test_latex_toplevel_sectioning_is_chapter(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1171,7 +1175,7 @@ def test_latex_toplevel_sectioning_is_chapter(app, status, warning): 'Georg Brandl', 'howto'), ]}) def test_latex_toplevel_sectioning_is_chapter_with_howto(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1183,7 +1187,7 @@ def test_latex_toplevel_sectioning_is_chapter_with_howto(app, status, warning): 'latex', testroot='toctree-maxdepth', confoverrides={'latex_toplevel_sectioning': 'section'}) def test_latex_toplevel_sectioning_is_section(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1194,7 +1198,7 @@ def test_latex_toplevel_sectioning_is_section(app, status, warning): @skip_if_stylefiles_notfound @pytest.mark.sphinx('latex', testroot='maxlistdepth') def test_maxlistdepth_at_ten(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) print(status.getvalue()) @@ -1206,7 +1210,7 @@ def test_maxlistdepth_at_ten(app, status, warning): confoverrides={'latex_table_style': []}) @pytest.mark.test_params(shared_result='latex-table') def test_latex_table_tabulars(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') tables = {} for chap in re.split(r'\\(?:section|chapter){', result)[1:]: @@ -1277,7 +1281,7 @@ def test_latex_table_tabulars(app, status, warning): confoverrides={'latex_table_style': []}) @pytest.mark.test_params(shared_result='latex-table') def test_latex_table_longtable(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') tables = {} for chap in re.split(r'\\(?:section|chapter){', result)[1:]: @@ -1338,7 +1342,7 @@ def test_latex_table_longtable(app, status, warning): confoverrides={'latex_table_style': []}) @pytest.mark.test_params(shared_result='latex-table') def test_latex_table_complex_tables(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') tables = {} for chap in re.split(r'\\(?:section|renewcommand){', result)[1:]: @@ -1368,7 +1372,7 @@ def test_latex_table_complex_tables(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-table') def test_latex_table_with_booktabs_and_colorrows(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') assert r'\PassOptionsToPackage{booktabs}{sphinx}' in result assert r'\PassOptionsToPackage{colorrows}{sphinx}' in result @@ -1384,7 +1388,7 @@ def test_latex_table_with_booktabs_and_colorrows(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-table', confoverrides={'templates_path': ['_mytemplates/latex']}) def test_latex_table_custom_template_caseA(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') assert 'SALUT LES COPAINS' in result @@ -1392,7 +1396,7 @@ def test_latex_table_custom_template_caseA(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-table', confoverrides={'templates_path': ['_mytemplates']}) def test_latex_table_custom_template_caseB(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') assert 'SALUT LES COPAINS' not in result @@ -1400,14 +1404,14 @@ def test_latex_table_custom_template_caseB(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-table') @pytest.mark.test_params(shared_result='latex-table') def test_latex_table_custom_template_caseC(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') assert 'SALUT LES COPAINS' not in result @pytest.mark.sphinx('latex', testroot='directives-raw') def test_latex_raw_directive(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') # standard case @@ -1422,18 +1426,19 @@ def test_latex_raw_directive(app, status, warning): @pytest.mark.sphinx('latex', testroot='images') def test_latex_images(app, status, warning): - app.builder.build_all() + with http_server(RemoteImageHandler, port=7777): + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') # images are copied - assert '\\sphinxincludegraphics{{python-logo}.png}' in result - assert (app.outdir / 'python-logo.png').exists() + assert '\\sphinxincludegraphics{{sphinx}.png}' in result + assert (app.outdir / 'sphinx.png').exists() # not found images assert '\\sphinxincludegraphics{{NOT_EXIST}.PNG}' not in result assert ('WARNING: Could not fetch remote image: ' - 'https://www.google.com/NOT_EXIST.PNG [404]' in warning.getvalue()) + 'http://localhost:7777/NOT_EXIST.PNG [404]' in warning.getvalue()) # an image having target assert ('\\sphinxhref{https://www.sphinx-doc.org/}' @@ -1446,7 +1451,7 @@ def test_latex_images(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-index') def test_latex_index(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') assert ('A \\index{famous@\\spxentry{famous}}famous ' @@ -1460,7 +1465,7 @@ def test_latex_index(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-equations') def test_latex_equations(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') expected = (app.srcdir / 'expects' / 'latex-equations.tex').read_text(encoding='utf8').strip() @@ -1470,7 +1475,7 @@ def test_latex_equations(app, status, warning): @pytest.mark.sphinx('latex', testroot='image-in-parsed-literal') def test_latex_image_in_parsed_literal(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') assert ('{\\sphinxunactivateextrasandspace \\raisebox{-0.5\\height}' @@ -1480,7 +1485,7 @@ def test_latex_image_in_parsed_literal(app, status, warning): @pytest.mark.sphinx('latex', testroot='nested-enumerated-list') def test_latex_nested_enumerated_list(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') assert ('\\sphinxsetlistlabels{\\arabic}{enumi}{enumii}{}{.}%\n' @@ -1497,7 +1502,7 @@ def test_latex_nested_enumerated_list(app, status, warning): @pytest.mark.sphinx('latex', testroot='footnotes') def test_latex_thebibliography(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) @@ -1510,7 +1515,7 @@ def test_latex_thebibliography(app, status, warning): @pytest.mark.sphinx('latex', testroot='glossary') def test_latex_glossary(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') assert (r'\sphinxlineitem{ähnlich\index{ähnlich@\spxentry{ähnlich}|spxpagem}' @@ -1534,7 +1539,7 @@ def test_latex_glossary(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-labels') def test_latex_labels(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') @@ -1583,7 +1588,7 @@ def test_latex_labels(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-figure-in-admonition') def test_latex_figure_in_admonition(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') assert r'\begin{figure}[H]' in result @@ -1594,7 +1599,6 @@ def test_default_latex_documents(): config = Config({'root_doc': 'index', 'project': 'STASI™ Documentation', 'author': "Wolfgang Schäuble & G'Beckstein."}) - config.init_values() config.add('latex_engine', None, True, None) config.add('latex_theme', 'manual', True, None) expected = [('index', 'stasi.tex', 'STASI™ Documentation', @@ -1606,7 +1610,7 @@ def test_default_latex_documents(): @skip_if_stylefiles_notfound @pytest.mark.sphinx('latex', testroot='latex-includegraphics') def test_includegraphics_oversized(app, status, warning): - app.builder.build_all() + app.build(force_all=True) print(status.getvalue()) print(warning.getvalue()) compile_latex_document(app) @@ -1614,7 +1618,7 @@ def test_includegraphics_oversized(app, status, warning): @pytest.mark.sphinx('latex', testroot='index_on_title') def test_index_on_title(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') assert ('\\chapter{Test for index in top level title}\n' '\\label{\\detokenize{contents:test-for-index-in-top-level-title}}' @@ -1625,7 +1629,7 @@ def test_index_on_title(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-unicode', confoverrides={'latex_engine': 'pdflatex'}) def test_texescape_for_non_unicode_supported_engine(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) assert 'script small e: e' in result @@ -1637,7 +1641,7 @@ def test_texescape_for_non_unicode_supported_engine(app, status, warning): @pytest.mark.sphinx('latex', testroot='latex-unicode', confoverrides={'latex_engine': 'xelatex'}) def test_texescape_for_unicode_supported_engine(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(result) assert 'script small e: e' in result @@ -1649,20 +1653,20 @@ def test_texescape_for_unicode_supported_engine(app, status, warning): @pytest.mark.sphinx('latex', testroot='basic', confoverrides={'latex_elements': {'extrapackages': r'\usepackage{foo}'}}) def test_latex_elements_extrapackages(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'test.tex').read_text(encoding='utf8') assert r'\usepackage{foo}' in result @pytest.mark.sphinx('latex', testroot='nested-tables') def test_latex_nested_tables(app, status, warning): - app.builder.build_all() + app.build(force_all=True) assert warning.getvalue() == '' @pytest.mark.sphinx('latex', testroot='latex-container') def test_latex_container(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') assert r'\begin{sphinxuseclass}{classname}' in result assert r'\end{sphinxuseclass}' in result @@ -1704,7 +1708,7 @@ def test_copy_images(app, status, warning): image.name for image in test_dir.rglob('*') if image.suffix in {'.gif', '.pdf', '.png', '.svg'} } - images.discard('python-logo.png') + images.discard('sphinx.png') assert images == { 'img.pdf', 'rimg.png', @@ -1745,7 +1749,7 @@ def test_duplicated_labels_before_module(app, status, warning): @pytest.mark.sphinx('latex', testroot='domain-py-python_maximum_signature_line_length', confoverrides={'python_maximum_signature_line_length': 23}) def test_one_parameter_per_line(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') # TODO: should these asserts check presence or absence of a final \sphinxparamcomma? diff --git a/tests/test_build_linkcheck.py b/tests/test_builders/test_build_linkcheck.py index 38a0bd1..c8d8515 100644 --- a/tests/test_build_linkcheck.py +++ b/tests/test_builders/test_build_linkcheck.py @@ -2,7 +2,6 @@ from __future__ import annotations -import http.server import json import re import sys @@ -10,10 +9,11 @@ import textwrap import time import wsgiref.handlers from base64 import b64encode -from os import path +from http.server import BaseHTTPRequestHandler from queue import Queue from unittest import mock +import docutils import pytest from urllib3.poolmanager import PoolManager @@ -23,18 +23,18 @@ from sphinx.builders.linkcheck import ( Hyperlink, HyperlinkAvailabilityCheckWorker, RateLimit, + compile_linkcheck_allowed_redirects, ) -from sphinx.testing.util import strip_escseq +from sphinx.deprecation import RemovedInSphinx80Warning from sphinx.util import requests from sphinx.util.console import strip_colors -from .utils import CERT_FILE, http_server, https_server +from tests.utils import CERT_FILE, serve_application ts_re = re.compile(r".*\[(?P<ts>.*)\].*") -SPHINX_DOCS_INDEX = path.abspath(path.join(__file__, "..", "roots", "test-linkcheck", "sphinx-docs-index.html")) -class DefaultsHandler(http.server.BaseHTTPRequestHandler): +class DefaultsHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def do_HEAD(self): @@ -102,7 +102,7 @@ class ConnectionMeasurement: @pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True) def test_defaults(app): - with http_server(DefaultsHandler): + with serve_application(app, DefaultsHandler) as address: with ConnectionMeasurement() as m: app.build() assert m.connection_count <= 5 @@ -115,9 +115,9 @@ def test_defaults(app): assert "Anchor 'top' not found" in content assert "Anchor 'does-not-exist' not found" in content # images should fail - assert "Not Found for url: http://localhost:7777/image.png" in content - assert "Not Found for url: http://localhost:7777/image2.png" in content - # looking for local file should fail + assert f"Not Found for url: http://{address}/image.png" in content + assert f"Not Found for url: http://{address}/image2.png" in content + # looking for missing local file should fail assert "[broken] path/to/notfound" in content assert len(content.splitlines()) == 5 @@ -135,35 +135,42 @@ def test_defaults(app): # the output order of the rows is not stable # due to possible variance in network latency rowsby = {row["uri"]: row for row in rows} - assert rowsby["http://localhost:7777#!bar"] == { + # looking for local file that exists should succeed + assert rowsby["conf.py"]["status"] == "working" + assert rowsby[f"http://{address}#!bar"] == { 'filename': 'links.rst', 'lineno': 5, 'status': 'working', 'code': 0, - 'uri': 'http://localhost:7777#!bar', + 'uri': f'http://{address}#!bar', 'info': '', } - assert rowsby['http://localhost:7777/image2.png'] == { - 'filename': 'links.rst', - 'lineno': 13, - 'status': 'broken', - 'code': 0, - 'uri': 'http://localhost:7777/image2.png', - 'info': '404 Client Error: Not Found for url: http://localhost:7777/image2.png', - } + + def _missing_resource(filename: str, lineno: int): + return { + 'filename': 'links.rst', + 'lineno': lineno, + 'status': 'broken', + 'code': 0, + 'uri': f'http://{address}/{filename}', + 'info': f'404 Client Error: Not Found for url: http://{address}/{filename}', + } + accurate_linenumbers = docutils.__version_info__[:2] >= (0, 21) + image2_lineno = 12 if accurate_linenumbers else 13 + assert rowsby[f'http://{address}/image2.png'] == _missing_resource("image2.png", image2_lineno) # looking for '#top' and '#does-not-exist' not found should fail - assert rowsby["http://localhost:7777/#top"]["info"] == "Anchor 'top' not found" - assert rowsby["http://localhost:7777/#top"]["status"] == "broken" - assert rowsby["http://localhost:7777#does-not-exist"]["info"] == "Anchor 'does-not-exist' not found" + assert rowsby[f"http://{address}/#top"]["info"] == "Anchor 'top' not found" + assert rowsby[f"http://{address}/#top"]["status"] == "broken" + assert rowsby[f"http://{address}#does-not-exist"]["info"] == "Anchor 'does-not-exist' not found" # images should fail - assert "Not Found for url: http://localhost:7777/image.png" in rowsby["http://localhost:7777/image.png"]["info"] + assert f"Not Found for url: http://{address}/image.png" in rowsby[f"http://{address}/image.png"]["info"] # anchor should be found - assert rowsby['http://localhost:7777/anchor.html#found'] == { + assert rowsby[f'http://{address}/anchor.html#found'] == { 'filename': 'links.rst', 'lineno': 14, 'status': 'working', 'code': 0, - 'uri': 'http://localhost:7777/anchor.html#found', + 'uri': f'http://{address}/anchor.html#found', 'info': '', } @@ -172,7 +179,7 @@ def test_defaults(app): 'linkcheck', testroot='linkcheck', freshenv=True, confoverrides={'linkcheck_anchors': False}) def test_check_link_response_only(app): - with http_server(DefaultsHandler): + with serve_application(app, DefaultsHandler) as address: app.build() # JSON output @@ -181,12 +188,12 @@ def test_check_link_response_only(app): rows = [json.loads(x) for x in content.splitlines()] rowsby = {row["uri"]: row for row in rows} - assert rowsby["http://localhost:7777/#top"]["status"] == "working" + assert rowsby[f"http://{address}/#top"]["status"] == "working" @pytest.mark.sphinx('linkcheck', testroot='linkcheck-too-many-retries', freshenv=True) def test_too_many_retries(app): - with http_server(DefaultsHandler): + with serve_application(app, DefaultsHandler) as address: app.build() # Text output @@ -210,12 +217,20 @@ def test_too_many_retries(app): assert row['lineno'] == 1 assert row['status'] == 'broken' assert row['code'] == 0 - assert row['uri'] == 'https://localhost:7777/doesnotexist' + assert row['uri'] == f'https://{address}/doesnotexist' @pytest.mark.sphinx('linkcheck', testroot='linkcheck-raw-node', freshenv=True) def test_raw_node(app): - with http_server(OKHandler): + with serve_application(app, OKHandler) as address: + # write an index file that contains a link back to this webserver's root + # URL. docutils will replace the raw node with the contents retrieved.. + # ..and then the linkchecker will check that the root URL is available. + index = (app.srcdir / "index.rst") + index.write_text( + ".. raw:: 'html'\n" + " :url: http://{address}/".format(address=address), + ) app.build() # JSON output @@ -231,7 +246,7 @@ def test_raw_node(app): 'lineno': 1, 'status': 'working', 'code': 0, - 'uri': 'http://localhost:7777/', + 'uri': f'http://{address}/', # the received rST contains a link to its' own URL 'info': '', } @@ -240,7 +255,7 @@ def test_raw_node(app): 'linkcheck', testroot='linkcheck-anchors-ignore', freshenv=True, confoverrides={'linkcheck_anchors_ignore': ["^!", "^top$"]}) def test_anchors_ignored(app): - with http_server(OKHandler): + with serve_application(app, OKHandler): app.build() assert (app.outdir / 'output.txt').exists() @@ -250,7 +265,7 @@ def test_anchors_ignored(app): assert not content -class AnchorsIgnoreForUrlHandler(http.server.BaseHTTPRequestHandler): +class AnchorsIgnoreForUrlHandler(BaseHTTPRequestHandler): def do_HEAD(self): if self.path in {'/valid', '/ignored'}: self.send_response(200, "OK") @@ -266,14 +281,13 @@ class AnchorsIgnoreForUrlHandler(http.server.BaseHTTPRequestHandler): self.wfile.write(b"no anchor but page exists\n") -@pytest.mark.sphinx( - 'linkcheck', testroot='linkcheck-anchors-ignore-for-url', freshenv=True, - confoverrides={'linkcheck_anchors_ignore_for_url': [ - 'http://localhost:7777/ignored', # existing page - 'http://localhost:7777/invalid', # unknown page - ]}) +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-anchors-ignore-for-url', freshenv=True) def test_anchors_ignored_for_url(app): - with http_server(AnchorsIgnoreForUrlHandler): + with serve_application(app, AnchorsIgnoreForUrlHandler) as address: + app.config.linkcheck_anchors_ignore_for_url = [ # type: ignore[attr-defined] + f'http://{address}/ignored', # existing page + f'http://{address}/invalid', # unknown page + ] app.build() assert (app.outdir / 'output.txt').exists() @@ -288,41 +302,41 @@ def test_anchors_ignored_for_url(app): # the order the threads are processing the links rows = {r['uri']: {'status': r['status'], 'info': r['info']} for r in data} - assert rows['http://localhost:7777/valid']['status'] == 'working' - assert rows['http://localhost:7777/valid#valid-anchor']['status'] == 'working' - assert rows['http://localhost:7777/valid#invalid-anchor'] == { + assert rows[f'http://{address}/valid']['status'] == 'working' + assert rows[f'http://{address}/valid#valid-anchor']['status'] == 'working' + assert rows[f'http://{address}/valid#invalid-anchor'] == { 'status': 'broken', 'info': "Anchor 'invalid-anchor' not found", } - assert rows['http://localhost:7777/ignored']['status'] == 'working' - assert rows['http://localhost:7777/ignored#invalid-anchor']['status'] == 'working' + assert rows[f'http://{address}/ignored']['status'] == 'working' + assert rows[f'http://{address}/ignored#invalid-anchor']['status'] == 'working' - assert rows['http://localhost:7777/invalid'] == { + assert rows[f'http://{address}/invalid'] == { 'status': 'broken', - 'info': '404 Client Error: Not Found for url: http://localhost:7777/invalid', + 'info': f'404 Client Error: Not Found for url: http://{address}/invalid', } - assert rows['http://localhost:7777/invalid#anchor'] == { + assert rows[f'http://{address}/invalid#anchor'] == { 'status': 'broken', - 'info': '404 Client Error: Not Found for url: http://localhost:7777/invalid', + 'info': f'404 Client Error: Not Found for url: http://{address}/invalid', } @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-anchor', freshenv=True) def test_raises_for_invalid_status(app): - class InternalServerErrorHandler(http.server.BaseHTTPRequestHandler): + class InternalServerErrorHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def do_GET(self): self.send_error(500, "Internal Server Error") - with http_server(InternalServerErrorHandler): + with serve_application(app, InternalServerErrorHandler) as address: app.build() content = (app.outdir / 'output.txt').read_text(encoding='utf8') assert content == ( - "index.rst:1: [broken] http://localhost:7777/#anchor: " + f"index.rst:1: [broken] http://{address}/#anchor: " "500 Server Error: Internal Server Error " - "for url: http://localhost:7777/\n" + f"for url: http://{address}/\n" ) @@ -338,13 +352,17 @@ def custom_handler(valid_credentials=(), success_criteria=lambda _: True): expected_token = b64encode(":".join(valid_credentials).encode()).decode("utf-8") del valid_credentials - class CustomHandler(http.server.BaseHTTPRequestHandler): + class CustomHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def authenticated(method): def method_if_authenticated(self): - if (expected_token is None - or self.headers["Authorization"] == f"Basic {expected_token}"): + if expected_token is None: + return method(self) + elif not self.headers["Authorization"]: + self.send_response(401, "Unauthorized") + self.end_headers() + elif self.headers["Authorization"] == f"Basic {expected_token}": return method(self) else: self.send_response(403, "Forbidden") @@ -370,15 +388,14 @@ def custom_handler(valid_credentials=(), success_criteria=lambda _: True): return CustomHandler -@pytest.mark.sphinx( - 'linkcheck', testroot='linkcheck-localserver', freshenv=True, - confoverrides={'linkcheck_auth': [ - (r'^$', ('no', 'match')), - (r'^http://localhost:7777/$', ('user1', 'password')), - (r'.*local.*', ('user2', 'hunter2')), - ]}) +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) def test_auth_header_uses_first_match(app): - with http_server(custom_handler(valid_credentials=("user1", "password"))): + with serve_application(app, custom_handler(valid_credentials=("user1", "password"))) as address: + app.config.linkcheck_auth = [ # type: ignore[attr-defined] + (r'^$', ('no', 'match')), + (fr'^http://{re.escape(address)}/$', ('user1', 'password')), + (r'.*local.*', ('user2', 'hunter2')), + ] app.build() with open(app.outdir / "output.json", encoding="utf-8") as fp: @@ -387,41 +404,51 @@ def test_auth_header_uses_first_match(app): assert content["status"] == "working" +@pytest.mark.filterwarnings('ignore::sphinx.deprecation.RemovedInSphinx80Warning') @pytest.mark.sphinx( 'linkcheck', testroot='linkcheck-localserver', freshenv=True, - confoverrides={'linkcheck_auth': [(r'^$', ('user1', 'password'))]}) -def test_auth_header_no_match(app): - with http_server(custom_handler(valid_credentials=("user1", "password"))): + confoverrides={'linkcheck_allow_unauthorized': False}) +def test_unauthorized_broken(app): + with serve_application(app, custom_handler(valid_credentials=("user1", "password"))): app.build() with open(app.outdir / "output.json", encoding="utf-8") as fp: content = json.load(fp) - # TODO: should this test's webserver return HTTP 401 here? - # https://github.com/sphinx-doc/sphinx/issues/11433 - assert content["info"] == "403 Client Error: Forbidden for url: http://localhost:7777/" + assert content["info"] == "unauthorized" assert content["status"] == "broken" @pytest.mark.sphinx( 'linkcheck', testroot='linkcheck-localserver', freshenv=True, - confoverrides={'linkcheck_request_headers': { - "http://localhost:7777/": { - "Accept": "text/html", - }, - "*": { - "X-Secret": "open sesami", - }, - }}) + confoverrides={'linkcheck_auth': [(r'^$', ('user1', 'password'))]}) +def test_auth_header_no_match(app): + with ( + serve_application(app, custom_handler(valid_credentials=("user1", "password"))), + pytest.warns(RemovedInSphinx80Warning, match='linkcheck builder encountered an HTTP 401'), + ): + app.build() + + with open(app.outdir / "output.json", encoding="utf-8") as fp: + content = json.load(fp) + + # This link is considered working based on the default linkcheck_allow_unauthorized=true + assert content["info"] == "unauthorized" + assert content["status"] == "working" + + +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) def test_linkcheck_request_headers(app): def check_headers(self): if "X-Secret" in self.headers: return False - if self.headers["Accept"] != "text/html": - return False - return True + return self.headers["Accept"] == "text/html" - with http_server(custom_handler(success_criteria=check_headers)): + with serve_application(app, custom_handler(success_criteria=check_headers)) as address: + app.config.linkcheck_request_headers = { # type: ignore[attr-defined] + f"http://{address}/": {"Accept": "text/html"}, + "*": {"X-Secret": "open sesami"}, + } app.build() with open(app.outdir / "output.json", encoding="utf-8") as fp: @@ -430,21 +457,18 @@ def test_linkcheck_request_headers(app): assert content["status"] == "working" -@pytest.mark.sphinx( - 'linkcheck', testroot='linkcheck-localserver', freshenv=True, - confoverrides={'linkcheck_request_headers': { - "http://localhost:7777": {"Accept": "application/json"}, - "*": {"X-Secret": "open sesami"}, - }}) +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) def test_linkcheck_request_headers_no_slash(app): def check_headers(self): if "X-Secret" in self.headers: return False - if self.headers["Accept"] != "application/json": - return False - return True + return self.headers["Accept"] == "application/json" - with http_server(custom_handler(success_criteria=check_headers)): + with serve_application(app, custom_handler(success_criteria=check_headers)) as address: + app.config.linkcheck_request_headers = { # type: ignore[attr-defined] + f"http://{address}": {"Accept": "application/json"}, + "*": {"X-Secret": "open sesami"}, + } app.build() with open(app.outdir / "output.json", encoding="utf-8") as fp: @@ -463,11 +487,9 @@ def test_linkcheck_request_headers_default(app): def check_headers(self): if self.headers["X-Secret"] != "open sesami": return False - if self.headers["Accept"] == "application/json": - return False - return True + return self.headers["Accept"] != "application/json" - with http_server(custom_handler(success_criteria=check_headers)): + with serve_application(app, custom_handler(success_criteria=check_headers)): app.build() with open(app.outdir / "output.json", encoding="utf-8") as fp: @@ -477,7 +499,7 @@ def test_linkcheck_request_headers_default(app): def make_redirect_handler(*, support_head): - class RedirectOnceHandler(http.server.BaseHTTPRequestHandler): + class RedirectOnceHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def do_HEAD(self): @@ -493,7 +515,7 @@ def make_redirect_handler(*, support_head): self.send_response(204, "No content") else: self.send_response(302, "Found") - self.send_header("Location", "http://localhost:7777/?redirected=1") + self.send_header("Location", "/?redirected=1") self.send_header("Content-Length", "0") self.end_headers() @@ -506,13 +528,13 @@ def make_redirect_handler(*, support_head): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) def test_follows_redirects_on_HEAD(app, capsys, warning): - with http_server(make_redirect_handler(support_head=True)): + with serve_application(app, make_redirect_handler(support_head=True)) as address: app.build() stdout, stderr = capsys.readouterr() content = (app.outdir / 'output.txt').read_text(encoding='utf8') assert content == ( "index.rst:1: [redirected with Found] " - "http://localhost:7777/ to http://localhost:7777/?redirected=1\n" + f"http://{address}/ to http://{address}/?redirected=1\n" ) assert stderr == textwrap.dedent( """\ @@ -525,13 +547,13 @@ def test_follows_redirects_on_HEAD(app, capsys, warning): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) def test_follows_redirects_on_GET(app, capsys, warning): - with http_server(make_redirect_handler(support_head=False)): + with serve_application(app, make_redirect_handler(support_head=False)) as address: app.build() stdout, stderr = capsys.readouterr() content = (app.outdir / 'output.txt').read_text(encoding='utf8') assert content == ( "index.rst:1: [redirected with Found] " - "http://localhost:7777/ to http://localhost:7777/?redirected=1\n" + f"http://{address}/ to http://{address}/?redirected=1\n" ) assert stderr == textwrap.dedent( """\ @@ -543,35 +565,34 @@ def test_follows_redirects_on_GET(app, capsys, warning): assert warning.getvalue() == '' -@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-warn-redirects', - freshenv=True, confoverrides={ - 'linkcheck_allowed_redirects': {'http://localhost:7777/.*1': '.*'}, - }) +@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-warn-redirects') def test_linkcheck_allowed_redirects(app, warning): - with http_server(make_redirect_handler(support_head=False)): + with serve_application(app, make_redirect_handler(support_head=False)) as address: + app.config.linkcheck_allowed_redirects = {f'http://{address}/.*1': '.*'} # type: ignore[attr-defined] + compile_linkcheck_allowed_redirects(app, app.config) app.build() with open(app.outdir / 'output.json', encoding='utf-8') as fp: - rows = [json.loads(l) for l in fp.readlines()] + rows = [json.loads(l) for l in fp] assert len(rows) == 2 records = {row["uri"]: row for row in rows} - assert records["http://localhost:7777/path1"]["status"] == "working" - assert records["http://localhost:7777/path2"] == { + assert records[f"http://{address}/path1"]["status"] == "working" + assert records[f"http://{address}/path2"] == { 'filename': 'index.rst', 'lineno': 3, 'status': 'redirected', 'code': 302, - 'uri': 'http://localhost:7777/path2', - 'info': 'http://localhost:7777/?redirected=1', + 'uri': f'http://{address}/path2', + 'info': f'http://{address}/?redirected=1', } - assert ("index.rst:3: WARNING: redirect http://localhost:7777/path2 - with Found to " - "http://localhost:7777/?redirected=1\n" in strip_escseq(warning.getvalue())) + assert (f"index.rst:3: WARNING: redirect http://{address}/path2 - with Found to " + f"http://{address}/?redirected=1\n" in strip_colors(warning.getvalue())) assert len(warning.getvalue().splitlines()) == 1 -class OKHandler(http.server.BaseHTTPRequestHandler): +class OKHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def do_HEAD(self): @@ -591,7 +612,7 @@ class OKHandler(http.server.BaseHTTPRequestHandler): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) def test_invalid_ssl(get_request, app): # Link indicates SSL should be used (https) but the server does not handle it. - with http_server(OKHandler): + with serve_application(app, OKHandler) as address: app.build() assert not get_request.called @@ -600,13 +621,13 @@ def test_invalid_ssl(get_request, app): assert content["status"] == "broken" assert content["filename"] == "index.rst" assert content["lineno"] == 1 - assert content["uri"] == "https://localhost:7777/" + assert content["uri"] == f"https://{address}/" assert "SSLError" in content["info"] @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) def test_connect_to_selfsigned_fails(app): - with https_server(OKHandler): + with serve_application(app, OKHandler, tls_enabled=True) as address: app.build() with open(app.outdir / 'output.json', encoding='utf-8') as fp: @@ -614,14 +635,14 @@ def test_connect_to_selfsigned_fails(app): assert content["status"] == "broken" assert content["filename"] == "index.rst" assert content["lineno"] == 1 - assert content["uri"] == "https://localhost:7777/" + assert content["uri"] == f"https://{address}/" assert "[SSL: CERTIFICATE_VERIFY_FAILED]" in content["info"] @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) def test_connect_to_selfsigned_with_tls_verify_false(app): app.config.tls_verify = False - with https_server(OKHandler): + with serve_application(app, OKHandler, tls_enabled=True) as address: app.build() with open(app.outdir / 'output.json', encoding='utf-8') as fp: @@ -631,7 +652,7 @@ def test_connect_to_selfsigned_with_tls_verify_false(app): "status": "working", "filename": "index.rst", "lineno": 1, - "uri": "https://localhost:7777/", + "uri": f'https://{address}/', "info": "", } @@ -639,7 +660,7 @@ def test_connect_to_selfsigned_with_tls_verify_false(app): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) def test_connect_to_selfsigned_with_tls_cacerts(app): app.config.tls_cacerts = CERT_FILE - with https_server(OKHandler): + with serve_application(app, OKHandler, tls_enabled=True) as address: app.build() with open(app.outdir / 'output.json', encoding='utf-8') as fp: @@ -649,7 +670,7 @@ def test_connect_to_selfsigned_with_tls_cacerts(app): "status": "working", "filename": "index.rst", "lineno": 1, - "uri": "https://localhost:7777/", + "uri": f'https://{address}/', "info": "", } @@ -657,7 +678,7 @@ def test_connect_to_selfsigned_with_tls_cacerts(app): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) def test_connect_to_selfsigned_with_requests_env_var(monkeypatch, app): monkeypatch.setenv("REQUESTS_CA_BUNDLE", CERT_FILE) - with https_server(OKHandler): + with serve_application(app, OKHandler, tls_enabled=True) as address: app.build() with open(app.outdir / 'output.json', encoding='utf-8') as fp: @@ -667,7 +688,7 @@ def test_connect_to_selfsigned_with_requests_env_var(monkeypatch, app): "status": "working", "filename": "index.rst", "lineno": 1, - "uri": "https://localhost:7777/", + "uri": f'https://{address}/', "info": "", } @@ -675,7 +696,7 @@ def test_connect_to_selfsigned_with_requests_env_var(monkeypatch, app): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-https', freshenv=True) def test_connect_to_selfsigned_nonexistent_cert_file(app): app.config.tls_cacerts = "does/not/exist" - with https_server(OKHandler): + with serve_application(app, OKHandler, tls_enabled=True) as address: app.build() with open(app.outdir / 'output.json', encoding='utf-8') as fp: @@ -685,17 +706,17 @@ def test_connect_to_selfsigned_nonexistent_cert_file(app): "status": "broken", "filename": "index.rst", "lineno": 1, - "uri": "https://localhost:7777/", + "uri": f'https://{address}/', "info": "Could not find a suitable TLS CA certificate bundle, invalid path: does/not/exist", } -class InfiniteRedirectOnHeadHandler(http.server.BaseHTTPRequestHandler): +class InfiniteRedirectOnHeadHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def do_HEAD(self): self.send_response(302, "Found") - self.send_header("Location", "http://localhost:7777/") + self.send_header("Location", "/") self.send_header("Content-Length", "0") self.end_headers() @@ -714,7 +735,7 @@ def test_TooManyRedirects_on_HEAD(app, monkeypatch): monkeypatch.setattr(requests.sessions, "DEFAULT_REDIRECT_LIMIT", 5) - with http_server(InfiniteRedirectOnHeadHandler): + with serve_application(app, InfiniteRedirectOnHeadHandler) as address: app.build() with open(app.outdir / 'output.json', encoding='utf-8') as fp: @@ -724,13 +745,13 @@ def test_TooManyRedirects_on_HEAD(app, monkeypatch): "status": "working", "filename": "index.rst", "lineno": 1, - "uri": "http://localhost:7777/", + "uri": f'http://{address}/', "info": "", } def make_retry_after_handler(responses): - class RetryAfterHandler(http.server.BaseHTTPRequestHandler): + class RetryAfterHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def do_HEAD(self): @@ -750,9 +771,11 @@ def make_retry_after_handler(responses): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) def test_too_many_requests_retry_after_int_delay(app, capsys, status): - with http_server(make_retry_after_handler([(429, "0"), (200, None)])), \ - mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0), \ - mock.patch("sphinx.builders.linkcheck.QUEUE_POLL_SECS", 0.01): + with ( + serve_application(app, make_retry_after_handler([(429, "0"), (200, None)])) as address, + mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0), + mock.patch("sphinx.builders.linkcheck.QUEUE_POLL_SECS", 0.01), + ): app.build() content = (app.outdir / 'output.json').read_text(encoding='utf8') assert json.loads(content) == { @@ -760,10 +783,10 @@ def test_too_many_requests_retry_after_int_delay(app, capsys, status): "lineno": 1, "status": "working", "code": 0, - "uri": "http://localhost:7777/", + "uri": f'http://{address}/', "info": "", } - rate_limit_log = "-rate limited- http://localhost:7777/ | sleeping...\n" + rate_limit_log = f"-rate limited- http://{address}/ | sleeping...\n" assert rate_limit_log in strip_colors(status.getvalue()) _stdout, stderr = capsys.readouterr() assert stderr == textwrap.dedent( @@ -787,7 +810,7 @@ def test_too_many_requests_retry_after_HTTP_date(tz, app, monkeypatch, capsys): m.setattr(sphinx.util.http_date, '_GMT_OFFSET', float(time.localtime().tm_gmtoff)) - with http_server(make_retry_after_handler([(429, retry_after), (200, None)])): + with serve_application(app, make_retry_after_handler([(429, retry_after), (200, None)])) as address: app.build() content = (app.outdir / 'output.json').read_text(encoding='utf8') @@ -796,7 +819,7 @@ def test_too_many_requests_retry_after_HTTP_date(tz, app, monkeypatch, capsys): "lineno": 1, "status": "working", "code": 0, - "uri": "http://localhost:7777/", + "uri": f'http://{address}/', "info": "", } _stdout, stderr = capsys.readouterr() @@ -810,8 +833,10 @@ def test_too_many_requests_retry_after_HTTP_date(tz, app, monkeypatch, capsys): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) def test_too_many_requests_retry_after_without_header(app, capsys): - with http_server(make_retry_after_handler([(429, None), (200, None)])), \ - mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0): + with ( + serve_application(app, make_retry_after_handler([(429, None), (200, None)])) as address, + mock.patch("sphinx.builders.linkcheck.DEFAULT_DELAY", 0), + ): app.build() content = (app.outdir / 'output.json').read_text(encoding='utf8') assert json.loads(content) == { @@ -819,7 +844,7 @@ def test_too_many_requests_retry_after_without_header(app, capsys): "lineno": 1, "status": "working", "code": 0, - "uri": "http://localhost:7777/", + "uri": f'http://{address}/', "info": "", } _stdout, stderr = capsys.readouterr() @@ -831,10 +856,36 @@ def test_too_many_requests_retry_after_without_header(app, capsys): ) +@pytest.mark.sphinx( + 'linkcheck', testroot='linkcheck-localserver', freshenv=True, + confoverrides={ + 'linkcheck_report_timeouts_as_broken': False, + 'linkcheck_timeout': 0.01, + } +) +def test_requests_timeout(app): + class DelayedResponseHandler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def do_GET(self): + time.sleep(0.2) # wait before sending any response data + self.send_response(200, "OK") + self.send_header("Content-Length", "0") + self.end_headers() + + with serve_application(app, DelayedResponseHandler): + app.build() + + with open(app.outdir / "output.json", encoding="utf-8") as fp: + content = json.load(fp) + + assert content["status"] == "timeout" + + @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) def test_too_many_requests_user_timeout(app): app.config.linkcheck_rate_limit_timeout = 0.0 - with http_server(make_retry_after_handler([(429, None)])): + with serve_application(app, make_retry_after_handler([(429, None)])) as address: app.build() content = (app.outdir / 'output.json').read_text(encoding='utf8') assert json.loads(content) == { @@ -842,8 +893,8 @@ def test_too_many_requests_user_timeout(app): "lineno": 1, "status": "broken", "code": 0, - "uri": "http://localhost:7777/", - "info": "429 Client Error: Too Many Requests for url: http://localhost:7777/", + "uri": f'http://{address}/', + "info": f"429 Client Error: Too Many Requests for url: http://{address}/", } @@ -901,14 +952,15 @@ def test_connection_contention(get_adapter, app, capsys): import socket socket.setdefaulttimeout(5) - # Place a workload into the linkcheck queue - link_count = 10 - rqueue, wqueue = Queue(), Queue() - for _ in range(link_count): - wqueue.put(CheckRequest(0, Hyperlink("http://localhost:7777", "test", "test.rst", 1))) - # Create parallel consumer threads - with http_server(make_redirect_handler(support_head=True)): + with serve_application(app, make_redirect_handler(support_head=True)) as address: + + # Place a workload into the linkcheck queue + link_count = 10 + rqueue, wqueue = Queue(), Queue() + for _ in range(link_count): + wqueue.put(CheckRequest(0, Hyperlink(f"http://{address}", "test", "test.rst", 1))) + begin, checked = time.time(), [] threads = [ HyperlinkAvailabilityCheckWorker( @@ -932,7 +984,7 @@ def test_connection_contention(get_adapter, app, capsys): assert "TimeoutError" not in stderr -class ConnectionResetHandler(http.server.BaseHTTPRequestHandler): +class ConnectionResetHandler(BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def do_HEAD(self): @@ -946,7 +998,7 @@ class ConnectionResetHandler(http.server.BaseHTTPRequestHandler): @pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True) def test_get_after_head_raises_connection_error(app): - with http_server(ConnectionResetHandler): + with serve_application(app, ConnectionResetHandler) as address: app.build() content = (app.outdir / 'output.txt').read_text(encoding='utf8') assert not content @@ -956,34 +1008,33 @@ def test_get_after_head_raises_connection_error(app): "lineno": 1, "status": "working", "code": 0, - "uri": "http://localhost:7777/", + "uri": f'http://{address}/', "info": "", } @pytest.mark.sphinx('linkcheck', testroot='linkcheck-documents_exclude', freshenv=True) def test_linkcheck_exclude_documents(app): - with http_server(DefaultsHandler): + with serve_application(app, DefaultsHandler): app.build() with open(app.outdir / 'output.json', encoding='utf-8') as fp: content = [json.loads(record) for record in fp] - assert content == [ - { - 'filename': 'broken_link.rst', - 'lineno': 4, - 'status': 'ignored', - 'code': 0, - 'uri': 'https://www.sphinx-doc.org/this-is-a-broken-link', - 'info': 'broken_link matched ^broken_link$ from linkcheck_exclude_documents', - }, - { - 'filename': 'br0ken_link.rst', - 'lineno': 4, - 'status': 'ignored', - 'code': 0, - 'uri': 'https://www.sphinx-doc.org/this-is-another-broken-link', - 'info': 'br0ken_link matched br[0-9]ken_link from linkcheck_exclude_documents', - }, - ] + assert len(content) == 2 + assert { + 'filename': 'broken_link.rst', + 'lineno': 4, + 'status': 'ignored', + 'code': 0, + 'uri': 'https://www.sphinx-doc.org/this-is-a-broken-link', + 'info': 'broken_link matched ^broken_link$ from linkcheck_exclude_documents', + } in content + assert { + 'filename': 'br0ken_link.rst', + 'lineno': 4, + 'status': 'ignored', + 'code': 0, + 'uri': 'https://www.sphinx-doc.org/this-is-another-broken-link', + 'info': 'br0ken_link matched br[0-9]ken_link from linkcheck_exclude_documents', + } in content diff --git a/tests/test_build_manpage.py b/tests/test_builders/test_build_manpage.py index e765644..7172281 100644 --- a/tests/test_build_manpage.py +++ b/tests/test_builders/test_build_manpage.py @@ -9,7 +9,7 @@ from sphinx.config import Config @pytest.mark.sphinx('man') def test_all(app, status, warning): - app.builder.build_all() + app.build(force_all=True) assert (app.outdir / 'sphinxtests.1').exists() content = (app.outdir / 'sphinxtests.1').read_text(encoding='utf8') @@ -34,7 +34,7 @@ def test_all(app, status, warning): @pytest.mark.sphinx('man', testroot='basic', confoverrides={'man_pages': [('index', 'title', None, [], 1)]}) def test_man_pages_empty_description(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'title.1').read_text(encoding='utf8') assert r'title \-' not in content @@ -49,7 +49,7 @@ def test_man_make_section_directory(app, status, warning): @pytest.mark.sphinx('man', testroot='directive-code') def test_captioned_code_block(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'python.1').read_text(encoding='utf8') if docutils.__version_info__[:2] < (0, 21): @@ -92,7 +92,6 @@ def test_default_man_pages(): config = Config({'project': 'STASI™ Documentation', 'author': "Wolfgang Schäuble & G'Beckstein", 'release': '1.0'}) - config.init_values() expected = [('index', 'stasi', 'STASI™ Documentation 1.0', ["Wolfgang Schäuble & G'Beckstein"], 1)] assert default_man_pages(config) == expected diff --git a/tests/test_build_texinfo.py b/tests/test_builders/test_build_texinfo.py index 9964382..f9effb2 100644 --- a/tests/test_build_texinfo.py +++ b/tests/test_builders/test_build_texinfo.py @@ -1,6 +1,5 @@ """Test the build process with Texinfo builder with the test root.""" -import os import re import subprocess from pathlib import Path @@ -11,37 +10,14 @@ import pytest from sphinx.builders.texinfo import default_texinfo_documents from sphinx.config import Config -from sphinx.testing.util import strip_escseq from sphinx.util.docutils import new_document from sphinx.writers.texinfo import TexinfoTranslator -from .test_build_html import ENV_WARNINGS - -TEXINFO_WARNINGS = ENV_WARNINGS + """\ -%(root)s/index.rst:\\d+: WARNING: unknown option: '&option' -%(root)s/index.rst:\\d+: WARNING: citation not found: missing -%(root)s/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: foo.\\* -%(root)s/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: \ -\\['application/pdf', 'image/svg\\+xml'\\] \\(svgimg.\\*\\) -""" - - -@pytest.mark.sphinx('texinfo', testroot='warnings', freshenv=True) -def test_texinfo_warnings(app, status, warning): - app.builder.build_all() - warnings = strip_escseq(re.sub(re.escape(os.sep) + '{1,2}', '/', warning.getvalue())) - warnings_exp = TEXINFO_WARNINGS % { - 'root': re.escape(app.srcdir.as_posix())} - assert re.match(warnings_exp + '$', warnings), \ - "Warnings don't match:\n" + \ - '--- Expected (regex):\n' + warnings_exp + \ - '--- Got:\n' + warnings - @pytest.mark.sphinx('texinfo') def test_texinfo(app, status, warning): TexinfoTranslator.ignore_missing_images = True - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'sphinxtests.texi').read_text(encoding='utf8') assert ('@anchor{markup doc}@anchor{11}' '@anchor{markup id1}@anchor{12}' @@ -71,7 +47,7 @@ def test_texinfo_rubric(app, status, warning): @pytest.mark.sphinx('texinfo', testroot='markup-citation') def test_texinfo_citation(app, status, warning): - app.builder.build_all() + app.build(force_all=True) output = (app.outdir / 'python.texi').read_text(encoding='utf8') assert 'This is a citation ref; @ref{1,,[CITE1]} and @ref{2,,[CITE2]}.' in output @@ -84,7 +60,6 @@ def test_texinfo_citation(app, status, warning): def test_default_texinfo_documents(): config = Config({'project': 'STASI™ Documentation', 'author': "Wolfgang Schäuble & G'Beckstein"}) - config.init_values() expected = [('index', 'stasi', 'STASI™ Documentation', "Wolfgang Schäuble & G'Beckstein", 'stasi', 'One line description of project', 'Miscellaneous')] @@ -110,7 +85,7 @@ def test_texinfo_escape_id(app, status, warning): @pytest.mark.sphinx('texinfo', testroot='footnotes') def test_texinfo_footnote(app, status, warning): - app.builder.build_all() + app.build(force_all=True) output = (app.outdir / 'python.texi').read_text(encoding='utf8') assert 'First footnote: @footnote{\nFirst\n}' in output @@ -118,13 +93,13 @@ def test_texinfo_footnote(app, status, warning): @pytest.mark.sphinx('texinfo') def test_texinfo_xrefs(app, status, warning): - app.builder.build_all() + app.build(force_all=True) output = (app.outdir / 'sphinxtests.texi').read_text(encoding='utf8') assert re.search(r'@ref{\w+,,--plugin\.option}', output) # Now rebuild it without xrefs app.config.texinfo_cross_references = False - app.builder.build_all() + app.build(force_all=True) output = (app.outdir / 'sphinxtests.texi').read_text(encoding='utf8') assert not re.search(r'@ref{\w+,,--plugin\.option}', output) assert 'Link to perl +p, --ObjC++, --plugin.option, create-auth-token, arg and -j' in output diff --git a/tests/test_build_text.py b/tests/test_builders/test_build_text.py index 4a53be3..6dc0d03 100644 --- a/tests/test_build_text.py +++ b/tests/test_builders/test_build_text.py @@ -17,7 +17,7 @@ def with_text_app(*args, **kw): @with_text_app() def test_maxwitdh_with_prefix(app, status, warning): - app.builder.build_update() + app.build() result = (app.outdir / 'maxwidth.txt').read_text(encoding='utf8') lines = result.splitlines() @@ -40,7 +40,7 @@ def test_maxwitdh_with_prefix(app, status, warning): @with_text_app() def test_lineblock(app, status, warning): # regression test for #1109: need empty line after line block - app.builder.build_update() + app.build() result = (app.outdir / 'lineblock.txt').read_text(encoding='utf8') expect = ( "* one\n" @@ -55,7 +55,7 @@ def test_lineblock(app, status, warning): @with_text_app() def test_nonascii_title_line(app, status, warning): - app.builder.build_update() + app.build() result = (app.outdir / 'nonascii_title.txt').read_text(encoding='utf8') expect_underline = '*********' result_underline = result.splitlines()[1].strip() @@ -64,7 +64,7 @@ def test_nonascii_title_line(app, status, warning): @with_text_app() def test_nonascii_table(app, status, warning): - app.builder.build_update() + app.build() result = (app.outdir / 'nonascii_table.txt').read_text(encoding='utf8') lines = [line.strip() for line in result.splitlines() if line.strip()] line_widths = [column_width(line) for line in lines] @@ -73,7 +73,7 @@ def test_nonascii_table(app, status, warning): @with_text_app() def test_nonascii_maxwidth(app, status, warning): - app.builder.build_update() + app.build() result = (app.outdir / 'nonascii_maxwidth.txt').read_text(encoding='utf8') lines = [line.strip() for line in result.splitlines() if line.strip()] line_widths = [column_width(line) for line in lines] @@ -117,7 +117,7 @@ def test_table_cell(): @with_text_app() def test_table_with_empty_cell(app, status, warning): - app.builder.build_update() + app.build() result = (app.outdir / 'table.txt').read_text(encoding='utf8') lines = [line.strip() for line in result.splitlines() if line.strip()] assert lines[0] == "+-------+-------+" @@ -131,7 +131,7 @@ def test_table_with_empty_cell(app, status, warning): @with_text_app() def test_table_with_rowspan(app, status, warning): - app.builder.build_update() + app.build() result = (app.outdir / 'table_rowspan.txt').read_text(encoding='utf8') lines = [line.strip() for line in result.splitlines() if line.strip()] assert lines[0] == "+-------+-------+" @@ -145,7 +145,7 @@ def test_table_with_rowspan(app, status, warning): @with_text_app() def test_table_with_colspan(app, status, warning): - app.builder.build_update() + app.build() result = (app.outdir / 'table_colspan.txt').read_text(encoding='utf8') lines = [line.strip() for line in result.splitlines() if line.strip()] assert lines[0] == "+-------+-------+" @@ -159,7 +159,7 @@ def test_table_with_colspan(app, status, warning): @with_text_app() def test_table_with_colspan_left(app, status, warning): - app.builder.build_update() + app.build() result = (app.outdir / 'table_colspan_left.txt').read_text(encoding='utf8') lines = [line.strip() for line in result.splitlines() if line.strip()] assert lines[0] == "+-------+-------+" @@ -173,7 +173,7 @@ def test_table_with_colspan_left(app, status, warning): @with_text_app() def test_table_with_colspan_and_rowspan(app, status, warning): - app.builder.build_update() + app.build() result = (app.outdir / 'table_colspan_and_rowspan.txt').read_text(encoding='utf8') lines = [line.strip() for line in result.splitlines() if line.strip()] assert result @@ -188,7 +188,7 @@ def test_table_with_colspan_and_rowspan(app, status, warning): @with_text_app() def test_list_items_in_admonition(app, status, warning): - app.builder.build_update() + app.build() result = (app.outdir / 'listitems.txt').read_text(encoding='utf8') lines = [line.rstrip() for line in result.splitlines()] assert lines[0] == "See also:" @@ -200,7 +200,7 @@ def test_list_items_in_admonition(app, status, warning): @with_text_app() def test_secnums(app, status, warning): - app.builder.build_all() + app.build(force_all=True) index = (app.outdir / 'index.txt').read_text(encoding='utf8') lines = index.splitlines() assert lines[0] == "* 1. Section A" @@ -226,7 +226,7 @@ def test_secnums(app, status, warning): assert doc2 == expect app.config.text_secnumber_suffix = " " - app.builder.build_all() + app.build(force_all=True) index = (app.outdir / 'index.txt').read_text(encoding='utf8') lines = index.splitlines() assert lines[0] == "* 1 Section A" @@ -252,7 +252,7 @@ def test_secnums(app, status, warning): assert doc2 == expect app.config.text_add_secnumbers = False - app.builder.build_all() + app.build(force_all=True) index = (app.outdir / 'index.txt').read_text(encoding='utf8') lines = index.splitlines() assert lines[0] == "* Section A" diff --git a/tests/test_builders/test_build_warnings.py b/tests/test_builders/test_build_warnings.py new file mode 100644 index 0000000..eeb7e9d --- /dev/null +++ b/tests/test_builders/test_build_warnings.py @@ -0,0 +1,89 @@ +import os +import re +import sys + +import pytest + +from sphinx.util.console import strip_colors + +ENV_WARNINGS = """\ +{root}/autodoc_fodder.py:docstring of autodoc_fodder.MarkupError:\\d+: \ +WARNING: Explicit markup ends without a blank line; unexpected unindent. +{root}/index.rst:\\d+: WARNING: Encoding 'utf-8-sig' used for reading included \ +file '{root}/wrongenc.inc' seems to be wrong, try giving an :encoding: option +{root}/index.rst:\\d+: WARNING: invalid single index entry '' +{root}/index.rst:\\d+: WARNING: image file not readable: foo.png +{root}/index.rst:\\d+: WARNING: download file not readable: {root}/nonexisting.png +{root}/undecodable.rst:\\d+: WARNING: undecodable source characters, replacing \ +with "\\?": b?'here: >>>(\\\\|/)xbb<<<((\\\\|/)r)?' +""" + +HTML_WARNINGS = ENV_WARNINGS + """\ +{root}/index.rst:\\d+: WARNING: unknown option: '&option' +{root}/index.rst:\\d+: WARNING: citation not found: missing +{root}/index.rst:\\d+: WARNING: a suitable image for html builder not found: foo.\\* +{root}/index.rst:\\d+: WARNING: Lexing literal_block ".*" as "c" resulted in an error at token: ".*". Retrying in relaxed mode. +""" + +LATEX_WARNINGS = ENV_WARNINGS + """\ +{root}/index.rst:\\d+: WARNING: unknown option: '&option' +{root}/index.rst:\\d+: WARNING: citation not found: missing +{root}/index.rst:\\d+: WARNING: a suitable image for latex builder not found: foo.\\* +{root}/index.rst:\\d+: WARNING: Lexing literal_block ".*" as "c" resulted in an error at token: ".*". Retrying in relaxed mode. +""" + +TEXINFO_WARNINGS = ENV_WARNINGS + """\ +{root}/index.rst:\\d+: WARNING: unknown option: '&option' +{root}/index.rst:\\d+: WARNING: citation not found: missing +{root}/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: foo.\\* +{root}/index.rst:\\d+: WARNING: a suitable image for texinfo builder not found: \ +\\['application/pdf', 'image/svg\\+xml'\\] \\(svgimg.\\*\\) +""" + + +def _check_warnings(expected_warnings: str, warning: str) -> None: + warnings = strip_colors(re.sub(re.escape(os.sep) + '{1,2}', '/', warning)) + assert re.match(f'{expected_warnings}$', warnings), ( + "Warnings don't match:\n" + + f'--- Expected (regex):\n{expected_warnings}\n' + + f'--- Got:\n{warnings}' + ) + sys.modules.pop('autodoc_fodder', None) + + +@pytest.mark.sphinx('html', testroot='warnings', freshenv=True) +def test_html_warnings(app, warning): + app.build(force_all=True) + warnings_exp = HTML_WARNINGS.format(root=re.escape(app.srcdir.as_posix())) + _check_warnings(warnings_exp, warning.getvalue()) + + +@pytest.mark.sphinx('latex', testroot='warnings', freshenv=True) +def test_latex_warnings(app, warning): + app.build(force_all=True) + warnings_exp = LATEX_WARNINGS.format(root=re.escape(app.srcdir.as_posix())) + _check_warnings(warnings_exp, warning.getvalue()) + + +@pytest.mark.sphinx('texinfo', testroot='warnings', freshenv=True) +def test_texinfo_warnings(app, warning): + app.build(force_all=True) + warnings_exp = TEXINFO_WARNINGS.format(root=re.escape(app.srcdir.as_posix())) + _check_warnings(warnings_exp, warning.getvalue()) + + +def test_uncacheable_config_warning(make_app, tmp_path): + """Test that an unpickleable config value raises a warning.""" + tmp_path.joinpath('conf.py').write_text("""\ +my_config = lambda: None +show_warning_types = True +def setup(app): + app.add_config_value('my_config', None, 'env') + """, encoding='utf-8') + tmp_path.joinpath('index.rst').write_text('Test\n====\n', encoding='utf-8') + app = make_app(srcdir=tmp_path) + app.build() + assert strip_colors(app.warning.getvalue()).strip() == ( + "WARNING: cannot cache unpickable configuration value: 'my_config' " + "(because it contains a function, class, or module object) [config.cache]" + ) diff --git a/tests/test_builder.py b/tests/test_builders/test_builder.py index 1ff8aea..ee946a5 100644 --- a/tests/test_builder.py +++ b/tests/test_builders/test_builder.py @@ -1,4 +1,7 @@ """Test the Builder class.""" + +import sys + import pytest @@ -37,3 +40,5 @@ def test_incremental_reading_for_missing_files(app): # "index" is listed up to updated because it contains references # to nonexisting downloadable or image files assert set(updated) == {'index'} + + sys.modules.pop('autodoc_fodder', None) diff --git a/tests/test_builders/xpath_data.py b/tests/test_builders/xpath_data.py new file mode 100644 index 0000000..30f8e07 --- /dev/null +++ b/tests/test_builders/xpath_data.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Final + +FIGURE_CAPTION: Final[str] = ".//figure/figcaption/p" diff --git a/tests/test_builders/xpath_util.py b/tests/test_builders/xpath_util.py new file mode 100644 index 0000000..7525c19 --- /dev/null +++ b/tests/test_builders/xpath_util.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import re +import textwrap +from typing import TYPE_CHECKING +from xml.etree.ElementTree import tostring + +if TYPE_CHECKING: + import os + from collections.abc import Callable, Iterable, Sequence + from xml.etree.ElementTree import Element, ElementTree + + +def _get_text(node: Element) -> str: + if node.text is not None: + # the node has only one text + return node.text + + # the node has tags and text; gather texts just under the node + return ''.join(n.tail or '' for n in node) + + +def _prettify(nodes: Iterable[Element]) -> str: + def pformat(node: Element) -> str: + return tostring(node, encoding='unicode', method='html') + + return ''.join(f'(i={index}) {pformat(node)}\n' for index, node in enumerate(nodes)) + + +def check_xpath( + etree: ElementTree, + filename: str | os.PathLike[str], + xpath: str, + check: str | re.Pattern[str] | Callable[[Sequence[Element]], None] | None, + be_found: bool = True, + *, + min_count: int = 1, +) -> None: + """Check that one or more nodes satisfy a predicate. + + :param etree: The element tree. + :param filename: The element tree source name (for errors only). + :param xpath: An XPath expression to use. + :param check: Optional regular expression or a predicate the nodes must validate. + :param be_found: If false, negate the predicate. + :param min_count: Minimum number of nodes expected to satisfy the predicate. + + * If *check* is empty (``''``), only the minimum count is checked. + * If *check* is ``None``, no node should satisfy the XPath expression. + """ + nodes = etree.findall(xpath) + assert isinstance(nodes, list) + + if check is None: + # use == to have a nice pytest diff + assert nodes == [], f'found nodes matching xpath {xpath!r} in file {filename}' + return + + assert len(nodes) >= min_count, (f'expecting at least {min_count} node(s) ' + f'to satisfy {xpath!r} in file {filename}') + + if check == '': + return + + if callable(check): + check(nodes) + return + + rex = re.compile(check) + if be_found: + if any(rex.search(_get_text(node)) for node in nodes): + return + else: + if all(not rex.search(_get_text(node)) for node in nodes): + return + + ctx = textwrap.indent(_prettify(nodes), ' ' * 2) + msg = f'{check!r} not found in any node matching {xpath!r} in file {filename}:\n{ctx}' + raise AssertionError(msg) diff --git a/tests/test_config/__init__.py b/tests/test_config/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_config/__init__.py diff --git a/tests/test_config.py b/tests/test_config/test_config.py index 0be0a58..e1cb1b0 100644 --- a/tests/test_config.py +++ b/tests/test_config/test_config.py @@ -1,15 +1,81 @@ """Test the sphinx.config.Config class.""" +from __future__ import annotations +import pickle import time +from collections import Counter from pathlib import Path +from typing import TYPE_CHECKING, Any from unittest import mock import pytest import sphinx -from sphinx.config import ENUM, Config, check_confval_types +from sphinx.builders.gettext import _gettext_compact_validator +from sphinx.config import ( + ENUM, + Config, + _Opt, + check_confval_types, + correct_copyright_year, + is_serializable, +) +from sphinx.deprecation import RemovedInSphinx90Warning from sphinx.errors import ConfigError, ExtensionError, VersionRequirementError +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Union + + CircularList = list[Union[int, 'CircularList']] + CircularDict = dict[str, Union[int, 'CircularDict']] + + +def check_is_serializable(subject: object, *, circular: bool) -> None: + assert is_serializable(subject) + + if circular: + class UselessGuard(frozenset[int]): + def __or__(self, other: object, /) -> UselessGuard: + # do nothing + return self + + def union(self, *args: Iterable[object]) -> UselessGuard: + # do nothing + return self + + # check that without recursive guards, a recursion error occurs + with pytest.raises(RecursionError): + assert is_serializable(subject, _seen=UselessGuard()) + + +def test_is_serializable() -> None: + subject = [1, [2, {3, 'a'}], {'x': {'y': frozenset((4, 5))}}] + check_is_serializable(subject, circular=False) + + a, b = [1], [2] # type: (CircularList, CircularList) + a.append(b) + b.append(a) + check_is_serializable(a, circular=True) + check_is_serializable(b, circular=True) + + x: CircularDict = {'a': 1, 'b': {'c': 1}} + x['b'] = x + check_is_serializable(x, circular=True) + + +def test_config_opt_deprecated(recwarn): + opt = _Opt('default', '', ()) + + with pytest.warns(RemovedInSphinx90Warning): + default, rebuild, valid_types = opt + + with pytest.warns(RemovedInSphinx90Warning): + _ = opt[0] + + with pytest.warns(RemovedInSphinx90Warning): + _ = list(opt) + @pytest.mark.sphinx(testroot='config', confoverrides={ 'root_doc': 'root', @@ -30,7 +96,7 @@ def test_core_config(app, status, warning): assert cfg.modindex_common_prefix == ['path1', 'path2'] # simple default values - assert 'locale_dirs' not in cfg.__dict__ + assert 'locale_dirs' in cfg.__dict__ assert cfg.locale_dirs == ['locales'] assert cfg.trim_footnote_reference_space is False @@ -71,6 +137,161 @@ def test_config_not_found(tmp_path): Config.read(tmp_path) +@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL))) +def test_config_pickle_protocol(tmp_path, protocol: int): + config = Config() + + pickled_config = pickle.loads(pickle.dumps(config, protocol)) + + assert list(config._options) == list(pickled_config._options) + assert repr(config) == repr(pickled_config) + + +def test_config_pickle_circular_reference_in_list(): + a, b = [1], [2] # type: (CircularList, CircularList) + a.append(b) + b.append(a) + + check_is_serializable(a, circular=True) + check_is_serializable(b, circular=True) + + config = Config() + config.add('a', [], '', types=list) + config.add('b', [], '', types=list) + config.a, config.b = a, b + + actual = pickle.loads(pickle.dumps(config)) + assert isinstance(actual.a, list) + check_is_serializable(actual.a, circular=True) + + assert isinstance(actual.b, list) + check_is_serializable(actual.b, circular=True) + + assert actual.a[0] == 1 + assert actual.a[1][0] == 2 + assert actual.a[1][1][0] == 1 + assert actual.a[1][1][1][0] == 2 + + assert actual.b[0] == 2 + assert actual.b[1][0] == 1 + assert actual.b[1][1][0] == 2 + assert actual.b[1][1][1][0] == 1 + + assert len(actual.a) == 2 + assert len(actual.a[1]) == 2 + assert len(actual.a[1][1]) == 2 + assert len(actual.a[1][1][1]) == 2 + assert len(actual.a[1][1][1][1]) == 2 + + assert len(actual.b) == 2 + assert len(actual.b[1]) == 2 + assert len(actual.b[1][1]) == 2 + assert len(actual.b[1][1][1]) == 2 + assert len(actual.b[1][1][1][1]) == 2 + + def check( + u: list[list[object] | int], + v: list[list[object] | int], + *, + counter: Counter[type, int] | None = None, + guard: frozenset[int] = frozenset(), + ) -> Counter[type, int]: + counter = Counter() if counter is None else counter + + if id(u) in guard and id(v) in guard: + return counter + + if isinstance(u, int): + assert v.__class__ is u.__class__ + assert u == v + counter[type(u)] += 1 + return counter + + assert isinstance(u, list) + assert v.__class__ is u.__class__ + assert len(u) == len(v) + + for u_i, v_i in zip(u, v): + counter[type(u)] += 1 + check(u_i, v_i, counter=counter, guard=guard | {id(u), id(v)}) + + return counter + + counter = check(actual.a, a) + # check(actual.a, a) + # check(actual.a[0], a[0]) -> ++counter[dict] + # ++counter[int] (a[0] is an int) + # check(actual.a[1], a[1]) -> ++counter[dict] + # check(actual.a[1][0], a[1][0]) -> ++counter[dict] + # ++counter[int] (a[1][0] is an int) + # check(actual.a[1][1], a[1][1]) -> ++counter[dict] + # recursive guard since a[1][1] == a + assert counter[type(a[0])] == 2 + assert counter[type(a[1])] == 4 + + # same logic as above + counter = check(actual.b, b) + assert counter[type(b[0])] == 2 + assert counter[type(b[1])] == 4 + + +def test_config_pickle_circular_reference_in_dict(): + x: CircularDict = {'a': 1, 'b': {'c': 1}} + x['b'] = x + check_is_serializable(x, circular=True) + + config = Config() + config.add('x', [], '', types=dict) + config.x = x + + actual = pickle.loads(pickle.dumps(config)) + check_is_serializable(actual.x, circular=True) + assert isinstance(actual.x, dict) + + assert actual.x['a'] == 1 + assert actual.x['b']['a'] == 1 + + assert len(actual.x) == 2 + assert len(actual.x['b']) == 2 + assert len(actual.x['b']['b']) == 2 + + def check( + u: dict[str, dict[str, object] | int], + v: dict[str, dict[str, object] | int], + *, + counter: Counter[type, int] | None = None, + guard: frozenset[int] = frozenset(), + ) -> Counter: + counter = Counter() if counter is None else counter + + if id(u) in guard and id(v) in guard: + return counter + + if isinstance(u, int): + assert v.__class__ is u.__class__ + assert u == v + counter[type(u)] += 1 + return counter + + assert isinstance(u, dict) + assert v.__class__ is u.__class__ + assert len(u) == len(v) + + for u_i, v_i in zip(u, v): + counter[type(u)] += 1 + check(u[u_i], v[v_i], counter=counter, guard=guard | {id(u), id(v)}) + return counter + + counters = check(actual.x, x, counter=Counter()) + # check(actual.x, x) + # check(actual.x['a'], x['a']) -> ++counter[dict] + # ++counter[int] (x['a'] is an int) + # check(actual.x['b'], x['b']) -> ++counter[dict] + # recursive guard since x['b'] == x + assert counters[type(x['a'])] == 1 + assert counters[type(x['b'])] == 2 + + def test_extension_values(): config = Config() @@ -104,7 +325,6 @@ def test_overrides(): config.add('value6', {'default': 0}, 'env', ()) config.add('value7', None, 'env', ()) config.add('value8', [], 'env', ()) - config.init_values() assert config.value1 == '1' assert config.value2 == 999 @@ -123,7 +343,6 @@ def test_overrides_boolean(): config.add('value1', None, 'env', [bool]) config.add('value2', None, 'env', [bool]) config.add('value3', True, 'env', ()) - config.init_values() assert config.value1 is True assert config.value2 is False @@ -131,6 +350,37 @@ def test_overrides_boolean(): @mock.patch("sphinx.config.logger") +def test_overrides_dict_str(logger): + config = Config({}, {'spam': 'lobster'}) + + config.add('spam', {'ham': 'eggs'}, 'env', {dict, str}) + + assert config.spam == {'ham': 'eggs'} + + # assert len(caplog.records) == 1 + # msg = caplog.messages[0] + assert logger.method_calls + msg = str(logger.method_calls[0].args[1]) + assert msg == ("cannot override dictionary config setting 'spam', " + "ignoring (use 'spam.key=value' to set individual elements)") + + +def test_callable_defer(): + config = Config() + config.add('alias', lambda c: c.master_doc, '', str) + + assert config.master_doc == 'index' + assert config.alias == 'index' + + config.master_doc = 'contents' + assert config.alias == 'contents' + + config.master_doc = 'master_doc' + config.alias = 'spam' + assert config.alias == 'spam' + + +@mock.patch("sphinx.config.logger") def test_errors_warnings(logger, tmp_path): # test the error for syntax errors in the config file (tmp_path / 'conf.py').write_text('project = \n', encoding='ascii') @@ -141,7 +391,6 @@ def test_errors_warnings(logger, tmp_path): # test the automatic conversion of 2.x only code in configs (tmp_path / 'conf.py').write_text('project = u"Jägermeister"\n', encoding='utf8') cfg = Config.read(tmp_path, {}, None) - cfg.init_values() assert cfg.project == 'Jägermeister' assert logger.called is False @@ -193,7 +442,6 @@ def test_config_eol(logger, tmp_path): for eol in (b'\n', b'\r\n'): configfile.write_bytes(b'project = "spam"' + eol) cfg = Config.read(tmp_path, {}, None) - cfg.init_values() assert cfg.project == 'spam' assert logger.called is False @@ -248,7 +496,6 @@ TYPECHECK_WARNINGS = [ def test_check_types(logger, name, default, annotation, actual, warned): config = Config({name: actual}) config.add(name, default, 'env', annotation or ()) - config.init_values() check_confval_types(None, config) assert logger.warning.called == warned @@ -257,9 +504,9 @@ TYPECHECK_WARNING_MESSAGES = [ ('value1', 'string', [str], ['foo', 'bar'], "The config value `value1' has type `list'; expected `str'."), ('value1', 'string', [str, int], ['foo', 'bar'], - "The config value `value1' has type `list'; expected `str' or `int'."), + "The config value `value1' has type `list'; expected `int' or `str'."), ('value1', 'string', [str, int, tuple], ['foo', 'bar'], - "The config value `value1' has type `list'; expected `str', `int', or `tuple'."), + "The config value `value1' has type `list'; expected `int', `str', or `tuple'."), ] @@ -268,7 +515,6 @@ TYPECHECK_WARNING_MESSAGES = [ def test_conf_warning_message(logger, name, default, annotation, actual, message): config = Config({name: actual}) config.add(name, default, False, annotation or ()) - config.init_values() check_confval_types(None, config) assert logger.warning.called assert logger.warning.call_args[0][0] == message @@ -278,7 +524,6 @@ def test_conf_warning_message(logger, name, default, annotation, actual, message def test_check_enum(logger): config = Config() config.add('value', 'default', False, ENUM('default', 'one', 'two')) - config.init_values() check_confval_types(None, config) logger.warning.assert_not_called() # not warned @@ -287,7 +532,6 @@ def test_check_enum(logger): def test_check_enum_failed(logger): config = Config({'value': 'invalid'}) config.add('value', 'default', False, ENUM('default', 'one', 'two')) - config.init_values() check_confval_types(None, config) assert logger.warning.called @@ -296,7 +540,6 @@ def test_check_enum_failed(logger): def test_check_enum_for_list(logger): config = Config({'value': ['one', 'two']}) config.add('value', 'default', False, ENUM('default', 'one', 'two')) - config.init_values() check_confval_types(None, config) logger.warning.assert_not_called() # not warned @@ -305,11 +548,18 @@ def test_check_enum_for_list(logger): def test_check_enum_for_list_failed(logger): config = Config({'value': ['one', 'two', 'invalid']}) config.add('value', 'default', False, ENUM('default', 'one', 'two')) - config.init_values() check_confval_types(None, config) assert logger.warning.called +@mock.patch("sphinx.config.logger") +def test_check_any(logger): + config = Config({'value': None}) + config.add('value', 'default', '', Any) + check_confval_types(None, config) + logger.warning.assert_not_called() # not warned + + nitpick_warnings = [ "WARNING: py:const reference target not found: prefix.anything.postfix", "WARNING: py:class reference target not found: prefix.anything", @@ -320,7 +570,7 @@ nitpick_warnings = [ @pytest.mark.sphinx(testroot='nitpicky-warnings') def test_nitpick_base(app, status, warning): - app.builder.build_all() + app.build(force_all=True) warning = warning.getvalue().strip().split('\n') assert len(warning) == len(nitpick_warnings) @@ -337,7 +587,7 @@ def test_nitpick_base(app, status, warning): }, }) def test_nitpick_ignore(app, status, warning): - app.builder.build_all() + app.build(force_all=True) assert not len(warning.getvalue().strip()) @@ -348,7 +598,7 @@ def test_nitpick_ignore(app, status, warning): ], }) def test_nitpick_ignore_regex1(app, status, warning): - app.builder.build_all() + app.build(force_all=True) assert not len(warning.getvalue().strip()) @@ -359,7 +609,7 @@ def test_nitpick_ignore_regex1(app, status, warning): ], }) def test_nitpick_ignore_regex2(app, status, warning): - app.builder.build_all() + app.build(force_all=True) assert not len(warning.getvalue().strip()) @@ -376,7 +626,7 @@ def test_nitpick_ignore_regex2(app, status, warning): ], }) def test_nitpick_ignore_regex_fullmatch(app, status, warning): - app.builder.build_all() + app.build(force_all=True) warning = warning.getvalue().strip().split('\n') assert len(warning) == len(nitpick_warnings) @@ -386,13 +636,11 @@ def test_nitpick_ignore_regex_fullmatch(app, status, warning): def test_conf_py_language_none(tmp_path): """Regression test for #10474.""" - # Given a conf.py file with language = None (tmp_path / 'conf.py').write_text("language = None", encoding='utf-8') # When we load conf.py into a Config object cfg = Config.read(tmp_path, {}, None) - cfg.init_values() # Then the language is coerced to English assert cfg.language == "en" @@ -401,7 +649,6 @@ def test_conf_py_language_none(tmp_path): @mock.patch("sphinx.config.logger") def test_conf_py_language_none_warning(logger, tmp_path): """Regression test for #10474.""" - # Given a conf.py file with language = None (tmp_path / 'conf.py').write_text("language = None", encoding='utf-8') @@ -418,13 +665,11 @@ def test_conf_py_language_none_warning(logger, tmp_path): def test_conf_py_no_language(tmp_path): """Regression test for #10474.""" - # Given a conf.py file with no language attribute (tmp_path / 'conf.py').write_text("", encoding='utf-8') # When we load conf.py into a Config object cfg = Config.read(tmp_path, {}, None) - cfg.init_values() # Then the language is coerced to English assert cfg.language == "en" @@ -432,13 +677,11 @@ def test_conf_py_no_language(tmp_path): def test_conf_py_nitpick_ignore_list(tmp_path): """Regression test for #11355.""" - # Given a conf.py file with no language attribute (tmp_path / 'conf.py').write_text("", encoding='utf-8') # When we load conf.py into a Config object cfg = Config.read(tmp_path, {}, None) - cfg.init_values() # Then the default nitpick_ignore[_regex] is an empty list assert cfg.nitpick_ignore == [] @@ -465,7 +708,7 @@ def source_date_year(request, monkeypatch): @pytest.mark.sphinx(testroot='copyright-multiline') def test_multi_line_copyright(source_date_year, app, monkeypatch): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf-8') @@ -515,3 +758,48 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch): f' \n' f' © Copyright 2022-{source_date_year}, Eve.' ) in content + + +@pytest.mark.parametrize(('conf_copyright', 'expected_copyright'), [ + ('1970', '{current_year}'), + # https://github.com/sphinx-doc/sphinx/issues/11913 + ('1970-1990', '1970-{current_year}'), + ('1970-1990 Alice', '1970-{current_year} Alice'), +]) +def test_correct_copyright_year(conf_copyright, expected_copyright, source_date_year): + config = Config({}, {'copyright': conf_copyright}) + correct_copyright_year(_app=None, config=config) + actual_copyright = config['copyright'] + + if source_date_year is None: + expected_copyright = conf_copyright + else: + expected_copyright = expected_copyright.format(current_year=source_date_year) + assert actual_copyright == expected_copyright + + +def test_gettext_compact_command_line_true(): + config = Config({}, {'gettext_compact': '1'}) + config.add('gettext_compact', True, '', {bool, str}) + _gettext_compact_validator(..., config) + + # regression test for #8549 (-D gettext_compact=1) + assert config.gettext_compact is True + + +def test_gettext_compact_command_line_false(): + config = Config({}, {'gettext_compact': '0'}) + config.add('gettext_compact', True, '', {bool, str}) + _gettext_compact_validator(..., config) + + # regression test for #8549 (-D gettext_compact=0) + assert config.gettext_compact is False + + +def test_gettext_compact_command_line_str(): + config = Config({}, {'gettext_compact': 'spam'}) + config.add('gettext_compact', True, '', {bool, str}) + _gettext_compact_validator(..., config) + + # regression test for #8549 (-D gettext_compact=spam) + assert config.gettext_compact == 'spam' diff --git a/tests/test_correct_year.py b/tests/test_config/test_correct_year.py index 4ef77a6..4ef77a6 100644 --- a/tests/test_correct_year.py +++ b/tests/test_config/test_correct_year.py diff --git a/tests/test_directives/__init__.py b/tests/test_directives/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_directives/__init__.py diff --git a/tests/test_directive_code.py b/tests/test_directives/test_directive_code.py index df7de57..2783d8f 100644 --- a/tests/test_directive_code.py +++ b/tests/test_directives/test_directive_code.py @@ -295,7 +295,7 @@ def test_LiteralIncludeReader_diff(testroot, literal_inc_path): @pytest.mark.sphinx('xml', testroot='directive-code') def test_code_block(app, status, warning): - app.builder.build('index') + app.build(filenames=[app.srcdir / 'index.rst']) et = etree_parse(app.outdir / 'index.xml') secs = et.findall('./section/section') code_block = secs[0].findall('literal_block') @@ -311,13 +311,13 @@ def test_code_block(app, status, warning): @pytest.mark.sphinx('html', testroot='directive-code') def test_force_option(app, status, warning): - app.builder.build(['force']) + app.build(filenames=[app.srcdir / 'force.rst']) assert 'force.rst' not in warning.getvalue() @pytest.mark.sphinx('html', testroot='directive-code') def test_code_block_caption_html(app, status, warning): - app.builder.build(['caption']) + app.build(filenames=[app.srcdir / 'caption.rst']) html = (app.outdir / 'caption.html').read_text(encoding='utf8') caption = ('<div class="code-block-caption">' '<span class="caption-number">Listing 1 </span>' @@ -329,7 +329,7 @@ def test_code_block_caption_html(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_code_block_caption_latex(app, status, warning): - app.builder.build_all() + app.build(force_all=True) latex = (app.outdir / 'python.tex').read_text(encoding='utf8') caption = '\\sphinxSetupCaptionForVerbatim{caption \\sphinxstyleemphasis{test} rb}' label = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:id1}}}' @@ -342,7 +342,7 @@ def test_code_block_caption_latex(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_code_block_namedlink_latex(app, status, warning): - app.builder.build_all() + app.build(force_all=True) latex = (app.outdir / 'python.tex').read_text(encoding='utf8') label1 = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:name-test-rb}}}' link1 = '\\hyperref[\\detokenize{caption:name-test-rb}]'\ @@ -359,7 +359,7 @@ def test_code_block_namedlink_latex(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_code_block_emphasize_latex(app, status, warning): - app.builder.build(['emphasize']) + app.build(filenames=[app.srcdir / 'emphasize.rst']) latex = (app.outdir / 'python.tex').read_text(encoding='utf8').replace('\r\n', '\n') includes = '\\fvset{hllines={, 5, 6, 13, 14, 15, 24, 25, 26,}}%\n' assert includes in latex @@ -369,7 +369,7 @@ def test_code_block_emphasize_latex(app, status, warning): @pytest.mark.sphinx('xml', testroot='directive-code') def test_literal_include(app, status, warning): - app.builder.build(['index']) + app.build(filenames=[app.srcdir / 'index.rst']) et = etree_parse(app.outdir / 'index.xml') secs = et.findall('./section/section') literal_include = secs[1].findall('literal_block') @@ -381,7 +381,7 @@ def test_literal_include(app, status, warning): @pytest.mark.sphinx('xml', testroot='directive-code') def test_literal_include_block_start_with_comment_or_brank(app, status, warning): - app.builder.build(['python']) + app.build(filenames=[app.srcdir / 'python.rst']) et = etree_parse(app.outdir / 'python.xml') secs = et.findall('./section/section') literal_include = secs[0].findall('literal_block') @@ -405,7 +405,7 @@ def test_literal_include_block_start_with_comment_or_brank(app, status, warning) @pytest.mark.sphinx('html', testroot='directive-code') def test_literal_include_linenos(app, status, warning): - app.builder.build(['linenos']) + app.build(filenames=[app.srcdir / 'linenos.rst']) html = (app.outdir / 'linenos.html').read_text(encoding='utf8') # :linenos: @@ -423,7 +423,7 @@ def test_literal_include_linenos(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_literalinclude_file_whole_of_emptyline(app, status, warning): - app.builder.build_all() + app.build(force_all=True) latex = (app.outdir / 'python.tex').read_text(encoding='utf8').replace('\r\n', '\n') includes = ( '\\begin{sphinxVerbatim}' @@ -437,7 +437,7 @@ def test_literalinclude_file_whole_of_emptyline(app, status, warning): @pytest.mark.sphinx('html', testroot='directive-code') def test_literalinclude_caption_html(app, status, warning): - app.builder.build_all() + app.build(force_all=True) html = (app.outdir / 'caption.html').read_text(encoding='utf8') caption = ('<div class="code-block-caption">' '<span class="caption-number">Listing 2 </span>' @@ -449,7 +449,7 @@ def test_literalinclude_caption_html(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_literalinclude_caption_latex(app, status, warning): - app.builder.build('index') + app.build(filenames='index') latex = (app.outdir / 'python.tex').read_text(encoding='utf8') caption = '\\sphinxSetupCaptionForVerbatim{caption \\sphinxstylestrong{test} py}' label = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:id2}}}' @@ -462,7 +462,7 @@ def test_literalinclude_caption_latex(app, status, warning): @pytest.mark.sphinx('latex', testroot='directive-code') def test_literalinclude_namedlink_latex(app, status, warning): - app.builder.build('index') + app.build(filenames='index') latex = (app.outdir / 'python.tex').read_text(encoding='utf8') label1 = '\\def\\sphinxLiteralBlockLabel{\\label{\\detokenize{caption:name-test-py}}}' link1 = '\\hyperref[\\detokenize{caption:name-test-py}]'\ @@ -479,7 +479,7 @@ def test_literalinclude_namedlink_latex(app, status, warning): @pytest.mark.sphinx('xml', testroot='directive-code') def test_literalinclude_classes(app, status, warning): - app.builder.build(['classes']) + app.build(filenames=[app.srcdir / 'classes.rst']) et = etree_parse(app.outdir / 'classes.xml') secs = et.findall('./section/section') @@ -496,7 +496,7 @@ def test_literalinclude_classes(app, status, warning): @pytest.mark.sphinx('xml', testroot='directive-code') def test_literalinclude_pydecorators(app, status, warning): - app.builder.build(['py-decorators']) + app.build(filenames=[app.srcdir / 'py-decorators.rst']) et = etree_parse(app.outdir / 'py-decorators.xml') secs = et.findall('./section/section') @@ -537,7 +537,7 @@ def test_literalinclude_pydecorators(app, status, warning): @pytest.mark.sphinx('dummy', testroot='directive-code') def test_code_block_highlighted(app, status, warning): - app.builder.build(['highlight']) + app.build(filenames=[app.srcdir / 'highlight.rst']) doctree = app.env.get_doctree('highlight') codeblocks = list(doctree.findall(nodes.literal_block)) @@ -549,7 +549,7 @@ def test_code_block_highlighted(app, status, warning): @pytest.mark.sphinx('html', testroot='directive-code') def test_linenothreshold(app, status, warning): - app.builder.build(['linenothreshold']) + app.build(filenames=[app.srcdir / 'linenothreshold.rst']) html = (app.outdir / 'linenothreshold.html').read_text(encoding='utf8') # code-block using linenothreshold @@ -570,7 +570,7 @@ def test_linenothreshold(app, status, warning): @pytest.mark.sphinx('dummy', testroot='directive-code') def test_code_block_dedent(app, status, warning): - app.builder.build(['dedent']) + app.build(filenames=[app.srcdir / 'dedent.rst']) doctree = app.env.get_doctree('dedent') codeblocks = list(doctree.findall(nodes.literal_block)) # Note: comparison string should not have newlines at the beginning or end diff --git a/tests/test_directive_object_description.py b/tests/test_directives/test_directive_object_description.py index f2c9f9d..f2c9f9d 100644 --- a/tests/test_directive_object_description.py +++ b/tests/test_directives/test_directive_object_description.py diff --git a/tests/test_directive_only.py b/tests/test_directives/test_directive_only.py index 2e9ea63..bf03c7b 100644 --- a/tests/test_directive_only.py +++ b/tests/test_directives/test_directive_only.py @@ -34,7 +34,7 @@ def test_sectioning(app, status, warning): 'Unnumbered section: %r' % subsect[0] testsects(prefix + str(i + 1) + '.', subsect, indent + 4) - app.builder.build(['only']) + app.build(filenames=[app.srcdir / 'only.rst']) doctree = app.env.get_doctree('only') app.env.apply_post_transforms(doctree, 'only') @@ -43,4 +43,4 @@ def test_sectioning(app, status, warning): for i, s in enumerate(parts): testsects(str(i + 1) + '.', s, 4) assert len(parts) == 4, 'Expected 4 document level headings, got:\n%s' % \ - '\n'.join([p[0] for p in parts]) + '\n'.join(p[0] for p in parts) diff --git a/tests/test_directives/test_directive_option.py b/tests/test_directives/test_directive_option.py new file mode 100644 index 0000000..76448cd --- /dev/null +++ b/tests/test_directives/test_directive_option.py @@ -0,0 +1,40 @@ +import pytest + + +@pytest.mark.sphinx('html', testroot='root', + confoverrides={'option_emphasise_placeholders': True}) +def test_option_emphasise_placeholders(app, status, warning): + app.build() + content = (app.outdir / 'objects.html').read_text(encoding='utf8') + assert '<em><span class="pre">TYPE</span></em>' in content + assert '{TYPE}' not in content + assert ('<em><span class="pre">WHERE</span></em>' + '<span class="pre">-</span>' + '<em><span class="pre">COUNT</span></em>' in content) + assert '<span class="pre">{{value}}</span>' in content + assert ('<span class="pre">--plugin.option</span></span>' + '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Link to this definition">¶</a></dt>') in content + + +@pytest.mark.sphinx('html', testroot='root') +def test_option_emphasise_placeholders_default(app, status, warning): + app.build() + content = (app.outdir / 'objects.html').read_text(encoding='utf8') + assert '<span class="pre">={TYPE}</span>' in content + assert '<span class="pre">={WHERE}-{COUNT}</span></span>' in content + assert '<span class="pre">{client_name}</span>' in content + assert ('<span class="pre">--plugin.option</span></span>' + '<span class="sig-prename descclassname"></span>' + '<a class="headerlink" href="#cmdoption-perl-plugin.option" title="Link to this definition">¶</a></dt>') in content + + +@pytest.mark.sphinx('html', testroot='root') +def test_option_reference_with_value(app, status, warning): + app.build() + content = (app.outdir / 'objects.html').read_text(encoding='utf-8') + assert ('<span class="pre">-mapi</span></span><span class="sig-prename descclassname">' + '</span><a class="headerlink" href="#cmdoption-git-commit-mapi"') in content + assert 'first option <a class="reference internal" href="#cmdoption-git-commit-mapi">' in content + assert ('<a class="reference internal" href="#cmdoption-git-commit-mapi">' + '<code class="xref std std-option docutils literal notranslate"><span class="pre">-mapi[=xxx]</span></code></a>') in content + assert '<span class="pre">-mapi</span> <span class="pre">with_space</span>' in content diff --git a/tests/test_directive_other.py b/tests/test_directives/test_directive_other.py index 1feb251..1feb251 100644 --- a/tests/test_directive_other.py +++ b/tests/test_directives/test_directive_other.py diff --git a/tests/test_directive_patch.py b/tests/test_directives/test_directive_patch.py index f4eb8f9..f4eb8f9 100644 --- a/tests/test_directive_patch.py +++ b/tests/test_directives/test_directive_patch.py diff --git a/tests/test_directives_no_typesetting.py b/tests/test_directives/test_directives_no_typesetting.py index fd101fb..fd101fb 100644 --- a/tests/test_directives_no_typesetting.py +++ b/tests/test_directives/test_directives_no_typesetting.py diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py deleted file mode 100644 index c5a044b..0000000 --- a/tests/test_domain_py.py +++ /dev/null @@ -1,2123 +0,0 @@ -"""Tests the Python Domain""" - -from __future__ import annotations - -import re -from unittest.mock import Mock - -import docutils.utils -import pytest -from docutils import nodes - -from sphinx import addnodes -from sphinx.addnodes import ( - desc, - desc_addname, - desc_annotation, - desc_content, - desc_name, - desc_optional, - desc_parameter, - desc_parameterlist, - desc_returns, - desc_sig_keyword, - desc_sig_literal_number, - desc_sig_literal_string, - desc_sig_name, - desc_sig_operator, - desc_sig_punctuation, - desc_sig_space, - desc_signature, - desc_type_parameter, - desc_type_parameter_list, - pending_xref, -) -from sphinx.domains import IndexEntry -from sphinx.domains.python import ( - PythonDomain, - PythonModuleIndex, - _parse_annotation, - _pseudo_parse_arglist, - py_sig_re, -) -from sphinx.testing import restructuredtext -from sphinx.testing.util import assert_node -from sphinx.writers.text import STDINDENT - - -def parse(sig): - m = py_sig_re.match(sig) - if m is None: - raise ValueError - name_prefix, tp_list, name, arglist, retann = m.groups() - signode = addnodes.desc_signature(sig, '') - _pseudo_parse_arglist(signode, arglist) - return signode.astext() - - -def test_function_signatures(): - rv = parse('func(a=1) -> int object') - assert rv == '(a=1)' - - rv = parse('func(a=1, [b=None])') - assert rv == '(a=1, [b=None])' - - rv = parse('func(a=1[, b=None])') - assert rv == '(a=1, [b=None])' - - rv = parse("compile(source : string, filename, symbol='file')") - assert rv == "(source : string, filename, symbol='file')" - - rv = parse('func(a=[], [b=None])') - assert rv == '(a=[], [b=None])' - - rv = parse('func(a=[][, b=None])') - assert rv == '(a=[], [b=None])' - - -@pytest.mark.sphinx('dummy', testroot='domain-py') -def test_domain_py_xrefs(app, status, warning): - """Domain objects have correct prefixes when looking up xrefs""" - app.builder.build_all() - - def assert_refnode(node, module_name, class_name, target, reftype=None, - domain='py'): - attributes = { - 'refdomain': domain, - 'reftarget': target, - } - if reftype is not None: - attributes['reftype'] = reftype - if module_name is not False: - attributes['py:module'] = module_name - if class_name is not False: - attributes['py:class'] = class_name - assert_node(node, **attributes) - - doctree = app.env.get_doctree('roles') - refnodes = list(doctree.findall(pending_xref)) - assert_refnode(refnodes[0], None, None, 'TopLevel', 'class') - assert_refnode(refnodes[1], None, None, 'top_level', 'meth') - assert_refnode(refnodes[2], None, 'NestedParentA', 'child_1', 'meth') - assert_refnode(refnodes[3], None, 'NestedParentA', 'NestedChildA.subchild_2', 'meth') - assert_refnode(refnodes[4], None, 'NestedParentA', 'child_2', 'meth') - assert_refnode(refnodes[5], False, 'NestedParentA', 'any_child', domain='') - assert_refnode(refnodes[6], None, 'NestedParentA', 'NestedChildA', 'class') - assert_refnode(refnodes[7], None, 'NestedParentA.NestedChildA', 'subchild_2', 'meth') - assert_refnode(refnodes[8], None, 'NestedParentA.NestedChildA', - 'NestedParentA.child_1', 'meth') - assert_refnode(refnodes[9], None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') - assert_refnode(refnodes[10], None, 'NestedParentB', 'child_1', 'meth') - assert_refnode(refnodes[11], None, 'NestedParentB', 'NestedParentB', 'class') - assert_refnode(refnodes[12], None, None, 'NestedParentA.NestedChildA', 'class') - assert len(refnodes) == 13 - - doctree = app.env.get_doctree('module') - refnodes = list(doctree.findall(pending_xref)) - assert_refnode(refnodes[0], 'module_a.submodule', None, - 'ModTopLevel', 'class') - assert_refnode(refnodes[1], 'module_a.submodule', 'ModTopLevel', - 'mod_child_1', 'meth') - assert_refnode(refnodes[2], 'module_a.submodule', 'ModTopLevel', - 'ModTopLevel.mod_child_1', 'meth') - assert_refnode(refnodes[3], 'module_a.submodule', 'ModTopLevel', - 'mod_child_2', 'meth') - assert_refnode(refnodes[4], 'module_a.submodule', 'ModTopLevel', - 'module_a.submodule.ModTopLevel.mod_child_1', 'meth') - assert_refnode(refnodes[5], 'module_a.submodule', 'ModTopLevel', - 'prop', 'attr') - assert_refnode(refnodes[6], 'module_a.submodule', 'ModTopLevel', - 'prop', 'meth') - assert_refnode(refnodes[7], 'module_b.submodule', None, - 'ModTopLevel', 'class') - assert_refnode(refnodes[8], 'module_b.submodule', 'ModTopLevel', - 'ModNoModule', 'class') - assert_refnode(refnodes[9], False, False, 'int', 'class') - assert_refnode(refnodes[10], False, False, 'tuple', 'class') - assert_refnode(refnodes[11], False, False, 'str', 'class') - assert_refnode(refnodes[12], False, False, 'float', 'class') - assert_refnode(refnodes[13], False, False, 'list', 'class') - assert_refnode(refnodes[14], False, False, 'ModTopLevel', 'class') - assert_refnode(refnodes[15], False, False, 'index', 'doc', domain='std') - assert len(refnodes) == 16 - - doctree = app.env.get_doctree('module_option') - refnodes = list(doctree.findall(pending_xref)) - print(refnodes) - print(refnodes[0]) - print(refnodes[1]) - assert_refnode(refnodes[0], 'test.extra', 'B', 'foo', 'meth') - assert_refnode(refnodes[1], 'test.extra', 'B', 'foo', 'meth') - assert len(refnodes) == 2 - - -@pytest.mark.sphinx('html', testroot='domain-py') -def test_domain_py_xrefs_abbreviations(app, status, warning): - app.builder.build_all() - - content = (app.outdir / 'abbr.html').read_text(encoding='utf8') - assert re.search(r'normal: <a .* href="module.html#module_a.submodule.ModTopLevel.' - r'mod_child_1" .*><.*>module_a.submodule.ModTopLevel.mod_child_1\(\)' - r'<.*></a>', - content) - assert re.search(r'relative: <a .* href="module.html#module_a.submodule.ModTopLevel.' - r'mod_child_1" .*><.*>ModTopLevel.mod_child_1\(\)<.*></a>', - content) - assert re.search(r'short name: <a .* href="module.html#module_a.submodule.ModTopLevel.' - r'mod_child_1" .*><.*>mod_child_1\(\)<.*></a>', - content) - assert re.search(r'relative \+ short name: <a .* href="module.html#module_a.submodule.' - r'ModTopLevel.mod_child_1" .*><.*>mod_child_1\(\)<.*></a>', - content) - assert re.search(r'short name \+ relative: <a .* href="module.html#module_a.submodule.' - r'ModTopLevel.mod_child_1" .*><.*>mod_child_1\(\)<.*></a>', - content) - - -@pytest.mark.sphinx('dummy', testroot='domain-py') -def test_domain_py_objects(app, status, warning): - app.builder.build_all() - - modules = app.env.domains['py'].data['modules'] - objects = app.env.domains['py'].data['objects'] - - assert 'module_a.submodule' in modules - assert 'module_a.submodule' in objects - assert 'module_b.submodule' in modules - assert 'module_b.submodule' in objects - - assert objects['module_a.submodule.ModTopLevel'][2] == 'class' - assert objects['module_a.submodule.ModTopLevel.mod_child_1'][2] == 'method' - assert objects['module_a.submodule.ModTopLevel.mod_child_2'][2] == 'method' - assert 'ModTopLevel.ModNoModule' not in objects - assert objects['ModNoModule'][2] == 'class' - assert objects['module_b.submodule.ModTopLevel'][2] == 'class' - - assert objects['TopLevel'][2] == 'class' - assert objects['top_level'][2] == 'method' - assert objects['NestedParentA'][2] == 'class' - assert objects['NestedParentA.child_1'][2] == 'method' - assert objects['NestedParentA.any_child'][2] == 'method' - assert objects['NestedParentA.NestedChildA'][2] == 'class' - assert objects['NestedParentA.NestedChildA.subchild_1'][2] == 'method' - assert objects['NestedParentA.NestedChildA.subchild_2'][2] == 'method' - assert objects['NestedParentA.child_2'][2] == 'method' - assert objects['NestedParentB'][2] == 'class' - assert objects['NestedParentB.child_1'][2] == 'method' - - -@pytest.mark.sphinx('html', testroot='domain-py') -def test_resolve_xref_for_properties(app, status, warning): - app.builder.build_all() - - content = (app.outdir / 'module.html').read_text(encoding='utf8') - assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"' - ' title="module_a.submodule.ModTopLevel.prop">' - '<code class="xref py py-attr docutils literal notranslate"><span class="pre">' - 'prop</span> <span class="pre">attribute</span></code></a>' in content) - assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"' - ' title="module_a.submodule.ModTopLevel.prop">' - '<code class="xref py py-meth docutils literal notranslate"><span class="pre">' - 'prop</span> <span class="pre">method</span></code></a>' in content) - assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"' - ' title="module_a.submodule.ModTopLevel.prop">' - '<code class="xref py py-attr docutils literal notranslate"><span class="pre">' - 'prop</span> <span class="pre">attribute</span></code></a>' in content) - - -@pytest.mark.sphinx('dummy', testroot='domain-py') -def test_domain_py_find_obj(app, status, warning): - - def find_obj(modname, prefix, obj_name, obj_type, searchmode=0): - return app.env.domains['py'].find_obj( - app.env, modname, prefix, obj_name, obj_type, searchmode) - - app.builder.build_all() - - assert (find_obj(None, None, 'NONEXISTANT', 'class') == []) - assert (find_obj(None, None, 'NestedParentA', 'class') == - [('NestedParentA', ('roles', 'NestedParentA', 'class', False))]) - assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') == - [('NestedParentA.NestedChildA', - ('roles', 'NestedParentA.NestedChildA', 'class', False))]) - assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') == - [('NestedParentA.NestedChildA', - ('roles', 'NestedParentA.NestedChildA', 'class', False))]) - assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'meth') == - [('NestedParentA.NestedChildA.subchild_1', - ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))]) - assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') == - [('NestedParentA.NestedChildA.subchild_1', - ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))]) - assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'meth') == - [('NestedParentA.NestedChildA.subchild_1', - ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))]) - - -@pytest.mark.sphinx('html', testroot='domain-py', freshenv=True) -def test_domain_py_canonical(app, status, warning): - app.builder.build_all() - - content = (app.outdir / 'canonical.html').read_text(encoding='utf8') - assert ('<a class="reference internal" href="#canonical.Foo" title="canonical.Foo">' - '<code class="xref py py-class docutils literal notranslate">' - '<span class="pre">Foo</span></code></a>' in content) - assert warning.getvalue() == '' - - -def test_get_full_qualified_name(): - env = Mock(domaindata={}) - domain = PythonDomain(env) - - # non-python references - node = nodes.reference() - assert domain.get_full_qualified_name(node) is None - - # simple reference - node = nodes.reference(reftarget='func') - assert domain.get_full_qualified_name(node) == 'func' - - # with py:module context - kwargs = {'py:module': 'module1'} - node = nodes.reference(reftarget='func', **kwargs) - assert domain.get_full_qualified_name(node) == 'module1.func' - - # with py:class context - kwargs = {'py:class': 'Class'} - node = nodes.reference(reftarget='func', **kwargs) - assert domain.get_full_qualified_name(node) == 'Class.func' - - # with both py:module and py:class context - kwargs = {'py:module': 'module1', 'py:class': 'Class'} - node = nodes.reference(reftarget='func', **kwargs) - assert domain.get_full_qualified_name(node) == 'module1.Class.func' - - -def test_parse_annotation(app): - doctree = _parse_annotation("int", app.env) - assert_node(doctree, ([pending_xref, "int"],)) - assert_node(doctree[0], pending_xref, refdomain="py", reftype="class", reftarget="int") - - doctree = _parse_annotation("List[int]", app.env) - assert_node(doctree, ([pending_xref, "List"], - [desc_sig_punctuation, "["], - [pending_xref, "int"], - [desc_sig_punctuation, "]"])) - - doctree = _parse_annotation("Tuple[int, int]", app.env) - assert_node(doctree, ([pending_xref, "Tuple"], - [desc_sig_punctuation, "["], - [pending_xref, "int"], - [desc_sig_punctuation, ","], - desc_sig_space, - [pending_xref, "int"], - [desc_sig_punctuation, "]"])) - - doctree = _parse_annotation("Tuple[()]", app.env) - assert_node(doctree, ([pending_xref, "Tuple"], - [desc_sig_punctuation, "["], - [desc_sig_punctuation, "("], - [desc_sig_punctuation, ")"], - [desc_sig_punctuation, "]"])) - - doctree = _parse_annotation("Tuple[int, ...]", app.env) - assert_node(doctree, ([pending_xref, "Tuple"], - [desc_sig_punctuation, "["], - [pending_xref, "int"], - [desc_sig_punctuation, ","], - desc_sig_space, - [desc_sig_punctuation, "..."], - [desc_sig_punctuation, "]"])) - - doctree = _parse_annotation("Callable[[int, int], int]", app.env) - assert_node(doctree, ([pending_xref, "Callable"], - [desc_sig_punctuation, "["], - [desc_sig_punctuation, "["], - [pending_xref, "int"], - [desc_sig_punctuation, ","], - desc_sig_space, - [pending_xref, "int"], - [desc_sig_punctuation, "]"], - [desc_sig_punctuation, ","], - desc_sig_space, - [pending_xref, "int"], - [desc_sig_punctuation, "]"])) - - doctree = _parse_annotation("Callable[[], int]", app.env) - assert_node(doctree, ([pending_xref, "Callable"], - [desc_sig_punctuation, "["], - [desc_sig_punctuation, "["], - [desc_sig_punctuation, "]"], - [desc_sig_punctuation, ","], - desc_sig_space, - [pending_xref, "int"], - [desc_sig_punctuation, "]"])) - - doctree = _parse_annotation("List[None]", app.env) - assert_node(doctree, ([pending_xref, "List"], - [desc_sig_punctuation, "["], - [pending_xref, "None"], - [desc_sig_punctuation, "]"])) - - # None type makes an object-reference (not a class reference) - doctree = _parse_annotation("None", app.env) - assert_node(doctree, ([pending_xref, "None"],)) - assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="None") - - # Literal type makes an object-reference (not a class reference) - doctree = _parse_annotation("typing.Literal['a', 'b']", app.env) - assert_node(doctree, ([pending_xref, "Literal"], - [desc_sig_punctuation, "["], - [desc_sig_literal_string, "'a'"], - [desc_sig_punctuation, ","], - desc_sig_space, - [desc_sig_literal_string, "'b'"], - [desc_sig_punctuation, "]"])) - assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="typing.Literal") - - -def test_parse_annotation_suppress(app): - doctree = _parse_annotation("~typing.Dict[str, str]", app.env) - assert_node(doctree, ([pending_xref, "Dict"], - [desc_sig_punctuation, "["], - [pending_xref, "str"], - [desc_sig_punctuation, ","], - desc_sig_space, - [pending_xref, "str"], - [desc_sig_punctuation, "]"])) - assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="typing.Dict") - - -def test_parse_annotation_Literal(app): - doctree = _parse_annotation("Literal[True, False]", app.env) - assert_node(doctree, ([pending_xref, "Literal"], - [desc_sig_punctuation, "["], - [desc_sig_keyword, "True"], - [desc_sig_punctuation, ","], - desc_sig_space, - [desc_sig_keyword, "False"], - [desc_sig_punctuation, "]"])) - - doctree = _parse_annotation("typing.Literal[0, 1, 'abc']", app.env) - assert_node(doctree, ([pending_xref, "Literal"], - [desc_sig_punctuation, "["], - [desc_sig_literal_number, "0"], - [desc_sig_punctuation, ","], - desc_sig_space, - [desc_sig_literal_number, "1"], - [desc_sig_punctuation, ","], - desc_sig_space, - [desc_sig_literal_string, "'abc'"], - [desc_sig_punctuation, "]"])) - - -def test_pyfunction_signature(app): - text = ".. py:function:: hello(name: str) -> str" - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_name, "hello"], - desc_parameterlist, - [desc_returns, pending_xref, "str"])], - desc_content)])) - assert_node(doctree[1], addnodes.desc, desctype="function", - domain="py", objtype="function", no_index=False) - assert_node(doctree[1][0][1], - [desc_parameterlist, desc_parameter, ([desc_sig_name, "name"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [nodes.inline, pending_xref, "str"])]) - - -def test_pyfunction_signature_full(app): - text = (".. py:function:: hello(a: str, b = 1, *args: str, " - "c: bool = True, d: tuple = (1, 2), **kwargs: str) -> str") - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_name, "hello"], - desc_parameterlist, - [desc_returns, pending_xref, "str"])], - desc_content)])) - assert_node(doctree[1], addnodes.desc, desctype="function", - domain="py", objtype="function", no_index=False) - assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "a"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [desc_sig_name, pending_xref, "str"])], - [desc_parameter, ([desc_sig_name, "b"], - [desc_sig_operator, "="], - [nodes.inline, "1"])], - [desc_parameter, ([desc_sig_operator, "*"], - [desc_sig_name, "args"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [desc_sig_name, pending_xref, "str"])], - [desc_parameter, ([desc_sig_name, "c"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [desc_sig_name, pending_xref, "bool"], - desc_sig_space, - [desc_sig_operator, "="], - desc_sig_space, - [nodes.inline, "True"])], - [desc_parameter, ([desc_sig_name, "d"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [desc_sig_name, pending_xref, "tuple"], - desc_sig_space, - [desc_sig_operator, "="], - desc_sig_space, - [nodes.inline, "(1, 2)"])], - [desc_parameter, ([desc_sig_operator, "**"], - [desc_sig_name, "kwargs"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [desc_sig_name, pending_xref, "str"])])]) - # case: separator at head - text = ".. py:function:: hello(*, a)" - doctree = restructuredtext.parse(app, text) - assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, nodes.inline, "*"], - [desc_parameter, desc_sig_name, "a"])]) - - # case: separator in the middle - text = ".. py:function:: hello(a, /, b, *, c)" - doctree = restructuredtext.parse(app, text) - assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"], - [desc_parameter, desc_sig_operator, "/"], - [desc_parameter, desc_sig_name, "b"], - [desc_parameter, desc_sig_operator, "*"], - [desc_parameter, desc_sig_name, "c"])]) - - # case: separator in the middle (2) - text = ".. py:function:: hello(a, /, *, b)" - doctree = restructuredtext.parse(app, text) - assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"], - [desc_parameter, desc_sig_operator, "/"], - [desc_parameter, desc_sig_operator, "*"], - [desc_parameter, desc_sig_name, "b"])]) - - # case: separator at tail - text = ".. py:function:: hello(a, /)" - doctree = restructuredtext.parse(app, text) - assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"], - [desc_parameter, desc_sig_operator, "/"])]) - - -def test_pyfunction_with_unary_operators(app): - text = ".. py:function:: menu(egg=+1, bacon=-1, sausage=~1, spam=not spam)" - doctree = restructuredtext.parse(app, text) - assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "egg"], - [desc_sig_operator, "="], - [nodes.inline, "+1"])], - [desc_parameter, ([desc_sig_name, "bacon"], - [desc_sig_operator, "="], - [nodes.inline, "-1"])], - [desc_parameter, ([desc_sig_name, "sausage"], - [desc_sig_operator, "="], - [nodes.inline, "~1"])], - [desc_parameter, ([desc_sig_name, "spam"], - [desc_sig_operator, "="], - [nodes.inline, "not spam"])])]) - - -def test_pyfunction_with_binary_operators(app): - text = ".. py:function:: menu(spam=2**64)" - doctree = restructuredtext.parse(app, text) - assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "spam"], - [desc_sig_operator, "="], - [nodes.inline, "2**64"])])]) - - -def test_pyfunction_with_number_literals(app): - text = ".. py:function:: hello(age=0x10, height=1_6_0)" - doctree = restructuredtext.parse(app, text) - assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "age"], - [desc_sig_operator, "="], - [nodes.inline, "0x10"])], - [desc_parameter, ([desc_sig_name, "height"], - [desc_sig_operator, "="], - [nodes.inline, "1_6_0"])])]) - - -def test_pyfunction_with_union_type_operator(app): - text = ".. py:function:: hello(age: int | None)" - doctree = restructuredtext.parse(app, text) - assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "age"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [desc_sig_name, ([pending_xref, "int"], - desc_sig_space, - [desc_sig_punctuation, "|"], - desc_sig_space, - [pending_xref, "None"])])])]) - - -def test_optional_pyfunction_signature(app): - text = ".. py:function:: compile(source [, filename [, symbol]]) -> ast object" - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_name, "compile"], - desc_parameterlist, - [desc_returns, pending_xref, "ast object"])], - desc_content)])) - assert_node(doctree[1], addnodes.desc, desctype="function", - domain="py", objtype="function", no_index=False) - assert_node(doctree[1][0][1], - ([desc_parameter, ([desc_sig_name, "source"])], - [desc_optional, ([desc_parameter, ([desc_sig_name, "filename"])], - [desc_optional, desc_parameter, ([desc_sig_name, "symbol"])])])) - - -def test_pyexception_signature(app): - text = ".. py:exception:: builtins.IOError" - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, ('exception', desc_sig_space)], - [desc_addname, "builtins."], - [desc_name, "IOError"])], - desc_content)])) - assert_node(doctree[1], desc, desctype="exception", - domain="py", objtype="exception", no_index=False) - - -def test_pydata_signature(app): - text = (".. py:data:: version\n" - " :type: int\n" - " :value: 1\n") - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_name, "version"], - [desc_annotation, ([desc_sig_punctuation, ':'], - desc_sig_space, - [pending_xref, "int"])], - [desc_annotation, ( - desc_sig_space, - [desc_sig_punctuation, '='], - desc_sig_space, - "1")], - )], - desc_content)])) - assert_node(doctree[1], addnodes.desc, desctype="data", - domain="py", objtype="data", no_index=False) - - -def test_pydata_signature_old(app): - text = (".. py:data:: version\n" - " :annotation: = 1\n") - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_name, "version"], - [desc_annotation, (desc_sig_space, - "= 1")])], - desc_content)])) - assert_node(doctree[1], addnodes.desc, desctype="data", - domain="py", objtype="data", no_index=False) - - -def test_pydata_with_union_type_operator(app): - text = (".. py:data:: version\n" - " :type: int | str") - doctree = restructuredtext.parse(app, text) - assert_node(doctree[1][0], - ([desc_name, "version"], - [desc_annotation, ([desc_sig_punctuation, ':'], - desc_sig_space, - [pending_xref, "int"], - desc_sig_space, - [desc_sig_punctuation, "|"], - desc_sig_space, - [pending_xref, "str"])])) - - -def test_pyobject_prefix(app): - text = (".. py:class:: Foo\n" - "\n" - " .. py:method:: Foo.say\n" - " .. py:method:: FooBar.say") - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, ('class', desc_sig_space)], - [desc_name, "Foo"])], - [desc_content, (addnodes.index, - desc, - addnodes.index, - desc)])])) - assert doctree[1][1][1].astext().strip() == 'say()' # prefix is stripped - assert doctree[1][1][3].astext().strip() == 'FooBar.say()' # not stripped - - -def test_pydata(app): - text = (".. py:module:: example\n" - ".. py:data:: var\n" - " :type: int\n") - domain = app.env.get_domain('py') - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - addnodes.index, - nodes.target, - [desc, ([desc_signature, ([desc_addname, "example."], - [desc_name, "var"], - [desc_annotation, ([desc_sig_punctuation, ':'], - desc_sig_space, - [pending_xref, "int"])])], - [desc_content, ()])])) - assert_node(doctree[3][0][2][2], pending_xref, **{"py:module": "example"}) - assert 'example.var' in domain.objects - assert domain.objects['example.var'] == ('index', 'example.var', 'data', False) - - -def test_pyfunction(app): - text = (".. py:function:: func1\n" - ".. py:module:: example\n" - ".. py:function:: func2\n" - " :async:\n") - domain = app.env.get_domain('py') - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_name, "func1"], - [desc_parameterlist, ()])], - [desc_content, ()])], - addnodes.index, - addnodes.index, - nodes.target, - [desc, ([desc_signature, ([desc_annotation, ([desc_sig_keyword, 'async'], - desc_sig_space)], - [desc_addname, "example."], - [desc_name, "func2"], - [desc_parameterlist, ()])], - [desc_content, ()])])) - assert_node(doctree[0], addnodes.index, - entries=[('pair', 'built-in function; func1()', 'func1', '', None)]) - assert_node(doctree[2], addnodes.index, - entries=[('pair', 'module; example', 'module-example', '', None)]) - assert_node(doctree[3], addnodes.index, - entries=[('single', 'func2() (in module example)', 'example.func2', '', None)]) - - assert 'func1' in domain.objects - assert domain.objects['func1'] == ('index', 'func1', 'function', False) - assert 'example.func2' in domain.objects - assert domain.objects['example.func2'] == ('index', 'example.func2', 'function', False) - - -def test_pyclass_options(app): - text = (".. py:class:: Class1\n" - ".. py:class:: Class2\n" - " :final:\n") - domain = app.env.get_domain('py') - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], - [desc_name, "Class1"])], - [desc_content, ()])], - addnodes.index, - [desc, ([desc_signature, ([desc_annotation, ("final", - desc_sig_space, - "class", - desc_sig_space)], - [desc_name, "Class2"])], - [desc_content, ()])])) - - # class - assert_node(doctree[0], addnodes.index, - entries=[('single', 'Class1 (built-in class)', 'Class1', '', None)]) - assert 'Class1' in domain.objects - assert domain.objects['Class1'] == ('index', 'Class1', 'class', False) - - # :final: - assert_node(doctree[2], addnodes.index, - entries=[('single', 'Class2 (built-in class)', 'Class2', '', None)]) - assert 'Class2' in domain.objects - assert domain.objects['Class2'] == ('index', 'Class2', 'class', False) - - -def test_pymethod_options(app): - text = (".. py:class:: Class\n" - "\n" - " .. py:method:: meth1\n" - " .. py:method:: meth2\n" - " :classmethod:\n" - " .. py:method:: meth3\n" - " :staticmethod:\n" - " .. py:method:: meth4\n" - " :async:\n" - " .. py:method:: meth5\n" - " :abstractmethod:\n" - " .. py:method:: meth6\n" - " :final:\n") - domain = app.env.get_domain('py') - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], - [desc_name, "Class"])], - [desc_content, (addnodes.index, - desc, - addnodes.index, - desc, - addnodes.index, - desc, - addnodes.index, - desc, - addnodes.index, - desc, - addnodes.index, - desc)])])) - - # method - assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'meth1() (Class method)', 'Class.meth1', '', None)]) - assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "meth1"], - [desc_parameterlist, ()])], - [desc_content, ()])) - assert 'Class.meth1' in domain.objects - assert domain.objects['Class.meth1'] == ('index', 'Class.meth1', 'method', False) - - # :classmethod: - assert_node(doctree[1][1][2], addnodes.index, - entries=[('single', 'meth2() (Class class method)', 'Class.meth2', '', None)]) - assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, ("classmethod", desc_sig_space)], - [desc_name, "meth2"], - [desc_parameterlist, ()])], - [desc_content, ()])) - assert 'Class.meth2' in domain.objects - assert domain.objects['Class.meth2'] == ('index', 'Class.meth2', 'method', False) - - # :staticmethod: - assert_node(doctree[1][1][4], addnodes.index, - entries=[('single', 'meth3() (Class static method)', 'Class.meth3', '', None)]) - assert_node(doctree[1][1][5], ([desc_signature, ([desc_annotation, ("static", desc_sig_space)], - [desc_name, "meth3"], - [desc_parameterlist, ()])], - [desc_content, ()])) - assert 'Class.meth3' in domain.objects - assert domain.objects['Class.meth3'] == ('index', 'Class.meth3', 'method', False) - - # :async: - assert_node(doctree[1][1][6], addnodes.index, - entries=[('single', 'meth4() (Class method)', 'Class.meth4', '', None)]) - assert_node(doctree[1][1][7], ([desc_signature, ([desc_annotation, ("async", desc_sig_space)], - [desc_name, "meth4"], - [desc_parameterlist, ()])], - [desc_content, ()])) - assert 'Class.meth4' in domain.objects - assert domain.objects['Class.meth4'] == ('index', 'Class.meth4', 'method', False) - - # :abstractmethod: - assert_node(doctree[1][1][8], addnodes.index, - entries=[('single', 'meth5() (Class method)', 'Class.meth5', '', None)]) - assert_node(doctree[1][1][9], ([desc_signature, ([desc_annotation, ("abstract", desc_sig_space)], - [desc_name, "meth5"], - [desc_parameterlist, ()])], - [desc_content, ()])) - assert 'Class.meth5' in domain.objects - assert domain.objects['Class.meth5'] == ('index', 'Class.meth5', 'method', False) - - # :final: - assert_node(doctree[1][1][10], addnodes.index, - entries=[('single', 'meth6() (Class method)', 'Class.meth6', '', None)]) - assert_node(doctree[1][1][11], ([desc_signature, ([desc_annotation, ("final", desc_sig_space)], - [desc_name, "meth6"], - [desc_parameterlist, ()])], - [desc_content, ()])) - assert 'Class.meth6' in domain.objects - assert domain.objects['Class.meth6'] == ('index', 'Class.meth6', 'method', False) - - -def test_pyclassmethod(app): - text = (".. py:class:: Class\n" - "\n" - " .. py:classmethod:: meth\n") - domain = app.env.get_domain('py') - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], - [desc_name, "Class"])], - [desc_content, (addnodes.index, - desc)])])) - assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'meth() (Class class method)', 'Class.meth', '', None)]) - assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("classmethod", desc_sig_space)], - [desc_name, "meth"], - [desc_parameterlist, ()])], - [desc_content, ()])) - assert 'Class.meth' in domain.objects - assert domain.objects['Class.meth'] == ('index', 'Class.meth', 'method', False) - - -def test_pystaticmethod(app): - text = (".. py:class:: Class\n" - "\n" - " .. py:staticmethod:: meth\n") - domain = app.env.get_domain('py') - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], - [desc_name, "Class"])], - [desc_content, (addnodes.index, - desc)])])) - assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'meth() (Class static method)', 'Class.meth', '', None)]) - assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("static", desc_sig_space)], - [desc_name, "meth"], - [desc_parameterlist, ()])], - [desc_content, ()])) - assert 'Class.meth' in domain.objects - assert domain.objects['Class.meth'] == ('index', 'Class.meth', 'method', False) - - -def test_pyattribute(app): - text = (".. py:class:: Class\n" - "\n" - " .. py:attribute:: attr\n" - " :type: Optional[str]\n" - " :value: ''\n") - domain = app.env.get_domain('py') - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], - [desc_name, "Class"])], - [desc_content, (addnodes.index, - desc)])])) - assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)]) - assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"], - [desc_annotation, ([desc_sig_punctuation, ':'], - desc_sig_space, - [pending_xref, "str"], - desc_sig_space, - [desc_sig_punctuation, "|"], - desc_sig_space, - [pending_xref, "None"])], - [desc_annotation, (desc_sig_space, - [desc_sig_punctuation, '='], - desc_sig_space, - "''")], - )], - [desc_content, ()])) - assert_node(doctree[1][1][1][0][1][2], pending_xref, **{"py:class": "Class"}) - assert_node(doctree[1][1][1][0][1][6], pending_xref, **{"py:class": "Class"}) - assert 'Class.attr' in domain.objects - assert domain.objects['Class.attr'] == ('index', 'Class.attr', 'attribute', False) - - -def test_pyproperty(app): - text = (".. py:class:: Class\n" - "\n" - " .. py:property:: prop1\n" - " :abstractmethod:\n" - " :type: str\n" - "\n" - " .. py:property:: prop2\n" - " :classmethod:\n" - " :type: str\n") - domain = app.env.get_domain('py') - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], - [desc_name, "Class"])], - [desc_content, (addnodes.index, - desc, - addnodes.index, - desc)])])) - assert_node(doctree[1][1][0], addnodes.index, - entries=[('single', 'prop1 (Class property)', 'Class.prop1', '', None)]) - assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("abstract", desc_sig_space, - "property", desc_sig_space)], - [desc_name, "prop1"], - [desc_annotation, ([desc_sig_punctuation, ':'], - desc_sig_space, - [pending_xref, "str"])])], - [desc_content, ()])) - assert_node(doctree[1][1][2], addnodes.index, - entries=[('single', 'prop2 (Class property)', 'Class.prop2', '', None)]) - assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, ("class", desc_sig_space, - "property", desc_sig_space)], - [desc_name, "prop2"], - [desc_annotation, ([desc_sig_punctuation, ':'], - desc_sig_space, - [pending_xref, "str"])])], - [desc_content, ()])) - assert 'Class.prop1' in domain.objects - assert domain.objects['Class.prop1'] == ('index', 'Class.prop1', 'property', False) - assert 'Class.prop2' in domain.objects - assert domain.objects['Class.prop2'] == ('index', 'Class.prop2', 'property', False) - - -def test_pydecorator_signature(app): - text = ".. py:decorator:: deco" - domain = app.env.get_domain('py') - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_addname, "@"], - [desc_name, "deco"])], - desc_content)])) - assert_node(doctree[1], addnodes.desc, desctype="function", - domain="py", objtype="function", no_index=False) - - assert 'deco' in domain.objects - assert domain.objects['deco'] == ('index', 'deco', 'function', False) - - -def test_pydecoratormethod_signature(app): - text = ".. py:decoratormethod:: deco" - domain = app.env.get_domain('py') - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_addname, "@"], - [desc_name, "deco"])], - desc_content)])) - assert_node(doctree[1], addnodes.desc, desctype="method", - domain="py", objtype="method", no_index=False) - - assert 'deco' in domain.objects - assert domain.objects['deco'] == ('index', 'deco', 'method', False) - - -def test_canonical(app): - text = (".. py:class:: io.StringIO\n" - " :canonical: _io.StringIO") - domain = app.env.get_domain('py') - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], - [desc_addname, "io."], - [desc_name, "StringIO"])], - desc_content)])) - assert 'io.StringIO' in domain.objects - assert domain.objects['io.StringIO'] == ('index', 'io.StringIO', 'class', False) - assert domain.objects['_io.StringIO'] == ('index', 'io.StringIO', 'class', True) - - -def test_canonical_definition_overrides(app, warning): - text = (".. py:class:: io.StringIO\n" - " :canonical: _io.StringIO\n" - ".. py:class:: _io.StringIO\n") - restructuredtext.parse(app, text) - assert warning.getvalue() == "" - - domain = app.env.get_domain('py') - assert domain.objects['_io.StringIO'] == ('index', 'id0', 'class', False) - - -def test_canonical_definition_skip(app, warning): - text = (".. py:class:: _io.StringIO\n" - ".. py:class:: io.StringIO\n" - " :canonical: _io.StringIO\n") - - restructuredtext.parse(app, text) - assert warning.getvalue() == "" - - domain = app.env.get_domain('py') - assert domain.objects['_io.StringIO'] == ('index', 'io.StringIO', 'class', False) - - -def test_canonical_duplicated(app, warning): - text = (".. py:class:: mypackage.StringIO\n" - " :canonical: _io.StringIO\n" - ".. py:class:: io.StringIO\n" - " :canonical: _io.StringIO\n") - - restructuredtext.parse(app, text) - assert warning.getvalue() != "" - - -def test_info_field_list(app): - text = (".. py:module:: example\n" - ".. py:class:: Class\n" - "\n" - " :meta blah: this meta-field must not show up in the toc-tree\n" - " :param str name: blah blah\n" - " :meta another meta field:\n" - " :param age: blah blah\n" - " :type age: int\n" - " :param items: blah blah\n" - " :type items: Tuple[str, ...]\n" - " :param Dict[str, str] params: blah blah\n") - doctree = restructuredtext.parse(app, text) - print(doctree) - - assert_node(doctree, (addnodes.index, - addnodes.index, - nodes.target, - [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], - [desc_addname, "example."], - [desc_name, "Class"])], - [desc_content, nodes.field_list, nodes.field])])) - assert_node(doctree[3][1][0][0], - ([nodes.field_name, "Parameters"], - [nodes.field_body, nodes.bullet_list, ([nodes.list_item, nodes.paragraph], - [nodes.list_item, nodes.paragraph], - [nodes.list_item, nodes.paragraph], - [nodes.list_item, nodes.paragraph])])) - - # :param str name: - assert_node(doctree[3][1][0][0][1][0][0][0], - ([addnodes.literal_strong, "name"], - " (", - [pending_xref, addnodes.literal_emphasis, "str"], - ")", - " -- ", - "blah blah")) - assert_node(doctree[3][1][0][0][1][0][0][0][2], pending_xref, - refdomain="py", reftype="class", reftarget="str", - **{"py:module": "example", "py:class": "Class"}) - - # :param age: + :type age: - assert_node(doctree[3][1][0][0][1][0][1][0], - ([addnodes.literal_strong, "age"], - " (", - [pending_xref, addnodes.literal_emphasis, "int"], - ")", - " -- ", - "blah blah")) - assert_node(doctree[3][1][0][0][1][0][1][0][2], pending_xref, - refdomain="py", reftype="class", reftarget="int", - **{"py:module": "example", "py:class": "Class"}) - - # :param items: + :type items: - assert_node(doctree[3][1][0][0][1][0][2][0], - ([addnodes.literal_strong, "items"], - " (", - [pending_xref, addnodes.literal_emphasis, "Tuple"], - [addnodes.literal_emphasis, "["], - [pending_xref, addnodes.literal_emphasis, "str"], - [addnodes.literal_emphasis, ", "], - [addnodes.literal_emphasis, "..."], - [addnodes.literal_emphasis, "]"], - ")", - " -- ", - "blah blah")) - assert_node(doctree[3][1][0][0][1][0][2][0][2], pending_xref, - refdomain="py", reftype="class", reftarget="Tuple", - **{"py:module": "example", "py:class": "Class"}) - assert_node(doctree[3][1][0][0][1][0][2][0][4], pending_xref, - refdomain="py", reftype="class", reftarget="str", - **{"py:module": "example", "py:class": "Class"}) - - # :param Dict[str, str] params: - assert_node(doctree[3][1][0][0][1][0][3][0], - ([addnodes.literal_strong, "params"], - " (", - [pending_xref, addnodes.literal_emphasis, "Dict"], - [addnodes.literal_emphasis, "["], - [pending_xref, addnodes.literal_emphasis, "str"], - [addnodes.literal_emphasis, ", "], - [pending_xref, addnodes.literal_emphasis, "str"], - [addnodes.literal_emphasis, "]"], - ")", - " -- ", - "blah blah")) - assert_node(doctree[3][1][0][0][1][0][3][0][2], pending_xref, - refdomain="py", reftype="class", reftarget="Dict", - **{"py:module": "example", "py:class": "Class"}) - assert_node(doctree[3][1][0][0][1][0][3][0][4], pending_xref, - refdomain="py", reftype="class", reftarget="str", - **{"py:module": "example", "py:class": "Class"}) - assert_node(doctree[3][1][0][0][1][0][3][0][6], pending_xref, - refdomain="py", reftype="class", reftarget="str", - **{"py:module": "example", "py:class": "Class"}) - - -def test_info_field_list_piped_type(app): - text = (".. py:module:: example\n" - ".. py:class:: Class\n" - "\n" - " :param age: blah blah\n" - " :type age: int | str\n") - doctree = restructuredtext.parse(app, text) - - assert_node(doctree, - (addnodes.index, - addnodes.index, - nodes.target, - [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], - [desc_addname, "example."], - [desc_name, "Class"])], - [desc_content, nodes.field_list, nodes.field, (nodes.field_name, - nodes.field_body)])])) - assert_node(doctree[3][1][0][0][1], - ([nodes.paragraph, ([addnodes.literal_strong, "age"], - " (", - [pending_xref, addnodes.literal_emphasis, "int"], - [addnodes.literal_emphasis, " | "], - [pending_xref, addnodes.literal_emphasis, "str"], - ")", - " -- ", - "blah blah")],)) - assert_node(doctree[3][1][0][0][1][0][2], pending_xref, - refdomain="py", reftype="class", reftarget="int", - **{"py:module": "example", "py:class": "Class"}) - assert_node(doctree[3][1][0][0][1][0][4], pending_xref, - refdomain="py", reftype="class", reftarget="str", - **{"py:module": "example", "py:class": "Class"}) - - -def test_info_field_list_Literal(app): - text = (".. py:module:: example\n" - ".. py:class:: Class\n" - "\n" - " :param age: blah blah\n" - " :type age: Literal['foo', 'bar', 'baz']\n") - doctree = restructuredtext.parse(app, text) - - assert_node(doctree, - (addnodes.index, - addnodes.index, - nodes.target, - [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], - [desc_addname, "example."], - [desc_name, "Class"])], - [desc_content, nodes.field_list, nodes.field, (nodes.field_name, - nodes.field_body)])])) - assert_node(doctree[3][1][0][0][1], - ([nodes.paragraph, ([addnodes.literal_strong, "age"], - " (", - [pending_xref, addnodes.literal_emphasis, "Literal"], - [addnodes.literal_emphasis, "["], - [addnodes.literal_emphasis, "'foo'"], - [addnodes.literal_emphasis, ", "], - [addnodes.literal_emphasis, "'bar'"], - [addnodes.literal_emphasis, ", "], - [addnodes.literal_emphasis, "'baz'"], - [addnodes.literal_emphasis, "]"], - ")", - " -- ", - "blah blah")],)) - assert_node(doctree[3][1][0][0][1][0][2], pending_xref, - refdomain="py", reftype="class", reftarget="Literal", - **{"py:module": "example", "py:class": "Class"}) - - -def test_info_field_list_var(app): - text = (".. py:class:: Class\n" - "\n" - " :var int attr: blah blah\n") - doctree = restructuredtext.parse(app, text) - - assert_node(doctree, (addnodes.index, - [desc, (desc_signature, - [desc_content, nodes.field_list, nodes.field])])) - assert_node(doctree[1][1][0][0], ([nodes.field_name, "Variables"], - [nodes.field_body, nodes.paragraph])) - - # :var int attr: - assert_node(doctree[1][1][0][0][1][0], - ([addnodes.literal_strong, "attr"], - " (", - [pending_xref, addnodes.literal_emphasis, "int"], - ")", - " -- ", - "blah blah")) - assert_node(doctree[1][1][0][0][1][0][2], pending_xref, - refdomain="py", reftype="class", reftarget="int", **{"py:class": "Class"}) - - -def test_info_field_list_napoleon_deliminator_of(app): - text = (".. py:module:: example\n" - ".. py:class:: Class\n" - "\n" - " :param list_str_var: example description.\n" - " :type list_str_var: list of str\n" - " :param tuple_int_var: example description.\n" - " :type tuple_int_var: tuple of tuple of int\n" - ) - doctree = restructuredtext.parse(app, text) - - # :param list of str list_str_var: - assert_node(doctree[3][1][0][0][1][0][0][0], - ([addnodes.literal_strong, "list_str_var"], - " (", - [pending_xref, addnodes.literal_emphasis, "list"], - [addnodes.literal_emphasis, " of "], - [pending_xref, addnodes.literal_emphasis, "str"], - ")", - " -- ", - "example description.")) - - # :param tuple of tuple of int tuple_int_var: - assert_node(doctree[3][1][0][0][1][0][1][0], - ([addnodes.literal_strong, "tuple_int_var"], - " (", - [pending_xref, addnodes.literal_emphasis, "tuple"], - [addnodes.literal_emphasis, " of "], - [pending_xref, addnodes.literal_emphasis, "tuple"], - [addnodes.literal_emphasis, " of "], - [pending_xref, addnodes.literal_emphasis, "int"], - ")", - " -- ", - "example description.")) - - -def test_info_field_list_napoleon_deliminator_or(app): - text = (".. py:module:: example\n" - ".. py:class:: Class\n" - "\n" - " :param bool_str_var: example description.\n" - " :type bool_str_var: bool or str\n" - " :param str_float_int_var: example description.\n" - " :type str_float_int_var: str or float or int\n" - ) - doctree = restructuredtext.parse(app, text) - - # :param bool or str bool_str_var: - assert_node(doctree[3][1][0][0][1][0][0][0], - ([addnodes.literal_strong, "bool_str_var"], - " (", - [pending_xref, addnodes.literal_emphasis, "bool"], - [addnodes.literal_emphasis, " or "], - [pending_xref, addnodes.literal_emphasis, "str"], - ")", - " -- ", - "example description.")) - - # :param str or float or int str_float_int_var: - assert_node(doctree[3][1][0][0][1][0][1][0], - ([addnodes.literal_strong, "str_float_int_var"], - " (", - [pending_xref, addnodes.literal_emphasis, "str"], - [addnodes.literal_emphasis, " or "], - [pending_xref, addnodes.literal_emphasis, "float"], - [addnodes.literal_emphasis, " or "], - [pending_xref, addnodes.literal_emphasis, "int"], - ")", - " -- ", - "example description.")) - - -def test_type_field(app): - text = (".. py:data:: var1\n" - " :type: .int\n" - ".. py:data:: var2\n" - " :type: ~builtins.int\n" - ".. py:data:: var3\n" - " :type: typing.Optional[typing.Tuple[int, typing.Any]]\n") - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_name, "var1"], - [desc_annotation, ([desc_sig_punctuation, ':'], - desc_sig_space, - [pending_xref, "int"])])], - [desc_content, ()])], - addnodes.index, - [desc, ([desc_signature, ([desc_name, "var2"], - [desc_annotation, ([desc_sig_punctuation, ':'], - desc_sig_space, - [pending_xref, "int"])])], - [desc_content, ()])], - addnodes.index, - [desc, ([desc_signature, ([desc_name, "var3"], - [desc_annotation, ([desc_sig_punctuation, ":"], - desc_sig_space, - [pending_xref, "Optional"], - [desc_sig_punctuation, "["], - [pending_xref, "Tuple"], - [desc_sig_punctuation, "["], - [pending_xref, "int"], - [desc_sig_punctuation, ","], - desc_sig_space, - [pending_xref, "Any"], - [desc_sig_punctuation, "]"], - [desc_sig_punctuation, "]"])])], - [desc_content, ()])])) - assert_node(doctree[1][0][1][2], pending_xref, reftarget='int', refspecific=True) - assert_node(doctree[3][0][1][2], pending_xref, reftarget='builtins.int', refspecific=False) - assert_node(doctree[5][0][1][2], pending_xref, reftarget='typing.Optional', refspecific=False) - assert_node(doctree[5][0][1][4], pending_xref, reftarget='typing.Tuple', refspecific=False) - assert_node(doctree[5][0][1][6], pending_xref, reftarget='int', refspecific=False) - assert_node(doctree[5][0][1][9], pending_xref, reftarget='typing.Any', refspecific=False) - - -@pytest.mark.sphinx(freshenv=True) -def test_module_index(app): - text = (".. py:module:: docutils\n" - ".. py:module:: sphinx\n" - ".. py:module:: sphinx.config\n" - ".. py:module:: sphinx.builders\n" - ".. py:module:: sphinx.builders.html\n" - ".. py:module:: sphinx_intl\n") - restructuredtext.parse(app, text) - index = PythonModuleIndex(app.env.get_domain('py')) - assert index.generate() == ( - [('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), - ('s', [IndexEntry('sphinx', 1, 'index', 'module-sphinx', '', '', ''), - IndexEntry('sphinx.builders', 2, 'index', 'module-sphinx.builders', '', '', ''), - IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', ''), - IndexEntry('sphinx.config', 2, 'index', 'module-sphinx.config', '', '', ''), - IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])], - False, - ) - - -@pytest.mark.sphinx(freshenv=True) -def test_module_index_submodule(app): - text = ".. py:module:: sphinx.config\n" - restructuredtext.parse(app, text) - index = PythonModuleIndex(app.env.get_domain('py')) - assert index.generate() == ( - [('s', [IndexEntry('sphinx', 1, '', '', '', '', ''), - IndexEntry('sphinx.config', 2, 'index', 'module-sphinx.config', '', '', '')])], - False, - ) - - -@pytest.mark.sphinx(freshenv=True) -def test_module_index_not_collapsed(app): - text = (".. py:module:: docutils\n" - ".. py:module:: sphinx\n") - restructuredtext.parse(app, text) - index = PythonModuleIndex(app.env.get_domain('py')) - assert index.generate() == ( - [('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), - ('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', '')])], - True, - ) - - -@pytest.mark.sphinx(freshenv=True, confoverrides={'modindex_common_prefix': ['sphinx.']}) -def test_modindex_common_prefix(app): - text = (".. py:module:: docutils\n" - ".. py:module:: sphinx\n" - ".. py:module:: sphinx.config\n" - ".. py:module:: sphinx.builders\n" - ".. py:module:: sphinx.builders.html\n" - ".. py:module:: sphinx_intl\n") - restructuredtext.parse(app, text) - index = PythonModuleIndex(app.env.get_domain('py')) - assert index.generate() == ( - [('b', [IndexEntry('sphinx.builders', 1, 'index', 'module-sphinx.builders', '', '', ''), - IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', '')]), - ('c', [IndexEntry('sphinx.config', 0, 'index', 'module-sphinx.config', '', '', '')]), - ('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), - ('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', ''), - IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])], - True, - ) - - -def test_no_index_entry(app): - text = (".. py:function:: f()\n" - ".. py:function:: g()\n" - " :no-index-entry:\n") - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, desc, addnodes.index, desc)) - assert_node(doctree[0], addnodes.index, entries=[('pair', 'built-in function; f()', 'f', '', None)]) - assert_node(doctree[2], addnodes.index, entries=[]) - - text = (".. py:class:: f\n" - ".. py:class:: g\n" - " :no-index-entry:\n") - doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, desc, addnodes.index, desc)) - assert_node(doctree[0], addnodes.index, entries=[('single', 'f (built-in class)', 'f', '', None)]) - assert_node(doctree[2], addnodes.index, entries=[]) - - -@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names') -def test_python_python_use_unqualified_type_names(app, status, warning): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ('<span class="n"><a class="reference internal" href="#foo.Name" title="foo.Name">' - '<span class="pre">Name</span></a></span>' in content) - assert '<span class="n"><span class="pre">foo.Age</span></span>' in content - assert ('<p><strong>name</strong> (<a class="reference internal" href="#foo.Name" ' - 'title="foo.Name"><em>Name</em></a>) – blah blah</p>' in content) - assert '<p><strong>age</strong> (<em>foo.Age</em>) – blah blah</p>' in content - - -@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names', - confoverrides={'python_use_unqualified_type_names': False}) -def test_python_python_use_unqualified_type_names_disabled(app, status, warning): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ('<span class="n"><a class="reference internal" href="#foo.Name" title="foo.Name">' - '<span class="pre">foo.Name</span></a></span>' in content) - assert '<span class="n"><span class="pre">foo.Age</span></span>' in content - assert ('<p><strong>name</strong> (<a class="reference internal" href="#foo.Name" ' - 'title="foo.Name"><em>foo.Name</em></a>) – blah blah</p>' in content) - assert '<p><strong>age</strong> (<em>foo.Age</em>) – blah blah</p>' in content - - -@pytest.mark.sphinx('dummy', testroot='domain-py-xref-warning') -def test_warn_missing_reference(app, status, warning): - app.build() - assert "index.rst:6: WARNING: undefined label: 'no-label'" in warning.getvalue() - assert ("index.rst:6: WARNING: Failed to create a cross reference. " - "A title or caption not found: 'existing-label'") in warning.getvalue() - - -@pytest.mark.sphinx(confoverrides={'nitpicky': True}) -@pytest.mark.parametrize('include_options', [True, False]) -def test_signature_line_number(app, include_options): - text = (".. py:function:: foo(bar : string)\n" + - (" :no-index-entry:\n" if include_options else "")) - doc = restructuredtext.parse(app, text) - xrefs = list(doc.findall(condition=addnodes.pending_xref)) - assert len(xrefs) == 1 - source, line = docutils.utils.get_source_line(xrefs[0]) - assert 'index.rst' in source - assert line == 1 - - -@pytest.mark.sphinx('html', confoverrides={ - 'python_maximum_signature_line_length': len("hello(name: str) -> str"), -}) -def test_pyfunction_signature_with_python_maximum_signature_line_length_equal(app): - text = ".. py:function:: hello(name: str) -> str" - doctree = restructuredtext.parse(app, text) - assert_node(doctree, ( - addnodes.index, - [desc, ( - [desc_signature, ( - [desc_name, "hello"], - desc_parameterlist, - [desc_returns, pending_xref, "str"], - )], - desc_content, - )], - )) - assert_node(doctree[1], addnodes.desc, desctype="function", - domain="py", objtype="function", no_index=False) - assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, ( - [desc_sig_name, "name"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [nodes.inline, pending_xref, "str"], - )]) - assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) - - -@pytest.mark.sphinx('html', confoverrides={ - 'python_maximum_signature_line_length': len("hello(name: str) -> str"), -}) -def test_pyfunction_signature_with_python_maximum_signature_line_length_force_single(app): - text = (".. py:function:: hello(names: str) -> str\n" - " :single-line-parameter-list:") - doctree = restructuredtext.parse(app, text) - assert_node(doctree, ( - addnodes.index, - [desc, ( - [desc_signature, ( - [desc_name, "hello"], - desc_parameterlist, - [desc_returns, pending_xref, "str"], - )], - desc_content, - )], - )) - assert_node(doctree[1], addnodes.desc, desctype="function", - domain="py", objtype="function", no_index=False) - assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, ( - [desc_sig_name, "names"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [nodes.inline, pending_xref, "str"], - )]) - assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) - - -@pytest.mark.sphinx('html', confoverrides={ - 'python_maximum_signature_line_length': len("hello(name: str) -> str"), -}) -def test_pyfunction_signature_with_python_maximum_signature_line_length_break(app): - text = ".. py:function:: hello(names: str) -> str" - doctree = restructuredtext.parse(app, text) - assert_node(doctree, ( - addnodes.index, - [desc, ( - [desc_signature, ( - [desc_name, "hello"], - desc_parameterlist, - [desc_returns, pending_xref, "str"], - )], - desc_content, - )], - )) - assert_node(doctree[1], addnodes.desc, desctype="function", - domain="py", objtype="function", no_index=False) - assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, ( - [desc_sig_name, "names"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [nodes.inline, pending_xref, "str"], - )]) - assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=True) - - -@pytest.mark.sphinx('html', confoverrides={ - 'maximum_signature_line_length': len("hello(name: str) -> str"), -}) -def test_pyfunction_signature_with_maximum_signature_line_length_equal(app): - text = ".. py:function:: hello(name: str) -> str" - doctree = restructuredtext.parse(app, text) - assert_node(doctree, ( - addnodes.index, - [desc, ( - [desc_signature, ( - [desc_name, "hello"], - desc_parameterlist, - [desc_returns, pending_xref, "str"], - )], - desc_content, - )], - )) - assert_node(doctree[1], addnodes.desc, desctype="function", - domain="py", objtype="function", no_index=False) - assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, ( - [desc_sig_name, "name"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [nodes.inline, pending_xref, "str"], - )]) - assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) - - -@pytest.mark.sphinx('html', confoverrides={ - 'maximum_signature_line_length': len("hello(name: str) -> str"), -}) -def test_pyfunction_signature_with_maximum_signature_line_length_force_single(app): - text = (".. py:function:: hello(names: str) -> str\n" - " :single-line-parameter-list:") - doctree = restructuredtext.parse(app, text) - assert_node(doctree, ( - addnodes.index, - [desc, ( - [desc_signature, ( - [desc_name, "hello"], - desc_parameterlist, - [desc_returns, pending_xref, "str"], - )], - desc_content, - )], - )) - assert_node(doctree[1], addnodes.desc, desctype="function", - domain="py", objtype="function", no_index=False) - assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, ( - [desc_sig_name, "names"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [nodes.inline, pending_xref, "str"], - )]) - assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) - - -@pytest.mark.sphinx('html', confoverrides={ - 'maximum_signature_line_length': len("hello(name: str) -> str"), -}) -def test_pyfunction_signature_with_maximum_signature_line_length_break(app): - text = ".. py:function:: hello(names: str) -> str" - doctree = restructuredtext.parse(app, text) - assert_node(doctree, ( - addnodes.index, - [desc, ( - [desc_signature, ( - [desc_name, "hello"], - desc_parameterlist, - [desc_returns, pending_xref, "str"], - )], - desc_content, - )], - )) - assert_node(doctree[1], addnodes.desc, desctype="function", - domain="py", objtype="function", no_index=False) - assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, ( - [desc_sig_name, "names"], - [desc_sig_punctuation, ":"], - desc_sig_space, - [nodes.inline, pending_xref, "str"], - )]) - assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=True) - - -@pytest.mark.sphinx( - 'html', - confoverrides={ - 'python_maximum_signature_line_length': len("hello(name: str) -> str"), - 'maximum_signature_line_length': 1, - }, -) -def test_python_maximum_signature_line_length_overrides_global(app): - text = ".. py:function:: hello(name: str) -> str" - doctree = restructuredtext.parse(app, text) - expected_doctree = (addnodes.index, - [desc, ([desc_signature, ([desc_name, "hello"], - desc_parameterlist, - [desc_returns, pending_xref, "str"])], - desc_content)]) - assert_node(doctree, expected_doctree) - assert_node(doctree[1], addnodes.desc, desctype="function", - domain="py", objtype="function", no_index=False) - signame_node = [desc_sig_name, "name"] - expected_sig = [desc_parameterlist, desc_parameter, (signame_node, - [desc_sig_punctuation, ":"], - desc_sig_space, - [nodes.inline, pending_xref, "str"])] - assert_node(doctree[1][0][1], expected_sig) - assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) - - -@pytest.mark.sphinx( - 'html', testroot='domain-py-python_maximum_signature_line_length', -) -def test_domain_py_python_maximum_signature_line_length_in_html(app, status, warning): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - expected_parameter_list_hello = """\ - -<dl> -<dd>\ -<em class="sig-param">\ -<span class="n"><span class="pre">name</span></span>\ -<span class="p"><span class="pre">:</span></span>\ -<span class="w"> </span>\ -<span class="n"><span class="pre">str</span></span>\ -</em>,\ -</dd> -</dl> - -<span class="sig-paren">)</span> \ -<span class="sig-return">\ -<span class="sig-return-icon">→</span> \ -<span class="sig-return-typehint"><span class="pre">str</span></span>\ -</span>\ -<a class="headerlink" href="#hello" title="Link to this definition">¶</a>\ -</dt>\ -""" - assert expected_parameter_list_hello in content - - param_line_fmt = '<dd>{}</dd>\n' - param_name_fmt = ( - '<em class="sig-param"><span class="n"><span class="pre">{}</span></span></em>' - ) - optional_fmt = '<span class="optional">{}</span>' - - expected_a = param_line_fmt.format( - optional_fmt.format("[") + param_name_fmt.format("a") + "," + optional_fmt.format("["), - ) - assert expected_a in content - - expected_b = param_line_fmt.format( - param_name_fmt.format("b") + "," + optional_fmt.format("]") + optional_fmt.format("]"), - ) - assert expected_b in content - - expected_c = param_line_fmt.format(param_name_fmt.format("c") + ",") - assert expected_c in content - - expected_d = param_line_fmt.format(param_name_fmt.format("d") + optional_fmt.format("[") + ",") - assert expected_d in content - - expected_e = param_line_fmt.format(param_name_fmt.format("e") + ",") - assert expected_e in content - - expected_f = param_line_fmt.format(param_name_fmt.format("f") + "," + optional_fmt.format("]")) - assert expected_f in content - - expected_parameter_list_foo = """\ - -<dl> -{}{}{}{}{}{}</dl> - -<span class="sig-paren">)</span>\ -<a class="headerlink" href="#foo" title="Link to this definition">¶</a>\ -</dt>\ -""".format(expected_a, expected_b, expected_c, expected_d, expected_e, expected_f) - assert expected_parameter_list_foo in content - - -@pytest.mark.sphinx( - 'text', testroot='domain-py-python_maximum_signature_line_length', -) -def test_domain_py_python_maximum_signature_line_length_in_text(app, status, warning): - app.build() - content = (app.outdir / 'index.txt').read_text(encoding='utf8') - param_line_fmt = STDINDENT * " " + "{}\n" - - expected_parameter_list_hello = "(\n{}) -> str".format(param_line_fmt.format("name: str,")) - - assert expected_parameter_list_hello in content - - expected_a = param_line_fmt.format("[a,[") - assert expected_a in content - - expected_b = param_line_fmt.format("b,]]") - assert expected_b in content - - expected_c = param_line_fmt.format("c,") - assert expected_c in content - - expected_d = param_line_fmt.format("d[,") - assert expected_d in content - - expected_e = param_line_fmt.format("e,") - assert expected_e in content - - expected_f = param_line_fmt.format("f,]") - assert expected_f in content - - expected_parameter_list_foo = "(\n{}{}{}{}{}{})".format( - expected_a, expected_b, expected_c, expected_d, expected_e, expected_f, - ) - assert expected_parameter_list_foo in content - - -def test_module_content_line_number(app): - text = (".. py:module:: foo\n" + - "\n" + - " Some link here: :ref:`abc`\n") - doc = restructuredtext.parse(app, text) - xrefs = list(doc.findall(condition=addnodes.pending_xref)) - assert len(xrefs) == 1 - source, line = docutils.utils.get_source_line(xrefs[0]) - assert 'index.rst' in source - assert line == 3 - - -@pytest.mark.sphinx(freshenv=True, confoverrides={'python_display_short_literal_types': True}) -def test_short_literal_types(app): - text = """\ -.. py:function:: literal_ints(x: Literal[1, 2, 3] = 1) -> None -.. py:function:: literal_union(x: Union[Literal["a"], Literal["b"], Literal["c"]]) -> None -""" - doctree = restructuredtext.parse(app, text) - assert_node(doctree, ( - addnodes.index, - [desc, ( - [desc_signature, ( - [desc_name, 'literal_ints'], - [desc_parameterlist, ( - [desc_parameter, ( - [desc_sig_name, 'x'], - [desc_sig_punctuation, ':'], - desc_sig_space, - [desc_sig_name, ( - [desc_sig_literal_number, '1'], - desc_sig_space, - [desc_sig_punctuation, '|'], - desc_sig_space, - [desc_sig_literal_number, '2'], - desc_sig_space, - [desc_sig_punctuation, '|'], - desc_sig_space, - [desc_sig_literal_number, '3'], - )], - desc_sig_space, - [desc_sig_operator, '='], - desc_sig_space, - [nodes.inline, '1'], - )], - )], - [desc_returns, pending_xref, 'None'], - )], - [desc_content, ()], - )], - addnodes.index, - [desc, ( - [desc_signature, ( - [desc_name, 'literal_union'], - [desc_parameterlist, ( - [desc_parameter, ( - [desc_sig_name, 'x'], - [desc_sig_punctuation, ':'], - desc_sig_space, - [desc_sig_name, ( - [desc_sig_literal_string, "'a'"], - desc_sig_space, - [desc_sig_punctuation, '|'], - desc_sig_space, - [desc_sig_literal_string, "'b'"], - desc_sig_space, - [desc_sig_punctuation, '|'], - desc_sig_space, - [desc_sig_literal_string, "'c'"], - )], - )], - )], - [desc_returns, pending_xref, 'None'], - )], - [desc_content, ()], - )], - )) - - -def test_function_pep_695(app): - text = """.. py:function:: func[\ - S,\ - T: int,\ - U: (int, str),\ - R: int | int,\ - A: int | Annotated[int, ctype("char")],\ - *V,\ - **P\ - ] - """ - doctree = restructuredtext.parse(app, text) - assert_node(doctree, ( - addnodes.index, - [desc, ( - [desc_signature, ( - [desc_name, 'func'], - [desc_type_parameter_list, ( - [desc_type_parameter, ([desc_sig_name, 'S'])], - [desc_type_parameter, ( - [desc_sig_name, 'T'], - [desc_sig_punctuation, ':'], - desc_sig_space, - [desc_sig_name, ([pending_xref, 'int'])], - )], - [desc_type_parameter, ( - [desc_sig_name, 'U'], - [desc_sig_punctuation, ':'], - desc_sig_space, - [desc_sig_punctuation, '('], - [desc_sig_name, ( - [pending_xref, 'int'], - [desc_sig_punctuation, ','], - desc_sig_space, - [pending_xref, 'str'], - )], - [desc_sig_punctuation, ')'], - )], - [desc_type_parameter, ( - [desc_sig_name, 'R'], - [desc_sig_punctuation, ':'], - desc_sig_space, - [desc_sig_name, ( - [pending_xref, 'int'], - desc_sig_space, - [desc_sig_punctuation, '|'], - desc_sig_space, - [pending_xref, 'int'], - )], - )], - [desc_type_parameter, ( - [desc_sig_name, 'A'], - [desc_sig_punctuation, ':'], - desc_sig_space, - [desc_sig_name, ([pending_xref, 'int | Annotated[int, ctype("char")]'])], - )], - [desc_type_parameter, ( - [desc_sig_operator, '*'], - [desc_sig_name, 'V'], - )], - [desc_type_parameter, ( - [desc_sig_operator, '**'], - [desc_sig_name, 'P'], - )], - )], - [desc_parameterlist, ()], - )], - [desc_content, ()], - )], - )) - - -def test_class_def_pep_695(app): - # Non-concrete unbound generics are allowed at runtime but type checkers - # should fail (https://peps.python.org/pep-0695/#type-parameter-scopes) - text = """.. py:class:: Class[S: Sequence[T], T, KT, VT](Dict[KT, VT])""" - doctree = restructuredtext.parse(app, text) - assert_node(doctree, ( - addnodes.index, - [desc, ( - [desc_signature, ( - [desc_annotation, ('class', desc_sig_space)], - [desc_name, 'Class'], - [desc_type_parameter_list, ( - [desc_type_parameter, ( - [desc_sig_name, 'S'], - [desc_sig_punctuation, ':'], - desc_sig_space, - [desc_sig_name, ( - [pending_xref, 'Sequence'], - [desc_sig_punctuation, '['], - [pending_xref, 'T'], - [desc_sig_punctuation, ']'], - )], - )], - [desc_type_parameter, ([desc_sig_name, 'T'])], - [desc_type_parameter, ([desc_sig_name, 'KT'])], - [desc_type_parameter, ([desc_sig_name, 'VT'])], - )], - [desc_parameterlist, ([desc_parameter, 'Dict[KT, VT]'])], - )], - [desc_content, ()], - )], - )) - - -def test_class_def_pep_696(app): - # test default values for type variables without using PEP 696 AST parser - text = """.. py:class:: Class[\ - T, KT, VT,\ - J: int,\ - K = list,\ - S: str = str,\ - L: (T, tuple[T, ...], collections.abc.Iterable[T]) = set[T],\ - Q: collections.abc.Mapping[KT, VT] = dict[KT, VT],\ - *V = *tuple[*Ts, bool],\ - **P = [int, Annotated[int, ValueRange(3, 10), ctype("char")]]\ - ](Other[T, KT, VT, J, S, L, Q, *V, **P]) - """ - doctree = restructuredtext.parse(app, text) - assert_node(doctree, ( - addnodes.index, - [desc, ( - [desc_signature, ( - [desc_annotation, ('class', desc_sig_space)], - [desc_name, 'Class'], - [desc_type_parameter_list, ( - [desc_type_parameter, ([desc_sig_name, 'T'])], - [desc_type_parameter, ([desc_sig_name, 'KT'])], - [desc_type_parameter, ([desc_sig_name, 'VT'])], - # J: int - [desc_type_parameter, ( - [desc_sig_name, 'J'], - [desc_sig_punctuation, ':'], - desc_sig_space, - [desc_sig_name, ([pending_xref, 'int'])], - )], - # K = list - [desc_type_parameter, ( - [desc_sig_name, 'K'], - desc_sig_space, - [desc_sig_operator, '='], - desc_sig_space, - [nodes.inline, 'list'], - )], - # S: str = str - [desc_type_parameter, ( - [desc_sig_name, 'S'], - [desc_sig_punctuation, ':'], - desc_sig_space, - [desc_sig_name, ([pending_xref, 'str'])], - desc_sig_space, - [desc_sig_operator, '='], - desc_sig_space, - [nodes.inline, 'str'], - )], - [desc_type_parameter, ( - [desc_sig_name, 'L'], - [desc_sig_punctuation, ':'], - desc_sig_space, - [desc_sig_punctuation, '('], - [desc_sig_name, ( - # T - [pending_xref, 'T'], - [desc_sig_punctuation, ','], - desc_sig_space, - # tuple[T, ...] - [pending_xref, 'tuple'], - [desc_sig_punctuation, '['], - [pending_xref, 'T'], - [desc_sig_punctuation, ','], - desc_sig_space, - [desc_sig_punctuation, '...'], - [desc_sig_punctuation, ']'], - [desc_sig_punctuation, ','], - desc_sig_space, - # collections.abc.Iterable[T] - [pending_xref, 'collections.abc.Iterable'], - [desc_sig_punctuation, '['], - [pending_xref, 'T'], - [desc_sig_punctuation, ']'], - )], - [desc_sig_punctuation, ')'], - desc_sig_space, - [desc_sig_operator, '='], - desc_sig_space, - [nodes.inline, 'set[T]'], - )], - [desc_type_parameter, ( - [desc_sig_name, 'Q'], - [desc_sig_punctuation, ':'], - desc_sig_space, - [desc_sig_name, ( - [pending_xref, 'collections.abc.Mapping'], - [desc_sig_punctuation, '['], - [pending_xref, 'KT'], - [desc_sig_punctuation, ','], - desc_sig_space, - [pending_xref, 'VT'], - [desc_sig_punctuation, ']'], - )], - desc_sig_space, - [desc_sig_operator, '='], - desc_sig_space, - [nodes.inline, 'dict[KT, VT]'], - )], - [desc_type_parameter, ( - [desc_sig_operator, '*'], - [desc_sig_name, 'V'], - desc_sig_space, - [desc_sig_operator, '='], - desc_sig_space, - [nodes.inline, '*tuple[*Ts, bool]'], - )], - [desc_type_parameter, ( - [desc_sig_operator, '**'], - [desc_sig_name, 'P'], - desc_sig_space, - [desc_sig_operator, '='], - desc_sig_space, - [nodes.inline, '[int, Annotated[int, ValueRange(3, 10), ctype("char")]]'], - )], - )], - [desc_parameterlist, ( - [desc_parameter, 'Other[T, KT, VT, J, S, L, Q, *V, **P]'], - )], - )], - [desc_content, ()], - )], - )) - - -@pytest.mark.parametrize(('tp_list', 'tptext'), [ - ('[T:int]', '[T: int]'), - ('[T:*Ts]', '[T: *Ts]'), - ('[T:int|(*Ts)]', '[T: int | (*Ts)]'), - ('[T:(*Ts)|int]', '[T: (*Ts) | int]'), - ('[T:(int|(*Ts))]', '[T: (int | (*Ts))]'), - ('[T:((*Ts)|int)]', '[T: ((*Ts) | int)]'), - ('[T:Annotated[int,ctype("char")]]', '[T: Annotated[int, ctype("char")]]'), -]) -def test_pep_695_and_pep_696_whitespaces_in_bound(app, tp_list, tptext): - text = f'.. py:function:: f{tp_list}()' - doctree = restructuredtext.parse(app, text) - assert doctree.astext() == f'\n\nf{tptext}()\n\n' - - -@pytest.mark.parametrize(('tp_list', 'tptext'), [ - ('[T:(int,str)]', '[T: (int, str)]'), - ('[T:(int|str,*Ts)]', '[T: (int | str, *Ts)]'), -]) -def test_pep_695_and_pep_696_whitespaces_in_constraints(app, tp_list, tptext): - text = f'.. py:function:: f{tp_list}()' - doctree = restructuredtext.parse(app, text) - assert doctree.astext() == f'\n\nf{tptext}()\n\n' - - -@pytest.mark.parametrize(('tp_list', 'tptext'), [ - ('[T=int]', '[T = int]'), - ('[T:int=int]', '[T: int = int]'), - ('[*V=*Ts]', '[*V = *Ts]'), - ('[*V=(*Ts)]', '[*V = (*Ts)]'), - ('[*V=*tuple[str,...]]', '[*V = *tuple[str, ...]]'), - ('[*V=*tuple[*Ts,...]]', '[*V = *tuple[*Ts, ...]]'), - ('[*V=*tuple[int,*Ts]]', '[*V = *tuple[int, *Ts]]'), - ('[*V=*tuple[*Ts,int]]', '[*V = *tuple[*Ts, int]]'), - ('[**P=[int,*Ts]]', '[**P = [int, *Ts]]'), - ('[**P=[int, int*3]]', '[**P = [int, int * 3]]'), - ('[**P=[int, *Ts*3]]', '[**P = [int, *Ts * 3]]'), - ('[**P=[int,A[int,ctype("char")]]]', '[**P = [int, A[int, ctype("char")]]]'), -]) -def test_pep_695_and_pep_696_whitespaces_in_default(app, tp_list, tptext): - text = f'.. py:function:: f{tp_list}()' - doctree = restructuredtext.parse(app, text) - assert doctree.astext() == f'\n\nf{tptext}()\n\n' diff --git a/tests/test_domains/__init__.py b/tests/test_domains/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_domains/__init__.py diff --git a/tests/test_domain_c.py b/tests/test_domains/test_domain_c.py index 6582a0c..a8a92cb 100644 --- a/tests/test_domain_c.py +++ b/tests/test_domains/test_domain_c.py @@ -19,17 +19,13 @@ from sphinx.addnodes import ( desc_signature_line, pending_xref, ) -from sphinx.domains.c import ( - DefinitionError, - DefinitionParser, - Symbol, - _id_prefix, - _macroKeywords, - _max_id, -) +from sphinx.domains.c._ids import _id_prefix, _macroKeywords, _max_id +from sphinx.domains.c._parser import DefinitionParser +from sphinx.domains.c._symbol import Symbol from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node +from sphinx.util.cfamily import DefinitionError from sphinx.writers.text import STDINDENT @@ -66,7 +62,7 @@ def _check(name, input, idDict, output, key, asTextOutput): ast = parse(name, inputActual) res = str(ast) if res != outputAst: - print("") + print() print("Input: ", input) print("Result: ", res) print("Expected: ", outputAst) @@ -79,7 +75,7 @@ def _check(name, input, idDict, output, key, asTextOutput): ast.describe_signature(signode, 'lastIsName', symbol, options={}) resAsText = parentNode.astext() if resAsText != outputAsText: - print("") + print() print("Input: ", input) print("astext(): ", resAsText) print("Expected: ", outputAsText) @@ -138,7 +134,7 @@ def test_domain_c_ast_expressions(): output = expr res = str(ast) if res != output: - print("") + print() print("Input: ", input) print("Result: ", res) print("Expected: ", output) @@ -146,7 +142,7 @@ def test_domain_c_ast_expressions(): displayString = ast.get_display_string() if res != displayString: # note: if the expression contains an anon name then this will trigger a falsely - print("") + print() print("Input: ", expr) print("Result: ", res) print("Display: ", displayString) @@ -658,14 +654,14 @@ def extract_role_links(app, filename): @pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) def test_domain_c_build(app, status, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "index") assert len(ws) == 0 @pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) def test_domain_c_build_namespace(app, status, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "namespace") assert len(ws) == 0 t = (app.outdir / "namespace.html").read_text(encoding='utf8') @@ -675,7 +671,7 @@ def test_domain_c_build_namespace(app, status, warning): @pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) def test_domain_c_build_anon_dup_decl(app, status, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "anon-dup-decl") assert len(ws) == 2 assert "WARNING: c:identifier reference target not found: @a" in ws[0] @@ -704,7 +700,7 @@ def test_domain_c_build_semicolon(app, warning): @pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) def test_domain_c_build_function_param_target(app, warning): # the anchor for function parameters should be the function - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "function_param_target") assert len(ws) == 0 entries = extract_role_links(app, "function_param_target.html") @@ -716,14 +712,14 @@ def test_domain_c_build_function_param_target(app, warning): @pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) def test_domain_c_build_ns_lookup(app, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "ns_lookup") assert len(ws) == 0 @pytest.mark.sphinx(testroot='domain-c', confoverrides={'nitpicky': True}) def test_domain_c_build_field_role(app, status, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "field-role") assert len(ws) == 0 @@ -753,7 +749,7 @@ def test_domain_c_build_intersphinx(tmp_path, app, status, warning): .. c:type:: _type .. c:function:: void _functionParam(int param) -""" # noqa: F841 +""" # NoQA: F841 inv_file = tmp_path / 'inventory' inv_file.write_bytes(b'''\ # Sphinx inventory version 2 @@ -773,7 +769,7 @@ _struct c:struct 1 index.html#c.$ - _type c:type 1 index.html#c.$ - _union c:union 1 index.html#c.$ - _var c:member 1 index.html#c.$ - -''')) # noqa: W291 +''')) # NoQA: W291 app.config.intersphinx_mapping = { 'https://localhost/intersphinx/c/': str(inv_file), } @@ -782,7 +778,7 @@ _var c:member 1 index.html#c.$ - normalize_intersphinx_mapping(app, app.config) load_mappings(app) - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "index") assert len(ws) == 0 diff --git a/tests/test_domain_cpp.py b/tests/test_domains/test_domain_cpp.py index dcc2b0f..abd0f82 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domains/test_domain_cpp.py @@ -20,17 +20,13 @@ from sphinx.addnodes import ( desc_signature_line, pending_xref, ) -from sphinx.domains.cpp import ( - DefinitionError, - DefinitionParser, - NoOldIdError, - Symbol, - _id_prefix, - _max_id, -) +from sphinx.domains.cpp._ids import _id_prefix, _max_id +from sphinx.domains.cpp._parser import DefinitionParser +from sphinx.domains.cpp._symbol import Symbol from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node +from sphinx.util.cfamily import DefinitionError, NoOldIdError from sphinx.writers.text import STDINDENT @@ -67,7 +63,7 @@ def _check(name, input, idDict, output, key, asTextOutput): ast = parse(name, inputActual) res = str(ast) if res != outputAst: - print("") + print() print("Input: ", input) print("Result: ", res) print("Expected: ", outputAst) @@ -80,7 +76,7 @@ def _check(name, input, idDict, output, key, asTextOutput): ast.describe_signature(signode, 'lastIsName', symbol, options={}) resAsText = parentNode.astext() if resAsText != outputAsText: - print("") + print() print("Input: ", input) print("astext(): ", resAsText) print("Expected: ", outputAsText) @@ -129,7 +125,7 @@ def check(name, input, idDict, output=None, key=None, asTextOutput=None): @pytest.mark.parametrize(('type_', 'id_v2'), - sphinx.domains.cpp._id_fundamental_v2.items()) + sphinx.domains.cpp._ids._id_fundamental_v2.items()) def test_domain_cpp_ast_fundamental_types(type_, id_v2): # see https://en.cppreference.com/w/cpp/language/types def make_id_v1(): @@ -184,14 +180,14 @@ def test_domain_cpp_ast_expressions(): ast = parser.parse_expression() res = str(ast) if res != expr: - print("") + print() print("Input: ", expr) print("Result: ", res) raise DefinitionError displayString = ast.get_display_string() if res != displayString: # note: if the expression contains an anon name then this will trigger a falsely - print("") + print() print("Input: ", expr) print("Result: ", res) print("Display: ", displayString) @@ -1116,7 +1112,7 @@ def filter_warnings(warning, file): @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) def test_domain_cpp_build_multi_decl_lookup(app, status, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "lookup-key-overload") assert len(ws) == 0 @@ -1126,7 +1122,7 @@ def test_domain_cpp_build_multi_decl_lookup(app, status, warning): @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) def test_domain_cpp_build_warn_template_param_qualified_name(app, status, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "warn-template-param-qualified-name") assert len(ws) == 2 assert "WARNING: cpp:type reference target not found: T::typeWarn" in ws[0] @@ -1135,14 +1131,14 @@ def test_domain_cpp_build_warn_template_param_qualified_name(app, status, warnin @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) def test_domain_cpp_build_backslash_ok_true(app, status, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "backslash") assert len(ws) == 0 @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) def test_domain_cpp_build_semicolon(app, status, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "semicolon") assert len(ws) == 0 @@ -1150,7 +1146,7 @@ def test_domain_cpp_build_semicolon(app, status, warning): @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True, 'strip_signature_backslash': True}) def test_domain_cpp_build_backslash_ok_false(app, status, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "backslash") assert len(ws) == 1 assert "WARNING: Parsing of expression failed. Using fallback parser." in ws[0] @@ -1158,7 +1154,7 @@ def test_domain_cpp_build_backslash_ok_false(app, status, warning): @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) def test_domain_cpp_build_anon_dup_decl(app, status, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "anon-dup-decl") assert len(ws) == 2 assert "WARNING: cpp:identifier reference target not found: @a" in ws[0] @@ -1167,7 +1163,7 @@ def test_domain_cpp_build_anon_dup_decl(app, status, warning): @pytest.mark.sphinx(testroot='domain-cpp') def test_domain_cpp_build_misuse_of_roles(app, status, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "roles-targets-ok") assert len(ws) == 0 @@ -1215,7 +1211,7 @@ def test_domain_cpp_build_misuse_of_roles(app, status, warning): @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'add_function_parentheses': True}) def test_domain_cpp_build_with_add_function_parentheses_is_True(app, status, warning): - app.builder.build_all() + app.build(force_all=True) def check(spec, text, file): pattern = '<li><p>%s<a .*?><code .*?><span .*?>%s</span></code></a></p></li>' % spec @@ -1256,7 +1252,7 @@ def test_domain_cpp_build_with_add_function_parentheses_is_True(app, status, war @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'add_function_parentheses': False}) def test_domain_cpp_build_with_add_function_parentheses_is_False(app, status, warning): - app.builder.build_all() + app.build(force_all=True) def check(spec, text, file): pattern = '<li><p>%s<a .*?><code .*?><span .*?>%s</span></code></a></p></li>' % spec @@ -1297,7 +1293,7 @@ def test_domain_cpp_build_with_add_function_parentheses_is_False(app, status, wa @pytest.mark.sphinx(testroot='domain-cpp') def test_domain_cpp_build_xref_consistency(app, status, warning): - app.builder.build_all() + app.build(force_all=True) test = 'xref_consistency.html' output = (app.outdir / test).read_text(encoding='utf8') @@ -1361,11 +1357,24 @@ not found in `{test}` @pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) def test_domain_cpp_build_field_role(app, status, warning): - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "field-role") assert len(ws) == 0 +@pytest.mark.sphinx(testroot='domain-cpp', confoverrides={'nitpicky': True}) +def test_domain_cpp_build_operator_lookup(app, status, warning): + app.builder.build_all() + ws = filter_warnings(warning, "operator-lookup") + assert len(ws) == 5 + # TODO: the first one should not happen + assert ":10: WARNING: cpp:identifier reference target not found: _lit" in ws[0] + assert ":18: WARNING: cpp:func reference target not found: int h" in ws[1] + assert ":19: WARNING: cpp:func reference target not found: int operator+(bool, bool)" in ws[2] + assert ":20: WARNING: cpp:func reference target not found: int operator\"\"_udl" in ws[3] + assert ":21: WARNING: cpp:func reference target not found: operator bool" in ws[4] + + @pytest.mark.sphinx(testroot='domain-cpp-intersphinx', confoverrides={'nitpicky': True}) def test_domain_cpp_build_intersphinx(tmp_path, app, status, warning): origSource = """\ @@ -1388,7 +1397,7 @@ def test_domain_cpp_build_intersphinx(tmp_path, app, status, warning): .. cpp:enum-class:: _enumClass .. cpp:function:: void _functionParam(int param) .. cpp:function:: template<typename TParam> void _templateParam() -""" # noqa: F841 +""" # NoQA: F841 inv_file = tmp_path / 'inventory' inv_file.write_bytes(b'''\ # Sphinx inventory version 2 @@ -1415,7 +1424,7 @@ _templateParam::TParam cpp:templateParam 1 index.html#_CPPv4I0E14_templateParamv _type cpp:type 1 index.html#_CPPv45$ - _union cpp:union 1 index.html#_CPPv46$ - _var cpp:member 1 index.html#_CPPv44$ - -''')) # noqa: W291 +''')) # NoQA: W291 app.config.intersphinx_mapping = { 'https://localhost/intersphinx/cpp/': str(inv_file), } @@ -1424,7 +1433,7 @@ _var cpp:member 1 index.html#_CPPv44$ - normalize_intersphinx_mapping(app, app.config) load_mappings(app) - app.builder.build_all() + app.build(force_all=True) ws = filter_warnings(warning, "index") assert len(ws) == 0 diff --git a/tests/test_domain_js.py b/tests/test_domains/test_domain_js.py index bf4c3fe..995a440 100644 --- a/tests/test_domain_js.py +++ b/tests/test_domains/test_domain_js.py @@ -28,7 +28,7 @@ from sphinx.writers.text import STDINDENT @pytest.mark.sphinx('dummy', testroot='domain-js') def test_domain_js_xrefs(app, status, warning): """Domain objects have correct prefixes when looking up xrefs""" - app.builder.build_all() + app.build(force_all=True) def assert_refnode(node, mod_name, prefix, target, reftype=None, domain='js'): @@ -83,7 +83,7 @@ def test_domain_js_xrefs(app, status, warning): @pytest.mark.sphinx('dummy', testroot='domain-js') def test_domain_js_objects(app, status, warning): - app.builder.build_all() + app.build(force_all=True) modules = app.env.domains['js'].data['modules'] objects = app.env.domains['js'].data['objects'] @@ -118,7 +118,7 @@ def test_domain_js_find_obj(app, status, warning): return app.env.domains['js'].find_obj( app.env, mod_name, prefix, obj_name, obj_type, searchmode) - app.builder.build_all() + app.build(force_all=True) assert (find_obj(None, None, 'NONEXISTANT', 'class') == (None, None)) assert (find_obj(None, None, 'NestedParentA', 'class') == diff --git a/tests/test_domains/test_domain_py.py b/tests/test_domains/test_domain_py.py new file mode 100644 index 0000000..e653c80 --- /dev/null +++ b/tests/test_domains/test_domain_py.py @@ -0,0 +1,1015 @@ +"""Tests the Python Domain""" + +from __future__ import annotations + +import re +from unittest.mock import Mock + +import docutils.utils +import pytest +from docutils import nodes + +from sphinx import addnodes +from sphinx.addnodes import ( + desc, + desc_annotation, + desc_content, + desc_name, + desc_parameter, + desc_parameterlist, + desc_returns, + desc_sig_keyword, + desc_sig_literal_number, + desc_sig_literal_string, + desc_sig_name, + desc_sig_operator, + desc_sig_punctuation, + desc_sig_space, + desc_signature, + desc_type_parameter, + desc_type_parameter_list, + pending_xref, +) +from sphinx.domains import IndexEntry +from sphinx.domains.python import PythonDomain, PythonModuleIndex +from sphinx.domains.python._annotations import _parse_annotation, _pseudo_parse_arglist +from sphinx.domains.python._object import py_sig_re +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node +from sphinx.writers.text import STDINDENT + + +def parse(sig): + m = py_sig_re.match(sig) + if m is None: + raise ValueError + name_prefix, tp_list, name, arglist, retann = m.groups() + signode = addnodes.desc_signature(sig, '') + _pseudo_parse_arglist(signode, arglist) + return signode.astext() + + +def test_function_signatures(): + rv = parse('func(a=1) -> int object') + assert rv == '(a=1)' + + rv = parse('func(a=1, [b=None])') + assert rv == '(a=1, [b=None])' + + rv = parse('func(a=1[, b=None])') + assert rv == '(a=1, [b=None])' + + rv = parse("compile(source : string, filename, symbol='file')") + assert rv == "(source : string, filename, symbol='file')" + + rv = parse('func(a=[], [b=None])') + assert rv == '(a=[], [b=None])' + + rv = parse('func(a=[][, b=None])') + assert rv == '(a=[], [b=None])' + + +@pytest.mark.sphinx('dummy', testroot='domain-py') +def test_domain_py_xrefs(app, status, warning): + """Domain objects have correct prefixes when looking up xrefs""" + app.build(force_all=True) + + def assert_refnode(node, module_name, class_name, target, reftype=None, + domain='py'): + attributes = { + 'refdomain': domain, + 'reftarget': target, + } + if reftype is not None: + attributes['reftype'] = reftype + if module_name is not False: + attributes['py:module'] = module_name + if class_name is not False: + attributes['py:class'] = class_name + assert_node(node, **attributes) + + doctree = app.env.get_doctree('roles') + refnodes = list(doctree.findall(pending_xref)) + assert_refnode(refnodes[0], None, None, 'TopLevel', 'class') + assert_refnode(refnodes[1], None, None, 'top_level', 'meth') + assert_refnode(refnodes[2], None, 'NestedParentA', 'child_1', 'meth') + assert_refnode(refnodes[3], None, 'NestedParentA', 'NestedChildA.subchild_2', 'meth') + assert_refnode(refnodes[4], None, 'NestedParentA', 'child_2', 'meth') + assert_refnode(refnodes[5], False, 'NestedParentA', 'any_child', domain='') + assert_refnode(refnodes[6], None, 'NestedParentA', 'NestedChildA', 'class') + assert_refnode(refnodes[7], None, 'NestedParentA.NestedChildA', 'subchild_2', 'meth') + assert_refnode(refnodes[8], None, 'NestedParentA.NestedChildA', + 'NestedParentA.child_1', 'meth') + assert_refnode(refnodes[9], None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') + assert_refnode(refnodes[10], None, 'NestedParentB', 'child_1', 'meth') + assert_refnode(refnodes[11], None, 'NestedParentB', 'NestedParentB', 'class') + assert_refnode(refnodes[12], None, None, 'NestedParentA.NestedChildA', 'class') + assert len(refnodes) == 13 + + doctree = app.env.get_doctree('module') + refnodes = list(doctree.findall(pending_xref)) + assert_refnode(refnodes[0], 'module_a.submodule', None, + 'ModTopLevel', 'class') + assert_refnode(refnodes[1], 'module_a.submodule', 'ModTopLevel', + 'mod_child_1', 'meth') + assert_refnode(refnodes[2], 'module_a.submodule', 'ModTopLevel', + 'ModTopLevel.mod_child_1', 'meth') + assert_refnode(refnodes[3], 'module_a.submodule', 'ModTopLevel', + 'mod_child_2', 'meth') + assert_refnode(refnodes[4], 'module_a.submodule', 'ModTopLevel', + 'module_a.submodule.ModTopLevel.mod_child_1', 'meth') + assert_refnode(refnodes[5], 'module_a.submodule', 'ModTopLevel', + 'prop', 'attr') + assert_refnode(refnodes[6], 'module_a.submodule', 'ModTopLevel', + 'prop', 'meth') + assert_refnode(refnodes[7], 'module_b.submodule', None, + 'ModTopLevel', 'class') + assert_refnode(refnodes[8], 'module_b.submodule', 'ModTopLevel', + 'ModNoModule', 'class') + assert_refnode(refnodes[9], False, False, 'int', 'class') + assert_refnode(refnodes[10], False, False, 'tuple', 'class') + assert_refnode(refnodes[11], False, False, 'str', 'class') + assert_refnode(refnodes[12], False, False, 'float', 'class') + assert_refnode(refnodes[13], False, False, 'list', 'class') + assert_refnode(refnodes[14], False, False, 'ModTopLevel', 'class') + assert_refnode(refnodes[15], False, False, 'index', 'doc', domain='std') + assert_refnode(refnodes[16], False, False, 'typing.Literal', 'obj', domain='py') + assert_refnode(refnodes[17], False, False, 'typing.Literal', 'obj', domain='py') + assert len(refnodes) == 18 + + doctree = app.env.get_doctree('module_option') + refnodes = list(doctree.findall(pending_xref)) + print(refnodes) + print(refnodes[0]) + print(refnodes[1]) + assert_refnode(refnodes[0], 'test.extra', 'B', 'foo', 'meth') + assert_refnode(refnodes[1], 'test.extra', 'B', 'foo', 'meth') + assert len(refnodes) == 2 + + +@pytest.mark.sphinx('html', testroot='domain-py') +def test_domain_py_xrefs_abbreviations(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'abbr.html').read_text(encoding='utf8') + assert re.search(r'normal: <a .* href="module.html#module_a.submodule.ModTopLevel.' + r'mod_child_1" .*><.*>module_a.submodule.ModTopLevel.mod_child_1\(\)' + r'<.*></a>', + content) + assert re.search(r'relative: <a .* href="module.html#module_a.submodule.ModTopLevel.' + r'mod_child_1" .*><.*>ModTopLevel.mod_child_1\(\)<.*></a>', + content) + assert re.search(r'short name: <a .* href="module.html#module_a.submodule.ModTopLevel.' + r'mod_child_1" .*><.*>mod_child_1\(\)<.*></a>', + content) + assert re.search(r'relative \+ short name: <a .* href="module.html#module_a.submodule.' + r'ModTopLevel.mod_child_1" .*><.*>mod_child_1\(\)<.*></a>', + content) + assert re.search(r'short name \+ relative: <a .* href="module.html#module_a.submodule.' + r'ModTopLevel.mod_child_1" .*><.*>mod_child_1\(\)<.*></a>', + content) + + +@pytest.mark.sphinx('dummy', testroot='domain-py') +def test_domain_py_objects(app, status, warning): + app.build(force_all=True) + + modules = app.env.domains['py'].data['modules'] + objects = app.env.domains['py'].data['objects'] + + assert 'module_a.submodule' in modules + assert 'module_a.submodule' in objects + assert 'module_b.submodule' in modules + assert 'module_b.submodule' in objects + + assert objects['module_a.submodule.ModTopLevel'][2] == 'class' + assert objects['module_a.submodule.ModTopLevel.mod_child_1'][2] == 'method' + assert objects['module_a.submodule.ModTopLevel.mod_child_2'][2] == 'method' + assert 'ModTopLevel.ModNoModule' not in objects + assert objects['ModNoModule'][2] == 'class' + assert objects['module_b.submodule.ModTopLevel'][2] == 'class' + + assert objects['TopLevel'][2] == 'class' + assert objects['top_level'][2] == 'method' + assert objects['NestedParentA'][2] == 'class' + assert objects['NestedParentA.child_1'][2] == 'method' + assert objects['NestedParentA.any_child'][2] == 'method' + assert objects['NestedParentA.NestedChildA'][2] == 'class' + assert objects['NestedParentA.NestedChildA.subchild_1'][2] == 'method' + assert objects['NestedParentA.NestedChildA.subchild_2'][2] == 'method' + assert objects['NestedParentA.child_2'][2] == 'method' + assert objects['NestedParentB'][2] == 'class' + assert objects['NestedParentB.child_1'][2] == 'method' + + +@pytest.mark.sphinx('html', testroot='domain-py') +def test_resolve_xref_for_properties(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'module.html').read_text(encoding='utf8') + assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"' + ' title="module_a.submodule.ModTopLevel.prop">' + '<code class="xref py py-attr docutils literal notranslate"><span class="pre">' + 'prop</span> <span class="pre">attribute</span></code></a>' in content) + assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"' + ' title="module_a.submodule.ModTopLevel.prop">' + '<code class="xref py py-meth docutils literal notranslate"><span class="pre">' + 'prop</span> <span class="pre">method</span></code></a>' in content) + assert ('Link to <a class="reference internal" href="#module_a.submodule.ModTopLevel.prop"' + ' title="module_a.submodule.ModTopLevel.prop">' + '<code class="xref py py-attr docutils literal notranslate"><span class="pre">' + 'prop</span> <span class="pre">attribute</span></code></a>' in content) + + +@pytest.mark.sphinx('dummy', testroot='domain-py') +def test_domain_py_find_obj(app, status, warning): + + def find_obj(modname, prefix, obj_name, obj_type, searchmode=0): + return app.env.domains['py'].find_obj( + app.env, modname, prefix, obj_name, obj_type, searchmode) + + app.build(force_all=True) + + assert (find_obj(None, None, 'NONEXISTANT', 'class') == []) + assert (find_obj(None, None, 'NestedParentA', 'class') == + [('NestedParentA', ('roles', 'NestedParentA', 'class', False))]) + assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') == + [('NestedParentA.NestedChildA', + ('roles', 'NestedParentA.NestedChildA', 'class', False))]) + assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') == + [('NestedParentA.NestedChildA', + ('roles', 'NestedParentA.NestedChildA', 'class', False))]) + assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'meth') == + [('NestedParentA.NestedChildA.subchild_1', + ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))]) + assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') == + [('NestedParentA.NestedChildA.subchild_1', + ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))]) + assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'meth') == + [('NestedParentA.NestedChildA.subchild_1', + ('roles', 'NestedParentA.NestedChildA.subchild_1', 'method', False))]) + + +def test_get_full_qualified_name(): + env = Mock(domaindata={}) + domain = PythonDomain(env) + + # non-python references + node = nodes.reference() + assert domain.get_full_qualified_name(node) is None + + # simple reference + node = nodes.reference(reftarget='func') + assert domain.get_full_qualified_name(node) == 'func' + + # with py:module context + kwargs = {'py:module': 'module1'} + node = nodes.reference(reftarget='func', **kwargs) + assert domain.get_full_qualified_name(node) == 'module1.func' + + # with py:class context + kwargs = {'py:class': 'Class'} + node = nodes.reference(reftarget='func', **kwargs) + assert domain.get_full_qualified_name(node) == 'Class.func' + + # with both py:module and py:class context + kwargs = {'py:module': 'module1', 'py:class': 'Class'} + node = nodes.reference(reftarget='func', **kwargs) + assert domain.get_full_qualified_name(node) == 'module1.Class.func' + + +def test_parse_annotation(app): + doctree = _parse_annotation("int", app.env) + assert_node(doctree, ([pending_xref, "int"],)) + assert_node(doctree[0], pending_xref, refdomain="py", reftype="class", reftarget="int") + + doctree = _parse_annotation("List[int]", app.env) + assert_node(doctree, ([pending_xref, "List"], + [desc_sig_punctuation, "["], + [pending_xref, "int"], + [desc_sig_punctuation, "]"])) + + doctree = _parse_annotation("Tuple[int, int]", app.env) + assert_node(doctree, ([pending_xref, "Tuple"], + [desc_sig_punctuation, "["], + [pending_xref, "int"], + [desc_sig_punctuation, ","], + desc_sig_space, + [pending_xref, "int"], + [desc_sig_punctuation, "]"])) + + doctree = _parse_annotation("Tuple[()]", app.env) + assert_node(doctree, ([pending_xref, "Tuple"], + [desc_sig_punctuation, "["], + [desc_sig_punctuation, "("], + [desc_sig_punctuation, ")"], + [desc_sig_punctuation, "]"])) + + doctree = _parse_annotation("Tuple[int, ...]", app.env) + assert_node(doctree, ([pending_xref, "Tuple"], + [desc_sig_punctuation, "["], + [pending_xref, "int"], + [desc_sig_punctuation, ","], + desc_sig_space, + [desc_sig_punctuation, "..."], + [desc_sig_punctuation, "]"])) + + doctree = _parse_annotation("Callable[[int, int], int]", app.env) + assert_node(doctree, ([pending_xref, "Callable"], + [desc_sig_punctuation, "["], + [desc_sig_punctuation, "["], + [pending_xref, "int"], + [desc_sig_punctuation, ","], + desc_sig_space, + [pending_xref, "int"], + [desc_sig_punctuation, "]"], + [desc_sig_punctuation, ","], + desc_sig_space, + [pending_xref, "int"], + [desc_sig_punctuation, "]"])) + + doctree = _parse_annotation("Callable[[], int]", app.env) + assert_node(doctree, ([pending_xref, "Callable"], + [desc_sig_punctuation, "["], + [desc_sig_punctuation, "["], + [desc_sig_punctuation, "]"], + [desc_sig_punctuation, ","], + desc_sig_space, + [pending_xref, "int"], + [desc_sig_punctuation, "]"])) + + doctree = _parse_annotation("List[None]", app.env) + assert_node(doctree, ([pending_xref, "List"], + [desc_sig_punctuation, "["], + [pending_xref, "None"], + [desc_sig_punctuation, "]"])) + + # None type makes an object-reference (not a class reference) + doctree = _parse_annotation("None", app.env) + assert_node(doctree, ([pending_xref, "None"],)) + assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="None") + + # Literal type makes an object-reference (not a class reference) + doctree = _parse_annotation("typing.Literal['a', 'b']", app.env) + assert_node(doctree, ([pending_xref, "Literal"], + [desc_sig_punctuation, "["], + [desc_sig_literal_string, "'a'"], + [desc_sig_punctuation, ","], + desc_sig_space, + [desc_sig_literal_string, "'b'"], + [desc_sig_punctuation, "]"])) + assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="typing.Literal") + + +def test_parse_annotation_suppress(app): + doctree = _parse_annotation("~typing.Dict[str, str]", app.env) + assert_node(doctree, ([pending_xref, "Dict"], + [desc_sig_punctuation, "["], + [pending_xref, "str"], + [desc_sig_punctuation, ","], + desc_sig_space, + [pending_xref, "str"], + [desc_sig_punctuation, "]"])) + assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="typing.Dict") + + +def test_parse_annotation_Literal(app): + doctree = _parse_annotation("Literal[True, False]", app.env) + assert_node(doctree, ([pending_xref, "Literal"], + [desc_sig_punctuation, "["], + [desc_sig_keyword, "True"], + [desc_sig_punctuation, ","], + desc_sig_space, + [desc_sig_keyword, "False"], + [desc_sig_punctuation, "]"])) + + doctree = _parse_annotation("typing.Literal[0, 1, 'abc']", app.env) + assert_node(doctree, ([pending_xref, "Literal"], + [desc_sig_punctuation, "["], + [desc_sig_literal_number, "0"], + [desc_sig_punctuation, ","], + desc_sig_space, + [desc_sig_literal_number, "1"], + [desc_sig_punctuation, ","], + desc_sig_space, + [desc_sig_literal_string, "'abc'"], + [desc_sig_punctuation, "]"])) + + +@pytest.mark.sphinx(freshenv=True) +def test_module_index(app): + text = (".. py:module:: docutils\n" + ".. py:module:: sphinx\n" + ".. py:module:: sphinx.config\n" + ".. py:module:: sphinx.builders\n" + ".. py:module:: sphinx.builders.html\n" + ".. py:module:: sphinx_intl\n") + restructuredtext.parse(app, text) + index = PythonModuleIndex(app.env.get_domain('py')) + assert index.generate() == ( + [('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), + ('s', [IndexEntry('sphinx', 1, 'index', 'module-sphinx', '', '', ''), + IndexEntry('sphinx.builders', 2, 'index', 'module-sphinx.builders', '', '', ''), + IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', ''), + IndexEntry('sphinx.config', 2, 'index', 'module-sphinx.config', '', '', ''), + IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])], + False, + ) + + +@pytest.mark.sphinx(freshenv=True) +def test_module_index_submodule(app): + text = ".. py:module:: sphinx.config\n" + restructuredtext.parse(app, text) + index = PythonModuleIndex(app.env.get_domain('py')) + assert index.generate() == ( + [('s', [IndexEntry('sphinx', 1, '', '', '', '', ''), + IndexEntry('sphinx.config', 2, 'index', 'module-sphinx.config', '', '', '')])], + False, + ) + + +@pytest.mark.sphinx(freshenv=True) +def test_module_index_not_collapsed(app): + text = (".. py:module:: docutils\n" + ".. py:module:: sphinx\n") + restructuredtext.parse(app, text) + index = PythonModuleIndex(app.env.get_domain('py')) + assert index.generate() == ( + [('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), + ('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', '')])], + True, + ) + + +@pytest.mark.sphinx(freshenv=True, confoverrides={'modindex_common_prefix': ['sphinx.']}) +def test_modindex_common_prefix(app): + text = (".. py:module:: docutils\n" + ".. py:module:: sphinx\n" + ".. py:module:: sphinx.config\n" + ".. py:module:: sphinx.builders\n" + ".. py:module:: sphinx.builders.html\n" + ".. py:module:: sphinx_intl\n") + restructuredtext.parse(app, text) + index = PythonModuleIndex(app.env.get_domain('py')) + assert index.generate() == ( + [('b', [IndexEntry('sphinx.builders', 1, 'index', 'module-sphinx.builders', '', '', ''), + IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', '')]), + ('c', [IndexEntry('sphinx.config', 0, 'index', 'module-sphinx.config', '', '', '')]), + ('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]), + ('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', ''), + IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])], + True, + ) + + +def test_no_index_entry(app): + text = (".. py:function:: f()\n" + ".. py:function:: g()\n" + " :no-index-entry:\n") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, desc, addnodes.index, desc)) + assert_node(doctree[0], addnodes.index, entries=[('pair', 'built-in function; f()', 'f', '', None)]) + assert_node(doctree[2], addnodes.index, entries=[]) + + text = (".. py:class:: f\n" + ".. py:class:: g\n" + " :no-index-entry:\n") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, desc, addnodes.index, desc)) + assert_node(doctree[0], addnodes.index, entries=[('single', 'f (built-in class)', 'f', '', None)]) + assert_node(doctree[2], addnodes.index, entries=[]) + + +@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names') +def test_python_python_use_unqualified_type_names(app, status, warning): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<span class="n"><a class="reference internal" href="#foo.Name" title="foo.Name">' + '<span class="pre">Name</span></a></span>' in content) + assert '<span class="n"><span class="pre">foo.Age</span></span>' in content + assert ('<p><strong>name</strong> (<a class="reference internal" href="#foo.Name" ' + 'title="foo.Name"><em>Name</em></a>) – blah blah</p>' in content) + assert '<p><strong>age</strong> (<em>foo.Age</em>) – blah blah</p>' in content + + +@pytest.mark.sphinx('html', testroot='domain-py-python_use_unqualified_type_names', + confoverrides={'python_use_unqualified_type_names': False}) +def test_python_python_use_unqualified_type_names_disabled(app, status, warning): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<span class="n"><a class="reference internal" href="#foo.Name" title="foo.Name">' + '<span class="pre">foo.Name</span></a></span>' in content) + assert '<span class="n"><span class="pre">foo.Age</span></span>' in content + assert ('<p><strong>name</strong> (<a class="reference internal" href="#foo.Name" ' + 'title="foo.Name"><em>foo.Name</em></a>) – blah blah</p>' in content) + assert '<p><strong>age</strong> (<em>foo.Age</em>) – blah blah</p>' in content + + +@pytest.mark.sphinx('dummy', testroot='domain-py-xref-warning') +def test_warn_missing_reference(app, status, warning): + app.build() + assert "index.rst:6: WARNING: undefined label: 'no-label'" in warning.getvalue() + assert ("index.rst:6: WARNING: Failed to create a cross reference. " + "A title or caption not found: 'existing-label'") in warning.getvalue() + + +@pytest.mark.sphinx(confoverrides={'nitpicky': True}) +@pytest.mark.parametrize('include_options', [True, False]) +def test_signature_line_number(app, include_options): + text = (".. py:function:: foo(bar : string)\n" + + (" :no-index-entry:\n" if include_options else "")) + doc = restructuredtext.parse(app, text) + xrefs = list(doc.findall(condition=addnodes.pending_xref)) + assert len(xrefs) == 1 + source, line = docutils.utils.get_source_line(xrefs[0]) + assert 'index.rst' in source + assert line == 1 + + +@pytest.mark.sphinx( + 'html', + confoverrides={ + 'python_maximum_signature_line_length': len("hello(name: str) -> str"), + 'maximum_signature_line_length': 1, + }, +) +def test_python_maximum_signature_line_length_overrides_global(app): + text = ".. py:function:: hello(name: str) -> str" + doctree = restructuredtext.parse(app, text) + expected_doctree = (addnodes.index, + [desc, ([desc_signature, ([desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "str"])], + desc_content)]) + assert_node(doctree, expected_doctree) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", no_index=False) + signame_node = [desc_sig_name, "name"] + expected_sig = [desc_parameterlist, desc_parameter, (signame_node, + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"])] + assert_node(doctree[1][0][1], expected_sig) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx( + 'html', testroot='domain-py-python_maximum_signature_line_length', +) +def test_domain_py_python_maximum_signature_line_length_in_html(app, status, warning): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + expected_parameter_list_hello = """\ + +<dl> +<dd>\ +<em class="sig-param">\ +<span class="n"><span class="pre">name</span></span>\ +<span class="p"><span class="pre">:</span></span>\ +<span class="w"> </span>\ +<span class="n"><span class="pre">str</span></span>\ +</em>,\ +</dd> +</dl> + +<span class="sig-paren">)</span> \ +<span class="sig-return">\ +<span class="sig-return-icon">→</span> \ +<span class="sig-return-typehint"><span class="pre">str</span></span>\ +</span>\ +<a class="headerlink" href="#hello" title="Link to this definition">¶</a>\ +</dt>\ +""" + assert expected_parameter_list_hello in content + + param_line_fmt = '<dd>{}</dd>\n' + param_name_fmt = ( + '<em class="sig-param"><span class="n"><span class="pre">{}</span></span></em>' + ) + optional_fmt = '<span class="optional">{}</span>' + + expected_a = param_line_fmt.format( + optional_fmt.format("[") + param_name_fmt.format("a") + "," + optional_fmt.format("["), + ) + assert expected_a in content + + expected_b = param_line_fmt.format( + param_name_fmt.format("b") + "," + optional_fmt.format("]") + optional_fmt.format("]"), + ) + assert expected_b in content + + expected_c = param_line_fmt.format(param_name_fmt.format("c") + ",") + assert expected_c in content + + expected_d = param_line_fmt.format(param_name_fmt.format("d") + optional_fmt.format("[") + ",") + assert expected_d in content + + expected_e = param_line_fmt.format(param_name_fmt.format("e") + ",") + assert expected_e in content + + expected_f = param_line_fmt.format(param_name_fmt.format("f") + "," + optional_fmt.format("]")) + assert expected_f in content + + expected_parameter_list_foo = """\ + +<dl> +{}{}{}{}{}{}</dl> + +<span class="sig-paren">)</span>\ +<a class="headerlink" href="#foo" title="Link to this definition">¶</a>\ +</dt>\ +""".format(expected_a, expected_b, expected_c, expected_d, expected_e, expected_f) + assert expected_parameter_list_foo in content + + +@pytest.mark.sphinx( + 'text', testroot='domain-py-python_maximum_signature_line_length', +) +def test_domain_py_python_maximum_signature_line_length_in_text(app, status, warning): + app.build() + content = (app.outdir / 'index.txt').read_text(encoding='utf8') + param_line_fmt = STDINDENT * " " + "{}\n" + + expected_parameter_list_hello = "(\n{}) -> str".format(param_line_fmt.format("name: str,")) + + assert expected_parameter_list_hello in content + + expected_a = param_line_fmt.format("[a,[") + assert expected_a in content + + expected_b = param_line_fmt.format("b,]]") + assert expected_b in content + + expected_c = param_line_fmt.format("c,") + assert expected_c in content + + expected_d = param_line_fmt.format("d[,") + assert expected_d in content + + expected_e = param_line_fmt.format("e,") + assert expected_e in content + + expected_f = param_line_fmt.format("f,]") + assert expected_f in content + + expected_parameter_list_foo = "(\n{}{}{}{}{}{})".format( + expected_a, expected_b, expected_c, expected_d, expected_e, expected_f, + ) + assert expected_parameter_list_foo in content + + +def test_module_content_line_number(app): + text = (".. py:module:: foo\n" + + "\n" + + " Some link here: :ref:`abc`\n") + doc = restructuredtext.parse(app, text) + xrefs = list(doc.findall(condition=addnodes.pending_xref)) + assert len(xrefs) == 1 + source, line = docutils.utils.get_source_line(xrefs[0]) + assert 'index.rst' in source + assert line == 3 + + +@pytest.mark.sphinx(freshenv=True, confoverrides={'python_display_short_literal_types': True}) +def test_short_literal_types(app): + text = """\ +.. py:function:: literal_ints(x: Literal[1, 2, 3] = 1) -> None +.. py:function:: literal_union(x: Union[Literal["a"], Literal["b"], Literal["c"]]) -> None +""" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, 'literal_ints'], + [desc_parameterlist, ( + [desc_parameter, ( + [desc_sig_name, 'x'], + [desc_sig_punctuation, ':'], + desc_sig_space, + [desc_sig_name, ( + [desc_sig_literal_number, '1'], + desc_sig_space, + [desc_sig_punctuation, '|'], + desc_sig_space, + [desc_sig_literal_number, '2'], + desc_sig_space, + [desc_sig_punctuation, '|'], + desc_sig_space, + [desc_sig_literal_number, '3'], + )], + desc_sig_space, + [desc_sig_operator, '='], + desc_sig_space, + [nodes.inline, '1'], + )], + )], + [desc_returns, pending_xref, 'None'], + )], + [desc_content, ()], + )], + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, 'literal_union'], + [desc_parameterlist, ( + [desc_parameter, ( + [desc_sig_name, 'x'], + [desc_sig_punctuation, ':'], + desc_sig_space, + [desc_sig_name, ( + [desc_sig_literal_string, "'a'"], + desc_sig_space, + [desc_sig_punctuation, '|'], + desc_sig_space, + [desc_sig_literal_string, "'b'"], + desc_sig_space, + [desc_sig_punctuation, '|'], + desc_sig_space, + [desc_sig_literal_string, "'c'"], + )], + )], + )], + [desc_returns, pending_xref, 'None'], + )], + [desc_content, ()], + )], + )) + + +def test_function_pep_695(app): + text = """.. py:function:: func[\ + S,\ + T: int,\ + U: (int, str),\ + R: int | int,\ + A: int | Annotated[int, ctype("char")],\ + *V,\ + **P\ + ] + """ + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, 'func'], + [desc_type_parameter_list, ( + [desc_type_parameter, ([desc_sig_name, 'S'])], + [desc_type_parameter, ( + [desc_sig_name, 'T'], + [desc_sig_punctuation, ':'], + desc_sig_space, + [desc_sig_name, ([pending_xref, 'int'])], + )], + [desc_type_parameter, ( + [desc_sig_name, 'U'], + [desc_sig_punctuation, ':'], + desc_sig_space, + [desc_sig_punctuation, '('], + [desc_sig_name, ( + [pending_xref, 'int'], + [desc_sig_punctuation, ','], + desc_sig_space, + [pending_xref, 'str'], + )], + [desc_sig_punctuation, ')'], + )], + [desc_type_parameter, ( + [desc_sig_name, 'R'], + [desc_sig_punctuation, ':'], + desc_sig_space, + [desc_sig_name, ( + [pending_xref, 'int'], + desc_sig_space, + [desc_sig_punctuation, '|'], + desc_sig_space, + [pending_xref, 'int'], + )], + )], + [desc_type_parameter, ( + [desc_sig_name, 'A'], + [desc_sig_punctuation, ':'], + desc_sig_space, + [desc_sig_name, ([pending_xref, 'int | Annotated[int, ctype("char")]'])], + )], + [desc_type_parameter, ( + [desc_sig_operator, '*'], + [desc_sig_name, 'V'], + )], + [desc_type_parameter, ( + [desc_sig_operator, '**'], + [desc_sig_name, 'P'], + )], + )], + [desc_parameterlist, ()], + )], + [desc_content, ()], + )], + )) + + +def test_class_def_pep_695(app): + # Non-concrete unbound generics are allowed at runtime but type checkers + # should fail (https://peps.python.org/pep-0695/#type-parameter-scopes) + text = """.. py:class:: Class[S: Sequence[T], T, KT, VT](Dict[KT, VT])""" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_annotation, ('class', desc_sig_space)], + [desc_name, 'Class'], + [desc_type_parameter_list, ( + [desc_type_parameter, ( + [desc_sig_name, 'S'], + [desc_sig_punctuation, ':'], + desc_sig_space, + [desc_sig_name, ( + [pending_xref, 'Sequence'], + [desc_sig_punctuation, '['], + [pending_xref, 'T'], + [desc_sig_punctuation, ']'], + )], + )], + [desc_type_parameter, ([desc_sig_name, 'T'])], + [desc_type_parameter, ([desc_sig_name, 'KT'])], + [desc_type_parameter, ([desc_sig_name, 'VT'])], + )], + [desc_parameterlist, ([desc_parameter, 'Dict[KT, VT]'])], + )], + [desc_content, ()], + )], + )) + + +def test_class_def_pep_696(app): + # test default values for type variables without using PEP 696 AST parser + text = """.. py:class:: Class[\ + T, KT, VT,\ + J: int,\ + K = list,\ + S: str = str,\ + L: (T, tuple[T, ...], collections.abc.Iterable[T]) = set[T],\ + Q: collections.abc.Mapping[KT, VT] = dict[KT, VT],\ + *V = *tuple[*Ts, bool],\ + **P = [int, Annotated[int, ValueRange(3, 10), ctype("char")]]\ + ](Other[T, KT, VT, J, S, L, Q, *V, **P]) + """ + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_annotation, ('class', desc_sig_space)], + [desc_name, 'Class'], + [desc_type_parameter_list, ( + [desc_type_parameter, ([desc_sig_name, 'T'])], + [desc_type_parameter, ([desc_sig_name, 'KT'])], + [desc_type_parameter, ([desc_sig_name, 'VT'])], + # J: int + [desc_type_parameter, ( + [desc_sig_name, 'J'], + [desc_sig_punctuation, ':'], + desc_sig_space, + [desc_sig_name, ([pending_xref, 'int'])], + )], + # K = list + [desc_type_parameter, ( + [desc_sig_name, 'K'], + desc_sig_space, + [desc_sig_operator, '='], + desc_sig_space, + [nodes.inline, 'list'], + )], + # S: str = str + [desc_type_parameter, ( + [desc_sig_name, 'S'], + [desc_sig_punctuation, ':'], + desc_sig_space, + [desc_sig_name, ([pending_xref, 'str'])], + desc_sig_space, + [desc_sig_operator, '='], + desc_sig_space, + [nodes.inline, 'str'], + )], + [desc_type_parameter, ( + [desc_sig_name, 'L'], + [desc_sig_punctuation, ':'], + desc_sig_space, + [desc_sig_punctuation, '('], + [desc_sig_name, ( + # T + [pending_xref, 'T'], + [desc_sig_punctuation, ','], + desc_sig_space, + # tuple[T, ...] + [pending_xref, 'tuple'], + [desc_sig_punctuation, '['], + [pending_xref, 'T'], + [desc_sig_punctuation, ','], + desc_sig_space, + [desc_sig_punctuation, '...'], + [desc_sig_punctuation, ']'], + [desc_sig_punctuation, ','], + desc_sig_space, + # collections.abc.Iterable[T] + [pending_xref, 'collections.abc.Iterable'], + [desc_sig_punctuation, '['], + [pending_xref, 'T'], + [desc_sig_punctuation, ']'], + )], + [desc_sig_punctuation, ')'], + desc_sig_space, + [desc_sig_operator, '='], + desc_sig_space, + [nodes.inline, 'set[T]'], + )], + [desc_type_parameter, ( + [desc_sig_name, 'Q'], + [desc_sig_punctuation, ':'], + desc_sig_space, + [desc_sig_name, ( + [pending_xref, 'collections.abc.Mapping'], + [desc_sig_punctuation, '['], + [pending_xref, 'KT'], + [desc_sig_punctuation, ','], + desc_sig_space, + [pending_xref, 'VT'], + [desc_sig_punctuation, ']'], + )], + desc_sig_space, + [desc_sig_operator, '='], + desc_sig_space, + [nodes.inline, 'dict[KT, VT]'], + )], + [desc_type_parameter, ( + [desc_sig_operator, '*'], + [desc_sig_name, 'V'], + desc_sig_space, + [desc_sig_operator, '='], + desc_sig_space, + [nodes.inline, '*tuple[*Ts, bool]'], + )], + [desc_type_parameter, ( + [desc_sig_operator, '**'], + [desc_sig_name, 'P'], + desc_sig_space, + [desc_sig_operator, '='], + desc_sig_space, + [nodes.inline, '[int, Annotated[int, ValueRange(3, 10), ctype("char")]]'], + )], + )], + [desc_parameterlist, ( + [desc_parameter, 'Other[T, KT, VT, J, S, L, Q, *V, **P]'], + )], + )], + [desc_content, ()], + )], + )) + + +@pytest.mark.parametrize(('tp_list', 'tptext'), [ + ('[T:int]', '[T: int]'), + ('[T:*Ts]', '[T: *Ts]'), + ('[T:int|(*Ts)]', '[T: int | (*Ts)]'), + ('[T:(*Ts)|int]', '[T: (*Ts) | int]'), + ('[T:(int|(*Ts))]', '[T: (int | (*Ts))]'), + ('[T:((*Ts)|int)]', '[T: ((*Ts) | int)]'), + ('[T:Annotated[int,ctype("char")]]', '[T: Annotated[int, ctype("char")]]'), +]) +def test_pep_695_and_pep_696_whitespaces_in_bound(app, tp_list, tptext): + text = f'.. py:function:: f{tp_list}()' + doctree = restructuredtext.parse(app, text) + assert doctree.astext() == f'\n\nf{tptext}()\n\n' + + +@pytest.mark.parametrize(('tp_list', 'tptext'), [ + ('[T:(int,str)]', '[T: (int, str)]'), + ('[T:(int|str,*Ts)]', '[T: (int | str, *Ts)]'), +]) +def test_pep_695_and_pep_696_whitespaces_in_constraints(app, tp_list, tptext): + text = f'.. py:function:: f{tp_list}()' + doctree = restructuredtext.parse(app, text) + assert doctree.astext() == f'\n\nf{tptext}()\n\n' + + +@pytest.mark.parametrize(('tp_list', 'tptext'), [ + ('[T=int]', '[T = int]'), + ('[T:int=int]', '[T: int = int]'), + ('[*V=*Ts]', '[*V = *Ts]'), + ('[*V=(*Ts)]', '[*V = (*Ts)]'), + ('[*V=*tuple[str,...]]', '[*V = *tuple[str, ...]]'), + ('[*V=*tuple[*Ts,...]]', '[*V = *tuple[*Ts, ...]]'), + ('[*V=*tuple[int,*Ts]]', '[*V = *tuple[int, *Ts]]'), + ('[*V=*tuple[*Ts,int]]', '[*V = *tuple[*Ts, int]]'), + ('[**P=[int,*Ts]]', '[**P = [int, *Ts]]'), + ('[**P=[int, int*3]]', '[**P = [int, int * 3]]'), + ('[**P=[int, *Ts*3]]', '[**P = [int, *Ts * 3]]'), + ('[**P=[int,A[int,ctype("char")]]]', '[**P = [int, A[int, ctype("char")]]]'), +]) +def test_pep_695_and_pep_696_whitespaces_in_default(app, tp_list, tptext): + text = f'.. py:function:: f{tp_list}()' + doctree = restructuredtext.parse(app, text) + assert doctree.astext() == f'\n\nf{tptext}()\n\n' diff --git a/tests/test_domains/test_domain_py_canonical.py b/tests/test_domains/test_domain_py_canonical.py new file mode 100644 index 0000000..3635cd1 --- /dev/null +++ b/tests/test_domains/test_domain_py_canonical.py @@ -0,0 +1,77 @@ +"""Tests the Python Domain""" + +from __future__ import annotations + +import pytest + +from sphinx import addnodes +from sphinx.addnodes import ( + desc, + desc_addname, + desc_annotation, + desc_content, + desc_name, + desc_sig_space, + desc_signature, +) +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node + + +@pytest.mark.sphinx('html', testroot='domain-py', freshenv=True) +def test_domain_py_canonical(app, status, warning): + app.build(force_all=True) + + content = (app.outdir / 'canonical.html').read_text(encoding='utf8') + assert ('<a class="reference internal" href="#canonical.Foo" title="canonical.Foo">' + '<code class="xref py py-class docutils literal notranslate">' + '<span class="pre">Foo</span></code></a>' in content) + assert warning.getvalue() == '' + + +def test_canonical(app): + text = (".. py:class:: io.StringIO\n" + " :canonical: _io.StringIO") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], + [desc_addname, "io."], + [desc_name, "StringIO"])], + desc_content)])) + assert 'io.StringIO' in domain.objects + assert domain.objects['io.StringIO'] == ('index', 'io.StringIO', 'class', False) + assert domain.objects['_io.StringIO'] == ('index', 'io.StringIO', 'class', True) + + +def test_canonical_definition_overrides(app, warning): + text = (".. py:class:: io.StringIO\n" + " :canonical: _io.StringIO\n" + ".. py:class:: _io.StringIO\n") + restructuredtext.parse(app, text) + assert warning.getvalue() == "" + + domain = app.env.get_domain('py') + assert domain.objects['_io.StringIO'] == ('index', 'id0', 'class', False) + + +def test_canonical_definition_skip(app, warning): + text = (".. py:class:: _io.StringIO\n" + ".. py:class:: io.StringIO\n" + " :canonical: _io.StringIO\n") + + restructuredtext.parse(app, text) + assert warning.getvalue() == "" + + domain = app.env.get_domain('py') + assert domain.objects['_io.StringIO'] == ('index', 'io.StringIO', 'class', False) + + +def test_canonical_duplicated(app, warning): + text = (".. py:class:: mypackage.StringIO\n" + " :canonical: _io.StringIO\n" + ".. py:class:: io.StringIO\n" + " :canonical: _io.StringIO\n") + + restructuredtext.parse(app, text) + assert warning.getvalue() != "" diff --git a/tests/test_domains/test_domain_py_fields.py b/tests/test_domains/test_domain_py_fields.py new file mode 100644 index 0000000..47c40f5 --- /dev/null +++ b/tests/test_domains/test_domain_py_fields.py @@ -0,0 +1,326 @@ +"""Tests the Python Domain""" + +from __future__ import annotations + +from docutils import nodes + +from sphinx import addnodes +from sphinx.addnodes import ( + desc, + desc_addname, + desc_annotation, + desc_content, + desc_name, + desc_sig_punctuation, + desc_sig_space, + desc_signature, + pending_xref, +) +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node + + +def test_info_field_list(app): + text = (".. py:module:: example\n" + ".. py:class:: Class\n" + "\n" + " :meta blah: this meta-field must not show up in the toc-tree\n" + " :param str name: blah blah\n" + " :meta another meta field:\n" + " :param age: blah blah\n" + " :type age: int\n" + " :param items: blah blah\n" + " :type items: Tuple[str, ...]\n" + " :param Dict[str, str] params: blah blah\n") + doctree = restructuredtext.parse(app, text) + print(doctree) + + assert_node(doctree, (addnodes.index, + addnodes.index, + nodes.target, + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], + [desc_addname, "example."], + [desc_name, "Class"])], + [desc_content, nodes.field_list, nodes.field])])) + assert_node(doctree[3][1][0][0], + ([nodes.field_name, "Parameters"], + [nodes.field_body, nodes.bullet_list, ([nodes.list_item, nodes.paragraph], + [nodes.list_item, nodes.paragraph], + [nodes.list_item, nodes.paragraph], + [nodes.list_item, nodes.paragraph])])) + + # :param str name: + assert_node(doctree[3][1][0][0][1][0][0][0], + ([addnodes.literal_strong, "name"], + " (", + [pending_xref, addnodes.literal_emphasis, "str"], + ")", + " -- ", + "blah blah")) + assert_node(doctree[3][1][0][0][1][0][0][0][2], pending_xref, + refdomain="py", reftype="class", reftarget="str", + **{"py:module": "example", "py:class": "Class"}) + + # :param age: + :type age: + assert_node(doctree[3][1][0][0][1][0][1][0], + ([addnodes.literal_strong, "age"], + " (", + [pending_xref, addnodes.literal_emphasis, "int"], + ")", + " -- ", + "blah blah")) + assert_node(doctree[3][1][0][0][1][0][1][0][2], pending_xref, + refdomain="py", reftype="class", reftarget="int", + **{"py:module": "example", "py:class": "Class"}) + + # :param items: + :type items: + assert_node(doctree[3][1][0][0][1][0][2][0], + ([addnodes.literal_strong, "items"], + " (", + [pending_xref, addnodes.literal_emphasis, "Tuple"], + [addnodes.literal_emphasis, "["], + [pending_xref, addnodes.literal_emphasis, "str"], + [addnodes.literal_emphasis, ", "], + [addnodes.literal_emphasis, "..."], + [addnodes.literal_emphasis, "]"], + ")", + " -- ", + "blah blah")) + assert_node(doctree[3][1][0][0][1][0][2][0][2], pending_xref, + refdomain="py", reftype="class", reftarget="Tuple", + **{"py:module": "example", "py:class": "Class"}) + assert_node(doctree[3][1][0][0][1][0][2][0][4], pending_xref, + refdomain="py", reftype="class", reftarget="str", + **{"py:module": "example", "py:class": "Class"}) + + # :param Dict[str, str] params: + assert_node(doctree[3][1][0][0][1][0][3][0], + ([addnodes.literal_strong, "params"], + " (", + [pending_xref, addnodes.literal_emphasis, "Dict"], + [addnodes.literal_emphasis, "["], + [pending_xref, addnodes.literal_emphasis, "str"], + [addnodes.literal_emphasis, ", "], + [pending_xref, addnodes.literal_emphasis, "str"], + [addnodes.literal_emphasis, "]"], + ")", + " -- ", + "blah blah")) + assert_node(doctree[3][1][0][0][1][0][3][0][2], pending_xref, + refdomain="py", reftype="class", reftarget="Dict", + **{"py:module": "example", "py:class": "Class"}) + assert_node(doctree[3][1][0][0][1][0][3][0][4], pending_xref, + refdomain="py", reftype="class", reftarget="str", + **{"py:module": "example", "py:class": "Class"}) + assert_node(doctree[3][1][0][0][1][0][3][0][6], pending_xref, + refdomain="py", reftype="class", reftarget="str", + **{"py:module": "example", "py:class": "Class"}) + + +def test_info_field_list_piped_type(app): + text = (".. py:module:: example\n" + ".. py:class:: Class\n" + "\n" + " :param age: blah blah\n" + " :type age: int | str\n") + doctree = restructuredtext.parse(app, text) + + assert_node(doctree, + (addnodes.index, + addnodes.index, + nodes.target, + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], + [desc_addname, "example."], + [desc_name, "Class"])], + [desc_content, nodes.field_list, nodes.field, (nodes.field_name, + nodes.field_body)])])) + assert_node(doctree[3][1][0][0][1], + ([nodes.paragraph, ([addnodes.literal_strong, "age"], + " (", + [pending_xref, addnodes.literal_emphasis, "int"], + [addnodes.literal_emphasis, " | "], + [pending_xref, addnodes.literal_emphasis, "str"], + ")", + " -- ", + "blah blah")],)) + assert_node(doctree[3][1][0][0][1][0][2], pending_xref, + refdomain="py", reftype="class", reftarget="int", + **{"py:module": "example", "py:class": "Class"}) + assert_node(doctree[3][1][0][0][1][0][4], pending_xref, + refdomain="py", reftype="class", reftarget="str", + **{"py:module": "example", "py:class": "Class"}) + + +def test_info_field_list_Literal(app): + text = (".. py:module:: example\n" + ".. py:class:: Class\n" + "\n" + " :param age: blah blah\n" + " :type age: Literal['foo', 'bar', 'baz']\n") + doctree = restructuredtext.parse(app, text) + + assert_node(doctree, + (addnodes.index, + addnodes.index, + nodes.target, + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], + [desc_addname, "example."], + [desc_name, "Class"])], + [desc_content, nodes.field_list, nodes.field, (nodes.field_name, + nodes.field_body)])])) + assert_node(doctree[3][1][0][0][1], + ([nodes.paragraph, ([addnodes.literal_strong, "age"], + " (", + [pending_xref, addnodes.literal_emphasis, "Literal"], + [addnodes.literal_emphasis, "["], + [addnodes.literal_emphasis, "'foo'"], + [addnodes.literal_emphasis, ", "], + [addnodes.literal_emphasis, "'bar'"], + [addnodes.literal_emphasis, ", "], + [addnodes.literal_emphasis, "'baz'"], + [addnodes.literal_emphasis, "]"], + ")", + " -- ", + "blah blah")],)) + assert_node(doctree[3][1][0][0][1][0][2], pending_xref, + refdomain="py", reftype="class", reftarget="Literal", + **{"py:module": "example", "py:class": "Class"}) + + +def test_info_field_list_var(app): + text = (".. py:class:: Class\n" + "\n" + " :var int attr: blah blah\n") + doctree = restructuredtext.parse(app, text) + + assert_node(doctree, (addnodes.index, + [desc, (desc_signature, + [desc_content, nodes.field_list, nodes.field])])) + assert_node(doctree[1][1][0][0], ([nodes.field_name, "Variables"], + [nodes.field_body, nodes.paragraph])) + + # :var int attr: + assert_node(doctree[1][1][0][0][1][0], + ([addnodes.literal_strong, "attr"], + " (", + [pending_xref, addnodes.literal_emphasis, "int"], + ")", + " -- ", + "blah blah")) + assert_node(doctree[1][1][0][0][1][0][2], pending_xref, + refdomain="py", reftype="class", reftarget="int", **{"py:class": "Class"}) + + +def test_info_field_list_napoleon_deliminator_of(app): + text = (".. py:module:: example\n" + ".. py:class:: Class\n" + "\n" + " :param list_str_var: example description.\n" + " :type list_str_var: list of str\n" + " :param tuple_int_var: example description.\n" + " :type tuple_int_var: tuple of tuple of int\n" + ) + doctree = restructuredtext.parse(app, text) + + # :param list of str list_str_var: + assert_node(doctree[3][1][0][0][1][0][0][0], + ([addnodes.literal_strong, "list_str_var"], + " (", + [pending_xref, addnodes.literal_emphasis, "list"], + [addnodes.literal_emphasis, " of "], + [pending_xref, addnodes.literal_emphasis, "str"], + ")", + " -- ", + "example description.")) + + # :param tuple of tuple of int tuple_int_var: + assert_node(doctree[3][1][0][0][1][0][1][0], + ([addnodes.literal_strong, "tuple_int_var"], + " (", + [pending_xref, addnodes.literal_emphasis, "tuple"], + [addnodes.literal_emphasis, " of "], + [pending_xref, addnodes.literal_emphasis, "tuple"], + [addnodes.literal_emphasis, " of "], + [pending_xref, addnodes.literal_emphasis, "int"], + ")", + " -- ", + "example description.")) + + +def test_info_field_list_napoleon_deliminator_or(app): + text = (".. py:module:: example\n" + ".. py:class:: Class\n" + "\n" + " :param bool_str_var: example description.\n" + " :type bool_str_var: bool or str\n" + " :param str_float_int_var: example description.\n" + " :type str_float_int_var: str or float or int\n" + ) + doctree = restructuredtext.parse(app, text) + + # :param bool or str bool_str_var: + assert_node(doctree[3][1][0][0][1][0][0][0], + ([addnodes.literal_strong, "bool_str_var"], + " (", + [pending_xref, addnodes.literal_emphasis, "bool"], + [addnodes.literal_emphasis, " or "], + [pending_xref, addnodes.literal_emphasis, "str"], + ")", + " -- ", + "example description.")) + + # :param str or float or int str_float_int_var: + assert_node(doctree[3][1][0][0][1][0][1][0], + ([addnodes.literal_strong, "str_float_int_var"], + " (", + [pending_xref, addnodes.literal_emphasis, "str"], + [addnodes.literal_emphasis, " or "], + [pending_xref, addnodes.literal_emphasis, "float"], + [addnodes.literal_emphasis, " or "], + [pending_xref, addnodes.literal_emphasis, "int"], + ")", + " -- ", + "example description.")) + + +def test_type_field(app): + text = (".. py:data:: var1\n" + " :type: .int\n" + ".. py:data:: var2\n" + " :type: ~builtins.int\n" + ".. py:data:: var3\n" + " :type: typing.Optional[typing.Tuple[int, typing.Any]]\n") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "var1"], + [desc_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, + [pending_xref, "int"])])], + [desc_content, ()])], + addnodes.index, + [desc, ([desc_signature, ([desc_name, "var2"], + [desc_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, + [pending_xref, "int"])])], + [desc_content, ()])], + addnodes.index, + [desc, ([desc_signature, ([desc_name, "var3"], + [desc_annotation, ([desc_sig_punctuation, ":"], + desc_sig_space, + [pending_xref, "Optional"], + [desc_sig_punctuation, "["], + [pending_xref, "Tuple"], + [desc_sig_punctuation, "["], + [pending_xref, "int"], + [desc_sig_punctuation, ","], + desc_sig_space, + [pending_xref, "Any"], + [desc_sig_punctuation, "]"], + [desc_sig_punctuation, "]"])])], + [desc_content, ()])])) + assert_node(doctree[1][0][1][2], pending_xref, reftarget='int', refspecific=True) + assert_node(doctree[3][0][1][2], pending_xref, reftarget='builtins.int', refspecific=False) + assert_node(doctree[5][0][1][2], pending_xref, reftarget='typing.Optional', refspecific=False) + assert_node(doctree[5][0][1][4], pending_xref, reftarget='typing.Tuple', refspecific=False) + assert_node(doctree[5][0][1][6], pending_xref, reftarget='int', refspecific=False) + assert_node(doctree[5][0][1][9], pending_xref, reftarget='typing.Any', refspecific=False) diff --git a/tests/test_domains/test_domain_py_pyfunction.py b/tests/test_domains/test_domain_py_pyfunction.py new file mode 100644 index 0000000..187b919 --- /dev/null +++ b/tests/test_domains/test_domain_py_pyfunction.py @@ -0,0 +1,396 @@ +"""Tests the Python Domain""" + +from __future__ import annotations + +import pytest +from docutils import nodes + +from sphinx import addnodes +from sphinx.addnodes import ( + desc, + desc_addname, + desc_annotation, + desc_content, + desc_name, + desc_optional, + desc_parameter, + desc_parameterlist, + desc_returns, + desc_sig_keyword, + desc_sig_name, + desc_sig_operator, + desc_sig_punctuation, + desc_sig_space, + desc_signature, + pending_xref, +) +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node + + +def test_pyfunction(app): + text = (".. py:function:: func1\n" + ".. py:module:: example\n" + ".. py:function:: func2\n" + " :async:\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "func1"], + [desc_parameterlist, ()])], + [desc_content, ()])], + addnodes.index, + addnodes.index, + nodes.target, + [desc, ([desc_signature, ([desc_annotation, ([desc_sig_keyword, 'async'], + desc_sig_space)], + [desc_addname, "example."], + [desc_name, "func2"], + [desc_parameterlist, ()])], + [desc_content, ()])])) + assert_node(doctree[0], addnodes.index, + entries=[('pair', 'built-in function; func1()', 'func1', '', None)]) + assert_node(doctree[2], addnodes.index, + entries=[('pair', 'module; example', 'module-example', '', None)]) + assert_node(doctree[3], addnodes.index, + entries=[('single', 'func2() (in module example)', 'example.func2', '', None)]) + + assert 'func1' in domain.objects + assert domain.objects['func1'] == ('index', 'func1', 'function', False) + assert 'example.func2' in domain.objects + assert domain.objects['example.func2'] == ('index', 'example.func2', 'function', False) + + +def test_pyfunction_signature(app): + text = ".. py:function:: hello(name: str) -> str" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "str"])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", no_index=False) + assert_node(doctree[1][0][1], + [desc_parameterlist, desc_parameter, ([desc_sig_name, "name"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"])]) + + +def test_pyfunction_signature_full(app): + text = (".. py:function:: hello(a: str, b = 1, *args: str, " + "c: bool = True, d: tuple = (1, 2), **kwargs: str) -> str") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "str"])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", no_index=False) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "a"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [desc_sig_name, pending_xref, "str"])], + [desc_parameter, ([desc_sig_name, "b"], + [desc_sig_operator, "="], + [nodes.inline, "1"])], + [desc_parameter, ([desc_sig_operator, "*"], + [desc_sig_name, "args"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [desc_sig_name, pending_xref, "str"])], + [desc_parameter, ([desc_sig_name, "c"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [desc_sig_name, pending_xref, "bool"], + desc_sig_space, + [desc_sig_operator, "="], + desc_sig_space, + [nodes.inline, "True"])], + [desc_parameter, ([desc_sig_name, "d"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [desc_sig_name, pending_xref, "tuple"], + desc_sig_space, + [desc_sig_operator, "="], + desc_sig_space, + [nodes.inline, "(1, 2)"])], + [desc_parameter, ([desc_sig_operator, "**"], + [desc_sig_name, "kwargs"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [desc_sig_name, pending_xref, "str"])])]) + # case: separator at head + text = ".. py:function:: hello(*, a)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, nodes.inline, "*"], + [desc_parameter, desc_sig_name, "a"])]) + + # case: separator in the middle + text = ".. py:function:: hello(a, /, b, *, c)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"], + [desc_parameter, desc_sig_operator, "/"], + [desc_parameter, desc_sig_name, "b"], + [desc_parameter, desc_sig_operator, "*"], + [desc_parameter, desc_sig_name, "c"])]) + + # case: separator in the middle (2) + text = ".. py:function:: hello(a, /, *, b)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"], + [desc_parameter, desc_sig_operator, "/"], + [desc_parameter, desc_sig_operator, "*"], + [desc_parameter, desc_sig_name, "b"])]) + + # case: separator at tail + text = ".. py:function:: hello(a, /)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, desc_sig_name, "a"], + [desc_parameter, desc_sig_operator, "/"])]) + + +def test_pyfunction_with_unary_operators(app): + text = ".. py:function:: menu(egg=+1, bacon=-1, sausage=~1, spam=not spam)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "egg"], + [desc_sig_operator, "="], + [nodes.inline, "+1"])], + [desc_parameter, ([desc_sig_name, "bacon"], + [desc_sig_operator, "="], + [nodes.inline, "-1"])], + [desc_parameter, ([desc_sig_name, "sausage"], + [desc_sig_operator, "="], + [nodes.inline, "~1"])], + [desc_parameter, ([desc_sig_name, "spam"], + [desc_sig_operator, "="], + [nodes.inline, "not spam"])])]) + + +def test_pyfunction_with_binary_operators(app): + text = ".. py:function:: menu(spam=2**64)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "spam"], + [desc_sig_operator, "="], + [nodes.inline, "2**64"])])]) + + +def test_pyfunction_with_number_literals(app): + text = ".. py:function:: hello(age=0x10, height=1_6_0)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "age"], + [desc_sig_operator, "="], + [nodes.inline, "0x10"])], + [desc_parameter, ([desc_sig_name, "height"], + [desc_sig_operator, "="], + [nodes.inline, "1_6_0"])])]) + + +def test_pyfunction_with_union_type_operator(app): + text = ".. py:function:: hello(age: int | None)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "age"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [desc_sig_name, ([pending_xref, "int"], + desc_sig_space, + [desc_sig_punctuation, "|"], + desc_sig_space, + [pending_xref, "None"])])])]) + + +def test_optional_pyfunction_signature(app): + text = ".. py:function:: compile(source [, filename [, symbol]]) -> ast object" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "compile"], + desc_parameterlist, + [desc_returns, pending_xref, "ast object"])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", no_index=False) + assert_node(doctree[1][0][1], + ([desc_parameter, ([desc_sig_name, "source"])], + [desc_optional, ([desc_parameter, ([desc_sig_name, "filename"])], + [desc_optional, desc_parameter, ([desc_sig_name, "symbol"])])])) + + +@pytest.mark.sphinx('html', confoverrides={ + 'python_maximum_signature_line_length': len("hello(name: str) -> str"), +}) +def test_pyfunction_signature_with_python_maximum_signature_line_length_equal(app): + text = ".. py:function:: hello(name: str) -> str" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "str"], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", no_index=False) + assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, ( + [desc_sig_name, "name"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"], + )]) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'python_maximum_signature_line_length': len("hello(name: str) -> str"), +}) +def test_pyfunction_signature_with_python_maximum_signature_line_length_force_single(app): + text = (".. py:function:: hello(names: str) -> str\n" + " :single-line-parameter-list:") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "str"], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", no_index=False) + assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, ( + [desc_sig_name, "names"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"], + )]) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'python_maximum_signature_line_length': len("hello(name: str) -> str"), +}) +def test_pyfunction_signature_with_python_maximum_signature_line_length_break(app): + text = ".. py:function:: hello(names: str) -> str" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "str"], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", no_index=False) + assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, ( + [desc_sig_name, "names"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"], + )]) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=True) + + +@pytest.mark.sphinx('html', confoverrides={ + 'maximum_signature_line_length': len("hello(name: str) -> str"), +}) +def test_pyfunction_signature_with_maximum_signature_line_length_equal(app): + text = ".. py:function:: hello(name: str) -> str" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "str"], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", no_index=False) + assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, ( + [desc_sig_name, "name"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"], + )]) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'maximum_signature_line_length': len("hello(name: str) -> str"), +}) +def test_pyfunction_signature_with_maximum_signature_line_length_force_single(app): + text = (".. py:function:: hello(names: str) -> str\n" + " :single-line-parameter-list:") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "str"], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", no_index=False) + assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, ( + [desc_sig_name, "names"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"], + )]) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=False) + + +@pytest.mark.sphinx('html', confoverrides={ + 'maximum_signature_line_length': len("hello(name: str) -> str"), +}) +def test_pyfunction_signature_with_maximum_signature_line_length_break(app): + text = ".. py:function:: hello(names: str) -> str" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "hello"], + desc_parameterlist, + [desc_returns, pending_xref, "str"], + )], + desc_content, + )], + )) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", no_index=False) + assert_node(doctree[1][0][1], [desc_parameterlist, desc_parameter, ( + [desc_sig_name, "names"], + [desc_sig_punctuation, ":"], + desc_sig_space, + [nodes.inline, pending_xref, "str"], + )]) + assert_node(doctree[1][0][1], desc_parameterlist, multi_line_parameter_list=True) diff --git a/tests/test_domains/test_domain_py_pyobject.py b/tests/test_domains/test_domain_py_pyobject.py new file mode 100644 index 0000000..04f9341 --- /dev/null +++ b/tests/test_domains/test_domain_py_pyobject.py @@ -0,0 +1,487 @@ +"""Tests the Python Domain""" + +from __future__ import annotations + +from docutils import nodes + +from sphinx import addnodes +from sphinx.addnodes import ( + desc, + desc_addname, + desc_annotation, + desc_content, + desc_name, + desc_parameterlist, + desc_sig_punctuation, + desc_sig_space, + desc_signature, + pending_xref, +) +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node + + +def test_pyexception_signature(app): + text = ".. py:exception:: builtins.IOError" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, ('exception', desc_sig_space)], + [desc_addname, "builtins."], + [desc_name, "IOError"])], + desc_content)])) + assert_node(doctree[1], desc, desctype="exception", + domain="py", objtype="exception", no_index=False) + + +def test_pydata_signature(app): + text = (".. py:data:: version\n" + " :type: int\n" + " :value: 1\n") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "version"], + [desc_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, + [pending_xref, "int"])], + [desc_annotation, ( + desc_sig_space, + [desc_sig_punctuation, '='], + desc_sig_space, + "1")], + )], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="data", + domain="py", objtype="data", no_index=False) + + +def test_pydata_signature_old(app): + text = (".. py:data:: version\n" + " :annotation: = 1\n") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "version"], + [desc_annotation, (desc_sig_space, + "= 1")])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="data", + domain="py", objtype="data", no_index=False) + + +def test_pydata_with_union_type_operator(app): + text = (".. py:data:: version\n" + " :type: int | str") + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0], + ([desc_name, "version"], + [desc_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, + [pending_xref, "int"], + desc_sig_space, + [desc_sig_punctuation, "|"], + desc_sig_space, + [pending_xref, "str"])])) + + +def test_pyobject_prefix(app): + text = (".. py:class:: Foo\n" + "\n" + " .. py:method:: Foo.say\n" + " .. py:method:: FooBar.say") + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, ('class', desc_sig_space)], + [desc_name, "Foo"])], + [desc_content, (addnodes.index, + desc, + addnodes.index, + desc)])])) + assert doctree[1][1][1].astext().strip() == 'say()' # prefix is stripped + assert doctree[1][1][3].astext().strip() == 'FooBar.say()' # not stripped + + +def test_pydata(app): + text = (".. py:module:: example\n" + ".. py:data:: var\n" + " :type: int\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + addnodes.index, + nodes.target, + [desc, ([desc_signature, ([desc_addname, "example."], + [desc_name, "var"], + [desc_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, + [pending_xref, "int"])])], + [desc_content, ()])])) + assert_node(doctree[3][0][2][2], pending_xref, **{"py:module": "example"}) + assert 'example.var' in domain.objects + assert domain.objects['example.var'] == ('index', 'example.var', 'data', False) + + +def test_pyclass_options(app): + text = (".. py:class:: Class1\n" + ".. py:class:: Class2\n" + " :final:\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], + [desc_name, "Class1"])], + [desc_content, ()])], + addnodes.index, + [desc, ([desc_signature, ([desc_annotation, ("final", + desc_sig_space, + "class", + desc_sig_space)], + [desc_name, "Class2"])], + [desc_content, ()])])) + + # class + assert_node(doctree[0], addnodes.index, + entries=[('single', 'Class1 (built-in class)', 'Class1', '', None)]) + assert 'Class1' in domain.objects + assert domain.objects['Class1'] == ('index', 'Class1', 'class', False) + + # :final: + assert_node(doctree[2], addnodes.index, + entries=[('single', 'Class2 (built-in class)', 'Class2', '', None)]) + assert 'Class2' in domain.objects + assert domain.objects['Class2'] == ('index', 'Class2', 'class', False) + + +def test_pymethod_options(app): + text = (".. py:class:: Class\n" + "\n" + " .. py:method:: meth1\n" + " .. py:method:: meth2\n" + " :classmethod:\n" + " .. py:method:: meth3\n" + " :staticmethod:\n" + " .. py:method:: meth4\n" + " :async:\n" + " .. py:method:: meth5\n" + " :abstractmethod:\n" + " .. py:method:: meth6\n" + " :final:\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], + [desc_name, "Class"])], + [desc_content, (addnodes.index, + desc, + addnodes.index, + desc, + addnodes.index, + desc, + addnodes.index, + desc, + addnodes.index, + desc, + addnodes.index, + desc)])])) + + # method + assert_node(doctree[1][1][0], addnodes.index, + entries=[('single', 'meth1() (Class method)', 'Class.meth1', '', None)]) + assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "meth1"], + [desc_parameterlist, ()])], + [desc_content, ()])) + assert 'Class.meth1' in domain.objects + assert domain.objects['Class.meth1'] == ('index', 'Class.meth1', 'method', False) + + # :classmethod: + assert_node(doctree[1][1][2], addnodes.index, + entries=[('single', 'meth2() (Class class method)', 'Class.meth2', '', None)]) + assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, ("classmethod", desc_sig_space)], + [desc_name, "meth2"], + [desc_parameterlist, ()])], + [desc_content, ()])) + assert 'Class.meth2' in domain.objects + assert domain.objects['Class.meth2'] == ('index', 'Class.meth2', 'method', False) + + # :staticmethod: + assert_node(doctree[1][1][4], addnodes.index, + entries=[('single', 'meth3() (Class static method)', 'Class.meth3', '', None)]) + assert_node(doctree[1][1][5], ([desc_signature, ([desc_annotation, ("static", desc_sig_space)], + [desc_name, "meth3"], + [desc_parameterlist, ()])], + [desc_content, ()])) + assert 'Class.meth3' in domain.objects + assert domain.objects['Class.meth3'] == ('index', 'Class.meth3', 'method', False) + + # :async: + assert_node(doctree[1][1][6], addnodes.index, + entries=[('single', 'meth4() (Class method)', 'Class.meth4', '', None)]) + assert_node(doctree[1][1][7], ([desc_signature, ([desc_annotation, ("async", desc_sig_space)], + [desc_name, "meth4"], + [desc_parameterlist, ()])], + [desc_content, ()])) + assert 'Class.meth4' in domain.objects + assert domain.objects['Class.meth4'] == ('index', 'Class.meth4', 'method', False) + + # :abstractmethod: + assert_node(doctree[1][1][8], addnodes.index, + entries=[('single', 'meth5() (Class method)', 'Class.meth5', '', None)]) + assert_node(doctree[1][1][9], ([desc_signature, ([desc_annotation, ("abstract", desc_sig_space)], + [desc_name, "meth5"], + [desc_parameterlist, ()])], + [desc_content, ()])) + assert 'Class.meth5' in domain.objects + assert domain.objects['Class.meth5'] == ('index', 'Class.meth5', 'method', False) + + # :final: + assert_node(doctree[1][1][10], addnodes.index, + entries=[('single', 'meth6() (Class method)', 'Class.meth6', '', None)]) + assert_node(doctree[1][1][11], ([desc_signature, ([desc_annotation, ("final", desc_sig_space)], + [desc_name, "meth6"], + [desc_parameterlist, ()])], + [desc_content, ()])) + assert 'Class.meth6' in domain.objects + assert domain.objects['Class.meth6'] == ('index', 'Class.meth6', 'method', False) + + +def test_pyclassmethod(app): + text = (".. py:class:: Class\n" + "\n" + " .. py:classmethod:: meth\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], + [desc_name, "Class"])], + [desc_content, (addnodes.index, + desc)])])) + assert_node(doctree[1][1][0], addnodes.index, + entries=[('single', 'meth() (Class class method)', 'Class.meth', '', None)]) + assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("classmethod", desc_sig_space)], + [desc_name, "meth"], + [desc_parameterlist, ()])], + [desc_content, ()])) + assert 'Class.meth' in domain.objects + assert domain.objects['Class.meth'] == ('index', 'Class.meth', 'method', False) + + +def test_pystaticmethod(app): + text = (".. py:class:: Class\n" + "\n" + " .. py:staticmethod:: meth\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], + [desc_name, "Class"])], + [desc_content, (addnodes.index, + desc)])])) + assert_node(doctree[1][1][0], addnodes.index, + entries=[('single', 'meth() (Class static method)', 'Class.meth', '', None)]) + assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("static", desc_sig_space)], + [desc_name, "meth"], + [desc_parameterlist, ()])], + [desc_content, ()])) + assert 'Class.meth' in domain.objects + assert domain.objects['Class.meth'] == ('index', 'Class.meth', 'method', False) + + +def test_pyattribute(app): + text = (".. py:class:: Class\n" + "\n" + " .. py:attribute:: attr\n" + " :type: Optional[str]\n" + " :value: ''\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], + [desc_name, "Class"])], + [desc_content, (addnodes.index, + desc)])])) + assert_node(doctree[1][1][0], addnodes.index, + entries=[('single', 'attr (Class attribute)', 'Class.attr', '', None)]) + assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"], + [desc_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, + [pending_xref, "str"], + desc_sig_space, + [desc_sig_punctuation, "|"], + desc_sig_space, + [pending_xref, "None"])], + [desc_annotation, (desc_sig_space, + [desc_sig_punctuation, '='], + desc_sig_space, + "''")], + )], + [desc_content, ()])) + assert_node(doctree[1][1][1][0][1][2], pending_xref, **{"py:class": "Class"}) + assert_node(doctree[1][1][1][0][1][6], pending_xref, **{"py:class": "Class"}) + assert 'Class.attr' in domain.objects + assert domain.objects['Class.attr'] == ('index', 'Class.attr', 'attribute', False) + + +def test_pyproperty(app): + text = (".. py:class:: Class\n" + "\n" + " .. py:property:: prop1\n" + " :abstractmethod:\n" + " :type: str\n" + "\n" + " .. py:property:: prop2\n" + " :classmethod:\n" + " :type: str\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], + [desc_name, "Class"])], + [desc_content, (addnodes.index, + desc, + addnodes.index, + desc)])])) + assert_node(doctree[1][1][0], addnodes.index, + entries=[('single', 'prop1 (Class property)', 'Class.prop1', '', None)]) + assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("abstract", desc_sig_space, + "property", desc_sig_space)], + [desc_name, "prop1"], + [desc_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, + [pending_xref, "str"])])], + [desc_content, ()])) + assert_node(doctree[1][1][2], addnodes.index, + entries=[('single', 'prop2 (Class property)', 'Class.prop2', '', None)]) + assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, ("class", desc_sig_space, + "property", desc_sig_space)], + [desc_name, "prop2"], + [desc_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, + [pending_xref, "str"])])], + [desc_content, ()])) + assert 'Class.prop1' in domain.objects + assert domain.objects['Class.prop1'] == ('index', 'Class.prop1', 'property', False) + assert 'Class.prop2' in domain.objects + assert domain.objects['Class.prop2'] == ('index', 'Class.prop2', 'property', False) + + +def test_pydecorator_signature(app): + text = ".. py:decorator:: deco" + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_addname, "@"], + [desc_name, "deco"])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="function", + domain="py", objtype="function", no_index=False) + + assert 'deco' in domain.objects + assert domain.objects['deco'] == ('index', 'deco', 'function', False) + + +def test_pydecoratormethod_signature(app): + text = ".. py:decoratormethod:: deco" + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_addname, "@"], + [desc_name, "deco"])], + desc_content)])) + assert_node(doctree[1], addnodes.desc, desctype="method", + domain="py", objtype="method", no_index=False) + + assert 'deco' in domain.objects + assert domain.objects['deco'] == ('index', 'deco', 'method', False) + + +def test_pycurrentmodule(app): + text = (".. py:module:: Other\n" + "\n" + ".. py:module:: Module\n" + ".. py:class:: A\n" + "\n" + " .. py:method:: m1\n" + " .. py:method:: m2\n" + "\n" + ".. py:currentmodule:: Other\n" + "\n" + ".. py:class:: B\n" + "\n" + " .. py:method:: m3\n" + " .. py:method:: m4\n") + domain = app.env.get_domain('py') + doctree = restructuredtext.parse(app, text) + print(doctree) + assert_node( + doctree, ( + addnodes.index, + addnodes.index, + addnodes.index, + nodes.target, + nodes.target, + [desc, ( + [desc_signature, ( + [desc_annotation, ("class", desc_sig_space)], + [desc_addname, "Module."], + [desc_name, "A"], + )], + [desc_content, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "m1"], + [desc_parameterlist, ()], + )], + [desc_content, ()], + )], + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "m2"], + [desc_parameterlist, ()], + )], + [desc_content, ()], + )], + )], + )], + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_annotation, ("class", desc_sig_space)], + [desc_addname, "Other."], + [desc_name, "B"], + )], + [desc_content, ( + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "m3"], + [desc_parameterlist, ()], + )], + [desc_content, ()], + )], + addnodes.index, + [desc, ( + [desc_signature, ( + [desc_name, "m4"], + [desc_parameterlist, ()], + )], + [desc_content, ()], + )], + )], + )], + )) + assert 'Module' in domain.objects + assert domain.objects['Module'] == ('index', 'module-Module', 'module', False) + assert 'Other' in domain.objects + assert domain.objects['Other'] == ('index', 'module-Other', 'module', False) + assert 'Module.A' in domain.objects + assert domain.objects['Module.A'] == ('index', 'Module.A', 'class', False) + assert 'Other.B' in domain.objects + assert domain.objects['Other.B'] == ('index', 'Other.B', 'class', False) + assert 'Module.A.m1' in domain.objects + assert domain.objects['Module.A.m1'] == ('index', 'Module.A.m1', 'method', False) + assert 'Module.A.m2' in domain.objects + assert domain.objects['Module.A.m2'] == ('index', 'Module.A.m2', 'method', False) + assert 'Other.B.m3' in domain.objects + assert domain.objects['Other.B.m3'] == ('index', 'Other.B.m3', 'method', False) + assert 'Other.B.m4' in domain.objects + assert domain.objects['Other.B.m4'] == ('index', 'Other.B.m4', 'method', False) diff --git a/tests/test_domain_rst.py b/tests/test_domains/test_domain_rst.py index 4445da1..4445da1 100644 --- a/tests/test_domain_rst.py +++ b/tests/test_domains/test_domain_rst.py diff --git a/tests/test_domain_std.py b/tests/test_domains/test_domain_std.py index 6d7ab53..52ecdf5 100644 --- a/tests/test_domain_std.py +++ b/tests/test_domains/test_domain_std.py @@ -5,7 +5,6 @@ from unittest import mock import pytest from docutils import nodes from docutils.nodes import definition, definition_list, definition_list_item, term -from html5lib import HTMLParser from sphinx import addnodes from sphinx.addnodes import ( @@ -20,7 +19,7 @@ from sphinx.addnodes import ( ) from sphinx.domains.std import StandardDomain from sphinx.testing import restructuredtext -from sphinx.testing.util import assert_node +from sphinx.testing.util import assert_node, etree_parse def test_process_doc_handle_figure_caption(): @@ -368,16 +367,18 @@ def test_multiple_cmdoptions(app): @pytest.mark.sphinx(testroot='productionlist') def test_productionlist(app, status, warning): - app.builder.build_all() + app.build(force_all=True) warnings = warning.getvalue().split("\n") assert len(warnings) == 2 assert warnings[-1] == '' assert "Dup2.rst:4: WARNING: duplicate token description of Dup, other instance in Dup1" in warnings[0] - with (app.outdir / 'index.html').open('rb') as f: - etree = HTMLParser(namespaceHTMLElements=False).parse(f) - ul = list(etree.iter('ul'))[1] + etree = etree_parse(app.outdir / 'index.html') + nodes = list(etree.iter('ul')) + assert len(nodes) >= 2 + + ul = nodes[1] cases = [] for li in list(ul): assert len(list(li)) == 1 @@ -493,3 +494,24 @@ def test_labeled_field(app): assert domain.labels['label1'] == ('index', 'label1', 'Foo blah blah blah') assert 'label2' in domain.labels assert domain.labels['label2'] == ('index', 'label2', 'Bar blah blah blah') + + +@pytest.mark.sphinx('html', testroot='manpage_url', + confoverrides={'manpages_url': 'https://example.com/{page}.{section}'}) +def test_html_manpage(app): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert ('<em class="manpage">' + '<a class="manpage reference external" href="https://example.com/man.1">man(1)</a>' + '</em>') in content + assert ('<em class="manpage">' + '<a class="manpage reference external" href="https://example.com/ls.1">ls.1</a>' + '</em>') in content + assert ('<em class="manpage">' + '<a class="manpage reference external" href="https://example.com/sphinx.">sphinx</a>' + '</em>') in content + assert ('<em class="manpage">' + '<a class="manpage reference external" href="https://example.com/bsd-mailx/mailx.1">mailx(1)</a>' + '</em>') in content + assert '<em class="manpage">man(1)</em>' in content diff --git a/tests/test_environment/__init__.py b/tests/test_environment/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_environment/__init__.py diff --git a/tests/test_environment.py b/tests/test_environment/test_environment.py index 8a34457..8a34457 100644 --- a/tests/test_environment.py +++ b/tests/test_environment/test_environment.py diff --git a/tests/test_environment_indexentries.py b/tests/test_environment/test_environment_indexentries.py index 4cfdc28..4cfdc28 100644 --- a/tests/test_environment_indexentries.py +++ b/tests/test_environment/test_environment_indexentries.py diff --git a/tests/test_environment_record_dependencies.py b/tests/test_environment/test_environment_record_dependencies.py index 0a17253..0a17253 100644 --- a/tests/test_environment_record_dependencies.py +++ b/tests/test_environment/test_environment_record_dependencies.py diff --git a/tests/test_environment_toctree.py b/tests/test_environment/test_environment_toctree.py index 5123715..175c6ab 100644 --- a/tests/test_environment_toctree.py +++ b/tests/test_environment/test_environment_toctree.py @@ -33,7 +33,7 @@ def test_process_doc(app): assert_node(toctree[0][1][0], addnodes.toctree, caption="Table of Contents", glob=False, hidden=False, titlesonly=False, maxdepth=2, numbered=999, - entries=[(None, 'foo'), (None, 'bar'), (None, 'http://sphinx-doc.org/'), + entries=[(None, 'foo'), (None, 'bar'), (None, 'https://sphinx-doc.org/'), (None, 'self')], includefiles=['foo', 'bar']) @@ -59,8 +59,8 @@ def test_process_doc(app): assert_node(toctree[1][1][1], addnodes.toctree, caption=None, glob=False, hidden=True, titlesonly=False, maxdepth=-1, numbered=0, - entries=[('Latest reference', 'http://sphinx-doc.org/latest/'), - ('Python', 'http://python.org/')]) + entries=[('Latest reference', 'https://sphinx-doc.org/latest/'), + ('Python', 'https://python.org/')]) assert_node(toctree[2][0], [compact_paragraph, reference, "Indices and tables"]) @@ -246,7 +246,7 @@ def test_global_toctree_for_doc(app): ([list_item, ([compact_paragraph, reference, "foo"], bullet_list)], [list_item, compact_paragraph, reference, "bar"], - [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"], + [list_item, compact_paragraph, reference, "https://sphinx-doc.org/"], [list_item, compact_paragraph, reference, "Welcome to Sphinx Tests’s documentation!"])) assert_node(toctree[1][0][1], @@ -259,7 +259,7 @@ def test_global_toctree_for_doc(app): assert_node(toctree[1][0][1][1][0][0], reference, refuri="foo#foo-1", secnumber=[1, 2]) assert_node(toctree[1][0][1][2][0][0], reference, refuri="foo#foo-2", secnumber=[1, 3]) assert_node(toctree[1][1][0][0], reference, refuri="bar", secnumber=[2]) - assert_node(toctree[1][2][0][0], reference, refuri="http://sphinx-doc.org/") + assert_node(toctree[1][2][0][0], reference, refuri="https://sphinx-doc.org/") assert_node(toctree[1][3][0][0], reference, refuri="") assert_node(toctree[2], @@ -267,8 +267,8 @@ def test_global_toctree_for_doc(app): assert_node(toctree[3], ([list_item, compact_paragraph, reference, "Latest reference"], [list_item, compact_paragraph, reference, "Python"])) - assert_node(toctree[3][0][0][0], reference, refuri="http://sphinx-doc.org/latest/") - assert_node(toctree[3][1][0][0], reference, refuri="http://python.org/") + assert_node(toctree[3][0][0][0], reference, refuri="https://sphinx-doc.org/latest/") + assert_node(toctree[3][1][0][0], reference, refuri="https://python.org/") @pytest.mark.sphinx('xml', testroot='toctree') @@ -285,12 +285,12 @@ def test_global_toctree_for_doc_collapse(app): assert_node(toctree[1], ([list_item, compact_paragraph, reference, "foo"], [list_item, compact_paragraph, reference, "bar"], - [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"], + [list_item, compact_paragraph, reference, "https://sphinx-doc.org/"], [list_item, compact_paragraph, reference, "Welcome to Sphinx Tests’s documentation!"])) assert_node(toctree[1][0][0][0], reference, refuri="foo", secnumber=[1]) assert_node(toctree[1][1][0][0], reference, refuri="bar", secnumber=[2]) - assert_node(toctree[1][2][0][0], reference, refuri="http://sphinx-doc.org/") + assert_node(toctree[1][2][0][0], reference, refuri="https://sphinx-doc.org/") assert_node(toctree[1][3][0][0], reference, refuri="") assert_node(toctree[2], @@ -298,8 +298,8 @@ def test_global_toctree_for_doc_collapse(app): assert_node(toctree[3], ([list_item, compact_paragraph, reference, "Latest reference"], [list_item, compact_paragraph, reference, "Python"])) - assert_node(toctree[3][0][0][0], reference, refuri="http://sphinx-doc.org/latest/") - assert_node(toctree[3][1][0][0], reference, refuri="http://python.org/") + assert_node(toctree[3][0][0][0], reference, refuri="https://sphinx-doc.org/latest/") + assert_node(toctree[3][1][0][0], reference, refuri="https://python.org/") @pytest.mark.sphinx('xml', testroot='toctree') @@ -318,7 +318,7 @@ def test_global_toctree_for_doc_maxdepth(app): ([list_item, ([compact_paragraph, reference, "foo"], bullet_list)], [list_item, compact_paragraph, reference, "bar"], - [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"], + [list_item, compact_paragraph, reference, "https://sphinx-doc.org/"], [list_item, compact_paragraph, reference, "Welcome to Sphinx Tests’s documentation!"])) assert_node(toctree[1][0][1], @@ -336,7 +336,7 @@ def test_global_toctree_for_doc_maxdepth(app): reference, refuri="foo#foo-1-1", secnumber=[1, 2, 1]) assert_node(toctree[1][0][1][2][0][0], reference, refuri="foo#foo-2", secnumber=[1, 3]) assert_node(toctree[1][1][0][0], reference, refuri="bar", secnumber=[2]) - assert_node(toctree[1][2][0][0], reference, refuri="http://sphinx-doc.org/") + assert_node(toctree[1][2][0][0], reference, refuri="https://sphinx-doc.org/") assert_node(toctree[1][3][0][0], reference, refuri="") assert_node(toctree[2], @@ -344,8 +344,8 @@ def test_global_toctree_for_doc_maxdepth(app): assert_node(toctree[3], ([list_item, compact_paragraph, reference, "Latest reference"], [list_item, compact_paragraph, reference, "Python"])) - assert_node(toctree[3][0][0][0], reference, refuri="http://sphinx-doc.org/latest/") - assert_node(toctree[3][1][0][0], reference, refuri="http://python.org/") + assert_node(toctree[3][0][0][0], reference, refuri="https://sphinx-doc.org/latest/") + assert_node(toctree[3][1][0][0], reference, refuri="https://python.org/") @pytest.mark.sphinx('xml', testroot='toctree') @@ -363,7 +363,7 @@ def test_global_toctree_for_doc_includehidden(app): ([list_item, ([compact_paragraph, reference, "foo"], bullet_list)], [list_item, compact_paragraph, reference, "bar"], - [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"], + [list_item, compact_paragraph, reference, "https://sphinx-doc.org/"], [list_item, compact_paragraph, reference, "Welcome to Sphinx Tests’s documentation!"])) assert_node(toctree[1][0][1], @@ -376,7 +376,7 @@ def test_global_toctree_for_doc_includehidden(app): assert_node(toctree[1][0][1][1][0][0], reference, refuri="foo#foo-1", secnumber=[1, 2]) assert_node(toctree[1][0][1][2][0][0], reference, refuri="foo#foo-2", secnumber=[1, 3]) assert_node(toctree[1][1][0][0], reference, refuri="bar", secnumber=[2]) - assert_node(toctree[1][2][0][0], reference, refuri="http://sphinx-doc.org/") + assert_node(toctree[1][2][0][0], reference, refuri="https://sphinx-doc.org/") assert_node(toctree[2], [bullet_list, list_item, compact_paragraph, reference, "baz"]) diff --git a/tests/test_events.py b/tests/test_events.py index d850a91..5097dc8 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -9,11 +9,11 @@ from sphinx.events import EventManager def test_event_priority(): result = [] events = EventManager(object()) # pass an dummy object as an app - events.connect('builder-inited', lambda app: result.append(1), priority = 500) - events.connect('builder-inited', lambda app: result.append(2), priority = 500) - events.connect('builder-inited', lambda app: result.append(3), priority = 200) # earlier - events.connect('builder-inited', lambda app: result.append(4), priority = 700) # later - events.connect('builder-inited', lambda app: result.append(5), priority = 500) + events.connect('builder-inited', lambda app: result.append(1), priority=500) + events.connect('builder-inited', lambda app: result.append(2), priority=500) + events.connect('builder-inited', lambda app: result.append(3), priority=200) # earlier + events.connect('builder-inited', lambda app: result.append(4), priority=700) # later + events.connect('builder-inited', lambda app: result.append(5), priority=500) events.emit('builder-inited') assert result == [3, 1, 2, 5, 4] diff --git a/tests/test_extensions/__init__.py b/tests/test_extensions/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_extensions/__init__.py diff --git a/tests/test_extensions/autodoc_util.py b/tests/test_extensions/autodoc_util.py new file mode 100644 index 0000000..7c4da07 --- /dev/null +++ b/tests/test_extensions/autodoc_util.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from unittest.mock import Mock + +# NEVER import those objects from sphinx.ext.autodoc directly +from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options +from sphinx.util.docutils import LoggingReporter + +if TYPE_CHECKING: + from typing import Any + + from docutils.statemachine import StringList + + from sphinx.application import Sphinx + + +def do_autodoc( + app: Sphinx, + objtype: str, + name: str, + options: dict[str, Any] | None = None, +) -> StringList: + options = {} if options is None else options.copy() + app.env.temp_data.setdefault('docname', 'index') # set dummy docname + doccls = app.registry.documenters[objtype] + docoptions = process_documenter_options(doccls, app.config, options) + state = Mock() + state.document.settings.tab_width = 8 + bridge = DocumenterBridge(app.env, LoggingReporter(''), docoptions, 1, state) + documenter = doccls(bridge, name) + documenter.generate() + return bridge.result diff --git a/tests/ext_napoleon_pep526_data_google.py b/tests/test_extensions/ext_napoleon_pep526_data_google.py index bb55b0f..d0692e0 100644 --- a/tests/ext_napoleon_pep526_data_google.py +++ b/tests/test_extensions/ext_napoleon_pep526_data_google.py @@ -5,7 +5,7 @@ module_level_var: int = 99 class PEP526GoogleClass: - """Sample class with PEP 526 annotations and google docstring + """Sample class with PEP 526 annotations and google docstring. Attributes: attr1: Attr1 description. diff --git a/tests/ext_napoleon_pep526_data_numpy.py b/tests/test_extensions/ext_napoleon_pep526_data_numpy.py index b3093a7..eff7746 100644 --- a/tests/ext_napoleon_pep526_data_numpy.py +++ b/tests/test_extensions/ext_napoleon_pep526_data_numpy.py @@ -16,5 +16,6 @@ class PEP526NumpyClass: attr2: Attr2 description """ + attr1: int attr2: str diff --git a/tests/test_ext_apidoc.py b/tests/test_extensions/test_ext_apidoc.py index 1e089a3..c3c979f 100644 --- a/tests/test_ext_apidoc.py +++ b/tests/test_extensions/test_ext_apidoc.py @@ -15,7 +15,7 @@ def apidoc(rootdir, tmp_path, apidoc_params): coderoot = rootdir / kwargs.get('coderoot', 'test-root') outdir = tmp_path / 'out' excludes = [str(coderoot / e) for e in kwargs.get('excludes', [])] - args = ['-o', str(outdir), '-F', str(coderoot)] + excludes + kwargs.get('options', []) + args = ['-o', str(outdir), '-F', str(coderoot), *excludes, *kwargs.get('options', [])] apidoc_main(args) return namedtuple('apidoc', 'coderoot,outdir')(coderoot, outdir) @@ -26,8 +26,7 @@ def apidoc_params(request): kwargs = {} for info in reversed(list(request.node.iter_markers("apidoc"))): - for i, a in enumerate(info.args): - pargs[i] = a + pargs |= dict(enumerate(info.args)) kwargs.update(info.kwargs) args = [pargs[i] for i in sorted(pargs.keys())] @@ -302,9 +301,9 @@ def test_extension_parsed(make_app, apidoc): ) def test_toc_all_references_should_exist_pep420_enabled(make_app, apidoc): """All references in toc should exist. This test doesn't say if - directories with empty __init__.py and and nothing else should be - skipped, just ensures consistency between what's referenced in the toc - and what is created. This is the variant with pep420 enabled. + directories with empty __init__.py and and nothing else should be + skipped, just ensures consistency between what's referenced in the toc + and what is created. This is the variant with pep420 enabled. """ outdir = apidoc.outdir assert (outdir / 'conf.py').is_file() @@ -332,9 +331,9 @@ def test_toc_all_references_should_exist_pep420_enabled(make_app, apidoc): ) def test_toc_all_references_should_exist_pep420_disabled(make_app, apidoc): """All references in toc should exist. This test doesn't say if - directories with empty __init__.py and and nothing else should be - skipped, just ensures consistency between what's referenced in the toc - and what is created. This is the variant with pep420 disabled. + directories with empty __init__.py and and nothing else should be + skipped, just ensures consistency between what's referenced in the toc + and what is created. This is the variant with pep420 disabled. """ outdir = apidoc.outdir assert (outdir / 'conf.py').is_file() @@ -379,7 +378,7 @@ def extract_toc(path): ) def test_subpackage_in_toc(make_app, apidoc): """Make sure that empty subpackages with non-empty subpackages in them - are not skipped (issue #4520) + are not skipped (issue #4520) """ outdir = apidoc.outdir assert (outdir / 'conf.py').is_file() @@ -643,7 +642,6 @@ def test_no_duplicates(rootdir, tmp_path): We can't use pytest.mark.apidoc here as we use a different set of arguments to apidoc_main """ - original_suffixes = sphinx.ext.apidoc.PY_SUFFIXES try: # Ensure test works on Windows diff --git a/tests/test_ext_autodoc.py b/tests/test_extensions/test_ext_autodoc.py index 7062763..54f81f2 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_extensions/test_ext_autodoc.py @@ -4,8 +4,14 @@ This tests mainly the Documenters; the auto directives are tested in a test source file translated by test_build. """ +from __future__ import annotations + +import functools +import itertools +import operator import sys from types import SimpleNamespace +from typing import TYPE_CHECKING from unittest.mock import Mock from warnings import catch_warnings @@ -14,8 +20,8 @@ from docutils.statemachine import ViewList from sphinx import addnodes from sphinx.ext.autodoc import ALL, ModuleLevelDocumenter, Options -from sphinx.ext.autodoc.directive import DocumenterBridge, process_documenter_options -from sphinx.util.docutils import LoggingReporter + +from tests.test_extensions.autodoc_util import do_autodoc try: # Enable pyximport to test cython module @@ -24,47 +30,35 @@ try: except ImportError: pyximport = None - -def do_autodoc(app, objtype, name, options=None): - if options is None: - options = {} - app.env.temp_data.setdefault('docname', 'index') # set dummy docname - doccls = app.registry.documenters[objtype] - docoptions = process_documenter_options(doccls, app.config, options) - state = Mock() - state.document.settings.tab_width = 8 - bridge = DocumenterBridge(app.env, LoggingReporter(''), docoptions, 1, state) - documenter = doccls(bridge, name) - documenter.generate() - - return bridge.result +if TYPE_CHECKING: + from typing import Any def make_directive_bridge(env): options = Options( - inherited_members = False, - undoc_members = False, - private_members = False, - special_members = False, - imported_members = False, - show_inheritance = False, - no_index = False, - annotation = None, - synopsis = '', - platform = '', - deprecated = False, - members = [], - member_order = 'alphabetical', - exclude_members = set(), - ignore_module_all = False, + inherited_members=False, + undoc_members=False, + private_members=False, + special_members=False, + imported_members=False, + show_inheritance=False, + no_index=False, + annotation=None, + synopsis='', + platform='', + deprecated=False, + members=[], + member_order='alphabetical', + exclude_members=set(), + ignore_module_all=False, ) directive = SimpleNamespace( - env = env, - genopt = options, - result = ViewList(), - record_dependencies = set(), - state = Mock(), + env=env, + genopt=options, + result=ViewList(), + record_dependencies=set(), + state=Mock(), ) directive.state.document.settings.tab_width = 8 @@ -74,23 +68,6 @@ def make_directive_bridge(env): processed_signatures = [] -def process_signature(app, what, name, obj, options, args, retann): - processed_signatures.append((what, name)) - if name == 'bar': - return '42', None - return None - - -def skip_member(app, what, name, obj, skip, options): - if name in ('__special1__', '__special2__'): - return skip - if name.startswith('__'): - return True - if name == 'skipmeth': - return True - return None - - def test_parse_name(app): def verify(objtype, name, result): inst = app.registry.documenters[objtype](directive, name) @@ -103,7 +80,7 @@ def test_parse_name(app): verify('module', 'test_ext_autodoc', ('test_ext_autodoc', [], None, None)) verify('module', 'test.test_ext_autodoc', ('test.test_ext_autodoc', [], None, None)) verify('module', 'test(arg)', ('test', [], 'arg', None)) - assert 'signature arguments' in app._warning.getvalue() + assert 'signature arguments' in app.warning.getvalue() # for functions/classes verify('function', 'test_ext_autodoc.raises', @@ -131,6 +108,21 @@ def test_parse_name(app): def test_format_signature(app): + def process_signature(app, what, name, obj, options, args, retann): + processed_signatures.append((what, name)) + if name == 'bar': + return '42', None + return None + + def skip_member(app, what, name, obj, skip, options): + if name in ('__special1__', '__special2__'): + return skip + if name.startswith('__'): + return True + if name == 'skipmeth': + return True + return None + app.connect('autodoc-process-signature', process_signature) app.connect('autodoc-skip-member', skip_member) @@ -226,12 +218,14 @@ def test_format_signature(app): class F2: """some docstring for F2.""" + def __init__(self, *args, **kw): """ __init__(a1, a2, kw1=True, kw2=False) some docstring for __init__. """ + class G2(F2): pass @@ -330,7 +324,7 @@ def test_get_doc(app): inst.format_signature() # handle docstring signatures! ds = inst.get_doc() # for testing purposes, concat them and strip the empty line at the end - res = sum(ds, [])[:-1] + res = functools.reduce(operator.iadd, ds, [])[:-1] print(res) return res @@ -342,6 +336,7 @@ def test_get_doc(app): # standard function, diverse docstring styles... def f(): """Docstring""" + def g(): """ Docstring @@ -837,9 +832,13 @@ def test_autodoc_special_members(app): ] # all special methods - options = {"members": None, - "undoc-members": None, - "special-members": None} + options = { + "members": None, + "undoc-members": None, + "special-members": None, + } + if sys.version_info >= (3, 13, 0, 'alpha', 5): + options["exclude-members"] = "__static_attributes__" actual = do_autodoc(app, 'class', 'target.Class', options) assert list(filter(lambda l: '::' in l, actual)) == [ '.. py:class:: Class(arg)', @@ -1402,73 +1401,411 @@ def test_slots(app): ] +class _EnumFormatter: + def __init__(self, name: str, *, module: str = 'target.enums') -> None: + self.name = name + self.module = module + + @property + def target(self) -> str: + """The autodoc target class.""" + return f'{self.module}.{self.name}' + + def subtarget(self, name: str) -> str: + """The autodoc sub-target (an attribute, method, etc).""" + return f'{self.target}.{name}' + + def _node( + self, role: str, name: str, doc: str, *, args: str, indent: int, **options: Any, + ) -> list[str]: + prefix = indent * ' ' + tab = ' ' * 3 + + def rst_option(name: str, value: Any) -> str: + value = '' if value in {1, True} else value + return f'{prefix}{tab}:{name}: {value!s}'.rstrip() + + lines = [ + '', + f'{prefix}.. py:{role}:: {name}{args}', + f'{prefix}{tab}:module: {self.module}', + *itertools.starmap(rst_option, options.items()), + ] + if doc: + lines.extend(['', f'{prefix}{tab}{doc}']) + lines.append('') + return lines + + def entry( + self, + entry_name: str, + doc: str = '', + *, + role: str, + args: str = '', + indent: int = 3, + **rst_options: Any, + ) -> list[str]: + """Get the RST lines for a named attribute, method, etc.""" + qualname = f'{self.name}.{entry_name}' + return self._node(role, qualname, doc, args=args, indent=indent, **rst_options) + + def brief(self, doc: str, *, indent: int = 0, **options: Any) -> list[str]: + """Generate the brief part of the class being documented.""" + assert doc, f'enumeration class {self.target!r} should have an explicit docstring' + + if sys.version_info[:2] >= (3, 13) or sys.version_info[:3] >= (3, 12, 3): + args = ('(value, names=<not given>, *values, module=None, ' + 'qualname=None, type=None, start=1, boundary=None)') + elif sys.version_info[:2] >= (3, 12): + args = ('(value, names=None, *values, module=None, ' + 'qualname=None, type=None, start=1, boundary=None)') + elif sys.version_info[:2] >= (3, 11): + args = ('(value, names=None, *, module=None, qualname=None, ' + 'type=None, start=1, boundary=None)') + else: + args = '(value)' + + return self._node('class', self.name, doc, args=args, indent=indent, **options) + + def method( + self, name: str, doc: str, *flags: str, args: str = '()', indent: int = 3, + ) -> list[str]: + rst_options = dict.fromkeys(flags, '') + return self.entry(name, doc, role='method', args=args, indent=indent, **rst_options) + + def member(self, name: str, value: Any, doc: str, *, indent: int = 3) -> list[str]: + rst_options = {'value': repr(value)} + return self.entry(name, doc, role='attribute', indent=indent, **rst_options) + + +@pytest.fixture() +def autodoc_enum_options() -> dict[str, object]: + """Default autodoc options to use when testing enum's documentation.""" + return {"members": None, "undoc-members": None} + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_class(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumCls') + options = autodoc_enum_options | {'private-members': None} + + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('say_goodbye', 'a classmethod says good-bye to you.', 'classmethod'), + *fmt.method('say_hello', 'a method says hello to you.'), + *fmt.member('val1', 12, 'doc for val1'), + *fmt.member('val2', 23, 'doc for val2'), + *fmt.member('val3', 34, 'doc for val3'), + *fmt.member('val4', 34, ''), # val4 is alias of val3 + ] + + # Inherited members exclude the native Enum API (in particular + # the 'name' and 'value' properties), unless they were explicitly + # redefined by the user in one of the bases. + actual = do_autodoc(app, 'class', fmt.target, options | {'inherited-members': None}) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('say_goodbye', 'a classmethod says good-bye to you.', 'classmethod'), + *fmt.method('say_hello', 'a method says hello to you.'), + *fmt.member('val1', 12, 'doc for val1'), + *fmt.member('val2', 23, 'doc for val2'), + *fmt.member('val3', 34, 'doc for val3'), + *fmt.member('val4', 34, ''), # val4 is alias of val3 + ] + + # checks for an attribute of EnumCls + actual = do_autodoc(app, 'attribute', fmt.subtarget('val1')) + assert list(actual) == fmt.member('val1', 12, 'doc for val1', indent=0) + + @pytest.mark.sphinx('html', testroot='ext-autodoc') -def test_enum_class(app): - options = {"members": None} - actual = do_autodoc(app, 'class', 'target.enums.EnumCls', options) - - if sys.version_info[:2] >= (3, 12): - args = ('(value, names=None, *values, module=None, ' - 'qualname=None, type=None, start=1, boundary=None)') - elif sys.version_info[:2] >= (3, 11): - args = ('(value, names=None, *, module=None, qualname=None, ' - 'type=None, start=1, boundary=None)') - else: - args = '(value)' +def test_enum_class_with_data_type(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumClassWithDataType') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) assert list(actual) == [ - '', - '.. py:class:: EnumCls' + args, - ' :module: target.enums', - '', - ' this is enum class', - '', - '', - ' .. py:method:: EnumCls.say_goodbye()', - ' :module: target.enums', - ' :classmethod:', - '', - ' a classmethod says good-bye to you.', - '', - '', - ' .. py:method:: EnumCls.say_hello()', - ' :module: target.enums', - '', - ' a method says hello to you.', - '', - '', - ' .. py:attribute:: EnumCls.val1', - ' :module: target.enums', - ' :value: 12', - '', - ' doc for val1', - '', - '', - ' .. py:attribute:: EnumCls.val2', - ' :module: target.enums', - ' :value: 23', - '', - ' doc for val2', - '', - '', - ' .. py:attribute:: EnumCls.val3', - ' :module: target.enums', - ' :value: 34', - '', - ' doc for val3', - '', + *fmt.brief('this is enum class'), + *fmt.method('say_goodbye', 'docstring', 'classmethod'), + *fmt.method('say_hello', 'docstring'), + *fmt.member('x', 'x', ''), ] - # checks for an attribute of EnumClass - actual = do_autodoc(app, 'attribute', 'target.enums.EnumCls.val1') + options = autodoc_enum_options | {'inherited-members': None} + actual = do_autodoc(app, 'class', fmt.target, options) assert list(actual) == [ - '', - '.. py:attribute:: EnumCls.val1', - ' :module: target.enums', - ' :value: 12', - '', - ' doc for val1', - '', + *fmt.brief('this is enum class'), + *fmt.entry('dtype', 'docstring', role='property'), + *fmt.method('isupper', 'inherited'), + *fmt.method('say_goodbye', 'docstring', 'classmethod'), + *fmt.method('say_hello', 'docstring'), + *fmt.member('x', 'x', ''), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_class_with_mixin_type(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumClassWithMixinType') + + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('say_goodbye', 'docstring', 'classmethod'), + *fmt.method('say_hello', 'docstring'), + *fmt.member('x', 'X', ''), + ] + + options = autodoc_enum_options | {'inherited-members': None} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('say_goodbye', 'docstring', 'classmethod'), + *fmt.method('say_hello', 'docstring'), + *fmt.entry('value', 'uppercased', role='property'), + *fmt.member('x', 'X', ''), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_class_with_mixin_type_and_inheritence(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumClassWithMixinTypeInherit') + + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.member('x', 'X', ''), + ] + + options = autodoc_enum_options | {'inherited-members': None} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('say_goodbye', 'inherited', 'classmethod'), + *fmt.method('say_hello', 'inherited'), + *fmt.entry('value', 'uppercased', role='property'), + *fmt.member('x', 'X', ''), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_class_with_mixin_enum_type(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumClassWithMixinEnumType') + + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + # override() is overridden at the class level so it should be rendered + *fmt.method('override', 'overridden'), + # say_goodbye() and say_hello() are not rendered since they are inherited + *fmt.member('x', 'x', ''), + ] + + options = autodoc_enum_options | {'inherited-members': None} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('override', 'overridden'), + *fmt.method('say_goodbye', 'inherited', 'classmethod'), + *fmt.method('say_hello', 'inherited'), + *fmt.member('x', 'x', ''), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_class_with_mixin_and_data_type(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumClassWithMixinAndDataType') + + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('isupper', 'overridden'), + *fmt.method('say_goodbye', 'overridden', 'classmethod'), + *fmt.method('say_hello', 'overridden'), + *fmt.member('x', 'X', ''), + ] + + # add the special member __str__ (but not the inherited members) + options = autodoc_enum_options | {'special-members': '__str__'} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('__str__', 'overridden'), + *fmt.method('isupper', 'overridden'), + *fmt.method('say_goodbye', 'overridden', 'classmethod'), + *fmt.method('say_hello', 'overridden'), + *fmt.member('x', 'X', ''), + ] + + options = autodoc_enum_options | {'inherited-members': None} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('dtype', 'docstring', role='property'), + *fmt.method('isupper', 'overridden'), + *fmt.method('say_goodbye', 'overridden', 'classmethod'), + *fmt.method('say_hello', 'overridden'), + *fmt.entry('value', 'uppercased', role='property'), + *fmt.member('x', 'X', ''), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_with_parent_enum(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumClassWithParentEnum') + + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('isupper', 'overridden'), + *fmt.member('x', 'X', ''), + ] + + # add the special member __str__ (but not the inherited members) + options = autodoc_enum_options | {'special-members': '__str__'} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('__str__', 'overridden'), + *fmt.method('isupper', 'overridden'), + *fmt.member('x', 'X', ''), + ] + + options = autodoc_enum_options | {'inherited-members': None} + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('dtype', 'docstring', role='property'), + *fmt.method('isupper', 'overridden'), + *fmt.method('override', 'inherited'), + *fmt.method('say_goodbye', 'inherited', 'classmethod'), + *fmt.method('say_hello', 'inherited'), + *fmt.entry('value', 'uppercased', role='property'), + *fmt.member('x', 'X', ''), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_sunder_method(app, autodoc_enum_options): + PRIVATE = {'private-members': None} # sunder methods are recognized as private + + fmt = _EnumFormatter('EnumSunderMissingInNonEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE) + assert list(actual) == [*fmt.brief('this is enum class')] + + fmt = _EnumFormatter('EnumSunderMissingInEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE) + assert list(actual) == [*fmt.brief('this is enum class')] + + fmt = _EnumFormatter('EnumSunderMissingInDataType') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE) + assert list(actual) == [*fmt.brief('this is enum class')] + + fmt = _EnumFormatter('EnumSunderMissingInClass') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options | PRIVATE) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('_missing_', 'docstring', 'classmethod', args='(value)'), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_inherited_sunder_method(app, autodoc_enum_options): + options = autodoc_enum_options | {'private-members': None, 'inherited-members': None} + + fmt = _EnumFormatter('EnumSunderMissingInNonEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('_missing_', 'inherited', 'classmethod', args='(value)'), + ] + + fmt = _EnumFormatter('EnumSunderMissingInEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('_missing_', 'inherited', 'classmethod', args='(value)'), + ] + + fmt = _EnumFormatter('EnumSunderMissingInDataType') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('_missing_', 'inherited', 'classmethod', args='(value)'), + *fmt.entry('dtype', 'docstring', role='property'), + *fmt.method('isupper', 'inherited'), + ] + + fmt = _EnumFormatter('EnumSunderMissingInClass') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.method('_missing_', 'docstring', 'classmethod', args='(value)'), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_custom_name_property(app, autodoc_enum_options): + fmt = _EnumFormatter('EnumNamePropertyInNonEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + + fmt = _EnumFormatter('EnumNamePropertyInEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + + fmt = _EnumFormatter('EnumNamePropertyInDataType') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [*fmt.brief('this is enum class')] + + fmt = _EnumFormatter('EnumNamePropertyInClass') + actual = do_autodoc(app, 'class', fmt.target, autodoc_enum_options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('name', 'docstring', role='property'), + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_enum_inherited_custom_name_property(app, autodoc_enum_options): + options = autodoc_enum_options | {"inherited-members": None} + + fmt = _EnumFormatter('EnumNamePropertyInNonEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('name', 'inherited', role='property'), + ] + + fmt = _EnumFormatter('EnumNamePropertyInEnumMixin') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('name', 'inherited', role='property'), + ] + + fmt = _EnumFormatter('EnumNamePropertyInDataType') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('dtype', 'docstring', role='property'), + *fmt.method('isupper', 'inherited'), + *fmt.entry('name', 'inherited', role='property'), + ] + + fmt = _EnumFormatter('EnumNamePropertyInClass') + actual = do_autodoc(app, 'class', fmt.target, options) + assert list(actual) == [ + *fmt.brief('this is enum class'), + *fmt.entry('name', 'docstring', role='property'), ] @@ -2103,6 +2440,55 @@ def test_singledispatchmethod_automethod(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_singledispatchmethod_classmethod(app): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.singledispatchmethod_classmethod', options) + + assert list(actual) == [ + '', + '.. py:module:: target.singledispatchmethod_classmethod', + '', + '', + '.. py:class:: Foo()', + ' :module: target.singledispatchmethod_classmethod', + '', + ' docstring', + '', + '', + ' .. py:method:: Foo.class_meth(arg, kwarg=None)', + ' Foo.class_meth(arg: float, kwarg=None)', + ' Foo.class_meth(arg: int, kwarg=None)', + ' Foo.class_meth(arg: str, kwarg=None)', + ' Foo.class_meth(arg: dict, kwarg=None)', + ' :module: target.singledispatchmethod_classmethod', + ' :classmethod:', + '', + ' A class method for general use.', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_singledispatchmethod_classmethod_automethod(app): + options = {} + actual = do_autodoc(app, 'method', 'target.singledispatchmethod_classmethod.Foo.class_meth', options) + + assert list(actual) == [ + '', + '.. py:method:: Foo.class_meth(arg, kwarg=None)', + ' Foo.class_meth(arg: float, kwarg=None)', + ' Foo.class_meth(arg: int, kwarg=None)', + ' Foo.class_meth(arg: str, kwarg=None)', + ' Foo.class_meth(arg: dict, kwarg=None)', + ' :module: target.singledispatchmethod_classmethod', + ' :classmethod:', + '', + ' A class method for general use.', + '', + ] + + @pytest.mark.skipif(sys.version_info[:2] >= (3, 13), reason='Cython does not support Python 3.13 yet.') @pytest.mark.skipif(pyximport is None, reason='cython is not installed') @@ -2276,7 +2662,7 @@ def test_pyclass_for_ClassLevelDocumenter(app): @pytest.mark.sphinx('dummy', testroot='ext-autodoc') def test_autodoc(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = app.env.get_doctree('index') assert isinstance(content[3], addnodes.desc) diff --git a/tests/test_ext_autodoc_autoattribute.py b/tests/test_extensions/test_ext_autodoc_autoattribute.py index 0424af0..41fcc99 100644 --- a/tests/test_ext_autodoc_autoattribute.py +++ b/tests/test_extensions/test_ext_autodoc_autoattribute.py @@ -6,7 +6,7 @@ source file translated by test_build. import pytest -from .test_ext_autodoc import do_autodoc +from tests.test_extensions.autodoc_util import do_autodoc @pytest.mark.sphinx('html', testroot='ext-autodoc') diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_extensions/test_ext_autodoc_autoclass.py index 92c259a..3e68d60 100644 --- a/tests/test_ext_autodoc_autoclass.py +++ b/tests/test_extensions/test_ext_autodoc_autoclass.py @@ -11,7 +11,7 @@ from typing import Union import pytest -from .test_ext_autodoc import do_autodoc +from tests.test_extensions.autodoc_util import do_autodoc @pytest.mark.sphinx('html', testroot='ext-autodoc') @@ -169,6 +169,7 @@ def test_undocumented_uninitialized_attributes(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') def test_decorators(app): actual = do_autodoc(app, 'class', 'target.decorator.Baz') assert list(actual) == [ @@ -275,8 +276,7 @@ def test_show_inheritance_for_subclass_of_generic_type(app): '.. py:class:: Quux(iterable=(), /)', ' :module: target.classes', '', - ' Bases: :py:class:`~typing.List`\\ ' - '[:py:obj:`~typing.Union`\\ [:py:class:`int`, :py:class:`float`]]', + ' Bases: :py:class:`~typing.List`\\ [:py:class:`int` | :py:class:`float`]', '', ' A subclass of List[Union[int, float]]', '', @@ -373,6 +373,7 @@ def test_class_doc_from_both(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') def test_class_alias(app): def autodoc_process_docstring(*args): """A handler always raises an error. @@ -391,6 +392,7 @@ def test_class_alias(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') def test_class_alias_having_doccomment(app): actual = do_autodoc(app, 'class', 'target.classes.OtherAlias') assert list(actual) == [ @@ -403,6 +405,7 @@ def test_class_alias_having_doccomment(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') def test_class_alias_for_imported_object_having_doccomment(app): actual = do_autodoc(app, 'class', 'target.classes.IntAlias') assert list(actual) == [ @@ -515,3 +518,49 @@ def test_autoattribute_TypeVar_module_level(app): " alias of TypeVar('T1')", '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_inherited_instance_variable_with_annotations(app): + options = {'members': None, + 'inherited-members': None} + actual = do_autodoc(app, 'class', 'target.inherited_annotations.NoTypeAnnotation', options) + assert list(actual) == [ + '', + '.. py:class:: NoTypeAnnotation()', + ' :module: target.inherited_annotations', + '', + '', + ' .. py:attribute:: NoTypeAnnotation.a', + ' :module: target.inherited_annotations', + ' :value: 1', + '', + ' Local', + '', + '', + ' .. py:attribute:: NoTypeAnnotation.inherit_me', + ' :module: target.inherited_annotations', + ' :type: int', + '', + ' Inherited', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_no_inherited_instance_variable_with_annotations(app): + options = {'members': None} + actual = do_autodoc(app, 'class', 'target.inherited_annotations.NoTypeAnnotation2', options) + assert list(actual) == [ + '', + '.. py:class:: NoTypeAnnotation2()', + ' :module: target.inherited_annotations', + '', + '', + ' .. py:attribute:: NoTypeAnnotation2.a', + ' :module: target.inherited_annotations', + ' :value: 1', + '', + ' Local', + '', + ] diff --git a/tests/test_ext_autodoc_autodata.py b/tests/test_extensions/test_ext_autodoc_autodata.py index 83647d9..b794666 100644 --- a/tests/test_ext_autodoc_autodata.py +++ b/tests/test_extensions/test_ext_autodoc_autodata.py @@ -6,7 +6,7 @@ source file translated by test_build. import pytest -from .test_ext_autodoc import do_autodoc +from tests.test_extensions.autodoc_util import do_autodoc @pytest.mark.sphinx('html', testroot='ext-autodoc') diff --git a/tests/test_ext_autodoc_autofunction.py b/tests/test_extensions/test_ext_autodoc_autofunction.py index b0cd7d9..5dfa42d 100644 --- a/tests/test_ext_autodoc_autofunction.py +++ b/tests/test_extensions/test_ext_autodoc_autofunction.py @@ -6,7 +6,7 @@ source file translated by test_build. import pytest -from .test_ext_autodoc import do_autodoc +from tests.test_extensions.autodoc_util import do_autodoc @pytest.mark.sphinx('html', testroot='ext-autodoc') @@ -199,3 +199,14 @@ def test_async_generator(app): ' :async:', '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_slice_function_arg(app): + actual = do_autodoc(app, 'function', 'target.functions.slice_arg_func') + assert list(actual) == [ + '', + '.. py:function:: slice_arg_func(arg: float64[:, :])', + ' :module: target.functions', + '', + ] diff --git a/tests/test_ext_autodoc_automodule.py b/tests/test_extensions/test_ext_autodoc_automodule.py index 2855020..92565ae 100644 --- a/tests/test_ext_autodoc_automodule.py +++ b/tests/test_extensions/test_ext_autodoc_automodule.py @@ -8,7 +8,7 @@ import sys import pytest -from .test_ext_autodoc import do_autodoc +from tests.test_extensions.autodoc_util import do_autodoc @pytest.mark.sphinx('html', testroot='ext-autodoc') diff --git a/tests/test_ext_autodoc_autoproperty.py b/tests/test_extensions/test_ext_autodoc_autoproperty.py index ca8b981..de33117 100644 --- a/tests/test_ext_autodoc_autoproperty.py +++ b/tests/test_extensions/test_ext_autodoc_autoproperty.py @@ -6,7 +6,7 @@ source file translated by test_build. import pytest -from .test_ext_autodoc import do_autodoc +from tests.test_extensions.autodoc_util import do_autodoc @pytest.mark.sphinx('html', testroot='ext-autodoc') diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_extensions/test_ext_autodoc_configs.py index 45bc729..6c2af5a 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_extensions/test_ext_autodoc_configs.py @@ -8,7 +8,7 @@ import pytest from sphinx.testing import restructuredtext -from .test_ext_autodoc import do_autodoc +from tests.test_extensions.autodoc_util import do_autodoc IS_PYPY = platform.python_implementation() == 'PyPy' @@ -969,9 +969,9 @@ def test_autodoc_typehints_description(app): assert ('target.typehints.incr(a, b=1)\n' '\n' ' Parameters:\n' - ' * **a** (*int*) --\n' + ' * **a** (*int*)\n' '\n' - ' * **b** (*int*) --\n' + ' * **b** (*int*)\n' '\n' ' Return type:\n' ' int\n' @@ -979,7 +979,7 @@ def test_autodoc_typehints_description(app): assert ('target.typehints.tuple_args(x)\n' '\n' ' Parameters:\n' - ' **x** (*tuple**[**int**, **int** | **str**]*) --\n' + ' **x** (*tuple**[**int**, **int** | **str**]*)\n' '\n' ' Return type:\n' ' tuple[int, int]\n' @@ -1117,11 +1117,11 @@ def test_autodoc_typehints_description_with_documented_init(app): ' Class docstring.\n' '\n' ' Parameters:\n' - ' * **x** (*int*) --\n' + ' * **x** (*int*)\n' '\n' - ' * **args** (*int*) --\n' + ' * **args** (*int*)\n' '\n' - ' * **kwargs** (*int*) --\n' + ' * **kwargs** (*int*)\n' '\n' ' __init__(x, *args, **kwargs)\n' '\n' @@ -1217,9 +1217,9 @@ def test_autodoc_typehints_both(app): assert ('target.typehints.incr(a: int, b: int = 1) -> int\n' '\n' ' Parameters:\n' - ' * **a** (*int*) --\n' + ' * **a** (*int*)\n' '\n' - ' * **b** (*int*) --\n' + ' * **b** (*int*)\n' '\n' ' Return type:\n' ' int\n' @@ -1227,7 +1227,7 @@ def test_autodoc_typehints_both(app): assert ('target.typehints.tuple_args(x: tuple[int, int | str]) -> tuple[int, int]\n' '\n' ' Parameters:\n' - ' **x** (*tuple**[**int**, **int** | **str**]*) --\n' + ' **x** (*tuple**[**int**, **int** | **str**]*)\n' '\n' ' Return type:\n' ' tuple[int, int]\n' @@ -1401,9 +1401,9 @@ def test_autodoc_typehints_description_and_type_aliases(app): ' docstring\n' '\n' ' Parameters:\n' - ' * **x** (*myint*) --\n' + ' * **x** (*myint*)\n' '\n' - ' * **y** (*myint*) --\n' + ' * **y** (*myint*)\n' '\n' ' Return type:\n' ' myint\n' @@ -1584,6 +1584,14 @@ def test_autodoc_typehints_format_fully_qualified_for_newtype_alias(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_default_options(app): + if ( + (3, 11, 7) <= sys.version_info < (3, 12) + or sys.version_info >= (3, 12, 1) + ): + list_of_weak_references = " list of weak references to the object" + else: + list_of_weak_references = " list of weak references to the object (if defined)" + # no settings actual = do_autodoc(app, 'class', 'target.enums.EnumCls') assert ' .. py:attribute:: EnumCls.val1' not in actual @@ -1627,7 +1635,7 @@ def test_autodoc_default_options(app): assert ' Iterate squares of each value.' in actual if not IS_PYPY: assert ' .. py:attribute:: CustomIter.__weakref__' in actual - assert ' list of weak references to the object (if defined)' in actual + assert list_of_weak_references in actual # :exclude-members: None - has no effect. Unlike :members:, # :special-members:, etc. where None == "include all", here None means @@ -1651,13 +1659,21 @@ def test_autodoc_default_options(app): assert ' Iterate squares of each value.' in actual if not IS_PYPY: assert ' .. py:attribute:: CustomIter.__weakref__' in actual - assert ' list of weak references to the object (if defined)' in actual + assert list_of_weak_references in actual assert ' .. py:method:: CustomIter.snafucate()' in actual assert ' Makes this snafucated.' in actual @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_default_options_with_values(app): + if ( + (3, 11, 7) <= sys.version_info < (3, 12) + or sys.version_info >= (3, 12, 1) + ): + list_of_weak_references = " list of weak references to the object" + else: + list_of_weak_references = " list of weak references to the object (if defined)" + # with :members: app.config.autodoc_default_options = {'members': 'val1,val2'} actual = do_autodoc(app, 'class', 'target.enums.EnumCls') @@ -1698,7 +1714,7 @@ def test_autodoc_default_options_with_values(app): assert ' Iterate squares of each value.' in actual if not IS_PYPY: assert ' .. py:attribute:: CustomIter.__weakref__' not in actual - assert ' list of weak references to the object (if defined)' not in actual + assert list_of_weak_references not in actual # with :exclude-members: app.config.autodoc_default_options = { @@ -1722,6 +1738,6 @@ def test_autodoc_default_options_with_values(app): assert ' Iterate squares of each value.' in actual if not IS_PYPY: assert ' .. py:attribute:: CustomIter.__weakref__' not in actual - assert ' list of weak references to the object (if defined)' not in actual + assert list_of_weak_references not in actual assert ' .. py:method:: CustomIter.snafucate()' not in actual assert ' Makes this snafucated.' not in actual diff --git a/tests/test_ext_autodoc_events.py b/tests/test_extensions/test_ext_autodoc_events.py index d821f4c..c0af254 100644 --- a/tests/test_ext_autodoc_events.py +++ b/tests/test_extensions/test_ext_autodoc_events.py @@ -4,7 +4,7 @@ import pytest from sphinx.ext.autodoc import between, cut_lines -from .test_ext_autodoc import do_autodoc +from tests.test_extensions.autodoc_util import do_autodoc @pytest.mark.sphinx('html', testroot='ext-autodoc') diff --git a/tests/test_ext_autodoc_mock.py b/tests/test_extensions/test_ext_autodoc_mock.py index 3b90693..3b90693 100644 --- a/tests/test_ext_autodoc_mock.py +++ b/tests/test_extensions/test_ext_autodoc_mock.py diff --git a/tests/test_ext_autodoc_preserve_defaults.py b/tests/test_extensions/test_ext_autodoc_preserve_defaults.py index 70b6146..c1a00ab 100644 --- a/tests/test_ext_autodoc_preserve_defaults.py +++ b/tests/test_extensions/test_ext_autodoc_preserve_defaults.py @@ -2,7 +2,7 @@ import pytest -from .test_ext_autodoc import do_autodoc +from tests.test_extensions.autodoc_util import do_autodoc @pytest.mark.sphinx('html', testroot='ext-autodoc', diff --git a/tests/test_ext_autodoc_private_members.py b/tests/test_extensions/test_ext_autodoc_private_members.py index bf707bf..bf14414 100644 --- a/tests/test_ext_autodoc_private_members.py +++ b/tests/test_extensions/test_ext_autodoc_private_members.py @@ -3,7 +3,7 @@ import pytest -from .test_ext_autodoc import do_autodoc +from tests.test_extensions.autodoc_util import do_autodoc @pytest.mark.sphinx('html', testroot='ext-autodoc') diff --git a/tests/test_ext_autosectionlabel.py b/tests/test_extensions/test_ext_autosectionlabel.py index f99a6d3..f854ecf 100644 --- a/tests/test_ext_autosectionlabel.py +++ b/tests/test_extensions/test_ext_autosectionlabel.py @@ -7,42 +7,42 @@ import pytest @pytest.mark.sphinx('html', testroot='ext-autosectionlabel') def test_autosectionlabel_html(app, status, warning, skipped_labels=False): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') html = ('<li><p><a class="reference internal" href="#introduce-of-sphinx">' '<span class=".*?">Introduce of Sphinx</span></a></p></li>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) html = ('<li><p><a class="reference internal" href="#installation">' '<span class="std std-ref">Installation</span></a></p></li>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) html = ('<li><p><a class="reference internal" href="#for-windows-users">' '<span class="std std-ref">For Windows users</span></a></p></li>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) html = ('<li><p><a class="reference internal" href="#for-unix-users">' '<span class="std std-ref">For UNIX users</span></a></p></li>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) html = ('<li><p><a class="reference internal" href="#linux">' '<span class="std std-ref">Linux</span></a></p></li>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) html = ('<li><p><a class="reference internal" href="#freebsd">' '<span class="std std-ref">FreeBSD</span></a></p></li>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) # for smart_quotes (refs: #4027) html = ('<li><p><a class="reference internal" ' 'href="#this-one-s-got-an-apostrophe">' '<span class="std std-ref">This one’s got an apostrophe' '</span></a></p></li>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) -# Re-use test definition from above, just change the test root directory +# Reuse test definition from above, just change the test root directory @pytest.mark.sphinx('html', testroot='ext-autosectionlabel-prefix-document') def test_autosectionlabel_prefix_document_html(app, status, warning): test_autosectionlabel_html(app, status, warning) @@ -51,27 +51,27 @@ def test_autosectionlabel_prefix_document_html(app, status, warning): @pytest.mark.sphinx('html', testroot='ext-autosectionlabel', confoverrides={'autosectionlabel_maxdepth': 3}) def test_autosectionlabel_maxdepth(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') # depth: 1 html = ('<li><p><a class="reference internal" href="#test-ext-autosectionlabel">' '<span class=".*?">test-ext-autosectionlabel</span></a></p></li>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) # depth: 2 html = ('<li><p><a class="reference internal" href="#installation">' '<span class="std std-ref">Installation</span></a></p></li>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) # depth: 3 html = ('<li><p><a class="reference internal" href="#for-windows-users">' '<span class="std std-ref">For Windows users</span></a></p></li>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) # depth: 4 html = '<li><p><span class="xref std std-ref">Linux</span></p></li>' - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) assert "WARNING: undefined label: 'linux'" in warning.getvalue() diff --git a/tests/test_ext_autosummary.py b/tests/test_extensions/test_ext_autosummary.py index 43f3ae0..d761978 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_extensions/test_ext_autosummary.py @@ -154,7 +154,7 @@ def test_get_items_summary(make_app, app_params): def new_get_items(self, names, *args, **kwargs): results = orig_get_items(self, names, *args, **kwargs) for name, result in zip(names, results): - autosummary_items[name] = result + autosummary_items[name] = result # NoQA: PERF403 return results def handler(app, what, name, obj, options, lines): @@ -167,7 +167,7 @@ def test_get_items_summary(make_app, app_params): sphinx.ext.autosummary.Autosummary.get_items = new_get_items try: - app.builder.build_all() + app.build(force_all=True) finally: sphinx.ext.autosummary.Autosummary.get_items = orig_get_items @@ -207,7 +207,7 @@ def str_content(elem): @pytest.mark.sphinx('xml', **default_kw) def test_escaping(app, status, warning): - app.builder.build_all() + app.build(force_all=True) outdir = Path(app.builder.outdir) @@ -358,7 +358,7 @@ def test_autosummary_generate_content_for_module_imported_members_inherited_modu @pytest.mark.sphinx('dummy', testroot='ext-autosummary') def test_autosummary_generate(app, status, warning): - app.builder.build_all() + app.build(force_all=True) doctree = app.env.get_doctree('index') assert_node(doctree, (nodes.paragraph, @@ -544,7 +544,7 @@ def test_autosummary_filename_map(app, status, warning): @pytest.mark.sphinx('latex', **default_kw) def test_autosummary_latex_table_colspec(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'python.tex').read_text(encoding='utf8') print(status.getvalue()) print(warning.getvalue()) diff --git a/tests/test_ext_coverage.py b/tests/test_extensions/test_ext_coverage.py index af8cf53..c9e9ba9 100644 --- a/tests/test_ext_coverage.py +++ b/tests/test_extensions/test_ext_coverage.py @@ -7,7 +7,7 @@ import pytest @pytest.mark.sphinx('coverage') def test_build(app, status, warning): - app.builder.build_all() + app.build(force_all=True) py_undoc = (app.outdir / 'python.txt').read_text(encoding='utf8') assert py_undoc.startswith('Undocumented Python objects\n' @@ -45,7 +45,7 @@ def test_build(app, status, warning): @pytest.mark.sphinx('coverage', testroot='ext-coverage') def test_coverage_ignore_pyobjects(app, status, warning): - app.builder.build_all() + app.build(force_all=True) actual = (app.outdir / 'python.txt').read_text(encoding='utf8') expected = '''\ Undocumented Python objects @@ -78,7 +78,7 @@ Classes: @pytest.mark.sphinx('coverage', confoverrides={'coverage_show_missing_items': True}) def test_show_missing_items(app, status, warning): - app.builder.build_all() + app.build(force_all=True) assert "undocumented" in status.getvalue() @@ -92,7 +92,7 @@ def test_show_missing_items(app, status, warning): @pytest.mark.sphinx('coverage', confoverrides={'coverage_show_missing_items': True}) def test_show_missing_items_quiet(app, status, warning): app.quiet = True - app.builder.build_all() + app.build(force_all=True) assert "undocumented python function: autodoc_target :: raises" in warning.getvalue() assert "undocumented python class: autodoc_target :: Base" in warning.getvalue() diff --git a/tests/test_ext_doctest.py b/tests/test_extensions/test_ext_doctest.py index c83e582..ab0dd62 100644 --- a/tests/test_ext_doctest.py +++ b/tests/test_extensions/test_ext_doctest.py @@ -16,9 +16,8 @@ cleanup_called = 0 def test_build(app, status, warning): global cleanup_called cleanup_called = 0 - app.builder.build_all() - if app.statuscode != 0: - raise AssertionError('failures in doctests:' + status.getvalue()) + app.build(force_all=True) + assert app.statuscode == 0, f'failures in doctests:\n{status.getvalue()}' # in doctest.txt, there are two named groups and the default group, # so the cleanup function must be called three times assert cleanup_called == 3, 'testcleanup did not get executed enough times' @@ -80,13 +79,13 @@ def test_skipif(app, status, warning): The tests are separated into a different test root directory since the ``app`` object only evaluates options once in its lifetime. If these tests were combined with the other doctest tests, the ``:skipif:`` evaluations - would be recorded only on the first ``app.builder.build_all()`` run, i.e. + would be recorded only on the first ``app.build(force_all=True)`` run, i.e. in ``test_build`` above, and the assertion below would fail. """ global recorded_calls recorded_calls = Counter() - app.builder.build_all() + app.build(force_all=True) if app.statuscode != 0: raise AssertionError('failures in doctests:' + status.getvalue()) # The `:skipif:` expressions are always run. @@ -124,7 +123,7 @@ def test_reporting_with_autodoc(app, status, warning, capfd): # Patch builder to get a copy of the output written = [] app.builder._warn_out = written.append - app.builder.build_all() + app.build(force_all=True) failures = [line.replace(os.sep, '/') for line in '\n'.join(written).splitlines() diff --git a/tests/test_ext_duration.py b/tests/test_extensions/test_ext_duration.py index 4fa4dfc..4fa4dfc 100644 --- a/tests/test_ext_duration.py +++ b/tests/test_extensions/test_ext_duration.py diff --git a/tests/test_ext_extlinks.py b/tests/test_extensions/test_ext_extlinks.py index 7634db6..7634db6 100644 --- a/tests/test_ext_extlinks.py +++ b/tests/test_extensions/test_ext_extlinks.py diff --git a/tests/test_ext_githubpages.py b/tests/test_extensions/test_ext_githubpages.py index 8e41537..879b6d1 100644 --- a/tests/test_ext_githubpages.py +++ b/tests/test_extensions/test_ext_githubpages.py @@ -5,7 +5,7 @@ import pytest @pytest.mark.sphinx('html', testroot='ext-githubpages') def test_githubpages(app, status, warning): - app.builder.build_all() + app.build(force_all=True) assert (app.outdir / '.nojekyll').exists() assert not (app.outdir / 'CNAME').exists() @@ -13,7 +13,7 @@ def test_githubpages(app, status, warning): @pytest.mark.sphinx('html', testroot='ext-githubpages', confoverrides={'html_baseurl': 'https://sphinx-doc.github.io'}) def test_no_cname_for_github_io_domain(app, status, warning): - app.builder.build_all() + app.build(force_all=True) assert (app.outdir / '.nojekyll').exists() assert not (app.outdir / 'CNAME').exists() @@ -21,6 +21,6 @@ def test_no_cname_for_github_io_domain(app, status, warning): @pytest.mark.sphinx('html', testroot='ext-githubpages', confoverrides={'html_baseurl': 'https://sphinx-doc.org'}) def test_cname_for_custom_domain(app, status, warning): - app.builder.build_all() + app.build(force_all=True) assert (app.outdir / '.nojekyll').exists() assert (app.outdir / 'CNAME').read_text(encoding='utf8') == 'sphinx-doc.org' diff --git a/tests/test_ext_graphviz.py b/tests/test_extensions/test_ext_graphviz.py index d63dc2a..866a92a 100644 --- a/tests/test_ext_graphviz.py +++ b/tests/test_extensions/test_ext_graphviz.py @@ -11,40 +11,40 @@ from sphinx.ext.graphviz import ClickableMapDefinition @pytest.mark.sphinx('html', testroot='ext-graphviz') @pytest.mark.usefixtures('if_graphviz_found') def test_graphviz_png_html(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') html = (r'<figure class="align-default" .*?>\s*' r'<div class="graphviz"><img .*?/></div>\s*<figcaption>\s*' r'<p><span class="caption-text">caption of graph</span>.*</p>\s*' r'</figcaption>\s*</figure>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) html = 'Hello <div class="graphviz"><img .*?/></div>\n graphviz world' - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) html = ('<img src=".*?" alt="digraph foo {\nbaz -> qux\n}" ' 'class="graphviz neato-graph" />') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) html = (r'<figure class="align-right" .*?>\s*' r'<div class="graphviz"><img .*?/></div>\s*<figcaption>\s*' r'<p><span class="caption-text">on <em>right</em></span>.*</p>\s*' r'</figcaption>\s*</figure>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) html = (r'<div align=\"center\" class=\"align-center\">' r'<div class="graphviz"><img src=\".*\.png\" alt=\"digraph foo {\n' r'centered\n' r'}\" class="graphviz" /></div>\n</div>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) @pytest.mark.sphinx('html', testroot='ext-graphviz', confoverrides={'graphviz_output_format': 'svg'}) @pytest.mark.usefixtures('if_graphviz_found') def test_graphviz_svg_html(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') @@ -57,12 +57,12 @@ def test_graphviz_svg_html(app, status, warning): r'<p><span class=\"caption-text\">caption of graph</span>.*</p>\n' r'</figcaption>\n' r'</figure>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) html = (r'Hello <div class="graphviz"><object.*>\n' r'\s*<p class=\"warning\">graph</p></object></div>\n' r' graphviz world') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) html = (r'<figure class=\"align-right\" .*\>\n' r'<div class="graphviz"><object data=\".*\.svg\".*>\n' @@ -73,7 +73,7 @@ def test_graphviz_svg_html(app, status, warning): r'<p><span class=\"caption-text\">on <em>right</em></span>.*</p>\n' r'</figcaption>\n' r'</figure>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) html = (r'<div align=\"center\" class=\"align-center\">' r'<div class="graphviz"><object data=\".*\.svg\".*>\n' @@ -81,10 +81,10 @@ def test_graphviz_svg_html(app, status, warning): r'centered\n' r'}</p></object></div>\n' r'</div>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) image_re = r'.*data="([^"]+)".*?digraph test' - image_path_match = re.search(image_re, content, re.S) + image_path_match = re.search(image_re, content, re.DOTALL) assert image_path_match image_path = image_path_match.group(1) @@ -103,37 +103,37 @@ def test_graphviz_svg_html(app, status, warning): @pytest.mark.sphinx('latex', testroot='ext-graphviz') @pytest.mark.usefixtures('if_graphviz_found') def test_graphviz_latex(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'python.tex').read_text(encoding='utf8') macro = ('\\\\begin{figure}\\[htbp\\]\n\\\\centering\n\\\\capstart\n\n' '\\\\sphinxincludegraphics\\[\\]{graphviz-\\w+.pdf}\n' '\\\\caption{caption of graph}\\\\label{.*}\\\\end{figure}') - assert re.search(macro, content, re.S) + assert re.search(macro, content, re.DOTALL) macro = 'Hello \\\\sphinxincludegraphics\\[\\]{graphviz-\\w+.pdf} graphviz world' - assert re.search(macro, content, re.S) + assert re.search(macro, content, re.DOTALL) macro = ('\\\\begin{wrapfigure}{r}{0pt}\n\\\\centering\n' '\\\\sphinxincludegraphics\\[\\]{graphviz-\\w+.pdf}\n' '\\\\caption{on \\\\sphinxstyleemphasis{right}}' '\\\\label{.*}\\\\end{wrapfigure}') - assert re.search(macro, content, re.S) + assert re.search(macro, content, re.DOTALL) macro = (r'\{\\hfill' r'\\sphinxincludegraphics\[\]{graphviz-.*}' r'\\hspace\*{\\fill}}') - assert re.search(macro, content, re.S) + assert re.search(macro, content, re.DOTALL) @pytest.mark.sphinx('html', testroot='ext-graphviz', confoverrides={'language': 'xx'}) @pytest.mark.usefixtures('if_graphviz_found') def test_graphviz_i18n(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') html = '<img src=".*?" alt="digraph {\n BAR -> BAZ\n}" class="graphviz" />' - assert re.search(html, content, re.M) + assert re.search(html, content, re.MULTILINE) def test_graphviz_parse_mapfile(): @@ -150,17 +150,17 @@ def test_graphviz_parse_mapfile(): # normal graph code = ('digraph {\n' - ' foo [href="http://www.google.com/"];\n' + ' foo [href="https://www.google.com/"];\n' ' foo -> bar;\n' '}\n') content = ('<map id="%3" name="%3">\n' - '<area shape="poly" id="node1" href="http://www.google.com/" title="foo" alt=""' + '<area shape="poly" id="node1" href="https://www.google.com/" title="foo" alt=""' ' coords="77,29,76,22,70,15,62,10,52,7,41,5,30,7,20,10,12,15,7,22,5,29,7,37,12,' '43,20,49,30,52,41,53,52,52,62,49,70,43,76,37"/>\n' '</map>') cmap = ClickableMapDefinition('dummy.map', content, code) assert cmap.filename == 'dummy.map' - assert cmap.id == 'grapviza4ccdd48ce' + assert cmap.id == 'grapvizff087ab863' assert len(cmap.clickable) == 1 assert cmap.generate_clickable_map() == content.replace('%3', cmap.id) diff --git a/tests/test_ext_ifconfig.py b/tests/test_extensions/test_ext_ifconfig.py index 0292699..3e46b1e 100644 --- a/tests/test_ext_ifconfig.py +++ b/tests/test_extensions/test_ext_ifconfig.py @@ -9,7 +9,7 @@ from sphinx.testing import restructuredtext @pytest.mark.sphinx('text', testroot='ext-ifconfig') def test_ifconfig(app, status, warning): - app.builder.build_all() + app.build(force_all=True) result = (app.outdir / 'index.txt').read_text(encoding='utf8') assert 'spam' in result assert 'ham' not in result diff --git a/tests/test_ext_imgconverter.py b/tests/test_extensions/test_ext_imgconverter.py index 18be700..c1d2061 100644 --- a/tests/test_ext_imgconverter.py +++ b/tests/test_extensions/test_ext_imgconverter.py @@ -10,7 +10,8 @@ def _if_converter_found(app): image_converter = getattr(app.config, 'image_converter', '') try: if image_converter: - subprocess.run([image_converter, '-version'], capture_output=True) # show version + # print the image_converter version, to check that the command is available + subprocess.run([image_converter, '-version'], capture_output=True, check=False) return except OSError: # No such file or directory pass @@ -21,7 +22,7 @@ def _if_converter_found(app): @pytest.mark.usefixtures('_if_converter_found') @pytest.mark.sphinx('latex', testroot='ext-imgconverter') def test_ext_imgconverter(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'python.tex').read_text(encoding='utf8') diff --git a/tests/test_ext_imgmockconverter.py b/tests/test_extensions/test_ext_imgmockconverter.py index b5d4e79..4c3c64e 100644 --- a/tests/test_ext_imgmockconverter.py +++ b/tests/test_extensions/test_ext_imgmockconverter.py @@ -5,7 +5,7 @@ import pytest @pytest.mark.sphinx('latex', testroot='ext-imgmockconverter') def test_ext_imgmockconverter(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'python.tex').read_text(encoding='utf8') diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_extensions/test_ext_inheritance_diagram.py index 9ace5ad..c13ccea 100644 --- a/tests/test_ext_inheritance_diagram.py +++ b/tests/test_extensions/test_ext_inheritance_diagram.py @@ -33,7 +33,7 @@ def test_inheritance_diagram(app, status, warning): InheritanceDiagram.run = new_run try: - app.builder.build_all() + app.build(force_all=True) finally: InheritanceDiagram.run = orig_run @@ -160,7 +160,7 @@ def test_inheritance_diagram_png_html(tmp_path, app): normalize_intersphinx_mapping(app, app.config) load_mappings(app) - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') base_maps = re.findall('<map .+\n.+\n</map>', content) @@ -171,7 +171,7 @@ def test_inheritance_diagram_png_html(tmp_path, app): 'class="inheritance graphviz" /></div>\n<figcaption>\n<p>' '<span class="caption-text">Test Foo!</span><a class="headerlink" href="#id1" ' 'title="Link to this image">\xb6</a></p>\n</figcaption>\n</figure>\n') - assert re.search(pattern, content, re.M) + assert re.search(pattern, content, re.MULTILINE) subdir_content = (app.outdir / 'subdir/page1.html').read_text(encoding='utf8') subdir_maps = re.findall('<map .+\n.+\n</map>', subdir_content) @@ -207,7 +207,7 @@ def test_inheritance_diagram_svg_html(tmp_path, app): normalize_intersphinx_mapping(app, app.config) load_mappings(app) - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') base_svgs = re.findall('<object data="(_images/inheritance-\\w+.svg?)"', content) @@ -216,12 +216,12 @@ def test_inheritance_diagram_svg_html(tmp_path, app): '<div class="graphviz">' '<object data="_images/inheritance-\\w+.svg" ' 'type="image/svg\\+xml" class="inheritance graphviz">\n' - '<p class=\"warning\">Inheritance diagram of test.Foo</p>' + '<p class="warning">Inheritance diagram of test.Foo</p>' '</object></div>\n<figcaption>\n<p><span class="caption-text">' 'Test Foo!</span><a class="headerlink" href="#id1" ' 'title="Link to this image">\xb6</a></p>\n</figcaption>\n</figure>\n') - assert re.search(pattern, content, re.M) + assert re.search(pattern, content, re.MULTILINE) subdir_content = (app.outdir / 'subdir/page1.html').read_text(encoding='utf8') subdir_svgs = re.findall('<object data="../(_images/inheritance-\\w+.svg?)"', subdir_content) @@ -249,14 +249,14 @@ def test_inheritance_diagram_svg_html(tmp_path, app): @pytest.mark.sphinx('latex', testroot='ext-inheritance_diagram') @pytest.mark.usefixtures('if_graphviz_found') def test_inheritance_diagram_latex(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'python.tex').read_text(encoding='utf8') pattern = ('\\\\begin{figure}\\[htbp]\n\\\\centering\n\\\\capstart\n\n' '\\\\sphinxincludegraphics\\[\\]{inheritance-\\w+.pdf}\n' '\\\\caption{Test Foo!}\\\\label{\\\\detokenize{index:id1}}\\\\end{figure}') - assert re.search(pattern, content, re.M) + assert re.search(pattern, content, re.MULTILINE) @pytest.mark.sphinx('html', testroot='ext-inheritance_diagram', @@ -264,7 +264,7 @@ def test_inheritance_diagram_latex(app, status, warning): @pytest.mark.usefixtures('if_graphviz_found') def test_inheritance_diagram_latex_alias(app, status, warning): app.config.inheritance_alias = {'test.Foo': 'alias.Foo'} - app.builder.build_all() + app.build(force_all=True) doc = app.env.get_and_resolve_doctree('index', app) aliased_graph = doc.children[0].children[3]['graph'].class_info @@ -282,7 +282,7 @@ def test_inheritance_diagram_latex_alias(app, status, warning): 'class="inheritance graphviz" /></div>\n<figcaption>\n<p>' '<span class="caption-text">Test Foo!</span><a class="headerlink" href="#id1" ' 'title="Link to this image">\xb6</a></p>\n</figcaption>\n</figure>\n') - assert re.search(pattern, content, re.M) + assert re.search(pattern, content, re.MULTILINE) def test_import_classes(rootdir): diff --git a/tests/test_ext_intersphinx.py b/tests/test_extensions/test_ext_intersphinx.py index 82bec9e..ef5a9b1 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_extensions/test_ext_intersphinx.py @@ -18,9 +18,10 @@ from sphinx.ext.intersphinx import ( normalize_intersphinx_mapping, ) from sphinx.ext.intersphinx import setup as intersphinx_setup +from sphinx.util.console import strip_colors -from .test_util_inventory import inventory_v2, inventory_v2_not_having_version -from .utils import http_server +from tests.test_util.intersphinx_data import INVENTORY_V2, INVENTORY_V2_NO_VERSION +from tests.utils import http_server def fake_node(domain, type, target, content, **attrs): @@ -52,46 +53,46 @@ def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app, status, _read_from_url().readline.return_value = b'# Sphinx inventory version 2' # same uri and inv, not redirected - _read_from_url().url = 'http://hostname/' + INVENTORY_FILENAME - fetch_inventory(app, 'http://hostname/', 'http://hostname/' + INVENTORY_FILENAME) + _read_from_url().url = 'https://hostname/' + INVENTORY_FILENAME + fetch_inventory(app, 'https://hostname/', 'https://hostname/' + INVENTORY_FILENAME) assert 'intersphinx inventory has moved' not in status.getvalue() - assert InventoryFile.load.call_args[0][1] == 'http://hostname/' + assert InventoryFile.load.call_args[0][1] == 'https://hostname/' # same uri and inv, redirected status.seek(0) status.truncate(0) - _read_from_url().url = 'http://hostname/new/' + INVENTORY_FILENAME + _read_from_url().url = 'https://hostname/new/' + INVENTORY_FILENAME - fetch_inventory(app, 'http://hostname/', 'http://hostname/' + INVENTORY_FILENAME) + fetch_inventory(app, 'https://hostname/', 'https://hostname/' + INVENTORY_FILENAME) assert status.getvalue() == ('intersphinx inventory has moved: ' - 'http://hostname/%s -> http://hostname/new/%s\n' % + 'https://hostname/%s -> https://hostname/new/%s\n' % (INVENTORY_FILENAME, INVENTORY_FILENAME)) - assert InventoryFile.load.call_args[0][1] == 'http://hostname/new' + assert InventoryFile.load.call_args[0][1] == 'https://hostname/new' # different uri and inv, not redirected status.seek(0) status.truncate(0) - _read_from_url().url = 'http://hostname/new/' + INVENTORY_FILENAME + _read_from_url().url = 'https://hostname/new/' + INVENTORY_FILENAME - fetch_inventory(app, 'http://hostname/', 'http://hostname/new/' + INVENTORY_FILENAME) + fetch_inventory(app, 'https://hostname/', 'https://hostname/new/' + INVENTORY_FILENAME) assert 'intersphinx inventory has moved' not in status.getvalue() - assert InventoryFile.load.call_args[0][1] == 'http://hostname/' + assert InventoryFile.load.call_args[0][1] == 'https://hostname/' # different uri and inv, redirected status.seek(0) status.truncate(0) - _read_from_url().url = 'http://hostname/other/' + INVENTORY_FILENAME + _read_from_url().url = 'https://hostname/other/' + INVENTORY_FILENAME - fetch_inventory(app, 'http://hostname/', 'http://hostname/new/' + INVENTORY_FILENAME) + fetch_inventory(app, 'https://hostname/', 'https://hostname/new/' + INVENTORY_FILENAME) assert status.getvalue() == ('intersphinx inventory has moved: ' - 'http://hostname/new/%s -> http://hostname/other/%s\n' % + 'https://hostname/new/%s -> https://hostname/other/%s\n' % (INVENTORY_FILENAME, INVENTORY_FILENAME)) - assert InventoryFile.load.call_args[0][1] == 'http://hostname/' + assert InventoryFile.load.call_args[0][1] == 'https://hostname/' def test_missing_reference(tmp_path, app, status, warning): inv_file = tmp_path / 'inventory' - inv_file.write_bytes(inventory_v2) + inv_file.write_bytes(INVENTORY_V2) set_config(app, { 'https://docs.python.org/': str(inv_file), 'py3k': ('https://docs.python.org/py3k/', str(inv_file)), @@ -169,7 +170,7 @@ def test_missing_reference(tmp_path, app, status, warning): def test_missing_reference_pydomain(tmp_path, app, status, warning): inv_file = tmp_path / 'inventory' - inv_file.write_bytes(inventory_v2) + inv_file.write_bytes(INVENTORY_V2) set_config(app, { 'https://docs.python.org/': str(inv_file), }) @@ -196,20 +197,10 @@ def test_missing_reference_pydomain(tmp_path, app, status, warning): rn = missing_reference(app, app.env, node, contnode) assert rn.astext() == 'Foo.bar' - # term reference (normal) - node, contnode = fake_node('std', 'term', 'a term', 'a term') - rn = missing_reference(app, app.env, node, contnode) - assert rn.astext() == 'a term' - - # term reference (case insensitive) - node, contnode = fake_node('std', 'term', 'A TERM', 'A TERM') - rn = missing_reference(app, app.env, node, contnode) - assert rn.astext() == 'A TERM' - def test_missing_reference_stddomain(tmp_path, app, status, warning): inv_file = tmp_path / 'inventory' - inv_file.write_bytes(inventory_v2) + inv_file.write_bytes(INVENTORY_V2) set_config(app, { 'cmd': ('https://docs.python.org/', str(inv_file)), }) @@ -236,11 +227,31 @@ def test_missing_reference_stddomain(tmp_path, app, status, warning): rn = missing_reference(app, app.env, node, contnode) assert rn.astext() == '-l' + # term reference (normal) + node, contnode = fake_node('std', 'term', 'a term', 'a term') + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'a term' + + # term reference (case insensitive) + node, contnode = fake_node('std', 'term', 'A TERM', 'A TERM') + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'A TERM' + + # label reference (normal) + node, contnode = fake_node('std', 'ref', 'The-Julia-Domain', 'The-Julia-Domain') + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'The Julia Domain' + + # label reference (case insensitive) + node, contnode = fake_node('std', 'ref', 'the-julia-domain', 'the-julia-domain') + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'The Julia Domain' + @pytest.mark.sphinx('html', testroot='ext-intersphinx-cppdomain') def test_missing_reference_cppdomain(tmp_path, app, status, warning): inv_file = tmp_path / 'inventory' - inv_file.write_bytes(inventory_v2) + inv_file.write_bytes(INVENTORY_V2) set_config(app, { 'https://docs.python.org/': str(inv_file), }) @@ -266,7 +277,7 @@ def test_missing_reference_cppdomain(tmp_path, app, status, warning): def test_missing_reference_jsdomain(tmp_path, app, status, warning): inv_file = tmp_path / 'inventory' - inv_file.write_bytes(inventory_v2) + inv_file.write_bytes(INVENTORY_V2) set_config(app, { 'https://docs.python.org/': str(inv_file), }) @@ -290,7 +301,7 @@ def test_missing_reference_jsdomain(tmp_path, app, status, warning): def test_missing_reference_disabled_domain(tmp_path, app, status, warning): inv_file = tmp_path / 'inventory' - inv_file.write_bytes(inventory_v2) + inv_file.write_bytes(INVENTORY_V2) set_config(app, { 'inv': ('https://docs.python.org/', str(inv_file)), }) @@ -352,7 +363,7 @@ def test_missing_reference_disabled_domain(tmp_path, app, status, warning): def test_inventory_not_having_version(tmp_path, app, status, warning): inv_file = tmp_path / 'inventory' - inv_file.write_bytes(inventory_v2_not_having_version) + inv_file.write_bytes(INVENTORY_V2_NO_VERSION) set_config(app, { 'https://docs.python.org/': str(inv_file), }) @@ -374,14 +385,14 @@ def test_load_mappings_warnings(tmp_path, app, status, warning): identifiers are not string """ inv_file = tmp_path / 'inventory' - inv_file.write_bytes(inventory_v2) + inv_file.write_bytes(INVENTORY_V2) set_config(app, { 'https://docs.python.org/': str(inv_file), 'py3k': ('https://docs.python.org/py3k/', str(inv_file)), - 'repoze.workflow': ('http://docs.repoze.org/workflow/', str(inv_file)), - 'django-taggit': ('http://django-taggit.readthedocs.org/en/latest/', + 'repoze.workflow': ('https://docs.repoze.org/workflow/', str(inv_file)), + 'django-taggit': ('https://django-taggit.readthedocs.org/en/latest/', str(inv_file)), - 12345: ('http://www.sphinx-doc.org/en/stable/', str(inv_file)), + 12345: ('https://www.sphinx-doc.org/en/stable/', str(inv_file)), }) # load the inventory and check if it's done correctly @@ -395,7 +406,7 @@ def test_load_mappings_warnings(tmp_path, app, status, warning): def test_load_mappings_fallback(tmp_path, app, status, warning): inv_file = tmp_path / 'inventory' - inv_file.write_bytes(inventory_v2) + inv_file.write_bytes(INVENTORY_V2) set_config(app, {}) # connect to invalid path @@ -429,23 +440,25 @@ def test_load_mappings_fallback(tmp_path, app, status, warning): class TestStripBasicAuth: """Tests for sphinx.ext.intersphinx._strip_basic_auth()""" + def test_auth_stripped(self): - """basic auth creds stripped from URL containing creds""" + """Basic auth creds stripped from URL containing creds""" url = 'https://user:12345@domain.com/project/objects.inv' expected = 'https://domain.com/project/objects.inv' actual = _strip_basic_auth(url) assert expected == actual def test_no_auth(self): - """url unchanged if param doesn't contain basic auth creds""" + """Url unchanged if param doesn't contain basic auth creds""" url = 'https://domain.com/project/objects.inv' expected = 'https://domain.com/project/objects.inv' actual = _strip_basic_auth(url) assert expected == actual def test_having_port(self): - """basic auth creds correctly stripped from URL containing creds even if URL - contains port""" + """Basic auth creds correctly stripped from URL containing creds even if URL + contains port + """ url = 'https://user:12345@domain.com:8080/project/objects.inv' expected = 'https://domain.com:8080/project/objects.inv' actual = _strip_basic_auth(url) @@ -492,7 +505,7 @@ def test_inspect_main_noargs(capsys): def test_inspect_main_file(capsys, tmp_path): """inspect_main interface, with file argument""" inv_file = tmp_path / 'inventory' - inv_file.write_bytes(inventory_v2) + inv_file.write_bytes(INVENTORY_V2) inspect_main([str(inv_file)]) @@ -507,15 +520,14 @@ def test_inspect_main_url(capsys): def do_GET(self): self.send_response(200, "OK") self.end_headers() - self.wfile.write(inventory_v2) + self.wfile.write(INVENTORY_V2) def log_message(*args, **kwargs): # Silenced. pass - url = 'http://localhost:7777/' + INVENTORY_FILENAME - - with http_server(InventoryHandler): + with http_server(InventoryHandler) as server: + url = f'http://localhost:{server.server_port}/{INVENTORY_FILENAME}' inspect_main([url]) stdout, stderr = capsys.readouterr() @@ -526,9 +538,9 @@ def test_inspect_main_url(capsys): @pytest.mark.sphinx('html', testroot='ext-intersphinx-role') def test_intersphinx_role(app, warning): inv_file = app.srcdir / 'inventory' - inv_file.write_bytes(inventory_v2) + inv_file.write_bytes(INVENTORY_V2) app.config.intersphinx_mapping = { - 'inv': ('http://example.org/', str(inv_file)), + 'inv': ('https://example.org/', str(inv_file)), } app.config.intersphinx_cache_limit = 0 app.config.nitpicky = True @@ -539,22 +551,27 @@ def test_intersphinx_role(app, warning): app.build() content = (app.outdir / 'index.html').read_text(encoding='utf8') - wStr = warning.getvalue() - - html = '<a class="reference external" href="http://example.org/{}" title="(in foo v2.0)">' + warnings = strip_colors(warning.getvalue()).splitlines() + index_path = app.srcdir / 'index.rst' + assert warnings == [ + f"{index_path}:21: WARNING: role for external cross-reference not found in domain 'py': 'nope'", + f"{index_path}:28: WARNING: role for external cross-reference not found in domains 'cpp', 'std': 'nope'", + f"{index_path}:39: WARNING: inventory for external cross-reference not found: 'invNope'", + f"{index_path}:44: WARNING: role for external cross-reference not found in domain 'c': 'function' (perhaps you meant one of: 'func', 'identifier', 'type')", + f"{index_path}:45: WARNING: role for external cross-reference not found in domains 'cpp', 'std': 'function' (perhaps you meant one of: 'cpp:func', 'cpp:identifier', 'cpp:type')", + f'{index_path}:9: WARNING: external py:mod reference target not found: module3', + f'{index_path}:14: WARNING: external py:mod reference target not found: module10', + f'{index_path}:19: WARNING: external py:meth reference target not found: inv:Foo.bar', + ] + + html = '<a class="reference external" href="https://example.org/{}" title="(in foo v2.0)">' assert html.format('foo.html#module-module1') in content assert html.format('foo.html#module-module2') in content - assert "WARNING: external py:mod reference target not found: module3" in wStr - assert "WARNING: external py:mod reference target not found: module10" in wStr assert html.format('sub/foo.html#module1.func') in content - assert "WARNING: external py:meth reference target not found: inv:Foo.bar" in wStr - - assert "WARNING: role for external cross-reference not found: py:nope" in wStr # default domain assert html.format('index.html#std_uint8_t') in content - assert "WARNING: role for external cross-reference not found: nope" in wStr # std roles without domain prefix assert html.format('docname.html') in content @@ -562,7 +579,6 @@ def test_intersphinx_role(app, warning): # explicit inventory assert html.format('cfunc.html#CFunc') in content - assert "WARNING: inventory for external cross-reference not found: invNope" in wStr # explicit title assert html.format('index.html#foons') in content diff --git a/tests/test_ext_math.py b/tests/test_extensions/test_ext_math.py index d5331f8..b673f83 100644 --- a/tests/test_ext_math.py +++ b/tests/test_extensions/test_ext_math.py @@ -25,9 +25,9 @@ def has_binary(binary): @pytest.mark.skipif(not has_binary('dvipng'), reason='Requires dvipng" binary') @pytest.mark.sphinx('html', testroot='ext-math-simple', - confoverrides = {'extensions': ['sphinx.ext.imgmath']}) + confoverrides={'extensions': ['sphinx.ext.imgmath']}) def test_imgmath_png(app, status, warning): - app.builder.build_all() + app.build(force_all=True) if "LaTeX command 'latex' cannot be run" in warning.getvalue(): msg = 'LaTeX command "latex" is not available' raise pytest.skip.Exception(msg) @@ -39,7 +39,7 @@ def test_imgmath_png(app, status, warning): shutil.rmtree(app.outdir) html = (r'<div class="math">\s*<p>\s*<img src="_images/math/\w+.png"' r'\s*alt="a\^2\+b\^2=c\^2"/>\s*</p>\s*</div>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) @pytest.mark.skipif(not has_binary('dvisvgm'), @@ -48,7 +48,7 @@ def test_imgmath_png(app, status, warning): confoverrides={'extensions': ['sphinx.ext.imgmath'], 'imgmath_image_format': 'svg'}) def test_imgmath_svg(app, status, warning): - app.builder.build_all() + app.build(force_all=True) if "LaTeX command 'latex' cannot be run" in warning.getvalue(): msg = 'LaTeX command "latex" is not available' raise pytest.skip.Exception(msg) @@ -60,7 +60,7 @@ def test_imgmath_svg(app, status, warning): shutil.rmtree(app.outdir) html = (r'<div class="math">\s*<p>\s*<img src="_images/math/\w+.svg"' r'\s*alt="a\^2\+b\^2=c\^2"/>\s*</p>\s*</div>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) @pytest.mark.skipif(not has_binary('dvisvgm'), @@ -70,7 +70,7 @@ def test_imgmath_svg(app, status, warning): 'imgmath_image_format': 'svg', 'imgmath_embed': True}) def test_imgmath_svg_embed(app, status, warning): - app.builder.build_all() + app.build(force_all=True) if "LaTeX command 'latex' cannot be run" in warning.getvalue(): msg = 'LaTeX command "latex" is not available' raise pytest.skip.Exception(msg) @@ -88,7 +88,7 @@ def test_imgmath_svg_embed(app, status, warning): confoverrides={'extensions': ['sphinx.ext.mathjax'], 'mathjax_options': {'integrity': 'sha384-0123456789'}}) def test_mathjax_options(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') shutil.rmtree(app.outdir) @@ -100,14 +100,14 @@ def test_mathjax_options(app, status, warning): @pytest.mark.sphinx('html', testroot='ext-math', confoverrides={'extensions': ['sphinx.ext.mathjax']}) def test_mathjax_align(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') shutil.rmtree(app.outdir) html = (r'<div class="math notranslate nohighlight">\s*' r'\\\[ \\begin\{align\}\\begin\{aligned\}S \&= \\pi r\^2\\\\' r'V \&= \\frac\{4\}\{3\} \\pi r\^3\\end\{aligned\}\\end\{align\} \\\]</div>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) @pytest.mark.sphinx('html', testroot='ext-math', @@ -119,7 +119,7 @@ def test_math_number_all_mathjax(app, status, warning): content = (app.outdir / 'index.html').read_text(encoding='utf8') html = (r'<div class="math notranslate nohighlight" id="equation-index-0">\s*' r'<span class="eqno">\(1\)<a .*>\xb6</a></span>\\\[a\^2\+b\^2=c\^2\\\]</div>') - assert re.search(html, content, re.S) + assert re.search(html, content, re.DOTALL) @pytest.mark.sphinx('latex', testroot='ext-math', @@ -131,31 +131,31 @@ def test_math_number_all_latex(app, status, warning): macro = (r'\\begin{equation\*}\s*' r'\\begin{split}a\^2\+b\^2=c\^2\\end{split}\s*' r'\\end{equation\*}') - assert re.search(macro, content, re.S) + assert re.search(macro, content, re.DOTALL) macro = r'Inline \\\(E=mc\^2\\\)' - assert re.search(macro, content, re.S) + assert re.search(macro, content, re.DOTALL) macro = (r'\\begin{equation\*}\s*' r'\\begin{split}e\^{i\\pi}\+1=0\\end{split}\s+' r'\\end{equation\*}') - assert re.search(macro, content, re.S) + assert re.search(macro, content, re.DOTALL) macro = (r'\\begin{align\*}\\!\\begin{aligned}\s*' r'S &= \\pi r\^2\\\\\s*' r'V &= \\frac\{4}\{3} \\pi r\^3\\\\\s*' r'\\end{aligned}\\end{align\*}') - assert re.search(macro, content, re.S) + assert re.search(macro, content, re.DOTALL) macro = r'Referencing equation \\eqref{equation:math:foo}.' - assert re.search(macro, content, re.S) + assert re.search(macro, content, re.DOTALL) @pytest.mark.sphinx('html', testroot='ext-math', confoverrides={'extensions': ['sphinx.ext.mathjax'], 'math_eqref_format': 'Eq.{number}'}) def test_math_eqref_format_html(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'math.html').read_text(encoding='utf8') html = ('<p>Referencing equation <a class="reference internal" ' @@ -168,12 +168,12 @@ def test_math_eqref_format_html(app, status, warning): confoverrides={'extensions': ['sphinx.ext.mathjax'], 'math_eqref_format': 'Eq.{number}'}) def test_math_eqref_format_latex(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'python.tex').read_text(encoding='utf8') macro = (r'Referencing equation Eq.\\ref{equation:math:foo} and ' r'Eq.\\ref{equation:math:foo}.') - assert re.search(macro, content, re.S) + assert re.search(macro, content, re.DOTALL) @pytest.mark.sphinx('html', testroot='ext-math', @@ -181,7 +181,7 @@ def test_math_eqref_format_latex(app, status, warning): 'numfig': True, 'math_numfig': True}) def test_mathjax_numfig_html(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'math.html').read_text(encoding='utf8') html = ('<div class="math notranslate nohighlight" id="equation-math-0">\n' @@ -199,7 +199,7 @@ def test_mathjax_numfig_html(app, status, warning): 'numfig_secnum_depth': 0, 'math_numfig': True}) def test_imgmath_numfig_html(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'page.html').read_text(encoding='utf8') html = '<span class="eqno">(3)<a class="headerlink" href="#equation-bar"' @@ -213,7 +213,7 @@ def test_imgmath_numfig_html(app, status, warning): @pytest.mark.sphinx('dummy', testroot='ext-math-compat') def test_math_compat(app, status, warning): with warnings.catch_warnings(record=True): - app.builder.build_all() + app.build(force_all=True) doctree = app.env.get_and_resolve_doctree('index', app.builder) assert_node(doctree, @@ -239,7 +239,7 @@ def test_math_compat(app, status, warning): confoverrides={'extensions': ['sphinx.ext.mathjax'], 'mathjax3_config': {'extensions': ['tex2jax.js']}}) def test_mathjax3_config(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') assert MATHJAX_URL in content @@ -251,7 +251,7 @@ def test_mathjax3_config(app, status, warning): confoverrides={'extensions': ['sphinx.ext.mathjax'], 'mathjax2_config': {'extensions': ['tex2jax.js']}}) def test_mathjax2_config(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') assert ('<script async="async" src="%s">' % MATHJAX_URL in content) @@ -265,7 +265,7 @@ def test_mathjax2_config(app, status, warning): 'mathjax_options': {'async': 'async'}, 'mathjax3_config': {'extensions': ['tex2jax.js']}}) def test_mathjax_options_async_for_mathjax3(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') assert MATHJAX_URL in content @@ -277,7 +277,7 @@ def test_mathjax_options_async_for_mathjax3(app, status, warning): 'mathjax_options': {'defer': 'defer'}, 'mathjax2_config': {'extensions': ['tex2jax.js']}}) def test_mathjax_options_defer_for_mathjax2(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') assert ('<script defer="defer" src="%s">' % MATHJAX_URL in content) @@ -291,7 +291,7 @@ def test_mathjax_options_defer_for_mathjax2(app, status, warning): }, ) def test_mathjax_path(app): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') assert '<script async="async" src="_static/MathJax.js"></script>' in content @@ -305,7 +305,7 @@ def test_mathjax_path(app): }, ) def test_mathjax_path_config(app): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') assert '<script async="async" src="_static/MathJax.js?config=scipy-mathjax"></script>' in content @@ -314,7 +314,7 @@ def test_mathjax_path_config(app): @pytest.mark.sphinx('html', testroot='ext-math', confoverrides={'extensions': ['sphinx.ext.mathjax']}) def test_mathjax_is_installed_only_if_document_having_math(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') assert MATHJAX_URL in content @@ -326,7 +326,7 @@ def test_mathjax_is_installed_only_if_document_having_math(app, status, warning) @pytest.mark.sphinx('html', testroot='basic', confoverrides={'extensions': ['sphinx.ext.mathjax']}) def test_mathjax_is_not_installed_if_no_equations(app, status, warning): - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') assert 'MathJax.js' not in content @@ -336,10 +336,54 @@ def test_mathjax_is_not_installed_if_no_equations(app, status, warning): confoverrides={'extensions': ['sphinx.ext.mathjax']}) def test_mathjax_is_installed_if_no_equations_when_forced(app, status, warning): app.set_html_assets_policy('always') - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'index.html').read_text(encoding='utf8') assert MATHJAX_URL in content content = (app.outdir / 'nomath.html').read_text(encoding='utf8') assert MATHJAX_URL in content + + +@pytest.mark.sphinx('html', testroot='ext-math-include', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_is_installed_if_included_file_has_equations(app): + app.build(force_all=True) + + # no real equations at the rst level, but includes "included" + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + + # no real equations at the rst level, but includes "math.rst" + content = (app.outdir / 'included.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + + content = (app.outdir / 'math.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + + +@pytest.mark.sphinx('singlehtml', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_is_installed_only_if_document_having_math_singlehtml(app): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert MATHJAX_URL in content + + +@pytest.mark.sphinx('singlehtml', testroot='basic', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_is_not_installed_if_no_equations_singlehtml(app): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert 'MathJax.js' not in content + + +@pytest.mark.sphinx('singlehtml', testroot='ext-math-include', + confoverrides={'extensions': ['sphinx.ext.mathjax']}) +def test_mathjax_is_installed_if_included_file_has_equations_singlehtml(app): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert MATHJAX_URL in content diff --git a/tests/test_ext_napoleon.py b/tests/test_extensions/test_ext_napoleon.py index 00b7ac1..466bd49 100644 --- a/tests/test_ext_napoleon.py +++ b/tests/test_extensions/test_ext_napoleon.py @@ -55,7 +55,7 @@ class SampleClass: @simple_decorator def __decorated_func__(self): - """doc""" + """Doc""" pass diff --git a/tests/test_ext_napoleon_docstring.py b/tests/test_extensions/test_ext_napoleon_docstring.py index 87fad61..d7ef489 100644 --- a/tests/test_ext_napoleon_docstring.py +++ b/tests/test_extensions/test_ext_napoleon_docstring.py @@ -1,13 +1,16 @@ """Tests for :mod:`sphinx.ext.napoleon.docstring` module.""" import re +import zlib from collections import namedtuple from inspect import cleandoc +from itertools import product from textwrap import dedent from unittest import mock import pytest +from sphinx.ext.intersphinx import load_mappings, normalize_intersphinx_mapping from sphinx.ext.napoleon import Config from sphinx.ext.napoleon.docstring import ( GoogleDocstring, @@ -17,9 +20,10 @@ from sphinx.ext.napoleon.docstring import ( _token_type, _tokenize_type_spec, ) +from sphinx.testing.util import etree_parse -from .ext_napoleon_pep526_data_google import PEP526GoogleClass -from .ext_napoleon_pep526_data_numpy import PEP526NumpyClass +from tests.test_extensions.ext_napoleon_pep526_data_google import PEP526GoogleClass +from tests.test_extensions.ext_napoleon_pep526_data_numpy import PEP526NumpyClass class NamedtupleSubclass(namedtuple('NamedtupleSubclass', ('attr1', 'attr2'))): @@ -36,6 +40,7 @@ class NamedtupleSubclass(namedtuple('NamedtupleSubclass', ('attr1', 'attr2'))): Adds a newline after the type """ + # To avoid creating a dict, as a namedtuple doesn't have it: __slots__ = () @@ -1156,7 +1161,7 @@ Methods: description -""" # noqa: W293 +""" # NoQA: W293 config = Config() actual = str(GoogleDocstring(docstring, config=config, app=None, what='module', options={'no-index': True})) @@ -1186,7 +1191,7 @@ Do as you please actual = str(GoogleDocstring(cleandoc(PEP526GoogleClass.__doc__), config, app=None, what="class", obj=PEP526GoogleClass)) expected = """\ -Sample class with PEP 526 annotations and google docstring +Sample class with PEP 526 annotations and google docstring. .. attribute:: attr1 @@ -2658,3 +2663,41 @@ def test_napoleon_and_autodoc_typehints_description_documented_params(app, statu '\n' ' * ****kwargs** (*int*) -- Extra arguments.\n' ) + + +@pytest.mark.sphinx('html', testroot='ext-napoleon-paramtype', freshenv=True) +def test_napoleon_keyword_and_paramtype(app, tmp_path): + inv_file = tmp_path / 'objects.inv' + inv_file.write_bytes(b'''\ +# Sphinx inventory version 2 +# Project: Intersphinx Test +# Version: 42 +# The remainder of this file is compressed using zlib. +''' + zlib.compress(b'''\ +None py:data 1 none.html - +list py:class 1 list.html - +int py:class 1 int.html - +''')) # NoQA: W291 + app.config.intersphinx_mapping = {'python': ('127.0.0.1:5555', str(inv_file))} + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + app.build(force_all=True) + + etree = etree_parse(app.outdir / 'index.html') + + for name, typename in product(('keyword', 'kwarg', 'kwparam'), ('paramtype', 'kwtype')): + param = f'{name}_{typename}' + li_ = list(etree.findall(f'.//li/p/strong[.="{param}"]/../..')) + assert len(li_) == 1 + li = li_[0] + + text = li.text or ''.join(li.itertext()) + assert text == f'{param} (list[int]) \u2013 some param' + + a_ = list(li.findall('.//a[@class="reference external"]')) + + assert len(a_) == 2 + for a, uri in zip(a_, ('list.html', 'int.html')): + assert a.attrib['href'] == f'127.0.0.1:5555/{uri}' + assert a.attrib['title'] == '(in Intersphinx Test v42)' diff --git a/tests/test_ext_todo.py b/tests/test_extensions/test_ext_todo.py index 7d39495..1903f9f 100644 --- a/tests/test_ext_todo.py +++ b/tests/test_extensions/test_ext_todo.py @@ -14,7 +14,7 @@ def test_todo(app, status, warning): todos.append(node) app.connect('todo-defined', on_todo_defined) - app.builder.build_all() + app.build(force_all=True) # check todolist content = (app.outdir / 'index.html').read_text(encoding='utf8') @@ -52,7 +52,7 @@ def test_todo_not_included(app, status, warning): todos.append(node) app.connect('todo-defined', on_todo_defined) - app.builder.build_all() + app.build(force_all=True) # check todolist content = (app.outdir / 'index.html').read_text(encoding='utf8') @@ -86,9 +86,8 @@ def test_todo_valid_link(app, status, warning): that exists in the LaTeX output. The target was previously incorrectly omitted (GitHub issue #1020). """ - # Ensure the LaTeX output is built. - app.builder.build_all() + app.build(force_all=True) content = (app.outdir / 'python.tex').read_text(encoding='utf8') diff --git a/tests/test_ext_viewcode.py b/tests/test_extensions/test_ext_viewcode.py index a1a0a6d..b2c6fc0 100644 --- a/tests/test_ext_viewcode.py +++ b/tests/test_extensions/test_ext_viewcode.py @@ -44,7 +44,7 @@ def check_viewcode_output(app, warning): confoverrides={"viewcode_line_numbers": True}) def test_viewcode_linenos(app, warning): shutil.rmtree(app.outdir / '_modules', ignore_errors=True) - app.builder.build_all() + app.build(force_all=True) result = check_viewcode_output(app, warning) assert '<span class="linenos"> 1</span>' in result @@ -54,7 +54,7 @@ def test_viewcode_linenos(app, warning): confoverrides={"viewcode_line_numbers": False}) def test_viewcode(app, warning): shutil.rmtree(app.outdir / '_modules', ignore_errors=True) - app.builder.build_all() + app.build(force_all=True) result = check_viewcode_output(app, warning) assert 'class="linenos">' not in result @@ -63,7 +63,7 @@ def test_viewcode(app, warning): @pytest.mark.sphinx('epub', testroot='ext-viewcode') def test_viewcode_epub_default(app, status, warning): shutil.rmtree(app.outdir) - app.builder.build_all() + app.build(force_all=True) assert not (app.outdir / '_modules/spam/mod1.xhtml').exists() @@ -74,7 +74,7 @@ def test_viewcode_epub_default(app, status, warning): @pytest.mark.sphinx('epub', testroot='ext-viewcode', confoverrides={'viewcode_enable_epub': True}) def test_viewcode_epub_enabled(app, status, warning): - app.builder.build_all() + app.build(force_all=True) assert (app.outdir / '_modules/spam/mod1.xhtml').exists() @@ -84,14 +84,14 @@ def test_viewcode_epub_enabled(app, status, warning): @pytest.mark.sphinx(testroot='ext-viewcode', tags=['test_linkcode']) def test_linkcode(app, status, warning): - app.builder.build(['objects']) + app.build(filenames=[app.srcdir / 'objects.rst']) stuff = (app.outdir / 'objects.html').read_text(encoding='utf8') - assert 'http://foobar/source/foolib.py' in stuff - assert 'http://foobar/js/' in stuff - assert 'http://foobar/c/' in stuff - assert 'http://foobar/cpp/' in stuff + assert 'https://foobar/source/foolib.py' in stuff + assert 'https://foobar/js/' in stuff + assert 'https://foobar/c/' in stuff + assert 'https://foobar/cpp/' in stuff @pytest.mark.sphinx(testroot='ext-viewcode-find', freshenv=True) @@ -117,7 +117,7 @@ def test_local_source_files(app, status, warning): return (source, tags) app.connect('viewcode-find-source', find_source) - app.builder.build_all() + app.build(force_all=True) warnings = re.sub(r'\\+', '/', warning.getvalue()) assert re.findall( diff --git a/tests/test_extension.py b/tests/test_extensions/test_extension.py index d74743c..d74743c 100644 --- a/tests/test_extension.py +++ b/tests/test_extensions/test_extension.py diff --git a/tests/test_intl/__init__.py b/tests/test_intl/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_intl/__init__.py diff --git a/tests/test_catalogs.py b/tests/test_intl/test_catalogs.py index b7fd7be..b7fd7be 100644 --- a/tests/test_catalogs.py +++ b/tests/test_intl/test_catalogs.py diff --git a/tests/test_intl.py b/tests/test_intl/test_intl.py index a07ebfb..6b1e9ba 100644 --- a/tests/test_intl.py +++ b/tests/test_intl/test_intl.py @@ -8,7 +8,6 @@ import os.path import re import shutil import time -from pathlib import Path import pytest from babel.messages import mofile, pofile @@ -16,13 +15,16 @@ from babel.messages.catalog import Catalog from docutils import nodes from sphinx import locale -from sphinx.testing.util import assert_node, etree_parse, strip_escseq +from sphinx.testing.util import assert_node, etree_parse +from sphinx.util.console import strip_colors from sphinx.util.nodes import NodeMatcher +_CATALOG_LOCALE = 'xx' + sphinx_intl = pytest.mark.sphinx( testroot='intl', confoverrides={ - 'language': 'xx', 'locale_dirs': ['.'], + 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'], 'gettext_compact': False, }, ) @@ -38,22 +40,20 @@ def write_mo(pathname, po): return mofile.write_mo(f, po) -@pytest.fixture(autouse=True) -def _setup_intl(app_params): - assert isinstance(app_params.kwargs['srcdir'], Path) - srcdir = app_params.kwargs['srcdir'] - for dirpath, _dirs, files in os.walk(srcdir): - dirpath = Path(dirpath) - for f in [f for f in files if f.endswith('.po')]: - po = str(dirpath / f) - mo = srcdir / 'xx' / 'LC_MESSAGES' / ( - os.path.relpath(po[:-3], srcdir) + '.mo') - if not mo.parent.exists(): - mo.parent.mkdir(parents=True, exist_ok=True) - - if not mo.exists() or os.stat(mo).st_mtime < os.stat(po).st_mtime: - # compile .mo file only if needed - write_mo(mo, read_po(po)) +def _set_mtime_ns(target, value): + os.utime(target, ns=(value, value)) + return os.stat(target).st_mtime_ns + + +def _get_bom_intl_path(app): + basedir = app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' + return basedir / 'bom.po', basedir / 'bom.mo' + + +def _get_update_targets(app): + app.env.find_files(app.config, app.builder) + added, changed, removed = app.env.get_outdated_files(config_changed=False) + return added, changed, removed @pytest.fixture(autouse=True) @@ -316,7 +316,7 @@ def test_text_glossary_term_inconsistencies(app, warning): def test_gettext_section(app): app.build() # --- section - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'section.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'section.po') actual = read_po(app.outdir / 'section.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -329,7 +329,7 @@ def test_text_section(app): app.build() # --- section result = (app.outdir / 'section.txt').read_text(encoding='utf8') - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'section.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'section.po') for expect_msg in [m for m in expect if m.id]: assert expect_msg.string in result @@ -468,12 +468,12 @@ def test_text_admonitions(app): def test_gettext_toctree(app): app.build() # --- toctree (index.rst) - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'index.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'index.po') actual = read_po(app.outdir / 'index.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] # --- toctree (toctree.rst) - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'toctree.po') actual = read_po(app.outdir / 'toctree.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -485,7 +485,7 @@ def test_gettext_toctree(app): def test_gettext_table(app): app.build() # --- toctree - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'table.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'table.po') actual = read_po(app.outdir / 'table.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -498,7 +498,7 @@ def test_text_table(app): app.build() # --- toctree result = (app.outdir / 'table.txt').read_text(encoding='utf8') - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'table.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'table.po') for expect_msg in [m for m in expect if m.id]: assert expect_msg.string in result @@ -515,7 +515,7 @@ def test_text_toctree(app): assert 'TABLE OF CONTENTS' in result # --- toctree (toctree.rst) result = (app.outdir / 'toctree.txt').read_text(encoding='utf8') - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'toctree.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'toctree.po') for expect_msg in (m for m in expect if m.id): assert expect_msg.string in result @@ -526,7 +526,7 @@ def test_text_toctree(app): def test_gettext_topic(app): app.build() # --- topic - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'topic.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'topic.po') actual = read_po(app.outdir / 'topic.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -539,7 +539,7 @@ def test_text_topic(app): app.build() # --- topic result = (app.outdir / 'topic.txt').read_text(encoding='utf8') - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'topic.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'topic.po') for expect_msg in [m for m in expect if m.id]: assert expect_msg.string in result @@ -550,7 +550,7 @@ def test_text_topic(app): def test_gettext_definition_terms(app): app.build() # --- definition terms: regression test for #2198, #2205 - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'definition_terms.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'definition_terms.po') actual = read_po(app.outdir / 'definition_terms.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -562,7 +562,7 @@ def test_gettext_definition_terms(app): def test_gettext_glossary_terms(app, warning): app.build() # --- glossary terms: regression test for #1090 - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'glossary_terms.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'glossary_terms.po') actual = read_po(app.outdir / 'glossary_terms.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -576,7 +576,7 @@ def test_gettext_glossary_terms(app, warning): def test_gettext_glossary_term_inconsistencies(app): app.build() # --- glossary term inconsistencies: regression test for #1090 - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'glossary_terms_inconsistency.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'glossary_terms_inconsistency.po') actual = read_po(app.outdir / 'glossary_terms_inconsistency.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -588,7 +588,7 @@ def test_gettext_glossary_term_inconsistencies(app): def test_gettext_literalblock(app): app.build() # --- gettext builder always ignores ``only`` directive - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'literalblock.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'literalblock.po') actual = read_po(app.outdir / 'literalblock.pot') for expect_msg in [m for m in expect if m.id]: if len(expect_msg.id.splitlines()) == 1: @@ -604,7 +604,7 @@ def test_gettext_literalblock(app): def test_gettext_buildr_ignores_only_directive(app): app.build() # --- gettext builder always ignores ``only`` directive - expect = read_po(app.srcdir / 'xx' / 'LC_MESSAGES' / 'only.po') + expect = read_po(app.srcdir / _CATALOG_LOCALE / 'LC_MESSAGES' / 'only.po') actual = read_po(app.outdir / 'only.pot') for expect_msg in [m for m in expect if m.id]: assert expect_msg.id in [m.id for m in actual if m.id] @@ -612,7 +612,7 @@ def test_gettext_buildr_ignores_only_directive(app): @sphinx_intl def test_node_translated_attribute(app): - app.builder.build_specific([app.srcdir / 'translation_progress.txt']) + app.build(filenames=[app.srcdir / 'translation_progress.txt']) doctree = app.env.get_doctree('translation_progress') @@ -625,7 +625,7 @@ def test_node_translated_attribute(app): @sphinx_intl def test_translation_progress_substitution(app): - app.builder.build_specific([app.srcdir / 'translation_progress.txt']) + app.build(filenames=[app.srcdir / 'translation_progress.txt']) doctree = app.env.get_doctree('translation_progress') @@ -633,12 +633,12 @@ def test_translation_progress_substitution(app): @pytest.mark.sphinx(testroot='intl', freshenv=True, confoverrides={ - 'language': 'xx', 'locale_dirs': ['.'], + 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'], 'gettext_compact': False, 'translation_progress_classes': True, }) def test_translation_progress_classes_true(app): - app.builder.build_specific([app.srcdir / 'translation_progress.txt']) + app.build(filenames=[app.srcdir / 'translation_progress.txt']) doctree = app.env.get_doctree('translation_progress') @@ -681,50 +681,188 @@ def test_translation_progress_classes_true(app): assert len(doctree[0]) == 20 +class _MockClock: + """Object for mocking :func:`time.time_ns` (if needed). + + Use :meth:`sleep` to make this specific clock sleep for some time. + """ + + def time(self) -> int: + """Nanosecond since 'fake' epoch.""" + raise NotImplementedError + + def sleep(self, ds: float) -> None: + """Sleep *ds* seconds.""" + raise NotImplementedError + + +class _MockWindowsClock(_MockClock): + """Object for mocking :func:`time.time_ns` on Windows platforms. + + The result is in 'nanoseconds' but with a microsecond resolution + so that the division by 1_000 does not cause rounding issues. + """ + + def __init__(self) -> None: + self.us: int = 0 # current microsecond 'tick' + + def time(self) -> int: + ret = 1_000 * self.us + self.us += 1 + return ret + + def sleep(self, ds: float) -> None: + self.us += int(ds * 1e6) + + +class _MockUnixClock(_MockClock): + """Object for mocking :func:`time.time_ns` on Unix platforms. + + Since nothing is needed for Unix platforms, this object acts as + a proxy so that the API is the same as :class:`_MockWindowsClock`. + """ + + def time(self) -> int: + return time.time_ns() + + def sleep(self, ds: float) -> None: + time.sleep(ds) + + +@pytest.fixture() +def mock_time_and_i18n( + monkeypatch: pytest.MonkeyPatch, +) -> tuple[pytest.MonkeyPatch, _MockClock]: + from sphinx.util.i18n import CatalogInfo + + # save the 'original' definition + catalog_write_mo = CatalogInfo.write_mo + + def mock_write_mo(self, locale, use_fuzzy=False): + catalog_write_mo(self, locale, use_fuzzy) + # ensure that the .mo file being written has a correct fake timestamp + _set_mtime_ns(self.mo_path, time.time_ns()) + + # see: https://github.com/pytest-dev/pytest/issues/363 + with pytest.MonkeyPatch.context() as mock: + if os.name == 'posix': + clock = _MockUnixClock() + else: + # When using pytest.mark.parametrize() to emulate test repetition, + # the teardown phase on Windows fails due to an error apparently in + # the colorama.ansitowin32 module, so we forcibly disable colors. + mock.setenv('NO_COLOR', '1') + # apply the patch only for Windows + clock = _MockWindowsClock() + mock.setattr('time.time_ns', clock.time) + # Use clock.sleep() to emulate time.sleep() but do not try + # to mock the latter since this might break other libraries. + mock.setattr('sphinx.util.i18n.CatalogInfo.write_mo', mock_write_mo) + yield mock, clock + + @sphinx_intl -# use individual shared_result directory to avoid "incompatible doctree" error -@pytest.mark.sphinx(testroot='builder-gettext-dont-rebuild-mo') -def test_gettext_dont_rebuild_mo(make_app, app_params): - # --- don't rebuild by .mo mtime - def get_update_targets(app_): - app_.env.find_files(app_.config, app_.builder) - added, changed, removed = app_.env.get_outdated_files(config_changed=False) - return added, changed, removed +# use the same testroot as 'gettext' since the latter contains less PO files +@pytest.mark.sphinx('dummy', testroot='builder-gettext-dont-rebuild-mo', freshenv=True) +def test_dummy_should_rebuild_mo(mock_time_and_i18n, make_app, app_params): + mock, clock = mock_time_and_i18n + assert os.name == 'posix' or clock.time() == 0 args, kwargs = app_params + app = make_app(*args, **kwargs) + po_path, mo_path = _get_bom_intl_path(app) + + # creation time of the those files (order does not matter) + bom_rst = app.srcdir / 'bom.rst' + bom_rst_time = time.time_ns() + + index_rst = app.srcdir / 'index.rst' + index_rst_time = time.time_ns() + po_time = time.time_ns() + + # patch the 'creation time' of the source files + assert _set_mtime_ns(po_path, po_time) == po_time + assert _set_mtime_ns(bom_rst, bom_rst_time) == bom_rst_time + assert _set_mtime_ns(index_rst, index_rst_time) == index_rst_time + + assert not mo_path.exists() + # when writing mo files, the counter is updated by calling + # patch_write_mo which is called to create .mo files (and + # thus the timestamp of the files are not those given by + # the OS but our fake ones) + app.build() + assert mo_path.exists() + # Do a real sleep on POSIX, or simulate a sleep on Windows + # to ensure that calls to time.time_ns() remain consistent. + clock.sleep(0.1 if os.name == 'posix' else 1) + + # check that the source files were not modified + assert bom_rst.stat().st_mtime_ns == bom_rst_time + assert index_rst.stat().st_mtime_ns == index_rst_time + # check that the 'bom' document is discovered after the .mo + # file has been written on the disk (i.e., read_doc() is called + # after the creation of the .mo files) + assert app.env.all_docs['bom'] > mo_path.stat().st_mtime_ns // 1000 - # phase1: build document with non-gettext builder and generate mo file in srcdir - app0 = make_app('dummy', *args, **kwargs) - app0.build() - time.sleep(0.01) - assert (app0.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo').exists() # Since it is after the build, the number of documents to be updated is 0 - update_targets = get_update_targets(app0) - assert update_targets[1] == set(), update_targets + update_targets = _get_update_targets(app) + assert update_targets[1] == set() # When rewriting the timestamp of mo file, the number of documents to be # updated will be changed. - mtime = (app0.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo').stat().st_mtime - os.utime(app0.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo', (mtime + 5, mtime + 5)) - update_targets = get_update_targets(app0) - assert update_targets[1] == {'bom'}, update_targets + new_mo_time = time.time_ns() + assert _set_mtime_ns(mo_path, new_mo_time) == new_mo_time + update_targets = _get_update_targets(app) + assert update_targets[1] == {'bom'} + mock.undo() # explicit call since it's not a context - # Because doctree for gettext builder can not be shared with other builders, - # erase doctreedir before gettext build. - shutil.rmtree(app0.doctreedir) + # remove all sources for the next test + shutil.rmtree(app.srcdir, ignore_errors=True) + time.sleep(0.1 if os.name == 'posix' else 0.5) # real sleep - # phase2: build document with gettext builder. + +@sphinx_intl +@pytest.mark.sphinx('gettext', testroot='builder-gettext-dont-rebuild-mo', freshenv=True) +def test_gettext_dont_rebuild_mo(mock_time_and_i18n, app): + mock, clock = mock_time_and_i18n + assert os.name == 'posix' or clock.time() == 0 + + assert app.srcdir.exists() + + # patch the 'creation time' of the source files + bom_rst = app.srcdir / 'bom.rst' + bom_rst_time = time.time_ns() + assert _set_mtime_ns(bom_rst, bom_rst_time) == bom_rst_time + + index_rst = app.srcdir / 'index.rst' + index_rst_time = time.time_ns() + assert _set_mtime_ns(index_rst, index_rst_time) == index_rst_time + + # phase 1: create fake MO file in the src directory + po_path, mo_path = _get_bom_intl_path(app) + write_mo(mo_path, read_po(po_path)) + po_time = time.time_ns() + assert _set_mtime_ns(po_path, po_time) == po_time + + # phase 2: build document with gettext builder. # The mo file in the srcdir directory is retained. - app = make_app('gettext', *args, **kwargs) app.build() - time.sleep(0.01) + # Do a real sleep on POSIX, or simulate a sleep on Windows + # to ensure that calls to time.time_ns() remain consistent. + clock.sleep(0.5 if os.name == 'posix' else 1) # Since it is after the build, the number of documents to be updated is 0 - update_targets = get_update_targets(app) - assert update_targets[1] == set(), update_targets + update_targets = _get_update_targets(app) + assert update_targets[1] == set() # Even if the timestamp of the mo file is updated, the number of documents # to be updated is 0. gettext builder does not rebuild because of mo update. - os.utime(app0.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo', (mtime + 10, mtime + 10)) - update_targets = get_update_targets(app) - assert update_targets[1] == set(), update_targets + new_mo_time = time.time_ns() + assert _set_mtime_ns(mo_path, new_mo_time) == new_mo_time + update_targets = _get_update_targets(app) + assert update_targets[1] == set() + mock.undo() # remove the patch + + # remove all sources for the next test + shutil.rmtree(app.srcdir, ignore_errors=True) + time.sleep(0.1 if os.name == 'posix' else 0.5) # real sleep @sphinx_intl @@ -761,7 +899,7 @@ def test_html_undefined_refs(app): result = (app.outdir / 'refs_inconsistency.html').read_text(encoding='utf8') expected_expr = ('<a class="reference external" ' - 'href="http://www.example.com">reference</a>') + 'href="https://www.example.com">reference</a>') assert len(re.findall(expected_expr, result)) == 2 expected_expr = ('<a class="reference internal" ' @@ -829,7 +967,7 @@ def test_html_versionchanges(app): assert expect1 == matched_content expect2 = ( - """<p><span class="versionmodified added">New in version 1.0: </span>""" + """<p><span class="versionmodified added">Added in version 1.0: </span>""" """THIS IS THE <em>FIRST</em> PARAGRAPH OF VERSIONADDED.</p>\n""") matched_content = get_content(result, "versionadded") assert expect2 == matched_content @@ -840,6 +978,12 @@ def test_html_versionchanges(app): matched_content = get_content(result, "versionchanged") assert expect3 == matched_content + expect4 = ( + """<p><span class="versionmodified removed">Removed in version 1.0: </span>""" + """THIS IS THE <em>FIRST</em> PARAGRAPH OF VERSIONREMOVED.</p>\n""") + matched_content = get_content(result, "versionremoved") + assert expect4 == matched_content + @sphinx_intl @pytest.mark.sphinx('html') @@ -868,16 +1012,17 @@ def test_html_template(app): def test_html_rebuild_mo(app): app.build() # --- rebuild by .mo mtime - app.builder.build_update() - app.env.find_files(app.config, app.builder) - _, updated, _ = app.env.get_outdated_files(config_changed=False) - assert len(updated) == 0 + app.build() + _, updated, _ = _get_update_targets(app) + assert updated == set() - mtime = (app.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo').stat().st_mtime - os.utime(app.srcdir / 'xx' / 'LC_MESSAGES' / 'bom.mo', (mtime + 5, mtime + 5)) - app.env.find_files(app.config, app.builder) - _, updated, _ = app.env.get_outdated_files(config_changed=False) - assert len(updated) == 1 + _, bom_file = _get_bom_intl_path(app) + old_mtime = bom_file.stat().st_mtime + new_mtime = old_mtime + (dt := 5) + os.utime(bom_file, (new_mtime, new_mtime)) + assert old_mtime + dt == new_mtime, (old_mtime + dt, new_mtime) + _, updated, _ = _get_update_targets(app) + assert updated == {'bom'} @sphinx_intl @@ -985,7 +1130,7 @@ def test_xml_keep_external_links(app): assert_elem( para0[0], ['EXTERNAL LINK TO', 'Python', '.'], - ['http://python.org/index.html']) + ['https://python.org/index.html']) # internal link check assert_elem( @@ -997,13 +1142,13 @@ def test_xml_keep_external_links(app): assert_elem( para0[2], ['INLINE LINK BY', 'THE SPHINX SITE', '.'], - ['http://sphinx-doc.org']) + ['https://sphinx-doc.org']) # unnamed link check assert_elem( para0[3], ['UNNAMED', 'LINK', '.'], - ['http://google.com']) + ['https://google.com']) # link target swapped translation para1 = secs[1].findall('paragraph') @@ -1015,7 +1160,7 @@ def test_xml_keep_external_links(app): assert_elem( para1[1], ['LINK TO', 'THE PYTHON SITE', 'AND', 'THE SPHINX SITE', '.'], - ['http://python.org', 'http://sphinx-doc.org']) + ['https://python.org', 'https://sphinx-doc.org']) # multiple references in the same line para2 = secs[2].findall('paragraph') @@ -1024,9 +1169,9 @@ def test_xml_keep_external_links(app): ['LINK TO', 'EXTERNAL LINKS', ',', 'Python', ',', 'THE SPHINX SITE', ',', 'UNNAMED', 'AND', 'THE PYTHON SITE', '.'], - ['i18n-with-external-links', 'http://python.org/index.html', - 'http://sphinx-doc.org', 'http://google.com', - 'http://python.org']) + ['i18n-with-external-links', 'https://python.org/index.html', + 'https://sphinx-doc.org', 'https://google.com', + 'https://python.org']) @sphinx_intl @@ -1195,7 +1340,7 @@ def test_additional_targets_should_not_be_translated(app): result = (app.outdir / 'raw.html').read_text(encoding='utf8') # raw block should not be translated - expected_expr = """<iframe src="http://sphinx-doc.org"></iframe></section>""" + expected_expr = """<iframe src="https://sphinx-doc.org"></iframe></section>""" assert_count(expected_expr, result, 1) # [figure.txt] @@ -1216,7 +1361,7 @@ def test_additional_targets_should_not_be_translated(app): 'html', srcdir='test_additional_targets_should_be_translated', confoverrides={ - 'language': 'xx', 'locale_dirs': ['.'], + 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'], 'gettext_compact': False, 'gettext_additional_targets': [ 'index', @@ -1274,7 +1419,7 @@ def test_additional_targets_should_be_translated(app): result = (app.outdir / 'raw.html').read_text(encoding='utf8') # raw block should be translated - expected_expr = """<iframe src="HTTP://SPHINX-DOC.ORG"></iframe></section>""" + expected_expr = """<iframe src="HTTPS://SPHINX-DOC.ORG"></iframe></section>""" assert_count(expected_expr, result, 1) # [figure.txt] @@ -1294,7 +1439,7 @@ def test_additional_targets_should_be_translated(app): 'html', testroot='intl_substitution_definitions', confoverrides={ - 'language': 'xx', 'locale_dirs': ['.'], + 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'], 'gettext_compact': False, 'gettext_additional_targets': [ 'index', @@ -1306,7 +1451,7 @@ def test_additional_targets_should_be_translated(app): }, ) def test_additional_targets_should_be_translated_substitution_definitions(app): - app.builder.build_all() + app.build(force_all=True) # [prolog_epilog_substitution.txt] @@ -1325,7 +1470,7 @@ def test_additional_targets_should_be_translated_substitution_definitions(app): @pytest.mark.sphinx('text') @pytest.mark.test_params(shared_result='test_intl_basic') def test_text_references(app, warning): - app.builder.build_specific([app.srcdir / 'refs.txt']) + app.build(filenames=[app.srcdir / 'refs.txt']) warnings = warning.getvalue().replace(os.sep, '/') warning_expr = 'refs.txt:\\d+: ERROR: Unknown target name:' @@ -1336,7 +1481,7 @@ def test_text_references(app, warning): 'text', testroot='intl_substitution_definitions', confoverrides={ - 'language': 'xx', 'locale_dirs': ['.'], + 'language': _CATALOG_LOCALE, 'locale_dirs': ['.'], 'gettext_compact': False, }, ) @@ -1362,7 +1507,7 @@ SUBSTITUTED IMAGE [image: SUBST_EPILOG_2 TRANSLATED][image] HERE. @pytest.mark.sphinx( 'dummy', testroot='images', srcdir='test_intl_images', - confoverrides={'language': 'xx'}, + confoverrides={'language': _CATALOG_LOCALE}, ) def test_image_glob_intl(app): app.build() @@ -1406,7 +1551,7 @@ def test_image_glob_intl(app): 'dummy', testroot='images', srcdir='test_intl_images', confoverrides={ - 'language': 'xx', + 'language': _CATALOG_LOCALE, 'figure_language_filename': '{root}{ext}.{language}', }, ) @@ -1449,7 +1594,7 @@ def test_image_glob_intl_using_figure_language_filename(app): def getwarning(warnings): - return strip_escseq(warnings.getvalue().replace(os.sep, '/')) + return strip_colors(warnings.getvalue().replace(os.sep, '/')) @pytest.mark.sphinx('html', testroot='basic', @@ -1491,13 +1636,13 @@ def test_gettext_disallow_fuzzy_translations(app): @pytest.mark.sphinx('html', testroot='basic', confoverrides={'language': 'de'}) -def test_customize_system_message(make_app, app_params, sphinx_test_tempdir): +def test_customize_system_message(make_app, app_params): try: # clear translators cache locale.translators.clear() # prepare message catalog (.po) - locale_dir = sphinx_test_tempdir / 'basic' / 'locales' / 'de' / 'LC_MESSAGES' + locale_dir = app_params.kwargs['srcdir'] / 'locales' / 'de' / 'LC_MESSAGES' locale_dir.mkdir(parents=True, exist_ok=True) with (locale_dir / 'sphinx.po').open('wb') as f: catalog = Catalog() diff --git a/tests/test_locale.py b/tests/test_intl/test_locale.py index 11dd95d..11dd95d 100644 --- a/tests/test_locale.py +++ b/tests/test_intl/test_locale.py diff --git a/tests/test_markup/__init__.py b/tests/test_markup/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_markup/__init__.py diff --git a/tests/test_markup.py b/tests/test_markup/test_markup.py index 0d877b3..c933481 100644 --- a/tests/test_markup.py +++ b/tests/test_markup/test_markup.py @@ -520,7 +520,7 @@ def test_XRefRole(inliner): @pytest.mark.sphinx('dummy', testroot='prolog') def test_rst_prolog(app, status, warning): - app.builder.build_all() + app.build(force_all=True) rst = app.env.get_doctree('restructuredtext') md = app.env.get_doctree('markdown') @@ -544,7 +544,7 @@ def test_rst_prolog(app, status, warning): @pytest.mark.sphinx('dummy', testroot='keep_warnings') def test_keep_warnings_is_True(app, status, warning): - app.builder.build_all() + app.build(force_all=True) doctree = app.env.get_doctree('index') assert_node(doctree[0], nodes.section) assert len(doctree[0]) == 2 @@ -554,7 +554,7 @@ def test_keep_warnings_is_True(app, status, warning): @pytest.mark.sphinx('dummy', testroot='keep_warnings', confoverrides={'keep_warnings': False}) def test_keep_warnings_is_False(app, status, warning): - app.builder.build_all() + app.build(force_all=True) doctree = app.env.get_doctree('index') assert_node(doctree[0], nodes.section) assert len(doctree[0]) == 1 @@ -562,7 +562,7 @@ def test_keep_warnings_is_False(app, status, warning): @pytest.mark.sphinx('dummy', testroot='refonly_bullet_list') def test_compact_refonly_bullet_list(app, status, warning): - app.builder.build_all() + app.build(force_all=True) doctree = app.env.get_doctree('index') assert_node(doctree[0], nodes.section) assert len(doctree[0]) == 5 @@ -580,7 +580,7 @@ def test_compact_refonly_bullet_list(app, status, warning): @pytest.mark.sphinx('dummy', testroot='default_role') def test_default_role1(app, status, warning): - app.builder.build_all() + app.build(force_all=True) # default-role: pep doctree = app.env.get_doctree('index') @@ -601,7 +601,7 @@ def test_default_role1(app, status, warning): @pytest.mark.sphinx('dummy', testroot='default_role', confoverrides={'default_role': 'guilabel'}) def test_default_role2(app, status, warning): - app.builder.build_all() + app.build(force_all=True) # default-role directive is stronger than configratuion doctree = app.env.get_doctree('index') diff --git a/tests/test_metadata.py b/tests/test_markup/test_metadata.py index 7f31997..7f31997 100644 --- a/tests/test_metadata.py +++ b/tests/test_markup/test_metadata.py diff --git a/tests/test_parser.py b/tests/test_markup/test_parser.py index 86163c6..86163c6 100644 --- a/tests/test_parser.py +++ b/tests/test_markup/test_parser.py diff --git a/tests/test_smartquotes.py b/tests/test_markup/test_smartquotes.py index 1d4e8e1..6c84386 100644 --- a/tests/test_smartquotes.py +++ b/tests/test_markup/test_smartquotes.py @@ -1,7 +1,8 @@ """Test smart quotes.""" import pytest -from html5lib import HTMLParser + +from sphinx.testing.util import etree_parse @pytest.mark.sphinx(buildername='html', testroot='smartquotes', freshenv=True) @@ -16,9 +17,7 @@ def test_basic(app, status, warning): def test_literals(app, status, warning): app.build() - with (app.outdir / 'literals.html').open(encoding='utf-8') as html_file: - etree = HTMLParser(namespaceHTMLElements=False).parse(html_file) - + etree = etree_parse(app.outdir / 'literals.html') for code_element in etree.iter('code'): code_text = ''.join(code_element.itertext()) diff --git a/tests/test_pycode/__init__.py b/tests/test_pycode/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_pycode/__init__.py diff --git a/tests/test_pycode.py b/tests/test_pycode/test_pycode.py index 5739787..5739787 100644 --- a/tests/test_pycode.py +++ b/tests/test_pycode/test_pycode.py diff --git a/tests/test_pycode_ast.py b/tests/test_pycode/test_pycode_ast.py index 5efd0cb..1ed43e1 100644 --- a/tests/test_pycode_ast.py +++ b/tests/test_pycode/test_pycode_ast.py @@ -18,7 +18,7 @@ from sphinx.pycode.ast import unparse as ast_unparse ("a and b and c", "a and b and c"), # BoolOp ("b'bytes'", "b'bytes'"), # Bytes ("object()", "object()"), # Call - ("1234", "1234"), # Constant + ("1234", "1234"), # Constant, Num ("{'key1': 'value1', 'key2': 'value2'}", "{'key1': 'value1', 'key2': 'value2'}"), # Dict ("a / b", "a / b"), # Div @@ -34,7 +34,6 @@ from sphinx.pycode.ast import unparse as ast_unparse ("a % b", "a % b"), # Mod ("a * b", "a * b"), # Mult ("sys", "sys"), # Name, NameConstant - ("1234", "1234"), # Num ("not a", "not a"), # Not ("a or b", "a or b"), # Or ("a**b", "a**b"), # Pow @@ -52,6 +51,13 @@ from sphinx.pycode.ast import unparse as ast_unparse "lambda x=0, /, y=1, *args, z, **kwargs: ..."), # posonlyargs ("0x1234", "0x1234"), # Constant ("1_000_000", "1_000_000"), # Constant + ("Tuple[:,:]", "Tuple[:, :]"), # Index, Subscript, 2x Slice + ("Tuple[1:2]", "Tuple[1:2]"), # Index, Subscript, Slice(no-step) + ("Tuple[1:2:3]", "Tuple[1:2:3]"), # Index, Subscript, Slice + ("x[:, np.newaxis, :, :]", + "x[:, np.newaxis, :, :]"), # Index, Subscript, numpy extended syntax + ("y[:, 1:3][np.array([0, 2, 4]), :]", + "y[:, 1:3][np.array([0, 2, 4]), :]"), # Index, 2x Subscript, numpy extended syntax ]) def test_unparse(source, expected): module = ast.parse(source) diff --git a/tests/test_pycode_parser.py b/tests/test_pycode/test_pycode_parser.py index fde648d..fde648d 100644 --- a/tests/test_pycode_parser.py +++ b/tests/test_pycode/test_pycode_parser.py diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 6a9f5c7..671aed4 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -211,7 +211,7 @@ def test_quickstart_and_build(tmp_path): 'html', # buildername status=StringIO(), warning=warnfile) - app.builder.build_all() + app.build(force_all=True) warnings = warnfile.getvalue() assert not warnings diff --git a/tests/test_search.py b/tests/test_search.py index 68a7b01..63443a8 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,4 +1,5 @@ """Test the search index builder.""" +from __future__ import annotations import json import warnings @@ -72,7 +73,7 @@ test that non-comments are indexed: fermion @pytest.mark.sphinx(testroot='ext-viewcode') def test_objects_are_escaped(app): - app.builder.build_all() + app.build(force_all=True) index = load_searchindex(app.outdir / 'searchindex.js') for item in index.get('objects').get(''): if item[-1] == 'n::Array<T, d>': # n::Array<T,d> is escaped @@ -83,7 +84,7 @@ def test_objects_are_escaped(app): @pytest.mark.sphinx(testroot='search') def test_meta_keys_are_handled_for_language_en(app): - app.builder.build_all() + app.build(force_all=True) searchindex = load_searchindex(app.outdir / 'searchindex.js') assert not is_registered_term(searchindex, 'thisnoteith') assert is_registered_term(searchindex, 'thisonetoo') @@ -96,7 +97,7 @@ def test_meta_keys_are_handled_for_language_en(app): @pytest.mark.sphinx(testroot='search', confoverrides={'html_search_language': 'de'}, freshenv=True) def test_meta_keys_are_handled_for_language_de(app): - app.builder.build_all() + app.build(force_all=True) searchindex = load_searchindex(app.outdir / 'searchindex.js') assert not is_registered_term(searchindex, 'thisnoteith') assert is_registered_term(searchindex, 'thisonetoo') @@ -109,14 +110,14 @@ def test_meta_keys_are_handled_for_language_de(app): @pytest.mark.sphinx(testroot='search') def test_stemmer_does_not_remove_short_words(app): - app.builder.build_all() + app.build(force_all=True) searchindex = (app.outdir / 'searchindex.js').read_text(encoding='utf8') assert 'bat' in searchindex @pytest.mark.sphinx(testroot='search') def test_stemmer(app): - app.builder.build_all() + app.build(force_all=True) searchindex = load_searchindex(app.outdir / 'searchindex.js') print(searchindex) assert is_registered_term(searchindex, 'findthisstemmedkei') @@ -125,7 +126,7 @@ def test_stemmer(app): @pytest.mark.sphinx(testroot='search') def test_term_in_heading_and_section(app): - app.builder.build_all() + app.build(force_all=True) searchindex = (app.outdir / 'searchindex.js').read_text(encoding='utf8') # if search term is in the title of one doc and in the text of another # both documents should be a hit in the search index as a title, @@ -136,7 +137,7 @@ def test_term_in_heading_and_section(app): @pytest.mark.sphinx(testroot='search') def test_term_in_raw_directive(app): - app.builder.build_all() + app.build(force_all=True) searchindex = load_searchindex(app.outdir / 'searchindex.js') assert not is_registered_term(searchindex, 'raw') assert is_registered_term(searchindex, 'rawword') @@ -157,8 +158,8 @@ def test_IndexBuilder(): index = IndexBuilder(env, 'en', {}, None) index.feed('docname1_1', 'filename1_1', 'title1_1', doc) index.feed('docname1_2', 'filename1_2', 'title1_2', doc) - index.feed('docname2_1', 'filename2_1', 'title2_1', doc) index.feed('docname2_2', 'filename2_2', 'title2_2', doc) + index.feed('docname2_1', 'filename2_1', 'title2_1', doc) assert index._titles == {'docname1_1': 'title1_1', 'docname1_2': 'title1_2', 'docname2_1': 'title2_1', 'docname2_2': 'title2_2'} assert index._filenames == {'docname1_1': 'filename1_1', 'docname1_2': 'filename1_2', @@ -279,7 +280,7 @@ def test_IndexBuilder_lookup(): srcdir='search_zh', ) def test_search_index_gen_zh(app): - app.builder.build_all() + app.build(force_all=True) index = load_searchindex(app.outdir / 'searchindex.js') assert 'chinesetest ' not in index['terms'] assert 'chinesetest' in index['terms'] @@ -304,3 +305,44 @@ def test_parallel(app): app.build() index = load_searchindex(app.outdir / 'searchindex.js') assert index['docnames'] == ['index', 'nosearch', 'tocitem'] + + +@pytest.mark.sphinx(testroot='search') +def test_search_index_is_deterministic(app): + app.build(force_all=True) + index = load_searchindex(app.outdir / 'searchindex.js') + # Pretty print the index. Only shown by pytest on failure. + print(f'searchindex.js contents:\n\n{json.dumps(index, indent=2)}') + assert_is_sorted(index, '') + + +def is_title_tuple_type(item: list[int | str]): + """ + In the search index, titles inside .alltitles are stored as a tuple of + (document_idx, title_anchor). Tuples are represented as lists in JSON, + but their contents must not be sorted. We cannot sort them anyway, as + document_idx is an int and title_anchor is a str. + """ + return len(item) == 2 and isinstance(item[0], int) and isinstance(item[1], str) + + +def assert_is_sorted(item, path: str): + lists_not_to_sort = { + # Each element of .titles is related to the element of .docnames in the same position. + # The ordering is deterministic because .docnames is sorted. + '.titles', + # Each element of .filenames is related to the element of .docnames in the same position. + # The ordering is deterministic because .docnames is sorted. + '.filenames', + } + + err_path = path or '<root>' + if isinstance(item, dict): + assert list(item.keys()) == sorted(item.keys()), f'{err_path} is not sorted' + for key, value in item.items(): + assert_is_sorted(value, f'{path}.{key}') + elif isinstance(item, list): + if not is_title_tuple_type(item) and path not in lists_not_to_sort: + assert item == sorted(item), f'{err_path} is not sorted' + for i, child in enumerate(item): + assert_is_sorted(child, f'{path}[{i}]') diff --git a/tests/test_theming.py b/tests/test_theming.py deleted file mode 100644 index b4c8511..0000000 --- a/tests/test_theming.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Test the Theme class.""" - -import os - -import alabaster -import pytest - -import sphinx.builders.html -from sphinx.theming import ThemeError - - -@pytest.mark.sphinx( - testroot='theming', - confoverrides={'html_theme': 'ziptheme', - 'html_theme_options.testopt': 'foo'}) -def test_theme_api(app, status, warning): - cfg = app.config - - themes = ['basic', 'default', 'scrolls', 'agogo', 'sphinxdoc', 'haiku', - 'traditional', 'epub', 'nature', 'pyramid', 'bizstyle', 'classic', 'nonav', - 'test-theme', 'ziptheme', 'staticfiles', 'parent', 'child'] - try: - alabaster_version = alabaster.__version_info__ - except AttributeError: - alabaster_version = alabaster.version.__version_info__ - if alabaster_version >= (0, 7, 11): - themes.append('alabaster') - - # test Theme class API - assert set(app.registry.html_themes.keys()) == set(themes) - assert app.registry.html_themes['test-theme'] == str(app.srcdir / 'test_theme' / 'test-theme') - assert app.registry.html_themes['ziptheme'] == str(app.srcdir / 'ziptheme.zip') - assert app.registry.html_themes['staticfiles'] == str(app.srcdir / 'test_theme' / 'staticfiles') - - # test Theme instance API - theme = app.builder.theme - assert theme.name == 'ziptheme' - themedir = theme.themedir - assert theme.base.name == 'basic' - assert len(theme.get_theme_dirs()) == 2 - - # direct setting - assert theme.get_config('theme', 'stylesheet') == 'custom.css' - # inherited setting - assert theme.get_config('options', 'nosidebar') == 'false' - # nonexisting setting - assert theme.get_config('theme', 'foobar', 'def') == 'def' - with pytest.raises(ThemeError): - theme.get_config('theme', 'foobar') - - # options API - - options = theme.get_options({'nonexisting': 'foo'}) - assert 'nonexisting' not in options - - options = theme.get_options(cfg.html_theme_options) - assert options['testopt'] == 'foo' - assert options['nosidebar'] == 'false' - - # cleanup temp directories - theme.cleanup() - assert not os.path.exists(themedir) - - -@pytest.mark.sphinx(testroot='double-inheriting-theme') -def test_double_inheriting_theme(app, status, warning): - assert app.builder.theme.name == 'base_theme2' - app.build() # => not raises TemplateNotFound - - -@pytest.mark.sphinx(testroot='theming', - confoverrides={'html_theme': 'child'}) -def test_nested_zipped_theme(app, status, warning): - assert app.builder.theme.name == 'child' - app.build() # => not raises TemplateNotFound - - -@pytest.mark.sphinx(testroot='theming', - confoverrides={'html_theme': 'staticfiles'}) -def test_staticfiles(app, status, warning): - app.build() - assert (app.outdir / '_static' / 'staticimg.png').exists() - assert (app.outdir / '_static' / 'statictmpl.html').exists() - assert (app.outdir / '_static' / 'statictmpl.html').read_text(encoding='utf8') == ( - '<!-- testing static templates -->\n' - '<html><project>Python</project></html>' - ) - - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert '<meta name="testopt" content="optdefault" />' in result - - -@pytest.mark.sphinx(testroot='theming', - confoverrides={'html_theme': 'test-theme'}) -def test_dark_style(app, monkeypatch): - monkeypatch.setattr(sphinx.builders.html, '_file_checksum', lambda o, f: '') - - style = app.builder.dark_highlighter.formatter_args.get('style') - assert style.__name__ == 'MonokaiStyle' - - app.build() - assert (app.outdir / '_static' / 'pygments_dark.css').exists() - - css_file, properties = app.registry.css_files[0] - assert css_file == 'pygments_dark.css' - assert "media" in properties - assert properties["media"] == '(prefers-color-scheme: dark)' - - assert sorted(f.filename for f in app.builder._css_files) == [ - '_static/classic.css', - '_static/pygments.css', - '_static/pygments_dark.css', - ] - - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert '<link rel="stylesheet" type="text/css" href="_static/pygments.css" />' in result - assert ('<link id="pygments_dark_css" media="(prefers-color-scheme: dark)" ' - 'rel="stylesheet" type="text/css" ' - 'href="_static/pygments_dark.css" />') in result - - -@pytest.mark.sphinx(testroot='theming') -def test_theme_sidebars(app, status, warning): - app.build() - - # test-theme specifies globaltoc and searchbox as default sidebars - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert '<h3><a href="#">Table of Contents</a></h3>' in result - assert '<h3>Related Topics</h3>' not in result - assert '<h3>This Page</h3>' not in result - assert '<h3 id="searchlabel">Quick search</h3>' in result diff --git a/tests/test_theming/__init__.py b/tests/test_theming/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_theming/__init__.py diff --git a/tests/test_theming/test_html_theme.py b/tests/test_theming/test_html_theme.py new file mode 100644 index 0000000..e9a183f --- /dev/null +++ b/tests/test_theming/test_html_theme.py @@ -0,0 +1,35 @@ +import pytest + + +@pytest.mark.sphinx('html', testroot='theming') +def test_theme_options(app, status, warning): + app.build() + + result = (app.outdir / '_static' / 'documentation_options.js').read_text(encoding='utf8') + assert 'NAVIGATION_WITH_KEYS: false' in result + assert 'ENABLE_SEARCH_SHORTCUTS: true' in result + + +@pytest.mark.sphinx( + 'html', + testroot='theming', + confoverrides={ + 'html_theme_options.navigation_with_keys': True, + 'html_theme_options.enable_search_shortcuts': False, + }, +) +def test_theme_options_with_override(app, status, warning): + app.build() + + result = (app.outdir / '_static' / 'documentation_options.js').read_text(encoding='utf8') + assert 'NAVIGATION_WITH_KEYS: true' in result + assert 'ENABLE_SEARCH_SHORTCUTS: false' in result + + +@pytest.mark.sphinx('html', testroot='build-html-theme-having-multiple-stylesheets') +def test_theme_having_multiple_stylesheets(app): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf-8') + + assert '<link rel="stylesheet" type="text/css" href="_static/mytheme.css" />' in content + assert '<link rel="stylesheet" type="text/css" href="_static/extra.css" />' in content diff --git a/tests/test_templating.py b/tests/test_theming/test_templating.py index a41af93..bc02e97 100644 --- a/tests/test_templating.py +++ b/tests/test_theming/test_templating.py @@ -10,7 +10,7 @@ def test_layout_overloading(make_app, app_params): args, kwargs = app_params app = make_app(*args, **kwargs) setup_documenters(app) - app.builder.build_update() + app.build() result = (app.outdir / 'index.html').read_text(encoding='utf8') assert '<!-- layout overloading -->' in result @@ -21,21 +21,28 @@ def test_autosummary_class_template_overloading(make_app, app_params): args, kwargs = app_params app = make_app(*args, **kwargs) setup_documenters(app) - app.builder.build_update() + app.build() - result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text(encoding='utf8') + result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text( + encoding='utf8' + ) assert 'autosummary/class.rst method block overloading' in result assert 'foobar' not in result -@pytest.mark.sphinx('html', testroot='templating', - confoverrides={'autosummary_context': {'sentence': 'foobar'}}) +@pytest.mark.sphinx( + 'html', + testroot='templating', + confoverrides={'autosummary_context': {'sentence': 'foobar'}}, +) def test_autosummary_context(make_app, app_params): args, kwargs = app_params app = make_app(*args, **kwargs) setup_documenters(app) - app.builder.build_update() + app.build() - result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text(encoding='utf8') + result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text( + encoding='utf8' + ) assert 'autosummary/class.rst method block overloading' in result assert 'foobar' in result diff --git a/tests/test_theming/test_theming.py b/tests/test_theming/test_theming.py new file mode 100644 index 0000000..867f8a0 --- /dev/null +++ b/tests/test_theming/test_theming.py @@ -0,0 +1,227 @@ +"""Test the Theme class.""" + +import os +import shutil +from pathlib import Path +from xml.etree.ElementTree import ParseError + +import pytest +from defusedxml.ElementTree import parse as xml_parse + +import sphinx.builders.html +from sphinx.errors import ThemeError +from sphinx.theming import ( + _ConfigFile, + _convert_theme_conf, + _convert_theme_toml, + _load_theme, + _load_theme_conf, + _load_theme_toml, +) + +HERE = Path(__file__).resolve().parent + + +@pytest.mark.sphinx( + testroot='theming', + confoverrides={'html_theme': 'ziptheme', 'html_theme_options.testopt': 'foo'}, +) +def test_theme_api(app, status, warning): + themes = [ + 'basic', + 'default', + 'scrolls', + 'agogo', + 'sphinxdoc', + 'haiku', + 'traditional', + 'epub', + 'nature', + 'pyramid', + 'bizstyle', + 'classic', + 'nonav', + 'test-theme', + 'ziptheme', + 'staticfiles', + 'parent', + 'child', + 'alabaster', + ] + + # test Theme class API + assert set(app.registry.html_themes.keys()) == set(themes) + assert app.registry.html_themes['test-theme'] == str( + app.srcdir / 'test_theme' / 'test-theme' + ) + assert app.registry.html_themes['ziptheme'] == str(app.srcdir / 'ziptheme.zip') + assert app.registry.html_themes['staticfiles'] == str( + app.srcdir / 'test_theme' / 'staticfiles' + ) + + # test Theme instance API + theme = app.builder.theme + assert theme.name == 'ziptheme' + assert len(theme.get_theme_dirs()) == 2 + + # direct setting + assert theme.get_config('theme', 'stylesheet') == 'custom.css' + # inherited setting + assert theme.get_config('options', 'nosidebar') == 'false' + # nonexisting setting + assert theme.get_config('theme', 'foobar', 'def') == 'def' + with pytest.raises(ThemeError): + theme.get_config('theme', 'foobar') + + # options API + + options = theme.get_options({'nonexisting': 'foo'}) + assert 'nonexisting' not in options + + options = theme.get_options(app.config.html_theme_options) + assert options['testopt'] == 'foo' + assert options['nosidebar'] == 'false' + + # cleanup temp directories + theme._cleanup() + assert not any(map(os.path.exists, theme._tmp_dirs)) + + +def test_nonexistent_theme_settings(tmp_path): + # Check that error occurs with a non-existent theme.toml or theme.conf + # (https://github.com/sphinx-doc/sphinx/issues/11668) + with pytest.raises(ThemeError): + _load_theme('', str(tmp_path)) + + +@pytest.mark.sphinx(testroot='double-inheriting-theme') +def test_double_inheriting_theme(app, status, warning): + assert app.builder.theme.name == 'base_theme2' + app.build() # => not raises TemplateNotFound + + +@pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'child'}) +def test_nested_zipped_theme(app, status, warning): + assert app.builder.theme.name == 'child' + app.build() # => not raises TemplateNotFound + + +@pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'staticfiles'}) +def test_staticfiles(app, status, warning): + app.build() + assert (app.outdir / '_static' / 'staticimg.png').exists() + assert (app.outdir / '_static' / 'statictmpl.html').exists() + assert (app.outdir / '_static' / 'statictmpl.html').read_text(encoding='utf8') == ( + '<!-- testing static templates -->\n<html><project>Python</project></html>' + ) + + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert '<meta name="testopt" content="optdefault" />' in result + + +@pytest.mark.sphinx(testroot='theming', confoverrides={'html_theme': 'test-theme'}) +def test_dark_style(app, monkeypatch): + monkeypatch.setattr(sphinx.builders.html, '_file_checksum', lambda o, f: '') + + style = app.builder.dark_highlighter.formatter_args.get('style') + assert style.__name__ == 'MonokaiStyle' + + app.build() + assert (app.outdir / '_static' / 'pygments_dark.css').exists() + + css_file, properties = app.registry.css_files[0] + assert css_file == 'pygments_dark.css' + assert 'media' in properties + assert properties['media'] == '(prefers-color-scheme: dark)' + + assert sorted(f.filename for f in app.builder._css_files) == [ + '_static/classic.css', + '_static/pygments.css', + '_static/pygments_dark.css', + ] + + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert '<link rel="stylesheet" type="text/css" href="_static/pygments.css" />' in result + assert ( + '<link id="pygments_dark_css" media="(prefers-color-scheme: dark)" ' + 'rel="stylesheet" type="text/css" ' + 'href="_static/pygments_dark.css" />' + ) in result + + +@pytest.mark.sphinx(testroot='theming') +def test_theme_sidebars(app, status, warning): + app.build() + + # test-theme specifies globaltoc and searchbox as default sidebars + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert '<h3><a href="#">Table of Contents</a></h3>' in result + assert '<h3>Related Topics</h3>' not in result + assert '<h3>This Page</h3>' not in result + assert '<h3 id="searchlabel">Quick search</h3>' in result + + +@pytest.mark.parametrize( + 'theme_name', + [ + 'alabaster', + 'agogo', + 'basic', + 'bizstyle', + 'classic', + 'default', + 'epub', + 'haiku', + 'nature', + 'nonav', + 'pyramid', + 'scrolls', + 'sphinxdoc', + 'traditional', + ], +) +def test_theme_builds(make_app, rootdir, sphinx_test_tempdir, theme_name): + """Test all the themes included with Sphinx build a simple project and produce valid XML.""" + testroot_path = rootdir / 'test-basic' + srcdir = sphinx_test_tempdir / f'test-theme-{theme_name}' + shutil.copytree(testroot_path, srcdir) + + app = make_app(srcdir=srcdir, confoverrides={'html_theme': theme_name}) + app.build() + assert not app.warning.getvalue().strip() + assert app.outdir.joinpath('index.html').exists() + + # check that the generated HTML files are well-formed (as strict XML) + for html_file in app.outdir.rglob('*.html'): + try: + xml_parse(html_file) + except ParseError as exc: + pytest.fail(f'Failed to parse {html_file.relative_to(app.outdir)}: {exc}') + + +def test_config_file_toml(): + config_path = HERE / 'theme.toml' + cfg = _load_theme_toml(str(config_path)) + config = _convert_theme_toml(cfg) + + assert config == _ConfigFile( + stylesheets=('spam.css', 'ham.css'), + sidebar_templates=None, + pygments_style_default='spam', + pygments_style_dark=None, + options={'lobster': 'thermidor'}, + ) + + +def test_config_file_conf(): + config_path = HERE / 'theme.conf' + cfg = _load_theme_conf(str(config_path)) + config = _convert_theme_conf(cfg) + + assert config == _ConfigFile( + stylesheets=('spam.css', 'ham.css'), + sidebar_templates=None, + pygments_style_default='spam', + pygments_style_dark=None, + options={'lobster': 'thermidor'}, + ) diff --git a/tests/test_theming/theme.conf b/tests/test_theming/theme.conf new file mode 100644 index 0000000..b53fcb7 --- /dev/null +++ b/tests/test_theming/theme.conf @@ -0,0 +1,7 @@ +[theme] +inherit = none +stylesheet = spam.css, ham.css +pygments_style = spam + +[options] +lobster = thermidor diff --git a/tests/test_theming/theme.toml b/tests/test_theming/theme.toml new file mode 100644 index 0000000..96a5668 --- /dev/null +++ b/tests/test_theming/theme.toml @@ -0,0 +1,10 @@ +[theme] +inherit = "none" +stylesheets = [ + "spam.css", + "ham.css", +] +pygments_style = { default = "spam" } + +[options] +lobster = "thermidor" diff --git a/tests/test_toctree.py b/tests/test_toctree.py index 39d0916..e59085d 100644 --- a/tests/test_toctree.py +++ b/tests/test_toctree.py @@ -6,7 +6,7 @@ import pytest @pytest.mark.sphinx(testroot='toctree-glob') def test_relations(app, status, warning): - app.builder.build_all() + app.build(force_all=True) assert app.builder.relations['index'] == [None, None, 'foo'] assert app.builder.relations['foo'] == ['index', 'index', 'bar/index'] assert app.builder.relations['bar/index'] == ['index', 'foo', 'bar/bar_1'] @@ -23,7 +23,7 @@ def test_relations(app, status, warning): @pytest.mark.sphinx('singlehtml', testroot='toctree-empty') def test_singlehtml_toctree(app, status, warning): - app.builder.build_all() + app.build(force_all=True) try: app.builder._get_local_toctree('index') except AttributeError: @@ -36,4 +36,4 @@ def test_numbered_toctree(app, status, warning): index = (app.srcdir / 'index.rst').read_text(encoding='utf8') index = re.sub(':numbered:.*', ':numbered: 1', index) (app.srcdir / 'index.rst').write_text(index, encoding='utf8') - app.builder.build_all() + app.build(force_all=True) diff --git a/tests/test_transforms/__init__.py b/tests/test_transforms/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_transforms/__init__.py diff --git a/tests/test_transforms_move_module_targets.py b/tests/test_transforms/test_transforms_move_module_targets.py index e0e9f1d..e0e9f1d 100644 --- a/tests/test_transforms_move_module_targets.py +++ b/tests/test_transforms/test_transforms_move_module_targets.py diff --git a/tests/test_transforms_post_transforms.py b/tests/test_transforms/test_transforms_post_transforms.py index b9b6126..c4e699b 100644 --- a/tests/test_transforms_post_transforms.py +++ b/tests/test_transforms/test_transforms_post_transforms.py @@ -79,6 +79,7 @@ def test_keyboard_hyphen_spaces(app): class TestSigElementFallbackTransform: """Integration test for :class:`sphinx.transforms.post_transforms.SigElementFallbackTransform`.""" + # safe copy of the "built-in" desc_sig_* nodes (during the test, instances of such nodes # will be created sequentially, so we fix a possible order at the beginning using a tuple) _builtin_sig_elements: tuple[type[addnodes.desc_sig_element], ...] = tuple(SIG_ELEMENTS) @@ -264,5 +265,5 @@ class TestSigElementFallbackTransform: # extract messages messages = caplog.record_tuples stdout = [message for _, lvl, message in messages if lvl == logging.INFO] - stderr = [message for _, lvl, message in messages if lvl == logging.WARN] + stderr = [message for _, lvl, message in messages if lvl == logging.WARNING] return document, stdout, stderr diff --git a/tests/test_transforms_post_transforms_code.py b/tests/test_transforms/test_transforms_post_transforms_code.py index 4423d5b..4423d5b 100644 --- a/tests/test_transforms_post_transforms_code.py +++ b/tests/test_transforms/test_transforms_post_transforms_code.py diff --git a/tests/test_transforms_reorder_nodes.py b/tests/test_transforms/test_transforms_reorder_nodes.py index 7ffdae6..7ffdae6 100644 --- a/tests/test_transforms_reorder_nodes.py +++ b/tests/test_transforms/test_transforms_reorder_nodes.py diff --git a/tests/test_util/__init__.py b/tests/test_util/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_util/__init__.py diff --git a/tests/test_util/intersphinx_data.py b/tests/test_util/intersphinx_data.py new file mode 100644 index 0000000..042ee76 --- /dev/null +++ b/tests/test_util/intersphinx_data.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import zlib +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Final + +INVENTORY_V1: Final[bytes] = b'''\ +# Sphinx inventory version 1 +# Project: foo +# Version: 1.0 +module mod foo.html +module.cls class foo.html +''' + +INVENTORY_V2: Final[bytes] = b'''\ +# Sphinx inventory version 2 +# Project: foo +# Version: 2.0 +# The remainder of this file is compressed with zlib. +''' + zlib.compress(b'''\ +module1 py:module 0 foo.html#module-module1 Long Module desc +module2 py:module 0 foo.html#module-$ - +module1.func py:function 1 sub/foo.html#$ - +module1.Foo.bar py:method 1 index.html#foo.Bar.baz - +CFunc c:function 2 cfunc.html#CFunc - +std cpp:type 1 index.html#std - +std::uint8_t cpp:type 1 index.html#std_uint8_t - +foo::Bar cpp:class 1 index.html#cpp_foo_bar - +foo::Bar::baz cpp:function 1 index.html#cpp_foo_bar_baz - +foons cpp:type 1 index.html#foons - +foons::bartype cpp:type 1 index.html#foons_bartype - +a term std:term -1 glossary.html#term-a-term - +ls.-l std:cmdoption 1 index.html#cmdoption-ls-l - +docname std:doc -1 docname.html - +foo js:module 1 index.html#foo - +foo.bar js:class 1 index.html#foo.bar - +foo.bar.baz js:method 1 index.html#foo.bar.baz - +foo.bar.qux js:data 1 index.html#foo.bar.qux - +a term including:colon std:term -1 glossary.html#term-a-term-including-colon - +The-Julia-Domain std:label -1 write_inventory/#$ The Julia Domain +''') + +INVENTORY_V2_NO_VERSION: Final[bytes] = b'''\ +# Sphinx inventory version 2 +# Project: foo +# Version: +# The remainder of this file is compressed with zlib. +''' + zlib.compress(b'''\ +module1 py:module 0 foo.html#module-module1 Long Module desc +''') diff --git a/tests/test_util.py b/tests/test_util/test_util.py index 4389894..4389894 100644 --- a/tests/test_util.py +++ b/tests/test_util/test_util.py diff --git a/tests/test_util/test_util_console.py b/tests/test_util/test_util_console.py new file mode 100644 index 0000000..b617a33 --- /dev/null +++ b/tests/test_util/test_util_console.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import itertools +import operator +from typing import TYPE_CHECKING + +import pytest + +from sphinx.util.console import blue, reset, strip_colors, strip_escape_sequences + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from typing import Final, TypeVar + + _T = TypeVar('_T') + +CURSOR_UP: Final[str] = '\x1b[2A' # ignored ANSI code +ERASE_LINE: Final[str] = '\x1b[2K' # supported ANSI code +TEXT: Final[str] = '\x07 Hello world!' + + +@pytest.mark.parametrize( + ('strip_function', 'ansi_base_blocks', 'text_base_blocks'), + [ + ( + strip_colors, + # double ERASE_LINE so that the tested strings may have 2 of them + [TEXT, blue(TEXT), reset(TEXT), ERASE_LINE, ERASE_LINE, CURSOR_UP], + # :func:`strip_colors` removes color codes but keeps ERASE_LINE and CURSOR_UP + [TEXT, TEXT, TEXT, ERASE_LINE, ERASE_LINE, CURSOR_UP], + ), + ( + strip_escape_sequences, + # double ERASE_LINE so that the tested strings may have 2 of them + [TEXT, blue(TEXT), reset(TEXT), ERASE_LINE, ERASE_LINE, CURSOR_UP], + # :func:`strip_escape_sequences` strips ANSI codes known by Sphinx + [TEXT, TEXT, TEXT, '', '', CURSOR_UP], + ), + ], + ids=[strip_colors.__name__, strip_escape_sequences.__name__], +) +def test_strip_ansi( + strip_function: Callable[[str], str], + ansi_base_blocks: Sequence[str], + text_base_blocks: Sequence[str], +) -> None: + assert callable(strip_function) + assert len(text_base_blocks) == len(ansi_base_blocks) + N = len(ansi_base_blocks) + + def next_ansi_blocks(choices: Sequence[str], n: int) -> Sequence[str]: + # Get a list of *n* words from a cyclic sequence of *choices*. + # + # For instance ``next_ansi_blocks(['a', 'b'], 3) == ['a', 'b', 'a']``. + stream = itertools.cycle(choices) + return list(map(operator.itemgetter(0), zip(stream, range(n)))) + + # generate all permutations of length N + for sigma in itertools.permutations(range(N), N): + # apply the permutation on the blocks with ANSI codes + ansi_blocks = list(map(ansi_base_blocks.__getitem__, sigma)) + # apply the permutation on the blocks with stripped codes + text_blocks = list(map(text_base_blocks.__getitem__, sigma)) + + for glue, n in itertools.product(['.', '\n', '\r\n'], range(4 * N)): + ansi_strings = next_ansi_blocks(ansi_blocks, n) + text_strings = next_ansi_blocks(text_blocks, n) + assert len(ansi_strings) == len(text_strings) == n + + ansi_string = glue.join(ansi_strings) + text_string = glue.join(text_strings) + assert strip_function(ansi_string) == text_string + + +def test_strip_ansi_short_forms(): + # In Sphinx, we always "normalize" the color codes so that they + # match "\x1b\[(\d\d;){0,2}(\d\d)m" but it might happen that + # some messages use '\x1b[0m' instead of ``reset(s)``, so we + # test whether this alternative form is supported or not. + + for strip_function in [strip_colors, strip_escape_sequences]: + # \x1b[m and \x1b[0m are equivalent to \x1b[00m + assert strip_function('\x1b[m') == '' + assert strip_function('\x1b[0m') == '' + + # \x1b[1m is equivalent to \x1b[01m + assert strip_function('\x1b[1mbold\x1b[0m') == 'bold' + + # \x1b[K is equivalent to \x1b[0K + assert strip_escape_sequences('\x1b[K') == '' diff --git a/tests/test_util_display.py b/tests/test_util/test_util_display.py index 9ecdd6a..a18fa1e 100644 --- a/tests/test_util_display.py +++ b/tests/test_util/test_util_display.py @@ -2,8 +2,8 @@ import pytest -from sphinx.testing.util import strip_escseq from sphinx.util import logging +from sphinx.util.console import strip_colors from sphinx.util.display import ( SkipProgressMessage, display_chunk, @@ -28,13 +28,14 @@ def test_status_iterator_length_0(app, status, warning): status.seek(0) status.truncate(0) yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... ')) - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing ... hello sphinx world \n' in output assert yields == ['hello', 'sphinx', 'world'] @pytest.mark.sphinx('dummy') -def test_status_iterator_verbosity_0(app, status, warning): +def test_status_iterator_verbosity_0(app, status, warning, monkeypatch): + monkeypatch.setenv("FORCE_COLOR", "1") logging.setup(app, status, warning) # test for status_iterator (verbosity=0) @@ -42,7 +43,7 @@ def test_status_iterator_verbosity_0(app, status, warning): status.truncate(0) yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... ', length=3, verbosity=0)) - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing ... [ 33%] hello\r' in output assert 'testing ... [ 67%] sphinx\r' in output assert 'testing ... [100%] world\r\n' in output @@ -50,7 +51,8 @@ def test_status_iterator_verbosity_0(app, status, warning): @pytest.mark.sphinx('dummy') -def test_status_iterator_verbosity_1(app, status, warning): +def test_status_iterator_verbosity_1(app, status, warning, monkeypatch): + monkeypatch.setenv("FORCE_COLOR", "1") logging.setup(app, status, warning) # test for status_iterator (verbosity=1) @@ -58,7 +60,7 @@ def test_status_iterator_verbosity_1(app, status, warning): status.truncate(0) yields = list(status_iterator(['hello', 'sphinx', 'world'], 'testing ... ', length=3, verbosity=1)) - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing ... [ 33%] hello\n' in output assert 'testing ... [ 67%] sphinx\n' in output assert 'testing ... [100%] world\n\n' in output @@ -73,14 +75,14 @@ def test_progress_message(app, status, warning): with progress_message('testing'): logger.info('blah ', nonl=True) - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing... blah done\n' in output # skipping case with progress_message('testing'): raise SkipProgressMessage('Reason: %s', 'error') # NoQA: EM101 - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing... skipped\nReason: error\n' in output # error case @@ -90,7 +92,7 @@ def test_progress_message(app, status, warning): except Exception: pass - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing... failed\n' in output # decorator @@ -99,5 +101,5 @@ def test_progress_message(app, status, warning): logger.info('in func ', nonl=True) func() - output = strip_escseq(status.getvalue()) + output = strip_colors(status.getvalue()) assert 'testing... in func done\n' in output diff --git a/tests/test_util_docstrings.py b/tests/test_util/test_util_docstrings.py index 813e84e..813e84e 100644 --- a/tests/test_util_docstrings.py +++ b/tests/test_util/test_util_docstrings.py diff --git a/tests/test_util_docutils.py b/tests/test_util/test_util_docutils.py index 69999eb..69999eb 100644 --- a/tests/test_util_docutils.py +++ b/tests/test_util/test_util_docutils.py diff --git a/tests/test_util_fileutil.py b/tests/test_util/test_util_fileutil.py index 9c23821..9c23821 100644 --- a/tests/test_util_fileutil.py +++ b/tests/test_util/test_util_fileutil.py diff --git a/tests/test_util_i18n.py b/tests/test_util/test_util_i18n.py index 9a1ecc5..f6baa04 100644 --- a/tests/test_util_i18n.py +++ b/tests/test_util/test_util_i18n.py @@ -160,6 +160,8 @@ def test_CatalogRepository(tmp_path): (tmp_path / 'loc2' / 'xx' / 'LC_MESSAGES').mkdir(parents=True, exist_ok=True) (tmp_path / 'loc2' / 'xx' / 'LC_MESSAGES' / 'test1.po').write_text('#', encoding='utf8') (tmp_path / 'loc2' / 'xx' / 'LC_MESSAGES' / 'test7.po').write_text('#', encoding='utf8') + (tmp_path / 'loc1' / 'xx' / 'LC_MESSAGES' / '.dotdir2').mkdir(parents=True, exist_ok=True) + (tmp_path / 'loc1' / 'xx' / 'LC_MESSAGES' / '.dotdir2' / 'test8.po').write_text('#', encoding='utf8') # for language xx repo = i18n.CatalogRepository(tmp_path, ['loc1', 'loc2'], 'xx', 'utf-8') diff --git a/tests/test_util_images.py b/tests/test_util/test_util_images.py index 15853c7..15853c7 100644 --- a/tests/test_util_images.py +++ b/tests/test_util/test_util_images.py diff --git a/tests/test_util_inspect.py b/tests/test_util/test_util_inspect.py index 73f9656..32840b8 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util/test_util_inspect.py @@ -62,6 +62,7 @@ async def coroutinefunc(): async def asyncgenerator(): yield + partial_func = functools.partial(func) partial_coroutinefunc = functools.partial(coroutinefunc) @@ -222,168 +223,140 @@ def test_signature_partialmethod(): def test_signature_annotations(): - from .typing_test_data import ( - Node, - f0, - f1, - f2, - f3, - f4, - f5, - f6, - f7, - f8, - f9, - f10, - f11, - f12, - f13, - f14, - f15, - f16, - f17, - f18, - f19, - f20, - f21, - f22, - f23, - f24, - f25, - ) + import tests.test_util.typing_test_data as mod # Class annotations - sig = inspect.signature(f0) + sig = inspect.signature(mod.f0) assert stringify_signature(sig) == '(x: int, y: numbers.Integral) -> None' # Generic types with concrete parameters - sig = inspect.signature(f1) + sig = inspect.signature(mod.f1) assert stringify_signature(sig) == '(x: list[int]) -> typing.List[int]' # TypeVars and generic types with TypeVars - sig = inspect.signature(f2) - assert stringify_signature(sig) == ('(x: typing.List[tests.typing_test_data.T],' - ' y: typing.List[tests.typing_test_data.T_co],' - ' z: tests.typing_test_data.T' - ') -> typing.List[tests.typing_test_data.T_contra]') + sig = inspect.signature(mod.f2) + assert stringify_signature(sig) == ('(x: typing.List[tests.test_util.typing_test_data.T],' + ' y: typing.List[tests.test_util.typing_test_data.T_co],' + ' z: tests.test_util.typing_test_data.T' + ') -> typing.List[tests.test_util.typing_test_data.T_contra]') # Union types - sig = inspect.signature(f3) + sig = inspect.signature(mod.f3) assert stringify_signature(sig) == '(x: str | numbers.Integral) -> None' # Quoted annotations - sig = inspect.signature(f4) + sig = inspect.signature(mod.f4) assert stringify_signature(sig) == '(x: str, y: str) -> None' # Keyword-only arguments - sig = inspect.signature(f5) + sig = inspect.signature(mod.f5) assert stringify_signature(sig) == '(x: int, *, y: str, z: str) -> None' # Keyword-only arguments with varargs - sig = inspect.signature(f6) + sig = inspect.signature(mod.f6) assert stringify_signature(sig) == '(x: int, *args, y: str, z: str) -> None' # Space around '=' for defaults - sig = inspect.signature(f7) + sig = inspect.signature(mod.f7) if sys.version_info[:2] <= (3, 10): assert stringify_signature(sig) == '(x: int | None = None, y: dict = {}) -> None' else: assert stringify_signature(sig) == '(x: int = None, y: dict = {}) -> None' # Callable types - sig = inspect.signature(f8) + sig = inspect.signature(mod.f8) assert stringify_signature(sig) == '(x: typing.Callable[[int, str], int]) -> None' - sig = inspect.signature(f9) + sig = inspect.signature(mod.f9) assert stringify_signature(sig) == '(x: typing.Callable) -> None' # Tuple types - sig = inspect.signature(f10) + sig = inspect.signature(mod.f10) assert stringify_signature(sig) == '(x: typing.Tuple[int, str], y: typing.Tuple[int, ...]) -> None' # Instance annotations - sig = inspect.signature(f11) + sig = inspect.signature(mod.f11) assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None' # tuple with more than two items - sig = inspect.signature(f12) + sig = inspect.signature(mod.f12) assert stringify_signature(sig) == '() -> typing.Tuple[int, str, int]' # optional - sig = inspect.signature(f13) + sig = inspect.signature(mod.f13) assert stringify_signature(sig) == '() -> str | None' # optional union - sig = inspect.signature(f20) + sig = inspect.signature(mod.f20) assert stringify_signature(sig) in ('() -> int | str | None', '() -> str | int | None') # Any - sig = inspect.signature(f14) + sig = inspect.signature(mod.f14) assert stringify_signature(sig) == '() -> typing.Any' # ForwardRef - sig = inspect.signature(f15) + sig = inspect.signature(mod.f15) assert stringify_signature(sig) == '(x: Unknown, y: int) -> typing.Any' # keyword only arguments (1) - sig = inspect.signature(f16) + sig = inspect.signature(mod.f16) assert stringify_signature(sig) == '(arg1, arg2, *, arg3=None, arg4=None)' # keyword only arguments (2) - sig = inspect.signature(f17) + sig = inspect.signature(mod.f17) assert stringify_signature(sig) == '(*, arg3, arg4)' - sig = inspect.signature(f18) + sig = inspect.signature(mod.f18) assert stringify_signature(sig) == ('(self, arg1: int | typing.Tuple = 10) -> ' 'typing.List[typing.Dict]') # annotations for variadic and keyword parameters - sig = inspect.signature(f19) + sig = inspect.signature(mod.f19) assert stringify_signature(sig) == '(*args: int, **kwargs: str)' # default value is inspect.Signature.empty - sig = inspect.signature(f21) + sig = inspect.signature(mod.f21) assert stringify_signature(sig) == "(arg1='whatever', arg2)" # type hints by string - sig = inspect.signature(Node.children) - assert stringify_signature(sig) == '(self) -> typing.List[tests.typing_test_data.Node]' + sig = inspect.signature(mod.Node.children) + assert stringify_signature(sig) == '(self) -> typing.List[tests.test_util.typing_test_data.Node]' - sig = inspect.signature(Node.__init__) - assert stringify_signature(sig) == '(self, parent: tests.typing_test_data.Node | None) -> None' + sig = inspect.signature(mod.Node.__init__) + assert stringify_signature(sig) == '(self, parent: tests.test_util.typing_test_data.Node | None) -> None' # show_annotation is False - sig = inspect.signature(f7) + sig = inspect.signature(mod.f7) assert stringify_signature(sig, show_annotation=False) == '(x=None, y={})' # show_return_annotation is False - sig = inspect.signature(f7) + sig = inspect.signature(mod.f7) if sys.version_info[:2] <= (3, 10): assert stringify_signature(sig, show_return_annotation=False) == '(x: int | None = None, y: dict = {})' else: assert stringify_signature(sig, show_return_annotation=False) == '(x: int = None, y: dict = {})' # unqualified_typehints is True - sig = inspect.signature(f7) + sig = inspect.signature(mod.f7) if sys.version_info[:2] <= (3, 10): assert stringify_signature(sig, unqualified_typehints=True) == '(x: int | None = None, y: dict = {}) -> None' else: assert stringify_signature(sig, unqualified_typehints=True) == '(x: int = None, y: dict = {}) -> None' # case: separator at head - sig = inspect.signature(f22) + sig = inspect.signature(mod.f22) assert stringify_signature(sig) == '(*, a, b)' # case: separator in the middle - sig = inspect.signature(f23) + sig = inspect.signature(mod.f23) assert stringify_signature(sig) == '(a, b, /, c, d)' - sig = inspect.signature(f24) + sig = inspect.signature(mod.f24) assert stringify_signature(sig) == '(a, /, *, b)' # case: separator at tail - sig = inspect.signature(f25) + sig = inspect.signature(mod.f25) assert stringify_signature(sig) == '(a, b, /)' @@ -667,6 +640,17 @@ def test_object_description_enum(): assert inspect.object_description(MyEnum.FOO) == "MyEnum.FOO" +def test_object_description_enum_custom_repr(): + class MyEnum(enum.Enum): + FOO = 1 + BAR = 2 + + def __repr__(self): + return self.name + + assert inspect.object_description(MyEnum.FOO) == "FOO" + + def test_getslots(): class Foo: pass @@ -842,7 +826,7 @@ def test_getdoc_inherited_decorated_method(): """ class Bar(Foo): - @functools.lru_cache # noqa: B019 + @functools.lru_cache # NoQA: B019 def meth(self): # inherited and decorated method pass diff --git a/tests/test_util_inventory.py b/tests/test_util/test_util_inventory.py index 2c20763..81d31b0 100644 --- a/tests/test_util_inventory.py +++ b/tests/test_util/test_util_inventory.py @@ -1,59 +1,21 @@ """Test inventory util functions.""" import os import posixpath -import zlib from io import BytesIO +import sphinx.locale from sphinx.testing.util import SphinxTestApp from sphinx.util.inventory import InventoryFile -inventory_v1 = b'''\ -# Sphinx inventory version 1 -# Project: foo -# Version: 1.0 -module mod foo.html -module.cls class foo.html -''' - -inventory_v2 = b'''\ -# Sphinx inventory version 2 -# Project: foo -# Version: 2.0 -# The remainder of this file is compressed with zlib. -''' + zlib.compress(b'''\ -module1 py:module 0 foo.html#module-module1 Long Module desc -module2 py:module 0 foo.html#module-$ - -module1.func py:function 1 sub/foo.html#$ - -module1.Foo.bar py:method 1 index.html#foo.Bar.baz - -CFunc c:function 2 cfunc.html#CFunc - -std cpp:type 1 index.html#std - -std::uint8_t cpp:type 1 index.html#std_uint8_t - -foo::Bar cpp:class 1 index.html#cpp_foo_bar - -foo::Bar::baz cpp:function 1 index.html#cpp_foo_bar_baz - -foons cpp:type 1 index.html#foons - -foons::bartype cpp:type 1 index.html#foons_bartype - -a term std:term -1 glossary.html#term-a-term - -ls.-l std:cmdoption 1 index.html#cmdoption-ls-l - -docname std:doc -1 docname.html - -foo js:module 1 index.html#foo - -foo.bar js:class 1 index.html#foo.bar - -foo.bar.baz js:method 1 index.html#foo.bar.baz - -foo.bar.qux js:data 1 index.html#foo.bar.qux - -a term including:colon std:term -1 glossary.html#term-a-term-including-colon - -''') - -inventory_v2_not_having_version = b'''\ -# Sphinx inventory version 2 -# Project: foo -# Version: -# The remainder of this file is compressed with zlib. -''' + zlib.compress(b'''\ -module1 py:module 0 foo.html#module-module1 Long Module desc -''') +from tests.test_util.intersphinx_data import ( + INVENTORY_V1, + INVENTORY_V2, + INVENTORY_V2_NO_VERSION, +) def test_read_inventory_v1(): - f = BytesIO(inventory_v1) + f = BytesIO(INVENTORY_V1) invdata = InventoryFile.load(f, '/util', posixpath.join) assert invdata['py:module']['module'] == \ ('foo', '1.0', '/util/foo.html#module-module', '-') @@ -62,7 +24,7 @@ def test_read_inventory_v1(): def test_read_inventory_v2(): - f = BytesIO(inventory_v2) + f = BytesIO(INVENTORY_V2) invdata = InventoryFile.load(f, '/util', posixpath.join) assert len(invdata['py:module']) == 2 @@ -80,7 +42,7 @@ def test_read_inventory_v2(): def test_read_inventory_v2_not_having_version(): - f = BytesIO(inventory_v2_not_having_version) + f = BytesIO(INVENTORY_V2_NO_VERSION) invdata = InventoryFile.load(f, '/util', posixpath.join) assert invdata['py:module']['module1'] == \ ('foo', '', '/util/foo.html#module-module1', 'Long Module desc') @@ -99,8 +61,8 @@ def _write_appconfig(dir, language, prefix=None): def _build_inventory(srcdir): app = SphinxTestApp(srcdir=srcdir) app.build() - app.cleanup() - return (app.outdir / 'objects.inv') + sphinx.locale.translators.clear() + return app.outdir / 'objects.inv' def test_inventory_localization(tmp_path): diff --git a/tests/test_util_logging.py b/tests/test_util/test_util_logging.py index 4d506a8..4ee548a 100644 --- a/tests/test_util_logging.py +++ b/tests/test_util/test_util_logging.py @@ -8,9 +8,8 @@ import pytest from docutils import nodes from sphinx.errors import SphinxWarning -from sphinx.testing.util import strip_escseq from sphinx.util import logging, osutil -from sphinx.util.console import colorize +from sphinx.util.console import colorize, strip_colors from sphinx.util.logging import is_suppressed_warning, prefixed_warnings from sphinx.util.parallel import ParallelTasks @@ -110,7 +109,7 @@ def test_once_warning_log(app, status, warning): logger.warning('message: %d', 1, once=True) logger.warning('message: %d', 2, once=True) - assert 'WARNING: message: 1\nWARNING: message: 2\n' in strip_escseq(warning.getvalue()) + assert 'WARNING: message: 1\nWARNING: message: 2\n' in strip_colors(warning.getvalue()) def test_is_suppressed_warning(): @@ -278,7 +277,7 @@ def test_pending_warnings(app, status, warning): assert 'WARNING: message3' not in warning.getvalue() # actually logged as ordered - assert 'WARNING: message2\nWARNING: message3' in strip_escseq(warning.getvalue()) + assert 'WARNING: message2\nWARNING: message3' in strip_colors(warning.getvalue()) def test_colored_logs(app, status, warning): @@ -396,3 +395,20 @@ def test_get_node_location_abspath(): location = logging.get_node_location(n) assert location == absolute_filename + ':' + + +@pytest.mark.sphinx(confoverrides={'show_warning_types': True}) +def test_show_warning_types(app, status, warning): + logging.setup(app, status, warning) + logger = logging.getLogger(__name__) + logger.warning('message2') + logger.warning('message3', type='test') + logger.warning('message4', type='test', subtype='logging') + + warnings = strip_colors(warning.getvalue()).splitlines() + + assert warnings == [ + 'WARNING: message2', + 'WARNING: message3 [test]', + 'WARNING: message4 [test.logging]', + ] diff --git a/tests/test_util_matching.py b/tests/test_util/test_util_matching.py index 7d865ba..7d865ba 100644 --- a/tests/test_util_matching.py +++ b/tests/test_util/test_util_matching.py diff --git a/tests/test_util_nodes.py b/tests/test_util/test_util_nodes.py index 92e4dc1..ddd5974 100644 --- a/tests/test_util_nodes.py +++ b/tests/test_util/test_util_nodes.py @@ -237,8 +237,8 @@ def test_split_explicit_target(title, expected): def test_apply_source_workaround_literal_block_no_source(): """Regression test for #11091. - Test that apply_source_workaround doesn't raise. - """ + Test that apply_source_workaround doesn't raise. + """ literal_block = nodes.literal_block('', '') list_item = nodes.list_item('', literal_block) bullet_list = nodes.bullet_list('', list_item) diff --git a/tests/test_util_rst.py b/tests/test_util/test_util_rst.py index d50c90c..d50c90c 100644 --- a/tests/test_util_rst.py +++ b/tests/test_util/test_util_rst.py diff --git a/tests/test_util_template.py b/tests/test_util/test_util_template.py index 4601179..4601179 100644 --- a/tests/test_util_template.py +++ b/tests/test_util/test_util_template.py diff --git a/tests/test_util_typing.py b/tests/test_util/test_util_typing.py index d79852e..9c28029 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util/test_util_typing.py @@ -1,15 +1,38 @@ """Tests util.typing functions.""" import sys +from contextvars import Context, ContextVar, Token from enum import Enum from numbers import Integral from struct import Struct -from types import TracebackType +from types import ( + AsyncGeneratorType, + BuiltinFunctionType, + BuiltinMethodType, + CellType, + ClassMethodDescriptorType, + CodeType, + CoroutineType, + FrameType, + FunctionType, + GeneratorType, + GetSetDescriptorType, + LambdaType, + MappingProxyType, + MemberDescriptorType, + MethodDescriptorType, + MethodType, + MethodWrapperType, + ModuleType, + TracebackType, + WrapperDescriptorType, +) from typing import ( Any, Callable, Dict, Generator, + Iterator, List, NewType, Optional, @@ -21,7 +44,7 @@ from typing import ( import pytest from sphinx.ext.autodoc import mock -from sphinx.util.typing import INVALID_BUILTIN_CLASSES, restify, stringify_annotation +from sphinx.util.typing import _INVALID_BUILTIN_CLASSES, restify, stringify_annotation class MyClass1: @@ -76,11 +99,55 @@ def test_restify(): def test_is_invalid_builtin_class(): # if these tests start failing, it means that the __module__ - # of one of these classes has changed, and INVALID_BUILTIN_CLASSES + # of one of these classes has changed, and _INVALID_BUILTIN_CLASSES # in sphinx.util.typing needs to be updated. - assert INVALID_BUILTIN_CLASSES.keys() == {Struct, TracebackType} + assert _INVALID_BUILTIN_CLASSES.keys() == { + Context, + ContextVar, + Token, + Struct, + AsyncGeneratorType, + BuiltinFunctionType, + BuiltinMethodType, + CellType, + ClassMethodDescriptorType, + CodeType, + CoroutineType, + FrameType, + FunctionType, + GeneratorType, + GetSetDescriptorType, + LambdaType, + MappingProxyType, + MemberDescriptorType, + MethodDescriptorType, + MethodType, + MethodWrapperType, + ModuleType, + TracebackType, + WrapperDescriptorType, + } assert Struct.__module__ == '_struct' + assert AsyncGeneratorType.__module__ == 'builtins' + assert BuiltinFunctionType.__module__ == 'builtins' + assert BuiltinMethodType.__module__ == 'builtins' + assert CellType.__module__ == 'builtins' + assert ClassMethodDescriptorType.__module__ == 'builtins' + assert CodeType.__module__ == 'builtins' + assert CoroutineType.__module__ == 'builtins' + assert FrameType.__module__ == 'builtins' + assert FunctionType.__module__ == 'builtins' + assert GeneratorType.__module__ == 'builtins' + assert GetSetDescriptorType.__module__ == 'builtins' + assert LambdaType.__module__ == 'builtins' + assert MappingProxyType.__module__ == 'builtins' + assert MemberDescriptorType.__module__ == 'builtins' + assert MethodDescriptorType.__module__ == 'builtins' + assert MethodType.__module__ == 'builtins' + assert MethodWrapperType.__module__ == 'builtins' + assert ModuleType.__module__ == 'builtins' assert TracebackType.__module__ == 'builtins' + assert WrapperDescriptorType.__module__ == 'builtins' def test_restify_type_hints_containers(): @@ -103,12 +170,14 @@ def test_restify_type_hints_containers(): assert restify(List[Dict[str, Tuple]]) == (":py:class:`~typing.List`\\ " "[:py:class:`~typing.Dict`\\ " "[:py:class:`str`, :py:class:`~typing.Tuple`]]") - assert restify(MyList[Tuple[int, int]]) == (":py:class:`tests.test_util_typing.MyList`\\ " + assert restify(MyList[Tuple[int, int]]) == (":py:class:`tests.test_util.test_util_typing.MyList`\\ " "[:py:class:`~typing.Tuple`\\ " "[:py:class:`int`, :py:class:`int`]]") assert restify(Generator[None, None, None]) == (":py:class:`~typing.Generator`\\ " "[:py:obj:`None`, :py:obj:`None`, " ":py:obj:`None`]") + assert restify(Iterator[None]) == (":py:class:`~typing.Iterator`\\ " + "[:py:obj:`None`]") def test_restify_type_hints_Callable(): @@ -121,24 +190,44 @@ def test_restify_type_hints_Callable(): def test_restify_type_hints_Union(): - assert restify(Optional[int]) == ":py:obj:`~typing.Optional`\\ [:py:class:`int`]" - assert restify(Union[str, None]) == ":py:obj:`~typing.Optional`\\ [:py:class:`str`]" - assert restify(Union[int, str]) == (":py:obj:`~typing.Union`\\ " - "[:py:class:`int`, :py:class:`str`]") - assert restify(Union[int, Integral]) == (":py:obj:`~typing.Union`\\ " - "[:py:class:`int`, :py:class:`numbers.Integral`]") - assert restify(Union[int, Integral], "smart") == (":py:obj:`~typing.Union`\\ " - "[:py:class:`int`," - " :py:class:`~numbers.Integral`]") + assert restify(Union[int]) == ":py:class:`int`" + assert restify(Union[int, str]) == ":py:class:`int` | :py:class:`str`" + assert restify(Optional[int]) == ":py:class:`int` | :py:obj:`None`" + + assert restify(Union[str, None]) == ":py:class:`str` | :py:obj:`None`" + assert restify(Union[None, str]) == ":py:obj:`None` | :py:class:`str`" + assert restify(Optional[str]) == ":py:class:`str` | :py:obj:`None`" + + assert restify(Union[int, str, None]) == ( + ":py:class:`int` | :py:class:`str` | :py:obj:`None`" + ) + assert restify(Optional[Union[int, str]]) in { + ":py:class:`str` | :py:class:`int` | :py:obj:`None`", + ":py:class:`int` | :py:class:`str` | :py:obj:`None`", + } + + assert restify(Union[int, Integral]) == ( + ":py:class:`int` | :py:class:`numbers.Integral`" + ) + assert restify(Union[int, Integral], "smart") == ( + ":py:class:`int` | :py:class:`~numbers.Integral`" + ) assert (restify(Union[MyClass1, MyClass2]) == - (":py:obj:`~typing.Union`\\ " - "[:py:class:`tests.test_util_typing.MyClass1`, " - ":py:class:`tests.test_util_typing.<MyClass2>`]")) + (":py:class:`tests.test_util.test_util_typing.MyClass1`" + " | :py:class:`tests.test_util.test_util_typing.<MyClass2>`")) assert (restify(Union[MyClass1, MyClass2], "smart") == - (":py:obj:`~typing.Union`\\ " - "[:py:class:`~tests.test_util_typing.MyClass1`," - " :py:class:`~tests.test_util_typing.<MyClass2>`]")) + (":py:class:`~tests.test_util.test_util_typing.MyClass1`" + " | :py:class:`~tests.test_util.test_util_typing.<MyClass2>`")) + + assert (restify(Optional[Union[MyClass1, MyClass2]]) == + (":py:class:`tests.test_util.test_util_typing.MyClass1`" + " | :py:class:`tests.test_util.test_util_typing.<MyClass2>`" + " | :py:obj:`None`")) + assert (restify(Optional[Union[MyClass1, MyClass2]], "smart") == + (":py:class:`~tests.test_util.test_util_typing.MyClass1`" + " | :py:class:`~tests.test_util.test_util_typing.<MyClass2>`" + " | :py:obj:`None`")) def test_restify_type_hints_typevars(): @@ -146,35 +235,35 @@ def test_restify_type_hints_typevars(): T_co = TypeVar('T_co', covariant=True) T_contra = TypeVar('T_contra', contravariant=True) - assert restify(T) == ":py:obj:`tests.test_util_typing.T`" - assert restify(T, "smart") == ":py:obj:`~tests.test_util_typing.T`" + assert restify(T) == ":py:obj:`tests.test_util.test_util_typing.T`" + assert restify(T, "smart") == ":py:obj:`~tests.test_util.test_util_typing.T`" - assert restify(T_co) == ":py:obj:`tests.test_util_typing.T_co`" - assert restify(T_co, "smart") == ":py:obj:`~tests.test_util_typing.T_co`" + assert restify(T_co) == ":py:obj:`tests.test_util.test_util_typing.T_co`" + assert restify(T_co, "smart") == ":py:obj:`~tests.test_util.test_util_typing.T_co`" - assert restify(T_contra) == ":py:obj:`tests.test_util_typing.T_contra`" - assert restify(T_contra, "smart") == ":py:obj:`~tests.test_util_typing.T_contra`" + assert restify(T_contra) == ":py:obj:`tests.test_util.test_util_typing.T_contra`" + assert restify(T_contra, "smart") == ":py:obj:`~tests.test_util.test_util_typing.T_contra`" - assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util_typing.T`]" - assert restify(List[T], "smart") == ":py:class:`~typing.List`\\ [:py:obj:`~tests.test_util_typing.T`]" + assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util.test_util_typing.T`]" + assert restify(List[T], "smart") == ":py:class:`~typing.List`\\ [:py:obj:`~tests.test_util.test_util_typing.T`]" - assert restify(list[T]) == ":py:class:`list`\\ [:py:obj:`tests.test_util_typing.T`]" - assert restify(list[T], "smart") == ":py:class:`list`\\ [:py:obj:`~tests.test_util_typing.T`]" + assert restify(list[T]) == ":py:class:`list`\\ [:py:obj:`tests.test_util.test_util_typing.T`]" + assert restify(list[T], "smart") == ":py:class:`list`\\ [:py:obj:`~tests.test_util.test_util_typing.T`]" if sys.version_info[:2] >= (3, 10): - assert restify(MyInt) == ":py:class:`tests.test_util_typing.MyInt`" - assert restify(MyInt, "smart") == ":py:class:`~tests.test_util_typing.MyInt`" + assert restify(MyInt) == ":py:class:`tests.test_util.test_util_typing.MyInt`" + assert restify(MyInt, "smart") == ":py:class:`~tests.test_util.test_util_typing.MyInt`" else: assert restify(MyInt) == ":py:class:`MyInt`" assert restify(MyInt, "smart") == ":py:class:`MyInt`" def test_restify_type_hints_custom_class(): - assert restify(MyClass1) == ":py:class:`tests.test_util_typing.MyClass1`" - assert restify(MyClass1, "smart") == ":py:class:`~tests.test_util_typing.MyClass1`" + assert restify(MyClass1) == ":py:class:`tests.test_util.test_util_typing.MyClass1`" + assert restify(MyClass1, "smart") == ":py:class:`~tests.test_util.test_util_typing.MyClass1`" - assert restify(MyClass2) == ":py:class:`tests.test_util_typing.<MyClass2>`" - assert restify(MyClass2, "smart") == ":py:class:`~tests.test_util_typing.<MyClass2>`" + assert restify(MyClass2) == ":py:class:`tests.test_util.test_util_typing.<MyClass2>`" + assert restify(MyClass2, "smart") == ":py:class:`~tests.test_util.test_util_typing.<MyClass2>`" def test_restify_type_hints_alias(): @@ -199,8 +288,8 @@ def test_restify_type_Literal(): from typing import Literal # type: ignore[attr-defined] assert restify(Literal[1, "2", "\r"]) == ":py:obj:`~typing.Literal`\\ [1, '2', '\\r']" - assert restify(Literal[MyEnum.a], 'fully-qualified-except-typing') == ':py:obj:`~typing.Literal`\\ [:py:attr:`tests.test_util_typing.MyEnum.a`]' - assert restify(Literal[MyEnum.a], 'smart') == ':py:obj:`~typing.Literal`\\ [:py:attr:`~tests.test_util_typing.MyEnum.a`]' + assert restify(Literal[MyEnum.a], 'fully-qualified-except-typing') == ':py:obj:`~typing.Literal`\\ [:py:attr:`tests.test_util.test_util_typing.MyEnum.a`]' + assert restify(Literal[MyEnum.a], 'smart') == ':py:obj:`~typing.Literal`\\ [:py:attr:`~tests.test_util.test_util_typing.MyEnum.a`]' def test_restify_pep_585(): @@ -223,7 +312,7 @@ def test_restify_pep_585(): "[:py:class:`str`, :py:class:`~typing.Tuple`\\ " "[:py:class:`str`, ...]]]") assert restify(tuple[MyList[list[int]], int]) == (":py:class:`tuple`\\ [" - ":py:class:`tests.test_util_typing.MyList`\\ " + ":py:class:`tests.test_util.test_util_typing.MyList`\\ " "[:py:class:`list`\\ [:py:class:`int`]], " ":py:class:`int`]") @@ -231,14 +320,15 @@ def test_restify_pep_585(): @pytest.mark.skipif(sys.version_info[:2] <= (3, 9), reason='python 3.10+ is required.') def test_restify_type_union_operator(): assert restify(int | None) == ":py:class:`int` | :py:obj:`None`" # type: ignore[attr-defined] + assert restify(None | int) == ":py:obj:`None` | :py:class:`int`" # type: ignore[attr-defined] assert restify(int | str) == ":py:class:`int` | :py:class:`str`" # type: ignore[attr-defined] assert restify(int | str | None) == (":py:class:`int` | :py:class:`str` | " # type: ignore[attr-defined] ":py:obj:`None`") def test_restify_broken_type_hints(): - assert restify(BrokenType) == ':py:class:`tests.test_util_typing.BrokenType`' - assert restify(BrokenType, "smart") == ':py:class:`~tests.test_util_typing.BrokenType`' + assert restify(BrokenType) == ':py:class:`tests.test_util.test_util_typing.BrokenType`' + assert restify(BrokenType, "smart") == ':py:class:`~tests.test_util.test_util_typing.BrokenType`' def test_restify_mock(): @@ -315,14 +405,18 @@ def test_stringify_type_hints_containers(): assert stringify_annotation(List[Dict[str, Tuple]], "fully-qualified") == "typing.List[typing.Dict[str, typing.Tuple]]" assert stringify_annotation(List[Dict[str, Tuple]], "smart") == "~typing.List[~typing.Dict[str, ~typing.Tuple]]" - assert stringify_annotation(MyList[Tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util_typing.MyList[Tuple[int, int]]" - assert stringify_annotation(MyList[Tuple[int, int]], "fully-qualified") == "tests.test_util_typing.MyList[typing.Tuple[int, int]]" - assert stringify_annotation(MyList[Tuple[int, int]], "smart") == "~tests.test_util_typing.MyList[~typing.Tuple[int, int]]" + assert stringify_annotation(MyList[Tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyList[Tuple[int, int]]" + assert stringify_annotation(MyList[Tuple[int, int]], "fully-qualified") == "tests.test_util.test_util_typing.MyList[typing.Tuple[int, int]]" + assert stringify_annotation(MyList[Tuple[int, int]], "smart") == "~tests.test_util.test_util_typing.MyList[~typing.Tuple[int, int]]" assert stringify_annotation(Generator[None, None, None], 'fully-qualified-except-typing') == "Generator[None, None, None]" assert stringify_annotation(Generator[None, None, None], "fully-qualified") == "typing.Generator[None, None, None]" assert stringify_annotation(Generator[None, None, None], "smart") == "~typing.Generator[None, None, None]" + assert stringify_annotation(Iterator[None], 'fully-qualified-except-typing') == "Iterator[None]" + assert stringify_annotation(Iterator[None], "fully-qualified") == "typing.Iterator[None]" + assert stringify_annotation(Iterator[None], "smart") == "~typing.Iterator[None]" + def test_stringify_type_hints_pep_585(): assert stringify_annotation(list[int], 'fully-qualified-except-typing') == "list[int]" @@ -346,9 +440,9 @@ def test_stringify_type_hints_pep_585(): assert stringify_annotation(list[dict[str, tuple]], 'fully-qualified-except-typing') == "list[dict[str, tuple]]" assert stringify_annotation(list[dict[str, tuple]], "smart") == "list[dict[str, tuple]]" - assert stringify_annotation(MyList[tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util_typing.MyList[tuple[int, int]]" - assert stringify_annotation(MyList[tuple[int, int]], "fully-qualified") == "tests.test_util_typing.MyList[tuple[int, int]]" - assert stringify_annotation(MyList[tuple[int, int]], "smart") == "~tests.test_util_typing.MyList[tuple[int, int]]" + assert stringify_annotation(MyList[tuple[int, int]], 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyList[tuple[int, int]]" + assert stringify_annotation(MyList[tuple[int, int]], "fully-qualified") == "tests.test_util.test_util_typing.MyList[tuple[int, int]]" + assert stringify_annotation(MyList[tuple[int, int]], "smart") == "~tests.test_util.test_util_typing.MyList[tuple[int, int]]" assert stringify_annotation(type[int], 'fully-qualified-except-typing') == "type[int]" assert stringify_annotation(type[int], "smart") == "type[int]" @@ -413,9 +507,12 @@ def test_stringify_type_hints_Union(): assert stringify_annotation(Optional[int], "fully-qualified") == "int | None" assert stringify_annotation(Optional[int], "smart") == "int | None" - assert stringify_annotation(Union[str, None], 'fully-qualified-except-typing') == "str | None" - assert stringify_annotation(Union[str, None], "fully-qualified") == "str | None" - assert stringify_annotation(Union[str, None], "smart") == "str | None" + assert stringify_annotation(Union[int, None], 'fully-qualified-except-typing') == "int | None" + assert stringify_annotation(Union[None, int], 'fully-qualified-except-typing') == "None | int" + assert stringify_annotation(Union[int, None], "fully-qualified") == "int | None" + assert stringify_annotation(Union[None, int], "fully-qualified") == "None | int" + assert stringify_annotation(Union[int, None], "smart") == "int | None" + assert stringify_annotation(Union[None, int], "smart") == "None | int" assert stringify_annotation(Union[int, str], 'fully-qualified-except-typing') == "int | str" assert stringify_annotation(Union[int, str], "fully-qualified") == "int | str" @@ -426,11 +523,11 @@ def test_stringify_type_hints_Union(): assert stringify_annotation(Union[int, Integral], "smart") == "int | ~numbers.Integral" assert (stringify_annotation(Union[MyClass1, MyClass2], 'fully-qualified-except-typing') == - "tests.test_util_typing.MyClass1 | tests.test_util_typing.<MyClass2>") + "tests.test_util.test_util_typing.MyClass1 | tests.test_util.test_util_typing.<MyClass2>") assert (stringify_annotation(Union[MyClass1, MyClass2], "fully-qualified") == - "tests.test_util_typing.MyClass1 | tests.test_util_typing.<MyClass2>") + "tests.test_util.test_util_typing.MyClass1 | tests.test_util.test_util_typing.<MyClass2>") assert (stringify_annotation(Union[MyClass1, MyClass2], "smart") == - "~tests.test_util_typing.MyClass1 | ~tests.test_util_typing.<MyClass2>") + "~tests.test_util.test_util_typing.MyClass1 | ~tests.test_util.test_util_typing.<MyClass2>") def test_stringify_type_hints_typevars(): @@ -438,35 +535,35 @@ def test_stringify_type_hints_typevars(): T_co = TypeVar('T_co', covariant=True) T_contra = TypeVar('T_contra', contravariant=True) - assert stringify_annotation(T, 'fully-qualified-except-typing') == "tests.test_util_typing.T" - assert stringify_annotation(T, "smart") == "~tests.test_util_typing.T" + assert stringify_annotation(T, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.T" + assert stringify_annotation(T, "smart") == "~tests.test_util.test_util_typing.T" - assert stringify_annotation(T_co, 'fully-qualified-except-typing') == "tests.test_util_typing.T_co" - assert stringify_annotation(T_co, "smart") == "~tests.test_util_typing.T_co" + assert stringify_annotation(T_co, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.T_co" + assert stringify_annotation(T_co, "smart") == "~tests.test_util.test_util_typing.T_co" - assert stringify_annotation(T_contra, 'fully-qualified-except-typing') == "tests.test_util_typing.T_contra" - assert stringify_annotation(T_contra, "smart") == "~tests.test_util_typing.T_contra" + assert stringify_annotation(T_contra, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.T_contra" + assert stringify_annotation(T_contra, "smart") == "~tests.test_util.test_util_typing.T_contra" - assert stringify_annotation(List[T], 'fully-qualified-except-typing') == "List[tests.test_util_typing.T]" - assert stringify_annotation(List[T], "smart") == "~typing.List[~tests.test_util_typing.T]" + assert stringify_annotation(List[T], 'fully-qualified-except-typing') == "List[tests.test_util.test_util_typing.T]" + assert stringify_annotation(List[T], "smart") == "~typing.List[~tests.test_util.test_util_typing.T]" - assert stringify_annotation(list[T], 'fully-qualified-except-typing') == "list[tests.test_util_typing.T]" - assert stringify_annotation(list[T], "smart") == "list[~tests.test_util_typing.T]" + assert stringify_annotation(list[T], 'fully-qualified-except-typing') == "list[tests.test_util.test_util_typing.T]" + assert stringify_annotation(list[T], "smart") == "list[~tests.test_util.test_util_typing.T]" if sys.version_info[:2] >= (3, 10): - assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "tests.test_util_typing.MyInt" - assert stringify_annotation(MyInt, "smart") == "~tests.test_util_typing.MyInt" + assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyInt" + assert stringify_annotation(MyInt, "smart") == "~tests.test_util.test_util_typing.MyInt" else: assert stringify_annotation(MyInt, 'fully-qualified-except-typing') == "MyInt" assert stringify_annotation(MyInt, "smart") == "MyInt" def test_stringify_type_hints_custom_class(): - assert stringify_annotation(MyClass1, 'fully-qualified-except-typing') == "tests.test_util_typing.MyClass1" - assert stringify_annotation(MyClass1, "smart") == "~tests.test_util_typing.MyClass1" + assert stringify_annotation(MyClass1, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.MyClass1" + assert stringify_annotation(MyClass1, "smart") == "~tests.test_util.test_util_typing.MyClass1" - assert stringify_annotation(MyClass2, 'fully-qualified-except-typing') == "tests.test_util_typing.<MyClass2>" - assert stringify_annotation(MyClass2, "smart") == "~tests.test_util_typing.<MyClass2>" + assert stringify_annotation(MyClass2, 'fully-qualified-except-typing') == "tests.test_util.test_util_typing.<MyClass2>" + assert stringify_annotation(MyClass2, "smart") == "~tests.test_util.test_util_typing.<MyClass2>" def test_stringify_type_hints_alias(): @@ -486,8 +583,8 @@ def test_stringify_type_Literal(): assert stringify_annotation(Literal[1, "2", "\r"], "fully-qualified") == "typing.Literal[1, '2', '\\r']" assert stringify_annotation(Literal[1, "2", "\r"], "smart") == "~typing.Literal[1, '2', '\\r']" - assert stringify_annotation(Literal[MyEnum.a], 'fully-qualified-except-typing') == 'Literal[tests.test_util_typing.MyEnum.a]' - assert stringify_annotation(Literal[MyEnum.a], 'fully-qualified') == 'typing.Literal[tests.test_util_typing.MyEnum.a]' + assert stringify_annotation(Literal[MyEnum.a], 'fully-qualified-except-typing') == 'Literal[tests.test_util.test_util_typing.MyEnum.a]' + assert stringify_annotation(Literal[MyEnum.a], 'fully-qualified') == 'typing.Literal[tests.test_util.test_util_typing.MyEnum.a]' assert stringify_annotation(Literal[MyEnum.a], 'smart') == '~typing.Literal[MyEnum.a]' @@ -510,8 +607,8 @@ def test_stringify_type_union_operator(): def test_stringify_broken_type_hints(): - assert stringify_annotation(BrokenType, 'fully-qualified-except-typing') == 'tests.test_util_typing.BrokenType' - assert stringify_annotation(BrokenType, "smart") == '~tests.test_util_typing.BrokenType' + assert stringify_annotation(BrokenType, 'fully-qualified-except-typing') == 'tests.test_util.test_util_typing.BrokenType' + assert stringify_annotation(BrokenType, "smart") == '~tests.test_util.test_util_typing.BrokenType' def test_stringify_mock(): diff --git a/tests/typing_test_data.py b/tests/test_util/typing_test_data.py index 8a7ebc4..e29b600 100644 --- a/tests/typing_test_data.py +++ b/tests/test_util/typing_test_data.py @@ -39,7 +39,7 @@ def f6(x: int, *args, y: str, z: str) -> None: pass -def f7(x: int = None, y: dict = {}) -> None: # NoQA: B006 +def f7(x: int = None, y: dict = {}) -> None: # NoQA: B006,RUF013 pass @@ -77,7 +77,7 @@ def f14() -> Any: pass -def f15(x: "Unknown", y: "int") -> Any: # noqa: F821 # type: ignore[attr-defined] +def f15(x: "Unknown", y: "int") -> Any: # NoQA: F821 # type: ignore[attr-defined] pass diff --git a/tests/test_writers/__init__.py b/tests/test_writers/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/test_writers/__init__.py diff --git a/tests/test_api_translator.py b/tests/test_writers/test_api_translator.py index 9f2bd44..9f2bd44 100644 --- a/tests/test_api_translator.py +++ b/tests/test_writers/test_api_translator.py diff --git a/tests/test_docutilsconf.py b/tests/test_writers/test_docutilsconf.py index def6cb6..6422c30 100644 --- a/tests/test_docutilsconf.py +++ b/tests/test_writers/test_docutilsconf.py @@ -18,8 +18,8 @@ def test_html_with_default_docutilsconf(app, status, warning): @pytest.mark.sphinx('dummy', testroot='docutilsconf', freshenv=True, - docutilsconf=('[restructuredtext parser]\n' - 'trim_footnote_reference_space: true\n')) + docutils_conf=('[restructuredtext parser]\n' + 'trim_footnote_reference_space: true\n')) def test_html_with_docutilsconf(app, status, warning): with patch_docutils(app.confdir): app.build() diff --git a/tests/test_writer_latex.py b/tests/test_writers/test_writer_latex.py index a0ab3ee..a0ab3ee 100644 --- a/tests/test_writer_latex.py +++ b/tests/test_writers/test_writer_latex.py diff --git a/tests/utils.py b/tests/utils.py index 32636b7..5636a13 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,55 +1,125 @@ -import contextlib -import http.server -import pathlib -import threading +from __future__ import annotations + +__all__ = ('http_server',) + +import socket +from contextlib import contextmanager +from http.server import ThreadingHTTPServer +from pathlib import Path from ssl import PROTOCOL_TLS_SERVER, SSLContext +from threading import Thread +from typing import TYPE_CHECKING +from urllib.parse import urlparse + +if TYPE_CHECKING: + from collections.abc import Iterator + from http.server import HTTPServer + from socketserver import BaseRequestHandler + from typing import Final -import filelock + from sphinx.application import Sphinx # Generated with: # $ openssl req -new -x509 -days 3650 -nodes -out cert.pem \ # -keyout cert.pem -addext "subjectAltName = DNS:localhost" -TESTS_ROOT = pathlib.Path(__file__).parent -CERT_FILE = str(TESTS_ROOT / "certs" / "cert.pem") +TESTS_ROOT: Final[Path] = Path(__file__).parent +CERT_FILE: Final[str] = str(TESTS_ROOT / 'certs' / 'cert.pem') -# File lock for tests -LOCK_PATH = str(TESTS_ROOT / 'test-server.lock') +class HttpServerThread(Thread): + def __init__(self, handler: type[BaseRequestHandler], *, port: int = 0) -> None: + """ + Constructs a threaded HTTP server. The default port number of ``0`` + delegates selection of a port number to bind to to Python. -class HttpServerThread(threading.Thread): - def __init__(self, handler, *args, **kwargs): - super().__init__(*args, **kwargs) - self.server = http.server.ThreadingHTTPServer(("localhost", 7777), handler) + Ref: https://docs.python.org/3.11/library/socketserver.html#asynchronous-mixins + """ + super().__init__(daemon=True) + self.server = ThreadingHTTPServer(('localhost', port), handler) - def run(self): + def run(self) -> None: self.server.serve_forever(poll_interval=0.001) - def terminate(self): + def terminate(self) -> None: self.server.shutdown() self.server.server_close() self.join() class HttpsServerThread(HttpServerThread): - def __init__(self, handler, *args, **kwargs): - super().__init__(handler, *args, **kwargs) + def __init__(self, handler: type[BaseRequestHandler], *, port: int = 0) -> None: + super().__init__(handler, port=port) sslcontext = SSLContext(PROTOCOL_TLS_SERVER) sslcontext.load_cert_chain(CERT_FILE) self.server.socket = sslcontext.wrap_socket(self.server.socket, server_side=True) -def create_server(thread_class): - def server(handler): - lock = filelock.FileLock(LOCK_PATH) - with lock: - server_thread = thread_class(handler, daemon=True) - server_thread.start() - try: - yield server_thread - finally: - server_thread.terminate() - return contextlib.contextmanager(server) +@contextmanager +def http_server( + handler: type[BaseRequestHandler], + *, + tls_enabled: bool = False, + port: int = 0, +) -> Iterator[HTTPServer]: + server_cls = HttpsServerThread if tls_enabled else HttpServerThread + server_thread = server_cls(handler, port=port) + server_thread.start() + server_port = server_thread.server.server_port + assert port == 0 or server_port == port + try: + socket.create_connection(('localhost', server_port), timeout=0.5).close() + yield server_thread.server # Connection has been confirmed possible; proceed. + finally: + server_thread.terminate() + + +@contextmanager +def rewrite_hyperlinks(app: Sphinx, server: HTTPServer) -> Iterator[None]: + """ + Rewrite hyperlinks that refer to network location 'localhost:7777', + allowing that location to vary dynamically with the arbitrary test HTTP + server port assigned during unit testing. + + :param app: The Sphinx application where link replacement is to occur. + :param server: Destination server to redirect the hyperlinks to. + """ + match_netloc, replacement_netloc = ( + 'localhost:7777', + f'localhost:{server.server_port}', + ) + + def rewrite_hyperlink(_app: Sphinx, uri: str) -> str | None: + parsed_uri = urlparse(uri) + if parsed_uri.netloc != match_netloc: + return uri + return parsed_uri._replace(netloc=replacement_netloc).geturl() + + listener_id = app.connect('linkcheck-process-uri', rewrite_hyperlink) + yield + app.disconnect(listener_id) + + +@contextmanager +def serve_application( + app: Sphinx, + handler: type[BaseRequestHandler], + *, + tls_enabled: bool = False, + port: int = 0, +) -> Iterator[str]: + """ + Prepare a temporary server to handle HTTP requests related to the links + found in a Sphinx application project. + :param app: The Sphinx application. + :param handler: Determines how each request will be handled. + :param tls_enabled: Whether TLS (SSL) should be enabled for the server. + :param port: Optional server port (default: auto). -http_server = create_server(HttpServerThread) -https_server = create_server(HttpsServerThread) + :return: The address of the temporary HTTP server. + """ + with ( + http_server(handler, tls_enabled=tls_enabled, port=port) as server, + rewrite_hyperlinks(app, server), + ): + yield f'localhost:{server.server_port}' |