diff --git a/src/Fable.Python.fsproj b/src/Fable.Python.fsproj
index b446621..b524b01 100644
--- a/src/Fable.Python.fsproj
+++ b/src/Fable.Python.fsproj
@@ -19,7 +19,9 @@
+
+
diff --git a/src/stdlib/Builtins.fs b/src/stdlib/Builtins.fs
index 6dcb665..4ec2c42 100644
--- a/src/stdlib/Builtins.fs
+++ b/src/stdlib/Builtins.fs
@@ -200,6 +200,10 @@ type IExports =
abstract int: obj -> int
/// Object to float
abstract float: obj -> float
+ /// Convert to bytes
+ abstract bytes: byte[] -> byte[]
+ /// Convert string to bytes with encoding
+ abstract bytes: string * encoding: string -> byte[]
/// Return the largest item in an iterable or the largest of two or more arguments.
abstract max: 'T * 'T -> 'T
diff --git a/src/stdlib/Logging.fs b/src/stdlib/Logging.fs
new file mode 100644
index 0000000..5dde395
--- /dev/null
+++ b/src/stdlib/Logging.fs
@@ -0,0 +1,148 @@
+/// Type bindings for Python logging module: https://docs.python.org/3/library/logging.html
+module Fable.Python.Logging
+
+open Fable.Core
+
+// fsharplint:disable MemberNames
+
+/// Logging levels
+[]
+module Level =
+ []
+ let CRITICAL = 50
+
+ []
+ let FATAL = 50
+
+ []
+ let ERROR = 40
+
+ []
+ let WARNING = 30
+
+ []
+ let WARN = 30
+
+ []
+ let INFO = 20
+
+ []
+ let DEBUG = 10
+
+ []
+ let NOTSET = 0
+
+/// Formatter for log records
+[]
+type Formatter(fmt: string, ?datefmt: string) =
+ member _.format(record: obj) : string = nativeOnly
+
+/// Handler base type
+[]
+type Handler() =
+ /// Set the logging level
+ member _.setLevel(level: int) : unit = nativeOnly
+ /// Set the formatter for this handler
+ member _.setFormatter(formatter: Formatter) : unit = nativeOnly
+
+/// StreamHandler - logs to a stream (stderr by default)
+[]
+type StreamHandler(?stream: obj) =
+ inherit Handler()
+
+/// FileHandler - logs to a file
+[]
+type FileHandler(filename: string, ?mode: string) =
+ inherit Handler()
+
+/// Logger instance
+[]
+type Logger(name: string, ?level: int) =
+ /// Log a message with severity DEBUG
+ member _.debug(msg: string) : unit = nativeOnly
+ /// Log a message with severity INFO
+ member _.info(msg: string) : unit = nativeOnly
+ /// Log a message with severity WARNING
+ member _.warning(msg: string) : unit = nativeOnly
+ /// Log a message with severity ERROR
+ member _.error(msg: string) : unit = nativeOnly
+ /// Log a message with severity CRITICAL
+ 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
+
+[]
+type IExports =
+ /// Log a message with severity DEBUG on the root logger
+ abstract debug: msg: string -> unit
+ /// Log a message with severity INFO on the root logger
+ abstract info: msg: string -> unit
+ /// Log a message with severity WARNING on the root logger
+ abstract warning: msg: string -> unit
+ /// Log a message with severity ERROR on the root logger
+ abstract error: msg: string -> unit
+ /// Log a message with severity CRITICAL on the root logger
+ abstract critical: msg: string -> unit
+ /// Log a message with severity ERROR and exception info on the root logger
+ abstract ``exception``: msg: string -> unit
+ /// Log a message with the specified level on the root logger
+ abstract log: level: int * msg: string -> unit
+
+ /// Return a logger with the specified name
+ []
+ abstract getLogger: name: string -> Logger
+ /// Return the root logger
+ []
+ abstract getLogger: unit -> Logger
+
+ /// Do basic configuration for the logging system
+ []
+ []
+ abstract basicConfig:
+ ?filename: string *
+ ?filemode: string *
+ ?format: string *
+ ?datefmt: string *
+ ?style: string *
+ ?level: int *
+ ?stream: obj *
+ ?force: bool ->
+ unit
+
+ /// Disable all logging calls of severity level and below
+ []
+ abstract disable: level: int -> unit
+
+ /// Logging level constants
+ abstract DEBUG: int
+ abstract INFO: int
+ abstract WARNING: int
+ abstract ERROR: int
+ abstract CRITICAL: int
+
+/// Logging facility for Python
+[]
+let logging: IExports = nativeOnly
diff --git a/src/stdlib/Random.fs b/src/stdlib/Random.fs
new file mode 100644
index 0000000..ae92219
--- /dev/null
+++ b/src/stdlib/Random.fs
@@ -0,0 +1,144 @@
+/// Type bindings for Python random module: https://docs.python.org/3/library/random.html
+module Fable.Python.Random
+
+open System.Collections.Generic
+open Fable.Core
+
+// fsharplint:disable MemberNames
+
+[]
+type IExports =
+ /// Initialize the random number generator
+ /// 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
+ /// Initialize the random number generator
+ /// See https://docs.python.org/3/library/random.html#random.seed
+ abstract seed: a: float -> unit
+ /// Initialize the random number generator
+ /// See https://docs.python.org/3/library/random.html#random.seed
+ abstract seed: a: string -> unit
+ /// Initialize the random number generator with current time
+ /// See https://docs.python.org/3/library/random.html#random.seed
+ abstract seed: unit -> unit
+
+ /// Return a random floating point number in the range [0.0, 1.0)
+ /// See https://docs.python.org/3/library/random.html#random.random
+ abstract random: unit -> float
+
+ /// Return a random floating point number N such that a <= N <= b
+ /// See https://docs.python.org/3/library/random.html#random.uniform
+ abstract uniform: a: float * b: float -> float
+
+ /// Return a random floating point number N such that low <= N <= high with triangular distribution
+ /// See https://docs.python.org/3/library/random.html#random.triangular
+ abstract triangular: low: float * high: float * mode: float -> float
+ /// Return a random floating point number N such that 0 <= N <= 1 with triangular distribution
+ /// See https://docs.python.org/3/library/random.html#random.triangular
+ abstract triangular: unit -> float
+
+ /// Return a random integer N such that a <= N <= b
+ /// See https://docs.python.org/3/library/random.html#random.randint
+ abstract randint: a: int * b: int -> int
+
+ /// Return a randomly selected element from range(start, stop, step)
+ /// See https://docs.python.org/3/library/random.html#random.randrange
+ abstract randrange: stop: int -> int
+ /// Return a randomly selected element from range(start, stop, step)
+ /// See https://docs.python.org/3/library/random.html#random.randrange
+ abstract randrange: start: int * stop: int -> int
+ /// Return a randomly selected element from range(start, stop, step)
+ /// See https://docs.python.org/3/library/random.html#random.randrange
+ abstract randrange: start: int * stop: int * step: int -> int
+
+ /// Return a random element from the non-empty sequence
+ /// 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
+
+ /// 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[] * 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
+ []
+ abstract sample: population: ResizeArray<'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[] * 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
+ []
+ abstract choices: population: ResizeArray<'T> * k: int -> ResizeArray<'T>
+
+ /// Shuffle the sequence x in place
+ /// See https://docs.python.org/3/library/random.html#random.shuffle
+ abstract shuffle: x: 'T[] -> unit
+ /// Shuffle the sequence x in place
+ /// See https://docs.python.org/3/library/random.html#random.shuffle
+ abstract shuffle: x: ResizeArray<'T> -> unit
+
+ /// Return a random integer with k random bits
+ /// See https://docs.python.org/3/library/random.html#random.getrandbits
+ abstract getrandbits: k: int -> int
+
+ /// Beta distribution
+ /// See https://docs.python.org/3/library/random.html#random.betavariate
+ abstract betavariate: alpha: float * beta: float -> float
+
+ /// Exponential distribution
+ /// See https://docs.python.org/3/library/random.html#random.expovariate
+ abstract expovariate: lambd: float -> float
+
+ /// Gamma distribution
+ /// See https://docs.python.org/3/library/random.html#random.gammavariate
+ abstract gammavariate: alpha: float * beta: float -> float
+
+ /// Gaussian distribution (same as normalvariate but faster)
+ /// See https://docs.python.org/3/library/random.html#random.gauss
+ abstract gauss: mu: float * sigma: float -> float
+
+ /// Log normal distribution
+ /// See https://docs.python.org/3/library/random.html#random.lognormvariate
+ abstract lognormvariate: mu: float * sigma: float -> float
+
+ /// Normal distribution
+ /// See https://docs.python.org/3/library/random.html#random.normalvariate
+ abstract normalvariate: mu: float * sigma: float -> float
+
+ /// Von Mises distribution
+ /// See https://docs.python.org/3/library/random.html#random.vonmisesvariate
+ abstract vonmisesvariate: mu: float * kappa: float -> float
+
+ /// Pareto distribution
+ /// See https://docs.python.org/3/library/random.html#random.paretovariate
+ abstract paretovariate: alpha: float -> float
+
+ /// Weibull distribution
+ /// See https://docs.python.org/3/library/random.html#random.weibullvariate
+ abstract weibullvariate: alpha: float * beta: float -> float
+
+/// Random variable generators
+[]
+let random: IExports = nativeOnly
diff --git a/src/stdlib/String.fs b/src/stdlib/String.fs
index 0136dc8..9bf2166 100644
--- a/src/stdlib/String.fs
+++ b/src/stdlib/String.fs
@@ -1,4 +1,4 @@
-/// Type bindings for Python string operations: https://docs.python.org/3/library/stdtypes.html#string-methods
+/// Type bindings for Python string module: https://docs.python.org/3/library/string.html
module Fable.Python.String
open System
@@ -10,3 +10,64 @@ type System.String with
[]
member _.format([] args: Object[]) = nativeOnly
+
+/// Template class for $-based string substitution
+[]
+type Template(template: string) =
+ /// The template string passed to the constructor
+ member _.template: string = nativeOnly
+
+ /// Perform substitution, returning a new string
+ /// Raises KeyError if placeholders are missing from mapping
+ []
+ member _.substitute(mapping: obj) : string = nativeOnly
+
+ /// Perform substitution using keyword arguments
+ []
+ member _.substituteKw(kwargs: obj) : string = nativeOnly
+
+ /// Like substitute(), but returns original placeholder if missing
+ []
+ member _.safe_substitute(mapping: obj) : string = nativeOnly
+
+ /// Like substitute(), but returns original placeholder if missing (keyword args)
+ []
+ member _.safe_substituteKw(kwargs: obj) : string = nativeOnly
+
+ /// Returns False if the template has invalid placeholders
+ []
+ member _.is_valid() : bool = nativeOnly
+
+ /// Returns a list of valid identifiers in the template
+ []
+ member _.get_identifiers() : ResizeArray = nativeOnly
+
+[]
+type IExports =
+ /// The concatenation of ascii_lowercase and ascii_uppercase
+ abstract ascii_letters: string
+ /// The lowercase letters 'abcdefghijklmnopqrstuvwxyz'
+ abstract ascii_lowercase: string
+ /// The uppercase letters 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ abstract ascii_uppercase: string
+ /// The string '0123456789'
+ abstract digits: string
+ /// The string '0123456789abcdefABCDEF'
+ abstract hexdigits: string
+ /// The string '01234567'
+ abstract octdigits: string
+ /// String of ASCII characters considered punctuation
+ abstract punctuation: string
+ /// String of ASCII characters considered printable
+ abstract printable: string
+ /// String containing all ASCII whitespace characters
+ abstract whitespace: string
+
+ /// Split the argument into words, capitalize each word, and join them
+ abstract capwords: s: string -> string
+ /// Split the argument into words using sep, capitalize each word, and join them
+ abstract capwords: s: string * sep: string -> string
+
+/// Python string module
+[]
+let pyString: IExports = nativeOnly
diff --git a/src/stdlib/Sys.fs b/src/stdlib/Sys.fs
index 23d6c76..1558e01 100644
--- a/src/stdlib/Sys.fs
+++ b/src/stdlib/Sys.fs
@@ -28,7 +28,7 @@ type VersionInfo =
[]
type IExports =
/// Command line arguments passed to the Python script
- abstract argv: string array
+ abstract argv: ResizeArray
/// Native byte order (endianness) of the system
abstract byteorder: ByteOrder
/// Version information encoded as a single integer
@@ -38,7 +38,7 @@ type IExports =
/// Maximum Unicode code point value
abstract maxunicode: int
/// Module search path - list of directory names where Python looks for modules
- abstract path: string array
+ abstract path: ResizeArray
/// Platform identifier string
abstract platform: string
/// Site-specific directory prefix where platform-independent Python files are installed
@@ -52,7 +52,11 @@ type IExports =
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
/// Exits with exit status 1, printing message to stderr
/// See https://docs.python.org/3/library/sys.html#sys.exit
abstract exit: message: string -> 'a
diff --git a/test/Fable.Python.Test.fsproj b/test/Fable.Python.Test.fsproj
index 88f4860..50dc0bd 100644
--- a/test/Fable.Python.Test.fsproj
+++ b/test/Fable.Python.Test.fsproj
@@ -18,6 +18,10 @@
+
+
+
+
diff --git a/test/TestAsyncIO.fs b/test/TestAsyncIO.fs
index 843a8a1..0f73981 100644
--- a/test/TestAsyncIO.fs
+++ b/test/TestAsyncIO.fs
@@ -29,5 +29,81 @@ let ``test sleep works`` () =
[]
let ``test sleep with value works`` () =
let tsk = task { return! asyncio.create_task (asyncio.sleep (0.1, 42)) }
- let result = asyncio.run (tsk)
+ let result = asyncio.run tsk
result |> equal 42
+
+[]
+let ``test multiple awaits work`` () =
+ let tsk =
+ task {
+ let! a = asyncio.create_task (asyncio.sleep (0.01, 10))
+ let! b = asyncio.create_task (asyncio.sleep (0.01, 20))
+ let! c = asyncio.create_task (asyncio.sleep (0.01, 12))
+ return a + b + c
+ }
+
+ let result = asyncio.run tsk
+ result |> equal 42
+
+[]
+let ``test nested tasks work`` () =
+ let inner () =
+ task {
+ do! asyncio.create_task (asyncio.sleep 0.01)
+ return 21
+ }
+
+ let outer =
+ task {
+ let! x = inner ()
+ let! y = inner ()
+ return x + y
+ }
+
+ let result = asyncio.run outer
+ result |> equal 42
+
+[]
+let ``test get_running_loop works`` () =
+ let tsk =
+ task {
+ let loop = asyncio.get_running_loop ()
+ return loop <> null
+ }
+
+ let result = asyncio.run tsk
+ result |> equal true
+
+[]
+let ``test task with string result works`` () =
+ let tsk =
+ task {
+ let! result = asyncio.create_task (asyncio.sleep (0.01, "hello"))
+ return result + " world"
+ }
+
+ let result = asyncio.run tsk
+ result |> equal "hello world"
+
+[]
+let ``test task with list result works`` () =
+ let tsk =
+ task {
+ let! a = asyncio.create_task (asyncio.sleep (0.01, [ 1; 2 ]))
+ let! b = asyncio.create_task (asyncio.sleep (0.01, [ 3; 4 ]))
+ return a @ b
+ }
+
+ let result = asyncio.run tsk
+ result |> equal [ 1; 2; 3; 4 ]
+
+[]
+let ``test task with option result works`` () =
+ let tsk =
+ task {
+ let! a = asyncio.create_task (asyncio.sleep (0.01, Some 42))
+ return a
+ }
+
+ let result = asyncio.run tsk
+ result |> equal (Some 42)
diff --git a/test/TestBase64.fs b/test/TestBase64.fs
new file mode 100644
index 0000000..05e0686
--- /dev/null
+++ b/test/TestBase64.fs
@@ -0,0 +1,87 @@
+module Fable.Python.Tests.Base64
+
+open Util.Testing
+open Fable.Python.Base64
+open Fable.Python.Builtins
+
+[]
+let ``test b64encode works`` () =
+ let input = builtins.bytes "Hello, World!"B
+ let result = base64.b64encode input
+ result |> equal (builtins.bytes "SGVsbG8sIFdvcmxkIQ=="B)
+
+[]
+let ``test b64decode from bytes works`` () =
+ let encoded = builtins.bytes "SGVsbG8sIFdvcmxkIQ=="B
+ let result = base64.b64decode encoded
+ result |> equal (builtins.bytes "Hello, World!"B)
+
+[]
+let ``test b64decode from string works`` () =
+ let encoded = "SGVsbG8sIFdvcmxkIQ=="
+ let result = base64.b64decode encoded
+ result |> equal (builtins.bytes "Hello, World!"B)
+
+[]
+let ``test roundtrip b64encode and b64decode works`` () =
+ let original = builtins.bytes "Test data 123!@#"B
+ let encoded = base64.b64encode original
+ let decoded = base64.b64decode encoded
+ decoded |> equal original
+
+[]
+let ``test standard_b64encode works`` () =
+ let input = builtins.bytes "Hello"B
+ let result = base64.standard_b64encode input
+ result |> equal (builtins.bytes "SGVsbG8="B)
+
+[]
+let ``test standard_b64decode from string works`` () =
+ let encoded = "SGVsbG8="
+ let result = base64.standard_b64decode encoded
+ result |> equal (builtins.bytes "Hello"B)
+
+[]
+let ``test standard_b64decode from bytes works`` () =
+ let encoded = builtins.bytes "SGVsbG8="B
+ let result = base64.standard_b64decode encoded
+ result |> equal (builtins.bytes "Hello"B)
+
+[]
+let ``test urlsafe_b64encode works`` () =
+ // Using bytes that produce + and / in standard base64
+ let input = builtins.bytes [| 0xfbuy; 0xffuy; 0xfeuy |]
+ let result = base64.urlsafe_b64encode input
+ // URL-safe uses - and _ instead of + and /
+ result |> equal (builtins.bytes "-__-"B)
+
+[]
+let ``test urlsafe_b64decode from string works`` () =
+ let encoded = "-__-"
+ let result = base64.urlsafe_b64decode encoded
+ result |> equal (builtins.bytes [| 0xfbuy; 0xffuy; 0xfeuy |])
+
+[]
+let ``test urlsafe_b64decode from bytes works`` () =
+ let encoded = builtins.bytes "-__-"B
+ let result = base64.urlsafe_b64decode encoded
+ result |> equal (builtins.bytes [| 0xfbuy; 0xffuy; 0xfeuy |])
+
+[]
+let ``test b32encode works`` () =
+ let input = builtins.bytes "Hello"B
+ let result = base64.b32encode input
+ result |> equal (builtins.bytes "JBSWY3DP"B)
+
+[]
+let ``test b16encode works`` () =
+ let input = builtins.bytes "Hello"B
+ let result = base64.b16encode input
+ result |> equal (builtins.bytes "48656C6C6F"B)
+
+[]
+let ``test empty input works`` () =
+ let input = builtins.bytes [||]
+ let encoded = base64.b64encode input
+ let decoded = base64.b64decode encoded
+ decoded |> equal (builtins.bytes [||])
diff --git a/test/TestLogging.fs b/test/TestLogging.fs
new file mode 100644
index 0000000..8f95323
--- /dev/null
+++ b/test/TestLogging.fs
@@ -0,0 +1,97 @@
+module Fable.Python.Tests.Logging
+
+open Util.Testing
+open Fable.Python.Logging
+
+[]
+let ``test getLogger works`` () =
+ let logger = logging.getLogger "test"
+ logger.name |> equal "test"
+
+[]
+let ``test getLogger without name returns root logger`` () =
+ let logger = logging.getLogger ()
+ logger.name |> equal "root"
+
+[]
+let ``test setLevel works`` () =
+ let logger = logging.getLogger "test_level"
+ logger.setLevel Level.DEBUG
+ logger.getEffectiveLevel () |> equal Level.DEBUG
+
+[]
+let ``test isEnabledFor works`` () =
+ let logger = logging.getLogger "test_enabled"
+ logger.setLevel Level.WARNING
+ logger.isEnabledFor Level.ERROR |> equal true
+ logger.isEnabledFor Level.DEBUG |> equal false
+
+[]
+let ``test debug logging works`` () =
+ let logger = logging.getLogger "test_debug"
+ logger.setLevel Level.DEBUG
+ // Just verify it doesn't throw
+ logger.debug "Debug message"
+ true |> equal true
+
+[]
+let ``test info logging works`` () =
+ let logger = logging.getLogger "test_info"
+ logger.setLevel Level.INFO
+ logger.info "Info message"
+ true |> equal true
+
+[]
+let ``test warning logging works`` () =
+ let logger = logging.getLogger "test_warning"
+ logger.warning "Warning message"
+ true |> equal true
+
+[]
+let ``test error logging works`` () =
+ let logger = logging.getLogger "test_error"
+ logger.error "Error message"
+ true |> equal true
+
+[]
+let ``test critical logging works`` () =
+ let logger = logging.getLogger "test_critical"
+ logger.critical "Critical message"
+ true |> equal true
+
+[]
+let ``test log with level works`` () =
+ let logger = logging.getLogger "test_log"
+ logger.setLevel Level.DEBUG
+ logger.log (Level.INFO, "Log message with level")
+ true |> equal true
+
+[]
+let ``test Level constants`` () =
+ Level.DEBUG |> equal 10
+ Level.INFO |> equal 20
+ Level.WARNING |> equal 30
+ Level.ERROR |> equal 40
+ Level.CRITICAL |> equal 50
+ Level.NOTSET |> equal 0
+
+[]
+let ``test logging module level constants`` () =
+ logging.DEBUG |> equal 10
+ logging.INFO |> equal 20
+ logging.WARNING |> equal 30
+ logging.ERROR |> equal 40
+ logging.CRITICAL |> equal 50
+
+[]
+let ``test hasHandlers works`` () =
+ let logger = logging.getLogger "test_has_handlers"
+ // A new logger typically has no handlers
+ // (unless propagating to parent that has handlers)
+ let _ = logger.hasHandlers ()
+ true |> equal true
+
+[]
+let ``test basicConfig works`` () =
+ logging.basicConfig (level = Level.DEBUG, format = "%(message)s")
+ true |> equal true
diff --git a/test/TestMath.fs b/test/TestMath.fs
new file mode 100644
index 0000000..55349f8
--- /dev/null
+++ b/test/TestMath.fs
@@ -0,0 +1,121 @@
+module Fable.Python.Tests.Math
+
+open Util.Testing
+open Fable.Python.Math
+
+[]
+let ``test ceil works`` () =
+ math.ceil 2.3 |> equal 3
+ math.ceil 2.9 |> equal 3
+ math.ceil -1.5 |> equal -1
+
+[]
+let ``test floor works`` () =
+ math.floor 2.3 |> equal 2
+ math.floor 2.9 |> equal 2
+ math.floor -1.5 |> equal -2
+
+[]
+let ``test comb works`` () =
+ math.comb 5 2 |> equal 10
+ math.comb 10 3 |> equal 120
+
+[]
+let ``test copysign works`` () =
+ math.copysign 1.0 -1 |> equal -1.0
+ math.copysign -1.0 1 |> equal 1.0
+
+[]
+let ``test fabs works`` () =
+ math.fabs -5.0 |> equal 5.0
+ math.fabs 5.0 |> equal 5.0
+
+[]
+let ``test factorial works`` () =
+ math.factorial 5.0 |> equal 120.0
+ math.factorial 0.0 |> equal 1.0
+
+[]
+let ``test fmod works`` () =
+ math.fmod 10 3 |> equal 1
+ math.fmod 7 2 |> equal 1
+
+[]
+let ``test gcd works`` () =
+ math.gcd (12, 8) |> equal 4
+ math.gcd (15, 25) |> equal 5
+
+[]
+let ``test lcm works`` () =
+ math.lcm (4, 6) |> equal 12
+ math.lcm (3, 5) |> equal 15
+
+[]
+let ``test isfinite works`` () =
+ math.isfinite 1.0 |> equal true
+ math.isfinite infinity |> equal false
+ math.isfinite nan |> equal false
+
+[]
+let ``test isinf works`` () =
+ math.isinf infinity |> equal true
+ math.isinf (-infinity) |> equal true
+ math.isinf 1.0 |> equal false
+
+[]
+let ``test isnan works`` () =
+ math.isnan nan |> equal true
+ math.isnan 1.0 |> equal false
+
+[]
+let ``test exp works`` () =
+ math.exp 0.0 |> equal 1.0
+ math.exp 1.0 |> fun x -> (x > 2.718 && x < 2.719) |> equal true
+
+[]
+let ``test log works`` () =
+ math.log 1.0 |> equal 0.0
+ math.log (math.exp 1.0) |> fun x -> (x > 0.999 && x < 1.001) |> equal true
+
+[]
+let ``test log2 works`` () =
+ math.log2 8.0 |> equal 3.0
+ math.log2 1.0 |> equal 0.0
+
+[]
+let ``test log10 works`` () =
+ math.log10 100.0 |> equal 2.0
+ math.log10 1.0 |> equal 0.0
+
+[]
+let ``test pow works`` () =
+ math.pow 2.0 3.0 |> equal 8.0
+ math.pow 10.0 2.0 |> equal 100.0
+
+[]
+let ``test sin works`` () =
+ math.sin 0.0 |> equal 0.0
+
+[]
+let ``test cos works`` () =
+ math.cos 0.0 |> equal 1.0
+
+[]
+let ``test tan works`` () =
+ math.tan 0.0 |> equal 0.0
+
+[]
+let ``test asin works`` () =
+ math.asin 0.0 |> equal 0.0
+
+[]
+let ``test acos works`` () =
+ math.acos 1.0 |> equal 0.0
+
+[]
+let ``test atan works`` () =
+ math.atan 0.0 |> equal 0.0
+
+[]
+let ``test atan2 works`` () =
+ math.atan2 0.0 1.0 |> equal 0.0
diff --git a/test/TestRandom.fs b/test/TestRandom.fs
new file mode 100644
index 0000000..7dfbae0
--- /dev/null
+++ b/test/TestRandom.fs
@@ -0,0 +1,121 @@
+module Fable.Python.Tests.Random
+
+open Util.Testing
+open Fable.Python.Random
+
+[]
+let ``test seed with int works`` () =
+ random.seed 42
+ let r1 = random.random ()
+ random.seed 42
+ let r2 = random.random ()
+ r1 |> equal r2
+
+[]
+let ``test random returns value in range`` () =
+ random.seed 42
+ let r = random.random ()
+ (r >= 0.0 && r < 1.0) |> equal true
+
+[]
+let ``test uniform works`` () =
+ random.seed 42
+ let r = random.uniform (10.0, 20.0)
+ (r >= 10.0 && r <= 20.0) |> equal true
+
+[]
+let ``test randint works`` () =
+ random.seed 42
+ let r = random.randint (1, 10)
+ (r >= 1 && r <= 10) |> equal true
+
+[]
+let ``test randrange with stop works`` () =
+ random.seed 42
+ let r = random.randrange 10
+ (r >= 0 && r < 10) |> equal true
+
+[]
+let ``test randrange with start and stop works`` () =
+ random.seed 42
+ let r = random.randrange (5, 10)
+ (r >= 5 && r < 10) |> equal true
+
+[]
+let ``test randrange with step works`` () =
+ random.seed 42
+ let r = random.randrange (0, 10, 2)
+ (r >= 0 && r < 10 && r % 2 = 0) |> equal true
+
+[]
+let ``test choice with array works`` () =
+ random.seed 42
+ let arr = [| 1; 2; 3; 4; 5 |]
+ let r = random.choice arr
+ (Array.contains r arr) |> equal true
+
+[]
+let ``test choice with list works`` () =
+ random.seed 42
+ let lst = [ "a"; "b"; "c" ]
+ let r = random.choice lst
+ (List.contains r lst) |> equal true
+
+[]
+let ``test sample works`` () =
+ random.seed 42
+ let arr = [| 1; 2; 3; 4; 5 |]
+ let r = random.sample (arr, 3)
+ Seq.length r |> equal 3
+
+[]
+let ``test choices works`` () =
+ random.seed 42
+ let arr = [| 1; 2; 3 |]
+ let r = random.choices (arr, 5)
+ Seq.length r |> equal 5
+
+[]
+let ``test shuffle works`` () =
+ random.seed 42
+ let arr = ResizeArray [ 1; 2; 3; 4; 5 ]
+ random.shuffle arr
+ // After shuffle, the array should still contain all original elements
+ (arr |> Seq.sort |> Seq.toList) |> equal [ 1; 2; 3; 4; 5 ]
+
+[]
+let ``test getrandbits works`` () =
+ random.seed 42
+ let r = random.getrandbits 8
+ (r >= 0 && r < 256) |> equal true
+
+[]
+let ``test gauss works`` () =
+ random.seed 42
+ // Just verify it returns a float without error
+ let r = random.gauss (0.0, 1.0)
+ (r = r) |> equal true // NaN check
+
+[]
+let ``test expovariate works`` () =
+ random.seed 42
+ let r = random.expovariate 1.0
+ (r >= 0.0) |> equal true
+
+[]
+let ``test betavariate works`` () =
+ random.seed 42
+ let r = random.betavariate (2.0, 5.0)
+ (r >= 0.0 && r <= 1.0) |> equal true
+
+[]
+let ``test triangular works`` () =
+ random.seed 42
+ let r = random.triangular (0.0, 10.0, 5.0)
+ (r >= 0.0 && r <= 10.0) |> equal true
+
+[]
+let ``test normalvariate works`` () =
+ random.seed 42
+ let r = random.normalvariate (0.0, 1.0)
+ (r = r) |> equal true // NaN check
diff --git a/test/TestString.fs b/test/TestString.fs
index c92b7c8..d449e77 100644
--- a/test/TestString.fs
+++ b/test/TestString.fs
@@ -12,3 +12,96 @@ let ``test string format works`` () =
let ``test string format 2 works`` () =
let result = "The sum of {0} + 2 is {1}".format (1, 1 + 2)
result |> equal "The sum of 1 + 2 is 3"
+
+// String module constants
+
+[]
+let ``test ascii_letters constant`` () =
+ pyString.ascii_letters |> equal "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+[]
+let ``test ascii_lowercase constant`` () =
+ pyString.ascii_lowercase |> equal "abcdefghijklmnopqrstuvwxyz"
+
+[]
+let ``test ascii_uppercase constant`` () =
+ pyString.ascii_uppercase |> equal "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+[]
+let ``test digits constant`` () =
+ pyString.digits |> equal "0123456789"
+
+[]
+let ``test hexdigits constant`` () =
+ pyString.hexdigits |> equal "0123456789abcdefABCDEF"
+
+[]
+let ``test octdigits constant`` () =
+ pyString.octdigits |> equal "01234567"
+
+[]
+let ``test punctuation constant`` () =
+ // Just verify it contains some expected punctuation
+ pyString.punctuation.Contains "!" |> equal true
+ pyString.punctuation.Contains "." |> equal true
+ pyString.punctuation.Contains "@" |> equal true
+
+[]
+let ``test printable constant`` () =
+ // Printable includes digits, letters, punctuation, whitespace
+ pyString.printable.Contains "a" |> equal true
+ pyString.printable.Contains "0" |> equal true
+ pyString.printable.Contains " " |> equal true
+
+[]
+let ``test whitespace constant`` () =
+ // Whitespace includes space, tab, newline, etc.
+ pyString.whitespace.Contains " " |> equal true
+ pyString.whitespace.Contains "\t" |> equal true
+ pyString.whitespace.Contains "\n" |> equal true
+
+// capwords function
+
+[]
+let ``test capwords works`` () =
+ pyString.capwords "hello world" |> equal "Hello World"
+
+[]
+let ``test capwords with multiple spaces`` () =
+ pyString.capwords "hello world" |> equal "Hello World"
+
+[]
+let ``test capwords with custom separator`` () =
+ pyString.capwords ("hello-world", "-") |> equal "Hello-World"
+
+// Template class
+
+[]
+let ``test Template substitute with dict works`` () =
+ let t = Template "$who likes $what"
+ let result = t.substitute {| who = "tim"; what = "kung pao" |}
+ result |> equal "tim likes kung pao"
+
+[]
+let ``test Template safe_substitute works`` () =
+ let t = Template "$who likes $what"
+ let result = t.safe_substitute {| who = "tim" |}
+ result |> equal "tim likes $what"
+
+[]
+let ``test Template template property`` () =
+ let t = Template "$name"
+ t.template |> equal "$name"
+
+[]
+let ``test Template is_valid works`` () =
+ let t = Template "$valid"
+ t.is_valid () |> equal true
+
+[]
+let ``test Template get_identifiers works`` () =
+ let t = Template "$who likes $what"
+ let ids = t.get_identifiers ()
+ Seq.length ids |> equal 2
+ Seq.contains "who" ids |> equal true
+ Seq.contains "what" ids |> equal true
diff --git a/uv.lock b/uv.lock
index dfa132e..fd4c328 100644
--- a/uv.lock
+++ b/uv.lock
@@ -161,7 +161,7 @@ wheels = [
[[package]]
name = "fable-python"
-version = "5.0.0a20"
+version = "0.0.0"
source = { virtual = "." }
dependencies = [
{ name = "fable-library" },