#Copyright (c) 2013 Marion Zepf #Permission is hereby granted, free of charge, to any person obtaining a copy #of this software and associated documentation files (the "Software"), to deal #in the Software without restriction, including without limitation the rights #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #copies of the Software, and to permit persons to whom the Software is #furnished to do so, subject to the following conditions: #The above copyright notice and this permission notice shall be included in #all copies or substantial portions of the Software. #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN #THE SOFTWARE. """ Python export tool """ import ast from gettext import gettext as _ from os import linesep import re import traceback import util.codegen as codegen #from ast_pprint import * # only used for debugging, safe to comment out from talogo import LogoCode from taprimitive import (ast_yield_true, Primitive, PyExportError, value_to_ast) from tautils import (find_group, find_top_block, get_stack_name) from tawindow import plugins_in_use _SETUP_CODE_START = """\ #!/usr/bin/env python _INSTALL_PATH = '/usr/share/sugar/activities/TurtleArt.activity' _ALTERNATIVE_INSTALL_PATH = \ '/usr/local/share/sugar/activities/TurtleArt.activity' import os, sys if os.path.exists('../TurtleBlocks.activity'): sys.path.insert(0, '../TurtleBlocks.activity') elif os.path.exists(os.path.join(os.path.expanduser('~'), 'Activities', 'TurtleBlocks.activity')): sys.path.insert(0, os.path.join(os.path.expanduser('~'), 'Activities', 'TurtleBlocks.activity')) elif os.path.exists(_INSTALL_PATH): sys.path.insert(0, _INSTALL_PATH) elif os.path.exists(_ALTERNATIVE_INSTALL_PATH): sys.path.insert(0, _ALTERNATIVE_INSTALL_PATH) else: print 'This code require the TurtleBlocks activity to be installed.' exit(1) from time import * from random import uniform from math import * from pyexported.window_setup import * tw = get_tw() BOX = {} ACTION = {} """ _SETUP_CODE_END = """\ if __name__ == '__main__': tw.lc.start_time = time() tw.lc.icall(start) gobject.idle_add(tw.lc.doevalstep) gtk.main() """ _ACTION_STACK_START = """\ def %s(): """ _START_STACK_START_ADD = """\ tw.start_plugins() global_objects = tw.get_global_objects() """ _ACTION_STACK_PREAMBLE = """\ turtles = tw.turtles turtle = turtles.get_active_turtle() canvas = tw.canvas logo = tw.lc """ _ACTION_STACK_END = """\ ACTION["%s"] = %s """ # character that is illegal in a Python identifier PAT_IDENTIFIER_ILLEGAL_CHAR = re.compile("[^A-Za-z0-9_]") def save_python(tw): """ Find all the action stacks and turn each into Python code """ all_blocks = tw.just_blocks() blocks_covered = set() tops_of_stacks = [] for block in all_blocks: if block not in blocks_covered: top = find_top_block(block) tops_of_stacks.append(top) block_stack = find_group(top) blocks_covered.update(set(block_stack)) snippets = [_SETUP_CODE_START] for block in tops_of_stacks: stack_name = get_stack_name(block) if stack_name: pythoncode = _action_stack_to_python(block, tw, name=stack_name) snippets.append(pythoncode) snippets.append(linesep) snippets.append(_SETUP_CODE_END) return "".join(snippets) def _action_stack_to_python(block, tw, name="start"): """ Turn a stack of blocks into Python code name -- the name of the action stack (defaults to "start") """ if isinstance(name, int): name = float(name) if not isinstance(name, basestring): name = str(name) # traverse the block stack and get the AST for every block ast_list = _walk_action_stack(block, tw.lc) if not ast_list or not isinstance(ast_list[-1], ast.Yield): ast_list.append(ast_yield_true()) action_stack_ast = ast.Module(body=ast_list) # serialize the ASTs into python code generated_code = codegen.to_source(action_stack_ast) # wrap the action stack setup code around everything name_id = _make_identifier(name) if name == 'start': pre_preamble = _START_STACK_START_ADD for k in plugins_in_use: pre_preamble += " %s = global_objects['%s']\n" % (k.lower(), k) else: pre_preamble = '' generated_code = _indent(generated_code, 1) if generated_code.endswith(linesep): newline = "" else: newline = linesep snippets = [_ACTION_STACK_START % (name_id), pre_preamble, _ACTION_STACK_PREAMBLE, generated_code, newline, _ACTION_STACK_END % (name, name_id)] return "".join(snippets) def _walk_action_stack(top_block, lc, convert_me=True): """ Turn a stack of blocks into a list of ASTs convert_me -- convert values and Primitives to ASTs or return them unconverted? """ block = top_block # value blocks don't have a primitive # (but constant blocks (colors, screen dimensions, etc.) do) if block.is_value_block(): raw_value = block.get_value(add_type_prefix=False) if convert_me: value_ast = value_to_ast(raw_value) if value_ast is not None: return [value_ast] else: return [] else: if raw_value is not None: return [raw_value] else: return [] def _get_prim(block): prim = lc.get_prim_callable(block.primitive) # fail gracefully if primitive is not a Primitive object if not isinstance(prim, Primitive): raise PyExportError(_("block is not exportable"), block=block) return prim prim = _get_prim(block) ast_list = [] arg_asts = [] def _finish_off(block, prim=None): """ Convert block to an AST and add it to the ast_list. Raise a PyExportError on failure. """ if prim is None: prim = _get_prim(block) if convert_me: if prim.export_me: try: new_ast = prim.get_ast(*arg_asts) except ValueError: traceback.print_exc() raise PyExportError(_("error while exporting block"), block=block) if isinstance(new_ast, (list, tuple)): ast_list.extend(new_ast) elif new_ast is not None: ast_list.append(new_ast) elif arg_asts: # TODO do we ever get here? new_ast = ast.List(elts=arg_asts, ctx=ast.Load) ast_list.append(new_ast) else: ast_list.append((prim, ) + tuple(arg_asts)) # skip the very first dock/ connection - it's either the previous block or # the return value of this block dock_queue = block.docks[1:] conn_queue = block.connections[1:] while dock_queue and conn_queue: dock = dock_queue.pop(0) conn = conn_queue.pop(0) if conn is None or dock[0] == 'unavailable': continue elif not dock_queue and dock[0] == 'flow': # finish off this block _finish_off(block, prim) arg_asts = [] # next block block = conn prim = _get_prim(block) dock_queue = block.docks[1:] conn_queue = block.connections[1:] else: # embedded stack of blocks (body of conditional or loop) or # argument block if dock[0] == 'flow': # body of conditional or loop new_arg_asts = _walk_action_stack(conn, lc, convert_me=convert_me) if (prim == LogoCode.prim_loop and not isinstance(new_arg_asts[-1], ast.Yield)): new_arg_asts.append(ast_yield_true()) arg_asts.append(new_arg_asts) else: # argument block new_arg_asts = _walk_action_stack(conn, lc, convert_me=False) arg_asts.append(*new_arg_asts) # finish off last block _finish_off(block, prim) return ast_list def _make_identifier(name): """ Turn name into a Python identifier name by replacing illegal characters """ replaced = re.sub(PAT_IDENTIFIER_ILLEGAL_CHAR, "_", name) # TODO find better strategy to avoid number at beginning if re.match("[0-9]", replaced): replaced = "_" + replaced return replaced def _indent(code, num_levels=1): """ Indent each line of code with num_levels * 4 spaces code -- some python code as a (multi-line) string """ indentation = " " * (4 * num_levels) line_list = code.split(linesep) new_line_list = [] for line in line_list: new_line_list.append(indentation + line) return linesep.join(new_line_list)