Add pytest-cov

Apply black formatting
Add component_css_dependencies_tag and component_js_dependencies_tag tags
Add convenience methods render_css_dependencies and render_js_dependencies to component class
Create additional test cases
Add license to setup.py
Add pytest.ini config file
This commit is contained in:
Bradley Stuart Kirton 2020-07-09 08:01:39 +02:00 committed by Emil Stenström
parent e9929b1bff
commit 2c644d4c06
8 changed files with 199 additions and 66 deletions

View file

@ -2,10 +2,11 @@ from django.forms.widgets import MediaDefiningClass
from django.template import Context
from django.template.base import NodeList, TextNode
from django.template.loader import get_template
from django.utils.safestring import mark_safe
from six import with_metaclass
# Allow "component.AlreadyRegistered" instead of having to import these everywhere
from django_components.component_registry import AlreadyRegistered, ComponentRegistry, NotRegistered # NOQA
from django_components.component_registry import AlreadyRegistered, ComponentRegistry, NotRegistered # noqa
# Python 2 compatibility
try:
@ -24,6 +25,7 @@ except ImportError:
VAR = TOKEN_VAR
BLOCK = TOKEN_BLOCK
class Component(with_metaclass(MediaDefiningClass)):
def context(self):
return {}
@ -32,8 +34,20 @@ class Component(with_metaclass(MediaDefiningClass)):
raise NotImplementedError("Missing template() method on component")
def render_dependencies(self):
"""Helper function to access media.render()"""
return self.media.render()
def render_css_dependencies(self):
"""Render only CSS dependencies available in the media class."""
return mark_safe("\n".join(self.media.render_css()))
def render_js_dependencies(self):
"""Render only JS dependencies available in the media class."""
return mark_safe("\n".join(self.media.render_js()))
def slots_in_template(self, template):
nodelist = NodeList()
for node in template.template.nodelist:
@ -48,7 +62,9 @@ class Component(with_metaclass(MediaDefiningClass)):
def render(self, slots_filled=None, *args, **kwargs):
slots_filled = slots_filled or []
context_args_variables = getfullargspec(self.context).args[1:]
context_args = {key: kwargs[key] for key in context_args_variables if key in kwargs}
context_args = {
key: kwargs[key] for key in context_args_variables if key in kwargs
}
context = self.context(**context_args)
template = get_template(self.template(context))
slots_in_template = self.slots_in_template(template)

View file

@ -1,9 +1,11 @@
class AlreadyRegistered(Exception):
pass
class NotRegistered(Exception):
pass
class ComponentRegistry(object):
def __init__(self):
self._registry = {} # component name -> component_class mapping

View file

