Skip to content

Commit

Permalink
Merge pull request #14 from axelfahy/feat/config
Browse files Browse the repository at this point in the history
feat(config): add class `FancyConfig` to handle configurations
  • Loading branch information
axelfahy committed Jul 2, 2019
2 parents 0b4aca7 + abce61d commit 2e76f3c
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 2 deletions.
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()

0 comments on commit 2e76f3c

Please sign in to comment.