Add replacable Trait classes

This commit is contained in:
Griatch 2020-04-14 00:32:37 +02:00
parent 485ab5907c
commit e0717fd07c
5 changed files with 183 additions and 115 deletions

View file

@ -237,20 +237,59 @@ Examples:
```
"""
from django.conf import settings
from functools import total_ordering
from evennia.utils.dbserialize import _SaverDict
from evennia.utils import logger
from evennia.utils.utils import inherits_from
from evennia.utils.utils import inherits_from, class_from_module
# This way the user can easily supply their own. Each
# class should have a class-property `trait_type` to
# identify the Trait class. The default ones are "static",
# "counter" and "gauge".
STATIC_TYPE = "static"
COUNTER_TYPE = "counter",
GAUGE_TYPE = "gauge"
_TRAIT_CLASS_PATHS = [
"evennia.contrib.traits.StaticTrait",
"evennia.contrib.traits.CounterTrait",
"evennia.contrib.traits.GaugeTrait",
]
if hasattr(settings, "TRAIT_CLASS_PATHS"):
_TRAIT_CLASS_PATHS += settings.TRAIT_CLASS_PATHS
# delay trait-class import to avoid circular import
_TRAIT_CLASSES = None
TRAIT_TYPES = (STATIC_TYPE, COUNTER_TYPE, GAUGE_TYPE)
RANGE_TRAITS = (COUNTER_TYPE, GAUGE_TYPE)
def _delayed_import_trait_classes():
"""
Import classes based on the given paths. Note that
imports from settings are last in the list, so if they
have the same trait_type set, they will replace the
default.
"""
global _TRAIT_CLASSES
if _TRAIT_CLASSES is None:
_TRAIT_CLASSES = {}
for classpath in _TRAIT_CLASS_PATHS:
try:
cls = class_from_module(classpath)
except ImportError:
logger.log_trace(f"Could not import Trait from {classpath}.")
else:
if hasattr(cls, "trait_type"):
trait_type = cls.trait_type
else:
trait_type = str(cls.__name___).lower()
_TRAIT_CLASSES[trait_type] = cls
_GA = object.__getattribute__
_SA = object.__setattr__
# this is the default we offer in TraitHandler.add
DEFAULT_TRAIT_TYPE = "static"
class TraitException(Exception):
@ -261,6 +300,7 @@ class TraitException(Exception):
msg (str): informative error message
"""
def __init__(self, msg):
self.msg = msg
@ -270,7 +310,8 @@ class TraitHandler:
Factory class that instantiates Trait objects.
"""
def __init__(self, obj, db_attribute_key='traits', db_attribute_category="traits"):
def __init__(self, obj, db_attribute_key="traits", db_attribute_category="traits"):
"""
Initialize the handler and set up its internal Attribute-based storage.
@ -279,6 +320,9 @@ class TraitHandler:
db_attribute_key (str): Name of the DB attribute for trait data storage
"""
# load the available classes, if necessary
_delayed_import_trait_classes()
# Note that this retains the connection to the database, meaning every
# update we do to .trait_data automatically syncs with database.
self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category)
@ -294,8 +338,8 @@ class TraitHandler:
def __setattr__(self, key, value):
"""Returns error message if trait objects are assigned directly."""
if key in ('trait_data', '_cache'):
super().__setattr__(key, value)
if key in ("trait_data", "_cache"):
_SA(self, key, value)
else:
raise TraitException(
"Trait object not settable directly. Assign to one of "
@ -306,13 +350,18 @@ class TraitHandler:
"""Returns error message if trait objects are assigned directly."""
return self.__setattr__(key, value)
def __getattr__(self, trait):
def __getattr__(self, key):
"""Returns Trait instances accessed as attributes."""
return self.get(trait)
return self.get(key)
def __getitem__(self, trait):
def __getitem__(self, key):
"""Returns `Trait` instances accessed as dict keys."""
return self.get(trait)
return self.get(key)
def __repr__(self):
return "TraitHandler ({num} Trait(s) stored): {keys}".format(
num=len(self), keys=", ".join(self.all)
)
@property
def all(self):
@ -325,7 +374,7 @@ class TraitHandler:
"""
return list(self.trait_data.keys())
def get(self, trait):
def get(self, key):
"""
Args:
trait (str): key from the traits dict containing config data
@ -336,14 +385,28 @@ class TraitHandler:
is not found in traits collection.
"""
trait = self._cache.get(trait)
if trait is None and trait in self.trait_data:
trait = self.cache[trait] = Trait(self.trait_data[trait])
trait = self._cache.get(key)
if trait is None and key in self.trait_data:
trait_type = self.trait_data[key]["trait_type"]
try:
trait_cls = _TRAIT_CLASSES[trait_type]
except KeyError:
raise TraitException("Trait class for {trait_type} could not be found.")
trait = self._cache[key] = trait_cls(self.trait_data[key])
return trait
def add(self, key, name=None, trait_type=STATIC_TYPE,
base=0, modifier=0, min_value=0, max_value=0,
force=False, **extra_properties):
def add(
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.
@ -369,13 +432,15 @@ class TraitHandler:
already exists (and `force` is unset).
"""
# from evennia import set_trace;set_trace()
if key in self.trait_data:
if force:
self.remove(key)
else:
raise TraitException(f"Trait '{key}' already exists.")
if trait_type not in TRAIT_TYPES:
if trait_type not in _TRAIT_CLASSES:
raise TraitException("Trait-type '{trait_type} is invalid.")
trait_kwargs = dict(
@ -385,7 +450,7 @@ class TraitHandler:
modifier=modifier,
min_value=min_value,
max_value=max_value,
extra_properties=extra_properties
extra_properties=extra_properties,
)
self.trait_data[key] = trait_kwargs
@ -402,7 +467,7 @@ class TraitHandler:
raise TraitException(f"Trait '{key}' not found.")
if key in self._cache:
del self.cache[key]
del self._cache[key]
del self.trait_data[key]
def clear(self):
@ -415,6 +480,7 @@ class TraitHandler:
# Parent Trait class
@total_ordering
class Trait:
"""Represents an object or Character trait.
@ -423,8 +489,16 @@ class Trait:
See module docstring for configuration details.
"""
_keys = set("name", "type", "base", "mod", "current",
"min", "max", "extra_properties")
_keys = (
"name",
"trait_type",
"base",
"modifier",
"current",
"min_value",
"max_value",
"extra_properties",
)
def __init__(self, trait_data):
"""
@ -436,20 +510,16 @@ class Trait:
save itself the database when updating
"""
if not all(key in trait_data for key in self.valid_keys):
raise TraitException(
f"Required keys missing from trait_data "
f"(input was {list(trait_data.keys())}, "
f"required are {self.valid_keys}).")
self._type = trait_data['trait_type']
self._type = trait_data["trait_type"]
self._data = trait_data
self._locked = True
if not isinstance(trait_data, _SaverDict):
logger.log_warn(
f"Non-persistent Trait data (type(trait_data)) "
f"loaded for {type(self).__name__}.")
f"loaded for {type(self).__name__}."
)
# Private helper members
@ -458,7 +528,7 @@ class Trait:
if self._type in RANGE_TRAITS:
if self.min is not None and value <= self.min:
return self.min
if self._data['max'] == 'base' and value >= self.mod + self.base:
if self._data["max"] == "base" and value >= self.mod + self.base:
return self.mod + self.base
if self.max is not None and value >= self.max:
return self.max
@ -474,15 +544,14 @@ class Trait:
"""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]))
", ".join(
["'{}': {!r}".format(k, self._data[k]) for k in self._keys if k in self._data]
),
)
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)
return "{name:12} {status} ({mod:+3})".format(name=self.name, status=status, mod=self.mod)
# Extra Properties - allow access to properties on Trait
@ -504,12 +573,11 @@ class Trait:
def __getattr__(self, key):
"""Access extra parameters as attributes."""
try:
return self._data['extra_properties'][key]
return self._data["extra_properties"][key]
except KeyError:
raise AttributeError(
"{} '{}' has no attribute {!r}".format(
type(self).__name__, self.name, key
))
"{} '{}' has no attribute {!r}".format(type(self).__name__, self.name, key)
)
def __setattr__(self, key, value):
"""Set extra parameters as attributes.
@ -526,16 +594,15 @@ class Trait:
raise AttributeError(f"Can't set attribute {key}.")
propobj.fset(self, value)
else:
if (self.__dict__.get('_locked', False) and
key not in ('_keys',)):
self._data['extra_properties'][key] = value
if self.__dict__.get("_locked", False) and key not in ("_keys",):
_GA(self, "_data")["extra_properties"][key] = value
else:
super().__setattr__(key, value)
_SA(self, key, value)
def __delattr__(self, key):
"""Delete extra parameters as attributes."""
if key in self._data['extra_parameters']:
del self._data['extra_parameters'][key]
if key in self._data["extra_properties"]:
del self._data["extra_properties"][key]
# Numeric operations
@ -630,7 +697,8 @@ class Trait:
@property
def name(self):
"""Display name for the trait."""
return self._data['name']
return self._data["name"]
key = name
@property
@ -646,24 +714,24 @@ class Trait:
The setter for this property will enforce any range bounds set
on this `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 self._data.get("max", None) == "base":
self._data["base"] = amount
if type(amount) in (int, float):
self._data['base'] = self._enforce_bounds(amount)
self._data["base"] = self._enforce_bounds(amount)
@property
def mod(self):
"""The trait's modifier."""
return self._data['modifier']
return self._data["modifier"]
@mod.setter
def mod(self, amount):
if type(amount) in (int, float):
self._data['modifier'] = amount
self._data["modifier"] = amount
@property
def min(self):
@ -675,7 +743,7 @@ class Trait:
@property
def max(self):
return self._data['max_value']
return self._data["max_value"]
@max.setter
def max(self, value):
@ -684,7 +752,7 @@ class Trait:
@property
def current(self):
"""The `current` value of the `Trait`."""
return self._data.get('current', self.base)
return self._data.get("current", self.base)
@current.setter
def current(self, value):
@ -693,7 +761,7 @@ class Trait:
@property
def extra(self):
"""Returns a list containing available extra data keys."""
return self._data['extra'].keys()
return self._data["extra"].keys()
def reset_mod(self):
"""Clears any mod value to 0."""
@ -710,11 +778,15 @@ class Trait:
# Implementation of the respective Trait types
class StaticTrait(Trait):
"""
Static Trait.
"""
trait_type = "static"
@property
def min(self):
raise TraitException(f"Static Trait {self.key} has no minimum value.")
@ -733,17 +805,16 @@ class StaticTrait(Trait):
@property
def current(self):
"""The `current` value of the `Trait`."""
return super().current
"""The `current` value of the `Trait`. This is the same as base for a Static Trait."""
return self.base
@current.setter
def current(self, value):
raise TraitException(
f"Cannot set 'current' property on static Trait {self.key}.")
"""Current == base for Static Traits."""
self.base = self.current = value
def reset(self):
raise TraitException(
f"Cannot reset static Trait {self.key}.")
raise TraitException(f"Cannot reset static Trait {self.key}.")
class CounterTrait(Trait):
@ -751,6 +822,9 @@ class CounterTrait(Trait):
Counter Trait.
"""
trait_type = "counter"
@property
def actual(self):
"The actual value of the Trait"
@ -763,14 +837,14 @@ class CounterTrait(Trait):
@min.setter
def min(self, amount):
if amount is None:
self._data['min'] = amount
if amount is None:
self._data["min"] = amount
elif type(amount) in (int, float):
self._data['min'] = amount if amount < self.base else self.base
self._data["min"] = amount if amount < self.base else self.base
@property
def max(self):
if self._data['max_value'] == 'base':
if self._data["max_value"] == "base":
return self._mod_base()
return super().max
@ -783,16 +857,16 @@ class CounterTrait(Trait):
When set this way, the property returns the value of the
`mod`+`base` properties.
"""
if self._data['max_value'] == 'base':
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:
self._data['max_value'] = value
if value == "base" or value is None:
self._data["max_value"] = value
elif type(value) in (int, float):
self._data['max_value'] = value if value > self.base else self.base
self._data["max_value"] = value if value > self.base else self.base
@property
def current(self):
@ -802,10 +876,9 @@ class CounterTrait(Trait):
@current.setter
def current(self, value):
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'.")
raise AttributeError("'current' property is read-only on static 'Trait'.")
def percent(self):
"""Returns the value formatted as a percentage."""
@ -822,14 +895,12 @@ class GaugeTrait(CounterTrait):
Gauge Trait.
"""
trait_type = "gauge"
def __str__(self):
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)
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)
@property
def actual(self):
@ -844,8 +915,8 @@ class GaugeTrait(CounterTrait):
@mod.setter
def mod(self, amount):
if type(amount) in (int, float):
self._data['modifier'] = amount
delta = amount - self._data['modifier']
self._data["modifier"] = amount
delta = amount - self._data["modifier"]
if delta >= 0:
# apply increases to current
self.current = self._enforce_bounds(self.current + delta)
@ -856,7 +927,7 @@ class GaugeTrait(CounterTrait):
@property
def current(self):
"""The `current` value of the `Trait`."""
return self._data.get('current', self._mod_base())
return self._data.get("current", self._mod_base())
@current.setter
def current(self, value):
@ -869,5 +940,4 @@ class GaugeTrait(CounterTrait):
Will honor the upper bound if set.
"""
self.current = \
self._enforce_bounds(self.current + self._mod_base())
self.current = self._enforce_bounds(self.current + self._mod_base())

