Replies: 5 comments 5 replies
-
On a tangent, wf.add currently returns my_shell_cmd = wf.add(
inputs=MyShellCmd.Inputs(x="foo", y="bar",...)
)
assert my_shell_cmd == wf.my_shell_cmd Returning the newly added tasks, should also make it possible (if I have thought it through properly) to get type-checking to work on lzout if we declared it to be of type task1 = wf.add(
IdentityShell(inputs=IdentityShell.Inputs(in_file="/path/tofoo")) # by templating the like Workflow.add(task: T) -> T the linter will know that task1 is of IdentityShell, whereas this wouldn't be possible with the current wf.identity_shell way of accessing the added task
)
task2 = wf.add(
IdentityShell(inputs=IdentityShell.Inputs(in_file=task1.lzout.out_file)) # lzout would be declared as being of type AShell.Outputs despite being a white lie
) |
Beta Was this translation helpful? Give feedback.
-
Another small tangent, how would people feel about aliasing "help_string" with "help" to make it consistent with argparse and click? Also, I think at the moment it is mandatory, which in most cases I agree with but I'm wondering whether it is just adding unnecessary work if it is obvious from the field name what the field is, e.g. "in_file" |
Beta Was this translation helpful? Give feedback.
-
Maybe something like this could be useful https://stackoverflow.com/questions/74103528/type-hinting-an-instance-of-a-nested-class |
Beta Was this translation helpful? Give feedback.
-
Playing around with this some more and integrating the suggestions in that stack overflow thread I mocked up this as an alternative syntax for creating workflows for people wanting the code to be type aware Inputs = ty.TypeVar("Inputs")
Outputs = ty.TypeVar("Outputs")
@attrs.define(auto_attribs=False)
class BaseTask(ty.Generic[Inputs, Outputs]):
@property
def lzout(self) -> Outputs:
return LzOut(self) # type: ignore
@classmethod
def add_to(cls, wf: pydra.Workflow, inputs: Inputs, name: ty.Optional[str] = None) -> Self:
if name is None:
name = cls.__name__
task = cls(inputs=inputs)
wf.add(task)
return task
inputs: Inputs = attrs.field()
class LzOut:
def __init__(self, task: BaseTask):
self.task = task
def __getattr__(self, name):
# tp = getattr(attrs.fields(self.task.Inputs), name).type
lf = LazyField(self.task, "output")
lf.field = name
return lf
class FunctionTask(BaseTask[Inputs, Outputs]):
function: ty.Callable
def __call__(self):
return self.function(**attrs.asdict(self.inputs))
@dataclass_transform()
def func_task(klass):
return attrs.define(auto_attribs=False, kw_only=True)(klass)
@func_task
class MyTask(FunctionTask["MyTask.Inputs", "MyTask.Outputs"]):
@staticmethod
def function(x: int, y: float) -> float:
return x * y
@attrs.define
class Inputs:
x: int = attrs.field()
y: float = attrs.field()
@attrs.define
class Outputs:
out: float = attrs.field()
# Not necessary for mypy but needed for pyright type-checking
inputs: Inputs
lzout: Outputs
wf = pydra.Workflow(name="my_wf", input_spec=["x"])
task1 = MyTask.add_to(wf, inputs=MyTask.Inputs(x=wf.lzin.x, y='bad'))
task2 = MyTask.add_to(wf, inputs=MyTask.Inputs(x=task1.lzout.out, y=task1.lzout.out), name="task2")
a: int = task1.inputs.y As in, mypy complains that the 'y' input to task1 is Having to use |
Beta Was this translation helpful? Give feedback.
-
Can't leave this alone, here is my latest attempt for a streamlined version, which I'm pretty happy with (note this could live side-by-side the existing workflow construction syntax) T = ty.TypeVar("T")
@attrs.define
class InBase(ty.Generic[T]):
@classmethod
def task_type(cls):
return cls.__args__[0] # pylint: disable=no-member
def attach(self, workflow: pydra.Workflow, name=None) -> T:
task_type = self.task_type()
if name is None:
name = task_type.__name__
task = task_type(inputs=self)
workflow.add(task)
return task
@attrs.define
class OutBase(ty.Generic[T]):
pass
@attrs.define(auto_attribs=False)
class BaseTask:
@property
def lzout(self):
return LzOut(self)
inputs: InBase = attrs.field()
lzout: OutBase # type: ignore[no-redef]
class LzOut:
def __init__(self, task: BaseTask):
self.task = task
def __getattr__(self, name):
# tp = getattr(attrs.fields(self.task.In), name).type
lf = LazyField(self.task, "output")
lf.field = name
return lf
class FunctionTask(BaseTask):
function: ty.Callable
def __call__(self):
return self.function(**attrs.asdict(self.inputs))
@dataclass_transform()
def func_task(klass):
return attrs.define(auto_attribs=False, kw_only=True, slots=False)(klass)
@func_task
class MyTask(FunctionTask):
@staticmethod
def function(x: int, y: float) -> float:
return x * y
@attrs.define
class In(InBase["MyTask"]):
x: int = attrs.field()
y: float = attrs.field()
@attrs.define
class Out(OutBase["MyTask"]):
out: float = attrs.field()
inputs: In
lzout: Out
wf = pydra.Workflow(name="my_wf", input_spec=["x"])
task1 = MyTask.In(x=wf.lzin.x, y="bad").attach(wf)
task2 = MyTask.In(x=task1.lzout.out, y=task1.lzout.out).attach(wf, name="task2")
a: int = task1.inputs.y |
Beta Was this translation helpful? Give feedback.
-
Might be a bit late in the day for this week's discussion but playing around with the shell and function task decorators has got me thinking about what it would take to make Pydra's type declarations lintable.
If we go with the proposal I put forward in #655, then the inputs and outputs would be defined in the
attrs
classesMyShellCommand.Inputs
andMyShellCommand.Outputs
. Seeing asattrs
supports the "dataclass transform" pattern, its init and setattr methods are type-checked. So if users wanted to utilise type-checking in their code we could possibly offer the following syntaxIf we do a bit of magic and insert a back-ref to the task class into the inputs in the
@shell_task
decorator then we could perhaps make it less verbose when adding a node to a workflow, e.g.The original syntax could continue to work as it is, but given VSCode+Pylance is now type-checking by default it will be a bit of a turn off if we can't offer a way to construct workflows not covered in red.
For function tasks we could perhaps offer the option of defining them as classes in a similar to ShellCommands, e.g.
It is a lot heavier than the elegant "task" decorator, but it also allows us to set the help strings properly and harmonises the shell and function task definitions.
We could still offer offer the existing
@task
decorator as a shortcut for small helper functions and disable the type-checking by putting in a lot oftyping.Any
declarations. But for larger library-style functions (e.g. using dipy or scipy) it might be worth going to the extra effort to declare the inputs/outputs in a type-checkable way.Beta Was this translation helpful? Give feedback.
All reactions