summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonathan LaCour <jonathan@dreamhost.com>2013-08-09 12:35:01 -0700
committerJonathan LaCour <jonathan@dreamhost.com>2013-08-09 12:35:01 -0700
commit2ccf45ba3c96ac69a33170d29894024f12f7cfce (patch)
treece3acb11f8e98391d93987c259f0a27ccc76e4b0
parent8e1ea099d5e64a4df8a11a3e73289368e098beeb (diff)
parentee7c3544bf2288f1e9cbb81d9609ca742a262ce0 (diff)
downloadpecan-2ccf45ba3c96ac69a33170d29894024f12f7cfce.tar.gz
Merge branch 'next' of github.com:dreamhost/pecan
-rw-r--r--docs/source/configuration.rst24
-rw-r--r--docs/source/hooks.rst41
-rw-r--r--pecan/__init__.py51
-rw-r--r--pecan/core.py21
-rw-r--r--pecan/routing.py7
-rw-r--r--pecan/scaffolds/base/+package+/app.py_tmpl12
-rw-r--r--pecan/scaffolds/base/config.py_tmpl4
-rw-r--r--pecan/tests/test_base.py29
-rw-r--r--pecan/tests/test_hooks.py29
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
)