(root)/
Python-3.12.0/
Lib/
test/
test_tabnanny.py
       1  """Testing `tabnanny` module.
       2  
       3  Glossary:
       4      * errored    : Whitespace related problems present in file.
       5  """
       6  from unittest import TestCase, mock
       7  import errno
       8  import os
       9  import tabnanny
      10  import tokenize
      11  import tempfile
      12  import textwrap
      13  from test.support import (captured_stderr, captured_stdout, script_helper,
      14                            findfile)
      15  from test.support.os_helper import unlink
      16  
      17  
      18  SOURCE_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  
      63  class ESC[4;38;5;81mTemporaryPyFile:
      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  
      82  class ESC[4;38;5;81mTestFormatWitnesses(ESC[4;38;5;149mTestCase):
      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  
      99  class ESC[4;38;5;81mTestErrPrint(ESC[4;38;5;149mTestCase):
     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  
     119  class ESC[4;38;5;81mTestNannyNag(ESC[4;38;5;149mTestCase):
     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  
     146  class ESC[4;38;5;81mTestCheck(ESC[4;38;5;149mTestCase):
     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  
     262  class ESC[4;38;5;81mTestProcessTokens(ESC[4;38;5;149mTestCase):
     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  
     292  class ESC[4;38;5;81mTestCommandLine(ESC[4;38;5;149mTestCase):
     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}: Token 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)