GrahamDumpleton / wrapt

A Python module for decorators, wrappers and monkey patching.
BSD 2-Clause "Simplified" License
2.03k stars 231 forks source link

Fix classmethod tests with Python 3.13+ #260

Open hroncok opened 6 months ago

hroncok commented 6 months ago

Fixes https://github.com/GrahamDumpleton/wrapt/issues/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 https://github.com/GrahamDumpleton/wrapt/issues/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.