Skip to content

Commit 34061f4

Browse files
committed
expanded print_help to be more helpful and updated related tests
1 parent ec79a99 commit 34061f4

File tree

15 files changed

+331
-134
lines changed

15 files changed

+331
-134
lines changed

.github/workflows/ci-cd.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,10 @@ jobs:
277277
run:
278278
shell: bash -l {0}
279279
steps:
280-
- name: Install Pandoc for NBSphinx
280+
- name: Install Pandoc for NBSphinx and graphviz for workflow plotting (dot)
281281
run: |
282282
sudo apt-get update
283-
sudo apt-get install -y pandoc
283+
sudo apt-get install -y pandoc graphviz
284284
- name: Install Dependencies for virtual notifications in Adv.-Exec Tutorial
285285
run: |
286286
sudo apt update

docs/source/tutorial/6-workflow.ipynb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
" return mul.out\n",
6464
"\n",
6565
"\n",
66+
"print_help(BasicWorkflow)\n",
6667
"show_workflow(BasicWorkflow, figsize=(2, 2.5))"
6768
]
6869
},
@@ -116,6 +117,7 @@
116117
" return output_video # test implicit detection of output name\n",
117118
"\n",
118119
"\n",
120+
"print_help(ShellWorkflow)\n",
119121
"show_workflow(ShellWorkflow, figsize=(2.5, 3))"
120122
]
121123
},
@@ -149,6 +151,7 @@
149151
" return sum.out\n",
150152
"\n",
151153
"\n",
154+
"print_help(SplitWorkflow)\n",
152155
"show_workflow(SplitWorkflow, figsize=(2, 2.5))"
153156
]
154157
},
@@ -173,6 +176,7 @@
173176
" return sum.out\n",
174177
"\n",
175178
"\n",
179+
"print_help(SplitThenCombineWorkflow)\n",
176180
"show_workflow(SplitThenCombineWorkflow, figsize=(3, 3.5))"
177181
]
178182
},
@@ -230,6 +234,7 @@
230234
" return output_video # test implicit detection of output name\n",
231235
"\n",
232236
"\n",
237+
"print_help(ConditionalWorkflow)\n",
233238
"show_workflow(ConditionalWorkflow(watermark_dims=(10, 10)), figsize=(2.5, 3))"
234239
]
235240
},
@@ -349,6 +354,7 @@
349354
" return handbrake.out_video\n",
350355
"\n",
351356
"\n",
357+
"print_help(TypeErrorWorkflow)\n",
352358
"show_workflow(TypeErrorWorkflow, plot_type=\"detailed\")"
353359
]
354360
},
@@ -404,6 +410,7 @@
404410
" return mul.out, divide.divided\n",
405411
"\n",
406412
"\n",
413+
"print_help(DirectAccesWorkflow)\n",
407414
"show_workflow(DirectAccesWorkflow(b=1), plot_type=\"detailed\")"
408415
]
409416
},
@@ -447,6 +454,7 @@
447454
" wf.outputs.out2 = divide.divided\n",
448455
"\n",
449456
"\n",
457+
"print_help(SetOutputsOfWorkflow)\n",
450458
"show_workflow(SetOutputsOfWorkflow(b=3), plot_type=\"detailed\")"
451459
]
452460
},

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@
195195
"outputs": [],
196196
"source": [
197197
"from pydra.compose import python, workflow\n",
198+
"from pydra.utils import print_help, show_workflow\n",
198199
"\n",
199200
"\n",
200201
"# Example python tasks\n",
@@ -233,7 +234,8 @@
233234
" out: float\n",
234235
"\n",
235236
"\n",
236-
"print_help(CanonicalWorkflowTask)"
237+
"print_help(CanonicalWorkflowTask)\n",
238+
"show_workflow(CanonicalWorkflowTask)"
237239
]
238240
}
239241
],

