Advanced Topics

Introduction

This section covers advanced usage patterns for dynadoc, including custom renderers, sophisticated annotation patterns, error handling strategies, performance optimization, and extension strategies for specialized documentation needs.

>>> import dynadoc
>>> import dynadoc.xtnsapi as xtnsapi
>>> from typing import Annotated

Custom Renderers

While the built-in Sphinx renderer handles most use cases, you can create custom renderers for different output formats or specialized documentation styles:

>>> def markdown_renderer( possessor, informations, context ):
...     ''' Custom renderer that outputs Markdown format. '''
...     lines = [ ]
...
...     for info in informations:
...         if isinstance( info, xtnsapi.ArgumentInformation ):
...             name = info.name
...             type_name = info.annotation.__name__ if hasattr( info.annotation, '__name__' ) else str( info.annotation )
...             description = info.description or 'No description'
...             lines.append( f"- **{name}** (`{type_name}`): {description}" )
...         elif isinstance( info, xtnsapi.ReturnInformation ):
...             type_name = info.annotation.__name__ if hasattr( info.annotation, '__name__' ) else str( info.annotation )
...             description = info.description or 'No description'
...             lines.append( f"- **Returns** (`{type_name}`): {description}" )
...
...     return '\n'.join( lines )
>>>
>>> @dynadoc.with_docstring( renderer = markdown_renderer )
... def process_api_request(
...     endpoint: Annotated[ str, dynadoc.Doc( "API endpoint URL to process" ) ],
...     timeout: Annotated[ float, dynadoc.Doc( "Request timeout in seconds" ) ] = 30.0,
... ) -> Annotated[ dict, dynadoc.Doc( "Processed API response data" ) ]:
...     ''' Process API request with custom Markdown rendering. '''
...     return { }
>>>
>>> print( process_api_request.__doc__ )
Process API request with custom Markdown rendering.

- **endpoint** (`str`): API endpoint URL to process
- **timeout** (`float`): Request timeout in seconds
- **Returns** (`dict`): Processed API response data

Error Handling Strategies

Different error handling strategies suit different development workflows and deployment environments:

Basic Error Handling

The default behavior issues warnings for most problems but continues processing:

>>> def basic_notifier( level: str, message: str ) -> None:
...     ''' Basic notifier that prints warnings and errors. '''
...     print( f"[DYNADOC {level.upper()}] {message}" )
>>>
>>> basic_context = dynadoc.produce_context( notifier = basic_notifier )
>>>
>>> # Use with missing fragment reference to see error handling
>>> fragments = { 'valid_key': 'This fragment exists' }
>>>
>>> @dynadoc.with_docstring( context = basic_context, table = fragments )
... def example_function(
...     param1: Annotated[ str, dynadoc.Fname( 'valid_key' ) ],
...     param2: Annotated[ int, dynadoc.Fname( 'missing_key' ) ],
... ) -> None:
...     ''' Example function with missing fragment reference. '''
...     pass
[DYNADOC ERROR] Fragment 'missing_key' not in provided table.

The function processes successfully despite the missing fragment, issuing a clear error message about the problem.

Strict Error Handling

For development environments where you want immediate feedback on documentation issues:

>>> def strict_notifier( level: str, message: str ) -> None:
...     ''' Strict error handling that fails fast on any issues. '''
...     if level == 'error':
...         raise ValueError( f"Documentation error: {message}" )
...     elif level == 'admonition':
...         print( f"WARNING: {message}" )
>>>
>>> strict_context = dynadoc.produce_context( notifier = strict_notifier )

This approach catches documentation problems early in development, ensuring clean documentation before deployment.

Development-Friendly Error Handling

For development workflows that need detailed debugging information:

>>> def development_notifier( level: str, message: str ) -> None:
...     ''' Development-friendly error handling with detailed output. '''
...     import sys
...     import traceback
...     timestamp = "2024-01-01 12:00:00"  # In real code, use datetime.now()
...     print( f"[{timestamp}] DYNADOC {level.upper()}: {message}", file = sys.stderr )
...     if level == 'error':
...         # In real development, you might want stack traces
...         print( f"  Context: Processing documentation generation", file = sys.stderr )
>>>
>>> dev_context = dynadoc.produce_context( notifier = development_notifier )

This provides rich context for debugging documentation generation issues during development.

