diff --git a/scanpydoc/elegant_typehints/formatting.py b/scanpydoc/elegant_typehints/formatting.py index 71b3d4c..8fcde93 100644 --- a/scanpydoc/elegant_typehints/formatting.py +++ b/scanpydoc/elegant_typehints/formatting.py @@ -93,7 +93,7 @@ def format_annotation(annotation: Type[Any], config: Config) -> Optional[str]: curframe = inspect.currentframe() calframe = inspect.getouterframes(curframe, 2) - if calframe[2].function == "process_docstring": + if "process_docstring" in {calframe[2].function, calframe[3].function}: return format_both(annotation, config) else: # recursive use return _format_full(annotation, config) @@ -102,6 +102,8 @@ def format_annotation(annotation: Type[Any], config: Config) -> Optional[str]: def format_both(annotation: Type[Any], config: Config) -> str: terse = _format_terse(annotation, config) full = _format_full(annotation, config) or _format_orig(annotation, config) + if terse == full: + return terse return f":annotation-terse:`{_escape(terse)}`\\ :annotation-full:`{_escape(full)}`" diff --git a/scanpydoc/rtd_github_links.py b/scanpydoc/rtd_github_links.py index f256353..c44e42f 100644 --- a/scanpydoc/rtd_github_links.py +++ b/scanpydoc/rtd_github_links.py @@ -24,7 +24,8 @@ ----- You can use the filter e.g. in `autosummary templates`_. -To configure the sphinx_rtd_theme_, override the ``autosummary/base.rst`` template like this: +To configure the sphinx_rtd_theme_, +override the ``autosummary/base.rst`` template like this: .. code:: restructuredtext @@ -32,9 +33,12 @@ {% extends "!autosummary/base.rst" %} -.. _autosummary templates: http://www.sphinx-doc.org/en/master/usage/extensions/autosummary.html#customizing-templates +.. _autosummary templates: \ + http://www.sphinx-doc.org/en/master/usage/extensions/autosummary.html#customizing-templates .. _sphinx_rtd_theme: https://sphinx-rtd-theme.readthedocs.io/en/latest/ """ +from __future__ import annotations + import inspect import sys from pathlib import Path, PurePosixPath @@ -74,7 +78,13 @@ def _get_obj_module(qualname: str) -> Tuple[Any, ModuleType]: mod = sys.modules[modname] obj = None for attr_name in attr_path: - thing = getattr(mod if obj is None else obj, attr_name) + try: + thing = getattr(mod if obj is None else obj, attr_name) + except AttributeError: + if is_dataclass(obj): + thing = next(f for f in fields(obj) if f.name == attr_name) + else: + raise if isinstance(thing, ModuleType): mod = thing else: @@ -159,3 +169,11 @@ def setup(app: Sphinx) -> Dict[str, Any]: DEFAULT_FILTERS["github_url"] = github_url return metadata + + +if True: # test data + from dataclasses import dataclass, field, fields, is_dataclass + + @dataclass + class _TestCls: + test_attr: dict[str, str] = field(default_factory=dict) diff --git a/tests/test_elegant_typehints.py b/tests/test_elegant_typehints.py index b4d52b1..7b4a994 100644 --- a/tests/test_elegant_typehints.py +++ b/tests/test_elegant_typehints.py @@ -80,12 +80,7 @@ def fn_test(s: str): :param s: Test """ - assert process_doc(fn_test) == [ - ":type s: " - r":annotation-terse:`:py:class:\`str\``\ " - r":annotation-full:`:py:class:\`str\``", - ":param s: Test", - ] + assert process_doc(fn_test) == [":type s: :py:class:`str`", ":param s: Test"] def test_defaults_simple(process_doc): @@ -97,20 +92,11 @@ def fn_test(s: str = "foo", n: None = None, i_: int = 1): """ assert process_doc(fn_test) == [ - ":type s: " - r":annotation-terse:`:py:class:\`str\``\ " - r":annotation-full:`:py:class:\`str\`` " - "(default: ``'foo'``)", + ":type s: :py:class:`str` (default: ``'foo'``)", ":param s: Test S", - ":type n: " - r":annotation-terse:`:py:obj:\`None\``\ " - r":annotation-full:`:py:obj:\`None\`` " - "(default: ``None``)", + ":type n: :py:obj:`None` (default: ``None``)", ":param n: Test N", - r":type i\_: " - r":annotation-terse:`:py:class:\`int\``\ " - r":annotation-full:`:py:class:\`int\`` " - "(default: ``1``)", + r":type i\_: :py:class:`int` (default: ``1``)", r":param i\_: Test I", ] @@ -335,13 +321,9 @@ def fn_test(): if not re.match("^:(rtype|param|annotation-(full|terse)):", l) ] assert lines == [ - r":return: foo : " - r":annotation-terse:`:py:class:\`str\``\ " - r":annotation-full:`:py:class:\`str\``", + r":return: foo : :py:class:`str`", " A foo!", - r" bar : " - r":annotation-terse:`:py:class:\`int\``\ " - r":annotation-full:`:py:class:\`int\``", + r" bar : :py:class:`int`", " A bar!", ] diff --git a/tests/test_rtd_github_links.py b/tests/test_rtd_github_links.py index f1e81f5..7c83281 100644 --- a/tests/test_rtd_github_links.py +++ b/tests/test_rtd_github_links.py @@ -1,3 +1,5 @@ +import dataclasses +from dataclasses import Field, dataclass, field from pathlib import Path import pytest @@ -30,3 +32,8 @@ def test_get_obj_module(): obj, mod = _get_obj_module("scanpydoc.get_version") assert obj is get_version.get_version assert mod is get_version + + +def test_get_obj_module_anntation(): + obj, mod = _get_obj_module("scanpydoc.rtd_github_links._TestCls.test_attr") + assert isinstance(obj, Field)