Cleanup and simplifications of Trait props

Remove NumericTrait type, instead incorporating it into the
Trait base. Change .actual to .value to homogenize the interface
for all Trait subclasses. Rename data_keys to default_keys.
Improve and correct documentation in several places.
This commit is contained in:
Griatch 2020-04-20 09:17:06 +02:00
parent 39c7889336
commit 06cb85ea6c
2 changed files with 187 additions and 257 deletions

View file

@ -34,7 +34,6 @@ class _MockObj:
# we want to test the base traits too # we want to test the base traits too
_TEST_TRAIT_CLASS_PATHS = [ _TEST_TRAIT_CLASS_PATHS = [
"evennia.contrib.traits.Trait", "evennia.contrib.traits.Trait",
"evennia.contrib.traits.NumericTrait",
"evennia.contrib.traits.StaticTrait", "evennia.contrib.traits.StaticTrait",
"evennia.contrib.traits.CounterTrait", "evennia.contrib.traits.CounterTrait",
"evennia.contrib.traits.GaugeTrait", "evennia.contrib.traits.GaugeTrait",
@ -169,7 +168,7 @@ class TraitHandlerTest(_TraitHandlerBase):
) )
class TraitTest(_TraitHandlerBase): class TestTrait(_TraitHandlerBase):
""" """
Test the base Trait class Test the base Trait class
""" """
@ -217,7 +216,7 @@ class TraitTest(_TraitHandlerBase):
"extra_val": 1000 "extra_val": 1000
} }
expected = copy(dat) expected = copy(dat)
expected["value"] = traits.Trait.data_keys['value'] expected["value"] = traits.Trait.default_keys['value']
self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat)) self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat))
# make sure extra values are cleaned if trait accepts no extras # make sure extra values are cleaned if trait accepts no extras
@ -246,10 +245,10 @@ class TraitTest(_TraitHandlerBase):
traits.Trait.validate_input(traits.Trait, dat) traits.Trait.validate_input(traits.Trait, dat)
# make value a required key # make value a required key
mock_data_keys = { mock_default_keys = {
"value": traits.MandatoryTraitKey "value": traits.MandatoryTraitKey
} }
with patch.object(traits.Trait, "data_keys", mock_data_keys): with patch.object(traits.Trait, "default_keys", mock_default_keys):
dat = { dat = {
"name": "Trait", "name": "Trait",
"trait_type": "trait", "trait_type": "trait",
@ -288,54 +287,13 @@ class TraitTest(_TraitHandlerBase):
self.trait.extra_val1 self.trait.extra_val1
del self.trait.value del self.trait.value
# fall back to default # fall back to default
self.assertTrue(self.trait.value == traits.Trait.data_keys["value"]) self.assertTrue(self.trait.value == traits.Trait.default_keys["value"])
def test_repr(self): def test_repr(self):
self.assertEqual(repr(self.trait), Something) self.assertEqual(repr(self.trait), Something)
self.assertEqual(str(self.trait), Something) self.assertEqual(str(self.trait), Something)
class TestTraitNumeric(_TraitHandlerBase):
"""
Test the numeric base class
"""
def setUp(self):
super().setUp()
self.traithandler.add(
"test1",
name="Test1",
trait_type='numeric',
base=1,
extra_val1="xvalue1",
extra_val2="xvalue2"
)
self.trait = self.traithandler.get("test1")
def _get_actuals(self):
"""Get trait actuals for comparisons"""
return self.trait.actual, self.trait2.actual
def test_init(self):
self.assertEqual(
self.trait._data,
{"name": "Test1",
"trait_type": "numeric",
"base": 1,
"extra_val1": "xvalue1",
"extra_val2": "xvalue2"
}
)
def test_set_wrong_type(self):
self.trait.base = "foo"
self.assertEqual(self.trait.base, 1)
def test_actual(self):
self.trait.base = 10
self.assertEqual(self.trait.actual, 10)
class TestTraitStatic(_TraitHandlerBase): class TestTraitStatic(_TraitHandlerBase):
""" """
Test for static Traits Test for static Traits
@ -354,7 +312,7 @@ class TestTraitStatic(_TraitHandlerBase):
self.trait = self.traithandler.get("test1") self.trait = self.traithandler.get("test1")
def _get_values(self): def _get_values(self):
return self.trait.base, self.trait.mod, self.trait.actual return self.trait.base, self.trait.mod, self.trait.value
def test_init(self): def test_init(self):
self.assertEqual( self.assertEqual(
@ -368,8 +326,8 @@ class TestTraitStatic(_TraitHandlerBase):
} }
) )
def test_actual(self): def test_value(self):
"""Actual is base + mod""" """value is base + mod"""
self.assertEqual(self._get_values(), (1, 2, 3)) self.assertEqual(self._get_values(), (1, 2, 3))
self.trait.base += 4 self.trait.base += 4
self.assertEqual(self._get_values(), (5, 2, 7)) self.assertEqual(self._get_values(), (5, 2, 7))
@ -410,9 +368,9 @@ class TestTraitCounter(_TraitHandlerBase):
self.trait = self.traithandler.get("test1") self.trait = self.traithandler.get("test1")
def _get_values(self): def _get_values(self):
"""Get (base, mod, actual, min, max).""" """Get (base, mod, value, min, max)."""
return (self.trait.base, self.trait.mod, return (self.trait.base, self.trait.mod,
self.trait.actual, self.trait.min, self.trait.max) self.trait.value, self.trait.min, self.trait.max)
def test_init(self): def test_init(self):
self.assertEqual( self.assertEqual(
@ -437,8 +395,8 @@ class TestTraitCounter(_TraitHandlerBase):
} }
) )
def test_actual(self): def test_value(self):
"""Actual is current + mod, where current defaults to base""" """value is current + mod, where current defaults to base"""
self.assertEqual(self._get_values(), (1, 2, 3, 0, 10)) self.assertEqual(self._get_values(), (1, 2, 3, 0, 10))
self.trait.base += 4 self.trait.base += 4
self.assertEqual(self._get_values(), (5, 2, 7, 0, 10)) self.assertEqual(self._get_values(), (5, 2, 7, 0, 10))
@ -600,7 +558,7 @@ class TestTraitCounterTimed(_TraitHandlerBase):
self.trait = self.traithandler.get("test1") self.trait = self.traithandler.get("test1")
def _get_timer_data(self): def _get_timer_data(self):
return (self.trait.actual, self.trait.current, self.trait.rate, return (self.trait.value, self.trait.current, self.trait.rate,
self.trait._data["last_update"], self.trait.ratetarget) self.trait._data["last_update"], self.trait.ratetarget)
@patch("evennia.contrib.traits.time") @patch("evennia.contrib.traits.time")
@ -671,8 +629,8 @@ class TestTraitGauge(_TraitHandlerBase):
self.trait = self.traithandler.get("test1") self.trait = self.traithandler.get("test1")
def _get_values(self): def _get_values(self):
"""Get (base, mod, actual, min, max).""" """Get (base, mod, value, min, max)."""
return (self.trait.base, self.trait.mod, self.trait.actual, return (self.trait.base, self.trait.mod, self.trait.value,
self.trait.min, self.trait.max) self.trait.min, self.trait.max)
def test_init(self): def test_init(self):
@ -696,8 +654,8 @@ class TestTraitGauge(_TraitHandlerBase):
"last_update": None, "last_update": None,
} }
) )
def test_actual(self): def test_value(self):
"""Actual is current, where current defaults to base + mod""" """value is current, where current defaults to base + mod"""
# current unset - follows base + mod # current unset - follows base + mod
self.assertEqual(self._get_values(), (8, 2, 10, 0, 10)) self.assertEqual(self._get_values(), (8, 2, 10, 0, 10))
self.trait.base += 4 self.trait.base += 4
@ -864,7 +822,7 @@ class TestTraitGaugeTimed(_TraitHandlerBase):
self.trait = self.traithandler.get("test1") self.trait = self.traithandler.get("test1")
def _get_timer_data(self): def _get_timer_data(self):
return (self.trait.actual, self.trait.current, self.trait.rate, return (self.trait.value, self.trait.current, self.trait.rate,
self.trait._data["last_update"], self.trait.ratetarget) self.trait._data["last_update"], self.trait.ratetarget)
@patch("evennia.contrib.traits.time") @patch("evennia.contrib.traits.time")
@ -919,24 +877,24 @@ class TestNumericTraitOperators(TestCase):
"""Test case for numeric magic method implementations.""" """Test case for numeric magic method implementations."""
def setUp(self): def setUp(self):
# direct instantiation for testing only; use TraitHandler in production # direct instantiation for testing only; use TraitHandler in production
self.st = traits.NumericTrait({ self.st = traits.Trait({
'name': 'Strength', 'name': 'Strength',
'trait_type': 'numeric', 'trait_type': 'trait',
'base': 8, 'value': 8,
}) })
self.at = traits.NumericTrait({ self.at = traits.Trait({
'name': 'Attack', 'name': 'Attack',
'trait_type': 'numeric', 'trait_type': 'trait',
'base': 4, 'value': 4,
}) })
def tearDown(self): def tearDown(self):
self.st, self.at = None, None self.st, self.at = None, None
def test_pos_shortcut(self): def test_pos_shortcut(self):
"""overridden unary + operator returns `actual` property""" """overridden unary + operator returns `value` property"""
self.assertIn(type(+self.st), (float, int)) self.assertIn(type(+self.st), (float, int))
self.assertEqual(+self.st, self.st.actual) self.assertEqual(+self.st, self.st.value)
self.assertEqual(+self.st, 8) self.assertEqual(+self.st, 8)
def test_add_traits(self): def test_add_traits(self):

