diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cb61ce8..6963835 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 @@ -35,6 +35,9 @@ jobs: - name: "Run black" run: black --check . + - name: "Run mypy" + run: mypy + tests: name: "Python ${{ matrix.python-version }}" needs: lint diff --git a/pyproject.toml b/pyproject.toml index 716f6e4..7333ce9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", +] diff --git a/render_block/base.py b/render_block/base.py index fa9d519..e137c99 100644 --- a/render_block/base.py +++ b/render_block/base.py @@ -1,4 +1,7 @@ -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: @@ -6,7 +9,7 @@ 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 @@ -14,7 +17,12 @@ class Jinja2Template: 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. diff --git a/render_block/django.py b/render_block/django.py index 86274ec..fab984b 100644 --- a/render_block/django.py +++ b/render_block/django.py @@ -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, @@ -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. @@ -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 @@ -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. @@ -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. diff --git a/render_block/jinja2.py b/render_block/jinja2.py index 8342f7f..02a87df 100644 --- a/render_block/jinja2.py +++ b/render_block/jinja2.py @@ -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 diff --git a/tests/settings.py b/tests/settings.py index ff132c3..4bb4fda 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -24,7 +24,7 @@ }, ] -MIDDLEWARE_CLASSES = tuple() +MIDDLEWARE_CLASSES: tuple = tuple() EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" diff --git a/tests/templatetags/test_tags.py b/tests/templatetags/test_tags.py index 8b7e1d2..fa6c321 100644 --- a/tests/templatetags/test_tags.py +++ b/tests/templatetags/test_tags.py @@ -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.") diff --git a/tests/tests.py b/tests/tests.py index 12dfd83..8401c70 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -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") @@ -21,17 +21,17 @@ 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") @@ -39,23 +39,23 @@ def test_no_block(self): 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( @@ -68,7 +68,7 @@ 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") @@ -76,7 +76,7 @@ def test_subblock(self): 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. @@ -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( @@ -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. @@ -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( @@ -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") @@ -180,18 +180,18 @@ 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") @@ -199,25 +199,25 @@ def test_no_block(self): 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") @@ -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. @@ -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})