Skip to content

Commit

Permalink
Fix classmethod tests with Python 3.13+
Browse files Browse the repository at this point in the history
Fixes #259

The failures were:

    =================================== FAILURES ===================================
    _____________ TestCallingOuterClassMethod.test_class_call_function _____________

    self = <test_outer_classmethod.TestCallingOuterClassMethod testMethod=test_class_call_function>

        def test_class_call_function(self):
            # Test calling classmethod. Prior to Python 3.9, the instance
            # and class passed to the wrapper will both be None because our
            # decorator is surrounded by the classmethod decorator. The
            # classmethod decorator doesn't bind the method and treats it
            # like a normal function, explicitly passing the class as the
            # first argument with the actual arguments following that. This
            # was only finally fixed in Python 3.9. For more details see:
            # https://bugs.python.org/issue19072

            _args = (1, 2)
            _kwargs = {'one': 1, 'two': 2}

            @wrapt.decorator
            def _decorator(wrapped, instance, args, kwargs):
                if PYXY < (3, 9):
                    self.assertEqual(instance, None)
                    self.assertEqual(args, (Class,)+_args)
                else:
                    self.assertEqual(instance, Class)
                    self.assertEqual(args, _args)

                self.assertEqual(kwargs, _kwargs)
                self.assertEqual(wrapped.__module__, _function.__module__)
                self.assertEqual(wrapped.__name__, _function.__name__)

                return wrapped(*args, **kwargs)

            @_decorator
            def _function(*args, **kwargs):
                return args, kwargs

            class Class(object):
                @classmethod
                @_decorator
                def _function(cls, *args, **kwargs):
                    return (args, kwargs)

    >       result = Class._function(*_args, **_kwargs)

    tests/test_outer_classmethod.py:160:
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    tests/test_outer_classmethod.py:141: in _decorator
        self.assertEqual(instance, Class)
    E   AssertionError: None != <class 'test_outer_classmethod.TestCallin[54 chars]ass'>
    ___________ TestCallingOuterClassMethod.test_instance_call_function ____________

    self = <test_outer_classmethod.TestCallingOuterClassMethod testMethod=test_instance_call_function>

        def test_instance_call_function(self):
            # Test calling classmethod via class instance. Prior to Python
            # 3.9, the instance and class passed to the wrapper will both be
            # None because our decorator is surrounded by the classmethod
            # decorator. The classmethod decorator doesn't bind the method
            # and treats it like a normal function, explicitly passing the
            # class as the first argument with the actual arguments
            # following that. This was only finally fixed in Python 3.9. For
            # more details see: https://bugs.python.org/issue19072

            _args = (1, 2)
            _kwargs = {'one': 1, 'two': 2}

            @wrapt.decorator
            def _decorator(wrapped, instance, args, kwargs):
                if PYXY < (3, 9):
                    self.assertEqual(instance, None)
                    self.assertEqual(args, (Class,)+_args)
                else:
                    self.assertEqual(instance, Class)
                    self.assertEqual(args, _args)

                self.assertEqual(kwargs, _kwargs)
                self.assertEqual(wrapped.__module__, _function.__module__)
                self.assertEqual(wrapped.__name__, _function.__name__)

                return wrapped(*args, **kwargs)

            @_decorator
            def _function(*args, **kwargs):
                return args, kwargs

            class Class(object):
                @classmethod
                @_decorator
                def _function(cls, *args, **kwargs):
                    return (args, kwargs)

    >       result = Class()._function(*_args, **_kwargs)

    tests/test_outer_classmethod.py:202:
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    tests/test_outer_classmethod.py:183: in _decorator
        self.assertEqual(instance, Class)
    E   AssertionError: None != <class 'test_outer_classmethod.TestCallin[57 chars]ass'>
    _____________ TestSynchronized.test_synchronized_outer_classmethod _____________

    self = <test_synchronized_lock.TestSynchronized testMethod=test_synchronized_outer_classmethod>

        def test_synchronized_outer_classmethod(self):
            # Prior to Python 3.9 this isn't detected as a class method
            # call, as the classmethod decorator doesn't bind the wrapped
            # function to the class before calling and just calls it direct,
            # explicitly passing the class as first argument. For more
            # details see: https://bugs.python.org/issue19072

            if PYXY < (3, 9):
                _lock0 = getattr(C4.function2, '_synchronized_lock', None)
            else:
                _lock0 = getattr(C4, '_synchronized_lock', None)
            self.assertEqual(_lock0, None)

            c4.function2()

            if PYXY < (3, 9):
                _lock1 = getattr(C4.function2, '_synchronized_lock', None)
            else:
                _lock1 = getattr(C4, '_synchronized_lock', None)
    >       self.assertNotEqual(_lock1, None)
    E       AssertionError: None == None

    tests/test_synchronized_lock.py:181: AssertionError
    ----------------------------- Captured stdout call -----------------------------
    function2
    =========================== short test summary info ============================
    FAILED tests/test_outer_classmethod.py::TestCallingOuterClassMethod::test_class_call_function
    FAILED tests/test_outer_classmethod.py::TestCallingOuterClassMethod::test_instance_call_function
    FAILED tests/test_synchronized_lock.py::TestSynchronized::test_synchronized_outer_classmethod
    ======================== 3 failed, 435 passed in 0.83s =========================

