Configuration

Introduction

The dynadoc library provides extensive configuration options through Context objects and IntrospectionControl settings. These allow you to customize how annotations are processed, how errors are handled, which objects are recursively documented, and how the final documentation is rendered.

>>> import dynadoc
>>> from typing import Annotated

Context Objects

A Context object controls the overall behavior of documentation generation. The library provides sensible defaults, but you can customize various aspects:

>>> # Create a custom context with different error handling
>>> def custom_notifier( level: str, message: str ) -> None:
...     print( f"[{level.upper()}] {message}" )
>>>
>>> custom_context = dynadoc.produce_context( notifier = custom_notifier )
>>>
>>> # Use the custom context with missing fragment references
>>> fragments = { 'existing key': 'This fragment exists' }
>>>
>>> @dynadoc.with_docstring( context = custom_context, table = fragments )
... def example_function(
...     param: Annotated[ str, dynadoc.Fname( 'missing key' ) ]
... ) -> None:
...     ''' Example with missing fragment. '''
...     pass
[ERROR] Fragment 'missing key' not in provided table.

The custom notifier receives error notifications and can handle them differently than the default warning system.

Fragment Rectification

Fragment rectifiers clean and normalize documentation text based on its source. You can provide custom rectification logic:

>>> def custom_rectifier( fragment: str, source ) -> str:
...     ''' Custom rectifier that adds prefixes based on source. '''
...     match source:
...         case dynadoc.FragmentSources.Annotation:
...             return f"Parameter: {fragment}"
...         case dynadoc.FragmentSources.Docstring:
...             return fragment.strip( ).upper( )
...         case _:
...             return fragment
>>>
>>> custom_context = dynadoc.produce_context(
...     fragment_rectifier = custom_rectifier
... )
>>>
>>> @dynadoc.with_docstring( context = custom_context )
... def demo_function(
...     value: Annotated[ int, dynadoc.Doc( "input number" ) ]
... ) -> None:
...     ''' processes the input value. '''
...     pass
>>>
>>> print( demo_function.__doc__ )
PROCESSES THE INPUT VALUE.

:argument value: Parameter: input number
:type value: int

The custom rectifier transforms the docstring to uppercase and adds “Parameter:” prefix to argument descriptions.

Introspection Control

IntrospectionControl objects determine which objects are recursively documented and how deeply the introspection proceeds:

>>> # Create introspection control for comprehensive documentation
>>> comprehensive_introspection = dynadoc.IntrospectionControl(
...     targets = (
...         dynadoc.IntrospectionTargets.Function |
...         dynadoc.IntrospectionTargets.Class |
...         dynadoc.IntrospectionTargets.Descriptor
...     )
... )
>>>
>>> # Note: the above is equivalent to:
>>> # comprehensive_introspection = dynadoc.IntrospectionControl(
>>> #     targets = dynadoc.IntrospectionTargetsSansModule
>>> # )
>>>
>>> # Create a class with methods and properties
>>> @dynadoc.with_docstring( introspection = comprehensive_introspection )
... class APIClient:
...     ''' HTTP client for API communication. '''
...
...     def get_data(
...         self,
...         endpoint: Annotated[ str, dynadoc.Doc( "API endpoint to query" ) ]
...     ) -> Annotated[ dict, dynadoc.Doc( "Response data from API" ) ]:
...         ''' Retrieve data from API endpoint. '''
...         return { }
...
...     @property
...     def base_url( self ) -> Annotated[ str, dynadoc.Doc( "Base URL for API requests" ) ]:
...         ''' Get the base URL. '''
...         return "https://api.example.com"

The comprehensive introspection automatically documents both the method and property:

>>> print( APIClient.get_data.__doc__ )
Retrieve data from API endpoint.

:argument self:
:argument endpoint: API endpoint to query
:type endpoint: str
:returns: Response data from API
:rtype: dict
>>> print( APIClient.base_url.__doc__ )
Get the base URL.

:returns: Base URL for API requests
:rtype: str

