Merge branch '3.x' into master_with_merged_3.x

This commit is contained in:
jfbu 2021-02-02 18:01:55 +01:00
commit fbafb308b8
23 changed files with 363 additions and 124 deletions

11
CHANGES
View File

@ -119,6 +119,7 @@ Features added
info-field-list info-field-list
* #8514: autodoc: Default values of overloaded functions are taken from actual * #8514: autodoc: Default values of overloaded functions are taken from actual
implementation if they're ellipsis implementation if they're ellipsis
* #8775: autodoc: Support type union operator (PEP-604) in Python 3.10 or above
* #8619: html: kbd role generates customizable HTML tags for compound keys * #8619: html: kbd role generates customizable HTML tags for compound keys
* #8634: html: Allow to change the order of JS/CSS via ``priority`` parameter * #8634: html: Allow to change the order of JS/CSS via ``priority`` parameter
for :meth:`Sphinx.add_js_file()` and :meth:`Sphinx.add_css_file()` for :meth:`Sphinx.add_js_file()` and :meth:`Sphinx.add_css_file()`
@ -135,6 +136,7 @@ Features added
* #8004: napoleon: Type definitions in Google style docstrings are rendered as * #8004: napoleon: Type definitions in Google style docstrings are rendered as
references when :confval:`napoleon_preprocess_types` enabled references when :confval:`napoleon_preprocess_types` enabled
* #6241: mathjax: Include mathjax.js only on the document using equations * #6241: mathjax: Include mathjax.js only on the document using equations
* #8775: py domain: Support type union operator (PEP-604)
* #8651: std domain: cross-reference for a rubric having inline item is broken * #8651: std domain: cross-reference for a rubric having inline item is broken
* #7642: std domain: Optimize case-insensitive match of term * #7642: std domain: Optimize case-insensitive match of term
* #8681: viewcode: Support incremental build * #8681: viewcode: Support incremental build
@ -158,6 +160,8 @@ Bugs fixed
contains invalid type comments contains invalid type comments
* #8693: autodoc: Default values for overloaded functions are rendered as string * #8693: autodoc: Default values for overloaded functions are rendered as string
* #8134: autodoc: crashes when mocked decorator takes arguments * #8134: autodoc: crashes when mocked decorator takes arguments
* #8800: autodoc: Uninitialized attributes in superclass are recognized as
undocumented
* #8306: autosummary: mocked modules are documented as empty page when using * #8306: autosummary: mocked modules are documented as empty page when using
:recursive: option :recursive: option
* #8232: graphviz: Image node is not rendered if graph file is in subdirectory * #8232: graphviz: Image node is not rendered if graph file is in subdirectory
@ -168,13 +172,16 @@ Bugs fixed
* #8123: html search: fix searching for terms containing + (Requires a custom * #8123: html search: fix searching for terms containing + (Requires a custom
search language that does not split on +) search language that does not split on +)
* #8665: html theme: Could not override globaltoc_maxdepth in theme.conf * #8665: html theme: Could not override globaltoc_maxdepth in theme.conf
* #8446: html: consecutive spaces are displayed as single space
* #8745: i18n: crashes with KeyError when translation message adds a new auto * #8745: i18n: crashes with KeyError when translation message adds a new auto
footnote reference footnote reference
* #4304: linkcheck: Fix race condition that could lead to checking the * #4304: linkcheck: Fix race condition that could lead to checking the
availability of the same URL twice availability of the same URL twice
* #8791: linkcheck: The docname for each hyperlink is not displayed
* #7118: sphinx-quickstart: questionare got Mojibake if libreadline unavailable * #7118: sphinx-quickstart: questionare got Mojibake if libreadline unavailable
* #8094: texinfo: image files on the different directory with document are not * #8094: texinfo: image files on the different directory with document are not
copied copied
* #8782: todo: Cross references in todolist get broken
* #8720: viewcode: module pages are generated for epub on incremental build * #8720: viewcode: module pages are generated for epub on incremental build
* #8704: viewcode: anchors are generated in incremental build after singlehtml * #8704: viewcode: anchors are generated in incremental build after singlehtml
* #8756: viewcode: highlighted code is generated even if not referenced * #8756: viewcode: highlighted code is generated even if not referenced
@ -194,6 +201,8 @@ Bugs fixed
:confval:`numfig` is not True :confval:`numfig` is not True
* #8442: LaTeX: some indexed terms are ignored when using xelatex engine * #8442: LaTeX: some indexed terms are ignored when using xelatex engine
(or pdflatex and :confval:`latex_use_xindy` set to True) with memoir class (or pdflatex and :confval:`latex_use_xindy` set to True) with memoir class
* #8750: LaTeX: URLs as footnotes fail to show in PDF if originating from
inside function type signatures
* #8780: LaTeX: long words in narrow columns may not be hyphenated * #8780: LaTeX: long words in narrow columns may not be hyphenated
* #8788: LaTeX: ``\titleformat`` last argument in sphinx.sty should be * #8788: LaTeX: ``\titleformat`` last argument in sphinx.sty should be
bracketed, not braced (and is anyhow not needed) bracketed, not braced (and is anyhow not needed)
@ -222,6 +231,8 @@ Bugs fixed
* #8655: autodoc: Failed to generate document if target module contains an * #8655: autodoc: Failed to generate document if target module contains an
object that raises an exception on ``hasattr()`` object that raises an exception on ``hasattr()``
* C, ``expr`` role should start symbol lookup in the current scope. * C, ``expr`` role should start symbol lookup in the current scope.
* #8796: LaTeX: potentially critical low level TeX coding mistake has gone
unnoticed so far
Testing Testing
-------- --------

View File

@ -70,13 +70,14 @@ Project information
The author name(s) of the document. The default value is ``'unknown'``. The author name(s) of the document. The default value is ``'unknown'``.
.. confval:: copyright .. confval:: copyright
.. confval:: project_copyright
A copyright statement in the style ``'2008, Author Name'``. A copyright statement in the style ``'2008, Author Name'``.
.. versionchanged:: 3.5 .. confval:: project_copyright
As an alias, ``project_copyright`` is also allowed. An alias of :confval:`copyright`.
.. versionadded:: 3.5
.. confval:: version .. confval:: version

View File

