flpit 0.1.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flpit
3
- Version: 0.1.7
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 where the underlying operation permits it. `FlpIt` wraps a standard Python `Iterable[T]`.
34
- * **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
35
- * **🎯 LINQ Semantics:** Operators such as `.any()`, `.first()`, `.single()`, `.order_by()`, and `.then_by()` follow .NET 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 from Python List Contracts (`.count()`)
201
+ ## Intentional Semantic Deviation: `count()`
190
202
 
191
- To faithfully provide standard .NET LINQ semantics, `FlpList` deliberately uses LINQ-style count() semantics instead of Python list.count(value) semantics of Python's standard `list.count(value)`.
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
- * **Vanilla Python `list.count(value)`** counts occurrences of a specific *value*.
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
- ```python
198
- # Pure LINQ semantics instead of standard list behavior
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
- Because `FlpList` is an explicit, fluent wrapper designed to enforce LINQ behavior, this signature change is a deliberate design choice. If you need the native Python behavior to count occurrences of a specific item value, use the explicitly provided fallback method:
211
+ In LINQ, `Count()` returns the total number of elements, or elements matching a predicate:
203
212
 
204
- ```python
205
- # Use the fallback for traditional Python value-counting
206
- occurrences = eager_list.count_item(4)
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
- groups = query.group_by(lambda x: x % 3)
280
+ from typing import NamedTuple
281
+
282
+ class GroupResult(NamedTuple):
283
+ remainder: int
284
+ values: list[int]
264
285
 
265
286
  result = (
266
- groups
267
- .where(...)
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 where the underlying operation permits it. `FlpIt` wraps a standard Python `Iterable[T]`.
14
- * **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
15
- * **🎯 LINQ Semantics:** Operators such as `.any()`, `.first()`, `.single()`, `.order_by()`, and `.then_by()` follow .NET 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 from Python List Contracts (`.count()`)
181
+ ## Intentional Semantic Deviation: `count()`
170
182
 
171
- To faithfully provide standard .NET LINQ semantics, `FlpList` deliberately uses LINQ-style count() semantics instead of Python list.count(value) semantics of Python's standard `list.count(value)`.
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
- * **Vanilla Python `list.count(value)`** counts occurrences of a specific *value*.
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
- ```python
178
- # Pure LINQ semantics instead of standard list behavior
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
- Because `FlpList` is an explicit, fluent wrapper designed to enforce LINQ behavior, this signature change is a deliberate design choice. If you need the native Python behavior to count occurrences of a specific item value, use the explicitly provided fallback method:
191
+ In LINQ, `Count()` returns the total number of elements, or elements matching a predicate:
183
192
 
184
- ```python
185
- # Use the fallback for traditional Python value-counting
186
- occurrences = eager_list.count_item(4)
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
- groups = query.group_by(lambda x: x % 3)
260
+ from typing import NamedTuple
261
+
262
+ class GroupResult(NamedTuple):
263
+ remainder: int
264
+ values: list[int]
244
265
 
245
266
  result = (
246
- groups
247
- .where(...)
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.1.7"
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 threading import Lock
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,9 +64,13 @@ 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
+
66
71
  class SelectorNoneError(TypeError):
67
72
  def __init__(self):
68
- super().__init__("Value cannot be null. (Parameter 'keySelector')")
73
+ super().__init__("Value cannot be None. (Parameter 'keySelector')")
69
74
 
70
75
 
71
76
  def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
@@ -75,12 +80,23 @@ def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
75
80
  try:
76
81
  return func(self, *args, **kwargs)
77
82
  except ValueError as e:
78
- if not self.data:
83
+ if not self._FlpList__list.data:
79
84
  raise EmptySequenceError() from e
80
85
  raise
81
86
  return wrapper
82
87
 
83
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
+
84
100
  class _FactoryIterable(Iterable[TItem], Generic[TItem]):
85
101
  """
86
102
  Wraps a generator factory function into a clean Iterable[T].
@@ -443,41 +459,61 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
443
459
  return self._extreme(builtins.max, key=key_selector)
444
460
 
445
461
  def average(
446
- self, selector: Callable[[TItem], Union[int, float]] | _Sentinel = _SENTINEL
447
- ) -> float:
462
+ self, selector: Callable[[TItem], int | float | Decimal] | _Sentinel = _SENTINEL
463
+ ) -> float | int | Decimal | None:
448
464
  """Computes the arithmetic mean of the sequence, optionally applying a selector."""
449
- total = 0.0
450
- count = 0
451
- query = (selector(x) for x in self) if selector is not _SENTINEL else self
452
- for item in query:
453
- total += float(item) # type: ignore[arg-type]
454
- count += 1
455
- if count == 0:
456
- raise EmptySequenceError()
457
- return total / count
465
+ with dotnet_context():
466
+ total, count = self.__sum_and_count(selector)
458
467
 
459
- avg = average
468
+ if total is None:
469
+ return None
460
470
 
461
- def average_by(self, key_selector: Callable[[TItem], Union[int, float]]) -> float:
462
- """Computes the average of a sequence of numeric values projected by a key selector."""
463
- total = 0.0
464
- count = 0
465
- for item in self:
466
- total += float(key_selector(item))
467
- count += 1
468
- if count == 0:
469
- raise EmptySequenceError()
470
- return total / count
471
+ if isinstance(total, Decimal):
472
+ return total / Decimal(count)
473
+
474
+ assert total is not None
475
+ assert isinstance(total, Decimal) or isinstance(total, int) or isinstance(total, float)
476
+ return total / count
471
477
 
472
- avg_by = average_by
478
+ avg = average
473
479
 
474
480
  def sum(
475
- self, selector: Callable[[TItem], Union[int, float]] | _Sentinel = _SENTINEL
476
- ) -> Union[int, float]:
481
+ self, selector: Callable[[TItem], int | float | Decimal] | _Sentinel = _SENTINEL
482
+ ) -> Union[int, float, Decimal]:
477
483
  """Calculates the sum of the sequence, optionally applying a selector."""
478
- if selector is not _SENTINEL:
479
- return builtins.sum(selector(x) for x in self)
480
- return builtins.sum(self) # type: ignore[arg-type]
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
+
481
517
 
482
518
  def count(
483
519
  self,
@@ -582,31 +618,7 @@ class OrderedIt(FlpIt[TItem]):
582
618
  else:
583
619
  self._criteria = _criteria + ((selector, descending),)
584
620
 
585
- # def __iter__(self) -> Iterator[TItem]:
586
- # items = list(self._iterable)
587
- #
588
- # for selector, descending in reversed(self._criteria):
589
- # keys = [selector(item) for item in items]
590
- #
591
- # if any(key is None for key in keys):
592
- # # Null-aware path
593
- # items = [
594
- # item
595
- # for _, item in sorted(
596
- # zip(keys, items),
597
- # key=lambda pair: (
598
- # pair[0] is not None,
599
- # pair[0] if pair[0] is not None else 0,
600
- # ),
601
- # reverse=descending,
602
- # )
603
- # ]
604
- # else:
605
- # # Fast path
606
- # items.sort(key=selector, reverse=descending)
607
- #
608
- # yield from items
609
-
621
+ @override
610
622
  def __iter__(self) -> Iterator[TItem]:
611
623
  items = list(self._iterable)
612
624
 
@@ -665,7 +677,7 @@ class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
665
677
  return self.key == other.key and self.to_list() == other.to_list()
666
678
 
667
679
 
668
- class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
680
+ class FlpList(Collection[TItem], Generic[TItem]):
669
681
  """
670
682
  | Fluent List
671
683
  A materialized list extending UserList that yields lazy FlpIt instances for query operations.
@@ -679,7 +691,88 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
679
691
  else:
680
692
  init_list = typing_cast(Iterable[TItem], source)
681
693
 
682
- super().__init__(init_list)
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
+ # =====================================================================
683
776
 
684
777
  def add(self, item: TItem) -> None:
685
778
  """
@@ -689,9 +782,9 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
689
782
  - type checks
690
783
  - manual of_type(...) filter if you don't trust your checks
691
784
  """
692
- self.data.append(item)
785
+ self.__list.data.append(item)
693
786
 
694
- def add_range(self, items: Iterable[TItem]) -> None:
787
+ def add_range(self, collection: Iterable[TItem]) -> None:
695
788
  """
696
789
  | Adds an Iterable sequence or stream.
697
790
  Optimizes paths based on input type without destroying volatile generators.
@@ -700,21 +793,131 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
700
793
  - type checks
701
794
  - manual of_type(...) filter if you don't trust your checks
702
795
  """
703
- # 1. Optimized Path: Fast memory extensions for pre-materialized sequences
704
- if isinstance(items, (Sequence, list, tuple, UserList)):
705
- self.data.extend(items)
706
- return
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
707
824
 
708
- # 2. Stream Path: Volatile one-shot generator handling
709
- it = iter(items)
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))
830
+
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:
710
847
  try:
