Source code for axisfuzzy.mixin.registry

#  Copyright (c) yibocat 2025 All Rights Reserved
#  Python: 3.12.7
#  Date: 2025/8/18 18:23
#  Author: yibow
#  Email: yibocat@yeah.net
#  Software: AxisFuzzy

"""
Mixin Function Registry for AxisFuzzy core class extensions.

This module provides the infrastructure for registering and dynamically injecting
mtype-agnostic structural operations into :class:`axisfuzzy.core.fuzznums.Fuzznum`
and :class:`axisfuzzy.core.fuzzarray.Fuzzarray` classes, as well as the top-level
:mod:`axisfuzzy` namespace.

The registry system enables a clean separation between core class definitions and
extended functionality, allowing for modular development while maintaining a
cohesive user interface.

Architecture Overview
---------------------
The mixin system follows a three-phase lifecycle:

1. **Registration Phase**: Functions are registered with metadata specifying
   their injection targets and exposure types using the :func:`register_mixin`
   decorator.

2. **Storage Phase**: The :class:`MixinFunctionRegistry` stores registered
   functions and their associated metadata in internal dictionaries.

3. **Injection Phase**: During library initialization, :meth:`MixinFunctionRegistry.build_and_inject`
   dynamically attaches registered functions to target classes and the module namespace.

Key Differences from Extension System
-------------------------------------
The mixin system differs from :mod:`axisfuzzy.extension` in several fundamental ways:

- **Scope**: Mixin functions are mtype-agnostic and focus on structural operations
  (reshape, transpose, concatenate) that work uniformly across all fuzzy number types.
- **Dispatch**: No runtime mtype-based dispatch is needed; all functions work the
  same way regardless of the underlying fuzzy number type.
- **Use Cases**: Primarily for NumPy-like array manipulation and container operations.

Injection Types
---------------
Functions can be exposed in three different ways:

- ``'instance_function'``: Injected as bound methods on target classes
  (e.g., ``my_fuzzarray.reshape(2, 3)``).
- ``'top_level_function'``: Injected into the module namespace
  (e.g., ``axisfuzzy.reshape(my_fuzzarray, 2, 3)``).
- ``'both'``: Exposed as both instance methods and top-level functions.

Thread Safety
--------------
The registry is not inherently thread-safe during the registration and injection
phases. However, since these operations typically occur during module initialization
(import time), this is generally not a concern in practice.

See Also
--------
axisfuzzy.mixin.factory : Implementation layer for mixin operations.
axisfuzzy.mixin.register : Registration declarations for standard operations.
axisfuzzy.extension : mtype-sensitive extension system for specialized operations.

Examples
--------
Registering a simple mixin function:

.. code-block:: python

    from axisfuzzy.mixin.registry import register_mixin

    @register_mixin(name='is_empty', target_classes=['Fuzzarray'],
                    injection_type='instance_function')
    def _is_empty_impl(self):
        return self.size == 0

    # After library initialization:
    # arr = fuzzarray([...])
    # arr.is_empty()  # True/False

Registering a function as both instance method and top-level function:

.. code-block:: python

    @register_mixin(name='normalize_shape', target_classes=['Fuzzarray'],
                    injection_type='both')
    def _normalize_shape_impl(obj):
        return obj.reshape(-1)

    # After library initialization:
    # arr.normalize_shape()  # instance method
    # axisfuzzy.normalize_shape(arr)  # top-level function

Manual injection during initialization:

.. code-block:: python

    from axisfuzzy.mixin.registry import get_registry_mixin
    from axisfuzzy.core import Fuzznum, Fuzzarray

    registry = get_registry_mixin()
    class_map = {'Fuzznum': Fuzznum, 'Fuzzarray': Fuzzarray}
    module_globals = globals()

    registry.build_and_inject(class_map, module_globals)
"""

import functools
from typing import Dict, Callable, Any, List, Optional, Literal


