From 4080be1e82a4942b64360ec3fe8c86438b494ef0 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:58:37 +0200 Subject: [PATCH 01/29] Improve cross-reference documentation (#12944) Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com> --- doc/usage/domains/index.rst | 19 +-- doc/usage/domains/python.rst | 51 ++++++-- doc/usage/referencing.rst | 180 ++++++++++++++++---------- doc/usage/restructuredtext/basics.rst | 2 + 4 files changed, 165 insertions(+), 87 deletions(-) diff --git a/doc/usage/domains/index.rst b/doc/usage/domains/index.rst index 8c47134159f..eda2da3f652 100644 --- a/doc/usage/domains/index.rst +++ b/doc/usage/domains/index.rst @@ -158,19 +158,22 @@ giving the domain name, i.e. :: Cross-referencing syntax ~~~~~~~~~~~~~~~~~~~~~~~~ -For cross-reference roles provided by domains, the same facilities exist as for -general cross-references. See :ref:`xref-syntax`. - +For cross-reference roles provided by domains, +the same :ref:`cross-referencing modifiers ` exist +as for general cross-references. In short: -* You may supply an explicit title and reference target: ``:role:`title - ``` will refer to *target*, but the link text will be *title*. +* You may supply an explicit title and reference target: + ``:py:mod:`mathematical functions ``` will refer to the ``math`` module, + but the link text will be "mathematical functions". -* If you prefix the content with ``!``, no reference/hyperlink will be created. +* If you prefix the content with an exclamation mark (``!``), + no reference/hyperlink will be created. * If you prefix the content with ``~``, the link text will only be the last - component of the target. For example, ``:py:meth:`~Queue.Queue.get``` will - refer to ``Queue.Queue.get`` but only display ``get`` as the link text. + component of the target. + For example, ``:py:meth:`~queue.Queue.get``` will + refer to ``queue.Queue.get`` but only display ``get`` as the link text. Built-in domains ---------------- diff --git a/doc/usage/domains/python.rst b/doc/usage/domains/python.rst index 5b98baa78b0..2ff27c4bb3d 100644 --- a/doc/usage/domains/python.rst +++ b/doc/usage/domains/python.rst @@ -716,18 +716,45 @@ a matching identifier is found: .. versionadded:: 0.4 -The name enclosed in this markup can include a module name and/or a class name. -For example, ``:py:func:`filter``` could refer to a function named ``filter`` -in the current module, or the built-in function of that name. In contrast, -``:py:func:`foo.filter``` clearly refers to the ``filter`` function in the -``foo`` module. - -Normally, names in these roles are searched first without any further -qualification, then with the current module name prepended, then with the -current module and class name (if any) prepended. If you prefix the name with -a dot, this order is reversed. For example, in the documentation of Python's -:mod:`codecs` module, ``:py:func:`open``` always refers to the built-in -function, while ``:py:func:`.open``` refers to :func:`codecs.open`. + +Target specification +^^^^^^^^^^^^^^^^^^^^ + +The target can be specified as a fully qualified name +(e.g. ``:py:meth:`my_module.MyClass.my_method```) +or any shortened version +(e.g. ``:py:meth:`MyClass.my_method``` or ``:py:meth:`my_method```). +See `target resolution`_ for details on the resolution of shortened names. + +:ref:`Cross-referencing modifiers ` can be applied. +In short: + +* You may supply an explicit title and reference target: + ``:py:mod:`mathematical functions ``` will refer to the ``math`` module, + but the link text will be "mathematical functions". + +* If you prefix the content with an exclamation mark (``!``), + no reference/hyperlink will be created. + +* If you prefix the content with ``~``, the link text will only be the last + component of the target. + For example, ``:py:meth:`~queue.Queue.get``` will + refer to ``queue.Queue.get`` but only display ``get`` as the link text. + + +Target resolution +^^^^^^^^^^^^^^^^^ + +A given link target name is resolved to an object using the following strategy: + +Names in these roles are searched first without any further qualification, +then with the current module name prepended, +then with the current module and class name (if any) prepended. + +If you prefix the name with a dot (``.``), this order is reversed. +For example, in the documentation of Python's :py:mod:`codecs` module, +``:py:func:`open``` always refers to the built-in function, +while ``:py:func:`.open``` refers to :func:`codecs.open`. A similar heuristic is used to determine whether the name is an attribute of the currently documented class. diff --git a/doc/usage/referencing.rst b/doc/usage/referencing.rst index 57611660f10..02beaa6b80a 100644 --- a/doc/usage/referencing.rst +++ b/doc/usage/referencing.rst @@ -4,83 +4,84 @@ Cross-referencing syntax ======================== -Cross-references are generated by many semantic interpreted text roles. -Basically, you only need to write ``:role:`target```, and a link will be -created to the item named *target* of the type indicated by *role*. The link's -text will be the same as *target*. - -There are some additional facilities, however, that make cross-referencing -roles more versatile: - -* You may supply an explicit title and reference target, - like in reStructuredText direct hyperlinks: - ``:role:`title ``` will refer to *target*, - but the link text will be *title*. - -* If you prefix the content with ``!``, no reference/hyperlink will be created. - -* If you prefix the content with ``~``, the link text will only be the last - component of the target. For example, ``:py:meth:`~Queue.Queue.get``` will - refer to ``Queue.Queue.get`` but only display ``get`` as the link text. This - does not work with all cross-reference roles, but is domain specific. - - In HTML output, the link's ``title`` attribute (that is e.g. shown as a - tool-tip on mouse-hover) will always be the full target name. - - -.. _any-role: - -Cross-referencing anything --------------------------- - -.. rst:role:: any - - .. versionadded:: 1.3 - - This convenience role tries to do its best to find a valid target for its - reference text. - - * First, it tries standard cross-reference targets that would be referenced - by :rst:role:`doc`, :rst:role:`ref` or :rst:role:`option`. - - Custom objects added to the standard domain by extensions (see - :meth:`.Sphinx.add_object_type`) are also searched. - - * Then, it looks for objects (targets) in all loaded domains. It is up to - the domains how specific a match must be. For example, in the Python - domain a reference of ``:any:`Builder``` would match the - ``sphinx.builders.Builder`` class. - - If none or multiple targets are found, a warning will be emitted. In the - case of multiple targets, you can change "any" to a specific role. - - This role is a good candidate for setting :confval:`default_role`. If you - do, you can write cross-references without a lot of markup overhead. For - example, in this Python function documentation:: - - .. function:: install() - - This function installs a `handler` for every signal known by the - `signal` module. See the section `about-signals` for more information. - - there could be references to a glossary term (usually ``:term:`handler```), a - Python module (usually ``:py:mod:`signal``` or ``:mod:`signal```) and a - section (usually ``:ref:`about-signals```). - - The :rst:role:`any` role also works together with the - :mod:`~sphinx.ext.intersphinx` extension: when no local cross-reference is - found, all object types of intersphinx inventories are also searched. +One of Sphinx's most useful features is creating automatic cross-references +through semantic cross-referencing roles. +A cross reference to an object description, such as ``:func:`spam```, +will create a link to the place where ``spam()`` is documented, +appropriate to each output format (HTML, PDF, ePUB, etc.). + +Sphinx supports various cross-referencing roles to create links +to other elements in the documentation. +In general, writing ``:role:`target``` creates a link to +the object called *target* of the type indicated by *role*. +The link's text depends the role but is often the same as or similar to *target*. + +.. _xref-modifiers: + +The behavior can be modified in the following ways: + +* **Custom link text:** + You can specify the link text explicitly using the same + notation as in reStructuredText :ref:`external links `: + ``:role:`custom text ``` will refer to *target* + and display *custom text* as the text of the link. + +* **Suppressed link:** + Prefixing with an exclamation mark (``!``) prevents the creation of a link + but otherwise keeps the visual output of the role. + + For example, writing ``:py:func:`!target``` displays :py:func:`!target`, + with no link generated. + + This is helpful for cases in which the link target does not exist; + e.g. changelog entries that describe removed functionality, + or third-party libraries that don't support :doc:`intersphinx + `. + Suppressing the link prevents warnings in :confval:`nitpicky` mode. + +* **Modified domain reference:** + When :ref:`referencing domain objects `, + a tilde ``~`` prefix shortens the link text the last component of the target. + For example, ``:py:meth:`~queue.Queue.get``` will + refer to ``queue.Queue.get`` but only display ``get`` as the link text. + + In HTML output, the link's ``title`` attribute + (that is e.g. shown as a tool-tip on mouse-hover) + will always be the full target name. + +Some of the built-in cross-reference roles are: + +* :rst:role:`:any: `, + :rst:role:`:doc: `, + :rst:role:`:ref: ` +* :rst:role:`:confval: `, + :rst:role:`:envvar: `, + :rst:role:`:option:
\n' in content + assert ' © Copyright 2006-2009, Alice.
\n' in content + assert ' © Copyright 2010-2013, Bob.
\n' in content + assert ' © Copyright 2014-2017, Charlie.
\n' in content + assert ' © Copyright 2018-2021, David.
\n' in content + assert ' © Copyright 2022-2025, Eve.' in content + + # check the raw copyright footer block (empty lines included) + assert ( + ' © Copyright 2006.
\n' + ' \n' + ' © Copyright 2006-2009, Alice.
\n' + ' \n' + ' © Copyright 2010-2013, Bob.
\n' + ' \n' + ' © Copyright 2014-2017, Charlie.
\n' + ' \n' + ' © Copyright 2018-2021, David.
\n' + ' \n' + ' © Copyright 2022-2025, Eve.' + ) in content + + +@pytest.mark.sphinx('html', testroot='copyright-multiline') +def test_html_multi_line_copyright_sde(source_date_year, app): + app.build(force_all=True) + + content = (app.outdir / 'index.html').read_text(encoding='utf-8') + + # check the copyright footer line by line (empty lines ignored) + assert f' © Copyright {source_date_year}.
\n' in content + assert f' © Copyright 2006-{source_date_year}, Alice.
\n' in content + assert f' © Copyright 2010-{source_date_year}, Bob.
\n' in content + assert f' © Copyright 2014-{source_date_year}, Charlie.
\n' in content + assert f' © Copyright 2018-{source_date_year}, David.
\n' in content + assert f' © Copyright 2022-{source_date_year}, Eve.' in content + + # check the raw copyright footer block (empty lines included) + assert ( + f' © Copyright {source_date_year}.
\n' + f' \n' + f' © Copyright 2006-{source_date_year}, Alice.
\n' + f' \n' + f' © Copyright 2010-{source_date_year}, Bob.
\n' + f' \n' + f' © Copyright 2014-{source_date_year}, Charlie.
\n' + f' \n' + f' © Copyright 2018-{source_date_year}, David.
\n' + f' \n' + f' © Copyright 2022-{source_date_year}, Eve.' + ) in content diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py index 7f748f11e70..f1d6c12a0fd 100644 --- a/tests/test_config/test_config.py +++ b/tests/test_config/test_config.py @@ -3,7 +3,6 @@ from __future__ import annotations import pickle -import time from collections import Counter from pathlib import Path from typing import TYPE_CHECKING, Any @@ -18,7 +17,6 @@ Config, _Opt, check_confval_types, - correct_copyright_year, is_serializable, ) from sphinx.deprecation import RemovedInSphinx90Warning @@ -741,101 +739,6 @@ def test_conf_py_nitpick_ignore_list(tmp_path): assert cfg.nitpick_ignore_regex == [] -@pytest.fixture( - params=[ - # test with SOURCE_DATE_EPOCH unset: no modification - None, - # test with SOURCE_DATE_EPOCH set: copyright year should be updated - 1293840000, - 1293839999, - ] -) -def source_date_year(request, monkeypatch): - sde = request.param - with monkeypatch.context() as m: - if sde: - m.setenv('SOURCE_DATE_EPOCH', str(sde)) - yield time.gmtime(sde).tm_year - else: - m.delenv('SOURCE_DATE_EPOCH', raising=False) - yield None - - -@pytest.mark.sphinx('html', testroot='copyright-multiline') -def test_multi_line_copyright(source_date_year, app): - app.build(force_all=True) - - content = (app.outdir / 'index.html').read_text(encoding='utf-8') - - if source_date_year is None: - # check the copyright footer line by line (empty lines ignored) - assert ' © Copyright 2006.
\n' in content - assert ' © Copyright 2006-2009, Alice.
\n' in content - assert ' © Copyright 2010-2013, Bob.
\n' in content - assert ' © Copyright 2014-2017, Charlie.
\n' in content - assert ' © Copyright 2018-2021, David.
\n' in content - assert ' © Copyright 2022-2025, Eve.' in content - - # check the raw copyright footer block (empty lines included) - assert ( - ' © Copyright 2006.
\n' - ' \n' - ' © Copyright 2006-2009, Alice.
\n' - ' \n' - ' © Copyright 2010-2013, Bob.
\n' - ' \n' - ' © Copyright 2014-2017, Charlie.
\n' - ' \n' - ' © Copyright 2018-2021, David.
\n' - ' \n' - ' © Copyright 2022-2025, Eve.' - ) in content - else: - # check the copyright footer line by line (empty lines ignored) - assert f' © Copyright {source_date_year}.
\n' in content - assert f' © Copyright 2006-{source_date_year}, Alice.
\n' in content - assert f' © Copyright 2010-{source_date_year}, Bob.
\n' in content - assert f' © Copyright 2014-{source_date_year}, Charlie.
\n' in content - assert f' © Copyright 2018-{source_date_year}, David.
\n' in content - assert f' © Copyright 2022-{source_date_year}, Eve.' in content - - # check the raw copyright footer block (empty lines included) - assert ( - f' © Copyright {source_date_year}.
\n' - f' \n' - f' © Copyright 2006-{source_date_year}, Alice.
\n' - f' \n' - f' © Copyright 2010-{source_date_year}, Bob.
\n' - f' \n' - f' © Copyright 2014-{source_date_year}, Charlie.
\n' - f' \n' - f' © Copyright 2018-{source_date_year}, David.
\n' - 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}) diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py index 815383dfaf4..4d0c70f32e4 100644 --- a/tests/test_config/test_correct_year.py +++ b/tests/test_config/test_correct_year.py @@ -1,20 +1,31 @@ """Test copyright year adjustment""" +import time + import pytest +from sphinx.config import Config, correct_copyright_year + +LT = time.localtime() +LT_NEW = (2009, *LT[1:], LT.tm_zone, LT.tm_gmtoff) +LOCALTIME_2009 = type(LT)(LT_NEW) + @pytest.fixture( params=[ # test with SOURCE_DATE_EPOCH unset: no modification - (None, '2006-2009'), + (None, ''), # test with SOURCE_DATE_EPOCH set: copyright year should be updated - ('1293840000', '2006-2011'), - ('1293839999', '2006-2010'), + ('1293840000', '2011'), + ('1293839999', '2010'), + ('1199145600', '2008'), + ('1199145599', '2007'), ], ) def expect_date(request, monkeypatch): sde, expect = request.param with monkeypatch.context() as m: + m.setattr(time, 'localtime', lambda *a: LOCALTIME_2009) if sde: m.setenv('SOURCE_DATE_EPOCH', sde) else: @@ -22,8 +33,140 @@ def expect_date(request, monkeypatch): yield expect -@pytest.mark.sphinx('html', testroot='correct-year') -def test_correct_year(expect_date, app): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - assert expect_date in content +def test_correct_year(expect_date): + # test that copyright is substituted + copyright_date = '2006-2009, Alice' + cfg = Config({'copyright': copyright_date}, {}) + assert cfg.copyright == copyright_date + correct_copyright_year(None, cfg) # type: ignore[arg-type] + if expect_date: + assert cfg.copyright == f'2006-{expect_date}, Alice' + else: + assert cfg.copyright == copyright_date + + +def test_correct_year_space(expect_date): + # test that copyright is substituted + copyright_date = '2006-2009 Alice' + cfg = Config({'copyright': copyright_date}, {}) + assert cfg.copyright == copyright_date + correct_copyright_year(None, cfg) # type: ignore[arg-type] + if expect_date: + assert cfg.copyright == f'2006-{expect_date} Alice' + else: + assert cfg.copyright == copyright_date + + +def test_correct_year_no_author(expect_date): + # test that copyright is substituted + copyright_date = '2006-2009' + cfg = Config({'copyright': copyright_date}, {}) + assert cfg.copyright == copyright_date + correct_copyright_year(None, cfg) # type: ignore[arg-type] + if expect_date: + assert cfg.copyright == f'2006-{expect_date}' + else: + assert cfg.copyright == copyright_date + + +def test_correct_year_single(expect_date): + # test that copyright is substituted + copyright_date = '2009, Alice' + cfg = Config({'copyright': copyright_date}, {}) + assert cfg.copyright == copyright_date + correct_copyright_year(None, cfg) # type: ignore[arg-type] + if expect_date: + assert cfg.copyright == f'{expect_date}, Alice' + else: + assert cfg.copyright == copyright_date + + +def test_correct_year_single_space(expect_date): + # test that copyright is substituted + copyright_date = '2009 Alice' + cfg = Config({'copyright': copyright_date}, {}) + assert cfg.copyright == copyright_date + correct_copyright_year(None, cfg) # type: ignore[arg-type] + if expect_date: + assert cfg.copyright == f'{expect_date} Alice' + else: + assert cfg.copyright == copyright_date + + +def test_correct_year_single_no_author(expect_date): + # test that copyright is substituted + copyright_date = '2009' + cfg = Config({'copyright': copyright_date}, {}) + assert cfg.copyright == copyright_date + correct_copyright_year(None, cfg) # type: ignore[arg-type] + if expect_date: + assert cfg.copyright == f'{expect_date}' + else: + assert cfg.copyright == copyright_date + + +def test_correct_year_multi_line(expect_date): + # test that copyright is substituted + copyright_dates = ( + '2006', + '2006-2009, Alice', + '2010-2013, Bob', + '2014-2017, Charlie', + '2018-2021, David', + '2022-2025, Eve', + ) + cfg = Config({'copyright': copyright_dates}, {}) + assert cfg.copyright == copyright_dates + correct_copyright_year(None, cfg) # type: ignore[arg-type] + if expect_date: + assert cfg.copyright == ( + f'{expect_date}', + f'2006-{expect_date}, Alice', + f'2010-{expect_date}, Bob', + f'2014-{expect_date}, Charlie', + f'2018-{expect_date}, David', + f'2022-{expect_date}, Eve', + ) + else: + assert cfg.copyright == copyright_dates + + +def test_correct_year_multi_line_all_formats(expect_date): + # test that copyright is substituted + copyright_dates = ( + '2009', + '2009 Alice', + '2009, Bob', + '2006-2009', + '2006-2009 Charlie', + '2006-2009, David', + ) + cfg = Config({'copyright': copyright_dates}, {}) + assert cfg.copyright == copyright_dates + correct_copyright_year(None, cfg) # type: ignore[arg-type] + if expect_date: + assert cfg.copyright == ( + f'{expect_date}', + f'{expect_date} Alice', + f'{expect_date}, Bob', + f'2006-{expect_date}', + f'2006-{expect_date} Charlie', + f'2006-{expect_date}, David', + ) + else: + assert cfg.copyright == copyright_dates + + +def test_correct_year_app(expect_date, tmp_path, make_app): + # integration test + copyright_date = '2006-2009, Alice' + (tmp_path / 'conf.py').touch() + app = make_app( + 'dummy', + srcdir=tmp_path, + confoverrides={'copyright': copyright_date}, + ) + if expect_date: + assert app.config.copyright == f'2006-{expect_date}, Alice' + else: + assert app.config.copyright == copyright_date From 5eb68b2bff559838e4bac2e30b4eabb2d1929d51 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 3 Oct 2024 22:20:55 +0100 Subject: [PATCH 07/29] Remove list of cross-reference roles --- doc/usage/referencing.rst | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/doc/usage/referencing.rst b/doc/usage/referencing.rst index 02beaa6b80a..a3fccdc7ce1 100644 --- a/doc/usage/referencing.rst +++ b/doc/usage/referencing.rst @@ -49,26 +49,6 @@ The behavior can be modified in the following ways: (that is e.g. shown as a tool-tip on mouse-hover) will always be the full target name. -Some of the built-in cross-reference roles are: - -* :rst:role:`:any: `, - :rst:role:`:doc: `, - :rst:role:`:ref: ` -* :rst:role:`:confval: `, - :rst:role:`:envvar: `, - :rst:role:`:option: