mirror of
https://github.com/evennia/evennia.git
synced 2026-03-30 04:27:16 +02:00
Components Squash (#1)
Adds the first part of Components Contrib and related docs
This commit is contained in:
parent
fdc0ad2c88
commit
8b5d978094
6 changed files with 749 additions and 0 deletions
170
evennia/contrib/base_systems/components/README.md
Normal file
170
evennia/contrib/base_systems/components/README.md
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# Components
|
||||
|
||||
_Contrib by ChrisLR 2021_
|
||||
|
||||
# The Components Contrib
|
||||
|
||||
This contrib introduces Components and Composition to Evennia.
|
||||
Each 'Component' class represents a feature that will be 'enabled' on a typeclass instance.
|
||||
You can register these components on an entire typeclass or a single object at runtime.
|
||||
It supports both persisted attributes and in-memory attributes by using Evennia's AttributeHandler.
|
||||
|
||||
# Pros
|
||||
- You can reuse a feature across multiple typeclasses without inheritance
|
||||
- You can cleanly organize each feature into a self-contained class.
|
||||
- You can check if your object supports a feature without checking its instance.
|
||||
|
||||
# Cons
|
||||
- It introduces additional complexity.
|
||||
- A host typeclass instance is required.
|
||||
|
||||
# How to install
|
||||
|
||||
To enable component support for a typeclass,
|
||||
import and inherit the ComponentHolderMixin, similar to this
|
||||
```python
|
||||
from evennia.contrib.base_systems.components import ComponentHolderMixin
|
||||
class Character(ComponentHolderMixin, DefaultCharacter):
|
||||
# ...
|
||||
```
|
||||
|
||||
Components need to inherit the Component class directly and require a name.
|
||||
```python
|
||||
from evennia.contrib.components import Component
|
||||
|
||||
class Health(Component):
|
||||
name = "health"
|
||||
```
|
||||
|
||||
Components may define DBFields or NDBFields at the class level.
|
||||
DBField will store its values in the host's DB with a prefixed key.
|
||||
NDBField will store its values in the host's NDB and will not persist.
|
||||
The key used will be 'component_name__field_name'.
|
||||
They use AttributeProperty under the hood.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from evennia.contrib.base_systems.components import Component, DBField
|
||||
|
||||
class Health(Component):
|
||||
health = DBField(default=1)
|
||||
```
|
||||
|
||||
Note that default is optional and will default to None
|
||||
|
||||
|
||||
Each typeclass using the ComponentHolderMixin can declare its components
|
||||
in the class via the ComponentProperty.
|
||||
These are components that will always be present in a typeclass.
|
||||
You can also pass kwargs to override the default values
|
||||
Example
|
||||
```python
|
||||
from evennia.contrib.base_systems.components import ComponentHolderMixin
|
||||
class Character(ComponentHolderMixin, DefaultCharacter):
|
||||
health = ComponentProperty("health", hp=10, max_hp=50)
|
||||
```
|
||||
|
||||
You can then use character.components.health to access it.
|
||||
The shorter form character.cmp.health also exists.
|
||||
character.health would also be accessible but only for typeclasses that have
|
||||
this component defined on the class.
|
||||
|
||||
Alternatively you can add those components at runtime.
|
||||
You will have to access those via the component handler.
|
||||
Example
|
||||
```python
|
||||
character = self
|
||||
vampirism = components.Vampirism.create(character)
|
||||
character.components.add(vampirism)
|
||||
|
||||
...
|
||||
|
||||
vampirism_from_elsewhere = character.components.get("vampirism")
|
||||
```
|
||||
|
||||
Keep in mind that all components must be imported to be visible in the listing.
|
||||
As such, I recommend regrouping them in a package.
|
||||
You can then import all your components in that package's __init__
|
||||
|
||||
Because of how Evennia import typeclasses and the behavior of python imports
|
||||
I recommend placing the components package inside the typeclass package.
|
||||
In other words, create a folder named components inside your typeclass folder.
|
||||
Then, inside the 'typeclasses/__init__.py' file add the import to the folder, like
|
||||
```python
|
||||
from typeclasses import components
|
||||
```
|
||||
This ensures that the components package will be imported when the typeclasses are imported.
|
||||
You will also need to import each components inside the package's own 'typeclasses/components/__init__.py' file.
|
||||
You only need to import each module/file from there but importing the right class is a good practice.
|
||||
```python
|
||||
from typeclasses.components.health import Health
|
||||
```
|
||||
```python
|
||||
from typeclasses.components import health
|
||||
```
|
||||
Both of the above examples will work.
|
||||
|
||||
# Full Example
|
||||
```python
|
||||
from evennia.contrib.base_systems import components
|
||||
|
||||
|
||||
# This is the Component class
|
||||
class Health(components.Component):
|
||||
name = "health"
|
||||
|
||||
# Stores the current and max values as Attributes on the host, defaulting to 100
|
||||
current = components.DBField(default=100)
|
||||
max = components.DBField(default=100)
|
||||
|
||||
def damage(self, value):
|
||||
if self.current <= 0:
|
||||
return
|
||||
|
||||
self.current -= value
|
||||
if self.current > 0:
|
||||
return
|
||||
|
||||
self.current = 0
|
||||
self.on_death()
|
||||
|
||||
def heal(self, value):
|
||||
hp = self.current
|
||||
hp += value
|
||||
if hp >= self.max_hp:
|
||||
hp = self.max_hp
|
||||
|
||||
self.current = hp
|
||||
|
||||
@property
|
||||
def is_dead(self):
|
||||
return self.current <= 0
|
||||
|
||||
def on_death(self):
|
||||
# Behavior is defined on the typeclass
|
||||
self.host.on_death()
|
||||
|
||||
|
||||
# This is how the Character inherits the mixin and registers the component 'health'
|
||||
class Character(ComponentHolderMixin, DefaultCharacter):
|
||||
health = ComponentProperty("health")
|
||||
|
||||
|
||||
# This is an example of a command that checks for the component
|
||||
class Attack(Command):
|
||||
key = "attack"
|
||||
aliases = ('melee', 'hit')
|
||||
|
||||
def at_pre_cmd(self):
|
||||
caller = self.caller
|
||||
targets = self.caller.search(args, quiet=True)
|
||||
valid_target = None
|
||||
for target in targets:
|
||||
# Attempt to retrieve the component, None is obtained if it does not exist.
|
||||
if target.components.health:
|
||||
valid_target = target
|
||||
|
||||
if not valid_target:
|
||||
caller.msg("You can't attack that!")
|
||||
return True
|
||||
```
|
||||
24
evennia/contrib/base_systems/components/__init__.py
Normal file
24
evennia/contrib/base_systems/components/__init__.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
"""
|
||||
Components - ChrisLR 2022
|
||||
|
||||
This is a basic Component System.
|
||||
It allows you to use components on typeclasses using a simple syntax.
|
||||
This helps writing isolated code and reusing it over multiple objects.
|
||||
|
||||
See the docs for more information.
|
||||
"""
|
||||
|
||||
from evennia.contrib.base_systems.components.component import Component
|
||||
from evennia.contrib.base_systems.components.dbfield import DBField, NDBField
|
||||
from evennia.contrib.base_systems.components.holder import ComponentHolderMixin, ComponentProperty
|
||||
|
||||
|
||||
def get_component_class(component_name):
|
||||
subclasses = Component.__subclasses__()
|
||||
component_class = next((sc for sc in subclasses if sc.name == component_name), None)
|
||||
if component_class is None:
|
||||
message = f"Component named {component_name} has not been found. " \
|
||||
f"Make sure it has been imported before being used."
|
||||
raise Exception(message)
|
||||
|
||||
return component_class
|
||||
149
evennia/contrib/base_systems/components/component.py
Normal file
149
evennia/contrib/base_systems/components/component.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"""
|
||||
Components - ChrisLR 2022
|
||||
|
||||
This file contains the base class to inherit for creating new components.
|
||||
"""
|
||||
import itertools
|
||||
|
||||
|
||||
class Component:
|
||||
"""
|
||||
This is the base class for components.
|
||||
Any component must inherit from this class to be considered for usage.
|
||||
|
||||
Each Component must supply the name, it is used as a slot name but also part of the attribute key.
|
||||
"""
|
||||
name = ""
|
||||
|
||||
def __init__(self, host=None):
|
||||
assert self.name, "All Components must have a Name"
|
||||
self.host = host
|
||||
|
||||
@classmethod
|
||||
def default_create(cls, host):
|
||||
"""
|
||||
This is called when the host is created
|
||||
and should return the base initialized state of a component.
|
||||
|
||||
Args:
|
||||
host (object): The host typeclass instance
|
||||
|
||||
Returns:
|
||||
Component: The created instance of the component
|
||||
|
||||
"""
|
||||
new = cls(host)
|
||||
return new
|
||||
|
||||
@classmethod
|
||||
def create(cls, host, **kwargs):
|
||||
"""
|
||||
This is the method to call when supplying kwargs to initialize a component.
|
||||
|
||||
Args:
|
||||
host (object): The host typeclass instance
|
||||
**kwargs: Key-Value of default values to replace.
|
||||
To persist the value, the key must correspond to a DBField.
|
||||
|
||||
Returns:
|
||||
Component: The created instance of the component
|
||||
|
||||
"""
|
||||
|
||||
new = cls.default_create(host)
|
||||
for key, value in kwargs.items():
|
||||
setattr(new, key, value)
|
||||
|
||||
return new
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
This deletes all component attributes from the host's db
|
||||
"""
|
||||
for attribute in self._all_db_field_names:
|
||||
delattr(self, attribute)
|
||||
|
||||
@classmethod
|
||||
def load(cls, host):
|
||||
"""
|
||||
Loads a component instance
|
||||
This is called whenever a component is loaded (ex: Server Restart)
|
||||
|
||||
Args:
|
||||
host (object): The host typeclass instance
|
||||
|
||||
Returns:
|
||||
Component: The loaded instance of the component
|
||||
|
||||
"""
|
||||
|
||||
return cls(host)
|
||||
|
||||
def at_added(self, host):
|
||||
"""
|
||||
This is the method called when a component is registered on a host.
|
||||
|
||||
Args:
|
||||
host (object): The host typeclass instance
|
||||
|
||||
"""
|
||||
|
||||
if self.host:
|
||||
if self.host == host:
|
||||
return
|
||||
else:
|
||||
raise ComponentRegisterError("Components must not register twice!")
|
||||
|
||||
self.host = host
|
||||
|
||||
def at_removed(self, host):
|
||||
"""
|
||||
This is the method called when a component is removed from a host.
|
||||
|
||||
Args:
|
||||
host (object): The host typeclass instance
|
||||
|
||||
"""
|
||||
if host != self.host:
|
||||
raise ComponentRegisterError("Component attempted to remove from the wrong host.")
|
||||
self.host = None
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
"""
|
||||
Shortcut property returning the host's AttributeHandler.
|
||||
|
||||
Returns:
|
||||
AttributeHandler: The Host's AttributeHandler
|
||||
|
||||
"""
|
||||
return self.host.attributes
|
||||
|
||||
@property
|
||||
def nattributes(self):
|
||||
"""
|
||||
Shortcut property returning the host's In-Memory AttributeHandler (Non Persisted).
|
||||
|
||||
Returns:
|
||||
AttributeHandler: The Host's In-Memory AttributeHandler
|
||||
|
||||
"""
|
||||
return self.host.nattributes
|
||||
|
||||
@property
|
||||
def _all_db_field_names(self):
|
||||
return itertools.chain(self.db_field_names, self.ndb_field_names)
|
||||
|
||||
@property
|
||||
def db_field_names(self):
|
||||
db_fields = getattr(self, "_db_fields", {})
|
||||
return db_fields.keys()
|
||||
|
||||
@property
|
||||
def ndb_field_names(self):
|
||||
ndb_fields = getattr(self, "_ndb_fields", {})
|
||||
return ndb_fields.keys()
|
||||
|
||||
|
||||
class ComponentRegisterError(Exception):
|
||||
pass
|
||||
54
evennia/contrib/base_systems/components/dbfield.py
Normal file
54
evennia/contrib/base_systems/components/dbfield.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"""
|
||||
Components - ChrisLR 2022
|
||||
|
||||
This file contains the Descriptors used to set Fields in Components
|
||||
"""
|
||||
from evennia.typeclasses.attributes import AttributeProperty, NAttributeProperty
|
||||
|
||||
|
||||
class DBField(AttributeProperty):
|
||||
"""
|
||||
Component Attribute Descriptor.
|
||||
Allows you to set attributes related to a component on the class.
|
||||
It uses AttributeProperty under the hood but prefixes the key with the component name.
|
||||
"""
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
"""
|
||||
Called when descriptor is first assigned to the class.
|
||||
|
||||
Args:
|
||||
owner (object): The component classF on which this is set
|
||||
name (str): The name that was used to set the DBField.
|
||||
"""
|
||||
key = f"{owner.name}__{name}"
|
||||
self._key = key
|
||||
db_fields = getattr(owner, "_db_fields", None)
|
||||
if db_fields is None:
|
||||
db_fields = {}
|
||||
setattr(owner, '_db_fields', db_fields)
|
||||
db_fields[name] = self
|
||||
|
||||
|
||||
class NDBField(NAttributeProperty):
|
||||
"""
|
||||
Component In-Memory Attribute Descriptor.
|
||||
Allows you to set in-memory attributes related to a component on the class.
|
||||
It uses NAttributeProperty under the hood but prefixes the key with the component name.
|
||||
"""
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
"""
|
||||
Called when descriptor is first assigned to the class.
|
||||
|
||||
Args:
|
||||
owner (object): The component class on which this is set
|
||||
name (str): The name that was used to set the DBField.
|
||||
"""
|
||||
key = f"{owner.name}__{name}"
|
||||
self._key = key
|
||||
ndb_fields = getattr(owner, "_ndb_fields", None)
|
||||
if ndb_fields is None:
|
||||
ndb_fields = {}
|
||||
setattr(owner, '_ndb_fields', ndb_fields)
|
||||
ndb_fields[name] = self
|
||||
243
evennia/contrib/base_systems/components/holder.py
Normal file
243
evennia/contrib/base_systems/components/holder.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
"""
|
||||
Components - ChrisLR 2022
|
||||
|
||||
This file contains the classes that allow a typeclass to use components.
|
||||
"""
|
||||
|
||||
from evennia.contrib.base_systems import components
|
||||
|
||||
|
||||
class ComponentProperty:
|
||||
"""
|
||||
This allows you to register a component on a typeclass.
|
||||
Components registered with this property are automatically added
|
||||
to any instance of this typeclass.
|
||||
|
||||
Defaults can be overridden for this typeclass by passing kwargs
|
||||
"""
|
||||
def __init__(self, component_name, **kwargs):
|
||||
"""
|
||||
Initializes the descriptor
|
||||
|
||||
Args:
|
||||
component_name (str): The name of the component
|
||||
**kwargs (any): Key=Values overriding default values of the component
|
||||
"""
|
||||
self.component_name = component_name
|
||||
self.values = kwargs
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
component = instance.components.get(self.component_name)
|
||||
return component
|
||||
|
||||
def __set__(self, instance, value):
|
||||
raise Exception("Cannot set a class property")
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
class_components = getattr(owner, "_class_components", None)
|
||||
if not class_components:
|
||||
class_components = []
|
||||
setattr(owner, "_class_components", class_components)
|
||||
|
||||
class_components.append((self.component_name, self.values))
|
||||
|
||||
|
||||
class ComponentHandler:
|
||||
"""
|
||||
This is the handler that will be added to any typeclass that inherits from ComponentHolder.
|
||||
It lets you add or remove components and will load components as needed.
|
||||
It stores the list of registered components on the host .db with component_names as key.
|
||||
"""
|
||||
def __init__(self, host):
|
||||
self.host = host
|
||||
self._loaded_components = {}
|
||||
|
||||
def add(self, component):
|
||||
"""
|
||||
Method to add a Component to a host.
|
||||
It caches the loaded component and appends its name to the host's component name list.
|
||||
It will also call the component's 'at_added' method, passing its host.
|
||||
|
||||
Args:
|
||||
component (object): The 'loaded' component instance to add.
|
||||
|
||||
"""
|
||||
self._set_component(component)
|
||||
self.db_names.append(component.name)
|
||||
component.at_added(self.host)
|
||||
|
||||
def add_default(self, name):
|
||||
"""
|
||||
Method to add a Component initialized to default values on a host.
|
||||
It will retrieve the proper component and instanciate it with 'default_create'.
|
||||
It will cache this new component and add it to its list.
|
||||
It will also call the component's 'at_added' method, passing its host.
|
||||
|
||||
Args:
|
||||
name (str): The name of the component class to add.
|
||||
|
||||
"""
|
||||
component = components.get_component_class(name)
|
||||
if not component:
|
||||
raise ComponentDoesNotExist(f"Component {name} does not exist.")
|
||||
|
||||
new_component = component.default_create(self.host)
|
||||
self._set_component(new_component)
|
||||
self.db_names.append(name)
|
||||
new_component.at_added(self.host)
|
||||
|
||||
def remove(self, component):
|
||||
"""
|
||||
Method to remove a component instance from a host.
|
||||
It removes the component from the cache and listing.
|
||||
It will call the component's 'at_removed' method.
|
||||
|
||||
Args:
|
||||
component (object): The component instance to remove.
|
||||
|
||||
"""
|
||||
component_name = component.name
|
||||
if component_name in self._loaded_components:
|
||||
component.at_removed(self.host)
|
||||
self.db_names.remove(component_name)
|
||||
del self._loaded_components[component_name]
|
||||
else:
|
||||
message = f"Cannot remove {component_name} from {self.host.name} as it is not registered."
|
||||
raise ComponentIsNotRegistered(message)
|
||||
|
||||
def remove_by_name(self, name):
|
||||
"""
|
||||
Method to remove a component instance from a host.
|
||||
It removes the component from the cache and listing.
|
||||
It will call the component's 'at_removed' method.
|
||||
|
||||
Args:
|
||||
name (str): The name of the component to remove.
|
||||
|
||||
"""
|
||||
instance = self.get(name)
|
||||
if not instance:
|
||||
message = f"Cannot remove {name} from {self.host.name} as it is not registered."
|
||||
raise ComponentIsNotRegistered(message)
|
||||
|
||||
instance.at_removed(self.host)
|
||||
self.db_names.remove(name)
|
||||
del self._loaded_components[name]
|
||||
|
||||
def get(self, name):
|
||||
"""
|
||||
Method to retrieve a cached Component instance by its name.
|
||||
|
||||
Args:
|
||||
name (str): The name of the component to retrieve.
|
||||
|
||||
"""
|
||||
return self._loaded_components.get(name)
|
||||
|
||||
def has(self, name):
|
||||
"""
|
||||
Method to check if a component is registered and ready.
|
||||
|
||||
Args:
|
||||
name (str): The name of the component.
|
||||
|
||||
"""
|
||||
return name in self._loaded_components
|
||||
|
||||
def initialize(self):
|
||||
"""
|
||||
Method that loads and caches each component currently registered on the host.
|
||||
It retrieves the names from the registered listing and calls 'load' on each
|
||||
prototype class that can be found from this listing.
|
||||
|
||||
"""
|
||||
component_names = self.db_names
|
||||
if not component_names:
|
||||
return
|
||||
|
||||
for component_name in component_names:
|
||||
component = components.get_component_class(component_name)
|
||||
if component:
|
||||
component_instance = component.load(self.host)
|
||||
self._set_component(component_instance)
|
||||
else:
|
||||
message = f"Could not initialize runtime component {component_name} of {self.host.name}"
|
||||
raise ComponentDoesNotExist(message)
|
||||
|
||||
def _set_component(self, component):
|
||||
self._loaded_components[component.name] = component
|
||||
|
||||
@property
|
||||
def db_names(self):
|
||||
"""
|
||||
Property shortcut to retrieve the registered component names
|
||||
|
||||
Returns:
|
||||
component_names (iterable): The name of each component that is registered
|
||||
|
||||
"""
|
||||
return self.host.attributes.get("component_names")
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self.get(name)
|
||||
|
||||
|
||||
class ComponentHolderMixin(object):
|
||||
"""
|
||||
Mixin to add component support to a typeclass
|
||||
|
||||
Components are set on objects using the component.name as an object attribute.
|
||||
All registered components are initialized on the typeclass.
|
||||
They will be of None value if not present in the class components or runtime components.
|
||||
"""
|
||||
|
||||
def at_init(self):
|
||||
"""
|
||||
Method that initializes the ComponentHandler.
|
||||
"""
|
||||
super(ComponentHolderMixin, self).at_init()
|
||||
setattr(self, "_component_handler", ComponentHandler(self))
|
||||
self.components.initialize()
|
||||
|
||||
def basetype_setup(self):
|
||||
"""
|
||||
Method that initializes the ComponentHandler, creates and registers all
|
||||
components that were set on the typeclass using ComponentProperty.
|
||||
"""
|
||||
super().basetype_setup()
|
||||
component_names = []
|
||||
setattr(self, "_component_handler", ComponentHandler(self))
|
||||
class_components = getattr(self, "_class_components", ())
|
||||
for component_name, values in class_components:
|
||||
component_class = components.get_component_class(component_name)
|
||||
component = component_class.create(self, **values)
|
||||
component_names.append(component_name)
|
||||
self.components._loaded_components[component_name] = component
|
||||
|
||||
self.db.component_names = component_names
|
||||
|
||||
@property
|
||||
def components(self) -> ComponentHandler:
|
||||
"""
|
||||
Property getter to retrieve the component_handler.
|
||||
Returns:
|
||||
ComponentHandler: This Host's ComponentHandler
|
||||
"""
|
||||
return getattr(self, "_component_handler", None)
|
||||
|
||||
@property
|
||||
def cmp(self) -> ComponentHandler:
|
||||
"""
|
||||
Shortcut Property getter to retrieve the component_handler.
|
||||
Returns:
|
||||
ComponentHandler: This Host's ComponentHandler
|
||||
"""
|
||||
return self.components
|
||||
|
||||
|
||||
class ComponentDoesNotExist(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ComponentIsNotRegistered(Exception):
|
||||
pass
|
||||
109
evennia/contrib/base_systems/components/tests.py
Normal file
109
evennia/contrib/base_systems/components/tests.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
from evennia.contrib.base_systems.components import Component, DBField
|
||||
from evennia.contrib.base_systems.components.holder import ComponentProperty, ComponentHolderMixin
|
||||
from evennia.objects.objects import DefaultCharacter
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
|
||||
|
||||
class ComponentTestA(Component):
|
||||
name = "test_a"
|
||||
my_int = DBField(default=1)
|
||||
my_list = DBField(default=[])
|
||||
|
||||
|
||||
class ComponentTestB(Component):
|
||||
name = "test_b"
|
||||
my_int = DBField(default=1)
|
||||
my_list = DBField(default=[])
|
||||
|
||||
|
||||
class RuntimeComponentTestC(Component):
|
||||
name = "test_c"
|
||||
my_int = DBField(default=6)
|
||||
my_dict = DBField(default={})
|
||||
|
||||
|
||||
class CharacterWithComponents(ComponentHolderMixin, DefaultCharacter):
|
||||
test_a = ComponentProperty("test_a")
|
||||
test_b = ComponentProperty("test_b", my_int=3, my_list=[1, 2, 3])
|
||||
|
||||
|
||||
class TestComponents(EvenniaTest):
|
||||
character_typeclass = CharacterWithComponents
|
||||
|
||||
def test_character_has_class_components(self):
|
||||
assert self.char1.test_a
|
||||
assert self.char1.test_b
|
||||
|
||||
def test_character_instances_components_properly(self):
|
||||
assert isinstance(self.char1.test_a, ComponentTestA)
|
||||
assert isinstance(self.char1.test_b, ComponentTestB)
|
||||
|
||||
def test_character_assigns_default_value(self):
|
||||
assert self.char1.test_a.my_int == 1
|
||||
assert self.char1.test_a.my_list == []
|
||||
|
||||
def test_character_assigns_default_provided_values(self):
|
||||
assert self.char1.test_b.my_int == 3
|
||||
assert self.char1.test_b.my_list == [1, 2, 3]
|
||||
|
||||
def test_character_can_register_runtime_component(self):
|
||||
rct = RuntimeComponentTestC.create(self.char1)
|
||||
self.char1.components.add(rct)
|
||||
test_c = self.char1.components.get('test_c')
|
||||
|
||||
assert test_c
|
||||
assert test_c.my_int == 6
|
||||
assert test_c.my_dict == {}
|
||||
|
||||
def test_handler_can_add_default_component(self):
|
||||
self.char1.components.add_default("test_c")
|
||||
test_c = self.char1.components.get("test_c")
|
||||
|
||||
assert test_c
|
||||
assert test_c.my_int == 6
|
||||
|
||||
def test_handler_has_returns_true_for_any_components(self):
|
||||
rct = RuntimeComponentTestC.create(self.char1)
|
||||
handler = self.char1.components
|
||||
handler.add(rct)
|
||||
|
||||
assert handler.has("test_a")
|
||||
assert handler.has("test_b")
|
||||
assert handler.has("test_c")
|
||||
|
||||
def test_can_remove_component(self):
|
||||
rct = RuntimeComponentTestC.create(self.char1)
|
||||
handler = self.char1.components
|
||||
handler.add(rct)
|
||||
handler.remove(rct)
|
||||
|
||||
assert handler.has("test_a")
|
||||
assert handler.has("test_b")
|
||||
assert not handler.has("test_c")
|
||||
|
||||
def test_can_remove_component_by_name(self):
|
||||
rct = RuntimeComponentTestC.create(self.char1)
|
||||
handler = self.char1.components
|
||||
handler.add(rct)
|
||||
handler.remove_by_name("test_c")
|
||||
|
||||
assert handler.has("test_a")
|
||||
assert handler.has("test_b")
|
||||
assert not handler.has("test_c")
|
||||
|
||||
def test_cannot_replace_component(self):
|
||||
with self.assertRaises(Exception):
|
||||
self.char1.test_a = None
|
||||
|
||||
def test_can_get_component(self):
|
||||
rct = RuntimeComponentTestC.create(self.char1)
|
||||
handler = self.char1.components
|
||||
handler.add(rct)
|
||||
|
||||
assert handler.get("test_c") is rct
|
||||
|
||||
def test_can_access_component_regular_get(self):
|
||||
assert self.char1.cmp.test_a is self.char1.components.get('test_a')
|
||||
|
||||
def test_returns_none_with_regular_get_when_no_attribute(self):
|
||||
assert self.char1.cmp.does_not_exist is None
|
||||
Loading…
Add table
Add a link
Reference in a new issue