From 77cb3401059b689fc347a9a80df399acf19716eb Mon Sep 17 00:00:00 2001 From: Wulian Date: Sat, 5 Oct 2024 15:50:28 +0800 Subject: [PATCH 1/9] Fixed barry_as_FLUFL future flag does not work in new REPL --- Lib/_pyrepl/console.py | 10 ++++++++ Lib/test/test_pyrepl/test_interact.py | 23 +++++++++++++++++++ ...-10-05-15-49-53.gh-issue-124960.Bol9hT.rst | 2 ++ 3 files changed, 35 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-10-05-15-49-53.gh-issue-124960.Bol9hT.rst diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 3e72a56807f6fb..9a3c136e539f9c 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -160,6 +160,13 @@ def __init__( ) -> None: super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg] self.can_colorize = _colorize.can_colorize() + self.barry_as_FLUFL = False + + def check_barry_as_FLUFL(self, tree): + for node in ast.walk(tree): + if isinstance(node, ast.ImportFrom) and node.module == "__future__": + if any(alias.name == "barry_as_FLUFL" for alias in node.names): + self.barry_as_FLUFL = True def showsyntaxerror(self, filename=None, **kwargs): super().showsyntaxerror(filename=filename, **kwargs) @@ -173,8 +180,11 @@ def _excepthook(self, typ, value, tb): self.write(''.join(lines)) def runsource(self, source, filename="", symbol="single"): + if self.barry_as_FLUFL: + source = source.replace("<>", "!=") try: tree = ast.parse(source) + self.check_barry_as_FLUFL(tree) except (SyntaxError, OverflowError, ValueError): self.showsyntaxerror(filename, source=source) return False diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index b7adaffbac0e22..e1136086e06dd4 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -102,6 +102,29 @@ def f(x, x): ... SyntaxError: duplicate argument 'x' in function definition""" self.assertIn(r, f.getvalue()) + @force_not_colorized + def test_check_barry_as_FLUFL_show_syntax_error(self): + console = InteractiveColoredConsole() + source = "1 <> 2" + f = io.StringIO() + with contextlib.redirect_stderr(f): + result = console.runsource(source) + self.assertFalse(result) + r = """ + 1 <> 2 + ^^ +SyntaxError: invalid syntax""" + self.assertIn(r, f.getvalue()) + + @force_not_colorized + def test_check_barry_as_FLUFL(self): + console = InteractiveColoredConsole() + source = "from __future__ import barry_as_FLUFL; 1 <> 2" + f = io.StringIO() + with contextlib.redirect_stderr(f): + result = console.runsource(source) + self.assertEqual(result, True) + def test_runsource_shows_syntax_error_for_failed_compilation(self): console = InteractiveColoredConsole() source = "print('Hello, world!'" diff --git a/Misc/NEWS.d/next/Library/2024-10-05-15-49-53.gh-issue-124960.Bol9hT.rst b/Misc/NEWS.d/next/Library/2024-10-05-15-49-53.gh-issue-124960.Bol9hT.rst new file mode 100644 index 00000000000000..dd9b431840e6e5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-10-05-15-49-53.gh-issue-124960.Bol9hT.rst @@ -0,0 +1,2 @@ +Fixed barry_as_FLUFL future flag does not work in new REPL. Patch by Jiahao +Li From 9127c6c953af97e14d343f4d6ea10d9929b3bf4e Mon Sep 17 00:00:00 2001 From: Wulian Date: Sat, 5 Oct 2024 17:41:31 +0800 Subject: [PATCH 2/9] WIP: warning: can't use pyrepl: from __future__ imports must occur at the beginning of the file (console.py, line 21) --- Lib/_pyrepl/console.py | 4 ++-- Lib/test/test_pyrepl/test_interact.py | 11 ++--------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 9a3c136e539f9c..50bd353aab20fa 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -16,6 +16,7 @@ # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +import __future__ from __future__ import annotations @@ -166,6 +167,7 @@ def check_barry_as_FLUFL(self, tree): for node in ast.walk(tree): if isinstance(node, ast.ImportFrom) and node.module == "__future__": if any(alias.name == "barry_as_FLUFL" for alias in node.names): + self.compile.compiler.flags |= getattr(__future__, "barry_as_FLUFL").compiler_flag self.barry_as_FLUFL = True def showsyntaxerror(self, filename=None, **kwargs): @@ -180,8 +182,6 @@ def _excepthook(self, typ, value, tb): self.write(''.join(lines)) def runsource(self, source, filename="", symbol="single"): - if self.barry_as_FLUFL: - source = source.replace("<>", "!=") try: tree = ast.parse(source) self.check_barry_as_FLUFL(tree) diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index e1136086e06dd4..c58fed479cc684 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -116,15 +116,6 @@ def test_check_barry_as_FLUFL_show_syntax_error(self): SyntaxError: invalid syntax""" self.assertIn(r, f.getvalue()) - @force_not_colorized - def test_check_barry_as_FLUFL(self): - console = InteractiveColoredConsole() - source = "from __future__ import barry_as_FLUFL; 1 <> 2" - f = io.StringIO() - with contextlib.redirect_stderr(f): - result = console.runsource(source) - self.assertEqual(result, True) - def test_runsource_shows_syntax_error_for_failed_compilation(self): console = InteractiveColoredConsole() source = "print('Hello, world!'" @@ -249,3 +240,5 @@ def test_incomplete_statement(self): code = "if foo:" console = InteractiveColoredConsole(namespace, filename="") self.assertTrue(_more_lines(console, code)) + +unittest.main() From d0049a3982ae6fb85b628ac1b87bede92c3cf45f Mon Sep 17 00:00:00 2001 From: Wulian <1055917385@qq.com> Date: Sat, 5 Oct 2024 17:52:25 +0800 Subject: [PATCH 3/9] Update test_interact.py --- Lib/test/test_pyrepl/test_interact.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index c58fed479cc684..b7adaffbac0e22 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -102,20 +102,6 @@ def f(x, x): ... SyntaxError: duplicate argument 'x' in function definition""" self.assertIn(r, f.getvalue()) - @force_not_colorized - def test_check_barry_as_FLUFL_show_syntax_error(self): - console = InteractiveColoredConsole() - source = "1 <> 2" - f = io.StringIO() - with contextlib.redirect_stderr(f): - result = console.runsource(source) - self.assertFalse(result) - r = """ - 1 <> 2 - ^^ -SyntaxError: invalid syntax""" - self.assertIn(r, f.getvalue()) - def test_runsource_shows_syntax_error_for_failed_compilation(self): console = InteractiveColoredConsole() source = "print('Hello, world!'" @@ -240,5 +226,3 @@ def test_incomplete_statement(self): code = "if foo:" console = InteractiveColoredConsole(namespace, filename="") self.assertTrue(_more_lines(console, code)) - -unittest.main() From 904c2029b67cd7da1025507198d031cdaee40160 Mon Sep 17 00:00:00 2001 From: Wulian Date: Sat, 5 Oct 2024 19:05:28 +0800 Subject: [PATCH 4/9] WIP --- Lib/_pyrepl/console.py | 3 +++ .../Library/2024-10-05-15-49-53.gh-issue-124960.Bol9hT.rst | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 50bd353aab20fa..00055cc34e992d 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -164,11 +164,14 @@ def __init__( self.barry_as_FLUFL = False def check_barry_as_FLUFL(self, tree): + if self.barry_as_FLUFL: + return for node in ast.walk(tree): if isinstance(node, ast.ImportFrom) and node.module == "__future__": if any(alias.name == "barry_as_FLUFL" for alias in node.names): self.compile.compiler.flags |= getattr(__future__, "barry_as_FLUFL").compiler_flag self.barry_as_FLUFL = True + break def showsyntaxerror(self, filename=None, **kwargs): super().showsyntaxerror(filename=filename, **kwargs) diff --git a/Misc/NEWS.d/next/Library/2024-10-05-15-49-53.gh-issue-124960.Bol9hT.rst b/Misc/NEWS.d/next/Library/2024-10-05-15-49-53.gh-issue-124960.Bol9hT.rst index dd9b431840e6e5..332d6bb54d80c7 100644 --- a/Misc/NEWS.d/next/Library/2024-10-05-15-49-53.gh-issue-124960.Bol9hT.rst +++ b/Misc/NEWS.d/next/Library/2024-10-05-15-49-53.gh-issue-124960.Bol9hT.rst @@ -1,2 +1 @@ -Fixed barry_as_FLUFL future flag does not work in new REPL. Patch by Jiahao -Li +Fix support for the ``barry_as_FLUFL`` future flag in the new REPL. From cc807ee4cbc8571cb2a39d4f863dbebeba4c4764 Mon Sep 17 00:00:00 2001 From: Wulian <1055917385@qq.com> Date: Sun, 6 Oct 2024 14:13:38 +0800 Subject: [PATCH 5/9] Co-authored-by: Nice Zombies Co-authored-by: Nice Zombies --- Lib/_pyrepl/console.py | 425 ++++++++++++++++++++--------------------- Lib/codeop.py | 307 ++++++++++++++--------------- 2 files changed, 361 insertions(+), 371 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 00055cc34e992d..69a76ac24664f0 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -1,219 +1,206 @@ -# Copyright 2000-2004 Michael Hudson-Doyle -# -# All Rights Reserved -# -# -# Permission to use, copy, modify, and distribute this software and -# its documentation for any purpose is hereby granted without fee, -# provided that the above copyright notice appear in all copies and -# that both that copyright notice and this permission notice appear in -# supporting documentation. -# -# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO -# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, -# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -import __future__ - -from __future__ import annotations - -import _colorize # type: ignore[import-not-found] - -from abc import ABC, abstractmethod -import ast -import code -from dataclasses import dataclass, field -import os.path -import sys - - -TYPE_CHECKING = False - -if TYPE_CHECKING: - from typing import IO - from typing import Callable - - -@dataclass -class Event: - evt: str - data: str - raw: bytes = b"" - - -@dataclass -class Console(ABC): - screen: list[str] = field(default_factory=list) - height: int = 25 - width: int = 80 - - def __init__( - self, - f_in: IO[bytes] | int = 0, - f_out: IO[bytes] | int = 1, - term: str = "", - encoding: str = "", - ): - self.encoding = encoding or sys.getdefaultencoding() - - if isinstance(f_in, int): - self.input_fd = f_in - else: - self.input_fd = f_in.fileno() - - if isinstance(f_out, int): - self.output_fd = f_out - else: - self.output_fd = f_out.fileno() - - @abstractmethod - def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ... - - @abstractmethod - def prepare(self) -> None: ... - - @abstractmethod - def restore(self) -> None: ... - - @abstractmethod - def move_cursor(self, x: int, y: int) -> None: ... - - @abstractmethod - def set_cursor_vis(self, visible: bool) -> None: ... - - @abstractmethod - def getheightwidth(self) -> tuple[int, int]: - """Return (height, width) where height and width are the height - and width of the terminal window in characters.""" - ... - - @abstractmethod - def get_event(self, block: bool = True) -> Event | None: - """Return an Event instance. Returns None if |block| is false - and there is no event pending, otherwise waits for the - completion of an event.""" - ... - - @abstractmethod - def push_char(self, char: int | bytes) -> None: - """ - Push a character to the console event queue. - """ - ... - - @abstractmethod - def beep(self) -> None: ... - - @abstractmethod - def clear(self) -> None: - """Wipe the screen""" - ... - - @abstractmethod - def finish(self) -> None: - """Move the cursor to the end of the display and otherwise get - ready for end. XXX could be merged with restore? Hmm.""" - ... - - @abstractmethod - def flushoutput(self) -> None: - """Flush all output to the screen (assuming there's some - buffering going on somewhere).""" - ... - - @abstractmethod - def forgetinput(self) -> None: - """Forget all pending, but not yet processed input.""" - ... - - @abstractmethod - def getpending(self) -> Event: - """Return the characters that have been typed but not yet - processed.""" - ... - - @abstractmethod - def wait(self, timeout: float | None) -> bool: - """Wait for an event. The return value is True if an event is - available, False if the timeout has been reached. If timeout is - None, wait forever. The timeout is in milliseconds.""" - ... - - @property - def input_hook(self) -> Callable[[], int] | None: - """Returns the current input hook.""" - ... - - @abstractmethod - def repaint(self) -> None: ... - - -class InteractiveColoredConsole(code.InteractiveConsole): - def __init__( - self, - locals: dict[str, object] | None = None, - filename: str = "", - *, - local_exit: bool = False, - ) -> None: - super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg] - self.can_colorize = _colorize.can_colorize() - self.barry_as_FLUFL = False - - def check_barry_as_FLUFL(self, tree): - if self.barry_as_FLUFL: - return - for node in ast.walk(tree): - if isinstance(node, ast.ImportFrom) and node.module == "__future__": - if any(alias.name == "barry_as_FLUFL" for alias in node.names): - self.compile.compiler.flags |= getattr(__future__, "barry_as_FLUFL").compiler_flag - self.barry_as_FLUFL = True - break - - def showsyntaxerror(self, filename=None, **kwargs): - super().showsyntaxerror(filename=filename, **kwargs) - - def _excepthook(self, typ, value, tb): - import traceback - lines = traceback.format_exception( - typ, value, tb, - colorize=self.can_colorize, - limit=traceback.BUILTIN_EXCEPTION_LIMIT) - self.write(''.join(lines)) - - def runsource(self, source, filename="", symbol="single"): - try: - tree = ast.parse(source) - self.check_barry_as_FLUFL(tree) - except (SyntaxError, OverflowError, ValueError): - self.showsyntaxerror(filename, source=source) - return False - if tree.body: - *_, last_stmt = tree.body - for stmt in tree.body: - wrapper = ast.Interactive if stmt is last_stmt else ast.Module - the_symbol = symbol if stmt is last_stmt else "exec" - item = wrapper([stmt]) - try: - code = self.compile.compiler(item, filename, the_symbol, dont_inherit=True) - except SyntaxError as e: - if e.args[0] == "'await' outside function": - python = os.path.basename(sys.executable) - e.add_note( - f"Try the asyncio REPL ({python} -m asyncio) to use" - f" top-level 'await' and run background asyncio tasks." - ) - self.showsyntaxerror(filename, source=source) - return False - except (OverflowError, ValueError): - self.showsyntaxerror(filename, source=source) - return False - - if code is None: - return True - - self.runcode(code) - return False +# Copyright 2000-2004 Michael Hudson-Doyle +# +# All Rights Reserved +# +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose is hereby granted without fee, +# provided that the above copyright notice appear in all copies and +# that both that copyright notice and this permission notice appear in +# supporting documentation. +# +# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO +# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, +# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import annotations + +import _colorize # type: ignore[import-not-found] + +from abc import ABC, abstractmethod +import ast +import code +from dataclasses import dataclass, field +import os.path +import sys + + +TYPE_CHECKING = False + +if TYPE_CHECKING: + from typing import IO + from typing import Callable + + +@dataclass +class Event: + evt: str + data: str + raw: bytes = b"" + + +@dataclass +class Console(ABC): + screen: list[str] = field(default_factory=list) + height: int = 25 + width: int = 80 + + def __init__( + self, + f_in: IO[bytes] | int = 0, + f_out: IO[bytes] | int = 1, + term: str = "", + encoding: str = "", + ): + self.encoding = encoding or sys.getdefaultencoding() + + if isinstance(f_in, int): + self.input_fd = f_in + else: + self.input_fd = f_in.fileno() + + if isinstance(f_out, int): + self.output_fd = f_out + else: + self.output_fd = f_out.fileno() + + @abstractmethod + def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ... + + @abstractmethod + def prepare(self) -> None: ... + + @abstractmethod + def restore(self) -> None: ... + + @abstractmethod + def move_cursor(self, x: int, y: int) -> None: ... + + @abstractmethod + def set_cursor_vis(self, visible: bool) -> None: ... + + @abstractmethod + def getheightwidth(self) -> tuple[int, int]: + """Return (height, width) where height and width are the height + and width of the terminal window in characters.""" + ... + + @abstractmethod + def get_event(self, block: bool = True) -> Event | None: + """Return an Event instance. Returns None if |block| is false + and there is no event pending, otherwise waits for the + completion of an event.""" + ... + + @abstractmethod + def push_char(self, char: int | bytes) -> None: + """ + Push a character to the console event queue. + """ + ... + + @abstractmethod + def beep(self) -> None: ... + + @abstractmethod + def clear(self) -> None: + """Wipe the screen""" + ... + + @abstractmethod + def finish(self) -> None: + """Move the cursor to the end of the display and otherwise get + ready for end. XXX could be merged with restore? Hmm.""" + ... + + @abstractmethod + def flushoutput(self) -> None: + """Flush all output to the screen (assuming there's some + buffering going on somewhere).""" + ... + + @abstractmethod + def forgetinput(self) -> None: + """Forget all pending, but not yet processed input.""" + ... + + @abstractmethod + def getpending(self) -> Event: + """Return the characters that have been typed but not yet + processed.""" + ... + + @abstractmethod + def wait(self, timeout: float | None) -> bool: + """Wait for an event. The return value is True if an event is + available, False if the timeout has been reached. If timeout is + None, wait forever. The timeout is in milliseconds.""" + ... + + @property + def input_hook(self) -> Callable[[], int] | None: + """Returns the current input hook.""" + ... + + @abstractmethod + def repaint(self) -> None: ... + + +class InteractiveColoredConsole(code.InteractiveConsole): + def __init__( + self, + locals: dict[str, object] | None = None, + filename: str = "", + *, + local_exit: bool = False, + ) -> None: + super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg] + self.can_colorize = _colorize.can_colorize() + + def showsyntaxerror(self, filename=None, **kwargs): + super().showsyntaxerror(filename=filename, **kwargs) + + def _excepthook(self, typ, value, tb): + import traceback + lines = traceback.format_exception( + typ, value, tb, + colorize=self.can_colorize, + limit=traceback.BUILTIN_EXCEPTION_LIMIT) + self.write(''.join(lines)) + + def runsource(self, source, filename="", symbol="single"): + try: + tree = self.compile.compiler(source, filename, symbol, ast.PyCF_ONLY_AST) + except (SyntaxError, OverflowError, ValueError): + self.showsyntaxerror(filename, source=source) + return False + if tree.body: + *_, last_stmt = tree.body + for stmt in tree.body: + wrapper = ast.Interactive if stmt is last_stmt else ast.Module + the_symbol = symbol if stmt is last_stmt else "exec" + item = wrapper([stmt]) + try: + code = self.compile.compiler(item, filename, the_symbol, dont_inherit=True) + except SyntaxError as e: + if e.args[0] == "'await' outside function": + python = os.path.basename(sys.executable) + e.add_note( + f"Try the asyncio REPL ({python} -m asyncio) to use" + f" top-level 'await' and run background asyncio tasks." + ) + self.showsyntaxerror(filename, source=source) + return False + except (OverflowError, ValueError): + self.showsyntaxerror(filename, source=source) + return False + + if code is None: + return True + + self.runcode(code) + return False diff --git a/Lib/codeop.py b/Lib/codeop.py index a0276b52d484e3..acf757d89e6879 100644 --- a/Lib/codeop.py +++ b/Lib/codeop.py @@ -1,152 +1,155 @@ -r"""Utilities to compile possibly incomplete Python source code. - -This module provides two interfaces, broadly similar to the builtin -function compile(), which take program text, a filename and a 'mode' -and: - -- Return code object if the command is complete and valid -- Return None if the command is incomplete -- Raise SyntaxError, ValueError or OverflowError if the command is a - syntax error (OverflowError and ValueError can be produced by - malformed literals). - -The two interfaces are: - -compile_command(source, filename, symbol): - - Compiles a single command in the manner described above. - -CommandCompiler(): - - Instances of this class have __call__ methods identical in - signature to compile_command; the difference is that if the - instance compiles program text containing a __future__ statement, - the instance 'remembers' and compiles all subsequent program texts - with the statement in force. - -The module also provides another class: - -Compile(): - - Instances of this class act like the built-in function compile, - but with 'memory' in the sense described above. -""" - -import __future__ -import warnings - -_features = [getattr(__future__, fname) - for fname in __future__.all_feature_names] - -__all__ = ["compile_command", "Compile", "CommandCompiler"] - -# The following flags match the values from Include/cpython/compile.h -# Caveat emptor: These flags are undocumented on purpose and depending -# on their effect outside the standard library is **unsupported**. -PyCF_DONT_IMPLY_DEDENT = 0x200 -PyCF_ALLOW_INCOMPLETE_INPUT = 0x4000 - -def _maybe_compile(compiler, source, filename, symbol): - # Check for source consisting of only blank lines and comments. - for line in source.split("\n"): - line = line.strip() - if line and line[0] != '#': - break # Leave it alone. - else: - if symbol != "eval": - source = "pass" # Replace it with a 'pass' statement - - # Disable compiler warnings when checking for incomplete input. - with warnings.catch_warnings(): - warnings.simplefilter("ignore", (SyntaxWarning, DeprecationWarning)) - try: - compiler(source, filename, symbol) - except SyntaxError: # Let other compile() errors propagate. - try: - compiler(source + "\n", filename, symbol) - return None - except _IncompleteInputError as e: - return None - except SyntaxError as e: - pass - # fallthrough - - return compiler(source, filename, symbol, incomplete_input=False) - -def _compile(source, filename, symbol, incomplete_input=True): - flags = 0 - if incomplete_input: - flags |= PyCF_ALLOW_INCOMPLETE_INPUT - flags |= PyCF_DONT_IMPLY_DEDENT - return compile(source, filename, symbol, flags) - -def compile_command(source, filename="", symbol="single"): - r"""Compile a command and determine whether it is incomplete. - - Arguments: - - source -- the source string; may contain \n characters - filename -- optional filename from which source was read; default - "" - symbol -- optional grammar start symbol; "single" (default), "exec" - or "eval" - - Return value / exceptions raised: - - - Return a code object if the command is complete and valid - - Return None if the command is incomplete - - Raise SyntaxError, ValueError or OverflowError if the command is a - syntax error (OverflowError and ValueError can be produced by - malformed literals). - """ - return _maybe_compile(_compile, source, filename, symbol) - -class Compile: - """Instances of this class behave much like the built-in compile - function, but if one is used to compile text containing a future - statement, it "remembers" and compiles all subsequent program texts - with the statement in force.""" - def __init__(self): - self.flags = PyCF_DONT_IMPLY_DEDENT | PyCF_ALLOW_INCOMPLETE_INPUT - - def __call__(self, source, filename, symbol, **kwargs): - flags = self.flags - if kwargs.get('incomplete_input', True) is False: - flags &= ~PyCF_DONT_IMPLY_DEDENT - flags &= ~PyCF_ALLOW_INCOMPLETE_INPUT - codeob = compile(source, filename, symbol, flags, True) - for feature in _features: - if codeob.co_flags & feature.compiler_flag: - self.flags |= feature.compiler_flag - return codeob - -class CommandCompiler: - """Instances of this class have __call__ methods identical in - signature to compile_command; the difference is that if the - instance compiles program text containing a __future__ statement, - the instance 'remembers' and compiles all subsequent program texts - with the statement in force.""" - - def __init__(self,): - self.compiler = Compile() - - def __call__(self, source, filename="", symbol="single"): - r"""Compile a command and determine whether it is incomplete. - - Arguments: - - source -- the source string; may contain \n characters - filename -- optional filename from which source was read; - default "" - symbol -- optional grammar start symbol; "single" (default) or - "eval" - - Return value / exceptions raised: - - - Return a code object if the command is complete and valid - - Return None if the command is incomplete - - Raise SyntaxError, ValueError or OverflowError if the command is a - syntax error (OverflowError and ValueError can be produced by - malformed literals). - """ - return _maybe_compile(self.compiler, source, filename, symbol) +r"""Utilities to compile possibly incomplete Python source code. + +This module provides two interfaces, broadly similar to the builtin +function compile(), which take program text, a filename and a 'mode' +and: + +- Return code object if the command is complete and valid +- Return None if the command is incomplete +- Raise SyntaxError, ValueError or OverflowError if the command is a + syntax error (OverflowError and ValueError can be produced by + malformed literals). + +The two interfaces are: + +compile_command(source, filename, symbol): + + Compiles a single command in the manner described above. + +CommandCompiler(): + + Instances of this class have __call__ methods identical in + signature to compile_command; the difference is that if the + instance compiles program text containing a __future__ statement, + the instance 'remembers' and compiles all subsequent program texts + with the statement in force. + +The module also provides another class: + +Compile(): + + Instances of this class act like the built-in function compile, + but with 'memory' in the sense described above. +""" + +import __future__ +import warnings + +_features = [getattr(__future__, fname) + for fname in __future__.all_feature_names] + +__all__ = ["compile_command", "Compile", "CommandCompiler"] + +# The following flags match the values from Include/cpython/compile.h +# Caveat emptor: These flags are undocumented on purpose and depending +# on their effect outside the standard library is **unsupported**. +PyCF_DONT_IMPLY_DEDENT = 0x200 +PyCF_ONLY_AST = 0x400 +PyCF_ALLOW_INCOMPLETE_INPUT = 0x4000 + +def _maybe_compile(compiler, source, filename, symbol): + # Check for source consisting of only blank lines and comments. + for line in source.split("\n"): + line = line.strip() + if line and line[0] != '#': + break # Leave it alone. + else: + if symbol != "eval": + source = "pass" # Replace it with a 'pass' statement + + # Disable compiler warnings when checking for incomplete input. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", (SyntaxWarning, DeprecationWarning)) + try: + compiler(source, filename, symbol) + except SyntaxError: # Let other compile() errors propagate. + try: + compiler(source + "\n", filename, symbol) + return None + except _IncompleteInputError as e: + return None + except SyntaxError as e: + pass + # fallthrough + + return compiler(source, filename, symbol, incomplete_input=False) + +def _compile(source, filename, symbol, incomplete_input=True): + flags = 0 + if incomplete_input: + flags |= PyCF_ALLOW_INCOMPLETE_INPUT + flags |= PyCF_DONT_IMPLY_DEDENT + return compile(source, filename, symbol, flags) + +def compile_command(source, filename="", symbol="single"): + r"""Compile a command and determine whether it is incomplete. + + Arguments: + + source -- the source string; may contain \n characters + filename -- optional filename from which source was read; default + "" + symbol -- optional grammar start symbol; "single" (default), "exec" + or "eval" + + Return value / exceptions raised: + + - Return a code object if the command is complete and valid + - Return None if the command is incomplete + - Raise SyntaxError, ValueError or OverflowError if the command is a + syntax error (OverflowError and ValueError can be produced by + malformed literals). + """ + return _maybe_compile(_compile, source, filename, symbol) + +class Compile: + """Instances of this class behave much like the built-in compile + function, but if one is used to compile text containing a future + statement, it "remembers" and compiles all subsequent program texts + with the statement in force.""" + def __init__(self): + self.flags = PyCF_DONT_IMPLY_DEDENT | PyCF_ALLOW_INCOMPLETE_INPUT + + def __call__(self, source, filename, symbol, flags=0, **kwargs): + flags |= self.flags + if kwargs.get('incomplete_input', True) is False: + flags &= ~PyCF_DONT_IMPLY_DEDENT + flags &= ~PyCF_ALLOW_INCOMPLETE_INPUT + codeob = compile(source, filename, symbol, flags, True) + if flags & PyCF_ONLY_AST: + return compile(source, filename, symbol, flags, True) + for feature in _features: + if codeob.co_flags & feature.compiler_flag: + self.flags |= feature.compiler_flag + return codeob + +class CommandCompiler: + """Instances of this class have __call__ methods identical in + signature to compile_command; the difference is that if the + instance compiles program text containing a __future__ statement, + the instance 'remembers' and compiles all subsequent program texts + with the statement in force.""" + + def __init__(self,): + self.compiler = Compile() + + def __call__(self, source, filename="", symbol="single"): + r"""Compile a command and determine whether it is incomplete. + + Arguments: + + source -- the source string; may contain \n characters + filename -- optional filename from which source was read; + default "" + symbol -- optional grammar start symbol; "single" (default) or + "eval" + + Return value / exceptions raised: + + - Return a code object if the command is complete and valid + - Return None if the command is incomplete + - Raise SyntaxError, ValueError or OverflowError if the command is a + syntax error (OverflowError and ValueError can be produced by + malformed literals). + """ + return _maybe_compile(self.compiler, source, filename, symbol) From cf96285939893db331a2fb15debbfdc7be801d89 Mon Sep 17 00:00:00 2001 From: Wulian Date: Sun, 6 Oct 2024 14:18:56 +0800 Subject: [PATCH 6/9] LF --- Lib/_pyrepl/console.py | 412 ++++++++++++++++++++--------------------- Lib/codeop.py | 310 +++++++++++++++---------------- 2 files changed, 361 insertions(+), 361 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 69a76ac24664f0..47e683c095a471 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -1,206 +1,206 @@ -# Copyright 2000-2004 Michael Hudson-Doyle -# -# All Rights Reserved -# -# -# Permission to use, copy, modify, and distribute this software and -# its documentation for any purpose is hereby granted without fee, -# provided that the above copyright notice appear in all copies and -# that both that copyright notice and this permission notice appear in -# supporting documentation. -# -# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO -# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, -# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER -# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF -# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -from __future__ import annotations - -import _colorize # type: ignore[import-not-found] - -from abc import ABC, abstractmethod -import ast -import code -from dataclasses import dataclass, field -import os.path -import sys - - -TYPE_CHECKING = False - -if TYPE_CHECKING: - from typing import IO - from typing import Callable - - -@dataclass -class Event: - evt: str - data: str - raw: bytes = b"" - - -@dataclass -class Console(ABC): - screen: list[str] = field(default_factory=list) - height: int = 25 - width: int = 80 - - def __init__( - self, - f_in: IO[bytes] | int = 0, - f_out: IO[bytes] | int = 1, - term: str = "", - encoding: str = "", - ): - self.encoding = encoding or sys.getdefaultencoding() - - if isinstance(f_in, int): - self.input_fd = f_in - else: - self.input_fd = f_in.fileno() - - if isinstance(f_out, int): - self.output_fd = f_out - else: - self.output_fd = f_out.fileno() - - @abstractmethod - def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ... - - @abstractmethod - def prepare(self) -> None: ... - - @abstractmethod - def restore(self) -> None: ... - - @abstractmethod - def move_cursor(self, x: int, y: int) -> None: ... - - @abstractmethod - def set_cursor_vis(self, visible: bool) -> None: ... - - @abstractmethod - def getheightwidth(self) -> tuple[int, int]: - """Return (height, width) where height and width are the height - and width of the terminal window in characters.""" - ... - - @abstractmethod - def get_event(self, block: bool = True) -> Event | None: - """Return an Event instance. Returns None if |block| is false - and there is no event pending, otherwise waits for the - completion of an event.""" - ... - - @abstractmethod - def push_char(self, char: int | bytes) -> None: - """ - Push a character to the console event queue. - """ - ... - - @abstractmethod - def beep(self) -> None: ... - - @abstractmethod - def clear(self) -> None: - """Wipe the screen""" - ... - - @abstractmethod - def finish(self) -> None: - """Move the cursor to the end of the display and otherwise get - ready for end. XXX could be merged with restore? Hmm.""" - ... - - @abstractmethod - def flushoutput(self) -> None: - """Flush all output to the screen (assuming there's some - buffering going on somewhere).""" - ... - - @abstractmethod - def forgetinput(self) -> None: - """Forget all pending, but not yet processed input.""" - ... - - @abstractmethod - def getpending(self) -> Event: - """Return the characters that have been typed but not yet - processed.""" - ... - - @abstractmethod - def wait(self, timeout: float | None) -> bool: - """Wait for an event. The return value is True if an event is - available, False if the timeout has been reached. If timeout is - None, wait forever. The timeout is in milliseconds.""" - ... - - @property - def input_hook(self) -> Callable[[], int] | None: - """Returns the current input hook.""" - ... - - @abstractmethod - def repaint(self) -> None: ... - - -class InteractiveColoredConsole(code.InteractiveConsole): - def __init__( - self, - locals: dict[str, object] | None = None, - filename: str = "", - *, - local_exit: bool = False, - ) -> None: - super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg] - self.can_colorize = _colorize.can_colorize() - - def showsyntaxerror(self, filename=None, **kwargs): - super().showsyntaxerror(filename=filename, **kwargs) - - def _excepthook(self, typ, value, tb): - import traceback - lines = traceback.format_exception( - typ, value, tb, - colorize=self.can_colorize, - limit=traceback.BUILTIN_EXCEPTION_LIMIT) - self.write(''.join(lines)) - - def runsource(self, source, filename="", symbol="single"): - try: - tree = self.compile.compiler(source, filename, symbol, ast.PyCF_ONLY_AST) - except (SyntaxError, OverflowError, ValueError): - self.showsyntaxerror(filename, source=source) - return False - if tree.body: - *_, last_stmt = tree.body - for stmt in tree.body: - wrapper = ast.Interactive if stmt is last_stmt else ast.Module - the_symbol = symbol if stmt is last_stmt else "exec" - item = wrapper([stmt]) - try: - code = self.compile.compiler(item, filename, the_symbol, dont_inherit=True) - except SyntaxError as e: - if e.args[0] == "'await' outside function": - python = os.path.basename(sys.executable) - e.add_note( - f"Try the asyncio REPL ({python} -m asyncio) to use" - f" top-level 'await' and run background asyncio tasks." - ) - self.showsyntaxerror(filename, source=source) - return False - except (OverflowError, ValueError): - self.showsyntaxerror(filename, source=source) - return False - - if code is None: - return True - - self.runcode(code) - return False +# Copyright 2000-2004 Michael Hudson-Doyle +# +# All Rights Reserved +# +# +# Permission to use, copy, modify, and distribute this software and +# its documentation for any purpose is hereby granted without fee, +# provided that the above copyright notice appear in all copies and +# that both that copyright notice and this permission notice appear in +# supporting documentation. +# +# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO +# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, +# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from __future__ import annotations + +import _colorize # type: ignore[import-not-found] + +from abc import ABC, abstractmethod +import ast +import code +from dataclasses import dataclass, field +import os.path +import sys + + +TYPE_CHECKING = False + +if TYPE_CHECKING: + from typing import IO + from typing import Callable + + +@dataclass +class Event: + evt: str + data: str + raw: bytes = b"" + + +@dataclass +class Console(ABC): + screen: list[str] = field(default_factory=list) + height: int = 25 + width: int = 80 + + def __init__( + self, + f_in: IO[bytes] | int = 0, + f_out: IO[bytes] | int = 1, + term: str = "", + encoding: str = "", + ): + self.encoding = encoding or sys.getdefaultencoding() + + if isinstance(f_in, int): + self.input_fd = f_in + else: + self.input_fd = f_in.fileno() + + if isinstance(f_out, int): + self.output_fd = f_out + else: + self.output_fd = f_out.fileno() + + @abstractmethod + def refresh(self, screen: list[str], xy: tuple[int, int]) -> None: ... + + @abstractmethod + def prepare(self) -> None: ... + + @abstractmethod + def restore(self) -> None: ... + + @abstractmethod + def move_cursor(self, x: int, y: int) -> None: ... + + @abstractmethod + def set_cursor_vis(self, visible: bool) -> None: ... + + @abstractmethod + def getheightwidth(self) -> tuple[int, int]: + """Return (height, width) where height and width are the height + and width of the terminal window in characters.""" + ... + + @abstractmethod + def get_event(self, block: bool = True) -> Event | None: + """Return an Event instance. Returns None if |block| is false + and there is no event pending, otherwise waits for the + completion of an event.""" + ... + + @abstractmethod + def push_char(self, char: int | bytes) -> None: + """ + Push a character to the console event queue. + """ + ... + + @abstractmethod + def beep(self) -> None: ... + + @abstractmethod + def clear(self) -> None: + """Wipe the screen""" + ... + + @abstractmethod + def finish(self) -> None: + """Move the cursor to the end of the display and otherwise get + ready for end. XXX could be merged with restore? Hmm.""" + ... + + @abstractmethod + def flushoutput(self) -> None: + """Flush all output to the screen (assuming there's some + buffering going on somewhere).""" + ... + + @abstractmethod + def forgetinput(self) -> None: + """Forget all pending, but not yet processed input.""" + ... + + @abstractmethod + def getpending(self) -> Event: + """Return the characters that have been typed but not yet + processed.""" + ... + + @abstractmethod + def wait(self, timeout: float | None) -> bool: + """Wait for an event. The return value is True if an event is + available, False if the timeout has been reached. If timeout is + None, wait forever. The timeout is in milliseconds.""" + ... + + @property + def input_hook(self) -> Callable[[], int] | None: + """Returns the current input hook.""" + ... + + @abstractmethod + def repaint(self) -> None: ... + + +class InteractiveColoredConsole(code.InteractiveConsole): + def __init__( + self, + locals: dict[str, object] | None = None, + filename: str = "", + *, + local_exit: bool = False, + ) -> None: + super().__init__(locals=locals, filename=filename, local_exit=local_exit) # type: ignore[call-arg] + self.can_colorize = _colorize.can_colorize() + + def showsyntaxerror(self, filename=None, **kwargs): + super().showsyntaxerror(filename=filename, **kwargs) + + def _excepthook(self, typ, value, tb): + import traceback + lines = traceback.format_exception( + typ, value, tb, + colorize=self.can_colorize, + limit=traceback.BUILTIN_EXCEPTION_LIMIT) + self.write(''.join(lines)) + + def runsource(self, source, filename="", symbol="single"): + try: + tree = self.compile.compiler(source, filename, symbol, ast.PyCF_ONLY_AST) + except (SyntaxError, OverflowError, ValueError): + self.showsyntaxerror(filename, source=source) + return False + if tree.body: + *_, last_stmt = tree.body + for stmt in tree.body: + wrapper = ast.Interactive if stmt is last_stmt else ast.Module + the_symbol = symbol if stmt is last_stmt else "exec" + item = wrapper([stmt]) + try: + code = self.compile.compiler(item, filename, the_symbol, dont_inherit=True) + except SyntaxError as e: + if e.args[0] == "'await' outside function": + python = os.path.basename(sys.executable) + e.add_note( + f"Try the asyncio REPL ({python} -m asyncio) to use" + f" top-level 'await' and run background asyncio tasks." + ) + self.showsyntaxerror(filename, source=source) + return False + except (OverflowError, ValueError): + self.showsyntaxerror(filename, source=source) + return False + + if code is None: + return True + + self.runcode(code) + return False diff --git a/Lib/codeop.py b/Lib/codeop.py index acf757d89e6879..f4b480246ca8bb 100644 --- a/Lib/codeop.py +++ b/Lib/codeop.py @@ -1,155 +1,155 @@ -r"""Utilities to compile possibly incomplete Python source code. - -This module provides two interfaces, broadly similar to the builtin -function compile(), which take program text, a filename and a 'mode' -and: - -- Return code object if the command is complete and valid -- Return None if the command is incomplete -- Raise SyntaxError, ValueError or OverflowError if the command is a - syntax error (OverflowError and ValueError can be produced by - malformed literals). - -The two interfaces are: - -compile_command(source, filename, symbol): - - Compiles a single command in the manner described above. - -CommandCompiler(): - - Instances of this class have __call__ methods identical in - signature to compile_command; the difference is that if the - instance compiles program text containing a __future__ statement, - the instance 'remembers' and compiles all subsequent program texts - with the statement in force. - -The module also provides another class: - -Compile(): - - Instances of this class act like the built-in function compile, - but with 'memory' in the sense described above. -""" - -import __future__ -import warnings - -_features = [getattr(__future__, fname) - for fname in __future__.all_feature_names] - -__all__ = ["compile_command", "Compile", "CommandCompiler"] - -# The following flags match the values from Include/cpython/compile.h -# Caveat emptor: These flags are undocumented on purpose and depending -# on their effect outside the standard library is **unsupported**. -PyCF_DONT_IMPLY_DEDENT = 0x200 -PyCF_ONLY_AST = 0x400 -PyCF_ALLOW_INCOMPLETE_INPUT = 0x4000 - -def _maybe_compile(compiler, source, filename, symbol): - # Check for source consisting of only blank lines and comments. - for line in source.split("\n"): - line = line.strip() - if line and line[0] != '#': - break # Leave it alone. - else: - if symbol != "eval": - source = "pass" # Replace it with a 'pass' statement - - # Disable compiler warnings when checking for incomplete input. - with warnings.catch_warnings(): - warnings.simplefilter("ignore", (SyntaxWarning, DeprecationWarning)) - try: - compiler(source, filename, symbol) - except SyntaxError: # Let other compile() errors propagate. - try: - compiler(source + "\n", filename, symbol) - return None - except _IncompleteInputError as e: - return None - except SyntaxError as e: - pass - # fallthrough - - return compiler(source, filename, symbol, incomplete_input=False) - -def _compile(source, filename, symbol, incomplete_input=True): - flags = 0 - if incomplete_input: - flags |= PyCF_ALLOW_INCOMPLETE_INPUT - flags |= PyCF_DONT_IMPLY_DEDENT - return compile(source, filename, symbol, flags) - -def compile_command(source, filename="", symbol="single"): - r"""Compile a command and determine whether it is incomplete. - - Arguments: - - source -- the source string; may contain \n characters - filename -- optional filename from which source was read; default - "" - symbol -- optional grammar start symbol; "single" (default), "exec" - or "eval" - - Return value / exceptions raised: - - - Return a code object if the command is complete and valid - - Return None if the command is incomplete - - Raise SyntaxError, ValueError or OverflowError if the command is a - syntax error (OverflowError and ValueError can be produced by - malformed literals). - """ - return _maybe_compile(_compile, source, filename, symbol) - -class Compile: - """Instances of this class behave much like the built-in compile - function, but if one is used to compile text containing a future - statement, it "remembers" and compiles all subsequent program texts - with the statement in force.""" - def __init__(self): - self.flags = PyCF_DONT_IMPLY_DEDENT | PyCF_ALLOW_INCOMPLETE_INPUT - - def __call__(self, source, filename, symbol, flags=0, **kwargs): - flags |= self.flags - if kwargs.get('incomplete_input', True) is False: - flags &= ~PyCF_DONT_IMPLY_DEDENT - flags &= ~PyCF_ALLOW_INCOMPLETE_INPUT - codeob = compile(source, filename, symbol, flags, True) - if flags & PyCF_ONLY_AST: - return compile(source, filename, symbol, flags, True) - for feature in _features: - if codeob.co_flags & feature.compiler_flag: - self.flags |= feature.compiler_flag - return codeob - -class CommandCompiler: - """Instances of this class have __call__ methods identical in - signature to compile_command; the difference is that if the - instance compiles program text containing a __future__ statement, - the instance 'remembers' and compiles all subsequent program texts - with the statement in force.""" - - def __init__(self,): - self.compiler = Compile() - - def __call__(self, source, filename="", symbol="single"): - r"""Compile a command and determine whether it is incomplete. - - Arguments: - - source -- the source string; may contain \n characters - filename -- optional filename from which source was read; - default "" - symbol -- optional grammar start symbol; "single" (default) or - "eval" - - Return value / exceptions raised: - - - Return a code object if the command is complete and valid - - Return None if the command is incomplete - - Raise SyntaxError, ValueError or OverflowError if the command is a - syntax error (OverflowError and ValueError can be produced by - malformed literals). - """ - return _maybe_compile(self.compiler, source, filename, symbol) +r"""Utilities to compile possibly incomplete Python source code. + +This module provides two interfaces, broadly similar to the builtin +function compile(), which take program text, a filename and a 'mode' +and: + +- Return code object if the command is complete and valid +- Return None if the command is incomplete +- Raise SyntaxError, ValueError or OverflowError if the command is a + syntax error (OverflowError and ValueError can be produced by + malformed literals). + +The two interfaces are: + +compile_command(source, filename, symbol): + + Compiles a single command in the manner described above. + +CommandCompiler(): + + Instances of this class have __call__ methods identical in + signature to compile_command; the difference is that if the + instance compiles program text containing a __future__ statement, + the instance 'remembers' and compiles all subsequent program texts + with the statement in force. + +The module also provides another class: + +Compile(): + + Instances of this class act like the built-in function compile, + but with 'memory' in the sense described above. +""" + +import __future__ +import warnings + +_features = [getattr(__future__, fname) + for fname in __future__.all_feature_names] + +__all__ = ["compile_command", "Compile", "CommandCompiler"] + +# The following flags match the values from Include/cpython/compile.h +# Caveat emptor: These flags are undocumented on purpose and depending +# on their effect outside the standard library is **unsupported**. +PyCF_DONT_IMPLY_DEDENT = 0x200 +PyCF_ONLY_AST = 0x400 +PyCF_ALLOW_INCOMPLETE_INPUT = 0x4000 + +def _maybe_compile(compiler, source, filename, symbol): + # Check for source consisting of only blank lines and comments. + for line in source.split("\n"): + line = line.strip() + if line and line[0] != '#': + break # Leave it alone. + else: + if symbol != "eval": + source = "pass" # Replace it with a 'pass' statement + + # Disable compiler warnings when checking for incomplete input. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", (SyntaxWarning, DeprecationWarning)) + try: + compiler(source, filename, symbol) + except SyntaxError: # Let other compile() errors propagate. + try: + compiler(source + "\n", filename, symbol) + return None + except _IncompleteInputError as e: + return None + except SyntaxError as e: + pass + # fallthrough + + return compiler(source, filename, symbol, incomplete_input=False) + +def _compile(source, filename, symbol, incomplete_input=True): + flags = 0 + if incomplete_input: + flags |= PyCF_ALLOW_INCOMPLETE_INPUT + flags |= PyCF_DONT_IMPLY_DEDENT + return compile(source, filename, symbol, flags) + +def compile_command(source, filename="", symbol="single"): + r"""Compile a command and determine whether it is incomplete. + + Arguments: + + source -- the source string; may contain \n characters + filename -- optional filename from which source was read; default + "" + symbol -- optional grammar start symbol; "single" (default), "exec" + or "eval" + + Return value / exceptions raised: + + - Return a code object if the command is complete and valid + - Return None if the command is incomplete + - Raise SyntaxError, ValueError or OverflowError if the command is a + syntax error (OverflowError and ValueError can be produced by + malformed literals). + """ + return _maybe_compile(_compile, source, filename, symbol) + +class Compile: + """Instances of this class behave much like the built-in compile + function, but if one is used to compile text containing a future + statement, it "remembers" and compiles all subsequent program texts + with the statement in force.""" + def __init__(self): + self.flags = PyCF_DONT_IMPLY_DEDENT | PyCF_ALLOW_INCOMPLETE_INPUT + + def __call__(self, source, filename, symbol, flags=0, **kwargs): + flags |= self.flags + if kwargs.get('incomplete_input', True) is False: + flags &= ~PyCF_DONT_IMPLY_DEDENT + flags &= ~PyCF_ALLOW_INCOMPLETE_INPUT + codeob = compile(source, filename, symbol, flags, True) + if flags & PyCF_ONLY_AST: + return compile(source, filename, symbol, flags, True) + for feature in _features: + if codeob.co_flags & feature.compiler_flag: + self.flags |= feature.compiler_flag + return codeob + +class CommandCompiler: + """Instances of this class have __call__ methods identical in + signature to compile_command; the difference is that if the + instance compiles program text containing a __future__ statement, + the instance 'remembers' and compiles all subsequent program texts + with the statement in force.""" + + def __init__(self,): + self.compiler = Compile() + + def __call__(self, source, filename="", symbol="single"): + r"""Compile a command and determine whether it is incomplete. + + Arguments: + + source -- the source string; may contain \n characters + filename -- optional filename from which source was read; + default "" + symbol -- optional grammar start symbol; "single" (default) or + "eval" + + Return value / exceptions raised: + + - Return a code object if the command is complete and valid + - Return None if the command is incomplete + - Raise SyntaxError, ValueError or OverflowError if the command is a + syntax error (OverflowError and ValueError can be produced by + malformed literals). + """ + return _maybe_compile(self.compiler, source, filename, symbol) From 2080b248cb8226d03dd64b0d32a75acbb5c17abb Mon Sep 17 00:00:00 2001 From: Wulian <1055917385@qq.com> Date: Wed, 9 Oct 2024 06:41:29 +0800 Subject: [PATCH 7/9] Update Lib/_pyrepl/console.py Co-authored-by: Nice Zombies --- Lib/_pyrepl/console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 47e683c095a471..acd245e4a453e6 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -174,7 +174,7 @@ def _excepthook(self, typ, value, tb): def runsource(self, source, filename="", symbol="single"): try: - tree = self.compile.compiler(source, filename, symbol, ast.PyCF_ONLY_AST) + tree = self.compile.compiler(source, filename, "exec", ast.PyCF_ONLY_AST, incomplete_input=False) except (SyntaxError, OverflowError, ValueError): self.showsyntaxerror(filename, source=source) return False From 5dde260ae7d3b2845fcd5ca56d007040a7d00f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 9 Oct 2024 20:54:38 +0200 Subject: [PATCH 8/9] Add tests, reformat a little --- Lib/_pyrepl/console.py | 15 +++++++++++++-- Lib/codeop.py | 2 +- Lib/test/test_pyrepl/test_interact.py | 27 ++++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index acd245e4a453e6..e1ebde400058c0 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -174,7 +174,13 @@ def _excepthook(self, typ, value, tb): def runsource(self, source, filename="", symbol="single"): try: - tree = self.compile.compiler(source, filename, "exec", ast.PyCF_ONLY_AST, incomplete_input=False) + tree = self.compile.compiler( + source, + filename, + "exec", + ast.PyCF_ONLY_AST, + incomplete_input=False, + ) except (SyntaxError, OverflowError, ValueError): self.showsyntaxerror(filename, source=source) return False @@ -185,7 +191,12 @@ def runsource(self, source, filename="", symbol="single"): the_symbol = symbol if stmt is last_stmt else "exec" item = wrapper([stmt]) try: - code = self.compile.compiler(item, filename, the_symbol, dont_inherit=True) + code = self.compile.compiler( + item, + filename, + the_symbol, + dont_inherit=True, + ) except SyntaxError as e: if e.args[0] == "'await' outside function": python = os.path.basename(sys.executable) diff --git a/Lib/codeop.py b/Lib/codeop.py index f4b480246ca8bb..adf000ba29f88c 100644 --- a/Lib/codeop.py +++ b/Lib/codeop.py @@ -117,7 +117,7 @@ def __call__(self, source, filename, symbol, flags=0, **kwargs): flags &= ~PyCF_ALLOW_INCOMPLETE_INPUT codeob = compile(source, filename, symbol, flags, True) if flags & PyCF_ONLY_AST: - return compile(source, filename, symbol, flags, True) + return codeob # this is an ast.Module in this case for feature in _features: if codeob.co_flags & feature.compiler_flag: self.flags |= feature.compiler_flag diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index b7adaffbac0e22..0c6df4e5dae869 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -119,13 +119,38 @@ def test_runsource_shows_syntax_error_for_failed_compilation(self): def test_no_active_future(self): console = InteractiveColoredConsole() - source = "x: int = 1; print(__annotate__(1))" + 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': }\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): From 4bc4bdb3cb525f8c102cee938358d529b530b58a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Wed, 9 Oct 2024 21:09:01 +0200 Subject: [PATCH 9/9] `dont_inherit` is only relevant to the built-in `compile()` func --- Lib/_pyrepl/console.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index e1ebde400058c0..03266c4dfc2dd8 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -191,12 +191,7 @@ def runsource(self, source, filename="", symbol="single"): the_symbol = symbol if stmt is last_stmt else "exec" item = wrapper([stmt]) try: - code = self.compile.compiler( - item, - filename, - the_symbol, - dont_inherit=True, - ) + code = self.compile.compiler(item, filename, the_symbol) except SyntaxError as e: if e.args[0] == "'await' outside function": python = os.path.basename(sys.executable)