Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

freeze_time class decoration causes problems with subclasses and Python 3.11.1 #485

Open
mathiasertl opened this issue Dec 11, 2022 · 2 comments

Comments

@mathiasertl
Copy link

Hi,

I have discovered that the way freezegun.freeze_time decorates unittest.TestCase classes causes Problems in Python 3.11.1 (yes, the micro version) if you create a subclass of a class with the decorator and you use unittest.TestCase.addClassCleanup() in setUpClass() (as Djangos SimpleTestCase classes do, that's how I stumbled on this problem).

The change in Python 3.11.1 is python/cpython#99645. While the problem only surfaces in that special combination, I do believe the right place to fix this is this library, as the freezegun decorator is what triggers this behavior.

To demonstrate the problem, let me first show a contrived example of test case classes:

@freeze_time("2019-04-14 12:26:00")
class FrozenTestCase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        print("BaseTest.setUpClass:", cls)
        super().setUpClass()
    
    def test_something(self):
        print(self.id(), datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"))


class FrozenSubTestCase(FrozenTestCase):
    def test_something_else(self):
        print(self.id(), datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"))

Now if you run the test suite, you'll see that both test methods will always get the frozen time (as expected), but setUpClass() will get the base class even in the subclass. Here's the output (full class paths removed for brevity):

BaseTest.setUpClass: <class 'FrozenTestCase'>
FrozenSubTestCase.test_something 2019-04-14 12:26:00
FrozenSubTestCase.test_something_else 2019-04-14 12:26:00
BaseTest.setUpClass: <class 'FrozenTestCase'>
FrozenTestCase.test_something 2019-04-14 12:26:00

I have checked that just removing the freeze_time decorator restores the expected behavior (setUpClass receives the subclass when run for the subclass) and that this also happens if the method is defined in yet a base class (e.g. Djangos SimpleTestCase).

While certainly odd, The above class works the same way as it does in Python 3.11.1. The problem starts if you have a call to addClassCleanup() in your base class. python/cpython#99645 means that only those registered for the exact class are called, and setUpClass() for the subclass now receives the wrong class (the parent). Our sub-testcase now registers a second cleanup for the parent instead of its own class.

If we add a cleanup function to the base class:

@freeze_time("2019-04-14 12:26:00")
class FrozenTestCase(unittest.TestCase):
    @classmethod
    def custom_cleanup(cls):
        print("Custom cleanup!", cls)

    @classmethod
    def setUpClass(cls):
        print("BaseTest.setUpClass:", cls)
        super().setUpClass()
        cls.addClassCleanup(cls.custom_cleanup)
...

and run the test suite again, you get the following output (again trimmed the output for brevity):

BaseTest.setUpClass: <class 'FrozenTestCase'>
FrozenSubTestCase.test_something 2019-04-14 12:26:00
FrozenSubTestCase.test_something_else 2019-04-14 12:26:00
BaseTest.setUpClass: <class 'FrozenTestCase'>
FrozenTestCase.test_something 2019-04-14 12:26:00
Custom cleanup! <class 'FrozenTestCase'>
Custom cleanup! <class 'FrozenTestCase'>

Notice that the cleanup function is now called twice for the base class, but not at all for the subclass. Sure enough, if you run this with 3.11.0, you get:

BaseTest.setUpClass: <class 'FrozenTestCase'>
FrozenSubTestCase.test_something 2019-04-14 12:26:00
FrozenSubTestCase.test_something_else 2019-04-14 12:26:00
Custom cleanup! <class 'FrozenTestCase'>
BaseTest.setUpClass: <class 'FrozenTestCase'>
FrozenTestCase.test_something 2019-04-14 12:26:00
Custom cleanup! <class 'FrozenTestCase'>

... sure enough, setUpClass() still gets the wrong class, but it's not a problem yet, as cleanups for all base classes are called.

Unfortunately I'm not quite sure how to correctly solve this issue, as I have not found a way to "correctly" decorate a class and wrap class methods so that they get the correct classes.

@blochsbek
Copy link

I think this is also happening in 3.10 as well, getting an import error with Freezetime and test set up....

yajo added a commit to moduon/sale-workflow that referenced this issue Oct 26, 2023
As seen in spulec/freezegun#485, when decorating a class with `@freezegun.freeze_time()`, class cleanups are not properly executed.

For Odoo, this means that the cursor is not properly closed. Thus, if you happen to run a test for another module after this one that needs a Postgres-level lock that conflicts with this module's, it will fail.

This change does the same, but without decorating the class. Then, the cleanups work as expected.

@moduon MT-1075
yajo added a commit to moduon/sale-workflow that referenced this issue Oct 30, 2023
As seen in spulec/freezegun#485, when decorating a class with `@freezegun.freeze_time()`, class cleanups are not properly executed.

For Odoo, this means that the cursor is not properly closed. Thus, if you happen to run a test for another module after this one that needs a Postgres-level lock that conflicts with this module's, it will fail.

This change does the same, but without decorating the class. Then, the cleanups work as expected.

@moduon MT-1075
alan196 pushed a commit to Jarsa/sale-workflow that referenced this issue Nov 23, 2023
As seen in spulec/freezegun#485, when decorating a class with `@freezegun.freeze_time()`, class cleanups are not properly executed.

For Odoo, this means that the cursor is not properly closed. Thus, if you happen to run a test for another module after this one that needs a Postgres-level lock that conflicts with this module's, it will fail.

This change does the same, but without decorating the class. Then, the cleanups work as expected.

@moduon MT-1075
@radekholy24
Copy link

In my case it causes really weird and impossible-to-debug Django transactional issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants