execsql2 2.0.1__py3-none-any.whl → 2.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. execsql/cli.py +322 -108
  2. execsql/config.py +134 -114
  3. execsql/db/access.py +89 -65
  4. execsql/db/base.py +97 -68
  5. execsql/db/dsn.py +45 -29
  6. execsql/db/duckdb.py +4 -5
  7. execsql/db/factory.py +27 -27
  8. execsql/db/firebird.py +30 -18
  9. execsql/db/mysql.py +38 -14
  10. execsql/db/oracle.py +58 -33
  11. execsql/db/postgres.py +68 -28
  12. execsql/db/sqlite.py +36 -27
  13. execsql/db/sqlserver.py +45 -30
  14. execsql/exceptions.py +68 -64
  15. execsql/exporters/__init__.py +1 -1
  16. execsql/exporters/base.py +42 -17
  17. execsql/exporters/delimited.py +60 -59
  18. execsql/exporters/duckdb.py +8 -12
  19. execsql/exporters/feather.py +32 -24
  20. execsql/exporters/html.py +33 -30
  21. execsql/exporters/json.py +18 -17
  22. execsql/exporters/latex.py +11 -13
  23. execsql/exporters/ods.py +50 -46
  24. execsql/exporters/parquet.py +32 -0
  25. execsql/exporters/pretty.py +16 -15
  26. execsql/exporters/raw.py +9 -11
  27. execsql/exporters/sqlite.py +38 -38
  28. execsql/exporters/templates.py +15 -72
  29. execsql/exporters/values.py +13 -12
  30. execsql/exporters/xls.py +26 -26
  31. execsql/exporters/xml.py +12 -12
  32. execsql/exporters/zip.py +0 -3
  33. execsql/gui/__init__.py +2 -2
  34. execsql/gui/console.py +0 -1
  35. execsql/gui/desktop.py +6 -7
  36. execsql/gui/tui.py +8 -14
  37. execsql/importers/base.py +6 -9
  38. execsql/importers/csv.py +10 -17
  39. execsql/importers/feather.py +16 -22
  40. execsql/importers/ods.py +3 -4
  41. execsql/importers/xls.py +5 -6
  42. execsql/metacommands/__init__.py +8 -8
  43. execsql/metacommands/conditions.py +41 -33
  44. execsql/metacommands/connect.py +113 -99
  45. execsql/metacommands/control.py +38 -26
  46. execsql/metacommands/data.py +35 -33
  47. execsql/metacommands/debug.py +13 -9
  48. execsql/metacommands/io.py +288 -229
  49. execsql/metacommands/prompt.py +179 -157
  50. execsql/metacommands/script_ext.py +11 -9
  51. execsql/metacommands/system.py +44 -25
  52. execsql/models.py +9 -16
  53. execsql/parser.py +10 -10
  54. execsql/script.py +183 -157
  55. execsql/state.py +170 -208
  56. execsql/types.py +46 -81
  57. execsql/utils/auth.py +114 -14
  58. execsql/utils/crypto.py +31 -4
  59. execsql/utils/datetime.py +7 -7
  60. execsql/utils/errors.py +34 -29
  61. execsql/utils/fileio.py +90 -55
  62. execsql/utils/gui.py +22 -23
  63. execsql/utils/mail.py +15 -17
  64. execsql/utils/numeric.py +2 -3
  65. execsql/utils/regex.py +9 -12
  66. execsql/utils/strings.py +10 -12
  67. execsql/utils/timer.py +0 -2
  68. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/execsql.conf +1 -1
  69. execsql2-2.1.2.dist-info/METADATA +300 -0
  70. execsql2-2.1.2.dist-info/RECORD +96 -0
  71. execsql2-2.0.1.dist-info/METADATA +0 -406
  72. execsql2-2.0.1.dist-info/RECORD +0 -95
  73. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/READ_ME.rst +0 -0
  74. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  75. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  76. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/make_config_db.sql +0 -0
  77. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_compare.sql +0 -0
  78. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_glossary.sql +0 -0
  79. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/md_upsert.sql +0 -0
  80. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_compare.sql +0 -0
  81. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  82. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  83. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/script_template.sql +0 -0
  84. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_compare.sql +0 -0
  85. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  86. {execsql2-2.0.1.data → execsql2-2.1.2.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  87. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/WHEEL +0 -0
  88. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/entry_points.txt +0 -0
  89. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/LICENSE.txt +0 -0
  90. {execsql2-2.0.1.dist-info → execsql2-2.1.2.dist-info}/licenses/NOTICE +0 -0
execsql/config.py CHANGED
@@ -16,10 +16,9 @@ Provides three classes:
16
16
  """
17
17
 
18
18
  import os
19
- import os.path
20
- import re
21
19
  import sys
22
20
  from configparser import ConfigParser
21
+ from pathlib import Path
23
22
 
24
23
  from execsql.exceptions import ConfigError
25
24
  from execsql.utils.crypto import Encrypt
@@ -63,6 +62,7 @@ class ConfigData:
63
62
  self.username = None
64
63
  self.access_username = None
65
64
  self.passwd_prompt = True
65
+ self.use_keyring = True
66
66
  self.db_file = None
67
67
  self.new_db = False
68
68
  self.user_logfile = False
@@ -105,10 +105,12 @@ class ConfigData:
105
105
  self.outfile_open_timeout = 600
106
106
  self.quote_all_text = False
107
107
  self.import_row_buffer = 1000
108
+ self.import_progress_interval = 0
108
109
  self.export_row_buffer = 1000
109
110
  self.template_processor = None
110
111
  self.tee_write_log = False
111
112
  self.log_datavars = True
113
+ self.max_log_size_mb = 0
112
114
  self.smtp_host = None
113
115
  self.smtp_port = None
114
116
  self.smtp_username = None
@@ -122,20 +124,20 @@ class ConfigData:
122
124
  self.dao_flush_delay_secs = 5.0
123
125
  self.zip_buffer_mb = 10
124
126
  if os.name == "posix":
125
- sys_config_file = os.path.join("/etc", self.config_file_name)
127
+ sys_config_file = str(Path("/etc") / self.config_file_name)
126
128
  else:
127
- sys_config_file = os.path.join(os.path.expandvars(r"%APPDATA%"), self.config_file_name)
128
- current_script = os.path.abspath(sys.argv[0])
129
- user_config_file = os.path.join(os.path.expanduser(r"~/.config"), self.config_file_name)
130
- script_config_file = os.path.join(script_path, self.config_file_name)
131
- startdir_config_file = os.path.join(os.path.abspath(os.path.curdir), self.config_file_name)
129
+ sys_config_file = str(Path(os.path.expandvars(r"%APPDATA%")) / self.config_file_name)
130
+ current_script = str(Path(sys.argv[0]).resolve())
131
+ user_config_file = str(Path("~/.config").expanduser() / self.config_file_name)
132
+ script_config_file = str(Path(script_path) / self.config_file_name)
133
+ startdir_config_file = str(Path(".").resolve() / self.config_file_name)
132
134
  if startdir_config_file != script_config_file:
133
135
  config_files = [sys_config_file, user_config_file, script_config_file, startdir_config_file]
134
136
  else:
135
137
  config_files = [sys_config_file, user_config_file, startdir_config_file]
136
138
  self.files_read: list = []
137
139
  for ix, configfile in enumerate(config_files):
138
- if configfile not in self.files_read and os.path.isfile(configfile):
140
+ if configfile not in self.files_read and Path(configfile).is_file():
139
141
  self.files_read.append(configfile)
140
142
  cp = ConfigParser()
141
143
  cp.read(configfile)
@@ -155,8 +157,8 @@ class ConfigData:
155
157
  if cp.has_option(self._CONNECT_SECTION, "port"):
156
158
  try:
157
159
  self.port = cp.getint(self._CONNECT_SECTION, "port")
158
- except Exception:
159
- raise ConfigError("Invalid port number.")
160
+ except Exception as e:
161
+ raise ConfigError("Invalid port number.") from e
160
162
  if cp.has_option(self._CONNECT_SECTION, "database"):
161
163
  self.db = cp.get(self._CONNECT_SECTION, "database")
162
164
  if self.db is None:
@@ -174,13 +176,18 @@ class ConfigData:
174
176
  if cp.has_option(self._CONNECT_SECTION, "password_prompt"):
175
177
  try:
176
178
  self.passwd_prompt = cp.getboolean(self._CONNECT_SECTION, "password_prompt")
177
- except Exception:
178
- raise ConfigError("Invalid argument for password_prompt.")
179
+ except Exception as e:
180
+ raise ConfigError("Invalid argument for password_prompt.") from e
181
+ if cp.has_option(self._CONNECT_SECTION, "use_keyring"):
182
+ try:
183
+ self.use_keyring = cp.getboolean(self._CONNECT_SECTION, "use_keyring")
184
+ except Exception as e:
185
+ raise ConfigError("Invalid argument for use_keyring.") from e
179
186
  if cp.has_option(self._CONNECT_SECTION, "new_db"):
180
187
  try:
181
188
  self.new_db = cp.getboolean(self._CONNECT_SECTION, "new_db")
182
- except Exception:
183
- raise ConfigError("Invalid argument for new_db.")
189
+ except Exception as e:
190
+ raise ConfigError("Invalid argument for new_db.") from e
184
191
  if cp.has_option(self._ENCODING_SECTION, "database"):
185
192
  self.db_encoding = cp.get(self._ENCODING_SECTION, "database")
186
193
  if cp.has_option(self._ENCODING_SECTION, "script"):
@@ -203,178 +210,183 @@ class ConfigData:
203
210
  if cp.has_option(self._INPUT_SECTION, "max_int"):
204
211
  try:
205
212
  maxint = cp.getint(self._INPUT_SECTION, "max_int")
206
- except Exception:
207
- raise ConfigError("Invalid argument to max_int.")
213
+ except Exception as e:
214
+ raise ConfigError("Invalid argument to max_int.") from e
208
215
  else:
209
216
  self.max_int = maxint
210
217
  if cp.has_option(self._INPUT_SECTION, "boolean_int"):
211
218
  try:
212
219
  self.boolean_int = cp.getboolean(self._INPUT_SECTION, "boolean_int")
213
- except Exception:
214
- raise ConfigError("Invalid argument to boolean_int.")
220
+ except Exception as e:
221
+ raise ConfigError("Invalid argument to boolean_int.") from e
215
222
  if cp.has_option(self._INPUT_SECTION, "boolean_words"):
216
223
  try:
217
224
  self.boolean_words = cp.getboolean(self._INPUT_SECTION, "boolean_words")
218
- except Exception:
219
- raise ConfigError("Invalid argument to boolean_words.")
225
+ except Exception as e:
226
+ raise ConfigError("Invalid argument to boolean_words.") from e
220
227
  if cp.has_option(self._INPUT_SECTION, "empty_strings"):
221
228
  try:
222
229
  self.empty_strings = cp.getboolean(self._INPUT_SECTION, "empty_strings")
223
- except Exception:
224
- raise ConfigError("Invalid argument to empty_strings.")
230
+ except Exception as e:
231
+ raise ConfigError("Invalid argument to empty_strings.") from e
225
232
  if cp.has_option(self._INPUT_SECTION, "only_strings"):
226
233
  try:
227
- self.all_strings = cp.getboolean(self._INPUT_SECTION, "only_strings")
228
- except Exception:
229
- raise ConfigError("Invalid argument to only_strings.")
234
+ self.only_strings = cp.getboolean(self._INPUT_SECTION, "only_strings")
235
+ except Exception as e:
236
+ raise ConfigError("Invalid argument to only_strings.") from e
230
237
  if cp.has_option(self._INPUT_SECTION, "empty_rows"):
231
238
  try:
232
239
  self.empty_rows = cp.getboolean(self._INPUT_SECTION, "empty_rows")
233
- except Exception:
234
- raise ConfigError("Invalid argument to empty_rows.")
240
+ except Exception as e:
241
+ raise ConfigError("Invalid argument to empty_rows.") from e
235
242
  if cp.has_option(self._INPUT_SECTION, "delete_empty_columns"):
236
243
  try:
237
244
  self.del_empty_cols = cp.getboolean(self._INPUT_SECTION, "delete_empty_columns")
238
- except Exception:
239
- raise ConfigError("Invalid argument to delete_empty_columns.")
245
+ except Exception as e:
246
+ raise ConfigError("Invalid argument to delete_empty_columns.") from e
240
247
  if cp.has_option(self._INPUT_SECTION, "create_column_headers"):
241
248
  try:
242
249
  self.create_col_hdrs = cp.getboolean(self._INPUT_SECTION, "create_column_headers")
243
- except Exception:
244
- raise ConfigError("Invalid argument to create_column_headers.")
250
+ except Exception as e:
251
+ raise ConfigError("Invalid argument to create_column_headers.") from e
245
252
  if cp.has_option(self._INPUT_SECTION, "trim_column_headers"):
246
253
  try:
247
254
  self.trim_col_hdrs = cp.get(self._INPUT_SECTION, "trim_column_headers").lower()
248
- except Exception:
249
- raise ConfigError("Invalid argument to trim_column_headers.")
255
+ except Exception as e:
256
+ raise ConfigError("Invalid argument to trim_column_headers.") from e
250
257
  if self.trim_col_hdrs not in ("none", "both", "left", "right"):
251
258
  raise ConfigError(f"Invalid argument to trim_column_headers: {self.trim_col_hdrs}.")
252
259
  if cp.has_option(self._INPUT_SECTION, "clean_column_headers"):
253
260
  try:
254
261
  self.clean_col_hdrs = cp.getboolean(self._INPUT_SECTION, "clean_column_headers")
255
- except Exception:
256
- raise ConfigError("Invalid argument to clean_column_headers.")
262
+ except Exception as e:
263
+ raise ConfigError("Invalid argument to clean_column_headers.") from e
257
264
  if cp.has_option(self._INPUT_SECTION, "fold_column_headers"):
258
265
  foldspec = cp.get(self._INPUT_SECTION, "fold_column_headers").lower()
259
266
  if foldspec not in ("no", "lower", "upper"):
260
267
  raise ConfigError(f"Invalid argument to fold_column_headers: {foldspec}.")
261
- self.fold_column_headers = foldspec
268
+ self.fold_col_hdrs = foldspec
262
269
  if cp.has_option(self._INPUT_SECTION, "dedup_column_headers"):
263
270
  try:
264
271
  self.dedup_col_hdrs = cp.getboolean(self._INPUT_SECTION, "dedup_column_headers")
265
- except Exception:
266
- raise ConfigError("Invalid argument to dedup_column_headers.")
272
+ except Exception as e:
273
+ raise ConfigError("Invalid argument to dedup_column_headers.") from e
267
274
  if cp.has_option(self._INPUT_SECTION, "trim_strings"):
268
275
  try:
269
276
  self.trim_strings = cp.getboolean(self._INPUT_SECTION, "trim_strings")
270
- except Exception:
271
- raise ConfigError("Invalid argument to trim_strings.")
277
+ except Exception as e:
278
+ raise ConfigError("Invalid argument to trim_strings.") from e
272
279
  if cp.has_option(self._INPUT_SECTION, "replace_newlines"):
273
280
  try:
274
- self.trim_strings = cp.getboolean(self._INPUT_SECTION, "replace_newlines")
275
- except Exception:
276
- raise ConfigError("Invalid argument to replace_newlines.")
281
+ self.replace_newlines = cp.getboolean(self._INPUT_SECTION, "replace_newlines")
282
+ except Exception as e:
283
+ raise ConfigError("Invalid argument to replace_newlines.") from e
277
284
  if cp.has_option(self._INPUT_SECTION, "import_row_buffer"):
278
285
  try:
279
- self.quote_all_text = cp.getint(self._INPUT_SECTION, "import_row_buffer")
280
- except Exception:
281
- raise ConfigError("Invalid argument for import_row_buffer.")
286
+ self.import_row_buffer = cp.getint(self._INPUT_SECTION, "import_row_buffer")
287
+ except Exception as e:
288
+ raise ConfigError("Invalid argument for import_row_buffer.") from e
289
+ if cp.has_option(self._INPUT_SECTION, "import_progress_interval"):
290
+ try:
291
+ self.import_progress_interval = cp.getint(self._INPUT_SECTION, "import_progress_interval")
292
+ except Exception as e:
293
+ raise ConfigError("Invalid argument for import_progress_interval.") from e
282
294
  if cp.has_option(self._INPUT_SECTION, "access_use_numeric"):
283
295
  try:
284
296
  self.access_use_numeric = cp.getboolean(self._INPUT_SECTION, "access_use_numeric")
285
- except Exception:
286
- raise ConfigError("Invalid argument to access_use_numeric.")
297
+ except Exception as e:
298
+ raise ConfigError("Invalid argument to access_use_numeric.") from e
287
299
  if cp.has_option(self._INPUT_SECTION, "import_only_common_columns"):
288
300
  try:
289
301
  self.import_common_cols_only = cp.getboolean(
290
302
  self._INPUT_SECTION,
291
303
  "import_only_common_columns",
292
304
  )
293
- except Exception:
294
- raise ConfigError("Invalid argument to import_only_common_columns.")
305
+ except Exception as e:
306
+ raise ConfigError("Invalid argument to import_only_common_columns.") from e
295
307
  if cp.has_option(self._INPUT_SECTION, "import_common_columns_only"):
296
308
  try:
297
309
  self.import_common_cols_only = cp.getboolean(
298
310
  self._INPUT_SECTION,
299
311
  "import_common_columns_only",
300
312
  )
301
- except Exception:
302
- raise ConfigError("Invalid argument to import_common_columns_only.")
313
+ except Exception as e:
314
+ raise ConfigError("Invalid argument to import_common_columns_only.") from e
303
315
  if cp.has_option(self._INPUT_SECTION, "scan_lines"):
304
316
  try:
305
317
  self.scan_lines = cp.getint(self._INPUT_SECTION, "scan_lines")
306
- except Exception:
307
- raise ConfigError("Invalid argument to scan_lines.")
318
+ except Exception as e:
319
+ raise ConfigError("Invalid argument to scan_lines.") from e
308
320
  if cp.has_option(self._INPUT_SECTION, "import_buffer"):
309
321
  try:
310
322
  self.import_buffer = cp.getint(self._INPUT_SECTION, "import_buffer") * 1024
311
- except Exception:
312
- raise ConfigError("Invalid argument for import_buffer.")
323
+ except Exception as e:
324
+ raise ConfigError("Invalid argument for import_buffer.") from e
313
325
  if cp.has_option(self._OUTPUT_SECTION, "log_write_messages"):
314
326
  try:
315
327
  self.tee_write_log = cp.getboolean(self._OUTPUT_SECTION, "log_write_messages")
316
- except Exception:
317
- raise ConfigError("Invalid argument to log_write_messages")
328
+ except Exception as e:
329
+ raise ConfigError("Invalid argument to log_write_messages") from e
318
330
  if cp.has_option(self._OUTPUT_SECTION, "hdf5_text_len"):
319
331
  try:
320
332
  self.hdf5_text_len = cp.getint(self._OUTPUT_SECTION, "hdf5_text_len")
321
- except Exception:
322
- raise ConfigError("Invalid argument to log_write_messages")
333
+ except Exception as e:
334
+ raise ConfigError("Invalid argument to log_write_messages") from e
323
335
  if cp.has_option(self._OUTPUT_SECTION, "css_file"):
324
336
  self.css_file = cp.get(self._OUTPUT_SECTION, "css_file")
325
337
  if self.css_file is None:
326
338
  raise ConfigError("The css_file name is missing.")
327
339
  if cp.has_option(self._OUTPUT_SECTION, "css_styles"):
328
340
  self.css_styles = cp.get(self._OUTPUT_SECTION, "css_styles")
329
- if self.css_file is None:
341
+ if self.css_styles is None:
330
342
  raise ConfigError("The css_styles are missing.")
331
343
  if cp.has_option(self._OUTPUT_SECTION, "make_export_dirs"):
332
344
  try:
333
345
  self.make_export_dirs = cp.getboolean(self._OUTPUT_SECTION, "make_export_dirs")
334
- except Exception:
335
- raise ConfigError("Invalid argument for make_export_dirs.")
346
+ except Exception as e:
347
+ raise ConfigError("Invalid argument for make_export_dirs.") from e
336
348
  if cp.has_option(self._OUTPUT_SECTION, "quote_all_text"):
337
349
  try:
338
350
  self.quote_all_text = cp.getboolean(self._OUTPUT_SECTION, "quote_all_text")
339
- except Exception:
340
- raise ConfigError("Invalid argument for make_export_dirs.")
351
+ except Exception as e:
352
+ raise ConfigError("Invalid argument for make_export_dirs.") from e
341
353
  if cp.has_option(self._OUTPUT_SECTION, "outfile_open_timeout"):
342
354
  try:
343
355
  self.outfile_open_timeout = cp.getint(self._OUTPUT_SECTION, "outfile_open_timeout")
344
- except Exception:
345
- raise ConfigError("Invalid argument for outfile_open_timeout.")
356
+ except Exception as e:
357
+ raise ConfigError("Invalid argument for outfile_open_timeout.") from e
346
358
  if cp.has_option(self._OUTPUT_SECTION, "export_row_buffer"):
347
359
  try:
348
360
  self.export_row_buffer = cp.getint(self._OUTPUT_SECTION, "export_row_buffer")
349
- except Exception:
350
- raise ConfigError("Invalid argument for export_row_buffer.")
361
+ except Exception as e:
362
+ raise ConfigError("Invalid argument for export_row_buffer.") from e
351
363
  if cp.has_option(self._OUTPUT_SECTION, "template_processor"):
352
364
  tp = cp.get(self._OUTPUT_SECTION, "template_processor").lower()
353
- if tp not in ("jinja", "airspeed"):
365
+ if tp not in ("jinja",):
354
366
  raise ConfigError(f"Invalid template processor name: {tp}")
355
367
  self.template_processor = tp
356
368
  if cp.has_option(self._OUTPUT_SECTION, "zip_buffer_mb"):
357
369
  try:
358
370
  self.zip_buffer_mb = cp.getint(self._OUTPUT_SECTION, "zip_buffer_mb")
359
- except Exception:
360
- raise ConfigError("Invalid argument for zip_buffer_mb.")
371
+ except Exception as e:
372
+ raise ConfigError("Invalid argument for zip_buffer_mb.") from e
361
373
  if cp.has_option(self._INTERFACE_SECTION, "write_warnings"):
362
374
  try:
363
375
  self.write_warnings = cp.getboolean(self._INTERFACE_SECTION, "write_warnings")
364
- except Exception:
365
- raise ConfigError("Invalid argument to write_warnings.")
376
+ except Exception as e:
377
+ raise ConfigError("Invalid argument to write_warnings.") from e
366
378
  if cp.has_option(self._INTERFACE_SECTION, "write_prefix"):
367
379
  try:
368
380
  self.write_prefix = cp.get(self._INTERFACE_SECTION, "write_prefix")
369
- except Exception:
370
- raise ConfigError("Invalid or missing argument to write_prefix.")
381
+ except Exception as e:
382
+ raise ConfigError("Invalid or missing argument to write_prefix.") from e
371
383
  if self.write_prefix.lower() == "clear":
372
384
  self.write_prefix = None
373
385
  if cp.has_option(self._INTERFACE_SECTION, "write_suffix"):
374
386
  try:
375
387
  self.write_suffix = cp.get(self._INTERFACE_SECTION, "write_suffix")
376
- except Exception:
377
- raise ConfigError("Invalid or missing argument to write_suffix.")
388
+ except Exception as e:
389
+ raise ConfigError("Invalid or missing argument to write_suffix.") from e
378
390
  if self.write_suffix.lower() == "clear":
379
391
  self.write_suffix = None
380
392
  if cp.has_option(self._INTERFACE_SECTION, "gui_level"):
@@ -389,57 +401,57 @@ class ConfigData:
389
401
  if cp.has_option(self._INTERFACE_SECTION, "console_height"):
390
402
  try:
391
403
  self.gui_console_height = max(5, cp.getint(self._INTERFACE_SECTION, "console_height"))
392
- except Exception:
393
- raise ConfigError("Invalid argument for console_height.")
404
+ except Exception as e:
405
+ raise ConfigError("Invalid argument for console_height.") from e
394
406
  if cp.has_option(self._INTERFACE_SECTION, "console_width"):
395
407
  try:
396
408
  self.gui_console_width = max(20, cp.getint(self._INTERFACE_SECTION, "console_width"))
397
- except Exception:
398
- raise ConfigError("Invalid argument for console_width.")
409
+ except Exception as e:
410
+ raise ConfigError("Invalid argument for console_width.") from e
399
411
  if cp.has_option(self._INTERFACE_SECTION, "console_wait_when_done"):
400
412
  try:
401
413
  self.gui_wait_on_exit = cp.getboolean(self._INTERFACE_SECTION, "console_wait_when_done")
402
- except Exception:
403
- raise ConfigError("Invalid argument for console_wait_when_done.")
414
+ except Exception as e:
415
+ raise ConfigError("Invalid argument for console_wait_when_done.") from e
404
416
  if cp.has_option(self._INTERFACE_SECTION, "console_wait_when_error_halt"):
405
417
  try:
406
418
  self.gui_wait_on_error_halt = cp.getboolean(
407
419
  self._INTERFACE_SECTION,
408
420
  "console_wait_when_error_halt",
409
421
  )
410
- except Exception:
411
- raise ConfigError("Invalid argument for console_wait_when_error_halt.")
422
+ except Exception as e:
423
+ raise ConfigError("Invalid argument for console_wait_when_error_halt.") from e
412
424
  if cp.has_option(self._CONFIG_SECTION, "config_file"):
413
425
  conffile = cp.get(self._CONFIG_SECTION, "config_file")
414
426
  if os.name == "posix" and conffile[0] == "~":
415
427
  if len(conffile) == 1:
416
- conffile = os.path.expanduser(r"~")
428
+ conffile = str(Path("~").expanduser())
417
429
  elif len(conffile) > 1 and conffile[1] == os.sep:
418
- conffile = os.path.join(os.path.expanduser(r"~"), conffile[2:])
430
+ conffile = str(Path("~").expanduser() / conffile[2:])
419
431
  conffile = variable_pool.substitute(conffile)[0]
420
- if not os.path.isfile(conffile):
421
- conffile = os.path.join(conffile, self.config_file_name)
422
- if os.path.isfile(conffile):
432
+ if not Path(conffile).is_file():
433
+ conffile = str(Path(conffile) / self.config_file_name)
434
+ if Path(conffile).is_file():
423
435
  # Silently ignore a non-existent file, for cross-OS compatibility.
424
436
  config_files.insert(ix + 1, conffile)
425
437
  if os.name == "posix" and cp.has_option(self._CONFIG_SECTION, "linux_config_file"):
426
438
  conffile = cp.get(self._CONFIG_SECTION, "linux_config_file")
427
439
  if conffile[0] == "~":
428
440
  if len(conffile) == 1:
429
- conffile = os.path.expanduser(r"~")
441
+ conffile = str(Path("~").expanduser())
430
442
  elif len(conffile) > 1 and conffile[1] == os.sep:
431
- conffile = os.path.join(os.path.expanduser(r"~"), conffile[2:])
443
+ conffile = str(Path("~").expanduser() / conffile[2:])
432
444
  conffile = variable_pool.substitute(conffile)[0]
433
- if not os.path.isfile(conffile):
434
- conffile = os.path.join(conffile, self.config_file_name)
435
- if os.path.isfile(conffile):
445
+ if not Path(conffile).is_file():
446
+ conffile = str(Path(conffile) / self.config_file_name)
447
+ if Path(conffile).is_file():
436
448
  config_files.insert(ix + 1, conffile)
437
449
  if os.name == "windows" and cp.has_option(self._CONFIG_SECTION, "win_config_file"):
438
450
  conffile = cp.get(self._CONFIG_SECTION, "win_config_file")
439
451
  conffile = variable_pool.substitute(conffile)[0]
440
- if not os.path.isfile(conffile):
441
- conffile = os.path.join(conffile, self.config_file_name)
442
- if os.path.isfile(conffile):
452
+ if not Path(conffile).is_file():
453
+ conffile = str(Path(conffile) / self.config_file_name)
454
+ if Path(conffile).is_file():
443
455
  config_files.insert(ix + 1, conffile)
444
456
  if cp.has_option(self._CONFIG_SECTION, "user_logfile"):
445
457
  self.user_logfile = cp.getboolean(self._CONFIG_SECTION, "user_logfile")
@@ -452,16 +464,21 @@ class ConfigData:
452
464
  if cp.has_option(self._CONFIG_SECTION, "log_datavars"):
453
465
  try:
454
466
  self.log_datavars = cp.getboolean(self._CONFIG_SECTION, "log_datavars")
455
- except Exception:
456
- raise ConfigError("Invalid argument to log_datavars setting.")
467
+ except Exception as e:
468
+ raise ConfigError("Invalid argument to log_datavars setting.") from e
469
+ if cp.has_option(self._CONFIG_SECTION, "max_log_size_mb"):
470
+ try:
471
+ self.max_log_size_mb = cp.getint(self._CONFIG_SECTION, "max_log_size_mb")
472
+ except Exception as e:
473
+ raise ConfigError("Invalid argument to max_log_size_mb setting.") from e
457
474
  if cp.has_option(self._EMAIL_SECTION, "host"):
458
475
  self.smtp_host = cp.get(self._EMAIL_SECTION, "host")
459
476
  if cp.has_option(self._EMAIL_SECTION, "port"):
460
477
  self.smtp_port = cp.get(self._EMAIL_SECTION, "port")
461
478
  try:
462
479
  self.smtp_port = cp.getint(self._EMAIL_SECTION, "port")
463
- except Exception:
464
- raise ConfigError("Invalid argument for email port.")
480
+ except Exception as e:
481
+ raise ConfigError("Invalid argument for email port.") from e
465
482
  if cp.has_option(self._EMAIL_SECTION, "username"):
466
483
  self.smtp_username = cp.get(self._EMAIL_SECTION, "username")
467
484
  if cp.has_option(self._EMAIL_SECTION, "password"):
@@ -471,13 +488,13 @@ class ConfigData:
471
488
  if cp.has_option(self._EMAIL_SECTION, "use_ssl"):
472
489
  try:
473
490
  self.smtp_ssl = cp.getboolean(self._EMAIL_SECTION, "use_ssl")
474
- except Exception:
475
- raise ConfigError("Invalid argument for email use_ssl.")
491
+ except Exception as e:
492
+ raise ConfigError("Invalid argument for email use_ssl.") from e
476
493
  if cp.has_option(self._EMAIL_SECTION, "use_tls"):
477
494
  try:
478
495
  self.smtp_tls = cp.getboolean(self._EMAIL_SECTION, "use_tls")
479
- except Exception:
480
- raise ConfigError("Invalid argument for email use_tls.")
496
+ except Exception as e:
497
+ raise ConfigError("Invalid argument for email use_tls.") from e
481
498
  if cp.has_option(self._EMAIL_SECTION, "email_format"):
482
499
  fmt = cp.get(self._EMAIL_SECTION, "email_format").lower()
483
500
  if fmt not in ("plain", "html"):
@@ -494,23 +511,26 @@ class ConfigData:
494
511
  if cp.has_section(self._INCLUDE_REQ_SECTION):
495
512
  imp_items = cp.items(self._INCLUDE_REQ_SECTION)
496
513
  ord_items = sorted([(int(i[0]), i[1]) for i in imp_items], key=lambda x: x[0])
497
- newfiles = [os.path.abspath(f[1]) for f in ord_items]
514
+ newfiles = [str(Path(f[1]).resolve()) for f in ord_items]
498
515
  u_files = []
499
516
  for f in newfiles:
500
517
  if not (f in u_files or f in self.include_req or f in self.include_opt) and f != current_script:
501
- if not os.path.exists(f):
518
+ if not Path(f).exists():
502
519
  raise ConfigError(f"Required include file {f} does not exist.")
503
520
  u_files.append(f)
504
521
  self.include_req.extend(u_files)
505
522
  if cp.has_section(self._INCLUDE_OPT_SECTION):
506
523
  imp_items = cp.items(self._INCLUDE_OPT_SECTION)
507
524
  ord_items = sorted([(int(i[0]), i[1]) for i in imp_items], key=lambda x: x[0])
508
- newfiles = [os.path.abspath(f[1]) for f in ord_items]
525
+ newfiles = [str(Path(f[1]).resolve()) for f in ord_items]
509
526
  u_files = []
510
527
  for f in newfiles:
511
- if not (f in u_files or f in self.include_req or f in self.include_opt) and f != current_script:
512
- if os.path.exists(f):
513
- u_files.append(f)
528
+ if (
529
+ not (f in u_files or f in self.include_req or f in self.include_opt)
530
+ and f != current_script
531
+ and Path(f).exists()
532
+ ):
533
+ u_files.append(f)
514
534
  self.include_opt.extend(u_files)
515
535
 
516
536