mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge branch '3.x'
This commit is contained in:
commit
71eddf3095
11
CHANGES
11
CHANGES
@ -72,6 +72,7 @@ Features added
|
||||
``:meta public:`` in info-field-list
|
||||
* #7487: autodoc: Allow to generate docs for singledispatch functions by
|
||||
py:autofunction
|
||||
* #7143: autodoc: Support final classes and methods
|
||||
* #7466: autosummary: headings in generated documents are not translated
|
||||
* #7490: autosummary: Add ``:caption:`` option to autosummary directive to set a
|
||||
caption to the toctree
|
||||
@ -79,8 +80,6 @@ Features added
|
||||
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
|
||||
* #7482: html theme: CSS spacing for code blocks with captions and line numbers
|
||||
* #7443: html theme: Add new options :confval:`globaltoc_collapse` and
|
||||
@ -95,12 +94,20 @@ Features added
|
||||
* #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.
|
||||
* #7143: py domain: Add ``:final:`` option to :rst:dir:`py:class:`,
|
||||
:rst:dir:`py:exception:` and :rst:dir:`py:method:` directives
|
||||
|
||||
Bugs fixed
|
||||
----------
|
||||
|
||||
* #6703: autodoc: incremental build does not work for imported objects
|
||||
* #7564: autodoc: annotations not to be shown for descriptors
|
||||
* #6588: autodoc: Decorated inherited method has no documentation
|
||||
* #7469: autodoc: The change of autodoc-process-docstring for variables is
|
||||
cached unexpectedly
|
||||
* #7535: sphinx-autogen: crashes when custom template uses inheritance
|
||||
* #7536: sphinx-autogen: crashes when template uses i18n feature
|
||||
* #2785: html: Bad alignment of equation links
|
||||
|
||||
Testing
|
||||
--------
|
||||
|
@ -212,6 +212,15 @@ The following directives are provided for module and class contents:
|
||||
Describes an exception class. The signature can, but need not include
|
||||
parentheses with constructor arguments.
|
||||
|
||||
.. rubric:: options
|
||||
|
||||
.. rst:directive:option:: final
|
||||
:type: no value
|
||||
|
||||
Indicate the class is a final class.
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
.. rst:directive:: .. py:class:: name
|
||||
.. py:class:: name(parameters)
|
||||
|
||||
@ -235,6 +244,15 @@ The following directives are provided for module and class contents:
|
||||
|
||||
The first way is the preferred one.
|
||||
|
||||
.. rubric:: options
|
||||
|
||||
.. rst:directive:option:: final
|
||||
:type: no value
|
||||
|
||||
Indicate the class is a final class.
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
.. rst:directive:: .. py:attribute:: name
|
||||
|
||||
Describes an object data attribute. The description should include
|
||||
@ -283,6 +301,13 @@ The following directives are provided for module and class contents:
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
.. rst:directive:option:: final
|
||||
:type: no value
|
||||
|
||||
Indicate the class is a final method.
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
.. rst:directive:option:: property
|
||||
:type: no value
|
||||
|
||||
|
@ -604,10 +604,18 @@ class PyClasslike(PyObject):
|
||||
Description of a class-like object (classes, interfaces, exceptions).
|
||||
"""
|
||||
|
||||
option_spec = PyObject.option_spec.copy()
|
||||
option_spec.update({
|
||||
'final': directives.flag,
|
||||
})
|
||||
|
||||
allow_nesting = True
|
||||
|
||||
def get_signature_prefix(self, sig: str) -> str:
|
||||
return self.objtype + ' '
|
||||
if 'final' in self.options:
|
||||
return 'final %s ' % self.objtype
|
||||
else:
|
||||
return '%s ' % self.objtype
|
||||
|
||||
def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
|
||||
if self.objtype == 'class':
|
||||
@ -628,6 +636,7 @@ class PyMethod(PyObject):
|
||||
'abstractmethod': directives.flag,
|
||||
'async': directives.flag,
|
||||
'classmethod': directives.flag,
|
||||
'final': directives.flag,
|
||||
'property': directives.flag,
|
||||
'staticmethod': directives.flag,
|
||||
})
|
||||
@ -640,6 +649,8 @@ class PyMethod(PyObject):
|
||||
|
||||
def get_signature_prefix(self, sig: str) -> str:
|
||||
prefix = []
|
||||
if 'final' in self.options:
|
||||
prefix.append('final')
|
||||
if 'abstractmethod' in self.options:
|
||||
prefix.append('abstract')
|
||||
if 'async' in self.options:
|
||||
|
@ -431,7 +431,8 @@ class Documenter:
|
||||
def get_doc(self, ignore: int = 1) -> List[List[str]]:
|
||||
"""Decode and return lines of the docstring(s) for the object."""
|
||||
docstring = getdoc(self.object, self.get_attr,
|
||||
self.env.config.autodoc_inherit_docstrings)
|
||||
self.env.config.autodoc_inherit_docstrings,
|
||||
self.parent, self.object_name)
|
||||
if docstring:
|
||||
tab_width = self.directive.state.document.settings.tab_width
|
||||
return [prepare_docstring(docstring, ignore, tab_width)]
|
||||
@ -462,7 +463,10 @@ class Documenter:
|
||||
key = ('.'.join(self.objpath[:-1]), self.objpath[-1])
|
||||
if key in attr_docs:
|
||||
no_docstring = True
|
||||
docstrings = [attr_docs[key]]
|
||||
# make a copy of docstring for attributes to avoid cache
|
||||
# the change of autodoc-process-docstring event.
|
||||
docstrings = [list(attr_docs[key])]
|
||||
|
||||
for i, line in enumerate(self.process_doc(docstrings)):
|
||||
self.add_line(line, sourcename, i)
|
||||
|
||||
@ -552,7 +556,8 @@ class Documenter:
|
||||
else:
|
||||
isattr = False
|
||||
|
||||
doc = getdoc(member, self.get_attr, self.env.config.autodoc_inherit_docstrings)
|
||||
doc = getdoc(member, self.get_attr, self.env.config.autodoc_inherit_docstrings,
|
||||
self.parent, self.object_name)
|
||||
if not isinstance(doc, str):
|
||||
# Ignore non-string __doc__
|
||||
doc = None
|
||||
@ -1200,10 +1205,15 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
||||
return super().format_signature(**kwargs)
|
||||
|
||||
def add_directive_header(self, sig: str) -> None:
|
||||
sourcename = self.get_sourcename()
|
||||
|
||||
if self.doc_as_attr:
|
||||
self.directivetype = 'attribute'
|
||||
super().add_directive_header(sig)
|
||||
|
||||
if self.analyzer and '.'.join(self.objpath) in self.analyzer.finals:
|
||||
self.add_line(' :final:', sourcename)
|
||||
|
||||
# add inheritance info, if wanted
|
||||
if not self.doc_as_attr and self.options.show_inheritance:
|
||||
sourcename = self.get_sourcename()
|
||||
@ -1233,7 +1243,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
||||
if content in ('both', 'init'):
|
||||
__init__ = self.get_attr(self.object, '__init__', None)
|
||||
initdocstring = getdoc(__init__, self.get_attr,
|
||||
self.env.config.autodoc_inherit_docstrings)
|
||||
self.env.config.autodoc_inherit_docstrings,
|
||||
self.parent, self.object_name)
|
||||
# for new-style classes, no __init__ means default __init__
|
||||
if (initdocstring is not None and
|
||||
(initdocstring == object.__init__.__doc__ or # for pypy
|
||||
@ -1243,7 +1254,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
|
||||
# try __new__
|
||||
__new__ = self.get_attr(self.object, '__new__', None)
|
||||
initdocstring = getdoc(__new__, self.get_attr,
|
||||
self.env.config.autodoc_inherit_docstrings)
|
||||
self.env.config.autodoc_inherit_docstrings,
|
||||
self.parent, self.object_name)
|
||||
# for new-style classes, no __new__ means default __new__
|
||||
if (initdocstring is not None and
|
||||
(initdocstring == object.__new__.__doc__ or # for pypy
|
||||
@ -1467,6 +1479,8 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
|
||||
self.add_line(' :classmethod:', sourcename)
|
||||
if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name):
|
||||
self.add_line(' :staticmethod:', sourcename)
|
||||
if self.analyzer and '.'.join(self.objpath) in self.analyzer.finals:
|
||||
self.add_line(' :final:', sourcename)
|
||||
|
||||
def document_members(self, all_members: bool = False) -> None:
|
||||
pass
|
||||
|
@ -133,6 +133,7 @@ class ModuleAnalyzer:
|
||||
# will be filled by parse()
|
||||
self.annotations = None # type: Dict[Tuple[str, str], str]
|
||||
self.attr_docs = None # type: Dict[Tuple[str, str], List[str]]
|
||||
self.finals = None # type: List[str]
|
||||
self.tagorder = None # type: Dict[str, int]
|
||||
self.tags = None # type: Dict[str, Tuple[str, int, int]]
|
||||
|
||||
@ -150,6 +151,7 @@ class ModuleAnalyzer:
|
||||
self.attr_docs[scope] = ['']
|
||||
|
||||
self.annotations = parser.annotations
|
||||
self.finals = parser.finals
|
||||
self.tags = parser.definitions
|
||||
self.tagorder = parser.deforders
|
||||
except Exception as exc:
|
||||
|
@ -231,6 +231,9 @@ class VariableCommentPicker(ast.NodeVisitor):
|
||||
self.annotations = {} # type: Dict[Tuple[str, str], str]
|
||||
self.previous = None # type: ast.AST
|
||||
self.deforders = {} # type: Dict[str, int]
|
||||
self.finals = [] # type: List[str]
|
||||
self.typing = None # type: str
|
||||
self.typing_final = None # type: str
|
||||
super().__init__()
|
||||
|
||||
def get_qualname_for(self, name: str) -> Optional[List[str]]:
|
||||
@ -249,6 +252,11 @@ class VariableCommentPicker(ast.NodeVisitor):
|
||||
if qualname:
|
||||
self.deforders[".".join(qualname)] = next(self.counter)
|
||||
|
||||
def add_final_entry(self, name: str) -> None:
|
||||
qualname = self.get_qualname_for(name)
|
||||
if qualname:
|
||||
self.finals.append(".".join(qualname))
|
||||
|
||||
def add_variable_comment(self, name: str, comment: str) -> None:
|
||||
qualname = self.get_qualname_for(name)
|
||||
if qualname:
|
||||
@ -261,6 +269,22 @@ class VariableCommentPicker(ast.NodeVisitor):
|
||||
basename = ".".join(qualname[:-1])
|
||||
self.annotations[(basename, name)] = unparse(annotation)
|
||||
|
||||
def is_final(self, decorators: List[ast.expr]) -> bool:
|
||||
final = []
|
||||
if self.typing:
|
||||
final.append('%s.final' % self.typing)
|
||||
if self.typing_final:
|
||||
final.append(self.typing_final)
|
||||
|
||||
for decorator in decorators:
|
||||
try:
|
||||
if unparse(decorator) in final:
|
||||
return True
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
def get_self(self) -> ast.arg:
|
||||
"""Returns the name of first argument if in function."""
|
||||
if self.current_function and self.current_function.args.args:
|
||||
@ -282,11 +306,19 @@ class VariableCommentPicker(ast.NodeVisitor):
|
||||
for name in node.names:
|
||||
self.add_entry(name.asname or name.name)
|
||||
|
||||
if name.name == 'typing':
|
||||
self.typing = name.asname or name.name
|
||||
elif name.name == 'typing.final':
|
||||
self.typing_final = name.asname or name.name
|
||||
|
||||
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
||||
"""Handles Import node and record it to definition orders."""
|
||||
for name in node.names:
|
||||
self.add_entry(name.asname or name.name)
|
||||
|
||||
if node.module == 'typing' and name.name == 'final':
|
||||
self.typing_final = name.asname or name.name
|
||||
|
||||
def visit_Assign(self, node: ast.Assign) -> None:
|
||||
"""Handles Assign node and pick up a variable comment."""
|
||||
try:
|
||||
@ -370,6 +402,8 @@ class VariableCommentPicker(ast.NodeVisitor):
|
||||
"""Handles ClassDef node and set context."""
|
||||
self.current_classes.append(node.name)
|
||||
self.add_entry(node.name)
|
||||
if self.is_final(node.decorator_list):
|
||||
self.add_final_entry(node.name)
|
||||
self.context.append(node.name)
|
||||
self.previous = node
|
||||
for child in node.body:
|
||||
@ -381,6 +415,8 @@ class VariableCommentPicker(ast.NodeVisitor):
|
||||
"""Handles FunctionDef node and set context."""
|
||||
if self.current_function is None:
|
||||
self.add_entry(node.name) # should be called before setting self.current_function
|
||||
if self.is_final(node.decorator_list):
|
||||
self.add_final_entry(node.name)
|
||||
self.context.append(node.name)
|
||||
self.current_function = node
|
||||
for child in node.body:
|
||||
@ -481,6 +517,7 @@ class Parser:
|
||||
self.comments = {} # type: Dict[Tuple[str, str], str]
|
||||
self.deforders = {} # type: Dict[str, int]
|
||||
self.definitions = {} # type: Dict[str, Tuple[str, int, int]]
|
||||
self.finals = [] # type: List[str]
|
||||
|
||||
def parse(self) -> None:
|
||||
"""Parse the source code."""
|
||||
@ -495,6 +532,7 @@ class Parser:
|
||||
self.annotations = picker.annotations
|
||||
self.comments = picker.comments
|
||||
self.deforders = picker.deforders
|
||||
self.finals = picker.finals
|
||||
|
||||
def parse_definition(self) -> None:
|
||||
"""Parse the location of definitions from the code."""
|
||||
|
@ -779,8 +779,7 @@ span.eqno {
|
||||
}
|
||||
|
||||
span.eqno a.headerlink {
|
||||
position: relative;
|
||||
left: 0px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
@ -526,13 +526,14 @@ def signature_from_str(signature: str) -> inspect.Signature:
|
||||
|
||||
|
||||
def getdoc(obj: Any, attrgetter: Callable = safe_getattr,
|
||||
allow_inherited: bool = False) -> str:
|
||||
allow_inherited: bool = False, cls: Any = None, name: str = None) -> str:
|
||||
"""Get the docstring for the object.
|
||||
|
||||
This tries to obtain the docstring for some kind of objects additionally:
|
||||
|
||||
* partial functions
|
||||
* inherited docstring
|
||||
* inherited decorated methods
|
||||
"""
|
||||
doc = attrgetter(obj, '__doc__', None)
|
||||
if ispartial(obj) and doc == obj.__class__.__doc__:
|
||||
@ -540,4 +541,14 @@ def getdoc(obj: Any, attrgetter: Callable = safe_getattr,
|
||||
elif doc is None and allow_inherited:
|
||||
doc = inspect.getdoc(obj)
|
||||
|
||||
if doc is None and cls:
|
||||
# inspect.getdoc() does not support some kind of inherited and decorated methods.
|
||||
# This tries to obtain the docstring from super classes.
|
||||
for basecls in getattr(cls, '__mro__', []):
|
||||
meth = safe_getattr(basecls, name, None)
|
||||
if meth:
|
||||
doc = inspect.getdoc(meth)
|
||||
if doc:
|
||||
break
|
||||
|
||||
return doc
|
||||
|
14
tests/roots/test-ext-autodoc/target/final.py
Normal file
14
tests/roots/test-ext-autodoc/target/final.py
Normal file
@ -0,0 +1,14 @@
|
||||
import typing
|
||||
from typing import final
|
||||
|
||||
|
||||
@typing.final
|
||||
class Class:
|
||||
"""docstring"""
|
||||
|
||||
@final
|
||||
def meth1(self):
|
||||
"""docstring"""
|
||||
|
||||
def meth2(self):
|
||||
"""docstring"""
|
@ -1691,3 +1691,36 @@ def test_cython():
|
||||
' Docstring.',
|
||||
'',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 8),
|
||||
reason='typing.final is available since python3.8')
|
||||
@pytest.mark.usefixtures('setup_test')
|
||||
def test_final():
|
||||
options = {"members": None}
|
||||
actual = do_autodoc(app, 'module', 'target.final', options)
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:module:: target.final',
|
||||
'',
|
||||
'',
|
||||
'.. py:class:: Class',
|
||||
' :module: target.final',
|
||||
' :final:',
|
||||
'',
|
||||
' docstring',
|
||||
'',
|
||||
'',
|
||||
' .. py:method:: Class.meth1()',
|
||||
' :module: target.final',
|
||||
' :final:',
|
||||
'',
|
||||
' docstring',
|
||||
'',
|
||||
'',
|
||||
' .. py:method:: Class.meth2()',
|
||||
' :module: target.final',
|
||||
'',
|
||||
' docstring',
|
||||
'',
|
||||
]
|
||||
|
@ -499,6 +499,34 @@ def test_pyfunction(app):
|
||||
assert domain.objects['example.func2'] == ('index', 'example.func2', 'function')
|
||||
|
||||
|
||||
def test_pyclass_options(app):
|
||||
text = (".. py:class:: Class1\n"
|
||||
".. py:class:: Class2\n"
|
||||
" :final:\n")
|
||||
domain = app.env.get_domain('py')
|
||||
doctree = restructuredtext.parse(app, text)
|
||||
assert_node(doctree, (addnodes.index,
|
||||
[desc, ([desc_signature, ([desc_annotation, "class "],
|
||||
[desc_name, "Class1"])],
|
||||
[desc_content, ()])],
|
||||
addnodes.index,
|
||||
[desc, ([desc_signature, ([desc_annotation, "final class "],
|
||||
[desc_name, "Class2"])],
|
||||
[desc_content, ()])]))
|
||||
|
||||
# class
|
||||
assert_node(doctree[0], addnodes.index,
|
||||
entries=[('single', 'Class1 (built-in class)', 'Class1', '', None)])
|
||||
assert 'Class1' in domain.objects
|
||||
assert domain.objects['Class1'] == ('index', 'Class1', 'class')
|
||||
|
||||
# :final:
|
||||
assert_node(doctree[2], addnodes.index,
|
||||
entries=[('single', 'Class2 (built-in class)', 'Class2', '', None)])
|
||||
assert 'Class2' in domain.objects
|
||||
assert domain.objects['Class2'] == ('index', 'Class2', 'class')
|
||||
|
||||
|
||||
def test_pymethod_options(app):
|
||||
text = (".. py:class:: Class\n"
|
||||
"\n"
|
||||
@ -512,7 +540,9 @@ def test_pymethod_options(app):
|
||||
" .. py:method:: meth5\n"
|
||||
" :property:\n"
|
||||
" .. py:method:: meth6\n"
|
||||
" :abstractmethod:\n")
|
||||
" :abstractmethod:\n"
|
||||
" .. py:method:: meth7\n"
|
||||
" :final:\n")
|
||||
domain = app.env.get_domain('py')
|
||||
doctree = restructuredtext.parse(app, text)
|
||||
assert_node(doctree, (addnodes.index,
|
||||
@ -529,6 +559,8 @@ def test_pymethod_options(app):
|
||||
addnodes.index,
|
||||
desc,
|
||||
addnodes.index,
|
||||
desc,
|
||||
addnodes.index,
|
||||
desc)])]))
|
||||
|
||||
# method
|
||||
@ -589,6 +621,16 @@ def test_pymethod_options(app):
|
||||
assert 'Class.meth6' in domain.objects
|
||||
assert domain.objects['Class.meth6'] == ('index', 'Class.meth6', 'method')
|
||||
|
||||
# :final:
|
||||
assert_node(doctree[1][1][12], addnodes.index,
|
||||
entries=[('single', 'meth7() (Class method)', 'Class.meth7', '', None)])
|
||||
assert_node(doctree[1][1][13], ([desc_signature, ([desc_annotation, "final "],
|
||||
[desc_name, "meth7"],
|
||||
[desc_parameterlist, ()])],
|
||||
[desc_content, ()]))
|
||||
assert 'Class.meth7' in domain.objects
|
||||
assert domain.objects['Class.meth7'] == ('index', 'Class.meth7', 'method')
|
||||
|
||||
|
||||
def test_pyclassmethod(app):
|
||||
text = (".. py:class:: Class\n"
|
||||
|
@ -374,3 +374,81 @@ def test_formfeed_char():
|
||||
parser = Parser(source)
|
||||
parser.parse()
|
||||
assert parser.comments == {('Foo', 'attr'): 'comment'}
|
||||
|
||||
|
||||
def test_typing_final():
|
||||
source = ('import typing\n'
|
||||
'\n'
|
||||
'@typing.final\n'
|
||||
'def func(): pass\n'
|
||||
'\n'
|
||||
'@typing.final\n'
|
||||
'class Foo:\n'
|
||||
' @typing.final\n'
|
||||
' def meth(self):\n'
|
||||
' pass\n')
|
||||
parser = Parser(source)
|
||||
parser.parse()
|
||||
assert parser.finals == ['func', 'Foo', 'Foo.meth']
|
||||
|
||||
|
||||
def test_typing_final_from_import():
|
||||
source = ('from typing import final\n'
|
||||
'\n'
|
||||
'@final\n'
|
||||
'def func(): pass\n'
|
||||
'\n'
|
||||
'@final\n'
|
||||
'class Foo:\n'
|
||||
' @final\n'
|
||||
' def meth(self):\n'
|
||||
' pass\n')
|
||||
parser = Parser(source)
|
||||
parser.parse()
|
||||
assert parser.finals == ['func', 'Foo', 'Foo.meth']
|
||||
|
||||
|
||||
def test_typing_final_import_as():
|
||||
source = ('import typing as foo\n'
|
||||
'\n'
|
||||
'@foo.final\n'
|
||||
'def func(): pass\n'
|
||||
'\n'
|
||||
'@foo.final\n'
|
||||
'class Foo:\n'
|
||||
' @typing.final\n'
|
||||
' def meth(self):\n'
|
||||
' pass\n')
|
||||
parser = Parser(source)
|
||||
parser.parse()
|
||||
assert parser.finals == ['func', 'Foo']
|
||||
|
||||
|
||||
def test_typing_final_from_import_as():
|
||||
source = ('from typing import final as bar\n'
|
||||
'\n'
|
||||
'@bar\n'
|
||||
'def func(): pass\n'
|
||||
'\n'
|
||||
'@bar\n'
|
||||
'class Foo:\n'
|
||||
' @final\n'
|
||||
' def meth(self):\n'
|
||||
' pass\n')
|
||||
parser = Parser(source)
|
||||
parser.parse()
|
||||
assert parser.finals == ['func', 'Foo']
|
||||
|
||||
|
||||
def test_typing_final_not_imported():
|
||||
source = ('@typing.final\n'
|
||||
'def func(): pass\n'
|
||||
'\n'
|
||||
'@typing.final\n'
|
||||
'class Foo:\n'
|
||||
' @final\n'
|
||||
' def meth(self):\n'
|
||||
' pass\n')
|
||||
parser = Parser(source)
|
||||
parser.parse()
|
||||
assert parser.finals == []
|
||||
|
@ -564,3 +564,18 @@ def test_unpartial():
|
||||
|
||||
assert inspect.unpartial(func2) is func1
|
||||
assert inspect.unpartial(func3) is func1
|
||||
|
||||
|
||||
def test_getdoc_inherited_decorated_method():
|
||||
class Foo:
|
||||
def meth(self):
|
||||
"""docstring."""
|
||||
|
||||
class Bar(Foo):
|
||||
@functools.lru_cache()
|
||||
def meth(self):
|
||||
# inherited and decorated method
|
||||
pass
|
||||
|
||||
assert inspect.getdoc(Bar.meth, getattr, False, Bar, "meth") is None
|
||||
assert inspect.getdoc(Bar.meth, getattr, True, Bar, "meth") == "docstring."
|
||||
|
Loading…
Reference in New Issue
Block a user