diff --git a/.env.example b/.env.example index b0b8141..0d5c876 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -ONION_CONFIG_EXTRA_DIR="./extra_configs_dir" +ONION_CONFIG_EXTRA_DIR="./extra_dir" diff --git a/.gitignore b/.gitignore index ed9a659..4da85ae 100644 --- a/.gitignore +++ b/.gitignore @@ -493,7 +493,6 @@ logs backup backups archive -extra_configs pythonpath *.bak *.pyc @@ -511,5 +510,4 @@ logs/ backup/ backups/ archive/ -extra_configs/ pythonpath/ diff --git a/README.md b/README.md index f553e26..4b7591c 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,12 @@ config: BaseConfig = ConfigLoader(auto_load=True).config ### **Simple** +[**`.env`**](https://github.com/bybatkhuu/mod.python-config/blob/main/examples/simple/.env) + +```sh +ENV=production +``` + [**`configs/1.base.yml`**](https://github.com/bybatkhuu/mod.python-config/blob/main/examples/simple/configs/1.base.yml): ```yaml @@ -162,8 +168,12 @@ logging.basicConfig(stream=sys.stdout, level=logging.INFO) logger = logging.getLogger(__name__) +class ConfigSchema(BaseConfig): + env: str = "local" + + try: - config: BaseConfig = ConfigLoader().load() + config: ConfigSchema = ConfigLoader(config_schema=ConfigSchema).load() except Exception: logger.exception("Failed to load config:") exit(2) @@ -191,27 +201,32 @@ INFO:__main__: Config: 'name': 'New App', 'nested': {'key': 'value', 'some': 'value'}, 'version': '0.0.1'}, - 'env': 'test'} + 'env': 'production'} ``` ### **Advanced** -[**`configs/logger.json`**](https://github.com/bybatkhuu/mod.python-config/blob/main/examples/advanced/configs/logger.json): +[**`.env.base`**](https://github.com/bybatkhuu/mod.python-config/blob/main/examples/advanced/.env.base): -```json -{ - "logger": - { - "level": "info", - "output": "file" - } -} +```sh +ENV=development +DEBUG=true +APP_NAME="Old App" +ONION_CONFIG_EXTRA_DIR="extra_configs" +``` + +[**`.env.prod`**](https://github.com/bybatkhuu/mod.python-config/blob/main/examples/advanced/.env.prod): + +```sh +ENV=production +APP_NAME="New App" +APP_SECRET="my_secret" ``` [**`configs/config.yml`**](https://github.com/bybatkhuu/mod.python-config/blob/main/examples/advanced/configs/config.yml): ```yaml -env: production +env: local app: name: "My App" @@ -221,18 +236,44 @@ app: ignore_val: "Ignore me" logger: - output: "stdout" + output: "file" +``` + +[**`configs/logger.json`**](https://github.com/bybatkhuu/mod.python-config/blob/main/examples/advanced/configs/logger.json): + +```json +{ + "logger": { + "level": "info", + "output": "stdout" + } +} ``` -[**`.env`**](https://github.com/bybatkhuu/mod.python-config/blob/main/examples/advanced/.env): +[**`configs_2/config.yml`**](https://github.com/bybatkhuu/mod.python-config/blob/main/examples/advanced/configs_2/config.yml): -```sh -ENV=development +```yaml +extra: + config: + key1: 1 +``` -DEBUG=true +[**`configs_2/config_2.yml`**](https://github.com/bybatkhuu/mod.python-config/blob/main/examples/advanced/configs_2/config_2.yml): -APP_NAME="New App" -APP_SECRET="my_secret" +```yaml +extra: + config: + key2: 2 +``` + +[**`extra_configs/extra.json`**](https://github.com/bybatkhuu/mod.python-config/blob/main/examples/advanced/extra_configs/extra.json): + +```json +{ + "extra": { + "type": "json" + } +} ``` [**`logger.py`**](https://github.com/bybatkhuu/mod.python-config/blob/main/examples/advanced/logger.py): @@ -301,7 +342,12 @@ def _pre_load_hook(config_data: dict) -> dict: config = None try: _config_loader = ConfigLoader( - config_schema=ConfigSchema, pre_load_hook=_pre_load_hook + config_schema=ConfigSchema, + configs_dirs=["configs", "configs_2", "/not_exixts/path/configs_3"], + env_file_paths=[".env", ".env.base", ".env.prod"], + pre_load_hook=_pre_load_hook, + config_data={"base": "start_value"}, + quiet=False, ) # Main config object: config: ConfigSchema = _config_loader.load() @@ -346,10 +392,12 @@ python ./app.py Output: ```txt -INFO:logger: ENV: development +WARNING:onion_config._base:'/home/user/workspaces/projects/onion_config/examples/advanced/.env' file is not exist! +WARNING:onion_config._base:'/not_exixts/path/configs_3' directory is not exist! +INFO:logger: ENV: production INFO:logger: DEBUG: True INFO:logger: Extra: Something extra! -INFO:logger: Logger: {'level': 'info', 'output': 'stdout'} +INFO:logger: Logger: {'output': 'stdout', 'level': 'info'} INFO:logger: App: name='New App' bind_host='0.0.0.0' port=80 secret=SecretStr('**********') version='0.0.1' description=None INFO:logger: Secret: 'my_secret' @@ -360,8 +408,10 @@ INFO:logger: Config: 'port': 80, 'secret': SecretStr('**********'), 'version': '0.0.1'}, + 'base': 'start_value', 'debug': True, - 'env': , + 'env': , + 'extra': {'config': {'key1': 1, 'key2': 2}, 'type': 'json'}, 'extra_val': 'Something extra!', 'logger': {'level': 'info', 'output': 'stdout'}} @@ -390,19 +440,23 @@ python -m pytest -sv Load order: -1. Load environment variables from `.env` file. -2. Check required environment variables are exist or not. -3. Load config files from `configs_dir` into `config_data`. -4. Load extra config files from `extra_configs_dir` into `config_data`. +1. Load all dotenv files from `env_file_paths` into environment variables. +1.1. Load each dotenv file into environment variables. +2. Check if required environment variables exist or not. +3. Load all config files from `configs_dirs` into `config_data`. +3.1. Load config files from each config directory into `config_data`. +3.1.a. Load each YAML config file into `config_data`. +3.1.b. Load each JSON config file into `config_data`. +4. Load extra config files from `extra_dir` into `config_data`. 5. Execute `pre_load_hook` method to modify `config_data`. -6. Init `config_schema` with `config_data` into final **`config`**. +6. Init `config_schema` with `config_data` into final `config`. ## Environment Variables You can use the following environment variables inside [**`.env.example`**](https://github.com/bybatkhuu/mod.python-config/blob/main/.env.example) file: ```sh -ONION_CONFIG_EXTRA_DIR="./extra_configs_dir" +ONION_CONFIG_EXTRA_DIR="./extra_dir" ``` ## Documentation diff --git a/docs/README.md b/docs/README.md index 758385e..49392c6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,36 +13,45 @@ The `ConfigLoader` class is the main class of the `onion_config` package. An ins `ConfigLoader` instances have the following properties: -- **config**: Main config object (config_schema) for the project. It is an instance of a `BaseConfig`, `BaseSettings`, or `BaseModel` class, which holds the loaded and validated configuration data. Defaults to `None`. +- **config**: Main config object (based on `config_schema`) for the project. It is an instance of a `BaseConfig`, `BaseSettings`, or `BaseModel` class, which holds the loaded and validated configuration data. Defaults to `BaseConfig`. - **config_schema**: Main config schema class to load and validate configs. Defaults to `BaseConfig`. - **config_data**: Loaded data from config files as a dictionary. Defaults to `{}`. -- **configs_dir**: Main configs directory to load all config files. Defaults to `'./configs'`. -- **extra_configs_dir**: Extra configs directory to load extra config files. Defaults to `None`, but will use the `ONION_CONFIG_EXTRA_DIR` environment variable if set. -- **env_file_path**: '.env' file path to load. Defaults to `.env`. +- **configs_dirs**: Main configs directories as list to load all config files. Defaults to `['./configs']`. +- **extra_dir**: Extra configs directory to load extra config files. Defaults to `None`, but will use the `ONION_CONFIG_EXTRA_DIR` environment variable if set. +- **env_file_paths**: Dotenv file paths as list to load. Defaults to `['.env']`. - **required_envs**: Required environment variables to check. Defaults to `[]`. - **pre_load_hook**: Custom pre-load method, this method will be executed before validating `config`. Defaults to `lambda config_data: config_data`. +- **quiet**: Quiet mode to suppress all warning logs. Defaults to `True`. ### Methods `ConfigLoader` instances have the following methods: - **load()**: 0. Load and validate every config into `config`. -- **_load_dotenv()**: 1. Loading environment variables from `.env` file, if it exists. +- **_load_dotenv_files()**: 1. Load all dotenv files from `env_file_paths` into environment variables. +- **_load_dotenv_file()**: 1.1. Load each dotenv file into environment variables. - **_check_required_envs()**: 2. Check if required environment variables exist. -- **_load_config_files()**: 3. Load config files from `configs_dir` into `config_data`. -- **_load_extra_config_files()**: 4. Load extra config files from `extra_configs_dir` and update `config_data`. +- **_load_configs_dirs()**: 3. Load all config files from `configs_dirs` into `config_data`. +- **_load_configs_dir()**: 3.1. Load config files from each config directory into `config_data`. +- **_load_yaml_file()**: 3.1.a. Load each YAML config file into `config_data`. +- **_load_json_file()**: 3.1.b. Load each JSON config file into `config_data`. +- **_load_extra_dir()**: 4. Load extra config files from `extra_dir` into `config_data`. ### Load order -1. Load environment variables from `.env` file. -2. Check required environment variables are exist or not. -3. Load config files from `configs_dir` into `config_data`. -4. Load extra config files from `extra_configs_dir` into `config_data`. +1. Load all dotenv files from `env_file_paths` into environment variables. +1.1. Load each dotenv file into environment variables. +2. Check if required environment variables exist or not. +3. Load all config files from `configs_dirs` into `config_data`. +3.1. Load config files from each config directory into `config_data`. +3.1.a. Load each YAML config file into `config_data`. +3.1.b. Load each JSON config file into `config_data`. +4. Load extra config files from `extra_dir` into `config_data`. 5. Execute `pre_load_hook` method to modify `config_data`. -6. Init `config_schema` with `config_data` into final **`config`**. +6. Init `config_schema` with `config_data` into final `config`. ## BaseConfig Class -The `BaseConfig` class is a subclass of `pydantic.BaseSettings` that is used as the default schema for validating configuration data in `onion_config`. It allows for arbitrary types and additional properties (extra). +The `BaseConfig` class is a subclass of `pydantic_settings.BaseSettings` that is used as the default schema for validating configuration data in `onion_config`. It allows for arbitrary types and additional properties (extra). -It also overrides the `customise_sources` method to specify the order in which the configuration sources are checked. In `onion_config`, the environment variables are checked first, followed by the initial settings and then the file secret settings. +It also overrides the `customise_sources` method to specify the order in which the configuration sources are checked. In `onion_config`, the dotenv files are checked first, environment variables are second, followed by the initial settings and then the file secret settings. diff --git a/docs/scripts/4.test.md b/docs/scripts/4.test.md new file mode 100644 index 0000000..d8fb929 --- /dev/null +++ b/docs/scripts/4.test.md @@ -0,0 +1,18 @@ +# test.sh + +This script is used to run the pytest tests for the project. + +The script performs the following operations: + +- **Loading base script**: Includes the `base.sh` script to gain access to its utility functions and environment variables. +- **Running pytest**: Runs the pytest tests for the project. + +**Usage**: + +To execute the test script, simply run the following command in the terminal: + +```sh +./test.sh +``` + +**Source code**: [**`test.sh`**](../../scripts/test.sh) diff --git a/docs/scripts/4.bump-version.md b/docs/scripts/5.bump-version.md similarity index 100% rename from docs/scripts/4.bump-version.md rename to docs/scripts/5.bump-version.md diff --git a/docs/scripts/5.build.md b/docs/scripts/6.build.md similarity index 100% rename from docs/scripts/5.build.md rename to docs/scripts/6.build.md diff --git a/docs/scripts/README.md b/docs/scripts/README.md index 89ff5ad..234eba4 100644 --- a/docs/scripts/README.md +++ b/docs/scripts/README.md @@ -5,8 +5,9 @@ This document provides an overview and usage instructions for the following scri - [**`base.sh`**](./1.base.md) - [**`clean.sh`**](./2.clean.md) - [**`get-version.sh`**](./3.get-version.md) -- [**`bump-version.sh`**](./4.bump-version.md) -- [**`build.sh`**](./5.build.md) +- [**`test.sh`**](./4.test.md) +- [**`bump-version.sh`**](./5.bump-version.md) +- [**`build.sh`**](./6.build.md) All the scripts are located in the [**`scripts`**](../../scripts) directory: @@ -16,7 +17,8 @@ scripts/ ├── build.sh ├── bump-version.sh ├── clean.sh -└── get-version.sh +├── get-version.sh +└── test.sh ``` These scripts are designed to be used in a Linux or macOS environment. They may work in a Windows environment with the appropriate tools installed, but this is not guaranteed. diff --git a/examples/advanced/.env.base b/examples/advanced/.env.base new file mode 100644 index 0000000..1c8f34a --- /dev/null +++ b/examples/advanced/.env.base @@ -0,0 +1,4 @@ +ENV=development +DEBUG=true +APP_NAME="Old App" +ONION_CONFIG_EXTRA_DIR="extra_configs" diff --git a/examples/advanced/.env b/examples/advanced/.env.prod similarity index 59% rename from examples/advanced/.env rename to examples/advanced/.env.prod index c00c39d..4f78da9 100644 --- a/examples/advanced/.env +++ b/examples/advanced/.env.prod @@ -1,6 +1,3 @@ -ENV=development - -DEBUG=true - +ENV=production APP_NAME="New App" APP_SECRET="my_secret" diff --git a/examples/advanced/config.py b/examples/advanced/config.py index a091ba1..79279fd 100644 --- a/examples/advanced/config.py +++ b/examples/advanced/config.py @@ -12,11 +12,15 @@ def _pre_load_hook(config_data: dict) -> dict: config_data["extra_val"] = "Something extra!" return config_data - config = None try: _config_loader = ConfigLoader( - config_schema=ConfigSchema, pre_load_hook=_pre_load_hook + config_schema=ConfigSchema, + configs_dirs=["configs", "configs_2", "/not_exixts/path/configs_3"], + env_file_paths=[".env", ".env.base", ".env.prod"], + pre_load_hook=_pre_load_hook, + config_data={"base": "start_value"}, + quiet=False, ) # Main config object: config: ConfigSchema = _config_loader.load() diff --git a/examples/advanced/configs/config.yml b/examples/advanced/configs/config.yml index 147b572..fcebd9e 100644 --- a/examples/advanced/configs/config.yml +++ b/examples/advanced/configs/config.yml @@ -1,4 +1,4 @@ -env: production +env: local app: name: "My App" @@ -8,4 +8,4 @@ app: ignore_val: "Ignore me" logger: - output: "stdout" + output: "file" diff --git a/examples/advanced/configs/logger.json b/examples/advanced/configs/logger.json index 0593d78..32c6df0 100644 --- a/examples/advanced/configs/logger.json +++ b/examples/advanced/configs/logger.json @@ -1,7 +1,6 @@ { - "logger": - { + "logger": { "level": "info", - "output": "file" + "output": "stdout" } -} +} \ No newline at end of file diff --git a/examples/advanced/configs_2/config.yml b/examples/advanced/configs_2/config.yml new file mode 100644 index 0000000..e73430f --- /dev/null +++ b/examples/advanced/configs_2/config.yml @@ -0,0 +1,3 @@ +extra: + config: + key1: 1 diff --git a/examples/advanced/configs_2/config_2.yml b/examples/advanced/configs_2/config_2.yml new file mode 100644 index 0000000..b7c2c82 --- /dev/null +++ b/examples/advanced/configs_2/config_2.yml @@ -0,0 +1,3 @@ +extra: + config: + key2: 2 diff --git a/examples/advanced/extra_configs/extra.json b/examples/advanced/extra_configs/extra.json new file mode 100644 index 0000000..7e90320 --- /dev/null +++ b/examples/advanced/extra_configs/extra.json @@ -0,0 +1,5 @@ +{ + "extra": { + "type": "json" + } +} \ No newline at end of file diff --git a/examples/advanced/logger.py b/examples/advanced/logger.py index 6cac7d7..3856b32 100644 --- a/examples/advanced/logger.py +++ b/examples/advanced/logger.py @@ -3,6 +3,5 @@ import sys import logging - logging.basicConfig(stream=sys.stdout, level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/examples/simple/.env b/examples/simple/.env new file mode 100644 index 0000000..4fdbe6f --- /dev/null +++ b/examples/simple/.env @@ -0,0 +1 @@ +ENV=production diff --git a/examples/simple/configs/1.base.yml b/examples/simple/configs/1.base.yml index 0a3e268..4448f19 100644 --- a/examples/simple/configs/1.base.yml +++ b/examples/simple/configs/1.base.yml @@ -1,4 +1,4 @@ -env: test +env: local app: name: "My App" diff --git a/examples/simple/main.py b/examples/simple/main.py index 5d33724..86bf3f5 100755 --- a/examples/simple/main.py +++ b/examples/simple/main.py @@ -12,8 +12,12 @@ logger = logging.getLogger(__name__) +class ConfigSchema(BaseConfig): + env: str = "local" + + try: - config: BaseConfig = ConfigLoader().load() + config: ConfigSchema = ConfigLoader(config_schema=ConfigSchema).load() except Exception: logger.exception("Failed to load config:") exit(2) diff --git a/onion_config/_base.py b/onion_config/_base.py index 58589f2..c8b74c8 100644 --- a/onion_config/_base.py +++ b/onion_config/_base.py @@ -27,25 +27,34 @@ class ConfigLoader: """A core class of `onion_config` module to use as the main config loader. Attributes: - _ENV_FILE_PATH (str ): Default '.env' file path to load. Defaults to '${PWD}/.env'. - _CONFIGS_DIR (str ): Default configs directory. Defaults to '${PWD}/configs'. - _PRE_LOAD_HOOK (function): Default lambda function for `pre_load_hook`. Defaults to `lambda config_data: config_data`. - - config (Union[BaseConfig, BaseSettings, BaseModel]): Main config object (config_schema) for project. Defaults to None. - config_schema (Union[Type[BaseConfig], Type[BaseSettings], Type[BaseModel]]): Main config schema class to load and validate configs. Defaults to `BaseConfig`. - config_data (dict ): Loaded data from config files as a dictionary. Defaults to {}. - configs_dir (str ): Main configs directory to load all config files. Defaults to `ConfigLoader._CONFIGS_DIR`. - extra_configs_dir (str ): Extra configs directory to load extra config files. Defaults to None, but will use the 'ONION_CONFIG_EXTRA_DIR' environment variable if set. - env_file_path (str ): '.env' file path to load. Defaults to `ConfigLoader._ENV_FILE_PATH`. - required_envs (str ): Required environment variables to check. Defaults to []. - pre_load_hook (function): Custom pre-load method, this method will executed before validating `config`. Defaults to `ConfigLoader._PRE_LOAD_HOOK`. + _ENV_FILE_PATH (str ): Default dotenv file path to load. Defaults to '${PWD}/.env'. + _CONFIGS_DIR (str ): Default configs directory. Defaults to '${PWD}/configs'. + _PRE_LOAD_HOOK (function ): Default lambda function for `pre_load_hook`. Defaults to `lambda config_data: config_data`. + + config (Union[BaseConfig, + BaseSettings, + BaseModel ]): Main config object (based on `config_schema`) for project. Defaults to None. + config_schema (Union[Type[BaseConfig], + Type[BaseSettings], + Type[BaseModel] ]): Main config schema class to load and validate configs. Defaults to `BaseConfig`. + config_data (dict ): Loaded data from config files as a dictionary. Defaults to {}. + configs_dirs (str ): Main configs directories as to load all config files. Defaults to [ConfigLoader._CONFIGS_DIR]. + extra_dir (str ): Extra configs directory to load extra config files. Defaults to None, but will use the 'ONION_CONFIG_EXTRA_DIR' environment variable if set. + env_file_paths (str ): Dotenv file paths as to load. Defaults to [ConfigLoader._ENV_FILE_PATH]. + required_envs (str ): Required environment variables to check. Defaults to []. + pre_load_hook (function ): Custom pre-load method, this method will executed before validating `config`. Defaults to `ConfigLoader._PRE_LOAD_HOOK`. + quiet (bool ): If False, will show warning messages. Defaults to True. Methods: - load() : Load and validate every configs into `config`. - _load_dotenv() : Loading environment variables from '.env' file, if it's exits. - _check_required_envs() : Check required environment variables are exist or not. - _load_config_files() : Load config files from `configs_dir` into `config_data`. - _load_extra_config_files(): Load extra config files from `extra_configs_dir` into `config_data`. + load() : Load and validate every configs into `config`. + _load_dotenv_files() : Load all dotenv files from `env_file_paths` into environment variables. + _load_dotenv_file() : Load each dotenv file into environment variables. + _check_required_envs(): Check required environment variables are exist or not. + _load_configs_dirs() : Load all config files from `configs_dirs` into `config_data`. + _load_configs_dir() : Load config files from each config directory into `config_data`. + _load_yaml_file() : Load each YAML config file into `config_data`. + _load_json_file() : Load each JSON config file into `config_data`. + _load_extra_dir() : Load extra config files from `extra_dir` into `config_data`. """ _ENV_FILE_PATH = os.path.join(os.getcwd(), ".env") @@ -55,74 +64,84 @@ class ConfigLoader: @validate_call def __init__( self, - configs_dir: str = _CONFIGS_DIR, config_schema: Union[ Type[BaseConfig], Type[BaseSettings], Type[BaseModel] ] = BaseConfig, - pre_load_hook: Callable = _PRE_LOAD_HOOK, - env_file_path: str = _ENV_FILE_PATH, + configs_dirs: Union[List[str], str] = _CONFIGS_DIR, + env_file_paths: Union[List[str], str] = _ENV_FILE_PATH, required_envs: List[str] = [], + pre_load_hook: Callable = _PRE_LOAD_HOOK, + extra_dir: Union[str, None] = None, config_data: dict = {}, - extra_configs_dir: Union[str, None] = None, + quiet: bool = True, auto_load: bool = False, ): """ConfigLoader constructor method. Args: - configs_dir (str, optional): Main configs directory to load all config files. Defaults to `ConfigLoader._CONFIGS_DIR`. - config_schema (Union[Type[BaseConfig], Type[BaseSettings], Type[BaseModel]], optional): Main config schema class to load and validate configs. Defaults to `BaseConfig`. - pre_load_hook (function, optional): Custom pre-load method, this method will executed before validating `config`. Defaults to `ConfigLoader._PRE_LOAD_HOOK`. - env_file_path (str, optional): '.env' file path to load. Defaults to `ConfigLoader._ENV_FILE_PATH`. - required_envs (list, optional): Required environment variables to check. Defaults to []. - config_data (dict, optional): Custom config data to load before validating `config`. Defaults to {}. - extra_configs_dir (str, optional): Extra configs directory to load extra config files. Defaults to None. - auto_load (bool, optional): Auto load configs on init or not. Defaults to False. + config_schema (Union[Type[BaseConfig], + Type[BaseSettings], + Type[BaseModel]] , optional): Main config schema class to load and validate configs. Defaults to `BaseConfig`. + configs_dirs (Union[List[str], str] , optional): Main configs directories as or to load all config files. Defaults to `ConfigLoader._CONFIGS_DIR`. + env_file_paths (Union[List[str], str] , optional): Dotenv file paths as or to load. Defaults to `ConfigLoader._ENV_FILE_PATH`. + required_envs (List[str] , optional): Required environment variables to check. Defaults to []. + pre_load_hook (function , optional): Custom pre-load method, this method will executed before validating `config`. Defaults to `ConfigLoader._PRE_LOAD_HOOK`. + extra_dir (Union[str, None] , optional): Extra configs directory to load extra config files. Defaults to None. + config_data (dict , optional): Base config data as before everything. Defaults to {}. + quiet (bool , optional): If False, will show warning messages. Defaults to True. + auto_load (bool , optional): Auto load configs on init or not. Defaults to False. """ - self.configs_dir = configs_dir self.config_schema = config_schema - self.pre_load_hook = pre_load_hook - self.env_file_path = env_file_path + self.configs_dirs = configs_dirs + self.env_file_paths = env_file_paths self.required_envs = required_envs + self.pre_load_hook = pre_load_hook + if extra_dir: + self.extra_dir = extra_dir self.config_data = config_data - if extra_configs_dir: - self.extra_configs_dir = extra_configs_dir + self.quiet = quiet if auto_load: self.load() + @validate_call def load(self) -> Union[BaseConfig, BaseSettings, BaseModel]: """Load and validate every configs into `config`. Load order: - 1. Load environment variables from '.env' file. - 2. Check required environment variables are exist or not. - 3. Load config files from `configs_dir` into `config_data`. - 4. Load extra config files from `extra_configs_dir` into `config_data`. - 5. Execute `pre_load_hook` method to modify `config_data`. - 6. Init `config_schema` with `config_data` into final `config`. - - Returns: - Union[BaseConfig, BaseSettings, BaseModel]: Main config object (config_schema) for project. + 1. Load all dotenv files from `env_file_paths` into environment variables. + 1.1. Load each dotenv file into environment variables. + 2. Check if required environment variables exist or not. + 3. Load all config files from `configs_dirs` into `config_data`. + 3.1. Load config files from each config directory into `config_data`. + 3.1.a. Load each YAML config file into `config_data`. + 3.1.b. Load each JSON config file into `config_data`. + 4. Load extra config files from `extra_dir` into `config_data`. + 5. Execute `pre_load_hook` method to modify `config_data`. + 6. Init `config_schema` with `config_data` into final `config`. Raises: - KeyError : If a required environment variable does not exist. - Exception: If `pre_load_hook` or `config_schema` failed to execute. + Exception: If `pre_load_hook` method failed to execute. + Exception: If `config_schema` failed to init. + + Returns: + Union[BaseConfig, BaseSettings, BaseModel]: Main config object (based on `config_schema`) for project. """ - self._load_dotenv() + self._load_dotenv_files() self._check_required_envs() - self._load_config_files() - self._load_extra_config_files() + self._load_configs_dirs() + self._load_extra_dir() try: - # 5. Execute `pre_load_hook` method: + # 5. Execute `pre_load_hook` method to modify `config_data`: self.config_data: dict = self.pre_load_hook(self.config_data) except Exception: logger.critical("Failed to execute `pre_load_hook` method:") raise try: - # 6. Init `config_schema` with `config_data`: + # 6. Init `config_schema` with `config_data` into final `config`: self.config: Union[ BaseConfig, BaseSettings, BaseModel ] = self.config_schema(**self.config_data) @@ -132,16 +151,34 @@ def load(self) -> Union[BaseConfig, BaseSettings, BaseModel]: return self.config - def _load_dotenv(self): - """1. Loading environment variables from '.env' file, if it's exits.""" + def _load_dotenv_files(self): + """1. Load all dotenv files from `env_file_paths` into environment variables.""" + + for _env_file_path in self.env_file_paths: + self._load_dotenv_file(env_file_path=_env_file_path) + + @validate_call + def _load_dotenv_file(self, env_file_path: str): + """1.1. Load each dotenv file into environment variables. + + Args: + env_file_path (str, required): Dotenv file path to load. + """ + + if not os.path.isabs(env_file_path): + env_file_path = os.path.join(os.getcwd(), env_file_path) - if os.path.isfile(self.env_file_path): - load_dotenv(dotenv_path=self.env_file_path, override=True, encoding="utf8") + if os.path.isfile(env_file_path): + load_dotenv(dotenv_path=env_file_path, override=True, encoding="utf8") + else: + if self.quiet: + logger.debug(f"'{env_file_path}' file is not exist!") + else: + logger.warning(f"'{env_file_path}' file is not exist!") def _check_required_envs(self): """2. Check if required environment variables exist or not. - - If a required environment variable does not exist, an error is logged and raise an exception. + If a required environment variable does not exist, raise an exception. Raises: KeyError: If a required environment variable does not exist. @@ -154,68 +191,124 @@ def _check_required_envs(self): logger.critical(f"Missing required '{_env}' environment variable.") raise + def _load_configs_dirs(self): + """3. Load all config files from `configs_dirs` into `config_data`.""" + + for _config_dir in self.configs_dirs: + self._load_configs_dir(configs_dir=_config_dir) + @validate_call - def _load_config_files(self, configs_dir: Union[str, None] = None): - """3. Load config files from `configs_dir` into `config_data`. + def _load_configs_dir(self, configs_dir: str): + """3.1. Load config files from each config directory into `config_data`. Args: - configs_dir (str, optional): Main configs directory to load all config files. Defaults to `ConfigLoader._CONFIGS_DIR`. + configs_dir (str, required): Configs directory to load. + """ + + if not os.path.isabs(configs_dir): + configs_dir = os.path.join(os.getcwd(), configs_dir) + + if os.path.isdir(configs_dir): + _file_paths = [] + _file_paths.extend(glob.glob(os.path.join(configs_dir, "*.yaml"))) + _file_paths.extend(glob.glob(os.path.join(configs_dir, "*.yml"))) + _file_paths.extend(glob.glob(os.path.join(configs_dir, "*.json"))) + # _file_paths.extend(glob.glob(os.path.join(configs_dir, "*.toml"))) + _file_paths.sort() + + for _file_path in _file_paths: + if _file_path.lower().endswith((".yml", ".yaml")): + self._load_yaml_file(file_path=_file_path) + elif _file_path.lower().endswith(".json"): + self._load_json_file(file_path=_file_path) + # elif _file_path.lower().endswith(".toml"): + # self._load_toml_file(file_path=_file_path) + else: + if self.quiet: + logger.debug(f"'{configs_dir}' directory is not exist!") + else: + logger.warning(f"'{configs_dir}' directory is not exist!") + + @validate_call + def _load_yaml_file(self, file_path: str): + """3.1.a. Load each YAML config file into `config_data`. + + Args: + file_path (str, required): YAML config file path to load. Raises: - Exception: If failed to load any config file. + Exception: If failed to load any YAML config file. """ - _configs_dir = self.configs_dir - if configs_dir: - _configs_dir = configs_dir - - if os.path.isdir(_configs_dir): - ## Loading all JSON config files from 'configs' directory: - _json_file_paths = sorted(glob.glob(os.path.join(_configs_dir, "*.json"))) - for _json_file_path in _json_file_paths: - try: - with open(_json_file_path, "r", encoding="utf8") as _json_file: - _new_config_dict = json.load(_json_file) or {} - self.config_data = deep_merge( - self.config_data, _new_config_dict - ) - except Exception: - logger.critical( - f"Failed to load '{_json_file_path}' json config file:" - ) - raise - - ## Loading all YAML config files from 'configs' directory: - _yaml_file_paths = glob.glob(os.path.join(_configs_dir, "*.yml")) - _yaml_file_paths.extend(glob.glob(os.path.join(_configs_dir, "*.yaml"))) - _yaml_file_paths.sort() - for _yaml_file_path in _yaml_file_paths: - try: - with open(_yaml_file_path, "r", encoding="utf8") as _yaml_file: - _new_config_dict = yaml.safe_load(_yaml_file) or {} - self.config_data = deep_merge( - self.config_data, _new_config_dict - ) - except Exception: - logger.critical( - f"Failed to load '{_yaml_file_path}' yaml config file:" - ) - raise - - def _load_extra_config_files(self): - """4. Load extra config files from `extra_configs_dir` into `config_data`. + if os.path.isfile(file_path): + try: + with open(file_path, "r", encoding="utf8") as _file: + _new_config_dict = yaml.safe_load(_file) or {} + self.config_data = deep_merge(self.config_data, _new_config_dict) + except Exception: + logger.critical(f"Failed to load '{file_path}' YAML config file:") + raise + else: + if not self.quiet: + logger.warning(f"'{file_path}' YAML config file is not exist!") + + @validate_call + def _load_json_file(self, file_path: str): + """3.1.b. Load each JSON config file into `config_data`. + + Args: + file_path (str, required): JSON config file path to load. Raises: - Exception: If failed to load any extra config file. + Exception: If failed to load any JSON config file. """ - if self.extra_configs_dir is None: - _env_extra_configs_dir = os.getenv("ONION_CONFIG_EXTRA_DIR") - if _env_extra_configs_dir: - self.extra_configs_dir = _env_extra_configs_dir + if os.path.isfile(file_path): + try: + with open(file_path, "r", encoding="utf8") as _file: + _new_config_dict = json.load(_file) or {} + self.config_data = deep_merge(self.config_data, _new_config_dict) + except Exception: + logger.critical(f"Failed to load '{file_path}' JSON config file:") + raise + else: + if not self.quiet: + logger.warning(f"'{file_path}' JSON config file is not exist!") + + # @validate_call + # def _load_toml_file(self, file_path: str): + # """3.1.c. Load each TOML config file into `config_data`. + + # Args: + # file_path (str, required): TOML config file path to load. + + # Raises: + # Exception: If failed to load any TOML config file. + # """ - if self.extra_configs_dir: - self._load_config_files(configs_dir=self.extra_configs_dir) + # if os.path.isfile(file_path): + # try: + # import toml + + # with open(file_path, "r", encoding="utf8") as _file: + # _new_config_dict = toml.load(_file) or {} + # self.config_data = deep_merge(self.config_data, _new_config_dict) + # except Exception: + # logger.critical(f"Failed to load '{file_path}' TOML config file:") + # raise + # else: + # if not self.quiet: + # logger.warning(f"'{file_path}' TOML config file is not exist!") + + def _load_extra_dir(self): + """4. Load extra config files from `extra_dir` into `config_data`.""" + + _env_extra_dir = os.getenv("ONION_CONFIG_EXTRA_DIR") + if _env_extra_dir: + self.extra_dir = _env_extra_dir + + if self.extra_dir: + self._load_configs_dir(configs_dir=self.extra_dir) ### ATTRIBUTES ### @@ -304,78 +397,100 @@ def config_data(self, config_data: dict): ## config_data ## - ## configs_dir ## + ## configs_dirs ## @property - def configs_dir(self) -> str: + def configs_dirs(self) -> List[str]: try: - return self.__configs_dir + return self.__configs_dirs except AttributeError: - self.__configs_dir = ConfigLoader._CONFIGS_DIR + self.__configs_dirs = [ConfigLoader._CONFIGS_DIR] - return self.__configs_dir + return self.__configs_dirs - @configs_dir.setter - def configs_dir(self, configs_dir: str): - if not isinstance(configs_dir, str): + @configs_dirs.setter + def configs_dirs(self, configs_dirs: Union[List[str], str]): + if (not isinstance(configs_dirs, str)) and (not isinstance(configs_dirs, list)): raise TypeError( - f"`configs_dir` attribute type {type(configs_dir)} is invalid, must be a !" + f"`configs_dirs` attribute type {type(configs_dirs)} is invalid, must be a or !" ) - configs_dir = configs_dir.strip() - if configs_dir == "": - raise ValueError("The `configs_dir` attribute value is empty!") + if isinstance(configs_dirs, str): + configs_dirs = configs_dirs.strip() + if configs_dirs == "": + raise ValueError("The `configs_dirs` attribute value is empty!") + + configs_dirs = [configs_dirs] + else: + configs_dirs = copy.deepcopy(configs_dirs) + + if not all(isinstance(_val, str) for _val in configs_dirs): + raise ValueError( + f"'configs_dirs' attribute value {configs_dirs} is invalid, must be a list of !" + ) - self.__configs_dir = configs_dir + self.__configs_dirs = configs_dirs - ## configs_dir ## + ## configs_dirs ## - ## extra_configs_dir ## + ## extra_dir ## @property - def extra_configs_dir(self) -> Union[str, None]: + def extra_dir(self) -> Union[str, None]: try: - return self.__extra_configs_dir + return self.__extra_dir except AttributeError: return None - @extra_configs_dir.setter - def extra_configs_dir(self, extra_configs_dir: str): - if not isinstance(extra_configs_dir, str): + @extra_dir.setter + def extra_dir(self, extra_dir: str): + if not isinstance(extra_dir, str): raise TypeError( - f"`extra_configs_dir` attribute type {type(extra_configs_dir)} is invalid, must be a !" + f"`extra_dir` attribute type {type(extra_dir)} is invalid, must be a !" ) - extra_configs_dir = extra_configs_dir.strip() - if extra_configs_dir == "": - raise ValueError("The `extra_configs_dir` attribute value is empty!") + extra_dir = extra_dir.strip() + if extra_dir == "": + raise ValueError("The `extra_dir` attribute value is empty!") - self.__extra_configs_dir = extra_configs_dir + self.__extra_dir = extra_dir - ## extra_configs_dir ## + ## extra_dir ## - ## env_file_path ## + ## env_file_paths ## @property - def env_file_path(self) -> str: + def env_file_paths(self) -> List[str]: try: - return self.__env_file_path + return self.__env_file_paths except AttributeError: - self.__env_file_path = ConfigLoader._ENV_FILE_PATH + self.__env_file_paths = [ConfigLoader._ENV_FILE_PATH] - return self.__env_file_path + return self.__env_file_paths - @env_file_path.setter - def env_file_path(self, env_file_path: str): - if not isinstance(env_file_path, str): + @env_file_paths.setter + def env_file_paths(self, env_file_paths: Union[List[str], str]): + if (not isinstance(env_file_paths, str)) and ( + not isinstance(env_file_paths, list) + ): raise TypeError( - f"The 'env_file_path' attribute type {type(env_file_path)} is invalid, must be a !" + f"'env_file_paths' attribute type {type(env_file_paths)} is invalid, must be a or !" ) - env_file_path = env_file_path.strip() - if env_file_path == "": - raise ValueError("The 'env_file_path' attribute value is empty!") + if isinstance(env_file_paths, str): + env_file_paths = env_file_paths.strip() + if env_file_paths == "": + raise ValueError("The `env_file_paths` attribute value is empty!") + + env_file_paths = [env_file_paths] + else: + env_file_paths = copy.deepcopy(env_file_paths) + + if not all(isinstance(_val, str) for _val in env_file_paths): + raise ValueError( + f"'env_file_paths' attribute value {env_file_paths} is invalid, must be a list of !" + ) - self.__env_file_path = env_file_path + self.__env_file_paths = env_file_paths - ## env_file_path ## + ## env_file_paths ## ## required_envs ## @property @@ -391,12 +506,12 @@ def required_envs(self) -> List[str]: def required_envs(self, required_envs: List[str]): if not isinstance(required_envs, list): raise TypeError( - f"The 'required_envs' attribute type {type(required_envs)} is invalid, must be a !" + f"'required_envs' attribute type {type(required_envs)} is invalid, must be a !" ) if not all(isinstance(_val, str) for _val in required_envs): raise ValueError( - f"The 'required_envs' attribute value {required_envs} is invalid, must be a list of !" + f"'required_envs' attribute value {required_envs} is invalid, must be a list of !" ) self.__required_envs = copy.deepcopy(required_envs) @@ -407,11 +522,11 @@ def required_envs(self, required_envs: List[str]): @property def pre_load_hook(self) -> Callable: try: - return self.__pre_load + return self.__pre_load_hook except AttributeError: - self.__pre_load = ConfigLoader._PRE_LOAD_HOOK + self.__pre_load_hook = ConfigLoader._PRE_LOAD_HOOK - return self.__pre_load + return self.__pre_load_hook @pre_load_hook.setter def pre_load_hook(self, pre_load_hook: Callable): @@ -420,7 +535,26 @@ def pre_load_hook(self, pre_load_hook: Callable): f"`pre_load_hook` argument type {type(pre_load_hook)} is invalid, should be callable !" ) - self.__pre_load = pre_load_hook + self.__pre_load_hook = pre_load_hook ## pre_load_hook ## + + ## quiet ## + @property + def quiet(self) -> bool: + try: + return self.__quiet + except AttributeError: + return True + + @quiet.setter + def quiet(self, quiet: bool): + if not isinstance(quiet, bool): + raise TypeError( + f"'quiet' attribute type {type(quiet)} is invalid, must be a !" + ) + + self.__quiet = quiet + + ## quiet ## ### ATTRIBUTES ### diff --git a/scripts/build.sh b/scripts/build.sh index 610e0c3..6bf7094 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -81,10 +81,7 @@ main() fi if [ "${_IS_TEST}" == true ]; then - echoInfo "Running test..." - # python -m pytest -sv -o log_cli=true || exit 2 - python -m pytest -sv || exit 2 - echoOk "Done." + ./scripts/test.sh || exit 2 fi echoInfo "Building package..." diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..b379dd3 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -euo pipefail + +## --- Base --- ## +# Getting path of this script file: +_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +_PROJECT_DIR="$(cd "${_SCRIPT_DIR}/.." >/dev/null 2>&1 && pwd)" +cd "${_PROJECT_DIR}" || exit 2 + +# Loading base script: +# shellcheck disable=SC1091 +source ./scripts/base.sh + + +if [ -z "$(which python)" ]; then + echoError "Python not found or not installed." + exit 1 +fi + +if [ -z "$(which pytest)" ]; then + echoError "Pytest not found or not installed." + exit 1 +fi +## --- Base --- ## + + +echoInfo "Running test..." +python -m pytest -sv || exit 2 +# python -m pytest -sv -o log_cli=true || exit 2 +# python -m pytest -sv --cov -o log_cli=true || exit 2 +echoOk "Done." diff --git a/tests/test_onion_config.py b/tests/test_onion_config.py index 38169e4..3f642b4 100644 --- a/tests/test_onion_config.py +++ b/tests/test_onion_config.py @@ -53,17 +53,17 @@ def test_init(config_loader): logger.info("Testing initialization of 'ConfigLoader'...") assert isinstance(config_loader, ConfigLoader) - assert isinstance(config_loader.configs_dir, str) - assert config_loader.configs_dir == ConfigLoader._CONFIGS_DIR + assert isinstance(config_loader.configs_dirs, list) + assert config_loader.configs_dirs == [ConfigLoader._CONFIGS_DIR] assert issubclass(config_loader.config_schema, BaseConfig) assert config_loader.config_schema == BaseConfig assert isinstance(config_loader.required_envs, list) assert config_loader.required_envs == [] assert isinstance(config_loader.pre_load_hook, Callable) assert config_loader.pre_load_hook == ConfigLoader._PRE_LOAD_HOOK - assert isinstance(config_loader.env_file_path, str) - assert config_loader.env_file_path == ConfigLoader._ENV_FILE_PATH - assert config_loader.extra_configs_dir == None + assert isinstance(config_loader.env_file_paths, list) + assert config_loader.env_file_paths == [ConfigLoader._ENV_FILE_PATH] + assert config_loader.extra_dir == None assert isinstance(config_loader.config_data, dict) assert config_loader.config_data == {} assert config_loader.config == None @@ -83,40 +83,43 @@ def test_load(config_loader): @pytest.mark.parametrize( - "configs_dir, config_schema, pre_load_hook, env_file_path, required_envs, config_data, extra_configs_dir, expected", + "config_schema, configs_dirs, env_file_paths, required_envs, pre_load_hook, extra_dir, config_data, quiet, expected", [ ( - ConfigLoader._CONFIGS_DIR, BaseConfig, - lambda config_data: config_data, + ConfigLoader._CONFIGS_DIR, ConfigLoader._ENV_FILE_PATH, [], - {}, + lambda config_data: config_data, None, {}, + True, + {}, ) ], ) def test_config_load( - configs_dir, config_schema, - pre_load_hook, - env_file_path, + configs_dirs, + env_file_paths, required_envs, + pre_load_hook, + extra_dir, config_data, - extra_configs_dir, + quiet, expected, ): logger.info("Testing main config cases...") _config = ConfigLoader( - configs_dir=configs_dir, config_schema=config_schema, - pre_load_hook=pre_load_hook, - env_file_path=env_file_path, + configs_dirs=configs_dirs, + env_file_paths=env_file_paths, required_envs=required_envs, + pre_load_hook=pre_load_hook, + extra_dir=extra_dir, config_data=config_data, - extra_configs_dir=extra_configs_dir, + quiet=quiet, ).load() assert isinstance(_config, config_schema) @@ -133,8 +136,8 @@ def test_config_load( ("TEST_ENV_VAR=123", "123"), ], ) -def test_load_dotenv(tmp_path, config_loader, content, expected): - logger.info("Testing 'load_dotenv' method...") +def test_load_dotenv_files(tmp_path, config_loader, content, expected): + logger.info("Testing '_load_dotenv_files' method...") _tmp_envs_dir_pl = tmp_path / "envs" _tmp_envs_dir_pl.mkdir() @@ -142,18 +145,18 @@ def test_load_dotenv(tmp_path, config_loader, content, expected): _tmp_env_file_pl.write_text(content) _tmp_env_path = str(_tmp_env_file_pl) - config_loader.env_file_path = _tmp_env_path - config_loader._load_dotenv() + config_loader.env_file_paths = _tmp_env_path + config_loader._load_dotenv_files() _env_var = content.split("=")[0] - assert config_loader.env_file_path == _tmp_env_path + assert config_loader.env_file_paths == [_tmp_env_path] assert os.getenv(_env_var) == expected - logger.info("Done: 'load_dotenv' method.") + logger.info("Done: '_load_dotenv_files' method.") def test_check_required_envs(config_loader): - logger.info("Testing 'check_required_envs' method...") + logger.info("Testing '_check_required_envs' method...") os.environ["REQUIRED_ENV_VAR"] = "required_value" @@ -169,15 +172,15 @@ def test_check_required_envs(config_loader): config_loader._check_required_envs() assert os.getenv(_none_existent_env_var) == None - logger.info("Done: 'check_required_envs' method.") + logger.info("Done: '_check_required_envs' method.") -def test_load_config_files(config_loader, configs_dir): - logger.info("Testing 'load_config_files' method...") +def test_load_configs_dirs(config_loader, configs_dir): + logger.info("Testing '_load_configs_dirs' method...") _configs_dir, _expected = configs_dir - config_loader.configs_dir = _configs_dir - config_loader._load_config_files() + config_loader.configs_dirs = [_configs_dir] + config_loader._load_configs_dirs() assert isinstance(config_loader.config_data, dict) assert config_loader.config_data["json_test"]["str_val"] == "some_value" @@ -185,28 +188,29 @@ def test_load_config_files(config_loader, configs_dir): assert config_loader.config_data["yaml_test"] == True assert config_loader.config_data == _expected - logger.info("Done: 'load_config_files' method.") + logger.info("Done: '_load_configs_dirs' method.") -def test_load_extra_config_files(tmp_path, config_loader, configs_dir): - logger.info("Testing 'load_extra_config_files' method...") +def test_load_extra_dir(tmp_path, config_loader, configs_dir): + logger.info("Testing '_load_extra_dir' method...") _configs_dir, _expected = configs_dir - config_loader._load_config_files(configs_dir=_configs_dir) + config_loader.configs_dirs = [_configs_dir] + config_loader._load_configs_dirs() - _tmp_extra_configs_dir_pl = tmp_path / "extra_configs" - _tmp_extra_configs_dir_pl.mkdir() - _tmp_yaml_file_pl = (_tmp_extra_configs_dir_pl / "test.yaml").resolve() + _tmp_extra_dir_pl = tmp_path / "extra_dir" + _tmp_extra_dir_pl.mkdir() + _tmp_yaml_file_pl = (_tmp_extra_dir_pl / "test.yaml").resolve() _tmp_yaml_file_pl.write_text( 'json_test:\n str_val: "updated_val"\n int_val: 321\nextra_test: "extra_value"' ) - _tmp_extra_configs_dir = str(_tmp_extra_configs_dir_pl) + _tmp_extra_dir = str(_tmp_extra_dir_pl) _expected["json_test"]["int_val"] = 321 _expected["json_test"]["str_val"] = "updated_val" _expected["extra_test"] = "extra_value" - config_loader.extra_configs_dir = _tmp_extra_configs_dir - config_loader._load_extra_config_files() + config_loader.extra_dir = _tmp_extra_dir + config_loader._load_extra_dir() assert isinstance(config_loader.config_data, dict) assert config_loader.config_data["json_test"]["str_val"] == "updated_val" @@ -214,7 +218,7 @@ def test_load_extra_config_files(tmp_path, config_loader, configs_dir): assert config_loader.config_data["extra_test"] == "extra_value" assert config_loader.config_data == _expected - logger.info("Done: 'load_extra_config_files' method.") + logger.info("Done: '_load_extra_dir' method.") def test_config(config_loader): @@ -225,7 +229,6 @@ class _ConfigSchema(BaseConfig): config_loader.config_schema = _ConfigSchema config_loader.load() - print(config_loader.config.model_dump()) assert isinstance(config_loader.config, _ConfigSchema) assert config_loader.config.test_var == "default_val" @@ -284,55 +287,55 @@ def test_config_data(config_loader): logger.info("Done: 'config_data' property.") -def test_configs_dir(config_loader): - logger.info("Testing 'configs_dir' property...") +def test_configs_dirs(config_loader): + logger.info("Testing 'configs_dirs' property...") - config_loader.configs_dir = "/tmp/pytest/configs_dir" - assert config_loader.configs_dir == "/tmp/pytest/configs_dir" + config_loader.configs_dirs = "/tmp/pytest/configs_dir" + assert config_loader.configs_dirs == ["/tmp/pytest/configs_dir"] with pytest.raises(TypeError): - config_loader.configs_dir = 3.14 - config_loader.configs_dir = False - config_loader.configs_dir = None + config_loader.configs_dirs = 3.14 + config_loader.configs_dirs = False + config_loader.configs_dirs = None with pytest.raises(ValueError): - config_loader.configs_dir = "" + config_loader.configs_dirs = "" - logger.info("Done: 'configs_dir' property.") + logger.info("Done: 'configs_dirs' property.") -def test_extra_configs_dir(config_loader): - logger.info("Testing 'extra_configs_dir' property...") +def test_extra_dir(config_loader): + logger.info("Testing 'extra_dir' property...") - config_loader.extra_configs_dir = "/tmp/pytest/extra_configs_dir" - assert config_loader.extra_configs_dir == "/tmp/pytest/extra_configs_dir" + config_loader.extra_dir = "/tmp/pytest/extra_dir" + assert config_loader.extra_dir == "/tmp/pytest/extra_dir" with pytest.raises(TypeError): - config_loader.extra_configs_dir = 3.14 - config_loader.extra_configs_dir = False - config_loader.extra_configs_dir = None + config_loader.extra_dir = 3.14 + config_loader.extra_dir = False + config_loader.extra_dir = None with pytest.raises(ValueError): - config_loader.extra_configs_dir = "" + config_loader.extra_dir = "" - logger.info("Done: 'extra_configs_dir' property.") + logger.info("Done: 'extra_dir' property.") -def test_env_file_path(config_loader): - logger.info("Testing 'env_file_path' property...") +def test_env_file_paths(config_loader): + logger.info("Testing 'env_file_paths' property...") - config_loader.env_file_path = "/tmp/pytest/.env" - assert config_loader.env_file_path == "/tmp/pytest/.env" + config_loader.env_file_paths = "/tmp/pytest/.env" + assert config_loader.env_file_paths == ["/tmp/pytest/.env"] with pytest.raises(TypeError): - config_loader.env_file_path = 3.14 - config_loader.env_file_path = False - config_loader.env_file_path = None + config_loader.env_file_paths = 3.14 + config_loader.env_file_paths = False + config_loader.env_file_paths = None with pytest.raises(ValueError): - config_loader.env_file_path = "" + config_loader.env_file_paths = "" - logger.info("Done: 'env_file_path' property.") + logger.info("Done: 'env_file_paths' property.") def test_required_envs(config_loader):