Skip to content

Commit 8f29fdc

Browse files
dbrattliclaude
andcommitted
feat: add Fable.Types module for runtime type detection
Add utilities for detecting Fable types at runtime in Python: - typeName: get Python type name of an object - isIntegralType: check for Int8-64, UInt8-64 - isNumericType: check for integral types + Float32/64 - isArrayType: check for FSharpArray, GenericArray, typed arrays Also includes code formatting improvements and test coverage. Release-As: 5.0.0-alpha.21.3 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 3eb77ef commit 8f29fdc

File tree

14 files changed

+269
-53
lines changed

14 files changed

+269
-53
lines changed

.markdownlint.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"MD024": false
3+
}

CHANGELOG.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22

33
## [5.0.0-alpha.21.1](https://github.com/fable-compiler/Fable.Python/compare/v5.0.0-alpha.21.0...v5.0.0-alpha.21.1) (2025-12-18)
44

5-
65
### Features
76

87
* Add Json static class with Fable-aware serialization ([#175](https://github.com/fable-compiler/Fable.Python/issues/175)) ([1eb5005](https://github.com/fable-compiler/Fable.Python/commit/1eb500523c6d6f43247bc3510a6045ec8d9d9d4e))
98

10-
119
### Bug Fixes
1210

1311
* correct manifest to last released version ([bb9cb88](https://github.com/fable-compiler/Fable.Python/commit/bb9cb881c00b7ea6809ad3a87a35a7ebc0ff9008))
@@ -16,7 +14,6 @@
1614

1715
## [5.0.0-alpha.21.0](https://github.com/fable-compiler/Fable.Python/compare/v5.0.0-alpha.21...v5.0.0-alpha.21.0) (2025-12-16)
1816

19-
2017
### Features
2118

2219
* Add exception types ([#173](https://github.com/fable-compiler/Fable.Python/issues/173)) ([72f09b2](https://github.com/fable-compiler/Fable.Python/commit/72f09b2ff6e208e3ab057430a355814611abd46c))
@@ -26,7 +23,6 @@
2623
* Fable v5 ([#147](https://github.com/fable-compiler/Fable.Python/issues/147)) ([abf8e6a](https://github.com/fable-compiler/Fable.Python/commit/abf8e6a1f7bbc2eae152431d04d6b8b5675f7795))
2724
* FastAPI async handlers ([#174](https://github.com/fable-compiler/Fable.Python/issues/174)) ([26cec1f](https://github.com/fable-compiler/Fable.Python/commit/26cec1f239f9244a7c1da1d00bbf1a479596bb3c))
2825

29-
3026
### Bug Fixes
3127

3228
* add missing models.py for pydantic example and update README ([#168](https://github.com/fable-compiler/Fable.Python/issues/168)) ([b76ce98](https://github.com/fable-compiler/Fable.Python/commit/b76ce989e0598d0007a8a1be9addfea97936eae7))
@@ -36,35 +32,30 @@
3632
* use plain int/string types for open ([b202a25](https://github.com/fable-compiler/Fable.Python/commit/b202a25bd7f48538fadc50125294a9252714d364))
3733
* use string types for open ([f211f8b](https://github.com/fable-compiler/Fable.Python/commit/f211f8bb9445dd6930926fe48aab6a69720dd30f))
3834

39-
4035
### Miscellaneous Chores
4136

4237
* sync with Fable 5.0.0-alpha.21 ([fd2685c](https://github.com/fable-compiler/Fable.Python/commit/fd2685c2f992c2d8058ac1bc1261d647271359df))
4338

4439
## [5.0.0-alpha.20.2](https://github.com/fable-compiler/Fable.Python/compare/v5.0.0-alpha.20.1...v5.0.0-alpha.20.2) (2025-12-09)
4540

46-
4741
### Features
4842

4943
* add Python stdlib bindings for logging, random, and expand string module ([#166](https://github.com/fable-compiler/Fable.Python/issues/166)) ([709d6c2](https://github.com/fable-compiler/Fable.Python/commit/709d6c2b29199965926ff639727fdcc1bb2e1fb8))
5044

5145
## [5.0.0-alpha.20.1](https://github.com/fable-compiler/Fable.Python/compare/v5.0.0-alpha.20...v5.0.0-alpha.20.1) (2025-12-08)
5246

53-
5447
### Bug Fixes
5548

5649
* relax FSharp.Core dependency to &gt;= 5.0.0 ([#163](https://github.com/fable-compiler/Fable.Python/issues/163)) ([10eb65b](https://github.com/fable-compiler/Fable.Python/commit/10eb65b22a157078e1b66bd8fb202b0cd2acbedc))
5750

5851
## [5.0.0-alpha.20](https://github.com/fable-compiler/Fable.Python/compare/v5.0.0-alpha.20...v5.0.0-alpha.20) (2025-12-08)
5952

60-
6153
### Features
6254

6355
* add write ([334e800](https://github.com/fable-compiler/Fable.Python/commit/334e80089c081bda25633f83dae037fc6c8fe6f5))
6456
* Added bindings for Pydantic and FastAPI + examples ([#151](https://github.com/fable-compiler/Fable.Python/issues/151)) ([826629e](https://github.com/fable-compiler/Fable.Python/commit/826629e465fca15d444a4ca37b851b8aab488f9a))
6557
* Fable v5 ([#147](https://github.com/fable-compiler/Fable.Python/issues/147)) ([abf8e6a](https://github.com/fable-compiler/Fable.Python/commit/abf8e6a1f7bbc2eae152431d04d6b8b5675f7795))
6658

67-
6859
### Bug Fixes
6960

7061
* handle return type correctly ([a634168](https://github.com/fable-compiler/Fable.Python/commit/a6341684ac8bb3f1b244f448c22e1fe6f208fbc0))

src/Fable.Python.fsproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636

3737
<Compile Include="pydantic/Pydantic.fs" />
3838
<Compile Include="fastapi/FastAPI.fs" />
39+
40+
<Compile Include="fable/Types.fs" />
3941
</ItemGroup>
4042
<ItemGroup>
4143
<Content Include="pyproject.toml; *.fsproj; **\*.fs; **\*.fsi" PackagePath="fable\" />

src/fable/Types.fs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/// Utilities for detecting and working with Fable types at runtime in Python.
2+
/// These helpers are useful when you need to distinguish between native Python types
3+
/// and Fable-compiled F# types (e.g., Int32, Float64, FSharpArray).
4+
module Fable.Python.Fable.Types
5+
6+
open Fable.Core
7+
8+
/// Get the Python type name of an object
9+
[<Emit("type($0).__name__")>]
10+
let typeName (o: obj) : string = nativeOnly
11+
12+
/// Check if an object is a Fable integral type (Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64)
13+
let isIntegralType (o: obj) : bool =
14+
match typeName o with
15+
| "Int8"
16+
| "Int16"
17+
| "Int32"
18+
| "Int64"
19+
| "UInt8"
20+
| "UInt16"
21+
| "UInt32"
22+
| "UInt64" -> true
23+
| _ -> false
24+
25+
/// Check if an object is a Fable numeric type (integral types + Float32, Float64)
26+
let isNumericType (o: obj) : bool =
27+
match typeName o with
28+
| "Int8"
29+
| "Int16"
30+
| "Int32"
31+
| "Int64"
32+
| "UInt8"
33+
| "UInt16"
34+
| "UInt32"
35+
| "UInt64"
36+
| "Float32"
37+
| "Float64" -> true
38+
| _ -> false
39+
40+
/// Check if an object is a Fable array type (FSharpArray, GenericArray, or typed arrays)
41+
let isArrayType (o: obj) : bool =
42+
match typeName o with
43+
| "FSharpArray"
44+
| "GenericArray"
45+
| "Int8Array"
46+
| "Int16Array"
47+
| "Int32Array"
48+
| "Int64Array"
49+
| "UInt8Array"
50+
| "UInt16Array"
51+
| "UInt32Array"
52+
| "UInt64Array"
53+
| "Float32Array"
54+
| "Float64Array" -> true
55+
| _ -> false

src/fastapi/FastAPI.fs

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -136,33 +136,27 @@ type RouterWebSocketAttribute(path: string) =
136136

137137
/// JSON response class
138138
[<Import("JSONResponse", "fastapi.responses")>]
139-
type JSONResponse(content: obj, ?status_code: int) =
140-
class end
139+
type JSONResponse(content: obj, ?status_code: int) = class end
141140

142141
/// HTML response class
143142
[<Import("HTMLResponse", "fastapi.responses")>]
144-
type HTMLResponse(content: string, ?status_code: int) =
145-
class end
143+
type HTMLResponse(content: string, ?status_code: int) = class end
146144

147145
/// Plain text response class
148146
[<Import("PlainTextResponse", "fastapi.responses")>]
149-
type PlainTextResponse(content: string, ?status_code: int) =
150-
class end
147+
type PlainTextResponse(content: string, ?status_code: int) = class end
151148

152149
/// Redirect response class
153150
[<Import("RedirectResponse", "fastapi.responses")>]
154-
type RedirectResponse(url: string, ?status_code: int) =
155-
class end
151+
type RedirectResponse(url: string, ?status_code: int) = class end
156152

157153
/// Streaming response class
158154
[<Import("StreamingResponse", "fastapi.responses")>]
159-
type StreamingResponse(content: obj, ?media_type: string) =
160-
class end
155+
type StreamingResponse(content: obj, ?media_type: string) = class end
161156

162157
/// File response class
163158
[<Import("FileResponse", "fastapi.responses")>]
164-
type FileResponse(path: string, ?filename: string, ?media_type: string) =
165-
class end
159+
type FileResponse(path: string, ?filename: string, ?media_type: string) = class end
166160

167161
// ============================================================================
168162
// Request and WebSocket
@@ -256,8 +250,7 @@ type WebSocket() =
256250

257251
/// HTTP exception for returning error responses
258252
[<Import("HTTPException", "fastapi")>]
259-
type HTTPException(status_code: int, ?detail: string) =
260-
class end
253+
type HTTPException(status_code: int, ?detail: string) = class end
261254

262255
// ============================================================================
263256
// Dependency Injection
@@ -422,7 +415,7 @@ type UploadFile() =
422415
/// File parameter marker
423416
[<Import("File", "fastapi")>]
424417
[<Emit("$0()")>]
425-
let File() : UploadFile = nativeOnly
418+
let File () : UploadFile = nativeOnly
426419

427420
/// Form parameter marker
428421
[<Import("Form", "fastapi")>]
@@ -452,7 +445,10 @@ type APIRouter(?prefix: string, ?tags: ResizeArray<string>) =
452445

453446
/// Include another router with prefix and tags
454447
[<Emit("$0.include_router($1, prefix=$2, tags=$3)")>]
455-
member _.include_router_with_prefix_and_tags(_router: APIRouter, _prefix: string, _tags: ResizeArray<string>) : unit = nativeOnly
448+
member _.include_router_with_prefix_and_tags
449+
(_router: APIRouter, _prefix: string, _tags: ResizeArray<string>)
450+
: unit =
451+
nativeOnly
456452

457453
// ============================================================================
458454
// FastAPI Application
@@ -471,7 +467,10 @@ type FastAPI(?title: string, ?description: string, ?version: string) =
471467

472468
/// Include a router with prefix and tags
473469
[<Emit("$0.include_router($1, prefix=$2, tags=$3)")>]
474-
member _.include_router_with_prefix_and_tags(_router: APIRouter, _prefix: string, _tags: ResizeArray<string>) : unit = nativeOnly
470+
member _.include_router_with_prefix_and_tags
471+
(_router: APIRouter, _prefix: string, _tags: ResizeArray<string>)
472+
: unit =
473+
nativeOnly
475474

476475
/// Add middleware
477476
[<Emit("$0.add_middleware($1)")>]
@@ -571,8 +570,7 @@ type OAuth2PasswordRequestForm() =
571570

572571
/// HTTP Basic authentication
573572
[<Import("HTTPBasic", "fastapi.security")>]
574-
type HTTPBasic() =
575-
class end
573+
type HTTPBasic() = class end
576574

577575
/// HTTP Basic credentials
578576
[<Import("HTTPBasicCredentials", "fastapi.security")>]
@@ -587,8 +585,7 @@ type HTTPBasicCredentials() =
587585

588586
/// HTTP Bearer authentication
589587
[<Import("HTTPBearer", "fastapi.security")>]
590-
type HTTPBearer() =
591-
class end
588+
type HTTPBearer() = class end
592589

593590
/// HTTP Bearer credentials (token in Authorization header)
594591
[<Import("HTTPAuthorizationCredentials", "fastapi.security")>]
@@ -603,27 +600,23 @@ type HTTPAuthorizationCredentials() =
603600

604601
/// API Key in header
605602
[<Import("APIKeyHeader", "fastapi.security")>]
606-
type APIKeyHeader(name: string) =
607-
class end
603+
type APIKeyHeader(name: string) = class end
608604

609605
/// API Key in query parameter
610606
[<Import("APIKeyQuery", "fastapi.security")>]
611-
type APIKeyQuery(name: string) =
612-
class end
607+
type APIKeyQuery(name: string) = class end
613608

614609
/// API Key in cookie
615610
[<Import("APIKeyCookie", "fastapi.security")>]
616-
type APIKeyCookie(name: string) =
617-
class end
611+
type APIKeyCookie(name: string) = class end
618612

619613
// ============================================================================
620614
// CORS Middleware
621615
// ============================================================================
622616

623617
/// CORS middleware for handling Cross-Origin Resource Sharing
624618
[<Import("CORSMiddleware", "fastapi.middleware.cors")>]
625-
type CORSMiddleware =
626-
class end
619+
type CORSMiddleware = class end
627620

628621
/// CORS middleware configuration helper
629622
module CORSMiddleware =
@@ -634,7 +627,15 @@ module CORSMiddleware =
634627

635628
/// Add CORS middleware to app with all origins allowed
636629
[<Emit("$0.add_middleware($1, allow_origins=$2, allow_credentials=$3, allow_methods=$4, allow_headers=$5)")>]
637-
let addToApp (_app: FastAPI) (_middleware: obj) (_origins: string array) (_credentials: bool) (_methods: string array) (_headers: string array) : unit = nativeOnly
630+
let addToApp
631+
(_app: FastAPI)
632+
(_middleware: obj)
633+
(_origins: string array)
634+
(_credentials: bool)
635+
(_methods: string array)
636+
(_headers: string array)
637+
: unit =
638+
nativeOnly
638639

639640
// ============================================================================
640641
// Encoders

src/flask/Flask.fs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,7 @@ let make_response_with_status (_body: obj) (_status: int) : Response = nativeOnl
259259

260260
/// Flask Blueprint for modular applications
261261
[<Import("Blueprint", "flask")>]
262-
type Blueprint(name: string, import_name: string, ?url_prefix: string) =
263-
class end
262+
type Blueprint(name: string, import_name: string, ?url_prefix: string) = class end
264263

265264
// ============================================================================
266265
// Flask Application

src/pydantic/Pydantic.fs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,4 +458,3 @@ type Base64Str = string
458458
/// JSON type - validates JSON string and parses it
459459
[<Import("Json", "pydantic")>]
460460
type JsonValue = obj
461-

src/stdlib/Json.fs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,28 @@ type IExports =
1515
/// Serialize obj to a JSON formatted string with indentation
1616
/// See https://docs.python.org/3/library/json.html#json.dumps
1717
abstract dumps: obj: obj * indent: int -> string
18+
1819
/// Serialize obj to a JSON formatted string with a custom default function
1920
/// See https://docs.python.org/3/library/json.html#json.dumps
2021
[<NamedParams(fromIndex = 1)>]
2122
abstract dumps: obj: obj * ``default``: (obj -> obj) -> string
23+
2224
/// Serialize obj to a JSON formatted string with indentation and custom default
2325
/// See https://docs.python.org/3/library/json.html#json.dumps
2426
[<NamedParams(fromIndex = 1)>]
2527
abstract dumps: obj: obj * indent: int * ``default``: (obj -> obj) -> string
28+
2629
/// Serialize obj to a JSON formatted string with separators, ensure_ascii, and custom default
2730
/// See https://docs.python.org/3/library/json.html#json.dumps
2831
[<NamedParams(fromIndex = 1)>]
29-
abstract dumps:
30-
obj: obj * separators: string array * ensure_ascii: bool * ``default``: (obj -> obj) -> string
32+
abstract dumps: obj: obj * separators: string array * ensure_ascii: bool * ``default``: (obj -> obj) -> string
33+
3134
/// Serialize obj to a JSON formatted string with indent, separators, ensure_ascii, and custom default
3235
/// See https://docs.python.org/3/library/json.html#json.dumps
3336
[<NamedParams(fromIndex = 1)>]
3437
abstract dumps:
3538
obj: obj * indent: int * separators: string array * ensure_ascii: bool * ``default``: (obj -> obj) -> string
39+
3640
/// Deserialize a JSON document from a string to a Python object
3741
/// See https://docs.python.org/3/library/json.html#json.loads
3842
abstract loads: s: string -> obj
@@ -42,14 +46,17 @@ type IExports =
4246
/// Serialize obj as a JSON formatted stream with indentation
4347
/// See https://docs.python.org/3/library/json.html#json.dump
4448
abstract dump: obj: obj * fp: TextIOWrapper * indent: int -> unit
49+
4550
/// Serialize obj as a JSON formatted stream with a custom default function
4651
/// See https://docs.python.org/3/library/json.html#json.dump
4752
[<NamedParams(fromIndex = 2)>]
4853
abstract dump: obj: obj * fp: TextIOWrapper * ``default``: (obj -> obj) -> unit
54+
4955
/// Serialize obj as a JSON formatted stream with indentation and custom default
5056
/// See https://docs.python.org/3/library/json.html#json.dump
5157
[<NamedParams(fromIndex = 2)>]
5258
abstract dump: obj: obj * fp: TextIOWrapper * indent: int * ``default``: (obj -> obj) -> unit
59+
5360
/// Deserialize a JSON document from a file-like object to a Python object
5461
/// See https://docs.python.org/3/library/json.html#json.load
5562
abstract load: fp: TextIOWrapper -> obj
@@ -127,7 +134,13 @@ let fableDefault (o: obj) : obj =
127134
if hasattr o "tag" && hasattr o "fields" then
128135
let cases = getCases o
129136
let tag: int = getattr o "tag" :?> int
130-
let caseName = if tag < cases.Length then cases.[tag] else "Case" + string tag
137+
138+
let caseName =
139+
if tag < cases.Length then
140+
cases.[tag]
141+
else
142+
"Case" + string tag
143+
131144
unionToList o caseName
132145
elif hasattr o "__slots__" then
133146
slotsToDict o
@@ -152,7 +165,13 @@ type Json =
152165

153166
/// Serialize obj to JSON with indentation, custom separators, and ensure_ascii, automatically handling Fable types
154167
static member inline dumps(obj: obj, indent: int, separators: string array, ensureAscii: bool) : string =
155-
json.dumps (obj, indent = indent, separators = separators, ensure_ascii = ensureAscii, ``default`` = fableDefault)
168+
json.dumps (
169+
obj,
170+
indent = indent,
171+
separators = separators,
172+
ensure_ascii = ensureAscii,
173+
``default`` = fableDefault
174+
)
156175

157176
/// Serialize obj as JSON stream to file, automatically handling Fable types
158177
static member inline dump(obj: obj, fp: TextIOWrapper) : unit =
@@ -163,9 +182,7 @@ type Json =
163182
json.dump (obj, fp, indent, ``default`` = fableDefault)
164183

165184
/// Deserialize a JSON document from a string to a Python object
166-
static member inline loads(s: string) : obj =
167-
json.loads s
185+
static member inline loads(s: string) : obj = json.loads s
168186

169187
/// Deserialize a JSON document from a file-like object to a Python object
170-
static member inline load(fp: TextIOWrapper) : obj =
171-
json.load fp
188+
static member inline load(fp: TextIOWrapper) : obj = json.load fp

0 commit comments

Comments
 (0)