AttributeProperty for managing Attributes similarly to Django fields

This commit is contained in:
Griatch 2021-11-09 23:08:28 +01:00
parent 507e896e06
commit 7a60e9ed0d
9 changed files with 587 additions and 225 deletions

View file

@ -118,6 +118,8 @@ Up requirements to Django 3.2+, Twisted 21+
bool True/False if deletion was successful (like `DefaultObject.delete` before them)
- `contrib.custom_gametime` days/weeks/months now always starts from 1 (to match
the standard calendar form ... there is no month 0 every year after all).
- `AttributeProperty`/`NAttributeProperty` to allow managing Attributes/NAttributes
on typeclasses in the same way as Django fields.
### Evennia 0.9.5 (2019-2020)

View file

@ -1,229 +1,427 @@
# Attributes
```python
# in-game
> set obj/myattr = "test"
When performing actions in Evennia it is often important that you store data for later. If you write
a menu system, you have to keep track of the current location in the menu tree so that the player
can give correct subsequent commands. If you are writing a combat system, you might have a
combattant's next roll get easier dependent on if their opponent failed. Your characters will
probably need to store roleplaying-attributes like strength and agility. And so on.
[Typeclassed](./Typeclasses.md) game entities ([Accounts](./Accounts.md), [Objects](./Objects.md),
[Scripts](./Scripts.md) and [Channels](./Communications.md)) always have *Attributes* associated with them.
Attributes are used to store any type of data 'on' such entities. This is different from storing
data in properties already defined on entities (such as `key` or `location`) - these have very
specific names and require very specific types of data (for example you couldn't assign a python
*list* to the `key` property no matter how hard you tried). `Attributes` come into play when you
want to assign arbitrary data to arbitrary names.
**Attributes are _not_ secure by default and any player may be able to change them unless you
[prevent this behavior](./Attributes.md#locking-and-checking-attributes).**
## The .db and .ndb shortcuts
To save persistent data on a Typeclassed object you normally use the `db` (DataBase) operator. Let's
try to save some data to a *Rose* (an [Object](./Objects.md)):
```python
# saving
rose.db.has_thorns = True
# getting it back
is_ouch = rose.db.has_thorns
# in code
obj.db.foo = [1,2,3, "bar"]
value = obj.db.foo
obj.attributes.add("myattr", 1234, category="bar")
value = attributes.get("myattr", category="bar")
```
This looks like any normal Python assignment, but that `db` makes sure that an *Attribute* is
created behind the scenes and is stored in the database. Your rose will continue to have thorns
throughout the life of the server now, until you deliberately remove them.
_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.
To be sure to save **non-persistently**, i.e. to make sure NOT to create a database entry, you use
`ndb` (NonDataBase). It works in the same way:
- [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
but you need to know the quirks.
- [NAttributes](#in-memory-attributes-nattributes) are the in-memory, non-persistent
siblings of Attributes.
- [Managing Attributes In-game](#managing-attributes-in-game) for in-game builder commands.
```python
# saving
rose.ndb.has_thorns = True
# getting it back
is_ouch = rose.ndb.has_thorns
## Managing Attributes in Code
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
are three ways to manage Attributes, all of which can be mixed.
- [Using the `.db` property shortcut](#using-db)
- [Using the `.attributes` manager (`AttributeManager`)](#using-attributes)
- [Using `AttributeProperty` for assigning Attributes in a way similar to Django fields](#using-attributeproperty)
### Using .db
The simplest way to get/set Attributes is to use the `.db` shortcut:
```python
import evennia
obj = evennia.create_object(key="Foo")
obj.db.foo1 = 1234
obj.db.foo2 = [1, 2, 3, 4]
obj.db.weapon = "sword"
obj.db.self_reference = obj # stores a reference to the obj
# (let's assume a rose exists in-game)
rose = evennia.search_object(key="rose")[0] # returns a list, grab 0th element
rose.db.has_thorns = True
# retrieving
val1 = obj.db.foo1
val2 = obj.db.foo2
weap = obj.db.weapon
myself = obj.db.self_reference # retrieve reference from db, get object back
is_ouch = rose.db.has_thorns
# this will return None, not AttributeError!
not_found = obj.db.jiwjpowiwwerw
# returns all Attributes on the object
obj.db.all
# delete an Attribute
del obj.db.foo2
```
Trying to access a non-existing Attribute will never lead to an `AttributeError`. Instead
you will get `None` back. The special `.db.all` will return a list of all Attributes on
the object. You can replace this with your own Attribute `all` if you want, it will replace the
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):
```python
is_ouch = rose.attributes.get("has_thorns")
obj.attributes.add("helmet", "Knight's helmet")
helmet = obj.attributes.get("helmet")
# you can give space-separated Attribute-names (can't do that with .db)
obj.attributes.add("my game log", "long text about ...")
```
Technically, `ndb` has nothing to do with `Attributes`, despite how similar they look. No
`Attribute` object is created behind the scenes when using `ndb`. In fact the database is not
invoked at all since we are not interested in persistence. There is however an important reason to
use `ndb` to store data rather than to just store variables direct on entities - `ndb`-stored data
is tracked by the server and will not be purged in various cache-cleanup operations Evennia may do
while it runs. Data stored on `ndb` (as well as `db`) will also be easily listed by example the
`@examine` command.
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:
You can also `del` properties on `db` and `ndb` as normal. This will for example delete an
`Attribute`:
```python
# store (let's say we have gold_necklace and ringmail_armor from before)
obj.attributes.add("neck", gold_necklace, category="clothing")
obj.attributes.add("neck", ringmail_armor, category="armor")
```python
del rose.db.has_thorns
# retrieve later - we'll get back gold_necklace and ringmail_armor
neck_clothing = obj.attributes.get("neck", category="clothing")
neck_armor = obj.attributes.get("neck", category="armor")
```
Both `db` and `ndb` defaults to offering an `all` property on themselves. This returns all
associated attributes or non-persistent properties.
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.
```python
list_of_all_rose_attributes = rose.db.all
list_of_all_rose_ndb_attrs = rose.ndb.all
```
> When using `.db`, you will always use the `None` category.
If you use `all` as the name of an attribute, this will be used instead. Later deleting your custom
`all` will return the default behaviour.
Here are the methods of the `AttributeHandler`. See
the [AttributeHandler API](evennia.typeclasses.attributes.AttributeHandler) for more details.
## The AttributeHandler
The `.db` and `.ndb` properties are very convenient but if you don't know the name of the Attribute
beforehand they cannot be used. Behind the scenes `.db` actually accesses the `AttributeHandler`
which sits on typeclassed entities as the `.attributes` property. `.ndb` does the same for the
`.nattributes` property.
The handlers have normal access methods that allow you to manage and retrieve `Attributes` and
`NAttributes`:
- `has('attrname')` - this checks if the object has an Attribute with this key. This is equivalent
to doing `obj.db.attrname`.
- `get(...)` - this retrieves the given Attribute. Normally the `value` property of the Attribute is
returned, but the method takes keywords for returning the Attribute object itself. By supplying an
- `has(...)` - this checks if the object has an Attribute with this key. This is equivalent
to doing `obj.db.attrname` except you can also check for a specific `category.
- `get(...)` - this retrieves the given Attribute. You can also provide a `default` value to return
if the Attribute is not defined (instead of None). By supplying an
`accessing_object` to the call one can also make sure to check permissions before modifying
anything.
anything. The `raise_exception` kwarg allows you to raise an `AttributeError` instead of returning
`None` when you access a non-existing `Attribute`. The `strattr` kwarg tells the system to store
the Attribute as a raw string rather than to pickle it. While an optimization this should usually
not be used unless the Attribute is used for some particular, limited purpose.
- `add(...)` - this adds a new Attribute to the object. An optional [lockstring](./Locks.md) can be
supplied here to restrict future access and also the call itself may be checked against locks.
- `remove(...)` - Remove the given Attribute. This can optionally be made to check for permission
before performing the deletion. - `clear(...)` - removes all Attributes from object.
- `all(...)` - returns all Attributes (of the given category) attached to this object.
- `all(category=None)` - returns all Attributes (of the given category) attached to this object.
See [this section](./Attributes.md#locking-and-checking-attributes) for more about locking down Attribute
access and editing. The `Nattribute` offers no concept of access control.
Examples:
Some examples:
```python
try:
# raise error if Attribute foo does not exist
val = obj.attributes.get("foo", raise_exception=True):
except AttributeError:
# ...
# return default value if foo2 doesn't exist
val2 = obj.attributes.get("foo2", default=[1, 2, 3, "bar"])
```python
import evennia
obj = evennia.search_object("MyObject")
obj.attributes.add("test", "testvalue")
print(obj.db.test) # prints "testvalue"
print(obj.attributes.get("test")) # "
print(obj.attributes.all()) # prints [<AttributeObject>]
obj.attributes.remove("test")
# delete foo if it exists (will silently fail if unset, unless
# raise_exception is set)
obj.attributes.remove("foo")
# view all clothes on obj
all_clothes = obj.attributes.all(category="clothes")
```
### Using AttributeProperty
## Properties of Attributes
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.
An Attribute object is stored in the database. It has the following properties:
```python
# mygame/typeclasses/characters.py
- `key` - the name of the Attribute. When doing e.g. `obj.db.attrname = value`, this property is set
to `attrname`.
- `value` - this is the value of the Attribute. This value can be anything which can be pickled -
objects, lists, numbers or what have you (see
[this section](./Attributes.md#what-types-of-data-can-i-save-in-an-attribute) for more info). In the
example
`obj.db.attrname = value`, the `value` is stored here.
- `category` - this is an optional property that is set to None for most Attributes. Setting this
allows to use Attributes for different functionality. This is usually not needed unless you want
to use Attributes for very different functionality ([Nicks](./Nicks.md) is an example of using
Attributes
in this way). To modify this property you need to use the
[Attribute Handler](./Attributes.md#the-attributehandler).
- `strvalue` - this is a separate value field that only accepts strings. This severely limits the
data possible to store, but allows for easier database lookups. This property is usually not used
except when re-using Attributes for some other purpose ([Nicks](./Nicks.md) use it). It is only
accessible via the [Attribute Handler](./Attributes.md#the-attributehandler).
from evennia import DefaultCharacter
from evennia.typeclasses.attributes import AttributeProperty
There are also two special properties:
class Character(DefaultCharacter):
- `attrtype` - this is used internally by Evennia to separate [Nicks](./Nicks.md), from Attributes (Nicks
use Attributes behind the scenes).
- `model` - this is a *natural-key* describing the model this Attribute is attached to. This is on
the form *appname.modelclass*, like `objects.objectdb`. It is used by the Attribute and
NickHandler to quickly sort matches in the database. Neither this nor `attrtype` should normally
need to be modified.
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)
def at_object_creation(self):
# ...
```
Non-database attributes have no equivalence to `category` nor `strvalue`, `attrtype` or `model`.
These "Attribute-properties" will be made available to all instances of the class.
## Persistent vs non-persistent
```{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).
```
So *persistent* data means that your data will survive a server reboot, whereas with
*non-persistent* data it will not ...
```python
char = evennia.search_object(Character, key="Bob")[0] # returns list, get 0th element
... So why would you ever want to use non-persistent data? The answer is, you don't have to. Most of
the time you really want to save as much as you possibly can. Non-persistent data is potentially
useful in a few situations though.
# get defaults
strength = char.strength # will get the default value 10
- You are worried about database performance. Since Evennia caches Attributes very aggressively,
this is not an issue unless you are reading *and* writing to your Attribute very often (like many
times per second). Reading from an already cached Attribute is as fast as reading any Python
property. But even then this is not likely something to worry about: Apart from Evennia's own
caching, modern database systems themselves also cache data very efficiently for speed. Our
default
database even runs completely in RAM if possible, alleviating much of the need to write to disk
during heavy loads.
- A more valid reason for using non-persistent data is if you *want* to lose your state when logging
off. Maybe you are storing throw-away data that are re-initialized at server startup. Maybe you
are implementing some caching of your own. Or maybe you are testing a buggy [Script](./Scripts.md) that
does potentially harmful stuff to your character object. With non-persistent storage you can be
sure
that whatever is messed up, it's nothing a server reboot can't clear up.
- NAttributes have no restrictions at all on what they can store (see next section), since they
don't need to worry about being saved to the database - they work very well for temporary storage.
- You want to implement a fully or partly *non-persistent world*. Who are we to argue with your
grand vision!
# 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:
```python
# in mygame/typeclasses/objects.py
from evennia import create_object
from evennia import DefaultObject
from evennia.typeclasses.attributes import AttributeProperty
class Object(DefaultObject):
value_a = AttributeProperty(default="foo")
value_b = AttributeProperty(default="bar", autocreate=True)
obj = evennia.create_object(key="Dummy")
# these will find NO Attributes!
obj.db.value_a
obj.attributes.get("value_a")
obj.db.value_b
obj.attributes.get("value_b")
# 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:
```python
# ...
class Character(DefaultCharacter):
strength = AttributeProperty(12, autocreate=True)
agility = AttributeProperty(12, autocreate=True)
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`.
```
## Managing Attributes in-game
Attributes are mainly used by code. But one can also allow the builder to use Attributes to
'turn knobs' in-game. For example a builder could want to manually tweak the "level" Attribute of an
enemy NPC to lower its difficuly.
When setting Attributes this way, you are severely limited in what can be stored - this is because
giving players (even builders) the ability to store arbitrary Python would be a severe security
problem.
In game you can set an Attribute like this:
set myobj/foo = "bar"
To view, do
set myobj/foo
or see them together with all object-info with
examine myobj
The first `set`-example will store a new Attribute `foo` on the object `myobj` and give it the
value "bar".
You can store numbers, booleans, strings, tuples, lists and dicts this way. But if
you store a list/tuple/dict they must be proper Python structures and may _only_ contain strings
or numbers. If you try to insert an unsupported structure, the input will be converted to a
string.
set myobj/mybool = True
set myobj/mybool = True
set myobj/mytuple = (1, 2, 3, "foo")
set myobj/mylist = ["foo", "bar", 2]
set myobj/mydict = {"a": 1, "b": 2, 3: 4}
set mypobj/mystring = [1, 2, foo] # foo is invalid Python (no quotes)
For the last line you'll get a warning and the value instead will be saved as a string `"[1, 2, foo]"`.
## Locking and checking Attributes
While the `set` command is limited to builders, individual Attributes are usually not
locked down. You may want to lock certain sensitive Attributes, in particular for games
where you allow player building. You can add such limitations by adding a [lock string](./Locks.md)
to your Attribute. A NAttribute have no locks.
The relevant lock types are
- `attrread` - limits who may read the value of the Attribute
- `attredit` - limits who may set/change this Attribute
You must use the `AttributeHandler` to assign the lockstring to the Attribute:
```python
lockstring = "attread:all();attredit:perm(Admins)"
obj.attributes.add("myattr", "bar", lockstring=lockstring)"
```
If you already have an Attribute and want to add a lock in-place you can do so
by having the `AttributeHandler` return the `Attribute` object itself (rather than
its value) and then assign the lock to it directly:
```python
lockstring = "attread:all();attredit:perm(Admins)"
obj.attributes.get("myattr", return_obj=True).locks.add(lockstring)
```
Note the `return_obj` keyword which makes sure to return the `Attribute` object so its LockHandler
could be accessed.
A lock is no good if nothing checks it -- and by default Evennia does not check locks on Attributes.
To check the `lockstring` you provided, make sure you include `accessing_obj` and set
`default_access=False` as you make a `get` call.
```python
# in some command code where we want to limit
# setting of a given attribute name on an object
attr = obj.attributes.get(attrname,
return_obj=True,
accessing_obj=caller,
default=None,
default_access=False)
if not attr:
caller.msg("You cannot edit that Attribute!")
return
# edit the Attribute here
```
The same keywords are available to use with `obj.attributes.set()` and `obj.attributes.remove()`,
those will check for the `attredit` lock type.
## What types of data can I save in an Attribute?
> None of the following affects NAttributes, which does not invoke the database at all. There are no
> restrictions to what can be stored in a NAttribute.
The database doesn't know anything about Python objects, so Evennia must *serialize* Attribute
values into a string representation in order to store it to the database. This is done using the
`pickle` module of Python (the only exception is if you use the `strattr` keyword of the
AttributeHandler to save to the `strvalue` field of the Attribute. In that case you can only save
*strings* which will not be pickled).
values into a string representation before storing it to the database. This is done using the
[pickle](https://docs.python.org/library/pickle.html) module of Python.
It's important to note that when you access the data in an Attribute you are *always* de-serializing
it from the database representation every time. This is because we allow for storing
database-entities in Attributes too. If we cached it as its Python form, we might end up with
situations where the database entity was deleted since we last accessed the Attribute.
De-serializing data with a database-entity in it means querying the database for that object and
making sure it still exists (otherwise it will be set to `None`). Performance-wise this is usually
not a big deal. But if you are accessing the Attribute as part of some big loop or doing a large
amount of reads/writes you should first extract it to a temporary variable, operate on *that* and
then save the result back to the Attribute. If you are storing a more complex structure like a
`dict` or a `list` you should make sure to "disconnect" it from the database before looping over it,
as mentioned in the [Retrieving Mutable Objects](./Attributes.md#retrieving-mutable-objects) section
below.
> The only exception is if you use the `strattr` keyword of the
`AttributeHandler` to save to the `strvalue` field of the Attribute. In that case you can _only_ save
*strings* and those will not be pickled).
### Storing single objects
With a single object, we mean anything that is *not iterable*, like numbers, strings or custom class
instances without the `__iter__` method.
* You can generally store any non-iterable Python entity that can be
[pickled](https://docs.python.org/library/pickle.html).
* Single database objects/typeclasses can be stored as any other in the Attribute. These can
normally *not* be pickled, but Evennia will behind the scenes convert them to an internal
representation using their classname, database-id and creation-date with a microsecond precision,
guaranteeing you get the same object back when you access the Attribute later.
* If you *hide* a database object inside a non-iterable custom class (like stored as a variable
inside it), Evennia will not know it's there and won't convert it safely. Storing classes with
such hidden database objects is *not* supported and will lead to errors!
* You can generally store any non-iterable Python entity that can be pickled.
* Single database objects/typeclasses can be stored, despite them normally not being possible
to pickle. Evennia wil convert them to an internal representation using their classname,
database-id and creation-date with a microsecond precision. When retrieving, the object
instance will be re-fetched from the database using this information.
* To convert the database object, Evennia must know it's there. If you *hide* a database object
inside a non-iterable class, you will run into errors - this is not supported!
```{code-block} python
:caption: Valid assignments
```python
# Examples of valid single-value attribute data:
obj.db.test1 = 23
obj.db.test1 = False
# a database object (will be stored as an internal representation)
obj.db.test2 = myobj
```
```{code-block} python
:caption: Invalid, 'hidden' dbobject
# example of an invalid, "hidden" dbobject
class Invalid(object):
def __init__(self, dbobj):
# no way for Evennia to know this is a dbobj
self.dbobj = dbobj
invalid = Invalid(myobj)
obj.db.invalid = invalid # will cause error!
class Container:
def __init__(self, mydbobj):
# no way for Evennia to know this is a database object!
self.mydbobj = mydbobj
container = Container(myobj)
obj.db.invalid = container # will cause error!
```
### Storing multiple objects
@ -237,8 +435,7 @@ entities you can loop over in a for-loop. Attribute-saving supports the followin
* [Dicts](https://docs.python.org/2/tutorial/datastructures.html#dictionaries), like `{1:2,
"test":<dbobj>]`.
* [Sets](https://docs.python.org/2/tutorial/datastructures.html#sets), like `{1,2,"test",<dbobj>}`.
*
[collections.OrderedDict](https://docs.python.org/2/library/collections.html#collections.OrderedDict),
* [collections.OrderedDict](https://docs.python.org/2/library/collections.html#collections.OrderedDict),
like `OrderedDict((1,2), ("test", <dbobj>))`.
* [collections.Deque](https://docs.python.org/2/library/collections.html#collections.deque), like
`deque((1,2,"test",<dbobj>))`.
@ -319,7 +516,6 @@ function `evennia.utils.dbserialize.deserialize`:
from evennia.utils.dbserialize import deserialize
decoupled_mutables = deserialize(nested_mutables)
```
The result of this operation will be a structure only consisting of normal Python mutables (`list`
@ -343,53 +539,95 @@ already disconnected from the database from the onset.
# without affecting the database.
```
> Attributes will fetch data fresh from the database whenever you read them, so
> if you are performing big operations on a mutable Attribute property (such as looping over a list
> or dict) you should make sure to "disconnect" the Attribute's value first and operate on this
> rather than on the Attribute. You can gain dramatic speed improvements to big loops this
> way.
## Properties of Attributes
An `Attribute` object is stored in the database. It has the following properties:
## Locking and checking Attributes
- `key` - the name of the Attribute. When doing e.g. `obj.db.attrname = value`, this property is set
to `attrname`.
- `value` - this is the value of the Attribute. This value can be anything which can be pickled -
objects, lists, numbers or what have you (see
[this section](./Attributes.md#what-types-of-data-can-i-save-in-an-attribute) for more info). In the
example
`obj.db.attrname = value`, the `value` is stored here.
- `category` - this is an optional property that is set to None for most Attributes. Setting this
allows to use Attributes for different functionality. This is usually not needed unless you want
to use Attributes for very different functionality ([Nicks](./Nicks.md) is an example of using
Attributes in this way). To modify this property you need to use the [Attribute Handler](#attributes)
- `strvalue` - this is a separate value field that only accepts strings. This severely limits the
data possible to store, but allows for easier database lookups. This property is usually not used
except when re-using Attributes for some other purpose ([Nicks](./Nicks.md) use it). It is only
accessible via the [Attribute Handler](#attributes).
Attributes are normally not locked down by default, but you can easily change that for individual
Attributes (like those that may be game-sensitive in games with user-level building).
There are also two special properties:
First you need to set a *lock string* on your Attribute. Lock strings are specified [Locks](./Locks.md).
The relevant lock types are
- `attrtype` - this is used internally by Evennia to separate [Nicks](./Nicks.md), from Attributes (Nicks
use Attributes behind the scenes).
- `model` - this is a *natural-key* describing the model this Attribute is attached to. This is on
the form *appname.modelclass*, like `objects.objectdb`. It is used by the Attribute and
NickHandler to quickly sort matches in the database. Neither this nor `attrtype` should normally
need to be modified.
- `attrread` - limits who may read the value of the Attribute
- `attredit` - limits who may set/change this Attribute
Non-database attributes are not stored in the database and have no equivalence
to `category` nor `strvalue`, `attrtype` or `model`.
You cannot use the `db` handler to modify Attribute object (such as setting a lock on them) - The
`db` handler will return the Attribute's *value*, not the Attribute object itself. Instead you use
the AttributeHandler and set it to return the object instead of the value:
# In-memory Attributes (NAttributes)
_NAttributes_ (short of Non-database Attributes) mimic Attributes in most things except they
are **non-persistent** - they will _not_ survive a server reload.
- Instead of `.db` use `.ndb`.
- Instead of `.attributes` use `.nattributes`
- Instead of `AttributeProperty`, use `NAttributeProperty`.
```python
lockstring = "attread:all();attredit:perm(Admins)"
obj.attributes.get("myattr", return_obj=True).locks.add(lockstring)
rose.ndb.has_thorns = True
is_ouch = rose.ndb.has_thorns
rose.nattributes.add("has_thorns", True)
is_ouch = rose.nattributes.get("has_thorns")
```
Note the `return_obj` keyword which makes sure to return the `Attribute` object so its LockHandler
could be accessed.
Differences between `Attributes` and `NAttributes`:
A lock is no good if nothing checks it -- and by default Evennia does not check locks on Attributes.
You have to add a check to your commands/code wherever it fits (such as before setting an
Attribute).
- `NAttribute`s are always wiped on a server reload.
- They only exist in memory and never involve the database at all, making them faster to
access and edit than `Attribute`s.
- `NAttribute`s can store _any_ Python structure (and database object) without limit.
- They can _not_ be set with the standard `set` command (but they are visible with `examine`)
```python
# in some command code where we want to limit
# setting of a given attribute name on an object
attr = obj.attributes.get(attrname,
return_obj=True,
accessing_obj=caller,
default=None,
default_access=False)
if not attr:
caller.msg("You cannot edit that Attribute!")
return
# edit the Attribute here
```
There are some important reasons we recommend using `ndb` to store temporary data rather than
the simple alternative of just storing a variable directly on an object:
The same keywords are available to use with `obj.attributes.set()` and `obj.attributes.remove()`,
those will check for the `attredit` lock type.
- NAttributes are tracked by Evennia and will not be purged in various cache-cleanup operations
the server may do. So using them guarantees that they'll remain available at least as long as
the server lives.
- It's a consistent style - `.db/.attributes` and `.ndb/.nattributes` makes for clean-looking code
where it's clear how long-lived (or not) your data is to be.
### Persistent vs non-persistent
So *persistent* data means that your data will survive a server reboot, whereas with
*non-persistent* data it will not ...
... So why would you ever want to use non-persistent data? The answer is, you don't have to. Most of
the time you really want to save as much as you possibly can. Non-persistent data is potentially
useful in a few situations though.
- You are worried about database performance. Since Evennia caches Attributes very aggressively,
this is not an issue unless you are reading *and* writing to your Attribute very often (like many
times per second). Reading from an already cached Attribute is as fast as reading any Python
property. But even then this is not likely something to worry about: Apart from Evennia's own
caching, modern database systems themselves also cache data very efficiently for speed. Our
default
database even runs completely in RAM if possible, alleviating much of the need to write to disk
during heavy loads.
- A more valid reason for using non-persistent data is if you *want* to lose your state when logging
off. Maybe you are storing throw-away data that are re-initialized at server startup. Maybe you
are implementing some caching of your own. Or maybe you are testing a buggy [Script](./Scripts.md) that
does potentially harmful stuff to your character object. With non-persistent storage you can be
sure that whatever is messed up, it's nothing a server reboot can't clear up.
- `NAttribute`s have no restrictions at all on what they can store, since they
don't need to worry about being saved to the database - they work very well for temporary storage.
- You want to implement a fully or partly *non-persistent world*. Who are we to argue with your
grand vision!

View file

@ -14,9 +14,9 @@ with [EvEditor](./EvEditor.md), flipping pages in [EvMore](./EvMore.md) or using
[Batch-Processor](./Batch-Processors.md)'s interactive mode.
```
- [**__unloggedin_look_command** [look, l]](evennia.commands.default.unloggedin.CmdUnconnectedLook) (cmdset: [UnloggedinCmdSet](evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet), help-category: _General_)
- [**__unloggedin_look_command** [l, look]](evennia.commands.default.unloggedin.CmdUnconnectedLook) (cmdset: [UnloggedinCmdSet](evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet), help-category: _General_)
- [**about** [version]](evennia.commands.default.system.CmdAbout) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_)
- [**access** [groups, hierarchy]](evennia.commands.default.general.CmdAccess) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**access** [hierarchy, groups]](evennia.commands.default.general.CmdAccess) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**accounts** [listaccounts, account]](evennia.commands.default.system.CmdAccounts) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_)
- [**addcom** [aliaschan, chanalias]](evennia.commands.default.comms.CmdAddCom) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_)
- [**alias** [setobjalias]](evennia.commands.default.building.CmdSetObjAlias) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
@ -33,7 +33,7 @@ with [EvEditor](./EvEditor.md), flipping pages in [EvMore](./EvMore.md) or using
- [**clock**](evennia.commands.default.comms.CmdClock) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_)
- [**cmdsets** [listcmsets]](evennia.commands.default.building.CmdListCmdSets) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**color**](evennia.commands.default.account.CmdColorTest) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_)
- [**connect** [conn, con, co]](evennia.commands.default.unloggedin.CmdUnconnectedConnect) (cmdset: [UnloggedinCmdSet](evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet), help-category: _General_)
- [**connect** [conn, co, con]](evennia.commands.default.unloggedin.CmdUnconnectedConnect) (cmdset: [UnloggedinCmdSet](evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet), help-category: _General_)
- [**copy**](evennia.commands.default.building.CmdCopy) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**cpattr**](evennia.commands.default.building.CmdCpAttr) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**create**](evennia.commands.default.building.CmdCreate) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
@ -41,7 +41,7 @@ with [EvEditor](./EvEditor.md), flipping pages in [EvMore](./EvMore.md) or using
- [**cwho**](evennia.commands.default.comms.CmdCWho) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_)
- [**delcom** [delchanalias, delaliaschan]](evennia.commands.default.comms.CmdDelCom) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_)
- [**desc** [describe]](evennia.commands.default.building.CmdDesc) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**destroy** [delete, del]](evennia.commands.default.building.CmdDestroy) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**destroy** [del, delete]](evennia.commands.default.building.CmdDestroy) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**dig**](evennia.commands.default.building.CmdDig) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**drop**](evennia.commands.default.general.CmdDrop) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**encoding** [encode]](evennia.commands.default.unloggedin.CmdUnconnectedEncoding) (cmdset: [UnloggedinCmdSet](evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet), help-category: _General_)
@ -55,23 +55,23 @@ with [EvEditor](./EvEditor.md), flipping pages in [EvMore](./EvMore.md) or using
- [**home**](evennia.commands.default.general.CmdHome) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**ic** [puppet]](evennia.commands.default.account.CmdIC) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_)
- [**info**](evennia.commands.default.unloggedin.CmdUnconnectedInfo) (cmdset: [UnloggedinCmdSet](evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet), help-category: _General_)
- [**inventory** [inv, i]](evennia.commands.default.general.CmdInventory) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**inventory** [i, inv]](evennia.commands.default.general.CmdInventory) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**irc2chan**](evennia.commands.default.comms.CmdIRC2Chan) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_)
- [**ircstatus**](evennia.commands.default.comms.CmdIRCStatus) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_)
- [**link**](evennia.commands.default.building.CmdLink) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**lock** [locks]](evennia.commands.default.building.CmdLock) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**look** [l, ls]](evennia.commands.default.account.CmdOOCLook) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_)
- [**look** [l, ls]](evennia.commands.default.general.CmdLook) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**look** [ls, l]](evennia.commands.default.account.CmdOOCLook) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_)
- [**look** [ls, l]](evennia.commands.default.general.CmdLook) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**mvattr**](evennia.commands.default.building.CmdMvAttr) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**name** [rename]](evennia.commands.default.building.CmdName) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**nick** [nickname, nicks]](evennia.commands.default.general.CmdNick) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_)
- [**objects** [db, stats, listobjs, listobjects]](evennia.commands.default.building.CmdObjects) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_)
- [**nick** [nicks, nickname]](evennia.commands.default.general.CmdNick) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_)
- [**objects** [listobjs, stats, db, listobjects]](evennia.commands.default.building.CmdObjects) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_)
- [**ooc** [unpuppet]](evennia.commands.default.account.CmdOOC) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_)
- [**open**](evennia.commands.default.building.CmdOpen) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**option** [options]](evennia.commands.default.account.CmdOption) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_)
- [**page** [tell]](evennia.commands.default.comms.CmdPage) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_)
- [**password**](evennia.commands.default.account.CmdPassword) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_)
- [**pose** [emote, :]](evennia.commands.default.general.CmdPose) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**pose** [:, emote]](evennia.commands.default.general.CmdPose) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**py** [!]](evennia.commands.default.system.CmdPy) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _System_)
- [**quell** [unquell]](evennia.commands.default.account.CmdQuell) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_)
- [**quit**](evennia.commands.default.account.CmdQuit) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_)
@ -79,7 +79,7 @@ with [EvEditor](./EvEditor.md), flipping pages in [EvMore](./EvMore.md) or using
- [**reload** [restart]](evennia.commands.default.system.CmdReload) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _System_)
- [**reset** [reboot]](evennia.commands.default.system.CmdReset) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _System_)
- [**rss2chan**](evennia.commands.default.comms.CmdRSS2Chan) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _Comms_)
- [**say** [', "]](evennia.commands.default.general.CmdSay) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**say** [", ']](evennia.commands.default.general.CmdSay) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**screenreader**](evennia.commands.default.unloggedin.CmdUnconnectedScreenreader) (cmdset: [UnloggedinCmdSet](evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet), help-category: _General_)
- [**scripts** [script]](evennia.commands.default.building.CmdScripts) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_)
- [**server** [serverload, serverprocess]](evennia.commands.default.system.CmdServerLoad) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_)
@ -98,7 +98,7 @@ with [EvEditor](./EvEditor.md), flipping pages in [EvMore](./EvMore.md) or using
- [**tickers**](evennia.commands.default.system.CmdTickers) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_)
- [**time** [uptime]](evennia.commands.default.system.CmdTime) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _System_)
- [**tunnel** [tun]](evennia.commands.default.building.CmdTunnel) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**typeclass** [parent, swap, type, typeclasses, update]](evennia.commands.default.building.CmdTypeclass) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**typeclass** [update, parent, type, typeclasses, swap]](evennia.commands.default.building.CmdTypeclass) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**unlink**](evennia.commands.default.building.CmdUnLink) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _Building_)
- [**whisper**](evennia.commands.default.general.CmdWhisper) (cmdset: [CharacterCmdSet](evennia.commands.default.cmdset_character.CharacterCmdSet), help-category: _General_)
- [**who** [doing]](evennia.commands.default.account.CmdWho) (cmdset: [AccountCmdSet](evennia.commands.default.cmdset_account.AccountCmdSet), help-category: _General_)

View file

@ -24,7 +24,7 @@ This documentation cover more over-arching concepts of Evennia, often involving
- [Change the language of the server](./Internationalization.md)
- [Server text-encoding](./Text-Encodings.md)
- [Text tags](./TextTags.md)
- [Change Messages Per receiver](Change-Messages-Per-Receiver.md)
- [Change Messages Per receiver](./Change-Messages-Per-Receiver.md)
## Web features

View file

@ -131,6 +131,7 @@ myst_substitution = {
# add anchors to h1, h2, ... level headings
myst_heading_anchors = 4
suppress_warnings = ["myst.ref"]
# reroute to github links or to the api

View file

@ -673,6 +673,7 @@ class CmdPose(COMMAND_DEFAULT_CLASS):
key = "pose"
aliases = [":", "emote"]
locks = "cmd:all()"
arg_regex = ""
# we want to be able to pose without whitespace between
# the command/alias and the pose (e.g. :pose)

View file

@ -350,6 +350,7 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
switch_options = ("time", "edit", "clientraw", "noecho")
locks = "cmd:perm(py) or perm(Developer)"
help_category = "System"
arg_regex = ""
def func(self):
"""hook function"""

View file

@ -932,6 +932,7 @@ class CmdEmote(RPCommand): # replaces the main emote
key = "emote"
aliases = [":"]
locks = "cmd:all()"
arg_regex = ""
def func(self):
"Perform the emote."
@ -959,6 +960,7 @@ class CmdSay(RPCommand): # replaces standard say
key = "say"
aliases = ['"', "'"]
locks = "cmd:all()"
arg_regex = ""
def func(self):
"Run the say command"

View file

@ -159,6 +159,123 @@ class InMemoryAttribute(IAttribute):
value = property(__value_get, __value_set, __value_del)
class AttributeProperty:
"""
Attribute property descriptor. Allows for specifying Attributes as Django-like 'fields'
on the class level. Note that while one can set a lock on the Attribute,
there is no way to *check* said lock when accessing via the property - use
the full AttributeHandler if you need to do access checks.
Example:
::
class Character(DefaultCharacter):
foo = AttributeProperty(default="Bar")
"""
attrhandler_name = "attributes"
def __init__(self, default=None, category=None, strattr=False, lockstring="",
autocreate=False):
"""
Initialize an Attribute as a property descriptor.
Keyword Args:
default (any): A default value if the attr is not set.
category (str): The attribute's category. If unset, use class default.
strattr (bool): If set, this Attribute *must* be a simple string, and will be
stored more efficiently.
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`).
"""
self._default = default
self._category = category
self._strattr = strattr
self._lockstring = lockstring
self._autocreate = autocreate
self._key = ""
def __set_name__(self, cls, name):
"""
Called when descriptor is first assigned to the class. It is called with
the name of the field.
"""
self._key = name
def __get__(self, instance, owner):
"""
Called when the attrkey is retrieved from the instance.
"""
value = self._default
try:
value = (
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
def __set__(self, instance, value):
"""
Called when assigning to the property (and when auto-creating an Attribute).
"""
(
getattr(instance, self.attrhandler_name)
.add(self._key,
value,
category=self._category,
lockstring=self._lockstring,
strattr=self._strattr)
)
def __delete__(self, instance):
"""
Called when running `del` on the field. Will remove/clear the Attribute.
"""
(
getattr(instance, self.attrhandler_name)
.remove(key=self._key,
category=self._category)
)
class NAttributeProperty(AttributeProperty):
"""
NAttribute property descriptor. Allows for specifying NAttributes as Django-like 'fields'
on the class level.
Example:
::
class Character(DefaultCharacter):
foo = NAttributeProperty(default="Bar")
"""
attrhandler_name = "nattributes"
class Attribute(IAttribute, SharedMemoryModel):
"""
This attribute is stored via Django. Most Attributes will be using this class.
@ -980,11 +1097,11 @@ class AttributeHandler:
key (str or list, optional): the attribute identifier or
multiple attributes to get. if a list of keys, the
method will return a list.
category (str, optional): the category within which to
retrieve attribute(s).
default (any, optional): The value to return if an
Attribute was not defined. If set, it will be returned in
a one-item list.
category (str, optional): the category within which to
retrieve attribute(s).
return_obj (bool, optional): If set, the return is not the value of the
Attribute but the Attribute object itself.
strattr (bool, optional): Return the `strvalue` field of