Skip to content

Commit 03e6951

Browse files
committed
resolved gnarly circular imports
1 parent d825056 commit 03e6951

File tree

20 files changed

+1653
-1450
lines changed

20 files changed

+1653
-1450
lines changed

pydra/design/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from .base import TaskSpec, list_fields
21
from . import python
32
from . import shell
3+
from . import workflow
44

55

6-
__all__ = ["TaskSpec", "list_fields", "python", "shell"]
6+
__all__ = ["python", "shell", "workflow"]

pydra/design/base.py

Lines changed: 18 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,35 @@
55
import enum
66
from pathlib import Path
77
from copy import copy
8-
from typing_extensions import Self
98
import attrs.validators
109
from attrs.converters import default_if_none
1110
from fileformats.generic import File
1211
from pydra.utils.typing import TypeParser, is_optional, is_fileset_or_union
13-
14-
# from pydra.utils.misc import get_undefined_symbols
15-
from pydra.engine.helpers import from_list_if_single, ensure_list
16-
from pydra.engine.specs import (
17-
LazyField,
12+
from pydra.engine.helpers import (
13+
from_list_if_single,
14+
ensure_list,
15+
PYDRA_ATTR_METADATA,
16+
list_fields,
17+
)
18+
from pydra.utils.typing import (
1819
MultiInputObj,
1920
MultiInputFile,
2021
MultiOutputObj,
2122
MultiOutputFile,
2223
)
23-
from pydra.engine.core import Task, AuditFlag
24+
from pydra.engine.workflow.lazy import LazyField
2425

2526

27+
if ty.TYPE_CHECKING:
28+
from pydra.engine.specs import OutputsSpec
29+
from pydra.engine.core import Task
30+
2631
__all__ = [
2732
"Field",
2833
"Arg",
2934
"Out",
30-
"TaskSpec",
31-
"OutputsSpec",
3235
"ensure_field_objects",
3336
"make_task_spec",
34-
"list_fields",
3537
]
3638

3739
RESERVED_OUTPUT_NAMES = ("split", "combine")
@@ -154,120 +156,6 @@ class Out(Field):
154156
pass
155157

156158

157-
class OutputsSpec:
158-
"""Base class for all output specifications"""
159-
160-
def split(
161-
self,
162-
splitter: ty.Union[str, ty.List[str], ty.Tuple[str, ...], None] = None,
163-
/,
164-
overwrite: bool = False,
165-
cont_dim: ty.Optional[dict] = None,
166-
**inputs,
167-
) -> Self:
168-
"""
169-
Run this task parametrically over lists of split inputs.
170-
171-
Parameters
172-
----------
173-
splitter : str or list[str] or tuple[str] or None
174-
the fields which to split over. If splitting over multiple fields, lists of
175-
fields are interpreted as outer-products and tuples inner-products. If None,
176-
then the fields to split are taken from the keyword-arg names.
177-
overwrite : bool, optional
178-
whether to overwrite an existing split on the node, by default False
179-
cont_dim : dict, optional
180-
Container dimensions for specific inputs, used in the splitter.
181-
If input name is not in cont_dim, it is assumed that the input values has
182-
a container dimension of 1, so only the most outer dim will be used for splitting.
183-
**inputs
184-
fields to split over, will automatically be wrapped in a StateArray object
185-
and passed to the node inputs
186-
187-
Returns
188-
-------
189-
self : TaskBase
190-
a reference to the task
191-
"""
192-
self._node.split(splitter, overwrite=overwrite, cont_dim=cont_dim, **inputs)
193-
return self
194-
195-
def combine(
196-
self,
197-
combiner: ty.Union[ty.List[str], str],
198-
overwrite: bool = False, # **kwargs
199-
) -> Self:
200-
"""
201-
Combine inputs parameterized by one or more previous tasks.
202-
203-
Parameters
204-
----------
205-
combiner : list[str] or str
206-
the field or list of inputs to be combined (i.e. not left split) after the
207-
task has been run
208-
overwrite : bool
209-
whether to overwrite an existing combiner on the node
210-
**kwargs : dict[str, Any]
211-
values for the task that will be "combined" before they are provided to the
212-
node
213-
214-
Returns
215-
-------
216-
self : Self
217-
a reference to the outputs object
218-
"""
219-
self._node.combine(combiner, overwrite=overwrite)
220-
return self
221-
222-
223-
OutputType = ty.TypeVar("OutputType", bound=OutputsSpec)
224-
225-
226-
class TaskSpec(ty.Generic[OutputType]):
227-
"""Base class for all task specifications"""
228-
229-
Task: ty.Type[Task]
230-
231-
def __call__(
232-
self,
233-
name: str | None = None,
234-
audit_flags: AuditFlag = AuditFlag.NONE,
235-
cache_dir=None,
236-
cache_locations=None,
237-
inputs: ty.Text | File | dict[str, ty.Any] | None = None,
238-
cont_dim=None,
239-
messenger_args=None,
240-
messengers=None,
241-
rerun=False,
242-
**kwargs,
243-
):
244-
self._check_for_unset_values()
245-
task = self.Task(
246-
self,
247-
name=name,
248-
audit_flags=audit_flags,
249-
cache_dir=cache_dir,
250-
cache_locations=cache_locations,
251-
inputs=inputs,
252-
cont_dim=cont_dim,
253-
messenger_args=messenger_args,
254-
messengers=messengers,
255-
rerun=rerun,
256-
)
257-
return task(**kwargs)
258-
259-
def _check_for_unset_values(self):
260-
if unset := [
261-
k
262-
for k, v in attrs.asdict(self, recurse=False).items()
263-
if v is attrs.NOTHING
264-
]:
265-
raise ValueError(
266-
f"The following values {unset} in the {self!r} interface need to be set "
267-
"before the workflow can be constructed"
268-
)
269-
270-
271159
def extract_fields_from_class(
272160
klass: type,
273161
arg_type: type[Arg],
@@ -352,7 +240,7 @@ def get_fields(klass, field_type, auto_attribs, helps) -> dict[str, Field]:
352240

353241

354242
def make_task_spec(
355-
task_type: type[Task],
243+
task_type: type["Task"],
356244
inputs: dict[str, Arg],
357245
outputs: dict[str, Out],
358246
klass: type | None = None,
@@ -389,6 +277,8 @@ def make_task_spec(
389277
klass : type
390278
The class created using the attrs package
391279
"""
280+
from pydra.engine.specs import TaskSpec
281+
392282
if name is None and klass is not None:
393283
name = klass.__name__
394284
outputs_klass = make_outputs_spec(outputs, outputs_bases, name)
@@ -457,7 +347,7 @@ def make_task_spec(
457347

458348
def make_outputs_spec(
459349
outputs: dict[str, Out], bases: ty.Sequence[type], spec_name: str
460-
) -> type[OutputsSpec]:
350+
) -> type["OutputsSpec"]:
461351
"""Create an outputs specification class and its outputs specification class from the
462352
output fields provided to the decorator/function.
463353
@@ -478,6 +368,8 @@ def make_outputs_spec(
478368
klass : type
479369
The class created using the attrs package
480370
"""
371+
from pydra.engine.specs import OutputsSpec
372+
481373
if not any(issubclass(b, OutputsSpec) for b in bases):
482374
outputs_bases = bases + (OutputsSpec,)
483375
if reserved_names := [n for n in outputs if n in RESERVED_OUTPUT_NAMES]:
@@ -880,16 +772,6 @@ def split_block(string: str) -> ty.Generator[str, None, None]:
880772
yield block.strip()
881773

882774

883-
def list_fields(interface: TaskSpec) -> list[Field]:
884-
if not attrs.has(interface):
885-
return []
886-
return [
887-
f.metadata[PYDRA_ATTR_METADATA]
888-
for f in attrs.fields(interface)
889-
if PYDRA_ATTR_METADATA in f.metadata
890-
]
891-
892-
893775
def check_explicit_fields_are_none(klass, inputs, outputs):
894776
if inputs is not None:
895777
raise ValueError(
@@ -918,5 +800,3 @@ def nothing_factory():
918800

919801

920802
white_space_re = re.compile(r"\s+")
921-
922-
PYDRA_ATTR_METADATA = "__PYDRA_METADATA__"

pydra/design/python.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
import inspect
33
import attrs
44
from pydra.engine.task import FunctionTask
5+
from pydra.engine.specs import TaskSpec
56
from .base import (
67
Arg,
78
Out,
89
ensure_field_objects,
910
make_task_spec,
10-
TaskSpec,
1111
parse_doc_string,
1212
extract_function_inputs_and_outputs,
1313
check_explicit_fields_are_none,

pydra/design/shell.py

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@
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
1415
from .base import (
1516
Arg,
1617
Out,
1718
check_explicit_fields_are_none,
1819
extract_fields_from_class,
1920
ensure_field_objects,
20-
TaskSpec,
2121
make_task_spec,
2222
EMPTY,
2323
)
24-
from pydra.utils.typing import is_fileset_or_union
25-
from pydra.engine.specs import MultiInputObj
24+
from pydra.utils.typing import is_fileset_or_union, MultiInputObj
2625
from pydra.engine.task import ShellCommandTask
2726

27+
2828
__all__ = ["arg", "out", "outarg", "define"]
2929

3030

@@ -180,7 +180,6 @@ class outarg(Out, arg):
180180
If provided, the field is treated also as an output field and it is added to
181181
the output spec. The template can use other fields, e.g. {file1}. Used in order
182182
to create an output specification.
183-
184183
"""
185184

186185
path_template: str | None = attrs.field(default=None)
@@ -204,7 +203,35 @@ def define(
204203
auto_attribs: bool = True,
205204
name: str | None = None,
206205
) -> TaskSpec:
207-
"""Create a shell command interface
206+
"""Create a task specification for a shell command. Can be used either as a decorator on
207+
the "canonical" dataclass-form of a task specification or as a function that takes a
208+
"shell-command template string" of the form
209+
210+
```
211+
shell.define("command <input1> <input2> --output <out|output1>")
212+
```
213+
214+
Fields are inferred from the template if not provided. In the template, inputs are
215+
specified with `<fieldname>` and outputs with `<out:fieldname>`.
216+
217+
```
218+
my_command <myinput> <out|myoutput2>
219+
```
220+
221+
The types of the fields can be specified using their MIME like (see fileformats.core.from_mime), e.g.
222+
223+
```
224+
my_command <myinput:text/csv> <out|myoutput2:image/png>
225+
```
226+
227+
The template can also specify options with `-` or `--` followed by the option name
228+
and arguments with `<argname:type>`. The type is optional and will default to
229+
`generic/fs-object` if not provided for arguments and `field/text` for
230+
options. The file-formats namespace can be dropped for generic and field formats, e.g.
231+
232+
```
233+
another-command <input1:directory> <input2:int> --output <out|output1:text/csv>
234+
```
208235
209236
Parameters
210237
----------
@@ -221,6 +248,11 @@ def define(
221248
as they appear in the template
222249
name: str | None
223250
The name of the returned class
251+
252+
Returns
253+
-------
254+
TaskSpec
255+
The interface for the shell command
224256
"""
225257

226258
def make(
@@ -331,9 +363,10 @@ def parse_command_line_template(
331363
outputs: list[str | Out] | dict[str, Out | type] | None = None,
332364
) -> ty.Tuple[str, dict[str, Arg | type], dict[str, Out | type]]:
333365
"""Parses a command line template into a name and input and output fields. Fields
334-
are inferred from the template if not provided, where inputs are specified with `<fieldname>`
335-
and outputs with `<out:fieldname>`. The types of the fields can be specified using their
336-
MIME like (see fileformats.core.from_mime), e.g.
366+
are inferred from the template if not explicitly provided.
367+
368+
In the template, inputs are specified with `<fieldname>` and outputs with `<out:fieldname>`.
369+
The types of the fields can be specified using their MIME like (see fileformats.core.from_mime), e.g.
337370
338371
```
339372
my_command <myinput> <out|myoutput2>
@@ -345,7 +378,7 @@ def parse_command_line_template(
345378
options. The file-formats namespace can be dropped for generic and field formats, e.g.
346379
347380
```
348-
another-command <input1:directory> <input2:integer> --output <out|output1:text/csv>
381+
another-command <input1:directory> <input2:int> --output <out|output1:text/csv>
349382
```
350383
351384
Parameters
@@ -365,6 +398,13 @@ def parse_command_line_template(
365398
The input fields of the command line template
366399
outputs : dict[str, Out | type]
367400
The output fields of the command line template
401+
402+
Raises
403+
------
404+
ValueError
405+
If an unknown token is found in the command line template
406+
TypeError
407+
If an unknown type is found in the command line template
368408
"""
369409
if isinstance(inputs, list):
370410
inputs = {arg.name: arg for arg in inputs}
@@ -437,9 +477,9 @@ def from_type_str(type_str) -> type:
437477
try:
438478
type_ = from_mime(f"generic/{tp}")
439479
except FormatRecognitionError:
440-
raise ValueError(
480+
raise TypeError(
441481
f"Found unknown type, {tp!r}, in command template: {template!r}"
442-
)
482+
) from None
443483
types.append(type_)
444484
if len(types) == 2 and types[1] == "...":
445485
type_ = MultiInputObj[types[0]]

pydra/design/tests/test_python.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from decimal import Decimal
44
import attrs
55
import pytest
6-
from pydra.design import list_fields, TaskSpec
6+
from pydra.engine.helpers import list_fields
7+
from pydra.engine.specs import TaskSpec
78
from pydra.design import python
89
from pydra.engine.task import FunctionTask
910

0 commit comments

Comments
 (0)