Skip to content

Commit

Permalink
Merge pull request #39 from wrabit/optimise_cvars
Browse files Browse the repository at this point in the history
Optimise cvars
  • Loading branch information
wrabit authored Jul 5, 2024
2 parents 40ca698 + 987c0d5 commit 87c337c
Show file tree
Hide file tree
Showing 10 changed files with 49 additions and 52 deletions.
2 changes: 0 additions & 2 deletions dev/example_project/example_project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,5 +132,3 @@
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# COTTON_TEMPLATE_CACHING_ENABLED = False
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "cotton/benchmarks/partials/native_main.html" %}
{% extends "benchmarks/native_main.html" %}

{% block top %}
I'm top
Expand Down
12 changes: 1 addition & 11 deletions dev/example_project/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
[project]
name = "cotton-dev-app"
requires-python = ">=3.8, <4"
dependencies = [
"django~=4.2.6",
"beautifulsoup4~=4.12.2",
"selenium~=4.13.0",
"chromedriver-py~=117.0.5938.92",
"webdriver-manager~=4.0.1"
]

[tool.poetry]
name = "cotton-dev-app"
Expand All @@ -18,7 +11,4 @@ authors = ["Will Abbott <willabb83@gmail.com>"]
[tool.poetry.dependencies]
python = "^3.8"
Django = "^4.2"
beautifulsoup4 = "~4.12.2"
selenium = "~4.13.0"
chromedriver-py = "~117.0.5938.92"
webdriver-manager = "~4.0.1"
beautifulsoup4 = "~4.12.2"
4 changes: 2 additions & 2 deletions dev/example_project/render_load_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
django.setup()


def benchmark_template_rendering(template_name, iterations=1000):
def benchmark_template_rendering(template_name, iterations=10000):
start_time = time.time()
for _ in range(iterations):
render_to_string(template_name)
Expand All @@ -56,7 +56,7 @@ def benchmark_template_rendering(template_name, iterations=1000):

