diff options
| author | Colin Watson <cjwatson@debian.org> | 2025-02-24 13:08:37 +0000 |
|---|---|---|
| committer | git-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com> | 2025-02-24 16:43:26 +0000 |
| commit | 2ba50cddcc72ba83e95358f1a5fdfc06efd1654d (patch) | |
| tree | 91dd65a240e8c7acf6c4c87a901e694d2e252500 | |
| parent | 225d8213a7da9cbc1681b74853238a27c4838ecd (diff) | |
7.3.2-1 (patches unapplied)import/7.3.2-1ubuntu/questing-develubuntu/questingubuntu/plucky-proposedubuntu/plucky-develubuntu/pluckydebian/sid
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).
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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -1,4 +1,4 @@ -[egg_info]
-tag_build =
-tag_date = 0
-
+[egg_info] +tag_build = +tag_date = 0 + @@ -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) |
