Made TraitHandler classes replaceable and generic

This commit is contained in:
Griatch 2020-04-16 00:47:27 +02:00
parent fb8403e729
commit 7c31e8bd5a

View file

@ -392,43 +392,28 @@ class TraitHandler:
trait_cls = _TRAIT_CLASSES[trait_type] trait_cls = _TRAIT_CLASSES[trait_type]
except KeyError: except KeyError:
raise TraitException("Trait class for {trait_type} could not be found.") raise TraitException("Trait class for {trait_type} could not be found.")
trait = self._cache[key] = trait_cls(self.trait_data[key]) trait = self._cache[key] = trait_cls(_GA(self, "trait_data")[key])
return trait return trait
def add( def add(self, key, name=None, trait_type=DEFAULT_TRAIT_TYPE, force=True, **trait_properties):
self,
key,
name=None,
trait_type=DEFAULT_TRAIT_TYPE,
base=0,
modifier=0,
min_value=None,
max_value=None,
force=False,
**extra_properties,
):
""" """
Create a new Trait and add it to the handler. Create a new Trait and add it to the handler.
Args: Args:
key (str): This is the name of the property that will be made key (str): This is the name of the property that will be made
available on this handler (example 'hp'). available on this handler (example 'hp').
name (str, optional): This is a longer name used in Trait name (str, optional): Name of the Trait, like "Health". If
string representation (example 'Health'). If not given, this not given, will use `key` starting with a capital letter.
will be set the same as `key`, starting with a capital letter.
trait_type (str, optional): One of 'static', 'counter' or 'gauge'. trait_type (str, optional): One of 'static', 'counter' or 'gauge'.
base (int or float, optional): The base value, or 'full' value in the case force_add (bool): If set, create a new Trait even if a Trait with
of a gauge. the same `key` already exists.
modifier (int, optional): A modifier affecting the current or base value. trait_properties (dict): These will all be use to initialize
min_value (int or float, optional): The minimum allowed value. the new trait. See the `properties` class variable on each
max_value (int or float, optional): The maximum allowed value. Trait class to see which are required.
force (bool, optional): Always add, replacing any existing trait.
**extra_properties (any): All other kwargs will be made available as key:value
properties on the handler. These must all be possible to store
in an Attribute.
Raises: Raises:
TraitException: If specifying invalid values or an existing trait TraitException: If specifying invalid values for the given Trait,
the `trait_type` is not recognized, or an existing trait
already exists (and `force` is unset). already exists (and `force` is unset).
""" """
@ -440,20 +425,19 @@ class TraitHandler:
else: else:
raise TraitException(f"Trait '{key}' already exists.") raise TraitException(f"Trait '{key}' already exists.")
if trait_type not in _TRAIT_CLASSES: trait_class = _TRAIT_CLASSES.get(trait_type)
if not trait_class:
raise TraitException("Trait-type '{trait_type} is invalid.") raise TraitException("Trait-type '{trait_type} is invalid.")
trait_kwargs = dict( trait_properties["name"] = key.title() if not name else name
name=name if name is not None else key.title(), trait_properties["trait_type"] = trait_type
trait_type=trait_type,
base=base,
modifier=modifier,
min_value=min_value,
max_value=max_value,
extra_properties=extra_properties,
)
self.trait_data[key] = trait_kwargs # this will raise exception if input is insufficient
trait_properties = trait_class.validate_input(trait_properties)
print("trait_properties", trait_properties)
self.trait_data[key] = trait_properties
def remove(self, key): def remove(self, key):
""" """
@ -481,39 +465,49 @@ class TraitHandler:
# Parent Trait class # Parent Trait class
@total_ordering
class Trait: class Trait:
"""Represents an object or Character trait. """Represents an object or Character trait.
Note: Note:
See module docstring for configuration details. See module docstring for configuration details.
"""
_keys = ( """
"name", # this is the name used to refer to this trait when adding
"trait_type", # a new trait in the TraitHandler
"base", trait_type = "trait"
"modifier",
"current", # These keywords form the internal data store of the Trait.
"min_value", # Unless a default value is also given, each must be given
"max_value", # supplied with an explicit value when creating this Trait.
"extra_properties", # This list should at minimum contain "name" and "trait_type".
) data_keys = ("name", "trait_type")
# If a dat key has a default, we will use this if it's not supplied at
# creation.
data_default = {}
# enable to set/retrieve other arbitrary properties on the Trait
# and have them treated like data to store.
allow_extra_properties = True
def __init__(self, trait_data): def __init__(self, trait_data):
""" """
Initialize a Trait with stored data. This both initializes and validates the Trait on creation. It must
raise exception if validation fails. The TraitHandler will call this
when the trait is furst added, to make sure it validates before
storing.
Args: Args:
trait_data (_SaverDict or dict): This will be a _SaverDict if trait_data (any): Any pickle-able values to store with this trait.
passed from the TraitHandler, which means this will automatically This must contain any cls.data_keys that do not have a default
save itself the database when updating value in cls.data_default_values. Any extra kwargs will be made
available as extra properties on the Trait, assuming the class
variable `allow_extra_properties` is set.
Raises:
TraitException: If input-validation failed.
""" """
self._data = self.__class__.validate_input(trait_data)
self._type = trait_data["trait_type"]
self._data = trait_data
self._locked = True
if not isinstance(trait_data, _SaverDict): if not isinstance(trait_data, _SaverDict):
logger.log_warn( logger.log_warn(
@ -521,20 +515,34 @@ class Trait:
f"loaded for {type(self).__name__}." f"loaded for {type(self).__name__}."
) )
def __repr__(self): @classmethod
"""Debug-friendly representation of this Trait.""" def validate_input(cls, trait_data):
return "{}({{{}}})".format( """
type(self).__name__, Validate input
", ".join(
["'{}': {!r}".format(k, self._data[k]) for k in self._keys if k in self._data] """
), inp, req = set(trait_data.keys()), set(cls.data_keys)
unset = req.difference(inp.intersection(req))
if unset:
# try to add defaults to those we have not set
no_defaults = unset.difference(set(cls.data_default))
print(f"inp: {inp}, req: {req}, unset: {unset}, no_defaults: {no_defaults}")
if no_defaults:
raise TraitException(
"Trait {} could not be created - misses required keys {}".format(
cls.trait_type, ", ".join(no_defaults)
) )
)
trait_data.update({key: cls.data_default[key] for key in unset})
def __str__(self): if not cls.allow_extra_properties:
status = "{actual:11}".format(actual=self.actual) # don't allow any extra properties - remove the extra data
return "{name:12} {status} ({mod:+3})".format(name=self.name, status=status, mod=self.mod) for key in inp.difference(req):
del trait_data[key]
# Extra Properties - allow access to properties on Trait return trait_data
# Grant access to properties on this Trait.
def __getitem__(self, key): def __getitem__(self, key):
"""Access extra parameters as dict keys.""" """Access extra parameters as dict keys."""
@ -553,11 +561,17 @@ class Trait:
def __getattr__(self, key): def __getattr__(self, key):
"""Access extra parameters as attributes.""" """Access extra parameters as attributes."""
if key in ("data_keys", "data_default", "trait_type", "allow_extra_properties"):
return _GA(self, key)
try: try:
return self._data["extra_properties"][key] return self._data[key]
except KeyError: except KeyError:
raise AttributeError( raise AttributeError(
"{} '{}' has no attribute {!r}".format(type(self).__name__, self.name, key) "{!r} {} ({}) has no attribute {!r}.".format(
self._data['name'],
type(self).__name__,
self.trait_type,
key)
) )
def __setattr__(self, key, value): def __setattr__(self, key, value):
@ -568,36 +582,71 @@ class Trait:
This behavior is enabled by setting the instance This behavior is enabled by setting the instance
variable `_locked` to True. variable `_locked` to True.
""" """
propobj = getattr(self.__class__, key, None) propobj = getattr(self.__class__, key, None)
if isinstance(propobj, property): if isinstance(propobj, property):
if propobj.fset is None: # we have a custom property named as this key, find and use its setter
raise AttributeError(f"Can't set attribute {key}.") if propobj.fset:
propobj.fset(self, value) propobj.fset(self, value)
return
else: else:
if self.__dict__.get("_locked", False) and key not in ("_keys",): # this is some other value
_GA(self, "_data")["extra_properties"][key] = value if key in ("_data", ):
else:
_SA(self, key, value) _SA(self, key, value)
return
if _GA(self, "allow_extra_properties"):
_GA(self, "_data")[key] = value
return
raise AttributeError(f"Can't set attribute {key} on "
f"{self.trait_type} Trait.")
def __delattr__(self, key): def __delattr__(self, key):
"""Delete extra parameters as attributes.""" """Delete extra parameters as attributes."""
if key in self._data["extra_properties"]: if key not in _GA(self, properties) and key in self._data:
del self._data["extra_properties"][key] del self._data[key]
# Limiting the value to set def __repr__(self):
"""Debug-friendly representation of this Trait."""
return "{}({{{}}})".format(
type(self).__name__,
", ".join(
["'{}': {!r}".format(k, self._data[k]) for k in self._keys if k in self._data]
),
)
def _enforce_bounds(self, value): def __str__(self):
"""Ensures that incoming value falls within trait's range.""" return f"<Trait {self.name}>"
return value
def _mod_base(self): # access properties
"""Calculate adding base and modifications"""
return self._enforce_bounds(self.mod + self.base)
def _mod_current(self): @property
"""Calculate the current value""" def name(self):
return self._enforce_bounds(self.mod + self.current) """Display name for the trait."""
return self._data["name"]
key = name
@total_ordering
class NumericTrait(Trait):
"""
Base trait for all Traits based on numbers. This implements
number-comparisons, limits etc. It also features a "modifier"
to the value, since this is a common use.
"""
trait_type = "numeric"
data_keys = (
"name",
"base",
)
data_default = {
"base": 0
}
# Numeric operations # Numeric operations
@ -689,17 +738,10 @@ class Trait:
# Public members # Public members
@property
def name(self):
"""Display name for the trait."""
return self._data["name"]
key = name
@property @property
def actual(self): def actual(self):
"The actual value of the trait" "The actual value of the trait"
return self._mod_base() return self.base_mod_base()
@property @property
def base(self): def base(self):
@ -711,115 +753,94 @@ class Trait:
""" """
return self._data["base"] return self._data["base"]
@base.setter
def base(self, amount):
if self._data.get("max", None) == "base":
self._data["base"] = amount
if type(amount) in (int, float):
self._data["base"] = self._enforce_bounds(amount)
@property
def mod(self):
"""The trait's modifier."""
return self._data["modifier"]
@mod.setter
def mod(self, amount):
if type(amount) in (int, float):
self._data["modifier"] = amount
@property
def min(self):
return self._data["min_value"]
@min.setter
def min(self, value):
self._data["min_value"] = value
@property
def max(self):
return self._data["max_value"]
@max.setter
def max(self, value):
self._data["max_value"] = value
@property
def current(self):
"""The `current` value of the `Trait`."""
return self._data.get("current", self.base)
@current.setter
def current(self, value):
self._data["current"] = value
@property
def extra(self):
"""Returns a list containing available extra data keys."""
return self._data["extra"].keys()
def reset_mod(self):
"""Clears any mod value to 0."""
self.mod = 0
def reset(self):
"""Resets `current` property equal to `base` value."""
self.current = self.base
def percent(self):
"""Returns the value formatted as a percentage."""
return "100.0%"
# Implementation of the respective Trait types # Implementation of the respective Trait types
class StaticTrait(Trait): class StaticTrait(NumericTrait):
""" """
Static Trait. Static Trait. This has a modification value.
""" """
trait_type = "static" trait_type = "static"
@property data_keys = (
def min(self): "name",
raise TraitException(f"Static Trait {self.key} has no minimum value.") "base",
"mod",
)
data_default = {
"base": 0,
"mod": 0
}
def __str__(self):
status = "{actual:11}".format(actual=self.actual)
return "{name:12} {status} ({mod:+3})".format(name=self.name, status=status, mod=self.mod)
# Helpers
@min.setter
def min(self):
raise TraitException(f"Cannot set minimum value for static Trait {self.key}.")
@property @property
def max(self): def mod(self):
raise TraitException("Static Trait {self.key} has no maximum value.") """The trait's modifier."""
return self._data["mod"]
@max.setter @mod.setter
def max(self): def mod(self, amount):
raise TraitException("Cannot set maximum value for static Trait {self.key}.") if type(amount) in (int, float):
self._data["mod"] = amount
@property @property
def current(self): def actual(self):
"""The `current` value of the `Trait`. This is the same as base for a Static Trait.""" "The actual value of the Trait"
return self.base return self.base + self.mod
@current.setter
def current(self, value):
"""Current == base for Static Traits."""
self.base = value
def reset(self):
raise TraitException(f"Cannot reset static Trait {self.key}.")
class CounterTrait(Trait): class CounterTrait(NumericTrait):
""" """
Counter Trait. Counter Trait.
This includes modifications and min/max limits as well as the notion of a
current value. The value can also be reset to the base value.
""" """
trait_type = "counter" trait_type = "counter"
data_keys = (
"name",
"base",
"mod",
"current",
"min_value",
"max_value"
)
data_default = {
"base": 0,
"mod": 0,
"current": 0,
"min_value": None,
"max_value": None
}
# Helpers
def _enforce_bounds(self, value):
"""Ensures that incoming value falls within trait's range."""
return value
def _mod_base(self):
"""Calculate adding base and modifications"""
return self._enforce_bounds(self.mod + self.base)
def _mod_current(self):
"""Calculate the current value"""
return self._enforce_bounds(self.mod + self.current)
def _enforce_bounds(self, value): def _enforce_bounds(self, value):
"""Ensures that incoming value falls within trait's range.""" """Ensures that incoming value falls within trait's range."""
if self.min is not None and value <= self.min: if self.min is not None and value <= self.min:
@ -830,15 +851,27 @@ class CounterTrait(Trait):
return self.max return self.max
return value return value
# properties
@property @property
def actual(self): def actual(self):
"The actual value of the Trait" "The actual value of the Trait"
return self._mod_current() return self._mod_current()
@property
def base(self):
return self._data["base"]
@base.setter
def base(self, amount):
if self._data.get("max", None) == "base":
self._data["base"] = amount
if type(amount) in (int, float):
self._data["base"] = self._enforce_bounds(amount)
@property @property
def min(self): def min(self):
"""The lower bound of the range.""" return self._data["min_value"]
return super().min
@min.setter @min.setter
def min(self, amount): def min(self, amount):
@ -851,7 +884,7 @@ class CounterTrait(Trait):
def max(self): def max(self):
if self._data["max_value"] == "base": if self._data["max_value"] == "base":
return self._mod_base() return self._mod_base()
return super().max return self._data["max_value"]
@max.setter @max.setter
def max(self): def max(self):
@ -862,12 +895,6 @@ class CounterTrait(Trait):
When set this way, the property returns the value of the When set this way, the property returns the value of the
`mod`+`base` properties. `mod`+`base` properties.
""" """
if self._data["max_value"] == "base":
return self._mod_base()
return super().max
@max.setter
def max(self, value):
if value == "base" or value is None: if value == "base" or value is None:
self._data["max_value"] = value self._data["max_value"] = value
elif type(value) in (int, float): elif type(value) in (int, float):
@ -876,14 +903,20 @@ class CounterTrait(Trait):
@property @property
def current(self): def current(self):
"""The `current` value of the `Trait`.""" """The `current` value of the `Trait`."""
return super().current return self._data.get("current", self.base)
@current.setter @current.setter
def current(self, value): def current(self, value):
if type(value) in (int, float): if type(value) in (int, float):
self._data["current"] = self._enforce_bounds(value) self._data["current"] = self._enforce_bounds(value)
else:
raise AttributeError("'current' property is read-only on static 'Trait'.") def reset_mod(self):
"""Clears any mod value to 0."""
self.mod = 0
def reset(self):
"""Resets `current` property equal to `base` value."""
self.current = self.base
def percent(self): def percent(self):
"""Returns the value formatted as a percentage.""" """Returns the value formatted as a percentage."""
@ -899,10 +932,29 @@ class GaugeTrait(CounterTrait):
""" """
Gauge Trait. Gauge Trait.
This emulates a gauge-meter that can be reset.
""" """
trait_type = "gauge" trait_type = "gauge"
# same as Counter, here for easy reference
data_keys = (
"name",
"base",
"mod",
"current",
"min_value",
"max_value"
)
data_default = {
"base": 0,
"mod": 0,
"current": 0,
"min_value": None,
"max_value": None
}
def __str__(self): def __str__(self):
status = "{actual:4} / {base:4}".format(actual=self.actual, base=self.base) status = "{actual:4} / {base:4}".format(actual=self.actual, base=self.base)
return "{name:12} {status} ({mod:+3})".format(name=self.name, status=status, mod=self.mod) return "{name:12} {status} ({mod:+3})".format(name=self.name, status=status, mod=self.mod)
@ -915,13 +967,13 @@ class GaugeTrait(CounterTrait):
@property @property
def mod(self): def mod(self):
"""The trait's modifier.""" """The trait's modifier."""
return super().mod return self._data["mod"]
@mod.setter @mod.setter
def mod(self, amount): def mod(self, amount):
if type(amount) in (int, float): if type(amount) in (int, float):
self._data["modifier"] = amount self._data["mod"] = amount
delta = amount - self._data["modifier"] delta = amount - self._data["mod"]
if delta >= 0: if delta >= 0:
# apply increases to current # apply increases to current
self.current = self._enforce_bounds(self.current + delta) self.current = self._enforce_bounds(self.current + delta)
@ -936,7 +988,8 @@ class GaugeTrait(CounterTrait):
@current.setter @current.setter
def current(self, value): def current(self, value):
super().current = value if type(value) in (int, float):
self._data["current"] = self._enforce_bounds(value)
def fill_gauge(self): def fill_gauge(self):
"""Adds the `mod`+`base` to the `current` value. """Adds the `mod`+`base` to the `current` value.