Repository: aws/chalice

Issue:
Imported Blueprint containing pure lambda does not execute
# Issue
Calling a pure lambda defined in a Blueprint that is registered in `app.py` returns a `TypeError`
# Received error
Error received from CLI invocation `chalice invoke -n token_generator`
```
Traceback (most recent call last):
  File "/var/runtime/bootstrap.py", line 131, in handle_event_request
    response = request_handler(event, lambda_context)
TypeError: _defer_wrap_handler() takes 1 positional argument but 2 were given
Error: Unhandled exception in Lambda function, details above
```
# Associated files
## File Structure
```
$ tree
.
├── chalicelib
│   ├── __init__.py
│   ├── blueprints
│   │   └── token_generatory_blueprint.py
│   └── utils
│        └── token_generatory.py
├── app.py
└── requirements.txt
```
## Files
`app.py`
```python
from chalice import Chalice
from chalicelib.blueprints from token_generator_blueprint

app = Chalice(app_name='project_name')
app.register_blueprint(token_generator_blueprint.token_generator_blueprint)
```
`token_generator_blueprint.py`
```
"""collection of pure AWS Lambda functions"""
from chalicelib.utils import token_generator

import logging

from chalice import Blueprint

token_generator_blueprint = Blueprint(__name__)
logger = logging.getLogger('root')

@token_generator_blueprint.lambda_function(name='token_generator')
def lambda_token_generator_blueprint(event, context):
  return token_generator.handler(event, context)
```
`token_generator.py`
```
def handler(event, context):
  return {"foo":"bar"}
```




Discussion:

Patch:
diff --git a/.changes/next-release/28759404254-bugfix-Blueprints-51536.json b/.changes/next-release/28759404254-bugfix-Blueprints-51536.json
new file mode 100644
index 0000000..ebb1743
--- /dev/null
+++ b/.changes/next-release/28759404254-bugfix-Blueprints-51536.json
@@ -0,0 +1,5 @@
+{
+  "type": "bugfix",
+  "category": "Blueprints",
+  "description": "Fix regression when invoking Lambda functions from blueprints (#1535)"
+}
diff --git a/chalice/app.py b/chalice/app.py
index a9506a9..08671a6 100644
--- a/chalice/app.py
+++ b/chalice/app.py
@@ -769,27 +769,7 @@ class DecoratorAPI(object):
         return _register_handler
 
     def _wrap_handler(self, handler_type, handler_name, user_handler):
-        event_classes = {
-            'on_s3_event': S3Event,
-            'on_sns_message': SNSEvent,
-            'on_sqs_message': SQSEvent,
-            'on_cw_event': CloudWatchEvent,
-            'on_kinesis_record': KinesisEvent,
-            'on_dynamodb_record': DynamoDBEvent,
-            'schedule': CloudWatchEvent,
-            'lambda_function': LambdaFunctionEvent,
-        }
-        middleware_mapping = {
-            'on_s3_event': 's3',
-            'on_sns_message': 'sns',
-            'on_sqs_message': 'sqs',
-            'on_cw_event': 'cloudwatch',
-            'on_kinesis_record': 'kinesis',
-            'on_dynamodb_record': 'dynamodb',
-            'schedule': 'scheduled',
-            'lambda_function': 'pure_lambda',
-        }
-        if handler_type in event_classes:
+        if handler_type in _EVENT_CLASSES:
             if handler_type == 'lambda_function':
                 # We have to wrap existing @app.lambda_function()
                 # handlers for backwards compat reasons so we can
@@ -799,9 +779,9 @@ class DecoratorAPI(object):
                 # here.
                 user_handler = PureLambdaWrapper(user_handler)
             return EventSourceHandler(
-                user_handler, event_classes[handler_type],
+                user_handler, _EVENT_CLASSES[handler_type],
                 middleware_handlers=self._get_middleware_handlers(
-                    event_type=middleware_mapping[handler_type],
+                    event_type=_MIDDLEWARE_MAPPING[handler_type],
                 )
             )
 
@@ -1506,6 +1486,14 @@ class EventSourceHandler(BaseLambdaHandler):
         self._middleware_handlers = middleware_handlers
         self.handler = None
 
