flpit 0.1.6__tar.gz → 0.2.0.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flpit-0.1.6 → flpit-0.2.0.dev2}/PKG-INFO +74 -21
- {flpit-0.1.6 → flpit-0.2.0.dev2}/README.md +73 -20
- {flpit-0.1.6 → flpit-0.2.0.dev2}/pyproject.toml +2 -1
- {flpit-0.1.6 → flpit-0.2.0.dev2}/src/flpit/core/linq.py +370 -211
- {flpit-0.1.6 → flpit-0.2.0.dev2}/.gitignore +0 -0
- {flpit-0.1.6 → flpit-0.2.0.dev2}/LICENSE +0 -0
- {flpit-0.1.6 → flpit-0.2.0.dev2}/src/flp/__init__.py +0 -0
- {flpit-0.1.6 → flpit-0.2.0.dev2}/src/flpit/__init__.py +0 -0
- {flpit-0.1.6 → flpit-0.2.0.dev2}/src/flpit/core/__init__.py +0 -0
- {flpit-0.1.6 → flpit-0.2.0.dev2}/src/flpit/flp.py +0 -0
- {flpit-0.1.6 → flpit-0.2.0.dev2}/src/flpit/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flpit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0.dev2
|
|
4
4
|
Summary: Fluent LINQ for Python — flip your iterables
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -28,11 +28,23 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
> Where Python's language or type system requires a different API contract, the contract is adapted accordingly.
|
|
29
29
|
> The implementation is actively developed, and semantic deviations may still exist.
|
|
30
30
|
|
|
31
|
+
> **⚠️ Breaking Change — `FlpList` — v0.2.0**
|
|
32
|
+
>
|
|
33
|
+
> `FlpList` no longer derives from `collections.UserList`.
|
|
34
|
+
>
|
|
35
|
+
> **Retained:** Python collection behavior such as iteration, `len()`, `in`, indexing, and slicing.
|
|
36
|
+
>
|
|
37
|
+
> **LINQ:** `append()`, `concat()`, and other LINQ operations remain available with lazy semantics.
|
|
38
|
+
>
|
|
39
|
+
> **`List<T>` semantics:** `add()`, `add_range()`, and other mutating operations follow the `List<T>` API where applicable.
|
|
40
|
+
>
|
|
41
|
+
> **Removed:** `copy()`, `+`, `*`, and other `UserList`-specific operations that are not part of the `FlpList` API.
|
|
42
|
+
|
|
31
43
|
## ⚡ Key Features
|
|
32
44
|
|
|
33
|
-
* **⚡ Lazy Evaluation (`FlpIt`):** Deferred, streaming execution
|
|
34
|
-
* **📦 Materialized Container (`FlpList`):** Eager,
|
|
35
|
-
* **🎯 LINQ Semantics:**
|
|
45
|
+
* **⚡ Lazy Evaluation (`FlpIt`):** Deferred, streaming execution wrapping any standard Python `Iterable[T]`.
|
|
46
|
+
* **📦 Materialized Container (`FlpList`):** Eager collection implementing Python's `Collection` protocol (with indexing and slicing), retaining familiar collection access while providing .NET `List<T>`-style semantics and fluent LINQ query operations.
|
|
47
|
+
* **🎯 LINQ Semantics:** Familiar operators like `.any()`, `.first()`, `.single()`, `.order_by()`, and `.then_by()` with predictable, composable behavior.
|
|
36
48
|
* **🔀 Independent Query Pipelines:** Derived queries are independent and never mutate or retroactively affect separately created queries.
|
|
37
49
|
* **🧠 Lazy Internal Materialization:** Some operators, such as ordering and grouping, require internal materialization during deferred execution, while the query itself remains lazy.
|
|
38
50
|
* **🔄 Source Enumeration Semantics** — FlpIt preserves the enumeration characteristics of its source. A query over a re-enumerable source can be enumerated repeatedly; a one-shot source such as a Python generator remains one-shot. FlpIt does not clone or rewind arbitrary Python iterators.
|
|
@@ -186,26 +198,31 @@ list(query) # [0, 1, 2]
|
|
|
186
198
|
list(query) # []
|
|
187
199
|
```
|
|
188
200
|
|
|
189
|
-
## Intentional Deviation
|
|
201
|
+
## Intentional Semantic Deviation: `count()`
|
|
190
202
|
|
|
191
|
-
|
|
203
|
+
`FlpList` is a LINQ-oriented collection. Its query methods follow LINQ semantics even when those semantics differ from Python's built-in collection methods.
|
|
192
204
|
|
|
193
|
-
|
|
194
|
-
* **FlpIt `FlpList.count()`** counts all elements.
|
|
195
|
-
* **FlpIt `FlpList.count(predicate)`** counts elements matching the predicate.
|
|
205
|
+
In Python, `list.count(value)` counts occurrences of a specific value:
|
|
196
206
|
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
total_evens = eager_list.count(lambda x: x % 2 == 0)
|
|
207
|
+
```
|
|
208
|
+
[1, 2, 2, 3].count(2) # 2
|
|
200
209
|
```
|
|
201
210
|
|
|
202
|
-
|
|
211
|
+
In LINQ, `Count()` returns the total number of elements, or elements matching a predicate:
|
|
203
212
|
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
|
|
213
|
+
```
|
|
214
|
+
FlpList([1, 2, 2, 3]).count() # 4
|
|
215
|
+
FlpList([1, 2, 2, 3]).count(lambda x: x > 1) # 3
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`FlpList.count()` deliberately follows the LINQ meaning. Python-style value counting remains available through normal Python operations when needed:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
list(FlpList([1, 2, 2, 3])).count(2) # 2
|
|
207
222
|
```
|
|
208
223
|
|
|
224
|
+
This behavior is a core part of `FlpList`'s LINQ-oriented contract, not an incomplete `list` implementation.
|
|
225
|
+
|
|
209
226
|
## 🔗 Independent Query Pipelines
|
|
210
227
|
|
|
211
228
|
Query pipelines can be composed independently without mutating the queries they were derived from:
|
|
@@ -260,14 +277,22 @@ Lazy execution does not mean that every operator is streaming. Some operators mu
|
|
|
260
277
|
`group_by()` produces a query of groupings rather than terminating the pipeline:
|
|
261
278
|
|
|
262
279
|
```python
|
|
263
|
-
|
|
280
|
+
from typing import NamedTuple
|
|
281
|
+
|
|
282
|
+
class GroupResult(NamedTuple):
|
|
283
|
+
remainder: int
|
|
284
|
+
values: list[int]
|
|
264
285
|
|
|
265
286
|
result = (
|
|
266
|
-
|
|
267
|
-
.
|
|
268
|
-
.select(
|
|
287
|
+
flp.it(range(1, 11))
|
|
288
|
+
.group_by(lambda x: x % 2)
|
|
289
|
+
.select(lambda group: GroupResult(
|
|
290
|
+
remainder=group.key,
|
|
291
|
+
values=list(group),
|
|
292
|
+
))
|
|
269
293
|
.to_list()
|
|
270
294
|
)
|
|
295
|
+
|
|
271
296
|
```
|
|
272
297
|
|
|
273
298
|
Terminal operators instead evaluate the query and return a result rather than another query:
|
|
@@ -328,4 +353,32 @@ uv add flpit
|
|
|
328
353
|
|
|
329
354
|
## 📜 License
|
|
330
355
|
|
|
331
|
-
Distributed under the MIT License. See `LICENSE` for more information.
|
|
356
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
### Migrated .NET Unit Tests
|
|
360
|
+
|
|
361
|
+
> Migrated from the official .NET Runtime LINQ Tests.
|
|
362
|
+
> Note: The test cases exercise both core types, `FlpIt` and `FlpList`, with transitions between lazy and eager execution occurring as part of the individual test cases.
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
================================================================
|
|
366
|
+
| Test Module File | Test Cases |
|
|
367
|
+
----------------------------------------------------------------
|
|
368
|
+
| tests/nettests/ | |
|
|
369
|
+
| ├── test_all.py | 80 |
|
|
370
|
+
| ├── test_any.py | 90 |
|
|
371
|
+
| ├── test_average.py | 572 |
|
|
372
|
+
| ├── test_count.py | 37 |
|
|
373
|
+
| ├── test_generic_list.py | 95 |
|
|
374
|
+
| ├── test_last.py | 29 |
|
|
375
|
+
| ├── test_order_by.py | 100 |
|
|
376
|
+
| ├── test_order_descending_by.py | 45 |
|
|
377
|
+
| ├── test_sum.py | 134 |
|
|
378
|
+
| ├── test_take.py | 39 |
|
|
379
|
+
| ├── test_then_by.py | 40 |
|
|
380
|
+
| └── test_then_descending_by.py | 34 |
|
|
381
|
+
----------------------------------------------------------------
|
|
382
|
+
| GRAND TOTALS | 1295 |
|
|
383
|
+
================================================================
|
|
384
|
+
```
|
|
@@ -8,11 +8,23 @@
|
|
|
8
8
|
> Where Python's language or type system requires a different API contract, the contract is adapted accordingly.
|
|
9
9
|
> The implementation is actively developed, and semantic deviations may still exist.
|
|
10
10
|
|
|
11
|
+
> **⚠️ Breaking Change — `FlpList` — v0.2.0**
|
|
12
|
+
>
|
|
13
|
+
> `FlpList` no longer derives from `collections.UserList`.
|
|
14
|
+
>
|
|
15
|
+
> **Retained:** Python collection behavior such as iteration, `len()`, `in`, indexing, and slicing.
|
|
16
|
+
>
|
|
17
|
+
> **LINQ:** `append()`, `concat()`, and other LINQ operations remain available with lazy semantics.
|
|
18
|
+
>
|
|
19
|
+
> **`List<T>` semantics:** `add()`, `add_range()`, and other mutating operations follow the `List<T>` API where applicable.
|
|
20
|
+
>
|
|
21
|
+
> **Removed:** `copy()`, `+`, `*`, and other `UserList`-specific operations that are not part of the `FlpList` API.
|
|
22
|
+
|
|
11
23
|
## ⚡ Key Features
|
|
12
24
|
|
|
13
|
-
* **⚡ Lazy Evaluation (`FlpIt`):** Deferred, streaming execution
|
|
14
|
-
* **📦 Materialized Container (`FlpList`):** Eager,
|
|
15
|
-
* **🎯 LINQ Semantics:**
|
|
25
|
+
* **⚡ Lazy Evaluation (`FlpIt`):** Deferred, streaming execution wrapping any standard Python `Iterable[T]`.
|
|
26
|
+
* **📦 Materialized Container (`FlpList`):** Eager collection implementing Python's `Collection` protocol (with indexing and slicing), retaining familiar collection access while providing .NET `List<T>`-style semantics and fluent LINQ query operations.
|
|
27
|
+
* **🎯 LINQ Semantics:** Familiar operators like `.any()`, `.first()`, `.single()`, `.order_by()`, and `.then_by()` with predictable, composable behavior.
|
|
16
28
|
* **🔀 Independent Query Pipelines:** Derived queries are independent and never mutate or retroactively affect separately created queries.
|
|
17
29
|
* **🧠 Lazy Internal Materialization:** Some operators, such as ordering and grouping, require internal materialization during deferred execution, while the query itself remains lazy.
|
|
18
30
|
* **🔄 Source Enumeration Semantics** — FlpIt preserves the enumeration characteristics of its source. A query over a re-enumerable source can be enumerated repeatedly; a one-shot source such as a Python generator remains one-shot. FlpIt does not clone or rewind arbitrary Python iterators.
|
|
@@ -166,26 +178,31 @@ list(query) # [0, 1, 2]
|
|
|
166
178
|
list(query) # []
|
|
167
179
|
```
|
|
168
180
|
|
|
169
|
-
## Intentional Deviation
|
|
181
|
+
## Intentional Semantic Deviation: `count()`
|
|
170
182
|
|
|
171
|
-
|
|
183
|
+
`FlpList` is a LINQ-oriented collection. Its query methods follow LINQ semantics even when those semantics differ from Python's built-in collection methods.
|
|
172
184
|
|
|
173
|
-
|
|
174
|
-
* **FlpIt `FlpList.count()`** counts all elements.
|
|
175
|
-
* **FlpIt `FlpList.count(predicate)`** counts elements matching the predicate.
|
|
185
|
+
In Python, `list.count(value)` counts occurrences of a specific value:
|
|
176
186
|
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
total_evens = eager_list.count(lambda x: x % 2 == 0)
|
|
187
|
+
```
|
|
188
|
+
[1, 2, 2, 3].count(2) # 2
|
|
180
189
|
```
|
|
181
190
|
|
|
182
|
-
|
|
191
|
+
In LINQ, `Count()` returns the total number of elements, or elements matching a predicate:
|
|
183
192
|
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
```
|
|
194
|
+
FlpList([1, 2, 2, 3]).count() # 4
|
|
195
|
+
FlpList([1, 2, 2, 3]).count(lambda x: x > 1) # 3
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`FlpList.count()` deliberately follows the LINQ meaning. Python-style value counting remains available through normal Python operations when needed:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
list(FlpList([1, 2, 2, 3])).count(2) # 2
|
|
187
202
|
```
|
|
188
203
|
|
|
204
|
+
This behavior is a core part of `FlpList`'s LINQ-oriented contract, not an incomplete `list` implementation.
|
|
205
|
+
|
|
189
206
|
## 🔗 Independent Query Pipelines
|
|
190
207
|
|
|
191
208
|
Query pipelines can be composed independently without mutating the queries they were derived from:
|
|
@@ -240,14 +257,22 @@ Lazy execution does not mean that every operator is streaming. Some operators mu
|
|
|
240
257
|
`group_by()` produces a query of groupings rather than terminating the pipeline:
|
|
241
258
|
|
|
242
259
|
```python
|
|
243
|
-
|
|
260
|
+
from typing import NamedTuple
|
|
261
|
+
|
|
262
|
+
class GroupResult(NamedTuple):
|
|
263
|
+
remainder: int
|
|
264
|
+
values: list[int]
|
|
244
265
|
|
|
245
266
|
result = (
|
|
246
|
-
|
|
247
|
-
.
|
|
248
|
-
.select(
|
|
267
|
+
flp.it(range(1, 11))
|
|
268
|
+
.group_by(lambda x: x % 2)
|
|
269
|
+
.select(lambda group: GroupResult(
|
|
270
|
+
remainder=group.key,
|
|
271
|
+
values=list(group),
|
|
272
|
+
))
|
|
249
273
|
.to_list()
|
|
250
274
|
)
|
|
275
|
+
|
|
251
276
|
```
|
|
252
277
|
|
|
253
278
|
Terminal operators instead evaluate the query and return a result rather than another query:
|
|
@@ -308,4 +333,32 @@ uv add flpit
|
|
|
308
333
|
|
|
309
334
|
## 📜 License
|
|
310
335
|
|
|
311
|
-
Distributed under the MIT License. See `LICENSE` for more information.
|
|
336
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
### Migrated .NET Unit Tests
|
|
340
|
+
|
|
341
|
+
> Migrated from the official .NET Runtime LINQ Tests.
|
|
342
|
+
> Note: The test cases exercise both core types, `FlpIt` and `FlpList`, with transitions between lazy and eager execution occurring as part of the individual test cases.
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
================================================================
|
|
346
|
+
| Test Module File | Test Cases |
|
|
347
|
+
----------------------------------------------------------------
|
|
348
|
+
| tests/nettests/ | |
|
|
349
|
+
| ├── test_all.py | 80 |
|
|
350
|
+
| ├── test_any.py | 90 |
|
|
351
|
+
| ├── test_average.py | 572 |
|
|
352
|
+
| ├── test_count.py | 37 |
|
|
353
|
+
| ├── test_generic_list.py | 95 |
|
|
354
|
+
| ├── test_last.py | 29 |
|
|
355
|
+
| ├── test_order_by.py | 100 |
|
|
356
|
+
| ├── test_order_descending_by.py | 45 |
|
|
357
|
+
| ├── test_sum.py | 134 |
|
|
358
|
+
| ├── test_take.py | 39 |
|
|
359
|
+
| ├── test_then_by.py | 40 |
|
|
360
|
+
| └── test_then_descending_by.py | 34 |
|
|
361
|
+
----------------------------------------------------------------
|
|
362
|
+
| GRAND TOTALS | 1295 |
|
|
363
|
+
================================================================
|
|
364
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flpit"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0.dev2"
|
|
4
4
|
description = "Fluent LINQ for Python — flip your iterables"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -27,6 +27,7 @@ dev = [
|
|
|
27
27
|
"pyrefly>=1.3.1",
|
|
28
28
|
"pyright>=1.1.414",
|
|
29
29
|
"pytest-benchmark>=5.3.0",
|
|
30
|
+
"mypy>=2.4.0",
|
|
30
31
|
]
|
|
31
32
|
|
|
32
33
|
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from
|
|
2
|
+
from typing import SupportsIndex
|
|
3
|
+
import decimal
|
|
4
|
+
from decimal import Decimal, Overflow, Inexact
|
|
3
5
|
|
|
4
6
|
import builtins
|
|
5
7
|
from collections import UserList
|
|
6
8
|
from functools import wraps
|
|
7
9
|
from itertools import islice
|
|
10
|
+
|
|
8
11
|
from typing import (
|
|
9
12
|
Any,
|
|
10
13
|
Callable,
|
|
11
14
|
Generic,
|
|
12
15
|
Iterable,
|
|
13
16
|
Iterator,
|
|
17
|
+
Collection,
|
|
14
18
|
List,
|
|
15
|
-
Optional,
|
|
16
|
-
Sequence,
|
|
17
19
|
Set,
|
|
18
20
|
Type,
|
|
19
21
|
TypeVar,
|
|
@@ -22,7 +24,6 @@ from typing import (
|
|
|
22
24
|
overload,
|
|
23
25
|
cast as typing_cast,
|
|
24
26
|
)
|
|
25
|
-
|
|
26
27
|
TItem = TypeVar("TItem")
|
|
27
28
|
TResult = TypeVar("TResult")
|
|
28
29
|
TKey = TypeVar("TKey")
|
|
@@ -63,6 +64,14 @@ class PredicateNoneError(TypeError):
|
|
|
63
64
|
def __init__(self):
|
|
64
65
|
super().__init__("Value cannot be None. (Argument 'predicate')")
|
|
65
66
|
|
|
67
|
+
class CollectionNoneError(TypeError):
|
|
68
|
+
def __init__(self):
|
|
69
|
+
super().__init__("Value cannot be None. (Argument 'collection')")
|
|
70
|
+
|
|
71
|
+
class SelectorNoneError(TypeError):
|
|
72
|
+
def __init__(self):
|
|
73
|
+
super().__init__("Value cannot be None. (Parameter 'keySelector')")
|
|
74
|
+
|
|
66
75
|
|
|
67
76
|
def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
68
77
|
"""Catches Python's native empty-sequence ValueError and re-raises LINQ-compliant error."""
|
|
@@ -71,12 +80,23 @@ def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
71
80
|
try:
|
|
72
81
|
return func(self, *args, **kwargs)
|
|
73
82
|
except ValueError as e:
|
|
74
|
-
if not self.data:
|
|
83
|
+
if not self._FlpList__list.data:
|
|
75
84
|
raise EmptySequenceError() from e
|
|
76
85
|
raise
|
|
77
86
|
return wrapper
|
|
78
87
|
|
|
79
88
|
|
|
89
|
+
# noinspection unresolved-references,protected-member
|
|
90
|
+
def dotnet_context() -> decimal._ContextManager: # awesome that type was not intended
|
|
91
|
+
# 1. Create and configure pure Context object
|
|
92
|
+
ctx = decimal.getcontext().copy()
|
|
93
|
+
ctx.prec = max(ctx.prec, 29)
|
|
94
|
+
ctx.traps[Overflow] = True
|
|
95
|
+
ctx.traps[Inexact] = True
|
|
96
|
+
|
|
97
|
+
return decimal.localcontext(ctx)
|
|
98
|
+
|
|
99
|
+
|
|
80
100
|
class _FactoryIterable(Iterable[TItem], Generic[TItem]):
|
|
81
101
|
"""
|
|
82
102
|
Wraps a generator factory function into a clean Iterable[T].
|
|
@@ -439,41 +459,61 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
439
459
|
return self._extreme(builtins.max, key=key_selector)
|
|
440
460
|
|
|
441
461
|
def average(
|
|
442
|
-
self, selector: Callable[[TItem],
|
|
443
|
-
) -> float:
|
|
462
|
+
self, selector: Callable[[TItem], int | float | Decimal] | _Sentinel = _SENTINEL
|
|
463
|
+
) -> float | int | Decimal | None:
|
|
444
464
|
"""Computes the arithmetic mean of the sequence, optionally applying a selector."""
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
query = (selector(x) for x in self) if selector is not _SENTINEL else self
|
|
448
|
-
for item in query:
|
|
449
|
-
total += float(item) # type: ignore[arg-type]
|
|
450
|
-
count += 1
|
|
451
|
-
if count == 0:
|
|
452
|
-
raise EmptySequenceError()
|
|
453
|
-
return total / count
|
|
465
|
+
with dotnet_context():
|
|
466
|
+
total, count = self.__sum_and_count(selector)
|
|
454
467
|
|
|
455
|
-
|
|
468
|
+
if total is None:
|
|
469
|
+
return None
|
|
456
470
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
total = 0.0
|
|
460
|
-
count = 0
|
|
461
|
-
for item in self:
|
|
462
|
-
total += float(key_selector(item))
|
|
463
|
-
count += 1
|
|
464
|
-
if count == 0:
|
|
465
|
-
raise EmptySequenceError()
|
|
466
|
-
return total / count
|
|
471
|
+
if isinstance(total, Decimal):
|
|
472
|
+
return total / Decimal(count)
|
|
467
473
|
|
|
468
|
-
|
|
474
|
+
assert total is not None
|
|
475
|
+
assert isinstance(total, Decimal) or isinstance(total, int) or isinstance(total, float)
|
|
476
|
+
return total / count
|
|
477
|
+
|
|
478
|
+
avg = average
|
|
469
479
|
|
|
470
480
|
def sum(
|
|
471
|
-
self, selector: Callable[[TItem],
|
|
472
|
-
) -> Union[int, float]:
|
|
481
|
+
self, selector: Callable[[TItem], int | float | Decimal] | _Sentinel = _SENTINEL
|
|
482
|
+
) -> Union[int, float, Decimal]:
|
|
473
483
|
"""Calculates the sum of the sequence, optionally applying a selector."""
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
484
|
+
with dotnet_context():
|
|
485
|
+
total, count = self.__sum_and_count(selector)
|
|
486
|
+
|
|
487
|
+
if count == 0:
|
|
488
|
+
return 0
|
|
489
|
+
|
|
490
|
+
assert total is not None
|
|
491
|
+
assert isinstance(total, Decimal) or isinstance(total, int) or isinstance(total, float)
|
|
492
|
+
return total
|
|
493
|
+
|
|
494
|
+
def __sum_and_count(self, selector: Callable[[TItem], int | float | Decimal]) -> tuple[Decimal | float | int | TItem | None, int]:
|
|
495
|
+
if selector is None:
|
|
496
|
+
raise SelectorNoneError()
|
|
497
|
+
|
|
498
|
+
total = None
|
|
499
|
+
count: int = 0
|
|
500
|
+
query = (selector(x) for x in self) if selector is not _SENTINEL else self
|
|
501
|
+
|
|
502
|
+
for item in query:
|
|
503
|
+
if item is not None:
|
|
504
|
+
if total is None:
|
|
505
|
+
total = item
|
|
506
|
+
else:
|
|
507
|
+
try:
|
|
508
|
+
total += item
|
|
509
|
+
except (Overflow, Inexact):
|
|
510
|
+
raise OverflowError(
|
|
511
|
+
"System.OverflowException: Value was either too large or too small for a Decimal."
|
|
512
|
+
)
|
|
513
|
+
count += 1
|
|
514
|
+
|
|
515
|
+
return total, count
|
|
516
|
+
|
|
477
517
|
|
|
478
518
|
def count(
|
|
479
519
|
self,
|
|
@@ -533,152 +573,85 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
533
573
|
"""Explicitly materializes the query into a FlpList."""
|
|
534
574
|
return FlpList(self)
|
|
535
575
|
|
|
576
|
+
from functools import partial
|
|
536
577
|
|
|
537
|
-
class
|
|
538
|
-
|
|
539
|
-
Shared lazy snapshot for an OrderBy/ThenBy chain.
|
|
540
|
-
|
|
541
|
-
The upstream source is consumed at most once. After materialization,
|
|
542
|
-
the original source reference is released.
|
|
543
|
-
"""
|
|
544
|
-
|
|
545
|
-
__slots__ = ("_source", "_items", "_lock")
|
|
546
|
-
|
|
547
|
-
def __init__(self, source: Iterable[TItem]) -> None:
|
|
548
|
-
self._source: Optional[Iterable[TItem]] = source
|
|
549
|
-
self._items: Optional[list[TItem]] = None
|
|
550
|
-
self._lock = Lock()
|
|
551
|
-
|
|
552
|
-
def materialize(self) -> list[TItem]:
|
|
553
|
-
items = self._items
|
|
554
|
-
|
|
555
|
-
if items is not None:
|
|
556
|
-
return items
|
|
557
|
-
|
|
558
|
-
with self._lock:
|
|
559
|
-
items = self._items
|
|
578
|
+
class _NoneOrderKey:
|
|
579
|
+
__slots__ = ()
|
|
560
580
|
|
|
561
|
-
|
|
562
|
-
|
|
581
|
+
def __lt__(self, other: object) -> bool:
|
|
582
|
+
return other is not self
|
|
563
583
|
|
|
564
|
-
|
|
565
|
-
|
|
584
|
+
def __gt__(self, other: object) -> bool:
|
|
585
|
+
return False
|
|
566
586
|
|
|
567
|
-
|
|
587
|
+
def __eq__(self, other: object) -> bool:
|
|
588
|
+
return other is self
|
|
568
589
|
|
|
569
|
-
self._source = None
|
|
570
|
-
self._items = items
|
|
571
590
|
|
|
572
|
-
|
|
591
|
+
_NONE_ORDER_KEY = _NoneOrderKey()
|
|
573
592
|
|
|
593
|
+
def _none_aware_key(
|
|
594
|
+
selector: Callable[[TItem], Any],
|
|
595
|
+
item: TItem,
|
|
596
|
+
) -> Any:
|
|
597
|
+
key = selector(item)
|
|
598
|
+
return _NONE_ORDER_KEY if key is None else key
|
|
574
599
|
|
|
575
600
|
class OrderedIt(FlpIt[TItem]):
|
|
576
|
-
""
|
|
577
|
-
Lazy, memoized ordered iterable.
|
|
578
|
-
|
|
579
|
-
The source is materialized once per OrderBy chain.
|
|
580
|
-
ThenBy nodes share that source snapshot but maintain independent
|
|
581
|
-
ordered-result caches.
|
|
582
|
-
"""
|
|
583
|
-
|
|
584
|
-
__slots__ = (
|
|
585
|
-
"_state",
|
|
586
|
-
"_criteria",
|
|
587
|
-
"_cached_result",
|
|
588
|
-
"_result_lock",
|
|
589
|
-
)
|
|
601
|
+
__slots__ = ("_criteria",)
|
|
590
602
|
|
|
591
603
|
def __init__(
|
|
592
604
|
self,
|
|
593
605
|
source: Iterable[TItem],
|
|
594
|
-
|
|
606
|
+
selector: Callable[[TItem], Any],
|
|
595
607
|
descending: bool = False,
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
self._state = _OrderState(source)
|
|
602
|
-
self._criteria: tuple[tuple[Callable[[TItem], Any], bool], ...] = (
|
|
603
|
-
(key_selector, descending),
|
|
604
|
-
)
|
|
608
|
+
*,
|
|
609
|
+
_criteria: tuple[tuple[Callable[[TItem], Any], bool], ...] | None = None,
|
|
610
|
+
):
|
|
611
|
+
if selector is None:
|
|
612
|
+
raise SelectorNoneError()
|
|
605
613
|
|
|
606
|
-
|
|
607
|
-
self._result_lock = Lock()
|
|
614
|
+
super().__init__(source)
|
|
608
615
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
key_selector: Callable[[TItem], Any],
|
|
614
|
-
descending: bool,
|
|
615
|
-
) -> OrderedIt[TItem]:
|
|
616
|
-
"""
|
|
617
|
-
Create a child ordering node sharing the parent's source state.
|
|
618
|
-
|
|
619
|
-
The FlpIt source is intentionally empty because OrderedIt overrides
|
|
620
|
-
__iter__ and uses _OrderState as its real source.
|
|
621
|
-
"""
|
|
622
|
-
child = cls.__new__(cls)
|
|
616
|
+
if _criteria is None:
|
|
617
|
+
self._criteria = ((selector, descending),)
|
|
618
|
+
else:
|
|
619
|
+
self._criteria = _criteria + ((selector, descending),)
|
|
623
620
|
|
|
624
|
-
|
|
625
|
-
|
|
621
|
+
@override
|
|
622
|
+
def __iter__(self) -> Iterator[TItem]:
|
|
623
|
+
items = list(self._iterable)
|
|
626
624
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
child._cached_result = None
|
|
633
|
-
child._result_lock = Lock()
|
|
625
|
+
for selector, descending in reversed(self._criteria):
|
|
626
|
+
items.sort(
|
|
627
|
+
key=partial(_none_aware_key, selector),
|
|
628
|
+
reverse=descending,
|
|
629
|
+
)
|
|
634
630
|
|
|
635
|
-
|
|
631
|
+
yield from items
|
|
636
632
|
|
|
637
633
|
def then_by(
|
|
638
634
|
self,
|
|
639
635
|
key_selector: Callable[[TItem], Any],
|
|
640
|
-
) -> OrderedIt[TItem]:
|
|
641
|
-
return
|
|
642
|
-
self,
|
|
636
|
+
) -> "OrderedIt[TItem]":
|
|
637
|
+
return OrderedIt(
|
|
638
|
+
self._iterable,
|
|
643
639
|
key_selector,
|
|
644
|
-
|
|
640
|
+
False,
|
|
641
|
+
_criteria=self._criteria,
|
|
645
642
|
)
|
|
646
643
|
|
|
647
644
|
def then_by_descending(
|
|
648
645
|
self,
|
|
649
646
|
key_selector: Callable[[TItem], Any],
|
|
650
|
-
) -> OrderedIt[TItem]:
|
|
651
|
-
return
|
|
652
|
-
self,
|
|
647
|
+
) -> "OrderedIt[TItem]":
|
|
648
|
+
return OrderedIt(
|
|
649
|
+
self._iterable,
|
|
653
650
|
key_selector,
|
|
654
|
-
|
|
651
|
+
True,
|
|
652
|
+
_criteria=self._criteria,
|
|
655
653
|
)
|
|
656
654
|
|
|
657
|
-
@override
|
|
658
|
-
def __iter__(self) -> Iterator[TItem]:
|
|
659
|
-
cached = self._cached_result
|
|
660
|
-
|
|
661
|
-
if cached is None:
|
|
662
|
-
with self._result_lock:
|
|
663
|
-
cached = self._cached_result
|
|
664
|
-
|
|
665
|
-
if cached is None:
|
|
666
|
-
# Never mutate the shared source snapshot.
|
|
667
|
-
result = list(self._state.materialize())
|
|
668
|
-
|
|
669
|
-
# Python sort is stable, so apply the least-significant
|
|
670
|
-
# criterion first.
|
|
671
|
-
for selector, descending in reversed(self._criteria):
|
|
672
|
-
result.sort(
|
|
673
|
-
key=selector,
|
|
674
|
-
reverse=descending,
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
cached = result
|
|
678
|
-
self._cached_result = cached
|
|
679
|
-
|
|
680
|
-
yield from cached
|
|
681
|
-
|
|
682
655
|
|
|
683
656
|
class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
|
|
684
657
|
"""Represents a collection of elements sharing a common key (.NET IGrouping<TKey, TElement>)."""
|
|
@@ -704,7 +677,7 @@ class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
|
|
|
704
677
|
return self.key == other.key and self.to_list() == other.to_list()
|
|
705
678
|
|
|
706
679
|
|
|
707
|
-
class FlpList(
|
|
680
|
+
class FlpList(Collection[TItem], Generic[TItem]):
|
|
708
681
|
"""
|
|
709
682
|
| Fluent List
|
|
710
683
|
A materialized list extending UserList that yields lazy FlpIt instances for query operations.
|
|
@@ -718,7 +691,88 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
718
691
|
else:
|
|
719
692
|
init_list = typing_cast(Iterable[TItem], source)
|
|
720
693
|
|
|
721
|
-
|
|
694
|
+
self.__list: UserList[TItem] = UserList[TItem](init_list)
|
|
695
|
+
|
|
696
|
+
# =====================================================================
|
|
697
|
+
# 1. THE ABC CONTRACT
|
|
698
|
+
# =====================================================================
|
|
699
|
+
def __iter__(self) -> Iterator[Any]:
|
|
700
|
+
return self.__list.__iter__()
|
|
701
|
+
|
|
702
|
+
def __len__(self) -> int:
|
|
703
|
+
"""Evaluates the stream to get total count."""
|
|
704
|
+
return self.__list.__len__()
|
|
705
|
+
|
|
706
|
+
def __contains__(self, item: TItem) -> bool:
|
|
707
|
+
return self.__list.__contains__(item)
|
|
708
|
+
|
|
709
|
+
# =====================================================================
|
|
710
|
+
# 2. Keep some of the Python UserList like behavior
|
|
711
|
+
# =====================================================================
|
|
712
|
+
|
|
713
|
+
def __repr__(self):
|
|
714
|
+
return self.__list.__repr__()
|
|
715
|
+
|
|
716
|
+
def __str__(self) -> str:
|
|
717
|
+
return self.__list.__str__()
|
|
718
|
+
|
|
719
|
+
def __eq__(self, other: Any) -> bool:
|
|
720
|
+
if isinstance(other, FlpList):
|
|
721
|
+
return self.__list.__eq__(other.__list)
|
|
722
|
+
return self.__list.__eq__(other)
|
|
723
|
+
|
|
724
|
+
def __lt__(self, other: Any) -> bool:
|
|
725
|
+
target = other.__list if isinstance(other, FlpList) else other
|
|
726
|
+
return self.__list.__lt__(target)
|
|
727
|
+
|
|
728
|
+
def __le__(self, other: Any) -> bool:
|
|
729
|
+
target = other.__list if isinstance(other, FlpList) else other
|
|
730
|
+
return self.__list.__le__(target)
|
|
731
|
+
|
|
732
|
+
def __gt__(self, other: Any) -> bool:
|
|
733
|
+
target = other.__list if isinstance(other, FlpList) else other
|
|
734
|
+
return self.__list.__gt__(target)
|
|
735
|
+
|
|
736
|
+
def __ge__(self, other: Any) -> bool:
|
|
737
|
+
target = other.__list if isinstance(other, FlpList) else other
|
|
738
|
+
return self.__list.__ge__(target)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
@overload
|
|
742
|
+
def __getitem__(self, index: SupportsIndex) -> TItem: ...
|
|
743
|
+
|
|
744
|
+
@overload
|
|
745
|
+
def __getitem__(self, index: slice) -> FlpList[TItem]: ...
|
|
746
|
+
|
|
747
|
+
def __getitem__(self, index: SupportsIndex | slice):
|
|
748
|
+
if isinstance(index, slice):
|
|
749
|
+
return self.__class__(self.__list[index])
|
|
750
|
+
else:
|
|
751
|
+
return self.__list[index]
|
|
752
|
+
|
|
753
|
+
@overload
|
|
754
|
+
def __setitem__(self, index: SupportsIndex, value: TItem) -> None: ...
|
|
755
|
+
|
|
756
|
+
@overload
|
|
757
|
+
def __setitem__(self, index: slice, value: Iterable[TItem]) -> None: ...
|
|
758
|
+
|
|
759
|
+
def __setitem__(
|
|
760
|
+
self,
|
|
761
|
+
index: SupportsIndex | slice,
|
|
762
|
+
value: TItem | Iterable[TItem],
|
|
763
|
+
) -> None:
|
|
764
|
+
if isinstance(index, slice):
|
|
765
|
+
if not isinstance(value, Iterable):
|
|
766
|
+
raise TypeError("slice assignment requires an iterable value")
|
|
767
|
+
self.__list.data[index] = value
|
|
768
|
+
else:
|
|
769
|
+
if isinstance(value, Iterable):
|
|
770
|
+
raise TypeError("item assignment requires a non-iterable value")
|
|
771
|
+
self.__list.data[index] = value
|
|
772
|
+
|
|
773
|
+
# =====================================================================
|
|
774
|
+
# 3. Put .NET List<T> mechanics on top
|
|
775
|
+
# =====================================================================
|
|
722
776
|
|
|
723
777
|
def add(self, item: TItem) -> None:
|
|
724
778
|
"""
|
|
@@ -728,9 +782,9 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
728
782
|
- type checks
|
|
729
783
|
- manual of_type(...) filter if you don't trust your checks
|
|
730
784
|
"""
|
|
731
|
-
self.data.append(item)
|
|
785
|
+
self.__list.data.append(item)
|
|
732
786
|
|
|
733
|
-
def add_range(self,
|
|
787
|
+
def add_range(self, collection: Iterable[TItem]) -> None:
|
|
734
788
|
"""
|
|
735
789
|
| Adds an Iterable sequence or stream.
|
|
736
790
|
Optimizes paths based on input type without destroying volatile generators.
|
|
@@ -739,21 +793,131 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
739
793
|
- type checks
|
|
740
794
|
- manual of_type(...) filter if you don't trust your checks
|
|
741
795
|
"""
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
796
|
+
if collection is None:
|
|
797
|
+
raise CollectionNoneError()
|
|
798
|
+
|
|
799
|
+
if isinstance(collection, UserList):
|
|
800
|
+
self.__list.data.extend(collection.data)
|
|
801
|
+
elif isinstance(collection, FlpList):
|
|
802
|
+
self.__list.data.extend(collection.__list.data)
|
|
803
|
+
else:
|
|
804
|
+
self.__list.data.extend(collection)
|
|
805
|
+
|
|
806
|
+
def clear(self) -> None:
|
|
807
|
+
self.__list.data.clear()
|
|
808
|
+
|
|
809
|
+
def contains(self, item: TItem) -> bool:
|
|
810
|
+
return item in self.__list.data
|
|
811
|
+
|
|
812
|
+
def exists(self, predicate: Callable[[TItem], bool]) -> bool:
|
|
813
|
+
return any(predicate(item) for item in self.__list.data)
|
|
814
|
+
|
|
815
|
+
def find(self, predicate: Callable[[TItem], bool]) -> TItem | None:
|
|
816
|
+
if predicate is None:
|
|
817
|
+
raise PredicateNoneError()
|
|
818
|
+
|
|
819
|
+
for item in self.__list.data:
|
|
820
|
+
if predicate(item):
|
|
821
|
+
return item
|
|
822
|
+
|
|
823
|
+
return None
|
|
824
|
+
|
|
825
|
+
def find_all(self, predicate: Callable[[TItem], bool]) -> FlpList[TItem]:
|
|
826
|
+
if predicate is None:
|
|
827
|
+
raise PredicateNoneError()
|
|
828
|
+
|
|
829
|
+
return FlpList(item for item in self.__list.data if predicate(item))
|
|
746
830
|
|
|
747
|
-
|
|
748
|
-
|
|
831
|
+
def insert(self, index: int, item: TItem) -> None:
|
|
832
|
+
if index < 0 or index > len(self.__list.data):
|
|
833
|
+
raise IndexError(f"Index {index} is out of range.")
|
|
834
|
+
|
|
835
|
+
self.__list.data.insert(index, item)
|
|
836
|
+
|
|
837
|
+
def insert_range(self, index: int, collection: Iterable[TItem]) -> None:
|
|
838
|
+
if collection is None:
|
|
839
|
+
raise CollectionNoneError()
|
|
840
|
+
|
|
841
|
+
if index < 0 or index > len(self.__list.data):
|
|
842
|
+
raise IndexError(f"Index {index} is out of range.")
|
|
843
|
+
|
|
844
|
+
self.__list.data[index:index] = list(collection)
|
|
845
|
+
|
|
846
|
+
def remove(self, item: TItem) -> bool:
|
|
749
847
|
try:
|
|
750
|
-
|
|
751
|
-
|
|
848
|
+
self.__list.data.remove(item)
|
|
849
|
+
return True
|
|
850
|
+
except ValueError:
|
|
851
|
+
return False
|
|
852
|
+
|
|
853
|
+
def remove_all(self, predicate: Callable[[TItem], bool]) -> int:
|
|
854
|
+
if predicate is None:
|
|
855
|
+
raise PredicateNoneError()
|
|
856
|
+
|
|
857
|
+
original_length = len(self.__list.data)
|
|
858
|
+
|
|
859
|
+
self.__list.data[:] = [
|
|
860
|
+
item
|
|
861
|
+
for item in self.__list.data
|
|
862
|
+
if not predicate(item)
|
|
863
|
+
]
|
|
864
|
+
|
|
865
|
+
return original_length - len(self.__list.data)
|
|
866
|
+
|
|
867
|
+
def remove_at(self, index: int) -> None:
|
|
868
|
+
if index < 0 or index > len(self.__list.data):
|
|
869
|
+
raise IndexError(f"Index {index} is out of range.")
|
|
870
|
+
|
|
871
|
+
del self.__list.data[index]
|
|
872
|
+
|
|
873
|
+
def remove_range(self, index: int, count: int) -> None:
|
|
874
|
+
size = len(self.__list.data)
|
|
875
|
+
|
|
876
|
+
if index < 0 or index > size:
|
|
877
|
+
raise IndexError("index")
|
|
878
|
+
|
|
879
|
+
if count < 0:
|
|
880
|
+
raise IndexError("count")
|
|
881
|
+
|
|
882
|
+
if count > size - index:
|
|
883
|
+
raise IndexError("The range is invalid.")
|
|
884
|
+
|
|
885
|
+
del self.__list.data[index : index + count]
|
|
886
|
+
|
|
887
|
+
@overload
|
|
888
|
+
def reverse(self) -> None: ...
|
|
889
|
+
|
|
890
|
+
@overload
|
|
891
|
+
def reverse(self, index: int, count: int) -> None: ...
|
|
892
|
+
|
|
893
|
+
def reverse(self, index: int = 0, count: int | None = None) -> None:
|
|
894
|
+
if count is None:
|
|
895
|
+
self.__list.data.reverse()
|
|
752
896
|
return
|
|
753
897
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
898
|
+
size = len(self.__list.data)
|
|
899
|
+
|
|
900
|
+
# Apply your List<T>-compatible argument validation here.
|
|
901
|
+
if index < 0 or index > size:
|
|
902
|
+
raise IndexError("Non-negative number required.")
|
|
903
|
+
|
|
904
|
+
if count < 0:
|
|
905
|
+
raise IndexError("Non-negative number required.")
|
|
906
|
+
|
|
907
|
+
if index + count > size:
|
|
908
|
+
raise IndexError(
|
|
909
|
+
"Offset and length were out of bounds for the array or "
|
|
910
|
+
"count is greater than the number of elements from index "
|
|
911
|
+
"to the end of the source collection."
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
self.__list.data[index:index + count] = reversed(
|
|
915
|
+
self.__list.data[index:index + count]
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
# =====================================================================
|
|
919
|
+
# 4. IEnumerable add-ons
|
|
920
|
+
# =====================================================================
|
|
757
921
|
|
|
758
922
|
@overload
|
|
759
923
|
def any(self) -> bool: ...
|
|
@@ -766,55 +930,55 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
766
930
|
predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
|
|
767
931
|
) -> bool:
|
|
768
932
|
if predicate is _SENTINEL:
|
|
769
|
-
return len(self.data) > 0
|
|
933
|
+
return len(self.__list.data) > 0
|
|
770
934
|
|
|
771
|
-
return any(predicate(item) for item in self.data)
|
|
935
|
+
return any(predicate(item) for item in self.__list.data)
|
|
772
936
|
|
|
773
937
|
def all(
|
|
774
938
|
self,
|
|
775
939
|
predicate: Callable[[TItem], bool],
|
|
776
940
|
) -> bool:
|
|
777
|
-
return all(predicate(item) for item in self.data)
|
|
941
|
+
return all(predicate(item) for item in self.__list.data)
|
|
778
942
|
|
|
779
|
-
def
|
|
943
|
+
def append(self, element: TItem) -> FlpIt[TItem]:
|
|
780
944
|
"""Appends an element to the sequence lazily, returning a FlpIt without mutating this list."""
|
|
781
|
-
return FlpIt(self.data).append(element)
|
|
945
|
+
return FlpIt(self.__list.data).append(element)
|
|
782
946
|
|
|
783
947
|
def prepend(self, element: TItem) -> FlpIt[TItem]:
|
|
784
948
|
"""Prepends an element to the sequence lazily, returning a FlpIt without mutating this list."""
|
|
785
|
-
return FlpIt(self.data).prepend(element)
|
|
949
|
+
return FlpIt(self.__list.data).prepend(element)
|
|
786
950
|
|
|
787
951
|
# noinspection unused-parameter
|
|
788
952
|
def as_type(self, target_type: Type[TResult]) -> FlpList[TResult]:
|
|
789
953
|
return self # type: ignore[return-value]
|
|
790
954
|
|
|
791
955
|
def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
|
|
792
|
-
return FlpIt(self.data).where(predicate)
|
|
956
|
+
return FlpIt(self.__list.data).where(predicate)
|
|
793
957
|
|
|
794
958
|
def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
|
|
795
|
-
return FlpIt(self.data).select(selector)
|
|
959
|
+
return FlpIt(self.__list.data).select(selector)
|
|
796
960
|
|
|
797
961
|
def select_many(
|
|
798
962
|
self, selector: Callable[[TItem], Iterable[TResult]]
|
|
799
963
|
) -> FlpIt[TResult]:
|
|
800
|
-
return FlpIt(self.data).select_many(selector)
|
|
964
|
+
return FlpIt(self.__list.data).select_many(selector)
|
|
801
965
|
|
|
802
966
|
def take(self, count: int) -> FlpIt[TItem]:
|
|
803
|
-
return FlpIt(self.data).take(count)
|
|
967
|
+
return FlpIt(self.__list.data).take(count)
|
|
804
968
|
|
|
805
969
|
def cast(self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
806
|
-
return FlpIt(self.data).cast(target_type)
|
|
970
|
+
return FlpIt(self.__list.data).cast(target_type)
|
|
807
971
|
|
|
808
972
|
def of_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
809
|
-
return FlpIt(self.data).of_type(target_type)
|
|
973
|
+
return FlpIt(self.__list.data).of_type(target_type)
|
|
810
974
|
|
|
811
975
|
def distinct(self) -> FlpIt[TItem]:
|
|
812
|
-
return FlpIt(self.data).distinct()
|
|
976
|
+
return FlpIt(self.__list.data).distinct()
|
|
813
977
|
|
|
814
978
|
def distinct_by(
|
|
815
979
|
self, key_selector: Callable[[TItem], TKey]
|
|
816
980
|
) -> FlpIt[TItem]:
|
|
817
|
-
return FlpIt(self.data).distinct_by(key_selector)
|
|
981
|
+
return FlpIt(self.__list.data).distinct_by(key_selector)
|
|
818
982
|
|
|
819
983
|
@overload
|
|
820
984
|
def zip(self, second: Iterable[TOther]) -> FlpIt[tuple[TItem, TOther]]: ...
|
|
@@ -831,25 +995,25 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
831
995
|
second: Iterable[TOther],
|
|
832
996
|
result_selector: Callable[[TItem, TOther], Any] | _Sentinel = _SENTINEL,
|
|
833
997
|
) -> FlpIt[Any]:
|
|
834
|
-
return FlpIt(self.data).zip(second, result_selector)
|
|
998
|
+
return FlpIt(self.__list.data).zip(second, result_selector)
|
|
835
999
|
|
|
836
1000
|
def chunk(self, size: int) -> FlpIt[FlpList[TItem]]:
|
|
837
|
-
return FlpIt(self.data).chunk(size)
|
|
1001
|
+
return FlpIt(self.__list.data).chunk(size)
|
|
838
1002
|
|
|
839
1003
|
def order_by(
|
|
840
1004
|
self, key_selector: Callable[[TItem], Any]
|
|
841
1005
|
) -> OrderedIt[TItem]:
|
|
842
|
-
return FlpIt(self.data).order_by(key_selector)
|
|
1006
|
+
return FlpIt(self.__list.data).order_by(key_selector)
|
|
843
1007
|
|
|
844
1008
|
def order_by_descending(
|
|
845
1009
|
self, key_selector: Callable[[TItem], Any]
|
|
846
1010
|
) -> OrderedIt[TItem]:
|
|
847
|
-
return FlpIt(self.data).order_by_descending(key_selector)
|
|
1011
|
+
return FlpIt(self.__list.data).order_by_descending(key_selector)
|
|
848
1012
|
|
|
849
1013
|
def group_by(
|
|
850
1014
|
self, key_selector: Callable[[TItem], TKey]
|
|
851
1015
|
) -> FlpIt[Grouping[TKey, TItem]]:
|
|
852
|
-
return FlpIt(self.data).group_by(key_selector)
|
|
1016
|
+
return FlpIt(self.__list.data).group_by(key_selector)
|
|
853
1017
|
|
|
854
1018
|
@overload
|
|
855
1019
|
def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
|
|
@@ -864,41 +1028,37 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
864
1028
|
func: Callable[[Any, Any], Any],
|
|
865
1029
|
seed: Any = _SENTINEL,
|
|
866
1030
|
) -> TItem | TAccumulate:
|
|
867
|
-
return FlpIt(self.data).aggregate(func, seed=seed)
|
|
1031
|
+
return FlpIt(self.__list.data).aggregate(func, seed=seed)
|
|
868
1032
|
|
|
869
1033
|
@_guard_empty
|
|
870
1034
|
def min(self) -> TItem:
|
|
871
|
-
return builtins.min(self.data) # pyrefly: ignore [bad-specialization]
|
|
1035
|
+
return builtins.min(self.__list.data) # pyrefly: ignore [bad-specialization]
|
|
872
1036
|
|
|
873
1037
|
@_guard_empty
|
|
874
1038
|
def min_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
|
|
875
|
-
return builtins.min(self.data, key=key_selector)
|
|
1039
|
+
return builtins.min(self.__list.data, key=key_selector)
|
|
876
1040
|
|
|
877
1041
|
@_guard_empty
|
|
878
1042
|
def max(self) -> TItem:
|
|
879
|
-
return builtins.max(self.data) # pyrefly: ignore [bad-specialization]
|
|
1043
|
+
return builtins.max(self.__list.data) # pyrefly: ignore [bad-specialization]
|
|
880
1044
|
|
|
881
1045
|
@_guard_empty
|
|
882
1046
|
def max_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
|
|
883
|
-
return builtins.max(self.data, key=key_selector)
|
|
1047
|
+
return builtins.max(self.__list.data, key=key_selector)
|
|
884
1048
|
|
|
885
1049
|
def sum(
|
|
886
|
-
self, selector: Callable[[TItem], int | float] | _Sentinel = _SENTINEL
|
|
887
|
-
) -> int | float:
|
|
1050
|
+
self, selector: Callable[[TItem], int | float | Decimal] | _Sentinel = _SENTINEL
|
|
1051
|
+
) -> int | float | Decimal:
|
|
888
1052
|
"""Calculates the sum of elements, optionally applying a selector."""
|
|
889
|
-
return FlpIt(self.data).sum(selector)
|
|
1053
|
+
return FlpIt(self.__list.data).sum(selector)
|
|
890
1054
|
|
|
891
1055
|
def average(
|
|
892
|
-
self, selector: Callable[[TItem], int | float] | _Sentinel = _SENTINEL
|
|
893
|
-
) -> float:
|
|
1056
|
+
self, selector: Callable[[TItem], int | float | Decimal] | _Sentinel = _SENTINEL
|
|
1057
|
+
) -> float | int | Decimal | None:
|
|
894
1058
|
"""Calculates the arithmetic mean, optionally applying a selector."""
|
|
895
|
-
return FlpIt(self.data).average(selector)
|
|
1059
|
+
return FlpIt(self.__list.data).average(selector)
|
|
896
1060
|
|
|
897
|
-
def
|
|
898
|
-
return self.data.count(item) # just redirect how the count redirected before
|
|
899
|
-
|
|
900
|
-
@override
|
|
901
|
-
def count( # type: ignore # pyrefly: ignore [bad-override-param-name] # pyright: ignore[reportIncompatibleMethodOverride]
|
|
1061
|
+
def count(
|
|
902
1062
|
self,
|
|
903
1063
|
predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
|
|
904
1064
|
) -> int:
|
|
@@ -906,22 +1066,21 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
906
1066
|
Counts elements using LINQ semantics.
|
|
907
1067
|
|
|
908
1068
|
Supported forms:
|
|
909
|
-
|
|
910
|
-
count()
|
|
911
|
-
count(predicate) -> number of elements satisfying the predicate
|
|
1069
|
+
- count() -> total number of elements
|
|
1070
|
+
- count(predicate) -> number of elements satisfying the predicate
|
|
912
1071
|
"""
|
|
913
1072
|
if predicate is _SENTINEL:
|
|
914
|
-
return len(self.data)
|
|
1073
|
+
return len(self.__list.data)
|
|
915
1074
|
|
|
916
|
-
return FlpIt(self.data).count(predicate)
|
|
1075
|
+
return FlpIt(self.__list.data).count(predicate)
|
|
917
1076
|
|
|
918
1077
|
def element_at(self, index: int) -> TItem:
|
|
919
|
-
if index < 0 or index >= len(self.data):
|
|
1078
|
+
if index < 0 or index >= len(self.__list.data):
|
|
920
1079
|
raise IndexError("Index out of range")
|
|
921
|
-
return self.data[index]
|
|
1080
|
+
return self.__list.data[index]
|
|
922
1081
|
|
|
923
1082
|
def first(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
|
|
924
|
-
return FlpIt(self.data).first(predicate)
|
|
1083
|
+
return FlpIt(self.__list.data).first(predicate)
|
|
925
1084
|
|
|
926
1085
|
@overload
|
|
927
1086
|
def last(self) -> TItem: ...
|
|
@@ -948,11 +1107,11 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
948
1107
|
raise PredicateNoneError()
|
|
949
1108
|
|
|
950
1109
|
if predicate is _SENTINEL:
|
|
951
|
-
if not self.data:
|
|
1110
|
+
if not self.__list.data:
|
|
952
1111
|
raise EmptySequenceError()
|
|
953
|
-
return self.data[-1]
|
|
1112
|
+
return self.__list.data[-1]
|
|
954
1113
|
|
|
955
|
-
for item in reversed(self.data):
|
|
1114
|
+
for item in reversed(self.__list.data):
|
|
956
1115
|
if predicate(item):
|
|
957
1116
|
return item
|
|
958
1117
|
|
|
@@ -961,11 +1120,11 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
961
1120
|
def first_or_default(
|
|
962
1121
|
self, default: TResult, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL
|
|
963
1122
|
) -> TItem | TResult:
|
|
964
|
-
return FlpIt(self.data).first_or_default(default, predicate)
|
|
1123
|
+
return FlpIt(self.__list.data).first_or_default(default, predicate)
|
|
965
1124
|
|
|
966
1125
|
def single(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
|
|
967
|
-
return FlpIt(self.data).single(predicate)
|
|
1126
|
+
return FlpIt(self.__list.data).single(predicate)
|
|
968
1127
|
|
|
969
1128
|
def to_list(self) -> FlpList[TItem]:
|
|
970
1129
|
"""Explicitly returns a shallow copy instance to isolate mutations."""
|
|
971
|
-
return FlpList(self.data.copy())
|
|
1130
|
+
return FlpList(self.__list.data.copy())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|