From d720acef1d7bb9cf24ea2712ba0d578c54bc163d Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 19 Apr 2020 15:47:11 +0200 Subject: [PATCH] Make Trait unit tests pass for base trait types --- evennia/contrib/test_traits.py | 293 +++++++++++++++++---------------- evennia/contrib/traits.py | 134 +++++++-------- evennia/utils/utils.py | 64 ++++--- 3 files changed, 264 insertions(+), 227 deletions(-) diff --git a/evennia/contrib/test_traits.py b/evennia/contrib/test_traits.py index 142c96063e..6a1e99c7b1 100644 --- a/evennia/contrib/test_traits.py +++ b/evennia/contrib/test_traits.py @@ -310,15 +310,15 @@ class TestTraitNumeric(_TraitHandlerBase): extra_val1="xvalue1", extra_val2="xvalue2" ) - self.trait1 = self.traithandler.get("test1") + self.trait = self.traithandler.get("test1") def _get_actuals(self): """Get trait actuals for comparisons""" - return self.trait1.actual, self.trait2.actual + return self.trait.actual, self.trait2.actual def test_init(self): self.assertEqual( - self.trait1._data, + self.trait._data, {"name": "Test1", "trait_type": "numeric", "base": 1, @@ -328,12 +328,12 @@ class TestTraitNumeric(_TraitHandlerBase): ) def test_set_wrong_type(self): - self.trait1.base = "foo" - self.assertEqual(self.trait1.base, 1) + self.trait.base = "foo" + self.assertEqual(self.trait.base, 1) def test_actual(self): - self.trait1.base = 10 - self.assertEqual(self.trait1.actual, 10) + self.trait.base = 10 + self.assertEqual(self.trait.actual, 10) class TestTraitStatic(_TraitHandlerBase): @@ -351,10 +351,10 @@ class TestTraitStatic(_TraitHandlerBase): extra_val1="xvalue1", extra_val2="xvalue2" ) - self.trait1 = self.traithandler.get("test1") + self.trait = self.traithandler.get("test1") def _get_values(self): - return self.trait1.base, self.trait1.mod, self.trait1.actual + return self.trait.base, self.trait.mod, self.trait.actual def test_init(self): self.assertEqual( @@ -371,16 +371,16 @@ class TestTraitStatic(_TraitHandlerBase): def test_actual(self): """Actual is base + mod""" self.assertEqual(self._get_values(), (1, 2, 3)) - self.trait1.base += 4 + self.trait.base += 4 self.assertEqual(self._get_values(), (5, 2, 7)) - self.trait1.mod -= 1 + self.trait.mod -= 1 self.assertEqual(self._get_values(), (5, 1, 6)) def test_delete(self): """Deleting resets to default.""" - del self.trait1.base + del self.trait.base self.assertEqual(self._get_values(), (0, 2, 2)) - del self.trait1.mod + del self.trait.mod self.assertEqual(self._get_values(), (0, 0, 0)) @@ -396,15 +396,17 @@ class TestTraitCounter(_TraitHandlerBase): trait_type='counter', base=1, mod=2, - min=-10, + min=0, max=10, extra_val1="xvalue1", extra_val2="xvalue2" ) - self.trait1 = self.traithandler.get("test1") + self.trait = self.traithandler.get("test1") def _get_values(self): - return self.trait1.base, self.trait1.mod, self.trait1.actual + """Get (base, mod, actual, min, max).""" + return (self.trait.base, self.trait.mod, + self.trait.actual, self.trait.min, self.trait.max) def test_init(self): self.assertEqual( @@ -413,7 +415,7 @@ class TestTraitCounter(_TraitHandlerBase): "trait_type": 'counter', "base": 1, "mod": 2, - "min": -10, + "min": 0, "max": 10, "extra_val1": "xvalue1", "extra_val2": "xvalue2" @@ -422,104 +424,116 @@ class TestTraitCounter(_TraitHandlerBase): def test_actual(self): """Actual is current + mod, where current defaults to base""" - self.assertEqual(self._get_values(), (1, 2, 3)) - self.trait1.base += 4 - self.assertEqual(self._get_values(), (5, 2, 7)) - self.trait1.mod -= 1 - self.assertEqual(self._get_values(), (5, 1, 6)) + self.assertEqual(self._get_values(), (1, 2, 3, 0, 10)) + self.trait.base += 4 + self.assertEqual(self._get_values(), (5, 2, 7, 0, 10)) + self.trait.mod -= 1 + self.assertEqual(self._get_values(), (5, 1, 6, 0, 10)) def test_boundaries__minmax(self): """Test range""" # should not exceed min/max values - self.trait1.base += 20 - self.assertEqual(self._get_values(), (10, 2, 10)) - self.trait1.base = 100 - self.assertEqual(self._get_values(), (10, 2, 10)) - self.trait1.base -= 40 - self.assertEqual(self._get_values(), (-10, 2, -8)) - self.trait1.base = -100 - self.assertEqual(self._get_values(), (-10, 2, -8)) + self.trait.base += 20 + self.assertEqual(self._get_values(), (8, 2, 10, 0, 10)) + self.trait.base = 100 + self.assertEqual(self._get_values(), (8, 2, 10, 0, 10)) + self.trait.base -= 40 + self.assertEqual(self._get_values(), (-2, 2, 0, 0, 10)) + self.trait.base = -100 + self.assertEqual(self._get_values(), (-2, 2, 0, 0, 10)) def test_boundaries__bigmod(self): """add a big mod""" - self.trait1.base = 5 - self.trait1.mod = 100 - self.assertEqual(self._get_values(), (5, 100, 10)) - self.trait1.mod = -100 - self.assertEqual(self._get_values(), (5, -100, -10)) + self.trait.base = 5 + self.trait.mod = 100 + self.assertEqual(self._get_values(), (5, 5, 10, 0, 10)) + self.trait.mod = -100 + self.assertEqual(self._get_values(), (5, -5, 0, 0, 10)) def test_boundaries__change_boundaries(self): """Change boundaries after base/mod change""" - self.trait1.base = 5 - self.trait1.mod = -100 - self.trait1.min = -20 - self.assertEqual(self._get_values(), (5, -100, -20)) - self.trait1.mod = 100 - self.trait1.max = 20 - self.assertEqual(self._get_values(), (5, 100, 20)) - - def test_boundaries__base_literal(self): - """Use the "base" literal makes the max become base+mod""" - self.trait1.base = 5 - self.trait1.mod = 100 - self.trait1.max = "base" - self.assertEqual(self._get_values(), (5, 100, 105)) + self.trait.base = 5 + self.trait.mod = -100 + self.trait.min = -20 + self.assertEqual(self._get_values(), (5, -5, 0, -20, 10)) + self.trait.mod -= 100 + self.assertEqual(self._get_values(), (5, -25, -20, -20, 10)) + self.trait.mod = 100 + self.trait.max = 20 + self.assertEqual(self._get_values(), (5, 5, 10, -20, 20)) + self.trait.mod = 100 + self.assertEqual(self._get_values(), (5, 15, 20, -20, 20)) def test_boundaries__disable(self): """Disable and re-enable boundaries""" - self.trait1.base = 5 - self.trait1.mod = 100 - del self.trait1.max - self.assertEqual(self.trait1.max, None) - del self.trait1.min - self.assertEqual(self.trait1.min, None) - self.trait1.base = 100 - self.assertEqual(self._get_values(), (100, 100, 200)) - self.trait1.base = -10 - self.assertEqual(self._get_values(), (-10, 100, 90)) + self.trait.base = 5 + self.trait.mod = 100 + self.assertEqual(self._get_values(), (5, 5, 10, 0, 10)) + del self.trait.max + self.assertEqual(self.trait.max, None) + del self.trait.min + self.assertEqual(self.trait.min, None) + self.trait.base = 100 + self.assertEqual(self._get_values(), (100, 5, 105, None, None)) + self.trait.base = -200 + self.assertEqual(self._get_values(), (-200, 5, -195, None, None)) # re-activate boundaries - self.trait1.max = 15 - self.trait1.min = 10 - self.assertEqual(self._get_values(), (-10, 100, 15)) + self.trait.max = 15 + self.trait.min = 10 # his is blocked since base+mod is lower + self.assertEqual(self._get_values(), (-200, 5, -195, -195, 15)) def test_boundaries__inverse(self): """Set inverse boundaries - limited by base""" - self.trait1.base = -10 - self.trait1.mod = 100 - self.trait1.min = 20 # will be set to base - self.assertEqual(self.trait1.min, -10) - self.trait1.max = -20 - self.assertEqual(self.trait1.max, -10) - self.assertEqual(self._get_values(), (-10, 100, -10)) + self.trait.mod = 0 + self.assertEqual(self._get_values(), (1, 0, 1, 0, 10)) + self.trait.min = 20 # will be set to base + self.assertEqual(self._get_values(), (1, 0, 1, 1, 10)) + self.trait.max = -20 + self.assertEqual(self._get_values(), (1, 0, 1, 1, 1)) def test_current(self): """Modifying current value""" - self.trait1.current = 5 - self.assertEqual(self._get_values(), (1, 2, 7)) - self.trait1.current = 10 - self.assertEqual(self._get_values(), (1, 2, 10)) - self.trait1.current = 12 - self.assertEqual(self._get_values(), (1, 2, 10)) + self.trait.current = 5 + self.assertEqual(self._get_values(), (1, 2, 7, 0, 10)) + self.trait.current = 10 + self.assertEqual(self._get_values(), (1, 2, 10, 0, 10)) + self.trait.current = 12 + self.assertEqual(self._get_values(), (1, 2, 10, 0, 10)) + self.trait.current = -1 + self.assertEqual(self._get_values(), (1, 2, 2, 0, 10)) + self.trait.current -= 10 + self.assertEqual(self._get_values(), (1, 2, 2, 0, 10)) def test_delete(self): """Deleting resets to default.""" - del self.trait1.base - self.assertEqual(self._get_values(), (0, 2, 2)) - del self.trait1.mod - self.assertEqual(self._get_values(), (0, 0, 0)) - del self.trait1.min - del self.trait1.max - self.assertEqual(self.trait1.max, None) - self.assertEqual(self.trait1.min, None) + del self.trait.base + self.assertEqual(self._get_values(), (0, 2, 2, 0, 10)) + del self.trait.mod + self.assertEqual(self._get_values(), (0, 0, 0, 0, 10)) + del self.trait.min + del self.trait.max + self.assertEqual(self._get_values(), (0, 0, 0, None, None)) def test_percentage(self): """Test percentage calculation""" - self.assertEqual(self.trait1.percent(), "100.0%") - self.trait1.current = 5 - self.assertEqual(self.trait1.percent(), "50.0%") - self.trait1.current = 3 - self.assertEqual(self.trait1.percent(), "33.3%") + self.trait.base = 8 + self.trait.mod = 2 + self.trait.min = 0 + self.trait.max = 10 + self.assertEqual(self.trait.percent(), "100.0%") + self.trait.current = 3 + self.assertEqual(self.trait.percent(), "50.0%") + self.trait.current = 1 + self.assertEqual(self.trait.percent(), "30.0%") + # have to lower this since max cannot be lowered below base+mod + self.trait.mod = 1 + self.trait.current = 2 + self.trait.max -= 1 + self.assertEqual(self.trait.percent(), "33.3%") + # open boundary + del self.trait.min + self.assertEqual(self.trait.percent(), "100.0%") class TestTraitGauge(_TraitHandlerBase): @@ -535,11 +549,12 @@ class TestTraitGauge(_TraitHandlerBase): extra_val1="xvalue1", extra_val2="xvalue2" ) - self.trait1 = self.traithandler.get("test1") + self.trait = self.traithandler.get("test1") def _get_values(self): - return (self.trait1.base, self.trait1.mod, self.trait1.actual, - self.trait1.min, self.trait1.max) + """Get (base, mod, actual, min, max).""" + return (self.trait.base, self.trait.mod, self.trait.actual, + self.trait.min, self.trait.max) def test_init(self): self.assertEqual( @@ -557,119 +572,121 @@ class TestTraitGauge(_TraitHandlerBase): """Actual is current, where current defaults to base + mod""" # current unset - follows base + mod self.assertEqual(self._get_values(), (8, 2, 10, 0, 10)) - self.trait1.base += 4 + self.trait.base += 4 self.assertEqual(self._get_values(), (12, 2, 14, 0, 14)) - self.trait1.mod -= 1 + self.trait.mod -= 1 self.assertEqual(self._get_values(), (12, 1, 13, 0, 13)) # set current, decouple from base + mod - self.trait1.current = 5 + self.trait.current = 5 self.assertEqual(self._get_values(), (12, 1, 5, 0, 13)) - self.trait1.mod += 1 - self.trait1.base -= 4 + self.trait.mod += 1 + self.trait.base -= 4 self.assertEqual(self._get_values(), (8, 2, 5, 0, 10)) - self.trait1.min = -100 - self.trait1.base = -20 + self.trait.min = -100 + self.trait.base = -20 self.assertEqual(self._get_values(), (-20, 2, -18, -100, -18)) def test_boundaries__minmax(self): """Test range""" # current unset - tied to base + mod - self.trait1.base += 20 + self.trait.base += 20 self.assertEqual(self._get_values(), (28, 2, 30, 0, 30)) # set current - decouple from base + mod - self.trait1.current = 19 + self.trait.current = 19 self.assertEqual(self._get_values(), (28, 2, 19, 0, 30)) # test upper bound - self.trait1.current = 100 + self.trait.current = 100 self.assertEqual(self._get_values(), (28, 2, 30, 0, 30)) # min defaults to 0 - self.trait1.current = -10 + self.trait.current = -10 self.assertEqual(self._get_values(), (28, 2, 0, 0, 30)) - self.trait1.min = -20 + self.trait.min = -20 self.assertEqual(self._get_values(), (28, 2, 0, -20, 30)) - self.trait1.current = -10 + self.trait.current = -10 self.assertEqual(self._get_values(), (28, 2, -10, -20, 30)) def test_boundaries__bigmod(self): """add a big mod""" - self.trait1.base = 5 - self.trait1.mod = 100 + self.trait.base = 5 + self.trait.mod = 100 self.assertEqual(self._get_values(), (5, 100, 105, 0, 105)) # restricted by min - self.trait1.mod = -100 + self.trait.mod = -100 self.assertEqual(self._get_values(), (5, -5, 0, 0, 0)) - self.trait1.min = -200 + self.trait.min = -200 self.assertEqual(self._get_values(), (5, -5, 0, -200, 0)) def test_boundaries__change_boundaries(self): """Change boundaries after current change""" - self.trait1.current = 20 + self.trait.current = 20 self.assertEqual(self._get_values(), (8, 2, 10, 0, 10)) - self.trait1.mod = 102 + self.trait.mod = 102 self.assertEqual(self._get_values(), (8, 102, 10, 0, 110)) # raising min past current value will force it upwards - self.trait1.min = 20 + self.trait.min = 20 self.assertEqual(self._get_values(), (8, 102, 20, 20, 110)) def test_boundaries__disable(self): """Disable and re-enable boundary""" - self.trait1.base = 5 - self.trait1.min = 1 + self.trait.base = 5 + self.trait.min = 1 self.assertEqual(self._get_values(), (5, 2, 7, 1, 7)) - del self.trait1.min + del self.trait.min self.assertEqual(self._get_values(), (5, 2, 7, 0, 7)) - del self.trait1.base - del self.trait1.mod + del self.trait.base + del self.trait.mod self.assertEqual(self._get_values(), (0, 0, 0, 0, 0)) with self.assertRaises(traits.TraitException): - del self.trait1.max + del self.trait.max def test_boundaries__inverse(self): """Try to set reversed boundaries""" - self.trait1.mod = 0 - self.trait1.base = -10 # limited by min + self.trait.mod = 0 + self.trait.base = -10 # limited by min self.assertEqual(self._get_values(), (0, 0, 0, 0, 0)) - self.trait1.min = -10 + self.trait.min = -10 self.assertEqual(self._get_values(), (0, 0, 0, -10, 0)) - self.trait1.base = -10 + self.trait.base = -10 self.assertEqual(self._get_values(), (-10, 0, -10, -10, -10)) self.min = 0 # limited by base + mod self.assertEqual(self._get_values(), (-10, 0, -10, -10, -10)) def test_current(self): """Modifying current value""" - self.trait1.base = 10 - self.trait1.current = 5 + self.trait.base = 10 + self.trait.current = 5 self.assertEqual(self._get_values(), (10, 2, 5, 0, 12)) - self.trait1.current = 10 + self.trait.current = 10 self.assertEqual(self._get_values(), (10, 2, 10, 0, 12)) - self.trait1.current = 12 + self.trait.current = 12 self.assertEqual(self._get_values(), (10, 2, 12, 0, 12)) - self.trait1.current = 0 + self.trait.current = 0 self.assertEqual(self._get_values(), (10, 2, 0, 0, 12)) - self.trait1.current = -1 + self.trait.current = -1 self.assertEqual(self._get_values(), (10, 2, 0, 0, 12)) def test_delete(self): """Deleting resets to default.""" - del self.trait1.mod + del self.trait.mod self.assertEqual(self._get_values(), (8, 0, 8, 0, 8)) - self.trait1.mod = 2 - del self.trait1.base + self.trait.mod = 2 + del self.trait.base self.assertEqual(self._get_values(), (0, 2, 2, 0, 2)) - del self.trait1.min + del self.trait.min self.assertEqual(self._get_values(), (0, 2, 2, 0, 2)) - self.trait1.min = -10 + self.trait.min = -10 self.assertEqual(self._get_values(), (0, 2, 2, -10, 2)) - del self.trait1.min + del self.trait.min self.assertEqual(self._get_values(), (0, 2, 2, 0, 2)) def test_percentage(self): """Test percentage calculation""" - self.assertEqual(self.trait1.percent(), "100.0%") - self.trait1.current = 5 - self.assertEqual(self.trait1.percent(), "50.0%") - self.trait1.current = 3 - self.assertEqual(self.trait1.percent(), "33.3%") + self.assertEqual(self.trait.percent(), "100.0%") + self.trait.current = 5 + self.assertEqual(self.trait.percent(), "50.0%") + self.trait.current = 3 + self.assertEqual(self.trait.percent(), "30.0%") + self.trait.mod -= 1 + self.assertEqual(self.trait.percent(), "33.3%") class TestNumericTraitOperators(TestCase): diff --git a/evennia/contrib/traits.py b/evennia/contrib/traits.py index 9b29b5801d..1a071c83ec 100644 --- a/evennia/contrib/traits.py +++ b/evennia/contrib/traits.py @@ -865,7 +865,8 @@ class NumericTrait(Trait): class StaticTrait(NumericTrait): """ - Static Trait. This has a modification value. + Static Trait. This is a single value with a modifier, + with no concept of a 'current' value. actual = base + mod @@ -906,15 +907,15 @@ class CounterTrait(NumericTrait): 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. - min/unset base max/unset - |-----------------------|----------X-------------------| - actual - = current - + mod + min/unset base base+mod max/unset + |--------------|--------|---------X--------X------------| + current actual + = current + + mod - - actual = current + mod, starts at base + - actual = current + mod, starts at base + mod - if min or max is None, there is no upper/lower bound (default) - - if max is set to "base", max will be set as base changes. + - if max is set to "base", max will be equal ot base+mod """ @@ -929,21 +930,12 @@ class CounterTrait(NumericTrait): } # Helpers - - 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_boundaries(self, value): """Ensures that incoming value falls within trait's range.""" if self.min is not None and value <= self.min: return self.min if self._data["max"] == "base" and value >= self.mod + self.base: - return self.mod + self.base + return self.base + self.mod if self.max is not None and value >= self.max: return self.max return value @@ -955,11 +947,31 @@ class CounterTrait(NumericTrait): 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) + def base(self, value): + if value is None: + self._data["base"] = self.data_keys['base'] + if type(value) in (int, float): + if self.min is not None and value + self.mod < self.min: + value = self.min - self.mod + if self.max is not None and value + self.mod > self.max: + value = self.max - self.mod + self._data["base"] = value + + @property + def mod(self): + return self._data["mod"] + + @mod.setter + def mod(self, value): + if value is None: + # unsetting the boundary to default + self._data["mod"] = self.data_keys['mod'] + elif type(value) in (int, float): + if self.min is not None and value + self.base < self.min: + value = self.min - self.base + if self.max is not None and value + self.base > self.max: + value = self.max - self.base + self._data["mod"] = value @property def min(self): @@ -968,56 +980,46 @@ class CounterTrait(NumericTrait): @min.setter def min(self, value): if value is None: + # unsetting the boundary self._data["min"] = value elif type(value) in (int, float): if self.max is not None: value = min(self.max, value) - self._data["min"] = value if value < self.base else self.base + self._data["min"] = min(value, self.base + self.mod) @property def max(self): - if self._data["max"] == "base": - return self._mod_base() return self._data["max"] @max.setter def max(self, value): - """The maximum value of the trait. - - Note: - This property may be set to the string literal 'base'. - When set this way, the property returns the value of the - `mod`+`base` properties. - """ - if value == "base" or value is None: + if value is None: + # unsetting the boundary self._data["max"] = value elif type(value) in (int, float): if self.min is not None: value = max(self.min, value) - self._data["max"] = value if value > self.base else self.base + self._data["max"] = max(value, self.base + self.mod) @property def current(self): - """The `current` value of the `Trait`.""" - return self._enforce_bounds(self._data.get("current", self.base)) + """The `current` value of the `Trait`. This does not have .mod added.""" + return self._data.get("current", self.base) @current.setter def current(self, value): if type(value) in (int, float): - self._data["current"] = self._enforce_bounds(value) + self._data["current"] = self._enforce_boundaries(value) + + @current.deleter + def current(self): + """reset back to base""" + self._data["current"] = self.base @property def actual(self): - "The actual value of the Trait" - return self._mod_current() - - 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 + "The actual value of the Trait (current + mod)" + return self._enforce_boundaries(self.current + self.mod) def percent(self, formatting="{:3.1f}%"): """ @@ -1032,7 +1034,11 @@ class CounterTrait(NumericTrait): float or str: Depending of if a `formatting` string is supplied or not. """ - return percent(self.current, self.min, self.max, formatting=formatting) + return percent(self.actual, self.min, self.max, formatting=formatting) + + def reset(self): + """Resets `current` property equal to `base` value.""" + del self.current class GaugeTrait(CounterTrait): @@ -1041,7 +1047,7 @@ class GaugeTrait(CounterTrait): This emulates a gauge-meter that empties from a base+mod value. - min/0 max=base + mod + min/0 max=base+mod |-----------------------X---------------------------| actual = current @@ -1063,15 +1069,7 @@ class GaugeTrait(CounterTrait): "min": 0, } - 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.current) - - def _enforce_bounds(self, value): + def _enforce_boundaries(self, value): """Ensures that incoming value falls within trait's range.""" if self.min is not None and value <= self.min: return self.min @@ -1135,12 +1133,18 @@ class GaugeTrait(CounterTrait): @property def current(self): """The `current` value of the gauge.""" - return self._enforce_bounds(self._data.get("current", self._mod_base())) + return self._enforce_boundaries( + self._data.get("current", self.base + self.mod)) @current.setter def current(self, value): if type(value) in (int, float): - self._data["current"] = self._enforce_bounds(value) + self._data["current"] = self._enforce_boundaries(value) + + @current.deleter + def current(self): + "Resets current back to 'full'" + self._data["current"] = self.base + self.mod @property def actual(self): @@ -1162,10 +1166,8 @@ class GaugeTrait(CounterTrait): """ return percent(self.current, self.min, self.max, formatting=formatting) - - def fill_gauge(self): + def reset(self): """ Fills the gauge to its maximum allowed by base + mod - """ - self.current = self.base + self.mod + del self.current diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 810bc664e0..5642caad25 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -1687,15 +1687,17 @@ def format_table(table, extra_space=1): return ftable -def percent(self, value, minval, maxval, formatting="{:3.1f}%"): +def percent(value, minval, maxval, formatting="{:3.1f}%"): """ Get a value in an interval as a percentage of its position in that interval. This also understands negative numbers. Args: value (number): This should be a value minval<=value<=maxval. - minval (number): Smallest value in interval. - maxval (number): Biggest value in interval. + minval (number or None): Smallest value in interval. This could be None + for an open interval (then return will always be 100%) + maxval (number or None): Biggest value in interval. This could be None + for an open interval (then return will always be 100%) formatted (str, optional): This is a string that should accept one formatting tag. This will receive the current value as a percentage. If None, the @@ -1703,30 +1705,46 @@ def percent(self, value, minval, maxval, formatting="{:3.1f}%"): Returns: str or float: The formatted value or the raw percentage as a float. - Raises: - RuntimeError: If min/max does not make sense. Notes: - We handle the case of minval==maxval because we may see this case and - don't want to raise exceptions unnecessarily. In that case we return - 100%. + We try to handle a weird interval gracefully. + - If either maxval or minval is None (open interval), + we (aribtrarily) assume 100%. + - If minval > maxval, we return 0%. + - If minval == maxval == value we are looking at a single value match + and return 100%. + - If minval == maxval != value we return 0%. + - If value not in [minval..maxval], we set value to the closest + boundary, so the result will be 0% or 100%, respectively. """ - if minval > maxval: - raise RuntimeError("The minimum value must be <= the max value.") - # constrain value to interval - value = min(max(minval, value), maxval) - - # these should both be >0 - dpart = value - minval - dfull = maxval - minval - try: - result = (dpart / dfull) * 100.0 - except ZeroDivisionError: - # this means minval == maxval + result = None + if None in (minval, maxval): + # we have no boundaries, percent calculation makes no sense, + # we set this to 100% since it result = 100.0 - if not isinstance(formatting, str): - return result - return formatting.format(result) + elif minval > maxval: + # interval has no width so we cannot + # occupy any position within it. + result = 0.0 + elif minval == maxval == value: + # this is a single value that we match + result = 100.0 + elif minval == maxval != value: + # interval has no width so we cannot be in it. + result = 0.0 + + if result is None: + # constrain value to interval + value = min(max(minval, value), maxval) + + # these should both be >0 + dpart = value - minval + dfull = maxval - minval + result = (dpart / dfull) * 100.0 + + if isinstance(formatting, str): + return formatting.format(result) + return result import functools # noqa