I am currently trying to learn a new method of programming. I am creating a Singleton class to help manage my config file, which is used across multiple areas of my code. The code that I'm using is below, but my main goal is to create a single instance of this class, and it's called within my code and pull information through when it is needed. Instead of attaching the config parser to a variable that would create an instance named config, I wanted to inherit the class and implement it myself inside my class. I understand typically, I would pull this information in and use a __init__, but in this case, I am using a __new__ and controlling the creation of this class. When i change the _initialize to __init__ i can get the code to work but from my understanding, I would create a new instance every time i call my class, instead of controlling __init__ with __new__. I guess the questions are: am I approaching this the right way, and am I missing something?
Error
File "...\Documents\GitHub\Strontium-Carbon-Version2\SrC\test_functions.py", line 89, in <module>
config = ConfigManager(config_path)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "...\Programs\Python\Python312\Lib\configparser.py", line 625, in __init__
self._read_defaults(defaults)
File "...\AppData\Local\Programs\Python\Python312\Lib\configparser.py", line 1200, in _read_defaults
self.read_dict({self.default_section: defaults})
File "...\AppData\Local\Programs\Python\Python312\Lib\configparser.py", line 734, in read_dict
for key, value in keys.items():
^^^^^^^^^^
AttributeError: 'str' object has no attribute 'items'
import os
import configparser
class ConfigManager(configparser.ConfigParser):
"""
Creating a singleton class to manage the inherited use of the ConfigParser as a global class without the need to
pull this class information through multiple classes. Once the class has been called (within the main
program function), the information can be pulled when needed.
"""
_instance = None # Singleton instance
def __new__(cls, config_path: str = None) -> 'ConfigManager':
"""
Using the special method __new__ to control the creation of a new instance of the ConfigManager Class.
Granting more control of the creation of an instance (when you call the class). Using the Singleton
pattern design with the __new__ method allows me to create one instance of this class for the program
and if one is already created, return that instance.
"""
if cls._instance is None:
cls._instance = super(ConfigManager, cls).__new__(cls)
cls._instance._initialize(config_path) # Initialize config on first creation
return cls._instance
def _initialize(self, config_path: str = None):
"""
This method is designed to set up the configuration by loading the config file when the class is
instantiated for the first time. If the config file doesn't exist, it creates a default configuration
file.
"""
super().__init__() # Call the ConfigParser __init__ method
configparser.ConfigParser.__init__(self)
if config_path is None:
raise ValueError('No path inputted')
if not os.path.exists(config_path):
print(f"Config file not found at {config_path}.")
else:
self.config_path = config_path
self.read(config_path)
def get_path(self, category: str, path: str) -> str:
"""
Retrieves a specific path from the configuration file.
:param category: The section or category in the config file (e.g., 'Path Location').
:type category: str
:param path: The option or path name under the category (e.g., 'main_folder').
:type path: str
:return: The value (path) associated with the category and path name.
:rtype: str
:Example:
>>> config_manager.get_path('Path Location', 'main_folder')
'~/Documents/test folder'
"""
try:
return self.get(category, path)
except (configparser.NoSectionError, configparser.NoOptionError) as e:
print(f"Error: {e}")
return None
def path_options(self) -> dict:
"""
Retrieves all categories (sections) and their associated paths (options) as a dictionary.
"""
all_paths = {}
for section in self.sections():
all_paths[section] = dict(self.items(section)) # Convert each section to a dict
return all_paths
def _create_default_config(self, file_locations: dict):
"""Creates a default configuration file if one does not exist."""
self['Path Location'] = file_locations
with open(self.config_path, 'w') as configfile:
self.write(configfile)
def save(self):
"""Saves the current configuration to the config file."""
with open(self.config_path, 'w') as configfile:
self.write(configfile)
# Main program to test the class
if __name__ == "__main__":
config_path = os.path.expanduser(r"~\Documents\test folder\Program files\config.ini")
config = ConfigManager(config_path)
print(config.get_path('Path Location', 'main_file'))
My expectation is is that this would access the config file and return a string that is associated with that specific config location.
configparser.ConfigParser.__init__(self)twice: Once usingsuper().__init__()and then with the explicit call.ConfigManager.__new__returns an instance ofConfigManager,type.__call__is going to invokeConfigManager.__init__, even if it's not a newly created instance. If you really want a singleton, use a separate class method (or even a standalone function that manages a "hidden" global variable) to enforce the uniqueness.