Repository: boto/boto

Issue:
MWS @ create_fulfillment_order method required args
It seems that the "desctructuration" of the DestinationAddress & Items makes the requires decorator not being able to recognize the Attributes (DestinationAddress & Items) in request.

```
@structured_objects('DestinationAddress', 'Items')
@requires(['SellerFulfillmentOrderId', 'DisplayableOrderId',
           'ShippingSpeedCategory',    'DisplayableOrderDateTime',
           'DestinationAddress',       'DisplayableOrderComment',
           'Items'])

@api_action('Outbound', 30, 0.5)
def create_fulfillment_order(self, request, response, **kw):
```

to : 

```
@requires(['SellerFulfillmentOrderId', 'DisplayableOrderId',
           'ShippingSpeedCategory',    'DisplayableOrderDateTime',
           'DestinationAddress',       'DisplayableOrderComment',
           'Items'])
@structured_objects('DestinationAddress', 'Items')
@api_action('Outbound', 30, 0.5)
def create_fulfillment_order(self, request, response, **kw):
```

works fine for me, could you check if it's a problem on my side ?

PS: beginner



Discussion:
Sorry for the delay, this is a defect.  Feel free to use your solution for now where necessary; I'll submit a pull request to patch the code.


Resolving patch:
diff --git a/boto/mws/connection.py b/boto/mws/connection.py
--- a/boto/mws/connection.py
+++ b/boto/mws/connection.py
@@ -64,6 +64,7 @@
 def add_attrs_from(func, to):
     for attr in decorated_attrs:
         setattr(to, attr, getattr(func, attr, None))
+    to.__wrapped__ = func
     return to
 
 
@@ -148,7 +149,7 @@ def requires(*groups):
 
     def decorator(func):
 
-        def wrapper(*args, **kw):
+        def requires(*args, **kw):
             hasgroup = lambda group: all(key in kw for key in group)
             if 1 != len(list(filter(hasgroup, groups))):
                 message = ' OR '.join(['+'.join(g) for g in groups])
@@ -157,9 +158,9 @@ def wrapper(*args, **kw):
                 raise KeyError(message)
             return func(*args, **kw)
         message = ' OR '.join(['+'.join(g) for g in groups])
-        wrapper.__doc__ = "{0}\nRequired: {1}".format(func.__doc__,
-                                                      message)
-        return add_attrs_from(func, to=wrapper)
+        requires.__doc__ = "{0}\nRequired: {1}".format(func.__doc__,
+                                                       message)
+        return add_attrs_from(func, to=requires)
     return decorator
 
 
@@ -206,15 +207,15 @@ def requires_some_of(*fields):
 
     def decorator(func):
 
-        def wrapper(*args, **kw):
+        def requires(*args, **kw):
             if not any(i in kw for i in fields):
                 message = "{0} requires at least one of {1} argument(s)" \
                           "".format(func.action, ', '.join(fields))
                 raise KeyError(message)
             return func(*args, **kw)
-        wrapper.__doc__ = "{0}\nSome Required: {1}".format(func.__doc__,
-                                                           ', '.join(fields))
-        return add_attrs_from(func, to=wrapper)
+        requires.__doc__ = "{0}\nSome Required: {1}".format(func.__doc__,
+                                                            ', '.join(fields))
+        return add_attrs_from(func, to=requires)
     return decorator
 
 
@@ -364,10 +365,10 @@ def iter_response(self, response):
             response = more(NextToken=response._result.NextToken)
             yield response
 
+    @requires(['FeedType'])
     @boolean_arguments('PurgeAndReplace')
     @http_body('FeedContent')
     @structured_lists('MarketplaceIdList.Id')
