from __future__ import annotations
from .errors import SimStateOptionsError
_NO_DEFAULT_VALUE = "_NO_DEFAULT_VALUE" # please god don't use this value as the default value of your state option
[docs]
class StateOption:
"""
Describes a state option.
"""
__slots__ = (
"name",
"types",
"default",
"description",
"_one_type",
)
[docs]
def __init__(self, name, types, default=_NO_DEFAULT_VALUE, description=None):
self.name = name
self.types = tuple(types)
self.default = default
self.description = description
# Sanity check
if not isinstance(self.default, tuple(self.types)):
raise SimStateOptionsError(
"The type of the default value does not match the expected types of this state option."
)
# Speed optimization
if len(self.types) == 1:
self._one_type = next(iter(self.types))
else:
self._one_type = None
@property
def has_default_value(self):
return self.default != _NO_DEFAULT_VALUE
[docs]
def one_type(self):
return self._one_type
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return isinstance(other, StateOption) and self.name == other.name and self.types == other.types
def __repr__(self):
desc = f": {self.description}" if self.description is not None else ""
types = self.one_type().__name__ if self.one_type() is not None else ",".join(t.__name__ for t in self.types)
return f"<O {self.name}[{types}]{desc}>"
def __getstate__(self):
return {
"name": self.name,
"types": self.types,
"default": self.default,
"description": self.description,
}
def __setstate__(self, state):
self.name = state["name"]
self.types = state["types"]
self.default = state["default"]
self.description = state["description"]
[docs]
class SimStateOptions:
"""
A per-state manager of state options. An option can be either a key-valued entry or a Boolean switch (which can be
seen as a key-valued entry whose value can only be either True or False).
"""
__slots__ = ("_options",)
OPTIONS = {}
[docs]
def __init__(self, thing):
"""
:param thing: Either a set of Boolean switches to enable, or an existing SimStateOptions instance.
"""
self._options = {}
if thing is None:
pass
elif isinstance(thing, (set, list)):
boolean_switches = thing
for name in boolean_switches:
self[name] = True
elif isinstance(thing, SimStateOptions):
ops = thing
self._options = ops._options.copy()
else:
raise SimStateOptionsError(f"Unsupported constructor argument type '{type(thing)}'.")
def _get_option_desc(self, key):
"""
Get the option descriptor from self.OPTIONS.
:param str key: Name of the state option.
:return: The option descriptor.
:rtype: StateOption
"""
try:
return self.OPTIONS[key]
except KeyError as err:
raise SimStateOptionsError(f"The state option '{key}' does not exist.") from err
def __repr__(self):
return "<SimStateOptions>"
def __contains__(self, key):
"""
[COMPATIBILITY]
In order to be compatible with the old interface, __contains__() only supports testing the value of a Boolean
switch.
E.g., in the old days:
>>> sim_options.SYMBOLIC in state.options
False
nowadays:
>>> sim_options.SYMBOLIC in state.options
False
But you cannot use it to test the value of a non-existent option, or the value of a key-valued entry that is not
linked to a Boolean value.
>>> "symbolic_ip_max_targets" in state.options
SimStateOptionsError('"symbolic_ip_max_targets" is not a Boolean switch.')
:param str key: Name of the Boolean switch.
:return: True if the switch is on (the option is switched on), False otherwise.
:rtype: bool
"""
# o = self._get_option_desc(key)
# if o.one_type() is not bool:
# raise SimStateOptionsError("The state option '%s' is not a Boolean switch." % key)
return key in self._options and self._options[key] is True
def __setitem__(self, key, value):
"""
Set the value of a state option.
:param str key: Name of the state option.
:param str value: The value of the state option. Must be of the same type as registered.
:return: None
"""
o = self._get_option_desc(key)
if type(value) not in o.types:
raise SimStateOptionsError(
f"The value '{value}' does not have an acceptable type for state option '{key}'. "
f"Accepted types are: {o.types!s}."
)
self._options[o.name] = value
def __getitem__(self, key):
"""
Get the value of a state option.
:param str key: Name of the state option.
:return: Value of the state option.
"""
o = self._get_option_desc(key)
if o.name not in self._options:
# Special handling for Boolean switches
if o.one_type() is bool:
return o.default
# Special handling for options with default values
if o.has_default_value:
return o.default
return self._options[o.name]
def __ior__(self, boolean_switches):
"""
[COMPATIBILITY]
In order to be compatible with the old interface, you can enable a collection of Boolean switches at the same
time by doing the following:
>>> state.options |= {sim_options.SYMBOLIC, sim_options.ABSTRACT_MEMORY}
:param set boolean_switches: A collection of Boolean switches to enable.
:return: self
"""
for name in boolean_switches:
self[name] = True
return self
def __isub__(self, boolean_switches):
"""
[COMPATIBILITY]
In order to be compatible with the old interface, you can disable a collection of Boolean switches at the same
time by doing the following:
>>> state.options -= {sim_options.SYMBOLIC, sim_options.ABSTRACT_MEMORY}
:param set boolean_switches: A collection of Boolean switches to disable.
:return: self
"""
for name in boolean_switches:
self[name] = False
return self
def __sub__(self, boolean_switches):
"""
[COMPATIBILITY]
You may disable a collection of Boolean switches by doing:
>>> state.options = state.options - {sim_options.SYMBOLIC}
:param set boolean_switches: A collection of Boolean switches to disable.
:return: A new SimStateOptions instance.
:rtype: SimStateOptions
"""
ops = SimStateOptions(self)
for name in boolean_switches:
ops[name] = False
return ops
def __getattr__(self, key):
if key in {"OPTIONS", "_options"}:
return self.__getattribute__(key)
if key.startswith("__") and key.endswith("__"):
return self.__getattribute__(key)
return self[key]
def __setattr__(self, key, value):
if key in {"OPTIONS", "_options"}:
super().__setattr__(key, value)
return
self[key] = value
def __getstate__(self):
return {
"_options": self._options,
}
def __setstate__(self, state):
self._options = state["_options"]
[docs]
def add(self, boolean_switch):
"""
[COMPATIBILITY]
Enable a Boolean switch.
:param str boolean_switch: Name of the Boolean switch.
:return: None
"""
self[boolean_switch] = True
[docs]
def update(self, boolean_switches):
"""
[COMPATIBILITY]
In order to be compatible with the old interface, you can enable a collection of Boolean switches at the same
time by doing the following:
>>> state.options.update({sim_options.SYMBOLIC, sim_options.ABSTRACT_MEMORY})
or
>>> state.options.update(sim_options.unicorn)
:param set boolean_switches: A collection of Boolean switches to enable.
:return: None
"""
for name in boolean_switches:
self[name] = True
[docs]
def remove(self, name):
"""
Drop a state option if it exists, or raise a KeyError if the state option is not set.
[COMPATIBILITY]
Remove a Boolean switch.
:param str name: Name of the state option.
:return: NNone
"""
del self._options[name]
[docs]
def discard(self, name):
"""
Drop a state option if it exists, or silently return if the state option is not set.
[COMPATIBILITY]
Disable a Boolean switch.
:param str name: Name of the Boolean switch.
:return: None
"""
if name in self._options:
del self._options[name]
[docs]
def difference(self, boolean_switches):
"""
[COMPATIBILITY]
Make a copy of the current instance, and then discard all options that are in boolean_switches.
:param set boolean_switches: A collection of Boolean switches to disable.
:return: A new SimStateOptions instance.
"""
ops = SimStateOptions(self)
for key in boolean_switches:
ops.discard(key)
return ops
[docs]
def copy(self):
"""
Get a copy of the current SimStateOptions instance.
:return: A new SimStateOptions instance.
:rtype: SimStateOptions
"""
return SimStateOptions(self)
[docs]
def tally(self, exclude_false=True, description=False):
"""
Return a string representation of all state options.
:param bool exclude_false: Whether to exclude Boolean switches that are disabled.
:param bool description: Whether to display the description of each option.
:return: A string representation.
:rtype: str
"""
total = []
for o in sorted(self.OPTIONS.values(), key=lambda x: x.name):
try:
value = self[o.name]
except SimStateOptionsError:
value = "<Unset>"
if exclude_false and o.one_type() is bool and value is False:
# Skip Boolean switches that are False
continue
s = f"{o.name}: {value}"
if description:
s += f" | {o.description}"
total.append(s)
return "\n".join(total)
[docs]
@classmethod
def register_option(cls, name, types, default=None, description=None):
"""
Register a state option.
:param str name: Name of the state option.
:param types: A collection of allowed types of this state option.
:param default: The default value of this state option.
:param str description: The description of this state option.
:return: None
"""
if name in cls.OPTIONS:
raise SimStateOptionsError("A state option with the same name has been registered.")
if isinstance(types, type):
types = {types}
o = StateOption(name, types, default=default, description=description)
cls.OPTIONS[name] = o
[docs]
@classmethod
def register_bool_option(cls, name, description=None):
"""
Register a Boolean switch as state option.
This is equivalent to cls.register_option(name, set([bool]), description=description)
:param str name: Name of the state option.
:param str description: The description of this state option.
:return: None
"""
cls.register_option(name, {bool}, default=False, description=description)