@ -108,11 +108,11 @@ class CheckExternalLinksBuilder(DummyBuilder):
def init(self) -> None: def init(self) -> None:
self.hyperlinks = {} # type: Dict[str, Hyperlink] self.hyperlinks = {} # type: Dict[str, Hyperlink]
self.to_ignore = [re.compile(x) for x in self.app.config.linkcheck_ignore] self.to_ignore = [re.compile(x) for x in self.config.linkcheck_ignore]
self.anchors_ignore = [re.compile(x) self.anchors_ignore = [re.compile(x)
for x in self.app.config.linkcheck_anchors_ignore] for x in self.config.linkcheck_anchors_ignore]
self.auth = [(re.compile(pattern), auth_info) for pattern, auth_info self.auth = [(re.compile(pattern), auth_info) for pattern, auth_info
in self.app.config.linkcheck_auth] in self.config.linkcheck_auth]
self._good = set() # type: Set[str] self._good = set() # type: Set[str]
self._broken = {} # type: Dict[str, str] self._broken = {} # type: Dict[str, str]
self._redirected = {} # type: Dict[str, Tuple[str, int]] self._redirected = {} # type: Dict[str, Tuple[str, int]]
@ -124,13 +124,13 @@ class CheckExternalLinksBuilder(DummyBuilder):
self.wqueue = queue.PriorityQueue() # type: queue.PriorityQueue self.wqueue = queue.PriorityQueue() # type: queue.PriorityQueue
self.rqueue = queue.Queue() # type: queue.Queue self.rqueue = queue.Queue() # type: queue.Queue
self.workers = [] # type: List[threading.Thread] self.workers = [] # type: List[threading.Thread]
for i in range(self.app.config.linkcheck_workers): for i in range(self.config.linkcheck_workers):
thread = threading.Thread(target=self.check_thread, daemon=True) thread = threading.Thread(target=self.check_thread, daemon=True)
thread.start() thread.start()
self.workers.append(thread) self.workers.append(thread)
@property @property
def good(self): def good(self) -> Set[str]:
warnings.warn( warnings.warn(
"%s.%s is deprecated." % (self.__class__.__name__, "good"), "%s.%s is deprecated." % (self.__class__.__name__, "good"),
RemovedInSphinx50Warning, RemovedInSphinx50Warning,
@ -139,7 +139,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
return self._good return self._good
@property @property
def broken(self): def broken(self) -> Dict[str, str]:
warnings.warn( warnings.warn(
"%s.%s is deprecated." % (self.__class__.__name__, "broken"), "%s.%s is deprecated." % (self.__class__.__name__, "broken"),
RemovedInSphinx50Warning, RemovedInSphinx50Warning,
@ -148,7 +148,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
return self._broken return self._broken
@property @property
def redirected(self): def redirected(self) -> Dict[str, Tuple[str, int]]:
warnings.warn( warnings.warn(
"%s.%s is deprecated." % (self.__class__.__name__, "redirected"), "%s.%s is deprecated." % (self.__class__.__name__, "redirected"),
RemovedInSphinx50Warning, RemovedInSphinx50Warning,
@ -158,8 +158,8 @@ class CheckExternalLinksBuilder(DummyBuilder):
def check_thread(self) -> None: def check_thread(self) -> None:
kwargs = {} kwargs = {}
if self.app.config.linkcheck_timeout: if self.config.linkcheck_timeout:
kwargs['timeout'] = self.app.config.linkcheck_timeout kwargs['timeout'] = self.config.linkcheck_timeout
def get_request_headers() -> Dict: def get_request_headers() -> Dict:
url = urlparse(uri) url = urlparse(uri)
@ -205,9 +205,9 @@ class CheckExternalLinksBuilder(DummyBuilder):
kwargs['headers'] = get_request_headers() kwargs['headers'] = get_request_headers()
try: try:
if anchor and self.app.config.linkcheck_anchors: if anchor and self.config.linkcheck_anchors:
# Read the whole document and see if #anchor exists # Read the whole document and see if #anchor exists
response = requests.get(req_url, stream=True, config=self.app.config, response = requests.get(req_url, stream=True, config=self.config,
auth=auth_info, **kwargs) auth=auth_info, **kwargs)
response.raise_for_status() response.raise_for_status()
found = check_anchor(response, unquote(anchor)) found = check_anchor(response, unquote(anchor))
@ -219,7 +219,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
# try a HEAD request first, which should be easier on # try a HEAD request first, which should be easier on
# the server and the network # the server and the network
response = requests.head(req_url, allow_redirects=True, response = requests.head(req_url, allow_redirects=True,
config=self.app.config, auth=auth_info, config=self.config, auth=auth_info,
**kwargs) **kwargs)
response.raise_for_status() response.raise_for_status()
except (HTTPError, TooManyRedirects) as err: except (HTTPError, TooManyRedirects) as err:
@ -228,7 +228,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
# retry with GET request if that fails, some servers # retry with GET request if that fails, some servers
# don't like HEAD requests. # don't like HEAD requests.
response = requests.get(req_url, stream=True, response = requests.get(req_url, stream=True,
config=self.app.config, config=self.config,
auth=auth_info, **kwargs) auth=auth_info, **kwargs)
response.raise_for_status() response.raise_for_status()
except HTTPError as err: except HTTPError as err:
@ -297,7 +297,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
return 'ignored', '', 0 return 'ignored', '', 0
# need to actually check the URI # need to actually check the URI
for _ in range(self.app.config.linkcheck_retries): for _ in range(self.config.linkcheck_retries):
status, info, code = check_uri() status, info, code = check_uri()
if status != "broken": if status != "broken":
break break
@ -360,7 +360,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
next_check = time.time() + delay next_check = time.time() + delay
netloc = urlparse(response.url).netloc netloc = urlparse(response.url).netloc
if next_check is None: if next_check is None:
max_delay = self.app.config.linkcheck_rate_limit_timeout max_delay = self.config.linkcheck_rate_limit_timeout
try: try:
rate_limit = self.rate_limits[netloc] rate_limit = self.rate_limits[netloc]
except KeyError: except KeyError:
@ -390,7 +390,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
self.write_linkstat(linkstat) self.write_linkstat(linkstat)
return return
if lineno: if lineno:
logger.info('(line %4d) ', lineno, nonl=True) logger.info('(%16s: line %4d) ', docname, lineno, nonl=True)
if status == 'ignored': if status == 'ignored':
if info: if info:
logger.info(darkgray('-ignored- ') + uri + ': ' + info) logger.info(darkgray('-ignored- ') + uri + ': ' + info)

View File

@ -101,17 +101,19 @@ def _parse_annotation(annotation: str, env: BuildEnvironment = None) -> List[Nod
def unparse(node: ast.AST) -> List[Node]: def unparse(node: ast.AST) -> List[Node]:
if isinstance(node, ast.Attribute): if isinstance(node, ast.Attribute):
return [nodes.Text("%s.%s" % (unparse(node.value)[0], node.attr))] return [nodes.Text("%s.%s" % (unparse(node.value)[0], node.attr))]
elif isinstance(node, ast.Constant): # type: ignore elif isinstance(node, ast.BinOp):
if node.value is Ellipsis: result = unparse(node.left) # type: List[Node]
return [addnodes.desc_sig_punctuation('', "...")] result.extend(unparse(node.op))
else: result.extend(unparse(node.right))
return [nodes.Text(node.value)] return result
elif isinstance(node, ast.BitOr):
return [nodes.Text(' '), addnodes.desc_sig_punctuation('', '|'), nodes.Text(' ')]
elif isinstance(node, ast.Expr): elif isinstance(node, ast.Expr):
return unparse(node.value) return unparse(node.value)
elif isinstance(node, ast.Index): elif isinstance(node, ast.Index):
return unparse(node.value) return unparse(node.value)
elif isinstance(node, ast.List): elif isinstance(node, ast.List):
result = [addnodes.desc_sig_punctuation('', '[')] # type: List[Node] result = [addnodes.desc_sig_punctuation('', '[')]
for elem in node.elts: for elem in node.elts:
result.extend(unparse(elem)) result.extend(unparse(elem))
result.append(addnodes.desc_sig_punctuation('', ', ')) result.append(addnodes.desc_sig_punctuation('', ', '))
@ -157,7 +159,7 @@ def _parse_annotation(annotation: str, env: BuildEnvironment = None) -> List[Nod
tree = ast_parse(annotation) tree = ast_parse(annotation)
result = unparse(tree) result = unparse(tree)
for i, node in enumerate(result): for i, node in enumerate(result):
if isinstance(node, nodes.Text): if isinstance(node, nodes.Text) and node.strip():
result[i] = type_to_xref(str(node), env) result[i] = type_to_xref(str(node), env)
return result return result
except SyntaxError: except SyntaxError:

View File

@ -1012,10 +1012,6 @@ class ModuleDocumenter(Documenter):
try: try:
if not self.options.ignore_module_all: if not self.options.ignore_module_all:
self.__all__ = inspect.getall(self.object) self.__all__ = inspect.getall(self.object)
except AttributeError as exc:
# __all__ raises an error.
logger.warning(__('%s.__all__ raises an error. Ignored: %r'),
(self.fullname, exc), type='autodoc')
except ValueError as exc: except ValueError as exc:
# invalid __all__ found. # invalid __all__ found.
logger.warning(__('__all__ should be a list of strings, not %r ' logger.warning(__('__all__ should be a list of strings, not %r '
@ -1056,14 +1052,11 @@ class ModuleDocumenter(Documenter):
continue continue
# annotation only member (ex. attr: int) # annotation only member (ex. attr: int)
try: for name in inspect.getannotations(self.object):
for name in inspect.getannotations(self.object): if name not in members:
if name not in members: docstring = attr_docs.get(('', name), [])
docstring = attr_docs.get(('', name), []) members[name] = ObjectMember(name, INSTANCEATTR,
members[name] = ObjectMember(name, INSTANCEATTR, docstring="\n".join(docstring))
docstring="\n".join(docstring))
except AttributeError:
pass
return members return members
@ -1890,16 +1883,16 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
def update_annotations(self, parent: Any) -> None: def update_annotations(self, parent: Any) -> None:
"""Update __annotations__ to support type_comment and so on.""" """Update __annotations__ to support type_comment and so on."""
try: annotations = dict(inspect.getannotations(parent))
annotations = dict(inspect.getannotations(parent)) parent.__annotations__ = annotations
parent.__annotations__ = annotations
try:
analyzer = ModuleAnalyzer.for_module(self.modname) analyzer = ModuleAnalyzer.for_module(self.modname)
analyzer.analyze() analyzer.analyze()
for (classname, attrname), annotation in analyzer.annotations.items(): for (classname, attrname), annotation in analyzer.annotations.items():
if classname == '' and attrname not in annotations: if classname == '' and attrname not in annotations:
annotations[attrname] = annotation annotations[attrname] = annotation
except AttributeError: except PycodeError:
pass pass
def import_object(self, raiseerror: bool = False) -> bool: def import_object(self, raiseerror: bool = False) -> bool:
@ -2212,7 +2205,7 @@ class SlotsMixin(DataDocumenterMixinBase):
return True return True
else: else:
return False return False
except (AttributeError, ValueError, TypeError): except (ValueError, TypeError):
return False return False
def import_object(self, raiseerror: bool = False) -> bool: def import_object(self, raiseerror: bool = False) -> bool:
@ -2238,7 +2231,7 @@ class SlotsMixin(DataDocumenterMixinBase):
return [docstring] return [docstring]
else: else:
return [] return []
except (AttributeError, ValueError) as exc: except ValueError as exc:
logger.warning(__('Invalid __slots__ found on %s. Ignored.'), logger.warning(__('Invalid __slots__ found on %s. Ignored.'),
(self.parent.__qualname__, exc), type='autodoc') (self.parent.__qualname__, exc), type='autodoc')
return [] return []
@ -2429,8 +2422,6 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
annotations[attrname] = annotation annotations[attrname] = annotation
except (AttributeError, PycodeError): except (AttributeError, PycodeError):
pass pass
except AttributeError:
pass
except TypeError: except TypeError:
# Failed to set __annotations__ (built-in, extensions, etc.) # Failed to set __annotations__ (built-in, extensions, etc.)
pass pass
@ -2484,22 +2475,19 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
pass pass
def get_attribute_comment(self, parent: Any, attrname: str) -> Optional[List[str]]: def get_attribute_comment(self, parent: Any, attrname: str) -> Optional[List[str]]:
try: for cls in inspect.getmro(parent):
for cls in inspect.getmro(parent): try:
try: module = safe_getattr(cls, '__module__')
module = safe_getattr(cls, '__module__') qualname = safe_getattr(cls, '__qualname__')
qualname = safe_getattr(cls, '__qualname__')
analyzer = ModuleAnalyzer.for_module(module) analyzer = ModuleAnalyzer.for_module(module)
analyzer.analyze() analyzer.analyze()
if qualname and self.objpath: if qualname and self.objpath:
key = (qualname, attrname) key = (qualname, attrname)
if key in analyzer.attr_docs: if key in analyzer.attr_docs:
return list(analyzer.attr_docs[key]) return list(analyzer.attr_docs[key])
except (AttributeError, PycodeError): except (AttributeError, PycodeError):
pass pass
except (AttributeError, PycodeError):
pass
return None return None

View File

@ -156,12 +156,9 @@ def get_module_members(module: Any) -> List[Tuple[str, Any]]:
continue continue
# annotation only member (ex. attr: int) # annotation only member (ex. attr: int)
try: for name in getannotations(module):
for name in getannotations(module): if name not in members:
if name not in members: members[name] = (name, INSTANCEATTR)
members[name] = (name, INSTANCEATTR)
except AttributeError:
pass
return sorted(list(members.values())) return sorted(list(members.values()))
@ -202,7 +199,7 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
for name in __slots__: for name in __slots__:
members[name] = Attribute(name, True, SLOTSATTR) members[name] = Attribute(name, True, SLOTSATTR)
except (AttributeError, TypeError, ValueError): except (TypeError, ValueError):
pass pass
# other members # other members
@ -218,13 +215,10 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
# annotation only member (ex. attr: int) # annotation only member (ex. attr: int)
for i, cls in enumerate(getmro(subject)): for i, cls in enumerate(getmro(subject)):
try: for name in getannotations(cls):
for name in getannotations(cls): name = unmangle(cls, name)
name = unmangle(cls, name) if name and name not in members:
if name and name not in members: members[name] = Attribute(name, i == 0, INSTANCEATTR)
members[name] = Attribute(name, i == 0, INSTANCEATTR)
except AttributeError:
pass
if analyzer: if analyzer:
# append instance attributes (cf. self.attr1) if analyzer knows # append instance attributes (cf. self.attr1) if analyzer knows
@ -267,7 +261,7 @@ def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable
for name, docstring in __slots__.items(): for name, docstring in __slots__.items():
members[name] = ObjectMember(name, SLOTSATTR, class_=subject, members[name] = ObjectMember(name, SLOTSATTR, class_=subject,
docstring=docstring) docstring=docstring)
except (AttributeError, TypeError, ValueError): except (TypeError, ValueError):
pass pass
# other members # other members
@ -288,27 +282,35 @@ def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable
try: try:
for cls in getmro(subject): for cls in getmro(subject):
# annotation only member (ex. attr: int)
try:
for name in getannotations(cls):
name = unmangle(cls, name)
if name and name not in members:
members[name] = ObjectMember(name, INSTANCEATTR, class_=cls)
except AttributeError:
pass
# append instance attributes (cf. self.attr1) if analyzer knows
try: try:
modname = safe_getattr(cls, '__module__') modname = safe_getattr(cls, '__module__')
qualname = safe_getattr(cls, '__qualname__') qualname = safe_getattr(cls, '__qualname__')
analyzer = ModuleAnalyzer.for_module(modname) analyzer = ModuleAnalyzer.for_module(modname)
analyzer.analyze() analyzer.analyze()
except AttributeError:
qualname = None
analyzer = None
except PycodeError:
analyzer = None
# annotation only member (ex. attr: int)
for name in getannotations(cls):
name = unmangle(cls, name)
if name and name not in members:
if analyzer and (qualname, name) in analyzer.attr_docs:
docstring = '\n'.join(analyzer.attr_docs[qualname, name])
else:
docstring = None
members[name] = ObjectMember(name, INSTANCEATTR, class_=cls,
docstring=docstring)
# append instance attributes (cf. self.attr1) if analyzer knows
if analyzer:
for (ns, name), docstring in analyzer.attr_docs.items(): for (ns, name), docstring in analyzer.attr_docs.items():
if ns == qualname and name not in members: if ns == qualname and name not in members:
members[name] = ObjectMember(name, INSTANCEATTR, class_=cls, members[name] = ObjectMember(name, INSTANCEATTR, class_=cls,
docstring='\n'.join(docstring)) docstring='\n'.join(docstring))
except (AttributeError, PycodeError):
pass
except AttributeError: except AttributeError:
pass pass

View File

@ -19,6 +19,7 @@ from docutils.parsers.rst import directives
from docutils.parsers.rst.directives.admonitions import BaseAdmonition from docutils.parsers.rst.directives.admonitions import BaseAdmonition
import sphinx import sphinx
from sphinx import addnodes
from sphinx.application import Sphinx from sphinx.application import Sphinx
from sphinx.domains import Domain from sphinx.domains import Domain
from sphinx.environment import BuildEnvironment from sphinx.environment import BuildEnvironment
@ -123,12 +124,12 @@ class TodoListProcessor:
self.config = app.config self.config = app.config
self.env = app.env self.env = app.env
self.domain = cast(TodoDomain, app.env.get_domain('todo')) self.domain = cast(TodoDomain, app.env.get_domain('todo'))
self.document = new_document('')
self.process(doctree, docname) self.process(doctree, docname)
def process(self, doctree: nodes.document, docname: str) -> None: def process(self, doctree: nodes.document, docname: str) -> None:
todos = sum(self.domain.todos.values(), []) # type: List[todo_node] todos = sum(self.domain.todos.values(), []) # type: List[todo_node]
document = new_document('')
for node in doctree.traverse(todolist): for node in doctree.traverse(todolist):
if not self.config.todo_include_todos: if not self.config.todo_include_todos:
node.parent.remove(node) node.parent.remove(node)
@ -144,12 +145,7 @@ class TodoListProcessor:
new_todo = todo.deepcopy() new_todo = todo.deepcopy()
new_todo['ids'].clear() new_todo['ids'].clear()
# (Recursively) resolve references in the todo content self.resolve_reference(new_todo, docname)
#
# Note: To resolve references, it is needed to wrap it with document node
document += new_todo
self.env.resolve_references(document, todo['docname'], self.builder)
document.remove(new_todo)
content.append(new_todo) content.append(new_todo)
todo_ref = self.create_todo_reference(todo, docname) todo_ref = self.create_todo_reference(todo, docname)
@ -185,6 +181,17 @@ class TodoListProcessor:
return para return para
def resolve_reference(self, todo: todo_node, docname: str) -> None:
"""Resolve references in the todo content."""
for node in todo.traverse(addnodes.pending_xref):
if 'refdoc' in node:
node['refdoc'] = docname
# Note: To resolve references, it is needed to wrap it with document node
self.document += todo
self.env.resolve_references(self.document, docname, self.builder)
self.document.remove(todo)
def visit_todo_node(self: HTMLTranslator, node: todo_node) -> None: def visit_todo_node(self: HTMLTranslator, node: todo_node) -> None:
if self.config.todo_include_todos: if self.config.todo_include_todos:

View File

@ -1,6 +1,6 @@
\NeedsTeXFormat{LaTeX2e} \NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{sphinxpackagefootnote}% \ProvidesPackage{sphinxpackagefootnote}%
[2021/01/26 v1.1b footnotehyper adapted to sphinx (Sphinx team)] [2021/01/29 v1.1c footnotehyper adapted to sphinx (Sphinx team)]
% Provides support for this output mark-up from Sphinx latex writer: % Provides support for this output mark-up from Sphinx latex writer:
% - footnote environment % - footnote environment
% - savenotes environment (table templates) % - savenotes environment (table templates)
@ -8,7 +8,7 @@
% %
%% %%
%% Package: sphinxpackagefootnote %% Package: sphinxpackagefootnote
%% Version: based on footnotehyper.sty 2021/01/26 v1.1b %% Version: based on footnotehyper.sty 2021/01/29 v1.1c
%% as available at https://www.ctan.org/pkg/footnotehyper %% as available at https://www.ctan.org/pkg/footnotehyper
%% License: the one applying to Sphinx %% License: the one applying to Sphinx
%% %%
@ -25,6 +25,7 @@
\DeclareOption*{\PackageWarning{sphinxpackagefootnote}{Option `\CurrentOption' is unknown}}% \DeclareOption*{\PackageWarning{sphinxpackagefootnote}{Option `\CurrentOption' is unknown}}%
\ProcessOptions\relax \ProcessOptions\relax
\newbox\FNH@notes \newbox\FNH@notes
\newtoks\FNH@toks % 1.1c
\newdimen\FNH@width \newdimen\FNH@width
\let\FNH@colwidth\columnwidth \let\FNH@colwidth\columnwidth
\newif\ifFNH@savingnotes \newif\ifFNH@savingnotes
@ -32,6 +33,7 @@
\let\FNH@latex@footnote \footnote \let\FNH@latex@footnote \footnote
\let\FNH@latex@footnotetext\footnotetext \let\FNH@latex@footnotetext\footnotetext
\let\FNH@H@@footnotetext \@footnotetext \let\FNH@H@@footnotetext \@footnotetext
\let\FNH@H@@mpfootnotetext \@mpfootnotetext
\newenvironment{savenotes} \newenvironment{savenotes}
{\FNH@savenotes\ignorespaces}{\FNH@spewnotes\ignorespacesafterend}% {\FNH@savenotes\ignorespaces}{\FNH@spewnotes\ignorespacesafterend}%
\let\spewnotes \FNH@spewnotes \let\spewnotes \FNH@spewnotes
@ -42,6 +44,7 @@
\@ifpackageloaded{hyperref} \@ifpackageloaded{hyperref}
{\ifHy@hyperfootnotes {\ifHy@hyperfootnotes
\let\FNH@H@@footnotetext\H@@footnotetext \let\FNH@H@@footnotetext\H@@footnotetext
\let\FNH@H@@mpfootnotetext\H@@mpfootnotetext
\else \else
\let\FNH@hyper@fntext\FNH@nohyp@fntext \let\FNH@hyper@fntext\FNH@nohyp@fntext
\fi}% \fi}%
@ -116,14 +119,33 @@
\fi \fi
}% }%
\def\FNH@spewnotes {% \def\FNH@spewnotes {%
\endgroup \if@endpe\ifx\par\@@par\FNH@toks{}\else
\FNH@toks\expandafter{\expandafter
\def\expandafter\par\expandafter{\par}\@endpetrue}%
\expandafter\expandafter\expandafter
\FNH@toks
\expandafter\expandafter\expandafter
{\expandafter\the\expandafter\FNH@toks
\expandafter\def\expandafter\@par\expandafter{\@par}}%
\expandafter\expandafter\expandafter
\FNH@toks
\expandafter\expandafter\expandafter
{\expandafter\the\expandafter\FNH@toks
\expandafter\everypar\expandafter{\the\everypar}}\fi
\else\FNH@toks{}\fi
\expandafter
\endgroup\the\FNH@toks
\ifFNH@savingnotes\else \ifFNH@savingnotes\else
\ifvoid\FNH@notes\else \ifvoid\FNH@notes\else
\begingroup \begingroup
\let\@makefntext\@empty \let\@makefntext\@empty
\let\@finalstrut\@gobble \let\@finalstrut\@gobble
\let\rule\@gobbletwo \let\rule\@gobbletwo
\FNH@H@@footnotetext{\unvbox\FNH@notes}% \ifx\@footnotetext\@mpfootnotetext
\expandafter\FNH@H@@mpfootnotetext
\else
\expandafter\FNH@H@@footnotetext
\fi{\unvbox\FNH@notes}%
\endgroup \endgroup
\fi \fi
\fi \fi

View File

@ -145,7 +145,6 @@ def getall(obj: Any) -> Optional[Sequence[str]]:
"""Get __all__ attribute of the module as dict. """Get __all__ attribute of the module as dict.
Return None if given *obj* does not have __all__. Return None if given *obj* does not have __all__.
Raises AttributeError if given *obj* raises an error on accessing __all__.
Raises ValueError if given *obj* have invalid __all__. Raises ValueError if given *obj* have invalid __all__.
""" """
__all__ = safe_getattr(obj, '__all__', None) __all__ = safe_getattr(obj, '__all__', None)
@ -159,10 +158,7 @@ def getall(obj: Any) -> Optional[Sequence[str]]:
def getannotations(obj: Any) -> Mapping[str, Any]: def getannotations(obj: Any) -> Mapping[str, Any]:
"""Get __annotations__ from given *obj* safely. """Get __annotations__ from given *obj* safely."""
Raises AttributeError if given *obj* raises an error on accessing __attribute__.
"""
__annotations__ = safe_getattr(obj, '__annotations__', None) __annotations__ = safe_getattr(obj, '__annotations__', None)
if isinstance(__annotations__, Mapping): if isinstance(__annotations__, Mapping):
return __annotations__ return __annotations__
@ -171,10 +167,7 @@ def getannotations(obj: Any) -> Mapping[str, Any]:
def getmro(obj: Any) -> Tuple["Type", ...]: def getmro(obj: Any) -> Tuple["Type", ...]:
"""Get __mro__ from given *obj* safely. """Get __mro__ from given *obj* safely."""
Raises AttributeError if given *obj* raises an error on accessing __mro__.
"""
__mro__ = safe_getattr(obj, '__mro__', None) __mro__ = safe_getattr(obj, '__mro__', None)
if isinstance(__mro__, tuple): if isinstance(__mro__, tuple):
return __mro__ return __mro__
@ -186,7 +179,6 @@ def getslots(obj: Any) -> Optional[Dict]:
"""Get __slots__ attribute of the class as dict. """Get __slots__ attribute of the class as dict.
Return None if gienv *obj* does not have __slots__. Return None if gienv *obj* does not have __slots__.
Raises AttributeError if given *obj* raises an error on accessing __slots__.
Raises TypeError if given *obj* is not a class. Raises TypeError if given *obj* is not a class.
Raises ValueError if given *obj* have invalid __slots__. Raises ValueError if given *obj* have invalid __slots__.
""" """
@ -464,12 +456,7 @@ def is_builtin_class_method(obj: Any, attr_name: str) -> bool:
but PyPy implements it by pure Python code. but PyPy implements it by pure Python code.
""" """
try: try:
mro = inspect.getmro(obj) mro = getmro(obj)
except AttributeError:
# no __mro__, assume the object has no methods as we know them
return False
try:
cls = next(c for c in mro if attr_name in safe_getattr(c, '__dict__', {})) cls = next(c for c in mro if attr_name in safe_getattr(c, '__dict__', {}))
except StopIteration: except StopIteration:
return False return False

View File

@ -30,6 +30,11 @@ else:
ref = _ForwardRef(self.arg) ref = _ForwardRef(self.arg)
return ref._eval_type(globalns, localns) return ref._eval_type(globalns, localns)
if sys.version_info > (3, 10):
from types import Union as types_Union
else:
types_Union = None
if False: if False:
# For type annotation # For type annotation
from typing import Type # NOQA # for python3.5.1 from typing import Type # NOQA # for python3.5.1
@ -72,7 +77,9 @@ def get_type_hints(obj: Any, globalns: Dict = None, localns: Dict = None) -> Dic
# Failed to evaluate ForwardRef (maybe TYPE_CHECKING) # Failed to evaluate ForwardRef (maybe TYPE_CHECKING)
return safe_getattr(obj, '__annotations__', {}) return safe_getattr(obj, '__annotations__', {})
except TypeError: except TypeError:
return {} # Invalid object is given. But try to get __annotations__ as a fallback for
# the code using type union operator (PEP 604) in python 3.9 or below.
return safe_getattr(obj, '__annotations__', {})
except KeyError: except KeyError:
# a broken class found (refs: https://github.com/sphinx-doc/sphinx/issues/8084) # a broken class found (refs: https://github.com/sphinx-doc/sphinx/issues/8084)
return {} return {}
@ -97,6 +104,12 @@ def restify(cls: Optional["Type"]) -> str:
return ':class:`struct.Struct`' return ':class:`struct.Struct`'
elif inspect.isNewType(cls): elif inspect.isNewType(cls):
return ':class:`%s`' % cls.__name__ return ':class:`%s`' % cls.__name__
elif types_Union and isinstance(cls, types_Union):
if len(cls.__args__) > 1 and None in cls.__args__:
args = ' | '.join(restify(a) for a in cls.__args__ if a)
return 'Optional[%s]' % args
else:
return ' | '.join(restify(a) for a in cls.__args__)
elif cls.__module__ in ('__builtin__', 'builtins'): elif cls.__module__ in ('__builtin__', 'builtins'):
return ':class:`%s`' % cls.__name__ return ':class:`%s`' % cls.__name__
else: else:
@ -290,6 +303,8 @@ def _stringify_py37(annotation: Any) -> str:
elif hasattr(annotation, '__origin__'): elif hasattr(annotation, '__origin__'):
# instantiated generic provided by a user # instantiated generic provided by a user
qualname = stringify(annotation.__origin__) qualname = stringify(annotation.__origin__)
elif types_Union and isinstance(annotation, types_Union): # types.Union (for py3.10+)
qualname = 'types.Union'
else: else:
# we weren't able to extract the base type, appending arguments would # we weren't able to extract the base type, appending arguments would
# only make them appear twice # only make them appear twice
@ -309,6 +324,12 @@ def _stringify_py37(annotation: Any) -> str:
else: else:
args = ', '.join(stringify(a) for a in annotation.__args__) args = ', '.join(stringify(a) for a in annotation.__args__)
return 'Union[%s]' % args return 'Union[%s]' % args
elif qualname == 'types.Union':
if len(annotation.__args__) > 1 and None in annotation.__args__:
args = ' | '.join(stringify(a) for a in annotation.__args__ if a)
return 'Optional[%s]' % args
else:
return ' | '.join(stringify(a) for a in annotation.__args__)
elif qualname == 'Callable': elif qualname == 'Callable':
args = ', '.join(stringify(a) for a in annotation.__args__[:-1]) args = ', '.join(stringify(a) for a in annotation.__args__[:-1])
returns = stringify(annotation.__args__[-1]) returns = stringify(annotation.__args__[-1])

View File

@ -116,8 +116,10 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator):
def visit_desc_signature(self, node: Element) -> None: def visit_desc_signature(self, node: Element) -> None:
# the id is set automatically # the id is set automatically
self.body.append(self.starttag(node, 'dt')) self.body.append(self.starttag(node, 'dt'))
self.protect_literal_text += 1
def depart_desc_signature(self, node: Element) -> None: def depart_desc_signature(self, node: Element) -> None:
self.protect_literal_text -= 1
if not node.get('is_multiline'): if not node.get('is_multiline'):
self.add_permalink_ref(node, _('Permalink to this definition')) self.add_permalink_ref(node, _('Permalink to this definition'))
self.body.append('</dt>\n') self.body.append('</dt>\n')

View File

@ -87,8 +87,10 @@ class HTML5Translator(SphinxTranslator, BaseTranslator):
def visit_desc_signature(self, node: Element) -> None: def visit_desc_signature(self, node: Element) -> None:
# the id is set automatically # the id is set automatically
self.body.append(self.starttag(node, 'dt')) self.body.append(self.starttag(node, 'dt'))
self.protect_literal_text += 1
def depart_desc_signature(self, node: Element) -> None: def depart_desc_signature(self, node: Element) -> None:
self.protect_literal_text -= 1
if not node.get('is_multiline'): if not node.get('is_multiline'):
self.add_permalink_ref(node, _('Permalink to this definition')) self.add_permalink_ref(node, _('Permalink to this definition'))
self.body.append('</dt>\n') self.body.append('</dt>\n')

View File

@ -702,12 +702,18 @@ class LaTeXTranslator(SphinxTranslator):
self.body.append(self.context.pop()) self.body.append(self.context.pop())
def visit_desc(self, node: Element) -> None: def visit_desc(self, node: Element) -> None:
self.body.append('\n\n\\begin{fulllineitems}\n') if self.config.latex_show_urls == 'footnote':
self.body.append('\n\n\\begin{savenotes}\\begin{fulllineitems}\n')
else:
self.body.append('\n\n\\begin{fulllineitems}\n')
if self.table: if self.table:
self.table.has_problematic = True self.table.has_problematic = True
def depart_desc(self, node: Element) -> None: def depart_desc(self, node: Element) -> None:
self.body.append('\n\\end{fulllineitems}\n\n') if self.config.latex_show_urls == 'footnote':
self.body.append('\n\\end{fulllineitems}\\end{savenotes}\n\n')
else:
self.body.append('\n\\end{fulllineitems}\n\n')
def _visit_signature_line(self, node: Element) -> None: def _visit_signature_line(self, node: Element) -> None:
for child in node: for child in node:

View File

@ -0,0 +1,16 @@
from __future__ import annotations
attr: int | str #: docstring
def sum(x: int | str, y: int | str) -> int | str:
"""docstring"""
class Foo:
"""docstring"""
attr: int | str #: docstring
def meth(self, x: int | str, y: int | str) -> int | str:
"""docstring"""

View File

@ -0,0 +1,8 @@
class Base:
attr1: int #: docstring
attr2: str
class Derived(Base):
attr3: int #: docstring
attr4: str

View File

@ -169,3 +169,9 @@ Footnote in term [#]_
def bar(x,y): def bar(x,y):
return x+y return x+y
The section with an object description
======================================
.. py:function:: dummy(N)
:noindex:

View File

@ -178,8 +178,8 @@ def test_html4_output(app, status, warning):
], ],
'autodoc.html': [ 'autodoc.html': [
(".//dl[@class='py class']/dt[@id='autodoc_target.Class']", ''), (".//dl[@class='py class']/dt[@id='autodoc_target.Class']", ''),
(".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span", r'\*\*'), (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'\*\*'),
(".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span", r'kwds'), (".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span/span", r'kwds'),
(".//dd/p", r'Return spam\.'), (".//dd/p", r'Return spam\.'),
], ],
'extapi.html': [ 'extapi.html': [
@ -279,8 +279,10 @@ def test_html4_output(app, status, warning):
'objects.html': [ 'objects.html': [
(".//dt[@id='mod.Cls.meth1']", ''), (".//dt[@id='mod.Cls.meth1']", ''),
(".//dt[@id='errmod.Error']", ''), (".//dt[@id='errmod.Error']", ''),
(".//dt/code", r'long\(parameter,\s* list\)'), (".//dt/code/span", r'long\(parameter,'),
(".//dt/code", 'another one'), (".//dt/code/span", r'list\)'),
(".//dt/code/span", 'another'),
(".//dt/code/span", 'one'),
(".//a[@href='#mod.Cls'][@class='reference internal']", ''), (".//a[@href='#mod.Cls'][@class='reference internal']", ''),
(".//dl[@class='std userdesc']", ''), (".//dl[@class='std userdesc']", ''),
(".//dt[@id='userdesc-myobj']", ''), (".//dt[@id='userdesc-myobj']", ''),

View File

@ -828,6 +828,7 @@ def test_latex_show_urls_is_inline(app, status, warning):
assert '\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result assert '\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result
assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}' assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}'
'{sphinx\\sphinxhyphen{}dev@googlegroups.com}') in result '{sphinx\\sphinxhyphen{}dev@googlegroups.com}') in result
assert '\\begin{savenotes}\\begin{fulllineitems}' not in result
@pytest.mark.sphinx( @pytest.mark.sphinx(
@ -882,6 +883,7 @@ def test_latex_show_urls_is_footnote(app, status, warning):
assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result) assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result)
assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}' assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}'
'{sphinx\\sphinxhyphen{}dev@googlegroups.com}\n') in result '{sphinx\\sphinxhyphen{}dev@googlegroups.com}\n') in result
assert '\\begin{savenotes}\\begin{fulllineitems}' in result
@pytest.mark.sphinx( @pytest.mark.sphinx(
@ -925,6 +927,7 @@ def test_latex_show_urls_is_no(app, status, warning):
assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result) assert ('\\sphinxurl{https://github.com/sphinx-doc/sphinx}\n' in result)
assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}' assert ('\\sphinxhref{mailto:sphinx-dev@googlegroups.com}'
'{sphinx\\sphinxhyphen{}dev@googlegroups.com}\n') in result '{sphinx\\sphinxhyphen{}dev@googlegroups.com}\n') in result
assert '\\begin{savenotes}\\begin{fulllineitems}' not in result
@pytest.mark.sphinx( @pytest.mark.sphinx(

View File

@ -431,6 +431,20 @@ def test_pyfunction_with_number_literals(app):
[nodes.inline, "1_6_0"])])]) [nodes.inline, "1_6_0"])])])
def test_pyfunction_with_union_type_operator(app):
text = ".. py:function:: hello(age: int | None)"
doctree = restructuredtext.parse(app, text)
assert_node(doctree[1][0][1],
[desc_parameterlist, ([desc_parameter, ([desc_sig_name, "age"],
[desc_sig_punctuation, ":"],
" ",
[desc_sig_name, ([pending_xref, "int"],
" ",
[desc_sig_punctuation, "|"],
" ",
[pending_xref, "None"])])])])
def test_optional_pyfunction_signature(app): def test_optional_pyfunction_signature(app):
text = ".. py:function:: compile(source [, filename [, symbol]]) -> ast object" text = ".. py:function:: compile(source [, filename [, symbol]]) -> ast object"
doctree = restructuredtext.parse(app, text) doctree = restructuredtext.parse(app, text)
@ -498,6 +512,20 @@ def test_pydata_signature_old(app):
domain="py", objtype="data", noindex=False) domain="py", objtype="data", noindex=False)
def test_pydata_with_union_type_operator(app):
text = (".. py:data:: version\n"
" :type: int | str")
doctree = restructuredtext.parse(app, text)
assert_node(doctree[1][0],
([desc_name, "version"],
[desc_annotation, (": ",
[pending_xref, "int"],
" ",
[desc_sig_punctuation, "|"],
" ",
[pending_xref, "str"])]))
def test_pyobject_prefix(app): def test_pyobject_prefix(app):
text = (".. py:class:: Foo\n" text = (".. py:class:: Foo\n"
"\n" "\n"

View File

@ -2226,6 +2226,50 @@ def test_name_mangling(app):
] ]
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_type_union_operator(app):
options = {'members': None}
actual = do_autodoc(app, 'module', 'target.pep604', options)
assert list(actual) == [
'',
'.. py:module:: target.pep604',
'',
'',
'.. py:class:: Foo()',
' :module: target.pep604',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr',
' :module: target.pep604',
' :type: int | str',
'',
' docstring',
'',
'',
' .. py:method:: Foo.meth(x: int | str, y: int | str) -> int | str',
' :module: target.pep604',
'',
' docstring',
'',
'',
'.. py:data:: attr',
' :module: target.pep604',
' :type: int | str',
'',
' docstring',
'',
'',
'.. py:function:: sum(x: int | str, y: int | str) -> int | str',
' :module: target.pep604',
'',
' docstring',
'',
]
@pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.') @pytest.mark.skipif(sys.version_info < (3, 6), reason='python 3.6+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc') @pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_hide_value(app): def test_hide_value(app):

