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.
@@ -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.2"
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 = "flp"
36
- module-root = "src"
48
+ module-name = "flpit"
49
+ namespace = true
50
+ source-include = [
51
+ "src/flp/**/*",
52
+ "src/flpit/**/*",
53
+ ]
37
54
 
38
- [tool.pyright]
39
- include = ["src"]
40
- exclude = [
55
+ [tool.pyrefly]
56
+ project-includes = ["src"]
57
+ project-excludes = [
41
58
  "**/__pycache__",
42
59
  "tests",
43
60
  ]
44
- typeCheckingMode = "basic"
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.2"
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 = "flp"
34
- module-root = "src"
37
+ module-name = "flpit"
38
+ namespace = true
39
+ source-include = ["src/flp/**/*", "src/flpit/**/*"]
35
40
 
36
- [tool.pyright]
37
- include = ["src"]
38
- exclude = ["**/__pycache__", "tests"]
39
- typeCheckingMode = "basic"
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, message="Sequence contains no elements"):
38
- super().__init__(message)
37
+ def __init__(self):
38
+ super().__init__("Sequence contains no elements")
39
39
 
40
40
  class NoMatchError(ValueError):
41
- def __init__(self, message="Sequence contains no matching elements"):
42
- super().__init__(message)
41
+ def __init__(self):
42
+ super().__init__("Sequence contains no matching elements")
43
43
 
44
44
  class MultipleMatchesError(ValueError):
45
- def __init__(self, message="Sequence contains more than one matching element"):
46
- super().__init__(message)
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, message="Sequence contains more than one element"):
50
- super().__init__(message)
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, iterable: Iterable[TItem]) -> None:
94
- self._iterable: Iterable[TItem] = iterable
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]] = None,
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
- last_item: TItem | object = _SENTINEL
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 None or predicate(item):
403
+ if predicate is _SENTINEL or predicate(item):
391
404
  last_item = item
392
405
 
393
- if last_item is _SENTINEL:
394
- raise EmptySequenceError() if predicate is None else NoMatchError()
406
+ if last_item is _MISSING:
407
+ raise EmptySequenceError() if predicate is _SENTINEL else NoMatchError()
395
408
 
396
- return cast(TItem, last_item)
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 cast(TItem, result)
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
- from threading import Lock
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
- Ordered Iterable.
570
+ Lazy, memoized ordered iterable.
521
571
 
522
- Sorts the source lazily on first iteration and caches the resulting order.
523
- The source is consumed at most once; subsequent iterations reuse the cached
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
- "_source",
529
- "_key_selector",
530
- "_descending",
531
- "_parent",
578
+ "_state",
579
+ "_criteria",
532
580
  "_cached_result",
533
- "_lock",
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
- super().__init__(source)
590
+ # Pass () to FlpIt to prevent storing a permanent reference to the raw source
591
+ super().__init__(())
544
592
 
545
- self._source = source
546
- self._key_selector = key_selector
547
- self._descending = descending
548
- self._parent = parent
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._lock = Lock()
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
- ) -> "OrderedIt[TItem]":
557
- return OrderedIt(
558
- self._source,
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
- ) -> "OrderedIt[TItem]":
568
- return OrderedIt(
569
- self._source,
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._lock:
655
+ with self._result_lock:
580
656
  cached = self._cached_result
581
657
 
582
658
  if cached is None:
583
- # Collect the complete ordering chain.
584
- comparers: list[
585
- tuple[Callable[[TItem], Any], bool]
586
- ] = []
587
-
588
- node: Optional["OrderedIt[TItem]"] = self
589
-
590
- while node is not None:
591
- comparers.append(
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
- """Counts elements in the list matching an optional predicate or exact value."""
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]] = None,
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 flp.core.linq import Grouping, FlpList, FlpIt, OrderedIt
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.