flpit 0.1.1.dev1__tar.gz → 0.1.1.dev3__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.dev1
3
+ Version: 0.1.1.dev3
4
4
  Summary: Fluent LINQ for Python — flip your iterables
5
5
  License: MIT
6
6
  Classifier: Development Status :: 3 - Alpha
@@ -18,28 +18,29 @@ Description-Content-Type: text/markdown
18
18
 
19
19
  > **Fluent LINQ for Python – flip your iterables**
20
20
 
21
- `flp` brings .NET's LINQ fluent API to standard Python iterables with full static typing.
21
+ `flp` brings a LINQ-inspired fluent API to standard Python iterables with full static typing.
22
22
 
23
23
  ## ⚡ Key Features
24
24
 
25
- * **⚡ Lazy Evaluation (`FlpIt`):** Deferred evaluation via pure Python generator expressions.
26
- * **📦 Eager Container (`FlpList`):** Persisted state backed by `collections.UserList`.
25
+ * **⚡ Lazy Evaluation (`FlpIt`):** Deferred execution using Python iterators and generators.
26
+ * **📦 Materialized Container (`FlpList`):** Eager, mutable container backed by `collections.UserList`.
27
27
  * **🔹 Static-First Typing:** Built for full `mypy` and `pyright` inference without plugins.
28
- * **🟢 O(1) Boundary Validation:** Boundary checks rely on O(1) sampling instead of O(N) scans.
28
+ * **🔄 No Implicit Caching:** Query results are not cached unless explicitly documented, such as `order_by`.
29
29
 
30
30
  ## ⚙️ Installation
31
31
 
32
32
  ```bash
33
- uv add flp
33
+ uv add flpit
34
34
  ```
35
35
 
36
36
  ## 💡 Quick Start
37
37
 
38
38
  ```python
39
- from flp import FlpList, FlpIt
39
+ import flp
40
+ from flp import FlpIt, FlpList
40
41
 
41
- # Eager collection
42
- data = FlpList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
42
+ # Deferred iterable via shorthand
43
+ data: FlpIt[int] = flp.it([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
43
44
 
44
45
  # Deferred / lazy pipeline
45
46
  query: FlpIt[int] = (
@@ -48,8 +49,11 @@ query: FlpIt[int] = (
48
49
  .select(lambda x: x * 10)
49
50
  )
50
51
 
51
- # Materialization happens explicitly
52
+ # Materialize query results explicitly
52
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])
53
57
  ```
54
58
 
55
59
  ## 📜 License
@@ -0,0 +1,45 @@
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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flpit"
3
- version = "0.1.1.dev1"
3
+ version = "0.1.1.dev3"
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.dev1"
3
+ version = "0.1.1.dev3"
4
4
  description = "Fluent LINQ for Python — flip your iterables"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -2,6 +2,8 @@
2
2
  Fluent LINQ for Python — flip your iterables.
3
3
  """
4
4
  from __future__ import annotations
5
+ from importlib.metadata import version
6
+ __version__ = version("flpit")
5
7
 
6
8
  import builtins
7
9
  from typing import Iterable as _Iterable, TypeVar
@@ -27,9 +29,6 @@ def repeat(element: TItem, count: int) -> FlpIt[TItem]:
27
29
  """Generates a lazy sequence that contains one repeated value."""
28
30
  return FlpIt(element for _ in builtins.range(count))
29
31
 
30
- Iterable = it
31
- List = lst
32
-
33
32
  __all__ = [
34
33
  "FlpIt",
35
34
  "OrderedIt",
@@ -37,8 +36,6 @@ __all__ = [
37
36
  "FlpList",
38
37
  "it",
39
38
  "lst",
40
- "Iterable",
41
- "List",
42
39
  "range",
43
40
  "repeat",
44
41
  ]
@@ -66,8 +66,12 @@ class _FactoryIterable(Iterable[TItem], Generic[TItem]):
66
66
  class FlpIt(Iterable[TItem], Generic[TItem]):
67
67
  """
68
68
  | Fluent Iterable
69
- Lazy evaluation wrapper around an iterable (matching .NET IEnumerable<T>). No internal caching.
70
- Supports multiple passes over queries if the underlying collection is re-iterable.
69
+ Lazy evaluation wrapper around an iterable, inspired by .NET LINQ's
70
+ IEnumerable<T> semantics. Operations are deferred and do not cache results
71
+ unless explicitly documented otherwise.
72
+
73
+ Multiple enumeration is supported when the underlying source is re-iterable;
74
+ one-shot iterators and generators remain one-shot.
71
75
  """
72
76
  __slots__ = ("_iterable",)
73
77
 
@@ -92,6 +96,13 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
92
96
 
93
97
  return FlpIt(_FactoryIterable(_generator))
94
98
 
99
+ def concat(self, second: Iterable[TItem]) -> "FlpIt[TItem]":
100
+ def _generator() -> Iterator[TItem]:
101
+ yield from self
102
+ yield from second
103
+
104
+ return FlpIt(_FactoryIterable(_generator))
105
+
95
106
  def prepend(self, element: TItem) -> FlpIt[TItem]:
96
107
  """Prepends an element to the beginning of the sequence (deferred)."""
97
108
  def _generator() -> Iterator[TItem]:
@@ -128,20 +139,25 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
128
139
  return FlpIt(_FactoryIterable(_generator))
129
140
 
130
141
  def take(self, count: int) -> "FlpIt[TItem]":
131
- """Returns a specified number of contiguous elements from the start."""
142
+ """
143
+ |Returns a specified number of contiguous elements from the start.
144
+
145
+ Execution is deferred until the returned sequence is enumerated.
146
+ The operator consumes no more than `count` elements from upstream.
147
+
148
+ `take()` does not own the upstream iterator and therefore does not
149
+ close it when the limit is reached. Upstream resource lifetime is the
150
+ responsibility of the code that owns/acquires the source; use an
151
+ explicit context manager or close the source explicitly when required.
152
+
153
+ This is intentional: reaching the `take()` limit is normal completion,
154
+ not cancellation or disposal of the upstream sequence.
155
+ """
132
156
  if count <= 0:
133
157
  return FlpIt(())
134
158
 
135
159
  def _generator() -> Iterator[TItem]:
136
- # 1. We must actively grab the live iterator instance from self
137
- upstream_iterator = iter(self)
138
- try:
139
- yield from islice(upstream_iterator, count)
140
- finally:
141
- # 2. once islice finishes or gets aborted,
142
- # we FORCE the upstream chain to collapse and trigger its cleanup!
143
- if hasattr(upstream_iterator, "close"):
144
- upstream_iterator.close()
160
+ yield from islice(iter(self), count)
145
161
 
146
162
  return FlpIt(_FactoryIterable(_generator))
147
163
 
@@ -259,29 +275,7 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
259
275
 
260
276
  return FlpIt(_FactoryIterable(_generator))
261
277
 
262
- def join(
263
- self,
264
- inner: Iterable[TOther],
265
- outer_key_selector: Callable[[TItem], TKey],
266
- inner_key_selector: Callable[[TOther], TKey],
267
- result_selector: Callable[[TItem, TOther], TResult],
268
- ) -> FlpIt[TResult]:
269
- """Correlates elements of two sequences based on matching keys (Hash Join)."""
270
- def _generator() -> Iterator[TResult]:
271
- lookup: dict[TKey, List[TOther]] = {}
272
- for inner_item in inner:
273
- key = inner_key_selector(inner_item)
274
- lookup.setdefault(key, []).append(inner_item)
275
-
276
- for outer_item in self:
277
- key = outer_key_selector(outer_item)
278
- if key in lookup:
279
- for inner_item in lookup[key]:
280
- yield result_selector(outer_item, inner_item)
281
-
282
- return FlpIt(_FactoryIterable(_generator))
283
-
284
- # --- Immediate Execution (Materialization & Aggregation) ---
278
+ # --- Immediate Execution (Materialization & Aggregation) ---
285
279
 
286
280
  @overload
287
281
  def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
@@ -427,42 +421,136 @@ class FlpIt(Iterable[TItem], Generic[TItem]):
427
421
  return FlpList(self)
428
422
 
429
423
 
424
+ from threading import Lock
425
+
430
426
  class OrderedIt(FlpIt[TItem]):
431
427
  """
