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

@ -4,6 +4,12 @@
⚠️ Major release ⚠️ - Please test thoroughly before / after upgrading.
This is the biggest step towards v1. While this version introduces
many small API changes, we don't expect to make further changes to
the affected parts before v1.
For more details see [#433](https://github.com/django-components/django-components/issues/433).
Summary:
- Overhauled typing system
@ -220,6 +226,38 @@ Summary:
return dynamic_template.render(context)
```
- Subclassing of components with `None` values has changed:
Previously, when a child component's template / JS / CSS attributes were set to `None`, the child component still inherited the parent's template / JS / CSS.
Now, the child component will not inherit the parent's template / JS / CSS if it sets the attribute to `None`.
Before:
```py
class Parent(Component):
template = "parent.html"
class Child(Parent):
template = None
# Child still inherited parent's template
assert Child.template == Parent.template
```
After:
```py
class Parent(Component):
template = "parent.html"
class Child(Parent):
template = None
# Child does not inherit parent's template
assert Child.template is None
```
- The `Component.Url` class was merged with `Component.View`.
Instead of `Component.Url.public`, use `Component.View.public`.

View file

@ -4,7 +4,7 @@ In such cases, you can extract shared behavior into a standalone component class
When subclassing a component, there's a couple of things to keep in mind:
### Template, JS, and CSS inheritance
## Template, JS, and CSS inheritance
When it comes to the pairs:
@ -52,7 +52,7 @@ class CustomCard(BaseCard):
"""
```
### Media inheritance
## Media inheritance
The [`Component.Media`](../../reference/api.md#django_components.Component.Media) nested class follows Django's media inheritance rules:
@ -83,7 +83,35 @@ class SimpleModal(BaseModal):
js = ["simple_modal.js"] # Only this JS will be included
```
### Regular Python inheritance
## Opt out of inheritance
For the following media attributes, when you don't want to inherit from the parent,
but you also don't need to set the template / JS / CSS to any specific value,
you can set these attributes to `None`.
- [`template`](../../reference/api.md#django_components.Component.template) / [`template_file`](../../reference/api.md#django_components.Component.template_file)
- [`js`](../../reference/api.md#django_components.Component.js) / [`js_file`](../../reference/api.md#django_components.Component.js_file)
- [`css`](../../reference/api.md#django_components.Component.css) / [`css_file`](../../reference/api.md#django_components.Component.css_file)
- [`Media`](../../reference/api.md#django_components.Component.Media) class
For example:
```djc_py
class BaseForm(Component):
template = "..."
css = "..."
js = "..."
class Media:
js = ["form.js"]
# Use parent's template and CSS, but no JS
class ContactForm(BaseForm):
js = None
Media = None
```
## Regular Python inheritance
All other attributes and methods (including the [`Component.View`](../../reference/api.md#django_components.ComponentView) class and its methods) follow standard Python inheritance rules.

View file

@ -18,6 +18,7 @@ from typing import (
Sequence,
Tuple,
Type,
TypeVar,
Union,
cast,
)
@ -27,6 +28,7 @@ from django.contrib.staticfiles import finders
from django.core.exceptions import ImproperlyConfigured
from django.forms.widgets import Media as MediaCls
from django.utils.safestring import SafeData
from typing_extensions import TypeGuard
from django_components.template import load_component_template
from django_components.util.loader import get_component_dirs, resolve_file
@ -37,10 +39,28 @@ if TYPE_CHECKING:
from django_components.component import Component
T = TypeVar("T")
# These are all the attributes that are handled by ComponentMedia and lazily-resolved
COMP_MEDIA_LAZY_ATTRS = ("media", "template", "template_file", "js", "js_file", "css", "css_file")
# Sentinel value to indicate that a media attribute is not set.
# We use this to differntiate between setting template to `None` and not setting it at all.
# If not set, we will use the template from the parent component.
# If set to `None`, then this component has no template.
class Unset:
def __bool__(self) -> bool:
return False
def __repr__(self) -> str:
return "<UNSET>"
UNSET = Unset()
ComponentMediaInputPath = Union[
str,
bytes,
@ -240,20 +260,36 @@ class ComponentMedia:
comp_cls: Type["Component"]
resolved: bool = False
resolved_relative_files: bool = False
Media: Optional[Type[ComponentMediaInput]] = None
template: Optional[str] = None
template_file: Optional[str] = None
js: Optional[str] = None
js_file: Optional[str] = None
css: Optional[str] = None
css_file: Optional[str] = None
Media: Union[Type[ComponentMediaInput], Unset, None] = UNSET
template: Union[str, Unset, None] = UNSET
template_file: Union[str, Unset, None] = UNSET
js: Union[str, Unset, None] = UNSET
js_file: Union[str, Unset, None] = UNSET
css: Union[str, Unset, None] = UNSET
css_file: Union[str, Unset, None] = UNSET
def __post_init__(self) -> None:
for inlined_attr in ("template", "js", "css"):
file_attr = f"{inlined_attr}_file"
if getattr(self, inlined_attr) is not None and getattr(self, file_attr) is not None:
inlined_val = getattr(self, inlined_attr)
file_val = getattr(self, file_attr)
# NOTE: We raise if Component class received both inlined and file values:
# ```py
# class MyComp(Component):
# js = "..."
# js_file = "..."
# ```
#
# But both `None` are allowed:
# ```py
# class MyComp(Component):
# js = None
# js_file = None
# ```
if (inlined_val is not UNSET and file_val is not UNSET) and not (inlined_val is None and file_val is None):
raise ImproperlyConfigured(
f"Received non-null value from both '{inlined_attr}' and '{file_attr}' in"
f"Received non-empty value from both '{inlined_attr}' and '{file_attr}' in"
f" Component {self.comp_cls.__name__}. Only one of the two must be set."
)
# Make a copy of the original state, so we can reset it in tests
@ -340,13 +376,13 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
resolved=False,
# NOTE: We take the values from `attrs` so we consider only the values that were set on THIS class,
# and not the values that were inherited from the parent classes.
Media=attrs.get("Media", None),
template=attrs.get("template", None),
template_file=attrs.get("template_file", None),
js=attrs.get("js", None),
js_file=attrs.get("js_file", None),
css=attrs.get("css", None),
css_file=attrs.get("css_file", None),
Media=attrs.get("Media", UNSET),
template=attrs.get("template", UNSET),
template_file=attrs.get("template_file", UNSET),
js=attrs.get("js", UNSET),
js_file=attrs.get("js_file", UNSET),
css=attrs.get("css", UNSET),
css_file=attrs.get("css_file", UNSET),
)
def get_comp_media_attr(attr: str) -> Any:
@ -383,34 +419,34 @@ def _get_comp_cls_attr(comp_cls: Type["Component"], attr: str) -> Any:
continue
if not comp_media.resolved:
_resolve_media(base, comp_media)
value = getattr(comp_media, attr, None)
# NOTE: We differentiate between `None` and `UNSET`, so that users can set `None` to
# override parent class's value and set it to `None`.
value = getattr(comp_media, attr, UNSET)
# For each of the pairs of inlined_content + file (e.g. `js` + `js_file`), if at least one of the two
# is defined, we interpret it such that this (sub)class has overridden what was set by the parent class(es),
# and we won't search further up the MRO.
def check_pair_empty(inline_attr: str, file_attr: str) -> bool:
inline_attr_empty = getattr(comp_media, inline_attr, None) is None
file_attr_empty = getattr(comp_media, file_attr, None) is None
return inline_attr_empty and file_attr_empty
def resolve_pair(inline_attr: str, file_attr: str) -> Any:
inline_attr_empty = getattr(comp_media, inline_attr, UNSET) is UNSET
file_attr_empty = getattr(comp_media, file_attr, UNSET) is UNSET
if attr in ("js", "js_file"):
if check_pair_empty("js", "js_file"):
continue
else:
return value
if attr in ("css", "css_file"):
if check_pair_empty("css", "css_file"):
continue
else:
return value
if attr in ("template", "template_file"):
if check_pair_empty("template", "template_file"):
continue
is_pair_empty = inline_attr_empty and file_attr_empty
if is_pair_empty:
return UNSET
else:
return value
# For the other attributes, simply search for the closest non-null
if value is not None:
if attr in ("js", "js_file"):
value = resolve_pair("js", "js_file")
elif attr in ("css", "css_file"):
value = resolve_pair("css", "css_file")
elif attr in ("template", "template_file"):
value = resolve_pair("template", "template_file")
if value is UNSET:
continue
else:
return value
return None
@ -452,8 +488,20 @@ def _get_comp_cls_media(comp_cls: Type["Component"]) -> Any:
continue
# Prepare base classes
media_input = getattr(curr_cls, "Media", None)
media_extend = getattr(media_input, "extend", True)
# NOTE: If the `Component.Media` class is explicitly set to `None`, then we should not inherit
# from any parent classes.
# ```py
# class MyComponent(Component):
# Media = None
# ```
# But if the `Component.Media` class is NOT set, then we inherit from the parent classes.
# ```py
# class MyComponent(Component):
# pass
# ```
media_input = getattr(curr_cls, "Media", UNSET)
default_extend = True if media_input is not None else False
media_extend = getattr(media_input, "extend", default_extend)
# This ensures the same behavior as Django's Media class, where:
# - If `Media.extend == True`, then the media files are inherited from the parent classes.
@ -746,13 +794,9 @@ def _resolve_component_relative_files(
# First check if we even need to resolve anything. If the class doesn't define any
# HTML/JS/CSS files, just skip.
will_resolve_files = False
if (
getattr(comp_media, "template_file", None)
or getattr(comp_media, "js_file", None)
or getattr(comp_media, "css_file", None)
):
if is_set(comp_media.template_file) or is_set(comp_media.js_file) or is_set(comp_media.css_file):
will_resolve_files = True
elif not will_resolve_files and getattr(comp_media, "Media", None):
elif not will_resolve_files and is_set(comp_media.Media):
if getattr(comp_media.Media, "css", None) or getattr(comp_media.Media, "js", None):
will_resolve_files = True
@ -831,14 +875,14 @@ def _resolve_component_relative_files(
return resolved_filepaths
# Check if template name is a local file or not
if getattr(comp_media, "template_file", None):
if is_set(comp_media.template_file):
comp_media.template_file = resolve_relative_media_file(comp_media.template_file, False)[0]
if getattr(comp_media, "js_file", None):
if is_set(comp_media.js_file):
comp_media.js_file = resolve_relative_media_file(comp_media.js_file, False)[0]
if getattr(comp_media, "css_file", None):
if is_set(comp_media.css_file):
comp_media.css_file = resolve_relative_media_file(comp_media.css_file, False)[0]
if hasattr(comp_media, "Media") and comp_media.Media:
if is_set(comp_media.Media):
_map_media_filepaths(
comp_media.Media,
# Media files can be defined as a glob patterns that match multiple files.
@ -933,7 +977,7 @@ def _get_asset(
file_attr: str,
comp_dirs: List[Path],
type: Literal["template", "static"],
) -> Optional[str]:
) -> Union[str, Unset, None]:
"""
In case of Component's JS or CSS, one can either define that as "inlined" or as a file.
@ -957,24 +1001,73 @@ def _get_asset(
These are mutually exclusive, so only one of the two can be set at class creation.
"""
asset_content = getattr(comp_media, inlined_attr, None)
asset_file = getattr(comp_media, file_attr, None)
asset_content = getattr(comp_media, inlined_attr, UNSET)
asset_file = getattr(comp_media, file_attr, UNSET)
# No inlined content, nor file name
if asset_content is None and asset_file is None:
# ```py
# class MyComp(Component):
# pass
# ```
if asset_content is UNSET and asset_file is UNSET:
return UNSET
# Either file or content attr was set to `None`
# ```py
# class MyComp(Component):
# js_file = None
# ```
# or
# ```py
# class MyComp(Component):
# js = None
# ```
# or
# ```py
# class MyComp(Component):
# js = None
# js_file = None
# ```
if (asset_content in (UNSET, None) and asset_file is None) or (
asset_content is None and asset_file in (UNSET, None)
):
return None
if asset_content is not None and asset_file is not None:
# Received both inlined content and file name
# ```py
# class MyComp(Component):
# js = "..."
# js_file = "..."
# ```
#
# Or received file name / content AND explicit `None`
# ```py
# class MyComp(Component):
# js = "..."
# js_file = None
# ```
# or
# ```py
# class MyComp(Component):
# js = None
# js_file = "..."
# ```
if asset_content is not UNSET and asset_file is not UNSET:
raise ValueError(
f"Received both '{inlined_attr}' and '{file_attr}' in Component {comp_cls.__qualname__}."
" Only one of the two must be set."
)
# At this point we can tell that only EITHER `asset_content` OR `asset_file` is set.
# If the content was inlined into the component (e.g. `Component.template = "..."`)
# then there's nothing to resolve. Return as is.
if asset_content is not None:
if asset_content is not UNSET:
return asset_content
if asset_file is None:
return None
# The rest of the code assumes that we were given only a file name
asset_file = cast(str, asset_file)
@ -1004,3 +1097,7 @@ def _get_asset(
# NOTE: `on_template_preprocess()` is already applied inside `load_component_template()`
return asset_content
def is_set(value: Union[T, Unset, None]) -> TypeGuard[T]:
return value is not None and value is not UNSET

View file

@ -381,7 +381,7 @@ def cache_component_template_file(component_cls: Type["Component"]) -> None:
return
# NOTE: Avoids circular import
from django_components.component_media import ComponentMedia, _resolve_component_relative_files
from django_components.component_media import ComponentMedia, Unset, _resolve_component_relative_files, is_set
# If we access the `Component.template_file` attribute, then this triggers media resolution if it was not done yet.
# The problem is that this also causes the loading of the Template, if Component has defined `template_file`.
@ -395,7 +395,7 @@ def cache_component_template_file(component_cls: Type["Component"]) -> None:
# directly, thus avoiding the triggering of the Template loading.
comp_media: ComponentMedia = component_cls._component_media # type: ignore[attr-defined]
if comp_media.resolved and comp_media.resolved_relative_files:
template_file = component_cls.template_file
template_file: Union[str, Unset, None] = component_cls.template_file
else:
# NOTE: This block of code is based on `_resolve_media()` in `component_media.py`
if not comp_media.resolved_relative_files:
@ -404,7 +404,7 @@ def cache_component_template_file(component_cls: Type["Component"]) -> None:
template_file = comp_media.template_file
if template_file is None:
if not is_set(template_file):
return
if template_file not in component_template_file_cache:

View file

@ -17,6 +17,7 @@ from django_components.component import ALL_COMPONENTS, Component, component_nod
from django_components.component_media import ComponentMedia
from django_components.component_registry import ALL_REGISTRIES, ComponentRegistry
from django_components.extension import extensions
from django_components.perfutil.provide import provide_cache
from django_components.template import _reset_component_template_file_cache, loading_components
# NOTE: `ReferenceType` is NOT a generic pre-3.9
@ -472,6 +473,9 @@ def _clear_djc_global_state(
if component_media_cache:
component_media_cache.clear()
if provide_cache:
provide_cache.clear()
# Remove cached Node subclasses
component_node_subclasses_by_name.clear()

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):