python-benedict 0.35.0__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 (76) hide show
  1. {python_benedict-0.35.0/python_benedict.egg-info → python_benedict-0.36.0}/PKG-INFO +42 -2
  2. {python_benedict-0.35.0 → python_benedict-0.36.0}/README.md +39 -0
  3. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/__init__.py +17 -2
  4. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/base/base_dict.py +48 -9
  5. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/keyattr/keyattr_dict.py +16 -0
  6. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/metadata.py +1 -1
  7. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/xls.py +11 -0
  8. {python_benedict-0.35.0 → python_benedict-0.36.0}/pyproject.toml +2 -1
  9. {python_benedict-0.35.0 → python_benedict-0.36.0/python_benedict.egg-info}/PKG-INFO +42 -2
  10. {python_benedict-0.35.0 → python_benedict-0.36.0}/python_benedict.egg-info/requires.txt +1 -1
  11. {python_benedict-0.35.0 → python_benedict-0.36.0}/LICENSE.txt +0 -0
  12. {python_benedict-0.35.0 → python_benedict-0.36.0}/MANIFEST.in +0 -0
  13. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/__init__.py +0 -0
  14. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/__init__.py +0 -0
  15. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/clean.py +0 -0
  16. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/clone.py +0 -0
  17. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/dump.py +0 -0
  18. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/filter.py +0 -0
  19. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/find.py +0 -0
  20. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/flatten.py +0 -0
  21. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/groupby.py +0 -0
  22. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/invert.py +0 -0
  23. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/items_sorted.py +0 -0
  24. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/keylists.py +0 -0
  25. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/keypaths.py +0 -0
  26. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/match.py +0 -0
  27. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/merge.py +0 -0
  28. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/move.py +0 -0
  29. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/nest.py +0 -0
  30. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/remove.py +0 -0
  31. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/rename.py +0 -0
  32. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/search.py +0 -0
  33. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/standardize.py +0 -0
  34. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/subset.py +0 -0
  35. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/swap.py +0 -0
  36. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/traverse.py +0 -0
  37. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/unflatten.py +0 -0
  38. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/core/unique.py +0 -0
  39. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/base/__init__.py +0 -0
  40. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/io/__init__.py +0 -0
  41. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/io/io_dict.py +0 -0
  42. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/io/io_util.py +0 -0
  43. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/keyattr/__init__.py +0 -0
  44. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/keylist/__init__.py +0 -0
  45. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/keylist/keylist_dict.py +0 -0
  46. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/keylist/keylist_util.py +0 -0
  47. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/keypath/__init__.py +0 -0
  48. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/keypath/keypath_dict.py +0 -0
  49. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/keypath/keypath_util.py +0 -0
  50. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/parse/__init__.py +0 -0
  51. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/parse/parse_dict.py +0 -0
  52. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/dicts/parse/parse_util.py +0 -0
  53. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/exceptions.py +0 -0
  54. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/extras.py +0 -0
  55. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/py.typed +0 -0
  56. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/__init__.py +0 -0
  57. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/abstract.py +0 -0
  58. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/base64.py +0 -0
  59. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/cli.py +0 -0
  60. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/csv.py +0 -0
  61. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/html.py +0 -0
  62. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/ini.py +0 -0
  63. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/json.py +0 -0
  64. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/pickle.py +0 -0
  65. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/plist.py +0 -0
  66. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/query_string.py +0 -0
  67. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/toml.py +0 -0
  68. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/xml.py +0 -0
  69. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/serializers/yaml.py +0 -0
  70. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/utils/__init__.py +0 -0
  71. {python_benedict-0.35.0 → python_benedict-0.36.0}/benedict/utils/type_util.py +0 -0
  72. {python_benedict-0.35.0 → python_benedict-0.36.0}/python_benedict.egg-info/SOURCES.txt +0 -0
  73. {python_benedict-0.35.0 → python_benedict-0.36.0}/python_benedict.egg-info/dependency_links.txt +0 -0
  74. {python_benedict-0.35.0 → python_benedict-0.36.0}/python_benedict.egg-info/top_level.txt +0 -0
  75. {python_benedict-0.35.0 → python_benedict-0.36.0}/setup.cfg +0 -0
  76. {python_benedict-0.35.0 → python_benedict-0.36.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-benedict
3
- Version: 0.35.0
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,6 +51,7 @@ 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
@@ -59,7 +60,7 @@ Classifier: Topic :: Utilities
59
60
  Classifier: Typing :: Typed
60
61
  Description-Content-Type: text/markdown
61
62
  License-File: LICENSE.txt
62
- Requires-Dist: python-fsutil<1.0.0,>=0.9.3
63
+ Requires-Dist: python-fsutil<1.0.0,>=0.16.0
63
64
  Requires-Dist: python-slugify<9.0.0,>=7.0.0
64
65
  Requires-Dist: requests<3.0.0,>=2.26.0
65
66
  Requires-Dist: typing_extensions<4.16.0,>=4.13.2
@@ -94,6 +95,7 @@ Dynamic: license-file
94
95
  [![](https://static.pepy.tech/badge/python-benedict/month)](https://pepy.tech/project/python-benedict)
95
96
  [![](https://img.shields.io/github/stars/fabiocaccamo/python-benedict?logo=github&style=flat)](https://github.com/fabiocaccamo/python-benedict/stargazers)
96
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)
97
99
 
98
100
  [![](https://results.pre-commit.ci/badge/github/fabiocaccamo/python-benedict/main.svg)](https://results.pre-commit.ci/latest/github/fabiocaccamo/python-benedict/main)
99
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)
@@ -102,6 +104,7 @@ Dynamic: license-file
102
104
  [![](https://img.shields.io/scrutinizer/quality/g/fabiocaccamo/python-benedict?logo=scrutinizer)](https://scrutinizer-ci.com/g/fabiocaccamo/python-benedict/?branch=main)
103
105
  [![](https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=black)](https://github.com/psf/black)
104
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)
105
108
 
106
109
  # python-benedict
107
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.
@@ -135,6 +138,7 @@ python-benedict is a dict subclass with **keylist/keypath/keyattr** support, **I
135
138
  - [I/O methods](#io-methods)
136
139
  - [Parse methods](#parse-methods)
137
140
  - [Testing](#testing)
141
+ - [Security](#security)
138
142
  - [License](#license)
139
143
 
140
144
  ## Installation
@@ -219,6 +223,9 @@ d.keyattr_dynamic = True
219
223
 
220
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.
221
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
+
222
229
  ### Keylist
223
230
  Wherever a **key** is used, it is possible to use also a **list of keys**.
224
231
 
@@ -379,6 +386,8 @@ Here are the details of the supported formats, operations and extra options docs
379
386
  - [`filter`](#filter)
380
387
  - [`find`](#find)
381
388
  - [`flatten`](#flatten)
389
+ - [`freeze`](#freeze)
390
+ - [`frozen`](#frozen)
382
391
  - [`groupby`](#groupby)
383
392
  - [`invert`](#invert)
384
393
  - [`items_sorted_by_keys`](#items_sorted_by_keys)
@@ -396,6 +405,7 @@ Here are the details of the supported formats, operations and extra options docs
396
405
  - [`swap`](#swap)
397
406
  - [`traverse`](#traverse)
398
407
  - [`unflatten`](#unflatten)
408
+ - [`unfreeze`](#unfreeze)
399
409
  - [`unique`](#unique)
400
410
 
401
411
  - **I/O methods**
@@ -511,6 +521,22 @@ f = d.find(keys, default=0)
511
521
  f = d.flatten(separator="_")
512
522
  ```
513
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
+
514
540
  #### `groupby`
515
541
 
516
542
  ```python
@@ -649,6 +675,13 @@ d.traverse(f)
649
675
  u = d.unflatten(separator="_")
650
676
  ```
651
677
 
678
+ #### `unfreeze`
679
+
680
+ ```python
681
+ # Make the dict mutable again after a freeze() call.
682
+ d.unfreeze()
683
+ ```
684
+
652
685
  #### `unique`
653
686
 
654
687
  ```python
@@ -1121,6 +1154,13 @@ tox
1121
1154
  python -m unittest
1122
1155
  ```
1123
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
+
1124
1164
  ## License
1125
1165
  Released under [MIT License](LICENSE.txt).
1126
1166
 
@@ -3,6 +3,7 @@
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)
@@ -11,6 +12,7 @@
11
12
  [![](https://img.shields.io/scrutinizer/quality/g/fabiocaccamo/python-benedict?logo=scrutinizer)](https://scrutinizer-ci.com/g/fabiocaccamo/python-benedict/?branch=main)
12
13
  [![](https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=black)](https://github.com/psf/black)
13
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)
14
16
 
15
17
  # python-benedict
16
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.
@@ -44,6 +46,7 @@ python-benedict is a dict subclass with **keylist/keypath/keyattr** support, **I
44
46
  - [I/O methods](#io-methods)
45
47
  - [Parse methods](#parse-methods)
46
48
  - [Testing](#testing)
49
+ - [Security](#security)
47
50
  - [License](#license)
48
51
 
49
52
  ## Installation
@@ -128,6 +131,9 @@ d.keyattr_dynamic = True
128
131
 
129
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.
130
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
+
131
137
  ### Keylist
132
138
  Wherever a **key** is used, it is possible to use also a **list of keys**.
133
139
 
@@ -288,6 +294,8 @@ Here are the details of the supported formats, operations and extra options docs
288
294
  - [`filter`](#filter)
289
295
  - [`find`](#find)
290
296
  - [`flatten`](#flatten)
297
+ - [`freeze`](#freeze)
298
+ - [`frozen`](#frozen)
291
299
  - [`groupby`](#groupby)
292
300
  - [`invert`](#invert)
293
301
  - [`items_sorted_by_keys`](#items_sorted_by_keys)
@@ -305,6 +313,7 @@ Here are the details of the supported formats, operations and extra options docs
305
313
  - [`swap`](#swap)
306
314
  - [`traverse`](#traverse)
307
315
  - [`unflatten`](#unflatten)
316
+ - [`unfreeze`](#unfreeze)
308
317
  - [`unique`](#unique)
309
318
 
310
319
  - **I/O methods**
@@ -420,6 +429,22 @@ f = d.find(keys, default=0)
420
429
  f = d.flatten(separator="_")
421
430
  ```
422
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
+
423
448
  #### `groupby`
424
449
 
425
450
  ```python
@@ -558,6 +583,13 @@ d.traverse(f)
558
583
  u = d.unflatten(separator="_")
559
584
  ```
560
585
 
586
+ #### `unfreeze`
587
+
588
+ ```python
589
+ # Make the dict mutable again after a freeze() call.
590
+ d.unfreeze()
591
+ ```
592
+
561
593
  #### `unique`
562
594
 
563
595
  ```python
@@ -1030,6 +1062,13 @@ tox
1030
1062
  python -m unittest
1031
1063
  ```
1032
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
+
1033
1072
  ## License
1034
1073
  Released under [MIT License](LICENSE.txt).
1035
1074
 
@@ -88,12 +88,15 @@ class benedict(KeyattrDict[_K, _V], KeypathDict[_V], IODict[_K, _V], ParseDict[_
88
88
  )
89
89
  for key, value in self.items():
90
90
  obj[key] = _clone(value, memo=memo)
91
+ if self._frozen:
92
+ obj.freeze()
91
93
  return obj
92
94
 
93
95
  def __getitem__(self, key: _KPT) -> Any: # type: ignore[override]
94
96
  return self._cast(super().__getitem__(key))
95
97
 
96
98
  def __setitem__(self, key: _KPT, value: _V) -> None: # type: ignore[override]
99
+ self._check_frozen()
97
100
  super().__setitem__(key, self._cast(value))
98
101
 
99
102
  def _cast(self, value: Any) -> Any:
@@ -133,7 +136,17 @@ class benedict(KeyattrDict[_K, _V], KeypathDict[_V], IODict[_K, _V], ParseDict[_
133
136
  """
134
137
  Creates and return a copy of the current instance (shallow copy).
135
138
  """
136
- return cast("Self", self._cast(super().copy()))
139
+ obj_type = type(self)
140
+ obj = obj_type(
141
+ keyattr_enabled=self._keyattr_enabled,
142
+ keyattr_dynamic=self._keyattr_dynamic,
143
+ keypath_separator=self._keypath_separator,
144
+ )
145
+ for key, value in self.items():
146
+ obj[key] = value
147
+ if self._frozen:
148
+ obj.freeze()
149
+ return obj
137
150
 
138
151
  def deepcopy(self) -> Self:
139
152
  """
@@ -195,7 +208,9 @@ class benedict(KeyattrDict[_K, _V], KeypathDict[_V], IODict[_K, _V], ParseDict[_
195
208
  """
196
209
  Group a list of dicts at key by the value of the given by_key and return a new dict.
197
210
  """
198
- return cast("Self", self._cast(_groupby(self[key], by_key)))
211
+ result = _groupby(self[key], by_key)
212
+ keypath_util.check_keys(result, self._keypath_separator)
213
+ return cast("Self", self._cast(result))
199
214
 
200
215
  def invert(self, flat: bool = False) -> Self:
201
216
  """
@@ -19,7 +19,8 @@ _V = TypeVar("_V", default=Any)
19
19
 
20
20
 
21
21
  class BaseDict(dict[_K, _V]):
22
- _dict: dict[_K, _V] | None = None
22
+ _dict: dict[_K, _V] | None
23
+ _frozen: bool
23
24
 
24
25
  @classmethod
25
26
  def _get_dict_or_value(cls, value: Any) -> Any:
@@ -32,11 +33,19 @@ class BaseDict(dict[_K, _V]):
32
33
  value[key] = key_val
33
34
  return value
34
35
 
36
+ def __new__(cls, *args: Any, **kwargs: Any) -> Self:
37
+ obj = super().__new__(cls)
38
+ # bypass subclass __setattr__ (e.g. KeyattrDict) which may access _dict before it exists
39
+ object.__setattr__(obj, "_dict", None)
40
+ object.__setattr__(obj, "_frozen", False)
41
+ return obj
42
+
35
43
  def __init__(self, *args: Any, **kwargs: Any) -> None:
36
- self._dict = None
37
44
  if len(args) == 1 and isinstance(args[0], Mapping):
38
- self._dict = self._get_dict_or_value(args[0])
39
- super().__init__(self._dict)
45
+ # bypass subclass __setattr__ (e.g. KeyattrDict) which may treat _dict as a key
46
+ d = self._get_dict_or_value(args[0])
47
+ object.__setattr__(self, "_dict", d)
48
+ super().__init__(d)
40
49
  return
41
50
  super().__init__(*args, **kwargs)
42
51
 
@@ -54,9 +63,12 @@ class BaseDict(dict[_K, _V]):
54
63
  obj = self.__class__()
55
64
  for key, value in self.items():
56
65
  obj[key] = _clone(value, memo=memo)
66
+ if self._frozen:
67
+ obj.freeze()
57
68
  return obj
58
69
 
59
70
  def __delitem__(self, key: _K) -> None:
71
+ self._check_frozen()
60
72
  if self._dict is not None:
61
73
  del self._dict[key]
62
74
  return
@@ -73,6 +85,7 @@ class BaseDict(dict[_K, _V]):
73
85
  return super().__getitem__(key)
74
86
 
75
87
  def __ior__(self, other: Any) -> Self: # type: ignore[misc,override]
88
+ self._check_frozen()
76
89
  if self._dict is not None:
77
90
  return cast("Self", self._dict.__ior__(other))
78
91
  return super().__ior__(other)
@@ -98,6 +111,7 @@ class BaseDict(dict[_K, _V]):
98
111
  return super().__repr__()
99
112
 
100
113
  def __setitem__(self, key: _K, value: _V) -> None:
114
+ self._check_frozen()
101
115
  value = self._get_dict_or_value(value)
102
116
  if self._dict is not None:
103
117
  is_dict_item = key in self._dict and isinstance(self._dict[key], dict)
@@ -114,24 +128,46 @@ class BaseDict(dict[_K, _V]):
114
128
  super().__setitem__(key, value)
115
129
 
116
130
  def __setstate__(self, state: Mapping[str, Any]) -> None:
117
- self._dict = state["_dict"]
118
- self._dict = state["_dict"]
131
+ object.__setattr__(self, "_dict", state.get("_dict", None))
132
+ object.__setattr__(self, "_frozen", state.get("_frozen", False))
119
133
 
120
134
  def __str__(self) -> str:
121
135
  if self._dict is not None:
122
136
  return str(self._dict)
123
137
  return super().__str__()
124
138
 
139
+ def _check_frozen(self) -> None:
140
+ if self._frozen:
141
+ raise TypeError(
142
+ f"{self.__class__.__name__!r} object is frozen and cannot be modified."
143
+ )
144
+
145
+ @property
146
+ def frozen(self) -> bool:
147
+ return self._frozen
148
+
149
+ def freeze(self) -> Self:
150
+ object.__setattr__(self, "_frozen", True)
151
+ return self
152
+
153
+ def unfreeze(self) -> Self:
154
+ object.__setattr__(self, "_frozen", False)
155
+ return self
156
+
125
157
  def clear(self) -> None:
158
+ self._check_frozen()
126
159
  if self._dict is not None:
127
160
  self._dict.clear()
128
161
  return
129
162
  super().clear()
130
163
 
131
164
  def copy(self) -> Self:
132
- if self._dict is not None:
133
- return cast("Self", self._dict.copy())
134
- return cast("Self", super().copy())
165
+ obj = self.__class__()
166
+ for key, value in self.items():
167
+ obj[key] = value
168
+ if self._frozen:
169
+ obj.freeze()
170
+ return obj
135
171
 
136
172
  def dict(self) -> Self:
137
173
  if self._dict is not None:
@@ -154,11 +190,13 @@ class BaseDict(dict[_K, _V]):
154
190
  return super().keys()
155
191
 
156
192
  def pop(self, key: _K, *args: Any) -> _V:
193
+ self._check_frozen()
157
194
  if self._dict is not None:
158
195
  return self._dict.pop(key, *args) # type: ignore[no-any-return]
159
196
  return super().pop(key, *args) # type: ignore[no-any-return]
160
197
 
161
198
  def setdefault(self, key: _K, default: _V | None = None) -> _V:
199
+ self._check_frozen()
162
200
  default = self._get_dict_or_value(default)
163
201
  assert default is not None
164
202
  if self._dict is not None:
@@ -166,6 +204,7 @@ class BaseDict(dict[_K, _V]):
166
204
  return super().setdefault(key, default)
167
205
 
168
206
  def update(self, other: Any) -> None:
207
+ self._check_frozen()
169
208
  other = self._get_dict_or_value(other)
170
209
  if self._dict is not None:
171
210
  self._dict.update(other)
@@ -68,6 +68,22 @@ class KeyattrDict(BaseDict[_K, _V]):
68
68
  raise AttributeError(attr_message)
69
69
  self.__setitem__(attr_k, value)
70
70
 
71
+ def __dir__(self) -> list[str]:
72
+ # Extend tab-completion (VSCode, IPython, etc.) with the dict keys when
73
+ # keyattr is enabled, since keys are accessible as attributes via __getattr__.
74
+ # Non-string keys are excluded: __dir__ must return strings (Python protocol)
75
+ # and non-string keys can never be valid attribute names anyway.
76
+ keys: list[str] = (
77
+ [key for key in self.keys() if isinstance(key, str)]
78
+ if self._keyattr_enabled
79
+ else []
80
+ )
81
+ return sorted(set(list(super().__dir__()) + keys))
82
+
83
+ def _ipython_key_completions_(self) -> list[Any]:
84
+ # Used by IPython/Jupyter for subscript tab-completion: obj["<TAB>"]
85
+ return list(self.keys())
86
+
71
87
  def __setstate__(self, state: Mapping[str, Any]) -> None:
72
88
  super().__setstate__(state)
73
89
  self._keyattr_enabled = state["_keyattr_enabled"]
@@ -8,4 +8,4 @@ __description__ = (
8
8
  __email__ = "fabio.caccamo@gmail.com"
9
9
  __license__ = "MIT"
10
10
  __title__ = "benedict"
11
- __version__ = "0.35.0"
11
+ __version__ = "0.36.0"
@@ -156,6 +156,17 @@ class XLSSerializer(AbstractSerializer[str, list[dict[str, Any]]]):
156
156
  values = [cell.value for cell in row]
157
157
  items.append(dict(zip(columns, values)))
158
158
 
159
+ try:
160
+ # explicitly close vba_archive if present: Workbook.close() never
161
+ # closes it, so relying on GC (__del__ -> close()) is unsafe because
162
+ # the underlying BytesIO may already be finalized first, causing a
163
+ # ValueError when ZipFile tries to flush the end-of-central-directory
164
+ vba_archive = getattr(workbook, "vba_archive", None)
165
+ if vba_archive is not None:
166
+ vba_archive.close()
167
+ except Exception:
168
+ pass
169
+
159
170
  # close the worksheet
160
171
  workbook.close()
161
172
 
@@ -81,6 +81,7 @@ classifiers = [
81
81
  "Programming Language :: Python :: 3.11",
82
82
  "Programming Language :: Python :: 3.12",
83
83
  "Programming Language :: Python :: 3.13",
84
+ "Programming Language :: Python :: 3.14",
84
85
  "Topic :: Education :: Testing",
85
86
  "Topic :: Software Development :: Build Tools",
86
87
  "Topic :: System :: Filesystems",
@@ -89,7 +90,7 @@ classifiers = [
89
90
  "Typing :: Typed",
90
91
  ]
91
92
  dependencies = [
92
- "python-fsutil >= 0.9.3, < 1.0.0",
93
+ "python-fsutil >= 0.16.0, < 1.0.0",
93
94
  "python-slugify >= 7.0.0, < 9.0.0",
94
95
  "requests >= 2.26.0, < 3.0.0",
95
96
  "typing_extensions >= 4.13.2, < 4.16.0",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-benedict
3
- Version: 0.35.0
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,6 +51,7 @@ 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
@@ -59,7 +60,7 @@ Classifier: Topic :: Utilities
59
60
  Classifier: Typing :: Typed
60
61
  Description-Content-Type: text/markdown
61
62
  License-File: LICENSE.txt
62
- Requires-Dist: python-fsutil<1.0.0,>=0.9.3
63
+ Requires-Dist: python-fsutil<1.0.0,>=0.16.0
63
64
  Requires-Dist: python-slugify<9.0.0,>=7.0.0
64
65
  Requires-Dist: requests<3.0.0,>=2.26.0
65
66
  Requires-Dist: typing_extensions<4.16.0,>=4.13.2
@@ -94,6 +95,7 @@ Dynamic: license-file
94
95
  [![](https://static.pepy.tech/badge/python-benedict/month)](https://pepy.tech/project/python-benedict)
95
96
  [![](https://img.shields.io/github/stars/fabiocaccamo/python-benedict?logo=github&style=flat)](https://github.com/fabiocaccamo/python-benedict/stargazers)
96
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)
97
99
 
98
100
  [![](https://results.pre-commit.ci/badge/github/fabiocaccamo/python-benedict/main.svg)](https://results.pre-commit.ci/latest/github/fabiocaccamo/python-benedict/main)
99
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)
@@ -102,6 +104,7 @@ Dynamic: license-file
102
104
  [![](https://img.shields.io/scrutinizer/quality/g/fabiocaccamo/python-benedict?logo=scrutinizer)](https://scrutinizer-ci.com/g/fabiocaccamo/python-benedict/?branch=main)
103
105
  [![](https://img.shields.io/badge/code%20style-black-000000.svg?logo=python&logoColor=black)](https://github.com/psf/black)
104
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)
105
108
 
106
109
  # python-benedict
107
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.
@@ -135,6 +138,7 @@ python-benedict is a dict subclass with **keylist/keypath/keyattr** support, **I
135
138
  - [I/O methods](#io-methods)
136
139
  - [Parse methods](#parse-methods)
137
140
  - [Testing](#testing)
141
+ - [Security](#security)
138
142
  - [License](#license)
139
143
 
140
144
  ## Installation
@@ -219,6 +223,9 @@ d.keyattr_dynamic = True
219
223
 
220
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.
221
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
+
222
229
  ### Keylist
223
230
  Wherever a **key** is used, it is possible to use also a **list of keys**.
224
231
 
@@ -379,6 +386,8 @@ Here are the details of the supported formats, operations and extra options docs
379
386
  - [`filter`](#filter)
380
387
  - [`find`](#find)
381
388
  - [`flatten`](#flatten)
389
+ - [`freeze`](#freeze)
390
+ - [`frozen`](#frozen)
382
391
  - [`groupby`](#groupby)
383
392
  - [`invert`](#invert)
384
393
  - [`items_sorted_by_keys`](#items_sorted_by_keys)
@@ -396,6 +405,7 @@ Here are the details of the supported formats, operations and extra options docs
396
405
  - [`swap`](#swap)
397
406
  - [`traverse`](#traverse)
398
407
  - [`unflatten`](#unflatten)
408
+ - [`unfreeze`](#unfreeze)
399
409
  - [`unique`](#unique)
400
410
 
401
411
  - **I/O methods**
@@ -511,6 +521,22 @@ f = d.find(keys, default=0)
511
521
  f = d.flatten(separator="_")
512
522
  ```
513
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
+
514
540
  #### `groupby`
515
541
 
516
542
  ```python
@@ -649,6 +675,13 @@ d.traverse(f)
649
675
  u = d.unflatten(separator="_")
650
676
  ```
651
677
 
678
+ #### `unfreeze`
679
+
680
+ ```python
681
+ # Make the dict mutable again after a freeze() call.
682
+ d.unfreeze()
683
+ ```
684
+
652
685
  #### `unique`
653
686
 
654
687
  ```python
@@ -1121,6 +1154,13 @@ tox
1121
1154
  python -m unittest
1122
1155
  ```
1123
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
+
1124
1164
  ## License
1125
1165
  Released under [MIT License](LICENSE.txt).
1126
1166
 
@@ -1,4 +1,4 @@
1
- python-fsutil<1.0.0,>=0.9.3
1
+ python-fsutil<1.0.0,>=0.16.0
2
2
  python-slugify<9.0.0,>=7.0.0
3
3
  requests<3.0.0,>=2.26.0
4
4
  typing_extensions<4.16.0,>=4.13.2