flpit 0.2.2__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flpit
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Fluent LINQ for Python — flip your iterables
5
5
  Project-URL: Homepage, https://github.com/riemar/flpit
6
6
  Project-URL: Repository, https://github.com/riemar/flpit
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flpit"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Fluent LINQ for Python — flip your iterables"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -55,7 +55,7 @@ include = ["src/flp", "src/flpit"]
55
55
  [tool.pyrefly]
56
56
  project-includes = ["src"]
57
57
  project-excludes = ["**/__pycache__", "tests"]
58
- search-path = ["src"]
58
+ search-path = ["src", "typing"]
59
59
  preset = "strict"
60
60
 
61
61
  [tool.pytest.ini_options]
@@ -14,7 +14,6 @@ from itertools import islice
14
14
  from typing import (
15
15
  Any,
16
16
  Callable,
17
- Generic,
18
17
  Iterable,
19
18
  Iterator,
20
19
  Collection,
@@ -27,14 +26,14 @@ from typing import (
27
26
  Union,
28
27
  override,
29
28
  overload,
30
- cast as typing_cast, BinaryIO,
29
+ cast as typing_cast,
30
+ BinaryIO,
31
+ Concatenate,
32
+ Sized
31
33
  )
32
34
 
33
- import flpit.json
34
35
  from flpit.json import JsonConvert, JsonSerializerOptions
35
36
 
36
- TItem = TypeVar("TItem")
37
- TResult = TypeVar("TResult")
38
37
  TKey = TypeVar("TKey")
39
38
  TOther = TypeVar("TOther")
40
39
  TAccumulate = TypeVar("TAccumulate")
@@ -114,16 +113,24 @@ class ArgumentOutOfBoundsError(ArgumentError):
114
113
  super().__init__("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection.")
115
114
 
116
115
 
117
- def _guard_empty(func: Callable[..., Any]) -> Callable[..., Any]:
118
- """Catches Python's native empty-sequence ValueError and re-raises LINQ-compliant error."""
116
+ def _guard_empty[SelfT: Sized, R, **P](
117
+ func: Callable[Concatenate[SelfT, P], R],
118
+ ) -> Callable[Concatenate[SelfT, P], R]:
119
+ """Translate empty-sequence ValueError into EmptySequenceError."""
120
+
119
121
  @wraps(func)
120
- def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
122
+ def wrapper(
123
+ self: SelfT,
124
+ *args: P.args,
125
+ **kwargs: P.kwargs,
126
+ ) -> R:
121
127
  try:
122
128
  return func(self, *args, **kwargs)
123
129
  except ValueError as e:
124
- if not self._FlpList__list.data:
130
+ if len(self) == 0:
125
131
  raise EmptySequenceError() from e
126
132
  raise
133
+
127
134
  return wrapper
128
135
 
129
136
 
@@ -137,7 +144,7 @@ def dotnet_context() -> AbstractContextManager[decimal.Context]:
137
144
  return decimal.localcontext(ctx)
138
145
 
139
146
 
140
- class _FactoryIterable(Iterable[TItem], Generic[TItem]):
147
+ class _FactoryIterable[TItem](Iterable[TItem]):
141
148
  """
142
149
  Wraps a generator factory function into a clean Iterable[T].
143
150
  Ensures __iter__ produces a brand-new iterator instance on every call
@@ -152,7 +159,7 @@ class _FactoryIterable(Iterable[TItem], Generic[TItem]):
152
159
  return self._factory()
153
160
 
154
161
 
155
- class FlpIt(Iterable[TItem], Generic[TItem]):
162
+ class FlpIt[TItem](Iterable[TItem]):
156
163
  """
157
164
  | Fluent Iterable
158
165
  Lazy evaluation wrapper around an iterable, inspired by .NET LINQ's
@@ -173,7 +180,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
173
180
  return iter(self._iterable)
174
181
 
175
182
  # noinspection unused-parameter
176
- def as_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
183
+ def as_type[TResult](self, target_type: Type[TResult]) -> FlpIt[TResult]:
177
184
  """Zero-cost static type hint update returning self directly without checks or allocations."""
178
185
  return self # type: ignore[return-value]
179
186
 
@@ -257,7 +264,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
257
264
  # return typing_cast(FlpIt[TItem], FlpIt[TItem](_FactoryIterable(_generator)))
258
265
  return FlpIt[TItem](_FactoryIterable(_generator))
259
266
 
260
- def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
267
+ def select[TResult](self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
261
268
  """Projects each element into a new form."""
262
269
  def _generator() -> Iterator[TResult]:
263
270
  for item in self:
@@ -265,7 +272,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
265
272
 
266
273
  return FlpIt(_FactoryIterable(_generator))
267
274
 
268
- def select_many(
275
+ def select_many[TResult](
269
276
  self, selector: Callable[[TItem], Iterable[TResult]]
270
277
  ) -> FlpIt[TResult]:
271
278
  """Flattens sequence projections."""
@@ -310,7 +317,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
310
317
 
311
318
  return FlpIt(_FactoryIterable(_generator))
312
319
 
313
- def cast(self, target_type: Type[TResult]) -> FlpIt[TResult]:
320
+ def cast[TResult](self, target_type: Type[TResult]) -> FlpIt[TResult]:
314
321
  """Casts elements to a specified type or raises TypeError if cast fails."""
315
322
  def _generator() -> Iterator[TResult]:
316
323
  for item in self:
@@ -322,7 +329,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
322
329
 
323
330
  return FlpIt(_FactoryIterable(_generator))
324
331
 
325
- def of_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
332
+ def of_type[TResult](self, target_type: Type[TResult]) -> FlpIt[TResult]:
326
333
  """Filters the elements of an Iterable based on a specified type."""
327
334
  def _generator() -> Iterator[TResult]:
328
335
  for item in self:
@@ -360,7 +367,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
360
367
  def zip(self, second: Iterable[TOther]) -> FlpIt[tuple[TItem, TOther]]: ...
361
368
 
362
369
  @overload
363
- def zip(
370
+ def zip[TResult, TOther](
364
371
  self,
365
372
  second: Iterable[TOther],
366
373
  result_selector: Callable[[TItem, TOther], TResult],
@@ -598,7 +605,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
598
605
  return item
599
606
  raise EmptySequenceError() if predicate is _SENTINEL else NoMatchError()
600
607
 
601
- def first_or_default(
608
+ def first_or_default[TResult](
602
609
  self, default: TResult, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL
603
610
  ) -> Union[TItem, TResult]:
604
611
  """Returns the first element matching a predicate, or a default value."""
@@ -673,14 +680,7 @@ class _NoneOrderKey:
673
680
 
674
681
  _NONE_ORDER_KEY = _NoneOrderKey()
675
682
 
676
- def _none_aware_key(
677
- selector: Callable[[TItem], Any],
678
- item: TItem,
679
- ) -> Any:
680
- key = selector(item)
681
- return _NONE_ORDER_KEY if key is None else key
682
-
683
- class OrderedIt(FlpIt[TItem]):
683
+ class OrderedIt[TItem](FlpIt[TItem]):
684
684
  __slots__ = ("_criteria",)
685
685
 
686
686
  def __init__(
@@ -701,13 +701,21 @@ class OrderedIt(FlpIt[TItem]):
701
701
  else:
702
702
  self._criteria = _criteria + ((selector, descending),)
703
703
 
704
+ @staticmethod
705
+ def __none_aware_key(
706
+ selector: Callable[[TItem], Any],
707
+ item: TItem,
708
+ ) -> Any:
709
+ key = selector(item)
710
+ return _NONE_ORDER_KEY if key is None else key
711
+
704
712
  @override
705
713
  def __iter__(self) -> Iterator[TItem]:
706
714
  items = list(self._iterable)
707
715
 
708
716
  for selector, descending in reversed(self._criteria):
709
717
  items.sort(
710
- key=partial(_none_aware_key, selector),
718
+ key=partial(self.__none_aware_key, selector),
711
719
  reverse=descending,
712
720
  )
713
721
 
@@ -736,7 +744,7 @@ class OrderedIt(FlpIt[TItem]):
736
744
  )
737
745
 
738
746
 
739
- class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
747
+ class Grouping[TKey, TItem](FlpIt[TItem]):
740
748
  """Represents a collection of elements sharing a common key (.NET IGrouping<TKey, TElement>)."""
741
749
  __slots__ = ("_key", )
742
750
 
@@ -760,7 +768,7 @@ class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
760
768
  return self.key == other.key and self.to_list() == other.to_list()
761
769
 
762
770
 
763
- class FlpList(Collection[TItem], Generic[TItem]):
771
+ class FlpList[TItem](Collection[TItem]):
764
772
  """
765
773
  | Fluent List
766
774
  A materialized list extending UserList that yields lazy FlpIt instances for query operations.
@@ -781,7 +789,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
781
789
  # =====================================================================
782
790
  # 1. THE ABC CONTRACT
783
791
  # =====================================================================
784
- def __iter__(self) -> Iterator[Any]:
792
+ def __iter__(self) -> Iterator[TItem]:
785
793
  return self.__list.__iter__()
786
794
 
787
795
  def __len__(self) -> int:
@@ -1032,17 +1040,16 @@ class FlpList(Collection[TItem], Generic[TItem]):
1032
1040
  def concat(self, second: Iterable[TItem]) -> FlpIt[TItem]:
1033
1041
  return FlpIt(self.__list.data).concat(second)
1034
1042
 
1035
- # noinspection unused-parameter
1036
- def as_type(self, target_type: Type[TResult]) -> FlpList[TResult]:
1037
- return self # type: ignore[return-value]
1043
+ def as_type[TResult](self, target_type: Type[TResult]) -> FlpList[TResult]:
1044
+ return typing_cast(FlpList[TResult], self)
1038
1045
 
1039
1046
  def where(self, predicate: Callable[[TItem], bool]) -> FlpIt[TItem]:
1040
1047
  return FlpIt[TItem](self.__list.data).where(predicate)
1041
1048
 
1042
- def select(self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
1049
+ def select[TResult](self, selector: Callable[[TItem], TResult]) -> FlpIt[TResult]:
1043
1050
  return FlpIt(self.__list.data).select(selector)
1044
1051
 
1045
- def select_many(
1052
+ def select_many[TResult](
1046
1053
  self, selector: Callable[[TItem], Iterable[TResult]]
1047
1054
  ) -> FlpIt[TResult]:
1048
1055
  return FlpIt(self.__list.data).select_many(selector)
@@ -1053,10 +1060,10 @@ class FlpList(Collection[TItem], Generic[TItem]):
1053
1060
  def take(self, count: int) -> FlpIt[TItem]:
1054
1061
  return FlpIt(self.__list.data).take(count)
1055
1062
 
1056
- def cast(self, target_type: Type[TResult]) -> FlpIt[TResult]:
1063
+ def cast[TResult](self, target_type: Type[TResult]) -> FlpIt[TResult]:
1057
1064
  return FlpIt(self.__list.data).cast(target_type)
1058
1065
 
1059
- def of_type(self, target_type: Type[TResult]) -> FlpIt[TResult]:
1066
+ def of_type[TResult](self, target_type: Type[TResult]) -> FlpIt[TResult]:
1060
1067
  return FlpIt(self.__list.data).of_type(target_type)
1061
1068
 
1062
1069
  def distinct(self) -> FlpIt[TItem]:
@@ -1071,7 +1078,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
1071
1078
  def zip(self, second: Iterable[TOther]) -> FlpIt[tuple[TItem, TOther]]: ...
1072
1079
 
1073
1080
  @overload
1074
- def zip(
1081
+ def zip[TResult, TOther](
1075
1082
  self,
1076
1083
  second: Iterable[TOther],
1077
1084
  result_selector: Callable[[TItem, TOther], TResult],
@@ -1204,7 +1211,7 @@ class FlpList(Collection[TItem], Generic[TItem]):
1204
1211
 
1205
1212
  raise EmptySequenceError() if predicate is _SENTINEL else NoMatchError()
1206
1213
 
1207
- def first_or_default(
1214
+ def first_or_default[TResult](
1208
1215
  self, default: TResult, predicate: Callable[[TItem], bool] | _Sentinel = _SENTINEL
1209
1216
  ) -> TItem | TResult:
1210
1217
  return FlpIt(self.__list.data).first_or_default(default, predicate)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes