googleapis / python-api-core

https://googleapis.dev/python/google-api-core/latest
Apache License 2.0
116 stars 85 forks source link

Use of `functools.wraps` unintentionally updates _GapicCallable class attributes #579

Open XuanWang-Amos opened 7 months ago

XuanWang-Amos commented 7 months ago

We got a CBF after we imported a PR in grpc Python to g3 (stack trace listed below).

After some digging, we found that it's because the use of functools.wraps unintentionally updates _GapicCallable class attributes _target.

In the PR, we introduced a new attribute _target to grpc multi callables, but the implementation of functools.wraps did more than what's intended, it changed _target attribute of _GapicCallable to the ones from multi callables, causing _GapicCallable._target to change from a function to a byte string, I also provided an example below.

Code example

import functools

class _GapicCallable(object):

    def __init__(self, target):
        print(f"create _GapicCallable with target: {target}")
        self._target = target

    def __call__(self, *args, **kwargs):
        print(f"_GapicCallable.__call__._target: {self._target}")

def wrap_method(func):
  # Fix 2: adding updated
  # return functools.wraps(func, updated=[])(
  return functools.wraps(func)(
      _GapicCallable(func)
  )

class TestCallble:
  # Fix 1: adding __slots__
  # __slots__ = ["_target"]
  def __init__(self):
    self._target = "test"

test_callable = TestCallble()
wrapped = wrap_method(test_callable)  # At this point, _GapicCallable._target is a TestCallble object.
wrapped() # Here, _GapicCallable._target becomes "test" string.

Stack trace

Traceback (most recent call last):
  File "/build/work/3acefca9d25380652e9aea15ea6b9206b95e/google3/runfiles/google3/third_party/tink/python/tink/integration/gcpkms/_gcp_kms_client_integration_test.py", line 77, in test_not_bound_to_key_uri_encrypt_decrypt
    ciphertext = gcp_aead.encrypt(plaintext, associated_data)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/build/work/3acefca9d25380652e9aea15ea6b9206b95e/google3/runfiles/google3/third_party/tink/python/tink/integration/gcpkms/_gcp_kms_client.py", line 48, in encrypt
    response = self.client.encrypt(
               ^^^^^^^^^^^^^^^^^^^^
  File "/build/work/3acefca9d25380652e9aea15ea6b9206b95e/google3/runfiles/google3/third_party/py/google/cloud/kms_v1/gapic/key_management_service_client.py", line 1731, in encrypt
    return self._inner_api_calls["encrypt"](
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/build/work/3acefca9d25380652e9aea15ea6b9206b95e/google3/runfiles/google3/third_party/py/google/api_core/gapic_v1/method.py", line 141, in __call__
    return wrapped_func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/build/work/3acefca9d25380652e9aea15ea6b9206b95e/google3/runfiles/google3/third_party/py/google/api_core/retry.py", line 269, in retry_wrapped_func
    return retry_target(
           ^^^^^^^^^^^^^
  File "/build/work/3acefca9d25380652e9aea15ea6b9206b95e/google3/runfiles/google3/third_party/py/google/api_core/retry.py", line 176, in retry_target
    return target()
           ^^^^^^^^
  File "/build/work/3acefca9d25380652e9aea15ea6b9206b95e/google3/runfiles/google3/third_party/py/google/api_core/timeout.py", line 197, in func_with_timeout
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
TypeError: 'bytes' object is not callable

Suggested Fix

XuanWang-Amos commented 7 months ago

@parthea just FYI.