mirror of
https://github.com/evennia/evennia.git
synced 2026-03-22 15:56:30 +01:00
Fix merge conflicts
This commit is contained in:
commit
1290f648d5
20 changed files with 769 additions and 761 deletions
|
|
@ -166,6 +166,12 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
|||
- New `at_pre_object_receive(obj, source_location)` method on Objects. Called on
|
||||
destination, mimicking behavior of `at_pre_move` hook - returning False will abort move.
|
||||
- New `at_pre_object_leave(obj, destination)` method on Objects. Called on
|
||||
- The db pickle-serializer now checks for methods `__serialize_dbobjs__` and `__deserialize_dbobjs__`
|
||||
to allow custom packing/unpacking of nested dbobjs, to allow storing in Attribute.
|
||||
- Optimizations to rpsystem contrib performance. Breaking change: `.get_sdesc()` will
|
||||
now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal)
|
||||
- Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal)
|
||||
- Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal)
|
||||
|
||||
|
||||
## Evennia 0.9.5
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ or in the chat.
|
|||
|
||||
[pep8]: http://www.python.org/dev/peps/pep-0008
|
||||
[pep8tool]: https://pypi.python.org/pypi/pep8
|
||||
[googlestyle]: http://www.sphinx-doc.org/en/stable/ext/example_google.html
|
||||
[googlestyle]: https://www.sphinx-doc.org/en/master/usage/extensions/example_google.html
|
||||
[githubmarkdown]: https://help.github.com/articles/github-flavored-markdown/
|
||||
[markdown-hilight]: https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting
|
||||
[command-docstrings]: https://github.com/evennia/evennia/wiki/Using%20MUX%20As%20a%20Standard#documentation-policy
|
||||
|
|
|
|||
|
|
@ -152,6 +152,8 @@ mv-local:
|
|||
@echo "Documentation built (multiversion + autodocs)."
|
||||
@echo "To see result, open evennia/docs/build/html/<version>/index.html in a browser."
|
||||
|
||||
# note - don't run the following manually, the result will clash with the result
|
||||
# of the github actions!
|
||||
deploy:
|
||||
make _multiversion-deploy
|
||||
@echo "Documentation deployed."
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
```{code-block}
|
||||
:caption: In-game
|
||||
> set obj/myattr = "test"
|
||||
```
|
||||
```
|
||||
```{code-block} python
|
||||
:caption: In-code, using the .db wrapper
|
||||
obj.db.foo = [1, 2, 3, "bar"]
|
||||
|
|
@ -16,8 +16,8 @@ value = attributes.get("myattr", category="bar")
|
|||
```
|
||||
```{code-block} python
|
||||
:caption: In-code, using `AttributeProperty` at class level
|
||||
from evennia import DefaultObject
|
||||
from evennia import AttributeProperty
|
||||
from evennia import DefaultObject
|
||||
from evennia import AttributeProperty
|
||||
|
||||
class MyObject(DefaultObject):
|
||||
foo = AttributeProperty(default=[1, 2, 3, "bar"])
|
||||
|
|
@ -25,20 +25,20 @@ class MyObject(DefaultObject):
|
|||
|
||||
```
|
||||
|
||||
_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
|
||||
_Attributes_ allow you to to store arbitrary data on objects and make sure the data survives a server reboot. An Attribute can store pretty much any
|
||||
Python data structure and data type, like numbers, strings, lists, dicts etc. You can also
|
||||
store (references to) database objects like characters and rooms.
|
||||
|
||||
- [What can be stored in an Attribute](#what-types-of-data-can-i-save-in-an-attribute) is a must-read to avoid being surprised, also for experienced developers. Attributes can store _almost_ everything
|
||||
but you need to know the quirks.
|
||||
- [NAttributes](#in-memory-attributes-nattributes) are the in-memory, non-persistent
|
||||
- [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.
|
||||
|
||||
## Managing Attributes in Code
|
||||
## 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
|
||||
Attributes are usually handled in code. All [Typeclassed](./Typeclasses.md) entities
|
||||
([Accounts](./Accounts.md), [Objects](./Objects.md), [Scripts](./Scripts.md) and
|
||||
[Channels](./Channels.md)) can (and usually do) have Attributes associated with them. There
|
||||
are three ways to manage Attributes, all of which can be mixed.
|
||||
|
||||
|
|
@ -50,8 +50,8 @@ are three ways to manage Attributes, all of which can be mixed.
|
|||
|
||||
The simplest way to get/set Attributes is to use the `.db` shortcut. This allows for setting and getting Attributes that lack a _category_ (having category `None`)
|
||||
|
||||
```python
|
||||
import evennia
|
||||
```python
|
||||
import evennia
|
||||
|
||||
obj = evennia.create_object(key="Foo")
|
||||
|
||||
|
|
@ -64,10 +64,10 @@ obj.db.self_reference = obj # stores a reference to the obj
|
|||
rose = evennia.search_object(key="rose")[0] # returns a list, grab 0th element
|
||||
rose.db.has_thorns = True
|
||||
|
||||
# retrieving
|
||||
# retrieving
|
||||
val1 = obj.db.foo1
|
||||
val2 = obj.db.foo2
|
||||
weap = obj.db.weapon
|
||||
weap = obj.db.weapon
|
||||
myself = obj.db.self_reference # retrieve reference from db, get object back
|
||||
|
||||
is_ouch = rose.db.has_thorns
|
||||
|
|
@ -75,25 +75,25 @@ 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
|
||||
# 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
|
||||
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 want to group your Attribute in a category, or don't know the name of the Attribute beforehand, you can make use of
|
||||
the [AttributeHandler](evennia.typeclasses.attributes.AttributeHandler), available as `.attributes` on all typeclassed entities. With no extra keywords, this is identical to using the `.db` shortcut (`.db` is actually using the `AttributeHandler` internally):
|
||||
If you want to group your Attribute in a category, or don't know the name of the Attribute beforehand, you can make use of
|
||||
the [AttributeHandler](evennia.typeclasses.attributes.AttributeHandler), available as `.attributes` on all typeclassed entities. With no extra keywords, this is identical to using the `.db` shortcut (`.db` is actually using the `AttributeHandler` internally):
|
||||
|
||||
```python
|
||||
is_ouch = rose.attributes.get("has_thorns")
|
||||
|
||||
```python
|
||||
is_ouch = rose.attributes.get("has_thorns")
|
||||
|
||||
obj.attributes.add("helmet", "Knight's helmet")
|
||||
helmet = obj.attributes.get("helmet")
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ obj.attributes.add("my game log", "long text about ...")
|
|||
|
||||
By using a category you can separate same-named Attributes on the same object to help organization.
|
||||
|
||||
```python
|
||||
```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")
|
||||
|
|
@ -113,19 +113,19 @@ neck_clothing = obj.attributes.get("neck", category="clothing")
|
|||
neck_armor = obj.attributes.get("neck", category="armor")
|
||||
```
|
||||
|
||||
If you don't specify a category, the Attribute's `category` will be `None` and can thus also be found via `.db`. `None` is considered a category of its own, so you won't find `None`-category Attributes mixed with Attributes having categories.
|
||||
If you don't specify a category, the Attribute's `category` will be `None` and can thus also be found via `.db`. `None` is considered a category of its own, so you won't find `None`-category Attributes mixed with Attributes having categories.
|
||||
|
||||
Here are the methods of the `AttributeHandler`. See
|
||||
Here are the methods of the `AttributeHandler`. See
|
||||
the [AttributeHandler API](evennia.typeclasses.attributes.AttributeHandler) for more details.
|
||||
|
||||
- `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
|
||||
- `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. 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
|
||||
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.
|
||||
|
|
@ -135,30 +135,30 @@ the [AttributeHandler API](evennia.typeclasses.attributes.AttributeHandler) for
|
|||
|
||||
Examples:
|
||||
|
||||
```python
|
||||
```python
|
||||
try:
|
||||
# raise error if Attribute foo does not exist
|
||||
# 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"])
|
||||
val2 = obj.attributes.get("foo2", default=[1, 2, 3, "bar"])
|
||||
|
||||
# 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")
|
||||
all_clothes = obj.attributes.all(category="clothes")
|
||||
```
|
||||
|
||||
### Using AttributeProperty
|
||||
### Using AttributeProperty
|
||||
|
||||
The third way to set up an Attribute is to use an `AttributeProperty`. This
|
||||
The third way to set up an Attribute is to use an `AttributeProperty`. This
|
||||
is done on the _class level_ of your typeclass and allows you to treat Attributes a bit like Django database Fields. Unlike using `.db` and `.attributes`, an `AttributeProperty` can't be created on the fly, you must assign it in the class code.
|
||||
|
||||
```python
|
||||
```python
|
||||
# mygame/typeclasses/characters.py
|
||||
|
||||
from evennia import DefaultCharacter
|
||||
|
|
@ -173,16 +173,16 @@ class Character(DefaultCharacter):
|
|||
|
||||
sleepy = AttributeProperty(False, autocreate=False)
|
||||
poisoned = AttributeProperty(False, autocreate=False)
|
||||
|
||||
def at_object_creation(self):
|
||||
# ...
|
||||
```
|
||||
|
||||
def at_object_creation(self):
|
||||
# ...
|
||||
```
|
||||
|
||||
When a new instance of the class is created, new `Attributes` will be created with the value and category given.
|
||||
|
||||
With `AttributeProperty`'s set up like this, one can access the underlying `Attribute` like a regular property on the created object:
|
||||
With `AttributeProperty`'s set up like this, one can access the underlying `Attribute` like a regular property on the created object:
|
||||
|
||||
```python
|
||||
```python
|
||||
char = create_object(Character)
|
||||
|
||||
char.strength # returns 10
|
||||
|
|
@ -195,15 +195,15 @@ char.db.sleepy # returns None because autocreate=False (see below)
|
|||
|
||||
```
|
||||
|
||||
```{warning}
|
||||
```{warning}
|
||||
Be careful to not assign AttributeProperty's to names of properties and methods already existing on the class, like 'key' or 'at_object_creation'. That could lead to very confusing errors.
|
||||
```
|
||||
|
||||
The `autocreate=False` (default is `True`) used for `sleepy` and `poisoned` is worth a closer explanation. When `False`, _no_ Attribute will be auto-created for these AttributProperties unless they are _explicitly_ set.
|
||||
The advantage of not creating an Attribute is that the default value given to `AttributeProperty` is returned with no database access unless you change it. This also means that if you want to change the default later, all entities previously create will inherit the new default.
|
||||
The drawback is that without a database precense you can't find the Attribute via `.db` and `.attributes.get` (or by querying for it in other ways in the database):
|
||||
The `autocreate=False` (default is `True`) used for `sleepy` and `poisoned` is worth a closer explanation. When `False`, _no_ Attribute will be auto-created for these AttributProperties unless they are _explicitly_ set.
|
||||
The advantage of not creating an Attribute is that the default value given to `AttributeProperty` is returned with no database access unless you change it. This also means that if you want to change the default later, all entities previously create will inherit the new default.
|
||||
The drawback is that without a database precense you can't find the Attribute via `.db` and `.attributes.get` (or by querying for it in other ways in the database):
|
||||
|
||||
```python
|
||||
```python
|
||||
char.sleepy # returns False, no db access
|
||||
|
||||
char.db.sleepy # returns None - no Attribute exists
|
||||
|
|
@ -217,39 +217,39 @@ char.sleepy # now returns True, involves db access
|
|||
|
||||
```
|
||||
|
||||
You can e.g. `del char.strength` to set the value back to the default (the value defined
|
||||
in the `AttributeProperty`).
|
||||
You can e.g. `del char.strength` to set the value back to the default (the value defined
|
||||
in the `AttributeProperty`).
|
||||
|
||||
See the [AttributeProperty API](evennia.typeclasses.attributes.AttributeProperty) for more details on how to create it with special options, like giving access-restrictions.
|
||||
|
||||
|
||||
## Managing Attributes in-game
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
problem.
|
||||
|
||||
In game you can set an Attribute like this:
|
||||
In game you can set an Attribute like this:
|
||||
|
||||
set myobj/foo = "bar"
|
||||
|
||||
To view, do
|
||||
To view, do
|
||||
|
||||
set myobj/foo
|
||||
set myobj/foo
|
||||
|
||||
or see them together with all object-info with
|
||||
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
|
||||
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 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
|
||||
or numbers. If you try to insert an unsupported structure, the input will be converted to a
|
||||
string.
|
||||
|
||||
set myobj/mybool = True
|
||||
|
|
@ -263,8 +263,8 @@ For the last line you'll get a warning and the value instead will be saved as a
|
|||
|
||||
## 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
|
||||
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.
|
||||
|
||||
|
|
@ -273,7 +273,7 @@ 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:
|
||||
You must use the `AttributeHandler` to assign the lockstring to the Attribute:
|
||||
|
||||
```python
|
||||
lockstring = "attread:all();attredit:perm(Admins)"
|
||||
|
|
@ -281,7 +281,7 @@ 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
|
||||
by having the `AttributeHandler` return the `Attribute` object itself (rather than
|
||||
its value) and then assign the lock to it directly:
|
||||
|
||||
```python
|
||||
|
|
@ -293,8 +293,8 @@ Note the `return_obj` keyword which makes sure to return the `Attribute` object
|
|||
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.
|
||||
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
|
||||
|
|
@ -328,13 +328,13 @@ values into a string representation before storing it to the database. This is d
|
|||
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.
|
||||
* 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
|
||||
* 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 will convert them to an internal representation using theihr 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!
|
||||
* If you 'hide' a db-obj as a property on a custom class, Evennia will not be
|
||||
able to find it to serialize it. For that you need to help it out (see below).
|
||||
|
||||
```{code-block} python
|
||||
:caption: Valid assignments
|
||||
|
|
@ -345,16 +345,55 @@ obj.db.test1 = False
|
|||
# a database object (will be stored as an internal representation)
|
||||
obj.db.test2 = myobj
|
||||
```
|
||||
|
||||
As mentioned, Evennia will not be able to automatically serialize db-objects
|
||||
'hidden' in arbitrary properties on an object. This will lead to an error
|
||||
when saving the Attribute.
|
||||
|
||||
```{code-block} python
|
||||
:caption: Invalid, 'hidden' dbobject
|
||||
|
||||
# example of an invalid, "hidden" dbobject
|
||||
# example of storing an invalid, "hidden" dbobject in Attribute
|
||||
class Container:
|
||||
def __init__(self, mydbobj):
|
||||
# no way for Evennia to know this is a database object!
|
||||
self.mydbobj = mydbobj
|
||||
|
||||
# let's assume myobj is a db-object
|
||||
container = Container(myobj)
|
||||
obj.db.invalid = container # will cause error!
|
||||
obj.db.mydata = container # will raise error!
|
||||
|
||||
```
|
||||
|
||||
By adding two methods `__serialize_dbobjs__` and `__deserialize_dbobjs__` to the
|
||||
object you want to save, you can pre-serialize and post-deserialize all 'hidden'
|
||||
objects before Evennia's main serializer gets to work. Inside these methods, use Evennia's
|
||||
[evennia.utils.dbserialize.dbserialize](api:evennia.utils.dbserialize.dbserialize) and
|
||||
[dbunserialize](api:evennia.utils.dbserialize.dbunserialize) functions to safely
|
||||
serialize the db-objects you want to store.
|
||||
|
||||
```{code-block} python
|
||||
:caption: Fixing an invalid 'hidden' dbobj for storing in Attribute
|
||||
|
||||
from evennia.utils import dbserialize # important
|
||||
|
||||
class Container:
|
||||
def __init__(self, mydbobj):
|
||||
# A 'hidden' db-object
|
||||
self.mydbobj = mydbobj
|
||||
|
||||
def __serialize_dbobjs__(self):
|
||||
"""This is called before serialization and allows
|
||||
us to custom-handle those 'hidden' dbobjs"""
|
||||
self.mydbobj = dbserialize.dbserialize(self.mydbobj
|
||||
|
||||
def __deserialize_dbobjs__(self):
|
||||
"""This is called after deserialization and allows you to
|
||||
restore the 'hidden' dbobjs you serialized before"""
|
||||
self.mydbobj = dbserialize.dbunserialize(self.mydbobj)
|
||||
|
||||
# let's assume myobj is a db-object
|
||||
container = Container(myobj)
|
||||
obj.db.mydata = container # will now work fine!
|
||||
```
|
||||
|
||||
### Storing multiple objects
|
||||
|
|
@ -404,6 +443,12 @@ obj.db.test8[2]["test"] = 5
|
|||
# test8 is now [4,2,{"test":5}]
|
||||
```
|
||||
|
||||
Note that if make some advanced iterable object, and store an db-object on it in
|
||||
a way such that it is _not_ returned by iterating over it, you have created a
|
||||
'hidden' db-object. See [the previous section](#storing-single-objects) for how
|
||||
to tell Evennia how to serialize such hidden objects safely.
|
||||
|
||||
|
||||
### Retrieving Mutable objects
|
||||
|
||||
A side effect of the way Evennia stores Attributes is that *mutable* iterables (iterables that can
|
||||
|
|
@ -429,41 +474,41 @@ print(obj.db.mylist) # now also [1, 2, 3, 5]
|
|||
```
|
||||
|
||||
When you extract your mutable Attribute data into a variable like `mylist`, think of it as getting a _snapshot_
|
||||
of the variable. If you update the snapshot, it will save to the database, but this change _will not propagate to
|
||||
of the variable. If you update the snapshot, it will save to the database, but this change _will not propagate to
|
||||
any other snapshots you may have done previously_.
|
||||
|
||||
```python
|
||||
```python
|
||||
obj.db.mylist = [1, 2, 3, 4]
|
||||
mylist1 = obj.db.mylist
|
||||
mylist2 = obj.db.mylist
|
||||
mylist1[3] = 5
|
||||
mylist1 = obj.db.mylist
|
||||
mylist2 = obj.db.mylist
|
||||
mylist1[3] = 5
|
||||
|
||||
print(mylist1) # this is now [1, 2, 3, 5]
|
||||
print(obj.db.mylist) # also updated to [1, 2, 3, 5]
|
||||
print(obj.db.mylist) # also updated to [1, 2, 3, 5]
|
||||
|
||||
print(mylist2) # still [1, 2, 3, 4] !
|
||||
print(mylist2) # still [1, 2, 3, 4] !
|
||||
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
Remember, the complexities of this section only relate to *mutable* iterables - things you can update
|
||||
in-place, like lists and dicts. [Immutable](https://en.wikipedia.org/wiki/Immutable) objects (strings,
|
||||
in-place, like lists and dicts. [Immutable](https://en.wikipedia.org/wiki/Immutable) objects (strings,
|
||||
numbers, tuples etc) are already disconnected from the database from the onset.
|
||||
```
|
||||
|
||||
To avoid confusion with mutable Attributes, only work with one variable (snapshot) at a time and save
|
||||
back the results as needed.
|
||||
To avoid confusion with mutable Attributes, only work with one variable (snapshot) at a time and save
|
||||
back the results as needed.
|
||||
|
||||
You can also choose to "disconnect" the Attribute entirely from the
|
||||
database with the help of the `.deserialize()` method:
|
||||
|
||||
```python
|
||||
obj.db.mylist = [1, 2, 3, 4, {1: 2}]
|
||||
mylist = obj.db.mylist.deserialize()
|
||||
mylist = obj.db.mylist.deserialize()
|
||||
```
|
||||
|
||||
The result of this operation will be a structure only consisting of normal Python mutables (`list`
|
||||
instead of `_SaverList`, `dict` instead of `_SaverDict` and so on). If you update it, you need to
|
||||
instead of `_SaverList`, `dict` instead of `_SaverDict` and so on). If you update it, you need to
|
||||
explicitly save it back to the Attribute for it to save.
|
||||
|
||||
## Properties of Attributes
|
||||
|
|
@ -518,7 +563,7 @@ are **non-persistent** - they will _not_ survive a server reload.
|
|||
Differences between `Attributes` and `NAttributes`:
|
||||
|
||||
- `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
|
||||
- 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`)
|
||||
|
|
@ -526,10 +571,10 @@ Differences between `Attributes` and `NAttributes`:
|
|||
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:
|
||||
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
|
|
@ -557,4 +602,4 @@ useful in a few situations though.
|
|||
- `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!
|
||||
grand vision!
|
||||
|
|
|
|||
|
|
@ -129,10 +129,11 @@ Contains a very useful list of things to think about when starting your new MUD.
|
|||
Essential reading for the design of any persistent game
|
||||
world, written by the co-creator of the original game *MUD*. Published in 2003 but it's still as
|
||||
relevant now as when it came out. Covers everything you need to know and then some.
|
||||
- Zed A. Shaw *Learn Python the Hard way* ([homepage](https://learnpythonthehardway.org/)) - Despite
|
||||
the imposing name this book is for the absolute Python/programming beginner. One learns the language
|
||||
by gradually creating a small text game! It has been used by multiple users before moving on to
|
||||
Evennia. *Update: This used to be free to read online, this is no longer the case.*
|
||||
|
||||
When the rights to Designing Virtual Worlds returned to him, Richard Bartle
|
||||
made the PDF of his Designing Virtual Worlds freely available through his own
|
||||
website ([Designing Virtual Worlds](https://mud.co.uk/dvw/)). A direct link to
|
||||
the PDF can be found [here](https://mud.co.uk/richard/DesigningVirtualWorlds.pdf).
|
||||
- David M. Beazley *Python Essential Reference (4th ed)*
|
||||
([amazon page](https://www.amazon.com/Python-Essential-Reference-David-Beazley/dp/0672329786/)) -
|
||||
Our recommended book on Python; it not only efficiently summarizes the language but is also
|
||||
|
|
|
|||
|
|
@ -2732,7 +2732,7 @@ class CmdExamine(ObjManipCommand):
|
|||
return
|
||||
|
||||
if ndb_attr and ndb_attr[0]:
|
||||
return "\n " + " \n".join(
|
||||
return "\n " + "\n ".join(
|
||||
sorted(self.format_single_attribute(attr) for attr in ndb_attr)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -328,4 +328,4 @@ class GametimeScript(DefaultScript):
|
|||
callback()
|
||||
|
||||
seconds = real_seconds_until(**self.db.gametime)
|
||||
self.restart(interval=seconds)
|
||||
self.start(interval=seconds,force_restart=True)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ Roleplaying emotes and language - Griatch, 2015
|
|||
"""
|
||||
|
||||
from .rpsystem import EmoteError, SdescError, RecogError, LanguageError # noqa
|
||||
from .rpsystem import ordered_permutation_regex, regex_tuple_from_key_alias # noqa
|
||||
from .rpsystem import parse_language, parse_sdescs_and_recogs, send_emote # noqa
|
||||
from .rpsystem import SdescHandler, RecogHandler # noqa
|
||||
from .rpsystem import RPCommand, CmdEmote, CmdSay, CmdSdesc, CmdPose, CmdRecog, CmdMask # noqa
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -96,7 +96,7 @@ recog01 = "Mr Receiver"
|
|||
recog02 = "Mr Receiver2"
|
||||
recog10 = "Mr Sender"
|
||||
emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."'
|
||||
case_emote = "/me looks at /first, then /FIRST, /First and /Colliding twice."
|
||||
case_emote = "/Me looks at /first. Then, /me looks at /FIRST, /First and /Colliding twice."
|
||||
|
||||
|
||||
class TestRPSystem(BaseEvenniaTest):
|
||||
|
|
@ -113,41 +113,11 @@ class TestRPSystem(BaseEvenniaTest):
|
|||
rpsystem.ContribRPCharacter, key="Receiver2", location=self.room
|
||||
)
|
||||
|
||||
def test_ordered_permutation_regex(self):
|
||||
self.assertEqual(
|
||||
rpsystem.ordered_permutation_regex(sdesc0),
|
||||
"/[0-9]*-*A\\ nice\\ sender\\ of\\ emotes(?=\\W|$)+|"
|
||||
"/[0-9]*-*nice\\ sender\\ of\\ emotes(?=\\W|$)+|"
|
||||
"/[0-9]*-*A\\ nice\\ sender\\ of(?=\\W|$)+|"
|
||||
"/[0-9]*-*sender\\ of\\ emotes(?=\\W|$)+|"
|
||||
"/[0-9]*-*nice\\ sender\\ of(?=\\W|$)+|"
|
||||
"/[0-9]*-*A\\ nice\\ sender(?=\\W|$)+|"
|
||||
"/[0-9]*-*nice\\ sender(?=\\W|$)+|"
|
||||
"/[0-9]*-*of\\ emotes(?=\\W|$)+|"
|
||||
"/[0-9]*-*sender\\ of(?=\\W|$)+|"
|
||||
"/[0-9]*-*A\\ nice(?=\\W|$)+|"
|
||||
"/[0-9]*-*emotes(?=\\W|$)+|"
|
||||
"/[0-9]*-*sender(?=\\W|$)+|"
|
||||
"/[0-9]*-*nice(?=\\W|$)+|"
|
||||
"/[0-9]*-*of(?=\\W|$)+|"
|
||||
"/[0-9]*-*A(?=\\W|$)+",
|
||||
)
|
||||
|
||||
def test_sdesc_handler(self):
|
||||
self.speaker.sdesc.add(sdesc0)
|
||||
self.assertEqual(self.speaker.sdesc.get(), sdesc0)
|
||||
self.speaker.sdesc.add("This is {#324} ignored")
|
||||
self.assertEqual(self.speaker.sdesc.get(), "This is 324 ignored")
|
||||
self.speaker.sdesc.add("Testing three words")
|
||||
self.assertEqual(
|
||||
self.speaker.sdesc.get_regex_tuple()[0].pattern,
|
||||
"/[0-9]*-*Testing\ three\ words(?=\W|$)+|"
|
||||
"/[0-9]*-*Testing\ three(?=\W|$)+|"
|
||||
"/[0-9]*-*three\ words(?=\W|$)+|"
|
||||
"/[0-9]*-*Testing(?=\W|$)+|"
|
||||
"/[0-9]*-*three(?=\W|$)+|"
|
||||
"/[0-9]*-*words(?=\W|$)+",
|
||||
)
|
||||
|
||||
def test_recog_handler(self):
|
||||
self.speaker.sdesc.add(sdesc0)
|
||||
|
|
@ -156,12 +126,8 @@ class TestRPSystem(BaseEvenniaTest):
|
|||
self.speaker.recog.add(self.receiver2, recog02)
|
||||
self.assertEqual(self.speaker.recog.get(self.receiver1), recog01)
|
||||
self.assertEqual(self.speaker.recog.get(self.receiver2), recog02)
|
||||
self.assertEqual(
|
||||
self.speaker.recog.get_regex_tuple(self.receiver1)[0].pattern,
|
||||
"/[0-9]*-*Mr\\ Receiver(?=\\W|$)+|/[0-9]*-*Receiver(?=\\W|$)+|/[0-9]*-*Mr(?=\\W|$)+",
|
||||
)
|
||||
self.speaker.recog.remove(self.receiver1)
|
||||
self.assertEqual(self.speaker.recog.get(self.receiver1), sdesc1)
|
||||
self.assertEqual(self.speaker.recog.get(self.receiver1), None)
|
||||
|
||||
self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2})
|
||||
|
||||
|
|
@ -198,6 +164,24 @@ class TestRPSystem(BaseEvenniaTest):
|
|||
result,
|
||||
)
|
||||
|
||||
def test_get_sdesc(self):
|
||||
looker = self.speaker # Sender
|
||||
target = self.receiver1 # Receiver1
|
||||
looker.sdesc.add(sdesc0) # A nice sender of emotes
|
||||
target.sdesc.add(sdesc1) # The first receiver of emotes.
|
||||
|
||||
# sdesc with no processing
|
||||
self.assertEqual(looker.get_sdesc(target), "The first receiver of emotes.")
|
||||
# sdesc with processing
|
||||
self.assertEqual(looker.get_sdesc(target, process=True), "|bThe first receiver of emotes.|n")
|
||||
|
||||
looker.recog.add(target, recog01) # Mr Receiver
|
||||
|
||||
# recog with no processing
|
||||
self.assertEqual(looker.get_sdesc(target), "Mr Receiver")
|
||||
# recog with processing
|
||||
self.assertEqual(looker.get_sdesc(target, process=True), "|mMr Receiver|n")
|
||||
|
||||
def test_send_emote(self):
|
||||
speaker = self.speaker
|
||||
receiver1 = self.receiver1
|
||||
|
|
@ -212,18 +196,18 @@ class TestRPSystem(BaseEvenniaTest):
|
|||
rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False)
|
||||
self.assertEqual(
|
||||
self.out0,
|
||||
"With a flair, |bSender|n looks at |bThe first receiver of emotes.|n "
|
||||
"With a flair, |mSender|n looks at |bThe first receiver of emotes.|n "
|
||||
'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
|
||||
)
|
||||
self.assertEqual(
|
||||
self.out1,
|
||||
"With a flair, |bA nice sender of emotes|n looks at |bReceiver1|n and "
|
||||
"With a flair, |bA nice sender of emotes|n looks at |mReceiver1|n and "
|
||||
'|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
|
||||
)
|
||||
self.assertEqual(
|
||||
self.out2,
|
||||
"With a flair, |bA nice sender of emotes|n looks at |bThe first "
|
||||
'receiver of emotes.|n and |bReceiver2|n. She says |w"This is a test."|n',
|
||||
'receiver of emotes.|n and |mReceiver2|n. She says |w"This is a test."|n',
|
||||
)
|
||||
|
||||
def test_send_case_sensitive_emote(self):
|
||||
|
|
@ -241,20 +225,21 @@ class TestRPSystem(BaseEvenniaTest):
|
|||
rpsystem.send_emote(speaker, receivers, case_emote)
|
||||
self.assertEqual(
|
||||
self.out0,
|
||||
"|bSender|n looks at |bthe first receiver of emotes.|n, then "
|
||||
"|bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n and "
|
||||
"|bAnother nice colliding sdesc-guy for tests|n twice.",
|
||||
"|mSender|n looks at |bthe first receiver of emotes.|n. Then, |mSender|n "
|
||||
"looks at |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n "
|
||||
"and |bAnother nice colliding sdesc-guy for tests|n twice.",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.out1,
|
||||
"|bA nice sender of emotes|n looks at |bReceiver1|n, then |bReceiver1|n, "
|
||||
"|bReceiver1|n and |bAnother nice colliding sdesc-guy for tests|n twice.",
|
||||
"|bA nice sender of emotes|n looks at |mReceiver1|n. Then, "
|
||||
"|ba nice sender of emotes|n looks at |mReceiver1|n, |mReceiver1|n "
|
||||
"and |bAnother nice colliding sdesc-guy for tests|n twice."
|
||||
)
|
||||
self.assertEqual(
|
||||
self.out2,
|
||||
"|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n, "
|
||||
"then |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of "
|
||||
"emotes.|n and |bReceiver2|n twice.",
|
||||
"|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n. "
|
||||
"Then, |ba nice sender of emotes|n looks at |bTHE FIRST RECEIVER OF EMOTES.|n, "
|
||||
"|bThe first receiver of emotes.|n and |mReceiver2|n twice.",
|
||||
)
|
||||
|
||||
def test_rpsearch(self):
|
||||
|
|
@ -265,18 +250,6 @@ class TestRPSystem(BaseEvenniaTest):
|
|||
self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1)
|
||||
self.assertEqual(self.speaker.search("colliding"), self.receiver2)
|
||||
|
||||
def test_regex_tuple_from_key_alias(self):
|
||||
self.speaker.aliases.add("foo bar")
|
||||
self.speaker.aliases.add("this thing is a long thing")
|
||||
t0 = time.time()
|
||||
result = rpsystem.regex_tuple_from_key_alias(self.speaker)
|
||||
t1 = time.time()
|
||||
result = rpsystem.regex_tuple_from_key_alias(self.speaker)
|
||||
t2 = time.time()
|
||||
# print(f"t1: {t1 - t0}, t2: {t2 - t1}")
|
||||
self.assertLess(t2 - t1, 10**-4)
|
||||
self.assertEqual(result, (Anything, self.speaker, self.speaker.key))
|
||||
|
||||
|
||||
class TestRPSystemCommands(BaseEvenniaCommandTest):
|
||||
def setUp(self):
|
||||
|
|
@ -305,7 +278,7 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
rpsystem.CmdRecog(),
|
||||
"barfoo as friend",
|
||||
"Char will now remember BarFoo Character as friend.",
|
||||
"You will now remember BarFoo Character as friend.",
|
||||
)
|
||||
self.call(
|
||||
rpsystem.CmdRecog(),
|
||||
|
|
@ -316,6 +289,6 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
rpsystem.CmdRecog(),
|
||||
"friend",
|
||||
"Char will now know them only as 'BarFoo Character'",
|
||||
"You will now know them only as 'BarFoo Character'",
|
||||
cmdstring="forget",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
|
|||
categories = make_iter(category) if category else []
|
||||
n_keys = len(keys)
|
||||
n_categories = len(categories)
|
||||
unique_categories = sorted(set(categories))
|
||||
unique_categories = set(categories)
|
||||
n_unique_categories = len(unique_categories)
|
||||
|
||||
dbmodel = self.model.__dbclass__.__name__.lower()
|
||||
|
|
|
|||
|
|
@ -142,6 +142,13 @@ class TestTypedObjectManager(BaseEvenniaTest):
|
|||
[self.obj1],
|
||||
)
|
||||
|
||||
def test_get_tag_with_any_including_nones(self):
|
||||
self.obj1.tags.add("tagA", "categoryA")
|
||||
self.assertEqual(
|
||||
self._manager("get_by_tag", ["tagA", "tagB"], ["categoryA", "categoryB", None], match="any"),
|
||||
[self.obj1],
|
||||
)
|
||||
|
||||
def test_get_tag_withnomatch(self):
|
||||
self.obj1.tags.add("tagC", "categoryC")
|
||||
self.assertEqual(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ evennia.OPTION_CLASSES
|
|||
|
||||
|
||||
from pickle import dumps
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
from django.conf import settings
|
||||
from evennia.utils.utils import class_from_module, callables_from_module
|
||||
from evennia.utils import logger
|
||||
|
|
@ -167,7 +168,6 @@ class GlobalScriptContainer(Container):
|
|||
|
||||
# store a hash representation of the setup
|
||||
script.attributes.add("_global_script_settings", compare_hash, category="settings_hash")
|
||||
script.start()
|
||||
|
||||
return script
|
||||
|
||||
|
|
@ -183,9 +183,16 @@ class GlobalScriptContainer(Container):
|
|||
# populate self.typeclass_storage
|
||||
self.load_data()
|
||||
|
||||
# start registered scripts
|
||||
# make sure settings-defined scripts are loaded
|
||||
for key in self.loaded_data:
|
||||
self._load_script(key)
|
||||
# start all global scripts
|
||||
try:
|
||||
for script in self._get_scripts():
|
||||
script.start()
|
||||
except (OperationalError, ProgrammingError):
|
||||
# this can happen if db is not loaded yet (such as when building docs)
|
||||
pass
|
||||
|
||||
def load_data(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -239,6 +239,9 @@ class _SaverMutable(object):
|
|||
def __gt__(self, other):
|
||||
return self._data > other
|
||||
|
||||
def __or__(self, other):
|
||||
return self._data | other
|
||||
|
||||
@_save
|
||||
def __setitem__(self, key, value):
|
||||
self._data.__setitem__(key, self._convert_mutables(value))
|
||||
|
|
@ -450,7 +453,9 @@ def deserialize(obj):
|
|||
elif tname in ("_SaverOrderedDict", "OrderedDict"):
|
||||
return OrderedDict([(_iter(key), _iter(val)) for key, val in obj.items()])
|
||||
elif tname in ("_SaverDefaultDict", "defaultdict"):
|
||||
return defaultdict(obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()})
|
||||
return defaultdict(
|
||||
obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()}
|
||||
)
|
||||
elif tname in _DESERIALIZE_MAPPING:
|
||||
return _DESERIALIZE_MAPPING[tname](_iter(val) for val in obj)
|
||||
elif is_iter(obj):
|
||||
|
|
@ -602,7 +607,9 @@ def to_pickle(data):
|
|||
|
||||
def process_item(item):
|
||||
"""Recursive processor and identification of data"""
|
||||
|
||||
dtype = type(item)
|
||||
|
||||
if dtype in (str, int, float, bool, bytes, SafeString):
|
||||
return item
|
||||
elif dtype == tuple:
|
||||
|
|
@ -612,7 +619,10 @@ def to_pickle(data):
|
|||
elif dtype in (dict, _SaverDict):
|
||||
return dict((process_item(key), process_item(val)) for key, val in item.items())
|
||||
elif dtype in (defaultdict, _SaverDefaultDict):
|
||||
return defaultdict(item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items()))
|
||||
return defaultdict(
|
||||
item.default_factory,
|
||||
((process_item(key), process_item(val)) for key, val in item.items()),
|
||||
)
|
||||
elif dtype in (set, _SaverSet):
|
||||
return set(process_item(val) for val in item)
|
||||
elif dtype in (OrderedDict, _SaverOrderedDict):
|
||||
|
|
@ -620,7 +630,20 @@ def to_pickle(data):
|
|||
elif dtype in (deque, _SaverDeque):
|
||||
return deque(process_item(val) for val in item)
|
||||
|
||||
elif hasattr(item, "__iter__"):
|
||||
# not one of the base types
|
||||
if hasattr(item, "__serialize_dbobjs__"):
|
||||
# Allows custom serialization of any dbobjects embedded in
|
||||
# the item that Evennia will otherwise not found (these would
|
||||
# otherwise lead to an error). Use the dbserialize helper from
|
||||
# this method.
|
||||
try:
|
||||
item.__serialize_dbobjs__()
|
||||
except TypeError:
|
||||
# we catch typerrors so we can handle both classes (requiring
|
||||
# classmethods) and instances
|
||||
pass
|
||||
|
||||
if hasattr(item, "__iter__"):
|
||||
# we try to conserve the iterable class, if not convert to list
|
||||
try:
|
||||
return item.__class__([process_item(val) for val in item])
|
||||
|
|
@ -678,7 +701,10 @@ def from_pickle(data, db_obj=None):
|
|||
elif dtype == dict:
|
||||
return dict((process_item(key), process_item(val)) for key, val in item.items())
|
||||
elif dtype == defaultdict:
|
||||
return defaultdict(item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items()))
|
||||
return defaultdict(
|
||||
item.default_factory,
|
||||
((process_item(key), process_item(val)) for key, val in item.items()),
|
||||
)
|
||||
elif dtype == set:
|
||||
return set(process_item(val) for val in item)
|
||||
elif dtype == OrderedDict:
|
||||
|
|
@ -692,6 +718,18 @@ def from_pickle(data, db_obj=None):
|
|||
return item.__class__(process_item(val) for val in item)
|
||||
except (AttributeError, TypeError):
|
||||
return [process_item(val) for val in item]
|
||||
|
||||
if hasattr(item, "__deserialize_dbobjs__"):
|
||||
# this allows the object to custom-deserialize any embedded dbobjs
|
||||
# that we previously serialized with __serialize_dbobjs__.
|
||||
# use the dbunserialize helper in this module.
|
||||
try:
|
||||
item.__deserialize_dbobjs__()
|
||||
except TypeError:
|
||||
# handle recoveries both of classes (requiring classmethods
|
||||
# or instances
|
||||
pass
|
||||
|
||||
return item
|
||||
|
||||
def process_tree(item, parent):
|
||||
|
|
|
|||
|
|
@ -274,12 +274,13 @@ import inspect
|
|||
|
||||
from ast import literal_eval
|
||||
from fnmatch import fnmatch
|
||||
from math import ceil
|
||||
|
||||
from inspect import isfunction, getargspec
|
||||
from django.conf import settings
|
||||
from evennia import Command, CmdSet
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.evtable import EvTable
|
||||
from evennia.utils.evtable import EvTable, EvColumn
|
||||
from evennia.utils.ansi import strip_ansi
|
||||
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
|
||||
from evennia.commands import cmdhandler
|
||||
|
|
@ -1210,7 +1211,6 @@ class EvMenu:
|
|||
Args:
|
||||
optionlist (list): List of (key, description) tuples for every
|
||||
option related to this node.
|
||||
caller (Object, Account or None, optional): The caller of the node.
|
||||
|
||||
Returns:
|
||||
options (str): The formatted option display.
|
||||
|
|
@ -1229,7 +1229,7 @@ class EvMenu:
|
|||
table = []
|
||||
for key, desc in optionlist:
|
||||
if key or desc:
|
||||
desc_string = ": %s" % desc if desc else ""
|
||||
desc_string = f": {desc}" if desc else ""
|
||||
table_width_max = max(
|
||||
table_width_max,
|
||||
max(m_len(p) for p in key.split("\n"))
|
||||
|
|
@ -1239,42 +1239,31 @@ class EvMenu:
|
|||
raw_key = strip_ansi(key)
|
||||
if raw_key != key:
|
||||
# already decorations in key definition
|
||||
table.append(" |lc%s|lt%s|le%s" % (raw_key, key, desc_string))
|
||||
table.append(f" |lc{raw_key}|lt{key}|le{desc_string}")
|
||||
else:
|
||||
# add a default white color to key
|
||||
table.append(" |lc%s|lt|w%s|n|le%s" % (raw_key, raw_key, desc_string))
|
||||
ncols = _MAX_TEXT_WIDTH // table_width_max # number of ncols
|
||||
table.append(f" |lc{raw_key}|lt|w{key}|n|le{desc_string}")
|
||||
ncols = _MAX_TEXT_WIDTH // table_width_max # number of columns
|
||||
|
||||
if ncols < 0:
|
||||
# no visible option at all
|
||||
# no visible options at all
|
||||
return ""
|
||||
|
||||
ncols = ncols + 1 if ncols == 0 else ncols
|
||||
# get the amount of rows needed (start with 4 rows)
|
||||
nrows = 4
|
||||
while nrows * ncols < nlist:
|
||||
nrows += 1
|
||||
ncols = nlist // nrows # number of full columns
|
||||
nlastcol = nlist % nrows # number of elements in last column
|
||||
ncols = 1 if ncols == 0 else ncols
|
||||
|
||||
# get the final column count
|
||||
ncols = ncols + 1 if nlastcol > 0 else ncols
|
||||
if ncols > 1:
|
||||
# only extend if longer than one column
|
||||
table.extend([" " for i in range(nrows - nlastcol)])
|
||||
# minimum number of rows in a column
|
||||
min_rows = 4
|
||||
|
||||
# build the actual table grid
|
||||
table = [table[icol * nrows : (icol * nrows) + nrows] for icol in range(0, ncols)]
|
||||
# split the items into columns
|
||||
split = max(min_rows, ceil(len(table)/ncols))
|
||||
max_end = len(table)
|
||||
cols_list = []
|
||||
for icol in range(ncols):
|
||||
start = icol*split
|
||||
end = min(start+split,max_end)
|
||||
cols_list.append(EvColumn(*table[start:end]))
|
||||
|
||||
# adjust the width of each column
|
||||
for icol in range(len(table)):
|
||||
col_width = (
|
||||
max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep
|
||||
)
|
||||
table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]]
|
||||
|
||||
# format the table into columns
|
||||
return str(EvTable(table=table, border="none"))
|
||||
return str(EvTable(table=cols_list, border="none"))
|
||||
|
||||
def node_formatter(self, nodetext, optionstext):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class TimeScript(DefaultScript):
|
|||
callback(*args, **kwargs)
|
||||
|
||||
seconds = real_seconds_until(**self.db.gametime)
|
||||
self.restart(interval=seconds)
|
||||
self.start(interval=seconds,force_restart=True)
|
||||
|
||||
|
||||
# Access functions
|
||||
|
|
|
|||
|
|
@ -62,10 +62,12 @@ class TestDbSerialize(TestCase):
|
|||
self.obj.db.test.sort(key=lambda d: str(d))
|
||||
self.assertEqual(self.obj.db.test, [{0: 1}, {1: 0}])
|
||||
|
||||
def test_dict(self):
|
||||
def test_saverdict(self):
|
||||
self.obj.db.test = {"a": True}
|
||||
self.obj.db.test.update({"b": False})
|
||||
self.assertEqual(self.obj.db.test, {"a": True, "b": False})
|
||||
self.obj.db.test |= {"c": 5}
|
||||
self.assertEqual(self.obj.db.test, {"a": True, "b": False, "c": 5})
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
|
|
@ -88,27 +90,30 @@ class TestDbSerialize(TestCase):
|
|||
self.assertIsInstance(value, base_type)
|
||||
self.assertNotIsInstance(value, saver_type)
|
||||
self.assertEqual(value, default_value)
|
||||
self.obj.db.test = {'a': True}
|
||||
self.obj.db.test.update({'b': False})
|
||||
self.assertEqual(self.obj.db.test, {'a': True, 'b': False})
|
||||
self.obj.db.test = {"a": True}
|
||||
self.obj.db.test.update({"b": False})
|
||||
self.assertEqual(self.obj.db.test, {"a": True, "b": False})
|
||||
|
||||
def test_defaultdict(self):
|
||||
from collections import defaultdict
|
||||
|
||||
# baseline behavior for a defaultdict
|
||||
_dd = defaultdict(list)
|
||||
_dd['a']
|
||||
self.assertEqual(_dd, {'a': []})
|
||||
_dd["a"]
|
||||
self.assertEqual(_dd, {"a": []})
|
||||
|
||||
# behavior after defaultdict is set as attribute
|
||||
|
||||
dd = defaultdict(list)
|
||||
self.obj.db.test = dd
|
||||
self.obj.db.test['a']
|
||||
self.assertEqual(self.obj.db.test, {'a': []})
|
||||
self.obj.db.test["a"]
|
||||
self.assertEqual(self.obj.db.test, {"a": []})
|
||||
|
||||
self.obj.db.test['a'].append(1)
|
||||
self.assertEqual(self.obj.db.test, {'a': [1]})
|
||||
self.obj.db.test['a'].append(2)
|
||||
self.assertEqual(self.obj.db.test, {'a': [1, 2]})
|
||||
self.obj.db.test['a'].append(3)
|
||||
self.assertEqual(self.obj.db.test, {'a': [1, 2, 3]})
|
||||
self.obj.db.test["a"].append(1)
|
||||
self.assertEqual(self.obj.db.test, {"a": [1]})
|
||||
self.obj.db.test["a"].append(2)
|
||||
self.assertEqual(self.obj.db.test, {"a": [1, 2]})
|
||||
self.obj.db.test["a"].append(3)
|
||||
self.assertEqual(self.obj.db.test, {"a": [1, 2, 3]})
|
||||
self.obj.db.test |= {"b": [5, 6]}
|
||||
self.assertEqual(self.obj.db.test, {"a": [1, 2, 3], "b": [5, 6]})
|
||||
|
|
|
|||
|
|
@ -7,20 +7,20 @@ import mock
|
|||
|
||||
|
||||
class TestText2Html(TestCase):
|
||||
def test_re_color(self):
|
||||
def test_format_styles(self):
|
||||
parser = text2html.HTML_PARSER
|
||||
self.assertEqual("foo", parser.re_color("foo"))
|
||||
self.assertEqual("foo", parser.format_styles("foo"))
|
||||
self.assertEqual(
|
||||
'<span class="color-001">red</span>foo',
|
||||
parser.re_color(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"),
|
||||
parser.format_styles(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"),
|
||||
)
|
||||
self.assertEqual(
|
||||
'<span class="bgcolor-001">red</span>foo',
|
||||
parser.re_color(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
|
||||
parser.format_styles(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
|
||||
)
|
||||
self.assertEqual(
|
||||
'<span class="bgcolor-001"><span class="color-002">red</span></span>foo',
|
||||
parser.re_color(
|
||||
'<span class="bgcolor-001 color-002">red</span>foo',
|
||||
parser.format_styles(
|
||||
ansi.ANSI_BACK_RED
|
||||
+ ansi.ANSI_UNHILITE
|
||||
+ ansi.ANSI_GREEN
|
||||
|
|
@ -29,63 +29,37 @@ class TestText2Html(TestCase):
|
|||
+ "foo"
|
||||
),
|
||||
)
|
||||
|
||||
@unittest.skip("parser issues")
|
||||
def test_re_bold(self):
|
||||
parser = text2html.HTML_PARSER
|
||||
self.assertEqual("foo", parser.re_bold("foo"))
|
||||
self.assertEqual(
|
||||
# "a <strong>red</strong>foo", # TODO: why not?
|
||||
"a <strong>redfoo</strong>",
|
||||
parser.re_bold("a " + ansi.ANSI_HILITE + "red" + ansi.ANSI_UNHILITE + "foo"),
|
||||
)
|
||||
|
||||
@unittest.skip("parser issues")
|
||||
def test_re_underline(self):
|
||||
parser = text2html.HTML_PARSER
|
||||
self.assertEqual("foo", parser.re_underline("foo"))
|
||||
self.assertEqual(
|
||||
'a <span class="underline">red</span>' + ansi.ANSI_NORMAL + "foo",
|
||||
parser.re_underline(
|
||||
'a <span class="underline">red</span>foo',
|
||||
parser.format_styles(
|
||||
"a "
|
||||
+ ansi.ANSI_UNDERLINE
|
||||
+ "red"
|
||||
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
|
||||
+ ansi.ANSI_NORMAL
|
||||
+ "foo"
|
||||
),
|
||||
)
|
||||
|
||||
@unittest.skip("parser issues")
|
||||
def test_re_blinking(self):
|
||||
parser = text2html.HTML_PARSER
|
||||
self.assertEqual("foo", parser.re_blinking("foo"))
|
||||
self.assertEqual(
|
||||
'a <span class="blink">red</span>' + ansi.ANSI_NORMAL + "foo",
|
||||
parser.re_blinking(
|
||||
'a <span class="blink">red</span>foo',
|
||||
parser.format_styles(
|
||||
"a "
|
||||
+ ansi.ANSI_BLINK
|
||||
+ "red"
|
||||
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
|
||||
+ ansi.ANSI_NORMAL
|
||||
+ "foo"
|
||||
),
|
||||
)
|
||||
|
||||
@unittest.skip("parser issues")
|
||||
def test_re_inversing(self):
|
||||
parser = text2html.HTML_PARSER
|
||||
self.assertEqual("foo", parser.re_inversing("foo"))
|
||||
self.assertEqual(
|
||||
'a <span class="inverse">red</span>' + ansi.ANSI_NORMAL + "foo",
|
||||
parser.re_inversing(
|
||||
'a <span class="bgcolor-007 color-000">red</span>foo',
|
||||
parser.format_styles(
|
||||
"a "
|
||||
+ ansi.ANSI_INVERSE
|
||||
+ "red"
|
||||
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
|
||||
+ ansi.ANSI_NORMAL
|
||||
+ "foo"
|
||||
),
|
||||
)
|
||||
|
||||
@unittest.skip("parser issues")
|
||||
def test_remove_bells(self):
|
||||
parser = text2html.HTML_PARSER
|
||||
self.assertEqual("foo", parser.remove_bells("foo"))
|
||||
|
|
@ -95,7 +69,7 @@ class TestText2Html(TestCase):
|
|||
"a "
|
||||
+ ansi.ANSI_BEEP
|
||||
+ "red"
|
||||
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
|
||||
+ ansi.ANSI_NORMAL
|
||||
+ "foo"
|
||||
),
|
||||
)
|
||||
|
|
@ -110,7 +84,6 @@ class TestText2Html(TestCase):
|
|||
self.assertEqual("foo", parser.convert_linebreaks("foo"))
|
||||
self.assertEqual("a<br> redfoo<br>", parser.convert_linebreaks("a\n redfoo\n"))
|
||||
|
||||
@unittest.skip("parser issues")
|
||||
def test_convert_urls(self):
|
||||
parser = text2html.HTML_PARSER
|
||||
self.assertEqual("foo", parser.convert_urls("foo"))
|
||||
|
|
@ -118,7 +91,6 @@ class TestText2Html(TestCase):
|
|||
'a <a href="http://redfoo" target="_blank">http://redfoo</a> runs',
|
||||
parser.convert_urls("a http://redfoo runs"),
|
||||
)
|
||||
# TODO: doesn't URL encode correctly
|
||||
|
||||
def test_sub_mxp_links(self):
|
||||
parser = text2html.HTML_PARSER
|
||||
|
|
@ -186,22 +158,22 @@ class TestText2Html(TestCase):
|
|||
self.assertEqual("foo", text2html.parse_html("foo"))
|
||||
self.maxDiff = None
|
||||
self.assertEqual(
|
||||
# TODO: note that the blink is currently *not* correctly aborted
|
||||
# with |n here! This is probably not possible to correctly handle
|
||||
# with regex - a stateful parser may be needed.
|
||||
# blink back-cyan normal underline red green yellow blue magenta cyan back-green
|
||||
text2html.parse_html("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"),
|
||||
'<span class="blink">'
|
||||
'<span class="bgcolor-006">Hello</span>' # noqa
|
||||
'<span class="underline">'
|
||||
'<span class="color-009">W</span>' # noqa
|
||||
'<span class="color-010">o</span>'
|
||||
'<span class="color-011">r</span>'
|
||||
'<span class="color-012">l</span>'
|
||||
'<span class="color-013">d</span>'
|
||||
'<span class="color-014">!'
|
||||
'<span class="bgcolor-002">!</span>' # noqa
|
||||
"</span>"
|
||||
"</span>"
|
||||
"</span>",
|
||||
'<span class="blink bgcolor-006">'
|
||||
'Hello'
|
||||
'</span><span class="underline color-009">'
|
||||
'W'
|
||||
'</span><span class="underline color-010">'
|
||||
'o'
|
||||
'</span><span class="underline color-011">'
|
||||
'r'
|
||||
'</span><span class="underline color-012">'
|
||||
'l'
|
||||
'</span><span class="underline color-013">'
|
||||
'd'
|
||||
'</span><span class="underline color-014">'
|
||||
'!'
|
||||
'</span><span class="underline bgcolor-002 color-014">'
|
||||
'!'
|
||||
'</span>',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,11 +12,10 @@ import re
|
|||
from html import escape as html_escape
|
||||
from .ansi import *
|
||||
|
||||
|
||||
# All xterm256 RGB equivalents
|
||||
|
||||
XTERM256_FG = "\033[38;5;%sm"
|
||||
XTERM256_BG = "\033[48;5;%sm"
|
||||
XTERM256_FG = "\033[38;5;{}m"
|
||||
XTERM256_BG = "\033[48;5;{}m"
|
||||
|
||||
|
||||
class TextToHTMLparser(object):
|
||||
|
|
@ -25,77 +24,65 @@ class TextToHTMLparser(object):
|
|||
"""
|
||||
|
||||
tabstop = 4
|
||||
# mapping html color name <-> ansi code.
|
||||
hilite = ANSI_HILITE
|
||||
unhilite = ANSI_UNHILITE # this will be stripped - there is no css equivalent.
|
||||
normal = ANSI_NORMAL # "
|
||||
underline = ANSI_UNDERLINE
|
||||
blink = ANSI_BLINK
|
||||
inverse = ANSI_INVERSE # this will produce an outline; no obvious css equivalent?
|
||||
colorcodes = [
|
||||
("color-000", unhilite + ANSI_BLACK), # pure black
|
||||
("color-001", unhilite + ANSI_RED),
|
||||
("color-002", unhilite + ANSI_GREEN),
|
||||
("color-003", unhilite + ANSI_YELLOW),
|
||||
("color-004", unhilite + ANSI_BLUE),
|
||||
("color-005", unhilite + ANSI_MAGENTA),
|
||||
("color-006", unhilite + ANSI_CYAN),
|
||||
("color-007", unhilite + ANSI_WHITE), # light grey
|
||||
("color-008", hilite + ANSI_BLACK), # dark grey
|
||||
("color-009", hilite + ANSI_RED),
|
||||
("color-010", hilite + ANSI_GREEN),
|
||||
("color-011", hilite + ANSI_YELLOW),
|
||||
("color-012", hilite + ANSI_BLUE),
|
||||
("color-013", hilite + ANSI_MAGENTA),
|
||||
("color-014", hilite + ANSI_CYAN),
|
||||
("color-015", hilite + ANSI_WHITE), # pure white
|
||||
] + [("color-%03i" % (i + 16), XTERM256_FG % ("%i" % (i + 16))) for i in range(240)]
|
||||
|
||||
colorback = [
|
||||
("bgcolor-000", ANSI_BACK_BLACK), # pure black
|
||||
("bgcolor-001", ANSI_BACK_RED),
|
||||
("bgcolor-002", ANSI_BACK_GREEN),
|
||||
("bgcolor-003", ANSI_BACK_YELLOW),
|
||||
("bgcolor-004", ANSI_BACK_BLUE),
|
||||
("bgcolor-005", ANSI_BACK_MAGENTA),
|
||||
("bgcolor-006", ANSI_BACK_CYAN),
|
||||
("bgcolor-007", ANSI_BACK_WHITE), # light grey
|
||||
("bgcolor-008", hilite + ANSI_BACK_BLACK), # dark grey
|
||||
("bgcolor-009", hilite + ANSI_BACK_RED),
|
||||
("bgcolor-010", hilite + ANSI_BACK_GREEN),
|
||||
("bgcolor-011", hilite + ANSI_BACK_YELLOW),
|
||||
("bgcolor-012", hilite + ANSI_BACK_BLUE),
|
||||
("bgcolor-013", hilite + ANSI_BACK_MAGENTA),
|
||||
("bgcolor-014", hilite + ANSI_BACK_CYAN),
|
||||
("bgcolor-015", hilite + ANSI_BACK_WHITE), # pure white
|
||||
] + [("bgcolor-%03i" % (i + 16), XTERM256_BG % ("%i" % (i + 16))) for i in range(240)]
|
||||
style_codes = [
|
||||
# non-color style markers
|
||||
ANSI_NORMAL,
|
||||
ANSI_UNDERLINE,
|
||||
ANSI_HILITE,
|
||||
ANSI_UNHILITE,
|
||||
ANSI_INVERSE,
|
||||
ANSI_BLINK,
|
||||
ANSI_INV_HILITE,
|
||||
ANSI_BLINK_HILITE,
|
||||
ANSI_INV_BLINK,
|
||||
ANSI_INV_BLINK_HILITE,
|
||||
]
|
||||
|
||||
# make sure to escape [
|
||||
# colorcodes = [(c, code.replace("[", r"\[")) for c, code in colorcodes]
|
||||
# colorback = [(c, code.replace("[", r"\[")) for c, code in colorback]
|
||||
fg_colormap = dict((code, clr) for clr, code in colorcodes)
|
||||
bg_colormap = dict((code, clr) for clr, code in colorback)
|
||||
ansi_color_codes = [
|
||||
# Foreground colors
|
||||
ANSI_BLACK,
|
||||
ANSI_RED,
|
||||
ANSI_GREEN,
|
||||
ANSI_YELLOW,
|
||||
ANSI_BLUE,
|
||||
ANSI_MAGENTA,
|
||||
ANSI_CYAN,
|
||||
ANSI_WHITE,
|
||||
]
|
||||
|
||||
# create stop markers
|
||||
fgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m|\033\[0m|$"
|
||||
bgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m|\033\[0m|$"
|
||||
bgfgstop = bgstop[:-2] + fgstop
|
||||
xterm_fg_codes = [XTERM256_FG.format(i + 16) for i in range(240)]
|
||||
|
||||
fgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m)"
|
||||
bgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m)"
|
||||
bgfgstart = bgstart + r"((?:\033\[1m|\033\[22m){0,1}\033\[[3-4][0-8].*?m){0,1}"
|
||||
ansi_bg_codes = [
|
||||
# Background colors
|
||||
ANSI_BACK_BLACK,
|
||||
ANSI_BACK_RED,
|
||||
ANSI_BACK_GREEN,
|
||||
ANSI_BACK_YELLOW,
|
||||
ANSI_BACK_BLUE,
|
||||
ANSI_BACK_MAGENTA,
|
||||
ANSI_BACK_CYAN,
|
||||
ANSI_BACK_WHITE,
|
||||
]
|
||||
|
||||
# extract color markers, tagging the start marker and the text marked
|
||||
re_fgs = re.compile(fgstart + "(.*?)(?=" + fgstop + ")")
|
||||
re_bgs = re.compile(bgstart + "(.*?)(?=" + bgstop + ")")
|
||||
re_bgfg = re.compile(bgfgstart + "(.*?)(?=" + bgfgstop + ")")
|
||||
xterm_bg_codes = [XTERM256_BG.format(i + 16) for i in range(240)]
|
||||
|
||||
re_style = re.compile(
|
||||
r"({})".format(
|
||||
"|".join(
|
||||
style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes
|
||||
).replace("[", r"\[")
|
||||
)
|
||||
)
|
||||
|
||||
colorlist = (
|
||||
[ANSI_UNHILITE + code for code in ansi_color_codes]
|
||||
+ [ANSI_HILITE + code for code in ansi_color_codes]
|
||||
+ xterm_fg_codes
|
||||
)
|
||||
|
||||
bglist = ansi_bg_codes + [ANSI_HILITE + code for code in ansi_bg_codes] + xterm_bg_codes
|
||||
|
||||
re_normal = re.compile(normal.replace("[", r"\["))
|
||||
re_hilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (hilite.replace("[", r"\["), fgstop, bgstop))
|
||||
re_unhilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (unhilite.replace("[", r"\["), fgstop, bgstop))
|
||||
re_uline = re.compile("(?:%s)(.*?)(?=%s|%s)" % (underline.replace("[", r"\["), fgstop, bgstop))
|
||||
re_blink = re.compile("(?:%s)(.*?)(?=%s|%s)" % (blink.replace("[", r"\["), fgstop, bgstop))
|
||||
re_inverse = re.compile("(?:%s)(.*?)(?=%s|%s)" % (inverse.replace("[", r"\["), fgstop, bgstop))
|
||||
re_string = re.compile(
|
||||
r"(?P<htmlchars>[<&>])|(?P<tab>[\t]+)|(?P<lineend>\r\n|\r|\n)",
|
||||
re.S | re.M | re.I,
|
||||
|
|
@ -106,100 +93,6 @@ class TextToHTMLparser(object):
|
|||
re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
|
||||
re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
|
||||
|
||||
def _sub_bgfg(self, colormatch):
|
||||
# print("colormatch.groups()", colormatch.groups())
|
||||
bgcode, fgcode, text = colormatch.groups()
|
||||
if not fgcode:
|
||||
ret = r"""<span class="%s">%s</span>""" % (
|
||||
self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")),
|
||||
text,
|
||||
)
|
||||
else:
|
||||
ret = r"""<span class="%s"><span class="%s">%s</span></span>""" % (
|
||||
self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")),
|
||||
self.fg_colormap.get(fgcode, self.bg_colormap.get(fgcode, "err")),
|
||||
text,
|
||||
)
|
||||
return ret
|
||||
|
||||
def _sub_fg(self, colormatch):
|
||||
code, text = colormatch.groups()
|
||||
return r"""<span class="%s">%s</span>""" % (self.fg_colormap.get(code, "err"), text)
|
||||
|
||||
def _sub_bg(self, colormatch):
|
||||
code, text = colormatch.groups()
|
||||
return r"""<span class="%s">%s</span>""" % (self.bg_colormap.get(code, "err"), text)
|
||||
|
||||
def re_color(self, text):
|
||||
"""
|
||||
Replace ansi colors with html color class names. Let the
|
||||
client choose how it will display colors, if it wishes to.
|
||||
|
||||
Args:
|
||||
text (str): the string with color to replace.
|
||||
|
||||
Returns:
|
||||
text (str): Re-colored text.
|
||||
|
||||
"""
|
||||
text = self.re_bgfg.sub(self._sub_bgfg, text)
|
||||
text = self.re_fgs.sub(self._sub_fg, text)
|
||||
text = self.re_bgs.sub(self._sub_bg, text)
|
||||
text = self.re_normal.sub("", text)
|
||||
return text
|
||||
|
||||
def re_bold(self, text):
|
||||
"""
|
||||
Clean out superfluous hilights rather than set <strong>to make
|
||||
it match the look of telnet.
|
||||
|
||||
Args:
|
||||
text (str): Text to process.
|
||||
|
||||
Returns:
|
||||
text (str): Processed text.
|
||||
|
||||
"""
|
||||
text = self.re_hilite.sub(r"<strong>\1</strong>", text)
|
||||
return self.re_unhilite.sub(r"\1", text) # strip unhilite - there is no equivalent in css.
|
||||
|
||||
def re_underline(self, text):
|
||||
"""
|
||||
Replace ansi underline with html underline class name.
|
||||
|
||||
Args:
|
||||
text (str): Text to process.
|
||||
|
||||
Returns:
|
||||
text (str): Processed text.
|
||||
|
||||
"""
|
||||
return self.re_uline.sub(r'<span class="underline">\1</span>', text)
|
||||
|
||||
def re_blinking(self, text):
|
||||
"""
|
||||
Replace ansi blink with custom blink css class
|
||||
|
||||
Args:
|
||||
text (str): Text to process.
|
||||
|
||||
Returns:
|
||||
text (str): Processed text.
|
||||
"""
|
||||
return self.re_blink.sub(r'<span class="blink">\1</span>', text)
|
||||
|
||||
def re_inversing(self, text):
|
||||
"""
|
||||
Replace ansi inverse with custom inverse css class
|
||||
|
||||
Args:
|
||||
text (str): Text to process.
|
||||
|
||||
Returns:
|
||||
text (str): Processed text.
|
||||
"""
|
||||
return self.re_inverse.sub(r'<span class="inverse">\1</span>', text)
|
||||
|
||||
def remove_bells(self, text):
|
||||
"""
|
||||
Remove ansi specials
|
||||
|
|
@ -211,7 +104,7 @@ class TextToHTMLparser(object):
|
|||
text (str): Processed text.
|
||||
|
||||
"""
|
||||
return text.replace("\07", "")
|
||||
return text.replace(ANSI_BEEP, "")
|
||||
|
||||
def remove_backspaces(self, text):
|
||||
"""
|
||||
|
|
@ -315,6 +208,128 @@ class TextToHTMLparser(object):
|
|||
return text
|
||||
return None
|
||||
|
||||
def format_styles(self, text):
|
||||
"""
|
||||
Takes a string with parsed ANSI codes and replaces them with
|
||||
HTML spans and CSS classes.
|
||||
|
||||
Args:
|
||||
text (str): The string to process.
|
||||
|
||||
Returns:
|
||||
text (str): Processed text.
|
||||
"""
|
||||
|
||||
# split out the ANSI codes and clean out any empty items
|
||||
str_list = [substr for substr in self.re_style.split(text) if substr]
|
||||
# initialize all the flags and classes
|
||||
classes = []
|
||||
clean = True
|
||||
inverse = False
|
||||
# default color is light grey - unhilite + white
|
||||
hilight = ANSI_UNHILITE
|
||||
fg = ANSI_WHITE
|
||||
# default bg is black
|
||||
bg = ANSI_BACK_BLACK
|
||||
|
||||
for i, substr in enumerate(str_list):
|
||||
# reset all current styling
|
||||
if substr == ANSI_NORMAL and not clean:
|
||||
# replace with close existing tag
|
||||
str_list[i] = "</span>"
|
||||
# reset to defaults
|
||||
classes = []
|
||||
clean = True
|
||||
inverse = False
|
||||
hilight = ANSI_UNHILITE
|
||||
fg = ANSI_WHITE
|
||||
bg = ANSI_BACK_BLACK
|
||||
|
||||
# change color
|
||||
elif substr in self.ansi_color_codes + self.xterm_fg_codes:
|
||||
# erase ANSI code from output
|
||||
str_list[i] = ""
|
||||
# set new color
|
||||
fg = substr
|
||||
|
||||
# change bg color
|
||||
elif substr in self.ansi_bg_codes + self.xterm_bg_codes:
|
||||
# erase ANSI code from output
|
||||
str_list[i] = ""
|
||||
# set new bg
|
||||
bg = substr
|
||||
|
||||
# non-color codes
|
||||
elif substr in self.style_codes:
|
||||
# erase ANSI code from output
|
||||
str_list[i] = ""
|
||||
|
||||
# hilight codes
|
||||
if substr in (ANSI_HILITE, ANSI_UNHILITE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
|
||||
# set new hilight status
|
||||
hilight = ANSI_UNHILITE if substr == ANSI_UNHILITE else ANSI_HILITE
|
||||
|
||||
# inversion codes
|
||||
if substr in (ANSI_INVERSE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
|
||||
inverse = True
|
||||
|
||||
# blink codes
|
||||
if (
|
||||
substr in (ANSI_BLINK, ANSI_BLINK_HILITE, ANSI_INV_BLINK_HILITE)
|
||||
and "blink" not in classes
|
||||
):
|
||||
classes.append("blink")
|
||||
|
||||
# underline
|
||||
if substr == ANSI_UNDERLINE and "underline" not in classes:
|
||||
classes.append("underline")
|
||||
|
||||
else:
|
||||
# normal text, add text back to list
|
||||
if not str_list[i - 1]:
|
||||
# prior entry was cleared, which means style change
|
||||
# get indices for the fg and bg codes
|
||||
bg_index = self.bglist.index(bg)
|
||||
try:
|
||||
color_index = self.colorlist.index(hilight + fg)
|
||||
except ValueError:
|
||||
# xterm256 colors don't have the hilight codes
|
||||
color_index = self.colorlist.index(fg)
|
||||
|
||||
if inverse:
|
||||
# inverse means swap fg and bg indices
|
||||
bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0"))
|
||||
color_class = "color-{}".format(str(bg_index).rjust(3, "0"))
|
||||
else:
|
||||
# use fg and bg indices for classes
|
||||
bg_class = "bgcolor-{}".format(str(bg_index).rjust(3, "0"))
|
||||
color_class = "color-{}".format(str(color_index).rjust(3, "0"))
|
||||
|
||||
# black bg is the default, don't explicitly style
|
||||
if bg_class != "bgcolor-000":
|
||||
classes.append(bg_class)
|
||||
# light grey text is the default, don't explicitly style
|
||||
if color_class != "color-007":
|
||||
classes.append(color_class)
|
||||
# define the new style span
|
||||
prefix = '<span class="{}">'.format(" ".join(classes))
|
||||
# close any prior span
|
||||
if not clean:
|
||||
prefix = "</span>" + prefix
|
||||
# add span to output
|
||||
str_list[i - 1] = prefix
|
||||
|
||||
# clean out color classes to easily update next time
|
||||
classes = [cls for cls in classes if "color" not in cls]
|
||||
# flag as currently being styled
|
||||
clean = False
|
||||
|
||||
# close span if necessary
|
||||
if not clean:
|
||||
str_list.append("</span>")
|
||||
# recombine back into string
|
||||
return "".join(str_list)
|
||||
|
||||
def parse(self, text, strip_ansi=False):
|
||||
"""
|
||||
Main access function, converts a text containing ANSI codes
|
||||
|
|
@ -328,19 +343,14 @@ class TextToHTMLparser(object):
|
|||
text (str): Parsed text.
|
||||
|
||||
"""
|
||||
# print(f"incoming text:\n{text}")
|
||||
# parse everything to ansi first
|
||||
text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True)
|
||||
# convert all ansi to html
|
||||
result = re.sub(self.re_string, self.sub_text, text)
|
||||
result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
|
||||
result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result)
|
||||
result = self.re_color(result)
|
||||
result = self.re_bold(result)
|
||||
result = self.re_underline(result)
|
||||
result = self.re_blinking(result)
|
||||
result = self.re_inversing(result)
|
||||
result = self.remove_bells(result)
|
||||
result = self.format_styles(result)
|
||||
result = self.convert_linebreaks(result)
|
||||
result = self.remove_backspaces(result)
|
||||
result = self.convert_urls(result)
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ An "emitter" object must have a function
|
|||
// kwargs (obj): keyword-args to listener
|
||||
//
|
||||
emit: function (cmdname, args, kwargs) {
|
||||
if (kwargs.cmdid) {
|
||||
if (kwargs.cmdid && (kwargs.cmdid in cmdmap)) {
|
||||
cmdmap[kwargs.cmdid].apply(this, [args, kwargs]);
|
||||
delete cmdmap[kwargs.cmdid];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue