pyochain 0.5.1__py3-none-any.whl → 0.5.32__py3-none-any.whl

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.

Potentially problematic release.


This version of pyochain might be problematic. Click here for more details.

pyochain/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from ._core import Wrapper
2
- from ._dict import Dict, Expr, key
2
+ from ._dict import Dict
3
3
  from ._iter import Iter, Seq
4
4
 
5
- __all__ = ["Dict", "Iter", "Wrapper", "key", "Seq", "Expr"]
5
+ __all__ = ["Dict", "Iter", "Wrapper", "Seq"]
@@ -1,6 +1,6 @@
1
+ from ._format import Peeked, peek, peekn
1
2
  from ._main import CommonBase, IterWrapper, MappingWrapper, Pipeable, Wrapper
2
3
  from ._protocols import (
3
- Peeked,
4
4
  SizedIterable,
5
5
  SupportsAllComparisons,
6
6
  SupportsKeysAndGetItem,
@@ -18,4 +18,6 @@ __all__ = [
18
18
  "Peeked",
19
19
  "SizedIterable",
20
20
  "Pipeable",
21
+ "peek",
22
+ "peekn",
21
23
  ]
@@ -0,0 +1,34 @@
1
+ from collections.abc import Iterable, Iterator, Mapping
2
+ from pprint import pformat
3
+ from typing import Any, NamedTuple
4
+
5
+ import cytoolz as cz
6
+
7
+
8
+ class Peeked[T](NamedTuple):
9
+ value: T | tuple[T, ...]
10
+ sequence: Iterator[T]
11
+
12
+
13
+ def peekn[T](data: Iterable[T], n: int) -> Iterator[T]:
14
+ peeked = Peeked(*cz.itertoolz.peekn(n, data))
15
+ print(f"Peeked {n} values: {peeked.value}")
16
+ return peeked.sequence
17
+
18
+
19
+ def peek[T](data: Iterable[T]) -> Iterator[T]:
20
+ peeked = Peeked(*cz.itertoolz.peek(data))
21
+ print(f"Peeked value: {peeked.value}")
22
+ return peeked.sequence
23
+
24
+
25
+ def dict_repr(
26
+ v: Mapping[Any, Any],
27
+ max_items: int = 20,
28
+ depth: int = 3,
29
+ width: int = 80,
30
+ compact: bool = True,
31
+ ) -> str:
32
+ truncated = dict(list(v.items())[:max_items])
33
+ suffix = "..." if len(v) > max_items else ""
34
+ return pformat(truncated, depth=depth, width=width, compact=compact) + suffix
pyochain/_core/_main.py CHANGED
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from collections.abc import Callable, Collection, Iterable, Iterator
4
+ from collections.abc import Callable, Iterable, Iterator, Sequence
5
5
  from typing import TYPE_CHECKING, Any, Concatenate, Self
6
6
 
7
+ from ._format import dict_repr
8
+
7
9
  if TYPE_CHECKING:
8
10
  from .._dict import Dict
9
11
  from .._iter import Iter, Seq
@@ -27,12 +29,12 @@ class CommonBase[T](ABC, Pipeable):
27
29
  The pipe unwrap method must be implemented to allow piping functions that transform the underlying data type, whilst retaining the wrapper.
28
30
  """
29
31
 
30
- _data: T
32
+ _inner: T
31
33
 
32
- __slots__ = ("_data",)
34
+ __slots__ = ("_inner",)
33
35
 
34
36
  def __init__(self, data: T) -> None:
35
- self._data = data
37
+ self._inner = data
36
38
 
37
39
  @abstractmethod
38
40
  def apply[**P](
@@ -53,9 +55,9 @@ class CommonBase[T](ABC, Pipeable):
53
55
  from pprint import pprint
54
56
 
55
57
  if pretty:
56
- pprint(self.unwrap(), sort_dicts=False)
58
+ self.into(pprint, sort_dicts=False)
57
59
  else:
58
- print(self.unwrap())
60
+ self.into(print)
59
61
  return self
60
62
 
61
63
  def unwrap(self) -> T:
@@ -64,7 +66,7 @@ class CommonBase[T](ABC, Pipeable):
64
66
 
65
67
  This is a terminal operation.
66
68
  """
67
- return self._data
69
+ return self._inner
68
70
 
69
71
  def into[**P, R](
70
72
  self,
@@ -86,57 +88,73 @@ class CommonBase[T](ABC, Pipeable):
86
88
  """
87
89
  return func(self.unwrap(), *args, **kwargs)
88
90
 
89
-
90
- class IterWrapper[T](CommonBase[Iterable[T]]):
91
- _data: Iterable[T]
92
-
93
- def apply[**P, R](
94
- self,
95
- func: Callable[Concatenate[Iterable[T], P], Iterator[R]],
96
- *args: P.args,
97
- **kwargs: P.kwargs,
98
- ) -> Iter[R]:
91
+ def equals_to(self, other: Self | T) -> bool:
99
92
  """
100
- Apply a function to the underlying iterable and return an Iter of the result.
101
- Allow to pass user defined functions that transform the iterable while retaining the Iter wrapper.
93
+ Check if two records are equal based on their data.
94
+
102
95
  Args:
103
- func: Function to apply to the underlying iterable.
104
- *args: Positional arguments to pass to the function.
105
- **kwargs: Keyword arguments to pass to the function.
96
+ other: Another instance or corresponding underlying data to compare against.
106
97
 
107
98
  Example:
108
99
  ```python
109
100
  >>> import pyochain as pc
110
- >>> def double(data: Iterable[int]) -> Iterator[int]:
111
- ... return (x * 2 for x in data)
112
- >>> pc.Iter.from_([1, 2, 3]).apply(double).into(list)
113
- [2, 4, 6]
101
+ >>> d1 = pc.Dict({"a": 1, "b": 2})
102
+ >>> d2 = pc.Dict({"a": 1, "b": 2})
103
+ >>> d3 = pc.Dict({"a": 1, "b": 3})
104
+ >>> d1.equals_to(d2)
105
+ True
106
+ >>> d1.equals_to(d3)
107
+ False
108
+
109
+ ```
114
110
  """
115
- from .._iter import Iter
111
+ other_data = other.unwrap() if isinstance(other, self.__class__) else other
112
+ return self.unwrap() == other_data
116
113
 
117
- return Iter(self.into(func, *args, **kwargs))
118
114
 
119
- def collect(self, factory: Callable[[Iterable[T]], Collection[T]] = list) -> Seq[T]:
120
- """
121
- Collect the elements into a sequence.
122
- Args:
123
- factory: A callable that takes an iterable and returns a collection. Defaults to list.
115
+ class IterWrapper[T](CommonBase[Iterable[T]]):
116
+ _inner: Iterable[T]
124
117
 
125
- Example:
126
- ```python
127
- >>> import pyochain as pc
128
- >>> pc.Iter.from_(range(5)).collect().unwrap()
129
- [0, 1, 2, 3, 4]
118
+ def __repr__(self) -> str:
119
+ return f"{self.__class__.__name__}({self.unwrap().__repr__()})"
130
120
 
131
- ```
132
- """
121
+ def _eager[**P, U](
122
+ self,
123
+ factory: Callable[Concatenate[Iterable[T], P], Sequence[U]],
124
+ *args: P.args,
125
+ **kwargs: P.kwargs,
126
+ ) -> Seq[U]:
133
127
  from .._iter import Seq
134
128
 
135
- return Seq(self.into(factory))
129
+ def _(data: Iterable[T]):
130
+ return Seq(factory(data, *args, **kwargs))
131
+
132
+ return self.into(_)
133
+
134
+ def _lazy[**P, U](
135
+ self,
136
+ factory: Callable[Concatenate[Iterable[T], P], Iterator[U]],
137
+ *args: P.args,
138
+ **kwargs: P.kwargs,
139
+ ) -> Iter[U]:
140
+ from .._iter import Iter
141
+
142
+ def _(data: Iterable[T]):
143
+ return Iter(factory(data, *args, **kwargs))
144
+
145
+ return self.into(_)
136
146
 
137
147
 
138
148
  class MappingWrapper[K, V](CommonBase[dict[K, V]]):
139
- _data: dict[K, V]
149
+ _inner: dict[K, V]
150
+
151
+ def __repr__(self) -> str:
152
+ return f"{self.into(dict_repr)}"
153
+
154
+ def _new[KU, VU](self, func: Callable[[dict[K, V]], dict[KU, VU]]) -> Dict[KU, VU]:
155
+ from .._dict import Dict
156
+
157
+ return Dict(func(self.unwrap()))
140
158
 
141
159
  def apply[**P, KU, VU](
142
160
  self,
@@ -147,6 +165,7 @@ class MappingWrapper[K, V](CommonBase[dict[K, V]]):
147
165
  """
148
166
  Apply a function to the underlying dict and return a Dict of the result.
149
167
  Allow to pass user defined functions that transform the dict while retaining the Dict wrapper.
168
+
150
169
  Args:
151
170
  func: Function to apply to the underlying dict.
152
171
  *args: Positional arguments to pass to the function.
@@ -161,9 +180,11 @@ class MappingWrapper[K, V](CommonBase[dict[K, V]]):
161
180
 
162
181
  ```
163
182
  """
164
- from .._dict import Dict
165
183
 
166
- return Dict(self.into(func, *args, **kwargs))
184
+ def _(data: dict[K, V]) -> dict[KU, VU]:
185
+ return func(data, *args, **kwargs)
186
+
187
+ return self._new(_)
167
188
 
168
189
 
169
190
  class Wrapper[T](CommonBase[T]):
@@ -1,10 +1,5 @@
1
- from collections.abc import Iterable, Iterator, Sized
2
- from typing import NamedTuple, Protocol
3
-
4
-
5
- class Peeked[T](NamedTuple):
6
- value: T | tuple[T, ...]
7
- sequence: Iterator[T]
1
+ from collections.abc import Iterable, Sized
2
+ from typing import Protocol
8
3
 
9
4
 
10
5
  class SupportsDunderLT[T](Protocol):
@@ -1,4 +1,3 @@
1
- from ._exprs import Expr, key
2
1
  from ._main import Dict
3
2
 
4
- __all__ = ["Dict", "key", "Expr"]
3
+ __all__ = ["Dict"]
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable, Mapping
4
4
  from functools import partial
5
- from typing import TYPE_CHECKING, Any, TypeGuard
5
+ from typing import TYPE_CHECKING, Any, TypeIs, overload
6
6
 
7
7
  import cytoolz as cz
8
8
 
@@ -13,9 +13,15 @@ if TYPE_CHECKING:
13
13
 
14
14
 
15
15
  class FilterDict[K, V](MappingWrapper[K, V]):
16
- def filter_keys(self, predicate: Callable[[K], bool]) -> Dict[K, V]:
16
+ @overload
17
+ def filter_keys[U](self, predicate: Callable[[K], TypeIs[U]]) -> Dict[U, V]: ...
18
+ @overload
19
+ def filter_keys(self, predicate: Callable[[K], bool]) -> Dict[K, V]: ...
20
+ def filter_keys[U](
21
+ self, predicate: Callable[[K], bool | TypeIs[U]]
22
+ ) -> Dict[K, V] | Dict[U, V]:
17
23
  """
18
- Return a new Dict containing keys that satisfy predicate.
24
+ Return keys that satisfy predicate.
19
25
 
20
26
  Args:
21
27
  predicate: Function to determine if a key should be included.
@@ -28,11 +34,17 @@ class FilterDict[K, V](MappingWrapper[K, V]):
28
34
 
29
35
  ```
30
36
  """
31
- return self.apply(partial(cz.dicttoolz.keyfilter, predicate))
32
-
33
- def filter_values(self, predicate: Callable[[V], bool]) -> Dict[K, V]:
37
+ return self._new(partial(cz.dicttoolz.keyfilter, predicate))
38
+
39
+ @overload
40
+ def filter_values[U](self, predicate: Callable[[V], TypeIs[U]]) -> Dict[K, U]: ...
41
+ @overload
42
+ def filter_values(self, predicate: Callable[[V], bool]) -> Dict[K, V]: ...
43
+ def filter_values[U](
44
+ self, predicate: Callable[[V], bool] | Callable[[V], TypeIs[U]]
45
+ ) -> Dict[K, V] | Dict[K, U]:
34
46
  """
35
- Return a new Dict containing items whose values satisfy predicate.
47
+ Return items whose values satisfy predicate.
36
48
 
37
49
  Args:
38
50
  predicate: Function to determine if a value should be included.
@@ -47,12 +59,9 @@ class FilterDict[K, V](MappingWrapper[K, V]):
47
59
 
48
60
  ```
49
61
  """
50
- return self.apply(partial(cz.dicttoolz.valfilter, predicate))
62
+ return self._new(partial(cz.dicttoolz.valfilter, predicate))
51
63
 
52
- def filter_items(
53
- self,
54
- predicate: Callable[[tuple[K, V]], bool],
55
- ) -> Dict[K, V]:
64
+ def filter_items(self, predicate: Callable[[tuple[K, V]], bool]) -> Dict[K, V]:
56
65
  """
57
66
  Filter items by predicate applied to (key, value) tuples.
58
67
 
@@ -73,12 +82,9 @@ class FilterDict[K, V](MappingWrapper[K, V]):
73
82
 
74
83
  ```
75
84
  """
76
- return self.apply(partial(cz.dicttoolz.itemfilter, predicate))
85
+ return self._new(partial(cz.dicttoolz.itemfilter, predicate))
77
86
 
78
- def filter_kv(
79
- self,
80
- predicate: Callable[[K, V], bool],
81
- ) -> Dict[K, V]:
87
+ def filter_kv(self, predicate: Callable[[K, V], bool]) -> Dict[K, V]:
82
88
  """
83
89
  Filter items by predicate applied to unpacked (key, value) tuples.
84
90
 
@@ -105,13 +111,14 @@ class FilterDict[K, V](MappingWrapper[K, V]):
105
111
 
106
112
  return cz.dicttoolz.itemfilter(_, data)
107
113
 
108
- return self.apply(_filter_kv)
114
+ return self._new(_filter_kv)
109
115
 
110
116
  def filter_attr[U](self, attr: str, dtype: type[U] = object) -> Dict[K, U]:
111
117
  """
112
118
  Filter values that have a given attribute.
113
119
 
114
120
  This does not enforce type checking at runtime for performance considerations.
121
+
115
122
  Args:
116
123
  attr: Attribute name to check for.
117
124
  dtype: Optional expected type of the attribute for type hinting.
@@ -127,19 +134,19 @@ class FilterDict[K, V](MappingWrapper[K, V]):
127
134
  """
128
135
 
129
136
  def _filter_attr(data: dict[K, V]) -> dict[K, U]:
130
- def has_attr(x: V) -> TypeGuard[U]:
137
+ def has_attr(x: object) -> TypeIs[U]:
131
138
  return hasattr(x, attr)
132
139
 
133
140
  return cz.dicttoolz.valfilter(has_attr, data)
134
141
 
135
- return self.apply(_filter_attr)
142
+ return self._new(_filter_attr)
136
143
 
137
- def filter_type[R](self, typ: type[R]) -> Dict[K, R]:
144
+ def filter_type[R](self, dtype: type[R]) -> Dict[K, R]:
138
145
  """
139
146
  Filter values by type.
140
147
 
141
148
  Args:
142
- typ: Type to filter values by.
149
+ dtype: Type to filter values by.
143
150
  Example:
144
151
  ```python
145
152
  >>> import pyochain as pc
@@ -151,34 +158,12 @@ class FilterDict[K, V](MappingWrapper[K, V]):
151
158
  """
152
159
 
153
160
  def _filter_type(data: dict[K, V]) -> dict[K, R]:
154
- def _(x: V) -> TypeGuard[R]:
155
- return isinstance(x, typ)
156
-
157
- return cz.dicttoolz.valfilter(_, data)
158
-
159
- return self.apply(_filter_type)
160
-
161
- def filter_callable(self) -> Dict[K, Callable[..., Any]]:
162
- """
163
- Filter values that are callable.
164
- ```python
165
- >>> import pyochain as pc
166
- >>> def foo():
167
- ... pass
168
- >>> data = {1: "one", 2: "two", 3: foo, 4: print}
169
- >>> pc.Dict(data).filter_callable().map_values(lambda x: x.__name__).unwrap()
170
- {3: 'foo', 4: 'print'}
171
-
172
- ```
173
- """
174
-
175
- def _filter_callable(data: dict[K, V]) -> dict[K, Callable[..., Any]]:
176
- def _(x: V) -> TypeGuard[Callable[..., Any]]:
177
- return callable(x)
161
+ def _(x: object) -> TypeIs[R]:
162
+ return isinstance(x, dtype)
178
163
 
179
164
  return cz.dicttoolz.valfilter(_, data)
180
165
 
181
- return self.apply(_filter_callable)
166
+ return self._new(_filter_type)
182
167
 
183
168
  def filter_subclass[U: type[Any], R](
184
169
  self: FilterDict[K, U], parent: type[R], keep_parent: bool = True
@@ -210,7 +195,7 @@ class FilterDict[K, V](MappingWrapper[K, V]):
210
195
  """
211
196
 
212
197
  def _filter_subclass(data: dict[K, U]) -> dict[K, type[R]]:
213
- def _(x: type[Any]) -> TypeGuard[type[R]]:
198
+ def _(x: type[Any]) -> TypeIs[type[R]]:
214
199
  if keep_parent:
215
200
  return issubclass(x, parent)
216
201
  else:
@@ -218,11 +203,11 @@ class FilterDict[K, V](MappingWrapper[K, V]):
218
203
 
219
204
  return cz.dicttoolz.valfilter(_, data)
220
205
 
221
- return self.apply(_filter_subclass)
206
+ return self._new(_filter_subclass)
222
207
 
223
208
  def intersect_keys(self, *others: Mapping[K, V]) -> Dict[K, V]:
224
209
  """
225
- Return a new Dict keeping only keys present in self and all others.
210
+ Keep only keys present in self and all others mappings.
226
211
 
227
212
  Args:
228
213
  *others: Other mappings to intersect keys with.
@@ -244,11 +229,11 @@ class FilterDict[K, V](MappingWrapper[K, V]):
244
229
  self_keys.intersection_update(other.keys())
245
230
  return {k: data[k] for k in self_keys}
246
231
 
247
- return self.apply(_intersect_keys)
232
+ return self._new(_intersect_keys)
248
233
 
249
234
  def diff_keys(self, *others: Mapping[K, V]) -> Dict[K, V]:
250
235
  """
251
- Return a new Dict keeping only keys present in self but not in others.
236
+ Keep only keys present in self but not in others mappings.
252
237
 
253
238
  Args:
254
239
  *others: Other mappings to exclude keys from.
@@ -270,4 +255,4 @@ class FilterDict[K, V](MappingWrapper[K, V]):
270
255
  self_keys.difference_update(other.keys())
271
256
  return {k: data[k] for k in self_keys}
272
257
 
273
- return self.apply(_diff_keys)
258
+ return self._new(_diff_keys)
pyochain/_dict/_groups.py CHANGED
@@ -34,7 +34,7 @@ class GroupsDict[K, V](MappingWrapper[K, V]):
34
34
 
35
35
  return cz.dicttoolz.valmap(dict, cz.itertoolz.groupby(_, data.items()))
36
36
 
37
- return self.apply(_group_by_value)
37
+ return self._new(_group_by_value)
38
38
 
39
39
  def group_by_key[G](self, func: Callable[[K], G]) -> Dict[G, dict[K, V]]:
40
40
  """
@@ -58,7 +58,7 @@ class GroupsDict[K, V](MappingWrapper[K, V]):
58
58
 
59
59
  return cz.dicttoolz.valmap(dict, cz.itertoolz.groupby(_, data.items()))
60
60
 
61
- return self.apply(_group_by_key)
61
+ return self._new(_group_by_key)
62
62
 
63
63
  def group_by_key_agg[G, R](
64
64
  self,
@@ -72,7 +72,7 @@ class GroupsDict[K, V](MappingWrapper[K, V]):
72
72
  key_func: Function to determine the group for each key.
73
73
  agg_func: Function to aggregate each sub-dictionary.
74
74
 
75
- This avoids materializing intermediate `Dict` objects if you only need
75
+ This avoids materializing intermediate `dict` objects if you only need
76
76
  an aggregated result for each group.
77
77
  ```python
78
78
  >>> import pyochain as pc
@@ -118,7 +118,7 @@ class GroupsDict[K, V](MappingWrapper[K, V]):
118
118
  groups = cz.itertoolz.groupby(_key_func, data.items())
119
119
  return cz.dicttoolz.valmap(_agg_func, groups)
120
120
 
121
- return self.apply(_group_by_key_agg)
121
+ return self._new(_group_by_key_agg)
122
122
 
123
123
  def group_by_value_agg[G, R](
124
124
  self,
@@ -132,7 +132,7 @@ class GroupsDict[K, V](MappingWrapper[K, V]):
132
132
  value_func: Function to determine the group for each value.
133
133
  agg_func: Function to aggregate each sub-dictionary.
134
134
 
135
- This avoids materializing intermediate `Dict` objects if you only need
135
+ This avoids materializing intermediate `dict` objects if you only need
136
136
  an aggregated result for each group.
137
137
  ```python
138
138
  >>> import pyochain as pc
@@ -143,8 +143,7 @@ class GroupsDict[K, V](MappingWrapper[K, V]):
143
143
  ... agg_func=lambda d: d.iter_keys().count(),
144
144
  ... ).unwrap()
145
145
  {'A': 2, 'B': 1}
146
- >>>
147
- >>> # --- Exemple 2: Agrégation plus complexe ---
146
+ >>> # Second example
148
147
  >>> sales_data = {
149
148
  ... "store_1": "Electronics",
150
149
  ... "store_2": "Groceries",
@@ -173,4 +172,4 @@ class GroupsDict[K, V](MappingWrapper[K, V]):
173
172
  groups = cz.itertoolz.groupby(_key_func, data.items())
174
173
  return cz.dicttoolz.valmap(_agg_func, groups)
175
174
 
176
- return self.apply(_group_by_value_agg)
175
+ return self._new(_group_by_value_agg)
pyochain/_dict/_iter.py CHANGED
@@ -1,14 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Callable, Iterable, Mapping
4
- from typing import TYPE_CHECKING, Concatenate
4
+ from typing import TYPE_CHECKING, Any, Concatenate
5
5
 
6
6
  import cytoolz as cz
7
7
 
8
8
  from .._core import MappingWrapper
9
9
 
10
10
  if TYPE_CHECKING:
11
- from .._iter import Iter
11
+ from .._iter import Iter, Seq
12
12
  from ._main import Dict
13
13
 
14
14
 
@@ -43,15 +43,15 @@ class IterDict[K, V](MappingWrapper[K, V]):
43
43
 
44
44
  def _itr(data: Mapping[K, Iterable[U]]) -> dict[K, R]:
45
45
  def _(v: Iterable[U]) -> R:
46
- return func(Iter.from_(v), *args, **kwargs)
46
+ return func(Iter(iter(v)), *args, **kwargs)
47
47
 
48
48
  return cz.dicttoolz.valmap(_, data)
49
49
 
50
- return self.apply(_itr)
50
+ return self._new(_itr)
51
51
 
52
52
  def iter_keys(self) -> Iter[K]:
53
53
  """
54
- Return a Iter of the dict's keys.
54
+ Return an Iter of the dict's keys.
55
55
  ```python
56
56
  >>> import pyochain as pc
57
57
  >>> pc.Dict({1: 2}).iter_keys().into(list)
@@ -61,7 +61,10 @@ class IterDict[K, V](MappingWrapper[K, V]):
61
61
  """
62
62
  from .._iter import Iter
63
63
 
64
- return Iter.from_(self.unwrap().keys())
64
+ def _keys(data: dict[K, V]) -> Iter[K]:
65
+ return Iter(iter(data.keys()))
66
+
67
+ return self.into(_keys)
65
68
 
66
69
  def iter_values(self) -> Iter[V]:
67
70
  """
@@ -75,11 +78,14 @@ class IterDict[K, V](MappingWrapper[K, V]):
75
78
  """
76
79
  from .._iter import Iter
77
80
 
78
- return Iter.from_(self.unwrap().values())
81
+ def _values(data: dict[K, V]) -> Iter[V]:
82
+ return Iter(iter(data.values()))
83
+
84
+ return self.into(_values)
79
85
 
80
86
  def iter_items(self) -> Iter[tuple[K, V]]:
81
87
  """
82
- Return a Iter of the dict's items.
88
+ Return an Iter of the dict's items.
83
89
  ```python
84
90
  >>> import pyochain as pc
85
91
  >>> pc.Dict({1: 2}).iter_items().into(list)
@@ -89,4 +95,41 @@ class IterDict[K, V](MappingWrapper[K, V]):
89
95
  """
90
96
  from .._iter import Iter
91
97
 
92
- return Iter.from_(self.unwrap().items())
98
+ def _items(data: dict[K, V]) -> Iter[tuple[K, V]]:
99
+ return Iter(iter(data.items()))
100
+
101
+ return self.into(_items)
102
+
103
+ def to_arrays(self) -> Seq[list[Any]]:
104
+ """
105
+ Convert the nested dictionary into a sequence of arrays.
106
+
107
+ The sequence represents all paths from root to leaves.
108
+
109
+ ```python
110
+ >>> import pyochain as pc
111
+ >>> data = {
112
+ ... "a": {"b": 1, "c": 2},
113
+ ... "d": {"e": {"f": 3}},
114
+ ... }
115
+ >>> pc.Dict(data).to_arrays().unwrap()
116
+ [['a', 'b', 1], ['a', 'c', 2], ['d', 'e', 'f', 3]]
117
+
118
+ ```
119
+ """
120
+ from .._iter import Seq
121
+
122
+ def _to_arrays(d: Mapping[Any, Any]) -> list[list[Any]]:
123
+ """from dictutils.pivot"""
124
+ match d:
125
+ case Mapping():
126
+ arr: list[Any] = []
127
+ for k, v in d.items():
128
+ for el in _to_arrays(v):
129
+ arr.append([k] + el)
130
+ return arr
131
+
132
+ case _:
133
+ return [[d]]
134
+
135
+ return Seq(self.into(_to_arrays))
pyochain/_dict/_joins.py CHANGED
@@ -33,7 +33,7 @@ class JoinsDict[K, V](MappingWrapper[K, V]):
33
33
  def _inner_join(data: Mapping[K, V]) -> dict[K, tuple[V, W]]:
34
34
  return {k: (v, other[k]) for k, v in data.items() if k in other}
35
35
 
36
- return self.apply(_inner_join)
36
+ return self._new(_inner_join)
37
37
 
38
38
  def left_join[W](self, other: Mapping[K, W]) -> Dict[K, tuple[V, W | None]]:
39
39
  """
@@ -56,7 +56,7 @@ class JoinsDict[K, V](MappingWrapper[K, V]):
56
56
  def _left_join(data: Mapping[K, V]) -> dict[K, tuple[V, W | None]]:
57
57
  return {k: (v, other.get(k)) for k, v in data.items()}
58
58
 
59
- return self.apply(_left_join)
59
+ return self._new(_left_join)
60
60
 
61
61
  def diff(self, other: Mapping[K, V]) -> Dict[K, tuple[V | None, V | None]]:
62
62
  """
@@ -77,9 +77,7 @@ class JoinsDict[K, V](MappingWrapper[K, V]):
77
77
  ```
78
78
  """
79
79
 
80
- def _diff(
81
- data: Mapping[K, V], other: Mapping[K, V]
82
- ) -> dict[K, tuple[V | None, V | None]]:
80
+ def _diff(data: Mapping[K, V]) -> dict[K, tuple[V | None, V | None]]:
83
81
  all_keys: set[K] = data.keys() | other.keys()
84
82
  diffs: dict[K, tuple[V | None, V | None]] = {}
85
83
  for key in all_keys:
@@ -89,11 +87,11 @@ class JoinsDict[K, V](MappingWrapper[K, V]):
89
87
  diffs[key] = (self_val, other_val)
90
88
  return diffs
91
89
 
92
- return self.apply(_diff, other)
90
+ return self._new(_diff)
93
91
 
94
92
  def merge(self, *others: Mapping[K, V]) -> Dict[K, V]:
95
93
  """
96
- Merge other dicts into this one and return a new Dict.
94
+ Merge other dicts into this one.
97
95
 
98
96
  Args:
99
97
  *others: One or more mappings to merge into the current dictionary.
@@ -108,7 +106,11 @@ class JoinsDict[K, V](MappingWrapper[K, V]):
108
106
 
109
107
  ```
110
108
  """
111
- return self.apply(cz.dicttoolz.merge, *others)
109
+
110
+ def _merge(data: Mapping[K, V]) -> dict[K, V]:
111
+ return cz.dicttoolz.merge(data, *others)
112
+
113
+ return self._new(_merge)
112
114
 
113
115
  def merge_with(
114
116
  self, *others: Mapping[K, V], func: Callable[[Iterable[V]], V]
@@ -134,4 +136,4 @@ class JoinsDict[K, V](MappingWrapper[K, V]):
134
136
  def _merge_with(data: Mapping[K, V]) -> dict[K, V]:
135
137
  return cz.dicttoolz.merge_with(func, data, *others)
136
138
 
137
- return self.apply(_merge_with)
139
+ return self._new(_merge_with)