Skip to content

Commit 52a5187

Browse files
committed
fixed up print help and optional outputs
1 parent bb255b5 commit 52a5187

File tree

9 files changed

+121
-71
lines changed

9 files changed

+121
-71
lines changed

docs/source/tutorial/4-python.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@
163163
"metadata": {},
164164
"outputs": [],
165165
"source": [
166-
"from pydra.utils import task_help\n",
166+
"from pydra.utils import print_help\n",
167167
"\n",
168168
"\n",
169169
"@python.define(outputs=[\"c\", \"d\"])\n",
@@ -182,7 +182,7 @@
182182
" return a + b, a * b\n",
183183
"\n",
184184
"\n",
185-
"task_help(DocStrExample)"
185+
"print_help(DocStrExample)"
186186
]
187187
},
188188
{

docs/source/tutorial/5-shell.ipynb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@
134134
"metadata": {},
135135
"outputs": [],
136136
"source": [
137-
"from pydra.utils import task_help\n",
137+
"from pydra.utils import print_help\n",
138138
"\n",
139139
"Cp = shell.define(\n",
140140
" \"cp <in_fs_objects:fs-object+> <out|out_dir:directory> \"\n",
@@ -144,7 +144,7 @@
144144
" \"--tuple-arg <tuple_arg:int,str*> \"\n",
145145
")\n",
146146
"\n",
147-
"task_help(Cp)"
147+
"print_help(Cp)"
148148
]
149149
},
150150
{
@@ -275,7 +275,7 @@
275275
")\n",
276276
"\n",
277277
"\n",
278-
"task_help(Cp)"
278+
"print_help(Cp)"
279279
]
280280
},
281281
{
@@ -389,7 +389,7 @@
389389
"outputs": [],
390390
"source": [
391391
"from fileformats.generic import File\n",
392-
"from pydra.utils import task_help\n",
392+
"from pydra.utils import print_help\n",
393393
"\n",
394394
"ACommand = shell.define(\n",
395395
" \"a-command\",\n",
@@ -406,7 +406,7 @@
406406
" },\n",
407407
")\n",
408408
"\n",
409-
"task_help(ACommand)"
409+
"print_help(ACommand)"
410410
]
411411
},
412412
{

docs/source/tutorial/7-canonical-form.ipynb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"metadata": {},
4747
"outputs": [],
4848
"source": [
49-
"from pydra.utils import task_help\n",
49+
"from pydra.utils import print_help\n",
5050
"from pydra.compose import python\n",
5151
"\n",
5252
"\n",
@@ -78,7 +78,7 @@
7878
" return a + b, a / b\n",
7979
"\n",
8080
"\n",
81-
"task_help(CanonicalPythonTask)"
81+
"print_help(CanonicalPythonTask)"
8282
]
8383
},
8484
{
@@ -126,7 +126,7 @@
126126
" return a + b, a / b\n",
127127
"\n",
128128
"\n",
129-
"task_help(CanonicalPythonTask)"
129+
"print_help(CanonicalPythonTask)"
130130
]
131131
},
132132
{
@@ -175,7 +175,7 @@
175175
" out_file_size: int = shell.out(callable=get_file_size)\n",
176176
"\n",
177177
"\n",
178-
"task_help(CpWithSize)"
178+
"print_help(CpWithSize)"
179179
]
180180
},
181181
{

pydra/compose/base/helpers.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import attrs
44
import re
55
from copy import copy
6-
from pydra.utils.typing import is_type
6+
from pydra.utils.typing import is_type, is_optional
77
from pydra.utils.general import task_fields
88
from .field import Field, Arg, Out, NO_DEFAULT
99

@@ -74,6 +74,8 @@ def ensure_field_objects(
7474
name=input_name,
7575
help=input_helps.get(input_name, ""),
7676
)
77+
if is_optional(arg):
78+
inputs[input_name].default = None
7779
elif isinstance(arg, dict):
7880
arg_kwds = copy(arg)
7981
if "help" not in arg_kwds:
@@ -102,12 +104,14 @@ def ensure_field_objects(
102104
out.name = output_name
103105
if not out.help:
104106
out.help = output_helps.get(output_name, "")
105-
elif inspect.isclass(out) or ty.get_origin(out):
107+
elif is_type(out):
106108
outputs[output_name] = out_type(
107109
type=out,
108110
name=output_name,
109111
help=output_helps.get(output_name, ""),
110112
)
113+
if is_optional(out):
114+
outputs[output_name].default = None
111115
elif isinstance(out, dict):
112116
out_kwds = copy(out)
113117
if "help" not in out_kwds:

pydra/compose/base/task.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
attrs_values,
2020
)
2121
from pydra.utils.hash import Cache, hash_single, register_serializer
22-
from .field import Field, Arg, Out
22+
from .field import Field, Arg, Out, NO_DEFAULT
2323

2424

2525
if ty.TYPE_CHECKING:
@@ -60,17 +60,16 @@ def _from_task(cls, job: "Job[TaskType]") -> Self:
6060
outputs : Outputs
6161
The outputs of the job
6262
"""
63-
outputs = cls(
64-
**{
65-
f.name: (
66-
f.default.factory()
67-
if isinstance(f.default, attrs.Factory)
68-
else f.default
69-
)
70-
for f in attrs_fields(cls)
71-
if not f.name.startswith("_")
72-
}
73-
)
63+
defaults = {}
64+
for output in task_fields(cls):
65+
if output.default is NO_DEFAULT:
66+
default = attrs.NOTHING
67+
elif isinstance(output.default, attrs.Factory):
68+
default = output.default.factory()
69+
else:
70+
default = output.default
71+
defaults[output.name] = default
72+
outputs = cls(**defaults)
7473
outputs._output_dir = job.output_dir
7574
return outputs
7675

pydra/compose/python.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,7 @@ def _run(self, job: "Job[Task]", rerun: bool = True) -> None:
236236
# Run the actual function
237237
returned = self.function(**inputs)
238238
# Collect the outputs and save them into the job.return_values dictionary
239-
job.return_values = {f.name: f.default for f in task_fields(self.Outputs)}
240-
return_names = list(job.return_values)
239+
return_names = [f.name for f in task_fields(self.Outputs)]
241240
if returned is None:
242241
job.return_values = {nm: None for nm in return_names}
243242
elif len(return_names) == 1:

pydra/engine/tests/test_task.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,24 +92,26 @@ def TestFunc(a: int, b: float = 0.1) -> float:
9292

9393
help = task_help(funky)
9494
assert help == [
95-
"Help for TestFunc",
96-
"Input Parameters:",
95+
"Help for 'TestFunc' tasks",
96+
"-------------------------",
97+
"Inputs:",
9798
"- a: int",
9899
"- b: float (default: 0.1)",
99-
"Output Parameters:",
100+
"- function: Callable (default: TestFunc())",
101+
"Outputs:",
100102
"- out_out: float",
101103
]
102104

103105

104-
def test_annotated_func_dictreturn():
106+
def test_annotated_func_dictreturn(tmp_path: Path):
105107
"""Test mapping from returned dictionary to output definition."""
106108

107-
@python.define(outputs={"sum": int, "mul": ty.Optional[int]})
109+
@python.define(outputs={"sum": int, "mul": int | None})
108110
def TestFunc(a: int, b: int):
109111
return dict(sum=a + b, diff=a - b)
110112

111113
task = TestFunc(a=2, b=3)
112-
outputs = task()
114+
outputs = task(cache_dir=tmp_path)
113115

114116
# Part of the annotation and returned, should be exposed to output.
115117
assert outputs.sum == 5
@@ -150,10 +152,12 @@ def TestFunc(
150152

151153
help = task_help(funky)
152154
assert help == [
153-
"Help for TestFunc",
154-
"Input Parameters:",
155+
"Help for 'TestFunc' tasks",
156+
"-------------------------",
157+
"Inputs:",
155158
"- a: float",
156-
"Output Parameters:",
159+
"- function: Callable (default: TestFunc())",
160+
"Outputs:",
157161
"- fractional: float",
158162
"- integer: int",
159163
]
@@ -453,11 +457,13 @@ def TestFunc(a, b) -> int:
453457
help = task_help(funky)
454458

455459
assert help == [
456-
"Help for TestFunc",
457-
"Input Parameters:",
460+
"Help for 'TestFunc' tasks",
461+
"-------------------------",
462+
"Inputs:",
458463
"- a: Any",
459464
"- b: Any",
460-
"Output Parameters:",
465+
"- function: Callable (default: TestFunc())",
466+
"Outputs:",
461467
"- out: int",
462468
]
463469

@@ -494,11 +500,13 @@ def TestFunc(a, b) -> tuple[int, int]:
494500
help = task_help(funky)
495501

496502
assert help == [
497-
"Help for TestFunc",
498-
"Input Parameters:",
503+
"Help for 'TestFunc' tasks",
504+
"-------------------------",
505+
"Inputs:",
499506
"- a: Any",
500507
"- b: Any",
501-
"Output Parameters:",
508+
"- function: Callable (default: TestFunc())",
509+
"Outputs:",
502510
"- out1: int",
503511
"- out2: int",
504512
]

pydra/utils/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
fields_dict,
44
plot_workflow,
55
task_help,
6+
print_help,
67
)
78
from ._version import __version__
89

9-
__all__ = ["__version__", "task_fields", "plot_workflow", "task_help", "fields_dict"]
10+
__all__ = [
11+
"__version__",
12+
"task_fields",
13+
"plot_workflow",
14+
"task_help",
15+
"print_help",
16+
"fields_dict",
17+
]

pydra/utils/general.py

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -297,37 +297,69 @@ def from_list_if_single(obj: ty.Any) -> ty.Any:
297297
return obj
298298

299299

300-
def task_help(defn: "Task[TaskType]", to_list: bool = False) -> list[str] | None:
300+
def fold_text(text: str, width: int = 70) -> str:
301+
"""Fold text to a given width, respecting word boundaries."""
302+
if not isinstance(text, str):
303+
return text
304+
if len(text) <= width:
305+
return text
306+
lines = []
307+
for line in text.splitlines():
308+
while len(line) > width:
309+
words = line.split()
310+
split_line = ""
311+
for word in words:
312+
if len(split_line) + len(word) + 1 > width:
313+
lines.append(split_line.strip())
314+
split_line = ""
315+
split_line += word + " "
316+
lines.append(split_line.strip())
317+
lines.append(line)
318+
return "\n".join(lines)
319+
320+
321+
def task_help(task_type: "type[Task] | Task", line_width: int = 79) -> list[str] | None:
301322
"""Visit a job object and print its input/output interface."""
302-
from pydra.compose.base import NO_DEFAULT
303-
304-
lines = [f"Help for {defn.__class__.__name__}"]
305-
if task_fields(defn):
306-
lines += ["Input Parameters:"]
307-
for f in task_fields(defn):
308-
if (defn._task_type == "python" and f.name == "function") or (
309-
defn._task_type == "workflow" and f.name == "constructor"
310-
):
311-
continue
312-
default = ""
313-
if f.default is not NO_DEFAULT and not f.name.startswith("_"):
314-
default = f" (default: {f.default})"
315-
try:
316-
name = f.type.__name__
317-
except AttributeError:
318-
name = str(f.type)
319-
lines += [f"- {f.name}: {name}{default}"]
320-
output_klass = defn.Outputs
321-
if task_fields(output_klass):
322-
lines += ["Output Parameters:"]
323-
for f in task_fields(output_klass):
323+
from pydra.compose.base import Task, NO_DEFAULT
324+
325+
if isinstance(task_type, Task):
326+
task_type = type(task_type)
327+
328+
def field_listing(field: "Field") -> str:
329+
"""Get the field listing for a task."""
324330
try:
325-
name = f.type.__name__
331+
type_str = field.type.__name__
326332
except AttributeError:
327-
name = str(f.type)
328-
lines += [f"- {f.name}: {name}"]
329-
if to_list:
330-
return lines
333+
type_str = str(field.type)
334+
field_str = f"- {field.name}: {type_str}"
335+
if isinstance(field.default, attrs.Factory):
336+
field_str += f" (factory: {field.default.factory.__name__})"
337+
elif callable(field.default):
338+
field_str += f" (default: {field.default.__name__}())"
339+
elif field.default is not NO_DEFAULT:
340+
field_str += f" (default: {field.default})"
341+
if field.help:
342+
field_str += f"\n {fold_text(field.help, width=line_width - 4)}"
343+
return field_str
344+
345+
lines = [f"Help for '{task_type.__name__}' tasks"]
346+
lines.append("-" * len(lines[0]))
347+
inputs = task_fields(task_type)
348+
if inputs:
349+
lines.append("Inputs:")
350+
for inpt in inputs:
351+
lines.append(field_listing(inpt))
352+
outputs = task_fields(task_type.Outputs)
353+
if outputs:
354+
lines.append("Outputs:")
355+
for output in outputs:
356+
lines.append(field_listing(output))
357+
return lines
358+
359+
360+
def print_help(task: "Task[TaskType]") -> None:
361+
"""Print help for a task."""
362+
lines = task_help(task)
331363
print("\n".join(lines))
332364

333365

0 commit comments

Comments
 (0)