fix(python): make [<AttachMembers>] union static members work (#4634)#4636
Open
dbrattli wants to merge 2 commits into
Open
fix(python): make [<AttachMembers>] union static members work (#4634)#4636dbrattli wants to merge 2 commits into
dbrattli wants to merge 2 commits into
Conversation
A discriminated union with [<AttachMembers>] is emitted in Python as a
private base class `_Demo` (which holds the attached static members), the
case classes `Demo_A`/`Demo_B`, and a public type alias `Demo = Demo_A | Demo_B`.
This shape was broken in two ways:
1. A static property such as `static member propDefault = A "prop"` was
emitted eagerly in the base-class body as
`prop_default = StaticProperty["Demo"](Demo_A("prop"))`. Python evaluates
`Demo_A("prop")` while defining `_Demo`, before the case class exists, so
the module failed at import with `NameError`. Read-only static property
values on unions are now deferred with a lambda (`StaticLazyProperty`),
which also matches F# getter semantics (re-evaluated on each access).
2. Use sites such as `Demo.propDefault`/`Demo.methDefault()` referenced the
public type alias, which is a `TypeAliasType` and does not carry the
members, raising `AttributeError`. Static member access on a union now
targets the underscore-prefixed base class `_Demo`, consistent with the
convention already used for reflection.
Also emit the descriptor's type annotation as a string forward reference
whenever the property type is the enclosing union, since the type alias is
only defined after the base class.
The FSharp2Fable change is gated to Python + IsFSharpUnion and is a no-op
for all other targets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Python Type Checking Results (Pyright)
Excluded files with errors (4 files)These files have known type errors and are excluded from CI. Remove from
|
The original fix only rewrote the same-file `IdentExpr` reference to the union base class (`_Demo`). A union defined in another file is referenced via an `Import`, which fell through to the type alias and raised `AttributeError` on static member access. Rewrite the import `Selector` to the base class too. Adds a cross-module regression test and cleans up the self-reference type-annotation check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collaborator
Author
Follow-up: cross-module static member access was still brokenWhile adding a cross-module regression test, I found the original fix only handled the same-file case. Changes (commit
|
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.
Fixes #4634
Problem
A discriminated union with
[<AttachMembers>]is emitted in Python as three pieces:_Demo(Union)that holds the attached static members,Demo_A,Demo_B,type Demo = Demo_A | Demo_B.Given:
this shape was broken in two ways (plus a latent third):
Eager evaluation, raising
NameErrorat import. The static property was emitted as a class-body assignment inside_Demo:Python evaluates
Demo_A("prop")while defining_Demo, before the case class exists, so the module failed to import.Use sites hit the type alias.
Demo.propDefault/Demo.methDefault()compiled toDemo.prop_default/Demo.meth_default(), butDemois aTypeAliasTypethat does not carry the members, raisingAttributeError. This broke both static methods and static properties.(Latent) bad type annotation. When the union lived in a module, the descriptor annotation was emitted as a bare
StaticLazyProperty[Issue4634_Demo]instead of a string forward reference, also raisingNameError. The eager-evaluation bug masked it.Fix
getInitialValue/transformStaticProperty(Fable2Python.Transforms.fs): defer a read-only union static property value with a lambda, producingStaticLazyProperty["Demo"](lambda: Demo_A("prop")). This avoids the forward reference and matches F# getter semantics (re-evaluated per access). Scoped to getter-only to preserve settable-field semantics.transformStaticProperty: emit the descriptor type annotation as a string forward reference whenever the property type is the enclosing union (compare byFullName, not onlyDisplayName).makeCallWithArgInfo(FSharp2Fable.Util.fs): when referencing a union to reach a static member in Python, target the_Demobase class instead of theDemotype alias — the same underscore convention already used for reflection. This change is gated toPython+IsFSharpUnionand is a no-op for every other target.Verification
Issue4634intests/Python/TestNonRegression.fs.x = A "prop", y = A "meth".🤖 Generated with Claude Code