Production Error Handling

For production environments where you want to log issues but never interrupt application startup:

>>> def production_notifier( level: str, message: str ) -> None:
...     ''' Production error handling that logs but doesn't interrupt. '''
...     # In real code, you'd use proper logging
...     if level == 'error':
...         # Log to error tracking system (e.g., Sentry, CloudWatch)
...         pass  # logger.error(f"Dynadoc error: {message}")
...     elif level == 'admonition':
...         # Log as warning
...         pass  # logger.warning(f"Dynadoc warning: {message}")
>>>
>>> prod_context = dynadoc.produce_context( notifier = production_notifier )

This ensures that documentation issues never prevent application deployment, while still capturing problems for later investigation.

Custom Introspection Limiters

Custom introspection limiters provide fine-grained control over how deeply dynadoc introspects different objects. Limiters are functions that can modify introspection behavior based on the specific object being documented:

>>> def depth_limiter(
...     objct: object,
...     introspection: dynadoc.IntrospectionControl
... ) -> dynadoc.IntrospectionControl:
...     ''' Limits introspection depth for nested classes. '''
...     import inspect
...
...     # If this is a nested class, disable further class introspection
...     if inspect.isclass( objct ) and '.' in getattr( objct, '__qualname__', '' ):
...         limit = dynadoc.IntrospectionLimit(
...             targets_exclusions = dynadoc.IntrospectionTargets.Class
...         )
...         return introspection.with_limit( limit )
...
...     return introspection
>>>
>>> # Configure introspection with the custom limiter
>>> introspection_with_limiter = dynadoc.IntrospectionControl(
...     targets = dynadoc.IntrospectionTargetsSansModule,
...     limiters = ( depth_limiter, )
... )
>>>
>>> @dynadoc.with_docstring( introspection = introspection_with_limiter )
... class DataProcessingPipeline:
...     ''' Data processing pipeline with nested configuration classes. '''
...
...     max_workers: Annotated[ int, dynadoc.Doc( "Maximum number of worker threads" ) ]
...
...     class ProcessingConfig:
...         ''' Processing configuration that should have limited introspection. '''
...         batch_size: Annotated[ int, dynadoc.Doc( "Number of items per batch" ) ]
>>>
>>> print( DataProcessingPipeline.__doc__ )
Data processing pipeline with nested configuration classes.

:ivar max_workers: Maximum number of worker threads
:vartype max_workers: int

The depth limiter prevents recursive introspection of nested classes, avoiding potential infinite loops and controlling documentation scope for complex class hierarchies. Similar limiters can be created for performance optimization, domain-specific documentation policies, or handling special object types.

Visibility Control

The dynadoc library provides multiple layers of visibility control to determine which attributes appear in documentation. Understanding these rules helps you create clean, comprehensive API documentation.

Attribute Visibility Rules

The library uses intuitive default visibility rules:

  • Public attributes (not starting with _) are always visible

  • Private attributes are visible only if they have documentation

  • Explicit visibility annotations override these rules

This design reflects a key principle: if you document a private attribute, you’re signaling it’s important enough for users to know about.

>>> @dynadoc.with_docstring( )
... class ConfigurationService:
...     ''' Demonstrates default visibility behavior. '''
...
...     # Public, documented - visible
...     api_endpoint: Annotated[ str, dynadoc.Doc( "Primary API endpoint URL" ) ]
...
...     # Public, undocumented - still visible (public API)
...     retry_count: int
...
...     # Private, documented - visible (intentionally exposed)
...     _debug_enabled: Annotated[ bool, dynadoc.Doc( "Internal debug flag for troubleshooting" ) ]
...
...     # Private, undocumented - hidden (truly internal)
...     _internal_cache: dict
...
>>> print( ConfigurationService.__doc__ )
Demonstrates default visibility behavior.

:ivar api_endpoint: Primary API endpoint URL
:vartype api_endpoint: str
:ivar retry_count:
:vartype retry_count: int
:ivar _debug_enabled: Internal debug flag for troubleshooting
:vartype _debug_enabled: bool

Notice that _internal_cache doesn’t appear because it lacks documentation, indicating it’s truly internal.

Explicit Visibility Control

For fine-grained control, use Visibilities annotations to override the default behavior:

