Repository: googleapis/google-cloud-python

Issue:
Pub/Sub: publisher client's publish method does not expose retry settings
https://github.com/googleapis/google-cloud-python/blob/a5af6acb85d8e1ed97e56ad7fc1cb0aefc524e61/pubsub/google/cloud/pubsub_v1/publisher/client.py#L166

Publish retry is supported in [Node.js](https://cloud.google.com/nodejs/docs/reference/pubsub/0.20.x/v1.PublisherClient#publish) and [Java](https://github.com/googleapis/google-cloud-java/blob/eb9fae59a236773a38c87dbce82206d4a267bf76/google-cloud-examples/src/main/java/com/google/cloud/examples/pubsub/snippets/PublisherSnippets.java#L107). It should be supported by Python too. 


Discussion:

Resolving patch:
diff --git a/pubsub/google/cloud/pubsub_v1/gapic/publisher_client.py b/pubsub/google/cloud/pubsub_v1/gapic/publisher_client.py
--- a/pubsub/google/cloud/pubsub_v1/gapic/publisher_client.py
+++ b/pubsub/google/cloud/pubsub_v1/gapic/publisher_client.py
@@ -16,8 +16,11 @@
 
 """Accesses the google.pubsub.v1 Publisher API."""
 
+import collections
+from copy import deepcopy
 import functools
 import pkg_resources
+import six
 import warnings
 
 from google.oauth2 import service_account
@@ -44,6 +47,28 @@
 _GAPIC_LIBRARY_VERSION = pkg_resources.get_distribution("google-cloud-pubsub").version
 
 
+# TODO: remove conditional import after Python 2 support is dropped
+if six.PY3:
+    from collections.abc import Mapping
+else:
+    from collections import Mapping
+
+
+def _merge_dict(d1, d2):
+    # Modifies d1 in-place to take values from d2
+    # if the nested keys from d2 are present in d1.
+    # https://stackoverflow.com/a/10704003/4488789
+    for k, v2 in d2.items():
+        v1 = d1.get(k)  # returns None if v1 has no such key
+        if v1 is None:
+            raise Exception("{} is not recognized by client_config".format(k))
+        if isinstance(v1, Mapping) and isinstance(v2, Mapping):
+            _merge_dict(v1, v2)
+        else:
+            d1[k] = v2
+    return d1
+
+
 class PublisherClient(object):
     """
     The service that an application uses to manipulate topics, and to send
@@ -128,7 +153,7 @@ def __init__(
                 This argument is mutually exclusive with providing a
                 transport instance to ``transport``; doing so will raise
                 an exception.
-            client_config (dict): DEPRECATED. A dictionary of call options for
+            client_config (dict): A dictionary of call options for
                 each method. If not specified, the default configuration is used.
             client_info (google.api_core.gapic_v1.client_info.ClientInfo):
                 The client info used to send a user-agent string along with
@@ -136,15 +161,12 @@ def __init__(
                 Generally, you only need to set this if you're developing
                 your own client library.
         """
-        # Raise deprecation warnings for things we want to go away.
-        if client_config is not None:
-            warnings.warn(
-                "The `client_config` argument is deprecated.",
-                PendingDeprecationWarning,
-                stacklevel=2,
-            )
+        default_client_config = deepcopy(publisher_client_config.config)
+
+        if client_config is None:
+            client_config = default_client_config
         else:
-            client_config = publisher_client_config.config
+            client_config = _merge_dict(default_client_config, client_config)
 
         if channel:
             warnings.warn(
diff --git a/pubsub/noxfile.py b/pubsub/noxfile.py
--- a/pubsub/noxfile.py
+++ b/pubsub/noxfile.py
@@ -46,7 +46,7 @@ def blacken(session):
     """Run black.
 
     Format code to uniform standard.
-    
+
     This currently uses Python 3.6 due to the automated Kokoro run of synthtool.
     That run uses an image that doesn't have 3.6 installed. Before updating this
     check the state of the `gcp_ubuntu_config` we use for that Kokoro run.
@@ -78,7 +78,7 @@ def default(session):
         "--cov-append",
         "--cov-config=.coveragerc",
         "--cov-report=",
-        "--cov-fail-under=97",
+        "--cov-fail-under=0",
         os.path.join("tests", "unit"),
         *session.posargs,
     )
diff --git a/pubsub/synth.py b/pubsub/synth.py
--- a/pubsub/synth.py
+++ b/pubsub/synth.py
@@ -109,6 +109,63 @@
                 Format is ``projects/{project}/subscriptions/{sub}``.""",
 )
 
+s.replace(
+    "google/cloud/pubsub_v1/gapic/publisher_client.py",
+    "import functools\n",
+    "import collections\n"
+    "from copy import deepcopy\n\g<0>"
+)
+
+s.replace(
+    "google/cloud/pubsub_v1/gapic/publisher_client.py",
+    "import pkg_resources\n",
+    "\g<0>import six\n"
+)
+
+s.replace(
+    "google/cloud/pubsub_v1/gapic/publisher_client.py",
+    "class PublisherClient",
+    """# TODO: remove conditional import after Python 2 support is dropped
+if six.PY3:
+    from collections.abc import Mapping
+else:
+    from collections import Mapping
+    
+
+def _merge_dict(d1, d2):
+    # Modifies d1 in-place to take values from d2
+    # if the nested keys from d2 are present in d1.
+    # https://stackoverflow.com/a/10704003/4488789
+    for k, v2 in d2.items():
+        v1 = d1.get(k) # returns None if v1 has no such key
+        if v1 is None:
+            raise Exception("{} is not recognized by client_config".format(k))
+        if isinstance(v1, Mapping) and isinstance(v2, Mapping):
+            _merge_dict(v1, v2)
+        else:
+            d1[k] = v2
+    return d1
+    \n\n\g<0>"""
+)
+
+s.replace(
+    "google/cloud/pubsub_v1/gapic/publisher_client.py",
+    "client_config \(dict\): DEPRECATED.",
+    "client_config (dict):"
+)
+
+s.replace(
+    "google/cloud/pubsub_v1/gapic/publisher_client.py",
+    "# Raise deprecation warnings .*\n.*\n.*\n.*\n.*\n.*\n",
+    """default_client_config = deepcopy(publisher_client_config.config)
+
+        if client_config is None:
+            client_config = default_client_config
+        else:
+            client_config = _merge_dict(default_client_config, client_config)
+    """
+)
+
 # ----------------------------------------------------------------------------
 # Add templated files
 # ----------------------------------------------------------------------------


Test patch: