diff --git a/hatchet/__init__.py b/hatchet/__init__.py index 9f0a9eb2..510d35f9 100644 --- a/hatchet/__init__.py +++ b/hatchet/__init__.py @@ -8,3 +8,4 @@ from .graphframe import GraphFrame from .query import QueryMatcher +from .util.rcmanager import RcParams diff --git a/hatchet/tests/rctest.py b/hatchet/tests/rctest.py new file mode 100644 index 00000000..01f92b78 --- /dev/null +++ b/hatchet/tests/rctest.py @@ -0,0 +1,125 @@ +from hatchet.util.rcmanager import ( + RcManager, + ConfigValidator, + _resolve_conf_file, + _read_config_from_file, +) + + +def test_creation(): + file = _resolve_conf_file() + config = _read_config_from_file(file) + RC = RcManager(config) + + assert RC is not None + + +def test_global_binding(): + import hatchet as ht + + assert hasattr(ht, "RcParams") + + +def test_change_value(): + import hatchet as ht + from hatchet.util.rcmanager import RcParams + + assert "logging" in ht.RcParams + + ht.RcParams["logging"] = True + assert ht.RcParams["logging"] is True + assert RcParams["logging"] is True + + ht.RcParams["logging"] = False + assert ht.RcParams["logging"] is False + assert RcParams["logging"] is False + + +def test_validator(): + V = ConfigValidator() + + # Add validators for keys where we can input + # bad values + V._validations["bad_bool"] = V.bool_validator + V._validations["bad_string"] = V.str_validator + V._validations["bad_int"] = V.int_validator + V._validations["bad_float"] = V.float_validator + V._validations["bad_list"] = V.list_validator + V._validations["bad_dict"] = V.dict_validator + + # Test passes if exception is thrown + # Else: test fails + try: + V.validate("bad_bool", "True") + assert False + except TypeError: + assert True + + try: + V.validate("bad_string", True) + assert False + except TypeError: + assert True + + try: + V.validate("bad_int", "123") + assert False + except TypeError: + assert True + + try: + V.validate("bad_float", "1.2387") + assert False + except TypeError: + assert True + + try: + V.validate("bad_list", {}) + assert False + except TypeError: + assert True + + try: + V.validate("bad_dict", []) + assert False + except TypeError: + assert True + + # Testing valid inputs + # Goes through to true assertion if + # validation works + try: + V.validate("bad_bool", True) + assert True + except TypeError: + assert False + + try: + V.validate("bad_string", "string") + assert True + except TypeError: + assert False + + try: + V.validate("bad_int", 1) + assert True + except TypeError: + assert False + + try: + V.validate("bad_float", 1.2387) + assert True + except TypeError: + assert False + + try: + V.validate("bad_list", []) + assert True + except TypeError: + assert False + + try: + V.validate("bad_dict", {}) + assert True + except TypeError: + assert False diff --git a/hatchet/util/rcmanager.py b/hatchet/util/rcmanager.py new file mode 100644 index 00000000..6f0bf547 --- /dev/null +++ b/hatchet/util/rcmanager.py @@ -0,0 +1,156 @@ +# Copyright 2021 The University of Arizona and other Hatchet Project +# Developers. See the top-level LICENSE file for details. +# +# SPDX-License-Identifier: MIT + +try: + from collections.abc import MutableMapping +except ImportError: + from collections import MutableMapping +import os.path as path +import yaml + + +class ConfigValidator: + def __init__(self): + self._validations = {} + self._set_validators_to_configs() + + def bool_validator(self, key, value): + if not isinstance(value, bool): + raise TypeError( + 'Error loading configuration: Configuration "{}" must be of type bool'.format( + key + ) + ) + else: + return value + + def str_validator(self, key, value): + if not isinstance(value, str): + raise TypeError( + 'Error loading configuration: Configuration "{}" must be of type string'.format( + key + ) + ) + else: + return value + + def int_validator(self, key, value): + if not isinstance(value, int): + raise TypeError( + 'Error loading configuration: Configuration "{}" must be of type int'.format( + key + ) + ) + else: + return value + + def float_validator(self, key, value): + if not isinstance(value, float): + raise TypeError( + 'Error loading configuration: Configuration "{}" must be of type float'.format( + key + ) + ) + else: + return value + + def list_validator(self, key, value): + if not isinstance(value, list): + raise TypeError( + 'Error loading configuration: Configuration "{}" must be of type list'.format( + key + ) + ) + else: + return value + + def dict_validator(self, key, value): + if not isinstance(value, dict): + raise TypeError( + 'Error loading configuration: Configuration "{}" must be of type dict'.format( + key + ) + ) + else: + return value + + def validate(self, key, value): + return self._validations[key](key, value) + + def _set_validators_to_configs(self): + self._validations["logging"] = self.bool_validator + self._validations["log_directory"] = self.str_validator + self._validations["highlight_name"] = self.bool_validator + self._validations["invert_colormap"] = self.bool_validator + + +class RcManager(MutableMapping): + """ + A runtime configurations class; modeled after the RcParams class in matplotlib. + The benifit of this class over a dictonary is validation of set item and formatting + of output. + """ + + def __init__(self, *args, **kwargs): + self._data = {} + self._validator = ConfigValidator() + self._data.update(*args, **kwargs) + + def __setitem__(self, key, val): + """ + Function loads valid configurations and prints errors for invalid configs. + """ + try: + self._validator.validate(key, val) + return self._data.__setitem__(key, val) + except TypeError as e: + raise e + + def __getitem__(self, key): + return self._data[key] + + def __iter__(self): + for kv in sorted(self._data.__iter__(self)): + yield kv + + def __str__(self): + return "\n".join(map("{0[0]}: {0[1]}".format, sorted(self.items()))) + + def __len__(self): + return len(self._data) + + def __delitem__(self, item): + del self._data[item] + + +def _resolve_conf_file(): + """ + Determines which configuration file to load. + Uses the precendence order: + 1. $HOME/.hatchet/hatchetrc.yaml + 2. $HATCHET_BASE_DIR/hatchetrc.yaml + """ + home = path.expanduser("~") + conf_dir = path.join(home, ".hatchet", "hatchetrc.yaml") + if path.exists(conf_dir): + return conf_dir + else: + hatchet_path = path.abspath(path.dirname(path.abspath(__file__))) + rel_path = path.join(hatchet_path, "..", "..", "hatchetrc.yaml") + return rel_path + + +def _read_config_from_file(filepath): + configs = {} + with open(filepath, "r") as f: + configs = yaml.load(f, yaml.FullLoader) + return configs + + +filepath = _resolve_conf_file() +configs = _read_config_from_file(filepath) + +# Global instance of configurations +RcParams = RcManager(configs) diff --git a/hatchetrc.yaml b/hatchetrc.yaml new file mode 100644 index 00000000..e4676edd --- /dev/null +++ b/hatchetrc.yaml @@ -0,0 +1,30 @@ +############################################## +## ## +## Global configuration file for hatchet. ## +## ## +############################################## + +## If you would like to customize these global configurations +## please copy it to the following directory and edit there: +## $HOME/.hatchet/ +## +## This configurations file is considered in the following order of precedence: +## 1. $HOME/.hatchet/hatchetrc.yaml (optional) +## 2. $HATCHET_BASE_DIR/hatchetrc.yaml +## +## If no file can be found at $HOME/.hatchet then configurations are drawn from +## the hatchetrc.yaml file at the root of hatchet's installation location + +## +## Logging configuration +## + +logging: False +log_directory: "~" + +## +## Tree Output Configuration +## + +highlight_name: False +invert_colormap: False \ No newline at end of file