Coverage for manila/tests/hacking/checks.py: 83%
111 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright (c) 2012, Cloudscaling
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
16import ast
17import re
19from hacking import core
22"""
23Guidelines for writing new hacking checks
25 - Use only for Manila specific tests. OpenStack general tests
26 should be submitted to the common 'hacking' module.
27 - Pick numbers in the range M3xx. Find the current test with
28 the highest allocated number and then pick the next value.
29 - Keep the test method code in the source file ordered based
30 on the M3xx value.
31 - List the new rule in the top level HACKING.rst file
32 - Add test cases for each new rule to manila/tests/test_hacking.py
34"""
36UNDERSCORE_IMPORT_FILES = []
38translated_log = re.compile(
39 r"(.)*LOG\."
40 r"(audit|debug|error|info|warn|warning|critical|exception)"
41 r"\("
42 r"(_|_LE|_LI|_LW)"
43 r"\(")
44string_translation = re.compile(r"[^_]*_\(\s*('|\")")
45underscore_import_check = re.compile(r"(.)*import _$")
46underscore_import_check_multi = re.compile(r"(.)*import (.)*_, (.)*")
47# We need this for cases where they have created their own _ function.
48custom_underscore_check = re.compile(r"(.)*_\s*=\s*(.)*")
49dict_constructor_with_list_copy_re = re.compile(r".*\bdict\((\[)?(\(|\[)")
50assert_True = re.compile(r".*assertEqual\(True, .*\)")
51no_log_warn = re.compile(r"\s*LOG.warn\(.*")
54class BaseASTChecker(ast.NodeVisitor):
55 """Provides a simple framework for writing AST-based checks.
57 Subclasses should implement visit_* methods like any other AST visitor
58 implementation. When they detect an error for a particular node the
59 method should call ``self.add_error(offending_node)``. Details about
60 where in the code the error occurred will be pulled from the node
61 object.
63 Subclasses should also provide a class variable named CHECK_DESC to
64 be used for the human readable error message.
66 """
68 CHECK_DESC = 'No check message specified'
70 def __init__(self, tree, filename):
71 """This object is created automatically by pep8.
73 :param tree: an AST tree
74 :param filename: name of the file being analyzed
75 (ignored by our checks)
76 """
77 self._tree = tree
78 self._errors = []
80 def run(self):
81 """Called automatically by pep8."""
82 self.visit(self._tree)
83 return self._errors
85 def add_error(self, node, message=None):
86 """Add an error caused by a node to the list of errors for pep8."""
87 message = message or self.CHECK_DESC
88 error = (node.lineno, node.col_offset, message, self.__class__)
89 self._errors.append(error)
91 def _check_call_names(self, call_node, names):
92 if isinstance(call_node, ast.Call):
93 if isinstance(call_node.func, ast.Name): 93 ↛ 96line 93 didn't jump to line 96 because the condition on line 93 was always true
94 if call_node.func.id in names: 94 ↛ 96line 94 didn't jump to line 96 because the condition on line 94 was always true
95 return True
96 return False
99@core.flake8ext
100def no_translate_logs(logical_line):
101 if translated_log.match(logical_line):
102 yield (0, "M359 Don't translate log messages!")
105class CheckLoggingFormatArgs(BaseASTChecker):
106 """Check for improper use of logging format arguments.
108 LOG.debug("Volume %s caught fire and is at %d degrees C and climbing.",
109 ('volume1', 500))
111 The format arguments should not be a tuple as it is easy to miss.
113 """
115 name = "check_logging_format_args"
116 version = "1.0"
117 CHECK_DESC = 'M310 Log method arguments should not be a tuple.'
118 LOG_METHODS = [
119 'debug', 'info',
120 'warn', 'warning',
121 'error', 'exception',
122 'critical', 'fatal',
123 'trace', 'log'
124 ]
126 def _find_name(self, node):
127 """Return the fully qualified name or a Name or Attribute."""
128 if isinstance(node, ast.Name): 128 ↛ 130line 128 didn't jump to line 130 because the condition on line 128 was always true
129 return node.id
130 elif (isinstance(node, ast.Attribute)
131 and isinstance(node.value, (ast.Name, ast.Attribute))):
132 method_name = node.attr
133 obj_name = self._find_name(node.value)
134 if obj_name is None:
135 return None
136 return obj_name + '.' + method_name
137 elif isinstance(node, str):
138 return node
139 else: # could be Subscript, Call or many more
140 return None
142 def visit_Call(self, node):
143 """Look for the 'LOG.*' calls."""
144 # extract the obj_name and method_name
145 if isinstance(node.func, ast.Attribute):
146 obj_name = self._find_name(node.func.value)
147 if isinstance(node.func.value, ast.Name): 147 ↛ 149line 147 didn't jump to line 149 because the condition on line 147 was always true
148 method_name = node.func.attr
149 elif isinstance(node.func.value, ast.Attribute):
150 obj_name = self._find_name(node.func.value)
151 method_name = node.func.attr
152 else: # could be Subscript, Call or many more
153 return super(CheckLoggingFormatArgs, self).generic_visit(node)
155 # obj must be a logger instance and method must be a log helper
156 if (obj_name != 'LOG'
157 or method_name not in self.LOG_METHODS):
158 return super(CheckLoggingFormatArgs, self).generic_visit(node)
160 # the call must have arguments
161 if not len(node.args): 161 ↛ 162line 161 didn't jump to line 162 because the condition on line 161 was never true
162 return super(CheckLoggingFormatArgs, self).generic_visit(node)
164 # any argument should not be a tuple
165 for arg in node.args:
166 if isinstance(arg, ast.Tuple):
167 self.add_error(arg)
169 return super(CheckLoggingFormatArgs, self).generic_visit(node)
172@core.flake8ext
173def check_explicit_underscore_import(logical_line, filename):
174 """Check for explicit import of the _ function
176 We need to ensure that any files that are using the _() function
177 to translate logs are explicitly importing the _ function. We
178 can't trust unit test to catch whether the import has been
179 added so we need to check for it here.
180 """
182 # Build a list of the files that have _ imported. No further
183 # checking needed once it is found.
184 if filename in UNDERSCORE_IMPORT_FILES:
185 pass
186 elif (underscore_import_check.match(logical_line) or
187 underscore_import_check_multi.match(logical_line) or
188 custom_underscore_check.match(logical_line)):
189 UNDERSCORE_IMPORT_FILES.append(filename)
190 elif string_translation.match(logical_line):
191 yield (0, "M323: Found use of _() without explicit import of _ !")
194class CheckForTransAdd(BaseASTChecker):
195 """Checks for the use of concatenation on a translated string.
197 Translations should not be concatenated with other strings, but
198 should instead include the string being added to the translated
199 string to give the translators the most information.
200 """
202 name = "check_for_trans_add"
203 version = "1.0"
204 CHECK_DESC = ('M326 Translated messages cannot be concatenated. '
205 'String should be included in translated message.')
207 TRANS_FUNC = ['_', '_LI', '_LW', '_LE', '_LC']
209 def visit_BinOp(self, node):
210 if isinstance(node.op, ast.Add): 210 ↛ 215line 210 didn't jump to line 215 because the condition on line 210 was always true
211 if self._check_call_names(node.left, self.TRANS_FUNC):
212 self.add_error(node.left)
213 elif self._check_call_names(node.right, self.TRANS_FUNC):
214 self.add_error(node.right)
215 super(CheckForTransAdd, self).generic_visit(node)
218@core.flake8ext
219def dict_constructor_with_list_copy(logical_line):
220 msg = ("M336: Must use a dict comprehension instead of a dict constructor"
221 " with a sequence of key-value pairs."
222 )
223 if dict_constructor_with_list_copy_re.match(logical_line):
224 yield (0, msg)
227@core.flake8ext
228def validate_assertTrue(logical_line):
229 if re.match(assert_True, logical_line):
230 msg = ("M313: Unit tests should use assertTrue(value) instead"
231 " of using assertEqual(True, value).")
232 yield (0, msg)
235@core.flake8ext
236def check_uuid4(logical_line):
237 """Generating UUID
239 Use oslo_utils.uuidutils to generate UUID instead of uuid4().
241 M354
242 """
244 msg = ("M354: Use oslo_utils.uuidutils to generate UUID instead "
245 "of uuid4().")
247 if "uuid4()." in logical_line:
248 return
250 if "uuid4()" in logical_line:
251 yield (0, msg)
254@core.flake8ext
255def no_log_warn_check(logical_line):
256 """Disallow 'LOG.warn'
258 Deprecated LOG.warn(), instead use LOG.warning
259 ://bugs.launchpad.net/manila/+bug/1508442
261 M338
262 """
263 msg = ("M338: LOG.warn is deprecated, use LOG.warning.")
264 if re.match(no_log_warn, logical_line):
265 yield (0, msg)