Selective Introspection

You can create more targeted introspection controls for specific use cases:

>>> # Only document functions, not classes or descriptors
>>> functions_only = dynadoc.IntrospectionControl(
...     targets = dynadoc.IntrospectionTargets.Function
... )
>>>
>>> # Only document classes, not their methods
>>> classes_only = dynadoc.IntrospectionControl(
...     targets = dynadoc.IntrospectionTargets.Class
... )
>>>
>>> # Document everything except modules (avoids recursing into submodules)
>>> everything_but_modules = dynadoc.IntrospectionControl(
...     targets = dynadoc.IntrospectionTargetsSansModule
... )

These targeted controls allow fine-grained control over documentation scope, useful for different documentation strategies or performance considerations.

Class Introspection Controls

Class introspection behavior can be customized with additional settings:

>>> # Enable inheritance scanning for comprehensive class documentation
>>> inheritance_introspection = dynadoc.IntrospectionControl(
...     targets = dynadoc.IntrospectionTargets.Class,
...     class_control = dynadoc.ClassIntrospectionControl(
...         inheritance = True,
...         scan_attributes = True
...     )
... )

This configuration tells the introspector to: - Include inherited annotations from parent classes - Scan for attributes not covered by annotations

Custom Visibility Decisions

You can customize which attributes are visible in documentation by providing a custom visibility decider:

>>> def strict_visibility( possessor, name: str, annotation, description ) -> bool:
...     ''' Only show attributes that have descriptions. '''
...     return bool( description )
>>>
>>> strict_context = dynadoc.produce_context(
...     visibility_decider = strict_visibility
... )
>>>
>>> @dynadoc.with_docstring( context = strict_context )
... class APIConfiguration:
...     ''' Example of strict visibility with class attributes. '''
...
...     api_key: Annotated[ str, dynadoc.Doc( "This attribute has a description" ) ]
...     timeout: int  # No Doc annotation
>>>
>>> print( APIConfiguration.__doc__ )
Example of strict visibility with class attributes.

:ivar api_key: This attribute has a description
:vartype api_key: str

Only the parameter with a description appears in the documentation, even though both parameters have type annotations.

Annotation Resolution

The context can be configured with different globals and locals for resolving string annotations:

# Example: Custom resolution context for a module
custom_globals = {
    'CustomType': MyCustomClass,
    'SpecialAlias': dict[ str, Any ],
}

resolution_context = dynadoc.produce_context(
    resolver_globals = custom_globals,
    resolver_locals = local_scope_vars
)

dynadoc.assign_module_docstring(
    __name__,
    context = resolution_context
)

This allows proper resolution of forward references and string annotations that reference types not available in the default scope.

Combining Configurations

Multiple configuration options can be combined for sophisticated documentation strategies:

>>> # Comprehensive configuration example
>>> def production_notifier( level: str, message: str ) -> None:
...     if level == 'error':
...         raise RuntimeError( f"Documentation error: {message}" )
...     print( f"Warning: {message}" )
>>>
>>> def clean_rectifier( fragment: str, source ) -> str:
...     ''' Clean and normalize all fragments consistently. '''
...     cleaned = fragment.strip( )
...     if cleaned and not cleaned.endswith( '.' ):
...         cleaned += '.'
...     return cleaned
>>>
>>> production_context = dynadoc.produce_context(
...     notifier = production_notifier,
...     fragment_rectifier = clean_rectifier
... )
>>>
>>> production_introspection = dynadoc.IntrospectionControl(
...     targets = dynadoc.IntrospectionTargetsSansModule,
...     class_control = dynadoc.ClassIntrospectionControl( inheritance = True )
... )

This production configuration ensures strict error handling, consistent formatting, and comprehensive documentation coverage.

Practical Configuration Examples

Here are some practical configuration patterns for different scenarios:

API Documentation - Comprehensive coverage with strict error handling:

api_context = dynadoc.produce_context(
    notifier = lambda level, msg: None if level == 'admonition' else print( msg )
)