711
- first_item = next(it)
712
- except StopIteration:
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()
713
896
  return
714
897
 
715
- # Append the tracked peek-element and stream the remainder safely
716
- self.data.append(first_item)
717
- self.data.extend(it)
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
+ # =====================================================================
718
921
 
719
922
  @overload
720
923
  def any(self) -> bool: ...
@@ -727,55 +930,55 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
727
930
  predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
728
931
  ) -> bool:
729
932
  if predicate is _SENTINEL:
730
- return len(self.data) > 0
933
+ return len(self.__list.data) > 0
731
934
 
732
- return any(predicate(item) for item in self.data)
935
+ return any(predicate(item) for item in self.__list.data)
733
936
 
734
937
  def all(
735
938
  self,
736
939
  predicate: Callable[[TItem], bool],
737
940
  ) -> bool:
738
- return all(predicate(item) for item in self.data)
941
+ return all(predicate(item) for item in self.__list.data)
739
942
 
740
- def append_linq(self, element: TItem) -> FlpIt[TItem]:
943
+ def append(self, element: TItem) -> FlpIt[TItem]:
741
944
  """Appends an element to the sequence lazily, returning a FlpIt without mutating this list."""
742
- return FlpIt(self.data).append(element)
945
+ return FlpIt(self.__list.data).append(element)
743
946
 
744
947
  def prepend(self, element: TItem) -> FlpIt[TItem]:
745
948
  """Prepends an element to the sequence lazily, returning a FlpIt without mutating this list."""
746
- return FlpIt(self.data).prepend(element)
949
+ return FlpIt(self.__list.data).prepend(element)
747
950
 
748
951
  # noinspection unused-parameter
749
952
  def as_type(self, target_type: Type[TResult]) -> FlpList[TResult]:
750
953
  return self # type: ignore[return-value]
751
954
 
752
955
  def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
753
- return FlpIt(self.data).where(predicate)
956
+ return FlpIt(self.__list.data).where(predicate)
754
957
 
755
958
  def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
756
- return FlpIt(self.data).select(selector)
959
+ return FlpIt(self.__list.data).select(selector)
757
960
 
758
961
  def select_many(
759
962
  self, selector: Callable[[TItem], Iterable[TResult]]
760
963
  ) -> FlpIt[TResult]:
761
- return FlpIt(self.data).select_many(selector)
964
+ return FlpIt(self.__list.data).select_many(selector)
762
965
 
763
966
  def take(self, count: int) -> FlpIt[TItem]:
764
- return FlpIt(self.data).take(count)
967
+ return FlpIt(self.__list.data).take(count)
765
968
 
