darabos commited on
Commit
d29f839
·
1 Parent(s): 28209f8

Support optional inputs in simple executor.

Browse files
lynxkite-core/pyproject.toml CHANGED
@@ -11,3 +11,6 @@ dependencies = [
11
  dev = [
12
  "pytest",
13
  ]
 
 
 
 
11
  dev = [
12
  "pytest",
13
  ]
14
+
15
+ [tool.pytest.ini_options]
16
+ asyncio_mode = "auto"
lynxkite-core/src/lynxkite/core/executors/simple.py CHANGED
@@ -42,7 +42,11 @@ async def execute(ws: workspace.Workspace, catalog: ops.Catalog):
42
  if i.name in edges and edges[i.name] in outputs:
43
  inputs.append(outputs[edges[i.name]])
44
  else:
45
- missing.append(i.name)
 
 
 
 
46
  if missing:
47
  node.publish_error(f"Missing input: {', '.join(missing)}")
48
  continue
 
42
  if i.name in edges and edges[i.name] in outputs:
43
  inputs.append(outputs[edges[i.name]])
44
  else:
45
+ opt_type = ops.get_optional_type(i.type)
46
+ if opt_type is not None:
47
+ inputs.append(None)
48
+ else:
49
+ missing.append(i.name)
50
  if missing:
51
  node.publish_error(f"Missing input: {', '.join(missing)}")
52
  continue
lynxkite-core/src/lynxkite/core/ops.py CHANGED
@@ -149,6 +149,16 @@ def basic_outputs(*names):
149
  return {name: Output(name=name, type=None) for name in names}
150
 
151
 
 
 
 
 
 
 
 
 
 
 
152
  def _param_to_type(name, value, type):
153
  value = value or ""
154
  if type is int:
@@ -160,12 +170,9 @@ def _param_to_type(name, value, type):
160
  if isinstance(type, enum.EnumMeta):
161
  assert value in type.__members__, f"{value} is not an option for {name}."
162
  return type[value]
163
- if isinstance(type, types.UnionType):
164
- match type.__args__:
165
- case (types.NoneType, type):
166
- return None if value == "" else _param_to_type(name, value, type)
167
- case (type, types.NoneType):
168
- return None if value == "" else _param_to_type(name, value, type)
169
  if isinstance(type, typeof) and issubclass(type, pydantic.BaseModel):
170
  try:
171
  return type.model_validate_json(value)
 
149
  return {name: Output(name=name, type=None) for name in names}
150
 
151
 
152
+ def get_optional_type(type):
153
+ """For a type like `int | None`, returns `int`. Returns `None` otherwise."""
154
+ if isinstance(type, types.UnionType):
155
+ match type.__args__:
156
+ case (types.NoneType, type):
157
+ return type
158
+ case (type, types.NoneType):
159
+ return type
160
+
161
+
162
  def _param_to_type(name, value, type):
163
  value = value or ""
164
  if type is int:
 
170
  if isinstance(type, enum.EnumMeta):
171
  assert value in type.__members__, f"{value} is not an option for {name}."
172
  return type[value]
173
+ opt_type = get_optional_type(type)
174
+ if opt_type:
175
+ return None if value == "" else _param_to_type(name, value, opt_type)
 
 
 
176
  if isinstance(type, typeof) and issubclass(type, pydantic.BaseModel):
177
  try:
178
  return type.model_validate_json(value)
lynxkite-core/src/lynxkite/core/workspace.py CHANGED
@@ -125,7 +125,7 @@ class Workspace(BaseConfig):
125
  return self.env in ops.EXECUTORS
126
 
127
  async def execute(self):
128
- await ops.EXECUTORS[self.env](self)
129
 
130
  def save(self, path: str):
131
  """Persist the workspace to a local file in JSON format."""
@@ -201,3 +201,35 @@ class Workspace(BaseConfig):
201
  if "data" not in nc:
202
  nc["data"] = pycrdt.Map()
203
  np._crdt = nc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  return self.env in ops.EXECUTORS
126
 
127
  async def execute(self):
128
+ return await ops.EXECUTORS[self.env](self)
129
 
130
  def save(self, path: str):
131
  """Persist the workspace to a local file in JSON format."""
 
201
  if "data" not in nc:
202
  nc["data"] = pycrdt.Map()
203
  np._crdt = nc
204
+
205
+ def add_node(self, func):
206
+ """For convenience in e.g. tests."""
207
+ node = WorkspaceNode(
208
+ id=func.__name__,
209
+ type=func.__op__.type,
210
+ data=WorkspaceNodeData(
211
+ title=func.__op__.name,
212
+ params={},
213
+ display=None,
214
+ input_metadata=None,
215
+ error=None,
216
+ status=NodeStatus.planned,
217
+ ),
218
+ position=Position(x=0, y=0),
219
+ )
220
+ self.nodes.append(node)
221
+ return node
222
+
223
+ def add_edge(
224
+ self, source: WorkspaceNode, sourceHandle: str, target: WorkspaceNode, targetHandle: str
225
+ ):
226
+ """For convenience in e.g. tests."""
227
+ edge = WorkspaceEdge(
228
+ id=f"{source.id} {sourceHandle} to {target.id} {targetHandle}",
229
+ source=source.id,
230
+ target=target.id,
231
+ sourceHandle=sourceHandle,
232
+ targetHandle=targetHandle,
233
+ )
234
+ self.edges.append(edge)
235
+ return edge
lynxkite-core/tests/test_simple.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from lynxkite.core import ops, workspace
2
+ from lynxkite.core.executors import simple
3
+
4
+
5
+ async def test_optional_inputs():
6
+ @ops.op("test", "one")
7
+ def one():
8
+ return 1
9
+
10
+ @ops.op("test", "maybe add")
11
+ def maybe_add(a: int, b: int | None = None):
12
+ return a + (b or 0)
13
+
14
+ assert maybe_add.__op__.inputs == [
15
+ ops.Input(name="a", type=int, position="left"),
16
+ ops.Input(name="b", type=int | None, position="left"),
17
+ ]
18
+ simple.register("test")
19
+ ws = workspace.Workspace(env="test", nodes=[], edges=[])
20
+ a = ws.add_node(one)
21
+ b = ws.add_node(maybe_add)
22
+ outputs = await ws.execute()
23
+ assert b.data.error == "Missing input: a"
24
+ ws.add_edge(a, "output", b, "a")
25
+ outputs = await ws.execute()
26
+ assert outputs[b.id, "output"] == 1