Skip to content

Commit

Permalink
Add type hints & configure mypy. (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
clokep authored Jun 28, 2024
1 parent 4ccec65 commit 95fdc36
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 41 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
python-version: "3.10"

- name: "Install dependencies"
run: python -m pip install pyupgrade==2.31.1 flake8==4.0.1 black==22.3.0 isort==5.10.1
run: python -m pip install pyupgrade==2.31.1 flake8==4.0.1 black==22.3.0 isort==5.10.1 mypy==1.10.1

- name: "Run pyupgrade"
run: pyupgrade --py38-plus **/*.py
Expand All @@ -35,6 +35,9 @@ jobs:
- name: "Run black"
run: black --check .

- name: "Run mypy"
run: mypy

tests:
name: "Python ${{ matrix.python-version }}"
needs: lint
Expand Down
15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
[build-system]
requires = ["setuptools>=38.6.0", "wheel"]
build-backend = "setuptools.build_meta"

[tool.mypy]
warn_unused_configs = true
strict = false
warn_return_any = true
follow_imports = "normal"
show_error_codes = true
disallow_untyped_defs = true
ignore_missing_imports = true
warn_unreachable = true
no_implicit_optional = true
files = [
"render_block",
"tests",
]
14 changes: 11 additions & 3 deletions render_block/base.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
from django.template import loader
from typing import List, Optional, Tuple, Union

from django.http import HttpRequest
from django.template import Context, loader
from django.template.backends.django import Template as DjangoTemplate

try:
from django.template.backends.jinja2 import Template as Jinja2Template
except ImportError:
# Most likely Jinja2 isn't installed, in that case just create a class since
# we always want it to be false anyway.
class Jinja2Template:
class Jinja2Template: # type: ignore[no-redef]
pass


from render_block.django import django_render_block
from render_block.exceptions import UnsupportedEngine


def render_block_to_string(template_name, block_name, context=None, request=None):
def render_block_to_string(
template_name: Union[str, Tuple[str], List[str]],
block_name: str,
context: Optional[Context] = None,
request: Optional[HttpRequest] = None,
) -> str:
"""
Loads the given template_name and renders the given block with the given
dictionary as context. Returns a string.
Expand Down
26 changes: 20 additions & 6 deletions render_block/django.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from copy import copy
from typing import Optional

from django.http import HttpRequest
from django.template import Context, RequestContext
from django.template.base import TextNode
from django.template.backends.django import Template as DjangoTemplate
from django.template.base import NodeList, Template, TextNode
from django.template.context import RenderContext
from django.template.loader_tags import (
BLOCK_CONTEXT_KEY,
Expand All @@ -13,7 +16,12 @@
from render_block.exceptions import BlockNotFound


def django_render_block(template, block_name, context, request=None):
def django_render_block(
template: DjangoTemplate,
block_name: str,
context: Context,
request: Optional[HttpRequest] = None,
) -> str:
# Create a Django Context if needed
if isinstance(context, Context):
# Make a copy of the context and reset the rendering state.
Expand Down Expand Up @@ -53,7 +61,7 @@ def django_render_block(template, block_name, context, request=None):
)


def _build_block_context(template, context):
def _build_block_context(template: Template, context: Context) -> Optional[Template]:
"""Populate the block context with BlockNodes from parent templates."""

# Ensure there's a BlockContext before rendering. This allows blocks in
Expand Down Expand Up @@ -84,13 +92,19 @@ def _build_block_context(template, context):
if not isinstance(node, TextNode):
break

return None

def _render_template_block(template, block_name, context):

def _render_template_block(
template: Template, block_name: str, context: Context
) -> str:
"""Renders a single block from a template."""
return _render_template_block_nodelist(template.nodelist, block_name, context)


def _render_template_block_nodelist(nodelist, block_name, context):
def _render_template_block_nodelist(
nodelist: NodeList, block_name: str, context: Context
) -> str:
"""Recursively iterate over a node to find the wanted block."""

# Attempt to find the wanted block in the current template.
Expand All @@ -102,7 +116,7 @@ def _render_template_block_nodelist(nodelist, block_name, context):

# If the name matches, you're all set and we found the block!
if node.name == block_name:
return node.render(context)
return node.render(context) # type: ignore[no-any-return]

# If a node has children, recurse into them. Based on
# django.template.base.Node.get_nodes_by_type.
Expand Down
7 changes: 6 additions & 1 deletion render_block/jinja2.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from django.template import Context
from django.template.backends.jinja2 import Template as Jinja2Template

from render_block.exceptions import BlockNotFound


def jinja2_render_block(template, block_name, context):
def jinja2_render_block(
template: Jinja2Template, block_name: str, context: Context
) -> str:
# Get the underlying jinja2.environment.Template object.
template = template.template

Expand Down
2 changes: 1 addition & 1 deletion tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
]

MIDDLEWARE_CLASSES = tuple()
MIDDLEWARE_CLASSES: tuple = tuple()

EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"

Expand Down
4 changes: 3 additions & 1 deletion tests/templatetags/test_tags.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import NoReturn

from django import template

register = template.Library()


@register.simple_tag()
def raise_exception():
def raise_exception() -> NoReturn:
raise Exception("Exception raised in template tag.")
56 changes: 28 additions & 28 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
class TestDjango(TestCase):
"""Test the Django templating engine."""

def assertExceptionMessageEquals(self, exception, expected):
def assertExceptionMessageEquals(self, exception: Exception, expected: str) -> None:
self.assertEqual(expected, exception.args[0])

def test_block(self):
def test_block(self) -> None:
"""Test rendering an individual block."""
result = render_block_to_string("test1.html", "block1")
self.assertEqual(result, "block1 from test1")
Expand All @@ -21,41 +21,41 @@ def test_block(self):
result = render_block_to_string("test1.html", "block2")
self.assertEqual(result, "block2 from test1")

def test_override(self):
def test_override(self) -> None:
"""This block is overridden in test2."""
result = render_block_to_string("test2.html", "block1")
self.assertEqual(result, "block1 from test2")

def test_inherit(self):
def test_inherit(self) -> None:
"""This block is inherited from test1."""
result = render_block_to_string("test2.html", "block2")
self.assertEqual(result, "block2 from test1")

def test_no_block(self):
def test_no_block(self) -> None:
"""Check if there's no block available an exception is raised."""
with self.assertRaises(BlockNotFound) as exc:
render_block_to_string("test1.html", "noblock")
self.assertExceptionMessageEquals(
exc.exception, "block with name 'noblock' does not exist"
)

def test_include(self):
def test_include(self) -> None:
"""Ensure that an include tag in a block still works."""
result = render_block_to_string("test3_django.html", "block1")
self.assertEqual(result, "included template")

def test_super(self):
def test_super(self) -> None:
"""Test that block.super works."""
result = render_block_to_string("test3_django.html", "block2")
self.assertEqual(result, "block2 from test3 - block2 from test1")

def test_multi_super(self):
def test_multi_super(self) -> None:
result = render_block_to_string("test6_django.html", "block2")
self.assertEqual(
result, "block2 from test6 - block2 from test3 - block2 from test1"
)

def test_super_with_same_context_on_multiple_executions(self):
def test_super_with_same_context_on_multiple_executions(self) -> None:
"""Test that block.super works when fed the same context object twice."""
context = Context()
result_one = render_block_to_string(
Expand All @@ -68,15 +68,15 @@ def test_super_with_same_context_on_multiple_executions(self):
result_one, result_two, "block2 from test3 - block2 from test1"
)

def test_subblock(self):
def test_subblock(self) -> None:
"""Test that a block within a block works."""
result = render_block_to_string("test5.html", "block1")
self.assertEqual(result, "block3 from test5")

result = render_block_to_string("test5.html", "block3")
self.assertEqual(result, "block3 from test5")

def test_subblock_no_parent(self):
def test_subblock_no_parent(self) -> None:
"""
Test that a block within a block works if the parent block is only found
in the base template.
Expand All @@ -91,26 +91,26 @@ def test_subblock_no_parent(self):
result = render_block_to_string("test_sub.html", "first")
self.assertEqual(result, "\nbar\n")

def test_exceptions(self):
def test_exceptions(self) -> None:
with self.assertRaises(Exception) as e:
render_block_to_string("test_exception.html", "exception_block")
self.assertEqual(str(e.exception), "Exception raised in template tag.")

@override_settings(DEBUG=True)
def test_exceptions_debug(self):
def test_exceptions_debug(self) -> None:
with self.assertRaises(Exception) as exc:
render_block_to_string("test_exception.html", "exception_block")
self.assertExceptionMessageEquals(
exc.exception, "Exception raised in template tag."
)

def test_context(self):
def test_context(self) -> None:
"""Test that a context is properly rendered in a template."""
data = "block2 from test5"
result = render_block_to_string("test5.html", "block2", {"foo": data})
self.assertEqual(result, data)

def test_context_autoescape_off(self):
def test_context_autoescape_off(self) -> None:
"""Test that the user can disable autoescape by providing a Context instance."""
data = "&'"
result = render_block_to_string(
Expand All @@ -127,7 +127,7 @@ def test_context_autoescape_off(self):
}
]
)
def test_different_backend(self):
def test_different_backend(self) -> None:
"""
Ensure an exception is thrown if a different backed from the Django
backend is used.
Expand All @@ -146,7 +146,7 @@ def test_different_backend(self):
],
},
)
def test_request_context(self):
def test_request_context(self) -> None:
"""Test that a request context data are properly rendered in a template."""
request = RequestFactory().get("dummy-url")
result = render_block_to_string(
Expand All @@ -168,10 +168,10 @@ def test_request_context(self):
class TestJinja2(TestCase):
"""Test the Django templating engine."""

def assertExceptionMessageEquals(self, exception, expected):
def assertExceptionMessageEquals(self, exception: Exception, expected: str) -> None:
self.assertEqual(expected, exception.args[0])

def test_block(self):
def test_block(self) -> None:
"""Test rendering an individual block."""
result = render_block_to_string("test1.html", "block1")
self.assertEqual(result, "block1 from test1")
Expand All @@ -180,44 +180,44 @@ def test_block(self):
result = render_block_to_string("test1.html", "block2")
self.assertEqual(result, "block2 from test1")

def test_override(self):
def test_override(self) -> None:
"""This block is overridden in test2."""
result = render_block_to_string("test2.html", "block1")
self.assertEqual(result, "block1 from test2")

@skip("Not currently supported.")
def test_inherit(self):
def test_inherit(self) -> None:
"""This block is inherited from test1."""
result = render_block_to_string("test2.html", "block2")
self.assertEqual(result, "block2 from test1")

def test_no_block(self):
def test_no_block(self) -> None:
"""Check if there's no block available an exception is raised."""
with self.assertRaises(BlockNotFound) as exc:
render_block_to_string("test1.html", "noblock")
self.assertExceptionMessageEquals(
exc.exception, "block with name 'noblock' does not exist"
)

def test_include(self):
def test_include(self) -> None:
"""Ensure that an include tag in a block still works."""
result = render_block_to_string("test3_jinja2.html", "block1")
self.assertEqual(result, "included template")

@skip("Not currently supported.")
def test_super(self):
def test_super(self) -> None:
"""Test that super() works."""
result = render_block_to_string("test3_jinja2.html", "block2")
self.assertEqual(result, "block2 from test3 - block2 from test1")

@skip("Not currently supported.")
def test_multi_super(self):
def test_multi_super(self) -> None:
result = render_block_to_string("test6_jinja2.html", "block2")
self.assertEqual(
result, "block2 from test6 - block2 from test3 - block2 from test1"
)

def test_subblock(self):
def test_subblock(self) -> None:
"""Test that a block within a block works."""
result = render_block_to_string("test5.html", "block1")
self.assertEqual(result, "block3 from test5")
Expand All @@ -226,7 +226,7 @@ def test_subblock(self):
self.assertEqual(result, "block3 from test5")

@skip("Not currently supported.")
def test_subblock_no_parent(self):
def test_subblock_no_parent(self) -> None:
"""
Test that a block within a block works if the parent block is only found
in the base template.
Expand All @@ -241,7 +241,7 @@ def test_subblock_no_parent(self):
result = render_block_to_string("test_sub.html", "first")
self.assertEqual(result, "\nbar\n")

def test_context(self):
def test_context(self) -> None:
"""Test that a context is properly rendered in a template."""
data = "block2 from test5"
result = render_block_to_string("test5.html", "block2", {"foo": data})
Expand Down

0 comments on commit 95fdc36

Please sign in to comment.