diff options
author | Jonathan LaCour <jonathan@dreamhost.com> | 2013-08-09 12:35:01 -0700 |
---|---|---|
committer | Jonathan LaCour <jonathan@dreamhost.com> | 2013-08-09 12:35:01 -0700 |
commit | 2ccf45ba3c96ac69a33170d29894024f12f7cfce (patch) | |
tree | ce3acb11f8e98391d93987c259f0a27ccc76e4b0 | |
parent | 8e1ea099d5e64a4df8a11a3e73289368e098beeb (diff) | |
parent | ee7c3544bf2288f1e9cbb81d9609ca742a262ce0 (diff) | |
download | pecan-2ccf45ba3c96ac69a33170d29894024f12f7cfce.tar.gz |
Merge branch 'next' of github.com:dreamhost/pecan
-rw-r--r-- | docs/source/configuration.rst | 24 | ||||
-rw-r--r-- | docs/source/hooks.rst | 41 | ||||
-rw-r--r-- | pecan/__init__.py | 51 | ||||
-rw-r--r-- | pecan/core.py | 21 | ||||
-rw-r--r-- | pecan/routing.py | 7 | ||||
-rw-r--r-- | pecan/scaffolds/base/+package+/app.py_tmpl | 12 | ||||
-rw-r--r-- | pecan/scaffolds/base/config.py_tmpl | 4 | ||||
-rw-r--r-- | pecan/tests/test_base.py | 29 | ||||
-rw-r--r-- | pecan/tests/test_hooks.py | 29 |
9 files changed, 133 insertions, 85 deletions
diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index f578ab4..ea4d29d 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -177,3 +177,27 @@ if you need to prefix the keys in the returned dictionary. Config({'app': Config({'errors': {}, 'template_path': '', 'static_root': 'public', [...] >>> conf.as_dict('prefixed_') {'prefixed_app': {'prefixed_errors': {}, 'prefixed_template_path': '', 'prefixed_static_root': 'prefixed_public', [...] + + +Dotted Keys and Native Dictionaries +----------------------------------- + +Sometimes you want to specify a configuration option that includes dotted keys. +This is especially common when configuring Python logging. By passing +a special key, ``__force_dict__``, individual configuration blocks can be +treated as native dictionaries. + +:: + + logging = { + 'loggers': { + 'root': {'level': 'INFO', 'handlers': ['console']}, + 'sqlalchemy.engine': {'level': 'INFO', 'handlers': ['console']}, + '__force_dict__': True + } + } + + from myapp import conf + assert isinstance(conf.logging.loggers, dict) + assert isinstance(conf.logging.loggers['root'], dict) + assert isinstance(conf.logging.loggers['sqlalchemy.engine'], dict) diff --git a/docs/source/hooks.rst b/docs/source/hooks.rst index e1c54c4..b7c01ce 100644 --- a/docs/source/hooks.rst +++ b/docs/source/hooks.rst @@ -54,17 +54,15 @@ Attaching Hooks --------------- Hooks can be attached in a project-wide manner by specifying a list of hooks -in your project's ``app.py`` file. +in your project's configuration file. :: - from application.root import RootController - from my_hooks import SimpleHook - - app = make_app( - RootController(), - hooks = [SimpleHook()] - ) + app = { + 'root' : '...' + # ... + 'hooks': lambda: [SimpleHook()] + } Hooks can also be applied selectively to controllers and their sub-controllers using the :attr:`__hooks__` attribute on one or more controllers. @@ -125,20 +123,6 @@ By default, both outputs are enabled. * :ref:`pecan_hooks` -Enabling RequestViewerHook -.......................... - -This hook can be automatically added to the application itself if a certain -key, ``requestviewer``, exists in the configuration used for the app, e.g.:: - - app = {} - server = {} - requestviewer = {} - -It does not need to contain anything (could be an empty dictionary), and this -is enough to force Pecan to load this hook when the WSGI application is -created. - Configuring RequestViewerHook ............................. @@ -179,25 +163,16 @@ response:: X-Pecan-params [] X-Pecan-hooks ['RequestViewerHook'] -The hook can be configured via a dictionary (or Config object from Pecan) when -adding it to the application or via the ``requestviewer`` key in the actual -configuration being passed to the application. - The configuration dictionary is flexible (none of the keys are required) and can hold two keys: ``items`` and ``blacklist``. -This is how the hook would look if configured directly when using ``make_app`` -(shortened for brevity):: +This is how the hook would look if configured directly (shortened for brevity):: ... - hooks = [ + 'hooks': lambda: [ RequestViewerHook({'items':['path']}) ] -And the same configuration could be set in the config file like:: - - requestviewer = {'items:['path']} - Modifying Output Format ....................... diff --git a/pecan/__init__.py b/pecan/__init__.py index cf175b8..c294572 100644 --- a/pecan/__init__.py +++ b/pecan/__init__.py @@ -17,6 +17,8 @@ try: except ImportError: from logutils.dictconfig import dictConfig as load_logging_config # noqa +import warnings + __all__ = [ 'make_app', 'load_app', 'Pecan', 'request', 'response', @@ -25,8 +27,7 @@ __all__ = [ ] -def make_app(root, static_root=None, logging={}, debug=False, - wrap_app=None, **kw): +def make_app(root, **kw): ''' Utility for creating the Pecan application object. This function should generally be called from the ``setup_app`` function in your project's @@ -37,8 +38,6 @@ def make_app(root, static_root=None, logging={}, debug=False, :param static_root: The relative path to a directory containing static files. Serving static files is only enabled when debug mode is set. - :param logging: A dictionary used to configure logging. This uses - ``logging.config.dictConfig``. :param debug: A flag to enable debug mode. This enables the debug middleware and serving static files. :param wrap_app: A function or middleware class to wrap the Pecan app. @@ -49,19 +48,31 @@ def make_app(root, static_root=None, logging={}, debug=False, This should be used if you want to use middleware to perform authentication or intercept all requests before they are routed to the root controller. + :param logging: A dictionary used to configure logging. This uses + ``logging.config.dictConfig``. All other keyword arguments are passed in to the Pecan app constructor. :returns: a ``Pecan`` object. ''' - # A shortcut for the RequestViewerHook middleware. - if hasattr(conf, 'requestviewer'): - existing_hooks = kw.get('hooks', []) - existing_hooks.append(RequestViewerHook(conf.requestviewer)) - kw['hooks'] = existing_hooks - # Pass logging configuration (if it exists) on to the Python logging module + logging = kw.get('logging', {}) + debug = kw.get('debug', False) if logging: + if debug: + try: + # + # By default, Python 2.7+ silences DeprecationWarnings. + # However, if conf.app.debug is True, we should probably ensure + # that users see these types of warnings. + # + from logging import captureWarnings + captureWarnings(True) + warnings.simplefilter("default", DeprecationWarning) + except ImportError: + # No captureWarnings on Python 2.6, DeprecationWarnings are on + pass + if isinstance(logging, Config): logging = logging.to_dict() if 'version' not in logging: @@ -72,28 +83,40 @@ def make_app(root, static_root=None, logging={}, debug=False, app = Pecan(root, **kw) # Optionally wrap the app in another WSGI app + wrap_app = kw.get('wrap_app', None) if wrap_app: app = wrap_app(app) # Configuration for serving custom error messages - if hasattr(conf.app, 'errors'): - app = ErrorDocumentMiddleware(app, conf.app.errors) + errors = kw.get('errors', getattr(conf.app, 'errors', {})) + if errors: + app = ErrorDocumentMiddleware(app, errors) # Included for internal redirect support app = RecursiveMiddleware(app) # When in debug mode, load our exception dumping middleware + static_root = kw.get('static_root', None) if debug: app = DebugMiddleware(app) # Support for serving static files (for development convenience) if static_root: app = StaticFileMiddleware(app, static_root) + elif static_root: - from warnings import warn - warn( + warnings.warn( "`static_root` is only used when `debug` is True, ignoring", RuntimeWarning ) + if hasattr(conf, 'requestviewer'): + warnings.warn(''.join([ + "`pecan.conf.requestviewer` is deprecated. To apply the ", + "`RequestViewerHook` to your application, add it to ", + "`pecan.conf.app.hooks` or manually in your project's `app.py` ", + "file."]), + DeprecationWarning + ) + return app diff --git a/pecan/core.py b/pecan/core.py index 29930b6..0aad76a 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -173,11 +173,12 @@ class Pecan(object): :param root: A string representing a root controller object (e.g., "myapp.controller.root.RootController") - :param default_renderer: The default rendering engine to use. Defaults - to mako. - :param template_path: The default relative path to use for templates. - Defaults to 'templates'. - :param hooks: A list of Pecan hook objects to use for this application. + :param default_renderer: The default template rendering engine to use. + Defaults to mako. + :param template_path: A relative file system path (from the project root) + where template files live. Defaults to 'templates'. + :param hooks: A callable which returns a list of + :class:`pecan.hooks.PecanHook`s :param custom_renderers: Custom renderer objects, as a dictionary keyed by engine name. :param extra_template_vars: Any variables to inject into the template @@ -195,9 +196,9 @@ class Pecan(object): ) def __init__(self, root, default_renderer='mako', - template_path='templates', hooks=[], custom_renderers={}, - extra_template_vars={}, force_canonical=True, - guess_content_type_from_ext=True): + template_path='templates', hooks=lambda: [], + custom_renderers={}, extra_template_vars={}, + force_canonical=True, guess_content_type_from_ext=True, **kw): if isinstance(root, six.string_types): root = self.__translate_root__(root) @@ -205,7 +206,11 @@ class Pecan(object): self.root = root self.renderers = RendererFactory(custom_renderers, extra_template_vars) self.default_renderer = default_renderer + # pre-sort these so we don't have to do it per-request + if six.callable(hooks): + hooks = hooks() + self.hooks = list(sorted( hooks, key=operator.attrgetter('priority') diff --git a/pecan/routing.py b/pecan/routing.py index 31cbf92..fc1a7d4 100644 --- a/pecan/routing.py +++ b/pecan/routing.py @@ -46,6 +46,13 @@ def lookup_controller(obj, url_path): # traversal result = handle_lookup_traversal(obj, remainder) if result: + # If no arguments are passed to the _lookup, yet the + # argspec requires at least one, raise a 404 + if ( + remainder == [''] + and len(obj._pecan['argspec'].args) > 1 + ): + raise return lookup_controller(*result) else: raise exc.HTTPNotFound diff --git a/pecan/scaffolds/base/+package+/app.py_tmpl b/pecan/scaffolds/base/+package+/app.py_tmpl index 191fc11..bf904b6 100644 --- a/pecan/scaffolds/base/+package+/app.py_tmpl +++ b/pecan/scaffolds/base/+package+/app.py_tmpl @@ -5,16 +5,10 @@ from ${package} import model def setup_app(config): model.init_model() + app_conf = dict(config.app) return make_app( - config.app.root, - static_root=config.app.static_root, - template_path=config.app.template_path, + app_conf.pop('root'), logging=getattr(config, 'logging', {}), - debug=getattr(config.app, 'debug', False), - force_canonical=getattr(config.app, 'force_canonical', True), - guess_content_type_from_ext=getattr( - config.app, - 'guess_content_type_from_ext', - True), + **app_conf ) diff --git a/pecan/scaffolds/base/config.py_tmpl b/pecan/scaffolds/base/config.py_tmpl index e37ffb0..16b70f3 100644 --- a/pecan/scaffolds/base/config.py_tmpl +++ b/pecan/scaffolds/base/config.py_tmpl @@ -20,7 +20,9 @@ app = { logging = { 'loggers': { 'root': {'level': 'INFO', 'handlers': ['console']}, - '${package}': {'level': 'DEBUG', 'handlers': ['console']} + '${package}': {'level': 'DEBUG', 'handlers': ['console']}, + 'py.warnings': {'handlers': ['console']}, + '__force_dict__': True }, 'handlers': { 'console': { diff --git a/pecan/tests/test_base.py b/pecan/tests/test_base.py index 37751b7..e5ec6af 100644 --- a/pecan/tests/test_base.py +++ b/pecan/tests/test_base.py @@ -182,6 +182,35 @@ class TestLookups(PecanTestCase): assert r.status_int == 404 +class TestCanonicalLookups(PecanTestCase): + + @property + def app_(self): + class LookupController(object): + def __init__(self, someID): + self.someID = someID + + @expose() + def index(self): + return self.someID + + class UserController(object): + @expose() + def _lookup(self, someID, *remainder): + return LookupController(someID), remainder + + class RootController(object): + users = UserController() + + return TestApp(Pecan(RootController())) + + def test_canonical_lookup(self): + assert self.app_.get('/users', expect_errors=404).status_int == 404 + assert self.app_.get('/users/', expect_errors=404).status_int == 404 + assert self.app_.get('/users/100').status_int == 302 + assert self.app_.get('/users/100/').body == b_('100') + + class TestControllerArguments(PecanTestCase): @property diff --git a/pecan/tests/test_hooks.py b/pecan/tests/test_hooks.py index 246af76..44963bf 100644 --- a/pecan/tests/test_hooks.py +++ b/pecan/tests/test_hooks.py @@ -1047,21 +1047,6 @@ class TestTransactionHook(PecanTestCase): class TestRequestViewerHook(PecanTestCase): - def test_hook_from_config(self): - from pecan.configuration import _runtime_conf as conf - conf['requestviewer'] = { - 'blacklist': ['/favicon.ico'] - } - - class RootController(object): - pass - - app = make_app(RootController()) - while hasattr(app, 'application'): - app = app.application - del conf.__values__['requestviewer'] - assert app.hooks - def test_basic_single_default_hook(self): _stdout = StringIO() @@ -1073,7 +1058,9 @@ class TestRequestViewerHook(PecanTestCase): app = TestApp( make_app( - RootController(), hooks=[RequestViewerHook(writer=_stdout)] + RootController(), hooks=lambda: [ + RequestViewerHook(writer=_stdout) + ] ) ) response = app.get('/') @@ -1104,7 +1091,9 @@ class TestRequestViewerHook(PecanTestCase): app = TestApp( make_app( - RootController(), hooks=[RequestViewerHook(writer=_stdout)] + RootController(), hooks=lambda: [ + RequestViewerHook(writer=_stdout) + ] ) ) response = app.get('/404', expect_errors=True) @@ -1134,7 +1123,7 @@ class TestRequestViewerHook(PecanTestCase): app = TestApp( make_app( RootController(), - hooks=[ + hooks=lambda: [ RequestViewerHook( config={'items': ['path']}, writer=_stdout ) @@ -1169,7 +1158,7 @@ class TestRequestViewerHook(PecanTestCase): app = TestApp( make_app( RootController(), - hooks=[ + hooks=lambda: [ RequestViewerHook( config={'blacklist': ['/']}, writer=_stdout ) @@ -1196,7 +1185,7 @@ class TestRequestViewerHook(PecanTestCase): app = TestApp( make_app( RootController(), - hooks=[ + hooks=lambda: [ RequestViewerHook( config={'items': ['date']}, writer=_stdout ) |