[docs] class MixinFunctionRegistry: """ Central registry for mtype-agnostic functions extending AxisFuzzy core classes. This registry manages the registration, storage, and injection of functions that provide NumPy-like structural operations for :class:`Fuzznum` and :class:`Fuzzarray` objects. Unlike the extension system, mixin functions work uniformly across all fuzzy number types without requiring dispatch logic. The registry supports three injection modes: instance methods, top-level functions, or both. It ensures that the extended functionality integrates seamlessly with the existing class interfaces. Attributes ---------- _functions : dict of str to callable Maps function names to their implementation callables. _metadata : dict of str to dict Maps function names to their registration metadata, including target classes and injection preferences. Notes ----- - The registry is designed as a singleton accessed via :func:`get_registry_mixin`. - Registration typically occurs at module import time via decorators. - Injection happens once during library initialization. See Also -------- register_mixin : Convenience decorator for function registration. get_registry_mixin : Access to the global registry singleton. """ def __init__(self): """ Initialize an empty mixin function registry. Creates internal storage for functions and their associated metadata. The registry starts empty and is populated through decorator-based registration during module imports. Examples -------- .. code-block:: python # Typically not called directly; use get_registry_mixin() instead registry = MixinFunctionRegistry() """ # Stores the core implementation of a function, mapped by its name. self._functions: Dict[str, Callable] = {} # Stores metadata for each function, such as injection type and target classes. self._metadata: Dict[str, Dict[str, Any]] = {}
[docs] def register(self, name: str, target_classes: Optional[List[str]] = None, injection_type: Literal['instance_function', 'top_level_function', 'both'] = 'both') -> Callable: """ Decorator factory to register a function for dynamic injection. This method provides the core registration mechanism for mixin functions. It validates registration parameters, stores the function and its metadata, and returns a decorator that can be applied to the implementation function. Parameters ---------- name : str Unique identifier for the function within the registry. This name will be used as the method/function name after injection. target_classes : list of str, optional Class names where the function should be injected as an instance method. Required when ``injection_type`` is 'instance_function' or 'both'. Common values are ['Fuzznum'], ['Fuzzarray'], or ['Fuzznum', 'Fuzzarray']. injection_type : {'instance_function', 'top_level_function', 'both'}, default 'both' Specifies how the function should be exposed: - 'instance_function': Only as a bound method on target classes - 'top_level_function': Only in the module namespace - 'both': As both instance methods and top-level functions Returns ------- callable A decorator function that accepts the implementation and registers it. Raises ------ ValueError - If ``injection_type`` is not one of the allowed values - If ``target_classes`` is None when instance injection is requested - If a function with the same ``name`` is already registered Examples -------- Register an instance-only method: .. code-block:: python @registry.register('is_normalized', target_classes=['Fuzznum', 'Fuzzarray'], injection_type='instance_function') def _is_normalized_impl(self): # Implementation logic return True Register a top-level-only function: .. code-block:: python @registry.register('create_identity', injection_type='top_level_function') def _create_identity_impl(mtype='qrofn'): # Implementation logic return Fuzznum(mtype).create(md=1.0, nmd=0.0) Register both instance method and top-level function: .. code-block:: python @registry.register('to_list', target_classes=['Fuzzarray'], injection_type='both') def _to_list_impl(self): # Works as both arr.to_list() and axisfuzzy.to_list(arr) return list(self) """ # Validate the provided injection_type to ensure it's one of the allowed literals. if injection_type not in ['instance_function', 'top_level_function', 'both']: raise ValueError(f"Invalid injection_type: {injection_type}. " f"Must be 'instance_function', 'top_level_function', or 'both'.") # Validate that target_classes are provided if instance method injection is requested. if injection_type in ['instance_function', 'both'] and not target_classes: raise ValueError(f"target_classes must be provided for injection_type '{injection_type}'.") def decorator(func: Callable) -> Callable: """ Inner decorator that performs the actual registration. Parameters ---------- func : callable The implementation function to register. Returns ------- callable The original function (unmodified). """ # Check for duplicate function names to prevent accidental overwrites. if name in self._functions: raise ValueError(f"Function '{name}' is already registered.") # Store the actual function implementation. self._functions[name] = func # Store the metadata associated with this function, including its injection preferences. self._metadata[name] = { 'target_classes': target_classes or [], # Ensure target_classes is always a list 'injection_type': injection_type } return func return decorator
[docs] def build_and_inject(self, class_map: Dict[str, type], module_namespace: Dict[str, Any]): """ Inject all registered functions into target classes and module namespace. This method performs the final injection phase, iterating through all registered functions and attaching them to their specified targets based on the injection metadata. It handles both instance method injection (via :func:`setattr` on classes) and top-level function injection (via dictionary assignment on the module namespace). Parameters ---------- class_map : dict of str to type Maps class names to actual class objects for instance method injection. Typically constructed as ``{'Fuzznum': Fuzznum, 'Fuzzarray': Fuzzarray}``. module_namespace : dict of str to any Target module's namespace (usually ``globals()`` of the target module) where top-level functions should be injected. Functions are added as key-value pairs to this dictionary. Notes ----- - Instance methods are injected using ``setattr(class, name, function)`` - Top-level functions use different strategies for 'both' vs 'top_level_function' only: - 'both': Creates a wrapper that delegates to the instance method - 'top_level_function': Injects the original function directly - Injection is idempotent but not thread-safe - Missing classes in ``class_map`` are silently ignored Examples -------- Typical usage during library initialization: .. code-block:: python from axisfuzzy.core import Fuzznum, Fuzzarray from axisfuzzy.mixin.registry import get_registry_mixin # Prepare injection targets class_map = { 'Fuzznum': Fuzznum, 'Fuzzarray': Fuzzarray } module_globals = globals() # Perform injection registry = get_registry_mixin() registry.build_and_inject(class_map, module_globals) # Now functions are available: # arr = Fuzzarray(...) # arr.reshape(2, 2) # instance method # reshape(arr, 2, 2) # top-level function Custom class mapping: .. code-block:: python # Only inject into Fuzzarray class_map = {'Fuzzarray': Fuzzarray} registry.build_and_inject(class_map, {}) """ # Iterate over each registered function and its associated metadata. for name, func in self._functions.items(): meta = self._metadata[name] injection_type = meta['injection_type'] target_classes = meta['target_classes'] # Handle injection as an instance method. if injection_type in ['instance_function', 'both']: for class_name in target_classes: # Check if the target class exists in the provided map. if class_name in class_map: target_class = class_map[class_name] # Dynamically add the function as a method to the class. setattr(target_class, name, func) # # Handle injection as a top-level function. if injection_type in ['top_level_function', 'both']: if injection_type == 'both': # If the function is injected as 'both', create a wrapper for the top-level # function. This wrapper will delegate the call to the instance method # that was (or will be) injected into the object's class. @functools.wraps(func) def top_level_wrapper(obj: Any, *args, current_name=name, **kwargs): # Check if the object has the method and it's callable. if hasattr(obj, current_name) and callable(getattr(obj, current_name)): # Call the instance method on the provided object. return getattr(obj, current_name)(*args, **kwargs) else: # Raise an error if the method is not supported for the given object type. raise TypeError(f"'{current_name}' is not supported for type '{type(obj).__name__}'") # Inject the wrapper into the module's namespace. module_namespace[name] = top_level_wrapper else: # If the function is 'top_level_function' only, inject the function directly # into the module's namespace without any wrapping. module_namespace[name] = func
[docs] def get_top_level_function_names(self) -> List[str]: """ Get names of all functions registered for top-level injection. This method scans the registry metadata and returns a list of function names that should be exposed as top-level functions. It's useful for populating ``__all__`` lists and documentation generation. Returns ------- list of str Sorted list of function names that have ``injection_type`` of 'top_level_function' or 'both'. Examples -------- .. code-block:: python registry = get_registry_mixin() # Register some functions @registry.register('func1', injection_type='instance_function', target_classes=['Fuzznum']) def f1(self): pass @registry.register('func2', injection_type='top_level_function') def f2(): pass @registry.register('func3', injection_type='both', target_classes=['Fuzzarray']) def f3(self): pass names = registry.get_top_level_function_names() print(names) # ['func2', 'func3'] Use in module ``__all__`` definition: .. code-block:: python from axisfuzzy.mixin.registry import get_registry_mixin # Get all mixin top-level functions _mixin_functions = get_registry_mixin().get_top_level_function_names() # Combine with other exports __all__ = ['Fuzznum', 'Fuzzarray'] + _mixin_functions """ names = [] # Iterate through the metadata of all registered functions. for name, meta in self._metadata.items(): # Check if the function's injection type includes 'top_level_function'. if meta['injection_type'] in ['top_level_function', 'both']: names.append(name) return names
# Create a global instance of the registry. # This ensures that all parts of the application use the same registry instance # to register and manage mixin functions, adhering to the Singleton pattern. _registry = MixinFunctionRegistry()
[docs] def get_registry_mixin(): """ Access the global singleton :class:`MixinFunctionRegistry` instance. This function provides the standard entry point to the mixin registry system. It returns the same registry instance across all calls, ensuring consistent registration and injection behavior throughout the library. Returns ------- MixinFunctionRegistry The global singleton registry instance. Notes ----- The registry is created once when this module is first imported and reused for all subsequent calls. This singleton pattern ensures that all registered functions are stored in the same location and available for injection. Examples -------- Basic registry access: .. code-block:: python from axisfuzzy.mixin.registry import get_registry_mixin registry = get_registry_mixin() # Use registry.register(...) to register functions Use in registration modules: .. code-block:: python # In axisfuzzy/mixin/register.py or similar from axisfuzzy.mixin.registry import get_registry_mixin registry = get_registry_mixin() @registry.register('my_function', target_classes=['Fuzzarray']) def _my_function_impl(self): return self.copy() Use in library initialization: .. code-block:: python # In axisfuzzy/__init__.py from axisfuzzy.mixin.registry import get_registry_mixin from axisfuzzy.core import Fuzznum, Fuzzarray # Inject all registered mixin functions registry = get_registry_mixin() class_map = {'Fuzznum': Fuzznum, 'Fuzzarray': Fuzzarray} registry.build_and_inject(class_map, globals()) """ return _registry
[docs] def register_mixin(name: str, target_classes: Optional[List[str]] = None, injection_type: Literal['instance_function', 'top_level_function', 'both'] = 'both') -> Callable: """ Convenience decorator for registering mixin functions. This function provides a streamlined interface to the global mixin registry, eliminating the need to explicitly access the registry instance. It's the recommended way to register mixin functions in most scenarios. Parameters ---------- name : str Unique function name for registry and injection. target_classes : list of str, optional Class names for instance method injection. Required for 'instance_function' and 'both' injection types. injection_type : {'instance_function', 'top_level_function', 'both'}, default 'both' How the function should be exposed after injection. Returns ------- callable Decorator function that registers the implementation. Raises ------ ValueError If parameters are invalid or if the function name is already registered. Examples -------- Register an instance method: .. code-block:: python from axisfuzzy.mixin.registry import register_mixin @register_mixin('is_square', target_classes=['Fuzzarray'], injection_type='instance_function') def _is_square_impl(self): return len(set(self.shape)) <= 1 Register a top-level function: .. code-block:: python @register_mixin('zeros_like', injection_type='top_level_function') def _zeros_like_impl(template): # Create zero array with same shape and mtype as template return Fuzzarray(shape=template.shape, mtype=template.mtype) Register both instance method and top-level function: .. code-block:: python @register_mixin('flatten', target_classes=['Fuzzarray'], injection_type='both') def _flatten_impl(self): # Available as both arr.flatten() and axisfuzzy.flatten(arr) return self.reshape(-1) See Also -------- MixinFunctionRegistry.register : The underlying registration method. get_registry_mixin : Access to the global registry instance. """ return get_registry_mixin().register(name, target_classes, injection_type)