Source code for pymodaq_utils.config

from abc import abstractproperty
from collections.abc import Iterable

from os import environ
import sys
import datetime
from pathlib import Path
from typing import Union, Dict, TypeVar, Any, List, TYPE_CHECKING
from typing import Iterable as IterableType

import toml
import logging
if TYPE_CHECKING:
    from pyqtgraph import Parameter


try:
    USER = environ['USERNAME'] if sys.platform == 'win32' else environ['USER']
except:
    USER = 'unknown_user'

CONFIG_BASE_PATH = Path(environ['PROGRAMDATA']) if sys.platform == 'win32' else \
    Path('/Library/Application Support') if sys.platform == 'darwin' else Path('/etc')


KeyType = TypeVar('KeyType')


[docs] def replace_item_in_list(items: list[str], item_to_check: str, item_to_replace:str): if not isinstance(items, list): items = list(items) index = items.index(item_to_check) items[index] = item_to_replace return items
[docs] def deep_update(mapping: Dict[KeyType, Any], *updating_mappings: Dict[KeyType, Any]) -> Dict[KeyType, Any]: """ Make sure a dictionary is updated using another dict in any nested level Taken from Pydantic v1 """ updated_mapping = mapping.copy() for updating_mapping in updating_mappings: for k, v in updating_mapping.items(): if k in updated_mapping and isinstance(updated_mapping[k], dict) and isinstance(v, dict): updated_mapping[k] = deep_update(updated_mapping[k], v) else: updated_mapping[k] = v return updated_mapping
[docs] def replace_file_extension(filename: str, ext: str): """Replace the extension of a file by the specified one, without the dot""" file_name = Path(filename).stem # remove eventual extensions if ext[0] == '.': ext = ext[1:] file_name += '.' + ext return file_name
[docs] def getitem_recursive(dic, *args, ndepth=0, create_if_missing=False): """Will scan recursively a dictionary in order to get the item defined by the iterable args Parameters ---------- dic: dict the dictionary to scan args: an iterable of str keys of the dict ndepth: int by default (0) get the last element defined by args. 1 would mean it get the parent dict, 2 the parent of the parent... create_if_missing: bool if the entry is not present, create it assigning the 'none' default value (as a lower case string) Returns ------- object or dict """ args = list(args) while len(args) > ndepth: try: arg = args.pop(0) dic = dic[arg] except KeyError as e: if create_if_missing: if len(args) > 0: dic[arg] = {} dic = dic[arg] else: dic[arg] = 'none' dic = 'none' else: raise e return dic
[docs] def recursive_iterable_flattening(aniterable: IterableType): flatten_iter = [] for elt in aniterable: if not isinstance(elt, str) and isinstance(elt, Iterable): flatten_iter.extend(recursive_iterable_flattening(elt)) else: flatten_iter.append(elt) return flatten_iter
[docs] def get_set_path(a_base_path: Path, dir_name: str) -> Path: path_to_get = a_base_path.joinpath(dir_name) if not path_to_get.is_dir(): try: path_to_get.mkdir() except PermissionError as e: logging.warning(f"Cannot create local config folder at this location: {path_to_get}" f", try using admin rights. " f"Changing the not permitted path to a user " f"one: {Path.home().joinpath(dir_name)}.") path_to_get = Path.home().joinpath(dir_name) if not path_to_get.is_dir(): path_to_get.mkdir() return path_to_get
[docs] def get_set_local_dir(user=False) -> Path: """Defines, creates and returns a local folder where configuration files will be saved Depending on the os the configurations files will be stored in CONFIG_BASE_PATH, then each user will have another one created that could override the default and system-wide base folder Parameters ---------- user: bool if False get the system-wide folder, otherwise the user folder Returns ------- Path: the local path """ if user: local_path = get_set_path(Path.home(), '.pymodaq') else: local_path = get_set_path(CONFIG_BASE_PATH, '.pymodaq') return local_path
[docs] def get_config_file(config_file_name: str, user=False): return get_set_local_dir(user).joinpath(replace_file_extension(config_file_name, 'toml'))
[docs] def get_set_config_dir(config_name='config', user=False): """Creates a folder in the local config directory to store specific configuration files Parameters ---------- config_name: (str) name of the configuration folder user: bool if False get the system-wide folder, otherwise the user folder Returns ------- Path See Also -------- get_set_local_dir """ return get_set_path(get_set_local_dir(user=user), config_name)
[docs] def get_set_log_path(): """ creates and return the config folder path for log files """ return get_set_config_dir('log')
[docs] def create_toml_from_dict(mydict: dict, dest_path: Path): """Create a Toml file at a given path from a dictionnary""" dest_path.write_text(toml.dumps(mydict))
[docs] def check_config(config_base: dict, config_local: dict): """Compare two configuration dictionaries. Adding missing keys Parameters ---------- config_base: dict The base dictionaries with possible new keys config_local: dict a dict from a local config file potentially missing keys Returns ------- bool: True if keys where missing else False """ status = False for key in config_base: if key in config_local: if isinstance(config_base[key], dict): status = status or check_config(config_base[key], config_local[key]) else: config_local[key] = config_base[key] status = True return status
[docs] def copy_template_config(config_file_name: str = 'config', source_path: Union[Path, str] = None, dest_path: Union[Path, str] = None): """Get a toml file path and copy it the destination is made of a given folder path (or the system-wide local path by default) and the config_file_name appended by the suffix '.toml' The source file (or pymodaq config template path by default) is read and dumped in this destination file Parameters ---------- config_file_name: str the name of the destination config file source_path: Path or str the path of the toml source to be copied dest_path: Path or str the destination path of the copied config Returns ------- Path: the path of the copied file """ if dest_path is None: dest_path = get_set_local_dir() file_name = Path(config_file_name).stem # remove eventual extensions file_name += '.toml' dest_path_with_filename = dest_path.joinpath(file_name) if source_path is None: config_template_dict = {} else: config_template_dict = toml.load(Path(source_path)) create_toml_from_dict(config_template_dict, dest_path_with_filename) return dest_path_with_filename
[docs] def load_system_config_and_update_from_user(config_file_name: str): """load from a system-wide config file, update it from the user config file Parameters ---------- config_file_name: str The config file to be loaded Returns ------- dict: contains the toml system-wide file update with the user file """ config_dict = dict([]) toml_base_path = get_config_file(config_file_name, user=False) if toml_base_path.is_file(): config_dict = toml.load(toml_base_path) toml_user_path = get_config_file(config_file_name, user=True) if toml_user_path.is_file(): config_dict = deep_update(config_dict, toml.load(toml_user_path)) return config_dict
[docs] class ConfigError(Exception): pass
[docs] class BaseConfig: """Base class to manage configuration files Should be subclassed with proper class attributes for each configuration file you need with pymodaq Attributes ---------- config_name: str The name with which the configuration will be saved config_template_path: Path The Path of the template from which the config is constructed """ config_template_path: Path = abstractproperty() config_name: str = abstractproperty() def __init__(self): self._config = self.load_config(self.config_name, self.config_template_path) def __repr__(self): return f'{self.config_name} configuration file' def __call__(self, *args): try: ret = getitem_recursive(self._config, *args) except KeyError as e: raise ConfigError(f'the path {args} does not exist in your configuration toml' f' file, check your config folder') return ret def __contains__(self, item): return self._config.__contains__(item)
[docs] def to_dict(self): return self._config
[docs] def get(self, key: Union[str, Iterable[str]], default=None): try: return self[key] except (KeyError, ConfigError): return default
def __getitem__(self, item): """for backcompatibility when it was a dictionnary""" if not isinstance(item, tuple): item = tuple([item]) return self(*item) def __setitem__(self, key, value): if isinstance(key, tuple): dic = getitem_recursive(self._config, *key, ndepth=1, create_if_missing=True) if value is None: # means the setting is a group value = {} dic[key[-1]] = value else: self._config[key] = value
[docs] def load_config(self, config_file_name, template_path: Path): """Load a configuration file from both system-wide and user file check also if missing entries in the configuration file compared to the template""" toml_base_path = get_config_file(config_file_name, user=False) toml_user_path = get_config_file(config_file_name, user=True) if toml_base_path.is_file(): config = toml.load(toml_base_path) if template_path is not None: config_template = toml.load(template_path) else: config_template = {} if check_config(config_template, config): # check if all fields from template are there # (could have been modified by some commits) create_toml_from_dict(config, toml_base_path) else: copy_template_config(config_file_name, template_path, toml_base_path.parent) if not toml_user_path.is_file(): # create the author from environment variable config_dict = self.dict_to_add_to_user() if config_dict is not None: create_toml_from_dict(config_dict, toml_user_path) config_dict = load_system_config_and_update_from_user(config_file_name) return config_dict
[docs] def dict_to_add_to_user(self): """To subclass""" return dict([])
@property def config_path(self): """Get the user config path""" return get_config_file(self.config_name, user=True) @property def system_config_path(self): """Get the system_wide config path""" return get_config_file(self.config_name, user=False)
[docs] def save(self): """Save the current Config object into the user toml file""" self.config_path.write_text(toml.dumps(self.to_dict()))
[docs] def get_children(self, *path: IterableType[str]): """ Get the list of config entries at a given path within the configulation toml file new in 4.3.0 """ return list(getitem_recursive(self._config, *path).keys())
[docs] class Config(BaseConfig): """Main class to deal with configuration values for PyMoDAQ""" config_template_path = Path(__file__).parent.joinpath('resources/config_template.toml') config_name = 'config_pymodaq_utils'
[docs] def dict_to_add_to_user(self): """To subclass""" return dict(user=dict(name=USER))
def __call__(self, *args): """ Patch in case of a mixup of configs from different version of pymodaq: v5.1 and v5.2 """ if 'backend' in args or 'debug_level' in args: entry = super().__call__(*args) if isinstance(entry, list): return entry[0] else: return entry return super().__call__(*args)