From 42d9d11247c4601d2a46feac1409723edd1c8cf0 Mon Sep 17 00:00:00 2001 From: Karolina Surma <33810531+befeleme@users.noreply.github.com> Date: Tue, 13 Jul 2021 09:22:09 +0200 Subject: [PATCH 1/8] Fix testcase: Enum changes were reverted in Python 3.10 --- tests/test_ext_autodoc.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 4c16886b3..3bb58cde3 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -1398,16 +1398,9 @@ def test_slots(app): def test_enum_class(app): options = {"members": None} actual = do_autodoc(app, 'class', 'target.enums.EnumCls', options) - - if sys.version_info < (3, 10): - sig = '(value)' - else: - sig = ('(value, names=None, *, module=None, qualname=None, type=None, start=1, ' - 'boundary=None)') - assert list(actual) == [ '', - '.. py:class:: EnumCls%s' % sig, + '.. py:class:: EnumCls(value)', ' :module: target.enums', '', ' this is enum class', From 120525563c4ed38ca2d05dc4757e81cf5415c473 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 17 Jul 2021 14:24:31 +0900 Subject: [PATCH 2/8] Cloase #9445: :py:property: directive now supports :classmethod: option Since python 3.9, `classmethod` starts to support creating a "class property". This allows to describe it. --- CHANGES | 3 +++ doc/usage/restructuredtext/domains.rst | 7 +++++++ sphinx/domains/python.py | 8 ++++++-- tests/test_domain_py.py | 24 +++++++++++++++++++----- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/CHANGES b/CHANGES index 5658cc43f..d4fc4d211 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,9 @@ Deprecated Features added -------------- +* #9445: py domain: ``:py:property:`` directive supports ``:classmethod:`` + option to describe the class property + Bugs fixed ---------- diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index 23844886e..abece4213 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -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 diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index d79de154e..e8330e81c 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -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: diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index e4fb08155..8b72f8b7a 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -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): From 38d80c3d0f899a6fa40e52b9b3f7ac5bcaa73311 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 17 Jul 2021 15:02:23 +0900 Subject: [PATCH 3/8] Close #9445: autodoc: Support class properties Since python 3.9, `classmethod` starts to support creating a "class property". This supports to generate document for it. --- CHANGES | 1 + sphinx/ext/autodoc/__init__.py | 31 +++++++++++++++++-- sphinx/util/inspect.py | 13 +++++++- .../test-ext-autodoc/target/properties.py | 7 ++++- tests/test_ext_autodoc_autoclass.py | 10 +++++- tests/test_ext_autodoc_autoproperty.py | 19 ++++++++++-- 6 files changed, 74 insertions(+), 7 deletions(-) diff --git a/CHANGES b/CHANGES index d4fc4d211..8289c5eb9 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,7 @@ Deprecated Features added -------------- +* #9445: autodoc: Support class properties * #9445: py domain: ``:py:property:`` directive supports ``:classmethod:`` option to describe the class property diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 1cecb1f79..fd5ca62a2 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -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: diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 23dd9e930..ac5182428 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -245,12 +245,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 +842,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) diff --git a/tests/roots/test-ext-autodoc/target/properties.py b/tests/roots/test-ext-autodoc/target/properties.py index 409fc2b5d..561daefb8 100644 --- a/tests/roots/test-ext-autodoc/target/properties.py +++ b/tests/roots/test-ext-autodoc/target/properties.py @@ -2,5 +2,10 @@ class Foo: """docstring""" @property - def prop(self) -> int: + def prop1(self) -> int: + """docstring""" + + @classmethod + @property + def prop2(self) -> int: """docstring""" diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py index 59ee1a948..24617bf0a 100644 --- a/tests/test_ext_autodoc_autoclass.py +++ b/tests/test_ext_autodoc_autoclass.py @@ -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', + '', ] diff --git a/tests/test_ext_autodoc_autoproperty.py b/tests/test_ext_autodoc_autoproperty.py index ee25aa8b7..2b4e5c12a 100644 --- a/tests/test_ext_autodoc_autoproperty.py +++ b/tests/test_ext_autodoc_autoproperty.py @@ -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', + '', + ] From 31e07c75dd235f30d70760f8d0181364b466a7ef Mon Sep 17 00:00:00 2001 From: James <50501825+Gobot1234@users.noreply.github.com> Date: Sat, 17 Jul 2021 17:57:00 +0100 Subject: [PATCH 4/8] Add support for PEP 585 generics --- sphinx/util/typing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 4e1b184e0..5c72d6f32 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -310,6 +310,8 @@ def stringify(annotation: Any) -> str: return INVALID_BUILTIN_CLASSES[annotation] elif (getattr(annotation, '__module__', None) == 'builtins' and hasattr(annotation, '__qualname__')): + if hasattr(annotation, '__args__'): # PEP 585 generic + return repr(annotation) return annotation.__qualname__ elif annotation is Ellipsis: return '...' From 451811c40cd9918213a13591a6b8dcaab4287ed5 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 18 Jul 2021 10:55:38 +0100 Subject: [PATCH 5/8] Respond to feedback --- sphinx/util/typing.py | 3 ++- tests/test_util_typing.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 5c72d6f32..4450d4edb 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -312,7 +312,8 @@ def stringify(annotation: Any) -> str: hasattr(annotation, '__qualname__')): if hasattr(annotation, '__args__'): # PEP 585 generic return repr(annotation) - return annotation.__qualname__ + else: + return annotation.__qualname__ elif annotation is Ellipsis: return '...' diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index 424715b39..afa5384f7 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -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 From af5363cefb32b2a7cde140ac4332417d9edb7374 Mon Sep 17 00:00:00 2001 From: jfbu <2589111+jfbu@users.noreply.github.com> Date: Mon, 19 Jul 2021 12:07:03 +0200 Subject: [PATCH 6/8] LaTeX: add some documentation for container support (#9166) --- doc/latex.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/latex.rst b/doc/latex.rst index f6d2987e2..f0b5a4aa8 100644 --- a/doc/latex.rst +++ b/doc/latex.rst @@ -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 From e1612b4909340a27096449de40d0f35954ec5276 Mon Sep 17 00:00:00 2001 From: jfbu <2589111+jfbu@users.noreply.github.com> Date: Mon, 19 Jul 2021 12:15:11 +0200 Subject: [PATCH 7/8] Remove mark-up breaking PDF output Close: #9475 Refs: #9355 --- doc/tutorial/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/tutorial/index.rst b/doc/tutorial/index.rst index 6ff268695..51409a6b0 100644 --- a/doc/tutorial/index.rst +++ b/doc/tutorial/index.rst @@ -233,7 +233,7 @@ Sweet! .. note:: Generating a PDF using Sphinx can be done running ``make latexpdf``, - provided that the system has a working :math:`\LaTeX` installation, + provided that the system has a working LaTeX installation, as explained in the documentation of :class:`sphinx.builders.latex.LaTeXBuilder`. Although this is perfectly feasible, such installations are often big, and in general LaTeX requires careful configuration in some cases, From 0565da65129c3b1e7d49dc453c5ef869a34fe792 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 20 Jul 2021 00:25:32 +0900 Subject: [PATCH 8/8] Fix testcases that failed in python3.10.0b4 --- tests/test_util_inspect.py | 7 ++----- tests/test_util_typing.py | 8 ++++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 2f805a87a..49a987159 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -215,11 +215,8 @@ def test_signature_annotations(): # optional union sig = inspect.signature(f20) - if sys.version_info < (3, 7): - assert stringify_signature(sig) in ('() -> Optional[Union[int, str]]', - '() -> Optional[Union[str, int]]') - else: - assert stringify_signature(sig) == '() -> Optional[Union[int, str]]' + assert stringify_signature(sig) in ('() -> Optional[Union[int, str]]', + '() -> Optional[Union[str, int]]') # Any sig = inspect.signature(f14) diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index 424715b39..d0c511d25 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -142,9 +142,9 @@ def test_restify_type_Literal(): @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 | None) == ":class:`int` | :obj:`None`" # type: ignore assert restify(int | str) == ":class:`int` | :class:`str`" # type: ignore - assert restify(int | str | None) == "Optional[:class:`int` | :class:`str`]" # type: ignore + assert restify(int | str | None) == ":class:`int` | :class:`str` | :obj:`None`" # type: ignore def test_restify_broken_type_hints(): @@ -253,9 +253,9 @@ def test_stringify_type_Literal(): @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 | None) == "int | None" # type: ignore assert stringify(int | str) == "int | str" # type: ignore - assert stringify(int | str | None) == "Optional[int | str]" # type: ignore + assert stringify(int | str | None) == "int | str | None" # type: ignore def test_stringify_broken_type_hints():