refactor: don't inherit media if child set to None (#1224)

* refactor: don't inherit media if child set to None

* refactor: fix typing errors

* refactor: more type fixes
This commit is contained in:
Juro Oravec 2025-06-02 16:24:27 +02:00 committed by GitHub
parent 8677ee7941
commit 09cb8714cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 482 additions and 77 deletions

View file

@ -1,8 +1,12 @@
import os
import re
import sys
from pathlib import Path
from textwrap import dedent
from typing import Optional
import pytest
from django.core.exceptions import ImproperlyConfigured
from django.forms.widgets import Media
from django.template import Context, Template
from django.templatetags.static import static
@ -11,6 +15,7 @@ from django.utils.safestring import mark_safe
from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import Component, autodiscover, registry, render_dependencies, types
from django_components.component_media import UNSET
from django_components.testing import djc_test
from .testutils import setup_test_config
@ -24,12 +29,14 @@ setup_test_config({"autodiscover": False})
class TestMainMedia:
def test_html_js_css_inlined(self):
class TestComponent(Component):
template = dedent("""
template = dedent(
"""
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
<div class='html-css-only'>Content</div>
""")
"""
)
css = ".html-css-only { color: blue; }"
js = "console.log('HTML and JS only');"
@ -52,12 +59,14 @@ class TestMainMedia:
)
# Check that the HTML / JS / CSS can be accessed on the component class
assert TestComponent.template == dedent("""
assert TestComponent.template == dedent(
"""
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
<div class='html-css-only'>Content</div>
""")
"""
)
assert TestComponent.css == ".html-css-only { color: blue; }"
assert TestComponent.js == "console.log('HTML and JS only');"
@ -170,8 +179,7 @@ class TestMainMedia:
"}"
)
assert TestComponent.js == (
"/* Used in `MainMediaTest` tests in `test_component_media.py` */\n"
'console.log("HTML and JS only");\n'
'/* Used in `MainMediaTest` tests in `test_component_media.py` */\nconsole.log("HTML and JS only");\n'
)
@djc_test(
@ -189,7 +197,7 @@ class TestMainMedia:
# NOTE: Since this is a subclass, actual CSS is defined on the parent class, and thus
# the corresponding ComponentMedia instance is also on the parent class.
assert AppLvlCompComponent._component_media.css is None # type: ignore[attr-defined]
assert AppLvlCompComponent._component_media.css is UNSET # type: ignore[attr-defined]
assert AppLvlCompComponent._component_media.css_file == "app_lvl_comp.css" # type: ignore[attr-defined]
# Access the property to load the CSS
@ -379,6 +387,7 @@ class TestComponentMedia:
)
def test_glob_pattern_relative_to_component(self):
from tests.components.glob.glob import GlobComponent
rendered = GlobComponent.render()
assertInHTML('<link href="glob/glob_1.css" media="all" rel="stylesheet">', rendered)
@ -393,6 +402,7 @@ class TestComponentMedia:
)
def test_glob_pattern_relative_to_root_dir(self):
from tests.components.glob.glob import GlobComponentRootDir
rendered = GlobComponentRootDir.render()
assertInHTML('<link href="glob/glob_1.css" media="all" rel="stylesheet">', rendered)
@ -407,6 +417,7 @@ class TestComponentMedia:
)
def test_non_globs_not_modified(self):
from tests.components.glob.glob import NonGlobComponentRootDir
rendered = NonGlobComponentRootDir.render()
assertInHTML('<link href="glob/glob_1.css" media="all" rel="stylesheet">', rendered)
@ -419,6 +430,7 @@ class TestComponentMedia:
)
def test_non_globs_not_modified_nonexist(self):
from tests.components.glob.glob import NonGlobNonexistComponentRootDir
rendered = NonGlobNonexistComponentRootDir.render()
assertInHTML('<link href="glob/glob_nonexist.css" media="all" rel="stylesheet">', rendered)
@ -426,6 +438,7 @@ class TestComponentMedia:
def test_glob_pattern_does_not_break_urls(self):
from tests.components.glob.glob import UrlComponent
rendered = UrlComponent.render()
assertInHTML('<link href="https://example.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
@ -434,10 +447,16 @@ class TestComponentMedia:
assertInHTML('<link href="%3A//example.com/example/style.min.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<link href="/path/to/style.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered)
assertInHTML('<script src="http://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered)
assertInHTML(
'<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered
)
assertInHTML(
'<script src="http://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered
)
# `://` is escaped because Django's `Media.absolute_path()` doesn't consider `://` a valid URL
assertInHTML('<script src="%3A//cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered)
assertInHTML(
'<script src="%3A//cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered
)
assertInHTML('<script src="/path/to/script.js"></script>', rendered)
@ -831,9 +850,7 @@ class TestMediaStaticfiles:
# NOTE: Since we're using ManifestStaticFilesStorage, we expect the rendered media to link
# to the files as defined in staticfiles.json
assertInHTML(
'<link href="/static/calendar/style.0eeb72042b59.css" media="all" rel="stylesheet">', rendered
)
assertInHTML('<link href="/static/calendar/style.0eeb72042b59.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<script defer src="/static/calendar/script.e1815e23e0ec.js"></script>', rendered)
@ -1014,6 +1031,129 @@ class TestMediaRelativePath:
assertInHTML('<script type="module" src="relative_file_pathobj.js"></script>', rendered)
@djc_test
class TestSubclassingAttributes:
def test_both_js_and_js_file_none(self):
class TestComp(Component):
js = None
js_file = None
assert TestComp.js is None
assert TestComp.js_file is None
def test_mixing_none_and_non_none_raises(self):
with pytest.raises(
ImproperlyConfigured,
match=re.escape("Received non-empty value from both 'js' and 'js_file' in Component TestComp"),
):
class TestComp(Component):
js = "console.log('hi')"
js_file = None
def test_both_non_none_raises(self):
with pytest.raises(
ImproperlyConfigured,
match=re.escape("Received non-empty value from both 'js' and 'js_file' in Component TestComp"),
):
class TestComp(Component):
js = "console.log('hi')"
js_file = "file.js"
def test_parent_non_null_child_non_null(self):
class ParentComp(Component):
js = "console.log('parent')"
class TestComp(ParentComp):
js = "console.log('child')"
assert TestComp.js == "console.log('child')"
assert TestComp.js_file is None
def test_parent_null_child_non_null(self):
class ParentComp(Component):
js = None
class TestComp(ParentComp):
js = "console.log('child')"
assert TestComp.js == "console.log('child')"
assert TestComp.js_file is None
def test_parent_non_null_child_null(self):
class ParentComp(Component):
js: Optional[str] = "console.log('parent')"
class TestComp(ParentComp):
js = None
assert TestComp.js is None
assert TestComp.js_file is None
def test_parent_null_child_null(self):
class ParentComp(Component):
js = None
class TestComp(ParentComp):
js = None
assert TestComp.js is None
assert TestComp.js_file is None
def test_grandparent_non_null_parent_pass_child_pass(self):
class GrandParentComp(Component):
js = "console.log('grandparent')"
class ParentComp(GrandParentComp):
pass
class TestComp(ParentComp):
pass
assert TestComp.js == "console.log('grandparent')"
assert TestComp.js_file is None
def test_grandparent_non_null_parent_null_child_pass(self):
class GrandParentComp(Component):
js: Optional[str] = "console.log('grandparent')"
class ParentComp(GrandParentComp):
js = None
class TestComp(ParentComp):
pass
assert TestComp.js is None
assert TestComp.js_file is None
def test_grandparent_non_null_parent_pass_child_non_null(self):
class GrandParentComp(Component):
js = "console.log('grandparent')"
class ParentComp(GrandParentComp):
pass
class TestComp(ParentComp):
js = "console.log('child')"
assert TestComp.js == "console.log('child')"
assert TestComp.js_file is None
def test_grandparent_null_parent_pass_child_non_null(self):
class GrandParentComp(Component):
js = None
class ParentComp(GrandParentComp):
pass
class TestComp(ParentComp):
js = "console.log('child')"
assert TestComp.js == "console.log('child')"
assert TestComp.js_file is None
@djc_test
class TestSubclassingMedia:
def test_media_in_child_and_parent(self):
@ -1060,8 +1200,9 @@ class TestSubclassingMedia:
css = "grandparent.css"
js = "grandparent.js"
# `pass` means that we inherit `Media` from `GrandParentComponent`
class ParentComponent(GrandParentComponent):
Media = None # type: ignore[assignment]
pass
class ChildComponent(ParentComponent):
class Media:
@ -1083,6 +1224,40 @@ class TestSubclassingMedia:
'<script src="grandparent.js"></script>'
)
# Check that setting `Media = None` on a child class means that we will NOT inherit `Media` from the parent class
def test_media_in_child_and_grandparent__inheritance_off(self):
class GrandParentComponent(Component):
template: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
"""
class Media:
css = "grandparent.css"
js = "grandparent.js"
# `None` means that we will NOT inherit `Media` from `GrandParentComponent`
class ParentComponent(GrandParentComponent):
Media = None # type: ignore[assignment]
class ChildComponent(ParentComponent):
class Media:
css = "child.css"
js = "child.js"
rendered = ChildComponent.render()
assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<script src="child.js"></script>', rendered)
assert "grandparent.css" not in rendered
assert "grandparent.js" not in rendered
assert str(ChildComponent.media) == (
'<link href="child.css" media="all" rel="stylesheet">\n<script src="child.js"></script>'
)
def test_media_in_parent_and_grandparent(self):
class GrandParentComponent(Component):
template: types.django_html = """
@ -1143,8 +1318,9 @@ class TestSubclassingMedia:
css = "parent1.css"
js = "parent1.js"
# `pass` means that we inherit `Media` from `GrandParent3Component` and `GrandParent4Component`
class Parent2Component(GrandParent3Component, GrandParent4Component):
Media = None # type: ignore[assignment]
pass
class ChildComponent(Parent1Component, Parent2Component):
template: types.django_html = """
@ -1180,6 +1356,69 @@ class TestSubclassingMedia:
'<script src="grandparent1.js"></script>'
)
# Check that setting `Media = None` on a child class means that we will NOT inherit `Media` from the parent class
def test_media_in_multiple_bases__inheritance_off(self):
class GrandParent1Component(Component):
class Media:
css = "grandparent1.css"
js = "grandparent1.js"
class GrandParent2Component(Component):
pass
# NOTE: The bases don't even have to be Component classes,
# as long as they have the nested `Media` class.
class GrandParent3Component:
# NOTE: When we don't subclass `Component`, we have to correctly format the `Media` class
class Media:
css = {"all": ["grandparent3.css"]}
js = ["grandparent3.js"]
class GrandParent4Component:
pass
class Parent1Component(GrandParent1Component, GrandParent2Component):
class Media:
css = "parent1.css"
js = "parent1.js"
# `None` means that we will NOT inherit `Media` from `GrandParent3Component` and `GrandParent4Component`
class Parent2Component(GrandParent3Component, GrandParent4Component):
Media = None # type: ignore[assignment]
class ChildComponent(Parent1Component, Parent2Component):
template: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
"""
class Media:
css = "child.css"
js = "child.js"
rendered = ChildComponent.render()
assertInHTML('<link href="child.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<link href="parent1.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<link href="grandparent1.css" media="all" rel="stylesheet">', rendered)
assertInHTML('<script src="child.js"></script>', rendered)
assertInHTML('<script src="parent1.js"></script>', rendered)
assertInHTML('<script src="grandparent1.js"></script>', rendered)
assert "grandparent3.css" not in rendered
assert "grandparent3.js" not in rendered
assert str(ChildComponent.media) == (
'<link href="child.css" media="all" rel="stylesheet">\n'
'<link href="parent1.css" media="all" rel="stylesheet">\n'
'<link href="grandparent1.css" media="all" rel="stylesheet">\n'
'<script src="child.js"></script>\n'
'<script src="parent1.js"></script>\n'
'<script src="grandparent1.js"></script>'
)
def test_extend_false_in_child(self):
class Parent1Component(Component):
template: types.django_html = """
@ -1214,8 +1453,7 @@ class TestSubclassingMedia:
assertInHTML('<script src="child.js"></script>', rendered)
assert str(ChildComponent.media) == (
'<link href="child.css" media="all" rel="stylesheet">\n'
'<script src="child.js"></script>'
'<link href="child.css" media="all" rel="stylesheet">\n<script src="child.js"></script>'
)
def test_extend_false_in_parent(self):