Skip to content

Commit 593ceeb

Browse files
committed
Better handling of configuration files
1 parent 1b09155 commit 593ceeb

5 files changed

Lines changed: 123 additions & 35 deletions

File tree

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ master
1818
1.0.5 (unreleased)
1919
------------------
2020

21+
Backward incompatible changes
22+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23+
* The default path for the user configuration file is now ``~/.config/<name>/<name>.yml``.
24+
25+
Added
26+
^^^^^
27+
* Better handling of configuration files. An environment variable ``$<NAME>_CONFIG_PATH`` (e.g., ``$MYPYTHON_CONFIG_PATH``) can be defined to point to the user configuration file. If defined, this path overrides the default location.
28+
2129
Changed
2230
^^^^^^^
2331
* Modified logger to deal with warnings. Added critical level printing. Fixes :issue:`12,13`.

docs/sphinx/index.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ Your new product contains a `YAML <http://yaml.org/>`_ configuration file in the
196196
print(mypython.config['option1']['suboption2'])
197197
>>> 'some text'
198198

199-
If the user creates a custom configuration file in ``~/.mypython/mypython.yml``, the contents of that file will be used to update the default options. For instance, if you create a file with the contents
199+
If the user creates a custom configuration file in ``~/.config/mypython/mypython.yml``, the contents of that file will be used to update the default options. For instance, if you create a file with the contents
200200

201201
.. code-block:: yaml
202202
@@ -210,6 +210,8 @@ the code above would return ::
210210
print(mypython.config['option1']['suboption2'])
211211
>>> 'a different text'
212212

213+
Another possibility is to define an environment variable ``$MYPYTHON_CONFIG_PATH`` pointing to the user configuration file to use. If the environment variable is set, it overrides the default location for the user configuration file.
214+
213215
The package also includes a logging object built around Python's `logging <https://docs.python.org/3/library/logging.html>`__ module. Our custom logger allows to file and screen at the same time and provides more colourful tracebacks and warnings. From anywhere in your code you can do ::
214216

215217
from mypython import log

{{cookiecutter.repo_name}}/python/{{cookiecutter.package_name|lower}}/__init__.py

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,18 @@
22

33
from __future__ import absolute_import, division, print_function, unicode_literals
44

5-
from pkg_resources import parse_version
6-
import os
7-
8-
import yaml
9-
10-
# Inits the logging system. Only shell logging, and exception and warning catching.
11-
# File logging can be started by calling log.start_file_logger(path).
12-
from .utils import get_logger
13-
14-
15-
def merge(user, default):
16-
"""Merges a user configuration with the default one."""
17-
18-
if isinstance(user, dict) and isinstance(default, dict):
19-
for kk, vv in default.items():
20-
if kk not in user:
21-
user[kk] = vv
22-
else:
23-
user[kk] = merge(user[kk], vv)
24-
25-
return user
5+
from .utils import get_config, get_logger
266

277

288
NAME = '{{cookiecutter.package_name}}'
299

3010

3111
# Loads config
32-
yaml_kwds = dict()
33-
if parse_version(yaml.__version__) >= parse_version('5.1'):
34-
yaml_kwds.update(Loader=yaml.FullLoader)
35-
36-
config_path = os.path.join(os.path.dirname(__file__), 'etc/{0}.yml'.format(NAME))
37-
with open(config_path, 'r') as fp:
38-
config = yaml.load(fp, **yaml_kwds)
39-
40-
# If there is a custom configuration file, updates the defaults using it.
41-
custom_config_path = os.path.expanduser('~/.{0}/{0}.yml'.format(NAME))
42-
if os.path.exists(custom_config_path):
43-
with open(custom_config_path, "r") as fp:
44-
config = merge(yaml.load(fp, **yaml_kwds), config)
12+
config = get_config(NAME)
4513

4614

15+
# Inits the logging system. Only shell logging, and exception and warning catching.
16+
# File logging can be started by calling log.start_file_logger(path).
4717
log = get_logger(NAME)
4818

4919

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11

2+
from .configuration import *
23
from .logger import *
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# @Author: José Sánchez-Gallego (gallegoj@uw.edu)
5+
# @Date: 2019-05-08
6+
# @Filename: configuration.py
7+
# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause)
8+
#
9+
# @Last modified by: José Sánchez-Gallego (gallegoj@uw.edu)
10+
# @Last modified time: 2019-05-09 10:44:49
11+
12+
import os
13+
14+
import yaml
15+
from pkg_resources import parse_version
16+
17+
18+
__all__ = ['get_config']
19+
20+
21+
def merge_config(user, default):
22+
"""Merges a user configuration with the default one."""
23+
24+
if isinstance(user, dict) and isinstance(default, dict):
25+
for kk, vv in default.items():
26+
if kk not in user:
27+
user[kk] = vv
28+
else:
29+
user[kk] = merge_config(user[kk], vv)
30+
31+
return user
32+
33+
34+
def get_config(name, allow_user=True, user_path=None, config_envvar=None,
35+
merge_mode='update'):
36+
"""Returns a configuration dictionary.
37+
38+
The configuration dictionary is created by merging the default
39+
configuration file that is part of the library (in ``etc/<name>.yml``)
40+
with a user configuration file. The path to the user configuration file
41+
can be defined as an environment variable to be passed to this function
42+
in ``config_envvar`` or as a path in ``user_path``. The environment
43+
variable, if exists, always takes precedence.
44+
45+
Parameters
46+
----------
47+
name : str
48+
The name of the package.
49+
allow_user : bool
50+
If `True`, looks for an user configuration file and merges is to the
51+
default configuration. Otherwise it returns just the default
52+
configuration.
53+
user_path : str
54+
The path to the user configuration file. Defaults to
55+
``~/.config/<name>/<name>.yml``. Ignored if the file does not exist.
56+
config_envvar : str
57+
The environment variable that contains the path to the user
58+
configuration file. Defaults to ``<name>_CONFIG_PATH``. If the
59+
environment variable exists, the ``user_path`` is ignored.
60+
merge_mode : str
61+
Defines how the default and user dictionaries will be merged. If
62+
``update``, the user dictionary will be used to update the default
63+
configuration. If ``replace``, only the user configuration will be
64+
returned.
65+
66+
Returns
67+
-------
68+
config : dict
69+
A dictionary containing the configuration.
70+
71+
"""
72+
73+
assert merge_mode in ['update', 'replace'], 'invalid merge mode.'
74+
75+
yaml_kwds = dict()
76+
if parse_version(yaml.__version__) >= parse_version('5.1'):
77+
yaml_kwds.update(Loader=yaml.FullLoader)
78+
79+
# Loads config
80+
config_path = os.path.join(os.path.dirname(__file__), '../etc/{0}.yml'.format(name))
81+
with open(config_path, 'r') as fp:
82+
config = yaml.load(fp, **yaml_kwds)
83+
84+
if allow_user is False:
85+
return config
86+
87+
config_envvar = config_envvar or '{}_CONFIG_PATH'.format(name.upper())
88+
89+
if user_path is not None:
90+
user_path = os.path.expanduser(os.path.expandvars(user_path))
91+
else:
92+
user_path = os.path.expanduser('~/.config/{0}/{0}.yml'.format(name))
93+
94+
if config_envvar in os.environ:
95+
custom_config_fn = os.environ[config_envvar]
96+
elif os.path.exists(user_path):
97+
custom_config_fn = user_path
98+
else:
99+
return config
100+
101+
with open(custom_config_fn, 'r') as fp:
102+
user_config = yaml.load(fp, **yaml_kwds)
103+
104+
if merge_mode == 'update':
105+
return merge_config(user_config, config)
106+
else:
107+
return user_config

0 commit comments

Comments
 (0)