flpit 0.1.2__tar.gz → 0.1.4.dev5__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.4.dev5/PKG-INFO +186 -0
- flpit-0.1.4.dev5/README.md +167 -0
- {flpit-0.1.2 → flpit-0.1.4.dev5}/pyproject.toml +33 -7
- {flpit-0.1.2 → flpit-0.1.4.dev5}/pyproject.toml.orig +23 -10
- flpit-0.1.4.dev5/src/flp/__init__.py +21 -0
- flpit-0.1.4.dev5/src/flpit/__init__.py +37 -0
- {flpit-0.1.2/src/flp → flpit-0.1.4.dev5/src/flpit}/core/linq.py +167 -112
- flpit-0.1.2/src/flp/__init__.py → flpit-0.1.4.dev5/src/flpit/flp.py +1 -6
- flpit-0.1.2/PKG-INFO +0 -61
- flpit-0.1.2/README.md +0 -45
- {flpit-0.1.2/src/flp → flpit-0.1.4.dev5/src/flpit}/core/__init__.py +0 -0
- {flpit-0.1.2/src/flp → flpit-0.1.4.dev5/src/flpit}/py.typed +0 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: flpit
|
|
3
|
+
Version: 0.1.4.dev5
|
|
4
|
+
Summary: Fluent LINQ for Python — flip your iterables
|
|
5
|
+
Keywords: linq,fluent,iterable,pipeline,lazy-evaluation,pyrefly,type-safe,generators
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# FlpIt
|
|
21
|
+
|
|
22
|
+
> **Fluent LINQ for Python — LINQ semantics, Python-native execution**
|
|
23
|
+
|
|
24
|
+
`FlpIt` brings a strongly typed, LINQ-style API to standard Python iterables. It combines the fluent query model and semantics of .NET LINQ with Python's native iterable and generator model.
|
|
25
|
+
|
|
26
|
+
> **Semantic goal:** `flp` strives to faithfully reproduce .NET LINQ semantics.
|
|
27
|
+
> Where Python's language or type system requires a different API contract, the contract is adapted accordingly.
|
|
28
|
+
> The implementation is actively developed, and semantic deviations may still exist.
|
|
29
|
+
|
|
30
|
+
## ⚡ Key Features
|
|
31
|
+
|
|
32
|
+
* **⚡ Lazy Evaluation (`FlpIt`):** Deferred, streaming execution where the underlying operation permits it. `FlpIt` wraps a standard Python `Iterable[T]`.
|
|
33
|
+
* **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
|
|
34
|
+
* **🎯 LINQ Semantics:** Operators such as `.any()`, `.first()`, `.single()`, `.order_by()`, and `.then_by()` follow .NET LINQ semantics.
|
|
35
|
+
* **🔀 Independent Query Pipelines:** Derived queries are independent and never mutate or retroactively affect separately created queries.
|
|
36
|
+
* **🧠 Lazy Internal Materialization:** Some operators, such as ordering, require internal materialization during deferred execution, while the query itself remains lazy.
|
|
37
|
+
* **🔹 Static-First Typing:** Designed for precise `pyrefly` inference without plugins (mypy and pyright might give varying results).
|
|
38
|
+
* **🐍 Python-Native:** Built on standard Python iterables, iterators, generators, and callable semantics.
|
|
39
|
+
|
|
40
|
+
## 🤸 Flipping the Pipeline
|
|
41
|
+
|
|
42
|
+
Native Python functional operations tend to produce an inside-out, right-to-left reading pattern...
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# 🔁 Native Built-ins
|
|
46
|
+
result = list(map(lambda x: x * 10, filter(lambda x: x % 2 == 0, range(1, 11))))
|
|
47
|
+
|
|
48
|
+
# 🤸 FlpIt Pipeline
|
|
49
|
+
result = (
|
|
50
|
+
flp.it(range(1, 11))
|
|
51
|
+
.where(lambda x: x % 2 == 0)
|
|
52
|
+
.select(lambda x: x * 10)
|
|
53
|
+
.to_list()
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 💡 Quick Start
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
|
|
61
|
+
from flpit import flp, FlpIt, FlpList
|
|
62
|
+
|
|
63
|
+
# Deferred iterable via shorthand
|
|
64
|
+
data: FlpIt[int] = flp.it(range(1, 11))
|
|
65
|
+
|
|
66
|
+
# Lazy, streaming pipeline
|
|
67
|
+
query: FlpIt[int] = (
|
|
68
|
+
data
|
|
69
|
+
.where(lambda x: x % 2 == 0)
|
|
70
|
+
.select(lambda x: x * 10)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Explicit materialization
|
|
74
|
+
result: FlpList[int] = query.to_list()
|
|
75
|
+
# [20, 40, 60, 80, 100]
|
|
76
|
+
|
|
77
|
+
# Or start directly with an eager container
|
|
78
|
+
eager_list: FlpList[int] = flp.lst([1, 2, 3, 4])
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 🔬 LINQ Semantics, Python Runtime
|
|
82
|
+
|
|
83
|
+
`flpit` deliberately separates the two:
|
|
84
|
+
|
|
85
|
+
- **LINQ defines the query semantics.**
|
|
86
|
+
- **Python provides the underlying iterable and execution model.**
|
|
87
|
+
|
|
88
|
+
For example:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
query.any()
|
|
92
|
+
query.first()
|
|
93
|
+
query.single()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
use LINQ semantics, while native Python operations remain Pythonic:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
any(query)
|
|
100
|
+
list(query)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This provides a predictable LINQ vocabulary without attempting to replace Python's native iteration model.
|
|
104
|
+
|
|
105
|
+
## 🔗 Independent Queries
|
|
106
|
+
|
|
107
|
+
LINQ queries are composable without mutating the query they were derived from.
|
|
108
|
+
|
|
109
|
+
For example:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
@dataclass
|
|
113
|
+
class Item: group: str; value: int
|
|
114
|
+
|
|
115
|
+
original = flp.it([Item("A", 2), Item("B", 1)])
|
|
116
|
+
|
|
117
|
+
ordered = original.order_by(lambda x: x.group)
|
|
118
|
+
|
|
119
|
+
extended = original.concat([Item("A", 1), Item("B", 2)])
|
|
120
|
+
|
|
121
|
+
ordered_by_group_and_value = ordered.then_by(lambda x: x.value)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The resulting query graph is:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
original
|
|
128
|
+
│
|
|
129
|
+
├── order_by ──→ ordered
|
|
130
|
+
│ │
|
|
131
|
+
│ └── then_by ──→ ordered_by_group_and_value
|
|
132
|
+
│
|
|
133
|
+
└── concat ─────→ extended
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`ordered_by_group_and_value` is derived from `ordered`.
|
|
137
|
+
It does not modify `original`, and it is not affected by the separately created `extended` query.
|
|
138
|
+
|
|
139
|
+
This matches the independent-query behavior of .NET LINQ.
|
|
140
|
+
|
|
141
|
+
## 🧠 Lazy Pipelines
|
|
142
|
+
|
|
143
|
+
Streaming operations do not materialize intermediate results:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
query = (
|
|
147
|
+
flp.it(source)
|
|
148
|
+
.where(predicate)
|
|
149
|
+
.select(selector)
|
|
150
|
+
.order_by(key_selector)
|
|
151
|
+
.take(100)
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Conceptually:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
source
|
|
159
|
+
│
|
|
160
|
+
▼
|
|
161
|
+
where ← streaming
|
|
162
|
+
│
|
|
163
|
+
▼
|
|
164
|
+
select ← streaming
|
|
165
|
+
│
|
|
166
|
+
▼
|
|
167
|
+
order_by ← lazy, internally buffers when enumerated
|
|
168
|
+
│
|
|
169
|
+
▼
|
|
170
|
+
take ← streaming
|
|
171
|
+
│
|
|
172
|
+
▼
|
|
173
|
+
to_list ← consumer-facing materialization
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Operations such as `order_by()` necessarily materialize their input internally to establish ordering.
|
|
177
|
+
|
|
178
|
+
## ⚙️ Installation
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
uv add flpit
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## 📜 License
|
|
185
|
+
|
|
186
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# FlpIt
|
|
2
|
+
|
|
3
|
+
> **Fluent LINQ for Python — LINQ semantics, Python-native execution**
|
|
4
|
+
|
|
5
|
+
`FlpIt` brings a strongly typed, LINQ-style API to standard Python iterables. It combines the fluent query model and semantics of .NET LINQ with Python's native iterable and generator model.
|
|
6
|
+
|
|
7
|
+
> **Semantic goal:** `flp` strives to faithfully reproduce .NET LINQ semantics.
|
|
8
|
+
> Where Python's language or type system requires a different API contract, the contract is adapted accordingly.
|
|
9
|
+
> The implementation is actively developed, and semantic deviations may still exist.
|
|
10
|
+
|
|
11
|
+
## ⚡ Key Features
|
|
12
|
+
|
|
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.
|
|
16
|
+
* **🔀 Independent Query Pipelines:** Derived queries are independent and never mutate or retroactively affect separately created queries.
|
|
17
|
+
* **🧠 Lazy Internal Materialization:** Some operators, such as ordering, require internal materialization during deferred execution, while the query itself remains lazy.
|
|
18
|
+
* **🔹 Static-First Typing:** Designed for precise `pyrefly` inference without plugins (mypy and pyright might give varying results).
|
|
19
|
+
* **🐍 Python-Native:** Built on standard Python iterables, iterators, generators, and callable semantics.
|
|
20
|
+
|
|
21
|
+
## 🤸 Flipping the Pipeline
|
|
22
|
+
|
|
23
|
+
Native Python functional operations tend to produce an inside-out, right-to-left reading pattern...
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
# 🔁 Native Built-ins
|
|
27
|
+
result = list(map(lambda x: x * 10, filter(lambda x: x % 2 == 0, range(1, 11))))
|
|
28
|
+
|
|
29
|
+
# 🤸 FlpIt Pipeline
|
|
30
|
+
result = (
|
|
31
|
+
flp.it(range(1, 11))
|
|
32
|
+
.where(lambda x: x % 2 == 0)
|
|
33
|
+
.select(lambda x: x * 10)
|
|
34
|
+
.to_list()
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 💡 Quick Start
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
|
|
42
|
+
from flpit import flp, FlpIt, FlpList
|
|
43
|
+
|
|
44
|
+
# Deferred iterable via shorthand
|
|
45
|
+
data: FlpIt[int] = flp.it(range(1, 11))
|
|
46
|
+
|
|
47
|
+
# Lazy, streaming pipeline
|
|
48
|
+
query: FlpIt[int] = (
|
|
49
|
+
data
|
|
50
|
+
.where(lambda x: x % 2 == 0)
|
|
51
|
+
.select(lambda x: x * 10)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Explicit materialization
|
|
55
|
+
result: FlpList[int] = query.to_list()
|
|
56
|
+
# [20, 40, 60, 80, 100]
|
|
57
|
+
|
|
58
|
+
# Or start directly with an eager container
|
|
59
|
+
eager_list: FlpList[int] = flp.lst([1, 2, 3, 4])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 🔬 LINQ Semantics, Python Runtime
|
|
63
|
+
|
|
64
|
+
`flpit` deliberately separates the two:
|
|
65
|
+
|
|
66
|
+
- **LINQ defines the query semantics.**
|
|
67
|
+
- **Python provides the underlying iterable and execution model.**
|
|
68
|
+
|
|
69
|
+
For example:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
query.any()
|
|
73
|
+
query.first()
|
|
74
|
+
query.single()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
use LINQ semantics, while native Python operations remain Pythonic:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
any(query)
|
|
81
|
+
list(query)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This provides a predictable LINQ vocabulary without attempting to replace Python's native iteration model.
|
|
85
|
+
|
|
86
|
+
## 🔗 Independent Queries
|
|
87
|
+
|
|
88
|
+
LINQ queries are composable without mutating the query they were derived from.
|
|
89
|
+
|
|
90
|
+
For example:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
@dataclass
|
|
94
|
+
class Item: group: str; value: int
|
|
95
|
+
|
|
96
|
+
original = flp.it([Item("A", 2), Item("B", 1)])
|
|
97
|
+
|
|
98
|
+
ordered = original.order_by(lambda x: x.group)
|
|
99
|
+
|
|
100
|
+
extended = original.concat([Item("A", 1), Item("B", 2)])
|
|
101
|
+
|
|
102
|
+
ordered_by_group_and_value = ordered.then_by(lambda x: x.value)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The resulting query graph is:
|
|
106
|
+
|
|
107
|
+
```text
|
|
108
|
+
original
|
|
109
|
+
│
|
|
110
|
+
├── order_by ──→ ordered
|
|
111
|
+
│ │
|
|
112
|
+
│ └── then_by ──→ ordered_by_group_and_value
|
|
113
|
+
│
|
|
114
|
+
└── concat ─────→ extended
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`ordered_by_group_and_value` is derived from `ordered`.
|
|
118
|
+
It does not modify `original`, and it is not affected by the separately created `extended` query.
|
|
119
|
+
|
|
120
|
+
This matches the independent-query behavior of .NET LINQ.
|
|
121
|
+
|
|
122
|
+
## 🧠 Lazy Pipelines
|
|
123
|
+
|
|
124
|
+
Streaming operations do not materialize intermediate results:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
query = (
|
|
128
|
+
flp.it(source)
|
|
129
|
+
.where(predicate)
|
|
130
|
+
.select(selector)
|
|
131
|
+
.order_by(key_selector)
|
|
132
|
+
.take(100)
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Conceptually:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
source
|
|
140
|
+
│
|
|
141
|
+
▼
|
|
142
|
+
where ← streaming
|
|
143
|
+
│
|
|
144
|
+
▼
|
|
145
|
+
select ← streaming
|
|
146
|
+
│
|
|
147
|
+
▼
|
|
148
|
+
order_by ← lazy, internally buffers when enumerated
|
|
149
|
+
│
|
|
150
|
+
▼
|
|
151
|
+
take ← streaming
|
|
152
|
+
│
|
|
153
|
+
▼
|
|
154
|
+
to_list ← consumer-facing materialization
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Operations such as `order_by()` necessarily materialize their input internally to establish ordering.
|
|
158
|
+
|
|
159
|
+
## ⚙️ Installation
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
uv add flpit
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## 📜 License
|
|
166
|
+
|
|
167
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flpit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.4.dev5"
|
|
4
4
|
description = "Fluent LINQ for Python — flip your iterables"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
|
+
keywords = [
|
|
8
|
+
"linq",
|
|
9
|
+
"fluent",
|
|
10
|
+
"iterable",
|
|
11
|
+
"pipeline",
|
|
12
|
+
"lazy-evaluation",
|
|
13
|
+
"pyrefly",
|
|
14
|
+
"type-safe",
|
|
15
|
+
"generators",
|
|
16
|
+
]
|
|
7
17
|
classifiers = [
|
|
8
18
|
"Development Status :: 3 - Alpha",
|
|
9
19
|
"Intended Audience :: Developers",
|
|
@@ -12,7 +22,9 @@ classifiers = [
|
|
|
12
22
|
"Programming Language :: Python :: 3.11",
|
|
13
23
|
"Programming Language :: Python :: 3.12",
|
|
14
24
|
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
15
26
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Typing :: Typed",
|
|
16
28
|
]
|
|
17
29
|
dependencies = []
|
|
18
30
|
|
|
@@ -25,6 +37,7 @@ dev = [
|
|
|
25
37
|
"hypothesis>=6.168.0",
|
|
26
38
|
"pyrefly>=1.3.1",
|
|
27
39
|
"pyright>=1.1.414",
|
|
40
|
+
"pytest-benchmark>=5.3.0",
|
|
28
41
|
]
|
|
29
42
|
|
|
30
43
|
[build-system]
|
|
@@ -32,13 +45,26 @@ requires = ["uv_build>=0.12.10,<0.13.0"]
|
|
|
32
45
|
build-backend = "uv_build"
|
|
33
46
|
|
|
34
47
|
[tool.uv.build-backend]
|
|
35
|
-
module-name = "
|
|
36
|
-
|
|
48
|
+
module-name = "flpit"
|
|
49
|
+
namespace = true
|
|
50
|
+
source-include = [
|
|
51
|
+
"src/flp/**/*",
|
|
52
|
+
"src/flpit/**/*",
|
|
53
|
+
]
|
|
37
54
|
|
|
38
|
-
[tool.
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
[tool.pyrefly]
|
|
56
|
+
project-includes = ["src"]
|
|
57
|
+
project-excludes = [
|
|
41
58
|
"**/__pycache__",
|
|
42
59
|
"tests",
|
|
43
60
|
]
|
|
44
|
-
|
|
61
|
+
search-path = ["src"]
|
|
62
|
+
preset = "strict"
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
pythonpath = [
|
|
66
|
+
"src",
|
|
67
|
+
"tests",
|
|
68
|
+
]
|
|
69
|
+
addopts = "-m 'not benchmark'"
|
|
70
|
+
markers = ["benchmark: performance benchmarks"]
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flpit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.4.dev5"
|
|
4
4
|
description = "Fluent LINQ for Python — flip your iterables"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
license = { text = "MIT" }
|
|
8
|
+
keywords = ["linq", "fluent", "iterable", "pipeline", "lazy-evaluation", "pyrefly", "type-safe", "generators"]
|
|
8
9
|
classifiers = [
|
|
9
10
|
"Development Status :: 3 - Alpha",
|
|
10
11
|
"Intended Audience :: Developers",
|
|
@@ -13,7 +14,9 @@ classifiers = [
|
|
|
13
14
|
"Programming Language :: Python :: 3.11",
|
|
14
15
|
"Programming Language :: Python :: 3.12",
|
|
15
16
|
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
16
18
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
"Typing :: Typed"
|
|
17
20
|
]
|
|
18
21
|
dependencies = []
|
|
19
22
|
|
|
@@ -23,6 +26,7 @@ dev = [
|
|
|
23
26
|
"hypothesis>=6.168.0",
|
|
24
27
|
"pyrefly>=1.3.1",
|
|
25
28
|
"pyright>=1.1.414",
|
|
29
|
+
"pytest-benchmark>=5.3.0",
|
|
26
30
|
]
|
|
27
31
|
|
|
28
32
|
[build-system]
|
|
@@ -30,14 +34,23 @@ requires = ["uv_build>=0.12.10,<0.13.0"]
|
|
|
30
34
|
build-backend = "uv_build"
|
|
31
35
|
|
|
32
36
|
[tool.uv.build-backend]
|
|
33
|
-
module-name = "
|
|
34
|
-
|
|
37
|
+
module-name = "flpit"
|
|
38
|
+
namespace = true
|
|
39
|
+
source-include = ["src/flp/**/*", "src/flpit/**/*"]
|
|
35
40
|
|
|
36
|
-
[tool.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
[tool.pyrefly]
|
|
42
|
+
project-includes = ["src"]
|
|
43
|
+
project-excludes = ["**/__pycache__", "tests"]
|
|
44
|
+
search-path = ["src"]
|
|
45
|
+
preset = "strict"
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
pythonpath = [
|
|
49
|
+
"src",
|
|
50
|
+
"tests"
|
|
51
|
+
]
|
|
52
|
+
addopts = "-m 'not benchmark'"
|
|
53
|
+
markers = [
|
|
54
|
+
"benchmark: performance benchmarks",
|
|
55
|
+
]
|
|
40
56
|
|
|
41
|
-
#[tool.pyrefly]
|
|
42
|
-
#extra-paths = ["src", "tests"]
|
|
43
|
-
#python-interpreter-path = ".venv/bin/python" # or .venv/Scripts/python.exe on Windows
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
import flpit # The NEW location
|
|
3
|
+
|
|
4
|
+
# Enforce a loud, clear deprecation alert
|
|
5
|
+
warnings.warn(
|
|
6
|
+
"The top-level 'flp' namespace is deprecated and has been moved to 'flpit'. "
|
|
7
|
+
"Please update your imports to 'from flpit import flp, FlpIt, FlpList'"
|
|
8
|
+
"This legacy entry point will throw a hard ImportError in a future version.",
|
|
9
|
+
DeprecationWarning,
|
|
10
|
+
stacklevel=2
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = getattr(flpit.flp, "__all__", []) + getattr(flpit, "__all__", [])
|
|
14
|
+
mods = [flpit, flpit.flp]
|
|
15
|
+
|
|
16
|
+
def __getattr__(name: str):
|
|
17
|
+
for mod in mods:
|
|
18
|
+
if hasattr(mod, name):
|
|
19
|
+
return getattr(mod, name)
|
|
20
|
+
|
|
21
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
|
|
@@ -0,0 +1,37 @@
|
|
|
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 flpit import flp
|
|
12
|
+
from flpit.core.linq import (
|
|
13
|
+
FlpIt,
|
|
14
|
+
FlpList,
|
|
15
|
+
Grouping,
|
|
16
|
+
OrderedIt,
|
|
17
|
+
EmptySequenceError,
|
|
18
|
+
NoMatchError,
|
|
19
|
+
MultipleElementsError,
|
|
20
|
+
MultipleMatchesError,
|
|
21
|
+
SourceNoneError,
|
|
22
|
+
PredicateNoneError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"FlpIt",
|
|
27
|
+
"FlpList",
|
|
28
|
+
"Grouping",
|
|
29
|
+
"OrderedIt",
|
|
30
|
+
"EmptySequenceError",
|
|
31
|
+
"NoMatchError",
|
|
32
|
+
"MultipleElementsError",
|
|
33
|
+
"MultipleMatchesError",
|
|
34
|
+
"SourceNoneError",
|
|
35
|
+
"PredicateNoneError",
|
|
36
|
+
]
|
|
37
|
+
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import cast
|
|
2
|
+
from threading import Lock
|
|
4
3
|
|
|
5
4
|
import builtins
|
|
6
5
|
from collections import UserList
|
|
@@ -19,6 +18,7 @@ from typing import (
|
|
|
19
18
|
Type,
|
|
20
19
|
TypeVar,
|
|
21
20
|
Union,
|
|
21
|
+
override,
|
|
22
22
|
overload,
|
|
23
23
|
cast as typing_cast,
|
|
24
24
|
)
|
|
@@ -34,26 +34,34 @@ _MISSING = object()
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class EmptySequenceError(ValueError):
|
|
37
|
-
def __init__(self
|
|
38
|
-
super().__init__(
|
|
37
|
+
def __init__(self):
|
|
38
|
+
super().__init__("Sequence contains no elements")
|
|
39
39
|
|
|
40
40
|
class NoMatchError(ValueError):
|
|
41
|
-
def __init__(self
|
|
42
|
-
super().__init__(
|
|
41
|
+
def __init__(self):
|
|
42
|
+
super().__init__("Sequence contains no matching elements")
|
|
43
43
|
|
|
44
44
|
class MultipleMatchesError(ValueError):
|
|
45
|
-
def __init__(self
|
|
46
|
-
super().__init__(
|
|
45
|
+
def __init__(self):
|
|
46
|
+
super().__init__("Sequence contains more than one matching element")
|
|
47
47
|
|
|
48
48
|
class MultipleElementsError(ValueError):
|
|
49
|
-
def __init__(self
|
|
50
|
-
super().__init__(
|
|
49
|
+
def __init__(self):
|
|
50
|
+
super().__init__("Sequence contains more than one element")
|
|
51
|
+
|
|
52
|
+
class SourceNoneError(TypeError):
|
|
53
|
+
def __init__(self):
|
|
54
|
+
super().__init__("Value cannot be None. (Argument 'source')")
|
|
55
|
+
|
|
56
|
+
class PredicateNoneError(TypeError):
|
|
57
|
+
def __init__(self):
|
|
58
|
+
super().__init__("Value cannot be None. (Argument 'predicate')")
|
|
51
59
|
|
|
52
60
|
|
|
53
61
|
def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
54
62
|
"""Catches Python's native empty-sequence ValueError and re-raises LINQ-compliant error."""
|
|
55
63
|
@wraps(func)
|
|
56
|
-
def wrapper(self, *args: Any, **kwargs: Any) -> Any:
|
|
64
|
+
def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
|
57
65
|
try:
|
|
58
66
|
return func(self, *args, **kwargs)
|
|
59
67
|
except ValueError as e:
|
|
@@ -90,8 +98,10 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
90
98
|
"""
|
|
91
99
|
__slots__ = ("_iterable",)
|
|
92
100
|
|
|
93
|
-
def __init__(self,
|
|
94
|
-
|
|
101
|
+
def __init__(self, source: Iterable[TItem]) -> None:
|
|
102
|
+
if source is None:
|
|
103
|
+
raise SourceNoneError()
|
|
104
|
+
self._iterable: Iterable[TItem] = source
|
|
95
105
|
|
|
96
106
|
def __iter__(self) -> Iterator[TItem]:
|
|
97
107
|
return iter(self._iterable)
|
|
@@ -371,7 +381,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
371
381
|
|
|
372
382
|
def last(
|
|
373
383
|
self,
|
|
374
|
-
predicate: Optional[Callable[[TItem], bool]] =
|
|
384
|
+
predicate: Optional[Callable[[TItem], bool]] = _SENTINEL,
|
|
375
385
|
) -> TItem:
|
|
376
386
|
"""
|
|
377
387
|
Returns the last element of the sequence, or the last element
|
|
@@ -384,24 +394,27 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
384
394
|
Raises EmptySequenceError if the sequence is empty or if no
|
|
385
395
|
element satisfies the predicate.
|
|
386
396
|
"""
|
|
387
|
-
|
|
397
|
+
if predicate is None:
|
|
398
|
+
raise PredicateNoneError()
|
|
399
|
+
|
|
400
|
+
last_item: TItem | object = _MISSING
|
|
388
401
|
|
|
389
402
|
for item in self:
|
|
390
|
-
if predicate is
|
|
403
|
+
if predicate is _SENTINEL or predicate(item):
|
|
391
404
|
last_item = item
|
|
392
405
|
|
|
393
|
-
if last_item is
|
|
394
|
-
raise EmptySequenceError() if predicate is
|
|
406
|
+
if last_item is _MISSING:
|
|
407
|
+
raise EmptySequenceError() if predicate is _SENTINEL else NoMatchError()
|
|
395
408
|
|
|
396
|
-
return
|
|
409
|
+
return typing_cast(TItem, last_item)
|
|
397
410
|
|
|
398
|
-
def _extreme(self, func, **kwargs) -> TItem:
|
|
411
|
+
def _extreme(self, func: Any, **kwargs: Any) -> TItem:
|
|
399
412
|
result = func(self, default=_MISSING, **kwargs)
|
|
400
413
|
|
|
401
414
|
if result is _MISSING:
|
|
402
415
|
raise EmptySequenceError()
|
|
403
416
|
|
|
404
|
-
return
|
|
417
|
+
return typing_cast(TItem, result)
|
|
405
418
|
|
|
406
419
|
def min(self) -> TItem:
|
|
407
420
|
"""Returns the minimum value in a sequence."""
|
|
@@ -486,6 +499,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
486
499
|
except (EmptySequenceError, NoMatchError):
|
|
487
500
|
return default
|
|
488
501
|
|
|
502
|
+
# pyrefly: ignore [bad-function-definition]
|
|
489
503
|
def single(self, predicate: Optional[Callable[[TItem], bool]] = _SENTINEL) -> TItem:
|
|
490
504
|
"""Returns the single, unique element matching a predicate."""
|
|
491
505
|
if predicate is None:
|
|
@@ -513,24 +527,58 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
513
527
|
return FlpList(self)
|
|
514
528
|
|
|
515
529
|
|
|
516
|
-
|
|
530
|
+
class _OrderState(Generic[TItem]):
|
|
531
|
+
"""
|
|
532
|
+
Shared lazy snapshot for an OrderBy/ThenBy chain.
|
|
533
|
+
|
|
534
|
+
The upstream source is consumed at most once. After materialization,
|
|
535
|
+
the original source reference is released.
|
|
536
|
+
"""
|
|
537
|
+
|
|
538
|
+
__slots__ = ("_source", "_items", "_lock")
|
|
539
|
+
|
|
540
|
+
def __init__(self, source: Iterable[TItem]) -> None:
|
|
541
|
+
self._source: Optional[Iterable[TItem]] = source
|
|
542
|
+
self._items: Optional[list[TItem]] = None
|
|
543
|
+
self._lock = Lock()
|
|
544
|
+
|
|
545
|
+
def materialize(self) -> list[TItem]:
|
|
546
|
+
items = self._items
|
|
547
|
+
|
|
548
|
+
if items is not None:
|
|
549
|
+
return items
|
|
550
|
+
|
|
551
|
+
with self._lock:
|
|
552
|
+
items = self._items
|
|
553
|
+
|
|
554
|
+
if items is None:
|
|
555
|
+
source = self._source
|
|
556
|
+
|
|
557
|
+
if source is None:
|
|
558
|
+
raise RuntimeError("OrderState source is unavailable")
|
|
559
|
+
|
|
560
|
+
items = list(source)
|
|
561
|
+
|
|
562
|
+
self._source = None
|
|
563
|
+
self._items = items
|
|
564
|
+
|
|
565
|
+
return items
|
|
566
|
+
|
|
517
567
|
|
|
518
568
|
class OrderedIt(FlpIt[TItem]):
|
|
519
569
|
"""
|
|
520
|
-
|
|
570
|
+
Lazy, memoized ordered iterable.
|
|
521
571
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
result.
|
|
572
|
+
The source is materialized once per OrderBy chain.
|
|
573
|
+
ThenBy nodes share that source snapshot but maintain independent
|
|
574
|
+
ordered-result caches.
|
|
525
575
|
"""
|
|
526
576
|
|
|
527
577
|
__slots__ = (
|
|
528
|
-
"
|
|
529
|
-
"
|
|
530
|
-
"_descending",
|
|
531
|
-
"_parent",
|
|
578
|
+
"_state",
|
|
579
|
+
"_criteria",
|
|
532
580
|
"_cached_result",
|
|
533
|
-
"
|
|
581
|
+
"_result_lock",
|
|
534
582
|
)
|
|
535
583
|
|
|
536
584
|
def __init__(
|
|
@@ -538,116 +586,96 @@ class OrderedIt(FlpIt[TItem]):
|
|
|
538
586
|
source: Iterable[TItem],
|
|
539
587
|
key_selector: Callable[[TItem], Any],
|
|
540
588
|
descending: bool = False,
|
|
541
|
-
parent: Optional["OrderedIt[TItem]"] = None,
|
|
542
589
|
) -> None:
|
|
543
|
-
|
|
590
|
+
# Pass () to FlpIt to prevent storing a permanent reference to the raw source
|
|
591
|
+
super().__init__(())
|
|
544
592
|
|
|
545
|
-
|
|
546
|
-
self.
|
|
547
|
-
self.
|
|
548
|
-
|
|
593
|
+
|
|
594
|
+
self._state = _OrderState(source)
|
|
595
|
+
self._criteria: tuple[tuple[Callable[[TItem], Any], bool], ...] = (
|
|
596
|
+
(key_selector, descending),
|
|
597
|
+
)
|
|
549
598
|
|
|
550
599
|
self._cached_result: Optional[list[TItem]] = None
|
|
551
|
-
self.
|
|
600
|
+
self._result_lock = Lock()
|
|
601
|
+
|
|
602
|
+
@classmethod
|
|
603
|
+
def _from_parent(
|
|
604
|
+
cls,
|
|
605
|
+
parent: OrderedIt[TItem],
|
|
606
|
+
key_selector: Callable[[TItem], Any],
|
|
607
|
+
descending: bool,
|
|
608
|
+
) -> OrderedIt[TItem]:
|
|
609
|
+
"""
|
|
610
|
+
Create a child ordering node sharing the parent's source state.
|
|
611
|
+
|
|
612
|
+
The FlpIt source is intentionally empty because OrderedIt overrides
|
|
613
|
+
__iter__ and uses _OrderState as its real source.
|
|
614
|
+
"""
|
|
615
|
+
child = cls.__new__(cls)
|
|
616
|
+
|
|
617
|
+
# Do not retain the parent OrderedIt through FlpIt's _iterable.
|
|
618
|
+
FlpIt.__init__(child, ())
|
|
619
|
+
|
|
620
|
+
child._state = parent._state
|
|
621
|
+
child._criteria = (
|
|
622
|
+
*parent._criteria,
|
|
623
|
+
(key_selector, descending),
|
|
624
|
+
)
|
|
625
|
+
child._cached_result = None
|
|
626
|
+
child._result_lock = Lock()
|
|
627
|
+
|
|
628
|
+
return child
|
|
552
629
|
|
|
553
630
|
def then_by(
|
|
554
631
|
self,
|
|
555
632
|
key_selector: Callable[[TItem], Any],
|
|
556
|
-
) ->
|
|
557
|
-
return
|
|
558
|
-
self
|
|
633
|
+
) -> OrderedIt[TItem]:
|
|
634
|
+
return self._from_parent(
|
|
635
|
+
self,
|
|
559
636
|
key_selector,
|
|
560
637
|
descending=False,
|
|
561
|
-
parent=self,
|
|
562
638
|
)
|
|
563
639
|
|
|
564
640
|
def then_by_descending(
|
|
565
641
|
self,
|
|
566
642
|
key_selector: Callable[[TItem], Any],
|
|
567
|
-
) ->
|
|
568
|
-
return
|
|
569
|
-
self
|
|
643
|
+
) -> OrderedIt[TItem]:
|
|
644
|
+
return self._from_parent(
|
|
645
|
+
self,
|
|
570
646
|
key_selector,
|
|
571
647
|
descending=True,
|
|
572
|
-
parent=self,
|
|
573
648
|
)
|
|
574
649
|
|
|
650
|
+
@override
|
|
575
651
|
def __iter__(self) -> Iterator[TItem]:
|
|
576
652
|
cached = self._cached_result
|
|
577
653
|
|
|
578
654
|
if cached is None:
|
|
579
|
-
with self.
|
|
655
|
+
with self._result_lock:
|
|
580
656
|
cached = self._cached_result
|
|
581
657
|
|
|
582
658
|
if cached is None:
|
|
583
|
-
#
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
(node._key_selector, node._descending)
|
|
659
|
+
# Never mutate the shared source snapshot.
|
|
660
|
+
result = list(self._state.materialize())
|
|
661
|
+
|
|
662
|
+
# Python sort is stable, so apply the least-significant
|
|
663
|
+
# criterion first.
|
|
664
|
+
for selector, descending in reversed(self._criteria):
|
|
665
|
+
result.sort(
|
|
666
|
+
key=selector,
|
|
667
|
+
reverse=descending,
|
|
593
668
|
)
|
|
594
|
-
node = node._parent
|
|
595
|
-
|
|
596
|
-
comparers.reverse()
|
|
597
|
-
|
|
598
|
-
# Consume the source exactly once.
|
|
599
|
-
items = list(self._source)
|
|
600
|
-
|
|
601
|
-
class SortWrapper:
|
|
602
|
-
__slots__ = ("obj", "keys")
|
|
603
|
-
|
|
604
|
-
def __init__(self, obj: Any) -> None:
|
|
605
|
-
self.obj = obj
|
|
606
|
-
self.keys = [
|
|
607
|
-
selector(obj)
|
|
608
|
-
for selector, _ in comparers
|
|
609
|
-
]
|
|
610
|
-
|
|
611
|
-
def __lt__(self, other: "SortWrapper") -> bool:
|
|
612
|
-
for index, (_, descending) in enumerate(comparers):
|
|
613
|
-
left = self.keys[index]
|
|
614
|
-
right = other.keys[index]
|
|
615
|
-
|
|
616
|
-
if left == right:
|
|
617
|
-
continue
|
|
618
|
-
|
|
619
|
-
return right < left if descending else left < right
|
|
620
|
-
|
|
621
|
-
return False
|
|
622
|
-
|
|
623
|
-
wrapped_items = [
|
|
624
|
-
SortWrapper(item)
|
|
625
|
-
for item in items
|
|
626
|
-
]
|
|
627
|
-
|
|
628
|
-
wrapped_items.sort()
|
|
629
|
-
|
|
630
|
-
# Store ONLY the actual result objects.
|
|
631
|
-
cached = [
|
|
632
|
-
wrapper.obj
|
|
633
|
-
for wrapper in wrapped_items
|
|
634
|
-
]
|
|
635
669
|
|
|
670
|
+
cached = result
|
|
636
671
|
self._cached_result = cached
|
|
637
672
|
|
|
638
|
-
# Important: __iter__ is a generator function.
|
|
639
|
-
# Release potentially large temporary structures
|
|
640
|
-
# before yielding anything.
|
|
641
|
-
del wrapped_items
|
|
642
|
-
del items
|
|
643
|
-
del comparers
|
|
644
|
-
|
|
645
673
|
yield from cached
|
|
646
674
|
|
|
647
675
|
|
|
648
676
|
class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
|
|
649
677
|
"""Represents a collection of elements sharing a common key (.NET IGrouping<TKey, TElement>)."""
|
|
650
|
-
__slots__ = ("_key")
|
|
678
|
+
__slots__ = ("_key", )
|
|
651
679
|
|
|
652
680
|
def __init__(self, key: TKey, elements: Iterable[TItem]) -> None:
|
|
653
681
|
self._key: TKey = key
|
|
@@ -674,6 +702,12 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
674
702
|
| Fluent List
|
|
675
703
|
A materialized list extending UserList that yields lazy FlpIt instances for query operations.
|
|
676
704
|
"""
|
|
705
|
+
def __init__(self, source: TItem=_SENTINEL):
|
|
706
|
+
if source is None:
|
|
707
|
+
raise SourceNoneError()
|
|
708
|
+
if source is _SENTINEL:
|
|
709
|
+
source = [] # don't depend on the base class to do the right thing
|
|
710
|
+
super().__init__(source)
|
|
677
711
|
|
|
678
712
|
def add(self, item: TItem) -> None:
|
|
679
713
|
"""
|
|
@@ -786,6 +820,7 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
786
820
|
second: Iterable[TOther],
|
|
787
821
|
result_selector: Optional[Callable[[TItem, TOther], Any]] = None,
|
|
788
822
|
) -> FlpIt[Any]:
|
|
823
|
+
# pyrefly: ignore [bad-argument-type] # yeah well if overloads are just pretend
|
|
789
824
|
return FlpIt(self.data).zip(second, result_selector)
|
|
790
825
|
|
|
791
826
|
def chunk(self, size: int) -> FlpIt[FlpList[TItem]]:
|
|
@@ -823,7 +858,7 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
823
858
|
|
|
824
859
|
@_guard_empty
|
|
825
860
|
def min(self) -> TItem:
|
|
826
|
-
return builtins.min(self.data)
|
|
861
|
+
return builtins.min(self.data) # pyrefly: ignore [bad-specialization]
|
|
827
862
|
|
|
828
863
|
@_guard_empty
|
|
829
864
|
def min_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
|
|
@@ -831,7 +866,7 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
831
866
|
|
|
832
867
|
@_guard_empty
|
|
833
868
|
def max(self) -> TItem:
|
|
834
|
-
return builtins.max(self.data)
|
|
869
|
+
return builtins.max(self.data) # pyrefly: ignore [bad-specialization]
|
|
835
870
|
|
|
836
871
|
@_guard_empty
|
|
837
872
|
def max_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
|
|
@@ -850,7 +885,7 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
850
885
|
return FlpIt(self.data).average(selector)
|
|
851
886
|
|
|
852
887
|
# --- Overloads for the Type System ---
|
|
853
|
-
# 1. Native compatibility path (MUST be first): exact value match
|
|
888
|
+
# 1. Native Python UserList compatibility path (MUST be first): exact value match
|
|
854
889
|
@overload
|
|
855
890
|
def count(self, item: TItem) -> int: ...
|
|
856
891
|
|
|
@@ -864,8 +899,24 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
864
899
|
|
|
865
900
|
# --- The Clean Implementation ---
|
|
866
901
|
# We name the parameter 'item' to perfectly match UserList, but default it to None
|
|
902
|
+
@override
|
|
867
903
|
def count(self, item: Any = _SENTINEL) -> int:
|
|
868
|
-
"""
|
|
904
|
+
"""
|
|
905
|
+
Counts elements using Python/UserList or .NET LINQ conventions.
|
|
906
|
+
|
|
907
|
+
Supported forms:
|
|
908
|
+
|
|
909
|
+
count() -> count all elements (.NET LINQ)
|
|
910
|
+
count(predicate) -> count elements matching predicate (.NET LINQ)
|
|
911
|
+
count(value) -> count occurrences of an exact value (Python)
|
|
912
|
+
|
|
913
|
+
Note:
|
|
914
|
+
Because Python callables are also valid list values, ``count(value)``
|
|
915
|
+
is inherently ambiguous when ``value`` is callable. A callable
|
|
916
|
+
argument is interpreted as a LINQ predicate, so counting occurrences
|
|
917
|
+
of a callable object via the native ``UserList.count(value)`` behavior
|
|
918
|
+
is not distinguishable from ``count(predicate)``.
|
|
919
|
+
"""
|
|
869
920
|
# Scenario C: No argument passed (.count()) -> Return total length
|
|
870
921
|
if item is _SENTINEL:
|
|
871
922
|
return len(self.data)
|
|
@@ -894,7 +945,7 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
894
945
|
|
|
895
946
|
def last(
|
|
896
947
|
self,
|
|
897
|
-
predicate: Optional[Callable[[TItem], bool]] =
|
|
948
|
+
predicate: Optional[Callable[[TItem], bool]] = _SENTINEL,
|
|
898
949
|
) -> TItem:
|
|
899
950
|
"""
|
|
900
951
|
Returns the last element of the sequence, or the last element
|
|
@@ -908,6 +959,9 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
908
959
|
element satisfies the predicate.
|
|
909
960
|
"""
|
|
910
961
|
if predicate is None:
|
|
962
|
+
raise PredicateNoneError()
|
|
963
|
+
|
|
964
|
+
if predicate is _SENTINEL:
|
|
911
965
|
if not self.data:
|
|
912
966
|
raise EmptySequenceError()
|
|
913
967
|
return self.data[-1]
|
|
@@ -916,13 +970,14 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
916
970
|
if predicate(item):
|
|
917
971
|
return item
|
|
918
972
|
|
|
919
|
-
raise NoMatchError()
|
|
973
|
+
raise EmptySequenceError() if predicate is None else NoMatchError()
|
|
920
974
|
|
|
921
975
|
def first_or_default(
|
|
922
976
|
self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
|
|
923
977
|
) -> Union[TItem, TResult]:
|
|
924
978
|
return FlpIt(self.data).first_or_default(default, predicate)
|
|
925
979
|
|
|
980
|
+
# pyrefly: ignore [bad-function-definition]
|
|
926
981
|
def single(self, predicate: Optional[Callable[[TItem], bool]] = _SENTINEL) -> TItem:
|
|
927
982
|
return FlpIt(self.data).single(predicate)
|
|
928
983
|
|
|
@@ -3,12 +3,11 @@ Fluent LINQ for Python — flip your iterables.
|
|
|
3
3
|
"""
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
from importlib.metadata import version
|
|
6
|
-
__version__ = version("flpit")
|
|
7
6
|
|
|
8
7
|
import builtins
|
|
9
8
|
from typing import Iterable as _Iterable, TypeVar
|
|
10
9
|
|
|
11
|
-
from
|
|
10
|
+
from flpit.core.linq import FlpList, FlpIt
|
|
12
11
|
|
|
13
12
|
TItem = TypeVar("TItem")
|
|
14
13
|
|
|
@@ -30,10 +29,6 @@ def repeat(element: TItem, count: int) -> FlpIt[TItem]:
|
|
|
30
29
|
return FlpIt(element for _ in builtins.range(count))
|
|
31
30
|
|
|
32
31
|
__all__ = [
|
|
33
|
-
"FlpIt",
|
|
34
|
-
"OrderedIt",
|
|
35
|
-
"Grouping",
|
|
36
|
-
"FlpList",
|
|
37
32
|
"it",
|
|
38
33
|
"lst",
|
|
39
34
|
"range",
|
flpit-0.1.2/PKG-INFO
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: flpit
|
|
3
|
-
Version: 0.1.2
|
|
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.2/README.md
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
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.
|
|
File without changes
|
|
File without changes
|