Skip to content

Commit 3a7a004

Browse files
authored
[Python] type annotation fixes (#4324)
1 parent 17782c8 commit 3a7a004

21 files changed

Lines changed: 357 additions & 388 deletions

pyproject.toml

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,26 +41,6 @@ include = ["./temp/fable-library-py/fable_library"]
4141
requires = ["hatchling"]
4242
build-backend = "hatchling.build"
4343

44-
[tool.pyright]
45-
reportMissingTypeStubs = false
46-
reportMissingImports = false
47-
reportUnnecessaryTypeIgnoreComment = true
48-
reportUnusedImport = false # Generated code may have unused imports due to optimizations, will be removed by Ruff formatter
49-
reportUnusedVariable = false
50-
reportUnnecessaryIsInstance = true
51-
reportUnnecessaryComparison = false # Generated Option tests compare unwrapped values to None
52-
reportUnnecessaryCast = true
53-
reportPrivateUsage = false # Getters/setters reference private members from outside the class
54-
reportImportCycles = true
55-
reportDuplicateImport = true
56-
reportConstantRedefinition = true
57-
reportOverlappingOverload = true
58-
reportInconsistentConstructor = true
59-
reportImplicitStringConcatenation = true
60-
61-
pythonVersion = "3.12"
62-
typeCheckingMode = "standard"
63-
6444
[tool.uv]
6545
package = false # This is a project, not a package
6646

pyrightconfig.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"pythonVersion": "3.12",
3+
"typeCheckingMode": "standard",
4+
"reportUnusedImport": false, // Generated code may have unused imports due to optimizations, will be removed by Ruff formatter
5+
"reportPrivateUsage": false, // Getters/setters reference private members from outside the class
6+
"reportUnusedVariable": false, // Generated code may have unused variables
7+
"reportMissingTypeStubs": false, // Pyright is confused by Fable Library code structure
8+
"reportMissingModuleSource": false, // Fable generates code that from F# and Rust
9+
"reportConstantRedefinition": false, // Fable preserves casing for variables starting with uppercase
10+
"reportUnnecessaryIsInstance": false, // Not a problem in generated code (look into this later)
11+
"reportUnnecessaryComparison": false, // Generated tests do this on purpose
12+
"reportImportCycles": true,
13+
"reportMissingImports": true,
14+
"reportUnnecessaryCast": true,
15+
"reportDuplicateImport": true,
16+
"reportOverlappingOverload": true,
17+
"reportInconsistentConstructor": true,
18+
"reportImplicitStringConcatenation": true,
19+
"reportUnnecessaryTypeIgnoreComment": true,
20+
"exclude": [
21+
"**/.venv/**",
22+
"**/node_modules/**"
23+
]
24+
}

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

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,62 @@ let getGenericArgs (typ: Fable.Type) : Fable.Type list =
7171
let containsGenericParams (t: Fable.Type) =
7272
FSharp2Fable.Util.getGenParamNames [ t ] |> List.isEmpty |> not
7373

74+
/// Check if a type is a callable type (Lambda or Delegate)
75+
let isCallableType (t: Fable.Type) =
76+
match t with
77+
| Fable.LambdaType _
78+
| Fable.DelegateType _ -> true
79+
| _ -> false
80+
81+
/// Get the final (non-callable) return type from a nested callable type.
82+
/// For A -> B -> C -> int, returns int.
83+
let rec getFinalReturnType (t: Fable.Type) =
84+
match t with
85+
| Fable.LambdaType(_, returnType) -> getFinalReturnType returnType
86+
| Fable.DelegateType(_, returnType) -> getFinalReturnType returnType
87+
| _ -> t
88+
89+
/// Get the immediate return type of a callable (one level deep).
90+
let getImmediateReturnType (t: Fable.Type) =
91+
match t with
92+
| Fable.LambdaType(_, returnType) -> returnType
93+
| Fable.DelegateType(_, returnType) -> returnType
94+
| _ -> t
95+
96+
/// Generate type annotation for a callable (lambda) type.
97+
/// For nested callables:
98+
/// - If returned callable returns another callable: Callable[..., Any]
99+
/// - If returned callable returns concrete type: Callable[..., Callable[..., ConcreteType]]
100+
/// For simple callables (depth 1), preserves full type information.
101+
let makeLambdaTypeAnnotation
102+
(com: IPythonCompiler)
103+
ctx
104+
(repeatedGenerics: Set<string> option)
105+
(argType: Fable.Type)
106+
(returnType: Fable.Type)
107+
: Expression * Statement list
108+
=
109+
if isCallableType returnType then
110+
// Check if the returned callable also returns a callable
111+
let innerReturnType = getImmediateReturnType returnType
112+
113+
if isCallableType innerReturnType then
114+
// Deeply nested: Callable[..., Any]
115+
let any, stmts = stdlibModuleTypeHint com ctx "typing" "Any" [] repeatedGenerics
116+
stdlibModuleAnnotation com ctx "collections.abc" "Callable" [ Expression.ellipsis; any ], stmts
117+
else
118+
// Returned callable returns concrete type: Callable[..., Callable[..., ConcreteType]]
119+
let concreteReturnExpr, stmts =
120+
typeAnnotation com ctx repeatedGenerics innerReturnType
121+
122+
let innerCallable =
123+
stdlibModuleAnnotation com ctx "collections.abc" "Callable" [ Expression.ellipsis; concreteReturnExpr ]
124+
125+
stdlibModuleAnnotation com ctx "collections.abc" "Callable" [ Expression.ellipsis; innerCallable ], stmts
126+
else
127+
// Simple case: Callable[[A], B] where B is not a callable - preserve full types
128+
stdlibModuleTypeHint com ctx "collections.abc" "Callable" [ argType; returnType ] repeatedGenerics
129+
74130
let getEntityGenParams (ent: Fable.Entity) =
75131
ent.GenericParameters |> Seq.map (fun x -> x.Name) |> Set.ofSeq
76132

