Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): add class FancyConfig to handle configurations #14

Merged
merged 1 commit into from Jul 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .pylintrc
Expand Up @@ -138,7 +138,8 @@ disable=print-statement,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape
comprehension-escape,
logging-fstring-interpolation

# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
Expand Down
12 changes: 11 additions & 1 deletion bff/__init__.py
@@ -1,10 +1,16 @@
"""All of bff' functions."""
import logging

from ._version import get_versions

from .fancy import (
concat_with_categories, get_peaks, idict, mem_usage_pd, parse_date,
plot_history, plot_predictions, plot_series, plot_true_vs_pred,
read_sql_by_chunks, sliding_window, value_2_list
)

from .config import FancyConfig

# Public object of the module.
__all__ = [
'concat_with_categories',
Expand All @@ -19,8 +25,12 @@
'read_sql_by_chunks',
'sliding_window',
'value_2_list',
'FancyConfig',
]

from ._version import get_versions
# Logging configuration.
FORMAT = '%(asctime)-15s %(message)s'
logging.basicConfig(format=FORMAT)

__version__ = get_versions()['version']
del get_versions
91 changes: 91 additions & 0 deletions bff/config.py
@@ -0,0 +1,91 @@
"""
FancyConfig, configuration loader.

Tool to load the configuration from a configuration file (`config.yml`).
"""
from collections.abc import Mapping
import logging
from pathlib import Path
import pprint
import yaml

LOGGER = logging.getLogger(__name__)


class FancyConfig(Mapping):
"""
Class to load the configuration file.

This class behaves like a dictionary that loads a
configuration file in yaml format.

If the configuration file does not exist, creates it from template.

Examples
--------
>>> config = FancyConfig()
>>> print(config)
{ 'database': { 'host': '127.0.0.1',
'name': 'porgs',
'port': 3306,
'pwd': 'bacca',
'user': 'Chew'},
'env': 'prod',
'imports': {'star_wars': ['ewok', 'bantha']}}
"""

def __init__(self, path_config_to_load: Path = Path.home().joinpath('.config/fancyconfig.yml'),
default_config_path: Path = (Path(__file__).resolve()
.parent.joinpath('config.yml'))):
"""
Initialization of configuration.

If the folder to store the configuration does not exist, create it.
If configuration file does not exist, copy it from default one.

Parameters
----------
path_config_to_load : Path, default '~/.config/'
Directory to store the configuration file and load the configuration from.
default_config_path: Path, default 'config.yml' current directory.
Name of the configuration file.
"""
# Create config file if does not exist.
if not path_config_to_load.exists():
LOGGER.info((f'Configuration file does not exist, '
f'creating it from {default_config_path}'))
# Creating folder of configuration (parent of file).
path_config_to_load.parent.mkdir(parents=True, exist_ok=True)
# Copy the configuration file.
path_config_to_load.write_bytes(default_config_path.read_bytes())

with path_config_to_load.open(mode='r', encoding='utf-8') as yaml_config_file:
self._config = yaml.load(yaml_config_file, Loader=yaml.FullLoader)

def __getitem__(self, item):
"""Getter of the class."""
try:
return self._config[item]
except KeyError:
LOGGER.error(f'Configuration for {item} does not exist.')

def __iter__(self):
"""Iterator of the class."""
return iter(self._config)

def __len__(self):
"""Lenght of the config."""
return len(self._config)

def __str__(self):
"""
__str__ method.

Pretty representation of the config.
"""
pretty = pprint.PrettyPrinter(indent=2)
return pretty.pformat(self._config)

def __repr__(self):
"""Representation of the config."""
return f'{super().__repr__}\n{str(self._config)}'
16 changes: 16 additions & 0 deletions bff/config.yml
@@ -0,0 +1,16 @@
# Configuration file example.

# Database information
database:
user: Chew
pwd: bacca
host: 127.0.0.1
port: 3306
name: porgs

imports:
star_wars:
- ewok
- bantha

env: prod
5 changes: 5 additions & 0 deletions doc/source/config.rst
@@ -0,0 +1,5 @@
FancyConfig
===========

.. automodule:: bff.FancyConfig
:members: __init__
2 changes: 2 additions & 0 deletions doc/source/index.rst
Expand Up @@ -29,3 +29,5 @@ Contents

fancy

config

1 change: 1 addition & 0 deletions setup.cfg
Expand Up @@ -18,6 +18,7 @@ exclude =
versioneer.py,
bff/_version.py,
setup.py
doc

[tool:pytest]
testpaths = bff
Expand Down
79 changes: 79 additions & 0 deletions tests/test_config.py
@@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
"""Test of config module

This module test the configuration loader
"""
import filecmp
from pathlib import Path
import unittest

from bff.config import FancyConfig


class TestiFancyConfig(unittest.TestCase):
"""
Unittest of config module.
"""

def test_create_config(self):
"""
Test of loading the config when it does not exists.
"""
# Default configuration
config_default = FancyConfig()

default_path_ok = Path(__file__).resolve().parent.parent.joinpath('bff/config.yml')
default_path_ko = Path.home().resolve().joinpath('config.yml')

dest = Path(__file__).resolve().parent.joinpath('config.yml')
config_a = FancyConfig(dest, default_path_ok)

# Check if the file is correctly copied.
self.assertTrue(filecmp.cmp(dest, default_path_ok))

# Check if the file is not overridden.
path_conf_b = Path(__file__).resolve().parent.joinpath('conf_b.yml')
with path_conf_b.open(mode='w', encoding='utf-8') as f:
f.write('testfile: True')
config_b = FancyConfig(dest, path_conf_b)

self.assertFalse(filecmp.cmp(dest, path_conf_b))
# Check that the configurations are the same.
self.assertEqual(config_a, config_b)

# Removes the configs.
dest.unlink()
path_conf_b.unlink()

# Create a config that is not called config.
config_c = FancyConfig(path_conf_b, default_path_ok)
self.assertEqual(config_a, config_c)

# Remove the files.
path_conf_b.unlink()

# If default config path is wrong, should fail.
with self.assertRaises(FileNotFoundError):
FancyConfig(dest, default_path_ko)

def test_access_config(self):
"""
Test the access of the configuration once loaded.

Should be accessible as a property.
"""
default_path_ok = Path(__file__).resolve().parent.parent.joinpath('bff/config.yml')
dest = Path(__file__).resolve().parent.joinpath('config.yml')
config = FancyConfig(dest, default_path_ok)

self.assertEqual(config['env'], 'prod')
self.assertEqual(config['database']['user'], 'Chew')
self.assertEqual(config['imports']['star_wars'], ['ewok', 'bantha'])

# Check the error message using a mock.
with unittest.mock.patch('logging.Logger.error') as mock_logging:
config['error']
mock_logging.assert_called_with('Configuration for error does not exist.')

# Remove the file.
dest.unlink()