mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge branch '4.x'
This commit is contained in:
commit
779274d4b8
13
CHANGES
13
CHANGES
@ -34,6 +34,10 @@ Deprecated
|
||||
Features added
|
||||
--------------
|
||||
|
||||
* #9445: autodoc: Support class properties
|
||||
* #9445: py domain: ``:py:property:`` directive supports ``:classmethod:``
|
||||
option to describe the class property
|
||||
|
||||
Bugs fixed
|
||||
----------
|
||||
|
||||
@ -49,6 +53,9 @@ Dependencies
|
||||
Incompatible changes
|
||||
--------------------
|
||||
|
||||
* #9435: linkcheck: Disable checking automatically generated anchors on
|
||||
github.com (ex. anchors in reST/Markdown documents)
|
||||
|
||||
Deprecated
|
||||
----------
|
||||
|
||||
@ -58,6 +65,12 @@ Features added
|
||||
Bugs fixed
|
||||
----------
|
||||
|
||||
* #9489: autodoc: Custom types using ``typing.NewType`` are not displayed well
|
||||
with the HEAD of 3.10
|
||||
* #9490: autodoc: Some objects under ``typing`` module are not displayed well
|
||||
with the HEAD of 3.10
|
||||
* #9435: linkcheck: Failed to check anchors in github.com
|
||||
|
||||
Testing
|
||||
--------
|
||||
|
||||
|
@ -1151,6 +1151,20 @@ Miscellany
|
||||
Formerly, use of *fncychap* with other styles than ``Bjarne`` was
|
||||
dysfunctional.
|
||||
|
||||
- Docutils :dudir:`container` directives are supported in LaTeX output: to
|
||||
let a container class with name ``foo`` influence the final PDF via LaTeX,
|
||||
it is only needed to define in the preamble an environment
|
||||
``sphinxclassfoo``. A simple example would be:
|
||||
|
||||
.. code-block:: latex
|
||||
|
||||
\newenvironment{sphinxclassred}{\color{red}}{}
|
||||
|
||||
Currently the class names must contain only ascii characters and avoid
|
||||
characters special to LaTeX such as ``\``.
|
||||
|
||||
.. versionadded:: 4.1.0
|
||||
|
||||
.. hint::
|
||||
|
||||
As an experimental feature, Sphinx can use user-defined template file for
|
||||
|
@ -329,6 +329,13 @@ The following directives are provided for module and class contents:
|
||||
|
||||
Indicate the property is abstract.
|
||||
|
||||
.. rst:directive:option:: classmethod
|
||||
:type: no value
|
||||
|
||||
Indicate the property is a classmethod.
|
||||
|
||||
.. versionaddedd: 4.2
|
||||
|
||||
.. rst:directive:option:: type: type of the property
|
||||
:type: text
|
||||
|
||||
|
@ -714,7 +714,10 @@ def setup(app: Sphinx) -> Dict[str, Any]:
|
||||
app.add_event('linkcheck-process-uri')
|
||||
|
||||
app.connect('config-inited', compile_linkcheck_allowed_redirects, priority=800)
|
||||
app.connect('linkcheck-process-uri', rewrite_github_anchor)
|
||||
|
||||
# FIXME: Disable URL rewrite handler for github.com temporarily.
|
||||
# ref: https://github.com/sphinx-doc/sphinx/issues/9435
|
||||
# app.connect('linkcheck-process-uri', rewrite_github_anchor)
|
||||
|
||||
return {
|
||||
'version': 'builtin',
|
||||
|
@ -852,6 +852,7 @@ class PyProperty(PyObject):
|
||||
option_spec = PyObject.option_spec.copy()
|
||||
option_spec.update({
|
||||
'abstractmethod': directives.flag,
|
||||
'classmethod': directives.flag,
|
||||
'type': directives.unchanged,
|
||||
})
|
||||
|
||||
@ -865,10 +866,13 @@ class PyProperty(PyObject):
|
||||
return fullname, prefix
|
||||
|
||||
def get_signature_prefix(self, sig: str) -> str:
|
||||
prefix = ['property']
|
||||
prefix = []
|
||||
if 'abstractmethod' in self.options:
|
||||
prefix.insert(0, 'abstract')
|
||||
prefix.append('abstract')
|
||||
if 'classmethod' in self.options:
|
||||
prefix.append('class')
|
||||
|
||||
prefix.append('property')
|
||||
return ' '.join(prefix) + ' '
|
||||
|
||||
def get_index_text(self, modname: str, name_cls: Tuple[str, str]) -> str:
|
||||
|
@ -718,7 +718,7 @@ class Documenter:
|
||||
isattr = False
|
||||
|
||||
doc = getdoc(member, self.get_attr, self.config.autodoc_inherit_docstrings,
|
||||
self.parent, self.object_name)
|
||||
self.object, membername)
|
||||
if not isinstance(doc, str):
|
||||
# Ignore non-string __doc__
|
||||
doc = None
|
||||
@ -2661,7 +2661,32 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): #
|
||||
@classmethod
|
||||
def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any
|
||||
) -> bool:
|
||||
return inspect.isproperty(member) and isinstance(parent, ClassDocumenter)
|
||||
if isinstance(parent, ClassDocumenter):
|
||||
if inspect.isproperty(member):
|
||||
return True
|
||||
else:
|
||||
__dict__ = safe_getattr(parent.object, '__dict__', {})
|
||||
obj = __dict__.get(membername)
|
||||
return isinstance(obj, classmethod) and inspect.isproperty(obj.__func__)
|
||||
else:
|
||||
return False
|
||||
|
||||
def import_object(self, raiseerror: bool = False) -> bool:
|
||||
"""Check the exisitence of uninitialized instance attribute when failed to import
|
||||
the attribute."""
|
||||
ret = super().import_object(raiseerror)
|
||||
if ret and not inspect.isproperty(self.object):
|
||||
__dict__ = safe_getattr(self.parent, '__dict__', {})
|
||||
obj = __dict__.get(self.objpath[-1])
|
||||
if isinstance(obj, classmethod) and inspect.isproperty(obj.__func__):
|
||||
self.object = obj.__func__
|
||||
self.isclassmethod = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
self.isclassmethod = False
|
||||
return ret
|
||||
|
||||
def document_members(self, all_members: bool = False) -> None:
|
||||
pass
|
||||
@ -2675,6 +2700,8 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): #
|
||||
sourcename = self.get_sourcename()
|
||||
if inspect.isabstractmethod(self.object):
|
||||
self.add_line(' :abstractmethod:', sourcename)
|
||||
if self.isclassmethod:
|
||||
self.add_line(' :classmethod:', sourcename)
|
||||
|
||||
if safe_getattr(self.object, 'fget', None) and self.config.autodoc_typehints != 'none':
|
||||
try:
|
||||
|
@ -211,12 +211,15 @@ def getslots(obj: Any) -> Optional[Dict]:
|
||||
|
||||
def isNewType(obj: Any) -> bool:
|
||||
"""Check the if object is a kind of NewType."""
|
||||
__module__ = safe_getattr(obj, '__module__', None)
|
||||
__qualname__ = safe_getattr(obj, '__qualname__', None)
|
||||
if __module__ == 'typing' and __qualname__ == 'NewType.<locals>.new_type':
|
||||
return True
|
||||
if sys.version_info >= (3, 10):
|
||||
return isinstance(obj, typing.NewType)
|
||||
else:
|
||||
return False
|
||||
__module__ = safe_getattr(obj, '__module__', None)
|
||||
__qualname__ = safe_getattr(obj, '__qualname__', None)
|
||||
if __module__ == 'typing' and __qualname__ == 'NewType.<locals>.new_type':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def isenumclass(x: Any) -> bool:
|
||||
@ -245,12 +248,17 @@ def ispartial(obj: Any) -> bool:
|
||||
return isinstance(obj, (partial, partialmethod))
|
||||
|
||||
|
||||
def isclassmethod(obj: Any) -> bool:
|
||||
def isclassmethod(obj: Any, cls: Any = None, name: str = None) -> bool:
|
||||
"""Check if the object is classmethod."""
|
||||
if isinstance(obj, classmethod):
|
||||
return True
|
||||
elif inspect.ismethod(obj) and obj.__self__ is not None and isclass(obj.__self__):
|
||||
return True
|
||||
elif cls and name:
|
||||
for basecls in getmro(cls):
|
||||
meth = basecls.__dict__.get(name)
|
||||
if meth:
|
||||
return isclassmethod(meth)
|
||||
|
||||
return False
|
||||
|
||||
@ -837,6 +845,12 @@ def getdoc(obj: Any, attrgetter: Callable = safe_getattr,
|
||||
* inherited docstring
|
||||
* inherited decorated methods
|
||||
"""
|
||||
if cls and name and isclassmethod(obj, cls, name):
|
||||
for basecls in getmro(cls):
|
||||
meth = basecls.__dict__.get(name)
|
||||
if meth:
|
||||
return getdoc(meth.__func__)
|
||||
|
||||
doc = attrgetter(obj, '__doc__', None)
|
||||
if ispartial(obj) and doc == obj.__class__.__doc__:
|
||||
return getdoc(obj.func)
|
||||
|
@ -171,17 +171,17 @@ def _restify_py37(cls: Optional[Type]) -> str:
|
||||
text += r"\ [%s]" % ", ".join(restify(a) for a in cls.__args__)
|
||||
|
||||
return text
|
||||
elif hasattr(cls, '__qualname__'):
|
||||
if cls.__module__ == 'typing':
|
||||
return ':class:`~%s.%s`' % (cls.__module__, cls.__qualname__)
|
||||
else:
|
||||
return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__)
|
||||
elif hasattr(cls, '_name'):
|
||||
# SpecialForm
|
||||
if cls.__module__ == 'typing':
|
||||
return ':obj:`~%s.%s`' % (cls.__module__, cls._name)
|
||||
else:
|
||||
return ':obj:`%s.%s`' % (cls.__module__, cls._name)
|
||||
elif hasattr(cls, '__qualname__'):
|
||||
if cls.__module__ == 'typing':
|
||||
return ':class:`~%s.%s`' % (cls.__module__, cls.__qualname__)
|
||||
else:
|
||||
return ':class:`%s.%s`' % (cls.__module__, cls.__qualname__)
|
||||
elif isinstance(cls, ForwardRef):
|
||||
return ':class:`%s`' % cls.__forward_arg__
|
||||
else:
|
||||
@ -309,8 +309,11 @@ def stringify(annotation: Any) -> str:
|
||||
elif annotation in INVALID_BUILTIN_CLASSES:
|
||||
return INVALID_BUILTIN_CLASSES[annotation]
|
||||
elif (getattr(annotation, '__module__', None) == 'builtins' and
|
||||
hasattr(annotation, '__qualname__')):
|
||||
return annotation.__qualname__
|
||||
getattr(annotation, '__qualname__', None)):
|
||||
if hasattr(annotation, '__args__'): # PEP 585 generic
|
||||
return repr(annotation)
|
||||
else:
|
||||
return annotation.__qualname__
|
||||
elif annotation is Ellipsis:
|
||||
return '...'
|
||||
|
||||
|
@ -2,5 +2,10 @@ class Foo:
|
||||
"""docstring"""
|
||||
|
||||
@property
|
||||
def prop(self) -> int:
|
||||
def prop1(self) -> int:
|
||||
"""docstring"""
|
||||
|
||||
@classmethod
|
||||
@property
|
||||
def prop2(self) -> int:
|
||||
"""docstring"""
|
||||
|
@ -13,8 +13,7 @@ Some additional anchors to exercise ignore code
|
||||
* `Complete nonsense <https://localhost:7777/doesnotexist>`_
|
||||
* `Example valid local file <conf.py>`_
|
||||
* `Example invalid local file <path/to/notfound>`_
|
||||
* https://github.com/sphinx-doc/sphinx#documentation
|
||||
* https://github.com/sphinx-doc/sphinx#user-content-testing
|
||||
* https://github.com/sphinx-doc/sphinx/blob/4.x/sphinx/__init__.py#L2
|
||||
|
||||
.. image:: https://www.google.com/image.png
|
||||
.. figure:: https://www.google.com/image2.png
|
||||
|
@ -66,8 +66,8 @@ def test_defaults_json(app):
|
||||
"info"]:
|
||||
assert attr in row
|
||||
|
||||
assert len(content.splitlines()) == 12
|
||||
assert len(rows) == 12
|
||||
assert len(content.splitlines()) == 11
|
||||
assert len(rows) == 11
|
||||
# the output order of the rows is not stable
|
||||
# due to possible variance in network latency
|
||||
rowsby = {row["uri"]: row for row in rows}
|
||||
@ -88,7 +88,7 @@ def test_defaults_json(app):
|
||||
assert dnerow['uri'] == 'https://localhost:7777/doesnotexist'
|
||||
assert rowsby['https://www.google.com/image2.png'] == {
|
||||
'filename': 'links.txt',
|
||||
'lineno': 20,
|
||||
'lineno': 19,
|
||||
'status': 'broken',
|
||||
'code': 0,
|
||||
'uri': 'https://www.google.com/image2.png',
|
||||
@ -102,10 +102,6 @@ def test_defaults_json(app):
|
||||
# images should fail
|
||||
assert "Not Found for url: https://www.google.com/image.png" in \
|
||||
rowsby["https://www.google.com/image.png"]["info"]
|
||||
# The anchor of the URI for github.com is automatically modified
|
||||
assert 'https://github.com/sphinx-doc/sphinx#documentation' not in rowsby
|
||||
assert 'https://github.com/sphinx-doc/sphinx#user-content-documentation' in rowsby
|
||||
assert 'https://github.com/sphinx-doc/sphinx#user-content-testing' in rowsby
|
||||
|
||||
|
||||
@pytest.mark.sphinx(
|
||||
|
@ -813,8 +813,12 @@ def test_pyattribute(app):
|
||||
def test_pyproperty(app):
|
||||
text = (".. py:class:: Class\n"
|
||||
"\n"
|
||||
" .. py:property:: prop\n"
|
||||
" .. py:property:: prop1\n"
|
||||
" :abstractmethod:\n"
|
||||
" :type: str\n"
|
||||
"\n"
|
||||
" .. py:property:: prop2\n"
|
||||
" :classmethod:\n"
|
||||
" :type: str\n")
|
||||
domain = app.env.get_domain('py')
|
||||
doctree = restructuredtext.parse(app, text)
|
||||
@ -822,15 +826,25 @@ def test_pyproperty(app):
|
||||
[desc, ([desc_signature, ([desc_annotation, "class "],
|
||||
[desc_name, "Class"])],
|
||||
[desc_content, (addnodes.index,
|
||||
desc,
|
||||
addnodes.index,
|
||||
desc)])]))
|
||||
assert_node(doctree[1][1][0], addnodes.index,
|
||||
entries=[('single', 'prop (Class property)', 'Class.prop', '', None)])
|
||||
entries=[('single', 'prop1 (Class property)', 'Class.prop1', '', None)])
|
||||
assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "abstract property "],
|
||||
[desc_name, "prop"],
|
||||
[desc_name, "prop1"],
|
||||
[desc_annotation, ": str"])],
|
||||
[desc_content, ()]))
|
||||
assert 'Class.prop' in domain.objects
|
||||
assert domain.objects['Class.prop'] == ('index', 'Class.prop', 'property', False)
|
||||
assert_node(doctree[1][1][2], addnodes.index,
|
||||
entries=[('single', 'prop2 (Class property)', 'Class.prop2', '', None)])
|
||||
assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, "class property "],
|
||||
[desc_name, "prop2"],
|
||||
[desc_annotation, ": str"])],
|
||||
[desc_content, ()]))
|
||||
assert 'Class.prop1' in domain.objects
|
||||
assert domain.objects['Class.prop1'] == ('index', 'Class.prop1', 'property', False)
|
||||
assert 'Class.prop2' in domain.objects
|
||||
assert domain.objects['Class.prop2'] == ('index', 'Class.prop2', 'property', False)
|
||||
|
||||
|
||||
def test_pydecorator_signature(app):
|
||||
|
@ -212,12 +212,20 @@ def test_properties(app):
|
||||
' docstring',
|
||||
'',
|
||||
'',
|
||||
' .. py:property:: Foo.prop',
|
||||
' .. py:property:: Foo.prop1',
|
||||
' :module: target.properties',
|
||||
' :type: int',
|
||||
'',
|
||||
' docstring',
|
||||
'',
|
||||
'',
|
||||
' .. py:property:: Foo.prop2',
|
||||
' :module: target.properties',
|
||||
' :classmethod:',
|
||||
' :type: int',
|
||||
'',
|
||||
' docstring',
|
||||
'',
|
||||
]
|
||||
|
||||
|
||||
|
@ -16,13 +16,28 @@ from .test_ext_autodoc import do_autodoc
|
||||
|
||||
@pytest.mark.sphinx('html', testroot='ext-autodoc')
|
||||
def test_properties(app):
|
||||
actual = do_autodoc(app, 'property', 'target.properties.Foo.prop')
|
||||
actual = do_autodoc(app, 'property', 'target.properties.Foo.prop1')
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:property:: Foo.prop',
|
||||
'.. py:property:: Foo.prop1',
|
||||
' :module: target.properties',
|
||||
' :type: int',
|
||||
'',
|
||||
' docstring',
|
||||
'',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.sphinx('html', testroot='ext-autodoc')
|
||||
def test_class_properties(app):
|
||||
actual = do_autodoc(app, 'property', 'target.properties.Foo.prop2')
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:property:: Foo.prop2',
|
||||
' :module: target.properties',
|
||||
' :classmethod:',
|
||||
' :type: int',
|
||||
'',
|
||||
' docstring',
|
||||
'',
|
||||
]
|
||||
|
@ -175,6 +175,18 @@ def test_stringify_type_hints_containers():
|
||||
assert stringify(Generator[None, None, None]) == "Generator[None, None, None]"
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.')
|
||||
def test_stringify_type_hints_pep_585():
|
||||
assert stringify(list[int]) == "list[int]"
|
||||
assert stringify(list[str]) == "list[str]"
|
||||
assert stringify(dict[str, float]) == "dict[str, float]"
|
||||
assert stringify(tuple[str, str, str]) == "tuple[str, str, str]"
|
||||
assert stringify(tuple[str, ...]) == "tuple[str, ...]"
|
||||
assert stringify(tuple[()]) == "tuple[()]"
|
||||
assert stringify(list[dict[str, tuple]]) == "list[dict[str, tuple]]"
|
||||
assert stringify(type[int]) == "type[int]"
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.')
|
||||
def test_stringify_Annotated():
|
||||
from typing import Annotated # type: ignore
|
||||
|
Loading…
Reference in New Issue
Block a user