mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Fix merge conflicts
This commit is contained in:
commit
b2e41c2ddc
26 changed files with 999 additions and 573 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -56,3 +56,6 @@ docs/build
|
|||
|
||||
# Visual Studio Code (VS-Code)
|
||||
.vscode/
|
||||
|
||||
# Obsidian
|
||||
.obsidian
|
||||
|
|
|
|||
|
|
@ -160,10 +160,12 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
|||
- Attribute storage support defaultdics (Hendher)
|
||||
- Add ObjectParent mixin to default game folder template as an easy, ready-made
|
||||
way to override features on all ObjectDB-inheriting objects easily.
|
||||
source location, mimicking behavior of `at_pre_move` hook - returning False will abort move.
|
||||
- Add `TagProperty`, `AliasProperty` and `PermissionProperty` to assign these
|
||||
data in a similar way to django fields.
|
||||
- New `at_pre_object_receive(obj, source_location)` method on Objects. Called on
|
||||
destination, mimicking behavior of `at_pre_move` hook - returning False will abort move.
|
||||
- New `at_pre_object_leave(obj, destination)` method on Objects. Called on
|
||||
source location, mimicking behavior of `at_pre_move` hook - returning False will abort move.
|
||||
|
||||
|
||||
## Evennia 0.9.5
|
||||
|
|
|
|||
|
|
@ -5,21 +5,31 @@
|
|||
> set obj/myattr = "test"
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In-code
|
||||
obj.db.foo = [1,2,3, "bar"]
|
||||
:caption: In-code, using the .db wrapper
|
||||
obj.db.foo = [1, 2, 3, "bar"]
|
||||
value = obj.db.foo
|
||||
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In-code, using the .attributes handler
|
||||
obj.attributes.add("myattr", 1234, category="bar")
|
||||
value = attributes.get("myattr", category="bar")
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In-code, using `AttributeProperty` at class level
|
||||
from evennia import DefaultObject
|
||||
from evennia import AttributeProperty
|
||||
|
||||
_Attributes_ allow you to to store arbitrary data on objects and make sure the data survives a
|
||||
server reboot. An Attribute can store pretty much any
|
||||
class MyObject(DefaultObject):
|
||||
foo = AttributeProperty(default=[1, 2, 3, "bar"])
|
||||
myattr = AttributeProperty(100, category='bar')
|
||||
|
||||
```
|
||||
|
||||
_Attributes_ allow you to to store arbitrary data on objects and make sure the data survives a server reboot. An Attribute can store pretty much any
|
||||
Python data structure and data type, like numbers, strings, lists, dicts etc. You can also
|
||||
store (references to) database objects like characters and rooms.
|
||||
|
||||
- [What can be stored in an Attribute](#what-types-of-data-can-i-save-in-an-attribute) is a must-read
|
||||
also for experienced developers, to avoid getting surprised. Attributes can store _almost_ everything
|
||||
- [What can be stored in an Attribute](#what-types-of-data-can-i-save-in-an-attribute) is a must-read to avoid being surprised, also for experienced developers. Attributes can store _almost_ everything
|
||||
but you need to know the quirks.
|
||||
- [NAttributes](#in-memory-attributes-nattributes) are the in-memory, non-persistent
|
||||
siblings of Attributes.
|
||||
|
|
@ -29,7 +39,7 @@ store (references to) database objects like characters and rooms.
|
|||
|
||||
Attributes are usually handled in code. All [Typeclassed](./Typeclasses.md) entities
|
||||
([Accounts](./Accounts.md), [Objects](./Objects.md), [Scripts](./Scripts.md) and
|
||||
[Channels](./Channels.md)) all can (and usually do) have Attributes associated with them. There
|
||||
[Channels](./Channels.md)) can (and usually do) have Attributes associated with them. There
|
||||
are three ways to manage Attributes, all of which can be mixed.
|
||||
|
||||
- [Using the `.db` property shortcut](#using-db)
|
||||
|
|
@ -38,7 +48,7 @@ are three ways to manage Attributes, all of which can be mixed.
|
|||
|
||||
### Using .db
|
||||
|
||||
The simplest way to get/set Attributes is to use the `.db` shortcut:
|
||||
The simplest way to get/set Attributes is to use the `.db` shortcut. This allows for setting and getting Attributes that lack a _category_ (having category `None`)
|
||||
|
||||
```python
|
||||
import evennia
|
||||
|
|
@ -78,9 +88,8 @@ default `all` functionality until you delete it again.
|
|||
|
||||
### Using .attributes
|
||||
|
||||
If you don't know the name of the Attribute beforehand you can also use
|
||||
the `AttributeHandler`, available as `.attributes`. With no extra keywords this is identical
|
||||
to using the `.db` shortcut (`.db` is actually using the `AttributeHandler` internally):
|
||||
If you want to group your Attribute in a category, or don't know the name of the Attribute beforehand, you can make use of
|
||||
the [AttributeHandler](evennia.typeclasses.attributes.AttributeHandler), available as `.attributes` on all typeclassed entities. With no extra keywords, this is identical to using the `.db` shortcut (`.db` is actually using the `AttributeHandler` internally):
|
||||
|
||||
```python
|
||||
is_ouch = rose.attributes.get("has_thorns")
|
||||
|
|
@ -92,8 +101,7 @@ helmet = obj.attributes.get("helmet")
|
|||
obj.attributes.add("my game log", "long text about ...")
|
||||
```
|
||||
|
||||
With the `AttributeHandler` you can also give Attributes a `category`. By using a category you can
|
||||
separate same-named Attributes on the same object which can help organization:
|
||||
By using a category you can separate same-named Attributes on the same object to help organization.
|
||||
|
||||
```python
|
||||
# store (let's say we have gold_necklace and ringmail_armor from before)
|
||||
|
|
@ -105,11 +113,7 @@ neck_clothing = obj.attributes.get("neck", category="clothing")
|
|||
neck_armor = obj.attributes.get("neck", category="armor")
|
||||
```
|
||||
|
||||
If you don't specify a category, the Attribute's `category` will be `None`. Note that
|
||||
`None` is also considered a category of its own, so you won't find `None`-category Attributes mixed
|
||||
with Attributes having categories.
|
||||
|
||||
> When using `.db`, you will always use the `None` category.
|
||||
If you don't specify a category, the Attribute's `category` will be `None` and can thus also be found via `.db`. `None` is considered a category of its own, so you won't find `None`-category Attributes mixed with Attributes having categories.
|
||||
|
||||
Here are the methods of the `AttributeHandler`. See
|
||||
the [AttributeHandler API](evennia.typeclasses.attributes.AttributeHandler) for more details.
|
||||
|
|
@ -151,9 +155,8 @@ all_clothes = obj.attributes.all(category="clothes")
|
|||
|
||||
### Using AttributeProperty
|
||||
|
||||
There is a third way to set up an Attribute, and that is by setting up an `AttributeProperty`. This
|
||||
is done on the _class level_ of your typeclass and allows you to treat Attributes a bit like Django
|
||||
database Fields.
|
||||
The third way to set up an Attribute is to use an `AttributeProperty`. This
|
||||
is done on the _class level_ of your typeclass and allows you to treat Attributes a bit like Django database Fields. Unlike using `.db` and `.attributes`, an `AttributeProperty` can't be created on the fly, you must assign it in the class code.
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/characters.py
|
||||
|
|
@ -163,133 +166,62 @@ from evennia.typeclasses.attributes import AttributeProperty
|
|||
|
||||
class Character(DefaultCharacter):
|
||||
|
||||
strength = AttributeProperty(default=10, category='stat', autocreate=True)
|
||||
constitution = AttributeProperty(default=10, category='stat', autocreate=True)
|
||||
agility = AttributeProperty(default=10, category='stat', autocreate=True)
|
||||
magic = AttributeProperty(default=10, category='stat', autocreate=True)
|
||||
|
||||
sleepy = AttributeProperty(default=False)
|
||||
poisoned = AttributeProperty(default=False)
|
||||
strength = AttributeProperty(10, category='stat')
|
||||
constitution = AttributeProperty(11, category='stat')
|
||||
agility = AttributeProperty(12, category='stat')
|
||||
magic = AttributeProperty(13, category='stat')
|
||||
|
||||
sleepy = AttributeProperty(False, autocreate=False)
|
||||
poisoned = AttributeProperty(False, autocreate=False)
|
||||
|
||||
def at_object_creation(self):
|
||||
# ...
|
||||
```
|
||||
|
||||
These "Attribute-properties" will be made available to all instances of the class.
|
||||
When a new instance of the class is created, new `Attributes` will be created with the value and category given.
|
||||
|
||||
```{important}
|
||||
If you change the `default` of an `AttributeProperty` (and reload), it will
|
||||
change the default for _all_ instances of that class (it will not override
|
||||
explicitly changed values).
|
||||
```
|
||||
|
||||
```python
|
||||
char = evennia.search_object(Character, key="Bob")[0] # returns list, get 0th element
|
||||
|
||||
# get defaults
|
||||
strength = char.strength # will get the default value 10
|
||||
|
||||
# assign new values (this will create/update new Attributes)
|
||||
char.strength = 12
|
||||
char.constitution = 16
|
||||
char.agility = 8
|
||||
char.magic = 2
|
||||
|
||||
# you can also do arithmetic etc
|
||||
char.magic += 2 # char.magic is now 4
|
||||
|
||||
# check Attributes
|
||||
strength = char.strength # this is now 12
|
||||
is_sleepy = char.sleepy
|
||||
is_poisoned = char.poisoned
|
||||
|
||||
del char.strength # wipes the Attribute
|
||||
strength = char.strengh # back to the default (10) again
|
||||
```
|
||||
|
||||
See the [AttributeProperty](evennia.typeclasses.attributes.AttributeProperty) docs for more
|
||||
details on arguments.
|
||||
|
||||
An `AttributeProperty` will _not_ create an `Attribute` by default. A new `Attribute` will be created
|
||||
(or an existing one retrieved/updated) will happen differently depending on how the `autocreate`
|
||||
keyword:
|
||||
|
||||
- If `autocreate=False` (default), an `Attribute` will be created only if the field is explicitly
|
||||
assigned a value (even if the value is the same as the default, such as `char.strength = 10`).
|
||||
- If `autocreate=True`, an `Attribute` will be created as soon as the field is _accessed_ in
|
||||
any way (So both `strength = char.strength` and `char.strength = 10` will both make sure that
|
||||
an `Attribute` exists.
|
||||
|
||||
Example:
|
||||
With `AttributeProperty`'s set up like this, one can access the underlying `Attribute` like a regular property on the created object:
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/objects.py
|
||||
char = create_object(Character)
|
||||
|
||||
from evennia import create_object
|
||||
from evennia import DefaultObject
|
||||
from evennia.typeclasses.attributes import AttributeProperty
|
||||
char.strength # returns 10
|
||||
char.agility = 15 # assign a new value (category remains 'stat')
|
||||
|
||||
class Object(DefaultObject):
|
||||
|
||||
value_a = AttributeProperty(default="foo")
|
||||
value_b = AttributeProperty(default="bar", autocreate=True)
|
||||
|
||||
obj = evennia.create_object(key="Dummy")
|
||||
char.db.magic # returns None (wrong category)
|
||||
char.attributes.get("agility", category="stat") # returns 15
|
||||
|
||||
# these will find NO Attributes!
|
||||
obj.db.value_a
|
||||
obj.attributes.get("value_a")
|
||||
obj.db.value_b
|
||||
obj.attributes.get("value_b")
|
||||
char.db.sleepy # returns None because autocreate=False (see below)
|
||||
|
||||
# get data from attribute-properties
|
||||
vala = obj.value_a # returns "foo"
|
||||
valb = obj.value_b # return "bar" AND creates the Attribute (autocreate)
|
||||
|
||||
# the autocreate property will now be found
|
||||
obj.db.value_a # still not found
|
||||
obj.attributes.get("value_a") # ''
|
||||
obj.db.value_b # now returns "bar"
|
||||
obj.attributes.get("value_b") # ''
|
||||
|
||||
# assign new values
|
||||
obj.value_a = 10 # will now create a new Attribute
|
||||
obj.value_b = 12 # will update the existing Attribute
|
||||
|
||||
# both are now found as Attributes
|
||||
obj.db.value_a # now returns 10
|
||||
obj.attributes.get("value_a") # ''
|
||||
obj.db.value_b # now returns 12
|
||||
obj.attributes.get("value_b") # ''
|
||||
```
|
||||
|
||||
If you always access your Attributes via the `AttributeProperty` this does not matter that much
|
||||
(it's also a bit of an optimization to not create an actual database `Attribute` unless the value changed).
|
||||
But until an `Attribute` has been created, `AttributeProperty` fields will _not_ show up with the
|
||||
`examine` command or by using the `.db` or `.attributes` handlers - so this is a bit inconsistent.
|
||||
If this is important, you need to 'initialize' them by accessing them at least once ... something
|
||||
like this:
|
||||
```{warning}
|
||||
Be careful to not assign AttributeProperty's to names of properties and methods already existing on the class, like 'key' or 'at_object_creation'. That could lead to very confusing errors.
|
||||
```
|
||||
|
||||
The `autocreate=False` (default is `True`) used for `sleepy` and `poisoned` is worth a closer explanation. When `False`, _no_ Attribute will be auto-created for these AttributProperties unless they are _explicitly_ set.
|
||||
The advantage of not creating an Attribute is that the default value given to `AttributeProperty` is returned with no database access unless you change it. This also means that if you want to change the default later, all entities previously create will inherit the new default.
|
||||
The drawback is that without a database precense you can't find the Attribute via `.db` and `.attributes.get` (or by querying for it in other ways in the database):
|
||||
|
||||
```python
|
||||
# ...
|
||||
class Character(DefaultCharacter):
|
||||
char.sleepy # returns False, no db access
|
||||
|
||||
strength = AttributeProperty(12, autocreate=True)
|
||||
agility = AttributeProperty(12, autocreate=True)
|
||||
char.db.sleepy # returns None - no Attribute exists
|
||||
char.attributes.get("sleepy") # returns None too
|
||||
|
||||
char.sleepy = True # now an Attribute is created
|
||||
char.db.sleepy # now returns True!
|
||||
char.attributes.get("sleepy") # now returns True
|
||||
|
||||
char.sleepy # now returns True, involves db access
|
||||
|
||||
def at_object_creation(self):
|
||||
# initializing
|
||||
self.strength # by accessing it, the Attribute is auto-created
|
||||
self.agility # ''
|
||||
```
|
||||
|
||||
```{important}
|
||||
If you created your `AttributeProperty` with a `category`, you *must* specify the
|
||||
category in `.attributes.get()` if you want to find it this way. Remember that
|
||||
`.db` always uses a `category` of `None`.
|
||||
```
|
||||
You can e.g. `del char.strength` to set the value back to the default (the value defined
|
||||
in the `AttributeProperty`).
|
||||
|
||||
See the [AttributeProperty API](evennia.typeclasses.attributes.AttributeProperty) for more details on how to create it with special options, like giving access-restrictions.
|
||||
|
||||
|
||||
## Managing Attributes in-game
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ All new accounts are given a default set of permissions defined by
|
|||
## Managing Permissions
|
||||
|
||||
In-game, you use the `perm` command to add and remove permissions
|
||||
|
||||
j
|
||||
perm/account Tommy = Builders
|
||||
perm/account/del Tommy = Builders
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,33 @@
|
|||
# Tags
|
||||
|
||||
```{code-block}
|
||||
:caption: In game
|
||||
> tag obj = tagname
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In code, using .tags (TagHandler)
|
||||
|
||||
A common task of a game designer is to organize and find groups of objects and do operations on
|
||||
them. A classic example is to have a weather script affect all "outside" rooms. Another would be for
|
||||
a player casting a magic spell that affects every location "in the dungeon", but not those
|
||||
"outside". Another would be to quickly find everyone joined with a particular guild or everyone
|
||||
currently dead.
|
||||
obj.tags.add("mytag", category="foo")
|
||||
obj.tags.get("mytag", category="foo")
|
||||
```
|
||||
|
||||
*Tags* are short text labels that you attach to objects so as to easily be able to retrieve and
|
||||
group them. An Evennia entity can be tagged with any number of Tags. On the database side, Tag
|
||||
entities are *shared* between all objects with that tag. This makes them very efficient but also
|
||||
fundamentally different from [Attributes](./Attributes.md), each of which always belongs to one *single*
|
||||
object.
|
||||
```{code-block} python
|
||||
:caption: In code, using TagProperty (auto-assign tag to all instances of the class)
|
||||
|
||||
In Evennia, Tags are technically also used to implement `Aliases` (alternative names for objects)
|
||||
and `Permissions` (simple strings for [Locks](./Locks.md) to check for).
|
||||
from evennia import DefaultObject
|
||||
from evennia import TagProperty
|
||||
class Sword(DefaultObject):
|
||||
can_be_wielded = TagProperty(category='combat')
|
||||
has_sharp_edge = TagProperty(category='combat')
|
||||
|
||||
```
|
||||
|
||||
_Tags_ are short text lables one can 'hang' on objects in order to organize, group and quickly find out their properties. An Evennia entity can be tagged by any number of tags. They are more efficient than [Attributes](Attributes) since on the database-side, Tags are _shared_ between all objects with that particular tag. A tag does not carry a value in itself; it either sits on the entity
|
||||
|
||||
Above, the tags inform us that the `Sword` is both sharp and can be wielded. If that's all they do, they could just be a normal Python flag. When tags become important is if there are a lot of objects with different combinations of tags. Maybe you have a magical spell that dulls _all_ sharp-edged objects in the castle - whether sword, dagger, spear or kitchen knife! You can then just grab all objects with the `has_sharp_edge` tag.
|
||||
Another example would be a weather script affecting all rooms tagged as `outdoors` or finding all characters tagged with `belongs_to_fighter_guild`.
|
||||
|
||||
In Evennia, Tags are technically also used to implement `Aliases` (alternative names for objects) and `Permissions` (simple strings for [Locks](./Locks.md) to check for).
|
||||
|
||||
|
||||
## Properties of Tags (and Aliases and Permissions)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ updated after May 2021 will be missing some translations.
|
|||
+===============+======================+==============+
|
||||
| es | Spanish | Aug 2019 |
|
||||
+---------------+----------------------+--------------+
|
||||
| fr | French | Nov 2018 |
|
||||
| fr | French | Mar 2022 |
|
||||
+---------------+----------------------+--------------+
|
||||
| it | Italian | Feb 2015 |
|
||||
+---------------+----------------------+--------------+
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ login systems, new command syntaxes, and build helpers._
|
|||
Contrib-AWSStorage.md
|
||||
Contrib-Building-Menu.md
|
||||
Contrib-Color-Markups.md
|
||||
Contrib-Components.md
|
||||
Contrib-Custom-Gametime.md
|
||||
Contrib-Email-Login.md
|
||||
Contrib-Ingame-Python.md
|
||||
|
|
@ -79,6 +80,16 @@ Additional color markup styles for Evennia (extending or replacing the default
|
|||
|
||||
|
||||
|
||||
### Contrib: `components`
|
||||
|
||||
__Contrib by ChrisLR 2021__
|
||||
|
||||
# The Components Contrib
|
||||
|
||||
[Read the documentation](./Contrib-Components.md) - [Browse the Code](evennia.contrib.base_systems.components)
|
||||
|
||||
|
||||
|
||||
### Contrib: `custom_gametime`
|
||||
|
||||
_Contrib by vlgeoff, 2017 - based on Griatch's core original_
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ evennia.contrib.base\_systems
|
|||
evennia.contrib.base_systems.awsstorage
|
||||
evennia.contrib.base_systems.building_menu
|
||||
evennia.contrib.base_systems.color_markups
|
||||
evennia.contrib.base_systems.components
|
||||
evennia.contrib.base_systems.custom_gametime
|
||||
evennia.contrib.base_systems.email_login
|
||||
evennia.contrib.base_systems.ingame_python
|
||||
|
|
|
|||
|
|
@ -1253,6 +1253,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
"""
|
||||
self.basetype_setup()
|
||||
self.at_account_creation()
|
||||
# initialize Attribute/TagProperties
|
||||
self.init_evennia_properties()
|
||||
|
||||
permissions = [settings.PERMISSION_ACCOUNT_DEFAULT]
|
||||
if hasattr(self, "_createdict"):
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
|
|||
elif len(channels) > 1:
|
||||
self.msg(
|
||||
"Multiple possible channel matches/alias for "
|
||||
"'{channelname}':\n" + ", ".join(chan.key for chan in channels)
|
||||
f"'{channelname}':\n" + ", ".join(chan.key for chan in channels)
|
||||
)
|
||||
return None
|
||||
return channels[0]
|
||||
|
|
|
|||
|
|
@ -19,15 +19,33 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
|
|||
create different types of communication channels.
|
||||
|
||||
Class-level variables:
|
||||
- `send_to_online_only` (bool, default True) - if set, will only try to
|
||||
send to subscribers that are actually active. This is a useful optimization.
|
||||
- `log_file` (str, default `"channel_{channelname}.log"`). This is the
|
||||
log file to which the channel history will be saved. The `{channelname}` tag
|
||||
will be replaced by the key of the Channel. If an Attribute 'log_file'
|
||||
is set, this will be used instead. If this is None and no Attribute is found,
|
||||
no history will be saved.
|
||||
- `channel_prefix_string` (str, default `"[{channelname} ]"`) - this is used
|
||||
as a simple template to get the channel prefix with `.channel_prefix()`.
|
||||
- `send_to_online_only` (bool, default True) - if set, will only try to
|
||||
send to subscribers that are actually active. This is a useful optimization.
|
||||
- `log_file` (str, default `"channel_{channelname}.log"`). This is the
|
||||
log file to which the channel history will be saved. The `{channelname}` tag
|
||||
will be replaced by the key of the Channel. If an Attribute 'log_file'
|
||||
is set, this will be used instead. If this is None and no Attribute is found,
|
||||
no history will be saved.
|
||||
- `channel_prefix_string` (str, default `"[{channelname} ]"`) - this is used
|
||||
as a simple template to get the channel prefix with `.channel_prefix()`. It is used
|
||||
in front of every channel message; use `{channelmessage}` token to insert the
|
||||
name of the current channel. Set to `None` if you want no prefix (or want to
|
||||
handle it in a hook during message generation instead.
|
||||
- `channel_msg_nick_pattern`(str, default `"{alias}\\s*?|{alias}\\s+?(?P<arg1>.+?)") -
|
||||
this is what used when a channel subscriber gets a channel nick assigned to this
|
||||
channel. The nickhandler uses the pattern to pick out this channel's name from user
|
||||
input. The `{alias}` token will get both the channel's key and any set/custom aliases
|
||||
per subscriber. You need to allow for an `<arg1>` regex group to catch any message
|
||||
that should be send to the channel. You usually don't need to change this pattern
|
||||
unless you are changing channel command-style entirely.
|
||||
- `channel_msg_nick_replacement` (str, default `"channel {channelname} = $1"` - this
|
||||
is used by the nickhandler to generate a replacement string once the nickhandler (using
|
||||
the `channel_msg_nick_pattern`) identifies that the channel should be addressed
|
||||
to send a message to it. The `<arg1>` regex pattern match from `channel_msg_nick_pattern`
|
||||
will end up at the `$1` position in the replacement. Together, this allows you do e.g.
|
||||
'public Hello' and have that become a mapping to `channel public = Hello`. By default,
|
||||
the account-level `channel` command is used. If you were to rename that command you must
|
||||
tweak the output to something like `yourchannelcommandname {channelname} = $1`.
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -58,6 +76,9 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
|
|||
"""
|
||||
self.basetype_setup()
|
||||
self.at_channel_creation()
|
||||
# initialize Attribute/TagProperties
|
||||
self.init_evennia_properties()
|
||||
|
||||
if hasattr(self, "_createdict"):
|
||||
# this is only set if the channel was created
|
||||
# with the utils.create.create_channel function.
|
||||
|
|
|
|||
|
|
@ -50,7 +50,29 @@ class Health(Component):
|
|||
health = DBField(default=1)
|
||||
```
|
||||
|
||||
Note that default is optional and will default to None
|
||||
Note that default is optional and will default to None.
|
||||
|
||||
Adding a component to a host will also a similarly named tag with 'components' as category.
|
||||
A Component named health will appear as key="health, category="components".
|
||||
This allows you to retrieve objects with specific components by searching with the tag.
|
||||
|
||||
It is also possible to add Component Tags the same way, using TagField.
|
||||
TagField accepts a default value and can be used to store a single or multiple tags.
|
||||
Default values are automatically added when the component is added.
|
||||
Component Tags are cleared from the host if the component is removed.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from evennia.contrib.base_systems.components import Component, TagField
|
||||
|
||||
class Health(Component):
|
||||
resistances = TagField()
|
||||
vulnerability = TagField(default="fire", enforce_single=True)
|
||||
```
|
||||
|
||||
The 'resistances' field in this example can be set to multiple times and it will keep the added tags.
|
||||
The 'vulnerability' field in this example will override the previous tag with the new one.
|
||||
|
||||
|
||||
|
||||
Each typeclass using the ComponentHolderMixin can declare its components
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ 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.dbfield import DBField, NDBField, TagField
|
||||
from evennia.contrib.base_systems.components.holder import ComponentHolderMixin, ComponentProperty
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -144,6 +144,11 @@ class Component:
|
|||
ndb_fields = getattr(self, "_ndb_fields", {})
|
||||
return ndb_fields.keys()
|
||||
|
||||
@property
|
||||
def tag_field_names(self):
|
||||
tag_fields = getattr(self, "_tag_fields", {})
|
||||
return tag_fields.keys()
|
||||
|
||||
|
||||
class ComponentRegisterError(Exception):
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -52,3 +52,64 @@ class NDBField(NAttributeProperty):
|
|||
ndb_fields = {}
|
||||
setattr(owner, '_ndb_fields', ndb_fields)
|
||||
ndb_fields[name] = self
|
||||
|
||||
|
||||
class TagField:
|
||||
"""
|
||||
Component Tags Descriptor.
|
||||
Allows you to set Tags related to a component on the class.
|
||||
The tags are set with a prefixed category, so it can support
|
||||
multiple tags or enforce a single one.
|
||||
|
||||
Default value of a tag is added when the component is registered.
|
||||
Tags are removed if the component itself is removed.
|
||||
"""
|
||||
def __init__(self, default=None, enforce_single=False):
|
||||
self._category_key = None
|
||||
self._default = default
|
||||
self._enforce_single = enforce_single
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
"""
|
||||
Called when TagField is first assigned to the class.
|
||||
It is called with the component class and the name of the field.
|
||||
"""
|
||||
self._category_key = f"{owner.name}::{name}"
|
||||
tag_fields = getattr(owner, "_tag_fields", None)
|
||||
if tag_fields is None:
|
||||
tag_fields = {}
|
||||
setattr(owner, '_tag_fields', tag_fields)
|
||||
tag_fields[name] = self
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""
|
||||
Called when retrieving the value of the TagField.
|
||||
It is called with the component instance and the class.
|
||||
"""
|
||||
tag_value = instance.host.tags.get(
|
||||
default=self._default,
|
||||
category=self._category_key,
|
||||
)
|
||||
return tag_value
|
||||
|
||||
def __set__(self, instance, value):
|
||||
"""
|
||||
Called when setting a value on the TagField.
|
||||
It is called with the component instance and the value.
|
||||
"""
|
||||
|
||||
tag_handler = instance.host.tags
|
||||
if self._enforce_single:
|
||||
tag_handler.clear(category=self._category_key)
|
||||
|
||||
tag_handler.add(
|
||||
key=value,
|
||||
category=self._category_key,
|
||||
)
|
||||
|
||||
def __delete__(self, instance):
|
||||
"""
|
||||
Used when 'del' is called on the TagField.
|
||||
It is called with the component instance.
|
||||
"""
|
||||
instance.host.tags.clear(category=self._category_key)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class ComponentHandler:
|
|||
"""
|
||||
self._set_component(component)
|
||||
self.db_names.append(component.name)
|
||||
self.host.tags.add(component.name, category="components")
|
||||
self._add_component_tags(component)
|
||||
component.at_added(self.host)
|
||||
|
||||
def add_default(self, name):
|
||||
|
|
@ -85,9 +85,24 @@ class ComponentHandler:
|
|||
new_component = component.default_create(self.host)
|
||||
self._set_component(new_component)
|
||||
self.db_names.append(name)
|
||||
self.host.tags.add(name, category="components")
|
||||
self._add_component_tags(new_component)
|
||||
new_component.at_added(self.host)
|
||||
|
||||
def _add_component_tags(self, component):
|
||||
"""
|
||||
Private method that adds the Tags set on a Component via TagFields
|
||||
It will also add the name of the component so objects can be filtered
|
||||
by the components the implement.
|
||||
|
||||
Args:
|
||||
component (object): The component instance that is added.
|
||||
"""
|
||||
self.host.tags.add(component.name, category="components")
|
||||
for tag_field_name in component.tag_field_names:
|
||||
default_tag = type(component).__dict__[tag_field_name]._default
|
||||
if default_tag:
|
||||
setattr(component, tag_field_name, default_tag)
|
||||
|
||||
def remove(self, component):
|
||||
"""
|
||||
Method to remove a component instance from a host.
|
||||
|
|
@ -100,9 +115,9 @@ class ComponentHandler:
|
|||
"""
|
||||
component_name = component.name
|
||||
if component_name in self._loaded_components:
|
||||
self._remove_component_tags(component)
|
||||
component.at_removed(self.host)
|
||||
self.db_names.remove(component_name)
|
||||
self.host.tags.remove(component_name, category="components")
|
||||
del self._loaded_components[component_name]
|
||||
else:
|
||||
message = f"Cannot remove {component_name} from {self.host.name} as it is not registered."
|
||||
|
|
@ -123,11 +138,24 @@ class ComponentHandler:
|
|||
message = f"Cannot remove {name} from {self.host.name} as it is not registered."
|
||||
raise ComponentIsNotRegistered(message)
|
||||
|
||||
self._remove_component_tags(instance)
|
||||
instance.at_removed(self.host)
|
||||
self.db_names.remove(name)
|
||||
self.host.tags.remove(name, category="components")
|
||||
|
||||
del self._loaded_components[name]
|
||||
|
||||
def _remove_component_tags(self, component):
|
||||
"""
|
||||
Private method that will remove the Tags set on a Component via TagFields
|
||||
It will also remove the component name tag.
|
||||
|
||||
Args:
|
||||
component (object): The component instance that is removed.
|
||||
"""
|
||||
self.host.tags.remove(component.name, category="components")
|
||||
for tag_field_name in component.tag_field_names:
|
||||
delattr(component, tag_field_name)
|
||||
|
||||
def get(self, name):
|
||||
"""
|
||||
Method to retrieve a cached Component instance by its name.
|
||||
|
|
@ -225,8 +253,8 @@ class ComponentHolderMixin(object):
|
|||
Method that add component related tags that were set using ComponentProperty.
|
||||
"""
|
||||
super().basetype_posthook_setup()
|
||||
for component_name in self.db.component_names:
|
||||
self.tags.add(component_name, category="components")
|
||||
for component in self.components._loaded_components.values():
|
||||
self.components._add_component_tags(component)
|
||||
|
||||
@property
|
||||
def components(self) -> ComponentHandler:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from evennia.contrib.base_systems.components import Component, DBField
|
||||
from evennia.contrib.base_systems.components import Component, DBField, TagField
|
||||
from evennia.contrib.base_systems.components.holder import ComponentProperty, ComponentHolderMixin
|
||||
from evennia.objects.objects import DefaultCharacter
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
|
|
@ -14,12 +14,17 @@ class ComponentTestB(Component):
|
|||
name = "test_b"
|
||||
my_int = DBField(default=1)
|
||||
my_list = DBField(default=[])
|
||||
default_tag = TagField(default="initial_value")
|
||||
single_tag = TagField(enforce_single=True)
|
||||
multiple_tags = TagField()
|
||||
default_single_tag = TagField(default="initial_value", enforce_single=True)
|
||||
|
||||
|
||||
class RuntimeComponentTestC(Component):
|
||||
name = "test_c"
|
||||
my_int = DBField(default=6)
|
||||
my_dict = DBField(default={})
|
||||
added_tag = TagField(default="added_value")
|
||||
|
||||
|
||||
class CharacterWithComponents(ComponentHolderMixin, DefaultCharacter):
|
||||
|
|
@ -111,7 +116,11 @@ class TestComponents(EvenniaTest):
|
|||
def test_host_has_class_component_tags(self):
|
||||
assert self.char1.tags.has(key="test_a", category="components")
|
||||
assert self.char1.tags.has(key="test_b", category="components")
|
||||
assert self.char1.tags.has(key="initial_value", category="test_b::default_tag")
|
||||
assert self.char1.test_b.default_tag == "initial_value"
|
||||
assert not self.char1.tags.has(key="test_c", category="components")
|
||||
assert not self.char1.tags.has(category="test_b::single_tag")
|
||||
assert not self.char1.tags.has(category="test_b::multiple_tags")
|
||||
|
||||
def test_host_has_added_component_tags(self):
|
||||
rct = RuntimeComponentTestC.create(self.char1)
|
||||
|
|
@ -119,12 +128,16 @@ class TestComponents(EvenniaTest):
|
|||
test_c = self.char1.components.get('test_c')
|
||||
|
||||
assert self.char1.tags.has(key="test_c", category="components")
|
||||
assert self.char1.tags.has(key="added_value", category="test_c::added_tag")
|
||||
assert test_c.added_tag == "added_value"
|
||||
|
||||
def test_host_has_added_default_component_tags(self):
|
||||
self.char1.components.add_default("test_c")
|
||||
test_c = self.char1.components.get("test_c")
|
||||
|
||||
assert self.char1.tags.has(key="test_c", category="components")
|
||||
assert self.char1.tags.has(key="added_value", category="test_c::added_tag")
|
||||
assert test_c.added_tag == "added_value"
|
||||
|
||||
def test_host_remove_component_tags(self):
|
||||
rct = RuntimeComponentTestC.create(self.char1)
|
||||
|
|
@ -134,6 +147,7 @@ class TestComponents(EvenniaTest):
|
|||
handler.remove(rct)
|
||||
|
||||
assert not self.char1.tags.has(key="test_c", category="components")
|
||||
assert not self.char1.tags.has(key="added_value", category="test_c::added_tag")
|
||||
|
||||
def test_host_remove_by_name_component_tags(self):
|
||||
rct = RuntimeComponentTestC.create(self.char1)
|
||||
|
|
@ -142,4 +156,33 @@ class TestComponents(EvenniaTest):
|
|||
assert self.char1.tags.has(key="test_c", category="components")
|
||||
handler.remove_by_name("test_c")
|
||||
|
||||
assert not self.char1.tags.has(key="test_c", category="components")
|
||||
assert not self.char1.tags.has(key="test_c", category="components")
|
||||
assert not self.char1.tags.has(key="added_value", category="test_c::added_tag")
|
||||
|
||||
def test_component_tags_only_hold_one_value_when_enforce_single(self):
|
||||
test_b = self.char1.components.get('test_b')
|
||||
test_b.single_tag = "first_value"
|
||||
test_b.single_tag = "second value"
|
||||
|
||||
assert self.char1.tags.has(key="second value", category="test_b::single_tag")
|
||||
assert test_b.single_tag == "second value"
|
||||
assert not self.char1.tags.has(key="first_value", category="test_b::single_tag")
|
||||
|
||||
def test_component_tags_default_value_is_overridden_when_enforce_single(self):
|
||||
test_b = self.char1.components.get('test_b')
|
||||
test_b.default_single_tag = "second value"
|
||||
|
||||
assert self.char1.tags.has(key="second value", category="test_b::default_single_tag")
|
||||
assert test_b.default_single_tag == "second value"
|
||||
assert not self.char1.tags.has(key="first_value", category="test_b::default_single_tag")
|
||||
|
||||
def test_component_tags_support_multiple_values_by_default(self):
|
||||
test_b = self.char1.components.get('test_b')
|
||||
test_b.multiple_tags = "first value"
|
||||
test_b.multiple_tags = "second value"
|
||||
test_b.multiple_tags = "third value"
|
||||
|
||||
assert all(val in test_b.multiple_tags for val in ("first value", "second value", "third value"))
|
||||
assert self.char1.tags.has(key="first value", category="test_b::multiple_tags")
|
||||
assert self.char1.tags.has(key="second value", category="test_b::multiple_tags")
|
||||
assert self.char1.tags.has(key="third value", category="test_b::multiple_tags")
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1246,6 +1246,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
"""
|
||||
self.basetype_setup()
|
||||
self.at_object_creation()
|
||||
# initialize Attribute/TagProperties
|
||||
self.init_evennia_properties()
|
||||
|
||||
if hasattr(self, "_createdict"):
|
||||
# this will only be set if the utils.create function
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase
|
||||
from evennia import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit
|
||||
from evennia.typeclasses.attributes import AttributeProperty
|
||||
from evennia.typeclasses.tags import TagProperty, AliasProperty, PermissionProperty
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.objects.objects import DefaultObject
|
||||
from evennia.utils import create
|
||||
|
||||
|
||||
|
|
@ -227,3 +230,53 @@ class TestContentHandler(BaseEvenniaTest):
|
|||
self.obj2.move_to(self.room1)
|
||||
self.obj2.move_to(self.room2)
|
||||
self.assertEqual(self.room2.contents, [self.obj1, self.obj2])
|
||||
|
||||
|
||||
class TestObjectPropertiesClass(DefaultObject):
|
||||
attr1 = AttributeProperty(default="attr1")
|
||||
attr2 = AttributeProperty(default="attr2", category="attrcategory")
|
||||
attr3 = AttributeProperty(default="attr3", autocreate=False)
|
||||
tag1 = TagProperty()
|
||||
tag2 = TagProperty(category="tagcategory")
|
||||
testalias = AliasProperty()
|
||||
testperm = PermissionProperty()
|
||||
|
||||
class TestProperties(EvenniaTestCase):
|
||||
"""
|
||||
Test Properties.
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
self.obj = create.create_object(TestObjectPropertiesClass, key="testobj")
|
||||
|
||||
def tearDown(self):
|
||||
self.obj.delete()
|
||||
|
||||
def test_properties(self):
|
||||
"""
|
||||
Test all properties assigned at class level.
|
||||
"""
|
||||
obj = self.obj
|
||||
|
||||
self.assertEqual(obj.db.attr1, "attr1")
|
||||
self.assertEqual(obj.attributes.get("attr1"), "attr1")
|
||||
self.assertEqual(obj.attr1, "attr1")
|
||||
|
||||
self.assertEqual(obj.attributes.get("attr2", category="attrcategory"), "attr2")
|
||||
self.assertEqual(obj.db.attr2, None) # category mismatch
|
||||
self.assertEqual(obj.attr2, "attr2")
|
||||
|
||||
self.assertEqual(obj.db.attr3, None) # non-autocreate, so not in db yet
|
||||
self.assertFalse(obj.attributes.has("attr3"))
|
||||
self.assertEqual(obj.attr3, "attr3")
|
||||
|
||||
obj.attr3 = "attr3b" # stores it in db!
|
||||
|
||||
self.assertEqual(obj.db.attr3, "attr3b")
|
||||
self.assertTrue(obj.attributes.has("attr3"))
|
||||
|
||||
self.assertTrue(obj.tags.has("tag1"))
|
||||
self.assertTrue(obj.tags.has("tag2", category="tagcategory"))
|
||||
|
||||
self.assertTrue(obj.aliases.has("testalias"))
|
||||
self.assertTrue(obj.permissions.has("testperm"))
|
||||
|
|
|
|||
|
|
@ -528,8 +528,10 @@ def search_prototype(
|
|||
|
||||
"""
|
||||
# This will load the prototypes the first time they are searched
|
||||
if not _MODULE_PROTOTYPE_MODULES:
|
||||
loaded = getattr(load_module_prototypes, '_LOADED', False)
|
||||
if not loaded:
|
||||
load_module_prototypes()
|
||||
setattr(load_module_prototypes, '_LOADED', True)
|
||||
|
||||
# prototype keys are always in lowecase
|
||||
if key:
|
||||
|
|
|
|||
|
|
@ -400,7 +400,10 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
overriding the call (unused by default).
|
||||
|
||||
"""
|
||||
self.basetype_setup()
|
||||
self.at_script_creation()
|
||||
# initialize Attribute/TagProperties
|
||||
self.init_evennia_properties()
|
||||
|
||||
if hasattr(self, "_createdict"):
|
||||
# this will only be set if the utils.create_script
|
||||
|
|
@ -471,6 +474,14 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
super().delete()
|
||||
return True
|
||||
|
||||
def basetype_setup(self):
|
||||
"""
|
||||
Changes fundamental aspects of the type. Usually changes are made in at_script creation
|
||||
instead.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_init(self):
|
||||
"""
|
||||
Called when the Script is cached in the idmapper. This is usually more reliable
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ class AttributeProperty:
|
|||
|
||||
attrhandler_name = "attributes"
|
||||
|
||||
def __init__(self, default=None, category=None, strattr=False, lockstring="", autocreate=False):
|
||||
def __init__(self, default=None, category=None, strattr=False, lockstring="", autocreate=True):
|
||||
"""
|
||||
Initialize an Attribute as a property descriptor.
|
||||
|
||||
|
|
@ -188,12 +188,12 @@ class AttributeProperty:
|
|||
lockstring (str): This is not itself useful with the property, but only if
|
||||
using the full AttributeHandler.get(accessing_obj=...) to access the
|
||||
Attribute.
|
||||
autocreate (bool): If an un-found Attr should lead to auto-creating the
|
||||
Attribute (with the default value). If `False`, the property will
|
||||
return the default value until it has been explicitly set. This means
|
||||
less database accesses, but also means the property will have no
|
||||
corresponding Attribute if wanting to access it directly via the
|
||||
AttributeHandler (it will also not show up in `examine`).
|
||||
autocreate (bool): True by default; this means Evennia makes sure to create a new
|
||||
copy of the Attribute (with the default value) whenever a new object with this
|
||||
property is created. If `False`, no Attribute will be created until the property
|
||||
is explicitly assigned a value. This makes it more efficient while it retains
|
||||
its default (there's no db access), but without an actual Attribute generated,
|
||||
one cannot access it via .db, the AttributeHandler or see it with `examine`.
|
||||
|
||||
"""
|
||||
self._default = default
|
||||
|
|
@ -218,21 +218,20 @@ class AttributeProperty:
|
|||
"""
|
||||
value = self._default
|
||||
try:
|
||||
value = getattr(instance, self.attrhandler_name).get(
|
||||
value = self.at_get(getattr(instance, self.attrhandler_name).get(
|
||||
key=self._key,
|
||||
default=self._default,
|
||||
category=self._category,
|
||||
strattr=self._strattr,
|
||||
raise_exception=self._autocreate,
|
||||
)
|
||||
))
|
||||
except AttributeError:
|
||||
if self._autocreate:
|
||||
# attribute didn't exist and autocreate is set
|
||||
self.__set__(instance, self._default)
|
||||
else:
|
||||
raise
|
||||
finally:
|
||||
return value
|
||||
return value
|
||||
|
||||
def __set__(self, instance, value):
|
||||
"""
|
||||
|
|
@ -242,7 +241,7 @@ class AttributeProperty:
|
|||
(
|
||||
getattr(instance, self.attrhandler_name).add(
|
||||
self._key,
|
||||
value,
|
||||
self.at_set(value),
|
||||
category=self._category,
|
||||
lockstring=self._lockstring,
|
||||
strattr=self._strattr,
|
||||
|
|
@ -251,10 +250,43 @@ class AttributeProperty:
|
|||
|
||||
def __delete__(self, instance):
|
||||
"""
|
||||
Called when running `del` on the field. Will remove/clear the Attribute.
|
||||
Called when running `del` on the property. Will remove/clear the Attribute. Note that
|
||||
the Attribute will be recreated next retrieval unless the AttributeProperty is also
|
||||
removed in code!
|
||||
|
||||
"""
|
||||
(getattr(instance, self.attrhandler_name).remove(key=self._key, category=self._category))
|
||||
getattr(instance, self.attrhandler_name).remove(key=self._key, category=self._category)
|
||||
|
||||
def at_set(self, value):
|
||||
"""
|
||||
The value to set is passed through the method. It can be used to customize/validate
|
||||
the input in a custom child class.
|
||||
|
||||
Args:
|
||||
value (any): The value about to the stored in this Attribute.
|
||||
|
||||
Returns:
|
||||
any: The value to store.
|
||||
|
||||
Raises:
|
||||
AttributeError: If the value is invalid to store.
|
||||
|
||||
"""
|
||||
return value
|
||||
|
||||
def at_get(self, value):
|
||||
"""
|
||||
The value returned from the Attribute is passed through this method. It can be used
|
||||
to react to the retrieval or modify the result in some way.
|
||||
|
||||
Args:
|
||||
value (any): Value returned from the Attribute.
|
||||
|
||||
Returns:
|
||||
any: The value to return to the caller.
|
||||
|
||||
"""
|
||||
return value
|
||||
|
||||
|
||||
class NAttributeProperty(AttributeProperty):
|
||||
|
|
|
|||
|
|
@ -325,6 +325,18 @@ class TypedObject(SharedMemoryModel):
|
|||
super().__init__(*args, **kwargs)
|
||||
self.set_class_from_typeclass(typeclass_path=typeclass_path)
|
||||
|
||||
def init_evennia_properties(self):
|
||||
"""
|
||||
Called by creation methods; makes sure to initialize Attribute/TagProperties
|
||||
by fetching them once.
|
||||
"""
|
||||
for propkey, prop in self.__class__.__dict__.items():
|
||||
if hasattr(prop, "__set_name__"):
|
||||
try:
|
||||
getattr(self, propkey)
|
||||
except Exception:
|
||||
log_trace()
|
||||
|
||||
# initialize all handlers in a lazy fashion
|
||||
@lazy_property
|
||||
def attributes(self):
|
||||
|
|
|
|||
|
|
@ -96,6 +96,75 @@ class Tag(models.Model):
|
|||
# Handlers making use of the Tags model
|
||||
#
|
||||
|
||||
class TagProperty:
|
||||
"""
|
||||
Tag property descriptor. Allows for setting tags on an object as Django-like 'fields'
|
||||
on the class level. Since Tags are almost always used for querying, Tags are always
|
||||
created/assigned along with the object. Make sure the property/tagname does not collide
|
||||
with an existing method/property on the class. If it does, you must use tags.add()
|
||||
instead.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
mytag = TagProperty() # category=None
|
||||
mytag2 = TagProperty(category="tagcategory")
|
||||
|
||||
"""
|
||||
taghandler_name = "tags"
|
||||
|
||||
def __init__(self, category=None, data=None):
|
||||
self._category = category
|
||||
self._data = data
|
||||
self._key = ""
|
||||
|
||||
def __set_name__(self, cls, name):
|
||||
"""
|
||||
Called when descriptor is first assigned to the class (not the instance!).
|
||||
It is called with the name of the field.
|
||||
|
||||
"""
|
||||
self._key = name
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""
|
||||
Called when accessing the tag as a property on the instance.
|
||||
|
||||
"""
|
||||
try:
|
||||
return getattr(instance, self.taghandler_name).get(
|
||||
key=self._key,
|
||||
category=self._category,
|
||||
return_list=False,
|
||||
raise_exception=True
|
||||
)
|
||||
except AttributeError:
|
||||
self.__set__(instance, self._category)
|
||||
|
||||
def __set__(self, instance, category):
|
||||
"""
|
||||
Assign a new category to the tag. It's not possible to set 'data' this way.
|
||||
|
||||
"""
|
||||
self._category = category
|
||||
(
|
||||
getattr(instance, self.taghandler_name).add(
|
||||
key=self._key,
|
||||
category=self._category,
|
||||
data=self._data
|
||||
)
|
||||
)
|
||||
|
||||
def __delete__(self, instance):
|
||||
"""
|
||||
Called when running `del` on the property. Will disconnect the object from
|
||||
the Tag. Note that the tag will be readded on next fetch unless the
|
||||
TagProperty is also removed in code!
|
||||
|
||||
"""
|
||||
getattr(instance, self.taghandler_name).remove(key=self._key, category=self._category)
|
||||
|
||||
|
||||
class TagHandler(object):
|
||||
"""
|
||||
|
|
@ -361,7 +430,8 @@ class TagHandler(object):
|
|||
|
||||
return ret[0] if len(ret) == 1 else ret
|
||||
|
||||
def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False):
|
||||
def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False,
|
||||
raise_exception=False):
|
||||
"""
|
||||
Get the tag for the given key, category or combination of the two.
|
||||
|
||||
|
|
@ -376,6 +446,8 @@ class TagHandler(object):
|
|||
instead of a string representation of the Tag.
|
||||
return_list (bool, optional): Always return a list, regardless
|
||||
of number of matches.
|
||||
raise_exception (bool, optional): Raise AttributeError if no matches
|
||||
are found.
|
||||
|
||||
Returns:
|
||||
tags (list): The matches, either string
|
||||
|
|
@ -383,6 +455,9 @@ class TagHandler(object):
|
|||
depending on `return_tagobj`. If 'default' is set, this
|
||||
will be a list with the default value as its only element.
|
||||
|
||||
Raises:
|
||||
AttributeError: If finding no matches and `raise_exception` is True.
|
||||
|
||||
"""
|
||||
ret = []
|
||||
for keystr in make_iter(key):
|
||||
|
|
@ -393,9 +468,14 @@ class TagHandler(object):
|
|||
for tag in self._getcache(keystr, category)
|
||||
]
|
||||
)
|
||||
if return_list:
|
||||
return ret if ret else [default] if default is not None else []
|
||||
return ret[0] if len(ret) == 1 else (ret if ret else default)
|
||||
if not ret:
|
||||
if raise_exception:
|
||||
raise AttributeError(f"No tags found matching input {key}, {category}.")
|
||||
elif return_list:
|
||||
return [default] if default is not None else []
|
||||
else:
|
||||
return default
|
||||
return ret if return_list else (ret[0] if len(ret) == 1 else ret)
|
||||
|
||||
def remove(self, key=None, category=None):
|
||||
"""
|
||||
|
|
@ -521,6 +601,21 @@ class TagHandler(object):
|
|||
return ",".join(self.all())
|
||||
|
||||
|
||||
class AliasProperty(TagProperty):
|
||||
"""
|
||||
Allows for setting aliases like Django fields:
|
||||
::
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
# note that every character will get the alias bob. Make sure
|
||||
# the alias property does not collide with an existing method
|
||||
# or property on the class.
|
||||
bob = AliasProperty()
|
||||
|
||||
"""
|
||||
taghandler_name = "aliases"
|
||||
|
||||
|
||||
class AliasHandler(TagHandler):
|
||||
"""
|
||||
A handler for the Alias Tag type.
|
||||
|
|
@ -530,6 +625,20 @@ class AliasHandler(TagHandler):
|
|||
_tagtype = "alias"
|
||||
|
||||
|
||||
class PermissionProperty(TagProperty):
|
||||
"""
|
||||
Allows for setting permissions like Django fields:
|
||||
::
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
# note that every character will get this permission! Make
|
||||
# sure it doesn't collide with an existing method or property.
|
||||
myperm = PermissionProperty()
|
||||
|
||||
"""
|
||||
taghandler_name = "permissions"
|
||||
|
||||
|
||||
class PermissionHandler(TagHandler):
|
||||
"""
|
||||
A handler for the Permission Tag type.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Unit tests for typeclass base system
|
|||
|
||||
"""
|
||||
from django.test import override_settings
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTestCase
|
||||
from evennia.typeclasses import attributes
|
||||
from mock import patch
|
||||
from parameterized import parameterized
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue