# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________
#
# This module was originally developed as part of the PyUtilib project
# Copyright (c) 2008 Sandia Corporation.
# This software is distributed under the BSD License.
# Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
# the U.S. Government retains certain rights in this software.
# ___________________________________________________________________________
import shlex
from collections.abc import Mapping
[docs]
class Bunch(dict):
"""A class that can be used to store a bunch of data dynamically.
This class allows for unspecified attributes to have a default value
of None. This borrows the output formatting ideas from the
ActiveState Code Container (recipe 496697).
For historical reasons, attributes / keys are stored in the
underlying dict unless they begin with an underscore, in which case
they are stored as object attributes.
"""
[docs]
def __init__(self, *args, **kw):
self._name_ = self.__class__.__name__
for arg in args:
if not isinstance(arg, str):
raise TypeError("Bunch() positional arguments must be strings")
for item in shlex.split(arg):
item = item.split('=', 1)
if len(item) != 2:
raise ValueError(
"Bunch() positional arguments must be space separated "
f"strings of form 'key=value', got '{item[0]}'"
)
# Historically, this used 'exec'. That is unsafe in
# this context (because anyone can pass arguments to a
# Bunch). While not strictly backwards compatible,
# Pyomo was not using this for anything past parsing
# None/float/int values. We will explicitly parse those
# values
try:
val = float(item[1])
if int(val) == val:
val = int(val)
item[1] = val
except:
if item[1].strip() == 'None':
item[1] = None
self[item[0]] = item[1]
for k, v in kw.items():
self[k] = v
[docs]
def update(self, d):
"""
The update is specialized for JSON-like data. This
recursively replaces dictionaries with Bunch objects.
"""
def _replace_dict_in_list(lst):
ans = []
for v in lst:
if type(v) is dict:
ans.append(Bunch())
ans[-1].update(v)
elif type(v) is list:
ans.append(_replace_dict_in_list(v))
else:
ans.append(v)
return ans
if isinstance(d, Mapping):
item_iter = d.items()
else:
item_iter = d
for k, v in item_iter:
if type(v) is dict:
self[k] = Bunch()
self[k].update(v)
elif type(v) is list:
self[k] = _replace_dict_in_list(v)
else:
self[k] = v
def set_name(self, name):
self._name_ = name
def __getitem__(self, name):
if not isinstance(name, str):
raise ValueError(f'Bunch keys must be str (got {type(name).__name__})')
# Map through Python's standard getattr functionality (which
# will resolve known attributes without hitting __getattr__)
return getattr(self, name)
def __setitem__(self, name, val):
if not isinstance(name, str):
raise ValueError(f'Bunch keys must be str (got {type(name).__name__})')
setattr(self, name, val)
def __delitem__(self, name):
if not isinstance(name, str):
raise ValueError(f'Bunch keys must be str (got {type(name).__name__})')
delattr(self, name)
def __getattr__(self, name):
if name[0] == '_':
raise AttributeError(f"Unknown attribute '{name}'")
return self.get(name, None)
def __setattr__(self, name, val):
if name[0] == '_':
super().__setattr__(name, val)
else:
super().__setitem__(name, val)
def __delattr__(self, name):
if name[0] == '_':
super().__delattr__(name)
else:
super().__delitem__(name)
def __repr__(self):
attrs = sorted(
"%s = %r" % (k, v) for k, v in self.items() if not k.startswith("_")
)
return "%s(%s)" % (self.__class__.__name__, ", ".join(attrs))
def __str__(self, nesting=0, indent=''):
attrs = []
indentation = indent + " " * nesting
for k, v in self.items():
text = [indentation, k, ":"]
if isinstance(v, Bunch):
if len(v) > 0:
text.append('\n')
text.append(v.__str__(nesting + 1))
elif isinstance(v, list):
if len(v) == 0:
text.append(' []')
else:
for v_ in v:
text.append('\n' + indentation + "-")
if isinstance(v_, Bunch):
text.append('\n' + v_.__str__(nesting + 1))
else:
text.append(" " + repr(v_))
else:
text.append(' ' + repr(v))
attrs.append("".join(text))
attrs.sort()
return "\n".join(attrs)