flpit 0.1.1.dev3__tar.gz → 0.1.2__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.dev3 → flpit-0.1.2}/PKG-INFO +1 -1
- {flpit-0.1.1.dev3 → flpit-0.1.2}/pyproject.toml +1 -1
- {flpit-0.1.1.dev3 → flpit-0.1.2}/pyproject.toml.orig +5 -1
- {flpit-0.1.1.dev3 → flpit-0.1.2}/src/flp/core/linq.py +170 -29
- {flpit-0.1.1.dev3 → flpit-0.1.2}/README.md +0 -0
- {flpit-0.1.1.dev3 → flpit-0.1.2}/src/flp/__init__.py +0 -0
- {flpit-0.1.1.dev3 → flpit-0.1.2}/src/flp/core/__init__.py +0 -0
- {flpit-0.1.1.dev3 → flpit-0.1.2}/src/flp/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flpit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.2"
|
|
4
4
|
description = "Fluent LINQ for Python — flip your iterables"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -37,3 +37,7 @@ module-root = "src"
|
|
|
37
37
|
include = ["src"]
|
|
38
38
|
exclude = ["**/__pycache__", "tests"]
|
|
39
39
|
typeCheckingMode = "basic"
|
|
40
|
+
|
|
41
|
+
#[tool.pyrefly]
|
|
42
|
+
#extra-paths = ["src", "tests"]
|
|
43
|
+
#python-interpreter-path = ".venv/bin/python" # or .venv/Scripts/python.exe on Windows
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
3
5
|
import builtins
|
|
4
6
|
from collections import UserList
|
|
5
7
|
from functools import wraps
|
|
@@ -28,12 +30,25 @@ TOther = TypeVar("TOther")
|
|
|
28
30
|
TAccumulate = TypeVar("TAccumulate")
|
|
29
31
|
|
|
30
32
|
_SENTINEL = object()
|
|
33
|
+
_MISSING = object()
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
class EmptySequenceError(ValueError):
|
|
34
37
|
def __init__(self, message="Sequence contains no elements"):
|
|
35
38
|
super().__init__(message)
|
|
36
39
|
|
|
40
|
+
class NoMatchError(ValueError):
|
|
41
|
+
def __init__(self, message="Sequence contains no matching elements"):
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
|
|
44
|
+
class MultipleMatchesError(ValueError):
|
|
45
|
+
def __init__(self, message="Sequence contains more than one matching element"):
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
|
|
48
|
+
class MultipleElementsError(ValueError):
|
|
49
|
+
def __init__(self, message="Sequence contains more than one element"):
|
|
50
|
+
super().__init__(message)
|
|
51
|
+
|
|
37
52
|
|
|
38
53
|
def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
39
54
|
"""Catches Python's native empty-sequence ValueError and re-raises LINQ-compliant error."""
|
|
@@ -96,6 +111,49 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
96
111
|
|
|
97
112
|
return FlpIt(_FactoryIterable(_generator))
|
|
98
113
|
|
|
114
|
+
@overload
|
|
115
|
+
def any(self) -> bool: ...
|
|
116
|
+
|
|
117
|
+
@overload
|
|
118
|
+
def any(self, predicate: Callable[[TItem], bool]) -> bool: ...
|
|
119
|
+
|
|
120
|
+
def any(
|
|
121
|
+
self,
|
|
122
|
+
predicate: Optional[Callable[[TItem], bool]] = None,
|
|
123
|
+
) -> bool:
|
|
124
|
+
"""
|
|
125
|
+
Determines whether the sequence contains any elements,
|
|
126
|
+
or whether any element satisfies the predicate.
|
|
127
|
+
|
|
128
|
+
Mirrors Enumerable.Any:
|
|
129
|
+
Any()
|
|
130
|
+
Any(predicate)
|
|
131
|
+
|
|
132
|
+
Evaluation stops as soon as the result is known.
|
|
133
|
+
"""
|
|
134
|
+
if predicate is not None:
|
|
135
|
+
return builtins.any(map(predicate, self))
|
|
136
|
+
|
|
137
|
+
iterator = iter(self)
|
|
138
|
+
try:
|
|
139
|
+
next(iterator)
|
|
140
|
+
except StopIteration:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def all(self, predicate: Callable[[TItem], bool]) -> bool:
|
|
147
|
+
"""
|
|
148
|
+
Determines whether all elements satisfy the predicate.
|
|
149
|
+
|
|
150
|
+
Mirrors Enumerable.All.
|
|
151
|
+
|
|
152
|
+
Returns True for an empty sequence.
|
|
153
|
+
Evaluation stops at the first element that fails the predicate.
|
|
154
|
+
"""
|
|
155
|
+
return builtins.all(map(predicate, self))
|
|
156
|
+
|
|
99
157
|
def concat(self, second: Iterable[TItem]) -> "FlpIt[TItem]":
|
|
100
158
|
def _generator() -> Iterator[TItem]:
|
|
101
159
|
yield from self
|
|
@@ -305,33 +363,61 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
305
363
|
accumulator = func(accumulator, item)
|
|
306
364
|
return accumulator
|
|
307
365
|
|
|
366
|
+
@overload
|
|
367
|
+
def last(self) -> TItem: ...
|
|
368
|
+
|
|
369
|
+
@overload
|
|
370
|
+
def last(self, predicate: Callable[[TItem], bool]) -> TItem: ...
|
|
371
|
+
|
|
372
|
+
def last(
|
|
373
|
+
self,
|
|
374
|
+
predicate: Optional[Callable[[TItem], bool]] = None,
|
|
375
|
+
) -> TItem:
|
|
376
|
+
"""
|
|
377
|
+
Returns the last element of the sequence, or the last element
|
|
378
|
+
satisfying the predicate.
|
|
379
|
+
|
|
380
|
+
Mirrors Enumerable.Last:
|
|
381
|
+
Last()
|
|
382
|
+
Last(predicate)
|
|
383
|
+
|
|
384
|
+
Raises EmptySequenceError if the sequence is empty or if no
|
|
385
|
+
element satisfies the predicate.
|
|
386
|
+
"""
|
|
387
|
+
last_item: TItem | object = _SENTINEL
|
|
388
|
+
|
|
389
|
+
for item in self:
|
|
390
|
+
if predicate is None or predicate(item):
|
|
391
|
+
last_item = item
|
|
392
|
+
|
|
393
|
+
if last_item is _SENTINEL:
|
|
394
|
+
raise EmptySequenceError() if predicate is None else NoMatchError()
|
|
395
|
+
|
|
396
|
+
return cast(TItem, last_item)
|
|
397
|
+
|
|
398
|
+
def _extreme(self, func, **kwargs) -> TItem:
|
|
399
|
+
result = func(self, default=_MISSING, **kwargs)
|
|
400
|
+
|
|
401
|
+
if result is _MISSING:
|
|
402
|
+
raise EmptySequenceError()
|
|
403
|
+
|
|
404
|
+
return cast(TItem, result)
|
|
405
|
+
|
|
308
406
|
def min(self) -> TItem:
|
|
309
407
|
"""Returns the minimum value in a sequence."""
|
|
310
|
-
|
|
311
|
-
return builtins.min(self) # type: ignore[type-var]
|
|
312
|
-
except ValueError:
|
|
313
|
-
raise EmptySequenceError()
|
|
408
|
+
return self._extreme(builtins.min)
|
|
314
409
|
|
|
315
410
|
def min_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
|
|
316
411
|
"""Returns the value in a sequence that has the minimum key value."""
|
|
317
|
-
|
|
318
|
-
return builtins.min(self, key=key_selector)
|
|
319
|
-
except ValueError:
|
|
320
|
-
raise EmptySequenceError()
|
|
412
|
+
return self._extreme(builtins.min, key=key_selector)
|
|
321
413
|
|
|
322
414
|
def max(self) -> TItem:
|
|
323
415
|
"""Returns the maximum value in a sequence."""
|
|
324
|
-
|
|
325
|
-
return builtins.max(self) # type: ignore[type-var]
|
|
326
|
-
except ValueError:
|
|
327
|
-
raise EmptySequenceError()
|
|
416
|
+
return self._extreme(builtins.max)
|
|
328
417
|
|
|
329
418
|
def max_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
|
|
330
419
|
"""Returns the value in a sequence that has the maximum key value."""
|
|
331
|
-
|
|
332
|
-
return builtins.max(self, key=key_selector)
|
|
333
|
-
except ValueError:
|
|
334
|
-
raise EmptySequenceError()
|
|
420
|
+
return self._extreme(builtins.max, key=key_selector)
|
|
335
421
|
|
|
336
422
|
def average(
|
|
337
423
|
self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
|
|
@@ -372,7 +458,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
372
458
|
|
|
373
459
|
def count(self, predicate: Optional[Callable[[TItem], bool]] = None) -> int:
|
|
374
460
|
"""Counts elements in the sequence matching an optional predicate."""
|
|
375
|
-
query = self.where(predicate) if predicate else self
|
|
461
|
+
query = self.where(predicate) if predicate is not None else self
|
|
376
462
|
return builtins.sum(1 for _ in query)
|
|
377
463
|
|
|
378
464
|
def element_at(self, index: int) -> TItem:
|
|
@@ -386,10 +472,10 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
386
472
|
|
|
387
473
|
def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
|
|
388
474
|
"""Returns the first element matching a predicate, or raises ValueError."""
|
|
389
|
-
query = self.where(predicate) if predicate else self
|
|
475
|
+
query = self.where(predicate) if predicate is not None else self
|
|
390
476
|
for item in query:
|
|
391
477
|
return item
|
|
392
|
-
raise
|
|
478
|
+
raise EmptySequenceError() if predicate is None else NoMatchError()
|
|
393
479
|
|
|
394
480
|
def first_or_default(
|
|
395
481
|
self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
|
|
@@ -397,24 +483,30 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
397
483
|
"""Returns the first element matching a predicate, or a default value."""
|
|
398
484
|
try:
|
|
399
485
|
return self.first(predicate)
|
|
400
|
-
except
|
|
486
|
+
except (EmptySequenceError, NoMatchError):
|
|
401
487
|
return default
|
|
402
488
|
|
|
403
|
-
def single(self, predicate: Optional[Callable[[TItem], bool]] =
|
|
489
|
+
def single(self, predicate: Optional[Callable[[TItem], bool]] = _SENTINEL) -> TItem:
|
|
404
490
|
"""Returns the single, unique element matching a predicate."""
|
|
405
|
-
|
|
491
|
+
if predicate is None:
|
|
492
|
+
raise TypeError("Predicate cannot be None")
|
|
493
|
+
|
|
494
|
+
has_predicate = predicate is not _SENTINEL
|
|
495
|
+
query = self.where(predicate) if has_predicate else self
|
|
406
496
|
it = iter(query)
|
|
497
|
+
|
|
407
498
|
try:
|
|
408
499
|
first_val = next(it)
|
|
409
500
|
except StopIteration:
|
|
410
|
-
raise
|
|
501
|
+
raise NoMatchError() if has_predicate else EmptySequenceError()
|
|
411
502
|
|
|
412
503
|
try:
|
|
413
504
|
next(it)
|
|
414
505
|
except StopIteration:
|
|
415
506
|
return first_val
|
|
416
507
|
|
|
417
|
-
raise
|
|
508
|
+
raise MultipleMatchesError() if has_predicate else MultipleElementsError()
|
|
509
|
+
|
|
418
510
|
|
|
419
511
|
def to_list(self) -> FlpList[TItem]:
|
|
420
512
|
"""Explicitly materializes the query into a FlpList."""
|
|
@@ -618,9 +710,26 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
618
710
|
self.data.append(first_item)
|
|
619
711
|
self.data.extend(it)
|
|
620
712
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
713
|
+
@overload
|
|
714
|
+
def any(self) -> bool: ...
|
|
715
|
+
|
|
716
|
+
@overload
|
|
717
|
+
def any(self, predicate: Callable[[TItem], bool]) -> bool: ...
|
|
718
|
+
|
|
719
|
+
def any(
|
|
720
|
+
self,
|
|
721
|
+
predicate: Optional[Callable[[TItem], bool]] = None,
|
|
722
|
+
) -> bool:
|
|
723
|
+
if predicate is None:
|
|
724
|
+
return len(self.data) > 0
|
|
725
|
+
|
|
726
|
+
return any(predicate(item) for item in self.data)
|
|
727
|
+
|
|
728
|
+
def all(
|
|
729
|
+
self,
|
|
730
|
+
predicate: Callable[[TItem], bool],
|
|
731
|
+
) -> bool:
|
|
732
|
+
return all(predicate(item) for item in self.data)
|
|
624
733
|
|
|
625
734
|
def append_linq(self, element: TItem) -> FlpIt[TItem]:
|
|
626
735
|
"""Appends an element to the sequence lazily, returning a FlpIt without mutating this list."""
|
|
@@ -777,14 +886,46 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
777
886
|
def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
|
|
778
887
|
return FlpIt(self.data).first(predicate)
|
|
779
888
|
|
|
889
|
+
@overload
|
|
890
|
+
def last(self) -> TItem: ...
|
|
891
|
+
|
|
892
|
+
@overload
|
|
893
|
+
def last(self, predicate: Callable[[TItem], bool]) -> TItem: ...
|
|
894
|
+
|
|
895
|
+
def last(
|
|
896
|
+
self,
|
|
897
|
+
predicate: Optional[Callable[[TItem], bool]] = None,
|
|
898
|
+
) -> TItem:
|
|
899
|
+
"""
|
|
900
|
+
Returns the last element of the sequence, or the last element
|
|
901
|
+
satisfying the predicate.
|
|
902
|
+
|
|
903
|
+
Mirrors Enumerable.Last:
|
|
904
|
+
Last()
|
|
905
|
+
Last(predicate)
|
|
906
|
+
|
|
907
|
+
Raises EmptySequenceError if the sequence is empty or if no
|
|
908
|
+
element satisfies the predicate.
|
|
909
|
+
"""
|
|
910
|
+
if predicate is None:
|
|
911
|
+
if not self.data:
|
|
912
|
+
raise EmptySequenceError()
|
|
913
|
+
return self.data[-1]
|
|
914
|
+
|
|
915
|
+
for item in reversed(self.data):
|
|
916
|
+
if predicate(item):
|
|
917
|
+
return item
|
|
918
|
+
|
|
919
|
+
raise NoMatchError()
|
|
920
|
+
|
|
780
921
|
def first_or_default(
|
|
781
922
|
self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
|
|
782
923
|
) -> Union[TItem, TResult]:
|
|
783
924
|
return FlpIt(self.data).first_or_default(default, predicate)
|
|
784
925
|
|
|
785
|
-
def single(self, predicate: Optional[Callable[[TItem], bool]] =
|
|
926
|
+
def single(self, predicate: Optional[Callable[[TItem], bool]] = _SENTINEL) -> TItem:
|
|
786
927
|
return FlpIt(self.data).single(predicate)
|
|
787
928
|
|
|
788
929
|
def to_list(self) -> FlpList[TItem]:
|
|
789
930
|
"""Explicitly returns a shallow copy instance to isolate mutations."""
|
|
790
|
-
return FlpList(self.data.copy())
|
|
931
|
+
return FlpList(self.data.copy())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|