cdgriffith / Box

Python dictionaries with advanced dot notation access
https://github.com/cdgriffith/Box/wiki
MIT License
2.63k stars 107 forks source link

Camel killer box without data mangling #279

Open jacobtomlinson opened 2 months ago

jacobtomlinson commented 2 months ago

The camel killer box is a great feature. I work a lot with data structures from Kubernetes which uses camel case everywhere, and it's really nice to be able to access them via snake case attributes.

>>> from box import Box
>>> data = Box({"loadBalancer": "foo"}, camel_killer_box=True)
>>> data.load_balancer
'foo'

I need to be able to modify these data structures and then send them back to the Kubernetes API. Unfortunately because Box defaults to destructively updating the underlying dictionary to use snake case there is no way for me to get a data structure out that the Kubernetes API will understand.

>>> data.load_balancer = "bar"
>>> data.to_dict()
{'load_balancer': 'bar'}  # Kubernetes wants {'loadBalancer': 'bar'}

Would it be possible to add some way to round trip a dict through a Box without mangling it, and still get the benefits of accessing snake case attributes?

cdgriffith commented 1 month ago

Hi @jacobtomlinson unfortunately it's named camel_killer as it does straight up destroy the original names and stores them as snake case.

You could subclass Box and add a function like to_camel_case_dict. Basic example:

from box import Box

class CamelBox(Box):

    def __init__(cls, *args, **kwargs):
        kwargs["camel_killer_box"] = True
        super().__init__(*args, **kwargs)

    def to_camel_case_dict(self, the_dict=None):
        def to_camel_case(snake_str):
            components = snake_str.split('_')
            return components[0] + ''.join(x.title() for x in components[1:])

        if not the_dict:
            the_dict = self.to_dict()

        out_dict = dict()
        for k, v in the_dict.items():
            if isinstance(v, dict):
                out_dict[to_camel_case(k)] = self.to_camel_case_dict(v)
            else:
                out_dict[to_camel_case(k)] = v
        return out_dict

Resulting in:

 my_dict = test_dict = {
    "key1": "value1",
    "BigCamel": "hi",
    "alist": [{"a": 1}],
    "lowerCamel": {"Key 3": "Value 3", "Key4": {"lowerCamelNumber2Or3Or5": "Value5"}},
}

camel_box = CamelBox(my_dict)

print(camel_box.to_dict())
# {'key1': 'value1', 'big_camel': 'hi', 'alist': [{'a': 1}], 'lower_camel': {'key 3': 'Value 3', 'key4': {'lower_camel_number2_or3_or5': 'Value5'}}}

print(camel_box.to_camel_case_dict())
# {'key1': 'value1', 'bigCamel': 'hi', 'alist': [{'a': 1}], 'lowerCamel': {'key 3': 'Value 3', 'key4': {'lowerCamelNumber2Or3Or5': 'Value5'}}}
jacobtomlinson commented 1 month ago

Thanks for the response @cdgriffith.

The trouble with that approach is you assume that everything should be camel case. I expect in these large user provided data structures there will be a mix of both. The example you have converts everything to camel case. So the data just gets mangled in a different way.

my_dict = {
    "BigCamel": "hi",
    "sneaky_snake": "hisss, I'm already snakey",
}

print(CamelBox(my_dict).to_camel_case_dict())
# {'bigCamel': 'hi', 'sneakySnake': "hisss, I'm already snakey"}