Merge branch 'master' into add-type-hints

This commit is contained in:
Dylan Castillo 2024-03-04 21:45:37 +01:00 committed by GitHub
commit 25fe39c6d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 649 additions and 1068 deletions

View file

@ -11,4 +11,4 @@ repos:
rev: 7.0.0
hooks:
- id: flake8
additional_dependencies: [flake8-pyproject]

View file

@ -7,7 +7,7 @@ A way to create simple reusable template components in Django.
It lets you create "template components", that contains both the template, the Javascript and the CSS needed to generate the front end code you need for a modern app. Components look like this:
```htmldjango
{% component "calendar" date="2015-06-19" %}
{% component "calendar" date="2015-06-19" %}{% endcomponent %}
```
And this is what gets rendered (plus the CSS and Javascript you've specified):
@ -20,17 +20,21 @@ Read on to learn about the details!
## Release notes
*Version 0.34* adds components as views, which allows you to handle requests and render responses from within a component. See the [documentation](#components-as-views) for more details.
🚨📢 **Version 0.5** CHANGES THE SYNTAX for components. `component_block` is now `component`, and `component` blocks need an ending `endcomponent` tag. The new `python manage.py upgradecomponent` command can be used to upgrade a directory (use --path argument to point to each dir) of components to the new syntax automatically.
*Version 0.28* introduces 'implicit' slot filling and the `default` option for `slot` tags.
This change is done to simplify the API in anticipation of a 1.0 release of django_components. After 1.0 we intend to be stricter with big changes like this in point releases.
*Version 0.27* adds a second installable app: *django_components.safer_staticfiles*. It provides the same behavior as *django.contrib.staticfiles* but with extra security guarantees (more info below in Security Notes).
**Version 0.34** adds components as views, which allows you to handle requests and render responses from within a component. See the [documentation](#components-as-views) for more details.
*Version 0.26* changes the syntax for `{% slot %}` tags. From now on, we separate defining a slot (`{% slot %}`) from filling a slot with content (`{% fill %}`). This means you will likely need to change a lot of slot tags to fill. We understand this is annoying, but it's the only way we can get support for nested slots that fill in other slots, which is a very nice feature to have access to. Hoping that this will feel worth it!
**Version 0.28** introduces 'implicit' slot filling and the `default` option for `slot` tags.
*Version 0.22* starts autoimporting all files inside components subdirectores, to simplify setup. An existing project might start to get AlreadyRegistered-errors because of this. To solve this, either remove your custom loading of components, or set "autodiscover": False in settings.COMPONENTS.
**Version 0.27** adds a second installable app: *django_components.safer_staticfiles*. It provides the same behavior as *django.contrib.staticfiles* but with extra security guarantees (more info below in Security Notes).
*Version 0.17* renames `Component.context` and `Component.template` to `get_context_data` and `get_template_name`. The old methods still work, but emit a deprecation warning. This change was done to sync naming with Django's class based views, and make using django-components more familiar to Django users. `Component.context` and `Component.template` will be removed when version 1.0 is released.
**Version 0.26** changes the syntax for `{% slot %}` tags. From now on, we separate defining a slot (`{% slot %}`) from filling a slot with content (`{% fill %}`). This means you will likely need to change a lot of slot tags to fill. We understand this is annoying, but it's the only way we can get support for nested slots that fill in other slots, which is a very nice featuPpre to have access to. Hoping that this will feel worth it!
**Version 0.22** starts autoimporting all files inside components subdirectores, to simplify setup. An existing project might start to get AlreadyRegistered-errors because of this. To solve this, either remove your custom loading of components, or set "autodiscover": False in settings.COMPONENTS.
**Version 0.17** renames `Component.context` and `Component.template` to `get_context_data` and `get_template_name`. The old methods still work, but emit a deprecation warning. This change was done to sync naming with Django's class based views, and make using django-components more familiar to Django users. `Component.context` and `Component.template` will be removed when version 1.0 is released.
## Security notes 🚨
@ -220,7 +224,7 @@ First load the `component_tags` tag library, then use the `component_[js/css]_de
{% component_css_dependencies %}
</head>
<body>
{% component "calendar" date="2015-06-19" %}
{% component "calendar" date="2015-06-19" %}{% endcomponent %}
{% component_js_dependencies %}
</body>
<html>
@ -300,7 +304,7 @@ This mechanism makes components more reusable and composable.
In the example below we introduce two block tags that work hand in hand to make this work. These are...
- `{% slot <name> %}`/`{% endslot %}`: Declares a new slot in the component template.
- `{% fill <name> %}`/`{% endfill %}`: (Used inside a `component_block` tag pair.) Fills a declared slot with the specified content.
- `{% fill <name> %}`/`{% endfill %}`: (Used inside a `component` tag pair.) Fills a declared slot with the specified content.
Let's update our calendar component to support more customization. We'll add `slot` tag pairs to its template, _calendar.html_.
@ -318,9 +322,9 @@ Let's update our calendar component to support more customization. We'll add `sl
When using the component, you specify which slots you want to fill and where you want to use the defaults from the template. It looks like this:
```htmldjango
{% component_block "calendar" date="2020-06-06" %}
{% component "calendar" date="2020-06-06" %}
{% fill "body" %}Can you believe it's already <span>{{ date }}</span>??{% endfill %}
{% endcomponent_block %}
{% endcomponent %}
```
Since the header block is unspecified, it's taken from the base template. If you put this in a template, and pass in `date=2020-06-06`, this is what gets rendered:
@ -338,7 +342,7 @@ Since the header block is unspecified, it's taken from the base template. If you
As you can see, component slots lets you write reusable containers that you fill in when you use a component. This makes for highly reusable components that can be used in different circumstances.
It can become tedious to use `fill` tags everywhere, especially when you're using a component that declares only one slot. To make things easier, `slot` tags can be marked with an optional keyword: `default`. When added to the end of the tag (as shown below), this option lets you pass filling content directly in the body of a `component_block` tag pair without using a `fill` tag. Choose carefully, though: a component template may contain at most one slot that is marked as `default`. The `default` option can be combined with other slot options, e.g. `required`.
It can become tedious to use `fill` tags everywhere, especially when you're using a component that declares only one slot. To make things easier, `slot` tags can be marked with an optional keyword: `default`. When added to the end of the tag (as shown below), this option lets you pass filling content directly in the body of a `component` tag pair without using a `fill` tag. Choose carefully, though: a component template may contain at most one slot that is marked as `default`. The `default` option can be combined with other slot options, e.g. `required`.
Here's the same example as before, except with default slots and implicit filling.
@ -358,9 +362,9 @@ The template:
Including the component (notice how the `fill` tag is omitted):
```htmldjango
{% component_block "calendar" date="2020-06-06" %}
{% component "calendar" date="2020-06-06" %}
Can you believe it's already <span>{{ date }}</span>??
{% endcomponent_block %}
{% endcomponent %}
```
The rendered result (exactly the same as before):
@ -380,32 +384,32 @@ You may be tempted to combine implicit fills with explicit `fill` tags. This wil
```htmldjango
{# DON'T DO THIS #}
{% component_block "calendar" date="2020-06-06" %}
{% component "calendar" date="2020-06-06" %}
{% fill "header" %}Totally new header!{% endfill %}
Can you believe it's already <span>{{ date }}</span>??
{% endcomponent_block %}
{% endcomponent %}
```
By contrast, it is permitted to use `fill` tags in nested components, e.g.:
```htmldjango
{% component_block "calendar" date="2020-06-06" %}
{% component_block "beautiful-box" %}
{% component "calendar" date="2020-06-06" %}
{% component "beautiful-box" %}
{% fill "content" %} Can you believe it's already <span>{{ date }}</span>?? {% endfill %}
{% endcomponent_block %}
{% endcomponent_block %}
{% endcomponent %}
{% endcomponent %}
```
This is fine too:
```htmldjango
{% component_block "calendar" date="2020-06-06" %}
{% component "calendar" date="2020-06-06" %}
{% fill "header" %}
{% component_block "calendar-header" %}
{% component "calendar-header" %}
Super Special Calendar Header
{% endcomponent_block %}
{% endcomponent %}
{% endfill %}
{% endcomponent_block %}
{% endcomponent %}
```
### Components as views
@ -482,9 +486,9 @@ If you're planning on passing an HTML string, check Django's use of [`format_htm
Certain properties of a slot can be accessed from within a 'fill' context. They are provided as attributes on a user-defined alias of the targeted slot. For instance, let's say you're filling a slot called 'body'. To access properties of this slot, alias it using the 'as' keyword to a new name -- or keep the original name. With the new slot alias, you can call `<alias>.default` to insert the default content.
```htmldjango
{% component_block "calendar" date="2020-06-06" %}
{% component "calendar" date="2020-06-06" %}
{% fill "body" as "body" %}{{ body.default }}. Have a great day!{% endfill %}
{% endcomponent_block %}
{% endcomponent %}
```
Produces:
@ -617,10 +621,10 @@ COMPONENTS = {
## Component context and scope
By default, components can access context variables from the parent template, just like templates that are included with the `{% include %}` tag. Just like with `{% include %}`, if you don't want the component template to have access to the parent context, add `only` to the end of the `{% component %}` (or `{% component_block %}` tag):
By default, components can access context variables from the parent template, just like templates that are included with the `{% include %}` tag. Just like with `{% include %}`, if you don't want the component template to have access to the parent context, add `only` to the end of the `{% component %}` tag):
```htmldjango
{% component "calendar" date="2015-06-19" only %}
{% component "calendar" date="2015-06-19" only %}{% endcomponent %}
```
NOTE: `{% csrf_token %}` tags need access to the top-level context, and they will not function properly if they are rendered in a component that is called with the `only` modifier.

View file

@ -4,10 +4,7 @@ from django.template import Context, Template
from django.test import override_settings
from django_components import component
from django_components.middleware import (
CSS_DEPENDENCY_PLACEHOLDER,
JS_DEPENDENCY_PLACEHOLDER,
)
from django_components.middleware import CSS_DEPENDENCY_PLACEHOLDER, JS_DEPENDENCY_PLACEHOLDER
from tests.django_test_setup import * # NOQA
from tests.testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
from tests.testutils import create_and_process_template_response
@ -75,9 +72,7 @@ class RenderBenchmarks(SimpleTestCase):
component.registry.clear()
component.registry.register("test_component", SlottedComponent)
component.registry.register("inner_component", SimpleComponent)
component.registry.register(
"breadcrumb_component", BreadcrumbComponent
)
component.registry.register("breadcrumb_component", BreadcrumbComponent)
@staticmethod
def timed_loop(func, iterations=1000):
@ -91,22 +86,28 @@ class RenderBenchmarks(SimpleTestCase):
def test_render_time_for_small_component(self):
template = Template(
"{% load component_tags %}{% component_block 'test_component' %}"
"{% slot \"header\" %}{% component 'inner_component' variable='foo' %}{% endslot %}"
"{% endcomponent_block %}",
name="root",
"""
{% load component_tags %}
{% component 'test_component' %}
{% slot "header" %}
{% component 'inner_component' variable='foo' %}{% endcomponent %}
{% endslot %}
{% endcomponent %}
"""
)
print(
f"{self.timed_loop(lambda: template.render(Context({})))} ms per iteration"
)
print(f"{self.timed_loop(lambda: template.render(Context({})))} ms per iteration")
def test_middleware_time_with_dependency_for_small_page(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'test_component' %}{% slot \"header\" %}"
"{% component 'inner_component' variable='foo' %}{% endslot %}{% endcomponent_block %}",
name="root",
"""
{% load component_tags %}{% component_dependencies %}
{% component 'test_component' %}
{% slot "header" %}
{% component 'inner_component' variable='foo' %}{% endcomponent %}
{% endslot %}
{% endcomponent %}
"""
)
# Sanity tests
response_content = create_and_process_template_response(template)
@ -116,15 +117,9 @@ class RenderBenchmarks(SimpleTestCase):
self.assertIn("script.js", response_content)
without_middleware = self.timed_loop(
lambda: create_and_process_template_response(
template, use_middleware=False
)
)
with_middleware = self.timed_loop(
lambda: create_and_process_template_response(
template, use_middleware=True
)
lambda: create_and_process_template_response(template, use_middleware=False)
)
with_middleware = self.timed_loop(lambda: create_and_process_template_response(template, use_middleware=True))
print("Small page middleware test")
self.report_results(with_middleware, without_middleware)
@ -140,14 +135,10 @@ class RenderBenchmarks(SimpleTestCase):
self.assertIn("test.js", response_content)
without_middleware = self.timed_loop(
lambda: create_and_process_template_response(
template, {}, use_middleware=False
)
lambda: create_and_process_template_response(template, {}, use_middleware=False)
)
with_middleware = self.timed_loop(
lambda: create_and_process_template_response(
template, {}, use_middleware=True
)
lambda: create_and_process_template_response(template, {}, use_middleware=True)
)
print("Large page middleware test")
@ -156,15 +147,9 @@ class RenderBenchmarks(SimpleTestCase):
@staticmethod
def report_results(with_middleware, without_middleware):
print(f"Middleware active\t\t{with_middleware:.3f} ms per iteration")
print(
f"Middleware inactive\t{without_middleware:.3f} ms per iteration"
)
print(f"Middleware inactive\t{without_middleware:.3f} ms per iteration")
time_difference = with_middleware - without_middleware
if without_middleware > with_middleware:
print(
f"Decrease of {-100 * time_difference / with_middleware:.2f}%"
)
print(f"Decrease of {-100 * time_difference / with_middleware:.2f}%")
else:
print(
f"Increase of {100 * time_difference / without_middleware:.2f}%"
)
print(f"Increase of {100 * time_difference / without_middleware:.2f}%")

View file

@ -26,9 +26,7 @@ class AppSettings:
@property
def CONTEXT_BEHAVIOR(self):
raw_value = self.settings.setdefault(
"context_behavior", ContextBehavior.GLOBAL.value
)
raw_value = self.settings.setdefault("context_behavior", ContextBehavior.GLOBAL.value)
return self._validate_context_behavior(raw_value)
def _validate_context_behavior(self, raw_value):
@ -36,9 +34,7 @@ class AppSettings:
return ContextBehavior(raw_value)
except ValueError:
valid_values = [behavior.value for behavior in ContextBehavior]
raise ValueError(
f"Invalid context behavior: {raw_value}. Valid options are {valid_values}"
)
raise ValueError(f"Invalid context behavior: {raw_value}. Valid options are {valid_values}")
app_settings = AppSettings()

View file

@ -80,9 +80,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
self,
registered_name: Optional[str] = None,
outer_context: Optional[Context] = None,
fill_content: Union[
DefaultFillContent, Iterable[NamedFillContent]
] = (),
fill_content: Union[DefaultFillContent, Iterable[NamedFillContent]] = (),
):
self.registered_name: Optional[str] = registered_name
self.outer_context: Context = outer_context or Context()
@ -152,14 +150,10 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
if slots_data:
self._fill_slots(slots_data, escape_slots_content)
updated_filled_slots_context: FilledSlotsContext = (
self._process_template_and_update_filled_slot_context(
updated_filled_slots_context: FilledSlotsContext = self._process_template_and_update_filled_slot_context(
context, template
)
)
with context.update(
{FILLED_SLOTS_CONTENT_CONTEXT_KEY: updated_filled_slots_context}
):
with context.update({FILLED_SLOTS_CONTENT_CONTEXT_KEY: updated_filled_slots_context}):
return template.render(context)
def render_to_response(
@ -201,19 +195,14 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
named_fills_content = {}
else:
default_fill_content = None
named_fills_content = {
name: (nodelist, alias)
for name, nodelist, alias in self.fill_content
}
named_fills_content = {name: (nodelist, alias) for name, nodelist, alias in self.fill_content}
# If value is `None`, then slot is unfilled.
slot_name2fill_content: Dict[SlotName, Optional[FillContent]] = {}
default_slot_encountered: bool = False
required_slot_names: Set[str] = set()
for node in template.nodelist.get_nodes_by_type(
(SlotNode, IfSlotFilledConditionBranchNode) # type: ignore
):
for node in template.nodelist.get_nodes_by_type((SlotNode, IfSlotFilledConditionBranchNode)): # type: ignore
if isinstance(node, SlotNode):
# Give slot node knowledge of its parent template.
node.template = template
@ -225,9 +214,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
f"To fix, check template '{template.name}' "
f"of component '{self.registered_name}'."
)
content_data: Optional[FillContent] = (
None # `None` -> unfilled
)
content_data: Optional[FillContent] = None # `None` -> unfilled
if node.is_required:
required_slot_names.add(node.name)
if node.is_default:
@ -245,25 +232,19 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
elif isinstance(node, IfSlotFilledConditionBranchNode):
node.template = template
else:
raise RuntimeError(
f"Node of {type(node).__name__} does not require linking."
)
raise RuntimeError(f"Node of {type(node).__name__} does not require linking.")
# Check: Only component templates that include a 'default' slot
# can be invoked with implicit filling.
if default_fill_content and not default_slot_encountered:
raise TemplateSyntaxError(
f"Component '{self.registered_name}' passed default fill content "
f"Component '{self.registered_name}' passed default fill content '{default_fill_content}'"
f"(i.e. without explicit 'fill' tag), "
f"even though none of its slots is marked as 'default'."
)
unfilled_slots: Set[str] = set(
k for k, v in slot_name2fill_content.items() if v is None
)
unmatched_fills: Set[str] = (
named_fills_content.keys() - slot_name2fill_content.keys()
)
unfilled_slots: Set[str] = set(k for k, v in slot_name2fill_content.items() if v is None)
unmatched_fills: Set[str] = named_fills_content.keys() - slot_name2fill_content.keys()
# Check that 'required' slots are filled.
for slot_name in unfilled_slots:
@ -286,9 +267,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
# Higher values make matching stricter. This is probably preferable, as it
# reduces false positives.
for fill_name in unmatched_fills:
fuzzy_slot_name_matches = difflib.get_close_matches(
fill_name, unfilled_slots, n=1, cutoff=0.7
)
fuzzy_slot_name_matches = difflib.get_close_matches(fill_name, unfilled_slots, n=1, cutoff=0.7)
msg = (
f"Component '{self.registered_name}' passed fill "
f"that refers to undefined slot: '{fill_name}'."
@ -305,9 +284,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
if content_data # Slots whose content is None (i.e. unfilled) are dropped.
}
try:
prev_context: FilledSlotsContext = context[
FILLED_SLOTS_CONTENT_CONTEXT_KEY
]
prev_context: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
return prev_context.new_child(filled_slots_map)
except KeyError:
return ChainMap(filled_slots_map)

View file

@ -12,13 +12,8 @@ class ComponentRegistry(object):
def register(self, name=None, component=None):
existing_component = self._registry.get(name)
if (
existing_component
and existing_component.class_hash != component.class_hash
):
raise AlreadyRegistered(
'The component "%s" has already been registered' % name
)
if existing_component and existing_component.class_hash != component.class_hash:
raise AlreadyRegistered('The component "%s" has already been registered' % name)
self._registry[name] = component
def unregister(self, name):

View file

@ -9,9 +9,7 @@ class Command(BaseCommand):
help = "Creates a new component"
def add_arguments(self, parser):
parser.add_argument(
"name", type=str, help="The name of the component to create"
)
parser.add_argument("name", type=str, help="The name of the component to create")
parser.add_argument(
"--path",
type=str,
@ -71,9 +69,7 @@ class Command(BaseCommand):
elif base_dir:
component_path = os.path.join(base_dir, "components", name)
else:
raise CommandError(
"You must specify a path or set BASE_DIR in your django settings"
)
raise CommandError("You must specify a path or set BASE_DIR in your django settings")
if os.path.exists(component_path):
if force:
@ -84,11 +80,7 @@ class Command(BaseCommand):
)
)
else:
self.stdout.write(
self.style.WARNING(
f'The component "{name}" already exists. Overwriting...'
)
)
self.stdout.write(self.style.WARNING(f'The component "{name}" already exists. Overwriting...'))
else:
raise CommandError(
f'The component "{name}" already exists at {component_path}. Use --force to overwrite.'
@ -107,9 +99,7 @@ class Command(BaseCommand):
)
f.write(script_content.strip())
with open(
os.path.join(component_path, css_filename), "w"
) as f:
with open(os.path.join(component_path, css_filename), "w") as f:
style_content = dedent(
f"""
.component-{name} {{
@ -119,9 +109,7 @@ class Command(BaseCommand):
)
f.write(style_content.strip())
with open(
os.path.join(component_path, template_filename), "w"
) as f:
with open(os.path.join(component_path, template_filename), "w") as f:
template_content = dedent(
f"""
<div class="component-{name}">
@ -133,9 +121,7 @@ class Command(BaseCommand):
)
f.write(template_content.strip())
with open(
os.path.join(component_path, f"{name}.py"), "w"
) as f:
with open(os.path.join(component_path, f"{name}.py"), "w") as f:
py_content = dedent(
f"""
from django_components import component
@ -157,16 +143,8 @@ class Command(BaseCommand):
f.write(py_content.strip())
if verbose:
self.stdout.write(
self.style.SUCCESS(
f"Successfully created {name} component at {component_path}"
)
)
self.stdout.write(self.style.SUCCESS(f"Successfully created {name} component at {component_path}"))
else:
self.stdout.write(
self.style.SUCCESS(
f"Successfully created {name} component"
)
)
self.stdout.write(self.style.SUCCESS(f"Successfully created {name} component"))
else:
raise CommandError("You must specify a component name")

View file

@ -0,0 +1,65 @@
import os
import re
from pathlib import Path
from django.conf import settings
from django.core.management.base import BaseCommand
from django.template.engine import Engine
from django_components.template_loader import Loader
class Command(BaseCommand):
help = "Updates component and component_block tags to the new syntax"
def add_arguments(self, parser):
parser.add_argument("--path", type=str, help="Path to search for components")
def handle(self, *args, **options):
current_engine = Engine.get_default()
loader = Loader(current_engine)
dirs = loader.get_dirs()
if settings.BASE_DIR:
dirs.append(Path(settings.BASE_DIR) / "templates")
if options["path"]:
dirs = [options["path"]]
for dir_path in dirs:
self.stdout.write(f"Searching for components in {dir_path}...")
for root, _, files in os.walk(dir_path):
for file in files:
if file.endswith((".html", ".py")):
file_path = os.path.join(root, file)
with open(file_path, "r+", encoding="utf-8") as f:
content = f.read()
content_with_closed_components, step0_count = re.subn(
r'({%\s*component\s*"(\w+?)"(.*?)%})(?!.*?{%\s*endcomponent\s*%})',
r"\1{% endcomponent %}",
content,
flags=re.DOTALL,
)
updated_content, step1_count_opening = re.subn(
r'{%\s*component_block\s*"(\w+?)"\s*(.*?)%}',
r'{% component "\1" \2%}',
content_with_closed_components,
flags=re.DOTALL,
)
updated_content, step2_count_closing = re.subn(
r'{%\s*endcomponent_block\s*"(\w+?)"\s*%}',
r"{% endcomponent %}",
updated_content,
flags=re.DOTALL,
)
updated_content, step2_count_closing_no_name = re.subn(
r"{%\s*endcomponent_block\s*%}", r"{% endcomponent %}", updated_content, flags=re.DOTALL
)
total_updates = (
step0_count + step1_count_opening + step2_count_closing + step2_count_closing_no_name
)
if total_updates > 0:
f.seek(0)
f.write(updated_content)
f.truncate()
self.stdout.write(f"Updated {file_path}: {total_updates} changes made")

View file

@ -11,9 +11,7 @@ CSS_DEPENDENCY_PLACEHOLDER = '<link name="CSS_PLACEHOLDER">'
JS_DEPENDENCY_PLACEHOLDER = '<script name="JS_PLACEHOLDER"></script>'
SCRIPT_TAG_REGEX = re.compile("<script")
COMPONENT_COMMENT_REGEX = re.compile(
rb"<!-- _RENDERED (?P<name>[\w\-/]+?) -->"
)
COMPONENT_COMMENT_REGEX = re.compile(rb"<!-- _RENDERED (?P<name>[\w\-/]+?) -->")
PLACEHOLDER_REGEX = re.compile(
rb"<!-- _RENDERED (?P<name>[\w\-/]+?) -->"
rb'|<link name="CSS_PLACEHOLDER">'
@ -32,9 +30,7 @@ class ComponentDependencyMiddleware:
def __call__(self, request):
response = self.get_response(request)
if (
getattr(settings, "COMPONENTS", {}).get(
"RENDER_DEPENDENCIES", False
)
getattr(settings, "COMPONENTS", {}).get("RENDER_DEPENDENCIES", False)
and not isinstance(response, StreamingHttpResponse)
and response.get("Content-Type", "").startswith("text/html")
):
@ -43,23 +39,12 @@ class ComponentDependencyMiddleware:
def process_response_content(content):
component_names_seen = {
match.group("name")
for match in COMPONENT_COMMENT_REGEX.finditer(content)
}
all_components = [
registry.get(name.decode("utf-8"))("") for name in component_names_seen
]
component_names_seen = {match.group("name") for match in COMPONENT_COMMENT_REGEX.finditer(content)}
all_components = [registry.get(name.decode("utf-8"))("") for name in component_names_seen]
all_media = join_media(all_components)
js_dependencies = b"".join(
media.encode("utf-8") for media in all_media.render_js()
)
css_dependencies = b"".join(
media.encode("utf-8") for media in all_media.render_css()
)
return PLACEHOLDER_REGEX.sub(
DependencyReplacer(css_dependencies, js_dependencies), content
)
js_dependencies = b"".join(media.encode("utf-8") for media in all_media.render_js())
css_dependencies = b"".join(media.encode("utf-8") for media in all_media.render_css())
return PLACEHOLDER_REGEX.sub(DependencyReplacer(css_dependencies, js_dependencies), content)
def add_module_attribute_to_scripts(scripts):

View file

@ -12,9 +12,7 @@ class SaferStaticFilesConfig(StaticFilesConfig):
by the static file server.
"""
default = (
True # Ensure that _this_ app is registered, as opposed to parent cls.
)
default = True # Ensure that _this_ app is registered, as opposed to parent cls.
ignore_patterns = StaticFilesConfig.ignore_patterns + [
"*.py",
"*.html",

View file

@ -9,13 +9,7 @@ else:
import django.template
from django.conf import settings
from django.template import Context, Template
from django.template.base import (
FilterExpression,
Node,
NodeList,
TextNode,
TokenType,
)
from django.template.base import FilterExpression, Node, NodeList, TextNode, TokenType
from django.template.defaulttags import CommentNode
from django.template.exceptions import TemplateSyntaxError
from django.template.library import parse_bits
@ -24,10 +18,7 @@ from django.utils.safestring import mark_safe
from django_components.app_settings import app_settings
from django_components.component_registry import ComponentRegistry
from django_components.component_registry import registry as component_registry
from django_components.middleware import (
CSS_DEPENDENCY_PLACEHOLDER,
JS_DEPENDENCY_PLACEHOLDER,
)
from django_components.middleware import CSS_DEPENDENCY_PLACEHOLDER, JS_DEPENDENCY_PLACEHOLDER
if TYPE_CHECKING:
from django_components.component import Component
@ -88,16 +79,8 @@ def component_dependencies_tag(preload=""):
if is_dependency_middleware_active():
preloaded_dependencies = []
for component in get_components_from_preload_str(preload):
preloaded_dependencies.append(
RENDERED_COMMENT_TEMPLATE.format(
name=component.registered_name
)
)
return mark_safe(
"\n".join(preloaded_dependencies)
+ CSS_DEPENDENCY_PLACEHOLDER
+ JS_DEPENDENCY_PLACEHOLDER
)
preloaded_dependencies.append(RENDERED_COMMENT_TEMPLATE.format(name=component.registered_name))
return mark_safe("\n".join(preloaded_dependencies) + CSS_DEPENDENCY_PLACEHOLDER + JS_DEPENDENCY_PLACEHOLDER)
else:
rendered_dependencies = []
for component in get_components_from_registry(component_registry):
@ -113,14 +96,8 @@ def component_css_dependencies_tag(preload=""):
if is_dependency_middleware_active():
preloaded_dependencies = []
for component in get_components_from_preload_str(preload):
preloaded_dependencies.append(
RENDERED_COMMENT_TEMPLATE.format(
name=component.registered_name
)
)
return mark_safe(
"\n".join(preloaded_dependencies) + CSS_DEPENDENCY_PLACEHOLDER
)
preloaded_dependencies.append(RENDERED_COMMENT_TEMPLATE.format(name=component.registered_name))
return mark_safe("\n".join(preloaded_dependencies) + CSS_DEPENDENCY_PLACEHOLDER)
else:
rendered_dependencies = []
for component in get_components_from_registry(component_registry):
@ -136,14 +113,8 @@ def component_js_dependencies_tag(preload=""):
if is_dependency_middleware_active():
preloaded_dependencies = []
for component in get_components_from_preload_str(preload):
preloaded_dependencies.append(
RENDERED_COMMENT_TEMPLATE.format(
name=component.registered_name
)
)
return mark_safe(
"\n".join(preloaded_dependencies) + JS_DEPENDENCY_PLACEHOLDER
)
preloaded_dependencies.append(RENDERED_COMMENT_TEMPLATE.format(name=component.registered_name))
return mark_safe("\n".join(preloaded_dependencies) + JS_DEPENDENCY_PLACEHOLDER)
else:
rendered_dependencies = []
for component in get_components_from_registry(component_registry):
@ -152,22 +123,6 @@ def component_js_dependencies_tag(preload=""):
return mark_safe("\n".join(rendered_dependencies))
@register.tag(name="component")
def do_component(parser, token):
bits = token.split_contents()
bits, isolated_context = check_for_isolated_context_keyword(bits)
component_name, context_args, context_kwargs = parse_component_with_args(
parser, bits, "component"
)
return ComponentNode(
FilterExpression(component_name, parser),
context_args,
context_kwargs,
isolated_context=isolated_context,
)
class UserSlotVar:
"""
Extensible mechanism for offering 'fill' blocks in template access to properties
@ -233,24 +188,17 @@ class SlotNode(Node, TemplateAwareNodeMixin):
def render(self, context):
try:
filled_slots_map: FilledSlotsContext = context[
FILLED_SLOTS_CONTENT_CONTEXT_KEY
]
filled_slots_map: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
except KeyError:
raise TemplateSyntaxError(
f"Attempted to render SlotNode '{self.name}' outside a parent component."
)
raise TemplateSyntaxError(f"Attempted to render SlotNode '{self.name}' outside a parent component.")
extra_context = {}
try:
slot_fill_content: Optional[FillContent] = filled_slots_map[
(self.name, self.template)
]
slot_fill_content: Optional[FillContent] = filled_slots_map[(self.name, self.template)]
except KeyError:
if self.is_required:
raise TemplateSyntaxError(
f"Slot '{self.name}' is marked as 'required' (i.e. non-optional), "
f"yet no fill is provided. "
f"Slot '{self.name}' is marked as 'required' (i.e. non-optional), " f"yet no fill is provided. "
)
nodelist = self.nodelist
else:
@ -274,9 +222,7 @@ def do_slot(parser, token):
if 1 <= len(args) <= 3:
slot_name, *options = args
if not is_wrapped_in_quotes(slot_name):
raise TemplateSyntaxError(
f"'{bits[0]}' name must be a string 'literal'."
)
raise TemplateSyntaxError(f"'{bits[0]}' name must be a string 'literal'.")
slot_name = strip_quotes(slot_name)
modifiers_count = len(options)
if SLOT_REQUIRED_OPTION_KEYWORD in options:
@ -290,9 +236,7 @@ def do_slot(parser, token):
SLOT_REQUIRED_OPTION_KEYWORD,
SLOT_DEFAULT_OPTION_KEYWORD,
]
raise TemplateSyntaxError(
f"Invalid options passed to 'slot' tag. Valid choices: {keywords}."
)
raise TemplateSyntaxError(f"Invalid options passed to 'slot' tag. Valid choices: {keywords}.")
else:
raise TemplateSyntaxError(
"'slot' tag does not match pattern "
@ -321,7 +265,7 @@ class BaseFillNode(Node):
raise TemplateSyntaxError(
"{% fill ... %} block cannot be rendered directly. "
"You are probably seeing this because you have used one outside "
"a {% component_block %} context."
"a {% component %} context."
)
@ -342,7 +286,7 @@ class NamedFillNode(BaseFillNode):
class ImplicitFillNode(BaseFillNode):
"""
Instantiated when a `component_block` tag pair is passed template content that
Instantiated when a `component` tag pair is passed template content that
excludes `fill` tags. Nodes of this type contribute their nodelists to slots marked
as 'default'.
"""
@ -354,10 +298,10 @@ class ImplicitFillNode(BaseFillNode):
@register.tag("fill")
def do_fill(parser, token):
"""Block tag whose contents 'fill' (are inserted into) an identically named
'slot'-block in the component template referred to by a parent component_block.
'slot'-block in the component template referred to by a parent component.
It exists to make component nesting easier.
This tag is available only within a {% component_block %}..{% endcomponent_block %} block.
This tag is available only within a {% component %}..{% endcomponent %} block.
Runtime checks should prohibit other usages.
"""
bits = token.split_contents()
@ -371,14 +315,10 @@ def do_fill(parser, token):
elif len(args) == 3:
tgt_slot_name, as_keyword, alias = args
if as_keyword.lower() != "as":
raise TemplateSyntaxError(
f"{tag} tag args do not conform to pattern '<target slot> as <alias>'"
)
raise TemplateSyntaxError(f"{tag} tag args do not conform to pattern '<target slot> as <alias>'")
alias_fexp = FilterExpression(alias, parser)
else:
raise TemplateSyntaxError(
f"'{tag}' tag takes either 1 or 3 arguments: Received {len(args)}."
)
raise TemplateSyntaxError(f"'{tag}' tag takes either 1 or 3 arguments: Received {len(args)}.")
nodelist = parser.parse(parse_until=["endfill"])
parser.delete_first_token()
@ -414,27 +354,18 @@ class ComponentNode(Node):
def __repr__(self):
return "<ComponentNode: %s. Contents: %r>" % (
self.name_fexp,
getattr(
self, "nodelist", None
), # 'nodelist' attribute only assigned later.
getattr(self, "nodelist", None), # 'nodelist' attribute only assigned later.
)
def render(self, context: Context):
resolved_component_name = self.name_fexp.resolve(context)
component_cls: Type[Component] = component_registry.get(
resolved_component_name
)
component_cls: Type[Component] = component_registry.get(resolved_component_name)
# Resolve FilterExpressions and Variables that were passed as args to the
# component, then call component's context method
# to get values to insert into the context
resolved_context_args = [
safe_resolve(arg, context) for arg in self.context_args
]
resolved_context_kwargs = {
key: safe_resolve(kwarg, context)
for key, kwarg in self.context_kwargs.items()
}
resolved_context_args = [safe_resolve(arg, context) for arg in self.context_args]
resolved_context_kwargs = {key: safe_resolve(kwarg, context) for key, kwarg in self.context_kwargs.items()}
if isinstance(self.fill_nodes, ImplicitFillNode):
fill_content = self.fill_nodes.nodelist
@ -454,9 +385,7 @@ class ComponentNode(Node):
)
else:
resolved_alias: None = None
fill_content.append(
(resolved_name, fill_node.nodelist, resolved_alias)
)
fill_content.append((resolved_name, fill_node.nodelist, resolved_alias))
component: Component = component_cls(
registered_name=resolved_component_name,
@ -464,9 +393,7 @@ class ComponentNode(Node):
fill_content=fill_content,
)
component_context: dict = component.get_context_data(
*resolved_context_args, **resolved_context_kwargs
)
component_context: dict = component.get_context_data(*resolved_context_args, **resolved_context_kwargs)
if self.isolated_context:
context = context.new()
@ -474,22 +401,19 @@ class ComponentNode(Node):
rendered_component = component.render(context)
if is_dependency_middleware_active():
return (
RENDERED_COMMENT_TEMPLATE.format(name=resolved_component_name)
+ rendered_component
)
return RENDERED_COMMENT_TEMPLATE.format(name=resolved_component_name) + rendered_component
else:
return rendered_component
@register.tag(name="component_block")
def do_component_block(parser, token):
@register.tag(name="component")
def do_component(parser, token):
"""
To give the component access to the template context:
{% component_block "name" positional_arg keyword_arg=value ... %}
{% component "name" positional_arg keyword_arg=value ... %}
To render the component in an isolated context:
{% component_block "name" positional_arg keyword_arg=value ... only %}
{% component "name" positional_arg keyword_arg=value ... only %}
Positional and keyword arguments can be literals or template variables.
The component name must be a single- or double-quotes string and must
@ -499,10 +423,8 @@ def do_component_block(parser, token):
bits = token.split_contents()
bits, isolated_context = check_for_isolated_context_keyword(bits)
component_name, context_args, context_kwargs = parse_component_with_args(
parser, bits, "component_block"
)
body: NodeList = parser.parse(parse_until=["endcomponent_block"])
component_name, context_args, context_kwargs = parse_component_with_args(parser, bits, "component")
body: NodeList = parser.parse(parse_until=["endcomponent"])
parser.delete_first_token()
fill_nodes = ()
if block_has_content(body):
@ -515,7 +437,7 @@ def do_component_block(parser, token):
break
else:
raise TemplateSyntaxError(
"Illegal content passed to 'component_block' tag pair. "
"Illegal content passed to 'component' tag pair. "
"Possible causes: 1) Explicit 'fill' tags cannot occur alongside other "
"tags except comment tags; 2) Default (default slot-targeting) content "
"is mixed with explict 'fill' tags."
@ -592,10 +514,7 @@ def is_whitespace_token(token):
def is_block_tag_token(token, name):
return (
token.token_type == TokenType.BLOCK
and token.split_contents()[0] == name
)
return token.token_type == TokenType.BLOCK and token.split_contents()[0] == name
@register.tag(name="if_filled")
@ -627,9 +546,7 @@ def do_if_filled_block(parser, token):
slot_name, is_positive = parse_if_filled_bits(bits)
nodelist = parser.parse(("elif_filled", "else_filled", "endif_filled"))
branches: List[_IfSlotFilledBranchNode] = [
IfSlotFilledConditionBranchNode(
slot_name=slot_name, nodelist=nodelist, is_positive=is_positive
)
IfSlotFilledConditionBranchNode(slot_name=slot_name, nodelist=nodelist, is_positive=is_positive)
]
token = parser.next_token()
@ -638,13 +555,9 @@ def do_if_filled_block(parser, token):
while token.contents.startswith("elif_filled"):
bits = token.split_contents()
slot_name, is_positive = parse_if_filled_bits(bits)
nodelist: NodeList = parser.parse(
("elif_filled", "else_filled", "endif_filled")
)
nodelist: NodeList = parser.parse(("elif_filled", "else_filled", "endif_filled"))
branches.append(
IfSlotFilledConditionBranchNode(
slot_name=slot_name, nodelist=nodelist, is_positive=is_positive
)
IfSlotFilledConditionBranchNode(slot_name=slot_name, nodelist=nodelist, is_positive=is_positive)
)
token = parser.next_token()
@ -673,9 +586,7 @@ def parse_if_filled_bits(
tag, args = bits[0], bits[1:]
if tag in ("else_filled", "endif_filled"):
if len(args) != 0:
raise TemplateSyntaxError(
f"Tag '{tag}' takes no arguments. Received '{' '.join(args)}'"
)
raise TemplateSyntaxError(f"Tag '{tag}' takes no arguments. Received '{' '.join(args)}'")
else:
return None, None
if len(args) == 1:
@ -686,13 +597,10 @@ def parse_if_filled_bits(
is_positive = bool_from_string(args[1])
else:
raise TemplateSyntaxError(
f"{bits[0]} tag arguments '{' '.join(args)}' do not match pattern "
f"'<slotname> (<is_positive>)'"
f"{bits[0]} tag arguments '{' '.join(args)}' do not match pattern " f"'<slotname> (<is_positive>)'"
)
if not is_wrapped_in_quotes(slot_name):
raise TemplateSyntaxError(
f"First argument of '{bits[0]}' must be a quoted string 'literal'."
)
raise TemplateSyntaxError(f"First argument of '{bits[0]}' must be a quoted string 'literal'.")
slot_name = strip_quotes(slot_name)
return slot_name, is_positive
@ -708,9 +616,7 @@ class _IfSlotFilledBranchNode(Node):
raise NotImplementedError
class IfSlotFilledConditionBranchNode(
_IfSlotFilledBranchNode, TemplateAwareNodeMixin
):
class IfSlotFilledConditionBranchNode(_IfSlotFilledBranchNode, TemplateAwareNodeMixin):
def __init__(
self,
slot_name: str,
@ -723,9 +629,7 @@ class IfSlotFilledConditionBranchNode(
def evaluate(self, context) -> bool:
try:
filled_slots: FilledSlotsContext = context[
FILLED_SLOTS_CONTENT_CONTEXT_KEY
]
filled_slots: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
except KeyError:
raise TemplateSyntaxError(
f"Attempted to render {type(self).__name__} outside a Component rendering context."
@ -794,9 +698,7 @@ def parse_component_with_args(parser, bits, tag_name):
)
if tag_name != tag_args[0].token:
raise RuntimeError(
f"Internal error: Expected tag_name to be {tag_name}, but it was {tag_args[0].token}"
)
raise RuntimeError(f"Internal error: Expected tag_name to be {tag_name}, but it was {tag_args[0].token}")
if len(tag_args) > 1:
# At least one position arg, so take the first as the component name
component_name = tag_args[1].token
@ -808,9 +710,7 @@ def parse_component_with_args(parser, bits, tag_name):
context_args = []
context_kwargs = tag_kwargs
except IndexError:
raise TemplateSyntaxError(
f"Call the '{tag_name}' tag with a component name as the first parameter"
)
raise TemplateSyntaxError(f"Call the '{tag_name}' tag with a component name as the first parameter")
return component_name, context_args, context_kwargs
@ -818,11 +718,7 @@ def parse_component_with_args(parser, bits, tag_name):
def safe_resolve(context_item, context):
"""Resolve FilterExpressions and Variables in context if possible. Return other items unchanged."""
return (
context_item.resolve(context)
if hasattr(context_item, "resolve")
else context_item
)
return context_item.resolve(context) if hasattr(context_item, "resolve") else context_item
def is_wrapped_in_quotes(s):
@ -830,9 +726,7 @@ def is_wrapped_in_quotes(s):
def is_dependency_middleware_active():
return getattr(settings, "COMPONENTS", {}).get(
"RENDER_DEPENDENCIES", False
)
return getattr(settings, "COMPONENTS", {}).get("RENDER_DEPENDENCIES", False)
def norm_and_validate_name(name: str, tag: str, context: Optional[str] = None):
@ -843,10 +737,7 @@ def norm_and_validate_name(name: str, tag: str, context: Optional[str] = None):
name = strip_quotes(name)
if not name.isidentifier():
context = f" in '{context}'" if context else ""
raise TemplateSyntaxError(
f"{tag} name '{name}'{context} "
"is not a valid Python identifier."
)
raise TemplateSyntaxError(f"{tag} name '{name}'{context} " "is not a valid Python identifier.")
return name

View file

@ -1,5 +1,5 @@
[tool.black]
line-length = 79
line-length = 119
include = '\.pyi?$'
exclude = '''
/(
@ -18,7 +18,21 @@ exclude = '''
[tool.isort]
profile = "black"
line_length = 79
line_length = 119
multi_line_output = 3
include_trailing_comma = "True"
known_first_party = "django_components"
[tool.flake8]
ignore = ['E302', 'W503']
max-line-length = 119
exclude = [
'migrations',
'__pycache__',
'manage.py',
'settings.py',
'env',
'.env',
'.venv',
'.tox',
]

View file

@ -2,5 +2,7 @@ django
tox
pytest
flake8
flake8-pyproject
isort
pre-commit
black

View file

@ -6,23 +6,31 @@
#
asgiref==3.7.2
# via django
black==24.2.0
# via -r requirements-dev.in
cachetools==5.3.2
# via tox
cfgv==3.4.0
# via pre-commit
chardet==5.2.0
# via tox
click==8.1.7
# via black
colorama==0.4.6
# via tox
distlib==0.3.8
# via virtualenv
django==5.0.2
django==5.0.3
# via -r requirements-dev.in
filelock==3.13.1
# via
# tox
# virtualenv
flake8==7.0.0
# via
# -r requirements-dev.in
# flake8-pyproject
flake8-pyproject==1.2.3
# via -r requirements-dev.in
identify==2.5.33
# via pre-commit
@ -32,15 +40,21 @@ isort==5.13.2
# via -r requirements-dev.in
mccabe==0.7.0
# via flake8
mypy-extensions==1.0.0
# via black
nodeenv==1.8.0
# via pre-commit
packaging==23.2
# via
# black
# pyproject-api
# pytest
# tox
pathspec==0.12.1
# via black
platformdirs==4.1.0
# via
# black
# tox
# virtualenv
pluggy==1.3.0
@ -55,7 +69,7 @@ pyflakes==3.2.0
# via flake8
pyproject-api==1.6.1
# via tox
pytest==8.0.1
pytest==8.0.2
# via -r requirements-dev.in
pyyaml==6.0.1
# via pre-commit

View file

@ -5,12 +5,12 @@
{% component_css_dependencies %}
</head>
<body>
{% component "calendar" date=date %}
{% component_block "greeting" name='Joe' %}
{% component "calendar" date=date %}{% endcomponent %}
{% component "greeting" name='Joe' %}
{% fill "message" %}
Howdy?
{% endfill %}
{% endcomponent_block %}
{% endcomponent %}
{% component_js_dependencies %}
</body>
</html>

View file

@ -3,17 +3,17 @@
<div>Your to-dos:</div>
<ul>
<li>
{% component_block "todo" %}
{% component "todo" %}
{% fill "todo_text" %}
Stop forgetting the milk!
{% endfill %}
{% endcomponent_block %}
{% endcomponent %}
</li>
<li>
{% component_block "todo" %}
{% component "todo" %}
{# As of v0.28, 'fill' tag optional for 1-slot filling if component template specifies a 'default' slot #}
Wear all-white clothes to laser tag tournament.
{% endcomponent_block %}
{% endcomponent %}
</li>
</ul>
</div>

View file

@ -30,9 +30,7 @@ def get_supported_versions(url):
django_to_python = {
version_to_tuple(python_version): [
version_to_tuple(version_string)
for version_string in re.findall(
r"(?<!\.)\d+\.\d+(?!\.)", django_versions
)
for version_string in re.findall(r"(?<!\.)\d+\.\d+(?!\.)", django_versions)
]
for python_version, django_versions in version_dict.items()
}
@ -46,9 +44,7 @@ def get_latest_version(url):
response_content = response.read()
content = response_content.decode("utf-8")
version_string = re.findall(
r"The latest official version is (\d+\.\d)", content
)[0]
version_string = re.findall(r"The latest official version is (\d+\.\d)", content)[0]
return version_to_tuple(version_string)
@ -108,9 +104,7 @@ def build_deps_envlist(python_to_django):
(
env_format(django_version),
env_format(django_version, divider="."),
env_format(
(django_version[0], django_version[1] + 1), divider="."
),
env_format((django_version[0], django_version[1] + 1), divider="."),
)
for django_version in sorted(all_django_versions)
]
@ -123,9 +117,7 @@ def build_pypi_classifiers(python_to_django):
all_python_versions = python_to_django.keys()
for python_version in all_python_versions:
classifiers.append(
f'"Programming Language :: Python :: {env_format(python_version, divider=".")}",'
)
classifiers.append(f'"Programming Language :: Python :: {env_format(python_version, divider=".")}",')
all_django_versions = set()
for django_versions in python_to_django.values():
@ -133,13 +125,9 @@ def build_pypi_classifiers(python_to_django):
all_django_versions.add(django_version)
for django_version in sorted(all_django_versions):
classifiers.append(
f'"Framework :: Django :: {env_format(django_version, divider=".")}",'
)
classifiers.append(f'"Framework :: Django :: {env_format(django_version, divider=".")}",')
return textwrap.indent(
"classifiers=[\n", prefix=" " * 4
) + textwrap.indent("\n".join(classifiers), prefix=" " * 8)
return textwrap.indent("classifiers=[\n", prefix=" " * 4) + textwrap.indent("\n".join(classifiers), prefix=" " * 8)
def build_readme(python_to_django):
@ -154,9 +142,7 @@ def build_readme(python_to_django):
lines = [
(
env_format(python_version, divider="."),
", ".join(
env_format(version, divider=".") for version in django_versions
),
", ".join(env_format(version, divider=".") for version in django_versions),
)
for python_version, django_versions in python_to_django.items()
]
@ -169,13 +155,9 @@ def build_pyenv(python_to_django):
lines = []
all_python_versions = python_to_django.keys()
for python_version in all_python_versions:
lines.append(
f'pyenv install -s {env_format(python_version, divider=".")}'
)
lines.append(f'pyenv install -s {env_format(python_version, divider=".")}')
lines.append(
f'pyenv local {" ".join(env_format(version, divider=".") for version in all_python_versions)}'
)
lines.append(f'pyenv local {" ".join(env_format(version, divider=".") for version in all_python_versions)}')
lines.append("tox -p")
@ -185,20 +167,15 @@ def build_pyenv(python_to_django):
def build_ci_python_versions(python_to_django):
# Outputs python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11']
lines = [
f"'{env_format(python_version, divider='.')}'"
for python_version, django_versions in python_to_django.items()
f"'{env_format(python_version, divider='.')}'" for python_version, django_versions in python_to_django.items()
]
lines = " " * 8 + f"python-version: [{', '.join(lines)}]"
return lines
def main():
django_to_python = get_supported_versions(
"https://docs.djangoproject.com/en/dev/faq/install/"
)
latest_version = get_latest_version(
"https://www.djangoproject.com/download/"
)
django_to_python = get_supported_versions("https://docs.djangoproject.com/en/dev/faq/install/")
latest_version = get_latest_version("https://www.djangoproject.com/download/")
python_to_django = build_python_to_django(django_to_python, latest_version)

View file

@ -1,12 +0,0 @@
[flake8]
ignore = E302,W503
max-line-length = 119
exclude =
migrations
__pycache__
manage.py
settings.py
env
.env
.venv
.tox

View file

@ -3,16 +3,14 @@ import os
from setuptools import find_packages, setup
VERSION = "0.37"
VERSION = "0.50"
setup(
name="django_components",
packages=find_packages(exclude=["tests"]),
version=VERSION,
description="A way to create simple reusable template components in Django.",
long_description=open(
os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf8"
).read(),
long_description=open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf8").read(),
long_description_content_type="text/markdown",
author="Emil Stenström",
author_email="emil@emilstenstrom.se",

View file

@ -11,9 +11,7 @@ if not settings.configured:
}
],
COMPONENTS={"template_cache_size": 128},
MIDDLEWARE=[
"django_components.middleware.ComponentDependencyMiddleware"
],
MIDDLEWARE=["django_components.middleware.ComponentDependencyMiddleware"],
DATABASES={
"default": {
"ENGINE": "django.db.backends.sqlite3",

View file

@ -200,7 +200,7 @@ Page lightly modified and partially extracted as a component #}
</div>
</header>
<div class="titlebar-container"><h1 class="title">Document and website structure</h1></div>
{% component 'breadcrumb_component' items=5 %}
{% component 'breadcrumb_component' items=5 %}{% endcomponent %}
<div class="locale-container">
<form class="language-menu"><label for="select_language" class="visually-hidden">Select your preferred
language</label> <select id="select_language" name="language">

View file

@ -2,11 +2,11 @@
<div>
<h1>Parent content</h1>
{% component name="variable_display" shadowing_variable='override' new_variable='unique_val' %}
{% component name="variable_display" shadowing_variable='override' new_variable='unique_val' %}{% endcomponent %}
</div>
<div>
{% slot 'content' %}
<h2>Slot content</h2>
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}{% endcomponent %}
{% endslot %}
</div>

View file

@ -2,11 +2,11 @@
<div>
<h1>Parent content</h1>
{% component name="variable_display" shadowing_variable=inner_parent_value new_variable='unique_val' %}
{% component name="variable_display" shadowing_variable=inner_parent_value new_variable='unique_val' %}{% endcomponent %}
</div>
<div>
{% slot 'content' %}
<h2>Slot content</h2>
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable=inner_parent_value %}
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable=inner_parent_value %}{% endcomponent %}
{% endslot %}
</div>

View file

@ -1,11 +1,11 @@
{% load component_tags %}
<div class="dashboard-component">
{% component_block "calendar" date="2020-06-06" %}
{% component "calendar" date="2020-06-06" %}
{% fill "header" %} {# fills and slots with same name relate to diff. things. #}
{% slot "header" %}Welcome to your dashboard!{% endslot %}
{% endfill %}
{% fill "body" %}Here are your to-do items for today:{% endfill %}
{% endcomponent_block %}
{% endcomponent %}
<ol>
{% for item in items %}
<li>{{ item }}</li>

View file

@ -4,7 +4,7 @@ from django.template.engine import Engine
from django.urls import include, path
# isort: off
from .django_test_setup import * # noqa
from .django_test_setup import settings
from .testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
# isort: on
@ -28,9 +28,7 @@ class TestAutodiscover(SimpleTestCase):
try:
autodiscover()
except component.AlreadyRegistered:
self.fail(
"Autodiscover should not raise AlreadyRegistered exception"
)
self.fail("Autodiscover should not raise AlreadyRegistered exception")
class TestLoaderSettingsModule(SimpleTestCase):
@ -42,26 +40,17 @@ class TestLoaderSettingsModule(SimpleTestCase):
current_engine = Engine.get_default()
loader = Loader(current_engine)
dirs = loader.get_dirs()
self.assertEqual(
dirs, [Path(__file__).parent.resolve() / "components"]
)
self.assertEqual(dirs, [Path(__file__).parent.resolve() / "components"])
def test_complex_settings_module(self):
settings.SETTINGS_MODULE = ( # noqa
"tests.test_structures.test_structure_1.config.settings"
)
settings.SETTINGS_MODULE = "tests.test_structures.test_structure_1.config.settings" # noqa
current_engine = Engine.get_default()
loader = Loader(current_engine)
dirs = loader.get_dirs()
self.assertEqual(
dirs,
[
Path(__file__).parent.resolve()
/ "test_structures"
/ "test_structure_1"
/ "components"
],
[Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components"],
)
def test_complex_settings_module_2(self):
@ -72,13 +61,7 @@ class TestLoaderSettingsModule(SimpleTestCase):
dirs = loader.get_dirs()
self.assertEqual(
dirs,
[
Path(__file__).parent.resolve()
/ "test_structures"
/ "test_structure_2"
/ "project"
/ "components"
],
[Path(__file__).parent.resolve() / "test_structures" / "test_structure_2" / "project" / "components"],
)
def test_complex_settings_module_3(self):
@ -88,19 +71,8 @@ class TestLoaderSettingsModule(SimpleTestCase):
loader = Loader(current_engine)
dirs = loader.get_dirs()
expected = [
(
Path(__file__).parent.resolve()
/ "test_structures"
/ "test_structure_3"
/ "components"
),
(
Path(__file__).parent.resolve()
/ "test_structures"
/ "test_structure_3"
/ "project"
/ "components"
),
(Path(__file__).parent.resolve() / "test_structures" / "test_structure_3" / "components"),
(Path(__file__).parent.resolve() / "test_structures" / "test_structure_3" / "project" / "components"),
]
self.assertEqual(
sorted(dirs),
@ -110,11 +82,7 @@ class TestLoaderSettingsModule(SimpleTestCase):
class TestBaseDir(SimpleTestCase):
def setUp(self):
settings.BASE_DIR = ( # noqa
Path(__file__).parent.resolve()
/ "test_structures"
/ "test_structure_1"
)
settings.BASE_DIR = Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" # noqa
settings.SETTINGS_MODULE = "tests_fake.test_autodiscover_fake" # noqa
def tearDown(self) -> None:
@ -125,10 +93,5 @@ class TestBaseDir(SimpleTestCase):
current_engine = Engine.get_default()
loader = Loader(current_engine)
dirs = loader.get_dirs()
expected = [
Path(__file__).parent.resolve()
/ "test_structures"
/ "test_structure_1"
/ "components"
]
expected = [Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components"]
self.assertEqual(dirs, expected)

View file

@ -234,9 +234,7 @@ class InlineComponentTest(SimpleTestCase):
css = "path/to/style.css"
js = "path/to/script.js"
comp = HTMLStringFileCSSJSComponent(
"html_string_file_css_js_component"
)
comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-string-file'>Content</div>",
@ -259,9 +257,7 @@ class InlineComponentTest(SimpleTestCase):
class Media:
css = "path/to/style.css"
comp = HTMLStringFileCSSJSComponent(
"html_string_file_css_js_component"
)
comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-string-file'>Content</div>",
@ -284,9 +280,7 @@ class InlineComponentTest(SimpleTestCase):
class Media:
js = "path/to/script.js"
comp = HTMLStringFileCSSJSComponent(
"html_string_file_css_js_component"
)
comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
self.assertHTMLEqual(
comp.render(Context({})),
"<div class='html-string-file'>Content</div>",
@ -303,9 +297,7 @@ class InlineComponentTest(SimpleTestCase):
def test_component_with_variable_in_html(self):
class VariableHTMLComponent(component.Component):
def get_template(self, context):
return Template(
"<div class='variable-html'>{{ variable }}</div>"
)
return Template("<div class='variable-html'>{{ variable }}</div>")
comp = VariableHTMLComponent("variable_html_component")
context = Context({"variable": "Dynamic Content"})
@ -403,15 +395,15 @@ class ComponentIsolationTests(SimpleTestCase):
template = Template(
"""
{% load component_tags %}
{% component_block "test" %}
{% component "test" %}
{% fill "header" %}Override header{% endfill %}
{% endcomponent_block %}
{% component_block "test" %}
{% endcomponent %}
{% component "test" %}
{% fill "main" %}Override main{% endfill %}
{% endcomponent_block %}
{% component_block "test" %}
{% endcomponent %}
{% component "test" %}
{% fill "footer" %}Override footer{% endfill %}
{% endcomponent_block %}
{% endcomponent %}
"""
)

View file

@ -49,9 +49,7 @@ class MockComponentSlot(component.Component):
"""
def get(self, request, *args, **kwargs) -> HttpResponse:
return self.render_to_response(
{"name": "Bob"}, {"second_slot": "Nice to meet you, Bob"}
)
return self.render_to_response({"name": "Bob"}, {"second_slot": "Nice to meet you, Bob"})
@component.register("testcomponent_context_insecure")
@ -64,9 +62,7 @@ class MockInsecureComponentContext(component.Component):
"""
def get(self, request, *args, **kwargs) -> HttpResponse:
return self.render_to_response(
{"variable": "<script>alert(1);</script>"}
)
return self.render_to_response({"variable": "<script>alert(1);</script>"})
@component.register("testcomponent_slot_insecure")
@ -80,16 +76,14 @@ class MockInsecureComponentSlot(component.Component):
"""
def get(self, request, *args, **kwargs) -> HttpResponse:
return self.render_to_response(
{}, {"test_slot": "<script>alert(1);</script>"}
)
return self.render_to_response({}, {"test_slot": "<script>alert(1);</script>"})
def render_template_view(request):
template = Template(
"""
{% load component_tags %}
{% component "testcomponent" variable="TEMPLATE" %}
{% component "testcomponent" variable="TEMPLATE" %}{% endcomponent %}
"""
)
return HttpResponse(template.render(Context({})))

View file

@ -65,15 +65,11 @@ class OuterContextComponent(component.Component):
component.registry.register(name="parent_component", component=ParentComponent)
component.registry.register(
name="parent_with_args", component=ParentComponentWithArgs
)
component.registry.register(name="parent_with_args", component=ParentComponentWithArgs)
component.registry.register(name="variable_display", component=VariableDisplay)
component.registry.register(name="incrementer", component=IncrementerComponent)
component.registry.register(name="simple_component", component=SimpleComponent)
component.registry.register(
name="outer_context_component", component=OuterContextComponent
)
component.registry.register(name="outer_context_component", component=OuterContextComponent)
class ContextTests(SimpleTestCase):
@ -82,73 +78,28 @@ class ContextTests(SimpleTestCase):
):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'parent_component' %}"
"{% component 'parent_component' %}{% endcomponent %}"
)
rendered = template.render(Context())
self.assertIn(
"<h1>Shadowing variable = override</h1>", rendered, rendered
)
self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
self.assertIn(
"<h1>Shadowing variable = slot_default_override</h1>",
rendered,
rendered,
)
self.assertNotIn(
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(
self,
):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component name='parent_component' %}"
"{% component name='parent_component' %}{% endcomponent %}"
)
rendered = template.render(Context())
self.assertIn(
"<h1>Uniquely named variable = unique_val</h1>", rendered, rendered
)
self.assertIn(
"<h1>Uniquely named variable = slot_default_unique</h1>",
rendered,
rendered,
)
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_block_tag(
self,
):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_component' %}{% endcomponent_block %}"
)
rendered = template.render(Context())
self.assertIn(
"<h1>Shadowing variable = override</h1>", rendered, rendered
)
self.assertIn(
"<h1>Shadowing variable = slot_default_override</h1>",
rendered,
rendered,
)
self.assertNotIn(
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_block_tag(
self,
):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_component' %}{% endcomponent_block %}"
)
rendered = template.render(Context())
self.assertIn(
"<h1>Uniquely named variable = unique_val</h1>", rendered, rendered
)
self.assertIn("<h1>Uniquely named variable = unique_val</h1>", rendered, rendered)
self.assertIn(
"<h1>Uniquely named variable = slot_default_unique</h1>",
rendered,
@ -157,41 +108,43 @@ class ContextTests(SimpleTestCase):
def test_nested_component_context_shadows_parent_with_filled_slots(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_component' %}"
"{% fill 'content' %}{% component name='variable_display' "
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endfill %}"
"{% endcomponent_block %}"
"""
{% load component_tags %}{% component_dependencies %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
)
rendered = template.render(Context())
self.assertIn(
"<h1>Shadowing variable = override</h1>", rendered, rendered
)
self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
self.assertIn(
"<h1>Shadowing variable = shadow_from_slot</h1>",
rendered,
rendered,
)
self.assertNotIn(
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
def test_nested_component_instances_have_unique_context_with_filled_slots(
self,
):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_component' %}"
"{% fill 'content' %}{% component name='variable_display' "
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endfill %}"
"{% endcomponent_block %}"
"""
{% load component_tags %}{% component_dependencies %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
)
rendered = template.render(Context())
self.assertIn(
"<h1>Uniquely named variable = unique_val</h1>", rendered, rendered
)
self.assertIn("<h1>Uniquely named variable = unique_val</h1>", rendered, rendered)
self.assertIn(
"<h1>Uniquely named variable = unique_from_slot</h1>",
rendered,
@ -203,180 +156,119 @@ class ContextTests(SimpleTestCase):
):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component name='parent_component' %}"
)
rendered = template.render(
Context({"shadowing_variable": "NOT SHADOWED"})
"{% component name='parent_component' %}{% endcomponent %}"
)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
self.assertIn(
"<h1>Shadowing variable = override</h1>", rendered, rendered
)
self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
self.assertIn(
"<h1>Shadowing variable = slot_default_override</h1>",
rendered,
rendered,
)
self.assertNotIn(
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_block_tag(
self,
):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_component' %}{% endcomponent_block %}"
)
rendered = template.render(
Context({"shadowing_variable": "NOT SHADOWED"})
)
self.assertIn(
"<h1>Shadowing variable = override</h1>", rendered, rendered
)
self.assertIn(
"<h1>Shadowing variable = slot_default_override</h1>",
rendered,
rendered,
)
self.assertNotIn(
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
def test_nested_component_context_shadows_outer_context_with_filled_slots(
self,
):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_component' %}"
"{% fill 'content' %}{% component name='variable_display' "
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endfill %}"
"{% endcomponent_block %}"
)
rendered = template.render(
Context({"shadowing_variable": "NOT SHADOWED"})
"""
{% load component_tags %}{% component_dependencies %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
self.assertIn(
"<h1>Shadowing variable = override</h1>", rendered, rendered
)
self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
self.assertIn(
"<h1>Shadowing variable = shadow_from_slot</h1>",
rendered,
rendered,
)
self.assertNotIn(
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
class ParentArgsTests(SimpleTestCase):
def test_parent_args_can_be_drawn_from_context(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_with_args' parent_value=parent_value %}"
"{% endcomponent_block %}"
"{% component 'parent_with_args' parent_value=parent_value %}"
"{% endcomponent %}"
)
rendered = template.render(Context({"parent_value": "passed_in"}))
self.assertIn(
"<h1>Shadowing variable = passed_in</h1>", rendered, rendered
)
self.assertIn(
"<h1>Uniquely named variable = passed_in</h1>", rendered, rendered
)
self.assertNotIn(
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
self.assertIn("<h1>Shadowing variable = passed_in</h1>", rendered, rendered)
self.assertIn("<h1>Uniquely named variable = passed_in</h1>", rendered, rendered)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
def test_parent_args_available_outside_slots(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_with_args' parent_value='passed_in' %}{%endcomponent_block %}"
"{% component 'parent_with_args' parent_value='passed_in' %}{%endcomponent %}"
)
rendered = template.render(Context())
self.assertIn(
"<h1>Shadowing variable = passed_in</h1>", rendered, rendered
)
self.assertIn(
"<h1>Uniquely named variable = passed_in</h1>", rendered, rendered
)
self.assertNotIn(
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
self.assertIn("<h1>Shadowing variable = passed_in</h1>", rendered, rendered)
self.assertIn("<h1>Uniquely named variable = passed_in</h1>", rendered, rendered)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
def test_parent_args_available_in_slots(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_with_args' parent_value='passed_in' %}"
"{% fill 'content' %}{% component name='variable_display' "
"shadowing_variable='value_from_slot' new_variable=inner_parent_value %}{% endfill %}"
"{%endcomponent_block %}"
"""
{% load component_tags %}{% component_dependencies %}
{% component 'parent_with_args' parent_value='passed_in' %}
{% fill 'content' %}
{% component name='variable_display' shadowing_variable='value_from_slot' new_variable=inner_parent_value %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
)
rendered = template.render(Context())
self.assertIn(
"<h1>Shadowing variable = value_from_slot</h1>", rendered, rendered
)
self.assertIn(
"<h1>Uniquely named variable = passed_in</h1>", rendered, rendered
)
self.assertNotIn(
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
self.assertIn("<h1>Shadowing variable = value_from_slot</h1>", rendered, rendered)
self.assertIn("<h1>Uniquely named variable = passed_in</h1>", rendered, rendered)
self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
class ContextCalledOnceTests(SimpleTestCase):
def test_one_context_call_with_simple_component(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component name='incrementer' %}"
"{% component name='incrementer' %}{% endcomponent %}"
)
rendered = template.render(Context()).strip()
self.assertEqual(
rendered, '<p class="incrementer">value=1;calls=1</p>', rendered
)
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
def test_one_context_call_with_simple_component_and_arg(self):
template = Template(
"{% load component_tags %}{% component name='incrementer' value='2' %}"
)
template = Template("{% load component_tags %}{% component name='incrementer' value='2' %}{% endcomponent %}")
rendered = template.render(Context()).strip()
self.assertEqual(
rendered, '<p class="incrementer">value=3;calls=1</p>', rendered
)
self.assertEqual(rendered, '<p class="incrementer">value=3;calls=1</p>', rendered)
def test_one_context_call_with_component_block(self):
template = Template(
"{% load component_tags %}"
"{% component_block 'incrementer' %}{% endcomponent_block %}"
)
def test_one_context_call_with_component(self):
template = Template("{% load component_tags %}" "{% component 'incrementer' %}{% endcomponent %}")
rendered = template.render(Context()).strip()
self.assertEqual(
rendered, '<p class="incrementer">value=1;calls=1</p>', rendered
)
self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
def test_one_context_call_with_component_block_and_arg(self):
template = Template(
"{% load component_tags %}"
"{% component_block 'incrementer' value='3' %}{% endcomponent_block %}"
)
def test_one_context_call_with_component_and_arg(self):
template = Template("{% load component_tags %}" "{% component 'incrementer' value='3' %}{% endcomponent %}")
rendered = template.render(Context()).strip()
self.assertEqual(
rendered, '<p class="incrementer">value=4;calls=1</p>', rendered
)
self.assertEqual(rendered, '<p class="incrementer">value=4;calls=1</p>', rendered)
def test_one_context_call_with_slot(self):
template = Template(
"{% load component_tags %}"
"{% component_block 'incrementer' %}{% fill 'content' %}"
"<p>slot</p>{% endfill %}{% endcomponent_block %}"
"{% component 'incrementer' %}{% fill 'content' %}"
"<p>slot</p>{% endfill %}{% endcomponent %}"
)
rendered = template.render(Context()).strip()
@ -389,8 +281,8 @@ class ContextCalledOnceTests(SimpleTestCase):
def test_one_context_call_with_slot_and_arg(self):
template = Template(
"{% load component_tags %}"
"{% component_block 'incrementer' value='3' %}{% fill 'content' %}"
"<p>slot</p>{% endfill %}{% endcomponent_block %}"
"{% component 'incrementer' value='3' %}{% fill 'content' %}"
"<p>slot</p>{% endfill %}{% endcomponent %}"
)
rendered = template.render(Context()).strip()
@ -405,11 +297,9 @@ class ComponentsCanAccessOuterContext(SimpleTestCase):
def test_simple_component_can_use_outer_context(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'simple_component' %}"
"{% component 'simple_component' %}{% endcomponent %}"
)
rendered = template.render(
Context({"variable": "outer_value"})
).strip()
rendered = template.render(Context({"variable": "outer_value"})).strip()
self.assertIn("outer_value", rendered, rendered)
@ -417,21 +307,17 @@ class IsolatedContextTests(SimpleTestCase):
def test_simple_component_can_pass_outer_context_in_args(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'simple_component' variable only %}"
"{% component 'simple_component' variable only %}{% endcomponent %}"
)
rendered = template.render(
Context({"variable": "outer_value"})
).strip()
rendered = template.render(Context({"variable": "outer_value"})).strip()
self.assertIn("outer_value", rendered, rendered)
def test_simple_component_cannot_use_outer_context(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'simple_component' only %}"
"{% component 'simple_component' only %}{% endcomponent %}"
)
rendered = template.render(
Context({"variable": "outer_value"})
).strip()
rendered = template.render(Context({"variable": "outer_value"})).strip()
self.assertNotIn("outer_value", rendered, rendered)
@ -452,7 +338,7 @@ class IsolatedContextSettingTests(SimpleTestCase):
):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'simple_component' variable %}"
"{% component 'simple_component' variable %}{% endcomponent %}"
)
rendered = template.render(Context({"variable": "outer_value"}))
self.assertIn("outer_value", rendered, rendered)
@ -462,29 +348,29 @@ class IsolatedContextSettingTests(SimpleTestCase):
):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'simple_component' %}"
"{% component 'simple_component' %}{% endcomponent %}"
)
rendered = template.render(Context({"variable": "outer_value"}))
self.assertNotIn("outer_value", rendered, rendered)
def test_component_block_includes_variable_with_isolated_context_from_settings(
def test_component_includes_variable_with_isolated_context_from_settings(
self,
):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'simple_component' variable %}"
"{% endcomponent_block %}"
"{% component 'simple_component' variable %}"
"{% endcomponent %}"
)
rendered = template.render(Context({"variable": "outer_value"}))
self.assertIn("outer_value", rendered, rendered)
def test_component_block_excludes_variable_with_isolated_context_from_settings(
def test_component_excludes_variable_with_isolated_context_from_settings(
self,
):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'simple_component' %}"
"{% endcomponent_block %}"
"{% component 'simple_component' %}"
"{% endcomponent %}"
)
rendered = template.render(Context({"variable": "outer_value"}))
self.assertNotIn("outer_value", rendered, rendered)
@ -494,19 +380,7 @@ class OuterContextPropertyTests(SimpleTestCase):
def test_outer_context_property_with_component(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'outer_context_component' only %}"
"{% component 'outer_context_component' only %}{% endcomponent %}"
)
rendered = template.render(
Context({"variable": "outer_value"})
).strip()
self.assertIn("outer_value", rendered, rendered)
def test_outer_context_property_with_component_block(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component_block 'outer_context_component' only %}{% endcomponent_block %}"
)
rendered = template.render(
Context({"variable": "outer_value"})
).strip()
rendered = template.render(Context({"variable": "outer_value"})).strip()
self.assertIn("outer_value", rendered, rendered)

View file

@ -52,9 +52,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
def test_no_dependencies_when_no_components_used(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template(
"{% load component_tags %}{% component_dependencies %}"
)
template = Template("{% load component_tags %}{% component_dependencies %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=0)
self.assertInHTML(
@ -66,18 +64,14 @@ class ComponentMediaRenderingTests(SimpleTestCase):
def test_no_js_dependencies_when_no_components_used(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template(
"{% load component_tags %}{% component_js_dependencies %}"
)
template = Template("{% load component_tags %}{% component_js_dependencies %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=0)
def test_no_css_dependencies_when_no_components_used(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template(
"{% load component_tags %}{% component_css_dependencies %}"
)
template = Template("{% load component_tags %}{% component_css_dependencies %}")
rendered = create_and_process_template_response(template)
self.assertInHTML(
'<link href="style.css" media="all" rel="stylesheet"/>',
@ -88,9 +82,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
def test_preload_dependencies_render_when_no_components_used(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template(
"{% load component_tags %}{% component_dependencies preload='test' %}"
)
template = Template("{% load component_tags %}{% component_dependencies preload='test' %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=1)
self.assertInHTML(
@ -102,9 +94,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
def test_preload_css_dependencies_render_when_no_components_used(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template(
"{% load component_tags %}{% component_css_dependencies preload='test' %}"
)
template = Template("{% load component_tags %}{% component_css_dependencies preload='test' %}")
rendered = create_and_process_template_response(template)
self.assertInHTML(
'<link href="style.css" media="all" rel="stylesheet"/>',
@ -117,7 +107,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'test' variable='foo' %}"
"{% component 'test' variable='foo' %}{% endcomponent %}"
)
rendered = create_and_process_template_response(template)
self.assertInHTML(
@ -132,7 +122,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'test' variable='foo' %}"
"{% component 'test' variable='foo' %}{% endcomponent %}"
)
rendered = create_and_process_template_response(template)
self.assertInHTML(
@ -147,7 +137,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
template = Template(
"{% load component_tags %}{% component_dependencies preload='test' %}"
"{% component 'test' variable='foo' %}"
"{% component 'test' variable='foo' %}{% endcomponent %}"
)
rendered = create_and_process_template_response(template)
self.assertInHTML(
@ -162,7 +152,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'test' variable='foo' %}"
"{% component 'test' variable='foo' %}{% endcomponent %}"
)
rendered = create_and_process_template_response(template)
self.assertNotIn("_RENDERED", rendered)
@ -170,9 +160,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
def test_placeholder_removed_when_preload_rendered(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template(
"{% load component_tags %}{% component_dependencies preload='test' %}"
)
template = Template("{% load component_tags %}{% component_dependencies preload='test' %}")
rendered = create_and_process_template_response(template)
self.assertNotIn("_RENDERED", rendered)
@ -181,7 +169,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
template = Template(
"{% load component_tags %}{% component_css_dependencies %}"
"{% component 'test' variable='foo' %}"
"{% component 'test' variable='foo' %}{% endcomponent %}"
)
rendered = create_and_process_template_response(template)
self.assertInHTML(
@ -195,7 +183,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
template = Template(
"{% load component_tags %}{% component_js_dependencies %}"
"{% component 'test' variable='foo' %}"
"{% component 'test' variable='foo' %}{% endcomponent %}"
)
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=1)
@ -205,7 +193,7 @@ class ComponentMediaRenderingTests(SimpleTestCase):
):
component.registry.register(name="test", component=MultistyleComponent)
template = Template(
"{% load component_tags %}{% component_dependencies %}{% component 'test' %}"
"{% load component_tags %}{% component_dependencies %}{% component 'test' %}{% endcomponent %}"
)
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=1)
@ -226,7 +214,10 @@ class ComponentMediaRenderingTests(SimpleTestCase):
):
component.registry.register(name="test", component=MultistyleComponent)
template = Template(
"{% load component_tags %}{% component_js_dependencies %}{% component 'test' %}"
"""
{% load component_tags %}{% component_js_dependencies %}
{% component 'test' %}{% endcomponent %}
"""
)
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=1)
@ -247,7 +238,10 @@ class ComponentMediaRenderingTests(SimpleTestCase):
):
component.registry.register(name="test", component=MultistyleComponent)
template = Template(
"{% load component_tags %}{% component_css_dependencies %}{% component 'test' %}"
"""
{% load component_tags %}{% component_css_dependencies %}
{% component 'test' %}{% endcomponent %}
"""
)
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=0)
@ -265,13 +259,9 @@ class ComponentMediaRenderingTests(SimpleTestCase):
def test_no_dependencies_with_multiple_unused_components(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(
name="test2", component=SimpleComponentAlternate
)
component.registry.register(name="test2", component=SimpleComponentAlternate)
template = Template(
"{% load component_tags %}{% component_dependencies %}"
)
template = Template("{% load component_tags %}{% component_dependencies %}")
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=0)
self.assertInHTML('<script src="script2.js">', rendered, count=0)
@ -288,13 +278,11 @@ class ComponentMediaRenderingTests(SimpleTestCase):
def test_correct_css_dependencies_with_multiple_components(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(
name="test2", component=SimpleComponentAlternate
)
component.registry.register(name="test2", component=SimpleComponentAlternate)
template = Template(
"{% load component_tags %}{% component_css_dependencies %}"
"{% component 'test1' 'variable' %}"
"{% component 'test1' 'variable' %}{% endcomponent %}"
)
rendered = create_and_process_template_response(template)
self.assertInHTML(
@ -310,13 +298,11 @@ class ComponentMediaRenderingTests(SimpleTestCase):
def test_correct_js_dependencies_with_multiple_components(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(
name="test2", component=SimpleComponentAlternate
)
component.registry.register(name="test2", component=SimpleComponentAlternate)
template = Template(
"{% load component_tags %}{% component_js_dependencies %}"
"{% component 'test1' 'variable' %}"
"{% component 'test1' 'variable' %}{% endcomponent %}"
)
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=1)
@ -324,13 +310,11 @@ class ComponentMediaRenderingTests(SimpleTestCase):
def test_correct_dependencies_with_multiple_components(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(
name="test2", component=SimpleComponentAlternate
)
component.registry.register(name="test2", component=SimpleComponentAlternate)
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'test2' variable='variable' %}"
"{% component 'test2' variable='variable' %}{% endcomponent %}"
)
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=0)
@ -348,17 +332,16 @@ class ComponentMediaRenderingTests(SimpleTestCase):
def test_shared_dependencies_rendered_once(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(
name="test2", component=SimpleComponentAlternate
)
component.registry.register(
name="test3", component=SimpleComponentWithSharedDependency
)
component.registry.register(name="test2", component=SimpleComponentAlternate)
component.registry.register(name="test3", component=SimpleComponentWithSharedDependency)
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'test1' variable='variable' %}{% component 'test2' variable='variable' %}"
"{% component 'test1' variable='variable' %}"
"""
{% load component_tags %}{% component_dependencies %}
{% component 'test1' variable='variable' %}{% endcomponent %}
{% component 'test2' variable='variable' %}{% endcomponent %}
{% component 'test1' variable='variable' %}{% endcomponent %}
"""
)
rendered = create_and_process_template_response(template)
self.assertInHTML('<script src="script.js">', rendered, count=1)
@ -376,26 +359,23 @@ class ComponentMediaRenderingTests(SimpleTestCase):
def test_placeholder_removed_when_multiple_component_rendered(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(
name="test2", component=SimpleComponentAlternate
)
component.registry.register(
name="test3", component=SimpleComponentWithSharedDependency
)
component.registry.register(name="test2", component=SimpleComponentAlternate)
component.registry.register(name="test3", component=SimpleComponentWithSharedDependency)
template = Template(
"{% load component_tags %}{% component_dependencies %}"
"{% component 'test1' variable='variable' %}{% component 'test2' variable='variable' %}"
"{% component 'test1' variable='variable' %}"
"""
{% load component_tags %}{% component_dependencies %}
{% component 'test1' variable='variable' %}{% endcomponent %}
{% component 'test2' variable='variable' %}{% endcomponent %}
{% component 'test1' variable='variable' %}{% endcomponent %}
"""
)
rendered = create_and_process_template_response(template)
self.assertNotIn("_RENDERED", rendered)
def test_middleware_response_without_content_type(self):
response = HttpResponseNotModified()
middleware = ComponentDependencyMiddleware(
get_response=lambda _: response
)
middleware = ComponentDependencyMiddleware(get_response=lambda _: response)
request = Mock()
self.assertEqual(response, middleware(request=request))
@ -408,14 +388,12 @@ class ComponentMediaRenderingTests(SimpleTestCase):
"test_component",
]
for component_name in component_names:
component.registry.register(
name=component_name, component=SimpleComponent
)
component.registry.register(name=component_name, component=SimpleComponent)
template = Template(
"{% load component_tags %}"
"{% component_js_dependencies %}"
"{% component_css_dependencies %}"
f"{{% component '{component_name}' variable='value' %}}"
f"{{% component '{component_name}' variable='value' %}}{{% endcomponent %}}"
)
rendered = create_and_process_template_response(template)
self.assertHTMLEqual(

View file

@ -27,9 +27,7 @@ class ComponentRegistryTest(unittest.TestCase):
class TestComponent(component.Component):
pass
self.assertEqual(
component.registry.get("decorated_component"), TestComponent
)
self.assertEqual(component.registry.get("decorated_component"), TestComponent)
def test_simple_register(self):
self.registry.register(name="testcomponent", component=MockComponent)
@ -49,18 +47,12 @@ class ComponentRegistryTest(unittest.TestCase):
def test_prevent_registering_different_components_with_the_same_name(self):
self.registry.register(name="testcomponent", component=MockComponent)
with self.assertRaises(component.AlreadyRegistered):
self.registry.register(
name="testcomponent", component=MockComponent2
)
self.registry.register(name="testcomponent", component=MockComponent2)
def test_allow_duplicated_registration_of_the_same_component(self):
try:
self.registry.register(
name="testcomponent", component=MockComponentView
)
self.registry.register(
name="testcomponent", component=MockComponentView
)
self.registry.register(name="testcomponent", component=MockComponentView)
self.registry.register(name="testcomponent", component=MockComponentView)
except component.AlreadyRegistered:
self.fail("Should not raise AlreadyRegistered")

View file

@ -52,15 +52,11 @@ class CreateComponentCommandTest(TestCase):
os.path.join(self.temp_dir, component_name, "test.js"),
os.path.join(self.temp_dir, component_name, "test.css"),
os.path.join(self.temp_dir, component_name, "test.html"),
os.path.join(
self.temp_dir, component_name, f"{component_name}.py"
),
os.path.join(self.temp_dir, component_name, f"{component_name}.py"),
]
for file_path in expected_files:
self.assertTrue(
os.path.exists(file_path), f"File {file_path} was not created"
)
self.assertTrue(os.path.exists(file_path), f"File {file_path} was not created")
def test_dry_run(self):
component_name = "dryruncomponent"
@ -80,9 +76,7 @@ class CreateComponentCommandTest(TestCase):
component_path = os.path.join(self.temp_dir, component_name)
os.makedirs(component_path)
with open(
os.path.join(component_path, f"{component_name}.py"), "w"
) as f:
with open(os.path.join(component_path, f"{component_name}.py"), "w") as f:
f.write("hello world")
call_command(
@ -93,9 +87,7 @@ class CreateComponentCommandTest(TestCase):
"--force",
)
with open(
os.path.join(component_path, f"{component_name}.py"), "r"
) as f:
with open(os.path.join(component_path, f"{component_name}.py"), "r") as f:
self.assertNotIn("hello world", f.read())
def test_error_existing_component_no_force(self):
@ -104,9 +96,7 @@ class CreateComponentCommandTest(TestCase):
os.makedirs(component_path)
with self.assertRaises(CommandError):
call_command(
"startcomponent", component_name, "--path", self.temp_dir
)
call_command("startcomponent", component_name, "--path", self.temp_dir)
def test_verbose_output(self):
component_name = "verbosecomponent"

File diff suppressed because it is too large Load diff

View file

@ -8,9 +8,7 @@ from django_components.middleware import ComponentDependencyMiddleware
# Create middleware instance
response_stash = None
middleware = ComponentDependencyMiddleware(
get_response=lambda _: response_stash
)
middleware = ComponentDependencyMiddleware(get_response=lambda _: response_stash)
class Django30CompatibleSimpleTestCase(SimpleTestCase):
@ -19,9 +17,7 @@ class Django30CompatibleSimpleTestCase(SimpleTestCase):
left = left.replace(' type="text/css"', "")
right = right.replace(' type="text/javascript"', "")
right = right.replace(' type="text/css"', "")
super(Django30CompatibleSimpleTestCase, self).assertHTMLEqual(
left, right
)
super(Django30CompatibleSimpleTestCase, self).assertHTMLEqual(left, right)
def assertInHTML(self, needle, haystack, count=None, msg_prefix=""):
haystack = haystack.replace(' type="text/javascript"', "")
@ -37,9 +33,7 @@ request = Mock()
mock_template = Mock()
def create_and_process_template_response(
template, context=None, use_middleware=True
):
def create_and_process_template_response(template, context=None, use_middleware=True):
context = context if context is not None else Context({})
mock_template.render = lambda context, _: template.render(context)
response = TemplateResponse(request, mock_template, context)

View file

@ -45,7 +45,9 @@ commands = py.test {posargs}
[testenv:flake8]
# Note: Settings for flake8 exists in the setup.cfg file
changedir = {toxinidir}
deps = flake8
deps =
flake8
flake8-pyproject
commands =
flake8 .