mirror of
https://github.com/evennia/evennia.git
synced 2026-03-23 08:16:30 +01:00
Merge branch 'GulliblePsychologist-argparser' into develop
This commit is contained in:
commit
cc352f7723
38 changed files with 12246 additions and 1728 deletions
|
|
@ -40,6 +40,10 @@
|
|||
code style and paradigms instead of relying on `Scripts` for everything.
|
||||
- Expand `CommandTest` with ability to check multipler msg-receivers; inspired by PR by
|
||||
user davewiththenicehat. Also add new doc string.
|
||||
- Add central `FuncParser` as a much more powerful replacement for the old `parse_inlinefunc`
|
||||
function.
|
||||
- Add `evennia/utils/verb_conjugation` for automatic verb conjugation (English only). This
|
||||
is useful for implementing actor-stance emoting for sending a string to different targets.
|
||||
|
||||
### Evennia 0.9.5 (2019-2020)
|
||||
|
||||
|
|
|
|||
382
docs/source/Components/FuncParser.md
Normal file
382
docs/source/Components/FuncParser.md
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
# The Inline Function Parser
|
||||
|
||||
The [FuncParser](api:evennia.utils.funcparser#evennia.utils.funcparser.FuncParser) extracts and executes
|
||||
'inline functions'
|
||||
embedded in a string on the form `$funcname(args, kwargs)`. Under the hood, this will
|
||||
lead to a call to a Python function you control. The inline function call will be replaced by
|
||||
the return from the function.
|
||||
|
||||
```python
|
||||
from evennia.utils.funcparser import FuncParser
|
||||
|
||||
def _power_callable(*args, **kwargs):
|
||||
"""This will be callable as $square(number, power=<num>) in string"""
|
||||
pow = int(kwargs.get('power', 2))
|
||||
return float(args[0]) ** pow
|
||||
|
||||
parser = FuncParser({"pow": _power_callable})
|
||||
|
||||
```
|
||||
Next, just pass a string into the parser, optionally containing `$func(...)` markers:
|
||||
|
||||
```python
|
||||
parser.parse("We have that 4 x 4 x 4 is $pow(4, power=3).")
|
||||
"We have that 4 x 4 x 4 is 64."
|
||||
```
|
||||
|
||||
Normally the return is always converted to a string but you can also get the actual data type from the call:
|
||||
|
||||
```python
|
||||
parser.parse_to_any("$pow(4)")
|
||||
16
|
||||
```
|
||||
|
||||
To show a `$func()` verbatim in your code without parsing it, escape it as either `$$func()` or `\$func()`:
|
||||
|
||||
|
||||
```python
|
||||
parser.parse("This is an escaped $$pow(4) and so is this \$pow(3)")
|
||||
"This is an escaped $pow(4) and so is this $pow(3)"
|
||||
```
|
||||
|
||||
## Uses in default Evennia
|
||||
|
||||
The FuncParser can be applied to any string. Out of the box it's applied in a few situations:
|
||||
|
||||
- _Outgoing messages_. All messages sent from the server is processed through FuncParser and every
|
||||
callable is provided the [Session](./Sessions) of the object receiving the message. This potentially
|
||||
allows a message to be modified on the fly to look different for different recipients.
|
||||
- _Prototype values_. A [Prototype](./Prototypes) dict's values are run through the parser such that every
|
||||
callable gets a reference to the rest of the prototype. In the Prototype ORM, this would allow builders
|
||||
to safely call functions to set non-string values to prototype values, get random values, reference
|
||||
other fields of the prototype, and more.
|
||||
- _Actor-stance in messages to others_. In the
|
||||
[Object.msg_contents](api:evennia.objects.objects#DefaultObject.msg_contents) method,
|
||||
the outgoing string is parsed for special `$You()` and `$conj()` callables to decide if a given recipient
|
||||
should see "You" or the character's name.
|
||||
|
||||
```important::
|
||||
The inline-function parser is not intended as a 'softcode' programming language. It does not
|
||||
have things like loops and conditionals, for example. While you could in principle extend it to
|
||||
do very advanced things and allow builders a lot of power, all-out coding is something
|
||||
Evennia expects you to do in a proper text editor, outside of the game, not from inside it.
|
||||
```
|
||||
|
||||
## Using the FuncParser
|
||||
|
||||
You can apply inline function parsing to any string. The
|
||||
[FuncParser](api:evennia.utils.funcparser.FuncParser) is found in `evennia.utils.funcparser.py`.
|
||||
|
||||
```python
|
||||
from evennia.utils import funcparser
|
||||
|
||||
parser = FuncParser(callables, **default_kwargs)
|
||||
parsed_string = parser.parser(input_string, raise_errors=False,
|
||||
escape=False, strip=False,
|
||||
return_str=True, **reserved_kwargs)
|
||||
|
||||
# callables can also be passed as paths to modules
|
||||
parser = FuncParser(["game.myfuncparser_callables", "game.more_funcparser_callables"])
|
||||
```
|
||||
|
||||
Here, `callables` points to a collection of normal Python functions (see next section) for you to make
|
||||
available to the parser as you parse strings with it. It can either be
|
||||
- A `dict` of `{"functionname": callable, ...}`. This allows you do pick and choose exactly which callables
|
||||
to include and how they should be named. Do you want a callable to be available under more than one name?
|
||||
Just add it multiple times to the dict, with a different key.
|
||||
- A `module` or (more commonly) a `python-path` to a module. This module can define a dict
|
||||
`FUNCPARSER_CALLABLES = {"funcname": callable, ...}` - this will be imported and used like ther `dict` above.
|
||||
If no such variable is defined, _every_ top-level function in the module (whose name doesn't start with
|
||||
an underscore `_`) will be considered a suitable callable. The name of the function will be the `$funcname`
|
||||
by which it can be called.
|
||||
- A `list` of modules/paths. This allows you to pull in modules from many sources for your parsing.
|
||||
|
||||
The other arguments to the parser:
|
||||
|
||||
- `raise_errors` - By default, any errors from a callable will be quietly ignored and the result
|
||||
will be that the failing function call will show verbatim. If `raise_errors` is set,
|
||||
then parsing will stop and whatever exception happened will be raised. It'd be up to you to handle
|
||||
this properly.
|
||||
- `escape` - Returns a string where every `$func(...)` has been escaped as `\$func()`.
|
||||
- `strip` - Remove all `$func(...)` calls from string (as if each returned `''`).
|
||||
- `return_str` - When `True` (default), `parser` always returns a string. If `False`, it may return
|
||||
the return value of a single function call in the string. This is the same as using the `.parse_to_any`
|
||||
method.
|
||||
- The `**default/reserved_keywords` are optional and allow you to pass custom data into _every_ function
|
||||
call. This is great for including things like the current session or config options. Defaults can be
|
||||
replaced if the user gives the same-named kwarg in the string's function call. Reserved kwargs are always passed,
|
||||
ignoring defaults or what the user passed. In addition, the `funcparser` and `raise_errors`
|
||||
reserved kwargs are always passed - the first is a back-reference to the `FuncParser` instance and the second
|
||||
is the `raise_errors` boolean passed into `FuncParser.parse`.
|
||||
|
||||
Here's an example of using the default/reserved keywords:
|
||||
|
||||
```python
|
||||
def _test(*args, **kwargs):
|
||||
# do stuff
|
||||
return something
|
||||
|
||||
parser = funcparser.FuncParser({"test": _test}, mydefault=2)
|
||||
result = parser.parse("$test(foo, bar=4)", myreserved=[1, 2, 3])
|
||||
```
|
||||
Here the callable will be called as
|
||||
|
||||
```python
|
||||
_test('foo', bar='4', mydefault=2, myreserved=[1, 2, 3],
|
||||
funcparser=<FuncParser>, raise_errrors=False)
|
||||
```
|
||||
|
||||
The `mydefault=2` kwarg could be overwritten if we made the call as `$test(mydefault=...)`
|
||||
but `myreserved=[1, 2, 3]` will _always_ be sent as-is and will override a call `$test(myreserved=...)`.
|
||||
The `funcparser`/`raise_errors` kwargs are also always included as reserved kwargs.
|
||||
|
||||
## Defining custom callables
|
||||
|
||||
All callables made available to the parser must have the following signature:
|
||||
|
||||
```python
|
||||
def funcname(*args, **kwargs):
|
||||
# ...
|
||||
return something
|
||||
```
|
||||
|
||||
> The `*args` and `**kwargs` must always be included. If you are unsure how `*args` and `**kwargs` work in Python,
|
||||
> [read about them here](https://www.digitalocean.com/community/tutorials/how-to-use-args-and-kwargs-in-python-3).
|
||||
|
||||
The input from the innermost `$funcname(...)` call in your callable will always be a `str`. Here's
|
||||
an example of an `$toint` function; it converts numbers to integers.
|
||||
|
||||
"There's a $toint(22.0)% chance of survival."
|
||||
|
||||
What will enter the `$toint` callable (as `args[0]`) is the _string_ `"22.0"`. The function is responsible
|
||||
for converting this to a number so that we can convert it to an integer. We must also properly handle invalid
|
||||
inputs (like non-numbers).
|
||||
|
||||
If you want to mark an error, raise `evennia.utils.funcparser.ParsingError`. This stops the entire parsing
|
||||
of the string and may or may not raise the exception depending on what you set `raise_errors` to when you
|
||||
created the parser.
|
||||
|
||||
However, if you _nest_ functions, the return of the innermost function may be something other than
|
||||
a string. Let's introduce the `$eval` function, which evaluates simple expressions using
|
||||
Python's `literal_eval` and/or `simple_eval`.
|
||||
|
||||
"There's a $toint($eval(10 * 2.2))% chance of survival."
|
||||
|
||||
Since the `$eval` is the innermost call, it will get a string as input - the string `"10 * 2.2"`.
|
||||
It evaluates this and returns the `float` `22.0`. This time the outermost `$toint` will be called with
|
||||
this `float` instead of with a string.
|
||||
|
||||
> It's important to safely validate your inputs since users may end up nesting your callables in any order.
|
||||
> See the next section for useful tools to help with this.
|
||||
|
||||
In these examples, the result will be embedded in the larger string, so the result of the entire parsing
|
||||
will be a string:
|
||||
|
||||
```python
|
||||
parser.parse(above_string)
|
||||
"There's a 22% chance of survival."
|
||||
```
|
||||
|
||||
However, if you use the `parse_to_any` (or `parse(..., return_str=True)`) and _don't add any extra string around the outermost function call_,
|
||||
you'll get the return type of the outermost callable back:
|
||||
|
||||
```python
|
||||
parser.parse_to_any("$toint($eval(10 * 2.2)%")
|
||||
"22%"
|
||||
parser.parse_to_any("$toint($eval(10 * 2.2)")
|
||||
22
|
||||
```
|
||||
|
||||
### Safe convertion of inputs
|
||||
|
||||
Since you don't know in which order users may use your callables, they should always check the types
|
||||
of its inputs and convert to the type the callable needs. Note also that when converting from strings,
|
||||
there are limits what inputs you can support. This is because FunctionParser strings are often used by
|
||||
non-developer players/builders and some things (such as complex classes/callables etc) are just not
|
||||
safe/possible to convert from string representation.
|
||||
|
||||
In `evennia.utils.utils` is a helper called
|
||||
[safe_convert_to_types](api:evennia.utils.utils#evennia.utils.utils.safe_convert_to_types). This function
|
||||
automates the conversion of simple data types in a safe way:
|
||||
|
||||
```python
|
||||
from evennia.utils.utils import safe_convert_to_types
|
||||
|
||||
def _process_callable(*args, **kwargs):
|
||||
"""
|
||||
A callable with a lot of custom options
|
||||
|
||||
$process(expression, local, extra=34, extra2=foo)
|
||||
|
||||
"""
|
||||
args, kwargs = safe_convert_to_type(
|
||||
(('py', 'py'), {'extra1': int, 'extra2': str}),
|
||||
*args, **kwargs)
|
||||
|
||||
# args/kwargs should be correct types now
|
||||
|
||||
```
|
||||
|
||||
In other words,
|
||||
|
||||
```python
|
||||
args, kwargs = safe_convert_to_type(
|
||||
(tuple_of_arg_converters, dict_of_kwarg_converters), *args, **kwargs)
|
||||
```
|
||||
|
||||
Each converter should be a callable taking one argument - this will be the arg/kwarg-value to convert. The
|
||||
special converter `"py"` will try to convert a string argument to a Python structure with the help of the
|
||||
following tools (which you may also find useful to experiment with on your own):
|
||||
|
||||
- [ast.literal_eval](https://docs.python.org/3.8/library/ast.html#ast.literal_eval) is an in-built Python
|
||||
function. It
|
||||
_only_ supports strings, bytes, numbers, tuples, lists, dicts, sets, booleans and `None`. That's
|
||||
it - no arithmetic or modifications of data is allowed. This is good for converting individual values and
|
||||
lists/dicts from the input line to real Python objects.
|
||||
- [simpleeval](https://pypi.org/project/simpleeval/) is a third-party tool included with Evennia. This
|
||||
allows for evaluation of simple (and thus safe) expressions. One can operate on numbers and strings
|
||||
with +-/* as well as do simple comparisons like `4 > 3` and more. It does _not_ accept more complex
|
||||
containers like lists/dicts etc, so this and `literal_eval` are complementary to each other.
|
||||
|
||||
```warning::
|
||||
It may be tempting to run use Python's in-built ``eval()`` or ``exec()`` functions as converters since
|
||||
these are able to convert any valid Python source code to Python. NEVER DO THIS unless you really, really
|
||||
know that ONLY developers will ever modify the string going into the callable. The parser is intended
|
||||
for untrusted users (if you were trusted you'd have access to Python already). Letting untrusted users
|
||||
pass strings to ``eval``/``exec`` is a MAJOR security risk. It allows the caller to run arbitrary
|
||||
Python code on your server. This is the path to maliciously deleted hard drives. Just don't do it and
|
||||
sleep better at night.
|
||||
```
|
||||
|
||||
## Default callables
|
||||
|
||||
These are some example callables you can import and add your parser. They are divided into
|
||||
global-level dicts in `evennia.utils.funcparser`. Just import the dict(s) and merge/add one or
|
||||
more to them when you create your `FuncParser` instance to have those callables be available.
|
||||
|
||||
### `evennia.utils.funcparser.FUNCPARSER_CALLABLES`
|
||||
|
||||
These are the 'base' callables.
|
||||
|
||||
- `$eval(expression)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_eval)) -
|
||||
this uses `literal_eval` and `simple_eval` (see previous section) attemt to convert a string expression
|
||||
to a python object. This handles e.g. lists of literals `[1, 2, 3]` and simple expressions like `"1 + 2"`.
|
||||
- `$toint(number)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_toint)) -
|
||||
always converts an output to an integer, if possible.
|
||||
- `$add/sub/mult/div(obj1, obj2)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_add)) -
|
||||
this adds/subtracts/multiplies and divides to elements together. While simple addition could be done with
|
||||
`$eval`, this could for example be used also to add two lists together, which is not possible with `eval`;
|
||||
for example `$add($eval([1,2,3]), $eval([4,5,6])) -> [1, 2, 3, 4, 5, 6]`.
|
||||
- `$round(float, significant)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_round)) -
|
||||
rounds an input float into the number of provided significant digits. For example `$round(3.54343, 3) -> 3.543`.
|
||||
- `$random([start, [end]])` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_random)) -
|
||||
this works like the Python `random()` function, but will randomize to an integer value if both start/end are
|
||||
integers. Without argument, will return a float between 0 and 1.
|
||||
- `$randint([start, [end]])` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_randint)) -
|
||||
works like the `randint()` python function and always returns an integer.
|
||||
- `$choice(list)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_choice)) -
|
||||
the input will automatically be parsed the same way as `$eval` and is expected to be an iterable. A random
|
||||
element of this list will be returned.
|
||||
- `$pad(text[, width, align, fillchar])` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_pad)) -
|
||||
this will pad content. `$pad("Hello", 30, c, -)` will lead to a text centered in a 30-wide block surrounded by `-`
|
||||
characters.
|
||||
- `$crop(text, width=78, suffix='[...]')` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_crop)) -
|
||||
this will crop a text longer than the width, by default ending it with a `[...]`-suffix that also fits within
|
||||
the width. If no width is given, the client width or `settings.DEFAULT_CLIENT_WIDTH` will be used.
|
||||
- `$space(num)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_space)) -
|
||||
this will insert `num` spaces.
|
||||
- `$just(string, width=40, align=c, indent=2)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_justify)) -
|
||||
justifies the text to a given width, aligning it left/right/center or 'f' for full (spread text across width).
|
||||
- `$ljust` - shortcut to justify-left. Takes all other kwarg of `$just`.
|
||||
- `$rjust` - shortcut to right justify.
|
||||
- `$cjust` - shortcut to center justify.
|
||||
- `$clr(startcolor, text[, endcolor])` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_clr)) -
|
||||
color text. The color is given with one or two characters without the preceeding `|`. If no endcolor is
|
||||
given, the string will go back to neutral, so `$clr(r, Hello)` is equivalent to `|rHello|n`.
|
||||
|
||||
### `evennia.utils.funcparser.SEARCHING_CALLABLES`
|
||||
|
||||
These are callables that requires access-checks in order to search for objects. So they require some
|
||||
extra reserved kwargs to be passed when running the parser:
|
||||
|
||||
```python
|
||||
parser.parse_to_any(string, caller=<object or account>, access="control", ...)`
|
||||
|
||||
```
|
||||
The `caller` is required, it's the the object to do the access-check for. The `access` kwarg is the
|
||||
[lock type](./Locks) to check, default being `"control"`.
|
||||
|
||||
- `$search(query,type=account|script,return_list=False)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_search)) -
|
||||
this will look up and try to match an object by key or alias. Use the `type` kwarg to
|
||||
search for `account` or `script` instead. By default this will return nothing if there are more than one
|
||||
match; if `return_list` is `True` a list of 0, 1 or more matches will be returned instead.
|
||||
- `$obj(query)`, `$dbref(query)` - legacy aliases for `$search`.
|
||||
- `$objlist(query)` - legacy alias for `$search`, always returning a list.
|
||||
|
||||
|
||||
### `evennia.utils.funcparser.ACTOR_STANCE_CALLABLES`
|
||||
|
||||
These are used to implement actor-stance emoting. They are used by the
|
||||
[DefaultObject.msg_contents](api:evennia.objects.objects#evennia.objects.objects.DefaultObject.msg_contents) method
|
||||
by default.
|
||||
|
||||
These all require extra kwargs be passed into the parser:
|
||||
|
||||
```python
|
||||
parser.parse(string, caller=<obj>, receiver=<obj>, mapping={'key': <obj>, ...})
|
||||
```
|
||||
|
||||
Here the `caller` is the one sending the message and `receiver` the one to see it. The `mapping` contains
|
||||
references to other objects accessible via these callables.
|
||||
|
||||
- `$you([key])` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_you)) -
|
||||
if no `key` is given, this represents the `caller`, otherwise an object from `mapping`
|
||||
will be used. As this message is sent to different recipients, the `receiver` will change and this will
|
||||
be replaced either with the string `you` (if you and the receiver is the same entity) or with the
|
||||
result of `you_obj.get_display_name(looker=receiver)`. This allows for a single string to echo differently
|
||||
depending on who sees it, and also to reference other people in the same way.
|
||||
- `$You([key])` - same as `$you` but always capitalized.
|
||||
- `$conj(verb)` ([code](api:evennia.utils.funcparser#evennia.utils.funcparser.funcparser_callable_conjugate)) -- conjugates a verb between 4nd person presens to 3rd person presence depending on who
|
||||
sees the string. For example `"$You() $conj(smiles)".` will show as "You smile." and "Tom smiles." depending
|
||||
on who sees it. This makes use of the tools in [evennia.utils.verb_conjugation](api:evennia.utils.verb_conjugation)
|
||||
to do this, and only works for English verbs.
|
||||
|
||||
### Example
|
||||
|
||||
Here's an example of including the default callables together with two custom ones.
|
||||
|
||||
```python
|
||||
from evennia.utils import funcparser
|
||||
from evennia.utils import gametime
|
||||
|
||||
def _dashline(*args, **kwargs):
|
||||
if args:
|
||||
return f"\n-------- {args[0]} --------"
|
||||
return ''
|
||||
|
||||
def _uptime(*args, **kwargs):
|
||||
return gametime.uptime()
|
||||
|
||||
callables = {
|
||||
"dashline": _dashline,
|
||||
"uptime": _uptime,
|
||||
**funcparser.FUNCPARSER_CALLABLES,
|
||||
**funcparser.ACTOR_STANCE_CALLABLES,
|
||||
**funcparser.SEARCHING_CALLABLES
|
||||
}
|
||||
|
||||
parser = funcparser.FuncParser(callables)
|
||||
|
||||
string = "This is the current uptime:$dashline($toint($uptime()) seconds)"
|
||||
result = parser.parse(string)
|
||||
|
||||
```
|
||||
|
||||
Above we define two callables `_dashline` and `_uptime` and map them to names `"dashline"` and `"uptime"`,
|
||||
which is what we then can call as `$header` and `$uptime` in the string. We also have access to
|
||||
all the defaults (like `$toint()`).
|
||||
|
||||
The parsed result of the above would be something like this:
|
||||
|
||||
This is the current uptime:
|
||||
------- 343 seconds -------
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
Whether due to abuse, blatant breaking of your rules, or some other reason, you will eventually find
|
||||
no other recourse but to kick out a particularly troublesome player. The default command set has
|
||||
admin tools to handle this, primarily `@ban`, `@unban`, and `@boot`.
|
||||
admin tools to handle this, primarily `ban`, `unban`, and `boot`.
|
||||
|
||||
## Creating a ban
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ have tried to be nice. Now you just want this troll gone.
|
|||
|
||||
The easiest recourse is to block the account YouSuck from ever connecting again.
|
||||
|
||||
@ban YouSuck
|
||||
ban YouSuck
|
||||
|
||||
This will lock the name YouSuck (as well as 'yousuck' and any other capitalization combination), and
|
||||
next time they try to log in with this name the server will not let them!
|
||||
|
|
@ -24,12 +24,12 @@ next time they try to log in with this name the server will not let them!
|
|||
You can also give a reason so you remember later why this was a good thing (the banned account will
|
||||
never see this)
|
||||
|
||||
@ban YouSuck:This is just a troll.
|
||||
ban YouSuck:This is just a troll.
|
||||
|
||||
If you are sure this is just a spam account, you might even consider deleting the player account
|
||||
outright:
|
||||
|
||||
@delaccount YouSuck
|
||||
accounts/delete YouSuck
|
||||
|
||||
Generally, banning the name is the easier and safer way to stop the use of an account -- if you
|
||||
change your mind you can always remove the block later whereas a deletion is permanent.
|
||||
|
|
@ -49,7 +49,7 @@ the `who` command, which will show you something like this:
|
|||
The "Host" bit is the IP address from which the account is connecting. Use this to define the ban
|
||||
instead of the name:
|
||||
|
||||
@ban 237.333.0.223
|
||||
ban 237.333.0.223
|
||||
|
||||
This will stop YouSuckMore connecting from their computer. Note however that IP address might change
|
||||
easily - either due to how the player's Internet Service Provider operates or by the user simply
|
||||
|
|
@ -58,7 +58,7 @@ groups of three digits in the address. So if you figure out that !YouSuckMore ma
|
|||
237.333.0.223, 237.333.0.225, and 237.333.0.256 (only changes in their subnet), it might be an idea
|
||||
to put down a ban like this to include any number in that subnet:
|
||||
|
||||
@ban 237.333.0.*
|
||||
ban 237.333.0.*
|
||||
|
||||
You should combine the IP ban with a name-ban too of course, so the account YouSuckMore is truly
|
||||
locked regardless of where they connect from.
|
||||
|
|
@ -71,16 +71,16 @@ blocking out innocent players who just happen to connect from the same subnet as
|
|||
YouSuck is not really noticing all this banning yet though - and won't until having logged out and
|
||||
trying to log back in again. Let's help the troll along.
|
||||
|
||||
@boot YouSuck
|
||||
boot YouSuck
|
||||
|
||||
Good riddance. You can give a reason for booting too (to be echoed to the player before getting
|
||||
kicked out).
|
||||
|
||||
@boot YouSuck:Go troll somewhere else.
|
||||
boot YouSuck:Go troll somewhere else.
|
||||
|
||||
### Lifting a ban
|
||||
|
||||
Use the `@unban` (or `@ban`) command without any arguments and you will see a list of all currently
|
||||
Use the `unban` (or `ban`) command without any arguments and you will see a list of all currently
|
||||
active bans:
|
||||
|
||||
Active bans
|
||||
|
|
@ -90,7 +90,7 @@ active bans:
|
|||
|
||||
Use the `id` from this list to find out which ban to lift.
|
||||
|
||||
@unban 2
|
||||
unban 2
|
||||
|
||||
Cleared ban 2: 237.333.0.*
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ case) the lock to fail.
|
|||
- **type thomas = FlowerPot** -- Turn an annoying player into a flower pot (assuming you have a
|
||||
`FlowerPot` typeclass ready)
|
||||
- **userpassword thomas = fooBarFoo** -- Change a user's password
|
||||
- **delaccount thomas** -- Delete a player account (not recommended, use **ban** instead)
|
||||
- **accounts/delete thomas** -- Delete a player account (not recommended, use **ban** instead)
|
||||
|
||||
- **server** -- Show server statistics, such as CPU load, memory usage, and how many objects are
|
||||
cached
|
||||
|
|
@ -141,4 +141,4 @@ cached
|
|||
- **reset** -- Restarts the server, kicking all connections
|
||||
- **shutdown** -- Stops the server cold without it auto-starting again
|
||||
- **py** -- Executes raw Python code, allows for direct inspection of the database and account
|
||||
objects on the fly. For advanced users.
|
||||
objects on the fly. For advanced users.
|
||||
|
|
|
|||
21
docs/source/Concepts/Clickable-Links.md
Normal file
21
docs/source/Concepts/Clickable-Links.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
## Clickable links
|
||||
|
||||
Evennia supports clickable links for clients that supports it. This marks certain text so it can be
|
||||
clicked by a mouse and trigger a given Evennia command. To support clickable links, Evennia requires
|
||||
the webclient or an third-party telnet client with [MXP](http://www.zuggsoft.com/zmud/mxp.htm)
|
||||
support (*Note: Evennia only supports clickable links, no other MXP features*).
|
||||
|
||||
- `|lc` to start the link, by defining the command to execute.
|
||||
- `|lt` to continue with the text to show to the user (the link text).
|
||||
- `|le` to end the link text and the link definition.
|
||||
|
||||
All elements must appear in exactly this order to make a valid link. For example,
|
||||
|
||||
```
|
||||
"If you go |lcnorth|ltto the north|le you will find a cottage."
|
||||
```
|
||||
|
||||
This will display as "If you go __to the north__ you will find a cottage." where clicking the link
|
||||
will execute the command `north`. If the client does not support clickable links, only the link text
|
||||
will be shown.
|
||||
|
||||
183
docs/source/Concepts/Colors.md
Normal file
183
docs/source/Concepts/Colors.md
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
# Colors
|
||||
|
||||
*Note that the Documentation does not display colour the way it would look on the screen.*
|
||||
|
||||
Color can be a very useful tool for your game. It can be used to increase readability and make your
|
||||
game more appealing visually.
|
||||
|
||||
Remember however that, with the exception of the webclient, you generally don't control the client
|
||||
used to connect to the game. There is, for example, one special tag meaning "yellow". But exactly
|
||||
*which* hue of yellow is actually displayed on the user's screen depends on the settings of their
|
||||
particular mud client. They could even swap the colours around or turn them off altogether if so
|
||||
desired. Some clients don't even support color - text games are also played with special reading
|
||||
equipment by people who are blind or have otherwise diminished eyesight.
|
||||
|
||||
So a good rule of thumb is to use colour to enhance your game but don't *rely* on it to display
|
||||
critical information. If you are coding the game, you can add functionality to let users disable
|
||||
colours as they please, as described [here](../Howto/Manually-Configuring-Color).
|
||||
|
||||
To see which colours your client support, use the default `@color` command. This will list all
|
||||
available colours for ANSI and Xterm256 along with the codes you use for them. You can find a list
|
||||
of all the parsed `ANSI`-colour codes in `evennia/utils/ansi.py`.
|
||||
|
||||
### ANSI colours
|
||||
|
||||
Evennia supports the `ANSI` standard for text. This is by far the most supported MUD-color standard,
|
||||
available in all but the most ancient mud clients. The ANSI colours are **r**ed, **g**reen,
|
||||
**y**ellow, **b**lue, **m**agenta, **c**yan, **w**hite and black. They are abbreviated by their
|
||||
first letter except for black which is abbreviated with the letter **x**. In ANSI there are "bright"
|
||||
and "normal" (darker) versions of each color, adding up to a total of 16 colours to use for
|
||||
foreground text. There are also 8 "background" colours. These have no bright alternative in ANSI
|
||||
(but Evennia uses the [Xterm256](./TextTags#xterm256-colours) extension behind the scenes to offer
|
||||
them anyway).
|
||||
|
||||
To colour your text you put special tags in it. Evennia will parse these and convert them to the
|
||||
correct markup for the client used. If the user's client/console/display supports ANSI colour, they
|
||||
will see the text in the specified colour, otherwise the tags will be stripped (uncolored text).
|
||||
This works also for non-terminal clients, such as the webclient. For the webclient, Evennia will
|
||||
translate the codes to HTML RGB colors.
|
||||
|
||||
Here is an example of the tags in action:
|
||||
|
||||
|rThis text is bright red.|n This is normal text.
|
||||
|RThis is a dark red text.|n This is normal text.
|
||||
|[rThis text has red background.|n This is normal text.
|
||||
|b|[yThis is bright blue text on yellow background.|n This is normal text.
|
||||
|
||||
- `|n` - this tag will turn off all color formatting, including background colors.
|
||||
- `|#`- markup marks the start of foreground color. The case defines if the text is "bright" or
|
||||
"normal". So `|g` is a bright green and `|G` is "normal" (darker) green.
|
||||
- `|[#` is used to add a background colour to the text. The case again specifies if it is "bright"
|
||||
or "normal", so `|[c` starts a bright cyan background and `|[C` a darker cyan background.
|
||||
- `|!#` is used to add foreground color without any enforced brightness/normal information.
|
||||
These are normal-intensity and are thus always given as uppercase, such as
|
||||
`|!R` for red. The difference between e.g. `|!R` and `|R` is that
|
||||
`|!R` will "inherit" the brightness setting from previously set color tags, whereas `|R` will
|
||||
always reset to the normal-intensity red. The `|#` format contains an implicit `|h`/`|H` tag in it:
|
||||
disabling highlighting when switching to a normal color, and enabling it for bright ones. So `|btest
|
||||
|!Rtest2` will result in a bright red `test2` since the brightness setting from `|b` "bleeds over".
|
||||
You could use this to for example quickly switch the intensity of a multitude of color tags. There
|
||||
is no background-color equivalent to `|!` style tags.
|
||||
- `|h` is used to make any following foreground ANSI colors bright (it has no effect on Xterm
|
||||
colors). This is only relevant to use with `|!` type tags and will be valid until the next `|n`,
|
||||
`|H` or normal (upper-case) `|#` tag. This tag will never affect background colors, those have to be
|
||||
set bright/normal explicitly. Technically, `|h|!G` is identical to `|g`.
|
||||
- `|H` negates the effects `|h` and returns all ANSI foreground colors (`|!` and `|` types) to
|
||||
'normal' intensity. It has no effect on background and Xterm colors.
|
||||
|
||||
> Note: The ANSI standard does not actually support bright backgrounds like `|[r` - the standard
|
||||
only supports "normal" intensity backgrounds. To get around this Evennia instead implements these
|
||||
as [Xterm256 colours](./TextTags#xterm256-colours) behind the scenes. If the client does not support
|
||||
Xterm256 the ANSI colors will be used instead and there will be no visible difference between using
|
||||
upper- and lower-case background tags.
|
||||
|
||||
If you want to display an ANSI marker as output text (without having any effect), you need to escape
|
||||
it by preceding its `|` with another `|`:
|
||||
|
||||
```
|
||||
say The ||r ANSI marker changes text color to bright red.
|
||||
```
|
||||
|
||||
This will output the raw `|r` without any color change. This can also be necessary if you are doing
|
||||
ansi art that uses `|` with a letter directly following it.
|
||||
|
||||
Use the command
|
||||
|
||||
@color ansi
|
||||
|
||||
to get a list of all supported ANSI colours and the tags used to produce them.
|
||||
|
||||
A few additional ANSI codes are supported:
|
||||
|
||||
- `|/` A line break. You cannot put the normal Python `\n` line breaks in text entered inside the
|
||||
game (Evennia will filter this for security reasons). This is what you use instead: use the `|/`
|
||||
marker to format text with line breaks from the game command line.
|
||||
- `` This will translate into a `TAB` character. This will not always show (or show differently) to
|
||||
the client since it depends on their local settings. It's often better to use multiple spaces.
|
||||
- `|_` This is a space. You can usually use the normal space character, but if the space is *at the
|
||||
end of the line*, Evennia will likely crop it. This tag will not be cropped but always result in a
|
||||
space.
|
||||
- `|*` This will invert the current text/background colours. Can be useful to mark things (but see
|
||||
below).
|
||||
|
||||
##### Caveats of `|*`
|
||||
|
||||
The `|*` tag (inverse video) is an old ANSI standard and should usually not be used for more than to
|
||||
mark short snippets of text. If combined with other tags it comes with a series of potentially
|
||||
confusing behaviors:
|
||||
|
||||
* The `|*` tag will only work once in a row:, ie: after using it once it won't have an effect again
|
||||
until you declare another tag. This is an example:
|
||||
|
||||
```
|
||||
Normal text, |*reversed text|*, still reversed text.
|
||||
```
|
||||
|
||||
that is, it will not reverse to normal at the second `|*`. You need to reset it manually:
|
||||
|
||||
```
|
||||
Normal text, |*reversed text|n, normal again.
|
||||
```
|
||||
|
||||
* The `|*` tag does not take "bright" colors into account:
|
||||
|
||||
```
|
||||
|RNormal red, |hnow brightened. |*BG is normal red.
|
||||
```
|
||||
|
||||
So `|*` only considers the 'true' foreground color, ignoring any highlighting. Think of the bright
|
||||
state (`|h`) as something like like `<strong>` in HTML: it modifies the _appearance_ of a normal
|
||||
foreground color to match its bright counterpart, without changing its normal color.
|
||||
* Finally, after a `|*`, if the previous background was set to a dark color (via `|[`), `|!#`) will
|
||||
actually change the background color instead of the foreground:
|
||||
|
||||
```
|
||||
|*reversed text |!R now BG is red.
|
||||
```
|
||||
For a detailed explanation of these caveats, see the [Understanding Color Tags](Understanding-Color-
|
||||
Tags) tutorial. But most of the time you might be better off to simply avoid `|*` and mark your text
|
||||
manually instead.
|
||||
|
||||
### Xterm256 Colours
|
||||
|
||||
The _Xterm256_ standard is a colour scheme that supports 256 colours for text and/or background.
|
||||
While this offers many more possibilities than traditional ANSI colours, be wary that too many text
|
||||
colors will be confusing to the eye. Also, not all clients support Xterm256 - these will instead see
|
||||
the closest equivalent ANSI color. You can mix Xterm256 tags with ANSI tags as you please.
|
||||
|
||||
|555 This is pure white text.|n This is normal text.
|
||||
|230 This is olive green text.
|
||||
|[300 This text has a dark red background.
|
||||
|005|[054 This is dark blue text on a bright cyan background.
|
||||
|=a This is a greyscale value, equal to black.
|
||||
|=m This is a greyscale value, midway between white and black.
|
||||
|=z This is a greyscale value, equal to white.
|
||||
|[=m This is a background greyscale value.
|
||||
|
||||
- `|###` - markup consists of three digits, each an integer from 0 to 5. The three digits describe
|
||||
the amount of **r**ed, **g**reen and **b**lue (RGB) components used in the colour. So `|500` means
|
||||
maximum red and none of the other colours - the result is a bright red. `|520` is red with a touch
|
||||
of green - the result is orange. As opposed to ANSI colors, Xterm256 syntax does not worry about
|
||||
bright/normal intensity, a brighter (lighter) color is just achieved by upping all RGB values with
|
||||
the same amount.
|
||||
- `|[###` - this works the same way but produces a coloured background.
|
||||
- `|=#` - markup produces the xterm256 gray scale tones, where `#` is a letter from `a` (black) to
|
||||
`z` (white). This offers many more nuances of gray than the normal `|###` markup (which only has
|
||||
four gray tones between solid black and white (`|000`, `|111`, `|222`, `|333` and `|444`)).
|
||||
- `|[=#` - this works in the same way but produces background gray scale tones.
|
||||
|
||||
If you have a client that supports Xterm256, you can use
|
||||
|
||||
@color xterm256
|
||||
|
||||
to get a table of all the 256 colours and the codes that produce them. If the table looks broken up
|
||||
into a few blocks of colors, it means Xterm256 is not supported and ANSI are used as a replacement.
|
||||
You can use the `@options` command to see if xterm256 is active for you. This depends on if your
|
||||
client told Evennia what it supports - if not, and you know what your client supports, you may have
|
||||
to activate some features manually.
|
||||
|
||||
## More reading
|
||||
|
||||
There is an [Understanding Color Tags](../Howto/Understanding-Color-Tags) tutorial which expands on the
|
||||
use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
|
||||
|
||||
|
|
@ -1,340 +1,19 @@
|
|||
# TextTags
|
||||
|
||||
|
||||
This documentation details the various text tags supported by Evennia, namely *colours*, *command
|
||||
links* and *inline functions*.
|
||||
|
||||
There is also an [Understanding Color Tags](../Howto/Understanding-Color-Tags) tutorial which expands on the
|
||||
use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
|
||||
|
||||
## Coloured text
|
||||
|
||||
*Note that the Documentation does not display colour the way it would look on the screen.*
|
||||
|
||||
Color can be a very useful tool for your game. It can be used to increase readability and make your
|
||||
game more appealing visually.
|
||||
|
||||
Remember however that, with the exception of the webclient, you generally don't control the client
|
||||
used to connect to the game. There is, for example, one special tag meaning "yellow". But exactly
|
||||
*which* hue of yellow is actually displayed on the user's screen depends on the settings of their
|
||||
particular mud client. They could even swap the colours around or turn them off altogether if so
|
||||
desired. Some clients don't even support color - text games are also played with special reading
|
||||
equipment by people who are blind or have otherwise diminished eyesight.
|
||||
|
||||
So a good rule of thumb is to use colour to enhance your game but don't *rely* on it to display
|
||||
critical information. If you are coding the game, you can add functionality to let users disable
|
||||
colours as they please, as described [here](../Howto/Manually-Configuring-Color).
|
||||
|
||||
To see which colours your client support, use the default `@color` command. This will list all
|
||||
available colours for ANSI and Xterm256 along with the codes you use for them. You can find a list
|
||||
of all the parsed `ANSI`-colour codes in `evennia/utils/ansi.py`.
|
||||
|
||||
### ANSI colours
|
||||
|
||||
Evennia supports the `ANSI` standard for text. This is by far the most supported MUD-color standard,
|
||||
available in all but the most ancient mud clients. The ANSI colours are **r**ed, **g**reen,
|
||||
**y**ellow, **b**lue, **m**agenta, **c**yan, **w**hite and black. They are abbreviated by their
|
||||
first letter except for black which is abbreviated with the letter **x**. In ANSI there are "bright"
|
||||
and "normal" (darker) versions of each color, adding up to a total of 16 colours to use for
|
||||
foreground text. There are also 8 "background" colours. These have no bright alternative in ANSI
|
||||
(but Evennia uses the [Xterm256](./TextTags#xterm256-colours) extension behind the scenes to offer
|
||||
them anyway).
|
||||
|
||||
To colour your text you put special tags in it. Evennia will parse these and convert them to the
|
||||
correct markup for the client used. If the user's client/console/display supports ANSI colour, they
|
||||
will see the text in the specified colour, otherwise the tags will be stripped (uncolored text).
|
||||
This works also for non-terminal clients, such as the webclient. For the webclient, Evennia will
|
||||
translate the codes to HTML RGB colors.
|
||||
|
||||
Here is an example of the tags in action:
|
||||
|
||||
|rThis text is bright red.|n This is normal text.
|
||||
|RThis is a dark red text.|n This is normal text.
|
||||
|[rThis text has red background.|n This is normal text.
|
||||
|b|[yThis is bright blue text on yellow background.|n This is normal text.
|
||||
|
||||
- `|n` - this tag will turn off all color formatting, including background colors.
|
||||
- `|#`- markup marks the start of foreground color. The case defines if the text is "bright" or
|
||||
"normal". So `|g` is a bright green and `|G` is "normal" (darker) green.
|
||||
- `|[#` is used to add a background colour to the text. The case again specifies if it is "bright"
|
||||
or "normal", so `|[c` starts a bright cyan background and `|[C` a darker cyan background.
|
||||
- `|!#` is used to add foreground color without any enforced brightness/normal information.
|
||||
These are normal-intensity and are thus always given as uppercase, such as
|
||||
`|!R` for red. The difference between e.g. `|!R` and `|R` is that
|
||||
`|!R` will "inherit" the brightness setting from previously set color tags, whereas `|R` will
|
||||
always reset to the normal-intensity red. The `|#` format contains an implicit `|h`/`|H` tag in it:
|
||||
disabling highlighting when switching to a normal color, and enabling it for bright ones. So `|btest
|
||||
|!Rtest2` will result in a bright red `test2` since the brightness setting from `|b` "bleeds over".
|
||||
You could use this to for example quickly switch the intensity of a multitude of color tags. There
|
||||
is no background-color equivalent to `|!` style tags.
|
||||
- `|h` is used to make any following foreground ANSI colors bright (it has no effect on Xterm
|
||||
colors). This is only relevant to use with `|!` type tags and will be valid until the next `|n`,
|
||||
`|H` or normal (upper-case) `|#` tag. This tag will never affect background colors, those have to be
|
||||
set bright/normal explicitly. Technically, `|h|!G` is identical to `|g`.
|
||||
- `|H` negates the effects `|h` and returns all ANSI foreground colors (`|!` and `|` types) to
|
||||
'normal' intensity. It has no effect on background and Xterm colors.
|
||||
|
||||
> Note: The ANSI standard does not actually support bright backgrounds like `|[r` - the standard
|
||||
only supports "normal" intensity backgrounds. To get around this Evennia instead implements these
|
||||
as [Xterm256 colours](./TextTags#xterm256-colours) behind the scenes. If the client does not support
|
||||
Xterm256 the ANSI colors will be used instead and there will be no visible difference between using
|
||||
upper- and lower-case background tags.
|
||||
|
||||
If you want to display an ANSI marker as output text (without having any effect), you need to escape
|
||||
it by preceding its `|` with another `|`:
|
||||
|
||||
```
|
||||
say The ||r ANSI marker changes text color to bright red.
|
||||
```
|
||||
|
||||
This will output the raw `|r` without any color change. This can also be necessary if you are doing
|
||||
ansi art that uses `|` with a letter directly following it.
|
||||
|
||||
Use the command
|
||||
|
||||
@color ansi
|
||||
|
||||
to get a list of all supported ANSI colours and the tags used to produce them.
|
||||
|
||||
A few additional ANSI codes are supported:
|
||||
|
||||
- `|/` A line break. You cannot put the normal Python `\n` line breaks in text entered inside the
|
||||
game (Evennia will filter this for security reasons). This is what you use instead: use the `|/`
|
||||
marker to format text with line breaks from the game command line.
|
||||
- `` This will translate into a `TAB` character. This will not always show (or show differently) to
|
||||
the client since it depends on their local settings. It's often better to use multiple spaces.
|
||||
- `|_` This is a space. You can usually use the normal space character, but if the space is *at the
|
||||
end of the line*, Evennia will likely crop it. This tag will not be cropped but always result in a
|
||||
space.
|
||||
- `|*` This will invert the current text/background colours. Can be useful to mark things (but see
|
||||
below).
|
||||
|
||||
##### Caveats of `|*`
|
||||
|
||||
The `|*` tag (inverse video) is an old ANSI standard and should usually not be used for more than to
|
||||
mark short snippets of text. If combined with other tags it comes with a series of potentially
|
||||
confusing behaviors:
|
||||
|
||||
* The `|*` tag will only work once in a row:, ie: after using it once it won't have an effect again
|
||||
until you declare another tag. This is an example:
|
||||
|
||||
```
|
||||
Normal text, |*reversed text|*, still reversed text.
|
||||
```
|
||||
|
||||
that is, it will not reverse to normal at the second `|*`. You need to reset it manually:
|
||||
|
||||
```
|
||||
Normal text, |*reversed text|n, normal again.
|
||||
```
|
||||
|
||||
* The `|*` tag does not take "bright" colors into account:
|
||||
|
||||
```
|
||||
|RNormal red, |hnow brightened. |*BG is normal red.
|
||||
```
|
||||
|
||||
So `|*` only considers the 'true' foreground color, ignoring any highlighting. Think of the bright
|
||||
state (`|h`) as something like like `<strong>` in HTML: it modifies the _appearance_ of a normal
|
||||
foreground color to match its bright counterpart, without changing its normal color.
|
||||
* Finally, after a `|*`, if the previous background was set to a dark color (via `|[`), `|!#`) will
|
||||
actually change the background color instead of the foreground:
|
||||
|
||||
```
|
||||
|*reversed text |!R now BG is red.
|
||||
```
|
||||
For a detailed explanation of these caveats, see the [Understanding Color Tags](Understanding-Color-
|
||||
Tags) tutorial. But most of the time you might be better off to simply avoid `|*` and mark your text
|
||||
manually instead.
|
||||
|
||||
### Xterm256 Colours
|
||||
|
||||
The _Xterm256_ standard is a colour scheme that supports 256 colours for text and/or background.
|
||||
While this offers many more possibilities than traditional ANSI colours, be wary that too many text
|
||||
colors will be confusing to the eye. Also, not all clients support Xterm256 - these will instead see
|
||||
the closest equivalent ANSI color. You can mix Xterm256 tags with ANSI tags as you please.
|
||||
|
||||
|555 This is pure white text.|n This is normal text.
|
||||
|230 This is olive green text.
|
||||
|[300 This text has a dark red background.
|
||||
|005|[054 This is dark blue text on a bright cyan background.
|
||||
|=a This is a greyscale value, equal to black.
|
||||
|=m This is a greyscale value, midway between white and black.
|
||||
|=z This is a greyscale value, equal to white.
|
||||
|[=m This is a background greyscale value.
|
||||
|
||||
- `|###` - markup consists of three digits, each an integer from 0 to 5. The three digits describe
|
||||
the amount of **r**ed, **g**reen and **b**lue (RGB) components used in the colour. So `|500` means
|
||||
maximum red and none of the other colours - the result is a bright red. `|520` is red with a touch
|
||||
of green - the result is orange. As opposed to ANSI colors, Xterm256 syntax does not worry about
|
||||
bright/normal intensity, a brighter (lighter) color is just achieved by upping all RGB values with
|
||||
the same amount.
|
||||
- `|[###` - this works the same way but produces a coloured background.
|
||||
- `|=#` - markup produces the xterm256 gray scale tones, where `#` is a letter from `a` (black) to
|
||||
`z` (white). This offers many more nuances of gray than the normal `|###` markup (which only has
|
||||
four gray tones between solid black and white (`|000`, `|111`, `|222`, `|333` and `|444`)).
|
||||
- `|[=#` - this works in the same way but produces background gray scale tones.
|
||||
|
||||
If you have a client that supports Xterm256, you can use
|
||||
|
||||
@color xterm256
|
||||
|
||||
to get a table of all the 256 colours and the codes that produce them. If the table looks broken up
|
||||
into a few blocks of colors, it means Xterm256 is not supported and ANSI are used as a replacement.
|
||||
You can use the `@options` command to see if xterm256 is active for you. This depends on if your
|
||||
client told Evennia what it supports - if not, and you know what your client supports, you may have
|
||||
to activate some features manually.
|
||||
|
||||
## Clickable links
|
||||
|
||||
Evennia supports clickable links for clients that supports it. This marks certain text so it can be
|
||||
clicked by a mouse and trigger a given Evennia command. To support clickable links, Evennia requires
|
||||
the webclient or an third-party telnet client with [MXP](http://www.zuggsoft.com/zmud/mxp.htm)
|
||||
support (*Note: Evennia only supports clickable links, no other MXP features*).
|
||||
|
||||
- `|lc` to start the link, by defining the command to execute.
|
||||
- `|lt` to continue with the text to show to the user (the link text).
|
||||
- `|le` to end the link text and the link definition.
|
||||
|
||||
All elements must appear in exactly this order to make a valid link. For example,
|
||||
|
||||
```
|
||||
"If you go |lcnorth|ltto the north|le you will find a cottage."
|
||||
```
|
||||
|
||||
This will display as "If you go __to the north__ you will find a cottage." where clicking the link
|
||||
will execute the command `north`. If the client does not support clickable links, only the link text
|
||||
will be shown.
|
||||
|
||||
## Inline functions
|
||||
|
||||
> Note: Inlinefuncs are **not** activated by default. To use them you need to add
|
||||
`INLINEFUNC_ENABLED=True` to your settings file.
|
||||
|
||||
Evennia has its own inline text formatting language, known as *inlinefuncs*. It allows the builder
|
||||
to include special function calls in code. They are executed dynamically by each session that
|
||||
receives them.
|
||||
|
||||
To add an inlinefunc, you embed it in a text string like this:
|
||||
|
||||
```
|
||||
"A normal string with $funcname(arg, arg, ...) embedded inside it."
|
||||
```
|
||||
|
||||
When this string is sent to a session (with the `msg()` method), these embedded inlinefuncs will be
|
||||
parsed. Their return value (which always is a string) replace their call location in the finalized
|
||||
string. The interesting thing with this is that the function called will have access to which
|
||||
session is seeing the string, meaning the string can end up looking different depending on who is
|
||||
looking. It could of course also vary depending on other factors like game time.
|
||||
|
||||
Any number of comma-separated arguments can be given (or none). No keywords are supported. You can
|
||||
also nest inlinefuncs by letting an argument itself also be another `$funcname(arg, arg, ...)` call
|
||||
(down to any depth of nesting). Function call resolution happens as in all programming languages
|
||||
inside-out, with the nested calls replacing the argument with their return strings before calling he
|
||||
parent.
|
||||
|
||||
```
|
||||
> say "This is $pad(a center-padded text, 30,c,-) of width 30."
|
||||
You say, "This is ---- a center-padded text----- of width 30."
|
||||
```
|
||||
|
||||
A special case happens if wanting to use an inlinefunc argument that itself includes a comma - this
|
||||
would be parsed as an argument separator. To escape commas you can either escape each comma manually
|
||||
with a backslash `\,`, or you can embed the entire string in python triple-quotes `"""` or `'''` -
|
||||
this will escape the entire argument, including commas and any nested inlinefunc calls within.
|
||||
|
||||
Only certain functions are available to use as inlinefuncs and the game developer may add their own
|
||||
functions as needed.
|
||||
|
||||
### New inlinefuncs
|
||||
|
||||
To add new inlinefuncs, edit the file `mygame/server/conf/inlinefuncs.py`.
|
||||
|
||||
*All globally defined functions in this module* are considered inline functions by the system. The
|
||||
only exception is functions whose name starts with an underscore `_`. An inlinefunc must be of the
|
||||
following form:
|
||||
|
||||
```python
|
||||
def funcname(*args, **kwargs):
|
||||
# ...
|
||||
return modified_text
|
||||
```
|
||||
|
||||
where `*args` denotes all the arguments this function will accept as an `$inlinefunc`. The inline
|
||||
function is expected to clean arguments and check that they are valid. If needed arguments are not
|
||||
given, default values should be used. The function should always return a string (even if it's
|
||||
empty). An inlinefunc should never cause a traceback regardless of the input (but it could log
|
||||
errors if desired).
|
||||
|
||||
Note that whereas the function should accept `**kwargs`, keyword inputs are *not* usable in the call
|
||||
to the inlinefunction. The `kwargs` part is instead intended for Evennia to be able to supply extra
|
||||
information. Currently Evennia sends a single keyword to every inline function and that is
|
||||
`session`, which holds the [serversession](../Components/Sessions) this text is targeted at. Through the session
|
||||
object, a lot of dynamic possibilities are opened up for your inline functions.
|
||||
|
||||
The `settings.INLINEFUNC_MODULES` configuration option is a list that decides which modules should
|
||||
be parsed for inline function definitions. This will include `mygame/server/conf/inlinefuncs.py` but
|
||||
more could be added. The list is read from left to right so if you want to overload default
|
||||
functions you just have to put your custom module-paths later in the list and name your functions
|
||||
the same as default ones.
|
||||
|
||||
Here is an example, the `crop` default inlinefunction:
|
||||
|
||||
```python
|
||||
from evennia.utils import utils
|
||||
|
||||
def crop(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Crops ingoing text to given widths.
|
||||
Args:
|
||||
text (str, optional): Text to crop.
|
||||
width (str, optional): Will be converted to an integer. Width of
|
||||
crop in characters.
|
||||
suffix (str, optional): End string to mark the fact that a part
|
||||
of the string was cropped. Defaults to `[...]`.
|
||||
Kwargs:
|
||||
session (Session): Session performing the crop.
|
||||
Example:
|
||||
`$crop(text, 50, [...])`
|
||||
|
||||
"""
|
||||
text, width, suffix = "", 78, "[...]"
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
text = args[0]
|
||||
if nargs > 1:
|
||||
width = int(args[1]) if args[1].strip().isdigit() else 78
|
||||
if nargs > 2:
|
||||
suffix = args[2]
|
||||
return utils.crop(text, width=width, suffix=suffix)
|
||||
```
|
||||
Another example, making use of the Session:
|
||||
|
||||
```python
|
||||
def charactername(*args, **kwargs):
|
||||
"""
|
||||
Inserts the character name of whomever sees the string
|
||||
(so everyone will see their own name). Uses the account
|
||||
name for OOC communications.
|
||||
|
||||
Example:
|
||||
say "This means YOU, $charactername()!"
|
||||
|
||||
"""
|
||||
session = kwargs["session"]
|
||||
if session.puppet:
|
||||
return kwargs["session"].puppet.key
|
||||
else:
|
||||
return session.account.key
|
||||
```
|
||||
|
||||
Evennia itself offers the following default inline functions (mostly as examples):
|
||||
|
||||
* `crop(text, width, suffix)` - See above.
|
||||
* `pad(text, width, align, fillchar)` - this pads the text to `width` (default 78), alignment ("c",
|
||||
"l" or "r", defaulting to "c") and fill-in character (defaults to space). Example: `$pad(40,l,-)`
|
||||
* `clr(startclr, text, endclr)` - A programmatic way to enter colored text for those who don't want
|
||||
to use the normal `|c` type color markers for some reason. The `color` argument is the same as the
|
||||
color markers except without the actual pre-marker, so `|r` would be just `r`. If `endclr` is not
|
||||
given, it defaults to resetting the color (`n`). Example: `$clr(b, A blue text)`
|
||||
* `space(number)` - Inserts the given number of spaces. If no argument is given, use 4 spaces.
|
||||
# In-text tags parsed by Evennia
|
||||
|
||||
Evennia understands various extra information embedded in text:
|
||||
|
||||
- [Colors](./Colors) - Using `|r`, `|n` etc can be used to mark parts of text with a color. The color will
|
||||
become ANSI/XTerm256 color tags for Telnet connections and CSS information for the webclient.
|
||||
- [Clickable links](./Clickable-Links) - This allows you to provide a text the user can click to execute an
|
||||
in-game command. This is on the form `|lc command |lt text |le`.
|
||||
- [FuncParser callables](../Components/FuncParser) - These are full-fledged function calls on the form `$funcname(args, kwargs)`
|
||||
that lead to calls to Python functions. The parser can be run with different available callables in different
|
||||
circumstances. The parser is run on all outgoing messages if `settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED=True`
|
||||
(disabled by default).
|
||||
|
||||
```toctree::
|
||||
|
||||
Colors.md
|
||||
Clickable-Links.md
|
||||
../Components/FuncParser.md
|
||||
```
|
||||
|
|
@ -77,6 +77,7 @@ The flat API is defined in `__init__.py` [viewable here](github:evennia/__init__
|
|||
- [evennia.EvForm](api:evennia.utils.evform#evennia.utils.evform.EvForm) - text form creator
|
||||
- Evennia.EvMore - text paginator
|
||||
- [evennia.EvEditor](api:evennia.utils.eveditor#evennia.utils.eveditor.EvEditor) - in game text line editor ([docs](Components/EvEditor))
|
||||
- [evennia.utils.funcparser.Funcparser](api:evennia.utils.funcparser) - inline parsing of functions ([docs](Components/FuncParser))
|
||||
|
||||
### Global singleton handlers
|
||||
|
||||
|
|
|
|||
7
docs/source/api/evennia.utils.funcparser.rst
Normal file
7
docs/source/api/evennia.utils.funcparser.rst
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
evennia.utils.funcparser
|
||||
===============================
|
||||
|
||||
.. automodule:: evennia.utils.funcparser
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
evennia.utils.inlinefuncs
|
||||
================================
|
||||
|
||||
.. automodule:: evennia.utils.inlinefuncs
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
|
@ -21,8 +21,8 @@ evennia.utils
|
|||
evennia.utils.evmenu
|
||||
evennia.utils.evmore
|
||||
evennia.utils.evtable
|
||||
evennia.utils.funcparser
|
||||
evennia.utils.gametime
|
||||
evennia.utils.inlinefuncs
|
||||
evennia.utils.logger
|
||||
evennia.utils.optionclasses
|
||||
evennia.utils.optionhandler
|
||||
|
|
@ -38,3 +38,4 @@ evennia.utils
|
|||
:maxdepth: 6
|
||||
|
||||
evennia.utils.idmapper
|
||||
evennia.utils.verb_conjugation
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
evennia.utils.verb\_conjugation.conjugate
|
||||
================================================
|
||||
|
||||
.. automodule:: evennia.utils.verb_conjugation.conjugate
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
15
docs/source/api/evennia.utils.verb_conjugation.rst
Normal file
15
docs/source/api/evennia.utils.verb_conjugation.rst
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
evennia.utils.verb\_conjugation
|
||||
=======================================
|
||||
|
||||
.. automodule:: evennia.utils.verb_conjugation
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 6
|
||||
|
||||
evennia.utils.verb_conjugation.conjugate
|
||||
evennia.utils.verb_conjugation.tests
|
||||
7
docs/source/api/evennia.utils.verb_conjugation.tests.rst
Normal file
7
docs/source/api/evennia.utils.verb_conjugation.tests.rst
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
evennia.utils.verb\_conjugation.tests
|
||||
============================================
|
||||
|
||||
.. automodule:: evennia.utils.verb_conjugation.tests
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# Toc
|
||||
|
||||
- [API root](api/evennia-api.rst)
|
||||
- [Coding/Coding Introduction](Coding/Coding-Introduction)
|
||||
- [Coding/Coding Overview](Coding/Coding-Overview)
|
||||
- [Coding/Continuous Integration](Coding/Continuous-Integration)
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
- [Components/EvEditor](Components/EvEditor)
|
||||
- [Components/EvMenu](Components/EvMenu)
|
||||
- [Components/EvMore](Components/EvMore)
|
||||
- [Components/FuncParser](Components/FuncParser)
|
||||
- [Components/Help System](Components/Help-System)
|
||||
- [Components/Inputfuncs](Components/Inputfuncs)
|
||||
- [Components/Locks](Components/Locks)
|
||||
|
|
@ -52,6 +53,8 @@
|
|||
- [Concepts/Banning](Concepts/Banning)
|
||||
- [Concepts/Bootstrap & Evennia](Concepts/Bootstrap-&-Evennia)
|
||||
- [Concepts/Building Permissions](Concepts/Building-Permissions)
|
||||
- [Concepts/Clickable Links](Concepts/Clickable-Links)
|
||||
- [Concepts/Colors](Concepts/Colors)
|
||||
- [Concepts/Concepts Overview](Concepts/Concepts-Overview)
|
||||
- [Concepts/Custom Protocols](Concepts/Custom-Protocols)
|
||||
- [Concepts/Guest Logins](Concepts/Guest-Logins)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from django.db.models import Q, Min, Max
|
|||
from evennia.objects.models import ObjectDB
|
||||
from evennia.locks.lockhandler import LockException
|
||||
from evennia.commands.cmdhandler import get_and_merge_cmdsets
|
||||
from evennia.utils import create, utils, search, logger
|
||||
from evennia.utils import create, utils, search, logger, funcparser
|
||||
from evennia.utils.utils import (
|
||||
inherits_from,
|
||||
class_from_module,
|
||||
|
|
@ -22,10 +22,11 @@ from evennia.utils.eveditor import EvEditor
|
|||
from evennia.utils.evmore import EvMore
|
||||
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
|
||||
from evennia.utils.ansi import raw as ansi_raw
|
||||
from evennia.utils.inlinefuncs import raw as inlinefunc_raw
|
||||
|
||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
|
||||
_FUNCPARSER = None
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = (
|
||||
"ObjManipCommand",
|
||||
|
|
@ -2122,7 +2123,8 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
)
|
||||
|
||||
if "prototype" in self.switches:
|
||||
modified = spawner.batch_update_objects_with_prototype(prototype, objects=[obj])
|
||||
modified = spawner.batch_update_objects_with_prototype(
|
||||
prototype, objects=[obj], caller=self.caller)
|
||||
prototype_success = modified > 0
|
||||
if not prototype_success:
|
||||
caller.msg("Prototype %s failed to apply." % prototype["key"])
|
||||
|
|
@ -2380,12 +2382,16 @@ class CmdExamine(ObjManipCommand):
|
|||
value (any): Attribute value.
|
||||
Returns:
|
||||
"""
|
||||
global _FUNCPARSER
|
||||
if not _FUNCPARSER:
|
||||
_FUNCPARSER = funcparser.FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES)
|
||||
|
||||
if attr is None:
|
||||
return "No such attribute was found."
|
||||
value = utils.to_str(value)
|
||||
if crop:
|
||||
value = utils.crop(value)
|
||||
value = inlinefunc_raw(ansi_raw(value))
|
||||
value = _FUNCPARSER.parse(ansi_raw(value), escape=True)
|
||||
if category:
|
||||
return f"{attr}[{category}] = {value}"
|
||||
else:
|
||||
|
|
@ -3458,7 +3464,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
"Python structures are allowed. \nMake sure to use correct "
|
||||
"Python syntax. Remember especially to put quotes around all "
|
||||
"strings inside lists and dicts.|n For more advanced uses, embed "
|
||||
"inlinefuncs in the strings."
|
||||
"funcparser callables ($funcs) in the strings."
|
||||
)
|
||||
else:
|
||||
string = "Expected {}, got {}.".format(expect, type(prototype))
|
||||
|
|
@ -3554,7 +3560,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
try:
|
||||
n_updated = spawner.batch_update_objects_with_prototype(
|
||||
prototype, objects=existing_objects
|
||||
prototype, objects=existing_objects, caller=caller,
|
||||
)
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
|
|
@ -3806,7 +3812,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
# proceed to spawning
|
||||
try:
|
||||
for obj in spawner.spawn(prototype):
|
||||
for obj in spawner.spawn(prototype, caller=self.caller):
|
||||
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
|
||||
if not prototype.get("location") and not noloc:
|
||||
# we don't hardcode the location in the prototype (unless the user
|
||||
|
|
|
|||
|
|
@ -695,27 +695,27 @@ If you want there is also some |wextra|n info for where to go beyond that.
|
|||
After playing through the tutorial-world quest, if you aim to make a game with
|
||||
Evennia you are wise to take a look at the |wEvennia documentation|n at
|
||||
|
||||
|yhttps://github.com/evennia/evennia/wiki|n
|
||||
|yhttps://www.evennia.com/docs/latest|n
|
||||
|
||||
- You can start by trying to build some stuff by following the |wBuilder quick-start|n:
|
||||
|
||||
|yhttps://github.com/evennia/evennia/wiki/Building-Quickstart|n
|
||||
|yhttps://www.evennia.com/docs/latest/Building-Quickstart|n
|
||||
|
||||
- The tutorial-world may or may not be your cup of tea, but it does show off
|
||||
several |wuseful tools|n of Evennia. You may want to check out how it works:
|
||||
|
||||
|yhttps://github.com/evennia/evennia/wiki/Tutorial-World-Introduction|n
|
||||
|yhttps://www.evennia.com/docs/latest/Tutorial-World-Introduction|n
|
||||
|
||||
- You can then continue looking through the |wTutorials|n and pick one that
|
||||
fits your level of understanding.
|
||||
|
||||
|yhttps://github.com/evennia/evennia/wiki/Tutorials|n
|
||||
|yhttps://www.evennia.com/docs/latest/Tutorials|n
|
||||
|
||||
- Make sure to |wjoin our forum|n and connect to our |wsupport chat|n! The
|
||||
Evennia community is very active and friendly and no question is too simple.
|
||||
You will often quickly get help. You can everything you need linked from
|
||||
|
||||
|yhttp://www.evennia.com|n
|
||||
|yhttps://www.evennia.com|n
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
"""
|
||||
Inlinefunc
|
||||
Outgoing callables to apply with the FuncParser on outgoing messages.
|
||||
|
||||
Inline functions allow for direct conversion of text users mark in a
|
||||
special way. Inlinefuncs are deactivated by default. To activate, add
|
||||
The functions in this module will become available as $funcname(args, kwargs)
|
||||
in all outgoing strings if you add
|
||||
|
||||
INLINEFUNC_ENABLED = True
|
||||
FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = True
|
||||
|
||||
to your settings file. The default inlinefuncs are found in
|
||||
evennia.utils.inlinefunc.
|
||||
to your settings file. The default inlinefuncs are found at the bottom of
|
||||
`evennia.utils.funcparser`.
|
||||
|
||||
In text, usage is straightforward:
|
||||
|
||||
$funcname([arg1,[arg2,...]])
|
||||
$funcname(arg1, arg2, ..., key=val, key2=val2, ...)
|
||||
|
||||
Example 1 (using the "pad" inlinefunc):
|
||||
say This is $pad("a center-padded text", 50,c,-) of width 50.
|
||||
|
|
@ -26,26 +26,14 @@ Example 2 (using nested "pad" and "time" inlinefuncs):
|
|||
To add more inline functions, add them to this module, using
|
||||
the following call signature:
|
||||
|
||||
def funcname(text, *args, **kwargs)
|
||||
|
||||
where `text` is always the part between {funcname(args) and
|
||||
{/funcname and the *args are taken from the appropriate part of the
|
||||
call. If no {/funcname is given, `text` will be the empty string.
|
||||
|
||||
It is important that the inline function properly clean the
|
||||
incoming `args`, checking their type and replacing them with sane
|
||||
defaults if needed. If impossible to resolve, the unmodified text
|
||||
should be returned. The inlinefunc should never cause a traceback.
|
||||
|
||||
While the inline function should accept **kwargs, the keyword is
|
||||
never accepted as a valid call - this is only intended to be used
|
||||
internally by Evennia, notably to send the `session` keyword to
|
||||
the function; this is the session of the object viewing the string
|
||||
and can be used to customize it to each session.
|
||||
def funcname(*args, **kwargs)
|
||||
...
|
||||
|
||||
"""
|
||||
|
||||
# def capitalize(text, *args, **kwargs):
|
||||
# "Silly capitalize example. Used as {capitalize() ... {/capitalize"
|
||||
# def capitalize(*args, **kwargs):
|
||||
# "Silly capitalize example. Used as $capitalize
|
||||
# if not args:
|
||||
# return ''
|
||||
# session = kwargs.get("session")
|
||||
# return text.capitalize()
|
||||
# return args[0].capitalize()
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from evennia.objects.models import ObjectDB
|
|||
from evennia.scripts.scripthandler import ScriptHandler
|
||||
from evennia.commands import cmdset, command
|
||||
from evennia.commands.cmdsethandler import CmdSetHandler
|
||||
from evennia.utils import funcparser
|
||||
from evennia.utils import create
|
||||
from evennia.utils import search
|
||||
from evennia.utils import logger
|
||||
|
|
@ -47,6 +48,12 @@ _COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
|||
# the sessid_max is based on the length of the db_sessid csv field (excluding commas)
|
||||
_SESSID_MAX = 16 if _MULTISESSION_MODE in (1, 3) else 1
|
||||
|
||||
_MSG_CONTENTS_PARSER = funcparser.FuncParser(
|
||||
{"you": funcparser.funcparser_callable_you,
|
||||
"You": funcparser.funcparser_callable_You,
|
||||
"conj": funcparser.funcparser_callable_conjugate
|
||||
})
|
||||
|
||||
|
||||
class ObjectSessionHandler(object):
|
||||
"""
|
||||
|
|
@ -717,64 +724,94 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
text (str or tuple): Message to send. If a tuple, this should be
|
||||
on the valid OOB outmessage form `(message, {kwargs})`,
|
||||
where kwargs are optional data passed to the `text`
|
||||
outputfunc.
|
||||
outputfunc. The message will be parsed for `{key}` formatting and
|
||||
`$You/$you()/$You(key)` and `$conj(verb)` inline function callables.
|
||||
The `key` is taken from the `mapping` kwarg {"key": object, ...}`.
|
||||
The `mapping[key].get_display_name(looker=recipient)` will be called
|
||||
for that key for every recipient of the string.
|
||||
exclude (list, optional): A list of objects not to send to.
|
||||
from_obj (Object, optional): An object designated as the
|
||||
"sender" of the message. See `DefaultObject.msg()` for
|
||||
more info.
|
||||
mapping (dict, optional): A mapping of formatting keys
|
||||
`{"key":<object>, "key2":<object2>,...}. The keys
|
||||
must match `{key}` markers in the `text` if this is a string or
|
||||
in the internal `message` if `text` is a tuple. These
|
||||
formatting statements will be
|
||||
replaced by the return of `<object>.get_display_name(looker)`
|
||||
for every looker in contents that receives the
|
||||
message. This allows for every object to potentially
|
||||
get its own customized string.
|
||||
Keyword Args:
|
||||
Keyword arguments will be passed on to `obj.msg()` for all
|
||||
messaged objects.
|
||||
`{"key":<object>, "key2":<object2>,...}.
|
||||
The keys must either match `{key}` or `$You(key)/$you(key)` markers
|
||||
in the `text` string. If `<object>` doesn't have a `get_display_name`
|
||||
method, it will be returned as a string. If not set, a key `you` will
|
||||
be auto-added to point to `from_obj` if given, otherwise to `self`.
|
||||
**kwargs: Keyword arguments will be passed on to `obj.msg()` for all
|
||||
messaged objects.
|
||||
|
||||
Notes:
|
||||
The `mapping` argument is required if `message` contains
|
||||
{}-style format syntax. The keys of `mapping` should match
|
||||
named format tokens, and its values will have their
|
||||
`get_display_name()` function called for each object in
|
||||
the room before substitution. If an item in the mapping does
|
||||
not have `get_display_name()`, its string value will be used.
|
||||
For 'actor-stance' reporting (You say/Name says), use the
|
||||
`$You()/$you()/$You(key)` and `$conj(verb)` (verb-conjugation)
|
||||
inline callables. This will use the respective `get_display_name()`
|
||||
for all onlookers except for `from_obj or self`, which will become
|
||||
'You/you'. If you use `$You/you(key)`, the key must be in `mapping`.
|
||||
|
||||
Example:
|
||||
Say Char is a Character object and Npc is an NPC object:
|
||||
For 'director-stance' reporting (Name says/Name says), use {key}
|
||||
syntax directly. For both `{key}` and `You/you(key)`,
|
||||
`mapping[key].get_display_name(looker=recipient)` may be called
|
||||
depending on who the recipient is.
|
||||
|
||||
char.location.msg_contents(
|
||||
"{attacker} kicks {defender}",
|
||||
mapping=dict(attacker=char, defender=npc), exclude=(char, npc))
|
||||
Examples:
|
||||
|
||||
This will result in everyone in the room seeing 'Char kicks NPC'
|
||||
where everyone may potentially see different results for Char and Npc
|
||||
depending on the results of `char.get_display_name(looker)` and
|
||||
`npc.get_display_name(looker)` for each particular onlooker
|
||||
Let's assume
|
||||
- `player1.key -> "Player1"`,
|
||||
`player1.get_display_name(looker=player2) -> "The First girl"`
|
||||
- `player2.key -> "Player2"`,
|
||||
`player2.get_display_name(looker=player1) -> "The Second girl"`
|
||||
|
||||
Actor-stance:
|
||||
::
|
||||
|
||||
char.location.msg_contents(
|
||||
"$You() $conj(attack) $you(defender).",
|
||||
mapping={"defender": player2})
|
||||
|
||||
- player1 will see `You attack The Second girl.`
|
||||
- player2 will see 'The First girl attacks you.'
|
||||
|
||||
Director-stance:
|
||||
::
|
||||
|
||||
char.location.msg_contents(
|
||||
"{attacker} attacks {defender}.",
|
||||
mapping={"attacker:player1, "defender":player2})
|
||||
|
||||
- player1 will see: 'Player1 attacks The Second girl.'
|
||||
- player2 will see: 'The First girl attacks Player2'
|
||||
|
||||
"""
|
||||
# we also accept an outcommand on the form (message, {kwargs})
|
||||
is_outcmd = text and is_iter(text)
|
||||
inmessage = text[0] if is_outcmd else text
|
||||
outkwargs = text[1] if is_outcmd and len(text) > 1 else {}
|
||||
mapping = mapping or {}
|
||||
you = from_obj or self
|
||||
|
||||
if 'you' not in mapping:
|
||||
mapping[you] = you
|
||||
|
||||
contents = self.contents
|
||||
if exclude:
|
||||
exclude = make_iter(exclude)
|
||||
contents = [obj for obj in contents if obj not in exclude]
|
||||
for obj in contents:
|
||||
if mapping:
|
||||
substitutions = {
|
||||
t: sub.get_display_name(obj) if hasattr(sub, "get_display_name") else str(sub)
|
||||
for t, sub in mapping.items()
|
||||
}
|
||||
outmessage = inmessage.format(**substitutions)
|
||||
else:
|
||||
outmessage = inmessage
|
||||
obj.msg(text=(outmessage, outkwargs), from_obj=from_obj, **kwargs)
|
||||
|
||||
for receiver in contents:
|
||||
|
||||
# actor-stance replacements
|
||||
inmessage = _MSG_CONTENTS_PARSER.parse(
|
||||
inmessage, raise_errors=True, return_string=True,
|
||||
caller=you, receiver=receiver, mapping=mapping)
|
||||
|
||||
# director-stance replacements
|
||||
outmessage = inmessage.format(
|
||||
**{key: obj.get_display_name(looker=receiver)
|
||||
if hasattr(obj, "get_display_name") else str(obj)
|
||||
for key, obj in mapping.items()})
|
||||
|
||||
receiver.msg(text=(outmessage, outkwargs), from_obj=from_obj, **kwargs)
|
||||
|
||||
def move_to(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -177,9 +177,7 @@ def _set_property(caller, raw_string, **kwargs):
|
|||
|
||||
if kwargs.get("test_parse", True):
|
||||
out.append(" Simulating prototype-func parsing ...")
|
||||
err, parsed_value = protlib.protfunc_parser(value, testing=True)
|
||||
if err:
|
||||
out.append(" |yPython `literal_eval` warning: {}|n".format(err))
|
||||
parsed_value = protlib.protfunc_parser(value, testing=True, prototype=prototype)
|
||||
if parsed_value != value:
|
||||
out.append(
|
||||
" |g(Example-)value when parsed ({}):|n {}".format(type(parsed_value), parsed_value)
|
||||
|
|
@ -264,7 +262,7 @@ def _validate_prototype(prototype):
|
|||
def _format_protfuncs():
|
||||
out = []
|
||||
sorted_funcs = [
|
||||
(key, func) for key, func in sorted(protlib.PROT_FUNCS.items(), key=lambda tup: tup[0])
|
||||
(key, func) for key, func in sorted(protlib.FUNC_PARSER.callables.items(), key=lambda tup: tup[0])
|
||||
]
|
||||
for protfunc_name, protfunc in sorted_funcs:
|
||||
out.append(
|
||||
|
|
@ -2115,7 +2113,8 @@ def _apply_diff(caller, **kwargs):
|
|||
objects = kwargs["objects"]
|
||||
back_node = kwargs["back_node"]
|
||||
diff = kwargs.get("diff", None)
|
||||
num_changed = spawner.batch_update_objects_with_prototype(prototype, diff=diff, objects=objects)
|
||||
num_changed = spawner.batch_update_objects_with_prototype(prototype, diff=diff, objects=objects,
|
||||
caller=caller)
|
||||
caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed))
|
||||
return back_node
|
||||
|
||||
|
|
@ -2483,7 +2482,7 @@ def _spawn(caller, **kwargs):
|
|||
if not prototype.get("location"):
|
||||
prototype["location"] = caller
|
||||
|
||||
obj = spawner.spawn(prototype)
|
||||
obj = spawner.spawn(prototype, caller=caller)
|
||||
if obj:
|
||||
obj = obj[0]
|
||||
text = "|gNew instance|n {key} ({dbref}) |gspawned at location |n{loc}|n|g.|n".format(
|
||||
|
|
|
|||
|
|
@ -1,33 +1,28 @@
|
|||
"""
|
||||
Protfuncs are function-strings embedded in a prototype and allows for a builder to create a
|
||||
prototype with custom logics without having access to Python. The Protfunc is parsed using the
|
||||
inlinefunc parser but is fired at the moment the spawning happens, using the creating object's
|
||||
session as input.
|
||||
Protfuncs are FuncParser-callables that can be embedded in a prototype to
|
||||
provide custom logic without having access to Python. The protfunc is parsed at
|
||||
the time of spawning, using the creating object's session as input. If the
|
||||
protfunc returns a non-string, this is what will be added to the prototype.
|
||||
|
||||
In the prototype dict, the protfunc is specified as a string inside the prototype, e.g.:
|
||||
|
||||
{ ...
|
||||
|
||||
"key": "$funcname(arg1, arg2, ...)"
|
||||
"key": "$funcname(args, kwargs)"
|
||||
|
||||
... }
|
||||
|
||||
and multiple functions can be nested (no keyword args are supported). The result will be used as the
|
||||
value for that prototype key for that individual spawn.
|
||||
|
||||
Available protfuncs are callables in one of the modules of `settings.PROT_FUNC_MODULES`. They
|
||||
are specified as functions
|
||||
Available protfuncs are either all callables in one of the modules of `settings.PROT_FUNC_MODULES`
|
||||
or all callables added to a dict FUNCPARSER_CALLABLES in such a module.
|
||||
|
||||
def funcname (*args, **kwargs)
|
||||
|
||||
where *args are the arguments given in the prototype, and **kwargs are inserted by Evennia:
|
||||
At spawn-time the spawner passes the following extra kwargs into each callable (in addition to
|
||||
what is added in the call itself):
|
||||
|
||||
- session (Session): The Session of the entity spawning using this prototype.
|
||||
- prototype (dict): The dict this protfunc is a part of.
|
||||
- current_key (str): The active key this value belongs to in the prototype.
|
||||
- testing (bool): This is set if this function is called as part of the prototype validation; if
|
||||
set, the protfunc should take care not to perform any persistent actions, such as operate on
|
||||
objects or add things to the database.
|
||||
|
||||
Any traceback raised by this function will be handled at the time of spawning and abort the spawn
|
||||
before any object is created/updated. It must otherwise return the value to store for the specified
|
||||
|
|
@ -35,312 +30,32 @@ prototype key (this value must be possible to serialize in an Attribute).
|
|||
|
||||
"""
|
||||
|
||||
from ast import literal_eval
|
||||
from random import randint as base_randint, random as base_random, choice as base_choice
|
||||
import re
|
||||
|
||||
from evennia.utils import search
|
||||
from evennia.utils.utils import justify as base_justify, is_iter, to_str
|
||||
|
||||
_PROTLIB = None
|
||||
|
||||
_RE_DBREF = re.compile(r"\#[0-9]+")
|
||||
from evennia.utils import funcparser
|
||||
|
||||
|
||||
# default protfuncs
|
||||
|
||||
|
||||
def random(*args, **kwargs):
|
||||
def protfunc_callable_protkey(*args, **kwargs):
|
||||
"""
|
||||
Usage: $random()
|
||||
Returns a random value in the interval [0, 1)
|
||||
|
||||
"""
|
||||
return base_random()
|
||||
|
||||
|
||||
def randint(*args, **kwargs):
|
||||
"""
|
||||
Usage: $randint(start, end)
|
||||
Returns random integer in interval [start, end]
|
||||
|
||||
"""
|
||||
if len(args) != 2:
|
||||
raise TypeError("$randint needs two arguments - start and end.")
|
||||
start, end = int(args[0]), int(args[1])
|
||||
return base_randint(start, end)
|
||||
|
||||
|
||||
def left_justify(*args, **kwargs):
|
||||
"""
|
||||
Usage: $left_justify(<text>)
|
||||
Returns <text> left-justified.
|
||||
|
||||
"""
|
||||
if args:
|
||||
return base_justify(args[0], align="l")
|
||||
return ""
|
||||
|
||||
|
||||
def right_justify(*args, **kwargs):
|
||||
"""
|
||||
Usage: $right_justify(<text>)
|
||||
Returns <text> right-justified across screen width.
|
||||
|
||||
"""
|
||||
if args:
|
||||
return base_justify(args[0], align="r")
|
||||
return ""
|
||||
|
||||
|
||||
def center_justify(*args, **kwargs):
|
||||
|
||||
"""
|
||||
Usage: $center_justify(<text>)
|
||||
Returns <text> centered in screen width.
|
||||
|
||||
"""
|
||||
if args:
|
||||
return base_justify(args[0], align="c")
|
||||
return ""
|
||||
|
||||
|
||||
def choice(*args, **kwargs):
|
||||
"""
|
||||
Usage: $choice(val, val, val, ...)
|
||||
Returns one of the values randomly
|
||||
"""
|
||||
if args:
|
||||
return base_choice(args)
|
||||
return ""
|
||||
|
||||
|
||||
def full_justify(*args, **kwargs):
|
||||
|
||||
"""
|
||||
Usage: $full_justify(<text>)
|
||||
Returns <text> filling up screen width by adding extra space.
|
||||
|
||||
"""
|
||||
if args:
|
||||
return base_justify(args[0], align="f")
|
||||
return ""
|
||||
|
||||
|
||||
def protkey(*args, **kwargs):
|
||||
"""
|
||||
Usage: $protkey(<key>)
|
||||
Usage: $protkey(keyname)
|
||||
Returns the value of another key in this prototoype. Will raise an error if
|
||||
the key is not found in this prototype.
|
||||
|
||||
"""
|
||||
if args:
|
||||
prototype = kwargs["prototype"]
|
||||
return prototype[args[0].strip()]
|
||||
if not args:
|
||||
return ""
|
||||
|
||||
prototype = kwargs.get("prototype", {})
|
||||
prot_value = prototype[args[0]]
|
||||
try:
|
||||
return funcparser.funcparser_callable_eval(prot_value, **kwargs)
|
||||
except funcparser.ParsingError:
|
||||
return prot_value
|
||||
|
||||
|
||||
def add(*args, **kwargs):
|
||||
"""
|
||||
Usage: $add(val1, val2)
|
||||
Returns the result of val1 + val2. Values must be
|
||||
valid simple Python structures possible to add,
|
||||
such as numbers, lists etc.
|
||||
|
||||
"""
|
||||
if len(args) > 1:
|
||||
val1, val2 = args[0], args[1]
|
||||
# try to convert to python structures, otherwise, keep as strings
|
||||
try:
|
||||
val1 = literal_eval(val1.strip())
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
val2 = literal_eval(val2.strip())
|
||||
except Exception:
|
||||
pass
|
||||
return val1 + val2
|
||||
raise ValueError("$add requires two arguments.")
|
||||
|
||||
|
||||
def sub(*args, **kwargs):
|
||||
"""
|
||||
Usage: $del(val1, val2)
|
||||
Returns the value of val1 - val2. Values must be
|
||||
valid simple Python structures possible to
|
||||
subtract.
|
||||
|
||||
"""
|
||||
if len(args) > 1:
|
||||
val1, val2 = args[0], args[1]
|
||||
# try to convert to python structures, otherwise, keep as strings
|
||||
try:
|
||||
val1 = literal_eval(val1.strip())
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
val2 = literal_eval(val2.strip())
|
||||
except Exception:
|
||||
pass
|
||||
return val1 - val2
|
||||
raise ValueError("$sub requires two arguments.")
|
||||
|
||||
|
||||
def mult(*args, **kwargs):
|
||||
"""
|
||||
Usage: $mul(val1, val2)
|
||||
Returns the value of val1 * val2. The values must be
|
||||
valid simple Python structures possible to
|
||||
multiply, like strings and/or numbers.
|
||||
|
||||
"""
|
||||
if len(args) > 1:
|
||||
val1, val2 = args[0], args[1]
|
||||
# try to convert to python structures, otherwise, keep as strings
|
||||
try:
|
||||
val1 = literal_eval(val1.strip())
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
val2 = literal_eval(val2.strip())
|
||||
except Exception:
|
||||
pass
|
||||
return val1 * val2
|
||||
raise ValueError("$mul requires two arguments.")
|
||||
|
||||
|
||||
def div(*args, **kwargs):
|
||||
"""
|
||||
Usage: $div(val1, val2)
|
||||
Returns the value of val1 / val2. Values must be numbers and
|
||||
the result is always a float.
|
||||
|
||||
"""
|
||||
if len(args) > 1:
|
||||
val1, val2 = args[0], args[1]
|
||||
# try to convert to python structures, otherwise, keep as strings
|
||||
try:
|
||||
val1 = literal_eval(val1.strip())
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
val2 = literal_eval(val2.strip())
|
||||
except Exception:
|
||||
pass
|
||||
return val1 / float(val2)
|
||||
raise ValueError("$mult requires two arguments.")
|
||||
|
||||
|
||||
def toint(*args, **kwargs):
|
||||
"""
|
||||
Usage: $toint(<number>)
|
||||
Returns <number> as an integer.
|
||||
"""
|
||||
if args:
|
||||
val = args[0]
|
||||
try:
|
||||
return int(literal_eval(val.strip()))
|
||||
except ValueError:
|
||||
return val
|
||||
raise ValueError("$toint requires one argument.")
|
||||
|
||||
|
||||
def eval(*args, **kwargs):
|
||||
"""
|
||||
Usage $eval(<expression>)
|
||||
Returns evaluation of a simple Python expression. The string may *only* consist of the following
|
||||
Python literal structures: strings, numbers, tuples, lists, dicts, booleans,
|
||||
and None. The strings can also contain #dbrefs. Escape embedded protfuncs as $$protfunc(..)
|
||||
- those will then be evaluated *after* $eval.
|
||||
|
||||
"""
|
||||
global _PROTLIB
|
||||
if not _PROTLIB:
|
||||
from evennia.prototypes import prototypes as _PROTLIB
|
||||
|
||||
string = ",".join(args)
|
||||
struct = literal_eval(string)
|
||||
|
||||
if isinstance(struct, str):
|
||||
# we must shield the string, otherwise it will be merged as a string and future
|
||||
# literal_evas will pick up e.g. '2' as something that should be converted to a number
|
||||
struct = '"{}"'.format(struct)
|
||||
|
||||
# convert any #dbrefs to objects (also in nested structures)
|
||||
struct = _PROTLIB.value_to_obj_or_any(struct)
|
||||
|
||||
return struct
|
||||
|
||||
|
||||
def _obj_search(*args, **kwargs):
|
||||
"Helper function to search for an object"
|
||||
|
||||
query = "".join(args)
|
||||
session = kwargs.get("session", None)
|
||||
return_list = kwargs.pop("return_list", False)
|
||||
account = None
|
||||
|
||||
if session:
|
||||
account = session.account
|
||||
|
||||
targets = search.search_object(query)
|
||||
|
||||
if return_list:
|
||||
retlist = []
|
||||
if account:
|
||||
for target in targets:
|
||||
if target.access(account, target, "control"):
|
||||
retlist.append(target)
|
||||
else:
|
||||
retlist = targets
|
||||
return retlist
|
||||
else:
|
||||
# single-match
|
||||
if not targets:
|
||||
raise ValueError("$obj: Query '{}' gave no matches.".format(query))
|
||||
if len(targets) > 1:
|
||||
raise ValueError(
|
||||
"$obj: Query '{query}' gave {nmatches} matches. Limit your "
|
||||
"query or use $objlist instead.".format(query=query, nmatches=len(targets))
|
||||
)
|
||||
target = targets[0]
|
||||
if account:
|
||||
if not target.access(account, target, "control"):
|
||||
raise ValueError(
|
||||
"$obj: Obj {target}(#{dbref} cannot be added - "
|
||||
"Account {account} does not have 'control' access.".format(
|
||||
target=target.key, dbref=target.id, account=account
|
||||
)
|
||||
)
|
||||
return target
|
||||
|
||||
|
||||
def obj(*args, **kwargs):
|
||||
"""
|
||||
Usage $obj(<query>)
|
||||
Returns one Object searched globally by key, alias or #dbref. Error if more than one.
|
||||
|
||||
"""
|
||||
obj = _obj_search(return_list=False, *args, **kwargs)
|
||||
if obj:
|
||||
return "#{}".format(obj.id)
|
||||
return "".join(args)
|
||||
|
||||
|
||||
def objlist(*args, **kwargs):
|
||||
"""
|
||||
Usage $objlist(<query>)
|
||||
Returns list with one or more Objects searched globally by key, alias or #dbref.
|
||||
|
||||
"""
|
||||
return ["#{}".format(obj.id) for obj in _obj_search(return_list=True, *args, **kwargs)]
|
||||
|
||||
|
||||
def dbref(*args, **kwargs):
|
||||
"""
|
||||
Usage $dbref(<#dbref>)
|
||||
Validate that a #dbref input is valid.
|
||||
"""
|
||||
if not args or len(args) < 1 or _RE_DBREF.match(args[0]) is None:
|
||||
raise ValueError("$dbref requires a valid #dbref argument.")
|
||||
|
||||
return obj(args[0])
|
||||
# this is picked up by FuncParser
|
||||
FUNCPARSER_CALLABLES = {
|
||||
"protkey": protfunc_callable_protkey,
|
||||
**funcparser.FUNCPARSER_CALLABLES,
|
||||
**funcparser.SEARCHING_CALLABLES,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ from evennia.utils.utils import (
|
|||
)
|
||||
from evennia.locks.lockhandler import validate_lockstring, check_lockstring
|
||||
from evennia.utils import logger
|
||||
from evennia.utils import inlinefuncs, dbserialize
|
||||
from evennia.utils.funcparser import FuncParser
|
||||
from evennia.utils import dbserialize
|
||||
from evennia.utils.evtable import EvTable
|
||||
|
||||
|
||||
|
|
@ -58,11 +59,14 @@ _PROTOTYPE_RESERVED_KEYS = _PROTOTYPE_META_NAMES + (
|
|||
)
|
||||
PROTOTYPE_TAG_CATEGORY = "from_prototype"
|
||||
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
|
||||
PROT_FUNCS = {}
|
||||
|
||||
_PROTOTYPE_FALLBACK_LOCK = "spawn:all();edit:all()"
|
||||
|
||||
|
||||
# the protfunc parser
|
||||
FUNC_PARSER = FuncParser(settings.PROT_FUNC_MODULES)
|
||||
|
||||
|
||||
class PermissionError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
|
@ -709,18 +713,7 @@ def validate_prototype(
|
|||
prototype["prototype_locks"] = prototype_locks
|
||||
|
||||
|
||||
# Protfunc parsing (in-prototype functions)
|
||||
|
||||
for mod in settings.PROT_FUNC_MODULES:
|
||||
try:
|
||||
callables = callables_from_module(mod)
|
||||
PROT_FUNCS.update(callables)
|
||||
except ImportError:
|
||||
logger.log_trace()
|
||||
raise
|
||||
|
||||
|
||||
def protfunc_parser(value, available_functions=None, testing=False, stacktrace=False, **kwargs):
|
||||
def protfunc_parser(value, available_functions=None, testing=False, stacktrace=False, caller=None, **kwargs):
|
||||
"""
|
||||
Parse a prototype value string for a protfunc and process it.
|
||||
|
||||
|
|
@ -732,45 +725,27 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F
|
|||
protfuncs, all other types are returned as-is.
|
||||
available_functions (dict, optional): Mapping of name:protfunction to use for this parsing.
|
||||
If not set, use default sources.
|
||||
testing (bool, optional): Passed to protfunc. If in a testing mode, some protfuncs may
|
||||
behave differently.
|
||||
stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser.
|
||||
|
||||
Keyword Args:
|
||||
session (Session): Passed to protfunc. Session of the entity spawning the prototype.
|
||||
protototype (dict): Passed to protfunc. The dict this protfunc is a part of.
|
||||
current_key(str): Passed to protfunc. The key in the prototype that will hold this value.
|
||||
caller (Object or Account): This is necessary for certain protfuncs that perform object
|
||||
searches and have to check permissions.
|
||||
any (any): Passed on to the protfunc.
|
||||
|
||||
Returns:
|
||||
testresult (tuple): If `testing` is set, returns a tuple (error, result) where error is
|
||||
either None or a string detailing the error from protfunc_parser or seen when trying to
|
||||
run `literal_eval` on the parsed string.
|
||||
any (any): A structure to replace the string on the prototype level. If this is a
|
||||
callable or a (callable, (args,)) structure, it will be executed as if one had supplied
|
||||
it to the prototype directly. This structure is also passed through literal_eval so one
|
||||
can get actual Python primitives out of it (not just strings). It will also identify
|
||||
eventual object #dbrefs in the output from the protfunc.
|
||||
any: A structure to replace the string on the prototype leve. Note
|
||||
that FunctionParser functions $funcname(*args, **kwargs) can return any
|
||||
data type to insert into the prototype.
|
||||
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
|
||||
available_functions = PROT_FUNCS if available_functions is None else available_functions
|
||||
result = FUNC_PARSER.parse(value, raise_errors=True, return_str=False, caller=caller, **kwargs)
|
||||
|
||||
result = inlinefuncs.parse_inlinefunc(
|
||||
value, available_funcs=available_functions, stacktrace=stacktrace, testing=testing, **kwargs
|
||||
)
|
||||
|
||||
err = None
|
||||
try:
|
||||
result = literal_eval(result)
|
||||
except ValueError:
|
||||
pass
|
||||
except Exception as exc:
|
||||
err = str(exc)
|
||||
if testing:
|
||||
return err, result
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -785,7 +760,7 @@ def format_available_protfuncs():
|
|||
clr (str, optional): What coloration tag to use.
|
||||
"""
|
||||
out = []
|
||||
for protfunc_name, protfunc in PROT_FUNCS.items():
|
||||
for protfunc_name, protfunc in FUNC_PARSER.callables.items():
|
||||
out.append(
|
||||
"- |c${name}|n - |W{docs}".format(
|
||||
name=protfunc_name, docs=protfunc.__doc__.strip().replace("\n", "")
|
||||
|
|
@ -910,7 +885,7 @@ def check_permission(prototype_key, action, default=True):
|
|||
return default
|
||||
|
||||
|
||||
def init_spawn_value(value, validator=None):
|
||||
def init_spawn_value(value, validator=None, caller=None):
|
||||
"""
|
||||
Analyze the prototype value and produce a value useful at the point of spawning.
|
||||
|
||||
|
|
@ -921,6 +896,8 @@ def init_spawn_value(value, validator=None):
|
|||
other - will be assigned depending on the variable type
|
||||
validator (callable, optional): If given, this will be called with the value to
|
||||
check and guarantee the outcome is of a given type.
|
||||
caller (Object or Account): This is necessary for certain protfuncs that perform object
|
||||
searches and have to check permissions.
|
||||
|
||||
Returns:
|
||||
any (any): The (potentially pre-processed value to use for this prototype key)
|
||||
|
|
@ -935,7 +912,7 @@ def init_spawn_value(value, validator=None):
|
|||
value = validator(value[0](*make_iter(args)))
|
||||
else:
|
||||
value = validator(value)
|
||||
result = protfunc_parser(value)
|
||||
result = protfunc_parser(value, caller=caller)
|
||||
if result != value:
|
||||
return validator(result)
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -607,7 +607,8 @@ def format_diff(diff, minimal=True):
|
|||
return "\n ".join(line for line in texts if line)
|
||||
|
||||
|
||||
def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exact=False):
|
||||
def batch_update_objects_with_prototype(prototype, diff=None, objects=None,
|
||||
exact=False, caller=None):
|
||||
"""
|
||||
Update existing objects with the latest version of the prototype.
|
||||
|
||||
|
|
@ -624,6 +625,7 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exac
|
|||
if it's not set in the prototype. With `exact=True`, all un-specified properties of the
|
||||
objects will be removed if they exist. This will lead to a more accurate 1:1 correlation
|
||||
between the object and the prototype but is usually impractical.
|
||||
caller (Object or Account, optional): This may be used by protfuncs to do permission checks.
|
||||
Returns:
|
||||
changed (int): The number of objects that had changes applied to them.
|
||||
|
||||
|
|
@ -675,33 +677,33 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exac
|
|||
do_save = True
|
||||
|
||||
if key == "key":
|
||||
obj.db_key = init_spawn_value(val, str)
|
||||
obj.db_key = init_spawn_value(val, str, caller=caller)
|
||||
elif key == "typeclass":
|
||||
obj.db_typeclass_path = init_spawn_value(val, str)
|
||||
obj.db_typeclass_path = init_spawn_value(val, str, caller=caller)
|
||||
elif key == "location":
|
||||
obj.db_location = init_spawn_value(val, value_to_obj)
|
||||
obj.db_location = init_spawn_value(val, value_to_obj, caller=caller)
|
||||
elif key == "home":
|
||||
obj.db_home = init_spawn_value(val, value_to_obj)
|
||||
obj.db_home = init_spawn_value(val, value_to_obj, caller=caller)
|
||||
elif key == "destination":
|
||||
obj.db_destination = init_spawn_value(val, value_to_obj)
|
||||
obj.db_destination = init_spawn_value(val, value_to_obj, caller=caller)
|
||||
elif key == "locks":
|
||||
if directive == "REPLACE":
|
||||
obj.locks.clear()
|
||||
obj.locks.add(init_spawn_value(val, str))
|
||||
obj.locks.add(init_spawn_value(val, str, caller=caller))
|
||||
elif key == "permissions":
|
||||
if directive == "REPLACE":
|
||||
obj.permissions.clear()
|
||||
obj.permissions.batch_add(*(init_spawn_value(perm, str) for perm in val))
|
||||
obj.permissions.batch_add(*(init_spawn_value(perm, str, caller=caller) for perm in val))
|
||||
elif key == "aliases":
|
||||
if directive == "REPLACE":
|
||||
obj.aliases.clear()
|
||||
obj.aliases.batch_add(*(init_spawn_value(alias, str) for alias in val))
|
||||
obj.aliases.batch_add(*(init_spawn_value(alias, str, caller=caller) for alias in val))
|
||||
elif key == "tags":
|
||||
if directive == "REPLACE":
|
||||
obj.tags.clear()
|
||||
obj.tags.batch_add(
|
||||
*(
|
||||
(init_spawn_value(ttag, str), tcategory, tdata)
|
||||
(init_spawn_value(ttag, str, caller=caller), tcategory, tdata)
|
||||
for ttag, tcategory, tdata in val
|
||||
)
|
||||
)
|
||||
|
|
@ -711,8 +713,8 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exac
|
|||
obj.attributes.batch_add(
|
||||
*(
|
||||
(
|
||||
init_spawn_value(akey, str),
|
||||
init_spawn_value(aval, value_to_obj),
|
||||
init_spawn_value(akey, str, caller=caller),
|
||||
init_spawn_value(aval, value_to_obj, caller=caller),
|
||||
acategory,
|
||||
alocks,
|
||||
)
|
||||
|
|
@ -723,7 +725,7 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exac
|
|||
# we don't auto-rerun exec statements, it would be huge security risk!
|
||||
pass
|
||||
else:
|
||||
obj.attributes.add(key, init_spawn_value(val, value_to_obj))
|
||||
obj.attributes.add(key, init_spawn_value(val, value_to_obj, caller=caller))
|
||||
elif directive == "REMOVE":
|
||||
do_save = True
|
||||
if key == "key":
|
||||
|
|
@ -836,7 +838,7 @@ def batch_create_object(*objparams):
|
|||
# Spawner mechanism
|
||||
|
||||
|
||||
def spawn(*prototypes, **kwargs):
|
||||
def spawn(*prototypes, caller=None, **kwargs):
|
||||
"""
|
||||
Spawn a number of prototyped objects.
|
||||
|
||||
|
|
@ -845,6 +847,7 @@ def spawn(*prototypes, **kwargs):
|
|||
prototype_key (will be used to find the prototype) or a full prototype
|
||||
dictionary. These will be batched-spawned as one object each.
|
||||
Keyword Args:
|
||||
caller (Object or Account, optional): This may be used by protfuncs to do access checks.
|
||||
prototype_modules (str or list): A python-path to a prototype
|
||||
module, or a list of such paths. These will be used to build
|
||||
the global protparents dictionary accessible by the input
|
||||
|
|
@ -910,39 +913,39 @@ def spawn(*prototypes, **kwargs):
|
|||
"key",
|
||||
"Spawned-{}".format(hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:6]),
|
||||
)
|
||||
create_kwargs["db_key"] = init_spawn_value(val, str)
|
||||
create_kwargs["db_key"] = init_spawn_value(val, str, caller=caller)
|
||||
|
||||
val = prot.pop("location", None)
|
||||
create_kwargs["db_location"] = init_spawn_value(val, value_to_obj)
|
||||
create_kwargs["db_location"] = init_spawn_value(val, value_to_obj, caller=caller)
|
||||
|
||||
val = prot.pop("home", None)
|
||||
if val:
|
||||
create_kwargs["db_home"] = init_spawn_value(val, value_to_obj)
|
||||
create_kwargs["db_home"] = init_spawn_value(val, value_to_obj, caller=caller)
|
||||
else:
|
||||
try:
|
||||
create_kwargs["db_home"] = init_spawn_value(settings.DEFAULT_HOME, value_to_obj)
|
||||
create_kwargs["db_home"] = init_spawn_value(settings.DEFAULT_HOME, value_to_obj, caller=caller)
|
||||
except ObjectDB.DoesNotExist:
|
||||
# settings.DEFAULT_HOME not existing is common for unittests
|
||||
pass
|
||||
|
||||
val = prot.pop("destination", None)
|
||||
create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj)
|
||||
create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj, caller=caller)
|
||||
|
||||
val = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
|
||||
create_kwargs["db_typeclass_path"] = init_spawn_value(val, str)
|
||||
create_kwargs["db_typeclass_path"] = init_spawn_value(val, str, caller=caller)
|
||||
|
||||
# extract calls to handlers
|
||||
val = prot.pop("permissions", [])
|
||||
permission_string = init_spawn_value(val, make_iter)
|
||||
permission_string = init_spawn_value(val, make_iter, caller=caller)
|
||||
val = prot.pop("locks", "")
|
||||
lock_string = init_spawn_value(val, str)
|
||||
lock_string = init_spawn_value(val, str, caller=caller)
|
||||
val = prot.pop("aliases", [])
|
||||
alias_string = init_spawn_value(val, make_iter)
|
||||
alias_string = init_spawn_value(val, make_iter, caller=caller)
|
||||
|
||||
val = prot.pop("tags", [])
|
||||
tags = []
|
||||
for (tag, category, *data) in val:
|
||||
tags.append((init_spawn_value(tag, str), category, data[0] if data else None))
|
||||
tags.append((init_spawn_value(tag, str, caller=caller), category, data[0] if data else None))
|
||||
|
||||
prototype_key = prototype.get("prototype_key", None)
|
||||
if prototype_key:
|
||||
|
|
@ -950,11 +953,11 @@ def spawn(*prototypes, **kwargs):
|
|||
tags.append((prototype_key, PROTOTYPE_TAG_CATEGORY))
|
||||
|
||||
val = prot.pop("exec", "")
|
||||
execs = init_spawn_value(val, make_iter)
|
||||
execs = init_spawn_value(val, make_iter, caller=caller)
|
||||
|
||||
# extract ndb assignments
|
||||
nattributes = dict(
|
||||
(key.split("_", 1)[1], init_spawn_value(val, value_to_obj))
|
||||
(key.split("_", 1)[1], init_spawn_value(val, value_to_obj, caller=caller))
|
||||
for key, val in prot.items()
|
||||
if key.startswith("ndb_")
|
||||
)
|
||||
|
|
@ -963,7 +966,7 @@ def spawn(*prototypes, **kwargs):
|
|||
val = make_iter(prot.pop("attrs", []))
|
||||
attributes = []
|
||||
for (attrname, value, *rest) in val:
|
||||
attributes.append((attrname, init_spawn_value(value),
|
||||
attributes.append((attrname, init_spawn_value(value, caller=caller),
|
||||
rest[0] if rest else None, rest[1] if len(rest) > 1 else None))
|
||||
|
||||
simple_attributes = []
|
||||
|
|
@ -975,7 +978,7 @@ def spawn(*prototypes, **kwargs):
|
|||
continue
|
||||
else:
|
||||
simple_attributes.append(
|
||||
(key, init_spawn_value(value, value_to_obj_or_any), None, None)
|
||||
(key, init_spawn_value(value, value_to_obj_or_any, caller=caller), None, None)
|
||||
)
|
||||
|
||||
attributes = attributes + simple_attributes
|
||||
|
|
|
|||
|
|
@ -338,223 +338,19 @@ class TestProtLib(EvenniaTest):
|
|||
self.assertEqual(match, [self.prot])
|
||||
|
||||
|
||||
@override_settings(PROT_FUNC_MODULES=["evennia.prototypes.protfuncs"], CLIENT_DEFAULT_WIDTH=20)
|
||||
class TestProtFuncs(EvenniaTest):
|
||||
def setUp(self):
|
||||
super(TestProtFuncs, self).setUp()
|
||||
self.prot = {
|
||||
"prototype_key": "test_prototype",
|
||||
"prototype_desc": "testing prot",
|
||||
"key": "ExampleObj",
|
||||
}
|
||||
|
||||
@mock.patch("evennia.prototypes.protfuncs.base_random", new=mock.MagicMock(return_value=0.5))
|
||||
@mock.patch("evennia.prototypes.protfuncs.base_randint", new=mock.MagicMock(return_value=5))
|
||||
def test_protfuncs(self):
|
||||
self.assertEqual(protlib.protfunc_parser("$random()"), 0.5)
|
||||
self.assertEqual(protlib.protfunc_parser("$randint(1, 10)"), 5)
|
||||
self.assertEqual(protlib.protfunc_parser("$left_justify( foo )"), "foo ")
|
||||
self.assertEqual(protlib.protfunc_parser("$right_justify( foo )"), " foo")
|
||||
self.assertEqual(protlib.protfunc_parser("$center_justify(foo )"), " foo ")
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("$full_justify(foo bar moo too)"), "foo bar moo too"
|
||||
)
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("$right_justify( foo )", testing=True),
|
||||
("unexpected indent (<unknown>, line 1)", " foo"),
|
||||
)
|
||||
|
||||
@override_settings(PROT_FUNC_MODULES=["evennia.prototypes.protfuncs"])
|
||||
def test_protkey_protfunc(self):
|
||||
test_prot = {"key1": "value1", "key2": 2}
|
||||
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("$protkey(key1)", testing=True, prototype=test_prot),
|
||||
(None, "value1"),
|
||||
"value1",
|
||||
)
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("$protkey(key2)", testing=True, prototype=test_prot), (None, 2)
|
||||
)
|
||||
|
||||
self.assertEqual(protlib.protfunc_parser("$add(1, 2)"), 3)
|
||||
self.assertEqual(protlib.protfunc_parser("$add(10, 25)"), 35)
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("$add('''[1,2,3]''', '''[4,5,6]''')"), [1, 2, 3, 4, 5, 6]
|
||||
)
|
||||
self.assertEqual(protlib.protfunc_parser("$add(foo, bar)"), "foo bar")
|
||||
|
||||
self.assertEqual(protlib.protfunc_parser("$sub(5, 2)"), 3)
|
||||
self.assertRaises(TypeError, protlib.protfunc_parser, "$sub(5, test)")
|
||||
|
||||
self.assertEqual(protlib.protfunc_parser("$mult(5, 2)"), 10)
|
||||
self.assertEqual(protlib.protfunc_parser("$mult( 5 , 10)"), 50)
|
||||
self.assertEqual(protlib.protfunc_parser("$mult('foo',3)"), "foofoofoo")
|
||||
self.assertEqual(protlib.protfunc_parser("$mult(foo,3)"), "foofoofoo")
|
||||
self.assertRaises(TypeError, protlib.protfunc_parser, "$mult(foo, foo)")
|
||||
|
||||
self.assertEqual(protlib.protfunc_parser("$toint(5.3)"), 5)
|
||||
|
||||
self.assertEqual(protlib.protfunc_parser("$div(5, 2)"), 2.5)
|
||||
self.assertEqual(protlib.protfunc_parser("$toint($div(5, 2))"), 2)
|
||||
self.assertEqual(protlib.protfunc_parser("$sub($add(5, 3), $add(10, 2))"), -4)
|
||||
|
||||
self.assertEqual(protlib.protfunc_parser("$eval('2')"), "2")
|
||||
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("$eval(['test', 1, '2', 3.5, \"foo\"])"),
|
||||
["test", 1, "2", 3.5, "foo"],
|
||||
)
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("$eval({'test': '1', 2:3, 3: $toint(3.5)})"),
|
||||
{"test": "1", 2: 3, 3: 3},
|
||||
)
|
||||
|
||||
# no object search
|
||||
odbref = self.obj1.dbref
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("obj({})".format(odbref), session=self.session),
|
||||
"obj({})".format(odbref),
|
||||
)
|
||||
mocked__obj_search.assert_not_called()
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("dbref({})".format(odbref), session=self.session),
|
||||
"dbref({})".format(odbref),
|
||||
)
|
||||
mocked__obj_search.assert_not_called()
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("stone(#12345)", session=self.session), "stone(#12345)"
|
||||
)
|
||||
mocked__obj_search.assert_not_called()
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(protlib.protfunc_parser(odbref, session=self.session), odbref)
|
||||
mocked__obj_search.assert_not_called()
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(protlib.protfunc_parser("#12345", session=self.session), "#12345")
|
||||
mocked__obj_search.assert_not_called()
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("nothing({})".format(odbref), session=self.session),
|
||||
"nothing({})".format(odbref),
|
||||
)
|
||||
mocked__obj_search.assert_not_called()
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(protlib.protfunc_parser("(#12345)", session=self.session), "(#12345)")
|
||||
mocked__obj_search.assert_not_called()
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("obj(Char)", session=self.session), "obj(Char)"
|
||||
)
|
||||
mocked__obj_search.assert_not_called()
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("objlist({})".format(odbref), session=self.session),
|
||||
"objlist({})".format(odbref),
|
||||
)
|
||||
mocked__obj_search.assert_not_called()
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("dbref(Char)", session=self.session), "dbref(Char)"
|
||||
)
|
||||
mocked__obj_search.assert_not_called()
|
||||
|
||||
# obj search happens
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("$objlist({})".format(odbref), session=self.session),
|
||||
[odbref],
|
||||
)
|
||||
mocked__obj_search.assert_called_once()
|
||||
assert (odbref,) == mocked__obj_search.call_args[0]
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("$obj({})".format(odbref), session=self.session), odbref
|
||||
)
|
||||
mocked__obj_search.assert_called_once()
|
||||
assert (odbref,) == mocked__obj_search.call_args[0]
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("$dbref({})".format(odbref), session=self.session), odbref
|
||||
)
|
||||
mocked__obj_search.assert_called_once()
|
||||
assert (odbref,) == mocked__obj_search.call_args[0]
|
||||
|
||||
cdbref = self.char1.dbref
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), cdbref)
|
||||
mocked__obj_search.assert_called_once()
|
||||
assert ("Char",) == mocked__obj_search.call_args[0]
|
||||
|
||||
# bad invocation
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertEqual(
|
||||
protlib.protfunc_parser("$badfunc(#112345)", session=self.session), "<UNKNOWN>"
|
||||
)
|
||||
mocked__obj_search.assert_not_called()
|
||||
|
||||
with mock.patch(
|
||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||
) as mocked__obj_search:
|
||||
self.assertRaises(ValueError, protlib.protfunc_parser, "$dbref(Char)")
|
||||
mocked__obj_search.assert_not_called()
|
||||
|
||||
self.assertEqual(
|
||||
protlib.value_to_obj(protlib.protfunc_parser(cdbref, session=self.session)), self.char1
|
||||
)
|
||||
self.assertEqual(
|
||||
protlib.value_to_obj_or_any(protlib.protfunc_parser(cdbref, session=self.session)),
|
||||
self.char1,
|
||||
)
|
||||
self.assertEqual(
|
||||
protlib.value_to_obj_or_any(
|
||||
protlib.protfunc_parser("[1,2,3,'{}',5]".format(cdbref), session=self.session)
|
||||
),
|
||||
[1, 2, 3, self.char1, 5],
|
||||
protlib.protfunc_parser("$protkey(key2)", testing=True, prototype=test_prot),
|
||||
2
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,21 @@ def check_errors(settings):
|
|||
"Update your settings file (see evennia/settings_default.py "
|
||||
"for more info)."
|
||||
)
|
||||
depstring = (
|
||||
"settings.{} was renamed to {}. Update your settings file (the FuncParser "
|
||||
"replaces and generalizes that which inlinefuncs used to do).")
|
||||
if hasattr(settings, "INLINEFUNC_ENABLED"):
|
||||
raise DeprecationWarning(depstring.format(
|
||||
"settings.INLINEFUNC_ENABLED", "FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLE"))
|
||||
if hasattr(settings, "INLINEFUNC_STACK_MAXSIZE"):
|
||||
raise DeprecationWarning(depstring.format(
|
||||
"settings.INLINEFUNC_STACK_MAXSIZE", "FUNCPARSER_MAX_NESTING"))
|
||||
if hasattr(settings, "INLINEFUNC_MODULES"):
|
||||
raise DeprecationWarning(depstring.format(
|
||||
"settings.INLINEFUNC_MODULES", "FUNCPARSER_OUTGOING_MESSAGES_MODULES"))
|
||||
if hasattr(settings, "PROTFUNC_MODULES"):
|
||||
raise DeprecationWarning(depstring.format(
|
||||
"settings.PROTFUNC_MODULES", "FUNCPARSER_PROTOTYPE_VALUE_MODULES"))
|
||||
|
||||
gametime_deprecation = (
|
||||
"The settings TIME_SEC_PER_MIN, TIME_MIN_PER_HOUR,"
|
||||
|
|
|
|||
|
|
@ -28,10 +28,9 @@ from evennia.utils.utils import (
|
|||
from evennia.server.portal import amp
|
||||
from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT
|
||||
from evennia.server.signals import SIGNAL_ACCOUNT_POST_FIRST_LOGIN, SIGNAL_ACCOUNT_POST_LAST_LOGOUT
|
||||
from evennia.utils.inlinefuncs import parse_inlinefunc
|
||||
from codecs import decode as codecs_decode
|
||||
|
||||
_INLINEFUNC_ENABLED = settings.INLINEFUNC_ENABLED
|
||||
_FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED
|
||||
|
||||
# delayed imports
|
||||
_AccountDB = None
|
||||
|
|
@ -59,6 +58,9 @@ _DELAY_CMD_LOGINSTART = settings.DELAY_CMD_LOGINSTART
|
|||
_MAX_SERVER_COMMANDS_PER_SECOND = 100.0
|
||||
_MAX_SESSION_COMMANDS_PER_SECOND = 5.0
|
||||
_MODEL_MAP = None
|
||||
_FUNCPARSER = None
|
||||
|
||||
|
||||
|
||||
# input handlers
|
||||
|
||||
|
|
@ -151,7 +153,8 @@ class SessionHandler(dict):
|
|||
|
||||
def clean_senddata(self, session, kwargs):
|
||||
"""
|
||||
Clean up data for sending across the AMP wire. Also apply INLINEFUNCS.
|
||||
Clean up data for sending across the AMP wire. Also apply the
|
||||
FuncParser using callables from `settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES`.
|
||||
|
||||
Args:
|
||||
session (Session): The relevant session instance.
|
||||
|
|
@ -167,10 +170,15 @@ class SessionHandler(dict):
|
|||
Returns:
|
||||
kwargs (dict): A cleaned dictionary of cmdname:[[args],{kwargs}] pairs,
|
||||
where the keys, args and kwargs have all been converted to
|
||||
send-safe entities (strings or numbers), and inlinefuncs have been
|
||||
send-safe entities (strings or numbers), and funcparser parsing has been
|
||||
applied.
|
||||
|
||||
"""
|
||||
global _FUNCPARSER
|
||||
if not _FUNCPARSER:
|
||||
from evennia.utils.funcparser import FuncParser
|
||||
_FUNCPARSER = FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULE, raise_errors=True)
|
||||
|
||||
options = kwargs.pop("options", None) or {}
|
||||
raw = options.get("raw", False)
|
||||
strip_inlinefunc = options.get("strip_inlinefunc", False)
|
||||
|
|
@ -202,9 +210,10 @@ class SessionHandler(dict):
|
|||
elif isinstance(data, (str, bytes)):
|
||||
data = _utf8(data)
|
||||
|
||||
if _INLINEFUNC_ENABLED and not raw and isinstance(self, ServerSessionHandler):
|
||||
# only parse inlinefuncs on the outgoing path (sessionhandler->)
|
||||
data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
|
||||
if _FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED and not raw and isinstance(self, ServerSessionHandler):
|
||||
# only apply funcparser on the outgoing path (sessionhandler->)
|
||||
# data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
|
||||
data = _FUNCPARSER.parse(data, strip=strip_inlinefunc, session=session)
|
||||
|
||||
return str(data)
|
||||
elif (
|
||||
|
|
|
|||
|
|
@ -598,23 +598,31 @@ TIME_GAME_EPOCH = None
|
|||
TIME_IGNORE_DOWNTIMES = False
|
||||
|
||||
######################################################################
|
||||
# Inlinefunc, PrototypeFuncs
|
||||
# FuncParser
|
||||
#
|
||||
# Strings parsed with the FuncParser can contain 'callables' on the
|
||||
# form $funcname(args,kwargs), which will lead to actual Python functions
|
||||
# being executed.
|
||||
######################################################################
|
||||
# Evennia supports inline function preprocessing. This allows users
|
||||
# to supply inline calls on the form $func(arg, arg, ...) to do
|
||||
# session-aware text formatting and manipulation on the fly. If
|
||||
# disabled, such inline functions will not be parsed.
|
||||
INLINEFUNC_ENABLED = False
|
||||
# This defined how deeply nested inlinefuncs can be. Set to <=0 to
|
||||
# disable (not recommended, this is a safeguard against infinite loops).
|
||||
INLINEFUNC_STACK_MAXSIZE = 20
|
||||
# This changes the start-symbol for the funcparser callable. Note that
|
||||
# this will make a lot of documentation invalid and there may also be
|
||||
# other unexpected side effects, so change with caution.
|
||||
FUNCPARSER_START_CHAR = '$'
|
||||
# The symbol to use to escape Func
|
||||
FUNCPARSER_ESCAPE_CHAR = '\\'
|
||||
# This is the global max nesting-level for nesting functions in
|
||||
# the funcparser. This protects against infinite loops.
|
||||
FUNCPARSER_MAX_NESTING = 20
|
||||
# Activate funcparser for all outgoing strings. The current Session
|
||||
# will be passed into the parser (used to be called inlinefuncs)
|
||||
FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = False
|
||||
# Only functions defined globally (and not starting with '_') in
|
||||
# these modules will be considered valid inlinefuncs. The list
|
||||
# is loaded from left-to-right, same-named functions will overload
|
||||
INLINEFUNC_MODULES = ["evennia.utils.inlinefuncs", "server.conf.inlinefuncs"]
|
||||
# Module holding handlers for ProtFuncs. These allow for embedding
|
||||
# functional code in prototypes and has the same syntax as inlinefuncs.
|
||||
PROTOTYPEFUNC_MODULES = ["evennia.utils.prototypefuncs", "server.conf.prototypefuncs"]
|
||||
FUNCPARSER_OUTGOING_MESSAGES_MODULES = ["evennia.utils.funcparser", "server.conf.inlinefuncs"]
|
||||
# Prototype values are also parsed with FuncParser. These modules
|
||||
# define which $func callables are available to use in prototypes.
|
||||
FUNCPARSER_PROTOTYPE_PARSING_MODULES = ["evennia.prototypes.protfuncs", "server.conf.prototypefuncs"]
|
||||
|
||||
######################################################################
|
||||
# Global Scripts
|
||||
|
|
|
|||
1182
evennia/utils/funcparser.py
Normal file
1182
evennia/utils/funcparser.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,625 +0,0 @@
|
|||
"""
|
||||
Inline functions (nested form).
|
||||
|
||||
This parser accepts nested inlinefunctions on the form
|
||||
|
||||
```python
|
||||
$funcname(arg, arg, ...)
|
||||
```
|
||||
|
||||
embedded in any text where any arg can be another ``$funcname()`` call.
|
||||
This functionality is turned off by default - to activate,
|
||||
`settings.INLINEFUNC_ENABLED` must be set to `True`.
|
||||
|
||||
Each token starts with `$funcname(` where there must be no space
|
||||
between the `$funcname` and `"("`. The inlinefunc ends with a matched ending parentesis.
|
||||
`")"`.
|
||||
|
||||
Inside the inlinefunc definition, one can use `\` to escape. This is
|
||||
mainly needed for escaping commas in flowing text (which would
|
||||
otherwise be interpreted as an argument separator), or to escape `)`
|
||||
when not intended to close the function block. Enclosing text in
|
||||
matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will
|
||||
also escape *everything* within without needing to escape individual
|
||||
characters.
|
||||
|
||||
The available inlinefuncs are defined as global-level functions in
|
||||
modules defined by `settings.INLINEFUNC_MODULES`. They are identified
|
||||
by their function name (and ignored if this name starts with `_`). They
|
||||
should be on the following form:
|
||||
|
||||
```python
|
||||
def funcname (*args, **kwargs):
|
||||
# ...
|
||||
```
|
||||
|
||||
Here, the arguments given to `$funcname(arg1,arg2)` will appear as the
|
||||
`*args` tuple. This will be populated by the arguments given to the
|
||||
inlinefunc in-game - the only part that will be available from
|
||||
in-game. `**kwargs` are not supported from in-game but are only used
|
||||
internally by Evennia to make details about the caller available to
|
||||
the function. The kwarg passed to all functions is `session`, the
|
||||
Sessionobject for the object seeing the string. This may be `None` if
|
||||
the string is sent to a non-puppetable object. The inlinefunc should
|
||||
never raise an exception.
|
||||
|
||||
There are two reserved function names:
|
||||
|
||||
- "nomatch": This is called if the user uses a functionname that is
|
||||
not registered. The nomatch function will get the name of the
|
||||
not-found function as its first argument followed by the normal
|
||||
arguments to the given function. If not defined the default effect is
|
||||
to print `<UNKNOWN>` to replace the unknown function.
|
||||
- "stackfull": This is called when the maximum nested function stack is reached.
|
||||
When this happens, the original parsed string is returned and the result of
|
||||
the `stackfull` inlinefunc is appended to the end. By default this is an
|
||||
error message.
|
||||
|
||||
Syntax errors, notably failing to completely closing all inlinefunc
|
||||
blocks, will lead to the entire string remaining unparsed. Inlineparsing should
|
||||
never traceback.
|
||||
|
||||
----
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import fnmatch
|
||||
import random as base_random
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.utils import utils, logger
|
||||
|
||||
# The stack size is a security measure. Set to <=0 to disable.
|
||||
_STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE
|
||||
|
||||
|
||||
# example/testing inline functions
|
||||
|
||||
|
||||
def random(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Returns a random number between
|
||||
0 and 1, from 0 to a maximum value, or within a given range (inclusive).
|
||||
|
||||
Args:
|
||||
minval (str, optional): Minimum value. If not given, assumed 0.
|
||||
maxval (str, optional): Maximum value.
|
||||
|
||||
Keyword argumuents:
|
||||
session (Session): Session getting the string.
|
||||
|
||||
Notes:
|
||||
If either of the min/maxvalue has a '.' in it, a floating-point random
|
||||
value will be returned. Otherwise it will be an integer value in the
|
||||
given range.
|
||||
|
||||
Example:
|
||||
`$random()`
|
||||
`$random(5)`
|
||||
`$random(5, 10)`
|
||||
|
||||
"""
|
||||
nargs = len(args)
|
||||
if nargs == 1:
|
||||
# only maxval given
|
||||
minval, maxval = "0", args[0]
|
||||
elif nargs > 1:
|
||||
minval, maxval = args[:2]
|
||||
else:
|
||||
minval, maxval = ("0", "1")
|
||||
|
||||
if "." in minval or "." in maxval:
|
||||
# float mode
|
||||
try:
|
||||
minval, maxval = float(minval), float(maxval)
|
||||
except ValueError:
|
||||
minval, maxval = 0, 1
|
||||
return "{:.2f}".format(minval + maxval * base_random.random())
|
||||
else:
|
||||
# int mode
|
||||
try:
|
||||
minval, maxval = int(minval), int(maxval)
|
||||
except ValueError:
|
||||
minval, maxval = 0, 1
|
||||
return str(base_random.randint(minval, maxval))
|
||||
|
||||
|
||||
def pad(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Pads text to given width.
|
||||
|
||||
Args:
|
||||
text (str, optional): Text to pad.
|
||||
width (str, optional): Will be converted to integer. Width
|
||||
of padding.
|
||||
align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'.
|
||||
fillchar (str, optional): Character used for padding. Defaults to a
|
||||
space.
|
||||
|
||||
Keyword Args:
|
||||
session (Session): Session performing the pad.
|
||||
|
||||
Example:
|
||||
`$pad(text, width, align, fillchar)`
|
||||
|
||||
"""
|
||||
text, width, align, fillchar = "", 78, "c", " "
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
text = args[0]
|
||||
if nargs > 1:
|
||||
width = int(args[1]) if args[1].strip().isdigit() else 78
|
||||
if nargs > 2:
|
||||
align = args[2] if args[2] in ("c", "l", "r") else "c"
|
||||
if nargs > 3:
|
||||
fillchar = args[3]
|
||||
return utils.pad(text, width=width, align=align, fillchar=fillchar)
|
||||
|
||||
|
||||
def crop(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Crops ingoing text to given widths.
|
||||
|
||||
Args:
|
||||
text (str, optional): Text to crop.
|
||||
width (str, optional): Will be converted to an integer. Width of
|
||||
crop in characters.
|
||||
suffix (str, optional): End string to mark the fact that a part
|
||||
of the string was cropped. Defaults to `[...]`.
|
||||
Keyword Args:
|
||||
session (Session): Session performing the crop.
|
||||
|
||||
Example:
|
||||
`$crop(text, width=78, suffix='[...]')`
|
||||
|
||||
"""
|
||||
text, width, suffix = "", 78, "[...]"
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
text = args[0]
|
||||
if nargs > 1:
|
||||
width = int(args[1]) if args[1].strip().isdigit() else 78
|
||||
if nargs > 2:
|
||||
suffix = args[2]
|
||||
return utils.crop(text, width=width, suffix=suffix)
|
||||
|
||||
|
||||
def space(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Inserts an arbitrary number of spaces. Defaults to 4 spaces.
|
||||
|
||||
Args:
|
||||
spaces (int, optional): The number of spaces to insert.
|
||||
|
||||
Keyword Args:
|
||||
session (Session): Session performing the crop.
|
||||
|
||||
Example:
|
||||
`$space(20)`
|
||||
|
||||
"""
|
||||
width = 4
|
||||
if args:
|
||||
width = abs(int(args[0])) if args[0].strip().isdigit() else 4
|
||||
return " " * width
|
||||
|
||||
|
||||
def clr(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Colorizes nested text.
|
||||
|
||||
Args:
|
||||
startclr (str, optional): An ANSI color abbreviation without the
|
||||
prefix `|`, such as `r` (red foreground) or `[r` (red background).
|
||||
text (str, optional): Text
|
||||
endclr (str, optional): The color to use at the end of the string. Defaults
|
||||
to `|n` (reset-color).
|
||||
Keyword Args:
|
||||
session (Session): Session object triggering inlinefunc.
|
||||
|
||||
Example:
|
||||
`$clr(startclr, text, endclr)`
|
||||
|
||||
"""
|
||||
text = ""
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
color = args[0].strip()
|
||||
if nargs > 1:
|
||||
text = args[1]
|
||||
text = "|" + color + text
|
||||
if nargs > 2:
|
||||
text += "|" + args[2].strip()
|
||||
else:
|
||||
text += "|n"
|
||||
return text
|
||||
|
||||
|
||||
def null(*args, **kwargs):
|
||||
return args[0] if args else ""
|
||||
|
||||
|
||||
def nomatch(name, *args, **kwargs):
|
||||
"""
|
||||
Default implementation of nomatch returns the function as-is as a string.
|
||||
|
||||
"""
|
||||
kwargs.pop("inlinefunc_stack_depth", None)
|
||||
kwargs.pop("session")
|
||||
|
||||
return "${name}({args}{kwargs})".format(
|
||||
name=name,
|
||||
args=",".join(args),
|
||||
kwargs=",".join("{}={}".format(key, val) for key, val in kwargs.items()),
|
||||
)
|
||||
|
||||
|
||||
_INLINE_FUNCS = {}
|
||||
|
||||
# we specify a default nomatch function to use if no matching func was
|
||||
# found. This will be overloaded by any nomatch function defined in
|
||||
# the imported modules.
|
||||
_DEFAULT_FUNCS = {
|
||||
"nomatch": lambda *args, **kwargs: "<UNKNOWN>",
|
||||
"stackfull": lambda *args, **kwargs: "\n (not parsed: ",
|
||||
}
|
||||
|
||||
_INLINE_FUNCS.update(_DEFAULT_FUNCS)
|
||||
|
||||
# load custom inline func modules.
|
||||
for module in utils.make_iter(settings.INLINEFUNC_MODULES):
|
||||
try:
|
||||
_INLINE_FUNCS.update(utils.callables_from_module(module))
|
||||
except ImportError as err:
|
||||
if module == "server.conf.inlinefuncs":
|
||||
# a temporary warning since the default module changed name
|
||||
raise ImportError(
|
||||
"Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should "
|
||||
"be renamed to mygame/server/conf/inlinefuncs.py (note "
|
||||
"the S at the end)." % err
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
# regex definitions
|
||||
|
||||
_RE_STARTTOKEN = re.compile(r"(?<!\\)\$(\w+)\(") # unescaped $funcname( (start of function call)
|
||||
|
||||
# note: this regex can be experimented with at https://regex101.com/r/kGR3vE/2
|
||||
_RE_TOKEN = re.compile(
|
||||
r"""
|
||||
(?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # single-triplets escape all inside
|
||||
(?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # double-triplets escape all inside
|
||||
(?P<comma>(?<!\\)\,)| # , (argument sep)
|
||||
(?P<end>(?<!\\)\))| # ) (possible end of func call)
|
||||
(?P<leftparens>(?<!\\)\()| # ( (lone left-parens)
|
||||
(?P<start>(?<!\\)\$\w+\()| # $funcname (start of func call)
|
||||
(?P<escaped> # escaped tokens to re-insert sans backslash
|
||||
\\\'|\\\"|\\\)|\\\$\w+\(|\\\()|
|
||||
(?P<rest> # everything else to re-insert verbatim
|
||||
\$(?!\w+\()|\'|\"|\\|[^),$\'\"\\\(]+)""",
|
||||
re.UNICODE | re.IGNORECASE | re.VERBOSE | re.DOTALL,
|
||||
)
|
||||
|
||||
# Cache for function lookups.
|
||||
_PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000)
|
||||
|
||||
|
||||
class ParseStack(list):
|
||||
"""
|
||||
Custom stack that always concatenates strings together when the
|
||||
strings are added next to one another. Tuples are stored
|
||||
separately and None is used to mark that a string should be broken
|
||||
up into a new chunk. Below is the resulting stack after separately
|
||||
appending 3 strings, None, 2 strings, a tuple and finally 2
|
||||
strings:
|
||||
|
||||
[string + string + string,
|
||||
None
|
||||
string + string,
|
||||
tuple,
|
||||
string + string]
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# always start stack with the empty string
|
||||
list.append(self, "")
|
||||
# indicates if the top of the stack is a string or not
|
||||
self._string_last = True
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
super().__eq__(other)
|
||||
and hasattr(other, "_string_last")
|
||||
and self._string_last == other._string_last
|
||||
)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def append(self, item):
|
||||
"""
|
||||
The stack will merge strings, add other things as normal
|
||||
"""
|
||||
if isinstance(item, str):
|
||||
if self._string_last:
|
||||
self[-1] += item
|
||||
else:
|
||||
list.append(self, item)
|
||||
self._string_last = True
|
||||
else:
|
||||
# everything else is added as normal
|
||||
list.append(self, item)
|
||||
self._string_last = False
|
||||
|
||||
|
||||
class InlinefuncError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False, **kwargs):
|
||||
"""
|
||||
Parse the incoming string.
|
||||
|
||||
Args:
|
||||
string (str): The incoming string to parse.
|
||||
strip (bool, optional): Whether to strip function calls rather than
|
||||
execute them.
|
||||
available_funcs (dict, optional): Define an alternative source of functions to parse for.
|
||||
If unset, use the functions found through `settings.INLINEFUNC_MODULES`.
|
||||
stacktrace (bool, optional): If set, print the stacktrace to log.
|
||||
Keyword Args:
|
||||
session (Session): This is sent to this function by Evennia when triggering
|
||||
it. It is passed to the inlinefunc.
|
||||
kwargs (any): All other kwargs are also passed on to the inlinefunc.
|
||||
|
||||
|
||||
"""
|
||||
global _PARSING_CACHE
|
||||
usecache = False
|
||||
if not available_funcs:
|
||||
available_funcs = _INLINE_FUNCS
|
||||
usecache = True
|
||||
else:
|
||||
# make sure the default keys are available, but also allow overriding
|
||||
tmp = _DEFAULT_FUNCS.copy()
|
||||
tmp.update(available_funcs)
|
||||
available_funcs = tmp
|
||||
|
||||
if usecache and string in _PARSING_CACHE:
|
||||
# stack is already cached
|
||||
stack = _PARSING_CACHE[string]
|
||||
elif not _RE_STARTTOKEN.search(string):
|
||||
# if there are no unescaped start tokens at all, return immediately.
|
||||
return string
|
||||
else:
|
||||
# no cached stack; build a new stack and continue
|
||||
stack = ParseStack()
|
||||
|
||||
# process string on stack
|
||||
ncallable = 0
|
||||
nlparens = 0
|
||||
nvalid = 0
|
||||
|
||||
if stacktrace:
|
||||
out = "STRING: {} =>".format(string)
|
||||
print(out)
|
||||
logger.log_info(out)
|
||||
|
||||
for match in _RE_TOKEN.finditer(string):
|
||||
gdict = match.groupdict()
|
||||
|
||||
if stacktrace:
|
||||
out = " MATCH: {}".format({key: val for key, val in gdict.items() if val})
|
||||
print(out)
|
||||
logger.log_info(out)
|
||||
|
||||
if gdict["singlequote"]:
|
||||
stack.append(gdict["singlequote"])
|
||||
elif gdict["doublequote"]:
|
||||
stack.append(gdict["doublequote"])
|
||||
elif gdict["leftparens"]:
|
||||
# we have a left-parens inside a callable
|
||||
if ncallable:
|
||||
nlparens += 1
|
||||
stack.append("(")
|
||||
elif gdict["end"]:
|
||||
if nlparens > 0:
|
||||
nlparens -= 1
|
||||
stack.append(")")
|
||||
continue
|
||||
if ncallable <= 0:
|
||||
stack.append(")")
|
||||
continue
|
||||
args = []
|
||||
while stack:
|
||||
operation = stack.pop()
|
||||
if callable(operation):
|
||||
if not strip:
|
||||
stack.append((operation, [arg for arg in reversed(args)]))
|
||||
ncallable -= 1
|
||||
break
|
||||
else:
|
||||
args.append(operation)
|
||||
elif gdict["start"]:
|
||||
funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1)
|
||||
try:
|
||||
# try to fetch the matching inlinefunc from storage
|
||||
stack.append(available_funcs[funcname])
|
||||
nvalid += 1
|
||||
except KeyError:
|
||||
stack.append(available_funcs["nomatch"])
|
||||
stack.append(funcname)
|
||||
stack.append(None)
|
||||
ncallable += 1
|
||||
elif gdict["escaped"]:
|
||||
# escaped tokens
|
||||
token = gdict["escaped"].lstrip("\\")
|
||||
stack.append(token)
|
||||
elif gdict["comma"]:
|
||||
if ncallable > 0:
|
||||
# commas outside strings and inside a callable are
|
||||
# used to mark argument separation - we use None
|
||||
# in the stack to indicate such a separation.
|
||||
stack.append(None)
|
||||
else:
|
||||
# no callable active - just a string
|
||||
stack.append(",")
|
||||
else:
|
||||
# the rest
|
||||
stack.append(gdict["rest"])
|
||||
|
||||
if ncallable > 0:
|
||||
# this means not all inlinefuncs were complete
|
||||
return string
|
||||
|
||||
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < nvalid:
|
||||
# if stack is larger than limit, throw away parsing
|
||||
return string + available_funcs["stackfull"](*args, **kwargs)
|
||||
elif usecache:
|
||||
# cache the stack - we do this also if we don't check the cache above
|
||||
_PARSING_CACHE[string] = stack
|
||||
|
||||
# run the stack recursively
|
||||
def _run_stack(item, depth=0):
|
||||
retval = item
|
||||
if isinstance(item, tuple):
|
||||
if strip:
|
||||
return ""
|
||||
else:
|
||||
func, arglist = item
|
||||
args = [""]
|
||||
for arg in arglist:
|
||||
if arg is None:
|
||||
# an argument-separating comma - start a new arg
|
||||
args.append("")
|
||||
else:
|
||||
# all other args should merge into one string
|
||||
args[-1] += _run_stack(arg, depth=depth + 1)
|
||||
# execute the inlinefunc at this point or strip it.
|
||||
kwargs["inlinefunc_stack_depth"] = depth
|
||||
retval = "" if strip else func(*args, **kwargs)
|
||||
return utils.to_str(retval)
|
||||
|
||||
retval = "".join(_run_stack(item) for item in stack)
|
||||
if stacktrace:
|
||||
out = "STACK: \n{} => {}\n".format(stack, retval)
|
||||
print(out)
|
||||
logger.log_info(out)
|
||||
|
||||
# execute the stack
|
||||
return retval
|
||||
|
||||
|
||||
def raw(string):
|
||||
"""
|
||||
Escape all inlinefuncs in a string so they won't get parsed.
|
||||
|
||||
Args:
|
||||
string (str): String with inlinefuncs to escape.
|
||||
"""
|
||||
|
||||
def _escape(match):
|
||||
return "\\" + match.group(0)
|
||||
|
||||
return _RE_STARTTOKEN.sub(_escape, string)
|
||||
|
||||
|
||||
#
|
||||
# Nick templating
|
||||
#
|
||||
|
||||
|
||||
"""
|
||||
This supports the use of replacement templates in nicks:
|
||||
|
||||
This happens in two steps:
|
||||
|
||||
1) The user supplies a template that is converted to a regex according
|
||||
to the unix-like templating language.
|
||||
2) This regex is tested against nicks depending on which nick replacement
|
||||
strategy is considered (most commonly inputline).
|
||||
3) If there is a template match and there are templating markers,
|
||||
these are replaced with the arguments actually given.
|
||||
|
||||
@desc $1 $2 $3
|
||||
|
||||
This will be converted to the following regex:
|
||||
|
||||
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
|
||||
|
||||
Supported template markers (through fnmatch)
|
||||
* matches anything (non-greedy) -> .*?
|
||||
? matches any single character ->
|
||||
[seq] matches any entry in sequence
|
||||
[!seq] matches entries not in sequence
|
||||
Custom arg markers
|
||||
$N argument position (1-99)
|
||||
|
||||
"""
|
||||
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
|
||||
_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)")
|
||||
_RE_NICK_SPACE = re.compile(r"\\ ")
|
||||
|
||||
|
||||
class NickTemplateInvalid(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def initialize_nick_templates(in_template, out_template):
|
||||
"""
|
||||
Initialize the nick templates for matching and remapping a string.
|
||||
|
||||
Args:
|
||||
in_template (str): The template to be used for nick recognition.
|
||||
out_template (str): The template to be used to replace the string
|
||||
matched by the in_template.
|
||||
|
||||
Returns:
|
||||
regex (regex): Regex to match against strings
|
||||
template (str): Template with markers {arg1}, {arg2}, etc for
|
||||
replacement using the standard .format method.
|
||||
|
||||
Raises:
|
||||
evennia.utils.inlinefuncs.NickTemplateInvalid: If the in/out template
|
||||
does not have a matching number of $args.
|
||||
|
||||
"""
|
||||
# create the regex for in_template
|
||||
regex_string = fnmatch.translate(in_template)
|
||||
n_inargs = len(_RE_NICK_ARG.findall(regex_string))
|
||||
regex_string = _RE_NICK_SPACE.sub("\s+", regex_string)
|
||||
regex_string = _RE_NICK_ARG.sub(lambda m: "(?P<arg%s>.+?)" % m.group(2), regex_string)
|
||||
|
||||
# create the out_template
|
||||
template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template)
|
||||
|
||||
# validate the tempaltes - they should at least have the same number of args
|
||||
n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
|
||||
if n_inargs != n_outargs:
|
||||
raise NickTemplateInvalid
|
||||
|
||||
return re.compile(regex_string), template_string
|
||||
|
||||
|
||||
def parse_nick_template(string, template_regex, outtemplate):
|
||||
"""
|
||||
Parse a text using a template and map it to another template
|
||||
|
||||
Args:
|
||||
string (str): The input string to processj
|
||||
template_regex (regex): A template regex created with
|
||||
initialize_nick_template.
|
||||
outtemplate (str): The template to which to map the matches
|
||||
produced by the template_regex. This should have $1, $2,
|
||||
etc to match the regex.
|
||||
|
||||
"""
|
||||
match = template_regex.match(string)
|
||||
if match:
|
||||
return outtemplate.format(**match.groupdict())
|
||||
return string
|
||||
466
evennia/utils/tests/test_funcparser.py
Normal file
466
evennia/utils/tests/test_funcparser.py
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
"""
|
||||
|
||||
Test the funcparser module.
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
from ast import literal_eval
|
||||
from simpleeval import simple_eval
|
||||
from parameterized import parameterized
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from evennia.utils import funcparser, test_resources
|
||||
|
||||
|
||||
def _test_callable(*args, **kwargs):
|
||||
kwargs.pop('funcparser', None)
|
||||
kwargs.pop('raise_errors', None)
|
||||
argstr = ", ".join(args)
|
||||
kwargstr = ""
|
||||
if kwargs:
|
||||
kwargstr = (", " if args else "") + (
|
||||
", ".join(f"{key}={val}" for key, val in kwargs.items()))
|
||||
return f"_test({argstr}{kwargstr})"
|
||||
|
||||
def _repl_callable(*args, **kwargs):
|
||||
if args:
|
||||
return f"r{args[0]}r"
|
||||
return "rr"
|
||||
|
||||
def _double_callable(*args, **kwargs):
|
||||
if args:
|
||||
try:
|
||||
return int(args[0]) * 2
|
||||
except ValueError:
|
||||
pass
|
||||
return 'N/A'
|
||||
|
||||
def _eval_callable(*args, **kwargs):
|
||||
if args:
|
||||
return simple_eval(args[0])
|
||||
return ''
|
||||
|
||||
def _clr_callable(*args, **kwargs):
|
||||
clr, string, *rest = args
|
||||
return f"|{clr}{string}|n"
|
||||
|
||||
def _typ_callable(*args, **kwargs):
|
||||
try:
|
||||
if isinstance(args[0], str):
|
||||
return type(literal_eval(args[0]))
|
||||
else:
|
||||
return type(args[0])
|
||||
except (SyntaxError, ValueError):
|
||||
return type("")
|
||||
|
||||
def _add_callable(*args, **kwargs):
|
||||
if len(args) > 1:
|
||||
return literal_eval(args[0]) + literal_eval(args[1])
|
||||
return ''
|
||||
|
||||
def _lit_callable(*args, **kwargs):
|
||||
return literal_eval(args[0])
|
||||
|
||||
def _lsum_callable(*args, **kwargs):
|
||||
if isinstance(args[0], (list, tuple)):
|
||||
return sum(val for val in args[0])
|
||||
return ''
|
||||
|
||||
_test_callables = {
|
||||
"foo": _test_callable,
|
||||
"bar": _test_callable,
|
||||
"with spaces": _test_callable,
|
||||
"repl": _repl_callable,
|
||||
"double": _double_callable,
|
||||
"eval": _eval_callable,
|
||||
"clr": _clr_callable,
|
||||
"typ": _typ_callable,
|
||||
"add": _add_callable,
|
||||
"lit": _lit_callable,
|
||||
"sum": _lsum_callable,
|
||||
}
|
||||
|
||||
class TestFuncParser(TestCase):
|
||||
"""
|
||||
Test the FuncParser class
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
|
||||
self.parser = funcparser.FuncParser(
|
||||
_test_callables
|
||||
)
|
||||
|
||||
@parameterized.expand([
|
||||
("Test normal string", "Test normal string"),
|
||||
("Test noargs1 $foo()", "Test noargs1 _test()"),
|
||||
("Test noargs2 $bar() etc.", "Test noargs2 _test() etc."),
|
||||
("Test noargs3 $with spaces() etc.", "Test noargs3 _test() etc."),
|
||||
("Test noargs4 $foo(), $bar() and $foo", "Test noargs4 _test(), _test() and $foo"),
|
||||
("$foo() Test noargs5", "_test() Test noargs5"),
|
||||
("Test args1 $foo(a,b,c)", "Test args1 _test(a, b, c)"),
|
||||
("Test args2 $bar(foo, bar, too)", "Test args2 _test(foo, bar, too)"),
|
||||
("Test args3 $bar(foo, bar, ' too')", "Test args3 _test(foo, bar, ' too')"),
|
||||
("Test args4 $foo('')", "Test args4 _test('')"),
|
||||
("Test args4 $foo(\"\")", "Test args4 _test(\"\")"),
|
||||
("Test args5 $foo(\(\))", "Test args5 _test(())"),
|
||||
("Test args6 $foo(\()", "Test args6 _test(()"),
|
||||
("Test args7 $foo(())", "Test args7 _test(())"),
|
||||
("Test args8 $foo())", "Test args8 _test())"),
|
||||
("Test args9 $foo(=)", "Test args9 _test(=)"),
|
||||
("Test args10 $foo(\,)", "Test args10 _test(,)"),
|
||||
("Test args10 $foo(',')", "Test args10 _test(',')"),
|
||||
("Test args11 $foo(()", "Test args11 $foo(()"), # invalid syntax
|
||||
("Test kwarg1 $bar(foo=1, bar='foo', too=ere)",
|
||||
"Test kwarg1 _test(foo=1, bar='foo', too=ere)"),
|
||||
("Test kwarg2 $bar(foo,bar,too=ere)",
|
||||
"Test kwarg2 _test(foo, bar, too=ere)"),
|
||||
("test kwarg3 $foo(foo = bar, bar = ere )",
|
||||
"test kwarg3 _test(foo=bar, bar=ere)"),
|
||||
("test kwarg4 $foo(foo =' bar ',\" bar \"= ere )",
|
||||
"test kwarg4 _test(foo=' bar ', \" bar \"=ere)"),
|
||||
("Test nest1 $foo($bar(foo,bar,too=ere))",
|
||||
"Test nest1 _test(_test(foo, bar, too=ere))"),
|
||||
("Test nest2 $foo(bar,$repl(a),$repl()=$repl(),a=b) etc",
|
||||
"Test nest2 _test(bar, rar, rr=rr, a=b) etc"),
|
||||
("Test nest3 $foo(bar,$repl($repl($repl(c))))",
|
||||
"Test nest3 _test(bar, rrrcrrr)"),
|
||||
("Test nest4 $foo($bar(a,b),$bar(a,$repl()),$bar())",
|
||||
"Test nest4 _test(_test(a, b), _test(a, rr), _test())"),
|
||||
("Test escape1 \\$repl(foo)", "Test escape1 $repl(foo)"),
|
||||
("Test escape2 \"This is $foo() and $bar($bar())\", $repl()",
|
||||
"Test escape2 \"This is _test() and _test(_test())\", rr"),
|
||||
("Test escape3 'This is $foo() and $bar($bar())', $repl()",
|
||||
"Test escape3 'This is _test() and _test(_test())', rr"),
|
||||
("Test escape4 $$foo() and $$bar(a,b), $repl()",
|
||||
"Test escape4 $foo() and $bar(a,b), rr"),
|
||||
("Test with color |r$foo(a,b)|n is ok",
|
||||
"Test with color |r_test(a, b)|n is ok"),
|
||||
("Test malformed1 This is $foo( and $bar(",
|
||||
"Test malformed1 This is $foo( and $bar("),
|
||||
("Test malformed2 This is $foo( and $bar()",
|
||||
"Test malformed2 This is $foo( and _test()"),
|
||||
("Test malformed3 $", "Test malformed3 $"),
|
||||
("Test malformed4 This is $foo(a=b and $bar(",
|
||||
"Test malformed4 This is $foo(a=b and $bar("),
|
||||
("Test malformed5 This is $foo(a=b, and $repl()",
|
||||
"Test malformed5 This is $foo(a=b, and rr"),
|
||||
("Test nonstr 4x2 = $double(4)", "Test nonstr 4x2 = 8"),
|
||||
("Test nonstr 4x2 = $double(foo)", "Test nonstr 4x2 = N/A"),
|
||||
("Test clr $clr(r, This is a red string!)", "Test clr |rThis is a red string!|n"),
|
||||
("Test eval1 $eval(21 + 21 - 10)", "Test eval1 32"),
|
||||
("Test eval2 $eval((21 + 21) / 2)", "Test eval2 21.0"),
|
||||
("Test eval3 $eval('21' + 'foo' + 'bar')", "Test eval3 21foobar"),
|
||||
("Test eval4 $eval('21' + '$repl()' + '' + str(10 // 2))", "Test eval4 21rr5"),
|
||||
("Test eval5 $eval('21' + '\$repl()' + '' + str(10 // 2))", "Test eval5 21$repl()5"),
|
||||
("Test eval6 $eval('$repl(a)' + '$repl(b)')", "Test eval6 rarrbr"),
|
||||
("Test type1 $typ([1,2,3,4])", "Test type1 <class 'list'>"),
|
||||
("Test type2 $typ((1,2,3,4))", "Test type2 <class 'tuple'>"),
|
||||
("Test type3 $typ({1,2,3,4})", "Test type3 <class 'set'>"),
|
||||
("Test type4 $typ({1:2,3:4})", "Test type4 <class 'dict'>"),
|
||||
("Test type5 $typ(1), $typ(1.0)", "Test type5 <class 'int'>, <class 'float'>"),
|
||||
("Test type6 $typ('1'), $typ(\"1.0\")", "Test type6 <class 'str'>, <class 'str'>"),
|
||||
("Test add1 $add(1, 2)", "Test add1 3"),
|
||||
("Test add2 $add([1,2,3,4], [5,6])", "Test add2 [1, 2, 3, 4, 5, 6]"),
|
||||
("Test literal1 $sum($lit([1,2,3,4,5,6]))", "Test literal1 21"),
|
||||
("Test literal2 $typ($lit(1))", "Test literal2 <class 'int'>"),
|
||||
("Test literal3 $typ($lit(1)aaa)", "Test literal3 <class 'str'>"),
|
||||
("Test literal4 $typ(aaa$lit(1))", "Test literal4 <class 'str'>"),
|
||||
])
|
||||
def test_parse(self, string, expected):
|
||||
"""
|
||||
Test parsing of string.
|
||||
|
||||
"""
|
||||
# t0 = time.time()
|
||||
# from evennia import set_trace;set_trace()
|
||||
ret = self.parser.parse(string, raise_errors=True)
|
||||
# t1 = time.time()
|
||||
# print(f"time: {(t1-t0)*1000} ms")
|
||||
self.assertEqual(expected, ret)
|
||||
|
||||
def test_parse_raise(self):
|
||||
"""
|
||||
Make sure error is raised if told to do so.
|
||||
|
||||
"""
|
||||
string = "Test malformed This is $dummy(a, b) and $bar("
|
||||
with self.assertRaises(funcparser.ParsingError):
|
||||
self.parser.parse(string, raise_errors=True)
|
||||
|
||||
def test_parse_strip(self):
|
||||
"""
|
||||
Test the parser's strip functionality.
|
||||
|
||||
"""
|
||||
string = "Test $foo(a,b, $bar()) and $repl($eval(3+2)) things"
|
||||
ret = self.parser.parse(string, strip=True)
|
||||
self.assertEqual("Test and things", ret)
|
||||
|
||||
def test_parse_escape(self):
|
||||
"""
|
||||
Test the parser's escape functionality.
|
||||
|
||||
"""
|
||||
string = "Test $foo(a) and $bar() and $rep(c) things"
|
||||
ret = self.parser.parse(string, escape=True)
|
||||
self.assertEqual("Test \$foo(a) and \$bar() and \$rep(c) things", ret)
|
||||
|
||||
def test_parse_lit(self):
|
||||
"""
|
||||
Get non-strings back from parsing.
|
||||
|
||||
"""
|
||||
string = "$lit(123)"
|
||||
|
||||
# normal parse
|
||||
ret = self.parser.parse(string)
|
||||
self.assertEqual('123', ret)
|
||||
self.assertTrue(isinstance(ret, str))
|
||||
|
||||
# parse lit
|
||||
ret = self.parser.parse_to_any(string)
|
||||
self.assertEqual(123, ret)
|
||||
self.assertTrue(isinstance(ret, int))
|
||||
|
||||
ret = self.parser.parse_to_any("$lit([1,2,3,4])")
|
||||
self.assertEqual([1, 2, 3, 4], ret)
|
||||
self.assertTrue(isinstance(ret, list))
|
||||
|
||||
ret = self.parser.parse_to_any("$lit('')")
|
||||
self.assertEqual("", ret)
|
||||
self.assertTrue(isinstance(ret, str))
|
||||
|
||||
# mixing a literal with other chars always make a string
|
||||
ret = self.parser.parse_to_any(string + "aa")
|
||||
self.assertEqual('123aa', ret)
|
||||
self.assertTrue(isinstance(ret, str))
|
||||
|
||||
ret = self.parser.parse_to_any("test")
|
||||
self.assertEqual('test', ret)
|
||||
self.assertTrue(isinstance(ret, str))
|
||||
|
||||
def test_kwargs_overrides(self):
|
||||
"""
|
||||
Test so default kwargs are added and overridden properly
|
||||
|
||||
"""
|
||||
# default kwargs passed on initializations
|
||||
parser = funcparser.FuncParser(
|
||||
_test_callables,
|
||||
test='foo'
|
||||
)
|
||||
ret = parser.parse("This is a $foo() string")
|
||||
self.assertEqual("This is a _test(test=foo) string", ret)
|
||||
|
||||
# override in the string itself
|
||||
|
||||
ret = parser.parse("This is a $foo(test=bar,foo=moo) string")
|
||||
self.assertEqual("This is a _test(test=bar, foo=moo) string", ret)
|
||||
|
||||
# parser kwargs override the other types
|
||||
|
||||
ret = parser.parse("This is a $foo(test=bar,foo=moo) string", test="override", foo="bar")
|
||||
self.assertEqual("This is a _test(test=override, foo=bar) string", ret)
|
||||
|
||||
# non-overridden kwargs shine through
|
||||
|
||||
ret = parser.parse("This is a $foo(foo=moo) string", foo="bar")
|
||||
self.assertEqual("This is a _test(test=foo, foo=bar) string", ret)
|
||||
|
||||
class _DummyObj:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def get_display_name(self, looker=None):
|
||||
return self.name
|
||||
|
||||
|
||||
class TestDefaultCallables(TestCase):
|
||||
"""
|
||||
Test default callables.
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
from django.conf import settings
|
||||
self.parser = funcparser.FuncParser({**funcparser.FUNCPARSER_CALLABLES,
|
||||
**funcparser.ACTOR_STANCE_CALLABLES})
|
||||
|
||||
self.obj1 = _DummyObj("Char1")
|
||||
self.obj2 = _DummyObj("Char2")
|
||||
|
||||
@parameterized.expand([
|
||||
("Test py1 $eval('')", "Test py1 "),
|
||||
])
|
||||
def test_callable(self, string, expected):
|
||||
"""
|
||||
Test callables with various input strings
|
||||
|
||||
"""
|
||||
ret = self.parser.parse(string, raise_errors=True)
|
||||
self.assertEqual(expected, ret)
|
||||
|
||||
@parameterized.expand([
|
||||
("$You() $conj(smile) at him.", "You smile at him.", "Char1 smiles at him."),
|
||||
("$You() $conj(smile) at $You(char1).", "You smile at You.", "Char1 smiles at Char1."),
|
||||
("$You() $conj(smile) at $You(char2).", "You smile at Char2.", "Char1 smiles at You."),
|
||||
("$You(char2) $conj(smile) at $you(char1).", "Char2 smile at you.", "You smiles at Char1."),
|
||||
])
|
||||
def test_conjugate(self, string, expected_you, expected_them):
|
||||
"""
|
||||
Test callables with various input strings
|
||||
|
||||
"""
|
||||
mapping = {"char1": self.obj1, "char2": self.obj2}
|
||||
ret = self.parser.parse(string, caller=self.obj1, receiver=self.obj1, mapping=mapping,
|
||||
raise_errors=True)
|
||||
self.assertEqual(expected_you, ret)
|
||||
ret = self.parser.parse(string, caller=self.obj1, receiver=self.obj2, mapping=mapping,
|
||||
raise_errors=True)
|
||||
self.assertEqual(expected_them, ret)
|
||||
|
||||
@parameterized.expand([
|
||||
("Test $pad(Hello, 20, c, -) there", "Test -------Hello-------- there"),
|
||||
("Test $pad(Hello, width=20, align=c, fillchar=-) there",
|
||||
"Test -------Hello-------- there"),
|
||||
("Test $crop(This is a long test, 12)", "Test This is[...]"),
|
||||
("Some $space(10) here", "Some here"),
|
||||
("Some $clr(b, blue color) now", "Some |bblue color|n now"),
|
||||
("Some $add(1, 2) things", "Some 3 things"),
|
||||
("Some $sub(10, 2) things", "Some 8 things"),
|
||||
("Some $mult(3, 2) things", "Some 6 things"),
|
||||
("Some $div(6, 2) things", "Some 3.0 things"),
|
||||
("Some $toint(6) things", "Some 6 things"),
|
||||
("Some $ljust(Hello, 30)", "Some Hello "),
|
||||
("Some $rjust(Hello, 30)", "Some Hello"),
|
||||
("Some $rjust(Hello, width=30)", "Some Hello"),
|
||||
("Some $cjust(Hello, 30)", "Some Hello "),
|
||||
("Some $eval('-'*20)Hello", "Some --------------------Hello"),
|
||||
])
|
||||
def test_other_callables(self, string, expected):
|
||||
"""
|
||||
Test default callables.
|
||||
|
||||
"""
|
||||
ret = self.parser.parse(string, raise_errors=True)
|
||||
self.assertEqual(expected, ret)
|
||||
|
||||
def test_random(self):
|
||||
string = "$random(1,10)"
|
||||
for i in range(100):
|
||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||
self.assertTrue(1 <= ret <= 10)
|
||||
|
||||
string = "$random()"
|
||||
for i in range(100):
|
||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||
self.assertTrue(0 <= ret <= 1)
|
||||
|
||||
string = "$random(1.0, 3.0)"
|
||||
for i in range(100):
|
||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||
self.assertTrue(isinstance(ret, float))
|
||||
self.assertTrue(1.0 <= ret <= 3.0)
|
||||
|
||||
def test_randint(self):
|
||||
string = "$randint(1.0, 3.0)"
|
||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||
self.assertTrue(isinstance(ret, int))
|
||||
self.assertTrue(1.0 <= ret <= 3.0)
|
||||
|
||||
def test_nofunc(self):
|
||||
self.assertEqual(
|
||||
self.parser.parse("as$382ewrw w we w werw,|44943}"),
|
||||
"as$382ewrw w we w werw,|44943}",
|
||||
)
|
||||
|
||||
def test_incomplete(self):
|
||||
self.assertEqual(
|
||||
self.parser.parse("testing $blah{without an ending."),
|
||||
"testing $blah{without an ending.",
|
||||
)
|
||||
|
||||
def test_single_func(self):
|
||||
self.assertEqual(
|
||||
self.parser.parse("this is a test with $pad(centered, 20) text in it."),
|
||||
"this is a test with centered text in it.",
|
||||
)
|
||||
|
||||
def test_nested(self):
|
||||
self.assertEqual(
|
||||
self.parser.parse(
|
||||
"this $crop(is a test with $pad(padded, 20) text in $pad(pad2, 10) a crop, 80)"
|
||||
),
|
||||
"this is a test with padded text in pad2 a crop",
|
||||
)
|
||||
|
||||
def test_escaped(self):
|
||||
self.assertEqual(
|
||||
self.parser.parse(
|
||||
"this should be $pad('''escaped,''' and '''instead,''' cropped $crop(with a long,5) text., 80)"
|
||||
),
|
||||
"this should be '''escaped,''' and '''instead,''' cropped with text. ",
|
||||
)
|
||||
|
||||
def test_escaped2(self):
|
||||
self.assertEqual(
|
||||
self.parser.parse(
|
||||
'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)'
|
||||
),
|
||||
"this should be \"\"\"escaped,\"\"\" and \"\"\"instead,\"\"\" cropped with text. ",
|
||||
)
|
||||
|
||||
|
||||
class TestCallableSearch(test_resources.EvenniaTest):
|
||||
"""
|
||||
Test the $search(query) callable
|
||||
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.parser = funcparser.FuncParser(funcparser.SEARCHING_CALLABLES)
|
||||
|
||||
def test_search_obj(self):
|
||||
"""
|
||||
Test searching for an object
|
||||
|
||||
"""
|
||||
string = "$search(Char)"
|
||||
expected = self.char1
|
||||
|
||||
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
|
||||
self.assertEqual(expected, ret)
|
||||
|
||||
def test_search_account(self):
|
||||
"""
|
||||
Test searching for an account
|
||||
|
||||
"""
|
||||
string = "$search(TestAccount, type=account)"
|
||||
expected = self.account
|
||||
|
||||
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
|
||||
self.assertEqual(expected, ret)
|
||||
|
||||
def test_search_script(self):
|
||||
"""
|
||||
Test searching for a script
|
||||
|
||||
"""
|
||||
string = "$search(Script, type=script)"
|
||||
expected = self.script
|
||||
|
||||
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
|
||||
self.assertEqual(expected, ret)
|
||||
|
||||
def test_search_obj_embedded(self):
|
||||
"""
|
||||
Test searching for an object - embedded in str
|
||||
|
||||
"""
|
||||
string = "This is $search(Char) the guy."
|
||||
expected = "This is " + str(self.char1) + " the guy."
|
||||
|
||||
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
|
||||
self.assertEqual(expected, ret)
|
||||
|
|
@ -3,10 +3,10 @@ Unit tests for all sorts of inline text-tag parsing, like ANSI, html conversion,
|
|||
|
||||
"""
|
||||
import re
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, override_settings
|
||||
from evennia.utils.ansi import ANSIString
|
||||
from evennia.utils.text2html import TextToHTMLparser
|
||||
from evennia.utils import inlinefuncs
|
||||
from evennia.utils import funcparser
|
||||
|
||||
|
||||
class ANSIStringTestCase(TestCase):
|
||||
|
|
@ -347,49 +347,3 @@ class TestTextToHTMLparser(TestCase):
|
|||
'</span><a href="http://example.com/" target="_blank">'
|
||||
'http://example.com/</a><span class="red">',
|
||||
)
|
||||
|
||||
|
||||
class TestInlineFuncs(TestCase):
|
||||
"""Test the nested inlinefunc module"""
|
||||
|
||||
def test_nofunc(self):
|
||||
self.assertEqual(
|
||||
inlinefuncs.parse_inlinefunc("as$382ewrw w we w werw,|44943}"),
|
||||
"as$382ewrw w we w werw,|44943}",
|
||||
)
|
||||
|
||||
def test_incomplete(self):
|
||||
self.assertEqual(
|
||||
inlinefuncs.parse_inlinefunc("testing $blah{without an ending."),
|
||||
"testing $blah{without an ending.",
|
||||
)
|
||||
|
||||
def test_single_func(self):
|
||||
self.assertEqual(
|
||||
inlinefuncs.parse_inlinefunc("this is a test with $pad(centered, 20) text in it."),
|
||||
"this is a test with centered text in it.",
|
||||
)
|
||||
|
||||
def test_nested(self):
|
||||
self.assertEqual(
|
||||
inlinefuncs.parse_inlinefunc(
|
||||
"this $crop(is a test with $pad(padded, 20) text in $pad(pad2, 10) a crop, 80)"
|
||||
),
|
||||
"this is a test with padded text in pad2 a crop",
|
||||
)
|
||||
|
||||
def test_escaped(self):
|
||||
self.assertEqual(
|
||||
inlinefuncs.parse_inlinefunc(
|
||||
"this should be $pad('''escaped,''' and '''instead,''' cropped $crop(with a long,5) text., 80)"
|
||||
),
|
||||
"this should be escaped, and instead, cropped with text. ",
|
||||
)
|
||||
|
||||
def test_escaped2(self):
|
||||
self.assertEqual(
|
||||
inlinefuncs.parse_inlinefunc(
|
||||
'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)'
|
||||
),
|
||||
"this should be escaped, and instead, cropped with text. ",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ TODO: Not nearly all utilities are covered yet.
|
|||
import os.path
|
||||
import random
|
||||
|
||||
from parameterized import parameterized
|
||||
import mock
|
||||
from django.test import TestCase
|
||||
from datetime import datetime
|
||||
|
|
@ -385,3 +386,43 @@ class TestPercent(TestCase):
|
|||
self.assertEqual(utils.percent(3, 1, 1), "0.0%")
|
||||
self.assertEqual(utils.percent(3, 0, 1), "100.0%")
|
||||
self.assertEqual(utils.percent(-3, 0, 1), "0.0%")
|
||||
|
||||
|
||||
class TestSafeConvert(TestCase):
|
||||
"""
|
||||
Test evennia.utils.utils.safe_convert_to_types
|
||||
|
||||
"""
|
||||
|
||||
@parameterized.expand([
|
||||
(('1', '2', 3, 4, '5'), {'a': 1, 'b': '2', 'c': 3},
|
||||
((int, float, str, int), {'a': int, 'b': float}), # "
|
||||
(1, 2.0, '3', 4, '5'), {'a': 1, 'b': 2.0, 'c': 3}),
|
||||
(('1 + 2', '[1, 2, 3]', [3, 4, 5]), {'a': '3 + 4', 'b': 5},
|
||||
(('py', 'py', 'py'), {'a': 'py', 'b': 'py'}),
|
||||
(3, [1, 2, 3], [3, 4, 5]), {'a': 7, 'b': 5}),
|
||||
])
|
||||
def test_conversion(self, args, kwargs, converters, expected_args, expected_kwargs):
|
||||
"""
|
||||
Test the converter with different inputs
|
||||
|
||||
"""
|
||||
result_args, result_kwargs = utils.safe_convert_to_types(
|
||||
converters, *args, raise_errors=True, **kwargs)
|
||||
self.assertEqual(expected_args, result_args)
|
||||
self.assertEqual(expected_kwargs, result_kwargs)
|
||||
|
||||
def test_conversion__fail(self):
|
||||
"""
|
||||
Test failing conversion
|
||||
|
||||
"""
|
||||
from evennia.utils.funcparser import ParsingError
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
utils.safe_convert_to_types(
|
||||
(int, ), *('foo', ), raise_errors=True)
|
||||
|
||||
with self.assertRaises(ParsingError) as err:
|
||||
utils.safe_convert_to_types(
|
||||
('py', {}), *('foo', ), raise_errors=True)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import traceback
|
|||
import importlib
|
||||
import importlib.util
|
||||
import importlib.machinery
|
||||
from ast import literal_eval
|
||||
from simpleeval import simple_eval
|
||||
from unicodedata import east_asian_width
|
||||
from twisted.internet.task import deferLater
|
||||
from twisted.internet.defer import returnValue # noqa - used as import target
|
||||
|
|
@ -1078,7 +1080,8 @@ def repeat(interval, callback, persistent=True, idstring="", stop=False,
|
|||
store_key (tuple, optional): This is only used in combination with `stop` and
|
||||
should be the return given from the original `repeat` call. If this
|
||||
is given, all other args except `stop` are ignored.
|
||||
*args, **kwargs: Used as arguments to `callback`.
|
||||
*args: Used as arguments to `callback`.
|
||||
**kwargs: Keyword-arguments to pass to `callback`.
|
||||
|
||||
Returns:
|
||||
tuple or None: The tuple is the `store_key` - the identifier for the
|
||||
|
|
@ -1112,8 +1115,8 @@ def unrepeat(store_key):
|
|||
|
||||
Args:
|
||||
store_key (tuple): This is the return from `repeat`, used to uniquely
|
||||
identify the ticker to stop. Without the store_key, the ticker
|
||||
must be stopped by passing its parameters to `TICKER_HANDLER.remove`
|
||||
identify the ticker to stop. Without the store_key, the ticker
|
||||
must be stopped by passing its parameters to `TICKER_HANDLER.remove`
|
||||
directly.
|
||||
|
||||
Returns:
|
||||
|
|
@ -2390,3 +2393,104 @@ def interactive(func):
|
|||
return ret
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def safe_convert_to_types(converters, *args, raise_errors=True, **kwargs):
|
||||
"""
|
||||
Helper function to safely convert inputs to expected data types.
|
||||
|
||||
Args:
|
||||
converters (tuple): A tuple `((converter, converter,...), {kwarg: converter, ...})` to
|
||||
match a converter to each element in `*args` and `**kwargs`.
|
||||
Each converter will will be called with the arg/kwarg-value as the only argument.
|
||||
If there are too few converters given, the others will simply not be converter. If the
|
||||
converter is given as the string 'py', it attempts to run
|
||||
`safe_eval`/`literal_eval` on the input arg or kwarg value. It's possible to
|
||||
skip the arg/kwarg part of the tuple, an empty tuple/dict will then be assumed.
|
||||
*args: The arguments to convert with `argtypes`.
|
||||
raise_errors (bool, optional): If set, raise any errors. This will
|
||||
abort the conversion at that arg/kwarg. Otherwise, just skip the
|
||||
conversion of the failing arg/kwarg. This will be set by the FuncParser if
|
||||
this is used as a part of a FuncParser callable.
|
||||
**kwargs: The kwargs to convert with `kwargtypes`
|
||||
|
||||
Returns:
|
||||
tuple: `(args, kwargs)` in converted form.
|
||||
|
||||
Raises:
|
||||
utils.funcparser.ParsingError: If parsing failed in the `'py'`
|
||||
converter. This also makes this compatible with the FuncParser
|
||||
interface.
|
||||
any: Any other exception raised from other converters, if raise_errors is True.
|
||||
|
||||
Notes:
|
||||
This function is often used to validate/convert input from untrusted sources. For
|
||||
security, the "py"-converter is deliberately limited and uses `safe_eval`/`literal_eval`
|
||||
which only supports simple expressions or simple containers with literals. NEVER
|
||||
use the python `eval` or `exec` methods as a converter for any untrusted input! Allowing
|
||||
untrusted sources to execute arbitrary python on your server is a severe security risk,
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
$funcname(1, 2, 3.0, c=[1,2,3])
|
||||
|
||||
def _funcname(*args, **kwargs):
|
||||
args, kwargs = safe_convert_input(((int, int, float), {'c': 'py'}), *args, **kwargs)
|
||||
# ...
|
||||
|
||||
"""
|
||||
def _safe_eval(inp):
|
||||
if not inp:
|
||||
return ''
|
||||
if not isinstance(inp, str):
|
||||
# already converted
|
||||
return inp
|
||||
|
||||
try:
|
||||
return literal_eval(inp)
|
||||
except Exception as err:
|
||||
literal_err = f"{err.__class__.__name__}: {err}"
|
||||
try:
|
||||
return simple_eval(inp)
|
||||
except Exception as err:
|
||||
simple_err = f"{str(err.__class__.__name__)}: {err}"
|
||||
pass
|
||||
|
||||
if raise_errors:
|
||||
from evennia.utils.funcparser import ParsingError
|
||||
err = (f"Errors converting '{inp}' to python:\n"
|
||||
f"literal_eval raised {literal_err}\n"
|
||||
f"simple_eval raised {simple_err}")
|
||||
raise ParsingError(err)
|
||||
|
||||
# handle an incomplete/mixed set of input converters
|
||||
if not converters:
|
||||
return args, kwargs
|
||||
arg_converters, *kwarg_converters = converters
|
||||
arg_converters = make_iter(arg_converters)
|
||||
kwarg_converters = kwarg_converters[0] if kwarg_converters else {}
|
||||
|
||||
# apply the converters
|
||||
if args and arg_converters:
|
||||
args = list(args)
|
||||
arg_converters = make_iter(arg_converters)
|
||||
for iarg, arg in enumerate(args[:len(arg_converters)]):
|
||||
converter = arg_converters[iarg]
|
||||
converter = _safe_eval if converter in ('py', 'python') else converter
|
||||
try:
|
||||
args[iarg] = converter(arg)
|
||||
except Exception:
|
||||
if raise_errors:
|
||||
raise
|
||||
args = tuple(args)
|
||||
if kwarg_converters and isinstance(kwarg_converters, dict):
|
||||
for key, converter in kwarg_converters.items():
|
||||
converter = _safe_eval if converter in ('py', 'python') else converter
|
||||
if key in {**kwargs}:
|
||||
try:
|
||||
kwargs[key] = converter(kwargs[key])
|
||||
except Exception:
|
||||
if raise_errors:
|
||||
raise
|
||||
return args, kwargs
|
||||
|
|
|
|||
340
evennia/utils/verb_conjugation/LICENSE.txt
Normal file
340
evennia/utils/verb_conjugation/LICENSE.txt
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
Public License instead of this License.
|
||||
0
evennia/utils/verb_conjugation/__init__.py
Normal file
0
evennia/utils/verb_conjugation/__init__.py
Normal file
387
evennia/utils/verb_conjugation/conjugate.py
Normal file
387
evennia/utils/verb_conjugation/conjugate.py
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
"""
|
||||
English verb conjugation
|
||||
|
||||
Original Author: Tom De Smedt <tomdesmedt@organisms.be> of Nodebox
|
||||
Refactored by Griatch 2021, for Evennia.
|
||||
|
||||
This is distributed under the GPL2 license. See ./LICENSE.txt for details.
|
||||
|
||||
The verb.txt morphology was adopted from the XTAG morph_englis.flat:
|
||||
http://www.cis.upenn.edu/~xtag/
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
_VERBS_FILE = "verbs.txt"
|
||||
|
||||
# Each verb and its tenses is a list in verbs.txt,
|
||||
# indexed according to the following keys:
|
||||
# the negated forms (for supported verbs) are ind+11.
|
||||
|
||||
verb_tenses_keys = {
|
||||
"infinitive": 0,
|
||||
"1st singular present": 1,
|
||||
"2nd singular present": 2,
|
||||
"3rd singular present": 3,
|
||||
"present plural": 4,
|
||||
"present participle": 5,
|
||||
"1st singular past": 6,
|
||||
"2nd singular past": 7,
|
||||
"3rd singular past": 8,
|
||||
"past plural": 9,
|
||||
"past": 10,
|
||||
"past participle": 11,
|
||||
}
|
||||
|
||||
# allow to specify tenses with a shorter notation
|
||||
verb_tenses_aliases = {
|
||||
"inf": "infinitive",
|
||||
"1sgpres": "1st singular present",
|
||||
"2sgpres": "2nd singular present",
|
||||
"3sgpres": "3rd singular present",
|
||||
"pl": "present plural",
|
||||
"prog": "present participle",
|
||||
"1sgpast": "1st singular past",
|
||||
"2sgpast": "2nd singular past",
|
||||
"3sgpast": "3rd singular past",
|
||||
"pastpl": "past plural",
|
||||
"ppart": "past participle",
|
||||
}
|
||||
|
||||
# Each verb has morphs for infinitve,
|
||||
# 3rd singular present, present participle,
|
||||
# past and past participle.
|
||||
# Verbs like "be" have other morphs as well
|
||||
# (i.e. I am, you are, she is, they aren't)
|
||||
# Additionally, the following verbs can be negated:
|
||||
# be, can, do, will, must, have, may, need, dare, ought.
|
||||
|
||||
# load the conjugation forms from ./verbs.txt
|
||||
verb_tenses = {}
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), _VERBS_FILE)
|
||||
with open(path) as fil:
|
||||
for line in fil.readlines():
|
||||
wordlist = [part.strip() for part in line.split(",")]
|
||||
verb_tenses[wordlist[0]] = wordlist
|
||||
|
||||
# Each verb can be lemmatised:
|
||||
# inflected morphs of the verb point
|
||||
# to its infinitive in this dictionary.
|
||||
verb_lemmas = {}
|
||||
for infinitive in verb_tenses:
|
||||
for tense in verb_tenses[infinitive]:
|
||||
if tense:
|
||||
verb_lemmas[tense] = infinitive
|
||||
|
||||
|
||||
def verb_infinitive(verb):
|
||||
"""
|
||||
Returns the uninflected form of the verb, like 'are' -> 'be'
|
||||
|
||||
Args:
|
||||
verb (str): The verb to get the uninflected form of.
|
||||
|
||||
Returns:
|
||||
str: The uninflected verb form of `verb`.
|
||||
|
||||
"""
|
||||
|
||||
return verb_lemmas.get(verb, '')
|
||||
|
||||
|
||||
def verb_conjugate(verb, tense="infinitive", negate=False):
|
||||
"""
|
||||
Inflects the verb to the given tense.
|
||||
|
||||
Args:
|
||||
verb (str): The single verb to conjugate.
|
||||
tense (str): The tense to convert to. This can be given either as a long or short form
|
||||
- "infinitive" ("inf") - be
|
||||
- "1st/2nd/3rd singular present" ("1/2/3sgpres") - am/are/is
|
||||
- "present plural" ("pl") - are
|
||||
- "present participle" ("prog") - being
|
||||
- "1st/2nd/3rd singular past" ("1/2/3sgpast") - was/were/was
|
||||
- "past plural" ("pastpl") - were
|
||||
- "past" - were
|
||||
- "past participle" ("ppart") - been
|
||||
negate (bool): Negates the verb. This only supported
|
||||
for a limited number of verbs: be, can, do, will, must, have, may,
|
||||
need, dare, ought.
|
||||
|
||||
Returns:
|
||||
str: The conjugated verb. If conjugation fails, the original verb is returned.
|
||||
|
||||
Examples:
|
||||
The verb 'be':
|
||||
- present: I am, you are, she is,
|
||||
- present participle: being,
|
||||
- past: I was, you were, he was,
|
||||
- past participle: been,
|
||||
- negated present: I am not, you aren't, it isn't.
|
||||
|
||||
"""
|
||||
tense = verb_tenses_aliases.get(tense, tense)
|
||||
verb = verb_infinitive(verb)
|
||||
ind = verb_tenses_keys[tense]
|
||||
if negate:
|
||||
ind += len(verb_tenses_keys)
|
||||
try:
|
||||
return verb_tenses[verb][ind]
|
||||
except IndexError:
|
||||
# TODO implement simple algorithm here with +s for certain tenses?
|
||||
return verb
|
||||
|
||||
|
||||
def verb_present(verb, person="", negate=False):
|
||||
"""
|
||||
Inflects the verb in the present tense.
|
||||
|
||||
Args:
|
||||
person (str or int): This can be 1, 2, 3, "1st", "2nd", "3rd", "plural" or "*".
|
||||
negate (bool): Some verbs like be, have, must, can be negated.
|
||||
|
||||
Returns:
|
||||
str: The present tense verb.
|
||||
|
||||
Example:
|
||||
had -> have
|
||||
|
||||
"""
|
||||
|
||||
person = str(person).replace("pl", "*").strip("stndrgural")
|
||||
mapping = {
|
||||
"1": "1st singular present",
|
||||
"2": "2nd singular present",
|
||||
"3": "3rd singular present",
|
||||
"*": "present plural",
|
||||
}
|
||||
if person in mapping and verb_conjugate(verb, mapping[person], negate) != "":
|
||||
return verb_conjugate(verb, mapping[person], negate)
|
||||
|
||||
return verb_conjugate(verb, "infinitive", negate)
|
||||
|
||||
|
||||
def verb_present_participle(verb):
|
||||
"""
|
||||
Inflects the verb in the present participle.
|
||||
|
||||
Args:
|
||||
verb (str): The verb to inflect.
|
||||
|
||||
Returns:
|
||||
str: The inflected verb.
|
||||
|
||||
Examples:
|
||||
give -> giving, be -> being, swim -> swimming
|
||||
|
||||
"""
|
||||
return verb_conjugate(verb, "present participle")
|
||||
|
||||
|
||||
def verb_past(verb, person="", negate=False):
|
||||
"""
|
||||
|
||||
Inflects the verb in the past tense.
|
||||
|
||||
Args:
|
||||
verb (str): The verb to inflect.
|
||||
person (str, optional): The person can be specified with 1, 2, 3,
|
||||
"1st", "2nd", "3rd", "plural", "*".
|
||||
negate (bool, optional): Some verbs like be, have, must, can be negated.
|
||||
|
||||
Returns:
|
||||
str: The inflected verb.
|
||||
|
||||
Examples:
|
||||
give -> gave, be -> was, swim -> swam
|
||||
|
||||
"""
|
||||
|
||||
person = str(person).replace("pl", "*").strip("stndrgural")
|
||||
mapping = {
|
||||
"1": "1st singular past",
|
||||
"2": "2nd singular past",
|
||||
"3": "3rd singular past",
|
||||
"*": "past plural",
|
||||
}
|
||||
if person in mapping and verb_conjugate(verb, mapping[person], negate) != "":
|
||||
return verb_conjugate(verb, mapping[person], negate)
|
||||
|
||||
return verb_conjugate(verb, "past", negate=negate)
|
||||
|
||||
|
||||
def verb_past_participle(verb):
|
||||
"""
|
||||
Inflects the verb in the present participle.
|
||||
|
||||
Args:
|
||||
verb (str): The verb to inflect.
|
||||
|
||||
Returns:
|
||||
str: The inflected verb.
|
||||
|
||||
Examples:
|
||||
give -> given, be -> been, swim -> swum
|
||||
|
||||
"""
|
||||
return verb_conjugate(verb, "past participle")
|
||||
|
||||
|
||||
def verb_all_tenses():
|
||||
"""
|
||||
Get all all possible verb tenses.
|
||||
|
||||
Returns:
|
||||
list: A list if string names.
|
||||
|
||||
"""
|
||||
|
||||
return list(verb_tenses_keys.keys())
|
||||
|
||||
|
||||
def verb_tense(verb):
|
||||
"""
|
||||
Returns a string from verb_tenses_keys representing the verb's tense.
|
||||
|
||||
Args:
|
||||
verb (str): The verb to check the tense of.
|
||||
|
||||
Returns:
|
||||
str: The tense.
|
||||
|
||||
Example:
|
||||
given -> "past participle"
|
||||
|
||||
"""
|
||||
infinitive = verb_infinitive(verb)
|
||||
data = verb_tenses[infinitive]
|
||||
for tense in verb_tenses_keys:
|
||||
if data[verb_tenses_keys[tense]] == verb:
|
||||
return tense
|
||||
if data[verb_tenses_keys[tense] + len(verb_tenses_keys)] == verb:
|
||||
return tense
|
||||
|
||||
|
||||
def verb_is_tense(verb, tense):
|
||||
"""
|
||||
Checks whether the verb is in the given tense.
|
||||
|
||||
Args:
|
||||
verb (str): The verb to check.
|
||||
tense (str): The tense to check.
|
||||
|
||||
Return:
|
||||
bool: If verb matches given tense.
|
||||
|
||||
"""
|
||||
tense = verb_tenses_aliases.get(tense, tense)
|
||||
return verb_tense(verb) == tense
|
||||
|
||||
|
||||
def verb_is_present(verb, person="", negated=False):
|
||||
"""
|
||||
Checks whether the verb is in the present tense.
|
||||
|
||||
Args:
|
||||
verb (str): The verb to check.
|
||||
person (str): Check which person.
|
||||
negated (bool): Check if verb was negated.
|
||||
|
||||
Returns:
|
||||
bool: If verb was in present tense.
|
||||
|
||||
"""
|
||||
|
||||
person = str(person).replace("*", "plural")
|
||||
tense = verb_tense(verb)
|
||||
if tense is not None:
|
||||
if "present" in tense and person in tense:
|
||||
if not negated:
|
||||
return True
|
||||
elif "n't" in verb or " not" in verb:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def verb_is_present_participle(verb):
|
||||
"""
|
||||
Checks whether the verb is in present participle.
|
||||
|
||||
Args:
|
||||
verb (str): The verb to check.
|
||||
|
||||
Returns:
|
||||
bool: Result of check.
|
||||
|
||||
"""
|
||||
|
||||
tense = verb_tense(verb)
|
||||
return tense == "present participle"
|
||||
|
||||
|
||||
def verb_is_past(verb, person="", negated=False):
|
||||
"""
|
||||
Checks whether the verb is in the past tense.
|
||||
|
||||
Args:
|
||||
verb (str): The verb to check.
|
||||
person (str): The person to check.
|
||||
negated (bool): Check if verb is negated.
|
||||
|
||||
Returns:
|
||||
bool: Result of check.
|
||||
|
||||
"""
|
||||
|
||||
person = str(person).replace("*", "plural")
|
||||
tense = verb_tense(verb)
|
||||
if tense is not None:
|
||||
if "past" in tense and person in tense:
|
||||
if not negated:
|
||||
return True
|
||||
elif "n't" in verb or " not" in verb:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def verb_is_past_participle(verb):
|
||||
"""
|
||||
Checks whether the verb is in past participle.
|
||||
|
||||
Args:
|
||||
verb (str): The verb to check.
|
||||
|
||||
Returns:
|
||||
bool: The result of the check.
|
||||
|
||||
"""
|
||||
tense = verb_tense(verb)
|
||||
return tense == "past participle"
|
||||
|
||||
|
||||
def verb_actor_stance_components(verb):
|
||||
"""
|
||||
Figure out actor stance components of a verb.
|
||||
|
||||
Args:
|
||||
verb (str): The verb to analyze
|
||||
|
||||
Returns:
|
||||
tuple: The 2nd person (you) and 3rd person forms of the verb,
|
||||
in the same tense as the ingoing verb.
|
||||
|
||||
"""
|
||||
tense = verb_tense(verb)
|
||||
if "participle" in tense or "plural" in tense:
|
||||
return (verb, verb)
|
||||
if tense == "infinitive" or "present" in tense:
|
||||
you_str = verb_present(verb, person="2") or verb
|
||||
them_str = verb_present(verb, person="3") or verb + "s"
|
||||
else:
|
||||
you_str = verb_past(verb, person="2") or verb
|
||||
them_str = verb_past(verb, person="3") or verb + "s"
|
||||
return (you_str, them_str)
|
||||
241
evennia/utils/verb_conjugation/tests.py
Normal file
241
evennia/utils/verb_conjugation/tests.py
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
"""
|
||||
Unit tests for verb conjugation.
|
||||
|
||||
"""
|
||||
|
||||
from parameterized import parameterized
|
||||
from django.test import TestCase
|
||||
from . import conjugate
|
||||
|
||||
|
||||
class TestVerbConjugate(TestCase):
|
||||
"""
|
||||
Test the conjugation.
|
||||
|
||||
"""
|
||||
@parameterized.expand([
|
||||
("have", "have"),
|
||||
("swim", "swim"),
|
||||
("give", "give"),
|
||||
("given", "give"),
|
||||
("am", "be"),
|
||||
("doing", "do"),
|
||||
("are", "be"),
|
||||
])
|
||||
def test_verb_infinitive(self, verb, expected):
|
||||
"""
|
||||
Test the infinite-getter.
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_infinitive(verb))
|
||||
|
||||
@parameterized.expand([
|
||||
("inf", "have", "have"),
|
||||
("inf", "swim", "swim"),
|
||||
("inf", "give", "give"),
|
||||
("inf", "given", "give"),
|
||||
("inf", "am", "be"),
|
||||
("inf", "doing", "do"),
|
||||
("inf", "are", "be"),
|
||||
("2sgpres", "am", "are"),
|
||||
("3sgpres", "am", "is"),
|
||||
])
|
||||
def test_verb_conjugate(self, tense, verb, expected):
|
||||
"""
|
||||
Test conjugation for different tenses.
|
||||
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_conjugate(verb, tense=tense))
|
||||
|
||||
@parameterized.expand([
|
||||
("1st", "have", "have"),
|
||||
("1st", "swim", "swim"),
|
||||
("1st", "give", "give"),
|
||||
("1st", "given", "give"),
|
||||
("1st", "am", "am"),
|
||||
("1st", "doing", "do"),
|
||||
("1st", "are", "am"),
|
||||
("2nd", "were", "are"),
|
||||
("3rd", "am", "is"),
|
||||
])
|
||||
def test_verb_present(self, person, verb, expected):
|
||||
"""
|
||||
Test the present.
|
||||
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_present(verb, person=person))
|
||||
|
||||
@parameterized.expand([
|
||||
("have", "having"),
|
||||
("swim", "swimming"),
|
||||
("give", "giving"),
|
||||
("given", "giving"),
|
||||
("am", "being"),
|
||||
("doing", "doing"),
|
||||
("are", "being"),
|
||||
])
|
||||
def test_verb_present_participle(self, verb, expected):
|
||||
"""
|
||||
Test the present_participle
|
||||
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_present_participle(verb))
|
||||
|
||||
@parameterized.expand([
|
||||
("1st", "have", "had"),
|
||||
("1st", "swim", "swam"),
|
||||
("1st", "give", "gave"),
|
||||
("1st", "given", "gave"),
|
||||
("1st", "am", "was"),
|
||||
("1st", "doing", "did"),
|
||||
("1st", "are", "was"),
|
||||
("2nd", "were", "were"),
|
||||
])
|
||||
def test_verb_past(self, person, verb, expected):
|
||||
"""
|
||||
Test the past getter.
|
||||
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_past(verb, person=person))
|
||||
|
||||
@parameterized.expand([
|
||||
("have", "had"),
|
||||
("swim", "swum"),
|
||||
("give", "given"),
|
||||
("given", "given"),
|
||||
("am", "been"),
|
||||
("doing", "done"),
|
||||
("are", "been"),
|
||||
])
|
||||
def test_verb_past_participle(self, verb, expected):
|
||||
"""
|
||||
Test the past participle.
|
||||
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_past_participle(verb))
|
||||
|
||||
def test_verb_get_all_tenses(self):
|
||||
"""
|
||||
Test getting all tenses.
|
||||
|
||||
"""
|
||||
self.assertEqual(list(conjugate.verb_tenses_keys.keys()), conjugate.verb_all_tenses())
|
||||
|
||||
@parameterized.expand([
|
||||
("have", "infinitive"),
|
||||
("swim", "infinitive"),
|
||||
("give", "infinitive"),
|
||||
("given", "past participle"),
|
||||
("am", "1st singular present"),
|
||||
("doing", "present participle"),
|
||||
("are", "2nd singular present"),
|
||||
])
|
||||
def test_verb_tense(self, verb, expected):
|
||||
"""
|
||||
Test the tense retriever.
|
||||
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_tense(verb))
|
||||
|
||||
@parameterized.expand([
|
||||
("inf", "have", True),
|
||||
("inf", "swim", True),
|
||||
("inf", "give", True),
|
||||
("inf", "given", False),
|
||||
("inf", "am", False),
|
||||
("inf", "doing", False),
|
||||
("inf", "are", False),
|
||||
])
|
||||
def test_verb_is_tense(self, tense, verb, expected):
|
||||
"""
|
||||
Test the tense-checker
|
||||
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_is_tense(verb, tense))
|
||||
|
||||
@parameterized.expand([
|
||||
("1st", "have", False),
|
||||
("1st", "swim", False),
|
||||
("1st", "give", False),
|
||||
("1st", "given", False),
|
||||
("1st", "am", True),
|
||||
("1st", "doing", False),
|
||||
("1st", "are", False),
|
||||
("1st", "had", False),
|
||||
])
|
||||
def test_verb_is_present(self, person, verb, expected):
|
||||
"""
|
||||
Test the tense-checker
|
||||
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_is_present(verb, person=person))
|
||||
|
||||
@parameterized.expand([
|
||||
("have", False),
|
||||
("swim", False),
|
||||
("give", False),
|
||||
("given", False),
|
||||
("am", False),
|
||||
("doing", True),
|
||||
("are", False),
|
||||
])
|
||||
def test_verb_is_present_participle(self, verb, expected):
|
||||
"""
|
||||
Test the tense-checker
|
||||
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_is_present_participle(verb))
|
||||
|
||||
@parameterized.expand([
|
||||
("1st", "have", False),
|
||||
("1st", "swim", False),
|
||||
("1st", "give", False),
|
||||
("1st", "given", False),
|
||||
("1st", "am", False),
|
||||
("1st", "doing", False),
|
||||
("1st", "are", False),
|
||||
("2nd", "were", True),
|
||||
])
|
||||
def test_verb_is_past(self, person, verb, expected):
|
||||
"""
|
||||
Test the tense-checker
|
||||
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_is_past(verb, person=person))
|
||||
|
||||
@parameterized.expand([
|
||||
("have", False),
|
||||
("swimming", False),
|
||||
("give", False),
|
||||
("given", True),
|
||||
("am", False),
|
||||
("doing", False),
|
||||
("are", False),
|
||||
("had", False),
|
||||
])
|
||||
def test_verb_is_past_participle(self, verb, expected):
|
||||
"""
|
||||
Test the tense-checker
|
||||
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_is_past_participle(verb))
|
||||
|
||||
@parameterized.expand([
|
||||
("have", ("have", "has")),
|
||||
("swimming", ("swimming", "swimming")),
|
||||
("give", ("give", "gives")),
|
||||
("given", ("given", "given")),
|
||||
("am", ("are", "is")),
|
||||
("doing", ("doing", "doing")),
|
||||
("are", ("are", "is")),
|
||||
("had", ("had", "had")),
|
||||
("grin", ("grin", "grins")),
|
||||
("smile", ("smile", "smiles")),
|
||||
("vex", ("vex", "vexes")),
|
||||
("thrust", ("thrust", "thrusts")),
|
||||
])
|
||||
def test_verb_actor_stance_components(self, verb, expected):
|
||||
"""
|
||||
Test the tense-checker
|
||||
|
||||
"""
|
||||
self.assertEqual(expected, conjugate.verb_actor_stance_components(verb))
|
||||
8567
evennia/utils/verb_conjugation/verbs.txt
Normal file
8567
evennia/utils/verb_conjugation/verbs.txt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -11,6 +11,11 @@ django-sekizai
|
|||
inflect >= 5.2.0
|
||||
autobahn >= 17.9.3
|
||||
lunr == 0.5.6
|
||||
simpleeval <= 1.0
|
||||
|
||||
# conjugation library, py3 version
|
||||
git+https://github.com/markrogersjr/nodebox_linguistics_extended.git
|
||||
|
||||
|
||||
# try to resolve dependency issue in py3.7
|
||||
attrs >= 19.2.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue