qplotter 1.6.0b3__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 (119) hide show
  1. qplot/__init__.py +46 -0
  2. qplot/__main__.py +232 -0
  3. qplot/_auxiliary_paths.py +63 -0
  4. qplot/_metadata.py +12 -0
  5. qplot/_repair.py +14 -0
  6. qplot/_shutdown_supervisor.py +4049 -0
  7. qplot/_version.py +12 -0
  8. qplot/_windows_shutdown_job.py +1166 -0
  9. qplot/configuration/__init__.py +3 -0
  10. qplot/configuration/config.py +573 -0
  11. qplot/configuration/config_schema.json +319 -0
  12. qplot/configuration/scripts.py +255 -0
  13. qplot/configuration/themes/__init__.py +13 -0
  14. qplot/configuration/themes/_base.py +579 -0
  15. qplot/configuration/themes/blank.py +17 -0
  16. qplot/configuration/themes/dark.py +54 -0
  17. qplot/configuration/themes/light.py +54 -0
  18. qplot/datahandling/LoadFromDB.py +224 -0
  19. qplot/datahandling/__init__.py +47 -0
  20. qplot/datahandling/_trusted_live_helper.py +812 -0
  21. qplot/datahandling/_trusted_live_protocol.py +1745 -0
  22. qplot/datahandling/database.py +2330 -0
  23. qplot/datahandling/dimensions.py +43 -0
  24. qplot/datahandling/file_identity.py +461 -0
  25. qplot/datahandling/parameter_data.py +149 -0
  26. qplot/datahandling/plot_progress.py +17 -0
  27. qplot/datahandling/qcodes_cache.py +277 -0
  28. qplot/datahandling/qcodes_compat.py +14 -0
  29. qplot/datahandling/readSQL.py +2442 -0
  30. qplot/datahandling/readonly.py +2561 -0
  31. qplot/datahandling/source_activity.py +21 -0
  32. qplot/datahandling/trusted_array_preview.py +127 -0
  33. qplot/datahandling/trusted_derived_cache.py +1605 -0
  34. qplot/datahandling/trusted_derived_rendering.py +868 -0
  35. qplot/datahandling/trusted_heatmap.py +241 -0
  36. qplot/datahandling/trusted_live.py +2808 -0
  37. qplot/datahandling/trusted_live_queries.py +5907 -0
  38. qplot/datahandling/trusted_live_service.py +1894 -0
  39. qplot/datahandling/trusted_live_supervisor.py +2161 -0
  40. qplot/datahandling/trusted_plot.py +283 -0
  41. qplot/datahandling/trusted_presentation.py +1065 -0
  42. qplot/datahandling/trusted_snapshot.py +1132 -0
  43. qplot/datahandling/trusted_work_coordinator.py +2157 -0
  44. qplot/datahandling/trusted_work_scheduler.py +1204 -0
  45. qplot/diagnostics.py +241 -0
  46. qplot/resources/testdata/qplot_test_db_01_10mb.csv +11 -0
  47. qplot/resources/testdata/qplot_test_db_02_25mb.csv +14 -0
  48. qplot/resources/testdata/qplot_test_db_03_50mb.csv +17 -0
  49. qplot/resources/testdata/qplot_test_db_04_100mb.csv +20 -0
  50. qplot/resources/testdata/qplot_test_db_05_250mb.csv +23 -0
  51. qplot/resources/testdata/qplot_test_db_06_500mb.csv +26 -0
  52. qplot/resources/testdata/qplot_test_db_07_1gb.csv +29 -0
  53. qplot/resources/testdata/qplot_test_db_08_5gb.csv +32 -0
  54. qplot/resources/testdata/qplot_test_db_09_10gb.csv +35 -0
  55. qplot/resources/testdata/qplot_test_db_10_30gb.csv +38 -0
  56. qplot/testdata.py +1733 -0
  57. qplot/tools/__init__.py +13 -0
  58. qplot/tools/finite_means.py +114 -0
  59. qplot/tools/general.py +134 -0
  60. qplot/tools/heatmap_geometry.py +381 -0
  61. qplot/tools/operation_registry.py +179 -0
  62. qplot/tools/plot_tools.py +625 -0
  63. qplot/tools/sample_statistics.py +56 -0
  64. qplot/tools/worker.py +2787 -0
  65. qplot/windows/__init__.py +9 -0
  66. qplot/windows/_colorbar.py +422 -0
  67. qplot/windows/_commands.py +523 -0
  68. qplot/windows/_config_persistence.py +123 -0
  69. qplot/windows/_database_actions.py +4904 -0
  70. qplot/windows/_dataset_handle.py +221 -0
  71. qplot/windows/_dragdrop.py +71 -0
  72. qplot/windows/_export_paths.py +1167 -0
  73. qplot/windows/_help.py +169 -0
  74. qplot/windows/_native_averaging.py +173 -0
  75. qplot/windows/_native_transforms.py +389 -0
  76. qplot/windows/_plot1d_snap.py +608 -0
  77. qplot/windows/_plot1d_traces.py +2183 -0
  78. qplot/windows/_plot2d_colorbar.py +1126 -0
  79. qplot/windows/_plot2d_colorbar_dialog.py +786 -0
  80. qplot/windows/_plot2d_layers.py +2197 -0
  81. qplot/windows/_plot2d_sweeps.py +712 -0
  82. qplot/windows/_plotWin.py +1993 -0
  83. qplot/windows/_plot_actions.py +2695 -0
  84. qplot/windows/_plot_appearance.py +276 -0
  85. qplot/windows/_plot_axis_scaling.py +1485 -0
  86. qplot/windows/_plot_export.py +1445 -0
  87. qplot/windows/_plot_feedback.py +110 -0
  88. qplot/windows/_plot_marquee.py +656 -0
  89. qplot/windows/_plot_refresh.py +942 -0
  90. qplot/windows/_plot_state.py +239 -0
  91. qplot/windows/_plot_transform_labels.py +60 -0
  92. qplot/windows/_preferences.py +635 -0
  93. qplot/windows/_refresh_interval.py +70 -0
  94. qplot/windows/_run_controls.py +828 -0
  95. qplot/windows/_shortcuts.py +47 -0
  96. qplot/windows/_subplots/__init__.py +8 -0
  97. qplot/windows/_subplots/subplot1d.py +531 -0
  98. qplot/windows/_subplots/subplot2d.py +918 -0
  99. qplot/windows/_svg_export.py +209 -0
  100. qplot/windows/_trusted_derived_qt.py +1759 -0
  101. qplot/windows/_widgets/__init__.py +23 -0
  102. qplot/windows/_widgets/_run_formatting.py +387 -0
  103. qplot/windows/_widgets/details_tables.py +1119 -0
  104. qplot/windows/_widgets/dropbox.py +290 -0
  105. qplot/windows/_widgets/operations.py +460 -0
  106. qplot/windows/_widgets/preview.py +2997 -0
  107. qplot/windows/_widgets/run_list_items.py +488 -0
  108. qplot/windows/_widgets/toolbar.py +111 -0
  109. qplot/windows/_widgets/treeWidgets.py +2572 -0
  110. qplot/windows/_window_controls.py +413 -0
  111. qplot/windows/main.py +1635 -0
  112. qplot/windows/plot1d.py +311 -0
  113. qplot/windows/plot2d.py +1901 -0
  114. qplotter-1.6.0b3.dist-info/METADATA +318 -0
  115. qplotter-1.6.0b3.dist-info/RECORD +119 -0
  116. qplotter-1.6.0b3.dist-info/WHEEL +5 -0
  117. qplotter-1.6.0b3.dist-info/entry_points.txt +4 -0
  118. qplotter-1.6.0b3.dist-info/licenses/LICENSE +21 -0
  119. qplotter-1.6.0b3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
