diff --git a/pyomo/common/deprecation.py b/pyomo/common/deprecation.py index 5a6ca456079..c674dcddc78 100644 --- a/pyomo/common/deprecation.py +++ b/pyomo/common/deprecation.py @@ -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 ) diff --git a/pyomo/common/tests/test_deprecated.py b/pyomo/common/tests/test_deprecated.py index 377e229c775..37e1ba81bb3 100644 --- a/pyomo/common/tests/test_deprecated.py +++ b/pyomo/common/tests/test_deprecated.py @@ -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(), "") diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 3eb18dde7a9..26f2d7071b1 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -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 diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index b656eea1bcd..3b335bd5fc4 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -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', diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 660f65f1944..3d578f7dc88 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -13,6 +13,7 @@ # from io import StringIO +import logging import os import sys import types @@ -2975,7 +2976,68 @@ def test_write_exceptions(self): with self.assertRaisesRegex(ValueError, ".*Cannot write model in format"): m.write(format="bogus") - def test_override_pprint(self): + 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_custom_block_ctypes(self): + @declare_custom_block('TestingBlock') + class TestingBlockData(BlockData): + pass + + self.assertIs(TestingBlock().ctype, Block) + + @declare_custom_block('TestingBlock', True) + class TestingBlockData(BlockData): + pass + + self.assertIs(TestingBlock().ctype, TestingBlock) + + @declare_custom_block('TestingBlock', Constraint) + class TestingBlockData(BlockData): + pass + + self.assertIs(TestingBlock().ctype, Constraint) + + with self.assertRaisesRegex( + ValueError, + r"Expected new_ctype to be either type or 'True'; received: \[\]", + ): + + @declare_custom_block('TestingBlock', []) + class TestingBlockData(BlockData): + pass + + def test_custom_block_override_pprint(self): @declare_custom_block('TempBlock') class TempBlockData(BlockData): def pprint(self, ostream=None, verbose=False, prefix=""):