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 , 3 weeks ago
| Cc: | added |
|---|
comment:2 by , 3 weeks ago
| Cc: | removed |
|---|
comment:3 by , 3 weeks ago
| Cc: | added |
|---|
comment:4 by , 3 weeks ago
| Has patch: | set |
|---|
comment:5 by , 3 weeks ago
| Has patch: | unset |
|---|
comment:6 by , 3 weeks ago
comment:7 by , 3 weeks ago
| Severity: | Normal → Release blocker |
|---|---|
| Summary: | MediaAsset instances with attributes violate equality/hash contract with strings → MediaAsset instances with attributes should not compare equal to strings |
| Triage Stage: | Unreviewed → Accepted |
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: 78 78 self.__class__ is other.__class__ 79 79 and self._path == other._path 80 80 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) 82 82 83 83 def __hash__(self): 84 84 # 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 , 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 , 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.
comment:10 by , 3 weeks ago
| Severity: | Release blocker → Normal |
|---|
Confirmed that this also doesn't work in 5.2 and 6.0, so is not a regression introduced in 6.1
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.