From a34c9b517c5bc8fb80371afe87572228063d7687 Mon Sep 17 00:00:00 2001 From: George Marchment <georgemarchment@yahoo.fr> Date: Mon, 17 Feb 2025 14:18:22 +0100 Subject: [PATCH] What looks like a stable version --- src/block.py | 2 +- src/call.py | 24 ++- src/executor.py | 10 +- src/workflow.py | 522 +++++++++++++++++++++++++++--------------------- 4 files changed, 320 insertions(+), 238 deletions(-) diff --git a/src/block.py b/src/block.py index 8d03cfc..8287f90 100644 --- a/src/block.py +++ b/src/block.py @@ -19,7 +19,7 @@ class Block(Root): #This method returns returns all the conditions above the block #Basically everything which needs to be true for the block to exist - def get_all_conditions(self, conditions = {}): + def get_all_conditions(self, conditions): conditions[self.condition] = '' self.origin.get_all_conditions(conditions = conditions) return conditions diff --git a/src/call.py b/src/call.py index d850f10..9a78472 100644 --- a/src/call.py +++ b/src/call.py @@ -42,6 +42,9 @@ class Call(Executor): def add_to_emits(self, emitted): self.emits.append(emitted) + + def get_later_emits(self): + return self.emits def __str__(self): return f"Call_{id(self)}" @@ -78,22 +81,26 @@ class Call(Executor): #Case the param is an operation elif(param.get_type()=="Operation"): + #If it's an artificial operation -> we don't need to do anything + if(not param.get_artificial_status()): - code = code.replace(param.get_code(get_OG=True), param_new_name) - lines = param.simplify_code().split('\n') - if(len(lines)==1): - new_bit = f"{param_new_name} = {lines[0]}" - else: - head = '\n'.join(lines[:-1]) - new_bit = f"{head}\n{param_new_name} = {lines[-1]}" - code = code.replace(tag_to_add, f"{tag_to_add}\n{new_bit}") + code = code.replace(param.get_code(get_OG=True), param_new_name) + lines = param.simplify_code().split('\n') + if(len(lines)==1): + new_bit = f"{param_new_name} = {lines[0]}" + else: + head = '\n'.join(lines[:-1]) + new_bit = f"{head}\n{param_new_name} = {lines[-1]}" + code = code.replace(tag_to_add, f"{tag_to_add}\n{new_bit}") #Case Channel elif(param.get_type()=="Channel"): + raise Exception("This shouldn't happen") None elif(param.get_type()=="Emitted"): None else: + print(param) raise Exception("This shouldn't happen") index+=1 return code.replace(tag_to_add, "").strip() @@ -166,6 +173,7 @@ class Call(Executor): channels = [channel] from .operation import Operation ope = Operation(f"{param}", self) + ope.set_as_artificial() for channel in channels: channel.add_sink(self) ope.add_element_origins(channel) diff --git a/src/executor.py b/src/executor.py index f37341a..5bcb33a 100644 --- a/src/executor.py +++ b/src/executor.py @@ -31,6 +31,14 @@ class Executor(Nextflow_Building_Blocks): def get_list_name_processes(self): return self.origin.get_list_name_processes() + def get_all_conditions(self): + if(self.origin.get_type()=="Root"): + return [] + elif(self.origin.get_type()=="Block"): + conditions = {} + return self.origin.get_all_conditions(conditions) + else: + return self.origin.get_all_conditions() def get_subworkflow_from_name(self, name): @@ -93,8 +101,6 @@ class Executor(Nextflow_Building_Blocks): def get_executors(self): return self.origin.get_executors() - def get_condition(self): - return self.condition def add_element_to_elements_being_called(self, element): self.origin.add_element_to_elements_being_called(element) diff --git a/src/workflow.py b/src/workflow.py index bc9524a..fdef4b1 100644 --- a/src/workflow.py +++ b/src/workflow.py @@ -446,7 +446,6 @@ George Marchment, Bryan Brancotte, Marie Schmit, Frédéric Lemoine, Sarah Cohen #Replace old analysis with new analysis (simplified code) self.__init__(str(temp_file), display_info = False, duplicate=True) self.initialise() - self.generate_all_graphs() def check_relevant_processes_in_workflow(self, relevant_processes): #Check all relevat processes are in wf @@ -547,6 +546,68 @@ George Marchment, Bryan Brancotte, Marie Schmit, Frédéric Lemoine, Sarah Cohen return code + #This Function returns the channels on which the subworkflow (things_added_in_cluster) depend on + def get_takes(self, things_added_in_cluster): + #Basiccaly this is a deco of channels to opeartions -> when the value is an empty list + #This means that the channel is totally definied in the subworkflow -> so we are searching for + #Channels which aren't totatly defined in the subworkflow + channels_2_sources = {} + + for ele in things_added_in_cluster: + if(ele.get_type() == "Operation"): + for o in ele.get_origins(): + channels_2_sources[o] = replace_thing_by_call(o.get_source()) + elif(ele.get_type() == "Call"): + for param in ele.get_parameters(): + if(param.get_type()=="Channel"): + print(param, param.get_code()) + raise Exception("This shouldn't happen -> with the rewrite all the params should be channels") + else: + for o in param.get_origins(): + if(o.get_type()=="Channel"): + channels_2_sources[o] = replace_thing_by_call(o.get_source()) + else: + raise Exception("This shouldn't happen -> with the rewrite all the params should be channels") + else: + raise Exception("This shouldn't happen") + + takes = [] + for channel in channels_2_sources: + if(set(channels_2_sources[channel]).intersection(things_added_in_cluster)!=set(channels_2_sources[channel])): + takes.append(channel) + #print(things_added_in_cluster) + #print(channels_2_operations_needed) + return takes + + #This Function returns the channels the subworkflow (things_added_in_cluster) emits (other things depend on) + def get_emits(self, things_added_in_cluster): + emits = [] + channel_2_sink = {} + #Basiccaly this is a deco of channels to opeartions -> when the value is an empty list + #This means that the channel is totally definied in the subworkflow -> so we are searching for + #Channels which aren't totatly defined in the subworkflow + #This means that things outside the subworkflow depend on this channel + channel_2_sink = {} + + for ele in things_added_in_cluster: + if(ele.get_type() == "Operation"): + for o in ele.get_gives(): + channel_2_sink[o] = replace_thing_by_call(o.get_sink()) + elif(ele.get_type() == "Call"): + #thing = ele.get_first_element_called() + for e in ele.get_later_emits(): + channel_2_sink[e] = replace_thing_by_call(e.get_sink()) + else: + print(ele) + raise Exception("This shouldn't happen") + + for channel in channel_2_sink: + if(set(channel_2_sink[channel]).intersection(things_added_in_cluster)!=set(channel_2_sink[channel])): + emits.append(channel) + return emits + + + #Method which rewrites the workflow follwong the user view #Conert workflow to user_view only makes sense when the option duplicate is activated -> otherwise is doesn't make sense + it makes the analysis way more complicated def convert_workflow_2_user_view(self, relevant_processes = []): @@ -623,7 +684,6 @@ George Marchment, Bryan Brancotte, Marie Schmit, Frédéric Lemoine, Sarah Cohen #Get the clusters and the code self.generate_user_view(relevant_processes = relevant_processes, processes_2_remove = []) clusters = self.graph.get_clusters_from_user_view() - print(clusters) #Get the clsuters with the corresponding operations inside @@ -637,231 +697,239 @@ George Marchment, Bryan Brancotte, Marie Schmit, Frédéric Lemoine, Sarah Cohen - ##Creating the subworkflows from clusters - #calls_in_operations = [] - #non_relevant_name = 1 - #channels_to_replace_outside_of_cluster = [] - #subworkflow_clusters_to_add, subworkflow_cluster_calls_to_add = [], [] - #index_cluster = 0 - #for elements in clusters: - # #Only create the subworkflows for clusters with more than one element - # processes_added = [] - # things_added_in_cluster = [] - # if(len(elements)>1): - # name, body, take, emit = "", "", "", "" - # first_element = True - # for ele in elements: - # - # if(ele.get_type()=="Process"): - # - # #Determine the name of the created subworkflow cluster - # if(ele.get_alias() in relevant_processes): - # name = f"cluster_{ele.get_alias()}" - # #Get the call of thing (either process or subworkflow) - # #TODO -> check it works with subworkflows - # call = ele.get_call() - # - # #This verification is really important - # if(len(call)!=1): - # for c in call: - # print(c.get_code(get_OG=True)) - # - # raise Exception("This shoudn't happen since duplicate mode is activated") - # call = call[0] - # - # #If first element -> add marker for the subworkflow call - # if(first_element): - # code = code.replace(call.get_code(get_OG = True), f"//Anker_cluster{index_cluster}") - # first_element = False - # else: - # code = code.replace(call.get_code(get_OG = True), "") - # - # - # processes_added.append(call.get_first_element_called()) - # printed_condition = " && ".join(call.get_condition().get_conditions()) - # if(printed_condition!=""): - # body+=f"if({printed_condition}) {{\n{call.get_code()}\n}}\n" - # else: - # body+=f"\n{call.get_code()}\n" - # things_added_in_cluster.append(call) - # elif(ele.get_type()=="Operation"): - # - # #If first element -> add marker for the subworkflow call - # if(first_element): - # code = code.replace(ele.get_code(get_OG = True), f"//Anker_cluster{index_cluster}") - # first_element = False - # else: - # code = code.replace(ele.get_code(get_OG = True), "") - # - # #Ignore these cases - # if(ele.get_code()[:4] not in ["emit", "take"]): - # origins = ele.get_origins() - # for o in origins: - # if(o.get_type()=="Call"): - # calls_in_operations.append(o) - # printed_condition = " && ".join(ele.get_condition().get_conditions()) - # if(printed_condition!=""): - # body+=f"if({printed_condition}) {{\n{ele.get_code()}\n}}\n" - # else: - # body+=f"\n{ele.get_code()}\n" - # things_added_in_cluster.append(ele) - # else: - # raise Exception("This shoudn't happen") - # - # #TODO check this part of the code is never seen - # #Here we removing the Call_12313 thing - # for call in calls_in_operations: - # raise Exception("This shouldn't happen since the workflows has been rewritten") - # body = body.replace(call.get_code(), "") - # body = body.replace(str(call), call.get_code()) - # - # #If the name=="" -> it means there isn't any relevant processes in the cluster -> it means it's a cluster of non relevant nodes - # if(name==""): - # #If there are processes called we are going to use them - # if(len(processes_added)>0): - # #TODO find a better naming system - # name = f"non_relevant_cluster_{processes_added[0].get_alias()}" - # else: - # #TODO find a better naming system - # name = f"non_relevant_cluster_{non_relevant_name}" - # non_relevant_name+=1 - # - # #Check that there is a single condtion in the body - # body = group_together_ifs(body) - # conditions_in_subworkflow = [] - # end = -1 - # for match in re.finditer(r"if\s*(\([^\{]+)\{", body): - # conditions_in_subworkflow.append(match.group(1).strip()) - # _, start = match.span(0) - # - # if(len(conditions_in_subworkflow)==1): - # end = extract_curly(body, start) - # body = body[start: end-1].strip() - # - # - # #TAKE - # #Adding take parameters on the inside of the subworkflow - # takes_param = self.get_takes(things_added_in_cluster) - # new_param_names, index, old_param_names = [], 1, [] - # for param in takes_param: - # param_name = f"param_{name}_{index}" - # new_param_names.append(param_name) - # old_param_names.append(param.get_code()) - # index += 1 - # if(len(new_param_names)>0): - # temp = '\n'.join(new_param_names) - # take = f"\ntake:\n{temp}\n" - # - # #EMIT - # #Adding the emitted outputs - # emitted_outputs = self.get_emits(things_added_in_cluster) - # new_output_names, index, old_output_names = [], 0, [] - # for output in emitted_outputs: - # output_name = f"{name}.out[{index}]" - # new_output_names.append(output_name) - # old_output_names.append(output.get_code()) - # index += 1 - # - # if(len(old_output_names)>0): - # temp = '\n'.join(old_output_names) - # emit = f"\nemit:\n{temp}\n" - # - # #Adding empty channels if it doesn't exist in the case of a negative condition - # body = get_channels_to_add_in_false_conditions(body, old_output_names) - # - # - # - # #Replace names inside subworkflow - # subworkflow_code = f"workflow {name} {{\n{take}\nmain:\n{body}\n{emit}\n}}" - # for i in range(len(new_param_names)): - # pattern = fr"[\=\,\(] *({re.escape(takes_param[i].get_code())})[\s\,\)\.]" - # subworkflow_code = replace_group1(subworkflow_code, pattern, new_param_names[i]) - # #subworkflow_code = subworkflow_code.replace(takes_param[i].get_code(), new_param_names[i]) - # - # #TODO -> added verification of conditions - # params = ", ".join(old_param_names) - # subworkfow_call_case_true = f"{name}({params})" - # subworkfow_call_case_false = "" - # for i in range(len(new_output_names)): - # #In the case of channels, we just add chanel = subworkflow.out[i] - # if(not bool(re.findall("\.\s*out", old_output_names[i]))): - # subworkfow_call_case_true+=f"\n{old_output_names[i]} = {new_output_names[i]}" - # subworkfow_call_case_false+=f"\n{old_output_names[i]} = Channel.empty()" - # #In the case of emitted values we need to replace the code on the outside - # else: - # param_out_name= f"{name}_out_{i+1}" - # subworkfow_call_case_true+=f"\n{param_out_name} = {new_output_names[i]}" - # subworkfow_call_case_false+=f"\n{param_out_name} = Channel.empty()" - # channels_to_replace_outside_of_cluster.append((old_output_names[i], param_out_name)) - # #If there was only one single condition in the subworkflow cluster -> then we add it when the call is done - # if(len(conditions_in_subworkflow)==1): - # subworkfow_call = f"if{conditions_in_subworkflow[0]} {{\n{subworkfow_call_case_true}\n}} else {{\n{subworkfow_call_case_false}\n}}" - # None - # else: - # subworkfow_call = subworkfow_call_case_true - # - # - # subworkflow_clusters_to_add.append(subworkflow_code) - # subworkflow_cluster_calls_to_add.append(subworkfow_call) - # index_cluster+=1 - # - # - ##TODO -> rmoving the conditions which are problematic - ##This might not be the probleme -> when rerunnung the analysis isn't totally robust - #still_simplifying_conditions = True - #while(still_simplifying_conditions): - # still_simplifying_conditions = False - # to_replace, anker1, anker2 = "", "", "" - # #Replace if/else - # for match in re.finditer(r"if\s*\([^\{]+\{\s*(\/\/Anker_cluster\d|\s)\s*\}\s*else\s*\{\s*(\/\/Anker_cluster\d|\s)\s*\}", code): - # to_replace = match.group(0) - # anker1, anker2 = match.group(1), match.group(2) - # still_simplifying_conditions = True - # break - # #Replace empty if on its own - # if(not still_simplifying_conditions): - # for match in re.finditer(r"(if\s*\([^\{]+\{\s*(\/\/Anker_cluster\d|\s)\s*\})\s*[^e]", code): - # to_replace = match.group(1) - # anker1 = match.group(2) - # still_simplifying_conditions = True - # break - # if(still_simplifying_conditions): - # code = code.replace(to_replace, f"{anker1}\n{anker2}") - # - # - ##Replace the ankers by the calls of the subworkflows - #for i in range(len(subworkflow_clusters_to_add)): - # #print(f"//Anker_cluster{i}", subworkflow_cluster_calls_to_add[i]) - # code = code.replace(f"//Anker_cluster{i}", subworkflow_cluster_calls_to_add[i]) - # - #for old, new in channels_to_replace_outside_of_cluster: - # pattern= fr"[ \(,]({re.escape(old)})" - # code = replace_group1(code, pattern, new) - # #code = code.replace(old, new) - # - # - ##Add the subworkflow defintions - ##------------------------------------- - ##Add anker - #subworkflow_section = f"//ANKER 4 SUBWORKFLOW DEF" - #to_replace = "" - #for match in re.finditer(r"workflow\s+\w*\s*\{", code): - # to_replace = match.group(0) - # break - #if(to_replace==""): - # raise Exception("No call to a workflow") - # - #code = code.replace(to_replace, f"{subworkflow_section}\n\n{to_replace}") - # - #for sub in subworkflow_clusters_to_add: - # code = code.replace(f'{subworkflow_section}', f"{sub}\n\n{subworkflow_section}") - # - # - ##Putting || back - #code = code.replace("$OR$", "||") - # - #return remove_extra_jumps(format_with_tabs(code)) - return code + #Creating the subworkflows from clusters + calls_in_operations = [] + non_relevant_name = 1 + channels_to_replace_outside_of_cluster = [] + subworkflow_clusters_to_add, subworkflow_cluster_calls_to_add = [], [] + index_cluster = 0 + for elements in clusters: + #Only create the subworkflows for clusters with more than one element + processes_added = [] + things_added_in_cluster = [] + if(len(elements)>1): + name, body, take, emit = "", "", "", "" + first_element = True + for ele in elements: + + if(ele.get_type()=="Process"): + + #Determine the name of the created subworkflow cluster + if(ele.get_alias() in relevant_processes): + name = f"cluster_{ele.get_alias()}" + #Get the call of thing (either process or subworkflow) + call = ele.get_call() + + #If first element -> add marker for the subworkflow call + if(first_element): + code = code.replace(call.get_code(get_OG = True), f"//Anker_cluster{index_cluster}") + first_element = False + else: + code = code.replace(call.get_code(get_OG = True), "") + + + processes_added.append(call.get_first_element_called()) + values = [] + for condition in call.get_all_conditions(): + values.append(condition.get_value()) + printed_condition = " && ".join(values) + if(printed_condition!=""): + body+=f"if({printed_condition}) {{\n{call.get_code()}\n}}\n" + else: + body+=f"\n{call.get_code()}\n" + things_added_in_cluster.append(call) + #Below + elif(ele.get_type()=="Operation"): + #TODO -> check this verification there might be some "effet de bord" + if(not ele.get_artificial_status()): + + #If first element -> add marker for the subworkflow call + print(ele.get_code(get_OG = True)) + if(first_element): + code = code.replace(ele.get_code(get_OG = True), f"//Anker_cluster{index_cluster}", 1) + first_element = False + else: + code = code.replace(ele.get_code(get_OG = True), "", 1) + + #Ignore these cases + if(ele.get_code()[:4] not in ["emit", "take"]): + origins = ele.get_origins() + for o in origins: + if(o.get_type()=="Call"): + calls_in_operations.append(o) + + values = [] + for condition in ele.get_all_conditions(): + values.append(condition.get_value()) + printed_condition = " && ".join(values) + + if(printed_condition!=""): + body+=f"if({printed_condition}) {{\n{ele.get_code()}\n}}\n" + else: + body+=f"\n{ele.get_code()}\n" + things_added_in_cluster.append(ele) + else: + raise Exception("This shoudn't happen") + + + #TODO check this part of the code is never seen + #Here we removing the Call_12313 thing + for call in calls_in_operations: + raise Exception("This shouldn't happen since the workflows has been rewritten") + body = body.replace(call.get_code(), "") + body = body.replace(str(call), call.get_code()) + + + #If the name=="" -> it means there isn't any relevant processes in the cluster -> it means it's a cluster of non relevant nodes + if(name==""): + #If there are processes called we are going to use them + if(len(processes_added)>0): + #TODO find a better naming system + name = f"non_relevant_cluster_{processes_added[0].get_alias()}" + else: + #TODO find a better naming system + name = f"non_relevant_cluster_{non_relevant_name}" + non_relevant_name+=1 + + #Check that there is a single condtion in the body + body = group_together_ifs(body) + conditions_in_subworkflow = [] + end = -1 + for match in re.finditer(r"if\s*(\([^\{]+)\{", body): + conditions_in_subworkflow.append(match.group(1).strip()) + _, start = match.span(0) + + if(len(conditions_in_subworkflow)==1): + end = extract_curly(body, start) + body = body[start: end-1].strip() + + + #TAKE + #Adding take parameters on the inside of the subworkflow + takes_param = self.get_takes(things_added_in_cluster) + new_param_names, index, old_param_names = [], 1, [] + for param in takes_param: + param_name = f"param_{name}_{index}" + new_param_names.append(param_name) + old_param_names.append(param.get_code()) + index += 1 + if(len(new_param_names)>0): + temp = '\n'.join(new_param_names) + take = f"\ntake:\n{temp}\n" + + #EMIT + #Adding the emitted outputs + emitted_outputs = self.get_emits(things_added_in_cluster) + new_output_names, index, old_output_names = [], 0, [] + for output in emitted_outputs: + output_name = f"{name}.out[{index}]" + new_output_names.append(output_name) + old_output_names.append(output.get_code()) + index += 1 + + if(len(old_output_names)>0): + temp = '\n'.join(old_output_names) + emit = f"\nemit:\n{temp}\n" + + #Adding empty channels if it doesn't exist in the case of a negative condition + body = get_channels_to_add_in_false_conditions(body, old_output_names) + + + + #Replace names inside subworkflow + subworkflow_code = f"workflow {name} {{\n{take}\nmain:\n{body}\n{emit}\n}}" + for i in range(len(new_param_names)): + pattern = fr"[\=\,\(] *({re.escape(takes_param[i].get_code())})[\s\,\)\.]" + subworkflow_code = replace_group1(subworkflow_code, pattern, new_param_names[i]) + #subworkflow_code = subworkflow_code.replace(takes_param[i].get_code(), new_param_names[i]) + + #TODO -> added verification of conditions + params = ", ".join(old_param_names) + subworkfow_call_case_true = f"{name}({params})" + subworkfow_call_case_false = "" + for i in range(len(new_output_names)): + #In the case of channels, we just add chanel = subworkflow.out[i] + if(not bool(re.findall("\.\s*out", old_output_names[i]))): + subworkfow_call_case_true+=f"\n{old_output_names[i]} = {new_output_names[i]}" + subworkfow_call_case_false+=f"\n{old_output_names[i]} = Channel.empty()" + #In the case of emitted values we need to replace the code on the outside + else: + param_out_name= f"{name}_out_{i+1}" + subworkfow_call_case_true+=f"\n{param_out_name} = {new_output_names[i]}" + subworkfow_call_case_false+=f"\n{param_out_name} = Channel.empty()" + channels_to_replace_outside_of_cluster.append((old_output_names[i], param_out_name)) + #If there was only one single condition in the subworkflow cluster -> then we add it when the call is done + if(len(conditions_in_subworkflow)==1): + subworkfow_call = f"if{conditions_in_subworkflow[0]} {{\n{subworkfow_call_case_true}\n}} else {{\n{subworkfow_call_case_false}\n}}" + None + else: + subworkfow_call = subworkfow_call_case_true + + + subworkflow_clusters_to_add.append(subworkflow_code) + subworkflow_cluster_calls_to_add.append(subworkfow_call) + index_cluster+=1 + + + + #TODO -> rmoving the conditions which are problematic + #This might not be the probleme -> when rerunnung the analysis isn't totally robust + still_simplifying_conditions = True + while(still_simplifying_conditions): + still_simplifying_conditions = False + to_replace, anker1, anker2 = "", "", "" + #Replace if/else + for match in re.finditer(r"if\s*\([^\{]+\{\s*(\/\/Anker_cluster\d|\s)\s*\}\s*else\s*\{\s*(\/\/Anker_cluster\d|\s)\s*\}", code): + to_replace = match.group(0) + anker1, anker2 = match.group(1), match.group(2) + still_simplifying_conditions = True + break + #Replace empty if on its own + if(not still_simplifying_conditions): + for match in re.finditer(r"(if\s*\([^\{]+\{\s*(\/\/Anker_cluster\d|\s)\s*\})\s*[^e]", code): + to_replace = match.group(1) + anker1 = match.group(2) + still_simplifying_conditions = True + break + if(still_simplifying_conditions): + code = code.replace(to_replace, f"{anker1}\n{anker2}") + + + + #Replace the ankers by the calls of the subworkflows + for i in range(len(subworkflow_clusters_to_add)): + #print(f"//Anker_cluster{i}", subworkflow_cluster_calls_to_add[i]) + code = code.replace(f"//Anker_cluster{i}", subworkflow_cluster_calls_to_add[i]) + + + for old, new in channels_to_replace_outside_of_cluster: + pattern= fr"[ \(,]({re.escape(old)})" + code = replace_group1(code, pattern, new) + #code = code.replace(old, new) + + + #Add the subworkflow defintions + #------------------------------------- + #Add anker + subworkflow_section = f"//ANKER 4 SUBWORKFLOW DEF" + to_replace = "" + for match in re.finditer(r"workflow\s+\w*\s*\{", code): + to_replace = match.group(0) + break + if(to_replace==""): + raise Exception("No call to a workflow") + + code = code.replace(to_replace, f"{subworkflow_section}\n\n{to_replace}") + + for sub in subworkflow_clusters_to_add: + code = code.replace(f'{subworkflow_section}', f"{sub}\n\n{subworkflow_section}") + + + #Putting || back + code = code.replace("$OR$", "||") + + return remove_extra_jumps(format_with_tabs(code)) + #return code # ##So basically when retriving a thing (process or subworkflow) ##There is necessarily one call associated with the thing -> since we have the option duplicate activated -- GitLab