Skip to content

Types of Boxes

Chris Griffith edited this page Dec 3, 2022 · 7 revisions
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.

Conversion Box

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:

  1. Convert to string (Will encode as UTF-8 with errors ignored)
  2. Replaces any spaces with underscores
  3. Remove anything other than ascii letters, numbers or underscores
  4. If the first character is an integer, it will prepend a lowercase 'x' to it
  5. If the string is a built-in that cannot be used, it will prepend a lowercase 'x'
  6. Removes any duplicate underscores

This does not change the case of any of the keys.

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.

Default Box

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 poping or deling items.

Box Dots

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.

Camel Killer Box

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.

Frozen Box

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.

Box Recast Values

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'>