def add_import(node: Module, module: str, cls: str, try_to_import: bool = True) -> None: """ Adds "from ... import ..." to the beginning of the script. Parameters ---------- node module cls Returns ------- """ class AddImportTransformer(NodeTransformer): def __init__(self, module: str, cls: str) -> None: self.done = False self.module = module self.cls = cls def visit_ImportFrom(self, node: ImportFrom) -> ImportFrom: if node.module == self.module: for aliass in node.names: if aliass.name == self.cls: self.done = True break else: node.names.append(alias(name=self.cls, asname=None)) self.done = True return node if try_to_import: try: imported_mod = importlib.import_module(module) except ModuleNotFoundError as e: raise SourceException(e) try: getattr(imported_mod, cls) except AttributeError as e: raise SourceException(e) tr = AddImportTransformer(module, cls) node = tr.visit(node) if not tr.done: node.body.insert( 0, ImportFrom(module=module, names=[alias(name=cls, asname=None)], level=0))
def visit_FunctionDef(self, node: FunctionDef) -> None: if node.name == 'main': for item in node.body: if isinstance(item, Assign): assert isinstance(item.targets[0], Name) if item.targets[0].id == name: # TODO assert for item.value if item.value.func.id != cls: # type: ignore raise SourceException( "Name '{}' already used for instance of '{}'!" .format( name, item.value.func.id)) # type: ignore self.found = True # TODO update arguments? if not self.found: self.generic_visit(node)
def main_loop(tree: Module) -> While: main = find_function("main", tree) for node in main.body: if isinstance( node, While ): # TODO more specific condition (test for True argument) return node raise SourceException("Main loop not found.")
def add_logic_to_loop(tree: Module, scene: Scene, project: Project) -> None: loop = main_loop_body(tree) try: actions_cache, first_action_id, last_action_id = get_actions_cache( project) except ProjectException as e: raise SourceException(e) if first_action_id is None: raise SourceException("'start' action not found.") if last_action_id is None: raise SourceException("'end' action not found.") next_action_id = first_action_id while True: act = actions_cache[next_action_id] if len(loop) == 1 and isinstance(loop[0], Pass): # pass is not necessary now loop.clear() ac_obj, ac_type = act.type.split('/') # for scene objects, convert ID to name try: ac_obj = scene.object(ac_obj).name except Arcor2Exception: pass append_method_call(loop, fix_object_name(ac_obj), ac_type, [get_name_attr("res", clean(act.name))], []) if act.id == last_action_id: break next_action_id = act.outputs[0].default
def check_object_type(object_type_source: str, type_name: str) -> None: """ Checks whether the object type source is a valid one. :param object_type_source: :return: """ try: type_def = hlp.type_def_from_source(object_type_source, type_name, Generic) except hlp.TypeDefException as e: raise SourceException(e) meta_from_def(type_def, False) object_actions(TYPE_TO_PLUGIN, type_def, object_type_source)
def check_service_type(service_type_source: str, type_name: str) -> None: """ Checks whether the service type source is a valid one. :param service_type_source: Source code. :param type_name: Class name. :return: """ try: type_def = hlp.type_def_from_source(service_type_source, type_name, Service) except hlp.TypeDefException as e: raise SourceException(e) # meta_from_def(type_def) # calls API which is probably undesirable for check object_actions(TYPE_TO_PLUGIN, type_def, service_type_source)
def find_class_def(name: str, tree: Union[Module, AST]) -> ClassDef: class FindClassDef(NodeVisitor): def __init__(self) -> None: self.cls_def_node: Optional[ClassDef] = None def visit_ClassDef(self, node: ClassDef) -> None: if node.name == name: self.cls_def_node = node return if not self.cls_def_node: self.generic_visit(node) ff = FindClassDef() ff.visit(tree) if ff.cls_def_node is None: raise SourceException(f"Class definition {name} not found.") return ff.cls_def_node
def find_function(name: str, tree: Union[Module, AST]) -> FunctionDef: class FindFunction(NodeVisitor): def __init__(self) -> None: self.function_node: Optional[FunctionDef] = None def visit_FunctionDef(self, node: FunctionDef) -> None: if node.name == name: self.function_node = node return if not self.function_node: self.generic_visit(node) ff = FindFunction() ff.visit(tree) if ff.function_node is None: raise SourceException(f"Function {name} not found.") return ff.function_node
def add_method_call_in_main(tree: Module, instance: str, method: str, args: List, kwargs: List) -> None: """ Places method call after block where instances are created. Parameters ---------- tree instance init args kwargs Returns ------- """ main_body = find_function("main", tree).body last_assign_idx = None for body_idx, body_item in enumerate(main_body): # TODO check if instance exists! if isinstance(body_item, Assign): last_assign_idx = body_idx if not last_assign_idx: raise SourceException() # TODO iterate over args/kwargs # TODO check actual number of method's arguments (and types?) main_body.insert( last_assign_idx + 1, Expr(value=Call( func=get_name_attr(instance, method), args=args, keywords=[])))
def parse_def(type_def: type) -> AST: try: return parse(inspect.getsource(type_def)) except OSError as e: raise SourceException("Failed to get the source code.") from e
def parse(source: str) -> AST: try: return ast.parse(source, feature_version=sys.version_info[0:2]) except (AssertionError, NotImplementedError, SyntaxError, ValueError) as e: raise SourceException("Failed to parse the code.") from e
def _add_logic(container: Container, current_action: Action, super_container: Optional[Container] = None) -> None: # more paths could lead to the same action, so it might be already added # ...this is easier than searching the tree if current_action.id in added_actions: logger.debug( f"Action {current_action.name} already added, skipping.") return inputs, outputs = project.action_io(current_action.id) logger.debug( f"Adding action {current_action.name}, with {len(inputs)} input(s) and {len(outputs)} output(s)." ) act = current_action.parse_type() ac_obj = scene.object(act.obj_id).name args: List[AST] = [] # TODO make sure that the order of parameters is correct / re-order for param in current_action.parameters: if param.type == ActionParameter.TypeEnum.LINK: parsed_link = param.parse_link() parent_action = project.action(parsed_link.action_id) # TODO add support for tuples assert len(parent_action.flow(FlowTypes.DEFAULT).outputs ) == 1, "Only one result is supported atm." assert parsed_link.output_index == 0 res_name = parent_action.flow(FlowTypes.DEFAULT).outputs[0] # make sure that the result already exists if parent_action.id not in added_actions: raise SourceException( f"Action {current_action.name} attempts to use result {res_name} " f"of subsequent action {parent_action.name}.") args.append(Name(id=res_name, ctx=Load())) elif param.type == ActionParameter.TypeEnum.PROJECT_PARAMETER: args.append( Name(id=project.parameter(param.str_from_value()).name, ctx=Load())) else: plugin = plugin_from_type_name(param.type) args.append( plugin.parameter_ast(type_defs, scene, project, current_action.id, param.name)) list_of_imp_tup = plugin.need_to_be_imported( type_defs, scene, project, current_action.id, param.name) if list_of_imp_tup: # TODO what if there are two same names? for imp_tup in list_of_imp_tup: add_import(tree, imp_tup.module_name, imp_tup.class_name, try_to_import=False) add_method_call( container.body, ac_obj, act.action_type, args, [keyword(arg="an", value=Str(s=current_action.name, kind=""))], current_action.flow(FlowTypes.DEFAULT).outputs, ) added_actions.add(current_action.id) if not outputs: raise SourceException( f"Action {current_action.name} has no outputs.") elif len(outputs) == 1: output = outputs[0] if output.end == output.END: # TODO this is just temporary (while there is while loop), should be rather Return() container.body.append(Continue()) return seq_act = project.action(output.end) seq_act_inputs, _ = project.action_io(seq_act.id) if len(seq_act_inputs ) > 1: # the action belongs to a different block if seq_act.id in added_actions: return logger.debug( f"Action {seq_act.name} going to be added to super_container." ) # test if this is the correct super_container -> count distance (number of blocks) to the START blocks_to_start: Dict[str, int] = {} for inp in seq_act_inputs: parsed_start = inp.parse_start() pact = project.action(parsed_start.start_action_id) blocks_to_start[pact.id] = _blocks_to_start(pact) winner = min(blocks_to_start, key=blocks_to_start.get ) # type: ignore # TODO what is wrong with it? # TODO if blocks_to_start is cached somewhere, the second part of the condition is useless # it might happen that there are two different ways with the same distance if winner == current_action.id or all( value == list(blocks_to_start.values())[0] for value in blocks_to_start.values()): assert super_container is not None _add_logic(super_container, seq_act) return logger.debug(f"Sequential action: {seq_act.name}") _add_logic(container, seq_act, super_container) else: root_if: Optional[If] = None # action has more outputs - each output should have condition for idx, output in enumerate(outputs): if not output.condition: raise SourceException("Missing condition.") # TODO use parameter plugin (action metadata will be needed - to get the return types) # TODO support for other countable types # ...this will only work for booleans from arcor2 import json condition_value = json.loads(output.condition.value) comp = NameConstant(value=condition_value, kind=None) what = output.condition.parse_what() output_name = project.action(what.action_id).flow( what.flow_name).outputs[what.output_index] cond = If( test=Compare(left=Name(id=output_name, ctx=Load()), ops=[Eq()], comparators=[comp]), body=[], orelse=[], ) if idx == 0: root_if = cond container.body.append(root_if) logger.debug(f"Adding branch for: {condition_value}") else: assert isinstance(root_if, If) root_if.orelse.append(cond) if output.end == output.END: cond.body.append( Continue()) # TODO should be rather return continue _add_logic(cond, project.action(output.end), container)
def get_logic_from_source(source_code: str, project: Project) -> None: tree = parse(source_code) assert isinstance(tree, Module) try: actions_cache, _, _ = get_actions_cache(project) except ProjectException as e: raise SourceException(e) # objects_cache = get_objects_cache(project, id_to_var=True) found_actions: Set[str] = set() loop = main_loop_body(tree) last_action: Union[None, Action] = None for node_idx, node in enumerate(loop): # simple checks for expected 'syntax' of action calls (e.g. 'robot.move_to(**res.MoveToBoxIN)') if not isinstance(node, Expr) or not isinstance( node.value, Call) or not isinstance(node.value.func, Attribute): raise SourceException("Unexpected content.") try: val = node.value obj_id = val.func.value.id # type: ignore method = val.func.attr # type: ignore except (AttributeError, IndexError) as e: print(e) raise SourceException("Script has unexpected content.") """ Support for both: robot.move_to(res.MoveToBoxIN) # args ...as well as robot.move_to(**res.MoveToBoxIN) # kwargs """ if len(val.args) == 1 and not val.keywords: action_id = val.args[0].attr # type: ignore elif len(val.keywords) == 1 and not val.args: action_id = val.keywords[0].value.attr # type: ignore else: raise SourceException("Unexpected argument(s) to the action.") if action_id in found_actions: raise SourceException(f"Duplicate action: {action_id}.") found_actions.add(action_id) # TODO test if object instance exists # raise GenerateSourceException(f"Unknown object id {obj_id}.") try: action = actions_cache[action_id] except KeyError: raise SourceException(f"Unknown action {action_id}.") at_obj, at_method = action.type.split("/") at_obj = camel_case_to_snake_case( at_obj) # convert obj id into script variable name if at_obj != obj_id or at_method != method: raise SourceException( f"Action type {action.type} does not correspond to source, where it is" f" {obj_id}/{method}.") action.inputs.clear() action.outputs.clear() if node_idx == 0: action.inputs.append(ActionIO(ActionIOEnum.FIRST.value)) else: assert last_action is not None action.inputs.append(ActionIO(last_action.id)) if node_idx > 0: assert last_action is not None actions_cache[last_action.id].outputs.append(ActionIO(action.id)) if node_idx == len(loop) - 1: action.outputs.append(ActionIO(ActionIOEnum.LAST.value)) last_action = action
def add_cls_inst(node: Module, cls: str, name: str, kwargs: Optional[Dict] = None, kwargs2parse: Optional[Dict] = None) -> None: class FindImport(NodeVisitor): def __init__(self, cls: str) -> None: self.found = False self.cls = cls def visit_ImportFrom(self, node: ImportFrom) -> None: for node_alias in node.names: if node_alias.name == self.cls: self.found = True if not self.found: self.generic_visit(node) class FindClsInst(NodeVisitor): def __init__(self) -> None: self.found = False def visit_FunctionDef(self, node: FunctionDef) -> None: if node.name == 'main': for item in node.body: if isinstance(item, Assign): assert isinstance(item.targets[0], Name) if item.targets[0].id == name: # TODO assert for item.value if item.value.func.id != cls: # type: ignore raise SourceException( "Name '{}' already used for instance of '{}'!" .format( name, item.value.func.id)) # type: ignore self.found = True # TODO update arguments? if not self.found: self.generic_visit(node) class AddClsInst(NodeTransformer): def visit_FunctionDef(self, node: FunctionDef) -> FunctionDef: if node.name == 'main': kw = [] if kwargs: for k, v in kwargs.items(): kw.append(keyword(arg=k, value=v)) if kwargs2parse: for k, v in kwargs2parse.items(): kw.append(keyword(arg=k, value=parse(v))) node.body.insert( 0, Assign(targets=[Name(id=name, ctx=Store())], value=Call(func=Name(id=cls, ctx=Load()), args=[], keywords=kw))) return node find_import = FindImport(cls) find_import.visit(node) if not find_import.found: raise SourceException("Class '{}' not imported!".format(cls)) vis = FindClsInst() vis.visit(node) if not vis.found: tr = AddClsInst() node = tr.visit(node)