@@ -88,9 +144,31 @@ let makeMemberTypeParams (com: IPythonCompiler) ctx (genParams: Fable.GenericPar
88144
else
89145
[]
90146

147+
/// Try to convert a generic constraint type to its non-generic base type.
148+
/// Python 3.12+ TypeVar bounds cannot use parameterized generic types,
149+
/// so we map e.g., IEnumerable<'T> to IEnumerable (non-generic).
150+
let private tryGetNonGenericBase (target: Fable.Type) : Fable.Type option =
151+
match target with
152+
| Fable.DeclaredType(entRef, _genArgs) ->
153+
match entRef.FullName with
154+
// IEnumerable<T> -> IEnumerable (non-generic)
155+
| Types.ienumerableGeneric ->
156+
let nonGenericRef: Fable.EntityRef =
157+
{
158+
FullName = Types.ienumerable
159+
Path = Fable.CoreAssemblyName "System.Runtime"
160+
}
161+
162+
Some(Fable.DeclaredType(nonGenericRef, []))
163+
// Add other mappings here as needed:
164+
// Types.icomparableGeneric -> Types.icomparable, etc.
165+
| _ -> None
166+
| _ -> None
167+
91168
/// Extract bound type from CoercesTo constraint if present.
92169
/// Returns the first CoercesTo constraint target type, or None if no such constraint exists.
93-
/// Only returns non-generic bounds since Python 3.12+ TypeVar bounds cannot be parameterized.
170+
/// For bounds with generic parameters, attempts to use a non-generic base type instead,
171+
/// since Python 3.12+ TypeVar bounds cannot be parameterized.
94172
let tryGetCoercesToBound (constraints: Fable.Constraint list) : Fable.Type option =
95173
constraints
96174
|> List.tryPick (
@@ -99,7 +177,8 @@ let tryGetCoercesToBound (constraints: Fable.Constraint list) : Fable.Type optio
99177
// Python 3.12+ doesn't support parameterized generic types as bounds
100178
// e.g., T: IEnumerable[U] is invalid, only T: SomeNonGenericType works
101179
if containsGenericParams target then
102-
None
180+
// Try to use a non-generic base type instead
181+
tryGetNonGenericBase target
103182
else
104183
Some target
105184
| _ -> None
@@ -305,10 +384,7 @@ let typeAnnotation
305384
| Fable.Char -> Expression.name "str", []
306385
| Fable.String -> Expression.name "str", []
307386
| Fable.Number(kind, info) -> makeNumberTypeAnnotation com ctx kind info
308-
| Fable.LambdaType(argType, returnType) ->
309-
// Keep curried structure: A -> (B -> C) becomes Callable[[A], Callable[[B], C]]
310-
// This matches the actual runtime code which generates nested lambdas
311-
stdlibModuleTypeHint com ctx "collections.abc" "Callable" [ argType; returnType ] repeatedGenerics
387+
| Fable.LambdaType(argType, returnType) -> makeLambdaTypeAnnotation com ctx repeatedGenerics argType returnType
312388
| Fable.DelegateType(argTypes, returnType) ->
313389
stdlibModuleTypeHint com ctx "collections.abc" "Callable" (argTypes @ [ returnType ]) repeatedGenerics
314390
| Fable.Nullable(genArg, isStruct) ->
@@ -545,13 +621,8 @@ let makeBuiltinTypeAnnotation com ctx typ repeatedGenerics kind =
545621
match kind with
546622
| Replacements.Util.BclGuid -> stdlibModuleTypeHint com ctx "uuid" "UUID" [] repeatedGenerics
547623
| Replacements.Util.FSharpReference genArg ->
548-
// For inref types (like struct instance member's 'this' parameter),
549-
// use the inner type directly since Python doesn't wrap them in FSharpRef
550-
if isInRefOrAnyType com typ then
551-
typeAnnotation com ctx repeatedGenerics genArg
552-
else
553-
let resolved, stmts = resolveGenerics com ctx [ genArg ] repeatedGenerics
554-
fableModuleAnnotation com ctx "types" "FSharpRef" resolved, stmts
624+
let resolved, stmts = resolveGenerics com ctx [ genArg ] repeatedGenerics
625+
fableModuleAnnotation com ctx "types" "FSharpRef" resolved, stmts
555626
(*
556627
| Replacements.Util.BclTimeSpan -> NumberTypeAnnotation
557628
| Replacements.Util.BclDateTime -> makeSimpleTypeAnnotation com ctx "Date"
@@ -574,6 +645,10 @@ let makeBuiltinTypeAnnotation com ctx typ repeatedGenerics kind =
574645
let resolved, stmts = resolveGenerics com ctx [ ok; err ] repeatedGenerics
575646

576647
fableModuleAnnotation com ctx "result" "FSharpResult_2" resolved, stmts
648+
| Replacements.Util.FSharpChoice genArgs ->
649+
let resolved, stmts = resolveGenerics com ctx genArgs repeatedGenerics
650+
let name = $"FSharpChoice_%d{List.length genArgs}"
651+
fableModuleAnnotation com ctx "choice" name resolved, stmts
577652
| _ -> stdlibModuleTypeHint com ctx "typing" "Any" [] repeatedGenerics
578653

579654
let transformFunctionWithAnnotations

0 commit comments

Comments
 (0)