diff options
| author | blob8108 <blob8108@gmail.com> | 2014-01-05 10:25:59 +0000 |
|---|---|---|
| committer | blob8108 <blob8108@gmail.com> | 2014-01-05 13:18:55 +0000 |
| commit | 6ae298c8904ff03d014bcf70a26f0dedca949d3b (patch) | |
| tree | 8bc0a0053fb9d8a408331c083b981a00933c6d80 | |
| parent | bc7af7204eae2a026a67d2ab9b3cc4e36f23382b (diff) | |
| download | blockext-6ae298c8904ff03d014bcf70a26f0dedca949d3b.tar.gz blockext-6ae298c8904ff03d014bcf70a26f0dedca949d3b.zip | |
Initial
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | blockext/__init__.py | 288 | ||||
| -rw-r--r-- | blockext/scratch.py | 72 | ||||
| -rw-r--r-- | blockext/snap.py | 88 | ||||
| -rw-r--r-- | example.py | 111 |
5 files changed, 531 insertions, 29 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/blockext/__init__.py b/blockext/__init__.py new file mode 100644 index 0000000..74bd7f8 --- /dev/null +++ b/blockext/__init__.py @@ -0,0 +1,288 @@ +from __future__ import unicode_literals + +from cgi import escape +import inspect +import itertools +import re +import threading +import time +try: + from urllib import unquote as _unquote_str + def unquote(part): + part = str(part) + return _unquote_str(part).decode("utf-8") +except ImportError: + from urllib.parse import unquote +try: + from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler + from SocketServer import ThreadingMixIn +except ImportError: + from http.server import HTTPServer, BaseHTTPRequestHandler + from socketserver import ThreadingMixIn + +# TODO rewrite entire thing for package manager. +# TODO scratch blocking commands +# TODO error handling + + + +# TODO fix this +try: + unicode +except NameError: + unicode = str + + + +class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): + """Handle requests in a separate thread.""" + + +class Blockext(BaseHTTPRequestHandler): + # HTTPServer makes one instance of this for each request. + + blocks = {} + handlers = {} + menus = {} + name = "A blockext extension" + port = 8080 + + def log_message(self, format, *args): + if isinstance(args[0], str) and args[0].startswith("GET /poll"): + return + return BaseHTTPRequestHandler.log_message(self, format, *args) + + def do_GET(self): + is_browser = "text/html" in self.headers.get("Accept", "") + mime_type = "text/plain" + + path = self.path.split("/")[1:] + path = [unquote(p) for p in path] + name = path[0] + args = path[1:] + + func = self.handlers.get(name, self.blocks.get(name, None)) + if func: + if isinstance(func, Block): + response = func(*args) + mime_type = "text/plain" + elif "is_browser" in inspect.getargspec(func).args: + (mime_type, response) = func(is_browser=is_browser, *args) + else: + (mime_type, response) = func(*args) + status = 200 + else: + response = "ERROR: Not found" + status = 404 + + if isinstance(response, bytes): + response = response.decode("utf-8") + else: + response = unicode(response) + + if is_browser and mime_type == "text/plain": + # Some browsers seem to hate plain text. + response = u"""<!DOCTYPE html> + <meta charset="utf8"> + <title>{title}</title> + <pre>{response}</pre> + """.format(title=name, response=escape(response)) + mime_type = "text/html" + + response = response.encode("utf-8") + self.send_response(status) + self.send_header("Content-Length", str(len(response))) + self.send_header("Content-Type", mime_type) + if mime_type == "application/octet-stream": + self.send_header("Content-Disposition", "attachment") + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + + self.wfile.write(response) + + @classmethod + def register(cls, name, block): + cls.blocks[name] = block + + @classmethod + def register_handler(cls, name, func, hidden=False, display=None): + cls.handlers[name] = func + func.is_hidden = hidden + func.display = display + + +def handler(name, **kwargs): + def wrapper(func): + Blockext.register_handler(name, func, **kwargs) + return func + return wrapper + + +def menu(name, values): + Blockext.menus[name] = list(map(unicode, values)) + + +@handler("", hidden=True) +def index(is_browser=False): + if is_browser: + html = """<!DOCTYPE html> + <meta charset="utf8"> + <title>blockext</title> + + <style> + body { font-family: sans-serif; } + a { color: #20e; text-decoration: none; } + a:hover { text-decoration: underline; } + </style> + """ + + html += "<h1>{name}</h1>".format(name=escape(Blockext.name)) + + html += "<ul>" + for name in sorted(Blockext.handlers): + func = Blockext.handlers[name] + if func.is_hidden: continue + html += '<li><a href="/{path}">{display}</a>'.format( + path=escape(name), + display=escape(func.display or name), + ) + html += "</ul>" + + html += "<h2>Blocks</h2>" + html += "<ul>" + for name in sorted(Blockext.blocks): + block = Blockext.blocks[name] + if block.is_hidden: continue + html += '<li><a href="/{path}">{text}</a>'.format( + path=escape("/".join(map(unicode, + [name] + block.defaults))), + text=escape(unicode(block)), + ) + html += "</ul>" + + return ("text/html", html) + else: + blocks = [n for (n, b) in Blockext.blocks.items() if not b.is_hidden] + return ("text/plain", "\n".join(blocks)) + + +class Block(object): + INPUT_RE = re.compile(r'(%.(?:\.[A-z]+)?)') + + SHAPE_FMTS = { + "predicate": "<%s>", + "reporter": "(%s)", + } + + INPUT_DEFAULTS = { + "n": 0, + "d": 0, + "b": False, + } + + INPUT_FMTS = { + "n": "(%s)", + "s": "[%s]", + "m": "[%s \u25be]", + "d": "(%s )", + "b": "<%s>", + } + + def __init__(self, text, shape, func, blocking=False, hidden=False): + self.text = text + self.shape = shape + self.func = func + self.is_blocking = blocking + self.is_hidden = hidden + + self.arg_shapes = [] + for part in Block.INPUT_RE.split(self.text): + if part.startswith("%") and part != "%%": + assert part[1] in "nbsmd" + if part[1] in "md": + assert "." in part + self.arg_shapes.append(part[1:]) + + defaults = list(inspect.getargspec(func).defaults or []) + padding = len(self.arg_shapes) - len(defaults) + defaults = [None] * padding + defaults + shape_defaults = map(Block.INPUT_DEFAULTS.get, self.arg_shapes) + self.defaults = [a or b for (a, b) in zip(defaults, shape_defaults)] + + def __repr__(self): + return "<Block(%r)>" % self.text + + def __str__(self): + r = "" + defaults = list(self.defaults) + for part in Block.INPUT_RE.split(self.text): + if part.startswith("%") and part != "%%": + shape = part[1] + fmt = Block.INPUT_FMTS.get(shape, "%s") + value = defaults.pop(0) + part = fmt % unicode(value or " ") + r += part + fmt = Block.SHAPE_FMTS.get(self.shape, "%s") + return fmt % r + + @staticmethod + def convert_arg(arg, type_): + shape = type_[0] + if shape == "n": + try: + arg = int(arg) + except ValueError: + try: + arg = float(arg) + except ValueError: + arg = 0 + elif shape == "b": + arg = True if arg == "true" else False if arg == "false" else None + elif shape == "m": + menu_name = arg[2:] + return arg + + def __call__(self, *args): + args = [Block.convert_arg(a, t) for (a, t) + in zip(args, self.arg_shapes)] + result = self.func(*args) + result = ("true" if result is True else + "false" if result is False else + "" if result == None else result) + return result + + + +def _shape(shape): + def decorator(text, **kwargs): + def wrapper(func): + selector = func.__name__ + block = Block(text, shape, func, **kwargs) + Blockext.register(selector, block) + return block + return wrapper + return decorator + + +command = _shape("command") +reporter = _shape("reporter") +predicate = _shape("predicate") + + +def run(name="", port=8080): + blocking_reporters = [b.text for b in Blockext.blocks.values() + if b.shape == "reporter" and b.is_blocking] + if blocking_reporters: + print("WARNING: Scratch 2.0 doesn't support blocking reporters yet.") + print("Affects: " + "\n ".join(blocking_reporters)) + print("") + + Blockext.name = name + Blockext.port = port + server = ThreadedHTTPServer(('localhost', port), Blockext) + print('Listening on {}'.format(port)) + server.serve_forever() + + +import blockext.scratch +import blockext.snap diff --git a/blockext/scratch.py b/blockext/scratch.py new file mode 100644 index 0000000..b633a5d --- /dev/null +++ b/blockext/scratch.py @@ -0,0 +1,72 @@ +import json + +from blockext import * + + + +CROSSDOMAIN_XML = """ +<?xml version="1.0"?> +<cross-domain-policy> + <allow-access-from domain="*" to-ports="*"/> +</cross-domain-policy> +""" + +@handler("crossdomain.xml", hidden=True) +def crossdomain(is_browser=False): + return ("application/xml", CROSSDOMAIN_XML) + +BLOCK_SHAPES = { + "command": " ", + "reporter": "r", + "predicate": "b", +} + +@handler("scratch_extension.s2e", display="Download Scratch 2.0 extension") +def generate_s2e(): + extension = { + "extensionName": Blockext.name, + "extensionPort": Blockext.port, + "blockSpecs": [], + "menus": Blockext.menus, + } + for name, block in Blockext.blocks.items(): + if block.is_hidden: continue + shape = BLOCK_SHAPES[block.shape] + if block.shape == "command" and block.is_blocking: + shape = "w" + blockspec = [shape, block.text, name] + block.defaults + extension["blockSpecs"].append(blockspec) + return ("application/octet-stream", json.dumps(extension)) + + +def menu_permutations(arg_shapes): + if not arg_shapes: + yield [] + return + input_selector = arg_shapes[0] + menu_name = input_selector[2:] + options = Blockext.menus[menu_name] + for rest in menu_permutations(arg_shapes[1:]): + for value in options: + yield [value] + rest + + +@handler("poll", hidden=True) +def poll(is_browser=False): + lines = "" + for name, block in Blockext.blocks.items(): + if block.is_blocking: continue + if block.shape not in ("reporter", "predicate"): + continue + if all(shape[0] in "md" for shape in block.arg_shapes): + for args in menu_permutations(block.arg_shapes): + lines += "{path} {result}\n".format( + path="/".join([name] + args), + result=block(*args) + ) + return ("text/plain", lines) + +@reporter("", hidden=True) +def _problem(): + return "The Scratch Sensor board is not connected.\xe2\x80\xa8 Foo." + diff --git a/blockext/snap.py b/blockext/snap.py new file mode 100644 index 0000000..8230356 --- /dev/null +++ b/blockext/snap.py @@ -0,0 +1,88 @@ +from xml.etree import ElementTree +from xml.etree.ElementTree import Element, SubElement + +from blockext import * + + + +INPUT_SELECTORS = { + "n": "n", + "s": "s", + "b": "b", + "m": "txt", + "d": "n", +} + +def pretty(stuff): + import xml.dom.minidom + xml = xml.dom.minidom.parseString(ElementTree.tostring(stuff)) + return xml.toprettyxml() + +@handler("snap_extension.xml", display="Download Snap! blocks") +def generate_s2e(): + root = Element("blocks", { + "app": "Snap! 4.0, http://snap.berkeley.edu", + "version": "1", + }) + for name, block in Blockext.blocks.items(): + if block.is_hidden: continue + + defn = SubElement(root, "block-definition", { + "type": block.shape, + "category": "other", + }) + SubElement(defn, "header") + SubElement(defn, "code") + inputs = SubElement(defn, "inputs") + + input_names = inspect.getargspec(block.func).args + defaults = list(block.defaults) + selector = "" + for part in Block.INPUT_RE.split(block.text): + if part.startswith("%") and part != "%%": + shape = part[1] + input_ = SubElement(inputs, "input", { + "type": "%{shape}".format(shape=INPUT_SELECTORS[shape]), + "readonly": "true" if shape == "m" else "", + }) + default = (defaults.pop(0) if defaults else "") or "" + input_.text = unicode(default) + if shape in "md": + options = SubElement(input_, "options") + menu_name = part[3:] + options.text = "\n".join(Blockext.menus[menu_name]) + # TODO menus + + input_name = input_names.pop(0) if input_names else "" + part = "%'{name}'".format(name=input_name) + selector += part + defn.attrib["s"] = selector + + http_block = Element("block", s="reportURL") + join_block = SubElement(http_block, "block", s="reportJoinWords") + list_ = SubElement(join_block, "list") + url = "localhost:{port}/{name}".format(port=Blockext.port, name=name) + SubElement(list_, "l").text = url + input_names = inspect.getargspec(block.func).args + for name in input_names: + SubElement(list_, "l").text = "/" + encode = SubElement(list_, "block", s="reportTextFunction") + l = SubElement(encode, "l") + SubElement(l, "option").text = "encode URI component" + SubElement(encode, "block", var=name) + + script = SubElement(defn, "script") + if block.shape == "command": + selector = "doRun" if block.is_blocking else "fork" + run = SubElement(script, "block", s=selector) + ring = SubElement(run, "block", s="reifyReporter") + lambda_ = SubElement(ring, "autolambda") + lambda_.append(http_block) + else: # reporter, predicate + SubElement(script, "block", s="doReport").append(http_block) + + # It's useful to change this to "application/xml" while debugging. + return ("application/octet-stream", ElementTree.tostring(root)) + + + @@ -1,43 +1,96 @@ -""" -blockext spec -for tim -because he's too good for a doc file - -Example code: -""" import blockext +from blockext import * + + + +@predicate("not %b") +def not_(value): + return not value + +@command("say %s for %n secs") +def say_for_secs(text="Hello", duration=5): + import time + print(text) + time.sleep(duration) + + +@command("play note %n", blocking=True) +def play_note(note): + print("DING {note}".format(note=note)) + time.sleep(2) + +menu("pizza", ["tomato", "cheese", "hawaii"]) + +@reporter("colour of %m.pizza flavour pizza") +def pizza_colour(pizza="tomato"): + return { + "tomato": "red", + "cheese": "yellow", + "hawaii": "orange", + }[pizza] -blockext.PORT = 1234 +@reporter("id %s") +def id(text): + return text -@blockext.predicate("/tf") + + +""" +@predicate("true or false") def tf(): - blockspec = "true or false" - return random.choice(True, False) + import random + return random.choice((True, False)) + +@reporter("numify %b") +def numify(value): + return int(not value) + +@command("set light to %b") +def set_light(boolean_value): + print("light is {}".format("on" if boolean_value else "off")) +menu("city", ["Boston", "Bournemouth", "Barcelona", "Belgium"]) -@blockext.reporter("/weather", blocking=True) -def weather(city): - blockspec = "current weather in %s" - return weatherIn(city) +@reporter("lookup weather in %m.city", blocking=True) +def weather(city="Boston"): + import time + return "{weather} in {city}".format(city=city, + weather=["sunny", "cloudy", "rainy", "snowy"][int(time.time() % 4)]) -@blockext.reporter("/light") +menu("motor", [1, 2, 3, "A", "B", "C"]) + +@command("move motor %d.motor by %n degrees", blocking=True) +def move_motor(motor="A", angle=4): + print "move {motor} by {angle} degrees!".format(motor=motor, angle=angle) + time.sleep(1) + print "..." + +@reporter("light sensor value") def light(): - blockspec = "light sensor value" - return lightsensor.get() + import random + return random.randint(0, 100) -@blockext.stack("/move") -def move(degrees): - blockspec = "move %n degrees" - move(degrees) +@command("drive %n steps") +def move(steps): + print("." * steps) -blockext.generate_extension(blockext.SCRATCH, "extension.json") -blockext.generate_extension(blockext.SNAP, "extension.xml") -if __name__ == "__main__": - blockext.run() + [" ", "beep", "playBeep"], + [" ", "set beep volume to %n", "setVolume", 5], + ["r", "beep volume", "volume"], + [" ", "order %m.pizzaFlavour pizza for %m.deliverOrCollect", + "orderPizza"], + ["r", "colour of %m.pizzaFlavour pizza", "pizzaColour"], + ["r", "random upto %n", "randomThingy"] + ], + "menus": { + "pizzaFlavour": ["tomato", "cheese", "mozarella", "hawaiian"], + "deliverOrCollect": ["delivery", "collection"] + } + } +""" -# I suppose a launcher could do that on demand +if __name__ == "__main__": + blockext.run("Fancy Spaceship", 1234) -# Or run() could do it for you. Really I wanted you to think about *why* you're -# doing stuff. |
