Skip to content

Workflow converter #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 88 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
aa9f4c0
started to implement workflow converter
tclose Mar 26, 2024
95413b0
implemented and tested split statements util
tclose Mar 27, 2024
b1a2193
implementing workflow conversion
tclose Mar 28, 2024
c8b11d9
added local classes and functions to converted workflow files
tclose Apr 2, 2024
f6ff24b
added support for tripple quotes in extract_args
tclose Apr 4, 2024
a6637db
workflow spec generation
tclose Apr 4, 2024
88efbea
unused import
tclose Apr 4, 2024
e4e6be7
unused exception
tclose Apr 4, 2024
508768d
implemented workflow converter
tclose Apr 4, 2024
7eb06c5
added example workflow specs
tclose Apr 4, 2024
4afe3bc
debugging workflow converter
tclose Apr 4, 2024
d798530
reorganised workflow converter code
tclose Apr 5, 2024
3fa6413
sorted out issues with config params
tclose Apr 5, 2024
d9601a4
fixed up node connections in workflow converter
tclose Apr 6, 2024
6e8fd04
sorted out intrapackage module writing
tclose Apr 7, 2024
2651209
fixed up locally defined nested workflows
tclose Apr 7, 2024
d465d3d
fixed up intra_pkg_funcs writing so that only one function is written…
tclose Apr 8, 2024
b0de34e
debugged input_spec + lzin issues
tclose Apr 8, 2024
54048b2
sorted out multiple potential nodes
tclose Apr 8, 2024
f8319da
added external_nested workflows to anat_qc_workflow
tclose Apr 8, 2024
4941d25
task tests pass after import refactor
tclose Apr 10, 2024
6a9da03
fixed up a range of bugs with importing interfaces
tclose Apr 10, 2024
28c46aa
split utils into separate modules
tclose Apr 10, 2024
a9eb426
implementing package translations
tclose Apr 11, 2024
e6fe44b
added in mriqc interfaces
tclose Apr 11, 2024
fa070f2
created package-wide converter for workflow packages
tclose Apr 15, 2024
9d9fd68
ensured that callables imports are absolute
tclose Apr 15, 2024
4ca63e1
added new pkg-generation spec for mriqc
tclose Apr 15, 2024
c8e2951
added in pkg-gen-specs yamls
tclose Apr 16, 2024
8b5c995
working on package generation for workflows
tclose Apr 16, 2024
0ba3031
setting up package gen to work for workflow-predominant packages
tclose Apr 16, 2024
b54c6d9
got interface-only package generation back working again after refactor
tclose Apr 16, 2024
99a9865
debugging wider mriqc workflows
tclose Apr 16, 2024
c5c539d
debugging workflow package conversion
tclose Apr 16, 2024
c83155d
changed gen-code location to use expand user
tclose Apr 17, 2024
1a82e1f
full mriqc package has been written
tclose Apr 17, 2024
b6b02a5
fixing up issues with workflow package conversion
tclose Apr 17, 2024
e8aaeb6
debugging imports
tclose Apr 18, 2024
e6d37e7
debugging import translation
tclose Apr 18, 2024
a351896
fixed up manual import translations
tclose Apr 18, 2024
09b0948
fixed up import locations
tclose Apr 18, 2024
755ef54
reinstated interface generation
tclose Apr 18, 2024
97a67cc
inline nipype non-interface objects
tclose Apr 18, 2024
d124953
refactor task writing modules
tclose Apr 18, 2024
7808c9c
implemented writing of __init__ files
tclose Apr 19, 2024
b4671a1
implemented inlining of nipype objects
tclose Apr 19, 2024
4f33f40
fixed up creating of init files so pydra-mriqc imports
tclose Apr 19, 2024
1188d2a
changed convert CLI to allow explicit inclusion of non-interface/work…
tclose Apr 20, 2024
c423829
fixed up indentation reduction
tclose Apr 20, 2024
586f428
reworked intra_package object handling
tclose Apr 21, 2024
197ae4d
fixed up intra pkg imports and a bug in split statement with inline c…
tclose Apr 22, 2024
7472203
fixed bug with nested workflows local names
tclose Apr 22, 2024
eabf37c
fixed up module omission
tclose Apr 22, 2024
e343633
added empty __init__.py files for pydra.tasks.<pkg> and below up unti…
tclose Apr 22, 2024
28a1aa4
debugging package conversion
tclose Apr 22, 2024
b6ca18a
debugging workflow/package generation
tclose Apr 22, 2024
8d73b4a
implemented ports of interfaces from the nipype core package
tclose Apr 23, 2024
ac49ca9
fixed writing of __init__ in auto package
tclose Apr 23, 2024
8b21444
debugging unittests
tclose Apr 23, 2024
1cd6ea2
deleted old specs
tclose Apr 23, 2024
4d8fb4e
added package conversion test and all unittests pass
tclose Apr 23, 2024
c79d0a9
added conversion from niu.Function to FunctionTask
tclose Apr 24, 2024
697a8ee
fixed up workflow conversions to take workflow inputs as function args
tclose Apr 24, 2024
8af19a1
write conftest for workflow tests
tclose Apr 24, 2024
5b8a7b4
debugging workflow converters
tclose Apr 25, 2024
cda19bd
implemented NestedWorkflowAssignmentConverter
tclose Apr 26, 2024
e56c4d2
moved config defaults into package converter
tclose Apr 26, 2024
af5818d
added test_inputs option
tclose Apr 26, 2024
50a2bad
started implementing NodeFactoryConverter
tclose Apr 26, 2024
521aecf
refactored statements into separate sub-package
tclose Apr 27, 2024
98afe8a
refactored parse_statements to put match/parse logic in respective cl…
tclose Apr 27, 2024
5d625cf
implemented get_imported_object and node factory handling
tclose Apr 27, 2024
abb024d
fixed up handling of node factory methods
tclose Apr 27, 2024
287f4e0
implementing class and function converters
tclose Apr 27, 2024
bfecbc2
debugging workflow and helper function/class generation
tclose Apr 30, 2024
b67b22e
working on assignment statements to get delayed var info
tclose May 9, 2024
e5247d3
handled varfield inputs to nested workflows
tclose May 9, 2024
c32a2b8
reworked nested workflow input/output detection and top-level export …
tclose May 12, 2024
8446187
fixing up regressions
tclose May 13, 2024
28d185b
fixed issue with multiple input conns to same node
tclose May 13, 2024
9667474
debugging input/output mapping
tclose May 13, 2024
2f63f18
finally sorted out nested-workflow input/output propagation
tclose May 14, 2024
28a2828
fixed duplicate output issue caused by replacement mapping
tclose May 14, 2024
c7b274b
check for replaced connections in get_(input|output)_from_conn methods
tclose May 15, 2024
749a805
implemented trimming of unused inputs
tclose May 16, 2024
cc7a270
fixed unittests
tclose May 16, 2024
284458d
added in example specs from mriqc and reworked test_package_complete …
tclose May 17, 2024
451b545
added dep and use '--break-system-packages' in gha
tclose May 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
reworked nested workflow input/output detection and top-level export …
…completes successfully
  • Loading branch information
