Source code for TidalPy.configurations

"""Helper functions for loading TidalPy's configurations.

It is recommended that you only change these once you have some experience with the package
You can check their default values by examining the same file at https://github.com/jrenaud90/TidalPy/TidalPy/defaultc.py
"""

import os
import warnings
from typing import Union
from itertools import islice

import toml

import TidalPy
from TidalPy import version
from TidalPy.exceptions import ConfigurationException, InitializationError
from TidalPy.paths import get_config_dir, get_worlds_dir, unique_path
from TidalPy.defaultc import default_config_str


[docs] def dict_replace_value(d_old: dict, d_new: dict) -> dict: merged_dict = {} for k, v in d_old.items(): if isinstance(v, dict): if k in d_new: v = dict_replace_value(v, d_new[k]) else: if k in d_new: v = d_new[k] merged_dict[k] = v return merged_dict
[docs] def save_dict_to_toml(dict_to_save: dict, file_path: str, overwrite: bool = True): """Saves a python dictionary to a toml file at the specified file path. Parameters ---------- dict_to_save : dict Python dictionary. file_path : str Filepath to save to. overwrite : bool, default = True If True, then the file will be overwritten if already present. by default True """ if '.toml' not in file_path: raise AttributeError('Please provide a toml file path (include ".toml" extension).') if type(dict_to_save) is not dict: raise AttributeError(f'Can only save python dictionaries to toml files, not {type(dict_to_save)}.') toml_output = None if os.path.isfile(file_path): if overwrite: os.remove(file_path) else: # Append a number to the config name until one is found that is not already in use. file_path = unique_path(file_path, is_dir=False, make_dir=False) with open(file_path, 'w') as toml_file: toml_output = toml.dump(dict_to_save, toml_file) return toml_output
[docs] def check_config_version( config_path: str, allow_bugfix_difference: bool = True, warn_on_false: bool = True, raise_on_false: bool = False) -> bool: """ Checks a TidalPy configuration file to ensure that it is compatible with this version of TidalPy. Parameters ---------- config_path : str Path to the configuration file to test. allow_bugfix_difference : bool, default = True If true, then a config with version A.B.C will still be allowed for TidalPy version A.B.D warn_on_false : bool, default = True If true, then a warning message will be shown if the config version check fails. raise_on_false : bool, default = False If true, then an error message will be raised if the config version check fails. Returns ------- compatible : bool Flag for if this configuration file is compatible. """ compatible = False with open(config_path, 'r') as config_file: config_version_found = False for line in islice(config_file, 0, 10): # Assume the version number is in the first 10 lines if 'version:' in line.lower(): config_version = line.split(': ')[1].split('\n')[0].strip() config_version_found = True break if not config_version_found: if raise_on_false: raise ConfigurationException(f'Can not find configuration version in {config_file}.') else: if warn_on_false: message = f'Could not determine version for TidalPy configuration file, {config_path}. ' + \ 'It may not be compatible with this version of TidalPy.\n' warnings.warn(message) return False if config_version == version: compatible = True elif allow_bugfix_difference: config_sub_vers = config_version.split('.') tpy_sub_vers = version.split('.') if (config_sub_vers[0] == tpy_sub_vers[0]) and (config_sub_vers[1] == tpy_sub_vers[1]): compatible = True if not compatible: message = f'TidalPy configuration file, {config_path}, was built for a different version of TidalPy ' + \ f'({config_version} vs. {version}). Unexpected behavior may arise.\n' if raise_on_false: raise ConfigurationException(message) elif warn_on_false: warnings.warn(message) return compatible
[docs] def get_default_config() -> dict: """ Loads TidalPy configurations that are found on the local disk. If no configuration file is found (likely when TidalPy is used for the first time) then default configurations will be saved to disk first. """ config_dir = get_config_dir() config_path = os.path.join(config_dir, 'TidalPy_Configs.toml') # Check if TidalPy's config file is not present. if not os.path.isfile(config_path): # Create toml file with default configurations. with open(config_path, 'w') as config_file: config_file.write('#===========================================================#\n') config_file.write(f'# TidalPy Default Configurations for Version: {version}\n') config_file.write('#===========================================================#\n\n') config_file.write(default_config_str) else: # Check if configuration file is for the correct version of TidalPy. check_config_version(config_path) # Load configurations (these may have been changed by the user) to dict config_dict = toml.load(config_path) # Update path TidalPy._config_path = config_path return config_dict
[docs] def set_config(new_config_path: Union[str, dict]) -> dict: """Sets TidalPy's configuration based on a provided configuration file path. Parameters ---------- config_path : str Path to the configuration file the user wishes to use. if set to "default" then the default config will be used. """ new_config_name = 'unknown' if isinstance(new_config_path, dict): new_config = new_config_path new_config_name = 'User-provided dict' elif isinstance(new_config_path, str): new_config_name = f'{new_config_path}' if new_config_path.lower() == 'default': # Use default path. new_config = get_default_config() else: # Check if file exists if not os.path.isfile(new_config_path): raise InitializationError(f'Provided configuration path is not a file: {new_config_path}.') # Check if the provided configuration file is for the correct version of TidalPy. check_config_version(new_config_path, warn_on_false=True, raise_on_false=False) # Update path TidalPy._config_path = new_config_path # Load configurations (these may have been changed by the user) to dict new_config = toml.load(new_config_path) else: raise TypeError("Unexpected type found for TidalPy config replacement. Expected configuration file filepath (str) or config (dict).") TidalPy.config = get_default_config() # Set or override configurations with this new config file. if TidalPy.config is None: # No config has been loaded. Use this as the base config. TidalPy.config = new_config else: # A base config has already been loaded, override the base with any items from this new config. TidalPy.config = dict_replace_value(TidalPy.config, new_config) if TidalPy._tidalpy_init: from TidalPy.logger import get_logger log = get_logger('TidalPy') log.debug(f"TidalPy Configs overridden by {new_config_name}.")
[docs] def get_default_world_dir() -> str: """ Find the directory containing TidalPy's world configuration files. If no directory is found (likely when TidalPy is used for the first time) then default configurations will be saved to disk first. """ worlds_dir = get_worlds_dir() install_worlds = True # Use a test world file to check that the default worlds are installed. # TODO: Update extension if/when converting world configs to toml. io_config = os.path.join(worlds_dir, 'io.toml') if os.path.isfile(io_config): # TODO: Have a check here to see if world config version matches tidalpy and rebuild if it doesn't? install_worlds = False if install_worlds: # Install worlds to world config. tpy_path = os.path.dirname(os.path.realpath(__file__)) world_config_zip = os.path.join(tpy_path, 'WorldPack', 'WorldPack.zip') if not os.path.isfile(world_config_zip): raise InitializationError("Can not find TidalPy's WorldPack. " + \ "There may have been an issue during TidalPy's installation.") import zipfile with zipfile.ZipFile(world_config_zip, 'r') as zip_ref: zip_ref.extractall(worlds_dir) # Re-perform Io test. if not os.path.isfile(io_config): raise InitializationError("Can not find Io configuration after WorldPack installation.") return worlds_dir
[docs] def set_world_dir(world_dir_path: str): """Sets TidalPy's worlds config file directory based on a provided directory path. Parameters ---------- world_dir_path : str Path to the worlds directory the user wishes to use. if set to "default" then the default directory will be used. """ if world_dir_path.lower() == 'default': # Use default path. TidalPy.world_config_dir = get_default_world_dir() else: # TODO: Check if the provided directory has files compatible with the correct version of TidalPy. TidalPy.world_config_dir = world_dir_path