简体   繁体   中英

Python: Working with configparser inside a class. How to access my attributes names and values dynamically inside my class-methods

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()  

My problem was, that every setting from the init needs to be written manually in the 2 methods as well. I found the solution, and the answer is on the bottom. Working Example!

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

Dynamically Accessing Attributes

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

It became more clear from our discussion, because the original post didn't include some details, but you can access instance variables, like so as the OP determined from the link above:

{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.

Accessing Static Attributes

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"

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.

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.

OK I finally solved it, and decided to share, in hope it helps someone else.

You can test it yourself....code is working even without a.ini file(will be created) Now I can add as many settings in init as I want, without editing the other 2 methods as well.

As a Bonus, now the values gets correctly parsed from the.ini into the variables. Meaning tuple will become tuple. Not supported normally from configparser. Additionally it checks whether the TYPE in the default section corresponds with the type inside the.ini...if not, ONLY the wrong line gets changed back to default. Except when str is expected, than all types in the ini pass. Which is somehow correct. Like a username is default a string, but custom username could be integer only, or even a tuple..lol. And if integer is expected, True and False pass as integer in ini, what is also technically correct..lol. But anything else gets found, eg. a broken tuple in the ini gets defaulted back, if a tuple is expected. If anything else is broken, ALL defaults are getting written. It supports the correct casting of (bool, tuple, int, float, set, dict, list) and of course str. Bool needed some special love,...now True/False upper/lowercase and inside "" or '' are supportet.

############################################### SETTINGS START ########################################################
import configparser
import logging.config
from ast import literal_eval

datatypes = (tuple, int, float, set, dict, list)

class Bcolors:
    HEADER = "\033[95m"
    OKBLUE = "\033[94m"
    OKCYAN = "\033[96m"
    OKGREEN = "\033[92m"
    YELLOW = "\033[93m"
    FAIL = "\033[91m"
    ENDC = "\033[0m"
    BOLD = "\033[1m"
    UNDERLINE = "\033[4m"

class SettingsGeneral:
    config = configparser.ConfigParser()
    configfile = "RealGui.ini"
    section = "GENERAL"

    def __init__(self):
        self.window_last_position = True
        self.window_last_size = [1500, 1060]
        self.filename_formatting = (33, 2)  # (33, 2) #2 rows
        self.graphics_directory = "G:\+++CODING+++\Thin-Plate-Spline-Motion-Model-Windows-main\RealGui\graphics\\"
        self.extensions = (".gif", ".png", ".jpg")

    def read_from_ini(self):
        tmp = list(self.__dict__.items())  #  This is the magic I was looking for. Need to be converted to a list, or a deep copy, because you cannot iterate over a dic and change it.
        try:
            self.config.read(self.configfile)
            for attr_name, attr_value in tmp:
                    inivalue = self.config[self.section][attr_name]
                    datatype = type(attr_value)
                    if datatype == bool:
                        if ''.join(e for e in inivalue.lower() if e.isalnum()) == 'false':
                            setattr(self, attr_name, bool(False))
                        elif ''.join(e for e in inivalue.lower() if e.isalnum()) == 'true':
                            setattr(self, attr_name, bool(True))
                        else:
                            logging.warning(f"{Bcolors.FAIL}invalid .ini entry: {attr_name} = \"{inivalue}\" ...expected type {datatype} ...overwriting with default{Bcolors.ENDC}")
                            self.config.set(self.section, attr_name, str(attr_value))
                    elif datatype in datatypes:
                        try:
                            if isinstance(literal_eval(inivalue), datatype):
                                setattr(self, attr_name, literal_eval(inivalue))
                            else:
                                logging.warning(f"{Bcolors.FAIL}invalid .ini entry: {attr_name} = \"{inivalue}\" ...expected type {datatype} ...overwriting with default{Bcolors.ENDC}")
                                self.config.set(self.section, attr_name, str(attr_value))
                        except Exception as e:
                            logging.warning(f"{Bcolors.YELLOW}invalid .ini entry: {attr_name} = \"{inivalue}\" ....overwriting with default{Bcolors.ENDC}")
                            logging.warning(f"{Bcolors.FAIL}{e}{Bcolors.ENDC}")
                            self.config.set(self.section, attr_name, str(attr_value))
                    elif datatype == str:
                            if isinstance(inivalue, datatype):
                                setattr(self, attr_name, inivalue)
        except Exception as e:
            logging.warning(f"{Bcolors.FAIL}failed to read ini..Section:{self.section}....overwriting with defaults{Bcolors.ENDC}")
            logging.warning(f"{Bcolors.FAIL}{e}{Bcolors.ENDC}")
        self.write_to_ini()

    def write_to_ini(self):
        with open(self.configfile, "w") as cfg:
            tmp = list(self.__dict__.items())  #  This is the magic I was looking for. Need to be converted to a list, or a deep copy, because you cannot iterate over a dic and change it.
            if not self.config.has_section(str(self.section)):
                self.config.add_section(str(self.section))
            for attr_name, attr_value in tmp:
                self.config.set(self.section, attr_name, str(attr_value))
            self.config.write(cfg)


class FolderLeftSettings(SettingsGeneral):
    config = configparser.ConfigParser()
    section = "FOLDER-LEFT"
    configfile = "RealGui.ini"
    def __init__(self):
        self.last_directory = "C:\\"
        self.row_size = 8
        self.column_size = 9

############################################### SETTINGS END ########################################################

and in main it can be used normally like:

settings=FolderLeftSettings()

print(settings.last_directory)  # Default Value according to __init__

settings.read_from_ini()
print(settings.last_directory)  # if .ini is correctly read..show new value

settings.last_directory ="Z:\\"
settings.write_to_ini()
print(settings.last_directory)  # New updated Value...no need to read from .ini again of course

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM