0

This is easier to explain with an example. Imagine I have the following dictionary..

myapps = {'app_1': {'username': 'admin',
                    'pwd': 'S3cret',
                    'ports': [8080, 443],
                    'users': {'user1': 'john',
                              'user2': 'ruth'}},
          'app_2': {'username': 'user1', 
                    'pwd': 'P@ssword'}
}

Now I'd like to use the data of this dictionary in the following manner:

print("App_2 username = ", myapps.app_2.username) # prints App_2 username = user1
print("App_2 pwd = ",      myapps.app_2.pwd)      # prints App_2 pwd = P@ssword
print("App_1 ports = ",    myapps.app_1.ports)    # prints App_1 ports = [8080, 443]

myapps.app_2.username = "MyNewAdminAccount"
print("App_2 username = ", myapps.app_2.username) # prints App_2 username = MyNewAdminAccount

I'm basically trying to write a class that can take a dictionary, go through it recursively and generate attributes for each key and subkey given in my dictionary.

martineau
  • 119,623
  • 25
  • 170
  • 301
JohnnyLoo
  • 877
  • 1
  • 11
  • 22
  • This is pretty straightforward to do with `setattr()`. What have you tried? Are the keys always the same? Is the dictionary always the same structure or is it arbitrarily nested? – ddejohn Oct 06 '21 at 15:47
  • 1
    share the code that you tried so far – Sabil Oct 06 '21 at 15:54
  • 1
    Does this answer your question? [Recursively access dict via attributes as well as index access?](https://stackoverflow.com/questions/3031219/recursively-access-dict-via-attributes-as-well-as-index-access) – rustyhu Oct 06 '21 at 16:09
  • I'd vote that this isn't necessary a duplicate of `dot.dict` Q [Accessing dict keys like an attribute?](https://stackoverflow.com/questions/4984647/accessing-dict-keys-like-an-attribute) since it's about a custom class (and assumes something perhaps more dynamic / inherited / specific). Though it is very similar. – Yaakov Bressler Oct 06 '21 at 16:11

2 Answers2

4

If I understand your question correctly, you are looking for a DotDict: Recursive DotDict

With this class you can remap the __getattr__ and _setattr__, which are called on the dot-operator to use the __getitem__ and __setitem__ functions which access dictionary data.

Joschua
  • 324
  • 3
  • 8
  • 1
    Thank you. Prodict does what I need and more. Never heard of that package before. – JohnnyLoo Oct 06 '21 at 18:25
  • You are welcome! It answers your question, but I'm not sure if it's the best way to go, as I don't really know your usecase, i.e. if you are building these dicts yourself or getting them as input etc. So in general, if you have full control over the code and data I would also use dataclasses (as recommended [below](https://stackoverflow.com/a/69469026/5609590) but without using the Wizard and just create the instances yourself). If you haven't heard about dataclasses, I can recommend this youtube channel: https://www.youtube.com/watch?v=vRVVyl9uaZc – Joschua Oct 07 '21 at 13:12
  • 1
    If you don't want to use dataclasses, you can also use [pydantic](https://pydantic-docs.helpmanual.io/) which supports this use case. It also has field types like [`Secret...`](https://pydantic-docs.helpmanual.io/usage/types/#secret-types) useful to to mask the password data, so you don't accidentally leak it when logging the data. But I agree the question was a bit vague in general. – rv.kvetch Oct 07 '21 at 13:27
2

For a more "IDE-friendly" option that provides type hinting and auto completion, I'd suggest looking into the Dataclass Wizard library for this. Just a disclaimer, that it's still in beta because I'm testing some things out, but it seems like it should work well enough for this use case.

Step 1: Generate a model for the data above

You can use an included CLI utility to do this as it's easier, but there's an easy way to do it programatically as well:

from dataclass_wizard.wizard_cli import PyCodeGenerator


string = """
{
  "myapps": {
    "app_1": {
      "username": "admin",
      "pwd": "S3cret",
      "ports": [
        8080,
        443
      ],
      "users": {
        "user1": "john",
        "user2": "ruth"
      }
    },
    "app_2": {
      "username": "user1",
      "pwd": "P@ssword"
    }
  }
}
"""

print(PyCodeGenerator(file_contents=string).py_code)

Step 2: Load data into the model

from dataclasses import dataclass
from typing import List

from dataclass_wizard import JSONWizard


@dataclass
class Data(JSONWizard):
    """
    Data dataclass

    """
    myapps: 'Myapps'


@dataclass
class Myapps:
    """
    Myapps dataclass

    """
    app_1: 'App1'
    app_2: 'App2'


@dataclass
class App1:
    """
    App1 dataclass

    """
    username: str
    pwd: str
    ports: List[int]
    users: 'Users'


@dataclass
class Users:
    """
    Users dataclass

    """
    user1: str
    user2: str


@dataclass
class App2:
    """
    App2 dataclass

    """
    username: str
    pwd: str

data = Data.from_json(string)

repr(data)
# Data(myapps=Myapps(app_1=App1(username='admin', pwd='S3cret', ports=[8080, 443], users=Users(user1='john', user2='ruth')), app_2=App2(username='user1', pwd='P@ssword')))

Now you can use the dot . access, as intended. Type hinting for attributes should also work with your IDE (at least in Pycharm)

myapps = data.myapps

print("App_2 username = ", myapps.app_2.username) # prints App_2 username = user1
print("App_2 pwd = ",      myapps.app_2.pwd)      # prints App_2 pwd = P@ssword
print("App_1 ports = ",    myapps.app_1.ports)    # prints App_1 ports = [8080, 443]

myapps.app_2.username = "MyNewAdminAccount"
print("App_2 username = ", myapps.app_2.username) # prints App_2 username = MyNewAdminAccount
rv.kvetch
  • 9,940
  • 3
  • 24
  • 53