summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/templite.py52
-rw-r--r--tests/test_templite.py96
-rw-r--r--tox.ini11
3 files changed, 87 insertions, 72 deletions
diff --git a/coverage/templite.py b/coverage/templite.py
index 29596d77..897a58f9 100644
--- a/coverage/templite.py
+++ b/coverage/templite.py
@@ -10,8 +10,14 @@ http://aosabook.org/en/500L/a-template-engine.html
# Coincidentally named the same as http://code.activestate.com/recipes/496702/
+from __future__ import annotations
+
import re
+from typing import (
+ Any, Callable, Dict, List, NoReturn, Optional, Set, Union, cast,
+)
+
class TempliteSyntaxError(ValueError):
"""Raised when a template has a syntax error."""
@@ -26,14 +32,14 @@ class TempliteValueError(ValueError):
class CodeBuilder:
"""Build source code conveniently."""
- def __init__(self, indent=0):
- self.code = []
+ def __init__(self, indent: int = 0) -> None:
+ self.code: List[Union[str, CodeBuilder]] = []
self.indent_level = indent
- def __str__(self):
+ def __str__(self) -> str:
return "".join(str(c) for c in self.code)
- def add_line(self, line):
+ def add_line(self, line: str) -> None:
"""Add a line of source to the code.
Indentation and newline will be added for you, don't provide them.
@@ -41,7 +47,7 @@ class CodeBuilder:
"""
self.code.extend([" " * self.indent_level, line, "\n"])
- def add_section(self):
+ def add_section(self) -> CodeBuilder:
"""Add a section, a sub-CodeBuilder."""
section = CodeBuilder(self.indent_level)
self.code.append(section)
@@ -49,22 +55,22 @@ class CodeBuilder:
INDENT_STEP = 4 # PEP8 says so!
- def indent(self):
+ def indent(self) -> None:
"""Increase the current indent for following lines."""
self.indent_level += self.INDENT_STEP
- def dedent(self):
+ def dedent(self) -> None:
"""Decrease the current indent for following lines."""
self.indent_level -= self.INDENT_STEP
- def get_globals(self):
+ def get_globals(self) -> Dict[str, Any]:
"""Execute the code, and return a dict of globals it defines."""
# A check that the caller really finished all the blocks they started.
assert self.indent_level == 0
# Get the Python source as a single string.
python_source = str(self)
# Execute the source, defining globals, and return them.
- global_namespace = {}
+ global_namespace: Dict[str, Any] = {}
exec(python_source, global_namespace)
return global_namespace
@@ -111,7 +117,7 @@ class Templite:
})
"""
- def __init__(self, text, *contexts):
+ def __init__(self, text: str, *contexts: Dict[str, Any]) -> None:
"""Construct a Templite with the given `text`.
`contexts` are dictionaries of values to use for future renderings.
@@ -122,8 +128,8 @@ class Templite:
for context in contexts:
self.context.update(context)
- self.all_vars = set()
- self.loop_vars = set()
+ self.all_vars: Set[str] = set()
+ self.loop_vars: Set[str] = set()
# We construct a function in source form, then compile it and hold onto
# it, and execute it to render the template.
@@ -137,9 +143,9 @@ class Templite:
code.add_line("extend_result = result.extend")
code.add_line("to_str = str")
- buffered = []
+ buffered: List[str] = []
- def flush_output():
+ def flush_output() -> None:
"""Force `buffered` to the code builder."""
if len(buffered) == 1:
code.add_line("append_result(%s)" % buffered[0])
@@ -232,9 +238,15 @@ class Templite:
code.add_line('return "".join(result)')
code.dedent()
- self._render_function = code.get_globals()['render_function']
+ self._render_function = cast(
+ Callable[
+ [Dict[str, Any], Callable[..., Any]],
+ str
+ ],
+ code.get_globals()['render_function'],
+ )
- def _expr_code(self, expr):
+ def _expr_code(self, expr: str) -> str:
"""Generate a Python expression for `expr`."""
if "|" in expr:
pipes = expr.split("|")
@@ -252,11 +264,11 @@ class Templite:
code = "c_%s" % expr
return code
- def _syntax_error(self, msg, thing):
+ def _syntax_error(self, msg: str, thing: Any) -> NoReturn:
"""Raise a syntax error using `msg`, and showing `thing`."""
raise TempliteSyntaxError(f"{msg}: {thing!r}")
- def _variable(self, name, vars_set):
+ def _variable(self, name: str, vars_set: Set[str]) -> None:
"""Track that `name` is used as a variable.
Adds the name to `vars_set`, a set of variable names.
@@ -268,7 +280,7 @@ class Templite:
self._syntax_error("Not a valid name", name)
vars_set.add(name)
- def render(self, context=None):
+ def render(self, context: Optional[Dict[str, Any]] = None) -> str:
"""Render this template by applying it to `context`.
`context` is a dictionary of values to use in this rendering.
@@ -280,7 +292,7 @@ class Templite:
render_context.update(context)
return self._render_function(render_context, self._do_dots)
- def _do_dots(self, value, *dots):
+ def _do_dots(self, value: Any, *dots: str) -> Any:
"""Evaluate dotted expressions at run-time."""
for dot in dots:
try:
diff --git a/tests/test_templite.py b/tests/test_templite.py
index d2e98479..e34f7169 100644
--- a/tests/test_templite.py
+++ b/tests/test_templite.py
@@ -3,8 +3,13 @@
"""Tests for coverage.templite."""
+from __future__ import annotations
+
import re
+from types import SimpleNamespace
+from typing import Any, ContextManager, Dict, List, Optional
+
import pytest
from coverage.templite import Templite, TempliteSyntaxError, TempliteValueError
@@ -13,23 +18,18 @@ from tests.coveragetest import CoverageTest
# pylint: disable=possibly-unused-variable
-class AnyOldObject:
- """Simple testing object.
-
- Use keyword arguments in the constructor to set attributes on the object.
-
- """
- def __init__(self, **attrs):
- for n, v in attrs.items():
- setattr(self, n, v)
-
class TempliteTest(CoverageTest):
"""Tests for Templite."""
run_in_temp_dir = False
- def try_render(self, text, ctx=None, result=None):
+ def try_render(
+ self,
+ text: str,
+ ctx: Optional[Dict[str, Any]] = None,
+ result: Optional[str] = None,
+ ) -> None:
"""Render `text` through `ctx`, and it had better be `result`.
Result defaults to None so we can shorten the calls where we expect
@@ -42,30 +42,30 @@ class TempliteTest(CoverageTest):
assert result is not None
assert actual == result
- def assertSynErr(self, msg):
+ def assertSynErr(self, msg: str) -> ContextManager[None]:
"""Assert that a `TempliteSyntaxError` will happen.
A context manager, and the message should be `msg`.
"""
pat = "^" + re.escape(msg) + "$"
- return pytest.raises(TempliteSyntaxError, match=pat)
+ return pytest.raises(TempliteSyntaxError, match=pat) # type: ignore
- def test_passthrough(self):
+ def test_passthrough(self) -> None:
# Strings without variables are passed through unchanged.
assert Templite("Hello").render() == "Hello"
assert Templite("Hello, 20% fun time!").render() == "Hello, 20% fun time!"
- def test_variables(self):
+ def test_variables(self) -> None:
# Variables use {{var}} syntax.
self.try_render("Hello, {{name}}!", {'name':'Ned'}, "Hello, Ned!")
- def test_undefined_variables(self):
+ def test_undefined_variables(self) -> None:
# Using undefined names is an error.
with pytest.raises(Exception, match="'name'"):
self.try_render("Hi, {{name}}!")
- def test_pipes(self):
+ def test_pipes(self) -> None:
# Variables can be filtered with pipes.
data = {
'name': 'Ned',
@@ -77,7 +77,7 @@ class TempliteTest(CoverageTest):
# Pipes can be concatenated.
self.try_render("Hello, {{name|upper|second}}!", data, "Hello, E!")
- def test_reusability(self):
+ def test_reusability(self) -> None:
# A single Templite can be used more than once with different data.
globs = {
'upper': lambda x: x.upper(),
@@ -88,30 +88,30 @@ class TempliteTest(CoverageTest):
assert template.render({'name':'Ned'}) == "This is NED!"
assert template.render({'name':'Ben'}) == "This is BEN!"
- def test_attribute(self):
+ def test_attribute(self) -> None:
# Variables' attributes can be accessed with dots.
- obj = AnyOldObject(a="Ay")
+ obj = SimpleNamespace(a="Ay")
self.try_render("{{obj.a}}", locals(), "Ay")
- obj2 = AnyOldObject(obj=obj, b="Bee")
+ obj2 = SimpleNamespace(obj=obj, b="Bee")
self.try_render("{{obj2.obj.a}} {{obj2.b}}", locals(), "Ay Bee")
- def test_member_function(self):
+ def test_member_function(self) -> None:
# Variables' member functions can be used, as long as they are nullary.
- class WithMemberFns(AnyOldObject):
+ class WithMemberFns(SimpleNamespace):
"""A class to try out member function access."""
- def ditto(self):
+ def ditto(self) -> str:
"""Return twice the .txt attribute."""
- return self.txt + self.txt
+ return self.txt + self.txt # type: ignore
obj = WithMemberFns(txt="Once")
self.try_render("{{obj.ditto}}", locals(), "OnceOnce")
- def test_item_access(self):
+ def test_item_access(self) -> None:
# Variables' items can be used.
d = {'a':17, 'b':23}
self.try_render("{{d.a}} < {{d.b}}", locals(), "17 < 23")
- def test_loops(self):
+ def test_loops(self) -> None:
# Loops work like in Django.
nums = [1,2,3,4]
self.try_render(
@@ -120,7 +120,7 @@ class TempliteTest(CoverageTest):
"Look: 1, 2, 3, 4, done."
)
# Loop iterables can be filtered.
- def rev(l):
+ def rev(l: List[int]) -> List[int]:
"""Return the reverse of `l`."""
l = l[:]
l.reverse()
@@ -132,21 +132,21 @@ class TempliteTest(CoverageTest):
"Look: 4, 3, 2, 1, done."
)
- def test_empty_loops(self):
+ def test_empty_loops(self) -> None:
self.try_render(
"Empty: {% for n in nums %}{{n}}, {% endfor %}done.",
{'nums':[]},
"Empty: done."
)
- def test_multiline_loops(self):
+ def test_multiline_loops(self) -> None:
self.try_render(
"Look: \n{% for n in nums %}\n{{n}}, \n{% endfor %}done.",
{'nums':[1,2,3]},
"Look: \n\n1, \n\n2, \n\n3, \ndone."
)
- def test_multiple_loops(self):
+ def test_multiple_loops(self) -> None:
self.try_render(
"{% for n in nums %}{{n}}{% endfor %} and " +
"{% for n in nums %}{{n}}{% endfor %}",
@@ -154,7 +154,7 @@ class TempliteTest(CoverageTest):
"123 and 123"
)
- def test_comments(self):
+ def test_comments(self) -> None:
# Single-line comments work:
self.try_render(
"Hello, {# Name goes here: #}{{name}}!",
@@ -166,7 +166,7 @@ class TempliteTest(CoverageTest):
{'name':'Ned'}, "Hello, Ned!"
)
- def test_if(self):
+ def test_if(self) -> None:
self.try_render(
"Hi, {% if ned %}NED{% endif %}{% if ben %}BEN{% endif %}!",
{'ned': 1, 'ben': 0},
@@ -193,10 +193,10 @@ class TempliteTest(CoverageTest):
"Hi, NEDBEN!"
)
- def test_complex_if(self):
- class Complex(AnyOldObject):
+ def test_complex_if(self) -> None:
+ class Complex(SimpleNamespace):
"""A class to try out complex data access."""
- def getit(self):
+ def getit(self): # type: ignore
"""Return it."""
return self.it
obj = Complex(it={'x':"Hello", 'y': 0})
@@ -210,7 +210,7 @@ class TempliteTest(CoverageTest):
"@XS!"
)
- def test_loop_if(self):
+ def test_loop_if(self) -> None:
self.try_render(
"@{% for n in nums %}{% if n %}Z{% endif %}{{n}}{% endfor %}!",
{'nums': [0,1,2]},
@@ -227,7 +227,7 @@ class TempliteTest(CoverageTest):
"X!"
)
- def test_nested_loops(self):
+ def test_nested_loops(self) -> None:
self.try_render(
"@" +
"{% for n in nums %}" +
@@ -238,7 +238,7 @@ class TempliteTest(CoverageTest):
"@a0b0c0a1b1c1a2b2c2!"
)
- def test_whitespace_handling(self):
+ def test_whitespace_handling(self) -> None:
self.try_render(
"@{% for n in nums %}\n" +
" {% for a in abc %}{{a}}{{n}}{% endfor %}\n" +
@@ -268,7 +268,7 @@ class TempliteTest(CoverageTest):
)
self.try_render(" hello ", {}, " hello ")
- def test_eat_whitespace(self):
+ def test_eat_whitespace(self) -> None:
self.try_render(
"Hey!\n" +
"{% joined %}\n" +
@@ -286,14 +286,14 @@ class TempliteTest(CoverageTest):
"Hey!\n@XYa0XYb0XYc0XYa1XYb1XYc1XYa2XYb2XYc2!\n"
)
- def test_non_ascii(self):
+ def test_non_ascii(self) -> None:
self.try_render(
"{{where}} ollǝɥ",
{ 'where': 'ǝɹǝɥʇ' },
"ǝɹǝɥʇ ollǝɥ"
)
- def test_exception_during_evaluation(self):
+ def test_exception_during_evaluation(self) -> None:
# TypeError: Couldn't evaluate {{ foo.bar.baz }}:
regex = "^Couldn't evaluate None.bar$"
with pytest.raises(TempliteValueError, match=regex):
@@ -301,7 +301,7 @@ class TempliteTest(CoverageTest):
"Hey {{foo.bar.baz}} there", {'foo': None}, "Hey ??? there"
)
- def test_bad_names(self):
+ def test_bad_names(self) -> None:
with self.assertSynErr("Not a valid name: 'var%&!@'"):
self.try_render("Wat: {{ var%&!@ }}")
with self.assertSynErr("Not a valid name: 'filter%&!@'"):
@@ -309,17 +309,17 @@ class TempliteTest(CoverageTest):
with self.assertSynErr("Not a valid name: '@'"):
self.try_render("Wat: {% for @ in x %}{% endfor %}")
- def test_bogus_tag_syntax(self):
+ def test_bogus_tag_syntax(self) -> None:
with self.assertSynErr("Don't understand tag: 'bogus'"):
self.try_render("Huh: {% bogus %}!!{% endbogus %}??")
- def test_malformed_if(self):
+ def test_malformed_if(self) -> None:
with self.assertSynErr("Don't understand if: '{% if %}'"):
self.try_render("Buh? {% if %}hi!{% endif %}")
with self.assertSynErr("Don't understand if: '{% if this or that %}'"):
self.try_render("Buh? {% if this or that %}hi!{% endif %}")
- def test_malformed_for(self):
+ def test_malformed_for(self) -> None:
with self.assertSynErr("Don't understand for: '{% for %}'"):
self.try_render("Weird: {% for %}loop{% endfor %}")
with self.assertSynErr("Don't understand for: '{% for x from y %}'"):
@@ -327,7 +327,7 @@ class TempliteTest(CoverageTest):
with self.assertSynErr("Don't understand for: '{% for x, y in z %}'"):
self.try_render("Weird: {% for x, y in z %}loop{% endfor %}")
- def test_bad_nesting(self):
+ def test_bad_nesting(self) -> None:
with self.assertSynErr("Unmatched action tag: 'if'"):
self.try_render("{% if x %}X")
with self.assertSynErr("Mismatched end tag: 'for'"):
@@ -335,7 +335,7 @@ class TempliteTest(CoverageTest):
with self.assertSynErr("Too many ends: '{% endif %}'"):
self.try_render("{% if x %}{% endif %}{% endif %}")
- def test_malformed_end(self):
+ def test_malformed_end(self) -> None:
with self.assertSynErr("Don't understand end: '{% end if %}'"):
self.try_render("{% if x %}X{% end if %}")
with self.assertSynErr("Don't understand end: '{% endif now %}'"):
diff --git a/tox.ini b/tox.ini
index bf5f40ba..0308b5a3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -100,16 +100,19 @@ setenv =
C3=coverage/data.py coverage/debug.py coverage/disposition.py coverage/env.py coverage/exceptions.py
C4=coverage/files.py coverage/inorout.py coverage/jsonreport.py coverage/lcovreport.py coverage/misc.py coverage/multiproc.py coverage/numbits.py
C5=coverage/parser.py coverage/phystokens.py coverage/plugin.py coverage/plugin_support.py coverage/python.py
- C6=coverage/report.py coverage/results.py coverage/sqldata.py coverage/summary.py coverage/tomlconfig.py coverage/types.py coverage/version.py coverage/xmlreport.py
+ C6=coverage/report.py coverage/results.py coverage/sqldata.py coverage/summary.py
+ C7=coverage/templite.py coverage/tomlconfig.py coverage/types.py coverage/version.py coverage/xmlreport.py
+ TYPEABLE_C={env:C1} {env:C2} {env:C3} {env:C4} {env:C5} {env:C6} {env:C7}
T1=tests/conftest.py tests/coveragetest.py tests/goldtest.py tests/helpers.py tests/mixins.py tests/osinfo.py
T2=tests/test_annotate.py tests/test_api.py tests/test_arcs.py tests/test_cmdline.py tests/test_collector.py tests/test_concurrency.py
T3=tests/test_config.py tests/test_context.py tests/test_coverage.py tests/test_data.py tests/test_debug.py tests/test_execfile.py
T4=tests/test_filereporter.py tests/test_files.py tests/test_goldtest.py tests/test_html.py tests/test_json.py tests/test_lcov.py
T5=tests/test_misc.py tests/test_mixins.py tests/test_numbits.py tests/test_oddball.py tests/test_parser.py tests/test_phystokens.py
T6=tests/test_process.py tests/test_python.py tests/test_report.py tests/test_results.py tests/test_setup.py
- T7=tests/test_summary.py tests/test_testing.py tests/test_version.py tests/test_xml.py
- # not done yet: test_plugins.py test_templite.py test_venv.py
- TYPEABLE={env:C1} {env:C2} {env:C3} {env:C4} {env:C5} {env:C6} {env:T1} {env:T2} {env:T3} {env:T4} {env:T5} {env:T6} {env:T7}
+ T7=tests/test_summary.py tests/test_templite.py tests/test_testing.py tests/test_version.py tests/test_xml.py
+ # not done yet: test_plugins.py test_venv.py
+ TYPEABLE_T={env:T1} {env:T2} {env:T3} {env:T4} {env:T5} {env:T6} {env:T7}
+ TYPEABLE={env:TYPEABLE_C} {env:TYPEABLE_T}
commands =
# PYVERSIONS