|
20 | 20 | "over `sqlalchemy.orm.relationship(backref=...)`" |
21 | 21 | ) |
22 | 22 | 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" |
23 | 26 |
|
24 | 27 |
|
25 | 28 | class WarehouseVisitor(ast.NodeVisitor): |
@@ -101,6 +104,46 @@ def visit_AnnAssign(self, node: ast.AnnAssign) -> None: # noqa: N802 |
101 | 104 | self.check_for_backref(node) |
102 | 105 | self.generic_visit(node) |
103 | 106 |
|
| 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 | + |
104 | 147 | def visit_FunctionDef(self, node: ast.FunctionDef) -> None: # noqa: N802 |
105 | 148 | for decorator in node.decorator_list: |
106 | 149 | if ( |
@@ -173,7 +216,45 @@ def my_view(request): |
173 | 216 | assert len(visitor.errors) == 0 |
174 | 217 |
|
175 | 218 |
|
| 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 | + |
176 | 255 | if __name__ == "__main__": |
177 | 256 | test_wh003_renderer_template_not_found() |
178 | 257 | 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