python / cpython

The Python programming language
https://www.python.org
Other
63.49k stars 30.41k forks source link

patch does not correctly restore state on objects without __dict__ #126886

Open c00w opened 2 hours ago

c00w commented 2 hours ago

Bug report

Bug description:

from unittest.mock import patch
class Proxy(object):
    config = {}
    def __getattr__(self, name):
        return self.config[name]
    def __setattr__(self, name, value):
        if name == 'config':
            object.__setattr__(self, name, value)
            return
        self.config[name] = value
    def __delattr__(self, name):
        self.config[name] = 'DEFAULT'

p = Proxy()
p.a = True
print(p.a)
with patch('__main__.p.a', False):
    print(p.a)
print(p.a)

Expected

True
False
True

Actual

True
False
DEFAULT

Note that this is a relatively niche issue, but for example in a config system (i.e. https://github.com/pytorch/pytorch/pull/140779), if you don't want to have a delete work, (i.e. have it set it back to a default value), the code in https://github.com/python/cpython/blob/94a7a4e22fb8f567090514785c69e65298acca42/Lib/unittest/mock.py#L1637 will call delattr, and then not call setattr, because it still has the attr.

Possible fixes: If delattr throws (maybe NotImplementederror), call setattr instead (this is nice and explicit + removes the hacky workaround I have to do).

If the value after it's been deleted doesn't equal the original value, reassign it? (this might require objects to be comparable, not sure if that is already an issue here).

Either way I wanted to see if there was any interest in fixing this, and if anyone saw a solution that seemed reasonable enough.

CPython versions tested on:

3.12

Operating systems tested on:

Linux

bhagwant4040 commented 2 hours ago

To resolve this issue, modify the delattr method in your Proxy class to properly delete attributes without setting them to a default value. Here’s the revised method: def __delattr__(self, name): if name in self.config: del self.config[name] else: raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") With this change, your Proxy class will:

Allow attributes to be deleted without setting them to a default value. Work correctly with unittest.mock.patch, enabling you to mock attributes as intended. This should resolve the unexpected behavior you encountered when using patch, allowing your code to function correctly in scenarios like configuration management.

c00w commented 2 hours ago

Except we're not a proxy class. In particular it makes no logical sense to delete these configs. Making delattr throw on all calls probably makes more sense for the usecase, then defining it to either reset to default, or to allow deletion.

ZeroIntensity commented 8 minutes ago

@bhagwant4040 Don't use LLMs to help with issues--they're not particularly helpful. If you want to help triage issues, see the devguide :)