summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorColin Watson <cjwatson@debian.org>2025-02-24 13:08:37 +0000
committergit-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com>2025-02-24 16:43:26 +0000
commit2ba50cddcc72ba83e95358f1a5fdfc06efd1654d (patch)
tree91dd65a240e8c7acf6c4c87a901e694d2e252500
parent225d8213a7da9cbc1681b74853238a27c4838ecd (diff)
Imported using git-ubuntu import.
Notes
Notes: * Team upload. [ Debian Janitor ] * Update standards version to 4.6.1, no changes needed. [ Colin Watson ] * d/watch: Adjust for PyPI normalization. * New upstream release. Now Architecture: any due to using Cython. * Use pybuild-plugin-pyproject. * Mark autopkgtest as superficial (closes: #974492).
-rw-r--r--AUTHORS.rst52
-rw-r--r--CHANGES.rst308
-rw-r--r--LICENSE2
-rw-r--r--MANIFEST.in11
-rw-r--r--PKG-INFO689
-rw-r--r--README.rst457
-rw-r--r--box.py1217
-rw-r--r--box/__init__.py24
-rw-r--r--box/box.py1195
-rw-r--r--box/box.pyi121
-rw-r--r--box/box_list.py476
-rw-r--r--box/box_list.pyi83
-rw-r--r--box/config_box.py133
-rw-r--r--box/config_box.pyi15
-rw-r--r--box/converters.py363
-rw-r--r--box/converters.pyi60
-rw-r--r--box/exceptions.py22
-rw-r--r--box/exceptions.pyi5
-rw-r--r--box/from_file.py149
-rw-r--r--box/from_file.pyi16
-rw-r--r--box/py.typed0
-rw-r--r--box/shorthand_box.py69
-rw-r--r--box/shorthand_box.pyi17
-rw-r--r--debian/.gitignore1
-rw-r--r--debian/changelog15
-rw-r--r--debian/control16
-rw-r--r--debian/patches/do-not-install-binary.py20
-rw-r--r--debian/patches/series1
-rw-r--r--debian/patches/use-py3.patch179
-rw-r--r--debian/tests/control1
-rw-r--r--debian/watch2
-rw-r--r--python_box.egg-info/PKG-INFO689
-rw-r--r--python_box.egg-info/SOURCES.txt25
-rw-r--r--python_box.egg-info/requires.txt29
-rw-r--r--setup.cfg8
-rw-r--r--setup.py100
-rw-r--r--test/test_box.py1448
-rw-r--r--test/test_box_list.py258
-rw-r--r--test/test_config_box.py51
-rw-r--r--test/test_converters.py104
-rw-r--r--test/test_from_file.py49
-rw-r--r--test/test_functional_box.py928
-rw-r--r--test/test_sbox.py28
-rw-r--r--test/test_unittests_box.py62
44 files changed, 5838 insertions, 3660 deletions
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 5a171ac..c713b93 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -15,8 +15,27 @@ Code contributions:
- (sdementen)
- Brandon Gomes (bhgomes)
- Stretch (str3tch)
-- (jandelgado)
+- (pwwang)
+- Harun Tuncay (haruntuncay)
- Jeremiah Lowin (jlowin)
+- (jandelgado)
+- Jonas Irgens Kylling (jkylling)
+- Bruno Rocha (rochacbruno)
+- Noam Graetz (NoamGraetz2)
+- Fabian Affolter (fabaff)
+- Varun Madiath (vamega)
+- Jacob Hayes (JacobHayes)
+- Dominic (Yobmod)
+- Ivan Pepelnjak (ipspace)
+- Michał Górny (mgorny)
+- Serge Lu (Serge45)
+- Eric Prestat (ericpre)
+- Gabriel Mitelman Tkacz (gtkacz)
+- Muspi Merol (CNSeniorious000)
+- YISH (mokeyish)
+- Bit0r
+- Jesper Schlegel (jesperschlegel)
+
Suggestions and bug reporting:
@@ -46,6 +65,35 @@ Suggestions and bug reporting:
- (deluxghost)
- Nikolay Stanishev (nikolaystanishev)
- Craig Quiter (crizCraig)
-- Jeremiah Lowin (jlowin)
- Michael Stella (alertedsnake)
+- (FunkyLoveCow)
+- Kevin Cross (kevinhcross)
+- (Patrock)
- Tim Gates (timgates42)
+- (iordanivanov)
+- Steven McGrath (SteveMcGrath)
+- Marcelo Huerta (richieadler)
+- Wenbo Zhao (zhaowb)
+- Yordan Ivanov (iordanivanov)
+- Lei (NEOOOOOOOOOO)
+- Pymancer
+- Krishna Penukonda (tasercake)
+- J Alan Brogan (jalanb)
+- Hitz (hitengajjar)
+- David Aronchick (aronchick)
+- Alexander Kapustin (dyens)
+- Marcelo Huerta (richieadler)
+- Tim Schwenke (trallnag)
+- Marcos Dione (mdione-cloudian)
+- Varun Madiath (vamega)
+- Rexbard
+- Martin Schorfmann (schorfma)
+- aviveh21
+- Nishikant Parmar (nishikantparmariam)
+- Peter B (barmettl)
+- Ash A. (dragonpaw)
+- Коптев Роман Викторович (romikforest)
+- lei wang (191801737)
+- d00m514y3r
+- Sébastien Weber (seb5g)
+- Ward Loos (wrdls)
diff --git a/CHANGES.rst b/CHANGES.rst
index 01812ef..f74e97d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,24 +1,286 @@
Changelog
----------
+=========
+
+Version 7.3.2
+-------------
+
+* Fixing #288 default get value error when using box_dots (thanks to Sébastien Weber)
+
+Version 7.3.1
+-------------
+
+* Fixing #275 default_box_create_on_get is ignored with from_yaml (thanks to Ward Loos)
+* Fixing #285 Infinite Recursion when accessing non existent list index in a DefaultBox with box_dots (thanks to Jesper Schlegel)
+
+
+Version 7.3.0
+-------------
+
+* Adding tests and Cython releases for Python 3.13
+* Fixing #281 consistent error message about missing YAML parser (thanks to J vanBemmel)
+* Removing support for Python 3.8 as it is EOL
+
+Version 7.2.0
+-------------
+
+* Adding #266 support for accessing nested items in BoxList using numpy-style tuple indexing (thanks to Bit0r)
+* Adding tests and Cython releases for Python 3.12
+* Fixing #251 support for circular references in lists (thanks to Muspi Merol)
+* Fixing #261 altering all `__repr__` methods so that subclassing will output the correct class name (thanks to Gabriel Tkacz)
+* Fixing #267 Fix type 'int' not iterable (thanks to YISH)
+
+Version 7.1.1
+-------------
+
+* Fixing Cython optimized build deployments for linux
+
+Version 7.1.0
+-------------
+
+* Adding #255 defer ipython import for large import speed improvements (thanks to Eric Prestat)
+* Adding testing for Python 3.12
+* Fixing #253 merge_update box list merge types not populated to sub dictionaries (thanks to lei wang)
+* Fixing #257 Two test failures due to arguments having incorrect types (thanks to Michał Górny)
+* Fixing stub files to match latest code signatures
+* Removing #251 support for circular references in lists (thanks to d00m514y3r)
+* Removing support for Python 3.7 as it is EOL
+
+Version 7.0.1
+-------------
+
+* Switching off of poetry due to multiple build issues
+
+Version 7.0.0
+-------------
+
+* Adding #169 default functions with the box_instance and key parameter (thanks to Коптев Роман Викторович)
+* Adding #170 Be able to initialize with a flattened dict - by using DDBox (thanks to Ash A.)
+* Adding #192 box_dots treats all keys with periods in them as separate keys (thanks to Rexbard)
+* Adding #211 support for properties and setters in subclasses (thanks to Serge Lu and David Aronchick)
+* Adding #226 namespace to track changes to the box (thanks to Jacob Hayes)
+* Adding #236 iPython detection to prevent adding attribute lookup words (thanks to Nishikant Parmar)
+* Adding #238 allow ``|`` and ``+`` for frozen boxes (thanks to Peter B)
+* Adding new DDBox class (Default Dots Box) that is a subclass of SBox
+* Adding #242 more Cython builds using cibuildwheel (thanks to Jacob Hayes)
+* Fixing #235 how ``|`` and ``+`` updates were performed for right operations (thanks to aviveh21)
+* Fixing #234 typos (thanks to Martin Schorfmann)
+* Fixing no implicit optionals with type hinting
+* Removing Cython builds for mac until we can build universal2 wheels for arm M1 macs
+
+Version 6.1.0
+-------------
+
+* Adding Python 3.11 support
+* Adding #195 box_from_string function (thanks to Marcelo Huerta)
+* Changing the deprecated ``toml`` package with modern ``tomllib``, ``tomli`` and ``tomli-w`` usage (thanks to Michał Górny)
+* Fixing mypy __ior__ type (thanks to Jacob Hayes)
+* Fixing line endings with a pre-commit update
+* Fixing BoxList was using old style of `super` in internal code usage
+
+Version 6.0.2
+-------------
+
+* Fixing that the typing `pyi` files were not included in the manifest (thanks to Julian Torres)
+
+Version 6.0.1
+-------------
+
+* Fixing #218 Box dots would not raise KeyError on bad key (thanks to Cliff Wells)
+* Fixing #217 wording in readme overview needed updated (thanks to Julie Jones)
+
+Version 6.0.0
+-------------
+
+* Adding Cython support to greatly speed up normal Box operations on supported systems
+* Adding #161 support for access box dots with `get` and checking with `in` (thanks to scott-createplay)
+* Adding #183 support for all allowed character sets (thanks to Giulio Malventi)
+* Adding #196 support for sliceable boxes (thanks to Dias)
+* Adding #164 default_box_create_on_get toggle to disable setting box variable on get request (thanks to ipcoder)
+* Changing #208 __repr__ to produce `eval`-able text (thanks to Jeff Robbins)
+* Changing #215 support ruamel.yaml new syntax (thanks to Ivan Pepelnjak)
+* Changing `update` and `merge_update` to not use a keyword that could cause issues in rare circumstances
+* Changing internal `_safe_key` logic to be twice as fast
+* Removing support for ruamel.yaml < 0.17
+
+Version 5.4.1
+-------------
+
+* Fixing #205 setdefault behavior with box_dots (thanks to Ivan Pepelnjak)
+
+Version 5.4.0
+-------------
+
+* Adding py.typed for mypy support (thanks to Dominic)
+* Adding testing for Python 3.10-dev
+* Fixing #189 by adding mappings for mypy
+* Fixing setdefault behavior with box_dots (thanks to ipcoder)
+* Changing #193 how magic methods are handled with default_box (thanks to Rexbard)
+
+
+Version 5.3.0
+-------------
+
+* Adding support for functions to box_recast (thanks to Jacob Hayes)
+* Adding #181 support for extending or adding new items to list during `merge_update` (thanks to Marcos Dione)
+* Fixing maintain stacktrace cause for BoxKeyError and BoxValueError (thanks to Jacob Hayes)
+* Fixing #177 that emtpy yaml files raised errors instead of returning empty objects (thanks to Tim Schwenke)
+* Fixing #171 that `popitems` wasn't first checking if box was frozen (thanks to Varun Madiath)
+* Changing all files to LF line endings
+* Removing duplicate `box_recast` calls (thanks to Jacob Hayes)
+* Removing coveralls code coverage, due to repeated issues with service
+
+Version 5.2.0
+-------------
+
+* Adding checks for frozen boxes to `pop`, `popitem` and `clear` (thanks to Varun Madiath)
+* Fixing requirements-test.txt (thanks to Fabian Affolter)
+* Fixing Flake8 conflicts with black (thanks to Varun Madiath)
+* Fixing coveralls update (thanks to Varun Madiath)
+
+Version 5.1.1
+-------------
+
+* Adding testing for Python 3.9
+* Fixing #165 `box_dots` to work with `default_box`
+
+Version 5.1.0
+-------------
+
+* Adding #152 `dotted` option for `items` function (thanks to ipcoder)
+* Fixing #157 bug in box.set_default where value is dictionary, return the internal value and not detached temporary (thanks to Noam Graetz)
+* Removing warnings on import if optional libraries are missing
+
+Version 5.0.1
+-------------
+
+* Fixing #155 default box saving internal method calls and restricted options (thanks to Marcelo Huerta)
+
+Version 5.0.0
+-------------
+
+* Adding support for msgpack converters `to_msgpack` and `from_msgpack`
+* Adding #144 support for comparision of `Box` to other boxes or dicts via the `-` sub operator (thanks to Hitz)
+* Adding support to `|` union boxes like will come default in Python 3.9 from PEP 0584
+* Adding `mypy` type checking, `black` formatting and other checks on commit
+* Adding #148 new parameter `box_class` for cleaner inheritance (thanks to David Aronchick)
+* Adding #152 `dotted` option for `keys` method to return box_dots style keys (thanks to ipcoder)
+* Fixing box_dots to properly delete items from lists
+* Fixing box_dots to properly find items with dots in their key
+* Fixing that recast of subclassses of `Box` or `BoxList` were not fed box properties (thanks to Alexander Kapustin)
+* Changing #150 that sub boxes are always created to properly propagate settings and copy objects (thanks to ipcoder)
+* Changing #67 that default_box will not raise key errors on `pop` (thanks to Patrock)
+* Changing `to_csv` and `from_csv` to have same string and filename options as all other transforms
+* Changing #127 back to no required external imports, instead have extra requires like [all] (thanks to wim glenn)
+* Changing from putting all details in README.rst to a github wiki at https://github.com/cdgriffith/Box/wiki
+* Changing `BoxList.box_class` to be stored in `BoxList.box_options` dict as `box_class`
+* Changing `del` will raise `BoxKeyError`, subclass of both `KeyError` and `BoxError`
+* Removing support for single level circular references
+* Removing readthedocs generation
+* Removing overrides for `keys`, `values` and `items` which will return views again
+
+Version 4.2.3
+-------------
+
+* Fixing README.md example #149 (thanks to J Alan Brogan)
+* Changing `protected_keys` to remove magic methods from dict #146 (thanks to Krishna Penukonda)
+
+Version 4.2.2
+-------------
+
+* Fixing `default_box` doesn't first look for safe attributes before falling back to default (thanks to Pymancer)
+* Changing from TravisCI to Github Actions
+* Changing that due to `default_box` fix, `pop` or `del` no longer raise BoxKeyErrors on missing items (UNCAUGHT BUG)
+
+Version 4.2.1
+-------------
+
+* Fixing uncaught print statement (thanks to Bruno Rocha)
+* Fixing old references to `box_it_up` in the documentation
+
+
+Version 4.2.0
+-------------
+
+* Adding optimizations for speed ups to creation and inserts
+* Adding internal record of safe attributes for faster lookups, increases memory footprint for speed (thanks to Jonas Irgens Kylling)
+* Adding all additional methods specific to `Box` as protected keys
+* Fixing `merge_update` from incorrectly calling `__setattr__` which was causing a huge slowdown (thanks to Jonas Irgens Kylling)
+* Fixing `copy` and `__copy__` not copying box options
+
+
+Version 4.1.0
+-------------
+
+* Adding support for list traversal with `box_dots` (thanks to Lei)
+* Adding `BoxWarning` class to allow for the clean suppression of warnings
+* Fixing default_box_attr to accept items that evaluate to `None` (thanks to Wenbo Zhao and Yordan Ivanov)
+* Fixing `BoxList` to properly send internal box options down into new lists
+* Fixing issues with conversion and camel killer boxes not being set properly on insert
+* Changing default_box to set objects in box on lookup
+* Changing `camel_killer` to convert items on insert, which will change the keys when converted back to dict unlike before
+* Fallback to `PyYAML` if `ruamel.yaml` is not detected (thanks to wim glenn)
+* Removing official support for `pypy` as it's pickling behavior is not the same as CPython
+* Removing internal __box_heritage as it was no longer needed due to behavior update
+
+Version 4.0.4
+-------------
+
+* Fixing `get` to return None when not using default box (thanks to Jeremiah Lowin)
+
+Version 4.0.3
+-------------
+
+* Fixing non-string keys breaking when box_dots is enabled (thanks to Marcelo Huerta)
+
+Version 4.0.2
+-------------
+
+* Fixing converters to properly pass through new box arguments (thanks to Marcelo Huerta)
+
+Version 4.0.1
+-------------
+
+* Fixing setup.py for release
+* Fixing documentation link
+
+Version 4.0.0
+-------------
+
+* Adding support for retrieving items via dot notation in keys
+* Adding `box_from_file` helper function
+* Adding merge_update that acts like previous Box magic update
+* Adding support to `+` boxes together
+* Adding default_box now can support expanding on `None` placeholders (thanks to Harun Tuncay and Jeremiah Lowin)
+* Adding ability to recast specified fields (thanks to Steven McGrath)
+* Adding to_csv and from_csv capability for BoxList objects (thanks to Jiuli Gao)
+* Changing layout of project to be more object specific
+* Changing update to act like normal dict update
+* Changing to 120 line character limit
+* Changing how `safe_attr` handles unsafe characters
+* Changing all exceptions to be bases of BoxError so can always be caught with that base exception
+* Changing delete to also access converted keys (thanks to iordanivanov)
+* Changing from `PyYAML` to `ruamel.yaml` as default yaml import, aka yaml version default is 1.2 instead of 1.1
+* Removing `ordered_box` as Python 3.6+ is ordered by default
+* Removing `BoxObject` in favor of it being another module
Version 3.4.6
-~~~~~~~~~~~~~
+-------------
* Fixing allowing frozen boxes to be deep copyable (thanks to jandelgado)
Version 3.4.5
-~~~~~~~~~~~~~
+-------------
* Fixing update does not convert new sub dictionaries or lists (thanks to Michael Stella)
* Changing update to work as it used to with sub merging until major release
Version 3.4.4
-~~~~~~~~~~~~~
+-------------
* Fixing pop not properly resetting box_heritage (thanks to Jeremiah Lowin)
Version 3.4.3
-~~~~~~~~~~~~~
+-------------
* Fixing propagation of box options when adding a new list via setdefault (thanks to Stretch)
* Fixing update does not keep box_intact_types (thanks to pwwang)
@@ -26,40 +288,40 @@ Version 3.4.3
* Fixing deepcopy not copying box options (thanks to Nikolay Stanishev)
Version 3.4.2
-~~~~~~~~~~~~~
+-------------
* Adding license, changes and authors files to source distribution
Version 3.4.1
-~~~~~~~~~~~~~
+-------------
* Fixing copy of inherited classes (thanks to pwwang)
* Fixing `get` when used with default_box
Version 3.4.0
-~~~~~~~~~~~~~
+-------------
* Adding `box_intact_types` that allows preservation of selected object types (thanks to pwwang)
* Adding limitations section to readme
Version 3.3.0
-~~~~~~~~~~~~~
+-------------
* Adding `BoxObject` (thanks to Brandon Gomes)
Version 3.2.4
-~~~~~~~~~~~~~
+-------------
* Fixing recursion issue #68 when using setdefault (thanks to sdementen)
* Fixing ordered_box would make 'ordered_box_values' internal helper as key in sub boxes
Version 3.2.3
-~~~~~~~~~~~~~
+-------------
* Fixing pickling with default box (thanks to sdementen)
Version 3.2.2
-~~~~~~~~~~~~~
+-------------
* Adding hash abilities to new frozen BoxList
* Fixing hashing returned unpredictable values (thanks to cebaa)
@@ -67,14 +329,14 @@ Version 3.2.2
* Removing non-collection support for mapping and callable identification
Version 3.2.1
-~~~~~~~~~~~~~
+-------------
* Fixing pickling on python 3.7 (thanks to Martijn Pieters)
* Fixing rumel loader error (thanks to richieadler)
* Fixing frozen_box does not freeze the outermost BoxList (thanks to V.Anh Tran)
Version 3.2.0
-~~~~~~~~~~~~~
+-------------
* Adding `ordered_box` option to keep key order based on insertion (thanks to pwwang)
* Adding custom `__iter__`, `__revered__`, `pop`, `popitems`
@@ -82,13 +344,13 @@ Version 3.2.0
* Fixing non string keys not being supported correctly (thanks to Matt Wisniewski)
Version 3.1.1
-~~~~~~~~~~~~~
+-------------
* Fixing `__contains__` (thanks to Jiang Chen)
* Fixing `get` could return non box objects
Version 3.1.0
-~~~~~~~~~~~~~
+-------------
* Adding `copy` and `deepcopy` support that with return a Box object
* Adding support for customizable safe attr replacement
@@ -98,14 +360,14 @@ Version 3.1.0
* Fixing pickling
Version 3.0.1
-~~~~~~~~~~~~~
+-------------
* Fixing first level recursion errors
* Fixing spelling mistakes (thanks to John Benediktsson)
* Fixing that list insert of lists did not use the original list but create an empty one
Version 3.0.0
-~~~~~~~~~~~~~
+-------------
* Adding default object abilities with `default_box` and `default_box_attr` kwargs
* Adding `from_json` and `from_yaml` functions to both `Box` and `BoxList`
@@ -124,21 +386,21 @@ Version 3.0.0
* Removing default indent for `to_json`
Version 2.2.0
-~~~~~~~~~~~~~
+-------------
* Adding support for `ruamel.yaml` (Thanks to Alexandre Decan)
* Adding Contributing and Authors files
Version 2.1.0
-~~~~~~~~~~~~~
+-------------
* Adding `.update` and `.set_default` functionality
* Adding `dir` support
Version 2.0.0
-~~~~~~~~~~~~~
+-------------
-* Adding `BoxList` to allow for `Box`es to be recursively added to lists as well
+* Adding `BoxList` to allow for any `Box` to be recursively added to lists as well
* Adding `to_json` and `to_yaml` functions
* Changing `Box` original functionality to `LightBox`, `Box` now searches lists
* Changing `Box` callable to return keys, not values, and they are sorted
@@ -146,7 +408,7 @@ Version 2.0.0
Version 1.0.0
-~~~~~~~~~~~~~
+-------------
* Initial release, copy from `reusables.Namespace`
* Original creation, 2\13\2014
diff --git a/LICENSE b/LICENSE
index 621f227..1cd0118 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2017-2019 Chris Griffith
+Copyright (c) 2017-2023 Chris Griffith
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/MANIFEST.in b/MANIFEST.in
index a50c1d5..462abbb 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,8 @@
-include LICENSE
-include AUTHORS.rst
-include CHANGES.rst
+include LICENSE
+include AUTHORS.rst
+include CHANGES.rst
+include box/py.typed
+include box/*.c
+include box/*.so
+include box/*.pyd
+include box/*.pyi
diff --git a/PKG-INFO b/PKG-INFO
index 65fd77a..3f28655 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,474 +1,215 @@
-Metadata-Version: 2.1
-Name: python-box
-Version: 3.4.6
-Summary: Advanced Python dictionaries with dot notation access
-Home-page: https://github.com/cdgriffith/Box
-Author: Chris Griffith
-Author-email: chris@cdgriffith.com
-License: MIT
-Description: |BuildStatus| |CoverageStatus| |License| |PyPi| |DocStatus|
-
- |BoxImage|
-
- Python dictionaries with advanced dot notation access.
-
- .. code:: python
-
- from box import Box
-
- movie_data = {
- "movies": {
- "Spaceballs": {
- "imdb stars": 7.1,
- "rating": "PG",
- "length": 96,
- "director": "Mel Brooks",
- "stars": [{"name": "Mel Brooks", "imdb": "nm0000316", "role": "President Skroob"},
- {"name": "John Candy","imdb": "nm0001006", "role": "Barf"},
- {"name": "Rick Moranis", "imdb": "nm0001548", "role": "Dark Helmet"}
- ]
- },
- "Robin Hood: Men in Tights": {
- "imdb stars": 6.7,
- "rating": "PG-13",
- "length": 104,
- "director": "Mel Brooks",
- "stars": [
- {"name": "Cary Elwes", "imdb": "nm0000144", "role": "Robin Hood"},
- {"name": "Richard Lewis", "imdb": "nm0507659", "role": "Prince John"},
- {"name": "Roger Rees", "imdb": "nm0715953", "role": "Sheriff of Rottingham"},
- {"name": "Amy Yasbeck", "imdb": "nm0001865", "role": "Marian"}
- ]
- }
- }
- }
-
- # Box is a conversion_box by default, pass in `conversion_box=False` to disable that behavior
- movie_box = Box(movie_data)
-
-
- movie_box.movies.Robin_Hood_Men_in_Tights.imdb_stars
- # 6.7
-
- movie_box.movies.Spaceballs.stars[0].name
- # 'Mel Brooks'
-
- # All new dict and lists added to a Box or BoxList object are converted
- movie_box.movies.Spaceballs.stars.append({"name": "Bill Pullman", "imdb": "nm0000597", "role": "Lone Starr"})
- movie_box.movies.Spaceballs.stars[-1].role
- # 'Lone Starr'
-
- Install
- =======
-
- .. code:: bash
-
- pip install python-box
-
- Box is tested on python 2.7 and 3.4+.
- If it does not install with this command, please
- open a github issue with the error you are experiencing!
-
- If you want to be able to use the `to_yaml` functionality make sure to
- install `PyYAML` or `ruamel.yaml` as well.
-
- Overview
- ========
-
- `Box` is designed to be an easy drop in transparently replacements for
- dictionaries, thanks to Python's
- duck typing capabilities, which adds dot notation access. Any sub
- dictionaries or ones set after initiation will be automatically converted to
- a `Box` object. You can always run `.to_dict()` on it to return the object
- and all sub objects back into a regular dictionary.
-
-
- .. code:: python
-
- movie_box.movies.Spaceballs.to_dict()
- {'director': 'Mel Brooks',
- 'imdb stars': 7.1,
- 'length': 96,
- 'personal thoughts': 'On second thought, it was hilarious!',
- 'rating': 'PG',
- 'stars': [{'imdb': 'nm0000316', 'name': 'Mel Brooks', 'role': 'President Skroob'},
- {'imdb': 'nm0001006', 'name': 'John Candy', 'role': 'Barf'},
- {'imdb': 'nm0001548', 'name': 'Rick Moranis', 'role': 'Dark Helmet'},
- {'imdb': 'nm0000597', 'name': 'Bill Pullman', 'role': 'Lone Starr'}]}
-
- Box version 3 (and greater) now do sub box creation upon lookup, which means
- it is only referencing the original dict objects until they are looked up
- or modified.
-
- .. code:: python
-
- a = {"a": {"b": {"c": {}}}}
- a_box = Box(a)
- a_box
- # <Box: {'a': {'b': {'c': {}}}}>
-
- a["a"]["b"]["d"] = "2"
-
- a_box
- # <Box: {'a': {'b': {'c': {}, 'd': '2'}}}>
-
- So if you plan to keep the original dict around, make sure to box_it_up or do a deepcopy first.
-
- .. code:: python
-
- safe_box = Box(a, box_it_up=True)
- a["a"]["b"]["d"] = "2"
-
- safe_box
- # <Box: {'a': {'b': {'c': {}}}}>
-
- Limitations
- -----------
-
- `Box` is a subclass of `dict` and as such, certain keys cannot be accessed via dot notation.
- This is because names such as `keys` and `pop` have already been declared as methods, so `Box` cannot
- use it's special sauce to overwrite them. However it is still possible to have items with those names
- in the `Box` and access them like a normal dictionary, such as `my_box['keys']`.
-
- *This is as designed, and will not be changed.*
-
- The non-magic methods that exist in a `Box` are:
- `box_it_up, clear, copy, from_json, fromkeys, get, items, keys, pop, popitem, setdefault, to_dict, to_json, update, values`.
- To view an entire list of what cannot be accessed via dot notation, run the command `dir(Box())`.
-
-
- Box
- ---
-
- `Box` can be instantiated the same ways as `dict`.
-
- .. code:: python
-
- Box({'data': 2, 'count': 5})
- Box(data=2, count=5)
- Box({'data': 2, 'count': 1}, count=5)
- Box([('data', 2), ('count', 5)])
-
- # All will create
- # <Box: {'data': 2, 'count': 5}>
-
- `Box` is a subclass of `dict` which overrides some base functionality to make
- sure everything stored in the dict can be accessed as an attribute or key value.
-
- .. code:: python
-
- small_box = Box({'data': 2, 'count': 5})
- small_box.data == small_box['data'] == getattr(small_box, 'data')
-
- All dicts (and lists) added to a `Box` will be converted on lookup to a `Box` (or `BoxList`),
- allowing for recursive dot notation access.
-
- `Box` also includes helper functions to transform it back into a `dict`,
- as well as into `JSON` or `YAML` strings or files.
-
- Conversion Box
- ~~~~~~~~~~~~~~
-
- By default, Box is now a `conversion_box`
- that adds automagic attribute access for keys that could not normally be attributes.
- It can of course be disabled with the keyword argument `conversion_box=False`.
-
- .. code:: python
-
- movie_box.movies.Spaceballs["personal thoughts"] = "It was a good laugh"
- movie_box.movies.Spaceballs.personal_thoughts
- # 'It was a good laugh'
-
- movie_box.movies.Spaceballs.personal_thoughts = "On second thought, it was hilarious!"
- movie_box.movies.Spaceballs["personal thoughts"]
- # 'On second thought, it was hilarious!'
-
- # If a safe attribute matches a key exists, it will not create a new key
- movie_box.movies.Spaceballs["personal_thoughts"]
- # KeyError: 'personal_thoughts'
-
- Keys are modified in the following steps to make sure they are attribute safe:
-
- 1. Convert to string (Will encode as UTF-8 with errors ignored)
- 2. Replaces any spaces with underscores
- 3. Remove anything other than ascii letters, numbers or underscores
- 4. If the first character is an integer, it will prepend a lowercase 'x' to it
- 5. If the string is a built-in that cannot be used, it will prepend a lowercase 'x'
- 6. Removes any duplicate underscores
-
- This does not change the case of any of the keys.
-
- .. code:: python
-
- bx = Box({"321 Is a terrible Key!": "yes, really"})
- bx.x321_Is_a_terrible_Key
- # 'yes, really'
-
- These keys are not stored anywhere, and trying to modify them as an
- attribute will actually modify the underlying regular key's value.
-
- **Warning: duplicate attributes possible**
-
- If you have two keys that evaluate to the same attribute, such as "a!b" and "a?b" would become `.ab`,
- there is no way to discern between them,
- only reference or update them via standard dictionary modification.
-
-
- Frozen Box
- ~~~~~~~~~~
-
- Want to show off your box without worrying about others messing it up? Freeze it!
-
- .. code:: python
-
- frigid = Box(data={'Python': 'Rocks', 'inferior': ['java', 'cobol']}, frozen_box=True)
-
- frigid.data.Python = "Stinks"
- # box.BoxError: Box is frozen
-
- frigid.data.Python
- # 'Rocks'
-
- hash(frigid)
- # 4021666719083772260
-
- frigid.data.inferior
- # ('java', 'cobol')
-
-
- It's hashing ability is the same as the humble `tuple`, it will not be hashable
- if it has mutable objects. Speaking of `tuple`, that's what all the lists
- becomes now.
-
- Default Box
- ~~~~~~~~~~~
-
- It's boxes all the way down. At least, when you specify `default_box=True` it can be.
-
- .. code:: python
-
- empty_box = Box(default_box=True)
-
- empty_box.a.b.c.d.e.f.g
- # <Box: {}>
-
- empty_box.a.b.c.d.e.f.g = "h"
- empty_box
- # <Box: {'a': {'b': {'c': {'d': {'e': {'f': {'g': 'h'}}}}}}}>
-
- Unless you want it to be something else.
-
- .. code:: python
-
- evil_box = Box(default_box=True, default_box_attr="Something Something Something Dark Side")
-
- evil_box.not_defined
- # 'Something Something Something Dark Side'
-
- # Keep in mind it will no longer be possible to go down multiple levels
- evil_box.not_defined.something_else
- # AttributeError: 'str' object has no attribute 'something_else'
-
- `default_box_attr` will first check if it is callable, and will call the object
- if it is, otherwise it will see if has the `copy` attribute and will call that,
- lastly, will just use the provided item as is.
-
- Camel Killer Box
- ~~~~~~~~~~~~~~~~
-
- Similar to how conversion box works, allow CamelCaseKeys to be found as
- snake_case_attributes.
-
- .. code:: python
-
- cameled = Box(BadHabit="I just can't stop!", camel_killer_box=True)
-
- cameled.bad_habit
- # "I just can't stop!"
-
- Ordered Box
- ~~~~~~~~~~~
-
- Preserve the order that the keys were entered into the box. The preserved order
- will be observed while iterating over the box, or calling `.keys()`,
- `.values()` or `.items()`
-
- .. code:: python
-
- box_of_order = Box(ordered_box=True)
- box_of_order.c = 1
- box_of_order.a = 2
- box_of_order.d = 3
-
- box_of_order.keys() == ['c', 'a', 'd']
-
- Keep in mind this will not guarantee order of `**kwargs` passed to Box,
- as they are inherently not ordered until Python 3.6.
-
-
-
- BoxList
- -------
-
- To make sure all items added to lists in the box are also converted, all lists
- are covered into `BoxList`. It's possible to
- initiate these directly and use them just like a `Box`.
-
- .. code:: python
-
- from box import BoxList
-
- my_boxlist = BoxList({'item': x} for x in range(10))
- # <BoxList: [<Box: {'item': 0}>, <Box: {'item': 1}>, ...
-
- my_boxlist[5].item
- # 5
-
-
- **to_list**
-
- Transform a `BoxList` and all components back into regular `list` and `dict` items.
-
- .. code:: python
-
- my_boxlist.to_list()
- # [{'item': 0},
- # {'item': 1},
- # ...
-
- SBox
- ----
-
- Shorthand Box, aka SBox for short(hand), has the properties `json`, `yaml` and
- `dict` for faster access than the regular `to_dict()` and so on.
-
- .. code:: python
-
- from box import SBox
-
- sb = SBox(test=True)
- sb.json
- # '{"test": true}'
-
- Note that in this case, `json` has no default indent, unlike `to_json`.
-
- ConfigBox
- ---------
-
- A Box with additional handling of string manipulation generally found in
- config files.
-
- test_config.ini
-
- .. code:: ini
-
- [General]
- example=A regular string
-
- [Examples]
- my_bool=yes
- anint=234
- exampleList=234,123,234,543
- floatly=4.4
-
-
- With the combination of `reusables` and `ConfigBox` you can easily read python
- config values into python types. It supports `list`, `bool`, `int` and `float`.
-
- .. code:: python
-
- import reusables
- from box import ConfigBox
-
- config = ConfigBox(reusables.config_dict("test_config.ini"))
- # <ConfigBox: {'General': {'example': 'A regular string'},
- # 'Examples': {'my_bool': 'yes', 'anint': '234', 'examplelist': '234,123,234,543', 'floatly': '4.4'}}>
-
- config.Examples.list('examplelist')
- # ['234', '123', '234', '543']
-
- config.Examples.float('floatly')
- # 4.4
-
- BoxObject
- ---------
-
- An object wrapper with a **Box** for a **__dict__**.
-
- .. code:: python
-
- import requests
- from box import BoxObject
-
- def get_html(session, url, *args, **kwargs):
- response = session.get(url, *args, **kwargs)
- text = response.text
- response_meta = response.__dict__
- for key in tuple(filter(lambda k: k.startswith('_'), response_meta)):
- response_meta.pop(key)
- return BoxObject(text, response_meta, frozen_box=True)
-
- box_url = 'https://raw.githubusercontent.com/cdgriffith/Box/master/box.py'
- with requests.Session() as session:
- box_source = get_html(session, box_url)
-
- box_source.url
- # https://raw.githubusercontent.com/cdgriffith/Box/master/box.py
-
- box_source.status_code
- # 200
-
- box_source.raw.reason
- # OK
-
- **BoxObject** act just like objects but they secretly carry around a **Box** with
- them to store attributes. **BoxObject** are built off of **wrapt.ObjectProxy** which
- can wrap almost any python object. They protect their wrapped objects storing them in
- the **__wrapped__** attribute and keeping the original **__dict__** in
- **__wrapped__.__dict__**.
-
- See the `Wrapt Documentation`_, specifically
- the section on **ObjectProxy**, for more information.
-
-
- License
- =======
-
- MIT License, Copyright (c) 2017-2018 Chris Griffith. See LICENSE file.
-
-
- .. |BoxImage| image:: https://raw.githubusercontent.com/cdgriffith/Box/master/box_logo.png
- :target: https://github.com/cdgriffith/Box
- .. |BuildStatus| image:: https://travis-ci.org/cdgriffith/Box.png?branch=master
- :target: https://travis-ci.org/cdgriffith/Box
- .. |CoverageStatus| image:: https://img.shields.io/coveralls/cdgriffith/Box/master.svg?maxAge=2592000
- :target: https://coveralls.io/r/cdgriffith/Box?branch=master
- .. |DocStatus| image:: https://readthedocs.org/projects/box/badge/?version=latest
- :target: http://box.readthedocs.org/en/latest/index.html
- .. |PyPi| image:: https://img.shields.io/pypi/v/python-box.svg?maxAge=2592000
- :target: https://pypi.python.org/pypi/python-box/
- .. |License| image:: https://img.shields.io/pypi/l/python-box.svg
- :target: https://pypi.python.org/pypi/python-box/
- .. _`Wrapt Documentation`: https://wrapt.readthedocs.io/en/latest
-
-Platform: any
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.3
-Classifier: Programming Language :: Python :: 3.4
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Natural Language :: English
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Operating System :: OS Independent
-Classifier: Topic :: Utilities
-Classifier: Topic :: Software Development
-Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Provides-Extra: testing
+Metadata-Version: 2.2
+Name: python-box
+Version: 7.3.2
+Summary: Advanced Python dictionaries with dot notation access
+Home-page: https://github.com/cdgriffith/Box
+Author: Chris Griffith
+Author-email: chris@cdgriffith.com
+License: MIT
+Platform: any
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Natural Language :: English
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Utilities
+Classifier: Topic :: Software Development
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Requires-Python: >=3.9
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+License-File: AUTHORS.rst
+Provides-Extra: all
+Requires-Dist: ruamel.yaml>=0.17; extra == "all"
+Requires-Dist: toml; extra == "all"
+Requires-Dist: msgpack; extra == "all"
+Provides-Extra: yaml
+Requires-Dist: ruamel.yaml>=0.17; extra == "yaml"
+Provides-Extra: ruamel-yaml
+Requires-Dist: ruamel.yaml>=0.17; extra == "ruamel-yaml"
+Provides-Extra: pyyaml
+Requires-Dist: PyYAML; extra == "pyyaml"
+Provides-Extra: tomli
+Requires-Dist: tomli; python_version < "3.11" and extra == "tomli"
+Requires-Dist: tomli-w; extra == "tomli"
+Provides-Extra: toml
+Requires-Dist: toml; extra == "toml"
+Provides-Extra: msgpack
+Requires-Dist: msgpack; extra == "msgpack"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: description-content-type
+Dynamic: home-page
+Dynamic: license
+Dynamic: platform
+Dynamic: provides-extra
+Dynamic: requires-python
+Dynamic: summary
+
+|BuildStatus| |License|
+
+|BoxImage|
+
+.. code:: python
+
+ from box import Box
+
+ movie_box = Box({ "Robin Hood: Men in Tights": { "imdb stars": 6.7, "length": 104 } })
+
+ movie_box.Robin_Hood_Men_in_Tights.imdb_stars
+ # 6.7
+
+
+Box will automatically make otherwise inaccessible keys safe to access as an attribute.
+You can always pass `conversion_box=False` to `Box` to disable that behavior.
+Also, all new dict and lists added to a Box or BoxList object are converted automatically.
+
+There are over a half dozen ways to customize your Box and make it work for you.
+
+Check out the new `Box github wiki <https://github.com/cdgriffith/Box/wiki>`_ for more details and examples!
+
+Install
+=======
+
+**Version Pin Your Box!**
+
+If you aren't in the habit of version pinning your libraries, it will eventually bite you.
+Box has a `list of breaking change <https://github.com/cdgriffith/Box/wiki/Major-Version-Breaking-Changes>`_ between major versions you should always check out before updating.
+
+requirements.txt
+----------------
+
+.. code:: text
+
+ python-box[all]~=7.0
+
+As Box adheres to semantic versioning (aka API changes will only occur on between major version),
+it is best to use `Compatible release <https://www.python.org/dev/peps/pep-0440/#compatible-release>`_ matching using the `~=` clause.
+
+Install from command line
+-------------------------
+
+.. code:: bash
+
+ python -m pip install --upgrade pip
+ pip install python-box[all]~=7.0 --upgrade
+
+Install with selected dependencies
+----------------------------------
+
+Box does not install external dependencies such as yaml and toml writers. Instead you can specify which you want,
+for example, `[all]` is shorthand for:
+
+.. code:: bash
+
+ pip install python-box[ruamel.yaml,tomli_w,msgpack]~=7.0 --upgrade
+
+But you can also sub out `ruamel.yaml` for `PyYAML`.
+
+Check out `more details <https://github.com/cdgriffith/Box/wiki/Installation>`_ on installation details.
+
+Box 7 is tested on python 3.7+, if you are upgrading from previous versions, please look through
+`any breaking changes and new features <https://github.com/cdgriffith/Box/wiki/Major-Version-Breaking-Changes>`_.
+
+Optimized Version
+-----------------
+
+Box has introduced Cython optimizations for major platforms by default.
+Loading large data sets can be up to 10x faster!
+
+If you are **not** on a x86_64 supported system you will need to do some extra work to install the optimized version.
+There will be an warning of "WARNING: Cython not installed, could not optimize box" during install.
+You will need python development files, system compiler, and the python packages `Cython` and `wheel`.
+
+**Linux Example:**
+
+First make sure you have python development files installed (`python3-dev` or `python3-devel` in most repos).
+You will then need `Cython` and `wheel` installed and then install (or re-install with `--force`) `python-box`.
+
+.. code:: bash
+
+ pip install Cython wheel
+ pip install python-box[all]~=7.0 --upgrade --force
+
+If you have any issues please open a github issue with the error you are experiencing!
+
+Overview
+========
+
+`Box` is designed to be a near transparent drop in replacements for
+dictionaries that add dot notation access and other powerful feature.
+
+There are a lot of `types of boxes <https://github.com/cdgriffith/Box/wiki/Types-of-Boxes>`_
+to customize it for your needs, as well as handy `converters <https://github.com/cdgriffith/Box/wiki/Converters>`_!
+
+Keep in mind any sub dictionaries or ones set after initiation will be automatically converted to
+a `Box` object, and lists will be converted to `BoxList`, all other objects stay intact.
+
+Check out the `Quick Start <https://github.com/cdgriffith/Box/wiki/Quick-Start>`_ for more in depth details.
+
+`Box` can be instantiated the same ways as `dict`.
+
+.. code:: python
+
+ Box({'data': 2, 'count': 5})
+ Box(data=2, count=5)
+ Box({'data': 2, 'count': 1}, count=5)
+ Box([('data', 2), ('count', 5)])
+
+ # All will create
+ # <Box: {'data': 2, 'count': 5}>
+
+`Box` is a subclass of `dict` which overrides some base functionality to make
+sure everything stored in the dict can be accessed as an attribute or key value.
+
+.. code:: python
+
+ small_box = Box({'data': 2, 'count': 5})
+ small_box.data == small_box['data'] == getattr(small_box, 'data')
+
+All dicts (and lists) added to a `Box` will be converted on insertion to a `Box` (or `BoxList`),
+allowing for recursive dot notation access.
+
+`Box` also includes helper functions to transform it back into a `dict`,
+as well as into `JSON`, `YAML`, `TOML`, or `msgpack` strings or files.
+
+
+Thanks
+======
+
+A huge thank you to everyone that has given features and feedback over the years to Box! Check out everyone that has contributed_.
+
+A big thanks to Python Software Foundation, and PSF-Trademarks Committee, for official approval to use the Python logo on the `Box` logo!
+
+Also special shout-out to PythonBytes_, who featured Box on their podcast.
+
+
+License
+=======
+
+MIT License, Copyright (c) 2017-2023 Chris Griffith. See LICENSE_ file.
+
+
+.. |BoxImage| image:: https://raw.githubusercontent.com/cdgriffith/Box/master/box_logo.png
+ :target: https://github.com/cdgriffith/Box
+.. |BuildStatus| image:: https://github.com/cdgriffith/Box/workflows/Tests/badge.svg?branch=master
+ :target: https://github.com/cdgriffith/Box/actions?query=workflow%3ATests
+.. |License| image:: https://img.shields.io/pypi/l/python-box.svg
+ :target: https://pypi.python.org/pypi/python-box/
+
+.. _PythonBytes: https://pythonbytes.fm/episodes/show/19/put-your-python-dictionaries-in-a-box-and-apparently-python-is-really-wanted
+.. _contributed: AUTHORS.rst
+.. _`Wrapt Documentation`: https://wrapt.readthedocs.io/en/latest
+.. _reusables: https://github.com/cdgriffith/reusables#reusables
+.. _created: https://github.com/cdgriffith/Reusables/commit/df20de4db74371c2fedf1578096f3e29c93ccdf3#diff-e9a0f470ef3e8afb4384dc2824943048R51
+.. _LICENSE: https://github.com/cdgriffith/Box/blob/master/LICENSE
diff --git a/README.rst b/README.rst
index 4e3e639..456dc40 100644
--- a/README.rst
+++ b/README.rst
@@ -1,136 +1,103 @@
-|BuildStatus| |CoverageStatus| |License| |PyPi| |DocStatus|
+|BuildStatus| |License|
|BoxImage|
-Python dictionaries with advanced dot notation access.
-
.. code:: python
- from box import Box
-
- movie_data = {
- "movies": {
- "Spaceballs": {
- "imdb stars": 7.1,
- "rating": "PG",
- "length": 96,
- "director": "Mel Brooks",
- "stars": [{"name": "Mel Brooks", "imdb": "nm0000316", "role": "President Skroob"},
- {"name": "John Candy","imdb": "nm0001006", "role": "Barf"},
- {"name": "Rick Moranis", "imdb": "nm0001548", "role": "Dark Helmet"}
- ]
- },
- "Robin Hood: Men in Tights": {
- "imdb stars": 6.7,
- "rating": "PG-13",
- "length": 104,
- "director": "Mel Brooks",
- "stars": [
- {"name": "Cary Elwes", "imdb": "nm0000144", "role": "Robin Hood"},
- {"name": "Richard Lewis", "imdb": "nm0507659", "role": "Prince John"},
- {"name": "Roger Rees", "imdb": "nm0715953", "role": "Sheriff of Rottingham"},
- {"name": "Amy Yasbeck", "imdb": "nm0001865", "role": "Marian"}
- ]
- }
- }
- }
-
- # Box is a conversion_box by default, pass in `conversion_box=False` to disable that behavior
- movie_box = Box(movie_data)
-
-
- movie_box.movies.Robin_Hood_Men_in_Tights.imdb_stars
- # 6.7
-
- movie_box.movies.Spaceballs.stars[0].name
- # 'Mel Brooks'
-
- # All new dict and lists added to a Box or BoxList object are converted
- movie_box.movies.Spaceballs.stars.append({"name": "Bill Pullman", "imdb": "nm0000597", "role": "Lone Starr"})
- movie_box.movies.Spaceballs.stars[-1].role
- # 'Lone Starr'
+ from box import Box
+
+ movie_box = Box({ "Robin Hood: Men in Tights": { "imdb stars": 6.7, "length": 104 } })
+
+ movie_box.Robin_Hood_Men_in_Tights.imdb_stars
+ # 6.7
+
+
+Box will automatically make otherwise inaccessible keys safe to access as an attribute.
+You can always pass `conversion_box=False` to `Box` to disable that behavior.
+Also, all new dict and lists added to a Box or BoxList object are converted automatically.
+
+There are over a half dozen ways to customize your Box and make it work for you.
+
+Check out the new `Box github wiki <https://github.com/cdgriffith/Box/wiki>`_ for more details and examples!
Install
=======
-.. code:: bash
+**Version Pin Your Box!**
- pip install python-box
+If you aren't in the habit of version pinning your libraries, it will eventually bite you.
+Box has a `list of breaking change <https://github.com/cdgriffith/Box/wiki/Major-Version-Breaking-Changes>`_ between major versions you should always check out before updating.
-Box is tested on python 2.7 and 3.4+.
-If it does not install with this command, please
-open a github issue with the error you are experiencing!
+requirements.txt
+----------------
-If you want to be able to use the `to_yaml` functionality make sure to
-install `PyYAML` or `ruamel.yaml` as well.
+.. code:: text
-Overview
-========
+ python-box[all]~=7.0
-`Box` is designed to be an easy drop in transparently replacements for
-dictionaries, thanks to Python's
-duck typing capabilities, which adds dot notation access. Any sub
-dictionaries or ones set after initiation will be automatically converted to
-a `Box` object. You can always run `.to_dict()` on it to return the object
-and all sub objects back into a regular dictionary.
+As Box adheres to semantic versioning (aka API changes will only occur on between major version),
+it is best to use `Compatible release <https://www.python.org/dev/peps/pep-0440/#compatible-release>`_ matching using the `~=` clause.
+Install from command line
+-------------------------
-.. code:: python
+.. code:: bash
- movie_box.movies.Spaceballs.to_dict()
- {'director': 'Mel Brooks',
- 'imdb stars': 7.1,
- 'length': 96,
- 'personal thoughts': 'On second thought, it was hilarious!',
- 'rating': 'PG',
- 'stars': [{'imdb': 'nm0000316', 'name': 'Mel Brooks', 'role': 'President Skroob'},
- {'imdb': 'nm0001006', 'name': 'John Candy', 'role': 'Barf'},
- {'imdb': 'nm0001548', 'name': 'Rick Moranis', 'role': 'Dark Helmet'},
- {'imdb': 'nm0000597', 'name': 'Bill Pullman', 'role': 'Lone Starr'}]}
-
-Box version 3 (and greater) now do sub box creation upon lookup, which means
-it is only referencing the original dict objects until they are looked up
-or modified.
+ python -m pip install --upgrade pip
+ pip install python-box[all]~=7.0 --upgrade
-.. code:: python
+Install with selected dependencies
+----------------------------------
- a = {"a": {"b": {"c": {}}}}
- a_box = Box(a)
- a_box
- # <Box: {'a': {'b': {'c': {}}}}>
+Box does not install external dependencies such as yaml and toml writers. Instead you can specify which you want,
+for example, `[all]` is shorthand for:
- a["a"]["b"]["d"] = "2"
+.. code:: bash
- a_box
- # <Box: {'a': {'b': {'c': {}, 'd': '2'}}}>
+ pip install python-box[ruamel.yaml,tomli_w,msgpack]~=7.0 --upgrade
-So if you plan to keep the original dict around, make sure to box_it_up or do a deepcopy first.
+But you can also sub out `ruamel.yaml` for `PyYAML`.
-.. code:: python
+Check out `more details <https://github.com/cdgriffith/Box/wiki/Installation>`_ on installation details.
- safe_box = Box(a, box_it_up=True)
- a["a"]["b"]["d"] = "2"
+Box 7 is tested on python 3.7+, if you are upgrading from previous versions, please look through
+`any breaking changes and new features <https://github.com/cdgriffith/Box/wiki/Major-Version-Breaking-Changes>`_.
- safe_box
- # <Box: {'a': {'b': {'c': {}}}}>
+Optimized Version
+-----------------
-Limitations
------------
+Box has introduced Cython optimizations for major platforms by default.
+Loading large data sets can be up to 10x faster!
-`Box` is a subclass of `dict` and as such, certain keys cannot be accessed via dot notation.
-This is because names such as `keys` and `pop` have already been declared as methods, so `Box` cannot
-use it's special sauce to overwrite them. However it is still possible to have items with those names
-in the `Box` and access them like a normal dictionary, such as `my_box['keys']`.
+If you are **not** on a x86_64 supported system you will need to do some extra work to install the optimized version.
+There will be an warning of "WARNING: Cython not installed, could not optimize box" during install.
+You will need python development files, system compiler, and the python packages `Cython` and `wheel`.
-*This is as designed, and will not be changed.*
+**Linux Example:**
-The non-magic methods that exist in a `Box` are:
-`box_it_up, clear, copy, from_json, fromkeys, get, items, keys, pop, popitem, setdefault, to_dict, to_json, update, values`.
-To view an entire list of what cannot be accessed via dot notation, run the command `dir(Box())`.
+First make sure you have python development files installed (`python3-dev` or `python3-devel` in most repos).
+You will then need `Cython` and `wheel` installed and then install (or re-install with `--force`) `python-box`.
+.. code:: bash
-Box
----
+ pip install Cython wheel
+ pip install python-box[all]~=7.0 --upgrade --force
+
+If you have any issues please open a github issue with the error you are experiencing!
+
+Overview
+========
+
+`Box` is designed to be a near transparent drop in replacements for
+dictionaries that add dot notation access and other powerful feature.
+
+There are a lot of `types of boxes <https://github.com/cdgriffith/Box/wiki/Types-of-Boxes>`_
+to customize it for your needs, as well as handy `converters <https://github.com/cdgriffith/Box/wiki/Converters>`_!
+
+Keep in mind any sub dictionaries or ones set after initiation will be automatically converted to
+a `Box` object, and lists will be converted to `BoxList`, all other objects stay intact.
+
+Check out the `Quick Start <https://github.com/cdgriffith/Box/wiki/Quick-Start>`_ for more in depth details.
`Box` can be instantiated the same ways as `dict`.
@@ -152,293 +119,39 @@ sure everything stored in the dict can be accessed as an attribute or key value.
small_box = Box({'data': 2, 'count': 5})
small_box.data == small_box['data'] == getattr(small_box, 'data')
-All dicts (and lists) added to a `Box` will be converted on lookup to a `Box` (or `BoxList`),
+All dicts (and lists) added to a `Box` will be converted on insertion to a `Box` (or `BoxList`),
allowing for recursive dot notation access.
`Box` also includes helper functions to transform it back into a `dict`,
-as well as into `JSON` or `YAML` strings or files.
-
-Conversion Box
-~~~~~~~~~~~~~~
-
-By default, Box is now a `conversion_box`
-that adds automagic attribute access for keys that could not normally be attributes.
-It can of course be disabled with the keyword argument `conversion_box=False`.
-
-.. code:: python
-
- movie_box.movies.Spaceballs["personal thoughts"] = "It was a good laugh"
- movie_box.movies.Spaceballs.personal_thoughts
- # 'It was a good laugh'
-
- movie_box.movies.Spaceballs.personal_thoughts = "On second thought, it was hilarious!"
- movie_box.movies.Spaceballs["personal thoughts"]
- # 'On second thought, it was hilarious!'
-
- # If a safe attribute matches a key exists, it will not create a new key
- movie_box.movies.Spaceballs["personal_thoughts"]
- # KeyError: 'personal_thoughts'
-
-Keys are modified in the following steps to make sure they are attribute safe:
-
-1. Convert to string (Will encode as UTF-8 with errors ignored)
-2. Replaces any spaces with underscores
-3. Remove anything other than ascii letters, numbers or underscores
-4. If the first character is an integer, it will prepend a lowercase 'x' to it
-5. If the string is a built-in that cannot be used, it will prepend a lowercase 'x'
-6. Removes any duplicate underscores
-
-This does not change the case of any of the keys.
-
-.. code:: python
-
- bx = Box({"321 Is a terrible Key!": "yes, really"})
- bx.x321_Is_a_terrible_Key
- # 'yes, really'
-
-These keys are not stored anywhere, and trying to modify them as an
-attribute will actually modify the underlying regular key's value.
-
-**Warning: duplicate attributes possible**
-
-If you have two keys that evaluate to the same attribute, such as "a!b" and "a?b" would become `.ab`,
-there is no way to discern between them,
-only reference or update them via standard dictionary modification.
-
-
-Frozen Box
-~~~~~~~~~~
-
-Want to show off your box without worrying about others messing it up? Freeze it!
-
-.. code:: python
-
- frigid = Box(data={'Python': 'Rocks', 'inferior': ['java', 'cobol']}, frozen_box=True)
-
- frigid.data.Python = "Stinks"
- # box.BoxError: Box is frozen
-
- frigid.data.Python
- # 'Rocks'
-
- hash(frigid)
- # 4021666719083772260
-
- frigid.data.inferior
- # ('java', 'cobol')
-
-
-It's hashing ability is the same as the humble `tuple`, it will not be hashable
-if it has mutable objects. Speaking of `tuple`, that's what all the lists
-becomes now.
-
-Default Box
-~~~~~~~~~~~
-
-It's boxes all the way down. At least, when you specify `default_box=True` it can be.
-
-.. code:: python
-
- empty_box = Box(default_box=True)
-
- empty_box.a.b.c.d.e.f.g
- # <Box: {}>
-
- empty_box.a.b.c.d.e.f.g = "h"
- empty_box
- # <Box: {'a': {'b': {'c': {'d': {'e': {'f': {'g': 'h'}}}}}}}>
-
-Unless you want it to be something else.
-
-.. code:: python
+as well as into `JSON`, `YAML`, `TOML`, or `msgpack` strings or files.
- evil_box = Box(default_box=True, default_box_attr="Something Something Something Dark Side")
- evil_box.not_defined
- # 'Something Something Something Dark Side'
+Thanks
+======
- # Keep in mind it will no longer be possible to go down multiple levels
- evil_box.not_defined.something_else
- # AttributeError: 'str' object has no attribute 'something_else'
+A huge thank you to everyone that has given features and feedback over the years to Box! Check out everyone that has contributed_.
-`default_box_attr` will first check if it is callable, and will call the object
-if it is, otherwise it will see if has the `copy` attribute and will call that,
-lastly, will just use the provided item as is.
+A big thanks to Python Software Foundation, and PSF-Trademarks Committee, for official approval to use the Python logo on the `Box` logo!
-Camel Killer Box
-~~~~~~~~~~~~~~~~
-
-Similar to how conversion box works, allow CamelCaseKeys to be found as
-snake_case_attributes.
-
-.. code:: python
-
- cameled = Box(BadHabit="I just can't stop!", camel_killer_box=True)
-
- cameled.bad_habit
- # "I just can't stop!"
-
-Ordered Box
-~~~~~~~~~~~
-
-Preserve the order that the keys were entered into the box. The preserved order
-will be observed while iterating over the box, or calling `.keys()`,
-`.values()` or `.items()`
-
-.. code:: python
-
- box_of_order = Box(ordered_box=True)
- box_of_order.c = 1
- box_of_order.a = 2
- box_of_order.d = 3
-
- box_of_order.keys() == ['c', 'a', 'd']
-
-Keep in mind this will not guarantee order of `**kwargs` passed to Box,
-as they are inherently not ordered until Python 3.6.
-
-
-
-BoxList
--------
-
-To make sure all items added to lists in the box are also converted, all lists
-are covered into `BoxList`. It's possible to
-initiate these directly and use them just like a `Box`.
-
-.. code:: python
-
- from box import BoxList
-
- my_boxlist = BoxList({'item': x} for x in range(10))
- # <BoxList: [<Box: {'item': 0}>, <Box: {'item': 1}>, ...
-
- my_boxlist[5].item
- # 5
-
-
-**to_list**
-
-Transform a `BoxList` and all components back into regular `list` and `dict` items.
-
-.. code:: python
-
- my_boxlist.to_list()
- # [{'item': 0},
- # {'item': 1},
- # ...
-
-SBox
-----
-
-Shorthand Box, aka SBox for short(hand), has the properties `json`, `yaml` and
-`dict` for faster access than the regular `to_dict()` and so on.
-
-.. code:: python
-
- from box import SBox
-
- sb = SBox(test=True)
- sb.json
- # '{"test": true}'
-
-Note that in this case, `json` has no default indent, unlike `to_json`.
-
-ConfigBox
----------
-
-A Box with additional handling of string manipulation generally found in
-config files.
-
-test_config.ini
-
-.. code:: ini
-
- [General]
- example=A regular string
-
- [Examples]
- my_bool=yes
- anint=234
- exampleList=234,123,234,543
- floatly=4.4
-
-
-With the combination of `reusables` and `ConfigBox` you can easily read python
-config values into python types. It supports `list`, `bool`, `int` and `float`.
-
-.. code:: python
-
- import reusables
- from box import ConfigBox
-
- config = ConfigBox(reusables.config_dict("test_config.ini"))
- # <ConfigBox: {'General': {'example': 'A regular string'},
- # 'Examples': {'my_bool': 'yes', 'anint': '234', 'examplelist': '234,123,234,543', 'floatly': '4.4'}}>
-
- config.Examples.list('examplelist')
- # ['234', '123', '234', '543']
-
- config.Examples.float('floatly')
- # 4.4
-
-BoxObject
----------
-
-An object wrapper with a **Box** for a **__dict__**.
-
-.. code:: python
-
- import requests
- from box import BoxObject
-
- def get_html(session, url, *args, **kwargs):
- response = session.get(url, *args, **kwargs)
- text = response.text
- response_meta = response.__dict__
- for key in tuple(filter(lambda k: k.startswith('_'), response_meta)):
- response_meta.pop(key)
- return BoxObject(text, response_meta, frozen_box=True)
-
- box_url = 'https://raw.githubusercontent.com/cdgriffith/Box/master/box.py'
- with requests.Session() as session:
- box_source = get_html(session, box_url)
-
- box_source.url
- # https://raw.githubusercontent.com/cdgriffith/Box/master/box.py
-
- box_source.status_code
- # 200
-
- box_source.raw.reason
- # OK
-
-**BoxObject** act just like objects but they secretly carry around a **Box** with
-them to store attributes. **BoxObject** are built off of **wrapt.ObjectProxy** which
-can wrap almost any python object. They protect their wrapped objects storing them in
-the **__wrapped__** attribute and keeping the original **__dict__** in
-**__wrapped__.__dict__**.
-
-See the `Wrapt Documentation`_, specifically
-the section on **ObjectProxy**, for more information.
+Also special shout-out to PythonBytes_, who featured Box on their podcast.
License
=======
-MIT License, Copyright (c) 2017-2018 Chris Griffith. See LICENSE file.
+MIT License, Copyright (c) 2017-2023 Chris Griffith. See LICENSE_ file.
.. |BoxImage| image:: https://raw.githubusercontent.com/cdgriffith/Box/master/box_logo.png
:target: https://github.com/cdgriffith/Box
-.. |BuildStatus| image:: https://travis-ci.org/cdgriffith/Box.png?branch=master
- :target: https://travis-ci.org/cdgriffith/Box
-.. |CoverageStatus| image:: https://img.shields.io/coveralls/cdgriffith/Box/master.svg?maxAge=2592000
- :target: https://coveralls.io/r/cdgriffith/Box?branch=master
-.. |DocStatus| image:: https://readthedocs.org/projects/box/badge/?version=latest
- :target: http://box.readthedocs.org/en/latest/index.html
-.. |PyPi| image:: https://img.shields.io/pypi/v/python-box.svg?maxAge=2592000
- :target: https://pypi.python.org/pypi/python-box/
+.. |BuildStatus| image:: https://github.com/cdgriffith/Box/workflows/Tests/badge.svg?branch=master
+ :target: https://github.com/cdgriffith/Box/actions?query=workflow%3ATests
.. |License| image:: https://img.shields.io/pypi/l/python-box.svg
:target: https://pypi.python.org/pypi/python-box/
+
+.. _PythonBytes: https://pythonbytes.fm/episodes/show/19/put-your-python-dictionaries-in-a-box-and-apparently-python-is-really-wanted
+.. _contributed: AUTHORS.rst
.. _`Wrapt Documentation`: https://wrapt.readthedocs.io/en/latest
+.. _reusables: https://github.com/cdgriffith/reusables#reusables
+.. _created: https://github.com/cdgriffith/Reusables/commit/df20de4db74371c2fedf1578096f3e29c93ccdf3#diff-e9a0f470ef3e8afb4384dc2824943048R51
+.. _LICENSE: https://github.com/cdgriffith/Box/blob/master/LICENSE
diff --git a/box.py b/box.py
deleted file mode 100644
index 94e3def..0000000
--- a/box.py
+++ /dev/null
@@ -1,1217 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: UTF-8 -*-
-#
-# Copyright (c) 2017-2019 - Chris Griffith - MIT License
-"""
-Improved dictionary access through dot notation with additional tools.
-"""
-import string
-import sys
-import json
-import re
-import copy
-from keyword import kwlist
-import warnings
-
-try:
- from collections.abc import Iterable, Mapping, Callable
-except ImportError:
- from collections import Iterable, Mapping, Callable
-
-yaml_support = True
-
-try:
- import yaml
-except ImportError:
- try:
- import ruamel.yaml as yaml
- except ImportError:
- yaml = None
- yaml_support = False
-
-wrapt_support = True
-
-try:
- import wrapt
-except ImportError:
- wrapt = None
- wrapt_support = False
-
-if sys.version_info >= (3, 0):
- basestring = str
-else:
- from io import open
-
-__all__ = ['Box', 'ConfigBox', 'BoxList', 'SBox',
- 'BoxError', 'BoxKeyError']
-__author__ = 'Chris Griffith'
-__version__ = '3.4.6'
-
-BOX_PARAMETERS = ('default_box', 'default_box_attr', 'conversion_box',
- 'frozen_box', 'camel_killer_box', 'box_it_up',
- 'box_safe_prefix', 'box_duplicates', 'ordered_box',
- 'box_intact_types')
-
-_first_cap_re = re.compile('(.)([A-Z][a-z]+)')
-_all_cap_re = re.compile('([a-z0-9])([A-Z])')
-
-
-class BoxError(Exception):
- """Non standard dictionary exceptions"""
-
-
-class BoxKeyError(BoxError, KeyError, AttributeError):
- """Key does not exist"""
-
-
-# Abstract converter functions for use in any Box class
-
-
-def _to_json(obj, filename=None,
- encoding="utf-8", errors="strict", **json_kwargs):
- json_dump = json.dumps(obj,
- ensure_ascii=False, **json_kwargs)
- if filename:
- with open(filename, 'w', encoding=encoding, errors=errors) as f:
- f.write(json_dump if sys.version_info >= (3, 0) else
- json_dump.decode("utf-8"))
- else:
- return json_dump
-
-
-def _from_json(json_string=None, filename=None,
- encoding="utf-8", errors="strict", multiline=False, **kwargs):
- if filename:
- with open(filename, 'r', encoding=encoding, errors=errors) as f:
- if multiline:
- data = [json.loads(line.strip(), **kwargs) for line in f
- if line.strip() and not line.strip().startswith("#")]
- else:
- data = json.load(f, **kwargs)
- elif json_string:
- data = json.loads(json_string, **kwargs)
- else:
- raise BoxError('from_json requires a string or filename')
- return data
-
-
-def _to_yaml(obj, filename=None, default_flow_style=False,
- encoding="utf-8", errors="strict",
- **yaml_kwargs):
- if filename:
- with open(filename, 'w',
- encoding=encoding, errors=errors) as f:
- yaml.dump(obj, stream=f,
- default_flow_style=default_flow_style,
- **yaml_kwargs)
- else:
- return yaml.dump(obj,
- default_flow_style=default_flow_style,
- **yaml_kwargs)
-
-
-def _from_yaml(yaml_string=None, filename=None,
- encoding="utf-8", errors="strict",
- **kwargs):
- if filename:
- with open(filename, 'r',
- encoding=encoding, errors=errors) as f:
- data = yaml.load(f, **kwargs)
- elif yaml_string:
- data = yaml.load(yaml_string, **kwargs)
- else:
- raise BoxError('from_yaml requires a string or filename')
- return data
-
-
-# Helper functions
-
-
-def _safe_key(key):
- try:
- return str(key)
- except UnicodeEncodeError:
- return key.encode("utf-8", "ignore")
-
-
-def _safe_attr(attr, camel_killer=False, replacement_char='x'):
- """Convert a key into something that is accessible as an attribute"""
- allowed = string.ascii_letters + string.digits + '_'
-
- attr = _safe_key(attr)
-
- if camel_killer:
- attr = _camel_killer(attr)
-
- attr = attr.replace(' ', '_')
-
- out = ''
- for character in attr:
- out += character if character in allowed else "_"
- out = out.strip("_")
-
- try:
- int(out[0])
- except (ValueError, IndexError):
- pass
- else:
- out = '{0}{1}'.format(replacement_char, out)
-
- if out in kwlist:
- out = '{0}{1}'.format(replacement_char, out)
-
- return re.sub('_+', '_', out)
-
-
-def _camel_killer(attr):
- """ CamelKiller, qu'est-ce que c'est? """
- try:
- attr = str(attr)
- except UnicodeEncodeError:
- attr = attr.encode("utf-8", "ignore")
-
- s1 = _first_cap_re.sub(r'\1_\2', attr)
- s2 = _all_cap_re.sub(r'\1_\2', s1)
- return re.sub('_+', '_', s2.casefold() if hasattr(s2, 'casefold')
- else s2.lower())
-
-
-def _recursive_tuples(iterable, box_class, recreate_tuples=False, **kwargs):
- out_list = []
- for i in iterable:
- if isinstance(i, dict):
- out_list.append(box_class(i, **kwargs))
- elif isinstance(i, list) or (recreate_tuples and isinstance(i, tuple)):
- out_list.append(_recursive_tuples(i, box_class,
- recreate_tuples, **kwargs))
- else:
- out_list.append(i)
- return tuple(out_list)
-
-
-def _conversion_checks(item, keys, box_config, check_only=False,
- pre_check=False):
- """
- Internal use for checking if a duplicate safe attribute already exists
-
- :param item: Item to see if a dup exists
- :param keys: Keys to check against
- :param box_config: Easier to pass in than ask for specific items
- :param check_only: Don't bother doing the conversion work
- :param pre_check: Need to add the item to the list of keys to check
- :return: the original unmodified key, if exists and not check_only
- """
- if box_config['box_duplicates'] != 'ignore':
- if pre_check:
- keys = list(keys) + [item]
-
- key_list = [(k,
- _safe_attr(k, camel_killer=box_config['camel_killer_box'],
- replacement_char=box_config['box_safe_prefix']
- )) for k in keys]
- if len(key_list) > len(set(x[1] for x in key_list)):
- seen = set()
- dups = set()
- for x in key_list:
- if x[1] in seen:
- dups.add("{0}({1})".format(x[0], x[1]))
- seen.add(x[1])
- if box_config['box_duplicates'].startswith("warn"):
- warnings.warn('Duplicate conversion attributes exist: '
- '{0}'.format(dups))
- else:
- raise BoxError('Duplicate conversion attributes exist: '
- '{0}'.format(dups))
- if check_only:
- return
- # This way will be slower for warnings, as it will have double work
- # But faster for the default 'ignore'
- for k in keys:
- if item == _safe_attr(k, camel_killer=box_config['camel_killer_box'],
- replacement_char=box_config['box_safe_prefix']):
- return k
-
-
-def _get_box_config(cls, kwargs):
- return {
- # Internal use only
- '__converted': set(),
- '__box_heritage': kwargs.pop('__box_heritage', None),
- '__created': False,
- '__ordered_box_values': [],
- # Can be changed by user after box creation
- 'default_box': kwargs.pop('default_box', False),
- 'default_box_attr': kwargs.pop('default_box_attr', cls),
- 'conversion_box': kwargs.pop('conversion_box', True),
- 'box_safe_prefix': kwargs.pop('box_safe_prefix', 'x'),
- 'frozen_box': kwargs.pop('frozen_box', False),
- 'camel_killer_box': kwargs.pop('camel_killer_box', False),
- 'modify_tuples_box': kwargs.pop('modify_tuples_box', False),
- 'box_duplicates': kwargs.pop('box_duplicates', 'ignore'),
- 'ordered_box': kwargs.pop('ordered_box', False),
- 'box_intact_types': tuple(kwargs.pop('box_intact_types', ())),
- }
-
-
-class Box(dict):
- """
- Improved dictionary access through dot notation with additional tools.
-
- :param default_box: Similar to defaultdict, return a default value
- :param default_box_attr: Specify the default replacement.
- WARNING: If this is not the default 'Box', it will not be recursive
- :param frozen_box: After creation, the box cannot be modified
- :param camel_killer_box: Convert CamelCase to snake_case
- :param conversion_box: Check for near matching keys as attributes
- :param modify_tuples_box: Recreate incoming tuples with dicts into Boxes
- :param box_it_up: Recursively create all Boxes from the start
- :param box_safe_prefix: Conversion box prefix for unsafe attributes
- :param box_duplicates: "ignore", "error" or "warn" when duplicates exists
- in a conversion_box
- :param ordered_box: Preserve the order of keys entered into the box
- :param box_intact_types: Keep data with given types intact
- """
-
- _protected_keys = dir({}) + ['to_dict', 'tree_view', 'to_json', 'to_yaml',
- 'from_yaml', 'from_json']
-
- def __new__(cls, *args, **kwargs):
- """
- Due to the way pickling works in python 3, we need to make sure
- the box config is created as early as possible.
- """
- obj = super(Box, cls).__new__(cls, *args, **kwargs)
- obj._box_config = _get_box_config(cls, kwargs)
- return obj
-
- def __init__(self, *args, **kwargs):
- self._box_config = _get_box_config(self.__class__, kwargs)
- if self._box_config['ordered_box']:
- self._box_config['__ordered_box_values'] = []
- if (not self._box_config['conversion_box'] and
- self._box_config['box_duplicates'] != "ignore"):
- raise BoxError('box_duplicates are only for conversion_boxes')
- if len(args) == 1:
- if isinstance(args[0], basestring):
- raise ValueError('Cannot extrapolate Box from string')
- if isinstance(args[0], Mapping):
- for k, v in args[0].items():
- if v is args[0]:
- v = self
- self[k] = v
- self.__add_ordered(k)
- elif isinstance(args[0], Iterable):
- for k, v in args[0]:
- self[k] = v
- self.__add_ordered(k)
-
- else:
- raise ValueError('First argument must be mapping or iterable')
- elif args:
- raise TypeError('Box expected at most 1 argument, '
- 'got {0}'.format(len(args)))
-
- box_it = kwargs.pop('box_it_up', False)
- for k, v in kwargs.items():
- if args and isinstance(args[0], Mapping) and v is args[0]:
- v = self
- self[k] = v
- self.__add_ordered(k)
-
- if (self._box_config['frozen_box'] or box_it or
- self._box_config['box_duplicates'] != 'ignore'):
- self.box_it_up()
-
- self._box_config['__created'] = True
-
- def __add_ordered(self, key):
- if (self._box_config['ordered_box'] and
- key not in self._box_config['__ordered_box_values']):
- self._box_config['__ordered_box_values'].append(key)
-
- def box_it_up(self):
- """
- Perform value lookup for all items in current dictionary,
- generating all sub Box objects, while also running `box_it_up` on
- any of those sub box objects.
- """
- for k in self:
- _conversion_checks(k, self.keys(), self._box_config,
- check_only=True)
- if self[k] is not self and hasattr(self[k], 'box_it_up'):
- self[k].box_it_up()
-
- def __hash__(self):
- if self._box_config['frozen_box']:
- hashing = 54321
- for item in self.items():
- hashing ^= hash(item)
- return hashing
- raise TypeError("unhashable type: 'Box'")
-
- def __dir__(self):
- allowed = string.ascii_letters + string.digits + '_'
- kill_camel = self._box_config['camel_killer_box']
- items = set(dir(dict) + ['to_dict', 'to_json',
- 'from_json', 'box_it_up'])
- # Only show items accessible by dot notation
- for key in self.keys():
- key = _safe_key(key)
- if (' ' not in key and key[0] not in string.digits and
- key not in kwlist):
- for letter in key:
- if letter not in allowed:
- break
- else:
- items.add(key)
-
- for key in self.keys():
- key = _safe_key(key)
- if key not in items:
- if self._box_config['conversion_box']:
- key = _safe_attr(key, camel_killer=kill_camel,
- replacement_char=self._box_config[
- 'box_safe_prefix'])
- if key:
- items.add(key)
- if kill_camel:
- snake_key = _camel_killer(key)
- if snake_key:
- items.remove(key)
- items.add(snake_key)
-
- if yaml_support:
- items.add('to_yaml')
- items.add('from_yaml')
-
- return list(items)
-
- def get(self, key, default=None):
- if key not in self:
- if default is None and self._box_config['default_box']:
- return self.__get_default(key)
- if isinstance(default, dict) and not isinstance(default, Box):
- return Box(default)
- if isinstance(default, list) and not isinstance(default, BoxList):
- return BoxList(default)
- return default
- return self[key]
-
- def copy(self):
- return Box(super(Box, self).copy())
-
- def __copy__(self):
- return Box(super(Box, self).copy())
-
- def __deepcopy__(self, memodict=None):
- frozen = self._box_config['frozen_box']
- config = self.__box_config()
- config['frozen_box'] = False
- out = self.__class__(**config)
- memodict = memodict or {}
- memodict[id(self)] = out
- for k, v in self.items():
- out[copy.deepcopy(k, memodict)] = copy.deepcopy(v, memodict)
- out._box_config['frozen_box'] = frozen
- return out
-
- def __setstate__(self, state):
- self._box_config = state['_box_config']
- self.__dict__.update(state)
-
- def __getitem__(self, item, _ignore_default=False):
- try:
- value = super(Box, self).__getitem__(item)
- except KeyError as err:
- if item == '_box_config':
- raise BoxKeyError('_box_config should only exist as an '
- 'attribute and is never defaulted')
- if self._box_config['default_box'] and not _ignore_default:
- return self.__get_default(item)
- raise BoxKeyError(str(err))
- else:
- return self.__convert_and_store(item, value)
-
- def keys(self):
- if self._box_config['ordered_box']:
- return self._box_config['__ordered_box_values']
- return super(Box, self).keys()
-
- def values(self):
- return [self[x] for x in self.keys()]
-
- def items(self):
- return [(x, self[x]) for x in self.keys()]
-
- def __get_default(self, item):
- default_value = self._box_config['default_box_attr']
- if default_value is self.__class__:
- return self.__class__(__box_heritage=(self, item),
- **self.__box_config())
- elif isinstance(default_value, Callable):
- return default_value()
- elif hasattr(default_value, 'copy'):
- return default_value.copy()
- return default_value
-
- def __box_config(self):
- out = {}
- for k, v in self._box_config.copy().items():
- if not k.startswith("__"):
- out[k] = v
- return out
-
- def __convert_and_store(self, item, value):
- if (item in self._box_config['__converted'] or
- (self._box_config['box_intact_types'] and
- isinstance(value, self._box_config['box_intact_types']))):
- return value
- if isinstance(value, dict) and not isinstance(value, Box):
- value = self.__class__(value, __box_heritage=(self, item),
- **self.__box_config())
- self[item] = value
- elif isinstance(value, list) and not isinstance(value, BoxList):
- if self._box_config['frozen_box']:
- value = _recursive_tuples(value, self.__class__,
- recreate_tuples=self._box_config[
- 'modify_tuples_box'],
- __box_heritage=(self, item),
- **self.__box_config())
- else:
- value = BoxList(value, __box_heritage=(self, item),
- box_class=self.__class__,
- **self.__box_config())
- self[item] = value
- elif (self._box_config['modify_tuples_box'] and
- isinstance(value, tuple)):
- value = _recursive_tuples(value, self.__class__,
- recreate_tuples=True,
- __box_heritage=(self, item),
- **self.__box_config())
- self[item] = value
- self._box_config['__converted'].add(item)
- return value
-
- def __create_lineage(self):
- if (self._box_config['__box_heritage'] and
- self._box_config['__created']):
- past, item = self._box_config['__box_heritage']
- if not past[item]:
- past[item] = self
- self._box_config['__box_heritage'] = None
-
- def __getattr__(self, item):
- try:
- try:
- value = self.__getitem__(item, _ignore_default=True)
- except KeyError:
- value = object.__getattribute__(self, item)
- except AttributeError as err:
- if item == "__getstate__":
- raise AttributeError(item)
- if item == '_box_config':
- raise BoxError('_box_config key must exist')
- kill_camel = self._box_config['camel_killer_box']
- if self._box_config['conversion_box'] and item:
- k = _conversion_checks(item, self.keys(), self._box_config)
- if k:
- return self.__getitem__(k)
- if kill_camel:
- for k in self.keys():
- if item == _camel_killer(k):
- return self.__getitem__(k)
- if self._box_config['default_box']:
- return self.__get_default(item)
- raise BoxKeyError(str(err))
- else:
- if item == '_box_config':
- return value
- return self.__convert_and_store(item, value)
-
- def __setitem__(self, key, value):
- if (key != '_box_config' and self._box_config['__created'] and
- self._box_config['frozen_box']):
- raise BoxError('Box is frozen')
- if self._box_config['conversion_box']:
- _conversion_checks(key, self.keys(), self._box_config,
- check_only=True, pre_check=True)
- super(Box, self).__setitem__(key, value)
- self.__add_ordered(key)
- self.__create_lineage()
-
- def __setattr__(self, key, value):
- if (key != '_box_config' and self._box_config['frozen_box'] and
- self._box_config['__created']):
- raise BoxError('Box is frozen')
- if key in self._protected_keys:
- raise AttributeError("Key name '{0}' is protected".format(key))
- if key == '_box_config':
- return object.__setattr__(self, key, value)
- if (key not in self.keys() and
- (self._box_config['conversion_box'] or
- self._box_config['camel_killer_box'])):
- if self._box_config['conversion_box']:
- k = _conversion_checks(key, self.keys(),
- self._box_config)
- self[key if not k else k] = value
- elif self._box_config['camel_killer_box']:
- for each_key in self:
- if key == _camel_killer(each_key):
- self[each_key] = value
- break
- else:
- self[key] = value
- self.__add_ordered(key)
- self.__create_lineage()
-
- def __delitem__(self, key):
- if self._box_config['frozen_box']:
- raise BoxError('Box is frozen')
- super(Box, self).__delitem__(key)
- if (self._box_config['ordered_box'] and
- key in self._box_config['__ordered_box_values']):
- self._box_config['__ordered_box_values'].remove(key)
-
- def __delattr__(self, item):
- if self._box_config['frozen_box']:
- raise BoxError('Box is frozen')
- if item == '_box_config':
- raise BoxError('"_box_config" is protected')
- if item in self._protected_keys:
- raise AttributeError("Key name '{0}' is protected".format(item))
- del self[item]
-
- def pop(self, key, *args):
- if args:
- if len(args) != 1:
- raise BoxError('pop() takes only one optional'
- ' argument "default"')
- try:
- item = self[key]
- except KeyError:
- return args[0]
- else:
- del self[key]
- if isinstance(item, Box):
- item._box_config['__box_heritage'] = ()
- return item
- try:
- item = self[key]
- except KeyError:
- raise BoxKeyError('{0}'.format(key))
- else:
- del self[key]
- if isinstance(item, Box):
- item._box_config['__box_heritage'] = ()
- return item
-
- def clear(self):
- self._box_config['__ordered_box_values'] = []
- super(Box, self).clear()
-
- def popitem(self):
- try:
- key = next(self.__iter__())
- except StopIteration:
- raise BoxKeyError('Empty box')
- return key, self.pop(key)
-
- def __repr__(self):
- return '<Box: {0}>'.format(str(self.to_dict()))
-
- def __str__(self):
- return str(self.to_dict())
-
- def __iter__(self):
- for key in self.keys():
- yield key
-
- def __reversed__(self):
- for key in reversed(list(self.keys())):
- yield key
-
- def to_dict(self):
- """
- Turn the Box and sub Boxes back into a native
- python dictionary.
-
- :return: python dictionary of this Box
- """
- out_dict = dict(self)
- for k, v in out_dict.items():
- if v is self:
- out_dict[k] = out_dict
- elif hasattr(v, 'to_dict'):
- out_dict[k] = v.to_dict()
- elif hasattr(v, 'to_list'):
- out_dict[k] = v.to_list()
- return out_dict
-
- def update(self, E=None, **F):
- if not E:
- E = F
- iter_over = E.items() if hasattr(E, 'items') else E
- for k, v in iter_over:
- intact_type = (self._box_config['box_intact_types'] and
- isinstance(v, self._box_config['box_intact_types']))
- if isinstance(v, dict) and not intact_type:
- # Box objects must be created in case they are already
- # in the `converted` box_config set
- v = self.__class__(v, **self.__box_config())
- if k in self and isinstance(self[k], dict):
- self[k].update(v)
- continue
- if isinstance(v, list) and not intact_type:
- v = BoxList(v, **self.__box_config())
- try:
- self.__setattr__(k, v)
- except (AttributeError, TypeError):
- self.__setitem__(k, v)
-
- def setdefault(self, item, default=None):
- if item in self:
- return self[item]
-
- if isinstance(default, dict):
- default = self.__class__(default, **self.__box_config())
- if isinstance(default, list):
- default = BoxList(default,
- box_class=self.__class__, **self.__box_config())
- self[item] = default
- return default
-
- def to_json(self, filename=None,
- encoding="utf-8", errors="strict", **json_kwargs):
- """
- Transform the Box object into a JSON string.
-
- :param filename: If provided will save to file
- :param encoding: File encoding
- :param errors: How to handle encoding errors
- :param json_kwargs: additional arguments to pass to json.dump(s)
- :return: string of JSON or return of `json.dump`
- """
- return _to_json(self.to_dict(), filename=filename,
- encoding=encoding, errors=errors, **json_kwargs)
-
- @classmethod
- def from_json(cls, json_string=None, filename=None,
- encoding="utf-8", errors="strict", **kwargs):
- """
- Transform a json object string into a Box object. If the incoming
- json is a list, you must use BoxList.from_json.
-
- :param json_string: string to pass to `json.loads`
- :param filename: filename to open and pass to `json.load`
- :param encoding: File encoding
- :param errors: How to handle encoding errors
- :param kwargs: parameters to pass to `Box()` or `json.loads`
- :return: Box object from json data
- """
- bx_args = {}
- for arg in kwargs.copy():
- if arg in BOX_PARAMETERS:
- bx_args[arg] = kwargs.pop(arg)
-
- data = _from_json(json_string, filename=filename,
- encoding=encoding, errors=errors, **kwargs)
-
- if not isinstance(data, dict):
- raise BoxError('json data not returned as a dictionary, '
- 'but rather a {0}'.format(type(data).__name__))
- return cls(data, **bx_args)
-
- if yaml_support:
- def to_yaml(self, filename=None, default_flow_style=False,
- encoding="utf-8", errors="strict",
- **yaml_kwargs):
- """
- Transform the Box object into a YAML string.
-
- :param filename: If provided will save to file
- :param default_flow_style: False will recursively dump dicts
- :param encoding: File encoding
- :param errors: How to handle encoding errors
- :param yaml_kwargs: additional arguments to pass to yaml.dump
- :return: string of YAML or return of `yaml.dump`
- """
- return _to_yaml(self.to_dict(), filename=filename,
- default_flow_style=default_flow_style,
- encoding=encoding, errors=errors, **yaml_kwargs)
-
- @classmethod
- def from_yaml(cls, yaml_string=None, filename=None,
- encoding="utf-8", errors="strict",
- loader=yaml.SafeLoader, **kwargs):
- """
- Transform a yaml object string into a Box object.
-
- :param yaml_string: string to pass to `yaml.load`
- :param filename: filename to open and pass to `yaml.load`
- :param encoding: File encoding
- :param errors: How to handle encoding errors
- :param loader: YAML Loader, defaults to SafeLoader
- :param kwargs: parameters to pass to `Box()` or `yaml.load`
- :return: Box object from yaml data
- """
- bx_args = {}
- for arg in kwargs.copy():
- if arg in BOX_PARAMETERS:
- bx_args[arg] = kwargs.pop(arg)
-
- data = _from_yaml(yaml_string=yaml_string, filename=filename,
- encoding=encoding, errors=errors,
- Loader=loader, **kwargs)
- if not isinstance(data, dict):
- raise BoxError('yaml data not returned as a dictionary'
- 'but rather a {0}'.format(type(data).__name__))
- return cls(data, **bx_args)
-
-
-class BoxList(list):
- """
- Drop in replacement of list, that converts added objects to Box or BoxList
- objects as necessary.
- """
-
- def __init__(self, iterable=None, box_class=Box, **box_options):
- self.box_class = box_class
- self.box_options = box_options
- self.box_org_ref = self.box_org_ref = id(iterable) if iterable else 0
- if iterable:
- for x in iterable:
- self.append(x)
- if box_options.get('frozen_box'):
- def frozen(*args, **kwargs):
- raise BoxError('BoxList is frozen')
-
- for method in ['append', 'extend', 'insert', 'pop',
- 'remove', 'reverse', 'sort']:
- self.__setattr__(method, frozen)
-
- def __delitem__(self, key):
- if self.box_options.get('frozen_box'):
- raise BoxError('BoxList is frozen')
- super(BoxList, self).__delitem__(key)
-
- def __setitem__(self, key, value):
- if self.box_options.get('frozen_box'):
- raise BoxError('BoxList is frozen')
- super(BoxList, self).__setitem__(key, value)
-
- def _is_intact_type(self, obj):
- try:
- if (self.box_options.get('box_intact_types') and
- isinstance(obj, self.box_options['box_intact_types'])):
- return True
- except AttributeError as err:
- if 'box_options' in self.__dict__:
- raise err
- return False
-
- def append(self, p_object):
- if isinstance(p_object, dict) and not self._is_intact_type(p_object):
- try:
- p_object = self.box_class(p_object, **self.box_options)
- except AttributeError as err:
- if 'box_class' in self.__dict__:
- raise err
- elif isinstance(p_object, list) and not self._is_intact_type(p_object):
- try:
- p_object = (self if id(p_object) == self.box_org_ref else
- BoxList(p_object))
- except AttributeError as err:
- if 'box_org_ref' in self.__dict__:
- raise err
- super(BoxList, self).append(p_object)
-
- def extend(self, iterable):
- for item in iterable:
- self.append(item)
-
- def insert(self, index, p_object):
- if isinstance(p_object, dict) and not self._is_intact_type(p_object):
- p_object = self.box_class(p_object, **self.box_options)
- elif isinstance(p_object, list) and not self._is_intact_type(p_object):
- p_object = (self if id(p_object) == self.box_org_ref else
- BoxList(p_object))
- super(BoxList, self).insert(index, p_object)
-
- def __repr__(self):
- return "<BoxList: {0}>".format(self.to_list())
-
- def __str__(self):
- return str(self.to_list())
-
- def __copy__(self):
- return BoxList((x for x in self),
- self.box_class,
- **self.box_options)
-
- def __deepcopy__(self, memodict=None):
- out = self.__class__(**self.box_options)
- memodict = memodict or {}
- memodict[id(self)] = out
- for k in self:
- out.append(copy.deepcopy(k))
- return out
-
- def __hash__(self):
- if self.box_options.get('frozen_box'):
- hashing = 98765
- hashing ^= hash(tuple(self))
- return hashing
- raise TypeError("unhashable type: 'BoxList'")
-
- def to_list(self):
- new_list = []
- for x in self:
- if x is self:
- new_list.append(new_list)
- elif isinstance(x, Box):
- new_list.append(x.to_dict())
- elif isinstance(x, BoxList):
- new_list.append(x.to_list())
- else:
- new_list.append(x)
- return new_list
-
- def to_json(self, filename=None,
- encoding="utf-8", errors="strict",
- multiline=False, **json_kwargs):
- """
- Transform the BoxList object into a JSON string.
-
- :param filename: If provided will save to file
- :param encoding: File encoding
- :param errors: How to handle encoding errors
- :param multiline: Put each item in list onto it's own line
- :param json_kwargs: additional arguments to pass to json.dump(s)
- :return: string of JSON or return of `json.dump`
- """
- if filename and multiline:
- lines = [_to_json(item, filename=False, encoding=encoding,
- errors=errors, **json_kwargs) for item in self]
- with open(filename, 'w', encoding=encoding, errors=errors) as f:
- f.write("\n".join(lines).decode('utf-8') if
- sys.version_info < (3, 0) else "\n".join(lines))
- else:
- return _to_json(self.to_list(), filename=filename,
- encoding=encoding, errors=errors, **json_kwargs)
-
- @classmethod
- def from_json(cls, json_string=None, filename=None, encoding="utf-8",
- errors="strict", multiline=False, **kwargs):
- """
- Transform a json object string into a BoxList object. If the incoming
- json is a dict, you must use Box.from_json.
-
- :param json_string: string to pass to `json.loads`
- :param filename: filename to open and pass to `json.load`
- :param encoding: File encoding
- :param errors: How to handle encoding errors
- :param multiline: One object per line
- :param kwargs: parameters to pass to `Box()` or `json.loads`
- :return: BoxList object from json data
- """
- bx_args = {}
- for arg in kwargs.copy():
- if arg in BOX_PARAMETERS:
- bx_args[arg] = kwargs.pop(arg)
-
- data = _from_json(json_string, filename=filename, encoding=encoding,
- errors=errors, multiline=multiline, **kwargs)
-
- if not isinstance(data, list):
- raise BoxError('json data not returned as a list, '
- 'but rather a {0}'.format(type(data).__name__))
- return cls(data, **bx_args)
-
- if yaml_support:
- def to_yaml(self, filename=None, default_flow_style=False,
- encoding="utf-8", errors="strict",
- **yaml_kwargs):
- """
- Transform the BoxList object into a YAML string.
-
- :param filename: If provided will save to file
- :param default_flow_style: False will recursively dump dicts
- :param encoding: File encoding
- :param errors: How to handle encoding errors
- :param yaml_kwargs: additional arguments to pass to yaml.dump
- :return: string of YAML or return of `yaml.dump`
- """
- return _to_yaml(self.to_list(), filename=filename,
- default_flow_style=default_flow_style,
- encoding=encoding, errors=errors, **yaml_kwargs)
-
- @classmethod
- def from_yaml(cls, yaml_string=None, filename=None,
- encoding="utf-8", errors="strict",
- loader=yaml.SafeLoader,
- **kwargs):
- """
- Transform a yaml object string into a BoxList object.
-
- :param yaml_string: string to pass to `yaml.load`
- :param filename: filename to open and pass to `yaml.load`
- :param encoding: File encoding
- :param errors: How to handle encoding errors
- :param loader: YAML Loader, defaults to SafeLoader
- :param kwargs: parameters to pass to `BoxList()` or `yaml.load`
- :return: BoxList object from yaml data
- """
- bx_args = {}
- for arg in kwargs.copy():
- if arg in BOX_PARAMETERS:
- bx_args[arg] = kwargs.pop(arg)
-
- data = _from_yaml(yaml_string=yaml_string, filename=filename,
- encoding=encoding, errors=errors,
- Loader=loader, **kwargs)
- if not isinstance(data, list):
- raise BoxError('yaml data not returned as a list'
- 'but rather a {0}'.format(type(data).__name__))
- return cls(data, **bx_args)
-
- def box_it_up(self):
- for v in self:
- if hasattr(v, 'box_it_up') and v is not self:
- v.box_it_up()
-
-
-class ConfigBox(Box):
- """
- Modified box object to add object transforms.
-
- Allows for build in transforms like:
-
- cns = ConfigBox(my_bool='yes', my_int='5', my_list='5,4,3,3,2')
-
- cns.bool('my_bool') # True
- cns.int('my_int') # 5
- cns.list('my_list', mod=lambda x: int(x)) # [5, 4, 3, 3, 2]
- """
-
- _protected_keys = dir({}) + ['to_dict', 'bool', 'int', 'float',
- 'list', 'getboolean', 'to_json', 'to_yaml',
- 'getfloat', 'getint',
- 'from_json', 'from_yaml']
-
- def __getattr__(self, item):
- """Config file keys are stored in lower case, be a little more
- loosey goosey"""
- try:
- return super(ConfigBox, self).__getattr__(item)
- except AttributeError:
- return super(ConfigBox, self).__getattr__(item.lower())
-
- def __dir__(self):
- return super(ConfigBox, self).__dir__() + ['bool', 'int', 'float',
- 'list', 'getboolean',
- 'getfloat', 'getint']
-
- def bool(self, item, default=None):
- """ Return value of key as a boolean
-
- :param item: key of value to transform
- :param default: value to return if item does not exist
- :return: approximated bool of value
- """
- try:
- item = self.__getattr__(item)
- except AttributeError as err:
- if default is not None:
- return default
- raise err
-
- if isinstance(item, (bool, int)):
- return bool(item)
-
- if (isinstance(item, str) and
- item.lower() in ('n', 'no', 'false', 'f', '0')):
- return False
-
- return True if item else False
-
- def int(self, item, default=None):
- """ Return value of key as an int
-
- :param item: key of value to transform
- :param default: value to return if item does not exist
- :return: int of value
- """
- try:
- item = self.__getattr__(item)
- except AttributeError as err:
- if default is not None:
- return default
- raise err
- return int(item)
-
- def float(self, item, default=None):
- """ Return value of key as a float
-
- :param item: key of value to transform
- :param default: value to return if item does not exist
- :return: float of value
- """
- try:
- item = self.__getattr__(item)
- except AttributeError as err:
- if default is not None:
- return default
- raise err
- return float(item)
-
- def list(self, item, default=None, spliter=",", strip=True, mod=None):
- """ Return value of key as a list
-
- :param item: key of value to transform
- :param mod: function to map against list
- :param default: value to return if item does not exist
- :param spliter: character to split str on
- :param strip: clean the list with the `strip`
- :return: list of items
- """
- try:
- item = self.__getattr__(item)
- except AttributeError as err:
- if default is not None:
- return default
- raise err
- if strip:
- item = item.lstrip('[').rstrip(']')
- out = [x.strip() if strip else x for x in item.split(spliter)]
- if mod:
- return list(map(mod, out))
- return out
-
- # loose configparser compatibility
-
- def getboolean(self, item, default=None):
- return self.bool(item, default)
-
- def getint(self, item, default=None):
- return self.int(item, default)
-
- def getfloat(self, item, default=None):
- return self.float(item, default)
-
- def __repr__(self):
- return '<ConfigBox: {0}>'.format(str(self.to_dict()))
-
- def copy(self):
- return ConfigBox(super(ConfigBox, self).copy())
-
- def __copy__(self):
- return ConfigBox(super(ConfigBox, self).copy())
-
-
-class SBox(Box):
- """
- ShorthandBox (SBox) allows for
- property access of `dict` `json` and `yaml`
- """
- _protected_keys = dir({}) + ['to_dict', 'tree_view', 'to_json', 'to_yaml',
- 'json', 'yaml', 'from_yaml', 'from_json',
- 'dict']
-
- @property
- def dict(self):
- return self.to_dict()
-
- @property
- def json(self):
- return self.to_json()
-
- if yaml_support:
- @property
- def yaml(self):
- return self.to_yaml()
-
- def __repr__(self):
- return '<ShorthandBox: {0}>'.format(str(self.to_dict()))
-
- def copy(self):
- return SBox(super(SBox, self).copy())
-
- def __copy__(self):
- return SBox(super(SBox, self).copy())
-
-
-if wrapt_support:
-
- class BoxObject(wrapt.ObjectProxy):
- """
- Wrapper for any Python object with a Box as __dict__.
-
- Simple Usage:
-
- import requests
- url = 'https://raw.githubusercontent.com/cdgriffith/Box/master/box.py'
- session = BoxObject(requests.Session())
- session.source_code = session.get(url).text
-
- :param wrapped: Wrapped Object.
- :param box_class: Custom internal Box class
- :param args: Arguments to fill Box
- :param kwargs: Keyword arguments to fill Box
- """
-
- def __init__(self, wrapped=None, *args, **kwargs):
- """Initialize Box Object with __dict__ as a Box."""
- super(BoxObject, self).__init__(wrapped)
- box_class = kwargs.pop('box_class', Box)
- try:
- base_dict = super(BoxObject, self).__getattr__('__dict__')
- if args:
- raise TypeError('Cannot pass dictionary arguments when '
- 'internal object has __dict__ attributes. '
- 'Pass arguments by keyword instead.')
- box = box_class(base_dict, **kwargs)
- except AttributeError:
- box = box_class(*args, **kwargs)
- super(BoxObject, self).__setattr__('__dict__', box)
-
- def __call__(self, *args, **kwargs):
- """Call Method for Callable Objects."""
- return self.__wrapped__(*args, **kwargs)
-
- def __getattr__(self, name):
- """Get Attribute from Wrapped Object or from Box."""
- try:
- return super(BoxObject, self).__getattr__(name)
- except AttributeError as error:
- try:
- return self.__dict__[name]
- except KeyError:
- raise error
-
- def __setattr__(self, name, value):
- """Set Attribute in Wrapped Object or Box."""
- if name == '__dict__':
- raise TypeError('cannot set __dict__')
- elif hasattr(self.__wrapped__, name):
- setattr(self.__wrapped__, name, value)
- else:
- self.__dict__[name] = value
-
- def __delattr__(self, name):
- """Delete Attribute in Wrapped Object or Box."""
- if name == '__dict__':
- super(BoxObject, self).__setattr__(
- '__dict__',
- getattr(self.__wrapped__, '__dict__', {})
- )
- else:
- try:
- delattr(self.__wrapped__, name)
- except AttributeError as error:
- try:
- del self.__dict__[name]
- except KeyError:
- raise error
-
-
- __all__ += ['BoxObject']
diff --git a/box/__init__.py b/box/__init__.py
new file mode 100644
index 0000000..7cd918f
--- /dev/null
+++ b/box/__init__.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+__author__ = "Chris Griffith"
+__version__ = "7.3.2"
+
+from box.box import Box
+from box.box_list import BoxList
+from box.config_box import ConfigBox
+from box.exceptions import BoxError, BoxKeyError
+from box.from_file import box_from_file, box_from_string
+from box.shorthand_box import SBox, DDBox
+import box.converters
+
+__all__ = [
+ "Box",
+ "BoxList",
+ "ConfigBox",
+ "BoxError",
+ "BoxKeyError",
+ "box_from_file",
+ "SBox",
+ "DDBox",
+]
diff --git a/box/box.py b/box/box.py
new file mode 100644
index 0000000..8be6738
--- /dev/null
+++ b/box/box.py
@@ -0,0 +1,1195 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017-2023 - Chris Griffith - MIT License
+"""
+Improved dictionary access through dot notation with additional tools.
+"""
+import copy
+import re
+import warnings
+from keyword import iskeyword
+from os import PathLike
+from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union, Literal
+from inspect import signature
+
+try:
+ from typing import Callable, Iterable, Mapping
+except ImportError:
+ from collections.abc import Callable, Iterable, Mapping
+
+
+import box
+from box.converters import (
+ BOX_PARAMETERS,
+ _from_json,
+ _from_msgpack,
+ _from_toml,
+ _from_yaml,
+ _to_json,
+ _to_msgpack,
+ _to_toml,
+ _to_yaml,
+ msgpack_available,
+ toml_read_library,
+ toml_write_library,
+ yaml_available,
+)
+from box.exceptions import BoxError, BoxKeyError, BoxTypeError, BoxValueError, BoxWarning
+
+__all__ = ["Box"]
+
+_first_cap_re = re.compile("(.)([A-Z][a-z]+)")
+_all_cap_re = re.compile("([a-z0-9])([A-Z])")
+_list_pos_re = re.compile(r"\[(\d+)\]")
+
+# a sentinel object for indicating no default, in order to allow users
+# to pass `None` as a valid default value
+NO_DEFAULT = object()
+# a sentinel object for indicating when to skip adding a new namespace, allowing `None` keys
+NO_NAMESPACE = object()
+
+
+def _is_ipython():
+ try:
+ from IPython import get_ipython
+ except ImportError:
+ ipython = False
+ else:
+ ipython = True if get_ipython() else False
+
+ return ipython
+
+
+def _exception_cause(e):
+ """
+ Unwrap BoxKeyError and BoxValueError errors to their cause.
+
+ Use with `raise ... from _exception_cause(err)` to avoid deeply nested stacktraces, but keep the
+ context.
+ """
+ return e.__cause__ if isinstance(e, (BoxKeyError, BoxValueError)) else e
+
+
+def _camel_killer(attr):
+ """
+ CamelKiller, qu'est-ce que c'est?
+
+ Taken from http://stackoverflow.com/a/1176023/3244542
+ """
+ attr = str(attr)
+
+ s1 = _first_cap_re.sub(r"\1_\2", attr)
+ s2 = _all_cap_re.sub(r"\1_\2", s1)
+ return re.sub(" *_+", "_", s2.lower())
+
+
+def _recursive_tuples(iterable, box_class, recreate_tuples=False, **kwargs):
+ out_list = []
+ for i in iterable:
+ if isinstance(i, dict):
+ out_list.append(box_class(i, **kwargs))
+ elif isinstance(i, list) or (recreate_tuples and isinstance(i, tuple)):
+ out_list.append(_recursive_tuples(i, box_class, recreate_tuples, **kwargs))
+ else:
+ out_list.append(i)
+ return tuple(out_list)
+
+
+def _parse_box_dots(bx, item, setting=False):
+ for idx, char in enumerate(item):
+ if char == "[":
+ return item[:idx], item[idx:]
+ elif char == ".":
+ return item[:idx], item[idx + 1 :]
+ if setting and "." in item:
+ return item.split(".", 1)
+ raise BoxError("Could not split box dots properly")
+
+
+def _get_dot_paths(bx, current=""):
+ """A generator of all the end node keys in a box in box_dots format"""
+
+ def handle_dicts(sub_bx, paths=""):
+ for key, value in sub_bx.items():
+ yield f"{paths}.{key}" if paths else key
+ if isinstance(value, dict):
+ yield from handle_dicts(value, f"{paths}.{key}" if paths else key)
+ elif isinstance(value, list):
+ yield from handle_lists(value, f"{paths}.{key}" if paths else key)
+
+ def handle_lists(bx_list, paths=""):
+ for i, value in enumerate(bx_list):
+ yield f"{paths}[{i}]"
+ if isinstance(value, list):
+ yield from handle_lists(value, f"{paths}[{i}]")
+ if isinstance(value, dict):
+ yield from handle_dicts(value, f"{paths}[{i}]")
+
+ yield from handle_dicts(bx, current)
+
+
+def _get_box_config():
+ return {
+ # Internal use only
+ "__created": False,
+ "__safe_keys": {},
+ }
+
+
+def _get_property_func(obj, key):
+ """
+ Try to get property helper functions of given object and property name.
+
+ :param obj: object to be checked for property
+ :param key: property name
+ :return: a tuple for helper functions(fget, fset, fdel). If no such property, a (None, None, None) returns
+ """
+ obj_type = type(obj)
+
+ if not hasattr(obj_type, key):
+ return None, None, None
+ attr = getattr(obj_type, key)
+ return attr.fget, attr.fset, attr.fdel
+
+
+class Box(dict):
+ """
+ Improved dictionary access through dot notation with additional tools.
+
+ :param default_box: Similar to defaultdict, return a default value
+ :param default_box_attr: Specify the default replacement.
+ WARNING: If this is not the default 'Box', it will not be recursive
+ :param default_box_none_transform: When using default_box, treat keys with none values as absent. True by default
+ :param default_box_create_on_get: On lookup of a key that doesn't exist, create it if missing
+ :param frozen_box: After creation, the box cannot be modified
+ :param camel_killer_box: Convert CamelCase to snake_case
+ :param conversion_box: Check for near matching keys as attributes
+ :param modify_tuples_box: Recreate incoming tuples with dicts into Boxes
+ :param box_safe_prefix: Conversion box prefix for unsafe attributes
+ :param box_duplicates: "ignore", "error" or "warn" when duplicates exists in a conversion_box
+ :param box_intact_types: tuple of types to ignore converting
+ :param box_recast: cast certain keys to a specified type
+ :param box_dots: access nested Boxes by period separated keys in string
+ :param box_class: change what type of class sub-boxes will be created as
+ :param box_namespace: the namespace this (possibly nested) Box lives within
+ """
+
+ _box_config: Dict[str, Any]
+
+ _protected_keys = [
+ "to_dict",
+ "to_json",
+ "to_yaml",
+ "from_yaml",
+ "from_json",
+ "from_toml",
+ "to_toml",
+ "merge_update",
+ ] + [attr for attr in dir({}) if not attr.startswith("_")]
+
+ def __new__(
+ cls,
+ *args: Any,
+ default_box: bool = False,
+ default_box_attr: Any = NO_DEFAULT,
+ default_box_none_transform: bool = True,
+ default_box_create_on_get: bool = True,
+ frozen_box: bool = False,
+ camel_killer_box: bool = False,
+ conversion_box: bool = True,
+ modify_tuples_box: bool = False,
+ box_safe_prefix: str = "x",
+ box_duplicates: str = "ignore",
+ box_intact_types: Union[Tuple, List] = (),
+ box_recast: Optional[Dict] = None,
+ box_dots: bool = False,
+ box_class: Optional[Union[Dict, Type["Box"]]] = None,
+ box_namespace: Union[Tuple[str, ...], Literal[False]] = (),
+ **kwargs: Any,
+ ):
+ """
+ Due to the way pickling works in python 3, we need to make sure
+ the box config is created as early as possible.
+ """
+ obj = super().__new__(cls, *args, **kwargs)
+ obj._box_config = _get_box_config()
+ obj._box_config.update(
+ {
+ "default_box": default_box,
+ "default_box_attr": cls.__class__ if default_box_attr is NO_DEFAULT else default_box_attr,
+ "default_box_none_transform": default_box_none_transform,
+ "default_box_create_on_get": default_box_create_on_get,
+ "conversion_box": conversion_box,
+ "box_safe_prefix": box_safe_prefix,
+ "frozen_box": frozen_box,
+ "camel_killer_box": camel_killer_box,
+ "modify_tuples_box": modify_tuples_box,
+ "box_duplicates": box_duplicates,
+ "box_intact_types": tuple(box_intact_types),
+ "box_recast": box_recast,
+ "box_dots": box_dots,
+ "box_class": box_class if box_class is not None else Box,
+ "box_namespace": box_namespace,
+ }
+ )
+ return obj
+
+ def __init__(
+ self,
+ *args: Any,
+ default_box: bool = False,
+ default_box_attr: Any = NO_DEFAULT,
+ default_box_none_transform: bool = True,
+ default_box_create_on_get: bool = True,
+ frozen_box: bool = False,
+ camel_killer_box: bool = False,
+ conversion_box: bool = True,
+ modify_tuples_box: bool = False,
+ box_safe_prefix: str = "x",
+ box_duplicates: str = "ignore",
+ box_intact_types: Union[Tuple, List] = (),
+ box_recast: Optional[Dict] = None,
+ box_dots: bool = False,
+ box_class: Optional[Union[Dict, Type["Box"]]] = None,
+ box_namespace: Union[Tuple[str, ...], Literal[False]] = (),
+ **kwargs: Any,
+ ):
+ super().__init__()
+ self._box_config = _get_box_config()
+ self._box_config.update(
+ {
+ "default_box": default_box,
+ "default_box_attr": self.__class__ if default_box_attr is NO_DEFAULT else default_box_attr,
+ "default_box_none_transform": default_box_none_transform,
+ "default_box_create_on_get": default_box_create_on_get,
+ "conversion_box": conversion_box,
+ "box_safe_prefix": box_safe_prefix,
+ "frozen_box": frozen_box,
+ "camel_killer_box": camel_killer_box,
+ "modify_tuples_box": modify_tuples_box,
+ "box_duplicates": box_duplicates,
+ "box_intact_types": tuple(box_intact_types),
+ "box_recast": box_recast,
+ "box_dots": box_dots,
+ "box_class": box_class if box_class is not None else self.__class__,
+ "box_namespace": box_namespace,
+ }
+ )
+ if not self._box_config["conversion_box"] and self._box_config["box_duplicates"] != "ignore":
+ raise BoxError("box_duplicates are only for conversion_boxes")
+ if len(args) == 1:
+ if isinstance(args[0], str):
+ raise BoxValueError("Cannot extrapolate Box from string")
+ if isinstance(args[0], Mapping):
+ for k, v in args[0].items():
+ if v is args[0]:
+ v = self
+ if v is None and self._box_config["default_box"] and self._box_config["default_box_none_transform"]:
+ continue
+ self.__setitem__(k, v)
+ elif isinstance(args[0], Iterable):
+ for k, v in args[0]:
+ self.__setitem__(k, v)
+ else:
+ raise BoxValueError("First argument must be mapping or iterable")
+ elif args:
+ raise BoxTypeError(f"Box expected at most 1 argument, got {len(args)}")
+
+ for k, v in kwargs.items():
+ if args and isinstance(args[0], Mapping) and v is args[0]:
+ v = self
+ self.__setitem__(k, v)
+
+ self._box_config["__created"] = True
+
+ def __add__(self, other: Mapping[Any, Any]):
+ if not isinstance(other, dict):
+ raise BoxTypeError("Box can only merge two boxes or a box and a dictionary.")
+ new_box = self.copy()
+ new_box._box_config["frozen_box"] = False
+ new_box.merge_update(other) # type: ignore[attr-defined]
+ new_box._box_config["frozen_box"] = self._box_config["frozen_box"]
+ return new_box
+
+ def __radd__(self, other: Mapping[Any, Any]):
+ if not isinstance(other, dict):
+ raise BoxTypeError("Box can only merge two boxes or a box and a dictionary.")
+
+ new_box = other.copy()
+ if not isinstance(other, Box):
+ new_box = self._box_config["box_class"](new_box)
+ new_box._box_config["frozen_box"] = False # type: ignore[attr-defined]
+ new_box.merge_update(self) # type: ignore[attr-defined]
+ new_box._box_config["frozen_box"] = self._box_config["frozen_box"] # type: ignore[attr-defined]
+ return new_box
+
+ def __iadd__(self, other: Mapping[Any, Any]):
+ if not isinstance(other, dict):
+ raise BoxTypeError("Box can only merge two boxes or a box and a dictionary.")
+ self.merge_update(other)
+ return self
+
+ def __or__(self, other: Mapping[Any, Any]):
+ if not isinstance(other, dict):
+ raise BoxTypeError("Box can only merge two boxes or a box and a dictionary.")
+ new_box = self.copy()
+ new_box._box_config["frozen_box"] = False
+ new_box.update(other) # type: ignore[attr-defined]
+ new_box._box_config["frozen_box"] = self._box_config["frozen_box"]
+ return new_box
+
+ def __ror__(self, other: Mapping[Any, Any]):
+ if not isinstance(other, dict):
+ raise BoxTypeError("Box can only merge two boxes or a box and a dictionary.")
+ new_box = other.copy()
+ if not isinstance(other, Box):
+ new_box = self._box_config["box_class"](new_box)
+ new_box._box_config["frozen_box"] = False # type: ignore[attr-defined]
+ new_box.update(self) # type: ignore[attr-defined]
+ new_box._box_config["frozen_box"] = self._box_config["frozen_box"] # type: ignore[attr-defined]
+ return new_box
+
+ def __ior__(self, other: Mapping[Any, Any]): # type: ignore[override]
+ if not isinstance(other, dict):
+ raise BoxTypeError("Box can only merge two boxes or a box and a dictionary.")
+ self.update(other)
+ return self
+
+ def __sub__(self, other: Mapping[Any, Any]):
+ frozen = self._box_config["frozen_box"]
+ config = self.__box_config()
+ config["frozen_box"] = False
+ config.pop("box_namespace") # Detach namespace; it will be reassigned if we nest again
+ output = self._box_config["box_class"](**config)
+ if not isinstance(other, dict):
+ raise BoxError("Box can only compare two boxes or a box and a dictionary.")
+ if not isinstance(other, Box):
+ other = self._box_config["box_class"](other, **config)
+ for item in self:
+ if item not in other:
+ output[item] = self[item]
+ elif isinstance(self.get(item), Box) and isinstance(other.get(item), Box):
+ output[item] = self[item] - other[item]
+ if not output[item]:
+ del output[item]
+ output._box_config["frozen_box"] = frozen
+ return output
+
+ def __hash__(self):
+ if self._box_config["frozen_box"]:
+ hashing = 54321
+ for item in self.items():
+ hashing ^= hash(item)
+ return hashing
+ raise BoxTypeError('unhashable type: "Box"')
+
+ def __dir__(self) -> List[str]:
+ items = set(super().__dir__())
+ # Only show items accessible by dot notation
+ for key in self.keys():
+ key = str(key)
+ if key.isidentifier() and not iskeyword(key):
+ items.add(key)
+
+ for key in self.keys():
+ if key not in items:
+ if self._box_config["conversion_box"]:
+ key = self._safe_attr(key)
+ if key:
+ items.add(key)
+
+ return list(items)
+
+ def __contains__(self, item):
+ in_me = super().__contains__(item)
+ if not self._box_config["box_dots"] or not isinstance(item, str):
+ return in_me
+ if in_me:
+ return True
+ if "." not in item:
+ return False
+ try:
+ first_item, children = _parse_box_dots(self, item)
+ except BoxError:
+ return False
+ else:
+ if not super().__contains__(first_item):
+ return False
+ it = self[first_item]
+ return isinstance(it, Iterable) and children in it
+
+ def keys(self, dotted: Union[bool] = False):
+ if not dotted:
+ return super().keys()
+
+ if not self._box_config["box_dots"]:
+ raise BoxError("Cannot return dotted keys as this Box does not have `box_dots` enabled")
+
+ keys = set()
+ for key, value in self.items():
+ added = False
+ if isinstance(key, str):
+ if isinstance(value, Box):
+ for sub_key in value.keys(dotted=True):
+ keys.add(f"{key}.{sub_key}")
+ added = True
+ elif isinstance(value, box.BoxList):
+ for pos in value._dotted_helper():
+ keys.add(f"{key}{pos}")
+ added = True
+ if not added:
+ keys.add(key)
+ return sorted(keys, key=lambda x: str(x))
+
+ def items(self, dotted: Union[bool] = False):
+ if not dotted:
+ return super().items()
+
+ if not self._box_config["box_dots"]:
+ raise BoxError("Cannot return dotted keys as this Box does not have `box_dots` enabled")
+
+ return [(k, self[k]) for k in self.keys(dotted=True)]
+
+ def get(self, key, default=NO_DEFAULT):
+ if key not in self:
+ if default is NO_DEFAULT:
+ if self._box_config["default_box"] and self._box_config["default_box_none_transform"]:
+ return self.__get_default(key)
+ else:
+ return None
+ if isinstance(default, dict) and not isinstance(default, Box):
+ return Box(default)
+ if isinstance(default, list) and not isinstance(default, box.BoxList):
+ return box.BoxList(default)
+ return default
+ return self[key]
+
+ def copy(self) -> "Box":
+ config = self.__box_config()
+ config.pop("box_namespace") # Detach namespace; it will be reassigned if we nest again
+ return Box(super().copy(), **config)
+
+ def __copy__(self) -> "Box":
+ return self.copy()
+
+ def __deepcopy__(self, memodict=None) -> "Box":
+ frozen = self._box_config["frozen_box"]
+ config = self.__box_config()
+ config["frozen_box"] = False
+ out = self._box_config["box_class"](**config)
+ memodict = memodict or {}
+ memodict[id(self)] = out
+ for k, v in self.items():
+ out[copy.deepcopy(k, memodict)] = copy.deepcopy(v, memodict)
+ out._box_config["frozen_box"] = frozen
+ return out
+
+ def __setstate__(self, state):
+ self._box_config = state["_box_config"]
+ self.__dict__.update(state)
+
+ def __get_default(self, item, attr=False):
+ if item in ("getdoc", "shape") and _is_ipython():
+ return None
+ default_value = self._box_config["default_box_attr"]
+ if default_value in (self._box_config["box_class"], dict):
+ value = self._box_config["box_class"](**self.__box_config(extra_namespace=item))
+ elif isinstance(default_value, dict):
+ value = self._box_config["box_class"](**self.__box_config(extra_namespace=item), **default_value)
+ elif isinstance(default_value, list):
+ value = box.BoxList(**self.__box_config(extra_namespace=item))
+ elif isinstance(default_value, Callable):
+ args = []
+ kwargs = {}
+ p_sigs = [
+ p.name
+ for p in signature(default_value).parameters.values()
+ if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
+ ]
+ k_sigs = [p.name for p in signature(default_value).parameters.values() if p.kind is p.KEYWORD_ONLY]
+ for name in p_sigs:
+ if name not in ("key", "box_instance"):
+ raise BoxError("default_box_attr can only have the arguments 'key' and 'box_instance'")
+ if "key" in p_sigs:
+ args.append(item)
+ if "box_instance" in p_sigs:
+ args.insert(p_sigs.index("box_instance"), self)
+ if "key" in k_sigs:
+ kwargs["key"] = item
+ if "box_instance" in k_sigs:
+ kwargs["box_instance"] = self
+ value = default_value(*args, **kwargs)
+ elif hasattr(default_value, "copy"):
+ value = default_value.copy()
+ else:
+ value = default_value
+ if self._box_config["default_box_create_on_get"]:
+ if not attr or not (item.startswith("_") and item.endswith("_")):
+ if self._box_config["box_dots"] and isinstance(item, str) and ("." in item or "[" in item):
+ first_item, children = _parse_box_dots(self, item, setting=True)
+ if first_item in self.keys():
+ if hasattr(self[first_item], "__setitem__"):
+ self[first_item].__setitem__(children, value)
+ else:
+ super().__setitem__(
+ first_item, self._box_config["box_class"](**self.__box_config(extra_namespace=first_item))
+ )
+ self[first_item].__setitem__(children, value)
+ else:
+ super().__setitem__(item, value)
+ return value
+
+ def __box_config(self, extra_namespace: Any = NO_NAMESPACE) -> Dict:
+ out = {}
+ for k, v in self._box_config.copy().items():
+ if not k.startswith("__"):
+ out[k] = v
+ if extra_namespace is not NO_NAMESPACE and self._box_config["box_namespace"] is not False:
+ out["box_namespace"] = (*out["box_namespace"], extra_namespace)
+ return out
+
+ def __recast(self, item, value):
+ if self._box_config["box_recast"] and item in self._box_config["box_recast"]:
+ recast = self._box_config["box_recast"][item]
+ try:
+ if isinstance(recast, type) and issubclass(recast, (Box, box.BoxList)):
+ return recast(value, **self.__box_config())
+ else:
+ return recast(value)
+ except ValueError as err:
+ raise BoxValueError(f"Cannot convert {value} to {recast}") from _exception_cause(err)
+ return value
+
+ def __convert_and_store(self, item, value):
+ if self._box_config["conversion_box"]:
+ safe_key = self._safe_attr(item)
+ self._box_config["__safe_keys"][safe_key] = item
+ if isinstance(value, (int, float, str, bytes, bytearray, bool, complex, set, frozenset)):
+ return super().__setitem__(item, value)
+ # If the value has already been converted or should not be converted, return it as-is
+ if self._box_config["box_intact_types"] and isinstance(value, self._box_config["box_intact_types"]):
+ return super().__setitem__(item, value)
+ # This is the magic sauce that makes sub dictionaries into new box objects
+ if isinstance(value, dict):
+ # We always re-create even if it was already a Box object to pass down configurations correctly
+ value = self._box_config["box_class"](value, **self.__box_config(extra_namespace=item))
+ elif isinstance(value, list) and not isinstance(value, box.BoxList):
+ if self._box_config["frozen_box"]:
+ value = _recursive_tuples(
+ value,
+ recreate_tuples=self._box_config["modify_tuples_box"],
+ **self.__box_config(extra_namespace=item),
+ )
+ else:
+ value = box.BoxList(value, **self.__box_config(extra_namespace=item))
+ elif isinstance(value, box.BoxList):
+ value.box_options.update(self.__box_config(extra_namespace=item))
+ elif self._box_config["modify_tuples_box"] and isinstance(value, tuple):
+ value = _recursive_tuples(value, recreate_tuples=True, **self.__box_config(extra_namespace=item))
+ super().__setitem__(item, value)
+
+ def __getitem__(self, item, _ignore_default=False):
+ try:
+ return super().__getitem__(item)
+ except KeyError as err:
+ if item == "_box_config":
+ cause = _exception_cause(err)
+ raise BoxKeyError("_box_config should only exist as an attribute and is never defaulted") from cause
+ if isinstance(item, slice):
+ # In Python 3.12 this changes to a KeyError instead of TypeError
+ new_box = self._box_config["box_class"](**self.__box_config())
+ for x in list(super().keys())[item.start : item.stop : item.step]:
+ new_box[x] = self[x]
+ return new_box
+ if self._box_config["box_dots"] and isinstance(item, str) and ("." in item or "[" in item):
+ try:
+ first_item, children = _parse_box_dots(self, item)
+ except BoxError:
+ if self._box_config["default_box"] and not _ignore_default:
+ return self.__get_default(item)
+ raise BoxKeyError(str(item)) from _exception_cause(err)
+ if first_item in self.keys():
+ if hasattr(self[first_item], "__getitem__"):
+ return self[first_item][children]
+ if self._box_config["camel_killer_box"] and isinstance(item, str):
+ converted = _camel_killer(item)
+ if converted in self.keys():
+ return super().__getitem__(converted)
+ if self._box_config["default_box"] and not _ignore_default:
+ return self.__get_default(item)
+ raise BoxKeyError(str(err)) from _exception_cause(err)
+ except TypeError as err:
+ if isinstance(item, slice):
+ new_box = self._box_config["box_class"](**self.__box_config())
+ for x in list(super().keys())[item.start : item.stop : item.step]:
+ new_box[x] = self[x]
+ return new_box
+ raise BoxTypeError(str(err)) from _exception_cause(err)
+
+ def __getattr__(self, item):
+ try:
+ try:
+ value = self.__getitem__(item, _ignore_default=True)
+ except KeyError:
+ value = object.__getattribute__(self, item)
+ except AttributeError as err:
+ if item == "__getstate__":
+ raise BoxKeyError(item) from _exception_cause(err)
+ if item == "_box_config":
+ raise BoxError("_box_config key must exist") from _exception_cause(err)
+ if self._box_config["conversion_box"]:
+ safe_key = self._safe_attr(item)
+ if safe_key in self._box_config["__safe_keys"]:
+ return self.__getitem__(self._box_config["__safe_keys"][safe_key])
+ if self._box_config["default_box"]:
+ if item.startswith("_") and item.endswith("_"):
+ raise BoxKeyError(f"{item}: Does not exist and internal methods are never defaulted")
+ return self.__get_default(item, attr=True)
+ raise BoxKeyError(str(err)) from _exception_cause(err)
+ return value
+
+ def __setitem__(self, key, value):
+ if key != "_box_config" and self._box_config["frozen_box"] and self._box_config["__created"]:
+ raise BoxError("Box is frozen")
+ if self._box_config["box_dots"] and isinstance(key, str) and ("." in key or "[" in key):
+ first_item, children = _parse_box_dots(self, key, setting=True)
+ if first_item in self.keys():
+ if hasattr(self[first_item], "__setitem__"):
+ return self[first_item].__setitem__(children, value)
+ elif self._box_config["default_box"]:
+ if children[0] == "[":
+ super().__setitem__(first_item, box.BoxList(**self.__box_config(extra_namespace=first_item)))
+ else:
+ super().__setitem__(
+ first_item, self._box_config["box_class"](**self.__box_config(extra_namespace=first_item))
+ )
+ return self[first_item].__setitem__(children, value)
+ else:
+ raise BoxKeyError(f"'{self.__class__}' object has no attribute {first_item}")
+ value = self.__recast(key, value)
+ if key not in self.keys() and self._box_config["camel_killer_box"]:
+ if self._box_config["camel_killer_box"] and isinstance(key, str):
+ key = _camel_killer(key)
+ if self._box_config["conversion_box"] and self._box_config["box_duplicates"] != "ignore":
+ self._conversion_checks(key)
+ self.__convert_and_store(key, value)
+
+ def __setattr__(self, key, value):
+ if key == "_box_config":
+ return object.__setattr__(self, key, value)
+ if self._box_config["frozen_box"] and self._box_config["__created"]:
+ raise BoxError("Box is frozen")
+ if key in self._protected_keys:
+ raise BoxKeyError(f'Key name "{key}" is protected')
+
+ safe_key = self._safe_attr(key)
+ if safe_key in self._box_config["__safe_keys"]:
+ key = self._box_config["__safe_keys"][safe_key]
+
+ # if user has customized property setter, fall back to default implementation
+ if _get_property_func(self, key)[1] is not None:
+ super().__setattr__(key, value)
+ else:
+ self.__setitem__(key, value)
+
+ def __delitem__(self, key):
+ if self._box_config["frozen_box"]:
+ raise BoxError("Box is frozen")
+ if (
+ key not in self.keys()
+ and self._box_config["box_dots"]
+ and isinstance(key, str)
+ and ("." in key or "[" in key)
+ ):
+ try:
+ first_item, children = _parse_box_dots(self, key)
+ except BoxError:
+ raise BoxKeyError(str(key)) from None
+ if hasattr(self[first_item], "__delitem__"):
+ return self[first_item].__delitem__(children)
+ if key not in self.keys() and self._box_config["camel_killer_box"]:
+ if self._box_config["camel_killer_box"] and isinstance(key, str):
+ for each_key in self:
+ if _camel_killer(key) == each_key:
+ key = each_key
+ break
+ try:
+ super().__delitem__(key)
+ except KeyError as err:
+ raise BoxKeyError(str(err)) from _exception_cause(err)
+
+ def __delattr__(self, item):
+ if self._box_config["frozen_box"]:
+ raise BoxError("Box is frozen")
+ if item == "_box_config":
+ raise BoxError('"_box_config" is protected')
+ if item in self._protected_keys:
+ raise BoxKeyError(f'Key name "{item}" is protected')
+
+ property_fdel = _get_property_func(self, item)[2]
+
+ # if user has customized property deleter, route to it
+ if property_fdel is not None:
+ property_fdel(self)
+ return
+ try:
+ self.__delitem__(item)
+ except KeyError as err:
+ if self._box_config["conversion_box"]:
+ safe_key = self._safe_attr(item)
+ if safe_key in self._box_config["__safe_keys"]:
+ self.__delitem__(self._box_config["__safe_keys"][safe_key])
+ del self._box_config["__safe_keys"][safe_key]
+ return
+ raise BoxKeyError(str(err)) from _exception_cause(err)
+
+ def pop(self, key, *args):
+ if self._box_config["frozen_box"]:
+ raise BoxError("Box is frozen")
+
+ if args:
+ if len(args) != 1:
+ raise BoxError('pop() takes only one optional argument "default"')
+ try:
+ item = self[key]
+ except KeyError:
+ return args[0]
+ else:
+ del self[key]
+ return item
+ try:
+ item = self[key]
+ except KeyError:
+ raise BoxKeyError(f"{key}") from None
+ else:
+ del self[key]
+ return item
+
+ def clear(self):
+ if self._box_config["frozen_box"]:
+ raise BoxError("Box is frozen")
+ super().clear()
+ self._box_config["__safe_keys"].clear()
+
+ def popitem(self):
+ if self._box_config["frozen_box"]:
+ raise BoxError("Box is frozen")
+ try:
+ key = next(self.__iter__())
+ except StopIteration:
+ raise BoxKeyError("Empty box") from None
+ return key, self.pop(key)
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}({self})"
+
+ def __str__(self) -> str:
+ return str(self.to_dict())
+
+ def __iter__(self) -> Generator:
+ for key in self.keys():
+ yield key
+
+ def __reversed__(self) -> Generator:
+ for key in reversed(list(self.keys())):
+ yield key
+
+ def to_dict(self) -> Dict:
+ """
+ Turn the Box and sub Boxes back into a native python dictionary.
+
+ :return: python dictionary of this Box
+ """
+ out_dict = dict(self)
+ for k, v in out_dict.items():
+ if v is self:
+ out_dict[k] = out_dict
+ elif isinstance(v, Box):
+ out_dict[k] = v.to_dict()
+ elif isinstance(v, box.BoxList):
+ out_dict[k] = v.to_list()
+ return out_dict
+
+ def update(self, *args, **kwargs):
+ if self._box_config["frozen_box"]:
+ raise BoxError("Box is frozen")
+ if (len(args) + int(bool(kwargs))) > 1:
+ raise BoxTypeError(f"update expected at most 1 argument, got {len(args) + int(bool(kwargs))}")
+ single_arg = next(iter(args), None)
+ if single_arg:
+ if hasattr(single_arg, "keys"):
+ for k in single_arg:
+ self.__convert_and_store(k, single_arg[k])
+ else:
+ for k, v in single_arg:
+ self.__convert_and_store(k, v)
+ for k in kwargs:
+ self.__convert_and_store(k, kwargs[k])
+
+ def merge_update(self, *args, **kwargs):
+ merge_type = None
+ if "box_merge_lists" in kwargs:
+ merge_type = kwargs.pop("box_merge_lists")
+
+ def convert_and_set(k, v):
+ intact_type = self._box_config["box_intact_types"] and isinstance(v, self._box_config["box_intact_types"])
+ if isinstance(v, dict) and not intact_type:
+ # Box objects must be created in case they are already
+ # in the `converted` box_config set
+ v = self._box_config["box_class"](v, **self.__box_config(extra_namespace=k))
+ if k in self and isinstance(self[k], dict):
+ self[k].merge_update(v, box_merge_lists=merge_type)
+ return
+ if isinstance(v, list) and not intact_type:
+ v = box.BoxList(v, **self.__box_config(extra_namespace=k))
+ if merge_type == "extend" and k in self and isinstance(self[k], list):
+ self[k].extend(v)
+ return
+ if merge_type == "unique" and k in self and isinstance(self[k], list):
+ for item in v:
+ if item not in self[k]:
+ self[k].append(item)
+ return
+ self.__setitem__(k, v)
+
+ if (len(args) + int(bool(kwargs))) > 1:
+ raise BoxTypeError(f"merge_update expected at most 1 argument, got {len(args) + int(bool(kwargs))}")
+ single_arg = next(iter(args), None)
+ if single_arg:
+ if hasattr(single_arg, "keys"):
+ for k in single_arg:
+ convert_and_set(k, single_arg[k])
+ else:
+ for k, v in single_arg:
+ convert_and_set(k, v)
+
+ for key in kwargs:
+ convert_and_set(key, kwargs[key])
+
+ def setdefault(self, item, default=None):
+ if item in self:
+ return self[item]
+
+ if self._box_config["box_dots"]:
+ if item in _get_dot_paths(self):
+ return self[item]
+
+ if isinstance(default, dict):
+ default = self._box_config["box_class"](default, **self.__box_config(extra_namespace=item))
+ if isinstance(default, list):
+ default = box.BoxList(default, **self.__box_config(extra_namespace=item))
+ self[item] = default
+ return self[item]
+
+ def _safe_attr(self, attr):
+ """Convert a key into something that is accessible as an attribute"""
+ if isinstance(attr, str):
+ # By assuming most people are using string first we get substantial speed ups
+ if attr.isidentifier() and not iskeyword(attr):
+ return attr
+
+ if isinstance(attr, tuple):
+ attr = "_".join([str(x) for x in attr])
+
+ attr = attr.decode("utf-8", "ignore") if isinstance(attr, bytes) else str(attr)
+ if self.__box_config()["camel_killer_box"]:
+ attr = _camel_killer(attr)
+
+ if attr.isidentifier() and not iskeyword(attr):
+ return attr
+
+ if sum(1 for character in attr if character.isidentifier() and not iskeyword(character)) == 0:
+ attr = f'{self.__box_config()["box_safe_prefix"]}{attr}'
+ if attr.isidentifier() and not iskeyword(attr):
+ return attr
+
+ out = []
+ last_safe = 0
+ for i, character in enumerate(attr):
+ if f"x{character}".isidentifier():
+ last_safe = i
+ out.append(character)
+ elif not out:
+ continue
+ else:
+ if last_safe == i - 1:
+ out.append("_")
+
+ out = "".join(out)[: last_safe + 1]
+
+ try:
+ int(out[0])
+ except (ValueError, IndexError):
+ pass
+ else:
+ out = f'{self.__box_config()["box_safe_prefix"]}{out}'
+
+ if iskeyword(out):
+ out = f'{self.__box_config()["box_safe_prefix"]}{out}'
+
+ return out
+
+ def _conversion_checks(self, item):
+ """
+ Internal use for checking if a duplicate safe attribute already exists
+
+ :param item: Item to see if a dup exists
+ """
+ safe_item = self._safe_attr(item)
+
+ if safe_item in self._box_config["__safe_keys"]:
+ dups = [f"{item}({safe_item})", f'{self._box_config["__safe_keys"][safe_item]}({safe_item})']
+ if self._box_config["box_duplicates"].startswith("warn"):
+ warnings.warn(f"Duplicate conversion attributes exist: {dups}", BoxWarning)
+ else:
+ raise BoxError(f"Duplicate conversion attributes exist: {dups}")
+
+ def to_json(
+ self,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **json_kwargs,
+ ):
+ """
+ Transform the Box object into a JSON string.
+
+ :param filename: If provided will save to file
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :param json_kwargs: additional arguments to pass to json.dump(s)
+ :return: string of JSON (if no filename provided)
+ """
+ return _to_json(self.to_dict(), filename=filename, encoding=encoding, errors=errors, **json_kwargs)
+
+ @classmethod
+ def from_json(
+ cls,
+ json_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+ ) -> "Box":
+ """
+ Transform a json object string into a Box object. If the incoming
+ json is a list, you must use BoxList.from_json.
+
+ :param json_string: string to pass to `json.loads`
+ :param filename: filename to open and pass to `json.load`
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :param kwargs: parameters to pass to `Box()` or `json.loads`
+ :return: Box object from json data
+ """
+ box_args = {}
+ for arg in kwargs.copy():
+ if arg in BOX_PARAMETERS:
+ box_args[arg] = kwargs.pop(arg)
+
+ data = _from_json(json_string, filename=filename, encoding=encoding, errors=errors, **kwargs)
+
+ if not isinstance(data, dict):
+ raise BoxError(f"json data not returned as a dictionary, but rather a {type(data).__name__}")
+ return cls(data, **box_args)
+
+ if yaml_available:
+
+ def to_yaml(
+ self,
+ filename: Optional[Union[str, PathLike]] = None,
+ default_flow_style: bool = False,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **yaml_kwargs,
+ ):
+ """
+ Transform the Box object into a YAML string.
+
+ :param filename: If provided will save to file
+ :param default_flow_style: False will recursively dump dicts
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :param yaml_kwargs: additional arguments to pass to yaml.dump
+ :return: string of YAML (if no filename provided)
+ """
+ return _to_yaml(
+ self.to_dict(),
+ filename=filename,
+ default_flow_style=default_flow_style,
+ encoding=encoding,
+ errors=errors,
+ **yaml_kwargs,
+ )
+
+ @classmethod
+ def from_yaml(
+ cls,
+ yaml_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+ ) -> "Box":
+ """
+ Transform a yaml object string into a Box object. By default will use SafeLoader.
+
+ :param yaml_string: string to pass to `yaml.load`
+ :param filename: filename to open and pass to `yaml.load`
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :param kwargs: parameters to pass to `Box()` or `yaml.load`
+ :return: Box object from yaml data
+ """
+ box_args = {}
+ for arg in kwargs.copy():
+ if arg in BOX_PARAMETERS:
+ box_args[arg] = kwargs.pop(arg)
+
+ data = _from_yaml(yaml_string=yaml_string, filename=filename, encoding=encoding, errors=errors, **kwargs)
+ if not data:
+ return cls(**box_args)
+ if not isinstance(data, dict):
+ raise BoxError(f"yaml data not returned as a dictionary but rather a {type(data).__name__}")
+ return cls(data, **box_args)
+
+ else:
+
+ def to_yaml(
+ self,
+ filename: Optional[Union[str, PathLike]] = None,
+ default_flow_style: bool = False,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **yaml_kwargs,
+ ):
+ raise BoxError('yaml is unavailable on this system, please install the "ruamel.yaml" or "PyYAML" package')
+
+ @classmethod
+ def from_yaml(
+ cls,
+ yaml_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+ ) -> "Box":
+ raise BoxError('yaml is unavailable on this system, please install the "ruamel.yaml" or "PyYAML" package')
+
+ if toml_write_library is not None:
+
+ def to_toml(
+ self, filename: Optional[Union[str, PathLike]] = None, encoding: str = "utf-8", errors: str = "strict"
+ ):
+ """
+ Transform the Box object into a toml string.
+
+ :param filename: File to write toml object too
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :return: string of TOML (if no filename provided)
+ """
+ return _to_toml(self.to_dict(), filename=filename, encoding=encoding, errors=errors)
+
+ else:
+
+ def to_toml(
+ self, filename: Optional[Union[str, PathLike]] = None, encoding: str = "utf-8", errors: str = "strict"
+ ):
+ raise BoxError('toml is unavailable on this system, please install the "tomli-w" package')
+
+ if toml_read_library is not None:
+
+ @classmethod
+ def from_toml(
+ cls,
+ toml_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+ ) -> "Box":
+ """
+ Transforms a toml string or file into a Box object
+
+ :param toml_string: string to pass to `toml.load`
+ :param filename: filename to open and pass to `toml.load`
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :param kwargs: parameters to pass to `Box()`
+ :return: Box object
+ """
+ box_args = {}
+ for arg in kwargs.copy():
+ if arg in BOX_PARAMETERS:
+ box_args[arg] = kwargs.pop(arg)
+
+ data = _from_toml(toml_string=toml_string, filename=filename, encoding=encoding, errors=errors)
+ return cls(data, **box_args)
+
+ else:
+
+ @classmethod
+ def from_toml(
+ cls,
+ toml_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+ ) -> "Box":
+ raise BoxError('toml is unavailable on this system, please install the "tomli" package')
+
+ if msgpack_available:
+
+ def to_msgpack(self, filename: Optional[Union[str, PathLike]] = None, **kwargs):
+ """
+ Transform the Box object into a msgpack string.
+
+ :param filename: File to write msgpack object too
+ :param kwargs: parameters to pass to `msgpack.pack`
+ :return: bytes of msgpack (if no filename provided)
+ """
+ return _to_msgpack(self.to_dict(), filename=filename, **kwargs)
+
+ @classmethod
+ def from_msgpack(
+ cls,
+ msgpack_bytes: Optional[bytes] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ **kwargs,
+ ) -> "Box":
+ """
+ Transforms msgpack bytes or file into a Box object
+
+ :param msgpack_bytes: string to pass to `msgpack.unpackb`
+ :param filename: filename to open and pass to `msgpack.unpack`
+ :param kwargs: parameters to pass to `Box()`
+ :return: Box object
+ """
+ box_args = {}
+ for arg in kwargs.copy():
+ if arg in BOX_PARAMETERS:
+ box_args[arg] = kwargs.pop(arg)
+
+ data = _from_msgpack(msgpack_bytes=msgpack_bytes, filename=filename, **kwargs)
+ if not isinstance(data, dict):
+ raise BoxError(f"msgpack data not returned as a dictionary but rather a {type(data).__name__}")
+ return cls(data, **box_args)
+
+ else:
+
+ def to_msgpack(self, filename: Optional[Union[str, PathLike]] = None, **kwargs):
+ raise BoxError('msgpack is unavailable on this system, please install the "msgpack" package')
+
+ @classmethod
+ def from_msgpack(
+ cls,
+ msgpack_bytes: Optional[bytes] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+ ) -> "Box":
+ raise BoxError('msgpack is unavailable on this system, please install the "msgpack" package')
diff --git a/box/box.pyi b/box/box.pyi
new file mode 100644
index 0000000..9d56ab3
--- /dev/null
+++ b/box/box.pyi
@@ -0,0 +1,121 @@
+from _typeshed import Incomplete
+from collections.abc import Mapping
+from os import PathLike
+from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union, Literal
+
+class Box(dict):
+ def __new__(
+ cls,
+ *args: Any,
+ default_box: bool = ...,
+ default_box_attr: Any = ...,
+ default_box_none_transform: bool = ...,
+ default_box_create_on_get: bool = ...,
+ frozen_box: bool = ...,
+ camel_killer_box: bool = ...,
+ conversion_box: bool = ...,
+ modify_tuples_box: bool = ...,
+ box_safe_prefix: str = ...,
+ box_duplicates: str = ...,
+ box_intact_types: Union[Tuple, List] = ...,
+ box_recast: Optional[Dict] = ...,
+ box_dots: bool = ...,
+ box_class: Optional[Union[Dict, Type["Box"]]] = ...,
+ box_namespace: Union[Tuple[str, ...], Literal[False]] = ...,
+ **kwargs: Any,
+ ): ...
+ def __init__(
+ self,
+ *args: Any,
+ default_box: bool = ...,
+ default_box_attr: Any = ...,
+ default_box_none_transform: bool = ...,
+ default_box_create_on_get: bool = ...,
+ frozen_box: bool = ...,
+ camel_killer_box: bool = ...,
+ conversion_box: bool = ...,
+ modify_tuples_box: bool = ...,
+ box_safe_prefix: str = ...,
+ box_duplicates: str = ...,
+ box_intact_types: Union[Tuple, List] = ...,
+ box_recast: Optional[Dict] = ...,
+ box_dots: bool = ...,
+ box_class: Optional[Union[Dict, Type["Box"]]] = ...,
+ box_namespace: Union[Tuple[str, ...], Literal[False]] = ...,
+ **kwargs: Any,
+ ) -> None: ...
+ def __add__(self, other: Mapping[Any, Any]): ...
+ def __radd__(self, other: Mapping[Any, Any]): ...
+ def __iadd__(self, other: Mapping[Any, Any]): ...
+ def __or__(self, other: Mapping[Any, Any]): ...
+ def __ror__(self, other: Mapping[Any, Any]): ...
+ def __ior__(self, other: Mapping[Any, Any]): ... # type: ignore[override]
+ def __sub__(self, other: Mapping[Any, Any]): ...
+ def __hash__(self): ...
+ def __dir__(self) -> List[str]: ...
+ def __contains__(self, item) -> bool: ...
+ def keys(self, dotted: Union[bool] = ...): ...
+ def items(self, dotted: Union[bool] = ...): ...
+ def get(self, key, default=...): ...
+ def copy(self) -> Box: ...
+ def __copy__(self) -> Box: ...
+ def __deepcopy__(self, memodict: Incomplete | None = ...) -> Box: ...
+ def __getitem__(self, item, _ignore_default: bool = ...): ...
+ def __getattr__(self, item): ...
+ def __setitem__(self, key, value): ...
+ def __setattr__(self, key, value): ...
+ def __delitem__(self, key): ...
+ def __delattr__(self, item) -> None: ...
+ def pop(self, key, *args): ...
+ def clear(self) -> None: ...
+ def popitem(self): ...
+ def __iter__(self) -> Generator: ...
+ def __reversed__(self) -> Generator: ...
+ def to_dict(self) -> Dict: ...
+ def update(self, *args, **kwargs) -> None: ...
+ def merge_update(self, *args, **kwargs) -> None: ...
+ def setdefault(self, item, default: Incomplete | None = ...): ...
+ def to_json(
+ self, filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ..., **json_kwargs
+ ): ...
+ @classmethod
+ def from_json(
+ cls,
+ json_string: Optional[str] = ...,
+ filename: Optional[Union[str, PathLike]] = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ **kwargs,
+ ) -> Box: ...
+ def to_yaml(
+ self,
+ filename: Optional[Union[str, PathLike]] = ...,
+ default_flow_style: bool = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ **yaml_kwargs,
+ ): ...
+ @classmethod
+ def from_yaml(
+ cls,
+ yaml_string: Optional[str] = ...,
+ filename: Optional[Union[str, PathLike]] = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ **kwargs,
+ ) -> Box: ...
+ def to_toml(self, filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ...): ...
+ @classmethod
+ def from_toml(
+ cls,
+ toml_string: Optional[str] = ...,
+ filename: Optional[Union[str, PathLike]] = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ **kwargs,
+ ) -> Box: ...
+ def to_msgpack(self, filename: Optional[Union[str, PathLike]] = ..., **kwargs): ...
+ @classmethod
+ def from_msgpack(
+ cls, msgpack_bytes: Optional[bytes] = ..., filename: Optional[Union[str, PathLike]] = ..., **kwargs
+ ) -> Box: ...
diff --git a/box/box_list.py b/box/box_list.py
new file mode 100644
index 0000000..fb00864
--- /dev/null
+++ b/box/box_list.py
@@ -0,0 +1,476 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017-2023 - Chris Griffith - MIT License
+import copy
+import re
+from os import PathLike
+from typing import Optional, Iterable, Type, Union, List, Any
+
+import box
+from box.converters import (
+ BOX_PARAMETERS,
+ _from_csv,
+ _from_json,
+ _from_msgpack,
+ _from_toml,
+ _from_yaml,
+ _to_csv,
+ _to_json,
+ _to_msgpack,
+ _to_toml,
+ _to_yaml,
+ msgpack_available,
+ toml_read_library,
+ yaml_available,
+)
+from box.exceptions import BoxError, BoxTypeError
+
+_list_pos_re = re.compile(r"\[(\d+)\]")
+
+
+class BoxList(list):
+ """
+ Drop in replacement of list, that converts added objects to Box or BoxList
+ objects as necessary.
+ """
+
+ def __new__(cls, *args, **kwargs):
+ obj = super().__new__(cls, *args, **kwargs)
+ # This is required for pickling to work correctly
+ obj.box_options = {"box_class": box.Box}
+ obj.box_options.update(kwargs)
+ obj.box_org_ref = None
+ return obj
+
+ def __init__(self, iterable: Optional[Iterable] = None, box_class: Type[box.Box] = box.Box, **box_options):
+ self.box_options = box_options
+ self.box_options["box_class"] = box_class
+ self.box_org_ref = iterable
+ if iterable:
+ for x in iterable:
+ self.append(x)
+ self.box_org_ref = None
+ if box_options.get("frozen_box"):
+
+ def frozen(*args, **kwargs):
+ raise BoxError("BoxList is frozen")
+
+ for method in ["append", "extend", "insert", "pop", "remove", "reverse", "sort"]:
+ self.__setattr__(method, frozen)
+
+ def __getitem__(self, item):
+ if self.box_options.get("box_dots") and isinstance(item, str) and item.startswith("["):
+ list_pos = _list_pos_re.search(item)
+ value = super().__getitem__(int(list_pos.groups()[0]))
+ if len(list_pos.group()) == len(item):
+ return value
+ return value.__getitem__(item[len(list_pos.group()) :].lstrip("."))
+ if isinstance(item, tuple):
+ result = self
+ for idx in item:
+ if isinstance(result, list):
+ result = result[idx]
+ else:
+ raise BoxTypeError(f"Cannot numpy-style indexing on {type(result).__name__}.")
+ return result
+ return super().__getitem__(item)
+
+ def __delitem__(self, key):
+ if self.box_options.get("frozen_box"):
+ raise BoxError("BoxList is frozen")
+ if self.box_options.get("box_dots") and isinstance(key, str) and key.startswith("["):
+ list_pos = _list_pos_re.search(key)
+ pos = int(list_pos.groups()[0])
+ if len(list_pos.group()) == len(key):
+ return super().__delitem__(pos)
+ if hasattr(self[pos], "__delitem__"):
+ return self[pos].__delitem__(key[len(list_pos.group()) :].lstrip(".")) # type: ignore
+ super().__delitem__(key)
+
+ def __setitem__(self, key, value):
+ if self.box_options.get("frozen_box"):
+ raise BoxError("BoxList is frozen")
+ if self.box_options.get("box_dots") and isinstance(key, str) and key.startswith("["):
+ list_pos = _list_pos_re.search(key)
+ pos = int(list_pos.groups()[0])
+ if pos >= len(self) and self.box_options.get("default_box"):
+ self.extend([None] * (pos - len(self) + 1))
+ if len(list_pos.group()) == len(key):
+ return super().__setitem__(pos, value)
+ children = key[len(list_pos.group()):].lstrip(".")
+ if self.box_options.get("default_box"):
+ if children[0] == "[":
+ super().__setitem__(pos, box.BoxList(**self.box_options))
+ else:
+ super().__setitem__(pos, self.box_options.get("box_class")(**self.box_options))
+ return super().__getitem__(pos).__setitem__(children, value)
+ super().__setitem__(key, value)
+
+ def _is_intact_type(self, obj):
+ if self.box_options.get("box_intact_types") and isinstance(obj, self.box_options["box_intact_types"]):
+ return True
+ return False
+
+ def _convert(self, p_object):
+ if isinstance(p_object, dict) and not self._is_intact_type(p_object):
+ p_object = self.box_options["box_class"](p_object, **self.box_options)
+ elif isinstance(p_object, box.Box):
+ p_object._box_config.update(self.box_options)
+ if isinstance(p_object, list) and not self._is_intact_type(p_object):
+ p_object = (
+ self
+ if p_object is self or p_object is self.box_org_ref
+ else self.__class__(p_object, **self.box_options)
+ )
+ elif isinstance(p_object, BoxList):
+ p_object.box_options.update(self.box_options)
+ return p_object
+
+ def append(self, p_object):
+ super().append(self._convert(p_object))
+
+ def extend(self, iterable):
+ for item in iterable:
+ self.append(item)
+
+ def insert(self, index, p_object):
+ super().insert(index, self._convert(p_object))
+
+ def _dotted_helper(self) -> List[str]:
+ keys = []
+ for idx, item in enumerate(self):
+ added = False
+ if isinstance(item, box.Box):
+ for key in item.keys(dotted=True):
+ keys.append(f"[{idx}].{key}")
+ added = True
+ elif isinstance(item, BoxList):
+ for key in item._dotted_helper():
+ keys.append(f"[{idx}]{key}")
+ added = True
+ if not added:
+ keys.append(f"[{idx}]")
+ return keys
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({self.to_list()})"
+
+ def __str__(self):
+ return str(self.to_list())
+
+ def __copy__(self):
+ return self.__class__((x for x in self), **self.box_options)
+
+ def __deepcopy__(self, memo=None):
+ out = self.__class__()
+ memo = memo or {}
+ memo[id(self)] = out
+ for k in self:
+ out.append(copy.deepcopy(k, memo=memo))
+ return out
+
+ def __hash__(self) -> int: # type: ignore[override]
+ if self.box_options.get("frozen_box"):
+ hashing = 98765
+ hashing ^= hash(tuple(self))
+ return hashing
+ raise BoxTypeError("unhashable type: 'BoxList'")
+
+ def to_list(self) -> List:
+ new_list: List[Any] = []
+ for x in self:
+ if x is self:
+ new_list.append(new_list)
+ elif isinstance(x, box.Box):
+ new_list.append(x.to_dict())
+ elif isinstance(x, BoxList):
+ new_list.append(x.to_list())
+ else:
+ new_list.append(x)
+ return new_list
+
+ def to_json(
+ self,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ multiline: bool = False,
+ **json_kwargs,
+ ):
+ """
+ Transform the BoxList object into a JSON string.
+
+ :param filename: If provided will save to file
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :param multiline: Put each item in list onto it's own line
+ :param json_kwargs: additional arguments to pass to json.dump(s)
+ :return: string of JSON or return of `json.dump`
+ """
+ if filename and multiline:
+ lines = [_to_json(item, filename=None, encoding=encoding, errors=errors, **json_kwargs) for item in self]
+ with open(filename, "w", encoding=encoding, errors=errors) as f:
+ f.write("\n".join(lines))
+ else:
+ return _to_json(self.to_list(), filename=filename, encoding=encoding, errors=errors, **json_kwargs)
+
+ @classmethod
+ def from_json(
+ cls,
+ json_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ multiline: bool = False,
+ **kwargs,
+ ):
+ """
+ Transform a json object string into a BoxList object. If the incoming
+ json is a dict, you must use Box.from_json.
+
+ :param json_string: string to pass to `json.loads`
+ :param filename: filename to open and pass to `json.load`
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :param multiline: One object per line
+ :param kwargs: parameters to pass to `Box()` or `json.loads`
+ :return: BoxList object from json data
+ """
+ box_args = {}
+ for arg in list(kwargs.keys()):
+ if arg in BOX_PARAMETERS:
+ box_args[arg] = kwargs.pop(arg)
+
+ data = _from_json(
+ json_string, filename=filename, encoding=encoding, errors=errors, multiline=multiline, **kwargs
+ )
+
+ if not isinstance(data, list):
+ raise BoxError(f"json data not returned as a list, but rather a {type(data).__name__}")
+ return cls(data, **box_args)
+
+ if yaml_available:
+
+ def to_yaml(
+ self,
+ filename: Optional[Union[str, PathLike]] = None,
+ default_flow_style: bool = False,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **yaml_kwargs,
+ ):
+ """
+ Transform the BoxList object into a YAML string.
+
+ :param filename: If provided will save to file
+ :param default_flow_style: False will recursively dump dicts
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :param yaml_kwargs: additional arguments to pass to yaml.dump
+ :return: string of YAML or return of `yaml.dump`
+ """
+ return _to_yaml(
+ self.to_list(),
+ filename=filename,
+ default_flow_style=default_flow_style,
+ encoding=encoding,
+ errors=errors,
+ **yaml_kwargs,
+ )
+
+ @classmethod
+ def from_yaml(
+ cls,
+ yaml_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+ ):
+ """
+ Transform a yaml object string into a BoxList object.
+
+ :param yaml_string: string to pass to `yaml.load`
+ :param filename: filename to open and pass to `yaml.load`
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :param kwargs: parameters to pass to `BoxList()` or `yaml.load`
+ :return: BoxList object from yaml data
+ """
+ box_args = {}
+ for arg in list(kwargs.keys()):
+ if arg in BOX_PARAMETERS:
+ box_args[arg] = kwargs.pop(arg)
+
+ data = _from_yaml(yaml_string=yaml_string, filename=filename, encoding=encoding, errors=errors, **kwargs)
+ if not data:
+ return cls(**box_args)
+ if not isinstance(data, list):
+ raise BoxError(f"yaml data not returned as a list but rather a {type(data).__name__}")
+ return cls(data, **box_args)
+
+ else:
+
+ def to_yaml(
+ self,
+ filename: Optional[Union[str, PathLike]] = None,
+ default_flow_style: bool = False,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **yaml_kwargs,
+ ):
+ raise BoxError('yaml is unavailable on this system, please install the "ruamel.yaml" or "PyYAML" package')
+
+ @classmethod
+ def from_yaml(
+ cls,
+ yaml_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+ ):
+ raise BoxError('yaml is unavailable on this system, please install the "ruamel.yaml" or "PyYAML" package')
+
+ if toml_read_library is not None:
+
+ def to_toml(
+ self,
+ filename: Optional[Union[str, PathLike]] = None,
+ key_name: str = "toml",
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ ):
+ """
+ Transform the BoxList object into a toml string.
+
+ :param filename: File to write toml object too
+ :param key_name: Specify the name of the key to store the string under
+ (cannot directly convert to toml)
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :return: string of TOML (if no filename provided)
+ """
+ return _to_toml({key_name: self.to_list()}, filename=filename, encoding=encoding, errors=errors)
+
+ else:
+
+ def to_toml(
+ self,
+ filename: Optional[Union[str, PathLike]] = None,
+ key_name: str = "toml",
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ ):
+ raise BoxError('toml is unavailable on this system, please install the "tomli-w" package')
+
+ if toml_read_library is not None:
+
+ @classmethod
+ def from_toml(
+ cls,
+ toml_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ key_name: str = "toml",
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+ ):
+ """
+ Transforms a toml string or file into a BoxList object
+
+ :param toml_string: string to pass to `toml.load`
+ :param filename: filename to open and pass to `toml.load`
+ :param key_name: Specify the name of the key to pull the list from
+ (cannot directly convert from toml)
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :param kwargs: parameters to pass to `Box()`
+ :return:
+ """
+ box_args = {}
+ for arg in list(kwargs.keys()):
+ if arg in BOX_PARAMETERS:
+ box_args[arg] = kwargs.pop(arg)
+
+ data = _from_toml(toml_string=toml_string, filename=filename, encoding=encoding, errors=errors)
+ if key_name not in data:
+ raise BoxError(f"{key_name} was not found.")
+ return cls(data[key_name], **box_args)
+
+ else:
+
+ @classmethod
+ def from_toml(
+ cls,
+ toml_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ key_name: str = "toml",
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+ ):
+ raise BoxError('toml is unavailable on this system, please install the "toml" package')
+
+ if msgpack_available:
+
+ def to_msgpack(self, filename: Optional[Union[str, PathLike]] = None, **kwargs):
+ """
+ Transform the BoxList object into a toml string.
+
+ :param filename: File to write toml object too
+ :return: string of TOML (if no filename provided)
+ """
+ return _to_msgpack(self.to_list(), filename=filename, **kwargs)
+
+ @classmethod
+ def from_msgpack(
+ cls, msgpack_bytes: Optional[bytes] = None, filename: Optional[Union[str, PathLike]] = None, **kwargs
+ ):
+ """
+ Transforms a toml string or file into a BoxList object
+
+ :param msgpack_bytes: string to pass to `msgpack.packb`
+ :param filename: filename to open and pass to `msgpack.pack`
+ :param kwargs: parameters to pass to `Box()`
+ :return:
+ """
+ box_args = {}
+ for arg in list(kwargs.keys()):
+ if arg in BOX_PARAMETERS:
+ box_args[arg] = kwargs.pop(arg)
+
+ data = _from_msgpack(msgpack_bytes=msgpack_bytes, filename=filename, **kwargs)
+ if not isinstance(data, list):
+ raise BoxError(f"msgpack data not returned as a list but rather a {type(data).__name__}")
+ return cls(data, **box_args)
+
+ else:
+
+ def to_msgpack(self, filename: Optional[Union[str, PathLike]] = None, **kwargs):
+ raise BoxError('msgpack is unavailable on this system, please install the "msgpack" package')
+
+ @classmethod
+ def from_msgpack(
+ cls,
+ msgpack_bytes: Optional[bytes] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+ ):
+ raise BoxError('msgpack is unavailable on this system, please install the "msgpack" package')
+
+ def to_csv(self, filename: Optional[Union[str, PathLike]] = None, encoding: str = "utf-8", errors: str = "strict"):
+ return _to_csv(self, filename=filename, encoding=encoding, errors=errors)
+
+ @classmethod
+ def from_csv(
+ cls,
+ csv_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ ):
+ return cls(_from_csv(csv_string=csv_string, filename=filename, encoding=encoding, errors=errors))
diff --git a/box/box_list.pyi b/box/box_list.pyi
new file mode 100644
index 0000000..982093f
--- /dev/null
+++ b/box/box_list.pyi
@@ -0,0 +1,83 @@
+import box
+from box.converters import (
+ BOX_PARAMETERS as BOX_PARAMETERS,
+ msgpack_available as msgpack_available,
+ toml_read_library as toml_read_library,
+ toml_write_library as toml_write_library,
+ yaml_available as yaml_available,
+)
+from os import PathLike as PathLike
+from typing import Any, Iterable, Optional, Type, Union, List
+
+class BoxList(list):
+ def __new__(cls, *args: Any, **kwargs: Any): ...
+ box_options: Any
+ box_org_ref: Any
+ def __init__(self, iterable: Iterable = ..., box_class: Type[box.Box] = ..., **box_options: Any) -> None: ...
+ def __getitem__(self, item: Any): ...
+ def __delitem__(self, key: Any): ...
+ def __setitem__(self, key: Any, value: Any): ...
+ def append(self, p_object: Any) -> None: ...
+ def extend(self, iterable: Any) -> None: ...
+ def insert(self, index: Any, p_object: Any) -> None: ...
+ def __copy__(self) -> "BoxList": ...
+ def __deepcopy__(self, memo: Optional[Any] = ...) -> "BoxList": ...
+ def __hash__(self) -> int: ... # type: ignore[override]
+ def to_list(self) -> List: ...
+ def _dotted_helper(self) -> List[str]: ...
+ def to_json(
+ self,
+ filename: Union[str, PathLike] = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ multiline: bool = ...,
+ **json_kwargs: Any,
+ ) -> Any: ...
+ @classmethod
+ def from_json(
+ cls,
+ json_string: str = ...,
+ filename: Union[str, PathLike] = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ multiline: bool = ...,
+ **kwargs: Any,
+ ) -> Any: ...
+ def to_yaml(
+ self,
+ filename: Union[str, PathLike] = ...,
+ default_flow_style: bool = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ **yaml_kwargs: Any,
+ ) -> Any: ...
+ @classmethod
+ def from_yaml(
+ cls,
+ yaml_string: str = ...,
+ filename: Union[str, PathLike] = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ **kwargs: Any,
+ ) -> Any: ...
+ def to_toml(
+ self, filename: Union[str, PathLike] = ..., key_name: str = ..., encoding: str = ..., errors: str = ...
+ ) -> Any: ...
+ @classmethod
+ def from_toml(
+ cls,
+ toml_string: str = ...,
+ filename: Union[str, PathLike] = ...,
+ key_name: str = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ **kwargs: Any,
+ ) -> Any: ...
+ def to_msgpack(self, filename: Union[str, PathLike] = ..., **kwargs: Any) -> Any: ...
+ @classmethod
+ def from_msgpack(cls, msgpack_bytes: bytes = ..., filename: Union[str, PathLike] = ..., **kwargs: Any) -> Any: ...
+ def to_csv(self, filename: Union[str, PathLike] = ..., encoding: str = ..., errors: str = ...) -> Any: ...
+ @classmethod
+ def from_csv(
+ cls, csv_string: str = ..., filename: Union[str, PathLike] = ..., encoding: str = ..., errors: str = ...
+ ) -> Any: ...
diff --git a/box/config_box.py b/box/config_box.py
new file mode 100644
index 0000000..4c48877
--- /dev/null
+++ b/box/config_box.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from typing import List
+
+from box.box import Box
+
+
+class ConfigBox(Box):
+ """
+ Modified box object to add object transforms.
+
+ Allows for build in transforms like:
+
+ cns = ConfigBox(my_bool='yes', my_int='5', my_list='5,4,3,3,2')
+
+ cns.bool('my_bool') # True
+ cns.int('my_int') # 5
+ cns.list('my_list', mod=lambda x: int(x)) # [5, 4, 3, 3, 2]
+ """
+
+ _protected_keys = dir(Box) + ["bool", "int", "float", "list", "getboolean", "getfloat", "getint"]
+
+ def __getattr__(self, item):
+ """
+ Config file keys are stored in lower case, be a little more
+ loosey goosey
+ """
+ try:
+ return super().__getattr__(item)
+ except AttributeError:
+ return super().__getattr__(item.lower())
+
+ def __dir__(self) -> List[str]:
+ return super().__dir__() + ["bool", "int", "float", "list", "getboolean", "getfloat", "getint"]
+
+ def bool(self, item, default=None):
+ """
+ Return value of key as a boolean
+
+ :param item: key of value to transform
+ :param default: value to return if item does not exist
+ :return: approximated bool of value
+ """
+ try:
+ item = self.__getattr__(item)
+ except AttributeError as err:
+ if default is not None:
+ return default
+ raise err
+
+ if isinstance(item, (bool, int)):
+ return bool(item)
+
+ if isinstance(item, str) and item.lower() in ("n", "no", "false", "f", "0"):
+ return False
+
+ return True if item else False
+
+ def int(self, item, default=None):
+ """
+ Return value of key as an int
+
+ :param item: key of value to transform
+ :param default: value to return if item does not exist
+ :return: int of value
+ """
+ try:
+ item = self.__getattr__(item)
+ except AttributeError as err:
+ if default is not None:
+ return default
+ raise err
+ return int(item)
+
+ def float(self, item, default=None):
+ """
+ Return value of key as a float
+
+ :param item: key of value to transform
+ :param default: value to return if item does not exist
+ :return: float of value
+ """
+ try:
+ item = self.__getattr__(item)
+ except AttributeError as err:
+ if default is not None:
+ return default
+ raise err
+ return float(item)
+
+ def list(self, item, default=None, spliter: str = ",", strip=True, mod=None):
+ """
+ Return value of key as a list
+
+ :param item: key of value to transform
+ :param mod: function to map against list
+ :param default: value to return if item does not exist
+ :param spliter: character to split str on
+ :param strip: clean the list with the `strip`
+ :return: list of items
+ """
+ try:
+ item = self.__getattr__(item)
+ except AttributeError as err:
+ if default is not None:
+ return default
+ raise err
+ if strip:
+ item = item.lstrip("[").rstrip("]")
+ out = [x.strip() if strip else x for x in item.split(spliter)]
+ if mod:
+ return list(map(mod, out))
+ return out
+
+ # loose configparser compatibility
+
+ def getboolean(self, item, default=None):
+ return self.bool(item, default)
+
+ def getint(self, item, default=None):
+ return self.int(item, default)
+
+ def getfloat(self, item, default=None):
+ return self.float(item, default)
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({str(self.to_dict())})"
+
+ def copy(self):
+ return ConfigBox(super().copy())
+
+ def __copy__(self):
+ return ConfigBox(super().copy())
diff --git a/box/config_box.pyi b/box/config_box.pyi
new file mode 100644
index 0000000..1022f1b
--- /dev/null
+++ b/box/config_box.pyi
@@ -0,0 +1,15 @@
+from box.box import Box as Box
+from typing import Any, Optional, List
+
+class ConfigBox(Box):
+ def __getattr__(self, item: Any): ...
+ def __dir__(self) -> List[str]: ...
+ def bool(self, item: Any, default: Optional[Any] = ...): ...
+ def int(self, item: Any, default: Optional[Any] = ...): ...
+ def float(self, item: Any, default: Optional[Any] = ...): ...
+ def list(self, item: Any, default: Optional[Any] = ..., spliter: str = ..., strip: bool = ..., mod: Optional[Any] = ...): ... # type: ignore
+ def getboolean(self, item: Any, default: Optional[Any] = ...): ...
+ def getint(self, item: Any, default: Optional[Any] = ...): ...
+ def getfloat(self, item: Any, default: Optional[Any] = ...): ...
+ def copy(self) -> "ConfigBox": ...
+ def __copy__(self) -> "ConfigBox": ...
diff --git a/box/converters.py b/box/converters.py
new file mode 100644
index 0000000..80c1ced
--- /dev/null
+++ b/box/converters.py
@@ -0,0 +1,363 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Abstract converter functions for use in any Box class
+
+import csv
+import json
+from io import StringIO
+from os import PathLike
+from pathlib import Path
+from typing import Union, Optional, Dict, Any, Callable
+
+from box.exceptions import BoxError
+
+pyyaml_available = True
+ruamel_available = True
+msgpack_available = True
+
+try:
+ from ruamel.yaml import version_info, YAML
+except ImportError:
+ ruamel_available = False
+else:
+ if version_info[1] < 17:
+ ruamel_available = False
+
+try:
+ import yaml
+except ImportError:
+ pyyaml_available = False
+
+MISSING_PARSER_ERROR = "No YAML Parser available, please install ruamel.yaml>=0.17 or PyYAML"
+
+toml_read_library: Optional[Any] = None
+toml_write_library: Optional[Any] = None
+toml_decode_error: Optional[Callable] = None
+
+__all__ = [
+ "_to_json",
+ "_to_yaml",
+ "_to_toml",
+ "_to_csv",
+ "_to_msgpack",
+ "_from_json",
+ "_from_yaml",
+ "_from_toml",
+ "_from_csv",
+ "_from_msgpack",
+]
+
+
+class BoxTomlDecodeError(BoxError):
+ """Toml Decode Error"""
+
+
+try:
+ import toml
+except ImportError:
+ pass
+else:
+ toml_read_library = toml
+ toml_write_library = toml
+ toml_decode_error = toml.TomlDecodeError
+
+ class BoxTomlDecodeError(BoxError, toml.TomlDecodeError): # type: ignore
+ """Toml Decode Error"""
+
+
+try:
+ import tomllib
+except ImportError:
+ pass
+else:
+ toml_read_library = tomllib
+ toml_decode_error = tomllib.TOMLDecodeError
+
+ class BoxTomlDecodeError(BoxError, tomllib.TOMLDecodeError): # type: ignore
+ """Toml Decode Error"""
+
+
+try:
+ import tomli
+except ImportError:
+ pass
+else:
+ toml_read_library = tomli
+ toml_decode_error = tomli.TOMLDecodeError
+
+ class BoxTomlDecodeError(BoxError, tomli.TOMLDecodeError): # type: ignore
+ """Toml Decode Error"""
+
+
+try:
+ import tomli_w
+except ImportError:
+ pass
+else:
+ toml_write_library = tomli_w
+
+
+try:
+ import msgpack # type: ignore
+except ImportError:
+ msgpack = None # type: ignore
+ msgpack_available = False
+
+yaml_available = pyyaml_available or ruamel_available
+
+BOX_PARAMETERS = (
+ "default_box",
+ "default_box_attr",
+ "default_box_none_transform",
+ "default_box_create_on_get",
+ "frozen_box",
+ "camel_killer_box",
+ "conversion_box",
+ "modify_tuples_box",
+ "box_safe_prefix",
+ "box_duplicates",
+ "box_intact_types",
+ "box_dots",
+ "box_recast",
+ "box_class",
+ "box_namespace",
+)
+
+
+def _exists(filename: Union[str, PathLike], create: bool = False) -> Path:
+ path = Path(filename)
+ if create:
+ try:
+ path.touch(exist_ok=True)
+ except OSError as err:
+ raise BoxError(f"Could not create file {filename} - {err}")
+ else:
+ return path
+ if not path.exists():
+ raise BoxError(f'File "{filename}" does not exist')
+ if not path.is_file():
+ raise BoxError(f"{filename} is not a file")
+ return path
+
+
+def _to_json(
+ obj, filename: Optional[Union[str, PathLike]] = None, encoding: str = "utf-8", errors: str = "strict", **json_kwargs
+):
+ if filename:
+ _exists(filename, create=True)
+ with open(filename, "w", encoding=encoding, errors=errors) as f:
+ json.dump(obj, f, ensure_ascii=False, **json_kwargs)
+ else:
+ return json.dumps(obj, ensure_ascii=False, **json_kwargs)
+
+
+def _from_json(
+ json_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ multiline: bool = False,
+ **kwargs,
+):
+ if filename:
+ with open(filename, "r", encoding=encoding, errors=errors) as f:
+ if multiline:
+ data = [
+ json.loads(line.strip(), **kwargs)
+ for line in f
+ if line.strip() and not line.strip().startswith("#")
+ ]
+ else:
+ data = json.load(f, **kwargs)
+ elif json_string:
+ data = json.loads(json_string, **kwargs)
+ else:
+ raise BoxError("from_json requires a string or filename")
+ return data
+
+
+def _to_yaml(
+ obj,
+ filename: Optional[Union[str, PathLike]] = None,
+ default_flow_style: bool = False,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ ruamel_typ: str = "rt",
+ ruamel_attrs: Optional[Dict] = None,
+ **yaml_kwargs,
+):
+ if not ruamel_attrs:
+ ruamel_attrs = {}
+ if filename:
+ _exists(filename, create=True)
+ with open(filename, "w", encoding=encoding, errors=errors) as f:
+ if ruamel_available:
+ yaml_dumper = YAML(typ=ruamel_typ)
+ yaml_dumper.default_flow_style = default_flow_style
+ for attr, value in ruamel_attrs.items():
+ setattr(yaml_dumper, attr, value)
+ return yaml_dumper.dump(obj, stream=f, **yaml_kwargs)
+ elif pyyaml_available:
+ return yaml.dump(obj, stream=f, default_flow_style=default_flow_style, **yaml_kwargs)
+ else:
+ raise BoxError(MISSING_PARSER_ERROR)
+
+ else:
+ if ruamel_available:
+ yaml_dumper = YAML(typ=ruamel_typ)
+ yaml_dumper.default_flow_style = default_flow_style
+ for attr, value in ruamel_attrs.items():
+ setattr(yaml_dumper, attr, value)
+ with StringIO() as string_stream:
+ yaml_dumper.dump(obj, stream=string_stream, **yaml_kwargs)
+ return string_stream.getvalue()
+ elif pyyaml_available:
+ return yaml.dump(obj, default_flow_style=default_flow_style, **yaml_kwargs)
+ else:
+ raise BoxError(MISSING_PARSER_ERROR)
+
+
+def _from_yaml(
+ yaml_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ ruamel_typ: str = "rt",
+ ruamel_attrs: Optional[Dict] = None,
+ **kwargs,
+):
+ if not ruamel_attrs:
+ ruamel_attrs = {}
+ if filename:
+ _exists(filename)
+ with open(filename, "r", encoding=encoding, errors=errors) as f:
+ if ruamel_available:
+ yaml_loader = YAML(typ=ruamel_typ)
+ for attr, value in ruamel_attrs.items():
+ setattr(yaml_loader, attr, value)
+ data = yaml_loader.load(stream=f)
+ elif pyyaml_available:
+ if "Loader" not in kwargs:
+ kwargs["Loader"] = yaml.SafeLoader
+ data = yaml.load(f, **kwargs)
+ else:
+ raise BoxError(MISSING_PARSER_ERROR)
+ elif yaml_string:
+ if ruamel_available:
+ yaml_loader = YAML(typ=ruamel_typ)
+ for attr, value in ruamel_attrs.items():
+ setattr(yaml_loader, attr, value)
+ data = yaml_loader.load(stream=yaml_string)
+ elif pyyaml_available:
+ if "Loader" not in kwargs:
+ kwargs["Loader"] = yaml.SafeLoader
+ data = yaml.load(yaml_string, **kwargs)
+ else:
+ raise BoxError(MISSING_PARSER_ERROR)
+ else:
+ raise BoxError("from_yaml requires a string or filename")
+ return data
+
+
+def _to_toml(obj, filename: Optional[Union[str, PathLike]] = None, encoding: str = "utf-8", errors: str = "strict"):
+ if filename:
+ _exists(filename, create=True)
+ if toml_write_library.__name__ == "toml": # type: ignore
+ with open(filename, "w", encoding=encoding, errors=errors) as f:
+ try:
+ toml_write_library.dump(obj, f) # type: ignore
+ except toml_decode_error as err: # type: ignore
+ raise BoxTomlDecodeError(err) from err
+ else:
+ with open(filename, "wb") as f:
+ try:
+ toml_write_library.dump(obj, f) # type: ignore
+ except toml_decode_error as err: # type: ignore
+ raise BoxTomlDecodeError(err) from err
+ else:
+ try:
+ return toml_write_library.dumps(obj) # type: ignore
+ except toml_decode_error as err: # type: ignore
+ raise BoxTomlDecodeError(err) from err
+
+
+def _from_toml(
+ toml_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+):
+ if filename:
+ _exists(filename)
+ if toml_read_library.__name__ == "toml": # type: ignore
+ with open(filename, "r", encoding=encoding, errors=errors) as f:
+ data = toml_read_library.load(f) # type: ignore
+ else:
+ with open(filename, "rb") as f:
+ data = toml_read_library.load(f) # type: ignore
+ elif toml_string:
+ data = toml_read_library.loads(toml_string) # type: ignore
+ else:
+ raise BoxError("from_toml requires a string or filename")
+ return data
+
+
+def _to_msgpack(obj, filename: Optional[Union[str, PathLike]] = None, **kwargs):
+ if filename:
+ _exists(filename, create=True)
+ with open(filename, "wb") as f:
+ msgpack.pack(obj, f, **kwargs)
+ else:
+ return msgpack.packb(obj, **kwargs)
+
+
+def _from_msgpack(msgpack_bytes: Optional[bytes] = None, filename: Optional[Union[str, PathLike]] = None, **kwargs):
+ if filename:
+ _exists(filename)
+ with open(filename, "rb") as f:
+ data = msgpack.unpack(f, **kwargs)
+ elif msgpack_bytes:
+ data = msgpack.unpackb(msgpack_bytes, **kwargs)
+ else:
+ raise BoxError("from_msgpack requires a string or filename")
+ return data
+
+
+def _to_csv(
+ box_list, filename: Optional[Union[str, PathLike]] = None, encoding: str = "utf-8", errors: str = "strict", **kwargs
+):
+ csv_column_names = list(box_list[0].keys())
+ for row in box_list:
+ if list(row.keys()) != csv_column_names:
+ raise BoxError("BoxList must contain the same dictionary structure for every item to convert to csv")
+
+ if filename:
+ _exists(filename, create=True)
+ out_data = open(filename, "w", encoding=encoding, errors=errors, newline="")
+ else:
+ out_data = StringIO("")
+ writer = csv.DictWriter(out_data, fieldnames=csv_column_names, **kwargs)
+ writer.writeheader()
+ for data in box_list:
+ writer.writerow(data)
+ if not filename:
+ return out_data.getvalue() # type: ignore
+ out_data.close()
+
+
+def _from_csv(
+ csv_string: Optional[str] = None,
+ filename: Optional[Union[str, PathLike]] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+):
+ if csv_string:
+ with StringIO(csv_string) as cs:
+ reader = csv.DictReader(cs)
+ return [row for row in reader]
+ _exists(filename) # type: ignore
+ with open(filename, "r", encoding=encoding, errors=errors, newline="") as f: # type: ignore
+ reader = csv.DictReader(f, **kwargs)
+ return [row for row in reader]
diff --git a/box/converters.pyi b/box/converters.pyi
new file mode 100644
index 0000000..43d2020
--- /dev/null
+++ b/box/converters.pyi
@@ -0,0 +1,60 @@
+from typing import Any, Callable, Optional, Union, Dict
+from os import PathLike
+
+yaml_available: bool
+toml_available: bool
+msgpack_available: bool
+BOX_PARAMETERS: Any
+toml_read_library: Optional[Any]
+toml_write_library: Optional[Any]
+toml_decode_error: Optional[Callable]
+
+def _to_json(
+ obj, filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ..., **json_kwargs
+): ...
+def _from_json(
+ json_string: Optional[str] = ...,
+ filename: Optional[Union[str, PathLike]] = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ multiline: bool = ...,
+ **kwargs,
+): ...
+def _to_yaml(
+ obj,
+ filename: Optional[Union[str, PathLike]] = ...,
+ default_flow_style: bool = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ ruamel_typ: str = ...,
+ ruamel_attrs: Optional[Dict] = ...,
+ **yaml_kwargs,
+): ...
+def _from_yaml(
+ yaml_string: Optional[str] = ...,
+ filename: Optional[Union[str, PathLike]] = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ ruamel_typ: str = ...,
+ ruamel_attrs: Optional[Dict] = ...,
+ **kwargs,
+): ...
+def _to_toml(obj, filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ...): ...
+def _from_toml(
+ toml_string: Optional[str] = ...,
+ filename: Optional[Union[str, PathLike]] = ...,
+ encoding: str = ...,
+ errors: str = ...,
+): ...
+def _to_msgpack(obj, filename: Optional[Union[str, PathLike]] = ..., **kwargs): ...
+def _from_msgpack(msgpack_bytes: Optional[bytes] = ..., filename: Optional[Union[str, PathLike]] = ..., **kwargs): ...
+def _to_csv(
+ box_list, filename: Optional[Union[str, PathLike]] = ..., encoding: str = ..., errors: str = ..., **kwargs
+): ...
+def _from_csv(
+ csv_string: Optional[str] = ...,
+ filename: Optional[Union[str, PathLike]] = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ **kwargs,
+): ...
diff --git a/box/exceptions.py b/box/exceptions.py
new file mode 100644
index 0000000..18c82d0
--- /dev/null
+++ b/box/exceptions.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+
+class BoxError(Exception):
+ """Non standard dictionary exceptions"""
+
+
+class BoxKeyError(BoxError, KeyError, AttributeError):
+ """Key does not exist"""
+
+
+class BoxTypeError(BoxError, TypeError):
+ """Cannot handle that instance's type"""
+
+
+class BoxValueError(BoxError, ValueError):
+ """Issue doing something with that value"""
+
+
+class BoxWarning(UserWarning):
+ """Here be dragons"""
diff --git a/box/exceptions.pyi b/box/exceptions.pyi
new file mode 100644
index 0000000..2be6b54
--- /dev/null
+++ b/box/exceptions.pyi
@@ -0,0 +1,5 @@
+class BoxError(Exception): ...
+class BoxKeyError(BoxError, KeyError, AttributeError): ...
+class BoxTypeError(BoxError, TypeError): ...
+class BoxValueError(BoxError, ValueError): ...
+class BoxWarning(UserWarning): ...
diff --git a/box/from_file.py b/box/from_file.py
new file mode 100644
index 0000000..8f4ce68
--- /dev/null
+++ b/box/from_file.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from json import JSONDecodeError
+from os import PathLike
+from pathlib import Path
+from typing import Optional, Callable, Dict, Union
+import sys
+
+from box.box import Box
+from box.box_list import BoxList
+from box.converters import msgpack_available, toml_read_library, yaml_available, toml_decode_error
+from box.exceptions import BoxError
+
+try:
+ from ruamel.yaml import YAMLError
+except ImportError:
+ try:
+ from yaml import YAMLError # type: ignore
+ except ImportError:
+ YAMLError = False # type: ignore
+
+try:
+ from msgpack import UnpackException # type: ignore
+except ImportError:
+ UnpackException = False # type: ignore
+
+
+__all__ = ["box_from_file", "box_from_string"]
+
+
+def _to_json(file, encoding, errors, **kwargs):
+ try:
+ return Box.from_json(filename=file, encoding=encoding, errors=errors, **kwargs)
+ except JSONDecodeError:
+ raise BoxError("File is not JSON as expected")
+ except BoxError:
+ return BoxList.from_json(filename=file, encoding=encoding, errors=errors, **kwargs)
+
+
+def _to_csv(file, encoding, errors, **kwargs):
+ return BoxList.from_csv(filename=file, encoding=encoding, errors=errors, **kwargs)
+
+
+def _to_yaml(file, encoding, errors, **kwargs):
+ if not yaml_available:
+ raise BoxError(
+ f'File "{file}" is yaml but no package is available to open it. Please install "ruamel.yaml" or "PyYAML"'
+ )
+ try:
+ return Box.from_yaml(filename=file, encoding=encoding, errors=errors, **kwargs)
+ except YAMLError:
+ raise BoxError("File is not YAML as expected")
+ except BoxError:
+ return BoxList.from_yaml(filename=file, encoding=encoding, errors=errors, **kwargs)
+
+
+def _to_toml(file, encoding, errors, **kwargs):
+ if not toml_read_library:
+ raise BoxError(f'File "{file}" is toml but no package is available to open it. Please install "tomli"')
+ try:
+ return Box.from_toml(filename=file, encoding=encoding, errors=errors, **kwargs)
+ except toml_decode_error:
+ raise BoxError("File is not TOML as expected")
+
+
+def _to_msgpack(file, _, __, **kwargs):
+ if not msgpack_available:
+ raise BoxError(f'File "{file}" is msgpack but no package is available to open it. Please install "msgpack"')
+ try:
+ return Box.from_msgpack(filename=file, **kwargs)
+ except (UnpackException, ValueError):
+ raise BoxError("File is not msgpack as expected")
+ except BoxError:
+ return BoxList.from_msgpack(filename=file, **kwargs)
+
+
+converters = {
+ "json": _to_json,
+ "jsn": _to_json,
+ "yaml": _to_yaml,
+ "yml": _to_yaml,
+ "toml": _to_toml,
+ "tml": _to_toml,
+ "msgpack": _to_msgpack,
+ "pack": _to_msgpack,
+ "csv": _to_csv,
+} # type: Dict[str, Callable]
+
+
+def box_from_file(
+ file: Union[str, PathLike],
+ file_type: Optional[str] = None,
+ encoding: str = "utf-8",
+ errors: str = "strict",
+ **kwargs,
+) -> Union[Box, BoxList]:
+ """
+ Loads the provided file and tries to parse it into a Box or BoxList object as appropriate.
+
+ :param file: Location of file
+ :param encoding: File encoding
+ :param errors: How to handle encoding errors
+ :param file_type: manually specify file type: json, toml or yaml
+ :return: Box or BoxList
+ """
+
+ if not isinstance(file, Path):
+ file = Path(file)
+ if not file.exists():
+ raise BoxError(f'file "{file}" does not exist')
+ file_type = file_type or file.suffix
+ file_type = file_type.lower().lstrip(".")
+ if file_type.lower() in converters:
+ return converters[file_type.lower()](file, encoding, errors, **kwargs) # type: ignore
+ raise BoxError(f'"{file_type}" is an unknown type. Please use either csv, toml, msgpack, yaml or json')
+
+
+def box_from_string(content: str, string_type: str = "json") -> Union[Box, BoxList]:
+ """
+ Parse the provided string into a Box or BoxList object as appropriate.
+
+ :param content: String to parse
+ :param string_type: manually specify file type: json, toml or yaml
+ :return: Box or BoxList
+ """
+
+ if string_type == "json":
+ try:
+ return Box.from_json(json_string=content)
+ except JSONDecodeError:
+ raise BoxError("File is not JSON as expected")
+ except BoxError:
+ return BoxList.from_json(json_string=content)
+ elif string_type == "toml":
+ try:
+ return Box.from_toml(toml_string=content)
+ except toml_decode_error: # type: ignore
+ raise BoxError("File is not TOML as expected")
+ except BoxError:
+ return BoxList.from_toml(toml_string=content)
+ elif string_type == "yaml":
+ try:
+ return Box.from_yaml(yaml_string=content)
+ except YAMLError:
+ raise BoxError("File is not YAML as expected")
+ except BoxError:
+ return BoxList.from_yaml(yaml_string=content)
+ else:
+ raise BoxError(f"Unsupported string_string of {string_type}")
diff --git a/box/from_file.pyi b/box/from_file.pyi
new file mode 100644
index 0000000..9e8be8a
--- /dev/null
+++ b/box/from_file.pyi
@@ -0,0 +1,16 @@
+from box.box import Box as Box
+from box.box_list import BoxList as BoxList
+from os import PathLike
+from typing import Any, Union
+
+def box_from_file(
+ file: Union[str, PathLike],
+ file_type: str = ...,
+ encoding: str = ...,
+ errors: str = ...,
+ **kwargs: Any,
+) -> Union[Box, BoxList]: ...
+def box_from_string(
+ content: str,
+ string_type: str = ...,
+) -> Union[Box, BoxList]: ...
diff --git a/box/py.typed b/box/py.typed
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/box/py.typed
diff --git a/box/shorthand_box.py b/box/shorthand_box.py
new file mode 100644
index 0000000..a82edbd
--- /dev/null
+++ b/box/shorthand_box.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from typing import Dict
+
+from box.box import Box
+
+__all__ = ["SBox", "DDBox"]
+
+
+class SBox(Box):
+ """
+ ShorthandBox (SBox) allows for
+ property access of `dict` `json` and `yaml`
+ """
+
+ _protected_keys = dir({}) + [
+ "to_dict",
+ "to_json",
+ "to_yaml",
+ "json",
+ "yaml",
+ "from_yaml",
+ "from_json",
+ "dict",
+ "toml",
+ "from_toml",
+ "to_toml",
+ ]
+
+ @property
+ def dict(self) -> Dict:
+ return self.to_dict()
+
+ @property
+ def json(self) -> str:
+ return self.to_json()
+
+ @property
+ def yaml(self) -> str:
+ return self.to_yaml()
+
+ @property
+ def toml(self) -> str:
+ return self.to_toml()
+
+ def __repr__(self):
+ return f"{self.__class__.__name__}({self})"
+
+ def copy(self) -> "SBox":
+ return SBox(super(SBox, self).copy())
+
+ def __copy__(self) -> "SBox":
+ return SBox(super(SBox, self).copy())
+
+
+class DDBox(SBox):
+ def __init__(self, *args, **kwargs):
+ kwargs["box_dots"] = True
+ kwargs["default_box"] = True
+ super().__init__(*args, **kwargs)
+
+ def __new__(cls, *args, **kwargs):
+ obj = super().__new__(cls, *args, **kwargs)
+ obj._box_config["box_dots"] = True
+ obj._box_config["default_box"] = True
+ return obj
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}({self})"
diff --git a/box/shorthand_box.pyi b/box/shorthand_box.pyi
new file mode 100644
index 0000000..deef693
--- /dev/null
+++ b/box/shorthand_box.pyi
@@ -0,0 +1,17 @@
+from typing import Dict
+
+from box.box import Box as Box
+
+class SBox(Box):
+ @property
+ def dict(self) -> Dict: ...
+ @property
+ def json(self) -> str: ...
+ @property
+ def yaml(self) -> str: ...
+ @property
+ def toml(self) -> str: ...
+ def copy(self) -> "SBox": ...
+ def __copy__(self) -> "SBox": ...
+
+class DDBox(Box): ...
diff --git a/debian/.gitignore b/debian/.gitignore
new file mode 100644
index 0000000..2c8afeb
--- /dev/null
+++ b/debian/.gitignore
@@ -0,0 +1 @@
+/files
diff --git a/debian/changelog b/debian/changelog
index d4c552b..f0cdfcb 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,18 @@
+python-box (7.3.2-1) unstable; urgency=medium
+
+ * Team upload.
+
+ [ Debian Janitor ]
+ * Update standards version to 4.6.1, no changes needed.
+
+ [ Colin Watson ]
+ * d/watch: Adjust for PyPI normalization.
+ * New upstream release. Now Architecture: any due to using Cython.
+ * Use pybuild-plugin-pyproject.
+ * Mark autopkgtest as superficial (closes: #974492).
+
+ -- Colin Watson <cjwatson@debian.org> Mon, 24 Feb 2025 13:08:37 +0000
+
python-box (3.4.6-3) unstable; urgency=medium
[ Ondřej Nový ]
diff --git a/debian/control b/debian/control
index ce1ff76..a080c70 100644
--- a/debian/control
+++ b/debian/control
@@ -5,26 +5,30 @@ Maintainer: Debian Python Team <team+python@tracker.debian.org>
Uploaders:
Michal Arbet <michal.arbet@ultimum.io>,
Build-Depends:
+ cython3,
debhelper-compat (= 13),
dh-python,
- python3-all,
+ pybuild-plugin-pyproject,
+ python3-all-dev,
+ python3-msgpack,
+ python3-pytest <!nocheck>,
+ python3-ruamel.yaml,
python3-setuptools,
-Build-Depends-Indep:
+ python3-tomli-w,
python3-yaml,
- python3-pytest,
- python3-pytest-runner,
-Standards-Version: 4.5.1
+Standards-Version: 4.6.1
Vcs-Browser: https://salsa.debian.org/python-team/packages/python-box
Vcs-Git: https://salsa.debian.org/python-team/packages/python-box.git
Homepage: https://github.com/cdgriffith/Box
Rules-Requires-Root: no
Package: python3-box
-Architecture: all
+Architecture: any
Depends:
python3-yaml,
${misc:Depends},
${python3:Depends},
+ ${shlibs:Depends},
Description: Python dictionaries with advanced dot notation access
Box is designed to be an easy drop in transparently replacements
for dictionaries, thanks to Python's duck typing capabilities, which
diff --git a/debian/patches/do-not-install-binary.py b/debian/patches/do-not-install-binary.py
deleted file mode 100644
index 423945c..0000000
--- a/debian/patches/do-not-install-binary.py
+++ /dev/null
@@ -1,20 +0,0 @@
-Description: Do not install box.py
- to /usr/bin as box is python module.
- /usr/bin/box.py is useless.
-Author: Michal Arbet <michal.arbet@ultimum.io>
-Forwarded: not-needed
-Last-Update: 2020-03-31
-
-diff --git a/setup.py b/setup.py
-index 5eaef7b..3a76663 100644
---- a/setup.py
-+++ b/setup.py
-@@ -29,7 +29,7 @@ setup(
- author_email='chris@cdgriffith.com',
- description='Advanced Python dictionaries with dot notation access',
- long_description=long_description,
-- scripts=['box.py'],
-+ #scripts=['box.py'],
- py_modules=['box'],
- include_package_data=True,
- platforms='any',
diff --git a/debian/patches/series b/debian/patches/series
index 35137a2..1409dbf 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,2 +1 @@
use-py3.patch
-do-not-install-binary.py
diff --git a/debian/patches/use-py3.patch b/debian/patches/use-py3.patch
index 7b5e41b..930ab51 100644
--- a/debian/patches/use-py3.patch
+++ b/debian/patches/use-py3.patch
@@ -1,22 +1,111 @@
-Description: Fixed shebangs to python3
- as dh_python3 --shebang=/usr/bin/python3
- was not working everywhere.
-Author: Michal Arbet <michal.arbet@ultimum.io>
+From: Michal Arbet <michal.arbet@ultimum.io>
+Date: Mon, 24 Feb 2025 12:49:20 +0000
+Subject: Fixed shebangs to python3
+
+as dh_python3 --shebang=/usr/bin/python3 was not working everywhere.
+
Forwarded: not-needed
-Last-Update: 2020-03-31
+Last-Update: 2025-02-24
+---
+ box/__init__.py | 2 +-
+ box/box.py | 2 +-
+ box/box_list.py | 2 +-
+ box/config_box.py | 2 +-
+ box/converters.py | 2 +-
+ box/exceptions.py | 2 +-
+ box/from_file.py | 2 +-
+ box/shorthand_box.py | 2 +-
+ setup.py | 2 +-
+ test/test_box.py | 2 +-
+ test/test_box_list.py | 2 +-
+ test/test_config_box.py | 2 +-
+ test/test_converters.py | 2 +-
+ test/test_from_file.py | 2 +-
+ test/test_sbox.py | 2 +-
+ 15 files changed, 15 insertions(+), 15 deletions(-)
-diff --git a/box.py b/box.py
-index 94e3def..ead3a8e 100644
---- a/box.py
-+++ b/box.py
+diff --git a/box/__init__.py b/box/__init__.py
+index 7cd918f..3c6f75b 100644
+--- a/box/__init__.py
++++ b/box/__init__.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python3
+ # -*- coding: utf-8 -*-
+
+ __author__ = "Chris Griffith"
+diff --git a/box/box.py b/box/box.py
+index 8be6738..66dd30e 100644
+--- a/box/box.py
++++ b/box/box.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
- # -*- coding: UTF-8 -*-
+ # -*- coding: utf-8 -*-
#
- # Copyright (c) 2017-2019 - Chris Griffith - MIT License
+ # Copyright (c) 2017-2023 - Chris Griffith - MIT License
+diff --git a/box/box_list.py b/box/box_list.py
+index fb00864..1be4a91 100644
+--- a/box/box_list.py
++++ b/box/box_list.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python3
+ # -*- coding: utf-8 -*-
+ #
+ # Copyright (c) 2017-2023 - Chris Griffith - MIT License
+diff --git a/box/config_box.py b/box/config_box.py
+index 4c48877..2d822cb 100644
+--- a/box/config_box.py
++++ b/box/config_box.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python3
+ # -*- coding: utf-8 -*-
+ from typing import List
+
+diff --git a/box/converters.py b/box/converters.py
+index 80c1ced..c76e4da 100644
+--- a/box/converters.py
++++ b/box/converters.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python3
+ # -*- coding: utf-8 -*-
+
+ # Abstract converter functions for use in any Box class
+diff --git a/box/exceptions.py b/box/exceptions.py
+index 18c82d0..64e7f47 100644
+--- a/box/exceptions.py
++++ b/box/exceptions.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python3
+ # -*- coding: utf-8 -*-
+
+
+diff --git a/box/from_file.py b/box/from_file.py
+index 8f4ce68..4166ba8 100644
+--- a/box/from_file.py
++++ b/box/from_file.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python3
+ # -*- coding: utf-8 -*-
+ from json import JSONDecodeError
+ from os import PathLike
+diff --git a/box/shorthand_box.py b/box/shorthand_box.py
+index a82edbd..628f6d5 100644
+--- a/box/shorthand_box.py
++++ b/box/shorthand_box.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python3
+ # -*- coding: utf-8 -*-
+ from typing import Dict
+
diff --git a/setup.py b/setup.py
-index 5eaef7b..3a76663 100644
+index 8abda61..d7bd112 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,4 @@
@@ -24,24 +113,64 @@ index 5eaef7b..3a76663 100644
+#!/usr/bin/env python3
# -*- coding: utf-8 -*-
- from setuptools import setup
-diff --git a/test/test_functional_box.py b/test/test_functional_box.py
-index 8db60ab..5f02571 100644
---- a/test/test_functional_box.py
-+++ b/test/test_functional_box.py
+ # Must import multiprocessing as a fix for issues with testing, experienced on win10
+diff --git a/test/test_box.py b/test/test_box.py
+index 1232f3c..6f1fc9d 100644
+--- a/test/test_box.py
++++ b/test/test_box.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
- # -*- coding: UTF-8 -*-
+ # -*- coding: utf-8 -*-
# Test files gathered from json.org and yaml.org
- from __future__ import absolute_import
-diff --git a/test/test_unittests_box.py b/test/test_unittests_box.py
-index 8dbf6e0..146d73c 100644
---- a/test/test_unittests_box.py
-+++ b/test/test_unittests_box.py
+ import copy
+diff --git a/test/test_box_list.py b/test/test_box_list.py
+index 99c65d7..043db86 100644
+--- a/test/test_box_list.py
++++ b/test/test_box_list.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
- # -*- coding: UTF-8 -*-
- from __future__ import absolute_import
+ # -*- coding: utf-8 -*-
+ # Test files gathered from json.org and yaml.org
+diff --git a/test/test_config_box.py b/test/test_config_box.py
+index 345ed55..55a73d3 100644
+--- a/test/test_config_box.py
++++ b/test/test_config_box.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python3
+ # -*- coding: utf-8 -*-
+
+ from test.common import test_dict
+diff --git a/test/test_converters.py b/test/test_converters.py
+index 014f1a4..262409f 100644
+--- a/test/test_converters.py
++++ b/test/test_converters.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python3
+ # -*- coding: utf-8 -*-
+ import json
+ import os
+diff --git a/test/test_from_file.py b/test/test_from_file.py
+index 7361e84..46aa84d 100644
+--- a/test/test_from_file.py
++++ b/test/test_from_file.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python3
+ # -*- coding: utf-8 -*-
+ from pathlib import Path
+ from test.common import test_root
+diff --git a/test/test_sbox.py b/test/test_sbox.py
+index ea0d06a..a7dd760 100644
+--- a/test/test_sbox.py
++++ b/test/test_sbox.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python3
+ # -*- coding: utf-8 -*-
+ import json
+ from test.common import test_dict
diff --git a/debian/tests/control b/debian/tests/control
index 4742caa..7ea639e 100644
--- a/debian/tests/control
+++ b/debian/tests/control
@@ -3,3 +3,4 @@ Depends:
python3-setuptools,
python3-box,
Test-Command: set -e; for py in $(py3versions -i); do echo "[*] testing on $py:"; $py -c "import box; print(box)"; done
+Restrictions: superficial
diff --git a/debian/watch b/debian/watch
index 5ec5dbe..4d38999 100644
--- a/debian/watch
+++ b/debian/watch
@@ -1,3 +1,3 @@
version=3
opts=uversionmangle=s/(rc|a|b|c)/~$1/ \
-https://pypi.debian.net/python-box/python-box-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
+https://pypi.debian.net/python-box/python[-_]box-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
diff --git a/python_box.egg-info/PKG-INFO b/python_box.egg-info/PKG-INFO
index 65fd77a..3f28655 100644
--- a/python_box.egg-info/PKG-INFO
+++ b/python_box.egg-info/PKG-INFO
@@ -1,474 +1,215 @@
-Metadata-Version: 2.1
-Name: python-box
-Version: 3.4.6
-Summary: Advanced Python dictionaries with dot notation access
-Home-page: https://github.com/cdgriffith/Box
-Author: Chris Griffith
-Author-email: chris@cdgriffith.com
-License: MIT
-Description: |BuildStatus| |CoverageStatus| |License| |PyPi| |DocStatus|
-
- |BoxImage|
-
- Python dictionaries with advanced dot notation access.
-
- .. code:: python
-
- from box import Box
-
- movie_data = {
- "movies": {
- "Spaceballs": {
- "imdb stars": 7.1,
- "rating": "PG",
- "length": 96,
- "director": "Mel Brooks",
- "stars": [{"name": "Mel Brooks", "imdb": "nm0000316", "role": "President Skroob"},
- {"name": "John Candy","imdb": "nm0001006", "role": "Barf"},
- {"name": "Rick Moranis", "imdb": "nm0001548", "role": "Dark Helmet"}
- ]
- },
- "Robin Hood: Men in Tights": {
- "imdb stars": 6.7,
- "rating": "PG-13",
- "length": 104,
- "director": "Mel Brooks",
- "stars": [
- {"name": "Cary Elwes", "imdb": "nm0000144", "role": "Robin Hood"},
- {"name": "Richard Lewis", "imdb": "nm0507659", "role": "Prince John"},
- {"name": "Roger Rees", "imdb": "nm0715953", "role": "Sheriff of Rottingham"},
- {"name": "Amy Yasbeck", "imdb": "nm0001865", "role": "Marian"}
- ]
- }
- }
- }
-
- # Box is a conversion_box by default, pass in `conversion_box=False` to disable that behavior
- movie_box = Box(movie_data)
-
-
- movie_box.movies.Robin_Hood_Men_in_Tights.imdb_stars
- # 6.7
-
- movie_box.movies.Spaceballs.stars[0].name
- # 'Mel Brooks'
-
- # All new dict and lists added to a Box or BoxList object are converted
- movie_box.movies.Spaceballs.stars.append({"name": "Bill Pullman", "imdb": "nm0000597", "role": "Lone Starr"})
- movie_box.movies.Spaceballs.stars[-1].role
- # 'Lone Starr'
-
- Install
- =======
-
- .. code:: bash
-
- pip install python-box
-
- Box is tested on python 2.7 and 3.4+.
- If it does not install with this command, please
- open a github issue with the error you are experiencing!
-
- If you want to be able to use the `to_yaml` functionality make sure to
- install `PyYAML` or `ruamel.yaml` as well.
-
- Overview
- ========
-
- `Box` is designed to be an easy drop in transparently replacements for
- dictionaries, thanks to Python's
- duck typing capabilities, which adds dot notation access. Any sub
- dictionaries or ones set after initiation will be automatically converted to
- a `Box` object. You can always run `.to_dict()` on it to return the object
- and all sub objects back into a regular dictionary.
-
-
- .. code:: python
-
- movie_box.movies.Spaceballs.to_dict()
- {'director': 'Mel Brooks',
- 'imdb stars': 7.1,
- 'length': 96,
- 'personal thoughts': 'On second thought, it was hilarious!',
- 'rating': 'PG',
- 'stars': [{'imdb': 'nm0000316', 'name': 'Mel Brooks', 'role': 'President Skroob'},
- {'imdb': 'nm0001006', 'name': 'John Candy', 'role': 'Barf'},
- {'imdb': 'nm0001548', 'name': 'Rick Moranis', 'role': 'Dark Helmet'},
- {'imdb': 'nm0000597', 'name': 'Bill Pullman', 'role': 'Lone Starr'}]}
-
- Box version 3 (and greater) now do sub box creation upon lookup, which means
- it is only referencing the original dict objects until they are looked up
- or modified.
-
- .. code:: python
-
- a = {"a": {"b": {"c": {}}}}
- a_box = Box(a)
- a_box
- # <Box: {'a': {'b': {'c': {}}}}>
-
- a["a"]["b"]["d"] = "2"
-
- a_box
- # <Box: {'a': {'b': {'c': {}, 'd': '2'}}}>
-
- So if you plan to keep the original dict around, make sure to box_it_up or do a deepcopy first.
-
- .. code:: python
-
- safe_box = Box(a, box_it_up=True)
- a["a"]["b"]["d"] = "2"
-
- safe_box
- # <Box: {'a': {'b': {'c': {}}}}>
-
- Limitations
- -----------
-
- `Box` is a subclass of `dict` and as such, certain keys cannot be accessed via dot notation.
- This is because names such as `keys` and `pop` have already been declared as methods, so `Box` cannot
- use it's special sauce to overwrite them. However it is still possible to have items with those names
- in the `Box` and access them like a normal dictionary, such as `my_box['keys']`.
-
- *This is as designed, and will not be changed.*
-
- The non-magic methods that exist in a `Box` are:
- `box_it_up, clear, copy, from_json, fromkeys, get, items, keys, pop, popitem, setdefault, to_dict, to_json, update, values`.
- To view an entire list of what cannot be accessed via dot notation, run the command `dir(Box())`.
-
-
- Box
- ---
-
- `Box` can be instantiated the same ways as `dict`.
-
- .. code:: python
-
- Box({'data': 2, 'count': 5})
- Box(data=2, count=5)
- Box({'data': 2, 'count': 1}, count=5)
- Box([('data', 2), ('count', 5)])
-
- # All will create
- # <Box: {'data': 2, 'count': 5}>
-
- `Box` is a subclass of `dict` which overrides some base functionality to make
- sure everything stored in the dict can be accessed as an attribute or key value.
-
- .. code:: python
-
- small_box = Box({'data': 2, 'count': 5})
- small_box.data == small_box['data'] == getattr(small_box, 'data')
-
- All dicts (and lists) added to a `Box` will be converted on lookup to a `Box` (or `BoxList`),
- allowing for recursive dot notation access.
-
- `Box` also includes helper functions to transform it back into a `dict`,
- as well as into `JSON` or `YAML` strings or files.
-
- Conversion Box
- ~~~~~~~~~~~~~~
-
- By default, Box is now a `conversion_box`
- that adds automagic attribute access for keys that could not normally be attributes.
- It can of course be disabled with the keyword argument `conversion_box=False`.
-
- .. code:: python
-
- movie_box.movies.Spaceballs["personal thoughts"] = "It was a good laugh"
- movie_box.movies.Spaceballs.personal_thoughts
- # 'It was a good laugh'
-
- movie_box.movies.Spaceballs.personal_thoughts = "On second thought, it was hilarious!"
- movie_box.movies.Spaceballs["personal thoughts"]
- # 'On second thought, it was hilarious!'
-
- # If a safe attribute matches a key exists, it will not create a new key
- movie_box.movies.Spaceballs["personal_thoughts"]
- # KeyError: 'personal_thoughts'
-
- Keys are modified in the following steps to make sure they are attribute safe:
-
- 1. Convert to string (Will encode as UTF-8 with errors ignored)
- 2. Replaces any spaces with underscores
- 3. Remove anything other than ascii letters, numbers or underscores
- 4. If the first character is an integer, it will prepend a lowercase 'x' to it
- 5. If the string is a built-in that cannot be used, it will prepend a lowercase 'x'
- 6. Removes any duplicate underscores
-
- This does not change the case of any of the keys.
-
- .. code:: python
-
- bx = Box({"321 Is a terrible Key!": "yes, really"})
- bx.x321_Is_a_terrible_Key
- # 'yes, really'
-
- These keys are not stored anywhere, and trying to modify them as an
- attribute will actually modify the underlying regular key's value.
-
- **Warning: duplicate attributes possible**
-
- If you have two keys that evaluate to the same attribute, such as "a!b" and "a?b" would become `.ab`,
- there is no way to discern between them,
- only reference or update them via standard dictionary modification.
-
-
- Frozen Box
- ~~~~~~~~~~
-
- Want to show off your box without worrying about others messing it up? Freeze it!
-
- .. code:: python
-
- frigid = Box(data={'Python': 'Rocks', 'inferior': ['java', 'cobol']}, frozen_box=True)
-
- frigid.data.Python = "Stinks"
- # box.BoxError: Box is frozen
-
- frigid.data.Python
- # 'Rocks'
-
- hash(frigid)
- # 4021666719083772260
-
- frigid.data.inferior
- # ('java', 'cobol')
-
-
- It's hashing ability is the same as the humble `tuple`, it will not be hashable
- if it has mutable objects. Speaking of `tuple`, that's what all the lists
- becomes now.
-
- Default Box
- ~~~~~~~~~~~
-
- It's boxes all the way down. At least, when you specify `default_box=True` it can be.
-
- .. code:: python
-
- empty_box = Box(default_box=True)
-
- empty_box.a.b.c.d.e.f.g
- # <Box: {}>
-
- empty_box.a.b.c.d.e.f.g = "h"
- empty_box
- # <Box: {'a': {'b': {'c': {'d': {'e': {'f': {'g': 'h'}}}}}}}>
-
- Unless you want it to be something else.
-
- .. code:: python
-
- evil_box = Box(default_box=True, default_box_attr="Something Something Something Dark Side")
-
- evil_box.not_defined
- # 'Something Something Something Dark Side'
-
- # Keep in mind it will no longer be possible to go down multiple levels
- evil_box.not_defined.something_else
- # AttributeError: 'str' object has no attribute 'something_else'
-
- `default_box_attr` will first check if it is callable, and will call the object
- if it is, otherwise it will see if has the `copy` attribute and will call that,
- lastly, will just use the provided item as is.
-
- Camel Killer Box
- ~~~~~~~~~~~~~~~~
-
- Similar to how conversion box works, allow CamelCaseKeys to be found as
- snake_case_attributes.
-
- .. code:: python
-
- cameled = Box(BadHabit="I just can't stop!", camel_killer_box=True)
-
- cameled.bad_habit
- # "I just can't stop!"
-
- Ordered Box
- ~~~~~~~~~~~
-
- Preserve the order that the keys were entered into the box. The preserved order
- will be observed while iterating over the box, or calling `.keys()`,
- `.values()` or `.items()`
-
- .. code:: python
-
- box_of_order = Box(ordered_box=True)
- box_of_order.c = 1
- box_of_order.a = 2
- box_of_order.d = 3
-
- box_of_order.keys() == ['c', 'a', 'd']
-
- Keep in mind this will not guarantee order of `**kwargs` passed to Box,
- as they are inherently not ordered until Python 3.6.
-
-
-
- BoxList
- -------
-
- To make sure all items added to lists in the box are also converted, all lists
- are covered into `BoxList`. It's possible to
- initiate these directly and use them just like a `Box`.
-
- .. code:: python
-
- from box import BoxList
-
- my_boxlist = BoxList({'item': x} for x in range(10))
- # <BoxList: [<Box: {'item': 0}>, <Box: {'item': 1}>, ...
-
- my_boxlist[5].item
- # 5
-
-
- **to_list**
-
- Transform a `BoxList` and all components back into regular `list` and `dict` items.
-
- .. code:: python
-
- my_boxlist.to_list()
- # [{'item': 0},
- # {'item': 1},
- # ...
-
- SBox
- ----
-
- Shorthand Box, aka SBox for short(hand), has the properties `json`, `yaml` and
- `dict` for faster access than the regular `to_dict()` and so on.
-
- .. code:: python
-
- from box import SBox
-
- sb = SBox(test=True)
- sb.json
- # '{"test": true}'
-
- Note that in this case, `json` has no default indent, unlike `to_json`.
-
- ConfigBox
- ---------
-
- A Box with additional handling of string manipulation generally found in
- config files.
-
- test_config.ini
-
- .. code:: ini
-
- [General]
- example=A regular string
-
- [Examples]
- my_bool=yes
- anint=234
- exampleList=234,123,234,543
- floatly=4.4
-
-
- With the combination of `reusables` and `ConfigBox` you can easily read python
- config values into python types. It supports `list`, `bool`, `int` and `float`.
-
- .. code:: python
-
- import reusables
- from box import ConfigBox
-
- config = ConfigBox(reusables.config_dict("test_config.ini"))
- # <ConfigBox: {'General': {'example': 'A regular string'},
- # 'Examples': {'my_bool': 'yes', 'anint': '234', 'examplelist': '234,123,234,543', 'floatly': '4.4'}}>
-
- config.Examples.list('examplelist')
- # ['234', '123', '234', '543']
-
- config.Examples.float('floatly')
- # 4.4
-
- BoxObject
- ---------
-
- An object wrapper with a **Box** for a **__dict__**.
-
- .. code:: python
-
- import requests
- from box import BoxObject
-
- def get_html(session, url, *args, **kwargs):
- response = session.get(url, *args, **kwargs)
- text = response.text
- response_meta = response.__dict__
- for key in tuple(filter(lambda k: k.startswith('_'), response_meta)):
- response_meta.pop(key)
- return BoxObject(text, response_meta, frozen_box=True)
-
- box_url = 'https://raw.githubusercontent.com/cdgriffith/Box/master/box.py'
- with requests.Session() as session:
- box_source = get_html(session, box_url)
-
- box_source.url
- # https://raw.githubusercontent.com/cdgriffith/Box/master/box.py
-
- box_source.status_code
- # 200
-
- box_source.raw.reason
- # OK
-
- **BoxObject** act just like objects but they secretly carry around a **Box** with
- them to store attributes. **BoxObject** are built off of **wrapt.ObjectProxy** which
- can wrap almost any python object. They protect their wrapped objects storing them in
- the **__wrapped__** attribute and keeping the original **__dict__** in
- **__wrapped__.__dict__**.
-
- See the `Wrapt Documentation`_, specifically
- the section on **ObjectProxy**, for more information.
-
-
- License
- =======
-
- MIT License, Copyright (c) 2017-2018 Chris Griffith. See LICENSE file.
-
-
- .. |BoxImage| image:: https://raw.githubusercontent.com/cdgriffith/Box/master/box_logo.png
- :target: https://github.com/cdgriffith/Box
- .. |BuildStatus| image:: https://travis-ci.org/cdgriffith/Box.png?branch=master
- :target: https://travis-ci.org/cdgriffith/Box
- .. |CoverageStatus| image:: https://img.shields.io/coveralls/cdgriffith/Box/master.svg?maxAge=2592000
- :target: https://coveralls.io/r/cdgriffith/Box?branch=master
- .. |DocStatus| image:: https://readthedocs.org/projects/box/badge/?version=latest
- :target: http://box.readthedocs.org/en/latest/index.html
- .. |PyPi| image:: https://img.shields.io/pypi/v/python-box.svg?maxAge=2592000
- :target: https://pypi.python.org/pypi/python-box/
- .. |License| image:: https://img.shields.io/pypi/l/python-box.svg
- :target: https://pypi.python.org/pypi/python-box/
- .. _`Wrapt Documentation`: https://wrapt.readthedocs.io/en/latest
-
-Platform: any
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.3
-Classifier: Programming Language :: Python :: 3.4
-Classifier: Programming Language :: Python :: 3.5
-Classifier: Programming Language :: Python :: 3.6
-Classifier: Programming Language :: Python :: 3.7
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Programming Language :: Python :: Implementation :: PyPy
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Natural Language :: English
-Classifier: Intended Audience :: Developers
-Classifier: License :: OSI Approved :: MIT License
-Classifier: Operating System :: OS Independent
-Classifier: Topic :: Utilities
-Classifier: Topic :: Software Development
-Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Provides-Extra: testing
+Metadata-Version: 2.2
+Name: python-box
+Version: 7.3.2
+Summary: Advanced Python dictionaries with dot notation access
+Home-page: https://github.com/cdgriffith/Box
+Author: Chris Griffith
+Author-email: chris@cdgriffith.com
+License: MIT
+Platform: any
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Natural Language :: English
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Utilities
+Classifier: Topic :: Software Development
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Requires-Python: >=3.9
+Description-Content-Type: text/x-rst
+License-File: LICENSE
+License-File: AUTHORS.rst
+Provides-Extra: all
+Requires-Dist: ruamel.yaml>=0.17; extra == "all"
+Requires-Dist: toml; extra == "all"
+Requires-Dist: msgpack; extra == "all"
+Provides-Extra: yaml
+Requires-Dist: ruamel.yaml>=0.17; extra == "yaml"
+Provides-Extra: ruamel-yaml
+Requires-Dist: ruamel.yaml>=0.17; extra == "ruamel-yaml"
+Provides-Extra: pyyaml
+Requires-Dist: PyYAML; extra == "pyyaml"
+Provides-Extra: tomli
+Requires-Dist: tomli; python_version < "3.11" and extra == "tomli"
+Requires-Dist: tomli-w; extra == "tomli"
+Provides-Extra: toml
+Requires-Dist: toml; extra == "toml"
+Provides-Extra: msgpack
+Requires-Dist: msgpack; extra == "msgpack"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: description
+Dynamic: description-content-type
+Dynamic: home-page
+Dynamic: license
+Dynamic: platform
+Dynamic: provides-extra
+Dynamic: requires-python
+Dynamic: summary
+
+|BuildStatus| |License|
+
+|BoxImage|
+
+.. code:: python
+
+ from box import Box
+
+ movie_box = Box({ "Robin Hood: Men in Tights": { "imdb stars": 6.7, "length": 104 } })
+
+ movie_box.Robin_Hood_Men_in_Tights.imdb_stars
+ # 6.7
+
+
+Box will automatically make otherwise inaccessible keys safe to access as an attribute.
+You can always pass `conversion_box=False` to `Box` to disable that behavior.
+Also, all new dict and lists added to a Box or BoxList object are converted automatically.
+
+There are over a half dozen ways to customize your Box and make it work for you.
+
+Check out the new `Box github wiki <https://github.com/cdgriffith/Box/wiki>`_ for more details and examples!
+
+Install
+=======
+
+**Version Pin Your Box!**
+
+If you aren't in the habit of version pinning your libraries, it will eventually bite you.
+Box has a `list of breaking change <https://github.com/cdgriffith/Box/wiki/Major-Version-Breaking-Changes>`_ between major versions you should always check out before updating.
+
+requirements.txt
+----------------
+
+.. code:: text
+
+ python-box[all]~=7.0
+
+As Box adheres to semantic versioning (aka API changes will only occur on between major version),
+it is best to use `Compatible release <https://www.python.org/dev/peps/pep-0440/#compatible-release>`_ matching using the `~=` clause.
+
+Install from command line
+-------------------------
+
+.. code:: bash
+
+ python -m pip install --upgrade pip
+ pip install python-box[all]~=7.0 --upgrade
+
+Install with selected dependencies
+----------------------------------
+
+Box does not install external dependencies such as yaml and toml writers. Instead you can specify which you want,
+for example, `[all]` is shorthand for:
+
+.. code:: bash
+
+ pip install python-box[ruamel.yaml,tomli_w,msgpack]~=7.0 --upgrade
+
+But you can also sub out `ruamel.yaml` for `PyYAML`.
+
+Check out `more details <https://github.com/cdgriffith/Box/wiki/Installation>`_ on installation details.
+
+Box 7 is tested on python 3.7+, if you are upgrading from previous versions, please look through
+`any breaking changes and new features <https://github.com/cdgriffith/Box/wiki/Major-Version-Breaking-Changes>`_.
+
+Optimized Version
+-----------------
+
+Box has introduced Cython optimizations for major platforms by default.
+Loading large data sets can be up to 10x faster!
+
+If you are **not** on a x86_64 supported system you will need to do some extra work to install the optimized version.
+There will be an warning of "WARNING: Cython not installed, could not optimize box" during install.
+You will need python development files, system compiler, and the python packages `Cython` and `wheel`.
+
+**Linux Example:**
+
+First make sure you have python development files installed (`python3-dev` or `python3-devel` in most repos).
+You will then need `Cython` and `wheel` installed and then install (or re-install with `--force`) `python-box`.
+
+.. code:: bash
+
+ pip install Cython wheel
+ pip install python-box[all]~=7.0 --upgrade --force
+
+If you have any issues please open a github issue with the error you are experiencing!
+
+Overview
+========
+
+`Box` is designed to be a near transparent drop in replacements for
+dictionaries that add dot notation access and other powerful feature.
+
+There are a lot of `types of boxes <https://github.com/cdgriffith/Box/wiki/Types-of-Boxes>`_
+to customize it for your needs, as well as handy `converters <https://github.com/cdgriffith/Box/wiki/Converters>`_!
+
+Keep in mind any sub dictionaries or ones set after initiation will be automatically converted to
+a `Box` object, and lists will be converted to `BoxList`, all other objects stay intact.
+
+Check out the `Quick Start <https://github.com/cdgriffith/Box/wiki/Quick-Start>`_ for more in depth details.
+
+`Box` can be instantiated the same ways as `dict`.
+
+.. code:: python
+
+ Box({'data': 2, 'count': 5})
+ Box(data=2, count=5)
+ Box({'data': 2, 'count': 1}, count=5)
+ Box([('data', 2), ('count', 5)])
+
+ # All will create
+ # <Box: {'data': 2, 'count': 5}>
+
+`Box` is a subclass of `dict` which overrides some base functionality to make
+sure everything stored in the dict can be accessed as an attribute or key value.
+
+.. code:: python
+
+ small_box = Box({'data': 2, 'count': 5})
+ small_box.data == small_box['data'] == getattr(small_box, 'data')
+
+All dicts (and lists) added to a `Box` will be converted on insertion to a `Box` (or `BoxList`),
+allowing for recursive dot notation access.
+
+`Box` also includes helper functions to transform it back into a `dict`,
+as well as into `JSON`, `YAML`, `TOML`, or `msgpack` strings or files.
+
+
+Thanks
+======
+
+A huge thank you to everyone that has given features and feedback over the years to Box! Check out everyone that has contributed_.
+
+A big thanks to Python Software Foundation, and PSF-Trademarks Committee, for official approval to use the Python logo on the `Box` logo!
+
+Also special shout-out to PythonBytes_, who featured Box on their podcast.
+
+
+License
+=======
+
+MIT License, Copyright (c) 2017-2023 Chris Griffith. See LICENSE_ file.
+
+
+.. |BoxImage| image:: https://raw.githubusercontent.com/cdgriffith/Box/master/box_logo.png
+ :target: https://github.com/cdgriffith/Box
+.. |BuildStatus| image:: https://github.com/cdgriffith/Box/workflows/Tests/badge.svg?branch=master
+ :target: https://github.com/cdgriffith/Box/actions?query=workflow%3ATests
+.. |License| image:: https://img.shields.io/pypi/l/python-box.svg
+ :target: https://pypi.python.org/pypi/python-box/
+
+.. _PythonBytes: https://pythonbytes.fm/episodes/show/19/put-your-python-dictionaries-in-a-box-and-apparently-python-is-really-wanted
+.. _contributed: AUTHORS.rst
+.. _`Wrapt Documentation`: https://wrapt.readthedocs.io/en/latest
+.. _reusables: https://github.com/cdgriffith/reusables#reusables
+.. _created: https://github.com/cdgriffith/Reusables/commit/df20de4db74371c2fedf1578096f3e29c93ccdf3#diff-e9a0f470ef3e8afb4384dc2824943048R51
+.. _LICENSE: https://github.com/cdgriffith/Box/blob/master/LICENSE
diff --git a/python_box.egg-info/SOURCES.txt b/python_box.egg-info/SOURCES.txt
index 1210405..c383800 100644
--- a/python_box.egg-info/SOURCES.txt
+++ b/python_box.egg-info/SOURCES.txt
@@ -3,12 +3,31 @@ CHANGES.rst
LICENSE
MANIFEST.in
README.rst
-box.py
setup.py
+box/__init__.py
+box/box.py
+box/box.pyi
+box/box_list.py
+box/box_list.pyi
+box/config_box.py
+box/config_box.pyi
+box/converters.py
+box/converters.pyi
+box/exceptions.py
+box/exceptions.pyi
+box/from_file.py
+box/from_file.pyi
+box/py.typed
+box/shorthand_box.py
+box/shorthand_box.pyi
python_box.egg-info/PKG-INFO
python_box.egg-info/SOURCES.txt
python_box.egg-info/dependency_links.txt
python_box.egg-info/requires.txt
python_box.egg-info/top_level.txt
-test/test_functional_box.py
-test/test_unittests_box.py \ No newline at end of file
+test/test_box.py
+test/test_box_list.py
+test/test_config_box.py
+test/test_converters.py
+test/test_from_file.py
+test/test_sbox.py \ No newline at end of file
diff --git a/python_box.egg-info/requires.txt b/python_box.egg-info/requires.txt
index e051eff..40f07da 100644
--- a/python_box.egg-info/requires.txt
+++ b/python_box.egg-info/requires.txt
@@ -1,5 +1,26 @@
-[testing]
-pytest
-coverage>=3.6
-pytest-cov
+[PyYAML]
+PyYAML
+
+[all]
+ruamel.yaml>=0.17
+toml
+msgpack
+
+[msgpack]
+msgpack
+
+[ruamel.yaml]
+ruamel.yaml>=0.17
+
+[toml]
+toml
+
+[tomli]
+tomli-w
+
+[tomli:python_version < "3.11"]
+tomli
+
+[yaml]
+ruamel.yaml>=0.17
diff --git a/setup.cfg b/setup.cfg
index 4927abe..8bfd5a1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,4 +1,4 @@
-[egg_info]
-tag_build =
-tag_date = 0
-
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/setup.py b/setup.py
index 5eaef7b..8abda61 100644
--- a/setup.py
+++ b/setup.py
@@ -1,61 +1,81 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
-from setuptools import setup
+# Must import multiprocessing as a fix for issues with testing, experienced on win10
+import multiprocessing # noqa: F401
import os
import re
+from pathlib import Path
+import sys
+import shutil
-# Fix for issues with testing, experienced on win10
-import multiprocessing
+from setuptools import setup
root = os.path.abspath(os.path.dirname(__file__))
-with open(os.path.join(root, "box.py"), "r") as reuse_file:
- reuse_content = reuse_file.read()
+try:
+ from Cython.Build import cythonize
+except ImportError:
+ extra = None
+else:
+ extra = cythonize(
+ [str(file.relative_to(root)) for file in Path(root, "box").glob("*.py") if file.name != "__init__.py"],
+ compiler_directives={"language_level": 3},
+ )
-attrs = dict(re.findall(r"__([a-z]+)__ *= *['\"](.+)['\"]", reuse_content))
+with open(os.path.join(root, "box", "__init__.py"), "r") as init_file:
+ init_content = init_file.read()
+
+attrs = dict(re.findall(r"__([a-z]+)__ *= *['\"](.+)['\"]", init_content))
with open("README.rst", "r") as readme_file:
long_description = readme_file.read()
setup(
- name='python-box',
- version=attrs['version'],
- url='https://github.com/cdgriffith/Box',
- license='MIT',
- author=attrs['author'],
- tests_require=["pytest", "coverage >= 3.6", "pytest-cov"],
+ name="python-box",
+ version=attrs["version"],
+ url="https://github.com/cdgriffith/Box",
+ license="MIT",
+ author=attrs["author"],
install_requires=[],
- author_email='chris@cdgriffith.com',
- description='Advanced Python dictionaries with dot notation access',
+ author_email="chris@cdgriffith.com",
+ description="Advanced Python dictionaries with dot notation access",
long_description=long_description,
- scripts=['box.py'],
- py_modules=['box'],
+ long_description_content_type="text/x-rst",
+ py_modules=["box"],
+ packages=["box"],
+ ext_modules=extra,
+ python_requires=">=3.9",
include_package_data=True,
- platforms='any',
- setup_requires=['pytest-runner'],
+ platforms="any",
classifiers=[
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.3',
- 'Programming Language :: Python :: 3.4',
- 'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: 3.6',
- 'Programming Language :: Python :: 3.7',
- 'Programming Language :: Python :: Implementation :: CPython',
- 'Programming Language :: Python :: Implementation :: PyPy',
- 'Development Status :: 5 - Production/Stable',
- 'Natural Language :: English',
- 'Intended Audience :: Developers',
- 'License :: OSI Approved :: MIT License',
- 'Operating System :: OS Independent',
- 'Topic :: Utilities',
- 'Topic :: Software Development',
- 'Topic :: Software Development :: Libraries :: Python Modules'
- ],
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Development Status :: 5 - Production/Stable",
+ "Natural Language :: English",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Topic :: Utilities",
+ "Topic :: Software Development",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ],
extras_require={
- 'testing': ["pytest", "coverage >= 3.6", "pytest-cov"],
- },
+ "all": ["ruamel.yaml>=0.17", "toml", "msgpack"],
+ "yaml": ["ruamel.yaml>=0.17"],
+ "ruamel.yaml": ["ruamel.yaml>=0.17"],
+ "PyYAML": ["PyYAML"],
+ "tomli": ["tomli; python_version < '3.11'", "tomli-w"],
+ "toml": ["toml"],
+ "msgpack": ["msgpack"],
+ },
)
+
+if not extra:
+ print("WARNING: Cython not installed, could not optimize box.", file=sys.stderr)
diff --git a/test/test_box.py b/test/test_box.py
new file mode 100644
index 0000000..1232f3c
--- /dev/null
+++ b/test/test_box.py
@@ -0,0 +1,1448 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Test files gathered from json.org and yaml.org
+import copy
+import json
+import os
+import pickle
+import platform
+import shutil
+from multiprocessing import Queue
+from pathlib import Path
+from io import StringIO
+from test.common import (
+ data_json_file,
+ data_yaml_file,
+ extended_test_dict,
+ movie_data,
+ test_dict,
+ test_root,
+ tmp_dir,
+ tmp_json_file,
+ tmp_msgpack_file,
+ tmp_yaml_file,
+)
+
+import pytest
+from ruamel.yaml import YAML
+
+from box import Box, BoxError, BoxKeyError, BoxList, ConfigBox, SBox, DDBox
+from box.box import _get_dot_paths, _camel_killer, _recursive_tuples # type: ignore
+from box.converters import BOX_PARAMETERS
+
+
+def mp_queue_test(q):
+ bx = q.get()
+ try:
+ assert isinstance(bx, Box)
+ assert bx.a == 4
+ except AssertionError:
+ q.put(False)
+ else:
+ q.put(True)
+
+
+class TestBox:
+ @pytest.fixture(autouse=True)
+ def temp_dir_cleanup(self):
+ shutil.rmtree(str(tmp_dir), ignore_errors=True)
+ try:
+ os.mkdir(str(tmp_dir))
+ except OSError:
+ pass
+ yield
+ shutil.rmtree(str(tmp_dir), ignore_errors=True)
+
+ def test_safe_attrs(self):
+ assert Box()._safe_attr("BAD!KEY!1") == "BAD_KEY_1"
+ assert Box(camel_killer_box=True)._safe_attr("BAD!KEY!2") == "bad_key_2"
+ assert Box()._safe_attr((5, 6, 7)) == "x5_6_7"
+ assert Box()._safe_attr(356) == "x356"
+
+ def test_camel_killer(self):
+ assert _camel_killer("CamelCase") == "camel_case"
+ assert _camel_killer("Terrible321KeyA") == "terrible321_key_a"
+ bx = Box(camel_killer_box=True, conversion_box=False)
+
+ bx.DeadCamel = 3
+ assert bx["dead_camel"] == 3
+ assert bx.dead_camel == 3
+
+ bx["BigCamel"] = 4
+ assert bx["big_camel"] == 4
+ assert bx.big_camel == 4
+ assert bx.BigCamel == 4
+
+ bx1 = Box(camel_killer_box=True, conversion_box=True)
+ bx1["BigCamel"] = 4
+ bx1.DeadCamel = 3
+ assert bx1["big_camel"] == 4
+ assert bx1["dead_camel"] == 3
+ assert bx1.big_camel == 4
+ assert bx1.dead_camel == 3
+ assert bx1.BigCamel == 4
+ assert bx1["BigCamel"] == 4
+
+ del bx1.DeadCamel
+ assert "dead_camel" not in bx1
+ del bx1["big_camel"]
+ assert "big_camel" not in bx1
+ assert len(bx1.keys()) == 0
+
+ def test_recursive_tuples(self):
+ out = _recursive_tuples(
+ ({"test": "a"}, ({"second": "b"}, {"third": "c"}, ("fourth",))), dict, recreate_tuples=True
+ )
+ assert isinstance(out, tuple)
+ assert isinstance(out[0], dict)
+ assert out[0] == {"test": "a"}
+ assert isinstance(out[1], tuple)
+ assert isinstance(out[1][2], tuple)
+ assert out[1][0] == {"second": "b"}
+
+ def test_box(self):
+ bx = Box(**test_dict)
+ assert bx.key1 == test_dict["key1"]
+ assert dict(getattr(bx, "Key 2")) == test_dict["Key 2"]
+ setattr(bx, "TEST_KEY", "VALUE")
+ assert bx.TEST_KEY == "VALUE"
+ delattr(bx, "TEST_KEY")
+ assert "TEST_KEY" not in bx.to_dict(), bx.to_dict()
+ assert isinstance(bx["Key 2"].Key4, Box)
+ assert "'key1': 'value1'" in str(bx)
+ assert repr(bx).startswith("Box(")
+ bx2 = Box([((3, 4), "A"), ("_box_config", "test")])
+ assert bx2[(3, 4)] == "A"
+ assert bx2["_box_config"] == "test"
+ bx3 = Box(a=4, conversion_box=False)
+ setattr(bx3, "key", 2)
+ assert bx3.key == 2
+ bx3.__setattr__("Test", 3)
+ assert bx3.Test == 3
+
+ def test_box_modify_at_depth(self):
+ bx = Box(**test_dict)
+ assert "key1" in bx
+ assert "key2" not in bx
+ bx["Key 2"].new_thing = "test"
+ assert bx["Key 2"].new_thing == "test"
+ bx["Key 2"].new_thing += "2"
+ assert bx["Key 2"].new_thing == "test2"
+ assert bx["Key 2"].to_dict()["new_thing"] == "test2"
+ assert bx.to_dict()["Key 2"]["new_thing"] == "test2"
+ bx.__setattr__("key1", 1)
+ assert bx["key1"] == 1
+ bx.__delattr__("key1")
+ assert "key1" not in bx
+
+ def test_error_box(self):
+ bx = Box(**test_dict)
+ with pytest.raises(AttributeError):
+ getattr(bx, "hello")
+
+ def test_box_from_dict(self):
+ ns = Box({"k1": "v1", "k2": {"k3": "v2"}})
+ assert ns.k2.k3 == "v2"
+
+ def test_box_from_bad_dict(self):
+ with pytest.raises(ValueError):
+ Box('{"k1": "v1", "k2": {"k3": "v2"}}')
+
+ def test_basic_box(self):
+ a = Box(one=1, two=2, three=3)
+ b = Box({"one": 1, "two": 2, "three": 3})
+ c = Box((zip(["one", "two", "three"], [1, 2, 3])))
+ d = Box(([("two", 2), ("one", 1), ("three", 3)]))
+ e = Box(({"three": 3, "one": 1, "two": 2}))
+ assert a == b == c == d == e
+
+ def test_protected_box_methods(self):
+ my_box = Box(a=3)
+ with pytest.raises(AttributeError):
+ my_box.to_dict = "test"
+
+ with pytest.raises(AttributeError):
+ del my_box.to_json
+
+ def test_bad_args(self):
+ with pytest.raises(TypeError):
+ Box("123", "432")
+
+ def test_box_inits(self):
+ a = Box({"data": 2, "count": 5})
+ b = Box(data=2, count=5)
+ c = Box({"data": 2, "count": 1}, count=5)
+ d = Box([("data", 2), ("count", 5)])
+ e = Box({"a": [{"item": 3}, {"item": []}]})
+ assert e.a[1].item == []
+ assert a == b == c == d
+
+ def test_bad_inits(self):
+ with pytest.raises(ValueError):
+ Box("testing")
+
+ with pytest.raises(ValueError):
+ Box(22)
+
+ with pytest.raises(TypeError):
+ Box(22, 33)
+
+ def test_create_subdicts(self):
+ a = Box({"data": 2, "count": 5})
+ a.brand_new = {"subdata": 1}
+ assert a.brand_new.subdata == 1
+ a.new_list = [{"sub_list_item": 1}]
+ assert a.new_list[0].sub_list_item == 1
+ assert isinstance(a.new_list, BoxList)
+ a.new_list2 = [[{"sub_list_item": 2}]]
+ assert a.new_list2[0][0].sub_list_item == 2
+ b = a.to_dict()
+ assert not isinstance(b["new_list"], BoxList)
+
+ def test_to_json_basic(self):
+ a = Box(test_dict)
+ assert json.loads(a.to_json(indent=0)) == test_dict
+
+ a.to_json(tmp_json_file)
+ with open(tmp_json_file) as f:
+ data = json.load(f)
+ assert data == test_dict
+
+ def test_to_yaml_basic(self):
+ a = Box(test_dict)
+ yaml = YAML(typ="safe")
+ assert yaml.load(a.to_yaml()) == test_dict
+
+ def test_to_yaml_file(self):
+ a = Box(test_dict)
+ a.to_yaml(tmp_yaml_file)
+ with open(tmp_yaml_file) as f:
+ yaml = YAML(typ="safe")
+ data = yaml.load(f)
+ assert data == test_dict
+
+ def test_dir(self):
+ a = Box(test_dict, camel_killer_box=True)
+ assert "key1" in dir(a)
+ assert "not$allowed" not in dir(a)
+ assert "key4" in a["key 2"]
+ for item in ("to_yaml", "to_dict", "to_json"):
+ assert item in dir(a)
+
+ assert a.big_camel == "hi"
+ assert "big_camel" in dir(a)
+
+ def test_update(self):
+ a = Box(test_dict)
+ a.grand = 1000
+ a.update({"key1": {"new": 5}, "Key 2": {"add_key": 6}, "lister": ["a"]})
+ a.update([("asdf", "fdsa")])
+ a.update(testkey=66)
+ a.update({"items": {"test": "pme"}})
+ a.update({"key1": {"gg": 4}})
+ b = Box()
+ b.update(item=1)
+ b.update(E=1)
+ b.update(__m=1)
+ with pytest.raises(ValueError):
+ b.update("test")
+
+ assert a.grand == 1000
+ assert a["grand"] == 1000
+ assert isinstance(a["items"], Box)
+ assert a["items"].test == "pme"
+ assert a["Key 2"].add_key == 6
+ assert isinstance(a.key1, Box)
+ assert isinstance(a.lister, BoxList)
+ assert a.asdf == "fdsa"
+ assert a.testkey == 66
+ assert a.key1.gg == 4
+ assert "new" not in a.key1.keys()
+
+ def test_merge_update(self):
+ a = Box(test_dict)
+ a.grand = 1000
+ a.merge_update({"key1": {"new": 5}, "Key 2": {"add_key": 6}, "lister": ["a"]})
+ a.merge_update([("asdf", "fdsa")])
+ a.merge_update(testkey=66)
+ a.merge_update({"items": {"test": "pme"}})
+ a.merge_update({"key1": {"gg": 4}})
+ b = Box()
+ b.merge_update(item=1)
+ b.merge_update(E=4)
+ b.merge_update(__m=1)
+
+ assert a.grand == 1000
+ assert a["grand"] == 1000
+ assert isinstance(a["items"], Box)
+ assert a["items"].test == "pme"
+ assert a.key1.new == 5
+ assert a["Key 2"].add_key == 6
+ assert isinstance(a.key1, Box)
+ assert isinstance(a.lister, BoxList)
+ assert a.asdf == "fdsa"
+ assert a.testkey == 66
+ assert a.key1.new == 5
+ assert a.key1.gg == 4
+ with pytest.raises(ValueError):
+ b.merge_update("test")
+
+ def test_auto_attr(self):
+ a = Box(test_dict, default_box=True)
+ assert isinstance(a.a.a.a.a, Box)
+ a.b.b = 4
+ assert a.b.b == 4
+
+ def test_set_default_dict(self):
+ a = Box(test_dict)
+ new = a.setdefault("key3", {})
+ new.yy = 8
+ assert a.key3.yy == 8
+
+ def test_set_default(self):
+ a = Box(test_dict)
+
+ new = a.setdefault("key3", {"item": 2})
+ new_list = a.setdefault("lister", [{"gah": 7}])
+ assert a.setdefault("key1", False) == "value1"
+
+ assert new == Box(item=2)
+ assert new_list == BoxList([{"gah": 7}])
+ assert a.key3.item == 2
+ assert a.lister[0].gah == 7
+
+ def test_set_default_box_dots(self):
+ a = Box(box_dots=True)
+ a["x"] = {"y": 10}
+ a.setdefault("x.y", 20)
+ assert a["x.y"] == 10
+
+ a["lists"] = [[[{"test": "here"}], {1, 2}], (4, 5)]
+ assert list(_get_dot_paths(a)) == [
+ "x",
+ "x.y",
+ "lists",
+ "lists[0]",
+ "lists[0][0]",
+ "lists[0][0][0]",
+ "lists[0][0][0].test",
+ "lists[0][1]",
+ "lists[1]",
+ ]
+
+ t = Box({"a": 1}, default_box=True, box_dots=True, default_box_none_transform=False)
+ assert t.setdefault("b", [1, 2]) == [1, 2]
+ assert t == Box(a=1, b=[1, 2])
+ assert t.setdefault("c", [{"d": 2}]) == BoxList([{"d": 2}])
+
+ def test_from_json_file(self):
+ bx = Box.from_json(filename=data_json_file)
+ assert isinstance(bx, Box)
+ assert bx.widget.window.height == 500
+
+ def test_from_yaml_file(self):
+ bx = Box.from_yaml(filename=data_yaml_file)
+ assert isinstance(bx, Box)
+ assert bx.total == 4443.52
+
+ def test_from_json(self):
+ bx = Box.from_json(json.dumps(test_dict))
+ assert isinstance(bx, Box)
+ assert bx.key1 == "value1"
+
+ def test_from_yaml(self):
+ yaml = YAML(typ="safe")
+ with StringIO() as sio:
+ yaml.dump(test_dict, sio)
+ data = sio.getvalue()
+ bx = Box.from_yaml(data, conversion_box=False, default_box=True)
+ assert isinstance(bx, Box)
+ assert bx.key1 == "value1"
+ assert bx.Key_2 == Box()
+
+ def test_bad_from_json(self):
+ with pytest.raises(BoxError):
+ Box.from_json()
+
+ with pytest.raises(BoxError):
+ Box.from_json(json_string="[1]")
+
+ def test_bad_from_yaml(self):
+ with pytest.raises(BoxError):
+ Box.from_yaml()
+
+ with pytest.raises(BoxError):
+ Box.from_yaml("lol")
+
+ def test_conversion_box(self):
+ bx = Box(extended_test_dict, conversion_box=True)
+ assert bx.Key_2.Key_3 == "Value 3"
+ assert bx.x3 == "howdy"
+ assert bx.xnot == "true"
+ assert bx.x3_4 == "test"
+ with pytest.raises(AttributeError):
+ getattr(bx, "(3, 4)")
+
+ def test_frozen(self):
+ bx = Box(extended_test_dict, frozen_box=True)
+
+ assert isinstance(bx.alist, tuple)
+ assert bx.alist[0] == {"a": 1}
+ with pytest.raises(BoxError):
+ bx.new = 3
+
+ with pytest.raises(BoxError):
+ bx["new"] = 3
+
+ with pytest.raises(BoxError):
+ del bx["not"]
+
+ with pytest.raises(BoxError):
+ delattr(bx, "key1")
+
+ with pytest.raises(TypeError):
+ hash(bx)
+
+ with pytest.raises(BoxError):
+ bx.clear()
+
+ with pytest.raises(BoxError):
+ bx.pop("alist")
+
+ with pytest.raises(BoxError):
+ bx.popitem()
+
+ with pytest.raises(BoxError):
+ bx.popitem()
+
+ with pytest.raises(BoxError):
+ bx.update({"another_list": []})
+
+ bx2 = Box(test_dict)
+ with pytest.raises(TypeError):
+ hash(bx2)
+
+ bx3 = Box(test_dict, frozen_box=True)
+
+ assert hash(bx3)
+
+ def test_hashing(self):
+ bx1 = Box(t=3, g=4, frozen_box=True)
+ bx2 = Box(g=4, t=3, frozen_box=True)
+ assert hash(bx1) == hash(bx2)
+
+ bl1 = BoxList([1, 2, 3, 4], frozen_box=True)
+ bl2 = BoxList([1, 2, 3, 4], frozen_box=True)
+ bl3 = BoxList([2, 1, 3, 4], frozen_box=True)
+ assert hash(bl2) == hash(bl1)
+ assert hash(bl3) != hash(bl2)
+
+ with pytest.raises(TypeError):
+ hash(BoxList([1, 2, 3]))
+
+ def test_config(self):
+ bx = Box(extended_test_dict)
+ assert bx["_box_config"] is True
+ assert isinstance(bx._box_config, dict)
+ with pytest.raises(BoxError):
+ delattr(bx, "_box_config")
+ bx._box_config
+
+ def test_default_box(self):
+ bx = Box(test_dict, default_box=True, default_box_attr={"hi": "there"})
+ assert bx.key_88 == {"hi": "there"}
+ assert bx["test"] == {"hi": "there"}
+
+ bx2 = Box(test_dict, default_box=True, default_box_attr=Box)
+ assert isinstance(bx2.key_77, Box)
+
+ bx3 = Box(default_box=True, default_box_attr=3)
+ assert bx3.hello == 3
+
+ bx4 = Box(default_box=True, default_box_attr=None)
+ assert bx4.who_is_there is None
+
+ bx5 = Box(default_box=True, default_box_attr=[])
+ assert isinstance(bx5.empty_list_please, list)
+ assert len(bx5.empty_list_please) == 0
+ bx5.empty_list_please.append(1)
+ assert bx5.empty_list_please[0] == 1
+
+ bx6 = Box(default_box=True, default_box_attr=[])
+ my_list = bx6.get("new_list")
+ my_list.append(5)
+ assert bx6.get("new_list")[0] == 5
+
+ bx7 = Box(default_box=True, default_box_attr=False)
+ assert bx7.nothing is False
+
+ bx8 = Box(default_box=True, default_box_attr=0)
+ assert bx8.nothing == 0
+
+ # Tests __get_default's `copy` clause
+ s = {1, 2, 3}
+ bx9 = Box(default_box=True, default_box_attr=s)
+ assert isinstance(bx9.test, set)
+ assert bx9.test == s
+ assert id(bx9.test) != id(s)
+
+ bx10 = Box({"from": "here"}, default_box=True)
+ assert bx10.xfrom == "here"
+ bx10.xfrom = 5
+ assert bx10.xfrom == 5
+ assert bx10 == {"from": 5}
+
+ # Issue#59 https://github.com/cdgriffith/Box/issues/59 "Treat None values as non existing keys for default_box"
+ def test_default_box_none_transforms(self):
+ bx4 = Box({"noneValue": None, "inner": {"noneInner": None}}, default_box=True, default_box_attr="issue#59")
+ assert bx4.noneValue == "issue#59"
+ assert bx4.inner.noneInner == "issue#59"
+
+ bx5 = Box(
+ {"noneValue": None, "inner": {"noneInner": None}},
+ default_box=True,
+ default_box_none_transform=False,
+ default_box_attr="attr",
+ )
+ assert bx5.noneValue is None
+ assert bx5.absentKey == "attr"
+ assert bx5.inner.noneInner is None
+
+ def test_camel_killer_box(self):
+ td = extended_test_dict.copy()
+ td["CamelCase"] = "Item"
+ td["321CamelCaseFever!"] = "Safe"
+
+ kill_box = Box(td, camel_killer_box=True, conversion_box=False)
+ assert kill_box.camel_case == "Item"
+ assert kill_box["321CamelCaseFever!"] == "Safe"
+
+ con_kill_box = Box(td, conversion_box=True, camel_killer_box=True)
+ assert con_kill_box.camel_case == "Item"
+ assert con_kill_box.x321_camel_case_fever == "Safe"
+
+ def test_default_and_camel_killer_box(self):
+ td = extended_test_dict.copy()
+ td["CamelCase"] = "Item"
+ killer_default_box = Box(td, camel_killer_box=True, default_box=True)
+ assert killer_default_box.camel_case == "Item"
+ assert killer_default_box.CamelCase == "Item"
+ assert isinstance(killer_default_box.does_not_exist, Box)
+ assert isinstance(killer_default_box["does_not_exist"], Box)
+
+ def test_box_modify_tuples(self):
+ bx = Box(extended_test_dict, modify_tuples_box=True)
+ assert bx.tuples_galore[0].item == 3
+ assert isinstance(bx.tuples_galore[0], Box)
+ assert isinstance(bx.tuples_galore[1], tuple)
+
+ def test_box_set_attribs(self):
+ bx = Box(extended_test_dict, conversion_box=False, camel_killer_box=True)
+ bx.camel_case = {"new": "item"}
+ assert bx["CamelCase"] == Box(new="item")
+
+ bx["CamelCase"] = 4
+ assert bx.camel_case == 4
+
+ bx2 = Box(extended_test_dict)
+ bx2.Key_2 = 4
+
+ assert bx2["Key 2"] == 4
+
+ def test_functional_data(self):
+ data = Box.from_json(filename=data_json_file, conversion_box=True, camel_killer_box=True, default_box=False)
+ assert data.widget
+
+ with pytest.raises(AttributeError):
+ data._bad_value
+
+ with pytest.raises(AttributeError):
+ data.widget._bad_value
+
+ base_config = data._Box__box_config()
+ assert base_config.pop("box_namespace") == ()
+ widget_config = data.widget._Box__box_config()
+ assert widget_config.pop("box_namespace") == ("widget",)
+
+ assert base_config == widget_config, "{} != {}".format(base_config, widget_config)
+
+ def test_functional_spaceballs(self):
+ my_box = Box(movie_data)
+
+ my_box.movies.Spaceballs.Stars.append({"name": "Bill Pullman", "imdb": "nm0000597", "role": "Lone Starr"})
+ assert my_box.movies.Spaceballs.Stars[-1].role == "Lone Starr"
+ assert my_box.movies.Robin_Hood_Men_in_Tights.length == 104
+ my_box.movies.Robin_Hood_Men_in_Tights.Stars.pop(0)
+ assert my_box.movies.Robin_Hood_Men_in_Tights.Stars[0].name == "Richard Lewis"
+
+ def test_circular_references(self):
+ circular_dict = {}
+ circular_dict["a"] = circular_dict
+ bx = Box(circular_dict)
+ assert bx.a == {}
+ circular_dict_2 = bx.to_dict()
+ assert str(circular_dict_2) == "{'a': {}}"
+
+ bx2 = Box(circular_dict, k=circular_dict)
+ assert bx2.k.a == bx2.a
+
+ bx.to_json()
+
+ def test_to_multiline(self):
+ a = BoxList([Box(a=1), Box(b=2), Box(three=5)])
+
+ a.to_json(tmp_json_file, multiline=True)
+ count = 0
+ with open(tmp_json_file) as f:
+ for line in f:
+ assert isinstance(json.loads(line), dict)
+ count += 1
+ assert count == 3
+
+ def test_from_multiline(self):
+ content = '{"a": 2}\n{"b": 3}\r\n \n'
+ with open(tmp_json_file, "w") as f:
+ f.write(content)
+
+ a = BoxList.from_json(filename=tmp_json_file, multiline=True)
+ assert a[1].b == 3
+
+ def test_duplicate_errors(self):
+ with pytest.raises(BoxError):
+ Box({"?a": 1, "!a": 3}, box_duplicates="error")
+
+ Box({"?a": 1, "!a": 3}, box_duplicates="ignore")
+
+ with pytest.warns(UserWarning) as warning:
+ Box({"?a": 1, "!a": 3}, box_duplicates="warn")
+ assert warning[0].message.args[0].startswith("Duplicate")
+
+ my_box = Box({"?a": 1}, box_duplicates="error")
+ with pytest.raises(BoxError):
+ my_box["^a"] = 3
+
+ def test_copy(self):
+ my_box = Box(movie_data, camel_killer_box=True)
+ bb = my_box.copy()
+ assert my_box == bb
+ assert isinstance(bb, Box)
+ assert bb._box_config["camel_killer_box"]
+
+ aa = copy.deepcopy(my_box)
+ assert my_box == aa
+ assert isinstance(aa, Box)
+
+ cc = my_box.__copy__()
+ assert my_box == cc
+ assert isinstance(cc, Box)
+ assert cc._box_config["camel_killer_box"]
+
+ dd = BoxList([my_box])
+ assert dd == copy.copy(dd)
+ assert isinstance(copy.copy(dd), BoxList)
+
+ def test_custom_key_errors(self):
+ my_box = Box()
+
+ with pytest.raises(BoxKeyError):
+ my_box.g
+
+ with pytest.raises(AttributeError):
+ my_box.g
+
+ with pytest.raises(KeyError):
+ my_box["g"]
+
+ with pytest.raises(BoxKeyError):
+ my_box["g"]
+
+ with pytest.raises(BoxError):
+ my_box["g"]
+
+ def test_pickle(self):
+ if platform.python_implementation() == "PyPy":
+ pytest.skip("Pickling does not work correctly on PyPy")
+ pic_file = os.path.join(tmp_dir, "test.p")
+ pic2_file = os.path.join(tmp_dir, "test.p2")
+ bb = Box(movie_data, conversion_box=False)
+ pickle.dump(bb, open(pic_file, "wb"))
+ loaded = pickle.load(open(pic_file, "rb"))
+ assert bb == loaded
+ assert loaded._box_config["conversion_box"] is False
+
+ ll = [[Box({"a": "b"})], [[{"c": "g"}]]]
+ bx = BoxList(ll)
+ pickle.dump(bx, open(pic2_file, "wb"))
+ loaded2 = pickle.load(open(pic2_file, "rb"))
+ assert bx == loaded2
+ loaded2.box_options = bx.box_options
+
+ def test_pickle_default_box(self):
+ if platform.python_implementation() == "PyPy":
+ pytest.skip("Pickling does not work correctly on PyPy")
+ bb = Box(default_box=True)
+ loaded = pickle.loads(pickle.dumps(bb))
+ assert bb == loaded
+
+ def test_conversion_dup_only(self):
+ with pytest.raises(BoxError):
+ Box(movie_data, conversion_box=False, box_duplicates="error")
+
+ def test_values(self):
+ b = Box()
+ b.foo = {}
+ assert isinstance(list(b.values())[0], Box)
+ c = Box()
+ c.foohoo = []
+ assert isinstance(list(c.values())[0], BoxList)
+ d = Box(movie_data)
+ assert len(movie_data["movies"].values()) == len(d.movies.values())
+
+ def test_items(self):
+ b = Box()
+ b.foo = {}
+ assert isinstance(list(b.items())[0][1], Box)
+ c = Box()
+ c.foohoo = []
+ assert isinstance(list(c.items())[0][1], BoxList)
+ d = Box(movie_data)
+ assert len(movie_data["movies"].items()) == len(d.movies.items())
+ e = Box(movie_data, box_dots=True)
+ assert sorted(e.items(dotted=True), key=lambda x: x[0]) == sorted(
+ [
+ ("movies.Robin Hood: Men in Tights.Director", "Mel Brooks"),
+ ("movies.Robin Hood: Men in Tights.Stars[0].imdb", "nm0000144"),
+ ("movies.Robin Hood: Men in Tights.Stars[0].name", "Cary Elwes"),
+ ("movies.Robin Hood: Men in Tights.Stars[0].role", "Robin Hood"),
+ ("movies.Robin Hood: Men in Tights.Stars[1].imdb", "nm0507659"),
+ ("movies.Robin Hood: Men in Tights.Stars[1].name", "Richard Lewis"),
+ ("movies.Robin Hood: Men in Tights.Stars[1].role", "Prince John"),
+ ("movies.Robin Hood: Men in Tights.Stars[2].imdb", "nm0715953"),
+ ("movies.Robin Hood: Men in Tights.Stars[2].name", "Roger Rees"),
+ ("movies.Robin Hood: Men in Tights.Stars[2].role", "Sheriff of Rottingham"),
+ ("movies.Robin Hood: Men in Tights.Stars[3].imdb", "nm0001865"),
+ ("movies.Robin Hood: Men in Tights.Stars[3].name", "Amy Yasbeck"),
+ ("movies.Robin Hood: Men in Tights.Stars[3].role", "Marian"),
+ ("movies.Robin Hood: Men in Tights.imdb_stars", 6.7),
+ ("movies.Robin Hood: Men in Tights.length", 104),
+ ("movies.Robin Hood: Men in Tights.rating", "PG-13"),
+ ("movies.Spaceballs.Director", "Mel Brooks"),
+ ("movies.Spaceballs.Stars[0].imdb", "nm0000316"),
+ ("movies.Spaceballs.Stars[0].name", "Mel Brooks"),
+ ("movies.Spaceballs.Stars[0].role", "President Skroob"),
+ ("movies.Spaceballs.Stars[1].imdb", "nm0001006"),
+ ("movies.Spaceballs.Stars[1].name", "John Candy"),
+ ("movies.Spaceballs.Stars[1].role", "Barf"),
+ ("movies.Spaceballs.Stars[2].imdb", "nm0001548"),
+ ("movies.Spaceballs.Stars[2].name", "Rick Moranis"),
+ ("movies.Spaceballs.Stars[2].role", "Dark Helmet"),
+ ("movies.Spaceballs.imdb_stars", 7.1),
+ ("movies.Spaceballs.length", 96),
+ ("movies.Spaceballs.rating", "PG"),
+ ],
+ key=lambda x: x[0],
+ )
+ with pytest.raises(BoxError):
+ Box(box_dots=False).items(dotted=True)
+
+ def test_get(self):
+ bx = Box()
+ bx["c"] = {}
+ assert bx.get("a") is None
+ assert isinstance(bx.get("c"), Box)
+ assert isinstance(bx.get("b", {}), Box)
+ assert "a" in bx.get("a", Box(a=1, conversion_box=False))
+ assert isinstance(bx.get("a", [1, 2]), BoxList)
+ bx_dot = Box(a=Box(b=Box(c="me!")), box_dots=True)
+ assert bx_dot.get("a.b.c") == "me!"
+ assert bx_dot.get("def.not.in.the.box", 4) == 4
+
+ def test_contains(self):
+ bx_dot = Box(a=Box(b=Box(c=Box())), box_dots=True)
+ assert "a.b.c" in bx_dot
+ assert "a.b.c.d" not in bx_dot
+
+ def test_get_default_box(self):
+ bx = Box(default_box=True)
+ assert bx.get("test", 4) == 4
+ assert isinstance(bx.get("a"), Box)
+ assert bx.get("test", None) is None
+
+ def test_inheritance_copy(self):
+ class Box2(Box):
+ pass
+
+ class SBox2(SBox):
+ pass
+
+ class ConfigBox2(ConfigBox):
+ pass
+
+ b = Box2(a=1)
+ c = b.copy()
+ assert c == b
+ assert isinstance(c, Box)
+ c = b.__copy__()
+ assert c == b
+ assert isinstance(c, Box)
+
+ d = SBox2(a=1)
+ e = d.copy()
+ assert e == d
+ assert isinstance(e, SBox)
+ e = d.__copy__()
+ assert e == d
+ assert isinstance(e, SBox)
+
+ f = ConfigBox2(a=1)
+ g = f.copy()
+ assert g == f
+ assert isinstance(g, ConfigBox)
+ g = f.__copy__()
+ assert g == f
+ assert isinstance(g, ConfigBox)
+
+ def test_inheritance(self):
+ data = {
+ "users": [
+ {"users": [{"name": "B"}]},
+ ],
+ }
+
+ class UsersBoxList(BoxList):
+ def find_by_name(self, name):
+ return next((i for i in self if i.name == name), None)
+
+ db = Box(data, box_recast={"users": UsersBoxList}, box_intact_types=[UsersBoxList])
+
+ assert isinstance(db.users, UsersBoxList)
+ assert isinstance(db.users[0].users, UsersBoxList)
+
+ def test_underscore_removal(self):
+ from box import Box
+
+ b = Box(_out="preserved", test_="safe")
+ b.update({"out": "updated", "test": "unsafe"})
+ assert b.out == "updated"
+ assert b._out == "preserved"
+ assert b.to_dict() == {"out": "updated", "test": "unsafe", "_out": "preserved", "test_": "safe"}
+ assert b.test == "unsafe"
+ assert b.test_ == "safe"
+
+ def test_is_in(self):
+ bx = Box()
+ dbx = Box(default_box=True)
+ assert "a" not in bx
+ assert "a" not in dbx
+ bx["b"] = 1
+ dbx["b"] = {}
+ assert "b" in bx
+ assert "b" in dbx
+
+ def test_through_queue(self):
+ my_box = Box(a=4, c={"d": 3})
+ queue = Queue()
+ queue.put(my_box)
+ assert queue.get()
+
+ def test_update_with_integer(self):
+ bx = Box()
+ bx[1] = 4
+ assert bx[1] == 4
+ bx.update({1: 2})
+ assert bx[1] == 2
+
+ def test_get_box_config(self):
+ bx = Box()
+ bx_config = bx.__getattr__("_box_config")
+ assert bx_config
+ with pytest.raises(BoxKeyError):
+ bx["_box_config"]
+
+ def test_pop(self):
+ bx = Box(a=4, c={"d": 3}, sub_box=Box(test=1))
+ assert bx.pop("a") == 4
+ with pytest.raises(BoxKeyError):
+ bx.pop("b")
+ assert bx.pop("a", None) is None
+ assert bx.pop("a", True) is True
+ with pytest.raises(BoxError):
+ bx.pop(1, 2, 3)
+ bx.pop("sub_box").pop("test")
+ assert bx == {"c": {"d": 3}}
+ assert bx.pop("c", True) is not True
+
+ def test_pop_items(self):
+ bx = Box(a=4)
+ assert bx.popitem() == ("a", 4)
+ with pytest.raises(BoxKeyError):
+ assert bx.popitem()
+
+ def test_iter(self):
+ bx = Box()
+ bx.a = 1
+ bx.c = 2
+ assert list(bx.__iter__()) == ["a", "c"]
+
+ def test_revered(self):
+ bx = Box()
+ bx.a = 1
+ bx.c = 2
+ assert list(reversed(bx)) == ["c", "a"]
+
+ def test_clear(self):
+ bx = Box()
+ bx.a = 1
+ bx.c = 4
+ bx["g"] = 7
+ bx.d = 2
+ assert list(bx.keys()) == ["a", "c", "g", "d"]
+ bx.clear()
+ assert bx == {}
+ assert not bx.keys()
+
+ def test_bad_recursive(self):
+ b = Box()
+ bl = b.setdefault("l", [])
+ bl.append(["foo"])
+ assert bl == [["foo"]], bl
+
+ def test_dots(self):
+ b = Box(movie_data.copy(), box_dots=True)
+ assert b["movies.Spaceballs.rating"] == "PG"
+ b["movies.Spaceballs.rating"] = 4
+ assert b["movies.Spaceballs.rating"] == 4
+ del b["movies.Spaceballs.rating"]
+ with pytest.raises(BoxKeyError):
+ b["movies.Spaceballs.rating"]
+ assert b["movies.Spaceballs.Stars[1].role"] == "Barf"
+ b["movies.Spaceballs.Stars[1].role"] = "Testing"
+ assert b["movies.Spaceballs.Stars[1].role"] == "Testing"
+ assert b.movies.Spaceballs.Stars[1].role == "Testing"
+ with pytest.raises(BoxError):
+ b["."]
+ with pytest.raises(BoxError):
+ from box.box import _parse_box_dots
+
+ _parse_box_dots({}, "-")
+
+ with pytest.raises(KeyError):
+ b["a.b"]
+ with pytest.raises(BoxKeyError):
+ b["a.b"]
+
+ with pytest.raises(KeyError):
+ del b["a.b"]
+ with pytest.raises(BoxKeyError):
+ del b["a.b"]
+
+ def test_unicode(self):
+ bx = Box()
+ bx["\U0001f631"] = 4
+
+ bx2 = Box(camel_killer_box=True)
+ bx2["\U0001f631"] = 4
+
+ assert bx == bx2 == {"😱": 4}
+
+ def test_camel_killer_hashables(self):
+ bx = Box(camel_killer_box=True)
+ bx[(1, 2)] = 32
+ assert bx == {(1, 2): 32}
+
+ def test_intact_types_dict(self):
+ from collections import OrderedDict
+
+ bx = Box(a=OrderedDict([("y", 1), ("x", 2)]))
+ assert isinstance(bx.a, Box)
+ assert not isinstance(bx.a, OrderedDict)
+ bx = Box(a=OrderedDict([("y", 1), ("x", 2)]), box_intact_types=[OrderedDict])
+ assert isinstance(bx.a, OrderedDict)
+ assert not isinstance(bx.a, Box)
+
+ def test_delete_attributes(self):
+ b = Box(notThief=1, sortaThief=0, reallyAThief=True, camel_killer_box=True)
+ b["$OhNo!"] = 3
+ c = Box(notThief=1, sortaThief=0, reallyAThief=True, camel_killer_box=True, conversion_box=False)
+ del b.not_thief
+ del b._oh_no_
+ del b.really_a_thief
+ with pytest.raises(KeyError):
+ del b.really_a_thief
+ with pytest.raises(KeyError):
+ del b._oh_no_
+
+ del c.not_thief
+ del c.really_a_thief
+ with pytest.raises(KeyError):
+ del c.really_a_thief
+
+ def test_add_boxes(self):
+ b = Box(c=1, d={"sub": 1}, e=1)
+ c = dict(d={"val": 2}, e=4)
+ assert b + c == Box(c=1, d={"sub": 1, "val": 2}, e=4)
+ with pytest.raises(BoxError):
+ Box() + BoxList()
+
+ def test_iadd_boxes(self):
+ b = Box(c=1, d={"sub": 1}, e=1)
+ c = dict(d={"val": 2}, e=4)
+ b += c
+ assert b == Box(c=1, d={"sub": 1, "val": 2}, e=4)
+ with pytest.raises(BoxError):
+ a = Box()
+ a += BoxList()
+
+ def test_radd_boxes(self):
+ a = dict(a=1)
+ d = Box(e=2)
+ d | a
+ b = dict(c=1, d={"sub": 1}, e=1)
+ c = Box(d={"val": 2}, e=4)
+ assert (b + c) == Box(c=1, d={"sub": 1, "val": 2}, e=4)
+ assert c + b == Box(c=1, d={"sub": 1, "val": 2}, e=1)
+ assert isinstance(b | c, Box)
+ with pytest.raises(BoxError):
+ BoxList() + Box()
+
+ def test_or_boxes(self):
+ b = Box(c=1, d={"sub": 1}, e=1)
+ c = dict(d={"val": 2}, e=4)
+ assert b | c == Box(c=1, d={"val": 2}, e=4)
+ with pytest.raises(BoxError):
+ Box() | BoxList()
+
+ def test_ior_boxes(self):
+ b = Box(c=1, d={"sub": 1}, e=1)
+ c = dict(d={"val": 2}, e=4)
+ b |= c
+ assert b == Box(c=1, d={"val": 2}, e=4)
+ with pytest.raises(BoxError):
+ a = Box()
+ a |= BoxList()
+
+ def test_ror_boxes(self):
+ b = dict(c=1, d={"sub": 1}, e=1)
+ c = Box(d={"val": 2}, e=4)
+ assert c.__ror__(b) == Box(c=1, d={"val": 2}, e=4)
+ assert c | b == Box(c=1, d={"sub": 1}, e=1)
+ assert isinstance(b | c, Box)
+ with pytest.raises(BoxError):
+ BoxList() | Box()
+
+ def test_type_recast(self):
+ b = Box(id="6", box_recast={"id": int})
+ assert isinstance(b.id, int)
+ with pytest.raises(ValueError):
+ b["sub_box"] = {"id": "bad_id"}
+
+ def test_nontype_recast(self):
+ class CustomError(ValueError):
+ pass
+
+ def cast_id(val) -> int:
+ if val == "bad_id":
+ raise CustomError()
+ return int(val)
+
+ b = Box(id="6", box_recast={"id": cast_id})
+ assert isinstance(b.id, int)
+ with pytest.raises(ValueError) as exc_info:
+ b["sub_box"] = {"id": "bad_id"}
+ assert isinstance(exc_info.value.__cause__, CustomError)
+
+ def test_box_dots(self):
+ b = Box(
+ {"my_key": {"does stuff": {"to get to": "where I want"}}, "key.with.list": [[[{"test": "value"}]]]},
+ box_dots=True,
+ default_box=True,
+ )
+ for key in b.keys(dotted=True):
+ b[key]
+
+ c = Box(extended_test_dict.copy(), box_dots=True)
+ for key in c.keys(dotted=True):
+ c[key]
+
+ assert b["my_key.does stuff.to get to"] == "where I want"
+ b["my_key.does stuff.to get to"] = "test"
+ assert b["my_key.does stuff.to get to"] == "test"
+ del b["my_key.does stuff"]
+ assert b["my_key"] == {}
+ b[4] = 2
+ assert b[4] == 2
+ del b[4]
+ assert b["key.with.list[0][0][0].test"] == "value"
+ b["key.with.list[0][0][0].test"] = "new_value"
+ assert b["key.with.list"][0][0][0]["test"] == "new_value"
+ del b["key.with.list[0][0][0].test"]
+ assert not b["key.with.list[0][0][0]"]
+ del b["key.with.list[0][0]"]
+ with pytest.raises(IndexError):
+ b["key.with.list[0][0][0]"]
+ del b["key.with.list[0]"]
+ with pytest.raises(IndexError):
+ b["key.with.list[0][0]"]
+
+ d = Box()
+ with pytest.raises(BoxError):
+ d.keys(dotted=True)
+
+ def test_toml(self):
+ b = Box.from_toml(filename=Path(test_root, "data", "toml_file.tml"), default_box=True)
+ assert b.database.server == "192.168.1.1"
+ assert b.clients.hosts == ["alpha", "omega"]
+ assert b.database.to_toml().startswith('server = "192.168.1.1"')
+ assert b._box_config["default_box"] is True
+
+ def test_parameter_pass_through(self):
+ bx = Box.from_yaml(
+ "uno: 2",
+ box_dots=True,
+ default_box=True,
+ default_box_attr=None,
+ default_box_none_transform=True,
+ frozen_box=False,
+ camel_killer_box=True,
+ conversion_box=True,
+ modify_tuples_box=True,
+ box_safe_prefix="x",
+ box_duplicates="warn",
+ box_intact_types=(),
+ box_recast=None,
+ )
+ assert bx.uno == 2
+
+ def test_sub(self):
+ difference = Box(extended_test_dict) - test_dict
+ assert difference == {
+ 3: "howdy",
+ "not": "true",
+ (3, 4): "test",
+ "_box_config": True,
+ "CamelCase": "21",
+ "321CamelCase": 321,
+ False: "tree",
+ "tuples_galore": ({"item": 3}, ({"item": 4}, 5)),
+ }
+
+ def test_sub_with_non_dict(self):
+ with pytest.raises(BoxError):
+ Box(extended_test_dict) - BoxList([1, 2, 3])
+
+ def test_sub_with_frozen_box(self):
+ difference = Box(extended_test_dict, frozen_box=True) - test_dict
+ assert difference == {
+ 3: "howdy",
+ "not": "true",
+ (3, 4): "test",
+ "_box_config": True,
+ "CamelCase": "21",
+ "321CamelCase": 321,
+ False: "tree",
+ "tuples_galore": ({"item": 3}, ({"item": 4}, 5)),
+ }
+
+ def test_no_key_error_pop(self):
+ box1 = Box(default_box=True)
+ box1.pop("non_exist_key")
+ assert box1 == {}
+
+ def test_key_error_popitem(self):
+ box1 = Box(default_box=True)
+ with pytest.raises(BoxKeyError):
+ box1.popitem()
+
+ def test_msgpack_strings(self):
+ box1 = Box(test_dict)
+ packed = box1.to_msgpack()
+ assert Box.from_msgpack(packed) == box1
+
+ def test_msgpack_strings_no_strick_keys(self):
+ box1 = Box(test_dict)
+ box1[5] = 2
+ packed = box1.to_msgpack()
+ assert Box.from_msgpack(packed, strict_map_key=False) == box1
+
+ def test_msgpack_files(self):
+ box1 = Box(test_dict)
+ box1.to_msgpack(filename=tmp_msgpack_file)
+ assert Box.from_msgpack(filename=tmp_msgpack_file) == box1
+
+ def test_msgpack_no_input(self):
+ with pytest.raises(BoxError):
+ Box.from_msgpack()
+
+ def test_value_view(self):
+ a = Box()
+ my_view = a.values()
+ assert len(my_view) == 0
+ a["test"] = "key_one"
+ a.test2 = "key_two"
+ assert len(my_view) == 2
+ assert "key_one" in my_view
+ assert "key_two" in my_view
+
+ def test_key_view(self):
+ a = Box()
+ my_view = a.keys()
+ assert len(my_view) == 0
+ a["test"] = "key_one"
+ a.test2 = "key_two"
+ assert len(my_view) == 2
+ assert "test" in my_view
+ assert "test2" in my_view
+
+ def test_item_view(self):
+ a = Box()
+ my_view = a.items()
+ assert len(my_view) == 0
+ a["test"] = "key_one"
+ a.test2 = "key_two"
+ assert len(my_view) == 2
+ assert ("test", "key_one") in my_view
+ assert ("test2", "key_two") in my_view
+
+ def test_box_propagation(self):
+ # Issue 150
+ hash(Box({"x": Box({"y": 2})}, frozen_box=True))
+ hash(Box({"x": [Box({"y": 2})]}, frozen_box=True))
+
+ def test_box_safe_references(self):
+ a = Box(c=5)
+ b = Box(a=a)
+ assert id(a) != id(b.a)
+
+ def test_default_box_restricted_calls(self):
+ a = Box(default_box=True)
+ with pytest.raises(BoxKeyError):
+ a._test_thing_
+ assert len(list(a.keys())) == 0
+
+ # Based on argparse.parse_args internal behavior, the following
+ # creates the attribute in hasattr due to default_box=True, then
+ # deletes it in delattr.
+ if hasattr(a, "_unrecognized_args"):
+ delattr(a, "_unrecognized_args")
+
+ a._allowed_prefix
+ a.allowed_postfix_
+ assert len(list(a.keys())) == 2
+
+ def test_default_dots(self):
+ bx1 = Box(default_box=True, box_dots=True)
+ bx1["a.a.a"]
+ assert bx1 == {"a": {"a": {"a": {}}}}
+
+ a = Box(default_box=True, box_dots=True)
+ a["a."]
+ a["a.."]
+ assert a == {"a": {"": {"": {}}}}
+ a["b.b"] = 3
+ assert a == {"a": {"": {"": {}}}, "b": {"b": 3}}
+ a.b.b = 4
+ assert a == {"a": {"": {"": {}}}, "b": {"b": 4}}
+ assert a["non.existent.key"] == {}
+
+ def test_merge_list_options(self):
+ a = Box()
+ a.merge_update({"lister": ["a"]})
+ a.merge_update({"lister": ["a", "b", "c"]}, box_merge_lists="extend")
+ assert a.lister == ["a", "a", "b", "c"]
+ a.merge_update({"lister": ["a", "b", "c"]}, box_merge_lists="unique")
+ assert a.lister == ["a", "a", "b", "c"]
+ a.merge_update({"lister": ["a", "d", "b", "c"]}, box_merge_lists="unique")
+ assert a.lister == ["a", "a", "b", "c", "d"]
+ a.merge_update({"key1": {"new": 5}, "Key 2": {"add_key": 6}, "lister": ["a"]})
+ assert a.lister == ["a"]
+
+ d1 = {"app": {"S3": {"S3Service": [{"bucket": "bucket001"}]}}}
+
+ d2 = {"app": {"S3": {"S3Service": [{"expirationDate": "2099-10-25"}]}}}
+
+ box1 = Box(d1)
+
+ box1.merge_update(d2, box_merge_lists="extend")
+
+ assert box1 == Box(
+ {"app": {"S3": {"S3Service": [{"bucket": "bucket001"}, {"expirationDate": "2099-10-25"}]}}}
+ ), box1
+
+ def test_box_from_empty_yaml(self):
+ out = Box.from_yaml("---")
+ assert out == Box()
+
+ out2 = BoxList.from_yaml("---")
+ assert out2 == BoxList()
+
+ def test_setdefault_simple(self):
+ box = Box({"a": 1})
+ box.setdefault("b", 2)
+ box.setdefault("c", "test")
+ box.setdefault("d", {"e": True})
+ box.setdefault("f", [1, 2])
+
+ assert box["b"] == 2
+ assert box["c"] == "test"
+ assert isinstance(box["d"], Box)
+ assert box["d"]["e"] == True
+ assert isinstance(box["f"], BoxList)
+ assert box["f"][1] == 2
+
+ def test_setdefault_dots(self):
+ box = Box({"a": 1}, box_dots=True)
+ box.setdefault("b", 2)
+ box.c = {"d": 3}
+ box.setdefault("c.e", "test")
+ box.setdefault("d", {"e": True})
+ box.setdefault("f", [1, 2])
+
+ assert box.b == 2
+ assert box.c.e == "test"
+ assert isinstance(box["d"], Box)
+ assert box.d.e == True
+ assert isinstance(box["f"], BoxList)
+ assert box.f[1] == 2
+
+ def test_setdefault_dots_default(self):
+ box = Box({"a": 1}, box_dots=True, default_box=True)
+ box.b.c.d.setdefault("e", 2)
+ box.c.setdefault("e", "test")
+ box.d.e.setdefault("f", {"g": True})
+ box.e.setdefault("f", [1, 2])
+
+ assert box["b.c.d"].e == 2
+ assert box.c.e == "test"
+ assert isinstance(box["d.e.f"], Box)
+ assert box.d.e["f.g"] is True
+ assert isinstance(box["e.f"], BoxList)
+ assert box.e.f[1] == 2
+
+ def test_box_slice(self):
+ data = Box(qwe=123, asd=234, q=1)
+ assert data[:-1] == Box(qwe=123, asd=234)
+
+ def test_box_kwargs_should_not_be_included(self):
+ params = {
+ "default_box": True,
+ "default_box_attr": True,
+ "conversion_box": True,
+ "frozen_box": True,
+ "camel_killer_box": True,
+ "box_safe_prefix": "x",
+ "box_duplicates": "error",
+ "default_box_none_transform": True,
+ "box_dots": True,
+ "modify_tuples_box": True,
+ "box_intact_types": (),
+ "box_recast": {"id": int},
+ }
+
+ bx = Box(**params)
+ assert bx == Box()
+
+ for param in params:
+ assert param in BOX_PARAMETERS
+
+ def test_box_greek(self):
+ # WARNING μ is ord 956 whereas µ is ord 181 and will not work due to python NFKC normalization
+ a = Box()
+ a.σeq = 1
+ a.µeq = 2
+ assert a == Box({"σeq": 1, "μeq": 2})
+
+ def test_box_default_not_create_on_get(self):
+ box = Box(default_box=True)
+
+ assert box.a.b.c == Box()
+
+ assert box == Box(a=Box(b=Box(c=Box())))
+ assert "c" in box.a.b
+
+ box2 = Box(default_box=True, default_box_create_on_get=False)
+
+ assert box2.a.b.c == Box()
+
+ assert "c" not in box2.a.b
+
+ assert box2 == Box()
+
+ def test_box_property_support(self):
+ class BoxWithProperty(Box):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ @property
+ def field(self):
+ return self._field
+
+ @field.setter
+ def field(self, value):
+ self._field = value
+
+ @field.deleter
+ def field(self):
+ """
+ This is required to make `del box.field` work properly otherwise a `BoxKeyError` would be thrown.
+ """
+ del self._field
+
+ box = BoxWithProperty()
+ box.field = 5
+
+ assert "field" not in box
+ assert "_field" in box
+ assert box.field == 5
+ assert box._field == 5
+ del box.field
+ assert not "_field" in box
+
+ def test_box_namespace(self):
+ bx = Box(default_box=True)
+ assert bx._box_config["box_namespace"] == ()
+ bx.a.b.c = 5
+ assert bx.a._box_config["box_namespace"] == ("a",)
+ assert bx.a.b._box_config["box_namespace"] == ("a", "b")
+ bx.x = {"y": {"z": 5}}
+ assert bx.x._box_config["box_namespace"] == ("x",)
+ assert bx.x.y._box_config["box_namespace"] == ("x", "y")
+ bx[None][1][2] = 3
+ assert bx[None][1]._box_config["box_namespace"] == (None, 1)
+
+ for modified_box in [
+ bx.a + bx.x,
+ bx.a - bx.x,
+ bx.a | bx.x,
+ ]:
+ assert modified_box._box_config["box_namespace"] == ()
+ assert modified_box.b._box_config["box_namespace"] == ("b",)
+ assert modified_box.y._box_config["box_namespace"] == ("y",)
+
+ bx.modified = {}
+ assert bx.modified._box_config["box_namespace"] == ("modified",)
+ bx.modified += bx.a
+ assert bx.modified.b._box_config["box_namespace"] == ("modified", "b")
+ bx.modified |= bx.x
+ assert bx.modified.y._box_config["box_namespace"] == ("modified", "y")
+ bx.modified -= bx.a
+ assert bx.modified._box_config["box_namespace"] == ("modified",)
+
+ bx2 = Box(box_namespace=False)
+ assert bx2._box_config["box_namespace"] is False
+ bx2["x"] = {"y": {"z": 5}}
+ assert bx2._box_config["box_namespace"] is False
+ assert bx2["x"]._box_config["box_namespace"] is False
+
+ def test_union_frozen_box(self):
+ my_box = Box(a=5, frozen_box=True)
+
+ assert my_box | {"a": 1} == {"a": 1}
+ assert {"a": 1} | my_box == {"a": 5}
+
+ def test_default_box_callable(self):
+ def func(box_instance, key):
+ return DDBox(bi=str(box_instance), key=key)
+
+ my_box = DDBox(default_box_attr=func)
+
+ assert my_box.a == {"bi": "{}", "key": "a"}
diff --git a/test/test_box_list.py b/test/test_box_list.py
new file mode 100644
index 0000000..99c65d7
--- /dev/null
+++ b/test/test_box_list.py
@@ -0,0 +1,258 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Test files gathered from json.org and yaml.org
+
+import json
+import os
+import shutil
+import sys
+import platform
+from pathlib import Path
+from io import StringIO
+from test.common import test_root, tmp_dir
+
+import pytest
+from ruamel.yaml import YAML
+
+from box import Box, BoxError, BoxList
+from box.converters import toml_read_library, toml_write_library
+
+
+class TestBoxList:
+ @pytest.fixture(autouse=True)
+ def temp_dir_cleanup(self):
+ shutil.rmtree(str(tmp_dir), ignore_errors=True)
+ try:
+ os.mkdir(str(tmp_dir))
+ except OSError:
+ pass
+ yield
+ shutil.rmtree(str(tmp_dir), ignore_errors=True)
+
+ def test_box_list(self):
+ new_list = BoxList({"item": x} for x in range(0, 10))
+ new_list.extend([{"item": 22}])
+ assert new_list[-1].item == 22
+ new_list.append([{"bad_item": 33}])
+ assert new_list[-1][0].bad_item == 33
+ new_list[-1].append([{"bad_item": 33}])
+ assert new_list[-1, -1, 0].bad_item == 33
+ bx = Box({0: {1: {2: {3: 3}}}, (0, 1, 2, 3): 4})
+ assert bx[0, 1, 2, 3] == 4
+ assert repr(new_list).startswith("BoxList(")
+ for x in new_list.to_list():
+ assert not isinstance(x, (BoxList, Box))
+ new_list.insert(0, {"test": 5})
+ new_list.insert(1, ["a", "b"])
+ new_list.append("x")
+ assert new_list[0].test == 5
+ assert isinstance(str(new_list), str)
+ assert isinstance(new_list[1], BoxList)
+ assert not isinstance(new_list.to_list(), BoxList)
+
+ def test_frozen_list(self):
+ bl = BoxList([5, 4, 3], frozen_box=True)
+ with pytest.raises(BoxError):
+ bl.pop(1)
+ with pytest.raises(BoxError):
+ bl.remove(4)
+ with pytest.raises(BoxError):
+ bl.sort()
+ with pytest.raises(BoxError):
+ bl.reverse()
+ with pytest.raises(BoxError):
+ bl.append("test")
+ with pytest.raises(BoxError):
+ bl.extend([4])
+ with pytest.raises(BoxError):
+ del bl[0]
+ with pytest.raises(BoxError):
+ bl[0] = 5
+ bl2 = BoxList([5, 4, 3])
+ del bl2[0]
+ assert bl2[0] == 4
+ bl2[1] = 4
+ assert bl2[1] == 4
+
+ def test_box_list_to_json(self):
+ bl = BoxList([{"item": 1, "CamelBad": 2}])
+ assert json.loads(bl.to_json())[0]["item"] == 1
+
+ def test_box_list_from_json(self):
+ alist = [{"item": 1}, {"CamelBad": 2}]
+ json_list = json.dumps(alist)
+ bl = BoxList.from_json(json_list, camel_killer_box=True)
+ assert bl[0].item == 1
+ assert bl[1].camel_bad == 2
+
+ with pytest.raises(BoxError):
+ BoxList.from_json(json.dumps({"a": 2}))
+
+ def test_box_list_to_yaml(self):
+ bl = BoxList([{"item": 1, "CamelBad": 2}])
+ yaml = YAML()
+ assert yaml.load(bl.to_yaml())[0]["item"] == 1
+
+ def test_box_list_from_yaml(self):
+ alist = [{"item": 1}, {"CamelBad": 2}]
+ yaml = YAML()
+ with StringIO() as sio:
+ yaml.dump(alist, stream=sio)
+ bl = BoxList.from_yaml(sio.getvalue(), camel_killer_box=True)
+ assert bl[0].item == 1
+ assert bl[1].camel_bad == 2
+
+ with pytest.raises(BoxError):
+ BoxList.from_yaml("a: 2")
+
+ def test_box_list_to_toml(self):
+ bl = BoxList([{"item": 1, "CamelBad": 2}])
+ assert toml_read_library.loads(bl.to_toml(key_name="test"))["test"][0]["item"] == 1
+ with pytest.raises(BoxError):
+ BoxList.from_toml("[[test]]\nitem = 1\nCamelBad = 2\n\n", key_name="does not exist")
+
+ def test_box_list_from_tml(self):
+ alist = [{"item": 1}, {"CamelBad": 2}]
+ toml_list = toml_write_library.dumps({"key": alist})
+ bl = BoxList.from_toml(toml_string=toml_list, key_name="key", camel_killer_box=True)
+ assert bl[0].item == 1
+ assert bl[1].camel_bad == 2
+
+ with pytest.raises(BoxError):
+ BoxList.from_toml(toml_write_library.dumps({"a": 2}), "a")
+
+ with pytest.raises(BoxError):
+ BoxList.from_toml(toml_list, "bad_key")
+
+ def test_intact_types_list(self):
+ class MyList(list):
+ pass
+
+ bl = BoxList([[1, 2], MyList([3, 4])], box_intact_types=(MyList,))
+ assert isinstance(bl[0], BoxList)
+
+ def test_to_csv(self):
+ data = BoxList(
+ [
+ {"Number": 1, "Name": "Chris", "Country": "US"},
+ {"Number": 2, "Name": "Sam", "Country": "US"},
+ {"Number": 3, "Name": "Jess", "Country": "US"},
+ {"Number": 4, "Name": "Frank", "Country": "UK"},
+ {"Number": 5, "Name": "Demo", "Country": "CA"},
+ ]
+ )
+
+ file = Path(tmp_dir, "csv_file.csv")
+ data.to_csv(filename=file)
+ assert file.read_text().startswith("Number,Name,Country\n1,Chris,US")
+ assert data.to_csv().endswith("2,Sam,US\r\n3,Jess,US\r\n4,Frank,UK\r\n5,Demo,CA\r\n")
+
+ def test_from_csv(self):
+ bl = BoxList.from_csv(filename=Path(test_root, "data", "csv_file.csv"))
+ assert bl[1].Name == "Sam"
+ b2 = BoxList.from_csv(
+ "Number,Name,Country\r\n1,Chris,US\r\n2,Sam" ",US\r\n3,Jess,US\r\n4,Frank,UK\r\n5,Demo,CA\r\n"
+ )
+ assert b2[2].Name == "Jess"
+
+ def test_bad_csv(self):
+ data = BoxList([{"test": 1}, {"bad": 2, "data": 3}])
+ file = Path(tmp_dir, "csv_file.csv")
+ with pytest.raises(BoxError):
+ data.to_csv(file)
+
+ def test_box_list_dots(self):
+ data = BoxList(
+ [
+ {"test": 1},
+ {"bad": 2, "data": 3},
+ [[[0, -1], [77, 88]], {"inner": "one", "lister": [[{"down": "rabbit"}]]}],
+ 4,
+ ],
+ box_dots=True,
+ )
+
+ assert data["[0].test"] == 1
+ assert data["[1].data"] == 3
+ assert data[1].data == 3
+ data["[1].data"] = "new_data"
+ assert data["[1].data"] == "new_data"
+ assert data["[2][0][0][1]"] == -1
+ assert data[2][0][0][1] == -1
+ data["[2][0][0][1]"] = 1_000_000
+ assert data["[2][0][0][1]"] == 1_000_000
+ assert data[2][0][0][1] == 1_000_000
+ assert data["[2][1].lister[0][0].down"] == "rabbit"
+ data["[2][1].lister[0][0].down"] = "hole"
+ assert data["[2][1].lister[0][0].down"] == "hole"
+ assert data[2][1].lister[0][0].down == "hole"
+
+ db = Box(a=data, box_dots=True)
+ keys = db.keys(dotted=True)
+ assert keys == [
+ "a[0].test",
+ "a[1].bad",
+ "a[1].data",
+ "a[2][0][0][0]",
+ "a[2][0][0][1]",
+ "a[2][0][1][0]",
+ "a[2][0][1][1]",
+ "a[2][1].inner",
+ "a[2][1].lister[0][0].down",
+ "a[3]",
+ ]
+ for key in keys:
+ db[key]
+
+ def test_box_list_default_dots(self):
+ box_1 = Box(default_box=True, box_dots=True)
+ box_1["a[0]"] = 42
+ assert box_1.a[0] == 42
+
+ box_1["b[0].c[0].d"] = 42
+ assert box_1.b[0].c[0].d == 42
+
+ box_1["c[0][0][0]"] = 42
+ assert box_1.c[0][0][0] == 42
+
+ box_2 = Box(default_box=True, box_dots=True)
+ box_2["a[4]"] = 42
+ assert box_2.a.to_list() == [None, None, None, None, 42]
+
+ box_3 = Box(default_box=True, box_dots=True)
+ box_3["a.b[0]"] = 42
+ assert box_3.a.b[0] == 42
+
+ def test_box_config_propagate(self):
+ structure = Box(a=[Box(default_box=False)], default_box=True, box_inherent_settings=True)
+ assert structure._box_config["default_box"] is True
+ assert structure.a[0]._box_config["default_box"] is True
+
+ base = BoxList([BoxList([Box(default_box=False)])], default_box=True)
+ assert base[0].box_options["default_box"] is True
+
+ base2 = BoxList((BoxList([Box()], default_box=False),), default_box=True)
+ assert base2[0][0]._box_config["default_box"] is True
+
+ base3 = Box(
+ a=[Box(default_box=False)], default_box=True, box_inherent_settings=True, box_intact_types=[Box, BoxList]
+ )
+ base3.a.append(Box(default_box=False))
+ base3.a.append(BoxList(default_box=False))
+
+ for item in base3.a:
+ if isinstance(item, Box):
+ assert item._box_config["default_box"] is True
+ elif isinstance(item, BoxList):
+ assert item.box_options["default_box"] is True
+
+ def test_no_recursion_errors(self):
+ a = Box({"list_of_dicts": [[{"example1": 1}]]})
+ a.list_of_dicts.append([{"example2": 2}])
+ assert a["list_of_dicts"][1] == [{"example2": 2}]
+
+ def test_circular_references(self):
+ circular_list = []
+ circular_list.append(circular_list)
+ circular_box = BoxList(circular_list)
+ assert circular_box[0] == circular_box
diff --git a/test/test_config_box.py b/test/test_config_box.py
new file mode 100644
index 0000000..345ed55
--- /dev/null
+++ b/test/test_config_box.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from test.common import test_dict
+
+from box import Box, ConfigBox
+
+
+class TestConfigBox:
+ def test_config_box(self):
+ g = {
+ "b0": "no",
+ "b1": "yes",
+ "b2": "True",
+ "b3": "false",
+ "b4": True,
+ "i0": "34",
+ "f0": "5.5",
+ "f1": "3.333",
+ "l0": "4,5,6,7,8",
+ "l1": "[2 3 4 5 6]",
+ }
+
+ cns = ConfigBox(bb=g)
+ assert cns.bb.list("l1", spliter=" ") == ["2", "3", "4", "5", "6"]
+ assert cns.bb.list("l0", mod=lambda x: int(x)) == [4, 5, 6, 7, 8]
+ assert not cns.bb.bool("b0")
+ assert cns.bb.bool("b1")
+ assert cns.bb.bool("b2")
+ assert not cns.bb.bool("b3")
+ assert cns.bb.int("i0") == 34
+ assert cns.bb.float("f0") == 5.5
+ assert cns.bb.float("f1") == 3.333
+ assert cns.bb.getboolean("b4"), cns.bb.getboolean("b4")
+ assert cns.bb.getfloat("f0") == 5.5
+ assert cns.bb.getint("i0") == 34
+ assert cns.bb.getint("Hello!", 5) == 5
+ assert cns.bb.getfloat("Wooo", 4.4) == 4.4
+ assert cns.bb.getboolean("huh", True) is True
+ assert cns.bb.list("Waaaa", [1]) == [1]
+ assert repr(cns).startswith("ConfigBox(")
+
+ def test_dir(self):
+ b = ConfigBox(test_dict)
+
+ for item in ("to_yaml", "to_dict", "to_json", "int", "list", "float"):
+ assert item in dir(b)
+
+ def test_config_default(self):
+ bx4 = Box(default_box=True, default_box_attr=ConfigBox)
+ assert isinstance(bx4.bbbbb, ConfigBox)
diff --git a/test/test_converters.py b/test/test_converters.py
new file mode 100644
index 0000000..014f1a4
--- /dev/null
+++ b/test/test_converters.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import json
+import os
+import shutil
+from pathlib import Path
+from test.common import movie_data, tmp_dir
+
+import msgpack
+import pytest
+from ruamel.yaml import YAML
+
+from box import BoxError
+from box.converters import _from_toml, _to_json, _to_msgpack, _to_toml, _to_yaml
+
+toml_string = """[movies.Spaceballs]
+imdb_stars = 7.1
+rating = "PG"
+length = 96
+Director = "Mel Brooks"
+[[movies.Spaceballs.Stars]]
+name = "Mel Brooks"
+imdb = "nm0000316"
+role = "President Skroob"
+
+[[movies.Spaceballs.Stars]]
+name = "John Candy"
+imdb = "nm0001006"
+role = "Barf"
+"""
+
+
+class TestConverters:
+ @pytest.fixture(autouse=True)
+ def temp_dir_cleanup(self):
+ shutil.rmtree(str(tmp_dir), ignore_errors=True)
+ try:
+ os.mkdir(str(tmp_dir))
+ except OSError:
+ pass
+ yield
+ shutil.rmtree(str(tmp_dir), ignore_errors=True)
+
+ def test_to_toml(self):
+ formatted = _to_toml(movie_data)
+ assert formatted.startswith("[movies.Spaceballs]")
+
+ def test_to_toml_file(self):
+ out_file = Path(tmp_dir, "toml_test.tml")
+ assert not out_file.exists()
+ _to_toml(movie_data, filename=out_file)
+ assert out_file.exists()
+ assert out_file.read_text().startswith("[movies.Spaceballs]")
+
+ def test_from_toml(self):
+ result = _from_toml(toml_string)
+ assert result["movies"]["Spaceballs"]["length"] == 96
+
+ def test_from_toml_file(self):
+ out_file = Path(tmp_dir, "toml_test.tml")
+ assert not out_file.exists()
+ out_file.write_text(toml_string)
+ result = _from_toml(filename=out_file)
+ assert result["movies"]["Spaceballs"]["length"] == 96
+
+ def test_bad_from_toml(self):
+ with pytest.raises(BoxError):
+ _from_toml()
+
+ def test_to_json(self):
+ m_file = os.path.join(tmp_dir, "movie_data")
+ movie_string = _to_json(movie_data)
+ assert "Rick Moranis" in movie_string
+ _to_json(movie_data, filename=m_file)
+ assert "Rick Moranis" in open(m_file).read()
+ assert json.load(open(m_file)) == json.loads(movie_string)
+
+ def test_to_yaml(self):
+ m_file = os.path.join(tmp_dir, "movie_data")
+ movie_string = _to_yaml(movie_data)
+ assert "Rick Moranis" in movie_string
+ _to_yaml(movie_data, filename=m_file)
+ assert "Rick Moranis" in open(m_file).read()
+ yaml = YAML()
+ assert yaml.load(open(m_file)) == yaml.load(movie_string)
+
+ def test_to_msgpack(self):
+ m_file = os.path.join(tmp_dir, "movie_data")
+ msg_data = _to_msgpack(movie_data)
+ assert b"Rick Moranis" in msg_data
+ _to_msgpack(movie_data, filename=m_file)
+ assert b"Rick Moranis" in open(m_file, "rb").read()
+ assert msgpack.unpack(open(m_file, "rb")) == msgpack.unpackb(msg_data)
+
+ def test_to_yaml_ruamel(self):
+ movie_string = _to_yaml(movie_data, ruamel_attrs={"width": 12})
+ multiline_except = """ - name: Roger
+ Rees
+ imdb: nm0715953
+ role: Sheriff
+ of Rottingham
+ - name: Amy
+ Yasbeck"""
+ assert multiline_except in movie_string
diff --git a/test/test_from_file.py b/test/test_from_file.py
new file mode 100644
index 0000000..7361e84
--- /dev/null
+++ b/test/test_from_file.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+from pathlib import Path
+from test.common import test_root
+
+import pytest
+
+from box import Box, BoxError, BoxList, box_from_file, box_from_string
+
+
+class TestFromFile:
+ def test_from_all(self):
+ assert isinstance(box_from_file(Path(test_root, "data", "json_file.json")), Box)
+ assert isinstance(box_from_file(Path(test_root, "data", "toml_file.tml")), Box)
+ assert isinstance(box_from_file(Path(test_root, "data", "yaml_file.yaml")), Box)
+ assert isinstance(box_from_file(Path(test_root, "data", "json_file.json"), file_type="json"), Box)
+ assert isinstance(box_from_file(Path(test_root, "data", "toml_file.tml"), file_type="toml"), Box)
+ assert isinstance(box_from_file(Path(test_root, "data", "yaml_file.yaml"), file_type="yaml"), Box)
+ assert isinstance(box_from_file(Path(test_root, "data", "json_list.json")), BoxList)
+ assert isinstance(box_from_file(Path(test_root, "data", "yaml_list.yaml")), BoxList)
+ assert isinstance(box_from_file(Path(test_root, "data", "msgpack_file.msgpack")), Box)
+ assert isinstance(box_from_file(Path(test_root, "data", "msgpack_list.msgpack")), BoxList)
+ assert isinstance(box_from_file(Path(test_root, "data", "csv_file.csv")), BoxList)
+
+ def test_bad_file(self):
+ with pytest.raises(BoxError):
+ box_from_file(Path(test_root, "data", "bad_file.txt"), file_type="json")
+ with pytest.raises(BoxError):
+ box_from_file(Path(test_root, "data", "bad_file.txt"), file_type="toml")
+ with pytest.raises(BoxError):
+ box_from_file(Path(test_root, "data", "bad_file.txt"), file_type="yaml")
+ with pytest.raises(BoxError):
+ box_from_file(Path(test_root, "data", "bad_file.txt"), file_type="msgpack")
+ with pytest.raises(BoxError):
+ box_from_file(Path(test_root, "data", "bad_file.txt"), file_type="unknown")
+ with pytest.raises(BoxError):
+ box_from_file(Path(test_root, "data", "bad_file.txt"))
+ with pytest.raises(BoxError):
+ box_from_file("does not exist")
+
+ def test_from_string_all(self):
+ with open(Path(test_root, "data", "json_file.json"), "r") as f:
+ box_from_string(f.read())
+
+ with open(Path(test_root, "data", "toml_file.tml"), "r") as f:
+ box_from_string(f.read(), string_type="toml")
+
+ with open(Path(test_root, "data", "yaml_file.yaml"), "r") as f:
+ box_from_string(f.read(), string_type="yaml")
diff --git a/test/test_functional_box.py b/test/test_functional_box.py
deleted file mode 100644
index 8db60ab..0000000
--- a/test/test_functional_box.py
+++ /dev/null
@@ -1,928 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: UTF-8 -*-
-# Test files gathered from json.org and yaml.org
-from __future__ import absolute_import
-
-from multiprocessing import Process, Queue
-import pytest
-import pickle
-
-try:
- from test.common import *
-except ImportError:
- from .common import *
-
-
-class TestBoxFunctional(unittest.TestCase):
-
- @pytest.fixture(autouse=True)
- def temp_dir_cleanup(self):
- shutil.rmtree(tmp_dir, ignore_errors=True)
- try:
- os.makedirs(tmp_dir)
- except OSError:
- pass
- yield
- shutil.rmtree(tmp_dir, ignore_errors=True)
-
- def test_box(self):
- bx = Box(**test_dict)
- assert bx.key1 == test_dict['key1']
- assert dict(getattr(bx, 'Key 2')) == test_dict['Key 2']
- setattr(bx, 'TEST_KEY', 'VALUE')
- assert bx.TEST_KEY == 'VALUE'
- delattr(bx, 'TEST_KEY')
- assert 'TEST_KEY' not in bx.to_dict(), bx.to_dict()
- assert isinstance(bx['Key 2'].Key4, Box)
- assert "'key1': 'value1'" in str(bx)
- assert repr(bx).startswith("<Box:")
- bx2 = Box([((3, 4), "A"), ("_box_config", 'test')])
- assert bx2[(3, 4)] == "A"
- assert bx2['_box_config'] == 'test'
- bx3 = Box(a=4, conversion_box=False)
- setattr(bx3, 'key', 2)
- assert bx3.key == 2
- bx3.__setattr__("Test", 3)
- assert bx3.Test == 3
-
- def test_box_modify_at_depth(self):
- bx = Box(**test_dict)
- assert 'key1' in bx
- assert 'key2' not in bx
- bx['Key 2'].new_thing = "test"
- assert bx['Key 2'].new_thing == "test"
- bx['Key 2'].new_thing += "2"
- assert bx['Key 2'].new_thing == "test2"
- assert bx['Key 2'].to_dict()['new_thing'] == "test2"
- assert bx.to_dict()['Key 2']['new_thing'] == "test2"
- bx.__setattr__('key1', 1)
- assert bx['key1'] == 1
- bx.__delattr__('key1')
- assert 'key1' not in bx
-
- def test_error_box(self):
- bx = Box(**test_dict)
- with pytest.raises(AttributeError):
- getattr(bx, 'hello')
-
- def test_box_from_dict(self):
- ns = Box({"k1": "v1", "k2": {"k3": "v2"}})
- assert ns.k2.k3 == "v2"
-
- def test_box_from_bad_dict(self):
- with pytest.raises(ValueError):
- Box('{"k1": "v1", "k2": {"k3": "v2"}}')
-
- def test_basic_box(self):
- a = Box(one=1, two=2, three=3)
- b = Box({'one': 1, 'two': 2, 'three': 3})
- c = Box((zip(['one', 'two', 'three'], [1, 2, 3])))
- d = Box(([('two', 2), ('one', 1), ('three', 3)]))
- e = Box(({'three': 3, 'one': 1, 'two': 2}))
- assert a == b == c == d == e
-
- def test_config_box(self):
- g = {"b0": 'no',
- "b1": 'yes',
- "b2": 'True',
- "b3": 'false',
- "b4": True,
- "i0": '34',
- "f0": '5.5',
- "f1": '3.333',
- "l0": '4,5,6,7,8',
- "l1": '[2 3 4 5 6]'}
-
- cns = ConfigBox(bb=g)
- assert cns.bb.list("l1", spliter=" ") == ["2", "3", "4", "5", "6"]
- assert cns.bb.list("l0", mod=lambda x: int(x)) == [4, 5, 6, 7, 8]
- assert not cns.bb.bool("b0")
- assert cns.bb.bool("b1")
- assert cns.bb.bool("b2")
- assert not cns.bb.bool("b3")
- assert cns.bb.int("i0") == 34
- assert cns.bb.float("f0") == 5.5
- assert cns.bb.float("f1") == 3.333
- assert cns.bb.getboolean("b4"), cns.bb.getboolean("b4")
- assert cns.bb.getfloat("f0") == 5.5
- assert cns.bb.getint("i0") == 34
- assert cns.bb.getint("Hello!", 5) == 5
- assert cns.bb.getfloat("Wooo", 4.4) == 4.4
- assert cns.bb.getboolean("huh", True) is True
- assert cns.bb.list("Waaaa", [1]) == [1]
- assert repr(cns).startswith("<ConfigBox")
-
- def test_protected_box_methods(self):
- my_box = Box(a=3)
- with pytest.raises(AttributeError):
- my_box.to_dict = 'test'
-
- with pytest.raises(AttributeError):
- del my_box.to_json
-
- def test_bad_args(self):
- with pytest.raises(TypeError):
- Box('123', '432')
-
- def test_box_inits(self):
- a = Box({'data': 2, 'count': 5})
- b = Box(data=2, count=5)
- c = Box({'data': 2, 'count': 1}, count=5)
- d = Box([('data', 2), ('count', 5)])
- e = Box({'a': [{'item': 3}, {'item': []}]})
- assert e.a[1].item == []
- assert a == b == c == d
-
- def test_bad_inits(self):
- with pytest.raises(ValueError):
- Box("testing")
-
- with pytest.raises(ValueError):
- Box(22)
-
- with pytest.raises(TypeError):
- Box(22, 33)
-
- def test_create_subdicts(self):
- a = Box({'data': 2, 'count': 5})
- a.brand_new = {'subdata': 1}
- assert a.brand_new.subdata == 1
- a.new_list = [{'sub_list_item': 1}]
- assert a.new_list[0].sub_list_item == 1
- assert isinstance(a.new_list, BoxList)
- a.new_list2 = [[{'sub_list_item': 2}]]
- assert a.new_list2[0][0].sub_list_item == 2
- b = a.to_dict()
- assert not isinstance(b['new_list'], BoxList)
-
- def test_to_json(self):
- a = Box(test_dict)
- assert json.loads(a.to_json(indent=0)) == test_dict
-
- a.to_json(tmp_json_file)
- with open(tmp_json_file) as f:
- data = json.load(f)
- assert data == test_dict
-
- def test_to_yaml(self):
- a = Box(test_dict)
- assert yaml.load(a.to_yaml(), Loader=yaml.SafeLoader) == test_dict
-
- def test_to_yaml_file(self):
- a = Box(test_dict)
- a.to_yaml(tmp_yaml_file)
- with open(tmp_yaml_file) as f:
- data = yaml.load(f, Loader=yaml.SafeLoader)
- assert data == test_dict
-
- def test_boxlist(self):
- new_list = BoxList({'item': x} for x in range(0, 10))
- new_list.extend([{'item': 22}])
- assert new_list[-1].item == 22
- new_list.append([{'bad_item': 33}])
- assert new_list[-1][0].bad_item == 33
- assert repr(new_list).startswith("<BoxList:")
- for x in new_list.to_list():
- assert not isinstance(x, (BoxList, Box))
- new_list.insert(0, {'test': 5})
- new_list.insert(1, ['a', 'b'])
- new_list.append('x')
- assert new_list[0].test == 5
- assert isinstance(str(new_list), str)
- assert isinstance(new_list[1], BoxList)
- assert not isinstance(new_list.to_list(), BoxList)
-
- def test_dir(self):
- a = Box(test_dict, camel_killer_box=True)
- assert 'key1' in dir(a)
- assert 'not$allowed' not in dir(a)
- assert 'Key4' in a['Key 2']
- for item in ('to_yaml', 'to_dict', 'to_json'):
- assert item in dir(a)
-
- assert a.big_camel == 'hi'
- assert 'big_camel' in dir(a)
-
- b = ConfigBox(test_dict)
-
- for item in ('to_yaml', 'to_dict', 'to_json', 'int', 'list', 'float'):
- assert item in dir(b)
-
- def test_update(self):
- a = Box(test_dict)
- a.grand = 1000
- a.update({'key1': {'new': 5}, 'Key 2': {"add_key": 6},
- 'lister': ['a']})
- a.update([('asdf', 'fdsa')])
- a.update(testkey=66)
- a.update({'items': {'test': 'pme'}})
- a.update({'key1': {'gg': 4}})
- b = Box()
- b.update(item=1)
-
- assert a.grand == 1000
- assert a['grand'] == 1000
- assert isinstance(a['items'], Box)
- assert a['items'].test == 'pme'
- assert a.key1.new == 5
- assert a['Key 2'].add_key == 6
- assert isinstance(a.key1, Box)
- assert isinstance(a.lister, BoxList)
- assert a.asdf == 'fdsa'
- assert a.testkey == 66
- assert a.key1.new == 5 # On regular dict update this shouldn't happen
- assert a.key1.gg == 4
-
- c = Box(box_intact_types=[list])
- c.a = [1, 2]
- c.update({'b': [3, 4]})
-
- assert c.a == [1, 2]
- assert isinstance(c.b, list)
- assert not isinstance(c.b, BoxList)
-
- def test_auto_attr(self):
- a = Box(test_dict, default_box=True)
- assert a.a.a.a.a == Box()
- a.b.b = 4
- assert a.b.b == 4
-
- def test_set_default(self):
- a = Box(test_dict)
-
- new = a.setdefault("key3", {'item': 2})
- new_list = a.setdefault("lister", [{'gah': 7}])
- assert a.setdefault("key1", False) == 'value1'
-
- assert new == Box(item=2)
- assert new_list == BoxList([{'gah': 7}])
- assert a.key3.item == 2
- assert a.lister[0].gah == 7
-
- # default_box propagates after a setdefault and list object
- a = Box(default_box=True)
- a.b.c.setdefault('d', [])
- a.b.c.d.append({})
- a.b.c.d[0].e.f = 1
-
- assert a.b.c.d[0].e.f == 1
-
- # without default_box we would get an error
- a = Box()
- a.setdefault('b', [])
- a.b.append({})
- with self.assertRaises(BoxKeyError):
- a.b[0].c.d = 1
-
- a = Box()
- a.setdefault('b', {})
- with self.assertRaises(BoxKeyError):
- a.b.c.d = 1
-
- a = Box(default_box=True)
- a.setdefault('b', {})
- a.b.c.d.e.f = 1
- assert a.b.c.d.e.f == 1
-
- def test_from_json_file(self):
- bx = Box.from_json(filename=data_json_file)
- assert isinstance(bx, Box)
- assert bx.widget.window.height == 500
-
- def test_from_yaml_file(self):
- bx = Box.from_yaml(filename=data_yaml_file)
- assert isinstance(bx, Box)
- assert bx.total == 4443.52
-
- def test_from_json(self):
- bx = Box.from_json(json.dumps(test_dict))
- assert isinstance(bx, Box)
- assert bx.key1 == 'value1'
-
- def test_from_yaml(self):
- bx = Box.from_yaml(yaml.dump(test_dict),
- conversion_box=False,
- default_box=True)
- assert isinstance(bx, Box)
- assert bx.key1 == 'value1'
- assert bx.Key_2 == Box()
-
- def test_bad_from_json(self):
- with pytest.raises(BoxError) as err:
- Box.from_json()
-
- with pytest.raises(BoxError) as err2:
- Box.from_json(json_string="[1]")
-
- def test_bad_from_yaml(self):
- with pytest.raises(BoxError) as err:
- Box.from_yaml()
-
- with pytest.raises(BoxError) as err2:
- Box.from_yaml('lol')
-
- def test_conversion_box(self):
- bx = Box(extended_test_dict, conversion_box=True)
- assert bx.Key_2.Key_3 == "Value 3"
- assert bx.x3 == 'howdy'
- assert bx.xnot == 'true'
- assert bx.x3_4 == 'test'
- with pytest.raises(AttributeError):
- getattr(bx, "(3, 4)")
-
- def test_frozen(self):
- bx = Box(extended_test_dict, frozen_box=True)
-
- assert isinstance(bx.alist, tuple)
- assert bx.alist[0] == {'a': 1}
- with pytest.raises(BoxError):
- bx.new = 3
-
- with pytest.raises(BoxError):
- bx['new'] = 3
-
- with pytest.raises(BoxError):
- del bx['not']
-
- with pytest.raises(BoxError):
- delattr(bx, "key1")
-
- with pytest.raises(TypeError):
- hash(bx)
-
- bx2 = Box(test_dict)
- with pytest.raises(TypeError):
- hash(bx2)
-
- bx3 = Box(test_dict, frozen_box=True)
-
- assert hash(bx3)
-
- def test_hashing(self):
- bx1 = Box(t=3, g=4, frozen_box=True)
- bx2 = Box(g=4, t=3, frozen_box=True)
- assert hash(bx1) == hash(bx2)
-
- bl1 = BoxList([1, 2, 3, 4], frozen_box=True)
- bl2 = BoxList([1, 2, 3, 4], frozen_box=True)
- bl3 = BoxList([2, 1, 3, 4], frozen_box=True)
- assert hash(bl2) == hash(bl1)
- assert hash(bl3) != hash(bl2)
-
- with pytest.raises(TypeError):
- hash(BoxList([1, 2, 3]))
-
- def test_frozen_list(self):
- bl = BoxList([5, 4, 3], frozen_box=True)
- with pytest.raises(BoxError):
- bl.pop(1)
- with pytest.raises(BoxError):
- bl.remove(4)
- with pytest.raises(BoxError):
- bl.sort()
- with pytest.raises(BoxError):
- bl.reverse()
- with pytest.raises(BoxError):
- bl.append('test')
- with pytest.raises(BoxError):
- bl.extend([4])
- with pytest.raises(BoxError):
- del bl[0]
- with pytest.raises(BoxError):
- bl[0] = 5
- bl2 = BoxList([5, 4, 3])
- del bl2[0]
- assert bl2[0] == 4
- bl2[1] = 4
- assert bl2[1] == 4
-
- def test_config(self):
- bx = Box(extended_test_dict)
- assert bx['_box_config'] is True
- assert isinstance(bx._box_config, dict)
- with pytest.raises(BoxError):
- delattr(bx, '_box_config')
- bx._box_config
-
- def test_default_box(self):
- bx = Box(test_dict, default_box=True, default_box_attr={'hi': 'there'})
- assert bx.key_88 == {'hi': 'there'}
-
- bx2 = Box(test_dict, default_box=True, default_box_attr=Box)
- assert isinstance(bx2.key_77, Box)
-
- bx3 = Box(default_box=True, default_box_attr=3)
- assert bx3.hello == 3
-
- bx4 = Box(default_box=True, default_box_attr=ConfigBox)
- assert isinstance(bx4.bbbbb, ConfigBox)
-
- def test_camel_killer_box(self):
- td = extended_test_dict.copy()
- td['CamelCase'] = 'Item'
- td['321CamelCaseFever!'] = 'Safe'
-
- kill_box = Box(td, camel_killer_box=True, conversion_box=False)
- assert kill_box.camel_case == 'Item'
- assert kill_box['321CamelCaseFever!'] == 'Safe'
-
- con_kill_box = Box(td, conversion_box=True, camel_killer_box=True)
- assert con_kill_box.camel_case == 'Item'
- assert con_kill_box.x321_camel_case_fever == 'Safe'
-
- def test_default_and_camel_killer_box(self):
- td = extended_test_dict.copy()
- td['CamelCase'] = 'Item'
- killer_default_box = Box(td, camel_killer_box=True, default_box=True)
- assert killer_default_box.camel_case == 'Item'
- assert killer_default_box.CamelCase == 'Item'
- assert isinstance(killer_default_box.does_not_exist, Box)
- assert isinstance(killer_default_box['does_not_exist'], Box)
-
- def test_property_box(self):
- td = test_dict.copy()
- td['inner'] = {'CamelCase': 'Item'}
-
- pbox = SBox(td, camel_killer_box=True)
- assert isinstance(pbox.inner, SBox)
- assert pbox.inner.camel_case == 'Item'
- assert json.loads(pbox.json)['inner']['CamelCase'] == 'Item'
- test_item = yaml.load(pbox.yaml, Loader=yaml.SafeLoader)
- assert test_item['inner']['CamelCase'] == 'Item'
- assert repr(pbox['inner']).startswith('<ShorthandBox')
- assert not isinstance(pbox.dict, Box)
- assert pbox.dict['inner']['CamelCase'] == 'Item'
-
- def test_box_list_to_json(self):
- bl = BoxList([{'item': 1, 'CamelBad': 2}])
- assert json.loads(bl.to_json())[0]['item'] == 1
-
- def test_box_list_from_json(self):
- alist = [{'item': 1}, {'CamelBad': 2}]
- json_list = json.dumps(alist)
- bl = BoxList.from_json(json_list, camel_killer_box=True)
- assert bl[0].item == 1
- assert bl[1].camel_bad == 2
-
- with pytest.raises(BoxError):
- BoxList.from_json(json.dumps({'a': 2}))
-
- def test_box_list_to_yaml(self):
- bl = BoxList([{'item': 1, 'CamelBad': 2}])
- assert yaml.load(bl.to_yaml(), Loader=yaml.SafeLoader)[0]['item'] == 1
-
- def test_box_list_from_yaml(self):
- alist = [{'item': 1}, {'CamelBad': 2}]
- yaml_list = yaml.dump(alist)
- bl = BoxList.from_yaml(yaml_list, camel_killer_box=True)
- assert bl[0].item == 1
- assert bl[1].camel_bad == 2
-
- with pytest.raises(BoxError):
- BoxList.from_yaml(yaml.dump({'a': 2}))
-
- def test_boxlist_box_it_up(self):
- bxl = BoxList([extended_test_dict])
- bxl.box_it_up()
- assert "Key 3" in bxl[0].Key_2._box_config['__converted']
-
- def test_box_modify_tuples(self):
- bx = Box(extended_test_dict, modify_tuples_box=True)
- assert bx.tuples_galore[0].item == 3
- assert isinstance(bx.tuples_galore[0], Box)
- assert isinstance(bx.tuples_galore[1], tuple)
-
- def test_box_set_attribs(self):
- bx = Box(extended_test_dict, conversion_box=False,
- camel_killer_box=True)
- bx.camel_case = {'new': 'item'}
- assert bx['CamelCase'] == Box(new='item')
-
- bx2 = Box(extended_test_dict)
- bx2.Key_2 = 4
- assert bx2["Key 2"] == 4
-
- def test_functional_hearthstone_data(self):
- hearth = Box.from_json(filename=data_hearthstone,
- conversion_box=True,
- camel_killer_box=True,
- default_box=False)
- assert hearth.the_jade_lotus
-
- with pytest.raises(AttributeError):
- hearth._bad_value
-
- with pytest.raises(AttributeError):
- hearth.the_jade_lotus._bad_value
-
- base_config = hearth._Box__box_config()
- jade_config = hearth.the_jade_lotus._Box__box_config()
-
- assert base_config == jade_config, "{} != {}".format(base_config,
- jade_config)
-
- def test_functional_spaceballs(self):
- my_box = Box(movie_data)
-
- my_box.movies.Spaceballs.Stars.append(
- {"name": "Bill Pullman", "imdb": "nm0000597",
- "role": "Lone Starr"})
- assert my_box.movies.Spaceballs.Stars[-1].role == "Lone Starr"
- assert my_box.movies.Robin_Hood_Men_in_Tights.length == 104
- my_box.movies.Robin_Hood_Men_in_Tights.Stars.pop(0)
- assert my_box.movies.Robin_Hood_Men_in_Tights.Stars[
- 0].name == "Richard Lewis"
-
- def test_circular_references(self):
- circular_dict = {}
- circular_dict['a'] = circular_dict
- bx = Box(circular_dict, box_it_up=True)
- assert bx.a.a == bx.a
- circular_dict_2 = bx.a.a.a.to_dict()
- assert str(circular_dict_2) == "{'a': {...}}"
-
- bx2 = Box(circular_dict, k=circular_dict)
- assert bx2.k == bx2.a
-
- with pytest.raises(ValueError):
- bx.to_json()
-
- circular_list = []
- circular_list.append(circular_list)
- bl = BoxList(circular_list)
- assert bl == bl[0]
- assert isinstance(bl[0], BoxList)
- circular_list_2 = bl.to_list()
- assert circular_list_2 == circular_list_2[0]
- assert isinstance(circular_list_2, list)
-
- def test_to_multiline(self):
- a = BoxList([Box(a=1), Box(b=2), Box(three=5)])
-
- a.to_json(tmp_json_file, multiline=True)
- count = 0
- with open(tmp_json_file) as f:
- for line in f:
- assert isinstance(json.loads(line), dict)
- count += 1
- assert count == 3
-
- def test_from_multiline(self):
- content = '{"a": 2}\n{"b": 3}\r\n \n'
- with open(tmp_json_file, 'w') as f:
- f.write(content)
-
- a = BoxList.from_json(filename=tmp_json_file, multiline=True)
- assert a[1].b == 3
-
- def test_duplicate_errors(self):
- with pytest.raises(BoxError) as err:
- Box({"?a": 1, "!a": 3}, box_duplicates="error")
-
- Box({"?a": 1, "!a": 3}, box_duplicates="ignore")
-
- with pytest.warns(UserWarning) as warning:
- Box({"?a": 1, "!a": 3}, box_duplicates="warn")
- assert warning[0].message.args[0].startswith("Duplicate")
-
- my_box = Box({"?a": 1}, box_duplicates="error")
- with pytest.raises(BoxError):
- my_box['^a'] = 3
-
- def test_copy(self):
- my_box = Box(movie_data, camel_killer_box=True)
- my_box.aB = 1
- bb = my_box.copy()
- assert my_box == bb
- assert isinstance(bb, Box)
-
- aa = copy.deepcopy(my_box)
- assert my_box == aa
- assert my_box.a_b == 1
- assert isinstance(aa, Box)
-
- cc = my_box.__copy__()
- assert my_box == cc
- assert isinstance(cc, Box)
-
- dd = BoxList([my_box])
- assert dd == copy.copy(dd)
- assert isinstance(copy.copy(dd), BoxList)
-
- def test_deepcopy_of_frozen_box(self):
- my_box = Box(data={'a': movie_data,
- 'b': Box(movie_data, frozen_box=True)},
- frozen_box=True)
- aa = copy.deepcopy(my_box)
- assert my_box == aa
- assert id(my_box) != id(aa)
- assert isinstance(aa, Box)
-
- def test_custom_key_errors(self):
- my_box = Box()
-
- with pytest.raises(BoxKeyError):
- my_box.g
-
- with pytest.raises(AttributeError):
- my_box.g
-
- with pytest.raises(KeyError):
- my_box['g']
-
- with pytest.raises(BoxKeyError):
- my_box['g']
-
- with pytest.raises(BoxError):
- my_box['g']
-
- def test_pickle(self):
- pic_file = os.path.join(tmp_dir, 'test.p')
- pic2_file = os.path.join(tmp_dir, 'test.p2')
- bb = Box(movie_data, conversion_box=False)
- with open(pic_file, 'wb') as pf:
- pickle.dump(bb, pf)
- with open(pic_file, 'rb') as pf:
- loaded = pickle.load(pf)
- assert bb == loaded
- assert loaded._box_config['conversion_box'] is False
-
- ll = [[Box({'a': 'b'}, ordered_box=True)], [[{'c': 'g'}]]]
- bx = BoxList(ll)
- with open(pic2_file, 'wb') as pf:
- pickle.dump(bx, pf)
- with open(pic2_file, 'rb') as pf:
- loaded2 = pickle.load(pf)
- assert bx == loaded2
- loaded2.box_options = bx.box_options
-
- def test_pickle_default_box(self):
- bb = Box(default_box=True)
- loaded = pickle.loads(pickle.dumps(bb))
- assert bb == loaded
-
- def test_conversion_dup_only(self):
- with pytest.raises(BoxError):
- Box(movie_data, conversion_box=False, box_duplicates='error')
-
- def test_values(self):
- b = Box()
- b.foo = {}
- assert isinstance(list(b.values())[0], Box)
- c = Box()
- c.foohoo = []
- assert isinstance(list(c.values())[0], BoxList)
- d = Box(movie_data)
- assert len(movie_data["movies"].values()) == len(d.movies.values())
-
- def test_items(self):
- b = Box()
- b.foo = {}
- assert isinstance(list(b.items())[0][1], Box)
- c = Box()
- c.foohoo = []
- assert isinstance(list(c.items())[0][1], BoxList)
- d = Box(movie_data)
- assert len(movie_data["movies"].items()) == len(d.movies.items())
-
- def test_get(self):
- bx = Box()
- bx["c"] = {}
- assert isinstance(bx.get("c"), Box)
- assert isinstance(bx.get("b", {}), Box)
- assert "a" in bx.get("a", Box(a=1, conversion_box=False))
- assert isinstance(bx.get("a", [1, 2]), BoxList)
-
- def test_get_default_box(self):
- bx = Box(default_box=True)
- assert bx.get('test', 4) == 4
- assert isinstance(bx.get('a'), Box)
-
- def test_is_in(self):
- bx = Box()
- dbx = Box(default_box=True)
- assert "a" not in bx
- assert "a" not in dbx
- bx["b"] = 1
- dbx["b"] = {}
- assert "b" in bx
- assert "b" in dbx
-
- def test_through_queue(self):
- my_box = Box(a=4, c={"d": 3})
-
- queue = Queue()
- queue.put(my_box)
-
- p = Process(target=mp_queue_test, args=(queue,))
- p.start()
- p.join()
-
- assert queue.get(timeout=1)
-
- def test_update_with_integer(self):
- bx = Box()
- bx[1] = 4
- assert bx[1] == 4
- bx.update({1: 2})
- assert bx[1] == 2
-
- def test_get_box_config(self):
- bx = Box()
- bx_config = bx.__getattr__('_box_config')
- assert bx_config
- with pytest.raises(BoxKeyError):
- bx['_box_config']
-
- def test_ordered_box(self):
- bx = Box(h=1, ordered_box=True)
- bx.a = 1
- bx.c = 4
- bx['g'] = 7
- bx.d = 2
- assert bx.keys() == ['h', 'a', 'c', 'g', 'd']
- assert list(bx.__iter__()) == ['h', 'a', 'c', 'g', 'd']
- assert list(reversed(bx)) == ['d', 'g', 'c', 'a', 'h']
- del bx.a
- bx.pop('c')
- bx.__delattr__('g')
- assert bx.keys() == ['h', 'd']
-
- def test_intact_types_dict(self):
- from collections import OrderedDict
- bx = Box(a=OrderedDict([('y', 1), ('x', 2)]))
- assert isinstance(bx.a, Box)
- assert not isinstance(bx.a, OrderedDict)
- bx = Box(a=OrderedDict([('y', 1), ('x', 2)]),
- box_intact_types=[OrderedDict])
- assert isinstance(bx.a, OrderedDict)
- assert not isinstance(bx.a, Box)
-
- def test_intact_types_list(self):
- class MyList(list):
- pass
-
- bl = BoxList([[1, 2], MyList([3, 4])], box_intact_types=(MyList,))
- assert isinstance(bl[0], BoxList)
-
- def test_pop(self):
- bx = Box(a=4, c={"d": 3}, b={"h": {"y": 2}})
- assert bx.pop('a') == 4
- assert bx.pop('b').h.y == 2
- with pytest.raises(BoxKeyError):
- bx.pop('b')
- assert bx.pop('a', None) is None
- assert bx.pop('a', True) is True
- assert bx == {'c': {"d": 3}}
- with pytest.raises(BoxError):
- bx.pop(1, 2, 3)
- assert bx.pop('c', True) is not True
-
- def test_pop_items(self):
- bx = Box(a=4)
- assert bx.popitem() == ('a', 4)
- with pytest.raises(BoxKeyError):
- assert bx.popitem()
-
- def test_iter(self):
- bx = Box(ordered_box=True)
- bx.a = 1
- bx.c = 2
- assert list(bx.__iter__()) == ['a', 'c']
-
- def test_revered(self):
- bx = Box(ordered_box=True)
- bx.a = 1
- bx.c = 2
- assert list(reversed(bx)) == ['c', 'a']
-
- def test_clear(self):
- bx = Box(ordered_box=True)
- bx.a = 1
- bx.c = 4
- bx['g'] = 7
- bx.d = 2
- assert bx.keys() == ['a', 'c', 'g', 'd']
- bx.clear()
- assert bx == {}
- assert bx.keys() == []
-
- def test_bad_recursive(self):
- b = Box()
- bl = b.setdefault("l", [])
- bl.append(["foo"])
- assert bl == [['foo']], bl
-
- def test_inheritance_copy(self):
-
- class Box2(Box):
- pass
-
- class SBox2(SBox):
- pass
-
- class ConfigBox2(ConfigBox):
- pass
-
- b = Box2(a=1)
- c = b.copy()
- assert c == b
- assert isinstance(c, Box)
- c = b.__copy__()
- assert c == b
- assert isinstance(c, Box)
-
- d = SBox2(a=1)
- e = d.copy()
- assert e == d
- assert isinstance(e, SBox)
- e = d.__copy__()
- assert e == d
- assert isinstance(e, SBox)
-
- f = ConfigBox2(a=1)
- g = f.copy()
- assert g == f
- assert isinstance(g, ConfigBox)
- g = f.__copy__()
- assert g == f
- assert isinstance(g, ConfigBox)
-
-
-class TestBoxObject:
-
- @pytest.mark.parametrize('wrapped', python_example_objects)
- def test_box_object_generic(self, wrapped):
- b = BoxObject(wrapped)
- assert b == wrapped
- assert not (b is wrapped)
- assert isinstance(b, BoxObject)
- assert isinstance(b, type(wrapped))
- b.box_key = 'secret_word'
- assert b.box_key == 'secret_word'
- assert 'box_key' in b.__dict__
- assert isinstance(b.__dict__, Box)
- assert b.__dict__ != getattr(b.__wrapped__, '__dict__', None)
- with pytest.raises(AttributeError):
- b.foo
- if hasattr(b.__wrapped__, 'b'):
- b.b = 1
- assert b.__wrapped__.b == 1
-
- @pytest.mark.parametrize('wrapped', python_example_objects)
- def test_box_object_deletion(self, wrapped):
- b = BoxObject(wrapped)
- with pytest.raises(TypeError):
- b.__dict__ = 0
- del b.__dict__
- assert b.__dict__ == getattr(b.__wrapped__, '__dict__', {})
- with pytest.raises(AttributeError):
- del b.foo
- if hasattr(b.__wrapped__, 'a'):
- del b.a
- if not hasattr(b.__wrapped__, 'b'):
- with pytest.raises(AttributeError):
- del b.b
-
- def test_box_object_attributes(self):
- b = BoxObject(test_dict, **movie_data)
- assert b == test_dict
- assert not (b is test_dict)
- assert b.__dict__ == movie_data
- assert isinstance(b.__dict__, Box)
- assert b.__dict__ != getattr(b.__wrapped__, '__dict__', None)
- for k, v in movie_data.items():
- assert getattr(b, k) == v
- tagged = k + '_b'
- setattr(b, tagged, [v])
- assert getattr(b, tagged) == [v]
- setattr(b, k, getattr(b, tagged))
- assert getattr(b, k) == [v]
- for k, v in test_dict.items():
- assert k in b
- assert b[k] == v
-
- def test_box_object_call(self):
- def f(*args, **kwargs):
- return args, kwargs
-
- b = BoxObject(f)
- assert b(list(test_dict),
- **movie_data) == f(list(test_dict), **movie_data)
-
- def test_box_object_double_args(self):
- with pytest.raises(TypeError):
- BoxObject(function_example,
- zip([1, 2, 3], [4, 5, 6]),
- **movie_data)
-
-
-def mp_queue_test(q):
- bx = q.get(timeout=1)
- try:
- assert isinstance(bx, Box)
- assert bx.a == 4
- except AssertionError:
- q.put(False)
- else:
- q.put(True)
diff --git a/test/test_sbox.py b/test/test_sbox.py
new file mode 100644
index 0000000..ea0d06a
--- /dev/null
+++ b/test/test_sbox.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+import json
+from test.common import test_dict
+
+import pytest
+
+from ruamel.yaml import YAML
+
+from box import Box, SBox
+
+
+class TestSBox:
+ def test_property_box(self):
+ td = test_dict.copy()
+ td["inner"] = {"CamelCase": "Item"}
+
+ pbox = SBox(td, camel_killer_box=True)
+ assert isinstance(pbox.inner, SBox)
+ assert pbox.inner.camel_case == "Item"
+ assert json.loads(pbox.json)["inner"]["camel_case"] == "Item"
+ yaml = YAML()
+ test_item = yaml.load(pbox.yaml)
+ assert test_item["inner"]["camel_case"] == "Item"
+ assert repr(pbox["inner"]).startswith("SBox(")
+ assert not isinstance(pbox.dict, Box)
+ assert pbox.dict["inner"]["camel_case"] == "Item"
+ assert pbox.toml.startswith('key1 = "value1"')
diff --git a/test/test_unittests_box.py b/test/test_unittests_box.py
deleted file mode 100644
index 8dbf6e0..0000000
--- a/test/test_unittests_box.py
+++ /dev/null
@@ -1,62 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: UTF-8 -*-
-from __future__ import absolute_import
-
-try:
- from test.common import *
-except ImportError:
- from .common import *
-
-
-class TestBoxUnit(unittest.TestCase):
- def setUp(self):
- shutil.rmtree(tmp_dir, ignore_errors=True)
- try:
- os.mkdir(tmp_dir)
- except OSError:
- pass
-
- def tearDown(self):
- shutil.rmtree(tmp_dir, ignore_errors=True)
-
- def test_safe_attrs(self):
- assert box._safe_attr("BAD!KEY!1", camel_killer=False) == "BAD_KEY_1"
- assert box._safe_attr("BAD!KEY!2", camel_killer=True) == "bad_key_2"
- assert box._safe_attr((5, 6, 7), camel_killer=False) == "x5_6_7"
- assert box._safe_attr(356, camel_killer=False) == "x356"
-
- def test_camel_killer(self):
- assert box._camel_killer("CamelCase") == "camel_case"
- assert box._camel_killer("Terrible321KeyA") == "terrible321_key_a"
-
- def test_safe_key(self):
- assert box._safe_key(("wer", "ah", 3)) == "('wer', 'ah', 3)"
-
- def test_recursive_tuples(self):
- out = box._recursive_tuples(({'test': 'a'},
- ({'second': 'b'},
- {'third': 'c'}, ('fourth',))),
- dict, recreate_tuples=True)
- assert isinstance(out, tuple)
- assert isinstance(out[0], dict)
- assert out[0] == {'test': 'a'}
- assert isinstance(out[1], tuple)
- assert isinstance(out[1][2], tuple)
- assert out[1][0] == {'second': 'b'}
-
- def test_to_json(self):
- m_file = os.path.join(tmp_dir, "movie_data")
- movie_string = box._to_json(movie_data)
- assert "Rick Moranis" in movie_string
- box._to_json(movie_data, filename=m_file)
- assert "Rick Moranis" in open(m_file).read()
- assert json.load(open(m_file)) == json.loads(movie_string)
-
- def test_to_yaml(self):
- m_file = os.path.join(tmp_dir, "movie_data")
- movie_string = box._to_yaml(movie_data)
- assert "Rick Moranis" in movie_string
- box._to_yaml(movie_data, filename=m_file)
- assert "Rick Moranis" in open(m_file).read()
- assert yaml.load(open(m_file), Loader=yaml.SafeLoader) == yaml.load(
- movie_string, Loader=yaml.SafeLoader)