flpit 0.1.1__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.1/PKG-INFO +61 -0
- flpit-0.1.1/README.md +45 -0
- flpit-0.1.1/pyproject.toml +44 -0
- flpit-0.1.1/pyproject.toml.orig +39 -0
- flpit-0.1.1/src/flp/__init__.py +41 -0
- flpit-0.1.1/src/flp/core/__init__.py +0 -0
- flpit-0.1.1/src/flp/core/linq.py +790 -0
- flpit-0.1.1/src/flp/py.typed +0 -0
flpit-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: flpit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Fluent LINQ for Python — flip your iterables
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# flp
|
|
18
|
+
|
|
19
|
+
> **Fluent LINQ for Python – flip your iterables**
|
|
20
|
+
|
|
21
|
+
`flp` brings a LINQ-inspired fluent API to standard Python iterables with full static typing.
|
|
22
|
+
|
|
23
|
+
## ⚡ Key Features
|
|
24
|
+
|
|
25
|
+
* **⚡ Lazy Evaluation (`FlpIt`):** Deferred execution using Python iterators and generators.
|
|
26
|
+
* **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
|
|
27
|
+
* **🔹 Static-First Typing:** Built for full `mypy` and `pyright` inference without plugins.
|
|
28
|
+
* **🔄 No Implicit Caching:** Query results are not cached unless explicitly documented, such as `order_by`.
|
|
29
|
+
|
|
30
|
+
## ⚙️ Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv add flpit
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 💡 Quick Start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import flp
|
|
40
|
+
from flp import FlpIt, FlpList
|
|
41
|
+
|
|
42
|
+
# Deferred iterable via shorthand
|
|
43
|
+
data: FlpIt[int] = flp.it([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
|
44
|
+
|
|
45
|
+
# Deferred / lazy pipeline
|
|
46
|
+
query: FlpIt[int] = (
|
|
47
|
+
data
|
|
48
|
+
.where(lambda x: x % 2 == 0)
|
|
49
|
+
.select(lambda x: x * 10)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Materialize query results explicitly
|
|
53
|
+
result: FlpList[int] = query.to_list() # [20, 40, 60, 80, 100]
|
|
54
|
+
|
|
55
|
+
# Or start directly with an eager container
|
|
56
|
+
eager_list: FlpList[int] = flp.lst([1, 2, 3, 4])
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 📜 License
|
|
60
|
+
|
|
61
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
flpit-0.1.1/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# flp
|
|
2
|
+
|
|
3
|
+
> **Fluent LINQ for Python – flip your iterables**
|
|
4
|
+
|
|
5
|
+
`flp` brings a LINQ-inspired fluent API to standard Python iterables with full static typing.
|
|
6
|
+
|
|
7
|
+
## ⚡ Key Features
|
|
8
|
+
|
|
9
|
+
* **⚡ Lazy Evaluation (`FlpIt`):** Deferred execution using Python iterators and generators.
|
|
10
|
+
* **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
|
|
11
|
+
* **🔹 Static-First Typing:** Built for full `mypy` and `pyright` inference without plugins.
|
|
12
|
+
* **🔄 No Implicit Caching:** Query results are not cached unless explicitly documented, such as `order_by`.
|
|
13
|
+
|
|
14
|
+
## ⚙️ Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv add flpit
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 💡 Quick Start
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import flp
|
|
24
|
+
from flp import FlpIt, FlpList
|
|
25
|
+
|
|
26
|
+
# Deferred iterable via shorthand
|
|
27
|
+
data: FlpIt[int] = flp.it([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
|
28
|
+
|
|
29
|
+
# Deferred / lazy pipeline
|
|
30
|
+
query: FlpIt[int] = (
|
|
31
|
+
data
|
|
32
|
+
.where(lambda x: x % 2 == 0)
|
|
33
|
+
.select(lambda x: x * 10)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Materialize query results explicitly
|
|
37
|
+
result: FlpList[int] = query.to_list() # [20, 40, 60, 80, 100]
|
|
38
|
+
|
|
39
|
+
# Or start directly with an eager container
|
|
40
|
+
eager_list: FlpList[int] = flp.lst([1, 2, 3, 4])
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 📜 License
|
|
44
|
+
|
|
45
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "flpit"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Fluent LINQ for Python — flip your iterables"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
classifiers = [
|
|
8
|
+
"Development Status :: 3 - Alpha",
|
|
9
|
+
"Intended Audience :: Developers",
|
|
10
|
+
"License :: OSI Approved :: MIT License",
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Programming Language :: Python :: 3.11",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
16
|
+
]
|
|
17
|
+
dependencies = []
|
|
18
|
+
|
|
19
|
+
[project.license]
|
|
20
|
+
text = "MIT"
|
|
21
|
+
|
|
22
|
+
[dependency-groups]
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=8.0",
|
|
25
|
+
"hypothesis>=6.168.0",
|
|
26
|
+
"pyrefly>=1.3.1",
|
|
27
|
+
"pyright>=1.1.414",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["uv_build>=0.12.10,<0.13.0"]
|
|
32
|
+
build-backend = "uv_build"
|
|
33
|
+
|
|
34
|
+
[tool.uv.build-backend]
|
|
35
|
+
module-name = "flp"
|
|
36
|
+
module-root = "src"
|
|
37
|
+
|
|
38
|
+
[tool.pyright]
|
|
39
|
+
include = ["src"]
|
|
40
|
+
exclude = [
|
|
41
|
+
"**/__pycache__",
|
|
42
|
+
"tests",
|
|
43
|
+
]
|
|
44
|
+
typeCheckingMode = "basic"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "flpit"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Fluent LINQ for Python — flip your iterables"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 3 - Alpha",
|
|
10
|
+
"Intended Audience :: Developers",
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
17
|
+
]
|
|
18
|
+
dependencies = []
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8.0",
|
|
23
|
+
"hypothesis>=6.168.0",
|
|
24
|
+
"pyrefly>=1.3.1",
|
|
25
|
+
"pyright>=1.1.414",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["uv_build>=0.12.10,<0.13.0"]
|
|
30
|
+
build-backend = "uv_build"
|
|
31
|
+
|
|
32
|
+
[tool.uv.build-backend]
|
|
33
|
+
module-name = "flp"
|
|
34
|
+
module-root = "src"
|
|
35
|
+
|
|
36
|
+
[tool.pyright]
|
|
37
|
+
include = ["src"]
|
|
38
|
+
exclude = ["**/__pycache__", "tests"]
|
|
39
|
+
typeCheckingMode = "basic"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fluent LINQ for Python — flip your iterables.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
__version__ = version("flpit")
|
|
7
|
+
|
|
8
|
+
import builtins
|
|
9
|
+
from typing import Iterable as _Iterable, TypeVar
|
|
10
|
+
|
|
11
|
+
from flp.core.linq import Grouping, FlpList, FlpIt, OrderedIt
|
|
12
|
+
|
|
13
|
+
TItem = TypeVar("TItem")
|
|
14
|
+
|
|
15
|
+
def it(iterable: _Iterable[TItem]) -> FlpIt[TItem]:
|
|
16
|
+
"""shorthand to create a linq query object"""
|
|
17
|
+
return FlpIt(iterable)
|
|
18
|
+
|
|
19
|
+
def lst(iterable: _Iterable[TItem]) -> FlpList[TItem]:
|
|
20
|
+
"""shorthand to create a linq list object"""
|
|
21
|
+
return FlpList(iterable)
|
|
22
|
+
|
|
23
|
+
# noinspection shadowing-builtins
|
|
24
|
+
def range(start: int, count: int) -> FlpIt[int]:
|
|
25
|
+
"""Generates a lazy sequence of integral numbers within a specified range."""
|
|
26
|
+
return FlpIt(builtins.range(start, start + count))
|
|
27
|
+
|
|
28
|
+
def repeat(element: TItem, count: int) -> FlpIt[TItem]:
|
|
29
|
+
"""Generates a lazy sequence that contains one repeated value."""
|
|
30
|
+
return FlpIt(element for _ in builtins.range(count))
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"FlpIt",
|
|
34
|
+
"OrderedIt",
|
|
35
|
+
"Grouping",
|
|
36
|
+
"FlpList",
|
|
37
|
+
"it",
|
|
38
|
+
"lst",
|
|
39
|
+
"range",
|
|
40
|
+
"repeat",
|
|
41
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import builtins
|
|
4
|
+
from collections import UserList
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from itertools import islice
|
|
7
|
+
from typing import (
|
|
8
|
+
Any,
|
|
9
|
+
Callable,
|
|
10
|
+
Generic,
|
|
11
|
+
Iterable,
|
|
12
|
+
Iterator,
|
|
13
|
+
List,
|
|
14
|
+
Optional,
|
|
15
|
+
Sequence,
|
|
16
|
+
Set,
|
|
17
|
+
Type,
|
|
18
|
+
TypeVar,
|
|
19
|
+
Union,
|
|
20
|
+
overload,
|
|
21
|
+
cast as typing_cast,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
TItem = TypeVar("TItem")
|
|
25
|
+
TResult = TypeVar("TResult")
|
|
26
|
+
TKey = TypeVar("TKey")
|
|
27
|
+
TOther = TypeVar("TOther")
|
|
28
|
+
TAccumulate = TypeVar("TAccumulate")
|
|
29
|
+
|
|
30
|
+
_SENTINEL = object()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EmptySequenceError(ValueError):
|
|
34
|
+
def __init__(self, message="Sequence contains no elements"):
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
39
|
+
"""Catches Python's native empty-sequence ValueError and re-raises LINQ-compliant error."""
|
|
40
|
+
@wraps(func)
|
|
41
|
+
def wrapper(self, *args: Any, **kwargs: Any) -> Any:
|
|
42
|
+
try:
|
|
43
|
+
return func(self, *args, **kwargs)
|
|
44
|
+
except ValueError as e:
|
|
45
|
+
if not self.data:
|
|
46
|
+
raise EmptySequenceError() from e
|
|
47
|
+
raise
|
|
48
|
+
return wrapper
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _FactoryIterable(Iterable[TItem], Generic[TItem]):
|
|
52
|
+
"""
|
|
53
|
+
Wraps a generator factory function into a clean Iterable[T].
|
|
54
|
+
Ensures __iter__ produces a brand-new iterator instance on every call
|
|
55
|
+
without type-checker warnings or runtime callable-check overhead.
|
|
56
|
+
"""
|
|
57
|
+
__slots__ = ("_factory",)
|
|
58
|
+
|
|
59
|
+
def __init__(self, factory: Callable[[], Iterator[TItem]]) -> None:
|
|
60
|
+
self._factory = factory
|
|
61
|
+
|
|
62
|
+
def __iter__(self) -> Iterator[TItem]:
|
|
63
|
+
return self._factory()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
67
|
+
"""
|
|
68
|
+
| Fluent Iterable
|
|
69
|
+
Lazy evaluation wrapper around an iterable, inspired by .NET LINQ's
|
|
70
|
+
IEnumerable<T> semantics. Operations are deferred and do not cache results
|
|
71
|
+
unless explicitly documented otherwise.
|
|
72
|
+
|
|
73
|
+
Multiple enumeration is supported when the underlying source is re-iterable;
|
|
74
|
+
one-shot iterators and generators remain one-shot.
|
|
75
|
+
"""
|
|
76
|
+
__slots__ = ("_iterable",)
|
|
77
|
+
|
|
78
|
+
def __init__(self, iterable: Iterable[TItem]) -> None:
|
|
79
|
+
self._iterable: Iterable[TItem] = iterable
|
|
80
|
+
|
|
81
|
+
def __iter__(self) -> Iterator[TItem]:
|
|
82
|
+
return iter(self._iterable)
|
|
83
|
+
|
|
84
|
+
# noinspection unused-parameter
|
|
85
|
+
def as_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
86
|
+
"""Zero-cost static type hint update returning self directly without checks or allocations."""
|
|
87
|
+
return self # type: ignore[return-value]
|
|
88
|
+
|
|
89
|
+
# --- Deferred Execution (Lazy Operations) ---
|
|
90
|
+
|
|
91
|
+
def append(self, element: TItem) -> FlpIt[TItem]:
|
|
92
|
+
"""Appends an element to the end of the sequence (deferred)."""
|
|
93
|
+
def _generator() -> Iterator[TItem]:
|
|
94
|
+
yield from self
|
|
95
|
+
yield element
|
|
96
|
+
|
|
97
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
98
|
+
|
|
99
|
+
def concat(self, second: Iterable[TItem]) -> "FlpIt[TItem]":
|
|
100
|
+
def _generator() -> Iterator[TItem]:
|
|
101
|
+
yield from self
|
|
102
|
+
yield from second
|
|
103
|
+
|
|
104
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
105
|
+
|
|
106
|
+
def prepend(self, element: TItem) -> FlpIt[TItem]:
|
|
107
|
+
"""Prepends an element to the beginning of the sequence (deferred)."""
|
|
108
|
+
def _generator() -> Iterator[TItem]:
|
|
109
|
+
yield element
|
|
110
|
+
yield from self
|
|
111
|
+
|
|
112
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
113
|
+
|
|
114
|
+
def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
|
|
115
|
+
"""Filters elements based on a predicate."""
|
|
116
|
+
def _generator() -> Iterator[TItem]:
|
|
117
|
+
for item in self:
|
|
118
|
+
if predicate(item):
|
|
119
|
+
yield item
|
|
120
|
+
|
|
121
|
+
return typing_cast(FlpIt[TItem], FlpIt(_FactoryIterable(_generator)))
|
|
122
|
+
|
|
123
|
+
def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
|
|
124
|
+
"""Projects each element into a new form."""
|
|
125
|
+
def _generator() -> Iterator[TResult]:
|
|
126
|
+
for item in self:
|
|
127
|
+
yield selector(item)
|
|
128
|
+
|
|
129
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
130
|
+
|
|
131
|
+
def select_many(
|
|
132
|
+
self, selector: Callable[[TItem], Iterable[TResult]]
|
|
133
|
+
) -> FlpIt[TResult]:
|
|
134
|
+
"""Flattens sequence projections."""
|
|
135
|
+
def _generator() -> Iterator[TResult]:
|
|
136
|
+
for item in self:
|
|
137
|
+
yield from selector(item)
|
|
138
|
+
|
|
139
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
140
|
+
|
|
141
|
+
def take(self, count: int) -> "FlpIt[TItem]":
|
|
142
|
+
"""
|
|
143
|
+
|Returns a specified number of contiguous elements from the start.
|
|
144
|
+
|
|
145
|
+
Execution is deferred until the returned sequence is enumerated.
|
|
146
|
+
The operator consumes no more than `count` elements from upstream.
|
|
147
|
+
|
|
148
|
+
`take()` does not own the upstream iterator and therefore does not
|
|
149
|
+
close it when the limit is reached. Upstream resource lifetime is the
|
|
150
|
+
responsibility of the code that owns/acquires the source; use an
|
|
151
|
+
explicit context manager or close the source explicitly when required.
|
|
152
|
+
|
|
153
|
+
This is intentional: reaching the `take()` limit is normal completion,
|
|
154
|
+
not cancellation or disposal of the upstream sequence.
|
|
155
|
+
"""
|
|
156
|
+
if count <= 0:
|
|
157
|
+
return FlpIt(())
|
|
158
|
+
|
|
159
|
+
def _generator() -> Iterator[TItem]:
|
|
160
|
+
yield from islice(iter(self), count)
|
|
161
|
+
|
|
162
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
163
|
+
|
|
164
|
+
def cast(self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
165
|
+
"""Casts elements to a specified type or raises TypeError if cast fails."""
|
|
166
|
+
def _generator() -> Iterator[TResult]:
|
|
167
|
+
for item in self:
|
|
168
|
+
if not isinstance(item, target_type):
|
|
169
|
+
raise TypeError(
|
|
170
|
+
f"Cannot cast element {item!r} of type {type(item).__name__} to {target_type.__name__}"
|
|
171
|
+
)
|
|
172
|
+
yield item # type: ignore[misc]
|
|
173
|
+
|
|
174
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
175
|
+
|
|
176
|
+
def of_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
177
|
+
"""Filters the elements of an Iterable based on a specified type."""
|
|
178
|
+
def _generator() -> Iterator[TResult]:
|
|
179
|
+
for item in self:
|
|
180
|
+
if isinstance(item, target_type):
|
|
181
|
+
yield item # type: ignore[misc]
|
|
182
|
+
|
|
183
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
184
|
+
|
|
185
|
+
def distinct(self) -> FlpIt[TItem]:
|
|
186
|
+
"""Returns distinct elements from a sequence by using O(1) set lookups."""
|
|
187
|
+
def _generator() -> Iterator[TItem]:
|
|
188
|
+
seen: Set[TItem] = set()
|
|
189
|
+
for item in self:
|
|
190
|
+
if item not in seen:
|
|
191
|
+
seen.add(item)
|
|
192
|
+
yield item
|
|
193
|
+
|
|
194
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
195
|
+
|
|
196
|
+
def distinct_by(
|
|
197
|
+
self, key_selector: Callable[[TItem], TKey]
|
|
198
|
+
) -> FlpIt[TItem]:
|
|
199
|
+
"""Returns distinct elements from a sequence according to a key selector function."""
|
|
200
|
+
def _generator() -> Iterator[TItem]:
|
|
201
|
+
seen: Set[TKey] = set()
|
|
202
|
+
for item in self:
|
|
203
|
+
key = key_selector(item)
|
|
204
|
+
if key not in seen:
|
|
205
|
+
seen.add(key)
|
|
206
|
+
yield item
|
|
207
|
+
|
|
208
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
209
|
+
|
|
210
|
+
@overload
|
|
211
|
+
def zip(self, second: Iterable[TOther]) -> FlpIt[tuple[TItem, TOther]]: ...
|
|
212
|
+
|
|
213
|
+
@overload
|
|
214
|
+
def zip(
|
|
215
|
+
self,
|
|
216
|
+
second: Iterable[TOther],
|
|
217
|
+
result_selector: Callable[[TItem, TOther], TResult],
|
|
218
|
+
) -> FlpIt[TResult]: ...
|
|
219
|
+
|
|
220
|
+
def zip(
|
|
221
|
+
self,
|
|
222
|
+
second: Iterable[TOther],
|
|
223
|
+
result_selector: Optional[Callable[[TItem, TOther], Any]] = None,
|
|
224
|
+
) -> FlpIt[Any]:
|
|
225
|
+
"""Applies a specified function to corresponding elements of two sequences."""
|
|
226
|
+
def _generator() -> Iterator[Any]:
|
|
227
|
+
for first_item, second_item in zip(self, second):
|
|
228
|
+
if result_selector is not None:
|
|
229
|
+
yield result_selector(first_item, second_item)
|
|
230
|
+
else:
|
|
231
|
+
yield first_item, second_item
|
|
232
|
+
|
|
233
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
234
|
+
|
|
235
|
+
def chunk(self, size: int) -> FlpIt[FlpList[TItem]]:
|
|
236
|
+
"""Splits the elements of a sequence into chunks of size at most size."""
|
|
237
|
+
if size <= 0:
|
|
238
|
+
raise ValueError("Chunk size must be greater than 0.")
|
|
239
|
+
|
|
240
|
+
def _generator() -> Iterator[FlpList[TItem]]:
|
|
241
|
+
current_chunk: List[TItem] = []
|
|
242
|
+
for item in self:
|
|
243
|
+
current_chunk.append(item)
|
|
244
|
+
if len(current_chunk) == size:
|
|
245
|
+
yield FlpList(current_chunk)
|
|
246
|
+
current_chunk = []
|
|
247
|
+
if current_chunk:
|
|
248
|
+
yield FlpList(current_chunk)
|
|
249
|
+
|
|
250
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
251
|
+
|
|
252
|
+
def order_by(
|
|
253
|
+
self, key_selector: Callable[[TItem], Any]
|
|
254
|
+
) -> OrderedIt[TItem]:
|
|
255
|
+
"""Sorts elements in ascending order according to a key."""
|
|
256
|
+
return OrderedIt(self, key_selector, descending=False)
|
|
257
|
+
|
|
258
|
+
def order_by_descending(
|
|
259
|
+
self, key_selector: Callable[[TItem], Any]
|
|
260
|
+
) -> OrderedIt[TItem]:
|
|
261
|
+
"""Sorts elements in descending order according to a key."""
|
|
262
|
+
return OrderedIt(self, key_selector, descending=True)
|
|
263
|
+
|
|
264
|
+
def group_by(
|
|
265
|
+
self, key_selector: Callable[[TItem], TKey]
|
|
266
|
+
) -> FlpIt[Grouping[TKey, TItem]]:
|
|
267
|
+
"""Groups elements according to a specified key selector function."""
|
|
268
|
+
def _generator() -> Iterator[Grouping[TKey, TItem]]:
|
|
269
|
+
groups: dict[TKey, List[TItem]] = {}
|
|
270
|
+
for item in self:
|
|
271
|
+
key = key_selector(item)
|
|
272
|
+
groups.setdefault(key, []).append(item)
|
|
273
|
+
for k, v in groups.items():
|
|
274
|
+
yield Grouping(k, v)
|
|
275
|
+
|
|
276
|
+
return FlpIt(_FactoryIterable(_generator))
|
|
277
|
+
|
|
278
|
+
# --- Immediate Execution (Materialization & Aggregation) ---
|
|
279
|
+
|
|
280
|
+
@overload
|
|
281
|
+
def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
|
|
282
|
+
|
|
283
|
+
@overload
|
|
284
|
+
def aggregate(
|
|
285
|
+
self, func: Callable[[TAccumulate, TItem], TAccumulate], seed: TAccumulate
|
|
286
|
+
) -> TAccumulate: ...
|
|
287
|
+
|
|
288
|
+
def aggregate(
|
|
289
|
+
self,
|
|
290
|
+
func: Callable[[Any, TItem], Any],
|
|
291
|
+
seed: Any = _SENTINEL,
|
|
292
|
+
) -> Any:
|
|
293
|
+
"""Applies an accumulator function over a sequence."""
|
|
294
|
+
it = iter(self)
|
|
295
|
+
if seed is _SENTINEL:
|
|
296
|
+
try:
|
|
297
|
+
accumulator = next(it)
|
|
298
|
+
except StopIteration:
|
|
299
|
+
raise EmptySequenceError()
|
|
300
|
+
for item in it:
|
|
301
|
+
accumulator = func(accumulator, item)
|
|
302
|
+
else:
|
|
303
|
+
accumulator = seed
|
|
304
|
+
for item in it:
|
|
305
|
+
accumulator = func(accumulator, item)
|
|
306
|
+
return accumulator
|
|
307
|
+
|
|
308
|
+
def min(self) -> TItem:
|
|
309
|
+
"""Returns the minimum value in a sequence."""
|
|
310
|
+
try:
|
|
311
|
+
return builtins.min(self) # type: ignore[type-var]
|
|
312
|
+
except ValueError:
|
|
313
|
+
raise EmptySequenceError()
|
|
314
|
+
|
|
315
|
+
def min_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
|
|
316
|
+
"""Returns the value in a sequence that has the minimum key value."""
|
|
317
|
+
try:
|
|
318
|
+
return builtins.min(self, key=key_selector)
|
|
319
|
+
except ValueError:
|
|
320
|
+
raise EmptySequenceError()
|
|
321
|
+
|
|
322
|
+
def max(self) -> TItem:
|
|
323
|
+
"""Returns the maximum value in a sequence."""
|
|
324
|
+
try:
|
|
325
|
+
return builtins.max(self) # type: ignore[type-var]
|
|
326
|
+
except ValueError:
|
|
327
|
+
raise EmptySequenceError()
|
|
328
|
+
|
|
329
|
+
def max_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
|
|
330
|
+
"""Returns the value in a sequence that has the maximum key value."""
|
|
331
|
+
try:
|
|
332
|
+
return builtins.max(self, key=key_selector)
|
|
333
|
+
except ValueError:
|
|
334
|
+
raise EmptySequenceError()
|
|
335
|
+
|
|
336
|
+
def average(
|
|
337
|
+
self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
|
|
338
|
+
) -> float:
|
|
339
|
+
"""Computes the arithmetic mean of the sequence, optionally applying a selector."""
|
|
340
|
+
total = 0.0
|
|
341
|
+
count = 0
|
|
342
|
+
query = (selector(x) for x in self) if selector is not None else self
|
|
343
|
+
for item in query:
|
|
344
|
+
total += float(item) # type: ignore[arg-type]
|
|
345
|
+
count += 1
|
|
346
|
+
if count == 0:
|
|
347
|
+
raise EmptySequenceError()
|
|
348
|
+
return total / count
|
|
349
|
+
|
|
350
|
+
avg = average
|
|
351
|
+
|
|
352
|
+
def average_by(self, key_selector: Callable[[TItem], Union[int, float]]) -> float:
|
|
353
|
+
"""Computes the average of a sequence of numeric values projected by a key selector."""
|
|
354
|
+
total = 0.0
|
|
355
|
+
count = 0
|
|
356
|
+
for item in self:
|
|
357
|
+
total += float(key_selector(item))
|
|
358
|
+
count += 1
|
|
359
|
+
if count == 0:
|
|
360
|
+
raise EmptySequenceError()
|
|
361
|
+
return total / count
|
|
362
|
+
|
|
363
|
+
avg_by = average_by
|
|
364
|
+
|
|
365
|
+
def sum(
|
|
366
|
+
self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
|
|
367
|
+
) -> Union[int, float]:
|
|
368
|
+
"""Calculates the sum of the sequence, optionally applying a selector."""
|
|
369
|
+
if selector is not None:
|
|
370
|
+
return builtins.sum(selector(x) for x in self)
|
|
371
|
+
return builtins.sum(self) # type: ignore[arg-type]
|
|
372
|
+
|
|
373
|
+
def count(self, predicate: Optional[Callable[[TItem], bool]] = None) -> int:
|
|
374
|
+
"""Counts elements in the sequence matching an optional predicate."""
|
|
375
|
+
query = self.where(predicate) if predicate else self
|
|
376
|
+
return builtins.sum(1 for _ in query)
|
|
377
|
+
|
|
378
|
+
def element_at(self, index: int) -> TItem:
|
|
379
|
+
"""Returns the element at a specified index in a sequence."""
|
|
380
|
+
if index < 0:
|
|
381
|
+
raise IndexError("Index out of range")
|
|
382
|
+
for i, item in enumerate(self):
|
|
383
|
+
if i == index:
|
|
384
|
+
return item
|
|
385
|
+
raise IndexError("Index out of range")
|
|
386
|
+
|
|
387
|
+
def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
|
|
388
|
+
"""Returns the first element matching a predicate, or raises ValueError."""
|
|
389
|
+
query = self.where(predicate) if predicate else self
|
|
390
|
+
for item in query:
|
|
391
|
+
return item
|
|
392
|
+
raise ValueError("Sequence contains no matching elements")
|
|
393
|
+
|
|
394
|
+
def first_or_default(
|
|
395
|
+
self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
|
|
396
|
+
) -> Union[TItem, TResult]:
|
|
397
|
+
"""Returns the first element matching a predicate, or a default value."""
|
|
398
|
+
try:
|
|
399
|
+
return self.first(predicate)
|
|
400
|
+
except ValueError:
|
|
401
|
+
return default
|
|
402
|
+
|
|
403
|
+
def single(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
|
|
404
|
+
"""Returns the single, unique element matching a predicate."""
|
|
405
|
+
query = self.where(predicate) if predicate else self
|
|
406
|
+
it = iter(query)
|
|
407
|
+
try:
|
|
408
|
+
first_val = next(it)
|
|
409
|
+
except StopIteration:
|
|
410
|
+
raise ValueError("Sequence contains no matching elements")
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
next(it)
|
|
414
|
+
except StopIteration:
|
|
415
|
+
return first_val
|
|
416
|
+
|
|
417
|
+
raise ValueError("Sequence contains more than one matching element")
|
|
418
|
+
|
|
419
|
+
def to_list(self) -> FlpList[TItem]:
|
|
420
|
+
"""Explicitly materializes the query into a FlpList."""
|
|
421
|
+
return FlpList(self)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
from threading import Lock
|
|
425
|
+
|
|
426
|
+
class OrderedIt(FlpIt[TItem]):
|
|
427
|
+
"""
|
|
428
|
+
Ordered Iterable.
|
|
429
|
+
|
|
430
|
+
Sorts the source lazily on first iteration and caches the resulting order.
|
|
431
|
+
The source is consumed at most once; subsequent iterations reuse the cached
|
|
432
|
+
result.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
__slots__ = (
|
|
436
|
+
"_source",
|
|
437
|
+
"_key_selector",
|
|
438
|
+
"_descending",
|
|
439
|
+
"_parent",
|
|
440
|
+
"_cached_result",
|
|
441
|
+
"_lock",
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
def __init__(
|
|
445
|
+
self,
|
|
446
|
+
source: Iterable[TItem],
|
|
447
|
+
key_selector: Callable[[TItem], Any],
|
|
448
|
+
descending: bool = False,
|
|
449
|
+
parent: Optional["OrderedIt[TItem]"] = None,
|
|
450
|
+
) -> None:
|
|
451
|
+
super().__init__(source)
|
|
452
|
+
|
|
453
|
+
self._source = source
|
|
454
|
+
self._key_selector = key_selector
|
|
455
|
+
self._descending = descending
|
|
456
|
+
self._parent = parent
|
|
457
|
+
|
|
458
|
+
self._cached_result: Optional[list[TItem]] = None
|
|
459
|
+
self._lock = Lock()
|
|
460
|
+
|
|
461
|
+
def then_by(
|
|
462
|
+
self,
|
|
463
|
+
key_selector: Callable[[TItem], Any],
|
|
464
|
+
) -> "OrderedIt[TItem]":
|
|
465
|
+
return OrderedIt(
|
|
466
|
+
self._source,
|
|
467
|
+
key_selector,
|
|
468
|
+
descending=False,
|
|
469
|
+
parent=self,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def then_by_descending(
|
|
473
|
+
self,
|
|
474
|
+
key_selector: Callable[[TItem], Any],
|
|
475
|
+
) -> "OrderedIt[TItem]":
|
|
476
|
+
return OrderedIt(
|
|
477
|
+
self._source,
|
|
478
|
+
key_selector,
|
|
479
|
+
descending=True,
|
|
480
|
+
parent=self,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
def __iter__(self) -> Iterator[TItem]:
|
|
484
|
+
cached = self._cached_result
|
|
485
|
+
|
|
486
|
+
if cached is None:
|
|
487
|
+
with self._lock:
|
|
488
|
+
cached = self._cached_result
|
|
489
|
+
|
|
490
|
+
if cached is None:
|
|
491
|
+
# Collect the complete ordering chain.
|
|
492
|
+
comparers: list[
|
|
493
|
+
tuple[Callable[[TItem], Any], bool]
|
|
494
|
+
] = []
|
|
495
|
+
|
|
496
|
+
node: Optional["OrderedIt[TItem]"] = self
|
|
497
|
+
|
|
498
|
+
while node is not None:
|
|
499
|
+
comparers.append(
|
|
500
|
+
(node._key_selector, node._descending)
|
|
501
|
+
)
|
|
502
|
+
node = node._parent
|
|
503
|
+
|
|
504
|
+
comparers.reverse()
|
|
505
|
+
|
|
506
|
+
# Consume the source exactly once.
|
|
507
|
+
items = list(self._source)
|
|
508
|
+
|
|
509
|
+
class SortWrapper:
|
|
510
|
+
__slots__ = ("obj", "keys")
|
|
511
|
+
|
|
512
|
+
def __init__(self, obj: Any) -> None:
|
|
513
|
+
self.obj = obj
|
|
514
|
+
self.keys = [
|
|
515
|
+
selector(obj)
|
|
516
|
+
for selector, _ in comparers
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
def __lt__(self, other: "SortWrapper") -> bool:
|
|
520
|
+
for index, (_, descending) in enumerate(comparers):
|
|
521
|
+
left = self.keys[index]
|
|
522
|
+
right = other.keys[index]
|
|
523
|
+
|
|
524
|
+
if left == right:
|
|
525
|
+
continue
|
|
526
|
+
|
|
527
|
+
return right < left if descending else left < right
|
|
528
|
+
|
|
529
|
+
return False
|
|
530
|
+
|
|
531
|
+
wrapped_items = [
|
|
532
|
+
SortWrapper(item)
|
|
533
|
+
for item in items
|
|
534
|
+
]
|
|
535
|
+
|
|
536
|
+
wrapped_items.sort()
|
|
537
|
+
|
|
538
|
+
# Store ONLY the actual result objects.
|
|
539
|
+
cached = [
|
|
540
|
+
wrapper.obj
|
|
541
|
+
for wrapper in wrapped_items
|
|
542
|
+
]
|
|
543
|
+
|
|
544
|
+
self._cached_result = cached
|
|
545
|
+
|
|
546
|
+
# Important: __iter__ is a generator function.
|
|
547
|
+
# Release potentially large temporary structures
|
|
548
|
+
# before yielding anything.
|
|
549
|
+
del wrapped_items
|
|
550
|
+
del items
|
|
551
|
+
del comparers
|
|
552
|
+
|
|
553
|
+
yield from cached
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
|
|
557
|
+
"""Represents a collection of elements sharing a common key (.NET IGrouping<TKey, TElement>)."""
|
|
558
|
+
__slots__ = ("_key")
|
|
559
|
+
|
|
560
|
+
def __init__(self, key: TKey, elements: Iterable[TItem]) -> None:
|
|
561
|
+
self._key: TKey = key
|
|
562
|
+
super().__init__(elements)
|
|
563
|
+
|
|
564
|
+
@property
|
|
565
|
+
def key(self) -> TKey:
|
|
566
|
+
return self._key
|
|
567
|
+
|
|
568
|
+
def __repr__(self) -> str:
|
|
569
|
+
return f"Grouping(key={self.key!r}, elements={self.to_list()!r})"
|
|
570
|
+
|
|
571
|
+
def __eq__(self, other: Any) -> bool:
|
|
572
|
+
# Check if the other object is a Grouping (or subclass)
|
|
573
|
+
if not isinstance(other, Grouping):
|
|
574
|
+
return False
|
|
575
|
+
|
|
576
|
+
# Compare the keys, then compare the elements inside FlpIt
|
|
577
|
+
return self.key == other.key and self.to_list() == other.to_list()
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
581
|
+
"""
|
|
582
|
+
| Fluent List
|
|
583
|
+
A materialized list extending UserList that yields lazy FlpIt instances for query operations.
|
|
584
|
+
"""
|
|
585
|
+
|
|
586
|
+
def add(self, item: TItem) -> None:
|
|
587
|
+
"""
|
|
588
|
+
|Adds an item and performs O(1) type consistency check against the first element.
|
|
589
|
+
|
|
590
|
+
|Type safety via
|
|
591
|
+
- type checks
|
|
592
|
+
- manual of_type(...) filter if you don't trust your checks
|
|
593
|
+
"""
|
|
594
|
+
self.data.append(item)
|
|
595
|
+
|
|
596
|
+
def add_range(self, items: Iterable[TItem]) -> None:
|
|
597
|
+
"""
|
|
598
|
+
| Adds an Iterable sequence or stream.
|
|
599
|
+
Optimizes paths based on input type without destroying volatile generators.
|
|
600
|
+
|
|
601
|
+
|Type safety via
|
|
602
|
+
- type checks
|
|
603
|
+
- manual of_type(...) filter if you don't trust your checks
|
|
604
|
+
"""
|
|
605
|
+
# 1. Optimized Path: Fast memory extensions for pre-materialized sequences
|
|
606
|
+
if isinstance(items, (Sequence, list, tuple, UserList)):
|
|
607
|
+
self.data.extend(items)
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
# 2. Stream Path: Volatile one-shot generator handling
|
|
611
|
+
it = iter(items)
|
|
612
|
+
try:
|
|
613
|
+
first_item = next(it)
|
|
614
|
+
except StopIteration:
|
|
615
|
+
return
|
|
616
|
+
|
|
617
|
+
# Append the tracked peek-element and stream the remainder safely
|
|
618
|
+
self.data.append(first_item)
|
|
619
|
+
self.data.extend(it)
|
|
620
|
+
|
|
621
|
+
def to_list(self) -> "FlpList[TItem]":
|
|
622
|
+
"""Explicitly returns a new shallow copy instance to isolate mutations matching .NET."""
|
|
623
|
+
return FlpList(self.data.copy())
|
|
624
|
+
|
|
625
|
+
def append_linq(self, element: TItem) -> FlpIt[TItem]:
|
|
626
|
+
"""Appends an element to the sequence lazily, returning a FlpIt without mutating this list."""
|
|
627
|
+
return FlpIt(self.data).append(element)
|
|
628
|
+
|
|
629
|
+
def prepend(self, element: TItem) -> FlpIt[TItem]:
|
|
630
|
+
"""Prepends an element to the sequence lazily, returning a FlpIt without mutating this list."""
|
|
631
|
+
return FlpIt(self.data).prepend(element)
|
|
632
|
+
|
|
633
|
+
# noinspection unused-parameter
|
|
634
|
+
def as_type(self, target_type: Type[TResult]) -> FlpList[TResult]:
|
|
635
|
+
return self # type: ignore[return-value]
|
|
636
|
+
|
|
637
|
+
def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
|
|
638
|
+
return FlpIt(self.data).where(predicate)
|
|
639
|
+
|
|
640
|
+
def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
|
|
641
|
+
return FlpIt(self.data).select(selector)
|
|
642
|
+
|
|
643
|
+
def select_many(
|
|
644
|
+
self, selector: Callable[[TItem], Iterable[TResult]]
|
|
645
|
+
) -> FlpIt[TResult]:
|
|
646
|
+
return FlpIt(self.data).select_many(selector)
|
|
647
|
+
|
|
648
|
+
def take(self, count: int) -> FlpIt[TItem]:
|
|
649
|
+
return FlpIt(self.data).take(count)
|
|
650
|
+
|
|
651
|
+
def cast(self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
652
|
+
return FlpIt(self.data).cast(target_type)
|
|
653
|
+
|
|
654
|
+
def of_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
|
|
655
|
+
return FlpIt(self.data).of_type(target_type)
|
|
656
|
+
|
|
657
|
+
def distinct(self) -> FlpIt[TItem]:
|
|
658
|
+
return FlpIt(self.data).distinct()
|
|
659
|
+
|
|
660
|
+
def distinct_by(
|
|
661
|
+
self, key_selector: Callable[[TItem], TKey]
|
|
662
|
+
) -> FlpIt[TItem]:
|
|
663
|
+
return FlpIt(self.data).distinct_by(key_selector)
|
|
664
|
+
|
|
665
|
+
@overload
|
|
666
|
+
def zip(self, second: Iterable[TOther]) -> FlpIt[tuple[TItem, TOther]]: ...
|
|
667
|
+
|
|
668
|
+
@overload
|
|
669
|
+
def zip(
|
|
670
|
+
self,
|
|
671
|
+
second: Iterable[TOther],
|
|
672
|
+
result_selector: Callable[[TItem, TOther], TResult],
|
|
673
|
+
) -> FlpIt[TResult]: ...
|
|
674
|
+
|
|
675
|
+
def zip(
|
|
676
|
+
self,
|
|
677
|
+
second: Iterable[TOther],
|
|
678
|
+
result_selector: Optional[Callable[[TItem, TOther], Any]] = None,
|
|
679
|
+
) -> FlpIt[Any]:
|
|
680
|
+
return FlpIt(self.data).zip(second, result_selector)
|
|
681
|
+
|
|
682
|
+
def chunk(self, size: int) -> FlpIt[FlpList[TItem]]:
|
|
683
|
+
return FlpIt(self.data).chunk(size)
|
|
684
|
+
|
|
685
|
+
def order_by(
|
|
686
|
+
self, key_selector: Callable[[TItem], Any]
|
|
687
|
+
) -> OrderedIt[TItem]:
|
|
688
|
+
return FlpIt(self.data).order_by(key_selector)
|
|
689
|
+
|
|
690
|
+
def order_by_descending(
|
|
691
|
+
self, key_selector: Callable[[TItem], Any]
|
|
692
|
+
) -> OrderedIt[TItem]:
|
|
693
|
+
return FlpIt(self.data).order_by_descending(key_selector)
|
|
694
|
+
|
|
695
|
+
def group_by(
|
|
696
|
+
self, key_selector: Callable[[TItem], TKey]
|
|
697
|
+
) -> FlpIt[Grouping[TKey, TItem]]:
|
|
698
|
+
return FlpIt(self.data).group_by(key_selector)
|
|
699
|
+
|
|
700
|
+
@overload
|
|
701
|
+
def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
|
|
702
|
+
|
|
703
|
+
@overload
|
|
704
|
+
def aggregate(
|
|
705
|
+
self, func: Callable[[TAccumulate, TItem], TAccumulate], seed: TAccumulate
|
|
706
|
+
) -> TAccumulate: ...
|
|
707
|
+
|
|
708
|
+
def aggregate(
|
|
709
|
+
self,
|
|
710
|
+
func: Callable[[Any, Any], Any],
|
|
711
|
+
seed: Any = _SENTINEL,
|
|
712
|
+
) -> Any:
|
|
713
|
+
return FlpIt(self.data).aggregate(func, seed=seed)
|
|
714
|
+
|
|
715
|
+
@_guard_empty
|
|
716
|
+
def min(self) -> TItem:
|
|
717
|
+
return builtins.min(self.data)
|
|
718
|
+
|
|
719
|
+
@_guard_empty
|
|
720
|
+
def min_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
|
|
721
|
+
return builtins.min(self.data, key=key_selector)
|
|
722
|
+
|
|
723
|
+
@_guard_empty
|
|
724
|
+
def max(self) -> TItem:
|
|
725
|
+
return builtins.max(self.data)
|
|
726
|
+
|
|
727
|
+
@_guard_empty
|
|
728
|
+
def max_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
|
|
729
|
+
return builtins.max(self.data, key=key_selector)
|
|
730
|
+
|
|
731
|
+
def sum(
|
|
732
|
+
self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
|
|
733
|
+
) -> Union[int, float]:
|
|
734
|
+
"""Calculates the sum of elements, optionally applying a selector."""
|
|
735
|
+
return FlpIt(self.data).sum(selector)
|
|
736
|
+
|
|
737
|
+
def average(
|
|
738
|
+
self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
|
|
739
|
+
) -> float:
|
|
740
|
+
"""Calculates the arithmetic mean, optionally applying a selector."""
|
|
741
|
+
return FlpIt(self.data).average(selector)
|
|
742
|
+
|
|
743
|
+
# --- Overloads for the Type System ---
|
|
744
|
+
# 1. Native compatibility path (MUST be first): exact value match
|
|
745
|
+
@overload
|
|
746
|
+
def count(self, item: TItem) -> int: ...
|
|
747
|
+
|
|
748
|
+
# 2. LINQ style path: matching via predicate function
|
|
749
|
+
@overload
|
|
750
|
+
def count(self, item: Callable[[TItem], bool]) -> int: ...
|
|
751
|
+
|
|
752
|
+
# 3. LINQ style path: no arguments (counts everything)
|
|
753
|
+
@overload
|
|
754
|
+
def count(self) -> int: ...
|
|
755
|
+
|
|
756
|
+
# --- The Clean Implementation ---
|
|
757
|
+
# We name the parameter 'item' to perfectly match UserList, but default it to None
|
|
758
|
+
def count(self, item: Any = _SENTINEL) -> int:
|
|
759
|
+
"""Counts elements in the list matching an optional predicate or exact value."""
|
|
760
|
+
# Scenario C: No argument passed (.count()) -> Return total length
|
|
761
|
+
if item is _SENTINEL:
|
|
762
|
+
return len(self.data)
|
|
763
|
+
|
|
764
|
+
# Scenario A: A LINQ predicate function was passed
|
|
765
|
+
if callable(item):
|
|
766
|
+
return FlpIt(self.data).count(item)
|
|
767
|
+
|
|
768
|
+
# Scenario B: An exact raw value was passed (.count(4))
|
|
769
|
+
# Invokes the original parent implementation of UserList to remain 100% compliant
|
|
770
|
+
return super().count(item)
|
|
771
|
+
|
|
772
|
+
def element_at(self, index: int) -> TItem:
|
|
773
|
+
if index < 0 or index >= len(self.data):
|
|
774
|
+
raise IndexError("Index out of range")
|
|
775
|
+
return self.data[index]
|
|
776
|
+
|
|
777
|
+
def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
|
|
778
|
+
return FlpIt(self.data).first(predicate)
|
|
779
|
+
|
|
780
|
+
def first_or_default(
|
|
781
|
+
self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
|
|
782
|
+
) -> Union[TItem, TResult]:
|
|
783
|
+
return FlpIt(self.data).first_or_default(default, predicate)
|
|
784
|
+
|
|
785
|
+
def single(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
|
|
786
|
+
return FlpIt(self.data).single(predicate)
|
|
787
|
+
|
|
788
|
+
def to_list(self) -> FlpList[TItem]:
|
|
789
|
+
"""Explicitly returns a shallow copy instance to isolate mutations."""
|
|
790
|
+
return FlpList(self.data.copy())
|
|
File without changes
|