flpit 0.1.1.dev3__tar.gz → 0.1.3__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.3}/PKG-INFO +1 -1
- {flpit-0.1.1.dev3 → flpit-0.1.3}/pyproject.toml +6 -1
- {flpit-0.1.1.dev3 → flpit-0.1.3}/pyproject.toml.orig +11 -1
- {flpit-0.1.1.dev3 → flpit-0.1.3}/src/flp/core/linq.py +271 -114
- {flpit-0.1.1.dev3 → flpit-0.1.3}/README.md +0 -0
- {flpit-0.1.1.dev3 → flpit-0.1.3}/src/flp/__init__.py +0 -0
- {flpit-0.1.1.dev3 → flpit-0.1.3}/src/flp/core/__init__.py +0 -0
- {flpit-0.1.1.dev3 → flpit-0.1.3}/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.3"
|
|
4
4
|
description = "Fluent LINQ for Python — flip your iterables"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -25,6 +25,7 @@ dev = [
|
|
|
25
25
|
"hypothesis>=6.168.0",
|
|
26
26
|
"pyrefly>=1.3.1",
|
|
27
27
|
"pyright>=1.1.414",
|
|
28
|
+
"pytest-benchmark>=5.3.0",
|
|
28
29
|
]
|
|
29
30
|
|
|
30
31
|
[build-system]
|
|
@@ -42,3 +43,7 @@ exclude = [
|
|
|
42
43
|
"tests",
|
|
43
44
|
]
|
|
44
45
|
typeCheckingMode = "basic"
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
addopts = "-m 'not benchmark'"
|
|
49
|
+
markers = ["benchmark: performance benchmarks"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flpit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
description = "Fluent LINQ for Python — flip your iterables"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -23,6 +23,7 @@ dev = [
|
|
|
23
23
|
"hypothesis>=6.168.0",
|
|
24
24
|
"pyrefly>=1.3.1",
|
|
25
25
|
"pyright>=1.1.414",
|
|
26
|
+
"pytest-benchmark>=5.3.0",
|
|
26
27
|
]
|
|
27
28
|
|
|
28
29
|
[build-system]
|
|
@@ -37,3 +38,12 @@ module-root = "src"
|
|
|
37
38
|
include = ["src"]
|
|
38
39
|
exclude = ["**/__pycache__", "tests"]
|
|
39
40
|
typeCheckingMode = "basic"
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
addopts = "-m 'not benchmark'"
|
|
44
|
+
markers = [
|
|
45
|
+
"benchmark: performance benchmarks",
|
|
46
|
+
]
|
|
47
|
+
#[tool.pyrefly]
|
|
48
|
+
#extra-paths = ["src", "tests"]
|
|
49
|
+
#python-interpreter-path = ".venv/bin/python" # or .venv/Scripts/python.exe on Windows
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
from threading import Lock
|
|
3
|
+
|
|
4
|
+
from typing import cast
|
|
2
5
|
|
|
3
6
|
import builtins
|
|
4
7
|
from collections import UserList
|
|
@@ -28,12 +31,25 @@ TOther = TypeVar("TOther")
|
|
|
28
31
|
TAccumulate = TypeVar("TAccumulate")
|
|
29
32
|
|
|
30
33
|
_SENTINEL = object()
|
|
34
|
+
_MISSING = object()
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
class EmptySequenceError(ValueError):
|
|
34
38
|
def __init__(self, message="Sequence contains no elements"):
|
|
35
39
|
super().__init__(message)
|
|
36
40
|
|
|
41
|
+
class NoMatchError(ValueError):
|
|
42
|
+
def __init__(self, message="Sequence contains no matching elements"):
|
|
43
|
+
super().__init__(message)
|
|
44
|
+
|
|
45
|
+
class MultipleMatchesError(ValueError):
|
|
46
|
+
def __init__(self, message="Sequence contains more than one matching element"):
|
|
47
|
+
super().__init__(message)
|
|
48
|
+
|
|
49
|
+
class MultipleElementsError(ValueError):
|
|
50
|
+
def __init__(self, message="Sequence contains more than one element"):
|
|
51
|
+
super().__init__(message)
|
|
52
|
+
|
|
37
53
|
|
|
38
54
|
def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
39
55
|
"""Catches Python's native empty-sequence ValueError and re-raises LINQ-compliant error."""
|
|
@@ -96,6 +112,49 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
96
112
|
|
|
97
113
|
return FlpIt(_FactoryIterable(_generator))
|
|
98
114
|
|
|
115
|
+
@overload
|
|
116
|
+
def any(self) -> bool: ...
|
|
117
|
+
|
|
118
|
+
@overload
|
|
119
|
+
def any(self, predicate: Callable[[TItem], bool]) -> bool: ...
|
|
120
|
+
|
|
121
|
+
def any(
|
|
122
|
+
self,
|
|
123
|
+
predicate: Optional[Callable[[TItem], bool]] = None,
|
|
124
|
+
) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Determines whether the sequence contains any elements,
|
|
127
|
+
or whether any element satisfies the predicate.
|
|
128
|
+
|
|
129
|
+
Mirrors Enumerable.Any:
|
|
130
|
+
Any()
|
|
131
|
+
Any(predicate)
|
|
132
|
+
|
|
133
|
+
Evaluation stops as soon as the result is known.
|
|
134
|
+
"""
|
|
135
|
+
if predicate is not None:
|
|
136
|
+
return builtins.any(map(predicate, self))
|
|
137
|
+
|
|
138
|
+
iterator = iter(self)
|
|
139
|
+
try:
|
|
140
|
+
next(iterator)
|
|
141
|
+
except StopIteration:
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def all(self, predicate: Callable[[TItem], bool]) -> bool:
|
|
148
|
+
"""
|
|
149
|
+
Determines whether all elements satisfy the predicate.
|
|
150
|
+
|
|
151
|
+
Mirrors Enumerable.All.
|
|
152
|
+
|
|
153
|
+
Returns True for an empty sequence.
|
|
154
|
+
Evaluation stops at the first element that fails the predicate.
|
|
155
|
+
"""
|
|
156
|
+
return builtins.all(map(predicate, self))
|
|
157
|
+
|
|
99
158
|
def concat(self, second: Iterable[TItem]) -> "FlpIt[TItem]":
|
|
100
159
|
def _generator() -> Iterator[TItem]:
|
|
101
160
|
yield from self
|
|
@@ -305,33 +364,61 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
305
364
|
accumulator = func(accumulator, item)
|
|
306
365
|
return accumulator
|
|
307
366
|
|
|
367
|
+
@overload
|
|
368
|
+
def last(self) -> TItem: ...
|
|
369
|
+
|
|
370
|
+
@overload
|
|
371
|
+
def last(self, predicate: Callable[[TItem], bool]) -> TItem: ...
|
|
372
|
+
|
|
373
|
+
def last(
|
|
374
|
+
self,
|
|
375
|
+
predicate: Optional[Callable[[TItem], bool]] = None,
|
|
376
|
+
) -> TItem:
|
|
377
|
+
"""
|
|
378
|
+
Returns the last element of the sequence, or the last element
|
|
379
|
+
satisfying the predicate.
|
|
380
|
+
|
|
381
|
+
Mirrors Enumerable.Last:
|
|
382
|
+
Last()
|
|
383
|
+
Last(predicate)
|
|
384
|
+
|
|
385
|
+
Raises EmptySequenceError if the sequence is empty or if no
|
|
386
|
+
element satisfies the predicate.
|
|
387
|
+
"""
|
|
388
|
+
last_item: TItem | object = _SENTINEL
|
|
389
|
+
|
|
390
|
+
for item in self:
|
|
391
|
+
if predicate is None or predicate(item):
|
|
392
|
+
last_item = item
|
|
393
|
+
|
|
394
|
+
if last_item is _SENTINEL:
|
|
395
|
+
raise EmptySequenceError() if predicate is None else NoMatchError()
|
|
396
|
+
|
|
397
|
+
return cast(TItem, last_item)
|
|
398
|
+
|
|
399
|
+
def _extreme(self, func, **kwargs) -> TItem:
|
|
400
|
+
result = func(self, default=_MISSING, **kwargs)
|
|
401
|
+
|
|
402
|
+
if result is _MISSING:
|
|
403
|
+
raise EmptySequenceError()
|
|
404
|
+
|
|
405
|
+
return cast(TItem, result)
|
|
406
|
+
|
|
308
407
|
def min(self) -> TItem:
|
|
309
408
|
"""Returns the minimum value in a sequence."""
|
|
310
|
-
|
|
311
|
-
return builtins.min(self) # type: ignore[type-var]
|
|
312
|
-
except ValueError:
|
|
313
|
-
raise EmptySequenceError()
|
|
409
|
+
return self._extreme(builtins.min)
|
|
314
410
|
|
|
315
411
|
def min_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
|
|
316
412
|
"""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()
|
|
413
|
+
return self._extreme(builtins.min, key=key_selector)
|
|
321
414
|
|
|
322
415
|
def max(self) -> TItem:
|
|
323
416
|
"""Returns the maximum value in a sequence."""
|
|
324
|
-
|
|
325
|
-
return builtins.max(self) # type: ignore[type-var]
|
|
326
|
-
except ValueError:
|
|
327
|
-
raise EmptySequenceError()
|
|
417
|
+
return self._extreme(builtins.max)
|
|
328
418
|
|
|
329
419
|
def max_by(self, key_selector: Callable[[TItem], Any]) -> TItem:
|
|
330
420
|
"""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()
|
|
421
|
+
return self._extreme(builtins.max, key=key_selector)
|
|
335
422
|
|
|
336
423
|
def average(
|
|
337
424
|
self, selector: Optional[Callable[[TItem], Union[int, float]]] = None
|
|
@@ -372,7 +459,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
372
459
|
|
|
373
460
|
def count(self, predicate: Optional[Callable[[TItem], bool]] = None) -> int:
|
|
374
461
|
"""Counts elements in the sequence matching an optional predicate."""
|
|
375
|
-
query = self.where(predicate) if predicate else self
|
|
462
|
+
query = self.where(predicate) if predicate is not None else self
|
|
376
463
|
return builtins.sum(1 for _ in query)
|
|
377
464
|
|
|
378
465
|
def element_at(self, index: int) -> TItem:
|
|
@@ -386,10 +473,10 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
386
473
|
|
|
387
474
|
def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
|
|
388
475
|
"""Returns the first element matching a predicate, or raises ValueError."""
|
|
389
|
-
query = self.where(predicate) if predicate else self
|
|
476
|
+
query = self.where(predicate) if predicate is not None else self
|
|
390
477
|
for item in query:
|
|
391
478
|
return item
|
|
392
|
-
raise
|
|
479
|
+
raise EmptySequenceError() if predicate is None else NoMatchError()
|
|
393
480
|
|
|
394
481
|
def first_or_default(
|
|
395
482
|
self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
|
|
@@ -397,48 +484,91 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
|
|
|
397
484
|
"""Returns the first element matching a predicate, or a default value."""
|
|
398
485
|
try:
|
|
399
486
|
return self.first(predicate)
|
|
400
|
-
except
|
|
487
|
+
except (EmptySequenceError, NoMatchError):
|
|
401
488
|
return default
|
|
402
489
|
|
|
403
|
-
def single(self, predicate: Optional[Callable[[TItem], bool]] =
|
|
490
|
+
def single(self, predicate: Optional[Callable[[TItem], bool]] = _SENTINEL) -> TItem:
|
|
404
491
|
"""Returns the single, unique element matching a predicate."""
|
|
405
|
-
|
|
492
|
+
if predicate is None:
|
|
493
|
+
raise TypeError("Predicate cannot be None")
|
|
494
|
+
|
|
495
|
+
has_predicate = predicate is not _SENTINEL
|
|
496
|
+
query = self.where(predicate) if has_predicate else self
|
|
406
497
|
it = iter(query)
|
|
498
|
+
|
|
407
499
|
try:
|
|
408
500
|
first_val = next(it)
|
|
409
501
|
except StopIteration:
|
|
410
|
-
raise
|
|
502
|
+
raise NoMatchError() if has_predicate else EmptySequenceError()
|
|
411
503
|
|
|
412
504
|
try:
|
|
413
505
|
next(it)
|
|
414
506
|
except StopIteration:
|
|
415
507
|
return first_val
|
|
416
508
|
|
|
417
|
-
raise
|
|
509
|
+
raise MultipleMatchesError() if has_predicate else MultipleElementsError()
|
|
510
|
+
|
|
418
511
|
|
|
419
512
|
def to_list(self) -> FlpList[TItem]:
|
|
420
513
|
"""Explicitly materializes the query into a FlpList."""
|
|
421
514
|
return FlpList(self)
|
|
422
515
|
|
|
423
516
|
|
|
424
|
-
|
|
517
|
+
_OrderCriterion = tuple[Callable[[TItem], Any], bool]
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class _OrderState(Generic[TItem]):
|
|
521
|
+
"""
|
|
522
|
+
Shared lazy snapshot for an OrderBy/ThenBy chain.
|
|
523
|
+
|
|
524
|
+
The upstream source is consumed at most once. After materialization,
|
|
525
|
+
the original source reference is released.
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
__slots__ = ("_source", "_items", "_lock")
|
|
529
|
+
|
|
530
|
+
def __init__(self, source: Iterable[TItem]) -> None:
|
|
531
|
+
self._source: Optional[Iterable[TItem]] = source
|
|
532
|
+
self._items: Optional[list[TItem]] = None
|
|
533
|
+
self._lock = Lock()
|
|
534
|
+
|
|
535
|
+
def materialize(self) -> list[TItem]:
|
|
536
|
+
items = self._items
|
|
537
|
+
|
|
538
|
+
if items is not None:
|
|
539
|
+
return items
|
|
540
|
+
|
|
541
|
+
with self._lock:
|
|
542
|
+
items = self._items
|
|
543
|
+
|
|
544
|
+
if items is None:
|
|
545
|
+
source = self._source
|
|
546
|
+
|
|
547
|
+
if source is None:
|
|
548
|
+
raise RuntimeError("OrderState source is unavailable")
|
|
549
|
+
|
|
550
|
+
items = list(source)
|
|
551
|
+
|
|
552
|
+
self._source = None
|
|
553
|
+
self._items = items
|
|
554
|
+
|
|
555
|
+
return items
|
|
556
|
+
|
|
425
557
|
|
|
426
558
|
class OrderedIt(FlpIt[TItem]):
|
|
427
559
|
"""
|
|
428
|
-
|
|
560
|
+
Lazy, memoized ordered iterable.
|
|
429
561
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
result.
|
|
562
|
+
The source is materialized once per OrderBy chain.
|
|
563
|
+
ThenBy nodes share that source snapshot but maintain independent
|
|
564
|
+
ordered-result caches.
|
|
433
565
|
"""
|
|
434
566
|
|
|
435
567
|
__slots__ = (
|
|
436
|
-
"
|
|
437
|
-
"
|
|
438
|
-
"_descending",
|
|
439
|
-
"_parent",
|
|
568
|
+
"_state",
|
|
569
|
+
"_criteria",
|
|
440
570
|
"_cached_result",
|
|
441
|
-
"
|
|
571
|
+
"_result_lock",
|
|
442
572
|
)
|
|
443
573
|
|
|
444
574
|
def __init__(
|
|
@@ -446,116 +576,94 @@ class OrderedIt(FlpIt[TItem]):
|
|
|
446
576
|
source: Iterable[TItem],
|
|
447
577
|
key_selector: Callable[[TItem], Any],
|
|
448
578
|
descending: bool = False,
|
|
449
|
-
parent: Optional["OrderedIt[TItem]"] = None,
|
|
450
579
|
) -> None:
|
|
451
|
-
|
|
580
|
+
# Pass () to FlpIt to prevent storing a permanent reference to the raw source
|
|
581
|
+
super().__init__(())
|
|
452
582
|
|
|
453
|
-
self.
|
|
454
|
-
self.
|
|
455
|
-
|
|
456
|
-
|
|
583
|
+
self._state = _OrderState(source)
|
|
584
|
+
self._criteria: tuple[_OrderCriterion, ...] = (
|
|
585
|
+
(key_selector, descending),
|
|
586
|
+
)
|
|
457
587
|
|
|
458
588
|
self._cached_result: Optional[list[TItem]] = None
|
|
459
|
-
self.
|
|
589
|
+
self._result_lock = Lock()
|
|
590
|
+
|
|
591
|
+
@classmethod
|
|
592
|
+
def _from_parent(
|
|
593
|
+
cls,
|
|
594
|
+
parent: OrderedIt[TItem],
|
|
595
|
+
key_selector: Callable[[TItem], Any],
|
|
596
|
+
descending: bool,
|
|
597
|
+
) -> OrderedIt[TItem]:
|
|
598
|
+
"""
|
|
599
|
+
Create a child ordering node sharing the parent's source state.
|
|
600
|
+
|
|
601
|
+
The FlpIt source is intentionally empty because OrderedIt overrides
|
|
602
|
+
__iter__ and uses _OrderState as its real source.
|
|
603
|
+
"""
|
|
604
|
+
child = cls.__new__(cls)
|
|
605
|
+
|
|
606
|
+
# Do not retain the parent OrderedIt through FlpIt's _iterable.
|
|
607
|
+
FlpIt.__init__(child, ())
|
|
608
|
+
|
|
609
|
+
child._state = parent._state
|
|
610
|
+
child._criteria = (
|
|
611
|
+
*parent._criteria,
|
|
612
|
+
(key_selector, descending),
|
|
613
|
+
)
|
|
614
|
+
child._cached_result = None
|
|
615
|
+
child._result_lock = Lock()
|
|
616
|
+
|
|
617
|
+
return child
|
|
460
618
|
|
|
461
619
|
def then_by(
|
|
462
620
|
self,
|
|
463
621
|
key_selector: Callable[[TItem], Any],
|
|
464
|
-
) ->
|
|
465
|
-
return
|
|
466
|
-
self
|
|
622
|
+
) -> OrderedIt[TItem]:
|
|
623
|
+
return self._from_parent(
|
|
624
|
+
self,
|
|
467
625
|
key_selector,
|
|
468
626
|
descending=False,
|
|
469
|
-
parent=self,
|
|
470
627
|
)
|
|
471
628
|
|
|
472
629
|
def then_by_descending(
|
|
473
630
|
self,
|
|
474
631
|
key_selector: Callable[[TItem], Any],
|
|
475
|
-
) ->
|
|
476
|
-
return
|
|
477
|
-
self
|
|
632
|
+
) -> OrderedIt[TItem]:
|
|
633
|
+
return self._from_parent(
|
|
634
|
+
self,
|
|
478
635
|
key_selector,
|
|
479
636
|
descending=True,
|
|
480
|
-
parent=self,
|
|
481
637
|
)
|
|
482
638
|
|
|
483
639
|
def __iter__(self) -> Iterator[TItem]:
|
|
484
640
|
cached = self._cached_result
|
|
485
641
|
|
|
486
642
|
if cached is None:
|
|
487
|
-
with self.
|
|
643
|
+
with self._result_lock:
|
|
488
644
|
cached = self._cached_result
|
|
489
645
|
|
|
490
646
|
if cached is None:
|
|
491
|
-
#
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
(node._key_selector, node._descending)
|
|
647
|
+
# Never mutate the shared source snapshot.
|
|
648
|
+
result = list(self._state.materialize())
|
|
649
|
+
|
|
650
|
+
# Python sort is stable, so apply the least-significant
|
|
651
|
+
# criterion first.
|
|
652
|
+
for selector, descending in reversed(self._criteria):
|
|
653
|
+
result.sort(
|
|
654
|
+
key=selector,
|
|
655
|
+
reverse=descending,
|
|
501
656
|
)
|
|
502
|
-
node = node._parent
|
|
503
|
-
|
|
504
|
-
comparers.reverse()
|
|
505
|
-
|
|
506
|
-
# Consume the source exactly once.
|
|
507
|
-
items = list(self._source)
|
|
508
|
-
|
|
509
|
-
class SortWrapper:
|
|
510
|
-
__slots__ = ("obj", "keys")
|
|
511
|
-
|
|
512
|
-
def __init__(self, obj: Any) -> None:
|
|
513
|
-
self.obj = obj
|
|
514
|
-
self.keys = [
|
|
515
|
-
selector(obj)
|
|
516
|
-
for selector, _ in comparers
|
|
517
|
-
]
|
|
518
|
-
|
|
519
|
-
def __lt__(self, other: "SortWrapper") -> bool:
|
|
520
|
-
for index, (_, descending) in enumerate(comparers):
|
|
521
|
-
left = self.keys[index]
|
|
522
|
-
right = other.keys[index]
|
|
523
|
-
|
|
524
|
-
if left == right:
|
|
525
|
-
continue
|
|
526
|
-
|
|
527
|
-
return right < left if descending else left < right
|
|
528
|
-
|
|
529
|
-
return False
|
|
530
|
-
|
|
531
|
-
wrapped_items = [
|
|
532
|
-
SortWrapper(item)
|
|
533
|
-
for item in items
|
|
534
|
-
]
|
|
535
|
-
|
|
536
|
-
wrapped_items.sort()
|
|
537
|
-
|
|
538
|
-
# Store ONLY the actual result objects.
|
|
539
|
-
cached = [
|
|
540
|
-
wrapper.obj
|
|
541
|
-
for wrapper in wrapped_items
|
|
542
|
-
]
|
|
543
657
|
|
|
658
|
+
cached = result
|
|
544
659
|
self._cached_result = cached
|
|
545
660
|
|
|
546
|
-
# Important: __iter__ is a generator function.
|
|
547
|
-
# Release potentially large temporary structures
|
|
548
|
-
# before yielding anything.
|
|
549
|
-
del wrapped_items
|
|
550
|
-
del items
|
|
551
|
-
del comparers
|
|
552
|
-
|
|
553
661
|
yield from cached
|
|
554
662
|
|
|
555
663
|
|
|
556
664
|
class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
|
|
557
665
|
"""Represents a collection of elements sharing a common key (.NET IGrouping<TKey, TElement>)."""
|
|
558
|
-
__slots__ = ("_key")
|
|
666
|
+
__slots__ = ("_key", )
|
|
559
667
|
|
|
560
668
|
def __init__(self, key: TKey, elements: Iterable[TItem]) -> None:
|
|
561
669
|
self._key: TKey = key
|
|
@@ -618,9 +726,26 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
618
726
|
self.data.append(first_item)
|
|
619
727
|
self.data.extend(it)
|
|
620
728
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
729
|
+
@overload
|
|
730
|
+
def any(self) -> bool: ...
|
|
731
|
+
|
|
732
|
+
@overload
|
|
733
|
+
def any(self, predicate: Callable[[TItem], bool]) -> bool: ...
|
|
734
|
+
|
|
735
|
+
def any(
|
|
736
|
+
self,
|
|
737
|
+
predicate: Optional[Callable[[TItem], bool]] = None,
|
|
738
|
+
) -> bool:
|
|
739
|
+
if predicate is None:
|
|
740
|
+
return len(self.data) > 0
|
|
741
|
+
|
|
742
|
+
return any(predicate(item) for item in self.data)
|
|
743
|
+
|
|
744
|
+
def all(
|
|
745
|
+
self,
|
|
746
|
+
predicate: Callable[[TItem], bool],
|
|
747
|
+
) -> bool:
|
|
748
|
+
return all(predicate(item) for item in self.data)
|
|
624
749
|
|
|
625
750
|
def append_linq(self, element: TItem) -> FlpIt[TItem]:
|
|
626
751
|
"""Appends an element to the sequence lazily, returning a FlpIt without mutating this list."""
|
|
@@ -777,14 +902,46 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
|
|
|
777
902
|
def first(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
|
|
778
903
|
return FlpIt(self.data).first(predicate)
|
|
779
904
|
|
|
905
|
+
@overload
|
|
906
|
+
def last(self) -> TItem: ...
|
|
907
|
+
|
|
908
|
+
@overload
|
|
909
|
+
def last(self, predicate: Callable[[TItem], bool]) -> TItem: ...
|
|
910
|
+
|
|
911
|
+
def last(
|
|
912
|
+
self,
|
|
913
|
+
predicate: Optional[Callable[[TItem], bool]] = None,
|
|
914
|
+
) -> TItem:
|
|
915
|
+
"""
|
|
916
|
+
Returns the last element of the sequence, or the last element
|
|
917
|
+
satisfying the predicate.
|
|
918
|
+
|
|
919
|
+
Mirrors Enumerable.Last:
|
|
920
|
+
Last()
|
|
921
|
+
Last(predicate)
|
|
922
|
+
|
|
923
|
+
Raises EmptySequenceError if the sequence is empty or if no
|
|
924
|
+
element satisfies the predicate.
|
|
925
|
+
"""
|
|
926
|
+
if predicate is None:
|
|
927
|
+
if not self.data:
|
|
928
|
+
raise EmptySequenceError()
|
|
929
|
+
return self.data[-1]
|
|
930
|
+
|
|
931
|
+
for item in reversed(self.data):
|
|
932
|
+
if predicate(item):
|
|
933
|
+
return item
|
|
934
|
+
|
|
935
|
+
raise NoMatchError()
|
|
936
|
+
|
|
780
937
|
def first_or_default(
|
|
781
938
|
self, default: TResult, predicate: Optional[Callable[[TItem], bool]] = None
|
|
782
939
|
) -> Union[TItem, TResult]:
|
|
783
940
|
return FlpIt(self.data).first_or_default(default, predicate)
|
|
784
941
|
|
|
785
|
-
def single(self, predicate: Optional[Callable[[TItem], bool]] =
|
|
942
|
+
def single(self, predicate: Optional[Callable[[TItem], bool]] = _SENTINEL) -> TItem:
|
|
786
943
|
return FlpIt(self.data).single(predicate)
|
|
787
944
|
|
|
788
945
|
def to_list(self) -> FlpList[TItem]:
|
|
789
946
|
"""Explicitly returns a shallow copy instance to isolate mutations."""
|
|
790
|
-
return FlpList(self.data.copy())
|
|
947
|
+
return FlpList(self.data.copy())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|