diff --git a/.github/workflows/docutils-latest.yml b/.github/workflows/docutils-latest.yml new file mode 100644 index 00000000000..35d4c751e4b --- /dev/null +++ b/.github/workflows/docutils-latest.yml @@ -0,0 +1,25 @@ +name: Test with the HEAD of docutils + +on: + schedule: + - cron: "0 0 * * SUN" + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + - name: Check Python version + run: python --version + - name: Unpin docutils + run: sed -i -e "s/'docutils>=.*'/'docutils'/" setup.py + - name: Install graphviz + run: sudo apt-get install graphviz + - name: Install dependencies + run: pip install -U tox codecov + - name: Run Tox + run: tox -e du-latest -- -vv diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 62a3d11394f..d9a21f501c5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - name: [py36, py37, py38, py39] + name: [py36, py37, py38, py39, py310] include: - name: py36 python: 3.6 @@ -23,9 +23,12 @@ jobs: python: 3.9 docutils: du17 coverage: "--cov ./ --cov-append --cov-config setup.cfg" - - name: py310-dev - python: 3.10-dev + - name: py310 + python: "3.10" docutils: du17 + - name: py311-dev + python: 3.11-dev + docutils: py311 env: PYTEST_ADDOPTS: ${{ matrix.coverage }} @@ -47,6 +50,9 @@ jobs: run: sudo apt-get install graphviz - name: Install dependencies run: pip install -U tox codecov + - name: Install the latest py package (for py3.11-dev) + run: pip install -U git+https://github.com/pytest-dev/py + if: ${{ matrix.python == '3.11-dev' }} - name: Run Tox run: tox -e ${{ matrix.docutils }} -- -vv - name: codecov diff --git a/.github/workflows/transifex.yml b/.github/workflows/transifex.yml index 3efae8ff262..5a9f929a709 100644 --- a/.github/workflows/transifex.yml +++ b/.github/workflows/transifex.yml @@ -15,6 +15,8 @@ jobs: ref: 4.x - name: Set up Python uses: actions/setup-python@v2 + with: + python-version: 3.9 # https://github.com/transifex/transifex-client/pull/330 - name: Install dependencies run: pip install -U babel jinja2 transifex-client - name: Extract translations from source code @@ -33,6 +35,8 @@ jobs: ref: 4.x - name: Set up Python uses: actions/setup-python@v2 + with: + python-version: 3.9 # https://github.com/transifex/transifex-client/pull/330 - name: Install dependencies run: pip install -U babel jinja2 transifex-client - name: Extract translations from source code diff --git a/CHANGES b/CHANGES index 331f14d81ab..d30115e978c 100644 --- a/CHANGES +++ b/CHANGES @@ -4,21 +4,100 @@ Release 4.3.0 (in development) Dependencies ------------ +* Support Python 3.10 + Incompatible changes -------------------- +* #9649: ``searchindex.js``: the embedded data has changed format to allow + objects with the same name in different domains. +* #9672: The rendering of Python domain declarations is implemented + with more docutils nodes to allow better CSS styling. + It may break existing styling. +* #9672: the signature of + :py:meth:`domains.py.PyObject.get_signature_prefix` has changed to + return a list of nodes instead of a plain string. +* #9695: ``domains.js.JSObject.display_prefix`` has been changed into a method + ``get_display_prefix`` which now returns a list of nodes + instead of a plain string. +* #9695: The rendering of Javascript domain declarations is implemented + with more docutils nodes to allow better CSS styling. + It may break existing styling. +* #9450: mathjax: Load MathJax via "defer" strategy + + Deprecated ---------- +* ``sphinx.ext.autodoc.AttributeDocumenter._datadescriptor`` +* ``sphinx.writers.html.HTMLTranslator._fieldlist_row_index`` +* ``sphinx.writers.html.HTMLTranslator._table_row_index`` +* ``sphinx.writers.html5.HTML5Translator._fieldlist_row_index`` +* ``sphinx.writers.html5.HTML5Translator._table_row_index`` + Features added -------------- +* #9639: autodoc: Support asynchronous generator functions +* #9664: autodoc: ``autodoc-process-bases`` supports to inject reST snippet as a + base class +* #9691: C, added new info-field ``retval`` + for :rst:dir:`c:function` and :rst:dir:`c:macro`. +* C++, added new info-field ``retval`` for :rst:dir:`cpp:function`. +* #9672: More CSS classes on Python domain descriptions +* #9695: More CSS classes on Javascript domain descriptions +* #9683: Revert the removal of ``add_stylesheet()`` API. It will be kept until + the Sphinx-6.0 release +* #2068, add :confval:`intersphinx_disabled_reftypes` for disabling + interphinx resolution of cross-references that do not have an explicit + inventory specification. Specific types of cross-references can be disabled, + e.g., ``std:doc`` or all cross-references in a specific domain, + e.g., ``std:*``. * #9623: Allow to suppress "toctree contains reference to excluded document" warnings using :confval:`suppress_warnings` Bugs fixed ---------- +* #9630: autodoc: Failed to build cross references if :confval:`primary_domain` + is not 'py' +* #9644: autodoc: Crashed on getting source info from problematic object +* #9655: autodoc: mocked object having doc comment is warned unexpectedly +* #9651: autodoc: return type field is not generated even if + :confval:`autodoc_typehints_description_target` is set to "documented" when + its info-field-list contains ``:returns:`` field +* #9657: autodoc: The base class for a subclass of mocked object is incorrect +* #9607: autodoc: Incorrect base class detection for the subclasses of the + generic class +* #9755: autodoc: memory addresses are shown for aliases +* #9752: autodoc: Failed to detect type annotation for slots attribute +* #9756: autodoc: Crashed if classmethod does not have __func__ attribute +* #9757: autodoc: :confval:`autodoc_inherit_docstrings` does not effect to + overriden classmethods +* #9781: autodoc: :confval:`autodoc_preserve_defaults` does not support + hexadecimal numeric +* #9630: autosummary: Failed to build summary table if :confval:`primary_domain` + is not 'py' +* #9670: html: Fix download file with special characters +* #9710: html: Wrong styles for even/odd rows in nested tables +* #9763: html: parameter name and its type annotation are not separated in HTML +* #9649: HTML search: when objects have the same name but in different domains, + return all of them as result instead of just one. +* #7634: intersphinx: references on the file in sub directory are broken +* #9737: LaTeX: hlist is rendered as a list containing "aggedright" text +* #9678: linkcheck: file extension was shown twice in warnings +* #9697: py domain: An index entry with parens was registered for ``py:method`` + directive with ``:property:`` option +* #9775: py domain: Literal typehint was converted to a cross reference when + :confval:`autodoc_typehints='description'` +* #9708: needs_extension failed to check double-digit version correctly +* #9688: Fix :rst:dir:`code`` does not recognize ``:class:`` option +* #9733: Fix for logging handler flushing warnings in the middle of the docs + build +* #9656: Fix warnings without subtype being incorrectly suppressed +* Intersphinx, for unresolved references with an explicit inventory, + e.g., ``proj:myFunc``, leave the inventory prefix in the unresolved text. + Testing -------- @@ -113,6 +192,7 @@ Bugs fixed with the HEAD of 3.10 * #9436, #9471: autodoc: crashed if ``autodoc_class_signature = "separated"`` * #9456: html search: html_copy_source can't control the search summaries +* #9500: LaTeX: Failed to build Japanese document on Windows * #9435: linkcheck: Failed to check anchors in github.com Release 4.1.1 (released Jul 15, 2021) diff --git a/doc/_static/conf.py.txt b/doc/_static/conf.py.txt index 844451fd8a8..9078199b3e1 100644 --- a/doc/_static/conf.py.txt +++ b/doc/_static/conf.py.txt @@ -1,8 +1,8 @@ # test documentation build configuration file, created by # sphinx-quickstart on Sun Jun 26 00:00:43 2016. # -# This file is execfile()d with the current directory set to its -# containing dir. +# This file is executed through importlib.import_module with +# the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. diff --git a/doc/_static/tutorial/lumache-autosummary.png b/doc/_static/tutorial/lumache-autosummary.png new file mode 100644 index 00000000000..ed54ea3802d Binary files /dev/null and b/doc/_static/tutorial/lumache-autosummary.png differ diff --git a/doc/_static/tutorial/lumache-py-function-full.png b/doc/_static/tutorial/lumache-py-function-full.png new file mode 100644 index 00000000000..d13b6377666 Binary files /dev/null and b/doc/_static/tutorial/lumache-py-function-full.png differ diff --git a/doc/_static/tutorial/lumache-py-function.png b/doc/_static/tutorial/lumache-py-function.png new file mode 100644 index 00000000000..06129d5df1e Binary files /dev/null and b/doc/_static/tutorial/lumache-py-function.png differ diff --git a/doc/_templates/index.html b/doc/_templates/index.html index 6fa0c3eccad..3cb884ab77e 100644 --- a/doc/_templates/index.html +++ b/doc/_templates/index.html @@ -118,7 +118,7 @@
1\n' '2\n' '3\n' @@ -1586,8 +1594,7 @@ def test_html_codeblock_linenos_style_inline(app): app.build() content = (app.outdir / 'index.html').read_text() - pygments_version = tuple(LooseVersion(pygments.__version__).version) - if pygments_version > (2, 7): + if PYGMENTS_VERSION > (2, 7): assert '1' in content else: assert '1 ' in content diff --git a/tests/test_build_linkcheck.py b/tests/test_build_linkcheck.py index 2c6244b0a2a..fa7af13a446 100644 --- a/tests/test_build_linkcheck.py +++ b/tests/test_build_linkcheck.py @@ -305,7 +305,7 @@ def test_linkcheck_allowed_redirects(app, warning): assert result["http://localhost:7777/path1"] == "working" assert result["http://localhost:7777/path2"] == "redirected" - assert ("index.rst.rst:1: WARNING: redirect http://localhost:7777/path2 - with Found to " + assert ("index.rst:1: WARNING: redirect http://localhost:7777/path2 - with Found to " "http://localhost:7777/?redirected=1\n" in strip_escseq(warning.getvalue())) assert len(warning.getvalue().splitlines()) == 1 diff --git a/tests/test_domain_js.py b/tests/test_domain_js.py index 1fb865d4b8c..434513063d5 100644 --- a/tests/test_domain_js.py +++ b/tests/test_domain_js.py @@ -15,7 +15,8 @@ from sphinx import addnodes from sphinx.addnodes import (desc, desc_annotation, desc_content, desc_name, desc_parameter, - desc_parameterlist, desc_signature) + desc_parameterlist, desc_sig_keyword, desc_sig_name, + desc_sig_space, desc_signature) from sphinx.domains.javascript import JavaScriptDomain from sphinx.testing import restructuredtext from sphinx.testing.util import assert_node @@ -184,11 +185,11 @@ def test_js_function(app): text = ".. js:function:: sum(a, b)" doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_name, "sum"], + [desc, ([desc_signature, ([desc_name, ([desc_sig_name, "sum"])], desc_parameterlist)], [desc_content, ()])])) - assert_node(doctree[1][0][1], [desc_parameterlist, ([desc_parameter, "a"], - [desc_parameter, "b"])]) + assert_node(doctree[1][0][1], [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "a"])], + [desc_parameter, ([desc_sig_name, "b"])])]) assert_node(doctree[0], addnodes.index, entries=[("single", "sum() (built-in function)", "sum", "", None)]) assert_node(doctree[1], addnodes.desc, domain="js", objtype="function", noindex=False) @@ -198,8 +199,9 @@ def test_js_class(app): text = ".. js:class:: Application" doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "class "], - [desc_name, "Application"], + [desc, ([desc_signature, ([desc_annotation, ([desc_sig_keyword, 'class'], + desc_sig_space)], + [desc_name, ([desc_sig_name, "Application"])], [desc_parameterlist, ()])], [desc_content, ()])])) assert_node(doctree[0], addnodes.index, @@ -211,7 +213,7 @@ def test_js_data(app): text = ".. js:data:: name" doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, desc_name, "name"], + [desc, ([desc_signature, ([desc_name, ([desc_sig_name, "name"])])], [desc_content, ()])])) assert_node(doctree[0], addnodes.index, entries=[("single", "name (global variable or constant)", "name", "", None)]) diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index b111e5276c0..e34218dfa4b 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -18,8 +18,10 @@ 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_name, desc_sig_operator, desc_sig_punctuation, - desc_signature, pending_xref) + 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, + pending_xref) from sphinx.domains import IndexEntry from sphinx.domains.python import (PythonDomain, PythonModuleIndex, _parse_annotation, _pseudo_parse_arglist, py_sig_re) @@ -290,7 +292,8 @@ def test_parse_annotation(app): assert_node(doctree, ([pending_xref, "Tuple"], [desc_sig_punctuation, "["], [pending_xref, "int"], - [desc_sig_punctuation, ", "], + [desc_sig_punctuation, ","], + desc_sig_space, [pending_xref, "int"], [desc_sig_punctuation, "]"])) @@ -305,19 +308,23 @@ def test_parse_annotation(app): assert_node(doctree, ([pending_xref, "Tuple"], [desc_sig_punctuation, "["], [pending_xref, "int"], - [desc_sig_punctuation, ", "], + [desc_sig_punctuation, ","], + desc_sig_space, [desc_sig_punctuation, "..."], [desc_sig_punctuation, "]"])) doctree = _parse_annotation("Callable[[int, int], int]", app.env) + print(doctree) assert_node(doctree, ([pending_xref, "Callable"], [desc_sig_punctuation, "["], [desc_sig_punctuation, "["], [pending_xref, "int"], - [desc_sig_punctuation, ", "], + [desc_sig_punctuation, ","], + desc_sig_space, [pending_xref, "int"], [desc_sig_punctuation, "]"], - [desc_sig_punctuation, ", "], + [desc_sig_punctuation, ","], + desc_sig_space, [pending_xref, "int"], [desc_sig_punctuation, "]"])) @@ -326,7 +333,8 @@ def test_parse_annotation(app): [desc_sig_punctuation, "["], [desc_sig_punctuation, "["], [desc_sig_punctuation, "]"], - [desc_sig_punctuation, ", "], + [desc_sig_punctuation, ","], + desc_sig_space, [pending_xref, "int"], [desc_sig_punctuation, "]"])) @@ -347,19 +355,22 @@ def test_parse_annotation_Literal(app): doctree = _parse_annotation("Literal[True, False]", app.env) assert_node(doctree, ([pending_xref, "Literal"], [desc_sig_punctuation, "["], - "True", - [desc_sig_punctuation, ", "], - "False", + [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, "typing.Literal"], [desc_sig_punctuation, "["], - "0", - [desc_sig_punctuation, ", "], - "1", - [desc_sig_punctuation, ", "], - "'abc'", + [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, "]"])) @@ -376,7 +387,7 @@ def test_pyfunction_signature(app): 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"])]) @@ -394,7 +405,7 @@ def test_pyfunction_signature_full(app): 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, "="], @@ -402,28 +413,28 @@ def test_pyfunction_signature_full(app): [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"])])]) @@ -482,11 +493,11 @@ def test_pyfunction_with_union_type_operator(app): 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"])])])]) @@ -501,16 +512,16 @@ def test_optional_pyfunction_signature(app): assert_node(doctree[1], addnodes.desc, desctype="function", domain="py", objtype="function", noindex=False) assert_node(doctree[1][0][1], - ([desc_parameter, "source"], - [desc_optional, ([desc_parameter, "filename"], - [desc_optional, desc_parameter, "symbol"])])) + ([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, ([desc_signature, ([desc_annotation, ('exception', desc_sig_space)], [desc_addname, "builtins."], [desc_name, "IOError"])], desc_content)])) @@ -525,9 +536,15 @@ def test_pydata_signature(app): doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, [desc, ([desc_signature, ([desc_name, "version"], - [desc_annotation, (": ", + [desc_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, [pending_xref, "int"])], - [desc_annotation, " = 1"])], + [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", noindex=False) @@ -539,7 +556,8 @@ def test_pydata_signature_old(app): doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, [desc, ([desc_signature, ([desc_name, "version"], - [desc_annotation, " = 1"])], + [desc_annotation, (desc_sig_space, + "= 1")])], desc_content)])) assert_node(doctree[1], addnodes.desc, desctype="data", domain="py", objtype="data", noindex=False) @@ -551,11 +569,12 @@ def test_pydata_with_union_type_operator(app): doctree = restructuredtext.parse(app, text) assert_node(doctree[1][0], ([desc_name, "version"], - [desc_annotation, (": ", + [desc_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, [pending_xref, "int"], - " ", + desc_sig_space, [desc_sig_punctuation, "|"], - " ", + desc_sig_space, [pending_xref, "str"])])) @@ -566,7 +585,7 @@ def test_pyobject_prefix(app): " .. py:method:: FooBar.say") doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "class "], + [desc, ([desc_signature, ([desc_annotation, ('class', desc_sig_space)], [desc_name, "Foo"])], [desc_content, (addnodes.index, desc, @@ -587,10 +606,11 @@ def test_pydata(app): addnodes.index, [desc, ([desc_signature, ([desc_addname, "example."], [desc_name, "var"], - [desc_annotation, (": ", + [desc_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, [pending_xref, "int"])])], [desc_content, ()])])) - assert_node(doctree[3][0][2][1], pending_xref, **{"py:module": "example"}) + 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) @@ -609,7 +629,8 @@ def test_pyfunction(app): nodes.target, addnodes.index, addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "async "], + [desc, ([desc_signature, ([desc_annotation, ([desc_sig_keyword, 'async'], + desc_sig_space)], [desc_addname, "example."], [desc_name, "func2"], [desc_parameterlist, ()])], @@ -634,11 +655,14 @@ def test_pyclass_options(app): domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "class "], + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], [desc_name, "Class1"])], [desc_content, ()])], addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "final class "], + [desc, ([desc_signature, ([desc_annotation, ("final", + desc_sig_space, + "class", + desc_sig_space)], [desc_name, "Class2"])], [desc_content, ()])])) @@ -674,7 +698,7 @@ def test_pymethod_options(app): domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "class "], + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], [desc_name, "Class"])], [desc_content, (addnodes.index, desc, @@ -703,7 +727,7 @@ def test_pymethod_options(app): # :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 "], + assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, ("classmethod", desc_sig_space)], [desc_name, "meth2"], [desc_parameterlist, ()])], [desc_content, ()])) @@ -713,7 +737,7 @@ def test_pymethod_options(app): # :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 "], + assert_node(doctree[1][1][5], ([desc_signature, ([desc_annotation, ("static", desc_sig_space)], [desc_name, "meth3"], [desc_parameterlist, ()])], [desc_content, ()])) @@ -723,7 +747,7 @@ def test_pymethod_options(app): # :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 "], + assert_node(doctree[1][1][7], ([desc_signature, ([desc_annotation, ("async", desc_sig_space)], [desc_name, "meth4"], [desc_parameterlist, ()])], [desc_content, ()])) @@ -732,8 +756,8 @@ def test_pymethod_options(app): # :property: assert_node(doctree[1][1][8], addnodes.index, - entries=[('single', 'meth5() (Class property)', 'Class.meth5', '', None)]) - assert_node(doctree[1][1][9], ([desc_signature, ([desc_annotation, "property "], + entries=[('single', 'meth5 (Class property)', 'Class.meth5', '', None)]) + assert_node(doctree[1][1][9], ([desc_signature, ([desc_annotation, ("property", desc_sig_space)], [desc_name, "meth5"])], [desc_content, ()])) assert 'Class.meth5' in domain.objects @@ -742,7 +766,7 @@ def test_pymethod_options(app): # :abstractmethod: 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, "abstract "], + assert_node(doctree[1][1][11], ([desc_signature, ([desc_annotation, ("abstract", desc_sig_space)], [desc_name, "meth6"], [desc_parameterlist, ()])], [desc_content, ()])) @@ -752,7 +776,7 @@ def test_pymethod_options(app): # :final: assert_node(doctree[1][1][12], addnodes.index, entries=[('single', 'meth7() (Class method)', 'Class.meth7', '', None)]) - assert_node(doctree[1][1][13], ([desc_signature, ([desc_annotation, "final "], + assert_node(doctree[1][1][13], ([desc_signature, ([desc_annotation, ("final", desc_sig_space)], [desc_name, "meth7"], [desc_parameterlist, ()])], [desc_content, ()])) @@ -767,13 +791,13 @@ def test_pyclassmethod(app): domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "class "], + [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 "], + assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("classmethod", desc_sig_space)], [desc_name, "meth"], [desc_parameterlist, ()])], [desc_content, ()])) @@ -788,13 +812,13 @@ def test_pystaticmethod(app): domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "class "], + [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 "], + assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("static", desc_sig_space)], [desc_name, "meth"], [desc_parameterlist, ()])], [desc_content, ()])) @@ -811,22 +835,27 @@ def test_pyattribute(app): domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "class "], + [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_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, [pending_xref, "Optional"], [desc_sig_punctuation, "["], [pending_xref, "str"], [desc_sig_punctuation, "]"])], - [desc_annotation, " = ''"])], + [desc_annotation, (desc_sig_space, + [desc_sig_punctuation, '='], + desc_sig_space, + "''")] + )], [desc_content, ()])) - assert_node(doctree[1][1][1][0][1][1], pending_xref, **{"py:class": "Class"}) - assert_node(doctree[1][1][1][0][1][3], pending_xref, **{"py:class": "Class"}) + assert_node(doctree[1][1][1][0][1][2], pending_xref, **{"py:class": "Class"}) + assert_node(doctree[1][1][1][0][1][4], pending_xref, **{"py:class": "Class"}) assert 'Class.attr' in domain.objects assert domain.objects['Class.attr'] == ('index', 'Class.attr', 'attribute', False) @@ -844,7 +873,7 @@ def test_pyproperty(app): domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "class "], + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], [desc_name, "Class"])], [desc_content, (addnodes.index, desc, @@ -852,16 +881,20 @@ def test_pyproperty(app): 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 property "], + assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, ("abstract", desc_sig_space, + "property", desc_sig_space)], [desc_name, "prop1"], - [desc_annotation, (": ", + [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 property "], + assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, ("class", desc_sig_space, + "property", desc_sig_space)], [desc_name, "prop2"], - [desc_annotation, (": ", + [desc_annotation, ([desc_sig_punctuation, ':'], + desc_sig_space, [pending_xref, "str"])])], [desc_content, ()])) assert 'Class.prop1' in domain.objects @@ -906,7 +939,7 @@ def test_canonical(app): domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "class "], + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], [desc_addname, "io."], [desc_name, "StringIO"])], desc_content)])) @@ -964,7 +997,7 @@ def test_info_field_list(app): assert_node(doctree, (nodes.target, addnodes.index, addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "class "], + [desc, ([desc_signature, ([desc_annotation, ("class", desc_sig_space)], [desc_addname, "example."], [desc_name, "Class"])], [desc_content, nodes.field_list, nodes.field])])) @@ -1055,7 +1088,7 @@ def test_info_field_list_piped_type(app): (nodes.target, addnodes.index, addnodes.index, - [desc, ([desc_signature, ([desc_annotation, "class "], + [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, @@ -1077,6 +1110,42 @@ def test_info_field_list_piped_type(app): **{"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, + (nodes.target, + addnodes.index, + addnodes.index, + [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" diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 299c1c68170..8f14392b218 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -984,7 +984,7 @@ def test_autodoc_inner_class(app): ' .. py:attribute:: Outer.factory', ' :module: target', '', - ' alias of :class:`dict`' + ' alias of :py:class:`dict`' ] actual = do_autodoc(app, 'class', 'target.Outer.Inner', options) @@ -1009,7 +1009,7 @@ def test_autodoc_inner_class(app): '', '.. py:class:: InnerChild()', ' :module: target', '', - ' Bases: :class:`target.Outer.Inner`', + ' Bases: :py:class:`target.Outer.Inner`', '', ' InnerChild docstring', '', @@ -1359,6 +1359,7 @@ def test_slots(app): '', ' .. py:attribute:: Bar.attr1', ' :module: target.slots', + ' :type: int', '', ' docstring of attr1', '', @@ -1399,9 +1400,16 @@ def test_slots(app): def test_enum_class(app): options = {"members": None} actual = do_autodoc(app, 'class', 'target.enums.EnumCls', options) + + if sys.version_info > (3, 11): + args = ('(value, names=None, *, module=None, qualname=None, ' + 'type=None, start=1, boundary=None)') + else: + args = '(value)' + assert list(actual) == [ '', - '.. py:class:: EnumCls(value)', + '.. py:class:: EnumCls' + args, ' :module: target.enums', '', ' this is enum class', @@ -1619,59 +1627,6 @@ def test_bound_method(app): ] -@pytest.mark.sphinx('html', testroot='ext-autodoc') -def test_coroutine(app): - actual = do_autodoc(app, 'function', 'target.functions.coroutinefunc') - assert list(actual) == [ - '', - '.. py:function:: coroutinefunc()', - ' :module: target.functions', - ' :async:', - '', - ] - - options = {"members": None} - actual = do_autodoc(app, 'class', 'target.coroutine.AsyncClass', options) - assert list(actual) == [ - '', - '.. py:class:: AsyncClass()', - ' :module: target.coroutine', - '', - '', - ' .. py:method:: AsyncClass.do_coroutine()', - ' :module: target.coroutine', - ' :async:', - '', - ' A documented coroutine function', - '', - '', - ' .. py:method:: AsyncClass.do_coroutine2()', - ' :module: target.coroutine', - ' :async:', - ' :classmethod:', - '', - ' A documented coroutine classmethod', - '', - '', - ' .. py:method:: AsyncClass.do_coroutine3()', - ' :module: target.coroutine', - ' :async:', - ' :staticmethod:', - '', - ' A documented coroutine staticmethod', - '', - ] - - # force-synchronized wrapper - actual = do_autodoc(app, 'function', 'target.coroutine.sync_func') - assert list(actual) == [ - '', - '.. py:function:: sync_func()', - ' :module: target.coroutine', - '', - ] - - @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_partialmethod(app): expected = [ @@ -1750,7 +1705,7 @@ def test_autodoc_typed_instance_variables(app): '.. py:attribute:: Alias', ' :module: target.typed_vars', '', - ' alias of :class:`target.typed_vars.Derived`', + ' alias of :py:class:`target.typed_vars.Derived`', '', '.. py:class:: Class()', ' :module: target.typed_vars', @@ -1915,12 +1870,12 @@ def test_autodoc_GenericAlias(app): ' .. py:attribute:: Class.T', ' :module: target.genericalias', '', - ' alias of :class:`~typing.List`\\ [:class:`int`]', + ' alias of :py:class:`~typing.List`\\ [:py:class:`int`]', '', '.. py:attribute:: T', ' :module: target.genericalias', '', - ' alias of :class:`~typing.List`\\ [:class:`int`]', + ' alias of :py:class:`~typing.List`\\ [:py:class:`int`]', ] else: assert list(actual) == [ @@ -1937,7 +1892,7 @@ def test_autodoc_GenericAlias(app): '', ' A list of int', '', - ' alias of :class:`~typing.List`\\ [:class:`int`]', + ' alias of :py:class:`~typing.List`\\ [:py:class:`int`]', '', '', '.. py:data:: T', @@ -1945,7 +1900,7 @@ def test_autodoc_GenericAlias(app): '', ' A list of int', '', - ' alias of :class:`~typing.List`\\ [:class:`int`]', + ' alias of :py:class:`~typing.List`\\ [:py:class:`int`]', '', ] @@ -1977,7 +1932,7 @@ def test_autodoc_TypeVar(app): '', ' T6', '', - ' alias of :class:`int`', + ' alias of :py:class:`int`', '', '', '.. py:data:: T1', @@ -2017,7 +1972,7 @@ def test_autodoc_TypeVar(app): '', ' T6', '', - ' alias of :class:`int`', + ' alias of :py:class:`int`', '', '', '.. py:data:: T7', @@ -2025,7 +1980,7 @@ def test_autodoc_TypeVar(app): '', ' T7', '', - " alias of TypeVar('T7', bound=\\ :class:`int`)", + " alias of TypeVar('T7', bound=\\ :py:class:`int`)", '', ] @@ -2159,6 +2114,9 @@ def test_singledispatchmethod_automethod(app): ] +@pytest.mark.skipif(sys.version_info > (3, 11), + reason=('cython does not support python-3.11 yet. ' + 'see https://github.com/cython/cython/issues/4365')) @pytest.mark.skipif(pyximport is None, reason='cython is not installed') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_cython(app): diff --git a/tests/test_ext_autodoc_autoattribute.py b/tests/test_ext_autodoc_autoattribute.py index 20317b8da39..8fe065d6533 100644 --- a/tests/test_ext_autodoc_autoattribute.py +++ b/tests/test_ext_autodoc_autoattribute.py @@ -129,6 +129,7 @@ def test_autoattribute_slots_variable_dict(app): '', '.. py:attribute:: Bar.attr1', ' :module: target.slots', + ' :type: int', '', ' docstring of attr1', '', @@ -167,7 +168,7 @@ def test_autoattribute_GenericAlias(app): '', ' A list of int', '', - ' alias of :class:`~typing.List`\\ [:class:`int`]', + ' alias of :py:class:`~typing.List`\\ [:py:class:`int`]', '', ] @@ -182,7 +183,7 @@ def test_autoattribute_NewType(app): '', ' T6', '', - ' alias of :class:`int`', + ' alias of :py:class:`int`', '', ] diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py index 24617bf0a5f..9c730f425c8 100644 --- a/tests/test_ext_autodoc_autoclass.py +++ b/tests/test_ext_autodoc_autoclass.py @@ -243,6 +243,7 @@ def test_slots_attribute(app): '', ' .. py:attribute:: Bar.attr1', ' :module: target.slots', + ' :type: int', '', ' docstring of attr1', '', @@ -265,14 +266,29 @@ def test_show_inheritance_for_subclass_of_generic_type(app): '.. py:class:: Quux(iterable=(), /)', ' :module: target.classes', '', - ' Bases: :class:`~typing.List`\\ ' - '[:obj:`~typing.Union`\\ [:class:`int`, :class:`float`]]', + ' Bases: :py:class:`~typing.List`\\ ' + '[:py:obj:`~typing.Union`\\ [:py:class:`int`, :py:class:`float`]]', '', ' A subclass of List[Union[int, float]]', '', ] +@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.') +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_show_inheritance_for_decendants_of_generic_type(app): + options = {'show-inheritance': None} + actual = do_autodoc(app, 'class', 'target.classes.Corge', options) + assert list(actual) == [ + '', + '.. py:class:: Corge(iterable=(), /)', + ' :module: target.classes', + '', + ' Bases: :py:class:`target.classes.Quux`', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_process_bases(app): def autodoc_process_bases(app, name, obj, options, bases): @@ -296,7 +312,7 @@ def autodoc_process_bases(app, name, obj, options, bases): '.. py:class:: Quux(*args, **kwds)', ' :module: target.classes', '', - ' Bases: :class:`int`, :class:`str`', + ' Bases: :py:class:`int`, :py:class:`str`', '', ' A subclass of List[Union[int, float]]', '', @@ -307,7 +323,7 @@ def autodoc_process_bases(app, name, obj, options, bases): '.. py:class:: Quux(iterable=(), /)', ' :module: target.classes', '', - ' Bases: :class:`int`, :class:`str`', + ' Bases: :py:class:`int`, :py:class:`str`', '', ' A subclass of List[Union[int, float]]', '', @@ -375,7 +391,7 @@ def autodoc_process_docstring(*args): '.. py:attribute:: Alias', ' :module: target.classes', '', - ' alias of :class:`target.classes.Foo`', + ' alias of :py:class:`target.classes.Foo`', ] @@ -389,3 +405,45 @@ def test_class_alias_having_doccomment(app): ' docstring', '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_coroutine(app): + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.coroutine.AsyncClass', options) + assert list(actual) == [ + '', + '.. py:class:: AsyncClass()', + ' :module: target.coroutine', + '', + '', + ' .. py:method:: AsyncClass.do_asyncgen()', + ' :module: target.coroutine', + ' :async:', + '', + ' A documented async generator', + '', + '', + ' .. py:method:: AsyncClass.do_coroutine()', + ' :module: target.coroutine', + ' :async:', + '', + ' A documented coroutine function', + '', + '', + ' .. py:method:: AsyncClass.do_coroutine2()', + ' :module: target.coroutine', + ' :async:', + ' :classmethod:', + '', + ' A documented coroutine classmethod', + '', + '', + ' .. py:method:: AsyncClass.do_coroutine3()', + ' :module: target.coroutine', + ' :async:', + ' :staticmethod:', + '', + ' A documented coroutine staticmethod', + '', + ] diff --git a/tests/test_ext_autodoc_autodata.py b/tests/test_ext_autodoc_autodata.py index d01e45fc10a..f983726adc0 100644 --- a/tests/test_ext_autodoc_autodata.py +++ b/tests/test_ext_autodoc_autodata.py @@ -96,7 +96,7 @@ def test_autodata_GenericAlias(app): '', ' A list of int', '', - ' alias of :class:`~typing.List`\\ [:class:`int`]', + ' alias of :py:class:`~typing.List`\\ [:py:class:`int`]', '', ] @@ -111,7 +111,7 @@ def test_autodata_NewType(app): '', ' T6', '', - ' alias of :class:`int`', + ' alias of :py:class:`int`', '', ] diff --git a/tests/test_ext_autodoc_autofunction.py b/tests/test_ext_autodoc_autofunction.py index ca2429b5e88..52af51abbfc 100644 --- a/tests/test_ext_autodoc_autofunction.py +++ b/tests/test_ext_autodoc_autofunction.py @@ -168,3 +168,38 @@ def test_wrapped_function_contextmanager(app): " You'll feel better in this context!", '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_coroutine(app): + actual = do_autodoc(app, 'function', 'target.functions.coroutinefunc') + assert list(actual) == [ + '', + '.. py:function:: coroutinefunc()', + ' :module: target.functions', + ' :async:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_synchronized_coroutine(app): + actual = do_autodoc(app, 'function', 'target.coroutine.sync_func') + assert list(actual) == [ + '', + '.. py:function:: sync_func()', + ' :module: target.coroutine', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_async_generator(app): + actual = do_autodoc(app, 'function', 'target.functions.asyncgenerator') + assert list(actual) == [ + '', + '.. py:function:: asyncgenerator()', + ' :module: target.functions', + ' :async:', + '', + ] diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index f6436f541a3..e04cd83b681 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -844,6 +844,10 @@ def test_autodoc_typehints_description_no_undoc(app): (app.srcdir / 'index.rst').write_text( '.. autofunction:: target.typehints.incr\n' '\n' + '.. autofunction:: target.typehints.decr\n' + '\n' + ' :returns: decremented number\n' + '\n' '.. autofunction:: target.typehints.tuple_args\n' '\n' ' :param x: arg\n' @@ -852,6 +856,14 @@ def test_autodoc_typehints_description_no_undoc(app): app.build() context = (app.outdir / 'index.txt').read_text() assert ('target.typehints.incr(a, b=1)\n' + '\n' + 'target.typehints.decr(a, b=1)\n' + '\n' + ' Returns:\n' + ' decremented number\n' + '\n' + ' Return type:\n' + ' int\n' '\n' 'target.typehints.tuple_args(x)\n' '\n' diff --git a/tests/test_ext_autodoc_preserve_defaults.py b/tests/test_ext_autodoc_preserve_defaults.py index c0b5a9f294f..955c60aa4bf 100644 --- a/tests/test_ext_autodoc_preserve_defaults.py +++ b/tests/test_ext_autodoc_preserve_defaults.py @@ -8,6 +8,8 @@ :license: BSD, see LICENSE for details. """ +import sys + import pytest from .test_ext_autodoc import do_autodoc @@ -16,6 +18,11 @@ @pytest.mark.sphinx('html', testroot='ext-autodoc', confoverrides={'autodoc_preserve_defaults': True}) def test_preserve_defaults(app): + if sys.version_info < (3, 8): + color = "16777215" + else: + color = "0xFFFFFF" + options = {"members": None} actual = do_autodoc(app, 'module', 'target.preserve_defaults', options) assert list(actual) == [ @@ -30,14 +37,14 @@ def test_preserve_defaults(app): '', '', ' .. py:method:: Class.meth(name: str = CONSTANT, sentinel: Any = SENTINEL, ' - 'now: datetime.datetime = datetime.now()) -> None', + 'now: datetime.datetime = datetime.now(), color: int = %s) -> None' % color, ' :module: target.preserve_defaults', '', ' docstring', '', '', '.. py:function:: foo(name: str = CONSTANT, sentinel: Any = SENTINEL, now: ' - 'datetime.datetime = datetime.now()) -> None', + 'datetime.datetime = datetime.now(), color: int = %s) -> None' % color, ' :module: target.preserve_defaults', '', ' docstring', diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index 28b5e63b150..e820730a19c 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -42,6 +42,12 @@ def reference_check(app, *args, **kwds): return missing_reference(app, app.env, node, contnode) +def set_config(app, mapping): + app.config.intersphinx_mapping = mapping + app.config.intersphinx_cache_limit = 0 + app.config.intersphinx_disabled_reftypes = [] + + @mock.patch('sphinx.ext.intersphinx.InventoryFile') @mock.patch('sphinx.ext.intersphinx._read_from_url') def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app, status, warning): @@ -90,13 +96,12 @@ def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app, status, def test_missing_reference(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_mapping = { + set_config(app, { 'https://docs.python.org/': inv_file, 'py3k': ('https://docs.python.org/py3k/', inv_file), 'py3krel': ('py3k', inv_file), # relative path 'py3krelparent': ('../../py3k', inv_file), # relative path, parent dir - } - app.config.intersphinx_cache_limit = 0 + }) # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) @@ -133,12 +138,12 @@ def test_missing_reference(tempdir, app, status, warning): refexplicit=True) assert rn[0].astext() == 'py3k:module2' - # prefix given, target not found and nonexplicit title: prefix is stripped + # prefix given, target not found and nonexplicit title: prefix is not stripped node, contnode = fake_node('py', 'mod', 'py3k:unknown', 'py3k:unknown', refexplicit=False) rn = missing_reference(app, app.env, node, contnode) assert rn is None - assert contnode[0].astext() == 'unknown' + assert contnode[0].astext() == 'py3k:unknown' # prefix given, target not found and explicit title: nothing is changed node, contnode = fake_node('py', 'mod', 'py3k:unknown', 'py3k:unknown', @@ -169,10 +174,9 @@ def test_missing_reference(tempdir, app, status, warning): def test_missing_reference_pydomain(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_mapping = { + set_config(app, { 'https://docs.python.org/': inv_file, - } - app.config.intersphinx_cache_limit = 0 + }) # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) @@ -210,10 +214,9 @@ def test_missing_reference_pydomain(tempdir, app, status, warning): def test_missing_reference_stddomain(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_mapping = { + set_config(app, { 'cmd': ('https://docs.python.org/', inv_file), - } - app.config.intersphinx_cache_limit = 0 + }) # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) @@ -242,10 +245,9 @@ def test_missing_reference_stddomain(tempdir, app, status, warning): def test_missing_reference_cppdomain(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_mapping = { + set_config(app, { 'https://docs.python.org/': inv_file, - } - app.config.intersphinx_cache_limit = 0 + }) # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) @@ -269,10 +271,9 @@ def test_missing_reference_cppdomain(tempdir, app, status, warning): def test_missing_reference_jsdomain(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_mapping = { + set_config(app, { 'https://docs.python.org/': inv_file, - } - app.config.intersphinx_cache_limit = 0 + }) # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) @@ -291,14 +292,75 @@ def test_missing_reference_jsdomain(tempdir, app, status, warning): assert rn.astext() == 'baz()' +def test_missing_reference_disabled_domain(tempdir, app, status, warning): + inv_file = tempdir / 'inventory' + inv_file.write_bytes(inventory_v2) + set_config(app, { + 'inv': ('https://docs.python.org/', inv_file), + }) + + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + def case(*, term, doc, py): + def assert_(rn, expected): + if expected is None: + assert rn is None + else: + assert rn.astext() == expected + + kwargs = {} + + node, contnode = fake_node('std', 'term', 'a term', 'a term', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, 'a term' if term else None) + + node, contnode = fake_node('std', 'term', 'inv:a term', 'a term', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, 'a term') + + node, contnode = fake_node('std', 'doc', 'docname', 'docname', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, 'docname' if doc else None) + + node, contnode = fake_node('std', 'doc', 'inv:docname', 'docname', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, 'docname') + + # an arbitrary ref in another domain + node, contnode = fake_node('py', 'func', 'module1.func', 'func()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, 'func()' if py else None) + + node, contnode = fake_node('py', 'func', 'inv:module1.func', 'func()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, 'func()') + + # the base case, everything should resolve + assert app.config.intersphinx_disabled_reftypes == [] + case(term=True, doc=True, py=True) + + # disabled a single ref type + app.config.intersphinx_disabled_reftypes = ['std:doc'] + case(term=True, doc=False, py=True) + + # disabled a whole domain + app.config.intersphinx_disabled_reftypes = ['std:*'] + case(term=False, doc=False, py=True) + + # disabled all domains + app.config.intersphinx_disabled_reftypes = ['*'] + case(term=False, doc=False, py=False) + + @pytest.mark.xfail(os.name != 'posix', reason="Path separator mismatch issue") def test_inventory_not_having_version(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2_not_having_version) - app.config.intersphinx_mapping = { + set_config(app, { 'https://docs.python.org/': inv_file, - } - app.config.intersphinx_cache_limit = 0 + }) # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) @@ -318,16 +380,15 @@ def test_load_mappings_warnings(tempdir, app, status, warning): """ inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_mapping = { + set_config(app, { 'https://docs.python.org/': inv_file, 'py3k': ('https://docs.python.org/py3k/', inv_file), 'repoze.workflow': ('http://docs.repoze.org/workflow/', inv_file), 'django-taggit': ('http://django-taggit.readthedocs.org/en/latest/', inv_file), 12345: ('http://www.sphinx-doc.org/en/stable/', inv_file), - } + }) - app.config.intersphinx_cache_limit = 0 # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) load_mappings(app) @@ -337,7 +398,7 @@ def test_load_mappings_warnings(tempdir, app, status, warning): def test_load_mappings_fallback(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_cache_limit = 0 + set_config(app, {}) # connect to invalid path app.config.intersphinx_mapping = { diff --git a/tests/test_ext_math.py b/tests/test_ext_math.py index 973fc369943..7c78954b7ff 100644 --- a/tests/test_ext_math.py +++ b/tests/test_ext_math.py @@ -71,7 +71,7 @@ def test_mathjax_options(app, status, warning): app.builder.build_all() content = (app.outdir / 'index.html').read_text() - assert ('' in content) diff --git a/tests/test_extension.py b/tests/test_extension.py new file mode 100644 index 00000000000..db9f4e48761 --- /dev/null +++ b/tests/test_extension.py @@ -0,0 +1,31 @@ +""" + test_extension + ~~~~~~~~~~~~~~ + + Test sphinx.extesion module. + + :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import pytest + +from sphinx.errors import VersionRequirementError +from sphinx.extension import Extension, verify_needs_extensions + + +def test_needs_extensions(app): + # empty needs_extensions + assert app.config.needs_extensions == {} + verify_needs_extensions(app, app.config) + + # needs_extensions fulfilled + app.config.needs_extensions = {'test.extension': '3.9'} + app.extensions['test.extension'] = Extension('test.extension', 'test.extension', version='3.10') + verify_needs_extensions(app, app.config) + + # needs_extensions not fulfilled + app.config.needs_extensions = {'test.extension': '3.11'} + app.extensions['test.extension'] = Extension('test.extension', 'test.extension', version='3.10') + with pytest.raises(VersionRequirementError): + verify_needs_extensions(app, app.config) diff --git a/tests/test_search.py b/tests/test_search.py index dc4f546ca2d..18407875ff5 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -66,7 +66,11 @@ def is_registered_term(index, keyword): def test_objects_are_escaped(app, status, warning): app.builder.build_all() index = jsload(app.outdir / 'searchindex.js') - assert 'n::Array<T, d>' in index.get('objects').get('') # n::Arrayis escaped + for item in index.get('objects').get(''): + if item[-1] == 'n::Array<T, d>': # n::Array is escaped + break + else: + assert False, index.get('objects').get('') @pytest.mark.sphinx(testroot='search') @@ -129,50 +133,58 @@ def test_term_in_raw_directive(app, status, warning): def test_IndexBuilder(): - domain = DummyDomain([('objname', 'objdispname', 'objtype', 'docname', '#anchor', 1), - ('objname2', 'objdispname2', 'objtype2', 'docname2', '', -1)]) - env = DummyEnvironment('1.0', {'dummy': domain}) + domain1 = DummyDomain([('objname1', 'objdispname1', 'objtype1', 'docname1_1', '#anchor', 1), + ('objname2', 'objdispname2', 'objtype2', 'docname1_2', '', -1)]) + domain2 = DummyDomain([('objname1', 'objdispname1', 'objtype1', 'docname2_1', '#anchor', 1), + ('objname2', 'objdispname2', 'objtype2', 'docname2_2', '', -1)]) + env = DummyEnvironment('1.0', {'dummy1': domain1, 'dummy2': domain2}) doc = utils.new_document(b'test data', settings) doc['file'] = 'dummy' parser.parse(FILE_CONTENTS, doc) # feed index = IndexBuilder(env, 'en', {}, None) - index.feed('docname', 'filename', 'title', doc) - index.feed('docname2', 'filename2', 'title2', doc) - assert index._titles == {'docname': 'title', 'docname2': 'title2'} - assert index._filenames == {'docname': 'filename', 'docname2': 'filename2'} + 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) + 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', + 'docname2_1': 'filename2_1', 'docname2_2': 'filename2_2'} assert index._mapping == { - 'ar': {'docname', 'docname2'}, - 'fermion': {'docname', 'docname2'}, - 'comment': {'docname', 'docname2'}, - 'non': {'docname', 'docname2'}, - 'index': {'docname', 'docname2'}, - 'test': {'docname', 'docname2'} + 'ar': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}, + 'fermion': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}, + 'comment': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}, + 'non': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}, + 'index': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}, + 'test': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'} } - assert index._title_mapping == {'section_titl': {'docname', 'docname2'}} + assert index._title_mapping == {'section_titl': {'docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'}} assert index._objtypes == {} assert index._objnames == {} # freeze assert index.freeze() == { - 'docnames': ('docname', 'docname2'), + 'docnames': ('docname1_1', 'docname1_2', 'docname2_1', 'docname2_2'), 'envversion': '1.0', - 'filenames': ['filename', 'filename2'], - 'objects': {'': {'objdispname': (0, 0, 1, '#anchor')}}, - 'objnames': {0: ('dummy', 'objtype', 'objtype')}, - 'objtypes': {0: 'dummy:objtype'}, - 'terms': {'ar': [0, 1], - 'comment': [0, 1], - 'fermion': [0, 1], - 'index': [0, 1], - 'non': [0, 1], - 'test': [0, 1]}, - 'titles': ('title', 'title2'), - 'titleterms': {'section_titl': [0, 1]} + 'filenames': ['filename1_1', 'filename1_2', 'filename2_1', 'filename2_2'], + 'objects': {'': [(0, 0, 1, '#anchor', 'objdispname1'), + (2, 1, 1, '#anchor', 'objdispname1')]}, + 'objnames': {0: ('dummy1', 'objtype1', 'objtype1'), 1: ('dummy2', 'objtype1', 'objtype1')}, + 'objtypes': {0: 'dummy1:objtype1', 1: 'dummy2:objtype1'}, + 'terms': {'ar': [0, 1, 2, 3], + 'comment': [0, 1, 2, 3], + 'fermion': [0, 1, 2, 3], + 'index': [0, 1, 2, 3], + 'non': [0, 1, 2, 3], + 'test': [0, 1, 2, 3]}, + 'titles': ('title1_1', 'title1_2', 'title2_1', 'title2_2'), + 'titleterms': {'section_titl': [0, 1, 2, 3]} } - assert index._objtypes == {('dummy', 'objtype'): 0} - assert index._objnames == {0: ('dummy', 'objtype', 'objtype')} + assert index._objtypes == {('dummy1', 'objtype1'): 0, ('dummy2', 'objtype1'): 1} + assert index._objnames == {0: ('dummy1', 'objtype1', 'objtype1'), + 1: ('dummy2', 'objtype1', 'objtype1')} # dump / load stream = BytesIO() @@ -195,40 +207,41 @@ def test_IndexBuilder(): assert index2._objnames == index._objnames # prune - index.prune(['docname2']) - assert index._titles == {'docname2': 'title2'} - assert index._filenames == {'docname2': 'filename2'} + index.prune(['docname1_2', 'docname2_2']) + assert index._titles == {'docname1_2': 'title1_2', 'docname2_2': 'title2_2'} + assert index._filenames == {'docname1_2': 'filename1_2', 'docname2_2': 'filename2_2'} assert index._mapping == { - 'ar': {'docname2'}, - 'fermion': {'docname2'}, - 'comment': {'docname2'}, - 'non': {'docname2'}, - 'index': {'docname2'}, - 'test': {'docname2'} + 'ar': {'docname1_2', 'docname2_2'}, + 'fermion': {'docname1_2', 'docname2_2'}, + 'comment': {'docname1_2', 'docname2_2'}, + 'non': {'docname1_2', 'docname2_2'}, + 'index': {'docname1_2', 'docname2_2'}, + 'test': {'docname1_2', 'docname2_2'} } - assert index._title_mapping == {'section_titl': {'docname2'}} - assert index._objtypes == {('dummy', 'objtype'): 0} - assert index._objnames == {0: ('dummy', 'objtype', 'objtype')} + assert index._title_mapping == {'section_titl': {'docname1_2', 'docname2_2'}} + assert index._objtypes == {('dummy1', 'objtype1'): 0, ('dummy2', 'objtype1'): 1} + assert index._objnames == {0: ('dummy1', 'objtype1', 'objtype1'), 1: ('dummy2', 'objtype1', 'objtype1')} # freeze after prune assert index.freeze() == { - 'docnames': ('docname2',), + 'docnames': ('docname1_2', 'docname2_2'), 'envversion': '1.0', - 'filenames': ['filename2'], + 'filenames': ['filename1_2', 'filename2_2'], 'objects': {}, - 'objnames': {0: ('dummy', 'objtype', 'objtype')}, - 'objtypes': {0: 'dummy:objtype'}, - 'terms': {'ar': 0, - 'comment': 0, - 'fermion': 0, - 'index': 0, - 'non': 0, - 'test': 0}, - 'titles': ('title2',), - 'titleterms': {'section_titl': 0} + 'objnames': {0: ('dummy1', 'objtype1', 'objtype1'), 1: ('dummy2', 'objtype1', 'objtype1')}, + 'objtypes': {0: 'dummy1:objtype1', 1: 'dummy2:objtype1'}, + 'terms': {'ar': [0, 1], + 'comment': [0, 1], + 'fermion': [0, 1], + 'index': [0, 1], + 'non': [0, 1], + 'test': [0, 1]}, + 'titles': ('title1_2', 'title2_2'), + 'titleterms': {'section_titl': [0, 1]} } - assert index._objtypes == {('dummy', 'objtype'): 0} - assert index._objnames == {0: ('dummy', 'objtype', 'objtype')} + assert index._objtypes == {('dummy1', 'objtype1'): 0, ('dummy2', 'objtype1'): 1} + assert index._objnames == {0: ('dummy1', 'objtype1', 'objtype1'), + 1: ('dummy2', 'objtype1', 'objtype1')} def test_IndexBuilder_lookup(): diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 49a9871592e..0b9dcc15db9 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -677,6 +677,25 @@ def func1(a, b, c): assert inspect.unpartial(func3) is func1 +def test_getdoc_inherited_classmethod(): + class Foo: + @classmethod + def meth(self): + """ + docstring + indented text + """ + + class Bar(Foo): + @classmethod + def meth(self): + # inherited classmethod + pass + + assert inspect.getdoc(Bar.meth, getattr, False, Bar, "meth") is None + assert inspect.getdoc(Bar.meth, getattr, True, Bar, "meth") == Foo.meth.__doc__ + + def test_getdoc_inherited_decorated_method(): class Foo: def meth(self): diff --git a/tests/test_util_logging.py b/tests/test_util_logging.py index a03f62b0122..5abcd02ef89 100644 --- a/tests/test_util_logging.py +++ b/tests/test_util_logging.py @@ -131,6 +131,7 @@ def test_is_suppressed_warning(): assert is_suppressed_warning("ref", "option", suppress_warnings) is True assert is_suppressed_warning("files", "image", suppress_warnings) is True assert is_suppressed_warning("files", "stylesheet", suppress_warnings) is True + assert is_suppressed_warning("rest", None, suppress_warnings) is False assert is_suppressed_warning("rest", "syntax", suppress_warnings) is False assert is_suppressed_warning("rest", "duplicated_labels", suppress_warnings) is True @@ -143,33 +144,39 @@ def test_suppress_warnings(app, status, warning): app.config.suppress_warnings = [] warning.truncate(0) + logger.warning('message0', type='test') logger.warning('message1', type='test', subtype='logging') logger.warning('message2', type='test', subtype='crash') logger.warning('message3', type='actual', subtype='logging') + assert 'message0' in warning.getvalue() assert 'message1' in warning.getvalue() assert 'message2' in warning.getvalue() assert 'message3' in warning.getvalue() - assert app._warncount == 3 + assert app._warncount == 4 app.config.suppress_warnings = ['test'] warning.truncate(0) + logger.warning('message0', type='test') logger.warning('message1', type='test', subtype='logging') logger.warning('message2', type='test', subtype='crash') logger.warning('message3', type='actual', subtype='logging') + assert 'message0' not in warning.getvalue() assert 'message1' not in warning.getvalue() assert 'message2' not in warning.getvalue() assert 'message3' in warning.getvalue() - assert app._warncount == 4 + assert app._warncount == 5 app.config.suppress_warnings = ['test.logging'] warning.truncate(0) + logger.warning('message0', type='test') logger.warning('message1', type='test', subtype='logging') logger.warning('message2', type='test', subtype='crash') logger.warning('message3', type='actual', subtype='logging') + assert 'message0' in warning.getvalue() assert 'message1' not in warning.getvalue() assert 'message2' in warning.getvalue() assert 'message3' in warning.getvalue() - assert app._warncount == 6 + assert app._warncount == 8 def test_warningiserror(app, status, warning): diff --git a/tests/test_util_nodes.py b/tests/test_util_nodes.py index cb2ae70a82d..421930cf5a2 100644 --- a/tests/test_util_nodes.py +++ b/tests/test_util_nodes.py @@ -60,31 +60,31 @@ def test_NodeMatcher(): # search by node class matcher = NodeMatcher(nodes.paragraph) - assert len(doctree.traverse(matcher)) == 3 + assert len(list(doctree.traverse(matcher))) == 3 # search by multiple node classes matcher = NodeMatcher(nodes.paragraph, nodes.literal_block) - assert len(doctree.traverse(matcher)) == 4 + assert len(list(doctree.traverse(matcher))) == 4 # search by node attribute matcher = NodeMatcher(block=1) - assert len(doctree.traverse(matcher)) == 1 + assert len(list(doctree.traverse(matcher))) == 1 # search by node attribute (Any) matcher = NodeMatcher(block=Any) - assert len(doctree.traverse(matcher)) == 3 + assert len(list(doctree.traverse(matcher))) == 3 # search by both class and attribute matcher = NodeMatcher(nodes.paragraph, block=Any) - assert len(doctree.traverse(matcher)) == 2 + assert len(list(doctree.traverse(matcher))) == 2 # mismatched matcher = NodeMatcher(nodes.title) - assert len(doctree.traverse(matcher)) == 0 + assert len(list(doctree.traverse(matcher))) == 0 # search with Any does not match to Text node matcher = NodeMatcher(blah=Any) - assert len(doctree.traverse(matcher)) == 0 + assert len(list(doctree.traverse(matcher))) == 0 @pytest.mark.parametrize( diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index bbee68f82cc..c34c4bebc8d 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -17,6 +17,7 @@ import pytest +from sphinx.ext.autodoc import mock from sphinx.util.typing import restify, stringify @@ -41,66 +42,70 @@ class BrokenType: def test_restify(): - assert restify(int) == ":class:`int`" - assert restify(str) == ":class:`str`" - assert restify(None) == ":obj:`None`" - assert restify(Integral) == ":class:`numbers.Integral`" - assert restify(Struct) == ":class:`struct.Struct`" - assert restify(TracebackType) == ":class:`types.TracebackType`" - assert restify(Any) == ":obj:`~typing.Any`" + assert restify(int) == ":py:class:`int`" + assert restify(str) == ":py:class:`str`" + assert restify(None) == ":py:obj:`None`" + assert restify(Integral) == ":py:class:`numbers.Integral`" + assert restify(Struct) == ":py:class:`struct.Struct`" + assert restify(TracebackType) == ":py:class:`types.TracebackType`" + assert restify(Any) == ":py:obj:`~typing.Any`" + assert restify('str') == "str" def test_restify_type_hints_containers(): - assert restify(List) == ":class:`~typing.List`" - assert restify(Dict) == ":class:`~typing.Dict`" - assert restify(List[int]) == ":class:`~typing.List`\\ [:class:`int`]" - assert restify(List[str]) == ":class:`~typing.List`\\ [:class:`str`]" - assert restify(Dict[str, float]) == (":class:`~typing.Dict`\\ " - "[:class:`str`, :class:`float`]") - assert restify(Tuple[str, str, str]) == (":class:`~typing.Tuple`\\ " - "[:class:`str`, :class:`str`, :class:`str`]") - assert restify(Tuple[str, ...]) == ":class:`~typing.Tuple`\\ [:class:`str`, ...]" - assert restify(Tuple[()]) == ":class:`~typing.Tuple`\\ [()]" - assert restify(List[Dict[str, Tuple]]) == (":class:`~typing.List`\\ " - "[:class:`~typing.Dict`\\ " - "[:class:`str`, :class:`~typing.Tuple`]]") - assert restify(MyList[Tuple[int, int]]) == (":class:`tests.test_util_typing.MyList`\\ " - "[:class:`~typing.Tuple`\\ " - "[:class:`int`, :class:`int`]]") - assert restify(Generator[None, None, None]) == (":class:`~typing.Generator`\\ " - "[:obj:`None`, :obj:`None`, :obj:`None`]") + assert restify(List) == ":py:class:`~typing.List`" + assert restify(Dict) == ":py:class:`~typing.Dict`" + assert restify(List[int]) == ":py:class:`~typing.List`\\ [:py:class:`int`]" + assert restify(List[str]) == ":py:class:`~typing.List`\\ [:py:class:`str`]" + assert restify(Dict[str, float]) == (":py:class:`~typing.Dict`\\ " + "[:py:class:`str`, :py:class:`float`]") + assert restify(Tuple[str, str, str]) == (":py:class:`~typing.Tuple`\\ " + "[:py:class:`str`, :py:class:`str`, " + ":py:class:`str`]") + assert restify(Tuple[str, ...]) == ":py:class:`~typing.Tuple`\\ [:py:class:`str`, ...]" + assert restify(Tuple[()]) == ":py:class:`~typing.Tuple`\\ [()]" + 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`\\ " + "[: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`]") def test_restify_type_hints_Callable(): - assert restify(Callable) == ":class:`~typing.Callable`" + assert restify(Callable) == ":py:class:`~typing.Callable`" if sys.version_info >= (3, 7): - assert restify(Callable[[str], int]) == (":class:`~typing.Callable`\\ " - "[[:class:`str`], :class:`int`]") - assert restify(Callable[..., int]) == (":class:`~typing.Callable`\\ " - "[[...], :class:`int`]") + assert restify(Callable[[str], int]) == (":py:class:`~typing.Callable`\\ " + "[[:py:class:`str`], :py:class:`int`]") + assert restify(Callable[..., int]) == (":py:class:`~typing.Callable`\\ " + "[[...], :py:class:`int`]") else: - assert restify(Callable[[str], int]) == (":class:`~typing.Callable`\\ " - "[:class:`str`, :class:`int`]") - assert restify(Callable[..., int]) == (":class:`~typing.Callable`\\ " - "[..., :class:`int`]") + assert restify(Callable[[str], int]) == (":py:class:`~typing.Callable`\\ " + "[:py:class:`str`, :py:class:`int`]") + assert restify(Callable[..., int]) == (":py:class:`~typing.Callable`\\ " + "[..., :py:class:`int`]") def test_restify_type_hints_Union(): - assert restify(Optional[int]) == ":obj:`~typing.Optional`\\ [:class:`int`]" - assert restify(Union[str, None]) == ":obj:`~typing.Optional`\\ [:class:`str`]" - assert restify(Union[int, str]) == ":obj:`~typing.Union`\\ [:class:`int`, :class:`str`]" + 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`]") if sys.version_info >= (3, 7): - assert restify(Union[int, Integral]) == (":obj:`~typing.Union`\\ " - "[:class:`int`, :class:`numbers.Integral`]") + assert restify(Union[int, Integral]) == (":py:obj:`~typing.Union`\\ " + "[:py:class:`int`, :py:class:`numbers.Integral`]") assert (restify(Union[MyClass1, MyClass2]) == - (":obj:`~typing.Union`\\ " - "[:class:`tests.test_util_typing.MyClass1`, " - ":class:`tests.test_util_typing. `]")) + (":py:obj:`~typing.Union`\\ " + "[:py:class:`tests.test_util_typing.MyClass1`, " + ":py:class:`tests.test_util_typing. `]")) else: - assert restify(Union[int, Integral]) == ":class:`numbers.Integral`" - assert restify(Union[MyClass1, MyClass2]) == ":class:`tests.test_util_typing.MyClass1`" + assert restify(Union[int, Integral]) == ":py:class:`numbers.Integral`" + assert restify(Union[MyClass1, MyClass2]) == ":py:class:`tests.test_util_typing.MyClass1`" @pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.') @@ -109,58 +114,67 @@ def test_restify_type_hints_typevars(): T_co = TypeVar('T_co', covariant=True) T_contra = TypeVar('T_contra', contravariant=True) - assert restify(T) == ":obj:`tests.test_util_typing.T`" - assert restify(T_co) == ":obj:`tests.test_util_typing.T_co`" - assert restify(T_contra) == ":obj:`tests.test_util_typing.T_contra`" - assert restify(List[T]) == ":class:`~typing.List`\\ [:obj:`tests.test_util_typing.T`]" + assert restify(T) == ":py:obj:`tests.test_util_typing.T`" + assert restify(T_co) == ":py:obj:`tests.test_util_typing.T_co`" + assert restify(T_contra) == ":py:obj:`tests.test_util_typing.T_contra`" + assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util_typing.T`]" if sys.version_info >= (3, 10): - assert restify(MyInt) == ":class:`tests.test_util_typing.MyInt`" + assert restify(MyInt) == ":py:class:`tests.test_util_typing.MyInt`" else: - assert restify(MyInt) == ":class:`MyInt`" + assert restify(MyInt) == ":py:class:`MyInt`" def test_restify_type_hints_custom_class(): - assert restify(MyClass1) == ":class:`tests.test_util_typing.MyClass1`" - assert restify(MyClass2) == ":class:`tests.test_util_typing. `" + assert restify(MyClass1) == ":py:class:`tests.test_util_typing.MyClass1`" + assert restify(MyClass2) == ":py:class:`tests.test_util_typing. `" def test_restify_type_hints_alias(): MyStr = str MyTuple = Tuple[str, str] - assert restify(MyStr) == ":class:`str`" - assert restify(MyTuple) == ":class:`~typing.Tuple`\\ [:class:`str`, :class:`str`]" + assert restify(MyStr) == ":py:class:`str`" + assert restify(MyTuple) == ":py:class:`~typing.Tuple`\\ [:py:class:`str`, :py:class:`str`]" @pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.') def test_restify_type_ForwardRef(): from typing import ForwardRef # type: ignore - assert restify(ForwardRef("myint")) == ":class:`myint`" + assert restify(ForwardRef("myint")) == ":py:class:`myint`" @pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') def test_restify_type_Literal(): from typing import Literal # type: ignore - assert restify(Literal[1, "2", "\r"]) == ":obj:`~typing.Literal`\\ [1, '2', '\\r']" + assert restify(Literal[1, "2", "\r"]) == ":py:obj:`~typing.Literal`\\ [1, '2', '\\r']" @pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.') def test_restify_pep_585(): - assert restify(list[str]) == ":class:`list`\\ [:class:`str`]" # type: ignore - assert restify(dict[str, str]) == ":class:`dict`\\ [:class:`str`, :class:`str`]" # type: ignore - assert restify(dict[str, tuple[int, ...]]) == \ - ":class:`dict`\\ [:class:`str`, :class:`tuple`\\ [:class:`int`, ...]]" # type: ignore + assert restify(list[str]) == ":py:class:`list`\\ [:py:class:`str`]" # type: ignore + assert restify(dict[str, str]) == (":py:class:`dict`\\ " # type: ignore + "[:py:class:`str`, :py:class:`str`]") + assert restify(dict[str, tuple[int, ...]]) == (":py:class:`dict`\\ " # type: ignore + "[:py:class:`str`, :py:class:`tuple`\\ " + "[:py:class:`int`, ...]]") @pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.') def test_restify_type_union_operator(): - assert restify(int | None) == ":class:`int` | :obj:`None`" # type: ignore - assert restify(int | str) == ":class:`int` | :class:`str`" # type: ignore - assert restify(int | str | None) == ":class:`int` | :class:`str` | :obj:`None`" # type: ignore + assert restify(int | None) == ":py:class:`int` | :py:obj:`None`" # type: ignore + assert restify(int | str) == ":py:class:`int` | :py:class:`str`" # type: ignore + assert restify(int | str | None) == (":py:class:`int` | :py:class:`str` | " # type: ignore + ":py:obj:`None`") def test_restify_broken_type_hints(): - assert restify(BrokenType) == ':class:`tests.test_util_typing.BrokenType`' + assert restify(BrokenType) == ':py:class:`tests.test_util_typing.BrokenType`' + + +def test_restify_mock(): + with mock(['unknown']): + import unknown + assert restify(unknown.secret.Class) == ':py:class:`unknown.secret.Class`' def test_stringify(): @@ -287,3 +301,9 @@ def test_stringify_type_union_operator(): def test_stringify_broken_type_hints(): assert stringify(BrokenType) == 'tests.test_util_typing.BrokenType' + + +def test_stringify_mock(): + with mock(['unknown']): + import unknown + assert stringify(unknown.secret.Class) == 'unknown.secret.Class' diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 33fb045ce5b..8ec1405adb0 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -11,12 +11,17 @@ import pickle import pytest -from docutils.parsers.rst.directives.html import MetaBody from sphinx import addnodes from sphinx.testing.util import SphinxTestApp from sphinx.versioning import add_uids, get_ratio, merge_doctrees +try: + from docutils.parsers.rst.directives.html import MetaBody + meta = MetaBody.meta +except ImportError: + from docutils.nodes import meta + app = original = original_uids = None @@ -64,7 +69,7 @@ def test_picklablility(): copy.settings.warning_stream = None copy.settings.env = None copy.settings.record_dependencies = None - for metanode in copy.traverse(MetaBody.meta): + for metanode in copy.traverse(meta): metanode.__class__ = addnodes.meta loaded = pickle.loads(pickle.dumps(copy, pickle.HIGHEST_PROTOCOL)) assert all(getattr(n, 'uid', False) for n in loaded.traverse(is_paragraph)) diff --git a/tox.ini b/tox.ini index b6a67fd8529..e703cd646c1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 2.4.0 -envlist = docs,flake8,mypy,twine,coverage,py{36,37,38,39},du{14,15,16,17} +envlist = docs,flake8,mypy,twine,coverage,py{36,37,38,39,310},du{14,15,16,17} [testenv] usedevelop = True @@ -15,7 +15,7 @@ passenv = EPUBCHECK_PATH TERM description = - py{36,37,38,39}: Run unit tests against {envname}. + py{36,37,38,39,310}: Run unit tests against {envname}. du{12,13,14}: Run unit tests with the given version of docutils. deps = du14: docutils==0.14.* @@ -30,6 +30,13 @@ setenv = commands= python -X dev -m pytest --durations 25 {posargs} +[testenv:du-latest] +commands = + git clone https://repo.or.cz/docutils.git {temp_dir}/docutils + python -m pip install {temp_dir}/docutils/docutils + rm -rf {temp_dir}/docutils + {[testenv]commands} + [testenv:flake8] basepython = python3 description =