Skip to content
This repository has been archived by the owner on Dec 7, 2022. It is now read-only.

Commit

Permalink
simplifying the config loading so that it uses a default config file
Browse files Browse the repository at this point in the history
  • Loading branch information
mhrivnak committed Jul 23, 2014
1 parent a178229 commit 4f7ca24
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 36 deletions.
89 changes: 66 additions & 23 deletions crane/config.py
@@ -1,30 +1,31 @@
from ConfigParser import ConfigParser
from ConfigParser import ConfigParser, NoSectionError, NoOptionError
from contextlib import contextmanager
import logging
import os

import pkg_resources


_logger = logging.getLogger(__name__)


DEFAULT_CONFIG_PATH = '/etc/crane.conf'
# the default location for a user-defined config file
CONFIG_PATH = '/etc/crane.conf'
# the environment variable whose value can override the CONFIG_PATH
CONFIG_ENV_NAME = 'CRANE_CONFIG_PATH'
# the resource path for the config file containing default values
DEFAULT_CONFIG_RESOURCE = 'data/default_config.conf'

# general app settings
SECTION_GENERAL = 'general'

KEY_DEBUG = 'debug'
KEY_DATA_DIR = 'data_dir'
KEY_ENDPOINT = 'endpoint'

# google search appliance settings
SECTION_GSA = 'gsa'

KEY_URL = 'url'

config_defaults = {
KEY_DEBUG: 'false',
KEY_DATA_DIR: '/var/lib/crane/metadata/',
KEY_ENDPOINT: '',
KEY_URL: '',
}

def load(app):
"""
Expand All @@ -35,29 +36,71 @@ def load(app):
:raises IOError: iff a non-default config path is specified but does not exist
"""
config_path = os.environ.get(CONFIG_ENV_NAME) or DEFAULT_CONFIG_PATH
parser = ConfigParser(defaults=config_defaults)
# load default values from the included default config file
try:
with pkg_resources.resource_stream(__package__, DEFAULT_CONFIG_RESOURCE) as default_config:
parser = ConfigParser()
parser.readfp(default_config)
read_config(app, parser)
except IOError:
_logger.error('could not open default config file')
raise

# load user-specified config values
config_path = os.environ.get(CONFIG_ENV_NAME) or CONFIG_PATH
try:
with open(config_path) as config_file:
parser = ConfigParser()
parser.readfp(config_file)
read_config(app, parser)
_logger.info('config loaded from %s' % config_path)
except IOError:
if config_path != DEFAULT_CONFIG_PATH:
if config_path != CONFIG_PATH:
_logger.error('config file not found at path %s' % config_path)
raise
# if the user did not specify a config path and there is not a file
# at the default path, just use the default settings.
_logger.info('no config specified or found, so using defaults')

# adding the empty section will allow defaults to be used below
for section in [SECTION_GENERAL, SECTION_GSA]:
if not parser.has_section(section):
parser.add_section(section)

# [general] section
app.config['DEBUG'] = parser.getboolean(SECTION_GENERAL, KEY_DEBUG)
app.config[KEY_DATA_DIR] = parser.get(SECTION_GENERAL, KEY_DATA_DIR)
app.config[KEY_ENDPOINT] = parser.get(SECTION_GENERAL, KEY_ENDPOINT)
def read_config(app, parser):
"""
Read the configuration from the parser, and apply it to the app
:param app: a flask app
:type app: Flask
:param parser: a ConfigParser that has a file already loaded
:type parser: ConfigParser
"""
# "general" section settings
with supress(NoSectionError):
app.config['DEBUG'] = parser.getboolean(SECTION_GENERAL, KEY_DEBUG)

# parse other "general" section values
for key in (KEY_DATA_DIR, KEY_ENDPOINT):
with supress(NoOptionError):
app.config[key] = parser.get(SECTION_GENERAL, key)

# "gsa" (Google Search Appliance) section settings
with supress(NoSectionError):
section = app.config.setdefault(SECTION_GSA, {})

for key in (KEY_URL,):
with supress(NoOptionError):
section[key] = parser.get(SECTION_GSA, key)

# [gsa] section
app.config[SECTION_GSA + '_' + KEY_URL] = parser.get(SECTION_GSA, KEY_URL)

@contextmanager
def supress(*exceptions):
"""
backported from python 3.4, because it's simple and awesome
https://docs.python.org/3.4/library/contextlib.html#contextlib.suppress
:param exceptions: list of Exception or subclasses
:type exceptions: list
"""
try:
yield
except exceptions:
pass
7 changes: 7 additions & 0 deletions crane/data/default_config.conf
@@ -0,0 +1,7 @@
[general]
debug: false
data_dir: /var/lib/crane/metadata/
endpoint:

