Latest Results
[external-field-type] - refactor: name the parent-linking capability instead of listing two classes
Three sites asked isinstance/issubclass against (BaseRedisType,
AtomicRedisModel) and then performed the same two writes. The pair was there
because both hierarchies implement parent linking independently and share no
base that names it.
ParentLinked, in a new leaf module with no rapyer imports, is that name. It is
declared, not probed: isinstance answers nominally and the abstract method
enforces the contract, so having the capability guarantees the wiring - unlike
the hasattr(val, "_base_model_link") that init_redis_field used, which any
object of the right shape satisfied.
It stays abstract because the storage genuinely differs. BaseRedisType keeps
plain instance attributes; AtomicRedisModel keeps a PrivateAttr behind a
property. Each owns its own link_to_parent.
The parameter is a path segment, not a name: field_path concatenates without
inserting a separator, so callers pass ".attr" (__setattr__, model_post_init,
RedisDict), "[3]" (RedisList) or a raw key (GenericRedisType), exactly as
before. Building the dot inside the method would have broken lists and dicts.
Four call sites now ask one question, and none of them reaches into
_base_model_link any more.
Equivalence checked, not assumed: is_redis_link feeds redis_link_fields(), so
ParentLinked was compared against the old two-class tuple across all 476 field
specs on 213 models - zero disagreements, 472 still linked. No live instance
carries _base_model_link without declaring the capability. Cascade plan hash
unchanged at 44136fa355b3678a.
2953 passed / 205 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BgS1CHD6yELZ32GDpwdHNrefactor/external-field-type [external-field-type] - refactor: address five findings from the preference review
Rule 1 - hop_roots was declared on walk(), forwarded into the recursion and
passed as False by its only caller, but never read in any condition. Removed
along with the test that pinned it having no effect. It comes back with the
cascade walk that needs it.
Rule 2 - the "does this field's own type carry this trait" predicate was
written out at five sites. It now lives once, on FieldSpec, which is the object
that owns the external slot it reads:
def has(self, trait: FieldTrait) -> bool
fields_with(), __setattr__, walk() and three planner sites call it. Routing
them through fields_with() instead would have been wrong: it returns a
frozenset, and the planner's suffix list feeds the plan hash, so hash-order
iteration would have reintroduced A3. Verified stable across PYTHONHASHSEED
0/1/7/99999.
Rule 6 - the dump-exclude dict and the planner's suffix list are built as
comprehensions instead of empty-init-then-append. The suffix one stays on
_field_specs rather than fields_with, for the ordering reason above.
Rule 8 - four calls that black split inside a larger expression are extracted
to named variables: the safe_issubclass in walk() and three fields_with calls
in test_special_types.py, where the extraction also splits the shared
Act / Assert step.
Rule 9 - BaseRedisType.traits() and .reachable_fields_w_traits() each still
imported FieldTrait inside the body, shadowing the module-level import added
in 51f8427. The earlier guard was vacuous: it asserted the absence of the
types.external path after the bulk pass had already rewritten those lines to
types.traits. Annotations unquoted with them.
2953 passed / 205 skipped; one fewer than before is the deleted hop_roots test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BgS1CHD6yELZ32GDpwdHNrefactor/external-field-type [external-field-type] - refactor: FieldTrait into a leaf module, and drop the utils -> types edge
FieldTrait moves to rapyer/types/traits.py, which imports only enum. Every
importer takes it from there at module level, so the two runtime imports inside
BaseRedisType.traits() and .reachable_fields_w_traits() are gone.
rapyer/utils/annotation.py imported RelationalFieldType to decide which
annotations to pass through untouched, which is a converter's call, not a
util's. That check moves behind a new TypeConverter.keeps_annotation() hook,
implemented in RedisConverter, which already lives in types/. annotation.py now
imports nothing from rapyer, and relational.py can import strip_optional at
module level instead of inside relational_targets().
Also names the condition in fetch_models_with_sf_loads, and moves two
classification guards out of the Arrange step into Assert, with the checks
bound to named variables.
Addresses six review comments on #295. 2954 passed / 205 skipped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018BgS1CHD6yELZ32GDpwdHNrefactor/external-field-type Latest Branches
+6%
refactor/external-field-type +14%
-1%
gsd/262-cascade-fk-multi-target-pr © 2026 CodSpeed Technology