Skip to content

Regression in Django with singledispatchmethod on models #127750

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

Closed
vodik opened this issue Dec 8, 2024 · 7 comments
Closed

Regression in Django with singledispatchmethod on models #127750

vodik opened this issue Dec 8, 2024 · 7 comments
Labels
3.13 bugs and security fixes 3.14 bugs and security fixes stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@vodik
Copy link

vodik commented Dec 8, 2024

Bug report

Bug description:

Also reported upstream to Django here.

The following code works on Python 3.12 and I believe #85160 is the cause.

When traversing a relationship and then calling a singledispatchmethod method registered on a model, the reference to self seems to get cached and locked to the first created model.

class Test(models.Model):
    ...

class Test2(models.Model):
    relationship = models.ForeignKey("Test", on_delete=models.CASCADE)

    @singledispatchmethod
    def bug(self, x):
        print(">", id(self))

    @bug.register
    def _(self, x: int):
        print(">", id(self))

and then it's pretty easy to trigger:

for t in test.test2_set.all():
    print("id:", id(t))  # always changes
    t.bug(4)             # only ever prints a single id

CPython versions tested on:

3.13

Operating systems tested on:

macOS

Linked PRs

@vodik vodik added the type-bug An unexpected behavior, bug, or error label Dec 8, 2024
@picnixz picnixz added extension-modules C modules in the Modules dir stdlib Python modules in the Lib dir 3.12 only security fixes 3.13 bugs and security fixes 3.14 bugs and security fixes and removed extension-modules C modules in the Modules dir 3.12 only security fixes labels Dec 8, 2024
@vodik
Copy link
Author

vodik commented Dec 9, 2024

I see what the issue is now: https://github.com/AlexWaygood/cpython/blob/3d8d4ba5f5367f2d87488b00afd55f75401c2b7d/Lib/functools.py#L950

Since dictionaries use __hash__/__eq__ for determining if a key is present, we can have collisions. Django hashs models based on the primary key. Here's a way to reproduce the same bug with just the stdlib:

from functools import singledispatchmethod
from dataclasses import dataclass

@dataclass(frozen=True)  # or (unsafe_hash=True)
class Test:
    pk: int

    @singledispatchmethod
    def broken(self, x):
        raise NotImplemenetedError

    @broken.register
    def _(self, x: int):
        return id(self)


t1 = Test(1)
print(id(t1))

t2 = Test(1)
print(id(t2))

assert id(t1) == t1.broken(2)  # both t1 and t2 will share the same dispatcher
assert id(t2) == t2.broken(2)  # fails! 

@vodik
Copy link
Author

vodik commented Dec 9, 2024

I have a fix, I think. Working on a PR.

@vodik
Copy link
Author

vodik commented Dec 9, 2024

I'm pretty sure the code in question is also leaking memory. See comment here.

@eendebakpt
Copy link
Contributor

eendebakpt commented Dec 9, 2024

@vodik There is indeed a behavior change between 3.12 and 3.13, so perhaps that needs to be fixed. Another example showing the issue is:

from functools import lru_cache
from dataclasses import dataclass

@dataclass(frozen=True)  # or (unsafe_hash=True)
class Test:
    pk: int

t1 = Test(1)
t2 = Test(1)  

@lru_cache
def return_id(obj):
    return id(obj)

print(return_id(t1))
print(return_id(t2))

@vodik
Copy link
Author

vodik commented Dec 9, 2024

I didn't think about that, but yeah, I guess yeah you'd see the same behaviour here.

Though there would be an expectation that a dictionary is involved with an lru_cache. Problem with the singledispatchmethod really is that it's hidden.

Funny this is actually documented behaviour on the WeakKeyDictionary docs1.

Footnotes

  1. https://docs.python.org/3/library/weakref.html#weakref.WeakKeyDictionary

serhiy-storchaka added a commit to serhiy-storchaka/cpython that referenced this issue Feb 11, 2025
Remove broken singledispatchmethod caching introduced in pythongh-85160.
Achieve the same performance using different optimization.
@serhiy-storchaka
Copy link
Member

The proposed solutions #127839 and #128648 still have issues with reference loops. They also do not work for all objects (immutable or unhashable). Every solution that involves caching will have such issues.

But caching is not needed for the original issue. Creating a new function is not slow, its updating multiple attributes which will never be used is slow. #130008 proposes an alternative solution. Instead of caching, it delays request of attributes. The resulting performance of calling the singledispatchmethod method is the same.

I think that that changes should be reverted in 3.13. There may be issues with the #130008 solution (not such severe like in the current code, but people can use it in a weird way). We should not risk.

@eendebakpt
Copy link
Contributor

The reference loops in #128648 might be ok from a performance point of view (but to be honest, I am unsure what kind of benchmarks we should run to prove or disprove this). The approach from @serhiy-storchaka seems better though.

I agree with reverting the changes in 3.13, I will make a PR later.

serhiy-storchaka added a commit that referenced this issue Feb 17, 2025
Remove broken singledispatchmethod caching introduced in gh-85160.
Achieve the same performance using different optimization.

* Add more tests.

* Fix issues with __module__ and __doc__ descriptors.
serhiy-storchaka added a commit to serhiy-storchaka/cpython that referenced this issue Feb 17, 2025
serhiy-storchaka added a commit to serhiy-storchaka/cpython that referenced this issue Feb 19, 2025
…ethod

The code is still flawed, because it does not recognize class and static
methods, and the first argument is not removed from the signature of
bound methods, but at least it does not worse than in 3.13 and older.
serhiy-storchaka added a commit that referenced this issue Feb 20, 2025
…H-130309)

The code is still flawed, because it does not recognize class and static
methods, and the first argument is not removed from the signature of
bound methods, but at least it does not worse than in 3.13 and older.
serhiy-storchaka added a commit to serhiy-storchaka/cpython that referenced this issue Feb 20, 2025
…spatchmethod (pythonGH-130309)

The code is still flawed, because it does not recognize class and static
methods, and the first argument is not removed from the signature of
bound methods, but at least it does not worse than in 3.13 and older.
(cherry picked from commit 10b3205)

Co-authored-by: Serhiy Storchaka <[email protected]>
serhiy-storchaka added a commit to serhiy-storchaka/cpython that referenced this issue Feb 20, 2025
serhiy-storchaka added a commit that referenced this issue Feb 20, 2025
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Feb 20, 2025
…ythonGH-130309) (pythonGH-130340)

(cherry picked from commit 68c57d6)

Co-authored-by: Serhiy Storchaka <[email protected]>
(cherry picked from commit 395335d)
(cherry picked from commit 10b3205)
serhiy-storchaka added a commit that referenced this issue Feb 20, 2025
…0309) (GH-130340) (GH-130341)

(cherry picked from commit 68c57d6)

(cherry picked from commit 395335d)
(cherry picked from commit 10b3205)

Co-authored-by: Serhiy Storchaka <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.13 bugs and security fixes 3.14 bugs and security fixes stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

4 participants