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
13 changes: 13 additions & 0 deletions src/Fable.Transforms/Python/Fable2Python.Transforms.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3538,6 +3538,18 @@ let declareDataClassType
=
let name = com.GetIdentifier(ctx, entName)

// Record slots follow Python naming conventions, but structured formatting must
// retain the original F# field names. Keep the names on the generated class so
// the runtime can pair them with the slots without attempting a lossy conversion.
let fsharpFieldNames =
ent.FSharpFields
|> List.filter (fun field -> not field.IsStatic)
|> List.map (fun field -> Expression.stringConstant field.Name)
|> Expression.tuple

let fsharpFieldNamesMember =
Statement.assign ([ Expression.name ("__fable_field_names__", Store) ], fsharpFieldNames)

// Generate field annotations from entity's FSharpFields to properly uncurry function types.
// Record/dataclass fields store functions uncurried at runtime, so we need to uncurry
// lambda types in the type annotations to match.
Expand Down Expand Up @@ -3661,6 +3673,7 @@ let declareDataClassType
let classBody =
let body =
[
yield fsharpFieldNamesMember
yield! staticFieldAnnotations
yield! props
yield! classMembers
Expand Down
22 changes: 15 additions & 7 deletions src/fable-library-py/fable_library/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,23 @@ def _field_to_string(value: object) -> str:


def record_to_string(self: Record) -> str:
if hasattr(self, "__slots__"):
return (
"{ "
+ "\n ".join(map(lambda slot: slot + " = " + _field_to_string(getattr(self, slot)), self.__slots__))
+ " }"
)
else:
if not hasattr(self, "__slots__"):
return "{ " + "\n ".join(map(lambda kv: kv[0] + " = " + _field_to_string(kv[1]), self.__dict__.items())) + " }"

slots = self.__slots__
# Newer generated records retain their source-level F# field names here.
# Fall back to slots so the current runtime remains compatible with code
# compiled before that metadata was emitted.
field_names = getattr(type(self), "__fable_field_names__", slots)
return (
"{ "
+ "\n ".join(
f"{field_name} = {_field_to_string(getattr(self, slot))}"
for slot, field_name in zip(slots, field_names, strict=True)
)
+ " }"
)


def record_get_hashcode(self: Record) -> int:
# Hash each field with `structural_hash` rather than hashing a Python tuple,
Expand Down
5 changes: 5 additions & 0 deletions tests/Python/TestRecordType.fs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ type StructRecord = { Size: int; Seed: int; Path: int list }

type StructRecordHolder = { Tag: int; Data: StructRecord }

[<Fact>]
let ``test sprintf formats record fields with their F# names`` () =
sprintf "%A" { Size = 1; Seed = 2; Path = [] }
|> equal "{ Size = 1\n Seed = 2\n Path = [] }"

let updateStructRecord (data: StructRecord) (path: int list) = { data with Path = List.rev path }

let holdStructRecord (data: StructRecord) (path: int list) =
Expand Down
Loading