Skip to content

Commit 05f2bea

Browse files
dbrattliclaude
andauthored
feat(stdlib): expose Python type references and dict/list constructors in Builtins (#291)
Closes #290. Adds the missing pieces in `Fable.Python.Builtins` that downstream interop libraries (Thoth.Json.Python, Fable.TypedJson) currently work around with private `[<Emit>]` declarations: - Type value references (`pyInt`, `pyFloat`, `pyBool`, `pyStr`, `pyBytes`, `pyList`, `pyDict`, `pyTuple`, `pySet`) for use as the second argument to `Fable.Core.PyInterop.pyInstanceof`. The `py` prefix avoids colliding with F#/.NET built-in types. - `pyNone` value for Python's `None` singleton, useful as a JSON-null sentinel and at interop sites that need an explicit None. - `bool`, `dict`, and `list` constructors on `Builtins.IExports`, completing the set alongside the existing `int`/`float`/`str`/`bytes` constructors. `dict` and `list` are typed to return `Dictionary<string,'V>` and `ResizeArray<'T>` respectively to stay idiomatic per BINDINGS_GUIDE.md. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 09cb36b commit 05f2bea

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

src/stdlib/Builtins.fs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,20 @@ type IExports =
200200
abstract int: obj -> int
201201
/// Object to float
202202
abstract float: obj -> float
203+
/// Object to bool
204+
abstract bool: obj -> bool
203205
/// Convert to bytes
204206
abstract bytes: byte[] -> byte[]
205207
/// Convert string to bytes with encoding
206208
abstract bytes: string * encoding: string -> byte[]
207209

210+
/// Create a new empty dictionary.
211+
abstract dict: unit -> Dictionary<string, obj>
212+
/// Create a dictionary from an iterable of key/value pairs.
213+
abstract dict: IEnumerable<string * 'V> -> Dictionary<string, 'V>
214+
/// Create a list from an iterable.
215+
abstract list: IEnumerable<'T> -> ResizeArray<'T>
216+
208217
/// Return the largest item in an iterable or the largest of two or more arguments.
209218
abstract max: 'T * 'T -> 'T
210219
/// Return the largest item in an iterable or the largest of two or more arguments.
@@ -349,6 +358,55 @@ type SystemError() =
349358
[<Emit("__name__")>]
350359
let __name__: string = nativeOnly
351360

361+
// ============================================================================
362+
// Python type references
363+
//
364+
// These expose Python's built-in types as values, primarily for use with
365+
// `Fable.Core.PyInterop.pyInstanceof`. F# names are prefixed with `py` to
366+
// avoid colliding with F#/.NET built-in types.
367+
// ============================================================================
368+
369+
/// Reference to Python's `int` type.
370+
[<Emit("int")>]
371+
let pyInt: obj = nativeOnly
372+
373+
/// Reference to Python's `float` type.
374+
[<Emit("float")>]
375+
let pyFloat: obj = nativeOnly
376+
377+
/// Reference to Python's `bool` type.
378+
[<Emit("bool")>]
379+
let pyBool: obj = nativeOnly
380+
381+
/// Reference to Python's `str` type.
382+
[<Emit("str")>]
383+
let pyStr: obj = nativeOnly
384+
385+
/// Reference to Python's `bytes` type.
386+
[<Emit("bytes")>]
387+
let pyBytes: obj = nativeOnly
388+
389+
/// Reference to Python's `list` type.
390+
[<Emit("list")>]
391+
let pyList: obj = nativeOnly
392+
393+
/// Reference to Python's `dict` type.
394+
[<Emit("dict")>]
395+
let pyDict: obj = nativeOnly
396+
397+
/// Reference to Python's `tuple` type.
398+
[<Emit("tuple")>]
399+
let pyTuple: obj = nativeOnly
400+
401+
/// Reference to Python's `set` type.
402+
[<Emit("set")>]
403+
let pySet: obj = nativeOnly
404+
405+
/// Python's `None` singleton, typed as `obj` for use as a sentinel
406+
/// (e.g. JSON null) and in interop sites that need an explicit None value.
407+
[<Emit("None")>]
408+
let pyNone: obj = nativeOnly
409+
352410
/// Python print function. Takes a single argument, so can be used with e.g string interpolation.
353411
let print obj = builtins.print obj
354412

test/TestBuiltins.fs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module Fable.Python.Tests.Builtins
22

3+
open Fable.Core.PyInterop
34
open Fable.Python.Testing
45
open Fable.Python.Builtins
56
open Fable.Python.Os
@@ -65,3 +66,57 @@ let ``test any works`` () =
6566
builtins.any [ false; false; true ] |> equal true
6667
builtins.any [ false; false; false ] |> equal false
6768
builtins.any [] |> equal false
69+
70+
[<Fact>]
71+
let ``test bool works`` () =
72+
builtins.bool 1 |> equal true
73+
builtins.bool 0 |> equal false
74+
builtins.bool "" |> equal false
75+
builtins.bool "x" |> equal true
76+
77+
[<Fact>]
78+
let ``test dict empty works`` () =
79+
let d = builtins.dict ()
80+
builtins.len d |> equal 0
81+
82+
[<Fact>]
83+
let ``test dict from pairs works`` () =
84+
let d = builtins.dict [ "a", 1; "b", 2; "c", 3 ]
85+
builtins.len d |> equal 3
86+
d.["a"] |> equal 1
87+
d.["b"] |> equal 2
88+
d.["c"] |> equal 3
89+
90+
[<Fact>]
91+
let ``test list works`` () =
92+
let xs = builtins.list (seq { 1..3 })
93+
builtins.len xs |> equal 3
94+
xs.[0] |> equal 1
95+
xs.[2] |> equal 3
96+
97+
[<Fact>]
98+
let ``test pyInstanceof with type references`` () =
99+
// Use emitPyExpr to construct genuinely Python-native values, since F#'s
100+
// `int`/`float`/`bool` compile to Fable wrapper classes, not Python primitives.
101+
let pyIntVal: obj = emitPyExpr () "42"
102+
let pyFloatVal: obj = emitPyExpr () "3.14"
103+
let pyBoolVal: obj = emitPyExpr () "True"
104+
let pyStrVal: obj = emitPyExpr () "'hello'"
105+
106+
pyInstanceof pyIntVal pyInt |> equal true
107+
pyInstanceof pyFloatVal pyFloat |> equal true
108+
pyInstanceof pyBoolVal pyBool |> equal true
109+
pyInstanceof pyStrVal pyStr |> equal true
110+
pyInstanceof (builtins.dict ()) pyDict |> equal true
111+
pyInstanceof (builtins.list (seq { 1..3 })) pyList |> equal true
112+
113+
// Cross-checks
114+
pyInstanceof pyStrVal pyInt |> equal false
115+
pyInstanceof (builtins.dict ()) pyList |> equal false
116+
117+
[<Fact>]
118+
let ``test pyNone is None`` () =
119+
// bool(None) is False
120+
builtins.bool pyNone |> equal false
121+
// None has type NoneType, so isinstance(None, type(None)) holds
122+
builtins.isinstance (pyNone, builtins.``type`` pyNone) |> equal true

0 commit comments

Comments
 (0)