@ -17,6 +17,7 @@ except ImportError:
VAR = TOKEN_VAR
BLOCK = TOKEN_BLOCK
# Django < 2.0 compatibility
if django.VERSION > (2, 0):
PARSE_BITS_DEFAULTS = {
@ -38,16 +39,43 @@ register = template.Library()
COMPONENT_CONTEXT_KEY = "component_context"
@register.simple_tag(name="component_dependencies")
def component_dependencies_tag():
def iter_components_from_registry(registry):
"""Yields unique components from the registry."""
unique_component_classes = set(registry.all().values())
out = []
for component_class in unique_component_classes:
component = component_class()
out.append(component.render_dependencies())
yield component_class()
return mark_safe("\n".join(out))
@register.simple_tag(name="component_dependencies")
def component_dependencies_tag():
"""Render both the CSS and JS dependency tags."""
component_gen = iter_components_from_registry(registry)
dependency_gen = map(lambda x: x.render_dependencies(), component_gen)
return mark_safe("\n".join(dependency_gen))
@register.simple_tag(name="component_css_dependencies")
def component_css_dependencies_tag():
"""Render the CSS tags."""
component_gen = iter_components_from_registry(registry)
dependency_gen = map(lambda x: x.render_css_dependencies(), component_gen)
return mark_safe("\n".join(dependency_gen))
@register.simple_tag(name="component_js_dependencies")
def component_js_dependencies_tag():
"""Render the JS tags."""
component_gen = iter_components_from_registry(registry)
dependency_gen = map(lambda x: x.render_js_dependencies(), component_gen)
return mark_safe("\n".join(dependency_gen))
@register.simple_tag(name="component")
@ -74,9 +102,11 @@ class SlotNode(Node):
rendered_slot = self.nodelist.render(context)
if self.component:
context.render_context[COMPONENT_CONTEXT_KEY][self.component][self.name] = rendered_slot
context.render_context[COMPONENT_CONTEXT_KEY][self.component][
self.name
] = rendered_slot
return ''
return ""
@register.tag("slot")
@ -135,11 +165,17 @@ def do_component(parser, token):
tag_name = tag_args.pop(0)
if len(bits) < 2:
raise TemplateSyntaxError("Call the '%s' tag with a component name as the first parameter" % tag_name)
raise TemplateSyntaxError(
"Call the '%s' tag with a component name as the first parameter" % tag_name
)
component_name = bits[1]
if not component_name.startswith(('"', "'")) or not component_name.endswith(('"', "'")):
raise TemplateSyntaxError("Component name '%s' should be in quotes" % component_name)
if not component_name.startswith(('"', "'")) or not component_name.endswith(
('"', "'")
):
raise TemplateSyntaxError(
"Component name '%s' should be in quotes" % component_name
)
component_name = component_name.strip('"')
component_class = registry.get(component_name)

5
pytest.ini Normal file
View file

@ -0,0 +1,5 @@
[pytest]
addopts = --cov=django_components
--cov-report=html
--cov-report=term-missing:skip-covered
--cov-fail-under=85

View file

@ -2,5 +2,6 @@ django
six
tox
pytest
pytest-cov
flake8
isort

View file

@ -5,26 +5,28 @@
# pip-compile requirements-dev.in
#
appdirs==1.4.4 # via virtualenv
asgiref==3.2.7 # via django
asgiref==3.2.10 # via django
attrs==19.3.0 # via pytest
distlib==0.3.0 # via virtualenv
django==3.0.7 # via -r requirements-dev.in
coverage==5.2 # via pytest-cov
distlib==0.3.1 # via virtualenv
django==3.0.8 # via -r requirements-dev.in
filelock==3.0.12 # via tox, virtualenv
flake8==3.8.2 # via -r requirements-dev.in
isort==4.3.21 # via -r requirements-dev.in
flake8==3.8.3 # via -r requirements-dev.in
isort==5.0.5 # via -r requirements-dev.in
mccabe==0.6.1 # via flake8
more-itertools==8.3.0 # via pytest
more-itertools==8.4.0 # via pytest
packaging==20.4 # via pytest, tox
pluggy==0.13.1 # via pytest, tox
py==1.8.1 # via pytest, tox
py==1.9.0 # via pytest, tox
pycodestyle==2.6.0 # via flake8
pyflakes==2.2.0 # via flake8
pyparsing==2.4.7 # via packaging
pytest==5.4.3 # via -r requirements-dev.in
pytest-cov==2.10.0 # via -r requirements-dev.in
pytest==5.4.3 # via -r requirements-dev.in, pytest-cov
pytz==2020.1 # via django
six==1.15.0 # via -r requirements-dev.in, packaging, tox, virtualenv
sqlparse==0.3.1 # via django
toml==0.10.1 # via tox
tox==3.15.1 # via -r requirements-dev.in
virtualenv==20.0.21 # via tox
wcwidth==0.2.3 # via pytest
tox==3.16.1 # via -r requirements-dev.in
virtualenv==20.0.26 # via tox
wcwidth==0.2.5 # via pytest

View file

@ -3,23 +3,21 @@ import os
from setuptools import find_packages, setup
VERSION = '0.3'
VERSION = "0.3"
setup(
name='django_reusable_components',
name="django_reusable_components",
packages=find_packages(exclude=["tests"]),
version=VERSION,
description='A way to create simple reusable template components in Django.',
long_description=open(os.path.join(os.path.dirname(__file__), 'README.md')).read(),
description="A way to create simple reusable template components in Django.",
long_description=open(os.path.join(os.path.dirname(__file__), "README.md")).read(),
long_description_content_type="text/markdown",
author=u'Emil Stenström',
author_email='em@kth.se',
url='https://github.com/EmilStenstrom/django-components/',
install_requires=[
"Django>=1.11",
"six",
],
keywords=['django', 'components', 'css', 'js', 'html'],
author=u"Emil Stenström",
author_email="em@kth.se",
url="https://github.com/EmilStenstrom/django-components/",
install_requires=["Django>=1.11", "six"],
license="MIT",
keywords=["django", "components", "css", "js", "html"],
classifiers=[
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",

View file

@ -32,10 +32,12 @@ class SlottedComponent(component.Component):
def template(self, context):
return "slotted_template.html"
class SlottedComponentNoSlots(component.Component):
def template(self, context):
return "slotted_template_no_slots.html"
class SlottedComponentWithContext(component.Component):
def context(self, variable):
return {"variable": variable}
@ -43,6 +45,7 @@ class SlottedComponentWithContext(component.Component):
def template(self, context):
return "slotted_template.html"
class ComponentTemplateTagTest(SimpleTestCase):
def setUp(self):
# NOTE: component.registry is global, so need to clear before each test
@ -51,34 +54,63 @@ class ComponentTemplateTagTest(SimpleTestCase):
def test_single_component_dependencies(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template('{% load component_tags %}{% component_dependencies %}')
template = Template("{% load component_tags %}{% component_dependencies %}")
rendered = template.render(Context())
self.assertHTMLEqual(rendered, dedent("""
<link href="style.css" type="text/css" media="all" rel="stylesheet">
<script type="text/javascript" src="script.js"></script>
""").strip())
expected_outcome = (
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">\n"""
"""<script type="text/javascript" src="script.js"></script>"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
def test_single_component_css_dependencies(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_css_dependencies %}")
rendered = template.render(Context())
expected_outcome = (
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
def test_single_component_js_dependencies(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_js_dependencies %}")
rendered = template.render(Context())
expected_outcome = (
"""<script type="text/javascript" src="script.js"></script>"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
def test_single_component(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template('{% load component_tags %}{% component name="test" variable="variable" %}')
template = Template(
'{% load component_tags %}{% component name="test" variable="variable" %}'
)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
def test_call_component_with_two_variables(self):
component.registry.register(name="test", component=IffedComponent)
template = Template('{% load component_tags %}{% component name="test" variable="variable" variable2="hej" %}')
template = Template(
"{% load component_tags %}"
'{% component name="test" variable="variable" variable2="hej" %}'
)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, dedent("""
Variable: <strong>variable</strong>
Variable2: <strong>hej</strong>
"""))
expected_outcome = (
"""Variable: <strong>variable</strong>\n"""
"""Variable2: <strong>hej</strong>"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
def test_component_called_with_positional_name(self):
component.registry.register(name="test", component=SimpleComponent)
template = Template('{% load component_tags %}{% component "test" variable="variable" %}')
template = Template(
'{% load component_tags %}{% component "test" variable="variable" %}'
)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "Variable: <strong>variable</strong>\n")
@ -86,12 +118,36 @@ class ComponentTemplateTagTest(SimpleTestCase):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(name="test2", component=SimpleComponent)
template = Template('{% load component_tags %}{% component_dependencies %}')
template = Template("{% load component_tags %}{% component_dependencies %}")
rendered = template.render(Context())
self.assertHTMLEqual(rendered, dedent("""
<link href="style.css" type="text/css" media="all" rel="stylesheet">
<script type="text/javascript" src="script.js"></script>
""").strip())
expected_outcome = (
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">\n"""
"""<script type="text/javascript" src="script.js"></script>"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
def test_multiple_component_css_dependencies(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(name="test2", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_css_dependencies %}")
rendered = template.render(Context())
expected_outcome = (
"""<link href="style.css" type="text/css" media="all" rel="stylesheet">"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
def test_multiple_component_js_dependencies(self):
component.registry.register(name="test1", component=SimpleComponent)
component.registry.register(name="test2", component=SimpleComponent)
template = Template("{% load component_tags %}{% component_js_dependencies %}")
rendered = template.render(Context())
expected_outcome = (
"""<script type="text/javascript" src="script.js"></script>"""
)
self.assertHTMLEqual(rendered, dedent(expected_outcome))
class ComponentSlottedTemplateTagTest(SimpleTestCase):
def setUp(self):
@ -102,7 +158,8 @@ class ComponentSlottedTemplateTagTest(SimpleTestCase):
component.registry.register(name="test1", component=SlottedComponent)
component.registry.register(name="test2", component=SimpleComponent)
template = Template("""
template = Template(
"""
{% load component_tags %}
{% component_block "test1" %}
{% slot "header" %}
@ -112,21 +169,26 @@ class ComponentSlottedTemplateTagTest(SimpleTestCase):
{% component "test2" variable="variable" %}
{% endslot %}
{% endcomponent_block %}
""")
"""
)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, """
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<header>Custom header</header>
<main>Variable: <strong>variable</strong></main>
<footer>Default footer</footer>
</custom-template>
""")
""",
)
def test_slotted_template_with_context_var(self):
component.registry.register(name="test1", component=SlottedComponentWithContext)
template = Template("""
template = Template(
"""
{% load component_tags %}
{% with my_first_variable="test123" %}
{% component_block "test1" variable="test456" %}
@ -138,37 +200,48 @@ class ComponentSlottedTemplateTagTest(SimpleTestCase):
{% endslot %}
{% endcomponent_block %}
{% endwith %}
""")
"""
)
rendered = template.render(Context({"my_second_variable": "test321"}))
self.assertHTMLEqual(rendered, """
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<header>Default header</header>
<main>test123 - test456</main>
<footer>test321</footer>
</custom-template>
""")
""",
)
def test_slotted_template_no_slots_filled(self):
component.registry.register(name="test", component=SlottedComponent)
template = Template('{% load component_tags %}{% component_block "test" %}{% endcomponent_block %}')
template = Template(
'{% load component_tags %}{% component_block "test" %}{% endcomponent_block %}'
)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, """
self.assertHTMLEqual(
rendered,
"""
<custom-template>
<header>Default header</header>
<main>Default main</main>
<footer>Default footer</footer>
</custom-template>
""")
""",
)
def test_slotted_template_without_slots(self):
component.registry.register(name="test", component=SlottedComponentNoSlots)
template = Template("""
template = Template(
"""
{% load component_tags %}
{% component_block "test" %}{% endcomponent_block %}
""")
"""
)
rendered = template.render(Context({}))
self.assertHTMLEqual(rendered, "<custom-template></custom-template>")