diff --git a/dev/example_project/render_load_test.py b/dev/example_project/render_load_test.py index d66b01e..3f503da 100644 --- a/dev/example_project/render_load_test.py +++ b/dev/example_project/render_load_test.py @@ -1,5 +1,6 @@ import time from django.template.loader import render_to_string +from statistics import mean def configure_django(): @@ -19,7 +20,6 @@ def configure_django(): { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": ["example_project/templates"], - # "APP_DIRS": True, "OPTIONS": { "loaders": [ ( @@ -53,62 +53,83 @@ def configure_django(): print(f"DEBUG: {settings.DEBUG}") +def template_bench(template_name, iterations=5000): + start_time = time.time() + for _ in range(iterations): + render_to_string(template_name) + end_time = time.time() + duration = round((end_time - start_time) * 1000, 2) + return duration + + +def template_bench_data_loop(template_name, iterations=5000): + data = list(range(1, iterations)) + start_time = time.time() + render_to_string(template_name, context={"data": data}) + end_time = time.time() + duration = round((end_time - start_time) * 1000, 2) + return duration + + +def run_benchmark(bench_func, template_name, iterations=5000, runs=5): + # Warm up + bench_func(template_name, iterations=1) + + results = [] + for _ in range(runs): + result = bench_func(template_name, iterations) + results.append(result) + + return mean(results) + + def main(): configure_django() - def template_bench(template_name, iterations=5000): - start_time = time.time() - for _ in range(iterations): - render_to_string(template_name) - end_time = time.time() - duration = round((end_time - start_time) * 1000, 2) + runs = 5 + iterations = 5000 - return duration, render_to_string(template_name) + print(f"Running benchmarks with {runs} runs, {iterations} iterations each") - def template_bench_data_loop(template_name, iterations=5000): - data = list(range(1, iterations)) - start_time = time.time() - render_to_string(template_name, context={"data": data}) - end_time = time.time() - duration = round((end_time - start_time) * 1000, 2) - - return duration, render_to_string(template_name) - - # warm caches - template_bench_data_loop("simple_native.html", iterations=1) - template_bench_data_loop("simple_cotton.html", iterations=1) - - simple_native, _ = template_bench_data_loop("simple_native.html") - simple_cotton, _ = template_bench_data_loop("simple_cotton.html") + simple_native = run_benchmark( + template_bench_data_loop, "simple_native.html", iterations, runs + ) + simple_cotton = run_benchmark( + template_bench_data_loop, "simple_cotton.html", iterations, runs + ) print("---") - print(f"Native Django {{% for %}} loop: {simple_native} ms") - print(f"Cotton {{% for %}} loop: {simple_cotton} ms") + print(f"Native Django {{% for %}} loop: {simple_native:.2f} ms") + print(f"Cotton {{% for %}} loop: {simple_cotton:.2f} ms") - # warm caches - template_bench("benchmarks/native_include.html", iterations=1) - template_bench("cotton/benchmarks/cotton_include.html", iterations=1) - - time_native_include, _ = template_bench("benchmarks/native_include.html") - time_cotton_include, _ = template_bench("cotton/benchmarks/cotton_include.html") + time_native_include = run_benchmark( + template_bench, "benchmarks/native_include.html", iterations, runs + ) + time_cotton_include = run_benchmark( + template_bench, "cotton/benchmarks/cotton_include.html", iterations, runs + ) print("---") - print(f"Native {{% include %}}: {time_native_include} ms") - print(f"Cotton for include: {time_cotton_include} ms") + print(f"Native {{% include %}}: {time_native_include:.2f} ms") + print(f"Cotton for include: {time_cotton_include:.2f} ms") - # warm caches - template_bench("benchmarks/native_extends.html", iterations=1) - template_bench("cotton/benchmarks/cotton_compiled.html", iterations=1) - template_bench("cotton/benchmarks/cotton_extends_equivalent.html", iterations=1) - - time_native_extends, _ = template_bench("benchmarks/native_extends.html") - time_compiled_cotton, _ = template_bench("cotton/benchmarks/cotton_compiled.html") - time_cotton, _ = template_bench("cotton/benchmarks/cotton_extends_equivalent.html") + time_native_extends = run_benchmark( + template_bench, "benchmarks/native_extends.html", iterations, runs + ) + time_compiled_cotton = run_benchmark( + template_bench, "cotton/benchmarks/cotton_compiled.html", iterations, runs + ) + time_cotton = run_benchmark( + template_bench, + "cotton/benchmarks/cotton_extends_equivalent.html", + iterations, + runs, + ) print("---") - print(f"Native {{% block %}} and {{% extends %}}: {time_native_extends} ms") - print(f"Compiled Cotton Template: {time_compiled_cotton} ms") - print(f"Uncompiled Cotton Template: {time_cotton} ms") + print(f"Native {{% block %}} and {{% extends %}}: {time_native_extends:.2f} ms") + print(f"Compiled Cotton Template: {time_compiled_cotton:.2f} ms") + print(f"Uncompiled Cotton Template: {time_cotton:.2f} ms") if __name__ == "__main__": diff --git a/django_cotton/cotton_loader.py b/django_cotton/cotton_loader.py index 4060b67..fd12993 100755 --- a/django_cotton/cotton_loader.py +++ b/django_cotton/cotton_loader.py @@ -38,7 +38,7 @@ class Loader(BaseLoader): if " tag, wrap content with {% cotton_vars_frame %} to be able to create and govern vars and attributes. To be able to defined new vars within a component and also have them available in the same component's context, we wrap the entire contents in another component: cotton_vars_frame. Only when is present.""" - - # create an attribute string from the cvars_el attrs. i.e. attr1 attr2="value" etc - # - this will be used to create the cvars tag cvars_attrs_string = " ".join(f'{k}="{v}"' for k, v in cvars_el.attrs.items()) - - # cvars = [] - # for var, value in cvars_el.attrs.items(): - # Attributes in context at this point will already have been formatted in _component to be accessible, so in order to cascade match the style. - # accessible_var = var.replace("-", "_") - - # using default filter is limiting our ability to override when the subject value is present but nor processable. - # - carry this functionality to the cvars tag - # - then we can choose to display the cvar value if one from parent is not processable (as well as not present etc) - - # cvars.append() - - # if value is None: - # vars_with_defaults.append(f"{var}={accessible_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 - # accessible_var = accessible_var[1:] # Remove the ':' prefix - # vars_with_defaults.append(f'{var}={accessible_var}|eval_default:"{value}"') - # else: - # # Assuming value is already a string that represents the default value - # vars_with_defaults.append(f'{var}={accessible_var}|default:"{value}"') - cvars_el.decompose() - - # Construct the {% with %} opening tag opening = f"{{% cvars {cvars_attrs_string} %}}" opening = opening.replace("\n", "") closing = "{% endcvars %}" @@ -269,21 +242,9 @@ class CottonCompiler: + str(soup.encode(formatter=UnsortedAttributes()).decode("utf-8")).strip() + closing ) - - # Since we can't replace the soup object itself, we create new soup instead new_soup = self._make_soup(wrapped_content) - return new_soup - # promote_cvars() (stack[-2], throws CvarsMustBeInComponentError) - # soup[stack[-2]].add_attrs(v) - # - # - # - # - # - # - def _transform_components(self, soup): """Replace tags with the {% cotton_component %} template tag""" for tag in soup.find_all(re.compile("^c-"), recursive=True): diff --git a/django_cotton/templatetags/_context_models.py b/django_cotton/templatetags/_context_models.py index baf70c7..47ea007 100644 --- a/django_cotton/templatetags/_context_models.py +++ b/django_cotton/templatetags/_context_models.py @@ -1,6 +1,6 @@ import ast from collections.abc import Mapping -from typing import Set, Any +from typing import Set, Any, Dict, List from django.template import Variable, TemplateSyntaxError, Context from django.template.base import VariableDoesNotExist, Template @@ -39,9 +39,7 @@ class DynamicAttr: raise UnprocessableDynamicAttr def _resolve_as_variable(self, context): - if not self._is_cvar: - return Variable(self.value).resolve(context) - raise VariableDoesNotExist(self.value) + return Variable(self.value).resolve(context) def _resolve_as_boolean(self, _): if self.value == "": @@ -64,18 +62,17 @@ class DynamicAttr: class Attrs(Mapping): - def __init__(self, attrs): + def __init__(self, attrs: Dict[str, Any]): self._attrs = attrs self._exclude_from_str: Set[str] = set() - self._unprocessable = [] + self._unprocessable: List[str] = [] def __str__(self): - output = mark_safe( + return mark_safe( " ".join( f'{k}="{v}"' for k, v in self._attrs.items() if k not in self._exclude_from_str ) ) - return output def __getitem__(self, key): return self._attrs[key] @@ -89,12 +86,6 @@ class Attrs(Mapping): def __len__(self): return len(self._attrs) - def __contains__(self, key): - return key in self._attrs - - def get(self, key, default=None): - return self._attrs.get(key, default) - def items(self): return self._attrs.items() @@ -119,5 +110,4 @@ class Attrs(Mapping): self._exclude_from_str.add(key) def make_attrs_accessible(self): - """Converts hyphens in attr names to underscores""" return {k.replace("-", "_"): v for k, v in self._attrs.items()} diff --git a/django_cotton/tests/utils.py b/django_cotton/tests/utils.py index 3d96fc1..3c2421e 100644 --- a/django_cotton/tests/utils.py +++ b/django_cotton/tests/utils.py @@ -102,7 +102,7 @@ class CottonTestCase(TestCase): def get_compiled(template_string): - return CottonLoader(engine=None).cotton_compiler.process(template_string, "test_key") + return CottonLoader(engine=None).cotton_compiler.process(template_string) def get_rendered(template_string, context: dict = None):