pydra/compose/base/builder.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import attrs.validators
66
from pydra.utils.typing import TypeParser, is_optional, is_fileset_or_union
77
import attrs
8-
from .field import NO_DEFAULT
98
from .task import Task, Outputs
109
from pydra.utils.hash import hash_function
1110
from pydra.utils.general import (
@@ -123,12 +122,12 @@ def build_task_class(
123122
if getattr(arg, "path_template", False):
124123
if is_optional(arg.type):
125124
field_type = Path | bool | None
126-
if arg.default is NO_DEFAULT:
125+
if arg.mandatory: # provide default if one is not provided
127126
attrs_kwargs["default"] = True if arg.requires else None
128127
del attrs_kwargs["factory"]
129128
else:
130129
field_type = Path | bool
131-
if arg.default is NO_DEFAULT:
130+
if arg.mandatory: # provide default if one is not provided
132131
attrs_kwargs["default"] = True # use the template by default
133132
del attrs_kwargs["factory"]
134133
elif is_optional(arg.type):
@@ -316,7 +315,7 @@ def allowed_values_validator(_, attribute, value):
316315

317316
def _get_attrs_kwargs(field: Field) -> dict[str, ty.Any]:
318317
kwargs = {}
319-
if field.default is not NO_DEFAULT:
318+
if not field.mandatory:
320319
kwargs["default"] = field.default
321320
# elif is_optional(field.type):
322321
# kwargs["default"] = None

pydra/compose/base/field.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
from typing import Self
44
import attrs.validators
55
from attrs.converters import default_if_none
6+
from fileformats.core import to_mime
67
from fileformats.generic import File, FileSet
7-
from pydra.utils.typing import TypeParser, is_optional, is_type
8-
from pydra.utils.general import task_fields
8+
from pydra.utils.typing import TypeParser, is_optional, is_type, is_union
9+
from pydra.utils.general import task_fields, wrap_text
910
import attrs
1011

1112
if ty.TYPE_CHECKING:
@@ -234,6 +235,64 @@ def _requires_validator(self, _, value):
234235
f"with None) or boolean, not type {self.type} ({self!r})"
235236
)
236237

238+
def markdown_listing(
239+
self, line_width: int = 79, help_indent: int = 4, **kwargs
240+
) -> str:
241+
"""Get the listing for the field in markdown-like format
242+
243+
Parameters
244+
----------
245+
line_width: int
246+
The maximum line width for the output, by default it is 79
247+
help_indent: int
248+
The indentation for the help text, by default it is 4
249+
250+
Returns
251+
-------
252+
str
253+
The listing for the field in markdown-like format
254+
"""
255+
256+
def type_to_str(type_: ty.Type[ty.Any]) -> str:
257+
if type_ is type(None):
258+
return "None"
259+
if is_union(type_):
260+
return " | ".join(
261+
type_to_str(t) for t in ty.get_args(type_) if t is not None
262+
)
263+
try:
264+
type_str = to_mime(type_, official=False)
265+
except Exception:
266+
if origin := ty.get_origin(type_):
267+
type_str = f"{origin.__name__}[{', '.join(map(type_to_str, ty.get_args(type_)))}]"
268+
else:
269+
try:
270+
type_str = type_.__name__
271+
except AttributeError:
272+
type_str = str(type_)
273+
return type_str
274+
275+
s = f"- {self.name}: {type_to_str(self.type)}"
276+
if isinstance(self.default, attrs.Factory):
277+
s += f"; default-factory = {self.default.factory.__name__}()"
278+
elif callable(self.default):
279+
s += f"; default = {self.default.__name__}()"
280+
elif not self.mandatory:
281+
s += f"; default = {self.default!r}"
282+
if self._additional_descriptors(**kwargs):
283+
s += f" ({', '.join(self._additional_descriptors(**kwargs))})"
284+
if self.help:
285+
s += f"\n{wrap_text(self.help, width=line_width, indent_size=help_indent)}"
286+
return s
287+
288+
def _additional_descriptors(self, **kwargs) -> list[str]:
289+
"""Get additional descriptors for the field"""
290+
return []
291+
292+
def __lt__(self, other: "Field") -> bool:
293+
"""Compare two fields based on their position"""
294+
return self.name < other.name
295+
237296

238297
@attrs.define(kw_only=True)
239298
class Arg(Field):
@@ -273,6 +332,13 @@ class Arg(Field):
273332
copy_ext_decomp: File.ExtensionDecomposition = File.ExtensionDecomposition.single
274333
readonly: bool = False
275334

335+
def _additional_descriptors(self, **kwargs) -> list[str]:
336+
"""Get additional descriptors for the field"""
337+
descriptors = super()._additional_descriptors(**kwargs)
338+
if self.allowed_values:
339+
descriptors.append(f"allowed_values={self.allowed_values}")
340+
return descriptors
341+
276342

277343
@attrs.define(kw_only=True, slots=False)
278344
class Out(Field):

pydra/compose/base/helpers.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ def extract_function_inputs_and_outputs(
208208
for inpt_name, default in input_defaults.items():
209209
inpt = inputs[inpt_name]
210210
if isinstance(inpt, arg_type):
211-
if inpt.default is NO_DEFAULT:
211+
if inpt.mandatory:
212212
inpt.default = default
213213
elif inspect.isclass(inpt) or ty.get_origin(inpt):
214214
inputs[inpt_name] = arg_type(type=inpt, default=default)
@@ -283,10 +283,10 @@ def parse_doc_string(doc_str: str) -> tuple[dict[str, str], dict[str, str] | lis
283283
for return_val, return_help in re.findall(r":return (\w+): (.*)", doc_str):
284284
output_helps[return_val] = return_help
285285
google_args_match = re.match(
286-
r".*\n\s*Args:\n(.*)", doc_str, flags=re.DOTALL | re.MULTILINE
286+
r"(?:.*\n)?\s*Args:\n(.*)", doc_str, flags=re.DOTALL | re.MULTILINE
287287
)
288288
google_returns_match = re.match(
289-
r".*\n\s*Returns:\n(.*)", doc_str, flags=re.DOTALL | re.MULTILINE
289+
r"(?:.*\n)?\s*Returns:\n(.*)", doc_str, flags=re.DOTALL | re.MULTILINE
290290
)
291291
if google_args_match:
292292
args_str = google_args_match.group(1)
@@ -303,12 +303,14 @@ def parse_doc_string(doc_str: str) -> tuple[dict[str, str], dict[str, str] | lis
303303
return_help = white_space_re.sub(" ", return_help).strip()
304304
output_helps[return_name] = return_help
305305
numpy_args_match = re.match(
306-
r".*\n\s+Parameters\n\s*----------\s*\n(.*)",
306+
r"(?:.*\n)?\s+Parameters\n\s*----------\s*\n(.*)",
307307
doc_str,
308308
flags=re.DOTALL | re.MULTILINE,
309309
)
310310
numpy_returns_match = re.match(
311-
r".*\n\s+Returns\n\s*-------\s*\n(.*)", doc_str, flags=re.DOTALL | re.MULTILINE
311+
r"(?:.*\n)?\s+Returns\n\s*-------\s*\n(.*)",
312+
doc_str,
313+
flags=re.DOTALL | re.MULTILINE,
312314
)
313315
if numpy_args_match:
314316
args_str = numpy_args_match.group(1)

pydra/compose/base/task.py

Lines changed: 3 additions & 3 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, NO_DEFAULT
22+
from .field import Field, Arg, Out
2323

2424

2525
if ty.TYPE_CHECKING:
@@ -62,7 +62,7 @@ def _from_task(cls, job: "Job[TaskType]") -> Self:
6262
"""
6363
defaults = {}
6464
for output in task_fields(cls):
65-
if output.default is NO_DEFAULT:
65+
if output.mandatory:
6666
default = attrs.NOTHING
6767
elif isinstance(output.default, attrs.Factory):
6868
default = output.default.factory()
@@ -598,7 +598,7 @@ def _check_resolved(self):
598598

599599

600600
# def set_none_default_if_optional(field: Field) -> None:
601-
# if is_optional(field.type) and field.default is NO_DEFAULT:
601+
# if is_optional(field.type) and field.mandatory:
602602
# field.default = None
603603

604604

pydra/compose/shell/builder.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,6 @@
3333
from .task import Task, Outputs
3434

3535

36-
EXECUTABLE_HELP_STRING = (
37-
"the first part of the command, can be a string, "
38-
"e.g. 'ls', or a list, e.g. ['ls', '-l', 'dirname']"
39-
)
40-
41-
4236
@dataclass_transform(
4337
kw_only_default=True,
4438
field_specifiers=(field.out, field.outarg),
@@ -210,7 +204,7 @@ def make(
210204
position=0,
211205
default=executable,
212206
validator=attrs.validators.min_len(1),
213-
help=EXECUTABLE_HELP_STRING,
207+
help=Task.EXECUTABLE_HELP,
214208
)
215209

216210
# Set positions for the remaining inputs that don't have an explicit position

0 commit comments

Comments
 (0)