Skip to content

Commit

Permalink
Work on Variable and Stage systems
Browse files Browse the repository at this point in the history
  • Loading branch information
ashuping committed Sep 11, 2024
1 parent 46f5e03 commit 8e98126
Show file tree
Hide file tree
Showing 14 changed files with 477 additions and 71 deletions.
46 changes: 41 additions & 5 deletions modules/data/Variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
'''
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -148,4 +170,18 @@ def var_type(self) -> Option[type]:

@property
def default(self) -> Option[A]:
return self.__default
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 ''
])
65 changes: 0 additions & 65 deletions modules/data/__init__.py

This file was deleted.

4 changes: 4 additions & 0 deletions modules/types/ComparableError.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions modules/types/Either.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion modules/types/Option.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,26 @@ 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

@abstractmethod
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)
Expand All @@ -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
Expand All @@ -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

Expand Down
3 changes: 3 additions & 0 deletions modules/types/Result.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
100 changes: 100 additions & 0 deletions modules/workflow/Workflow.py
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
'''

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.
'''
Loading

0 comments on commit 8e98126

Please sign in to comment.