Source code for depdigest.core.decorator

import inspect
from functools import wraps
from typing import Any, Callable, Dict, Optional
from .checker import check_dependency
from .config import resolve_config


def _condition_value_matches(value: Any, expected: Any) -> bool:
    """Return whether a runtime argument satisfies a conditional dependency.

    Scientific arguments can define vectorized equality. In those cases, ``==``
    returns an array-like object whose truth value is ambiguous. Treat those
    results as matching only when every comparison element is true.
    """

    if expected is None or value is None:
        return value is expected

    try:
        comparison = value == expected
    except Exception:
        return False

    if isinstance(comparison, bool):
        return comparison

    try:
        return bool(comparison)
    except (TypeError, ValueError):
        pass

    all_method = getattr(comparison, "all", None)
    if callable(all_method):
        try:
            return bool(all_method())
        except (TypeError, ValueError):
            return False

    return False


def _condition_parameter_sources(sig: inspect.Signature, names: set[str]):
    sources = {}
    positional_index = 0
    for name, parameter in sig.parameters.items():
        if parameter.kind in (
            inspect.Parameter.POSITIONAL_ONLY,
            inspect.Parameter.POSITIONAL_OR_KEYWORD,
        ):
            if name in names:
                sources[name] = (positional_index, parameter.default)
            positional_index += 1
        elif parameter.kind is inspect.Parameter.KEYWORD_ONLY and name in names:
            sources[name] = (None, parameter.default)
    return sources


def _resolve_condition_value(source, name, args, kwargs):
    position, default = source
    if name in kwargs:
        if position is not None and position < len(args):
            return False, None
        return True, kwargs[name]
    if position is not None and position < len(args):
        return True, args[position]
    if default is not inspect.Parameter.empty:
        return True, default
    return False, None


[docs] def dep_digest(library: str, when: Optional[Dict[str, Any]] = None): """ Decorator to declare and enforce a dependency. Resolved dynamically at runtime to support configuration changes. """ def decorator(func: Callable): # 1. Metadata Registration (Still at definition time) if not hasattr(func, '_dependencies'): func._dependencies = [] func._dependencies.append({'library': library, 'when': when}) # Pre-compute signature sig = inspect.signature(func) condition_sources = _condition_parameter_sources(sig, set(when or {})) module_path = func.__module__ @wraps(func) def wrapper(*args, **kwargs): # 2. RESOLVE CONFIG AT RUNTIME # This allows tests to register config AFTER function definition cfg = resolve_config(module_path) should_check = True if when is not None: for k, v in when.items(): source = condition_sources.get(k) if source is None: bound = sig.bind(*args, **kwargs) bound.apply_defaults() matched = k in bound.arguments and _condition_value_matches( bound.arguments[k], v ) else: found, value = _resolve_condition_value( source, k, args, kwargs ) matched = found and _condition_value_matches(value, v) if not matched: should_check = False break if should_check: lib_info = cfg.libraries.get(library, {}) pypi_name = lib_info.get('pypi') check_dependency(library, pypi_name=pypi_name, caller=func.__name__, exception_class=cfg.exception_class) return func(*args, **kwargs) return wrapper return decorator