+    @property
+    def middleware_handlers(self):
+        return self._middleware_handlers
+
+    @middleware_handlers.setter
+    def middleware_handlers(self, value):
+        self._middleware_handlers = value
+
     def __call__(self, event, context):
         event_obj = self.event_class(event, context)
         if self.handler is None:
@@ -1906,17 +1894,6 @@ class Blueprint(DecoratorAPI):
     # these methods are not public in the sense that we don't want users to
     # call them, they're available for blueprints to use in order to avoid
     # boilerplate code.
-    def _wrap_handler(self, handler_type, handler_name, user_handler):
-        # This deferred wrap handler will be called when we call
-        # Blueprint.register() and provide an app object.  Once
-        # we have access to an app, we can call the actual _wrap_handler
-        # which will have access to middleware.
-        @functools.wraps(user_handler)
-        def _defer_wrap_handler(app):
-            # pylint: disable=protected-access
-            return app._wrap_handler(handler_type, handler_name,
-                                     user_handler)
-        return _defer_wrap_handler
 
     def register_middleware(self, func, event_type='all'):
         self._deferred_registrations.append(
@@ -1932,18 +1909,22 @@ class Blueprint(DecoratorAPI):
         # we have to duplicate either the methods or the params in this
         # class.  We're using _register_handler as a tradeoff for cutting
         # down on the duplication.
-        self._deferred_registrations.append(
+        def _register_blueprint_handler(app, options):
+            if handler_type in _EVENT_CLASSES:
+                # pylint: disable=protected-access
+                wrapped_handler.middleware_handlers = \
+                    app._get_middleware_handlers(
+                        _MIDDLEWARE_MAPPING[handler_type])
             # pylint: disable=protected-access
-            lambda app, options: app._register_handler(
-                handler_type, name, user_handler, wrapped_handler(app),
+            app._register_handler(
+                handler_type, name, user_handler, wrapped_handler,
                 kwargs, options
             )
-        )
+        self._deferred_registrations.append(_register_blueprint_handler)
 
     def _get_middleware_handlers(self, event_type):
-        raise RuntimeError("Blueprints are unable to retrieve middleware "
-                           "handlers, must be registered to an app using "
-                           "register_blueprint()")
+        # This will get filled in later during the registration process.
+        return []
 
 
 # This class is used to convert any existing/3rd party decorators
@@ -1977,3 +1958,27 @@ class ConvertToMiddleware(object):
         if isinstance(event, Request):
             return event.to_original_event(), event.lambda_context
         return event.to_dict(), event.context
+
+
+_EVENT_CLASSES = {
+    'on_s3_event': S3Event,
+    'on_sns_message': SNSEvent,
+    'on_sqs_message': SQSEvent,
+    'on_cw_event': CloudWatchEvent,
+    'on_kinesis_record': KinesisEvent,
+    'on_dynamodb_record': DynamoDBEvent,
+    'schedule': CloudWatchEvent,
+    'lambda_function': LambdaFunctionEvent,
+}
+
+
+_MIDDLEWARE_MAPPING = {
+    'on_s3_event': 's3',
+    'on_sns_message': 'sns',
+    'on_sqs_message': 'sqs',
+    'on_cw_event': 'cloudwatch',
+    'on_kinesis_record': 'kinesis',
+    'on_dynamodb_record': 'dynamodb',
+    'schedule': 'scheduled',
+    'lambda_function': 'pure_lambda',
+}


Test patch:
diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py
index bf5d5b4..8a2a261 100644
--- a/tests/unit/test_app.py
+++ b/tests/unit/test_app.py
@@ -2185,6 +2185,23 @@ def test_can_combine_multiple_blueprints_in_single_app():
     assert sorted(list(myapp.routes)) == ['/bar', '/foo']
 
 
+def test_can_preserve_signature_on_blueprint():
+    myapp = app.Chalice('myapp')
+    foo = app.Blueprint('foo')
+
+    @foo.lambda_function()
+    def first(event, context):
+        return {'foo': 'bar'}
+
+    myapp.register_blueprint(foo)
+
+    # The handler string given to a blueprint
+    # is the "module.function_name" so we have
+    # to ensure we can continue to invoke the
+    # function with its expected signature.
+    assert first({}, None) == {'foo': 'bar'}
+
+
 def test_doc_saved_on_route():
     myapp = app.Chalice('myapp')