diff --git a/.vscode/settings.json b/.vscode/settings.json
index 371c316..aa4016e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -18,6 +18,9 @@
"editor.defaultFormatter": "ms-python.black-formatter"
},
"cSpell.words": [
- "pysqlsync"
+ "autodoc",
+ "Levente",
+ "pysqlsync",
+ "setuptools"
]
}
diff --git a/README.md b/README.md
index cb3388f..f4344e9 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ This extension to Sphinx [autodoc](https://www.sphinx-doc.org/en/master/usage/ex
1. Ensure that you have type hints in all your classes, functions and methods.
2. Add description to your classes, functions and methods as a doc-string.
3. Use `:param name: text` to assign a description to member variables and function parameters.
-4. Register `DocstringProcessor` to the event `autodoc-process-docstring` in Sphinx's `conf.py`.
+4. Register `Processor` to the events `autodoc-process-docstring` and `autodoc-before-process-signature` in Sphinx's `conf.py`.
5. Enjoy how type information is automatically injected in the doc-string on `make html`.
## Motivation
@@ -49,3 +49,12 @@ def send_message(
:returns: The message identifier.
"""
```
+
+## Features
+
+* Data-class member variables are published if they have a corresponding `:param ...:` in the class-level doc-string.
+* All enumeration members are published, even if they lack a description.
+* Magic methods (e.g. `__eq__`) are published if they have a doc-string.
+* Multi-line code blocks in doc-strings are converted to syntax-highlighted monospace text.
+* Primary keys in entity classes have extra visuals (e.g. with a symbol 🔑). See [pysqlsync](https://github.com/hunyadi/pysqlsync) for how to define an entity class (using the `@dataclass` syntax) with a primary key (with the type hint `PrimaryKey[T]`).
+* Type aliases are substituted even if [Postponed Evaluation of Annotations (PEP 563)](https://peps.python.org/pep-0563/) is turned off.
diff --git a/doc/conf.py b/doc/conf.py
index 0660751..f7bb1a8 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -6,7 +6,7 @@
from sphinx.application import Sphinx
from strong_typing.core import JsonType
-from sphinx_doc.autodoc import DocstringProcessor, skip_member
+from sphinx_doc.autodoc import Processor, skip_member
sys.path.insert(0, os.path.abspath("."))
@@ -53,6 +53,7 @@
add_module_names = False
autoclass_content = "class"
autodoc_class_signature = "separated"
+autodoc_member_order = "bysource"
autodoc_default_options = {
"exclude-members": "__init__",
"member-order": "bysource",
@@ -76,8 +77,7 @@ class json:
def setup(app: Sphinx) -> None:
- app.connect(
- "autodoc-process-docstring",
- DocstringProcessor(type_transform={JsonType: json}),
- )
+ processor = Processor(type_transform={JsonType: json})
+ app.connect("autodoc-process-docstring", processor.process_docstring)
+ app.connect("autodoc-before-process-signature", processor.process_signature)
app.connect("autodoc-skip-member", skip_member)
diff --git a/doc/example.py b/doc/example.py
index 39f3eae..a764459 100644
--- a/doc/example.py
+++ b/doc/example.py
@@ -1,5 +1,3 @@
-from __future__ import annotations
-
import enum
import sys
from dataclasses import dataclass
@@ -7,6 +5,7 @@
from typing import Union
from pysqlsync.model.key_types import PrimaryKey, Unique
+from strong_typing.auxiliary import int8, int16, uint32, uint64
from strong_typing.core import JsonType, Schema
if sys.version_info > (3, 10):
@@ -42,6 +41,25 @@ class SampleClass:
"""
A data-class with several member variables.
+ Class doc-strings can include code blocks.
+
+ A code block formatted as HTML:
+
+ ```html
+
+
+ A paragraph.
+
+
+ ```
+
+ A code block formatted as Python:
+
+ ```python
+ if sys.version_info > (3, 10):
+ SimpleType = bool | int | float | str
+ ```
+
:param boolean: A member variable of type `bool`.
:param integer: A member variable of type `int`.
:param double: A member variable of type `float`.
@@ -75,7 +93,7 @@ def __gt__(self, other: "SampleClass") -> bool:
return self.integer > other.integer
- def to_json(self) -> JsonType:
+ def to_json(self) -> "JsonType":
"""
Serializes the data to JSON.
@@ -84,7 +102,7 @@ def to_json(self) -> JsonType:
...
@staticmethod
- def from_json(obj: JsonType) -> "SampleClass":
+ def from_json(obj: "JsonType") -> "SampleClass":
"""
De-serializes the data from JSON.
@@ -109,6 +127,23 @@ class DerivedClass(SampleClass):
schema: Schema
+@dataclass
+class FixedWidthIntegers:
+ """
+ Fixed-width integers have a compact representation.
+
+ :param integer8: A signed integer of 8 bits.
+ :param integer16: A signed integer of 16 bits.
+ :param unsigned32: An unsigned integer of 32 bits.
+ :param unsigned64: An unsigned integer of 64 bits.
+ """
+
+ integer8: int8
+ integer16: int16
+ unsigned32: uint32
+ unsigned64: uint64
+
+
@dataclass
class LookupTable:
"""
diff --git a/doc/index.rst b/doc/index.rst
index 025c165..feb11fd 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -1,6 +1,21 @@
Enrich Sphinx documentation with Python type information
========================================================
+This extension to Sphinx `autodoc `_ enriches class
+member variable and function parameter lists with type information extracted from Python type annotations.
+
+This page is a demonstration of the tool's capabilities; visit the `project page `_
+for more information.
+
+Usage
+-----
+
+1. Ensure that you have type hints in all your classes, functions and methods.
+2. Add description to your classes, functions and methods as a doc-string.
+3. Use ``:param name: text`` to assign a description to member variables and function parameters.
+4. Register ``Processor`` to the events ``autodoc-process-docstring`` and ``autodoc-before-process-signature`` in Sphinx's ``conf.py``.
+5. Enjoy how type information is automatically injected in the doc-string on ``make html``.
+
.. toctree::
:maxdepth: 2
:caption: Contents:
diff --git a/doc/objects.rst b/doc/objects.rst
index 7afdc9a..d690ca6 100644
--- a/doc/objects.rst
+++ b/doc/objects.rst
@@ -6,5 +6,8 @@
Types in the module
-------------------
+This page showcases classes and functions extracted from the module
+`example `_.
+
.. automodule:: example
- :members: EnumType, MyException, PlainClass, SampleClass, DerivedClass, LookupTable, EntityTable, send_message
+ :members: EnumType, MyException, PlainClass, SampleClass, DerivedClass, FixedWidthIntegers, LookupTable, EntityTable, send_message
diff --git a/sphinx_doc/autodoc.py b/sphinx_doc/autodoc.py
index 6178df6..a672276 100644
--- a/sphinx_doc/autodoc.py
+++ b/sphinx_doc/autodoc.py
@@ -3,6 +3,7 @@
import inspect
import re
import sys
+import types
import typing
from dataclasses import dataclass
from typing import Annotated, Any, Callable, Optional, TypeVar, Union
@@ -46,7 +47,9 @@ def __init__(
def __call__(self, arg_type: TypeLike) -> TypeLike:
if isinstance(arg_type, str):
- raise TypeError("expected: evaluated type; got: `str`")
+ raise TypeError(
+ f"expected: evaluated type; got: `str` with value: {arg_type}"
+ )
mapped_type = self.type_transform.get(
python_type_to_str(arg_type, use_union_operator=True)
@@ -84,18 +87,20 @@ class MissingFieldError(SphinxError):
T = TypeVar("T")
-class DocstringProcessor:
+class Processor:
"""
Ensures a compact yet comprehensive documentation is generated for classes and enumerations.
- This class should be registered with a call to
+ This class should be registered as follows:
```
- app.connect("autodoc-process-docstring", DocstringProcessor(...))
+ p = Processor(...)
+ app.connect("autodoc-process-docstring", p.process_docstring)
+ app.connect("autodoc-before-process-signature", p.process_signature)
```
"""
- symbols: Symbols
- type_transform: TypeTransform
+ _symbols: Symbols
+ _type_transform: TypeTransform
def __init__(
self,
@@ -110,19 +115,19 @@ def __init__(
"""
if symbols is not None:
- self.symbols = symbols
+ self._symbols = symbols
else:
- self.symbols = Symbols()
+ self._symbols = Symbols()
if type_transform is not None:
- self.type_transform = TypeTransform(type_transform)
+ self._type_transform = TypeTransform(type_transform)
else:
- self.type_transform = TypeTransform()
+ self._type_transform = TypeTransform()
def _python_type_to_str(self, arg_type: TypeLike) -> str:
"""Emits a string representation of a Python type, with substitutions."""
- transformed_type = self.type_transform(arg_type)
+ transformed_type = self._type_transform(arg_type)
return python_type_to_str(transformed_type, use_union_operator=True)
def _process_object(
@@ -168,7 +173,7 @@ def _transform_field(self, field_type: TypeLike, text: str) -> tuple[str, str]:
return self._python_type_to_str(field_type), text
- def process_class(self, cls: type, lines: list[str]) -> None:
+ def _process_class(self, cls: type, lines: list[str]) -> None:
"""
Lists all fields of a plain class including field name, type and description string.
@@ -183,7 +188,9 @@ def process_class(self, cls: type, lines: list[str]) -> None:
}
self._process_object(cls.__name__, fields, lines, self._transform_field)
- def process_dataclass(self, cls: type[DataclassInstance], lines: list[str]) -> None:
+ def _process_dataclass(
+ self, cls: type[DataclassInstance], lines: list[str]
+ ) -> None:
"""
Lists all fields of a data-class including field name, type and description string.
@@ -218,16 +225,16 @@ def _transform_column(self, prop: FieldProperties, text: str) -> tuple[str, str]
# emit an emoji for SQL primary key and unique constraint
pieces: list[str] = []
if prop.is_primary:
- pieces.append(self.symbols.primary_key)
+ pieces.append(self._symbols.primary_key)
if prop.is_unique:
- pieces.append(self.symbols.unique_constraint)
+ pieces.append(self._symbols.unique_constraint)
pieces.append(text)
description = " ".join(pieces)
return field_type, description
- def process_table(self, cls: type[DataclassInstance], lines: list[str]) -> None:
+ def _process_table(self, cls: type[DataclassInstance], lines: list[str]) -> None:
"""
Lists all fields of an entity that translates to a SQL table.
@@ -242,11 +249,11 @@ def process_table(self, cls: type[DataclassInstance], lines: list[str]) -> None:
}
self._process_object(cls.__name__, fields, lines, self._transform_column)
- def process_function(self, func: Callable[..., Any], lines: list[str]) -> None:
+ def _process_function(self, func: Callable[..., Any], lines: list[str]) -> None:
params = typing.get_type_hints(func, include_extras=True)
self._process_object(func.__name__, params, lines, self._transform_field)
- def process_enum(
+ def _process_enum(
self, cls: type[enum.Enum], options: Options, lines: list[str]
) -> None:
"""
@@ -259,7 +266,7 @@ def process_enum(
options["undoc-members"] = True
return
- def process_codeblock(self, lines: list[str]) -> None:
+ def _process_codeblock(self, lines: list[str]) -> None:
"""
Replaces Markdown-style code blocks with Sphinx-style code blocks.
@@ -287,7 +294,7 @@ def process_codeblock(self, lines: list[str]) -> None:
lines[:] = target_lines
- def __call__(
+ def process_docstring(
self,
app: Sphinx,
what: str,
@@ -312,25 +319,60 @@ def __call__(
:param lines: The lines of the docstring.
"""
- self.process_codeblock(lines)
+ self._process_codeblock(lines)
if what == "class":
cls: type = typing.cast(type, obj)
if issubclass(cls, enum.Enum):
- self.process_enum(cls, options, lines)
+ self._process_enum(cls, options, lines)
elif is_dataclass_type(cls):
if dataclass_has_primary_key(cls):
- self.process_table(cls, lines)
+ self._process_table(cls, lines)
else:
- self.process_dataclass(cls, lines)
+ self._process_dataclass(cls, lines)
elif inspect.isclass(cls):
- self.process_class(cls, lines)
+ self._process_class(cls, lines)
elif what == "exception":
exc = typing.cast(type[Exception], obj)
- self.process_class(exc, lines)
+ self._process_class(exc, lines)
elif what in ["function", "method"]:
func = typing.cast(Callable[..., Any], obj)
- self.process_function(func, lines)
+ self._process_function(func, lines)
+
+ def _transform_param(
+ self, param_type: TypeLike, module: types.ModuleType
+ ) -> TypeLike:
+ """
+ Maps type hints for function parameters.
+
+ :param param_type: The parameter type (including forward references) to transform.
+ :param module: The context in which to evaluate types.
+ """
+
+ param_type = evaluate_type(param_type, module)
+ if isinstance(param_type, str):
+ # may hit this path with `from __future__ import annotations`
+ param_type = evaluate_type(param_type, module)
+ return self._type_transform(param_type)
+
+ def process_signature(
+ self, app: Sphinx, obj: types.FunctionType, bound_method: bool
+ ) -> None:
+ """
+ Ensures a compact yet comprehensive documentation is generated for functions and bound functions.
+
+ The parameters are passed by `autodoc` in Sphinx.
+
+ :param app: The Sphinx application object.
+ :param obj: The function object itself.
+ :param bound_method: True if the object is a bound method, False otherwise.
+ """
+
+ module = sys.modules[obj.__module__]
+ obj.__annotations__.update(
+ (name, self._transform_param(param_type, module))
+ for name, param_type in obj.__annotations__.items()
+ )
def process_docstring(
@@ -345,8 +387,8 @@ def process_docstring(
```
"""
- processor = DocstringProcessor()
- processor(app, what, name, obj, options, lines)
+ processor = Processor()
+ processor.process_docstring(app, what, name, obj, options, lines)
def skip_member(