-    @requires(['FeedType'])
     @api_action('Feeds', 15, 120)
     def submit_feed(self, request, response, headers=None, body='', **kw):
         """Uploads a feed for processing by Amazon MWS.
@@ -423,9 +424,9 @@ def get_service_status(self, **kw):
                   "{1}".format(self.__class__.__name__, sections)
         raise AttributeError(message)
 
+    @requires(['ReportType'])
     @structured_lists('MarketplaceIdList.Id')
     @boolean_arguments('ReportOptions=ShowSalesChannel')
-    @requires(['ReportType'])
     @api_action('Reports', 15, 60)
     def request_report(self, request, response, **kw):
         """Creates a report request and submits the request to Amazon MWS.
@@ -536,8 +537,8 @@ def get_report_schedule_count(self, request, response, **kw):
         """
         return self._post_request(request, kw, response)
 
-    @boolean_arguments('Acknowledged')
     @requires(['ReportIdList'])
+    @boolean_arguments('Acknowledged')
     @structured_lists('ReportIdList.Id')
     @api_action('Reports', 10, 45)
     def update_report_acknowledgements(self, request, response, **kw):
@@ -643,8 +644,8 @@ def get_package_tracking_details(self, request, response, **kw):
         """
         return self._post_request(request, kw, response)
 
-    @structured_objects('Address', 'Items')
     @requires(['Address', 'Items'])
+    @structured_objects('Address', 'Items')
     @api_action('Outbound', 30, 0.5)
     def get_fulfillment_preview(self, request, response, **kw):
         """Returns a list of fulfillment order previews based on items
@@ -652,11 +653,11 @@ def get_fulfillment_preview(self, request, response, **kw):
         """
         return self._post_request(request, kw, response)
 
-    @structured_objects('DestinationAddress', 'Items')
     @requires(['SellerFulfillmentOrderId', 'DisplayableOrderId',
                'ShippingSpeedCategory',    'DisplayableOrderDateTime',
                'DestinationAddress',       'DisplayableOrderComment',
                'Items'])
+    @structured_objects('DestinationAddress', 'Items')
     @api_action('Outbound', 30, 0.5)
     def create_fulfillment_order(self, request, response, **kw):
         """Requests that Amazon ship items from the seller's inventory
@@ -703,12 +704,12 @@ def get_outbound_service_status(self, request, response, **kw):
         return self._post_request(request, kw, response)
 
     @requires(['CreatedAfter'], ['LastUpdatedAfter'])
+    @requires(['MarketplaceId'])
     @exclusive(['CreatedAfter'], ['LastUpdatedAfter'])
     @dependent('CreatedBefore', ['CreatedAfter'])
     @exclusive(['LastUpdatedAfter'], ['BuyerEmail'], ['SellerOrderId'])
     @dependent('LastUpdatedBefore', ['LastUpdatedAfter'])
     @exclusive(['CreatedAfter'], ['LastUpdatedBefore'])
-    @requires(['MarketplaceId'])
     @structured_objects('OrderTotal', 'ShippingAddress',
                         'PaymentExecutionDetail')
     @structured_lists('MarketplaceId.Id', 'OrderStatus.Status',


Test patch:
diff --git a/tests/unit/mws/test_connection.py b/tests/unit/mws/test_connection.py
--- a/tests/unit/mws/test_connection.py
+++ b/tests/unit/mws/test_connection.py
@@ -83,6 +83,21 @@ def test_destructure_object(self):
             destructure_object(user, result, prefix='Prefix', members=members)
             self.assertEqual(result, amazon)
 
+    def test_decorator_order(self):
+        for action, func in api_call_map.items():
+            func = getattr(self.service_connection, func)
+            decs = [func.__name__]
+            while func:
+                i = 0
+                while i < len(func.__closure__):
+                    value = func.__closure__[i].cell_contents
+                    if hasattr(value, '__call__'):
+                        if 'requires' == value.__name__:
+                            self.assertTrue(not decs or decs[-1] == 'requires')
+                        decs.append(value.__name__)
+                    i += 1
+                func = getattr(func, '__wrapped__', None)
+
     def test_built_api_call_map(self):
         # Ensure that the map is populated.
         # It starts empty, but the decorators should add to it as they're