/
test_code_formatting.py
301 lines (266 loc) · 11.5 KB
/
test_code_formatting.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function,
unicode_literals)
from future.builtins import * # NOQA @UnusedWildImport
import codecs
import fnmatch
import os
import re
import unittest
import obspy
from obspy.core.util.misc import get_untracked_files_from_git
from obspy.core.util.testing import get_all_py_files
try:
import flake8
except ImportError: # pragma: no cover
HAS_FLAKE8_AT_LEAST_VERSION_3 = False
else:
if int(flake8.__version__.split(".")[0]) >= 3:
HAS_FLAKE8_AT_LEAST_VERSION_3 = True
else: # pragma: no cover
HAS_FLAKE8_AT_LEAST_VERSION_3 = False
# List of flake8 error codes to ignore. Keep it as small as possible - there
# usually is little reason to fight flake8.
# NOTE: Keep consistent between..
# - obspy/core/tests/test_code_formatting.py FLAKE8_IGNORE_CODES
# - .circleci/config.yml --ignore
FLAKE8_IGNORE_CODES = [
# E402 module level import not at top of file
# This is really annoying when using the standard library import hooks
# from the future package.
"E402",
"E504",
"W504",
# E133 closing bracket is missing indentation
# this is an Error shown for one alternative form of closing bracket,
# closing it without indentation with regard to opening line. This gets
# raised when --hang-closing is selected to allow the form with 4 spaces
# as indent (which is valid according to PEP8 but raised by pycodestyle)
"E133",
]
FLAKE8_EXCLUDE_FILES = [
"*/__init__.py",
]
_pattern = re.compile(r"^\d+\.\d+\.\d+$")
CLEAN_VERSION_NUMBER = bool(_pattern.match(obspy.__version__))
def _match_exceptions(filename, exceptions):
for pattern in exceptions:
if fnmatch.fnmatch(filename, pattern):
return True
return False
class CodeFormattingTestCase(unittest.TestCase):
"""
Test codebase for compliance with the flake8 tool.
"""
@unittest.skipIf(CLEAN_VERSION_NUMBER,
"No code formatting tests for release builds")
@unittest.skipIf(not HAS_FLAKE8_AT_LEAST_VERSION_3,
"Formatting tests require at least flake8 3.0.")
@unittest.skipIf('OBSPY_NO_FLAKE8' in os.environ, 'flake8 check disabled')
def test_flake8(self):
"""
Test codebase for compliance with the flake8 tool.
"""
# Import the legacy API as flake8 3.0 currently has not official
# public API - this has to be changed at some point.
from flake8.api import legacy as flake8
# not sure if there's a better way to get a hold of default ignore
# codes..
default_ignore_codes = \
flake8.get_style_guide().options.__dict__['ignore']
try:
import pycodestyle
except ImportError:
pass
else:
default_ignore_codes += pycodestyle.DEFAULT_IGNORE.split(',')
ignore_codes = list(set(default_ignore_codes + FLAKE8_IGNORE_CODES))
# --hang-closing allows valid indented closing brackets, see
# https://github.com/PyCQA/pycodestyle/issues/103#issuecomment-17366719
style_guide = flake8.get_style_guide(
ignore=ignore_codes, hang_closing=True)
untracked_files = get_untracked_files_from_git() or []
files = []
for filename in get_all_py_files():
if filename in untracked_files:
continue
for pattern in FLAKE8_EXCLUDE_FILES:
if fnmatch.fnmatch(filename, pattern):
break
else:
files.append(filename)
report = style_guide.check_files(files)
# Make sure no error occurred.
assert report.total_errors == 0
@unittest.skipIf(CLEAN_VERSION_NUMBER,
"No code formatting tests for release builds")
def test_use_obspy_deprecation_warning(self):
"""
Tests that ObsPyDeprecationWarning is used rather than the usual
DeprecationWarning when using `warnings.warn()`
(because the latter is not shown by Python by default anymore).
"""
msg = ("File '%s' seems to use DeprecationWarning instead of "
"obspy.core.util.deprecation_helpers.ObsPyDeprecationWarning:"
"\n\n%s")
pattern = r'warn\([^)]*?([\w]*?)DeprecationWarning[^)]*\)'
failures = []
for filename in get_all_py_files():
with codecs.open(filename, "r", encoding="utf-8") as fh:
content = fh.read()
for match in re.finditer(pattern, content):
if match.group(1) != 'ObsPy':
failures.append(msg % (filename, match.group(0)))
self.assertEqual(len(failures), 0, "\n" + "\n".join(failures))
class FutureUsageTestCase(unittest.TestCase):
@unittest.skipIf(CLEAN_VERSION_NUMBER,
"No code formatting tests for release builds")
def test_future_imports_in_every_file(self):
"""
Tests that every single Python file includes the appropriate future
headers to enforce consistent behavior.
"""
# There are currently only three exceptions. Two files are imported
# during installation and thus cannot contain future imports. The
# third file is the compatibility layer which naturally also does
# not want to import future.
exceptions = [
os.path.join('core', 'util', 'libnames.py'),
os.path.join('core', 'util', 'version.py'),
os.path.join('core', 'compatibility.py'),
os.path.join('lib', '*'),
]
exceptions = [os.path.join("*", "obspy", i) for i in exceptions]
future_import_line = (
"from __future__ import (absolute_import, division, "
"print_function, unicode_literals)")
builtins_line = "from future.builtins import * # NOQA"
future_imports_pattern = re.compile(
r"^from __future__ import \(absolute_import,\s*"
r"division,\s*print_function,\s*unicode_literals\)$",
flags=re.MULTILINE)
builtin_pattern = re.compile(
r"^from future\.builtins import \* # NOQA",
flags=re.MULTILINE)
failures = []
for filename in get_all_py_files():
if _match_exceptions(filename, exceptions):
continue
with codecs.open(filename, "r", encoding="utf-8") as fh:
content = fh.read()
if re.search(future_imports_pattern, content) is None:
failures.append("File '%s' misses imports: %s" %
(filename, future_import_line))
if re.search(builtin_pattern, content) is None:
failures.append("File '%s' misses imports: %s" %
(filename, builtins_line))
self.assertEqual(len(failures), 0, "\n" + "\n".join(failures))
class MatplotlibBackendUsageTestCase(unittest.TestCase):
patterns = (
r" *from pylab import",
r" *from pylab\..*? import",
r" *import pylab",
r" *from matplotlib import (.*?, *)*(pyplot|backends)",
r" *import matplotlib\.(pyplot|backends)")
def forbidden_match(self, line):
for pattern in self.patterns:
if re.match(pattern, line):
return pattern
return False
def test_no_pyplot_regex(self):
"""
Tests that the regex patterns to match forbidden lines works
as expected.
"""
positives = (
'from pylab import something',
' from pylab import something',
'from pylab.something import something',
'import pylab',
' import pylab',
'from matplotlib import pyplot',
' from matplotlib import pyplot',
'from matplotlib import backends',
' from matplotlib import backends',
'from matplotlib import dates, backends',
'import matplotlib.pyplot as plt',
'import matplotlib.pyplot',
'import matplotlib.backends',
' import matplotlib.backends',
)
negatives = (
'#from pylab import something',
'# from pylab import something',
'# from pylab import something',
'import os # from pylab import something',
'#import pylab',
'# import pylab',
' #from matplotlib import pyplot',
'# from matplotlib import pyplot',
'#from matplotlib import backends',
'# from matplotlib import backends',
'#from matplotlib import dates, backends',
' # import matplotlib.pyplot as plt',
'import os # import matplotlib.pyplot',
' # import matplotlib.backends',
'from cryptography.hazmat.backends import default_backend',
)
for line in positives:
self.assertTrue(
self.forbidden_match(line),
msg="Line '{}' should be detected as forbidden but it was "
"not.".format(line))
for line in negatives:
pattern = self.forbidden_match(line)
self.assertFalse(
pattern,
msg="Line '{}' should not be detected as forbidden but it "
"was, by pattern '{}'.".format(line, pattern))
@unittest.skipIf(CLEAN_VERSION_NUMBER,
"No code formatting tests for release builds")
def test_no_pyplot_import_in_any_file(self):
"""
Tests that no Python file spoils matplotlib backend switching by
importing e.g. `matplotlib.pyplot` (not enclosed in a def/class
statement).
"""
msg = ("File '{}' (line {})\nmatches a forbidden matplotlib import "
"statement outside of class/def statements\n(breaking "
"matplotlib backend switching on some systems):\n '{}'")
exceptions = [
os.path.join('io', 'css', 'contrib', 'css28fix.py'),
os.path.join('*', 'tests', '*'),
os.path.join('*', '*', 'tests', '*'),
]
exceptions = [os.path.join("*", "obspy", i) for i in exceptions]
failures = []
for filename in get_all_py_files():
if _match_exceptions(filename, exceptions):
continue
line_number = 1
in_docstring = False
with codecs.open(filename, "r", encoding="utf-8") as fh:
line = fh.readline()
while line:
# detect start/end of docstring
if re.match(r"['\"]{3}", line):
in_docstring = not in_docstring
# skip if inside docstring
if not in_docstring:
# stop searching at first unindented class/def
if re.match(r"(class)|(def) ", line):
break
if self.forbidden_match(line) is not False:
failures.append(msg.format(
filename, line_number, line.rstrip()))
line = fh.readline()
line_number += 1
self.assertEqual(len(failures), 0, "\n" + "\n\n".join(failures))
def suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(CodeFormattingTestCase, 'test'))
suite.addTest(unittest.makeSuite(FutureUsageTestCase, 'test'))
suite.addTest(unittest.makeSuite(MatplotlibBackendUsageTestCase, 'test'))
return suite
if __name__ == '__main__':
unittest.main(defaultTest='suite')