>>> @dynadoc.with_docstring( )
... class CacheManager:
...     ''' Demonstrates explicit visibility control. '''
...
...     # Force visibility for private attribute
...     _cache_stats: Annotated[
...         dict,
...         dynadoc.Doc( "Internal cache statistics for monitoring" ),
...         dynadoc.Visibilities.Reveal
...     ]
...
...     # Hide implementation detail from documentation
...     buffer_implementation: Annotated[
...         str,
...         dynadoc.Doc( "Internal buffer implementation details" ),
...         dynadoc.Visibilities.Conceal
...     ]
...
...     # Normal public attribute
...     cache_size: Annotated[ int, dynadoc.Doc( "Maximum number of cached items" ) ]
...
>>> print( CacheManager.__doc__ )
Demonstrates explicit visibility control.

:ivar _cache_stats: Internal cache statistics for monitoring
:vartype _cache_stats: dict
:ivar cache_size: Maximum number of cached items
:vartype cache_size: int

The Visibilities annotations take precedence over both default rules and custom visibility deciders.

Custom Visibility Deciders

For advanced scenarios, you can implement custom visibility logic that replaces the default rules (but is still overridden by explicit Visibilities annotations):

>>> def api_visibility_decider( possessor, name: str, annotation, description ):
...     ''' Custom visibility for API documentation. '''
...     import inspect
...
...     # Always hide private names
...     if name.startswith( '_' ):
...         return False
...
...     # For modules, respect __all__ if present
...     if inspect.ismodule( possessor ):
...         all_list = getattr( possessor, '__all__', None )
...         if all_list is not None:
...             return name in all_list
...
...     # Only show documented public attributes
...     return bool( description )
>>>
>>> api_context = dynadoc.produce_context(
...     visibility_decider = api_visibility_decider
... )
>>>
>>> @dynadoc.with_docstring( context = api_context )
... class PublicAPIClient:
...     ''' API client with strict visibility rules. '''
...
...     documented_endpoint: Annotated[ str, dynadoc.Doc( "Public API endpoint" ) ]
...     undocumented_setting: str  # No documentation
...     _private_config: Annotated[ str, dynadoc.Doc( "Private but documented" ) ]
>>>
>>> print( PublicAPIClient.__doc__ )
API client with strict visibility rules.

:ivar documented_endpoint: Public API endpoint
:vartype documented_endpoint: str

The custom decider hides both undocumented_setting (no description) and _private_config (private name), creating stricter API documentation.

Controlling Attribute Value Display

Sometimes you want to control how attribute values appear in documentation, especially for complex objects that don’t render well or when you want to provide more descriptive information. The Default annotation provides control over value display:

>>> @dynadoc.with_docstring( )
... class ServiceConfiguration:
...     ''' Service configuration with controlled value display. '''
...
...     # Normal value display
...     service_version: Annotated[ str, dynadoc.Doc( "Current service version" ) ] = "v2.1"
...
...     # Suppress value display for function objects
...     error_handler: Annotated[
...         callable,
...         dynadoc.Doc( "Default error handling function" ),
...         dynadoc.Default( mode = dynadoc.ValuationModes.Suppress )
...     ] = lambda error: print( f"Error: {error}" )
...
...     # Use surrogate description instead of actual value
...     database_config: Annotated[
...         dict,
...         dynadoc.Doc( "Database connection configuration" ),
...         dynadoc.Default(
...             mode = dynadoc.ValuationModes.Surrogate,
...             surrogate = "Loaded from environment variables"
...         )
...     ] = { "host": "localhost", "database": "prod_db" }
...
>>> print( ServiceConfiguration.__doc__ )
Service configuration with controlled value display.

:ivar service_version: Current service version
:vartype service_version: str
:ivar error_handler: Default error handling function
:vartype error_handler: callable
:ivar database_config: Database connection configuration
:vartype database_config: dict

The ValuationModes provide three options:

  • Accept (default): Show the actual attribute value

  • Suppress: Hide the value entirely (useful for function objects, complex instances)

  • Surrogate: Display an alternative description instead of the actual value

This is particularly useful when you want to document the purpose of attributes without exposing implementation details like function memory addresses or when you want to provide more meaningful descriptions than the raw data structure.

Stringified Annotations Support

