Skip to content

Commit

Permalink
Merge branch 'main' into mis
Browse files Browse the repository at this point in the history
  • Loading branch information
DLWoodruff authored May 5, 2024
2 parents 21aa8c8 + 9dd854b commit a16bf7f
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 69 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/test_branches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,7 @@ jobs:
if test -z "${{matrix.slim}}"; then
python -m pip install --cache-dir cache/pip cplex docplex \
|| echo "WARNING: CPLEX Community Edition is not available"
python -m pip install --cache-dir cache/pip \
-i https://pypi.gurobi.com gurobipy==10.0.3 \
python -m pip install --cache-dir cache/pip gurobipy==10.0.3\
|| echo "WARNING: Gurobi is not available"
python -m pip install --cache-dir cache/pip xpress \
|| echo "WARNING: Xpress Community Edition is not available"
Expand Down
3 changes: 1 addition & 2 deletions .github/workflows/test_pr_and_main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,7 @@ jobs:
if test -z "${{matrix.slim}}"; then
python -m pip install --cache-dir cache/pip cplex docplex \
|| echo "WARNING: CPLEX Community Edition is not available"
python -m pip install --cache-dir cache/pip \
-i https://pypi.gurobi.com gurobipy==10.0.3 \
python -m pip install --cache-dir cache/pip gurobipy==10.0.3 \
|| echo "WARNING: Gurobi is not available"
python -m pip install --cache-dir cache/pip xpress \
|| echo "WARNING: Xpress Community Edition is not available"
Expand Down
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
64 changes: 63 additions & 1 deletion 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,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=""):
Expand Down

0 comments on commit a16bf7f

Please sign in to comment.