Skip to content

Commit

Permalink
Implement --template-tags CLI flag
Browse files Browse the repository at this point in the history
  • Loading branch information
thibaudcolas committed Apr 18, 2021
1 parent fd8964a commit 77a0f1f
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 29 deletions.
24 changes: 23 additions & 1 deletion curlylint/cli.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import re
from functools import partial
from pathlib import Path
from typing import Any, Dict, Mapping, Optional, Pattern, Set, Tuple, Union
from typing import (
Any,
Dict,
List,
Mapping,
Optional,
Pattern,
Set,
Tuple,
Union,
)

import click # lgtm [py/import-and-import-from]

from curlylint.rule_param import RULE
from curlylint.template_tags_param import TEMPLATE_TAGS

from . import __version__
from .config import (
Expand Down Expand Up @@ -122,6 +133,15 @@ def path_empty(
),
multiple=True,
)
@click.option(
"--template-tags",
type=TEMPLATE_TAGS,
default="[]",
help=(
'Specify additional sets of template tags, with the syntax --template-tags \'[["cache", "endcache"]]\'. '
),
show_default=True,
)
@click.argument(
"src",
nargs=-1,
Expand Down Expand Up @@ -161,6 +181,7 @@ def main(
include: str,
exclude: str,
rule: Union[Mapping[str, Any], Tuple[Mapping[str, Any], ...]],
template_tags: List[List[str]],
src: Tuple[str, ...],
) -> None:
"""Prototype linter for Jinja and Django templates, forked from jinjalint"""
Expand Down Expand Up @@ -236,6 +257,7 @@ def main(
configuration["rules"] = rules
configuration["verbose"] = verbose
configuration["parse_only"] = parse_only
configuration["template_tags"] = template_tags

if stdin_filepath:
configuration["stdin_filepath"] = Path(stdin_filepath)
Expand Down
78 changes: 60 additions & 18 deletions curlylint/cli_test.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,53 @@
import unittest

from io import BytesIO
from typing import List

from curlylint.tests.utils import BlackRunner

from curlylint.cli import main


class TestParser(unittest.TestCase):
def test_no_flag(self):
class TestCLI(unittest.TestCase):
"""
Heavily inspired by Black’s CLI tests.
See https://github.com/psf/black/blob/master/tests/test_black.py.
"""

def invoke_curlylint(
self, exit_code: int, args: List[str], input: str = None
):
runner = BlackRunner()
result = runner.invoke(main, [])
result = runner.invoke(
main, args, input=BytesIO(input.encode("utf8")) if input else None
)
self.assertEqual(
result.exit_code,
exit_code,
msg=(
f"Failed with args: {args}\n"
f"stdout: {runner.stdout_bytes.decode()!r}\n"
f"stderr: {runner.stderr_bytes.decode()!r}\n"
f"exception: {result.exception}"
),
)
return runner

def test_no_flag(self):
runner = self.invoke_curlylint(0, [])
self.assertEqual(runner.stdout_bytes.decode(), "")
self.assertEqual(
runner.stderr_bytes.decode(), "No Path provided. Nothing to do 😴\n"
)
self.assertEqual(result.exit_code, 0)

def test_stdin(self):
runner = BlackRunner()
result = runner.invoke(
main, ["-"], input=BytesIO("<p>Hello, World!</p>".encode("utf8")),
)
runner = self.invoke_curlylint(0, ["-"], input="<p>Hello, World!</p>")
self.assertEqual(runner.stdout_bytes.decode(), "")
self.assertEqual(runner.stderr_bytes.decode(), "All done! ✨ 🍰 ✨\n\n")
self.assertEqual(result.exit_code, 0)

def test_stdin_verbose(self):
runner = BlackRunner()
result = runner.invoke(
main,
["--verbose", "-"],
input=BytesIO("<p>Hello, World!</p>".encode("utf8")),
runner = self.invoke_curlylint(
0, ["--verbose", "-"], input="<p>Hello, World!</p>"
)
self.assertEqual(runner.stdout_bytes.decode(), "")
self.assertIn(
Expand All @@ -45,14 +61,40 @@ def test_stdin_verbose(self):
""",
runner.stderr_bytes.decode(),
)
self.assertEqual(result.exit_code, 0)

def test_flag_help(self):
runner = BlackRunner()
result = runner.invoke(main, ["--help"])
runner = self.invoke_curlylint(0, ["--help"])
self.assertIn(
"Prototype linter for Jinja and Django templates",
runner.stdout_bytes.decode(),
)
self.assertEqual(runner.stderr_bytes.decode(), "")
self.assertEqual(result.exit_code, 0)

def test_template_tags_validation_fail_no_nesting(self):
runner = self.invoke_curlylint(
2,
["--template-tags", '["cache", "endcache"]', "-"],
input="<p>Hello, World!</p>",
)
self.assertIn(
"Error: Invalid value for '--template-tags': expected a list of lists of tags as JSON, got '[\"cache\", \"endcache\"]'",
runner.stderr_bytes.decode(),
)

def test_template_tags_cli_configured(self):
self.invoke_curlylint(
0,
["--template-tags", '[["of", "elseof", "endof"]]', "-"],
input="<p>{% of a %}c{% elseof %}test{% endof %}</p>",
)

def test_template_tags_cli_unconfigured_fails(self):
runner = self.invoke_curlylint(
1,
["--template-tags", "[]", "-"],
input="<p>{% of a %}c{% elseof %}test{% endof %}</p>",
)
self.assertIn(
"Parse error: expected one of 'autoescape', 'block', 'blocktrans', 'comment', 'filter', 'for', 'if', 'ifchanged', 'ifequal', 'ifnotequal', 'not an intermediate Jinja tag name', 'spaceless', 'verbatim', 'with' at 0:17\tparse_error",
runner.stdout_bytes.decode(),
)
2 changes: 2 additions & 0 deletions curlylint/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,8 @@ def make_jinja_parser(config, content):
(names[0], names)
for names in (
DEFAULT_JINJA_STRUCTURED_ELEMENTS_NAMES
+ config.get("template_tags", [])
# Deprecated, will be removed in a future release.
+ config.get("jinja_custom_elements_names", [])
)
).values()
Expand Down
32 changes: 24 additions & 8 deletions curlylint/parse_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,25 +372,25 @@ def test_jinja_blocks(self):
src = "{% if a %}b{% elif %}c{% elif %}d{% else %}e{% endif %}"
self.assertEqual(src, str(jinja.parse(src)))

def test_jinja_custom_block_self_closing(self):
def test_jinja_custom_tag_self_closing(self):
self.assertEqual(
jinja.parse("{% exampletest %}"),
jinja.parse("{% potato %}"),
JinjaElement(
parts=[
JinjaElementPart(
tag=JinjaTag(name="exampletest", content=""),
content=None,
tag=JinjaTag(name="potato", content=""), content=None,
)
],
closing_tag=None,
),
)

def test_jinja_custom_block_open_close_unconfigured(self):
def test_jinja_custom_tag_open_close_unconfigured(self):
with pytest.raises(P.ParseError):
jinja.parse("{% of a %}c{% endof %}")

def test_jinja_custom_block_open_close_configured(self):
def test_jinja_custom_tag_open_close_configured_deprecated(self):
# Deprecated, will be removed in a future release.
parser = make_parser({"jinja_custom_elements_names": [["of", "endof"]]})
jinja = parser["jinja"]
self.assertEqual(
Expand All @@ -406,11 +406,27 @@ def test_jinja_custom_block_open_close_configured(self):
),
)

def test_jinja_custom_block_open_middle_close_unconfigured(self):
def test_jinja_custom_tag_open_close_configured(self):
parser = make_parser({"template_tags": [["of", "endof"]]})
jinja = parser["jinja"]
self.assertEqual(
jinja.parse("{% of a %}c{% endof %}"),
JinjaElement(
parts=[
JinjaElementPart(
tag=JinjaTag(name="of", content="a"),
content=Interp(["c"]),
),
],
closing_tag=JinjaTag(name="endof", content=""),
),
)

def test_jinja_custom_tag_open_middle_close_unconfigured(self):
with pytest.raises(P.ParseError):
jinja.parse("{% of a %}b{% elseof %}c{% endof %}")

def test_jinja_custom_block_open_middle_close(self):
def test_jinja_custom_tag_open_middle_close(self):
parser = make_parser(
{"jinja_custom_elements_names": [["of", "elseof", "endof"]]}
)
Expand Down
35 changes: 35 additions & 0 deletions curlylint/template_tags_param.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import json

import click


class TemplateTagsParamType(click.ParamType):
"""
Validates and converts CLI-provided template tags configuration.
Expects: --template-tags '[["cache", "endcache"]]'
"""

name = "template tags"

def convert(self, value, param, ctx):
try:
if isinstance(value, str):
template_tags = json.loads(value)
else:
template_tags = value
# Validate the expected list of lists.
if not isinstance(template_tags, (list, tuple)):
raise ValueError
for tags in template_tags:
if not isinstance(tags, (list, tuple)):
raise ValueError
return template_tags
except ValueError:
self.fail(
f"expected a list of lists of tags as JSON, got {value!r}",
param,
ctx,
)


TEMPLATE_TAGS = TemplateTagsParamType()
5 changes: 3 additions & 2 deletions example_pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
[tool.curlylint]
# Specify additional Jinja elements which can wrap HTML here. You
# don't neet to specify simple elements which can't wrap anything like
# dont neet to specify simple elements which can't wrap anything like
# {% extends %} or {% include %}.
jinja-custom-elements-names = [
template_tags = [
["of", "elseof", "endof"],
["cache", "endcache"],
["captureas", "endcaptureas"]
]
Expand Down
10 changes: 10 additions & 0 deletions website/docs/command-line-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ curlylint --rule 'html_has_lang: "en"' template-directory/
curlylint --rule 'html_has_lang: ["en", "en-US"]' template-directory/
```

### `--template-tags`

Specify additional sets of template tags, with the syntax `--template-tags '[["start_tag", "end_tag"]]'`. This is only needed for tags that wrap other markup (like `{% block %}<p>Hello!</p>{% endblock %}`), not for single / “void” tags.

🚧 Note the list of lists is formatted as JSON, with each sub-list containing the tags expected to work together as opening/intermediary/closing tags.

```bash
curlylint --template-tags '[["cache", "endcache"]]' template-directory/
```

### `--config`

Read configuration from the provided file.
Expand Down

0 comments on commit 77a0f1f

Please sign in to comment.