1

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!

Jan Garaj
  • 25,598
  • 3
  • 38
  • 59
Leonick
  • 45
  • 7
  • @Shmack thats's something I don't understand. cfgparser has many exceptions. I would need to try out everything to get the exception. like wrong header, wrong sections, integer is string, and so on. What if I miss one? I don't care why cfgparser can't read my ini(because of the thousand ways the user could mess it up). I could use `except Exception as e:` and print e. to actually gibe me and the user a hint. But why is catching all bad in this case?? I want to read ini....if it goes wrong....I reset it. Why risk the termination of the program in the final built, because I missed one? – Leonick Nov 14 '22 at 16:38
  • I better know what I want now: Accesing my attribute names and values inside my class funtions. so I don't need to write every attribute name in every function. like:(inside method) for x in self.__class__.__getattribute__: print(x) print(x.__value__) – Leonick Nov 14 '22 at 16:42

2 Answers2

1

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.

Shmack
  • 1,933
  • 2
  • 18
  • 23
  • wow thx for the effort. I still don't understand why I need to state every exception. I don't care why cfgparser can't read my ini (because of the thousand ways the user could mess it up, and all the diff exceptions inside cgfparser). I could use `except Exception as e:` and `print e`. to actually give me and the user a hint. But why is catching all bad in this case?? I want to read ini....if it goes wrong....I reset it. Why risk the termination of the program in the final built, because I missed one? part2.... – Leonick Nov 14 '22 at 21:08
  • Another Suggestion: That'a actually a good suggestion, to include the things related to the class inside the class. Because until now I have in my nearly 1k lines of code avoided globals. That's also why I use classes. There is only one instance of settings, and of course I know how to access classes. My program(file-explorer) is 1k lines and pretty complex already, uses even multiprocessing, and nearly all is inside classes. When creating the thread I did not know exactly what I was looking for, just to avoid having `last_directory, row_size` and all other settings 3 times in my code....... – Leonick Nov 14 '22 at 21:19
  • I may need to way shorten and totally change my question, now that I now what I want exactly. After the `__init__` I want to loop through my instance names and variables and use them dynamically, something like (INSIDE CLASS METHOD) `for attr, value in self.__class:` I will check out your link, at first glance I could not make it work. But if there are no other answers, I will accept yours, just for the effort. – Leonick Nov 14 '22 at 21:26
  • Wow I found it!! That's how I can elegantly rewrite it, und use all my setting names only one time in the `__init__` (my defaults) And use in my other methods following. `{key: value for key, value in self.__dict__.items() if not key.startswith("__")}` If part not really needed, but that's how I can solve it. so self.__dict__.items what I was looking for...that's what I meant by `self.__class__.__getattribute` thx Shmack. Is it ok too accept your answer as correct, and then put my own answer under it, with the implemented solution like I meant it? – Leonick Nov 14 '22 at 21:44
  • Sure, I could include it in mine for a complete answer, if you think that'd be better. I'm going to start killing off some of my comments, but I revised my answer - feel free to put your own. I really think your answer has to do with Abstract Base Classes, but maybe I'm wrong, right now it's tough for me to wrap my head around them and proper uses for them, but I included it nonetheless. – Shmack Nov 14 '22 at 21:53
  • thx again. I accepted your answer, not because you answered it, but it helped me find what I was looking for as stated in the tooltip. I think I am gonna write my own answer later(need to do some other things first) if you don't mind. Regarding the exception, ok now I understand what you mean, by resetting the ini, what could NOT be what the user wants. But the ini is just to safe the state of user actions, everything gets dynamically changed inside the program, row_size by mouse, last folder by clicking icons(file-explorer) rest in settings. It's just a failsafe, in case user messes with it. – Leonick Nov 14 '22 at 22:03
  • Well, if you think adding an answer would be more beneficial for others trying to understand the problem and what the solution was, then after 2 days, be sure to accept yours as the actual answer (I don't think that you can actually accept it til 2 days later). Glad I was able to help. – Shmack Nov 14 '22 at 22:09
  • 1
    maybe a solution would be to encrypt the .ini, , but that would be against open source principle, or make the ini a ONE-string ugly format. Just wanted to test concepts, and I am aware of your point, and in a different scenario I would solve the ini problem different....lots of more checks need to be involved....you never know what the user can do. I have a setting: number of cores to use for thumbnail creation. You never know what the user does...maybe he changes the max number integer ,dynamically inserted for his system automatically, to `ALL`...lol. I don't care..Use the settings dear user – Leonick Nov 14 '22 at 22:10
  • 1
    In case you are interested, I posted my solution. – Leonick Nov 15 '22 at 00:06
1

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
Leonick
  • 45
  • 7
  • Nice. I do believe that calling `vars()` on self (i.e. `vars(self)`) is in essence the same as what you did (`self.__dict__.items()`), because I think under the hood `vars()` calls `__dict__`. At this point, that could be a matter of preference or readibility. – Shmack Nov 15 '22 at 07:11
  • 1
    @Shmack yes, it is exactly the same – juanpa.arrivillaga Nov 15 '22 at 07:13
  • 1
    @Shmack guys I just edited and put more functions inside it, I did not know configparser doesn't support correct casting. So tuples will be tuples now, also types gettting compared. – Leonick Nov 15 '22 at 07:20
  • Ok I forgot some datatype checks, not working for all types..... need some more love. – Leonick Nov 15 '22 at 07:34