Migrating to 0.5¶
0.5 finishes what 0.4 started: topo=False — the flat, value-semantics
posture — is now the default everywhere, and the one error it raises is
named after its cause. Two breaking changes, both mechanical to migrate.
1. SymbolicModeError is now TopoNotEnabled¶
| Before | After |
|---|---|
except pg.SymbolicModeError |
except pg.TopoNotEnabled |
A rename with no alias — a sweep of except clauses is the whole migration.
The exception moves for the same reason the topo_* family did in 0.4: it is
raised for exactly one cause — the class was declared topo=False — so it now
says so. SymbolicModeError was also a misnomer: a topo=False object is
a symbolic object (it keeps its init args, validates, compares by value,
serializes, and rebinds); what it lacks is a parent link.
2. pg.functor, pg.symbolize and pg.wrap are now topo=False¶
The symbolizing decorators no longer join the symbolic tree by default —
functions (@pg.functor, pg.symbolize on a function) and classes
(pg.symbolize on a class, pg.wrap) alike, aligning them with pg.Object's
own default.
The old default made every call adopt its arguments, so ordinary Python call patterns raised:
@pg.functor()
def combine(a, b): ...
shared = Node(1)
combine(shared, shared) # 0.4: SharedHoldError — held at two keys
combine(shared, 1) # 0.4: SharedHoldError — held by an earlier call
Both work now. A call is a call; it does not take ownership of what it is passed.
What you lose is upward reach from a decorated value: topo_parent /
topo_path / topo_root raise TopoNotEnabled,
changes do not climb from it to its holder, and contextual resolution
and origin tracking — which walk the parent chain — do not resolve through one.
Everything else is unchanged: arguments are still validated, compared by value,
serialized, and rebindable at any depth, and partial and incremental binding
work exactly as before.
Class wrappers rebuild without the tree. A flat wrapper still reruns the
wrapped __init__ on rebind: its raw container members funnel path writes
through the wrapper itself, which is notified. What does not fire flat is
interior mutation of a symbolic member — the ordinary flat value-bag
posture, same as any topo=False pg.Object holding one.
The bases flip with their decorators. pg.Functor and ClassWrapper are
topo=False, so class Sum(pg.Functor) matches @pg.functor and a direct
ClassWrapper subclass matches pg.wrap. The bases that stay topo=True are
exactly the ones that require the tree: pg.Ref, InferredValue and
pg.Compound.
If you need the tree, ask for it where the type is made:
That is the right call for a pg.hyper search space, where a where clause
filters candidates by topo_path. An explicit topo= or a base_class= you
supplied always wins over the default.
For advanced callers of pg.typing.Schema.apply: respect_raw_storage is
now a tri-state, mirroring the native signature. None (the new default)
means no per-field gating — the old behavior of the former False default.
An explicit respect_raw_storage=False now means "gate const fields,
unauthored specs resolve raw" (a flat owner's axis) rather than "no
gating"; spell the old intent as None, or just omit the argument.
3. Raw-member path writes now notify¶
Not breaking, but observable: a sym_rebind whose path lands in a raw
dict / list member used to change the value silently. It now notifies
the nearest symbolic holder (on_sym_change fires with the full path) and
participates in model-validation rollback, so the same change reports
identically whichever spelling delivers it:
f.sym_rebind({'x.y': 2}) # 0.4: silent 0.5: on_sym_change(['x.y'])
f.sym_rebind(x={'y': 2}) # notified in both
Direct mutation (f.x['y'] = 2) stays invisible — PyGX is not involved in
it. If an on_sym_change override filtered on top-level keys only, it will
now also see dotted paths into raw members.
4. A no-op sym_rebind no longer raises (0.5.2)¶
sym_rebind's raise_on_no_change now defaults to False: a call that
carries no updates — an empty mapping / no kwargs, or a rebinder that returns
everything unchanged — returns self silently instead of raising
ValueError.
d = pg.Dict(a=1)
d.sym_rebind(lambda k, v, p: v) # 0.5.1: ValueError now: no-op
d.sym_rebind({}) # 0.5.1: ValueError now: no-op
d.sym_rebind({}, raise_on_no_change=True) # opt back in: ValueError
Assigning a value equal to the current one was never a "no change" — it is an update that happens to keep the value — so it did not raise before and still does not.
If code relied on the raise to detect an empty rebinder result, pass
raise_on_no_change=True explicitly.
5. Renames without aliases (0.5.2)¶
Three misspelled public names are renamed outright — imports of the old spellings fail, and there are no compatibility aliases:
| Before | After |
|---|---|
pg.algo.early_stopping.EarlyStopingPolicyBase |
pg.algo.early_stopping.EarlyStoppingPolicyBase |
pg.algo.scalars.Substraction |
pg.algo.scalars.Subtraction |
pg.algo.mutfun.Substract |
pg.algo.mutfun.Subtract |
Wire type names follow the classes: JSON serialized before 0.5.2 that
contains one of the old _type names no longer loads. Re-serialize such
artifacts with 0.5.2, or rewrite the _type strings in place.
6. Behavior fixes in 0.5.2¶
Parameters that were silently inert now do what their docs say, and a few inconsistencies are gone. Adjust call sites that (knowingly or not) depended on the old behavior:
pg.patching.patch_on_*: calling withoutvalueorvalue_fnnow raises (it used to silently patch matched fields toNone); an explicitvalue=Nonepatches TONone. Spell the old no-arg behavior asvalue=None.pg.KeyPathSet.union(other)now merges into the receiver and returns it; passcopy=Truefor the previous always-copy behavior.pg.List: every shrinking op honors the value spec'smin_size—pop()anddel l[i]used to slip below it silently.remove()now works underaccessor_writable=False, like the other mutating methods.pg.materialize(..., use_literal_values=...): the flag works again (integer decisions read as literal values when True) and its dead default flips True→False, so default calls keep index interpretation.pg.concurrent.thread_local_value_scope: a key absent on entry is restored toinitial_valueon exit instead of being deleted.pg.codingpermissions:ASSIGNnow gates augmented (x += y) and annotated (x: int = y) assignments;LOOPgates plainwithblocks.pg.typing.ListKey:max_value=0is a real (empty) bound; unbounded is spelledNoneor-1.pg.coding.run/sandbox_call: a child process that dies without reporting (typically thespawnre-import of an unguarded__main__) raisesChildProcessErrorimmediately instead of hanging until timeout.- Tuning: a trial's final measurement is the one at the largest step
(previously last-added);
Trial.completed_timeis now populated. - Plus fixes with no migration impact:
pg.from_jsonno longer consumes its input payload;pg.notify_onceworks fortopo=Falseobjects; nativeList.extend/+=/*clone held elements like the oracle;List.sym_hashis class-blind like itseq;pg.typing.is_subclassaccepts union types; patcher validators no longer run against a replacement object they never saw.
7. Behavior fixes in 0.5.3¶
- Bound methods render by name.
repr(),str()andpg.to_html()used to blow the stack on any value holding a bound method of an object that (directly or transitively) held it back — CPython renders a bound method as<bound method C.m of {self!r}>, embeddingrepr(__self__), and that nestedreprre-entered formatting through Python rather than throughformat(), so the existing cycle guard never saw it. A bound method now renders as<bound method C.m>, without expanding its owner. A method is behavior, not data: naming it is both safer and more useful. Affects the rendered text of any object holding a bound method, cycle or not.
This fix spans both cores, so the native half ships in pygx-core
0.5.2; pygx 0.5.3 pins it. Upgrading pygx pulls the matching core.
8. Behavior fixes in 0.5.4¶
-
A hyper primitive reused at several locations is ONE decision. A
pg.oneof/pg.floatv/pg.manyofvalue bound to a name and placed at two or more locations in a search space used to be treated as that many independent decisions; it is now a single decision whose choice applies at every location. This shrinks the search space of any program that did this —pg.dna_spec(...)reports fewer decision points, and a search visits fewer candidates. To keep independent decisions, create a separate primitive per location (callpg.oneof(...)once per site). To tie two locations together explicitly,pg.hyper.referenceremains the spelled-out form. (#821) -
pg.Refin atopo=Falseholder now resolves. A flat holder only refuses the inferential values that need to walk the tree (Inferential.needs_topo()— e.g.pg.ContextualGetter); apg.Refreads as its referent instead of raisingTopoNotEnabled. Code that caught that error as a signal no longer sees it. (#823) -
A subclass that changes
topore-bakes its field accessors. Reading an inferential field on atopo=Falsesubclass of atopo=Trueclass (or the reverse) used to go through the accessor baked for the base's mode and leak the inferential's rawAttributeError; it now behaves as the subclass's mode dictates. Reads on such subclasses are slower until the native-core route in #827 lands; no class inpygxitself hits this. (#825)
These fixes span both cores, so the native half ships in pygx-core
0.5.3; pygx 0.5.4 pins it. Upgrading pygx pulls the matching core.
9. Behavior fixes in 0.5.5¶
- A
topo=Falseholder refuses a tree-walking inferential where it lands. Constructing, assigning orsym_rebind-ing a contextual value (ValueFromParentChain,pg.contextual(...), anyInferentialwhoseneeds_topo()is true) into atopo=Falseobject now raisesTopoNotEnabledat that write, naming the field — previously the write succeeded and the error surfaced at the first read. A class whose field defaults to such a value can no longer be constructed without supplying that field; declare ittopo=True(or give the inferential aneeds_topo()that returnsFalseif itsinfer()reads no tree state).pg.Refand other self-contained inferentials are unaffected. (#829)
This fix spans both cores, so the native half ships in pygx-core
0.5.4; pygx 0.5.5 pins it. Upgrading pygx pulls the matching core.
10. Why this changed¶
0.4 made pg.Object flat by default on the argument that most objects are
values, not tree nodes. The decorators stayed tree-mode — and the seam showed:
one object passed to two functor arguments raised SharedHoldError, the same
value reused across two calls raised, and the workarounds leaked (pg.Ref
handed the callee a Ref; assign='ref' was not reachable from the
decorator).
The features that once pinned the decorators to the tree stopped needing it:
partial binding works flat, and a flat wrapper's reset-on-rebind fires from
holder notification on every schema shape. What remained of topo=True in
the decorators was the cost — call-time adoption — without the benefit, so
the default moved to where pg.Object's already was, and topo=True became
what it is on classes: the explicit opt-in for values that live in a tree.