432
- | Ordered Iterable
433
- Represents a sorted sequence (matching .NET IOrderedEnumerable<T>). Supports then_by chaining.
428
+ Ordered Iterable.
429
+
430
+ Sorts the source lazily on first iteration and caches the resulting order.
431
+ The source is consumed at most once; subsequent iterations reuse the cached
432
+ result.
434
433
  """
435
- __slots__ = ("_source", "_comparers")
434
+
435
+ __slots__ = (
436
+ "_source",
437
+ "_key_selector",
438
+ "_descending",
439
+ "_parent",
440
+ "_cached_result",
441
+ "_lock",
442
+ )
436
443
 
437
444
  def __init__(
438
445
  self,
439
446
  source: Iterable[TItem],
440
447
  key_selector: Callable[[TItem], Any],
441
448
  descending: bool = False,
449
+ parent: Optional["OrderedIt[TItem]"] = None,
442
450
  ) -> None:
443
451
  super().__init__(source)
444
- self._source: Iterable[TItem] = source
445
- self._comparers: list[tuple[Callable[[TItem], Any], bool]] = [
446
- (key_selector, descending)
447
- ]
448
-
449
- def then_by(self, key_selector: Callable[[TItem], Any]) -> OrderedIt[TItem]:
450
- """Performs a subsequent ordering in ascending order."""
451
- new_ordered = OrderedIt(self._source, key_selector, descending=False)
452
- new_ordered._comparers = self._comparers + [(key_selector, False)]
453
- return new_ordered
454
-
455
- def then_by_descending(self, key_selector: Callable[[TItem], Any]) -> OrderedIt[TItem]:
456
- """Performs a subsequent ordering in descending order."""
457
- new_ordered = OrderedIt(self._source, key_selector, descending=True)
458
- new_ordered._comparers = self._comparers + [(key_selector, True)]
459
- return new_ordered
452
+
453
+ self._source = source
454
+ self._key_selector = key_selector
455
+ self._descending = descending
456
+ self._parent = parent
457
+
458
+ self._cached_result: Optional[list[TItem]] = None
459
+ self._lock = Lock()
460
+
461
+ def then_by(
462
+ self,
463
+ key_selector: Callable[[TItem], Any],
464
+ ) -> "OrderedIt[TItem]":
465
+ return OrderedIt(
466
+ self._source,
467
+ key_selector,
468
+ descending=False,
469
+ parent=self,
470
+ )
471
+
472
+ def then_by_descending(
473
+ self,
474
+ key_selector: Callable[[TItem], Any],
475
+ ) -> "OrderedIt[TItem]":
476
+ return OrderedIt(
477
+ self._source,
478
+ key_selector,
479
+ descending=True,
480
+ parent=self,
481
+ )
460
482
 
461
483
  def __iter__(self) -> Iterator[TItem]:
462
- items = list(self._source)
463
- for key_selector, descending in reversed(self._comparers):
464
- items.sort(key=key_selector, reverse=descending)
465
- return iter(items)
484
+ cached = self._cached_result
485
+
486
+ if cached is None:
487
+ with self._lock:
488
+ cached = self._cached_result
489
+
490
+ if cached is None:
491
+ # Collect the complete ordering chain.
492
+ comparers: list[
493
+ tuple[Callable[[TItem], Any], bool]
494
+ ] = []
495
+
496
+ node: Optional["OrderedIt[TItem]"] = self
497
+
498
+ while node is not None:
499
+ comparers.append(
500
+ (node._key_selector, node._descending)
501
+ )
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
+
544
+ self._cached_result = cached
545
+
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
+ yield from cached
466
554
 
467
555
 
468
556
  class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
@@ -480,6 +568,14 @@ class Grouping(FlpIt[TItem], Generic[TKey, TItem]):
480
568
  def __repr__(self) -> str:
481
569
  return f"Grouping(key={self.key!r}, elements={self.to_list()!r})"
482
570
 
571
+ def __eq__(self, other: Any) -> bool:
572
+ # Check if the other object is a Grouping (or subclass)
573
+ if not isinstance(other, Grouping):
574
+ return False
575
+
576
+ # Compare the keys, then compare the elements inside FlpIt
577
+ return self.key == other.key and self.to_list() == other.to_list()
578
+
483
579
 
484
580
  class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
485
581
  """
