Skip to content

Commit 8223cc8

Browse files
dbrattliclaude
andauthored
feat: Add threading, heapq, traceback bindings and extend builtins/queue (#219)
Add new stdlib bindings so downstream projects don't need to create their own FFI wrappers for common Python modules. New modules: - Threading: Thread, Lock, RLock, Event classes + module functions - Heapq: heap operations (heappush, heappop, heapify, nlargest, etc.) - Traceback: print_exc, format_exc, print_stack, format_stack Extended existing modules: - Builtins: getattr, setattr, hasattr, isinstance, type - Queue: get_nowait(), Empty and Full exceptions Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 293bf89 commit 8223cc8

File tree

12 files changed

+364
-19
lines changed

12 files changed

+364
-19
lines changed

src/Fable.Python.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@
2323
<Compile Include="stdlib/Math.fs" />
2424
<Compile Include="stdlib/Random.fs" />
2525
<Compile Include="stdlib/Os.fs" />
26+
<Compile Include="stdlib/Heapq.fs" />
2627
<Compile Include="stdlib/Queue.fs" />
2728
<Compile Include="stdlib/String.fs" />
2829
<Compile Include="stdlib/Sys.fs" />
30+
<Compile Include="stdlib/Threading.fs" />
2931
<Compile Include="stdlib/Time.fs" />
3032
<Compile Include="stdlib/TkInter.fs" />
33+
<Compile Include="stdlib/Traceback.fs" />
3134

3235
<Compile Include="cognite-sdk/CogniteSdk.fs" />
3336
<Compile Include="flask/Flask.fs" />

src/stdlib/Builtins.fs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,26 @@ type IExports =
270270
?opener: _Opener ->
271271
TextIOWrapper
272272

273+
/// Return the value of the named attribute of object. name must be a string.
274+
/// See https://docs.python.org/3/library/functions.html#getattr
275+
abstract getattr: obj: obj * name: string -> 'T
276+
/// Return the value of the named attribute of object. If the attribute does not exist,
277+
/// default is returned.
278+
/// See https://docs.python.org/3/library/functions.html#getattr
279+
abstract getattr: obj: obj * name: string * ``default``: 'T -> 'T
280+
/// Sets the named attribute on the given object to the specified value.
281+
/// See https://docs.python.org/3/library/functions.html#setattr
282+
abstract setattr: obj: obj * name: string * value: obj -> unit
283+
/// Return True if the string is the name of one of the object's attributes.
284+
/// See https://docs.python.org/3/library/functions.html#hasattr
285+
abstract hasattr: obj: obj * name: string -> bool
286+
/// Return True if the object argument is an instance of the classinfo argument.
287+
/// See https://docs.python.org/3/library/functions.html#isinstance
288+
abstract isinstance: obj: obj * classinfo: obj -> bool
289+
/// Return the type of an object.
290+
/// See https://docs.python.org/3/library/functions.html#type
291+
abstract ``type``: obj -> obj
292+
273293
[<ImportAll("builtins")>]
274294
let builtins: IExports = nativeOnly
275295

@@ -331,3 +351,12 @@ let __name__: string = nativeOnly
331351

332352
/// Python print function. Takes a single argument, so can be used with e.g string interpolation.
333353
let print obj = builtins.print obj
354+
355+
/// Return the value of the named attribute of object with a default.
356+
let getattr obj name defaultValue = builtins.getattr (obj, name, defaultValue)
357+
358+
/// Sets the named attribute on the given object to the specified value.
359+
let setattr obj name value = builtins.setattr (obj, name, value)
360+
361+
/// Return True if the string is the name of one of the object's attributes.
362+
let hasattr obj name = builtins.hasattr (obj, name)

src/stdlib/Heapq.fs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/// Type bindings for Python heapq module: https://docs.python.org/3/library/heapq.html
2+
module Fable.Python.Heapq
3+
4+
open Fable.Core
5+
6+
// fsharplint:disable MemberNames
7+
8+
[<Erase>]
9+
type IExports =
10+
/// Push the value item onto the heap, maintaining the heap invariant
11+
/// See https://docs.python.org/3/library/heapq.html#heapq.heappush
12+
abstract heappush: heap: ResizeArray<'T> * item: 'T -> unit
13+
/// Pop and return the smallest item from the heap, maintaining the heap invariant
14+
/// See https://docs.python.org/3/library/heapq.html#heapq.heappop
15+
abstract heappop: heap: ResizeArray<'T> -> 'T
16+
/// Push item on the heap, then pop and return the smallest item from the heap
17+
/// See https://docs.python.org/3/library/heapq.html#heapq.heappushpop
18+
abstract heappushpop: heap: ResizeArray<'T> * item: 'T -> 'T
19+
/// Transform list into a heap, in-place, in linear time
20+
/// See https://docs.python.org/3/library/heapq.html#heapq.heapify
21+
abstract heapify: x: ResizeArray<'T> -> unit
22+
/// Pop and return the smallest item from the heap, and also push the new item
23+
/// See https://docs.python.org/3/library/heapq.html#heapq.heapreplace
24+
abstract heapreplace: heap: ResizeArray<'T> * item: 'T -> 'T
25+
/// Return a list with the n largest elements from the dataset
26+
/// See https://docs.python.org/3/library/heapq.html#heapq.nlargest
27+
abstract nlargest: n: int * iterable: 'T seq -> ResizeArray<'T>
28+
/// Return a list with the n smallest elements from the dataset
29+
/// See https://docs.python.org/3/library/heapq.html#heapq.nsmallest
30+
abstract nsmallest: n: int * iterable: 'T seq -> ResizeArray<'T>
31+
32+
/// Heap queue algorithm
33+
[<ImportAll("heapq")>]
34+
let heapq: IExports = nativeOnly

src/stdlib/Queue.fs

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ open Fable.Core
77

88
[<Import("Queue", "queue")>]
99
type Queue<'T>() =
10-
/// Return the approximate size of the queue. Note, qsize() > 0 doesnt guarantee that a subsequent get() will not
10+
/// Return the approximate size of the queue. Note, qsize() > 0 doesn't guarantee that a subsequent get() will not
1111
/// block, nor will qsize() < maxsize guarantee that put() will not block.
1212
member x.qsize() : int = nativeOnly
13-
/// Return True if the queue is empty, False otherwise. If empty() returns True it doesnt guarantee that a
14-
/// subsequent call to put() will not block. Similarly, if empty() returns False it doesnt guarantee that a
13+
/// Return True if the queue is empty, False otherwise. If empty() returns True it doesn't guarantee that a
14+
/// subsequent call to put() will not block. Similarly, if empty() returns False it doesn't guarantee that a
1515
/// subsequent call to get() will not block.
1616
member x.empty() : bool = nativeOnly
17-
/// Return True if the queue is full, False otherwise. If full() returns True it doesnt guarantee that a subsequent
18-
/// call to get() will not block. Similarly, if full() returns False it doesnt guarantee that a subsequent call to
17+
/// Return True if the queue is full, False otherwise. If full() returns True it doesn't guarantee that a subsequent
18+
/// call to get() will not block. Similarly, if full() returns False it doesn't guarantee that a subsequent call to
1919
/// put() will not block.
2020
member x.full() : bool = nativeOnly
2121
/// Put item into the queue. If optional args block is true and timeout is None (the default), block if necessary
@@ -34,6 +34,10 @@ type Queue<'T>() =
3434
/// operation goes into an uninterruptible wait on an underlying lock. This means that no exceptions can occur, and
3535
/// in particular a SIGINT will not trigger a KeyboardInterrupt.
3636
member x.get(?block: bool, ?timeout: float) : 'T = nativeOnly
37+
/// Equivalent to get(False).
38+
/// See https://docs.python.org/3/library/queue.html#queue.Queue.get_nowait
39+
[<Emit("$0.get_nowait()")>]
40+
member x.get_nowait() : 'T = nativeOnly
3741
/// Blocks until all items in the queue have been gotten and processed.
3842
///
3943
/// The count of unfinished tasks goes up whenever an item is added to the queue. The count goes down whenever a
@@ -57,24 +61,31 @@ type LifoQueue<'T>() =
5761

5862
[<Import("SimpleQueue", "queue")>]
5963
type SimpleQueue<'T>() =
60-
/// Return the approximate size of the queue. Note, qsize() > 0 doesnt guarantee that a subsequent get() will not
64+
/// Return the approximate size of the queue. Note, qsize() > 0 doesn't guarantee that a subsequent get() will not
6165
/// block, nor will qsize() < maxsize guarantee that put() will not block.
6266
member x.qsize() : int = nativeOnly
63-
/// Return True if the queue is empty, False otherwise. If empty() returns True it doesnt guarantee that a
64-
/// subsequent call to put() will not block. Similarly, if empty() returns False it doesnt guarantee that a
67+
/// Return True if the queue is empty, False otherwise. If empty() returns True it doesn't guarantee that a
68+
/// subsequent call to put() will not block. Similarly, if empty() returns False it doesn't guarantee that a
6569
/// subsequent call to get() will not block.
6670
member x.empty() : bool = nativeOnly
67-
/// Return True if the queue is full, False otherwise. If full() returns True it doesnt guarantee that a subsequent
68-
/// call to get() will not block. Similarly, if full() returns False it doesnt guarantee that a subsequent call to
71+
/// Return True if the queue is full, False otherwise. If full() returns True it doesn't guarantee that a subsequent
72+
/// call to get() will not block. Similarly, if full() returns False it doesn't guarantee that a subsequent call to
6973
/// put() will not block.
7074
member x.put(item: 'T, ?block: bool, ?timeout: float) : unit = nativeOnly
71-
/// Remove and return an item from the queue. If optional args block is true and timeout is None (the default),
72-
/// block if necessary until an item is available. If timeout is a positive number, it blocks at most timeout
73-
/// seconds and raises the Empty exception if no item was available within that time. Otherwise (block is false),
74-
/// return an item if one is immediately available, else raise the Empty exception (timeout is ignored in that
75-
/// case).
76-
///
77-
/// Prior to 3.0 on POSIX systems, and for all versions on Windows, if block is true and timeout is None, this
78-
/// operation goes into an uninterruptible wait on an underlying lock. This means that no exceptions can occur, and
79-
/// in particular a SIGINT will not trigger a KeyboardInterrupt.
75+
/// Remove and return an item from the queue.
8076
member x.get(?block: bool, ?timeout: float) : 'T = nativeOnly
77+
/// Equivalent to get(False).
78+
[<Emit("$0.get_nowait()")>]
79+
member x.get_nowait() : 'T = nativeOnly
80+
81+
/// Exception raised when non-blocking get() is called on an empty Queue
82+
/// See https://docs.python.org/3/library/queue.html#queue.Empty
83+
[<Import("Empty", "queue")>]
84+
type Empty() =
85+
inherit exn()
86+
87+
/// Exception raised when non-blocking put() is called on a full Queue
88+
/// See https://docs.python.org/3/library/queue.html#queue.Full
89+
[<Import("Full", "queue")>]
90+
type Full() =
91+
inherit exn()

src/stdlib/Threading.fs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/// Type bindings for Python threading module: https://docs.python.org/3/library/threading.html
2+
module Fable.Python.Threading
3+
4+
open Fable.Core
5+
6+
// fsharplint:disable MemberNames
7+
8+
[<Erase>]
9+
type IExports =
10+
/// Return the 'thread identifier' of the current thread
11+
/// See https://docs.python.org/3/library/threading.html#threading.get_ident
12+
abstract get_ident: unit -> int
13+
/// Return the main Thread object
14+
/// See https://docs.python.org/3/library/threading.html#threading.main_thread
15+
abstract main_thread: unit -> Thread
16+
/// Return the current Thread object
17+
/// See https://docs.python.org/3/library/threading.html#threading.current_thread
18+
abstract current_thread: unit -> Thread
19+
/// Return the number of Thread objects currently alive
20+
/// See https://docs.python.org/3/library/threading.html#threading.active_count
21+
abstract active_count: unit -> int
22+
/// Return a list of all Thread objects currently active
23+
/// See https://docs.python.org/3/library/threading.html#threading.enumerate
24+
abstract enumerate: unit -> Thread list
25+
/// Return a new thread-local data object
26+
/// See https://docs.python.org/3/library/threading.html#threading.local
27+
abstract local: unit -> obj
28+
29+
/// A thread of execution
30+
/// See https://docs.python.org/3/library/threading.html#threading.Thread
31+
and [<Import("Thread", "threading")>] Thread(?target: unit -> unit, ?name: string, ?daemon: bool) =
32+
/// Start the thread's activity
33+
member _.start() : unit = nativeOnly
34+
/// Wait until the thread terminates
35+
member _.join(?timeout: float) : unit = nativeOnly
36+
/// A boolean value indicating whether this thread is a daemon thread
37+
member _.daemon: bool = nativeOnly
38+
/// The thread's name
39+
member _.name: string = nativeOnly
40+
/// The 'thread identifier' of this thread
41+
member _.ident: int option = nativeOnly
42+
/// Whether the thread is alive
43+
member _.is_alive() : bool = nativeOnly
44+
45+
/// A lock object (mutual exclusion)
46+
/// See https://docs.python.org/3/library/threading.html#threading.Lock
47+
[<Import("Lock", "threading")>]
48+
type Lock() =
49+
/// Acquire the lock
50+
member _.acquire(?blocking: bool, ?timeout: float) : bool = nativeOnly
51+
/// Release the lock
52+
member _.release() : unit = nativeOnly
53+
/// Return whether the lock is locked
54+
member _.locked() : bool = nativeOnly
55+
56+
/// A reentrant lock object
57+
/// See https://docs.python.org/3/library/threading.html#threading.RLock
58+
[<Import("RLock", "threading")>]
59+
type RLock() =
60+
/// Acquire the lock
61+
member _.acquire(?blocking: bool, ?timeout: float) : bool = nativeOnly
62+
/// Release the lock
63+
member _.release() : unit = nativeOnly
64+
65+
/// An event object for thread synchronization
66+
/// See https://docs.python.org/3/library/threading.html#threading.Event
67+
[<Import("Event", "threading")>]
68+
type Event() =
69+
/// Set the internal flag to true
70+
member _.set() : unit = nativeOnly
71+
/// Reset the internal flag to false
72+
member _.clear() : unit = nativeOnly
73+
/// Return true if and only if the internal flag is true
74+
member _.is_set() : bool = nativeOnly
75+
/// Block until the internal flag is true or timeout
76+
member _.wait(?timeout: float) : bool = nativeOnly
77+
78+
/// Threading module access and utilities
79+
[<ImportAll("threading")>]
80+
let threading: IExports = nativeOnly

src/stdlib/Traceback.fs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// Type bindings for Python traceback module: https://docs.python.org/3/library/traceback.html
2+
module Fable.Python.Traceback
3+
4+
open Fable.Core
5+
6+
// fsharplint:disable MemberNames
7+
8+
[<Erase>]
9+
type IExports =
10+
/// Print exception information and stack trace entries
11+
/// See https://docs.python.org/3/library/traceback.html#traceback.print_exc
12+
abstract print_exc: unit -> unit
13+
/// Format exception information and stack trace entries as a string
14+
/// See https://docs.python.org/3/library/traceback.html#traceback.format_exc
15+
abstract format_exc: unit -> string
16+
/// Print stack trace entries
17+
/// See https://docs.python.org/3/library/traceback.html#traceback.print_stack
18+
abstract print_stack: unit -> unit
19+
/// Format stack trace entries as a list of strings
20+
/// See https://docs.python.org/3/library/traceback.html#traceback.format_stack
21+
abstract format_stack: unit -> ResizeArray<string>
22+
23+
/// Extract, format and print exceptions and their tracebacks
24+
[<ImportAll("traceback")>]
25+
let traceback: IExports = nativeOnly

test/Fable.Python.Test.fsproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
<Compile Include="TestAst.fs" />
1616
<Compile Include="TestAsyncIO.fs" />
1717
<Compile Include="TestBuiltins.fs" />
18+
<Compile Include="TestBuiltinsAttr.fs" />
19+
<Compile Include="TestHeapq.fs" />
20+
<Compile Include="TestQueue.fs" />
21+
<Compile Include="TestThreading.fs" />
22+
<Compile Include="TestTraceback.fs" />
1823
<Compile Include="TestOs.fs" />
1924
<Compile Include="TestJson.fs" />
2025
<Compile Include="TestLogging.fs" />

test/TestBuiltinsAttr.fs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module Fable.Python.Tests.BuiltinsAttr
2+
3+
open Fable.Python.Testing
4+
open Fable.Python.Builtins
5+
open Fable.Python.Threading
6+
7+
[<Fact>]
8+
let ``test hasattr works`` () =
9+
let local = threading.local ()
10+
setattr local "name" "test"
11+
builtins.hasattr (local, "name") |> equal true
12+
13+
[<Fact>]
14+
let ``test getattr with default works`` () =
15+
let local = threading.local ()
16+
builtins.getattr (local, "missing", "default") |> equal "default"
17+
18+
[<Fact>]
19+
let ``test setattr and getattr work`` () =
20+
let local = threading.local ()
21+
builtins.setattr (local, "x", 42)
22+
builtins.getattr (local, "x", 0) |> equal 42
23+
24+
[<Fact>]
25+
let ``test hasattr returns false for missing attribute`` () =
26+
let local = threading.local ()
27+
builtins.hasattr (local, "nonexistent") |> equal false

test/TestHeapq.fs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module Fable.Python.Tests.Heapq
2+
3+
open Fable.Python.Testing
4+
open Fable.Python.Heapq
5+
6+
[<Fact>]
7+
let ``test heappush and heappop work`` () =
8+
let heap = ResizeArray<int>()
9+
heapq.heappush (heap, 3)
10+
heapq.heappush (heap, 1)
11+
heapq.heappush (heap, 2)
12+
heapq.heappop heap |> equal 1
13+
heapq.heappop heap |> equal 2
14+
heapq.heappop heap |> equal 3
15+
16+
[<Fact>]
17+
let ``test heapify works`` () =
18+
let heap = ResizeArray [5; 3; 1; 4; 2]
19+
heapq.heapify heap
20+
heapq.heappop heap |> equal 1
21+
22+
[<Fact>]
23+
let ``test heappushpop works`` () =
24+
let heap = ResizeArray [2; 4; 6]
25+
heapq.heapify heap
26+
// Push 1, then pop smallest (1)
27+
heapq.heappushpop (heap, 1) |> equal 1
28+
// Push 3, then pop smallest (2)
29+
heapq.heappushpop (heap, 3) |> equal 2
30+
31+
[<Fact>]
32+
let ``test nlargest works`` () =
33+
let result = heapq.nlargest (3, [1; 5; 2; 8; 3; 7])
34+
result |> equal (ResizeArray [8; 7; 5])
35+
36+
[<Fact>]
37+
let ``test nsmallest works`` () =
38+
let result = heapq.nsmallest (3, [1; 5; 2; 8; 3; 7])
39+
result |> equal (ResizeArray [1; 2; 3])

0 commit comments

Comments
 (0)