From cbc296a419fda831478e26ce99f4072886f1228c Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 22 Apr 2026 04:03:02 +0000
Subject: [PATCH 1/2] 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>
---
src/Fable.Python.fsproj | 1 +
src/stdlib/Collections.fs | 264 ++++++++++++++++++++++++++++++++++
test/Fable.Python.Test.fsproj | 1 +
test/TestCollections.fs | 231 +++++++++++++++++++++++++++++
4 files changed, 497 insertions(+)
create mode 100644 src/stdlib/Collections.fs
create mode 100644 test/TestCollections.fs
diff --git a/src/Fable.Python.fsproj b/src/Fable.Python.fsproj
index 3dff498..fc91a6f 100644
--- a/src/Fable.Python.fsproj
+++ b/src/Fable.Python.fsproj
@@ -23,6 +23,7 @@
+
diff --git a/src/stdlib/Collections.fs b/src/stdlib/Collections.fs
new file mode 100644
index 0000000..d5d61e6
--- /dev/null
+++ b/src/stdlib/Collections.fs
@@ -0,0 +1,264 @@
+/// Type bindings for Python collections module: https://docs.python.org/3/library/collections.html
+module Fable.Python.Collections
+
+open Fable.Core
+
+// fsharplint:disable MemberNames
+
+// ============================================================================
+// Counter
+// ============================================================================
+
+/// A dict subclass for counting hashable objects.
+/// Elements are stored as dictionary keys and their counts are stored as values.
+/// Counts are allowed to be any integer value including zero or negative counts.
+/// See https://docs.python.org/3/library/collections.html#collections.Counter
+[]
+type Counter<'T>() =
+ /// Get the count for key; missing keys return 0 (unlike a regular dict)
+ []
+ member _.Item(key: 'T) : int = nativeOnly
+
+ /// Return elements and their counts as key-value pairs
+ member _.items() : seq<'T * int> = nativeOnly
+
+ /// Return an iterator over elements, repeating each as many times as its count.
+ /// Elements with counts <= 0 are not included.
+ /// See https://docs.python.org/3/library/collections.html#collections.Counter.elements
+ member _.elements() : seq<'T> = nativeOnly
+
+ /// Return the n most common elements and their counts (most common first).
+ /// If n is omitted, return all elements in counter order.
+ /// See https://docs.python.org/3/library/collections.html#collections.Counter.most_common
+ member _.most_common() : seq<'T * int> = nativeOnly
+
+ /// Return the n most common elements and their counts (most common first).
+ /// See https://docs.python.org/3/library/collections.html#collections.Counter.most_common
+ []
+ member _.most_common(n: int) : seq<'T * int> = nativeOnly
+
+ /// Return the total of all counts (requires Python 3.10+).
+ /// See https://docs.python.org/3/library/collections.html#collections.Counter.total
+ member _.total() : int = nativeOnly
+
+ /// Add counts from the iterable or mapping; count becomes sum of old and new counts.
+ /// See https://docs.python.org/3/library/collections.html#collections.Counter.update
+ member _.update(iterable: 'T seq) : unit = nativeOnly
+
+ /// Subtract counts from the iterable or mapping; count becomes difference.
+ /// Counts can become negative.
+ /// See https://docs.python.org/3/library/collections.html#collections.Counter.subtract
+ member _.subtract(iterable: 'T seq) : unit = nativeOnly
+
+ /// Return a Counter from a sequence of elements.
+ /// See https://docs.python.org/3/library/collections.html#collections.Counter
+ []
+ static member ofSeq(iterable: 'T seq) : Counter<'T> = nativeOnly
+
+// ============================================================================
+// defaultdict
+// ============================================================================
+
+/// A dict subclass that calls a factory to supply missing values.
+/// When a key is not found, the factory function (called with no arguments)
+/// is called to produce a new value, which is then stored and returned.
+/// If the factory is not set (None), missing keys raise KeyError as normal.
+/// See https://docs.python.org/3/library/collections.html#collections.defaultdict
+[]
+type defaultdict<'TKey, 'TValue>(defaultFactory: unit -> 'TValue) =
+ /// Get or set the value for key; missing keys invoke the factory
+ []
+ member _.Item(key: 'TKey) : 'TValue = nativeOnly
+
+ /// Set value for key
+ []
+ member _.set(key: 'TKey, value: 'TValue) : unit = nativeOnly
+
+ /// Return key-value pairs
+ member _.items() : seq<'TKey * 'TValue> = nativeOnly
+
+ /// Return keys
+ member _.keys() : seq<'TKey> = nativeOnly
+
+ /// Return values
+ member _.values() : seq<'TValue> = nativeOnly
+
+ /// Return value for key if present, otherwise None.
+ /// Does NOT invoke the factory.
+ member _.get(key: 'TKey) : 'TValue option = nativeOnly
+
+ /// Return value for key if present, otherwise defaultValue.
+ /// Does NOT invoke the factory.
+ []
+ member _.get(key: 'TKey, defaultValue: 'TValue) : 'TValue = nativeOnly
+
+ /// If key is in the dict, return its value.
+ /// If not, insert key with the factory's value and return that value.
+ member _.setdefault(key: 'TKey) : 'TValue = nativeOnly
+
+ /// Remove and return the value for key, or raise KeyError.
+ member _.pop(key: 'TKey) : 'TValue = nativeOnly
+
+ /// Remove and return the value for key, or return defaultValue.
+ []
+ member _.pop(key: 'TKey, defaultValue: 'TValue) : 'TValue = nativeOnly
+
+ /// Merge another dict into this one
+ member _.update(other: System.Collections.Generic.IDictionary<'TKey, 'TValue>) : unit = nativeOnly
+
+ /// Remove all items
+ member _.clear() : unit = nativeOnly
+
+ /// Return a shallow copy
+ member _.copy() : defaultdict<'TKey, 'TValue> = nativeOnly
+
+ /// Check if a key is present (does NOT invoke factory)
+ []
+ member _.contains(key: 'TKey) : bool = nativeOnly
+
+// ============================================================================
+// deque
+// ============================================================================
+
+/// A double-ended queue with O(1) appends and pops from either end.
+/// If maxlen is set, the deque is bounded to that maximum length; items are
+/// discarded from the opposite end when the bound is reached.
+/// See https://docs.python.org/3/library/collections.html#collections.deque
+[]
+type deque<'T>() =
+ /// Number of elements in the deque
+ []
+ member _.length() : int = nativeOnly
+
+ /// Get element at index
+ []
+ member _.Item(index: int) : 'T = nativeOnly
+
+ /// Maximum length of the deque, or None if unbounded
+ member _.maxlen : int option = nativeOnly
+
+ /// Add item to the right end
+ member _.append(item: 'T) : unit = nativeOnly
+
+ /// Add item to the left end
+ member _.appendleft(item: 'T) : unit = nativeOnly
+
+ /// Remove and return item from the right end
+ member _.pop() : 'T = nativeOnly
+
+ /// Remove and return item from the left end
+ member _.popleft() : 'T = nativeOnly
+
+ /// Extend the right side of the deque by appending elements from iterable
+ member _.extend(iterable: 'T seq) : unit = nativeOnly
+
+ /// Extend the left side of the deque by appending elements from iterable.
+ /// Note: each element is appended to the left, reversing the iterable order.
+ member _.extendleft(iterable: 'T seq) : unit = nativeOnly
+
+ /// Rotate the deque n steps to the right. If n is negative, rotate left.
+ member _.rotate(n: int) : unit = nativeOnly
+
+ /// Count the number of occurrences of value
+ []
+ member _.count(value: 'T) : int = nativeOnly
+
+ /// Return the position of value (raise ValueError if not found)
+ member _.index(value: 'T) : int = nativeOnly
+
+ /// Insert value before position i
+ member _.insert(i: int, value: 'T) : unit = nativeOnly
+
+ /// Remove the first occurrence of value (raise ValueError if not found)
+ member _.remove(value: 'T) : unit = nativeOnly
+
+ /// Reverse the deque in-place
+ member _.reverse() : unit = nativeOnly
+
+ /// Remove all elements
+ member _.clear() : unit = nativeOnly
+
+ /// Return a shallow copy
+ member _.copy() : deque<'T> = nativeOnly
+
+ /// Create a deque from a sequence
+ []
+ static member ofSeq(iterable: 'T seq) : deque<'T> = nativeOnly
+
+ /// Create a bounded deque from a sequence with maximum length
+ []
+ static member ofSeq(iterable: 'T seq, maxlen: int) : deque<'T> = nativeOnly
+
+ /// Create an empty bounded deque with maximum length
+ []
+ static member withMaxlen(maxlen: int) : deque<'T> = nativeOnly
+
+// ============================================================================
+// OrderedDict
+// ============================================================================
+
+/// A dict subclass that remembers insertion order. Since Python 3.7, all dicts
+/// maintain insertion order, but OrderedDict has a few extra features:
+/// `move_to_end` and order-sensitive equality.
+/// See https://docs.python.org/3/library/collections.html#collections.OrderedDict
+[]
+type OrderedDict<'TKey, 'TValue>() =
+ /// Get or set value for key
+ []
+ member _.Item(key: 'TKey) : 'TValue = nativeOnly
+
+ /// Set value for key
+ []
+ member _.set(key: 'TKey, value: 'TValue) : unit = nativeOnly
+
+ /// Return key-value pairs in insertion order
+ member _.items() : seq<'TKey * 'TValue> = nativeOnly
+
+ /// Return keys in insertion order
+ member _.keys() : seq<'TKey> = nativeOnly
+
+ /// Return values in insertion order
+ member _.values() : seq<'TValue> = nativeOnly
+
+ /// Get value for key, or None if missing
+ member _.get(key: 'TKey) : 'TValue option = nativeOnly
+
+ /// Get value for key, or defaultValue if missing
+ []
+ member _.get(key: 'TKey, defaultValue: 'TValue) : 'TValue = nativeOnly
+
+ /// Remove and return the value for key (or raise KeyError)
+ member _.pop(key: 'TKey) : 'TValue = nativeOnly
+
+ /// Remove and return the value for key, or return defaultValue
+ []
+ member _.pop(key: 'TKey, defaultValue: 'TValue) : 'TValue = nativeOnly
+
+ /// Move key to the end. If last is False, move to the beginning.
+ /// See https://docs.python.org/3/library/collections.html#collections.OrderedDict.move_to_end
+ member _.move_to_end(key: 'TKey) : unit = nativeOnly
+
+ /// Move key to the end (last=True) or beginning (last=False).
+ []
+ member _.move_to_end(key: 'TKey, last: bool) : unit = nativeOnly
+
+ /// Remove and return a (key, value) pair. last=True removes from the end.
+ /// See https://docs.python.org/3/library/collections.html#collections.OrderedDict.popitem
+ member _.popitem() : 'TKey * 'TValue = nativeOnly
+
+ /// Remove and return from end (last=True) or beginning (last=False).
+ []
+ member _.popitem(last: bool) : 'TKey * 'TValue = nativeOnly
+
+ /// Merge another dict into this one
+ member _.update(other: System.Collections.Generic.IDictionary<'TKey, 'TValue>) : unit = nativeOnly
+
+ /// Remove all items
+ member _.clear() : unit = nativeOnly
+
+ /// Return a shallow copy
+ member _.copy() : OrderedDict<'TKey, 'TValue> = nativeOnly
+
+ /// Check if key is present
+ []
+ member _.contains(key: 'TKey) : bool = nativeOnly
diff --git a/test/Fable.Python.Test.fsproj b/test/Fable.Python.Test.fsproj
index 441f8a7..69c1f74 100644
--- a/test/Fable.Python.Test.fsproj
+++ b/test/Fable.Python.Test.fsproj
@@ -17,6 +17,7 @@
+
diff --git a/test/TestCollections.fs b/test/TestCollections.fs
new file mode 100644
index 0000000..931e48c
--- /dev/null
+++ b/test/TestCollections.fs
@@ -0,0 +1,231 @@
+module Fable.Python.Tests.Collections
+
+open Fable.Python.Testing
+open Fable.Python.Collections
+
+// ============================================================================
+// Counter tests
+// ============================================================================
+
+[]
+let ``Counter: empty counter has zero count for missing key`` () =
+ let c = Counter()
+ c.Item("x") |> equal 0
+
+[]
+let ``Counter: ofSeq counts elements`` () =
+ let c = Counter.ofSeq [ "a"; "b"; "a"; "c"; "a"; "b" ]
+ c.Item("a") |> equal 3
+ c.Item("b") |> equal 2
+ c.Item("c") |> equal 1
+
+[]
+let ``Counter: missing key returns 0`` () =
+ let c = Counter.ofSeq [ "a"; "b" ]
+ c.Item("z") |> equal 0
+
+[]
+let ``Counter: most_common returns all elements sorted by count`` () =
+ let c = Counter.ofSeq [ "a"; "b"; "a"; "c"; "a"; "b" ]
+ let top = c.most_common() |> Seq.head
+ top |> equal ("a", 3)
+
+[]
+let ``Counter: most_common n returns top n elements`` () =
+ let c = Counter.ofSeq [ "a"; "b"; "a"; "c"; "a"; "b" ]
+ let topTwo = c.most_common(2) |> Seq.toList
+ topTwo |> List.length |> equal 2
+ topTwo |> List.head |> equal ("a", 3)
+
+[]
+let ``Counter: elements returns repeated sequence`` () =
+ let c = Counter.ofSeq [ "a"; "a"; "b" ]
+ let elems = c.elements() |> Seq.toList |> List.sort
+ elems |> equal [ "a"; "a"; "b" ]
+
+[]
+let ``Counter: total sums all counts`` () =
+ let c = Counter.ofSeq [ "a"; "b"; "a"; "c" ]
+ c.total() |> equal 4
+
+[]
+let ``Counter: update adds counts`` () =
+ let c = Counter.ofSeq [ "a"; "b" ]
+ c.update([ "a"; "c" ])
+ c.Item("a") |> equal 2
+ c.Item("c") |> equal 1
+
+[]
+let ``Counter: subtract reduces counts`` () =
+ let c = Counter.ofSeq [ "a"; "a"; "b" ]
+ c.subtract([ "a" ])
+ c.Item("a") |> equal 1
+
+// ============================================================================
+// defaultdict tests
+// ============================================================================
+
+[]
+let ``defaultdict: missing key invokes factory`` () =
+ let d = defaultdict>(fun () -> ResizeArray())
+ let list = d.Item("key")
+ list.Count |> equal 0
+
+[]
+let ``defaultdict: factory creates separate instances`` () =
+ let d = defaultdict>(fun () -> ResizeArray())
+ let list1 = d.Item("a")
+ list1.Add(1)
+ let list2 = d.Item("b")
+ list2.Count |> equal 0
+
+[]
+let ``defaultdict: int factory starts at zero`` () =
+ let d = defaultdict(fun () -> 0)
+ d.Item("key") |> equal 0
+
+[]
+let ``defaultdict: get returns None for missing key without invoking factory`` () =
+ let mutable factoryCalled = false
+ let d = defaultdict(fun () -> factoryCalled <- true; 0)
+ let result = d.get("missing")
+ result |> equal None
+ factoryCalled |> equal false
+
+[]
+let ``defaultdict: get with default returns default for missing key`` () =
+ let d = defaultdict(fun () -> 0)
+ d.get("missing", 42) |> equal 42
+
+[]
+let ``defaultdict: contains returns false for missing key`` () =
+ let d = defaultdict(fun () -> 0)
+ d.contains("key") |> equal false
+
+[]
+let ``defaultdict: contains returns true after access`` () =
+ let d = defaultdict(fun () -> 99)
+ let _ = d.Item("key")
+ d.contains("key") |> equal true
+
+// ============================================================================
+// deque tests
+// ============================================================================
+
+[]
+let ``deque: empty deque has length 0`` () =
+ let d = deque()
+ d.length() |> equal 0
+
+[]
+let ``deque: ofSeq creates deque from sequence`` () =
+ let d = deque.ofSeq [ 1; 2; 3 ]
+ d.length() |> equal 3
+
+[]
+let ``deque: append adds to right`` () =
+ let d = deque.ofSeq [ 1; 2 ]
+ d.append(3)
+ d.Item(2) |> equal 3
+
+[]
+let ``deque: appendleft adds to left`` () =
+ let d = deque.ofSeq [ 1; 2 ]
+ d.appendleft(0)
+ d.Item(0) |> equal 0
+ d.length() |> equal 3
+
+[]
+let ``deque: pop removes from right`` () =
+ let d = deque.ofSeq [ 1; 2; 3 ]
+ let v = d.pop()
+ v |> equal 3
+ d.length() |> equal 2
+
+[]
+let ``deque: popleft removes from left`` () =
+ let d = deque.ofSeq [ 1; 2; 3 ]
+ let v = d.popleft()
+ v |> equal 1
+ d.length() |> equal 2
+
+[]
+let ``deque: rotate shifts elements right`` () =
+ let d = deque.ofSeq [ 1; 2; 3; 4; 5 ]
+ d.rotate(2)
+ d.Item(0) |> equal 4
+ d.Item(1) |> equal 5
+
+[]
+let ``deque: maxlen is None for unbounded deque`` () =
+ let d = deque.ofSeq [ 1; 2; 3 ]
+ d.maxlen |> equal None
+
+[]
+let ``deque: withMaxlen creates bounded deque`` () =
+ let d = deque.withMaxlen(3)
+ d.append(1)
+ d.append(2)
+ d.append(3)
+ d.append(4) // should push out 1
+ d.length() |> equal 3
+ d.Item(0) |> equal 2
+
+[]
+let ``deque: ofSeq with maxlen creates bounded deque`` () =
+ let d = deque.ofSeq ([ 1; 2; 3; 4; 5 ], 3)
+ d.length() |> equal 3
+ d.maxlen |> equal (Some 3)
+
+[]
+let ``deque: count occurrences`` () =
+ let d = deque.ofSeq [ 1; 2; 1; 3; 1 ]
+ d.count(1) |> equal 3
+
+// ============================================================================
+// OrderedDict tests
+// ============================================================================
+
+[]
+let ``OrderedDict: preserves insertion order`` () =
+ let od = OrderedDict()
+ od.set("a", 1)
+ od.set("b", 2)
+ od.set("c", 3)
+ od.keys() |> Seq.toList |> equal [ "a"; "b"; "c" ]
+
+[]
+let ``OrderedDict: get existing key`` () =
+ let od = OrderedDict()
+ od.set("x", 42)
+ od.Item("x") |> equal 42
+
+[]
+let ``OrderedDict: get returns None for missing key`` () =
+ let od = OrderedDict()
+ od.get("missing") |> equal None
+
+[]
+let ``OrderedDict: move_to_end moves last element`` () =
+ let od = OrderedDict()
+ od.set("a", 1)
+ od.set("b", 2)
+ od.set("c", 3)
+ od.move_to_end("a")
+ od.keys() |> Seq.toList |> equal [ "b"; "c"; "a" ]
+
+[]
+let ``OrderedDict: move_to_end with last false moves to front`` () =
+ let od = OrderedDict()
+ od.set("a", 1)
+ od.set("b", 2)
+ od.set("c", 3)
+ od.move_to_end("c", false)
+ od.keys() |> Seq.toList |> equal [ "c"; "a"; "b" ]
+
+[]
+let ``OrderedDict: contains returns correct result`` () =
+ let od = OrderedDict()
+ od.set("a", 1)
+ od.contains("a") |> equal true
+ od.contains("b") |> equal false
From 8cbb159baa2f31db4d9ab1e0e672fad7710490a3 Mon Sep 17 00:00:00 2001
From: Dag Brattli
Date: Sun, 26 Apr 2026 10:03:04 +0200
Subject: [PATCH 2/2] 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 `[]` 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)
---
src/stdlib/Collections.fs | 51 ++++++++++++++----
test/TestCollections.fs | 107 ++++++++++++++++++++++++--------------
2 files changed, 107 insertions(+), 51 deletions(-)
diff --git a/src/stdlib/Collections.fs b/src/stdlib/Collections.fs
index d5d61e6..8727336 100644
--- a/src/stdlib/Collections.fs
+++ b/src/stdlib/Collections.fs
@@ -22,34 +22,51 @@ type Counter<'T>() =
/// Return elements and their counts as key-value pairs
member _.items() : seq<'T * int> = nativeOnly
+ /// Return the elements (keys) of the counter
+ member _.keys() : seq<'T> = nativeOnly
+
+ /// Return the counts (values) of the counter
+ member _.values() : seq = nativeOnly
+
/// Return an iterator over elements, repeating each as many times as its count.
/// Elements with counts <= 0 are not included.
/// See https://docs.python.org/3/library/collections.html#collections.Counter.elements
member _.elements() : seq<'T> = nativeOnly
- /// Return the n most common elements and their counts (most common first).
- /// If n is omitted, return all elements in counter order.
+ /// Return all elements and their counts, ordered from most common to least common.
/// See https://docs.python.org/3/library/collections.html#collections.Counter.most_common
member _.most_common() : seq<'T * int> = nativeOnly
/// Return the n most common elements and their counts (most common first).
/// See https://docs.python.org/3/library/collections.html#collections.Counter.most_common
- []
member _.most_common(n: int) : seq<'T * int> = nativeOnly
/// Return the total of all counts (requires Python 3.10+).
/// See https://docs.python.org/3/library/collections.html#collections.Counter.total
member _.total() : int = nativeOnly
- /// Add counts from the iterable or mapping; count becomes sum of old and new counts.
+ /// Add counts from the iterable; count becomes sum of old and new counts.
/// See https://docs.python.org/3/library/collections.html#collections.Counter.update
member _.update(iterable: 'T seq) : unit = nativeOnly
- /// Subtract counts from the iterable or mapping; count becomes difference.
- /// Counts can become negative.
+ /// Subtract counts from the iterable; count becomes difference. Counts can become negative.
/// See https://docs.python.org/3/library/collections.html#collections.Counter.subtract
member _.subtract(iterable: 'T seq) : unit = nativeOnly
+ /// Remove and return the count for key, or raise KeyError if missing.
+ member _.pop(key: 'T) : int = nativeOnly
+
+ /// Remove and return the count for key, or return defaultValue if missing.
+ []
+ member _.pop(key: 'T, defaultValue: int) : int = nativeOnly
+
+ /// Remove all items
+ member _.clear() : unit = nativeOnly
+
+ /// Check if a key is present in the counter
+ []
+ member _.contains(key: 'T) : bool = nativeOnly
+
/// Return a Counter from a sequence of elements.
/// See https://docs.python.org/3/library/collections.html#collections.Counter
[]
@@ -62,16 +79,23 @@ type Counter<'T>() =
/// A dict subclass that calls a factory to supply missing values.
/// When a key is not found, the factory function (called with no arguments)
/// is called to produce a new value, which is then stored and returned.
-/// If the factory is not set (None), missing keys raise KeyError as normal.
+///
+/// Use the `withFactory` static method to attach a factory; the empty
+/// constructor produces a defaultdict with no factory (missing keys raise KeyError).
/// See https://docs.python.org/3/library/collections.html#collections.defaultdict
[]
-type defaultdict<'TKey, 'TValue>(defaultFactory: unit -> 'TValue) =
+type defaultdict<'TKey, 'TValue>() =
+ /// Create a defaultdict with the given factory for missing keys.
+ /// The factory is invoked with no arguments and must return a new value of type 'TValue.
+ []
+ static member withFactory(defaultFactory: unit -> 'TValue) : defaultdict<'TKey, 'TValue> = nativeOnly
+
/// Get or set the value for key; missing keys invoke the factory
[]
member _.Item(key: 'TKey) : 'TValue = nativeOnly
/// Set value for key
- []
+ []
member _.set(key: 'TKey, value: 'TValue) : unit = nativeOnly
/// Return key-value pairs
@@ -106,6 +130,9 @@ type defaultdict<'TKey, 'TValue>(defaultFactory: unit -> 'TValue) =
/// Merge another dict into this one
member _.update(other: System.Collections.Generic.IDictionary<'TKey, 'TValue>) : unit = nativeOnly
+ /// Merge an iterable of key-value pairs into this dict
+ member _.update(items: seq<'TKey * 'TValue>) : unit = nativeOnly
+
/// Remove all items
member _.clear() : unit = nativeOnly
@@ -160,7 +187,6 @@ type deque<'T>() =
member _.rotate(n: int) : unit = nativeOnly
/// Count the number of occurrences of value
- []
member _.count(value: 'T) : int = nativeOnly
/// Return the position of value (raise ValueError if not found)
@@ -208,7 +234,7 @@ type OrderedDict<'TKey, 'TValue>() =
member _.Item(key: 'TKey) : 'TValue = nativeOnly
/// Set value for key
- []
+ []
member _.set(key: 'TKey, value: 'TValue) : unit = nativeOnly
/// Return key-value pairs in insertion order
@@ -253,6 +279,9 @@ type OrderedDict<'TKey, 'TValue>() =
/// Merge another dict into this one
member _.update(other: System.Collections.Generic.IDictionary<'TKey, 'TValue>) : unit = nativeOnly
+ /// Merge an iterable of key-value pairs into this dict
+ member _.update(items: seq<'TKey * 'TValue>) : unit = nativeOnly
+
/// Remove all items
member _.clear() : unit = nativeOnly
diff --git a/test/TestCollections.fs b/test/TestCollections.fs
index 931e48c..a47f6c3 100644
--- a/test/TestCollections.fs
+++ b/test/TestCollections.fs
@@ -8,103 +8,121 @@ open Fable.Python.Collections
// ============================================================================
[]
-let ``Counter: empty counter has zero count for missing key`` () =
+let ``test Counter empty counter has zero count for missing key`` () =
let c = Counter()
c.Item("x") |> equal 0
[]
-let ``Counter: ofSeq counts elements`` () =
+let ``test Counter ofSeq counts elements`` () =
let c = Counter.ofSeq [ "a"; "b"; "a"; "c"; "a"; "b" ]
c.Item("a") |> equal 3
c.Item("b") |> equal 2
c.Item("c") |> equal 1
[]
-let ``Counter: missing key returns 0`` () =
+let ``test Counter missing key returns 0`` () =
let c = Counter.ofSeq [ "a"; "b" ]
c.Item("z") |> equal 0
[]
-let ``Counter: most_common returns all elements sorted by count`` () =
+let ``test Counter most_common returns all elements sorted by count`` () =
let c = Counter.ofSeq [ "a"; "b"; "a"; "c"; "a"; "b" ]
let top = c.most_common() |> Seq.head
top |> equal ("a", 3)
[]
-let ``Counter: most_common n returns top n elements`` () =
+let ``test Counter most_common n returns top n elements`` () =
let c = Counter.ofSeq [ "a"; "b"; "a"; "c"; "a"; "b" ]
let topTwo = c.most_common(2) |> Seq.toList
topTwo |> List.length |> equal 2
topTwo |> List.head |> equal ("a", 3)
[]
-let ``Counter: elements returns repeated sequence`` () =
+let ``test Counter elements returns repeated sequence`` () =
let c = Counter.ofSeq [ "a"; "a"; "b" ]
let elems = c.elements() |> Seq.toList |> List.sort
elems |> equal [ "a"; "a"; "b" ]
[]
-let ``Counter: total sums all counts`` () =
+let ``test Counter total sums all counts`` () =
let c = Counter.ofSeq [ "a"; "b"; "a"; "c" ]
c.total() |> equal 4
[]
-let ``Counter: update adds counts`` () =
+let ``test Counter update adds counts`` () =
let c = Counter.ofSeq [ "a"; "b" ]
c.update([ "a"; "c" ])
c.Item("a") |> equal 2
c.Item("c") |> equal 1
[]
-let ``Counter: subtract reduces counts`` () =
+let ``test Counter subtract reduces counts`` () =
let c = Counter.ofSeq [ "a"; "a"; "b" ]
c.subtract([ "a" ])
c.Item("a") |> equal 1
+[]
+let ``test Counter contains reflects key presence`` () =
+ let c = Counter.ofSeq [ "a"; "b" ]
+ c.contains("a") |> equal true
+ c.contains("z") |> equal false
+
+[]
+let ``test Counter keys and values enumerate the counter`` () =
+ let c = Counter.ofSeq [ "a"; "b"; "a" ]
+ c.keys() |> Seq.toList |> List.sort |> equal [ "a"; "b" ]
+ c.values() |> Seq.sum |> equal 3
+
+[]
+let ``test Counter pop removes and returns count`` () =
+ let c = Counter.ofSeq [ "a"; "a"; "b" ]
+ c.pop("a") |> equal 2
+ c.contains("a") |> equal false
+
// ============================================================================
// defaultdict tests
// ============================================================================
[]
-let ``defaultdict: missing key invokes factory`` () =
- let d = defaultdict>(fun () -> ResizeArray())
+let ``test defaultdict missing key invokes factory`` () =
+ let d = defaultdict>.withFactory(fun () -> ResizeArray())
let list = d.Item("key")
list.Count |> equal 0
[]
-let ``defaultdict: factory creates separate instances`` () =
- let d = defaultdict>(fun () -> ResizeArray())
+let ``test defaultdict factory creates separate instances`` () =
+ let d = defaultdict>.withFactory(fun () -> ResizeArray())
let list1 = d.Item("a")
list1.Add(1)
let list2 = d.Item("b")
list2.Count |> equal 0
[]
-let ``defaultdict: int factory starts at zero`` () =
- let d = defaultdict(fun () -> 0)
+let ``test defaultdict int factory starts at zero`` () =
+ let d = defaultdict.withFactory(fun () -> 0)
d.Item("key") |> equal 0
[]
-let ``defaultdict: get returns None for missing key without invoking factory`` () =
+let ``test defaultdict get returns None for missing key without invoking factory`` () =
let mutable factoryCalled = false
- let d = defaultdict(fun () -> factoryCalled <- true; 0)
+ let d = defaultdict.withFactory(fun () -> factoryCalled <- true; 0)
let result = d.get("missing")
result |> equal None
factoryCalled |> equal false
[]
-let ``defaultdict: get with default returns default for missing key`` () =
- let d = defaultdict(fun () -> 0)
+let ``test defaultdict get with default returns default for missing key`` () =
+ let d = defaultdict.withFactory(fun () -> 0)
d.get("missing", 42) |> equal 42
[]
-let ``defaultdict: contains returns false for missing key`` () =
- let d = defaultdict(fun () -> 0)
+let ``test defaultdict contains returns false for missing key`` () =
+ let d = defaultdict.withFactory(fun () -> 0)
d.contains("key") |> equal false
[]
-let ``defaultdict: contains returns true after access`` () =
- let d = defaultdict(fun () -> 99)
+let ``test defaultdict contains returns true after access`` () =
+ let d = defaultdict.withFactory(fun () -> 99)
let _ = d.Item("key")
d.contains("key") |> equal true
@@ -113,56 +131,56 @@ let ``defaultdict: contains returns true after access`` () =
// ============================================================================
[]
-let ``deque: empty deque has length 0`` () =
+let ``test deque empty deque has length 0`` () =
let d = deque()
d.length() |> equal 0
[]
-let ``deque: ofSeq creates deque from sequence`` () =
+let ``test deque ofSeq creates deque from sequence`` () =
let d = deque.ofSeq [ 1; 2; 3 ]
d.length() |> equal 3
[]
-let ``deque: append adds to right`` () =
+let ``test deque append adds to right`` () =
let d = deque.ofSeq [ 1; 2 ]
d.append(3)
d.Item(2) |> equal 3
[]
-let ``deque: appendleft adds to left`` () =
+let ``test deque appendleft adds to left`` () =
let d = deque.ofSeq [ 1; 2 ]
d.appendleft(0)
d.Item(0) |> equal 0
d.length() |> equal 3
[]
-let ``deque: pop removes from right`` () =
+let ``test deque pop removes from right`` () =
let d = deque.ofSeq [ 1; 2; 3 ]
let v = d.pop()
v |> equal 3
d.length() |> equal 2
[]
-let ``deque: popleft removes from left`` () =
+let ``test deque popleft removes from left`` () =
let d = deque.ofSeq [ 1; 2; 3 ]
let v = d.popleft()
v |> equal 1
d.length() |> equal 2
[]
-let ``deque: rotate shifts elements right`` () =
+let ``test deque rotate shifts elements right`` () =
let d = deque.ofSeq [ 1; 2; 3; 4; 5 ]
d.rotate(2)
d.Item(0) |> equal 4
d.Item(1) |> equal 5
[]
-let ``deque: maxlen is None for unbounded deque`` () =
+let ``test deque maxlen is None for unbounded deque`` () =
let d = deque.ofSeq [ 1; 2; 3 ]
d.maxlen |> equal None
[]
-let ``deque: withMaxlen creates bounded deque`` () =
+let ``test deque withMaxlen creates bounded deque`` () =
let d = deque.withMaxlen(3)
d.append(1)
d.append(2)
@@ -172,22 +190,31 @@ let ``deque: withMaxlen creates bounded deque`` () =
d.Item(0) |> equal 2
[]
-let ``deque: ofSeq with maxlen creates bounded deque`` () =
+let ``test deque ofSeq with maxlen creates bounded deque`` () =
let d = deque.ofSeq ([ 1; 2; 3; 4; 5 ], 3)
d.length() |> equal 3
d.maxlen |> equal (Some 3)
[]
-let ``deque: count occurrences`` () =
+let ``test deque count occurrences`` () =
let d = deque.ofSeq [ 1; 2; 1; 3; 1 ]
d.count(1) |> equal 3
+[]
+let ``test deque extendleft reverses iterable order`` () =
+ let d = deque.ofSeq [ 3 ]
+ d.extendleft([ 1; 2 ])
+ // Each element pushed onto the left in turn => final order [2; 1; 3]
+ d.Item(0) |> equal 2
+ d.Item(1) |> equal 1
+ d.Item(2) |> equal 3
+
// ============================================================================
// OrderedDict tests
// ============================================================================
[]
-let ``OrderedDict: preserves insertion order`` () =
+let ``test OrderedDict preserves insertion order`` () =
let od = OrderedDict()
od.set("a", 1)
od.set("b", 2)
@@ -195,18 +222,18 @@ let ``OrderedDict: preserves insertion order`` () =
od.keys() |> Seq.toList |> equal [ "a"; "b"; "c" ]
[]
-let ``OrderedDict: get existing key`` () =
+let ``test OrderedDict get existing key`` () =
let od = OrderedDict()
od.set("x", 42)
od.Item("x") |> equal 42
[]
-let ``OrderedDict: get returns None for missing key`` () =
+let ``test OrderedDict get returns None for missing key`` () =
let od = OrderedDict()
od.get("missing") |> equal None
[]
-let ``OrderedDict: move_to_end moves last element`` () =
+let ``test OrderedDict move_to_end moves last element`` () =
let od = OrderedDict()
od.set("a", 1)
od.set("b", 2)
@@ -215,7 +242,7 @@ let ``OrderedDict: move_to_end moves last element`` () =
od.keys() |> Seq.toList |> equal [ "b"; "c"; "a" ]
[]
-let ``OrderedDict: move_to_end with last false moves to front`` () =
+let ``test OrderedDict move_to_end with last false moves to front`` () =
let od = OrderedDict()
od.set("a", 1)
od.set("b", 2)
@@ -224,7 +251,7 @@ let ``OrderedDict: move_to_end with last false moves to front`` () =
od.keys() |> Seq.toList |> equal [ "c"; "a"; "b" ]
[]
-let ``OrderedDict: contains returns correct result`` () =
+let ``test OrderedDict contains returns correct result`` () =
let od = OrderedDict()
od.set("a", 1)
od.contains("a") |> equal true