Skip to content

Commit 6e1833b

Browse files
committed
Release v0.8.0
1 parent 12263d3 commit 6e1833b

10 files changed

Lines changed: 239 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
v0.8.0 (2026-06-16)
2+
-------------------
3+
4+
**Enhancements**
5+
6+
* Add ``dict.get_path`` and ``dict.set_path`` helpers for nested mapping access.
7+
* Add ``dict.deep_merge`` for recursive mapping merges.
8+
* Align public documentation with the current Python 3.9+ support policy.
9+
10+
111
v0.7.4 (2026-06-15)
212
-------------------
313

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
[![pydu](https://img.shields.io/github/contributors/flaggo/pydu.svg)](https://github.com/flaggo/pydu/graphs/contributors)
1010

1111
**pydu** is a library of useful **d**ata structures and **u**tils
12-
for Python 2 and 3, which collected from open source projects and created by contributors.
12+
for Python 3.9+, which collected from open source projects and created by contributors.
1313

1414

1515
## Installation

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
## pydu
22

33
> **pydu(Python Data structures and Utils)** is a library of useful data structures and utils
4-
for Python 2 and 3, which collected from open source projects and created by contributors.
4+
for Python 3.9+, which collected from open source projects and created by contributors.
55

66

77
## Installation

docs/dict.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,58 @@ Return a new dict without selected keys from `mapping`. Missing keys are ignored
152152
>>> omit(data, 'private')
153153
{'name': 'pydu', 'version': '0.7.3'}
154154
```
155+
156+
157+
## dict.get_path
158+
```python
159+
get_path(mapping, path, default=None, separator='.')
160+
```
161+
162+
Return a nested value from `mapping`. If any path segment is missing, return
163+
`default`. `path` can be a dotted string or an iterable of path segments.
164+
165+
```python
166+
>>> from pydu.dict import get_path
167+
>>> data = {'user': {'profile': {'name': 'pydu'}}}
168+
>>> get_path(data, 'user.profile.name')
169+
'pydu'
170+
>>> get_path(data, ('user', 'profile', 'missing'), default='unknown')
171+
'unknown'
172+
```
173+
174+
175+
## dict.set_path
176+
```python
177+
set_path(mapping, path, value, separator='.')
178+
```
179+
180+
Set a nested value on `mapping`, creating intermediate dictionaries as needed.
181+
The original mapping is updated and returned.
182+
183+
```python
184+
>>> from pydu.dict import set_path
185+
>>> data = {}
186+
>>> set_path(data, 'user.profile.name', 'pydu')
187+
{'user': {'profile': {'name': 'pydu'}}}
188+
>>> data
189+
{'user': {'profile': {'name': 'pydu'}}}
190+
```
191+
192+
193+
## dict.deep_merge
194+
```python
195+
deep_merge(*mappings)
196+
```
197+
198+
Return a new dict by recursively merging mappings from left to right. Later
199+
values override earlier values. Nested mappings are merged; other values are
200+
replaced.
201+
202+
```python
203+
>>> from pydu.dict import deep_merge
204+
>>> deep_merge(
205+
... {'user': {'name': 'pydu', 'active': True}},
206+
... {'user': {'active': False, 'role': 'admin'}},
207+
... )
208+
{'user': {'name': 'pydu', 'active': False, 'role': 'admin'}}
209+
```

docs/zh-cn/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## pydu
22

3-
> **pydu(Python Data structures and Utils)** 是面向Python 2 和 3 的实用数据结构和工具库。
3+
> **pydu(Python Data structures and Utils)** 是面向 Python 3.9+ 的实用数据结构和工具库。
44
它收集自开源项目,也有来自开发者贡献。
55

66

docs/zh-cn/dict.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,56 @@ omit(mapping, keys)
144144
>>> omit(data, 'private')
145145
{'name': 'pydu', 'version': '0.7.3'}
146146
```
147+
148+
149+
## dict.get_path
150+
```python
151+
get_path(mapping, path, default=None, separator='.')
152+
```
153+
154+
`mapping` 中读取嵌套值。如果任意路径片段不存在,返回 `default`
155+
`path` 可以是点分隔字符串,也可以是路径片段的可迭代对象。
156+
157+
```python
158+
>>> from pydu.dict import get_path
159+
>>> data = {'user': {'profile': {'name': 'pydu'}}}
160+
>>> get_path(data, 'user.profile.name')
161+
'pydu'
162+
>>> get_path(data, ('user', 'profile', 'missing'), default='unknown')
163+
'unknown'
164+
```
165+
166+
167+
## dict.set_path
168+
```python
169+
set_path(mapping, path, value, separator='.')
170+
```
171+
172+
`mapping` 中设置嵌套值,并按需创建中间字典。原 mapping 会被更新并返回。
173+
174+
```python
175+
>>> from pydu.dict import set_path
176+
>>> data = {}
177+
>>> set_path(data, 'user.profile.name', 'pydu')
178+
{'user': {'profile': {'name': 'pydu'}}}
179+
>>> data
180+
{'user': {'profile': {'name': 'pydu'}}}
181+
```
182+
183+
184+
## dict.deep_merge
185+
```python
186+
deep_merge(*mappings)
187+
```
188+
189+
返回一个从左到右递归合并 mappings 后的新字典。后面的值覆盖前面的值。
190+
嵌套 mapping 会被递归合并,其他值会被替换。
191+
192+
```python
193+
>>> from pydu.dict import deep_merge
194+
>>> deep_merge(
195+
... {'user': {'name': 'pydu', 'active': True}},
196+
... {'user': {'active': False, 'role': 'admin'}},
197+
... )
198+
{'user': {'name': 'pydu', 'active': False, 'role': 'admin'}}
199+
```

pydu/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44
from __future__ import absolute_import
55

6-
__version__ = '0.7.4'
6+
__version__ = '0.8.0'
77

88

99
# Set logging handler to avoid "No handler found" warnings.

pydu/dict.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,20 @@ def _normalize_keys(keys):
226226
return keys
227227

228228

229+
def _normalize_path(path, separator='.'):
230+
if isinstance(path, string_types):
231+
if path == '':
232+
return tuple()
233+
if separator is None:
234+
return (path,)
235+
return tuple(path.split(separator))
236+
try:
237+
iter(path)
238+
except TypeError:
239+
return (path,)
240+
return tuple(path)
241+
242+
229243
def pick(mapping, keys):
230244
"""
231245
Return a new dict with selected keys from mapping.
@@ -243,3 +257,54 @@ def omit(mapping, keys):
243257
keys = set(_normalize_keys(keys))
244258
return dict((key, value) for key, value in mapping.items()
245259
if key not in keys)
260+
261+
262+
def get_path(mapping, path, default=None, separator='.'):
263+
"""
264+
Return the nested value at path, or default when any segment is missing.
265+
"""
266+
value = mapping
267+
for key in _normalize_path(path, separator=separator):
268+
try:
269+
value = value[key]
270+
except (KeyError, IndexError, TypeError):
271+
return default
272+
return value
273+
274+
275+
def set_path(mapping, path, value, separator='.'):
276+
"""
277+
Set a nested value at path, creating dictionaries as needed.
278+
"""
279+
keys = _normalize_path(path, separator=separator)
280+
if not keys:
281+
raise ValueError('path must not be empty')
282+
283+
current = mapping
284+
for key in keys[:-1]:
285+
try:
286+
next_value = current[key]
287+
except (KeyError, TypeError):
288+
next_value = {}
289+
current[key] = next_value
290+
if not isinstance(next_value, MutableMapping):
291+
next_value = {}
292+
current[key] = next_value
293+
current = next_value
294+
current[keys[-1]] = value
295+
return mapping
296+
297+
298+
def deep_merge(*mappings):
299+
"""
300+
Return a new dict by recursively merging mappings from left to right.
301+
"""
302+
result = {}
303+
for mapping in mappings:
304+
for key, value in mapping.items():
305+
existing = result.get(key)
306+
if isinstance(existing, Mapping) and isinstance(value, Mapping):
307+
result[key] = deep_merge(existing, value)
308+
else:
309+
result[key] = value
310+
return result

stubs/pydu/dict.pyi

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import collections
1+
from collections.abc import MutableMapping
22
from typing import Iterable, Tuple, Any, Mapping, Dict, Union
33

44

5-
class CaseInsensitiveDict(collections.MutableMapping):
5+
class CaseInsensitiveDict(MutableMapping):
66
_store = ... # type: dict
77
def __init__(self, data: dict=None, **kwargs) -> None: ...
88
def lower_items(self) -> Iterable[Tuple[str, Any]]: ...
99

1010
def pick(mapping: Mapping[Any, Any], keys: Union[Any, Iterable[Any]]) -> Dict[Any, Any]: ...
1111
def omit(mapping: Mapping[Any, Any], keys: Union[Any, Iterable[Any]]) -> Dict[Any, Any]: ...
12+
def get_path(mapping: Any, path: Union[Any, Iterable[Any]], default: Any=None, separator: str='.') -> Any: ...
13+
def set_path(mapping: MutableMapping[Any, Any], path: Union[Any, Iterable[Any]], value: Any, separator: str='.') -> MutableMapping[Any, Any]: ...
14+
def deep_merge(*mappings: Mapping[Any, Any]) -> Dict[Any, Any]: ...

tests/test_dict.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import unittest
33

44
from pydu.dict import (AttrDict, LookupDict, CaseInsensitiveDict,
5-
OrderedDefaultDict, attrify, pick, omit)
5+
OrderedDefaultDict, attrify, pick, omit,
6+
get_path, set_path, deep_merge)
67

78

89
class TestAttrDict:
@@ -151,3 +152,48 @@ def test_omit_accepts_single_string_key():
151152
data = {'name': 'pydu', 'private': True}
152153

153154
assert omit(data, 'private') == {'name': 'pydu'}
155+
156+
157+
def test_get_path_reads_dotted_and_iterable_paths():
158+
data = {'user': {'profile': {'name': 'pydu'}}}
159+
160+
assert get_path(data, 'user.profile.name') == 'pydu'
161+
assert get_path(data, ('user', 'profile', 'name')) == 'pydu'
162+
163+
164+
def test_get_path_returns_default_when_missing():
165+
data = {'user': {'profile': {}}}
166+
167+
assert get_path(data, 'user.profile.name', default='unknown') == 'unknown'
168+
assert get_path(data, 'user.profile.name') is None
169+
170+
171+
def test_set_path_creates_nested_dicts_and_returns_mapping():
172+
data = {}
173+
174+
result = set_path(data, 'user.profile.name', 'pydu')
175+
176+
assert result is data
177+
assert data == {'user': {'profile': {'name': 'pydu'}}}
178+
179+
180+
def test_set_path_replaces_non_mapping_intermediate_values():
181+
data = {'user': 'legacy'}
182+
183+
set_path(data, ('user', 'profile', 'name'), 'pydu')
184+
185+
assert data == {'user': {'profile': {'name': 'pydu'}}}
186+
187+
188+
def test_deep_merge_recursively_combines_mappings_without_mutating_inputs():
189+
base = {'user': {'name': 'pydu', 'active': True}, 'tags': ['old']}
190+
override = {'user': {'active': False, 'role': 'admin'}, 'tags': ['new']}
191+
192+
result = deep_merge(base, override)
193+
194+
assert result == {
195+
'user': {'name': 'pydu', 'active': False, 'role': 'admin'},
196+
'tags': ['new'],
197+
}
198+
assert base == {'user': {'name': 'pydu', 'active': True}, 'tags': ['old']}
199+
assert override == {'user': {'active': False, 'role': 'admin'}, 'tags': ['new']}

0 commit comments

Comments
 (0)