pyochain 0.5.3__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.
pyochain/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from ._core import Wrapper
2
+ from ._dict import Dict
3
+ from ._iter import Iter, Seq
4
+
5
+ __all__ = ["Dict", "Iter", "Wrapper", "Seq"]
@@ -0,0 +1,23 @@
1
+ from ._format import Peeked, peek, peekn
2
+ from ._main import CommonBase, IterWrapper, MappingWrapper, Pipeable, Wrapper
3
+ from ._protocols import (
4
+ SizedIterable,
5
+ SupportsAllComparisons,
6
+ SupportsKeysAndGetItem,
7
+ SupportsRichComparison,
8
+ )
9
+
10
+ __all__ = [
11
+ "MappingWrapper",
12
+ "CommonBase",
13
+ "IterWrapper",
14
+ "Wrapper",
15
+ "SupportsAllComparisons",
16
+ "SupportsRichComparison",
17
+ "SupportsKeysAndGetItem",
18
+ "Peeked",
19
+ "SizedIterable",
20
+ "Pipeable",
21
+ "peek",
22
+ "peekn",
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
@@ -0,0 +1,205 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Callable, Iterable, Iterator, Sequence
5
+ from typing import TYPE_CHECKING, Any, Concatenate, Self
6
+
7
+ from ._format import dict_repr
8
+
9
+ if TYPE_CHECKING:
10
+ from .._dict import Dict
11
+ from .._iter import Iter, Seq
12
+
13
+
14
+ class Pipeable:
15
+ def pipe[**P, R](
16
+ self,
17
+ func: Callable[Concatenate[Self, P], R],
18
+ *args: P.args,
19
+ **kwargs: P.kwargs,
20
+ ) -> R:
21
+ """Pipe the instance in the function and return the result."""
22
+ return func(self, *args, **kwargs)
23
+
24
+
25
+ class CommonBase[T](ABC, Pipeable):
26
+ """
27
+ Base class for all wrappers.
28
+ You can subclass this to create your own wrapper types.
29
+ The pipe unwrap method must be implemented to allow piping functions that transform the underlying data type, whilst retaining the wrapper.
30
+ """
31
+
32
+ _inner: T
33
+
34
+ __slots__ = ("_inner",)
35
+
36
+ def __init__(self, data: T) -> None:
37
+ self._inner = data
38
+
39
+ @abstractmethod
40
+ def apply[**P](
41
+ self,
42
+ func: Callable[Concatenate[T, P], Any],
43
+ *args: P.args,
44
+ **kwargs: P.kwargs,
45
+ ) -> Any:
46
+ raise NotImplementedError
47
+
48
+ def println(self, pretty: bool = True) -> Self:
49
+ """
50
+ Print the underlying data and return self for chaining.
51
+
52
+ Useful for debugging, simply insert `.println()` in the chain,
53
+ and then removing it will not affect the rest of the chain.
54
+ """
55
+ from pprint import pprint
56
+
57
+ if pretty:
58
+ self.into(pprint, sort_dicts=False)
59
+ else:
60
+ self.into(print)
61
+ return self
62
+
63
+ def unwrap(self) -> T:
64
+ """
65
+ Return the underlying data.
66
+
67
+ This is a terminal operation.
68
+ """
69
+ return self._inner
70
+
71
+ def into[**P, R](
72
+ self,
73
+ func: Callable[Concatenate[T, P], R],
74
+ *args: P.args,
75
+ **kwargs: P.kwargs,
76
+ ) -> R:
77
+ """
78
+ Pass the *unwrapped* underlying data into a function.
79
+
80
+ The result is not wrapped.
81
+ ```python
82
+ >>> import pyochain as pc
83
+ >>> pc.Iter.from_(range(5)).into(list)
84
+ [0, 1, 2, 3, 4]
85
+
86
+ ```
87
+ This is a core functionality that allows ending the chain whilst keeping the code style consistent.
88
+ """
89
+ return func(self.unwrap(), *args, **kwargs)
90
+
91
+ def equals_to(self, other: Self | T) -> bool:
92
+ """
93
+ Check if two records are equal based on their data.
94
+
95
+ Args:
96
+ other: Another instance or corresponding underlying data to compare against.
97
+
98
+ Example:
99
+ ```python
100
+ >>> import pyochain as pc
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
+ ```
110
+ """
111
+ other_data = other.unwrap() if isinstance(other, self.__class__) else other
112
+ return self.unwrap() == other_data
113
+
114
+
115
+ class IterWrapper[T](CommonBase[Iterable[T]]):
116
+ _inner: Iterable[T]
117
+
118
+ def __repr__(self) -> str:
119
+ return f"{self.__class__.__name__}({self.unwrap().__repr__()})"
120
+
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]:
127
+ from .._iter import Seq
128
+
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(_)
146
+
147
+
148
+ class MappingWrapper[K, V](CommonBase[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()))
158
+
159
+ def apply[**P, KU, VU](
160
+ self,
161
+ func: Callable[Concatenate[dict[K, V], P], dict[KU, VU]],
162
+ *args: P.args,
163
+ **kwargs: P.kwargs,
164
+ ) -> Dict[KU, VU]:
165
+ """
166
+ Apply a function to the underlying dict and return a Dict of the result.
167
+ Allow to pass user defined functions that transform the dict while retaining the Dict wrapper.
168
+
169
+ Args:
170
+ func: Function to apply to the underlying dict.
171
+ *args: Positional arguments to pass to the function.
172
+ **kwargs: Keyword arguments to pass to the function.
173
+ Example:
174
+ ```python
175
+ >>> import pyochain as pc
176
+ >>> def invert_dict(d: dict[K, V]) -> dict[V, K]:
177
+ ... return {v: k for k, v in d.items()}
178
+ >>> pc.Dict({'a': 1, 'b': 2}).apply(invert_dict).unwrap()
179
+ {1: 'a', 2: 'b'}
180
+
181
+ ```
182
+ """
183
+
184
+ def _(data: dict[K, V]) -> dict[KU, VU]:
185
+ return func(data, *args, **kwargs)
186
+
187
+ return self._new(_)
188
+
189
+
190
+ class Wrapper[T](CommonBase[T]):
191
+ """
192
+ A generic Wrapper for any type.
193
+ The pipe into method is implemented to return a Wrapper of the result type.
194
+
195
+ This class is intended for use with other types/implementations that do not support the fluent/functional style.
196
+ This allow the use of a consistent code style across the code base.
197
+ """
198
+
199
+ def apply[**P, R](
200
+ self,
201
+ func: Callable[Concatenate[T, P], R],
202
+ *args: P.args,
203
+ **kwargs: P.kwargs,
204
+ ) -> Wrapper[R]:
205
+ return Wrapper(self.into(func, *args, **kwargs))
@@ -0,0 +1,38 @@
1
+ from collections.abc import Iterable, Sized
2
+ from typing import Protocol
3
+
4
+
5
+ class SupportsDunderLT[T](Protocol):
6
+ def __lt__(self, other: T, /) -> bool: ...
7
+
8
+
9
+ class SupportsDunderGT[T](Protocol):
10
+ def __gt__(self, other: T, /) -> bool: ...
11
+
12
+
13
+ class SupportsDunderLE[T](Protocol):
14
+ def __le__(self, other: T, /) -> bool: ...
15
+
16
+
17
+ class SupportsDunderGE[T](Protocol):
18
+ def __ge__(self, other: T, /) -> bool: ...
19
+
20
+
21
+ class SupportsKeysAndGetItem[K, V](Protocol):
22
+ def keys(self) -> Iterable[K]: ...
23
+ def __getitem__(self, key: K, /) -> V: ...
24
+
25
+
26
+ class SupportsAllComparisons[T](
27
+ SupportsDunderLT[T],
28
+ SupportsDunderGT[T],
29
+ SupportsDunderLE[T],
30
+ SupportsDunderGE[T],
31
+ Protocol,
32
+ ): ...
33
+
34
+
35
+ type SupportsRichComparison[T] = SupportsDunderLT[T] | SupportsDunderGT[T]
36
+
37
+
38
+ class SizedIterable[T](Sized, Iterable[T]): ...
@@ -0,0 +1,3 @@
1
+ from ._main import Dict
2
+
3
+ __all__ = ["Dict"]
@@ -0,0 +1,268 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Mapping
4
+ from functools import partial
5
+ from typing import TYPE_CHECKING, Any, TypeGuard
6
+
7
+ import cytoolz as cz
8
+
9
+ from .._core import MappingWrapper
10
+
11
+ if TYPE_CHECKING:
12
+ from ._main import Dict
13
+
14
+
15
+ class FilterDict[K, V](MappingWrapper[K, V]):
16
+ def filter_keys(self, predicate: Callable[[K], bool]) -> Dict[K, V]:
17
+ """
18
+ Return keys that satisfy predicate.
19
+
20
+ Args:
21
+ predicate: Function to determine if a key should be included.
22
+ Example:
23
+ ```python
24
+ >>> import pyochain as pc
25
+ >>> d = {1: 2, 2: 3, 3: 4, 4: 5}
26
+ >>> pc.Dict(d).filter_keys(lambda x: x % 2 == 0).unwrap()
27
+ {2: 3, 4: 5}
28
+
29
+ ```
30
+ """
31
+ return self._new(partial(cz.dicttoolz.keyfilter, predicate))
32
+
33
+ def filter_values(self, predicate: Callable[[V], bool]) -> Dict[K, V]:
34
+ """
35
+ Return items whose values satisfy predicate.
36
+
37
+ Args:
38
+ predicate: Function to determine if a value should be included.
39
+ Example:
40
+ ```python
41
+ >>> import pyochain as pc
42
+ >>> d = {1: 2, 2: 3, 3: 4, 4: 5}
43
+ >>> pc.Dict(d).filter_values(lambda x: x % 2 == 0).unwrap()
44
+ {1: 2, 3: 4}
45
+ >>> pc.Dict(d).filter_values(lambda x: not x > 3).unwrap()
46
+ {1: 2, 2: 3}
47
+
48
+ ```
49
+ """
50
+ return self._new(partial(cz.dicttoolz.valfilter, predicate))
51
+
52
+ def filter_items(self, predicate: Callable[[tuple[K, V]], bool]) -> Dict[K, V]:
53
+ """
54
+ Filter items by predicate applied to (key, value) tuples.
55
+
56
+ Args:
57
+ predicate: Function to determine if a (key, value) pair should be included.
58
+ Example:
59
+ ```python
60
+ >>> import pyochain as pc
61
+ >>> def isvalid(item):
62
+ ... k, v = item
63
+ ... return k % 2 == 0 and v < 4
64
+ >>> d = pc.Dict({1: 2, 2: 3, 3: 4, 4: 5})
65
+ >>>
66
+ >>> d.filter_items(isvalid).unwrap()
67
+ {2: 3}
68
+ >>> d.filter_items(lambda kv: not isvalid(kv)).unwrap()
69
+ {1: 2, 3: 4, 4: 5}
70
+
71
+ ```
72
+ """
73
+ return self._new(partial(cz.dicttoolz.itemfilter, predicate))
74
+
75
+ def filter_kv(self, predicate: Callable[[K, V], bool]) -> Dict[K, V]:
76
+ """
77
+ Filter items by predicate applied to unpacked (key, value) tuples.
78
+
79
+ Args:
80
+ predicate: Function to determine if a key-value pair should be included.
81
+ Example:
82
+ ```python
83
+ >>> import pyochain as pc
84
+ >>> def isvalid(key, value):
85
+ ... return key % 2 == 0 and value < 4
86
+ >>> d = pc.Dict({1: 2, 2: 3, 3: 4, 4: 5})
87
+ >>>
88
+ >>> d.filter_kv(isvalid).unwrap()
89
+ {2: 3}
90
+ >>> d.filter_kv(lambda k, v: not isvalid(k, v)).unwrap()
91
+ {1: 2, 3: 4, 4: 5}
92
+
93
+ ```
94
+ """
95
+
96
+ def _filter_kv(data: dict[K, V]) -> dict[K, V]:
97
+ def _(kv: tuple[K, V]) -> bool:
98
+ return predicate(kv[0], kv[1])
99
+
100
+ return cz.dicttoolz.itemfilter(_, data)
101
+
102
+ return self._new(_filter_kv)
103
+
104
+ def filter_attr[U](self, attr: str, dtype: type[U] = object) -> Dict[K, U]:
105
+ """
106
+ Filter values that have a given attribute.
107
+
108
+ This does not enforce type checking at runtime for performance considerations.
109
+
110
+ Args:
111
+ attr: Attribute name to check for.
112
+ dtype: Optional expected type of the attribute for type hinting.
113
+ Example:
114
+ ```python
115
+ >>> import pyochain as pc
116
+ >>> pc.Dict({"a": "hello", "b": "world", "c": 2, "d": 5}).filter_attr(
117
+ ... "capitalize", str
118
+ ... ).unwrap()
119
+ {'a': 'hello', 'b': 'world'}
120
+
121
+ ```
122
+ """
123
+
124
+ def _filter_attr(data: dict[K, V]) -> dict[K, U]:
125
+ def has_attr(x: V) -> TypeGuard[U]:
126
+ return hasattr(x, attr)
127
+
128
+ return cz.dicttoolz.valfilter(has_attr, data)
129
+
130
+ return self._new(_filter_attr)
131
+
132
+ def filter_type[R](self, typ: type[R]) -> Dict[K, R]:
133
+ """
134
+ Filter values by type.
135
+
136
+ Args:
137
+ typ: Type to filter values by.
138
+ Example:
139
+ ```python
140
+ >>> import pyochain as pc
141
+ >>> data = {"a": "one", "b": "two", "c": 3, "d": 4}
142
+ >>> pc.Dict(data).filter_type(str).unwrap()
143
+ {'a': 'one', 'b': 'two'}
144
+
145
+ ```
146
+ """
147
+
148
+ def _filter_type(data: dict[K, V]) -> dict[K, R]:
149
+ def _(x: V) -> TypeGuard[R]:
150
+ return isinstance(x, typ)
151
+
152
+ return cz.dicttoolz.valfilter(_, data)
153
+
154
+ return self._new(_filter_type)
155
+
156
+ def filter_callable(self) -> Dict[K, Callable[..., Any]]:
157
+ """
158
+ Filter values that are callable.
159
+ ```python
160
+ >>> import pyochain as pc
161
+ >>> def foo():
162
+ ... pass
163
+ >>> data = {1: "one", 2: "two", 3: foo, 4: print}
164
+ >>> pc.Dict(data).filter_callable().map_values(lambda x: x.__name__).unwrap()
165
+ {3: 'foo', 4: 'print'}
166
+
167
+ ```
168
+ """
169
+
170
+ def _filter_callable(data: dict[K, V]) -> dict[K, Callable[..., Any]]:
171
+ def _(x: V) -> TypeGuard[Callable[..., Any]]:
172
+ return callable(x)
173
+
174
+ return cz.dicttoolz.valfilter(_, data)
175
+
176
+ return self._new(_filter_callable)
177
+
178
+ def filter_subclass[U: type[Any], R](
179
+ self: FilterDict[K, U], parent: type[R], keep_parent: bool = True
180
+ ) -> Dict[K, type[R]]:
181
+ """
182
+ Filter values that are subclasses of a given parent class.
183
+
184
+ Args:
185
+ parent: Parent class to check against.
186
+ keep_parent: Whether to include the parent class itself. Defaults to True.
187
+
188
+ ```python
189
+ >>> import pyochain as pc
190
+ >>> class A:
191
+ ... pass
192
+ >>> class B(A):
193
+ ... pass
194
+ >>> class C:
195
+ ... pass
196
+ >>> def name(cls: type[Any]) -> str:
197
+ ... return cls.__name__
198
+ >>> data = pc.Dict({"first": A, "second": B, "third": C})
199
+ >>> data.filter_subclass(A).map_values(name).unwrap()
200
+ {'first': 'A', 'second': 'B'}
201
+ >>> data.filter_subclass(A, keep_parent=False).map_values(name).unwrap()
202
+ {'second': 'B'}
203
+
204
+ ```
205
+ """
206
+
207
+ def _filter_subclass(data: dict[K, U]) -> dict[K, type[R]]:
208
+ def _(x: type[Any]) -> TypeGuard[type[R]]:
209
+ if keep_parent:
210
+ return issubclass(x, parent)
211
+ else:
212
+ return issubclass(x, parent) and x is not parent
213
+
214
+ return cz.dicttoolz.valfilter(_, data)
215
+
216
+ return self._new(_filter_subclass)
217
+
218
+ def intersect_keys(self, *others: Mapping[K, V]) -> Dict[K, V]:
219
+ """
220
+ Keep only keys present in self and all others mappings.
221
+
222
+ Args:
223
+ *others: Other mappings to intersect keys with.
224
+
225
+ ```python
226
+ >>> import pyochain as pc
227
+ >>> d1 = {"a": 1, "b": 2, "c": 3}
228
+ >>> d2 = {"b": 10, "c": 20}
229
+ >>> d3 = {"c": 30}
230
+ >>> pc.Dict(d1).intersect_keys(d2, d3).unwrap()
231
+ {'c': 3}
232
+
233
+ ```
234
+ """
235
+
236
+ def _intersect_keys(data: dict[K, V]) -> dict[K, V]:
237
+ self_keys = set(data.keys())
238
+ for other in others:
239
+ self_keys.intersection_update(other.keys())
240
+ return {k: data[k] for k in self_keys}
241
+
242
+ return self._new(_intersect_keys)
243
+
244
+ def diff_keys(self, *others: Mapping[K, V]) -> Dict[K, V]:
245
+ """
246
+ Keep only keys present in self but not in others mappings.
247
+
248
+ Args:
249
+ *others: Other mappings to exclude keys from.
250
+
251
+ ```python
252
+ >>> import pyochain as pc
253
+ >>> d1 = {"a": 1, "b": 2, "c": 3}
254
+ >>> d2 = {"b": 10, "d": 40}
255
+ >>> d3 = {"c": 30}
256
+ >>> pc.Dict(d1).diff_keys(d2, d3).unwrap()
257
+ {'a': 1}
258
+
259
+ ```
260
+ """
261
+
262
+ def _diff_keys(data: dict[K, V]) -> dict[K, V]:
263
+ self_keys = set(data.keys())
264
+ for other in others:
265
+ self_keys.difference_update(other.keys())
266
+ return {k: data[k] for k in self_keys}
267
+
268
+ return self._new(_diff_keys)