diff options
author | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2020-01-19 22:47:02 +0900 |
---|---|---|
committer | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2020-01-19 22:47:02 +0900 |
commit | 347e301727c3b2b08e277b0d8a72c33a1eba13d8 (patch) | |
tree | 8b0c32ac6d5bd84ce4a8746eff3ef54acec93830 | |
parent | ad271f4ca33d298a880da8fdc75cc318b4a7842f (diff) | |
parent | eb273fdc08840945b9c2419f20fb2e0220b0a004 (diff) | |
download | sphinx-git-347e301727c3b2b08e277b0d8a72c33a1eba13d8.tar.gz |
Merge branch '2.0'
-rw-r--r-- | CHANGES | 8 | ||||
-rw-r--r-- | doc/usage/extensions/autodoc.rst | 11 | ||||
-rw-r--r-- | setup.py | 2 | ||||
-rw-r--r-- | sphinx/ext/autodoc/__init__.py | 13 | ||||
-rw-r--r-- | sphinx/ext/autodoc/type_comment.py | 74 | ||||
-rw-r--r-- | sphinx/pycode/ast.py | 80 | ||||
-rw-r--r-- | sphinx/util/docutils.py | 41 | ||||
-rw-r--r-- | sphinx/util/inspect.py | 72 | ||||
-rw-r--r-- | tests/roots/test-ext-autodoc/target/partialfunction.py | 7 | ||||
-rw-r--r-- | tests/roots/test-ext-autodoc/target/partialmethod.py | 2 | ||||
-rw-r--r-- | tests/roots/test-ext-autodoc/target/pep570.py | 5 | ||||
-rw-r--r-- | tests/roots/test-ext-autodoc/target/typehints.py | 14 | ||||
-rw-r--r-- | tests/test_autodoc.py | 47 | ||||
-rw-r--r-- | tests/test_ext_autodoc_configs.py | 25 | ||||
-rw-r--r-- | tests/test_pycode_ast.py | 40 | ||||
-rw-r--r-- | tests/test_util_docutils.py | 35 | ||||
-rw-r--r-- | tests/test_util_inspect.py | 27 |
17 files changed, 456 insertions, 47 deletions
@@ -79,6 +79,13 @@ Features added * #6696: html: ``:scale:`` option of image/figure directive not working for SVG images (imagesize-1.2.0 or above is required) * #6994: imgconverter: Support illustrator file (.ai) to .png conversion +* autodoc: Support Positional-Only Argument separator (PEP-570 compliant) +* #2755: autodoc: Add new event: :event:`autodoc-before-process-signature` +* #2755: autodoc: Support type_comment style (ex. ``# type: (str) -> str``) + annotation (python3.8+ or `typed_ast <https://github.com/python/typed_ast>`_ + is required) +* SphinxTranslator now calls visitor/departure method for super node class if + visitor/departure method for original node class not found Bugs fixed ---------- @@ -89,6 +96,7 @@ Bugs fixed * #6559: Wrong node-ids are generated in glossary directive * #6986: apidoc: misdetects module name for .so file inside module * #6999: napoleon: fails to parse tilde in :exc: role +* #7023: autodoc: nested partial functions are not listed Testing -------- diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index 78852fe1e..6ca148706 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -526,6 +526,17 @@ autodoc provides the following additional events: auto directive :param lines: the lines of the docstring, see above +.. event:: autodoc-before-process-signature (app, obj, bound_method) + + .. versionadded:: 2.4 + + Emitted before autodoc formats a signature for an object. The event handler + can modify an object to change its signature. + + :param app: the Sphinx application object + :param obj: the object itself + :param bound_method: a boolean indicates an object is bound method or not + .. event:: autodoc-process-signature (app, what, name, obj, options, signature, return_annotation) .. versionadded:: 0.5 @@ -42,7 +42,7 @@ extras_require = { 'sphinxcontrib-websupport', ], 'test': [ - 'pytest', + 'pytest < 5.3.3', 'pytest-cov', 'html5lib', 'flake8>=3.5.0', diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 4af9bea93..58c9897a9 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1013,8 +1013,11 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ not inspect.isbuiltin(self.object) and not inspect.isclass(self.object) and hasattr(self.object, '__call__')): + self.env.app.emit('autodoc-before-process-signature', + self.object.__call__, False) sig = inspect.signature(self.object.__call__) else: + self.env.app.emit('autodoc-before-process-signature', self.object, False) sig = inspect.signature(self.object) args = stringify_signature(sig, **kwargs) except TypeError: @@ -1026,9 +1029,13 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ # typing) we try to use the constructor signature as function # signature without the first argument. try: + self.env.app.emit('autodoc-before-process-signature', + self.object.__new__, True) sig = inspect.signature(self.object.__new__, bound_method=True) args = stringify_signature(sig, show_return_annotation=False, **kwargs) except TypeError: + self.env.app.emit('autodoc-before-process-signature', + self.object.__init__, True) sig = inspect.signature(self.object.__init__, bound_method=True) args = stringify_signature(sig, show_return_annotation=False, **kwargs) @@ -1111,6 +1118,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: not(inspect.ismethod(initmeth) or inspect.isfunction(initmeth)): return None try: + self.env.app.emit('autodoc-before-process-signature', initmeth, True) sig = inspect.signature(initmeth, bound_method=True) return stringify_signature(sig, show_return_annotation=False, **kwargs) except TypeError: @@ -1314,8 +1322,10 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: # can never get arguments of a C function or method return None if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): + self.env.app.emit('autodoc-before-process-signature', self.object, False) sig = inspect.signature(self.object, bound_method=False) else: + self.env.app.emit('autodoc-before-process-signature', self.object, True) sig = inspect.signature(self.object, bound_method=True) args = stringify_signature(sig, **kwargs) @@ -1556,8 +1566,11 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('autodoc_typehints', "signature", True, ENUM("signature", "none")) app.add_config_value('autodoc_warningiserror', True, True) app.add_config_value('autodoc_inherit_docstrings', True, True) + app.add_event('autodoc-before-process-signature') app.add_event('autodoc-process-docstring') app.add_event('autodoc-process-signature') app.add_event('autodoc-skip-member') + app.setup_extension('sphinx.ext.autodoc.type_comment') + return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/autodoc/type_comment.py b/sphinx/ext/autodoc/type_comment.py new file mode 100644 index 000000000..c94020bf0 --- /dev/null +++ b/sphinx/ext/autodoc/type_comment.py @@ -0,0 +1,74 @@ +""" + sphinx.ext.autodoc.type_comment + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Update annotations info of living objects using type_comments. + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import ast +from inspect import getsource +from typing import Any, Dict +from typing import cast + +import sphinx +from sphinx.application import Sphinx +from sphinx.pycode.ast import parse as ast_parse +from sphinx.pycode.ast import unparse as ast_unparse +from sphinx.util import inspect +from sphinx.util import logging + +logger = logging.getLogger(__name__) + + +def get_type_comment(obj: Any) -> ast.FunctionDef: + """Get type_comment'ed FunctionDef object from living object. + + This tries to parse original code for living object and returns + AST node for given *obj*. It requires py38+ or typed_ast module. + """ + try: + source = getsource(obj) + if source.startswith((' ', r'\t')): + # subject is placed inside class or block. To read its docstring, + # this adds if-block before the declaration. + module = ast_parse('if True:\n' + source) + subject = cast(ast.FunctionDef, module.body[0].body[0]) # type: ignore + else: + module = ast_parse(source) + subject = cast(ast.FunctionDef, module.body[0]) # type: ignore + + if getattr(subject, "type_comment", None): + return ast_parse(subject.type_comment, mode='func_type') # type: ignore + else: + return None + except (OSError, TypeError): # failed to load source code + return None + except SyntaxError: # failed to parse type_comments + return None + + +def update_annotations_using_type_comments(app: Sphinx, obj: Any, bound_method: bool) -> None: + """Update annotations info of *obj* using type_comments.""" + try: + function = get_type_comment(obj) + if function and hasattr(function, 'argtypes'): + if function.argtypes != [ast.Ellipsis]: # type: ignore + sig = inspect.signature(obj, bound_method) + for i, param in enumerate(sig.parameters.values()): + if param.name not in obj.__annotations__: + annotation = ast_unparse(function.argtypes[i]) # type: ignore + obj.__annotations__[param.name] = annotation + + if 'return' not in obj.__annotations__: + obj.__annotations__['return'] = ast_unparse(function.returns) # type: ignore + except NotImplementedError as exc: # failed to ast.unparse() + logger.warning("Failed to parse type_comment for %r: %s", obj, exc) + + +def setup(app: Sphinx) -> Dict[str, Any]: + app.connect('autodoc-before-process-signature', update_annotations_using_type_comments) + + return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py new file mode 100644 index 000000000..155ae86d5 --- /dev/null +++ b/sphinx/pycode/ast.py @@ -0,0 +1,80 @@ +""" + sphinx.pycode.ast + ~~~~~~~~~~~~~~~~~ + + Helpers for AST (Abstract Syntax Tree). + + :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import sys + +if sys.version_info > (3, 8): + import ast +else: + try: + # use typed_ast module if installed + from typed_ast import ast3 as ast + except ImportError: + import ast # type: ignore + + +def parse(code: str, mode: str = 'exec') -> "ast.AST": + """Parse the *code* using built-in ast or typed_ast. + + This enables "type_comments" feature if possible. + """ + try: + # type_comments parameter is available on py38+ + return ast.parse(code, mode=mode, type_comments=True) # type: ignore + except TypeError: + # fallback to ast module. + # typed_ast is used to parse type_comments if installed. + return ast.parse(code, mode=mode) + + +def unparse(node: ast.AST) -> str: + """Unparse an AST to string.""" + if node is None: + return None + elif isinstance(node, ast.Attribute): + return "%s.%s" % (unparse(node.value), node.attr) + elif isinstance(node, ast.Bytes): + return repr(node.s) + elif isinstance(node, ast.Call): + args = ([unparse(e) for e in node.args] + + ["%s=%s" % (k.arg, unparse(k.value)) for k in node.keywords]) + return "%s(%s)" % (unparse(node.func), ", ".join(args)) + elif isinstance(node, ast.Dict): + keys = (unparse(k) for k in node.keys) + values = (unparse(v) for v in node.values) + items = (k + ": " + v for k, v in zip(keys, values)) + return "{" + ", ".join(items) + "}" + elif isinstance(node, ast.Ellipsis): + return "..." + elif isinstance(node, ast.Index): + return unparse(node.value) + elif isinstance(node, ast.Lambda): + return "<function <lambda>>" # TODO + elif isinstance(node, ast.List): + return "[" + ", ".join(unparse(e) for e in node.elts) + "]" + elif isinstance(node, ast.Name): + return node.id + elif isinstance(node, ast.NameConstant): + return repr(node.value) + elif isinstance(node, ast.Num): + return repr(node.n) + elif isinstance(node, ast.Set): + return "{" + ", ".join(unparse(e) for e in node.elts) + "}" + elif isinstance(node, ast.Str): + return repr(node.s) + elif isinstance(node, ast.Subscript): + return "%s[%s]" % (unparse(node.value), unparse(node.slice)) + elif isinstance(node, ast.Tuple): + return ", ".join(unparse(e) for e in node.elts) + elif sys.version_info > (3, 6) and isinstance(node, ast.Constant): + # this branch should be placed at last + return repr(node.value) + else: + raise NotImplementedError('Unable to parse %s object' % type(node).__name__) diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index 16b09bd7c..9add18ec0 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -429,7 +429,10 @@ class ReferenceRole(SphinxRole): class SphinxTranslator(nodes.NodeVisitor): """A base class for Sphinx translators. - This class provides helper methods for Sphinx translators. + This class adds a support for visitor/departure method for super node class + if visitor/departure method for node class is not found. + + It also provides helper methods for Sphinx translators. .. note:: The subclasses of this class might not work with docutils. This class is strongly coupled with Sphinx. @@ -441,6 +444,42 @@ class SphinxTranslator(nodes.NodeVisitor): self.config = builder.config self.settings = document.settings + def dispatch_visit(self, node): + """ + Dispatch node to appropriate visitor method. + The priority of visitor method is: + + 1. ``self.visit_{node_class}()`` + 2. ``self.visit_{supre_node_class}()`` + 3. ``self.unknown_visit()`` + """ + for node_class in node.__class__.__mro__: + method = getattr(self, 'visit_%s' % (node_class.__name__), None) + if method: + logger.debug('SphinxTranslator.dispatch_visit calling %s for %s' % + (method.__name__, node)) + return method(node) + else: + super().dispatch_visit(node) + + def dispatch_departure(self, node): + """ + Dispatch node to appropriate departure method. + The priority of departure method is: + + 1. ``self.depart_{node_class}()`` + 2. ``self.depart_{super_node_class}()`` + 3. ``self.unknown_departure()`` + """ + for node_class in node.__class__.__mro__: + method = getattr(self, 'depart_%s' % (node_class.__name__), None) + if method: + logger.debug('SphinxTranslator.dispatch_departure calling %s for %s' % + (method.__name__, node)) + return method(node) + else: + super().dispatch_departure(node) + # cache a vanilla instance of nodes.document # Used in new_document() function diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index e8e858f68..16f153ff8 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -121,6 +121,17 @@ def isenumattribute(x: Any) -> bool: return isinstance(x, enum.Enum) +def unpartial(obj: Any) -> Any: + """Get an original object from partial object. + + This returns given object itself if not partial. + """ + while ispartial(obj): + obj = obj.func + + return obj + + def ispartial(obj: Any) -> bool: """Check if the object is partial.""" return isinstance(obj, (partial, partialmethod)) @@ -197,24 +208,21 @@ def isattributedescriptor(obj: Any) -> bool: def isfunction(obj: Any) -> bool: """Check if the object is function.""" - return inspect.isfunction(obj) or ispartial(obj) and inspect.isfunction(obj.func) + return inspect.isfunction(unpartial(obj)) def isbuiltin(obj: Any) -> bool: """Check if the object is builtin.""" - return inspect.isbuiltin(obj) or ispartial(obj) and inspect.isbuiltin(obj.func) + return inspect.isbuiltin(unpartial(obj)) def iscoroutinefunction(obj: Any) -> bool: """Check if the object is coroutine-function.""" + obj = unpartial(obj) if hasattr(obj, '__code__') and inspect.iscoroutinefunction(obj): # check obj.__code__ because iscoroutinefunction() crashes for custom method-like # objects (see https://github.com/sphinx-doc/sphinx/issues/6605) return True - elif (ispartial(obj) and hasattr(obj.func, '__code__') and - inspect.iscoroutinefunction(obj.func)): - # partialed - return True else: return False @@ -374,44 +382,40 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, args = [] last_kind = None for param in sig.parameters.values(): - # insert '*' between POSITIONAL args and KEYWORD_ONLY args:: - # func(a, b, *, c, d): - if param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD, - param.POSITIONAL_ONLY, - None): + if param.kind != param.POSITIONAL_ONLY and last_kind == param.POSITIONAL_ONLY: + # PEP-570: Separator for Positional Only Parameter: / + args.append('/') + elif param.kind == param.KEYWORD_ONLY and last_kind in (param.POSITIONAL_OR_KEYWORD, + param.POSITIONAL_ONLY, + None): + # PEP-3102: Separator for Keyword Only Parameter: * args.append('*') arg = StringIO() - if param.kind in (param.POSITIONAL_ONLY, - param.POSITIONAL_OR_KEYWORD, - param.KEYWORD_ONLY): - arg.write(param.name) - if show_annotation and param.annotation is not param.empty: - arg.write(': ') - arg.write(stringify_annotation(param.annotation)) - if param.default is not param.empty: - if show_annotation and param.annotation is not param.empty: - arg.write(' = ') - arg.write(object_description(param.default)) - else: - arg.write('=') - arg.write(object_description(param.default)) - elif param.kind == param.VAR_POSITIONAL: - arg.write('*') - arg.write(param.name) - if show_annotation and param.annotation is not param.empty: - arg.write(': ') - arg.write(stringify_annotation(param.annotation)) + if param.kind == param.VAR_POSITIONAL: + arg.write('*' + param.name) elif param.kind == param.VAR_KEYWORD: - arg.write('**') + arg.write('**' + param.name) + else: arg.write(param.name) + + if show_annotation and param.annotation is not param.empty: + arg.write(': ') + arg.write(stringify_annotation(param.annotation)) + if param.default is not param.empty: if show_annotation and param.annotation is not param.empty: - arg.write(': ') - arg.write(stringify_annotation(param.annotation)) + arg.write(' = ') + else: + arg.write('=') + arg.write(object_description(param.default)) args.append(arg.getvalue()) last_kind = param.kind + if last_kind == Parameter.POSITIONAL_ONLY: + # PEP-570: Separator for Positional Only Parameter: / + args.append('/') + if (sig.return_annotation is Parameter.empty or show_annotation is False or show_return_annotation is False): diff --git a/tests/roots/test-ext-autodoc/target/partialfunction.py b/tests/roots/test-ext-autodoc/target/partialfunction.py index 727a62680..3be63eeee 100644 --- a/tests/roots/test-ext-autodoc/target/partialfunction.py +++ b/tests/roots/test-ext-autodoc/target/partialfunction.py @@ -1,11 +1,12 @@ from functools import partial -def func1(): +def func1(a, b, c): """docstring of func1""" pass -func2 = partial(func1) -func3 = partial(func1) +func2 = partial(func1, 1) +func3 = partial(func2, 2) func3.__doc__ = "docstring of func3" +func4 = partial(func3, 3) diff --git a/tests/roots/test-ext-autodoc/target/partialmethod.py b/tests/roots/test-ext-autodoc/target/partialmethod.py index 01cf4e798..4966a984f 100644 --- a/tests/roots/test-ext-autodoc/target/partialmethod.py +++ b/tests/roots/test-ext-autodoc/target/partialmethod.py @@ -14,5 +14,5 @@ class Cell(object): #: Make a cell alive. set_alive = partialmethod(set_state, True) + # a partialmethod with no docstring set_dead = partialmethod(set_state, False) - """Make a cell dead.""" diff --git a/tests/roots/test-ext-autodoc/target/pep570.py b/tests/roots/test-ext-autodoc/target/pep570.py new file mode 100644 index 000000000..904692eeb --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/pep570.py @@ -0,0 +1,5 @@ +def foo(a, b, /, c, d): + pass + +def bar(a, b, /): + pass diff --git a/tests/roots/test-ext-autodoc/target/typehints.py b/tests/roots/test-ext-autodoc/target/typehints.py index eedaab3b9..842530c13 100644 --- a/tests/roots/test-ext-autodoc/target/typehints.py +++ b/tests/roots/test-ext-autodoc/target/typehints.py @@ -2,9 +2,23 @@ def incr(a: int, b: int = 1) -> int: return a + b +def decr(a, b = 1): + # type: (int, int) -> int + return a - b + + class Math: def __init__(self, s: str, o: object = None) -> None: pass def incr(self, a: int, b: int = 1) -> int: return a + b + + def decr(self, a, b = 1): + # type: (int, int) -> int + return a - b + + +def complex_func(arg1, arg2, arg3=None, *args, **kwargs): + # type: (str, List[int], Tuple[int, Union[str, Unknown]], *str, **str) -> None + pass diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 819cbdcde..71ddb6624 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1265,19 +1265,25 @@ def test_partialfunction(): '.. py:module:: target.partialfunction', '', '', - '.. py:function:: func1()', + '.. py:function:: func1(a, b, c)', ' :module: target.partialfunction', '', ' docstring of func1', ' ', '', - '.. py:function:: func2()', + '.. py:function:: func2(b, c)', ' :module: target.partialfunction', '', ' docstring of func1', ' ', '', - '.. py:function:: func3()', + '.. py:function:: func3(c)', + ' :module: target.partialfunction', + '', + ' docstring of func3', + ' ', + '', + '.. py:function:: func4()', ' :module: target.partialfunction', '', ' docstring of func3', @@ -1348,12 +1354,40 @@ def test_partialmethod(app): ' Make a cell alive.', ' ', ' ', - ' .. py:method:: Cell.set_dead()', + ' .. py:method:: Cell.set_state(state)', + ' :module: target.partialmethod', + ' ', + ' Update state of cell to *state*.', + ' ', + ] + + options = {"members": None} + actual = do_autodoc(app, 'class', 'target.partialmethod.Cell', options) + assert list(actual) == expected + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_partialmethod_undoc_members(app): + expected = [ + '', + '.. py:class:: Cell', + ' :module: target.partialmethod', + '', + ' An example for partialmethod.', + ' ', + ' refs: https://docs.python.jp/3/library/functools.html#functools.partialmethod', + ' ', + ' ', + ' .. py:method:: Cell.set_alive()', ' :module: target.partialmethod', ' ', - ' Make a cell dead.', + ' Make a cell alive.', ' ', ' ', + ' .. py:method:: Cell.set_dead()', + ' :module: target.partialmethod', + ' ', + ' ', ' .. py:method:: Cell.set_state(state)', ' :module: target.partialmethod', ' ', @@ -1361,7 +1395,8 @@ def test_partialmethod(app): ' ', ] - options = {"members": None} + options = {"members": None, + "undoc-members": None} actual = do_autodoc(app, 'class', 'target.partialmethod.Cell', options) assert list(actual) == expected diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index 4446ee0a8..3913bfac4 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -478,10 +478,23 @@ def test_autodoc_typehints_signature(app): ' :module: target.typehints', '', ' ', + ' .. py:method:: Math.decr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + ' ', + ' ', ' .. py:method:: Math.incr(a: int, b: int = 1) -> int', ' :module: target.typehints', ' ', '', + '.. py:function:: complex_func(arg1: str, arg2: List[int], arg3: Tuple[int, ' + 'Union[str, Unknown]] = None, *args: str, **kwargs: str) -> None', + ' :module: target.typehints', + '', + '', + '.. py:function:: decr(a: int, b: int = 1) -> int', + ' :module: target.typehints', + '', + '', '.. py:function:: incr(a: int, b: int = 1) -> int', ' :module: target.typehints', '' @@ -504,10 +517,22 @@ def test_autodoc_typehints_none(app): ' :module: target.typehints', '', ' ', + ' .. py:method:: Math.decr(a, b=1)', + ' :module: target.typehints', + ' ', + ' ', ' .. py:method:: Math.incr(a, b=1)', ' :module: target.typehints', ' ', '', + '.. py:function:: complex_func(arg1, arg2, arg3=None, *args, **kwargs)', + ' :module: target.typehints', + '', + '', + '.. py:function:: decr(a, b=1)', + ' :module: target.typehints', + '', + '', '.. py:function:: incr(a, b=1)', ' :module: target.typehints', '' diff --git a/tests/test_pycode_ast.py b/tests/test_pycode_ast.py new file mode 100644 index 000000000..af7e34a86 --- /dev/null +++ b/tests/test_pycode_ast.py @@ -0,0 +1,40 @@ +""" + test_pycode_ast + ~~~~~~~~~~~~~~~ + + Test pycode.ast + + :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import pytest + +from sphinx.pycode import ast + + +@pytest.mark.parametrize('source,expected', [ + ("os.path", "os.path"), # Attribute + ("b'bytes'", "b'bytes'"), # Bytes + ("object()", "object()"), # Call + ("1234", "1234"), # Constant + ("{'key1': 'value1', 'key2': 'value2'}", + "{'key1': 'value1', 'key2': 'value2'}"), # Dict + ("...", "..."), # Ellipsis + ("Tuple[int, int]", "Tuple[int, int]"), # Index, Subscript + ("lambda x, y: x + y", + "<function <lambda>>"), # Lambda + ("[1, 2, 3]", "[1, 2, 3]"), # List + ("sys", "sys"), # Name, NameConstant + ("1234", "1234"), # Num + ("{1, 2, 3}", "{1, 2, 3}"), # Set + ("'str'", "'str'"), # Str + ("(1, 2, 3)", "1, 2, 3"), # Tuple +]) +def test_unparse(source, expected): + module = ast.parse(source) + assert ast.unparse(module.body[0].value) == expected + + +def test_unparse_None(): + assert ast.unparse(None) is None diff --git a/tests/test_util_docutils.py b/tests/test_util_docutils.py index c1df3f7ca..a22cf277a 100644 --- a/tests/test_util_docutils.py +++ b/tests/test_util_docutils.py @@ -12,7 +12,9 @@ import os from docutils import nodes -from sphinx.util.docutils import SphinxFileOutput, docutils_namespace, register_node +from sphinx.util.docutils import ( + SphinxFileOutput, SphinxTranslator, docutils_namespace, new_document, register_node +) def test_register_node(): @@ -61,3 +63,34 @@ def test_SphinxFileOutput(tmpdir): # overrite it again (content changed) output.write(content + "; content change") assert os.stat(filename).st_mtime != 0 # updated + + +def test_SphinxTranslator(app): + class CustomNode(nodes.inline): + pass + + class MyTranslator(SphinxTranslator): + def __init__(self, *args): + self.called = [] + super().__init__(*args) + + def visit_document(self, node): + pass + + def depart_document(self, node): + pass + + def visit_inline(self, node): + self.called.append('visit_inline') + + def depart_inline(self, node): + self.called.append('depart_inline') + + document = new_document('') + document += CustomNode() + + translator = MyTranslator(document, app.builder) + document.walkabout(translator) + + # MyTranslator does not have visit_CustomNode. But it calls visit_inline instead. + assert translator.called == ['visit_inline', 'depart_inline'] diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 68d1ac604..34844c9bf 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -294,6 +294,21 @@ def test_signature_annotations(): sig = inspect.signature(f7) assert stringify_signature(sig, show_return_annotation=False) == '(x: int = None, y: dict = {})' + +@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') +@pytest.mark.sphinx(testroot='ext-autodoc') +def test_signature_annotations_py38(app): + from target.pep570 import foo, bar + + # case: separator in the middle + sig = inspect.signature(foo) + assert stringify_signature(sig) == '(a, b, /, c, d)' + + # case: separator at tail + sig = inspect.signature(bar) + assert stringify_signature(sig) == '(a, b, /)' + + def test_safe_getattr_with_default(): class Foo: def __getattr__(self, item): @@ -496,3 +511,15 @@ def test_isproperty(app): assert inspect.isproperty(Base.meth) is False # method of class assert inspect.isproperty(Base().meth) is False # method of instance assert inspect.isproperty(func) is False # function + + +def test_unpartial(): + def func1(a, b, c): + pass + + func2 = functools.partial(func1, 1) + func2.__doc__ = "func2" + func3 = functools.partial(func2, 2) # nested partial object + + assert inspect.unpartial(func2) is func1 + assert inspect.unpartial(func3) is func1 |