Skip to content

Commit ba03f4a

Browse files
authored
[Python] Redesign Discriminated Union (DU) type (#4331)
1 parent 32350df commit ba03f4a

File tree

15 files changed

+996
-137
lines changed

15 files changed

+996
-137
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ dev = [
2424
"dunamai>=1.23.1,<2",
2525
"pydantic>=2.11.7",
2626
"attrs>=25.3.0",
27-
"pyright>=1.1.407",
27+
"pyright>=1.1.408",
2828
"ty>=0.0.7",
2929
]
3030

src/Fable.Build/FableLibrary/Python.fs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ type BuildFableLibraryPython(?skipCore: bool) =
2222
// Copy all Python/F# files to the build directory
2323
Directory.GetFiles(this.LibraryDir, "*") |> Shell.copyFiles this.BuildDir
2424
Directory.GetFiles(this.SourceDir, "*.py") |> Shell.copyFiles this.OutDir
25-
Directory.GetFiles(this.SourceDir, "*.pyi") |> Shell.copyFiles this.OutDir
2625

2726
// Python extension modules
2827
Directory.GetFiles(Path.Combine(this.SourceDir, "core"), "*")

src/Fable.Cli/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Changed
1111

12+
* [Python] Changed DU representation to use separate classes for each case (by @dbrattli)
1213
* [Python] Fable will no longer auto-generate `__str__` or `__hash__` for custom types. Use the `Py.Stringable` and `Py.Hashable` marker interfaces to generate these methods (by @dbrattli)
1314

1415
### Added

src/Fable.Compiler/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Changed
1111

12+
* [Python] Changed DU representation to use separate classes for each case (by @dbrattli)
1213
* [Python] Fable will no longer auto-generate `__str__` or `__hash__` for custom types. Use the `Py.Stringable` and `Py.Hashable` marker interfaces to generate these methods (by @dbrattli)
1314

1415
### Added

src/Fable.Transforms/Python/Fable2Python.Annotation.fs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,12 +373,28 @@ let makeGenericTypeAnnotation'
373373

374374
Expression.subscript (name, Expression.tuple genArgs)
375375

376+
/// Creates a subscript expression for generic type parameters from a list of names.
377+
/// For a single param, returns just the name; for multiple, returns a tuple.
378+
/// E.g., [] -> baseExpr, [T] -> baseExpr[T], [T1, T2] -> baseExpr[T1, T2]
379+
let makeGenericParamSubscript (genParamNames: string list) (baseExpr: Expression) =
380+
if List.isEmpty genParamNames then
381+
baseExpr
382+
else
383+
let genArgs = genParamNames |> List.map Expression.name
384+
385+
let slice =
386+
match genArgs with
387+
| [ single ] -> single
388+
| multiple -> Expression.tuple multiple
389+
390+
Expression.subscript (baseExpr, slice)
391+
376392
let resolveGenerics com ctx generics repeatedGenerics : Expression list * Statement list =
377393
generics
378394
|> List.map (typeAnnotation com ctx repeatedGenerics)
379395
|> Helpers.unzipArgs
380396

381-
let typeAnnotation
397+
let rec typeAnnotation
382398
(com: IPythonCompiler)
383399
ctx
384400
(repeatedGenerics: Set<string> option)
@@ -644,7 +660,36 @@ let makeEntityTypeAnnotation com ctx (entRef: Fable.EntityRef) genArgs repeatedG
644660
| "string" -> StringTypeAnnotation
645661
| _ -> AnyTypeAnnotation*)
646662
| Expression.Name { Id = Identifier id } ->
647-
makeGenericTypeAnnotation com ctx id genArgs repeatedGenerics, stmts
663+
// For F# union types, tryPyConstructor returns the underscore-prefixed base class
664+
// name (e.g., "_MyUnion"). For type annotations:
665+
// - Inside base class definition: use base class name (_MyUnion)
666+
// - Elsewhere: use type alias (MyUnion) for public API
667+
let isInsideThisUnionBaseClass =
668+
match ctx.EnclosingUnionBaseClass with
669+
| Some enclosingName -> ent.DisplayName = enclosingName
670+
| None -> false
671+
672+
let annotationName =
673+
if
674+
ent.IsFSharpUnion
675+
&& id.StartsWith("_", StringComparison.Ordinal)
676+
&& not isInsideThisUnionBaseClass
677+
then
678+
// Outside base class - use type alias (strip underscore)
679+
id.Substring(1)
680+
else
681+
// Inside base class or not a union - use as-is
682+
id
683+
684+
// Import the type if it's from another file
685+
if ent.IsFSharpUnion then
686+
match ent.Ref.SourcePath with
687+
| Some path when path <> com.CurrentFile ->
688+
let importPath = Path.getRelativeFileOrDirPath false com.CurrentFile false path
689+
com.GetImportExpr(ctx, importPath, annotationName) |> ignore
690+
| _ -> ()
691+
692+
makeGenericTypeAnnotation com ctx annotationName genArgs repeatedGenerics, stmts
648693
// TODO: Resolve references to types in nested modules
649694
| _ -> stdlibModuleTypeHint com ctx "typing" "Any" [] repeatedGenerics
650695
| None -> stdlibModuleTypeHint com ctx "typing" "Any" [] repeatedGenerics
@@ -684,6 +729,7 @@ let makeBuiltinTypeAnnotation com ctx typ repeatedGenerics kind =
684729
fableModuleAnnotation com ctx "result" "FSharpResult_2" resolved, stmts
685730
| Replacements.Util.FSharpChoice genArgs ->
686731
let resolved, stmts = resolveGenerics com ctx genArgs repeatedGenerics
732+
// Use the type alias (clean name without underscore prefix)
687733
let name = $"FSharpChoice_%d{List.length genArgs}"
688734
fableModuleAnnotation com ctx "choice" name resolved, stmts
689735
| _ -> stdlibModuleTypeHint com ctx "typing" "Any" [] repeatedGenerics

src/Fable.Transforms/Python/Fable2Python.Reflection.fs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,34 @@ let private transformUnionReflectionInfo com ctx r (ent: Fable.Entity) generics
8585

8686
let py, stmts = pyConstructor com ctx ent
8787

88-
[ fullnameExpr; arrayExpr com ctx generics; py; cases ]
88+
// Generate case constructors list for make_union
89+
// Use full case class names (UnionName_CaseName) to match the generated classes,
90+
// except for library types (Result, Choice) which use simple names
91+
let usesSimpleNames = Util.usesSimpleCaseNames ent.FullName
92+
93+
// Get the entity declaration name (with module scope) for consistent naming
94+
let entityDeclName = FSharp2Fable.Helpers.getEntityDeclarationName com ent.Ref
95+
96+
let caseConstructors =
97+
ent.UnionCases
98+
|> Seq.map (fun uci ->
99+
let caseName =
100+
match uci.CompiledName with
101+
| Some cname -> cname
102+
| None -> uci.Name
103+
104+
let caseClassName =
105+
if usesSimpleNames then
106+
caseName
107+
else
108+
$"%s{entityDeclName}_%s{caseName}"
109+
110+
com.GetIdentifierAsExpr(ctx, caseClassName)
111+
)
112+
|> Seq.toList
113+
|> Expression.list
114+
115+
[ fullnameExpr; arrayExpr com ctx generics; py; cases; caseConstructors ]
89116
|> libReflectionCall com ctx None "union",
90117
stmts
91118

0 commit comments

Comments
 (0)