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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: flpit
3
- Version: 0.1.1.dev3
3
+ Version: 0.1.2
4
4
  Summary: Fluent LINQ for Python — flip your iterables
5
5
  License: MIT
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flpit"
3
- version = "0.1.1.dev3"
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"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flpit"
3
- version = "0.1.1.dev3"
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
- try:
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
- try:
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
- try:
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
- try:
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 ValueError("Sequence contains no matching elements")
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 ValueError:
486
+ except (EmptySequenceError, NoMatchError):
401
487
  return default
402
488
 
403
- def single(self, predicate: Optional[Callable[[TItem], bool]] = None) -> TItem:
489
+ def single(self, predicate: Optional[Callable[[TItem], bool]] = _SENTINEL) -> TItem:
404
490
  """Returns the single, unique element matching a predicate."""
405
- query = self.where(predicate) if predicate else self
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 ValueError("Sequence contains no matching elements")
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 ValueError("Sequence contains more than one matching element")
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
- def to_list(self) -> "FlpList[TItem]":
622
- """Explicitly returns a new shallow copy instance to isolate mutations matching .NET."""
623
- return FlpList(self.data.copy())
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]] = None) -> TItem:
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