python (3.11.7)

(root)/
lib/
python3.11/
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 captured_stderr() as stderr:
     114                      tabnanny.errprint(*args)
     115                  self.assertEqual(stderr.getvalue() , expected)
     116  
     117  
     118  class ESC[4;38;5;81mTestNannyNag(ESC[4;38;5;149mTestCase):
     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  
     145  class ESC[4;38;5;81mTestCheck(ESC[4;38;5;149mTestCase):
     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  
     257  class ESC[4;38;5;81mTestProcessTokens(ESC[4;38;5;149mTestCase):
     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  
     287  class ESC[4;38;5;81mTestCommandLine(ESC[4;38;5;149mTestCase):
     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 = os.fsdecode(out)
     296          err = os.fsdecode(err)
     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)