Opened 3 weeks ago

Last modified 3 weeks ago

#37310 assigned Bug

MediaAsset instances with attributes should not compare equal to strings

Reported by: Milad Khoshdel Owned by: Milad Khoshdel
Component: Forms Version: 6.1
Severity: Normal Keywords: MediaAsset hash equality
Cc: Johannes Maron Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

After the fix for #37088, MediaAsset.__eq__() still considers an asset equal to a string containing the same path, while MediaAsset.__hash__() includes the asset's attributes.

This violates Python's requirement that equal objects have equal hashes.

Reproduction on Django 6.2.dev20260831204047:

from django.forms import Script

asset = Script("/static/app.js", defer=True)
path = "/static/app.js"

print(asset == path)
print(hash(asset) == hash(path))
print(path in {asset})

Output:

True
False
False

Although asset == path, a set cannot find path because the two objects are placed in different hash buckets. Dictionary lookups can be affected in the same way. Stylesheet instances with attributes have the same issue.

Expected behavior: Whenever asset == path, hash(asset) == hash(path).

This appears to be a regression introduced by bc9bed573ce39c3b739a4a3b7848816d464b6bdc for #37088 in Django 6.1.

A possible fix is to hash only self._path. Assets with the same path but different attributes may then share a hash while remaining unequal, which is valid Python behavior.

Change History (10)

comment:1 by blighj, 3 weeks ago

Cc: Johannes Maron added

comment:2 by blighj, 3 weeks ago

Cc: Johannes Maron removed

comment:3 by blighj, 3 weeks ago

Cc: Johannes Maron added

comment:4 by Milad Khoshdel, 3 weeks ago

Has patch: set

comment:5 by Milad Khoshdel, 3 weeks ago

Has patch: unset

comment:6 by Milad Khoshdel, 3 weeks ago

I have a patch and regression test ready for this issue. PR #21866 https://github.com/django/django/pull/21866 was automatically closed because the ticket is still Unreviewed. I am continuing to work on the ticket and will reopen or update the PR once the ticket is accepted.

comment:7 by Jacob Walls, 3 weeks ago

Severity: NormalRelease blocker
Summary: MediaAsset instances with attributes violate equality/hash contract with stringsMediaAsset instances with attributes should not compare equal to strings
Triage Stage: UnreviewedAccepted

Thanks, I agree this this doesn't look right. We don't seem to be checking attributes when other is str, e.g.

from django.forms import Script, Stylesheet
asset = Stylesheet("/static/app.js")
asset2 = "/static/app.js"
assert asset == asset2  # works (shouldn't)
asset3 = Script("/static/app.js")
assert asset == asset3  # fails only here

That would lead me to expect a solution like:

  • django/forms/widgets.py

    diff --git a/django/forms/widgets.py b/django/forms/widgets.py
    index a52ad18b09..997c0c92b4 100644
    a b class MediaAsset:  
    7878            self.__class__ is other.__class__
    7979            and self._path == other._path
    8080            and self.attributes == other.attributes
    81         ) or (isinstance(other, str) and self._path == other)
     81        ) or (isinstance(other, str) and not self.attributes and self._path == other)
    8282
    8383    def __hash__(self):
    8484        # Compare path and attrs to ensure performant comparison

Which gives test failures like:

======================================================================
FAIL: test_add_empty (forms_tests.tests.test_media.FormsMediaTestCase.test_add_empty)
----------------------------------------------------------------------
Traceback (most recent call last):
...
    ^^^^^^^^^^^^^^^
AssertionError: Lists differ: [{'screen': [Stylesheet('a.css')]}] != [{'screen': ['a.css']}]

First differing element 0:
{'screen': [Stylesheet('a.css')]}
{'screen': ['a.css']}

- [{'screen': [Stylesheet('a.css')]}]
?              -----------       -

+ [{'screen': ['a.css']}]

... but if there is no functional impact to that change, then can we just update the assertions?

comment:8 by Milad Khoshdel, 3 weeks ago

Thanks, that makes sense. The failing assertions currently rely on this comparison:

Stylesheet("a.css", media="screen") == "a.css"

This is currently True, even though _css_lists contains a Stylesheet instance rather than a string. With the proposed change, it becomes False, so the expected value should use the normalized object:

{"screen": [Stylesheet("a.css", media="screen")]}

The functional change is the intended one: assets with attributes no longer compare equal to plain strings. Regular media merging and rendering remain unchanged because ordinary string paths are normalized into Script or Stylesheet instances before merging.

After updating these assertions, the full Forms test suite passes. I’ll revise the patch accordingly and reopen the PR.

comment:9 by Milad Khoshdel, 3 weeks ago

Has patch: set

A patch is available at PR.

It prevents MediaAsset instances with attributes from comparing equal to strings and updates the affected tests to use normalized Stylesheet instances.

Last edited 3 weeks ago by Milad Khoshdel (previous) (diff)

comment:10 by Sarah Boyce, 3 weeks ago

Severity: Release blockerNormal

Confirmed that this also doesn't work in 5.2 and 6.0, so is not a regression introduced in 6.1

Note: See TracTickets for help on using tickets.
Back to Top