# -*- coding: utf-8 -*-
"""
Static file handler and mixin for handling qudi configuration files.
.. Copyright (c) 2021, the qudi developers. See the AUTHORS.md file at the top-level directory of this
.. distribution and on <https://github.com/Ulm-IQO/qudi-core/>
..
.. This file is part of qudi.
..
.. Qudi is free software: you can redistribute it and/or modify it under the terms of
.. the GNU Lesser General Public License as published by the Free Software Foundation,
.. either version 3 of the License, or (at your option) any later version.
..
.. Qudi is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
.. without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
.. See the GNU Lesser General Public License for more details.
..
.. You should have received a copy of the GNU Lesser General Public License along with qudi.
.. If not, see <https://www.gnu.org/licenses/>.
"""
__all__ = [
'FileHandler',
'FileHandlerBase',
'ParserError',
'ValidationError',
'YAMLError',
'DuplicateKeyError',
]
import os
from typing import Any, Dict, Mapping
from qudi.util.paths import get_default_config_dir, get_appdata_dir
from qudi.util.yaml import (
yaml_dump,
yaml_load,
ParserError,
YAMLError,
DuplicateKeyError,
)
from .validator import validate_config, ValidationError
[docs]
class FileHandlerBase:
"""File handler base class providing static methods for handling raw qudi configuration files."""
@classmethod
def _load(cls, path: str) -> Dict[str, Any]:
return yaml_load(cls._relative_to_absolute_path(path))
@classmethod
def _dump(cls, path: str, config: Mapping[str, Any]) -> None:
if not path.endswith('.cfg'):
raise ValueError('Configuration file must have ".cfg" file extension.')
path = cls._relative_to_absolute_path(path)
os.makedirs(os.path.dirname(path), exist_ok=True)
return yaml_dump(path, config)
[docs]
@classmethod
def set_default_path(cls, path: str) -> None:
"""Writes the given config file path to "<AppData>/qudi/load.cfg" to be used as default
config at the next start of qudi."""
# Write current config file path to load.cfg
yaml_dump(
os.path.join(get_appdata_dir(create_missing=True), 'load.cfg'),
{'load_config_path': cls._relative_to_absolute_path(path)},
)
[docs]
@staticmethod
def get_saved_path() -> str:
"""Tries to parse "<AppData>/qudi/load.cfg" and return the stored config file path.
Raises FileNotFoundError if unsuccessful or if the recovered file path does not exist.
"""
# Try loading config file path from last session
load_cfg = yaml_load(
os.path.join(get_appdata_dir(), 'load.cfg'), ignore_missing=True
)
file_path = load_cfg.get('load_config_path', '')
if os.path.exists(file_path) and file_path.endswith('.cfg'):
return file_path
# Raise error if no last run config file could be found
raise FileNotFoundError('No config file path saved from previous qudi sessions')
[docs]
@staticmethod
def get_default_path() -> str:
"""Tries to find config file named "default.cfg" in several locations with the following,
non-recursive search directory priority:
1. <UserHome>/qudi/config/
2. <AppData>/qudi/
Raises FileNotFoundError if no "default.cfg" file could be found in the above locations.
"""
# Try default.cfg in user home directory
file_path = os.path.join(
get_default_config_dir(create_missing=False), 'default.cfg'
)
if os.path.exists(file_path):
return file_path
# Fall back to default.cfg in qudi AppData directory if possible
file_path = os.path.join(get_appdata_dir(create_missing=False), 'default.cfg')
if os.path.exists(file_path):
return file_path
# Raise error if no config file could be found
raise FileNotFoundError('No config file could be found in default directories')
@staticmethod
def _relative_to_absolute_path(path):
"""Helper method converting given relative path to an existing absolute path.
Prepends directories to given path with the following priority until an existing path has
been created:
1. <UserHome>/qudi/config/
2. <AppData>/qudi/
Raises FileNotFoundError if no existing path could be reconstructed by the above algorithm.
"""
# absolute or relative path? Existing?
if os.path.isabs(path) and os.path.exists(path):
return path
# relative path? Try relative to userdata dir, user home dir and relative to main dir
for search_dir in [get_default_config_dir(), get_appdata_dir()]:
new_path = os.path.abspath(os.path.join(search_dir, path))
if os.path.exists(new_path):
return new_path
# Raise exception if no existing path can be determined
raise FileNotFoundError(
f'Qudi relative path "{path}" can not be resolved or does not exist.'
)
[docs]
class FileHandler(FileHandlerBase):
"""File handler class providing static methods for handling raw qudi configuration files.
Also applies qudi configuration validation and default value insertion upon loading/dumping
configuration files.
"""
[docs]
@classmethod
def load(cls, path: str) -> Dict[str, Any]:
"""Load and validate a qudi configuration file from disk.
Raises jsonschema.ValidationError if validation fails.
"""
config = cls._load(path)
validate_config(config)
return config
[docs]
@classmethod
def dump(cls, path: str, config: Dict[str, Any]) -> None:
"""Validate and dump a qudi configuration file to disk.
Raises jsonschema.ValidationError if validation fails.
"""
validate_config(config)
cls._dump(path, config)