1
+ """
2
+ Imported in src.__init__
3
+ """
@@ -0,0 +1,573 @@
1
+ import json
2
+ import math
3
+ import os
4
+ import stat
5
+ import tempfile
6
+ from copy import deepcopy
7
+ from importlib.resources import files
8
+ from os import makedirs, path
9
+ from shutil import copyfileobj
10
+
11
+ import jsonschema
12
+
13
+ from qplot._auxiliary_paths import ensure_safe_auxiliary_path
14
+ from qplot.diagnostics import log_exception
15
+
16
+ from .themes import dark, light, pyqt
17
+
18
+ THEME_CLASSES = {
19
+ "light": light,
20
+ "dark": dark,
21
+ "pyqt": pyqt,
22
+ }
23
+
24
+
25
+ def _strict_integer(_checker, instance):
26
+ return isinstance(instance, int) and not isinstance(instance, bool)
27
+
28
+
29
+ def _finite_number(_checker, instance):
30
+ if isinstance(instance, bool) or not isinstance(instance, (int, float)):
31
+ return False
32
+ try:
33
+ return math.isfinite(instance)
34
+ except (OverflowError, TypeError, ValueError):
35
+ return False
36
+
37
+
38
+ _STRICT_TYPE_CHECKER = (
39
+ jsonschema.Draft202012Validator.TYPE_CHECKER
40
+ .redefine("integer", _strict_integer)
41
+ .redefine("number", _finite_number)
42
+ )
43
+ StrictConfigValidator = jsonschema.validators.extend(
44
+ jsonschema.Draft202012Validator,
45
+ type_checker=_STRICT_TYPE_CHECKER,
46
+ )
47
+
48
+
49
+ def _reject_json_constant(value):
50
+ raise json.JSONDecodeError(
51
+ f"Non-standard JSON numeric constant {value!r}",
52
+ value,
53
+ 0,
54
+ )
55
+
56
+ class config:
57
+ """
58
+ Class for reading and writing to the config.json file.
59
+
60
+ **NOTE, config search functions are only set up for 1 level of nested
61
+ dictionaries and will require edits or work arounds after that.
62
+
63
+ see self.default_file for config location
64
+
65
+ .config_schema.json control defaults and restrictions on what can be changed
66
+ """
67
+ config_file_name = "config.json"
68
+ schema_file_name = "config_schema.json"
69
+
70
+ default_path = path.expanduser(
71
+ path.join("~", ".qplot")
72
+ )
73
+ default_file = path.join(default_path, config_file_name)
74
+ default__schema_file = str(files("qplot.configuration") / schema_file_name)
75
+
76
+ def __init__(self):
77
+ self.schema = self.load_config(self.default__schema_file)
78
+ self.startup_warning = None
79
+
80
+ try:
81
+ loaded_config = self.load_config(self.default_file)
82
+ except FileNotFoundError as error:
83
+ # A read can fail with ENOENT even when the settings path still
84
+ # exists (for example, a dangling link). Keep that original.
85
+ try:
86
+ os.lstat(self.default_file)
87
+ except FileNotFoundError:
88
+ pass
89
+ except OSError as stat_error:
90
+ self._recover_unreadable_config(stat_error)
91
+ return
92
+ else:
93
+ self._recover_unreadable_config(error)
94
+ return
95
+ self.config = self.build_default_config()
96
+ try:
97
+ self.save_config(self.default_file)
98
+ except OSError as error:
99
+ self._record_startup_persistence_failure(
100
+ "Could not create default configuration at "
101
+ f"{self.default_file}",
102
+ error,
103
+ )
104
+ except (ValueError, UnicodeDecodeError, RecursionError) as error:
105
+ # JSONDecodeError subclasses ValueError. The decoder also raises
106
+ # ValueError for integer conversion limits and RecursionError for
107
+ # excessive nesting; these concern only the user settings read.
108
+ self._recover_invalid_config(error)
109
+ except OSError as error:
110
+ self._recover_unreadable_config(error)
111
+ else:
112
+ try:
113
+ if not isinstance(loaded_config, dict):
114
+ raise jsonschema.ValidationError(
115
+ "config.json root must be a JSON object"
116
+ )
117
+
118
+ migrated = self._migrate_config(loaded_config)
119
+ self.validate(loaded_config)
120
+ self.config = loaded_config
121
+ if migrated:
122
+ try:
123
+ self.save_config(self.default_file)
124
+ except OSError as error:
125
+ self._record_startup_persistence_failure(
126
+ "Could not persist migrated configuration at "
127
+ f"{self.default_file}",
128
+ error,
129
+ )
130
+ except jsonschema.ValidationError as error:
131
+ self._recover_invalid_config(error)
132
+
133
+
134
+ def __str__(self) -> str:
135
+ """
136
+ Produces a display similar to how the config.json file looks
137
+
138
+ Returns
139
+ -------
140
+ str
141
+
142
+ """
143
+ return json.dumps(self.config, indent=4)
144
+
145
+
146
+ def __repr__(self):
147
+ return str(self)
148
+
149
+
150
+ def dump(self):
151
+ """
152
+ Prints out Information about config.json, including location and all data
153
+ contained inside the file
154
+
155
+ """
156
+ print(f"config.json at: {self.default_file} \ncontents:")
157
+ print(str(self))
158
+
159
+
160
+ def get(self, key):
161
+ """
162
+ Returns data of at specified key.
163
+ Key must be laid out as a dot (.) seperated path, i.e.
164
+ 'GUI.main_frame_size'
165
+
166
+ Parameters
167
+ ----------
168
+ key : str
169
+ Key of value.
170
+
171
+ Raises
172
+ ------
173
+ KeyError
174
+ Key passed was invalid, other not in correct for or key not found.
175
+
176
+ Returns
177
+ -------
178
+ out : any
179
+ Value at specified key.
180
+
181
+ """
182
+ out = None
183
+ # Get number of nests to look though
184
+ keys = key.split(".")
185
+ if len(keys) == 1:
186
+ out = self.config.get(key)
187
+ elif len(keys) == 2:
188
+ section = self.config.get(keys[0])
189
+ if isinstance(section, dict):
190
+ out = section.get(keys[1])
191
+ else:
192
+ raise KeyError(f"Key length too long, {key}. Please ensure you use a dot (.) seperated key")
193
+
194
+ # Return value if found
195
+ if out != None:
196
+ return out
197
+ else:
198
+ raise KeyError(f"Key: {key}, not found. Please ensure you use a dot (.) seperated key")
199
+
200
+
201
+ def update(self, key, value):
202
+ """
203
+ Updates value at key location
204
+
205
+ Parameters
206
+ ----------
207
+ key : str
208
+ Lookup Key of value.
209
+ value : any
210
+ Value to be changed to.
211
+
212
+ Raises
213
+ ------
214
+ jsonschema.exceptions.ValidationError
215
+ Value updated is not allowed under conditions set by schema.
216
+ Either due to incorrect typing or trying to add a new value.
217
+
218
+ """
219
+ self.update_many({key: value})
220
+
221
+
222
+ def update_many(self, values):
223
+ """Validate and persist several values as one configuration update."""
224
+
225
+ updated_config = deepcopy(self.config)
226
+ for key, value in values.items():
227
+ keys = key.split(".")
228
+ target = updated_config
229
+ for part in keys[:-1]:
230
+ try:
231
+ target = target[part]
232
+ except KeyError as err:
233
+ raise KeyError(
234
+ f"Key: {key}, not found. Please ensure you use a dot (.) seperated key"
235
+ ) from err
236
+
237
+ if not isinstance(target, dict):
238
+ raise KeyError(
239
+ f"Key: {key}, cannot be updated because {part} is not a section"
240
+ )
241
+
242
+ if keys[-1] not in target:
243
+ raise KeyError(
244
+ f"Key: {key}, not found. Please ensure you use a dot (.) seperated key"
245
+ )
246
+ target[keys[-1]] = value
247
+
248
+ self.validate(updated_config)
249
+ previous_config = self.config
250
+ self.config = updated_config
251
+ try:
252
+ self.save_config(self.default_file)
253
+ except Exception:
254
+ self.config = previous_config
255
+ raise
256
+
257
+
258
+ def load_config(self, path: str):
259
+ """
260
+ Load a config JSON file
261
+
262
+ Parameters
263
+ ----------
264
+ path: str
265
+ path to the config file
266
+
267
+ Return
268
+ ------
269
+ config: dict
270
+ Returns config file in form of a dictionary
271
+
272
+ Raises
273
+ ------
274
+ FileNotFoundError:
275
+ if config is missing
276
+
277
+ """
278
+ with open(path, encoding="utf-8") as fp:
279
+ config = json.load(fp, parse_constant=_reject_json_constant)
280
+ return config
281
+
282
+
283
+ def save_config(self, path: str) -> None:
284
+ """
285
+ Save current config to file at given path.
286
+
287
+ Parameters
288
+ ----------
289
+ path: str
290
+ path of file
291
+
292
+ """
293
+ ensure_safe_auxiliary_path(path)
294
+ directory = os.path.dirname(os.path.abspath(path))
295
+ makedirs(directory, exist_ok=True)
296
+ file_mode = None
297
+ try:
298
+ file_mode = stat.S_IMODE(os.stat(path).st_mode)
299
+ except FileNotFoundError:
300
+ pass
301
+
302
+ file_descriptor, temporary_path = tempfile.mkstemp(
303
+ prefix=f".{os.path.basename(path)}.",
304
+ suffix=".tmp",
305
+ dir=directory,
306
+ text=True,
307
+ )
308
+ try:
309
+ with os.fdopen(file_descriptor, "w", encoding="utf-8") as fp:
310
+ json.dump(self.config, fp, indent=4, allow_nan=False)
311
+ fp.write("\n")
312
+ fp.flush()
313
+ os.fsync(fp.fileno())
314
+ if file_mode is not None:
315
+ os.chmod(temporary_path, file_mode)
316
+ ensure_safe_auxiliary_path(path)
317
+ os.replace(temporary_path, path)
318
+ self._sync_config_directory(directory)
319
+ except Exception:
320
+ try:
321
+ os.unlink(temporary_path)
322
+ except FileNotFoundError:
323
+ pass
324
+ raise
325
+
326
+
327
+ @staticmethod
328
+ def _sync_config_directory(directory: str) -> None:
329
+ """Persist the rename on platforms that support directory fsync."""
330
+
331
+ if os.name == "nt":
332
+ return
333
+ try:
334
+ descriptor = os.open(directory, os.O_RDONLY)
335
+ except OSError:
336
+ return
337
+ try:
338
+ os.fsync(descriptor)
339
+ except OSError:
340
+ pass
341
+ finally:
342
+ os.close(descriptor)
343
+
344
+
345
+ def reset_to_defaults(self):
346
+ """
347
+ Resets the config.json file to default determined by the config_schema
348
+
349
+ Needs adjusting if nesting of config increases
350
+ """
351
+ config = self.build_default_config()
352
+
353
+ # Save reset to file. Keep the last persisted configuration
354
+ # authoritative if any part of the atomic write fails.
355
+ had_previous_config = hasattr(self, "config")
356
+ previous_config = getattr(self, "config", {})
357
+ self.config = config
358
+ try:
359
+ self.save_config(self.default_file)
360
+ except Exception:
361
+ if had_previous_config:
362
+ self.config = previous_config
363
+ else:
364
+ del self.config
365
+ raise
366
+
367
+
368
+ def build_default_config(self):
369
+ """Construct and validate a fresh default configuration in memory."""
370
+
371
+ config = {}
372
+ for key, val in self.schema["properties"].items():
373
+ config[key] = {
374
+ setting: deepcopy(setting_schema["default"])
375
+ for setting, setting_schema in val["properties"].items()
376
+ }
377
+ self.validate(config)
378
+ return config
379
+
380
+
381
+ def _recover_invalid_config(self, original_error):
382
+ """Use defaults after an invalid config without risking the original."""
383
+
384
+ diagnostic = original_error
385
+ if isinstance(original_error, jsonschema.ValidationError):
386
+ # ValidationError.__str__ pretty-prints the rejected instance.
387
+ # Deep, invalid settings must not prevent recovery while logging.
388
+ diagnostic = f"ValidationError: {original_error.message}"
389
+ log_exception(
390
+ f"Invalid configuration at {self.default_file}",
391
+ diagnostic,
392
+ __name__,
393
+ )
394
+ self.config = self.build_default_config()
395
+ self.startup_warning = (
396
+ "Invalid configuration was reset to defaults; the original "
397
+ "settings were backed up."
398
+ )
399
+ try:
400
+ self.invalid_config_backup_file = self.backup_invalid_config()
401
+ except OSError as error:
402
+ self._record_startup_persistence_failure(
403
+ "Could not back up invalid configuration at "
404
+ f"{self.default_file}",
405
+ error,
406
+ )
407
+ return
408
+
409
+ try:
410
+ self.save_config(self.default_file)
411
+ except OSError as error:
412
+ self._record_startup_persistence_failure(
413
+ "Could not replace invalid configuration at "
414
+ f"{self.default_file}",
415
+ error,
416
+ )
417
+
418
+
419
+ def _recover_unreadable_config(self, original_error):
420
+ """Use defaults without changing a settings file that could not be read."""
421
+
422
+ log_exception(
423
+ f"Could not read configuration at {self.default_file}",
424
+ original_error,
425
+ __name__,
426
+ )
427
+ self.config = self.build_default_config()
428
+ self.startup_warning = (
429
+ "Could not read configuration; using defaults for this session. "
430
+ "The original settings were left untouched."
431
+ )
432
+
433
+
434
+ def _record_startup_persistence_failure(self, context, error):
435
+ """Log a failed startup write and retain a non-blocking UI warning."""
436
+
437
+ log_exception(context, error, __name__)
438
+ self.startup_warning = (
439
+ "Configuration recovery could not be saved; using defaults for "
440
+ "this session."
441
+ )
442
+
443
+
444
+ def validate(self, candidate):
445
+ """Validate a config with strict Python integer and finite-number types."""
446
+
447
+ StrictConfigValidator(self.schema).validate(candidate)
448
+
449
+
450
+ def _migrate_config(self, candidate):
451
+ """Apply non-destructive migrations for newly introduced settings."""
452
+
453
+ preferences = candidate.get("user_preference")
454
+ migrated = False
455
+
456
+ if isinstance(preferences, dict):
457
+ preference_schema = self.schema["properties"]["user_preference"]["properties"]
458
+ # ``axis_tick_density`` was superseded by an explicit target because
459
+ # PyQtGraph clamps density values and cannot produce sparse axes.
460
+ if preferences.pop("axis_tick_density", None) is not None:
461
+ migrated = True
462
+
463
+ for key in ("colorbar_width", "axis_tick_width", "axis_major_tick_count"):
464
+ if key not in preferences:
465
+ preferences[key] = deepcopy(preference_schema[key]["default"])
466
+ migrated = True
467
+
468
+ runtime_settings = candidate.get("runtime_settings")
469
+ if isinstance(runtime_settings, dict):
470
+ runtime_schema = self.schema["properties"]["runtime_settings"]["properties"]
471
+ for key in ("max_heatmap_grid_cells", "max_heatmap_grid_side"):
472
+ if key not in runtime_settings:
473
+ runtime_settings[key] = deepcopy(runtime_schema[key]["default"])
474
+ migrated = True
475
+
476
+ return migrated
477
+
478
+
479
+ def schema_for(self, key):
480
+ """Return the JSON-schema node for a supported dotted config key."""
481
+
482
+ keys = key.split(".")
483
+ if len(keys) != 2:
484
+ raise KeyError(
485
+ f"Key: {key}, not found. Please ensure you use a dot (.) seperated key"
486
+ )
487
+ try:
488
+ return self.schema["properties"][keys[0]]["properties"][keys[1]]
489
+ except KeyError as err:
490
+ raise KeyError(
491
+ f"Key: {key}, not found. Please ensure you use a dot (.) seperated key"
492
+ ) from err
493
+
494
+
495
+ def backup_invalid_config(self):
496
+ """
497
+ Copies an invalid config file aside before resetting to defaults.
498
+
499
+ """
500
+ ensure_safe_auxiliary_path(self.default_file)
501
+ directory = path.dirname(self.default_file)
502
+ makedirs(directory, exist_ok=True)
503
+ source_stat = os.stat(self.default_file)
504
+
505
+ while True:
506
+ backup_file = self.next_invalid_config_backup_file()
507
+ try:
508
+ descriptor = os.open(
509
+ backup_file,
510
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL,
511
+ stat.S_IMODE(source_stat.st_mode),
512
+ )
513
+ except FileExistsError:
514
+ continue
515
+ break
516
+
517
+ try:
518
+ with (
519
+ open(self.default_file, "rb") as source,
520
+ os.fdopen(descriptor, "wb") as destination,
521
+ ):
522
+ copyfileobj(source, destination)
523
+ destination.flush()
524
+ os.fsync(destination.fileno())
525
+ os.chmod(backup_file, stat.S_IMODE(source_stat.st_mode))
526
+ os.utime(
527
+ backup_file,
528
+ ns=(source_stat.st_atime_ns, source_stat.st_mtime_ns),
529
+ )
530
+ except OSError:
531
+ try:
532
+ os.unlink(backup_file)
533
+ except FileNotFoundError:
534
+ pass
535
+ raise
536
+ return backup_file
537
+
538
+
539
+ def next_invalid_config_backup_file(self):
540
+ """
541
+ Returns a non-existing path for an invalid config backup.
542
+
543
+ """
544
+ directory = path.dirname(self.default_file)
545
+ stem, suffix = path.splitext(path.basename(self.default_file))
546
+ candidate = path.join(directory, f"{stem}.invalid{suffix}")
547
+ if not path.exists(candidate):
548
+ return candidate
549
+
550
+ index = 1
551
+ while True:
552
+ candidate = path.join(directory, f"{stem}.invalid.{index}{suffix}")
553
+ if not path.exists(candidate):
554
+ return candidate
555
+ index += 1
556
+
557
+ ###############################################################################
558
+ #handled functions
559
+
560
+ @property
561
+ def theme(self):
562
+ """
563
+ Fetches theme data from .themes
564
+
565
+ Returns
566
+ -------
567
+ callable
568
+ Returns the class in qplt.configuration.themes coresponding to the
569
+ set config value
570
+
571
+ """
572
+ config_theme = self.get("user_preference.theme")
573
+ return THEME_CLASSES[config_theme]