python: Working with configparser inside a class. How to access my attributes name and values inside my class-methods?

Question:

My first program is getting much bigger than excepted. ๐Ÿ™‚

import configparser

config = configparser.ConfigParser()    
configfile = 'RealGui.ini'
      
class FolderLeftSettings:
    def __init__(self):
        self.name = "SECTION"
        self.last_directory = ""
        self.row_size = 6
        self.column_size = 9

    def write_to_ini(self):
        if not config.has_section(str(self.name)):
            config.add_section(str(self.name))
        with open('configfile', 'w') as configfile:
            config.set(self.name, 'last_directory', str(self.last_directory))
            config.set(self.name, 'row_size', str(self.row_size))
            config.set(self.name, 'column_size', str(self.column_size))
            config.write(configfile)

    def read_from_ini(self):
        try:
            config.read('configfile')
            if config.has_section(str(self.name)):
                self.last_directory = (config[self.name]['last_directory'])
                self.row_size = int((config[self.name]['row_size']))
                self.column_size = int((config[self.name]['column_size']))
        except Exception as e:
            print("failed to read ini....overwriting with defaults")
            print(e)
            self.write_to_ini()

 settings=FolderLeftSettings()  

I try to read the .ini file.
Now my very broad exception(normally considered bad) is catching all the errors the .ini can have:
(wrong header, missing X, integer is string, and so on)
If anything is off, then the whole .ini gets replaced by the default values from __init__

settings.read_from_ini()

Now in my code I can change settings easily, and use autocomplete from IDE :)…that’s primary reason why I use a class.

settings.row_size=11
settings.write_to_ini()

Something like

with open('configfile', 'w') as configfile:
    config.set('FOLDER-LEFT', 'row_size', "12")
    config.write(configfile)

is 3 lines long, and invites for mistakes.(No autocomplete)

I don’t care if I have to always write the whole ini, if a value gets changed, it’s way to easy, and if I am not mistaken, the whole file gets replaced always anyway by configparser.

So far I am quiet happy with it(trimmed down for readability).
But I read, that every time you have to repeat yourself in code, it’s bad practice, and in my class I have to write all the things 3 times.
As the settings grow, this looks uglier and uglier.
Pretty sure there is a better way. I think these magic __xxx__ are somehow key…lol.

If I do it like that:

class FolderLeftSettings:
    def __init__(self):
        self.name = "SECTION"
        self.last_directory = ""
        self.row_size = 6
        self.column_size = 9

How can I then access the attribute names inside the methods somehow.
WITHOUT USING explicit names?? something like:

def write_to_ini(self):
    for x in self.__class__.__getattribute__:
        # do something with x
        # do something with x.__value__
        

Excpected:

'name'
'Section'
...
'row_size'
'6'

Appreciate any help.

THX.

Asked By: Leonick

||

Answers:

My comments were getting lengthy, so here is a post:

Exceptions

Check out this link in regards to how you should use general exceptions.

What’s funny is, your side note about the broad exceptions "(wrong header, missing X, integer is string, and so on)" is actually how you would catch those exceptions,

except (ValueError, OSError)

and so on. So if you know what could go wrong, you could replace your broad except with a tuple of exceptions that you are expecting. In some since, the exceptions have been handled. Consider an ini file that is so corrupt that your parser can’t handle it. What would you want your parser to do? I’d want mine to exit with an error code. If you don’t want that, why not? Catching the exceptions allows for you to handle the bad data that might potentially be passed, but if not enough data is passed (like if I can’t load ini file), then you can’t really do anything with that object, because you don’t know what the user wants to do to the data – at that point it’d be better if the object didn’t even exist. At the end of the day, it largely depends on context. If you wanted to build a GUI and allow users to use your parser through it, I wouldn’t want the GUI to crash, so I’d have to do more error handling, but if you’re using a CLI, you’d want it to either just exit, or (lets say you were using a loop to iterate over the files) you’d want it to skip that file or take defaults. Let me take the one method that utilizes the try, except clause and recode it to how I would have it for you to consider it ->

