diff options
author | Ryan Petrello <lists@ryanpetrello.com> | 2013-03-01 06:53:08 -0800 |
---|---|---|
committer | Ryan Petrello <lists@ryanpetrello.com> | 2013-03-01 06:53:08 -0800 |
commit | d78a4b22a4145ae46d6eb55325870ab863b0c86c (patch) | |
tree | 8b78b182e7655824227ca0599bf12e082dc96ed7 | |
parent | c17022a251ff90070f6645fcb1aee36ffbd8f892 (diff) | |
parent | 424eb031540583c2a340315d4a33ab686ca6eac1 (diff) | |
download | pecan-d78a4b22a4145ae46d6eb55325870ab863b0c86c.tar.gz |
Merge pull request #174 from alfredodeza/env_var
Environment Variable implementation
-rw-r--r-- | docs/source/commands.rst | 40 | ||||
-rw-r--r-- | pecan/commands/base.py | 6 | ||||
-rw-r--r-- | pecan/commands/serve.py | 7 | ||||
-rw-r--r-- | pecan/configuration.py | 24 | ||||
-rw-r--r-- | pecan/core.py | 8 | ||||
-rw-r--r-- | pecan/testing.py | 16 | ||||
-rw-r--r-- | pecan/tests/compat/test_dictconfig.py | 14 | ||||
-rw-r--r-- | pecan/tests/test_conf.py | 65 |
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 |