diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..f2a375a --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,3 @@ +{ + "MD024": false +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c14f63..c1f1bf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,10 @@ ## [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) - ### Features * 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)) - ### Bug Fixes * correct manifest to last released version ([bb9cb88](https://github.com/fable-compiler/Fable.Python/commit/bb9cb881c00b7ea6809ad3a87a35a7ebc0ff9008)) @@ -23,7 +21,6 @@ ## [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) - ### Features * Add exception types ([#173](https://github.com/fable-compiler/Fable.Python/issues/173)) ([72f09b2](https://github.com/fable-compiler/Fable.Python/commit/72f09b2ff6e208e3ab057430a355814611abd46c)) @@ -33,7 +30,6 @@ * Fable v5 ([#147](https://github.com/fable-compiler/Fable.Python/issues/147)) ([abf8e6a](https://github.com/fable-compiler/Fable.Python/commit/abf8e6a1f7bbc2eae152431d04d6b8b5675f7795)) * FastAPI async handlers ([#174](https://github.com/fable-compiler/Fable.Python/issues/174)) ([26cec1f](https://github.com/fable-compiler/Fable.Python/commit/26cec1f239f9244a7c1da1d00bbf1a479596bb3c)) - ### Bug Fixes * 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)) @@ -43,35 +39,30 @@ * use plain int/string types for open ([b202a25](https://github.com/fable-compiler/Fable.Python/commit/b202a25bd7f48538fadc50125294a9252714d364)) * use string types for open ([f211f8b](https://github.com/fable-compiler/Fable.Python/commit/f211f8bb9445dd6930926fe48aab6a69720dd30f)) - ### Miscellaneous Chores * sync with Fable 5.0.0-alpha.21 ([fd2685c](https://github.com/fable-compiler/Fable.Python/commit/fd2685c2f992c2d8058ac1bc1261d647271359df)) ## [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) - ### Features * 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)) ## [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) - ### Bug Fixes * relax FSharp.Core dependency to >= 5.0.0 ([#163](https://github.com/fable-compiler/Fable.Python/issues/163)) ([10eb65b](https://github.com/fable-compiler/Fable.Python/commit/10eb65b22a157078e1b66bd8fb202b0cd2acbedc)) ## [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) - ### Features * add write ([334e800](https://github.com/fable-compiler/Fable.Python/commit/334e80089c081bda25633f83dae037fc6c8fe6f5)) * 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)) * Fable v5 ([#147](https://github.com/fable-compiler/Fable.Python/issues/147)) ([abf8e6a](https://github.com/fable-compiler/Fable.Python/commit/abf8e6a1f7bbc2eae152431d04d6b8b5675f7795)) - ### Bug Fixes * handle return type correctly ([a634168](https://github.com/fable-compiler/Fable.Python/commit/a6341684ac8bb3f1b244f448c22e1fe6f208fbc0)) diff --git a/release-please-config.json b/release-please-config.json index aa93e33..255646e 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -3,7 +3,7 @@ "packages": { ".": { "release-type": "simple", - "release-as": "5.0.0-alpha.21.2", + "release-as": "5.0.0-alpha.21.3", "include-component-in-tag": false, "include-v-in-tag": true, "changelog-path": "CHANGELOG.md" diff --git a/src/Fable.Python.fsproj b/src/Fable.Python.fsproj index b524b01..42effae 100644 --- a/src/Fable.Python.fsproj +++ b/src/Fable.Python.fsproj @@ -36,6 +36,8 @@ + + diff --git a/src/fable/Types.fs b/src/fable/Types.fs new file mode 100644 index 0000000..267ae23 --- /dev/null +++ b/src/fable/Types.fs @@ -0,0 +1,55 @@ +/// Utilities for detecting and working with Fable types at runtime in Python. +/// These helpers are useful when you need to distinguish between native Python types +/// and Fable-compiled F# types (e.g., Int32, Float64, FSharpArray). +module Fable.Python.Fable.Types + +open Fable.Core + +/// Get the Python type name of an object +[] +let typeName (o: obj) : string = nativeOnly + +/// Check if an object is a Fable integral type (Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64) +let isIntegralType (o: obj) : bool = + match typeName o with + | "Int8" + | "Int16" + | "Int32" + | "Int64" + | "UInt8" + | "UInt16" + | "UInt32" + | "UInt64" -> true + | _ -> false + +/// Check if an object is a Fable numeric type (integral types + Float32, Float64) +let isNumericType (o: obj) : bool = + match typeName o with + | "Int8" + | "Int16" + | "Int32" + | "Int64" + | "UInt8" + | "UInt16" + | "UInt32" + | "UInt64" + | "Float32" + | "Float64" -> true + | _ -> false + +/// Check if an object is a Fable array type (FSharpArray, GenericArray, or typed arrays) +let isArrayType (o: obj) : bool = + match typeName o with + | "FSharpArray" + | "GenericArray" + | "Int8Array" + | "Int16Array" + | "Int32Array" + | "Int64Array" + | "UInt8Array" + | "UInt16Array" + | "UInt32Array" + | "UInt64Array" + | "Float32Array" + | "Float64Array" -> true + | _ -> false diff --git a/src/fastapi/FastAPI.fs b/src/fastapi/FastAPI.fs index f3060f5..c49e457 100644 --- a/src/fastapi/FastAPI.fs +++ b/src/fastapi/FastAPI.fs @@ -136,33 +136,27 @@ type RouterWebSocketAttribute(path: string) = /// JSON response class [] -type JSONResponse(content: obj, ?status_code: int) = - class end +type JSONResponse(content: obj, ?status_code: int) = class end /// HTML response class [] -type HTMLResponse(content: string, ?status_code: int) = - class end +type HTMLResponse(content: string, ?status_code: int) = class end /// Plain text response class [] -type PlainTextResponse(content: string, ?status_code: int) = - class end +type PlainTextResponse(content: string, ?status_code: int) = class end /// Redirect response class [] -type RedirectResponse(url: string, ?status_code: int) = - class end +type RedirectResponse(url: string, ?status_code: int) = class end /// Streaming response class [] -type StreamingResponse(content: obj, ?media_type: string) = - class end +type StreamingResponse(content: obj, ?media_type: string) = class end /// File response class [] -type FileResponse(path: string, ?filename: string, ?media_type: string) = - class end +type FileResponse(path: string, ?filename: string, ?media_type: string) = class end // ============================================================================ // Request and WebSocket @@ -256,8 +250,7 @@ type WebSocket() = /// HTTP exception for returning error responses [] -type HTTPException(status_code: int, ?detail: string) = - class end +type HTTPException(status_code: int, ?detail: string) = class end // ============================================================================ // Dependency Injection @@ -422,7 +415,7 @@ type UploadFile() = /// File parameter marker [] [] -let File() : UploadFile = nativeOnly +let File () : UploadFile = nativeOnly /// Form parameter marker [] @@ -452,7 +445,10 @@ type APIRouter(?prefix: string, ?tags: ResizeArray) = /// Include another router with prefix and tags [] - member _.include_router_with_prefix_and_tags(_router: APIRouter, _prefix: string, _tags: ResizeArray) : unit = nativeOnly + member _.include_router_with_prefix_and_tags + (_router: APIRouter, _prefix: string, _tags: ResizeArray) + : unit = + nativeOnly // ============================================================================ // FastAPI Application @@ -471,7 +467,10 @@ type FastAPI(?title: string, ?description: string, ?version: string) = /// Include a router with prefix and tags [] - member _.include_router_with_prefix_and_tags(_router: APIRouter, _prefix: string, _tags: ResizeArray) : unit = nativeOnly + member _.include_router_with_prefix_and_tags + (_router: APIRouter, _prefix: string, _tags: ResizeArray) + : unit = + nativeOnly /// Add middleware [] @@ -571,8 +570,7 @@ type OAuth2PasswordRequestForm() = /// HTTP Basic authentication [] -type HTTPBasic() = - class end +type HTTPBasic() = class end /// HTTP Basic credentials [] @@ -587,8 +585,7 @@ type HTTPBasicCredentials() = /// HTTP Bearer authentication [] -type HTTPBearer() = - class end +type HTTPBearer() = class end /// HTTP Bearer credentials (token in Authorization header) [] @@ -603,18 +600,15 @@ type HTTPAuthorizationCredentials() = /// API Key in header [] -type APIKeyHeader(name: string) = - class end +type APIKeyHeader(name: string) = class end /// API Key in query parameter [] -type APIKeyQuery(name: string) = - class end +type APIKeyQuery(name: string) = class end /// API Key in cookie [] -type APIKeyCookie(name: string) = - class end +type APIKeyCookie(name: string) = class end // ============================================================================ // CORS Middleware @@ -622,8 +616,7 @@ type APIKeyCookie(name: string) = /// CORS middleware for handling Cross-Origin Resource Sharing [] -type CORSMiddleware = - class end +type CORSMiddleware = class end /// CORS middleware configuration helper module CORSMiddleware = @@ -634,7 +627,15 @@ module CORSMiddleware = /// Add CORS middleware to app with all origins allowed [] - let addToApp (_app: FastAPI) (_middleware: obj) (_origins: string array) (_credentials: bool) (_methods: string array) (_headers: string array) : unit = nativeOnly + let addToApp + (_app: FastAPI) + (_middleware: obj) + (_origins: string array) + (_credentials: bool) + (_methods: string array) + (_headers: string array) + : unit = + nativeOnly // ============================================================================ // Encoders diff --git a/src/flask/Flask.fs b/src/flask/Flask.fs index f07348f..d1ee175 100644 --- a/src/flask/Flask.fs +++ b/src/flask/Flask.fs @@ -259,8 +259,7 @@ let make_response_with_status (_body: obj) (_status: int) : Response = nativeOnl /// Flask Blueprint for modular applications [] -type Blueprint(name: string, import_name: string, ?url_prefix: string) = - class end +type Blueprint(name: string, import_name: string, ?url_prefix: string) = class end // ============================================================================ // Flask Application diff --git a/src/pydantic/Pydantic.fs b/src/pydantic/Pydantic.fs index 8b95cda..0f0be90 100644 --- a/src/pydantic/Pydantic.fs +++ b/src/pydantic/Pydantic.fs @@ -458,4 +458,3 @@ type Base64Str = string /// JSON type - validates JSON string and parses it [] type JsonValue = obj - diff --git a/src/stdlib/Json.fs b/src/stdlib/Json.fs index d87043d..4172e7d 100644 --- a/src/stdlib/Json.fs +++ b/src/stdlib/Json.fs @@ -15,24 +15,28 @@ type IExports = /// Serialize obj to a JSON formatted string with indentation /// See https://docs.python.org/3/library/json.html#json.dumps abstract dumps: obj: obj * indent: int -> string + /// Serialize obj to a JSON formatted string with a custom default function /// See https://docs.python.org/3/library/json.html#json.dumps [] abstract dumps: obj: obj * ``default``: (obj -> obj) -> string + /// Serialize obj to a JSON formatted string with indentation and custom default /// See https://docs.python.org/3/library/json.html#json.dumps [] abstract dumps: obj: obj * indent: int * ``default``: (obj -> obj) -> string + /// Serialize obj to a JSON formatted string with separators, ensure_ascii, and custom default /// See https://docs.python.org/3/library/json.html#json.dumps [] - abstract dumps: - obj: obj * separators: string array * ensure_ascii: bool * ``default``: (obj -> obj) -> string + abstract dumps: obj: obj * separators: string array * ensure_ascii: bool * ``default``: (obj -> obj) -> string + /// Serialize obj to a JSON formatted string with indent, separators, ensure_ascii, and custom default /// See https://docs.python.org/3/library/json.html#json.dumps [] abstract dumps: obj: obj * indent: int * separators: string array * ensure_ascii: bool * ``default``: (obj -> obj) -> string + /// Deserialize a JSON document from a string to a Python object /// See https://docs.python.org/3/library/json.html#json.loads abstract loads: s: string -> obj @@ -42,14 +46,17 @@ type IExports = /// Serialize obj as a JSON formatted stream with indentation /// See https://docs.python.org/3/library/json.html#json.dump abstract dump: obj: obj * fp: TextIOWrapper * indent: int -> unit + /// Serialize obj as a JSON formatted stream with a custom default function /// See https://docs.python.org/3/library/json.html#json.dump [] abstract dump: obj: obj * fp: TextIOWrapper * ``default``: (obj -> obj) -> unit + /// Serialize obj as a JSON formatted stream with indentation and custom default /// See https://docs.python.org/3/library/json.html#json.dump [] abstract dump: obj: obj * fp: TextIOWrapper * indent: int * ``default``: (obj -> obj) -> unit + /// Deserialize a JSON document from a file-like object to a Python object /// See https://docs.python.org/3/library/json.html#json.load abstract load: fp: TextIOWrapper -> obj @@ -127,7 +134,13 @@ let fableDefault (o: obj) : obj = if hasattr o "tag" && hasattr o "fields" then let cases = getCases o let tag: int = getattr o "tag" :?> int - let caseName = if tag < cases.Length then cases.[tag] else "Case" + string tag + + let caseName = + if tag < cases.Length then + cases.[tag] + else + "Case" + string tag + unionToList o caseName elif hasattr o "__slots__" then slotsToDict o @@ -152,7 +165,13 @@ type Json = /// Serialize obj to JSON with indentation, custom separators, and ensure_ascii, automatically handling Fable types static member inline dumps(obj: obj, indent: int, separators: string array, ensureAscii: bool) : string = - json.dumps (obj, indent = indent, separators = separators, ensure_ascii = ensureAscii, ``default`` = fableDefault) + json.dumps ( + obj, + indent = indent, + separators = separators, + ensure_ascii = ensureAscii, + ``default`` = fableDefault + ) /// Serialize obj as JSON stream to file, automatically handling Fable types static member inline dump(obj: obj, fp: TextIOWrapper) : unit = @@ -163,9 +182,7 @@ type Json = json.dump (obj, fp, indent, ``default`` = fableDefault) /// Deserialize a JSON document from a string to a Python object - static member inline loads(s: string) : obj = - json.loads s + static member inline loads(s: string) : obj = json.loads s /// Deserialize a JSON document from a file-like object to a Python object - static member inline load(fp: TextIOWrapper) : obj = - json.load fp + static member inline load(fp: TextIOWrapper) : obj = json.load fp diff --git a/src/stdlib/Logging.fs b/src/stdlib/Logging.fs index 5dde395..8523e68 100644 --- a/src/stdlib/Logging.fs +++ b/src/stdlib/Logging.fs @@ -70,27 +70,35 @@ type Logger(name: string, ?level: int) = member _.critical(msg: string) : unit = nativeOnly /// Log a message with severity ERROR and exception info member _.``exception``(msg: string) : unit = nativeOnly + /// Log a message with the specified level [] member _.log(level: int, msg: string) : unit = nativeOnly + /// Set the logging level [] member _.setLevel(level: int) : unit = nativeOnly + /// Get the effective logging level [] member _.getEffectiveLevel() : int = nativeOnly + /// Check if the logger is enabled for the specified level [] member _.isEnabledFor(level: int) : bool = nativeOnly + /// Add the specified handler to this logger [] member _.addHandler(handler: Handler) : unit = nativeOnly + /// Remove the specified handler from this logger [] member _.removeHandler(handler: Handler) : unit = nativeOnly + /// Check if this logger has any handlers configured [] member _.hasHandlers() : bool = nativeOnly + /// The name of the logger member _.name: string = nativeOnly @@ -114,6 +122,7 @@ type IExports = /// Return a logger with the specified name [] abstract getLogger: name: string -> Logger + /// Return the root logger [] abstract getLogger: unit -> Logger diff --git a/src/stdlib/Random.fs b/src/stdlib/Random.fs index ae92219..9eb989a 100644 --- a/src/stdlib/Random.fs +++ b/src/stdlib/Random.fs @@ -12,6 +12,7 @@ type IExports = /// See https://docs.python.org/3/library/random.html#random.seed [] abstract seed: a: int -> unit + /// Initialize the random number generator /// See https://docs.python.org/3/library/random.html#random.seed abstract seed: a: nativeint -> unit @@ -58,10 +59,12 @@ type IExports = /// See https://docs.python.org/3/library/random.html#random.choice [] abstract choice: seq: 'T[] -> 'T + /// Return a random element from the non-empty sequence /// See https://docs.python.org/3/library/random.html#random.choice [] abstract choice: seq: 'T list -> 'T + /// Return a random element from the non-empty sequence /// See https://docs.python.org/3/library/random.html#random.choice abstract choice: seq: ResizeArray<'T> -> 'T @@ -70,10 +73,12 @@ type IExports = /// See https://docs.python.org/3/library/random.html#random.sample [] abstract sample: population: 'T[] * k: int -> ResizeArray<'T> + /// Return a k length list of unique elements chosen from the population sequence /// See https://docs.python.org/3/library/random.html#random.sample [] abstract sample: population: 'T list * k: int -> ResizeArray<'T> + /// Return a k length list of unique elements chosen from the population sequence /// See https://docs.python.org/3/library/random.html#random.sample [] @@ -83,10 +88,12 @@ type IExports = /// See https://docs.python.org/3/library/random.html#random.choices [] abstract choices: population: 'T[] * k: int -> ResizeArray<'T> + /// Return a k sized list of elements chosen from the population with replacement /// See https://docs.python.org/3/library/random.html#random.choices [] abstract choices: population: 'T list * k: int -> ResizeArray<'T> + /// Return a k sized list of elements chosen from the population with replacement /// See https://docs.python.org/3/library/random.html#random.choices [] diff --git a/src/stdlib/Sys.fs b/src/stdlib/Sys.fs index 317e5b4..fe84be6 100644 --- a/src/stdlib/Sys.fs +++ b/src/stdlib/Sys.fs @@ -60,10 +60,12 @@ type IExports = /// Exits with code 0, indicating success /// See https://docs.python.org/3/library/sys.html#sys.exit abstract exit: unit -> 'a + /// Exits with provided status /// See https://docs.python.org/3/library/sys.html#sys.exit [] abstract exit: status: int -> 'a + /// Exits with provided status /// See https://docs.python.org/3/library/sys.html#sys.exit abstract exit: status: nativeint -> 'a diff --git a/src/stdlib/asyncio/Futures.fs b/src/stdlib/asyncio/Futures.fs index dcc1700..54dc034 100644 --- a/src/stdlib/asyncio/Futures.fs +++ b/src/stdlib/asyncio/Futures.fs @@ -3,9 +3,7 @@ namespace Fable.Python.AsyncIO open Fable.Core -type Awaitable<'T> = - interface - end +type Awaitable<'T> = interface end type Future<'T> = inherit Awaitable<'T> diff --git a/test/Fable.Python.Test.fsproj b/test/Fable.Python.Test.fsproj index 50dc0bd..6292242 100644 --- a/test/Fable.Python.Test.fsproj +++ b/test/Fable.Python.Test.fsproj @@ -25,6 +25,7 @@ + diff --git a/test/TestTypes.fs b/test/TestTypes.fs new file mode 100644 index 0000000..66743c8 --- /dev/null +++ b/test/TestTypes.fs @@ -0,0 +1,132 @@ +module Fable.Python.Tests.Types + +open Util.Testing +open Fable.Python.Fable.Types + +// Test typeName function + +[] +let ``test typeName returns Int32 for int`` () = + let value = 42 + typeName value |> equal "Int32" + +[] +let ``test typeName returns Int64 for int64`` () = + let value = 42L + typeName value |> equal "Int64" + +[] +let ``test typeName returns Float64 for float`` () = + let value = 3.14 + typeName value |> equal "Float64" + +[] +let ``test typeName returns str for string`` () = + let value = "hello" + typeName value |> equal "str" + +// Test isIntegralType function + +[] +let ``test isIntegralType returns true for int`` () = + let value = 42 + isIntegralType value |> equal true + +[] +let ``test isIntegralType returns true for int64`` () = + let value = 42L + isIntegralType value |> equal true + +[] +let ``test isIntegralType returns true for int16`` () = + let value = 42s + isIntegralType value |> equal true + +[] +let ``test isIntegralType returns true for int8`` () = + let value = 42y + isIntegralType value |> equal true + +[] +let ``test isIntegralType returns true for uint32`` () = + let value = 42u + isIntegralType value |> equal true + +[] +let ``test isIntegralType returns true for uint64`` () = + let value = 42UL + isIntegralType value |> equal true + +[] +let ``test isIntegralType returns false for float`` () = + let value = 3.14 + isIntegralType value |> equal false + +[] +let ``test isIntegralType returns false for string`` () = + let value = "hello" + isIntegralType value |> equal false + +// Test isNumericType function + +[] +let ``test isNumericType returns true for int`` () = + let value = 42 + isNumericType value |> equal true + +[] +let ``test isNumericType returns true for int64`` () = + let value = 42L + isNumericType value |> equal true + +[] +let ``test isNumericType returns true for float`` () = + let value = 3.14 + isNumericType value |> equal true + +[] +let ``test isNumericType returns true for float32`` () = + let value = 3.14f + isNumericType value |> equal true + +[] +let ``test isNumericType returns false for string`` () = + let value = "hello" + isNumericType value |> equal false + +[] +let ``test isNumericType returns false for bool`` () = + let value = true + isNumericType value |> equal false + +// Test isArrayType function + +[] +let ``test isArrayType returns true for int array`` () = + let value = [| 1; 2; 3 |] + isArrayType value |> equal true + +[] +let ``test isArrayType returns true for string array`` () = + let value = [| "a"; "b"; "c" |] + isArrayType value |> equal true + +[] +let ``test isArrayType returns true for float array`` () = + let value = [| 1.0; 2.0; 3.0 |] + isArrayType value |> equal true + +[] +let ``test isArrayType returns false for list`` () = + let value = [ 1; 2; 3 ] + isArrayType value |> equal false + +[] +let ``test isArrayType returns false for string`` () = + let value = "hello" + isArrayType value |> equal false + +[] +let ``test isArrayType returns false for int`` () = + let value = 42 + isArrayType value |> equal false