Repository navigation
Conversation
Nothing in node-graph or aiida-workgraph ever read self.graph_type; its only observed value was the "NORMAL" default. Drop the constructor argument, the attribute, and its slot in get_metadata()/to_dict(); from_dict() now discards a stored graph_type key instead of round-tripping it, so old serialized graphs still load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A downstream package (aiida-workgraph) needs to know which metadata keys a Graph subclass already reserves for its own serialization bookkeeping, so it can validate its own metadata schema without guessing or hand-coding node-graph's internals (aiidateam/aiida-workgraph#812). - Add `Graph._declared_metadata_keys`, a frozenset class attribute (`{"graph_class", "definition"}` on the base class) that subclasses extend by union. `definition` is the key `utils/graph.py` writes when a graph is built from a `@task.graph`-decorated function. - Add `Graph._validate_metadata_keys`, an opt-in flag (default `False`). When a subclass sets it `True`, a `metadata=` key outside `_declared_metadata_keys` raises `ValueError` at construction, naming the valid set. Base `Graph` stays permissive, matching today's behavior for callers who stash arbitrary keys in `metadata`. - Add `_skip_metadata_validation`, an internal constructor flag `from_dict()` sets when reconstructing a graph. Metadata keys an older version wrote (or a subclass's declared set has since dropped) still load; only fresh, caller-driven construction validates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`_skip_metadata_validation` let from_dict() silently reload metadata keys outside a validating subclass's declared set; from_dict() now validates exactly like fresh construction, so stale keys raise instead of loading. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #181 +/- ##
==========================================
+ Coverage 89.68% 89.78% +0.10%
==========================================
Files 81 81
Lines 8984 9073 +89
==========================================
+ Hits 8057 8146 +89
Misses 927 927 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
elinscott
marked this pull request as draft
August 26, 2026 17:09
The key-name registry accepted a declared key holding any value, and accepted any key added to `_metadata` after construction, so both reached `to_dict()` unremarked. - Add `GraphMetadata`, a `total=False` TypedDict naming `graph_class` and `definition`, carrying `extra="allow"` so a bare `Graph` still keeps any other key a caller stashes in `metadata`. - Point the new `Graph._metadata_schema` attribute at it. A subclass narrows by inheriting a wider TypedDict from `GraphMetadata` and overriding `__pydantic_config__` with `extra="forbid"`. - Add `Graph.validate_metadata()`, returning a validated copy from a cached `TypeAdapter`, and call it at three seams: `__init__`, `from_dict()` (naming the graph in the error) and `get_metadata()`, whose result `to_dict()` writes. - Remove `_declared_metadata_keys`, `_validate_metadata_keys` and `_validate_metadata`; `_metadata` is a plain dict between seams. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reaching a graph's metadata meant touching `_metadata`, a private name, which downstream subclasses were already doing. - Add a `Graph.metadata` property, reading and writing `_metadata` itself: a plain dict, unchecked on mutation and on whole-dict reassignment, with `_metadata_schema` applied at the seams as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`utils/graph.py` reached into `_metadata` to record a built graph's callable identity. - Use the `Graph.metadata` property instead; same dict, public name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GraphMetadata accepted values pydantic would coerce into a declared field's type (a bool field given "yes" or 1, an int field given a numeral spelled as a string) instead of refusing them, and each validation seam raised a bare pydantic.ValidationError naming neither the graph nor the schema's valid keys. - Add strict=True to GraphMetadata.__pydantic_config__, refusing type coercion alongside the existing extra check. - Add MetadataValidationError (a ValueError) naming the graph and listing _metadata_schema's valid keys; validate_metadata raises it instead of the raw pydantic error, threaded through __init__, get_metadata, and from_dict. - Update tests/test_graph.py's pytest.raises expectations from pydantic.ValidationError to ValueError to match, and the from_dict message assertion to the new wording. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Graph._declared_metadata_keys & opt-in metadata key validationGraph.metadata a schema: a TypedDict validated at the boundaries
Graph.metadata a schema: a TypedDict validated at the boundariesGraph.metadata a schema
Three tests each declared the same StrictMetadata/StrictGraph pair inline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A module-level lru_cache stood between a subclass and its own validator; the adapter now appears on each Graph subclass at class definition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
elinscott
marked this pull request as ready for review
August 27, 2026 09:49
elinscott
added a commit
to elinscott/node-graph
that referenced
this pull request
Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #180 (removes
graph_type); review the commits after it.TL;DR
Problem
Graph.metadatahas no schema: any key a caller passes at construction is silently accepted, stored, and round-tripped, whether or not anything downstream ever reads it.Graphitself, whose own reserved keys are few and well known.WorkGraphalso usesmetadatato carry AiiDA's own process-launch keys (label,description, ...), and inheriting node-graph's schema-less dict meant an AiiDA user writing the obviousWorkGraph(name, metadata={'label': ...})got silently the wrongmetadata— accepted without complaint, stored as graph bookkeeping, and never properly passed on as AiiDA metadata (metadatameans two different things, and the constructor accepts the wrong one silently aiidateam/aiida-workgraph#812).node-graph isn't at fault for not knowing what
labelmeans to AiiDA, but without a schema that declares which keysGraphitself reserves, a downstream subclass has no way to distinguish "this key is mine" from "this key is a typo" either, so it can't validate an extended metadata schema.Changes
The schema
GraphMetadatais atotal=FalseTypedDict naming the keysGraphwrites for its own serialization bookkeeping, and what each one holds. It carriesextra="allow", so a bareGraphkeeps behaving as it does today for callers who stash arbitrary keys — but a declared key still has to hold the right thing.strict=Truerefuses type coercion, not just unknown keys: a declared field holding a value of the wrong type is rejected rather than converted (aboolfield given"yes"or1, anintfield given a numeral spelled as a string).Narrowing in a subclass
A subclass declares its own schema by inheriting a wider TypedDict from
GraphMetadataand pointing_metadata_schemaat it.extra="forbid"on that TypedDict is what turns a typo into an error; the schema and the strictness travel together, so there is one thing to declare rather than two.Validated at the boundaries, not on every mutation
metadatais a plain dict — no custom dict class, no setter that checks, nothing to keep in step.Graph.validate_metadata()runs a cachedTypeAdapterand returns a validated copy, and it is called at three seams: construction,from_dict(), andget_metadata(), whose resultto_dict()writes. So a stray key set after construction is not refused at the line that sets it; it is refused the moment the graph is serialized.Graph.metadatais new here too: a plain property over_metadata, so downstream code and subclasses stop reaching for a private name.Reconstruction validates like construction
from_dict()runs the same check, naming the graph it could not load. For a validating subclass this is a deliberate break: a serialized graph carrying a key outside the current declared set (schema drift, or a stray key from an older version) now raises on load instead of loading silently, and affected data needs that key stripped before it will load again.Upstream
Paired with aiidateam/aiida-workgraph's
unified-metadatabranch, which composesGraphMetadatawith AiiDA's own process-launch port names (introspected fromWorkGraphEngine's spec) and forbids extras. That PR implements the wiring half of #812; this is the schema primitive it's built on.