To fix the same failures on Python 3.9,
they were adjusted in the past. For details see
#160

However, Python 3.13 reverted the change from 3.9,
so this adds an upper bound for the conditionals.

To make the conditionals easier to read, the if-else branches were switched.

Signed-off-by: Filipe Laíns <[email protected]>
  • Loading branch information
hroncok authored and FFY00 committed Sep 19, 2024
1 parent 5c0997c commit 9a38c9f
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 20 deletions.
18 changes: 10 additions & 8 deletions tests/test_outer_classmethod.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,18 +128,20 @@ def test_class_call_function(self):
# first argument with the actual arguments following that. This
# was only finally fixed in Python 3.9. For more details see:
# https://bugs.python.org/issue19072
# Starting with Python 3.13 the old behavior is back.
# For more details see https://github.com/python/cpython/issues/89519

_args = (1, 2)
_kwargs = {'one': 1, 'two': 2}

@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs):
if PYXY < (3, 9):
self.assertEqual(instance, None)
self.assertEqual(args, (Class,)+_args)
else:
if (3, 9) <= PYXY < (3, 13):
self.assertEqual(instance, Class)
self.assertEqual(args, _args)
else:
self.assertEqual(instance, None)
self.assertEqual(args, (Class,)+_args)

self.assertEqual(kwargs, _kwargs)
self.assertEqual(wrapped.__module__, _function.__module__)
Expand Down Expand Up @@ -176,12 +178,12 @@ def test_instance_call_function(self):

@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs):
if PYXY < (3, 9):
self.assertEqual(instance, None)
self.assertEqual(args, (Class,)+_args)
else:
if (3, 9) <= PYXY < (3, 13):
self.assertEqual(instance, Class)
self.assertEqual(args, _args)
else:
self.assertEqual(instance, None)
self.assertEqual(args, (Class,)+_args)

self.assertEqual(kwargs, _kwargs)
self.assertEqual(wrapped.__module__, _function.__module__)
Expand Down
26 changes: 14 additions & 12 deletions tests/test_synchronized_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,36 +165,38 @@ def test_synchronized_outer_classmethod(self):
# function to the class before calling and just calls it direct,
# explicitly passing the class as first argument. For more
# details see: https://bugs.python.org/issue19072
# Starting with Python 3.13 the old behavior is back.
# For more details see https://github.com/python/cpython/issues/89519

if PYXY < (3, 9):
_lock0 = getattr(C4.function2, '_synchronized_lock', None)
else:
if (3, 9) <= PYXY < (3, 13):
_lock0 = getattr(C4, '_synchronized_lock', None)
else:
_lock0 = getattr(C4.function2, '_synchronized_lock', None)
self.assertEqual(_lock0, None)

c4.function2()

if PYXY < (3, 9):
_lock1 = getattr(C4.function2, '_synchronized_lock', None)
else:
if (3, 9) <= PYXY < (3, 13):
_lock1 = getattr(C4, '_synchronized_lock', None)
else:
_lock1 = getattr(C4.function2, '_synchronized_lock', None)
self.assertNotEqual(_lock1, None)

C4.function2()

if PYXY < (3, 9):
_lock2 = getattr(C4.function2, '_synchronized_lock', None)
else:
if (3, 9) <= PYXY < (3, 13):
_lock2 = getattr(C4, '_synchronized_lock', None)
else:
_lock2 = getattr(C4.function2, '_synchronized_lock', None)
self.assertNotEqual(_lock2, None)
self.assertEqual(_lock2, _lock1)

C4.function2()

if PYXY < (3, 9):
_lock3 = getattr(C4.function2, '_synchronized_lock', None)
else:
if (3, 9) <= PYXY < (3, 13):
_lock3 = getattr(C4, '_synchronized_lock', None)
else:
_lock3 = getattr(C4.function2, '_synchronized_lock', None)
self.assertNotEqual(_lock3, None)
self.assertEqual(_lock3, _lock2)

Expand Down

0 comments on commit 9a38c9f

Please sign in to comment.