improve interactive client

- remove irrelevant traceback on remote errors
- add run() function to execute scripts
- when started with bin/frappy-cli, use separate namespace

Change-Id: Ic808a76fa76ecd8d814d52b15a6d7d2203c6a2f3
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/30957
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
zolliker 2023-04-13 16:02:43 +02:00
parent 5784aa0f5d
commit 748ea1400a
3 changed files with 110 additions and 65 deletions

View File

@ -23,8 +23,6 @@
#
# *****************************************************************************
from __future__ import print_function
import sys
import argparse
from os import path
@ -32,7 +30,7 @@ from os import path
# Add import path for inplace usage
sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..')))
from frappy.client.interactive import Client, watch, Console
from frappy.client.interactive import Client, Console, clientenv, run
def parseArgv(argv):
@ -49,58 +47,56 @@ def parseArgv(argv):
return parser.parse_args(argv)
_USAGE = """
USAGE = """
Usage:
%s
{client_assign}
# for all SECoP modules objects are created in the main namespace
<module> # list all parameters
<module>.<param> = <value> # change parameter
<module>(<target>) # set target and wait until not busy
# 'status' and 'value' changes are shown every 1 sec
%s.mininterval = 0.2 # change minimal update interval to 0.2 sec (default is 1 second)
{client_name}.mininterval = 0.2 # change minimal update interval to 0.2 s (default is 1 s)
watch(T) # watch changes of T.status and T.value (stop with ctrl-C)
watch(T='status target') # watch status and target parameters
watch(io, T=True) # watch io and all parameters of T
"""
_CLIENT_USAGE = """
c = Client('localhost:5000')
"""
Client.show_usage = False
{tail}"""
args = parseArgv(sys.argv[1:])
if not args.node:
_usage_args = ("\ncli = Client('localhost:5000')\n", 'cli')
usage_args = {
'client_assign': "\ncli = Client('localhost:5000')\n",
'client_name': 'cli'}
success = True
else:
_usage_args = ('', '_c0')
usage_args = {
'client_assign': '',
'client_name': '_c0'}
success = False
for _idx, _node in enumerate(args.node):
_client_name = '_c%d' % _idx
clientenv.init()
for idx, node in enumerate(args.node):
client_name = '_c%d' % idx
try:
setattr(sys.modules['__main__'], _client_name, Client(_node, name=_client_name))
clientenv.namespace[client_name] = Client(node, name=client_name)
success = True
except Exception as e:
print(repr(e))
run_error = ''
file_success = False
try:
for file in args.include:
with open(file, 'r') as f:
exec(f.read())
run(file)
file_success = True
except Exception as e:
print('Error while executing %s: %s' % (file, e))
run_error = f'\n{clientenv.short_traceback()}'
if success:
if args.include and file_success and args.only_execute:
print('skipping interactive mode')
exit()
print(_USAGE % _usage_args)
Console(sys.modules['__main__'].__dict__)
print(USAGE.format(tail=run_error, **usage_args))
Console()

View File

@ -36,4 +36,4 @@ if len(sys.argv) > 1:
else:
print(USAGE)
Console(sys.modules['__main__'].__dict__, 'play')
Console('play', sys.modules['__main__'].__dict__)

View File

