Merge branch '3.x'

This commit is contained in:
Takeshi KOMIYA 2020-04-28 01:42:37 +09:00
commit aa773cbc88
16 changed files with 281 additions and 117 deletions

24
CHANGES
View File

@ -70,11 +70,15 @@ Features added
* #2044: autodoc: Suppress default value for instance attributes
* #7473: autodoc: consider a member public if docstring contains
``:meta public:`` in info-field-list
* #7487: autodoc: Allow to generate docs for singledispatch functions by
py:autofunction
* #7466: autosummary: headings in generated documents are not translated
* #7490: autosummary: Add ``:caption:`` option to autosummary directive to set a
caption to the toctree
* #248, #6040: autosummary: Add ``:recursive:`` option to autosummary directive
to generate stub files recursively
* #4030: autosummary: Add :confval:`autosummary_context` to add template
variables for custom templates
* #7535: sphinx-autogen: crashes when custom template uses inheritance
* #7536: sphinx-autogen: crashes when template uses i18n feature
* #7481: html theme: Add right margin to footnote/citation labels
@ -88,6 +92,7 @@ Features added
* #7533: html theme: Avoid whitespace at the beginning of genindex.html
* #7541: html theme: Add a "clearer" at the end of the "body"
* #7542: html theme: Make admonition/topic/sidebar scrollable
* #7543: html theme: Add top and bottom margins to tables
* C and C++: allow semicolon in the end of declarations.
* C++, parse parameterized noexcept specifiers.
@ -95,11 +100,12 @@ Bugs fixed
----------
* #6703: autodoc: incremental build does not work for imported objects
* #7564: autodoc: annotations not to be shown for descriptors
Testing
--------
Release 3.0.3 (in development)
Release 3.0.4 (in development)
==============================
Dependencies
@ -117,9 +123,25 @@ Features added
Bugs fixed
----------
* #7567: autodoc: parametrized types are shown twice for generic types
Testing
--------
Release 3.0.3 (released Apr 26, 2020)
=====================================
Features added
--------------
* C, parse array declarators with static, qualifiers, and VLA specification.
Bugs fixed
----------
* #7516: autodoc: crashes if target object raises an error on accessing
its attributes
Release 3.0.2 (released Apr 19, 2020)
=====================================

View File

@ -151,6 +151,13 @@ Generating stub pages automatically
If you do not want to create stub pages with :program:`sphinx-autogen`, you can
also use these config values:
.. confval:: autosummary_context
A dictionary of values to pass into the template engine's context for
autosummary stubs files.
.. versionadded:: 3.1
.. confval:: autosummary_generate
Boolean indicating whether to scan all found documents for autosummary

View File

@ -792,20 +792,60 @@ class ASTDeclSpecs(ASTBase):
################################################################################
class ASTArray(ASTBase):
def __init__(self, size: ASTExpression):
def __init__(self, static: bool, const: bool, volatile: bool, restrict: bool,
vla: bool, size: ASTExpression):
self.static = static
self.const = const
self.volatile = volatile
self.restrict = restrict
self.vla = vla
self.size = size
if vla:
assert size is None
if size is not None:
assert not vla
def _stringify(self, transform: StringifyTransform) -> str:
if self.size:
return '[' + transform(self.size) + ']'
else:
return '[]'
el = []
if self.static:
el.append('static')
if self.restrict:
el.append('restrict')
if self.volatile:
el.append('volatile')
if self.const:
el.append('const')
if self.vla:
return '[' + ' '.join(el) + '*]'
elif self.size:
el.append(transform(self.size))
return '[' + ' '.join(el) + ']'
def describe_signature(self, signode: TextElement, mode: str,
env: "BuildEnvironment", symbol: "Symbol") -> None:
verify_description_mode(mode)
signode.append(nodes.Text("["))
if self.size:
addSpace = False
def _add(signode: TextElement, text: str) -> bool:
if addSpace:
signode += nodes.Text(' ')
signode += addnodes.desc_annotation(text, text)
return True
if self.static:
addSpace = _add(signode, 'static')
if self.restrict:
addSpace = _add(signode, 'restrict')
if self.volatile:
addSpace = _add(signode, 'volatile')
if self.const:
addSpace = _add(signode, 'const')
if self.vla:
signode.append(nodes.Text('*'))
elif self.size:
if addSpace:
signode += nodes.Text(' ')
self.size.describe_signature(signode, mode, env, symbol)
signode.append(nodes.Text("]"))
@ -2595,18 +2635,45 @@ class DefinitionParser(BaseParser):
self.skip_ws()
if typed and self.skip_string('['):
self.skip_ws()
if self.skip_string(']'):
arrayOps.append(ASTArray(None))
continue
static = False
const = False
volatile = False
restrict = False
while True:
if not static:
if self.skip_word_and_ws('static'):
static = True
continue
if not const:
if self.skip_word_and_ws('const'):
const = True
continue
if not volatile:
if self.skip_word_and_ws('volatile'):
volatile = True
continue
if not restrict:
if self.skip_word_and_ws('restrict'):
restrict = True
continue
break
vla = False if static else self.skip_string_and_ws('*')
if vla:
if not self.skip_string(']'):
self.fail("Expected ']' in end of array operator.")
size = None
else:
if self.skip_string(']'):
size = None
else:
def parser() -> ASTExpression:
return self._parse_expression()
value = self._parse_expression_fallback([']'], parser)
if not self.skip_string(']'):
self.fail("Expected ']' in end of array operator.")
arrayOps.append(ASTArray(value))
continue
def parser():
return self._parse_expression()
size = self._parse_expression_fallback([']'], parser)
self.skip_ws()
if not self.skip_string(']'):
self.fail("Expected ']' in end of array operator.")
arrayOps.append(ASTArray(static, const, volatile, restrict, vla, size))
else:
break
param = self._parse_parameters(paramMode)

