diff options
62 files changed, 595 insertions, 246 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml index 7f4de4ae0..6b5c7379b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,6 +9,6 @@ jobs: - run: /python3.6/bin/pip install -U pip setuptools - run: /python3.6/bin/pip install -U .[test] - run: mkdir -p test-reports/pytest - - run: make test PYTHON=/python3.6/bin/python TEST=--junitxml=test-reports/pytest/results.xml + - run: make test PYTHON=/python3.6/bin/python TEST="--junitxml=test-reports/pytest/results.xml -vv" - store_test_results: path: test-reports diff --git a/.github/workflows/builddoc.yml b/.github/workflows/builddoc.yml new file mode 100644 index 000000000..809fb68e6 --- /dev/null +++ b/.github/workflows/builddoc.yml @@ -0,0 +1,21 @@ +name: Build document + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.6 + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y graphviz + pip install -U tox + - name: Run Tox + run: tox -e docs diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..fea1f17a2 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +name: Lint source code + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + tool: [docslint, flake8, mypy, twine] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.6 + - name: Install dependencies + run: pip install -U tox + - name: Run Tox + run: tox -e ${{ matrix.tool }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fa2c1154a..7acfef6d2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,4 +18,4 @@ jobs: - name: Install dependencies run: pip install -U tox - name: Run Tox - run: tox -e py + run: tox -e py -- -vv diff --git a/.travis.yml b/.travis.yml index 61f400f63..f9c4cb714 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,14 +27,6 @@ jobs: - python: 'nightly' env: - TOXENV=py310 - - python: '3.6' - env: TOXENV=docs - - python: '3.6' - env: TOXENV=docslint - - python: '3.6' - env: TOXENV=mypy - - python: '3.6' - env: TOXENV=flake8 - language: node_js node_js: '10.7' @@ -47,7 +39,7 @@ install: - if [ $IS_PYTHON = false ]; then npm install; fi script: - - if [ $IS_PYTHON = true ]; then tox -- -v; fi + - if [ $IS_PYTHON = true ]; then tox -- -vv; fi - if [ $IS_PYTHON = false ]; then npm test; fi after_success: @@ -51,10 +51,24 @@ Deprecated Features added -------------- +* #7853: C and C++, support parameterized GNU style attributes. +* #7888: napoleon: Add aliases Warn and Raise. +* C, added :rst:dir:`c:alias` directive for inserting copies + of existing declarations. + Bugs fixed ---------- +* #7839: autosummary: cannot handle umlauts in function names +* #7865: autosummary: Failed to extract summary line when abbreviations found +* #7866: autosummary: Failed to extract correct summary line when docstring + contains a hyperlink target * #7715: LaTeX: ``numfig_secnum_depth > 1`` leads to wrong figure links +* #7846: html theme: XML-invalid files were generated +* #7869: :rst:role:`abbr` role without an explanation will show the explanation + from the previous abbr role +* C and C++, removed ``noindex`` directive option as it did + nothing. Testing -------- @@ -77,6 +91,17 @@ Features added Bugs fixed ---------- +* #7844: autodoc: Failed to detect module when relative module name given +* #7856: autodoc: AttributeError is raised when non-class object is given to + the autoclass directive +* #7850: autodoc: KeyError is raised for invalid mark up when autodoc_typehints + is 'description' +* #7812: autodoc: crashed if the target name matches to both an attribute and + module that are same name +* #7812: autosummary: generates broken stub files if the target code contains + an attribute and module that are same name +* #7806: viewcode: Failed to resolve viewcode references on 3rd party builders + Testing -------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 73ea1e52e..a20192679 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -8,10 +8,10 @@ reports/feature requests. Our contributing guide can be found online at: -https://www.sphinx-doc.org/en/master/internals/contributing/ +https://www.sphinx-doc.org/en/master/internals/contributing.html You can also browse it from this repository from -``doc/internals/contributing/`` +``doc/internals/contributing.rst`` Sphinx uses GitHub to host source code, track patches and bugs, and more. Please make an effort to provide as much possible when filing bugs. @@ -1,7 +1,7 @@ License for Sphinx ================== -Copyright (c) 2007-2019 by the Sphinx team (see AUTHORS file). +Copyright (c) 2007-2020 by the Sphinx team (see AUTHORS file). All rights reserved. Redistribution and use in source and binary forms, with or without @@ -83,6 +83,6 @@ build: .PHONY: docs docs: ifndef target - $(info You need to give a provide a target variable, e.g. `make docs target=html`.) + $(info You need to provide a target variable, e.g. `make docs target=html`.) endif $(MAKE) -C doc $(target) diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index d5b1a8a98..036b57ec1 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -157,6 +157,40 @@ connect handlers to the events. Example: app.connect('source-read', source_read_handler) +Below is an overview of each event that happens during a build. In the list +below, we include the event name, its callback parameters, and the input and output +type for that event:: + + 1. event.config-inited(app,config) + 2. event.builder-inited(app) + 3. event.env-get-outdated(app, env, added, changed, removed) + 4. event.env-before-read-docs(app, env, docnames) + + for docname in docnames: + 5. event.env-purge-doc(app, env, docname) + if doc changed and not removed: + 6. source-read(app, docname, source) + 7. run source parsers: text -> docutils.document (parsers can be added with the app.add_source_parser() API) + 8. apply transforms (by priority): docutils.document -> docutils.document + - event.doctree-read(app, doctree) is called in the middly of transforms, + transforms come before/after this event depending on their priority. + 9. (if running in parallel mode, for each process) event.env-merged-info(app, env, docnames, other) + 10. event.env-updated(app, env) + 11. event.env-get-updated(app, env) + 11. event.env-check-consistency(app, env) + + # For builders that output a single page, they are first joined into a single doctree before post-transforms/doctree-resolved + for docname in docnames: + 12. apply post-transforms (by priority): docutils.document -> docutils.document + 13. event.doctree-resolved(app, doctree, docname) + - (for any reference node that fails to resolve) event.missing-reference(env, node, contnode) + + 14. Generate output files + + 15. event.build-finished(app, exception) + +Here is a more detailed list of these events. + .. event:: builder-inited (app) Emitted when the builder object has been created. It is available as diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 51ae2bd1a..76b50ce2e 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -659,10 +659,11 @@ documentation on :ref:`intl` for details. generated by Sphinx will be in that language. Also, Sphinx will try to substitute individual paragraphs from your documents with the translation sets obtained from :confval:`locale_dirs`. Sphinx will search - language-specific figures named by `figure_language_filename` and substitute - them for original figures. In the LaTeX builder, a suitable language will - be selected as an option for the *Babel* package. Default is ``None``, - which means that no translation will be done. + language-specific figures named by `figure.language.filename` + (e.g. the German version of ``myfigure.png`` will be ``myfigure.de.png``) + and substitute them for original figures. In the LaTeX builder, a suitable + language will be selected as an option for the *Babel* package. Default is + ``None``, which means that no translation will be done. .. versionadded:: 0.5 diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index ec1d6c9b5..150b62c4d 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -280,6 +280,12 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, .. versionadded:: 1.3 + * As a hint to autodoc extension, you can put a ``::`` separator in between + module name and object name to let autodoc know the correct module name if + it is ambiguous. :: + + .. autoclass:: module.name::Noodle + .. rst:directive:: autofunction autodecorator diff --git a/doc/usage/extensions/napoleon.rst b/doc/usage/extensions/napoleon.rst index b12dd03ba..76c423dc0 100644 --- a/doc/usage/extensions/napoleon.rst +++ b/doc/usage/extensions/napoleon.rst @@ -115,6 +115,7 @@ All of the following section headers are supported: * ``Parameters`` * ``Return`` *(alias of Returns)* * ``Returns`` + * ``Raise`` *(alias of Raises)* * ``Raises`` * ``References`` * ``See Also`` @@ -122,6 +123,7 @@ All of the following section headers are supported: * ``Todo`` * ``Warning`` * ``Warnings`` *(alias of Warning)* + * ``Warn`` *(alias of Warns)* * ``Warns`` * ``Yield`` *(alias of Yields)* * ``Yields`` diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index 59e273c93..2d4247fee 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -739,6 +739,41 @@ Explicit ref: :c:var:`Data.@data.a`. Short-hand ref: :c:var:`Data.a`. .. versionadded:: 3.0 +Aliasing Declarations +~~~~~~~~~~~~~~~~~~~~~ + +.. c:namespace-push:: @alias + +Sometimes it may be helpful list declarations elsewhere than their main +documentation, e.g., when creating a synopsis of an interface. +The following directive can be used for this purpose. + +.. rst:directive:: .. c:alias:: name + + Insert one or more alias declarations. Each entity can be specified + as they can in the :rst:role:`c:any` role. + + For example:: + + .. c:var:: int data + .. c:function:: int f(double k) + + .. c:alias:: data + f + + becomes + + .. c:var:: int data + .. c:function:: int f(double k) + + .. c:alias:: data + f + + .. versionadded:: 3.2 + +.. c:namespace-pop:: + + Inline Expressions and Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1078,7 +1113,6 @@ Options Some directives support options: -- ``:noindex:``, see :ref:`basic-domain-markup`. - ``:tparam-line-spec:``, for templated declarations. If specified, each template parameter will be rendered on a separate line. diff --git a/sphinx/builders/gettext.py b/sphinx/builders/gettext.py index cfc615847..eef5f072f 100644 --- a/sphinx/builders/gettext.py +++ b/sphinx/builders/gettext.py @@ -253,7 +253,7 @@ class MessageCatalogBuilder(I18nBuilder): origin = MsgOrigin(template, line) self.catalogs['sphinx'].add(msg, origin) except Exception as exc: - raise ThemeError('%s: %r' % (template, exc)) + raise ThemeError('%s: %r' % (template, exc)) from exc def build(self, docnames: Iterable[str], summary: str = None, method: str = 'update') -> None: # NOQA self._extract_from_template() diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 440ee1f1e..626f173f5 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -135,7 +135,7 @@ class BuildInfo: build_info.tags_hash = lines[3].split()[1].strip() return build_info except Exception as exc: - raise ValueError(__('build info file is broken: %r') % exc) + raise ValueError(__('build info file is broken: %r') % exc) from exc def __init__(self, config: Config = None, tags: Tags = None, config_categories: List[str] = []) -> None: # NOQA self.config_hash = '' @@ -1010,7 +1010,7 @@ class StandaloneHTMLBuilder(Builder): return except Exception as exc: raise ThemeError(__("An error happened in rendering the page %s.\nReason: %r") % - (pagename, exc)) + (pagename, exc)) from exc if not outfilename: outfilename = self.get_outfilename(pagename) diff --git a/sphinx/builders/latex/theming.py b/sphinx/builders/latex/theming.py index da6a7fa04..130bded4a 100644 --- a/sphinx/builders/latex/theming.py +++ b/sphinx/builders/latex/theming.py @@ -87,10 +87,12 @@ class UserTheme(Theme): try: value = self.config.get('theme', key) setattr(self, key, value) - except configparser.NoSectionError: - raise ThemeError(__('%r doesn\'t have "theme" setting') % filename) + except configparser.NoSectionError as exc: + raise ThemeError(__('%r doesn\'t have "theme" setting') % + filename) from exc except configparser.NoOptionError as exc: - raise ThemeError(__('%r doesn\'t have "%s" setting') % (filename, exc.args[0])) + raise ThemeError(__('%r doesn\'t have "%s" setting') % + (filename, exc.args[0])) from exc for key in self.OPTIONAL_CONFIG_KEYS: try: diff --git a/sphinx/config.py b/sphinx/config.py index 8206653ab..a6d070e3f 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -191,9 +191,9 @@ class Config: elif isinstance(defvalue, int): try: return int(value) - except ValueError: + except ValueError as exc: raise ValueError(__('invalid number %r for config value %r, ignoring') % - (value, name)) + (value, name)) from exc elif hasattr(defvalue, '__call__'): return value elif defvalue is not None and not isinstance(defvalue, str): @@ -316,17 +316,17 @@ def eval_config_file(filename: str, tags: Tags) -> Dict[str, Any]: exec(code, namespace) except SyntaxError as err: msg = __("There is a syntax error in your configuration file: %s\n") - raise ConfigError(msg % err) - except SystemExit: + raise ConfigError(msg % err) from err + except SystemExit as exc: msg = __("The configuration file (or one of the modules it imports) " "called sys.exit()") - raise ConfigError(msg) + raise ConfigError(msg) from exc except ConfigError: # pass through ConfigError from conf.py as is. It will be shown in console. raise - except Exception: + except Exception as exc: msg = __("There is a programmable error in your configuration file:\n\n%s") - raise ConfigError(msg % traceback.format_exc()) + raise ConfigError(msg % traceback.format_exc()) from exc return namespace diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index 36a8f1f65..8a0b8a105 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -10,7 +10,7 @@ import re from typing import ( - Any, Callable, Dict, Generator, Iterator, List, Type, Tuple, Union + Any, Callable, Dict, Generator, Iterator, List, Type, TypeVar, Tuple, Union ) from typing import cast @@ -26,9 +26,12 @@ from sphinx.domains import Domain, ObjType from sphinx.environment import BuildEnvironment from sphinx.locale import _, __ from sphinx.roles import SphinxRole, XRefRole +from sphinx.transforms import SphinxTransform +from sphinx.transforms.post_transforms import ReferencesResolver from sphinx.util import logging from sphinx.util.cfamily import ( - NoOldIdError, ASTBaseBase, verify_description_mode, StringifyTransform, + NoOldIdError, ASTBaseBase, ASTBaseParenExprList, + verify_description_mode, StringifyTransform, BaseParser, DefinitionError, UnsupportedMultiCharacterCharLiteral, identifier_re, anon_identifier_re, integer_literal_re, octal_literal_re, hex_literal_re, binary_literal_re, integers_literal_suffix_re, @@ -40,6 +43,7 @@ from sphinx.util.docutils import SphinxDirective from sphinx.util.nodes import make_refnode logger = logging.getLogger(__name__) +T = TypeVar('T') # https://en.cppreference.com/w/c/keyword _keywords = [ @@ -1053,7 +1057,7 @@ class ASTDeclaratorParen(ASTDeclarator): # Initializer ################################################################################ -class ASTParenExprList(ASTBase): +class ASTParenExprList(ASTBaseParenExprList): def __init__(self, exprs: List[ASTExpression]) -> None: self.exprs = exprs @@ -1883,7 +1887,7 @@ class Symbol: ourChild._fill_empty(otherChild.declaration, otherChild.docname) elif ourChild.docname != otherChild.docname: name = str(ourChild.declaration) - msg = __("Duplicate declaration, also defined in '%s'.\n" + msg = __("Duplicate C declaration, also defined in '%s'.\n" "Declaration is '%s'.") msg = msg % (ourChild.docname, name) logger.warning(msg, location=otherChild.docname) @@ -2300,7 +2304,8 @@ class DefinitionParser(BaseParser): errs = [] errs.append((exCast, "If type cast expression")) errs.append((exUnary, "If unary expression")) - raise self._make_multi_error(errs, "Error in cast expression.") + raise self._make_multi_error(errs, + "Error in cast expression.") from exUnary else: return self._parse_unary_expression() @@ -2767,7 +2772,7 @@ class DefinitionParser(BaseParser): msg += " (e.g., 'void (*f(int arg))(double)')" prevErrors.append((exNoPtrParen, msg)) header = "Error in declarator" - raise self._make_multi_error(prevErrors, header) + raise self._make_multi_error(prevErrors, header) from exNoPtrParen pos = self.pos try: return self._parse_declarator_name_suffix(named, paramMode, typed) @@ -2775,7 +2780,7 @@ class DefinitionParser(BaseParser): self.pos = pos prevErrors.append((e, "If declarator-id")) header = "Error in declarator or parameters" - raise self._make_multi_error(prevErrors, header) + raise self._make_multi_error(prevErrors, header) from e def _parse_initializer(self, outer: str = None, allowFallback: bool = True ) -> ASTInitializer: @@ -2843,7 +2848,7 @@ class DefinitionParser(BaseParser): if True: header = "Type must be either just a name or a " header += "typedef-like declaration." - raise self._make_multi_error(prevErrors, header) + raise self._make_multi_error(prevErrors, header) from exTyped else: # For testing purposes. # do it again to get the proper traceback (how do you @@ -2994,7 +2999,7 @@ class DefinitionParser(BaseParser): errs = [] errs.append((exExpr, "If expression")) errs.append((exType, "If type")) - raise self._make_multi_error(errs, header) + raise self._make_multi_error(errs, header) from exType return res @@ -3017,6 +3022,13 @@ class CObject(ObjectDescription): names=('rtype',)), ] + option_spec = { + # have a dummy option to ensure proper errors on options, + # otherwise the option is taken as a continuation of the + # argument + 'dummy': None + } + def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None: assert ast.objectType == 'enumerator' # find the parent, if it exists && is an enum @@ -3083,7 +3095,8 @@ class CObject(ObjectDescription): self.state.document.note_explicit_target(signode) domain = cast(CDomain, self.env.get_domain('c')) - domain.note_object(name, self.objtype, newestId) + if name not in domain.objects: + domain.objects[name] = (domain.env.docname, newestId, self.objtype) indexText = self.get_index_text(name) self.indexnode['entries'].append(('single', indexText, newestId, '', None)) @@ -3132,7 +3145,7 @@ class CObject(ObjectDescription): name = _make_phony_error_name() symbol = parentSymbol.add_name(name) self.env.temp_data['c:last_symbol'] = symbol - raise ValueError + raise ValueError from e try: symbol = parentSymbol.add_declaration(ast, docname=self.env.docname) @@ -3148,7 +3161,10 @@ class CObject(ObjectDescription): # Assume we are actually in the old symbol, # instead of the newly created duplicate. self.env.temp_data['c:last_symbol'] = e.symbol - logger.warning("Duplicate declaration, %s", sig, location=signode) + msg = __("Duplicate C declaration, also defined in '%s'.\n" + "Declaration is '%s'.") + msg = msg % (e.symbol.docname, sig) + logger.warning(msg, location=signode) if ast.objectType == 'enumerator': self._add_enumerator_to_parent(ast) @@ -3309,6 +3325,106 @@ class CNamespacePopObject(SphinxDirective): return [] +class AliasNode(nodes.Element): + def __init__(self, sig: str, env: "BuildEnvironment" = None, + parentKey: LookupKey = None) -> None: + super().__init__() + self.sig = sig + if env is not None: + if 'c:parent_symbol' not in env.temp_data: + root = env.domaindata['c']['root_symbol'] + env.temp_data['c:parent_symbol'] = root + self.parentKey = env.temp_data['c:parent_symbol'].get_lookup_key() + else: + assert parentKey is not None + self.parentKey = parentKey + + def copy(self: T) -> T: + return self.__class__(self.sig, env=None, parentKey=self.parentKey) # type: ignore + + +class AliasTransform(SphinxTransform): + default_priority = ReferencesResolver.default_priority - 1 + + def apply(self, **kwargs: Any) -> None: + for node in self.document.traverse(AliasNode): + sig = node.sig + parentKey = node.parentKey + try: + parser = DefinitionParser(sig, location=node, + config=self.env.config) + name = parser.parse_xref_object() + except DefinitionError as e: + logger.warning(e, location=node) + name = None + + if name is None: + # could not be parsed, so stop here + signode = addnodes.desc_signature(sig, '') + signode.clear() + signode += addnodes.desc_name(sig, sig) + node.replace_self(signode) + continue + + rootSymbol = self.env.domains['c'].data['root_symbol'] # type: Symbol + parentSymbol = rootSymbol.direct_lookup(parentKey) # type: Symbol + if not parentSymbol: + print("Target: ", sig) + print("ParentKey: ", parentKey) + print(rootSymbol.dump(1)) + assert parentSymbol # should be there + + s = parentSymbol.find_declaration( + name, 'any', + matchSelf=True, recurseInAnon=True) + if s is None: + signode = addnodes.desc_signature(sig, '') + node.append(signode) + signode.clear() + signode += addnodes.desc_name(sig, sig) + + logger.warning("Could not find C declaration for alias '%s'." % name, + location=node) + node.replace_self(signode) + else: + nodes = [] + options = dict() # type: ignore + signode = addnodes.desc_signature(sig, '') + nodes.append(signode) + s.declaration.describe_signature(signode, 'markName', self.env, options) + node.replace_self(nodes) + + +class CAliasObject(ObjectDescription): + option_spec = {} # type: Dict + + def run(self) -> List[Node]: + if ':' in self.name: + self.domain, self.objtype = self.name.split(':', 1) + else: + self.domain, self.objtype = '', self.name + + node = addnodes.desc() + node.document = self.state.document + node['domain'] = self.domain + # 'desctype' is a backwards compatible attribute + node['objtype'] = node['desctype'] = self.objtype + node['noindex'] = True + + self.names = [] # type: List[str] + signatures = self.get_signatures() + for i, sig in enumerate(signatures): + node.append(AliasNode(sig, env=self.env)) + + contentnode = addnodes.desc_content() + node.append(contentnode) + self.before_content() + self.state.nested_parse(self.content, self.content_offset, contentnode) + self.env.temp_data['object'] = None + self.after_content() + return [node] + + class CXRefRole(XRefRole): def process_link(self, env: BuildEnvironment, refnode: Element, has_explicit_title: bool, title: str, target: str) -> Tuple[str, str]: @@ -3392,6 +3508,8 @@ class CDomain(Domain): 'namespace': CNamespaceObject, 'namespace-push': CNamespacePushObject, 'namespace-pop': CNamespacePopObject, + # other + 'alias': CAliasObject } roles = { 'member': CXRefRole(), @@ -3416,14 +3534,6 @@ class CDomain(Domain): def objects(self) -> Dict[str, Tuple[str, str, str]]: return self.data.setdefault('objects', {}) # fullname -> docname, node_id, objtype - def note_object(self, name: str, objtype: str, node_id: str, location: Any = None) -> None: - if name in self.objects: - docname = self.objects[name][0] - logger.warning(__('Duplicate C object description of %s, ' - 'other instance in %s, use :noindex: for one of them'), - name, docname, location=location) - self.objects[name] = (self.env.docname, node_id, objtype) - def clear_doc(self, docname: str) -> None: if Symbol.debug_show_tree: print("clear_doc:", docname) @@ -3469,13 +3579,9 @@ class CDomain(Domain): ourObjects = self.data['objects'] for fullname, (fn, id_, objtype) in otherdata['objects'].items(): if fn in docnames: - if fullname in ourObjects: - msg = __("Duplicate declaration, also defined in '%s'.\n" - "Name of declaration is '%s'.") - msg = msg % (ourObjects[fullname], fullname) - logger.warning(msg, location=fn) - else: + if fullname not in ourObjects: ourObjects[fullname] = (fn, id_, objtype) + # no need to warn on duplicates, the symbol merge already does that def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, @@ -3539,6 +3645,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_domain(CDomain) app.add_config_value("c_id_attributes", [], 'env') app.add_config_value("c_paren_attributes", [], 'env') + app.add_post_transform(AliasTransform) return { 'version': 'builtin', diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 1783db491..310691d49 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -31,7 +31,8 @@ from sphinx.transforms import SphinxTransform from sphinx.transforms.post_transforms import ReferencesResolver from sphinx.util import logging from sphinx.util.cfamily import ( - NoOldIdError, ASTBaseBase, ASTAttribute, verify_description_mode, StringifyTransform, + NoOldIdError, ASTBaseBase, ASTAttribute, ASTBaseParenExprList, + verify_description_mode, StringifyTransform, BaseParser, DefinitionError, UnsupportedMultiCharacterCharLiteral, identifier_re, anon_identifier_re, integer_literal_re, octal_literal_re, hex_literal_re, binary_literal_re, integers_literal_suffix_re, @@ -2742,7 +2743,7 @@ class ASTPackExpansionExpr(ASTExpression): signode += nodes.Text('...') -class ASTParenExprList(ASTBase): +class ASTParenExprList(ASTBaseParenExprList): def __init__(self, exprs: List[Union[ASTExpression, ASTBracedInitList]]) -> None: self.exprs = exprs @@ -4454,7 +4455,7 @@ class Symbol: ourChild._fill_empty(otherChild.declaration, otherChild.docname) elif ourChild.docname != otherChild.docname: name = str(ourChild.declaration) - msg = __("Duplicate declaration, also defined in '%s'.\n" + msg = __("Duplicate C++ declaration, also defined in '%s'.\n" "Declaration is '%s'.") msg = msg % (ourChild.docname, name) logger.warning(msg, location=otherChild.docname) @@ -4882,7 +4883,7 @@ class DefinitionParser(BaseParser): raise self._make_multi_error([ (eFold, "If fold expression"), (eExpr, "If parenthesized expression") - ], "Error in fold expression or parenthesized expression.") + ], "Error in fold expression or parenthesized expression.") from eExpr return ASTParenExpr(res) # now it definitely is a fold expression if self.skip_string(')'): @@ -5066,7 +5067,7 @@ class DefinitionParser(BaseParser): errors = [] errors.append((eType, "If type")) errors.append((eExpr, "If expression")) - raise self._make_multi_error(errors, header) + raise self._make_multi_error(errors, header) from eExpr else: # a primary expression or a type pos = self.pos try: @@ -5093,7 +5094,7 @@ class DefinitionParser(BaseParser): errors = [] errors.append((eOuter, "If primary expression")) errors.append((eInner, "If type")) - raise self._make_multi_error(errors, header) + raise self._make_multi_error(errors, header) from eInner # and now parse postfixes postFixes = [] # type: List[ASTPostfixOp] @@ -5253,7 +5254,8 @@ class DefinitionParser(BaseParser): errs = [] errs.append((exCast, "If type cast expression")) errs.append((exUnary, "If unary expression")) - raise self._make_multi_error(errs, "Error in cast expression.") + raise self._make_multi_error(errs, + "Error in cast expression.") from exUnary else: return self._parse_unary_expression() @@ -5504,7 +5506,7 @@ class DefinitionParser(BaseParser): self.pos = pos prevErrors.append((e, "If non-type argument")) header = "Error in parsing template argument list." - raise self._make_multi_error(prevErrors, header) + raise self._make_multi_error(prevErrors, header) from e if parsedEnd: assert not parsedComma break @@ -5949,7 +5951,7 @@ class DefinitionParser(BaseParser): self.pos = pos prevErrors.append((exNoPtrParen, "If parenthesis in noptr-declarator")) header = "Error in declarator" - raise self._make_multi_error(prevErrors, header) + raise self._make_multi_error(prevErrors, header) from exNoPtrParen if typed: # pointer to member pos = self.pos try: @@ -5988,7 +5990,7 @@ class DefinitionParser(BaseParser): self.pos = pos prevErrors.append((e, "If declarator-id")) header = "Error in declarator or parameters-and-qualifiers" - raise self._make_multi_error(prevErrors, header) + raise self._make_multi_error(prevErrors, header) from e def _parse_initializer(self, outer: str = None, allowFallback: bool = True ) -> ASTInitializer: @@ -6096,7 +6098,7 @@ class DefinitionParser(BaseParser): header = "Error when parsing function declaration." else: assert False - raise self._make_multi_error(prevErrors, header) + raise self._make_multi_error(prevErrors, header) from exTyped else: # For testing purposes. # do it again to get the proper traceback (how do you @@ -6163,7 +6165,7 @@ class DefinitionParser(BaseParser): errs.append((eType, "If default template argument is a type")) msg = "Error in non-type template parameter" msg += " or constrained template parameter." - raise self._make_multi_error(errs, msg) + raise self._make_multi_error(errs, msg) from eType def _parse_type_using(self) -> ASTTypeUsing: name = self._parse_nested_name() @@ -6510,7 +6512,7 @@ class DefinitionParser(BaseParser): self.pos = pos prevErrors.append((e, "If type alias or template alias")) header = "Error in type declaration." - raise self._make_multi_error(prevErrors, header) + raise self._make_multi_error(prevErrors, header) from e elif objectType == 'concept': declaration = self._parse_concept() elif objectType == 'member': @@ -6576,7 +6578,7 @@ class DefinitionParser(BaseParser): errs.append((e1, "If shorthand ref")) errs.append((e2, "If full function ref")) msg = "Error in cross-reference." - raise self._make_multi_error(errs, msg) + raise self._make_multi_error(errs, msg) from e2 def parse_expression(self) -> Union[ASTExpression, ASTType]: pos = self.pos @@ -6597,7 +6599,7 @@ class DefinitionParser(BaseParser): errs = [] errs.append((exExpr, "If expression")) errs.append((exType, "If type")) - raise self._make_multi_error(errs, header) + raise self._make_multi_error(errs, header) from exType def _make_phony_error_name() -> ASTNestedName: @@ -6622,8 +6624,9 @@ class CPPObject(ObjectDescription): names=('returns', 'return')), ] - option_spec = dict(ObjectDescription.option_spec) - option_spec['tparam-line-spec'] = directives.flag + option_spec = { + 'tparam-line-spec': directives.flag, + } def _add_enumerator_to_parent(self, ast: ASTDeclaration) -> None: assert ast.objectType == 'enumerator' @@ -6790,7 +6793,7 @@ class CPPObject(ObjectDescription): name = _make_phony_error_name() symbol = parentSymbol.add_name(name) self.env.temp_data['cpp:last_symbol'] = symbol - raise ValueError + raise ValueError from e try: symbol = parentSymbol.add_declaration(ast, docname=self.env.docname) @@ -6806,7 +6809,10 @@ class CPPObject(ObjectDescription): # Assume we are actually in the old symbol, # instead of the newly created duplicate. self.env.temp_data['cpp:last_symbol'] = e.symbol - logger.warning("Duplicate declaration, %s", sig, location=signode) + msg = __("Duplicate C++ declaration, also defined in '%s'.\n" + "Declaration is '%s'.") + msg = msg % (e.symbol.docname, sig) + logger.warning(msg, location=signode) if ast.objectType == 'enumerator': self._add_enumerator_to_parent(ast) @@ -7081,7 +7087,6 @@ class CPPAliasObject(ObjectDescription): node['domain'] = self.domain # 'desctype' is a backwards compatible attribute node['objtype'] = node['desctype'] = self.objtype - node['noindex'] = True self.names = [] # type: List[str] signatures = self.get_signatures() @@ -7273,13 +7278,9 @@ class CPPDomain(Domain): ourNames = self.data['names'] for name, docname in otherdata['names'].items(): if docname in docnames: - if name in ourNames: - msg = __("Duplicate declaration, also defined in '%s'.\n" - "Name of declaration is '%s'.") - msg = msg % (ourNames[name], name) - logger.warning(msg, location=docname) - else: + if name not in ourNames: ourNames[name] = docname + # no need to warn on duplicates, the symbol merge already does that if Symbol.debug_show_tree: print("\tresult:") print(self.data['root_symbol'].dump(1)) diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index 637dbd305..865feb67b 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -1041,10 +1041,10 @@ class StandardDomain(Domain): try: figure_id = target_node['ids'][0] return env.toc_fignumbers[docname][figtype][figure_id] - except (KeyError, IndexError): + except (KeyError, IndexError) as exc: # target_node is found, but fignumber is not assigned. # Maybe it is defined in orphaned document. - raise ValueError + raise ValueError from exc def get_full_qualified_name(self, node: Element) -> str: if node.get('reftype') == 'option': diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 9451d1451..6de352498 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -376,7 +376,8 @@ class BuildEnvironment: if catalog.domain == domain: self.dependencies[docname].add(catalog.mo_path) except OSError as exc: - raise DocumentError(__('Failed to scan documents in %s: %r') % (self.srcdir, exc)) + raise DocumentError(__('Failed to scan documents in %s: %r') % + (self.srcdir, exc)) from exc def get_outdated_files(self, config_changed: bool) -> Tuple[Set[str], Set[str], Set[str]]: """Return (added, changed, removed) sets.""" @@ -501,8 +502,8 @@ class BuildEnvironment: """ try: return self.domains[domainname] - except KeyError: - raise ExtensionError(__('Domain %r is not registered') % domainname) + except KeyError as exc: + raise ExtensionError(__('Domain %r is not registered') % domainname) from exc # --------- RESOLVING REFERENCES AND TOCTREES ------------------------------ diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index a8c571f3e..28cfe06b5 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -33,7 +33,6 @@ from sphinx.locale import _, __ from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.util import inspect from sphinx.util import logging -from sphinx.util import split_full_qualified_name from sphinx.util.docstrings import extract_metadata, prepare_docstring from sphinx.util.inspect import getdoc, object_description, safe_getattr, stringify_signature from sphinx.util.typing import stringify as stringify_typehint @@ -821,7 +820,12 @@ class Documenter: self.add_line('', sourcename) # format the object's signature, if any - sig = self.format_signature() + try: + sig = self.format_signature() + except Exception as exc: + logger.warning(__('error while formatting signature for %s: %s'), + self.fullname, exc, type='autodoc') + return # generate the directive header and options, if applicable self.add_directive_header(sig) @@ -970,14 +974,8 @@ class ModuleLevelDocumenter(Documenter): ) -> Tuple[str, List[str]]: if modname is None: if path: - stripped = path.rstrip('.') - modname, qualname = split_full_qualified_name(stripped) - if qualname: - parents = qualname.split(".") - else: - parents = [] - - if modname is None: + modname = path.rstrip('.') + else: # if documenting a toplevel object without explicit module, # it can be contained in another auto directive ... modname = self.env.temp_data.get('autodoc:module') @@ -1010,13 +1008,8 @@ class ClassLevelDocumenter(Documenter): # ... if still None, there's no way to know if mod_cls is None: return None, [] - - try: - modname, qualname = split_full_qualified_name(mod_cls) - parents = qualname.split(".") if qualname else [] - except ImportError: - parents = mod_cls.split(".") - + modname, sep, cls = mod_cls.rpartition('.') + parents = [cls] # if the module name is still missing, get it like above if not modname: modname = self.env.temp_data.get('autodoc:module') diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index cb06edbac..637c69814 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -32,7 +32,7 @@ def import_module(modname: str, warningiserror: bool = False) -> Any: except BaseException as exc: # Importing modules may cause any side effects, including # SystemExit, so we need to catch all errors. - raise ImportError(exc, traceback.format_exc()) + raise ImportError(exc, traceback.format_exc()) from exc def import_object(modname: str, objpath: List[str], objtype: str = '', @@ -97,7 +97,7 @@ def import_object(modname: str, objpath: List[str], objtype: str = '', errmsg += '; the following exception was raised:\n%s' % traceback.format_exc() logger.debug(errmsg) - raise ImportError(errmsg) + raise ImportError(errmsg) from exc def get_module_members(module: Any) -> List[Tuple[str, Any]]: diff --git a/sphinx/ext/autodoc/typehints.py b/sphinx/ext/autodoc/typehints.py index b763bdfc7..4f81a6eae 100644 --- a/sphinx/ext/autodoc/typehints.py +++ b/sphinx/ext/autodoc/typehints.py @@ -46,11 +46,16 @@ def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element if objtype == 'class' and app.config.autoclass_content not in ('init', 'both'): return - signature = cast(addnodes.desc_signature, contentnode.parent[0]) - if signature['module']: - fullname = '.'.join([signature['module'], signature['fullname']]) - else: - fullname = signature['fullname'] + try: + signature = cast(addnodes.desc_signature, contentnode.parent[0]) + if signature['module']: + fullname = '.'.join([signature['module'], signature['fullname']]) + else: + fullname = signature['fullname'] + except KeyError: + # signature node does not have valid context info for the target object + return + annotations = app.env.temp_data.get('annotations', {}) if annotations.get(fullname, {}): field_lists = [n for n in contentnode if isinstance(n, nodes.field_list)] diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index bc8319a20..38a5e7e85 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -95,6 +95,8 @@ logger = logging.getLogger(__name__) periods_re = re.compile(r'\.(?:\s+)') literal_re = re.compile(r'::\s*$') +WELL_KNOWN_ABBREVIATIONS = (' i.e.',) + # -- autosummary_toc node ------------------------------------------------------ @@ -470,6 +472,13 @@ def mangle_signature(sig: str, max_chars: int = 30) -> str: def extract_summary(doc: List[str], document: Any) -> str: """Extract summary from docstring.""" + def parse(doc: List[str], settings: Any) -> nodes.document: + state_machine = RSTStateMachine(state_classes, 'Body') + node = new_document('', settings) + node.reporter = NullReporter() + state_machine.run(doc, node) + + return node # Skip a blank lines at the top while doc and not doc[0].strip(): @@ -487,11 +496,7 @@ def extract_summary(doc: List[str], document: Any) -> str: return '' # parse the docstring - state_machine = RSTStateMachine(state_classes, 'Body') - node = new_document('', document.settings) - node.reporter = NullReporter() - state_machine.run(doc, node) - + node = parse(doc, document.settings) if not isinstance(node[0], nodes.paragraph): # document starts with non-paragraph: pick up the first line summary = doc[0].strip() @@ -502,11 +507,13 @@ def extract_summary(doc: List[str], document: Any) -> str: summary = sentences[0].strip() else: summary = '' - while sentences: - summary += sentences.pop(0) + '.' + for i in range(len(sentences)): + summary = ". ".join(sentences[:i + 1]).rstrip(".") + "." node[:] = [] - state_machine.run([summary], node) - if not node.traverse(nodes.system_message): + node = parse(doc, document.settings) + if summary.endswith(WELL_KNOWN_ABBREVIATIONS): + pass + elif not node.traverse(nodes.system_message): # considered as that splitting by period does not break inline markups break @@ -620,7 +627,7 @@ def _import_by_name(name: str) -> Tuple[Any, Any, str]: else: return sys.modules[modname], None, modname except (ValueError, ImportError, AttributeError, KeyError) as e: - raise ImportError(*e.args) + raise ImportError(*e.args) from e # -- :autolink: (smart default role) ------------------------------------------- @@ -701,7 +708,8 @@ def process_generate_options(app: Sphinx) -> None: with mock(app.config.autosummary_mock_imports): generate_autosummary_docs(genfiles, suffix=suffix, base_path=app.srcdir, app=app, imported_members=imported_members, - overwrite=app.config.autosummary_generate_overwrite) + overwrite=app.config.autosummary_generate_overwrite, + encoding=app.config.source_encoding) def setup(app: Sphinx) -> Dict[str, Any]: diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index cccbd1487..91ef067a9 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -226,7 +226,8 @@ class ModuleScanner: def generate_autosummary_content(name: str, obj: Any, parent: Any, template: AutosummaryRenderer, template_name: str, imported_members: bool, app: Any, - recursive: bool, context: Dict) -> str: + recursive: bool, context: Dict, + modname: str = None, qualname: str = None) -> str: doc = get_documenter(app, obj, parent) def skip_member(obj: Any, name: str, objtype: str) -> bool: @@ -315,7 +316,9 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any, ns['attributes'], ns['all_attributes'] = \ get_members(obj, {'attribute', 'property'}) - modname, qualname = split_full_qualified_name(name) + if modname is None or qualname is None: + modname, qualname = split_full_qualified_name(name) + if doc.objtype in ('method', 'attribute', 'property'): ns['class'] = qualname.rsplit(".", 1)[0] @@ -342,7 +345,7 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, suffix: str = '.rst', base_path: str = None, builder: Builder = None, template_dir: str = None, imported_members: bool = False, app: Any = None, - overwrite: bool = True) -> None: + overwrite: bool = True, encoding: str = 'utf-8') -> None: if builder: warnings.warn('builder argument for generate_autosummary_docs() is deprecated.', RemovedInSphinx50Warning, stacklevel=2) @@ -382,7 +385,8 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, ensuredir(path) try: - name, obj, parent, mod_name = import_by_name(entry.name) + name, obj, parent, modname = import_by_name(entry.name) + qualname = name.replace(modname + ".", "") except ImportError as e: logger.warning(__('[autosummary] failed to import %r: %s') % (entry.name, e)) continue @@ -392,21 +396,22 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, context.update(app.config.autosummary_context) content = generate_autosummary_content(name, obj, parent, template, entry.template, - imported_members, app, entry.recursive, context) + imported_members, app, entry.recursive, context, + modname, qualname) filename = os.path.join(path, name + suffix) if os.path.isfile(filename): - with open(filename) as f: + with open(filename, encoding=encoding) as f: old_content = f.read() if content == old_content: continue elif overwrite: # content has changed - with open(filename, 'w') as f: + with open(filename, 'w', encoding=encoding) as f: f.write(content) new_files.append(filename) else: - with open(filename, 'w') as f: + with open(filename, 'w', encoding=encoding) as f: f.write(content) new_files.append(filename) diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index c21868a6f..4a8dd0a4d 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -256,7 +256,7 @@ def render_dot(self: SphinxTranslator, code: str, options: Dict, return None, None except CalledProcessError as exc: raise GraphvizError(__('dot exited with error:\n[stderr]\n%r\n' - '[stdout]\n%r') % (exc.stderr, exc.stdout)) + '[stdout]\n%r') % (exc.stderr, exc.stdout)) from exc def render_dot_html(self: HTMLTranslator, node: graphviz, code: str, options: Dict, @@ -270,7 +270,7 @@ def render_dot_html(self: HTMLTranslator, node: graphviz, code: str, options: Di fname, outfn = render_dot(self, code, options, format, prefix) except GraphvizError as exc: logger.warning(__('dot code %r: %s'), code, exc) - raise nodes.SkipNode + raise nodes.SkipNode from exc classes = [imgcls, 'graphviz'] + node.get('classes', []) imgcls = ' '.join(filter(None, classes)) @@ -321,7 +321,7 @@ def render_dot_latex(self: LaTeXTranslator, node: graphviz, code: str, fname, outfn = render_dot(self, code, options, 'pdf', prefix) except GraphvizError as exc: logger.warning(__('dot code %r: %s'), code, exc) - raise nodes.SkipNode + raise nodes.SkipNode from exc is_inline = self.is_inline(node) @@ -358,7 +358,7 @@ def render_dot_texinfo(self: TexinfoTranslator, node: graphviz, code: str, fname, outfn = render_dot(self, code, options, 'png', prefix) except GraphvizError as exc: logger.warning(__('dot code %r: %s'), code, exc) - raise nodes.SkipNode + raise nodes.SkipNode from exc if fname is not None: self.body.append('@image{%s,,,[graphviz],png}\n' % fname[:-4]) raise nodes.SkipNode diff --git a/sphinx/ext/imgconverter.py b/sphinx/ext/imgconverter.py index bf4b9b9d1..dd13a9879 100644 --- a/sphinx/ext/imgconverter.py +++ b/sphinx/ext/imgconverter.py @@ -70,7 +70,7 @@ class ImagemagickConverter(ImageConverter): except CalledProcessError as exc: raise ExtensionError(__('convert exited with error:\n' '[stderr]\n%r\n[stdout]\n%r') % - (exc.stderr, exc.stdout)) + (exc.stderr, exc.stdout)) from exc def setup(app: Sphinx) -> Dict[str, Any]: diff --git a/sphinx/ext/imgmath.py b/sphinx/ext/imgmath.py index 65f281223..a8da39709 100644 --- a/sphinx/ext/imgmath.py +++ b/sphinx/ext/imgmath.py @@ -137,13 +137,13 @@ def compile_math(latex: str, builder: Builder) -> str: try: subprocess.run(command, stdout=PIPE, stderr=PIPE, cwd=tempdir, check=True) return path.join(tempdir, 'math.dvi') - except OSError: + except OSError as exc: logger.warning(__('LaTeX command %r cannot be run (needed for math ' 'display), check the imgmath_latex setting'), builder.config.imgmath_latex) - raise InvokeError + raise InvokeError from exc except CalledProcessError as exc: - raise MathExtError('latex exited with error', exc.stderr, exc.stdout) + raise MathExtError('latex exited with error', exc.stderr, exc.stdout) from exc def convert_dvi_to_image(command: List[str], name: str) -> Tuple[str, str]: @@ -151,13 +151,13 @@ def convert_dvi_to_image(command: List[str], name: str) -> Tuple[str, str]: try: ret = subprocess.run(command, stdout=PIPE, stderr=PIPE, check=True, encoding='ascii') return ret.stdout, ret.stderr - except OSError: + except OSError as exc: logger.warning(__('%s command %r cannot be run (needed for math ' 'display), check the imgmath_%s setting'), name, command[0], name) - raise InvokeError + raise InvokeError from exc except CalledProcessError as exc: - raise MathExtError('%s exited with error' % name, exc.stderr, exc.stdout) + raise MathExtError('%s exited with error' % name, exc.stderr, exc.stdout) from exc def convert_dvi_to_png(dvipath: str, builder: Builder) -> Tuple[str, int]: @@ -298,7 +298,7 @@ def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None: backrefs=[], source=node.astext()) sm.walkabout(self) logger.warning(__('display latex %r: %s'), node.astext(), msg) - raise nodes.SkipNode + raise nodes.SkipNode from exc if fname is None: # something failed -- use text-only as a bad substitute self.body.append('<span class="math">%s</span>' % @@ -324,7 +324,7 @@ def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None backrefs=[], source=node.astext()) sm.walkabout(self) logger.warning(__('inline latex %r: %s'), node.astext(), msg) - raise nodes.SkipNode + raise nodes.SkipNode from exc self.body.append(self.starttag(node, 'div', CLASS='math')) self.body.append('<p>') if node['number']: diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 02f605bac..a6c4ef694 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -179,7 +179,7 @@ def fetch_inventory(app: Sphinx, uri: str, inv: Any) -> Any: join = path.join if localuri else posixpath.join invdata = InventoryFile.load(f, uri, join) except ValueError as exc: - raise ValueError('unknown or unsupported inventory version: %r' % exc) + raise ValueError('unknown or unsupported inventory version: %r' % exc) from exc except Exception as err: err.args = ('intersphinx inventory %r not readable due to %s: %s', inv, err.__class__.__name__, str(err)) diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 0683a06ed..39f2335f4 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -158,6 +158,7 @@ class GoogleDocstring: 'parameters': self._parse_parameters_section, 'return': self._parse_returns_section, 'returns': self._parse_returns_section, + 'raise': self._parse_raises_section, 'raises': self._parse_raises_section, 'references': self._parse_references_section, 'see also': self._parse_see_also_section, @@ -165,6 +166,7 @@ class GoogleDocstring: 'todo': partial(self._parse_admonition, 'todo'), 'warning': partial(self._parse_admonition, 'warning'), 'warnings': partial(self._parse_admonition, 'warning'), + 'warn': self._parse_warns_section, 'warns': self._parse_warns_section, 'yield': self._parse_yields_section, 'yields': self._parse_yields_section, diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index a2eeb7891..dc24a1993 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -131,10 +131,8 @@ def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str], def missing_reference(app: Sphinx, env: BuildEnvironment, node: Element, contnode: Node ) -> Node: - if app.builder.format != 'html': - return None - elif node['reftype'] == 'viewcode': - # resolve our "viewcode" reference nodes -- they need special treatment + # resolve our "viewcode" reference nodes -- they need special treatment + if node['reftype'] == 'viewcode': return make_refnode(app.builder, node['refdoc'], node['reftarget'], node['refid'], contnode) diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index 87ac5d9ec..295963ca3 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -35,7 +35,7 @@ class ModuleAnalyzer: try: mod = import_module(modname) except Exception as err: - raise PycodeError('error importing %r' % modname, err) + raise PycodeError('error importing %r' % modname, err) from err loader = getattr(mod, '__loader__', None) filename = getattr(mod, '__file__', None) if loader and getattr(loader, 'get_source', None): @@ -52,7 +52,7 @@ class ModuleAnalyzer: try: filename = loader.get_filename(modname) except ImportError as err: - raise PycodeError('error getting filename for %r' % modname, err) + raise PycodeError('error getting filename for %r' % modname, err) from err if filename is None: # all methods for getting filename failed, so raise... raise PycodeError('no source found for module %r' % modname) @@ -90,7 +90,7 @@ class ModuleAnalyzer: if '.egg' + path.sep in filename: obj = cls.cache['file', filename] = cls.for_egg(filename, modname) else: - raise PycodeError('error opening %r' % filename, err) + raise PycodeError('error opening %r' % filename, err) from err return obj @classmethod @@ -102,7 +102,7 @@ class ModuleAnalyzer: code = egg.read(relpath).decode() return cls.for_string(code, modname, filename) except Exception as exc: - raise PycodeError('error opening %r' % filename, exc) + raise PycodeError('error opening %r' % filename, exc) from exc @classmethod def for_module(cls, modname: str) -> "ModuleAnalyzer": @@ -158,7 +158,7 @@ class ModuleAnalyzer: self.tags = parser.definitions self.tagorder = parser.deforders except Exception as exc: - raise PycodeError('parsing %r failed: %r' % (self.srcname, exc)) + raise PycodeError('parsing %r failed: %r' % (self.srcname, exc)) from exc def find_attr_docs(self) -> Dict[Tuple[str, str], List[str]]: """Find class and module-level attributes and their documentation.""" diff --git a/sphinx/registry.py b/sphinx/registry.py index c36671103..3dde26b67 100644 --- a/sphinx/registry.py +++ b/sphinx/registry.py @@ -138,9 +138,9 @@ class SphinxComponentRegistry: entry_points = iter_entry_points('sphinx.builders', name) try: entry_point = next(entry_points) - except StopIteration: + except StopIteration as exc: raise SphinxError(__('Builder name %s not registered or available' - ' through entry point') % name) + ' through entry point') % name) from exc self.load_extension(app, entry_point.module_name) @@ -272,8 +272,8 @@ class SphinxComponentRegistry: def get_source_parser(self, filetype: str) -> "Type[Parser]": try: return self.source_parsers[filetype] - except KeyError: - raise SphinxError(__('Source parser for %s not registered') % filetype) + except KeyError as exc: + raise SphinxError(__('Source parser for %s not registered') % filetype) from exc def get_source_parsers(self) -> Dict[str, "Type[Parser]"]: return self.source_parsers @@ -310,9 +310,11 @@ class SphinxComponentRegistry: try: visit, depart = handlers # unpack once for assertion translation_handlers[node.__name__] = (visit, depart) - except ValueError: - raise ExtensionError(__('kwargs for add_node() must be a (visit, depart) ' - 'function tuple: %r=%r') % (builder_name, handlers)) + except ValueError as exc: + raise ExtensionError( + __('kwargs for add_node() must be a (visit, depart) ' + 'function tuple: %r=%r') % (builder_name, handlers) + ) from exc def get_translator_class(self, builder: Builder) -> "Type[nodes.NodeVisitor]": return self.translators.get(builder.name, @@ -406,7 +408,8 @@ class SphinxComponentRegistry: mod = import_module(extname) except ImportError as err: logger.verbose(__('Original exception:\n') + traceback.format_exc()) - raise ExtensionError(__('Could not import extension %s') % extname, err) + raise ExtensionError(__('Could not import extension %s') % extname, + err) from err setup = getattr(mod, 'setup', None) if setup is None: @@ -422,7 +425,7 @@ class SphinxComponentRegistry: __('The %s extension used by this project needs at least ' 'Sphinx v%s; it therefore cannot be built with this ' 'version.') % (extname, err) - ) + ) from err if metadata is None: metadata = {} diff --git a/sphinx/roles.py b/sphinx/roles.py index 7bb7ac5e2..426a62e90 100644 --- a/sphinx/roles.py +++ b/sphinx/roles.py @@ -331,14 +331,15 @@ class Abbreviation(SphinxRole): abbr_re = re.compile(r'\((.*)\)$', re.S) def run(self) -> Tuple[List[Node], List[system_message]]: + options = self.options.copy() matched = self.abbr_re.search(self.text) if matched: text = self.text[:matched.start()].strip() - self.options['explanation'] = matched.group(1) + options['explanation'] = matched.group(1) else: text = self.text - return [nodes.abbreviation(self.rawtext, text, **self.options)], [] + return [nodes.abbreviation(self.rawtext, text, **options)], [] specific_docroles = { diff --git a/sphinx/search/ja.py b/sphinx/search/ja.py index d1f444be1..c1e72b8f8 100644 --- a/sphinx/search/ja.py +++ b/sphinx/search/ja.py @@ -528,9 +528,9 @@ class SearchJapanese(SearchLanguage): dotted_path = options.get('type', 'sphinx.search.ja.DefaultSplitter') try: self.splitter = import_object(dotted_path)(options) - except ExtensionError: + except ExtensionError as exc: raise ExtensionError("Splitter module %r can't be imported" % - dotted_path) + dotted_path) from exc def split(self, input: str) -> List[str]: return self.splitter.split(input) diff --git a/sphinx/setup_command.py b/sphinx/setup_command.py index 36e15cd19..2b0e4b74b 100644 --- a/sphinx/setup_command.py +++ b/sphinx/setup_command.py @@ -198,7 +198,7 @@ class BuildDoc(Command): except Exception as exc: handle_exception(app, self, exc, sys.stderr) if not self.pdb: - raise SystemExit(1) + raise SystemExit(1) from exc if not self.link_index: continue diff --git a/sphinx/themes/basic/layout.html b/sphinx/themes/basic/layout.html index 50feffd71..9163a18a2 100644 --- a/sphinx/themes/basic/layout.html +++ b/sphinx/themes/basic/layout.html @@ -120,7 +120,7 @@ {%- else %} <meta http-equiv="Content-Type" content="text/html; charset={{ encoding }}" /> {%- endif %} - <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> {{- metatags }} {%- block htmltitle %} <title>{{ title|striptags|e }}{{ titlesuffix }}</title> diff --git a/sphinx/themes/bizstyle/layout.html b/sphinx/themes/bizstyle/layout.html index c946f8435..603eb2326 100644 --- a/sphinx/themes/bizstyle/layout.html +++ b/sphinx/themes/bizstyle/layout.html @@ -20,7 +20,7 @@ {%- endblock %} {%- block extrahead %} - <meta name="viewport" content="width=device-width,initial-scale=1.0"> + <meta name="viewport" content="width=device-width,initial-scale=1.0" /> <!--[if lt IE 9]> <script src="_static/css3-mediaqueries.js"></script> <![endif]--> diff --git a/sphinx/theming.py b/sphinx/theming.py index 2636329a0..c05d87407 100644 --- a/sphinx/theming.py +++ b/sphinx/theming.py @@ -74,17 +74,17 @@ class Theme: try: inherit = self.config.get('theme', 'inherit') - except configparser.NoSectionError: - raise ThemeError(__('theme %r doesn\'t have "theme" setting') % name) - except configparser.NoOptionError: - raise ThemeError(__('theme %r doesn\'t have "inherit" setting') % name) + except configparser.NoSectionError as exc: + raise ThemeError(__('theme %r doesn\'t have "theme" setting') % name) from exc + except configparser.NoOptionError as exc: + raise ThemeError(__('theme %r doesn\'t have "inherit" setting') % name) from exc if inherit != 'none': try: self.base = factory.create(inherit) - except ThemeError: + except ThemeError as exc: raise ThemeError(__('no theme named %r found, inherited by %r') % - (inherit, name)) + (inherit, name)) from exc def get_theme_dirs(self) -> List[str]: """Return a list of theme directories, beginning with this theme's, @@ -101,13 +101,13 @@ class Theme: """ try: return self.config.get(section, name) - except (configparser.NoOptionError, configparser.NoSectionError): + except (configparser.NoOptionError, configparser.NoSectionError) as exc: if self.base: return self.base.get_config(section, name, default) if default is NODEFAULT: raise ThemeError(__('setting %s.%s occurs in none of the ' - 'searched theme configs') % (section, name)) + 'searched theme configs') % (section, name)) from exc else: return default diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index 4499e3376..7dc14af52 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -82,8 +82,8 @@ class ReferencesResolver(SphinxPostTransform): # let the domain try to resolve the reference try: domain = self.env.domains[node['refdomain']] - except KeyError: - raise NoUri(target, typ) + except KeyError as exc: + raise NoUri(target, typ) from exc newnode = domain.resolve_xref(self.env, refdoc, self.app.builder, typ, target, node, contnode) # really hardwired reference types diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index fd6410a36..e02e8adce 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -334,8 +334,8 @@ def parselinenos(spec: str, total: int) -> List[int]: items.extend(range(start - 1, end)) else: raise ValueError - except Exception: - raise ValueError('invalid line number spec: %r' % spec) + except Exception as exc: + raise ValueError('invalid line number spec: %r' % spec) from exc return items @@ -406,9 +406,9 @@ def import_object(objname: str, source: str = None) -> Any: except (AttributeError, ImportError) as exc: if source: raise ExtensionError('Could not import %s (needed for %s)' % - (objname, source), exc) + (objname, source), exc) from exc else: - raise ExtensionError('Could not import %s' % objname, exc) + raise ExtensionError('Could not import %s' % objname, exc) from exc def split_full_qualified_name(name: str) -> Tuple[str, str]: @@ -425,26 +425,16 @@ def split_full_qualified_name(name: str) -> Tuple[str, str]: Therefore you need to mock 3rd party modules if needed before calling this function. """ - from sphinx.util import inspect - parts = name.split('.') for i, part in enumerate(parts, 1): try: modname = ".".join(parts[:i]) - module = import_module(modname) - - # check the module has a member named as attrname - # - # Note: This is needed to detect the attribute having the same name - # as the module. - # ref: https://github.com/sphinx-doc/sphinx/issues/7812 - attrname = parts[i] - if hasattr(module, attrname): - value = inspect.safe_getattr(module, attrname) - if not inspect.ismodule(value): - return ".".join(parts[:i]), ".".join(parts[i:]) + import_module(modname) except ImportError: - return ".".join(parts[:i - 1]), ".".join(parts[i - 1:]) + if parts[:i - 1]: + return ".".join(parts[:i - 1]), ".".join(parts[i - 1:]) + else: + return None, ".".join(parts) except IndexError: pass diff --git a/sphinx/util/cfamily.py b/sphinx/util/cfamily.py index 8e07ae92a..f8ce4c827 100644 --- a/sphinx/util/cfamily.py +++ b/sphinx/util/cfamily.py @@ -11,7 +11,7 @@ import re from copy import deepcopy from typing import ( - Any, Callable, List, Match, Pattern, Tuple, Union + Any, Callable, List, Match, Optional, Pattern, Tuple, Union ) from docutils import nodes @@ -141,16 +141,14 @@ class ASTCPPAttribute(ASTAttribute): class ASTGnuAttribute(ASTBaseBase): - def __init__(self, name: str, args: Any) -> None: + def __init__(self, name: str, args: Optional["ASTBaseParenExprList"]) -> None: self.name = name self.args = args def _stringify(self, transform: StringifyTransform) -> str: res = [self.name] if self.args: - res.append('(') res.append(transform(self.args)) - res.append(')') return ''.join(res) @@ -204,6 +202,11 @@ class ASTParenAttribute(ASTAttribute): ################################################################################ +class ASTBaseParenExprList(ASTBaseBase): + pass + + +################################################################################ class UnsupportedMultiCharacterCharLiteral(Exception): pass @@ -398,11 +401,8 @@ class BaseParser: while 1: if self.match(identifier_re): name = self.matched_text - self.skip_ws() - if self.skip_string_and_ws('('): - self.fail('Parameterized GNU style attribute not yet supported.') - attrs.append(ASTGnuAttribute(name, None)) - # TODO: parse arguments for the attribute + exprs = self._parse_paren_expression_list() + attrs.append(ASTGnuAttribute(name, exprs)) if self.skip_string_and_ws(','): continue elif self.skip_string_and_ws(')'): @@ -430,3 +430,6 @@ class BaseParser: return ASTParenAttribute(id, arg) return None + + def _parse_paren_expression_list(self) -> ASTBaseParenExprList: + raise NotImplementedError diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index 398accf1a..7ead23683 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -238,7 +238,7 @@ def get_image_filename_for_language(filename: str, env: "BuildEnvironment") -> s try: return filename_format.format(**d) except KeyError as exc: - raise SphinxError('Invalid figure_language_filename: %r' % exc) + raise SphinxError('Invalid figure_language_filename: %r' % exc) from exc def search_image_for_language(filename: str, env: "BuildEnvironment") -> str: diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 036bbc364..8e85ea392 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -322,7 +322,7 @@ def safe_getattr(obj: Any, name: str, *defargs: Any) -> Any: """A getattr() that turns all exceptions into AttributeErrors.""" try: return getattr(obj, name, *defargs) - except Exception: + except Exception as exc: # sometimes accessing a property raises an exception (e.g. # NotImplementedError), so let's try to read the attribute directly try: @@ -337,7 +337,7 @@ def safe_getattr(obj: Any, name: str, *defargs: Any) -> Any: if defargs: return defargs[0] - raise AttributeError(name) + raise AttributeError(name) from exc def object_description(object: Any) -> str: @@ -369,8 +369,8 @@ def object_description(object: Any) -> str: for x in sorted_values) try: s = repr(object) - except Exception: - raise ValueError + except Exception as exc: + raise ValueError from exc # Strip non-deterministic memory addresses such as # ``<__main__.A at 0x7f68cb685710>`` s = memory_address_re.sub('', s) diff --git a/sphinx/util/osutil.py b/sphinx/util/osutil.py index e3c4ebedb..bd774cdad 100644 --- a/sphinx/util/osutil.py +++ b/sphinx/util/osutil.py @@ -148,10 +148,10 @@ def abspath(pathdir: str) -> str: if isinstance(pathdir, bytes): try: pathdir = pathdir.decode(fs_encoding) - except UnicodeDecodeError: + except UnicodeDecodeError as exc: raise UnicodeDecodeError('multibyte filename not supported on ' 'this filesystem encoding ' - '(%r)' % fs_encoding) + '(%r)' % fs_encoding) from exc return pathdir diff --git a/sphinx/util/pycompat.py b/sphinx/util/pycompat.py index 7d22a792a..5aeb0f9cc 100644 --- a/sphinx/util/pycompat.py +++ b/sphinx/util/pycompat.py @@ -26,11 +26,11 @@ def convert_with_2to3(filepath: str) -> str: try: from lib2to3.refactor import RefactoringTool, get_fixers_from_package from lib2to3.pgen2.parse import ParseError - except ImportError: + except ImportError as exc: # python 3.9.0a6+ emits PendingDeprecationWarning for lib2to3. # Additionally, removal of the module is still discussed at PEP-594. # To support future python, this catches ImportError for lib2to3. - raise SyntaxError + raise SyntaxError from exc fixers = get_fixers_from_package('lib2to3.fixes') refactoring_tool = RefactoringTool(fixers) @@ -41,7 +41,8 @@ def convert_with_2to3(filepath: str) -> str: # do not propagate lib2to3 exceptions lineno, offset = err.context[1] # try to match ParseError details with SyntaxError details - raise SyntaxError(err.msg, (filepath, lineno, offset, err.value)) + + raise SyntaxError(err.msg, (filepath, lineno, offset, err.value)) from err return str(tree) diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index bd30a73ee..317c6231b 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -852,8 +852,8 @@ class TexinfoTranslator(SphinxTranslator): num = node.astext().strip() try: footnode, used = self.footnotestack[-1][num] - except (KeyError, IndexError): - raise nodes.SkipNode + except (KeyError, IndexError) as exc: + raise nodes.SkipNode from exc # footnotes are repeated for each reference footnode.walkabout(self) # type: ignore raise nodes.SkipChildren diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py index 7ac5f52c6..858c68806 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -222,7 +222,7 @@ class Table: for left, right in zip(out, out[1:]) ] glue.append(tail) - return head + "".join(chain(*zip(out, glue))) + return head + "".join(chain.from_iterable(zip(out, glue))) for lineno, line in enumerate(self.lines): if self.separator and lineno == self.separator: diff --git a/tests/roots/test-ext-autodoc/target/name_conflict/__init__.py b/tests/roots/test-ext-autodoc/target/name_conflict/__init__.py new file mode 100644 index 000000000..7ad521fc2 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/name_conflict/__init__.py @@ -0,0 +1,5 @@ +from .foo import bar + +class foo: + """docstring of target.name_conflict::foo.""" + pass diff --git a/tests/roots/test-ext-autodoc/target/name_conflict/foo.py b/tests/roots/test-ext-autodoc/target/name_conflict/foo.py new file mode 100644 index 000000000..bb83ca0d4 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/name_conflict/foo.py @@ -0,0 +1,2 @@ +class bar: + """docstring of target.name_conflict.foo::bar.""" diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index eaf77189b..818a493a5 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -63,8 +63,8 @@ def compile_latex_document(app, filename='python.tex'): '-output-directory=%s' % app.config.latex_engine, filename] subprocess.run(args, stdout=PIPE, stderr=PIPE, check=True) - except OSError: # most likely the latex executable was not found - raise pytest.skip.Exception + except OSError as exc: # most likely the latex executable was not found + raise pytest.skip.Exception from exc except CalledProcessError as exc: print(exc.stdout) print(exc.stderr) @@ -1538,7 +1538,7 @@ def test_texescape_for_unicode_supported_engine(app, status, warning): assert 'superscript: ⁰, ¹' in result assert 'subscript: ₀, ₁' in result - + @pytest.mark.sphinx('latex', testroot='basic', confoverrides={'latex_elements': {'extrapackages': r'\usepackage{foo}'}}) def test_latex_elements_extrapackages(app, status, warning): diff --git a/tests/test_build_texinfo.py b/tests/test_build_texinfo.py index 378eaa192..9833218d7 100644 --- a/tests/test_build_texinfo.py +++ b/tests/test_build_texinfo.py @@ -58,8 +58,8 @@ def test_texinfo(app, status, warning): try: args = ['makeinfo', '--no-split', 'sphinxtests.texi'] subprocess.run(args, stdout=PIPE, stderr=PIPE, cwd=app.outdir, check=True) - except OSError: - raise pytest.skip.Exception # most likely makeinfo was not found + except OSError as exc: + raise pytest.skip.Exception from exc # most likely makeinfo was not found except CalledProcessError as exc: print(exc.stdout) print(exc.stderr) diff --git a/tests/test_domain_c.py b/tests/test_domain_c.py index 10a20b9d1..347f8c7d6 100644 --- a/tests/test_domain_c.py +++ b/tests/test_domain_c.py @@ -469,6 +469,8 @@ def test_attributes(): check('member', '__attribute__(()) int f', {1: 'f'}) check('member', '__attribute__((a)) int f', {1: 'f'}) check('member', '__attribute__((a, b)) int f', {1: 'f'}) + check('member', '__attribute__((optimize(3))) int f', {1: 'f'}) + check('member', '__attribute__((format(printf, 1, 2))) int f', {1: 'f'}) # style: user-defined id check('member', 'id_attr int f', {1: 'f'}) # style: user-defined paren diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index 48227dddc..bf355ae1f 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -896,6 +896,8 @@ def test_attributes(): check('member', '__attribute__(()) int f', {1: 'f__i', 2: '1f'}) check('member', '__attribute__((a)) int f', {1: 'f__i', 2: '1f'}) check('member', '__attribute__((a, b)) int f', {1: 'f__i', 2: '1f'}) + check('member', '__attribute__((optimize(3))) int f', {1: 'f__i', 2: '1f'}) + check('member', '__attribute__((format(printf, 1, 2))) int f', {1: 'f__i', 2: '1f'}) # style: user-defined id check('member', 'id_attr int f', {1: 'f__i', 2: '1f'}) # style: user-defined paren diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index e4ec4a815..652c7f10b 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -121,15 +121,16 @@ def test_parse_name(app): verify('class', 'Base', ('test_ext_autodoc', ['Base'], None, None)) # for members - directive.env.ref_context['py:module'] = 'foo' - verify('method', 'util.SphinxTestApp.cleanup', - ('foo', ['util', 'SphinxTestApp', 'cleanup'], None, None)) - directive.env.ref_context['py:module'] = 'util' + directive.env.ref_context['py:module'] = 'sphinx.testing.util' + verify('method', 'SphinxTestApp.cleanup', + ('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None)) + directive.env.ref_context['py:module'] = 'sphinx.testing.util' directive.env.ref_context['py:class'] = 'Foo' directive.env.temp_data['autodoc:class'] = 'SphinxTestApp' - verify('method', 'cleanup', ('util', ['SphinxTestApp', 'cleanup'], None, None)) + verify('method', 'cleanup', + ('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None)) verify('method', 'SphinxTestApp.cleanup', - ('util', ['SphinxTestApp', 'cleanup'], None, None)) + ('sphinx.testing.util', ['SphinxTestApp', 'cleanup'], None, None)) def test_format_signature(app): @@ -800,14 +801,14 @@ def test_autodoc_inner_class(app): actual = do_autodoc(app, 'class', 'target.Outer.Inner', options) assert list(actual) == [ '', - '.. py:class:: Outer.Inner()', - ' :module: target', + '.. py:class:: Inner()', + ' :module: target.Outer', '', ' Foo', '', '', - ' .. py:method:: Outer.Inner.meth()', - ' :module: target', + ' .. py:method:: Inner.meth()', + ' :module: target.Outer', '', ' Foo', '', @@ -1881,6 +1882,43 @@ def test_overload(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_pymodule_for_ModuleLevelDocumenter(app): + app.env.ref_context['py:module'] = 'target.classes' + actual = do_autodoc(app, 'class', 'Foo') + assert list(actual) == [ + '', + '.. py:class:: Foo()', + ' :module: target.classes', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_pymodule_for_ClassLevelDocumenter(app): + app.env.ref_context['py:module'] = 'target.methods' + actual = do_autodoc(app, 'method', 'Base.meth') + assert list(actual) == [ + '', + '.. py:method:: Base.meth()', + ' :module: target.methods', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_pyclass_for_ClassLevelDocumenter(app): + app.env.ref_context['py:module'] = 'target.methods' + app.env.ref_context['py:class'] = 'Base' + actual = do_autodoc(app, 'method', 'meth') + assert list(actual) == [ + '', + '.. py:method:: Base.meth()', + ' :module: target.methods', + '', + ] + + @pytest.mark.sphinx('dummy', testroot='ext-autodoc') def test_autodoc(app, status, warning): app.builder.build_all() @@ -1899,3 +1937,26 @@ my_name alias of bug2437.autodoc_dummy_foo.Foo""" assert warning.getvalue() == '' + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_name_conflict(app): + actual = do_autodoc(app, 'class', 'target.name_conflict.foo') + assert list(actual) == [ + '', + '.. py:class:: foo()', + ' :module: target.name_conflict', + '', + ' docstring of target.name_conflict::foo.', + '', + ] + + actual = do_autodoc(app, 'class', 'target.name_conflict.foo.bar') + assert list(actual) == [ + '', + '.. py:class:: bar()', + ' :module: target.name_conflict.foo', + '', + ' docstring of target.name_conflict.foo::bar.', + '', + ] diff --git a/tests/test_ext_autodoc_autofunction.py b/tests/test_ext_autodoc_autofunction.py index 579ad9f48..da090d83a 100644 --- a/tests/test_ext_autodoc_autofunction.py +++ b/tests/test_ext_autodoc_autofunction.py @@ -85,8 +85,8 @@ def test_methoddescriptor(app): actual = do_autodoc(app, 'function', 'builtins.int.__add__') assert list(actual) == [ '', - '.. py:function:: int.__add__(self, value, /)', - ' :module: builtins', + '.. py:function:: __add__(self, value, /)', + ' :module: builtins.int', '', ' Return self+value.', '', diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index 674620df0..88519b5fd 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -13,6 +13,8 @@ import sys import pytest +from sphinx.testing import restructuredtext + from test_ext_autodoc import do_autodoc IS_PYPY = platform.python_implementation() == 'PyPy' @@ -633,6 +635,12 @@ def test_autodoc_typehints_description(app): in context) +@pytest.mark.sphinx('text', testroot='ext-autodoc', + confoverrides={'autodoc_typehints': "description"}) +def test_autodoc_typehints_description_for_invalid_node(app): + text = ".. py:function:: hello; world" + restructuredtext.parse(app, text) # raises no error + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_default_options(app): diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index a65826141..3dd90b8ce 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -97,7 +97,7 @@ def test_extract_summary(capsys): # abbreviations doc = ['Blabla, i.e. bla.'] - assert extract_summary(doc, document) == 'Blabla, i.e.' + assert extract_summary(doc, document) == ' '.join(doc) # literal doc = ['blah blah::'] @@ -108,6 +108,12 @@ def test_extract_summary(capsys): '========='] assert extract_summary(doc, document) == 'blah blah' + # hyperlink target + doc = ['Do `this <https://www.sphinx-doc.org/>`_ and that. ' + 'blah blah blah.'] + assert (extract_summary(doc, document) == + 'Do `this <https://www.sphinx-doc.org/>`_ and that.') + _, err = capsys.readouterr() assert err == '' @@ -1,6 +1,6 @@ [tox] minversion = 2.4.0 -envlist = docs,flake8,mypy,coverage,py{36,37,38,39},du{14,15,16} +envlist = docs,flake8,mypy,twine,coverage,py{36,37,38,39},du{14,15,16} [testenv] usedevelop = True @@ -86,6 +86,16 @@ extras = commands = python utils/doclinter.py CHANGES CONTRIBUTING.rst README.rst doc/ +[testenv:twine] +basepython = python3 +description = + Lint package. +deps = + twine +commands = + python setup.py release bdist_wheel sdist + twine check dist/* + [testenv:bindep] description = Install binary dependencies. diff --git a/utils/release-checklist b/utils/release-checklist index 5a60e59c8..582d26685 100644 --- a/utils/release-checklist +++ b/utils/release-checklist @@ -11,7 +11,6 @@ for stable releases * ``git commit -am 'Bump to X.Y.Z final'`` * ``make clean`` * ``python setup.py release bdist_wheel sdist`` -* ``twine check dist/Sphinx-*`` * ``twine upload dist/Sphinx-* --sign --identity [your GPG key]`` * open https://pypi.org/project/Sphinx/ and check there are no obvious errors * ``sh utils/bump_docker.sh X.Y.Z`` @@ -38,7 +37,6 @@ for first beta releases * ``git commit -am 'Bump to X.Y.0 beta1'`` * ``make clean`` * ``python setup.py release bdist_wheel sdist`` -* ``twine check dist/Sphinx-*`` * ``twine upload dist/Sphinx-* --sign --identity [your GPG key]`` * open https://pypi.org/project/Sphinx/ and check there are no obvious errors * ``git tag vX.Y.0b1`` @@ -67,7 +65,6 @@ for other beta releases * ``git commit -am 'Bump to X.Y.0 betaN'`` * ``make clean`` * ``python setup.py release bdist_wheel sdist`` -* ``twine check dist/Sphinx-*`` * ``twine upload dist/Sphinx-* --sign --identity [your GPG key]`` * open https://pypi.org/project/Sphinx/ and check there are no obvious errors * ``git tag vX.Y.0bN`` @@ -95,7 +92,6 @@ for major releases * ``git commit -am 'Bump to X.Y.0 final'`` * ``make clean`` * ``python setup.py release bdist_wheel sdist`` -* ``twine check dist/Sphinx-*`` * ``twine upload dist/Sphinx-* --sign --identity [your GPG key]`` * open https://pypi.org/project/Sphinx/ and check there are no obvious errors * ``sh utils/bump_docker.sh X.Y.Z`` |