removed cotton copy

This commit is contained in:
Will Abbott 2024-07-09 23:09:29 +01:00
parent 419292664c
commit c3431c6241
40 changed files with 0 additions and 1234 deletions

View file

@ -1,304 +0,0 @@
import warnings
import hashlib
import os
import re
from django.template.loaders.base import Loader as BaseLoader
from django.core.exceptions import SuspiciousFileOperation
from django.template import TemplateDoesNotExist
from bs4.formatter import HTMLFormatter
from django.utils._os import safe_join
from django.template import Template
from django.core.cache import cache
from django.template import Origin
from django.conf import settings
from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
class Loader(BaseLoader):
is_usable = True
def __init__(self, engine, dirs=None):
super().__init__(engine)
self.cache_handler = CottonTemplateCacheHandler()
self.template_processor = CottonTemplateProcessor()
self.dirs = dirs
def get_contents(self, origin):
# check if file exists, whilst getting the mtime for cache key
try:
mtime = os.path.getmtime(origin.name)
except FileNotFoundError:
raise TemplateDoesNotExist(origin)
cache_key = self.cache_handler.get_cache_key(origin.template_name, mtime)
cached_content = self.cache_handler.get_cached_template(cache_key)
if cached_content is not None:
return cached_content
template_string = self._get_template_string(origin.name)
compiled_template = self.template_processor.process(
template_string, origin.template_name
)
self.cache_handler.cache_template(cache_key, compiled_template)
return compiled_template
def get_template_from_string(self, template_string):
"""Create and return a Template object from a string. Used primarily for testing."""
return Template(template_string, engine=self.engine)
def _get_template_string(self, template_name):
try:
with open(template_name, "r") as f:
return f.read()
except FileNotFoundError:
raise TemplateDoesNotExist(template_name)
def get_dirs(self):
return self.dirs if self.dirs is not None else self.engine.dirs
def get_template_sources(self, template_name):
"""Return an Origin object pointing to an absolute path in each directory
in template_dirs. For security reasons, if a path doesn't lie inside
one of the template_dirs it is excluded from the result set."""
for template_dir in self.get_dirs():
try:
name = safe_join(template_dir, template_name)
except SuspiciousFileOperation:
# The joined path was located outside of this template_dir
# (it might be inside another one, so this isn't fatal).
continue
yield Origin(
name=name,
template_name=template_name,
loader=self,
)
class UnsortedAttributes(HTMLFormatter):
"""This keeps BS4 from re-ordering attributes"""
def attributes(self, tag):
for k, v in tag.attrs.items():
yield k, v
class CottonTemplateProcessor:
DJANGO_SYNTAX_PLACEHOLDER_PREFIX = "__django_syntax__"
COTTON_VERBATIM_PATTERN = re.compile(
r"\{% cotton_verbatim %\}(.*?)\{% endcotton_verbatim %\}", re.DOTALL
)
DJANGO_TAG_PATTERN = re.compile(r"\{%.*?%\}")
DJANGO_VAR_PATTERN = re.compile(r"\{\{.*?\}\}")
def __init__(self):
self.django_syntax_placeholders = []
def process(self, content, component_key):
content = self._replace_syntax_with_placeholders(content)
content = self._compile_cotton_to_django(content, component_key)
content = self._replace_placeholders_with_syntax(content)
return self._revert_bs4_attribute_empty_attribute_fixing(content)
def _replace_syntax_with_placeholders(self, content):
"""# replace {% ... %} and {{ ... }} with placeholders so they dont get touched
or encoded by bs4. Store them to later switch them back in after transformation.
"""
self.django_syntax_placeholders = []
def replace_pattern(pattern, replacement_func):
return pattern.sub(replacement_func, content)
def replace_cotton_verbatim(match):
inner_content = match.group(1)
self.django_syntax_placeholders.append(inner_content)
return f"{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{len(self.django_syntax_placeholders)}__"
def replace_django_syntax(match):
self.django_syntax_placeholders.append(match.group(0))
return f"{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{len(self.django_syntax_placeholders)}__"
# Replace cotton_verbatim blocks
content = replace_pattern(self.COTTON_VERBATIM_PATTERN, replace_cotton_verbatim)
# Replace {% ... %}
content = replace_pattern(self.DJANGO_TAG_PATTERN, replace_django_syntax)
# Replace {{ ... }}
content = replace_pattern(self.DJANGO_VAR_PATTERN, replace_django_syntax)
return content
def _compile_cotton_to_django(self, html_content, component_key):
"""Convert cotton <c-* syntax to {%."""
soup = BeautifulSoup(html_content, "html.parser")
# 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"))
def _replace_placeholders_with_syntax(self, content):
"""After modifying the content, replace the placeholders with the django template tags and variables."""
for i, placeholder in enumerate(self.django_syntax_placeholders, 1):
content = content.replace(
f"{self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX}{i}__", placeholder
)
return content
def _revert_bs4_attribute_empty_attribute_fixing(self, contents):
"""
Removes empty attribute values added by BeautifulSoup to Django template tags.
BeautifulSoup adds ="" to empty attribute-like parts in HTML-like nodes.
This method removes these additions for Django template tags.
Examples:
- <div {{ something }}=""> becomes <div {{ something }}>
- <div {% something %}=""> becomes <div {% something %}>
"""
# Remove ="" after Django variable tags
contents = contents.replace('}}=""', "}}")
# Remove ="" after Django template tags
contents = contents.replace('%}=""', "%}")
return contents
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 = []
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("-", "_")
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 = "{% cotton_vars_frame " + " ".join(vars_with_defaults) + " %}"
closing = "{% endcotton_vars_frame %}"
# Convert the remaining soup back to a string and wrap it within {% with %} block
wrapped_content = (
opening
+ 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 = BeautifulSoup(wrapped_content, "html.parser")
return new_soup
def _transform_components(self, soup, parent_key):
"""Replace <c-[component path]> tags with the {% cotton_component %} template tag"""
for tag in soup.find_all(re.compile("^c-"), recursive=True):
if tag.name == "c-slot":
self._transform_named_slot(tag, parent_key)
continue
component_key = tag.name[2:]
component_path = component_key.replace(".", "/").replace("-", "_")
opening_tag = f"{{% cotton_component {'{}/{}.html'.format(settings.COTTON_DIR if hasattr(settings, 'COTTON_DIR') else 'cotton', component_path)} {component_key} "
# Store attributes that contain template expressions, they are when we use '{{' or '{%' in the value of an attribute
expression_attrs = []
# Build the attributes
for key, value in tag.attrs.items():
# BS4 stores class values as a list, so we need to join them back into a string
if key == "class":
value = " ".join(value)
# Django templates tags cannot have {{ or {% expressions in their attribute values
# Neither can they have new lines, let's treat them both as "expression attrs"
if self.DJANGO_SYNTAX_PLACEHOLDER_PREFIX in value or "\n" in value:
expression_attrs.append((key, value))
continue
opening_tag += ' {}="{}"'.format(key, value)
opening_tag += " %}"
component_tag = opening_tag
if expression_attrs:
for key, value in expression_attrs:
component_tag += f"{{% cotton_slot {key} {component_key} expression_attr %}}{value}{{% end_cotton_slot %}}"
if tag.contents:
tag_soup = BeautifulSoup(tag.decode_contents(), "html.parser")
self._transform_components(tag_soup, component_key)
component_tag += str(
tag_soup.encode(formatter=UnsortedAttributes()).decode("utf-8")
)
component_tag += "{% end_cotton_component %}"
# Replace the original tag with the compiled django syntax
new_soup = BeautifulSoup(component_tag, "html.parser")
tag.replace_with(new_soup)
return soup
def _transform_named_slot(self, slot_tag, component_key):
"""Compile <c-slot> to {% cotton_slot %}"""
slot_name = slot_tag.get("name", "").strip()
inner_html = "".join(str(content) for content in slot_tag.contents)
# Check and process any components in the slot content
slot_soup = BeautifulSoup(inner_html, "html.parser")
self._transform_components(slot_soup, component_key)
cotton_slot_tag = f"{{% cotton_slot {slot_name} {component_key} %}}{str(slot_soup.encode(formatter=UnsortedAttributes()).decode('utf-8'))}{{% end_cotton_slot %}}"
slot_tag.replace_with(BeautifulSoup(cotton_slot_tag, "html.parser"))
class CottonTemplateCacheHandler:
"""Handles caching of cotton templates so the html parsing is only done on first load of each view or component."""
def __init__(self):
self.enabled = getattr(settings, "COTTON_TEMPLATE_CACHING_ENABLED", True)
def get_cache_key(self, template_name, mtime):
template_hash = hashlib.sha256(template_name.encode()).hexdigest()
return f"cotton_cache_{template_hash}_{mtime}"
def get_cached_template(self, cache_key):
if not self.enabled:
return None
return cache.get(cache_key)
def cache_template(self, cache_key, content, timeout=None):
if self.enabled:
cache.set(cache_key, content, timeout=timeout)

View file

@ -1,3 +0,0 @@
<c-merges-attributes class="extra-class" silica:model="test" another="test">
ss
</c-merges-attributes>

View file

@ -1,3 +0,0 @@
<c-receives-attributes attribute_1="hello" and-another="woo1" thirdForLuck="yes">
ss
</c-receives-attributes>

View file

@ -1,3 +0,0 @@
<c-parent>
<c-child>d</c-child>
</c-parent>

View file

@ -1 +0,0 @@
<div class="i-am-child"></div>

View file

@ -1,11 +0,0 @@
<div>
Header:
{{ header }}
</div>
<div>
Content:
{{ slot }}
</div>

View file

@ -1,27 +0,0 @@
{% if none is None %}
<p>none is None</p>
{% endif %}
{% if number == 1 %}
<p>number is 1</p>
{% endif %}
{% if boolean_true is True %}
<p>boolean_true is True</p>
{% endif %}
{% if boolean_false is False %}
<p>boolean_false is False</p>
{% endif %}
{% if dict.key == 'value' %}
<p>dict.key is 'value'</p>
{% endif %}
{% if list.0 == 1 %}
<p>list.0 is 1</p>
{% endif %}
{% if listdict.0.key == 'value' %}
<p>listdict.0.key is 'value'</p>
{% endif %}

View file

@ -1,29 +0,0 @@
<c-vars :none="None" :number="1" :boolean_true="True" :boolean_false="False" :dict="{'key': 'value'}" :list="[1, 2, 3]" :listdict="[{'key': 'value'}]" />
{% if none is None %}
<p>none is None</p>
{% endif %}
{% if number == 1 %}
<p>number is 1</p>
{% endif %}
{% if boolean_true is True %}
<p>boolean_true is True</p>
{% endif %}
{% if boolean_false is False %}
<p>boolean_false is False</p>
{% endif %}
{% if dict.key == 'value' %}
<p>dict.key is 'value'</p>
{% endif %}
{% if list.0 == 1 %}
<p>list.0 is 1</p>
{% endif %}
{% if listdict.0.key == 'value' %}
<p>listdict.0.key is 'value'</p>
{% endif %}

View file

@ -1,3 +0,0 @@
<div {{ attrs_dict|merge:'class:form-group another-class-with:colon' }}>
</div>

View file

@ -1,3 +0,0 @@
<div>
{{ name }}
</div>

View file

@ -1,4 +0,0 @@
Attribute 1 says: '{{ attr1 }}'
Attribute 2 says: '{{ attr2 }}'
Attribute 3 says: '{{ attr3 }}'
attrs tag is: '{{ attrs }}'

View file

@ -1,3 +0,0 @@
<div class="i-am-parent">
{{slot}}
</div>

View file

@ -1,3 +0,0 @@
<div {{ attrs }}>
</div>

View file

@ -1,13 +0,0 @@
<c-vars var1 default_var="default var" />
<p>slot: '{{ slot }}'</p>
<p>attr1: '{{ attr1 }}'</p>
<p>attr2: '{{ attr2 }}'</p>
<p>var1: '{{ var1 }}'</p>
<p>default_var: '{{ default_var }}'</p>
<p>named_slot: '{{ named_slot }}'</p>
<p>attrs: '{{ attrs }}'</p>

View file

@ -1,3 +0,0 @@
{% if attr1 is True %}
It's True
{% endif %}

View file

@ -1 +0,0 @@
<div class="{% if 1 < 2 %} some-class {% endif %}">Hello, World!</div>

View file

@ -1,9 +0,0 @@
<c-eval-attributes-test-component
:none="None"
:number="1"
:boolean_true="True"
:boolean_false="False"
:dict="{'key': 'value'}"
:list="[1, 2, 3]"
:listdict="[{'key': 'value'}]"
/>

View file

@ -1 +0,0 @@
<c-eval-vars-test-component />

View file

@ -1,3 +0,0 @@
<c-parent>
<c-forms.input name="test" style="width: 100%" silica:model="first_name"/>
</c-parent>

View file

@ -1,7 +0,0 @@
{% for item in items %}
<c-named-slot-component>
<c-slot name="name">
item name: {{ item.name }}
</c-slot>
</c-named-slot-component>
{% endfor %}

View file

@ -1,12 +0,0 @@
<c-vars test="world" name="Will"></c-vars>
<!-- create different types of native tag examples -->
<c-native-tags-in-attributes
attr1="Hello {{ name }}"
attr2="{{ test|default:"none" }}"
attr3="{% if 1 == 1 %}cowabonga!{% endif %}"
normal="normal"
>
<c-slot name="named">test</c-slot>
</c-native-tags-in-attributes>

View file

@ -1 +0,0 @@
<c-parent></c-parent>

View file

@ -1,4 +0,0 @@
{% load static %}
<c-parent/>

View file

@ -1,5 +0,0 @@
<c-test-component var1="string with space" attr1="I have spaces">
<c-slot name="named_slot">
named_slot with spaces
</c-slot>
</c-test-component>

View file

@ -1 +0,0 @@
<c-valueless-attribute-test-component attr1 />

View file

@ -1,2 +0,0 @@
<c-test-component attr1="variable" :attr2="variable">
</c-test-component>

View file

@ -1,115 +0,0 @@
import ast
from functools import lru_cache
from django import template
from django.template import Node
from django.template.loader import get_template
from django.utils.safestring import mark_safe
from django_cotton.utils import ensure_quoted
@lru_cache(maxsize=128)
def get_cached_template(template_name):
return get_template(template_name)
def render_template(template_name, context):
template = get_cached_template(template_name)
return template.render(context)
def cotton_component(parser, token):
"""
Template tag to render a cotton component with dynamic attributes.
Usage:
{% cotton_component 'template_path' 'component_key' key1="value1" :key2="dynamic_value" %}
"""
bits = token.split_contents()
template_path = bits[1]
component_key = bits[2]
kwargs = {}
for bit in bits[3:]:
key, value = bit.split("=")
kwargs[key] = value
nodelist = parser.parse(("end_cotton_component",))
parser.delete_first_token()
return CottonComponentNode(nodelist, template_path, component_key, kwargs)
class CottonComponentNode(Node):
def __init__(self, nodelist, template_path, component_key, kwargs):
self.nodelist = nodelist
self.template_path = template_path
self.component_key = component_key
self.kwargs = kwargs
def render(self, context):
local_context = context.flatten()
attrs = {}
for key, value in self.kwargs.items():
value = value.strip("'\"")
if key.startswith(":"):
key = key[1:]
attrs[key] = self.process_dynamic_attribute(value, context)
elif value == "":
attrs[key] = True
else:
attrs[key] = value
# Add the remainder as the default slot
local_context["slot"] = self.nodelist.render(context)
# Merge slots and attributes into the local context
all_slots = context.get("cotton_slots", {})
component_slots = all_slots.get(self.component_key, {})
local_context.update(component_slots)
# We need to check if any dynamic attributes are present in the component slots and move them over to attrs
if "ctn_template_expression_attrs" in component_slots:
for expression_attr in component_slots["ctn_template_expression_attrs"]:
attrs[expression_attr] = component_slots[expression_attr]
# Build attrs string before formatting any '-' to '_' in attr names
attrs_string = " ".join(
f"{key}={ensure_quoted(value)}" for key, value in attrs.items()
)
local_context["attrs"] = mark_safe(attrs_string)
# Make the attrs available in the context for the vars frame, also before formatting the attr names
local_context["attrs_dict"] = attrs
# Store attr names in a callable format, i.e. 'x-init' will be accessible by {{ x_init }} when called explicitly and not in {{ attrs }}
attrs = {key.replace("-", "_"): value for key, value in attrs.items()}
local_context.update(attrs)
# Reset the component's slots in context to prevent bleeding into sibling components
all_slots[self.component_key] = {}
return render_template(self.template_path, local_context)
def process_dynamic_attribute(self, value, context):
"""
Process a dynamic attribute, resolving template variables and evaluating literals.
"""
try:
return template.Variable(value).resolve(context)
except template.VariableDoesNotExist:
pass
# Check for boolean attribute
if value == "":
return True
# 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):
return value

View file

@ -1,57 +0,0 @@
from django import template
from django.utils.safestring import mark_safe
def cotton_slot(parser, token):
"""
Template tag to render a cotton slot.
is_expression_attr: bool whether the attribute is dynamic or not.
dynamic attributes are from the use of native tags {{ {% in the component attribute. We put them through as a named
slot so they can be rendered and provided as a template variable
"""
try:
tag_name, slot_name, component_key, *optional = token.split_contents()
is_expression_attr = optional[0] if optional else None
except ValueError:
raise template.TemplateSyntaxError("incomplete c-slot %r" % token.contents)
nodelist = parser.parse(("end_cotton_slot",))
parser.delete_first_token()
return CottonSlotNode(slot_name, nodelist, component_key, is_expression_attr)
class CottonSlotNode(template.Node):
def __init__(self, slot_name, nodelist, component_key, is_expression_attr):
self.slot_name = slot_name
self.nodelist = nodelist
self.component_key = component_key
self.is_expression_attr = is_expression_attr
def render(self, context):
# Add the rendered content to the context.
if "cotton_slots" not in context:
context.update({"cotton_slots": {}})
output = self.nodelist.render(context)
# Store the slot data in a component-namespaced dictionary
if self.component_key not in context["cotton_slots"]:
context["cotton_slots"][self.component_key] = {}
context["cotton_slots"][self.component_key][self.slot_name] = mark_safe(output)
# If the slot is a dynamic attribute, we record it so it can be transferred to attrs in the component
if self.is_expression_attr:
if (
"ctn_template_expression_attrs"
not in context["cotton_slots"][self.component_key]
):
context["cotton_slots"][self.component_key][
"ctn_template_expression_attrs"
] = []
context["cotton_slots"][self.component_key][
"ctn_template_expression_attrs"
].append(self.slot_name)
return ""

View file

@ -1,65 +0,0 @@
from django import template
from django.utils.safestring import mark_safe
from django_cotton.utils import ensure_quoted
register = template.Library()
def cotton_vars_frame(parser, token):
"""The job of the vars frame is:
1. to filter out attributes declared as vars inside {{ attrs }} string.
2. to provide default values to attributes.
Because we're effecting variables inside the same component, which is not possible usually, we we wrap
the vars frame around the contents of the component so we can govern the attributes and vars that are available.
"""
bits = token.split_contents()[1:] # Skip the tag name
# We dont use token_kwargs because it doesn't allow for hyphens in key names, i.e. x-data=""
tag_kwargs = {}
for bit in bits:
key, value = bit.split("=")
tag_kwargs[key] = parser.compile_filter(value)
nodelist = parser.parse(("endcotton_vars_frame",))
parser.delete_first_token()
return CottonVarsFrameNode(nodelist, tag_kwargs)
class CottonVarsFrameNode(template.Node):
def __init__(self, nodelist, kwargs):
self.nodelist = nodelist
self.kwargs = kwargs
def render(self, context):
# Assume 'attrs' are passed from the parent and are available in the context
component_attrs = context.get("attrs_dict", {})
# Initialize vars based on the frame's kwargs and parent attrs
vars = {}
for key, value in self.kwargs.items():
# Check if the var exists in component attrs; if so, use it, otherwise use the resolved default
if key in component_attrs:
vars[key] = component_attrs[key]
else:
# Attempt to resolve each kwarg value (which may include template variables)
resolved_value = value.resolve(context)
vars[key] = resolved_value
# Overwrite 'attrs' in the local context by excluding keys that are identified as vars
attrs_without_vars = {k: v for k, v in component_attrs.items() if k not in vars}
# Provide all of the attrs as a string to pass to the component before any '-' to '_' replacing
attrs = " ".join(
f"{key}={ensure_quoted(value)}" for key, value in attrs_without_vars.items()
)
context["attrs"] = mark_safe(attrs)
context["attrs_dict"] = attrs_without_vars
# Store attr names in a callable format, i.e. 'x-init' will be accessible by {{ x_init }} when called explicitly and not in {{ attrs }}
formatted_vars = {key.replace("-", "_"): value for key, value in vars.items()}
context.update(formatted_vars)
return self.nodelist.render(context)

View file

@ -1,30 +0,0 @@
from django import template
from django.utils.html import format_html_join
from django_cotton.templatetags._component import cotton_component
from django_cotton.templatetags._slot import cotton_slot
from django_cotton.templatetags._vars_frame import cotton_vars_frame
from django_cotton.utils import eval_string
register = template.Library()
register.tag("cotton_component", cotton_component)
register.tag("cotton_slot", cotton_slot)
register.tag("cotton_vars_frame", cotton_vars_frame)
@register.filter
def merge(attrs, args):
# attrs is expected to be a dictionary of existing attributes
# args is a string of additional attributes to merge, e.g., "class:extra-class"
for arg in args.split(","):
key, value = arg.split(":", 1)
if key in attrs:
attrs[key] = value + " " + attrs[key]
else:
attrs[key] = value
return format_html_join(" ", '{0}="{1}"', attrs.items())
@register.filter
def eval_default(value, arg):
return value or eval_string(arg)

View file

@ -1,78 +0,0 @@
import os
import sys
import shutil
import tempfile
from django.core.cache import cache
from django.urls import path
from django.test import override_settings
from django.views.generic import TemplateView
from django.conf import settings
from django.test import TestCase
class DynamicURLModule:
def __init__(self):
self.urlpatterns = []
def __call__(self):
return self.urlpatterns
class CottonInlineTestCase(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Set tmp dir and register a url module for our tmp files
cls.temp_dir = tempfile.mkdtemp()
cls.url_module = DynamicURLModule()
cls.url_module_name = f"dynamic_urls_{cls.__name__}"
sys.modules[cls.url_module_name] = cls.url_module
# Register our temp directory as a TEMPLATES path
cls.new_templates_setting = settings.TEMPLATES.copy()
cls.new_templates_setting[0]["DIRS"] = [
cls.temp_dir
] + cls.new_templates_setting[0]["DIRS"]
# Apply the setting
cls.templates_override = override_settings(TEMPLATES=cls.new_templates_setting)
cls.templates_override.enable()
@classmethod
def tearDownClass(cls):
"""Remove temporary directory and clean up modules"""
cls.templates_override.disable()
shutil.rmtree(cls.temp_dir, ignore_errors=True)
del sys.modules[cls.url_module_name]
super().tearDownClass()
def tearDown(self):
"""Clear cache between tests so that we can use the same file names for simplicity"""
cache.clear()
def create_template(self, name, content):
"""Create a template file in the temporary directory and return the path"""
path = os.path.join(self.temp_dir, name)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
f.write(content)
return path
def make_view(self, template_name):
"""Make a view that renders the given template"""
return TemplateView.as_view(template_name=template_name)
def register_url(self, url, view):
"""Register a URL pattern and returns path"""
url_pattern = path(url, view)
self.url_module.urlpatterns.append(url_pattern)
return url_pattern
def setUp(self):
super().setUp()
self.url_module.urlpatterns = []
def get_url_conf(self):
return self.url_module_name

View file

@ -1,276 +0,0 @@
from django.test import TestCase
from django_cotton.tests.inline_test_case import CottonInlineTestCase
from django_cotton.tests.utils import get_compiled, get_rendered
class InlineTestCase(CottonInlineTestCase):
def test_component_is_rendered(self):
self.create_template(
"cotton/component.html",
"""<div class="i-am-component">{{ slot }}</div>""",
)
self.create_template(
"view.html",
"""<c-component>Hello, World!</c-component>""",
)
# Register Url
self.register_url("view/", self.make_view("view.html"))
# Override URLconf
with self.settings(ROOT_URLCONF=self.get_url_conf()):
response = self.client.get("/view/")
self.assertContains(response, '<div class="i-am-component">')
self.assertContains(response, "Hello, World!")
def test_new_lines_in_attributes_are_preserved(self):
self.create_template(
"cotton/preserved.html",
"""<div {{ attrs }}>{{ slot }}</div>""",
)
self.create_template(
"preserved_view.html",
"""
<c-preserved x-data="{
attr1: 'im an attr',
var1: 'im a var',
method() {
return 'im a method';
}
}" />
""",
)
# Register Url
self.register_url("view/", self.make_view("preserved_view.html"))
# Override URLconf
with self.settings(ROOT_URLCONF=self.get_url_conf()):
response = self.client.get("/view/")
self.assertTrue(
"""{
attr1: 'im an attr',
var1: 'im a var',
method() {
return 'im a method';
}
}"""
in response.content.decode()
)
def test_attribute_names_on_component_containing_hyphens_are_converted_to_underscores(
self,
):
self.create_template(
"cotton/hyphens.html",
"""
<div x-data="{{ x_data }}" x-init="{{ x_init }}"></div>
""",
)
self.create_template(
"hyphens_view.html",
"""
<c-hyphens x-data="{}" x-init="do_something()" />
""",
)
# Register Url
self.register_url("view/", self.make_view("hyphens_view.html"))
# Override URLconf
with self.settings(ROOT_URLCONF=self.get_url_conf()):
response = self.client.get("/view/")
self.assertContains(response, 'x-data="{}" x-init="do_something()"')
def test_attribute_names_on_cvars_containing_hyphens_are_converted_to_underscores(
self,
):
self.create_template(
"cotton/cvar_hyphens.html",
"""
<c-vars x-data="{}" x-init="do_something()" />
<div x-data="{{ x_data }}" x-init="{{ x_init }}"></div>
""",
)
self.create_template(
"cvar_hyphens_view.html",
"""
<c-cvar-hyphens />
""",
)
# Register Url
self.register_url("view/", self.make_view("cvar_hyphens_view.html"))
# Override URLconf
with self.settings(ROOT_URLCONF=self.get_url_conf()):
response = self.client.get("/view/")
self.assertContains(response, 'x-data="{}" x-init="do_something()"')
def test_cotton_directory_can_be_configured(self):
custom_dir = "components"
self.create_template(
f"{custom_dir}/component.html",
"""<div class="i-am-component">{{ slot }}</div>""",
)
self.create_template(
"view.html",
"""<c-component>Hello, World!</c-component>""",
)
# Register Url
self.register_url("view/", self.make_view("view.html"))
# Override URLconf
with self.settings(ROOT_URLCONF=self.get_url_conf(), COTTON_DIR=custom_dir):
response = self.client.get("/view/")
self.assertContains(response, '<div class="i-am-component">')
self.assertContains(response, "Hello, World!")
class CottonTestCase(TestCase):
def test_parent_component_is_rendered(self):
response = self.client.get("/parent")
self.assertContains(response, '<div class="i-am-parent">')
def test_child_is_rendered(self):
response = self.client.get("/child")
self.assertContains(response, '<div class="i-am-parent">')
self.assertContains(response, '<div class="i-am-child">')
def test_self_closing_is_rendered(self):
response = self.client.get("/self-closing")
self.assertContains(response, '<div class="i-am-parent">')
def test_named_slots_correctly_display_in_loop(self):
response = self.client.get("/named-slot-in-loop")
self.assertContains(response, "item name: Item 1")
self.assertContains(response, "item name: Item 2")
self.assertContains(response, "item name: Item 3")
def test_attribute_passing(self):
response = self.client.get("/attribute-passing")
self.assertContains(
response, '<div attribute_1="hello" and-another="woo1" thirdforluck="yes">'
)
def test_attribute_merging(self):
response = self.client.get("/attribute-merging")
self.assertContains(
response, 'class="form-group another-class-with:colon extra-class"'
)
def test_django_syntax_decoding(self):
response = self.client.get("/django-syntax-decoding")
self.assertContains(response, "some-class")
def test_vars_are_converted_to_vars_frame_tags(self):
compiled = get_compiled(
"""
<c-vars var1="string with space" />
content
"""
)
self.assertEquals(
compiled,
"""{% cotton_vars_frame var1=var1|default:"string with space" %}content{% endcotton_vars_frame %}""",
)
def test_attrs_do_not_contain_vars(self):
response = self.client.get("/vars-test")
self.assertContains(response, "attr1: 'im an attr'")
self.assertContains(response, "var1: 'im a var'")
self.assertContains(response, """attrs: 'attr1="im an attr"'""")
def test_strings_with_spaces_can_be_passed(self):
response = self.client.get("/string-with-spaces")
self.assertContains(response, "attr1: 'I have spaces'")
self.assertContains(response, "var1: 'string with space'")
self.assertContains(response, "default_var: 'default var'")
self.assertContains(response, "named_slot: '")
self.assertContains(response, "named_slot with spaces")
self.assertContains(response, """attrs: 'attr1="I have spaces"'""")
def test_named_slots_dont_bleed_into_sibling_components(self):
html = """
<c-test-component>
component1
<c-slot name="named_slot">named slot 1</c-slot>
</c-test-component>
<c-test-component>
component2
</c-test-component>
"""
rendered = get_rendered(html)
self.assertTrue("named_slot: 'named slot 1'" in rendered)
self.assertTrue("named_slot: ''" in rendered)
def test_template_variables_are_not_parsed(self):
html = """
<c-test-component attr1="variable" :attr2="variable">
<c-slot name="named_slot">
<a href="#" silica:click.prevent="variable = 'lineage'">test</a>
</c-slot>
</c-test-component>
"""
rendered = get_rendered(html, {"variable": 1})
self.assertTrue("attr1: 'variable'" in rendered)
self.assertTrue("attr2: '1'" in rendered)
def test_valueless_attributes_are_process_as_true(self):
response = self.client.get("/test/valueless-attributes")
self.assertContains(response, "It's True")
def test_component_attributes_can_converted_to_python_types(self):
response = self.client.get("/test/eval-attributes")
self.assertContains(response, "none is None")
self.assertContains(response, "number is 1")
self.assertContains(response, "boolean_true is True")
self.assertContains(response, "boolean_false is False")
self.assertContains(response, "list.0 is 1")
self.assertContains(response, "dict.key is 'value'")
self.assertContains(response, "listdict.0.key is 'value'")
def test_cvars_can_be_converted_to_python_types(self):
response = self.client.get("/test/eval-vars")
self.assertContains(response, "none is None")
self.assertContains(response, "number is 1")
self.assertContains(response, "boolean_true is True")
self.assertContains(response, "boolean_false is False")
self.assertContains(response, "list.0 is 1")
self.assertContains(response, "dict.key is 'value'")
self.assertContains(response, "listdict.0.key is 'value'")
def test_attributes_can_contain_django_native_tags(self):
response = self.client.get("/test/native-tags-in-attributes")
self.assertContains(response, "Attribute 1 says: 'Hello Will'")
self.assertContains(response, "Attribute 2 says: 'world'")
self.assertContains(response, "Attribute 3 says: 'cowabonga!'")
self.assertContains(
response,
"""attrs tag is: 'normal="normal" attr1="Hello Will" attr2="world" attr3="cowabonga!"'""",
)
# TODO: implement inline test asset creation, i.e. store_template("native-tags-in-attributes", """)

View file

@ -1,18 +0,0 @@
from django.template import Context, Template
from django_cotton.cotton_loader import Loader as CottonLoader
def get_compiled(template_string):
return CottonLoader(engine=None).template_processor.process(
template_string, "test_key"
)
def get_rendered(template_string, context: dict = None):
if context is None:
context = {}
compiled_string = get_compiled(template_string)
return Template(compiled_string).render(Context(context))

View file

@ -1,52 +0,0 @@
from django.views.generic import TemplateView
from . import views
from django.urls import path
app_name = "django_cotton"
class NamedSlotInLoop(TemplateView):
template_name = "named_slot_in_loop.html"
def get_context_data(self, **kwargs):
return {
"items": [
{"name": "Item 1"},
{"name": "Item 2"},
{"name": "Item 3"},
]
}
urlpatterns = [
path("parent", TemplateView.as_view(template_name="parent_test.html")),
path("child", TemplateView.as_view(template_name="child_test.html")),
path(
"self-closing",
TemplateView.as_view(template_name="self_closing_test.html"),
),
path("include", TemplateView.as_view(template_name="cotton_include.html")),
path("playground", TemplateView.as_view(template_name="playground.html")),
path("tag", TemplateView.as_view(template_name="tag.html")),
path("named-slot-in-loop", NamedSlotInLoop.as_view()),
path("test/compiled-cotton", views.compiled_cotton_test_view),
path("test/cotton", views.cotton_test_view),
path("test/native-extends", views.native_extends_test_view),
path("test/native-include", views.native_include_test_view),
path("test/valueless-attributes", views.valueless_attributes_test_view),
path("attribute-merging", views.attribute_merging_test_view),
path("attribute-passing", views.attribute_passing_test_view),
path("django-syntax-decoding", views.django_syntax_decoding_test_view),
path(
"string-with-spaces",
TemplateView.as_view(template_name="string_with_spaces.html"),
),
path("vars-test", TemplateView.as_view(template_name="vars_test.html")),
path("variable-parsing", views.variable_parsing_test_view),
path("test/eval-vars", views.eval_vars_test_view),
path("test/eval-attributes", views.eval_attributes_test_view),
path(
"test/native-tags-in-attributes",
TemplateView.as_view(template_name="native_tags_in_attributes_view.html"),
),
]

View file

@ -1,18 +0,0 @@
import ast
def eval_string(value):
"""
Evaluate a string representation of a constant, list, or dictionary to the actual Python object.
"""
try:
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}"'

View file

@ -1,50 +0,0 @@
from django.shortcuts import render
# benchmark tests
def compiled_cotton_test_view(request):
return render(request, "compiled_cotton_test.html")
def cotton_test_view(request):
return render(request, "cotton_test.html")
def native_extends_test_view(request):
return render(request, "native_extends_test.html")
def native_include_test_view(request):
return render(request, "native_include_test.html")
# Django tests
def attribute_merging_test_view(request):
return render(request, "attribute_merging_test.html")
def attribute_passing_test_view(request):
return render(request, "attribute_passing_test.html")
def django_syntax_decoding_test_view(request):
return render(request, "django_syntax_decoding_test.html")
def variable_parsing_test_view(request):
return render(request, "variable_parsing_test.html", {"variable": "some-class"})
def valueless_attributes_test_view(request):
return render(request, "valueless_attributes_test_view.html")
def eval_vars_test_view(request):
return render(request, "eval_vars_test_view.html")
def eval_attributes_test_view(request):
return render(request, "eval_attributes_test_view.html")

View file

@ -1,16 +0,0 @@
"""
WSGI config for django_cotton project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
application = get_wsgi_application()