Skip to content

Commit 9dd854b

Browse files
authored
Merge pull request #3253 from jsiirola/component-data-compatability
ComponentData backwards compatibility
2 parents b5d23b3 + eab5a78 commit 9dd854b

File tree

5 files changed

+159
-65
lines changed

5 files changed

+159
-65
lines changed

Diff for: pyomo/common/deprecation.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ def __renamed__warning__(msg):
542542

543543
if new_class is None and '__renamed__new_class__' not in classdict:
544544
if not any(
545-
hasattr(base, '__renamed__new_class__')
545+
hasattr(mro, '__renamed__new_class__')
546546
for mro in itertools.chain.from_iterable(
547547
base.__mro__ for base in renamed_bases
548548
)

Diff for: pyomo/common/tests/test_deprecated.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,10 @@ class DeprecatedClassSubclass(DeprecatedClass):
529529
out = StringIO()
530530
with LoggingIntercept(out):
531531

532-
class DeprecatedClassSubSubclass(DeprecatedClassSubclass):
532+
class otherClass:
533+
pass
534+
535+
class DeprecatedClassSubSubclass(DeprecatedClassSubclass, otherClass):
533536
attr = 'DeprecatedClassSubSubclass'
534537

535538
self.assertEqual(out.getvalue(), "")

Diff for: pyomo/core/base/block.py

+81-62
Original file line numberDiff line numberDiff line change
@@ -2333,98 +2333,117 @@ def components_data(block, ctype, sort=None, sort_by_keys=False, sort_by_names=F
23332333
BlockData._Block_reserved_words = set(dir(Block()))
23342334

23352335

2336-
class _IndexedCustomBlockMeta(type):
2337-
"""Metaclass for creating an indexed custom block."""
2338-
2339-
pass
2340-
2341-
2342-
class _ScalarCustomBlockMeta(type):
2343-
"""Metaclass for creating a scalar custom block."""
2344-
2345-
def __new__(meta, name, bases, dct):
2346-
def __init__(self, *args, **kwargs):
2347-
# bases[0] is the custom block data object
2348-
bases[0].__init__(self, component=self)
2349-
# bases[1] is the custom block object that
2350-
# is used for declaration
2351-
bases[1].__init__(self, *args, **kwargs)
2352-
2353-
dct["__init__"] = __init__
2354-
return type.__new__(meta, name, bases, dct)
2336+
class ScalarCustomBlockMixin(object):
2337+
def __init__(self, *args, **kwargs):
2338+
# __bases__ for the ScalarCustomBlock is
2339+
#
2340+
# (ScalarCustomBlockMixin, {custom_data}, {custom_block})
2341+
#
2342+
# Unfortunately, we cannot guarantee that this is being called
2343+
# from the ScalarCustomBlock (someone could have inherited from
2344+
# that class to make another scalar class). We will walk up the
2345+
# MRO to find the Scalar class (which should be the only class
2346+
# that has this Mixin as the first base class)
2347+
for cls in self.__class__.__mro__:
2348+
if cls.__bases__[0] is ScalarCustomBlockMixin:
2349+
_mixin, _data, _block = cls.__bases__
2350+
_data.__init__(self, component=self)
2351+
_block.__init__(self, *args, **kwargs)
2352+
break
23552353

23562354

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

2360-
def __init__(self, *args, **kwds):
2358+
def __init__(self, *args, **kwargs):
23612359
if self._default_ctype is not None:
2362-
kwds.setdefault('ctype', self._default_ctype)
2363-
Block.__init__(self, *args, **kwds)
2364-
2365-
def __new__(cls, *args, **kwds):
2366-
if cls.__name__.startswith('_Indexed') or cls.__name__.startswith('_Scalar'):
2367-
# we are entering here the second time (recursive)
2368-
# therefore, we need to create what we have
2369-
return super(CustomBlock, cls).__new__(cls)
2360+
kwargs.setdefault('ctype', self._default_ctype)
2361+
Block.__init__(self, *args, **kwargs)
2362+
2363+
def __new__(cls, *args, **kwargs):
2364+
if cls.__bases__[0] is not CustomBlock:
2365+
# we are creating a class other than the "generic" derived
2366+
# custom block class. We can assume that the routing of the
2367+
# generic block class to the specific Scalar or Indexed
2368+
# subclass has already occurred and we can pass control up
2369+
# to (toward) object.__new__()
2370+
return super().__new__(cls, *args, **kwargs)
2371+
# If the first base class is this CustomBlock class, then the
2372+
# user is attempting to create the "generic" block class.
2373+
# Depending on the arguments, we need to map this to either the
2374+
# Scalar or Indexed block subclass.
23702375
if not args or (args[0] is UnindexedComponent_set and len(args) == 1):
2371-
n = _ScalarCustomBlockMeta(
2372-
"_Scalar%s" % (cls.__name__,), (cls._ComponentDataClass, cls), {}
2373-
)
2374-
return n.__new__(n)
2376+
return super().__new__(cls._scalar_custom_block, *args, **kwargs)
23752377
else:
2376-
n = _IndexedCustomBlockMeta("_Indexed%s" % (cls.__name__,), (cls,), {})
2377-
return n.__new__(n)
2378+
return super().__new__(cls._indexed_custom_block, *args, **kwargs)
23782379

23792380

23802381
def declare_custom_block(name, new_ctype=None):
23812382
"""Decorator to declare components for a custom block data class
23822383
2383-
>>> @declare_custom_block(name=FooBlock)
2384+
>>> @declare_custom_block(name="FooBlock")
23842385
... class FooBlockData(BlockData):
23852386
... # custom block data class
23862387
... pass
23872388
"""
23882389

2389-
def proc_dec(cls):
2390-
# this is the decorator function that
2391-
# creates the block component class
2390+
def block_data_decorator(block_data):
2391+
# this is the decorator function that creates the block
2392+
# component classes
23922393

2393-
# Default (derived) Block attributes
2394-
clsbody = {
2395-
"__module__": cls.__module__, # magic to fix the module
2396-
# Default IndexedComponent data object is the decorated class:
2397-
"_ComponentDataClass": cls,
2398-
# By default this new block does not declare a new ctype
2399-
"_default_ctype": None,
2400-
}
2401-
2402-
c = type(
2394+
# Declare the new Block component (derived from CustomBlock)
2395+
# corresponding to the BlockData that we are decorating
2396+
#
2397+
# Note the use of `type(CustomBlock)` to pick up the metaclass
2398+
# that was used to create the CustomBlock (in general, it should
2399+
# be `type`)
2400+
comp = type(CustomBlock)(
24032401
name, # name of new class
24042402
(CustomBlock,), # base classes
2405-
clsbody, # class body definitions (will populate __dict__)
2403+
# class body definitions (populate the new class' __dict__)
2404+
{
2405+
# ensure the created class is associated with the calling module
2406+
"__module__": block_data.__module__,
2407+
# Default IndexedComponent data object is the decorated class:
2408+
"_ComponentDataClass": block_data,
2409+
# By default this new block does not declare a new ctype
2410+
"_default_ctype": None,
2411+
},
24062412
)
24072413

24082414
if new_ctype is not None:
24092415
if new_ctype is True:
2410-
c._default_ctype = c
2411-
elif type(new_ctype) is type:
2412-
c._default_ctype = new_ctype
2416+
comp._default_ctype = comp
2417+
elif isinstance(new_ctype, type):
2418+
comp._default_ctype = new_ctype
24132419
else:
24142420
raise ValueError(
24152421
"Expected new_ctype to be either type "
24162422
"or 'True'; received: %s" % (new_ctype,)
24172423
)
24182424

2419-
# Register the new Block type in the same module as the BlockData
2420-
setattr(sys.modules[cls.__module__], name, c)
2421-
# TODO: can we also register concrete Indexed* and Scalar*
2422-
# classes into the original BlockData module (instead of relying
2423-
# on metaclasses)?
2425+
# Declare Indexed and Scalar versions of the custom block. We
2426+
# will register them both with the calling module scope, and
2427+
# with the CustomBlock (so that CustomBlock.__new__ can route
2428+
# the object creation to the correct class)
2429+
comp._indexed_custom_block = type(comp)(
2430+
"Indexed" + name,
2431+
(comp,),
2432+
{ # ensure the created class is associated with the calling module
2433+
"__module__": block_data.__module__
2434+
},
2435+
)
2436+
comp._scalar_custom_block = type(comp)(
2437+
"Scalar" + name,
2438+
(ScalarCustomBlockMixin, block_data, comp),
2439+
{ # ensure the created class is associated with the calling module
2440+
"__module__": block_data.__module__
2441+
},
2442+
)
24242443

2425-
# are these necessary?
2426-
setattr(cls, '_orig_name', name)
2427-
setattr(cls, '_orig_module', cls.__module__)
2428-
return cls
2444+
# Register the new Block types in the same module as the BlockData
2445+
for _cls in (comp, comp._indexed_custom_block, comp._scalar_custom_block):
2446+
setattr(sys.modules[block_data.__module__], _cls.__name__, _cls)
2447+
return block_data
24292448

2430-
return proc_dec
2449+
return block_data_decorator

Diff for: pyomo/core/expr/numvalue.py

+10
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@
4444
"be treated as if they were bool (as was the case for the other "
4545
"native_*_types sets). Users likely should use native_logical_types.",
4646
)
47+
relocated_module_attribute(
48+
'pyomo_constant_types',
49+
'pyomo.common.numeric_types._pyomo_constant_types',
50+
version='6.7.2.dev0',
51+
f_globals=globals(),
52+
msg="The pyomo_constant_types set will be removed in the future: the set "
53+
"contained only NumericConstant and _PythonCallbackFunctionID, and provided "
54+
"no meaningful value to clients or walkers. Users should likely handle "
55+
"these types in the same manner as immutable Params.",
56+
)
4757
relocated_module_attribute(
4858
'RegisterNumericType',
4959
'pyomo.common.numeric_types.RegisterNumericType',

Diff for: pyomo/core/tests/unit/test_block.py

+63-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#
1414

1515
from io import StringIO
16+
import logging
1617
import os
1718
import sys
1819
import types
@@ -2975,7 +2976,68 @@ def test_write_exceptions(self):
29752976
with self.assertRaisesRegex(ValueError, ".*Cannot write model in format"):
29762977
m.write(format="bogus")
29772978

2978-
def test_override_pprint(self):
2979+
def test_custom_block(self):
2980+
@declare_custom_block('TestingBlock')
2981+
class TestingBlockData(BlockData):
2982+
def __init__(self, component):
2983+
BlockData.__init__(self, component)
2984+
logging.getLogger(__name__).warning("TestingBlockData.__init__")
2985+
2986+
self.assertIn('TestingBlock', globals())
2987+
self.assertIn('ScalarTestingBlock', globals())
2988+
self.assertIn('IndexedTestingBlock', globals())
2989+
self.assertIs(TestingBlock.__module__, __name__)
2990+
self.assertIs(ScalarTestingBlock.__module__, __name__)
2991+
self.assertIs(IndexedTestingBlock.__module__, __name__)
2992+
2993+
with LoggingIntercept() as LOG:
2994+
obj = TestingBlock()
2995+
self.assertIs(type(obj), ScalarTestingBlock)
2996+
self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__")
2997+
2998+
with LoggingIntercept() as LOG:
2999+
obj = TestingBlock([1, 2])
3000+
self.assertIs(type(obj), IndexedTestingBlock)
3001+
self.assertEqual(LOG.getvalue(), "")
3002+
3003+
# Test that we can derive from a ScalarCustomBlock
3004+
class DerivedScalarTestingBlock(ScalarTestingBlock):
3005+
pass
3006+
3007+
with LoggingIntercept() as LOG:
3008+
obj = DerivedScalarTestingBlock()
3009+
self.assertIs(type(obj), DerivedScalarTestingBlock)
3010+
self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__")
3011+
3012+
def test_custom_block_ctypes(self):
3013+
@declare_custom_block('TestingBlock')
3014+
class TestingBlockData(BlockData):
3015+
pass
3016+
3017+
self.assertIs(TestingBlock().ctype, Block)
3018+
3019+
@declare_custom_block('TestingBlock', True)
3020+
class TestingBlockData(BlockData):
3021+
pass
3022+
3023+
self.assertIs(TestingBlock().ctype, TestingBlock)
3024+
3025+
@declare_custom_block('TestingBlock', Constraint)
3026+
class TestingBlockData(BlockData):
3027+
pass
3028+
3029+
self.assertIs(TestingBlock().ctype, Constraint)
3030+
3031+
with self.assertRaisesRegex(
3032+
ValueError,
3033+
r"Expected new_ctype to be either type or 'True'; received: \[\]",
3034+
):
3035+
3036+
@declare_custom_block('TestingBlock', [])
3037+
class TestingBlockData(BlockData):
3038+
pass
3039+
3040+
def test_custom_block_override_pprint(self):
29793041
@declare_custom_block('TempBlock')
29803042
class TempBlockData(BlockData):
29813043
def pprint(self, ostream=None, verbose=False, prefix=""):

0 commit comments

Comments
 (0)