766
969
  def cast(self, target_type: Type[TResult]) -> FlpIt[TResult]:
767
- return FlpIt(self.data).cast(target_type)
970
+ return FlpIt(self.__list.data).cast(target_type)
768
971
 
769
972
  def of_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
770
- return FlpIt(self.data).of_type(target_type)
973
+ return FlpIt(self.__list.data).of_type(target_type)
771
974
 
772
975
  def distinct(self) -> FlpIt[TItem]:
773
- return FlpIt(self.data).distinct()
976
+ return FlpIt(self.__list.data).distinct()
774
977
 
775
978
  def distinct_by(
776
979
  self, key_selector: Callable[[TItem], TKey]
777
980
  ) -> FlpIt[TItem]:
778
- return FlpIt(self.data).distinct_by(key_selector)
981
+ return FlpIt(self.__list.data).distinct_by(key_selector)
779
982
 
780
983
  @overload
781
984
  def zip(self, second: Iterable[TOther]) -> FlpIt[tuple[TItem, TOther]]: ...
@@ -792,25 +995,25 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
792
995
  second: Iterable[TOther],
793
996
  result_selector: Callable[[TItem, TOther], Any] | _Sentinel = _SENTINEL,
794
997
  ) -> FlpIt[Any]:
795
- return FlpIt(self.data).zip(second, result_selector)
998
+ return FlpIt(self.__list.data).zip(second, result_selector)
796
999
 
797
1000
  def chunk(self, size: int) -> FlpIt[FlpList[TItem]]:
798
- return FlpIt(self.data).chunk(size)
1001
+ return FlpIt(self.__list.data).chunk(size)
799
1002
 
800
1003
  def order_by(
801
1004
  self, key_selector: Callable[[TItem], Any]
802
1005
  ) -> OrderedIt[TItem]:
803
- return FlpIt(self.data).order_by(key_selector)
1006
+ return FlpIt(self.__list.data).order_by(key_selector)
804
1007
 
805
1008
  def order_by_descending(
806
1009
  self, key_selector: Callable[[TItem], Any]
807
1010
  ) -> OrderedIt[TItem]:
808
- return FlpIt(self.data).order_by_descending(key_selector)
1011
+ return FlpIt(self.__list.data).order_by_descending(key_selector)
809
1012
 
810
1013
  def group_by(
811
1014
  self, key_selector: Callable[[TItem], TKey]
812
1015
  ) -> FlpIt[Grouping[TKey, TItem]]:
813
- return FlpIt(self.data).group_by(key_selector)
1016
+ return FlpIt(self.__list.data).group_by(key_selector)
814
1017
 
815
1018
  @overload
816
1019
  def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
@@ -825,41 +1028,37 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
825
1028
  func: Callable[[Any, Any], Any],
826
1029
  seed: Any = _SENTINEL,
827
1030
  ) -> TItem | TAccumulate:
828
- return FlpIt(self.data).aggregate(func, seed=seed)
1031
+ return FlpIt(self.__list.data).aggregate(func, seed=seed)
829
1032
 
830
1033
  @_guard_empty
831
1034
  def min(self) -> TItem:
832
- return builtins.min(self.data) # pyrefly: ignore [bad-specialization]
1035
+ return builtins.min(self.__list.data) # pyrefly: ignore [bad-specialization]
833
1036
 
834
1037
  @_guard_empty
835
1038
  def min_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
836
- return builtins.min(self.data, key=key_selector)
1039
+ return builtins.min(self.__list.data, key=key_selector)
837
1040
 
838
1041
  @_guard_empty
839
1042
  def max(self) -> TItem:
840
- return builtins.max(self.data) # pyrefly: ignore [bad-specialization]
1043
+ return builtins.max(self.__list.data) # pyrefly: ignore [bad-specialization]
841
1044
 
842
1045
  @_guard_empty
843
1046
  def max_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
