Source code for QInstrument.lib.Configure
from __future__ import annotations
import json
import logging
from datetime import datetime
from pathlib import Path
from qtpy import QtCore, QtWidgets
logger = logging.getLogger(__name__)
[docs]
class Configure(QtCore.QObject):
'''Save and restore instrument configuration to and from JSON files.
Configuration files are named after the class of the target object
and stored under :attr:`configdir`. Timestamped data filenames are
generated under :attr:`datadir`.
Parameters
----------
datadir : str | None
Directory for timestamped data files.
Default: ``~/data/``.
configdir : str | None
Directory for JSON configuration files.
Default: ``~/.QInstrument/``.
Attributes
----------
datadir : Path
Resolved path to the data directory.
configdir : Path
Resolved path to the configuration directory.
'''
def __init__(self,
datadir: str | None = None,
configdir: str | None = None) -> None:
super().__init__()
self.datadir = Path(datadir or '~/data/').expanduser()
self.configdir = Path(configdir or '~/.QInstrument/').expanduser()
if not self.datadir.exists():
logger.info(f'Creating data directory: {self.datadir}')
self.datadir.mkdir(parents=True)
if not self.configdir.exists():
logger.info(f'Creating configuration directory: {self.configdir}')
self.configdir.mkdir(parents=True)
[docs]
def timestamp(self) -> str:
'''Return a string representing the current date and time.
Returns
-------
str
Timestamp formatted as ``_YYYYMonDD_HHMMSS``
(e.g. ``_2024Jan15_143022``).
'''
return datetime.now().strftime('_%Y%b%d_%H%M%S')
[docs]
def filename(self,
prefix: str = 'QInstrument',
suffix: str = '') -> str:
'''Return a timestamped filename under :attr:`datadir`.
Parameters
----------
prefix : str
String prepended to the filename. Default: ``'QInstrument'``.
suffix : str
String appended after the timestamp. Default: ``''``.
Returns
-------
str
Absolute path string of the form
``<datadir>/<prefix><timestamp><suffix>``.
'''
return str(self.datadir / (prefix + self.timestamp() + suffix))
[docs]
def configname(self, obj: object) -> str:
'''Return the path to the JSON configuration file for *obj*.
The filename is derived from ``obj``'s class name, so all
instances of the same class share one configuration file.
Parameters
----------
obj : object
Object whose class name determines the configuration filename.
Returns
-------
str
Absolute path string of the form
``<configdir>/<ClassName>.json``.
'''
return str(self.configdir / (obj.__class__.__name__ + '.json'))
[docs]
def save(self, obj: object,
settings: dict | None = None) -> None:
'''Save *obj*'s settings to its JSON configuration file.
Reads ``obj.settings`` (a dict) and serializes it to
:meth:`configname`. Does nothing when the settings dict is empty.
An explicit *settings* dict may be supplied to avoid reading from
*obj* directly — useful when *obj* lives in a worker thread.
Parameters
----------
obj : object
Object whose class name determines the configuration filename.
settings : dict or None, optional
Settings to save. When ``None`` (default), reads
``obj.settings``.
'''
settings = obj.settings if settings is None else settings
if not settings:
return
filename = self.configname(obj)
with open(filename, 'w', encoding='utf-8') as configfile:
json.dump(settings, configfile,
indent=2, separators=(',', ': '),
ensure_ascii=False)
[docs]
def restore(self, obj: object) -> None:
'''Restore *obj*'s settings from its JSON configuration file.
Reads the JSON file at :meth:`configname` and assigns the result
to ``obj.settings``. Logs a warning and leaves *obj* unchanged
if the file does not exist or cannot be parsed.
Parameters
----------
obj : object
Object with a ``settings`` property whose setter accepts a
``dict[str, bool | int | float | str]``.
'''
filename = self.configname(obj)
try:
logger.info(f'Configuring {filename}')
with open(filename, 'r', encoding='utf-8') as configfile:
obj.settings = json.load(configfile)
except Exception as ex:
logger.warning(
f'Could not read {filename}: {ex}'
'\n\tUsing default configuration.')
[docs]
def read(self, obj: object) -> dict | None:
'''Read and return *obj*'s saved configuration without applying it.
Reads the JSON file at :meth:`configname` and returns its
contents as a dict. Returns ``None`` if the file does not
exist or cannot be parsed, without logging a warning.
Parameters
----------
obj : object
Object whose class name determines the configuration filename.
Returns
-------
dict or None
Saved configuration dict, or ``None`` if unavailable.
'''
filename = self.configname(obj)
try:
with open(filename, 'r', encoding='utf-8') as configfile:
return json.load(configfile)
except Exception:
return None
[docs]
def query_save(self, obj: object) -> None:
'''Prompt the user and save *obj*'s settings if confirmed.
Opens a modal dialog asking whether to save the current
configuration. Calls :meth:`save` if the user clicks Yes.
Parameters
----------
obj : object
Object to save if the user confirms.
'''
mbox = QtWidgets.QMessageBox
msg = mbox(self.parent())
msg.setWindowTitle('Confirmation')
msg.setText('Save current configuration?')
msg.setStandardButtons(mbox.StandardButton.Yes |
mbox.StandardButton.No)
if msg.exec() == mbox.StandardButton.Yes:
self.save(obj)
__all__ = ['Configure']