api_introspection = dynadoc.IntrospectionControl(
    targets = dynadoc.IntrospectionTargetsSansModule,
    class_control = dynadoc.ClassIntrospectionControl(
        inheritance = True,
        scan_attributes = True
    )
)

Internal Documentation - Relaxed settings for development:

dev_context = dynadoc.produce_context(
    notifier = lambda level, msg: print( f"[DEV] {msg}" )
)

dev_introspection = dynadoc.IntrospectionControl(
    targets = dynadoc.IntrospectionTargets.Function
)

Library Documentation - High-quality output with custom formatting:

def library_rectifier( fragment: str, source ) -> str:
    # Add consistent punctuation and formatting
    cleaned = fragment.strip( )
    match source:
        case dynadoc.FragmentSources.Annotation:
            cleaned = cleaned[ 0 ].lower( ) + cleaned[ 1: ] if len( cleaned ) > 1 else cleaned.lower( )
    return cleaned

library_context = dynadoc.produce_context(
    fragment_rectifier = library_rectifier
)

Troubleshooting

When working with dynadoc, you may encounter some common technical issues:

Module Ownership Requirements

dynadoc only processes objects that belong to the same module as their containing class or module. This prevents recursion into unrelated code:

# This will work - function belongs to same module as class
class DataProcessor:
    def process_data( self, data: list ) -> dict:
        return { }

# This will NOT work - imported function from different module
from external_lib import external_function
class DataProcessor:
    process = external_function  # Won't be documented

Solution: Ensure that methods and functions you want documented are defined in the same module as the class that contains them. For imported functionality, document the import separately or create wrapper methods.

Fragment Reference Confusion

Strings and bytes are sequences in Python, which can cause unexpected behavior with fragment validation:

# This might not work as expected
class Example:
    _dynadoc_fragments_ = "single string"  # Treated as sequence of characters

# Use a tuple instead
class Example:
    _dynadoc_fragments_ = ( "fragment_reference", )

Solution: Always use tuples or lists for _dynadoc_fragments_, even for single items. This ensures proper sequence handling.

Visibility Rules with __all__

When __all__ is present in a module, it overrides all other visibility rules:

# Even documented private attributes won't appear
__all__ = [ 'public_function' ]

_private_attr: Annotated[ str, dynadoc.Doc( "Well documented" ) ] = "hidden"

def public_function( ) -> None:
    pass

def undocumented_function( ) -> None:  # Won't appear despite being public
    pass

Solution: Either include all desired attributes in __all__, or remove __all__ to use standard visibility rules (public names + documented private attributes).

Property Documentation Issues

Properties require explicit descriptor introspection to be documented:

# This won't document the property
@dynadoc.with_docstring( )
class Example:
    @property
    def value( self ) -> str: return "test"

# Enable descriptor introspection
@dynadoc.with_docstring(
    introspection = dynadoc.IntrospectionControl(
        targets = dynadoc.IntrospectionTargets.Descriptor
    )
)
class Example:
    @property
    def value( self ) -> str: return "test"

Solution: Include IntrospectionTargets.Descriptor in your introspection targets when you want properties documented.

Annotation Resolution Failures

Forward references and complex type annotations may fail to resolve:

# This might fail if ForwardRef isn't available
def process( data: 'ForwardRef' ) -> None:
    pass

Solution: Provide appropriate resolver_globals and resolver_locals in your context, or ensure all referenced types are imported and available.

Configuration Best Practices

When configuring dynadoc:

Start with defaults and only customize what you need:

# Good: minimal necessary customization
context = dynadoc.produce_context( notifier = custom_notifier )

Use targeted introspection to control documentation scope:

# Good: specific targets for clear intent
introspection = dynadoc.IntrospectionControl(
    targets = dynadoc.IntrospectionTargets.Function
)

Test configuration changes with representative code to ensure they work as expected.

Document your configuration choices so team members understand the documentation generation strategy.

Consider performance when enabling comprehensive introspection on large codebases - start narrow and expand as needed.