# Benchmarking each template
time_native_extends, output_native_extends = benchmark_template_rendering(
"cotton/benchmarks/native_extends.html"
"benchmarks/native_extends.html"
)
time_compiled_cotton, output_compiled_cotton = benchmark_template_rendering(
"cotton/benchmarks/cotton_compiled.html"
Expand Down
42 changes: 19 additions & 23 deletions django_cotton/cotton_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ def get_template_sources(self, template_name):
class UnsortedAttributes(HTMLFormatter):
def attributes(self, tag):
for k, v in tag.attrs.items():
# remove any new lines in v
yield k, v


Expand Down Expand Up @@ -139,9 +138,10 @@ def _compile_cotton_to_django(self, html_content, component_key):
"""Convert cotton <c-* syntax to {%."""
soup = BeautifulSoup(html_content, "html.parser")

# TODO: Performance optimisation - Make vars_frame optional, only adding it when the user actually provided
# vars in a component
soup = self._wrap_with_cotton_vars_frame(soup)
# check if soup contains a 'c-vars' tag
if cvars_el := soup.find("c-vars"):
soup = self._wrap_with_cotton_vars_frame(soup, cvars_el)

self._transform_components(soup, component_key)

return str(soup.encode(formatter=UnsortedAttributes()).decode("utf-8"))
Expand Down Expand Up @@ -174,29 +174,25 @@ def _revert_bs4_attribute_empty_attribute_fixing(self, contents):

return contents

def _wrap_with_cotton_vars_frame(self, soup):
def _wrap_with_cotton_vars_frame(self, soup, cvars_el):
"""Wrap content with {% cotton_vars_frame %} to be able to govern vars and attributes. In order to recognise
vars defined in a component and also have them available in the same component's context, we wrap the entire
contents in another component: cotton_vars_frame."""

vars_with_defaults = []
c_vars = soup.find("c-vars")

# parse c-vars tag to extract variables and defaults
if c_vars:
vars_with_defaults = []
for var, value in c_vars.attrs.items():
if value is None:
vars_with_defaults.append(f"{var}={var}")
elif var.startswith(":"):
# If ':' is present, the user wants to parse a literal string as the default value,
# i.e. "['a', 'b']", "{'a': 'b'}", "True", "False", "None" or "1".
var = var[1:] # Remove the ':' prefix
vars_with_defaults.append(f'{var}={var}|eval_default:"{value}"')
else:
# Assuming value is already a string that represents the default value
vars_with_defaults.append(f'{var}={var}|default:"{value}"')

c_vars.decompose()
for var, value in cvars_el.attrs.items():
if value is None:
vars_with_defaults.append(f"{var}={var}")
elif var.startswith(":"):
# If ':' is present, the user wants to parse a literal string as the default value,
# i.e. "['a', 'b']", "{'a': 'b'}", "True", "False", "None" or "1".
var = var[1:] # Remove the ':' prefix
vars_with_defaults.append(f'{var}={var}|eval_default:"{value}"')
else:
# Assuming value is already a string that represents the default value
vars_with_defaults.append(f'{var}={var}|default:"{value}"')

cvars_el.decompose()

# Construct the {% with %} opening tag
opening = "{% cotton_vars_frame " + " ".join(vars_with_defaults) + " %}"
Expand Down
20 changes: 15 additions & 5 deletions django_cotton/templatetags/_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from django import template
from django.template import Node
from django.template.loader import render_to_string
from django.utils.safestring import mark_safe

from django_cotton.utils import ensure_quoted


def cotton_component(parser, token):
Expand Down Expand Up @@ -42,7 +45,7 @@ def render(self, context):
value = value.strip("'\"")

if key.startswith(":"):
key = key[1:] # Remove ':' prefix
key = key[1:]
attrs[key] = self.process_dynamic_attribute(value, context)
elif value == "":
attrs[key] = True
Expand All @@ -63,13 +66,19 @@ def render(self, context):
for expression_attr in component_slots["ctn_template_expression_attrs"]:
attrs[expression_attr] = component_slots[expression_attr]

# Make the attrs available in the context for the vars frame
local_context["attrs_dict"] = attrs

# Reset the component's slots in context to prevent bleeding into sibling components
if self.component_key in all_slots:
all_slots[self.component_key] = {}
all_slots[self.component_key] = {}

# Provide all of the attrs as a string to pass to the component
local_context.update(attrs)
attrs_string = " ".join(
f"{key}={ensure_quoted(value)}" for key, value in attrs.items()
)
local_context["attrs"] = mark_safe(attrs_string)

context.update({"cotton_slots": all_slots})
return render_to_string(self.template_path, local_context)

def process_dynamic_attribute(self, value, context):
Expand All @@ -85,7 +94,8 @@ def process_dynamic_attribute(self, value, context):
if value == "":
return True

# Evaluate literal string or pass back raw value
# It's not a template var or boolean attribute,
# attempt to evaluate literal string or pass back raw value
try:
return ast.literal_eval(value)
except (ValueError, SyntaxError):
Expand Down
11 changes: 3 additions & 8 deletions django_cotton/templatetags/_vars_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from django.template.base import token_kwargs
from django.utils.safestring import mark_safe

from django_cotton.utils import ensure_quoted

register = template.Library()


Expand Down Expand Up @@ -46,21 +48,14 @@ def render(self, context):
context["attrs_dict"] = attrs_without_vars

# Provide all of the attrs as a string to pass to the component
def ensure_quoted(value):
if isinstance(value, str) and value.startswith('"') and value.endswith('"'):
return value
else:
return f'"{value}"'

attrs = " ".join(
[
f"{key}={ensure_quoted(value)}"
for key, value in attrs_without_vars.items()
]
)

context.update({"attrs": mark_safe(attrs)})
context.update(attrs_without_vars)
context["attrs"] = mark_safe(attrs)
context.update(vars)

return self.nodelist.render(context)
1 change: 1 addition & 0 deletions django_cotton/tests/test_cotton.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def test_new_lines_in_attributes_are_preserved(self):
# Override URLconf
with self.settings(ROOT_URLCONF=self.get_url_conf()):
response = self.client.get("/view/")

self.assertTrue(
"""{
attr1: 'im an attr',
Expand Down
7 changes: 7 additions & 0 deletions django_cotton/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ def eval_string(value):
return ast.literal_eval(value)
except (ValueError, SyntaxError):
return value


def ensure_quoted(value):
if isinstance(value, str) and value.startswith('"') and value.endswith('"'):
return value
else:
return f'"{value}"'

0 comments on commit 87c337c

Please sign in to comment.