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/_dict/_main.py CHANGED
@@ -1,47 +1,51 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections import defaultdict
4
- from collections.abc import Callable, Mapping
5
- from functools import partial
6
- from typing import Any, Self
7
-
8
- import cytoolz as cz
3
+ from collections.abc import Iterable, Mapping
4
+ from typing import Any
9
5
 
10
6
  from .._core import SupportsKeysAndGetItem
11
- from ._exprs import IntoExpr, compute_exprs
12
7
  from ._filters import FilterDict
13
- from ._funcs import dict_repr
14
8
  from ._groups import GroupsDict
15
9
  from ._iter import IterDict
16
10
  from ._joins import JoinsDict
11
+ from ._maps import MapDict
17
12
  from ._nested import NestedDict
18
13
  from ._process import ProcessDict
19
14
 
20
15
 
21
- class Dict[K, V](
16
+ class DictCommonMethods[K, V](
22
17
  ProcessDict[K, V],
23
18
  IterDict[K, V],
24
19
  NestedDict[K, V],
20
+ MapDict[K, V],
25
21
  JoinsDict[K, V],
26
22
  FilterDict[K, V],
27
23
  GroupsDict[K, V],
28
24
  ):
25
+ pass
26
+
27
+
28
+ class Dict[K, V](DictCommonMethods[K, V]):
29
29
  """
30
30
  Wrapper for Python dictionaries with chainable methods.
31
31
  """
32
32
 
33
33
  __slots__ = ()
34
34
 
35
- def __repr__(self) -> str:
36
- return f"{self.__class__.__name__}({dict_repr(self.unwrap())})"
37
-
38
35
  @staticmethod
39
- def from_[G, I](data: Mapping[G, I] | SupportsKeysAndGetItem[G, I]) -> Dict[G, I]:
36
+ def from_[G, I](
37
+ data: Mapping[G, I] | Iterable[tuple[G, I]] | SupportsKeysAndGetItem[G, I],
38
+ ) -> Dict[G, I]:
40
39
  """
41
- Create a Dict from a mapping or SupportsKeysAndGetItem.
40
+ Create a Dict from a convertible value.
42
41
 
43
42
  Args:
44
- data: A mapping or object supporting keys and item access to convert into a Dict.
43
+ data: A mapping, Iterable of tuples, or object supporting keys and item access to convert into a Dict.
44
+
45
+ Returns:
46
+ A Dict instance containing the data from the input.
47
+
48
+ Example:
45
49
 
46
50
  ```python
47
51
  >>> import pyochain as pc
@@ -57,6 +61,8 @@ class Dict[K, V](
57
61
  >>>
58
62
  >>> pc.Dict.from_(MyMapping()).unwrap()
59
63
  {1: 'a', 2: 'b', 3: 'c'}
64
+ >>> pc.Dict.from_([("d", "e"), ("f", "g")]).unwrap()
65
+ {'d': 'e', 'f': 'g'}
60
66
 
61
67
  ```
62
68
  """
@@ -84,224 +90,24 @@ class Dict[K, V](
84
90
  """
85
91
  return Dict(obj.__dict__)
86
92
 
87
- def select(self: Dict[str, Any], *exprs: IntoExpr) -> Dict[str, Any]:
88
- """
89
- Select and alias fields from the dict based on expressions and/or strings.
90
-
91
- Navigate nested fields using the `pyochain.key` function.
92
-
93
- - Chain `key.key()` calls to access nested fields.
94
- - Use `key.apply()` to transform values.
95
- - Use `key.alias()` to rename fields in the resulting dict.
96
-
97
- Args:
98
- *exprs: Expressions or strings to select and alias fields from the dictionary.
99
-
100
- ```python
101
- >>> import pyochain as pc
102
- >>> data = {
103
- ... "name": "Alice",
104
- ... "age": 30,
105
- ... "scores": {"eng": [85, 90, 95], "math": [80, 88, 92]},
106
- ... }
107
- >>> scores_expr = pc.key("scores") # save an expression for reuse
108
- >>> pc.Dict(data).select(
109
- ... pc.key("name").alias("student_name"),
110
- ... "age", # shorthand for pc.key("age")
111
- ... scores_expr.key("math").alias("math_scores"),
112
- ... scores_expr.key("eng")
113
- ... .apply(lambda v: pc.Seq(v).mean())
114
- ... .alias("average_eng_score"),
115
- ... ).unwrap()
116
- {'student_name': 'Alice', 'age': 30, 'math_scores': [80, 88, 92], 'average_eng_score': 90}
117
-
118
- ```
119
- """
120
-
121
- def _select(data: dict[str, Any]) -> dict[str, Any]:
122
- return compute_exprs(exprs, data, {})
123
-
124
- return self.apply(_select)
125
-
126
- def with_fields(self: Dict[str, Any], *exprs: IntoExpr) -> Dict[str, Any]:
127
- """
128
- Merge aliased expressions into the root dict (overwrite on collision).
129
-
130
- Args:
131
- *exprs: Expressions to merge into the root dictionary.
132
-
133
- ```python
134
- >>> import pyochain as pc
135
- >>> data = {
136
- ... "name": "Alice",
137
- ... "age": 30,
138
- ... "scores": {"eng": [85, 90, 95], "math": [80, 88, 92]},
139
- ... }
140
- >>> scores_expr = pc.key("scores") # save an expression for reuse
141
- >>> pc.Dict(data).with_fields(
142
- ... scores_expr.key("eng")
143
- ... .apply(lambda v: pc.Seq(v).mean())
144
- ... .alias("average_eng_score"),
145
- ... ).unwrap()
146
- {'name': 'Alice', 'age': 30, 'scores': {'eng': [85, 90, 95], 'math': [80, 88, 92]}, 'average_eng_score': 90}
147
-
148
- ```
149
- """
150
-
151
- def _with_fields(data: dict[str, Any]) -> dict[str, Any]:
152
- return compute_exprs(exprs, data, data.copy())
153
-
154
- return self.apply(_with_fields)
155
-
156
- def map_keys[T](self, func: Callable[[K], T]) -> Dict[T, V]:
157
- """
158
- Return a Dict with keys transformed by func.
159
-
160
- Args:
161
- func: Function to apply to each key in the dictionary.
162
-
163
- ```python
164
- >>> import pyochain as pc
165
- >>> pc.Dict({"Alice": [20, 15, 30], "Bob": [10, 35]}).map_keys(
166
- ... str.lower
167
- ... ).unwrap()
168
- {'alice': [20, 15, 30], 'bob': [10, 35]}
169
- >>>
170
- >>> pc.Dict({1: "a"}).map_keys(str).unwrap()
171
- {'1': 'a'}
172
-
173
- ```
174
- """
175
- return self.apply(partial(cz.dicttoolz.keymap, func))
176
-
177
- def map_values[T](self, func: Callable[[V], T]) -> Dict[K, T]:
178
- """
179
- Return a Dict with values transformed by func.
180
-
181
- Args:
182
- func: Function to apply to each value in the dictionary.
183
-
184
- ```python
185
- >>> import pyochain as pc
186
- >>> pc.Dict({"Alice": [20, 15, 30], "Bob": [10, 35]}).map_values(sum).unwrap()
187
- {'Alice': 65, 'Bob': 45}
188
- >>>
189
- >>> pc.Dict({1: 1}).map_values(lambda v: v + 1).unwrap()
190
- {1: 2}
191
-
192
- ```
93
+ def pivot(self, *indices: int) -> Dict[Any, Any]:
193
94
  """
194
- return self.apply(partial(cz.dicttoolz.valmap, func))
95
+ Pivot a nested dictionary by rearranging the key levels according to order.
195
96
 
196
- def map_items[KR, VR](
197
- self,
198
- func: Callable[[tuple[K, V]], tuple[KR, VR]],
199
- ) -> Dict[KR, VR]:
200
- """
201
- Transform (key, value) pairs using a function that takes a (key, value) tuple.
97
+ Syntactic sugar for to_arrays().rearrange(*indices).to_records()
202
98
 
203
99
  Args:
204
- func: Function to transform each (key, value) pair into a new (key, value) tuple.
100
+ indices: Indices specifying the new order of key levels
205
101
 
206
- ```python
207
- >>> import pyochain as pc
208
- >>> pc.Dict({"Alice": 10, "Bob": 20}).map_items(
209
- ... lambda kv: (kv[0].upper(), kv[1] * 2)
210
- ... ).unwrap()
211
- {'ALICE': 20, 'BOB': 40}
212
-
213
- ```
214
- """
215
- return self.apply(partial(cz.dicttoolz.itemmap, func))
216
-
217
- def map_kv[KR, VR](
218
- self,
219
- func: Callable[[K, V], tuple[KR, VR]],
220
- ) -> Dict[KR, VR]:
221
- """
222
- Transform (key, value) pairs using a function that takes key and value as separate arguments.
223
-
224
- Args:
225
- func: Function to transform each key and value into a new (key, value) tuple.
226
-
227
- ```python
228
- >>> import pyochain as pc
229
- >>> pc.Dict({1: 2}).map_kv(lambda k, v: (k + 1, v * 10)).unwrap()
230
- {2: 20}
231
-
232
- ```
233
- """
234
-
235
- def _map_kv(data: dict[K, V]) -> dict[KR, VR]:
236
- def _(kv: tuple[K, V]) -> tuple[KR, VR]:
237
- return func(kv[0], kv[1])
238
-
239
- return cz.dicttoolz.itemmap(_, data)
240
-
241
- return self.apply(_map_kv)
242
-
243
- def invert(self) -> Dict[V, list[K]]:
244
- """
245
- Invert the dictionary, grouping keys by common (and hashable) values.
246
- ```python
247
- >>> import pyochain as pc
248
- >>> d = {"a": 1, "b": 2, "c": 1}
249
- >>> pc.Dict(d).invert().unwrap()
250
- {1: ['a', 'c'], 2: ['b']}
251
-
252
- ```
253
- """
254
-
255
- def _invert(data: dict[K, V]) -> dict[V, list[K]]:
256
- inverted: dict[V, list[K]] = defaultdict(list)
257
- for k, v in data.items():
258
- inverted[v].append(k)
259
- return dict(inverted)
260
-
261
- return self.apply(_invert)
262
-
263
- def implode(self) -> Dict[K, list[V]]:
264
- """
265
- Nest all the values in lists.
266
- syntactic sugar for map_values(lambda v: [v])
267
- ```python
268
- >>> import pyochain as pc
269
- >>> pc.Dict({1: 2, 3: 4}).implode().unwrap()
270
- {1: [2], 3: [4]}
271
-
272
- ```
273
- """
274
-
275
- def _implode(data: dict[K, V]) -> dict[K, list[V]]:
276
- def _(v: V) -> list[V]:
277
- return [v]
278
-
279
- return cz.dicttoolz.valmap(_, data)
280
-
281
- return self.apply(_implode)
282
-
283
- def equals_to(self, other: Self | Mapping[Any, Any]) -> bool:
284
- """
285
- Check if two records are equal based on their data.
286
-
287
- Args:
288
- other: Another Dict or mapping to compare against.
102
+ Returns:
103
+ Pivoted dictionary with keys rearranged
289
104
 
290
105
  Example:
291
106
  ```python
292
107
  >>> import pyochain as pc
293
- >>> d1 = pc.Dict({"a": 1, "b": 2})
294
- >>> d2 = pc.Dict({"a": 1, "b": 2})
295
- >>> d3 = pc.Dict({"a": 1, "b": 3})
296
- >>> d1.equals_to(d2)
297
- True
298
- >>> d1.equals_to(d3)
299
- False
300
-
301
- ```
108
+ >>> d = {"A": {"X": 1, "Y": 2}, "B": {"X": 3, "Y": 4}}
109
+ >>> pc.Dict(d).pivot(1, 0).unwrap()
110
+ {'X': {'A': 1, 'B': 3}, 'Y': {'A': 2, 'B': 4}}
302
111
  """
303
- return (
304
- self.unwrap() == other.unwrap()
305
- if isinstance(other, Dict)
306
- else self.unwrap() == other
307
- )
112
+
113
+ return self.to_arrays().rearrange(*indices).to_records()
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from collections.abc import Callable
5
+ from functools import partial
6
+ from typing import TYPE_CHECKING
7
+
8
+ import cytoolz as cz
9
+
10
+ from .._core import MappingWrapper
11
+
12
+ if TYPE_CHECKING:
13
+ from ._main import Dict
14
+
15
+
16
+ class MapDict[K, V](MappingWrapper[K, V]):
17
+ def map_keys[T](self, func: Callable[[K], T]) -> Dict[T, V]:
18
+ """
19
+ Return keys transformed by func.
20
+
21
+ Args:
22
+ func: Function to apply to each key in the dictionary.
23
+
24
+ ```python
25
+ >>> import pyochain as pc
26
+ >>> pc.Dict({"Alice": [20, 15, 30], "Bob": [10, 35]}).map_keys(
27
+ ... str.lower
28
+ ... ).unwrap()
29
+ {'alice': [20, 15, 30], 'bob': [10, 35]}
30
+ >>>
31
+ >>> pc.Dict({1: "a"}).map_keys(str).unwrap()
32
+ {'1': 'a'}
33
+
34
+ ```
35
+ """
36
+ return self._new(partial(cz.dicttoolz.keymap, func))
37
+
38
+ def map_values[T](self, func: Callable[[V], T]) -> Dict[K, T]:
39
+ """
40
+ Return values transformed by func.
41
+
42
+ Args:
43
+ func: Function to apply to each value in the dictionary.
44
+
45
+ ```python
46
+ >>> import pyochain as pc
47
+ >>> pc.Dict({"Alice": [20, 15, 30], "Bob": [10, 35]}).map_values(sum).unwrap()
48
+ {'Alice': 65, 'Bob': 45}
49
+ >>>
50
+ >>> pc.Dict({1: 1}).map_values(lambda v: v + 1).unwrap()
51
+ {1: 2}
52
+
53
+ ```
54
+ """
55
+ return self._new(partial(cz.dicttoolz.valmap, func))
56
+
57
+ def map_items[KR, VR](
58
+ self,
59
+ func: Callable[[tuple[K, V]], tuple[KR, VR]],
60
+ ) -> Dict[KR, VR]:
61
+ """
62
+ Transform (key, value) pairs using a function that takes a (key, value) tuple.
63
+
64
+ Args:
65
+ func: Function to transform each (key, value) pair into a new (key, value) tuple.
66
+
67
+ ```python
68
+ >>> import pyochain as pc
69
+ >>> pc.Dict({"Alice": 10, "Bob": 20}).map_items(
70
+ ... lambda kv: (kv[0].upper(), kv[1] * 2)
71
+ ... ).unwrap()
72
+ {'ALICE': 20, 'BOB': 40}
73
+
74
+ ```
75
+ """
76
+ return self._new(partial(cz.dicttoolz.itemmap, func))
77
+
78
+ def map_kv[KR, VR](
79
+ self,
80
+ func: Callable[[K, V], tuple[KR, VR]],
81
+ ) -> Dict[KR, VR]:
82
+ """
83
+ Transform (key, value) pairs using a function that takes key and value as separate arguments.
84
+
85
+ Args:
86
+ func: Function to transform each key and value into a new (key, value) tuple.
87
+
88
+ ```python
89
+ >>> import pyochain as pc
90
+ >>> pc.Dict({1: 2}).map_kv(lambda k, v: (k + 1, v * 10)).unwrap()
91
+ {2: 20}
92
+
93
+ ```
94
+ """
95
+
96
+ def _map_kv(data: dict[K, V]) -> dict[KR, VR]:
97
+ def _(kv: tuple[K, V]) -> tuple[KR, VR]:
98
+ return func(kv[0], kv[1])
99
+
100
+ return cz.dicttoolz.itemmap(_, data)
101
+
102
+ return self._new(_map_kv)
103
+
104
+ def invert(self) -> Dict[V, list[K]]:
105
+ """
106
+ Invert the dictionary, grouping keys by common (and hashable) values.
107
+ ```python
108
+ >>> import pyochain as pc
109
+ >>> d = {"a": 1, "b": 2, "c": 1}
110
+ >>> pc.Dict(d).invert().unwrap()
111
+ {1: ['a', 'c'], 2: ['b']}
112
+
113
+ ```
114
+ """
115
+
116
+ def _invert(data: dict[K, V]) -> dict[V, list[K]]:
117
+ inverted: dict[V, list[K]] = defaultdict(list)
118
+ for k, v in data.items():
119
+ inverted[v].append(k)
120
+ return dict(inverted)
121
+
122
+ return self._new(_invert)
123
+
124
+ def implode(self) -> Dict[K, list[V]]:
125
+ """
126
+ Nest all the values in lists.
127
+ syntactic sugar for map_values(lambda v: [v])
128
+ ```python
129
+ >>> import pyochain as pc
130
+ >>> pc.Dict({1: 2, 3: 4}).implode().unwrap()
131
+ {1: [2], 3: [4]}
132
+
133
+ ```
134
+ """
135
+
136
+ def _implode(data: dict[K, V]) -> dict[K, list[V]]:
137
+ def _(v: V) -> list[V]:
138
+ return [v]
139
+
140
+ return cz.dicttoolz.valmap(_, data)
141
+
142
+ return self._new(_implode)
pyochain/_dict/_nested.py CHANGED
@@ -2,14 +2,44 @@ 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, Concatenate
5
+ from typing import TYPE_CHECKING, Any, Concatenate, TypeIs
6
6
 
7
7
  import cytoolz as cz
8
8
 
9
9
  from .._core import MappingWrapper
10
10
 
11
11
  if TYPE_CHECKING:
12
- from .._dict import Dict
12
+ from ._main import Dict
13
+
14
+
15
+ def _drop_nones(
16
+ data: dict[Any, Any] | list[Any], remove_empty: bool = True
17
+ ) -> dict[Any, Any] | list[Any] | None:
18
+ match data:
19
+ case dict():
20
+ pruned_dict: dict[Any, Any] = {}
21
+ for k, v in data.items():
22
+ pruned_v = _drop_nones(v, remove_empty)
23
+
24
+ is_empty = remove_empty and (pruned_v is None or pruned_v in ({}, []))
25
+ if not is_empty:
26
+ pruned_dict[k] = pruned_v
27
+ return pruned_dict if pruned_dict or not remove_empty else None
28
+
29
+ case list():
30
+ pruned_list = [_drop_nones(item, remove_empty) for item in data]
31
+ if remove_empty:
32
+ pruned_list = [
33
+ item
34
+ for item in pruned_list
35
+ if not (item is None or item in ({}, []))
36
+ ]
37
+ return pruned_list if pruned_list or not remove_empty else None
38
+
39
+ case _:
40
+ if remove_empty and data is None:
41
+ return None
42
+ return data
13
43
 
14
44
 
15
45
  class NestedDict[K, V](MappingWrapper[K, V]):
@@ -36,10 +66,8 @@ class NestedDict[K, V](MappingWrapper[K, V]):
36
66
  ... }
37
67
  >>> pc.Dict(data).struct(lambda d: d.map_keys(str.upper).drop("AGE").unwrap())
38
68
  ... # doctest: +NORMALIZE_WHITESPACE
39
- Dict({
40
- 'person1': {'NAME': 'Alice', 'CITY': 'New York'},
41
- 'person2': {'NAME': 'Bob', 'CITY': 'Los Angeles'}
42
- })
69
+ {'person1': {'CITY': 'New York', 'NAME': 'Alice'},
70
+ 'person2': {'CITY': 'Los Angeles', 'NAME': 'Bob'}}
43
71
 
44
72
  ```
45
73
  """
@@ -51,7 +79,7 @@ class NestedDict[K, V](MappingWrapper[K, V]):
51
79
 
52
80
  return cz.dicttoolz.valmap(_, data)
53
81
 
54
- return self.apply(_struct)
82
+ return self._new(_struct)
55
83
 
56
84
  def flatten(
57
85
  self: NestedDict[str, Any], sep: str = ".", max_depth: int | None = None
@@ -79,22 +107,52 @@ class NestedDict[K, V](MappingWrapper[K, V]):
79
107
  """
80
108
 
81
109
  def _flatten(
82
- d: dict[Any, Any], parent_key: str = "", current_depth: int = 1
110
+ d: Mapping[Any, Any], parent_key: str = "", current_depth: int = 1
83
111
  ) -> dict[str, Any]:
112
+ def _can_recurse(v: object) -> TypeIs[Mapping[Any, Any]]:
113
+ return isinstance(v, Mapping) and (
114
+ max_depth is None or current_depth < max_depth + 1
115
+ )
116
+
84
117
  items: list[tuple[str, Any]] = []
85
118
  for k, v in d.items():
86
119
  new_key = parent_key + sep + k if parent_key else k
87
- if isinstance(v, dict) and (
88
- max_depth is None or current_depth < max_depth + 1
89
- ):
90
- items.extend(
91
- _flatten(v, new_key, current_depth + 1).items() # type: ignore
92
- )
120
+ if _can_recurse(v):
121
+ items.extend(_flatten(v, new_key, current_depth + 1).items())
93
122
  else:
94
- items.append((new_key, v)) # type: ignore
123
+ items.append((new_key, v))
95
124
  return dict(items)
96
125
 
97
- return self.apply(_flatten)
126
+ return self._new(_flatten)
127
+
128
+ def unpivot(
129
+ self: NestedDict[str, Mapping[str, Any]],
130
+ ) -> Dict[str, dict[str, Any]]:
131
+ """
132
+ Unpivot a nested dictionary by swapping rows and columns.
133
+
134
+ Example:
135
+ ```python
136
+ >>> import pyochain as pc
137
+ >>> data = {
138
+ ... "row1": {"col1": "A", "col2": "B"},
139
+ ... "row2": {"col1": "C", "col2": "D"},
140
+ ... }
141
+ >>> pc.Dict(data).unpivot()
142
+ ... # doctest: +NORMALIZE_WHITESPACE
143
+ {'col1': {'row1': 'A', 'row2': 'C'}, 'col2': {'row1': 'B', 'row2': 'D'}}
144
+ """
145
+
146
+ def _unpivot(
147
+ data: Mapping[str, Mapping[str, Any]],
148
+ ) -> dict[str, dict[str, Any]]:
149
+ out: dict[str, dict[str, Any]] = {}
150
+ for rkey, inner in data.items():
151
+ for ckey, val in inner.items():
152
+ out.setdefault(ckey, {})[rkey] = val
153
+ return out
154
+
155
+ return self._new(_unpivot)
98
156
 
99
157
  def with_nested_key(self, *keys: K, value: V) -> Dict[K, V]:
100
158
  """
@@ -117,56 +175,11 @@ class NestedDict[K, V](MappingWrapper[K, V]):
117
175
 
118
176
  ```
119
177
  """
120
- return self.apply(cz.dicttoolz.assoc_in, keys, value=value)
121
-
122
- def schema(self, max_depth: int = 1) -> Dict[str, Any]:
123
- """
124
- Return the schema of the dictionary up to a maximum depth.
125
178
 
126
- Args:
127
- max_depth: Maximum depth to inspect. Nested dicts beyond this depth are marked as 'dict'.
179
+ def _with_nested_key(data: dict[K, V]) -> dict[K, V]:
180
+ return cz.dicttoolz.assoc_in(data, keys, value=value)
128
181
 
129
- When the max depth is reached, nested dicts are marked as 'dict'.
130
- For lists, only the first element is inspected.
131
- ```python
132
- >>> import pyochain as pc
133
- >>> # Depth 2: we see up to level2
134
- >>> data = {
135
- ... "level1": {"level2": {"level3": {"key": "value"}}},
136
- ... "other_key": 123,
137
- ... "list_key": [{"sub_key": "sub_value"}],
138
- ... }
139
- >>> pc.Dict(data).schema(max_depth=1).unwrap()
140
- {'level1': 'dict', 'other_key': 'int', 'list_key': 'list'}
141
- >>> pc.Dict(data).schema(max_depth=2).unwrap()
142
- {'level1': {'level2': 'dict'}, 'other_key': 'int', 'list_key': 'dict'}
143
- >>>
144
- >>> # Depth 3: we see up to level3
145
- >>> pc.Dict(data).schema(max_depth=3).unwrap()
146
- {'level1': {'level2': {'level3': 'dict'}}, 'other_key': 'int', 'list_key': {'sub_key': 'str'}}
147
-
148
- ```
149
- """
150
-
151
- def _schema(data: dict[Any, Any]) -> Any:
152
- def _recurse_schema(node: Any, current_depth: int) -> Any:
153
- if isinstance(node, dict):
154
- if current_depth >= max_depth:
155
- return "dict"
156
- return {
157
- k: _recurse_schema(v, current_depth + 1)
158
- for k, v in node.items() # type: ignore
159
- }
160
- elif cz.itertoolz.isiterable(node):
161
- if current_depth >= max_depth:
162
- return type(node).__name__
163
- return _recurse_schema(cz.itertoolz.first(node), current_depth + 1)
164
- else:
165
- return type(node).__name__
166
-
167
- return _recurse_schema(data, 0)
168
-
169
- return self.apply(_schema)
182
+ return self._new(_with_nested_key)
170
183
 
171
184
  def pluck[U: str | int](self: NestedDict[U, Any], *keys: str) -> Dict[U, Any]:
172
185
  """
@@ -191,7 +204,7 @@ class NestedDict[K, V](MappingWrapper[K, V]):
191
204
  def _pluck(data: Mapping[U, Any]) -> dict[U, Any]:
192
205
  return cz.dicttoolz.valmap(getter, data)
193
206
 
194
- return self.apply(_pluck)
207
+ return self._new(_pluck)
195
208
 
196
209
  def get_in(self, *keys: K, default: Any = None) -> Any:
197
210
  """
@@ -216,3 +229,44 @@ class NestedDict[K, V](MappingWrapper[K, V]):
216
229
  return cz.dicttoolz.get_in(keys, data, default)
217
230
 
218
231
  return self.into(_get_in)
232
+
233
+ def drop_nones(self, remove_empty: bool = True) -> Dict[K, V]:
234
+ """
235
+ Recursively drop None values from the dictionary.
236
+
237
+ Options to also remove empty dicts and lists.
238
+
239
+ Args:
240
+ remove_empty: If True (default), removes `None`, `{}` and `[]`.
241
+
242
+ Example:
243
+ ```python
244
+ >>> import pyochain as pc
245
+ >>> data = {
246
+ ... "a": 1,
247
+ ... "b": None,
248
+ ... "c": {},
249
+ ... "d": [],
250
+ ... "e": {"f": None, "g": 2},
251
+ ... "h": [1, None, {}],
252
+ ... "i": 0,
253
+ ... }
254
+ >>> p_data = pc.Dict(data)
255
+ >>>
256
+ >>> p_data.drop_nones().unwrap()
257
+ {'a': 1, 'e': {'g': 2}, 'h': [1], 'i': 0}
258
+ >>>
259
+ >>> p_data.drop_nones().unwrap()
260
+ {'a': 1, 'e': {'g': 2}, 'h': [1], 'i': 0}
261
+ >>>
262
+ >>> p_data.drop_nones(remove_empty=False).unwrap()
263
+ {'a': 1, 'b': None, 'c': {}, 'd': [], 'e': {'f': None, 'g': 2}, 'h': [1, None, {}], 'i': 0}
264
+
265
+ ```
266
+ """
267
+
268
+ def _apply_drop_nones(data: dict[K, V]) -> dict[Any, Any]:
269
+ result = _drop_nones(data, remove_empty)
270
+ return result if isinstance(result, dict) else dict()
271
+
272
+ return self._new(_apply_drop_nones)