Fix neutron client dict grabbing

Due to a bug in python3.13 [1] the following code will leads to an
emptied dict by the GC even though we hold a reference to the dict.

import gc

class A:

    def __init__(self, client):
        self.__dict__ = client.__dict__
        self.client = client

class B:
    def __init__(self):
        self.test_attr = "foo"

a = A(B())
print(a.__dict__)
print(a.client.__dict__)
gc.collect()
print("##  After gc.collect()")
print(a.__dict__)
print(a.client.__dict__)

 # Output with Python 13
{'test_attr': 'foo', 'client': <__main__.B object at 0x73ea355a8590>}
{'test_attr': 'foo', 'client': <__main__.B object at 0x73ea355a8590>}
 ##  After gc.collect()
{'test_attr': 'foo', 'client': <__main__.B object at 0x73ea355a8590>}
{}

 # Output with Python 12
{'test_attr': 'foo', 'client': <__main__.B object at 0x79c86f355400>}
{'test_attr': 'foo', 'client': <__main__.B object at 0x79c86f355400>}
 ##  After gc.collect()
{'test_attr': 'foo', 'client': <__main__.B object at 0x79c86f355400>}
{'test_attr': 'foo', 'client': <__main__.B object at 0x79c86f355400>

Our neutron client has this kind of code and therefore failing in
python3.13. This patch adds __getattr__ instead of trying to hold a
direct reference to the __dict__. This seems to work around the
problem.

Co-Authored-By: Johannes Kulik <johannes.kulik@sap.com>

[1] https://github.com/python/cpython/issues/130327
Closes-Bug: #2103413

Change-Id: I87c9fbb9331135674232c6e77d700966a938b0ac
(cherry picked from commit 7d946c4535)
This commit is contained in:
Balazs Gibizer
2025-06-25 10:30:09 +02:00
parent 8b0ae7243f
commit bdf62d0653

View File

@@ -171,7 +171,7 @@ def refresh_cache(f):
@profiler.trace_cls("neutron_api")
class ClientWrapper(clientv20.Client):
class ClientWrapper:
"""A Neutron client wrapper class.
Wraps the callable methods, catches Unauthorized,Forbidden from Neutron and
@@ -179,16 +179,18 @@ class ClientWrapper(clientv20.Client):
"""
def __init__(self, base_client, admin):
# Expose all attributes from the base_client instance
self.__dict__ = base_client.__dict__
self.base_client = base_client
self.admin = admin
def __getattribute__(self, name):
obj = object.__getattribute__(self, name)
if callable(obj):
obj = object.__getattribute__(self, 'proxy')(obj)
return obj
def __getattr__(self, name):
base_attr = getattr(self.base_client, name)
# Each callable base client attr is wrapped so that we can translate
# the Unauthorized exception based on if we have an admin client or
# not.
if callable(base_attr):
return self.proxy(base_attr)
return base_attr
def proxy(self, obj):
def wrapper(*args, **kwargs):