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 rev: 7.0.0
hooks: hooks:
- id: flake8 - 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: 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 ```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): 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 ## 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 🚨 ## Security notes 🚨
@ -220,7 +224,7 @@ First load the `component_tags` tag library, then use the `component_[js/css]_de
{% component_css_dependencies %} {% component_css_dependencies %}
</head> </head>
<body> <body>
{% component "calendar" date="2015-06-19" %} {% component "calendar" date="2015-06-19" %}{% endcomponent %}
{% component_js_dependencies %} {% component_js_dependencies %}
</body> </body>
<html> <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... 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. - `{% 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_. 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: 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 ```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 %} {% 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: 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. 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. 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): Including the component (notice how the `fill` tag is omitted):
```htmldjango ```htmldjango
{% component_block "calendar" date="2020-06-06" %} {% component "calendar" date="2020-06-06" %}
Can you believe it's already <span>{{ date }}</span>?? Can you believe it's already <span>{{ date }}</span>??
{% endcomponent_block %} {% endcomponent %}
``` ```
The rendered result (exactly the same as before): 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 ```htmldjango
{# DON'T DO THIS #} {# DON'T DO THIS #}
{% component_block "calendar" date="2020-06-06" %} {% component "calendar" date="2020-06-06" %}
{% fill "header" %}Totally new header!{% endfill %} {% fill "header" %}Totally new header!{% endfill %}
Can you believe it's already <span>{{ date }}</span>?? 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.: By contrast, it is permitted to use `fill` tags in nested components, e.g.:
```htmldjango ```htmldjango
{% component_block "calendar" date="2020-06-06" %} {% component "calendar" date="2020-06-06" %}
{% component_block "beautiful-box" %} {% component "beautiful-box" %}
{% fill "content" %} Can you believe it's already <span>{{ date }}</span>?? {% endfill %} {% fill "content" %} Can you believe it's already <span>{{ date }}</span>?? {% endfill %}
{% endcomponent_block %} {% endcomponent %}
{% endcomponent_block %} {% endcomponent %}
``` ```
This is fine too: This is fine too:
```htmldjango ```htmldjango
{% component_block "calendar" date="2020-06-06" %} {% component "calendar" date="2020-06-06" %}
{% fill "header" %} {% fill "header" %}
{% component_block "calendar-header" %} {% component "calendar-header" %}
Super Special Calendar Header Super Special Calendar Header
{% endcomponent_block %} {% endcomponent %}
{% endfill %} {% endfill %}
{% endcomponent_block %} {% endcomponent %}
``` ```
### Components as views ### 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. 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 ```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 %} {% fill "body" as "body" %}{{ body.default }}. Have a great day!{% endfill %}
{% endcomponent_block %} {% endcomponent %}
``` ```
Produces: Produces:
@ -617,10 +621,10 @@ COMPONENTS = {
## Component context and scope ## 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 ```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. 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.test import override_settings
from django_components import component from django_components import component
from django_components.middleware import ( from django_components.middleware import CSS_DEPENDENCY_PLACEHOLDER, JS_DEPENDENCY_PLACEHOLDER
CSS_DEPENDENCY_PLACEHOLDER,
JS_DEPENDENCY_PLACEHOLDER,
)
from tests.django_test_setup import * # NOQA from tests.django_test_setup import * # NOQA
from tests.testutils import Django30CompatibleSimpleTestCase as SimpleTestCase from tests.testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
from tests.testutils import create_and_process_template_response from tests.testutils import create_and_process_template_response
@ -75,9 +72,7 @@ class RenderBenchmarks(SimpleTestCase):
component.registry.clear() component.registry.clear()
component.registry.register("test_component", SlottedComponent) component.registry.register("test_component", SlottedComponent)
component.registry.register("inner_component", SimpleComponent) component.registry.register("inner_component", SimpleComponent)
component.registry.register( component.registry.register("breadcrumb_component", BreadcrumbComponent)
"breadcrumb_component", BreadcrumbComponent
)
@staticmethod @staticmethod
def timed_loop(func, iterations=1000): def timed_loop(func, iterations=1000):
@ -91,22 +86,28 @@ class RenderBenchmarks(SimpleTestCase):
def test_render_time_for_small_component(self): def test_render_time_for_small_component(self):
template = Template( template = Template(
"{% load component_tags %}{% component_block 'test_component' %}" """
"{% slot \"header\" %}{% component 'inner_component' variable='foo' %}{% endslot %}" {% load component_tags %}
"{% endcomponent_block %}", {% component 'test_component' %}
name="root", {% slot "header" %}
{% component 'inner_component' variable='foo' %}{% endcomponent %}
{% endslot %}
{% endcomponent %}
"""
) )
print( print(f"{self.timed_loop(lambda: template.render(Context({})))} ms per iteration")
f"{self.timed_loop(lambda: template.render(Context({})))} ms per iteration"
)
def test_middleware_time_with_dependency_for_small_page(self): def test_middleware_time_with_dependency_for_small_page(self):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" """
"{% component_block 'test_component' %}{% slot \"header\" %}" {% load component_tags %}{% component_dependencies %}
"{% component 'inner_component' variable='foo' %}{% endslot %}{% endcomponent_block %}", {% component 'test_component' %}
name="root", {% slot "header" %}
{% component 'inner_component' variable='foo' %}{% endcomponent %}
{% endslot %}
{% endcomponent %}
"""
) )
# Sanity tests # Sanity tests
response_content = create_and_process_template_response(template) response_content = create_and_process_template_response(template)
@ -116,15 +117,9 @@ class RenderBenchmarks(SimpleTestCase):
self.assertIn("script.js", response_content) self.assertIn("script.js", response_content)
without_middleware = self.timed_loop( without_middleware = self.timed_loop(
lambda: create_and_process_template_response( lambda: create_and_process_template_response(template, use_middleware=False)
template, use_middleware=False
)
)
with_middleware = self.timed_loop(
lambda: create_and_process_template_response(
template, use_middleware=True
)
) )
with_middleware = self.timed_loop(lambda: create_and_process_template_response(template, use_middleware=True))
print("Small page middleware test") print("Small page middleware test")
self.report_results(with_middleware, without_middleware) self.report_results(with_middleware, without_middleware)
@ -140,14 +135,10 @@ class RenderBenchmarks(SimpleTestCase):
self.assertIn("test.js", response_content) self.assertIn("test.js", response_content)
without_middleware = self.timed_loop( without_middleware = self.timed_loop(
lambda: create_and_process_template_response( lambda: create_and_process_template_response(template, {}, use_middleware=False)
template, {}, use_middleware=False
)
) )
with_middleware = self.timed_loop( with_middleware = self.timed_loop(
lambda: create_and_process_template_response( lambda: create_and_process_template_response(template, {}, use_middleware=True)
template, {}, use_middleware=True
)
) )
print("Large page middleware test") print("Large page middleware test")
@ -156,15 +147,9 @@ class RenderBenchmarks(SimpleTestCase):
@staticmethod @staticmethod
def report_results(with_middleware, without_middleware): def report_results(with_middleware, without_middleware):
print(f"Middleware active\t\t{with_middleware:.3f} ms per iteration") print(f"Middleware active\t\t{with_middleware:.3f} ms per iteration")
print( print(f"Middleware inactive\t{without_middleware:.3f} ms per iteration")
f"Middleware inactive\t{without_middleware:.3f} ms per iteration"
)
time_difference = with_middleware - without_middleware time_difference = with_middleware - without_middleware
if without_middleware > with_middleware: if without_middleware > with_middleware:
print( print(f"Decrease of {-100 * time_difference / with_middleware:.2f}%")
f"Decrease of {-100 * time_difference / with_middleware:.2f}%"
)
else: else:
print( print(f"Increase of {100 * time_difference / without_middleware:.2f}%")
f"Increase of {100 * time_difference / without_middleware:.2f}%"
)

View file

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

View file

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

View file

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

View file

@ -9,9 +9,7 @@ class Command(BaseCommand):
help = "Creates a new component" help = "Creates a new component"
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument("name", type=str, help="The name of the component to create")
"name", type=str, help="The name of the component to create"
)
parser.add_argument( parser.add_argument(
"--path", "--path",
type=str, type=str,
@ -71,9 +69,7 @@ class Command(BaseCommand):
elif base_dir: elif base_dir:
component_path = os.path.join(base_dir, "components", name) component_path = os.path.join(base_dir, "components", name)
else: else:
raise CommandError( raise CommandError("You must specify a path or set BASE_DIR in your django settings")
"You must specify a path or set BASE_DIR in your django settings"
)
if os.path.exists(component_path): if os.path.exists(component_path):
if force: if force:
@ -84,11 +80,7 @@ class Command(BaseCommand):
) )
) )
else: else:
self.stdout.write( self.stdout.write(self.style.WARNING(f'The component "{name}" already exists. Overwriting...'))
self.style.WARNING(
f'The component "{name}" already exists. Overwriting...'
)
)
else: else:
raise CommandError( raise CommandError(
f'The component "{name}" already exists at {component_path}. Use --force to overwrite.' 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()) f.write(script_content.strip())
with open( with open(os.path.join(component_path, css_filename), "w") as f:
os.path.join(component_path, css_filename), "w"
) as f:
style_content = dedent( style_content = dedent(
f""" f"""
.component-{name} {{ .component-{name} {{
@ -119,9 +109,7 @@ class Command(BaseCommand):
) )
f.write(style_content.strip()) f.write(style_content.strip())
with open( with open(os.path.join(component_path, template_filename), "w") as f:
os.path.join(component_path, template_filename), "w"
) as f:
template_content = dedent( template_content = dedent(
f""" f"""
<div class="component-{name}"> <div class="component-{name}">
@ -133,9 +121,7 @@ class Command(BaseCommand):
) )
f.write(template_content.strip()) f.write(template_content.strip())
with open( with open(os.path.join(component_path, f"{name}.py"), "w") as f:
os.path.join(component_path, f"{name}.py"), "w"
) as f:
py_content = dedent( py_content = dedent(
f""" f"""
from django_components import component from django_components import component
@ -157,16 +143,8 @@ class Command(BaseCommand):
f.write(py_content.strip()) f.write(py_content.strip())
if verbose: if verbose:
self.stdout.write( self.stdout.write(self.style.SUCCESS(f"Successfully created {name} component at {component_path}"))
self.style.SUCCESS(
f"Successfully created {name} component at {component_path}"
)
)
else: else:
self.stdout.write( self.stdout.write(self.style.SUCCESS(f"Successfully created {name} component"))
self.style.SUCCESS(
f"Successfully created {name} component"
)
)
else: else:
raise CommandError("You must specify a component name") 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>' JS_DEPENDENCY_PLACEHOLDER = '<script name="JS_PLACEHOLDER"></script>'
SCRIPT_TAG_REGEX = re.compile("<script") SCRIPT_TAG_REGEX = re.compile("<script")
COMPONENT_COMMENT_REGEX = re.compile( COMPONENT_COMMENT_REGEX = re.compile(rb"<!-- _RENDERED (?P<name>[\w\-/]+?) -->")
rb"<!-- _RENDERED (?P<name>[\w\-/]+?) -->"
)
PLACEHOLDER_REGEX = re.compile( PLACEHOLDER_REGEX = re.compile(
rb"<!-- _RENDERED (?P<name>[\w\-/]+?) -->" rb"<!-- _RENDERED (?P<name>[\w\-/]+?) -->"
rb'|<link name="CSS_PLACEHOLDER">' rb'|<link name="CSS_PLACEHOLDER">'
@ -32,9 +30,7 @@ class ComponentDependencyMiddleware:
def __call__(self, request): def __call__(self, request):
response = self.get_response(request) response = self.get_response(request)
if ( if (
getattr(settings, "COMPONENTS", {}).get( getattr(settings, "COMPONENTS", {}).get("RENDER_DEPENDENCIES", False)
"RENDER_DEPENDENCIES", False
)
and not isinstance(response, StreamingHttpResponse) and not isinstance(response, StreamingHttpResponse)
and response.get("Content-Type", "").startswith("text/html") and response.get("Content-Type", "").startswith("text/html")
): ):
@ -43,23 +39,12 @@ class ComponentDependencyMiddleware:
def process_response_content(content): def process_response_content(content):
component_names_seen = { component_names_seen = {match.group("name") for match in COMPONENT_COMMENT_REGEX.finditer(content)}
match.group("name") all_components = [registry.get(name.decode("utf-8"))("") for name in component_names_seen]
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) all_media = join_media(all_components)
js_dependencies = b"".join( js_dependencies = b"".join(media.encode("utf-8") for media in all_media.render_js())
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)
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): def add_module_attribute_to_scripts(scripts):

View file

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

View file

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

View file

@ -1,5 +1,5 @@
[tool.black] [tool.black]
line-length = 79 line-length = 119
include = '\.pyi?$' include = '\.pyi?$'
exclude = ''' exclude = '''
/( /(
@ -18,7 +18,21 @@ exclude = '''
[tool.isort] [tool.isort]
profile = "black" profile = "black"
line_length = 79 line_length = 119
multi_line_output = 3 multi_line_output = 3
include_trailing_comma = "True" include_trailing_comma = "True"
known_first_party = "django_components" 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 tox
pytest pytest
flake8 flake8
flake8-pyproject
isort isort
pre-commit pre-commit
black

View file

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

View file

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

View file

@ -3,17 +3,17 @@
<div>Your to-dos:</div> <div>Your to-dos:</div>
<ul> <ul>
<li> <li>
{% component_block "todo" %} {% component "todo" %}
{% fill "todo_text" %} {% fill "todo_text" %}
Stop forgetting the milk! Stop forgetting the milk!
{% endfill %} {% endfill %}
{% endcomponent_block %} {% endcomponent %}
</li> </li>
<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 #} {# 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. Wear all-white clothes to laser tag tournament.
{% endcomponent_block %} {% endcomponent %}
</li> </li>
</ul> </ul>
</div> </div>

View file

@ -30,9 +30,7 @@ def get_supported_versions(url):
django_to_python = { django_to_python = {
version_to_tuple(python_version): [ version_to_tuple(python_version): [
version_to_tuple(version_string) version_to_tuple(version_string)
for version_string in re.findall( for version_string in re.findall(r"(?<!\.)\d+\.\d+(?!\.)", django_versions)
r"(?<!\.)\d+\.\d+(?!\.)", django_versions
)
] ]
for python_version, django_versions in version_dict.items() for python_version, django_versions in version_dict.items()
} }
@ -46,9 +44,7 @@ def get_latest_version(url):
response_content = response.read() response_content = response.read()
content = response_content.decode("utf-8") content = response_content.decode("utf-8")
version_string = re.findall( version_string = re.findall(r"The latest official version is (\d+\.\d)", content)[0]
r"The latest official version is (\d+\.\d)", content
)[0]
return version_to_tuple(version_string) 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),
env_format(django_version, divider="."), env_format(django_version, divider="."),
env_format( env_format((django_version[0], django_version[1] + 1), divider="."),
(django_version[0], django_version[1] + 1), divider="."
),
) )
for django_version in sorted(all_django_versions) 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() all_python_versions = python_to_django.keys()
for python_version in all_python_versions: for python_version in all_python_versions:
classifiers.append( classifiers.append(f'"Programming Language :: Python :: {env_format(python_version, divider=".")}",')
f'"Programming Language :: Python :: {env_format(python_version, divider=".")}",'
)
all_django_versions = set() all_django_versions = set()
for django_versions in python_to_django.values(): 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) all_django_versions.add(django_version)
for django_version in sorted(all_django_versions): for django_version in sorted(all_django_versions):
classifiers.append( classifiers.append(f'"Framework :: Django :: {env_format(django_version, divider=".")}",')
f'"Framework :: Django :: {env_format(django_version, divider=".")}",'
)
return textwrap.indent( return textwrap.indent("classifiers=[\n", prefix=" " * 4) + textwrap.indent("\n".join(classifiers), prefix=" " * 8)
"classifiers=[\n", prefix=" " * 4
) + textwrap.indent("\n".join(classifiers), prefix=" " * 8)
def build_readme(python_to_django): def build_readme(python_to_django):
@ -154,9 +142,7 @@ def build_readme(python_to_django):
lines = [ lines = [
( (
env_format(python_version, divider="."), env_format(python_version, divider="."),
", ".join( ", ".join(env_format(version, divider=".") for version in django_versions),
env_format(version, divider=".") for version in django_versions
),
) )
for python_version, django_versions in python_to_django.items() for python_version, django_versions in python_to_django.items()
] ]
@ -169,13 +155,9 @@ def build_pyenv(python_to_django):
lines = [] lines = []
all_python_versions = python_to_django.keys() all_python_versions = python_to_django.keys()
for python_version in all_python_versions: for python_version in all_python_versions:
lines.append( lines.append(f'pyenv install -s {env_format(python_version, divider=".")}')
f'pyenv install -s {env_format(python_version, divider=".")}'
)
lines.append( lines.append(f'pyenv local {" ".join(env_format(version, divider=".") for version in all_python_versions)}')
f'pyenv local {" ".join(env_format(version, divider=".") for version in all_python_versions)}'
)
lines.append("tox -p") lines.append("tox -p")
@ -185,20 +167,15 @@ def build_pyenv(python_to_django):
def build_ci_python_versions(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'] # Outputs python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11']
lines = [ lines = [
f"'{env_format(python_version, divider='.')}'" f"'{env_format(python_version, divider='.')}'" for python_version, django_versions in python_to_django.items()
for python_version, django_versions in python_to_django.items()
] ]
lines = " " * 8 + f"python-version: [{', '.join(lines)}]" lines = " " * 8 + f"python-version: [{', '.join(lines)}]"
return lines return lines
def main(): def main():
django_to_python = get_supported_versions( django_to_python = get_supported_versions("https://docs.djangoproject.com/en/dev/faq/install/")
"https://docs.djangoproject.com/en/dev/faq/install/" latest_version = get_latest_version("https://www.djangoproject.com/download/")
)
latest_version = get_latest_version(
"https://www.djangoproject.com/download/"
)
python_to_django = build_python_to_django(django_to_python, latest_version) 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 from setuptools import find_packages, setup
VERSION = "0.37" VERSION = "0.50"
setup( setup(
name="django_components", name="django_components",
packages=find_packages(exclude=["tests"]), packages=find_packages(exclude=["tests"]),
version=VERSION, version=VERSION,
description="A way to create simple reusable template components in Django.", description="A way to create simple reusable template components in Django.",
long_description=open( long_description=open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf8").read(),
os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf8"
).read(),
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
author="Emil Stenström", author="Emil Stenström",
author_email="emil@emilstenstrom.se", author_email="emil@emilstenstrom.se",

View file

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

View file

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

View file

@ -2,11 +2,11 @@
<div> <div>
<h1>Parent content</h1> <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>
<div> <div>
{% slot 'content' %} {% slot 'content' %}
<h2>Slot content</h2> <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 %} {% endslot %}
</div> </div>

View file

@ -2,11 +2,11 @@
<div> <div>
<h1>Parent content</h1> <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>
<div> <div>
{% slot 'content' %} {% slot 'content' %}
<h2>Slot content</h2> <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 %} {% endslot %}
</div> </div>

View file

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

View file

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

View file

@ -234,9 +234,7 @@ class InlineComponentTest(SimpleTestCase):
css = "path/to/style.css" css = "path/to/style.css"
js = "path/to/script.js" js = "path/to/script.js"
comp = HTMLStringFileCSSJSComponent( comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
"html_string_file_css_js_component"
)
self.assertHTMLEqual( self.assertHTMLEqual(
comp.render(Context({})), comp.render(Context({})),
"<div class='html-string-file'>Content</div>", "<div class='html-string-file'>Content</div>",
@ -259,9 +257,7 @@ class InlineComponentTest(SimpleTestCase):
class Media: class Media:
css = "path/to/style.css" css = "path/to/style.css"
comp = HTMLStringFileCSSJSComponent( comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
"html_string_file_css_js_component"
)
self.assertHTMLEqual( self.assertHTMLEqual(
comp.render(Context({})), comp.render(Context({})),
"<div class='html-string-file'>Content</div>", "<div class='html-string-file'>Content</div>",
@ -284,9 +280,7 @@ class InlineComponentTest(SimpleTestCase):
class Media: class Media:
js = "path/to/script.js" js = "path/to/script.js"
comp = HTMLStringFileCSSJSComponent( comp = HTMLStringFileCSSJSComponent("html_string_file_css_js_component")
"html_string_file_css_js_component"
)
self.assertHTMLEqual( self.assertHTMLEqual(
comp.render(Context({})), comp.render(Context({})),
"<div class='html-string-file'>Content</div>", "<div class='html-string-file'>Content</div>",
@ -303,9 +297,7 @@ class InlineComponentTest(SimpleTestCase):
def test_component_with_variable_in_html(self): def test_component_with_variable_in_html(self):
class VariableHTMLComponent(component.Component): class VariableHTMLComponent(component.Component):
def get_template(self, context): def get_template(self, context):
return Template( return Template("<div class='variable-html'>{{ variable }}</div>")
"<div class='variable-html'>{{ variable }}</div>"
)
comp = VariableHTMLComponent("variable_html_component") comp = VariableHTMLComponent("variable_html_component")
context = Context({"variable": "Dynamic Content"}) context = Context({"variable": "Dynamic Content"})
@ -403,15 +395,15 @@ class ComponentIsolationTests(SimpleTestCase):
template = Template( template = Template(
""" """
{% load component_tags %} {% load component_tags %}
{% component_block "test" %} {% component "test" %}
{% fill "header" %}Override header{% endfill %} {% fill "header" %}Override header{% endfill %}
{% endcomponent_block %} {% endcomponent %}
{% component_block "test" %} {% component "test" %}
{% fill "main" %}Override main{% endfill %} {% fill "main" %}Override main{% endfill %}
{% endcomponent_block %} {% endcomponent %}
{% component_block "test" %} {% component "test" %}
{% fill "footer" %}Override footer{% endfill %} {% 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: def get(self, request, *args, **kwargs) -> HttpResponse:
return self.render_to_response( return self.render_to_response({"name": "Bob"}, {"second_slot": "Nice to meet you, Bob"})
{"name": "Bob"}, {"second_slot": "Nice to meet you, Bob"}
)
@component.register("testcomponent_context_insecure") @component.register("testcomponent_context_insecure")
@ -64,9 +62,7 @@ class MockInsecureComponentContext(component.Component):
""" """
def get(self, request, *args, **kwargs) -> HttpResponse: def get(self, request, *args, **kwargs) -> HttpResponse:
return self.render_to_response( return self.render_to_response({"variable": "<script>alert(1);</script>"})
{"variable": "<script>alert(1);</script>"}
)
@component.register("testcomponent_slot_insecure") @component.register("testcomponent_slot_insecure")
@ -80,16 +76,14 @@ class MockInsecureComponentSlot(component.Component):
""" """
def get(self, request, *args, **kwargs) -> HttpResponse: def get(self, request, *args, **kwargs) -> HttpResponse:
return self.render_to_response( return self.render_to_response({}, {"test_slot": "<script>alert(1);</script>"})
{}, {"test_slot": "<script>alert(1);</script>"}
)
def render_template_view(request): def render_template_view(request):
template = Template( template = Template(
""" """
{% load component_tags %} {% load component_tags %}
{% component "testcomponent" variable="TEMPLATE" %} {% component "testcomponent" variable="TEMPLATE" %}{% endcomponent %}
""" """
) )
return HttpResponse(template.render(Context({}))) 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_component", component=ParentComponent)
component.registry.register( component.registry.register(name="parent_with_args", component=ParentComponentWithArgs)
name="parent_with_args", component=ParentComponentWithArgs
)
component.registry.register(name="variable_display", component=VariableDisplay) component.registry.register(name="variable_display", component=VariableDisplay)
component.registry.register(name="incrementer", component=IncrementerComponent) component.registry.register(name="incrementer", component=IncrementerComponent)
component.registry.register(name="simple_component", component=SimpleComponent) component.registry.register(name="simple_component", component=SimpleComponent)
component.registry.register( component.registry.register(name="outer_context_component", component=OuterContextComponent)
name="outer_context_component", component=OuterContextComponent
)
class ContextTests(SimpleTestCase): class ContextTests(SimpleTestCase):
@ -82,73 +78,28 @@ class ContextTests(SimpleTestCase):
): ):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component 'parent_component' %}" "{% component 'parent_component' %}{% endcomponent %}"
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertIn( self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
"<h1>Shadowing variable = override</h1>", rendered, rendered
)
self.assertIn( self.assertIn(
"<h1>Shadowing variable = slot_default_override</h1>", "<h1>Shadowing variable = slot_default_override</h1>",
rendered, rendered,
rendered, rendered,
) )
self.assertNotIn( self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag( def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(
self, self,
): ):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component name='parent_component' %}" "{% component name='parent_component' %}{% endcomponent %}"
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertIn( self.assertIn("<h1>Uniquely named variable = unique_val</h1>", rendered, rendered)
"<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( self.assertIn(
"<h1>Uniquely named variable = slot_default_unique</h1>", "<h1>Uniquely named variable = slot_default_unique</h1>",
rendered, rendered,
@ -157,41 +108,43 @@ class ContextTests(SimpleTestCase):
def test_nested_component_context_shadows_parent_with_filled_slots(self): def test_nested_component_context_shadows_parent_with_filled_slots(self):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" """
"{% component_block 'parent_component' %}" {% load component_tags %}{% component_dependencies %}
"{% fill 'content' %}{% component name='variable_display' " {% component 'parent_component' %}
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endfill %}" {% fill 'content' %}
"{% endcomponent_block %}" {% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertIn( self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
"<h1>Shadowing variable = override</h1>", rendered, rendered
)
self.assertIn( self.assertIn(
"<h1>Shadowing variable = shadow_from_slot</h1>", "<h1>Shadowing variable = shadow_from_slot</h1>",
rendered, rendered,
rendered, rendered,
) )
self.assertNotIn( self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
def test_nested_component_instances_have_unique_context_with_filled_slots( def test_nested_component_instances_have_unique_context_with_filled_slots(
self, self,
): ):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" """
"{% component_block 'parent_component' %}" {% load component_tags %}{% component_dependencies %}
"{% fill 'content' %}{% component name='variable_display' " {% component 'parent_component' %}
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endfill %}" {% fill 'content' %}
"{% endcomponent_block %}" {% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertIn( self.assertIn("<h1>Uniquely named variable = unique_val</h1>", rendered, rendered)
"<h1>Uniquely named variable = unique_val</h1>", rendered, rendered
)
self.assertIn( self.assertIn(
"<h1>Uniquely named variable = unique_from_slot</h1>", "<h1>Uniquely named variable = unique_from_slot</h1>",
rendered, rendered,
@ -203,180 +156,119 @@ class ContextTests(SimpleTestCase):
): ):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component name='parent_component' %}" "{% component name='parent_component' %}{% endcomponent %}"
)
rendered = template.render(
Context({"shadowing_variable": "NOT SHADOWED"})
) )
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
self.assertIn( self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
"<h1>Shadowing variable = override</h1>", rendered, rendered
)
self.assertIn( self.assertIn(
"<h1>Shadowing variable = slot_default_override</h1>", "<h1>Shadowing variable = slot_default_override</h1>",
rendered, rendered,
rendered, rendered,
) )
self.assertNotIn( self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
"<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
)
def test_nested_component_context_shadows_outer_context_with_filled_slots( def test_nested_component_context_shadows_outer_context_with_filled_slots(
self, self,
): ):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" """
"{% component_block 'parent_component' %}" {% load component_tags %}{% component_dependencies %}
"{% fill 'content' %}{% component name='variable_display' " {% component 'parent_component' %}
"shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}{% endfill %}" {% fill 'content' %}
"{% endcomponent_block %}" {% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
) {% endcomponent %}
rendered = template.render( {% endfill %}
Context({"shadowing_variable": "NOT SHADOWED"}) {% endcomponent %}
""" # NOQA
) )
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
self.assertIn( self.assertIn("<h1>Shadowing variable = override</h1>", rendered, rendered)
"<h1>Shadowing variable = override</h1>", rendered, rendered
)
self.assertIn( self.assertIn(
"<h1>Shadowing variable = shadow_from_slot</h1>", "<h1>Shadowing variable = shadow_from_slot</h1>",
rendered, rendered,
rendered, rendered,
) )
self.assertNotIn( self.assertNotIn("<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered)
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
class ParentArgsTests(SimpleTestCase): class ParentArgsTests(SimpleTestCase):
def test_parent_args_can_be_drawn_from_context(self): def test_parent_args_can_be_drawn_from_context(self):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component_block 'parent_with_args' parent_value=parent_value %}" "{% component 'parent_with_args' parent_value=parent_value %}"
"{% endcomponent_block %}" "{% endcomponent %}"
) )
rendered = template.render(Context({"parent_value": "passed_in"})) rendered = template.render(Context({"parent_value": "passed_in"}))
self.assertIn( self.assertIn("<h1>Shadowing variable = passed_in</h1>", rendered, rendered)
"<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>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): def test_parent_args_available_outside_slots(self):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% 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()) rendered = template.render(Context())
self.assertIn( self.assertIn("<h1>Shadowing variable = passed_in</h1>", rendered, rendered)
"<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>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): def test_parent_args_available_in_slots(self):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" """
"{% component_block 'parent_with_args' parent_value='passed_in' %}" {% load component_tags %}{% component_dependencies %}
"{% fill 'content' %}{% component name='variable_display' " {% component 'parent_with_args' parent_value='passed_in' %}
"shadowing_variable='value_from_slot' new_variable=inner_parent_value %}{% endfill %}" {% fill 'content' %}
"{%endcomponent_block %}" {% component name='variable_display' shadowing_variable='value_from_slot' new_variable=inner_parent_value %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
) )
rendered = template.render(Context()) rendered = template.render(Context())
self.assertIn( self.assertIn("<h1>Shadowing variable = value_from_slot</h1>", rendered, rendered)
"<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>Uniquely named variable = passed_in</h1>", rendered, rendered
)
self.assertNotIn(
"<h1>Shadowing variable = NOT SHADOWED</h1>", rendered, rendered
)
class ContextCalledOnceTests(SimpleTestCase): class ContextCalledOnceTests(SimpleTestCase):
def test_one_context_call_with_simple_component(self): def test_one_context_call_with_simple_component(self):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component name='incrementer' %}" "{% component name='incrementer' %}{% endcomponent %}"
) )
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
self.assertEqual( self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
rendered, '<p class="incrementer">value=1;calls=1</p>', rendered
)
def test_one_context_call_with_simple_component_and_arg(self): def test_one_context_call_with_simple_component_and_arg(self):
template = Template( template = Template("{% load component_tags %}{% component name='incrementer' value='2' %}{% endcomponent %}")
"{% load component_tags %}{% component name='incrementer' value='2' %}"
)
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
self.assertEqual( self.assertEqual(rendered, '<p class="incrementer">value=3;calls=1</p>', rendered)
rendered, '<p class="incrementer">value=3;calls=1</p>', rendered
)
def test_one_context_call_with_component_block(self): def test_one_context_call_with_component(self):
template = Template( template = Template("{% load component_tags %}" "{% component 'incrementer' %}{% endcomponent %}")
"{% load component_tags %}"
"{% component_block 'incrementer' %}{% endcomponent_block %}"
)
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
self.assertEqual( self.assertEqual(rendered, '<p class="incrementer">value=1;calls=1</p>', rendered)
rendered, '<p class="incrementer">value=1;calls=1</p>', rendered
)
def test_one_context_call_with_component_block_and_arg(self): def test_one_context_call_with_component_and_arg(self):
template = Template( template = Template("{% load component_tags %}" "{% component 'incrementer' value='3' %}{% endcomponent %}")
"{% load component_tags %}"
"{% component_block 'incrementer' value='3' %}{% endcomponent_block %}"
)
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
self.assertEqual( self.assertEqual(rendered, '<p class="incrementer">value=4;calls=1</p>', rendered)
rendered, '<p class="incrementer">value=4;calls=1</p>', rendered
)
def test_one_context_call_with_slot(self): def test_one_context_call_with_slot(self):
template = Template( template = Template(
"{% load component_tags %}" "{% load component_tags %}"
"{% component_block 'incrementer' %}{% fill 'content' %}" "{% component 'incrementer' %}{% fill 'content' %}"
"<p>slot</p>{% endfill %}{% endcomponent_block %}" "<p>slot</p>{% endfill %}{% endcomponent %}"
) )
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
@ -389,8 +281,8 @@ class ContextCalledOnceTests(SimpleTestCase):
def test_one_context_call_with_slot_and_arg(self): def test_one_context_call_with_slot_and_arg(self):
template = Template( template = Template(
"{% load component_tags %}" "{% load component_tags %}"
"{% component_block 'incrementer' value='3' %}{% fill 'content' %}" "{% component 'incrementer' value='3' %}{% fill 'content' %}"
"<p>slot</p>{% endfill %}{% endcomponent_block %}" "<p>slot</p>{% endfill %}{% endcomponent %}"
) )
rendered = template.render(Context()).strip() rendered = template.render(Context()).strip()
@ -405,11 +297,9 @@ class ComponentsCanAccessOuterContext(SimpleTestCase):
def test_simple_component_can_use_outer_context(self): def test_simple_component_can_use_outer_context(self):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component 'simple_component' %}" "{% component 'simple_component' %}{% endcomponent %}"
) )
rendered = template.render( rendered = template.render(Context({"variable": "outer_value"})).strip()
Context({"variable": "outer_value"})
).strip()
self.assertIn("outer_value", rendered, rendered) self.assertIn("outer_value", rendered, rendered)
@ -417,21 +307,17 @@ class IsolatedContextTests(SimpleTestCase):
def test_simple_component_can_pass_outer_context_in_args(self): def test_simple_component_can_pass_outer_context_in_args(self):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component 'simple_component' variable only %}" "{% component 'simple_component' variable only %}{% endcomponent %}"
) )
rendered = template.render( rendered = template.render(Context({"variable": "outer_value"})).strip()
Context({"variable": "outer_value"})
).strip()
self.assertIn("outer_value", rendered, rendered) self.assertIn("outer_value", rendered, rendered)
def test_simple_component_cannot_use_outer_context(self): def test_simple_component_cannot_use_outer_context(self):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component 'simple_component' only %}" "{% component 'simple_component' only %}{% endcomponent %}"
) )
rendered = template.render( rendered = template.render(Context({"variable": "outer_value"})).strip()
Context({"variable": "outer_value"})
).strip()
self.assertNotIn("outer_value", rendered, rendered) self.assertNotIn("outer_value", rendered, rendered)
@ -452,7 +338,7 @@ class IsolatedContextSettingTests(SimpleTestCase):
): ):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component 'simple_component' variable %}" "{% component 'simple_component' variable %}{% endcomponent %}"
) )
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertIn("outer_value", rendered, rendered) self.assertIn("outer_value", rendered, rendered)
@ -462,29 +348,29 @@ class IsolatedContextSettingTests(SimpleTestCase):
): ):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component 'simple_component' %}" "{% component 'simple_component' %}{% endcomponent %}"
) )
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertNotIn("outer_value", rendered, rendered) 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, self,
): ):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component_block 'simple_component' variable %}" "{% component 'simple_component' variable %}"
"{% endcomponent_block %}" "{% endcomponent %}"
) )
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertIn("outer_value", rendered, rendered) 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, self,
): ):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component_block 'simple_component' %}" "{% component 'simple_component' %}"
"{% endcomponent_block %}" "{% endcomponent %}"
) )
rendered = template.render(Context({"variable": "outer_value"})) rendered = template.render(Context({"variable": "outer_value"}))
self.assertNotIn("outer_value", rendered, rendered) self.assertNotIn("outer_value", rendered, rendered)
@ -494,19 +380,7 @@ class OuterContextPropertyTests(SimpleTestCase):
def test_outer_context_property_with_component(self): def test_outer_context_property_with_component(self):
template = Template( template = Template(
"{% load component_tags %}{% component_dependencies %}" "{% load component_tags %}{% component_dependencies %}"
"{% component 'outer_context_component' only %}" "{% component 'outer_context_component' only %}{% endcomponent %}"
) )
rendered = template.render( rendered = template.render(Context({"variable": "outer_value"})).strip()
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()
self.assertIn("outer_value", rendered, rendered) self.assertIn("outer_value", rendered, rendered)

View file

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

View file

@ -27,9 +27,7 @@ class ComponentRegistryTest(unittest.TestCase):
class TestComponent(component.Component): class TestComponent(component.Component):
pass pass
self.assertEqual( self.assertEqual(component.registry.get("decorated_component"), TestComponent)
component.registry.get("decorated_component"), TestComponent
)
def test_simple_register(self): def test_simple_register(self):
self.registry.register(name="testcomponent", component=MockComponent) 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): def test_prevent_registering_different_components_with_the_same_name(self):
self.registry.register(name="testcomponent", component=MockComponent) self.registry.register(name="testcomponent", component=MockComponent)
with self.assertRaises(component.AlreadyRegistered): with self.assertRaises(component.AlreadyRegistered):
self.registry.register( self.registry.register(name="testcomponent", component=MockComponent2)
name="testcomponent", component=MockComponent2
)
def test_allow_duplicated_registration_of_the_same_component(self): def test_allow_duplicated_registration_of_the_same_component(self):
try: try:
self.registry.register( self.registry.register(name="testcomponent", component=MockComponentView)
name="testcomponent", component=MockComponentView self.registry.register(name="testcomponent", component=MockComponentView)
)
self.registry.register(
name="testcomponent", component=MockComponentView
)
except component.AlreadyRegistered: except component.AlreadyRegistered:
self.fail("Should not raise 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.js"),
os.path.join(self.temp_dir, component_name, "test.css"), 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, "test.html"),
os.path.join( os.path.join(self.temp_dir, component_name, f"{component_name}.py"),
self.temp_dir, component_name, f"{component_name}.py"
),
] ]
for file_path in expected_files: for file_path in expected_files:
self.assertTrue( self.assertTrue(os.path.exists(file_path), f"File {file_path} was not created")
os.path.exists(file_path), f"File {file_path} was not created"
)
def test_dry_run(self): def test_dry_run(self):
component_name = "dryruncomponent" component_name = "dryruncomponent"
@ -80,9 +76,7 @@ class CreateComponentCommandTest(TestCase):
component_path = os.path.join(self.temp_dir, component_name) component_path = os.path.join(self.temp_dir, component_name)
os.makedirs(component_path) os.makedirs(component_path)
with open( with open(os.path.join(component_path, f"{component_name}.py"), "w") as f:
os.path.join(component_path, f"{component_name}.py"), "w"
) as f:
f.write("hello world") f.write("hello world")
call_command( call_command(
@ -93,9 +87,7 @@ class CreateComponentCommandTest(TestCase):
"--force", "--force",
) )
with open( with open(os.path.join(component_path, f"{component_name}.py"), "r") as f:
os.path.join(component_path, f"{component_name}.py"), "r"
) as f:
self.assertNotIn("hello world", f.read()) self.assertNotIn("hello world", f.read())
def test_error_existing_component_no_force(self): def test_error_existing_component_no_force(self):
@ -104,9 +96,7 @@ class CreateComponentCommandTest(TestCase):
os.makedirs(component_path) os.makedirs(component_path)
with self.assertRaises(CommandError): with self.assertRaises(CommandError):
call_command( call_command("startcomponent", component_name, "--path", self.temp_dir)
"startcomponent", component_name, "--path", self.temp_dir
)
def test_verbose_output(self): def test_verbose_output(self):
component_name = "verbosecomponent" 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 # Create middleware instance
response_stash = None response_stash = None
middleware = ComponentDependencyMiddleware( middleware = ComponentDependencyMiddleware(get_response=lambda _: response_stash)
get_response=lambda _: response_stash
)
class Django30CompatibleSimpleTestCase(SimpleTestCase): class Django30CompatibleSimpleTestCase(SimpleTestCase):
@ -19,9 +17,7 @@ class Django30CompatibleSimpleTestCase(SimpleTestCase):
left = left.replace(' type="text/css"', "") left = left.replace(' type="text/css"', "")
right = right.replace(' type="text/javascript"', "") right = right.replace(' type="text/javascript"', "")
right = right.replace(' type="text/css"', "") right = right.replace(' type="text/css"', "")
super(Django30CompatibleSimpleTestCase, self).assertHTMLEqual( super(Django30CompatibleSimpleTestCase, self).assertHTMLEqual(left, right)
left, right
)
def assertInHTML(self, needle, haystack, count=None, msg_prefix=""): def assertInHTML(self, needle, haystack, count=None, msg_prefix=""):
haystack = haystack.replace(' type="text/javascript"', "") haystack = haystack.replace(' type="text/javascript"', "")
@ -37,9 +33,7 @@ request = Mock()
mock_template = Mock() mock_template = Mock()
def create_and_process_template_response( def create_and_process_template_response(template, context=None, use_middleware=True):
template, context=None, use_middleware=True
):
context = context if context is not None else Context({}) context = context if context is not None else Context({})
mock_template.render = lambda context, _: template.render(context) mock_template.render = lambda context, _: template.render(context)
response = TemplateResponse(request, mock_template, context) response = TemplateResponse(request, mock_template, context)

View file

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