Skip to content

Commit 45fabc6

Browse files
committed
Improve Pydantic model usage
1 parent b061299 commit 45fabc6

5 files changed

Lines changed: 97 additions & 26 deletions

File tree

docs/gallery/howto/autogen/annotate_inputs_outputs.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -255,23 +255,27 @@ def DynamicNestedDictGenerator(n: int):
255255
from aiida_workgraph.socket_spec import Leaf
256256

257257

258+
class InputsModel(BaseModel):
259+
x: int
260+
y: int
261+
262+
258263
class OutputsModel(BaseModel):
259264
sum: int
260265
product: int
261266

262267

263268
@task
264-
def add_multiply_pydantic_in_out(x, y) -> OutputsModel:
265-
return {'sum': x + y, 'product': x * y}
269+
def add_multiply_pydantic_in_out(data: InputsModel) -> OutputsModel:
270+
return {'sum': data.x + data.y, 'product': data.x * data.y}
266271

267272

268273
@task.graph
269-
def AddMultiplyPydantic():
270-
# IMPORTANT: pass a plain dict, not OutputsModel(x=3, y=4)
271-
add_multiply_pydantic_in_out(x=3, y=4)
274+
def AddMultiplyPydantic(data: InputsModel) -> OutputsModel:
275+
return add_multiply_pydantic_in_out(data=data)
272276

273277

274-
wg = AddMultiplyPydantic.build()
278+
wg = AddMultiplyPydantic.build(data=InputsModel(x=3, y=4))
275279
wg.run()
276280
wg.generate_provenance_graph()
277281

@@ -309,6 +313,12 @@ def GraphDynamicOut(n: int):
309313
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
310314
#
311315
# Sometimes you want to **validate** with a Pydantic model but store it as a **single node** instead of expanding fields.
316+
# For leaf models, WorkGraph treats the value as a blob. If a serializer is registered for the class, it is used;
317+
# otherwise ``JsonableData`` stores the model as a JSON-friendly dict.
318+
#
319+
# Note: Pydantic annotations define the *schema* for WorkGraph sockets. Outputs are stored as typed
320+
# AiiDA nodes per field, so runtime results are dicts of nodes (not Pydantic instances). Use those
321+
# nodes for linking/provenance; rebuild a Pydantic model only for convenience.
312322
# There are two ways:
313323
#
314324
# 1) Mark the model: ``model_config = {"leaf": True}``
@@ -325,7 +335,7 @@ class BlobModel(BaseModel):
325335
@task
326336
def consume_blob(m: BlobModel) -> dict:
327337
# 'm' is validated by Pydantic but stored/treated as one leaf node
328-
return {'sum': m['a'] + m['b']}
338+
return {'sum': m.a + m.b}
329339

330340

331341
# Per-use override without modifying the model:
@@ -336,13 +346,13 @@ class AnotherModel(BaseModel):
336346

337347
@task
338348
def consume_blob_per_use(m: Leaf[AnotherModel]) -> dict:
339-
return {'sum': m['a'] + m['b']}
349+
return {'sum': m.a + m.b}
340350

341351

342352
@task.graph
343353
def BlobExamples():
344-
consume_blob(m={'a': 1, 'b': 2})
345-
consume_blob_per_use(m={'a': 3, 'b': 4})
354+
consume_blob(m=BlobModel(a=1, b=2))
355+
consume_blob_per_use(m=AnotherModel(a=3, b=4))
346356

347357

348358
wg = BlobExamples.build()
@@ -402,12 +412,14 @@ def AddMultiplyDataclass():
402412
# %%
403413
# .. important::
404414
#
405-
# Models/dataclasses are annotation-only
406-
# Even when you annotate with BaseModel or @dataclass, do not pass instances of these types to tasks/graphs. Always pass plain dictionaries:
415+
# Structured models (Pydantic or dataclasses) are supported as *runtime* values.
416+
# You may pass instances to tasks/graphs and return them from tasks:
407417
#
408-
# - This lets WorkGraph expand inputs/outputs into individual sockets, so it can wire provenance edges precisely (e.g., data.x --> task.data.x).
409-
# - It allows graph inputs to be collected from task outputs as a dict of AiiDA ORM nodes, preserving AiiDA links between nodes.
410-
# - Validation still happens via the WorkGraph spec (derived from your annotations)--you’re just not constructing runtime model/dataclass objects.
418+
# - Instances are expanded to plain dicts when assigned to namespace sockets, so WorkGraph can
419+
# wire provenance edges precisely (e.g., data.x --> task.data.x).
420+
# - Graph inputs can still be collected from task outputs as a dict of AiiDA ORM nodes,
421+
# preserving AiiDA links between nodes.
422+
# - Validation still happens via the WorkGraph spec (derived from your annotations).
411423
#
412424
# Data linkage
413425
# ------------

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ classifiers = [
1919
dependencies = [
2020
"numpy",
2121
"scipy",
22-
"node-graph~=0.6.0",
22+
"node-graph~=0.6.1",
2323
"node-graph-widget>=0.0.5",
2424
"aiida-core~=2.7.1",
2525
"cloudpickle",
2626
"aiida-shell~=0.8",
27-
"aiida-pythonjob~=0.5.0",
27+
"aiida-pythonjob~=0.5.1",
2828
"jsonschema"
2929
]
3030
description = "Design flexible node-based workflow for AiiDA calculation."

src/aiida_workgraph/tasks/tests.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
from aiida_workgraph import task, Task
22
from aiida.calculations.arithmetic.add import ArithmeticAddCalculation
3+
from pydantic import BaseModel
4+
5+
6+
class BlobModel(BaseModel):
7+
model_config = {'leaf': True} # always a leaf blob
8+
9+
a: int
10+
b: int
11+
12+
13+
class AnotherModel(BaseModel):
14+
a: int
15+
b: int
316

417

518
@task

tests/test_pydantic.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import pytest
2+
3+
from aiida_workgraph import task
4+
5+
try:
6+
from pydantic import BaseModel as _BaseModel
7+
except Exception:
8+
_BaseModel = None
9+
10+
if _BaseModel is not None:
11+
12+
class PydanticInputs(_BaseModel):
13+
x: int
14+
y: int
15+
16+
class PydanticOutputs(_BaseModel):
17+
sum: int
18+
product: int
19+
20+
else:
21+
PydanticInputs = None
22+
PydanticOutputs = None
23+
24+
25+
if _BaseModel is not None:
26+
27+
@task
28+
def add_multiply_pydantic(data: 'PydanticInputs') -> 'PydanticOutputs':
29+
return PydanticOutputs(sum=data.x + data.y, product=data.x * data.y)
30+
else:
31+
add_multiply_pydantic = None
32+
33+
34+
def test_workgraph_pydantic_inputs_outputs():
35+
pytest.importorskip('pydantic')
36+
37+
@task.graph
38+
def add_graph(data: 'PydanticInputs') -> 'PydanticOutputs':
39+
return add_multiply_pydantic(data=data)
40+
41+
result, wg = add_graph.run_get_graph(data=PydanticInputs(x=2, y=3))
42+
43+
assert result['sum'] == 5
44+
assert result['product'] == 6
45+
assert wg.outputs.sum.value == 5
46+
assert wg.outputs.product.value == 6

uv.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)