diff --git a/webpython/Dockerfile b/webpython/Dockerfile index 54f1b014..76eae9dc 100644 --- a/webpython/Dockerfile +++ b/webpython/Dockerfile @@ -2,9 +2,10 @@ FROM ubuntu:14.04 MAINTAINER "Martin v. Löwis" RUN locale-gen en_US.UTF-8 ENV LANG en_US.UTF-8 +ADD assess.py /usr/lib/python3.4/assess.py ADD webpython.py /usr/lib/python3.4/webpython.py RUN rm /usr/lib/python3.4/turtle.py ADD turtle.py /usr/lib/python3.4/turtle.py RUN adduser --disabled-password --gecos Python python USER python -WORKDIR /home/python +WORKDIR /usr/lib/python3.4 diff --git a/webpython/assess.py b/webpython/assess.py new file mode 100644 index 00000000..b14a13b9 --- /dev/null +++ b/webpython/assess.py @@ -0,0 +1,234 @@ +import webpython, contextlib, io, json, sys, turtle, ast +from webpython import shell + +turtle_operations = [] +bindings = {} + +class RecordingPen: + _pen = None + _screen = None + def __init__(self): + self.operations = turtle_operations + self._pos = (0,0) + turtle_operations.append(('__init__',())) + + def reset(self): + turtle_operations.clear() + + def onclick(self, fun, btn=1, add=None): + self.operations.append(('onclick', (fun,))) + def eventfun(event): + fun(event.x, event.y) + bindings[''] = eventfun + + def goto(self, x, y): + self._pos = (x,y) + self.operations.append(('goto', (x,y))) + + def pos(self): + self.operations.append(('pos', ())) + return self._pos + + def __getattr__(self, method): + def func(*args): + self.operations.append((method, args)) + return func + +class FakeCanvas(turtle.WebCanvas): + def flushbatch(self): + pass + + def get_width(self): + return 400 + + def get_height(self): + return 400 + + def delete(self, item): + pass + + def css(self, key, value): + pass + +fake_events = [] +def mainloop(): + while fake_events: + e = turtle.Event(fake_events.pop(0)) + if e.type in bindings: + bindings[e.type](e) + +turtle.Turtle = RecordingPen +turtle.WebCanvas = FakeCanvas +pen = turtle._getpen() +turtle.mainloop = mainloop + +def filter_operations(name): + return [o for o in turtle_operations if o[0] == name] + +@contextlib.contextmanager +def capture(): + global captured_out + import sys + oldout,olderr = sys.stdout, sys.stderr + try: + out=[io.StringIO(), io.StringIO()] + captured_out = out + sys.stdout,sys.stderr = out + yield out + finally: + sys.stdout,sys.stderr = oldout, olderr + out[0] = out[0].getvalue() + out[1] = out[1].getvalue() + +def get_source(): + message = json.loads(sys.argv[1]) + return message['data'] + +def get_ast(): + s = get_source() + return ast.parse(s, "programm.py", "exec") + +def has_bare_except(): + for node in ast.walk(get_ast()): + if isinstance(node, ast.ExceptHandler): + if node.type is None: + return True + return False + +def runcaptured(prefix='', tracing=None, variables=None, source=''): + #message = json.loads(sys.argv[1]) + #source = prefix + message['data'] + with open("programm.py", "w", encoding='utf-8') as f: + f.write(source) + c = compile(source, "programm.py", 'exec') + with capture() as out, trace(tracing): + if variables is None: + variables = {} + exec(c, variables) + return source, out[0], out[1], variables + +def runfunc(func, *args, tracing=None): + with capture() as out, trace(tracing): + res = func(*args) + return out[0], out[1], res + +def passed(): + msg_in = json.loads(sys.argv[1]) + msg_out = {'cmd':'passed'} + msg_out['lis_outcome_service_url'] = msg_in['lis_outcome_service_url'] + msg_out['lis_result_sourcedid'] = msg_in['lis_result_sourcedid'] + webpython.shell.sendpickle(msg_out) + +def failed(msg): + msg_in = json.loads(sys.argv[1]) + msg_out = {'cmd':'failed', 'data':'Dein Programm ist leider falsch:\n'+msg} + msg_out['lis_outcome_service_url'] = msg_in['lis_outcome_service_url'] + msg_out['lis_result_sourcedid'] = msg_in['lis_result_sourcedid'] + webpython.shell.sendpickle(msg_out) + +def modified(variables, name, val): + if variables.get(name) != val: + msg_in = json.loads(sys.argv[1]) + msg_out = {'cmd':'failed', + 'data':('Bitte lösche Deine Zuweisung der Variable %s, '+ + 'damit wir Dein Programm überprüfen können.') % name} + msg_out['lis_outcome_service_url'] = msg_in['lis_outcome_service_url'] + msg_out['lis_result_sourcedid'] = msg_in['lis_result_sourcedid'] + webpython.shell.sendpickle(msg_out) + return True + return False + +undefined = object() +def getvar(variables, name): + try: + return variables['name'] + except KeyError: + name = name.lower() + for k,v in variables.items(): + if k.lower() == name: + return v + return undefined + +def _match(n1, n2): + if n1 == n2: + return True + if n1 is None or n2 is None: + return False + return n1.lower() == n2.lower() + +class Call: + def __init__(self, name, args): + self.name = name + self.args = args + self.calls = [] + self.current = None + + def findcall(self, f): + if _match(self.name, f): + return self + for c in self.calls: + r = c.findcall(f) + if r: + return r + return None + + def calling(self, caller, callee): + if _match(self.name, caller): + for c in self.calls: + if _match(c.name, callee): + return True + for c in self.calls: + if c.calling(caller, callee): + return True + return False + + def countcalls(self, caller, callee): + calls = 0 + if _match(self.name, caller): + for c in self.calls: + if _match(c.name, callee): + calls += 1 + return calls + for c in self.calls: + r = c.countcalls(caller, callee) + if r > 0: + return r + return 0 + +class Tracing(Call): + def __init__(self): + Call.__init__(self, None, None) + + def trace(self, frame, event, arg): + if event == 'call': + c = Call(frame.f_code.co_name, frame.f_locals.copy()) + cur = self + while cur.current: + cur = cur.current + cur.calls.append(c) + cur.current = c + return self.trace + elif event in ('return', 'exception'): + cur = self + if not cur.current: + # XXX return without call? happens when invocation of top function fails + return + while cur.current.current: + cur = cur.current + cur.current = None + + def start(self): + sys.settrace(self.trace) + + def stop(self): + sys.settrace(None) + +@contextlib.contextmanager +def trace(t): + try: + if t: + t.start() + yield + finally: + if t: + t.stop() diff --git a/webpython/webpython.py b/webpython/webpython.py index 350f3755..b81ac680 100644 --- a/webpython/webpython.py +++ b/webpython/webpython.py @@ -4,6 +4,7 @@ import io, select, sys, os, threading, code import pickle, struct, builtins, json #, ressource from queue import Queue +from argparse import ArgumentParser # hard limit to 64M #try: @@ -163,12 +164,15 @@ builtins.input = shell.input #iothread.start() if __name__ == '__main__': - #script = "print (\"Hello world.\")\n\rmsg = input()\n\rprint(\"Out:\")\n\rprint(msg)" - #with open("programm.py", "w", encoding='utf-8') as f: - # f.write(script) - with open(os.path.join("/", "workspace", "exercise.py"), "r", encoding='utf-8') as f: + parser = ArgumentParser(description='A python interpreter that generates json commands based on the standard I/O streams.') + parser.add_argument('-f', '--filename', type=str, required=True, default='exercise.py', help='Python file to be interpreted.') + args = parser.parse_args() + + filepath = os.path.join("/", "workspace", args.filename) + with open(filepath, "r", encoding='utf-8') as f: script = f.read() - c = compile(script, "exercise.py", 'exec') + c = compile(script, args.filename, 'exec') exec(c, {}) + # work-around for docker not terminating properly shell.sendpickle({'cmd':'exit'})