Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ reportMissingTypeStubs = false
reportMissingImports = false
reportUnnecessaryTypeIgnoreComment = true
reportUnusedImport = true
reportUnusedVariable = true
reportUnusedVariable = false
reportUnnecessaryIsInstance = true
reportUnnecessaryComparison = true
reportUnnecessaryCast = true
Expand All @@ -54,7 +54,7 @@ reportOverlappingOverload = true
reportInconsistentConstructor = true
reportImplicitStringConcatenation = true
pythonVersion = "3.12"
typeCheckingMode = "strict"
typeCheckingMode = "standard"

[tool.ruff]
# Keep in sync with .pre-commit-config.yaml
Expand Down
1 change: 0 additions & 1 deletion src/Fable.Build/FableLibrary/Python.fs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ type BuildFableLibraryPython() =

Shell.deleteDir (this.BuildDir </> "fable_library/fable-library-ts")


// Run Ruff linter checking import sorting and fix any issues
Command.Run("uv", $"run ruff check --select I --fix {this.BuildDir}")
// Run Ruff formatter on all generated files
Expand Down
1 change: 1 addition & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

* [Python] Improve Python (e.g. Pydantic) interop (by @dbrattli)
* [All] Fixed #4041 missing unit argument (by @ncave)
* [JS/TS/Python] Fixed eq comparer mangling (by @ncave)
* [All] Fix all `BitConverter` return types (by @ncave)
Expand Down
4 changes: 0 additions & 4 deletions src/Fable.Transforms/Python/Fable2Python.Annotation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,6 @@ let getGenericTypeParams (types: Fable.Type list) =
let getEntityGenParams (ent: Fable.Entity) =
ent.GenericParameters |> Seq.map (fun x -> x.Name) |> Set.ofSeq

let makeTypeParamDecl (com: IPythonCompiler) ctx (genParams: Set<string>) =
// Python 3.12+ syntax: no longer need Generic[T] base classes
[]