844
- return builtins.max(self.data, key=key_selector)
1047
+ return builtins.max(self.__list.data, key=key_selector)
845
1048
 
846
1049
  def sum(
847
- self, selector: Callable[[TItem], int | float] | _Sentinel = _SENTINEL
848
- ) -> int | float:
1050
+ self, selector: Callable[[TItem], int | float | Decimal] | _Sentinel = _SENTINEL
1051
+ ) -> int | float | Decimal:
849
1052
  """Calculates the sum of elements, optionally applying a selector."""
850
- return FlpIt(self.data).sum(selector)
1053
+ return FlpIt(self.__list.data).sum(selector)
851
1054
 
852
1055
  def average(
853
- self, selector: Callable[[TItem], int | float] | _Sentinel = _SENTINEL
854
- ) -> float:
1056
+ self, selector: Callable[[TItem], int | float | Decimal] | _Sentinel = _SENTINEL
1057
+ ) -> float | int | Decimal | None:
855
1058
  """Calculates the arithmetic mean, optionally applying a selector."""
856
- return FlpIt(self.data).average(selector)
857
-
858
- def count_item(self, item: TItem) -> int:
859
- return self.data.count(item) # just redirect how the count redirected before
1059
+ return FlpIt(self.__list.data).average(selector)
860
1060
 
861
- @override
862
- def count( # type: ignore # pyrefly: ignore [bad-override-param-name] # pyright: ignore[reportIncompatibleMethodOverride]
1061
+ def count(
863
1062
  self,
864
1063
  predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL,
865
1064
  ) -> int:
@@ -867,22 +1066,21 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
867
1066
  Counts elements using LINQ semantics.
868
1067
 
869
1068
  Supported forms:
870
-
871
- count() -> total number of elements
872
- count(predicate) -> number of elements satisfying the predicate
1069
+ - count() -> total number of elements
1070
+ - count(predicate) -> number of elements satisfying the predicate
873
1071
  """
874
1072
  if predicate is _SENTINEL:
875
- return len(self.data)
1073
+ return len(self.__list.data)
876
1074
 
877
- return FlpIt(self.data).count(predicate)
1075
+ return FlpIt(self.__list.data).count(predicate)
878
1076
 
879
1077
  def element_at(self, index: int) -> TItem:
880
- if index < 0 or index >= len(self.data):
1078
+ if index < 0 or index >= len(self.__list.data):
881
1079
  raise IndexError("Index out of range")
882
- return self.data[index]
1080
+ return self.__list.data[index]
883
1081
 
884
1082
  def first(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
885
- return FlpIt(self.data).first(predicate)
1083
+ return FlpIt(self.__list.data).first(predicate)
886
1084
 
887
1085
  @overload
888
1086
  def last(self) -> TItem: ...
@@ -909,11 +1107,11 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
909
1107
  raise PredicateNoneError()
910
1108
 
911
1109
  if predicate is _SENTINEL:
912
- if not self.data:
1110
+ if not self.__list.data:
913
1111
  raise EmptySequenceError()
914
- return self.data[-1]
1112
+ return self.__list.data[-1]
915
1113
 
916
- for item in reversed(self.data):
1114
+ for item in reversed(self.__list.data):
917
1115
  if predicate(item):
918
1116
  return item
919
1117
 
@@ -922,11 +1120,11 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
922
1120
  def first_or_default(
923
1121
  self, default: TResult, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL
924
1122
  ) -> TItem | TResult:
925
- return FlpIt(self.data).first_or_default(default, predicate)
1123
+ return FlpIt(self.__list.data).first_or_default(default, predicate)
926
1124
 
927
1125
  def single(self, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL) -> TItem:
928
- return FlpIt(self.data).single(predicate)
1126
+ return FlpIt(self.__list.data).single(predicate)
929
1127
 
930
1128
  def to_list(self) -> FlpList[TItem]:
931
1129
  """Explicitly returns a shallow copy instance to isolate mutations."""
932
- 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