From bdf62d0653c2e31ae291fc51e7016407b19f6ff4 Mon Sep 17 00:00:00 2001 From: Balazs Gibizer Date: Wed, 25 Jun 2025 10:30:09 +0200 Subject: [PATCH] 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 [1] https://github.com/python/cpython/issues/130327 Closes-Bug: #2103413 Change-Id: I87c9fbb9331135674232c6e77d700966a938b0ac (cherry picked from commit 7d946c45350be935a964990e60bd5213c9aa1423) --- nova/network/neutron.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/nova/network/neutron.py b/nova/network/neutron.py index f24177de15d4..3160846a3b77 100644 --- a/nova/network/neutron.py +++ b/nova/network/neutron.py @@ -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):