let makeTypeParams (com: IPythonCompiler) ctx (genParams: Set<string>) : TypeParam list =
// Python 3.12+ syntax: create TypeParam list for class/function declaration
genParams
Expand Down
165 changes: 109 additions & 56 deletions src/Fable.Transforms/Python/Fable2Python.Transforms.fs
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ let extractBaseExprFromBaseCall (com: IPythonCompiler) (ctx: Context) (baseType:
| Fable.IdentExpr id -> com.GetIdentifierAsExpr(ctx, id.Name), []
| _ -> transformAsExpr com ctx baseRef

let expr, keywords, stmts' = transformCallArgs com ctx info
let expr, keywords, stmts' = transformCallArgs com ctx info true

Some(baseExpr, (expr, keywords, stmts @ stmts'))
| Some(Fable.ObjectExpr([], Fable.Unit, None)), _ ->
Expand Down Expand Up @@ -640,10 +640,12 @@ let transformObjectExpr

Expression.call (Expression.name name), [ stmt ] @ stmts


let transformCallArgs
(com: IPythonCompiler)
ctx
(callInfo: Fable.CallInfo)
(isBaseConstructorCall: bool)
: Expression list * Keyword list * Statement list
=

Expand All @@ -653,7 +655,11 @@ let transformCallArgs
let paramsInfo =
callInfo.MemberRef |> Option.bind com.TryGetMember |> Option.map getParamsInfo

let args, objArg, stmts =
// Enhanced handling for constructor calls with named arguments
let isConstructorCall =
List.contains "new" callInfo.Tags && not isBaseConstructorCall

let getCallArgs paramsInfo args =
paramsInfo
|> Option.map (splitNamedArgs args)
|> function
Expand All @@ -662,21 +668,69 @@ let transformCallArgs
| Some(args, Some namedArgs) ->
let objArg, stmts =
namedArgs
|> List.choose (fun (p, v) ->
match p.Name, v with
| Some k, Fable.Value(Fable.NewOption(value, _, _), _) -> value |> Option.map (fun v -> k, v)
| Some k, v -> Some(k, v)
|> List.choose (fun (param, value) ->
match param.Name, value with
| Some keyword, Fable.Value(Fable.NewOption(value, _, _), _) ->
value |> Option.map (fun value -> keyword, value)
| Some keyword, value -> Some(keyword, value)
| None, _ -> None
)
|> List.map (fun (k, v) -> k, com.TransformAsExpr(ctx, v))
|> List.map (fun (k, (v, stmts)) -> ((k, v), stmts))
|> List.unzip
|> (fun (kv, stmts) ->
kv |> List.map (fun (k, v) -> Keyword.keyword (Identifier k, v)), stmts |> List.collect id
|> List.map (fun (keyword, value) ->
let value, stmts = com.TransformAsExpr(ctx, value)
(keyword, value), stmts
)
|> List.unzip
|> fun (kv, stmts) ->
kv
|> List.map (fun (keyword, value) -> Keyword.keyword (Identifier keyword, value)),
stmts |> List.collect id

args, Some objArg, stmts

let args, objArg, stmts =
match paramsInfo, isConstructorCall with
| Some info, true when args.Length <= info.Parameters.Length && args.Length > 0 ->
// For constructor calls, check if we should use keyword arguments
// Only apply if we have parameter names for all arguments
let relevantParams = List.take args.Length info.Parameters

let hasAllParameterNames = relevantParams |> List.forall (fun p -> p.Name.IsSome)

if hasAllParameterNames then
// Try to create keyword arguments for constructor calls
let keywordArgs =
List.zip relevantParams args
|> List.choose (fun (param, arg) ->
match param.Name with
| Some paramName -> Some(paramName, arg)
| None -> None
)

if keywordArgs.Length = args.Length then
// All parameters have names, convert to keyword arguments
let objArg, stmts =
keywordArgs
|> List.map (fun (kw, value) ->
let value, stmts = com.TransformAsExpr(ctx, value)
(kw, value), stmts
)
|> List.unzip
|> fun (kv, stmts) ->
kv
|> List.map (fun (keyword, value) -> Keyword.keyword (Identifier keyword, value)),
stmts |> List.collect id

[], Some objArg, stmts
else
// Fallback to regular handling
getCallArgs paramsInfo args
else
// Fallback to regular handling when not all parameters have names
getCallArgs paramsInfo args
| _ ->
// Regular handling for non-constructor calls
getCallArgs paramsInfo args

let hasSpread =
paramsInfo |> Option.map (fun i -> i.HasSpread) |> Option.defaultValue false

Expand All @@ -703,10 +757,7 @@ let transformCallArgs

match objArg with
| None -> args, [], stmts @ stmts'
| Some objArg ->
//let name = Expression.name(Helpers.getUniqueIdentifier "kw")
//let kw = Statement.assign([ name], objArg)
args, objArg, stmts @ stmts'
| Some objArg -> args, objArg, stmts @ stmts'

let resolveExpr (ctx: Context) _t strategy pyExpr : Statement list =
// printfn "resolveExpr: %A" (pyExpr, strategy)
Expand All @@ -722,12 +773,6 @@ let resolveExpr (ctx: Context) _t strategy pyExpr : Statement list =
let transformOperation com ctx range opKind tags : Expression * Statement list =
match opKind with
| Fable.Unary(op, TransformExpr com ctx (expr, stmts)) -> Expression.unaryOp (op, expr, ?loc = range), stmts

// | Fable.Binary (BinaryInstanceOf, TransformExpr com ctx (left, stmts), TransformExpr com ctx (right, stmts')) ->
// let func = Expression.name ("isinstance")
// let args = [ left; right ]
// Expression.call (func, args), stmts' @ stmts

| Fable.Binary(op, left, right: Fable.Expr) ->
let typ = right.Type
let left_typ = left.Type
Expand Down Expand Up @@ -816,7 +861,7 @@ let transformEmit (com: IPythonCompiler) ctx range (info: Fable.EmitInfo) =
|> Option.toList
|> Helpers.unzipArgs

let exprs, kw, stmts' = transformCallArgs com ctx info
let exprs, kw, stmts' = transformCallArgs com ctx info false

if macro.StartsWith("functools", StringComparison.Ordinal) then
com.GetImportExpr(ctx, "functools") |> ignore
Expand Down Expand Up @@ -858,7 +903,7 @@ let transformCall (com: IPythonCompiler) ctx range callee (callInfo: Fable.CallI
// printfn "transformCall: %A" (callee, callInfo)
let callee', stmts = com.TransformAsExpr(ctx, callee)

let args, kw, stmts' = transformCallArgs com ctx callInfo
let args, kw, stmts' = transformCallArgs com ctx callInfo false

match callee, callInfo.ThisArg with
| Fable.Get(expr, Fable.FieldGet { Name = "Dispose" }, _, _), _ ->
Expand Down Expand Up @@ -2330,9 +2375,6 @@ let declareModuleMember (com: IPythonCompiler) ctx _isPublic (membName: Identifi
let name = Expression.name membName
varDeclaration ctx name typ expr

let makeEntityTypeParamDecl (com: IPythonCompiler) ctx (ent: Fable.Entity) =
getEntityGenParams ent |> makeTypeParamDecl com ctx

let makeEntityTypeParams (com: IPythonCompiler) ctx (ent: Fable.Entity) : TypeParam list =
getEntityGenParams ent |> makeTypeParams com ctx

Expand Down Expand Up @@ -2403,7 +2445,6 @@ let declareDataClassType
)


let generics = makeEntityTypeParamDecl com ctx ent
let typeParams = makeEntityTypeParams com ctx ent
let bases = baseExpr |> Option.toList

Expand Down Expand Up @@ -2485,27 +2526,30 @@ let transformClassAttributes
// Default values are handled by the constructor parameters
Statement.annAssign (Expression.name (propertyName |> Naming.toPropertyNaming), annotation = ta)
else
// For attributes style with init=false, class attributes should have initial values
// Use the extracted initial values from the constructor body
// printfn "Looking for extracted initial value for property: %s" propertyName
let defaultValue =
// For attributes style with init=false, avoid emitting initializers that reference
// constructor parameters (e.g., Age: int = Age) because no __init__ is generated and
// libraries like Pydantic will synthesize it. Keep literal/call defaults like Field("...").
let defaultValueOpt =
extractedInitialValues
|> List.tryFind (fun (propName, _getterMemb, _defaultValue) ->
propName = (propertyName |> Naming.toPropertyNaming)
)
|> function
| Some(_propName, _getterMemb, defaultValue) ->
// printfn "Found extracted initial value for %s: %A" propertyName defaultValue
defaultValue
| None ->
// printfn "No extracted initial value found for %s, using type default" propertyName
Util.getDefaultValueForType com ctx getter.ReturnParameter.Type

Statement.annAssign (
Expression.name (propertyName |> Naming.toPropertyNaming),
annotation = ta,
value = defaultValue
)
|> Option.map (fun (_propName, _getterMemb, defaultValue) -> defaultValue)

match defaultValueOpt with
// Skip defaults that are just bare names (likely ctor params), emit only annotation
| Some(Name _) ->
Statement.annAssign (Expression.name (propertyName |> Naming.toPropertyNaming), annotation = ta)
// If we have a non-name default (e.g., Field("Name")), keep it
| Some defaultValue ->
Statement.annAssign (
Expression.name (propertyName |> Naming.toPropertyNaming),
annotation = ta,
value = defaultValue
)
// Otherwise, emit only the annotation (no default)
| None ->
Statement.annAssign (Expression.name (propertyName |> Naming.toPropertyNaming), annotation = ta)
)
|> List.ofSeq
| _ -> []
Expand All @@ -2520,13 +2564,12 @@ let declareClassType
(consBody: Statement list)
(baseExpr: Expression option)
(classMembers: Statement list)
slotMembers
(slotMembers: Statement list)
(classAttributes: ClassAttributes option)
(attachedMembers: Fable.MemberDecl list)
(extractedInitialValues: (string * Fable.MemberDecl * Expression) list)
=
// printfn "declareClassType: %A" consBody
let generics = makeEntityTypeParamDecl com ctx ent
let typeParams = makeEntityTypeParams com ctx ent

let fieldTypes =
Expand Down Expand Up @@ -3143,19 +3186,29 @@ let transformClassWithPrimaryConstructor
if classAttributes.Style = ClassStyle.Attributes then
if classAttributes.Init then
// For attributes style with init=true, generate new constructor with property names
// Place required params (no safe default) before optional params (with safe default),
// so Python's trailing-default rule is respected.
let requiredProps, optionalProps =
extractedInitialValues
|> List.partition (fun (propertyName, _getterMemb, defaultValue) ->
match defaultValue with
// Treat as required only when default is a bare name equal to the parameter itself
// e.g., Age: int = Age. Keep None and other expressions as optional defaults.
| Name name when name.Id.Name = propertyName -> true
| _ -> false
)

let propertyArgs =
let args =
extractedInitialValues
|> List.map (fun (propertyName, getterMemb, _defaultValue) ->
let ta, _ = Annotation.typeAnnotation com ctx None getterMemb.Body.Type
Arg.arg (propertyName, annotation = ta)
)
let toArg (propertyName: string, getterMemb: Fable.MemberDecl, _defaultValue: Expression) =
let ta, _ = Annotation.typeAnnotation com ctx None getterMemb.Body.Type
Arg.arg (propertyName, annotation = ta)

let argsRequired = requiredProps |> List.map toArg
let argsOptional = optionalProps |> List.map toArg

let defaults =
extractedInitialValues
|> List.map (fun (_propertyName, _getterMemb, defaultValue) -> defaultValue)
let defaults = optionalProps |> List.map (fun (_pn, _gm, dv) -> dv)

Arguments.arguments (args = args, defaults = defaults)
Arguments.arguments (args = (argsRequired @ argsOptional), defaults = defaults)

let propertyBody =
extractedInitialValues
Expand Down
6 changes: 4 additions & 2 deletions src/Fable.Transforms/Python/Replacements.fs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,9 @@ let toInt com (ctx: Context) r targetType (args: Expr list) =
| UInt8 -> Helper.LibCall(com, "types", "byte", targetType, [ arg ])
| UInt16 -> Helper.LibCall(com, "types", "uint16", targetType, [ arg ])
| UInt32 -> Helper.LibCall(com, "types", "uint32", targetType, [ arg ])
| _ -> FableError $"Unexpected non-integer type %A{typeTo}" |> raise
| _ ->
// Use normal Python int for BigInt, NativeInt, UNativeInt
Helper.GlobalCall("int", targetType, [ arg ])

match sourceType, targetType with
| Char, Number(typeTo, _) ->
Expand All @@ -326,7 +328,7 @@ let toChar com (ctx: Context) r (arg: Expr) =
| Char
| String -> arg
| _ ->
let code = toInt com ctx r UInt16.Number [ arg ]
let code = Helper.GlobalCall("int", Int32.Number, [ arg ])
Helper.GlobalCall("chr", Char, [ code ])

let toString com (ctx: Context) r (args: Expr list) =
Expand Down
6 changes: 3 additions & 3 deletions src/fable-library-py/fable_library/big_int.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from decimal import Decimal
from typing import Any

from .types import FSharpRef
from .types import FSharpRef, int32, int64


def compare(x: int, y: int) -> int:
Expand Down Expand Up @@ -188,11 +188,11 @@ def from_one() -> int:
return 1


def from_int32(x: int) -> int:
def from_int32(x: int32) -> int:
return int(x)


def from_int64(x: int) -> int:
def from_int64(x: int64) -> int:
return int(x)


Expand Down
2 changes: 2 additions & 0 deletions src/fable-library-py/fable_library/core/array.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ ArrayType = Literal[
"UInt8",
"Int16",
"UInt16",
"Int32",
"UInt32",
"SupportsInt",
"USupportsInt",
"Int64",
Expand Down
Loading
Loading