././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737054602.3072853 python_box-7.3.2/0000755000175100001660000000000014742254612013326 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/AUTHORS.rst0000644000175100001660000000454414742254572015221 0ustar00runnerdockerBox is written and maintained by Chris Griffith chris@cdgriffith.com. A big thank you to everyone that has helped! From PRs to suggestions and bug reporting, all input is greatly appreciated! Code contributions: - Alexandre Decan (AlexandreDecan) - dhilipsiva (dhilipsiva) - MAA (FooBarQuaxx) - Jiang Chen (criver) - Matan Rosenberg (matan129) - Matt Wisniewski (polishmatt) - Martijn Pieters (mjpieters) - (sdementen) - Brandon Gomes (bhgomes) - Stretch (str3tch) - (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: - JiuLi Gao (gaojiuli) - Jürgen Hermann (jhermann) - tilkau [reddit] - Jumpy89 [reddit] - can_dry [reddit] - spidyfan21 [reddit] - Casey Havenor (chavenor) - wim glenn (wimglenn) - Vishwas B Sharma (csurfer) - John Benediktsson (mrjbq7) - delirious_lettuce [reddit] - Justin Iso (justiniso) - (crazyplum) - Christopher Toth (ctoth) - RickS (rshap91) - askvictor [Hacker News] - wouter bolsterlee (wbolster) - Mickaël Thomas (mickael9) - (pwwang) - (richieadler) - V.Anh Tran (tranvietanh1991) - (ipcoder) - (cebaa) - (deluxghost) - Nikolay Stanishev (nikolaystanishev) - Craig Quiter (crizCraig) - 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/CHANGES.rst0000644000175100001660000004004214742254572015135 0ustar00runnerdockerChangelog ========= 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) * Fixing update to operate the same way as a normal dictionary (thanks to Craig Quiter) * 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) * Fixing update to not handle protected words correctly (thanks to deluxghost) * 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` * Fixing ordering of camel_case_killer vs default_box (thanks to Matan Rosenberg) * 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 * Adding custom error for missing keys * Changing that for this 3.x release, 2.6 support exists * Fixing that a recursion loop could occur if `_box_config` was somehow removed * 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` * Adding `frozen_box` option * Adding `BoxError` exception for custom errors * Adding `conversion_box` to automatically try to find matching attributes * Adding `camel_killer_box` that converts CamelCaseKeys to camel_case_keys * Adding `SBox` that has `json` and `yaml` properties that map to default `to_json()` and `to_yaml()` * Adding `box_it_up` property that will make sure all boxes are created and populated like previous version * Adding `modify_tuples_box` option to recreate tuples with Boxes instead of dicts * Adding `to_json` and `to_yaml` for `BoxList` * Changing how the Box object works, to conversion on extraction * Removing `__call__` for compatibly with django and to make more like dict object * Removing support for python 2.6 * Removing `LightBox` * 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 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 * Removing `tree_view` as near same can be seen with YAML Version 1.0.0 ------------- * Initial release, copy from `reusables.Namespace` * Original creation, 2\13\2014 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/LICENSE0000644000175100001660000000206414742254572014342 0ustar00runnerdockerMIT License 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 in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/MANIFEST.in0000644000175100001660000000022214742254572015065 0ustar00runnerdockerinclude LICENSE include AUTHORS.rst include CHANGES.rst include box/py.typed include box/*.c include box/*.so include box/*.pyd include box/*.pyi ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737054602.3072853 python_box-7.3.2/PKG-INFO0000644000175100001660000001755714742254612014442 0ustar00runnerdockerMetadata-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 `_ 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 `_ 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 `_ 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 `_ 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 `_. 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 `_ to customize it for your needs, as well as handy `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 `_ 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` 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/README.rst0000644000175100001660000001362614742254572015032 0ustar00runnerdocker|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 `_ 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 `_ 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 `_ 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 `_ 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 `_. 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 `_ to customize it for your needs, as well as handy `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 `_ 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` 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737054602.3032851 python_box-7.3.2/box/0000755000175100001660000000000014742254612014116 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/__init__.py0000644000175100001660000000076714742254572016246 0ustar00runnerdocker#!/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", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/box.py0000644000175100001660000013546014742254572015276 0ustar00runnerdocker#!/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') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/box.pyi0000644000175100001660000001070614742254572015442 0ustar00runnerdockerfrom _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: ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/box_list.py0000644000175100001660000004234014742254572016323 0ustar00runnerdocker#!/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)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/box_list.pyi0000644000175100001660000000557714742254572016507 0ustar00runnerdockerimport 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: ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/config_box.py0000644000175100001660000000744214742254572016621 0ustar00runnerdocker#!/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()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/config_box.pyi0000644000175100001660000000144514742254572016767 0ustar00runnerdockerfrom 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": ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/converters.py0000644000175100001660000002542314742254572016675 0ustar00runnerdocker#!/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] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/converters.pyi0000644000175100001660000000353014742254572017041 0ustar00runnerdockerfrom 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, ): ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/exceptions.py0000644000175100001660000000067614742254572016667 0ustar00runnerdocker#!/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""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/exceptions.pyi0000644000175100001660000000033114742254572017024 0ustar00runnerdockerclass BoxError(Exception): ... class BoxKeyError(BoxError, KeyError, AttributeError): ... class BoxTypeError(BoxError, TypeError): ... class BoxValueError(BoxError, ValueError): ... class BoxWarning(UserWarning): ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/from_file.py0000644000175100001660000001163014742254572016440 0ustar00runnerdocker#!/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}") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/from_file.pyi0000644000175100001660000000062114742254572016607 0ustar00runnerdockerfrom 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]: ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/py.typed0000644000175100001660000000000014742254572015610 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/shorthand_box.py0000644000175100001660000000267514742254572017351 0ustar00runnerdocker#!/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})" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/box/shorthand_box.pyi0000644000175100001660000000053614742254572017514 0ustar00runnerdockerfrom 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): ... ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737054602.3052852 python_box-7.3.2/python_box.egg-info/0000755000175100001660000000000014742254612017211 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054602.0 python_box-7.3.2/python_box.egg-info/PKG-INFO0000644000175100001660000001755714742254612020325 0ustar00runnerdockerMetadata-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 `_ 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 `_ 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 `_ 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 `_ 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 `_. 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 `_ to customize it for your needs, as well as handy `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 `_ 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` 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054602.0 python_box-7.3.2/python_box.egg-info/SOURCES.txt0000644000175100001660000000117214742254612021076 0ustar00runnerdockerAUTHORS.rst CHANGES.rst LICENSE MANIFEST.in README.rst 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_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././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054602.0 python_box-7.3.2/python_box.egg-info/dependency_links.txt0000644000175100001660000000000114742254612023257 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054602.0 python_box-7.3.2/python_box.egg-info/requires.txt0000644000175100001660000000031214742254612021605 0ustar00runnerdocker [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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054602.0 python_box-7.3.2/python_box.egg-info/top_level.txt0000644000175100001660000000000414742254612021735 0ustar00runnerdockerbox ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737054602.3072853 python_box-7.3.2/setup.cfg0000644000175100001660000000004614742254612015147 0ustar00runnerdocker[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/setup.py0000644000175100001660000000511314742254572015045 0ustar00runnerdocker#!/usr/bin/env python # -*- coding: utf-8 -*- # 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 from setuptools import setup root = os.path.abspath(os.path.dirname(__file__)) 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}, ) 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"], install_requires=[], author_email="chris@cdgriffith.com", description="Advanced Python dictionaries with dot notation access", long_description=long_description, 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", classifiers=[ "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={ "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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1737054602.3052852 python_box-7.3.2/test/0000755000175100001660000000000014742254612014305 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/test/test_box.py0000644000175100001660000013433314742254572016522 0ustar00runnerdocker#!/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"} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/test/test_box_list.py0000644000175100001660000002137514742254572017556 0ustar00runnerdocker#!/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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/test/test_config_box.py0000644000175100001660000000312214742254572020036 0ustar00runnerdocker#!/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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/test/test_converters.py0000644000175100001660000000616014742254572020120 0ustar00runnerdocker#!/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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/test/test_from_file.py0000644000175100001660000000476214742254572017676 0ustar00runnerdocker#!/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") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1737054586.0 python_box-7.3.2/test/test_sbox.py0000644000175100001660000000150114742254572016673 0ustar00runnerdocker#!/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"')