• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Testing `tabnanny` module.
2
3Glossary:
4    * errored    : Whitespace related problems present in file.
5"""
6from unittest import TestCase, mock
7from unittest import mock
8import errno
9import os
10import tabnanny
11import tokenize
12import tempfile
13import textwrap
14from test.support import (captured_stderr, captured_stdout, script_helper,
15                          findfile, unlink)
16
17
18SOURCE_CODES = {
19    "incomplete_expression": (
20        'fruits = [\n'
21        '    "Apple",\n'
22        '    "Orange",\n'
23        '    "Banana",\n'
24        '\n'
25        'print(fruits)\n'
26    ),
27    "wrong_indented": (
28        'if True:\n'
29        '    print("hello")\n'
30        '  print("world")\n'
31        'else:\n'
32        '    print("else called")\n'
33    ),
34    "nannynag_errored": (
35        'if True:\n'
36        ' \tprint("hello")\n'
37        '\tprint("world")\n'
38        'else:\n'
39        '    print("else called")\n'
40    ),
41    "error_free": (
42        'if True:\n'
43        '    print("hello")\n'
44        '    print("world")\n'
45        'else:\n'
46        '    print("else called")\n'
47    ),
48    "tab_space_errored_1": (
49        'def my_func():\n'
50        '\t  print("hello world")\n'
51        '\t  if True:\n'
52        '\t\tprint("If called")'
53    ),
54    "tab_space_errored_2": (
55        'def my_func():\n'
56        '\t\tprint("Hello world")\n'
57        '\t\tif True:\n'
58        '\t        print("If called")'
59    )
60}
61
62
63class TemporaryPyFile:
64    """Create a temporary python source code file."""
65
66    def __init__(self, source_code='', directory=None):
67        self.source_code = source_code
68        self.dir = directory
69
70    def __enter__(self):
71        with tempfile.NamedTemporaryFile(
72            mode='w', dir=self.dir, suffix=".py", delete=False
73        ) as f:
74            f.write(self.source_code)
75        self.file_path = f.name
76        return self.file_path
77
78    def __exit__(self, exc_type, exc_value, exc_traceback):
79        unlink(self.file_path)
80
81
82class TestFormatWitnesses(TestCase):
83    """Testing `tabnanny.format_witnesses()`."""
84
85    def test_format_witnesses(self):
86        """Asserting formatter result by giving various input samples."""
87        tests = [
88            ('Test', 'at tab sizes T, e, s, t'),
89            ('', 'at tab size '),
90            ('t', 'at tab size t'),
91            ('  t  ', 'at tab sizes  ,  , t,  ,  '),
92        ]
93
94        for words, expected in tests:
95            with self.subTest(words=words, expected=expected):
96                self.assertEqual(tabnanny.format_witnesses(words), expected)
97
98
99class TestErrPrint(TestCase):
100    """Testing `tabnanny.errprint()`."""
101
102    def test_errprint(self):
103        """Asserting result of `tabnanny.errprint()` by giving sample inputs."""
104        tests = [
105            (['first', 'second'], 'first second\n'),
106            (['first'], 'first\n'),
107            ([1, 2, 3], '1 2 3\n'),
108            ([], '\n')
109        ]
110
111        for args, expected in tests:
112            with self.subTest(arguments=args, expected=expected):
113                with captured_stderr() as stderr:
114                    tabnanny.errprint(*args)
115                self.assertEqual(stderr.getvalue() , expected)
116
117
118class TestNannyNag(TestCase):
119    def test_all_methods(self):
120        """Asserting behaviour of `tabnanny.NannyNag` exception."""
121        tests = [
122            (
123                tabnanny.NannyNag(0, "foo", "bar"),
124                {'lineno': 0, 'msg': 'foo', 'line': 'bar'}
125            ),
126            (
127                tabnanny.NannyNag(5, "testmsg", "testline"),
128                {'lineno': 5, 'msg': 'testmsg', 'line': 'testline'}
129            )
130        ]
131        for nanny, expected in tests:
132            line_number = nanny.get_lineno()
133            msg = nanny.get_msg()
134            line = nanny.get_line()
135            with self.subTest(
136                line_number=line_number, expected=expected['lineno']
137            ):
138                self.assertEqual(expected['lineno'], line_number)
139            with self.subTest(msg=msg, expected=expected['msg']):
140                self.assertEqual(expected['msg'], msg)
141            with self.subTest(line=line, expected=expected['line']):
142                self.assertEqual(expected['line'], line)
143
144
145class TestCheck(TestCase):
146    """Testing tabnanny.check()."""
147
148    def setUp(self):
149        self.addCleanup(setattr, tabnanny, 'verbose', tabnanny.verbose)
150        tabnanny.verbose = 0  # Forcefully deactivating verbose mode.
151
152    def verify_tabnanny_check(self, dir_or_file, out="", err=""):
153        """Common verification for tabnanny.check().
154
155        Use this method to assert expected values of `stdout` and `stderr` after
156        running tabnanny.check() on given `dir` or `file` path. Because
157        tabnanny.check() captures exceptions and writes to `stdout` and
158        `stderr`, asserting standard outputs is the only way.
159        """
160        with captured_stdout() as stdout, captured_stderr() as stderr:
161            tabnanny.check(dir_or_file)
162        self.assertEqual(stdout.getvalue(), out)
163        self.assertEqual(stderr.getvalue(), err)
164
165    def test_correct_file(self):
166        """A python source code file without any errors."""
167        with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path:
168            self.verify_tabnanny_check(file_path)
169
170    def test_correct_directory_verbose(self):
171        """Directory containing few error free python source code files.
172
173        Because order of files returned by `os.lsdir()` is not fixed, verify the
174        existence of each output lines at `stdout` using `in` operator.
175        `verbose` mode of `tabnanny.verbose` asserts `stdout`.
176        """
177        with tempfile.TemporaryDirectory() as tmp_dir:
178            lines = [f"{tmp_dir!r}: listing directory\n",]
179            file1 = TemporaryPyFile(SOURCE_CODES["error_free"], directory=tmp_dir)
180            file2 = TemporaryPyFile(SOURCE_CODES["error_free"], directory=tmp_dir)
181            with file1 as file1_path, file2 as file2_path:
182                for file_path in (file1_path, file2_path):
183                    lines.append(f"{file_path!r}: Clean bill of health.\n")
184
185                tabnanny.verbose = 1
186                with captured_stdout() as stdout, captured_stderr() as stderr:
187                    tabnanny.check(tmp_dir)
188                stdout = stdout.getvalue()
189                for line in lines:
190                    with self.subTest(line=line):
191                        self.assertIn(line, stdout)
192                self.assertEqual(stderr.getvalue(), "")
193
194    def test_correct_directory(self):
195        """Directory which contains few error free python source code files."""
196        with tempfile.TemporaryDirectory() as tmp_dir:
197            with TemporaryPyFile(SOURCE_CODES["error_free"], directory=tmp_dir):
198                self.verify_tabnanny_check(tmp_dir)
199
200    def test_when_wrong_indented(self):
201        """A python source code file eligible for raising `IndentationError`."""
202        with TemporaryPyFile(SOURCE_CODES["wrong_indented"]) as file_path:
203            err = ('unindent does not match any outer indentation level'
204                ' (<tokenize>, line 3)\n')
205            err = f"{file_path!r}: Indentation Error: {err}"
206            self.verify_tabnanny_check(file_path, err=err)
207
208    def test_when_tokenize_tokenerror(self):
209        """A python source code file eligible for raising 'tokenize.TokenError'."""
210        with TemporaryPyFile(SOURCE_CODES["incomplete_expression"]) as file_path:
211            err = "('EOF in multi-line statement', (7, 0))\n"
212            err = f"{file_path!r}: Token Error: {err}"
213            self.verify_tabnanny_check(file_path, err=err)
214
215    def test_when_nannynag_error_verbose(self):
216        """A python source code file eligible for raising `tabnanny.NannyNag`.
217
218        Tests will assert `stdout` after activating `tabnanny.verbose` mode.
219        """
220        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path:
221            out = f"{file_path!r}: *** Line 3: trouble in tab city! ***\n"
222            out += "offending line: '\\tprint(\"world\")\\n'\n"
223            out += "indent not equal e.g. at tab size 1\n"
224
225            tabnanny.verbose = 1
226            self.verify_tabnanny_check(file_path, out=out)
227
228    def test_when_nannynag_error(self):
229        """A python source code file eligible for raising `tabnanny.NannyNag`."""
230        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path:
231            out = f"{file_path} 3 '\\tprint(\"world\")\\n'\n"
232            self.verify_tabnanny_check(file_path, out=out)
233
234    def test_when_no_file(self):
235        """A python file which does not exist actually in system."""
236        path = 'no_file.py'
237        err = (f"{path!r}: I/O Error: [Errno {errno.ENOENT}] "
238              f"{os.strerror(errno.ENOENT)}: {path!r}\n")
239        self.verify_tabnanny_check(path, err=err)
240
241    def test_errored_directory(self):
242        """Directory containing wrongly indented python source code files."""
243        with tempfile.TemporaryDirectory() as tmp_dir:
244            error_file = TemporaryPyFile(
245                SOURCE_CODES["wrong_indented"], directory=tmp_dir
246            )
247            code_file = TemporaryPyFile(
248                SOURCE_CODES["error_free"], directory=tmp_dir
249            )
250            with error_file as e_file, code_file as c_file:
251                err = ('unindent does not match any outer indentation level'
252                            ' (<tokenize>, line 3)\n')
253                err = f"{e_file!r}: Indentation Error: {err}"
254                self.verify_tabnanny_check(tmp_dir, err=err)
255
256
257class TestProcessTokens(TestCase):
258    """Testing `tabnanny.process_tokens()`."""
259
260    @mock.patch('tabnanny.NannyNag')
261    def test_with_correct_code(self, MockNannyNag):
262        """A python source code without any whitespace related problems."""
263
264        with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path:
265            with open(file_path) as f:
266                tabnanny.process_tokens(tokenize.generate_tokens(f.readline))
267            self.assertFalse(MockNannyNag.called)
268
269    def test_with_errored_codes_samples(self):
270        """A python source code with whitespace related sampled problems."""
271
272        # "tab_space_errored_1": executes block under type == tokenize.INDENT
273        #                        at `tabnanny.process_tokens()`.
274        # "tab space_errored_2": executes block under
275        #                        `check_equal and type not in JUNK` condition at
276        #                        `tabnanny.process_tokens()`.
277
278        for key in ["tab_space_errored_1", "tab_space_errored_2"]:
279            with self.subTest(key=key):
280                with TemporaryPyFile(SOURCE_CODES[key]) as file_path:
281                    with open(file_path) as f:
282                        tokens = tokenize.generate_tokens(f.readline)
283                        with self.assertRaises(tabnanny.NannyNag):
284                            tabnanny.process_tokens(tokens)
285
286
287class TestCommandLine(TestCase):
288    """Tests command line interface of `tabnanny`."""
289
290    def validate_cmd(self, *args, stdout="", stderr="", partial=False):
291        """Common function to assert the behaviour of command line interface."""
292        _, out, err = script_helper.assert_python_ok('-m', 'tabnanny', *args)
293        # Note: The `splitlines()` will solve the problem of CRLF(\r) added
294        # by OS Windows.
295        out = out.decode('ascii')
296        err = err.decode('ascii')
297        if partial:
298            for std, output in ((stdout, out), (stderr, err)):
299                _output = output.splitlines()
300                for _std in std.splitlines():
301                    with self.subTest(std=_std, output=_output):
302                        self.assertIn(_std, _output)
303        else:
304            self.assertListEqual(out.splitlines(), stdout.splitlines())
305            self.assertListEqual(err.splitlines(), stderr.splitlines())
306
307    def test_with_errored_file(self):
308        """Should displays error when errored python file is given."""
309        with TemporaryPyFile(SOURCE_CODES["wrong_indented"]) as file_path:
310            stderr  = f"{file_path!r}: Indentation Error: "
311            stderr += ('unindent does not match any outer indentation level'
312                    ' (<tokenize>, line 3)')
313            self.validate_cmd(file_path, stderr=stderr)
314
315    def test_with_error_free_file(self):
316        """Should not display anything if python file is correctly indented."""
317        with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path:
318            self.validate_cmd(file_path)
319
320    def test_command_usage(self):
321        """Should display usage on no arguments."""
322        path = findfile('tabnanny.py')
323        stderr = f"Usage: {path} [-v] file_or_directory ..."
324        self.validate_cmd(stderr=stderr)
325
326    def test_quiet_flag(self):
327        """Should display less when quite mode is on."""
328        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path:
329            stdout = f"{file_path}\n"
330            self.validate_cmd("-q", file_path, stdout=stdout)
331
332    def test_verbose_mode(self):
333        """Should display more error information if verbose mode is on."""
334        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as path:
335            stdout = textwrap.dedent(
336                "offending line: '\\tprint(\"world\")\\n'"
337            ).strip()
338            self.validate_cmd("-v", path, stdout=stdout, partial=True)
339
340    def test_double_verbose_mode(self):
341        """Should display detailed error information if double verbose is on."""
342        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as path:
343            stdout = textwrap.dedent(
344                "offending line: '\\tprint(\"world\")\\n'"
345            ).strip()
346            self.validate_cmd("-vv", path, stdout=stdout, partial=True)
347