The dynadoc library gracefully handles stringified type annotations, which commonly occur with forward references in self-referential or mutually dependent classes. Instead of attempting to resolve these strings (which could fail), the library processes them robustly.

This feature is particularly useful for classes that reference themselves:

>>> @dynadoc.with_docstring( )
... class TreeNode:
...     ''' Binary tree node with parent and child references. '''
...
...     value: Annotated[ int, dynadoc.Doc( "Node value" ) ]
...     parent: Annotated[ 'TreeNode | None', dynadoc.Doc( "Parent node reference" ) ]
...     left: Annotated[ 'TreeNode | None', dynadoc.Doc( "Left child node" ) ]
...     right: Annotated[ 'TreeNode | None', dynadoc.Doc( "Right child node" ) ]
>>>
>>> print( TreeNode.__doc__ )
Binary tree node with parent and child references.

:ivar value: Node value
:vartype value: int
:ivar parent: Parent node reference
:vartype parent: TreeNode | None
:ivar left: Left child node
:vartype left: TreeNode | None
:ivar right: Right child node
:vartype right: TreeNode | None

Note how the stringified forward references like 'TreeNode | None' are handled gracefully. The dynadoc library extracts clean type names from forward references, ensuring robust documentation generation even with self-referential type dependencies.

Performance Optimization

For large codebases, strategic configuration can improve documentation generation performance:

# Minimal introspection for faster processing
fast_introspection = dynadoc.IntrospectionControl(
    targets = dynadoc.IntrospectionTargets.Function  # Only functions
)

# Lightweight context with minimal processing
fast_context = dynadoc.produce_context(
    notifier = lambda level, msg: None,  # Silent operation
    fragment_rectifier = lambda fragment, source: fragment  # No processing
)

# Apply to modules without recursion
dynadoc.assign_module_docstring(
    __name__,
    context = fast_context,
    introspection = fast_introspection
)

Performance considerations:

  • Limit introspection targets to only what you need

  • Avoid deep recursion in large package hierarchies

  • Use simple renderers for better performance

  • Cache contexts when documenting multiple modules

  • Profile documentation generation for bottlenecks

Extension Patterns

Building extensions on top of dynadoc enables specialized functionality. Here are some practical patterns for extending the library:

Domain-Specific Renderers

Create renderers tailored to specific domains or output formats:

def rest_api_renderer( possessor, informations, context ):
    ''' Specialized renderer for REST API documentation. '''

    lines = [ ]
    for info in informations:
        if isinstance( info, ArgumentInformation ):
            # Format API parameters with HTTP context
            if info.name in ( 'method', 'endpoint', 'headers' ):
                lines.append( f":http-param {info.name}: {info.description}" )
            else:
                lines.append( f":param {info.name}: {info.description}" )
        elif isinstance( info, ReturnInformation ):
            # Format API responses
            lines.append( f":returns: {info.description}" )
            lines.append( f":response-type: {format_type( info.annotation )}" )

    return '\n'.join( lines )

Configuration Management Extensions

Build configuration systems on top of dynadoc for consistent documentation across teams:

class DocumentationStandards:
    ''' Company-wide documentation standards. '''

    @staticmethod
    def create_api_context( ):
        return dynadoc.produce_context(
            notifier = standards_notifier,
            fragment_rectifier = corporate_rectifier,
            visibility_decider = public_api_visibility
        )

    @staticmethod
    def create_internal_context( ):
        return dynadoc.produce_context(
            notifier = development_notifier,
            fragment_rectifier = relaxed_rectifier,
            visibility_decider = internal_visibility
        )

Best Practices for Advanced Usage

When implementing advanced dynadoc patterns:

Design for maintainability - Keep custom renderers and configurations simple and well-documented.

Test thoroughly - Advanced configurations can have subtle interactions, so comprehensive testing is essential.

Profile performance - Custom renderers and complex introspection can impact build times, especially in large projects.

Document your extensions - Custom patterns should be well-documented for team members and future maintenance.

Consider backward compatibility - When building on dynadoc, ensure your extensions can adapt to library updates.

Start simple and evolve - Begin with basic configurations and add complexity only when needed to solve specific problems.

Use error handling strategically - Choose error handling approaches that match your development workflow and deployment requirements.

Leverage visibility control - Use the multiple layers of visibility control to create clean, focused API documentation that serves your users’ needs.