Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/reference/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Creating objects
~new_model
~new_space
~defcells
~defmacro
~uncached
~cached

Expand Down
1 change: 1 addition & 0 deletions doc/source/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Reference Guide
model
space/index
cells
macro
reference
view
iospec
Expand Down
17 changes: 17 additions & 0 deletions doc/source/reference/macro.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Macro
=======================

.. currentmodule:: modelx.core.macro

.. autoclass:: Macro


Basic properties
----------------

.. autosummary::
:toctree: generated/
:template: mxbase.rst

~Macro.formula
~Macro.parent
11 changes: 11 additions & 0 deletions doc/source/reference/model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Model properties
~Model.properties
~Model.spaces
~Model.refs
~Model.macros
~Model.iospecs
~Model.tracegraph

Expand All @@ -38,6 +39,16 @@ Model operations
~Model.set_property


Macro operations
----------------

.. autosummary::
:toctree: generated/
:template: mxbase.rst

~Model.new_macro


Saving operations
-----------------

Expand Down
109 changes: 109 additions & 0 deletions modelx/core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

from modelx.core import mxsys as _system
from modelx.core.cells import CellsMaker as _CellsMaker
from modelx.core.macro import MacroMaker as _MacroMaker
from modelx.core.space import BaseSpace as _Space
from modelx.core.model import Model as _Model
from modelx.core.base import get_interface_dict as _get_interfaces
Expand Down Expand Up @@ -274,6 +275,114 @@ def foo(x):
return defcells(space, name, is_cached=False, *funcs)


def defmacro(model=None, name=None, *funcs):
"""Decorator to create or update a macro from a Python function.

This convenience function serves as a decorator to create a new macro or
update an existing macro directly from a Python function definition.
Macros are Python functions that can be saved within a Model and executed
to manipulate or query the model.

All macros in a model share the same dedicated global namespace.
In the namespace, the model is defined as a global variable, ``mx_model``
as well as by its model name.

Examples:

**1. As a decorator without arguments**

The code below creates a macro in the current model.
If a macro with the same name already exists, updates its formula.

If the current model does not exist, a new model is created::

>>> import modelx as mx

>>> m = mx.new_model('MyModel')

>>> @mx.defmacro
... def get_model_name():
... return mx_model._name

>>> get_model_name
<Macro MyModel.get_model_name>

>>> m.get_model_name()
'MyModel'

**2. As a decorator with arguments**

The code below creates a macro in a specified model with the specified name::

>>> m = mx.new_model('MyModel')

>>> @mx.defmacro(model=m, name='print_name')
... def print_model_name(message):
... print(f"{message} {get_model_name()}")

>>> print_model_name
<Macro MyModel.print_name>

>>> m.print_name("This model is")
This model is MyModel

**3. As a function**

Creates multiple macros from multiple function definitions::

def foo():
return mx_model._name

def bar():
return foo()

foo, bar = defmacro(foo, bar)

Args:
model (optional): For usage 2, specifies the model to create the macro in.
Defaults to the current model.
name (optional): For usage 2, specifies the name of the created macro.
Defaults to the function name.
*funcs: For usage 3, function objects. (``model`` and ``name`` can also
accept function objects for this usage.)

Returns:
For usage 1 and 2, the newly created single macro is returned.
For usage 3, a list of newly created macros is returned.

.. versionadded:: 0.30.0
"""
if isinstance(model, _FunctionType) and name is None:
# called as a function decorator
func = model
cur_model_obj = cur_model()
if cur_model_obj is None:
cur_model_obj = new_model()
return _MacroMaker(
model=cur_model_obj._impl, name=func.__name__
).create_or_change_macro(func)

elif (isinstance(model, _Model) or model is None) and (
isinstance(name, str) or name is None
):
# return decorator itself
if model is None:
cur_model_obj = cur_model()
if cur_model_obj is None:
cur_model_obj = new_model()
model = cur_model_obj

return _MacroMaker(model=model._impl, name=name)

elif all(
isinstance(func, _FunctionType) for func in (model, name) + funcs
):
return [defmacro(func) for func in (model, name) + funcs]

else:
raise TypeError("invalid defmacro arguments")


def get_models():
"""Returns a dict that maps model names to models.

Expand Down
209 changes: 209 additions & 0 deletions modelx/core/macro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# Copyright (c) 2017-2025 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


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)

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)
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
Loading
Loading