| import contextlib | |
| import io | |
| import warnings | |
| import unittest | |
| from unittest.mock import patch | |
| from textwrap import dedent | |
| from test.support import force_not_colorized | |
| from _pyrepl.console import InteractiveColoredConsole | |
| from _pyrepl.simple_interact import _more_lines | |
| class TestSimpleInteract(unittest.TestCase): | |
| def test_multiple_statements(self): | |
| namespace = {} | |
| code = dedent("""\ | |
| class A: | |
| def foo(self): | |
| pass | |
| class B: | |
| def bar(self): | |
| pass | |
| a = 1 | |
| a | |
| """) | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| f = io.StringIO() | |
| with ( | |
| patch.object(InteractiveColoredConsole, "showsyntaxerror") as showsyntaxerror, | |
| patch.object(InteractiveColoredConsole, "runsource", wraps=console.runsource) as runsource, | |
| contextlib.redirect_stdout(f), | |
| ): | |
| more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg] | |
| self.assertFalse(more) | |
| showsyntaxerror.assert_not_called() | |
| def test_multiple_statements_output(self): | |
| namespace = {} | |
| code = dedent("""\ | |
| b = 1 | |
| b | |
| a = 1 | |
| a | |
| """) | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| f = io.StringIO() | |
| with contextlib.redirect_stdout(f): | |
| more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg] | |
| self.assertFalse(more) | |
| self.assertEqual(f.getvalue(), "1\n") | |
| @force_not_colorized | |
| def test_multiple_statements_fail_early(self): | |
| console = InteractiveColoredConsole() | |
| code = dedent("""\ | |
| raise Exception('foobar') | |
| print('spam', 'eggs', sep='&') | |
| """) | |
| f = io.StringIO() | |
| with contextlib.redirect_stderr(f): | |
| console.runsource(code) | |
| self.assertIn('Exception: foobar', f.getvalue()) | |
| self.assertNotIn('spam&eggs', f.getvalue()) | |
| def test_empty(self): | |
| namespace = {} | |
| code = "" | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| f = io.StringIO() | |
| with contextlib.redirect_stdout(f): | |
| more = console.push(code, filename="<stdin>", _symbol="single") # type: ignore[call-arg] | |
| self.assertFalse(more) | |
| self.assertEqual(f.getvalue(), "") | |
| def test_runsource_compiles_and_runs_code(self): | |
| console = InteractiveColoredConsole() | |
| source = "print('Hello, world!')" | |
| with patch.object(console, "runcode") as mock_runcode: | |
| console.runsource(source) | |
| mock_runcode.assert_called_once() | |
| def test_runsource_returns_false_for_successful_compilation(self): | |
| console = InteractiveColoredConsole() | |
| source = "print('Hello, world!')" | |
| f = io.StringIO() | |
| with contextlib.redirect_stdout(f): | |
| result = console.runsource(source) | |
| self.assertFalse(result) | |
| @force_not_colorized | |
| def test_runsource_returns_false_for_failed_compilation(self): | |
| console = InteractiveColoredConsole() | |
| source = "print('Hello, world!'" | |
| f = io.StringIO() | |
| with contextlib.redirect_stderr(f): | |
| result = console.runsource(source) | |
| self.assertFalse(result) | |
| self.assertIn('SyntaxError', f.getvalue()) | |
| @force_not_colorized | |
| def test_runsource_show_syntax_error_location(self): | |
| console = InteractiveColoredConsole() | |
| source = "def f(x, x): ..." | |
| f = io.StringIO() | |
| with contextlib.redirect_stderr(f): | |
| result = console.runsource(source) | |
| self.assertFalse(result) | |
| r = """ | |
| def f(x, x): ... | |
| ^ | |
| SyntaxError: duplicate parameter 'x' in function definition""" | |
| self.assertIn(r, f.getvalue()) | |
| def test_runsource_shows_syntax_error_for_failed_compilation(self): | |
| console = InteractiveColoredConsole() | |
| source = "print('Hello, world!'" | |
| with patch.object(console, "showsyntaxerror") as mock_showsyntaxerror: | |
| console.runsource(source) | |
| mock_showsyntaxerror.assert_called_once() | |
| source = dedent("""\ | |
| match 1: | |
| case {0: _, 0j: _}: | |
| pass | |
| """) | |
| with patch.object(console, "showsyntaxerror") as mock_showsyntaxerror: | |
| console.runsource(source) | |
| mock_showsyntaxerror.assert_called_once() | |
| @force_not_colorized | |
| def test_runsource_compile_error(self): | |
| # Any error raised by compile() is reported (gh-69919). | |
| console = InteractiveColoredConsole() | |
| source = '-' * 100_000 + '1' | |
| f = io.StringIO() | |
| with contextlib.redirect_stderr(f): | |
| result = console.runsource(source) | |
| self.assertFalse(result) | |
| self.assertRegex(f.getvalue(), r'^(MemoryError|RecursionError): ') | |
| def test_runsource_survives_null_bytes(self): | |
| console = InteractiveColoredConsole() | |
| source = "\x00\n" | |
| f = io.StringIO() | |
| with contextlib.redirect_stdout(f), contextlib.redirect_stderr(f): | |
| result = console.runsource(source) | |
| self.assertFalse(result) | |
| self.assertIn("source code string cannot contain null bytes", f.getvalue()) | |
| def test_no_active_future(self): | |
| console = InteractiveColoredConsole() | |
| source = dedent("""\ | |
| x: int = 1 | |
| print(__annotate__(1)) | |
| """) | |
| f = io.StringIO() | |
| with contextlib.redirect_stdout(f): | |
| result = console.runsource(source) | |
| self.assertFalse(result) | |
| self.assertEqual(f.getvalue(), "{'x': <class 'int'>}\n") | |
| def test_future_annotations(self): | |
| console = InteractiveColoredConsole() | |
| source = dedent("""\ | |
| from __future__ import annotations | |
| def g(x: int): ... | |
| print(g.__annotations__) | |
| """) | |
| f = io.StringIO() | |
| with contextlib.redirect_stdout(f): | |
| result = console.runsource(source) | |
| self.assertFalse(result) | |
| self.assertEqual(f.getvalue(), "{'x': 'int'}\n") | |
| def test_future_barry_as_flufl(self): | |
| console = InteractiveColoredConsole() | |
| f = io.StringIO() | |
| with contextlib.redirect_stdout(f): | |
| result = console.runsource("from __future__ import barry_as_FLUFL\n") | |
| result = console.runsource("""print("black" <> 'blue')\n""") | |
| self.assertFalse(result) | |
| self.assertEqual(f.getvalue(), "True\n") | |
| class TestMoreLines(unittest.TestCase): | |
| def test_invalid_syntax_single_line(self): | |
| namespace = {} | |
| code = "if foo" | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| self.assertFalse(_more_lines(console, code)) | |
| def test_compile_error_single_line(self): | |
| namespace = {} | |
| code = '-' * 100_000 + '1' # MemoryError or RecursionError | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| self.assertFalse(_more_lines(console, code)) | |
| def test_empty_line(self): | |
| namespace = {} | |
| code = "" | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| self.assertFalse(_more_lines(console, code)) | |
| def test_valid_single_statement(self): | |
| namespace = {} | |
| code = "foo = 1" | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| self.assertFalse(_more_lines(console, code)) | |
| def test_multiline_single_assignment(self): | |
| namespace = {} | |
| code = dedent("""\ | |
| foo = [ | |
| 1, | |
| 2, | |
| 3, | |
| ]""") | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| self.assertFalse(_more_lines(console, code)) | |
| def test_multiline_single_block(self): | |
| namespace = {} | |
| code = dedent("""\ | |
| def foo(): | |
| '''docs''' | |
| return 1""") | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| self.assertTrue(_more_lines(console, code)) | |
| def test_multiple_statements_single_line(self): | |
| namespace = {} | |
| code = "foo = 1;bar = 2" | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| self.assertFalse(_more_lines(console, code)) | |
| def test_multiple_statements(self): | |
| namespace = {} | |
| code = dedent("""\ | |
| import time | |
| foo = 1""") | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| self.assertTrue(_more_lines(console, code)) | |
| def test_multiple_blocks(self): | |
| namespace = {} | |
| code = dedent("""\ | |
| from dataclasses import dataclass | |
| @dataclass | |
| class Point: | |
| x: float | |
| y: float""") | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| self.assertTrue(_more_lines(console, code)) | |
| def test_multiple_blocks_empty_newline(self): | |
| namespace = {} | |
| code = dedent("""\ | |
| from dataclasses import dataclass | |
| @dataclass | |
| class Point: | |
| x: float | |
| y: float | |
| """) | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| self.assertFalse(_more_lines(console, code)) | |
| def test_multiple_blocks_indented_newline(self): | |
| namespace = {} | |
| code = ( | |
| "from dataclasses import dataclass\n" | |
| "\n" | |
| "@dataclass\n" | |
| "class Point:\n" | |
| " x: float\n" | |
| " y: float\n" | |
| " " | |
| ) | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| self.assertFalse(_more_lines(console, code)) | |
| def test_incomplete_statement(self): | |
| namespace = {} | |
| code = "if foo:" | |
| console = InteractiveColoredConsole(namespace, filename="<stdin>") | |
| self.assertTrue(_more_lines(console, code)) | |
| class TestWarnings(unittest.TestCase): | |
| def test_pep_765_warning(self): | |
| """ | |
| Test that a SyntaxWarning emitted from the | |
| AST optimizer is only shown once in the REPL. | |
| """ | |
| # gh-131927 | |
| console = InteractiveColoredConsole() | |
| code = dedent("""\ | |
| def f(): | |
| try: | |
| return 1 | |
| finally: | |
| return 2 | |
| """) | |
| with warnings.catch_warnings(record=True) as caught: | |
| warnings.simplefilter("always") | |
| console.runsource(code) | |
| count = sum("'return' in a 'finally' block" in str(w.message) | |
| for w in caught) | |
| self.assertEqual(count, 1) |