@ -32,11 +32,14 @@ client = Client('localhost:5000') # start client.
<module>.<param> = <value> # change parameter
<module>(<target>) # set target and wait until not busy
# 'status' and 'value' changes are shown every 1 sec
client.mininterval = 0.2 # change minimal update interval to 0.2 sec (default is 1 second)
client.mininterval = 0.2 # change minimal update interval to 0.2 s (default is 1 s)
watch(T) # watch changes of T.status and T.value (stop with ctrl-C)
watch(T='status target') # watch status and target parameters
watch(io, T=True) # watch io and all parameters of T
run('filename') # execute a script
/filename # execute a script
"""
import sys
@ -45,6 +48,7 @@ import re
import code
import signal
import os
import traceback
from os.path import expanduser
from queue import Queue
from frappy.client import SecopClient
@ -55,8 +59,6 @@ try:
except ImportError:
readline = None
main = sys.modules['__main__']
LOG_LEVELS = {'debug', 'comlog', 'info', 'warning', 'error', 'off'}
CLR = '\r\x1b[K' # code to move to the left and clear current line
@ -210,7 +212,7 @@ class Module:
def read(self, pname='value'):
value, _, error = self._secnode.readParameter(self._name, pname)
if error:
Console.raise_without_traceback(error)
clientenv.raise_with_short_traceback(error)
return value
def __call__(self, target=None):
@ -231,7 +233,7 @@ class Module:
return self.value
def __repr__(self):
wid = max(len(k) for k in self._parameters)
wid = max((len(k) for k in self._parameters), default=0)
return '%s\n%s%s' % (
self._title,
'\n'.join(self._one_line(k, wid) for k in self._parameters),
@ -260,8 +262,7 @@ class Param:
return self
value, _, error = obj._secnode.cache[obj._name, self.name]
if error:
Console.raise_without_traceback(error)
raise error
clientenv.raise_with_short_traceback(error)
return value
def formatted(self, obj):
@ -275,9 +276,10 @@ class Param:
obj._running = Queue()
try:
obj._secnode.setParameter(obj._name, self.name, value)
return
except SECoPError as e:
Console.raise_without_traceback(e)
# obj._secnode.log.error(repr(e))
clientenv.raise_with_short_traceback(e)
obj._secnode.log.error(repr(e))
def format(self, value):
return self.datatype.format_value(value)
@ -294,6 +296,8 @@ class Command:
if args:
raise TypeError('mixed arguments forbidden')
result, _ = self.exec(self.modname, self.name, kwds)
elif len(args) == 1:
result, _ = self.exec(self.modname, self.name, *args)
else:
result, _ = self.exec(self.modname, self.name, args or None)
return result
@ -306,7 +310,7 @@ class Command:
def show_parameter(modname, pname, *args, forced=False, mininterval=0):
"""show parameter update"""
mobj = getattr(main, modname)
mobj = clientenv.namespace[modname]
mobj._watch_parameter(modname, pname, *args)
@ -320,7 +324,7 @@ def watch(*args, **kwds):
else:
print(f'do not know {mobj!r}')
for key, arg in kwds.items():
mobj = getattr(main, key, None)
mobj = clientenv.namespace.get(key)
if mobj is None:
print(f'do not know {key!r}')
else:
@ -345,6 +349,9 @@ class Client(SecopClient):
mininterval = 1
def __init__(self, uri, loglevel='info', name=''):
if clientenv.namespace is None:
# called from a simple python interpeter
clientenv.init(sys.modules['__main__'].__dict__)
# remove previous client:
prev = self.secnodes.pop(uri, None)
log = Logger(loglevel)
@ -352,10 +359,10 @@ class Client(SecopClient):
if prev:
log.info('remove previous client to %s', uri)
for modname in prev.modules:
prevnode = getattr(getattr(main, modname, None), '_secnode', None)
prevnode = getattr(clientenv.namespace.get(modname), '_secnode', None)
if prevnode == prev:
removed_modules.append(modname)
delattr(main, modname)
clientenv.namespace.pop(modname)
prev.disconnect()
self.secnodes[uri] = self
if name:
@ -365,7 +372,7 @@ class Client(SecopClient):
created_modules = []
skipped_modules = []
for modname, moddesc in self.modules.items():
prev = getattr(main, modname, None)
prev = clientenv.namespace.get(modname)
if prev is None:
created_modules.append(modname)
else:
@ -383,7 +390,7 @@ class Client(SecopClient):
if 'status' in mobj._parameters:
self.register_callback((modname, 'status'), updateEvent=mobj._status_value_update)
self.register_callback((modname, 'value'), updateEvent=mobj._status_value_update)
setattr(main, modname, mobj)
clientenv.namespace[modname] = mobj
if removed_modules:
self.log.info('removed modules: %s', ' '.join(removed_modules))
if skipped_modules:
@ -397,7 +404,7 @@ class Client(SecopClient):
"""handle logging messages"""
if action == 'log':
modname, loglevel = ident.split(':')
modobj = getattr(main, modname, None)
modobj = clientenv.namespace.get(modname)
if modobj:
modobj.handle_log_message_(loglevel, data)
return
@ -408,9 +415,54 @@ class Client(SecopClient):
return f'Client({self.uri!r})'
def run(filepath):
clientenv.namespace.update({
"__file__": filepath,
"__name__": "__main__",
})
with open(filepath, 'rb') as file:
# pylint: disable=exec-used
exec(compile(file.read(), filepath, 'exec'), clientenv.namespace, None)
class ClientEnvironment:
namespace = None
last_frames = 0
def init(self, namespace=None):
self.namespace = namespace or {}
self.namespace.update(run=run, watch=watch, Client=Client)
def raise_with_short_traceback(self, exc):
# count number of lines of internal irrelevant stack (for remote errors)
self.last_frames = len(traceback.format_exception(*sys.exc_info()))
raise exc
def short_traceback(self):
"""cleanup tracback from irrelevant lines"""
lines = traceback.format_exception(*sys.exc_info())
# line 0: Traceback header
# skip line 1+2 (contains unspecific console line and exec code)
lines[1:3] = []
if ' exec(' in lines[1]:
# replace additional irrelevant exec line if needed with run command
lines[1:2] = []
# skip lines of client code not relevant for remote errors
lines[-self.last_frames-1:-1] = []
self.last_frames = 0
if len(lines) <= 2: # traceback contains only console line
lines = lines[-1:]
return ''.join(lines)
clientenv = ClientEnvironment()
class Console(code.InteractiveConsole):
def __init__(self, local, name='cli'):
super().__init__(local)
def __init__(self, name='cli', namespace=None):
if namespace:
clientenv.namespace = namespace
super().__init__(clientenv.namespace)
history = None
if readline:
try:
@ -428,12 +480,9 @@ class Console(code.InteractiveConsole):
Logger.sigwinch = bool(readline) # activate refresh signal
line = input(prompt)
Logger.sigwinch = False
if line.startswith('/'):
line = f"run('{line[1:].strip()}')"
return line
@classmethod
def raise_without_traceback(cls, exc):
def omit_traceback_once(cls):
del Console.showtraceback
cls.showtraceback = omit_traceback_once
print('ERROR:', repr(exc))
raise exc
def showtraceback(self):
self.write(clientenv.short_traceback())