From the docs:

If a file named in filenames cannot be opened, that file will be ignored. This is designed so that you can specify an iterable of potential configuration file locations (for example, the current directory, the userโ€™s home directory, and some system-wide directory), and all existing configuration files in the iterable will be read.

def read_from_ini(self):
    # the below doesn't throw an error - they handle exceptions on the read() call.
    config.read('configfile')
    # the if statement won't throw an error, unless self.name is not a datatype
    # handled by has_section, but if you convert it to a str() in your __init__ 
    # method, you wont have to worry about that.
    if config.has_section(self.name):
        try:
            # not going to throw an error, unless it doesn't have the key
            # I didn't check the docs to see if it will always have this key
            # or not, so I am including it under the try clause...
            self.last_directory = config[self.name]['last_directory']
            # the 2 below might throw a value error, but not likely, since you
            # are reading the integers from the config as opposed to taking user
            # input and setting them
            self.row_size = int((config[self.name]['row_size']))
            self.column_size = int((config[self.name]['column_size']))
        except (ValueError, KeyError):
            print(f"Failed to read {self.name}... Overwriting with defaults")
            self.write_to_ini()
    else:
        print(f"Failed to read {self.name}... Overwriting with defaults")
        self.write_to_ini()

The above can be reworked to be more elegant in my opinion, but I chose to make it more simple to understand from your section of code. Here is one way that would make it more elegant:

# returns True if it could use the settings, else False
def read_from_ini(self):
    config.read('configfile')

    if not config.has_section(self.name):
        print(f"Failed to read {self.name}... Overwriting with defaults")
        self.write_to_ini()
        return False

    try:
        self.last_directory = config[self.name]['last_directory']
        self.row_size = int((config[self.name]['row_size']))
        self.column_size = int((config[self.name]['column_size']))
    except (ValueError, KeyError):
        print(f"Failed to read {self.name}... Overwriting with defaults")
        self.write_to_ini()
        return False
    else:
        return True

Notice now, how all the tries, ifs, elses, etc are all on the same block level, simply because I inverted the original if.

Your Question And My Answer

Hopefully I’m not missing your question, but as it stands now ("How to access my attributes name and values inside my class-methods?"), you already know how to access your attributes and functions available to your class – you were doing that with self. If you mean after you’ve created an object from your class, then its almost no different:

settings=FolderLeftSettings()
print(settings.name)

will return the name that was given to it. If you want to set them, you can do:

settings.name = "whatever"

Dynamically Accessing Attributes

As per this answer, you can access attributes for an instance using vars(my_object).

You can access instance variables, like so:

{key: value for key, value in self.__dict__.items() if not key.startswith("__")}

but at that point, it sounds like you should be considering, Abstract Base Classes and why / how you would use them.

Another Suggestion

I would say don’t do this:

config = configparser.ConfigParser()    
configfile = 'RealGui.ini'
      
class FolderLeftSettings:
    ...

A suggestion is to either assign a config instance to the class you are creating or use 1 for all of your instances.

Option 1

class FolderLeftSettings:
    def __init__(self, configfile, name, last_directory, row_size, column_size):
        self.config = configparser.ConfigParser()
        self.configfile = configfile
        self.name = name
        self.last_directory = last_directory
        self.row_size = row_size
        self.column_size = column_size

Option 2

main.py

import my_class as mc
# here you can instantiate new configs in a for loop
files = ["path/to/config.ini", "another/path/to/new_config.ini]
for file in files:
    config = configparser.ConfigParser()
    configfile = file
    name = "dynamically_allocated_name_here"
    last_directory = "last_dir_here"
    row_size = 6
    column_size = 9
    mc.FolderLeftSettings(config, configfile, name, last_directory, row_size, columns_size)

my_class.py

class FolderLeftSettings:
    def __init__(self, config, configfile, name, last_directory, row_size, column_size):
        self.config = config
        self.configfile = configfile
        self.name = name
        self.last_directory = last_directory
        self.row_size = row_size
        self.column_size = column_size

or something of the sort.

Answered By: Shmack
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.