2

So I have this class:

import yaml

class Config():
        def __init__(self, filename):
                self.config_filename=filename

        def __read_config_file(self):
                with open(self.config_filename) as f:
                        self.cfg = yaml.safe_load(f)

        def get(self):
                self.__read_config_file()
                return self.cfg

And it works fine. The thought behind it is to force a reread of the config file every time I use something in the configuration. Here is an example of usage:

cfg = Config('myconfig.yaml')

for name in cfg.get()['persons']:
    print (cfg.get()['persons'][name]['phone']) 
    print (cfg.get()['persons'][name]['address']) 

This works, but I think it looks extremely ugly. I could do something like this:

c = cfg.get()['persons']
for name in c:
    print (c['persons'][name]['phone']) 
    print (c['persons'][name]['address']) 

Which looks just a tiny bit better, but I also lose the benefit of reloading on access, but what I want to do is something this (which obviously does not work):

for name in c:
    print (name['phone']) 
    print (name['address'])

It seems like it's something I don't understand about iterating over dictionaries, but my main concern here is that I want to reload the configuration file each time any value from that file is used, and I want it in a nice readable way. So how can I redesign this?

Example of configuration file. It's possible to change the format here if necessary.

persons:
    john:
        address: "street A"
        phone: "123"
    george:
        address: "street B"
        phone: "456"
klutt
  • 30,332
  • 17
  • 55
  • 95
  • What does the YAML file look like? – RoadRunner May 07 '20 at 07:53
  • @RoadRunner Does that really matter? But ok, I'll update the question. – klutt May 07 '20 at 07:55
  • Have a look here, this might be helpful: https://stackoverflow.com/questions/35968189/retrieving-data-from-a-yaml-file-based-on-a-python-list – Petronella May 07 '20 at 08:06
  • @klutt Well, it always to good include an example file. Some future readers might not be so comfortable with YAML to understand what your code is doing. – RoadRunner May 07 '20 at 08:33
  • 2
    I'd make Config class a subclass of UserDict and then overwrite `__getitem__()` to do read before values are returned .. – rasjani May 07 '20 at 08:33
  • @rasjani I was actually doing something like that right now. But why UserDict instead of dict? – klutt May 07 '20 at 08:41
  • I would do the same as @rasjani (I have implemented something losely related that way). For performance I do recommend to check the timestamp of the data file before reading it. If it is older than the timestamp of last read, you may use the existing data. Finally, pay attention to data consitency (i.e. you may fetch the phone number from old file and then the address from new file) – VPfB May 07 '20 at 08:41
  • @rasjani Actually, that worked very well. However, there is one thing. I start with `x = Config` and then continue with `x['persons']` it reloads just fine. But how about only using `x` without any indexing? Then it does not reload. – klutt May 07 '20 at 14:04
  • @rasjani I went for your solution. Wanna write a proper answer? – klutt May 08 '20 at 08:51
  • @rasjani I wrote an answer. You can have a look if you want. – klutt May 08 '20 at 10:35

3 Answers3

0

Since it is a dict, normal iteration doesn't work. we can iterate through the dictionary by the following method,

for name, person in c.items():
   print(name)
   print(person['phone']) 
   print(person['address'])

Hope this helps

Kumar KS
  • 873
  • 10
  • 21
  • It helps a bit, but this will not reload the config file for each print statement, right? – klutt May 07 '20 at 08:11
  • yes, it will not. Since you have created a object `cfg`, it will be iterating through the value from the object. – Kumar KS May 07 '20 at 08:13
0

I have made your 'persons' data :

persons={}
persons["name"]={"john":{'phone':'123', 'address':'street A'}, 
                 "george":{'phone':'456', 'address':'street B'}}

Here is something interesting, you can get all the names that are written in "persons":

L_names = list(persons['name'].keys())
print(L_names)  # returns ['john', 'george']

So if you get the data of each character :

L_names_data = []
for i in list(persons['name'].keys()):
    L_names_data.append(persons['name'][i])

You can easily write what you wanted : (a nice and simple for loop)

for name_data in L_names_data:
    print(name_data['address'])
    print(name_data['phone'])

#returns :
#street A
#123
#street B
#456

The inconvenient is that you loose the 'name' info ('john' and 'george' strings don't appear in 'L_names_data' which is a list of dictionnaries).

ClemPat
  • 49
  • 8
0

The user rasjani made some comments that helped me solve it. What I do is basically this:

import collections

class Config(collections.UserDict):
        def __getitem__(self, key):
                self.reload()
                return super().__getitem__(key)

        def reload(self):
                with open(self.filename) as f:
                        c=yaml.safe_load(f)
                super().__init__(c)

The actual class is more complicated with more error checking and features, but the above is what does the magic I asked about.

The method __getitem__ is called everytime you use the dictionary. So what I do is simply calling reload before calling __getitem__ from the superclass.

One should be aware of one exception here.

a = Config('myconf.yml')
b = a['field'] # Will reload the configuration file
c = a          # But this will not
d = c['field'] # However, this will

I thought about solving that by also modifying __getattribute__ in the same way, but I ended up with all sorts of problems with endless recursion, and in practice I am not sure if it is a problem worth solving, or even a problem at all.

klutt
  • 30,332
  • 17
  • 55
  • 95