summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorblob8108 <blob8108@gmail.com>2014-01-05 10:25:59 +0000
committerblob8108 <blob8108@gmail.com>2014-01-05 13:18:55 +0000
commit6ae298c8904ff03d014bcf70a26f0dedca949d3b (patch)
tree8bc0a0053fb9d8a408331c083b981a00933c6d80
parentbc7af7204eae2a026a67d2ab9b3cc4e36f23382b (diff)
downloadblockext-6ae298c8904ff03d014bcf70a26f0dedca949d3b.tar.gz
blockext-6ae298c8904ff03d014bcf70a26f0dedca949d3b.zip
Initial
-rw-r--r--.gitignore1
-rw-r--r--blockext/__init__.py288
-rw-r--r--blockext/scratch.py72
-rw-r--r--blockext/snap.py88
-rw-r--r--example.py111
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))
+
+
+
diff --git a/example.py b/example.py
index c717ef2..07838cf 100644
--- a/example.py
+++ b/example.py
@@ -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.