[gsa]
url:
2 changes: 1 addition & 1 deletion crane/search/__init__.py
Expand Up @@ -17,7 +17,7 @@ def load_config(app):
"""
global backend

url = app.config.get(config.SECTION_GSA + '_' + config.KEY_URL)
url = app.config.get(config.SECTION_GSA, {}).get(config.KEY_URL)
if url:
backend = GSA(url)
return
Expand Down
1 change: 1 addition & 0 deletions python-crane.spec
Expand Up @@ -21,6 +21,7 @@ BuildRequires: python-mock
BuildRequires: python-unittest2

Requires: python-flask >= 0.9
Requires: python-setuptools
Requires(post): policycoreutils-python
Requires(postun): policycoreutils-python

Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
@@ -1,4 +1,6 @@
Flask>=0.9
M2Crypto>=0.21.0
python-dateutil>=1.5
iniparse>=0.4
# for pkg_resources
setuptools
iniparse>=0.4
3 changes: 3 additions & 0 deletions setup.py
Expand Up @@ -23,4 +23,7 @@
install_requires=requirements,
tests_require=test_requirements,
test_suite='unittest2.collector',
package_data={
'crane': ['data/*.conf'],
},
)
2 changes: 1 addition & 1 deletion tests/search/test_init.py
Expand Up @@ -24,7 +24,7 @@ def test_gsa(self):
mock_app = mock.MagicMock()
fake_url = 'http://pulpproject.org/search'
mock_app.config = {
config.SECTION_GSA + '_' + config.KEY_URL: fake_url,
config.SECTION_GSA: {config.KEY_URL: fake_url},
}

search.load_config(mock_app)
Expand Down
30 changes: 23 additions & 7 deletions tests/test_config.py
Expand Up @@ -11,21 +11,23 @@ def setUp(self):
self.app = mock.MagicMock()
self.app.config = {}

@mock.patch('ConfigParser.ConfigParser.readfp', side_effect=IOError, spec_set=True)
@mock.patch('os.environ.get', return_value=None, spec_set=True)
def test_defaults(self, mock_get, mock_readfp):
@mock.patch('os.environ.get', return_value='/dev/null', spec_set=True)
def test_defaults(self, mock_get):
"""
test that when no config file is found, and no path was specified,
default values get used.
test that when no config options are specified, default values get used.
"""
config.load(self.app)

self.assertTrue(self.app.config.get('DEBUG') is False)
self.assertEqual(self.app.config.get(config.KEY_DATA_DIR), '/var/lib/crane/metadata/')
self.assertEqual(self.app.config.get(config.KEY_ENDPOINT), '')
configured_gsa_url = self.app.config.get(config.SECTION_GSA + '_' + config.KEY_URL)
configured_gsa_url = self.app.config.get(config.SECTION_GSA, {}).get(config.KEY_URL)
self.assertEqual(configured_gsa_url, '')

@mock.patch('pkg_resources.resource_stream', side_effect=IOError, spec_set=True)
def test_defaults_not_found(self, mock_resource_stream):
self.assertRaises(IOError, config.load, self.app)

@mock.patch('os.environ.get', return_value='/a/b/c/idontexist', spec_set=True)
def test_file_not_found(self, mock_get):
"""
Expand All @@ -40,5 +42,19 @@ def test_demo_config(self, mock_get):

self.assertTrue(self.app.config.get('DEBUG') is True)

configured_gsa_url = self.app.config.get(config.SECTION_GSA + '_' + config.KEY_URL)
configured_gsa_url = self.app.config.get(config.SECTION_GSA, {}).get(config.KEY_URL)
self.assertEqual(configured_gsa_url, 'http://pulpproject.org/search')


class TestSupress(unittest.TestCase):
def test_supression(self):
with config.supress(ValueError):
raise ValueError

def test_does_not_over_supress(self):
with config.supress(TypeError):
self.assertRaises(ValueError, int, 'notanint')

def test_supress_multiple(self):
with config.supress(TypeError, ValueError):
raise ValueError
3 changes: 0 additions & 3 deletions tests/test_data.py
Expand Up @@ -52,7 +52,6 @@ def tearDown(self):
def test_with_metadata_good(self, mock_glob):
mock_glob.return_value = [demo_data.foo_metadata_path]
mock_app = mock.MagicMock()
mock_app.config = config.config_defaults.copy()

data.load_all(mock_app)

Expand All @@ -72,7 +71,6 @@ def test_with_metadata_good(self, mock_glob):
def test_with_metadata_bad(self, mock_error, mock_glob):
mock_glob.return_value = [demo_data.wrong_version_path]
mock_app = mock.MagicMock()
mock_app.config = config.config_defaults.copy()

data.load_all(mock_app)

Expand All @@ -87,7 +85,6 @@ def test_with_metadata_bad(self, mock_error, mock_glob):
def test_with_wrong_path(self, mock_error, mock_glob):
mock_glob.return_value = ['/a/b/c/d.json']
mock_app = mock.MagicMock()
mock_app.config = config.config_defaults.copy()

data.load_all(mock_app)

Expand Down

0 comments on commit 4f7ca24

Please sign in to comment.