Skip to content

Commit 977667c

Browse files
authored
test: add checker for metrics tags (#18927)
Adds a check to verify that the `tags` value passed to metrics calls is a list of strings, not a dictionary. Would be better via `mypy`, but the lack of underlying types for Pyramid makes this much harder, and would need to build out custom stubs. Refs: #18926 Signed-off-by: Mike Fiedler <miketheman@gmail.com>
1 parent 9653855 commit 977667c

File tree

1 file changed

+82
-1
lines changed

1 file changed

+82
-1
lines changed

dev/flake8/checkers.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"over `sqlalchemy.orm.relationship(backref=...)`"
2121
)
2222
WH003_msg = "WH003 `@view_config.renderer` configured template file not found"
23+
# TODO: This would be better served by mypy, no types in Pyramid makes this harder.
24+
# Support https://github.com/Pylons/pyramid/issues/2638 for general Pyramid type info.
25+
WH004_msg = "WH004 metrics tags must be a list of strings"
2326

2427

2528
class WarehouseVisitor(ast.NodeVisitor):
@@ -101,6 +104,46 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None: # noqa: N802
101104
self.check_for_backref(node)
102105
self.generic_visit(node)
103106

107+
def is_metrics_method_call(self, node: ast.Call) -> bool:
108+
"""Check if this is a call to a metrics method."""
109+
if not isinstance(node.func, ast.Attribute):
110+
return False
111+
112+
# Check for metrics.<method>()
113+
if isinstance(node.func.value, ast.Name) and node.func.value.id == "metrics":
114+
return True
115+
116+
# Check for request.metrics.<method>() or any_obj.metrics.<method>()
117+
if (
118+
isinstance(node.func.value, ast.Attribute)
119+
and node.func.value.attr == "metrics"
120+
):
121+
return True
122+
123+
return False
124+
125+
def check_metrics_tags(self, node: ast.Call) -> None:
126+
"""Check that tags parameter in metrics calls is a list."""
127+
if not self.is_metrics_method_call(node):
128+
return
129+
130+
# Check keyword arguments for tags=
131+
for kw in node.keywords:
132+
if kw.arg == "tags":
133+
# tags should be None, a variable (Name), or a List
134+
# Flag if it's a literal non-list type (string, tuple, dict, set, etc.)
135+
if isinstance(kw.value, (ast.Constant, ast.Tuple, ast.Dict, ast.Set)):
136+
# Allow None
137+
if isinstance(kw.value, ast.Constant) and kw.value.value is None:
138+
continue
139+
self.errors.append(
140+
(kw.value.lineno, kw.value.col_offset, WH004_msg)
141+
)
142+
143+
def visit_Call(self, node: ast.Call) -> None: # noqa: N802
144+
self.check_metrics_tags(node)
145+
self.generic_visit(node)
146+
104147
def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802
105148
for decorator in node.decorator_list:
106149
if (
@@ -173,7 +216,45 @@ def my_view(request):
173216
assert len(visitor.errors) == 0
174217

175218

219+
def test_wh004_metrics_tags_invalid_types():
220+
# Test case: Invalid tag types (should error)
221+
code = dedent(
222+
"""
223+
metrics.increment("counter", tags="string")
224+
request.metrics.gauge("gauge", tags=("tuple",))
225+
metrics.histogram("hist", tags={"dict": "value"})
226+
"""
227+
)
228+
tree = ast.parse(code)
229+
visitor = WarehouseVisitor(filename="test_file.py")
230+
visitor.visit(tree)
231+
232+
# Assert that all 3 errors are raised
233+
assert len(visitor.errors) == 3
234+
assert all(error[2] == WH004_msg for error in visitor.errors)
235+
236+
237+
def test_wh004_metrics_tags_valid_types():
238+
# Test case: Valid tag types (should not error)
239+
code = dedent(
240+
"""
241+
metrics.increment("counter", tags=["tag1", "tag2"])
242+
request.metrics.gauge("gauge", tags=None)
243+
tag_list = ["tag1"]
244+
metrics.histogram("hist", tags=tag_list)
245+
"""
246+
)
247+
tree = ast.parse(code)
248+
visitor = WarehouseVisitor(filename="test_file.py")
249+
visitor.visit(tree)
250+
251+
# Assert that no errors are raised
252+
assert len(visitor.errors) == 0
253+
254+
176255
if __name__ == "__main__":
177256
test_wh003_renderer_template_not_found()
178257
test_wh003_renderer_template_in_package_path()
179-
print("Test passed!")
258+
test_wh004_metrics_tags_invalid_types()
259+
test_wh004_metrics_tags_valid_types()
260+
print("All tests passed!")

0 commit comments

Comments
 (0)