Merge branch 'develop' into mapping-contrib

This commit is contained in:
Griatch 2021-06-19 17:28:41 +02:00
commit 79019a5813
16 changed files with 503 additions and 81 deletions

View file

@ -66,6 +66,9 @@ Up requirements to Django 3.2+
into a more consistent structure for overriding. Expanded webpage documentation considerably.
- REST API list-view was shortened (#2401). New CSS/HTML. Add ReDoc for API autodoc page.
- Update and fix dummyrunner with cleaner code and setup.
- Made `iter_to_str` format prettier strings, using Oxford comma.
- Added an MXP anchor tag to also support clickable web links.
- New `tasks` command for managing tasks started with `utils.delay` (PR by davewiththenicehat)
### Evennia 0.9.5 (2019-2020)
@ -149,6 +152,7 @@ without arguments starts a full interactive Python console.
- Include more Web-client info in `session.protocol_flags`.
- Fixes in multi-match situations - don't allow finding/listing multimatches for 3-box when
only two boxes in location.
- Fix for TaskHandler with proper deferred returns/ability to cancel etc (PR by davewiththenicehat)
## Evennia 0.9 (2018-2019)

View file

@ -1,11 +1,12 @@
## 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*).
clicked by a mouse and either trigger a given Evennia command, or open a URL in an external web
browser. 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.
- `|lu` to start the link, by defining the URL to open.
- `|lt` to continue with the text to show to the user (the link text).
- `|le` to end the link text and the link definition.

View file

@ -4,10 +4,10 @@
### How to *get* Help
If you cannot find what you are looking for in the documentation, here's what to do:
- If you think the documentation is not clear enough, create a [documentation ticket](github:issue).
- If you have trouble with a missing feature or a problem you think is a bug, look through the
the list of known [issue][issues] if you can't find your issue in the list, make a
- If you have trouble with a missing feature or a problem you think is a bug, look through the
the list of known [issue][issues] if you can't find your issue in the list, make a
new one [here](github:issue).
- If you need help, want to start a discussion or get some input on something you are working on,
make a post to the [discussions group][group] This is technically a 'mailing list', but you don't
@ -26,8 +26,8 @@ eventually!
Evennia is open-source and non-commercial. It relies on the time donated by its users and developers in order to progress.
- Spread the word! If you like Evennia, consider writing a blog post about it.
- Take part in the Evennia community! Join the [chat][chat] or [forum][group].
- Report problems you find or features you'd like to our [issue tracker](github:issue).
- Take part in the Evennia community! Join the [chat][chat] or [forum][group].
- Report problems you find or features you'd like to our [issue tracker](github:issue).
```important::
Just the simple act of us know you are out there using Evennia helps a lot!
@ -35,9 +35,9 @@ Evennia is open-source and non-commercial. It relies on the time donated by its
If you'd like to help develop Evennia more hands-on, here are some ways to get going:
- Look through this [online documentation](./index#Evennia-documentation) and see if you can help improve or expand the
documentation (even small things like fixing typos!). [See here](./Contributing-Docs) on how you
contribute to the docs.
- Look through this [online documentation](./index#Evennia-documentation) and see if you can help improve or expand the
documentation (even small things like fixing typos!). [See here](./Contributing-Docs) on how you
contribute to the docs.
- Send a message to our [discussion group][group] and/or our [IRC chat][chat] asking about what
needs doing, along with what your interests and skills are.
- Take a look at our [issue tracker][issues] and see if there's something you feel like taking on.
@ -47,10 +47,10 @@ needs doing, along with what your interests and skills are.
github.
... And finally, if you want to help motivate and support development you can also drop some coins
in the developer's cup. You can [make a donation via PayPal][paypal] or, even better, [become an
Evennia patron on Patreon][patreon]! This is a great way to tip your hat and show that you
in the developer's cup. You can [make a donation via PayPal][paypal] or, even better,
[become an Evennia patron on Patreon][patreon]! This is a great way to tip your hat and show that you
appreciate the work done with the server! You can also encourage the community to take on particular
issues by putting up a monetary [bounty][bountysource] on it.
issues by putting up a monetary [bounty][bountysource] on it.
[form]: https://docs.google.com/spreadsheet/viewform?hl=en_US&formkey=dGN0VlJXMWpCT3VHaHpscDEzY1RoZGc6MQ#gid=0
@ -63,4 +63,4 @@ issues by putting up a monetary [bounty][bountysource] on it.
[patreon]: https://www.patreon.com/griatch
[patreon-img]: http://www.evennia.com/_/rsrc/1424724909023/home/evennia_patreon_100x100.png
[issues-bounties]:https://github.com/evennia/evennia/labels/bounty
[bountysource]: https://www.bountysource.com/teams/evennia
[bountysource]: https://www.bountysource.com/teams/evennia

View file

@ -50,6 +50,7 @@ class CharacterCmdSet(CmdSet):
self.add(system.CmdServerLoad())
# self.add(system.CmdPs())
self.add(system.CmdTickers())
self.add(system.CmdTasks())
# Admin commands
self.add(admin.CmdBoot())

View file

@ -8,7 +8,6 @@ System commands
import code
import traceback
import os
import io
import datetime
import sys
import django
@ -25,9 +24,12 @@ from evennia.utils import logger, utils, gametime, create, search
from evennia.utils.eveditor import EvEditor
from evennia.utils.evtable import EvTable
from evennia.utils.evmore import EvMore
from evennia.utils.utils import crop, class_from_module
from evennia.utils.evmenu import ask_yes_no
from evennia.utils.utils import crop, class_from_module, iter_to_str
from evennia.scripts.taskhandler import TaskHandlerTask
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
_TASK_HANDLER = None
# delayed imports
_RESOURCE = None
@ -45,6 +47,7 @@ __all__ = (
"CmdAbout",
"CmdTime",
"CmdServerLoad",
"CmdTasks",
)
@ -1203,3 +1206,224 @@ class CmdTickers(COMMAND_DEFAULT_CLASS):
"*" if sub[5] else "-",
)
self.caller.msg("|wActive tickers|n:\n" + str(table))
class CmdTasks(COMMAND_DEFAULT_CLASS):
"""
Display or terminate active tasks (delays).
Usage:
tasks[/switch] [task_id or function_name]
Switches:
pause - Pause the callback of a task.
unpause - Process all callbacks made since pause() was called.
do_task - Execute the task (call its callback).
call - Call the callback of this task.
remove - Remove a task without executing it.
cancel - Stop a task from automatically executing.
Notes:
A task is a single use method of delaying the call of a function. Calls are created
in code, using `evennia.utils.delay`.
See |luhttps://www.evennia.com/docs/latest/Command-Duration.html|ltthe docs|le for help.
By default, tasks that are canceled and never called are cleaned up after one minute.
Examples:
- `tasks/cancel move_callback` - Cancels all movement delays from the slow_exit contrib.
In this example slow exits creates it's tasks with
`utils.delay(move_delay, move_callback)`
- `tasks/cancel 2` - Cancel task id 2.
"""
key = "tasks"
aliases = ["delays", "task"]
switch_options = ("pause", "unpause", "do_task", "call", "remove", "cancel")
locks = "perm(Developer)"
help_category = "System"
@staticmethod
def coll_date_func(task):
"""Replace regex characters in date string and collect deferred function name."""
t_comp_date = str(task[0]).replace('-', '/')
t_func_name = str(task[1]).split(' ')
t_func_mem_ref = t_func_name[3] if len(t_func_name) >= 4 else None
return t_comp_date, t_func_mem_ref
def do_task_action(self, *args, **kwargs):
"""
Process the action of a tasks command.
This exists to gain support with yes or no function from EvMenu.
"""
task_id = self.task_id
# get a reference of the global task handler
global _TASK_HANDLER
if _TASK_HANDLER is None:
from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER
# verify manipulating the correct task
task_args = _TASK_HANDLER.tasks.get(task_id, False)
if not task_args: # check if the task is still active
self.msg('Task completed while waiting for input.')
return
else:
# make certain a task with matching IDs has not been created
t_comp_date, t_func_mem_ref = self.coll_date_func(task_args)
if self.t_comp_date != t_comp_date or self.t_func_mem_ref != t_func_mem_ref:
self.msg('Task completed while waiting for input.')
return
# Do the action requested by command caller
action_return = self.task_action()
self.msg(f'{self.action_request} request completed.')
self.msg(f'The task function {self.action_request} returned: {action_return}')
def func(self):
# get a reference of the global task handler
global _TASK_HANDLER
if _TASK_HANDLER is None:
from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER
# handle no tasks active.
if not _TASK_HANDLER.tasks:
self.msg('There are no active tasks.')
if self.switches or self.args:
self.msg('Likely the task has completed and been removed.')
return
# handle caller's request to manipulate a task(s)
if self.switches and self.lhs:
# find if the argument is a task id or function name
action_request = self.switches[0]
try:
arg_is_id = int(self.lhslist[0])
except ValueError:
arg_is_id = False
# if the argument is a task id, proccess the action on a single task
if arg_is_id:
err_arg_msg = 'Switch and task ID are required when manipulating a task.'
task_comp_msg = 'Task completed while processing request.'
# handle missing arguments or switches
if not self.switches and self.lhs:
self.msg(err_arg_msg)
return
# create a handle for the task
task_id = arg_is_id
task = TaskHandlerTask(task_id)
# handle task no longer existing
if not task.exists():
self.msg(f'Task {task_id} does not exist.')
return
# get a reference of the function caller requested
switch_action = getattr(task, action_request, False)
if not switch_action:
self.msg(f'{self.switches[0]}, is not an acceptable task action or ' \
f'{task_comp_msg.lower()}')
# verify manipulating the correct task
if task_id in _TASK_HANDLER.tasks:
task_args = _TASK_HANDLER.tasks.get(task_id, False)
if not task_args: # check if the task is still active
self.msg(task_comp_msg)
return
else:
t_comp_date, t_func_mem_ref = self.coll_date_func(task_args)
t_func_name = str(task_args[1]).split(' ')
t_func_name = t_func_name[1] if len(t_func_name) >= 2 else None
if task.exists(): # make certain the task has not been called yet.
prompt = (f'{action_request.capitalize()} task {task_id} with completion date '
f'{t_comp_date} ({t_func_name}) {{options}}?')
no_msg = f'No {action_request} processed.'
# record variables for use in do_task_action method
self.task_id = task_id
self.t_comp_date = t_comp_date
self.t_func_mem_ref = t_func_mem_ref
self.task_action = switch_action
self.action_request = action_request
ask_yes_no(self.caller,
prompt=prompt,
yes_action=self.do_task_action,
no_action=no_msg,
default="Y",
allow_abort=True)
return True
else:
self.msg(task_comp_msg)
return
# the argument is not a task id, process the action on all task deferring the function
# specified as an argument
else:
name_match_found = False
arg_func_name = self.lhslist[0].lower()
# repack tasks into a new dictionary
current_tasks = {}
for task_id, task_args in _TASK_HANDLER.tasks.items():
current_tasks.update({task_id: task_args})
# call requested action on all tasks with the function name
for task_id, task_args in current_tasks.items():
t_func_name = str(task_args[1]).split(' ')
t_func_name = t_func_name[1] if len(t_func_name) >= 2 else None
# skip this task if it is not for the function desired
if arg_func_name != t_func_name:
continue
name_match_found = True
task = TaskHandlerTask(task_id)
switch_action = getattr(task, action_request, False)
if switch_action:
action_return = switch_action()
self.msg(f'Task action {action_request} completed on task ID {task_id}.')
self.msg(f'The task function {action_request} returned: {action_return}')
# provide a message if not tasks of the function name was found
if not name_match_found:
self.msg(f'No tasks deferring function name {arg_func_name} found.')
return
return True
# check if an maleformed request was created
elif self.switches or self.lhs:
self.msg('Task command misformed.')
self.msg('Proper format tasks[/switch] [function name or task id]')
return
# No task manupilation requested, build a table of tasks and display it
# get the width of screen in characters
width = self.client_width()
# create table header and list to hold tasks data and actions
tasks_header = ('Task ID', 'Completion Date', 'Function', 'Arguments', 'KWARGS',
'persistent')
# empty list of lists, the size of the header
tasks_list = [list() for i in range(len(tasks_header))]
for task_id, task in _TASK_HANDLER.tasks.items():
# collect data from the task
t_comp_date, t_func_mem_ref = self.coll_date_func(task)
t_func_name = str(task[1]).split(' ')
t_func_name = t_func_name[1] if len(t_func_name) >= 2 else None
t_args = str(task[2])
t_kwargs = str(task[3])
t_pers = str(task[4])
# add task data to the tasks list
task_data = (task_id, t_comp_date, t_func_name, t_args, t_kwargs, t_pers)
for i in range(len(tasks_header)):
tasks_list[i].append(task_data[i])
# create and display the table
tasks_table = EvTable(*tasks_header, table=tasks_list, maxwidth=width, border='cells',
align='center')
actions = (f'/{switch}' for switch in self.switch_options)
helptxt = f"\nActions: {iter_to_str(actions)}"
self.msg(str(tasks_table) + helptxt)

View file

@ -18,6 +18,7 @@ from anything import Anything
from parameterized import parameterized
from django.conf import settings
from twisted.internet import task
from unittest.mock import patch, Mock, MagicMock
from evennia import DefaultRoom, DefaultExit, ObjectDB
@ -575,6 +576,170 @@ class TestSystem(CommandTest):
def test_server_load(self):
self.call(system.CmdServerLoad(), "", "Server CPU and Memory load:")
_TASK_HANDLER = None
def func_test_cmd_tasks():
return 'success'
class TestCmdTasks(CommandTest):
def setUp(self):
super().setUp()
# get a reference of TASK_HANDLER
self.timedelay = 5
global _TASK_HANDLER
if _TASK_HANDLER is None:
from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER
_TASK_HANDLER.clock = task.Clock()
self.task_handler = _TASK_HANDLER
self.task_handler.clear()
self.task = self.task_handler.add(self.timedelay, func_test_cmd_tasks)
task_args = self.task_handler.tasks.get(self.task.get_id(), False)
def tearDown(self):
super().tearDown()
self.task_handler.clear()
def test_no_tasks(self):
self.task_handler.clear()
self.call(system.CmdTasks(), '', 'There are no active tasks.')
def test_active_task(self):
cmd_result = self.call(system.CmdTasks(), '')
for ptrn in ('Task ID', 'Completion Date', 'Function', 'KWARGS', 'persisten',
'1', r'\d+/\d+/\d+', r'\d+\:\d+\:\d+', r'ms\:\d+', 'func_test', '{}',
'False'):
self.assertRegex(cmd_result, ptrn)
def test_persistent_task(self):
self.task_handler.clear()
self.task_handler.add(self.timedelay, func_test_cmd_tasks, persistent=True)
cmd_result = self.call(system.CmdTasks(), '')
self.assertRegex(cmd_result, 'True')
def test_pause_unpause(self):
# test pause
args = f'/pause {self.task.get_id()}'
wanted_msg = 'Yes or No, pause task 1? With completion date'
cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
self.assertRegex(cmd_result, '\. Deferring function func_test_cmd_tasks\.')
self.char1.execute_cmd('y')
self.assertTrue(self.task.paused)
self.task_handler.clock.advance(self.timedelay + 1)
# test unpause
args = f'/unpause {self.task.get_id()}'
self.assertTrue(self.task.exists())
wanted_msg = 'Yes or No, unpause task 1? With completion date'
cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
self.assertRegex(cmd_result, '\. Deferring function func_test_cmd_tasks\.')
self.char1.execute_cmd('y')
# verify task continues after unpause
self.task_handler.clock.advance(1)
self.assertFalse(self.task.exists())
def test_do_task(self):
args = f'/do_task {self.task.get_id()}'
wanted_msg = 'Yes or No, do_task task 1? With completion date'
cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
self.assertRegex(cmd_result, '\. Deferring function func_test_cmd_tasks\.')
self.char1.execute_cmd('y')
self.assertFalse(self.task.exists())
def test_remove(self):
args = f'/remove {self.task.get_id()}'
wanted_msg = 'Yes or No, remove task 1? With completion date'
cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
self.assertRegex(cmd_result, '\. Deferring function func_test_cmd_tasks\.')
self.char1.execute_cmd('y')
self.assertFalse(self.task.exists())
def test_call(self):
args = f'/call {self.task.get_id()}'
wanted_msg = 'Yes or No, call task 1? With completion date'
cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
self.assertRegex(cmd_result, '\. Deferring function func_test_cmd_tasks\.')
self.char1.execute_cmd('y')
# make certain the task is still active
self.assertTrue(self.task.active())
# go past delay time, the task should call do_task and remove itself after calling.
self.task_handler.clock.advance(self.timedelay + 1)
self.assertFalse(self.task.exists())
def test_cancel(self):
args = f'/cancel {self.task.get_id()}'
wanted_msg = 'Yes or No, cancel task 1? With completion date'
cmd_result = self.call(system.CmdTasks(), args, wanted_msg)
self.assertRegex(cmd_result, '\. Deferring function func_test_cmd_tasks\.')
self.char1.execute_cmd('y')
self.assertTrue(self.task.exists())
self.assertFalse(self.task.active())
def test_func_name_manipulation(self):
self.task_handler.add(self.timedelay, func_test_cmd_tasks) # add an extra task
args = f'/remove func_test_cmd_tasks'
wanted_msg = 'Task action remove completed on task ID 1.|The task function remove returned: True|' \
'Task action remove completed on task ID 2.|The task function remove returned: True'
self.call(system.CmdTasks(), args, wanted_msg)
self.assertFalse(self.task_handler.tasks) # no tasks should exist.
def test_wrong_func_name(self):
args = f'/remove intentional_fail'
wanted_msg = 'No tasks deferring function name intentional_fail found.'
self.call(system.CmdTasks(), args, wanted_msg)
self.assertTrue(self.task.active())
def test_no_input(self):
args = f'/cancel {self.task.get_id()}'
self.call(system.CmdTasks(), args)
# task should complete since no input was received
self.task_handler.clock.advance(self.timedelay + 1)
self.assertFalse(self.task.exists())
def test_responce_of_yes(self):
self.call(system.CmdTasks(), f'/cancel {self.task.get_id()}')
self.char1.msg = Mock()
self.char1.execute_cmd('y')
text = ''
for _, _, kwargs in self.char1.msg.mock_calls:
text += kwargs.get('text', '')
self.assertEqual(text, 'cancel request completed.The task function cancel returned: True')
self.assertTrue(self.task.exists())
def test_task_complete_waiting_input(self):
"""Test for task completing while waiting for input."""
self.call(system.CmdTasks(), f'/cancel {self.task.get_id()}')
self.task_handler.clock.advance(self.timedelay + 1)
self.char1.msg = Mock()
self.char1.execute_cmd('y')
text = ''
for _, _, kwargs in self.char1.msg.mock_calls:
text += kwargs.get('text', '')
self.assertEqual(text, 'Task completed while waiting for input.')
self.assertFalse(self.task.exists())
def test_new_task_waiting_input(self):
"""
Test task completing than a new task with the same ID being made while waitinf for input.
"""
self.assertTrue(self.task.get_id(), 1)
self.call(system.CmdTasks(), f'/cancel {self.task.get_id()}')
self.task_handler.clock.advance(self.timedelay + 1)
self.assertFalse(self.task.exists())
self.task = self.task_handler.add(self.timedelay, func_test_cmd_tasks)
self.assertTrue(self.task.get_id(), 1)
self.char1.msg = Mock()
self.char1.execute_cmd('y')
text = ''
for _, _, kwargs in self.char1.msg.mock_calls:
text += kwargs.get('text', '')
self.assertEqual(text, 'Task completed while waiting for input.')
def test_misformed_command(self):
wanted_msg = 'Task command misformed.|Proper format tasks[/switch] ' \
'[function name or task id]'
self.call(system.CmdTasks(), f'/cancel', wanted_msg)
class TestAdmin(CommandTest):
def test_emit(self):

View file

@ -182,9 +182,9 @@ class TestCraftingRecipe(TestCase):
"i1": "cons2",
"i2": "cons3",
"o0": "Result1",
"tools": "bar, bar2 and bar3",
"tools": "bar, bar2, and bar3",
"consumables": "cons1 and cons2",
"inputs": "cons1, cons2 and cons3",
"inputs": "cons1, cons2, and cons3",
"outputs": "Result1",
}

View file

@ -292,14 +292,25 @@ class TaskHandler(object):
if not persistent:
continue
safe_callback = callback
if getattr(callback, "__self__", None):
# `callback` is an instance method
obj = callback.__self__
name = callback.__name__
callback = (obj, name)
safe_callback = (obj, name)
# Check if callback can be pickled. args and kwargs have been checked
self.to_save[task_id] = dbserialize((date, callback, args, kwargs))
try:
dbserialize(safe_callback)
except (TypeError, AttributeError, PickleError) as err:
raise ValueError(
"the specified callback {callback} cannot be pickled. "
"It must be a top-level function in a module or an "
"instance method ({err}).".format(callback=callback, err=err)
)
self.to_save[task_id] = dbserialize((date, safe_callback, args, kwargs))
ServerConfig.objects.conf("delayed_tasks", self.to_save)
def add(self, timedelay, callback, *args, **kwargs):
@ -348,17 +359,6 @@ class TaskHandler(object):
safe_args = []
safe_kwargs = {}
# an unsaveable callback should immediately abort
try:
dbserialize(callback)
except (TypeError, AttributeError, PickleError):
raise ValueError(
"the specified callback {} cannot be pickled. "
"It must be a top-level function in a module or an "
"instance method.".format(callback)
)
return
# Check that args and kwargs contain picklable information
for arg in args:
try:

View file

@ -16,12 +16,14 @@ http://www.gammon.com.au/mushclient/addingservermxp.htm
import re
LINKS_SUB = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
URL_SUB = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
# MXP Telnet option
MXP = bytes([91]) # b"\x5b"
MXP_TEMPSECURE = "\x1B[4z"
MXP_SEND = MXP_TEMPSECURE + '<SEND HREF="\\1">' + "\\2" + MXP_TEMPSECURE + "</SEND>"
MXP_URL = MXP_TEMPSECURE + '<A HREF="\\1">' + "\\2" + MXP_TEMPSECURE + "</A>"
def mxp_parse(text):
@ -38,6 +40,7 @@ def mxp_parse(text):
text = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
text = LINKS_SUB.sub(MXP_SEND, text)
text = URL_SUB.sub(MXP_URL, text)
return text

View file

@ -128,8 +128,8 @@ def c_login(client):
def c_login_nodig(client):
"logins, don't dig its own room"
cname = DUMMY_NAME % client.gid
cpwd = DUMMY_PWD % client.gid
cname = DUMMY_NAME.format(gid=client.gid)
cpwd = DUMMY_PWD.format(gid=client.gid)
cmds = (
f"create {cname} {cpwd}",
f"connect {cname} {cpwd}"
@ -183,7 +183,7 @@ def c_digs(client):
exitname1 = EXIT_TEMPLATE % client.counter()
exitname2 = EXIT_TEMPLATE % client.counter()
client.exits.extend([exitname1, exitname2])
return ("@dig/tel %s = %s, %s" % (roomname, exitname1, exitname2),)
return ("dig/tel %s = %s, %s" % (roomname, exitname1, exitname2),)
def c_creates_obj(client):
@ -191,10 +191,10 @@ def c_creates_obj(client):
objname = OBJ_TEMPLATE % client.counter()
client.objs.append(objname)
cmds = (
"@create %s" % objname,
'@desc %s = "this is a test object' % objname,
"@set %s/testattr = this is a test attribute value." % objname,
"@set %s/testattr2 = this is a second test attribute." % objname,
"create %s" % objname,
'desc %s = "this is a test object' % objname,
"set %s/testattr = this is a test attribute value." % objname,
"set %s/testattr2 = this is a second test attribute." % objname,
)
return cmds
@ -203,7 +203,7 @@ def c_creates_button(client):
"creates example button, storing name on client"
objname = TOBJ_TEMPLATE % client.counter()
client.objs.append(objname)
cmds = ("@create %s:%s" % (objname, TOBJ_TYPECLASS), "@desc %s = test red button!" % objname)
cmds = ("create %s:%s" % (objname, TOBJ_TYPECLASS), "desc %s = test red button!" % objname)
return cmds
@ -295,7 +295,7 @@ elif PROFILE == 'normal_builder':
(0.1, c_help),
(0.01, c_digs),
(0.01, c_creates_obj),
(0.2, c_moves)
(0.2, c_moves),
(0.1, c_measure_lag)
)
elif PROFILE == 'heavy_builder':

View file

@ -1,5 +1,6 @@
from django.test import TestCase
from mock import Mock, patch, mock_open
from anything import Something
from .dummyrunner_settings import (
c_creates_button,
c_creates_obj,
@ -29,8 +30,8 @@ class TestDummyrunnerSettings(TestCase):
self.client.cid = 1
self.client.counter = Mock(return_value=1)
self.client.gid = "20171025161153-1"
self.client.name = "Dummy-%s" % self.client.gid
self.client.password = "password-%s" % self.client.gid
self.client.name = "Dummy_%s" % self.client.gid
self.client.password = Something,
self.client.start_room = "testing_room_start_%s" % self.client.gid
self.client.objs = []
self.client.exits = []
@ -43,28 +44,25 @@ class TestDummyrunnerSettings(TestCase):
self.assertEqual(
c_login(self.client),
(
"create %s %s" % (self.client.name, self.client.password),
"connect %s %s" % (self.client.name, self.client.password),
"@dig %s" % self.client.start_room,
"@teleport %s" % self.client.start_room,
"@dig testing_room_1 = exit_1, exit_1",
Something, # create
Something, # connect
"dig %s" % self.client.start_room,
"teleport %s" % self.client.start_room,
"py from evennia.server.profiling.dummyrunner import DummyRunnerCmdSet;"
"self.cmdset.add(DummyRunnerCmdSet, persistent=False)"
),
)
def test_c_login_no_dig(self):
self.assertEqual(
c_login_nodig(self.client),
(
"create %s %s" % (self.client.name, self.client.password),
"connect %s %s" % (self.client.name, self.client.password),
),
)
cmd1, cmd2 = c_login_nodig(self.client)
self.assertTrue(cmd1.startswith("create " + self.client.name + " "))
self.assertTrue(cmd2.startswith("connect " + self.client.name + " "))
def test_c_logout(self):
self.assertEqual(c_logout(self.client), "@quit")
self.assertEqual(c_logout(self.client), ("quit",))
def perception_method_tests(self, func, verb, alone_suffix=""):
self.assertEqual(func(self.client), "%s%s" % (verb, alone_suffix))
self.assertEqual(func(self.client), ("%s%s" % (verb, alone_suffix),))
self.client.exits = ["exit1", "exit2"]
self.assertEqual(func(self.client), ["%s exit1" % verb, "%s exit2" % verb])
self.client.objs = ["foo", "bar"]
@ -83,11 +81,11 @@ class TestDummyrunnerSettings(TestCase):
def test_c_help(self):
self.assertEqual(
c_help(self.client),
("help", "help @teleport", "help look", "help @tunnel", "help @dig"),
("help", "dummyrunner_echo_response"),
)
def test_c_digs(self):
self.assertEqual(c_digs(self.client), ("@dig/tel testing_room_1 = exit_1, exit_1"))
self.assertEqual(c_digs(self.client), ("dig/tel testing_room_1 = exit_1, exit_1", ))
self.assertEqual(self.client.exits, ["exit_1", "exit_1"])
self.clear_client_lists()
@ -96,10 +94,10 @@ class TestDummyrunnerSettings(TestCase):
self.assertEqual(
c_creates_obj(self.client),
(
"@create %s" % objname,
'@desc %s = "this is a test object' % objname,
"@set %s/testattr = this is a test attribute value." % objname,
"@set %s/testattr2 = this is a second test attribute." % objname,
"create %s" % objname,
'desc %s = "this is a test object' % objname,
"set %s/testattr = this is a test attribute value." % objname,
"set %s/testattr2 = this is a second test attribute." % objname,
),
)
self.assertEqual(self.client.objs, [objname])
@ -110,7 +108,7 @@ class TestDummyrunnerSettings(TestCase):
typeclass_name = "contrib.tutorial_examples.red_button.RedButton"
self.assertEqual(
c_creates_button(self.client),
("@create %s:%s" % (objname, typeclass_name), "@desc %s = test red button!" % objname),
("create %s:%s" % (objname, typeclass_name), "desc %s = test red button!" % objname),
)
self.assertEqual(self.client.objs, [objname])
self.clear_client_lists()
@ -119,25 +117,23 @@ class TestDummyrunnerSettings(TestCase):
self.assertEqual(
c_socialize(self.client),
(
"ooc Hello!",
"ooc Testing ...",
"ooc Testing ... times 2",
"pub Hello!",
"say Yo!",
"emote stands looking around.",
),
)
def test_c_moves(self):
self.assertEqual(c_moves(self.client), "look")
self.assertEqual(c_moves(self.client), ("look",))
self.client.exits = ["south", "north"]
self.assertEqual(c_moves(self.client), ["south", "north"])
self.clear_client_lists()
def test_c_move_n(self):
self.assertEqual(c_moves_n(self.client), "north")
self.assertEqual(c_moves_n(self.client), ("north",))
def test_c_move_s(self):
self.assertEqual(c_moves_s(self.client), "south")
self.assertEqual(c_moves_s(self.client), ("south",))
class TestMemPlot(TestCase):

View file

@ -223,6 +223,7 @@ class ANSIParser(object):
ansi_xterm256_bright_bg_map += settings.COLOR_ANSI_XTERM256_BRIGHT_BG_EXTRA_MAP
mxp_re = r"\|lc(.*?)\|lt(.*?)\|le"
mxp_url_re = r"\|lu(.*?)\|lt(.*?)\|le"
# prepare regex matching
brightbg_sub = re.compile(
@ -237,6 +238,7 @@ class ANSIParser(object):
# xterm256_sub = re.compile(r"|".join([tup[0] for tup in xterm256_map]), re.DOTALL)
ansi_sub = re.compile(r"|".join([re.escape(tup[0]) for tup in ansi_map]), re.DOTALL)
mxp_sub = re.compile(mxp_re, re.DOTALL)
mxp_url_sub = re.compile(mxp_url_re, re.DOTALL)
# used by regex replacer to correctly map ansi sequences
ansi_map_dict = dict(ansi_map)
@ -424,7 +426,9 @@ class ANSIParser(object):
string (str): The processed string.
"""
return self.mxp_sub.sub(r"\2", string)
string = self.mxp_sub.sub(r"\2", string)
string = self.mxp_url_sub.sub(r"\1", string) # replace with url verbatim
return string
def parse_ansi(self, string, strip_ansi=False, xterm256=False, mxp=False):
"""

View file

@ -145,13 +145,17 @@ class ANSIStringTestCase(TestCase):
"""
mxp1 = "|lclook|ltat|le"
mxp2 = "Start to |lclook here|ltclick somewhere here|le first"
mxp3 = "Check out |luhttps://www.example.com|ltmy website|le!"
self.assertEqual(15, len(ANSIString(mxp1)))
self.assertEqual(53, len(ANSIString(mxp2)))
self.assertEqual(53, len(ANSIString(mxp3)))
# These would indicate an issue with the tables.
self.assertEqual(len(ANSIString(mxp1)), len(ANSIString(mxp1).split("\n")[0]))
self.assertEqual(len(ANSIString(mxp2)), len(ANSIString(mxp2).split("\n")[0]))
self.assertEqual(len(ANSIString(mxp3)), len(ANSIString(mxp3).split("\n")[0]))
self.assertEqual(mxp1, ANSIString(mxp1))
self.assertEqual(mxp2, str(ANSIString(mxp2)))
self.assertEqual(mxp3, str(ANSIString(mxp3)))
def test_add(self):
"""

View file

@ -72,10 +72,11 @@ class TestListToString(TestCase):
def test_list_to_string(self):
self.assertEqual("1, 2, 3", utils.list_to_string([1, 2, 3], endsep=""))
self.assertEqual('"1", "2", "3"', utils.list_to_string([1, 2, 3], endsep="", addquote=True))
self.assertEqual("1, 2 and 3", utils.list_to_string([1, 2, 3]))
self.assertEqual("1, 2, and 3", utils.list_to_string([1, 2, 3]))
self.assertEqual(
'"1", "2" and "3"', utils.list_to_string([1, 2, 3], endsep="and", addquote=True)
'"1", "2", and "3"', utils.list_to_string([1, 2, 3], endsep="and", addquote=True)
)
self.assertEqual("1 and 2", utils.list_to_string([1, 2]))
class TestMLen(TestCase):

View file

@ -103,9 +103,10 @@ class TextToHTMLparser(object):
)
re_dblspace = re.compile(r" {2,}", re.M)
re_url = re.compile(
r'((?:ftp|www|https?)\W+(?:(?!\.(?:\s|$)|&\w+;)[^"\',;$*^\\(){}<>\[\]\s])+)(\.(?:\s|$)|&\w+;|)'
r'(?<!=")((?:ftp|www|https?)\W+(?:(?!\.(?:\s|$)|&\w+;)[^"\',;$*^\\(){}<>\[\]\s])+)(\.(?:\s|$)|&\w+;|)'
)
re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
def _sub_bgfg(self, colormatch):
# print("colormatch.groups()", colormatch.groups())
@ -290,6 +291,21 @@ class TextToHTMLparser(object):
)
return val
def sub_mxp_urls(self, match):
"""
Helper method to be passed to re.sub,
replaces MXP links with HTML code.
Args:
match (re.Matchobject): Match for substitution.
Returns:
text (str): Processed text.
"""
url, text = [grp.replace('"', "\\&quot;") for grp in match.groups()]
val = (
r"""<a id="mxplink" href="{url}" target="_blank">{text}</a>""".format(url=url, text=text)
)
return val
def sub_text(self, match):
"""
Helper method to be passed to re.sub,
@ -337,6 +353,7 @@ class TextToHTMLparser(object):
# convert all ansi to html
result = re.sub(self.re_string, self.sub_text, text)
result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result)
result = self.re_color(result)
result = self.re_bold(result)
result = self.re_underline(result)

View file

@ -381,15 +381,13 @@ def iter_to_str(initer, endsep="and", addquote=False):
>>> list_to_string([1,2,3], endsep='')
'1, 2, 3'
>>> list_to_string([1,2,3], ensdep='and')
'1, 2 and 3'
'1, 2, and 3'
>>> list_to_string([1,2,3], endsep='and', addquote=True)
'"1", "2" and "3"'
'"1", "2", and "3"'
```
"""
if not endsep:
endsep = ","
else:
if endsep:
endsep = " " + endsep
if not initer:
return ""
@ -397,11 +395,15 @@ def iter_to_str(initer, endsep="and", addquote=False):
if addquote:
if len(initer) == 1:
return '"%s"' % initer[0]
return ", ".join('"%s"' % v for v in initer[:-1]) + "%s %s" % (endsep, '"%s"' % initer[-1])
elif len(initer) == 2:
return '"%s"' % ('"%s "' % endsep).join(str(v) for v in initer)
return ", ".join('"%s"' % v for v in initer[:-1]) + ",%s %s" % (endsep, '"%s"' % initer[-1])
else:
if len(initer) == 1:
return str(initer[0])
return ", ".join(str(v) for v in initer[:-1]) + "%s %s" % (endsep, initer[-1])
elif len(initer) == 2:
return ("%s " % endsep).join(str(v) for v in initer)
return ", ".join(str(v) for v in initer[:-1]) + ",%s %s" % (endsep, initer[-1])
# legacy aliases