Skip to content
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

ComponentData backwards compatibility #3253

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
87 changes: 42 additions & 45 deletions pyomo/core/base/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -2333,48 +2333,42 @@ 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)
kwargs.setdefault('ctype', self._default_ctype)
Block.__init__(self, *args, **kwargs)

def __new__(cls, *args, **kwds):
if cls.__name__.startswith('_Indexed') or cls.__name__.startswith('_Scalar'):
def __new__(cls, *args, **kwargs):
if cls.__bases__[0] is not CustomBlock:
# we are entering here the second time (recursive)
# therefore, we need to create what we have
return super(CustomBlock, cls).__new__(cls)
return super().__new__(cls, *args, **kwargs)
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):
Expand All @@ -2386,9 +2380,9 @@ def declare_custom_block(name, new_ctype=None):
... pass
"""

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

# Default (derived) Block attributes
clsbody = {
Expand All @@ -2399,7 +2393,7 @@ def proc_dec(cls):
"_default_ctype": None,
}

c = type(
c = type(CustomBlock)(
name, # name of new class
(CustomBlock,), # base classes
clsbody, # class body definitions (will populate __dict__)
Expand All @@ -2408,23 +2402,26 @@ def proc_dec(cls):
if new_ctype is not None:
if new_ctype is True:
c._default_ctype = c
elif type(new_ctype) is type:
elif isinstance(new_ctype, type):
c._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 blocks. 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)
c._indexed_custom_block = type(c)("Indexed" + name, (c,), {})
c._scalar_custom_block = type(c)(
"Scalar" + name, (ScalarCustomBlockMixin, cls, c), {}
)

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

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
31 changes: 31 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,36 @@ 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())

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 DerivedScalarTstingBlock(ScalarTestingBlock):
jsiirola marked this conversation as resolved.
Show resolved Hide resolved
pass

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

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