tclose committed May 12, 2024
commit c32a2b83ad95873e9f374da74d7f5b99636a526c
10 changes: 1 addition & 9 deletions nipype2pydra/interface/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@
UsedSymbols,
types_converter,
from_dict_converter,
unwrap_nested_type,
)
from ..statements import (
ImportStatement,
parse_imports,
ExplicitImport,
from_list_to_imports,
)
from fileformats.core.mixin import WithClassifiers
from fileformats.generic import File
import nipype2pydra.package

Expand Down Expand Up @@ -755,14 +755,6 @@ def construct_imports(
continue
stmts.append(stmt)

def unwrap_nested_type(t: type) -> ty.List[type]:
if issubclass(t, WithClassifiers) and t.is_classified:
unwrapped = [t.unclassified]
for c in t.classifiers:
unwrapped.extend(unwrap_nested_type(c))
return unwrapped
return [t]

for tp in itertools.chain(*(unwrap_nested_type(t) for t in nonstd_types)):
stmts.append(ImportStatement.from_object(tp))
if include_task:
Expand Down
5 changes: 5 additions & 0 deletions nipype2pydra/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -803,11 +803,15 @@ def write_to_module(
converted_code: ty.Optional[str] = None,
find_replace: ty.Optional[ty.List[ty.Tuple[str, str]]] = None,
inline_intra_pkg: bool = False,
additional_imports: ty.Optional[ty.List[ImportStatement]] = None,
):
"""Writes the given imports, constants, classes, and functions to the file at the given path,
merging with existing code if it exists"""
from .helpers import FunctionConverter, ClassConverter

if additional_imports is None:
additional_imports = []

if find_replace is None:
find_replace = self.find_replace
else:
Expand Down Expand Up @@ -936,6 +940,7 @@ def write_to_module(
+ converter_imports
+ [i for i in used.imports if not i.indent]
+ GENERIC_PYDRA_IMPORTS
+ additional_imports
)

if module_fspath.name != "__init__.py":
Expand Down
104 changes: 62 additions & 42 deletions nipype2pydra/statements/workflow_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ class AssignmentStatement:
indent: str = attrs.field()
assignments: ty.Dict[str, str] = attrs.field()

matches_re = re.compile(r"(\s*)(\w[\w\s,]*)\s*=\s*(.*)")
matches_re = re.compile(
r"(\s*)(\w[\w\s,]*)\s*=\s*(.*)$", flags=re.MULTILINE | re.DOTALL
)

def __iter__(self):
return iter(self.assignments)
Expand Down Expand Up @@ -62,7 +64,7 @@ def parse(cls, statement: str) -> "AssignmentStatement":
)

