summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyan Petrello <lists@ryanpetrello.com>2013-03-01 06:53:08 -0800
committerRyan Petrello <lists@ryanpetrello.com>2013-03-01 06:53:08 -0800
commitd78a4b22a4145ae46d6eb55325870ab863b0c86c (patch)
tree8b78b182e7655824227ca0599bf12e082dc96ed7
parentc17022a251ff90070f6645fcb1aee36ffbd8f892 (diff)
parent424eb031540583c2a340315d4a33ab686ca6eac1 (diff)
downloadpecan-d78a4b22a4145ae46d6eb55325870ab863b0c86c.tar.gz
Merge pull request #174 from alfredodeza/env_var
Environment Variable implementation
-rw-r--r--docs/source/commands.rst40
-rw-r--r--pecan/commands/base.py6
-rw-r--r--pecan/commands/serve.py7
-rw-r--r--pecan/configuration.py24
-rw-r--r--pecan/core.py8
-rw-r--r--pecan/testing.py16
-rw-r--r--pecan/tests/compat/test_dictconfig.py14
-rw-r--r--pecan/tests/test_conf.py65
8 files changed, 141 insertions, 39 deletions
diff --git a/docs/source/commands.rst b/docs/source/commands.rst
index 367c8b5..9105f41 100644
--- a/docs/source/commands.rst
+++ b/docs/source/commands.rst
@@ -8,7 +8,7 @@ Command Line Pecan
Any Pecan application can be controlled and inspected from the command line
using the built-in ``pecan`` command. The usage examples of the ``pecan``
command in this document are intended to be invoked from your project's root
-directory.
+directory.
Serving a Pecan App For Development
-----------------------------------
@@ -40,7 +40,7 @@ command::
Pecan Interactive Shell
Python 2.7.1 (r271:86832, Jul 31 2011, 19:30:53)
[GCC 4.2.1 (Based on Apple Inc. build 5658)
-
+
The following objects are available:
wsgiapp - This project's WSGI App instance
conf - The current configuration
@@ -51,7 +51,7 @@ command::
'app': Config({
'root': 'myapp.controllers.root.RootController',
'modules': ['myapp'],
- 'static_root': '/Users/somebody/myapp/public',
+ 'static_root': '/Users/somebody/myapp/public',
'template_path': '/Users/somebody/myapp/project/templates',
'errors': {'404': '/error/404'},
'debug': True
@@ -79,6 +79,34 @@ which can be specified with the ``--shell`` flag (or its abbreviated alias,
$ pecan shell --shell=ipython config.py
$ pecan shell -s bpython config.py
+
+.. _env_config:
+
+Configuration from an environment variable
+------------------------------------------
+In all the examples shown, you will see that the `pecan` commands were
+accepting a file path to the configuration file. An alternative to this is to
+specify the configuration file in an environment variable (``PECAN_CONFIG``).
+
+This is completely optional; if a file path is passed in explicitly, Pecan will
+honor that before looking for an environment variable.
+
+For example, to ``serve`` a Pecan application, a variable could be exported and
+subsequently be re-used when no path is passed in::
+
+ $ export PECAN_CONFIG=/path/to/app/config.py
+ $ pecan serve
+ Starting server in PID 000.
+ serving on 0.0.0.0:8080, view at http://127.0.0.1:8080
+
+Note that the path needs to reference a valid pecan configuration file,
+otherwise the command will error out with a meaningful message indicating that
+the path is invalid (for example, if a directory is passed in).
+
+If ``PECAN_CONFIG`` is not set and no configuration is passed in, the command
+will error out because it will not be able to locate a configuration file.
+
+
Extending ``pecan`` with Custom Commands
----------------------------------------
While the commands packaged with Pecan are useful, the real utility of its
@@ -125,7 +153,7 @@ Overriding the ``run`` Method
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
First, we're subclassing ``pecan.commands.BaseCommand`` and extending
-the ``run`` method to:
+the ``run`` method to:
* Load a Pecan application - ``self.load_app()``
* Wrap it in a fake WGSI environment - ``webtest.TestApp()``
@@ -184,11 +212,11 @@ e.g., ::
...
)
-Assuming it doesn't exist already, we'll add the ``entry_points`` argument
+Assuming it doesn't exist already, we'll add the ``entry_points`` argument
to the ``setup()`` call, and define a ``[pecan.command]`` definition for your custom
command::
-
+
# myapp/setup.py
...
setup(
diff --git a/pecan/commands/base.py b/pecan/commands/base.py
index acfa4d7..68b5c0e 100644
--- a/pecan/commands/base.py
+++ b/pecan/commands/base.py
@@ -136,7 +136,9 @@ class BaseCommand(object):
arguments = ({
'name': 'config_file',
- 'help': 'a Pecan configuration file'
+ 'help': 'a Pecan configuration file',
+ 'nargs': '?',
+ 'default': None,
},)
def run(self, args):
@@ -144,6 +146,4 @@ class BaseCommand(object):
def load_app(self):
from pecan import load_app
- if not os.path.isfile(self.args.config_file):
- raise RuntimeError('`%s` is not a file.' % self.args.config_file)
return load_app(self.args.config_file)
diff --git a/pecan/commands/serve.py b/pecan/commands/serve.py
index 119d8d7..9dd431e 100644
--- a/pecan/commands/serve.py
+++ b/pecan/commands/serve.py
@@ -17,15 +17,12 @@ class ServeCommand(BaseCommand):
configuration file for the server and application.
"""
- arguments = ({
- 'name': 'config_file',
- 'help': 'a Pecan configuration file'
- }, {
+ arguments = BaseCommand.arguments + ({
'name': '--reload',
'help': 'Watch for changes and automatically reload.',
'default': False,
'action': 'store_true'
- })
+ },)
def run(self, args):
super(ServeCommand, self).run(args)
diff --git a/pecan/configuration.py b/pecan/configuration.py
index 94dd579..9925ba9 100644
--- a/pecan/configuration.py
+++ b/pecan/configuration.py
@@ -147,6 +147,8 @@ def conf_from_file(filepath):
abspath = os.path.abspath(os.path.expanduser(filepath))
conf_dict = {}
+ if not os.path.isfile(abspath):
+ raise RuntimeError('`%s` is not a file.' % abspath)
execfile(abspath, globals(), conf_dict)
conf_dict['__file__'] = abspath
@@ -154,6 +156,25 @@ def conf_from_file(filepath):
return conf_from_dict(conf_dict)
+def conf_from_env():
+ '''
+ If the ``PECAN_CONFIG`` environment variable exists and it points to
+ a valid path it will return that, otherwise it will raise
+ a ``RuntimeError``.
+ '''
+ config_path = os.environ.get('PECAN_CONFIG')
+ error = None
+ if not config_path:
+ error = "PECAN_CONFIG is not set and " \
+ "no config file was passed as an argument."
+ elif not os.path.isfile(config_path):
+ error = "PECAN_CONFIG was set to an invalid path: %s" % config_path
+
+ if error:
+ raise RuntimeError(error)
+ return config_path
+
+
def conf_from_dict(conf_dict):
'''
Creates a configuration dictionary from a dictionary.
@@ -191,6 +212,9 @@ def set_config(config, overwrite=False):
if overwrite is True:
_runtime_conf.empty()
+ if config is None:
+ config = conf_from_env()
+
if isinstance(config, basestring):
config = conf_from_file(config)
_runtime_conf.update(config)
diff --git a/pecan/core.py b/pecan/core.py
index 1005965..8817ac6 100644
--- a/pecan/core.py
+++ b/pecan/core.py
@@ -138,13 +138,15 @@ def render(template, namespace):
return state.app.render(template, namespace)
-def load_app(config):
+def load_app(config=None):
'''
Used to load a ``Pecan`` application and its environment based on passed
configuration.
- :param config: Can be a dictionary containing configuration, or a string
- which represents a (relative) configuration filename.
+ :param config: Can be a dictionary containing configuration, a string which
+ represents a (relative) configuration filename or ``None``
+ which will fallback to get the ``PECAN_CONFIG`` env
+ variable.
returns a pecan.Pecan object
'''
diff --git a/pecan/testing.py b/pecan/testing.py
index b690ff2..ee2896e 100644
--- a/pecan/testing.py
+++ b/pecan/testing.py
@@ -2,13 +2,15 @@ from pecan import load_app
from webtest import TestApp
-def load_test_app(config):
+def load_test_app(config=None):
"""
Used for functional tests where you need to test your
literal application and its integration with the framework.
- :param config: Can be a dictionary containing configuration, or a string
- which represents a (relative) configuration filename.
+ :param config: Can be a dictionary containing configuration, a string which
+ represents a (relative) configuration filename or ``None``
+ which will fallback to get the ``PECAN_CONFIG`` env
+ variable.
returns a pecan.Pecan WSGI application wrapped in a webtest.TestApp
instance.
@@ -21,5 +23,13 @@ def load_test_app(config):
resp = app.post('/path/to/some/resource', params={'param': 'value'})
assert resp.status_int == 302
+
+ Alternatively you could call ``load_test_app`` with no parameters if the
+ environment variable is set ::
+
+ app = load_test_app()
+
+ resp = app.get('/path/to/some/resource').status_int
+ assert resp.status_int == 200
"""
return TestApp(load_app(config))
diff --git a/pecan/tests/compat/test_dictconfig.py b/pecan/tests/compat/test_dictconfig.py
index aba824c..7c31474 100644
--- a/pecan/tests/compat/test_dictconfig.py
+++ b/pecan/tests/compat/test_dictconfig.py
@@ -94,7 +94,7 @@ class BaseTest(unittest.TestCase):
match = pat.search(actual)
if not match:
self.fail("Log line does not match expected pattern:\n" +
- actual)
+ actual)
self.assertEquals(tuple(match.groups()), expected)
s = stream.read()
if s:
@@ -303,7 +303,7 @@ class ConfigDictTest(BaseTest):
},
'root': {
'level': 'NOTSET',
- 'handlers': ['hand1'],
+ 'handlers': ['hand1'],
},
}
@@ -337,7 +337,7 @@ class ConfigDictTest(BaseTest):
},
'root': {
'level': 'NOTSET',
- 'handlers': ['hand1'],
+ 'handlers': ['hand1'],
},
}
@@ -602,8 +602,8 @@ class ConfigDictTest(BaseTest):
except RuntimeError:
logging.exception("just testing")
sys.stdout.seek(0)
- self.assertEquals(output.getvalue(),
- "ERROR:root:just testing\nGot a [RuntimeError]\n")
+ expected = "ERROR:root:just testing\nGot a [RuntimeError]\n"
+ self.assertEquals(output.getvalue(), expected)
# Original logger output is empty
self.assert_log_lines([])
@@ -617,8 +617,8 @@ class ConfigDictTest(BaseTest):
except RuntimeError:
logging.exception("just testing")
sys.stdout.seek(0)
- self.assertEquals(output.getvalue(),
- "ERROR:root:just testing\nGot a [RuntimeError]\n")
+ expected = "ERROR:root:just testing\nGot a [RuntimeError]\n"
+ self.assertEquals(output.getvalue(), expected)
# Original logger output is empty
self.assert_log_lines([])
diff --git a/pecan/tests/test_conf.py b/pecan/tests/test_conf.py
index 4b276cb..772b466 100644
--- a/pecan/tests/test_conf.py
+++ b/pecan/tests/test_conf.py
@@ -110,22 +110,22 @@ class TestConf(TestCase):
from pecan import configuration
path = ('doesnotexist.py',)
configuration.Config({})
- self.assertRaises(IOError, configuration.conf_from_file, os.path.join(
- __here__,
- 'config_fixtures',
- *path
- ))
+ self.assertRaises(
+ RuntimeError,
+ configuration.conf_from_file,
+ os.path.join(__here__, 'config_fixtures', *path)
+ )
def test_config_missing_file_on_path(self):
from pecan import configuration
path = ('bad', 'bad', 'doesnotexist.py',)
configuration.Config({})
- self.assertRaises(IOError, configuration.conf_from_file, os.path.join(
- __here__,
- 'config_fixtures',
- *path
- ))
+ self.assertRaises(
+ RuntimeError,
+ configuration.conf_from_file,
+ os.path.join(__here__, 'config_fixtures', *path)
+ )
def test_config_with_syntax_error(self):
from pecan import configuration
@@ -274,6 +274,47 @@ class TestGlobalConfig(TestCase):
)
assert dict(configuration._runtime_conf) == {'foo': 'bar'}
- def test_set_config_invalid_type(self):
+ def test_set_config_none_type(self):
+ from pecan import configuration
+ self.assertRaises(RuntimeError, configuration.set_config, None)
+
+ def test_set_config_to_dir(self):
+ from pecan import configuration
+ self.assertRaises(RuntimeError, configuration.set_config, '/')
+
+
+class TestConfFromEnv(TestCase):
+
+ def setUp(self):
+ self.conf_from_env = self.get_conf_from_env()
+ os.environ['PECAN_CONFIG'] = ''
+
+ def tearDown(self):
+ os.environ['PECAN_CONFIG'] = ''
+
+ def get_conf_from_env(self):
from pecan import configuration
- self.assertRaises(TypeError, configuration.set_config, None)
+ return configuration.conf_from_env
+
+ def assertRaisesMessage(self, msg, exc, func, *args, **kwargs):
+ try:
+ func(*args, **kwargs)
+ self.assertFail()
+ except Exception as error:
+ assert issubclass(exc, error.__class__)
+ assert error.message == msg
+
+ def test_invalid_path(self):
+ os.environ['PECAN_CONFIG'] = '/'
+ msg = "PECAN_CONFIG was set to an invalid path: /"
+ self.assertRaisesMessage(msg, RuntimeError, self.conf_from_env)
+
+ def test_is_not_set(self):
+ msg = "PECAN_CONFIG is not set and " \
+ "no config file was passed as an argument."
+ self.assertRaisesMessage(msg, RuntimeError, self.conf_from_env)
+
+ def test_return_valid_path(self):
+ here = os.path.abspath(__file__)
+ os.environ['PECAN_CONFIG'] = here
+ assert self.conf_from_env() == here