Enable `strict-optional` for several more modules (#11523)

This enables mypy's ``strict-optional`` mode for:

* ``sphinx.builders.html``
* ``sphinx.builders.latex``
* ``sphinx.domains.python``
* ``sphinx.domains.std``
* ``sphinx.environment``
* ``sphinx.ext.apidoc``
* ``sphinx.ext.autodoc``
* ``sphinx.ext.autodoc.importer``
* ``sphinx.ext.autosummary``
* ``sphinx.ext.autosummary.generate``
* ``sphinx.ext.inheritance_diagram``
* ``sphinx.ext.intersphinx``
* ``sphinx.ext.imgmath``
* ``sphinx.ext.mathjax``
* ``sphinx.ext.napoleon.docstring``
* ``sphinx.registry``
* ``sphinx.writers.latex``
This commit is contained in:
Adam Turner
2023-07-27 20:17:35 +01:00
committed by GitHub
parent 5233086b8c
commit cb6d568715
22 changed files with 469 additions and 403 deletions

View File

@@ -310,25 +310,8 @@ disallow_any_generics = true
[[tool.mypy.overrides]]
module = [
"sphinx.builders.html",
"sphinx.builders.latex",
"sphinx.domains.c",
"sphinx.domains.cpp",
"sphinx.domains.python",
"sphinx.domains.std",
"sphinx.environment",
"sphinx.ext.apidoc",
"sphinx.ext.autodoc",
"sphinx.ext.autodoc.importer",
"sphinx.ext.autosummary",
"sphinx.ext.autosummary.generate",
"sphinx.ext.inheritance_diagram",
"sphinx.ext.intersphinx",
"sphinx.ext.imgmath",
"sphinx.ext.mathjax",
"sphinx.ext.napoleon.docstring",
"sphinx.registry",
"sphinx.writers.latex",
]
strict_optional = false

View File

@@ -1240,8 +1240,8 @@ class Sphinx:
def add_html_math_renderer(
self,
name: str,
inline_renderers: tuple[Callable | None, Callable | None] | None = None,
block_renderers: tuple[Callable | None, Callable | None] | None = None,
inline_renderers: tuple[Callable, Callable | None] | None = None,
block_renderers: tuple[Callable, Callable | None] | None = None,
) -> None:
"""Register a math renderer for HTML.

View File

@@ -102,11 +102,11 @@ class Stylesheet(str):
its filename (str).
"""
attributes: dict[str, str] = None
filename: str = None
priority: int = None
attributes: dict[str, str]
filename: str
priority: int
def __new__(cls, filename: str, *args: str, priority: int = 500, **attributes: Any,
def __new__(cls, filename: str, *args: str, priority: int = 500, **attributes: str,
) -> Stylesheet:
self = str.__new__(cls, filename)
self.filename = filename
@@ -128,9 +128,9 @@ class JavaScript(str):
its filename (str).
"""
attributes: dict[str, str] = None
filename: str = None
priority: int = None
attributes: dict[str, str]
filename: str
priority: int
def __new__(cls, filename: str, priority: int = 500, **attributes: str) -> JavaScript:
self = str.__new__(cls, filename)
@@ -221,10 +221,10 @@ class StandaloneHTMLBuilder(Builder):
use_index = False
download_support = True # enable download role
imgpath: str = None
imgpath: str = ''
domain_indices: list[DOMAIN_INDEX_TYPE] = []
def __init__(self, app: Sphinx, env: BuildEnvironment | None = None) -> None:
def __init__(self, app: Sphinx, env: BuildEnvironment) -> None:
super().__init__(app, env)
# CSS files
@@ -256,7 +256,7 @@ class StandaloneHTMLBuilder(Builder):
# section numbers for headings in the currently visited document
self.secnumbers: dict[str, tuple[int, ...]] = {}
# currently written docname
self.current_docname: str = None
self.current_docname: str = ''
self.init_templates()
self.init_highlighter()
@@ -290,7 +290,7 @@ class StandaloneHTMLBuilder(Builder):
for jsfile in candidates:
if path.isfile(jsfile):
return jsfile
return None
return ''
def _get_style_filenames(self) -> Iterator[str]:
if isinstance(self.config.html_style, str):
@@ -329,6 +329,7 @@ class StandaloneHTMLBuilder(Builder):
else:
dark_style = None
self.dark_highlighter: PygmentsBridge | None
if dark_style is not None:
self.dark_highlighter = PygmentsBridge('html', dark_style)
self.app.add_css_file('pygments_dark.css',
@@ -365,11 +366,11 @@ class StandaloneHTMLBuilder(Builder):
self.add_js_file('sphinx_highlight.js', priority=200)
for filename, attrs in self.app.registry.js_files:
self.add_js_file(filename, **attrs)
self.add_js_file(filename or '', **attrs)
for filename, attrs in self.get_builder_config('js_files', 'html'):
attrs.setdefault('priority', 800) # User's JSs are loaded after extensions'
self.add_js_file(filename, **attrs)
self.add_js_file(filename or '', **attrs)
if self._get_translations_js():
self.add_js_file('translations.js')
@@ -381,7 +382,7 @@ class StandaloneHTMLBuilder(Builder):
self.script_files.append(JavaScript(filename, **kwargs))
@property
def math_renderer_name(self) -> str:
def math_renderer_name(self) -> str | None:
name = self.get_builder_config('math_renderer', 'html')
if name is not None:
# use given name
@@ -459,7 +460,7 @@ class StandaloneHTMLBuilder(Builder):
doc.append(node)
self._publisher.set_source(doc)
self._publisher.publish()
return self._publisher.writer.parts
return self._publisher.writer.parts # type: ignore[union-attr]
def prepare_writing(self, docnames: set[str]) -> None:
# create the search indexer
@@ -502,6 +503,7 @@ class StandaloneHTMLBuilder(Builder):
# format the "last updated on" string, only once is enough since it
# typically doesn't include the time of day
self.last_updated: str | None
lufmt = self.config.html_last_updated_fmt
if lufmt is not None:
self.last_updated = format_date(lufmt or _('%b %d, %Y'),
@@ -824,9 +826,9 @@ class StandaloneHTMLBuilder(Builder):
for jsfile in self.indexer.get_js_stemmer_rawcodes():
copyfile(jsfile, path.join(self.outdir, '_static', path.basename(jsfile)))
else:
jsfile = self.indexer.get_js_stemmer_rawcode()
if jsfile:
copyfile(jsfile, path.join(self.outdir, '_static', '_stemmer.js'))
if js_stemmer_rawcode := self.indexer.get_js_stemmer_rawcode():
copyfile(js_stemmer_rawcode,
path.join(self.outdir, '_static', '_stemmer.js'))
def copy_theme_static_files(self, context: dict[str, Any]) -> None:
def onerror(filename: str, error: Exception) -> None:
@@ -934,6 +936,7 @@ class StandaloneHTMLBuilder(Builder):
reference.append(node)
def load_indexer(self, docnames: Iterable[str]) -> None:
assert self.indexer is not None
keep = set(self.env.all_docs) - set(docnames)
try:
searchindexfn = path.join(self.outdir, self.searchindex_filename)
@@ -954,7 +957,7 @@ class StandaloneHTMLBuilder(Builder):
def index_page(self, pagename: str, doctree: nodes.document, title: str) -> None:
# only index pages with title
if self.indexer is not None and title:
filename = self.env.doc2path(pagename, base=None)
filename = self.env.doc2path(pagename, base=False)
metadata = self.env.metadata.get(pagename, {})
if 'nosearch' in metadata:
self.indexer.feed(pagename, filename, '', new_document(''))
@@ -1131,8 +1134,7 @@ class StandaloneHTMLBuilder(Builder):
pass
def handle_finish(self) -> None:
if self.indexer:
self.finish_tasks.add_task(self.dump_search_index)
self.finish_tasks.add_task(self.dump_search_index)
self.finish_tasks.add_task(self.dump_inventory)
@progress_message(__('dumping object inventory'))
@@ -1140,6 +1142,9 @@ class StandaloneHTMLBuilder(Builder):
InventoryFile.dump(path.join(self.outdir, INVENTORY_FILENAME), self.env, self)
def dump_search_index(self) -> None:
if self.indexer is None:
return
with progress_message(__('dumping search index in %s') % self.indexer.label()):
self.indexer.prune(self.env.all_docs)
searchindexfn = path.join(self.outdir, self.searchindex_filename)
@@ -1194,7 +1199,7 @@ def setup_css_tag_helper(app: Sphinx, pagename: str, templatename: str,
.. note:: This set up function is added to keep compatibility with webhelper.
"""
pathto = context.get('pathto')
pathto = context['pathto']
def css_tag(css: Stylesheet) -> str:
attrs = []
@@ -1217,7 +1222,7 @@ def setup_js_tag_helper(app: Sphinx, pagename: str, templatename: str,
.. note:: This set up function is added to keep compatibility with webhelper.
"""
pathto = context.get('pathto')
pathto = context['pathto']
def js_tag(js: JavaScript) -> str:
attrs = []
@@ -1267,7 +1272,7 @@ def _file_checksum(outdir: str, filename: str) -> str:
def setup_resource_paths(app: Sphinx, pagename: str, templatename: str,
context: dict, doctree: Node) -> None:
"""Set up relative resource paths."""
pathto = context.get('pathto')
pathto = context['pathto']
# favicon_url
favicon_url = context.get('favicon_url')
@@ -1387,7 +1392,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.add_config_value('html_secnumber_suffix', '. ', 'html')
app.add_config_value('html_search_language', None, 'html', [str])
app.add_config_value('html_search_options', {}, 'html')
app.add_config_value('html_search_scorer', '', None)
app.add_config_value('html_search_scorer', '', '')
app.add_config_value('html_scaled_image_link', True, 'html')
app.add_config_value('html_baseurl', '', 'html')
app.add_config_value('html_codeblock_linenos_style', 'inline', 'html', # RemovedInSphinx70Warning # noqa: E501

View File

@@ -119,7 +119,7 @@ class LaTeXBuilder(Builder):
default_translator_class = LaTeXTranslator
def init(self) -> None:
self.babel: ExtBabel = None
self.babel: ExtBabel
self.context: dict[str, Any] = {}
self.docnames: Iterable[str] = {}
self.document_data: list[tuple[str, str, str, str, str, bool]] = []
@@ -317,7 +317,7 @@ class LaTeXBuilder(Builder):
def get_contentsname(self, indexfile: str) -> str:
tree = self.env.get_doctree(indexfile)
contentsname = None
contentsname = ''
for toctree in tree.findall(addnodes.toctree):
if 'caption' in toctree:
contentsname = toctree['caption']

View File

@@ -60,7 +60,7 @@ class ENUM:
Example:
app.add_config_value('latex_show_urls', 'no', None, ENUM('no', 'footnote', 'inline'))
"""
def __init__(self, *candidates: str | bool) -> None:
def __init__(self, *candidates: str | bool | None) -> None:
self.candidates = candidates
def match(self, value: str | list | tuple) -> bool:

View File

@@ -108,7 +108,7 @@ def parse_reftarget(reftarget: str, suppress_prefix: bool = False,
return reftype, reftarget, title, refspecific
def type_to_xref(target: str, env: BuildEnvironment | None = None,
def type_to_xref(target: str, env: BuildEnvironment, *,
suppress_prefix: bool = False) -> addnodes.pending_xref:
"""Convert a type string to a cross reference node."""
if env:
@@ -134,7 +134,7 @@ def type_to_xref(target: str, env: BuildEnvironment | None = None,
refspecific=refspecific, **kwargs)
def _parse_annotation(annotation: str, env: BuildEnvironment | None) -> list[Node]:
def _parse_annotation(annotation: str, env: BuildEnvironment) -> list[Node]:
"""Parse type annotation."""
short_literals = env.config.python_display_short_literal_types
@@ -269,25 +269,25 @@ class _TypeParameterListParser(TokenProcessor):
def fetch_type_param_spec(self) -> list[Token]:
tokens = []
while self.fetch_token():
tokens.append(self.current)
while current := self.fetch_token():
tokens.append(current)
for ldelim, rdelim in ('(', ')'), ('{', '}'), ('[', ']'):
if self.current == [token.OP, ldelim]:
if current == [token.OP, ldelim]:
tokens += self.fetch_until([token.OP, rdelim])
break
else:
if self.current == token.INDENT:
if current == token.INDENT:
tokens += self.fetch_until(token.DEDENT)
elif self.current.match(
elif current.match(
[token.OP, ':'], [token.OP, '='], [token.OP, ',']):
tokens.pop()
break
return tokens
def parse(self) -> None:
while self.fetch_token():
if self.current == token.NAME:
tp_name = self.current.value.strip()
while current := self.fetch_token():
if current == token.NAME:
tp_name = current.value.strip()
if self.previous and self.previous.match([token.OP, '*'], [token.OP, '**']):
if self.previous == [token.OP, '*']:
tp_kind = Parameter.VAR_POSITIONAL
@@ -299,13 +299,13 @@ class _TypeParameterListParser(TokenProcessor):
tp_ann: Any = Parameter.empty
tp_default: Any = Parameter.empty
self.fetch_token()
if self.current and self.current.match([token.OP, ':'], [token.OP, '=']):
if self.current == [token.OP, ':']:
current = self.fetch_token()
if current and current.match([token.OP, ':'], [token.OP, '=']):
if current == [token.OP, ':']:
tokens = self.fetch_type_param_spec()
tp_ann = self._build_identifier(tokens)
if self.current == [token.OP, '=']:
if self.current and self.current == [token.OP, '=']:
tokens = self.fetch_type_param_spec()
tp_default = self._build_identifier(tokens)
@@ -395,7 +395,7 @@ class _TypeParameterListParser(TokenProcessor):
def _parse_type_list(
tp_list: str, env: BuildEnvironment | None = None,
tp_list: str, env: BuildEnvironment,
multi_line_parameter_list: bool = False,
) -> addnodes.desc_type_parameter_list:
"""Parse a list of type parameters according to PEP 695."""
@@ -456,7 +456,7 @@ def _parse_type_list(
def _parse_arglist(
arglist: str, env: BuildEnvironment | None = None, multi_line_parameter_list: bool = False,
arglist: str, env: BuildEnvironment, multi_line_parameter_list: bool = False,
) -> addnodes.desc_parameterlist:
"""Parse a list of arguments using AST parser"""
params = addnodes.desc_parameterlist(arglist)
@@ -580,6 +580,7 @@ class PyXrefMixin:
innernode, contnode,
env, inliner=None, location=None)
if isinstance(result, pending_xref):
assert env is not None
result['refspecific'] = True
result['py:module'] = env.ref_context.get('py:module')
result['py:class'] = env.ref_context.get('py:class')
@@ -968,9 +969,9 @@ class PyFunction(PyObject):
text = f'built-in function; {name}()'
self.indexnode['entries'].append(('pair', text, node_id, '', None))
def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str | None:
def get_index_text(self, modname: str, name_cls: tuple[str, str]) -> str:
# add index in own add_target_and_index() instead.
return None
return ''
class PyDecoratorFunction(PyFunction):
@@ -1556,7 +1557,7 @@ class PythonDomain(Domain):
newname = None
if searchmode == 1:
if type is None:
objtypes = list(self.object_types)
objtypes: list[str] | None = list(self.object_types)
else:
objtypes = self.objtypes_for_role(type)
if objtypes is not None:
@@ -1671,9 +1672,9 @@ class PythonDomain(Domain):
# if not found, use contnode
children = [contnode]
results.append(('py:' + self.role_for_objtype(obj[2]),
make_refnode(builder, fromdocname, obj[0], obj[1],
children, name)))
role = 'py:' + self.role_for_objtype(obj[2]) # type: ignore[operator]
results.append((role, make_refnode(builder, fromdocname, obj[0], obj[1],
children, name)))
return results
def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str,

View File

@@ -216,7 +216,7 @@ class Cmdoption(ObjectDescription[str]):
self.state.document.note_explicit_target(signode)
domain = cast(StandardDomain, self.env.get_domain('std'))
domain = self.env.domains['std']
for optname in signode.get('allnames', []):
domain.add_program_option(currprogram, optname,
self.env.docname, signode['ids'][0])
@@ -383,10 +383,12 @@ class Glossary(SphinxDirective):
parts = split_term_classifiers(line)
# parse the term with inline markup
# classifiers (parts[1:]) will not be shown on doctree
textnodes, sysmsg = self.state.inline_text(parts[0], lineno)
textnodes, sysmsg = self.state.inline_text(parts[0], # type: ignore[arg-type]
lineno)
# use first classifier as a index key
term = make_glossary_term(self.env, textnodes, parts[1], source, lineno,
term = make_glossary_term(self.env, textnodes,
parts[1], source, lineno, # type: ignore[arg-type]
node_id=None, document=self.state.document)
term.rawsource = line
system_messages.extend(sysmsg)
@@ -647,7 +649,7 @@ class StandardDomain(Domain):
self._terms[term.lower()] = (self.env.docname, labelid)
@property
def progoptions(self) -> dict[tuple[str, str], tuple[str, str]]:
def progoptions(self) -> dict[tuple[str | None, str], tuple[str, str]]:
return self.data.setdefault('progoptions', {}) # (program, name) -> docname, labelid
@property
@@ -706,7 +708,7 @@ class StandardDomain(Domain):
node = document.ids[labelid]
if isinstance(node, nodes.target) and 'refid' in node:
# indirect hyperlink targets
node = document.ids.get(node['refid'])
node = document.ids.get(node['refid']) # type: ignore[assignment]
labelid = node['names'][0]
if (node.tagname == 'footnote' or
'refuri' in node or
@@ -721,11 +723,11 @@ class StandardDomain(Domain):
self.anonlabels[name] = docname, labelid
if node.tagname == 'section':
title = cast(nodes.title, node[0])
sectname: str | None = clean_astext(title)
sectname = clean_astext(title)
elif node.tagname == 'rubric':
sectname = clean_astext(node)
elif self.is_enumerable_node(node):
sectname = self.get_numfig_title(node)
sectname = self.get_numfig_title(node) or ''
if not sectname:
continue
else:
@@ -740,13 +742,14 @@ class StandardDomain(Domain):
else:
toctree = next(node.findall(addnodes.toctree), None)
if toctree and toctree.get('caption'):
sectname = toctree.get('caption')
sectname = toctree['caption']
else:
# anonymous-only labels
continue
self.labels[name] = docname, labelid, sectname
def add_program_option(self, program: str, name: str, docname: str, labelid: str) -> None:
def add_program_option(self, program: str | None, name: str,
docname: str, labelid: str) -> None:
# prefer first command option entry
if (program, name) not in self.progoptions:
self.progoptions[program, name] = (docname, labelid)
@@ -827,6 +830,7 @@ class StandardDomain(Domain):
return None
target_node = env.get_doctree(docname).ids.get(labelid)
assert target_node is not None
figtype = self.get_enumerable_node_type(target_node)
if figtype is None:
return None
@@ -985,9 +989,9 @@ class StandardDomain(Domain):
key = (objtype, ltarget)
if key in self.objects:
docname, labelid = self.objects[key]
results.append(('std:' + self.role_for_objtype(objtype),
make_refnode(builder, fromdocname, docname,
labelid, contnode)))
role = 'std:' + self.role_for_objtype(objtype) # type: ignore[operator]
results.append((role, make_refnode(builder, fromdocname, docname,
labelid, contnode)))
return results
def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]:

View File

@@ -60,6 +60,7 @@ default_settings: dict[str, Any] = {
ENV_VERSION = 58
# config status
CONFIG_UNSET = -1
CONFIG_OK = 1
CONFIG_NEW = 2
CONFIG_CHANGED = 3
@@ -145,25 +146,25 @@ class BuildEnvironment:
# --------- ENVIRONMENT INITIALIZATION -------------------------------------
def __init__(self, app: Sphinx):
self.app: Sphinx = None
self.doctreedir: str = None
self.srcdir: str = None
self.config: Config = None
self.config_status: int = None
self.config_status_extra: str = None
self.events: EventManager = None
self.project: Project = None
self.version: dict[str, str] = None
self.app: Sphinx = app
self.doctreedir: str = app.doctreedir
self.srcdir: str = app.srcdir
self.config: Config = None # type: ignore[assignment]
self.config_status: int = CONFIG_UNSET
self.config_status_extra: str = ''
self.events: EventManager = app.events
self.project: Project = app.project
self.version: dict[str, str] = app.registry.get_envversion(app)
# the method of doctree versioning; see set_versioning_method
self.versioning_condition: bool | Callable = None
self.versioning_compare: bool = None
self.versioning_condition: bool | Callable | None = None
self.versioning_compare: bool | None = None
# all the registered domains, set by the application
self.domains = _DomainsType()
# the docutils settings for building
self.settings = default_settings.copy()
self.settings: dict[str, Any] = default_settings.copy()
self.settings['env'] = self
# All "docnames" here are /-separated and relative and exclude

View File

@@ -57,7 +57,7 @@ def is_initpy(filename: str) -> bool:
)
def module_join(*modnames: str) -> str:
def module_join(*modnames: str | None) -> str:
"""Join module names with dots."""
return '.'.join(filter(None, modnames))
@@ -91,7 +91,7 @@ def write_file(name: str, text: str, opts: Any) -> None:
f.write(text)
def create_module_file(package: str, basename: str, opts: Any,
def create_module_file(package: str | None, basename: str, opts: Any,
user_template_dir: str | None = None) -> None:
"""Build the text of the file and write the file."""
options = copy(OPTIONS)
@@ -105,11 +105,16 @@ def create_module_file(package: str, basename: str, opts: Any,
'qualname': qualname,
'automodule_options': options,
}
text = ReSTRenderer([user_template_dir, template_dir]).render('module.rst_t', context)
if user_template_dir is not None:
template_path = [user_template_dir, template_dir]
else:
template_path = [template_dir]
text = ReSTRenderer(template_path).render('module.rst_t', context)
write_file(qualname, text, opts)
def create_package_file(root: str, master_package: str, subroot: str, py_files: list[str],
def create_package_file(root: str, master_package: str | None, subroot: str,
py_files: list[str],
opts: Any, subs: list[str], is_namespace: bool,
excludes: list[str] = [], user_template_dir: str | None = None,
) -> None:
@@ -141,7 +146,11 @@ def create_package_file(root: str, master_package: str, subroot: str, py_files:
'show_headings': not opts.noheadings,
'maxdepth': opts.maxdepth,
}
text = ReSTRenderer([user_template_dir, template_dir]).render('package.rst_t', context)
if user_template_dir is not None:
template_path = [user_template_dir, template_dir]
else:
template_path = [template_dir]
text = ReSTRenderer(template_path).render('package.rst_t', context)
write_file(pkgname, text, opts)
if submodules and opts.separatemodules:
@@ -166,7 +175,11 @@ def create_modules_toc_file(modules: list[str], opts: Any, name: str = 'modules'
'maxdepth': opts.maxdepth,
'docnames': modules,
}
text = ReSTRenderer([user_template_dir, template_dir]).render('toc.rst_t', context)
if user_template_dir is not None:
template_path = [user_template_dir, template_dir]
else:
template_path = [template_dir]
text = ReSTRenderer(template_path).render('toc.rst_t', context)
write_file(name, text, opts)

View File

@@ -330,21 +330,21 @@ class Documenter:
self.indent = indent
# the module and object path within the module, and the fully
# qualified name (all set after resolve_name succeeds)
self.modname: str = None
self.module: ModuleType = None
self.objpath: list[str] = None
self.fullname: str = None
self.modname: str = ''
self.module: ModuleType | None = None
self.objpath: list[str] = []
self.fullname = ''
# extra signature items (arguments and return annotation,
# also set after resolve_name succeeds)
self.args: str = None
self.retann: str = None
self.args: str | None = None
self.retann = ''
# the object to document (set after import_object succeeds)
self.object: Any = None
self.object_name: str = None
self.object_name = ''
# the parent/owner of the object to document
self.parent: Any = None
# the module analyzer to get at attribute docs, or None
self.analyzer: ModuleAnalyzer = None
self.analyzer: ModuleAnalyzer | None = None
@property
def documenters(self) -> dict[str, type[Documenter]]:
@@ -358,8 +358,8 @@ class Documenter:
else:
self.directive.result.append('', source, *lineno)
def resolve_name(self, modname: str, parents: Any, path: str, base: Any,
) -> tuple[str, list[str]]:
def resolve_name(self, modname: str | None, parents: Any, path: str, base: str,
) -> tuple[str | None, list[str]]:
"""Resolve the module and name of the object to document given by the
arguments and the current module/class.
@@ -380,6 +380,7 @@ class Documenter:
# an autogenerated one
try:
matched = py_ext_sig_re.match(self.name)
assert matched is not None
explicit_modname, path, base, tp_list, args, retann = matched.groups()
except AttributeError:
logger.warning(__('invalid signature for auto%s (%r)') % (self.objtype, self.name),
@@ -395,11 +396,12 @@ class Documenter:
parents = []
with mock(self.config.autodoc_mock_imports):
self.modname, self.objpath = self.resolve_name(modname, parents, path, base)
modname, self.objpath = self.resolve_name(modname, parents, path, base)
if not self.modname:
if not modname:
return False
self.modname = modname
self.args = args
self.retann = retann
self.fullname = ((self.modname or '') +
@@ -449,12 +451,12 @@ class Documenter:
return False
return True
def format_args(self, **kwargs: Any) -> str | None:
def format_args(self, **kwargs: Any) -> str:
"""Format the argument signature of *self.object*.
Should return None if the object does not have a signature.
"""
pass
return ''
def format_name(self) -> str:
"""Format the name of *self.object*.
@@ -467,7 +469,7 @@ class Documenter:
# directives of course)
return '.'.join(self.objpath) or self.modname
def _call_format_args(self, **kwargs: Any) -> str | None:
def _call_format_args(self, **kwargs: Any) -> str:
if kwargs:
try:
return self.format_args(**kwargs)
@@ -591,9 +593,9 @@ class Documenter:
docstring = False
# make a copy of docstring for attributes to avoid cache
# the change of autodoc-process-docstring event.
docstrings = [list(attr_docs[key])]
attribute_docstrings = [list(attr_docs[key])]
for i, line in enumerate(self.process_doc(docstrings)):
for i, line in enumerate(self.process_doc(attribute_docstrings)):
self.add_line(line, sourcename, i)
# add content from docstrings
@@ -804,7 +806,7 @@ class Documenter:
classes.sort(key=lambda cls: cls.priority)
# give explicitly separated module name, so that members
# of inner classes can be documented
full_mname = self.modname + '::' + '.'.join(self.objpath + [mname])
full_mname = f'{self.modname}::' + '.'.join((*self.objpath, mname))
documenter = classes[-1](self.directive, full_mname, self.indent)
memberdocumenters.append((documenter, isattr))
@@ -889,8 +891,8 @@ class Documenter:
# no source file -- e.g. for builtin and C modules
self.analyzer = None
# at least add the module.__file__ as a dependency
if hasattr(self.module, '__file__') and self.module.__file__:
self.directive.record_dependencies.add(self.module.__file__)
if module___file__ := getattr(self.module, '__file__', ''):
self.directive.record_dependencies.add(module___file__)
else:
self.directive.record_dependencies.add(self.analyzer.srcname)
@@ -980,8 +982,8 @@ class ModuleDocumenter(Documenter):
# don't document submodules automatically
return False
def resolve_name(self, modname: str, parents: Any, path: str, base: Any,
) -> tuple[str, list[str]]:
def resolve_name(self, modname: str | None, parents: Any, path: str, base: str,
) -> tuple[str | None, list[str]]:
if modname is not None:
logger.warning(__('"::" in automodule name doesn\'t make sense'),
type='autodoc')
@@ -1078,16 +1080,21 @@ class ModuleDocumenter(Documenter):
def sort_members(self, documenters: list[tuple[Documenter, bool]],
order: str) -> list[tuple[Documenter, bool]]:
if order == 'bysource' and self.__all__:
assert self.__all__ is not None
module_all = self.__all__
module_all_set = set(module_all)
module_all_len = len(module_all)
# Sort alphabetically first (for members not listed on the __all__)
documenters.sort(key=lambda e: e[0].name)
# Sort by __all__
def keyfunc(entry: tuple[Documenter, bool]) -> int:
name = entry[0].name.split('::')[1]
if self.__all__ and name in self.__all__:
return self.__all__.index(name)
if name in module_all_set:
return module_all.index(name)
else:
return len(self.__all__)
return module_all_len
documenters.sort(key=keyfunc)
return documenters
@@ -1100,19 +1107,21 @@ class ModuleLevelDocumenter(Documenter):
Specialized Documenter subclass for objects on module level (functions,
classes, data/constants).
"""
def resolve_name(self, modname: str, parents: Any, path: str, base: Any,
) -> tuple[str, list[str]]:
if modname is None:
if path:
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')
# ... or in the scope of a module directive
if not modname:
modname = self.env.ref_context.get('py:module')
# ... else, it stays None, which means invalid
def resolve_name(self, modname: str | None, parents: Any, path: str, base: str,
) -> tuple[str | None, list[str]]:
if modname is not None:
return modname, parents + [base]
if path:
modname = path.rstrip('.')
return modname, parents + [base]
# if documenting a toplevel object without explicit module,
# it can be contained in another auto directive ...
modname = self.env.temp_data.get('autodoc:module')
# ... or in the scope of a module directive
if not modname:
modname = self.env.ref_context.get('py:module')
# ... else, it stays None, which means invalid
return modname, parents + [base]
@@ -1121,31 +1130,33 @@ class ClassLevelDocumenter(Documenter):
Specialized Documenter subclass for objects on class level (methods,
attributes).
"""
def resolve_name(self, modname: str, parents: Any, path: str, base: Any,
) -> tuple[str, list[str]]:
if modname is None:
if path:
mod_cls = path.rstrip('.')
else:
mod_cls = None
# if documenting a class-level object without path,
# there must be a current class, either from a parent
# auto directive ...
mod_cls = self.env.temp_data.get('autodoc:class')
# ... or from a class directive
if mod_cls is None:
mod_cls = self.env.ref_context.get('py:class')
def resolve_name(self, modname: str | None, parents: Any, path: str, base: str,
) -> tuple[str | None, list[str]]:
if modname is not None:
return modname, parents + [base]
if path:
mod_cls = path.rstrip('.')
else:
# if documenting a class-level object without path,
# there must be a current class, either from a parent
# auto directive ...
mod_cls_ = self.env.temp_data.get('autodoc:class')
# ... or from a class directive
if mod_cls_ is None:
mod_cls_ = self.env.ref_context.get('py:class')
# ... if still None, there's no way to know
if mod_cls is None:
if mod_cls_ is None:
return None, []
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')
if not modname:
modname = self.env.ref_context.get('py:module')
# ... else, it stays None, which means invalid
mod_cls = mod_cls_
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')
if not modname:
modname = self.env.ref_context.get('py:module')
# ... else, it stays None, which means invalid
return modname, parents + [base]
@@ -1154,10 +1165,10 @@ class DocstringSignatureMixin:
Mixin for FunctionDocumenter and MethodDocumenter to provide the
feature of reading the signature from the docstring.
"""
_new_docstrings: list[list[str]] = None
_signatures: list[str] = None
_new_docstrings: list[list[str]] | None = None
_signatures: list[str] = []
def _find_signature(self) -> tuple[str | None, str | None]:
def _find_signature(self) -> tuple[str | None, str | None] | None:
# candidates of the object name
valid_names = [self.objpath[-1]] # type: ignore
if isinstance(self, ClassDocumenter):
@@ -1202,13 +1213,13 @@ class DocstringSignatureMixin:
# subsequent signatures
self._signatures.append(f"({args}) -> {retann}")
if result:
if result is not None:
# finish the loop when signature found
break
return result
def get_doc(self) -> list[list[str]]:
def get_doc(self) -> list[list[str]] | None:
if self._new_docstrings is not None:
return self._new_docstrings
return super().get_doc() # type: ignore[misc]
@@ -1262,7 +1273,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
return (inspect.isfunction(member) or inspect.isbuiltin(member) or
(inspect.isroutine(member) and isinstance(parent, ModuleDocumenter)))
def format_args(self, **kwargs: Any) -> str | None:
def format_args(self, **kwargs: Any) -> str:
if self.config.autodoc_typehints in ('none', 'description'):
kwargs.setdefault('show_annotation', False)
if self.config.autodoc_typehints_format == "short":
@@ -1275,7 +1286,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"),
self.fullname, exc)
return None
return ''
except ValueError:
args = ''
@@ -1319,13 +1330,13 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
if dispatchfunc:
documenter = FunctionDocumenter(self.directive, '')
documenter.object = dispatchfunc
documenter.objpath = [None]
documenter.objpath = ['']
sigs.append(documenter.format_signature())
if overloaded:
if overloaded and self.analyzer is not None:
actual = inspect.signature(self.object,
type_aliases=self.config.autodoc_type_aliases)
__globals__ = safe_getattr(self.object, '__globals__', {})
for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
for overload in self.analyzer.overloads['.'.join(self.objpath)]:
overload = self.merge_default_value(actual, overload)
overload = evaluate_signature(overload, __globals__,
self.config.autodoc_type_aliases)
@@ -1384,12 +1395,12 @@ class DecoratorDocumenter(FunctionDocumenter):
# must be lower than FunctionDocumenter
priority = -1
def format_args(self, **kwargs: Any) -> Any:
def format_args(self, **kwargs: Any) -> str:
args = super().format_args(**kwargs)
if ',' in args:
return args
else:
return None
return ''
# Types which have confusing metaclass signatures it would be best not to show.
@@ -1427,7 +1438,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
priority = 15
_signature_class: Any = None
_signature_method_name: str = None
_signature_method_name: str = ''
def __init__(self, *args: Any) -> None:
super().__init__(*args)
@@ -1549,26 +1560,27 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
# with __init__ in C and no `__text_signature__`.
return None, None, None
def format_args(self, **kwargs: Any) -> str | None:
def format_args(self, **kwargs: Any) -> str:
if self.config.autodoc_typehints in ('none', 'description'):
kwargs.setdefault('show_annotation', False)
if self.config.autodoc_typehints_format == "short":
kwargs.setdefault('unqualified_typehints', True)
try:
self._signature_class, self._signature_method_name, sig = self._get_signature()
self._signature_class, _signature_method_name, sig = self._get_signature()
except TypeError as exc:
# __signature__ attribute contained junk
logger.warning(__("Failed to get a constructor signature for %s: %s"),
self.fullname, exc)
return None
return ''
self._signature_method_name = _signature_method_name or ''
if sig is None:
return None
return ''
return stringify_signature(sig, show_return_annotation=False, **kwargs)
def _find_signature(self) -> tuple[str, str]:
def _find_signature(self) -> tuple[str | None, str | None] | None:
result = super()._find_signature()
if result is not None:
# Strip a return value from signature of constructor in docstring (first entry)
@@ -1731,8 +1743,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
pass
if self.doc_as_attr:
# Don't show the docstring of the class when it is an alias.
comment = self.get_variable_comment()
if comment:
if self.get_variable_comment():
return []
else:
return None
@@ -1882,12 +1893,12 @@ class ExceptionDocumenter(ClassDocumenter):
class DataDocumenterMixinBase:
# define types of instance variables
config: Config = None
env: BuildEnvironment = None
modname: str = None
parent: Any = None
object: Any = None
objpath: list[str] = None
config: Config
env: BuildEnvironment
modname: str
parent: Any
object: Any
objpath: list[str]
def should_suppress_directive_header(self) -> bool:
"""Check directive header should be suppressed."""
@@ -2007,7 +2018,7 @@ class DataDocumenter(GenericAliasMixin,
if super().should_suppress_value_header():
return True
else:
doc = self.get_doc()
doc = self.get_doc() or []
docstring, metadata = separate_metadata('\n'.join(sum(doc, [])))
if 'hide-value' in metadata:
return True
@@ -2116,7 +2127,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
return ret
def format_args(self, **kwargs: Any) -> str | None:
def format_args(self, **kwargs: Any) -> str:
if self.config.autodoc_typehints in ('none', 'description'):
kwargs.setdefault('show_annotation', False)
if self.config.autodoc_typehints_format == "short":
@@ -2142,7 +2153,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
except TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"),
self.fullname, exc)
return None
return ''
except ValueError:
args = ''
@@ -2197,9 +2208,9 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
documenter = MethodDocumenter(self.directive, '')
documenter.parent = self.parent
documenter.object = dispatchmeth
documenter.objpath = [None]
documenter.objpath = ['']
sigs.append(documenter.format_signature())
if overloaded:
if overloaded and self.analyzer is not None:
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name):
actual = inspect.signature(self.object, bound_method=False,
type_aliases=self.config.autodoc_type_aliases)
@@ -2208,7 +2219,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
type_aliases=self.config.autodoc_type_aliases)
__globals__ = safe_getattr(self.object, '__globals__', {})
for overload in self.analyzer.overloads.get('.'.join(self.objpath)):
for overload in self.analyzer.overloads['.'.join(self.objpath)]:
overload = self.merge_default_value(actual, overload)
overload = evaluate_signature(overload, __globals__,
self.config.autodoc_type_aliases)
@@ -2337,8 +2348,10 @@ class SlotsMixin(DataDocumenterMixinBase):
def isslotsattribute(self) -> bool:
"""Check the subject is an attribute in __slots__."""
try:
__slots__ = inspect.getslots(self.parent)
return bool(__slots__) and self.objpath[-1] in __slots__
if parent___slots__ := inspect.getslots(self.parent):
return self.objpath[-1] in parent___slots__
else:
return False
except (ValueError, TypeError):
return False
@@ -2358,9 +2371,9 @@ class SlotsMixin(DataDocumenterMixinBase):
def get_doc(self) -> list[list[str]] | None:
if self.object is SLOTSATTR:
try:
__slots__ = inspect.getslots(self.parent)
if __slots__ and __slots__.get(self.objpath[-1]):
docstring = prepare_docstring(__slots__[self.objpath[-1]])
parent___slots__ = inspect.getslots(self.parent)
if parent___slots__ and parent___slots__.get(self.objpath[-1]):
docstring = prepare_docstring(parent___slots__[self.objpath[-1]])
return [docstring]
else:
return []
@@ -2411,7 +2424,7 @@ class RuntimeInstanceAttributeMixin(DataDocumenterMixinBase):
except (AttributeError, PycodeError):
pass
return None
return False
def import_object(self, raiseerror: bool = False) -> bool:
"""Check the existence of runtime instance attribute after failing to import the
@@ -2497,8 +2510,7 @@ class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
def get_doc(self) -> list[list[str]] | None:
if self.object is UNINITIALIZED_ATTR:
return None
else:
return super().get_doc() # type: ignore
return super().get_doc() # type: ignore
class AttributeDocumenter(GenericAliasMixin, SlotsMixin, # type: ignore
@@ -2698,10 +2710,10 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): #
self.isclassmethod = False
return ret
def format_args(self, **kwargs: Any) -> str | None:
def format_args(self, **kwargs: Any) -> str:
func = self._get_property_getter()
if func is None:
return None
return ''
# update the annotations of the property getter
self.env.app.emit('autodoc-before-process-signature', func, False)

View File

@@ -189,11 +189,11 @@ def get_object_members(
# members in __slots__
try:
__slots__ = getslots(subject)
if __slots__:
subject___slots__ = getslots(subject)
if subject___slots__:
from sphinx.ext.autodoc import SLOTSATTR
for name in __slots__:
for name in subject___slots__:
members[name] = Attribute(name, True, SLOTSATTR)
except (TypeError, ValueError):
pass
@@ -226,7 +226,7 @@ def get_object_members(
return members
def get_class_members(subject: Any, objpath: list[str], attrgetter: Callable,
def get_class_members(subject: Any, objpath: Any, attrgetter: Callable,
inherit_docstrings: bool = True) -> dict[str, ObjectMember]:
"""Get members and attributes of target class."""
from sphinx.ext.autodoc import INSTANCEATTR, ObjectMember
@@ -250,11 +250,11 @@ def get_class_members(subject: Any, objpath: list[str], attrgetter: Callable,
# members in __slots__
try:
__slots__ = getslots(subject)
if __slots__:
subject___slots__ = getslots(subject)
if subject___slots__:
from sphinx.ext.autodoc import SLOTSATTR
for name, docstring in __slots__.items():
for name, docstring in subject___slots__.items():
members[name] = ObjectMember(name, SLOTSATTR, class_=subject,
docstring=docstring)
except (TypeError, ValueError):

View File

@@ -151,7 +151,7 @@ class FakeApplication:
self.extensions: dict[str, Extension] = {}
self.srcdir = None
self.config = Config()
self.project = Project(None, None)
self.project = Project('', {})
self.registry = SphinxComponentRegistry()
@@ -671,7 +671,7 @@ def _import_by_name(name: str, grouped_exception: bool = True) -> tuple[Any, Any
# ... then as MODNAME, MODNAME.OBJ1, MODNAME.OBJ1.OBJ2, ...
last_j = 0
modname = None
modname = ''
for j in reversed(range(1, len(name_parts) + 1)):
last_j = j
modname = '.'.join(name_parts[:j])
@@ -742,6 +742,7 @@ class AutoLink(SphinxRole):
"""
def run(self) -> tuple[list[Node], list[system_message]]:
pyobj_role = self.env.get_domain('py').role('obj')
assert pyobj_role is not None
objects, errors = pyobj_role('obj', self.rawtext, self.text, self.lineno,
self.inliner, self.options, self.content)
if errors:

View File

@@ -50,7 +50,7 @@ from sphinx.util.osutil import ensuredir
from sphinx.util.template import SphinxTemplateLoader
if TYPE_CHECKING:
from collections.abc import Sequence
from collections.abc import Sequence, Set
from gettext import NullTranslations
logger = logging.getLogger(__name__)
@@ -80,7 +80,7 @@ class DummyApplication:
class AutosummaryEntry(NamedTuple):
name: str
path: str
path: str | None
template: str
recursive: bool
@@ -231,104 +231,6 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
qualname: str | None = None) -> str:
doc = get_documenter(app, obj, parent)
def skip_member(obj: Any, name: str, objtype: str) -> bool:
try:
return app.emit_firstresult('autodoc-skip-member', objtype, name,
obj, False, {})
except Exception as exc:
logger.warning(__('autosummary: failed to determine %r to be documented, '
'the following exception was raised:\n%s'),
name, exc, type='autosummary')
return False
def get_class_members(obj: Any) -> dict[str, Any]:
members = sphinx.ext.autodoc.get_class_members(obj, [qualname], safe_getattr)
return {name: member.object for name, member in members.items()}
def get_module_members(obj: Any) -> dict[str, Any]:
members = {}
for name in members_of(obj, app.config):
try:
members[name] = safe_getattr(obj, name)
except AttributeError:
continue
return members
def get_all_members(obj: Any) -> dict[str, Any]:
if doc.objtype == "module":
return get_module_members(obj)
elif doc.objtype == "class":
return get_class_members(obj)
return {}
def get_members(obj: Any, types: set[str], include_public: list[str] = [],
imported: bool = True) -> tuple[list[str], list[str]]:
items: list[str] = []
public: list[str] = []
all_members = get_all_members(obj)
for name, value in all_members.items():
documenter = get_documenter(app, value, obj)
if documenter.objtype in types:
# skip imported members if expected
if imported or getattr(value, '__module__', None) == obj.__name__:
skipped = skip_member(value, name, documenter.objtype)
if skipped is True:
pass
elif skipped is False:
# show the member forcedly
items.append(name)
public.append(name)
else:
items.append(name)
if name in include_public or not name.startswith('_'):
# considers member as public
public.append(name)
return public, items
def get_module_attrs(members: Any) -> tuple[list[str], list[str]]:
"""Find module attributes with docstrings."""
attrs, public = [], []
try:
analyzer = ModuleAnalyzer.for_module(name)
attr_docs = analyzer.find_attr_docs()
for namespace, attr_name in attr_docs:
if namespace == '' and attr_name in members:
attrs.append(attr_name)
if not attr_name.startswith('_'):
public.append(attr_name)
except PycodeError:
pass # give up if ModuleAnalyzer fails to parse code
return public, attrs
def get_modules(
obj: Any,
skip: Sequence[str],
public_members: Sequence[str] | None = None) -> tuple[list[str], list[str]]:
items: list[str] = []
public: list[str] = []
for _, modname, _ispkg in pkgutil.iter_modules(obj.__path__):
if modname in skip:
# module was overwritten in __init__.py, so not accessible
continue
fullname = name + '.' + modname
try:
module = import_module(fullname)
if module and hasattr(module, '__sphinx_mock__'):
continue
except ImportError:
pass
items.append(fullname)
if public_members is not None:
if modname in public_members:
public.append(fullname)
else:
if not modname.startswith('_'):
public.append(fullname)
return public, items
ns: dict[str, Any] = {}
ns.update(context)
@@ -340,13 +242,13 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
imported_members = imported_members or ('__all__' in dir(obj) and respect_module_all)
ns['functions'], ns['all_functions'] = \
get_members(obj, {'function'}, imported=imported_members)
_get_members(doc, app, obj, {'function'}, imported=imported_members)
ns['classes'], ns['all_classes'] = \
get_members(obj, {'class'}, imported=imported_members)
_get_members(doc, app, obj, {'class'}, imported=imported_members)
ns['exceptions'], ns['all_exceptions'] = \
get_members(obj, {'exception'}, imported=imported_members)
_get_members(doc, app, obj, {'exception'}, imported=imported_members)
ns['attributes'], ns['all_attributes'] = \
get_module_attrs(ns['members'])
_get_module_attrs(name, ns['members'])
ispackage = hasattr(obj, '__path__')
if ispackage and recursive:
# Use members that are not modules as skip list, because it would then mean
@@ -366,7 +268,7 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
# Otherwise, use get_modules method normally
if respect_module_all and '__all__' in dir(obj):
imported_modules, all_imported_modules = \
get_members(obj, {'module'}, imported=True)
_get_members(doc, app, obj, {'module'}, imported=True)
skip += all_imported_modules
imported_modules = [name + '.' + modname for modname in imported_modules]
all_imported_modules = \
@@ -376,7 +278,8 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
imported_modules, all_imported_modules = [], []
public_members = None
modules, all_modules = get_modules(obj, skip=skip, public_members=public_members)
modules, all_modules = _get_modules(obj, skip=skip, name=name,
public_members=public_members)
ns['modules'] = imported_modules + modules
ns["all_modules"] = all_imported_modules + all_modules
elif doc.objtype == 'class':
@@ -384,9 +287,9 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
ns['inherited_members'] = \
set(dir(obj)) - set(obj.__dict__.keys())
ns['methods'], ns['all_methods'] = \
get_members(obj, {'method'}, ['__init__'])
_get_members(doc, app, obj, {'method'}, include_public={'__init__'})
ns['attributes'], ns['all_attributes'] = \
get_members(obj, {'attribute', 'property'})
_get_members(doc, app, obj, {'attribute', 'property'})
if modname is None or qualname is None:
modname, qualname = split_full_qualified_name(name)
@@ -413,6 +316,114 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
return template.render(doc.objtype, ns)
def _skip_member(app: Sphinx, obj: Any, name: str, objtype: str) -> bool:
try:
return app.emit_firstresult('autodoc-skip-member', objtype, name,
obj, False, {})
except Exception as exc:
logger.warning(__('autosummary: failed to determine %r to be documented, '
'the following exception was raised:\n%s'),
name, exc, type='autosummary')
return False
def _get_class_members(obj: Any) -> dict[str, Any]:
members = sphinx.ext.autodoc.get_class_members(obj, None, safe_getattr)
return {name: member.object for name, member in members.items()}
def _get_module_members(app: Sphinx, obj: Any) -> dict[str, Any]:
members = {}
for name in members_of(obj, app.config):
try:
members[name] = safe_getattr(obj, name)
except AttributeError:
continue
return members
def _get_all_members(doc: type[Documenter], app: Sphinx, obj: Any) -> dict[str, Any]:
if doc.objtype == 'module':
return _get_module_members(app, obj)
elif doc.objtype == 'class':
return _get_class_members(obj)
return {}
def _get_members(doc: type[Documenter], app: Sphinx, obj: Any, types: set[str], *,
include_public: Set[str] = frozenset(),
imported: bool = True) -> tuple[list[str], list[str]]:
items: list[str] = []
public: list[str] = []
all_members = _get_all_members(doc, app, obj)
for name, value in all_members.items():
documenter = get_documenter(app, value, obj)
if documenter.objtype in types:
# skip imported members if expected
if imported or getattr(value, '__module__', None) == obj.__name__:
skipped = _skip_member(app, value, name, documenter.objtype)
if skipped is True:
pass
elif skipped is False:
# show the member forcedly
items.append(name)
public.append(name)
else:
items.append(name)
if name in include_public or not name.startswith('_'):
# considers member as public
public.append(name)
return public, items
def _get_module_attrs(name: str, members: Any) -> tuple[list[str], list[str]]:
"""Find module attributes with docstrings."""
attrs, public = [], []
try:
analyzer = ModuleAnalyzer.for_module(name)
attr_docs = analyzer.find_attr_docs()
for namespace, attr_name in attr_docs:
if namespace == '' and attr_name in members:
attrs.append(attr_name)
if not attr_name.startswith('_'):
public.append(attr_name)
except PycodeError:
pass # give up if ModuleAnalyzer fails to parse code
return public, attrs
def _get_modules(
obj: Any,
*,
skip: Sequence[str],
name: str,
public_members: Sequence[str] | None = None) -> tuple[list[str], list[str]]:
items: list[str] = []
public: list[str] = []
for _, modname, _ispkg in pkgutil.iter_modules(obj.__path__):
if modname in skip:
# module was overwritten in __init__.py, so not accessible
continue
fullname = name + '.' + modname
try:
module = import_module(fullname)
if module and hasattr(module, '__sphinx_mock__'):
continue
except ImportError:
pass
items.append(fullname)
if public_members is not None:
if modname in public_members:
public.append(fullname)
else:
if not modname.startswith('_'):
public.append(fullname)
return public, items
def generate_autosummary_docs(sources: list[str], output_dir: str | None = None,
suffix: str = '.rst', base_path: str | None = None,
imported_members: bool = False, app: Any = None,
@@ -567,7 +578,7 @@ def find_autosummary_in_lines(
recursive = False
toctree: str | None = None
template = None
template = ''
current_module = module
in_autosummary = False
base_indent = ""
@@ -617,7 +628,7 @@ def find_autosummary_in_lines(
base_indent = m.group(1)
recursive = False
toctree = None
template = None
template = ''
continue
m = automodule_re.search(line)

View File

@@ -163,7 +163,7 @@ class InheritanceGraph:
return classes
def _class_info(self, classes: list[Any], show_builtins: bool, private_bases: bool,
parts: int, aliases: dict[str, str], top_classes: list[Any],
parts: int, aliases: dict[str, str] | None, top_classes: list[Any],
) -> list[tuple[str, str, list[str], str]]:
"""Return name and bases for all classes that are ancestors of
*classes*.
@@ -219,7 +219,7 @@ class InheritanceGraph:
for cls in classes:
recurse(cls)
return list(all_classes.values())
return list(all_classes.values()) # type: ignore[arg-type]
def class_name(
self, cls: Any, parts: int = 0, aliases: dict[str, str] | None = None,
@@ -361,7 +361,7 @@ class InheritanceDiagram(SphinxDirective):
# Create a graph starting with the list of classes
try:
graph = InheritanceGraph(
class_names, self.env.ref_context.get('py:module'),
class_names, self.env.ref_context.get('py:module'), # type: ignore[arg-type]
parts=node['parts'],
private_bases='private-bases' in self.options,
aliases=self.config.inheritance_alias,

View File

@@ -119,7 +119,7 @@ def _strip_basic_auth(url: str) -> str:
return urlunsplit(frags)
def _read_from_url(url: str, config: Config | None = None) -> IO:
def _read_from_url(url: str, *, config: Config) -> IO:
"""Reads data from *url* with an HTTP *GET*.
This function supports fetching from resources which use basic HTTP auth as
@@ -430,7 +430,7 @@ def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: I
and (domain_name + ":*") in env.config.intersphinx_disabled_reftypes:
return None
domain = env.get_domain(domain_name)
objtypes = domain.objtypes_for_role(typ)
objtypes = domain.objtypes_for_role(typ) or ()
if not objtypes:
return None
return _resolve_reference_in_domain(env, inv_name, inventory,
@@ -555,15 +555,20 @@ class IntersphinxRole(SphinxRole):
def get_inventory_and_name_suffix(self, name: str) -> tuple[str | None, str]:
assert name.startswith('external'), name
assert name[8] in ':+', name
# either we have an explicit inventory name, i.e,
# :external+inv:role: or
# :external+inv:domain:role:
# or we look in all inventories, i.e.,
# :external:role: or
# :external:domain:role:
inv, suffix = IntersphinxRole._re_inv_ref.fullmatch(name, 8).group(2, 3)
return inv, suffix
suffix = name[9:]
if name[8] == '+':
inv_name, suffix = suffix.split(':', 1)
return inv_name, suffix
elif name[8] == ':':
return None, suffix
else:
raise ValueError(f'Malformed :external: role name: {name}')
def get_role_name(self, name: str) -> tuple[str, str] | None:
names = name.split(':')
@@ -597,6 +602,7 @@ class IntersphinxRole(SphinxRole):
domain = self.env.get_domain(role[0])
if domain:
role_func = domain.role(role[1])
assert role_func is not None
return role_func(':'.join(role), self.rawtext, self.text, self.lineno,
self.inliner, self.options, self.content)
@@ -704,8 +710,8 @@ def inspect_main(argv: list[str]) -> None:
class MockConfig:
intersphinx_timeout: int | None = None
tls_verify = False
tls_cacerts = None
user_agent = None
tls_cacerts: str | dict[str, str] | None = None
user_agent: str = ''
class MockApp:
srcdir = ''

View File

@@ -156,12 +156,15 @@ class GoogleDocstring:
obj: Any = None,
options: Any = None,
) -> None:
self._config = config
self._app = app
if not self._config:
if config:
self._config = config
elif app:
self._config = app.config
else:
from sphinx.ext.napoleon import Config
self._config = self._app.config if self._app else Config() # type: ignore
self._config = Config() # type: ignore
if not what:
if inspect.isclass(obj):
@@ -1048,7 +1051,8 @@ def _convert_numpy_type_spec(
"reference": lambda x: x,
}
converted = "".join(converters.get(type_)(token) for token, type_ in types)
converted = "".join(converters.get(type_)(token) # type: ignore[misc]
for token, type_ in types)
return converted
@@ -1273,7 +1277,7 @@ class NumpyDocstring(GoogleDocstring):
return g[2], g[1]
raise ValueError("%s is not a item name" % text)
def push_item(name: str, rest: list[str]) -> None:
def push_item(name: str | None, rest: list[str]) -> None:
if not name:
return
name, role = parse_item_name(name)

View File

@@ -145,21 +145,21 @@ class TokenProcessor:
return self.current
def fetch_until(self, condition: Any) -> list[Token | None]:
def fetch_until(self, condition: Any) -> list[Token]:
"""Fetch tokens until specified token appeared.
.. note:: This also handles parenthesis well.
"""
tokens = []
while self.fetch_token():
tokens.append(self.current)
if self.current == condition:
while current := self.fetch_token():
tokens.append(current)
if current == condition:
break
if self.current == [OP, '(']:
if current == [OP, '(']:
tokens += self.fetch_until([OP, ')'])
elif self.current == [OP, '{']:
elif current == [OP, '{']:
tokens += self.fetch_until([OP, '}'])
elif self.current == [OP, '[']:
elif current == [OP, '[']:
tokens += self.fetch_until([OP, ']'])
return tokens
@@ -176,22 +176,22 @@ class AfterCommentParser(TokenProcessor):
super().__init__(lines)
self.comment: str | None = None
def fetch_rvalue(self) -> list[Token | None]:
def fetch_rvalue(self) -> list[Token]:
"""Fetch right-hand value of assignment."""
tokens = []
while self.fetch_token():
tokens.append(self.current)
if self.current == [OP, '(']:
while current := self.fetch_token():
tokens.append(current)
if current == [OP, '(']:
tokens += self.fetch_until([OP, ')'])
elif self.current == [OP, '{']:
elif current == [OP, '{']:
tokens += self.fetch_until([OP, '}'])
elif self.current == [OP, '[']:
elif current == [OP, '[']:
tokens += self.fetch_until([OP, ']'])
elif self.current == INDENT:
elif current == INDENT:
tokens += self.fetch_until(DEDENT)
elif self.current == [OP, ';']: # NoQA: SIM114
elif current == [OP, ';']: # NoQA: SIM114
break
elif self.current and self.current.kind not in {OP, NAME, NUMBER, STRING}:
elif current and current.kind not in {OP, NAME, NUMBER, STRING}:
break
return tokens
@@ -200,14 +200,17 @@ class AfterCommentParser(TokenProcessor):
"""Parse the code and obtain comment after assignment."""
# skip lvalue (or whole of AnnAssign)
while (tok := self.fetch_token()) and not tok.match([OP, '='], NEWLINE, COMMENT):
assert self.current
assert tok
assert tok is not None
# skip rvalue (if exists)
if self.current == [OP, '=']:
if tok == [OP, '=']:
self.fetch_rvalue()
tok = self.current
assert tok is not None
if self.current == COMMENT:
self.comment = self.current.value # type: ignore[union-attr]
if tok == COMMENT:
self.comment = tok.value
class VariableCommentPicker(ast.NodeVisitor):

View File

@@ -85,12 +85,14 @@ class SphinxComponentRegistry:
#: additional enumerable nodes
#: a dict of node class -> tuple of figtype and title_getter function
self.enumerable_nodes: dict[type[Node], tuple[str, TitleGetter]] = {}
self.enumerable_nodes: dict[type[Node], tuple[str, TitleGetter | None]] = {}
#: HTML inline and block math renderers
#: a dict of name -> tuple of visit function and depart function
self.html_inline_math_renderers: dict[str, tuple[Callable, Callable]] = {}
self.html_block_math_renderers: dict[str, tuple[Callable, Callable]] = {}
self.html_inline_math_renderers: dict[str,
tuple[Callable, Callable | None]] = {}
self.html_block_math_renderers: dict[str,
tuple[Callable, Callable | None]] = {}
#: HTML assets
self.html_assets_policy: str = 'per_page'
@@ -99,12 +101,12 @@ class SphinxComponentRegistry:
self.html_themes: dict[str, str] = {}
#: js_files; list of JS paths or URLs
self.js_files: list[tuple[str, dict[str, Any]]] = []
self.js_files: list[tuple[str | None, dict[str, Any]]] = []
#: LaTeX packages; list of package names and its options
self.latex_packages: list[tuple[str, str]] = []
self.latex_packages: list[tuple[str, str | None]] = []
self.latex_packages_after_hyperref: list[tuple[str, str]] = []
self.latex_packages_after_hyperref: list[tuple[str, str | None]] = []
#: post transforms; list of transforms
self.post_transforms: list[type[Transform]] = []
@@ -120,7 +122,7 @@ class SphinxComponentRegistry:
#: custom handlers for translators
#: a dict of builder name -> dict of node name -> visitor and departure functions
self.translation_handlers: dict[str, dict[str, tuple[Callable, Callable]]] = {}
self.translation_handlers: dict[str, dict[str, tuple[Callable, Callable | None]]] = {}
#: additional transforms; list of transforms
self.transforms: list[type[Transform]] = []
@@ -151,8 +153,7 @@ class SphinxComponentRegistry:
self.load_extension(app, entry_point.module)
def create_builder(self, app: Sphinx, name: str,
env: BuildEnvironment | None = None) -> Builder:
def create_builder(self, app: Sphinx, name: str, env: BuildEnvironment) -> Builder:
if name not in self.builders:
raise SphinxError(__('Builder name %s not registered') % name)
@@ -234,7 +235,7 @@ class SphinxComponentRegistry:
directive = type(directivename,
(GenericObject, object),
{'indextemplate': indextemplate,
'parse_node': staticmethod(parse_node),
'parse_node': parse_node and staticmethod(parse_node),
'doc_field_types': doc_field_types})
self.add_directive_to_domain('std', directivename, directive)
@@ -314,7 +315,7 @@ class SphinxComponentRegistry:
def add_translation_handlers(
self,
node: type[Element],
**kwargs: tuple[Callable | None, Callable | None],
**kwargs: tuple[Callable, Callable | None],
) -> None:
logger.debug('[app] adding translation_handlers: %r, %r', node, kwargs)
for builder_name, handlers in kwargs.items():
@@ -412,16 +413,18 @@ class SphinxComponentRegistry:
def add_html_math_renderer(
self,
name: str,
inline_renderers: tuple[Callable | None, Callable | None] | None,
block_renderers: tuple[Callable | None, Callable | None] | None,
inline_renderers: tuple[Callable, Callable | None] | None,
block_renderers: tuple[Callable, Callable | None] | None,
) -> None:
logger.debug('[app] adding html_math_renderer: %s, %r, %r',
name, inline_renderers, block_renderers)
if name in self.html_inline_math_renderers:
raise ExtensionError(__('math renderer %s is already registered') % name)
self.html_inline_math_renderers[name] = inline_renderers
self.html_block_math_renderers[name] = block_renderers
if inline_renderers is not None:
self.html_inline_math_renderers[name] = inline_renderers
if block_renderers is not None:
self.html_block_math_renderers[name] = block_renderers
def add_html_theme(self, name: str, theme_path: str) -> None:
self.html_themes[name] = theme_path

View File

@@ -10,7 +10,7 @@ from docutils.parsers.rst.states import Body
field_list_item_re = re.compile(Body.patterns['field_marker'])
def separate_metadata(s: str) -> tuple[str, dict[str, str]]:
def separate_metadata(s: str | None) -> tuple[str | None, dict[str, str]]:
"""Separate docstring into metadata and others."""
in_other_element = False
metadata: dict[str, str] = {}

View File

@@ -130,7 +130,7 @@ def getorigbases(obj: Any) -> tuple[Any, ...] | None:
return None
def getslots(obj: Any) -> dict | None:
def getslots(obj: Any) -> dict[str, Any] | None:
"""Get __slots__ attribute of the class as dict.
Return None if gienv *obj* does not have __slots__.
@@ -148,7 +148,7 @@ def getslots(obj: Any) -> dict | None:
elif isinstance(__slots__, str):
return {__slots__: None}
elif isinstance(__slots__, (list, tuple)):
return {e: None for e in __slots__}
return dict.fromkeys(__slots__)
else:
raise ValueError

View File

@@ -898,25 +898,29 @@ class HTML5Translator(SphinxTranslator, BaseTranslator):
node['classes'].append('field-odd')
def visit_math(self, node: Element, math_env: str = '') -> None:
name = self.builder.math_renderer_name
# see validate_math_renderer
name: str = self.builder.math_renderer_name # type: ignore[assignment]
visit, _ = self.builder.app.registry.html_inline_math_renderers[name]
visit(self, node)
def depart_math(self, node: Element, math_env: str = '') -> None:
name = self.builder.math_renderer_name
# see validate_math_renderer
name: str = self.builder.math_renderer_name # type: ignore[assignment]
_, depart = self.builder.app.registry.html_inline_math_renderers[name]
if depart: # type: ignore[truthy-function]
if depart:
depart(self, node)
def visit_math_block(self, node: Element, math_env: str = '') -> None:
name = self.builder.math_renderer_name
# see validate_math_renderer
name: str = self.builder.math_renderer_name # type: ignore[assignment]
visit, _ = self.builder.app.registry.html_block_math_renderers[name]
visit(self, node)
def depart_math_block(self, node: Element, math_env: str = '') -> None:
name = self.builder.math_renderer_name
# see validate_math_renderer
name: str = self.builder.math_renderer_name # type: ignore[assignment]
_, depart = self.builder.app.registry.html_block_math_renderers[name]
if depart: # type: ignore[truthy-function]
if depart:
depart(self, node)
# See Docutils r9413

View File

@@ -75,12 +75,11 @@ class LaTeXWriter(writers.Writer):
))
settings_defaults: dict[str, Any] = {}
output = None
theme: Theme
def __init__(self, builder: LaTeXBuilder) -> None:
super().__init__()
self.builder = builder
self.theme: Theme = None
def translate(self) -> None:
visitor = self.builder.create_translator(self.document, self.builder, self.theme)
@@ -110,17 +109,18 @@ class Table:
elif 'colorrows' in self.classes:
self.styles.append('colorrows')
self.colcount = 0
self.colspec: str = None
self.colsep: str = None
self.colspec: str = ''
if 'booktabs' in self.styles or 'borderless' in self.styles:
self.colsep = ''
self.colsep: str | None = ''
elif 'standard' in self.styles:
self.colsep = '|'
else:
self.colsep = None
self.colwidths: list[int] = []
self.has_problematic = False
self.has_oldproblematic = False
self.has_verbatim = False
self.caption: list[str] = None
self.caption: list[str] = []
self.stubs: list[int] = []
# current position
@@ -169,6 +169,7 @@ class Table:
return self.colspec
_colsep = self.colsep
assert _colsep is not None
if self.colwidths and 'colwidths-given' in self.classes:
total = sum(self.colwidths)
colspecs = [r'\X{%d}{%d}' % (width, total) for width in self.colwidths]
@@ -660,6 +661,7 @@ class LaTeXTranslator(SphinxTranslator):
def depart_title(self, node: Element) -> None:
self.in_title = 0
if isinstance(node.parent, nodes.table):
assert self.table is not None
self.table.caption = self.popbody()
else:
self.body.append(self.context.pop())
@@ -992,6 +994,7 @@ class LaTeXTranslator(SphinxTranslator):
def visit_table(self, node: Element) -> None:
if len(self.tables) == 1:
assert self.table is not None
if self.table.get_table_type() == 'longtable':
raise UnsupportedError(
'%s:%s: longtable does not support nesting a table.' %
@@ -1004,26 +1007,28 @@ class LaTeXTranslator(SphinxTranslator):
'%s:%s: deeply nested tables are not implemented.' %
(self.curfilestack[-1], node.line or ''))
self.tables.append(Table(node))
if self.table.colsep is None:
self.table.colsep = '' if (
'booktabs' in self.builder.config.latex_table_style or
'borderless' in self.builder.config.latex_table_style
) else '|'
table = Table(node)
self.tables.append(table)
if table.colsep is None:
table.colsep = '|' * (
'booktabs' not in self.builder.config.latex_table_style
and 'borderless' not in self.builder.config.latex_table_style
)
if self.next_table_colspec:
self.table.colspec = '{%s}' % self.next_table_colspec + CR
if '|' in self.table.colspec:
self.table.styles.append('vlines')
self.table.colsep = '|'
table.colspec = '{%s}' % self.next_table_colspec + CR
if '|' in table.colspec:
table.styles.append('vlines')
table.colsep = '|'
else:
self.table.styles.append('novlines')
self.table.colsep = ''
table.styles.append('novlines')
table.colsep = ''
if 'colwidths-given' in node.get('classes', []):
logger.info(__('both tabularcolumns and :widths: option are given. '
':widths: is ignored.'), location=node)
self.next_table_colspec = None
def depart_table(self, node: Element) -> None:
assert self.table is not None
labels = self.hypertarget_to(node)
table_type = self.table.get_table_type()
table = self.render(table_type + '.tex_t',
@@ -1035,6 +1040,7 @@ class LaTeXTranslator(SphinxTranslator):
self.tables.pop()
def visit_colspec(self, node: Element) -> None:
assert self.table is not None
self.table.colcount += 1
if 'colwidth' in node:
self.table.colwidths.append(node['colwidth'])
@@ -1051,6 +1057,7 @@ class LaTeXTranslator(SphinxTranslator):
pass
def visit_thead(self, node: Element) -> None:
assert self.table is not None
# Redirect head output until header is finished.
self.pushbody(self.table.header)
@@ -1060,6 +1067,7 @@ class LaTeXTranslator(SphinxTranslator):
self.popbody()
def visit_tbody(self, node: Element) -> None:
assert self.table is not None
# Redirect body output until table is finished.
self.pushbody(self.table.body)
@@ -1069,6 +1077,7 @@ class LaTeXTranslator(SphinxTranslator):
self.popbody()
def visit_row(self, node: Element) -> None:
assert self.table is not None
self.table.col = 0
_colsep = self.table.colsep
# fill columns if the row starts with the bottom of multirow cell
@@ -1088,9 +1097,11 @@ class LaTeXTranslator(SphinxTranslator):
(cell.width, _colsep, _colsep, cell.cell_id))
def depart_row(self, node: Element) -> None:
assert self.table is not None
self.body.append(r'\\' + CR)
cells = [self.table.cell(self.table.row, i) for i in range(self.table.colcount)]
underlined = [cell.row + cell.height == self.table.row + 1 for cell in cells]
underlined = [cell.row + cell.height == self.table.row + 1 # type: ignore[union-attr]
for cell in cells]
if all(underlined):
self.body.append(r'\sphinxhline')
else:
@@ -1099,7 +1110,7 @@ class LaTeXTranslator(SphinxTranslator):
if underlined[0] is False:
i = 1
while i < self.table.colcount and underlined[i] is False:
if cells[i - 1].cell_id != cells[i].cell_id:
if cells[i - 1].cell_id != cells[i].cell_id: # type: ignore[union-attr]
self.body.append(r'\sphinxvlinecrossing{%d}' % i)
i += 1
while i < self.table.colcount:
@@ -1109,17 +1120,19 @@ class LaTeXTranslator(SphinxTranslator):
i += j
i += 1
while i < self.table.colcount and underlined[i] is False:
if cells[i - 1].cell_id != cells[i].cell_id:
if cells[i - 1].cell_id != cells[i].cell_id: # type: ignore[union-attr]
self.body.append(r'\sphinxvlinecrossing{%d}' % i)
i += 1
self.body.append(r'\sphinxfixclines{%d}' % self.table.colcount)
self.table.row += 1
def visit_entry(self, node: Element) -> None:
assert self.table is not None
if self.table.col > 0:
self.body.append('&')
self.table.add_cell(node.get('morerows', 0) + 1, node.get('morecols', 0) + 1)
cell = self.table.cell()
assert cell is not None
context = ''
_colsep = self.table.colsep
if cell.width > 1:
@@ -1166,7 +1179,9 @@ class LaTeXTranslator(SphinxTranslator):
self.body.append(self.context.pop())
assert self.table is not None
cell = self.table.cell()
assert cell is not None
self.table.col += cell.width
_colsep = self.table.colsep
@@ -1628,7 +1643,7 @@ class LaTeXTranslator(SphinxTranslator):
ids = node['ids'][:] # copy to avoid side-effects
while has_dup_label(prev):
ids.remove(prev['refid']) # type: ignore
prev = get_prev_node(prev)
prev = get_prev_node(prev) # type: ignore[arg-type]
else:
ids = iter(node['ids']) # read-only iterator
else: