-
Notifications
You must be signed in to change notification settings - Fork 108
Types of Boxes
Argument | Default | Modifies Keys | Description |
---|---|---|---|
conversion_box | true | no | Make keys such as "my key" attribute accessible, into .my_key
|
default_box | false | no | If a key doesn't exist on lookup, it will be created with an empty Box as the value |
box_dots | false | no | Makes my_box.a.b.c also be accessible via my_box["a.b.c"]
|
camel_killer_box | false | yes | Got PeskyAndAnnoyingKeys? Turn them into less pesky_and_annoying_keys |
frozen_box | false | no | After the first insert of data, make the box unchangeable, and hashable! |
box_recast | false | no | Automatically convert all incoming values of a particular key to a specified type. |
box_intact_types | false | no | Don't allow conversions for certain types. |
supplemental argument | default | description |
---|---|---|
box_safe_prefix | "x" | Character or prefix to prepend to otherwise invalid attributes |
box_duplicates | "ignore" | When conversion duplicates are spotted, either ignore, warn or error |
By default, Box is now a conversion_box that adds automagic attribute access for keys that could not normally be attributes. It can of course be disabled with the keyword argument conversion_box=False.
movie_box.movies.Spaceballs["personal thoughts"] = "It was a good laugh"
movie_box.movies.Spaceballs.personal_thoughts
# 'It was a good laugh'
movie_box.movies.Spaceballs.personal_thoughts = "On second thought, it was hilarious!"
movie_box.movies.Spaceballs["personal thoughts"]
# 'On second thought, it was hilarious!'
# If a safe attribute matches a key exists, it will not create a new key
movie_box.movies.Spaceballs["personal_thoughts"]
# KeyError: 'personal_thoughts'
Keys are modified in the following steps to make sure they are attribute safe:
- Convert to string (Will encode as UTF-8 with errors ignored)
- Replaces any spaces with underscores
- Remove anything other than ascii letters, numbers or underscores
- If the first character is an integer, it will prepend a lowercase 'x' to it
- If the string is a built-in that cannot be used, it will prepend a lowercase 'x'
- Removes any duplicate underscores
This does not change the case of any of the keys.
bx = Box({"321 Is a terrible Key!": "yes, really"})
bx.x321_Is_a_terrible_Key
# 'yes, really'
These keys are not stored anywhere, and trying to modify them as an attribute will actually modify the underlying regular key's value.
Warning: duplicate attributes possible
If you have two keys that evaluate to the same attribute, such as "a!b" and "a?b" would become .ab, there is no way to discern between them, only reference or update them via standard dictionary modification.
supplemental argument | default | description |
---|---|---|
default_box_attr | Box | What will be used as the default value for missing keys |
default_box_none_transform | true | If a key exists, but has a value of None return the default attribute instead |
default_box_no_key_error | true | Unlikecollections.defaultdict , don't even raise KeyError during pop or del
|
default_box_create_on_get | true | On lookup of a key that doesn't exist, create it if missing |
It's boxes all the way down. At least, when you specify default_box=True
it can be.
empty_box = Box(default_box=True)
empty_box.a.b.c.d.e.f.g
# <Box: {}>
# BOX 4.1 change: on lookup the sub boxes are created
print(empty_box)
# <Box: {'a': {'b': {'c': {'d': {'e': {'f': {'g': {}}}}}}}}>
empty_box.a.b.c.d.e.f.g = "h"
empty_box
# <Box: {'a': {'b': {'c': {'d': {'e': {'f': {'g': 'h'}}}}}}}>
Unless you want it to be something else.
evil_box = Box(default_box=True, default_box_attr="Something Something Something Dark Side")
evil_box.not_defined
# 'Something Something Something Dark Side'
# Keep in mind it will no longer be possible to go down multiple levels
evil_box.not_defined.something_else
# AttributeError: 'str' object has no attribute 'something_else'
default_box_attr
will first check if it is callable, and will call the object
if it is, otherwise it will see if has the copy
attribute and will call that,
lastly, will just use the provided item as is.
Default Boxes will not raise Key Errors on missing keys when pop
ing or del
ing items.
A new way to traverse the Box!
my_box = Box(box_dots=True)
my_box.incoming = {'new': {'source 1': {'$$$': 'money'}}}
print(my_box['incoming.new.source 1.$$$'])
# money
my_box['incoming.new.source 1.$$$'] = 'spent'
print(my_box)
# {'incoming': {'new': {'source 1': {'$$$': 'spent'}}}}
Be aware, if those sub boxes didn't exist as planned, a new key with that value would be created instead
del my_box['incoming']
my_box['incoming.new.source 1.$$$'] = 'test'
print(my_box)
# {'incoming.new.source 1.$$$': 'test'}
Support for traversing box lists as well!
my_box = Box({'data': [ {'rabbit': 'hole'} ] }, box_dots=True)
print(my_box.data[0].rabbit)
# hole
This only works for keys that are already strings currently.
Similar to how conversion box works, allow CamelCaseKeys to be found as snake_case_attributes.
cameled = Box(BadHabit="I just can't stop!", camel_killer_box=True)
cameled.bad_habit
# "I just can't stop!"
This is destructive to the incoming dictionary keys for this modification. So if you send it .to_dict()
, it will still appear with the newly snake_cased keys.
Want to show off your box without worrying about others messing it up? Freeze it!
frigid = Box(data={'Python': 'Rocks', 'inferior': ['java', 'cobol']}, frozen_box=True)
frigid.data.Python = "Stinks"
# box.BoxError: Box is frozen
frigid.data.Python
# 'Rocks'
hash(frigid)
# 4021666719083772260
frigid.data.inferior
# ('java', 'cobol')
It's hashing ability is the same as the humble tuple
, it will not be hashable
if it has mutable objects. Speaking of tuple
, that's what all the lists
becomes now.
Automatically convert all incoming values of a particular key (at root or any sub box) to a different type.
For example, if you wanted to make sure any field labeled 'id' was an integer:
my_box = Box(box_recast={'id': int})
my_box.new_key = {'id': '55', 'example': 'value'}
print(type(my_box.new_key.id))
# 55
If it cannot be converted, it will raise a BoxValueError
(catchable with either BoxError
or ValueError
as well)
my_box = Box(box_recast={'id': int})
my_box.id = 'Harry'
# box.exceptions.BoxValueError: Cannot convert Harry to <class 'int'>
Do you not want box to convert lists or tuples or incoming dictionaries for some reason? That's totally fine, we got you covered!
my_box = Box(box_intact_types=[list, tuple])
# Don't automatically convert lists into #BoxList
my_box.new_data = [{'test': 'data'}]
print(type(my_box.new_data))
# <class 'list'>
These documents are for Box 5.0+