python-benedict 0.34.1__tar.gz → 0.36.0__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.
Files changed (91) hide show
  1. {python_benedict-0.34.1/python_benedict.egg-info → python_benedict-0.36.0}/PKG-INFO +50 -7
  2. {python_benedict-0.34.1 → python_benedict-0.36.0}/README.md +40 -2
  3. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/clean.py +28 -10
  4. python_benedict-0.36.0/benedict/core/clone.py +18 -0
  5. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/dump.py +3 -1
  6. python_benedict-0.36.0/benedict/core/filter.py +21 -0
  7. python_benedict-0.36.0/benedict/core/find.py +16 -0
  8. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/flatten.py +13 -6
  9. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/groupby.py +10 -2
  10. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/invert.py +12 -4
  11. python_benedict-0.36.0/benedict/core/items_sorted.py +25 -0
  12. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/keylists.py +17 -4
  13. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/keypaths.py +11 -1
  14. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/match.py +10 -1
  15. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/merge.py +24 -5
  16. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/move.py +9 -1
  17. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/nest.py +17 -2
  18. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/remove.py +4 -1
  19. python_benedict-0.36.0/benedict/core/rename.py +10 -0
  20. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/search.py +24 -5
  21. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/standardize.py +5 -3
  22. python_benedict-0.36.0/benedict/core/subset.py +24 -0
  23. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/swap.py +7 -1
  24. python_benedict-0.36.0/benedict/core/traverse.py +46 -0
  25. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/unflatten.py +8 -3
  26. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/unique.py +5 -1
  27. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/dicts/__init__.py +112 -61
  28. python_benedict-0.36.0/benedict/dicts/base/base_dict.py +217 -0
  29. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/dicts/io/io_dict.py +75 -47
  30. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/dicts/io/io_util.py +44 -23
  31. python_benedict-0.36.0/benedict/dicts/keyattr/keyattr_dict.py +90 -0
  32. python_benedict-0.36.0/benedict/dicts/keylist/keylist_dict.py +116 -0
  33. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/dicts/keylist/keylist_util.py +21 -9
  34. python_benedict-0.36.0/benedict/dicts/keypath/keypath_dict.py +77 -0
  35. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/dicts/keypath/keypath_util.py +12 -8
  36. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/dicts/parse/parse_dict.py +200 -60
  37. python_benedict-0.36.0/benedict/dicts/parse/parse_util.py +344 -0
  38. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/exceptions.py +1 -1
  39. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/extras.py +8 -8
  40. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/metadata.py +1 -1
  41. python_benedict-0.36.0/benedict/py.typed +0 -0
  42. python_benedict-0.36.0/benedict/serializers/__init__.py +194 -0
  43. python_benedict-0.36.0/benedict/serializers/abstract.py +27 -0
  44. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/base64.py +23 -13
  45. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/cli.py +13 -8
  46. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/csv.py +8 -5
  47. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/html.py +8 -4
  48. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/ini.py +20 -9
  49. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/json.py +10 -7
  50. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/pickle.py +10 -4
  51. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/plist.py +5 -4
  52. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/query_string.py +20 -6
  53. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/toml.py +6 -4
  54. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/xls.py +32 -13
  55. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/xml.py +9 -5
  56. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/serializers/yaml.py +9 -7
  57. python_benedict-0.36.0/benedict/utils/type_util.py +109 -0
  58. {python_benedict-0.34.1 → python_benedict-0.36.0}/pyproject.toml +31 -4
  59. {python_benedict-0.34.1 → python_benedict-0.36.0/python_benedict.egg-info}/PKG-INFO +50 -7
  60. {python_benedict-0.34.1 → python_benedict-0.36.0}/python_benedict.egg-info/SOURCES.txt +1 -0
  61. {python_benedict-0.34.1 → python_benedict-0.36.0}/python_benedict.egg-info/requires.txt +5 -3
  62. python_benedict-0.34.1/benedict/core/clone.py +0 -8
  63. python_benedict-0.34.1/benedict/core/filter.py +0 -13
  64. python_benedict-0.34.1/benedict/core/find.py +0 -5
  65. python_benedict-0.34.1/benedict/core/items_sorted.py +0 -10
  66. python_benedict-0.34.1/benedict/core/rename.py +0 -5
  67. python_benedict-0.34.1/benedict/core/subset.py +0 -12
  68. python_benedict-0.34.1/benedict/core/traverse.py +0 -29
  69. python_benedict-0.34.1/benedict/dicts/base/base_dict.py +0 -163
  70. python_benedict-0.34.1/benedict/dicts/keyattr/keyattr_dict.py +0 -62
  71. python_benedict-0.34.1/benedict/dicts/keylist/keylist_dict.py +0 -95
  72. python_benedict-0.34.1/benedict/dicts/keypath/keypath_dict.py +0 -65
  73. python_benedict-0.34.1/benedict/dicts/parse/parse_util.py +0 -231
  74. python_benedict-0.34.1/benedict/serializers/__init__.py +0 -88
  75. python_benedict-0.34.1/benedict/serializers/abstract.py +0 -17
  76. python_benedict-0.34.1/benedict/utils/type_util.py +0 -95
  77. {python_benedict-0.34.1 → python_benedict-0.36.0}/LICENSE.txt +0 -0
  78. {python_benedict-0.34.1 → python_benedict-0.36.0}/MANIFEST.in +0 -0
  79. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/__init__.py +0 -0
  80. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/core/__init__.py +0 -0
  81. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/dicts/base/__init__.py +0 -0
  82. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/dicts/io/__init__.py +0 -0
  83. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/dicts/keyattr/__init__.py +0 -0
  84. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/dicts/keylist/__init__.py +0 -0
  85. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/dicts/keypath/__init__.py +0 -0
  86. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/dicts/parse/__init__.py +0 -0
  87. {python_benedict-0.34.1 → python_benedict-0.36.0}/benedict/utils/__init__.py +0 -0
  88. {python_benedict-0.34.1 → python_benedict-0.36.0}/python_benedict.egg-info/dependency_links.txt +0 -0
  89. {python_benedict-0.34.1 → python_benedict-0.36.0}/python_benedict.egg-info/top_level.txt +0 -0
  90. {python_benedict-0.34.1 → python_benedict-0.36.0}/setup.cfg +0 -0
  91. {python_benedict-0.34.1 → python_benedict-0.36.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: python-benedict
3
- Version: 0.34.1
3
+ Version: 0.36.0
4
4
  Summary: python-benedict is a dict subclass with keylist/keypath/keyattr support, normalized I/O operations (base64, csv, ini, json, pickle, plist, query-string, toml, xls, xml, yaml) and many utilities... for humans, obviously.
5
5
  Author-email: Fabio Caccamo <fabio.caccamo@gmail.com>
6
6
  Maintainer-email: Fabio Caccamo <fabio.caccamo@gmail.com>
@@ -51,16 +51,20 @@ Classifier: Programming Language :: Python :: 3.10
51
51
  Classifier: Programming Language :: Python :: 3.11
52
52
  Classifier: Programming Language :: Python :: 3.12
53
53
  Classifier: Programming Language :: Python :: 3.13
54
+ Classifier: Programming Language :: Python :: 3.14
54
55
  Classifier: Topic :: Education :: Testing
55
56
  Classifier: Topic :: Software Development :: Build Tools
56
57
  Classifier: Topic :: System :: Filesystems
57
58
  Classifier: Topic :: Text Processing :: Markup :: XML
58
59
  Classifier: Topic :: Utilities
60
+ Classifier: Typing :: Typed
59
61
  Description-Content-Type: text/markdown
60
62
  License-File: LICENSE.txt
61
- Requires-Dist: python-fsutil<1.0.0,>=0.9.3
63
+ Requires-Dist: python-fsutil<1.0.0,>=0.16.0
62
64
  Requires-Dist: python-slugify<9.0.0,>=7.0.0
63
65
  Requires-Dist: requests<3.0.0,>=2.26.0
66
+ Requires-Dist: typing_extensions<4.16.0,>=4.13.2
67
+ Requires-Dist: useful-types<0.3.0,>=0.2.1
64
68
  Provides-Extra: all
65
69
  Requires-Dist: python-benedict[io,parse,s3]; extra == "all"
66
70
  Provides-Extra: html
@@ -71,7 +75,7 @@ Requires-Dist: python-benedict[html,toml,xls,xml,yaml]; extra == "io"
71
75
  Provides-Extra: parse
72
76
  Requires-Dist: ftfy<7.0.0,>=6.0.0; extra == "parse"
73
77
  Requires-Dist: mailchecker<7.0.0,>=4.1.0; extra == "parse"
74
- Requires-Dist: phonenumbers<9.0.0,>=8.12.0; extra == "parse"
78
+ Requires-Dist: phonenumbers<10.0.0,>=8.12.0; extra == "parse"
75
79
  Requires-Dist: python-dateutil<3.0.0,>=2.8.0; extra == "parse"
76
80
  Provides-Extra: s3
77
81
  Requires-Dist: boto3<2.0.0,>=1.24.89; extra == "s3"
@@ -81,24 +85,26 @@ Provides-Extra: xls
81
85
  Requires-Dist: openpyxl<4.0.0,>=3.0.0; extra == "xls"
82
86
  Requires-Dist: xlrd<3.0.0,>=2.0.0; extra == "xls"
83
87
  Provides-Extra: xml
84
- Requires-Dist: xmltodict<1.0.0,>=0.12.0; extra == "xml"
88
+ Requires-Dist: xmltodict<2.0.0,>=0.12.0; extra == "xml"
85
89
  Provides-Extra: yaml
86
90
  Requires-Dist: pyyaml<7.0,>=6.0; extra == "yaml"
91
+ Dynamic: license-file
87
92
 
88
93
  [![](https://img.shields.io/pypi/pyversions/python-benedict.svg?color=blue&logo=python&logoColor=white)](https://www.python.org/)
89
94
  [![](https://img.shields.io/pypi/v/python-benedict.svg?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/python-benedict/)
90
95
  [![](https://static.pepy.tech/badge/python-benedict/month)](https://pepy.tech/project/python-benedict)
91
96
  [![](https://img.shields.io/github/stars/fabiocaccamo/python-benedict?logo=github&style=flat)](https://github.com/fabiocaccamo/python-benedict/stargazers)
92
97
  [![](https://img.shields.io/pypi/l/python-benedict.svg?color=blue)](https://github.com/fabiocaccamo/python-benedict/blob/main/LICENSE.txt)
98
+ [![](https://img.shields.io/badge/SBOM-CycloneDX-blue?logo=dependabot)](https://github.com/fabiocaccamo/python-benedict/releases/latest)
93
99
 
94
100
  [![](https://results.pre-commit.ci/badge/github/fabiocaccamo/python-benedict/main.svg)](https://results.pre-commit.ci/latest/github/fabiocaccamo/python-benedict/main)
95
101
  [![](https://img.shields.io/github/actions/workflow/status/fabiocaccamo/python-benedict/test-package.yml?branch=main&label=build&logo=github)](https://github.com/fabiocaccamo/python-benedict)
96
102
  [![](https://img.shields.io/codecov/c/gh/fabiocaccamo/python-benedict?logo=codecov)](https://codecov.io/gh/fabiocaccamo/python-benedict)
97
- [![](https://img.shields.io/codeclimate/maintainability/fabiocaccamo/python-benedict?logo=code-climate)](https://codeclimate.com/github/fabiocaccamo/python-benedict/)
98
103
  [![](https://img.shields.io/codacy/grade/0dbd5cc2089f4dce80a0e49e6822be3c?logo=codacy)](https://www.codacy.com/app/fabiocaccamo/python-benedict)
99
104
  [![](https://img.shields.io/scrutinizer/quality/g/fabiocaccamo/python-benedict?logo=scrutinizer)](https://scrutinizer-ci.com/g/fabiocaccamo/python-benedict/?branch=main)
100
105
  [![](https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=black)](https://github.com/psf/black)
101
106
  [![](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
107
+ [![](https://api.securityscorecards.dev/projects/github.com/fabiocaccamo/python-benedict/badge)](https://scorecard.dev/viewer/?uri=github.com/fabiocaccamo/python-benedict)
102
108
 
103
109
  # python-benedict
104
110
  python-benedict is a dict subclass with **keylist/keypath/keyattr** support, **I/O** shortcuts (`base64`, `cli`, `csv`, `html`, `ini`, `json`, `pickle`, `plist`, `query-string`, `toml`, `xls`, `xml`, `yaml`) and many **utilities**... for humans, obviously.
@@ -132,6 +138,7 @@ python-benedict is a dict subclass with **keylist/keypath/keyattr** support, **I
132
138
  - [I/O methods](#io-methods)
133
139
  - [Parse methods](#parse-methods)
134
140
  - [Testing](#testing)
141
+ - [Security](#security)
135
142
  - [License](#license)
136
143
 
137
144
  ## Installation
@@ -216,6 +223,9 @@ d.keyattr_dynamic = True
216
223
 
217
224
  > **Warning** - even if this feature is very useful, it has some obvious limitations: it works only for string keys that are *unprotected* (not starting with an `_`) and that don't clash with the currently supported methods names.
218
225
 
226
+ #### Tab-completion support
227
+ `benedict` supports tab-completion in **VSCode**, **IPython**, and **Jupyter** for both attribute-style access (`d.<TAB>`) and subscript-style access (`d["<TAB>"]`).
228
+
219
229
  ### Keylist
220
230
  Wherever a **key** is used, it is possible to use also a **list of keys**.
221
231
 
@@ -376,6 +386,8 @@ Here are the details of the supported formats, operations and extra options docs
376
386
  - [`filter`](#filter)
377
387
  - [`find`](#find)
378
388
  - [`flatten`](#flatten)
389
+ - [`freeze`](#freeze)
390
+ - [`frozen`](#frozen)
379
391
  - [`groupby`](#groupby)
380
392
  - [`invert`](#invert)
381
393
  - [`items_sorted_by_keys`](#items_sorted_by_keys)
@@ -393,6 +405,7 @@ Here are the details of the supported formats, operations and extra options docs
393
405
  - [`swap`](#swap)
394
406
  - [`traverse`](#traverse)
395
407
  - [`unflatten`](#unflatten)
408
+ - [`unfreeze`](#unfreeze)
396
409
  - [`unique`](#unique)
397
410
 
398
411
  - **I/O methods**
@@ -508,6 +521,22 @@ f = d.find(keys, default=0)
508
521
  f = d.flatten(separator="_")
509
522
  ```
510
523
 
524
+ #### `freeze`
525
+
526
+ ```python
527
+ # Make the dict immutable: any attempt to modify it will raise a TypeError.
528
+ # Only top-level keys are frozen; nested dicts are not affected.
529
+ d.freeze()
530
+ ```
531
+
532
+ #### `frozen`
533
+
534
+ ```python
535
+ # Return True if the dict is frozen (immutable), False otherwise.
536
+ if d.frozen:
537
+ ...
538
+ ```
539
+
511
540
  #### `groupby`
512
541
 
513
542
  ```python
@@ -646,6 +675,13 @@ d.traverse(f)
646
675
  u = d.unflatten(separator="_")
647
676
  ```
648
677
 
678
+ #### `unfreeze`
679
+
680
+ ```python
681
+ # Make the dict mutable again after a freeze() call.
682
+ d.unfreeze()
683
+ ```
684
+
649
685
  #### `unique`
650
686
 
651
687
  ```python
@@ -1118,6 +1154,13 @@ tox
1118
1154
  python -m unittest
1119
1155
  ```
1120
1156
 
1157
+ ## Security
1158
+
1159
+ - **SBOM** — a Software Bill of Materials in [CycloneDX](https://cyclonedx.org/) format (JSON and XML) is generated and published as a release asset on every release. You can download it from the [Releases](https://github.com/fabiocaccamo/python-benedict/releases/latest) page.
1160
+ - **Trusted Publishing** — packages are published to PyPI via [OIDC Trusted Publishing](https://docs.pypi.org/trusted-publishers/), without long-lived secrets.
1161
+ - **OpenSSF Scorecard** — the repository is evaluated weekly against the [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/fabiocaccamo/python-benedict) checks; results are visible in the GitHub Security tab.
1162
+ - **Reporting** — to report a vulnerability, please follow the [Security Policy](SECURITY.md).
1163
+
1121
1164
  ## License
1122
1165
  Released under [MIT License](LICENSE.txt).
1123
1166
 
@@ -1127,7 +1170,7 @@ Released under [MIT License](LICENSE.txt).
1127
1170
 
1128
1171
  - :star: Star this project on [GitHub](https://github.com/fabiocaccamo/python-benedict)
1129
1172
  - :octocat: Follow me on [GitHub](https://github.com/fabiocaccamo)
1130
- - :blue_heart: Follow me on [Twitter](https://twitter.com/fabiocaccamo)
1173
+ - :blue_heart: Follow me on [Bluesky](https://bsky.app/profile/fabiocaccamo.bsky.social)
1131
1174
  - :moneybag: Sponsor me on [Github](https://github.com/sponsors/fabiocaccamo)
1132
1175
 
1133
1176
  ## See also
@@ -3,15 +3,16 @@
3
3
  [![](https://static.pepy.tech/badge/python-benedict/month)](https://pepy.tech/project/python-benedict)
4
4
  [![](https://img.shields.io/github/stars/fabiocaccamo/python-benedict?logo=github&style=flat)](https://github.com/fabiocaccamo/python-benedict/stargazers)
5
5
  [![](https://img.shields.io/pypi/l/python-benedict.svg?color=blue)](https://github.com/fabiocaccamo/python-benedict/blob/main/LICENSE.txt)
6
+ [![](https://img.shields.io/badge/SBOM-CycloneDX-blue?logo=dependabot)](https://github.com/fabiocaccamo/python-benedict/releases/latest)
6
7
 
7
8
  [![](https://results.pre-commit.ci/badge/github/fabiocaccamo/python-benedict/main.svg)](https://results.pre-commit.ci/latest/github/fabiocaccamo/python-benedict/main)
8
9
  [![](https://img.shields.io/github/actions/workflow/status/fabiocaccamo/python-benedict/test-package.yml?branch=main&label=build&logo=github)](https://github.com/fabiocaccamo/python-benedict)
9
10
  [![](https://img.shields.io/codecov/c/gh/fabiocaccamo/python-benedict?logo=codecov)](https://codecov.io/gh/fabiocaccamo/python-benedict)
10
- [![](https://img.shields.io/codeclimate/maintainability/fabiocaccamo/python-benedict?logo=code-climate)](https://codeclimate.com/github/fabiocaccamo/python-benedict/)
11
11
  [![](https://img.shields.io/codacy/grade/0dbd5cc2089f4dce80a0e49e6822be3c?logo=codacy)](https://www.codacy.com/app/fabiocaccamo/python-benedict)
12
12
  [![](https://img.shields.io/scrutinizer/quality/g/fabiocaccamo/python-benedict?logo=scrutinizer)](https://scrutinizer-ci.com/g/fabiocaccamo/python-benedict/?branch=main)
13
13
  [![](https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=black)](https://github.com/psf/black)
14
14
  [![](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
15
+ [![](https://api.securityscorecards.dev/projects/github.com/fabiocaccamo/python-benedict/badge)](https://scorecard.dev/viewer/?uri=github.com/fabiocaccamo/python-benedict)
15
16
 
16
17
  # python-benedict
17
18
  python-benedict is a dict subclass with **keylist/keypath/keyattr** support, **I/O** shortcuts (`base64`, `cli`, `csv`, `html`, `ini`, `json`, `pickle`, `plist`, `query-string`, `toml`, `xls`, `xml`, `yaml`) and many **utilities**... for humans, obviously.
@@ -45,6 +46,7 @@ python-benedict is a dict subclass with **keylist/keypath/keyattr** support, **I
45
46
  - [I/O methods](#io-methods)
46
47
  - [Parse methods](#parse-methods)
47
48
  - [Testing](#testing)
49
+ - [Security](#security)
48
50
  - [License](#license)
49
51
 
50
52
  ## Installation
@@ -129,6 +131,9 @@ d.keyattr_dynamic = True
129
131
 
130
132
  > **Warning** - even if this feature is very useful, it has some obvious limitations: it works only for string keys that are *unprotected* (not starting with an `_`) and that don't clash with the currently supported methods names.
131
133
 
134
+ #### Tab-completion support
135
+ `benedict` supports tab-completion in **VSCode**, **IPython**, and **Jupyter** for both attribute-style access (`d.<TAB>`) and subscript-style access (`d["<TAB>"]`).
136
+
132
137
  ### Keylist
133
138
  Wherever a **key** is used, it is possible to use also a **list of keys**.
134
139
 
@@ -289,6 +294,8 @@ Here are the details of the supported formats, operations and extra options docs
289
294
  - [`filter`](#filter)
290
295
  - [`find`](#find)
291
296
  - [`flatten`](#flatten)
297
+ - [`freeze`](#freeze)
298
+ - [`frozen`](#frozen)
292
299
  - [`groupby`](#groupby)
293
300
  - [`invert`](#invert)
294
301
  - [`items_sorted_by_keys`](#items_sorted_by_keys)
@@ -306,6 +313,7 @@ Here are the details of the supported formats, operations and extra options docs
306
313
  - [`swap`](#swap)
307
314
  - [`traverse`](#traverse)
308
315
  - [`unflatten`](#unflatten)
316
+ - [`unfreeze`](#unfreeze)
309
317
  - [`unique`](#unique)
310
318
 
311
319
  - **I/O methods**
@@ -421,6 +429,22 @@ f = d.find(keys, default=0)
421
429
  f = d.flatten(separator="_")
422
430
  ```
423
431
 
432
+ #### `freeze`
433
+
434
+ ```python
435
+ # Make the dict immutable: any attempt to modify it will raise a TypeError.
436
+ # Only top-level keys are frozen; nested dicts are not affected.
437
+ d.freeze()
438
+ ```
439
+
440
+ #### `frozen`
441
+
442
+ ```python
443
+ # Return True if the dict is frozen (immutable), False otherwise.
444
+ if d.frozen:
445
+ ...
446
+ ```
447
+
424
448
  #### `groupby`
425
449
 
426
450
  ```python
@@ -559,6 +583,13 @@ d.traverse(f)
559
583
  u = d.unflatten(separator="_")
560
584
  ```
561
585
 
586
+ #### `unfreeze`
587
+
588
+ ```python
589
+ # Make the dict mutable again after a freeze() call.
590
+ d.unfreeze()
591
+ ```
592
+
562
593
  #### `unique`
563
594
 
564
595
  ```python
@@ -1031,6 +1062,13 @@ tox
1031
1062
  python -m unittest
1032
1063
  ```
1033
1064
 
1065
+ ## Security
1066
+
1067
+ - **SBOM** — a Software Bill of Materials in [CycloneDX](https://cyclonedx.org/) format (JSON and XML) is generated and published as a release asset on every release. You can download it from the [Releases](https://github.com/fabiocaccamo/python-benedict/releases/latest) page.
1068
+ - **Trusted Publishing** — packages are published to PyPI via [OIDC Trusted Publishing](https://docs.pypi.org/trusted-publishers/), without long-lived secrets.
1069
+ - **OpenSSF Scorecard** — the repository is evaluated weekly against the [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/fabiocaccamo/python-benedict) checks; results are visible in the GitHub Security tab.
1070
+ - **Reporting** — to report a vulnerability, please follow the [Security Policy](SECURITY.md).
1071
+
1034
1072
  ## License
1035
1073
  Released under [MIT License](LICENSE.txt).
1036
1074
 
@@ -1040,7 +1078,7 @@ Released under [MIT License](LICENSE.txt).
1040
1078
 
1041
1079
  - :star: Star this project on [GitHub](https://github.com/fabiocaccamo/python-benedict)
1042
1080
  - :octocat: Follow me on [GitHub](https://github.com/fabiocaccamo)
1043
- - :blue_heart: Follow me on [Twitter](https://twitter.com/fabiocaccamo)
1081
+ - :blue_heart: Follow me on [Bluesky](https://bsky.app/profile/fabiocaccamo.bsky.social)
1044
1082
  - :moneybag: Sponsor me on [Github](https://github.com/sponsors/fabiocaccamo)
1045
1083
 
1046
1084
  ## See also
@@ -1,4 +1,16 @@
1
- def _clean_dict(d, strings, collections):
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import MutableMapping, MutableSequence
4
+ from typing import Any, TypeVar
5
+
6
+ _K = TypeVar("_K")
7
+ _V = TypeVar("_V")
8
+ _T = TypeVar("_T")
9
+
10
+
11
+ def _clean_dict(
12
+ d: MutableMapping[_K, _V], strings: bool, collections: bool
13
+ ) -> MutableMapping[_K, _V]:
2
14
  keys = list(d.keys())
3
15
  for key in keys:
4
16
  d[key] = _clean_value(d[key], strings=strings, collections=collections)
@@ -7,7 +19,9 @@ def _clean_dict(d, strings, collections):
7
19
  return d
8
20
 
9
21
 
10
- def _clean_list(ls, strings, collections):
22
+ def _clean_list(
23
+ ls: MutableSequence[_T], strings: bool, collections: bool
24
+ ) -> MutableSequence[_T]:
11
25
  for i in range(len(ls) - 1, -1, -1):
12
26
  ls[i] = _clean_value(ls[i], strings=strings, collections=collections)
13
27
  if ls[i] is None:
@@ -15,7 +29,7 @@ def _clean_list(ls, strings, collections):
15
29
  return ls
16
30
 
17
31
 
18
- def _clean_set(values, strings, collections):
32
+ def _clean_set(values: set[_T], strings: bool, collections: bool) -> set[_T]:
19
33
  return {
20
34
  value
21
35
  for value in values
@@ -23,11 +37,13 @@ def _clean_set(values, strings, collections):
23
37
  }
24
38
 
25
39
 
26
- def _clean_str(s, strings, collections):
40
+ def _clean_str(s: str, strings: bool, collections: bool) -> str | None:
27
41
  return s if s and s.strip() else None
28
42
 
29
43
 
30
- def _clean_tuple(values, strings, collections):
44
+ def _clean_tuple(
45
+ values: tuple[_T, ...], strings: bool, collections: bool
46
+ ) -> tuple[_T, ...]:
31
47
  return tuple(
32
48
  value
33
49
  for value in values
@@ -35,13 +51,15 @@ def _clean_tuple(values, strings, collections):
35
51
  )
36
52
 
37
53
 
38
- def _clean_value(value, strings, collections):
54
+ def _clean_value(value: Any, strings: bool, collections: bool) -> Any:
39
55
  if value is None:
40
56
  return value
41
- elif isinstance(value, list) and collections:
57
+ elif isinstance(value, MutableSequence) and collections:
42
58
  value = _clean_list(value, strings=strings, collections=collections) or None
43
- elif isinstance(value, dict) and collections:
44
- value = _clean_dict(value, strings=strings, collections=collections) or None
59
+ elif isinstance(value, MutableMapping) and collections:
60
+ value = (
61
+ _clean_dict(dict(value), strings=strings, collections=collections) or None
62
+ )
45
63
  elif isinstance(value, set) and collections:
46
64
  value = _clean_set(value, strings=strings, collections=collections) or None
47
65
  elif isinstance(value, str) and strings:
@@ -51,5 +69,5 @@ def _clean_value(value, strings, collections):
51
69
  return value
52
70
 
53
71
 
54
- def clean(d, strings=True, collections=True):
72
+ def clean(d: Any, strings: bool = True, collections: bool = True) -> Any:
55
73
  return _clean_dict(d, strings=strings, collections=collections)
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ from collections.abc import MutableMapping
5
+ from typing import Any, TypeVar
6
+
7
+ _T = TypeVar("_T")
8
+
9
+
10
+ def clone(
11
+ obj: _T,
12
+ empty: bool = False,
13
+ memo: dict[int, Any] | None = None,
14
+ ) -> _T:
15
+ d = copy.deepcopy(obj, memo)
16
+ if empty and isinstance(d, MutableMapping):
17
+ d.clear()
18
+ return d
@@ -1,7 +1,9 @@
1
+ from typing import Any
2
+
1
3
  from benedict.serializers import JSONSerializer
2
4
 
3
5
 
4
- def dump(obj, **kwargs):
6
+ def dump(obj: Any, **kwargs: Any) -> str:
5
7
  serializer = JSONSerializer()
6
8
  options = {"indent": 4, "sort_keys": True}
7
9
  options.update(**kwargs)
@@ -0,0 +1,21 @@
1
+ from collections.abc import Callable, MutableMapping
2
+ from typing import TypeVar
3
+
4
+ from benedict.core.clone import clone
5
+
6
+ _K = TypeVar("_K")
7
+ _V = TypeVar("_V")
8
+
9
+
10
+ def filter(
11
+ d: MutableMapping[_K, _V], predicate: Callable[[_K, _V], bool]
12
+ ) -> MutableMapping[_K, _V]:
13
+ if not callable(predicate):
14
+ raise ValueError("predicate argument must be a callable.")
15
+ new_dict = clone(d, empty=True)
16
+ keys = list(d.keys())
17
+ for key in keys:
18
+ value = d[key]
19
+ if predicate(key, value):
20
+ new_dict[key] = value
21
+ return new_dict
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable, Mapping
4
+ from typing import TypeVar
5
+
6
+ _K = TypeVar("_K")
7
+ _V = TypeVar("_V")
8
+
9
+
10
+ def find(
11
+ d: Mapping[_K, _V], keys: Iterable[_K], default: _V | None = None
12
+ ) -> _V | None:
13
+ for key in keys:
14
+ if key in d:
15
+ return d[key]
16
+ return default
@@ -1,20 +1,27 @@
1
- from benedict.core import clone
2
- from benedict.utils import type_util
1
+ from collections.abc import Mapping
2
+ from typing import Any
3
3
 
4
+ from benedict.core.clone import clone
4
5
 
5
- def _flatten_key(base_key, key, separator):
6
+
7
+ def _flatten_key(base_key: str, key: str, separator: str) -> str:
6
8
  if base_key and separator:
7
9
  return f"{base_key}{separator}{key}"
8
10
  return key
9
11
 
10
12
 
11
- def _flatten_item(d, base_dict, base_key, separator):
13
+ def _flatten_item(
14
+ d: Any,
15
+ base_dict: Any,
16
+ base_key: str,
17
+ separator: str,
18
+ ) -> Any:
12
19
  new_dict = base_dict
13
20
  keys = list(d.keys())
14
21
  for key in keys:
15
22
  new_key = _flatten_key(base_key, key, separator)
16
23
  value = d.get(key, None)
17
- if type_util.is_dict(value):
24
+ if isinstance(value, Mapping):
18
25
  new_value = _flatten_item(
19
26
  value, base_dict=new_dict, base_key=new_key, separator=separator
20
27
  )
@@ -26,6 +33,6 @@ def _flatten_item(d, base_dict, base_key, separator):
26
33
  return new_dict
27
34
 
28
35
 
29
- def flatten(d, separator="_"):
36
+ def flatten(d: Any, separator: str = "_") -> Any:
30
37
  new_dict = clone(d, empty=True)
31
38
  return _flatten_item(d, base_dict=new_dict, base_key="", separator=separator)
@@ -1,10 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, MutableSequence, Sequence
4
+ from typing import Any, TypeVar
5
+
1
6
  from benedict.utils import type_util
2
7
 
8
+ _K = TypeVar("_K")
9
+ _V = TypeVar("_V", bound=MutableSequence[Any])
10
+
3
11
 
4
- def groupby(items, key):
12
+ def groupby(items: Sequence[Mapping[_K, Any]], key: _K) -> dict[Any, Any]:
5
13
  if not type_util.is_list(items):
6
14
  raise ValueError("items should be a list of dicts.")
7
- items_grouped = {}
15
+ items_grouped: dict[Any, Any] = {}
8
16
  for item in items:
9
17
  if not type_util.is_dict(item):
10
18
  raise ValueError("item should be a dict.")
@@ -1,20 +1,28 @@
1
- from benedict.core import clone
1
+ from collections.abc import MutableMapping, Sequence
2
+ from typing import Any, TypeVar
3
+
4
+ from benedict.core.clone import clone
2
5
  from benedict.utils import type_util
3
6
 
7
+ _K = TypeVar("_K")
8
+ _V = TypeVar("_V")
9
+
4
10
 
5
- def _invert_item(d, key, value, flat):
11
+ def _invert_item(d: MutableMapping[Any, Any], key: _K, value: _V, flat: bool) -> None:
6
12
  if flat:
7
13
  d.setdefault(value, key)
8
14
  else:
9
15
  d.setdefault(value, []).append(key)
10
16
 
11
17
 
12
- def _invert_list(d, key, value, flat):
18
+ def _invert_list(
19
+ d: MutableMapping[Any, Any], key: _K, value: Sequence[Any], flat: bool
20
+ ) -> None:
13
21
  for value_item in value:
14
22
  _invert_item(d, key, value_item, flat)
15
23
 
16
24
 
17
- def invert(d, flat=False):
25
+ def invert(d: MutableMapping[_K, _V], flat: bool = False) -> MutableMapping[Any, Any]:
18
26
  new_dict = clone(d, empty=True)
19
27
  for key, value in d.items():
20
28
  if type_util.is_list_or_tuple(value):
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+
5
+ from useful_types import SupportsRichComparisonT
6
+
7
+
8
+ def _items_sorted_by_item_at_index(
9
+ d: Mapping[SupportsRichComparisonT, SupportsRichComparisonT],
10
+ index: int,
11
+ reverse: bool,
12
+ ) -> list[tuple[SupportsRichComparisonT, SupportsRichComparisonT]]:
13
+ return sorted(d.items(), key=lambda item: item[index], reverse=reverse)
14
+
15
+
16
+ def items_sorted_by_keys(
17
+ d: Mapping[SupportsRichComparisonT, SupportsRichComparisonT], reverse: bool = False
18
+ ) -> list[tuple[SupportsRichComparisonT, SupportsRichComparisonT]]:
19
+ return _items_sorted_by_item_at_index(d, 0, reverse)
20
+
21
+
22
+ def items_sorted_by_values(
23
+ d: Mapping[SupportsRichComparisonT, SupportsRichComparisonT], reverse: bool = False
24
+ ) -> list[tuple[SupportsRichComparisonT, SupportsRichComparisonT]]:
25
+ return _items_sorted_by_item_at_index(d, 1, reverse)
@@ -1,7 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import Any
5
+
1
6
  from benedict.utils import type_util
2
7
 
3
8
 
4
- def _get_keylist_for_dict(d, parent_keys, indexes):
9
+ def _get_keylist_for_dict(
10
+ d: Mapping[Any, Any], parent_keys: list[Any], indexes: bool
11
+ ) -> list[list[Any]]:
5
12
  keylist = []
6
13
  for key, value in d.items():
7
14
  keys = parent_keys + [key]
@@ -10,7 +17,9 @@ def _get_keylist_for_dict(d, parent_keys, indexes):
10
17
  return keylist
11
18
 
12
19
 
13
- def _get_keylist_for_list(ls, parent_keys, indexes):
20
+ def _get_keylist_for_list(
21
+ ls: Sequence[Any], parent_keys: list[Any], indexes: bool
22
+ ) -> list[list[Any]]:
14
23
  keylist = []
15
24
  for key, value in enumerate(ls):
16
25
  keys = list(parent_keys)
@@ -20,7 +29,9 @@ def _get_keylist_for_list(ls, parent_keys, indexes):
20
29
  return keylist
21
30
 
22
31
 
23
- def _get_keylist_for_value(value, parent_keys, indexes):
32
+ def _get_keylist_for_value(
33
+ value: Mapping[Any, Any] | Sequence[Any], parent_keys: list[Any], indexes: bool
34
+ ) -> list[list[Any]]:
24
35
  if type_util.is_dict(value):
25
36
  return _get_keylist_for_dict(value, parent_keys, indexes)
26
37
  elif type_util.is_list(value) and indexes:
@@ -28,5 +39,7 @@ def _get_keylist_for_value(value, parent_keys, indexes):
28
39
  return []
29
40
 
30
41
 
31
- def keylists(d, indexes=False):
42
+ def keylists(
43
+ d: Mapping[Any, Any] | Sequence[Any], indexes: bool = False
44
+ ) -> list[list[Any]]:
32
45
  return _get_keylist_for_value(d, [], indexes)
@@ -1,8 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
1
6
  from benedict.core.keylists import keylists
2
7
  from benedict.utils import type_util
3
8
 
4
9
 
5
- def keypaths(d, separator=".", indexes=False, sort=True):
10
+ def keypaths(
11
+ d: Mapping[Any, Any],
12
+ separator: str | None = ".",
13
+ indexes: bool = False,
14
+ sort: bool = True,
15
+ ) -> list[str]:
6
16
  separator = separator or "."
7
17
  if not type_util.is_string(separator):
8
18
  raise ValueError("separator argument must be a (non-empty) string.")
@@ -1,10 +1,19 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
4
+ from collections.abc import Mapping
5
+ from typing import Any
2
6
 
3
7
  from benedict.core.keypaths import keypaths
4
8
  from benedict.utils import type_util
5
9
 
6
10
 
7
- def match(d, pattern, separator=".", indexes=True):
11
+ def match(
12
+ d: Mapping[Any, Any],
13
+ pattern: str | re.Pattern[str],
14
+ separator: str | None = ".",
15
+ indexes: bool = True,
16
+ ) -> list[Any]:
8
17
  if type_util.is_regex(pattern):
9
18
  regex = pattern
10
19
  elif type_util.is_string(pattern):