Repository: googleapis/google-cloud-python

Issue:
Vision: Single feature functions generated by cloud vision client library does not support parameter max_results
As specified in [the gRPC reference](https://cloud.google.com/vision/docs/reference/rpc/google.cloud.vision.v1#google.cloud.vision.v1.AnnotateImageRequest), AnnotateImageRequest message have three fields, _image_, _features[]_ and _image_context_, in which the _features[]_ field determines what feature user is request (_type_) and max number of returned results (_max_results_). The code for generating single-feature functions (for example, _face_detection()_), however, does not allow user to specify _max_results_:

```
feature_value = {'type': enum.__dict__[feature]}
def inner(self, image, options=None, **kwargs):
        request = dict(
            image=image,
            features=[feature_value],
            **kwargs
        )
        return self.annotate_image(request, options=options)
```

Reported in https://github.com/GoogleCloudPlatform/python-docs-samples/issues/1173


Discussion:

Resolving patch:
diff --git a/vision/google/cloud/vision_helpers/decorators.py b/vision/google/cloud/vision_helpers/decorators.py
--- a/vision/google/cloud/vision_helpers/decorators.py
+++ b/vision/google/cloud/vision_helpers/decorators.py
@@ -83,8 +83,11 @@ def _create_single_feature_method(feature, enum):
 
     Args:
         image (:class:`~.{module}.types.Image`): The image to analyze.
-        options (:class:`google.gax.CallOptions`): Overrides the
-            default settings for this call, e.g, timeout, retries, etc.
+        max_results (int):
+            Number of results to return, does not apply for
+            TEXT_DETECTION, DOCUMENT_TEXT_DETECTION, or CROP_HINTS.
+        retry (int): Number of retries to do before giving up.
+        timeout (int): Number of seconds before timing out.
         kwargs (dict): Additional properties to be set on the
             :class:`~.{module}.types.AnnotateImageRequest`.
 
@@ -96,18 +99,23 @@ def _create_single_feature_method(feature, enum):
     feature_value = {'type': enum.__dict__[feature]}
 
     # Define the function to be returned.
-    def inner(self, image, retry=None, timeout=None, **kwargs):
+    def inner(self, image, max_results=None,
+              retry=None, timeout=None, **kwargs):
         """Return a single feature annotation for the given image.
 
         Intended for use with functools.partial, to create the particular
         single-feature methods.
         """
+        copied_features = feature_value.copy()
+        if max_results is not None:
+            copied_features['max_results'] = max_results
         request = dict(
             image=image,
-            features=[feature_value],
+            features=[copied_features],
             **kwargs
         )
-        return self.annotate_image(request, retry=retry, timeout=timeout)
+        response = self.annotate_image(request, retry=retry, timeout=timeout)
+        return response
 
     # Set the appropriate function metadata.
     inner.__name__ = fx_name


Test patch:
diff --git a/vision/tests/unit/test_decorators.py b/vision/tests/unit/test_decorators.py
--- a/vision/tests/unit/test_decorators.py
+++ b/vision/tests/unit/test_decorators.py
@@ -52,6 +52,7 @@ class A(object):
 
 
 class SingleFeatureMethodTests(unittest.TestCase):
+
     @mock.patch.object(vision.ImageAnnotatorClient, 'annotate_image')
     def test_runs_generic_single_image(self, ai):
         ai.return_value = vision.types.AnnotateImageResponse()
@@ -60,6 +61,32 @@ def test_runs_generic_single_image(self, ai):
         # image context, will be preserved.
         SENTINEL = mock.sentinel.image_context
 
+        # Make a face detection request.
+        client = vision.ImageAnnotatorClient(
+            credentials=mock.Mock(spec=Credentials),
+        )
+        image = {'source': {'image_uri': 'gs://my-test-bucket/image.jpg'}}
+        max_results = 50
+        response = client.face_detection(image, image_context=SENTINEL,
+                                         max_results=max_results)
+        assert isinstance(response, vision.types.AnnotateImageResponse)
+
+        # Assert that the single-image method was called as expected.
+        ai.assert_called_once_with({
+            'features': [{'type': vision.enums.Feature.Type.FACE_DETECTION,
+                          'max_results': max_results}],
+            'image': image,
+            'image_context': SENTINEL,
+        }, retry=None, timeout=None)
+
+    @mock.patch.object(vision.ImageAnnotatorClient, 'annotate_image')
+    def test_runs_generic_single_image_without_max_results(self, ai):
+        ai.return_value = vision.types.AnnotateImageResponse()
+
+        # Prove that other aspects of the AnnotateImageRequest, such as the
+        # image context, will be preserved.
+        SENTINEL = mock.sentinel.image_context
+
         # Make a face detection request.
         client = vision.ImageAnnotatorClient(
             credentials=mock.Mock(spec=Credentials),