def __str__(self):
return f"{self.indent}{', '.join(self.varnames)} = {', '.join(self.value)}"
return f"{self.indent}{', '.join(self.keys())} = {', '.join(self.values())}"


@attrs.define
Expand Down Expand Up @@ -171,11 +173,17 @@ def targets(self):

@property
def wf_in(self):
return self.source_name is None
return self.source_name is None or (
(self.target_name, str(self.target_in))
in self.workflow_converter._input_mapping
)

@property
def wf_out(self):
return self.target_name is None
return self.target_name is None or (
(self.source_name, str(self.source_out))
in self.workflow_converter._output_mapping
)

@cached_property
def conditional(self):
Expand All @@ -200,28 +208,28 @@ def wf_in_name(self):
raise ValueError(
f"Cannot get wf_in_name for {self} as it is not a workflow input"
)
source_out_name = (
self.source_out
if not isinstance(self.source_out, DynamicField)
else self.source_out.varname
)
return self.workflow_converter.input_name(self.source_name, source_out_name)
# source_out_name = (
# self.source_out
# if not isinstance(self.source_out, DynamicField)
# else self.source_out.varname
# )
return self.workflow_converter.get_input(self.source_out, self.source_name).name

@property
def wf_out_name(self):
if not self.wf_out:
raise ValueError(
f"Cannot get wf_out_name for {self} as it is not a workflow output"
)
return self.workflow_converter.output_name(self.target_name, self.target_in)
return self.workflow_converter.get_output(self.target_in, self.target_name).name

def __str__(self):
if not self.include:
return ""
return f"{self.indent}pass\n" if self.conditional else ""
code_str = ""
# Get source lazy-field
if self.wf_in:
src = f"{self.workflow_variable}.lzin.{self.wf_in_name}"
src = f"{self.workflow_variable}.lzin.{self.source_out}"
else:
src = f"{self.workflow_variable}.{self.source_name}.lzout.{self.source_out}"
if isinstance(self.source_out, DynamicField):
Expand Down Expand Up @@ -257,7 +265,7 @@ def __str__(self):
f'{intf_name}({self.wf_in_name}={src}, name="{intf_name}"))\n\n'
)
src = f"{self.workflow_variable}.{intf_name}.lzout.out"
code_str += f"{self.indent}{self.workflow_variable}.set_output([({self.wf_out_name!r}, {src})])"
code_str += f"{self.indent}{self.workflow_variable}.set_output([({self.target_in!r}, {src})])"
elif isinstance(self.target_in, VarField):
code_str += f"{self.indent}setattr({self.workflow_variable}.{self.target_name}.inputs, {self.target_in}, {src})"
else:
Expand Down Expand Up @@ -417,15 +425,15 @@ def converted_interface(self):