View File

@ -581,9 +581,9 @@ class Documenter:
isprivate = membername.startswith('_')
keep = False
if getattr(member, '__sphinx_mock__', False):
if safe_getattr(member, '__sphinx_mock__', False):
# mocked module or object
keep = False
pass
elif want_all and membername.startswith('__') and \
membername.endswith('__') and len(membername) > 4:
# special __methods__
@ -1077,30 +1077,15 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
def add_directive_header(self, sig: str) -> None:
sourcename = self.get_sourcename()
super().add_directive_header(sig)
if inspect.is_singledispatch_function(self.object):
self.add_singledispatch_directive_header(sig)
else:
super().add_directive_header(sig)
if inspect.iscoroutinefunction(self.object):
self.add_line(' :async:', sourcename)
class SingledispatchFunctionDocumenter(FunctionDocumenter):
"""
Specialized Documenter subclass for singledispatch'ed functions.
"""
objtype = 'singledispatch_function'
directivetype = 'function'
member_order = 30
# before FunctionDocumenter
priority = FunctionDocumenter.priority + 1
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
return (super().can_document_member(member, membername, isattr, parent) and
inspect.is_singledispatch_function(member))
def add_directive_header(self, sig: str) -> None:
def add_singledispatch_directive_header(self, sig: str) -> None:
sourcename = self.get_sourcename()
# intercept generated directive headers
@ -1140,6 +1125,14 @@ class SingledispatchFunctionDocumenter(FunctionDocumenter):
func.__signature__ = sig.replace(parameters=params) # type: ignore
class SingledispatchFunctionDocumenter(FunctionDocumenter):
"""
Used to be a specialized Documenter subclass for singledispatch'ed functions.
Retained for backwards compatibility, now does the same as the FunctionDocumenter
"""
class DecoratorDocumenter(FunctionDocumenter):
"""
Specialized Documenter subclass for decorator functions.
@ -1474,7 +1467,11 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
return args
def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
meth = self.parent.__dict__.get(self.objpath[-1])
if inspect.is_singledispatch_method(meth):
self.add_singledispatch_directive_header(sig)
else:
super().add_directive_header(sig)
sourcename = self.get_sourcename()
obj = self.parent.__dict__.get(self.object_name, self.object)
@ -1490,28 +1487,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
def document_members(self, all_members: bool = False) -> None:
pass
class SingledispatchMethodDocumenter(MethodDocumenter):
"""
Specialized Documenter subclass for singledispatch'ed methods.
"""
objtype = 'singledispatch_method'
directivetype = 'method'
member_order = 50
# before MethodDocumenter
priority = MethodDocumenter.priority + 1
@classmethod
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
) -> bool:
if super().can_document_member(member, membername, isattr, parent) and parent.object:
meth = parent.object.__dict__.get(membername)
return inspect.is_singledispatch_method(meth)
else:
return False
def add_directive_header(self, sig: str) -> None:
def add_singledispatch_directive_header(self, sig: str) -> None:
sourcename = self.get_sourcename()
# intercept generated directive headers
@ -1552,6 +1528,14 @@ class SingledispatchMethodDocumenter(MethodDocumenter):
func.__signature__ = sig.replace(parameters=params) # type: ignore
class SingledispatchMethodDocumenter(MethodDocumenter):
"""
Used to be a specialized Documenter subclass for singledispatch'ed methods.
Retained for backwards compatibility, now does the same as the MethodDocumenter
"""
class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # type: ignore
"""
Specialized Documenter subclass for attributes.
@ -1603,18 +1587,19 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
super().add_directive_header(sig)
sourcename = self.get_sourcename()
if not self.options.annotation:
if not self._datadescriptor:
# obtain annotation for this attribute
annotations = getattr(self.parent, '__annotations__', {})
if annotations and self.objpath[-1] in annotations:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
else:
key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
if self.analyzer and key in self.analyzer.annotations:
self.add_line(' :type: ' + self.analyzer.annotations[key],
sourcename)
# obtain type annotation for this attribute
annotations = getattr(self.parent, '__annotations__', {})
if annotations and self.objpath[-1] in annotations:
objrepr = stringify_typehint(annotations.get(self.objpath[-1]))
self.add_line(' :type: ' + objrepr, sourcename)
else:
key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
if self.analyzer and key in self.analyzer.annotations:
self.add_line(' :type: ' + self.analyzer.annotations[key],
sourcename)
# data descriptors do not have useful values
if not self._datadescriptor:
try:
if self.object is INSTANCEATTR:
pass
@ -1769,10 +1754,8 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_autodocumenter(DataDocumenter)
app.add_autodocumenter(DataDeclarationDocumenter)
app.add_autodocumenter(FunctionDocumenter)
app.add_autodocumenter(SingledispatchFunctionDocumenter)
app.add_autodocumenter(DecoratorDocumenter)
app.add_autodocumenter(MethodDocumenter)
app.add_autodocumenter(SingledispatchMethodDocumenter)
app.add_autodocumenter(AttributeDocumenter)
app.add_autodocumenter(PropertyDocumenter)
app.add_autodocumenter(InstanceAttributeDocumenter)

View File

@ -771,6 +771,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_directive('autosummary', Autosummary)
app.add_role('autolink', AutoLink())
app.connect('builder-inited', process_generate_options)
app.add_config_value('autosummary_context', {}, True)
app.add_config_value('autosummary_generate', [], True, [bool])
app.add_config_value('autosummary_generate_overwrite', True, False)
app.add_config_value('autosummary_mock_imports',

View File

@ -66,6 +66,7 @@ class DummyApplication:
self._warncount = 0
self.warningiserror = False
self.config.add('autosummary_context', {}, True, None)
self.config.init_values()
def emit_firstresult(self, *args: Any) -> None:
@ -175,7 +176,7 @@ class AutosummaryRenderer:
def generate_autosummary_content(name: str, obj: Any, parent: Any,
template: AutosummaryRenderer, template_name: str,
imported_members: bool, app: Any,
recursive: bool) -> str:
recursive: bool, context: Dict) -> str:
doc = get_documenter(app, obj, parent)
def skip_member(obj: Any, name: str, objtype: str) -> bool:
@ -224,6 +225,7 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
return public, items
ns = {} # type: Dict[str, Any]
ns.update(context)
if doc.objtype == 'module':
ns['members'] = dir(obj)
@ -329,8 +331,12 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None,
_warn(__('[autosummary] failed to import %r: %s') % (entry.name, e))
continue
context = {}
if app:
context.update(app.config.autosummary_context)
content = generate_autosummary_content(name, obj, parent, template, entry.template,
imported_members, app, entry.recursive)
imported_members, app, entry.recursive, context)
filename = os.path.join(path, name + suffix)
if os.path.isfile(filename):

View File

