From 4de540efb64c589dd9c8d23b5b76d093355ce055 Mon Sep 17 00:00:00 2001 From: danieleades <33452915+danieleades@users.noreply.github.com> Date: Sun, 23 Jul 2023 21:29:04 +0100 Subject: [PATCH] Enable mypy 'strict optional' for 19 modules (#11422) Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com> --- pyproject.toml | 19 ----- sphinx/builders/html/__init__.py | 9 ++- sphinx/builders/linkcheck.py | 6 +- sphinx/builders/xml.py | 1 + sphinx/domains/__init__.py | 4 +- sphinx/domains/cpp.py | 46 ++++++----- sphinx/domains/javascript.py | 8 +- sphinx/domains/rst.py | 9 ++- sphinx/environment/adapters/toctree.py | 2 +- sphinx/ext/autodoc/mock.py | 2 +- sphinx/ext/autodoc/preserve_defaults.py | 3 +- sphinx/ext/doctest.py | 23 +++--- sphinx/ext/graphviz.py | 1 + sphinx/ext/linkcode.py | 1 + sphinx/ext/napoleon/__init__.py | 4 +- sphinx/locale/__init__.py | 4 +- sphinx/pycode/parser.py | 23 +++--- sphinx/testing/util.py | 1 + sphinx/transforms/__init__.py | 4 +- sphinx/transforms/i18n.py | 8 +- sphinx/transforms/post_transforms/images.py | 16 ++-- sphinx/util/cfamily.py | 6 +- sphinx/util/docfields.py | 89 +++++++++++++++------ sphinx/util/docutils.py | 10 +-- sphinx/util/images.py | 12 ++- sphinx/util/nodes.py | 3 +- sphinx/writers/text.py | 25 ++++-- sphinx/writers/xml.py | 4 +- tests/test_ext_doctest.py | 19 +++-- 29 files changed, 217 insertions(+), 145 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 94f0133ef..d6f0ef980 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -309,42 +309,23 @@ disallow_any_generics = true module = [ "sphinx.builders.html", "sphinx.builders.latex", - "sphinx.builders.linkcheck", - "sphinx.domains", "sphinx.domains.c", "sphinx.domains.cpp", - "sphinx.domains.javascript", "sphinx.domains.python", "sphinx.domains.std", "sphinx.environment", - "sphinx.environment.adapters.toctree", "sphinx.ext.apidoc", "sphinx.ext.autodoc", - "sphinx.ext.autodoc.mock", "sphinx.ext.autodoc.importer", - "sphinx.ext.autodoc.preserve_defaults", "sphinx.ext.autosummary", "sphinx.ext.autosummary.generate", - "sphinx.ext.doctest", - "sphinx.ext.graphviz", "sphinx.ext.inheritance_diagram", "sphinx.ext.intersphinx", "sphinx.ext.imgmath", - "sphinx.ext.linkcode", "sphinx.ext.mathjax", - "sphinx.ext.napoleon", "sphinx.ext.napoleon.docstring", - "sphinx.pycode.parser", "sphinx.registry", - "sphinx.testing.util", - "sphinx.transforms.i18n", - "sphinx.transforms.post_transforms.images", - "sphinx.util.cfamily", - "sphinx.util.docfields", - "sphinx.util.docutils", "sphinx.writers.latex", - "sphinx.writers.text", - "sphinx.writers.xml", ] strict_optional = false diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 5eb526c29..922397bd4 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -160,7 +160,10 @@ class BuildInfo: raise ValueError(__('build info file is broken: %r') % exc) from exc def __init__( - self, config: Config = None, tags: Tags = None, config_categories: list[str] = [], + self, + config: Config | None = None, + tags: Tags | None = None, + config_categories: list[str] = [], ) -> None: self.config_hash = '' self.tags_hash = '' @@ -217,7 +220,7 @@ class StandaloneHTMLBuilder(Builder): imgpath: str = None domain_indices: list[DOMAIN_INDEX_TYPE] = [] - def __init__(self, app: Sphinx, env: BuildEnvironment = None) -> None: + def __init__(self, app: Sphinx, env: BuildEnvironment | None = None) -> None: super().__init__(app, env) # CSS files @@ -1016,7 +1019,7 @@ class StandaloneHTMLBuilder(Builder): # --------- these are overwritten by the serialization builder - def get_target_uri(self, docname: str, typ: str = None) -> str: + def get_target_uri(self, docname: str, typ: str | None = None) -> str: return quote(docname) + self.link_suffix def handle_page(self, pagename: str, addctx: dict, templatename: str = 'page.html', diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 428669349..983ef3671 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -174,7 +174,7 @@ def _add_uri(app: Sphinx, uri: str, node: nodes.Element, try: lineno = get_node_line(node) except ValueError: - lineno = None + lineno = -1 if uri not in hyperlinks: hyperlinks[uri] = Hyperlink(uri, docname, app.env.doc2path(docname), lineno) @@ -184,7 +184,7 @@ class Hyperlink(NamedTuple): uri: str docname: str docpath: str - lineno: int | None + lineno: int class HyperlinkAvailabilityChecker: @@ -374,7 +374,7 @@ class HyperlinkAvailabilityCheckWorker(Thread): # - Attempt HTTP HEAD before HTTP GET unless page content is required. # - Follow server-issued HTTP redirects. # - Respect server-issued HTTP 429 back-offs. - error_message = None + error_message = '' status_code = -1 response_url = retry_after = '' for retrieval_method, kwargs in _retrieval_methods(self.check_anchors, anchor): diff --git a/sphinx/builders/xml.py b/sphinx/builders/xml.py index e8c29dfcd..c6ac752b4 100644 --- a/sphinx/builders/xml.py +++ b/sphinx/builders/xml.py @@ -32,6 +32,7 @@ class XMLBuilder(Builder): allow_parallel = True _writer_class: type[XMLWriter] | type[PseudoXMLWriter] = XMLWriter + writer: XMLWriter | PseudoXMLWriter default_translator_class = XMLTranslator def init(self) -> None: diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py index 19d464d57..8e4d22f1a 100644 --- a/sphinx/domains/__init__.py +++ b/sphinx/domains/__init__.py @@ -226,8 +226,8 @@ class Domain: for rolename in obj.roles: self._role2type.setdefault(rolename, []).append(name) self._type2role[name] = obj.roles[0] if obj.roles else '' - self.objtypes_for_role: Callable[[str], list[str]] = self._role2type.get - self.role_for_objtype: Callable[[str], str] = self._type2role.get + self.objtypes_for_role = self._role2type.get + self.role_for_objtype = self._type2role.get def setup(self) -> None: """Set up domain object.""" diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 41f2bd076..5b0a24163 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -2013,7 +2013,9 @@ class ASTFunctionParameter(ASTBase): self.arg = arg self.ellipsis = ellipsis - def get_id(self, version: int, objectType: str = None, symbol: Symbol = None) -> str: + def get_id( + self, version: int, objectType: str | None = None, symbol: Symbol | None = None, + ) -> str: # this is not part of the normal name mangling in C++ if symbol: # the anchor will be our parent @@ -3114,8 +3116,8 @@ class ASTType(ASTBase): def trailingReturn(self) -> ASTType: return self.decl.trailingReturn - def get_id(self, version: int, objectType: str = None, - symbol: Symbol = None) -> str: + def get_id(self, version: int, objectType: str | None = None, + symbol: Symbol | None = None) -> str: if version == 1: res = [] if objectType: # needs the name @@ -3211,7 +3213,9 @@ class ASTTemplateParamConstrainedTypeWithInit(ASTBase): def isPack(self) -> bool: return self.type.isPack - def get_id(self, version: int, objectType: str = None, symbol: Symbol = None) -> str: + def get_id( + self, version: int, objectType: str | None = None, symbol: Symbol | None = None, + ) -> str: # this is not part of the normal name mangling in C++ assert version >= 2 if symbol: @@ -3250,8 +3254,8 @@ class ASTTypeWithInit(ASTBase): def isPack(self) -> bool: return self.type.isPack - def get_id(self, version: int, objectType: str = None, - symbol: Symbol = None) -> str: + def get_id(self, version: int, objectType: str | None = None, + symbol: Symbol | None = None) -> str: if objectType != 'member': return self.type.get_id(version, objectType) if version == 1: @@ -3279,8 +3283,8 @@ class ASTTypeUsing(ASTBase): self.name = name self.type = type - def get_id(self, version: int, objectType: str = None, - symbol: Symbol = None) -> str: + def get_id(self, version: int, objectType: str | None = None, + symbol: Symbol | None = None) -> str: if version == 1: raise NoOldIdError() return symbol.get_full_nested_name().get_id(version) @@ -3319,8 +3323,8 @@ class ASTConcept(ASTBase): def name(self) -> ASTNestedName: return self.nestedName - def get_id(self, version: int, objectType: str = None, - symbol: Symbol = None) -> str: + def get_id(self, version: int, objectType: str | None = None, + symbol: Symbol | None = None) -> str: if version == 1: raise NoOldIdError() return symbol.get_full_nested_name().get_id(version) @@ -3628,7 +3632,9 @@ class ASTTemplateParamType(ASTTemplateParam): def get_identifier(self) -> ASTIdentifier: return self.data.get_identifier() - def get_id(self, version: int, objectType: str = None, symbol: Symbol = None) -> str: + def get_id( + self, version: int, objectType: str | None = None, symbol: Symbol | None = None, + ) -> str: # this is not part of the normal name mangling in C++ assert version >= 2 if symbol: @@ -3715,7 +3721,9 @@ class ASTTemplateParamNonType(ASTTemplateParam): else: return None - def get_id(self, version: int, objectType: str = None, symbol: Symbol = None) -> str: + def get_id( + self, version: int, objectType: str | None = None, symbol: Symbol | None = None, + ) -> str: assert version >= 2 # this is not part of the normal name mangling in C++ if symbol: @@ -3836,7 +3844,9 @@ class ASTTemplateIntroductionParameter(ASTBase): def get_identifier(self) -> ASTIdentifier: return self.identifier - def get_id(self, version: int, objectType: str = None, symbol: Symbol = None) -> str: + def get_id( + self, version: int, objectType: str | None = None, symbol: Symbol | None = None, + ) -> str: assert version >= 2 # this is not part of the normal name mangling in C++ if symbol: @@ -4933,7 +4943,7 @@ class Symbol: Symbol.debug_indent -= 2 def add_name(self, nestedName: ASTNestedName, - templatePrefix: ASTTemplateDeclarationPrefix = None) -> Symbol: + templatePrefix: ASTTemplateDeclarationPrefix | None = None) -> Symbol: if Symbol.debug_lookup: Symbol.debug_indent += 1 Symbol.debug_print("add_name:") @@ -6543,7 +6553,7 @@ class DefinitionParser(BaseParser): header = "Error in declarator or parameters-and-qualifiers" raise self._make_multi_error(prevErrors, header) from e - def _parse_initializer(self, outer: str = None, allowFallback: bool = True, + def _parse_initializer(self, outer: str | None = None, allowFallback: bool = True, ) -> ASTInitializer: # initializer # global vars # -> brace-or-equal-initializer @@ -6592,7 +6602,7 @@ class DefinitionParser(BaseParser): value = self._parse_expression_fallback(fallbackEnd, parser, allow=allowFallback) return ASTInitializer(value) - def _parse_type(self, named: bool | str, outer: str = None) -> ASTType: + def _parse_type(self, named: bool | str, outer: str | None = None) -> ASTType: """ named=False|'maybe'|True: 'maybe' is e.g., for function objects which doesn't need to name the arguments @@ -7571,8 +7581,8 @@ class CPPNamespacePopObject(SphinxDirective): class AliasNode(nodes.Element): def __init__(self, sig: str, aliasOptions: dict, - env: BuildEnvironment = None, - parentKey: LookupKey = None) -> None: + env: BuildEnvironment | None = None, + parentKey: LookupKey | None = None) -> None: super().__init__() self.sig = sig self.aliasOptions = aliasOptions diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index c6baab8a9..885d84e61 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -143,7 +143,7 @@ class JSObject(ObjectDescription[Tuple[str, str]]): domain.note_object(fullname, self.objtype, node_id, location=signode) if 'noindexentry' not in self.options: - indextext = self.get_index_text(mod_name, name_obj) + indextext = self.get_index_text(mod_name, name_obj) # type: ignore[arg-type] if indextext: self.indexnode['entries'].append(('single', indextext, node_id, '', None)) @@ -438,11 +438,13 @@ class JavaScriptDomain(Domain): searches.reverse() newname = None + object_ = None for search_name in searches: if search_name in self.objects: newname = search_name + object_ = self.objects[search_name] - return newname, self.objects.get(newname) + return newname, object_ def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element, @@ -463,7 +465,7 @@ class JavaScriptDomain(Domain): name, obj = self.find_obj(env, mod_name, prefix, target, None, 1) if not obj: return [] - return [('js:' + self.role_for_objtype(obj[2]), + return [('js:' + self.role_for_objtype(obj[2]), # type: ignore[operator] make_refnode(builder, fromdocname, obj[0], obj[1], contnode, name))] def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index 84a651231..c81e40e95 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -250,6 +250,8 @@ class ReSTDomain(Domain): typ: str, target: str, node: pending_xref, contnode: Element, ) -> Element | None: objtypes = self.objtypes_for_role(typ) + if not objtypes: + return None for objtype in objtypes: result = self.objects.get((objtype, target)) if result: @@ -266,9 +268,10 @@ class ReSTDomain(Domain): result = self.objects.get((objtype, target)) if result: todocname, node_id = result - results.append(('rst:' + self.role_for_objtype(objtype), - make_refnode(builder, fromdocname, todocname, node_id, - contnode, target + ' ' + objtype))) + results.append( + ('rst:' + self.role_for_objtype(objtype), # type: ignore[operator] + make_refnode(builder, fromdocname, todocname, node_id, + contnode, target + ' ' + objtype))) return results def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]: diff --git a/sphinx/environment/adapters/toctree.py b/sphinx/environment/adapters/toctree.py index a7e9eca89..4f651e09a 100644 --- a/sphinx/environment/adapters/toctree.py +++ b/sphinx/environment/adapters/toctree.py @@ -212,7 +212,7 @@ class TocTree: if sub_toc_node.get('hidden', False) and not includehidden: continue for i, entry in enumerate( - _entries_from_toctree(sub_toc_node, [refdoc] + parents, + _entries_from_toctree(sub_toc_node, [refdoc or ''] + parents, subtree=True), start=sub_toc_node.parent.index(sub_toc_node) + 1, ): diff --git a/sphinx/ext/autodoc/mock.py b/sphinx/ext/autodoc/mock.py index bae0504ae..bbec2ec6f 100644 --- a/sphinx/ext/autodoc/mock.py +++ b/sphinx/ext/autodoc/mock.py @@ -117,7 +117,7 @@ class MockFinder(MetaPathFinder): self.mocked_modules: list[str] = [] def find_spec(self, fullname: str, path: Sequence[bytes | str] | None, - target: ModuleType = None) -> ModuleSpec | None: + target: ModuleType | None = None) -> ModuleSpec | None: for modname in self.modnames: # check if fullname is (or is a descendant of) one of our targets if modname == fullname or fullname.startswith(modname + '.'): diff --git a/sphinx/ext/autodoc/preserve_defaults.py b/sphinx/ext/autodoc/preserve_defaults.py index a0ceb1ac2..0d58ae54e 100644 --- a/sphinx/ext/autodoc/preserve_defaults.py +++ b/sphinx/ext/autodoc/preserve_defaults.py @@ -72,6 +72,7 @@ def update_defvalue(app: Sphinx, obj: Any, bound_method: bool) -> None: try: function = get_function_def(obj) + assert function is not None # for mypy if function.args.defaults or function.args.kw_defaults: sig = inspect.signature(obj) defaults = list(function.args.defaults) @@ -90,7 +91,7 @@ def update_defvalue(app: Sphinx, obj: Any, bound_method: bool) -> None: value = ast_unparse(default) parameters[i] = param.replace(default=DefaultValue(value)) else: - default = kw_defaults.pop(0) + default = kw_defaults.pop(0) # type: ignore[assignment] value = get_default_value(lines, default) if value is None: value = ast_unparse(default) diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index 7eadce804..10eb0fc82 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -192,7 +192,7 @@ class TestGroup: def __init__(self, name: str) -> None: self.name = name self.setup: list[TestCode] = [] - self.tests: list[list[TestCode]] = [] + self.tests: list[list[TestCode] | tuple[TestCode, None]] = [] self.cleanup: list[TestCode] = [] def add_code(self, code: TestCode, prepend: bool = False) -> None: @@ -206,10 +206,13 @@ class TestGroup: elif code.type == 'doctest': self.tests.append([code]) elif code.type == 'testcode': - self.tests.append([code, None]) + # "testoutput" may replace the second element + self.tests.append((code, None)) elif code.type == 'testoutput': - if self.tests and len(self.tests[-1]) == 2: - self.tests[-1][1] = code + if self.tests: + latest_test = self.tests[-1] + if len(latest_test) == 2: + self.tests[-1] = [latest_test[0], code] else: raise RuntimeError(__('invalid TestCode type')) @@ -233,7 +236,7 @@ class TestCode: class SphinxDocTestRunner(doctest.DocTestRunner): - def summarize(self, out: Callable, verbose: bool = None, # type: ignore + def summarize(self, out: Callable, verbose: bool | None = None, # type: ignore ) -> tuple[int, int]: string_io = StringIO() old_stdout = sys.stdout @@ -339,7 +342,7 @@ Doctest summary if self.total_failures or self.setup_failures or self.cleanup_failures: self.app.statuscode = 1 - def write(self, build_docnames: Iterable[str], updated_docnames: Sequence[str], + def write(self, build_docnames: Iterable[str] | None, updated_docnames: Sequence[str], method: str = 'update') -> None: if build_docnames is None: build_docnames = sorted(self.env.all_docs) @@ -361,7 +364,7 @@ Doctest summary return filename @staticmethod - def get_line_number(node: Node) -> int | None: + def get_line_number(node: Node) -> int: """Get the real line number or admit we don't know.""" # TODO: Work out how to store or calculate real (file-relative) # line numbers for doctest blocks in docstrings. @@ -370,7 +373,7 @@ Doctest summary # not the file. This is correct where it is set, in # `docutils.nodes.Node.setup_child`, but Sphinx should report # relative to the file, not the docstring. - return None + return None # type: ignore[return-value] if node.line is not None: # TODO: find the root cause of this off by one error. return node.line - 1 @@ -438,12 +441,12 @@ Doctest summary group.add_code(code) if self.config.doctest_global_setup: code = TestCode(self.config.doctest_global_setup, - 'testsetup', filename=None, lineno=0) + 'testsetup', filename='', lineno=0) for group in groups.values(): group.add_code(code, prepend=True) if self.config.doctest_global_cleanup: code = TestCode(self.config.doctest_global_cleanup, - 'testcleanup', filename=None, lineno=0) + 'testcleanup', filename='', lineno=0) for group in groups.values(): group.add_code(code) if not groups: diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index 37626e04f..941c08372 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -298,6 +298,7 @@ def render_dot_html(self: HTML5Translator, node: graphviz, code: str, options: d self.body.append('

%s

' % alt) self.body.append('\n') else: + assert outfn is not None with open(outfn + '.map', encoding='utf-8') as mapfile: imgmap = ClickableMapDefinition(outfn + '.map', mapfile.read(), dot=code) if imgmap.clickable: diff --git a/sphinx/ext/linkcode.py b/sphinx/ext/linkcode.py index 5cee2f05a..259a5d390 100644 --- a/sphinx/ext/linkcode.py +++ b/sphinx/ext/linkcode.py @@ -25,6 +25,7 @@ def doctree_read(app: Sphinx, doctree: Node) -> None: if not callable(env.config.linkcode_resolve): raise LinkcodeError( "Function `linkcode_resolve` is not given in conf.py") + assert resolve_target is not None # for mypy domain_keys = { 'py': ['module', 'fullname'], diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py index 887dab3c6..18b851370 100644 --- a/sphinx/ext/napoleon/__init__.py +++ b/sphinx/ext/napoleon/__init__.py @@ -377,7 +377,7 @@ def _process_docstring(app: Sphinx, what: str, name: str, obj: Any, """ result_lines = lines - docstring: GoogleDocstring = None + docstring: GoogleDocstring if app.config.napoleon_numpy_docstring: docstring = NumpyDocstring(result_lines, app.config, app, what, name, obj, options) @@ -390,7 +390,7 @@ def _process_docstring(app: Sphinx, what: str, name: str, obj: Any, def _skip_member(app: Sphinx, what: str, name: str, obj: Any, - skip: bool, options: Any) -> bool: + skip: bool, options: Any) -> bool | None: """Determine if private and special class members are included in docs. The following settings in conf.py determine if private and special class diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index 8ab90d191..46f629ca3 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations import locale from gettext import NullTranslations, translation from os import path -from typing import Any, Callable +from typing import Any, Callable, Iterable class _TranslationProxy: @@ -90,7 +90,7 @@ translators: dict[tuple[str, str], NullTranslations] = {} def init( - locale_dirs: list[str | None], + locale_dirs: Iterable[str | None], language: str | None, catalog: str = 'sphinx', namespace: str = 'general', diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index aecb095b2..fba10df27 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -145,7 +145,7 @@ class TokenProcessor: return self.current - def fetch_until(self, condition: Any) -> list[Token]: + def fetch_until(self, condition: Any) -> list[Token | None]: """Fetch tokens until specified token appeared. .. note:: This also handles parenthesis well. @@ -176,7 +176,7 @@ class AfterCommentParser(TokenProcessor): super().__init__(lines) self.comment: str | None = None - def fetch_rvalue(self) -> list[Token]: + def fetch_rvalue(self) -> list[Token | None]: """Fetch right-hand value of assignment.""" tokens = [] while self.fetch_token(): @@ -191,7 +191,7 @@ class AfterCommentParser(TokenProcessor): tokens += self.fetch_until(DEDENT) elif self.current == [OP, ';']: # NoQA: SIM114 break - elif self.current.kind not in (OP, NAME, NUMBER, STRING): + elif self.current and self.current.kind not in {OP, NAME, NUMBER, STRING}: break return tokens @@ -199,7 +199,7 @@ class AfterCommentParser(TokenProcessor): def parse(self) -> None: """Parse the code and obtain comment after assignment.""" # skip lvalue (or whole of AnnAssign) - while not self.fetch_token().match([OP, '='], NEWLINE, COMMENT): + while (tok := self.fetch_token()) and not tok.match([OP, '='], NEWLINE, COMMENT): assert self.current # skip rvalue (if exists) @@ -207,7 +207,7 @@ class AfterCommentParser(TokenProcessor): self.fetch_rvalue() if self.current == COMMENT: - self.comment = self.current.value + self.comment = self.current.value # type: ignore[union-attr] class VariableCommentPicker(ast.NodeVisitor): @@ -502,22 +502,23 @@ class DefinitionFinder(TokenProcessor): def parse_definition(self, typ: str) -> None: """Parse AST of definition.""" name = self.fetch_token() - self.context.append(name.value) + self.context.append(name.value) # type: ignore[union-attr] funcname = '.'.join(self.context) if self.decorator: start_pos = self.decorator.start[0] self.decorator = None else: - start_pos = name.start[0] + start_pos = name.start[0] # type: ignore[union-attr] self.fetch_until([OP, ':']) - if self.fetch_token().match(COMMENT, NEWLINE): + if self.fetch_token().match(COMMENT, NEWLINE): # type: ignore[union-attr] self.fetch_until(INDENT) self.indents.append((typ, funcname, start_pos)) else: # one-liner - self.add_definition(funcname, (typ, start_pos, name.end[0])) + self.add_definition(funcname, + (typ, start_pos, name.end[0])) # type: ignore[union-attr] self.context.pop() def finalize_block(self) -> None: @@ -525,11 +526,11 @@ class DefinitionFinder(TokenProcessor): definition = self.indents.pop() if definition[0] != 'other': typ, funcname, start_pos = definition - end_pos = self.current.end[0] - 1 + end_pos = self.current.end[0] - 1 # type: ignore[union-attr] while emptyline_re.match(self.get_line(end_pos)): end_pos -= 1 - self.add_definition(funcname, (typ, start_pos, end_pos)) + self.add_definition(funcname, (typ, start_pos, end_pos)) # type: ignore[arg-type] self.context.pop() diff --git a/sphinx/testing/util.py b/sphinx/testing/util.py index 66cfc5172..72b6a13d6 100644 --- a/sphinx/testing/util.py +++ b/sphinx/testing/util.py @@ -111,6 +111,7 @@ class SphinxTestApp(application.Sphinx): docutilsconf: str | None = None, parallel: int = 0, ) -> None: + assert srcdir is not None self.docutils_conf_path = srcdir / 'docutils.conf' if docutilsconf is not None: diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index f36473084..4cfc2b5bc 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -24,7 +24,7 @@ from sphinx.util.nodes import apply_source_workaround, is_smartquotable if TYPE_CHECKING: from sphinx.application import Sphinx - from sphinx.domain.std import StandardDomain + from sphinx.domains.std import StandardDomain from sphinx.environment import BuildEnvironment @@ -163,7 +163,7 @@ class AutoNumbering(SphinxTransform): default_priority = 210 def apply(self, **kwargs: Any) -> None: - domain: StandardDomain = self.env.get_domain('std') + domain: StandardDomain = self.env.domains['std'] for node in self.document.findall(nodes.Element): if (domain.is_enumerable_node(node) and diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index 83fa77aa1..a6585a34c 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -300,7 +300,7 @@ class _NodeUpdater: __('inconsistent term references in translated message.' + ' original: {0}, translated: {1}')) - xref_reftarget_map = {} + xref_reftarget_map: dict[tuple[str, str, str] | None, dict[str, Any]] = {} def get_ref_key(node: addnodes.pending_xref) -> tuple[str, str, str] | None: case = node["refdomain"], node["reftype"] @@ -393,10 +393,10 @@ class Locale(SphinxTransform): for _id in node['ids']: parts = split_term_classifiers(msgstr) patch = publish_msgstr( - self.app, parts[0], source, node.line, self.config, settings, + self.app, parts[0] or '', source, node.line, self.config, settings, ) updater.patch = make_glossary_term( - self.env, patch, parts[1], source, node.line, _id, self.document, + self.env, patch, parts[1] or '', source, node.line, _id, self.document, ) processed = True @@ -497,7 +497,7 @@ class Locale(SphinxTransform): if 'index' in self.config.gettext_additional_targets: # Extract and translate messages for index entries. for node, entries in traverse_translatable_index(self.document): - new_entries: list[tuple[str, str, str, str, str]] = [] + new_entries: list[tuple[str, str, str, str, str | None]] = [] for type, msg, tid, main, _key in entries: msg_parts = split_index_msg(type, msg) msgstr_parts = [] diff --git a/sphinx/transforms/post_transforms/images.py b/sphinx/transforms/post_transforms/images.py index 07b931d63..1eb713e85 100644 --- a/sphinx/transforms/post_transforms/images.py +++ b/sphinx/transforms/post_transforms/images.py @@ -117,6 +117,7 @@ class DataURIExtractor(BaseImageConverter): def handle(self, node: nodes.image) -> None: image = parse_data_uri(node['uri']) + assert image is not None ext = get_image_extension(image.mimetype) if ext is None: logger.warning(__('Unknown image format: %s...'), node['uri'][:32], @@ -140,7 +141,7 @@ class DataURIExtractor(BaseImageConverter): def get_filename_for(filename: str, mimetype: str) -> str: basename = os.path.basename(filename) basename = re.sub(CRITICAL_PATH_CHAR_RE, "_", basename) - return os.path.splitext(basename)[0] + get_image_extension(mimetype) + return os.path.splitext(basename)[0] + (get_image_extension(mimetype) or '') class ImageConverter(BaseImageConverter): @@ -202,8 +203,12 @@ class ImageConverter(BaseImageConverter): if not self.available: return False else: - rule = self.get_conversion_rule(node) - return bool(rule) + try: + self.get_conversion_rule(node) + except ValueError: + return False + else: + return True def get_conversion_rule(self, node: nodes.image) -> tuple[str, str]: for candidate in self.guess_mimetypes(node): @@ -212,7 +217,7 @@ class ImageConverter(BaseImageConverter): if rule in self.conversion_rules: return rule - return None + raise ValueError('No conversion rule found') def is_available(self) -> bool: """Return the image converter is available or not.""" @@ -222,7 +227,8 @@ class ImageConverter(BaseImageConverter): if '?' in node['candidates']: return [] elif '*' in node['candidates']: - return [guess_mimetype(node['uri'])] + guessed = guess_mimetype(node['uri']) + return [guessed] if guessed is not None else [] else: return node['candidates'].keys() diff --git a/sphinx/util/cfamily.py b/sphinx/util/cfamily.py index debe23df4..c6b8b8f47 100644 --- a/sphinx/util/cfamily.py +++ b/sphinx/util/cfamily.py @@ -94,7 +94,8 @@ class ASTBaseBase: return False return True - __hash__: Callable[[], int] = None + # Defining __hash__ = None is not strictly needed when __eq__ is defined. + __hash__ = None # type: ignore[assignment] def clone(self) -> Any: return deepcopy(self) @@ -346,8 +347,7 @@ class BaseParser: def matched_text(self) -> str: if self.last_match is not None: return self.last_match.group() - else: - return None + return '' def read_rest(self) -> str: rv = self.definition[self.pos:] diff --git a/sphinx/util/docfields.py b/sphinx/util/docfields.py index 84905fea4..6d0468227 100644 --- a/sphinx/util/docfields.py +++ b/sphinx/util/docfields.py @@ -5,16 +5,18 @@ be domain-specifically transformed to a more appealing presentation. """ from __future__ import annotations +import contextlib from typing import TYPE_CHECKING, Any, List, Tuple, cast from docutils import nodes -from docutils.nodes import Node +from docutils.nodes import Element, Node from docutils.parsers.rst.states import Inliner from sphinx import addnodes from sphinx.environment import BuildEnvironment from sphinx.locale import __ from sphinx.util import logging +from sphinx.util.nodes import get_node_line from sphinx.util.typing import TextlikeNode if TYPE_CHECKING: @@ -52,8 +54,15 @@ class Field: is_grouped = False is_typed = False - def __init__(self, name: str, names: tuple[str, ...] = (), label: str = None, - has_arg: bool = True, rolename: str = None, bodyrolename: str = None) -> None: + def __init__( + self, + name: str, + names: tuple[str, ...] = (), + label: str = '', + has_arg: bool = True, + rolename: str = '', + bodyrolename: str = '', + ) -> None: self.name = name self.names = names self.label = label @@ -63,8 +72,8 @@ class Field: def make_xref(self, rolename: str, domain: str, target: str, innernode: type[TextlikeNode] = addnodes.literal_emphasis, - contnode: Node = None, env: BuildEnvironment = None, - inliner: Inliner = None, location: Node = None) -> Node: + contnode: Node | None = None, env: BuildEnvironment | None = None, + inliner: Inliner | None = None, location: Element | None = None) -> Node: # note: for backwards compatibility env is last, but not optional assert env is not None assert (inliner is None) == (location is None), (inliner, location) @@ -83,23 +92,33 @@ class Field: refnode += contnode or innernode(target, target) env.get_domain(domain).process_field_xref(refnode) return refnode - lineno = logging.get_source_line(location)[1] + lineno = -1 + if location is not None: + with contextlib.suppress(ValueError): + lineno = get_node_line(location) ns, messages = role(rolename, target, target, lineno, inliner, {}, []) return nodes.inline(target, '', *ns) def make_xrefs(self, rolename: str, domain: str, target: str, innernode: type[TextlikeNode] = addnodes.literal_emphasis, - contnode: Node = None, env: BuildEnvironment = None, - inliner: Inliner = None, location: Node = None) -> list[Node]: + contnode: Node | None = None, env: BuildEnvironment | None = None, + inliner: Inliner | None = None, location: Element | None = None, + ) -> list[Node]: return [self.make_xref(rolename, domain, target, innernode, contnode, env, inliner, location)] def make_entry(self, fieldarg: str, content: list[Node]) -> tuple[str, list[Node]]: return (fieldarg, content) - def make_field(self, types: dict[str, list[Node]], domain: str, - item: tuple, env: BuildEnvironment = None, - inliner: Inliner = None, location: Node = None) -> nodes.field: + def make_field( + self, + types: dict[str, list[Node]], + domain: str, + item: tuple, + env: BuildEnvironment | None = None, + inliner: Inliner | None = None, + location: Element | None = None, + ) -> nodes.field: fieldarg, content = item fieldname = nodes.field_name('', self.label) if fieldarg: @@ -135,14 +154,20 @@ class GroupedField(Field): is_grouped = True list_type = nodes.bullet_list - def __init__(self, name: str, names: tuple[str, ...] = (), label: str = None, - rolename: str = None, can_collapse: bool = False) -> None: + def __init__(self, name: str, names: tuple[str, ...] = (), label: str = '', + rolename: str = '', can_collapse: bool = False) -> None: super().__init__(name, names, label, True, rolename) self.can_collapse = can_collapse - def make_field(self, types: dict[str, list[Node]], domain: str, - items: tuple, env: BuildEnvironment = None, - inliner: Inliner = None, location: Node = None) -> nodes.field: + def make_field( + self, + types: dict[str, list[Node]], + domain: str, + items: tuple, + env: BuildEnvironment | None = None, + inliner: Inliner | None = None, + location: Element | None = None, + ) -> nodes.field: fieldname = nodes.field_name('', self.label) listnode = self.list_type() for fieldarg, content in items: @@ -184,16 +209,29 @@ class TypedField(GroupedField): """ is_typed = True - def __init__(self, name: str, names: tuple[str, ...] = (), typenames: tuple[str, ...] = (), - label: str = None, rolename: str = None, typerolename: str = None, - can_collapse: bool = False) -> None: + def __init__( + self, + name: str, + names: tuple[str, ...] = (), + typenames: tuple[str, ...] = (), + label: str = '', + rolename: str = '', + typerolename: str = '', + can_collapse: bool = False, + ) -> None: super().__init__(name, names, label, rolename, can_collapse) self.typenames = typenames self.typerolename = typerolename - def make_field(self, types: dict[str, list[Node]], domain: str, - items: tuple, env: BuildEnvironment = None, - inliner: Inliner = None, location: Node = None) -> nodes.field: + def make_field( + self, + types: dict[str, list[Node]], + domain: str, + items: tuple, + env: BuildEnvironment | None = None, + inliner: Inliner | None = None, + location: Element | None = None, + ) -> nodes.field: def handle_item(fieldarg: str, content: str) -> nodes.paragraph: par = nodes.paragraph() par.extend(self.make_xrefs(self.rolename, domain, fieldarg, @@ -251,7 +289,7 @@ class DocFieldTransformer: """Transform a single field list *node*.""" typemap = self.typemap - entries: list[nodes.field | tuple[Field, Any, Node]] = [] + entries: list[nodes.field | tuple[Field, Any, Element]] = [] groupindices: dict[str, int] = {} types: dict[str, dict] = {} @@ -292,7 +330,7 @@ class DocFieldTransformer: target = content[0].astext() xrefs = typed_field.make_xrefs( typed_field.typerolename, - self.directive.domain, + self.directive.domain or '', target, contnode=content[0], env=self.directive.state.document.settings.env, @@ -362,7 +400,8 @@ class DocFieldTransformer: fieldtypes = types.get(fieldtype.name, {}) env = self.directive.state.document.settings.env inliner = self.directive.state.inliner - new_list += fieldtype.make_field(fieldtypes, self.directive.domain, items, + domain = self.directive.domain or '' + new_list += fieldtype.make_field(fieldtypes, domain, items, env=env, inliner=inliner, location=location) node.replace_self(new_list) diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index cbf26fcd5..5485caeff 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -143,7 +143,7 @@ def patched_get_language() -> Generator[None, None, None]: """ from docutils.languages import get_language - def patched_get_language(language_code: str, reporter: Reporter = None) -> Any: + def patched_get_language(language_code: str, reporter: Reporter | None = None) -> Any: return get_language(language_code) try: @@ -167,7 +167,7 @@ def patched_rst_get_language() -> Generator[None, None, None]: """ from docutils.parsers.rst.languages import get_language - def patched_get_language(language_code: str, reporter: Reporter = None) -> Any: + def patched_get_language(language_code: str, reporter: Reporter | None = None) -> Any: return get_language(language_code) try: @@ -378,7 +378,7 @@ def switch_source_input(state: State, content: StringList) -> Generator[None, No get_source_and_line = state.memo.reporter.get_source_and_line # type: ignore # replace it by new one - state_machine = StateMachine([], None) + state_machine = StateMachine([], None) # type: ignore[arg-type] state_machine.input_lines = content state.memo.reporter.get_source_and_line = state_machine.get_source_and_line # type: ignore # noqa: E501 @@ -492,12 +492,12 @@ class SphinxRole: """Reference to the :class:`.Config` object.""" return self.env.config - def get_source_info(self, lineno: int = None) -> tuple[str, int]: + def get_source_info(self, lineno: int | None = None) -> tuple[str, int]: if lineno is None: lineno = self.lineno return self.inliner.reporter.get_source_and_line(lineno) # type: ignore - def set_source_info(self, node: Node, lineno: int = None) -> None: + def set_source_info(self, node: Node, lineno: int | None = None) -> None: node.source, node.line = self.get_source_info(lineno) def get_location(self) -> str: diff --git a/sphinx/util/images.py b/sphinx/util/images.py index 7297e1699..f9bac6e56 100644 --- a/sphinx/util/images.py +++ b/sphinx/util/images.py @@ -4,7 +4,7 @@ from __future__ import annotations import base64 from os import path -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING, NamedTuple, overload import imagesize @@ -51,6 +51,16 @@ def get_image_size(filename: str) -> tuple[int, int] | None: return None +@overload +def guess_mimetype(filename: PathLike[str] | str, default: str) -> str: + ... + + +@overload +def guess_mimetype(filename: PathLike[str] | str, default: None = None) -> str | None: + ... + + def guess_mimetype( filename: PathLike[str] | str = '', default: str | None = None, diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 3dfdd4f26..35c654a6c 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -19,7 +19,6 @@ from sphinx.util import logging if TYPE_CHECKING: from sphinx.builders import Builder - from sphinx.domain import IndexEntry from sphinx.environment import BuildEnvironment from sphinx.util.tags import Tags @@ -300,7 +299,7 @@ def get_prev_node(node: Node) -> Node | None: def traverse_translatable_index( doctree: Element, -) -> Iterable[tuple[Element, list[IndexEntry]]]: +) -> Iterable[tuple[Element, list[tuple[str, str, str, str, str | None]]]]: """Traverse translatable index node from a document tree.""" matcher = NodeMatcher(addnodes.index, inline=False) for node in doctree.findall(matcher): # type: addnodes.index diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py index 8e3d9df24..7efb77abe 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -38,6 +38,9 @@ class Cell: def __hash__(self) -> int: return hash((self.col, self.row)) + def __bool__(self) -> bool: + return self.text != '' and self.col is not None and self.row is not None + def wrap(self, width: int) -> None: self.wrapped = my_wrap(self.text, width) @@ -88,7 +91,7 @@ class Table: +--------+--------+ """ - def __init__(self, colwidth: list[int] = None) -> None: + def __init__(self, colwidth: list[int] | None = None) -> None: self.lines: list[list[Cell]] = [] self.separator = 0 self.colwidth: list[int] = (colwidth if colwidth is not None else []) @@ -140,7 +143,7 @@ class Table: def _ensure_has_column(self, col: int) -> None: for line in self.lines: while len(line) < col: - line.append(None) + line.append(Cell()) def __repr__(self) -> str: return "\n".join(repr(line) for line in self.lines) @@ -150,6 +153,8 @@ class Table: ``self.colwidth`` or ``self.measured_widths``). This takes into account cells spanning multiple columns. """ + if cell.row is None or cell.col is None: + raise ValueError('Cell co-ordinates have not been set') width = 0 for i in range(self[cell.row, cell.col].colspan): width += source[cell.col + i] @@ -173,6 +178,8 @@ class Table: cell.wrap(width=self.cell_width(cell, self.colwidth)) if not cell.wrapped: continue + if cell.row is None or cell.col is None: + raise ValueError('Cell co-ordinates have not been set') width = math.ceil(max(column_width(x) for x in cell.wrapped) / cell.colspan) for col in range(cell.col, cell.col + cell.colspan): self.measured_widths[col] = max(self.measured_widths[col], width) @@ -358,7 +365,7 @@ class TextWriter(writers.Writer): settings_spec = ('No options here.', '', ()) settings_defaults: dict[str, Any] = {} - output: str = None + output: str def __init__(self, builder: TextBuilder) -> None: super().__init__() @@ -391,7 +398,7 @@ class TextTranslator(SphinxTranslator): self.list_counter: list[int] = [] self.sectionlevel = 0 self.lineblocklevel = 0 - self.table: Table = None + self.table: Table def add_text(self, text: str) -> None: self.states[-1].append((-1, text)) @@ -400,7 +407,9 @@ class TextTranslator(SphinxTranslator): self.states.append([]) self.stateindent.append(indent) - def end_state(self, wrap: bool = True, end: list[str] = [''], first: str = None) -> None: + def end_state( + self, wrap: bool = True, end: list[str] | None = [''], first: str | None = None, + ) -> None: content = self.states.pop() maxindent = sum(self.stateindent) indent = self.stateindent.pop() @@ -843,17 +852,17 @@ class TextTranslator(SphinxTranslator): self.stateindent.pop() self.entry.text = text self.table.add_cell(self.entry) - self.entry = None + del self.entry def visit_table(self, node: Element) -> None: - if self.table: + if hasattr(self, 'table'): raise NotImplementedError('Nested tables are not supported.') self.new_state(0) self.table = Table() def depart_table(self, node: Element) -> None: self.add_text(str(self.table)) - self.table = None + del self.table self.end_state(wrap=False) def visit_acks(self, node: Element) -> None: diff --git a/sphinx/writers/xml.py b/sphinx/writers/xml.py index 124c5e66a..1b6868e10 100644 --- a/sphinx/writers/xml.py +++ b/sphinx/writers/xml.py @@ -10,6 +10,8 @@ from sphinx.builders import Builder class XMLWriter(BaseXMLWriter): + output: str + def __init__(self, builder: Builder) -> None: super().__init__() self.builder = builder @@ -34,7 +36,7 @@ class PseudoXMLWriter(BaseXMLWriter): config_section = 'pseudoxml writer' config_section_dependencies = ('writers',) - output = None + output: str """Final translated form of `document`.""" def __init__(self, builder: Builder) -> None: diff --git a/tests/test_ext_doctest.py b/tests/test_ext_doctest.py index 3b82bca05..9602f483d 100644 --- a/tests/test_ext_doctest.py +++ b/tests/test_ext_doctest.py @@ -125,13 +125,12 @@ def test_reporting_with_autodoc(app, status, warning, capfd): written = [] app.builder._warn_out = written.append app.builder.build_all() - lines = '\n'.join(written).replace(os.sep, '/').split('\n') - failures = [l for l in lines if l.startswith('File')] - expected = [ - 'File "dir/inner.rst", line 1, in default', - 'File "dir/bar.py", line ?, in default', - 'File "foo.py", line ?, in default', - 'File "index.rst", line 4, in default', - ] - for location in expected: - assert location in failures + + failures = [line.replace(os.sep, '/') + for line in '\n'.join(written).splitlines() + if line.startswith('File')] + + assert 'File "dir/inner.rst", line 1, in default' in failures + assert 'File "dir/bar.py", line ?, in default' in failures + assert 'File "foo.py", line ?, in default' in failures + assert 'File "index.rst", line 4, in default' in failures