Skip to content

Derive requiredness from the remaining defaults - #174

Open
elinscott wants to merge 3 commits into
scinode:mainfrom
elinscott:fix/default-requiredness-remaining-paths
Open

elinscott wants to merge 3 commits into
scinode:mainfrom
elinscott:fix/default-requiredness-remaining-paths

Conversation

@elinscott

@elinscott elinscott commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator

Giving a socket a default still left it reported as required, so a caller who relied on the default was told the input was missing. Given

from node_graph import socket_spec as ss

spec = ss.namespace(a=int, b=(int, 5))
print(spec.fields["b"].default)        # 5
print(spec.fields["b"].meta.required)  # True

b carries the default it was given and is still required, so nothing downstream can tell it apart from an input the caller simply never supplied.

This stacks on #173, which fixed the same defect for a dataclass or pydantic field. That PR listed three paths it did not cover; these are those three:

  • the tuple form of a namespace entry, namespace(b=(int, 5)), which is also how a dynamic namespace's fixed fields are declared
  • set_default(spec, "b", 5)
  • the mapping default of a namespace parameter, def f(cfg: ns(x=int, y=int) = {"y": 3}), which sets defaults on the leaves the mapping names

Changes

Mark a leaf optional when any of those three gives it a default. That leaves SocketSpec(identifier, default=...), built by hand, as the only remaining way to end up with a default and required=True — see below.

set_default is a mutation rather than a construction, so it needs an inverse: unset_default makes the leaf required again. It restores requiredness by the same rule that removed it, so it only reverses optionality this mechanism introduced; a leaf whose optionality was declared some other way — a NotRequired TypedDict key, or an explicit required=False — is currently made required too, which is wrong and worth fixing separately.

Inner namespaces are left alone here, departing from #173, which marked a defaulted namespace field optional. The two construction paths refuse a namespace default outright, so no namespace case arises; and a mapping default names only some of the leaves below a namespace, so descending into one says nothing about whether the caller may skip it. #173's namespaces were different: a field annotated Optional[Sub] = None is a statement about the whole namespace.

Following #173, requiredness is set to False rather than left as None, since SocketMeta.to_dict drops None and from_dict then restores the True default.

What is left

A SocketSpec constructed by hand with a default — as While.max_iterations and If.invert_condition are in tasks/builtins.py — still comes out required. Deriving requiredness in SocketSpec.__post_init__ would catch that case and every other one, but it would also rewrite specs restored by from_dict, changing how an already-serialized graph deserializes. Decided to separate that decision from this PR.

elinscott and others added 3 commits August 14, 2026 09:52
A dataclass or pydantic field with a default reached the caller as a
required socket, so a graph that left it alone failed with a missing
required input naming that field.

- Mark a field optional when it declares a default or a default factory,
  in the pydantic, dataclass, and dynamic branches of `from_model`.
- Read the factory's presence only, never calling it, so a field's
  default value is still copied only from a plain default.
- Apply requiredness to namespace fields too, which carry no default
  value but may be omitted just the same.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three of the new tests asserted the same thing twice in one function,
once for a dataclass and once for a pydantic model, so a failure did not
say which kind broke.

- Parametrize them over the two model kinds, with `dataclass` and
  `pydantic` ids.
- Build the default-factory models from a per-case factory, so each case
  asserts its own factory went uncalled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three ways of giving a socket a default left it reported as required, so a
caller who relied on the default was told the input was missing.

- `namespace(b=(int, 5))`, and the same tuple form among a dynamic
  namespace's fixed fields, now yield an optional socket.
- `set_default` marks the leaf optional; `unset_default` makes it required
  again, unless it had no default to lose.
- A leaf named by the mapping default of a namespace parameter comes out
  optional; the namespaces the mapping descends into keep their
  requiredness, since the mapping need not name every field below them.
- One test per path, each failing without the change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.92308% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.82%. Comparing base (8d85e61) to head (19e03b5).

Files with missing lines Patch % Lines
tests/test_socket_spec.py 96.22% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #174      +/-   ##
==========================================
+ Coverage   89.68%   89.82%   +0.14%     
==========================================
  Files          81       81              
  Lines        8984     9109     +125     
==========================================
+ Hits         8057     8182     +125     
  Misses        927      927              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

elinscott added a commit to elinscott/node-graph that referenced this pull request Aug 14, 2026
# Conflicts:
#	src/node_graph/socket_spec.py
elinscott added a commit to elinscott/node-graph that referenced this pull request Aug 17, 2026
Resolution notes, all of it integration between two open pull requests:

- socket_spec.py: scinode#159 reads a Pydantic field's annotation through
  `_pydantic_field_annotation` and scinode#174 derives requiredness from the field's
  default at the same two sites; both are kept.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant