From b7bdd215a12ea40f5e9156ba7b1a42ca76e107fd Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 30 Sep 2026 19:22:23 +0200 Subject: [PATCH 1/2] fix(python): import union cases for reflection Fixes #5025 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Python/Fable2Python.Reflection.fs | 23 +------- .../Python/Fable2Python.Transforms.fs | 55 ------------------- .../Python/Fable2Python.Util.fs | 47 ++++++++++++++++ tests/Python/Fable.Tests.Python.fsproj | 1 + tests/Python/TestReflectionUnionImports.fs | 19 +++++++ 5 files changed, 68 insertions(+), 77 deletions(-) create mode 100644 tests/Python/TestReflectionUnionImports.fs diff --git a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs index c28508fd91..6dd9bac272 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs @@ -104,30 +104,9 @@ let private transformUnionReflectionInfo com ctx r (ent: Fable.Entity) generics let py, stmts = pyConstructor com ctx ent - // Generate case constructors list for make_union - // Use full case class names (UnionName_CaseName) to match the generated classes, - // except for library types (Result, Choice) which use simple names - let usesSimpleNames = Util.usesSimpleCaseNames ent.FullName - - // Get the entity declaration name (with module scope) for consistent naming - let entityDeclName = FSharp2Fable.Helpers.getEntityDeclarationName com ent.Ref - let caseConstructors = ent.UnionCases - |> Seq.map (fun uci -> - let caseName = - match uci.CompiledName with - | Some cname -> cname - | None -> uci.Name - - let caseClassName = - if usesSimpleNames then - caseName - else - $"%s{entityDeclName}_%s{caseName}" - - com.GetIdentifierAsExpr(ctx, caseClassName) - ) + |> Seq.map (Util.getUnionCaseRef com ctx ent.Ref ent) |> Seq.toList |> Expression.list diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index 5bde8afa1f..cfbe4a9ebd 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -111,65 +111,10 @@ let getMemberArgsAndBody (com: IPythonCompiler) ctx kind hasSpread (args: Fable. args, body, returnType -let getUnionCaseName (uci: Fable.UnionCase) = - match uci.CompiledName with - | Some cname -> cname - | None -> uci.Name - /// Attribute holding the precomputed singleton instance of a zero-field union case (see transformUnion). let unionCaseSingletonAttrName = "singleton" let unionCaseSingletonAttr = Identifier unionCaseSingletonAttrName -/// Gets the unique case class name by prefixing with the union type name. -/// This prevents collisions when different union types have cases with the same name. -/// Library types (Result, Choice) use simple case names without prefix. -/// The optional entityName parameter should be the compiled entity name (with module scope). -let getUnionCaseClassName - (com: IPythonCompiler) - (ent: Fable.Entity) - (uci: Fable.UnionCase) - (entityName: string option) - = - let caseName = getUnionCaseName uci - // Library types use simple names (Ok, Error, Choice1Of2, etc.) for backwards compatibility - if usesSimpleCaseNames ent.FullName then - caseName - else - // Use provided entity name or compute from entity reference - let unionName = - match entityName with - | Some name -> name - | None -> FSharp2Fable.Helpers.getEntityDeclarationName com ent.Ref - - $"%s{unionName}_%s{caseName}" - -/// Resolves the expression referring to a union case's class (constructor / type), -/// handling both library types (Result, Choice - simple names from fable_library) -/// and user-defined unions (full case class name, imported from another module if needed). -let getUnionCaseRef (com: IPythonCompiler) ctx (entRef: Fable.EntityRef) (ent: Fable.Entity) (uci: Fable.UnionCase) = - if isLibraryUnionType entRef.FullName then - let caseName = getUnionCaseName uci - // Result uses "result" module, Choice uses "choice" module - let moduleName = - if entRef.FullName = Types.result then - "result" - else - "choice" - - libValue com ctx moduleName caseName - else - // User-defined union - use full case class name (UnionName_CaseName) - let caseClassName = getUnionCaseClassName com ent uci None - - match entRef.SourcePath with - | Some path when path <> com.CurrentFile -> - // Import from another module - let importPath = Path.getRelativeFileOrDirPath false com.CurrentFile false path - com.GetImportExpr(ctx, importPath, caseClassName) - | _ -> - // Local - just get identifier - com.GetIdentifierAsExpr(ctx, caseClassName) - let getUnionExprTag (com: IPythonCompiler) ctx r (fableExpr: Fable.Expr) = Expression.withStmts { let! expr = com.TransformAsExpr(ctx, fableExpr) diff --git a/src/Fable.Transforms/Python/Fable2Python.Util.fs b/src/Fable.Transforms/Python/Fable2Python.Util.fs index 51a854e994..e449751b98 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Util.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Util.fs @@ -53,6 +53,53 @@ module Util = | "FSharp.Core.FSharpResult`2" -> true | _ -> false + let getUnionCaseName (uci: Fable.UnionCase) = + match uci.CompiledName with + | Some cname -> cname + | None -> uci.Name + + let getUnionCaseClassName + (com: IPythonCompiler) + (ent: Fable.Entity) + (uci: Fable.UnionCase) + (entityName: string option) + = + let caseName = getUnionCaseName uci + + if usesSimpleCaseNames ent.FullName then + caseName + else + let unionName = + match entityName with + | Some name -> name + | None -> FSharp2Fable.Helpers.getEntityDeclarationName com ent.Ref + + $"%s{unionName}_%s{caseName}" + + let getUnionCaseRef + (com: IPythonCompiler) + ctx + (entRef: Fable.EntityRef) + (ent: Fable.Entity) + (uci: Fable.UnionCase) + = + if isLibraryUnionType entRef.FullName then + let moduleName = + if entRef.FullName = Types.result then + "result" + else + "choice" + + libValue com ctx moduleName (getUnionCaseName uci) + else + let caseClassName = getUnionCaseClassName com ent uci None + + match entRef.SourcePath with + | Some path when path <> com.CurrentFile -> + let importPath = Path.getRelativeFileOrDirPath false com.CurrentFile false path + com.GetImportExpr(ctx, importPath, caseClassName) + | _ -> com.GetIdentifierAsExpr(ctx, caseClassName) + /// Ensures a statement list is non-empty by adding Pass if needed. /// Python requires at least one statement in function/class/match bodies. let ensureNonEmptyBody stmts = diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index 7cc7ffee62..24ef0fbcf9 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -63,6 +63,7 @@ + diff --git a/tests/Python/TestReflectionUnionImports.fs b/tests/Python/TestReflectionUnionImports.fs new file mode 100644 index 0000000000..1934a4a7e0 --- /dev/null +++ b/tests/Python/TestReflectionUnionImports.fs @@ -0,0 +1,19 @@ +module Fable.Tests.ReflectionUnionImports + +open FSharp.Reflection +open Util.Testing + +type RecordWithResult = { Value: Result } + +let getGenericTypeArguments (typ: System.Type) = typ.GenericTypeArguments + +[] +let ``test FSharp.Reflection: record field Result imports its case constructors`` () = + let field = FSharpType.GetRecordFields(typeof).[0] + field.PropertyType |> equal typeof> + +[] +let ``test FSharp.Reflection: Choice imports its case constructors`` () = + getGenericTypeArguments typeof> + |> Array.length + |> equal 2 From 3f8606a9392a8977f558f75c8a47bd0264e06105 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Wed, 30 Sep 2026 19:29:08 +0200 Subject: [PATCH 2/2] fix(python): handle Rune reflection type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Fable.Transforms/Python/Fable2Python.Reflection.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs index 6dd9bac272..0c45731748 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Reflection.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Reflection.fs @@ -216,6 +216,7 @@ let transformTypeInfo (com: IPythonCompiler) ctx r (genMap: Map genericEntity fullName [], [] | Replacements.Util.BclHashSet gen | Replacements.Util.FSharpSet gen ->