Skip to content

Commit 032fd4e

Browse files
committed
restored functionality from specs
1 parent 553bb2f commit 032fd4e

File tree

13 files changed

+629
-1118
lines changed

13 files changed

+629
-1118
lines changed

pydra/design/base.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@
1414
ensure_list,
1515
PYDRA_ATTR_METADATA,
1616
list_fields,
17+
is_lazy,
1718
)
1819
from pydra.utils.typing import (
1920
MultiInputObj,
2021
MultiInputFile,
2122
MultiOutputObj,
2223
MultiOutputFile,
2324
)
24-
from pydra.engine.workflow.lazy import LazyField
2525

2626

2727
if ty.TYPE_CHECKING:
28-
from pydra.engine.specs import OutputsSpec
28+
from pydra.engine.specs import TaskSpec, OutSpec
2929
from pydra.engine.core import Task
3030

3131
__all__ = [
@@ -84,7 +84,9 @@ class Field:
8484
validator=is_type, default=ty.Any, converter=default_if_none(ty.Any)
8585
)
8686
help_string: str = ""
87-
requires: list | None = None
87+
requires: list[str] | list[list[str]] = attrs.field(
88+
factory=list, converter=ensure_list
89+
)
8890
converter: ty.Callable | None = None
8991
validator: ty.Callable | None = None
9092

@@ -240,6 +242,8 @@ def get_fields(klass, field_type, auto_attribs, helps) -> dict[str, Field]:
240242

241243

