Skip to content

Commit a3a7880

Browse files
authored
lint: check @view_config.renderer filenames (#18366)
1 parent 41b3aba commit a3a7880

File tree

2 files changed

+67
-1
lines changed

2 files changed

+67
-1
lines changed

dev/flake8/checkers.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"""
99

1010
import ast
11+
from pathlib import Path
12+
from textwrap import dedent # for testing
1113

1214
from collections.abc import Generator
1315
from typing import Any
@@ -17,6 +19,7 @@
1719
"WH002 Prefer `sqlalchemy.orm.relationship(back_populates=...)` "
1820
"over `sqlalchemy.orm.relationship(backref=...)`"
1921
)
22+
WH003_msg = "WH003 `@view_config.renderer` configured template file not found"
2023

2124

2225
class WarehouseVisitor(ast.NodeVisitor):
@@ -47,6 +50,24 @@ def _check_keywords(keywords: list[ast.keyword]) -> None:
4750
):
4851
_check_keywords(node.value.keywords)
4952

53+
def template_exists(self, template_name: str) -> bool:
54+
settings = {}
55+
# TODO: Replace with actual configuration retrieval if it makes sense
56+
# Get Jinja2 search paths from warehouse config
57+
# settings = configure().get_settings()
58+
search_paths = settings.get("jinja2.searchpath", [])
59+
# If not set, fallback to default templates path
60+
if not search_paths:
61+
repo_root = Path(__file__).parent.parent.parent
62+
search_paths = [
63+
str(repo_root / "warehouse" / "templates"),
64+
str(repo_root / "warehouse" / "admin" / "templates"),
65+
]
66+
for path in search_paths:
67+
if Path(path, template_name).is_file():
68+
return True
69+
return False
70+
5071
def visit_Name(self, node: ast.Name) -> None: # noqa: N802
5172
if node.id == "urlparse":
5273
self.errors.append((node.lineno, node.col_offset, WH001_msg))
@@ -71,6 +92,25 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None: # noqa: N802
7192
self.check_for_backref(node)
7293
self.generic_visit(node)
7394

95+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802
96+
for decorator in node.decorator_list:
97+
if (
98+
isinstance(decorator, ast.Call)
99+
and getattr(decorator.func, "id", None) == "view_config"
100+
):
101+
for kw in decorator.keywords:
102+
if (
103+
kw.arg == "renderer"
104+
and isinstance(kw.value, ast.Constant)
105+
# TODO: Is there a "string-that-looks-like-a-filename"?
106+
and kw.value.value not in ["json", "xmlrpc", "string"]
107+
):
108+
if not self.template_exists(kw.value.value):
109+
self.errors.append(
110+
(kw.value.lineno, kw.value.col_offset, WH003_msg)
111+
)
112+
self.generic_visit(node)
113+
74114

75115
class WarehouseCheck:
76116
def __init__(self, tree: ast.AST, filename: str) -> None:
@@ -83,3 +123,29 @@ def run(self) -> Generator[tuple[int, int, str, type[Any]]]:
83123

84124
for e in visitor.errors:
85125
yield *e, type(self)
126+
127+
128+
# Testing
129+
def test_wh003_renderer_template_not_found():
130+
# Simulate a Python file with a @view_config decorator and a non-existent template
131+
code = dedent(
132+
"""
133+
from pyramid.view import view_config
134+
135+
@view_config(renderer="non_existent_template.html")
136+
def my_view(request):
137+
pass
138+
"""
139+
)
140+
tree = ast.parse(code)
141+
visitor = WarehouseVisitor(filename="test_file.py")
142+
visitor.visit(tree)
143+
144+
# Assert that the WH003 error is raised
145+
assert len(visitor.errors) == 1
146+
assert visitor.errors[0][2] == WH003_msg
147+
148+
149+
if __name__ == "__main__":
150+
test_wh003_renderer_template_not_found()
151+
print("Test passed!")

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[flake8]
22
max-line-length = 88
3-
exclude = *.egg,*/interfaces.py,node_modules,.state
3+
exclude = *.egg,*/interfaces.py,node_modules,.state,.venv
44
ignore = W503,E203,E701
55
select = E,W,F,N,P
66
per-file-ignores =

0 commit comments

Comments
 (0)