From 570e598380e2669967b1b87991788f434ca48e44 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Fri, 28 Mar 2025 21:36:46 +0330 Subject: [PATCH 1/9] update readme --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index f5c7ec1..719809d 100644 --- a/readme.md +++ b/readme.md @@ -39,7 +39,7 @@ Implementation of .NET's [IEnumerable](https://learn.microsoft.com/en-us/dotnet/ - [x] Max - [x] remove `Comparable` bind from type variables - [x] Publish on pypi -- [ ] Add external wrapper constructor +- [x] Add external wrapper constructor ### v1.1.x - [ ] Improve test code quality - [ ] Add hashed pure python implementation of `Enumerable` (assuming inputs are guaranteed to be `Hashable` & immutable & not maintaining order) From 5552d8de67bb67a5d96fe757c1cd62e8ae35b730 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Fri, 28 Mar 2025 23:35:51 +0330 Subject: [PATCH 2/9] update readme --- readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.md b/readme.md index 719809d..5a0d735 100644 --- a/readme.md +++ b/readme.md @@ -40,6 +40,7 @@ Implementation of .NET's [IEnumerable](https://learn.microsoft.com/en-us/dotnet/ - [x] remove `Comparable` bind from type variables - [x] Publish on pypi - [x] Add external wrapper constructor +- [x] Add technical documentation ### v1.1.x - [ ] Improve test code quality - [ ] Add hashed pure python implementation of `Enumerable` (assuming inputs are guaranteed to be `Hashable` & immutable & not maintaining order) From 17afec474f2a42fe0ded9ecbb81d54013560f836 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Fri, 28 Mar 2025 23:35:59 +0330 Subject: [PATCH 3/9] improve testsg --- .../enumerable/chunk_/test_chunk_method.py | 16 +++++++++++++--- .../enumerable/concat/test_concat_method.py | 4 ++-- .../distinct/test_distinct_by_method.py | 2 +- .../enumerable/group_by/test_group_by_method.py | 2 ++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/test/unit/pure_python/enumerable/chunk_/test_chunk_method.py b/test/unit/pure_python/enumerable/chunk_/test_chunk_method.py index 19c3c22..d0a7f82 100644 --- a/test/unit/pure_python/enumerable/chunk_/test_chunk_method.py +++ b/test/unit/pure_python/enumerable/chunk_/test_chunk_method.py @@ -5,9 +5,19 @@ class TestChunkMethod: def test_chunk(self) -> None: - obj = PurePythonEnumerable(*range(length := 7)) + obj = PurePythonEnumerable( + one := 1, + two := 2, + three := 3, + four := 4, + five := 5, + six := 6, + seven := 7 + ) res = obj.chunk(size := 3) - assert len(res) == ceil(length / size) - assert len(res[-1].source) == 1 + assert len(res) == ceil(len(obj.source) / size) + assert res[0].source == (one, two, three) + assert res[1].source == (four, five, six) + assert res[2].source == (seven,) diff --git a/test/unit/pure_python/enumerable/concat/test_concat_method.py b/test/unit/pure_python/enumerable/concat/test_concat_method.py index 4b25d6b..91c079f 100644 --- a/test/unit/pure_python/enumerable/concat/test_concat_method.py +++ b/test/unit/pure_python/enumerable/concat/test_concat_method.py @@ -11,6 +11,6 @@ def test_concat(self) -> None: *(second_items := (-9, -3, -8, -1, 8, -9)), ) - final = first.concat(second) + res = first.concat(second) - assert final.source == first_items + second_items + assert res.source == first_items + second_items diff --git a/test/unit/pure_python/enumerable/distinct/test_distinct_by_method.py b/test/unit/pure_python/enumerable/distinct/test_distinct_by_method.py index 35d6445..8da118e 100644 --- a/test/unit/pure_python/enumerable/distinct/test_distinct_by_method.py +++ b/test/unit/pure_python/enumerable/distinct/test_distinct_by_method.py @@ -27,7 +27,7 @@ def test_without_comparer(self) -> None: from_iterable=(items := tuple(range(7)), [-i for i in items]), ) - res = obj.distinct_by(lambda i: i**2) + res = obj.distinct_by(lambda i: abs(i)) assert res.source == items diff --git a/test/unit/pure_python/enumerable/group_by/test_group_by_method.py b/test/unit/pure_python/enumerable/group_by/test_group_by_method.py index 539911a..d4afdc3 100644 --- a/test/unit/pure_python/enumerable/group_by/test_group_by_method.py +++ b/test/unit/pure_python/enumerable/group_by/test_group_by_method.py @@ -38,3 +38,5 @@ def test_group_by(self) -> None: p.y == second_group_key for p in res.source[second_group_index].source ) + assert res.source[first_group_index].key == first_group_key + assert res.source[second_group_index].key == second_group_key From 5dc1a180747f80ad1411e77258f2df6d37de4bf5 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Fri, 28 Mar 2025 23:36:10 +0330 Subject: [PATCH 4/9] add documentation; incomplete --- documentation.md | 180 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 documentation.md diff --git a/documentation.md b/documentation.md new file mode 100644 index 0000000..c9753e7 --- /dev/null +++ b/documentation.md @@ -0,0 +1,180 @@ +# PyEnumerable ![WTFPL License](http://www.wtfpl.net/wp-content/uploads/2012/12/wtfpl-badge-4.png) + +Implementation of .NET's [IEnumerable](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1?view=net-9.0) interface in python W/ support for generics. + +## Architecture & Design + +PyEnumerable follows a relatively simple architecture, mainly because there isn't any reason to do otherwise! +Extension methods defined by `IEnumerable` interface are grouped by their functionality under protocols located `pyenumerable.protocol` package; The main advantage provided by protocols over ABCs (abstract base classes) is the ability to define overloads w/ different signatures. + +### Protocols + +#### Common Type Variables Among Protocols + +##### `TSource` + +Type of the items inside an instance of a particular implementation of `Enumerable`. + +#### Common Arguments Among Methods + +##### `comparer` + +A callable which accepts two arguments of type `Tsource` & returns a `bool` value; The meaning of the returned value is dependant on the context of its method; usually defaults to `lambda i, o: i == o`. + +#### `Enumerable` + +This protocol consolidates all other protocols into a single one, allowing implementations to reference it instead of listing each individual protocol. This approach minimizes the risk of omitting any methods due to oversight. +It also enforces the presence of a property called `source` which can be used to access actual items inside an instance of a particular implementation. + +#### `Associable` + +This protocol is the return type of `group_by` method; It inherits `Enumerable` & enforces an additional property called `key` which can be used to access the common key of a particular group. + +#### `SupportsAggregate` + +##### `aggregate` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.aggregate(lambda x, y: x + y) == 15 +assert one.aggregate(lambda x, y: x + y, seed=10) == 25 +``` + +#### `SupportsAll` + +##### `all` + +usage: +```py +assert one.source == (True, True) +assert one.all() is True +assert two.source == (True, False) +assert two.all() is False +assert three.source == (1, 2, 3, 4, 5) +assert three.all(lambda x: x < 7) is True +assert three.source == (1, 2, 3, 4, 5) +assert three.all(lambda x: x > 7) is False +``` + +#### `SupportsAny` + +##### `any` + +usage: +```py +assert one.source == (True, False) +assert one.any() is True +assert two.source == (False, False) +assert two.any() is False +assert three.source == (1, 2, 3, 4, 5) +assert three.any(lambda x: x == 7) is False +assert four.source == (1, 2, 3, 4, 5) +assert four.any(lambda x: x == 1) is True +``` + +#### `SupportsAppend` + +##### `append` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.append(7).source == (1, 2, 3, 4, 5, 7) +``` + +#### `SupportsAverage` + +##### `average` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.average() == 3.0 +``` + +#### `SupportsChunk` + +##### `chunk` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.chunk(2) == (res := (two, three, four)) +assert two.source == (1, 2) +assert three.source == (3, 4) +assert four.source == (5,) +``` + +#### `SupportsConcat` + +##### `concat` + +usage: +```py +assert one.source == (1, 2, 3) +assert two.source == (4, 5) +assert one.concat(two).source == (1, 2, 3, 4, 5) +``` + +#### `SupportsContains` + +##### `contains` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.contains(3) is True +assert one.contains(7) is False +``` + +#### `SupportsCount` + +##### `count` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.count() == 5 +assert one.count(lambda x: x >= 3) == 3 +``` + +#### `SupportsDistinct` + +##### `distinct` + +usage: +```py +assert one.source == (1, 2, 2, 3, 3, 4, 5) +assert one.distinct().res == (1, 2, 3, 4, 5) +``` + +##### `distinct_by` + +usage: +```py +assert one.source == (1, 2, -1, -2, -3, 4, -5) +assert one.distinct_by(lambda x: abs(x)).res == (1, 2, -3, 4, -5) +``` + +#### `SupportsGroupBy` + +##### `group_by` + +usage: +```py +Point = namedtuple('Point', ('x', 'y')) + +assert one.source == ( + Point(1, 1), + Point(2, 1), + Point(3, 2), + Point(4, 2), + Point(5, 2), +) +assert one.group_by(lambda point: point.y).source == (two, three) +assert two.key == 1 +assert two.source == (Point(1, 1), Point(2, 1)) +assert three.key == 2 +assert three.source == (Point(3, 2), Point(4, 2), Point(5, 2)) +``` From 5cbd69029dbf9e9abcd04dc2f53bc5fca39d8aad Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Sat, 29 Mar 2025 16:30:41 +0330 Subject: [PATCH 5/9] update uv lock --- uv.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/uv.lock b/uv.lock index e04028d..2ca4db3 100644 --- a/uv.lock +++ b/uv.lock @@ -77,7 +77,7 @@ wheels = [ [[package]] name = "pyenumerable" -version = "0.0.0" +version = "1.0.2" source = { virtual = "." } [package.dev-dependencies] @@ -100,15 +100,15 @@ dev = [ [[package]] name = "pyright" -version = "1.1.397" +version = "1.1.398" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/23/cefa10c9cb198e0858ed0b9233371d62bca880337f628e58f50dfdfb12f0/pyright-1.1.397.tar.gz", hash = "sha256:07530fd65a449e4b0b28dceef14be0d8e0995a7a5b1bb2f3f897c3e548451ce3", size = 3818998 } +sdist = { url = "https://files.pythonhosted.org/packages/24/d6/48740f1d029e9fc4194880d1ad03dcf0ba3a8f802e0e166b8f63350b3584/pyright-1.1.398.tar.gz", hash = "sha256:357a13edd9be8082dc73be51190913e475fa41a6efb6ec0d4b7aab3bc11638d8", size = 3892675 } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/b5/98ec41e1e0ad5576ecd42c90ec363560f7b389a441722ea3c7207682dec7/pyright-1.1.397-py3-none-any.whl", hash = "sha256:2e93fba776e714a82b085d68f8345b01f91ba43e1ab9d513e79b70fc85906257", size = 5693631 }, + { url = "https://files.pythonhosted.org/packages/58/e0/5283593f61b3c525d6d7e94cfb6b3ded20b3df66e953acaf7bb4f23b3f6e/pyright-1.1.398-py3-none-any.whl", hash = "sha256:0a70bfd007d9ea7de1cf9740e1ad1a40a122592cfe22a3f6791b06162ad08753", size = 5780235 }, ] [[package]] @@ -166,9 +166,9 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/0e/3e/b00a62db91a83fff600de219b6ea9908e6918664899a2d85db222f4fbf19/typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b", size = 106520 } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 }, ] From 78561a7eb36b8d83fa24f815c6b36b524e5734ff Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Sat, 29 Mar 2025 16:30:47 +0330 Subject: [PATCH 6/9] finish docs --- documentation.md | 526 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 525 insertions(+), 1 deletion(-) diff --git a/documentation.md b/documentation.md index c9753e7..922d81a 100644 --- a/documentation.md +++ b/documentation.md @@ -13,7 +13,7 @@ Extension methods defined by `IEnumerable` interface are grouped by their functi ##### `TSource` -Type of the items inside an instance of a particular implementation of `Enumerable`. +Type of items of `self`. #### Common Arguments Among Methods @@ -157,6 +157,46 @@ assert one.source == (1, 2, -1, -2, -3, 4, -5) assert one.distinct_by(lambda x: abs(x)).res == (1, 2, -3, 4, -5) ``` +type parameters: +- `TKey`: Type of key to distinguish items by. + +#### `SupportsExcept` + +##### `except_` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert two.source == (1, 2, 3) +assert one.except_(two).source == (4, 5) +``` + +##### `except_by` + +usage: +```py +Point = namedtuple('Point', ('x', 'y')) + +assert one.source == ( + Point(1, 1), + Point(1, 2), + Point(1, 3), + Point(1, 4), + Point(1, 5) +) +assert two.source == ( + Point(2, 1), + Point(2, 2), + Point(2, 3), +) +assert one.except_by(two, lambda point: point.y).source == ( + Point(1, 4), + Point(1, 5) +) +``` +type parameters: +- `TKey`: Type of key to identify items by. + #### `SupportsGroupBy` ##### `group_by` @@ -178,3 +218,487 @@ assert two.source == (Point(1, 1), Point(2, 1)) assert three.key == 2 assert three.source == (Point(3, 2), Point(4, 2), Point(5, 2)) ``` + +type parameters: +- `TKey`: Type of key returned by `key_selector`; Will be used to group items by. + +#### `SupportsGroupJoin` + +##### `group_join` + +usage: +```py +Point = namedtuple('Point', ('x', 'y')) + +assert one.source == (1, 2) +assert two.source == ( + Point(1, 1), + Point(2, 1), + Point(3, 2), + Point(4, 2), + Point(5, 2), +) +assert one.group_join( + two, + lambda x: x, + lambda point: point.y, + lambda x, + points: (x, points.source) +).source == ( + (1, Point(1, 1), Point(2, 1)), + (2, Point(3, 2), Point(4, 2), Point(5, 2)) +) +``` + +type parameters: +- `TKey`: Type of keys to group & join items by. +- `TInner`: Type of items of the second enumerable. +- `TResult`: Type of result items. + +#### `SupportsIntersect` + +##### `intersect` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert two.source == (1, 2, 3) +assert one.intersect(two) == (1, 2, 3) +``` + +##### `intersect_by` + +usage: +```py +Point = namedtuple('Point', ('x', 'y')) + +assert one.source == ( + Point(1, 1), + Point(1, 2), + Point(1, 3), + Point(1, 4), + Point(1, 5) +) +assert two.source == (1, 2, 3) +assert one.intersect_by(two, lambda point: point.y).source == ( + Point(1, 1), + Point(1, 2), + Point(1, 3) +) +``` + +type parameters: +- `TKey`: Type of key to identify items by. + +#### `SupportsJoin` + +##### `join` + +usage: +```py +Point = namedtuple('Point', ('x', 'y')) + +assert one.source == ( + Point(0, 1), + Point(0, 2), + Point(1, 2), + Point(0, 3), + Point(1, 1), +) +assert two.source == ( + Point(1, 0), + Point(1, 1), + Point(4, 0), + Point(4, 1), + Point(3, 0), +) +assert one.join( + two, + lambda point: point.y, + lambda point: point.x, + lambda outer_point, + inner_point: (outer_point, inner_point), +).source == ( + (Point(0, 1), Point(1, 0)), + (Point(0, 1), Point(1, 1)), + (Point(0, 3), Point(3, 0)), + (Point(1, 3), Point(3, 0)), +) +``` + +type parameters: +- `TKey`: Type of keys to join items by. +- `TInner`: Type of items of the second enumerable. +- `TResult`: Type of result items. + +#### `SupportsMax` + +##### `max_` + +usage: +```py +assert one.source == (2, 4, 5, 3, 1) +assert one.max_() == 5 +``` + +##### `max_by` + +usage: +```py +Point = namedtuple('Point', ('x', 'y')) + +assert one.source == ( + Point(2, 1), + Point(4, 1), + Point(5, 1), + Point(3, 1), + Point(1, 1), +) +assert one.max_by(lambda point: point.x) == Point(5, 1) +``` + +type parameters: +- `TKey`: Type of key to compare items by. + +#### `SupportsMin` + +##### `min_` + +usage: +```py +assert one.source == (5, 3, 1, 2, 4) +assert one.min_() == 1 +``` + +##### `min_by` + +usage: +```py +Point = namedtuple('Point', ('x', 'y')) + +assert one.source == ( + Point(5, 1), + Point(3, 1), + Point(1, 1), + Point(2, 1), + Point(4, 1), +) +assert one.min_by(lambda point: point.x) == Point(1, 1) +``` + +type parameters: +- `TKey`: Type of key to compare items by. + +#### `SupportsOfType` + +##### `of_type` + +usage: +```py +assert one.source == (0, None, "one", 2, 3) +assert one.of_type(int) == (0, 2, 3) +``` + +type parameters: +- `TResult`: Type of result items to filter items of enumerable on. + +#### `SupportsOrder` + +##### `order` +usage: +```py +assert one.source == (1, 2, 5, 4, 3) +assert one.order().source == (1, 2, 3, 4, 5) +``` + +##### `order_by` +usage: +```py +Point = namedtuple('Point', ('x', 'y')) + +assert one.source == (Point(1, 2), Point(1, 3), Point(1, 1)) +assert one.order_by(lambda point: point.y).source == (Point(1, 1), Point(1, 2), Point(1, 3)) +``` + +type parameters: +- `TKey`: Type of key to compare items by. + +##### `order_descending` +usage: +```py +assert one.source == (1, 2, 5, 4, 3) +assert one.order().source == (5, 4, 3, 2, 1) +``` + +##### `order_by_descending` +usage: +```py +Point = namedtuple('Point', ('x', 'y')) + +assert one.source == (Point(1, 2), Point(1, 3), Point(1, 1)) +assert one.order_by(lambda point: point.y).source == (Point(1, 3), Point(1, 2), Point(1, 1)) +``` + +type parameters: +- `TKey`: Type of key to compare items by. + +#### `SupportsPrepend` + +##### `prepend` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.prepend(0).source == (0, 1, 2, 3, 4, 5) +``` + +#### `SupportsReverse` + +##### `reverse` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.reverse().source == (5, 4, 3, 2, 1) +``` + +#### `SupportsSelect` + +type parameters: +- `TResult`: Type of result items. + +##### `select` + +usage: +```py +Point = namedtuple('Point', ('x', 'y')) + +assert one.source == ( + Point(0, 1), + Point(0, 2), + Point(0, 3), + Point(0, 4), + Point(0, 5) +) +assert one.select(lambda idx, point: (idx, point.y)).source == ( + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), +) +``` + +##### `select_many` + +usage: +```py +assert one.source == ( + Point(0, 1), + Point(0, 2), + Point(0, 3), + Point(0, 4), + Point(0, 5) +) +assert one.select_many(lambda _, point: (point.x, point.y)).source == ( + 0, + 1, + 0, + 2, + 0, + 3, + 0, + 4, + 0, + 5 +) +``` + +#### `SupportsSequenceEqual` + +##### `sequence_equal` + +usage: +```py +assert one.source == (1, 2, 3) +assert two.source == (3, 2, 1) +assert one.sequence_equal(two) is False +assert three.source == (1, 2, 3, 4) +assert one.sequence_equal(three) is False +assert four.source == (1, 2, 3) +assert one.sequence_equal(four) is True +``` + +#### `SupportsSingle` + +##### `single` + +usage: +```py +assert one.source == (7) +assert one.single() == 7 +assert two.source == (1, 2, 3, 4, 5) +assert two.single(lambda x: x % 3 == 0) == 3 +``` + +##### `single_or_default` + +usage: +```py +assert one.source == (7) +assert one.single_or_default(5) == 7 +assert two.source == (,) +assert two.single_or_default(5) == 5 +assert three.source == (1, 2, 3, 4, 5) +assert three.single_or_default(0, lambda x: x % 7 == 0, ) == 0 +assert four.source == (1, 2, 3, 4, 5) +assert four.single_or_default(0, lambda x: x % 2 == 0, ) == 0 +assert five.source == (1, 2, 3, 4, 5) +assert five.single_or_default(0, lambda x: x % 5 == 0, ) == 5 +``` + +#### `SupportsSkip` + +##### `skip` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.skip(2).source == (3, 4, 5) +assert two.source == (1, 2, 3, 4, 5) +assert two.skip(2, 4).source == (1, 2, 5) +``` + +##### `skip_last` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.skip_last(2).source == (1, 2, 3) +``` + +##### `skip_while` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.skip_while(lambda idx, _: idx <= 3).source == (5,) +assert two.source == (1, 2, 3, 4, 5) +assert two.skip_while(lambda _, x: x <= 3).source == (4, 5) +``` + +#### `SupportsSum` + +##### `sum` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.sum() == 15 +``` + +#### `SupportsTake` + +##### `take` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.take(2).source == (1, 2) +assert two.source == (1, 2, 3, 4, 5) +assert two.take(2, 4).source == (3, 4) +``` + +##### `take_last` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.take_last(2).source == (4, 5) +``` + +##### `take_while` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert one.take_while(lambda idx, _: idx <= 3).source == (1, 2, 3, 4) +assert two.source == (1, 2, 3, 4, 5) +assert two.take_while(lambda _, x: x <= 3).source == (1, 2, 3) +``` + +#### `SupportsUnion` + +##### `union` + +usage: +```py +assert one.source == (1, 2, 2, 3, 5) +assert two.source == (1, 3, 3, 4, 5) +assert one.union(two).source == (1, 2, 3, 5, 4) +``` + +##### `union_by` + +usage: +```py +Point = namedtuple('Point', ('x', 'y')) + +assert one.source == (Point(0, 1), Point(0, 2), Point(0, 3)) +assert two.source == (Point(1, 3), Point(1, 4), Point(1, 5)) +assert one.union_by(two, lambda point: point.y).source == ( + Point(0, 1), + Point(0, 2), + Point(0, 3), + Point(1, 4), + Point(1, 4), +) +``` + +type parameters: +- `TKey`: Type of key to identify items by. + +#### `SupportsWhere` + +##### `where` + +usage: +```py +Point = namedtuple('Point', ('x', 'y')) + +assert one.source == ( + Point(0, 1), + Point(2, 3), + Point(4, 5), + Point(6, 7), +) +assert one.where(lambda _, point: point.y <= 3).source == ( + Point(0, 1), + Point(2, 3), +) +assert two.source == ( + Point(0, 1), + Point(2, 3), + Point(4, 5), + Point(6, 7), +) +assert two.where(lambda idx, _: idx <= 2).source == ( + Point(0, 1), + Point(2, 3), + Point(4, 5), +) +``` + +#### `SupportsZip` + +##### `zip` + +usage: +```py +assert one.source == (1, 2, 3, 4, 5) +assert two.source == ("five", "four", "three") +assert one.zip(two).source == ((1, "five"), (2, "four"), (3, "three")) +``` + +type parameters: +- `TSecond`: Type of items of the second enumerable. From 0fddcd0c1b4523aa5695c69cef76b478268eae8f Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Sat, 29 Mar 2025 16:31:00 +0330 Subject: [PATCH 7/9] update readme --- readme.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 5a0d735..7da33cb 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ Implementation of .NET's [IEnumerable](https://learn.microsoft.com/en-us/dotnet/ ### v1.0.x - [x] Design protocols for each operation set - [x] Design & Implement `Enumerable` constructor(s) for PP implementation -- [x] Add pure python implementation of `Enumerable` (assuming inputs aren't guaranteed to be `Hashable` or immutable & maintaining order) +- [x] Add pure python implementation of `Enumerable` (assuming inputs aren't guaranteed to be `Hashable` or immutable; preserving order) - [x] Any - [x] All - [x] Aggregate @@ -40,7 +40,8 @@ Implementation of .NET's [IEnumerable](https://learn.microsoft.com/en-us/dotnet/ - [x] remove `Comparable` bind from type variables - [x] Publish on pypi - [x] Add external wrapper constructor -- [x] Add technical documentation +- [x] Add technical documentation pure python implementation +- [ ] Implement `__str__` & `__repr__` for ### v1.1.x - [ ] Improve test code quality -- [ ] Add hashed pure python implementation of `Enumerable` (assuming inputs are guaranteed to be `Hashable` & immutable & not maintaining order) +- [ ] Add hashed pure python implementation of `Enumerable` (assuming inputs are guaranteed to be `Hashable` & immutable; not preserving order) From 2704177fe0692dfb15c4bca2eb60fa0c99699a84 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Sat, 29 Mar 2025 16:31:21 +0330 Subject: [PATCH 8/9] improve tests --- .../enumerable/chunk_/test_chunk_method.py | 2 +- .../group_join/test_group_join_method.py | 10 ++-- .../enumerable/join/test_join_method.py | 58 +++++++------------ .../enumerable/of_type/test_of_type_method.py | 15 ++--- .../test_sequence_equal_method.py | 20 +------ 5 files changed, 33 insertions(+), 72 deletions(-) diff --git a/test/unit/pure_python/enumerable/chunk_/test_chunk_method.py b/test/unit/pure_python/enumerable/chunk_/test_chunk_method.py index d0a7f82..88443de 100644 --- a/test/unit/pure_python/enumerable/chunk_/test_chunk_method.py +++ b/test/unit/pure_python/enumerable/chunk_/test_chunk_method.py @@ -12,7 +12,7 @@ def test_chunk(self) -> None: four := 4, five := 5, six := 6, - seven := 7 + seven := 7, ) res = obj.chunk(size := 3) diff --git a/test/unit/pure_python/enumerable/group_join/test_group_join_method.py b/test/unit/pure_python/enumerable/group_join/test_group_join_method.py index 73939c2..ce0b317 100644 --- a/test/unit/pure_python/enumerable/group_join/test_group_join_method.py +++ b/test/unit/pure_python/enumerable/group_join/test_group_join_method.py @@ -18,16 +18,16 @@ def test_group_join(self) -> None: res = first_object.group_join( second_object, - lambda parent: parent.age, - lambda child: child.parent.age + lambda parent: parent.name, + lambda child: child.parent.name if child.parent is not None else None, - lambda parent, children: (parent.age, children.source), + lambda parent, children: (parent, children.source), ) assert res.source == ( - (first_parent.age, (first_child, second_child)), - (second_parent.age, (third_child, fourth_child, fifth_child)), + (first_parent, (first_child, second_child)), + (second_parent, (third_child, fourth_child, fifth_child)), ) def test_overlap_remove(self) -> None: diff --git a/test/unit/pure_python/enumerable/join/test_join_method.py b/test/unit/pure_python/enumerable/join/test_join_method.py index 1b34310..a30b1a7 100644 --- a/test/unit/pure_python/enumerable/join/test_join_method.py +++ b/test/unit/pure_python/enumerable/join/test_join_method.py @@ -8,14 +8,14 @@ def test_without_outcaster(self) -> None: second_group_key = 3 first_object = PurePythonEnumerable( - fgfo := Point(0, first_group_key), - sgfo := Point(0, second_group_key), - sgso := Point(1, second_group_key), + first_group_first_outer := Point(0, first_group_key), + second_group_first_outer := Point(0, second_group_key), + second_group_second_outer := Point(1, second_group_key), ) second_object = PurePythonEnumerable( - fgfi := Point(first_group_key, 0), - fgsi := Point(first_group_key, 1), - sgfi := Point(second_group_key, 0), + first_group_first_inner := Point(first_group_key, 0), + first_group_second_inner := Point(first_group_key, 1), + second_group_first_inner := Point(second_group_key, 0), ) res = first_object.join( @@ -26,19 +26,10 @@ def test_without_outcaster(self) -> None: ) assert res.source == ( - (fgfo, fgfi), # (first group first outer, first group first inner) - ( - fgfo, - fgsi, - ), # (first group first outer, first group second inner) - ( - sgfo, - sgfi, - ), # (second group first outer, second group first inner) - ( - sgso, - sgfi, - ), # (second group second outer, second group first inner) + (first_group_first_outer, first_group_first_inner), + (first_group_first_outer, first_group_second_inner), + (second_group_first_outer, second_group_first_inner), + (second_group_second_outer, second_group_first_inner), ) def test_with_outcaster(self) -> None: @@ -46,18 +37,18 @@ def test_with_outcaster(self) -> None: second_group_key = 3 first_object = PurePythonEnumerable( - fgfo := Point(0, first_group_key), + first_group_first_outer := Point(0, first_group_key), Point(0, 2), Point(1, 2), - sgfo := Point(0, second_group_key), - sgso := Point(1, second_group_key), + second_group_first_outer := Point(0, second_group_key), + second_group_second_outer := Point(1, second_group_key), ) second_object = PurePythonEnumerable( - fgfi := Point(first_group_key, 0), - fgsi := Point(first_group_key, 1), + first_group_first_inner := Point(first_group_key, 0), + first_group_second_inner := Point(first_group_key, 1), Point(4, 0), Point(4, 1), - sgfi := Point(second_group_key, 0), + second_group_first_inner := Point(second_group_key, 0), ) res = first_object.join( @@ -68,17 +59,8 @@ def test_with_outcaster(self) -> None: ) assert res.source == ( - (fgfo, fgfi), # (first group first outer, first group first inner) - ( - fgfo, - fgsi, - ), # (first group first outer, first group second inner) - ( - sgfo, - sgfi, - ), # (second group first outer, second group first inner) - ( - sgso, - sgfi, - ), # (second group second outer, second group first inner) + (first_group_first_outer, first_group_first_inner), + (first_group_first_outer, first_group_second_inner), + (second_group_first_outer, second_group_first_inner), + (second_group_second_outer, second_group_first_inner), ) diff --git a/test/unit/pure_python/enumerable/of_type/test_of_type_method.py b/test/unit/pure_python/enumerable/of_type/test_of_type_method.py index 941932a..6f8f90d 100644 --- a/test/unit/pure_python/enumerable/of_type/test_of_type_method.py +++ b/test/unit/pure_python/enumerable/of_type/test_of_type_method.py @@ -2,16 +2,11 @@ class TestOfTypeMethod: - def test_capturing_functionality(self) -> None: - obj = PurePythonEnumerable(None, "first", *(items := (1, 2))) + def test_of_type(self) -> None: + obj = PurePythonEnumerable( + zero := 0, None, "one", two := 2, three := 3 + ) res = obj.of_type(int) - assert len(res.source) == len(items) - - def test_filtering_functionality(self) -> None: - obj = PurePythonEnumerable(None, "first", two := 2, three := 3) - - res = obj.of_type(int) - - assert set(res.source) == {two, three} + assert res.source == (zero, two, three) diff --git a/test/unit/pure_python/enumerable/sequence_equal/test_sequence_equal_method.py b/test/unit/pure_python/enumerable/sequence_equal/test_sequence_equal_method.py index f320590..31b3ec2 100644 --- a/test/unit/pure_python/enumerable/sequence_equal/test_sequence_equal_method.py +++ b/test/unit/pure_python/enumerable/sequence_equal/test_sequence_equal_method.py @@ -3,23 +3,7 @@ class TestSequenceEqualMethod: - def test_pass_without_comparer(self) -> None: - first_object = PurePythonEnumerable(*range(stop := 7)) - second_object = PurePythonEnumerable(*range(stop)) - - res = first_object.sequence_equal(second_object) - - assert res is True - - def test_fail_without_comparer(self) -> None: - first_object = PurePythonEnumerable(*range(7)) - second_object = PurePythonEnumerable(*range(1, 8)) - - res = first_object.sequence_equal(second_object) - - assert res is False - - def test_pass_with_comparer(self) -> None: + def test_pass(self) -> None: first_object = PurePythonEnumerable( Point(0, 1), Point(1, 3), Point(2, 4) ) @@ -35,7 +19,7 @@ def test_pass_with_comparer(self) -> None: assert res is True - def test_fail_with_comparer(self) -> None: + def test_fail(self) -> None: first_object = PurePythonEnumerable( Point(0, 1), Point(1, 3), Point(2, 4) ) From ee491c352c2ef77088f5daadcefc42454fabf607 Mon Sep 17 00:00:00 2001 From: AmirHossein Ahmadi Date: Sat, 29 Mar 2025 16:31:30 +0330 Subject: [PATCH 9/9] ruff format --- pyenumerable/__init__.py | 1 - pyenumerable/constructors.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pyenumerable/__init__.py b/pyenumerable/__init__.py index cf69920..bd2f8c7 100644 --- a/pyenumerable/__init__.py +++ b/pyenumerable/__init__.py @@ -2,7 +2,6 @@ Implementation of .NET's IEnumerable interface in python W/ support for generics. """ # noqa: E501 - from pyenumerable.constructors import pp_enumerable from pyenumerable.implementations import PurePythonEnumerable from pyenumerable.protocol import Enumerable diff --git a/pyenumerable/constructors.py b/pyenumerable/constructors.py index b392438..c8a7076 100644 --- a/pyenumerable/constructors.py +++ b/pyenumerable/constructors.py @@ -5,7 +5,6 @@ def pp_enumerable[TSource]( - *items: TSource, - from_iterable: Iterable[Iterable[TSource]] | None = None + *items: TSource, from_iterable: Iterable[Iterable[TSource]] | None = None ) -> Enumerable[TSource]: return PurePythonEnumerable(*items, from_iterable=from_iterable)