Skip to content

Commit 8c11d74

Browse files
github-actions[bot]Copilotdbrattliclaude
authored
feat(stdlib): add collections module bindings (Counter, defaultdict, deque, OrderedDict) (#272)
* feat(stdlib): add collections module bindings (Counter, defaultdict, deque, OrderedDict) Adds F# bindings for the most commonly used classes in Python's collections module: - Counter<'T>: count hashable objects; supports most_common, elements, total, update, subtract; static Counter.ofSeq factory - defaultdict<'TKey, 'TValue>: dict with callable factory for missing keys; includes contains helper to check key presence without invoking factory - deque<'T>: O(1) double-ended queue with append/appendleft, pop/popleft, rotate, and optional bounded maxlen; static ofSeq and withMaxlen factories - OrderedDict<'TKey, 'TValue>: dict subclass with move_to_end and order-sensitive popitem Also adds 35 tests covering construction, mutation, and edge cases for all four types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(stdlib): address review feedback on collections bindings - defaultdict: switch to `defaultdict.withFactory(...)` static factory. The primary-ctor approach compiled to `defaultdict(default_factory=...)`, which Python rejects: defaultdict only accepts the factory positionally. Tests now actually exercise the factory path. - Counter: add `keys`, `values`, `pop`, `clear`, `contains` so the dict-side of the API is reachable; tighten `most_common()` docstring. - Drop unnecessary `[<Emit>]` attributes that duplicated Fable defaults (`Counter.most_common(n)`, `deque.count`). - Replace `__setitem__` Emit on `defaultdict.set` / `OrderedDict.set` with `$0[$1] = $2` for cleaner generated Python. - Add iterable-of-pairs `update` overload on defaultdict and OrderedDict. - Rename test cases to start with `test ` so pytest discovers them (previously skipped because the compiled function names didn't match pytest's default discovery pattern). Add `extendleft` reversal test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Dag Brattli <dag@brattli.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5d82af6 commit 8c11d74

4 files changed

Lines changed: 553 additions & 0 deletions

File tree

src/Fable.Python.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<Compile Include="stdlib/Math.fs" />
2424
<Compile Include="stdlib/Random.fs" />
2525
<Compile Include="stdlib/Os.fs" />
26+
<Compile Include="stdlib/Collections.fs" />
2627
<Compile Include="stdlib/Heapq.fs" />
2728
<Compile Include="stdlib/Itertools.fs" />
2829
<Compile Include="stdlib/Datetime.fs" />

src/stdlib/Collections.fs

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
/// Type bindings for Python collections module: https://docs.python.org/3/library/collections.html
2+
module Fable.Python.Collections
3+
4+
open Fable.Core
5+
6+
// fsharplint:disable MemberNames
7+
8+
// ============================================================================
9+
// Counter
10+
// ============================================================================
11+
12+
/// A dict subclass for counting hashable objects.
13+
/// Elements are stored as dictionary keys and their counts are stored as values.
14+
/// Counts are allowed to be any integer value including zero or negative counts.
15+
/// See https://docs.python.org/3/library/collections.html#collections.Counter
16+
[<Import("Counter", "collections")>]
17+
type Counter<'T>() =
18+
/// Get the count for key; missing keys return 0 (unlike a regular dict)
19+
[<Emit("$0[$1]")>]
20+
member _.Item(key: 'T) : int = nativeOnly
21+
22+
/// Return elements and their counts as key-value pairs
23+
member _.items() : seq<'T * int> = nativeOnly
24+
25+
/// Return the elements (keys) of the counter
26+
member _.keys() : seq<'T> = nativeOnly
27+
28+
/// Return the counts (values) of the counter
29+
member _.values() : seq<int> = nativeOnly
30+
31+
/// Return an iterator over elements, repeating each as many times as its count.
32+
/// Elements with counts <= 0 are not included.
33+
/// See https://docs.python.org/3/library/collections.html#collections.Counter.elements
34+
member _.elements() : seq<'T> = nativeOnly
35+
36+
/// Return all elements and their counts, ordered from most common to least common.
37+
/// See https://docs.python.org/3/library/collections.html#collections.Counter.most_common
38+
member _.most_common() : seq<'T * int> = nativeOnly
39+
40+
/// Return the n most common elements and their counts (most common first).
41+
/// See https://docs.python.org/3/library/collections.html#collections.Counter.most_common
42+
member _.most_common(n: int) : seq<'T * int> = nativeOnly
43+
44+
/// Return the total of all counts (requires Python 3.10+).
45+
/// See https://docs.python.org/3/library/collections.html#collections.Counter.total
46+
member _.total() : int = nativeOnly
47+
48+
/// Add counts from the iterable; count becomes sum of old and new counts.
49+
/// See https://docs.python.org/3/library/collections.html#collections.Counter.update
50+
member _.update(iterable: 'T seq) : unit = nativeOnly
51+
52+
/// Subtract counts from the iterable; count becomes difference. Counts can become negative.
53+
/// See https://docs.python.org/3/library/collections.html#collections.Counter.subtract
54+
member _.subtract(iterable: 'T seq) : unit = nativeOnly
55+
56+
/// Remove and return the count for key, or raise KeyError if missing.
57+
member _.pop(key: 'T) : int = nativeOnly
58+
59+
/// Remove and return the count for key, or return defaultValue if missing.
60+
[<Emit("$0.pop($1, $2)")>]
61+
member _.pop(key: 'T, defaultValue: int) : int = nativeOnly
62+
63+
/// Remove all items
64+
member _.clear() : unit = nativeOnly
65+
66+
/// Check if a key is present in the counter
67+
[<Emit("$1 in $0")>]
68+
member _.contains(key: 'T) : bool = nativeOnly
69+
70+
/// Return a Counter from a sequence of elements.
71+
/// See https://docs.python.org/3/library/collections.html#collections.Counter
72+
[<Emit("Counter($0)")>]
73+
static member ofSeq(iterable: 'T seq) : Counter<'T> = nativeOnly
74+
75+
// ============================================================================
76+
// defaultdict
77+
// ============================================================================
78+
79+
/// A dict subclass that calls a factory to supply missing values.
80+
/// When a key is not found, the factory function (called with no arguments)
81+
/// is called to produce a new value, which is then stored and returned.
82+
///
83+
/// Use the `withFactory` static method to attach a factory; the empty
84+
/// constructor produces a defaultdict with no factory (missing keys raise KeyError).
85+
/// See https://docs.python.org/3/library/collections.html#collections.defaultdict
86+
[<Import("defaultdict", "collections")>]
87+
type defaultdict<'TKey, 'TValue>() =
88+
/// Create a defaultdict with the given factory for missing keys.
89+
/// The factory is invoked with no arguments and must return a new value of type 'TValue.
90+
[<Emit("defaultdict($0)")>]
91+
static member withFactory(defaultFactory: unit -> 'TValue) : defaultdict<'TKey, 'TValue> = nativeOnly
92+
93+
/// Get or set the value for key; missing keys invoke the factory
94+
[<Emit("$0[$1]")>]
95+
member _.Item(key: 'TKey) : 'TValue = nativeOnly
96+
97+
/// Set value for key
98+
[<Emit("$0[$1] = $2")>]
99+
member _.set(key: 'TKey, value: 'TValue) : unit = nativeOnly
100+
101+
/// Return key-value pairs
102+
member _.items() : seq<'TKey * 'TValue> = nativeOnly
103+
104+
/// Return keys
105+
member _.keys() : seq<'TKey> = nativeOnly
106+
107+
/// Return values
108+
member _.values() : seq<'TValue> = nativeOnly
109+
110+
/// Return value for key if present, otherwise None.
111+
/// Does NOT invoke the factory.
112+
member _.get(key: 'TKey) : 'TValue option = nativeOnly
113+
114+
/// Return value for key if present, otherwise defaultValue.
115+
/// Does NOT invoke the factory.
116+
[<Emit("$0.get($1, $2)")>]
117+
member _.get(key: 'TKey, defaultValue: 'TValue) : 'TValue = nativeOnly
118+
119+
/// If key is in the dict, return its value.
120+
/// If not, insert key with the factory's value and return that value.
121+
member _.setdefault(key: 'TKey) : 'TValue = nativeOnly
122+
123+
/// Remove and return the value for key, or raise KeyError.
124+
member _.pop(key: 'TKey) : 'TValue = nativeOnly
125+
126+
/// Remove and return the value for key, or return defaultValue.
127+
[<Emit("$0.pop($1, $2)")>]
128+
member _.pop(key: 'TKey, defaultValue: 'TValue) : 'TValue = nativeOnly
129+
130+
/// Merge another dict into this one
131+
member _.update(other: System.Collections.Generic.IDictionary<'TKey, 'TValue>) : unit = nativeOnly
132+
133+
/// Merge an iterable of key-value pairs into this dict
134+
member _.update(items: seq<'TKey * 'TValue>) : unit = nativeOnly
135+
136+
/// Remove all items
137+
member _.clear() : unit = nativeOnly
138+
139+
/// Return a shallow copy
140+
member _.copy() : defaultdict<'TKey, 'TValue> = nativeOnly
141+
142+
/// Check if a key is present (does NOT invoke factory)
143+
[<Emit("$1 in $0")>]
144+
member _.contains(key: 'TKey) : bool = nativeOnly
145+
146+
// ============================================================================
147+
// deque
148+
// ============================================================================
149+
150+
/// A double-ended queue with O(1) appends and pops from either end.
151+
/// If maxlen is set, the deque is bounded to that maximum length; items are
152+
/// discarded from the opposite end when the bound is reached.
153+
/// See https://docs.python.org/3/library/collections.html#collections.deque
154+
[<Import("deque", "collections")>]
155+
type deque<'T>() =
156+
/// Number of elements in the deque
157+
[<Emit("len($0)")>]
158+
member _.length() : int = nativeOnly
159+
160+
/// Get element at index
161+
[<Emit("$0[$1]")>]
162+
member _.Item(index: int) : 'T = nativeOnly
163+
164+
/// Maximum length of the deque, or None if unbounded
165+
member _.maxlen : int option = nativeOnly
166+
167+
/// Add item to the right end
168+
member _.append(item: 'T) : unit = nativeOnly
169+
170+
/// Add item to the left end
171+
member _.appendleft(item: 'T) : unit = nativeOnly
172+
173+
/// Remove and return item from the right end
174+
member _.pop() : 'T = nativeOnly
175+
176+
/// Remove and return item from the left end
177+
member _.popleft() : 'T = nativeOnly
178+
179+
/// Extend the right side of the deque by appending elements from iterable
180+
member _.extend(iterable: 'T seq) : unit = nativeOnly
181+
182+
/// Extend the left side of the deque by appending elements from iterable.
183+
/// Note: each element is appended to the left, reversing the iterable order.
184+
member _.extendleft(iterable: 'T seq) : unit = nativeOnly
185+
186+
/// Rotate the deque n steps to the right. If n is negative, rotate left.
187+
member _.rotate(n: int) : unit = nativeOnly
188+
189+
/// Count the number of occurrences of value
190+
member _.count(value: 'T) : int = nativeOnly
191+
192+
/// Return the position of value (raise ValueError if not found)
193+
member _.index(value: 'T) : int = nativeOnly
194+
195+
/// Insert value before position i
196+
member _.insert(i: int, value: 'T) : unit = nativeOnly
197+
198+
/// Remove the first occurrence of value (raise ValueError if not found)
199+
member _.remove(value: 'T) : unit = nativeOnly
200+
201+
/// Reverse the deque in-place
202+
member _.reverse() : unit = nativeOnly
203+
204+
/// Remove all elements
205+
member _.clear() : unit = nativeOnly
206+
207+
/// Return a shallow copy
208+
member _.copy() : deque<'T> = nativeOnly
209+
210+
/// Create a deque from a sequence
211+
[<Emit("deque($0)")>]
212+
static member ofSeq(iterable: 'T seq) : deque<'T> = nativeOnly
213+
214+
/// Create a bounded deque from a sequence with maximum length
215+
[<Emit("deque($0, maxlen=int($1))")>]
216+
static member ofSeq(iterable: 'T seq, maxlen: int) : deque<'T> = nativeOnly
217+
218+
/// Create an empty bounded deque with maximum length
219+
[<Emit("deque(maxlen=int($0))")>]
220+
static member withMaxlen(maxlen: int) : deque<'T> = nativeOnly
221+
222+
// ============================================================================
223+
// OrderedDict
224+
// ============================================================================
225+
226+
/// A dict subclass that remembers insertion order. Since Python 3.7, all dicts
227+
/// maintain insertion order, but OrderedDict has a few extra features:
228+
/// `move_to_end` and order-sensitive equality.
229+
/// See https://docs.python.org/3/library/collections.html#collections.OrderedDict
230+
[<Import("OrderedDict", "collections")>]
231+
type OrderedDict<'TKey, 'TValue>() =
232+
/// Get or set value for key
233+
[<Emit("$0[$1]")>]
234+
member _.Item(key: 'TKey) : 'TValue = nativeOnly
235+
236+
/// Set value for key
237+
[<Emit("$0[$1] = $2")>]
238+
member _.set(key: 'TKey, value: 'TValue) : unit = nativeOnly
239+
240+
/// Return key-value pairs in insertion order
241+
member _.items() : seq<'TKey * 'TValue> = nativeOnly
242+
243+
/// Return keys in insertion order
244+
member _.keys() : seq<'TKey> = nativeOnly
245+
246+
/// Return values in insertion order
247+
member _.values() : seq<'TValue> = nativeOnly
248+
249+
/// Get value for key, or None if missing
250+
member _.get(key: 'TKey) : 'TValue option = nativeOnly
251+
252+
/// Get value for key, or defaultValue if missing
253+
[<Emit("$0.get($1, $2)")>]
254+
member _.get(key: 'TKey, defaultValue: 'TValue) : 'TValue = nativeOnly
255+
256+
/// Remove and return the value for key (or raise KeyError)
257+
member _.pop(key: 'TKey) : 'TValue = nativeOnly
258+
259+
/// Remove and return the value for key, or return defaultValue
260+
[<Emit("$0.pop($1, $2)")>]
261+
member _.pop(key: 'TKey, defaultValue: 'TValue) : 'TValue = nativeOnly
262+
263+
/// Move key to the end. If last is False, move to the beginning.
264+
/// See https://docs.python.org/3/library/collections.html#collections.OrderedDict.move_to_end
265+
member _.move_to_end(key: 'TKey) : unit = nativeOnly
266+
267+
/// Move key to the end (last=True) or beginning (last=False).
268+
[<Emit("$0.move_to_end($1, last=$2)")>]
269+
member _.move_to_end(key: 'TKey, last: bool) : unit = nativeOnly
270+
271+
/// Remove and return a (key, value) pair. last=True removes from the end.
272+
/// See https://docs.python.org/3/library/collections.html#collections.OrderedDict.popitem
273+
member _.popitem() : 'TKey * 'TValue = nativeOnly
274+
275+
/// Remove and return from end (last=True) or beginning (last=False).
276+
[<Emit("$0.popitem(last=$1)")>]
277+
member _.popitem(last: bool) : 'TKey * 'TValue = nativeOnly
278+
279+
/// Merge another dict into this one
280+
member _.update(other: System.Collections.Generic.IDictionary<'TKey, 'TValue>) : unit = nativeOnly
281+
282+
/// Merge an iterable of key-value pairs into this dict
283+
member _.update(items: seq<'TKey * 'TValue>) : unit = nativeOnly
284+
285+
/// Remove all items
286+
member _.clear() : unit = nativeOnly
287+
288+
/// Return a shallow copy
289+
member _.copy() : OrderedDict<'TKey, 'TValue> = nativeOnly
290+
291+
/// Check if key is present
292+
[<Emit("$1 in $0")>]
293+
member _.contains(key: 'TKey) : bool = nativeOnly

test/Fable.Python.Test.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<Compile Include="TestBuiltins.fs" />
1818
<Compile Include="TestBuiltinsAttr.fs" />
1919
<Compile Include="TestHeapq.fs" />
20+
<Compile Include="TestCollections.fs" />
2021
<Compile Include="TestItertools.fs" />
2122
<Compile Include="TestFunctools.fs" />
2223
<Compile Include="TestQueue.fs" />

0 commit comments

Comments
 (0)