diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2023-01-07 22:54:05 -0500 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2023-01-07 22:54:05 -0500 |
commit | 08564c09144b2223be808f49b001c8856966bd46 (patch) | |
tree | 3bfe15a660292e1463448988c95de683a4a553cd | |
parent | 65aad086ccccea33f0ff9535c9612e1b4b6712ce (diff) | |
download | python-coveragepy-git-08564c09144b2223be808f49b001c8856966bd46.tar.gz |
mypy: templite.py test_templite.py
-rw-r--r-- | coverage/templite.py | 52 | ||||
-rw-r--r-- | tests/test_templite.py | 96 | ||||
-rw-r--r-- | tox.ini | 11 |
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 %}'"): @@ -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 |