Source code for genro_toolbox.decorators

"""
Decorators for Genro-Toolbox.

Provides utilities for extracting and grouping keyword arguments.
"""

from collections.abc import Callable
from functools import wraps
from typing import Any, TypeVar

from .dict_utils import dictExtract

F = TypeVar("F", bound=Callable[..., Any])

# Constants to avoid recreating dicts
_DEFAULT_EXTRACT_OPTIONS = {"slice_prefix": True, "pop": False}
_POP_EXTRACT_OPTIONS = {"slice_prefix": True, "pop": True}


[docs] def extract_kwargs( _adapter: str | None = None, _dictkwargs: dict[str, Any] | None = None, **extraction_specs: Any, ) -> Callable[[F], F]: """A decorator that extracts ``**kwargs`` into sub-families by prefix. This decorator allows methods to accept kwargs with prefixes (e.g., `logging_level`, `cache_ttl`) and automatically groups them into separate kwargs dictionaries (e.g., `logging_kwargs`, `cache_kwargs`). Args: _adapter: Optional name of a method on self that will pre-process kwargs. The adapter method receives kwargs dict and can modify it in-place. _dictkwargs: Optional dict to use instead of ``**extraction_specs``. Useful for dynamic extraction specifications. **extraction_specs: Extraction specifications where keys are prefix names. Values can be: - True: Extract and remove (pop=True) - dict: Custom options (slice_prefix, pop) Returns: Decorated function that extracts kwargs by prefix. Example: >>> @extract_kwargs(palette=True, dialog=True, default=True) ... def my_method(self, pane, table=None, ... palette_kwargs=None, dialog_kwargs=None, default_kwargs=None, ... **kwargs): ... pass ... >>> # Call with prefixed parameters >>> obj.my_method(palette_height='200px', palette_width='300px', ... dialog_height='250px') >>> # palette_kwargs={'height': '200px', 'width': '300px'} >>> # dialog_kwargs={'height': '250px'} Notes: - The decorated function MUST have `{prefix}_kwargs` parameters for each prefix - Reserved keyword 'class' is automatically renamed to '_class' - Works with both class methods (with self) and standalone functions - Maintains 100% compatibility with original Genropy implementation """ # Use _dictkwargs if provided, otherwise use extraction_specs # Note: We use a different variable name to avoid shadowing the parameter specs_to_use = _dictkwargs if _dictkwargs is not None else extraction_specs def decorator(func: F) -> F: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: # Call adapter if specified (assumes method usage with self as args[0]) if _adapter and args: adapter_method = getattr(args[0], _adapter, None) if adapter_method is not None: adapter_method(kwargs) # Process each extraction specification for extract_key, extract_value in specs_to_use.items(): grp_key = f"{extract_key}_kwargs" # Get existing grouped kwargs (if explicitly passed) current = kwargs.pop(grp_key, None) if current is None: current = {} elif not isinstance(current, dict): raise TypeError(f"{grp_key} must be a dict, got {type(current).__name__}") # Determine extraction options based on extract_value if extract_value is True: # True means: extract and remove from source extract_options = _POP_EXTRACT_OPTIONS elif isinstance(extract_value, dict): # Dict means: custom options extract_options = {**_DEFAULT_EXTRACT_OPTIONS, **extract_value} else: # Default: extract but don't remove from source extract_options = _DEFAULT_EXTRACT_OPTIONS # Extract prefixed kwargs prefix = f"{extract_key}_" extracted = dictExtract(kwargs, prefix, **extract_options) # Merge extracted kwargs with current current.update(extracted) # Set the grouped kwargs back # Always set as dict (never None), matching original behavior kwargs[grp_key] = current return func(*args, **kwargs) return wrapper # type: ignore return decorator