View File

@ -106,6 +106,73 @@ def test_inherited_instance_variable(app):
] ]
@pytest.mark.skipif(sys.version_info < (3, 6), reason='py36+ is available since python3.6.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_uninitialized_attributes(app):
options = {"members": None,
"inherited-members": True}
actual = do_autodoc(app, 'class', 'target.uninitialized_attributes.Derived', options)
assert list(actual) == [
'',
'.. py:class:: Derived()',
' :module: target.uninitialized_attributes',
'',
'',
' .. py:attribute:: Derived.attr1',
' :module: target.uninitialized_attributes',
' :type: int',
'',
' docstring',
'',
'',
' .. py:attribute:: Derived.attr3',
' :module: target.uninitialized_attributes',
' :type: int',
'',
' docstring',
'',
]
@pytest.mark.skipif(sys.version_info < (3, 6), reason='py36+ is available since python3.6.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_undocumented_uninitialized_attributes(app):
options = {"members": None,
"inherited-members": True,
"undoc-members": True}
actual = do_autodoc(app, 'class', 'target.uninitialized_attributes.Derived', options)
assert list(actual) == [
'',
'.. py:class:: Derived()',
' :module: target.uninitialized_attributes',
'',
'',
' .. py:attribute:: Derived.attr1',
' :module: target.uninitialized_attributes',
' :type: int',
'',
' docstring',
'',
'',
' .. py:attribute:: Derived.attr2',
' :module: target.uninitialized_attributes',
' :type: str',
'',
'',
' .. py:attribute:: Derived.attr3',
' :module: target.uninitialized_attributes',
' :type: int',
'',
' docstring',
'',
'',
' .. py:attribute:: Derived.attr4',
' :module: target.uninitialized_attributes',
' :type: str',
'',
]
def test_decorators(app): def test_decorators(app):
actual = do_autodoc(app, 'class', 'target.decorator.Baz') actual = do_autodoc(app, 'class', 'target.decorator.Baz')
assert list(actual) == [ assert list(actual) == [

View File

@ -250,10 +250,10 @@ def test_missing_reference_cppdomain(tempdir, app, status, warning):
'<span class="pre">Bar</span></code></a>' in html) '<span class="pre">Bar</span></code></a>' in html)
assert ('<a class="reference external"' assert ('<a class="reference external"'
' href="https://docs.python.org/index.html#foons"' ' href="https://docs.python.org/index.html#foons"'
' title="(in foo v2.0)">foons</a>' in html) ' title="(in foo v2.0)"><span class="pre">foons</span></a>' in html)
assert ('<a class="reference external"' assert ('<a class="reference external"'
' href="https://docs.python.org/index.html#foons_bartype"' ' href="https://docs.python.org/index.html#foons_bartype"'
' title="(in foo v2.0)">bartype</a>' in html) ' title="(in foo v2.0)"><span class="pre">bartype</span></a>' in html)
def test_missing_reference_jsdomain(tempdir, app, status, warning): def test_missing_reference_jsdomain(tempdir, app, status, warning):

View File

@ -117,6 +117,13 @@ def test_restify_type_ForwardRef():
assert restify(ForwardRef("myint")) == ":class:`myint`" assert restify(ForwardRef("myint")) == ":class:`myint`"
@pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.')
def test_restify_type_union_operator():
assert restify(int | None) == "Optional[:class:`int`]" # type: ignore
assert restify(int | str) == ":class:`int` | :class:`str`" # type: ignore
assert restify(int | str | None) == "Optional[:class:`int` | :class:`str`]" # type: ignore
def test_restify_broken_type_hints(): def test_restify_broken_type_hints():
assert restify(BrokenType) == ':class:`tests.test_util_typing.BrokenType`' assert restify(BrokenType) == ':class:`tests.test_util_typing.BrokenType`'
@ -206,5 +213,12 @@ def test_stringify_type_hints_alias():
assert stringify(MyTuple) == "Tuple[str, str]" # type: ignore assert stringify(MyTuple) == "Tuple[str, str]" # type: ignore
@pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.')
def test_stringify_type_union_operator():
assert stringify(int | None) == "Optional[int]" # type: ignore
assert stringify(int | str) == "int | str" # type: ignore
assert stringify(int | str | None) == "Optional[int | str]" # type: ignore
def test_stringify_broken_type_hints(): def test_stringify_broken_type_hints():
assert stringify(BrokenType) == 'tests.test_util_typing.BrokenType' assert stringify(BrokenType) == 'tests.test_util_typing.BrokenType'