libtextworker 0.1.4
Cross-platform, free and open library for Python projects
get_config.py
1 """
2 @package libtextworker.get_config
3 @brief Contains classes for generic INI files parsing
4 
5 See the documentation in /usage/getconfig.
6 """
7 
8 # A cross-platform library for Python apps.
9 # Copyright (C) 2023-2024 Le Bao Nguyen and contributors.
10 # This is a part of the libtextworker project.
11 # Licensed under the GNU General Public License version 3.0 or later.
12 
13 import json
14 import os
15 import typing
16 
17 from .general import WalkCreation, libTewException
18 from warnings import warn
19 from watchdog.observers import Observer
20 from watchdog.events import *
21 
22 __all__ = ["ConfigurationError", "GetConfig"]
23 
24 try:
25  from commentedconfigparser import CommentedConfigParser as ConfigParser
26 except ImportError:
27  from configparser import ConfigParser
28 
30  def __init__(this, path: str, msg: str, section: str,
31  option: str = "\\not_specified\\"):
32 
33  full = "Configuration file {}, path [{}->{}]: {}"
34  full = full.format(path, section, option, msg)
35  libTewException.__init__(this, full)
36 
37 class GetConfig(ConfigParser, FileSystemEventHandler):
38  # Values
39  yes_values: list = ["yes", "True", True, "1", 1, "on"]
40  no_values: list = ["no", "False", False, "0", 0, "off"]
41 
42  aliases: dict = {}
43  backups: dict = {}
44  cfg: dict = {}
45  detailedlogs: bool = True
46 
47  _observer = Observer()
48 
49  for item in yes_values:
50  aliases[item] = True
51 
52  for item in no_values:
53  aliases[item] = False
54 
55  def __init__(this, config: dict[str] | str | None, file: str, **kwds):
56  """
57  A customized INI file parser.
58  @param config (dict[str] or str) : Your stock settings, used to reset the file or do some comparisions
59  @param file : Configuration file
60  @param **kwds : To pass to configparser.ConfigParser (base class)
61 
62  When initialized, GetConfig loads all default configs (from config parameter) and store it in
63  a dictionary for further actions (backup/restore file).
64 
65  @since 0.1.3: Allow config parameter as a str object
66  @since 0.1.4: JSON support, allow config parameter to be None, file system watch
67  """
68  ConfigParser.__init__(this, **kwds)
69 
70  if isinstance(config, str):
71  this.read_string(config)
72  elif isinstance(config, dict):
73  this.read_dict(config)
74 
75  if config != None:
76  for key in this:
77  this.cfg[key] = this[key]
78 
79  this.readf(file)
80 
81  # File tasks
82  def readf(this, file: str, encoding: str | None = None):
83  """
84  Reads all settings from a file.
85  Mostly for application/GetConfig internal use.
86  """
87  WalkCreation(os.path.dirname(file))
88  if not os.path.exists(file):
89  this.write(open(file, "w"))
90  else:
91  try:
92  this.read_dict(json.loads(open(file, "r").read()))
93  except:
94  this.read(file, encoding)
95 
96  this._file = file
97  this._observer.schedule(this, file)
98  this._observer.start()
99 
100  def __del__(this):
101  if this._observer.is_alive():
102  this._observer.stop()
103  this._observer.join()
104 
105  def reset(this, restore: bool = False):
106  """
107  Loads default settings to GetConfig and loaded file.
108  Also restore backups if restore is True and GetConfig.backups is not empty.
109  """
110  os.remove(this._file)
111  for key in this.cfg:
112  this[key] = this.cfg[key]
113 
114  if restore and this.backups:
115  for key in this.backups:
116  this[key] = this.backups[key]
117 
118  this.update()
119 
120  def update(this):
121  """
122  Writes current settings to loaded file.
123  """
124  with open(this._file, "w") as f:
125  this.write(f)
126  this.read(this._file)
127 
128  # Options
129  def backup(this, keys: dict, direct_to_keys: bool = False) -> dict:
130  """
131  Backs up user data, specified by the keys parameter (dictionary).
132  Returns the successfully *updated* dictionary (if direct_to_keys is True),
133  or the self-made dict.
134  """
135  target = keys if direct_to_keys else this.backups
136  for key in keys:
137  for subelm in keys[key]:
138  target[key][subelm] = this[key][subelm]
139 
140  return target
141 
142  def full_backup(this, noFile: bool, path: str, use_json: bool = False):
143  """
144  @since 0.1.4
145  Backup all settings by writing to GetConfig.backups and/or another file.
146  @param noFile (bool): Don't write to any file else.
147  @param path (str): Target backup file
148  @param use_json (bool = False): Use the backup file in JSON format
149  """
150  if path == this._file:
151  raise Exception("GetConfig.full_backup: filepath must be the loaded file!")
152 
153  for section in this.sections():
154  this.backups[section] = this[section]
155 
156  if not noFile:
157  with open(path, "w") as f:
158  if use_json:
159  json.dump(this, f)
160  else:
161  this.write(f)
162 
163  def restore(this, keys: dict[str, str] | None, optional_path: str):
164  """
165  @since 0.1.4
166  Restore options.
167  @param keys (dict[str, str] or None): Keys + options to restore.
168  Optional but self.backups must not be empty.
169  @param optional_path (str): The name says it all. If specified,
170  both this path and self._file will be written.
171  You can also use move() function for a more complex method.
172  """
173 
174  if not keys and this.backups:
175  raise AttributeError(
176  "GetConfig.restore: this.backups and keys parameter are empty/died"
177  )
178  for key in keys:
179  for option in keys[key]:
180  this.set_and_update(key, option, keys[key][option])
181 
182  if optional_path:
183  with open(optional_path, "w") as f:
184  this.write(f)
185 
186  def getkey(this, section: str, option: str, needed: bool = False,
187  make: bool = False, noraiseexp: bool = False, raw: bool = False) -> typing.Any | None:
188  """
189  Try to get the value of an option under the spectified section.
190 
191  @param section, option: Target section->option
192  @param needed (boolean=False): The target option is needed - should use with make & noraiseexp.
193  @param make (boolean=False): Create the option if it is not found from the search
194  @param noraiseexp (boolean=False): Make getkey() raise exceptions or not (when neccessary)
195  @param raw (boolean=False): Don't use aliases for the value we get.
196 
197  @return False if the option does not exist and needed parameter set to False.
198  """
199 
200  def bringitback() -> typing.Any | None:
201  target = this.backups
202  value_: typing.Any
203 
204  if make:
205  if not target:
206  target = this.cfg
207  if not target[section]:
208  raise ConfigurationError(this._file, "Unable to find the section in both GetConfig.backups and GetConfig.cfg!",
209  section, option)
210  if not target[section][option]:
211  raise ConfigurationError(this._file, "Unable to find the option in both GetConfig.backups and GetConfig.cfg!",
212  section, option)
213  if not section in this.sections():
214  this.add_section(section)
215  value_ = target[section][option]
216  if needed:
217  this.set_and_update(section, option, value_)
218  else:
219  this.set(section, option, value_)
220  return value_
221 
222  try:
223  value = this.get(section, option)
224  except:
225  if noraiseexp:
226  value = bringitback()
227  if not value:
228  return None
229  else:
230  raise ConfigurationError(this._file, "Section or option not found", section, option)
231 
232  # Remove ' / "
233  trans = ["'", '"']
234  for key in trans:
235  value = value.removeprefix(key).removesuffix(key)
236 
237  if not value in this.aliases or raw is True:
238  return value
239  else:
240  return this.aliases[value]
241 
242  def aliasyesno(this, yesvalue=None, novalue=None):
243  if yesvalue:
244  this.yes_values.append(yesvalue)
245  this.aliases[yesvalue] = True
246 
247  if novalue:
248  this.no_values.append(novalue)
249  this.aliases[novalue] = False
250 
251  def alias(this, value, value2):
252  this.aliases[value] = value2
253 
254  def move(this, list_: dict[str, dict[str, str]]):
255  """
256  @since 0.1.3
257 
258  Move configurations found from the file that GetConfig currently uses.
259  Below is an example:
260  ```
261  move(
262  list_={
263  "section1->option1": {
264  "newpath": "section_one->option1",
265  "file": "unchanged"
266  },
267  "special->option0": {
268  "newpath": "special_thing->option0",
269  "file": "~/.config/test.ini"
270  }
271  }
272  )
273  ```
274 
275  "list_" is a dictionary - each key is a dictionary which defines how a setting should be moved.
276  The key name is your setting (section->option). Under that 'key' is 2 options:
277  * "newpath" specifies the location of the setting in the target file (section->option)
278  * "file" specifies the location of the target file we'll move the options to. Ignore it or leave it "unchanged" to tell
279  the function that you don't want to move the setting else where than the current file.
280  * "delete_entire_section" (ignorable, values are 'yes' and 'no') allows you to remove the old section after the move.
281  """
282 
283  for section in list_.keys():
284  # Prepare for the move
285  section_ = section.split("->")[0]
286  option_ = section.split("->")[1]
287 
288  if not section_ in this.sections():
289  raise ConfigurationError(this._file, "Section not found", section_)
290  if not option_ in this[section]:
291  raise ConfigurationError(this._file, "Option not found", section_, option_)
292 
293  value = this[section_][option_]
294 
295  # Start
296  newsection = list_[section]["newpath"].split("->")[0]
297  newoption = list_[section]["newpath"].split("->")[1]
298  if not "file" in list_[section] or list_[section]["file"] == "unchanged":
299  if not newsection in this.sections():
300  this.add_section(newsection)
301  this.set_and_update(newsection, newoption, value)
302  else:
303  newobj = GetConfig(None, list_[section]["file"])
304  newobj.add_section(newsection)
305  newobj.set_and_update(newsection, newoption, value)
306 
307  if "delete_entire_section" in list_[section] \
308  and list_[section_]["delete_entire_section"] in this.yes_values:
309  this.remove_section(section_)
310 
311  def set_and_update(this, section: str, option: str, value: str | None = None):
312  """
313  @since 0.1.3
314  Set an option, and eventually apply it to the file.
315  """
316  this.set(section, option, value)
317  this.update()
318 
319  """
320  FileSystemEventHandler
321  """
322 
323  def on_any_event(this, event: FileSystemEvent):
324  assert event.src_path == this._file
325  assert event.is_directory == False
326 
327  if isinstance(event, (FileModifiedEvent, FileCreatedEvent)):
328  this.readf(event.src_path)
329  elif isinstance(event, (FileOpenedEvent, FileClosedEvent)):
330  return
331  else:
332  warn(f"{event.src_path} has gone!")
333 
334 
335 
def __init__(this, dict[str]|str|None config, str file, **kwds)
Definition: get_config.py:55
def restore(this, dict[str, str]|None keys, str optional_path)
Definition: get_config.py:163
typing.Any|None getkey(this, str section, str option, bool needed=False, bool make=False, bool noraiseexp=False, bool raw=False)
Definition: get_config.py:187
def reset(this, bool restore=False)
Definition: get_config.py:105
dict backup(this, dict keys, bool direct_to_keys=False)
Definition: get_config.py:129
def set_and_update(this, str section, str option, str|None value=None)
Definition: get_config.py:311
def move(this, dict[str, dict[str, str]] list_)
Definition: get_config.py:254
def full_backup(this, bool noFile, str path, bool use_json=False)
Definition: get_config.py:142
def readf(this, str file, str|None encoding=None)
Definition: get_config.py:82