Basic Usage¶
Introduction¶
While Sphinx Autodoc can process simple type annotations, it falls down when
encountering Annotated
types with rich metadata. The dynadoc
library
bridges this gap by extracting documentation from PEP 727 Doc
objects,
Raises
specifications, and other annotation metadata to generate
comprehensive docstrings.
>>> import dynadoc
>>> from typing import Annotated
Simple Function Documentation¶
The most basic use case is decorating a function with type annotations that
include Doc
objects:
>>> @dynadoc.with_docstring( )
... def validate_api_response(
... response_data: Annotated[ dict, dynadoc.Doc( "Raw API response data" ) ],
... schema_name: Annotated[ str, dynadoc.Doc( "Name of validation schema to use" ) ],
... strict_mode: Annotated[ bool, dynadoc.Doc( "Whether to enforce strict validation" ) ],
... timeout: Annotated[ float, dynadoc.Doc( "Validation timeout in seconds" ) ],
... ) -> Annotated[ bool, dynadoc.Doc( "True if response is valid" ) ]:
... ''' Validate API response data against specified schema. '''
... return True
...
>>> print( validate_api_response.__doc__ )
Validate API response data against specified schema.
:argument response_data: Raw API response data
:type response_data: dict
:argument schema_name: Name of validation schema to use
:type schema_name: str
:argument strict_mode: Whether to enforce strict validation
:type strict_mode: bool
:argument timeout: Validation timeout in seconds
:type timeout: float
:returns: True if response is valid
:rtype: bool
Exception Documentation¶
Functions that raise exceptions can document them using Raises
annotations:
>>> @dynadoc.with_docstring( )
... def parse_config_file(
... filepath: Annotated[ str, dynadoc.Doc( "Path to configuration file" ) ],
... encoding: Annotated[ str, dynadoc.Doc( "File encoding to use" ) ] = "utf-8",
... ) -> Annotated[
... dict,
... dynadoc.Doc( "Parsed configuration data" ),
... dynadoc.Raises( FileNotFoundError, "When config file does not exist" ),
... dynadoc.Raises( ValueError, "When file contains invalid JSON/YAML" ),
... ]:
... if not filepath.endswith( ( '.json', '.yaml', '.yml' ) ):
... raise ValueError( "Unsupported file format" )
... return { }
...
>>> print( parse_config_file.__doc__ )
:argument filepath: Path to configuration file
:type filepath: str
:argument encoding: File encoding to use
:type encoding: str
:returns: Parsed configuration data
:rtype: dict
:raises FileNotFoundError: When config file does not exist
:raises ValueError: When file contains invalid JSON/YAML
Multiple Exception Types¶
When a function can raise multiple exception types for the same condition, you
can specify them as a sequence in a single Raises
annotation. This allows
multiple exceptions to share the same description:
>>> @dynadoc.with_docstring( )
... def download_file(
... url: Annotated[ str, dynadoc.Doc( "URL of file to download" ) ],
... output_path: Annotated[ str, dynadoc.Doc( "Local path to save file" ) ]
... ) -> Annotated[
... int,
... dynadoc.Doc( "Number of bytes downloaded" ),
... dynadoc.Raises(
... [ ConnectionError, TimeoutError ],
... "When network connection fails"
... ),
... dynadoc.Raises(
... [ PermissionError, OSError ],
... "When file cannot be saved to output path"
... ),
... ]:
... ''' Download file from URL to local filesystem. '''
... return 0
...
>>> print( download_file.__doc__ )
Download file from URL to local filesystem.
:argument url: URL of file to download
:type url: str
:argument output_path: Local path to save file
:type output_path: str
:returns: Number of bytes downloaded
:rtype: int
:raises ConnectionError: When network connection fails
:raises TimeoutError: When network connection fails
:raises PermissionError: When file cannot be saved to output path
:raises OSError: When file cannot be saved to output path
Notice how each exception type in the sequence gets its own :raises:
line
with the same description, allowing comprehensive documentation of all possible
exception scenarios.
Preserving Existing Docstrings¶
By default, dynadoc
preserves any existing docstring content and appends
the generated documentation:
>>> @dynadoc.with_docstring( )
... def transform_data(
... raw_data: Annotated[ list[ dict ], dynadoc.Doc( "Input data records" ) ],
... normalize: Annotated[ bool, dynadoc.Doc( "Whether to normalize values" ) ] = True,
... ) -> Annotated[ list[ dict ], dynadoc.Doc( "Transformed data records" ) ]:
... ''' Transform raw data records with optional normalization.
...
... This function demonstrates how dynadoc preserves existing
... docstring content while adding parameter documentation.
...
... The transformation includes data cleaning, type conversion,
... and optional value normalization.
... '''
... result = [ { k: str( v ).strip( ) for k, v in record.items( ) } for record in raw_data ]
... if normalize:
... result = [ { k: v.lower( ) if isinstance( v, str ) else v for k, v in record.items( ) } for record in result ]
... return result
...
>>> print( transform_data.__doc__ )
Transform raw data records with optional normalization.
This function demonstrates how dynadoc preserves existing
docstring content while adding parameter documentation.
The transformation includes data cleaning, type conversion,
and optional value normalization.
:argument raw_data: Input data records
:type raw_data: list[ dict ]
:argument normalize: Whether to normalize values
:type normalize: bool
:returns: Transformed data records
:rtype: list[ dict ]
To replace existing docstrings instead of preserving them, use preserve = False
:
>>> @dynadoc.with_docstring( preserve = False )
... def calculate_checksum(
... data: Annotated[ bytes, dynadoc.Doc( "Data to checksum" ) ],
... algorithm: Annotated[ str, dynadoc.Doc( "Hash algorithm to use" ) ] = "sha256",
... ) -> Annotated[ str, dynadoc.Doc( "Hexadecimal checksum string" ) ]:
... ''' This docstring will be replaced. '''
... return "abc123"
...
>>> print( calculate_checksum.__doc__ )
:argument data: Data to checksum
:type data: bytes
:argument algorithm: Hash algorithm to use
:type algorithm: str
:returns: Hexadecimal checksum string
:rtype: str
Optional Parameters and Defaults¶
The library handles optional parameters and default values appropriately:
>>> @dynadoc.with_docstring( )
... def create_api_client(
... base_url: Annotated[ str, dynadoc.Doc( "Base URL for API requests" ) ],
... api_key: Annotated[ str, dynadoc.Doc( "Authentication API key" ) ],
... timeout: Annotated[ int | None, dynadoc.Doc( "Request timeout in seconds" ) ] = None,
... verify_ssl: Annotated[ bool, dynadoc.Doc( "Whether to verify SSL certificates" ) ] = True,
... ) -> Annotated[ dict, dynadoc.Doc( "Configured API client instance" ) ]:
... client_config = { "base_url": base_url, "api_key": api_key, "verify_ssl": verify_ssl }
... if timeout is not None:
... client_config[ "timeout" ] = timeout
... return client_config
...
>>> print( create_api_client.__doc__ )
:argument base_url: Base URL for API requests
:type base_url: str
:argument api_key: Authentication API key
:type api_key: str
:argument timeout: Request timeout in seconds
:type timeout: int | None
:argument verify_ssl: Whether to verify SSL certificates
:type verify_ssl: bool
:returns: Configured API client instance
:rtype: dict
Rendering Styles¶
The default renderer produces Sphinx-compatible reStructuredText with legible spacing. For more compact output following PEP 8 style guidelines:
>>> from dynadoc.renderers import sphinxad
>>> def compact_renderer( obj, info, context ):
... return sphinxad.produce_fragment( obj, info, context, style = sphinxad.Style.Pep8 )
>>>
>>> @dynadoc.with_docstring( renderer = compact_renderer )
... def process_metadata(
... data_map: Annotated[ dict[ str, list[ int ] ], dynadoc.Doc( "Mapping of identifiers to value lists" ) ],
... ) -> Annotated[ dict[ str, int ], dynadoc.Doc( "Processed summary data" ) ]:
... return { k: sum( v ) for k, v in data_map.items( ) }
...
>>> print( process_metadata.__doc__ )
:argument data_map: Mapping of identifiers to value lists
:type data_map: dict[str, list[int]]
:returns: Processed summary data
:rtype: dict[str, int]
Compare this compact PEP 8 style (dict[str, list[int]]
) with the default
legible style (dict[ str, list[ int ] ]
) used in all previous examples.
The difference is most apparent with complex generic types.