Python 3.10 implicit-Optional fix; emit every componentRef locator (v0.1.26) - #69
Merged
Volv-G merged 2 commits intoSep 26, 2026
Conversation
Python 3.10's get_type_hints still applies PEP 484 implicit Optional, which 3.11 removed, so `x: In[str] = None` resolved to Optional[In[str]] and was no longer recognised as a graph input. Strip an Optional wrapper when it is paired with a None default, in the @pipeline signature path and the cfg predicate. Written as a behavioural rule rather than a version check, so all supported Pythons take the same code path. A wider union is left alone.
ref() rejected url= with name=, so five corpus pipelines under relevance/experiments and relevance-tools could not be expressed in Python at all. The dehydrated schema allows url, name and digest together, and the hydrator resolves each present locator independently and keeps the highest component version. Allow any combination of the three and emit each one in the canonical key order url, name, digest. Multiple locators are an upgrade path, not a narrowing filter: the name can win over the url. tag= and a ref with no locator stay rejected.
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.
(AI-assisted)
Two independent authoring fixes, both blocking Python-authored pipelines that the YAML corpus already contains.
1. Accept a
Nonedefault onIn[T]under Python 3.10What
x: In[str] = Nonefails to compile on Python 3.10 withpipeline parameter 'x' must be annotated In[T] or Out[T]. It works on 3.11+.This is pre-existing, not a 0.1.25 regression — 0.1.24 and 0.1.25 fail identically on 3.10.
pyproject.tomldeclaresrequires-python = ">=3.10", but CI only tests 3.12/3.13, so nothing caught it.Why
Python 3.10's
get_type_hintsstill applies PEP 484 implicitOptional: a parameter with aNonedefault has its annotation silently rewritten toOptional[T]. 3.11 removed that behaviour.In[T]detection isgetattr(annotation, "__origin__", None) is In, and on the rewritten annotation that origin istyping.Union, so the parameter stops looking like a graph input.How
_strip_optional_none(annotation, default)inpython_pipeline/trace.py, next to the existing_is_in_annotation: when the default isNoneand the annotation is a two-armOptional, unwrap it. Applied in the@pipelinesignature loop and in thecfgpredicate inpipeline_compiler.py— the only two sites that read resolved hints for pipeline parameters.Written as a behavioural rule, not a
sys.version_infocheck, so all four supported Pythons take the same code path rather than 3.10 running code the CI Pythons never execute.Blast radius checked:
component_from_func.py(the@taskpath) is unaffected — it produces identical component YAML on 3.10 and 3.12 — and return annotations don't go through this path.2. Emit every
componentReflocatorref()is givenWhat
ref()rejectedurl=together withname=as "conflicting locators". Five corpus pipelines could therefore not be expressed in Python at all, across 29 task refs:relevance-tools/…/oasis/pipelines/lgbm_from_dnn.yaml{name, url}×2…/search_reranker/tangle/pipeline/build_vantage_features…{name, url}…/relevance_judge…{name, url}×15…/daily_vantage_s…{digest, name, url}×4…/daily_pulse_scr…{digest, name, url}×3Why the rejection was wrong
The dehydrated schema's
DehydratedComponentRefisanyOf: [url, digest, name]with no mutual exclusion —{url, name}and{url, name, digest}both validate today.And the hydrator supports it deliberately.
_resolve_taskcollects every present locator key; with two or more it calls_resolve_best_ref, which resolves each one independently and hands the candidates to_pick_best_candidate, keeping the highest component version (utils.get_version_from_data), tie-breakingdigest>name>url.So the semantics are worth stating plainly, because they are not the obvious reading:
nameis neither a selector within the URL nor a validation of the fetched component;{name, digest, spec}.ref(url=…, name=…)means "use this file, but prefer the published component if it is newer".How
ref()now accepts any combination ofurl/name/digest; the XOR check is gone._emit_component_refchanged from first-branch-wins to accumulating every locator present, in the canonical key orderurl, name, digest— extending the existing "url first, digest last" rule. Corpus files happen to write name-first, but the emitter has always imposed its own order, and nothing keys on it (sidecar/dedup identity is unaffected).tag=stays rejected (the hydrator has no tag fetcher andtagis not even a schema property), as does a ref with no locator. Diagnostics name the keyword, never the value.No compile-time validation of the combination is added: detecting a "mismatch" would require resolving both locators at compile time, which the compiler never does.
Failure modes
Optional[In[str]] = Nonenow compiles on 3.11+ where it previously raised. That turns an error into output; it never changes output that already existed. Byte-identity is pinned by a test.Union[A, B, None]) is deliberately left alone: unwrapping would invent a type the author never wrote.ref()call emits exactly what it emitted before. Only the previously-rejected combinations are new.Review focus
_strip_optional_none— is "two-armOptionalplus aNonedefault" the right trigger?url, name, digest, given the corpus writes name-first.namebeside aurldeserves a compile-time warning, since the resolved component may not be the one at the url.Tophatting
On master, 3.10 shows exactly
1 failed(test_signature_defaults.py::test_a_none_default_is_left_alone).Checklist
tests/test_optional_annotations.py; 16 intests/test_ref_locator_combinations.py(every legal combination's emitted shape, canonical key order independent of kwarg order, the corpus shape, chaining through.named/.bind/.with_annotations, two hydration tests proving the version pick in both directions, a compile→hydrate end-to-end, and the surviving rejections).b737f4bfa79c8664, identical to released 0.1.25 on 3.12.git diff --checkclean.uv.lockhand-edited on thetangle-cliself-entry only (one line);uv lockwas not run.Follow-up (not in this PR)
requires-python = ">=3.10"with a["3.12", "3.13"]CI matrix is what let (1) ship. The suite passes clean on 3.10 and 3.11 today, so adding them to the matrix is a one-line change whenever you want it.