@ -14,7 +14,7 @@ import sys
import tokenize
from token import NAME, NEWLINE, INDENT, DEDENT, NUMBER, OP, STRING
from tokenize import COMMENT, NL
from typing import Any, Dict, List, Tuple
from typing import Any, Dict, List, Optional, Tuple
from sphinx.pycode.ast import ast # for py37 or older
from sphinx.pycode.ast import parse, unparse
@ -233,41 +233,33 @@ class VariableCommentPicker(ast.NodeVisitor):
self.deforders = {} # type: Dict[str, int]
super().__init__()
def add_entry(self, name: str) -> None:
def get_qualname_for(self, name: str) -> Optional[List[str]]:
"""Get qualified name for given object as a list of string."""
if self.current_function:
if self.current_classes and self.context[-1] == "__init__":
# store variable comments inside __init__ method of classes
definition = self.context[:-1] + [name]
return self.context[:-1] + [name]
else:
return
return None
else:
definition = self.context + [name]
return self.context + [name]
self.deforders[".".join(definition)] = next(self.counter)
def add_entry(self, name: str) -> None:
qualname = self.get_qualname_for(name)
if qualname:
self.deforders[".".join(qualname)] = next(self.counter)
def add_variable_comment(self, name: str, comment: str) -> None:
if self.current_function:
if self.current_classes and self.context[-1] == "__init__":
# store variable comments inside __init__ method of classes
context = ".".join(self.context[:-1])
else:
return
else:
context = ".".join(self.context)
self.comments[(context, name)] = comment
qualname = self.get_qualname_for(name)
if qualname:
basename = ".".join(qualname[:-1])
self.comments[(basename, name)] = comment
def add_variable_annotation(self, name: str, annotation: ast.AST) -> None:
if self.current_function:
if self.current_classes and self.context[-1] == "__init__":
# store variable comments inside __init__ method of classes
context = ".".join(self.context[:-1])
else:
return
else:
context = ".".join(self.context)
self.annotations[(context, name)] = unparse(annotation)
qualname = self.get_qualname_for(name)
if qualname:
basename = ".".join(qualname[:-1])
self.annotations[(basename, name)] = unparse(annotation)
def get_self(self) -> ast.arg:
"""Returns the name of first argument if in function."""
@ -288,18 +280,12 @@ class VariableCommentPicker(ast.NodeVisitor):
def visit_Import(self, node: ast.Import) -> None:
"""Handles Import node and record it to definition orders."""
for name in node.names:
if name.asname:
self.add_entry(name.asname)
else:
self.add_entry(name.name)
self.add_entry(name.asname or name.name)
def visit_ImportFrom(self, node: ast.Import) -> None:
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
"""Handles Import node and record it to definition orders."""
for name in node.names:
if name.asname:
self.add_entry(name.asname)
else:
self.add_entry(name.name)
self.add_entry(name.asname or name.name)
def visit_Assign(self, node: ast.Assign) -> None:
"""Handles Assign node and pick up a variable comment."""

View File

@ -377,6 +377,8 @@ div.body p.centered {
/* -- tables ---------------------------------------------------------------- */
table.docutils {
margin-top: 10px;
margin-bottom: 10px;
border: 0;
border-collapse: collapse;
}

View File

@ -75,8 +75,13 @@ def _stringify_py37(annotation: Any) -> str:
qualname = stringify(annotation.__origin__) # ex. Union
elif hasattr(annotation, '__qualname__'):
qualname = '%s.%s' % (module, annotation.__qualname__)
elif hasattr(annotation, '__origin__'):
# instantiated generic provided by a user
qualname = stringify(annotation.__origin__)
else:
qualname = repr(annotation)
# we weren't able to extract the base type, appending arguments would
# only make them appear twice
return repr(annotation)
if getattr(annotation, '__args__', None):
if qualname == 'Union':
@ -91,7 +96,7 @@ def _stringify_py37(annotation: Any) -> str:
return '%s[[%s], %s]' % (qualname, args, returns)
elif str(annotation).startswith('typing.Annotated'): # for py39+
return stringify(annotation.__args__[0])
elif annotation._special:
elif getattr(annotation, '_special', False):
return qualname
else:
args = ', '.join(stringify(a) for a in annotation.__args__)

View File

@ -6,11 +6,20 @@ attr2: str
attr3 = '' # type: str
class _Descriptor:
def __init__(self, name):
self.__doc__ = "This is {}".format(name)
def __get__(self):
pass
class Class:
attr1: int = 0
attr2: int
attr3 = 0 # type: int
descr4: int = _Descriptor("descr4")
def __init__(self):
self.attr4: int = 0 #: attr4
self.attr5: int #: attr5

View File

@ -3,6 +3,7 @@
{% block methods %}
.. note:: autosummary/class.rst method block overloading
{{ sentence }}
{{ super() }}
{% endblock %}

View File

@ -1511,6 +1511,13 @@ def test_autodoc_typed_instance_variables(app):
' attr6',
'',
'',
' .. py:attribute:: Class.descr4',
' :module: target.typed_vars',
' :type: int',
'',
' This is descr4',
'',
'',
'.. py:data:: attr1',
' :module: target.typed_vars',
' :type: str',
@ -1596,6 +1603,21 @@ def test_singledispatch():
]
@pytest.mark.usefixtures('setup_test')
def test_singledispatch_autofunction():
options = {}
actual = do_autodoc(app, 'function', 'target.singledispatch.func', options)
assert list(actual) == [
'',
'.. py:function:: func(arg, kwarg=None)',
' func(arg: int, kwarg=None)',
' func(arg: str, kwarg=None)',
' :module: target.singledispatch',
'',
' A function for general use.',
'',
]
@pytest.mark.skipif(sys.version_info < (3, 8),
reason='singledispatchmethod is available since python3.8')
@pytest.mark.usefixtures('setup_test')
@ -1623,6 +1645,23 @@ def test_singledispatchmethod():
]
@pytest.mark.skipif(sys.version_info < (3, 8),
reason='singledispatchmethod is available since python3.8')
@pytest.mark.usefixtures('setup_test')
def test_singledispatchmethod_automethod():
options = {}
actual = do_autodoc(app, 'method', 'target.singledispatchmethod.Foo.meth', options)
assert list(actual) == [
'',
'.. py:method:: Foo.meth(arg, kwarg=None)',
' Foo.meth(arg: int, kwarg=None)',
' Foo.meth(arg: str, kwarg=None)',
' :module: target.singledispatchmethod',
'',
' A method for general use.',
'',
]
@pytest.mark.usefixtures('setup_test')
@pytest.mark.skipif(pyximport is None, reason='cython is not installed')
def test_cython():

View File

@ -362,6 +362,21 @@ def test_function_definitions():
check('function', 'void f(enum E e)', {1: 'f'})
check('function', 'void f(union E e)', {1: 'f'})
# array declarators
check('function', 'void f(int arr[])', {1: 'f'})
check('function', 'void f(int arr[*])', {1: 'f'})
cvrs = ['', 'const', 'volatile', 'restrict', 'restrict volatile const']
for cvr in cvrs:
space = ' ' if len(cvr) != 0 else ''
check('function', 'void f(int arr[{}*])'.format(cvr), {1: 'f'})
check('function', 'void f(int arr[{}])'.format(cvr), {1: 'f'})
check('function', 'void f(int arr[{}{}42])'.format(cvr, space), {1: 'f'})
check('function', 'void f(int arr[static{}{} 42])'.format(space, cvr), {1: 'f'})
check('function', 'void f(int arr[{}{}static 42])'.format(cvr, space), {1: 'f'},
output='void f(int arr[static{}{} 42])'.format(space, cvr))
check('function', 'void f(int arr[const static volatile 42])', {1: 'f'},
output='void f(int arr[static volatile const 42])')
def test_union_definitions():
check('struct', 'A', {1: 'A'})

View File

@ -353,7 +353,8 @@ def test_autosummary_imported_members(app, status, warning):
sys.modules.pop('autosummary_dummy_package', None)
@pytest.mark.sphinx(testroot='ext-autodoc')
@pytest.mark.sphinx(testroot='ext-autodoc',
confoverrides={'extensions': ['sphinx.ext.autosummary']})
def test_generate_autosummary_docs_property(app):
with patch('sphinx.ext.autosummary.generate.find_autosummary_in_files') as mock:
mock.return_value = [AutosummaryEntry('target.methods.Base.prop', 'prop', None, False)]

View File

@ -33,3 +33,17 @@ def test_autosummary_class_template_overloading(make_app, app_params):
result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text()
assert 'autosummary/class.rst method block overloading' in result
assert 'foobar' not in result
@pytest.mark.sphinx('html', testroot='templating',
confoverrides={'autosummary_context': {'sentence': 'foobar'}})
def test_autosummary_context(make_app, app_params):
args, kwargs = app_params
app = make_app(*args, **kwargs)
setup_documenters(app)
app.builder.build_update()
result = (app.outdir / 'generated' / 'sphinx.application.TemplateBridge.html').read_text()
assert 'autosummary/class.rst method block overloading' in result
assert 'foobar' in result

View File

@ -10,7 +10,7 @@
import sys
from numbers import Integral
from typing import Any, Dict, List, TypeVar, Union, Callable, Tuple, Optional
from typing import Any, Dict, List, TypeVar, Union, Callable, Tuple, Optional, Generic
import pytest
@ -24,6 +24,11 @@ class MyClass1:
class MyClass2(MyClass1):
__qualname__ = '<MyClass2>'
T = TypeVar('T')
class MyList(List[T]):
pass
def test_stringify():
assert stringify(int) == "int"
@ -42,6 +47,7 @@ def test_stringify_type_hints_containers():
assert stringify(Tuple[str, str, str]) == "Tuple[str, str, str]"
assert stringify(Tuple[str, ...]) == "Tuple[str, ...]"
assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]"
assert stringify(MyList[Tuple[int, int]]) == "test_util_typing.MyList[Tuple[int, int]]"
@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.')