@@ -488,31 +584,44 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
488
584
  """
489
585
 
490
586
  def add(self, item: TItem) -> None:
491
- """Adds an item and performs O(1) type consistency check against the first element."""
492
- if self.data and not isinstance(item, type(self.data[0])):
493
- raise TypeError(
494
- f"Element of type '{type(item).__name__}' does not match "
495
- f"list item type '{type(self.data[0]).__name__}'."
496
- )
587
+ """
588
+ |Adds an item and performs O(1) type consistency check against the first element.
589
+
590
+ |Type safety via
591
+ - type checks
592
+ - manual of_type(...) filter if you don't trust your checks
593
+ """
497
594
  self.data.append(item)
498
595
 
499
596
  def add_range(self, items: Iterable[TItem]) -> None:
500
- """Adds a sequence of items and performs O(1) type checking on the first incoming element."""
597
+ """
598
+ | Adds an Iterable sequence or stream.
599
+ Optimizes paths based on input type without destroying volatile generators.
600
+
601
+ |Type safety via
602
+ - type checks
603
+ - manual of_type(...) filter if you don't trust your checks
604
+ """
605
+ # 1. Optimized Path: Fast memory extensions for pre-materialized sequences
606
+ if isinstance(items, (Sequence, list, tuple, UserList)):
607
+ self.data.extend(items)
608
+ return
609
+
610
+ # 2. Stream Path: Volatile one-shot generator handling
501
611
  it = iter(items)
502
612
  try:
503
613
  first_item = next(it)
504
614
  except StopIteration:
505
615
  return
506
616
 
507
- if self.data and not isinstance(first_item, type(self.data[0])):
508
- raise TypeError(
509
- f"Element of type '{type(first_item).__name__}' does not match "
510
- f"list item type '{type(self.data[0]).__name__}'."
511
- )
512
-
617
+ # Append the tracked peek-element and stream the remainder safely
513
618
  self.data.append(first_item)
514
619
  self.data.extend(it)
515
620
 
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())
624
+
516
625
  def append_linq(self, element: TItem) -> FlpIt[TItem]:
517
626
  """Appends an element to the sequence lazily, returning a FlpIt without mutating this list."""
518
627
  return FlpIt(self.data).append(element)
@@ -588,17 +697,6 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
588
697
  ) -> FlpIt[Grouping[TKey, TItem]]:
589
698
  return FlpIt(self.data).group_by(key_selector)
590
699
 
591
- def join(
592
- self,
593
- inner: Iterable[TOther],
594
- outer_key_selector: Callable[[TItem], TKey],
595
- inner_key_selector: Callable[[TOther], TKey],
596
- result_selector: Callable[[TItem, TOther], TResult],
597
- ) -> FlpIt[TResult]:
598
- return FlpIt(self.data).join(
599
- inner, outer_key_selector, inner_key_selector, result_selector
600
- )
601
-
602
700
  @overload
603
701
  def aggregate(self, func: Callable[[TItem, TItem], TItem]) -> TItem: ...
604
702
 
@@ -688,4 +786,5 @@ class FlpList(UserList[TItem], Sequence[TItem], Generic[TItem]):
688
786
  return FlpIt(self.data).single(predicate)
689
787
 
690
788
  def to_list(self) -> FlpList[TItem]:
691
- return self
789
+ """Explicitly returns a shallow copy instance to isolate mutations."""
790
+ return FlpList(self.data.copy())
@@ -1,41 +0,0 @@
1
- # flp
2
-
3
- > **Fluent LINQ for Python – flip your iterables**
4
-
5
- `flp` brings .NET's LINQ fluent API to standard Python iterables with full static typing.
6
-
7
- ## ⚡ Key Features
8
-
9
- * **⚡ Lazy Evaluation (`FlpIt`):** Deferred evaluation via pure Python generator expressions.
10
- * **📦 Eager Container (`FlpList`):** Persisted state backed by `collections.UserList`.
11
- * **🔹 Static-First Typing:** Built for full `mypy` and `pyright` inference without plugins.
12
- * **🟢 O(1) Boundary Validation:** Boundary checks rely on O(1) sampling instead of O(N) scans.
13
-
14
- ## ⚙️ Installation
15
-
16
- ```bash
17
- uv add flp
18
- ```
19
-
20
- ## 💡 Quick Start
21
-
22
- ```python
23
- from flp import FlpList, FlpIt
24
-
25
- # Eager collection
26
- data = FlpList([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
27
-
28
- # Deferred / lazy pipeline
29
- query: FlpIt[int] = (
30
- data
31
- .where(lambda x: x % 2 == 0)
32
- .select(lambda x: x * 10)
33
- )
34
-
35
- # Materialization happens explicitly
36
- result: FlpList[int] = query.to_list() # [20, 40, 60, 80, 100]
37
- ```
38
-
39
- ## 📜 License
40
-
41
- Distributed under the MIT License. See `LICENSE` for more information.
File without changes