From fe830674abd4926d96d38f9992f3e31b00cd891a Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 25 Feb 2021 11:37:42 +0000 Subject: [PATCH 2/4] Fix tests on Python 3.9 I'm not entirely sure this is correct, but it's the only thing I can find related to changes in classmethod in Python 3.9. Signed-off-by: Stephen Finucane --- fixtures/_fixtures/monkeypatch.py | 9 +++- fixtures/tests/_fixtures/test_monkeypatch.py | 48 ++++++++++++++------ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/fixtures/_fixtures/monkeypatch.py b/fixtures/_fixtures/monkeypatch.py index b5e8564..710a79c 100644 --- a/fixtures/_fixtures/monkeypatch.py +++ b/fixtures/_fixtures/monkeypatch.py @@ -83,9 +83,11 @@ def _coerce_values(obj, name, new_value, sentinel): # bound state rather than having it bound to the new object # it has been patched onto. captured_method = new_value + @functools.wraps(old_value) def avoid_get(*args, **kwargs): return captured_method(*args, **kwargs) + new_value = avoid_get return (new_value, old_value) @@ -138,18 +140,21 @@ def _setUp(self): __import__(location, {}, {}) except ImportError: pass + components = location.split('.') current = __import__(components[0], {}, {}) for component in components[1:]: current = getattr(current, component) sentinel = object() - new_value, old_value = _coerce_values(current, attribute, - self.new_value, sentinel) + new_value, old_value = _coerce_values( + current, attribute, self.new_value, sentinel) + if self.new_value is self.delete: if old_value is not sentinel: delattr(current, attribute) else: setattr(current, attribute, new_value) + if old_value is sentinel: self.addCleanup(self._safe_delete, current, attribute) else: diff --git a/fixtures/tests/_fixtures/test_monkeypatch.py b/fixtures/tests/_fixtures/test_monkeypatch.py index 6f11fab..746f6dd 100644 --- a/fixtures/tests/_fixtures/test_monkeypatch.py +++ b/fixtures/tests/_fixtures/test_monkeypatch.py @@ -14,6 +14,7 @@ # limitations under that license. import functools +import sys import testtools from testtools.matchers import Is @@ -32,7 +33,7 @@ def foo_cls(cls): pass class D(object): def bar(self): pass - def bar_two_args(self, arg): + def bar_two_args(self, arg=None): return (self, arg) @classmethod def bar_cls(cls): @@ -188,14 +189,27 @@ def test_patch_classmethod_with_classmethod(self): 'fixtures.tests._fixtures.test_monkeypatch.C.foo_cls', D.bar_cls_args) with fixture: - cls, target_class = C.foo_cls() - self.expectThat(cls, Is(D)) - self.expectThat(target_class, Is(C)) - cls, target_class = C().foo_cls() - self.expectThat(cls, Is(D)) - self.expectThat(target_class, Is(C)) - self._check_restored_static_or_class_method(oldmethod, oldmethod_inst, - C, 'foo_cls') + # Python 3.9 changes the behavior of the classmethod decorator so + # that it now honours the descriptor binding protocol [1]. + # This means we're now going to call the monkeypatched classmethod + # the way we would have if it hadn't been monkeypatched: simply + # with the class + # + # https://bugs.python.org/issue19072 + if sys.version_info >= (3, 9): + cls, = C.foo_cls() + self.expectThat(cls, Is(D)) + cls, = C().foo_cls() + self.expectThat(cls, Is(D)) + else: + cls, target_class = C.foo_cls() + self.expectThat(cls, Is(D)) + self.expectThat(target_class, Is(C)) + cls, target_class = C().foo_cls() + self.expectThat(cls, Is(D)) + self.expectThat(target_class, Is(C)) + self._check_restored_static_or_class_method( + oldmethod, oldmethod_inst, C, 'foo_cls') def test_patch_classmethod_with_function(self): oldmethod = C.foo_cls @@ -222,12 +236,20 @@ def test_patch_classmethod_with_boundmethod(self): with fixture: slf, cls = C.foo_cls() self.expectThat(slf, Is(d)) - self.expectThat(cls, Is(C)) + # See note in test_patch_classmethod_with_classmethod on changes in + # Python 3.9 + if sys.version_info >= (3, 9): + self.expectThat(cls, Is(None)) + else: + self.expectThat(cls, Is(C)) slf, cls = C().foo_cls() self.expectThat(slf, Is(d)) - self.expectThat(cls, Is(C)) - self._check_restored_static_or_class_method(oldmethod, oldmethod_inst, - C, 'foo_cls') + if sys.version_info >= (3, 9): + self.expectThat(cls, Is(None)) + else: + self.expectThat(cls, Is(C)) + self._check_restored_static_or_class_method( + oldmethod, oldmethod_inst, C, 'foo_cls') def test_patch_function_with_staticmethod(self): oldmethod = fake_no_args