View file

@ -46,8 +46,8 @@ A trait is added to the traithandler, after which one can access it
as a property on the handler (similarly to how you can do .db.attrname for Attributes as a property on the handler (similarly to how you can do .db.attrname for Attributes
in Evennia). in Evennia).
```python ```python
# this is an example using the "static" trait, described below
>>> obj.traits.add("hunting", "Hunting Skill", trait_type="static", base=4) >>> obj.traits.add("hunting", "Hunting Skill", trait_type="static", base=4)
>>> obj.traits.hunting.value >>> obj.traits.hunting.value
4 4
@ -55,21 +55,25 @@ in Evennia).
>>> obj.traits.hunting.value >>> obj.traits.hunting.value
9 9
>>> obj.traits.add("hp", "Health", trait_type="gauge", min=0, max=100) >>> obj.traits.add("hp", "Health", trait_type="gauge", min=0, max=100)
>>> obj.traits.hp.actual >>> obj.traits.hp.value
100 100
>>> obj.traits.hp -= 200 >>> obj.traits.hp -= 200
>>> obj.traits.hp.actual >>> obj.traits.hp.value
0 0
>>> obj.traits.hp.reset() >>> obj.traits.hp.reset()
>>> obj.traits.hp.actual >>> obj.traits.hp.value
100 100
# you can also access property with getitem # you can also access property with getitem
>>> obj.traits.hp["actual"] >>> obj.traits.hp["value"]
100 100
# you can store arbitrary data persistently as well
>>> obj.traits.hp.effect = "poisoned!"
>>> obj.traits.hp.effect
"poisoned!"
``` ```
When creating the trait, you supply the name of the property (`hunting`) along When adding the trait, you supply the name of the property (`hunting`) along
with a more human-friendly name ("Hunting Skill"). The latter will show if you with a more human-friendly name ("Hunting Skill"). The latter will show if you
print the trait etc. The `trait_type` is important, this specifies which type print the trait etc. The `trait_type` is important, this specifies which type
of trait this is. of trait this is.
@ -77,11 +81,23 @@ of trait this is.
## Trait types ## Trait types
All the default-available traits are number-based. They all have a read-only All default traits have a read-only `.value` property that shows the relevant or
`actual` property that shows the relevant value. Exactly what this means depends 'current' value of the trait. Exactly what this means depends on the type of trait.
on the type of trait.
Traits can also be combined to do arithmetic with their .value, if both have a
compatible type.
```python
>>> trait1 + trait2
54
>>> trait1.value
3
>>> trait1 + 2
>>> trait1.value
5
```
Numerical traits can also be combined to do arithmetic with their .actual values.
Two numerical traits can also be compared (bigger-than etc), which is useful in Two numerical traits can also be compared (bigger-than etc), which is useful in
all sorts of rule-resolution. all sorts of rule-resolution.
@ -91,55 +107,52 @@ if trait1 > trait2:
# do stuff # do stuff
``` ```
## Static trait
## Static traits `value = base + mod`
The static trait has a `base` value and an optional `mod`-ifier. A typical use
actual = base + mod of a static trait would be a Strength stat or Skill value. That is, something
that varies slowly or not at all, and which may be modified in-place.
The static trait has
a `base` value and an optional `mod`-ifier. A typical use of a static trait
would be a Strength stat or Skill value. That is, something that varies slowly or
not at all, and which can have modifier.
```python ```python
>>> obj.traits.add("str", "Strength", trait_type="static", base=10, mod=2) >>> obj.traits.add("str", "Strength", trait_type="static", base=10, mod=2)
>>> obj.traits.mytrait.actual >>> obj.traits.mytrait.value
12 # base + mod 12 # base + mod
>>> obj.traits.mytrait.base += 2 >>> obj.traits.mytrait.base += 2
>>> obj.traits.mytrait.mod += 1 >>> obj.traits.mytrait.mod += 1
>>> obj.traits.mytrait.actual >>> obj.traits.mytrait.value
15 15
>>> obj.traits.mytrait.mod = 0 >>> obj.traits.mytrait.mod = 0
>>> obj.traits.mytrait.actual >>> obj.traits.mytrait.value
12 12
``` ```
### Counter ### Counter
min/unset base base+mod max/unset min/unset base base+mod max/unset
|--------------|--------|---------X--------X------------| |--------------|--------|---------X--------X------------|
current actual current value
= current = current
+ mod + mod
A counter describes a value that varies from a base value. The `current` property A counter describes a value that can move from a base. The `current` property
starts at the `base` and tracks the current value. One can also add a modifier, is the thing usually modified. It starts at the `base`. One can also add a modifier,
which will both be added to the base and to current (forming actual). which will both be added to the base and to current (forming .value).
The min/max of the range are optional, a boundary set to None will remove it. The min/max of the range are optional, a boundary set to None will remove it.
```python ```python
>>> obj.traits.add("hunting", "Hunting Skill", trait_type="counter", >>> obj.traits.add("hunting", "Hunting Skill", trait_type="counter",
base=10, mod=1, min=0, max=100) base=10, mod=1, min=0, max=100)
>>> obj.traits.hunting.actual >>> obj.traits.hunting.value
11 # current starts at base + mod 11 # current starts at base + mod
>>> obj.traits.hunting.current += 10 >>> obj.traits.hunting.current += 10
>>> obj.traits.hunting.actual >>> obj.traits.hunting.value
21 21
# reset back to base+mod by deleting # reset back to base+mod by deleting current
>>> del obj.traits.hunting.current >>> del obj.traits.hunting.current
>>> obj.traits.hunting.actual >>> obj.traits.hunting.value
11 11
>>> obj.traits.hunting.max = None # removing upper bound >>> obj.traits.hunting.max = None # removing upper bound
@ -147,60 +160,71 @@ The min/max of the range are optional, a boundary set to None will remove it.
Counters have some extra properties: Counters have some extra properties:
`descs` is a dict of upper-bounds to a text description. This allows for easily `descs` is a dict {upper_bound:text_description}. This allows for easily
storing getting a more human-friendly description of the current value in the storing a more human-friendly description of the current value in the
interval. Here is an example for skill values between 0 and 10: interval. Here is an example for skill values between 0 and 10:
{0: "unskilled", 1: "neophyte", 5: "trained", 7: "expert", 9: "master"} {0: "unskilled", 1: "neophyte", 5: "trained", 7: "expert", 9: "master"}
The list must go from smallest to largest. Any values below the lowest and above the The keys must be supplied from smallest to largest. Any values below the lowest and above the
highest description will be considered to be included in the closest description slot. highest description will be considered to be included in the closest description slot.
By calling `.desc()` on the Counter, will you get the text matching the current `actual` By calling `.desc()` on the Counter, will you get the text matching the current `value`
value. value.
```python ```python
# (could also have passed descs= to traits.add()) # (could also have passed descs= to traits.add())
>>> obj.traits.hunting.descs = { >>> obj.traits.hunting.descs = {
0: "unskilled", 10: "neophyte", 50: "trained", 70: "expert", 90: "master"} 0: "unskilled", 10: "neophyte", 50: "trained", 70: "expert", 90: "master"}
>>> obj.traits.hunting.actual >>> obj.traits.hunting.value
11 11
>>> obj.traits.hunting.desc() >>> obj.traits.hunting.desc()
"neophyte" "neophyte"
>>> obj.traits.hunting.current += 60 >>> obj.traits.hunting.current += 60
>>> obj.traits.hunting.actual >>> obj.traits.hunting.value
71 71
>>> obj.traits.hunting.desc() >>> obj.traits.hunting.desc()
"expert" "expert"
``` ```
`rate` defaults to 0, but allows the trait to change value dynamically. This could be #### .rate
used for example for an attribute that was temporarily lowered but will gradually
(or abruptly) recover after a certain time. The rate is given per-second, and the value
will still be restrained by min/max boundaries, if given.
It is also possible to set a "ratetarget", for the auto-change to stop at (rather The `rate` property defaults to 0. If set to a value different from 0, it
than at the min/max boundaries). This allows for returning to some previous value. allows the trait to change value dynamically. This could be used for example
for an attribute that was temporarily lowered but will gradually (or abruptly)
recover after a certain time. The rate is given as change of the `current`
per-second, and the .value will still be restrained by min/max boundaries, if
those are set.
It is also possible to set a ".ratetarget", for the auto-change to stop at
(rather than at the min/max boundaries). This allows the value to return to
a previous value.
```python ```python
>>> obj.traits.hunting.actual >>> obj.traits.hunting.value
71 71
>>> obj.traits.hunting.ratetarget = 71 >>> obj.traits.hunting.ratetarget = 71
# debuff hunting for some reason
>>> obj.traits.hunting.current -= 30 >>> obj.traits.hunting.current -= 30
>>> obj.traits.hunting.actual >>> obj.traits.hunting.value
41 41
>>> obj.traits.hunting.rate = 1 # 1/s increase >>> obj.traits.hunting.rate = 1 # 1/s increase
# Waiting 5s # Waiting 5s
>>> obj.traits.hunting.actual >>> obj.traits.hunting.value
46 46
# Waiting 8s # Waiting 8s
>>> obj.traits.hunting.actual >>> obj.traits.hunting.value
54 54
# Waiting 100s # Waiting 100s
>>> obj.traits.hunting.actual >>> obj.traits.hunting.value
71 # we have stopped at the ratetarget 71 # we have stopped at the ratetarget
>>> obj.traits.hunting.rate = 0 # disable auto-change >>> obj.traits.hunting.rate = 0 # disable auto-change
``` ```
Note that if rate is a non-integer, the resulting .value (at least until it
reaches the boundary) will likely also come out a float. If you expect an
integer, you must run run int() on the result yourself.
#### .percentage()
If both min and max are defined, the `.percentage()` method of the trait will If both min and max are defined, the `.percentage()` method of the trait will
return the value as a percentage. return the value as a percentage.
@ -211,31 +235,30 @@ return the value as a percentage.
``` ```
### Gauge ### Gauge
This emulates a [fuel-] gauge, that empties from a base+mod value. This emulates a [fuel-] gauge that empties from a base+mod value.
min/0 max=base+mod min/0 max=base+mod
|-----------------------X---------------------------| |-----------------------X---------------------------|
actual value
= current = current
The 'current' value will be with a full gauge. Modifiers only add to the maximum, The 'current' value will start from a full gauge. The .max property is
which is set by base + mod. The minimum bound defaults to 0. This trait is useful read-only and is set by .base + .mod. So contrary to a Counter, the modifier
for showing resources that can deplete, like health or stamina etc. only applies to the max value of the gauge and not the current value. The
minimum bound defaults to 0. This trait is useful for showing resources that
can deplete, like health, stamina and the like.
```python ```python
>>> obj.traits.add("hp", "Health", trait_type="gauge", base=100) >>> obj.traits.add("hp", "Health", trait_type="gauge", base=100)
>>> obj.traits.hp.actual # (or .current) >>> obj.traits.hp.value # (or .current)
100 100
>>> obj.traits.hp.mod = 10 >>> obj.traits.hp.mod = 10
>>> obj.traits.hp.actual >>> obj.traits.hp.value
110 110
>>> obj.traits.hp.current -= 30 >>> obj.traits.hp.current -= 30
>>> obj.traits.hp.actual >>> obj.traits.hp.value
80 80
``` ```
@ -246,14 +269,15 @@ for gauges, for everything from poison slowly draining your health, to resting g
increasing it. You can also use the `.percentage()` function to show the current value increasing it. You can also use the `.percentage()` function to show the current value
as a percentage. as a percentage.
### Trait ### Trait
A single value of any type. A single value of any type.
This is not a numerical trait and does not have an .actual property. This is This is the 'base' Trait, meant to inherit from if you want to make your own
the 'base' Trait, meant to inherit from if you want to make your own trait-types (see below). Its .value can be anything (that can be stored in an Attribute)
trait-types (see below), but you can also use it directly: and if it's a integer/float you can do arithmetic with it, but otherwise it
acts just like a glorified Attribute.
```python ```python
>>> obj.traits.add("mytrait", "My Trait", trait_type="trait", value=30) >>> obj.traits.add("mytrait", "My Trait", trait_type="trait", value=30)
@ -264,17 +288,6 @@ trait-types (see below), but you can also use it directly:
"stringvalue" "stringvalue"
``` ```
The "trait" trait-type is little more than a glorified Attribute. It has a .value
that can be anything, and nothing more fancy. It's meant to expand on.
### NumericTrait
A single value, actual = base
This is a base class for the numeric traits. Basically the Static Trait but
without the modifier. Is useful to inherit from. It adds arithmetic so that
you can add two traits together, do comparisons between them etc.
## Expanding with your own Traits ## Expanding with your own Traits
@ -289,7 +302,7 @@ from evennia.contrib.traits import Trait
class RageTrait(Trait): class RageTrait(Trait):
trait_type = "rage" trait_type = "rage"
data_keys = { default_keys = {
"rage": 0 "rage": 0
} }
@ -334,6 +347,7 @@ from evennia.utils.utils import (
# "counter" and "gauge". # "counter" and "gauge".
_TRAIT_CLASS_PATHS = [ _TRAIT_CLASS_PATHS = [
"evennia.contrib.traits.Trait",
"evennia.contrib.traits.StaticTrait", "evennia.contrib.traits.StaticTrait",
"evennia.contrib.traits.CounterTrait", "evennia.contrib.traits.CounterTrait",
"evennia.contrib.traits.GaugeTrait", "evennia.contrib.traits.GaugeTrait",
@ -443,7 +457,7 @@ class TraitHandler:
_SA(self, trait_key, value) _SA(self, trait_key, value)
else: else:
trait_cls = self._get_trait_class(trait_key=trait_key) trait_cls = self._get_trait_class(trait_key=trait_key)
valid_keys = list_to_string(list(trait_cls.data_keys.keys()), endsep="or") valid_keys = list_to_string(list(trait_cls.default_keys.keys()), endsep="or")
raise TraitException( raise TraitException(
"Trait object not settable directly. " "Trait object not settable directly. "
f"Assign to {trait_key}.{valid_keys}." f"Assign to {trait_key}.{valid_keys}."
@ -578,7 +592,7 @@ class TraitHandler:
# Parent Trait class # Parent Trait class
@total_ordering
class Trait: class Trait:
"""Represents an object or Character trait. This simple base is just """Represents an object or Character trait. This simple base is just
storing anything in it's 'value' property, so it's pretty much just a storing anything in it's 'value' property, so it's pretty much just a
@ -600,7 +614,7 @@ class Trait:
# not given, set the default value to the `traits.MandatoryTraitKey` class. # not given, set the default value to the `traits.MandatoryTraitKey` class.
# Apart from the keys given here, "name" and "trait_type" will also always # Apart from the keys given here, "name" and "trait_type" will also always
# have to be a apart of the data. # have to be a apart of the data.
data_keys = {"value": None} default_keys = {"value": None}
# enable to set/retrieve other arbitrary properties on the Trait # enable to set/retrieve other arbitrary properties on the Trait
# and have them treated like data to store. # and have them treated like data to store.
@ -615,7 +629,7 @@ class Trait:
Args: Args:
trait_data (any): Any pickle-able values to store with this trait. trait_data (any): Any pickle-able values to store with this trait.
This must contain any cls.data_keys that do not have a default This must contain any cls.default_keys that do not have a default
value in cls.data_default_values. Any extra kwargs will be made value in cls.data_default_values. Any extra kwargs will be made
available as extra properties on the Trait, assuming the class available as extra properties on the Trait, assuming the class
variable `allow_extra_properties` is set. variable `allow_extra_properties` is set.
@ -642,7 +656,7 @@ class Trait:
initialization of this trait. initialization of this trait.
Returns: Returns:
dict: Validated data, possibly complemented with default dict: Validated data, possibly complemented with default
values from data_keys. values from default_keys.
Raises: Raises:
TraitException: If finding unset keys without a default. TraitException: If finding unset keys without a default.
@ -663,9 +677,9 @@ class Trait:
_raise_err(unsets) _raise_err(unsets)
# check other keys, these likely have defaults to fall back to # check other keys, these likely have defaults to fall back to
req = set(list(cls.data_keys.keys())) req = set(list(cls.default_keys.keys()))
unsets = req.difference(inp.intersection(req)) unsets = req.difference(inp.intersection(req))
unset_defaults = {key: cls.data_keys[key] for key in unsets} unset_defaults = {key: cls.default_keys[key] for key in unsets}
if MandatoryTraitKey in unset_defaults.values(): if MandatoryTraitKey in unset_defaults.values():
# we have one or more unset keys that was mandatory # we have one or more unset keys that was mandatory
@ -701,7 +715,7 @@ 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"): if key in ("default_keys", "data_default", "trait_type", "allow_extra_properties"):
return _GA(self, key) return _GA(self, key)
try: try:
return self._data[key] return self._data[key]
@ -752,18 +766,18 @@ class Trait:
without a default value to reset to. without a default value to reset to.
Notes: Notes:
This will outright delete extra keys (if allow_extra_properties is This will outright delete extra keys (if allow_extra_properties is
set). Keys in self.data_keys with a default value will be set). Keys in self.default_keys with a default value will be
reset to default. A data_key with a default of MandatoryDefaultKey reset to default. A data_key with a default of MandatoryDefaultKey
will raise a TraitException. Unfound matches will be silently ignored. will raise a TraitException. Unfound matches will be silently ignored.
""" """
if key in self.data_keys: if key in self.default_keys:
if self.data_keys[key] == MandatoryTraitKey: if self.default_keys[key] == MandatoryTraitKey:
raise TraitException( raise TraitException(
"Trait-Key {key} cannot be deleted: It's a mandatory property " "Trait-Key {key} cannot be deleted: It's a mandatory property "
"with no default value to fall back to.") "with no default value to fall back to.")
# set to default # set to default
self._data[key] = self.data_keys[key] self._data[key] = self.default_keys[key]
elif key in self._data: elif key in self._data:
try: try:
# check if we have a custom deleter # check if we have a custom deleter
@ -783,7 +797,7 @@ class Trait:
return "{}({{{}}})".format( return "{}({{{}}})".format(
type(self).__name__, type(self).__name__,
", ".join( ", ".join(
["'{}': {!r}".format(k, self._data[k]) for k in self.data_keys if k in self._data] ["'{}': {!r}".format(k, self._data[k]) for k in self.default_keys if k in self._data]
), ),
) )
@ -799,37 +813,6 @@ class Trait:
key = name key = name
@property
def value(self):
"""Store a value"""
return self._data["value"]
@value.setter
def value(self, value):
"""Get value"""
self._data["value"] = value
@total_ordering
class NumericTrait(Trait):
"""
Base trait for all Traits based on numbers. This implements
number-comparisons, limits etc. It works on the 'base' property since this
makes more sense for child classes. For this base class, the .actual
property is just an alias of .base.
actual = base
"""
trait_type = "numeric"
data_keys = {
"base": 0
}
def __str__(self):
return f"<Trait {self.name}: {self._data['base']}>"
# Numeric operations # Numeric operations
def __eq__(self, other): def __eq__(self, other):
@ -841,58 +824,58 @@ class NumericTrait(Trait):
`__eq__` and `__lt__` are implemented. `__eq__` and `__lt__` are implemented.
""" """
if inherits_from(other, Trait): if inherits_from(other, Trait):
return self.actual == other.actual return self.value == other.value
elif type(other) in (float, int): elif type(other) in (float, int):
return self.actual == other return self.value == other
else: else:
return NotImplemented return NotImplemented
def __lt__(self, other): def __lt__(self, other):
"""Support less than comparison between `Trait`s or `Trait` and numeric.""" """Support less than comparison between `Trait`s or `Trait` and numeric."""
if inherits_from(other, Trait): if inherits_from(other, Trait):
return self.actual < other.actual return self.value < other.value
elif type(other) in (float, int): elif type(other) in (float, int):
return self.actual < other return self.value < other
else: else:
return NotImplemented return NotImplemented
def __pos__(self): def __pos__(self):
"""Access `actual` property through unary `+` operator.""" """Access `value` property through unary `+` operator."""
return self.actual return self.value
def __add__(self, other): def __add__(self, other):
"""Support addition between `Trait`s or `Trait` and numeric""" """Support addition between `Trait`s or `Trait` and numeric"""
if inherits_from(other, Trait): if inherits_from(other, Trait):
return self.actual + other.actual return self.value + other.value
elif type(other) in (float, int): elif type(other) in (float, int):
return self.actual + other return self.value + other
else: else:
return NotImplemented return NotImplemented
def __sub__(self, other): def __sub__(self, other):
"""Support subtraction between `Trait`s or `Trait` and numeric""" """Support subtraction between `Trait`s or `Trait` and numeric"""
if inherits_from(other, Trait): if inherits_from(other, Trait):
return self.actual - other.actual return self.value - other.value
elif type(other) in (float, int): elif type(other) in (float, int):
return self.actual - other return self.value - other
else: else:
return NotImplemented return NotImplemented
def __mul__(self, other): def __mul__(self, other):
"""Support multiplication between `Trait`s or `Trait` and numeric""" """Support multiplication between `Trait`s or `Trait` and numeric"""
if inherits_from(other, Trait): if inherits_from(other, Trait):
return self.actual * other.actual return self.value * other.value
elif type(other) in (float, int): elif type(other) in (float, int):
return self.actual * other return self.value * other
else: else:
return NotImplemented return NotImplemented
def __floordiv__(self, other): def __floordiv__(self, other):
"""Support floor division between `Trait`s or `Trait` and numeric""" """Support floor division between `Trait`s or `Trait` and numeric"""
if inherits_from(other, Trait): if inherits_from(other, Trait):
return self.actual // other.actual return self.value // other.value
elif type(other) in (float, int): elif type(other) in (float, int):
return self.actual // other return self.value // other
else: else:
return NotImplemented return NotImplemented
@ -903,64 +886,53 @@ class NumericTrait(Trait):
def __rsub__(self, other): def __rsub__(self, other):
"""Support subtraction between `Trait`s or `Trait` and numeric""" """Support subtraction between `Trait`s or `Trait` and numeric"""
if inherits_from(other, Trait): if inherits_from(other, Trait):
return other.actual - self.actual return other.value - self.value
elif type(other) in (float, int): elif type(other) in (float, int):
return other - self.actual return other - self.value
else: else:
return NotImplemented return NotImplemented
def __rfloordiv__(self, other): def __rfloordiv__(self, other):
"""Support floor division between `Trait`s or `Trait` and numeric""" """Support floor division between `Trait`s or `Trait` and numeric"""
if inherits_from(other, Trait): if inherits_from(other, Trait):
return other.actual // self.actual return other.value // self.value
elif type(other) in (float, int): elif type(other) in (float, int):
return other // self.actual return other // self.value
else: else:
return NotImplemented return NotImplemented
# Public members # Public members
@property @property
def actual(self): def value(self):
"The actual value of the trait" """Store a value"""
return self.base return self._data["value"]
@property @value.setter
def base(self): def value(self, value):
"""The trait's base value. """Get value"""
self._data["value"] = value
Note:
The setter for this property will enforce any range bounds set
on this `Trait`.
"""
return self._data["base"]
@base.setter
def base(self, value):
"""Base must be a numerical value."""
if type(value) in (int, float):
self._data["base"] = value
# Implementation of the respective Trait types # Implementation of the respective Trait types
class StaticTrait(NumericTrait): class StaticTrait(Trait):
""" """
Static Trait. This is a single value with a modifier, Static Trait. This is a single value with a modifier,
with no concept of a 'current' value. with no concept of a 'current' value.
actual = base + mod value = base + mod
""" """
trait_type = "static" trait_type = "static"
data_keys = { default_keys = {
"base": 0, "base": 0,
"mod": 0 "mod": 0
} }
def __str__(self): def __str__(self):
status = "{actual:11}".format(actual=self.actual) status = "{value:11}".format(value=self.value)
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)
# Helpers # Helpers
@ -976,12 +948,12 @@ class StaticTrait(NumericTrait):
self._data["mod"] = amount self._data["mod"] = amount
@property @property
def actual(self): def value(self):
"The actual value of the Trait" "The value of the Trait"
return self.base + self.mod return self.base + self.mod
class CounterTrait(NumericTrait): class CounterTrait(Trait):
""" """
Counter Trait. Counter Trait.
@ -990,15 +962,15 @@ class CounterTrait(NumericTrait):
min/unset base base+mod max/unset min/unset base base+mod max/unset
|--------------|--------|---------X--------X------------| |--------------|--------|---------X--------X------------|
current actual current value
= current = current
+ mod + mod
- actual = current + mod, starts at base + mod - value = current + mod, starts at base + mod
- if min or max is None, there is no upper/lower bound (default) - if min or max is None, there is no upper/lower bound (default)
- if max is set to "base", max will be equal ot base+mod - if max is set to "base", max will be equal ot base+mod
- descs are used to optionally describe each value interval. - descs are used to optionally describe each value interval.
The desc of the current `actual` value can then be retrieved The desc of the current `value` value can then be retrieved
with .desc(). The property is set as {lower_bound_inclusive:desc} with .desc(). The property is set as {lower_bound_inclusive:desc}
and should be given smallest-to-biggest. For example, for and should be given smallest-to-biggest. For example, for
a skill rating between 0 and 10: a skill rating between 0 and 10:
@ -1018,7 +990,7 @@ class CounterTrait(NumericTrait):
trait_type = "counter" trait_type = "counter"
# current starts equal to base. # current starts equal to base.
data_keys = { default_keys = {
"base": 0, "base": 0,
"mod": 0, "mod": 0,
"min": None, "min": None,
@ -1095,16 +1067,16 @@ class CounterTrait(NumericTrait):
now = time() now = time()
tdiff = now - self._data['last_update'] tdiff = now - self._data['last_update']
current += rate * tdiff current += rate * tdiff
actual = current + self.mod value = current + self.mod
# we must make sure so we don't overstep our bounds # we must make sure so we don't overstep our bounds
# even if .mod is included # even if .mod is included
if self._passed_ratetarget(actual): if self._passed_ratetarget(value):
current = self._data['ratetarget'] - self.mod current = self._data['ratetarget'] - self.mod
self._stop_timer() self._stop_timer()
elif not self._within_boundaries(actual): elif not self._within_boundaries(value):
current = self._enforce_boundaries(actual) - self.mod current = self._enforce_boundaries(value) - self.mod
self._stop_timer() self._stop_timer()
else: else:
self._data['last_update'] = now self._data['last_update'] = now
@ -1122,7 +1094,7 @@ class CounterTrait(NumericTrait):
@base.setter @base.setter
def base(self, value): def base(self, value):
if value is None: if value is None:
self._data["base"] = self.data_keys['base'] self._data["base"] = self.default_keys['base']
if type(value) in (int, float): if type(value) in (int, float):
if self.min is not None and value + self.mod < self.min: if self.min is not None and value + self.mod < self.min:
value = self.min - self.mod value = self.min - self.mod
@ -1138,7 +1110,7 @@ class CounterTrait(NumericTrait):
def mod(self, value): def mod(self, value):
if value is None: if value is None:
# unsetting the boundary to default # unsetting the boundary to default
self._data["mod"] = self.data_keys['mod'] self._data["mod"] = self.default_keys['mod']
elif type(value) in (int, float): elif type(value) in (int, float):
if self.min is not None and value + self.base < self.min: if self.min is not None and value + self.base < self.min:
value = self.min - self.base value = self.min - self.base
@ -1190,8 +1162,8 @@ class CounterTrait(NumericTrait):
self._data["current"] = self.base self._data["current"] = self.base
@property @property
def actual(self): def value(self):
"The actual value of the Trait (current + mod)" "The value of the Trait (current + mod)"
return self._enforce_boundaries(self.current + self.mod) return self._enforce_boundaries(self.current + self.mod)
@property @property
@ -1201,7 +1173,7 @@ class CounterTrait(NumericTrait):
@ratetarget.setter @ratetarget.setter
def ratetarget(self, value): def ratetarget(self, value):
self._data['ratetarget'] = self._enforce_boundaries(value) self._data['ratetarget'] = self._enforce_boundaries(value)
self._check_and_start_timer(self.actual) self._check_and_start_timer(self.value)
def percent(self, formatting="{:3.1f}%"): def percent(self, formatting="{:3.1f}%"):
""" """
@ -1216,7 +1188,7 @@ class CounterTrait(NumericTrait):
float or str: Depending of if a `formatting` string float or str: Depending of if a `formatting` string
is supplied or not. is supplied or not.
""" """
return percent(self.actual, self.min, self.max, formatting=formatting) return percent(self.value, self.min, self.max, formatting=formatting)
def reset(self): def reset(self):
"""Resets `current` property equal to `base` value.""" """Resets `current` property equal to `base` value."""
@ -1233,13 +1205,13 @@ class CounterTrait(NumericTrait):
describe the interval. describe the interval.
Returns: Returns:
str: The description describing the `actual` value. str: The description describing the `value` value.
If not found, returns the empty string. If not found, returns the empty string.
""" """
descs = self._data["descs"] descs = self._data["descs"]
if descs is None: if descs is None:
return "" return ""
value = self.actual value = self.value
# we rely on Python3.7+ dicts retaining ordering # we rely on Python3.7+ dicts retaining ordering
highest = "" highest = ""
for bound, txt in descs.items(): for bound, txt in descs.items():
@ -1259,13 +1231,13 @@ class GaugeTrait(CounterTrait):
min/0 max=base+mod min/0 max=base+mod
|-----------------------X---------------------------| |-----------------------X---------------------------|
actual value
= current = current
- min defaults to 0 - min defaults to 0
- max value is always base + mad - max value is always base + mad
- .max is an alias of .base - .max is an alias of .base
- actual = current and varies from min to max. - value = current and varies from min to max.
- descs is a mapping {upper_bound_inclusive: desc}. These - descs is a mapping {upper_bound_inclusive: desc}. These
are checked with .desc() and can be retrieve a text are checked with .desc() and can be retrieve a text
description for a given current value. description for a given current value.
@ -1284,7 +1256,7 @@ class GaugeTrait(CounterTrait):
# same as Counter, here for easy reference # same as Counter, here for easy reference
# current starts out equal to base # current starts out equal to base
data_keys = { default_keys = {
"base": 0, "base": 0,
"mod": 0, "mod": 0,
"min": 0, "min": 0,
@ -1300,15 +1272,15 @@ class GaugeTrait(CounterTrait):
now = time() now = time()
tdiff = now - self._data['last_update'] tdiff = now - self._data['last_update']
current += rate * tdiff current += rate * tdiff
actual = current value = current
# we don't worry about .mod for gauges # we don't worry about .mod for gauges
if self._passed_ratetarget(actual): if self._passed_ratetarget(value):
current = self._data['ratetarget'] current = self._data['ratetarget']
self._stop_timer() self._stop_timer()
elif not self._within_boundaries(actual): elif not self._within_boundaries(value):
current = self._enforce_boundaries(actual) current = self._enforce_boundaries(value)
self._stop_timer() self._stop_timer()
else: else:
self._data['last_update'] = now self._data['last_update'] = now
@ -1324,7 +1296,7 @@ class GaugeTrait(CounterTrait):
return min(self.mod + self.base, value) return min(self.mod + self.base, value)
def __str__(self): def __str__(self):
status = "{actual:4} / {base:4}".format(actual=self.actual, base=self.base) status = "{value:4} / {base:4}".format(value=self.value, 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)
@property @property
@ -1354,13 +1326,13 @@ class GaugeTrait(CounterTrait):
@property @property
def min(self): def min(self):
val = self._data["min"] val = self._data["min"]
return self.data_keys["min"] if val is None else val return self.default_keys["min"] if val is None else val
@min.setter @min.setter
def min(self, value): def min(self, value):
"""Limit so min can never be greater than base+mod.""" """Limit so min can never be greater than base+mod."""
if value is None: if value is None:
self._data["min"] = self.data_keys['min'] self._data["min"] = self.default_keys['min']
elif type(value) in (int, float): elif type(value) in (int, float):
self._data["min"] = min(value, self.base + self.mod) self._data["min"] = min(value, self.base + self.mod)
@ -1395,8 +1367,8 @@ class GaugeTrait(CounterTrait):
self._data["current"] = self.base + self.mod self._data["current"] = self.base + self.mod
@property @property
def actual(self): def value(self):
"The actual value of the trait" "The value of the trait"
return self.current return self.current
def percent(self, formatting="{:3.1f}%"): def percent(self, formatting="{:3.1f}%"):