Extend py command to return stdout inline

This commit is contained in:
Griatch 2019-08-09 01:01:45 +02:00
parent 7400b4af21
commit 435a33e216

View file

@ -169,6 +169,7 @@ def _run_code_snippet(caller, pycode, mode="eval", measure_time=False,
sessions = caller.sessions.all() sessions = caller.sessions.all()
available_vars = evennia_local_vars(caller) available_vars = evennia_local_vars(caller)
if show_input: if show_input:
for session in sessions: for session in sessions:
try: try:
@ -177,7 +178,25 @@ def _run_code_snippet(caller, pycode, mode="eval", measure_time=False,
except TypeError: except TypeError:
caller.msg(">>> %s" % pycode, options={"raw": True}) caller.msg(">>> %s" % pycode, options={"raw": True})
try: try:
# reroute standard output to game client console
old_stdout = sys.stdout
old_stderr = sys.stderr
class FakeStd:
def __init__(self, caller):
self.caller = caller
def write(self, string):
self.caller.msg(string.rsplit("\n", 1)[0])
fake_std = FakeStd(caller)
sys.stdout = fake_std
sys.stderr = fake_std
try: try:
pycode_compiled = compile(pycode, "", mode) pycode_compiled = compile(pycode, "", mode)
except Exception: except Exception:
@ -190,18 +209,22 @@ def _run_code_snippet(caller, pycode, mode="eval", measure_time=False,
ret = eval(pycode_compiled, {}, available_vars) ret = eval(pycode_compiled, {}, available_vars)
t1 = time.time() t1 = time.time()
duration = " (runtime ~ %.4f ms)" % ((t1 - t0) * 1000) duration = " (runtime ~ %.4f ms)" % ((t1 - t0) * 1000)
caller.msg(duration)
else: else:
ret = eval(pycode_compiled, {}, available_vars) ret = eval(pycode_compiled, {}, available_vars)
if mode == "eval":
ret = "%s%s" % (str(ret), duration)
else:
ret = " Done (use self.msg() if you want to catch output)%s" % duration
except Exception: except Exception:
errlist = traceback.format_exc().split('\n') errlist = traceback.format_exc().split('\n')
if len(errlist) > 4: if len(errlist) > 4:
errlist = errlist[4:] errlist = errlist[4:]
ret = "\n".join("%s" % line for line in errlist if line) ret = "\n".join("%s" % line for line in errlist if line)
finally:
# return to old stdout
sys.stdout = old_stdout
sys.stderr = old_stderr
if not ret:
return
for session in sessions: for session in sessions:
try: try:
@ -223,6 +246,8 @@ def evennia_local_vars(caller):
} }
class EvenniaPythonConsole(code.InteractiveConsole): class EvenniaPythonConsole(code.InteractiveConsole):
"""Evennia wrapper around a Python interactive console.""" """Evennia wrapper around a Python interactive console."""
@ -244,7 +269,7 @@ class EvenniaPythonConsole(code.InteractiveConsole):
self.caller = caller self.caller = caller
def write(self, string): def write(self, string):
self.caller.msg(string) self.caller.msg(string.split("\n", 1)[0])
fake_std = FakeStd(self.caller) fake_std = FakeStd(self.caller)
sys.stdout = fake_std sys.stdout = fake_std
@ -272,27 +297,22 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
lead to different output depending on prototocol (such as angular brackets lead to different output depending on prototocol (such as angular brackets
being parsed as HTML in the webclient but not in telnet clients) being parsed as HTML in the webclient but not in telnet clients)
Without argument, this command opens a Python console in your client, Without argument, open a Python console in-game. This is a full console,
in which you can enter several lines of code. This console is similar accepting multi-line Python code for testing and debugging. Type `exit` to
to the standard Python console (the one that opens when typing return to the game. If Evennia is reloaded, thek console will be closed.
'python' or 'python3.7' in your console). This Python console is not
blocking so other operations can happen at the same time. You can enter
one or more Python instructions, including conditions and loops (just
be sure to include the proper level of indentation). The variables
you create in the console will be kept while the console is running.
Type the 'exit' command to quit this console without stopping Evennia.
If Evennia is reloaded, this console will be closed.
Alternatively, enter a line of instruction after the 'py' command to Enter a line of instruction after the 'py' command to execute it
execute it. Separate multiple commands by ';' or open the editor using the immediately. Separate multiple commands by ';' or open the code editor
/edit switch. A few variables are made available for convenience using the /edit switch (all lines added in editor will be executed
in order to offer access to the system (you can import more at immediately when closing or using the execute command in the editor).
execution time).
A few variables are made available for convenience in order to offer access
to the system (you can import more at execution time).
Available variables in py environment: Available variables in py environment:
self, me : caller self, me : caller
here : caller.location here : caller.location
ev : the evennia API evennia : the evennia API
inherits_from(obj, parent) : check object inheritance inherits_from(obj, parent) : check object inheritance
You can explore The evennia API from inside the game by calling You can explore The evennia API from inside the game by calling
@ -300,8 +320,8 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
py evennia.__doc__ py evennia.__doc__
py evennia.managers.__doc__ py evennia.managers.__doc__
|rNote: In the wrong hands this command is a severe security risk. |rNote: In the wrong hands this command is a severe security risk. It
It should only be accessible by trusted server admins/superusers.|n should only be accessible by trusted server admins/superusers.|n
""" """
key = "py" key = "py"
@ -327,15 +347,15 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
if not pycode: if not pycode:
# Run in interactive mode # Run in interactive mode
console = EvenniaPythonConsole(self.caller) console = EvenniaPythonConsole(self.caller)
banner = f"Python {sys.version} on {sys.platform}" banner = (f"|gPython {sys.version} on {sys.platform}\n"
banner += "\nType 'exit' to quit this console." "Evennia interactive console mode - type 'exit()' to leave.|n")
self.msg(banner) self.msg(banner)
line = "" line = ""
prompt = ">>>" prompt = ">>>"
while line.lower() not in ("exit", "exit()"): while line.lower() not in ("exit", "exit()"):
line = yield(prompt) line = yield(prompt)
prompt = "..." if console.push(line) else ">>>" prompt = "..." if console.push(line) else ">>>"
self.msg("Closing the Python console.") self.msg("|gClosing the Python console.|n")
return return
_run_code_snippet(caller, self.args, measure_time="time" in self.switches, _run_code_snippet(caller, self.args, measure_time="time" in self.switches,