From 7a60e9ed0dfbd26712cd7479a5537a4154a63de8 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 9 Nov 2021 23:08:28 +0100 Subject: [PATCH] AttributeProperty for managing Attributes similarly to Django fields --- CHANGELOG.md | 2 + docs/source/Components/Attributes.md | 658 ++++++++++++++------- docs/source/Components/Default-Commands.md | 24 +- docs/source/Concepts/Concepts-Overview.md | 2 +- docs/source/conf.py | 1 + evennia/commands/default/general.py | 1 + evennia/commands/default/system.py | 1 + evennia/contrib/rpsystem.py | 2 + evennia/typeclasses/attributes.py | 121 +++- 9 files changed, 587 insertions(+), 225 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8da278f59..02548491ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/docs/source/Components/Attributes.md b/docs/source/Components/Attributes.md index 3c6e373cd8..7d18743a20 100644 --- a/docs/source/Components/Attributes.md +++ b/docs/source/Components/Attributes.md @@ -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 [] - 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":]`. * [Sets](https://docs.python.org/2/tutorial/datastructures.html#sets), like `{1,2,"test",}`. -* -[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", ))`. * [collections.Deque](https://docs.python.org/2/library/collections.html#collections.deque), like `deque((1,2,"test",))`. @@ -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! \ No newline at end of file diff --git a/docs/source/Components/Default-Commands.md b/docs/source/Components/Default-Commands.md index 8391304bc6..9abdeac289 100644 --- a/docs/source/Components/Default-Commands.md +++ b/docs/source/Components/Default-Commands.md @@ -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_) diff --git a/docs/source/Concepts/Concepts-Overview.md b/docs/source/Concepts/Concepts-Overview.md index 9f70130e7f..11da8aed43 100644 --- a/docs/source/Concepts/Concepts-Overview.md +++ b/docs/source/Concepts/Concepts-Overview.md @@ -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 diff --git a/docs/source/conf.py b/docs/source/conf.py index 41d8edf06d..24b7cff166 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -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 diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index e0f7d84526..0f64c86502 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -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) diff --git a/evennia/commands/default/system.py b/evennia/commands/default/system.py index fdb18a13f1..b98f22889a 100644 --- a/evennia/commands/default/system.py +++ b/evennia/commands/default/system.py @@ -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""" diff --git a/evennia/contrib/rpsystem.py b/evennia/contrib/rpsystem.py index fec2e0ac00..cad9236ff7 100644 --- a/evennia/contrib/rpsystem.py +++ b/evennia/contrib/rpsystem.py @@ -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" diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 6f70e9976d..e934e4c4e4 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -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