def __str__(self):
if not self.include:
return ""
return f"{self.indent}pass\n" if self.conditional else ""
args = ["=".join(a) for a in self.arg_name_vals]
conn_args = []
for conn in sorted(self.in_conns, key=attrgetter("target_in")):
if not conn.include or not conn.lzouttable:
continue
if conn.wf_in:
arg = (
f"{conn.target_in}={self.workflow_variable}.lzin.{conn.wf_in_name}"
f"{conn.target_in}={self.workflow_variable}.lzin.{conn.source_out}"
)
else:
arg = (
Expand Down Expand Up @@ -547,7 +555,7 @@ class AddNestedWorkflowStatement(AddNodeStatement):

def __str__(self):
if not self.include:
return ""
return f"{self.indent}pass\n" if self.conditional else ""
if self.nested_workflow:
config_params = [
f"{n}_{c}={n}_{c}" for n, c in self.nested_workflow.used_configs
Expand Down Expand Up @@ -618,26 +626,31 @@ def add_input_connection(self, conn: ConnectionStatement):
target_name, target_in = conn.target_in.match_to_workflow(
self.nested_workflow
)
else:
elif isinstance(conn.target_in, NestedVarField):
target_name = conn.target_in.node_name
target_in = conn.target_in.varname
else:
target_in = conn.target_in
target_name = None
if target_name == self.nested_workflow.input_node:
target_name = None
nested_input = self.nested_workflow.get_input(target_in, node_name=target_name)
conn.target_in = nested_input.name
super().add_input_connection(conn)
if target_name:
# If not connected to the input node, add connections from the nested
# workflow input to the target node
for node in self.nested_workflow.nodes[target_name]:
node.add_input_connection(
ConnectionStatement(
source_name=None,
source_out=nested_input.name,
target_name=target_name,
target_in=target_in,
indent=conn.indent,
workflow_converter=self.nested_workflow,
)
node_conn = ConnectionStatement(
source_name=None,
source_out=nested_input.name,
target_name=target_name,
target_in=target_in,
indent=conn.indent,
workflow_converter=self.nested_workflow,
)
self.nested_workflow.connections.append(node_conn)
node.add_input_connection(node_conn)

def add_output_connection(self, conn: ConnectionStatement):
"""Adds and output connection to a node, setting as an output of the whole
Expand All @@ -658,9 +671,12 @@ def add_output_connection(self, conn: ConnectionStatement):
source_name, source_out = conn.source_out.match_to_workflow(
self.nested_workflow
)
else:
elif isinstance(conn.source_out, NestedVarField):
source_name = conn.source_out.node_name
source_out = conn.source_out.varname
else:
source_out = conn.source_out
source_name = None
if source_name == self.nested_workflow.output_node:
source_name = None
nested_output = self.nested_workflow.get_output(
Expand All @@ -669,17 +685,19 @@ def add_output_connection(self, conn: ConnectionStatement):
conn.source_out = nested_output.name
super().add_output_connection(conn)
if source_name:
# If not the output node, add connections to the nested workflow output
# from the source node
for node in self.nested_workflow.nodes[source_name]:
node.add_output_connection(
ConnectionStatement(
source_name=source_name,
source_out=source_out,
target_name=None,
target_in=nested_output.name,
indent=conn.indent,
workflow_converter=self.nested_workflow,
)
node_conn = ConnectionStatement(
source_name=source_name,
source_out=source_out,
target_name=None,
target_in=nested_output.name,
indent=conn.indent,
workflow_converter=self.nested_workflow,
)
self.nested_workflow.connections.append(node_conn)
node.add_output_connection(node_conn)


@attrs.define
Expand All @@ -698,11 +716,11 @@ def __str__(self):
node_name = node.name
workflow_variable = self.nodes[0].workflow_variable
if self.is_workflow:
nested_wf = node.nested_spec
nested_wf = node.nested_workflow
parts = self.attribute.split(".")
nested_node_name = parts[2]
attribute_name = parts[3]
target_in = nested_wf.input_name(nested_node_name, attribute_name)
target_in = nested_wf.get_input(attribute_name, nested_node_name).name
attribute = ".".join(parts[:2] + [target_in] + parts[4:])
workflow_variable = self.nodes[0].workflow_variable
assert (n.workflow_variable == workflow_variable for n in self.nodes)
Expand Down Expand Up @@ -781,14 +799,14 @@ def __str__(self):
f" {self.varname} = Workflow("
f"name={self.workflow_name}, input_spec={{"
+ ", ".join(
f"'{i.name}': {i.type}"
f"'{i.name}': {i.type.__name__}"
for i in sorted(
self.workflow_converter.inputs.values(), key=attrgetter("name")
)
)
+ "}, output_spec={"
+ ", ".join(
f"'{o.name}': {o.type}"
f"'{o.name}': {o.type.__name__}"
for o in sorted(
self.workflow_converter.outputs.values(), key=attrgetter("name")
)
Expand Down Expand Up @@ -830,4 +848,6 @@ def __str__(self):

@classmethod
def parse(cls, statement: str) -> "OtherStatement":
return OtherStatement(*re.match(r"(\s*)(.*)", statement).groups())
return OtherStatement(
*re.match(r"(\s*)(.*)$", statement, flags=re.MULTILINE | re.DOTALL).groups()
)
1 change: 1 addition & 0 deletions nipype2pydra/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from_named_dicts_converter,
str_to_type,
types_converter,
unwrap_nested_type,
INBUILT_NIPYPE_TRAIT_NAMES,
)
from .symbols import ( # noqa: F401
Expand Down
10 changes: 10 additions & 0 deletions nipype2pydra/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from contextlib import contextmanager
from pathlib import Path
from fileformats.core import FileSet, from_mime
from fileformats.core.mixin import WithClassifiers
from ..exceptions import (
UnmatchedParensException,
UnmatchedQuoteException,
Expand Down Expand Up @@ -518,3 +519,12 @@ def types_converter(types: ty.Dict[str, ty.Union[str, type]]) -> ty.Dict[str, ty
tp = str_to_type(tp_or_str)
converted[name] = tp
return converted


def unwrap_nested_type(t: type) -> ty.List[type]:
if issubclass(t, WithClassifiers) and t.is_classified:
unwrapped = [t.unclassified]
for c in t.classifiers:
unwrapped.extend(unwrap_nested_type(c))
return unwrapped
return [t]
Loading