diff --git a/.gitignore b/.gitignore index 0893750..c263660 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ venv dist build wheel -.coverage \ No newline at end of file +.coverage +.pytest_cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..9074658 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: check-merge-conflict + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - repo: local + hooks: + - id: pytest + name: pytest + entry: pytest + language: python + types: [python] diff --git a/README.md b/README.md index 29a482a..e07b42e 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,13 @@ HOW TO USE --- By default, the config file will look for the following config files in the `.config` directory: `config.json`, `config.yaml`, `config.yml`. -You can also pass a config directory of your preference (assuming your current directory). +You can also pass a config directory and or config file of your preference (assuming your current directory). + +```python +from pyconfigparser import configparser + +configparser.get_config(CONFIG_SCHEMA, config_dir='your_config_dir_path', file_name='your_config_file_name') +``` Schema validation --- @@ -98,7 +104,7 @@ A json config file would be something like: } ``` -The instance of Config Class: +The config instance ```python from pyconfigparser import configparser, ConfigError import logging @@ -155,6 +161,18 @@ from pyconfigparser import configparser configparser.hold_an_instance = False ``` +Environment Variables Interpolation +--- +If the process does not find a value already set to your env variables +It will raise a ConfigError. But you can disable this behavior, and the parser will set `None` to these unresolved env vars + +```python +from pyconfigparser import configparser + +configparser.ignore_unset_env_vars = True +config = configparser.get_config() +``` + CONTRIBUTE --- --- diff --git a/pyconfigparser.py b/pyconfigparser.py index f6d7cdd..71f1ed0 100644 --- a/pyconfigparser.py +++ b/pyconfigparser.py @@ -45,6 +45,7 @@ class ConfigParser: def __init__(self): self.__instance = None self.__hold_an_instance = True + self.__ignore_unsetted_env_vars = False @property def hold_an_instance(self): @@ -56,15 +57,22 @@ def hold_an_instance(self, value): raise ValueError('value must be a bool') self.__hold_an_instance = value - def get_config(self, schema: dict = None, config_dir: str = 'config', file_name: Any = DEFAULT_CONFIG_FILES): + @property + def ignore_unset_env_vars(self): + return self.__ignore_unsetted_env_vars - if self.__instance is None: - instance = self.__create_new_instance(schema, config_dir, file_name) - if self.__hold_an_instance: - self.__instance = instance - else: - return instance - return self.__instance + @ignore_unset_env_vars.setter + def ignore_unset_env_vars(self, value): + if type(value) is not bool: + raise ValueError('value must be a bool') + self.__ignore_unsetted_env_vars = value + + def get_config(self, schema: dict = None, config_dir: str = 'config', file_name: Any = DEFAULT_CONFIG_FILES): + if self.__hold_an_instance: + if self.__instance is None: + self.__instance = self.__create_new_instance(schema, config_dir, file_name) + return self.__instance + return self.__create_new_instance(schema, config_dir, file_name) def __create_new_instance(self, schema, config_dir, file_name): file_path = self.__get_file_path(config_dir, file_name) @@ -125,6 +133,14 @@ def __dict_2_obj(self, data: Any): return self.__interpol_variable(data) return data + def __interpol_variable(self, data): + try: + return os.environ[self.__extract_env_variable_key(data)] + except KeyError: + if self.__ignore_unsetted_env_vars: + return None + raise ConfigError(f'Environment variable {data} was not found') + def __is_a_valid_object_key(self, key): if re.search(ENTITY_NAME_PATTERN, key) is None: raise ConfigError(f'The key {key} is invalid. The entity keys only may have words, number and underscores.') @@ -132,12 +148,6 @@ def __is_a_valid_object_key(self, key): def __is_variable(self, data): return type(data) is str and re.search(VARIABLE_PATTERN, data) is not None - def __interpol_variable(self, data): - try: - return os.environ[self.__extract_env_variable_key(data)] - except KeyError: - raise ConfigError(f'Environment variable {data} was not found') - def __extract_env_variable_key(self, variable): variable = variable[1:] if variable[0] == '{': diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..43b02ac --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +pre-commit>=1.2.2 diff --git a/requirements.txt b/requirements.txt index f1cb0f5..8d20263 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ schema>=0.7.1 -PyYaml>=3.12.0 \ No newline at end of file +PyYaml>=3.12.0 diff --git a/test_configparser.py b/test_configparser.py index e5c9405..8530ef1 100644 --- a/test_configparser.py +++ b/test_configparser.py @@ -1,4 +1,4 @@ -from pyconfigparser import configparser, ConfigError, ConfigFileNotFoundError +from pyconfigparser import ConfigParser, ConfigError, ConfigFileNotFoundError from config.schemas import SIMPLE_SCHEMA_CONFIG, UNSUPPORTED_OBJECT_KEYS_SCHEMA import unittest import os @@ -9,25 +9,32 @@ class ConfigTestCase(unittest.TestCase): def setUp(self) -> None: - configparser.hold_an_instance = False os.environ['DATE_FORMAT_TEST'] = DT_FMT_TEST os.environ['LOG_LEVEL_TEST'] = VAR_LOG_LEVEL_INFO def test_schema_checking(self): + configparser = ConfigParser() self.assertRaises(ConfigError, configparser.get_config, 1) def test_config_without_file(self): + configparser = ConfigParser() self.assertRaises(ConfigFileNotFoundError, configparser.get_config, SIMPLE_SCHEMA_CONFIG, 'config', 'some_non_exists_file.json') def test_undefined_env_var(self): try: + configparser = ConfigParser() configparser.get_config(file_name='config.yaml') except Exception as e: self.assertIn('Environment', str(e)) + configparser = ConfigParser() + configparser.ignore_unset_env_vars = True + configparser.get_config(file_name='config.yaml') + def test_to_access_attr_from_config(self): + configparser = ConfigParser() config = configparser.get_config(SIMPLE_SCHEMA_CONFIG) self.assertEqual(VAR_LOG_LEVEL_INFO, config.core.logging.level) self.assertEqual(DT_FMT_TEST, config.core.logging.datefmt) @@ -36,28 +43,55 @@ def test_to_access_attr_from_config(self): self.assertEqual('Mike', config.core.obj_list[0]['name']) # <- using subscriptable access def test_access_fake_attr(self): + configparser = ConfigParser() config = configparser.get_config(SIMPLE_SCHEMA_CONFIG) self.assertRaises(AttributeError, lambda: config.fake_attr) def test_unsupported_object_key(self): + configparser = ConfigParser() self.assertRaises(ConfigError, configparser.get_config, UNSUPPORTED_OBJECT_KEYS_SCHEMA, file_name='unsupported_object_key.json') - def test_set_hold_an_invalid_instance(self): - def assign_a_bad_type(): - configparser.hold_an_instance = [] - self.assertRaises(ValueError, assign_a_bad_type) - def test_config_with_wrong_json_model(self): + configparser = ConfigParser() self.assertRaises(ConfigError, configparser.get_config, SIMPLE_SCHEMA_CONFIG, file_name='wrong_model.json') def test_config_file_with_unsupported_extension(self): + configparser = ConfigParser() self.assertRaises(ConfigError, configparser.get_config, SIMPLE_SCHEMA_CONFIG, file_name='config.bad_extension') def test_bad_decoder_error(self): + configparser = ConfigParser() self.assertRaises(ConfigError, configparser.get_config, SIMPLE_SCHEMA_CONFIG, file_name='bad_content.json') self.assertRaises(ConfigError, configparser.get_config, SIMPLE_SCHEMA_CONFIG, file_name='bad_content.yaml') + def test_caching_instance(self): + configparser = ConfigParser() + config1 = configparser.get_config() + config2 = configparser.get_config() + self.assertIs(config1, config2) + configparser.hold_an_instance = False + + config2 = configparser.get_config() + self.assertIsNot(config1, config2) + + def test_configparser_config_switches(self): + configparser = ConfigParser() + + def assign_a_bad_type_hold_an_instance(): + configparser.hold_an_instance = [] + + def assign_a_bad_type_ignore_unsetted_env_vars(): + configparser.ignore_unset_env_vars = [] + + self.assertRaises(ValueError, assign_a_bad_type_hold_an_instance) + self.assertRaises(ValueError, assign_a_bad_type_ignore_unsetted_env_vars) + configparser.hold_an_instance = False + configparser.ignore_unset_env_vars = True + self.assertIs(configparser.hold_an_instance, False) + self.assertIs(configparser.ignore_unset_env_vars, True) + self.assertIsInstance(configparser.ignore_unset_env_vars, bool) + if __name__ == '__main__': unittest.main()