Source code for modelx.core.macro

# Copyright (c) 2017-2026 Fumito Hamamura <fumito.ham@gmail.com>

# This library is free software: you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation version 3.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.  If not, see <http://www.gnu.org/licenses/>.

import types
from collections.abc import Callable

from modelx.core.base import (
    Impl, Interface, get_mixin_slots
)
from modelx.core.formula import Formula
from modelx.core.util import is_valid_name


class MacroMaker:
    """Factory for creating Macro objects"""
    
    def __init__(self, *, model, name):
        self.model = model  # ModelImpl
        self.name = name
    
    def __call__(self, func):
        return self.create_or_change_macro(func)
    
    def create_or_change_macro(self, func):
        self.name = func.__name__ if self.name is None else self.name
        
        if not is_valid_name(self.name):
            raise ValueError(f"Invalid macro name: {self.name}")
        
        if self.name in self.model.macros:
            # Update existing macro
            macro = self.model.macros[self.name]
            macro.set_formula(func)
            return macro.interface
        else:
            # Create new macro
            return self.model.new_macro(name=self.name, formula=func).interface


[docs] class Macro(Interface, Callable): """A callable Python function that can be saved within a Model. Macros are Python functions stored in a model that can be used to manipulate and interact with the model. All macros in a model share a dedicated global namespace that includes the model itself as both ``mx_model`` and by the model's name. Creation: Macros can be created using the :func:`~modelx.defmacro` decorator:: >>> import modelx as mx >>> m = mx.new_model('MyModel') >>> @mx.defmacro ... def get_model_name(): ... return mx_model._name >>> @mx.defmacro(model=m, name='print_name') ... def print_model_name(message): ... print(f"{message} {get_model_name()}") Execution: Macros are executed by calling them as model attributes:: >>> m.get_model_name() 'MyModel' >>> m.print_name("This model is") This model is MyModel Listing Macros: Access all macros through the model's :attr:`~modelx.core.model.Model.macros` property:: >>> m.macros {'get_model_name': <Macro MyModel.get_model_name>, 'print_name': <Macro MyModel.print_name>} Export: When a model is exported, macros are saved in ``_mx_macros.py`` as regular Python functions, allowing them to work with both modelx models and exported models. See Also: * :func:`~modelx.defmacro`: Decorator to create macros * :attr:`~modelx.core.model.Model.macros`: Access model's macros * :meth:`~modelx.core.model.Model.export`: Export model as Python package .. versionadded:: 0.30.0 """ __slots__ = () def __call__(self, *args, **kwargs): """Execute the macro with given arguments""" return self._impl.execute(*args, **kwargs) def __repr__(self): return f"<Macro {self._impl.repr_parent()}.{self._impl.repr_self()}>" @property def formula(self): """The formula object of the macro""" return self._impl.formula @property def parent(self): """The parent model of the macro""" if self._impl.parent is not None: return self._impl.parent.interface else: return None
class MacroImpl(Impl): """Implementation of Macro interface""" interface_cls = Macro __slots__ = ( "formula", "_namespace" ) + get_mixin_slots(Impl) def __init__(self, *, system, parent, name, formula): """Initialize MacroImpl Args: system: The system object parent: The parent ModelImpl object name: Name of the macro formula: Formula object or callable """ Impl.__init__( self, system=system, parent=parent, name=name, spmgr=parent.spmgr ) if not isinstance(formula, Formula): formula = Formula(formula, name=name) self.formula = formula self._namespace = None def execute(self, *args, **kwargs): """Execute the macro function Args: *args: Positional arguments for the macro function **kwargs: Keyword arguments for the macro function Returns: The return value of the macro function """ # Get the namespace with mx_model and model name namespace = self.parent.get_macro_namespace() # Execute the function with the namespace as globals func = self.formula.func # Create a new function with the correct globals new_func = types.FunctionType( func.__code__, namespace, func.__name__, func.__defaults__, func.__closure__ ) return new_func(*args, **kwargs) def set_formula(self, func): """Update the macro's formula Args: func: New function to use as the formula """ if not isinstance(func, Formula): func = Formula(func, name=self.name) self.formula = func def repr_parent(self): """Return parent representation""" if self.parent.repr_parent(): return self.parent.repr_parent() + "." + self.parent.repr_self() else: return self.parent.repr_self() def repr_self(self, add_params=True): """Return self representation""" return self.name def on_delete(self): """Cleanup when macro is deleted""" pass