Skip to content

ComponentData backwards compatibility #3253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 4, 2024
2 changes: 1 addition & 1 deletion pyomo/common/deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ def __renamed__warning__(msg):

if new_class is None and '__renamed__new_class__' not in classdict:
if not any(
hasattr(base, '__renamed__new_class__')
hasattr(mro, '__renamed__new_class__')
for mro in itertools.chain.from_iterable(
base.__mro__ for base in renamed_bases
)
Expand Down
5 changes: 4 additions & 1 deletion pyomo/common/tests/test_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,10 @@ class DeprecatedClassSubclass(DeprecatedClass):
out = StringIO()
with LoggingIntercept(out):

class DeprecatedClassSubSubclass(DeprecatedClassSubclass):
class otherClass:
pass

class DeprecatedClassSubSubclass(DeprecatedClassSubclass, otherClass):
attr = 'DeprecatedClassSubSubclass'

self.assertEqual(out.getvalue(), "")
Expand Down
143 changes: 81 additions & 62 deletions pyomo/core/base/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -2333,98 +2333,117 @@ def components_data(block, ctype, sort=None, sort_by_keys=False, sort_by_names=F
BlockData._Block_reserved_words = set(dir(Block()))


class _IndexedCustomBlockMeta(type):
"""Metaclass for creating an indexed custom block."""

pass


class _ScalarCustomBlockMeta(type):
"""Metaclass for creating a scalar custom block."""

def __new__(meta, name, bases, dct):
def __init__(self, *args, **kwargs):
# bases[0] is the custom block data object
bases[0].__init__(self, component=self)
# bases[1] is the custom block object that
# is used for declaration
bases[1].__init__(self, *args, **kwargs)

dct["__init__"] = __init__
return type.__new__(meta, name, bases, dct)
class ScalarCustomBlockMixin(object):
def __init__(self, *args, **kwargs):
# __bases__ for the ScalarCustomBlock is
#
# (ScalarCustomBlockMixin, {custom_data}, {custom_block})
#
# Unfortunately, we cannot guarantee that this is being called
# from the ScalarCustomBlock (someone could have inherited from
# that class to make another scalar class). We will walk up the
# MRO to find the Scalar class (which should be the only class
# that has this Mixin as the first base class)
for cls in self.__class__.__mro__:
if cls.__bases__[0] is ScalarCustomBlockMixin:
_mixin, _data, _block = cls.__bases__
_data.__init__(self, component=self)
_block.__init__(self, *args, **kwargs)
break


class CustomBlock(Block):
"""The base class used by instances of custom block components"""

def __init__(self, *args, **kwds):
def __init__(self, *args, **kwargs):
if self._default_ctype is not None:
kwds.setdefault('ctype', self._default_ctype)
Block.__init__(self, *args, **kwds)

def __new__(cls, *args, **kwds):
if cls.__name__.startswith('_Indexed') or cls.__name__.startswith('_Scalar'):
# we are entering here the second time (recursive)
# therefore, we need to create what we have
return super(CustomBlock, cls).__new__(cls)
kwargs.setdefault('ctype', self._default_ctype)
Block.__init__(self, *args, **kwargs)

def __new__(cls, *args, **kwargs):
if cls.__bases__[0] is not CustomBlock:
# we are creating a class other than the "generic" derived
# custom block class. We can assume that the routing of the
# generic block class to the specific Scalar or Indexed
# subclass has already occurred and we can pass control up
# to (toward) object.__new__()
return super().__new__(cls, *args, **kwargs)
# If the first base class is this CustomBlock class, then the
# user is attempting to create the "generic" block class.
# Depending on the arguments, we need to map this to either the
# Scalar or Indexed block subclass.
if not args or (args[0] is UnindexedComponent_set and len(args) == 1):
n = _ScalarCustomBlockMeta(
"_Scalar%s" % (cls.__name__,), (cls._ComponentDataClass, cls), {}
)
return n.__new__(n)
return super().__new__(cls._scalar_custom_block, *args, **kwargs)
else:
n = _IndexedCustomBlockMeta("_Indexed%s" % (cls.__name__,), (cls,), {})
return n.__new__(n)
return super().__new__(cls._indexed_custom_block, *args, **kwargs)


def declare_custom_block(name, new_ctype=None):
"""Decorator to declare components for a custom block data class

>>> @declare_custom_block(name=FooBlock)
>>> @declare_custom_block(name="FooBlock")
... class FooBlockData(BlockData):
... # custom block data class
... pass
"""

def proc_dec(cls):
# this is the decorator function that
# creates the block component class
def block_data_decorator(block_data):
# this is the decorator function that creates the block
# component classes

# Default (derived) Block attributes
clsbody = {
"__module__": cls.__module__, # magic to fix the module
# Default IndexedComponent data object is the decorated class:
"_ComponentDataClass": cls,
# By default this new block does not declare a new ctype
"_default_ctype": None,
}

c = type(
# Declare the new Block component (derived from CustomBlock)
# corresponding to the BlockData that we are decorating
#
# Note the use of `type(CustomBlock)` to pick up the metaclass
# that was used to create the CustomBlock (in general, it should
# be `type`)
comp = type(CustomBlock)(
name, # name of new class
(CustomBlock,), # base classes
clsbody, # class body definitions (will populate __dict__)
# class body definitions (populate the new class' __dict__)
{
# ensure the created class is associated with the calling module
"__module__": block_data.__module__,
# Default IndexedComponent data object is the decorated class:
"_ComponentDataClass": block_data,
# By default this new block does not declare a new ctype
"_default_ctype": None,
},
)

if new_ctype is not None:
if new_ctype is True:
c._default_ctype = c
elif type(new_ctype) is type:
c._default_ctype = new_ctype
comp._default_ctype = comp
elif isinstance(new_ctype, type):
comp._default_ctype = new_ctype
else:
raise ValueError(
"Expected new_ctype to be either type "
"or 'True'; received: %s" % (new_ctype,)
)

# Register the new Block type in the same module as the BlockData
setattr(sys.modules[cls.__module__], name, c)
# TODO: can we also register concrete Indexed* and Scalar*
# classes into the original BlockData module (instead of relying
# on metaclasses)?
# Declare Indexed and Scalar versions of the custom block. We
# will register them both with the calling module scope, and
# with the CustomBlock (so that CustomBlock.__new__ can route
# the object creation to the correct class)
comp._indexed_custom_block = type(comp)(
"Indexed" + name,
(comp,),
{ # ensure the created class is associated with the calling module
"__module__": block_data.__module__
},
)
comp._scalar_custom_block = type(comp)(
"Scalar" + name,
(ScalarCustomBlockMixin, block_data, comp),
{ # ensure the created class is associated with the calling module
"__module__": block_data.__module__
},
)

# are these necessary?
setattr(cls, '_orig_name', name)
setattr(cls, '_orig_module', cls.__module__)
return cls
# Register the new Block types in the same module as the BlockData
for _cls in (comp, comp._indexed_custom_block, comp._scalar_custom_block):
setattr(sys.modules[block_data.__module__], _cls.__name__, _cls)
return block_data

return proc_dec
return block_data_decorator
10 changes: 10 additions & 0 deletions pyomo/core/expr/numvalue.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@
"be treated as if they were bool (as was the case for the other "
"native_*_types sets). Users likely should use native_logical_types.",
)
relocated_module_attribute(
'pyomo_constant_types',
'pyomo.common.numeric_types._pyomo_constant_types',
version='6.7.2.dev0',
f_globals=globals(),
msg="The pyomo_constant_types set will be removed in the future: the set "
"contained only NumericConstant and _PythonCallbackFunctionID, and provided "
"no meaningful value to clients or walkers. Users should likely handle "
"these types in the same manner as immutable Params.",
)
relocated_module_attribute(
'RegisterNumericType',
'pyomo.common.numeric_types.RegisterNumericType',
Expand Down
34 changes: 34 additions & 0 deletions pyomo/core/tests/unit/test_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#

from io import StringIO
import logging
import os
import sys
import types
Expand Down Expand Up @@ -2975,6 +2976,39 @@ def test_write_exceptions(self):
with self.assertRaisesRegex(ValueError, ".*Cannot write model in format"):
m.write(format="bogus")

def test_custom_block(self):
@declare_custom_block('TestingBlock')
class TestingBlockData(BlockData):
def __init__(self, component):
BlockData.__init__(self, component)
logging.getLogger(__name__).warning("TestingBlockData.__init__")

self.assertIn('TestingBlock', globals())
self.assertIn('ScalarTestingBlock', globals())
self.assertIn('IndexedTestingBlock', globals())
self.assertIs(TestingBlock.__module__, __name__)
self.assertIs(ScalarTestingBlock.__module__, __name__)
self.assertIs(IndexedTestingBlock.__module__, __name__)

with LoggingIntercept() as LOG:
obj = TestingBlock()
self.assertIs(type(obj), ScalarTestingBlock)
self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__")

with LoggingIntercept() as LOG:
obj = TestingBlock([1, 2])
self.assertIs(type(obj), IndexedTestingBlock)
self.assertEqual(LOG.getvalue(), "")

# Test that we can derive from a ScalarCustomBlock
class DerivedScalarTestingBlock(ScalarTestingBlock):
pass

with LoggingIntercept() as LOG:
obj = DerivedScalarTestingBlock()
self.assertIs(type(obj), DerivedScalarTestingBlock)
self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__")

def test_override_pprint(self):
@declare_custom_block('TempBlock')
class TempBlockData(BlockData):
Expand Down
Loading