8
8
"""
9
9
10
10
import ast
11
+ from pathlib import Path
12
+ from textwrap import dedent # for testing
11
13
12
14
from collections .abc import Generator
13
15
from typing import Any
17
19
"WH002 Prefer `sqlalchemy.orm.relationship(back_populates=...)` "
18
20
"over `sqlalchemy.orm.relationship(backref=...)`"
19
21
)
22
+ WH003_msg = "WH003 `@view_config.renderer` configured template file not found"
20
23
21
24
22
25
class WarehouseVisitor (ast .NodeVisitor ):
@@ -47,6 +50,24 @@ def _check_keywords(keywords: list[ast.keyword]) -> None:
47
50
):
48
51
_check_keywords (node .value .keywords )
49
52
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
+
50
71
def visit_Name (self , node : ast .Name ) -> None : # noqa: N802
51
72
if node .id == "urlparse" :
52
73
self .errors .append ((node .lineno , node .col_offset , WH001_msg ))
@@ -71,6 +92,25 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None: # noqa: N802
71
92
self .check_for_backref (node )
72
93
self .generic_visit (node )
73
94
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
+
74
114
75
115
class WarehouseCheck :
76
116
def __init__ (self , tree : ast .AST , filename : str ) -> None :
@@ -83,3 +123,29 @@ def run(self) -> Generator[tuple[int, int, str, type[Any]]]:
83
123
84
124
for e in visitor .errors :
85
125
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!" )
0 commit comments