Repository: falconry/falcon

Issue:
Support passing args to hooks
See also: https://stackoverflow.com/questions/27028029/how-to-pass-arguments-to-falcon-before-hook

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/falconry/falcon/pull/1151?src=pr&el=h1) Report
> Merging [#1151](https://codecov.io/gh/falconry/falcon/pull/1151?src=pr&el=desc) into [master](https://codecov.io/gh/falconry/falcon/commit/0c6b6deb8692b06c677893f97c89cc3870595853?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/falconry/falcon/pull/1151/graphs/tree.svg?width=650&token=zOtkfNQ3h6&height=150&src=pr)](https://codecov.io/gh/falconry/falcon/pull/1151?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           master   #1151   +/-   ##
======================================
  Coverage     100%    100%           
======================================
  Files          36      36           
  Lines        2378    2378           
  Branches      347     347           
======================================
  Hits         2378    2378
```


| [Impacted Files](https://codecov.io/gh/falconry/falcon/pull/1151?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [falcon/hooks.py](https://codecov.io/gh/falconry/falcon/pull/1151/diff?src=pr&el=tree#diff-ZmFsY29uL2hvb2tzLnB5) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/falconry/falcon/pull/1151?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/falconry/falcon/pull/1151?src=pr&el=footer). Last update [0c6b6de...8bb3244](https://codecov.io/gh/falconry/falcon/pull/1151?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/api/hooks.rst b/docs/api/hooks.rst
index 16284c6..c8dbbd6 100644
--- a/docs/api/hooks.rst
+++ b/docs/api/hooks.rst
@@ -33,12 +33,43 @@ decorate the resource class:
 
     @falcon.before(extract_project_id)
     class Message(object):
-        def on_post(self, req, resp):
+        def on_post(self, req, resp, project_id):
             pass
 
-        def on_get(self, req, resp):
+        def on_get(self, req, resp, project_id):
             pass
 
+Note also that you can pass additional arguments to your hook function
+as needed:
+
+.. code:: python
+
+    def validate_image_type(req, resp, resource, params, allowed_types):
+        if req.content_type not in allowed_types:
+            msg = 'Image type not allowed.'
+            raise falcon.HTTPBadRequest('Bad request', msg)
+
+    @falcon.before(validate_image_type, ['image/png'])
+    def on_post(self, req, resp):
+        pass
+
+Falcon supports using any callable as a hook. This allows for using a class
+instead of a function:
+
+.. code:: python
+
+    class Authorize(object):
+        def __init__(self, roles):
+            self._roles = roles
+
+        def __call__(self, req, resp, resource, params):
+            pass
+
+    @falcon.before(Authorize(['admin']))
+    def on_post(self, req, resp):
+        pass
+
+
 Falcon :ref:`middleware components <middleware>` can also be used to insert
 logic before and after requests. However, unlike hooks,
 :ref:`middleware components <middleware>` are triggered **globally** for all
diff --git a/falcon/hooks.py b/falcon/hooks.py
index e302b9a..d983759 100644
--- a/falcon/hooks.py
+++ b/falcon/hooks.py
@@ -22,7 +22,7 @@ from falcon import COMBINED_METHODS
 from falcon.util.misc import get_argnames
 
 
-def before(action):
+def before(action, *args, **kwargs):
     """Decorator to execute the given action function *before* the responder.
 
     Args:
@@ -45,6 +45,12 @@ def before(action):
 
                         params['answer'] = 42
 
+        *args: Any additional arguments will be passed to *action* in the
+            order given, immediately following the *req*, *resp*, *resource*,
+            and *params* arguments.
+
+        **kwargs: Any additional keyword arguments will be passed through to
+            *action*.
     """
 
     def _before(responder_or_resource):
@@ -68,7 +74,7 @@ def before(action):
                         # variable that is shared between iterations of the
                         # for loop, above.
                         def let(responder=responder):
-                            do_before_all = _wrap_with_before(action, responder)
+                            do_before_all = _wrap_with_before(responder, action, args, kwargs)
 
                             setattr(resource, responder_name, do_before_all)
 
@@ -78,14 +84,14 @@ def before(action):
 
         else:
             responder = responder_or_resource
-            do_before_one = _wrap_with_before(action, responder)
+            do_before_one = _wrap_with_before(responder, action, args, kwargs)
 
             return do_before_one
 
     return _before
 
 
-def after(action):
+def after(action, *args, **kwargs):
     """Decorator to execute the given action function *after* the responder.
 
     Args:
@@ -94,6 +100,12 @@ def after(action):
             reference to the resource class instance associated with the
             request
 
+        *args: Any additional arguments will be passed to *action* in the
+            order given, immediately following the *req*, *resp*, *resource*,
+            and *params* arguments.
+
+        **kwargs: Any additional keyword arguments will be passed through to
+            *action*.
     """
 
     def _after(responder_or_resource):
@@ -113,7 +125,7 @@ def after(action):
                     if callable(responder):
 
                         def let(responder=responder):
-                            do_after_all = _wrap_with_after(action, responder)
+                            do_after_all = _wrap_with_after(responder, action, args, kwargs)
 
                             setattr(resource, responder_name, do_after_all)
 
@@ -123,7 +135,7 @@ def after(action):
 
         else:
             responder = responder_or_resource
-            do_after_one = _wrap_with_after(action, responder)
+            do_after_one = _wrap_with_after(responder, action, args, kwargs)
 
             return do_after_one
 
@@ -135,13 +147,15 @@ def after(action):
 # -----------------------------------------------------------------------------
 
 
-def _wrap_with_after(action, responder):
+def _wrap_with_after(responder, action, action_args, action_kwargs):
     """Execute the given action function after a responder method.
 
     Args:
+        responder: The responder method to wrap.
         action: A function with a signature similar to a resource responder
             method, taking the form ``func(req, resp, resource)``.
-        responder: The responder method to wrap.
+        action_args: Additiona positional agruments to pass to *action*.
+        action_kwargs: Additional keyword arguments to pass to *action*.
     """
 
     # NOTE(swistakm): create shim before checking what will be actually
@@ -153,8 +167,8 @@ def _wrap_with_after(action, responder):
         # classes in Python vesions prior to 3.4.
         #
         # @wraps(action)
-        def shim(req, resp, resource):
-            action(req, resp)
+        def shim(req, resp, resource, *args, **kwargs):
+            action(req, resp, *args, **kwargs)
 
     responder_argnames = get_argnames(responder)
     extra_argnames = responder_argnames[2:]  # Skip req, resp
@@ -165,18 +179,20 @@ def _wrap_with_after(action, responder):
             _merge_responder_args(args, kwargs, extra_argnames)
 
         responder(self, req, resp, **kwargs)
-        shim(req, resp, self)
+        shim(req, resp, self, *action_args, **action_kwargs)
 
     return do_after
 
 
-def _wrap_with_before(action, responder):
+def _wrap_with_before(responder, action, action_args, action_kwargs):
     """Execute the given action function before a responder method.
 
     Args:
+        responder: The responder method to wrap.
         action: A function with a similar signature to a resource responder
             method, taking the form ``func(req, resp, resource, params)``.
-        responder: The responder method to wrap
+        action_args: Additiona positional agruments to pass to *action*.
+        action_kwargs: Additional keyword arguments to pass to *action*.
     """
 
     # NOTE(swistakm): create shim before checking what will be actually
@@ -188,10 +204,10 @@ def _wrap_with_before(action, responder):
         # classes in Python versions prior to 3.4.
         #
         # @wraps(action)
-        def shim(req, resp, resource, kwargs):
+        def shim(req, resp, resource, params, *args, **kwargs):
             # NOTE(kgriffs): Don't have to pass "self" even if has_self,
             # since method is assumed to be bound.
-            action(req, resp, kwargs)
+            action(req, resp, params, *args, **kwargs)
 
     responder_argnames = get_argnames(responder)
     extra_argnames = responder_argnames[2:]  # Skip req, resp
@@ -201,7 +217,7 @@ def _wrap_with_before(action, responder):
         if args:
             _merge_responder_args(args, kwargs, extra_argnames)
 
-        shim(req, resp, self, kwargs)
+        shim(req, resp, self, kwargs, *action_args, **action_kwargs)
         responder(self, req, resp, **kwargs)
 
     return do_before


Test patch:
diff --git a/tests/test_after_hooks.py b/tests/test_after_hooks.py
index 8675604..b8cf3d2 100644
--- a/tests/test_after_hooks.py
+++ b/tests/test_after_hooks.py
@@ -48,8 +48,10 @@ def serialize_body(req, resp):
         resp.body = 'Nothing to see here. Move along.'
 
 
-def fluffiness(req, resp):
+def fluffiness(req, resp, animal=''):
     resp.body = 'fluffy'
+    if animal:
+        resp.set_header('X-Animal', animal)
 
 
 def resource_aware_fluffiness(req, resp, resource):
@@ -63,14 +65,14 @@ class ResourceAwareFluffiness(object):
         fluffiness(req, resp)
 
 
-def cuteness(req, resp):
-    if resp.body == 'fluffy':
-        resp.body += ' and cute'
+def cuteness(req, resp, check, postfix=' and cute'):
+    if resp.body == check:
+        resp.body += postfix
 
 
 def resource_aware_cuteness(req, resp, resource):
     assert resource
-    cuteness(req, resp)
+    cuteness(req, resp, 'fluffy')
 
 
 class Smartness(object):
@@ -95,8 +97,8 @@ cuteness_in_the_head = functools.partial(things_in_the_head,
                                          'X-Cuteness', 'cute')
 
 
-def fluffiness_in_the_head(req, resp):
-    resp.set_header('X-Fluffiness', 'fluffy')
+def fluffiness_in_the_head(req, resp, value='fluffy'):
+    resp.set_header('X-Fluffiness', value)
 
 
 # --------------------------------------------------------------------
@@ -123,8 +125,8 @@ class WrappedRespondersResource(object):
         pass
 
 
-@falcon.after(cuteness)
-@falcon.after(fluffiness)
+@falcon.after(cuteness, 'fluffy', postfix=' and innocent')
+@falcon.after(fluffiness, 'kitten')
 class WrappedClassResource(object):
 
     # Test that the decorator skips non-callables
@@ -153,7 +155,7 @@ class WrappedClassResourceChild(WrappedClassResource):
 
 class ClassResourceWithURIFields(object):
 
-    @falcon.after(fluffiness_in_the_head)
+    @falcon.after(fluffiness_in_the_head, 'fluffy')
     def on_get(self, req, resp, field1, field2):
         self.fields = (field1, field2)
 
@@ -255,12 +257,14 @@ def test_wrapped_resource(client, resource):
     client.app.add_route('/wrapped', resource)
     result = client.simulate_get('/wrapped')
     assert result.status_code == 200
-    assert result.text == 'fluffy and cute'
+    assert result.text == 'fluffy and innocent'
+    assert result.headers['X-Animal'] == 'kitten'
 
     result = client.simulate_head('/wrapped')
     assert result.status_code == 200
     assert result.headers['X-Fluffiness'] == 'fluffy'
     assert result.headers['X-Cuteness'] == 'cute'
+    assert result.headers['X-Animal'] == 'kitten'
 
     result = client.simulate_post('/wrapped')
     assert result.status_code == 405
@@ -272,6 +276,7 @@ def test_wrapped_resource(client, resource):
     result = client.simulate_options('/wrapped')
     assert result.status_code == 200
     assert not result.text
+    assert 'X-Animal' not in result.headers
 
 
 def test_wrapped_resource_with_hooks_aware_of_resource(client, wrapped_resource_aware):
diff --git a/tests/test_before_hooks.py b/tests/test_before_hooks.py
index 1d58df3..01bebb1 100644
--- a/tests/test_before_hooks.py
+++ b/tests/test_before_hooks.py
@@ -17,28 +17,29 @@ def validate(req, resp, params):
                                 'formatted correctly.')
 
 
-def validate_param(req, resp, params):
-    limit = req.get_param_as_int('limit')
-    if limit and int(limit) > 100:
-        raise falcon.HTTPBadRequest('Out of range', 'limit must be <= 100')
+def validate_param(req, resp, params, param_name, maxval=100):
+    limit = req.get_param_as_int(param_name)
+    if limit and int(limit) > maxval:
+        msg = '{0} must be <= {1}'.format(param_name, maxval)
+        raise falcon.HTTPBadRequest('Out of Range', msg)
 
 
-def resource_aware_validate_param(req, resp, resource, params):
+def resource_aware_validate_param(req, resp, resource, params, param_name, maxval=100):
     assert resource
-    validate_param(req, resp, params)
+    validate_param(req, resp, params, param_name, maxval)
 
 
 class ResourceAwareValidateParam(object):
     def __call__(self, req, resp, resource, params):
         assert resource
-        validate_param(req, resp, params)
+        validate_param(req, resp, params, 'limit')
 
 
-def validate_field(req, resp, params):
+def validate_field(req, resp, params, field_name='test'):
     try:
-        params['id'] = int(params['id'])
+        params[field_name] = int(params[field_name])
     except ValueError:
-        raise falcon.HTTPBadRequest('Invalid ID', 'ID was not valid.')
+        raise falcon.HTTPBadRequest()
 
 
 def parse_body(req, resp, params):
@@ -92,7 +93,7 @@ frogs_in_the_head = functools.partial(
 
 class WrappedRespondersResource(object):
 
-    @falcon.before(validate_param)
+    @falcon.before(validate_param, 'limit', 100)
     @falcon.before(parse_body)
     def on_get(self, req, resp, doc):
         self.req = req
@@ -107,6 +108,10 @@ class WrappedRespondersResource(object):
 
 class WrappedRespondersResourceChild(WrappedRespondersResource):
 
+    @falcon.before(validate_param, 'x', maxval=1000)
+    def on_get(self, req, resp):
+        pass
+
     def on_put(self, req, resp):
         # Test passing no extra args
         super(WrappedRespondersResourceChild, self).on_put(req, resp)
@@ -120,11 +125,11 @@ class WrappedClassResource(object):
     # Test non-callable should be skipped by decorator
     on_patch = {}
 
-    @falcon.before(validate_param)
+    @falcon.before(validate_param, 'limit')
     def on_get(self, req, resp, bunnies):
         self._capture(req, resp, bunnies)
 
-    @falcon.before(validate_param)
+    @falcon.before(validate_param, 'limit')
     def on_head(self, req, resp, bunnies):
         self._capture(req, resp, bunnies)
 
@@ -150,11 +155,11 @@ class WrappedClassResource(object):
 class ClassResourceWithAwareHooks(object):
     hook_as_class = ResourceAwareValidateParam()
 
-    @falcon.before(resource_aware_validate_param)
+    @falcon.before(resource_aware_validate_param, 'limit', 10)
     def on_get(self, req, resp, bunnies):
         self._capture(req, resp, bunnies)
 
-    @falcon.before(resource_aware_validate_param)
+    @falcon.before(resource_aware_validate_param, 'limit')
     def on_head(self, req, resp, bunnies):
         self._capture(req, resp, bunnies)
 
@@ -174,7 +179,7 @@ class ClassResourceWithAwareHooks(object):
 
 class TestFieldResource(object):
 
-    @falcon.before(validate_field)
+    @falcon.before(validate_field, field_name='id')
     def on_get(self, req, resp, id):
         self.id = id
 
@@ -266,6 +271,12 @@ def test_input_validator_inherited(client):
     result = client.simulate_put('/')
     assert result.status_code == 400
 
+    result = client.simulate_get('/', query_string='x=1000')
+    assert result.status_code == 200
+
+    result = client.simulate_get('/', query_string='x=1001')
+    assert result.status_code == 400
+
 
 def test_param_validator(client):
     result = client.simulate_get('/', query_string='limit=10', body='{}')
@@ -335,6 +346,6 @@ def test_wrapped_resource_with_hooks_aware_of_resource(client, wrapped_aware_res
         assert result.status_code == 200
         assert wrapped_aware_resource.bunnies == 'fuzzy'
 
-    result = client.simulate_get('/wrapped_aware', query_string='limit=101')
+    result = client.simulate_get('/wrapped_aware', query_string='limit=11')
     assert result.status_code == 400
     assert wrapped_aware_resource.bunnies == 'fuzzy'