From 5c77044886a338fb409c17491f6636b11ef8b84e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 04:06:43 +0000 Subject: [PATCH 1/2] feat(stdlib): add itertools module bindings Add comprehensive F# bindings for Python's itertools module including: - Infinite iterators: count, cycle, repeat - Finite iterators: accumulate, chain, chainFromIterable, compress, dropwhile, filterfalse, groupby, islice, pairwise, takewhile, zip_longest - Combinatoric iterators: permutations, combinations, combinationsWithReplacement, product Also adds 20 tests in TestItertools.fs covering all major functions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 + src/Fable.Python.fsproj | 1 + src/stdlib/Itertools.fs | 190 ++++++++++++++++++++++++++++++++++ test/Fable.Python.Test.fsproj | 1 + test/TestItertools.fs | 121 ++++++++++++++++++++++ 5 files changed, 314 insertions(+) create mode 100644 src/stdlib/Itertools.fs create mode 100644 test/TestItertools.fs diff --git a/CHANGELOG.md b/CHANGELOG.md index 749c832..f72e806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file. ### ✨ Enhancements +* Add `itertools` module bindings: `count`, `cycle`, `repeat`, `accumulate`, `chain`, `chainFromIterable`, `compress`, `dropwhile`, `filterfalse`, `groupby`, `islice`, `pairwise`, `takewhile`, `zip_longest`, `permutations`, `combinations`, `combinationsWithReplacement`, `product` * Add missing `math` module constants: `pi`, `e`, `tau`, `inf`, `nan` * Add missing `math` module functions: `sqrt`, `degrees`, `radians`, `trunc`, `hypot`, `fsum`, `isqrt`, `prod`, `perm`, `acosh`, `asinh`, `atanh`, `cosh`, `sinh`, `tanh`, `erf`, `erfc`, `gamma`, `lgamma` * Fix `math.dist` signature to accept float arrays (for multi-dimensional distance) diff --git a/src/Fable.Python.fsproj b/src/Fable.Python.fsproj index e7eb9a7..1a155e9 100644 --- a/src/Fable.Python.fsproj +++ b/src/Fable.Python.fsproj @@ -24,6 +24,7 @@ + diff --git a/src/stdlib/Itertools.fs b/src/stdlib/Itertools.fs new file mode 100644 index 0000000..1c24898 --- /dev/null +++ b/src/stdlib/Itertools.fs @@ -0,0 +1,190 @@ +/// Type bindings for Python itertools module: https://docs.python.org/3/library/itertools.html +module Fable.Python.Itertools + +open Fable.Core + +// fsharplint:disable MemberNames + +[] +type IExports = + // ======================================================================== + // Infinite iterators + // ======================================================================== + + /// Make an iterator that returns evenly spaced values starting with number start + /// See https://docs.python.org/3/library/itertools.html#itertools.count + [] + abstract count: start: int -> seq + + /// Make an iterator that returns evenly spaced values starting with number start, + /// incrementing by step + /// See https://docs.python.org/3/library/itertools.html#itertools.count + [] + abstract count: start: int * step: int -> seq + + /// Make an iterator that returns evenly spaced float values + /// See https://docs.python.org/3/library/itertools.html#itertools.count + [] + abstract count: start: float -> seq + + /// Make an iterator that returns evenly spaced float values starting with start, + /// incrementing by step + /// See https://docs.python.org/3/library/itertools.html#itertools.count + [] + abstract count: start: float * step: float -> seq + + /// Make an iterator returning elements from the iterable and saving a copy of each. + /// When the iterable is exhausted, return elements from the saved copy. Repeats indefinitely. + /// See https://docs.python.org/3/library/itertools.html#itertools.cycle + abstract cycle: iterable: 'T seq -> seq<'T> + + /// Make an iterator that returns object elem over and over again. Runs indefinitely. + /// See https://docs.python.org/3/library/itertools.html#itertools.repeat + abstract repeat: elem: 'T -> seq<'T> + + /// Make an iterator that returns object elem, times times. + /// See https://docs.python.org/3/library/itertools.html#itertools.repeat + [] + abstract repeat: elem: 'T * times: int -> seq<'T> + + // ======================================================================== + // Finite iterators + // ======================================================================== + + /// Make an iterator that returns accumulated sums + /// See https://docs.python.org/3/library/itertools.html#itertools.accumulate + abstract accumulate: iterable: 'T seq -> seq<'T> + + /// Make an iterator that returns accumulated results of a binary function. + /// Uses a curried function: fun a b -> a + b (Fable wraps it to Python's two-arg call) + /// See https://docs.python.org/3/library/itertools.html#itertools.accumulate + [] + abstract accumulate: iterable: 'T seq * func: ('T -> 'T -> 'T) -> seq<'T> + + /// Make an iterator that returns accumulated results of a binary function, + /// with an initial value. + /// Uses a curried function: fun a b -> a + b (Fable wraps it to Python's two-arg call) + /// See https://docs.python.org/3/library/itertools.html#itertools.accumulate + [] + abstract accumulate: iterable: 'T seq * func: ('T -> 'T -> 'T) * initial: 'T -> seq<'T> + + /// Make an iterator that returns elements from the first iterable until it is exhausted, + /// then proceeds to the next iterable, until all of the iterables are exhausted. + /// See https://docs.python.org/3/library/itertools.html#itertools.chain + abstract chain: a: 'T seq * b: 'T seq -> seq<'T> + + /// Make an iterator that chains three iterables together. + /// See https://docs.python.org/3/library/itertools.html#itertools.chain + abstract chain: a: 'T seq * b: 'T seq * c: 'T seq -> seq<'T> + + /// Make an iterator that chains four iterables together. + /// See https://docs.python.org/3/library/itertools.html#itertools.chain + abstract chain: a: 'T seq * b: 'T seq * c: 'T seq * d: 'T seq -> seq<'T> + + /// Make an iterator that chains iterables from a single iterable of iterables. + /// Equivalent to Python's itertools.chain.from_iterable. + /// See https://docs.python.org/3/library/itertools.html#itertools.chain.from_iterable + [] + abstract chainFromIterable: iterable: 'T seq seq -> seq<'T> + + /// Make an iterator that filters elements from data returning only those that have + /// a corresponding element in selectors that evaluates to True. + /// See https://docs.python.org/3/library/itertools.html#itertools.compress + abstract compress: data: 'T seq * selectors: bool seq -> seq<'T> + + /// Make an iterator that drops elements from the iterable as long as the predicate is true; + /// afterwards, returns every element. + /// See https://docs.python.org/3/library/itertools.html#itertools.dropwhile + abstract dropwhile: predicate: ('T -> bool) * iterable: 'T seq -> seq<'T> + + /// Make an iterator that filters elements from iterable returning only those for which + /// the predicate returns False. + /// See https://docs.python.org/3/library/itertools.html#itertools.filterfalse + abstract filterfalse: predicate: ('T -> bool) * iterable: 'T seq -> seq<'T> + + /// Make an iterator that returns consecutive keys and groups from the iterable + /// (using identity as the key function). + /// Note: The group iterators share the underlying iterable — consume each group before advancing. + /// See https://docs.python.org/3/library/itertools.html#itertools.groupby + abstract groupby: iterable: 'T seq -> seq<'T * seq<'T>> + + /// Make an iterator that returns consecutive keys and groups from the iterable. + /// The key is computed for each element; elements with equal consecutive keys are grouped. + /// Note: The group iterators share the underlying iterable — consume each group before advancing. + /// See https://docs.python.org/3/library/itertools.html#itertools.groupby + abstract groupby: iterable: 'T seq * key: ('T -> 'K) -> seq<'K * seq<'T>> + + /// Make an iterator that returns selected elements from the iterable. Stops at position stop. + /// See https://docs.python.org/3/library/itertools.html#itertools.islice + [] + abstract islice: iterable: 'T seq * stop: int -> seq<'T> + + /// Make an iterator that returns selected elements from the iterable, starting at start. + /// See https://docs.python.org/3/library/itertools.html#itertools.islice + [] + abstract islice: iterable: 'T seq * start: int * stop: int -> seq<'T> + + /// Make an iterator that returns selected elements from the iterable, with a step. + /// See https://docs.python.org/3/library/itertools.html#itertools.islice + [] + abstract islice: iterable: 'T seq * start: int * stop: int * step: int -> seq<'T> + + /// Return successive overlapping pairs taken from the iterable. + /// Requires Python 3.10+. + /// See https://docs.python.org/3/library/itertools.html#itertools.pairwise + abstract pairwise: iterable: 'T seq -> seq<'T * 'T> + + /// Make an iterator that returns elements from the iterable as long as the predicate is true. + /// See https://docs.python.org/3/library/itertools.html#itertools.takewhile + abstract takewhile: predicate: ('T -> bool) * iterable: 'T seq -> seq<'T> + + /// Make an iterator that aggregates elements from each of the iterables. + /// If the iterables are of uneven length, missing values are filled with None (option). + /// See https://docs.python.org/3/library/itertools.html#itertools.zip_longest + abstract zip_longest: a: 'T seq * b: 'U seq -> seq<'T option * 'U option> + + /// Make an iterator that aggregates elements from each of the iterables. + /// If the iterables are of uneven length, missing values are filled with fillvalue. + /// See https://docs.python.org/3/library/itertools.html#itertools.zip_longest + [] + abstract zip_longest: a: 'T seq * b: 'T seq * fillvalue: 'T -> seq<'T * 'T> + + // ======================================================================== + // Combinatoric iterators + // ======================================================================== + + /// Return successive r-length permutations of elements in the iterable. + /// All elements are used (no r limit). + /// See https://docs.python.org/3/library/itertools.html#itertools.permutations + [] + abstract permutations: iterable: 'T seq -> seq<'T[]> + + /// Return successive r-length permutations of elements in the iterable. + /// See https://docs.python.org/3/library/itertools.html#itertools.permutations + [] + abstract permutations: iterable: 'T seq * r: int -> seq<'T[]> + + /// Return successive r-length combinations of elements in the iterable (no repeated elements). + /// See https://docs.python.org/3/library/itertools.html#itertools.combinations + [] + abstract combinations: iterable: 'T seq * r: int -> seq<'T[]> + + /// Return successive r-length combinations of elements in the iterable, allowing individual + /// elements to be repeated. + /// See https://docs.python.org/3/library/itertools.html#itertools.combinations_with_replacement + [] + abstract combinationsWithReplacement: iterable: 'T seq * r: int -> seq<'T[]> + + /// Return the Cartesian product of the two input iterables. + /// See https://docs.python.org/3/library/itertools.html#itertools.product + abstract product: a: 'T seq * b: 'U seq -> seq<'T * 'U> + + /// Return the Cartesian product of the iterable with itself, repeated r times. + /// Each element is a list of r items. + /// See https://docs.python.org/3/library/itertools.html#itertools.product + [] + abstract product: iterable: 'T seq * repeat: int -> seq> + +/// Functions creating iterators for efficient looping +[] +let itertools: IExports = nativeOnly diff --git a/test/Fable.Python.Test.fsproj b/test/Fable.Python.Test.fsproj index 188b590..7b0d3b9 100644 --- a/test/Fable.Python.Test.fsproj +++ b/test/Fable.Python.Test.fsproj @@ -17,6 +17,7 @@ + diff --git a/test/TestItertools.fs b/test/TestItertools.fs new file mode 100644 index 0000000..0be962d --- /dev/null +++ b/test/TestItertools.fs @@ -0,0 +1,121 @@ +module Fable.Python.Tests.Itertools + +open Fable.Python.Testing +open Fable.Python.Itertools + +[] +let ``test count from start works`` () = + itertools.count 1 + |> Seq.take 4 + |> Seq.toList + |> equal [ 1; 2; 3; 4 ] + +[] +let ``test count with step works`` () = + itertools.count (0, 2) + |> Seq.take 4 + |> Seq.toList + |> equal [ 0; 2; 4; 6 ] + +[] +let ``test cycle works`` () = + itertools.cycle [ 1; 2; 3 ] + |> Seq.take 7 + |> Seq.toList + |> equal [ 1; 2; 3; 1; 2; 3; 1 ] + +[] +let ``test repeat with times works`` () = + itertools.repeat ("x", 3) + |> Seq.toList + |> equal [ "x"; "x"; "x" ] + +[] +let ``test accumulate works`` () = + itertools.accumulate [ 1; 2; 3; 4; 5 ] + |> Seq.toList + |> equal [ 1; 3; 6; 10; 15 ] + +[] +let ``test accumulate with func works`` () = + itertools.accumulate ([ 1; 2; 3; 4 ], fun a b -> a * b) + |> Seq.toList + |> equal [ 1; 2; 6; 24 ] + +[] +let ``test chain two sequences works`` () = + itertools.chain ([ 1; 2 ], [ 3; 4 ]) + |> Seq.toList + |> equal [ 1; 2; 3; 4 ] + +[] +let ``test chain three sequences works`` () = + itertools.chain ([ 1 ], [ 2; 3 ], [ 4; 5 ]) + |> Seq.toList + |> equal [ 1; 2; 3; 4; 5 ] + +[] +let ``test chainFromIterable works`` () = + itertools.chainFromIterable [ [ 1; 2 ]; [ 3; 4 ]; [ 5 ] ] + |> Seq.toList + |> equal [ 1; 2; 3; 4; 5 ] + +[] +let ``test compress works`` () = + itertools.compress ([ 1; 2; 3; 4; 5 ], [ true; false; true; false; true ]) + |> Seq.toList + |> equal [ 1; 3; 5 ] + +[] +let ``test dropwhile works`` () = + itertools.dropwhile ((fun x -> x < 3), [ 1; 2; 3; 4; 5 ]) + |> Seq.toList + |> equal [ 3; 4; 5 ] + +[] +let ``test filterfalse works`` () = + itertools.filterfalse ((fun x -> x % 2 = 0), [ 1; 2; 3; 4; 5 ]) + |> Seq.toList + |> equal [ 1; 3; 5 ] + +[] +let ``test islice with stop works`` () = + itertools.islice ([ 1; 2; 3; 4; 5 ], 3) + |> Seq.toList + |> equal [ 1; 2; 3 ] + +[] +let ``test islice with start and stop works`` () = + itertools.islice ([ 1; 2; 3; 4; 5 ], 1, 4) + |> Seq.toList + |> equal [ 2; 3; 4 ] + +[] +let ``test pairwise works`` () = + itertools.pairwise [ 1; 2; 3; 4 ] + |> Seq.toList + |> equal [ (1, 2); (2, 3); (3, 4) ] + +[] +let ``test takewhile works`` () = + itertools.takewhile ((fun x -> x < 4), [ 1; 2; 3; 4; 5 ]) + |> Seq.toList + |> equal [ 1; 2; 3 ] + +[] +let ``test combinations works`` () = + itertools.combinations ([ 1; 2; 3 ], 2) + |> Seq.toList + |> equal [ [| 1; 2 |]; [| 1; 3 |]; [| 2; 3 |] ] + +[] +let ``test permutations with r works`` () = + itertools.permutations ([ 1; 2; 3 ], 2) + |> Seq.length + |> equal 6 + +[] +let ``test product two sequences works`` () = + itertools.product ([ 1; 2 ], [ "a"; "b" ]) + |> Seq.toList + |> equal [ (1, "a"); (1, "b"); (2, "a"); (2, "b") ] From 650dd10b6f6603b40d2daf743192e27c1cbd75d9 Mon Sep 17 00:00:00 2001 From: Dag Brattli Date: Fri, 17 Apr 2026 06:57:36 +0200 Subject: [PATCH 2/2] fix(itertools): correct accumulate callable + combinatoric return types - accumulate with func: replace curried lambda bridge with System.Func<_,_,_>. Fable uncurries F# binary functions to 2-arg Python callables, so the previous lambda a,b: f(a)(b) wrapper failed at runtime. System.Func compiles directly to a 2-arg Python callable and also unblocks overload resolution (curried arg methods can't be overloaded). - permutations / combinations / combinations_with_replacement / product (repeat overload): change return type from seq<'T[]> / seq> to seq<'T seq>. Python itertools yields tuples, which don't compare equal to F# arrays; seq<'T seq> matches the tuple shape and lets callers pick their own materialization. - Revert CHANGELOG.md edit per AGENTS.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 1 - src/stdlib/Itertools.fs | 27 +++++++++++++-------------- test/TestItertools.fs | 3 ++- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f72e806..749c832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,6 @@ All notable changes to this project will be documented in this file. ### ✨ Enhancements -* Add `itertools` module bindings: `count`, `cycle`, `repeat`, `accumulate`, `chain`, `chainFromIterable`, `compress`, `dropwhile`, `filterfalse`, `groupby`, `islice`, `pairwise`, `takewhile`, `zip_longest`, `permutations`, `combinations`, `combinationsWithReplacement`, `product` * Add missing `math` module constants: `pi`, `e`, `tau`, `inf`, `nan` * Add missing `math` module functions: `sqrt`, `degrees`, `radians`, `trunc`, `hypot`, `fsum`, `isqrt`, `prod`, `perm`, `acosh`, `asinh`, `atanh`, `cosh`, `sinh`, `tanh`, `erf`, `erfc`, `gamma`, `lgamma` * Fix `math.dist` signature to accept float arrays (for multi-dimensional distance) diff --git a/src/stdlib/Itertools.fs b/src/stdlib/Itertools.fs index 1c24898..bdb2aaf 100644 --- a/src/stdlib/Itertools.fs +++ b/src/stdlib/Itertools.fs @@ -56,17 +56,14 @@ type IExports = abstract accumulate: iterable: 'T seq -> seq<'T> /// Make an iterator that returns accumulated results of a binary function. - /// Uses a curried function: fun a b -> a + b (Fable wraps it to Python's two-arg call) /// See https://docs.python.org/3/library/itertools.html#itertools.accumulate - [] - abstract accumulate: iterable: 'T seq * func: ('T -> 'T -> 'T) -> seq<'T> + abstract accumulate: iterable: 'T seq * func: System.Func<'T, 'T, 'T> -> seq<'T> /// Make an iterator that returns accumulated results of a binary function, /// with an initial value. - /// Uses a curried function: fun a b -> a + b (Fable wraps it to Python's two-arg call) /// See https://docs.python.org/3/library/itertools.html#itertools.accumulate - [] - abstract accumulate: iterable: 'T seq * func: ('T -> 'T -> 'T) * initial: 'T -> seq<'T> + [] + abstract accumulate: iterable: 'T seq * func: System.Func<'T, 'T, 'T> * initial: 'T -> seq<'T> /// Make an iterator that returns elements from the first iterable until it is exhausted, /// then proceeds to the next iterable, until all of the iterables are exhausted. @@ -154,36 +151,38 @@ type IExports = // ======================================================================== /// Return successive r-length permutations of elements in the iterable. - /// All elements are used (no r limit). + /// All elements are used (no r limit). Each group is a Python tuple, exposed as seq<'T>. /// See https://docs.python.org/3/library/itertools.html#itertools.permutations [] - abstract permutations: iterable: 'T seq -> seq<'T[]> + abstract permutations: iterable: 'T seq -> seq<'T seq> /// Return successive r-length permutations of elements in the iterable. + /// Each group is a Python tuple, exposed as seq<'T>. /// See https://docs.python.org/3/library/itertools.html#itertools.permutations [] - abstract permutations: iterable: 'T seq * r: int -> seq<'T[]> + abstract permutations: iterable: 'T seq * r: int -> seq<'T seq> /// Return successive r-length combinations of elements in the iterable (no repeated elements). + /// Each group is a Python tuple, exposed as seq<'T>. /// See https://docs.python.org/3/library/itertools.html#itertools.combinations [] - abstract combinations: iterable: 'T seq * r: int -> seq<'T[]> + abstract combinations: iterable: 'T seq * r: int -> seq<'T seq> /// Return successive r-length combinations of elements in the iterable, allowing individual - /// elements to be repeated. + /// elements to be repeated. Each group is a Python tuple, exposed as seq<'T>. /// See https://docs.python.org/3/library/itertools.html#itertools.combinations_with_replacement [] - abstract combinationsWithReplacement: iterable: 'T seq * r: int -> seq<'T[]> + abstract combinationsWithReplacement: iterable: 'T seq * r: int -> seq<'T seq> /// Return the Cartesian product of the two input iterables. /// See https://docs.python.org/3/library/itertools.html#itertools.product abstract product: a: 'T seq * b: 'U seq -> seq<'T * 'U> /// Return the Cartesian product of the iterable with itself, repeated r times. - /// Each element is a list of r items. + /// Each group is a Python tuple, exposed as seq<'T>. /// See https://docs.python.org/3/library/itertools.html#itertools.product [] - abstract product: iterable: 'T seq * repeat: int -> seq> + abstract product: iterable: 'T seq * repeat: int -> seq<'T seq> /// Functions creating iterators for efficient looping [] diff --git a/test/TestItertools.fs b/test/TestItertools.fs index 0be962d..1f9a413 100644 --- a/test/TestItertools.fs +++ b/test/TestItertools.fs @@ -105,8 +105,9 @@ let ``test takewhile works`` () = [] let ``test combinations works`` () = itertools.combinations ([ 1; 2; 3 ], 2) + |> Seq.map Seq.toList |> Seq.toList - |> equal [ [| 1; 2 |]; [| 1; 3 |]; [| 2; 3 |] ] + |> equal [ [ 1; 2 ]; [ 1; 3 ]; [ 2; 3 ] ] [] let ``test permutations with r works`` () =