diff --git a/CHANGES b/CHANGES index 5637f66d6..600efc466 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,8 @@ Deprecated * ``sphinx.builders.latex.LaTeXBuilder.usepackages`` * ``sphinx.builders.latex.LaTeXBuilder.usepackages_afger_hyperref`` +* ``sphinx.ext.autodoc.SingledispatchFunctionDocumenter`` +* ``sphinx.ext.autodoc.SingledispatchMethodDocumenter`` Features added -------------- @@ -22,11 +24,14 @@ Features added nested declarations. * #8081: LaTeX: Allow to add LaTeX package via ``app.add_latex_package()`` until just before writing .tex file +* #7996: manpage: Add :confval:`man_make_section_directory` to make a section + directory on build man page Bugs fixed ---------- * #8085: i18n: Add support for having single text domain +* #6640: i18n: Failed to override system message translation * #8143: autodoc: AttributeError is raised when False value is passed to autodoc_default_options * #8103: autodoc: functools.cached_property is not considered as a property @@ -34,15 +39,22 @@ Bugs fixed by string not ending with blank lines * #8142: autodoc: Wrong constructor signature for the class derived from typing.Generic +* #8157: autodoc: TypeError is raised when annotation has invalid __args__ +* #7964: autodoc: Tuple in default value is wrongly rendered * #8200: autodoc: type aliases break type formatting of autoattribute * #8192: napoleon: description is disappeared when it contains inline literals * #8142: napoleon: Potential of regex denial of service in google style docs * #8169: LaTeX: pxjahyper loaded even when latex_engine is not platex * #8175: intersphinx: Potential of regex denial of service by broken inventory +* #8277: sphinx-build: missing and redundant spacing (and etc) for console + output on building +* #7973: imgconverter: Check availability of imagemagick many times * #8093: The highlight warning has wrong location in some builders (LaTeX, singlehtml and so on) * #8239: Failed to refer a token in productionlist if it is indented * #8268: linkcheck: Report HTTP errors when ``linkcheck_anchors`` is ``True`` +* #8245: linkcheck: take source directory into account for local files +* #6914: figure numbers are unexpectedly assigned to uncaptioned items Testing -------- diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index d7ad21fff..2bb8aebfd 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -36,6 +36,16 @@ The following is a list of deprecated interfaces. - 5.0 - N/A + * - ``sphinx.ext.autodoc.SingledispatchFunctionDocumenter`` + - 3.3 + - 5.0 + - ``sphinx.ext.autodoc.FunctionDocumenter`` + + * - ``sphinx.ext.autodoc.SingledispatchMethodDocumenter`` + - 3.3 + - 5.0 + - ``sphinx.ext.autodoc.MethodDocumenter`` + * - ``sphinx.ext.autodoc.members_set_option()`` - 3.2 - 5.0 diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 3fc3a5306..270fcbf33 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -2245,6 +2245,12 @@ These options influence manual page output. .. versionadded:: 1.1 +.. confval:: man_make_section_directory + + If true, make a section directory on build man page. Default is False. + + .. versionadded:: 3.3 + .. _texinfo-options: diff --git a/sphinx/application.py b/sphinx/application.py index 385b74d8a..f91027bf7 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -18,7 +18,7 @@ import warnings from collections import deque from io import StringIO from os import path -from typing import Any, Callable, Dict, IO, List, Tuple, Union +from typing import Any, Callable, Dict, IO, List, Optional, Tuple, Union from docutils import nodes from docutils.nodes import Element, TextElement @@ -293,7 +293,10 @@ class Sphinx: if catalog.domain == 'sphinx' and catalog.is_outdated(): catalog.write_mo(self.config.language) - locale_dirs = [None, path.join(package_dir, 'locale')] + list(repo.locale_dirs) + locale_dirs = [None] # type: List[Optional[str]] + locale_dirs += list(repo.locale_dirs) + locale_dirs += [path.join(package_dir, 'locale')] + self.translator, has_translation = locale.init(locale_dirs, self.config.language) if has_translation or self.config.language == 'en': # "en" never needs to be translated diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index c30aa9cfd..beb650991 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -641,17 +641,17 @@ class StandaloneHTMLBuilder(Builder): def gen_additional_pages(self) -> None: # additional pages from conf.py for pagename, template in self.config.html_additional_pages.items(): - logger.info(' ' + pagename, nonl=True) + logger.info(pagename + ' ', nonl=True) self.handle_page(pagename, {}, template) # the search page if self.search: - logger.info(' search', nonl=True) + logger.info('search ', nonl=True) self.handle_page('search', {}, 'search.html') # the opensearch xml file if self.config.html_use_opensearch and self.search: - logger.info(' opensearch', nonl=True) + logger.info('opensearch ', nonl=True) fn = path.join(self.outdir, '_static', 'opensearch.xml') self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn) @@ -669,7 +669,7 @@ class StandaloneHTMLBuilder(Builder): 'genindexcounts': indexcounts, 'split_index': self.config.html_split_index, } - logger.info(' genindex', nonl=True) + logger.info('genindex ', nonl=True) if self.config.html_split_index: self.handle_page('genindex', genindexcontext, @@ -691,7 +691,7 @@ class StandaloneHTMLBuilder(Builder): 'content': content, 'collapse_index': collapse, } - logger.info(' ' + indexname, nonl=True) + logger.info(indexname + ' ', nonl=True) self.handle_page(indexname, indexcontext, 'domainindex.html') def copy_image_files(self) -> None: @@ -785,7 +785,7 @@ class StandaloneHTMLBuilder(Builder): def copy_static_files(self) -> None: try: - with progress_message(__('copying static files... ')): + with progress_message(__('copying static files')): ensuredir(path.join(self.outdir, '_static')) # prepare context for templates diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 1083e82ec..a9e6b05b0 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -211,7 +211,7 @@ class CheckExternalLinksBuilder(Builder): else: return 'redirected', new_url, 0 - def check() -> Tuple[str, str, int]: + def check(docname: str) -> Tuple[str, str, int]: # check for various conditions without bothering the network if len(uri) == 0 or uri.startswith(('#', 'mailto:')): return 'unchecked', '', 0 @@ -220,7 +220,8 @@ class CheckExternalLinksBuilder(Builder): # non supported URI schemes (ex. ftp) return 'unchecked', '', 0 else: - if path.exists(path.join(self.srcdir, uri)): + srcdir = path.dirname(self.env.doc2path(docname)) + if path.exists(path.join(srcdir, uri)): return 'working', '', 0 else: for rex in self.to_ignore: @@ -257,7 +258,7 @@ class CheckExternalLinksBuilder(Builder): uri, docname, lineno = self.wqueue.get() if uri is None: break - status, info, code = check() + status, info, code = check(docname) self.rqueue.put((uri, docname, lineno, status, info, code)) def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None: diff --git a/sphinx/builders/manpage.py b/sphinx/builders/manpage.py index 4166dece9..2a10ba62f 100644 --- a/sphinx/builders/manpage.py +++ b/sphinx/builders/manpage.py @@ -24,7 +24,7 @@ from sphinx.util import logging from sphinx.util import progress_message from sphinx.util.console import darkgreen # type: ignore from sphinx.util.nodes import inline_all_toctrees -from sphinx.util.osutil import make_filename_from_project +from sphinx.util.osutil import ensuredir, make_filename_from_project from sphinx.writers.manpage import ManualPageWriter, ManualPageTranslator @@ -80,7 +80,12 @@ class ManualPageBuilder(Builder): docsettings.authors = authors docsettings.section = section - targetname = '%s.%s' % (name, section) + if self.config.man_make_section_directory: + ensuredir(path.join(self.outdir, str(section))) + targetname = '%s/%s.%s' % (section, name, section) + else: + targetname = '%s.%s' % (name, section) + logger.info(darkgreen(targetname) + ' { ', nonl=True) destination = FileOutput( destination_path=path.join(self.outdir, targetname), @@ -115,6 +120,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('man_pages', default_man_pages, None) app.add_config_value('man_show_urls', False, None) + app.add_config_value('man_make_section_directory', False, None) return { 'version': 'builtin', diff --git a/sphinx/environment/collectors/toctree.py b/sphinx/environment/collectors/toctree.py index e168bd9c4..d6cdc8354 100644 --- a/sphinx/environment/collectors/toctree.py +++ b/sphinx/environment/collectors/toctree.py @@ -224,6 +224,10 @@ class TocTreeCollector(EnvironmentCollector): def get_figtype(node: Node) -> str: for domain in env.domains.values(): figtype = domain.get_enumerable_node_type(node) + if domain.name == 'std' and not domain.get_numfig_title(node): # type: ignore + # Skip if uncaptioned node + continue + if figtype: return figtype diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 83c44c8df..434fc40e9 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1302,6 +1302,11 @@ class SingledispatchFunctionDocumenter(FunctionDocumenter): Retained for backwards compatibility, now does the same as the FunctionDocumenter """ + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn("%s is deprecated." % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + super().__init__(*args, **kwargs) + class DecoratorDocumenter(FunctionDocumenter): """ @@ -1938,6 +1943,11 @@ class SingledispatchMethodDocumenter(MethodDocumenter): Retained for backwards compatibility, now does the same as the MethodDocumenter """ + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn("%s is deprecated." % self.__class__.__name__, + RemovedInSphinx50Warning, stacklevel=2) + super().__init__(*args, **kwargs) + class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # type: ignore """ diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index 385ca3566..5210dc725 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -106,7 +106,7 @@ class _TranslationProxy(UserString): translators = defaultdict(NullTranslations) # type: Dict[Tuple[str, str], NullTranslations] -def init(locale_dirs: List[str], language: str, +def init(locale_dirs: List[Optional[str]], language: str, catalog: str = 'sphinx', namespace: str = 'general') -> Tuple[NullTranslations, bool]: """Look for message catalogs in `locale_dirs` and *ensure* that there is at least a NullTranslations catalog set in `translators`. If called multiple diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py index 9bafff11c..2583448d5 100644 --- a/sphinx/pycode/ast.py +++ b/sphinx/pycode/ast.py @@ -166,14 +166,28 @@ class _UnparseVisitor(ast.NodeVisitor): return "{" + ", ".join(self.visit(e) for e in node.elts) + "}" def visit_Subscript(self, node: ast.Subscript) -> str: - return "%s[%s]" % (self.visit(node.value), self.visit(node.slice)) + def is_simple_tuple(value: ast.AST) -> bool: + return ( + isinstance(value, ast.Tuple) and + bool(value.elts) and + not any(isinstance(elt, ast.Starred) for elt in value.elts) + ) + + if is_simple_tuple(node.slice): + elts = ", ".join(self.visit(e) for e in node.slice.elts) # type: ignore + return "%s[%s]" % (self.visit(node.value), elts) + elif isinstance(node.slice, ast.Index) and is_simple_tuple(node.slice.value): + elts = ", ".join(self.visit(e) for e in node.slice.value.elts) # type: ignore + return "%s[%s]" % (self.visit(node.value), elts) + else: + return "%s[%s]" % (self.visit(node.value), self.visit(node.slice)) def visit_UnaryOp(self, node: ast.UnaryOp) -> str: return "%s %s" % (self.visit(node.op), self.visit(node.operand)) def visit_Tuple(self, node: ast.Tuple) -> str: if node.elts: - return ", ".join(self.visit(e) for e in node.elts) + return "(" + ", ".join(self.visit(e) for e in node.elts) + ")" else: return "()" diff --git a/sphinx/transforms/post_transforms/images.py b/sphinx/transforms/post_transforms/images.py index f2cacac3c..949c09dde 100644 --- a/sphinx/transforms/post_transforms/images.py +++ b/sphinx/transforms/post_transforms/images.py @@ -11,7 +11,7 @@ import os import re from math import ceil -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Optional, Tuple from docutils import nodes @@ -175,6 +175,13 @@ class ImageConverter(BaseImageConverter): """ default_priority = 200 + #: The converter is available or not. Will be filled at the first call of + #: the build. The result is shared in the same process. + #: + #: .. todo:: This should be refactored not to store the state without class + #: variable. + available = None # type: Optional[bool] + #: A conversion rules the image converter supports. #: It is represented as a list of pair of source image format (mimetype) and #: destination one:: @@ -187,16 +194,14 @@ class ImageConverter(BaseImageConverter): conversion_rules = [] # type: List[Tuple[str, str]] def __init__(self, *args: Any, **kwargs: Any) -> None: - self.available = None # type: bool - # the converter is available or not. - # Will be checked at first conversion super().__init__(*args, **kwargs) def match(self, node: nodes.image) -> bool: if not self.app.builder.supported_image_types: return False elif self.available is None: - self.available = self.is_available() + # store the value to the class variable to share it during the build + self.__class__.available = self.is_available() if not self.available: return False diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 4dac3b695..8eca67220 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -109,7 +109,10 @@ def _stringify_py37(annotation: Any) -> str: return repr(annotation) if getattr(annotation, '__args__', None): - if qualname == 'Union': + if not isinstance(annotation.__args__, (list, tuple)): + # broken __args__ found + pass + elif qualname == 'Union': if len(annotation.__args__) > 1 and annotation.__args__[-1] is NoneType: if len(annotation.__args__) > 2: args = ', '.join(stringify(a) for a in annotation.__args__[:-1]) diff --git a/tests/test_build_manpage.py b/tests/test_build_manpage.py index c3c41a2ae..d4b1a320e 100644 --- a/tests/test_build_manpage.py +++ b/tests/test_build_manpage.py @@ -30,6 +30,13 @@ def test_all(app, status, warning): assert 'Footnotes' not in content +@pytest.mark.sphinx('man', testroot='basic', + confoverrides={'man_make_section_directory': True}) +def test_man_make_section_directory(app, status, warning): + app.build() + assert (app.outdir / '1' / 'python.1').exists() + + @pytest.mark.sphinx('man', testroot='directive-code') def test_captioned_code_block(app, status, warning): app.builder.build_all() diff --git a/tests/test_intl.py b/tests/test_intl.py index 1d1282baa..c0b87d5ce 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -14,8 +14,10 @@ import re import pytest from babel.messages import pofile, mofile +from babel.messages.catalog import Catalog from docutils import nodes +from sphinx import locale from sphinx.testing.util import ( path, etree_parse, strip_escseq, assert_re_search, assert_not_re_search, assert_startswith, assert_node @@ -1289,3 +1291,30 @@ def test_image_glob_intl_using_figure_language_filename(app): def getwarning(warnings): return strip_escseq(warnings.getvalue().replace(os.sep, '/')) + + +@pytest.mark.sphinx('html', testroot='basic', confoverrides={'language': 'de'}) +def test_customize_system_message(make_app, app_params, sphinx_test_tempdir): + try: + # clear translators cache + locale.translators.clear() + + # prepare message catalog (.po) + locale_dir = sphinx_test_tempdir / 'basic' / 'locales' / 'de' / 'LC_MESSAGES' + locale_dir.makedirs() + with (locale_dir / 'sphinx.po').open('wb') as f: + catalog = Catalog() + catalog.add('Quick search', 'QUICK SEARCH') + pofile.write_po(f, catalog) + + # construct application and convert po file to .mo + args, kwargs = app_params + app = make_app(*args, **kwargs) + assert (locale_dir / 'sphinx.mo').exists() + assert app.translator.gettext('Quick search') == 'QUICK SEARCH' + + app.build() + content = (app.outdir / 'index.html').read_text() + assert 'QUICK SEARCH' in content + finally: + locale.translators.clear() diff --git a/tests/test_pycode_ast.py b/tests/test_pycode_ast.py index 9b12d24d5..32a784b74 100644 --- a/tests/test_pycode_ast.py +++ b/tests/test_pycode_ast.py @@ -53,7 +53,7 @@ from sphinx.pycode import ast ("+ a", "+ a"), # UAdd ("- 1", "- 1"), # UnaryOp ("- a", "- a"), # USub - ("(1, 2, 3)", "1, 2, 3"), # Tuple + ("(1, 2, 3)", "(1, 2, 3)"), # Tuple ("()", "()"), # Tuple (empty) ]) def test_unparse(source, expected): diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index 932fdbfc0..3f8ddbb37 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -32,6 +32,10 @@ class MyList(List[T]): pass +class BrokenType: + __args__ = int + + def test_stringify(): assert stringify(int) == "int" assert stringify(str) == "str" @@ -113,3 +117,7 @@ def test_stringify_type_hints_alias(): MyTuple = Tuple[str, str] assert stringify(MyStr) == "str" assert stringify(MyTuple) == "Tuple[str, str]" # type: ignore + + +def test_stringify_broken_type_hints(): + assert stringify(BrokenType) == 'test_util_typing.BrokenType'