diff --git a/modules/data/Variable.py b/modules/data/Variable.py
index 62208a6..51413a5 100644
--- a/modules/data/Variable.py
+++ b/modules/data/Variable.py
@@ -76,12 +76,19 @@ def default(self) -> Option[A]:
@abstractmethod
def bind(self, val: A) -> 'BoundVariable[A]':
''' Bind a value to this Variable.
-
- This only works for unbound Variables; otherwise, a TypeError will
- be raised.
'''
... # pragma: no cover
+ def __eq__(self, other: 'Variable') -> bool:
+ return all([
+ self.name == other.name,
+ self.is_bound == other.is_bound,
+ self.desc == other.desc,
+ (self.val == other.val) if self.is_bound else True,
+ self.var_type == other.var_type,
+ self.default == other.default
+ ])
+
class BoundVariable[A](Variable):
''' A Variable that has already been given a value.
'''
@@ -116,11 +123,26 @@ def var_type(self) -> Option[type]:
def default(self) -> Option[A]:
return self.__default
+ def bind(self, val: A) -> 'BoundVariable[A]':
+ return BoundVariable(
+ self.name, val, self.var_type, self.desc, self.default
+ )
+
+ def __str__(self) -> str:
+ return ''.join([
+ 'BoundVariable',
+ f'[{self.var_type.val.__name__}]' if self.var_type != Nothing() else '',
+ f' {self.name}',
+ f' = {self.val}',
+ f' (default {self.default.val})' if self.default != Nothing() else '',
+ f': {self.desc.val}' if self.desc != Nothing() else ''
+ ])
+
class UnboundVariable[A](Variable):
''' A Variable which has not yet been given a value.
'''
- def __init__(self, name: str, var_type: Option[type], desc: Option[str] = Nothing(), default: Option[A] = Nothing()):
+ def __init__(self, name: str, var_type: Option[type] = Nothing(), desc: Option[str] = Nothing(), default: Option[A] = Nothing()):
self.__name = name
self.__type = var_type
self.__desc = desc
@@ -148,4 +170,18 @@ def var_type(self) -> Option[type]:
@property
def default(self) -> Option[A]:
- return self.__default
\ No newline at end of file
+ return self.__default
+
+ def bind(self, val: A) -> BoundVariable[A]:
+ return BoundVariable(
+ self.name, val, self.var_type, self.desc, self.default
+ )
+
+ def __str__(self) -> str:
+ return ''.join([
+ 'UnboundVariable',
+ f'[{self.var_type.val.__name__}]' if self.var_type != Nothing() else '',
+ f' {self.name}',
+ f' (default {self.default.val})' if self.default != Nothing() else '',
+ f': {self.desc.val}' if self.desc != Nothing() else ''
+ ])
\ No newline at end of file
diff --git a/modules/data/__init__.py b/modules/data/__init__.py
deleted file mode 100644
index 3b87fb2..0000000
--- a/modules/data/__init__.py
+++ /dev/null
@@ -1,65 +0,0 @@
-'''
- Chicory ML Workflow Manager
- Copyright (C) 2024 Alexis Maya-Isabelle Shuping
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-'''
-
-from abc import ABC, abstractmethod
-
-class DataStore[T](ABC):
- '''
- A write-once data-store.
- '''
- @abstractmethod
- def __init__(self, label: str, prev: DataStore = None): ...
-
- @property
- @abstractmethod
- def label(self) -> str: ...
-
- @property
- @abstractmethod
- def data(self) -> T: ...
-
- @property
- @abstractmethod
- def prev(self) -> DataStore|None: ...
-
- def __iter__(self):
- cur = self
- while(cur is not None):
- yield cur
- cur = cur.prev
-
-
- Nothing = NoneDataStore('Nothing')
-
-class NoneDataStore(DataStore):
- def __init__(self, label: str, prev: DataStore = None):
- self.__label = label
- self.__prev = prev
-
- @property
- def label(self) -> str:
- return self.__label
-
- @property
- def data(self) -> None:
- return None
-
- @property
- def prev(self) -> DataStore|None:
- return self.__prev
-
diff --git a/modules/types/ComparableError.py b/modules/types/ComparableError.py
index 6e036ac..d80b6a5 100644
--- a/modules/types/ComparableError.py
+++ b/modules/types/ComparableError.py
@@ -58,6 +58,10 @@ def __eq__(self, other):
- The two `args` attributes evaluate to equal.
'''
other = ComparableError.encapsulate(other) # ensure `other` is comparable
+
+ if type(other) != ComparableError:
+ return False
+
if type(self.exc) != type(other.exc):
return False
diff --git a/modules/types/Either.py b/modules/types/Either.py
index 286aac3..51290d4 100644
--- a/modules/types/Either.py
+++ b/modules/types/Either.py
@@ -79,6 +79,9 @@ def __eq__(self, other) -> bool:
''' Returns True iff both Eithers being compared are the same type (i.e.
both Left or both Right) AND the relevant inner values are the same.
'''
+ if not issubclass(type(other), Either):
+ return False
+
if self.is_right:
if not other.is_right:
return False
diff --git a/modules/types/Option.py b/modules/types/Option.py
index 3f03beb..926d6d1 100644
--- a/modules/types/Option.py
+++ b/modules/types/Option.py
@@ -53,6 +53,9 @@ def map(self, f: Callable[[A], B]) -> 'Option[B]':
of the result.
If it is `Nothing`, do not call `f` and just return `Nothing`.
+
+ :param: f: A function that takes an `A` and converts it to a `B`
+ :returns: `Some(f(a))` if this Option is `Some(a)`, else `Nothing()`
'''
... # pragma: no cover
@@ -60,11 +63,16 @@ def map(self, f: Callable[[A], B]) -> 'Option[B]':
def flat_map(self, f: Callable[[A], 'Option[B]']) -> 'Option[B]':
''' Similar to Map, except that `f` should convert `A`'s directly into
`Option[B]`'s.
-
+
+ :param f: A function that takes an `A` and converts it to an
+ `Option[B]`
+ :returns: `f(a)` if this `Option` is `Some(a)`, else `Nothing()`
'''
... # pragma: no cover
def __eq__(self, other):
+ if not issubclass(type(other), Option):
+ return False
if self.has_val:
if other.has_val:
return ComparableError.encapsulate(self.val) == ComparableError.encapsulate(other.val)
@@ -77,6 +85,7 @@ def __eq__(self, other):
return True
class Nothing(Option):
+ ''' Represents an empty `Option`.'''
@property
def has_val(self) -> bool:
return False
@@ -95,6 +104,7 @@ def __str__(self):
return f'Nothing'
class Some[A](Option):
+ ''' Represents an `Option` that contains a concrete value. '''
def __init__(self, val: A):
self.__val = val
diff --git a/modules/types/Result.py b/modules/types/Result.py
index d3eec8e..a0e46db 100644
--- a/modules/types/Result.py
+++ b/modules/types/Result.py
@@ -104,6 +104,9 @@ def flat_map(self, f: Callable[[C], 'Result[A, B, D]']) -> 'Result[A, B, D]':
... # pragma: no cover
def __eq__(self, other):
+ if not issubclass(type(other), Result):
+ return False
+
if self.is_okay:
if not other.is_okay:
return False
diff --git a/modules/workflow/Workflow.py b/modules/workflow/Workflow.py
new file mode 100644
index 0000000..0c9e41d
--- /dev/null
+++ b/modules/workflow/Workflow.py
@@ -0,0 +1,100 @@
+'''
+ Chicory ML Workflow Manager
+ Copyright (C) 2024 Alexis Maya-Isabelle Shuping
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+'''
+
+import logging
+import random
+
+from modules.data.Variable import Variable, UnboundVariable
+from modules.types.Option import Nothing, Some
+
+log = logging.get_logger(__name__)
+
+class Workflow():
+ ''' Base class for Workflows.
+
+ Instead of instantiating this directly, use `MakeWorkflow`.
+ '''
+ def __init__(self, stages, inputs=[], outputs=[]):
+ pass
+
+class MakeWorkflow():
+ ''' Workflow factory. Use this class to construct a Workflow that can then
+ be instantiated.
+ '''
+
+ def __init__(self, name=None):
+ if name is None:
+ name = f'Unnamed Workflow {random.randrange(0, 999999):06}'
+
+ self.name = name
+ self.stages = []
+ self.inputs = []
+ self.outputs = []
+
+ def given(self, *args, **kwargs):
+ ''' Add one or more inputs to the Workflow.
+
+ Arguments should either be `Variable`s or strings. In the latter
+ case, appropriate variables are created for the strings.
+
+ Keyword arguments may be provided - these are interpreted as default
+ values for the variable. For example, providing `my_var=1` creates a
+ new `UnboundVariable` called `my_var` with a default value of `1`.
+
+ Keyword argument names must, of course, be strings. To provide a
+ pre-existing `Variable` with a default value, the default must be
+ provided in the `Variable`'s instantiation
+ '''
+ for arg in args:
+ if issubclass(type(arg), Variable):
+ self.inputs.append(arg)
+ elif type(arg) is str:
+ self.inputs.append(UnboundVariable(arg))
+ else:
+ raise TypeError('Workflow inputs must be `Variable`s or strings.')
+
+ for kwarg, kwdefault in kwargs.items():
+ self.inputs.append(UnboundVariable(
+ arg,
+ default=Some(kwdefault)
+ ))
+
+ def output(self, *args):
+ ''' Add one or more outputs to the Workflow.
+
+ Arguments should either be `Variable`s or strings. In the latter
+ case, wiring will search for the latest `Variable` with the provided
+ name.
+ '''
+ for arg in args:
+ if issubclass(type(arg), Variable):
+ self.outputs.append(arg)
+ elif type(arg) is str:
+ self.outputs.append(UnboundVariable(arg))
+ else:
+ raise TypeError('Workflow outputs must be `Variable`s or strings.')
+
+ def from_stages(self, *args):
+ ''' Add stages to the Workflow.
+ '''
+ self.stages = args
+
+ def __call__(self, *args, **kwargs):
+ ''' Instantiate the Workflow and bind the provided arguments to its
+ inputs.
+ '''
\ No newline at end of file
diff --git a/modules/workflow/stage/Stage.py b/modules/workflow/stage/Stage.py
new file mode 100644
index 0000000..ed1bd15
--- /dev/null
+++ b/modules/workflow/stage/Stage.py
@@ -0,0 +1,140 @@
+'''
+ Chicory ML Workflow Manager
+ Copyright (C) 2024 Alexis Maya-Isabelle Shuping
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+'''
+
+from modules.data.Variable import Variable, UnboundVariable
+from modules.types.Option import Option, Some, Nothing
+
+from dataclasses import dataclass
+from typing import Callable
+from abc import ABC, abstractmethod
+
+def _process_stage_io(
+ dex: int,
+ i: Variable,
+ args: list[Variable|str],
+ kwargs: dict[str, Variable|str]
+) -> Option[Variable]:
+ ''' Match a single input (or output) from our variable list to the provided
+ parameters.
+
+ :param dex: Offset in our input `Variable` list for `i`
+ :param i: Input `Variable` to match
+ :param args: Positional function args to match against
+ :param kwargs: Keyword function args to match against
+ :returns: The matching `Variable` if found, or `Nothing` if not.
+ '''
+ def __atov(a: Variable|str) -> Variable:
+ if type(a) == str:
+ return UnboundVariable(a)
+ else:
+ return a
+
+ if args and len(args) > dex:
+ return Some(__atov(args[dex]))
+ elif kwargs and i.name in kwargs.keys():
+ return Some(__atov(kwargs[i.name]))
+ else:
+ return Nothing()
+
+class Stage(ABC):
+ ''' Represents a Stage - something that can be used as a step in a Workflow.
+
+ Stages are immutable - their inputs (and outputs, if applicable), do not
+ change once set. Functions modifying Stages will return a new Stage with
+ the desired modifications, rather than modifying the existing Stage
+ in-place.
+ '''
+
+ input_vars: tuple[UnboundVariable] = ()
+ output_vars: tuple[UnboundVariable] = ()
+
+ def __init__(self,
+ *args: list[str|Variable],
+ _to: tuple[list[str|Variable], dict[str, str|Variable]]=([], {}),
+ **kwargs: dict[str, str|Variable]
+ ):
+ ''' Create a Stage and bind its input variables.
+
+ All arguments should be either `Variable`s or strings. Strings are
+ automatically converted to `UnboundVariable`s. Keyword arguments are
+ matched by name; positional arguments are matched by position.
+
+ :param _to: Internal-use constructor for supplying output variable
+ information.
+ '''
+ self.__inputs = tuple([
+ _process_stage_io(dex, i, args, kwargs)
+ for dex, i in enumerate(self.input_vars)
+ ])
+
+ self.__outputs = tuple([
+ _process_stage_io(dex, i, _to[0], _to[1])
+ for dex, i in enumerate(self.output_vars)
+ ])
+
+ self.__args = tuple(args)
+ self.__kwargs = tuple(kwargs)
+ self.__to = _to
+
+ @abstractmethod
+ def run(self, inputs: list[Variable]) -> 'StageResult':
+ ''' Run this stage, returning a StageResult containing output
+ information.
+
+ :param inputs: A list of all `Variables` needed for this `Stage` to
+ run.
+ :returns: A `StageResult` containing output `Variable`s for this
+ `Stage`.
+ '''
+ ...
+
+ @property
+ def _input_bindings(self) -> list[Variable]:
+ ''' A list of Input bindings for this stage. Used for internal wiring.
+ '''
+ return self.__inputs
+
+ @property
+ def _output_bindings(self) -> Option[list[Variable]]:
+ ''' An `Option`al list of Output bindings for this stage. Used for
+ internal wiring.
+ '''
+ return self.__outputs
+
+ def to(self, *args, **kwargs) -> 'Stage':
+ ''' Bind the outputs of the `Stage` to a `Variable` or name.
+
+ :param args: Outputs to be bound by position
+ :params kwargs: Outputs to be bound by name
+ :returns: A copy of this `Stage` with the outputs bound.
+ '''
+ _to = (args, kwargs)
+
+ return type(self)(
+ _to=_to,
+ *self.__args,
+ **self.__kwargs
+ )
+
+
+@dataclass(frozen=True)
+class StageResult():
+ ''' Holds the result of an executed Stage.
+ '''
+ stage: Stage
+ outputs: tuple[Variable]
\ No newline at end of file
diff --git a/modules/workflow/stage/StageFromFunction.py b/modules/workflow/stage/StageFromFunction.py
new file mode 100644
index 0000000..ad7ba72
--- /dev/null
+++ b/modules/workflow/stage/StageFromFunction.py
@@ -0,0 +1,106 @@
+'''
+ Chicory ML Workflow Manager
+ Copyright (C) 2024 Alexis Maya-Isabelle Shuping
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+'''
+
+from modules.data.Variable import Variable, UnboundVariable
+from modules.types.Option import Option, Some, Nothing
+from modules.workflow.stage.Stage import Stage, StageResult
+
+from abc import ABC, abstractmethod
+import inspect
+from inspect import Parameter, Signature
+from typing import Callable
+
+def _input_list_to_kwargs(input_list: tuple[Variable]) -> dict[str, any]:
+ kwargs = {}
+ for v in input_list:
+ kwargs[v.name] = v.val
+
+ return kwargs
+
+class StageFromFunction(Stage):
+ ''' A `Stage` constructed automatically from a function.
+
+ To construct a `StageFromFunction`, use `make_StageFromFunction`.
+ '''
+
+ @abstractmethod
+ def inner_function(self, *args, **kwargs):
+ ''' The actual function encapsulated in a StageFromFunction.
+ '''
+ ... #pragma: no cover
+
+ def run(self, inputs: tuple[Variable]) -> StageResult:
+ rval = self.inner_function(**_input_list_to_kwargs(inputs))
+
+ return StageResult(
+ self,
+ (self.output_vars[0].bind(rval),) if len(self.output_vars) > 0 else ()
+ )
+
+
+def _extract_func_args_and_ret(fn: Callable) -> tuple[list[Variable], Option[Variable]]:
+ ''' Extract the arguments and (if present) return-type annotation from a
+ function.
+
+ :param fn: The function to extract data from.
+ :returns: A tuple whose first element is the list of input `Variable`s
+ and the second element is an `Option`al return-type `Variable`
+ '''
+ sig = inspect.signature(fn)
+
+ args = [
+ UnboundVariable(
+ param.name,
+ Some(param.annotation) if param.annotation != Parameter.empty else Nothing(),
+ default=Some(param.default) if param.default != Parameter.empty else Nothing()
+ ) for param in sig.parameters.values()
+ ]
+
+ ret = Some(UnboundVariable(
+ 'return',
+ Some(sig.return_annotation)
+ )) if sig.return_annotation != Signature.empty else Nothing()
+
+ return (args, ret)
+
+
+def make_StageFromFunction(fn: Callable) -> type[StageFromFunction]:
+ ''' Return a concrete subclass of `StageFromFunction`, with its inner
+ function set to the provided `fn`. Unlike `StageFromFunction` itself,
+ these subclasses can be directly instantiated.
+
+ :param fn: The function to convert.
+ :returns: A concrete subclass of `StageFromFunction`
+ '''
+ args, ret = _extract_func_args_and_ret(fn)
+
+ inputs = tuple(args)
+ outputs = (ret.val,) if ret != Nothing() else ()
+
+ print(f'inputs: {inputs}')
+ print(f'outputs: {outputs}')
+
+ return type(
+ f'StageFromFunction_{fn.__name__}_{hash(fn)}',
+ (StageFromFunction,),
+ {
+ 'input_vars': inputs,
+ 'output_vars': outputs,
+ 'inner_function': lambda self, *args, **kwargs: fn(*args, **kwargs)
+ }
+ )
\ No newline at end of file
diff --git a/test/types/test_ComparableError.py b/test/types/test_ComparableError.py
index 2f90deb..8966aab 100644
--- a/test/types/test_ComparableError.py
+++ b/test/types/test_ComparableError.py
@@ -21,6 +21,7 @@ def test_eq():
assert ComparableError(TypeError("test")) == ComparableError(TypeError("test"))
assert ComparableError(TypeError("test")) != ComparableError(TypeError("test2"))
assert ComparableError(TypeError("test")) != ComparableError(ArithmeticError("test"))
+ assert ComparableError(TypeError("test")) != "test"
def test_auto_encapsulate():
assert ComparableError(TypeError("test")) == TypeError("test")
diff --git a/test/types/test_Either.py b/test/types/test_Either.py
index 52ee904..ab90c7a 100644
--- a/test/types/test_Either.py
+++ b/test/types/test_Either.py
@@ -20,10 +20,12 @@ def test_equals():
assert Left(12) == Left(12)
assert Left(12) != Left(13)
assert Left(12) != Left("12")
+ assert Left(12) != 12
assert Right(12) == Right(12)
assert Right(12) != Right(13)
assert Right(12) != Right("12")
+ assert Right(12) != 12
assert Right(12) != Left(12)
assert Left(12) != Right(12)
diff --git a/test/types/test_Option.py b/test/types/test_Option.py
index afa4740..af05d47 100644
--- a/test/types/test_Option.py
+++ b/test/types/test_Option.py
@@ -29,6 +29,7 @@ def test_eq():
assert Some(None) == Some(None)
assert Some(37) != Some(36)
+ assert Some(37) != 37
assert Some("test") != Some("tes")
assert Some(37) != Some("37")
@@ -39,6 +40,7 @@ def test_eq():
assert Nothing() != Some(37)
assert Nothing() != Some("test")
assert Nothing() != Some(None)
+ assert Nothing() != None
assert Nothing() == Nothing()
diff --git a/test/types/test_Result.py b/test/types/test_Result.py
index 49ccc44..22c46e9 100644
--- a/test/types/test_Result.py
+++ b/test/types/test_Result.py
@@ -51,17 +51,27 @@ def test_eq():
assert Okay(37) != Okay(36)
assert Okay(37) != Okay("37")
+ assert Okay(37) != 37
assert Warning([ArithmeticError(6)], 37) != Warning([ArithmeticError(6)], 36)
assert Warning([ArithmeticError(6)], 37) != Warning([ArithmeticError(6)], "37")
assert Warning([ArithmeticError(6)], 37) != Warning([SyntaxError(6)], 37)
assert Warning([ArithmeticError(6)], 37) != Warning([ArithmeticError(5)], 37)
+ assert Warning([ArithmeticError(6)], 37) != 37
+ assert Warning([], 37) != 37
+ assert Warning([ArithmeticError(6)], 37) != ArithmeticError(6)
assert Error([SyntaxError("test")], [ArithmeticError(6)]) != Error([SyntaxError("test")], [ArithmeticError(5)])
assert Error([SyntaxError("test")], [ArithmeticError(6)]) != Error([ArithmeticError(6)], [SyntaxError("test")])
assert Error([SyntaxError("test")], [ArithmeticError(6)]) != Error([SyntaxError("test")], [ArithmeticError(5)])
assert Error([SyntaxError("test")], [ArithmeticError(6)]) != Error([SyntaxError("test")], [ArithmeticError("6")])
assert Error([SyntaxError("test")], [ArithmeticError(6)]) != Error([SyntaxError("test2")], [ArithmeticError(6)])
+ assert Error([SyntaxError("test")], [ArithmeticError(6)]) != SyntaxError("test")
+ assert Error([SyntaxError("test")], [ArithmeticError(6)]) != [SyntaxError("test")]
+ assert Error([SyntaxError("test")], [ArithmeticError(6)]) != "test"
+ assert Error([SyntaxError("test")], [ArithmeticError(6)]) != ArithmeticError(6)
+ assert Error([SyntaxError("test")], [ArithmeticError(6)]) != [ArithmeticError(6)]
+ assert Error([SyntaxError("test")], [ArithmeticError(6)]) != 6
assert Okay(37) != Warning([ArithmeticError(6)], 37)
assert Okay(37) != Error([SyntaxError()], [ArithmeticError(6)])
diff --git a/test/workflow/stage/test_StageFromFunction.py b/test/workflow/stage/test_StageFromFunction.py
new file mode 100644
index 0000000..1ad3f9b
--- /dev/null
+++ b/test/workflow/stage/test_StageFromFunction.py
@@ -0,0 +1,54 @@
+
+from modules.data.Variable import BoundVariable, UnboundVariable
+from modules.workflow.stage.StageFromFunction import StageFromFunction, make_StageFromFunction
+from modules.types.Option import Some
+
+def test_basic_function():
+ def fn(x: int) -> int:
+ print('calling inner function')
+ return 2*x
+
+ stage = make_StageFromFunction(fn)(
+ UnboundVariable('x')
+ )
+
+ res = stage.run(
+ (BoundVariable('x', 2),)
+ )
+
+ assert len(res.outputs) == 1
+ assert res.outputs[0].val == 4
+
+def test_input_types():
+ def fn(x: int, y: str, z = 'test') -> bool:
+ return True
+
+ stage_cls = make_StageFromFunction(fn)
+
+ print(f'input_vars: {stage_cls.input_vars}')
+
+ [print(var) for var in stage_cls.input_vars]
+
+ assert stage_cls.input_vars == (
+ UnboundVariable('x', Some(int)),
+ UnboundVariable('y', Some(str)),
+ UnboundVariable('z', default=Some('test'))
+ )
+
+ assert stage_cls.output_vars == (UnboundVariable('return', Some(bool)),)
+
+def test_side_effecting_function():
+ global fn_run_count
+ fn_run_count = 0
+ def fn():
+ global fn_run_count
+ fn_run_count += 1
+
+
+ stage_cls = make_StageFromFunction(fn)
+
+ assert stage_cls.input_vars == ()
+ assert stage_cls.output_vars == ()
+
+ assert len(stage_cls().run(()).outputs) == 0
+ assert fn_run_count == 1
\ No newline at end of file