• 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
7import errno
8import os
9import tabnanny
10import tokenize
11import tempfile
12import textwrap
13from test.support import (captured_stderr, captured_stdout, script_helper,
14                          findfile)
15from test.support.os_helper import 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 self.assertRaises(SystemExit):
114                    with captured_stderr() as stderr:
115                        tabnanny.errprint(*args)
116                    self.assertEqual(stderr.getvalue() , expected)
117
118
119class TestNannyNag(TestCase):
120    def test_all_methods(self):
121        """Asserting behaviour of `tabnanny.NannyNag` exception."""
122        tests = [
123            (
124                tabnanny.NannyNag(0, "foo", "bar"),
125                {'lineno': 0, 'msg': 'foo', 'line': 'bar'}
126            ),
127            (
128                tabnanny.NannyNag(5, "testmsg", "testline"),
129                {'lineno': 5, 'msg': 'testmsg', 'line': 'testline'}
130            )
131        ]
132        for nanny, expected in tests:
133            line_number = nanny.get_lineno()
134            msg = nanny.get_msg()
135            line = nanny.get_line()
136            with self.subTest(
137                line_number=line_number, expected=expected['lineno']
138            ):
139                self.assertEqual(expected['lineno'], line_number)
140            with self.subTest(msg=msg, expected=expected['msg']):
141                self.assertEqual(expected['msg'], msg)
142            with self.subTest(line=line, expected=expected['line']):
143                self.assertEqual(expected['line'], line)
144
145
146class TestCheck(TestCase):
147    """Testing tabnanny.check()."""
148
149    def setUp(self):
150        self.addCleanup(setattr, tabnanny, 'verbose', tabnanny.verbose)
151        tabnanny.verbose = 0  # Forcefully deactivating verbose mode.
152
153    def verify_tabnanny_check(self, dir_or_file, out="", err=""):
154        """Common verification for tabnanny.check().
155
156        Use this method to assert expected values of `stdout` and `stderr` after
157        running tabnanny.check() on given `dir` or `file` path. Because
158        tabnanny.check() captures exceptions and writes to `stdout` and
159        `stderr`, asserting standard outputs is the only way.
160        """
161        with captured_stdout() as stdout, captured_stderr() as stderr:
162            tabnanny.check(dir_or_file)
163        self.assertEqual(stdout.getvalue(), out)
164        self.assertEqual(stderr.getvalue(), err)
165
166    def test_correct_file(self):
167        """A python source code file without any errors."""
168        with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path:
169            self.verify_tabnanny_check(file_path)
170
171    def test_correct_directory_verbose(self):
172        """Directory containing few error free python source code files.
173
174        Because order of files returned by `os.lsdir()` is not fixed, verify the
175        existence of each output lines at `stdout` using `in` operator.
176        `verbose` mode of `tabnanny.verbose` asserts `stdout`.
177        """
178        with tempfile.TemporaryDirectory() as tmp_dir:
179            lines = [f"{tmp_dir!r}: listing directory\n",]
180            file1 = TemporaryPyFile(SOURCE_CODES["error_free"], directory=tmp_dir)
181            file2 = TemporaryPyFile(SOURCE_CODES["error_free"], directory=tmp_dir)
182            with file1 as file1_path, file2 as file2_path:
183                for file_path in (file1_path, file2_path):
184                    lines.append(f"{file_path!r}: Clean bill of health.\n")
185
186                tabnanny.verbose = 1
187                with captured_stdout() as stdout, captured_stderr() as stderr:
188                    tabnanny.check(tmp_dir)
189                stdout = stdout.getvalue()
190                for line in lines:
191                    with self.subTest(line=line):
192                        self.assertIn(line, stdout)
193                self.assertEqual(stderr.getvalue(), "")
194
195    def test_correct_directory(self):
196        """Directory which contains few error free python source code files."""
197        with tempfile.TemporaryDirectory() as tmp_dir:
198            with TemporaryPyFile(SOURCE_CODES["error_free"], directory=tmp_dir):
199                self.verify_tabnanny_check(tmp_dir)
200
201    def test_when_wrong_indented(self):
202        """A python source code file eligible for raising `IndentationError`."""
203        with TemporaryPyFile(SOURCE_CODES["wrong_indented"]) as file_path:
204            err = ('unindent does not match any outer indentation level'
205                ' (<tokenize>, line 3)\n')
206            err = f"{file_path!r}: Indentation Error: {err}"
207            with self.assertRaises(SystemExit):
208                self.verify_tabnanny_check(file_path, err=err)
209
210    def test_when_tokenize_tokenerror(self):
211        """A python source code file eligible for raising 'tokenize.TokenError'."""
212        with TemporaryPyFile(SOURCE_CODES["incomplete_expression"]) as file_path:
213            err = "('EOF in multi-line statement', (7, 0))\n"
214            err = f"{file_path!r}: Token Error: {err}"
215            with self.assertRaises(SystemExit):
216                self.verify_tabnanny_check(file_path, err=err)
217
218    def test_when_nannynag_error_verbose(self):
219        """A python source code file eligible for raising `tabnanny.NannyNag`.
220
221        Tests will assert `stdout` after activating `tabnanny.verbose` mode.
222        """
223        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path:
224            out = f"{file_path!r}: *** Line 3: trouble in tab city! ***\n"
225            out += "offending line: '\\tprint(\"world\")'\n"
226            out += "inconsistent use of tabs and spaces in indentation\n"
227
228            tabnanny.verbose = 1
229            self.verify_tabnanny_check(file_path, out=out)
230
231    def test_when_nannynag_error(self):
232        """A python source code file eligible for raising `tabnanny.NannyNag`."""
233        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path:
234            out = f"{file_path} 3 '\\tprint(\"world\")'\n"
235            self.verify_tabnanny_check(file_path, out=out)
236
237    def test_when_no_file(self):
238        """A python file which does not exist actually in system."""
239        path = 'no_file.py'
240        err = (f"{path!r}: I/O Error: [Errno {errno.ENOENT}] "
241              f"{os.strerror(errno.ENOENT)}: {path!r}\n")
242        with self.assertRaises(SystemExit):
243            self.verify_tabnanny_check(path, err=err)
244
245    def test_errored_directory(self):
246        """Directory containing wrongly indented python source code files."""
247        with tempfile.TemporaryDirectory() as tmp_dir:
248            error_file = TemporaryPyFile(
249                SOURCE_CODES["wrong_indented"], directory=tmp_dir
250            )
251            code_file = TemporaryPyFile(
252                SOURCE_CODES["error_free"], directory=tmp_dir
253            )
254            with error_file as e_file, code_file as c_file:
255                err = ('unindent does not match any outer indentation level'
256                            ' (<tokenize>, line 3)\n')
257                err = f"{e_file!r}: Indentation Error: {err}"
258                with self.assertRaises(SystemExit):
259                    self.verify_tabnanny_check(tmp_dir, err=err)
260
261
262class TestProcessTokens(TestCase):
263    """Testing `tabnanny.process_tokens()`."""
264
265    @mock.patch('tabnanny.NannyNag')
266    def test_with_correct_code(self, MockNannyNag):
267        """A python source code without any whitespace related problems."""
268
269        with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path:
270            with open(file_path) as f:
271                tabnanny.process_tokens(tokenize.generate_tokens(f.readline))
272            self.assertFalse(MockNannyNag.called)
273
274    def test_with_errored_codes_samples(self):
275        """A python source code with whitespace related sampled problems."""
276
277        # "tab_space_errored_1": executes block under type == tokenize.INDENT
278        #                        at `tabnanny.process_tokens()`.
279        # "tab space_errored_2": executes block under
280        #                        `check_equal and type not in JUNK` condition at
281        #                        `tabnanny.process_tokens()`.
282
283        for key in ["tab_space_errored_1", "tab_space_errored_2"]:
284            with self.subTest(key=key):
285                with TemporaryPyFile(SOURCE_CODES[key]) as file_path:
286                    with open(file_path) as f:
287                        tokens = tokenize.generate_tokens(f.readline)
288                        with self.assertRaises(tabnanny.NannyNag):
289                            tabnanny.process_tokens(tokens)
290
291
292class TestCommandLine(TestCase):
293    """Tests command line interface of `tabnanny`."""
294
295    def validate_cmd(self, *args, stdout="", stderr="", partial=False, expect_failure=False):
296        """Common function to assert the behaviour of command line interface."""
297        if expect_failure:
298            _, out, err = script_helper.assert_python_failure('-m', 'tabnanny', *args)
299        else:
300            _, out, err = script_helper.assert_python_ok('-m', 'tabnanny', *args)
301        # Note: The `splitlines()` will solve the problem of CRLF(\r) added
302        # by OS Windows.
303        out = os.fsdecode(out)
304        err = os.fsdecode(err)
305        if partial:
306            for std, output in ((stdout, out), (stderr, err)):
307                _output = output.splitlines()
308                for _std in std.splitlines():
309                    with self.subTest(std=_std, output=_output):
310                        self.assertIn(_std, _output)
311        else:
312            self.assertListEqual(out.splitlines(), stdout.splitlines())
313            self.assertListEqual(err.splitlines(), stderr.splitlines())
314
315    def test_with_errored_file(self):
316        """Should displays error when errored python file is given."""
317        with TemporaryPyFile(SOURCE_CODES["wrong_indented"]) as file_path:
318            stderr  = f"{file_path!r}: Indentation Error: "
319            stderr += ('unindent does not match any outer indentation level'
320                       ' (<string>, line 3)')
321            self.validate_cmd(file_path, stderr=stderr, expect_failure=True)
322
323    def test_with_error_free_file(self):
324        """Should not display anything if python file is correctly indented."""
325        with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path:
326            self.validate_cmd(file_path)
327
328    def test_command_usage(self):
329        """Should display usage on no arguments."""
330        path = findfile('tabnanny.py')
331        stderr = f"Usage: {path} [-v] file_or_directory ..."
332        self.validate_cmd(stderr=stderr, expect_failure=True)
333
334    def test_quiet_flag(self):
335        """Should display less when quite mode is on."""
336        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path:
337            stdout = f"{file_path}\n"
338            self.validate_cmd("-q", file_path, stdout=stdout)
339
340    def test_verbose_mode(self):
341        """Should display more error information if verbose mode is on."""
342        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as path:
343            stdout = textwrap.dedent(
344                "offending line: '\\tprint(\"world\")'"
345            ).strip()
346            self.validate_cmd("-v", path, stdout=stdout, partial=True)
347
348    def test_double_verbose_mode(self):
349        """Should display detailed error information if double verbose is on."""
350        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as path:
351            stdout = textwrap.dedent(
352                "offending line: '\\tprint(\"world\")'"
353            ).strip()
354            self.validate_cmd("-vv", path, stdout=stdout, partial=True)
355