fable-compiler / Fable

F# to JavaScript, TypeScript, Python, Rust and Dart Compiler
http://fable.io/
MIT License
2.89k stars 295 forks source link

python: anonymous record creation generates camel case dict fields #3870

Open joprice opened 1 month ago

joprice commented 1 month ago

Description

When using the python backend and making use of an anonymous record, the accessors use snake case, while the construction code has camel cased fields, resulting in a KeyError when called:

Repro code

type data = {| fromId: int |}

// this creates a dict with camel case field "fromId"
let makeData() = {| fromId = 1 |}

let y (d: data) = 
  d.fromId
from typing import Any

def make_data(__unit: None=None) -> dict[str, Any]:
    return {
        "fromId": 1
    }

def y(d: dict[str, Any]) -> int:
    return d["from_id"]

https://fable.io/repl/#?code=FAFwngDgpgBAJgQxAmBeGBvAPjAZgJwHsBbASTgC4YBLAOxBiwF9hgB6NmEAC2oGcYAY3xQkUASjjVBDAO7UeQhMSgAbJX1i5qauDABEBEuX3BVUBsQQBrKABEkCABQBKNJhxGye9AEZGLGYWMGAwTpTwjm7owDDwAHRe5EA&html=Q&css=Q

Related information

joprice commented 1 month ago

Something else to note: when the field starts with a capital, it is left unchanged:

type data = {| FromId: int |}

// this creates a dict with camel case field "fromId"
let makeData() = {| FromId = 1 |}

let y (d: data) = 
  d.FromId
from typing import Any

def make_data(__unit: None=None) -> dict[str, Any]:
    return {
        "FromId": 1
    }

def y(d: dict[str, Any]) -> int:
    return d["FromId"]
MangelMaxime commented 1 month ago

A similar problem happens when using Record.

type Test =
    {
        FirstName : string  
        lastName : string
    }

let test =
    {
        FirstName = ""
        lastName = ""
    }

test.FirstName
test.lastName
from __future__ import annotations
from dataclasses import dataclass
from fable_library_js.reflection import (TypeInfo, string_type, record_type)
from fable_library_js.types import Record

def _expr0() -> TypeInfo:
    return record_type("Test.Test", [], Test, lambda: [("FirstName", string_type), ("last_name", string_type)])

@dataclass(eq = False, repr = False, slots = True)
class Test(Record):
    FirstName: str
    last_name: str

Test_reflection = _expr0

test: Test = Test("", "")

test.FirstName

test.last_name

I suspect that the convention in Python is to use snake_case? @dbrattli

joprice commented 1 month ago

As far as I can understand, the record one seems to be working, in the sense that the class contains the same fields as the accessor used at the call-site: FirstName:str vs test.FirstName. Whereas with the anonymous one, the two do not line up: the creation side uses camel case {"fromId": 1 } and the accessor uses snake case d["from_id"].

So it seems there's a convention in the python backend of converting to snake case automatically, but it's not implemented correctly for anonymous records.

MangelMaxime commented 1 month ago

Yes, but it also seems like the snake_case conversion is only if you use camelCase.

I don't know if this should also be applied to properties that use PascalCase.

However, if we do so there is a need to mangle the name of the property because FirstName and firstName would both be translated to first_name. Perhaps, this is the reason why only camelCase are transformed.