View file

@ -261,6 +261,7 @@ if TELNET_ENABLED:
# Start telnet game connections
from evennia.server.portal import telnet
_telnet_protocol = class_from_module(settings.TELNET_PROTOCOL_CLASS)
for interface in TELNET_INTERFACES:
@ -285,6 +286,7 @@ if SSL_ENABLED:
# Start Telnet+SSL game connection (requires PyOpenSSL).
from evennia.server.portal import telnet_ssl
_ssl_protocol = class_from_module(settings.SSL_PROTOCOL_CLASS)
for interface in SSL_INTERFACES:
@ -319,6 +321,7 @@ if SSH_ENABLED:
# evennia/game if necessary.
from evennia.server.portal import ssh
_ssh_protocol = class_from_module(settings.SSH_PROTOCOL_CLASS)
for interface in SSH_INTERFACES:
@ -328,11 +331,7 @@ if SSH_ENABLED:
for port in SSH_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = ssh.makeFactory(
{
"protocolFactory": _ssh_protocol,
"protocolArgs": (),
"sessions": PORTAL_SESSIONS,
}
{"protocolFactory": _ssh_protocol, "protocolArgs": (), "sessions": PORTAL_SESSIONS,}
)
factory.noisy = False
ssh_service = internet.TCPServer(port, factory, interface=interface)

