# this module requires Python 2.6+ from __future__ import with_statement from contextlib import contextmanager from operator import attrgetter from gettext import gettext as _ import imp import inspect import os import sys import cmd import shlex import subprocess import argparse import itertools import traceback import multiprocessing import signal import threading import plac_core if sys.version < '3': def exec_(_code_, _globs_=None, _locs_=None): if _globs_ is None: frame = sys._getframe(1) _globs_ = frame.f_globals if _locs_ is None: _locs_ = frame.f_locals del frame elif _locs_ is None: _locs_ = _globs_ exec("""exec _code_ in _globs_, _locs_""") exec(''' def raise_(tp, value=None, tb=None): raise tp, value, tb ''') else: exec_ = eval('exec') def raise_(tp, value=None, tb=None): """ A function that matches the Python 2.x ``raise`` statement. This allows re-raising exceptions with the cls value and traceback on Python 2 and 3. """ if value is not None and isinstance(tp, Exception): raise TypeError("instance exception may not have a separate value") if value is not None: exc = tp(value) else: exc = tp if exc.__traceback__ is not tb: raise exc.with_traceback(tb) raise exc def decode(val): """ Decode an object assuming the encoding is UTF-8. """ try: # assume it is an encoded bytes object return val.decode('utf-8') except AttributeError: # it was an already decoded unicode object return val # ############################ generic utils ############################### # @contextmanager def stdout(fileobj): "usage: with stdout(file('out.txt', 'a')): do_something()" orig_stdout = sys.stdout sys.stdout = fileobj try: yield finally: sys.stdout = orig_stdout def write(x): "Write str(x) on stdout and flush, no newline added" sys.stdout.write(str(x)) sys.stdout.flush() def gen_val(value): "Return a generator object with a single element" yield value def gen_exc(etype, exc, tb): "Return a generator object raising an exception" raise_(etype, exc, tb) yield def less(text): "Send a text to less via a pipe" # -c clear the screen before starting less po = subprocess.Popen(['less', '-c'], stdin=subprocess.PIPE) try: po.stdin.write(text) except IOError: pass po.stdin.close() po.wait() use_less = (sys.platform != 'win32') # unices class TerminatedProcess(Exception): pass def terminatedProcess(signum, frame): raise TerminatedProcess # ########################## readline support ############################ # def read_line(stdin, prompt=''): "Read a line from stdin, using readline when possible" if isinstance(stdin, ReadlineInput): return stdin.readline(prompt) else: write(prompt) return stdin.readline() def read_long_line(stdin, terminator): """ Read multiple lines from stdin until the terminator character is found, then yield a single space-separated long line. """ while True: lines = [] while True: line = stdin.readline() # ends with \n if not line: # EOF return line = line.strip() if not line: continue elif line[-1] == terminator: lines.append(line[:-1]) break else: lines.append(line) yield ' '.join(lines) class ReadlineInput(object): """ An iterable with a .readline method reading from stdin. """ def __init__(self, completions, case_sensitive=True, histfile=None): self.completions = completions self.case_sensitive = case_sensitive self.histfile = histfile if not case_sensitive: self.completions = [c.upper() for c in completions] import readline self.rl = readline readline.parse_and_bind("tab: complete") readline.set_completer(self.complete) def __enter__(self): self.old_completer = self.rl.get_completer() try: if self.histfile: self.rl.read_history_file(self.histfile) except IOError: # the first time pass return self def __exit__(self, etype, exc, tb): self.rl.set_completer(self.old_completer) if self.histfile: self.rl.write_history_file(self.histfile) def complete(self, kw, state): # state is 0, 1, 2, ... and increases by hitting TAB if not self.case_sensitive: kw = kw.upper() try: return [k for k in self.completions if k.startswith(kw)][state] except IndexError: # no completions return # exit def readline(self, prompt=''): try: return raw_input(prompt) + '\n' except EOFError: return '' def __iter__(self): return iter(self.readline, '') # ################# help functionality in plac interpreters ################# # class HelpSummary(object): "Build the help summary consistently with the cmd module" @classmethod def add(cls, obj, specialcommands): p = plac_core.parser_from(obj) c = cmd.Cmd(stdout=cls()) c.stdout.write('\n') c.print_topics('special commands', sorted(specialcommands), 15, 80) c.print_topics('custom commands', sorted(obj.commands), 15, 80) c.print_topics('commands run in external processes', sorted(obj.mpcommands), 15, 80) c.print_topics('threaded commands', sorted(obj.thcommands), 15, 80) p.helpsummary = str(c.stdout) def __init__(self): self._ls = [] def write(self, s): self._ls.append(s) def __str__(self): return ''.join(self._ls) class PlacFormatter(argparse.RawDescriptionHelpFormatter): def _metavar_formatter(self, action, default_metavar): 'Remove special commands from the usage message' choices = action.choices or {} action.choices = dict((n, c) for n, c in choices.items() if not n.startswith('.')) return super(PlacFormatter, self)._metavar_formatter( action, default_metavar) def format_help(self): "Attached to plac_core.ArgumentParser for plac interpreters" try: return self.helpsummary except AttributeError: return super(plac_core.ArgumentParser, self).format_help() plac_core.ArgumentParser.format_help = format_help def default_help(obj, cmd=None): "The default help functionality in plac interpreters" parser = plac_core.parser_from(obj) if cmd is None: yield parser.format_help() return subp = parser.subparsers._name_parser_map.get(cmd) if subp is None: yield _('Unknown command %s' % cmd) elif getattr(obj, '_interact_', False): # in interactive mode formatter = subp._get_formatter() formatter._prog = cmd # remove the program name from the usage formatter.add_usage( subp.usage, [a for a in subp._actions if a.dest != 'help'], subp._mutually_exclusive_groups) formatter.add_text(subp.description) for action_group in subp._action_groups: formatter.start_section(action_group.title) formatter.add_text(action_group.description) formatter.add_arguments(a for a in action_group._group_actions if a.dest != 'help') formatter.end_section() yield formatter.format_help() else: # regular argparse help yield subp.format_help() # ######################## import management ############################## # try: PLACDIRS = os.environ.get('PLACPATH', '.').split(':') except: raise ValueError(_('Ill-formed PLACPATH: got %PLACPATHs') % os.environ) def partial_call(factory, arglist): "Call a container factory with the arglist and return a plac object" a = plac_core.parser_from(factory).argspec if a.defaults or a.varargs or a.varkw: raise TypeError('Interpreter.call must be invoked on ' 'factories with required arguments only') required_args = ', '.join(a.args) if required_args: required_args += ',' # trailing comma code = '''def makeobj(interact, %s *args): obj = factory(%s) obj._interact_ = interact obj._args_ = args return obj\n''' % (required_args, required_args) dic = dict(factory=factory) exec_(code, dic) makeobj = dic['makeobj'] makeobj.add_help = False if inspect.isclass(factory): makeobj.__annotations__ = getattr( factory.__init__, '__annotations__', {}) else: makeobj.__annotations__ = getattr( factory, '__annotations__', {}) makeobj.__annotations__['interact'] = ( 'start interactive interpreter', 'flag', 'i') return plac_core.call(makeobj, arglist) def import_main(path, *args): """ An utility to import the main function of a plac tool. It also works with command container factories. """ if ':' in path: # importing a factory path, factory_name = path.split(':') else: # importing the main function factory_name = None if not os.path.isabs(path): # relative path, look at PLACDIRS for placdir in PLACDIRS: fullpath = os.path.join(placdir, path) if os.path.exists(fullpath): break else: # no break raise ImportError(_('Cannot find %s' % path)) else: fullpath = path name, ext = os.path.splitext(os.path.basename(fullpath)) module = imp.load_module(name, open(fullpath), fullpath, (ext, 'U', 1)) if factory_name: tool = partial_call(getattr(module, factory_name), args) else: tool = module.main return tool # ############################ Task classes ############################# # # base class not instantiated directly class BaseTask(object): """ A task is a wrapper over a generator object with signature Task(no, arglist, genobj), attributes .no .arglist .outlist .str .etype .exc .tb .status and methods .run and .kill. """ STATES = ('SUBMITTED', 'RUNNING', 'TOBEKILLED', 'KILLED', 'FINISHED', 'ABORTED') def __init__(self, no, arglist, genobj): self.no = no self.arglist = arglist self._genobj = self._wrap(genobj) self.str, self.etype, self.exc, self.tb = '', None, None, None self.status = 'SUBMITTED' self.outlist = [] def notify(self, msg): "Notifies the underlying monitor. To be implemented" def _wrap(self, genobj, stringify_tb=False): """ Wrap the genobj into a generator managing the exceptions, populating the .outlist, setting the .status and yielding None. stringify_tb must be True if the traceback must be sent to a process. """ self.status = 'RUNNING' try: for value in genobj: if self.status == 'TOBEKILLED': # exit from the loop raise GeneratorExit if value is not None: # add output self.outlist.append(value) self.notify(decode(value)) yield except Interpreter.Exit: # wanted exit self._regular_exit() raise except (GeneratorExit, TerminatedProcess, KeyboardInterrupt): # soft termination self.status = 'KILLED' except: # unexpected exception self.etype, self.exc, tb = sys.exc_info() self.tb = ''.join(traceback.format_tb(tb)) if stringify_tb else tb self.status = 'ABORTED' else: self._regular_exit() def _regular_exit(self): self.status = 'FINISHED' try: self.str = '\n'.join(map(decode, self.outlist)) except IndexError: self.str = 'no result' def run(self): "Run the inner generator" for none in self._genobj: pass def kill(self): "Set a TOBEKILLED status" self.status = 'TOBEKILLED' def wait(self): "Wait for the task to finish: to be overridden" @property def traceback(self): "Return the traceback as a (possibly empty) string" if self.tb is None: return '' elif isinstance(self.tb, (str, bytes)): return self.tb else: return ''.join(traceback.format_tb(self.tb)) @property def result(self): self.wait() if self.exc: if isinstance(self.tb, (str, bytes)): raise self.etype(self.tb) else: raise_(self.etype, self.exc, self.tb or None) if not self.outlist: return None return self.outlist[-1] def __repr__(self): "String representation containing class name, number, arglist, status" return '<%s %d [%s] %s>' % ( self.__class__.__name__, self.no, ' '.join(self.arglist), self.status) nulltask = BaseTask(0, [], ('skip' for dummy in (1,))) # ######################## synchronous tasks ############################## # class SynTask(BaseTask): """ Synchronous task running in the interpreter loop and displaying its output as soon as available. """ def __str__(self): "Return the output string or the error message" if self.etype: # there was an error return '%s: %s' % (self.etype.__name__, self.exc) else: return '\n'.join(map(str, self.outlist)) class ThreadedTask(BaseTask): """ A task running in a separated thread. """ def __init__(self, no, arglist, genobj): BaseTask.__init__(self, no, arglist, genobj) self.thread = threading.Thread(target=super(ThreadedTask, self).run) def run(self): "Run the task into a thread" self.thread.start() def wait(self): "Block until the thread ends" self.thread.join() # ######################## multiprocessing tasks ######################### # def sharedattr(name, on_error): "Return a property to be attached to an MPTask" def get(self): try: return getattr(self.ns, name) except: # the process was killed or died hard return on_error def set(self, value): try: setattr(self.ns, name, value) except: # the process was killed or died hard pass return property(get, set) class MPTask(BaseTask): """ A task running as an external process. The current implementation only works on Unix-like systems, where multiprocessing use forks. """ str = sharedattr('str', '') etype = sharedattr('etype', None) exc = sharedattr('exc', None) tb = sharedattr('tb', None) status = sharedattr('status', 'ABORTED') @property def outlist(self): try: return self._outlist except: # the process died hard return [] def notify(self, msg): self.man.notify_listener(self.no, msg) def __init__(self, no, arglist, genobj, manager): """ The monitor has a .send method and a .man multiprocessing.Manager """ self.no = no self.arglist = arglist self._genobj = self._wrap(genobj, stringify_tb=True) self.man = manager self._outlist = manager.mp.list() self.ns = manager.mp.Namespace() self.status = 'SUBMITTED' self.etype, self.exc, self.tb = None, None, None self.str = repr(self) self.proc = multiprocessing.Process(target=super(MPTask, self).run) def run(self): "Run the task into an external process" self.proc.start() def wait(self): "Block until the external process ends or is killed" self.proc.join() def kill(self): """Kill the process with a SIGTERM inducing a TerminatedProcess exception in the children""" self.proc.terminate() # ######################## Task Manager ###################### # class TaskManager(object): """ Store the given commands into a task registry. Provides methods to manage the submitted tasks. """ cmdprefix = '.' specialcommands = set(['.last_tb']) def __init__(self, obj): self.obj = obj self.registry = {} # {taskno : task} if obj.mpcommands or obj.thcommands: self.specialcommands.update(['.kill', '.list', '.output']) interact = getattr(obj, '_interact_', False) self.parser = plac_core.parser_from( obj, prog='' if interact else None, formatter_class=PlacFormatter) HelpSummary.add(obj, self.specialcommands) self.man = Manager() if obj.mpcommands else None signal.signal(signal.SIGTERM, terminatedProcess) def close(self): "Kill all the running tasks" for task in self.registry.values(): try: if task.status == 'RUNNING': task.kill() task.wait() except: # task killed, nothing to wait pass if self.man: self.man.stop() def _get_latest(self, taskno=-1, status=None): "Get the latest submitted task from the registry" assert taskno < 0, 'You must pass a negative number' if status: tasks = [t for t in self.registry.values() if t.status == status] else: tasks = [t for t in self.registry.values()] tasks.sort(key=attrgetter('no')) if len(tasks) >= abs(taskno): return tasks[taskno] # ########################## special commands ######################## # @plac_core.annotations( taskno=('task to kill', 'positional', None, int)) def kill(self, taskno=-1): 'kill the given task (-1 to kill the latest running task)' if taskno < 0: task = self._get_latest(taskno, status='RUNNING') if task is None: yield 'Nothing to kill' return elif taskno not in self.registry: yield 'Unknown task %d' % taskno return else: task = self.registry[taskno] if task.status in ('ABORTED', 'KILLED', 'FINISHED'): yield 'Already finished %s' % task return task.kill() yield task @plac_core.annotations( status=('', 'positional', None, str, BaseTask.STATES)) def list(self, status='RUNNING'): 'list tasks with a given status' for task in self.registry.values(): if task.status == status: yield task @plac_core.annotations( taskno=('task number', 'positional', None, int)) def output(self, taskno=-1, fname=None): 'show the output of a given task (and optionally save it to a file)' if taskno < 0: task = self._get_latest(taskno) if task is None: yield 'Nothing to show' return elif taskno not in self.registry: yield 'Unknown task %d' % taskno return else: task = self.registry[taskno] outstr = '\n'.join(map(str, task.outlist)) if fname: open(fname, 'w').write(outstr) yield 'saved output of %d into %s' % (taskno, fname) return yield task if len(task.outlist) > 20 and use_less: less(outstr) # has no meaning for a plac server else: yield outstr @plac_core.annotations( taskno=('task number', 'positional', None, int)) def last_tb(self, taskno=-1): "show the traceback of a given task, if any" task = self._get_latest(taskno) if task: yield task.traceback else: yield 'Nothing to show' # ########################## SyncProcess ############################# # class Process(subprocess.Popen): "Start the interpreter specified by the params in a subprocess" def __init__(self, params): signal.signal(signal.SIGPIPE, signal.SIG_DFL) # to avoid broken pipe messages code = '''import plac, sys sys.argv[0] = '<%s>' plac.Interpreter(plac.import_main(*%s)).interact(prompt='i>\\n') ''' % (params[0], params) subprocess.Popen.__init__( self, [sys.executable, '-u', '-c', code], stdin=subprocess.PIPE, stdout=subprocess.PIPE) self.man = multiprocessing.Manager() def close(self): "Close stdin and stdout" self.stdin.close() self.stdout.close() self.man.shutdown() def recv(self): # char-by-char cannot work "Return the output of the subprocess, line-by-line until the prompt" lines = [] while True: lines.append(self.stdout.readline()) if lines[-1] == 'i>\n': out = ''.join(lines) return out[:-1] + ' ' # remove last newline def send(self, line): """Send a line (adding a newline) to the underlying subprocess and wait for the answer""" self.stdin.write(line + os.linesep) return self.recv() class StartStopObject(object): started = False def start(self): pass def stop(self): pass class Monitor(StartStopObject): """ Base monitor class with methods add_listener/del_listener/notify_listener read_queue and and start/stop. """ def __init__(self, name, queue=None): self.name = name self.queue = queue or multiprocessing.Queue() def add_listener(self, taskno): pass def del_listener(self, taskno): pass def notify_listener(self, taskno, msg): pass def start(self): pass def stop(self): pass def read_queue(self): pass class Manager(StartStopObject): """ The plac Manager contains a multiprocessing.Manager and a set of slave monitor processes to which we can send commands. There is a manager for each interpreter with mpcommands. """ def __init__(self): self.registry = {} self.started = False self.mp = None def add(self, monitor): 'Add or replace a monitor in the registry' proc = multiprocessing.Process(None, monitor.start, monitor.name) proc.queue = monitor.queue self.registry[monitor.name] = proc def delete(self, name): 'Remove a named monitor from the registry' del self.registry[name] # can be called more than once def start(self): if self.mp is None: self.mp = multiprocessing.Manager() for monitor in self.registry.values(): monitor.start() self.started = True def stop(self): for monitor in self.registry.values(): monitor.queue.close() monitor.terminate() if self.mp: self.mp.shutdown() self.mp = None self.started = False def notify_listener(self, taskno, msg): for monitor in self.registry.values(): monitor.queue.put(('notify_listener', taskno, msg)) def add_listener(self, no): for monitor in self.registry.values(): monitor.queue.put(('add_listener', no)) # ######################### plac server ############################# # import asyncore import asynchat import socket class _AsynHandler(asynchat.async_chat): "asynchat handler starting a new interpreter loop for each connection" terminator = '\r\n' # the standard one for telnet prompt = 'i> ' def __init__(self, socket, interpreter): asynchat.async_chat.__init__(self, socket) self.set_terminator(self.terminator) self.i = interpreter self.i.__enter__() self.data = [] self.write(self.prompt) def write(self, data, *args): "Push a string back to the client" if args: data %= args if data.endswith('\n') and not data.endswith(self.terminator): data = data[:-1] + self.terminator # fix newlines self.push(data) def collect_incoming_data(self, data): "Collect one character at the time" self.data.append(data) def found_terminator(self): "Put in the queue the line received from the client" line = ''.join(self.data) self.log('Received line %r from %s' % (line, self.addr)) if line == 'EOF': self.i.__exit__(None, None, None) self.handle_close() else: task = self.i.submit(line) task.run() # synchronous or not if task.etype: # manage exception error = '%s: %s\nReceived: %s' % ( task.etype.__name__, task.exc, ' '.join(task.arglist)) self.log_info(task.traceback + error) # on the server self.write(error + self.terminator) # back to the client else: # no exception self.write(task.str + self.terminator) self.data = [] self.write(self.prompt) class _AsynServer(asyncore.dispatcher): "asyncore-based server spawning AsynHandlers" def __init__(self, interpreter, newhandler, port, listen=5): self.interpreter = interpreter self.newhandler = newhandler self.port = port asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.bind(('', port)) self.listen(listen) def handle_accept(self): clientsock, clientaddr = self.accept() self.log('Connected from %s' % str(clientaddr)) i = self.interpreter.__class__(self.interpreter.obj) # new interpreter self.newhandler(clientsock, i) # spawn a new handler # ########################## the Interpreter ############################ # class Interpreter(object): """ A context manager with a .send method and a few utility methods: execute, test and doctest. """ class Exit(Exception): pass def __init__(self, obj, commentchar='#', split=shlex.split): self.obj = obj try: self.name = obj.__module__ except AttributeError: self.name = 'plac' self.commentchar = commentchar self.split = split self._set_commands(obj) self.tm = TaskManager(obj) self.man = self.tm.man self.parser = self.tm.parser if self.commands: self.parser.addsubcommands( self.tm.specialcommands, self.tm, title='special commands') if obj.mpcommands: self.parser.addsubcommands( obj.mpcommands, obj, title='commands run in external processes') if obj.thcommands: self.parser.addsubcommands( obj.thcommands, obj, title='threaded commands') self.parser.error = lambda msg: sys.exit(msg) # patch the parser self._interpreter = None def _set_commands(self, obj): "Make sure obj has the right command attributes as Python sets" for attrname in ('commands', 'mpcommands', 'thcommands'): setattr(self, attrname, set(getattr(self.__class__, attrname, []))) setattr(obj, attrname, set(getattr(obj, attrname, []))) self.commands = obj.commands self.mpcommands.update(obj.mpcommands) self.thcommands.update(obj.thcommands) if (obj.commands or obj.mpcommands or obj.thcommands) and \ not hasattr(obj, 'help'): # add default help obj.help = default_help.__get__(obj, obj.__class__) self.commands.add('help') def __enter__(self): "Start the inner interpreter loop" self._interpreter = self._make_interpreter() self._interpreter.send(None) return self def __exit__(self, exctype, exc, tb): "Close the inner interpreter and the task manager" self.close(exctype, exc, tb) def submit(self, line): "Send a line to the underlying interpreter and return a task object" if self._interpreter is None: raise RuntimeError(_('%r not initialized: probably you forgot to ' 'use the with statement') % self) if isinstance(line, (str, bytes)): arglist = self.split(line, self.commentchar) else: # expects a list of strings arglist = line if not arglist: return nulltask m = self.tm.man # manager if m and not m.started: m.start() task = self._interpreter.send(arglist) # nonblocking if not plac_core._match_cmd(arglist[0], self.tm.specialcommands): self.tm.registry[task.no] = task if m: m.add_listener(task.no) return task def send(self, line): """Send a line to the underlying interpreter and return the finished task""" task = self.submit(line) BaseTask.run(task) # blocking return task def tasks(self): "The full lists of the submitted tasks" return self.tm.registry.values() def close(self, exctype=None, exc=None, tb=None): "Can be called to close the interpreter prematurely" self.tm.close() if exctype is not None: self._interpreter.throw(exctype, exc, tb) else: self._interpreter.close() def _make_interpreter(self): "The interpreter main loop, from lists of arguments to task objects" enter = getattr(self.obj, '__enter__', lambda: None) exit = getattr(self.obj, '__exit__', lambda et, ex, tb: None) enter() task = None try: for no in itertools.count(1): arglist = yield task try: cmd, result = self.parser.consume(arglist) except SystemExit as e: # for invalid commands if e.args == (0,): # raised as sys.exit(0) errlist = [] else: errlist = [str(e)] task = SynTask(no, arglist, iter(errlist)) continue except: # anything else task = SynTask(no, arglist, gen_exc(*sys.exc_info())) continue if not plac_core.iterable(result): # atomic result task = SynTask(no, arglist, gen_val(result)) elif cmd in self.obj.mpcommands: task = MPTask(no, arglist, result, self.tm.man) elif cmd in self.obj.thcommands: task = ThreadedTask(no, arglist, result) else: # blocking task task = SynTask(no, arglist, result) except GeneratorExit: # regular exit exit(None, None, None) except: # exceptional exit exit(*sys.exc_info()) raise def check(self, given_input, expected_output): "Make sure you get the expected_output from the given_input" output = self.send(given_input).str # blocking ok = (output == expected_output) if not ok: # the message here is not internationalized on purpose msg = 'input: %s\noutput: %s\nexpected: %s' % ( given_input, output, expected_output) raise AssertionError(msg) def _parse_doctest(self, lineiter): "Returns the lines of input, the lines of output, and the line number" lines = [line.strip() for line in lineiter] inputs = [] positions = [] for i, line in enumerate(lines): if line.startswith('i> '): inputs.append(line[3:]) positions.append(i) positions.append(len(lines) + 1) # last position outputs = [] for i, start in enumerate(positions[:-1]): end = positions[i + 1] outputs.append('\n'.join(lines[start+1:end])) return zip(inputs, outputs, positions) def doctest(self, lineiter, verbose=False): """ Parse a text containing doctests in a context and tests of all them. Raise an error even if a single doctest if broken. Use this for sequential tests which are logically grouped. """ with self: try: for input, output, no in self._parse_doctest(lineiter): if verbose: write('i> %s\n' % input) write('-> %s\n' % output) task = self.send(input) # blocking if not str(task) == output: msg = ('line %d: input: %s\noutput: %s\nexpected: %s\n' % (no + 1, input, task, output)) write(msg) if task.exc: raise_(task.etype, task.exc, task.tb) except self.Exit: pass def execute(self, lineiter, verbose=False): "Execute a lineiter of commands in a context and print the output" with self: try: for line in lineiter: if verbose: write('i> ' + line) task = self.send(line) # finished task if task.etype: # there was an error raise_(task.etype, task.exc, task.tb) write('%s\n' % task.str) except self.Exit: pass def multiline(self, stdin=sys.stdin, terminator=';', verbose=False): "The multiline mode is especially suited for usage with emacs" with self: try: for line in read_long_line(stdin, terminator): task = self.submit(line) task.run() write('%s\n' % task.str) if verbose and task.traceback: write(task.traceback) except self.Exit: pass def interact(self, stdin=sys.stdin, prompt='i> ', verbose=False): "Starts an interactive command loop reading commands from the consolle" try: import readline readline_present = True except ImportError: readline_present = False if stdin is sys.stdin and readline_present: # use readline histfile = os.path.expanduser('~/.%s.history' % self.name) completions = list(self.commands) + list(self.mpcommands) + \ list(self.thcommands) + list(self.tm.specialcommands) self.stdin = ReadlineInput(completions, histfile=histfile) else: self.stdin = stdin self.prompt = prompt self.verbose = verbose intro = self.obj.__doc__ or '' write(intro + '\n') with self: self.obj._interact_ = True if self.stdin is sys.stdin: # do not close stdin automatically self._manage_input() else: with self.stdin: # close stdin automatically self._manage_input() def _manage_input(self): "Convert input lines into task which are then executed" try: for line in iter(lambda: read_line(self.stdin, self.prompt), ''): line = line.strip() if not line: continue task = self.submit(line) task.run() # synchronous or not write(str(task) + '\n') if self.verbose and task.etype: write(task.traceback) except self.Exit: pass def start_server(self, port=2199, **kw): """Starts an asyncore server reading commands for clients and opening a new interpreter for each connection.""" _AsynServer(self, _AsynHandler, port) # register the server try: asyncore.loop(**kw) except (KeyboardInterrupt, TerminatedProcess): pass finally: asyncore.close_all() def add_monitor(self, mon): self.man.add(mon) def del_monitor(self, name): self.man.delete(name) @classmethod def call(cls, factory, arglist=sys.argv[1:], commentchar='#', split=shlex.split, stdin=sys.stdin, prompt='i> ', verbose=False): """ Call a container factory with the arglist and instantiate an interpreter object. If there are remaining arguments, send them to the interpreter, else start an interactive session. """ obj = partial_call(factory, arglist) i = cls(obj, commentchar, split) if i.obj._args_: with i: task = i.send(i.obj._args_) # synchronous if task.exc: raise_(task.etype, task.exc, task.tb) out = str(task) if out: print(out) elif i.obj._interact_: i.interact(stdin, prompt, verbose) else: i.parser.print_usage() # ################################## runp ################################### # class _TaskLauncher(object): "Helper for runp" def __init__(self, genseq, mode): if mode == 'p': self.mpcommands = ['rungen'] else: self.thcommands = ['rungen'] self.genlist = list(genseq) def rungen(self, i): for out in self.genlist[int(i) - 1]: yield out def runp(genseq, mode='p'): """Run a sequence of generators in parallel. Mode can be 'p' (use processes) or 't' (use threads). After all of them are finished, return a list of task objects. """ assert mode in 'pt', mode launcher = _TaskLauncher(genseq, mode) res = [] with Interpreter(launcher) as inter: for i in range(len(launcher.genlist)): inter.submit('rungen %d' % (i + 1)).run() for task in inter.tasks(): try: res.append(task.result) except Exception as e: res.append(e) return res