242244
def make_task_spec(
245+
spec_type: type["TaskSpec"],
246+
out_type: type["OutSpec"],
243247
task_type: type["Task"],
244248
inputs: dict[str, Arg],
245249
outputs: dict[str, Out],
@@ -281,14 +285,16 @@ def make_task_spec(
281285

282286
if name is None and klass is not None:
283287
name = klass.__name__
284-
outputs_klass = make_outputs_spec(outputs, outputs_bases, name)
285-
if klass is None or not issubclass(klass, TaskSpec):
288+
outputs_klass = make_outputs_spec(out_type, outputs, outputs_bases, name)
289+
if klass is None or not issubclass(klass, spec_type):
286290
if name is None:
287291
raise ValueError("name must be provided if klass is not")
292+
if klass is not None and issubclass(klass, TaskSpec):
293+
raise ValueError(f"Cannot change type of spec {klass} to {spec_type}")
288294
bases = tuple(bases)
289295
# Ensure that TaskSpec is a base class
290-
if not any(issubclass(b, TaskSpec) for b in bases):
291-
bases = bases + (TaskSpec,)
296+
if not any(issubclass(b, spec_type) for b in bases):
297+
bases = bases + (spec_type,)
292298
# If building from a decorated class (as opposed to dynamically from a function
293299
# or shell-template), add any base classes not already in the bases tuple
294300
if klass is not None:
@@ -346,8 +352,11 @@ def make_task_spec(
346352

347353

348354
def make_outputs_spec(
349-
outputs: dict[str, Out], bases: ty.Sequence[type], spec_name: str
350-
) -> type["OutputsSpec"]:
355+
spec_type: type["OutSpec"],
356+
outputs: dict[str, Out],
357+
bases: ty.Sequence[type],
358+
spec_name: str,
359+
) -> type["OutSpec"]:
351360
"""Create an outputs specification class and its outputs specification class from the
352361
output fields provided to the decorator/function.
353362
@@ -368,10 +377,14 @@ def make_outputs_spec(
368377
klass : type
369378
The class created using the attrs package
370379
"""
371-
from pydra.engine.specs import OutputsSpec
380+
from pydra.engine.specs import OutSpec
372381

373-
if not any(issubclass(b, OutputsSpec) for b in bases):
374-
outputs_bases = bases + (OutputsSpec,)
382+
if not any(issubclass(b, spec_type) for b in bases):
383+
if out_spec_bases := [b for b in bases if issubclass(b, OutSpec)]:
384+
raise ValueError(
385+
f"Cannot make {spec_type} output spec from {out_spec_bases} bases"
386+
)
387+
outputs_bases = bases + (spec_type,)
375388
if reserved_names := [n for n in outputs if n in RESERVED_OUTPUT_NAMES]:
376389
raise ValueError(
377390
f"{reserved_names} are reserved and cannot be used for output field names"
@@ -549,7 +562,7 @@ def make_validator(field: Field, interface_name: str) -> ty.Callable[..., None]
549562
def allowed_values_validator(_, attribute, value):
550563
"""checking if the values is in allowed_values"""
551564
allowed = attribute.metadata[PYDRA_ATTR_METADATA].allowed_values
552-
if value is attrs.NOTHING or isinstance(value, LazyField):
565+
if value is attrs.NOTHING or is_lazy(value):
553566
pass
554567
elif value not in allowed:
555568
raise ValueError(

pydra/design/python.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import inspect
33
import attrs
44
from pydra.engine.task import FunctionTask
5-
from pydra.engine.specs import TaskSpec
5+
from pydra.engine.specs import PythonSpec, PythonOutSpec
66
from .base import (
77
Arg,
88
Out,
@@ -87,7 +87,7 @@ def define(
8787
bases: ty.Sequence[type] = (),
8888
outputs_bases: ty.Sequence[type] = (),
8989
auto_attribs: bool = True,
90-
) -> TaskSpec:
90+
) -> PythonSpec:
9191
"""
9292
Create an interface for a function or a class.
9393
@@ -103,7 +103,7 @@ def define(
103103
Whether to use auto_attribs mode when creating the class.
104104
"""
105105

106-
def make(wrapped: ty.Callable | type) -> TaskSpec:
106+
def make(wrapped: ty.Callable | type) -> PythonSpec:
107107
if inspect.isclass(wrapped):
108108
klass = wrapped
109109
function = klass.function
@@ -139,6 +139,8 @@ def make(wrapped: ty.Callable | type) -> TaskSpec:
139139
)
140140

141141
interface = make_task_spec(
142+
PythonSpec,
143+
PythonOutSpec,
142144
FunctionTask,
143145
parsed_inputs,
144146
parsed_outputs,

pydra/design/shell.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from fileformats.core import from_mime
1212
from fileformats import generic
1313
from fileformats.core.exceptions import FormatRecognitionError
14-
from pydra.engine.specs import TaskSpec
14+
from pydra.engine.specs import ShellSpec, ShellOutSpec
1515
from .base import (
1616
Arg,
1717
Out,
@@ -177,9 +177,8 @@ class outarg(Out, arg):
177177
inputs (entire inputs will be passed) or any input field name (a specific input
178178
field will be sent).
179179
path_template: str, optional
180-
If provided, the field is treated also as an output field and it is added to
181-
the output spec. The template can use other fields, e.g. {file1}. Used in order
182-
to create an output specification.
180+
The template used to specify where the output file will be written to can use
181+
other fields, e.g. {file1}. Used in order to create an output specification.
183182
"""
184183

185184
path_template: str | None = attrs.field(default=None)
@@ -202,7 +201,7 @@ def define(
202201
outputs_bases: ty.Sequence[type] = (),
203202
auto_attribs: bool = True,
204203
name: str | None = None,
205-
) -> TaskSpec:
204+
) -> ShellSpec:
206205
"""Create a task specification for a shell command. Can be used either as a decorator on
207206
the "canonical" dataclass-form of a task specification or as a function that takes a
208207
"shell-command template string" of the form
@@ -251,13 +250,13 @@ def define(
251250
252251
Returns
253252
-------
254-
TaskSpec
253+
ShellSpec
255254
The interface for the shell command
256255
"""
257256

258257
def make(
259258
wrapped: ty.Callable | type | None = None,
260-
) -> TaskSpec:
259+
) -> ShellSpec:
261260

262261
if inspect.isclass(wrapped):
263262
klass = wrapped
@@ -272,6 +271,14 @@ def make(
272271
f"Shell task class {wrapped} must have an `executable` "
273272
"attribute that specifies the command to run"
274273
) from None
274+
if not isinstance(executable, str) and not (
275+
isinstance(executable, ty.Sequence)
276+
and all(isinstance(e, str) for e in executable)
277+
):
278+
raise ValueError(
279+
"executable must be a string or a sequence of strings"
280+
f", not {executable!r}"
281+
)
275282
class_name = klass.__name__
276283
check_explicit_fields_are_none(klass, inputs, outputs)
277284
parsed_inputs, parsed_outputs = extract_fields_from_class(
@@ -309,7 +316,15 @@ def make(
309316
{o.name: o for o in parsed_outputs.values() if isinstance(o, arg)}
310317
)
311318
parsed_inputs["executable"] = arg(
312-
name="executable", type=str, argstr="", position=0, default=executable
319+
name="executable",
320+
type=str | ty.Sequence[str],
321+
argstr="",
322+
position=0,
323+
default=executable,
324+
help_string=(
325+
"the first part of the command, can be a string, "
326+
"e.g. 'ls', or a list, e.g. ['ls', '-l', 'dirname']"
327+
),
313328
)
314329

315330
# Set positions for the remaining inputs that don't have an explicit position
@@ -319,6 +334,8 @@ def make(
319334
inpt.position = position_stack.pop(0)
320335

321336
interface = make_task_spec(
337+
ShellSpec,
338+
ShellOutSpec,
322339
ShellCommandTask,
323340
parsed_inputs,
324341
parsed_outputs,

pydra/design/tests/test_workflow.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,9 @@ def MyTestWorkflow(a: list[int], b: list[float]) -> list[float]:
344344
wf = Workflow.construct(MyTestWorkflow(a=[1, 2, 3], b=[1.0, 10.0, 100.0]))
345345
assert wf["Mul"].splitter == ["Mul.x", "Mul.y"]
346346
assert wf["Mul"].combiner == ["Mul.x"]
347-
assert wf.outputs.out == LazyOutField(node=wf["Sum"], field="out", type=list[float])
347+
assert wf.outputs.out == LazyOutField(
348+
node=wf["Sum"], field="out", type=list[float], type_checked=True
349+
)
348350

349351

350352
def test_workflow_split_combine2():

pydra/design/workflow.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
check_explicit_fields_are_none,
1414
extract_fields_from_class,
1515
)
16-
from pydra.engine.specs import TaskSpec
16+
from pydra.engine.specs import TaskSpec, OutSpec, WorkflowSpec, WorkflowOutSpec
1717

1818

1919
__all__ = ["define", "add", "this", "arg", "out"]
@@ -154,6 +154,8 @@ def make(wrapped: ty.Callable | type) -> TaskSpec:
154154
parsed_inputs[inpt_name].lazy = True
155155

156156
interface = make_task_spec(
157+
WorkflowSpec,
158+
WorkflowOutSpec,
157159
WorkflowTask,
158160
parsed_inputs,
159161
parsed_outputs,
@@ -172,9 +174,6 @@ def make(wrapped: ty.Callable | type) -> TaskSpec:
172174
return make
173175

174176

175-
OutputType = ty.TypeVar("OutputType")
176-
177-
178177
def this() -> Workflow:
179178
"""Get the workflow currently being constructed.
180179
@@ -186,7 +185,10 @@ def this() -> Workflow:
186185
return Workflow.under_construction
187186

188187

189-
def add(task_spec: TaskSpec[OutputType], name: str = None) -> OutputType:
188+
OutSpecType = ty.TypeVar("OutSpecType", bound=OutSpec)
189+
190+
191+
def add(task_spec: TaskSpec[OutSpecType], name: str = None) -> OutSpecType:
190192
"""Add a node to the workflow currently being constructed
191193
192194
Parameters
@@ -199,7 +201,7 @@ def add(task_spec: TaskSpec[OutputType], name: str = None) -> OutputType:
199201
200202
Returns
201203
-------
202-
OutputType
204+
OutSpec
203205
The outputs specification of the node
204206
"""
205207
return this().add(task_spec, name=name)

0 commit comments

Comments
 (0)