View file

@ -58,7 +58,6 @@ _HTTP_WARNING = bytes(
_BASE_SESSION_CLASS = class_from_module(settings.BASE_SESSION_CLASS)
class TelnetServerFactory(protocol.ServerFactory):
"This is only to name this better in logs"
noisy = False

View file

@ -23,7 +23,7 @@ from evennia.utils.utils import (
make_iter,
delay,
callables_from_module,
class_from_module
class_from_module,
)
from evennia.server.portal import amp
from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT

View file

@ -972,7 +972,7 @@ REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
"PAGE_SIZE": 25,
# require logged in users to call API so that access checks can work on them
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated", ],
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated",],
# These are the different ways people can authenticate for API requests - via
# session or with user/password. Other ways are possible, such as via tokens
# or oauth, but require additional dependencies.
@ -1040,28 +1040,28 @@ PORTAL_SESSION_HANDLER_CLASS = "evennia.server.portal.portalsessionhandler.Porta
# so the additions have somewhere to go. These must be simple things that
# can be pickled - stuff you could serialize to JSON is best.
SESSION_SYNC_ATTRS = (
"protocol_key",
"address",
"suid",
"sessid",
"uid",
"csessid",
"uname",
"logged_in",
"puid",
"conn_time",
"cmd_last",
"cmd_last_visible",
"cmd_total",
"protocol_flags",
"server_data",
"cmdset_storage_string"
)
"protocol_key",
"address",
"suid",
"sessid",
"uid",
"csessid",
"uname",
"logged_in",
"puid",
"conn_time",
"cmd_last",
"cmd_last_visible",
"cmd_total",
"protocol_flags",
"server_data",
"cmdset_storage_string",
)
# The following are used for the communications between the Portal and Server.
# Very dragons territory.
AMP_SERVER_PROTOCOL_CLASS = 'evennia.server.portal.amp_server.AMPServerProtocol'
AMP_CLIENT_PROTOCOL_CLASS = 'evennia.server.amp_client.AMPServerClientProtocol'
AMP_SERVER_PROTOCOL_CLASS = "evennia.server.portal.amp_server.AMPServerProtocol"
AMP_CLIENT_PROTOCOL_CLASS = "evennia.server.amp_client.AMPServerClientProtocol"
######################################################################