refactor: rename template_name to template_file (#878)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Juro Oravec 2025-01-01 17:06:14 +01:00 committed by GitHub
parent b99e32e9d5
commit d94a459c8d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 251 additions and 138 deletions

View file

@ -158,14 +158,32 @@ class ComponentVars(NamedTuple):
"""
# Descriptor to pass getting/setting of `template_name` onto `template_file`
class ComponentTemplateNameDescriptor:
def __get__(self, instance: Optional["Component"], cls: Type["Component"]) -> Any:
obj = instance if instance is not None else cls
return obj.template_file # type: ignore[attr-defined]
def __set__(self, instance_or_cls: Union["Component", Type["Component"]], value: Any) -> None:
cls = instance_or_cls if isinstance(instance_or_cls, type) else instance_or_cls.__class__
cls.template_file = value
class ComponentMeta(ComponentMediaMeta):
pass
def __new__(mcs, name: Any, bases: Tuple, attrs: Dict) -> Any:
# If user set `template_name` on the class, we instead set it to `template_file`,
# because we want `template_name` to be the descriptor that proxies to `template_file`.
if "template_name" in attrs:
attrs["template_file"] = attrs.pop("template_name")
attrs["template_name"] = ComponentTemplateNameDescriptor()
return super().__new__(mcs, name, bases, attrs)
# NOTE: We use metaclass to automatically define the HTTP methods as defined
# in `View.http_method_names`.
class ComponentViewMeta(type):
def __new__(cls, name: str, bases: Any, dct: Dict) -> Any:
def __new__(mcs, name: str, bases: Any, dct: Dict) -> Any:
# Default implementation shared by all HTTP methods
def create_handler(method: str) -> Callable:
def handler(self, request: HttpRequest, *args: Any, **kwargs: Any): # type: ignore[no-untyped-def]
@ -179,7 +197,7 @@ class ComponentViewMeta(type):
if method_name not in dct:
dct[method_name] = create_handler(method_name)
return super().__new__(cls, name, bases, dct)
return super().__new__(mcs, name, bases, dct)
class ComponentView(View, metaclass=ComponentViewMeta):
@ -205,14 +223,14 @@ class Component(
# PUBLIC API (Configurable by users)
# #####################################
template_name: Optional[str] = None
template_file: Optional[str] = None
"""
Filepath to the Django template associated with this component.
The filepath must be relative to either the file where the component class was defined,
or one of the roots of `STATIFILES_DIRS`.
Only one of [`template_name`](../api#django_components.Component.template_name),
Only one of [`template_file`](../api#django_components.Component.template_file),
[`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
@ -221,13 +239,27 @@ class Component(
```py
class MyComponent(Component):
template_name = "path/to/template.html"
template_file = "path/to/template.html"
def get_context_data(self):
return {"name": "World"}
```
"""
# NOTE: This attribute is managed by `ComponentTemplateNameDescriptor` that's set in the metaclass.
# But we still define it here for documenting and type hinting.
template_name: Optional[str]
"""
Alias for [`template_file`](../api#django_components.Component.template_file).
For historical reasons, django-components used `template_name` to align with Django's
[TemplateView](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.TemplateView).
`template_file` was introduced to align with `js/js_file` and `css/css_file`.
Setting and accessing this attribute is proxied to `template_file`.
"""
def get_template_name(self, context: Context) -> Optional[str]:
"""
Filepath to the Django template associated with this component.
@ -235,7 +267,7 @@ class Component(
The filepath must be relative to either the file where the component class was defined,
or one of the roots of `STATIFILES_DIRS`.
Only one of [`template_name`](../api#django_components.Component.template_name),
Only one of [`template_file`](../api#django_components.Component.template_file),
[`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
@ -246,7 +278,7 @@ class Component(
"""
Inlined Django template associated with this component. Can be a plain string or a Template instance.
Only one of [`template_name`](../api#django_components.Component.template_name),
Only one of [`template_file`](../api#django_components.Component.template_file),
[`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
@ -266,7 +298,7 @@ class Component(
"""
Inlined Django template associated with this component. Can be a plain string or a Template instance.
Only one of [`template_name`](../api#django_components.Component.template_name),
Only one of [`template_file`](../api#django_components.Component.template_file),
[`get_template_name`](../api#django_components.Component.get_template_name),
[`template`](../api#django_components.Component.template)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
@ -596,21 +628,21 @@ class Component(
return ctx.is_filled
# NOTE: When the template is taken from a file (AKA specified via `template_name`),
# NOTE: When the template is taken from a file (AKA specified via `template_file`),
# then we leverage Django's template caching. This means that the same instance
# of Template is reused. This is important to keep in mind, because the implication
# is that we should treat Templates AND their nodelists as IMMUTABLE.
def _get_template(self, context: Context) -> Template:
# Resolve template name
template_name = self.template_name
if self.template_name is not None:
template_file = self.template_file
if self.template_file is not None:
if self.get_template_name(context) is not None:
raise ImproperlyConfigured(
"Received non-null value from both 'template_name' and 'get_template_name' in"
"Received non-null value from both 'template_file' and 'get_template_name' in"
f" Component {type(self).__name__}. Only one of the two must be set."
)
else:
template_name = self.get_template_name(context)
template_file = self.get_template_name(context)
# Resolve template str
template_input = self.template
@ -625,14 +657,14 @@ class Component(
template_getter = getattr(self, "get_template_string", self.get_template)
template_input = template_getter(context)
if template_name is not None and template_input is not None:
if template_file is not None and template_input is not None:
raise ImproperlyConfigured(
f"Received both 'template_name' and 'template' in Component {type(self).__name__}."
f"Received both 'template_file' and 'template' in Component {type(self).__name__}."
" Only one of the two must be set."
)
if template_name is not None:
return get_template(template_name).template
if template_file is not None:
return get_template(template_file).template
elif template_input is not None:
# We got template string, so we convert it to Template
@ -644,7 +676,7 @@ class Component(
return template
raise ImproperlyConfigured(
f"Either 'template_name' or 'template' must be set for Component {type(self).__name__}."
f"Either 'template_file' or 'template' must be set for Component {type(self).__name__}."
)
def inject(self, key: str, default: Optional[Any] = None) -> Any:

View file

@ -18,7 +18,7 @@ if TYPE_CHECKING:
# These are all the attributes that are handled by ComponentMedia and lazily-resolved
COMP_MEDIA_LAZY_ATTRS = ("media", "template", "template_name", "js", "js_file", "css", "css_file")
COMP_MEDIA_LAZY_ATTRS = ("media", "template", "template_file", "js", "js_file", "css", "css_file")
ComponentMediaInputPath = Union[
@ -176,7 +176,7 @@ class ComponentMedia:
media: Optional[MediaCls] = None
media_class: Type[MediaCls] = MediaCls
template: Optional[str] = None
template_name: Optional[str] = None
template_file: Optional[str] = None
js: Optional[str] = None
js_file: Optional[str] = None
css: Optional[str] = None
@ -185,9 +185,9 @@ class ComponentMedia:
# This metaclass is all about one thing - lazily resolving the media files.
#
# All the CSS/JS/HTML associated with a component - e.g. the `js`, `js_file`, `template_name` or `Media` class,
# All the CSS/JS/HTML associated with a component - e.g. the `js`, `js_file`, `template_file` or `Media` class,
# are all class attributes. And some of these attributes need to be resolved, e.g. to find the files
# that `js_file`, `css_file` and `template_name` point to.
# that `js_file`, `css_file` and `template_file` point to.
#
# Some of the resolutions we need to do is:
# - Component's HTML/JS/CSS files can be defined as relative to the component class file. So for each file,
@ -240,7 +240,14 @@ class ComponentMediaMeta(type):
" already resolved. This may lead to unexpected behavior."
)
super().__setattr__(name, value)
# NOTE: When a metaclass specifies a `__setattr__` method, this overrides the normal behavior of
# setting an attribute on the class with Descriptors. So we need to call the normal behavior explicitly.
# NOTE 2: `__dict__` is used to access the class attributes directly, without triggering the descriptors.
desc = cls.__dict__.get(name, None)
if hasattr(desc, "__set__"):
desc.__set__(cls, value)
else:
super().__setattr__(name, value)
# This sets up the lazy resolution of the media attributes.
@ -254,7 +261,7 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
media=attrs.get("media", None),
media_class=attrs.get("media_class", None),
template=attrs.get("template", None),
template_name=attrs.get("template_name", 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),
@ -292,8 +299,8 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
continue
else:
return value
if attr in ("template", "template_name"):
if check_pair_empty("template", "template_name"):
if attr in ("template", "template_file"):
if check_pair_empty("template", "template_file"):
continue
else:
return value
@ -544,7 +551,7 @@ def _resolve_component_relative_files(
# HTML/JS/CSS files, just skip.
will_resolve_files = False
if (
getattr(comp_media, "template_name", None)
getattr(comp_media, "template_file", None)
or getattr(comp_media, "js_file", None)
or getattr(comp_media, "css_file", None)
):
@ -609,8 +616,8 @@ def _resolve_component_relative_files(
return filepath
# Check if template name is a local file or not
if getattr(comp_media, "template_name", None):
comp_media.template_name = resolve_media_file(comp_media.template_name)
if getattr(comp_media, "template_file", None):
comp_media.template_file = resolve_media_file(comp_media.template_file)
if getattr(comp_media, "js_file", None):
comp_media.js_file = resolve_media_file(comp_media.js_file)
if getattr(comp_media, "css_file", None):

View file

@ -197,7 +197,7 @@ class Command(BaseCommand):
@register("{name}")
class {name.capitalize()}(Component):
template_name = "{name}/{template_filename}"
template_file = "{name}/{template_filename}"
def get_context_data(self, value):
return {{