From 83ca33e02fb54640ddd0ff6b956059225b05586c Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 17 Nov 2021 19:36:13 -0500 Subject: [PATCH 001/262] Yield from import module discovery Avoid interruptions by yielding more frequently. This prevent unresponsiveness while working through portions of directory trees without any Python modules. --- bpython/importcompletion.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 00bf99f3..c5dc28f0 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -155,7 +155,9 @@ def complete(self, cursor_offset: int, line: str) -> Optional[Set[str]]: else: return None - def find_modules(self, path: Path) -> Generator[str, None, None]: + def find_modules( + self, path: Path + ) -> Generator[Union[str, None], None, None]: """Find all modules (and packages) for a given directory.""" if not path.is_dir(): # Perhaps a zip file @@ -219,9 +221,12 @@ def find_modules(self, path: Path) -> Generator[str, None, None]: if (stat.st_dev, stat.st_ino) not in self.paths: self.paths.add((stat.st_dev, stat.st_ino)) for subname in self.find_modules(path_real): - if subname != "__init__": + if subname is None: + yield None # take a break to avoid unresponsiveness + elif subname != "__init__": yield f"{name}.{subname}" yield name + yield None # take a break to avoid unresponsiveness def find_all_modules( self, paths: Iterable[Path] @@ -231,7 +236,8 @@ def find_all_modules( for p in paths: for module in self.find_modules(p): - self.modules.add(module) + if module is not None: + self.modules.add(module) yield def find_coroutine(self) -> Optional[bool]: From f3f9e420a9c61b19dbb7aba6274bb43185f8ddd3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 21 Nov 2021 17:38:37 +0100 Subject: [PATCH 002/262] Add brackets_completion documentation to configuration-options --- doc/sphinx/source/configuration-options.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst index 4d13cbae..1521542e 100644 --- a/doc/sphinx/source/configuration-options.rst +++ b/doc/sphinx/source/configuration-options.rst @@ -22,6 +22,13 @@ characters (default: simple). None disables autocompletion. .. versionadded:: 0.12 +brackets_completion +^^^^^^^^^^^^^^^^^^^ +Whether opening character of the pairs ``()``, ``[]``, ``""``, and ``''`` should be auto-closed +(default: False). + +.. versionadded:: 0.23 + .. _configuration_color_scheme: color_scheme @@ -173,7 +180,7 @@ Soft tab size (default 4, see PEP-8). unicode_box ^^^^^^^^^^^ -Whether to use Unicode characters to draw boxes. +Whether to use Unicode characters to draw boxes (default: True). .. versionadded:: 0.14 From 51ebc86070c7a49abe78ba87a0e8268a09f141a6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 18:18:17 +0100 Subject: [PATCH 003/262] Add a typing compat module to avoid a dependency on typing-extensions for Py >= 3.8 --- bpython/_typing_compat.py | 33 +++++++++++++++++++++++++++ bpython/curtsies.py | 2 +- bpython/curtsiesfrontend/_internal.py | 2 +- bpython/curtsiesfrontend/repl.py | 2 +- bpython/filelock.py | 2 +- bpython/inspection.py | 2 +- bpython/repl.py | 2 +- setup.cfg | 2 +- 8 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 bpython/_typing_compat.py diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py new file mode 100644 index 00000000..31fb6428 --- /dev/null +++ b/bpython/_typing_compat.py @@ -0,0 +1,33 @@ +# The MIT License +# +# Copyright (c) 2021 Sebastian Ramacher +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +try: + # introduced in Python 3.8 + from typing import Literal +except ImportError: + from typing_extensions import Literal # type: ignore + +try: + # introduced in Python 3.8 + from typing import Protocol +except ImportError: + from typing_extensions import Protocol # type: ignore diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 1d41c3b5..7ffe9010 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -33,7 +33,7 @@ Tuple, Union, ) -from typing_extensions import Protocol +from ._typing_compat import Protocol logger = logging.getLogger(__name__) diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index 633174a8..79c5e974 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -23,7 +23,7 @@ import pydoc from types import TracebackType from typing import Optional, Type -from typing_extensions import Literal +from .._typing_compat import Literal from .. import _internal diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 6693c851..892dc528 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -14,7 +14,7 @@ from enum import Enum from types import TracebackType from typing import Dict, Any, List, Optional, Tuple, Union, cast, Type -from typing_extensions import Literal +from .._typing_compat import Literal import blessings import greenlet diff --git a/bpython/filelock.py b/bpython/filelock.py index 6558fc58..429f708b 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -21,7 +21,7 @@ # THE SOFTWARE. from typing import Optional, Type, IO -from typing_extensions import Literal +from ._typing_compat import Literal from types import TracebackType has_fcntl = True diff --git a/bpython/inspection.py b/bpython/inspection.py index e7ab0a15..21193ef3 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -28,7 +28,7 @@ from collections import namedtuple from typing import Any, Optional, Type from types import MemberDescriptorType, TracebackType -from typing_extensions import Literal +from ._typing_compat import Literal from pygments.token import Token from pygments.lexers import Python3Lexer diff --git a/bpython/repl.py b/bpython/repl.py index e1a5429d..a071d8b5 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -38,7 +38,7 @@ from pathlib import Path from types import ModuleType, TracebackType from typing import cast, Tuple, Any, Optional, Type -from typing_extensions import Literal +from ._typing_compat import Literal from pygments.lexers import Python3Lexer from pygments.token import Token diff --git a/setup.cfg b/setup.cfg index 96ef47be..f5c1bc84 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ install_requires = pygments pyxdg requests - typing-extensions + typing-extensions; python_version < "3.8" [options.extras_require] clipboard = pyperclip From edb8391f3c48e6ad76cb972362d1032cdb354d82 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 18:54:36 +0100 Subject: [PATCH 004/262] Remove unused function --- bpython/repl.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index a071d8b5..719baec5 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -126,9 +126,6 @@ def __init__(self, locals=None, encoding=None): super().__init__(locals) self.timer = RuntimeTimer() - def reset_running_time(self): - self.running_time = 0 - def runsource(self, source, filename=None, symbol="single", encode="auto"): """Execute Python code. From 1b3c87eade6d7e75ae37c699bcd99424068cc762 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 18:54:50 +0100 Subject: [PATCH 005/262] Add type annotations for RuntimeTimer --- bpython/repl.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 719baec5..5802d7d7 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -63,11 +63,11 @@ class RuntimeTimer: """Calculate running time""" - def __init__(self): + def __init__(self) -> None: self.reset_timer() self.time = time.monotonic if hasattr(time, "monotonic") else time.time - def __enter__(self): + def __enter__(self) -> None: self.start = self.time() def __exit__( @@ -80,11 +80,11 @@ def __exit__( self.running_time += self.last_command return False - def reset_timer(self): + def reset_timer(self) -> None: self.running_time = 0.0 self.last_command = 0.0 - def estimate(self): + def estimate(self) -> float: return self.running_time - self.last_command From d0cdeb4926317af49fc791ff5a83efc0382fa242 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 20:24:25 +0100 Subject: [PATCH 006/262] Directly use time.monotonic It's always available starting with Python 3.5. --- bpython/repl.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 5802d7d7..cbfc987e 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -65,10 +65,9 @@ class RuntimeTimer: def __init__(self) -> None: self.reset_timer() - self.time = time.monotonic if hasattr(time, "monotonic") else time.time def __enter__(self) -> None: - self.start = self.time() + self.start = time.monotonic() def __exit__( self, @@ -76,7 +75,7 @@ def __exit__( exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: - self.last_command = self.time() - self.start + self.last_command = time.monotonic() - self.start self.running_time += self.last_command return False From a96d6e3483e7743fd8e0453532b93154fe0772f6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 19:20:49 +0100 Subject: [PATCH 007/262] Reset correct members --- bpython/repl.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index cbfc987e..9a60300c 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -234,9 +234,9 @@ def __init__(self): # which word is currently replacing the current word self.index = -1 # cursor position in the original line - self.orig_cursor_offset = None + self.orig_cursor_offset = -1 # original line (before match replacements) - self.orig_line = None + self.orig_line = "" # class describing the current type of completion self.completer = None @@ -327,8 +327,8 @@ def update(self, cursor_offset, current_line, matches, completer): def clear(self): self.matches = [] - self.cursor_offset = -1 - self.current_line = "" + self.orig_cursor_offset = -1 + self.orig_line = "" self.current_word = "" self.start = None self.end = None From 9284640d4064c2fcb207e7fc35a5fc4041ba729e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 19:23:41 +0100 Subject: [PATCH 008/262] Add type annotations to MatchesIterator --- bpython/repl.py | 50 +++++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 9a60300c..689115ff 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -37,7 +37,7 @@ from itertools import takewhile from pathlib import Path from types import ModuleType, TracebackType -from typing import cast, Tuple, Any, Optional, Type +from typing import cast, List, Tuple, Any, Optional, Type from ._typing_compat import Literal from pygments.lexers import Python3Lexer @@ -226,11 +226,11 @@ class MatchesIterator: A MatchesIterator can be `clear`ed to reset match iteration, and `update`ed to set what matches will be iterated over.""" - def __init__(self): + def __init__(self) -> None: # word being replaced in the original line of text self.current_word = "" # possible replacements for current_word - self.matches = None + self.matches: List[str] = [] # which word is currently replacing the current word self.index = -1 # cursor position in the original line @@ -238,63 +238,67 @@ def __init__(self): # original line (before match replacements) self.orig_line = "" # class describing the current type of completion - self.completer = None + self.completer: Optional[autocomplete.BaseCompletionType] = None - def __nonzero__(self): + def __nonzero__(self) -> bool: """MatchesIterator is False when word hasn't been replaced yet""" return self.index != -1 - def __bool__(self): + def __bool__(self) -> bool: return self.index != -1 @property - def candidate_selected(self): + def candidate_selected(self) -> bool: """True when word selected/replaced, False when word hasn't been replaced yet""" return bool(self) - def __iter__(self): + def __iter__(self) -> "MatchesIterator": return self - def current(self): + def current(self) -> str: if self.index == -1: raise ValueError("No current match.") return self.matches[self.index] - def __next__(self): + def __next__(self) -> str: self.index = (self.index + 1) % len(self.matches) return self.matches[self.index] - def previous(self): + def previous(self) -> str: if self.index <= 0: self.index = len(self.matches) self.index -= 1 return self.matches[self.index] - def cur_line(self): + def cur_line(self) -> Tuple[int, str]: """Returns a cursor offset and line with the current substitution made""" return self.substitute(self.current()) - def substitute(self, match): + def substitute(self, match) -> Tuple[int, str]: """Returns a cursor offset and line with match substituted in""" - start, end, word = self.completer.locate( + assert self.completer is not None + + start, end, _ = self.completer.locate( self.orig_cursor_offset, self.orig_line - ) + ) # type: ignore return ( start + len(match), self.orig_line[:start] + match + self.orig_line[end:], ) - def is_cseq(self): + def is_cseq(self) -> bool: return bool( os.path.commonprefix(self.matches)[len(self.current_word) :] ) - def substitute_cseq(self): + def substitute_cseq(self) -> Tuple[int, str]: """Returns a new line by substituting a common sequence in, and update matches""" + assert self.completer is not None + cseq = os.path.commonprefix(self.matches) new_cursor_offset, new_line = self.substitute(cseq) if len(self.matches) == 1: @@ -307,7 +311,13 @@ def substitute_cseq(self): self.clear() return new_cursor_offset, new_line - def update(self, cursor_offset, current_line, matches, completer): + def update( + self, + cursor_offset: int, + current_line: str, + matches: List[str], + completer: autocomplete.BaseCompletionType, + ) -> None: """Called to reset the match index and update the word being replaced Should only be called if there's a target to update - otherwise, call @@ -323,9 +333,9 @@ def update(self, cursor_offset, current_line, matches, completer): self.index = -1 self.start, self.end, self.current_word = self.completer.locate( self.orig_cursor_offset, self.orig_line - ) + ) # type: ignore - def clear(self): + def clear(self) -> None: self.matches = [] self.orig_cursor_offset = -1 self.orig_line = "" From 8bf810d6142f4f3e30a18b7ca8912d181ae52322 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 20:24:05 +0100 Subject: [PATCH 009/262] Remove unused function --- bpython/repl.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 689115ff..8cf88962 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -1178,20 +1178,6 @@ def next_indentation(line, tab_length): return indentation -def next_token_inside_string(code_string, inside_string): - """Given a code string s and an initial state inside_string, return - whether the next token will be inside a string or not.""" - for token, value in Python3Lexer().get_tokens(code_string): - if token is Token.String: - value = value.lstrip("bBrRuU") - if value in ('"""', "'''", '"', "'"): - if not inside_string: - inside_string = value - elif value == inside_string: - inside_string = False - return inside_string - - def split_lines(tokens): for (token, value) in tokens: if not value: From 3c9e35dc6c38e807116b0b1406df0406fd04f5c0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 22:54:12 +0100 Subject: [PATCH 010/262] Add return type for __enter__ --- bpython/inspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 21193ef3..5ebfdbe8 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -58,7 +58,7 @@ class AttrCleaner: def __init__(self, obj: Any) -> None: self.obj = obj - def __enter__(self): + def __enter__(self) -> None: """Try to make an object not exhibit side-effects on attribute lookup.""" type_ = type(self.obj) From fdd4ad960351e5cb7e4b77bd891cecc0d638d199 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 31 Dec 2021 13:59:15 +0100 Subject: [PATCH 011/262] Fix tests with Python 3.10.1 --- bpython/test/test_interpreter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index ca64de77..1a5eed39 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -35,7 +35,7 @@ def test_syntaxerror(self): i.runsource("1.1.1.1") - if sys.version_info[:2] >= (3, 10): + if (3, 10, 0) <= sys.version_info[:3] < (3, 10, 1): expected = ( " File " + green('""') @@ -47,7 +47,7 @@ def test_syntaxerror(self): + cyan("invalid syntax. Perhaps you forgot a comma?") + "\n" ) - elif (3, 8) <= sys.version_info[:2] <= (3, 9): + elif (3, 8) <= sys.version_info[:2]: expected = ( " File " + green('""') From b46afa3bb1ab783c96dc80c5184090a171ab70d4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 31 Dec 2021 14:00:46 +0100 Subject: [PATCH 012/262] Apply black --- bpython/autocomplete.py | 1 - bpython/urwid.py | 1 - 2 files changed, 2 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 7a65d1b3..71e7fe09 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -626,7 +626,6 @@ def matches( def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return None - else: class JediCompletion(BaseCompletionType): diff --git a/bpython/urwid.py b/bpython/urwid.py index e3aab75c..33d1f4b8 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -128,7 +128,6 @@ def wrapper(*args, **kwargs): return wrapper - else: TwistedEventLoop = getattr(urwid, "TwistedEventLoop", None) From 4510162c67e9b54b1258bb4c47bca0797d51ba3f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 31 Dec 2021 14:07:44 +0100 Subject: [PATCH 013/262] Apply black --- bpython/curtsiesfrontend/filewatch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index e3607180..7616d484 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -11,7 +11,6 @@ def ModuleChangedEventHandler(*args): return None - else: class ModuleChangedEventHandler(FileSystemEventHandler): # type: ignore [no-redef] From 4d33cc6ef6114fb173452ade774b8995ffc54783 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 31 Dec 2021 14:08:35 +0100 Subject: [PATCH 014/262] Really fix tests with Python 3.10.1 --- bpython/test/test_interpreter.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 1a5eed39..45ffa66d 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -35,7 +35,19 @@ def test_syntaxerror(self): i.runsource("1.1.1.1") - if (3, 10, 0) <= sys.version_info[:3] < (3, 10, 1): + if (3, 10, 1) <= sys.version_info[:3]: + expected = ( + " File " + + green('""') + + ", line " + + bold(magenta("1")) + + "\n 1.1.1.1\n ^^\n" + + bold(red("SyntaxError")) + + ": " + + cyan("invalid syntax") + + "\n" + ) + elif (3, 10) <= sys.version_info[:2]: expected = ( " File " + green('""') From 60baf5eee9c107df5cce2319227fb129ff64bb77 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 8 Jan 2022 20:27:26 +0100 Subject: [PATCH 015/262] Fix call of __exit__ (fixes #948) --- bpython/curtsiesfrontend/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 892dc528..0d45593f 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -611,7 +611,7 @@ def sigwinch_handler(self, signum, frame): def sigtstp_handler(self, signum, frame): self.scroll_offset = len(self.lines_for_display) - self.__exit__() + self.__exit__(None, None, None) self.on_suspend() os.kill(os.getpid(), signal.SIGTSTP) self.after_suspend() From 9465700bc899eda0c7d71c7baf47ec1c06414511 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 8 Jan 2022 20:33:56 +0100 Subject: [PATCH 016/262] Simplify is_closing_quote --- bpython/curtsiesfrontend/repl.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 0d45593f..758c9745 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -814,16 +814,14 @@ def process_key_event(self, e: str) -> None: else: self.add_normal_character(e) - def is_closing_quote(self, e): + def is_closing_quote(self, e: str) -> bool: char_count = self._current_line.count(e) - if ( + return ( char_count % 2 == 0 and cursor_on_closing_char_pair( self._cursor_offset, self._current_line, e )[0] - ): - return True - return False + ) def insert_char_pair_start(self, e): """Accepts character which is a part of CHARACTER_PAIR_MAP From 30f8dbaa8d38f7b1f3652c21e1cbc6b1ae969f82 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 8 Jan 2022 20:43:03 +0100 Subject: [PATCH 017/262] Check for None --- bpython/line.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index cbfa682d..3572e45d 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -300,7 +300,7 @@ def cursor_on_closing_char_pair(cursor_offset, line, ch=None): if cursor_offset < len(line): cur_char = line[cursor_offset] if cur_char in CHARACTER_PAIR_MAP.values(): - on_closing_char = True if not ch else cur_char == ch + on_closing_char = True if ch is None else cur_char == ch if cursor_offset > 0: prev_char = line[cursor_offset - 1] if ( @@ -308,5 +308,5 @@ def cursor_on_closing_char_pair(cursor_offset, line, ch=None): and prev_char in CHARACTER_PAIR_MAP and CHARACTER_PAIR_MAP[prev_char] == cur_char ): - pair_close = True if not ch else prev_char == ch + pair_close = True if ch is None else prev_char == ch return on_closing_char, pair_close From 562ad3f68a25ed805e81755ef26fcb6507614520 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 8 Jan 2022 20:44:01 +0100 Subject: [PATCH 018/262] Simplify insert_char_pair_start --- bpython/curtsiesfrontend/repl.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 758c9745..97365421 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -834,7 +834,6 @@ def insert_char_pair_start(self, e): """ self.add_normal_character(e) if self.config.brackets_completion: - allowed_chars = ["}", ")", "]", " "] start_of_line = len(self._current_line) == 1 end_of_line = len(self._current_line) == self._cursor_offset can_lookup_next = len(self._current_line) > self._cursor_offset @@ -843,10 +842,8 @@ def insert_char_pair_start(self, e): if not can_lookup_next else self._current_line[self._cursor_offset] ) - next_char_allowed = next_char in allowed_chars - if start_of_line or end_of_line or next_char_allowed: - closing_char = CHARACTER_PAIR_MAP[e] - self.add_normal_character(closing_char, narrow_search=False) + if start_of_line or end_of_line or next_char in "})] ": + self.add_normal_character(CHARACTER_PAIR_MAP[e], narrow_search=False) self._cursor_offset -= 1 def insert_char_pair_end(self, e): From 94bda61004598cea8b988ecc4067c494f8901c69 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 9 Jan 2022 00:19:29 +0100 Subject: [PATCH 019/262] Apply black --- bpython/curtsiesfrontend/repl.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 97365421..01890452 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -816,7 +816,7 @@ def process_key_event(self, e: str) -> None: def is_closing_quote(self, e: str) -> bool: char_count = self._current_line.count(e) - return ( + return ( char_count % 2 == 0 and cursor_on_closing_char_pair( self._cursor_offset, self._current_line, e @@ -843,7 +843,9 @@ def insert_char_pair_start(self, e): else self._current_line[self._cursor_offset] ) if start_of_line or end_of_line or next_char in "})] ": - self.add_normal_character(CHARACTER_PAIR_MAP[e], narrow_search=False) + self.add_normal_character( + CHARACTER_PAIR_MAP[e], narrow_search=False + ) self._cursor_offset -= 1 def insert_char_pair_end(self, e): From 189da3ecbaa30212b8ba73aeb321b6a6a324348b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 9 Jan 2022 00:22:52 +0100 Subject: [PATCH 020/262] Add type annotations --- bpython/line.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index 3572e45d..ee964a08 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -7,7 +7,7 @@ import re from itertools import chain -from typing import Optional, NamedTuple +from typing import Optional, NamedTuple, Tuple from .lazyre import LazyReCompile @@ -290,7 +290,9 @@ def current_expression_attribute( return None -def cursor_on_closing_char_pair(cursor_offset, line, ch=None): +def cursor_on_closing_char_pair( + cursor_offset: int, line: str, ch: Optional[str] = None +) -> Tuple[bool, bool]: """Checks if cursor sits on closing character of a pair and whether its pair character is directly behind it """ From 95539ccdf8ccc1b614a62a757044c08a308a5375 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 9 Jan 2022 16:11:05 +0100 Subject: [PATCH 021/262] Avoid indexing and tuple unpacking of LinePart instances --- bpython/autocomplete.py | 2 +- bpython/importcompletion.py | 12 +++++----- bpython/line.py | 23 ++++++++---------- bpython/repl.py | 21 +++++++++------- bpython/test/test_autocomplete.py | 5 +++- bpython/test/test_line_properties.py | 36 ++++++++++++++++++---------- bpython/test/test_repl.py | 12 ++++++---- 7 files changed, 64 insertions(+), 47 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 71e7fe09..bb8a8b0c 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -481,7 +481,7 @@ def matches( if current_dict_parts is None: return None - _, _, dexpr = current_dict_parts + dexpr = current_dict_parts.word try: obj = safe_eval(dexpr, locals_) except EvaluationError: diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index c5dc28f0..96936144 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -135,22 +135,22 @@ def complete(self, cursor_offset: int, line: str) -> Optional[Set[str]]: if import_import is not None: # `from a import ` completion matches = self.module_matches( - import_import[2], from_import_from[2] + import_import.word, from_import_from.word ) matches.update( - self.attr_matches(import_import[2], from_import_from[2]) + self.attr_matches(import_import.word, from_import_from.word) ) else: # `from ` completion - matches = self.module_attr_matches(from_import_from[2]) - matches.update(self.module_matches(from_import_from[2])) + matches = self.module_attr_matches(from_import_from.word) + matches.update(self.module_matches(from_import_from.word)) return matches cur_import = current_import(cursor_offset, line) if cur_import is not None: # `import ` completion - matches = self.module_matches(cur_import[2]) - matches.update(self.module_attr_matches(cur_import[2])) + matches = self.module_matches(cur_import.word) + matches.update(self.module_attr_matches(cur_import.word)) return matches else: return None diff --git a/bpython/line.py b/bpython/line.py index ee964a08..99615efe 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -130,15 +130,14 @@ def current_object(cursor_offset: int, line: str) -> Optional[LinePart]: match = current_word(cursor_offset, line) if match is None: return None - start, end, word = match s = ".".join( m.group(1) - for m in _current_object_re.finditer(word) - if m.end(1) + start < cursor_offset + for m in _current_object_re.finditer(match.word) + if m.end(1) + match.start < cursor_offset ) if not s: return None - return LinePart(start, start + len(s), s) + return LinePart(match.start, match.start + len(s), s) _current_object_attribute_re = LazyReCompile(r"([\w_][\w0-9_]*)[.]?") @@ -152,12 +151,13 @@ def current_object_attribute( match = current_word(cursor_offset, line) if match is None: return None - start, end, word = match - matches = _current_object_attribute_re.finditer(word) + matches = _current_object_attribute_re.finditer(match.word) next(matches) for m in matches: - if m.start(1) + start <= cursor_offset <= m.end(1) + start: - return LinePart(m.start(1) + start, m.end(1) + start, m.group(1)) + if m.start(1) + match.start <= cursor_offset <= m.end(1) + match.start: + return LinePart( + m.start(1) + match.start, m.end(1) + match.start, m.group(1) + ) return None @@ -266,11 +266,8 @@ def current_dotted_attribute( ) -> Optional[LinePart]: """The dotted attribute-object pair before the cursor""" match = current_word(cursor_offset, line) - if match is None: - return None - start, end, word = match - if "." in word[1:]: - return LinePart(start, end, word) + if match is not None and "." in match.word[1:]: + return match return None diff --git a/bpython/repl.py b/bpython/repl.py index 8cf88962..4ee4eeba 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -239,6 +239,8 @@ def __init__(self) -> None: self.orig_line = "" # class describing the current type of completion self.completer: Optional[autocomplete.BaseCompletionType] = None + self.start: Optional[int] = None + self.end: Optional[int] = None def __nonzero__(self) -> bool: """MatchesIterator is False when word hasn't been replaced yet""" @@ -277,16 +279,15 @@ def cur_line(self) -> Tuple[int, str]: made""" return self.substitute(self.current()) - def substitute(self, match) -> Tuple[int, str]: + def substitute(self, match: str) -> Tuple[int, str]: """Returns a cursor offset and line with match substituted in""" assert self.completer is not None - start, end, _ = self.completer.locate( - self.orig_cursor_offset, self.orig_line - ) # type: ignore + lp = self.completer.locate(self.orig_cursor_offset, self.orig_line) + assert lp is not None return ( - start + len(match), - self.orig_line[:start] + match + self.orig_line[end:], + lp.start + len(match), + self.orig_line[: lp.start] + match + self.orig_line[lp.stop :], ) def is_cseq(self) -> bool: @@ -331,9 +332,11 @@ def update( self.matches = matches self.completer = completer self.index = -1 - self.start, self.end, self.current_word = self.completer.locate( - self.orig_cursor_offset, self.orig_line - ) # type: ignore + lp = self.completer.locate(self.orig_cursor_offset, self.orig_line) + assert lp is not None + self.start = lp.start + self.end = lp.stop + self.current_word = lp.word def clear(self) -> None: self.matches = [] diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index ad991abe..8b171d03 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -12,6 +12,7 @@ has_jedi = False from bpython import autocomplete +from bpython.line import LinePart glob_function = "glob.iglob" @@ -114,7 +115,9 @@ def test_locate_fails_when_not_in_string(self): self.assertEqual(self.completer.locate(4, "abcd"), None) def test_locate_succeeds_when_in_string(self): - self.assertEqual(self.completer.locate(4, "a'bc'd"), (2, 4, "bc")) + self.assertEqual( + self.completer.locate(4, "a'bc'd"), LinePart(2, 4, "bc") + ) def test_issue_491(self): self.assertNotEqual(self.completer.matches(9, '"a[a.l-1]'), None) diff --git a/bpython/test/test_line_properties.py b/bpython/test/test_line_properties.py index 592a6176..967ecbe0 100644 --- a/bpython/test/test_line_properties.py +++ b/bpython/test/test_line_properties.py @@ -1,7 +1,9 @@ import re +from typing import Optional, Tuple import unittest from bpython.line import ( + LinePart, current_word, current_dict_key, current_dict, @@ -25,7 +27,7 @@ def cursor(s): return cursor_offset, line -def decode(s): +def decode(s: str) -> Tuple[Tuple[int, str], Optional[LinePart]]: """'ad' -> ((3, 'abcd'), (1, 3, 'bdc'))""" if not s.count("|") == 1: @@ -41,16 +43,16 @@ def decode(s): assert len(d) in [1, 3], "need all the parts just once! %r" % d if "<" in d: - return (d["|"], s), (d["<"], d[">"], s[d["<"] : d[">"]]) + return (d["|"], s), LinePart(d["<"], d[">"], s[d["<"] : d[">"]]) else: return (d["|"], s), None -def line_with_cursor(cursor_offset, line): +def line_with_cursor(cursor_offset: int, line: str) -> str: return line[:cursor_offset] + "|" + line[cursor_offset:] -def encode(cursor_offset, line, result): +def encode(cursor_offset: int, line: str, result: Optional[LinePart]) -> str: """encode(3, 'abdcd', (1, 3, 'bdc')) -> ad' Written for prettier assert error messages @@ -58,7 +60,9 @@ def encode(cursor_offset, line, result): encoded_line = line_with_cursor(cursor_offset, line) if result is None: return encoded_line - start, end, value = result + start = result.start + end = result.stop + value = result.word assert line[start:end] == value if start < cursor_offset: encoded_line = encoded_line[:start] + "<" + encoded_line[start:] @@ -107,19 +111,25 @@ def test_I(self): self.assertEqual(cursor("asd|fgh"), (3, "asdfgh")) def test_decode(self): - self.assertEqual(decode("ad"), ((3, "abdcd"), (1, 4, "bdc"))) - self.assertEqual(decode("a|d"), ((1, "abdcd"), (1, 4, "bdc"))) - self.assertEqual(decode("ad|"), ((5, "abdcd"), (1, 4, "bdc"))) + self.assertEqual( + decode("ad"), ((3, "abdcd"), LinePart(1, 4, "bdc")) + ) + self.assertEqual( + decode("a|d"), ((1, "abdcd"), LinePart(1, 4, "bdc")) + ) + self.assertEqual( + decode("ad|"), ((5, "abdcd"), LinePart(1, 4, "bdc")) + ) def test_encode(self): - self.assertEqual(encode(3, "abdcd", (1, 4, "bdc")), "ad") - self.assertEqual(encode(1, "abdcd", (1, 4, "bdc")), "a|d") - self.assertEqual(encode(4, "abdcd", (1, 4, "bdc")), "ad") - self.assertEqual(encode(5, "abdcd", (1, 4, "bdc")), "ad|") + self.assertEqual(encode(3, "abdcd", LinePart(1, 4, "bdc")), "ad") + self.assertEqual(encode(1, "abdcd", LinePart(1, 4, "bdc")), "a|d") + self.assertEqual(encode(4, "abdcd", LinePart(1, 4, "bdc")), "ad") + self.assertEqual(encode(5, "abdcd", LinePart(1, 4, "bdc")), "ad|") def test_assert_access(self): def dumb_func(cursor_offset, line): - return (0, 2, "ab") + return LinePart(0, 2, "ab") self.func = dumb_func self.assertAccess("d") diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index e29c5a4e..a4241087 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -10,8 +10,12 @@ from unittest import mock from bpython import config, repl, cli, autocomplete -from bpython.test import MagicIterMock, FixLanguageTestCase as TestCase -from bpython.test import TEST_CONFIG +from bpython.line import LinePart +from bpython.test import ( + MagicIterMock, + FixLanguageTestCase as TestCase, + TEST_CONFIG, +) pypy = "PyPy" in sys.version @@ -99,7 +103,7 @@ def test_update(self): newmatches = ["string", "str", "set"] completer = mock.Mock() - completer.locate.return_value = (0, 1, "s") + completer.locate.return_value = LinePart(0, 1, "s") self.matches_iterator.update(1, "s", newmatches, completer) newslice = islice(newmatches, 0, 3) @@ -108,7 +112,7 @@ def test_update(self): def test_cur_line(self): completer = mock.Mock() - completer.locate.return_value = ( + completer.locate.return_value = LinePart( 0, self.matches_iterator.orig_cursor_offset, self.matches_iterator.orig_line, From 809003c24bfe0577a936670355e3856388eb77b4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 9 Jan 2022 15:28:33 +0100 Subject: [PATCH 022/262] Replace NamedTuple with dataclass --- bpython/line.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index 99615efe..cbc3bf37 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -6,13 +6,15 @@ import re +from dataclasses import dataclass from itertools import chain -from typing import Optional, NamedTuple, Tuple +from typing import Optional, Tuple from .lazyre import LazyReCompile -class LinePart(NamedTuple): +@dataclass +class LinePart: start: int stop: int word: str From a60ae154152c4199336be70a53df0091a9061832 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 9 Jan 2022 23:22:38 +0100 Subject: [PATCH 023/262] Fix type annotation --- bpython/autocomplete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index bb8a8b0c..916a6229 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -63,7 +63,7 @@ class AutocompleteModes(Enum): FUZZY = "fuzzy" @classmethod - def from_string(cls, value: str) -> Optional[Any]: + def from_string(cls, value: str) -> Optional["AutocompleteModes"]: if value.upper() in cls.__members__: return cls.__members__[value.upper()] return None From 5629bbe2225976defd628b4063fe61ec8595909b Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Mon, 3 Jan 2022 22:11:08 -0600 Subject: [PATCH 024/262] Adding type hints to formatter.py --- bpython/formatter.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/bpython/formatter.py b/bpython/formatter.py index 9618979a..f216f213 100644 --- a/bpython/formatter.py +++ b/bpython/formatter.py @@ -24,9 +24,14 @@ # Pygments really kicks ass, it made it really easy to # get the exact behaviour I wanted, thanks Pygments.:) +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True + +from typing import Any, MutableMapping, Iterable, TextIO from pygments.formatter import Formatter from pygments.token import ( + _TokenType, Keyword, Name, Comment, @@ -96,7 +101,9 @@ class BPythonFormatter(Formatter): See the Pygments source for more info; it's pretty straightforward.""" - def __init__(self, color_scheme, **options): + def __init__( + self, color_scheme: MutableMapping[str, str], **options: Any + ) -> None: self.f_strings = {} for k, v in theme_map.items(): self.f_strings[k] = f"\x01{color_scheme[v]}" @@ -106,14 +113,21 @@ def __init__(self, color_scheme, **options): self.f_strings[k] += "I" super().__init__(**options) - def format(self, tokensource, outfile): - o = "" + def format( + self, + tokensource: Iterable[MutableMapping[_TokenType, str]], + outfile: TextIO, + ) -> None: + o: str = "" for token, text in tokensource: if text == "\n": continue while token not in self.f_strings: - token = token.parent + if token.parent is None: + break + else: + token = token.parent o += f"{self.f_strings[token]}\x03{text}\x04" outfile.write(o.rstrip()) From f8ce9162bfccc20b978536c581192984cf43f87f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 2 Feb 2022 14:41:22 +0100 Subject: [PATCH 025/262] Mark optional arguments as optional --- bpython/curtsiesfrontend/interpreter.py | 8 ++++++-- bpython/curtsiesfrontend/repl.py | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 48ee15ba..91dba96a 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -1,5 +1,5 @@ import sys -from typing import Any, Dict +from typing import Any, Dict, Optional from pygments.token import Generic, Token, Keyword, Name, Comment, String from pygments.token import Error, Literal, Number, Operator, Punctuation @@ -60,7 +60,11 @@ def format(self, tokensource, outfile): class Interp(ReplInterpreter): - def __init__(self, locals: Dict[str, Any] = None, encoding=None): + def __init__( + self, + locals: Optional[Dict[str, Any]] = None, + encoding: Optional[str] = None, + ) -> None: """Constructor. We include an argument for the outfile to pass to the formatter for it diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 01890452..59dcd481 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -312,10 +312,10 @@ class BaseRepl(Repl): def __init__( self, config: Config, - locals_: Dict[str, Any] = None, - banner: str = None, - interp: code.InteractiveInterpreter = None, - orig_tcattrs: List[Any] = None, + locals_: Optional[Dict[str, Any]] = None, + banner: Optional[str] = None, + interp: Optional[code.InteractiveInterpreter] = None, + orig_tcattrs: Optional[List[Any]] = None, ): """ locals_ is a mapping of locals to pass into the interpreter From 6005b72b1065f71290008b6e880c56e7256ce0f2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 2 Feb 2022 14:54:49 +0100 Subject: [PATCH 026/262] Replace a loop with an rfind --- bpython/autocomplete.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 916a6229..81cea894 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -398,13 +398,10 @@ def matches( assert "." in r.word - for i in range(1, len(r.word) + 1): - if r.word[-i] == "[": - i -= 1 - break - methodtext = r.word[-i:] + i = r.word.rfind("[") + 1 + methodtext = r.word[i:] matches = { - "".join([r.word[:-i], m]) + "".join([r.word[:i], m]) for m in self.attr_matches(methodtext, locals_) } From 2dd27f9eacf893042fee0a6789c10241460e3915 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 3 Feb 2022 23:03:03 +0100 Subject: [PATCH 027/262] Add type check instead of using exceptions --- bpython/patch_linecache.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index e1d94a15..f947f7c4 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -53,10 +53,10 @@ def __delitem__(self, key): return super().__delitem__(key) -def _bpython_clear_linecache(): - try: +def _bpython_clear_linecache() -> None: + if isinstance(linecache.cache, BPythonLinecache): bpython_history = linecache.cache.bpython_history - except AttributeError: + else: bpython_history = [] linecache.cache = BPythonLinecache() linecache.cache.bpython_history = bpython_history @@ -68,12 +68,12 @@ def _bpython_clear_linecache(): linecache.clearcache = _bpython_clear_linecache -def filename_for_console_input(code_string): +def filename_for_console_input(code_string: str) -> str: """Remembers a string of source code, and returns a fake filename to use to retrieve it later.""" - try: + if isinstance(linecache.cache, BPythonLinecache): return linecache.cache.remember_bpython_input(code_string) - except AttributeError: + else: # If someone else has patched linecache.cache, better for code to # simply be unavailable to inspect.getsource() than to raise # an exception. From 9984c2391f1621c280dd32361f8ea0f712cee8b6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 3 Feb 2022 23:03:15 +0100 Subject: [PATCH 028/262] Add type annotations for line cache --- bpython/patch_linecache.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index f947f7c4..4a89f23a 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -1,22 +1,23 @@ import linecache +from typing import Any, List, Tuple class BPythonLinecache(dict): """Replaces the cache dict in the standard-library linecache module, to also remember (in an unerasable way) bpython console input.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.bpython_history = [] - def is_bpython_filename(self, fname): + def is_bpython_filename(self, fname: Any) -> bool: try: return fname.startswith(" Tuple[int, None, List[str], str]: """Given a filename provided by remember_bpython_input, returns the associated source string.""" try: @@ -25,21 +26,21 @@ def get_bpython_history(self, key): except (IndexError, ValueError): raise KeyError - def remember_bpython_input(self, source): + def remember_bpython_input(self, source: str) -> str: """Remembers a string of source code, and returns a fake filename to use to retrieve it later.""" - filename = "" % len(self.bpython_history) + filename = f"" self.bpython_history.append( (len(source), None, source.splitlines(True), filename) ) return filename - def __getitem__(self, key): + def __getitem__(self, key: Any) -> Any: if self.is_bpython_filename(key): return self.get_bpython_history(key) return super().__getitem__(key) - def __contains__(self, key): + def __contains__(self, key: Any) -> bool: if self.is_bpython_filename(key): try: self.get_bpython_history(key) @@ -48,9 +49,9 @@ def __contains__(self, key): return False return super().__contains__(key) - def __delitem__(self, key): + def __delitem__(self, key: Any) -> None: if not self.is_bpython_filename(key): - return super().__delitem__(key) + super().__delitem__(key) def _bpython_clear_linecache() -> None: From 92c3d1f6ff64b09ba1a161ea75e8738b4286de8c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 3 Feb 2022 23:20:16 +0100 Subject: [PATCH 029/262] Fix mypy regression --- bpython/patch_linecache.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index 4a89f23a..82b38dd7 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -8,12 +8,12 @@ class BPythonLinecache(dict): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self.bpython_history = [] + self.bpython_history: List[Tuple[int, None, List[str], str]] = [] def is_bpython_filename(self, fname: Any) -> bool: - try: + if isinstance(fname, str): return fname.startswith(" Date: Mon, 25 Apr 2022 22:59:29 +0200 Subject: [PATCH 030/262] Avoid a potential import of fcntl --- bpython/config.py | 10 +++++++--- bpython/curtsiesfrontend/parse.py | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index c6cadf85..29b906dd 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -1,7 +1,7 @@ # The MIT License # # Copyright (c) 2009-2015 the bpython authors. -# Copyright (c) 2015-2020 Sebastian Ramacher +# Copyright (c) 2015-2022 Sebastian Ramacher # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -35,9 +35,13 @@ from xdg import BaseDirectory from .autocomplete import AutocompleteModes -from .curtsiesfrontend.parse import CNAMES default_completion = AutocompleteModes.SIMPLE +# All supported letters for colors for themes +# +# Instead of importing it from .curtsiesfrontend.parse, we define them here to +# avoid a potential import of fcntl on Windows. +COLOR_LETTERS = tuple("krgybmcwd") class UnknownColorCode(Exception): @@ -381,7 +385,7 @@ def load_theme( colors[k] = theme.get("syntax", k) else: colors[k] = theme.get("interface", k) - if colors[k].lower() not in CNAMES: + if colors[k].lower() not in COLOR_LETTERS: raise UnknownColorCode(k, colors[k]) # Check against default theme to see if all values are defined diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 6a42b376..d10a0f5c 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -8,11 +8,12 @@ ) from functools import partial +from ..config import COLOR_LETTERS from ..lazyre import LazyReCompile COLORS = CURTSIES_COLORS + ("default",) -CNAMES = dict(zip("krgybmcwd", COLORS)) +CNAMES = dict(zip(COLOR_LETTERS, COLORS)) # hack for finding the "inverse" INVERSE_COLORS = { CURTSIES_COLORS[idx]: CURTSIES_COLORS[ From 8c4fd2304da4f4858ce87621d80aebb8c0104072 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Apr 2022 23:11:50 +0200 Subject: [PATCH 031/262] Add type annotations --- bpython/curtsiesfrontend/parse.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index d10a0f5c..13a200ec 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -1,4 +1,6 @@ import re +from functools import partial +from typing import Any, Callable, Dict, Tuple from curtsies.formatstring import fmtstr, FmtStr from curtsies.termformatconstants import ( @@ -6,7 +8,6 @@ BG_COLORS, colors as CURTSIES_COLORS, ) -from functools import partial from ..config import COLOR_LETTERS from ..lazyre import LazyReCompile @@ -24,7 +25,9 @@ INVERSE_COLORS["default"] = INVERSE_COLORS[CURTSIES_COLORS[0]] -def func_for_letter(letter_color_code: str, default: str = "k"): +def func_for_letter( + letter_color_code: str, default: str = "k" +) -> Callable[..., FmtStr]: """Returns FmtStr constructor for a bpython-style color code""" if letter_color_code == "d": letter_color_code = default @@ -37,13 +40,13 @@ def func_for_letter(letter_color_code: str, default: str = "k"): ) -def color_for_letter(letter_color_code: str, default: str = "k"): +def color_for_letter(letter_color_code: str, default: str = "k") -> str: if letter_color_code == "d": letter_color_code = default return CNAMES[letter_color_code.lower()] -def parse(s): +def parse(s: str) -> FmtStr: """Returns a FmtStr object from a bpython-formatted colored string""" rest = s stuff = [] @@ -59,7 +62,7 @@ def parse(s): ) -def fs_from_match(d): +def fs_from_match(d: Dict[str, Any]) -> FmtStr: atts = {} if d["fg"]: # this isn't according to spec as I understand it @@ -97,7 +100,7 @@ def fs_from_match(d): ) -def peel_off_string(s): +def peel_off_string(s: str) -> Tuple[Dict[str, Any], str]: m = peel_off_string_re.match(s) assert m, repr(s) d = m.groupdict() From f38ced3cb299b43430e3be21d065cb7c6626d648 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Apr 2022 23:12:01 +0200 Subject: [PATCH 032/262] Simplify loop condition --- bpython/curtsiesfrontend/parse.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 13a200ec..8eb1fddd 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -50,9 +50,7 @@ def parse(s: str) -> FmtStr: """Returns a FmtStr object from a bpython-formatted colored string""" rest = s stuff = [] - while True: - if not rest: - break + while rest: start, rest = peel_off_string(rest) stuff.append(start) return ( From 4c5e18b7c244cad8e21600fae61d6faa3a8ef54b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Apr 2022 23:12:26 +0200 Subject: [PATCH 033/262] Apply black --- bpython/test/test_preprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/test_preprocess.py b/bpython/test/test_preprocess.py index 03e9a3b8..ee3f2085 100644 --- a/bpython/test/test_preprocess.py +++ b/bpython/test/test_preprocess.py @@ -15,7 +15,7 @@ def get_fodder_source(test_name): - pattern = fr"#StartTest-{test_name}\n(.*?)#EndTest" + pattern = rf"#StartTest-{test_name}\n(.*?)#EndTest" orig, xformed = [ re.search(pattern, inspect.getsource(module), re.DOTALL) for module in [original, processed] From ace2d066a1e70a710807740739ab325cbe39212c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Apr 2022 23:19:16 +0200 Subject: [PATCH 034/262] Ensure that color is always initialized --- bpython/curtsiesfrontend/parse.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 8eb1fddd..88a149a6 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -62,6 +62,7 @@ def parse(s: str) -> FmtStr: def fs_from_match(d: Dict[str, Any]) -> FmtStr: atts = {} + color = "default" if d["fg"]: # this isn't according to spec as I understand it if d["fg"].isupper(): From 2784b2f781becb8e36531377b851ed6148936e3f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Apr 2022 23:27:06 +0200 Subject: [PATCH 035/262] Fix type annotations --- bpython/args.py | 4 ++-- bpython/simpleeval.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 1ab61d26..04cb8fc3 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -77,9 +77,9 @@ def copyright_banner() -> str: def parse( args: Optional[List[str]], - extras: Options = None, + extras: Optional[Options] = None, ignore_stdin: bool = False, -) -> Tuple: +) -> Tuple[Config, argparse.Namespace, List[str]]: """Receive an argument list - if None, use sys.argv - parse all args and take appropriate action. Also receive optional extra argument: this should be a tuple of (title, description, callback) diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 3992a70f..193a6989 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -28,7 +28,7 @@ import ast import sys import builtins -from typing import Dict, Any +from typing import Dict, Any, Optional from . import line as line_properties from .inspection import getattr_safe @@ -216,7 +216,7 @@ def find_attribute_with_name(node, name): def evaluate_current_expression( - cursor_offset: int, line: str, namespace: Dict[str, Any] = None + cursor_offset: int, line: str, namespace: Optional[Dict[str, Any]] = None ): """ Return evaluated expression to the right of the dot of current attribute. From ee7e295477357abce1f730c6b05e0787a7864313 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Apr 2022 23:53:11 +0200 Subject: [PATCH 036/262] Fix type --- bpython/test/test_interpreter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 45ffa66d..4c18f8fd 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -130,7 +130,7 @@ def gfunc(): self.assertEqual(plain("").join(a), expected) def test_runsource_bytes_over_128_syntax_error_py3(self): - i = interpreter.Interp(encoding=b"latin-1") + i = interpreter.Interp(encoding="latin-1") i.showsyntaxerror = mock.Mock(return_value=None) i.runsource("a = b'\xfe'") From 10c8b37077d825ce8c4f40b2bd2f8bb3647cd2f3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 26 Apr 2022 23:40:48 +0200 Subject: [PATCH 037/262] Add type annotations --- bpython/paste.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/bpython/paste.py b/bpython/paste.py index 4d118cfc..9daca01a 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2014-2020 Sebastian Ramacher +# Copyright (c) 2014-2022 Sebastian Ramacher # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -21,13 +21,14 @@ # THE SOFTWARE. import errno -import requests import subprocess -import unicodedata - -from locale import getpreferredencoding +from typing import Tuple from urllib.parse import urljoin, urlparse +import requests +import unicodedata + +from .config import getpreferredencoding from .translations import _ @@ -36,11 +37,11 @@ class PasteFailed(Exception): class PastePinnwand: - def __init__(self, url, expiry): + def __init__(self, url: str, expiry: str) -> None: self.url = url self.expiry = expiry - def paste(self, s): + def paste(self, s: str) -> Tuple[str, str]: """Upload to pastebin via json interface.""" url = urljoin(self.url, "/api/v1/paste") @@ -64,10 +65,10 @@ def paste(self, s): class PasteHelper: - def __init__(self, executable): + def __init__(self, executable: str) -> None: self.executable = executable - def paste(self, s): + def paste(self, s: str) -> Tuple[str, None]: """Call out to helper program for pastebin upload.""" try: @@ -77,6 +78,7 @@ def paste(self, s): stdin=subprocess.PIPE, stdout=subprocess.PIPE, ) + assert helper.stdin is not None helper.stdin.write(s.encode(getpreferredencoding())) output = helper.communicate()[0].decode(getpreferredencoding()) paste_url = output.split()[0] @@ -89,8 +91,8 @@ def paste(self, s): if helper.returncode != 0: raise PasteFailed( _( - "Helper program returned non-zero exit " - "status %d." % (helper.returncode,) + "Helper program returned non-zero exit status %d." + % (helper.returncode,) ) ) From 8aef6a7f2474cf82f015d065dfda7d4b3ecf1427 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 26 Apr 2022 23:40:56 +0200 Subject: [PATCH 038/262] Fix exception handling --- bpython/paste.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/paste.py b/bpython/paste.py index 9daca01a..ebea9e28 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -54,7 +54,7 @@ def paste(self, s: str) -> Tuple[str, str]: response = requests.post(url, json=payload, verify=True) response.raise_for_status() except requests.exceptions.RequestException as exc: - raise PasteFailed(exc.message) + raise PasteFailed(str(exc)) data = response.json() From 229961238d6cb783067eae3c7bd07f68a4b49927 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 26 Apr 2022 23:50:08 +0200 Subject: [PATCH 039/262] Cache encoding --- bpython/paste.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bpython/paste.py b/bpython/paste.py index ebea9e28..126f5a43 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -79,8 +79,9 @@ def paste(self, s: str) -> Tuple[str, None]: stdout=subprocess.PIPE, ) assert helper.stdin is not None - helper.stdin.write(s.encode(getpreferredencoding())) - output = helper.communicate()[0].decode(getpreferredencoding()) + encoding = getpreferredencoding() + helper.stdin.write(s.encode(encoding)) + output = helper.communicate()[0].decode(encoding) paste_url = output.split()[0] except OSError as e: if e.errno == errno.ENOENT: From deb64165ad23b99dc1c6f54d63fefed41fb69d48 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 26 Apr 2022 23:50:23 +0200 Subject: [PATCH 040/262] Define a protocol for paster implementations --- bpython/paste.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bpython/paste.py b/bpython/paste.py index 126f5a43..fd140a0e 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -22,7 +22,7 @@ import errno import subprocess -from typing import Tuple +from typing import Optional, Tuple, Protocol from urllib.parse import urljoin, urlparse import requests @@ -36,6 +36,11 @@ class PasteFailed(Exception): pass +class Paster(Protocol): + def paste(self, s: str) -> Tuple[str, Optional[str]]: + ... + + class PastePinnwand: def __init__(self, url: str, expiry: str) -> None: self.url = url From af5e90ab270956d45b9c9399fc2929ab996d22b6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 27 Apr 2022 00:11:34 +0200 Subject: [PATCH 041/262] Fix import of Protocol --- bpython/paste.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/paste.py b/bpython/paste.py index fd140a0e..ceba5938 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -22,7 +22,7 @@ import errno import subprocess -from typing import Optional, Tuple, Protocol +from typing import Optional, Tuple from urllib.parse import urljoin, urlparse import requests @@ -30,6 +30,7 @@ from .config import getpreferredencoding from .translations import _ +from ._typing_compat import Protocol class PasteFailed(Exception): From 14669c08e28dcb0ad1a186b486e50595bad952f5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 3 May 2022 00:19:15 +0200 Subject: [PATCH 042/262] Set up __main__ module (fixes #959, #868) --- bpython/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 04cb8fc3..1212fe3f 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -243,10 +243,10 @@ def exec_code( raise SystemExit(e.errno) old_argv, sys.argv = sys.argv, args sys.path.insert(0, os.path.abspath(os.path.dirname(args[0]))) - spec = importlib.util.spec_from_loader("__console__", loader=None) + spec = importlib.util.spec_from_loader("__main__", loader=None) assert spec mod = importlib.util.module_from_spec(spec) - sys.modules["__console__"] = mod + sys.modules["__main__"] = mod interpreter.locals.update(mod.__dict__) # type: ignore # TODO use a more specific type that has a .locals attribute interpreter.locals["__file__"] = args[0] # type: ignore # TODO use a more specific type that has a .locals attribute interpreter.runsource(source, args[0], "exec") From 3d3a6633394c69404e53aa6dc1d61b6126489b51 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Sun, 6 Feb 2022 17:22:15 -0600 Subject: [PATCH 043/262] Just starting out --- bpython/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bpython/cli.py b/bpython/cli.py index 28cc67c7..03987d07 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -21,6 +21,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True # Modified by Brandon Navra # Notes for Windows From 8a292ed6aafaca98b9532d5f343070956daf89a9 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Tue, 8 Feb 2022 23:08:21 -0600 Subject: [PATCH 044/262] Done through line 120 --- bpython/cli.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 03987d07..7194cde3 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -52,7 +52,7 @@ import struct import sys import time -from typing import Iterator, NoReturn, List +from typing import Iterator, NoReturn, List, MutableMapping, Any import unicodedata from dataclasses import dataclass @@ -66,7 +66,7 @@ from pygments import format from pygments.formatters import TerminalFormatter from pygments.lexers import Python3Lexer -from pygments.token import Token +from pygments.token import Token, _TokenType from .formatter import BPythonFormatter # This for config @@ -100,7 +100,9 @@ class ShowListState: wl: int = 0 -def calculate_screen_lines(tokens, width, cursor=0) -> int: +def calculate_screen_lines( + tokens: MutableMapping[_TokenType, str], width: int, cursor: int = 0 +) -> int: """Given a stream of tokens and a screen width plus an optional initial cursor position, return the amount of needed lines on the screen.""" From 7ae4c794dfd54b803bdf241c963aaa6201d44ab7 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Wed, 9 Feb 2022 20:39:08 -0600 Subject: [PATCH 045/262] Not sure about forward_if_not_current() typing. --- bpython/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 7194cde3..9720e76d 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -52,7 +52,7 @@ import struct import sys import time -from typing import Iterator, NoReturn, List, MutableMapping, Any +from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable import unicodedata from dataclasses import dataclass @@ -118,9 +118,9 @@ def calculate_screen_lines( return lines -def forward_if_not_current(func): +def forward_if_not_current(func: Callable) -> Callable: @functools.wraps(func) - def newfunc(self, *args, **kwargs): + def newfunc(self, *args: Any, **kwargs: Any) -> Any: dest = self.get_dest() if self is dest: return func(self, *args, **kwargs) From 6fecebdcbe084bf88aa884e6987c327e7694989d Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Thu, 17 Feb 2022 08:55:35 -0600 Subject: [PATCH 046/262] Did the forward_if_not_current decorator function. Not 100% sure about the types. --- bpython/cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 9720e76d..2ad20f29 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -52,7 +52,7 @@ import struct import sys import time -from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable +from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable, TypeVar, cast import unicodedata from dataclasses import dataclass @@ -84,6 +84,7 @@ from .pager import page from .args import parse as argsparse +F = TypeVar('F', bound=Callable[..., Any]) # --- module globals --- stdscr = None @@ -118,16 +119,16 @@ def calculate_screen_lines( return lines -def forward_if_not_current(func: Callable) -> Callable: +def forward_if_not_current(func: F) -> F: @functools.wraps(func) - def newfunc(self, *args: Any, **kwargs: Any) -> Any: + def newfunc(self, *args, **kwargs): # type: ignore dest = self.get_dest() if self is dest: return func(self, *args, **kwargs) else: return getattr(self.get_dest(), newfunc.__name__)(*args, **kwargs) - return newfunc + return cast(F, newfunc) class FakeStream: From 9c56f05775cd599279caa340d2a0d66e5ea8daa2 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 18 Feb 2022 20:12:34 -0600 Subject: [PATCH 047/262] Had to punt on get_color() --- bpython/cli.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 2ad20f29..15f93bb8 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -52,7 +52,7 @@ import struct import sys import time -from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable, TypeVar, cast +from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable, TypeVar, cast, IO, Iterable, Optional import unicodedata from dataclasses import dataclass @@ -70,7 +70,7 @@ from .formatter import BPythonFormatter # This for config -from .config import getpreferredencoding +from .config import getpreferredencoding, Config # This for keys from .keys import cli_key_dispatch as key_dispatch @@ -88,7 +88,7 @@ # --- module globals --- stdscr = None -colors = None +colors: Optional[MutableMapping[str, int]] = None DO_RESIZE = False # --- @@ -135,17 +135,17 @@ class FakeStream: """Provide a fake file object which calls functions on the interface provided.""" - def __init__(self, interface, get_dest): + def __init__(self, interface: 'CLIRepl', get_dest: IO[str]) -> None: self.encoding: str = getpreferredencoding() self.interface = interface self.get_dest = get_dest @forward_if_not_current - def write(self, s) -> None: + def write(self, s: str) -> None: self.interface.write(s) @forward_if_not_current - def writelines(self, l) -> None: + def writelines(self, l: Iterable[str]) -> None: for s in l: self.write(s) @@ -160,7 +160,7 @@ def flush(self) -> None: class FakeStdin: """Provide a fake stdin type for things like raw_input() etc.""" - def __init__(self, interface) -> None: + def __init__(self, interface: 'CLIRepl') -> None: """Take the curses Repl on init and assume it provides a get_key method which, fortunately, it does.""" @@ -171,11 +171,11 @@ def __init__(self, interface) -> None: def __iter__(self) -> Iterator: return iter(self.readlines()) - def flush(self): + def flush(self) -> None: """Flush the internal buffer. This is a no-op. Flushing stdin doesn't make any sense anyway.""" - def write(self, value) -> NoReturn: + def write(self, value: str) -> NoReturn: # XXX IPython expects sys.stdin.write to exist, there will no doubt be # others, so here's a hack to keep them happy raise OSError(errno.EBADF, "sys.stdin is read-only") @@ -183,7 +183,7 @@ def write(self, value) -> NoReturn: def isatty(self) -> bool: return True - def readline(self, size=-1): + def readline(self, size: int = -1) -> str: """I can't think of any reason why anything other than readline would be useful in the context of an interactive interpreter so this is the only one I've done anything with. The others are just there in case @@ -228,7 +228,7 @@ def readline(self, size=-1): return buffer - def read(self, size=None): + def read(self, size: Optional[int] = None) -> str: if size == 0: return "" @@ -243,7 +243,7 @@ def read(self, size=None): return "".join(data) - def readlines(self, size=-1): + def readlines(self, size: int = -1) -> List[str]: return list(iter(self.readline, "")) @@ -260,17 +260,20 @@ def readlines(self, size=-1): # the addstr stuff to a higher level. # - -def get_color(config, name): +# Have to ignore the return type on this one because the colors variable +# is Optional[MutableMapping[str, int]] but for the purposes of this +# function it can't be None +def get_color(config: Config, name: str) -> int: # type: ignore[return] global colors - return colors[config.color_scheme[name].lower()] + if colors: + return colors[config.color_scheme[name].lower()] -def get_colpair(config, name): +def get_colpair(config: Config, name: str) -> int: return curses.color_pair(get_color(config, name) + 1) -def make_colors(config): +def make_colors(config: Config) -> MutableMapping[str, int]: """Init all the colours in curses and bang them into a dictionary""" # blacK, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default: From 3c3a81d0056f272aeeb7c3ec16deb285f1265947 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 18 Feb 2022 23:58:16 -0600 Subject: [PATCH 048/262] Still working my way down. --- bpython/cli.py | 63 +++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 15f93bb8..812d4b60 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -315,10 +315,10 @@ def make_colors(config: Config) -> MutableMapping[str, int]: class CLIInteraction(repl.Interaction): - def __init__(self, config, statusbar=None): + def __init__(self, config: Config, statusbar: 'Statusbar' = None): super().__init__(config, statusbar) - def confirm(self, q): + def confirm(self, q: str) -> bool: """Ask for yes or no and return boolean""" try: reply = self.statusbar.prompt(q) @@ -327,15 +327,15 @@ def confirm(self, q): return reply.lower() in (_("y"), _("yes")) - def notify(self, s, n=10, wait_for_keypress=False): + def notify(self, s: str, n: int = 10, wait_for_keypress: bool = False) -> None: return self.statusbar.message(s, n) - def file_prompt(self, s): + def file_prompt(self, s: int) -> str: return self.statusbar.prompt(s) class CLIRepl(repl.Repl): - def __init__(self, scr, interp, statusbar, config, idle=None): + def __init__(self, scr: curses.window, interp: repl.Interpreter, statusbar: 'Statusbar', config: Config, idle: None = None): super().__init__(interp, config) self.interp.writetb = self.writetb self.scr = scr @@ -357,10 +357,10 @@ def __init__(self, scr, interp, statusbar, config, idle=None): if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1: config.cli_suggestion_width = 0.8 - def _get_cursor_offset(self): + def _get_cursor_offset(self) -> int: return len(self.s) - self.cpos - def _set_cursor_offset(self, offset): + def _set_cursor_offset(self, offset: int) -> None: self.cpos = len(self.s) - offset cursor_offset = property( @@ -370,7 +370,7 @@ def _set_cursor_offset(self, offset): "The cursor offset from the beginning of the line", ) - def addstr(self, s): + def addstr(self, s: str) -> None: """Add a string to the current input line and figure out where it should go, depending on the cursor position.""" self.rl_history.reset() @@ -382,7 +382,7 @@ def addstr(self, s): self.complete() - def atbol(self): + def atbol(self) -> bool: """Return True or False accordingly if the cursor is at the beginning of the line (whitespace is ignored). This exists so that p_key() knows how to handle the tab key being pressed - if there is nothing but white @@ -391,17 +391,18 @@ def atbol(self): return not self.s.lstrip() - def bs(self, delete_tabs=True): + # This function shouldn't return None because of pos -= self.bs() later on + def bs(self, delete_tabs: bool = True) -> int: # type: ignore[return-value] """Process a backspace""" self.rl_history.reset() y, x = self.scr.getyx() if not self.s: - return + return None # type: ignore[return-value] if x == self.ix and y == self.iy: - return + return None # type: ignore[return-value] n = 1 @@ -422,7 +423,7 @@ def bs(self, delete_tabs=True): return n - def bs_word(self): + def bs_word(self) -> str: self.rl_history.reset() pos = len(self.s) - self.cpos - 1 deleted = [] @@ -437,7 +438,7 @@ def bs_word(self): return "".join(reversed(deleted)) - def check(self): + def check(self) -> None: """Check if paste mode should still be active and, if not, deactivate it and force syntax highlighting.""" @@ -448,14 +449,14 @@ def check(self): self.paste_mode = False self.print_line(self.s) - def clear_current_line(self): + def clear_current_line(self) -> None: """Called when a SyntaxError occurred in the interpreter. It is used to prevent autoindentation from occurring after a traceback.""" repl.Repl.clear_current_line(self) self.s = "" - def clear_wrapped_lines(self): + def clear_wrapped_lines(self) -> None: """Clear the wrapped lines of the current input.""" # curses does not handle this on its own. Sad. height, width = self.scr.getmaxyx() @@ -464,7 +465,7 @@ def clear_wrapped_lines(self): self.scr.move(y, 0) self.scr.clrtoeol() - def complete(self, tab=False): + def complete(self, tab: bool = False) -> None: """Get Autocomplete list and window. Called whenever these should be updated, and called @@ -494,7 +495,7 @@ def complete(self, tab=False): self.scr.redrawwin() self.scr.refresh() - def clrtobol(self): + def clrtobol(self) -> None: """Clear from cursor to beginning of line; usual C-u behaviour""" self.clear_wrapped_lines() @@ -507,10 +508,10 @@ def clrtobol(self): self.scr.redrawwin() self.scr.refresh() - def _get_current_line(self): + def _get_current_line(self) -> str: return self.s - def _set_current_line(self, line): + def _set_current_line(self, line: str) -> None: self.s = line current_line = property( @@ -520,7 +521,7 @@ def _set_current_line(self, line): "The characters of the current line", ) - def cut_to_buffer(self): + def cut_to_buffer(self) -> None: """Clear from cursor to end of line, placing into cut buffer""" self.cut_buffer = self.s[-self.cpos :] self.s = self.s[: -self.cpos] @@ -529,7 +530,7 @@ def cut_to_buffer(self): self.scr.redrawwin() self.scr.refresh() - def delete(self): + def delete(self) -> None: """Process a del""" if not self.s: return @@ -537,7 +538,7 @@ def delete(self): if self.mvc(-1): self.bs(False) - def echo(self, s, redraw=True): + def echo(self, s: str, redraw: bool = True) -> None: """Parse and echo a formatted string with appropriate attributes. It uses the formatting method as defined in formatter.py to parse the strings. It won't update the screen if it's reevaluating the code (as it @@ -571,7 +572,7 @@ def echo(self, s, redraw=True): if redraw and not self.evaluating: self.scr.refresh() - def end(self, refresh=True): + def end(self, refresh: bool = True) -> bool: self.cpos = 0 h, w = gethw() y, x = divmod(len(self.s) + self.ix, w) @@ -582,7 +583,7 @@ def end(self, refresh=True): return True - def hbegin(self): + def hbegin(self) -> None: """Replace the active line with first line in history and increment the index to keep track""" self.cpos = 0 @@ -591,7 +592,7 @@ def hbegin(self): self.s = self.rl_history.first() self.print_line(self.s, clr=True) - def hend(self): + def hend(self) -> None: """Same as hbegin() but, well, forward""" self.cpos = 0 self.clear_wrapped_lines() @@ -599,7 +600,7 @@ def hend(self): self.s = self.rl_history.last() self.print_line(self.s, clr=True) - def back(self): + def back(self) -> None: """Replace the active line with previous line in history and increment the index to keep track""" @@ -609,7 +610,7 @@ def back(self): self.s = self.rl_history.back() self.print_line(self.s, clr=True) - def fwd(self): + def fwd(self) -> None: """Same as back() but, well, forward""" self.cpos = 0 @@ -618,7 +619,7 @@ def fwd(self): self.s = self.rl_history.forward() self.print_line(self.s, clr=True) - def search(self): + def search(self) -> None: """Search with the partial matches from the history object.""" self.cpo = 0 @@ -627,7 +628,7 @@ def search(self): self.s = self.rl_history.back(start=False, search=True) self.print_line(self.s, clr=True) - def get_key(self): + def get_key(self) -> str: key = "" while True: try: @@ -667,7 +668,7 @@ def get_key(self): if self.idle: self.idle(self) - def get_line(self): + def get_line(self) -> Optional[str]: """Get a line of text and return it This function initialises an empty string and gets the curses cursor position on the screen and stores it From f42d1beca1dbae44906943ecf7e299f10e4b11fa Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Sun, 20 Feb 2022 17:26:33 -0600 Subject: [PATCH 049/262] Saving progress --- bpython/cli.py | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 812d4b60..cfb1fa25 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -52,7 +52,7 @@ import struct import sys import time -from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable, TypeVar, cast, IO, Iterable, Optional +from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable, TypeVar, cast, IO, Iterable, Optional, Union, Tuple import unicodedata from dataclasses import dataclass @@ -330,7 +330,7 @@ def confirm(self, q: str) -> bool: def notify(self, s: str, n: int = 10, wait_for_keypress: bool = False) -> None: return self.statusbar.message(s, n) - def file_prompt(self, s: int) -> str: + def file_prompt(self, s: str) -> str: return self.statusbar.prompt(s) @@ -668,7 +668,7 @@ def get_key(self) -> str: if self.idle: self.idle(self) - def get_line(self) -> Optional[str]: + def get_line(self) -> str: """Get a line of text and return it This function initialises an empty string and gets the curses cursor position on the screen and stores it @@ -694,14 +694,14 @@ def get_line(self) -> Optional[str]: self.s = self.s[4:] return self.s - def home(self, refresh=True): + def home(self, refresh: bool = True) -> bool: self.scr.move(self.iy, self.ix) self.cpos = len(self.s) if refresh: self.scr.refresh() return True - def lf(self): + def lf(self) -> None: """Process a linefeed character; it only needs to check the cursor position and move appropriately so it doesn't clear the current line after the cursor.""" @@ -713,7 +713,12 @@ def lf(self): self.print_line(self.s, newline=True) self.echo("\n") - def mkargspec(self, topline, in_arg, down): + def mkargspec( + self, + topline: Any, # Named tuples don't seem to play nice with mypy + in_arg: Union[str, int], + down: bool + ) -> int: """This figures out what to do with the argspec and puts it nicely into the list window. It returns the number of lines used to display the argspec. It's also kind of messy due to it having to call so many @@ -817,7 +822,7 @@ def mkargspec(self, topline, in_arg, down): return r - def mvc(self, i, refresh=True): + def mvc(self, i: int, refresh: bool = True) -> bool: """This method moves the cursor relatively from the current position, where: 0 == (right) end of current line @@ -850,7 +855,7 @@ def mvc(self, i, refresh=True): return True - def p_key(self, key): + def p_key(self, key: str) -> Union[None, str, bool]: """Process a keypress""" if key is None: @@ -1044,7 +1049,7 @@ def p_key(self, key): return True - def print_line(self, s, clr=False, newline=False): + def print_line(self, s: Optional[str], clr: bool = False, newline: bool = False) -> None: """Chuck a line of text through the highlighter, move the cursor to the beginning of the line and output it to the screen.""" @@ -1080,7 +1085,10 @@ def print_line(self, s, clr=False, newline=False): self.mvc(1) self.cpos = t - def prompt(self, more): + def prompt( + self, + more: Any # I'm not sure of the type on this one + ) -> None: """Show the appropriate Python prompt""" if not more: self.echo( @@ -1101,7 +1109,7 @@ def prompt(self, more): f"\x01{prompt_more_color}\x03{self.ps2}\x04" ) - def push(self, s, insert_into_history=True): + def push(self, s: str, insert_into_history: bool = True) -> bool: # curses.raw(True) prevents C-c from causing a SIGINT curses.raw(False) try: @@ -1114,7 +1122,7 @@ def push(self, s, insert_into_history=True): finally: curses.raw(True) - def redraw(self): + def redraw(self) -> None: """Redraw the screen using screen_hist""" self.scr.erase() for k, s in enumerate(self.screen_hist): @@ -1130,7 +1138,7 @@ def redraw(self): self.scr.refresh() self.statusbar.refresh() - def repl(self): + def repl(self) -> Tuple: """Initialise the repl and jump into the loop. This method also has to keep a stack of lines entered for the horrible "undo" feature. It also tracks everything that would normally go to stdout in the normal Python @@ -1171,7 +1179,7 @@ def repl(self): self.s = "" return self.exit_value - def reprint_line(self, lineno, tokens): + def reprint_line(self, lineno: int, tokens: MutableMapping[_TokenType, str]) -> None: """Helper function for paren highlighting: Reprint line at offset `lineno` in current input buffer.""" if not self.buffer or lineno == len(self.buffer): @@ -1194,7 +1202,7 @@ def reprint_line(self, lineno, tokens): for string in line.split("\x04"): self.echo(string) - def resize(self): + def resize(self) -> None: """This method exists simply to keep it straight forward when initialising a window and resizing it.""" self.size() @@ -1204,13 +1212,13 @@ def resize(self): self.statusbar.resize(refresh=False) self.redraw() - def getstdout(self): + def getstdout(self) -> str: """This method returns the 'spoofed' stdout buffer, for writing to a file or sending to a pastebin or whatever.""" return self.stdout_hist + "\n" - def reevaluate(self): + def reevaluate(self) -> None: """Clear the buffer, redraw the screen and re-evaluate the history""" self.evaluating = True @@ -1249,7 +1257,7 @@ def reevaluate(self): # map(self.push, self.history) # ^-- That's how simple this method was at first :( - def write(self, s): + def write(self, s: str) -> None: """For overriding stdout defaults""" if "\x04" in s: for block in s.split("\x04"): @@ -1418,7 +1426,7 @@ def suspend(self): curses.endwin() os.kill(os.getpid(), signal.SIGSTOP) - def tab(self, back=False): + def tab(self, back: bool = False) -> bool: """Process the tab key being hit. If there's only whitespace @@ -1498,7 +1506,7 @@ def yank_from_buffer(self): self.addstr(self.cut_buffer) self.print_line(self.s, clr=True) - def send_current_line_to_editor(self): + def send_current_line_to_editor(self) -> str: lines = self.send_to_external_editor(self.s).split("\n") self.s = "" self.print_line(self.s) From b47b91852ebca009a99e1364fbb174b4b1065760 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Wed, 23 Feb 2022 16:21:01 -0600 Subject: [PATCH 050/262] Progress --- bpython/cli.py | 124 ++++++++++++++++++++++++++++++------------------- 1 file changed, 75 insertions(+), 49 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index cfb1fa25..88c2b026 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -335,7 +335,14 @@ def file_prompt(self, s: str) -> str: class CLIRepl(repl.Repl): - def __init__(self, scr: curses.window, interp: repl.Interpreter, statusbar: 'Statusbar', config: Config, idle: None = None): + def __init__( + self, + scr: curses.window, + interp: repl.Interpreter, + statusbar: 'Statusbar', + config: Config, + idle: Optional[Callable] = None + ): super().__init__(interp, config) self.interp.writetb = self.writetb self.scr = scr @@ -343,7 +350,7 @@ def __init__(self, scr: curses.window, interp: repl.Interpreter, statusbar: 'Sta self.list_win = newwin(get_colpair(config, "background"), 1, 1, 1, 1) self.cpos = 0 self.do_exit = False - self.exit_value = () + self.exit_value: Tuple[Any, ...] = () self.f_string = "" self.idle = idle self.in_hist = False @@ -1138,7 +1145,7 @@ def redraw(self) -> None: self.scr.refresh() self.statusbar.refresh() - def repl(self) -> Tuple: + def repl(self) -> Tuple[Any, ...]: """Initialise the repl and jump into the loop. This method also has to keep a stack of lines entered for the horrible "undo" feature. It also tracks everything that would normally go to stdout in the normal Python @@ -1277,8 +1284,13 @@ def write(self, s: str) -> None: self.screen_hist.append(s.rstrip()) def show_list( - self, items, arg_pos, topline=None, formatter=None, current_item=None - ): + self, + items: List[str], + arg_pos: Union[str, int], + topline: Any = None, # Named tuples don't play nice with mypy + formatter: Optional[Callable] = None, + current_item: Optional[bool] = None + ) -> None: shared = ShowListState() y, x = self.scr.getyx() h, w = self.scr.getmaxyx() @@ -1290,7 +1302,7 @@ def show_list( max_w = int(w * self.config.cli_suggestion_width) self.list_win.erase() - if items: + if items and formatter: items = [formatter(x) for x in items] if current_item: current_item = formatter(current_item) @@ -1300,7 +1312,7 @@ def show_list( else: height_offset = 0 - def lsize(): + def lsize() -> bool: wl = max(len(i) for i in v_items) + 1 if not wl: wl = 1 @@ -1410,17 +1422,18 @@ def lsize(): self.scr.move(*self.scr.getyx()) self.list_win.refresh() - def size(self): + def size(self) -> None: """Set instance attributes for x and y top left corner coordinates and width and height for the window.""" global stdscr - h, w = stdscr.getmaxyx() - self.y = 0 - self.w = w - self.h = h - 1 - self.x = 0 - - def suspend(self): + if stdscr: + h, w = stdscr.getmaxyx() + self.y: int = 0 + self.w: int = w + self.h: int = h - 1 + self.x: int = 0 + + def suspend(self) -> None: """Suspend the current process for shell job control.""" if platform.system() != "Windows": curses.endwin() @@ -1489,19 +1502,19 @@ def tab(self, back: bool = False) -> bool: self.print_line(self.s, True) return True - def undo(self, n=1): + def undo(self, n: int = 1) -> None: repl.Repl.undo(self, n) # This will unhighlight highlighted parens self.print_line(self.s) - def writetb(self, lines): + def writetb(self, lines: List[str]) -> None: for line in lines: self.write( "\x01{}\x03{}".format(self.config.color_scheme["error"], line) ) - def yank_from_buffer(self): + def yank_from_buffer(self) -> None: """Paste the text from the cut buffer at the current cursor location""" self.addstr(self.cut_buffer) self.print_line(self.s, clr=True) @@ -1569,7 +1582,15 @@ class Statusbar: """ - def __init__(self, scr, pwin, background, config, s=None, c=None): + def __init__( + self, + scr: curses.window, + pwin: curses.window, + background: int, + config: Config, + s: Optional[str] = None, + c: Optional[int] = None + ): """Initialise the statusbar and display the initial text (if any)""" self.size() self.win = newwin(background, self.h, self.w, self.y, self.x) @@ -1581,9 +1602,10 @@ def __init__(self, scr, pwin, background, config, s=None, c=None): self.c = c self.timer = 0 self.pwin = pwin - self.settext(s, c) + if s: + self.settext(s, c) - def size(self): + def size(self) -> None: """Set instance attributes for x and y top left corner coordinates and width and height for the window.""" h, w = gethw() @@ -1592,7 +1614,7 @@ def size(self): self.h = 1 self.x = 0 - def resize(self, refresh=True): + def resize(self, refresh: bool = True) -> None: """This method exists simply to keep it straight forward when initialising a window and resizing it.""" self.size() @@ -1601,12 +1623,12 @@ def resize(self, refresh=True): if refresh: self.refresh() - def refresh(self): + def refresh(self) -> None: """This is here to make sure the status bar text is redraw properly after a resize.""" self.settext(self._s) - def check(self): + def check(self) -> None: """This is the method that should be called every half second or so to see if the status bar needs updating.""" if not self.timer: @@ -1617,13 +1639,13 @@ def check(self): self.settext(self._s) - def message(self, s, n=3): + def message(self, s: str, n: int = 3) -> None: """Display a message for a short n seconds on the statusbar and return it to its original state.""" - self.timer = time.time() + n + self.timer = int(time.time() + n) self.settext(s) - def prompt(self, s=""): + def prompt(self, s: str = "") -> str: """Prompt the user for some input (with the optional prompt 's') and return the input text, then restore the statusbar to its original value.""" @@ -1631,7 +1653,7 @@ def prompt(self, s=""): self.settext(s or "? ", p=True) iy, ix = self.win.getyx() - def bs(s): + def bs(s: str) -> str: y, x = self.win.getyx() if x == ix: return s @@ -1656,14 +1678,14 @@ def bs(s): raise ValueError # literal elif 0 < c < 127: - c = chr(c) - self.win.addstr(c, get_colpair(self.config, "prompt")) - o += c + d = chr(c) + self.win.addstr(d, get_colpair(self.config, "prompt")) + o += d self.settext(self._s) return o - def settext(self, s, c=None, p=False): + def settext(self, s: str, c: Optional[int] = None, p: bool = False) -> None: """Set the text on the status bar to a new permanent value; this is the value that will be set after a prompt or message. c is the optional curses colour pair to use (if not specified the last specified colour @@ -1690,12 +1712,12 @@ def settext(self, s, c=None, p=False): else: self.win.refresh() - def clear(self): + def clear(self) -> None: """Clear the status bar.""" self.win.clear() -def init_wins(scr, config): +def init_wins(scr: curses.window, config: Config) -> Tuple[curses.window, Statusbar]: """Initialise the two windows (the main repl interface and the little status bar at the bottom with some stuff in it)""" # TODO: Document better what stuff is on the status bar. @@ -1705,7 +1727,9 @@ def init_wins(scr, config): main_win = newwin(background, h - 1, w, 0, 0) main_win.scrollok(True) - main_win.keypad(1) + + # I think this is supposed to be True instead of 1? + main_win.keypad(1) # type:ignore[arg-type] # Thanks to Angus Gibson for pointing out this missing line which was causing # problems that needed dirty hackery to fix. :) @@ -1728,18 +1752,18 @@ def init_wins(scr, config): return main_win, statusbar -def sigwinch(unused_scr): +def sigwinch(unused_scr: curses.window) -> None: global DO_RESIZE DO_RESIZE = True -def sigcont(unused_scr): +def sigcont(unused_scr: curses.window) -> None: sigwinch(unused_scr) # Forces the redraw curses.ungetch("\x00") -def gethw(): +def gethw() -> Tuple[int, int]: """I found this code on a usenet post, and snipped out the bit I needed, so thanks to whoever wrote that, sorry I forgot your name, I'm sure you're a great guy. @@ -1757,10 +1781,10 @@ def gethw(): if platform.system() != "Windows": h, w = struct.unpack( - "hhhh", fcntl.ioctl(sys.__stdout__, termios.TIOCGWINSZ, "\000" * 8) + "hhhh", fcntl.ioctl(sys.__stdout__, termios.TIOCGWINSZ, "\000" * 8) # type:ignore[call-overload] )[0:2] else: - from ctypes import windll, create_string_buffer + from ctypes import windll, create_string_buffer # type:ignore[attr-defined] # stdin handle is -10 # stdout handle is -11 @@ -1786,7 +1810,7 @@ def gethw(): ) = struct.unpack("hhhhHhhhhhh", csbi.raw) sizex = right - left + 1 sizey = bottom - top + 1 - else: + elif stdscr: # can't determine actual size - return default values sizex, sizey = stdscr.getmaxyx() @@ -1794,7 +1818,7 @@ def gethw(): return h, w -def idle(caller): +def idle(caller: CLIRepl) -> None: """This is called once every iteration through the getkey() loop (currently in the Repl class, see the get_line() method). The statusbar check needs to go here to take care of timed @@ -1817,7 +1841,7 @@ def idle(caller): do_resize(caller) -def do_resize(caller): +def do_resize(caller: CLIRepl) -> None: """This needs to hack around readline and curses not playing nicely together. See also gethw() above.""" global DO_RESIZE @@ -1844,14 +1868,14 @@ class FakeDict: used as a hacky solution to using a colours dict containing colour codes if colour initialisation fails.""" - def __init__(self, val): + def __init__(self, val: int): self._val = val - def __getitem__(self, k): + def __getitem__(self, k: Any) -> int: return self._val -def newwin(background, *args): +def newwin(background: int, *args: int) -> curses.window: """Wrapper for curses.newwin to automatically set background colour on any newly created window.""" win = curses.newwin(*args) @@ -1859,7 +1883,7 @@ def newwin(background, *args): return win -def curses_wrapper(func, *args, **kwargs): +def curses_wrapper(func: Callable, *args: Any, **kwargs: Any) -> Any: """Like curses.wrapper(), but reuses stdscr when called again.""" global stdscr if stdscr is None: @@ -1867,7 +1891,8 @@ def curses_wrapper(func, *args, **kwargs): try: curses.noecho() curses.cbreak() - stdscr.keypad(1) + # Should this be keypad(True)? + stdscr.keypad(1) # type:ignore[arg-type] try: curses.start_color() @@ -1876,7 +1901,8 @@ def curses_wrapper(func, *args, **kwargs): return func(stdscr, *args, **kwargs) finally: - stdscr.keypad(0) + # Should this be keypad(False)? + stdscr.keypad(0) # type:ignore[arg-type] curses.echo() curses.nocbreak() curses.endwin() From 4e55dde2e2582158cf8d6301b1af292003f3a150 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Sun, 6 Mar 2022 21:06:40 -0600 Subject: [PATCH 051/262] Finished squashing the mypy errors, use --follow-imports=error to ignore errors from imports that haven't been typed yet. --- bpython/cli.py | 56 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 88c2b026..29658f7e 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -315,23 +315,29 @@ def make_colors(config: Config) -> MutableMapping[str, int]: class CLIInteraction(repl.Interaction): - def __init__(self, config: Config, statusbar: 'Statusbar' = None): + def __init__(self, config: Config, statusbar: Optional['Statusbar'] = None): super().__init__(config, statusbar) def confirm(self, q: str) -> bool: """Ask for yes or no and return boolean""" try: - reply = self.statusbar.prompt(q) + if self.statusbar: + reply = self.statusbar.prompt(q) except ValueError: return False return reply.lower() in (_("y"), _("yes")) def notify(self, s: str, n: int = 10, wait_for_keypress: bool = False) -> None: - return self.statusbar.message(s, n) + if self.statusbar: + self.statusbar.message(s, n) - def file_prompt(self, s: str) -> str: - return self.statusbar.prompt(s) + def file_prompt(self, s: str) -> Optional[str]: + if self.statusbar: + # This thows a mypy error because repl.py isn't typed yet + return self.statusbar.prompt(s) # type:ignore[no-any-return] + else: + return None class CLIRepl(repl.Repl): @@ -970,7 +976,7 @@ def p_key(self, key: str) -> Union[None, str, bool]: elif key in key_dispatch[config.clear_screen_key]: # clear all but current line - self.screen_hist = [self.screen_hist[-1]] + self.screen_hist: List = [self.screen_hist[-1]] self.highlighted_paren = None self.redraw() return "" @@ -1120,7 +1126,8 @@ def push(self, s: str, insert_into_history: bool = True) -> bool: # curses.raw(True) prevents C-c from causing a SIGINT curses.raw(False) try: - return repl.Repl.push(self, s, insert_into_history) + x: bool = repl.Repl.push(self, s, insert_into_history) + return x except SystemExit as e: # Avoid a traceback on e.g. quit() self.do_exit = True @@ -1231,7 +1238,7 @@ def reevaluate(self) -> None: self.evaluating = True self.stdout_hist = "" self.f_string = "" - self.buffer = [] + self.buffer: List[str] = [] self.scr.erase() self.screen_hist = [] # Set cursor position to -1 to prevent paren matching @@ -1593,7 +1600,7 @@ def __init__( ): """Initialise the statusbar and display the initial text (if any)""" self.size() - self.win = newwin(background, self.h, self.w, self.y, self.x) + self.win: curses.window = newwin(background, self.h, self.w, self.y, self.x) self.config = config @@ -1908,7 +1915,14 @@ def curses_wrapper(func: Callable, *args: Any, **kwargs: Any) -> Any: curses.endwin() -def main_curses(scr, args, config, interactive=True, locals_=None, banner=None): +def main_curses( + scr: curses.window, + args: List[str], + config: Config, + interactive: bool = True, + locals_: Optional[MutableMapping[str, str]] = None, + banner: Optional[str] = None +) -> Tuple[Tuple[Any, ...], str]: """main function for the curses convenience wrapper Initialise the two main objects: the interpreter @@ -1941,7 +1955,9 @@ def main_curses(scr, args, config, interactive=True, locals_=None, banner=None): curses.use_default_colors() cols = make_colors(config) except curses.error: - cols = FakeDict(-1) + # Not sure what to do with the types here... + # FakeDict acts as a dictionary, but isn't actually a dictionary + cols = FakeDict(-1) # type:ignore[assignment] # FIXME: Gargh, bad design results in using globals without a refactor :( colors = cols @@ -1956,12 +1972,13 @@ def main_curses(scr, args, config, interactive=True, locals_=None, banner=None): clirepl = CLIRepl(main_win, interpreter, statusbar, config, idle) clirepl._C = cols - sys.stdin = FakeStdin(clirepl) - sys.stdout = FakeStream(clirepl, lambda: sys.stdout) - sys.stderr = FakeStream(clirepl, lambda: sys.stderr) + # Not sure how to type these Fake types + sys.stdin = FakeStdin(clirepl) # type:ignore[assignment] + sys.stdout = FakeStream(clirepl, lambda: sys.stdout) # type:ignore + sys.stderr = FakeStream(clirepl, lambda: sys.stderr) # type:ignore if args: - exit_value = () + exit_value: Tuple[Any, ...] = () try: bpargs.exec_code(interpreter, args) except SystemExit as e: @@ -1995,7 +2012,8 @@ def main_curses(scr, args, config, interactive=True, locals_=None, banner=None): exit_value = clirepl.repl() if hasattr(sys, "exitfunc"): - sys.exitfunc() + # Seems like the if statment should satisfy mypy, but it doesn't + sys.exitfunc() # type:ignore[attr-defined] delattr(sys, "exitfunc") main_win.erase() @@ -2012,7 +2030,11 @@ def main_curses(scr, args, config, interactive=True, locals_=None, banner=None): return (exit_value, clirepl.getstdout()) -def main(args=None, locals_=None, banner=None): +def main( + args: Optional[List[str]] = None, + locals_: Optional[MutableMapping[str, str]] = None, + banner: Optional[str] = None +) -> Any: translations.init() config, options, exec_args = argsparse(args) From 136631d4547effee53b7eba8aaeb75e904598497 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Sun, 6 Mar 2022 21:13:23 -0600 Subject: [PATCH 052/262] Formatted with Black --- bpython/cli.py | 127 ++++++++++++++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 50 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 29658f7e..ed885669 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -52,7 +52,21 @@ import struct import sys import time -from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable, TypeVar, cast, IO, Iterable, Optional, Union, Tuple +from typing import ( + Iterator, + NoReturn, + List, + MutableMapping, + Any, + Callable, + TypeVar, + cast, + IO, + Iterable, + Optional, + Union, + Tuple, +) import unicodedata from dataclasses import dataclass @@ -84,7 +98,7 @@ from .pager import page from .args import parse as argsparse -F = TypeVar('F', bound=Callable[..., Any]) +F = TypeVar("F", bound=Callable[..., Any]) # --- module globals --- stdscr = None @@ -135,7 +149,7 @@ class FakeStream: """Provide a fake file object which calls functions on the interface provided.""" - def __init__(self, interface: 'CLIRepl', get_dest: IO[str]) -> None: + def __init__(self, interface: "CLIRepl", get_dest: IO[str]) -> None: self.encoding: str = getpreferredencoding() self.interface = interface self.get_dest = get_dest @@ -160,7 +174,7 @@ def flush(self) -> None: class FakeStdin: """Provide a fake stdin type for things like raw_input() etc.""" - def __init__(self, interface: 'CLIRepl') -> None: + def __init__(self, interface: "CLIRepl") -> None: """Take the curses Repl on init and assume it provides a get_key method which, fortunately, it does.""" @@ -263,7 +277,7 @@ def readlines(self, size: int = -1) -> List[str]: # Have to ignore the return type on this one because the colors variable # is Optional[MutableMapping[str, int]] but for the purposes of this # function it can't be None -def get_color(config: Config, name: str) -> int: # type: ignore[return] +def get_color(config: Config, name: str) -> int: # type: ignore[return] global colors if colors: return colors[config.color_scheme[name].lower()] @@ -315,7 +329,7 @@ def make_colors(config: Config) -> MutableMapping[str, int]: class CLIInteraction(repl.Interaction): - def __init__(self, config: Config, statusbar: Optional['Statusbar'] = None): + def __init__(self, config: Config, statusbar: Optional["Statusbar"] = None): super().__init__(config, statusbar) def confirm(self, q: str) -> bool: @@ -328,7 +342,9 @@ def confirm(self, q: str) -> bool: return reply.lower() in (_("y"), _("yes")) - def notify(self, s: str, n: int = 10, wait_for_keypress: bool = False) -> None: + def notify( + self, s: str, n: int = 10, wait_for_keypress: bool = False + ) -> None: if self.statusbar: self.statusbar.message(s, n) @@ -342,12 +358,12 @@ def file_prompt(self, s: str) -> Optional[str]: class CLIRepl(repl.Repl): def __init__( - self, - scr: curses.window, - interp: repl.Interpreter, - statusbar: 'Statusbar', - config: Config, - idle: Optional[Callable] = None + self, + scr: curses.window, + interp: repl.Interpreter, + statusbar: "Statusbar", + config: Config, + idle: Optional[Callable] = None, ): super().__init__(interp, config) self.interp.writetb = self.writetb @@ -412,10 +428,10 @@ def bs(self, delete_tabs: bool = True) -> int: # type: ignore[return-value] y, x = self.scr.getyx() if not self.s: - return None # type: ignore[return-value] + return None # type: ignore[return-value] if x == self.ix and y == self.iy: - return None # type: ignore[return-value] + return None # type: ignore[return-value] n = 1 @@ -727,10 +743,10 @@ def lf(self) -> None: self.echo("\n") def mkargspec( - self, - topline: Any, # Named tuples don't seem to play nice with mypy - in_arg: Union[str, int], - down: bool + self, + topline: Any, # Named tuples don't seem to play nice with mypy + in_arg: Union[str, int], + down: bool, ) -> int: """This figures out what to do with the argspec and puts it nicely into the list window. It returns the number of lines used to display the @@ -1062,7 +1078,9 @@ def p_key(self, key: str) -> Union[None, str, bool]: return True - def print_line(self, s: Optional[str], clr: bool = False, newline: bool = False) -> None: + def print_line( + self, s: Optional[str], clr: bool = False, newline: bool = False + ) -> None: """Chuck a line of text through the highlighter, move the cursor to the beginning of the line and output it to the screen.""" @@ -1098,10 +1116,7 @@ def print_line(self, s: Optional[str], clr: bool = False, newline: bool = False) self.mvc(1) self.cpos = t - def prompt( - self, - more: Any # I'm not sure of the type on this one - ) -> None: + def prompt(self, more: Any) -> None: # I'm not sure of the type on this one """Show the appropriate Python prompt""" if not more: self.echo( @@ -1193,7 +1208,9 @@ def repl(self) -> Tuple[Any, ...]: self.s = "" return self.exit_value - def reprint_line(self, lineno: int, tokens: MutableMapping[_TokenType, str]) -> None: + def reprint_line( + self, lineno: int, tokens: MutableMapping[_TokenType, str] + ) -> None: """Helper function for paren highlighting: Reprint line at offset `lineno` in current input buffer.""" if not self.buffer or lineno == len(self.buffer): @@ -1291,12 +1308,12 @@ def write(self, s: str) -> None: self.screen_hist.append(s.rstrip()) def show_list( - self, - items: List[str], - arg_pos: Union[str, int], - topline: Any = None, # Named tuples don't play nice with mypy - formatter: Optional[Callable] = None, - current_item: Optional[bool] = None + self, + items: List[str], + arg_pos: Union[str, int], + topline: Any = None, # Named tuples don't play nice with mypy + formatter: Optional[Callable] = None, + current_item: Optional[bool] = None, ) -> None: shared = ShowListState() y, x = self.scr.getyx() @@ -1591,16 +1608,18 @@ class Statusbar: def __init__( self, - scr: curses.window, - pwin: curses.window, - background: int, - config: Config, - s: Optional[str] = None, - c: Optional[int] = None + scr: curses.window, + pwin: curses.window, + background: int, + config: Config, + s: Optional[str] = None, + c: Optional[int] = None, ): """Initialise the statusbar and display the initial text (if any)""" self.size() - self.win: curses.window = newwin(background, self.h, self.w, self.y, self.x) + self.win: curses.window = newwin( + background, self.h, self.w, self.y, self.x + ) self.config = config @@ -1724,7 +1743,9 @@ def clear(self) -> None: self.win.clear() -def init_wins(scr: curses.window, config: Config) -> Tuple[curses.window, Statusbar]: +def init_wins( + scr: curses.window, config: Config +) -> Tuple[curses.window, Statusbar]: """Initialise the two windows (the main repl interface and the little status bar at the bottom with some stuff in it)""" # TODO: Document better what stuff is on the status bar. @@ -1788,10 +1809,16 @@ def gethw() -> Tuple[int, int]: if platform.system() != "Windows": h, w = struct.unpack( - "hhhh", fcntl.ioctl(sys.__stdout__, termios.TIOCGWINSZ, "\000" * 8) # type:ignore[call-overload] + "hhhh", + fcntl.ioctl( + sys.__stdout__, termios.TIOCGWINSZ, "\000" * 8 + ), # type:ignore[call-overload] )[0:2] else: - from ctypes import windll, create_string_buffer # type:ignore[attr-defined] + from ctypes import ( + windll, + create_string_buffer, + ) # type:ignore[attr-defined] # stdin handle is -10 # stdout handle is -11 @@ -1916,12 +1943,12 @@ def curses_wrapper(func: Callable, *args: Any, **kwargs: Any) -> Any: def main_curses( - scr: curses.window, - args: List[str], - config: Config, - interactive: bool = True, - locals_: Optional[MutableMapping[str, str]] = None, - banner: Optional[str] = None + scr: curses.window, + args: List[str], + config: Config, + interactive: bool = True, + locals_: Optional[MutableMapping[str, str]] = None, + banner: Optional[str] = None, ) -> Tuple[Tuple[Any, ...], str]: """main function for the curses convenience wrapper @@ -2031,9 +2058,9 @@ def main_curses( def main( - args: Optional[List[str]] = None, - locals_: Optional[MutableMapping[str, str]] = None, - banner: Optional[str] = None + args: Optional[List[str]] = None, + locals_: Optional[MutableMapping[str, str]] = None, + banner: Optional[str] = None, ) -> Any: translations.init() From 2ae5c4510ddfd7e6852a429c33fa149e38957754 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Wed, 16 Mar 2022 21:54:27 -0500 Subject: [PATCH 053/262] Done with cli.py, working on repl.py --- bpython/cli.py | 44 +++++++++++++++++++++-------- bpython/repl.py | 74 +++++++++++++++++++++++++------------------------ 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index ed885669..c88dac97 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -66,6 +66,8 @@ Optional, Union, Tuple, + Collection, + Dict ) import unicodedata from dataclasses import dataclass @@ -287,7 +289,7 @@ def get_colpair(config: Config, name: str) -> int: return curses.color_pair(get_color(config, name) + 1) -def make_colors(config: Config) -> MutableMapping[str, int]: +def make_colors(config: Config) -> Dict[str, int]: """Init all the colours in curses and bang them into a dictionary""" # blacK, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default: @@ -366,8 +368,10 @@ def __init__( idle: Optional[Callable] = None, ): super().__init__(interp, config) - self.interp.writetb = self.writetb - self.scr = scr + # mypy doesn't quite understand the difference between a class variable with a callable type and a method. + # https://github.com/python/mypy/issues/2427 + self.interp.writetb = self.writetb # type:ignore[assignment] + self.scr: curses.window = scr self.stdout_hist = "" # native str (bytes in Py2, unicode in Py3) self.list_win = newwin(get_colpair(config, "background"), 1, 1, 1, 1) self.cpos = 0 @@ -382,6 +386,10 @@ def __init__( self.statusbar = statusbar self.formatter = BPythonFormatter(config.color_scheme) self.interact = CLIInteraction(self.config, statusbar=self.statusbar) + self.ix: int + self.iy: int + self.arg_pos: Union[str, int, None] + self.prev_block_finished: int if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1: config.cli_suggestion_width = 0.8 @@ -505,13 +513,18 @@ def complete(self, tab: bool = False) -> None: return list_win_visible = repl.Repl.complete(self, tab) + + f = None + if self.matches_iter.completer: + f = self.matches_iter.completer.format + if list_win_visible: try: self.show_list( self.matches_iter.matches, self.arg_pos, topline=self.funcprops, - formatter=self.matches_iter.completer.format, + formatter=f, ) except curses.error: # XXX: This is a massive hack, it will go away when I get @@ -745,7 +758,7 @@ def lf(self) -> None: def mkargspec( self, topline: Any, # Named tuples don't seem to play nice with mypy - in_arg: Union[str, int], + in_arg: Union[str, int, None], down: bool, ) -> int: """This figures out what to do with the argspec and puts it nicely into @@ -1310,11 +1323,12 @@ def write(self, s: str) -> None: def show_list( self, items: List[str], - arg_pos: Union[str, int], + arg_pos: Union[str, int, None], topline: Any = None, # Named tuples don't play nice with mypy formatter: Optional[Callable] = None, - current_item: Optional[bool] = None, + current_item: Optional[str] = None, ) -> None: + v_items: Collection shared = ShowListState() y, x = self.scr.getyx() h, w = self.scr.getmaxyx() @@ -1475,6 +1489,10 @@ def tab(self, back: bool = False) -> bool: and don't indent if there are only whitespace in the line. """ + f = None + if self.matches_iter.completer: + f = self.matches_iter.completer.format + # 1. check if we should add a tab character if self.atbol() and not back: x_pos = len(self.s) - self.cpos @@ -1505,15 +1523,16 @@ def tab(self, back: bool = False) -> bool: # 4. swap current word for a match list item elif self.matches_iter.matches: - current_match = ( - back and self.matches_iter.previous() or next(self.matches_iter) + n: str = next(self.matches_iter) + current_match: Optional[str] = ( + back and self.matches_iter.previous() or n ) try: self.show_list( self.matches_iter.matches, self.arg_pos, topline=self.funcprops, - formatter=self.matches_iter.completer.format, + formatter=f, current_item=current_match, ) except curses.error: @@ -1815,10 +1834,11 @@ def gethw() -> Tuple[int, int]: ), # type:ignore[call-overload] )[0:2] else: - from ctypes import ( + # Ignoring mypy's windll error because it's Windows-specific + from ctypes import ( # type:ignore[attr-defined] windll, create_string_buffer, - ) # type:ignore[attr-defined] + ) # stdin handle is -10 # stdout handle is -11 diff --git a/bpython/repl.py b/bpython/repl.py index 4ee4eeba..f2e599ed 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -37,11 +37,11 @@ from itertools import takewhile from pathlib import Path from types import ModuleType, TracebackType -from typing import cast, List, Tuple, Any, Optional, Type +from typing import cast, List, Tuple, Any, Optional, Type, Union, MutableMapping, Callable, Dict from ._typing_compat import Literal from pygments.lexers import Python3Lexer -from pygments.token import Token +from pygments.token import Token, _TokenType have_pyperclip = True try: @@ -50,7 +50,8 @@ have_pyperclip = False from . import autocomplete, inspection, simpleeval -from .config import getpreferredencoding +from .cli import Statusbar +from .config import getpreferredencoding, Config from .formatter import Parenthesis from .history import History from .lazyre import LazyReCompile @@ -92,7 +93,7 @@ class Interpreter(code.InteractiveInterpreter): bpython_input_re = LazyReCompile(r"") - def __init__(self, locals=None, encoding=None): + def __init__(self, locals: Optional[MutableMapping[str, str]] = None, encoding: Optional[str] = None): """Constructor. The optional 'locals' argument specifies the dictionary in which code @@ -114,7 +115,7 @@ def __init__(self, locals=None, encoding=None): """ self.encoding = encoding or getpreferredencoding() - self.syntaxerror_callback = None + self.syntaxerror_callback: Optional[Callable] = None if locals is None: # instead of messing with sys.modules, we should modify sys.modules @@ -349,7 +350,7 @@ def clear(self) -> None: class Interaction: - def __init__(self, config, statusbar=None): + def __init__(self, config: Config, statusbar: Optional[Statusbar] = None): self.config = config if statusbar: @@ -402,7 +403,7 @@ class Repl: XXX Subclasses should implement echo, current_line, cw """ - def __init__(self, interp, config): + def __init__(self, interp: Interpreter, config: Config): """Initialise the repl. interp is a Python code.InteractiveInterpreter instance @@ -412,7 +413,7 @@ def __init__(self, interp, config): self.config = config self.cut_buffer = "" - self.buffer = [] + self.buffer: List[str] = [] self.interp = interp self.interp.syntaxerror_callback = self.clear_current_line self.match = False @@ -421,17 +422,17 @@ def __init__(self, interp, config): ) # all input and output, stored as old style format strings # (\x01, \x02, ...) for cli.py - self.screen_hist = [] - self.history = [] # commands executed since beginning of session - self.redo_stack = [] + self.screen_hist: List[str] = [] + self.history: List[str] = [] # commands executed since beginning of session + self.redo_stack: List[str] = [] self.evaluating = False self.matches_iter = MatchesIterator() self.funcprops = None - self.arg_pos = None + self.arg_pos: Union[str, int, None] = None self.current_func = None self.highlighted_paren = None - self._C = {} - self.prev_block_finished = 0 + self._C: Dict[str, int] = {} + self.prev_block_finished: int = 0 self.interact = Interaction(self.config) # previous pastebin content to prevent duplicate pastes, filled on call # to repl.pastebin @@ -441,11 +442,12 @@ def __init__(self, interp, config): # Necessary to fix mercurial.ui.ui expecting sys.stderr to have this # attribute self.closed = False + self.paster: Union[PasteHelper, PastePinnwand] if self.config.hist_file.exists(): try: self.rl_history.load( - self.config.hist_file, getpreferredencoding() or "ascii" + str(self.config.hist_file), getpreferredencoding() or "ascii" ) except OSError: pass @@ -472,7 +474,7 @@ def ps1(self) -> str: def ps2(self) -> str: return cast(str, getattr(sys, "ps2", "... ")) - def startup(self): + def startup(self) -> None: """ Execute PYTHONSTARTUP file if it exits. Call this after front end-specific initialisation. @@ -642,7 +644,7 @@ def get_args(self): self.arg_pos = None return False - def get_source_of_current_name(self): + def get_source_of_current_name(self) -> str: """Return the unicode source code of the object which is bound to the current name in the current input line. Throw `SourceNotFound` if the source cannot be found.""" @@ -692,7 +694,7 @@ def set_docstring(self): # If exactly one match that is equal to current line, clear matches # If example one match and tab=True, then choose that and clear matches - def complete(self, tab=False): + def complete(self, tab: bool = False) -> Optional[bool]: """Construct a full list of possible completions and display them in a window. Also check if there's an available argspec (via the inspect module) and bang that on top of the completions too. @@ -743,7 +745,7 @@ def complete(self, tab=False): else: return tab or completer.shown_before_tab - def format_docstring(self, docstring, width, height): + def format_docstring(self, docstring: str, width: int, height: int) -> str: """Take a string and try to format it into a sane list of strings to be put into the suggestion box.""" @@ -763,7 +765,7 @@ def format_docstring(self, docstring, width, height): out[-1] = out[-1].rstrip() return out - def next_indentation(self): + def next_indentation(self) -> int: """Return the indentation of the next line based on the current input buffer.""" if self.buffer: @@ -804,7 +806,7 @@ def process(): return "\n".join(process()) - def write2file(self): + def write2file(self) -> None: """Prompt for a filename and write the current contents of the stdout buffer to disk.""" @@ -851,7 +853,7 @@ def write2file(self): else: self.interact.notify(_("Saved to %s.") % (fn,)) - def copy2clipboard(self): + def copy2clipboard(self) -> None: """Copy current content to clipboard.""" if not have_pyperclip: @@ -866,7 +868,7 @@ def copy2clipboard(self): else: self.interact.notify(_("Copied content to clipboard.")) - def pastebin(self, s=None): + def pastebin(self, s=None) -> Optional[str]: """Upload to a pastebin and display the URL in the status bar.""" if s is None: @@ -879,7 +881,7 @@ def pastebin(self, s=None): else: return self.do_pastebin(s) - def do_pastebin(self, s): + def do_pastebin(self, s) -> Optional[str]: """Actually perform the upload.""" if s == self.prev_pastebin_content: self.interact.notify( @@ -911,7 +913,7 @@ def do_pastebin(self, s): return paste_url - def push(self, s, insert_into_history=True): + def push(self, s, insert_into_history=True) -> bool: """Push a line of code onto the buffer so it can process it all at once when a code block ends""" # This push method is used by cli and urwid, but not curtsies @@ -936,7 +938,7 @@ def insert_into_history(self, s): except RuntimeError as e: self.interact.notify(f"{e}") - def prompt_undo(self): + def prompt_undo(self) -> int: """Returns how many lines to undo, 0 means don't undo""" if ( self.config.single_undo_time < 0 @@ -944,14 +946,14 @@ def prompt_undo(self): ): return 1 est = self.interp.timer.estimate() - n = self.interact.file_prompt( + m = self.interact.file_prompt( _("Undo how many lines? (Undo will take up to ~%.1f seconds) [1]") % (est,) ) try: - if n == "": - n = "1" - n = int(n) + if m == "": + m = "1" + n = int(m) except ValueError: self.interact.notify(_("Undo canceled"), 0.1) return 0 @@ -968,7 +970,7 @@ def prompt_undo(self): self.interact.notify(message % (n, est), 0.1) return n - def undo(self, n=1): + def undo(self, n: int = 1) -> None: """Go back in the undo history n steps and call reevaluate() Note that in the program this is called "Rewind" because I want it to be clear that this is by no means a true undo @@ -992,7 +994,7 @@ def undo(self, n=1): self.rl_history.entries = entries - def flush(self): + def flush(self) -> None: """Olivier Grisel brought it to my attention that the logging module tries to call this method, since it makes assumptions about stdout that may not necessarily be true. The docs for @@ -1009,7 +1011,7 @@ def flush(self): def close(self): """See the flush() method docstring.""" - def tokenize(self, s, newline=False): + def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: """Tokenizes a line of code, returning pygments tokens with side effects/impurities: - reads self.cpos to see what parens should be highlighted @@ -1107,11 +1109,11 @@ def tokenize(self, s, newline=False): return list() return line_tokens - def clear_current_line(self): + def clear_current_line(self) -> None: """This is used as the exception callback for the Interpreter instance. It prevents autoindentation from occurring after a traceback.""" - def send_to_external_editor(self, text): + def send_to_external_editor(self, text: str) -> str: """Returns modified text from an editor, or the original text if editor exited with non-zero""" @@ -1169,7 +1171,7 @@ def edit_config(self): self.interact.notify(_("Error editing config file: %s") % e) -def next_indentation(line, tab_length): +def next_indentation(line, tab_length) -> int: """Given a code line, return the indentation of the next line.""" line = line.expandtabs(tab_length) indentation = (len(line) - len(line.lstrip(" "))) // tab_length From c2f1c3919606e80d5376865a4cb1beefe69d0080 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Wed, 23 Mar 2022 20:49:33 -0500 Subject: [PATCH 054/262] Mostly done, but have some questions about the missing attributes in repl.py --- bpython/cli.py | 3 +-- bpython/repl.py | 60 +++++++++++++++++++++++++++---------------------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index c88dac97..f129ba76 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -352,8 +352,7 @@ def notify( def file_prompt(self, s: str) -> Optional[str]: if self.statusbar: - # This thows a mypy error because repl.py isn't typed yet - return self.statusbar.prompt(s) # type:ignore[no-any-return] + return self.statusbar.prompt(s) else: return None diff --git a/bpython/repl.py b/bpython/repl.py index f2e599ed..f13d487a 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -430,7 +430,7 @@ def __init__(self, interp: Interpreter, config: Config): self.funcprops = None self.arg_pos: Union[str, int, None] = None self.current_func = None - self.highlighted_paren = None + self.highlighted_paren: Optional[Tuple[Any, List[Tuple[_TokenType, str]]]] = None self._C: Dict[str, int] = {} self.prev_block_finished: int = 0 self.interact = Interaction(self.config) @@ -649,7 +649,7 @@ def get_source_of_current_name(self) -> str: current name in the current input line. Throw `SourceNotFound` if the source cannot be found.""" - obj = self.current_func + obj: Optional[Callable] = self.current_func try: if obj is None: line = self.current_line @@ -657,7 +657,8 @@ def get_source_of_current_name(self) -> str: raise SourceNotFound(_("Nothing to get source of")) if inspection.is_eval_safe_name(line): obj = self.get_object(line) - return inspect.getsource(obj) + # Ignoring the next mypy error because we want this to fail if obj is None + return inspect.getsource(obj) # type:ignore[arg-type] except (AttributeError, NameError) as e: msg = _("Cannot get source: %s") % (e,) except OSError as e: @@ -724,28 +725,31 @@ def complete(self, tab: bool = False) -> Optional[bool]: self.matches_iter.clear() return bool(self.funcprops) - self.matches_iter.update( - self.cursor_offset, self.current_line, matches, completer - ) + if completer: + self.matches_iter.update( + self.cursor_offset, self.current_line, matches, completer + ) - if len(matches) == 1: - if tab: - # if this complete is being run for a tab key press, substitute - # common sequence - ( - self._cursor_offset, - self._current_line, - ) = self.matches_iter.substitute_cseq() - return Repl.complete(self) # again for - elif self.matches_iter.current_word == matches[0]: - self.matches_iter.clear() - return False - return completer.shown_before_tab + if len(matches) == 1: + if tab: + # if this complete is being run for a tab key press, substitute + # common sequence + ( + self._cursor_offset, + self._current_line, + ) = self.matches_iter.substitute_cseq() + return Repl.complete(self) # again for + elif self.matches_iter.current_word == matches[0]: + self.matches_iter.clear() + return False + return completer.shown_before_tab + else: + return tab or completer.shown_before_tab else: - return tab or completer.shown_before_tab + return False - def format_docstring(self, docstring: str, width: int, height: int) -> str: + def format_docstring(self, docstring: str, width: int, height: int) -> List[str]: """Take a string and try to format it into a sane list of strings to be put into the suggestion box.""" @@ -878,11 +882,13 @@ def pastebin(self, s=None) -> Optional[str]: _("Pastebin buffer? (y/N) ") ): self.interact.notify(_("Pastebin aborted.")) + return None else: return self.do_pastebin(s) def do_pastebin(self, s) -> Optional[str]: """Actually perform the upload.""" + paste_url: str if s == self.prev_pastebin_content: self.interact.notify( _("Duplicate pastebin. Previous URL: %s. " "Removal URL: %s") @@ -896,7 +902,7 @@ def do_pastebin(self, s) -> Optional[str]: paste_url, removal_url = self.paster.paste(s) except PasteFailed as e: self.interact.notify(_("Upload failed: %s") % e) - return + return None self.prev_pastebin_content = s self.prev_pastebin_url = paste_url @@ -923,7 +929,7 @@ def push(self, s, insert_into_history=True) -> bool: if insert_into_history: self.insert_into_history(s) - more = self.interp.runsource("\n".join(self.buffer)) + more: bool = self.interp.runsource("\n".join(self.buffer)) if not more: self.buffer = [] @@ -1028,7 +1034,7 @@ def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: cursor = len(source) - self.cpos if self.cpos: cursor += 1 - stack = list() + stack: List[Any] = list() all_tokens = list(Python3Lexer().get_tokens(source)) # Unfortunately, Pygments adds a trailing newline and strings with # no size, so strip them @@ -1037,8 +1043,8 @@ def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: all_tokens[-1] = (all_tokens[-1][0], all_tokens[-1][1].rstrip("\n")) line = pos = 0 parens = dict(zip("{([", "})]")) - line_tokens = list() - saved_tokens = list() + line_tokens: List[Tuple[_TokenType, str]] = list() + saved_tokens: List[Tuple[_TokenType, str]] = list() search_for_paren = True for (token, value) in split_lines(all_tokens): pos += len(value) @@ -1174,7 +1180,7 @@ def edit_config(self): def next_indentation(line, tab_length) -> int: """Given a code line, return the indentation of the next line.""" line = line.expandtabs(tab_length) - indentation = (len(line) - len(line.lstrip(" "))) // tab_length + indentation: int = (len(line) - len(line.lstrip(" "))) // tab_length if line.rstrip().endswith(":"): indentation += 1 elif indentation >= 1: From 2de72b30131ced90bc11e818fb20725632969269 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Wed, 23 Mar 2022 21:02:28 -0500 Subject: [PATCH 055/262] Ran black and codespell --- bpython/cli.py | 4 ++-- bpython/repl.py | 34 ++++++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index f129ba76..d081e940 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -67,7 +67,7 @@ Union, Tuple, Collection, - Dict + Dict, ) import unicodedata from dataclasses import dataclass @@ -2058,7 +2058,7 @@ def main_curses( exit_value = clirepl.repl() if hasattr(sys, "exitfunc"): - # Seems like the if statment should satisfy mypy, but it doesn't + # Seems like the if statement should satisfy mypy, but it doesn't sys.exitfunc() # type:ignore[attr-defined] delattr(sys, "exitfunc") diff --git a/bpython/repl.py b/bpython/repl.py index f13d487a..31294bde 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -37,7 +37,18 @@ from itertools import takewhile from pathlib import Path from types import ModuleType, TracebackType -from typing import cast, List, Tuple, Any, Optional, Type, Union, MutableMapping, Callable, Dict +from typing import ( + cast, + List, + Tuple, + Any, + Optional, + Type, + Union, + MutableMapping, + Callable, + Dict, +) from ._typing_compat import Literal from pygments.lexers import Python3Lexer @@ -93,7 +104,11 @@ class Interpreter(code.InteractiveInterpreter): bpython_input_re = LazyReCompile(r"") - def __init__(self, locals: Optional[MutableMapping[str, str]] = None, encoding: Optional[str] = None): + def __init__( + self, + locals: Optional[MutableMapping[str, str]] = None, + encoding: Optional[str] = None, + ): """Constructor. The optional 'locals' argument specifies the dictionary in which code @@ -423,14 +438,18 @@ def __init__(self, interp: Interpreter, config: Config): # all input and output, stored as old style format strings # (\x01, \x02, ...) for cli.py self.screen_hist: List[str] = [] - self.history: List[str] = [] # commands executed since beginning of session + self.history: List[ + str + ] = [] # commands executed since beginning of session self.redo_stack: List[str] = [] self.evaluating = False self.matches_iter = MatchesIterator() self.funcprops = None self.arg_pos: Union[str, int, None] = None self.current_func = None - self.highlighted_paren: Optional[Tuple[Any, List[Tuple[_TokenType, str]]]] = None + self.highlighted_paren: Optional[ + Tuple[Any, List[Tuple[_TokenType, str]]] + ] = None self._C: Dict[str, int] = {} self.prev_block_finished: int = 0 self.interact = Interaction(self.config) @@ -447,7 +466,8 @@ def __init__(self, interp: Interpreter, config: Config): if self.config.hist_file.exists(): try: self.rl_history.load( - str(self.config.hist_file), getpreferredencoding() or "ascii" + str(self.config.hist_file), + getpreferredencoding() or "ascii", ) except OSError: pass @@ -749,7 +769,9 @@ def complete(self, tab: bool = False) -> Optional[bool]: else: return False - def format_docstring(self, docstring: str, width: int, height: int) -> List[str]: + def format_docstring( + self, docstring: str, width: int, height: int + ) -> List[str]: """Take a string and try to format it into a sane list of strings to be put into the suggestion box.""" From 7d4f80d6a37519a34cdcc3d591dc0d37280bda21 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 15 Apr 2022 13:14:41 -0400 Subject: [PATCH 056/262] good-enough solutions --- bpython/cli.py | 9 +++++++-- bpython/curtsiesfrontend/repl.py | 9 +++++++-- bpython/repl.py | 32 +++++++++++++++++++++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index d081e940..6c15f5e2 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -1101,7 +1101,12 @@ def print_line( if self.highlighted_paren is not None: # Clear previous highlighted paren - self.reprint_line(*self.highlighted_paren) + + lineno = self.highlighted_paren[0] + tokens = self.highlighted_paren[1] + # mypy thinks tokens is List[Tuple[_TokenType, str]] + # but it is supposed to be MutableMapping[_TokenType, str] + self.reprint_line(lineno, tokens) self.highlighted_paren = None if self.config.syntax and (not self.paste_mode or newline): @@ -1221,7 +1226,7 @@ def repl(self) -> Tuple[Any, ...]: return self.exit_value def reprint_line( - self, lineno: int, tokens: MutableMapping[_TokenType, str] + self, lineno: int, tokens: List[Tuple[_TokenType, str]] ) -> None: """Helper function for paren highlighting: Reprint line at offset `lineno` in current input buffer.""" diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 59dcd481..0d318fd1 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -13,9 +13,12 @@ import unicodedata from enum import Enum from types import TracebackType -from typing import Dict, Any, List, Optional, Tuple, Union, cast, Type +from typing import Dict, Any, List, Optional, Tuple, Union, cast, Type, TYPE_CHECKING from .._typing_compat import Literal +if TYPE_CHECKING: + from ..repl import Interpreter + import blessings import greenlet from curtsies import ( @@ -357,7 +360,9 @@ def __init__( ) self.edit_keys = edit_keys.mapping_with_config(config, key_dispatch) logger.debug("starting parent init") - super().__init__(interp, config) + # interp is a subclass of repl.Interpreter, so it definitely, + # implements the methods of Interpreter! + super().__init__(cast('Interpreter', interp), config) self.formatter = BPythonFormatter(config.color_scheme) diff --git a/bpython/repl.py b/bpython/repl.py index 31294bde..1f4b6fc6 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -48,6 +48,7 @@ MutableMapping, Callable, Dict, + TYPE_CHECKING, ) from ._typing_compat import Literal @@ -61,7 +62,10 @@ have_pyperclip = False from . import autocomplete, inspection, simpleeval -from .cli import Statusbar + +if TYPE_CHECKING: + from .cli import Statusbar + from .config import getpreferredencoding, Config from .formatter import Parenthesis from .history import History @@ -365,7 +369,7 @@ def clear(self) -> None: class Interaction: - def __init__(self, config: Config, statusbar: Optional[Statusbar] = None): + def __init__(self, config: Config, statusbar: Optional['Statusbar'] = None): self.config = config if statusbar: @@ -418,6 +422,29 @@ class Repl: XXX Subclasses should implement echo, current_line, cw """ + @abstractmethod + @property + def current_line(self): + pass + + @abstractmethod + @property + def cursor_offset(self): + pass + + @abstractmethod + def reevaluate(self): + pass + + @abstractmethod + def reprint_line( + self, lineno: int, tokens: List[Tuple[_TokenType, str]] + ) -> None: + pass + + # not actually defined, subclasses must define + cpos: int + def __init__(self, interp: Interpreter, config: Config): """Initialise the repl. @@ -425,7 +452,6 @@ def __init__(self, interp: Interpreter, config: Config): config is a populated bpython.config.Struct. """ - self.config = config self.cut_buffer = "" self.buffer: List[str] = [] From d1891d279879c48b84498bead4e78f8629a45e7e Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 15 Apr 2022 12:23:37 -0500 Subject: [PATCH 057/262] formatted with black --- bpython/curtsiesfrontend/repl.py | 14 ++++++++++++-- bpython/repl.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 0d318fd1..696e84bf 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -13,7 +13,17 @@ import unicodedata from enum import Enum from types import TracebackType -from typing import Dict, Any, List, Optional, Tuple, Union, cast, Type, TYPE_CHECKING +from typing import ( + Dict, + Any, + List, + Optional, + Tuple, + Union, + cast, + Type, + TYPE_CHECKING, +) from .._typing_compat import Literal if TYPE_CHECKING: @@ -362,7 +372,7 @@ def __init__( logger.debug("starting parent init") # interp is a subclass of repl.Interpreter, so it definitely, # implements the methods of Interpreter! - super().__init__(cast('Interpreter', interp), config) + super().__init__(cast("Interpreter", interp), config) self.formatter = BPythonFormatter(config.color_scheme) diff --git a/bpython/repl.py b/bpython/repl.py index 1f4b6fc6..19062a1e 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -369,7 +369,7 @@ def clear(self) -> None: class Interaction: - def __init__(self, config: Config, statusbar: Optional['Statusbar'] = None): + def __init__(self, config: Config, statusbar: Optional["Statusbar"] = None): self.config = config if statusbar: From 1745ad5e2845751ad7fddd4b8327cfff1c015fc8 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 15 Apr 2022 12:39:57 -0500 Subject: [PATCH 058/262] hide properties under type checking --- bpython/repl.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 19062a1e..bf9ee82c 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -422,28 +422,29 @@ class Repl: XXX Subclasses should implement echo, current_line, cw """ - @abstractmethod - @property - def current_line(self): - pass + if TYPE_CHECKING: + @property + @abstractmethod + def current_line(self): + pass - @abstractmethod - @property - def cursor_offset(self): - pass + @property + @abstractmethod + def cursor_offset(self): + pass - @abstractmethod - def reevaluate(self): - pass + @abstractmethod + def reevaluate(self): + pass - @abstractmethod - def reprint_line( - self, lineno: int, tokens: List[Tuple[_TokenType, str]] - ) -> None: - pass + @abstractmethod + def reprint_line( + self, lineno: int, tokens: List[Tuple[_TokenType, str]] + ) -> None: + pass - # not actually defined, subclasses must define - cpos: int + # not actually defined, subclasses must define + cpos: int def __init__(self, interp: Interpreter, config: Config): """Initialise the repl. From a832ea35de4de1e00b8de6903b722c27df1b3def Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 15 Apr 2022 16:58:44 -0500 Subject: [PATCH 059/262] Found and fixed the mock error, still need to address the matches_iter error --- bpython/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 6c15f5e2..b1bff126 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -68,6 +68,7 @@ Tuple, Collection, Dict, + Literal, ) import unicodedata from dataclasses import dataclass @@ -1330,7 +1331,7 @@ def show_list( arg_pos: Union[str, int, None], topline: Any = None, # Named tuples don't play nice with mypy formatter: Optional[Callable] = None, - current_item: Optional[str] = None, + current_item: Union[str, Literal[False]] = None, ) -> None: v_items: Collection shared = ShowListState() @@ -1527,9 +1528,8 @@ def tab(self, back: bool = False) -> bool: # 4. swap current word for a match list item elif self.matches_iter.matches: - n: str = next(self.matches_iter) - current_match: Optional[str] = ( - back and self.matches_iter.previous() or n + current_match: Union[str, Literal[False]] = ( + back and self.matches_iter.previous() or next(self.matches_iter) ) try: self.show_list( From 6f4b3c2e5a6f8eb0659d44e9f31578147d975dc7 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 15 Apr 2022 20:18:02 -0500 Subject: [PATCH 060/262] Fixed pytest errors --- bpython/cli.py | 16 ++++++++-------- bpython/repl.py | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index b1bff126..db090cca 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -514,12 +514,12 @@ def complete(self, tab: bool = False) -> None: list_win_visible = repl.Repl.complete(self, tab) - f = None - if self.matches_iter.completer: - f = self.matches_iter.completer.format - if list_win_visible: try: + f = None + if self.matches_iter.completer: + f = self.matches_iter.completer.format + self.show_list( self.matches_iter.matches, self.arg_pos, @@ -1494,10 +1494,6 @@ def tab(self, back: bool = False) -> bool: and don't indent if there are only whitespace in the line. """ - f = None - if self.matches_iter.completer: - f = self.matches_iter.completer.format - # 1. check if we should add a tab character if self.atbol() and not back: x_pos = len(self.s) - self.cpos @@ -1532,6 +1528,10 @@ def tab(self, back: bool = False) -> bool: back and self.matches_iter.previous() or next(self.matches_iter) ) try: + f = None + if self.matches_iter.completer: + f = self.matches_iter.completer.format + self.show_list( self.matches_iter.matches, self.arg_pos, diff --git a/bpython/repl.py b/bpython/repl.py index bf9ee82c..2da6a9a1 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -423,6 +423,7 @@ class Repl: """ if TYPE_CHECKING: + @property @abstractmethod def current_line(self): From bcd3c31a22ee1e73d89c9f1117b14ec979312170 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 15 Apr 2022 20:25:57 -0500 Subject: [PATCH 061/262] Imported Literal using _typing_compat instead of typing --- bpython/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/cli.py b/bpython/cli.py index db090cca..4f9b50c8 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -68,8 +68,8 @@ Tuple, Collection, Dict, - Literal, ) +from ._typing_compat import Literal import unicodedata from dataclasses import dataclass From d8d68d20018a55c8062e8d5a0468a2e6c0980fd0 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Mon, 18 Apr 2022 12:16:59 -0500 Subject: [PATCH 062/262] Changed type hinting on curses windows to be compatible with python 3.7 --- bpython/cli.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 4f9b50c8..8bace509 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -68,7 +68,12 @@ Tuple, Collection, Dict, + TYPE_CHECKING, ) + +if TYPE_CHECKING: + from _curses import _CursesWindow + from ._typing_compat import Literal import unicodedata from dataclasses import dataclass @@ -361,7 +366,7 @@ def file_prompt(self, s: str) -> Optional[str]: class CLIRepl(repl.Repl): def __init__( self, - scr: curses.window, + scr: "_CursesWindow", interp: repl.Interpreter, statusbar: "Statusbar", config: Config, @@ -371,7 +376,7 @@ def __init__( # mypy doesn't quite understand the difference between a class variable with a callable type and a method. # https://github.com/python/mypy/issues/2427 self.interp.writetb = self.writetb # type:ignore[assignment] - self.scr: curses.window = scr + self.scr: "_CursesWindow" = scr self.stdout_hist = "" # native str (bytes in Py2, unicode in Py3) self.list_win = newwin(get_colpair(config, "background"), 1, 1, 1, 1) self.cpos = 0 @@ -1631,8 +1636,8 @@ class Statusbar: def __init__( self, - scr: curses.window, - pwin: curses.window, + scr: "_CursesWindow", + pwin: "_CursesWindow", background: int, config: Config, s: Optional[str] = None, @@ -1640,7 +1645,7 @@ def __init__( ): """Initialise the statusbar and display the initial text (if any)""" self.size() - self.win: curses.window = newwin( + self.win: "_CursesWindow" = newwin( background, self.h, self.w, self.y, self.x ) @@ -1767,8 +1772,8 @@ def clear(self) -> None: def init_wins( - scr: curses.window, config: Config -) -> Tuple[curses.window, Statusbar]: + scr: "_CursesWindow", config: Config +) -> Tuple["_CursesWindow", Statusbar]: """Initialise the two windows (the main repl interface and the little status bar at the bottom with some stuff in it)""" # TODO: Document better what stuff is on the status bar. @@ -1803,12 +1808,12 @@ def init_wins( return main_win, statusbar -def sigwinch(unused_scr: curses.window) -> None: +def sigwinch(unused_scr: "_CursesWindow") -> None: global DO_RESIZE DO_RESIZE = True -def sigcont(unused_scr: curses.window) -> None: +def sigcont(unused_scr: "_CursesWindow") -> None: sigwinch(unused_scr) # Forces the redraw curses.ungetch("\x00") @@ -1933,7 +1938,7 @@ def __getitem__(self, k: Any) -> int: return self._val -def newwin(background: int, *args: int) -> curses.window: +def newwin(background: int, *args: int) -> "_CursesWindow": """Wrapper for curses.newwin to automatically set background colour on any newly created window.""" win = curses.newwin(*args) @@ -1967,7 +1972,7 @@ def curses_wrapper(func: Callable, *args: Any, **kwargs: Any) -> Any: def main_curses( - scr: curses.window, + scr: "_CursesWindow", args: List[str], config: Config, interactive: bool = True, From 9e0feb33fc5c1626ba19dbfba552bb669203c548 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 3 May 2022 00:32:15 +0200 Subject: [PATCH 063/262] Fix some type annotatations --- bpython/curtsies.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 7ffe9010..82adc15f 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -55,9 +55,9 @@ class FullCurtsiesRepl(BaseRepl): def __init__( self, config: Config, - locals_: Optional[Dict[str, Any]], - banner: Optional[str], - interp: code.InteractiveInterpreter = None, + locals_: Optional[Dict[str, Any]] = None, + banner: Optional[str] = None, + interp: Optional[Interp] = None, ) -> None: self.input_generator = curtsies.input.Input( keynames="curtsies", sigint_event=True, paste_threshold=None @@ -182,10 +182,10 @@ def mainloop( def main( - args: List[str] = None, - locals_: Dict[str, Any] = None, - banner: str = None, - welcome_message: str = None, + args: Optional[List[str]] = None, + locals_: Optional[Dict[str, Any]] = None, + banner: Optional[str] = None, + welcome_message: Optional[str] = None, ) -> Any: """ banner is displayed directly after the version information. From cca075a6346fda5177146ca6dbbbcde0a29ea3a9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 3 May 2022 00:42:06 +0200 Subject: [PATCH 064/262] Remove unused function --- bpython/cli.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 8bace509..21d79ba3 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -123,24 +123,6 @@ class ShowListState: wl: int = 0 -def calculate_screen_lines( - tokens: MutableMapping[_TokenType, str], width: int, cursor: int = 0 -) -> int: - """Given a stream of tokens and a screen width plus an optional - initial cursor position, return the amount of needed lines on the - screen.""" - lines = 1 - pos = cursor - for (token, value) in tokens: - if token is Token.Text and value == "\n": - lines += 1 - else: - pos += len(value) - lines += pos // width - pos %= width - return lines - - def forward_if_not_current(func: F) -> F: @functools.wraps(func) def newfunc(self, *args, **kwargs): # type: ignore From 069a0a655facba02b74362da2684a2504d1d7fa6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 3 May 2022 00:42:18 +0200 Subject: [PATCH 065/262] Set to empty string if no removal URL is available --- bpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/repl.py b/bpython/repl.py index 2da6a9a1..a74fa206 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -956,7 +956,7 @@ def do_pastebin(self, s) -> Optional[str]: self.prev_pastebin_content = s self.prev_pastebin_url = paste_url - self.prev_removal_url = removal_url + self.prev_removal_url = removal_url if removal_url is not None else "" if removal_url is not None: self.interact.notify( From a808521ad8301c237bde708d161d32daa49a7ec9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 00:11:32 +0200 Subject: [PATCH 066/262] Remove no longer used encoding --- bpython/cli.py | 2 +- bpython/curtsiesfrontend/interpreter.py | 3 +- bpython/repl.py | 44 +++++++------------------ bpython/test/test_interpreter.py | 11 ++----- bpython/urwid.py | 2 +- 5 files changed, 16 insertions(+), 46 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 21d79ba3..3291be09 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -2005,7 +2005,7 @@ def main_curses( curses.raw(True) main_win, statusbar = init_wins(scr, config) - interpreter = repl.Interpreter(locals_, getpreferredencoding()) + interpreter = repl.Interpreter(locals_) clirepl = CLIRepl(main_win, interpreter, statusbar, config, idle) clirepl._C = cols diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 91dba96a..1b313345 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -63,14 +63,13 @@ class Interp(ReplInterpreter): def __init__( self, locals: Optional[Dict[str, Any]] = None, - encoding: Optional[str] = None, ) -> None: """Constructor. We include an argument for the outfile to pass to the formatter for it to write to. """ - super().__init__(locals, encoding) + super().__init__(locals) # typically changed after being instantiated # but used when interpreter used corresponding REPL diff --git a/bpython/repl.py b/bpython/repl.py index a74fa206..1f318f8f 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -110,9 +110,8 @@ class Interpreter(code.InteractiveInterpreter): def __init__( self, - locals: Optional[MutableMapping[str, str]] = None, - encoding: Optional[str] = None, - ): + locals: Optional[MutableMapping[str, Any]] = None, + ) -> None: """Constructor. The optional 'locals' argument specifies the dictionary in which code @@ -126,14 +125,8 @@ def __init__( callback can be added to the Interpreter instance afterwards - more specifically, this is so that autoindentation does not occur after a traceback. - - encoding is only used in Python 2, where it may be necessary to add an - encoding comment to a source bytestring before running it. - encoding must be a bytestring in Python 2 because it will be templated - into a bytestring source as part of an encoding comment. """ - self.encoding = encoding or getpreferredencoding() self.syntaxerror_callback: Optional[Callable] = None if locals is None: @@ -145,36 +138,21 @@ def __init__( super().__init__(locals) self.timer = RuntimeTimer() - def runsource(self, source, filename=None, symbol="single", encode="auto"): + def runsource( + self, + source: str, + filename: Optional[str] = None, + symbol: str = "single", + ) -> bool: """Execute Python code. source, filename and symbol are passed on to - code.InteractiveInterpreter.runsource. If encode is True, - an encoding comment will be added to the source. - On Python 3.X, encode will be ignored. - - encode should only be used for interactive interpreter input, - files should always already have an encoding comment or be ASCII. - By default an encoding line will be added if no filename is given. - - source must be a string - - Because adding an encoding comment to a unicode string in Python 2 - would cause a syntax error to be thrown which would reference code - the user did not write, setting encoding to True when source is a - unicode string in Python 2 will throw a ValueError.""" - if encode and filename is not None: - # files have encoding comments or implicit encoding of ASCII - if encode != "auto": - raise ValueError("shouldn't add encoding line to file contents") - encode = False + code.InteractiveInterpreter.runsource.""" if filename is None: filename = filename_for_console_input(source) with self.timer: - return code.InteractiveInterpreter.runsource( - self, source, filename, symbol - ) + return super().runsource(source, filename, symbol) def showsyntaxerror(self, filename=None): """Override the regular handler, the code's copied and pasted from @@ -532,7 +510,7 @@ def startup(self) -> None: encoding = inspection.get_encoding_file(filename) with open(filename, encoding=encoding) as f: source = f.read() - self.interp.runsource(source, filename, "exec", encode=False) + self.interp.runsource(source, filename, "exec") def current_string(self, concatenate=False): """If the line ends in a string get it, otherwise return ''""" diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 4c18f8fd..281ad5fa 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -38,7 +38,7 @@ def test_syntaxerror(self): if (3, 10, 1) <= sys.version_info[:3]: expected = ( " File " - + green('""') + + green('""') + ", line " + bold(magenta("1")) + "\n 1.1.1.1\n ^^\n" @@ -50,7 +50,7 @@ def test_syntaxerror(self): elif (3, 10) <= sys.version_info[:2]: expected = ( " File " - + green('""') + + green('""') + ", line " + bold(magenta("1")) + "\n 1.1.1.1\n ^^^^^\n" @@ -129,13 +129,6 @@ def gfunc(): self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) self.assertEqual(plain("").join(a), expected) - def test_runsource_bytes_over_128_syntax_error_py3(self): - i = interpreter.Interp(encoding="latin-1") - i.showsyntaxerror = mock.Mock(return_value=None) - - i.runsource("a = b'\xfe'") - i.showsyntaxerror.assert_called_with(mock.ANY) - def test_getsource_works_on_interactively_defined_functions(self): source = "def foo(x):\n return x + 1\n" i = interpreter.Interp() diff --git a/bpython/urwid.py b/bpython/urwid.py index 33d1f4b8..ec7038e8 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -1243,7 +1243,7 @@ def options_callback(group): extend_locals["service"] = serv reactor.callWhenRunning(serv.startService) exec_args = [] - interpreter = repl.Interpreter(locals_, locale.getpreferredencoding()) + interpreter = repl.Interpreter(locals_) # TODO: replace with something less hack-ish interpreter.locals.update(extend_locals) From 8caa915e2f0f7e96b5945c01443e418e1fb3494e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 00:27:08 +0200 Subject: [PATCH 067/262] Make Interaction an abstract base class --- bpython/cli.py | 18 +++++------ bpython/curtsiesfrontend/interaction.py | 4 +-- bpython/repl.py | 41 ++++++++++++++++++------- bpython/urwid.py | 7 ++++- 4 files changed, 45 insertions(+), 25 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 3291be09..5d8b0ed4 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -319,14 +319,14 @@ def make_colors(config: Config) -> Dict[str, int]: class CLIInteraction(repl.Interaction): - def __init__(self, config: Config, statusbar: Optional["Statusbar"] = None): - super().__init__(config, statusbar) + def __init__(self, config: Config, statusbar: "Statusbar"): + super().__init__(config) + self.statusbar = statusbar def confirm(self, q: str) -> bool: """Ask for yes or no and return boolean""" try: - if self.statusbar: - reply = self.statusbar.prompt(q) + reply = self.statusbar.prompt(q) except ValueError: return False @@ -335,14 +335,10 @@ def confirm(self, q: str) -> bool: def notify( self, s: str, n: int = 10, wait_for_keypress: bool = False ) -> None: - if self.statusbar: - self.statusbar.message(s, n) + self.statusbar.message(s, n) def file_prompt(self, s: str) -> Optional[str]: - if self.statusbar: - return self.statusbar.prompt(s) - else: - return None + return self.statusbar.prompt(s) class CLIRepl(repl.Repl): @@ -1675,7 +1671,7 @@ def check(self) -> None: self.settext(self._s) - def message(self, s: str, n: int = 3) -> None: + def message(self, s: str, n: float = 3.0) -> None: """Display a message for a short n seconds on the statusbar and return it to its original state.""" self.timer = int(time.time() + n) diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index 79622d14..17b178de 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -39,7 +39,7 @@ def __init__( self.prompt = "" self._message = "" self.message_start_time = time.time() - self.message_time = 3 + self.message_time = 3.0 self.permanent_stack = [] if permanent_text: self.permanent_stack.append(permanent_text) @@ -149,7 +149,7 @@ def should_show_message(self): return bool(self.current_line) # interaction interface - should be called from other greenlets - def notify(self, msg, n=3, wait_for_keypress=False): + def notify(self, msg, n=3.0, wait_for_keypress=False): self.request_context = greenlet.getcurrent() self.message_time = n self.message(msg, schedule_refresh=wait_for_keypress) diff --git a/bpython/repl.py b/bpython/repl.py index 1f318f8f..f5c21def 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -21,6 +21,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import abc import code import inspect import os @@ -346,21 +347,39 @@ def clear(self) -> None: self.index = -1 -class Interaction: - def __init__(self, config: Config, statusbar: Optional["Statusbar"] = None): +class Interaction(metaclass=abc.ABCMeta): + def __init__(self, config: Config): self.config = config - if statusbar: - self.statusbar = statusbar + @abc.abstractmethod + def confirm(self, s: str) -> bool: + pass - def confirm(self, s): - raise NotImplementedError + @abc.abstractmethod + def notify( + self, s: str, n: float = 10.0, wait_for_keypress: bool = False + ) -> None: + pass + + @abc.abstractmethod + def file_prompt(self, s: str) -> Optional[str]: + pass + + +class NoInteraction(Interaction): + def __init__(self, config: Config): + super().__init__(config) - def notify(self, s, n=10, wait_for_keypress=False): - raise NotImplementedError + def confirm(self, s: str) -> bool: + return False + + def notify( + self, s: str, n: float = 10.0, wait_for_keypress: bool = False + ) -> None: + pass - def file_prompt(self, s): - raise NotImplementedError + def file_prompt(self, s: str) -> Optional[str]: + return None class SourceNotFound(Exception): @@ -458,7 +477,7 @@ def __init__(self, interp: Interpreter, config: Config): ] = None self._C: Dict[str, int] = {} self.prev_block_finished: int = 0 - self.interact = Interaction(self.config) + self.interact: Interaction = NoInteraction(self.config) # previous pastebin content to prevent duplicate pastes, filled on call # to repl.pastebin self.prev_pastebin_content = "" diff --git a/bpython/urwid.py b/bpython/urwid.py index ec7038e8..0a2ffe31 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -38,6 +38,7 @@ import locale import signal import urwid +from typing import Optional from . import args as bpargs, repl, translations from .formatter import theme_map @@ -526,7 +527,8 @@ def render(self, size, focus=False): class URWIDInteraction(repl.Interaction): def __init__(self, config, statusbar, frame): - super().__init__(config, statusbar) + super().__init__(config) + self.statusbar = statusbar self.frame = frame urwid.connect_signal(statusbar, "prompt_result", self._prompt_result) self.callback = None @@ -563,6 +565,9 @@ def _prompt_result(self, text): self.callback = None callback(text) + def file_prompt(self, s: str) -> Optional[str]: + raise NotImplementedError + class URWIDRepl(repl.Repl): From deb757d880791d56421216991cc949e40511b669 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 00:27:53 +0200 Subject: [PATCH 068/262] Add more type annotations and fix some mypy errors --- bpython/cli.py | 2 +- bpython/repl.py | 52 ++++++++++++++++++++++++------------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 5d8b0ed4..559b63e7 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -333,7 +333,7 @@ def confirm(self, q: str) -> bool: return reply.lower() in (_("y"), _("yes")) def notify( - self, s: str, n: int = 10, wait_for_keypress: bool = False + self, s: str, n: float = 10.0, wait_for_keypress: bool = False ) -> None: self.statusbar.message(s, n) diff --git a/bpython/repl.py b/bpython/repl.py index f5c21def..8294f2c8 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -39,6 +39,7 @@ from pathlib import Path from types import ModuleType, TracebackType from typing import ( + Iterable, cast, List, Tuple, @@ -155,7 +156,7 @@ def runsource( with self.timer: return super().runsource(source, filename, symbol) - def showsyntaxerror(self, filename=None): + def showsyntaxerror(self, filename: Optional[str] = None) -> None: """Override the regular handler, the code's copied and pasted from code.py, as per showtraceback, but with the syntaxerror callback called and the text in a pretty colour.""" @@ -182,7 +183,7 @@ def showsyntaxerror(self, filename=None): exc_formatted = traceback.format_exception_only(exc_type, value) self.writetb(exc_formatted) - def showtraceback(self): + def showtraceback(self) -> None: """This needs to override the default traceback thing so it can put it into a pretty colour and maybe other stuff, I don't know""" @@ -194,11 +195,10 @@ def showtraceback(self): tblist = traceback.extract_tb(tb) del tblist[:1] - for i, (fname, lineno, module, something) in enumerate(tblist): - # strip linecache line number - if self.bpython_input_re.match(fname): - fname = "" - tblist[i] = (fname, lineno, module, something) + for frame in tblist: + if self.bpython_input_re.match(frame.filename): + # strip linecache line number + frame.filename = "" l = traceback.format_list(tblist) if l: @@ -209,7 +209,7 @@ def showtraceback(self): self.writetb(l) - def writetb(self, lines): + def writetb(self, lines: Iterable[str]) -> None: """This outputs the traceback and should be overridden for anything fancy.""" for line in lines: @@ -463,9 +463,8 @@ def __init__(self, interp: Interpreter, config: Config): # all input and output, stored as old style format strings # (\x01, \x02, ...) for cli.py self.screen_hist: List[str] = [] - self.history: List[ - str - ] = [] # commands executed since beginning of session + # commands executed since beginning of session + self.history: List[str] = [] self.redo_stack: List[str] = [] self.evaluating = False self.matches_iter = MatchesIterator() @@ -870,25 +869,22 @@ def write2file(self) -> None: self.interact.notify(_("Save cancelled.")) return - fn = Path(fn).expanduser() - if fn.suffix != ".py" and self.config.save_append_py: + path = Path(fn).expanduser() + if path.suffix != ".py" and self.config.save_append_py: # fn.with_suffix(".py") does not append if fn has a non-empty suffix - fn = Path(f"{fn}.py") + path = Path(f"{path}.py") mode = "w" - if fn.exists(): - mode = self.interact.file_prompt( + if path.exists(): + new_mode = self.interact.file_prompt( _( - "%s already exists. Do you " - "want to (c)ancel, " - " (o)verwrite or " - "(a)ppend? " + "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " ) - % (fn,) + % (path,) ) - if mode in ("o", "overwrite", _("overwrite")): + if new_mode in ("o", "overwrite", _("overwrite")): mode = "w" - elif mode in ("a", "append", _("append")): + elif new_mode in ("a", "append", _("append")): mode = "a" else: self.interact.notify(_("Save cancelled.")) @@ -897,12 +893,12 @@ def write2file(self) -> None: stdout_text = self.get_session_formatted_for_file() try: - with open(fn, mode) as f: + with open(path, mode) as f: f.write(stdout_text) except OSError as e: - self.interact.notify(_("Error writing file '%s': %s") % (fn, e)) + self.interact.notify(_("Error writing file '%s': %s") % (path, e)) else: - self.interact.notify(_("Saved to %s.") % (fn,)) + self.interact.notify(_("Saved to %s.") % (path,)) def copy2clipboard(self) -> None: """Copy current content to clipboard.""" @@ -1003,6 +999,10 @@ def prompt_undo(self) -> int: _("Undo how many lines? (Undo will take up to ~%.1f seconds) [1]") % (est,) ) + if m is None: + self.interact.notify(_("Undo canceled"), 0.1) + return 0 + try: if m == "": m = "1" From 59a8095a962cebe9a17783e872ca15a19a326fe0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 01:05:55 +0200 Subject: [PATCH 069/262] Handle end_lineno and end_offset in SyntaxError --- bpython/repl.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 8294f2c8..d3b64676 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -166,20 +166,21 @@ def showsyntaxerror(self, filename: Optional[str] = None) -> None: exc_type, value, sys.last_traceback = sys.exc_info() sys.last_type = exc_type sys.last_value = value - if filename and exc_type is SyntaxError: - # Work hard to stuff the correct filename in the exception - try: - msg, (dummy_filename, lineno, offset, line) = value.args - except: - # Not the format we expect; leave it alone - pass - else: - # Stuff in the right filename and right lineno - # strip linecache line number - if self.bpython_input_re.match(filename): - filename = "" - value = SyntaxError(msg, (filename, lineno, offset, line)) - sys.last_value = value + if ( + filename + and exc_type is SyntaxError + and value is not None + and len(value.args) >= 4 + ): + msg = str(value) + lineno = value.args[1] + offset = value.args[2] + line = value.args[3] + # strip linechache line number + if self.bpython_input_re.match(filename): + filename = "" + value = SyntaxError(msg, (filename, lineno, offset, line)) + sys.last_value = value exc_formatted = traceback.format_exception_only(exc_type, value) self.writetb(exc_formatted) From 937626dd46abcc50cd8192e22c8185955a5bf780 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 01:25:42 +0200 Subject: [PATCH 070/262] Improve handling of SyntaxErrors is now detected and replaced with in more cases. --- bpython/repl.py | 17 +++++------------ bpython/test/test_interpreter.py | 4 ++-- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index d3b64676..fda79928 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -166,20 +166,13 @@ def showsyntaxerror(self, filename: Optional[str] = None) -> None: exc_type, value, sys.last_traceback = sys.exc_info() sys.last_type = exc_type sys.last_value = value - if ( - filename - and exc_type is SyntaxError - and value is not None - and len(value.args) >= 4 - ): - msg = str(value) - lineno = value.args[1] - offset = value.args[2] - line = value.args[3] + if filename and exc_type is SyntaxError and value is not None: + msg = value.args[0] + args = list(value.args[1]) # strip linechache line number if self.bpython_input_re.match(filename): - filename = "" - value = SyntaxError(msg, (filename, lineno, offset, line)) + args[0] = "" + value = SyntaxError(msg, tuple(args)) sys.last_value = value exc_formatted = traceback.format_exception_only(exc_type, value) self.writetb(exc_formatted) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 281ad5fa..65b60a92 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -38,7 +38,7 @@ def test_syntaxerror(self): if (3, 10, 1) <= sys.version_info[:3]: expected = ( " File " - + green('""') + + green('""') + ", line " + bold(magenta("1")) + "\n 1.1.1.1\n ^^\n" @@ -50,7 +50,7 @@ def test_syntaxerror(self): elif (3, 10) <= sys.version_info[:2]: expected = ( " File " - + green('""') + + green('""') + ", line " + bold(magenta("1")) + "\n 1.1.1.1\n ^^^^^\n" From cec06780ef158a14fb3fc6b372c30717821cd6cc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 01:25:51 +0200 Subject: [PATCH 071/262] GA: do not fail fast --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 62ebac0f..89edab7c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,6 +12,7 @@ jobs: runs-on: ubuntu-latest continue-on-error: ${{ matrix.python-version == 'pypy-3.7' }} strategy: + fail-fast: false matrix: python-version: [3.7, 3.8, 3.9, "3.10", "pypy-3.7"] steps: From 0ada13d6645d80c18399c738d01e75380b6c431f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 09:52:41 +0200 Subject: [PATCH 072/262] Provide more type annotations --- bpython/repl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index fda79928..41c28d5a 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -417,12 +417,12 @@ class Repl: @property @abstractmethod - def current_line(self): + def current_line(self) -> str: pass @property @abstractmethod - def cursor_offset(self): + def cursor_offset(self) -> int: pass @abstractmethod From e906df7c2c437970b95fad75dca925cb41895f0d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 10:31:42 +0200 Subject: [PATCH 073/262] Refactor _funcname_and_argnum to avoid Exception-based control flow --- bpython/repl.py | 86 ++++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 41c28d5a..6e827822 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -35,6 +35,7 @@ import time import traceback from abc import abstractmethod +from dataclasses import dataclass from itertools import takewhile from pathlib import Path from types import ModuleType, TracebackType @@ -380,6 +381,17 @@ class SourceNotFound(Exception): """Exception raised when the requested source could not be found.""" +@dataclass +class _FuncExpr: + """Stack element in Repl._funcname_and_argnum""" + + full_expr: str + function_expr: str + arg_number: int + opening: str + keyword: Optional[str] = None + + class Repl: """Implements the necessary guff for a Python-repl-alike interface @@ -564,37 +576,37 @@ def get_object(self, name): return obj @classmethod - def _funcname_and_argnum(cls, line): + def _funcname_and_argnum( + cls, line: str + ) -> Tuple[Optional[str], Optional[Union[str, int]]]: """Parse out the current function name and arg from a line of code.""" - # each list in stack: - # [full_expr, function_expr, arg_number, opening] - # arg_number may be a string if we've encountered a keyword - # argument so we're done counting - stack = [["", "", 0, ""]] + # each element in stack is a _FuncExpr instance + # if keyword is not None, we've encountered a keyword and so we're done counting + stack = [_FuncExpr("", "", 0, "")] try: for (token, value) in Python3Lexer().get_tokens(line): if token is Token.Punctuation: if value in "([{": - stack.append(["", "", 0, value]) + stack.append(_FuncExpr("", "", 0, value)) elif value in ")]}": - full, _, _, start = stack.pop() - expr = start + full + value - stack[-1][1] += expr - stack[-1][0] += expr + element = stack.pop() + expr = element.opening + element.full_expr + value + stack[-1].function_expr += expr + stack[-1].full_expr += expr elif value == ",": - try: - stack[-1][2] += 1 - except TypeError: - stack[-1][2] = "" - stack[-1][1] = "" - stack[-1][0] += value - elif value == ":" and stack[-1][3] == "lambda": - expr = stack.pop()[0] + ":" - stack[-1][1] += expr - stack[-1][0] += expr + if stack[-1].keyword is None: + stack[-1].arg_number += 1 + else: + stack[-1].keyword = "" + stack[-1].function_expr = "" + stack[-1].full_expr += value + elif value == ":" and stack[-1].opening == "lambda": + expr = stack.pop().full_expr + ":" + stack[-1].function_expr += expr + stack[-1].full_expr += expr else: - stack[-1][1] = "" - stack[-1][0] += value + stack[-1].function_expr = "" + stack[-1].full_expr += value elif ( token is Token.Number or token in Token.Number.subtypes @@ -603,25 +615,25 @@ def _funcname_and_argnum(cls, line): or token is Token.Operator and value == "." ): - stack[-1][1] += value - stack[-1][0] += value + stack[-1].function_expr += value + stack[-1].full_expr += value elif token is Token.Operator and value == "=": - stack[-1][2] = stack[-1][1] - stack[-1][1] = "" - stack[-1][0] += value + stack[-1].keyword = stack[-1].function_expr + stack[-1].function_expr = "" + stack[-1].full_expr += value elif token is Token.Number or token in Token.Number.subtypes: - stack[-1][1] = value - stack[-1][0] += value + stack[-1].function_expr = value + stack[-1].full_expr += value elif token is Token.Keyword and value == "lambda": - stack.append([value, "", 0, value]) + stack.append(_FuncExpr(value, "", 0, value)) else: - stack[-1][1] = "" - stack[-1][0] += value - while stack[-1][3] in "[{": + stack[-1].function_expr = "" + stack[-1].full_expr += value + while stack[-1].opening in "[{": stack.pop() - _, _, arg_number, _ = stack.pop() - _, func, _, _ = stack.pop() - return func, arg_number + elem1 = stack.pop() + elem2 = stack.pop() + return elem2.function_expr, elem1.keyword or elem1.arg_number except IndexError: return None, None From b86de33c17413b6fa7cbb0597bd491605dc85412 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 10:59:00 +0200 Subject: [PATCH 074/262] Make implementations of current_line and cursor_offset consistent --- bpython/cli.py | 14 -------- bpython/curtsiesfrontend/repl.py | 18 +++------- bpython/repl.py | 60 ++++++++++++++++++++++---------- bpython/test/test_repl.py | 30 ++++++++++++++-- 4 files changed, 73 insertions(+), 49 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 559b63e7..5383a7c3 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -383,13 +383,6 @@ def _get_cursor_offset(self) -> int: def _set_cursor_offset(self, offset: int) -> None: self.cpos = len(self.s) - offset - cursor_offset = property( - _get_cursor_offset, - _set_cursor_offset, - None, - "The cursor offset from the beginning of the line", - ) - def addstr(self, s: str) -> None: """Add a string to the current input line and figure out where it should go, depending on the cursor position.""" @@ -539,13 +532,6 @@ def _get_current_line(self) -> str: def _set_current_line(self, line: str) -> None: self.s = line - current_line = property( - _get_current_line, - _set_current_line, - None, - "The characters of the current line", - ) - def cut_to_buffer(self) -> None: """Clear from cursor to end of line, placing into cut buffer""" self.cut_buffer = self.s[-self.cpos :] diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 696e84bf..b8c7f016 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1866,18 +1866,13 @@ def __repr__(self): lines scrolled down: {self.scroll_offset} >""" - @property - def current_line(self): + def _get_current_line(self) -> str: """The current line""" return self._current_line - @current_line.setter - def current_line(self, value): - self._set_current_line(value) - def _set_current_line( self, - line, + line: str, update_completion=True, reset_rl_history=True, clear_special_mode=True, @@ -1895,18 +1890,13 @@ def _set_current_line( self.special_mode = None self.unhighlight_paren() - @property - def cursor_offset(self): + def _get_cursor_offset(self) -> int: """The current cursor offset from the front of the "line".""" return self._cursor_offset - @cursor_offset.setter - def cursor_offset(self, value): - self._set_cursor_offset(value) - def _set_cursor_offset( self, - offset, + offset: int, update_completion=True, reset_rl_history=False, clear_special_mode=True, diff --git a/bpython/repl.py b/bpython/repl.py index 6e827822..6420a178 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -392,7 +392,7 @@ class _FuncExpr: keyword: Optional[str] = None -class Repl: +class Repl(metaclass=abc.ABCMeta): """Implements the necessary guff for a Python-repl-alike interface The execution of the code entered and all that stuff was taken from the @@ -425,27 +425,51 @@ class Repl: XXX Subclasses should implement echo, current_line, cw """ - if TYPE_CHECKING: + @abc.abstractmethod + def reevaluate(self): + pass - @property - @abstractmethod - def current_line(self) -> str: - pass + @abc.abstractmethod + def reprint_line( + self, lineno: int, tokens: List[Tuple[_TokenType, str]] + ) -> None: + pass - @property - @abstractmethod - def cursor_offset(self) -> int: - pass + @abc.abstractmethod + def _get_current_line(self) -> str: + pass - @abstractmethod - def reevaluate(self): - pass + @abc.abstractmethod + def _set_current_line(self, val: str) -> None: + pass - @abstractmethod - def reprint_line( - self, lineno: int, tokens: List[Tuple[_TokenType, str]] - ) -> None: - pass + @property + def current_line(self) -> str: + """The current line""" + return self._get_current_line() + + @current_line.setter + def current_line(self, value: str) -> None: + self._set_current_line(value) + + @abc.abstractmethod + def _get_cursor_offset(self) -> int: + pass + + @abc.abstractmethod + def _set_cursor_offset(self, val: int) -> None: + pass + + @property + def cursor_offset(self) -> int: + """The current cursor offset from the front of the "line".""" + return self._get_cursor_offset() + + @cursor_offset.setter + def cursor_offset(self, value: int) -> None: + self._set_cursor_offset(value) + + if TYPE_CHECKING: # not actually defined, subclasses must define cpos: int diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index a4241087..63309364 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -5,6 +5,7 @@ import tempfile import unittest +from typing import List, Tuple from itertools import islice from pathlib import Path from unittest import mock @@ -38,9 +39,32 @@ def reset(self): class FakeRepl(repl.Repl): def __init__(self, conf=None): - repl.Repl.__init__(self, repl.Interpreter(), setup_config(conf)) - self.current_line = "" - self.cursor_offset = 0 + super().__init__(repl.Interpreter(), setup_config(conf)) + self._current_line = "" + self._cursor_offset = 0 + + def _get_current_line(self) -> str: + return self._current_line + + def _set_current_line(self, val: str) -> None: + self._current_line = val + + def _get_cursor_offset(self) -> int: + return self._cursor_offset + + def _set_cursor_offset(self, val: int) -> None: + self._cursor_offset = val + + def getstdout(self) -> str: + raise NotImplementedError + + def reprint_line( + self, lineno: int, tokens: List[Tuple[repl._TokenType, str]] + ) -> None: + raise NotImplementedError + + def reevaluate(self): + raise NotImplementedError class FakeCliRepl(cli.CLIRepl, FakeRepl): From cc3bac7e552cb7d88d25bac0011caf045714126c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 11:09:05 +0200 Subject: [PATCH 075/262] Change filenames in History to Path --- bpython/history.py | 15 ++++++++------- bpython/repl.py | 6 +++--- bpython/test/test_history.py | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/bpython/history.py b/bpython/history.py index dfbab2ad..a870d4b2 100644 --- a/bpython/history.py +++ b/bpython/history.py @@ -22,9 +22,10 @@ # THE SOFTWARE. import os +from pathlib import Path import stat from itertools import islice, chain -from typing import Iterable, Optional, List, TextIO +from typing import Iterable, Optional, List, TextIO, Union from .translations import _ from .filelock import FileLock @@ -190,9 +191,9 @@ def reset(self) -> None: self.index = 0 self.saved_line = "" - def load(self, filename: str, encoding: str) -> None: + def load(self, filename: Path, encoding: str) -> None: with open(filename, encoding=encoding, errors="ignore") as hfile: - with FileLock(hfile, filename=filename): + with FileLock(hfile, filename=str(filename)): self.entries = self.load_from(hfile) def load_from(self, fd: TextIO) -> List[str]: @@ -201,14 +202,14 @@ def load_from(self, fd: TextIO) -> List[str]: self.append_to(entries, line) return entries if len(entries) else [""] - def save(self, filename: str, encoding: str, lines: int = 0) -> None: + def save(self, filename: Path, encoding: str, lines: int = 0) -> None: fd = os.open( filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, stat.S_IRUSR | stat.S_IWUSR, ) with open(fd, "w", encoding=encoding, errors="ignore") as hfile: - with FileLock(hfile, filename=filename): + with FileLock(hfile, filename=str(filename)): self.save_to(hfile, self.entries, lines) def save_to( @@ -221,7 +222,7 @@ def save_to( fd.write("\n") def append_reload_and_write( - self, s: str, filename: str, encoding: str + self, s: str, filename: Path, encoding: str ) -> None: if not self.hist_size: return self.append(s) @@ -233,7 +234,7 @@ def append_reload_and_write( stat.S_IRUSR | stat.S_IWUSR, ) with open(fd, "a+", encoding=encoding, errors="ignore") as hfile: - with FileLock(hfile, filename=filename): + with FileLock(hfile, filename=str(filename)): # read entries hfile.seek(0, os.SEEK_SET) entries = self.load_from(hfile) diff --git a/bpython/repl.py b/bpython/repl.py index 6420a178..e0d42b1f 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -520,7 +520,7 @@ def __init__(self, interp: Interpreter, config: Config): if self.config.hist_file.exists(): try: self.rl_history.load( - str(self.config.hist_file), + self.config.hist_file, getpreferredencoding() or "ascii", ) except OSError: @@ -744,7 +744,7 @@ def get_source_of_current_name(self) -> str: msg = _("No source code found for %s") % (self.current_line,) raise SourceNotFound(msg) - def set_docstring(self): + def set_docstring(self) -> None: self.docstring = None if not self.get_args(): self.funcprops = None @@ -1009,7 +1009,7 @@ def push(self, s, insert_into_history=True) -> bool: return more - def insert_into_history(self, s): + def insert_into_history(self, s: str): try: self.rl_history.append_reload_and_write( s, self.config.hist_file, getpreferredencoding() diff --git a/bpython/test/test_history.py b/bpython/test/test_history.py index 544a644e..d810cf6b 100644 --- a/bpython/test/test_history.py +++ b/bpython/test/test_history.py @@ -86,7 +86,7 @@ def test_reset(self): class TestHistoryFileAccess(unittest.TestCase): def setUp(self): self.tempdir = tempfile.TemporaryDirectory() - self.filename = str(Path(self.tempdir.name) / "history_temp_file") + self.filename = Path(self.tempdir.name) / "history_temp_file" self.encoding = getpreferredencoding() with open( From 167712230a803edd7b34a1cb127926632c964a01 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 23:50:02 +0200 Subject: [PATCH 076/262] Simplify type annotations --- bpython/curtsiesfrontend/repl.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index b8c7f016..c2b502fe 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1,4 +1,3 @@ -import code import contextlib import errno import itertools @@ -22,13 +21,9 @@ Union, cast, Type, - TYPE_CHECKING, ) from .._typing_compat import Literal -if TYPE_CHECKING: - from ..repl import Interpreter - import blessings import greenlet from curtsies import ( @@ -327,7 +322,7 @@ def __init__( config: Config, locals_: Optional[Dict[str, Any]] = None, banner: Optional[str] = None, - interp: Optional[code.InteractiveInterpreter] = None, + interp: Optional[Interp] = None, orig_tcattrs: Optional[List[Any]] = None, ): """ @@ -372,7 +367,7 @@ def __init__( logger.debug("starting parent init") # interp is a subclass of repl.Interpreter, so it definitely, # implements the methods of Interpreter! - super().__init__(cast("Interpreter", interp), config) + super().__init__(interp, config) self.formatter = BPythonFormatter(config.color_scheme) From 2a95aa790aabe21dab4860b30e69f954627c5ed3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 23:50:11 +0200 Subject: [PATCH 077/262] Remove unused import --- bpython/curtsies.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 82adc15f..102252b2 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -3,7 +3,6 @@ # mypy: disallow_untyped_calls=True import argparse -import code import collections import logging import sys From 741015e1c786999f66fd49af08bf28419a5ab478 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 23:50:22 +0200 Subject: [PATCH 078/262] Fix default argument --- bpython/curtsiesfrontend/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index c2b502fe..8cf1467d 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -544,7 +544,7 @@ def request_reload(self, files_modified=()): if self.watching_files: self._request_reload(files_modified=files_modified) - def schedule_refresh(self, when="now"): + def schedule_refresh(self, when: float = 0) -> None: """Schedule a ScheduledRefreshRequestEvent for when. Such a event should interrupt if blockied waiting for keyboard input""" From 44c7d42fb8a6fda96529f454715ae7628e54e5c8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 23:59:52 +0200 Subject: [PATCH 079/262] Turn search modes into an Enum --- bpython/curtsiesfrontend/repl.py | 40 +++++++++++++++++++------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 8cf1467d..258a937b 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -84,6 +84,12 @@ MAX_EVENTS_POSSIBLY_NOT_PASTE = 20 +class SearchMode(Enum): + NO_SEARCH = 0 + INCREMENTAL_SEARCH = 1 + REVERSE_INCREMENTAL_SEARCH = 2 + + class LineType(Enum): """Used when adding a tuple to all_logical_lines, to get input / output values having to actually type/know the strings""" @@ -452,9 +458,7 @@ def __init__( # whether auto reloading active self.watching_files = config.default_autoreload - # 'reverse_incremental_search', 'incremental_search' or None - self.incr_search_mode = None - + self.incr_search_mode = SearchMode.NO_SEARCH self.incr_search_target = "" self.original_modules = set(sys.modules.keys()) @@ -758,7 +762,7 @@ def process_key_event(self, e: str) -> None: self.incremental_search() elif ( e in (("",) + key_dispatch[self.config.backspace_key]) - and self.incr_search_mode + and self.incr_search_mode != SearchMode.NO_SEARCH ): self.add_to_incremental_search(self, backspace=True) elif e in self.edit_keys.cut_buffer_edits: @@ -808,7 +812,7 @@ def process_key_event(self, e: str) -> None: elif e in key_dispatch[self.config.edit_current_block_key]: self.send_current_block_to_external_editor() elif e in ("",): - self.incr_search_mode = None + self.incr_search_mode = SearchMode.NO_SEARCH elif e in ("",): self.add_normal_character(" ") elif e in CHARACTER_PAIR_MAP.keys(): @@ -852,7 +856,11 @@ def insert_char_pair_start(self, e): if not can_lookup_next else self._current_line[self._cursor_offset] ) - if start_of_line or end_of_line or next_char in "})] ": + if ( + start_of_line + or end_of_line + or (next_char is not None and next_char in "})] ") + ): self.add_normal_character( CHARACTER_PAIR_MAP[e], narrow_search=False ) @@ -891,7 +899,7 @@ def get_last_word(self): ) def incremental_search(self, reverse=False, include_current=False): - if self.incr_search_mode is None: + if self.incr_search_mode == SearchMode.NO_SEARCH: self.rl_history.enter(self.current_line) self.incr_search_target = "" else: @@ -920,9 +928,9 @@ def incremental_search(self, reverse=False, include_current=False): clear_special_mode=False, ) if reverse: - self.incr_search_mode = "reverse_incremental_search" + self.incr_search_mode = SearchMode.REVERSE_INCREMENTAL_SEARCH else: - self.incr_search_mode = "incremental_search" + self.incr_search_mode = SearchMode.INCREMENTAL_SEARCH def readline_kill(self, e): func = self.edit_keys[e] @@ -1172,7 +1180,7 @@ def toggle_file_watch(self): def add_normal_character(self, char, narrow_search=True): if len(char) > 1 or is_nop(char): return - if self.incr_search_mode: + if self.incr_search_mode != SearchMode.NO_SEARCH: self.add_to_incremental_search(char) else: self._set_current_line( @@ -1209,9 +1217,9 @@ def add_to_incremental_search(self, char=None, backspace=False): self.incr_search_target = self.incr_search_target[:-1] else: self.incr_search_target += char - if self.incr_search_mode == "reverse_incremental_search": + if self.incr_search_mode == SearchMode.REVERSE_INCREMENTAL_SEARCH: self.incremental_search(reverse=True, include_current=True) - elif self.incr_search_mode == "incremental_search": + elif self.incr_search_mode == SearchMode.INCREMENTAL_SEARCH: self.incremental_search(include_current=True) else: raise ValueError("add_to_incremental_search not in a special mode") @@ -1419,7 +1427,7 @@ def current_line_formatted(self): fs = bpythonparse( pygformat(self.tokenize(self.current_line), self.formatter) ) - if self.incr_search_mode: + if self.incr_search_mode != SearchMode.NO_SEARCH: if self.incr_search_target in self.current_line: fs = fmtfuncs.on_magenta(self.incr_search_target).join( fs.split(self.incr_search_target) @@ -1467,12 +1475,12 @@ def display_line_with_prompt(self): """colored line with prompt""" prompt = func_for_letter(self.config.color_scheme["prompt"]) more = func_for_letter(self.config.color_scheme["prompt_more"]) - if self.incr_search_mode == "reverse_incremental_search": + if self.incr_search_mode == SearchMode.REVERSE_INCREMENTAL_SEARCH: return ( prompt(f"(reverse-i-search)`{self.incr_search_target}': ") + self.current_line_formatted ) - elif self.incr_search_mode == "incremental_search": + elif self.incr_search_mode == SearchMode.INCREMENTAL_SEARCH: return prompt(f"(i-search)`%s': ") + self.current_line_formatted return ( prompt(self.ps1) if self.done else more(self.ps2) @@ -1905,7 +1913,7 @@ def _set_cursor_offset( if reset_rl_history: self.rl_history.reset() if clear_special_mode: - self.incr_search_mode = None + self.incr_search_mode = SearchMode.NO_SEARCH self._cursor_offset = offset if update_completion: self.update_completion() From 49255d6bfcc69465fe1a2c5d82d015a2fdf4488f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 00:10:13 +0200 Subject: [PATCH 080/262] Re-arrange checks --- bpython/curtsiesfrontend/repl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 258a937b..98b78d05 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1211,12 +1211,12 @@ def add_to_incremental_search(self, char=None, backspace=False): The only operations allowed in incremental search mode are adding characters and backspacing.""" - if char is None and not backspace: - raise ValueError("must provide a char or set backspace to True") if backspace: self.incr_search_target = self.incr_search_target[:-1] - else: + elif char is not None: self.incr_search_target += char + else: + raise ValueError("must provide a char or set backspace to True") if self.incr_search_mode == SearchMode.REVERSE_INCREMENTAL_SEARCH: self.incremental_search(reverse=True, include_current=True) elif self.incr_search_mode == SearchMode.INCREMENTAL_SEARCH: From 2776156c535368e723582ca1835efd6881e7e8e1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 00:21:35 +0200 Subject: [PATCH 081/262] Directly sum up lengths There is no need to build the complete string. --- bpython/curtsiesfrontend/repl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 98b78d05..7020b678 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1536,15 +1536,15 @@ def number_of_padding_chars_on_current_cursor_line(self): Should return zero unless there are fullwidth characters.""" full_line = self.current_cursor_line_without_suggestion - line_with_padding = "".join( - line.s + line_with_padding_len = sum( + len(line.s) for line in paint.display_linize( self.current_cursor_line_without_suggestion.s, self.width ) ) # the difference in length here is how much padding there is - return len(line_with_padding) - len(full_line) + return line_with_padding_len - len(full_line) def paint( self, From 2c80e03655c79527f05abb5a2e12a733bd50d11a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 00:52:30 +0200 Subject: [PATCH 082/262] Add type annotations --- bpython/curtsiesfrontend/preprocess.py | 35 +++++++++++++------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/bpython/curtsiesfrontend/preprocess.py b/bpython/curtsiesfrontend/preprocess.py index e0d15f4e..5e59dd49 100644 --- a/bpython/curtsiesfrontend/preprocess.py +++ b/bpython/curtsiesfrontend/preprocess.py @@ -1,34 +1,38 @@ """Tools for preparing code to be run in the REPL (removing blank lines, etc)""" -from ..lazyre import LazyReCompile +from codeop import CommandCompiler +from typing import Match from itertools import tee, islice, chain +from ..lazyre import LazyReCompile + # TODO specifically catch IndentationErrors instead of any syntax errors indent_empty_lines_re = LazyReCompile(r"\s*") tabs_to_spaces_re = LazyReCompile(r"^\t+") -def indent_empty_lines(s, compiler): +def indent_empty_lines(s: str, compiler: CommandCompiler) -> str: """Indents blank lines that would otherwise cause early compilation Only really works if starting on a new line""" - lines = s.split("\n") + initial_lines = s.split("\n") ends_with_newline = False - if lines and not lines[-1]: + if initial_lines and not initial_lines[-1]: ends_with_newline = True - lines.pop() + initial_lines.pop() result_lines = [] - prevs, lines, nexts = tee(lines, 3) + prevs, lines, nexts = tee(initial_lines, 3) prevs = chain(("",), prevs) nexts = chain(islice(nexts, 1, None), ("",)) for p_line, line, n_line in zip(prevs, lines, nexts): if len(line) == 0: - p_indent = indent_empty_lines_re.match(p_line).group() - n_indent = indent_empty_lines_re.match(n_line).group() + # "\s*" always matches + p_indent = indent_empty_lines_re.match(p_line).group() # type: ignore + n_indent = indent_empty_lines_re.match(n_line).group() # type: ignore result_lines.append(min([p_indent, n_indent], key=len) + line) else: result_lines.append(line) @@ -36,17 +40,14 @@ def indent_empty_lines(s, compiler): return "\n".join(result_lines) + ("\n" if ends_with_newline else "") -def leading_tabs_to_spaces(s): - lines = s.split("\n") - result_lines = [] - - def tab_to_space(m): +def leading_tabs_to_spaces(s: str) -> str: + def tab_to_space(m: Match[str]) -> str: return len(m.group()) * 4 * " " - for line in lines: - result_lines.append(tabs_to_spaces_re.sub(tab_to_space, line)) - return "\n".join(result_lines) + return "\n".join( + tabs_to_spaces_re.sub(tab_to_space, line) for line in s.split("\n") + ) -def preprocess(s, compiler): +def preprocess(s: str, compiler: CommandCompiler) -> str: return indent_empty_lines(leading_tabs_to_spaces(s), compiler) From 9ffa5f3eef565fed62a7c7976ec856970249fd14 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 01:13:50 +0200 Subject: [PATCH 083/262] Add type annotations --- bpython/curtsiesfrontend/filewatch.py | 25 +++++++++++++------------ stubs/watchdog/events.pyi | 4 ++++ stubs/watchdog/observers.pyi | 6 +++++- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 7616d484..8f64fd3d 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -1,11 +1,12 @@ import os from collections import defaultdict +from typing import Dict, Iterable, Set, List from .. import importcompletion try: from watchdog.observers import Observer - from watchdog.events import FileSystemEventHandler + from watchdog.events import FileSystemEventHandler, FileSystemEvent except ImportError: def ModuleChangedEventHandler(*args): @@ -14,12 +15,12 @@ def ModuleChangedEventHandler(*args): else: class ModuleChangedEventHandler(FileSystemEventHandler): # type: ignore [no-redef] - def __init__(self, paths, on_change): - self.dirs = defaultdict(set) + def __init__(self, paths: Iterable[str], on_change) -> None: + self.dirs: Dict[str, Set[str]] = defaultdict(set) self.on_change = on_change - self.modules_to_add_later = [] + self.modules_to_add_later: List[str] = [] self.observer = Observer() - self.old_dirs = defaultdict(set) + self.old_dirs: Dict[str, Set[str]] = defaultdict(set) self.started = False self.activated = False for path in paths: @@ -27,13 +28,13 @@ def __init__(self, paths, on_change): super().__init__() - def reset(self): + def reset(self) -> None: self.dirs = defaultdict(set) del self.modules_to_add_later[:] self.old_dirs = defaultdict(set) self.observer.unschedule_all() - def _add_module(self, path): + def _add_module(self, path: str) -> None: """Add a python module to track changes""" path = os.path.abspath(path) for suff in importcompletion.SUFFIXES: @@ -45,10 +46,10 @@ def _add_module(self, path): self.observer.schedule(self, dirname, recursive=False) self.dirs[dirname].add(path) - def _add_module_later(self, path): + def _add_module_later(self, path: str) -> None: self.modules_to_add_later.append(path) - def track_module(self, path): + def track_module(self, path: str) -> None: """ Begins tracking this if activated, or remembers to track later. """ @@ -57,7 +58,7 @@ def track_module(self, path): else: self._add_module_later(path) - def activate(self): + def activate(self) -> None: if self.activated: raise ValueError(f"{self!r} is already activated.") if not self.started: @@ -70,13 +71,13 @@ def activate(self): del self.modules_to_add_later[:] self.activated = True - def deactivate(self): + def deactivate(self) -> None: if not self.activated: raise ValueError(f"{self!r} is not activated.") self.observer.unschedule_all() self.activated = False - def on_any_event(self, event): + def on_any_event(self, event: FileSystemEvent) -> None: dirpath = os.path.dirname(event.src_path) paths = [path + ".py" for path in self.dirs[dirpath]] if event.src_path in paths: diff --git a/stubs/watchdog/events.pyi b/stubs/watchdog/events.pyi index 6e17bd6d..ded1fe94 100644 --- a/stubs/watchdog/events.pyi +++ b/stubs/watchdog/events.pyi @@ -1 +1,5 @@ +class FileSystemEvent: + @property + def src_path(self) -> str: ... + class FileSystemEventHandler: ... diff --git a/stubs/watchdog/observers.pyi b/stubs/watchdog/observers.pyi index 7db3099f..c4596f2d 100644 --- a/stubs/watchdog/observers.pyi +++ b/stubs/watchdog/observers.pyi @@ -1,4 +1,8 @@ +from .events import FileSystemEventHandler + class Observer: def start(self): ... - def schedule(self, dirname: str, recursive: bool): ... + def schedule( + self, observer: FileSystemEventHandler, dirname: str, recursive: bool + ): ... def unschedule_all(self): ... From 43778e8dc955ca0163fc846eaa0400dee096b9e9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 01:21:54 +0200 Subject: [PATCH 084/262] Remove unused member --- bpython/curtsiesfrontend/filewatch.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 8f64fd3d..33054c4c 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -20,7 +20,6 @@ def __init__(self, paths: Iterable[str], on_change) -> None: self.on_change = on_change self.modules_to_add_later: List[str] = [] self.observer = Observer() - self.old_dirs: Dict[str, Set[str]] = defaultdict(set) self.started = False self.activated = False for path in paths: @@ -31,7 +30,6 @@ def __init__(self, paths: Iterable[str], on_change) -> None: def reset(self) -> None: self.dirs = defaultdict(set) del self.modules_to_add_later[:] - self.old_dirs = defaultdict(set) self.observer.unschedule_all() def _add_module(self, path: str) -> None: From 5f070f474e5a3a0761c29621a72829edf32d6734 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 09:06:08 +0200 Subject: [PATCH 085/262] Initialize width/height using os.get_terminal_size --- bpython/curtsiesfrontend/repl.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 7020b678..8ba7545d 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -19,7 +19,6 @@ Optional, Tuple, Union, - cast, Type, ) from .._typing_compat import Literal @@ -465,8 +464,7 @@ def __init__( # as long as the first event received is a window resize event, # this works fine... - self.width: int = cast(int, None) - self.height: int = cast(int, None) + self.width, self.height = os.get_terminal_size() self.status_bar.message(banner) From e82b4fc5279974598a1c0650ce9b2df35333df3a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 09:29:37 +0200 Subject: [PATCH 086/262] Handle OSError from unit tests --- bpython/curtsiesfrontend/repl.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 8ba7545d..2ea4269c 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -464,7 +464,12 @@ def __init__( # as long as the first event received is a window resize event, # this works fine... - self.width, self.height = os.get_terminal_size() + try: + self.width, self.height = os.get_terminal_size() + except OSError: + # this case will trigger during unit tests when stdout is redirected + self.width = -1 + self.height = -1 self.status_bar.message(banner) From 592725a487a9da0fd5523676a99993314a974d8e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 09:35:32 +0200 Subject: [PATCH 087/262] Improve on_any_event and add type annotations --- bpython/curtsies.py | 2 +- bpython/curtsiesfrontend/filewatch.py | 15 ++++++++++----- bpython/curtsiesfrontend/repl.py | 7 ++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 102252b2..caa6e08e 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -104,7 +104,7 @@ def _request_refresh(self) -> None: def _schedule_refresh(self, when: float) -> None: return self._schedule_refresh_callback(when) - def _request_reload(self, files_modified: Sequence[str] = ("?",)) -> None: + def _request_reload(self, files_modified: Sequence[str]) -> None: return self._request_reload_callback(files_modified) def interrupting_refresh(self) -> None: diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 33054c4c..8933e293 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -1,6 +1,6 @@ import os from collections import defaultdict -from typing import Dict, Iterable, Set, List +from typing import Callable, Dict, Iterable, Sequence, Set, List from .. import importcompletion @@ -15,7 +15,11 @@ def ModuleChangedEventHandler(*args): else: class ModuleChangedEventHandler(FileSystemEventHandler): # type: ignore [no-redef] - def __init__(self, paths: Iterable[str], on_change) -> None: + def __init__( + self, + paths: Iterable[str], + on_change: Callable[[Sequence[str]], None], + ) -> None: self.dirs: Dict[str, Set[str]] = defaultdict(set) self.on_change = on_change self.modules_to_add_later: List[str] = [] @@ -77,6 +81,7 @@ def deactivate(self) -> None: def on_any_event(self, event: FileSystemEvent) -> None: dirpath = os.path.dirname(event.src_path) - paths = [path + ".py" for path in self.dirs[dirpath]] - if event.src_path in paths: - self.on_change(files_modified=[event.src_path]) + if any( + event.src_path == f"{path}.py" for path in self.dirs[dirpath] + ): + self.on_change((event.src_path,)) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 2ea4269c..2226cae3 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -17,6 +17,7 @@ Any, List, Optional, + Sequence, Tuple, Union, Type, @@ -516,7 +517,7 @@ def _request_refresh(self): RefreshRequestEvent.""" raise NotImplementedError - def _request_reload(self, files_modified=("?",)): + def _request_reload(self, files_modified: Sequence[str]) -> None: """Like request_refresh, but for reload requests events.""" raise NotImplementedError @@ -546,10 +547,10 @@ def request_refresh(self): else: self._request_refresh() - def request_reload(self, files_modified=()): + def request_reload(self, files_modified: Sequence[str] = ()) -> None: """Request that a ReloadEvent be passed next into process_event""" if self.watching_files: - self._request_reload(files_modified=files_modified) + self._request_reload(files_modified) def schedule_refresh(self, when: float = 0) -> None: """Schedule a ScheduledRefreshRequestEvent for when. From 56dfd88d11212a1834222a52a981b4a7bb0713f1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 09:36:13 +0200 Subject: [PATCH 088/262] Ignore mypy cache --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1bda0f25..7a81cbfe 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ doc/sphinx/build/* bpython/_version.py venv/ .venv/ +.mypy_cache/ From e987ad32baf45f0bb89e4a2fd3f9f6d36abfd1d6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 09:36:45 +0200 Subject: [PATCH 089/262] Add type annotations --- bpython/curtsiesfrontend/repl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 2226cae3..7a591d41 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -11,7 +11,7 @@ import time import unicodedata from enum import Enum -from types import TracebackType +from types import FrameType, TracebackType from typing import ( Dict, Any, @@ -611,7 +611,7 @@ def __exit__( sys.meta_path = self.orig_meta_path return False - def sigwinch_handler(self, signum, frame): + def sigwinch_handler(self, signum: int, frame: Optional[FrameType]) -> None: old_rows, old_columns = self.height, self.width self.height, self.width = self.get_term_hw() cursor_dy = self.get_cursor_vertical_diff() @@ -627,7 +627,7 @@ def sigwinch_handler(self, signum, frame): self.scroll_offset, ) - def sigtstp_handler(self, signum, frame): + def sigtstp_handler(self, signum: int, frame: Optional[FrameType]) -> None: self.scroll_offset = len(self.lines_for_display) self.__exit__(None, None, None) self.on_suspend() From 28ea475bbafa85864c476a129725b3bf4d006cf5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 23:18:52 +0200 Subject: [PATCH 090/262] Store window to avoid implicit dependency on blessing/blessed --- bpython/curtsies.py | 3 ++- bpython/curtsiesfrontend/repl.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index caa6e08e..f7b2ef47 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -61,7 +61,7 @@ def __init__( self.input_generator = curtsies.input.Input( keynames="curtsies", sigint_event=True, paste_threshold=None ) - self.window = curtsies.window.CursorAwareWindow( + window = curtsies.window.CursorAwareWindow( sys.stdout, sys.stdin, keep_last_line=True, @@ -92,6 +92,7 @@ def __init__( super().__init__( config, + window, locals_=locals_, banner=banner, interp=interp, diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 7a591d41..3a44f4e5 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -24,7 +24,6 @@ ) from .._typing_compat import Literal -import blessings import greenlet from curtsies import ( FSArray, @@ -37,6 +36,7 @@ ) from curtsies.configfile_keynames import keymap as key_dispatch from curtsies.input import is_main_thread +from curtsies.window import BaseWindow from cwcwidth import wcswidth from pygments import format as pygformat from pygments.formatters import TerminalFormatter @@ -326,6 +326,7 @@ class BaseRepl(Repl): def __init__( self, config: Config, + window: BaseWindow, locals_: Optional[Dict[str, Any]] = None, banner: Optional[str] = None, interp: Optional[Interp] = None, @@ -340,6 +341,7 @@ def __init__( """ logger.debug("starting init") + self.window = window # If creating a new interpreter on undo would be unsafe because initial # state was passed in @@ -2077,7 +2079,7 @@ def focus_on_subprocess(self, args): try: signal.signal(signal.SIGWINCH, self.orig_sigwinch_handler) with Termmode(self.orig_stdin, self.orig_tcattrs): - terminal = blessings.Terminal(stream=sys.__stdout__) + terminal = self.window.t with terminal.fullscreen(): sys.__stdout__.write(terminal.save) sys.__stdout__.write(terminal.move(0, 0)) From 0d16fe6e3a896a4fa759e1cb08605b0c7bf25c83 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 23:32:56 +0200 Subject: [PATCH 091/262] Add type annotations --- bpython/curtsiesfrontend/repl.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 3a44f4e5..b5956ba7 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -388,7 +388,7 @@ def __init__( self._current_line = "" # current line of output - stdout and stdin go here - self.current_stdouterr_line = "" # Union[str, FmtStr] + self.current_stdouterr_line: Union[str, FmtStr] = "" # this is every line that's been displayed (input and output) # as with formatting applied. Logical lines that exceeded the terminal width @@ -657,8 +657,7 @@ def process_event(self, e: Union[events.Event, str]) -> Optional[bool]: self.process_key_event(e) return None - def process_control_event(self, e) -> Optional[bool]: - + def process_control_event(self, e: events.Event) -> Optional[bool]: if isinstance(e, bpythonevents.ScheduledRefreshRequestEvent): # This is a scheduled refresh - it's really just a refresh (so nop) pass @@ -703,9 +702,9 @@ def process_control_event(self, e) -> Optional[bool]: elif isinstance(e, bpythonevents.RunStartupFileEvent): try: self.startup() - except OSError as e: + except OSError as err: self.status_bar.message( - _("Executing PYTHONSTARTUP failed: %s") % (e,) + _("Executing PYTHONSTARTUP failed: %s") % (err,) ) elif isinstance(e, bpythonevents.UndoEvent): From 80bd3e0ef05ac40abe98aa9f3b84db1e25687c28 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 23:47:30 +0200 Subject: [PATCH 092/262] Make window optional for tests Where required, assert that window is not None. --- bpython/curtsiesfrontend/repl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index b5956ba7..2c8e93e1 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -326,7 +326,7 @@ class BaseRepl(Repl): def __init__( self, config: Config, - window: BaseWindow, + window: Optional[BaseWindow] = None, locals_: Optional[Dict[str, Any]] = None, banner: Optional[str] = None, interp: Optional[Interp] = None, @@ -2078,6 +2078,7 @@ def focus_on_subprocess(self, args): try: signal.signal(signal.SIGWINCH, self.orig_sigwinch_handler) with Termmode(self.orig_stdin, self.orig_tcattrs): + assert self.window is not None terminal = self.window.t with terminal.fullscreen(): sys.__stdout__.write(terminal.save) From 8d16a71ef404db66d2c6fae6c362640da8ae240d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 6 May 2022 00:02:30 +0200 Subject: [PATCH 093/262] Change the type to CursorAwareWindow Pass None in the tests. This is good enough. --- bpython/curtsiesfrontend/repl.py | 4 ++-- bpython/test/test_brackets_completion.py | 7 ++++++- bpython/test/test_curtsies_painting.py | 8 ++++++-- bpython/test/test_curtsies_repl.py | 6 +++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 2c8e93e1..49d61a84 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -36,7 +36,7 @@ ) from curtsies.configfile_keynames import keymap as key_dispatch from curtsies.input import is_main_thread -from curtsies.window import BaseWindow +from curtsies.window import CursorAwareWindow from cwcwidth import wcswidth from pygments import format as pygformat from pygments.formatters import TerminalFormatter @@ -326,7 +326,7 @@ class BaseRepl(Repl): def __init__( self, config: Config, - window: Optional[BaseWindow] = None, + window: CursorAwareWindow, locals_: Optional[Dict[str, Any]] = None, banner: Optional[str] = None, interp: Optional[Interp] = None, diff --git a/bpython/test/test_brackets_completion.py b/bpython/test/test_brackets_completion.py index fd983665..4340ad3d 100644 --- a/bpython/test/test_brackets_completion.py +++ b/bpython/test/test_brackets_completion.py @@ -1,9 +1,12 @@ import os +from typing import cast from bpython.test import FixLanguageTestCase as TestCase, TEST_CONFIG from bpython.curtsiesfrontend import repl as curtsiesrepl from bpython import config +from curtsies.window import CursorAwareWindow + def setup_config(conf): config_struct = config.Config(TEST_CONFIG) @@ -18,7 +21,9 @@ def create_repl(brackets_enabled=False, **kwargs): config = setup_config( {"editor": "true", "brackets_completion": brackets_enabled} ) - repl = curtsiesrepl.BaseRepl(config, **kwargs) + repl = curtsiesrepl.BaseRepl( + config, cast(None, CursorAwareWindow), **kwargs + ) os.environ["PAGER"] = "true" os.environ.pop("PYTHONSTARTUP", None) repl.width = 50 diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 9f98bf06..813b4fb3 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -5,12 +5,14 @@ import sys from contextlib import contextmanager +from typing import cast from curtsies.formatstringarray import ( fsarray, assertFSArraysEqual, assertFSArraysEqualIgnoringFormatting, ) from curtsies.fmtfuncs import cyan, bold, green, yellow, on_magenta, red +from curtsies.window import CursorAwareWindow from unittest import mock from bpython.curtsiesfrontend.events import RefreshRequestEvent @@ -56,7 +58,7 @@ class TestRepl(BaseRepl): def _request_refresh(inner_self): pass - self.repl = TestRepl(config=setup_config()) + self.repl = TestRepl(setup_config(), cast(None, CursorAwareWindow)) self.repl.height, self.repl.width = (5, 10) @property @@ -284,7 +286,9 @@ class TestRepl(BaseRepl): def _request_refresh(inner_self): self.refresh() - self.repl = TestRepl(banner="", config=setup_config()) + self.repl = TestRepl( + setup_config(), cast(None, CursorAwareWindow), banner="" + ) self.repl.height, self.repl.width = (5, 32) def send_key(self, key): diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py index a6e4c786..5a19c6ab 100644 --- a/bpython/test/test_curtsies_repl.py +++ b/bpython/test/test_curtsies_repl.py @@ -3,6 +3,7 @@ import sys import tempfile import io +from typing import cast import unittest from contextlib import contextmanager @@ -23,6 +24,7 @@ ) from curtsies import events +from curtsies.window import CursorAwareWindow from importlib import invalidate_caches @@ -231,7 +233,9 @@ def captured_output(): def create_repl(**kwargs): config = setup_config({"editor": "true"}) - repl = curtsiesrepl.BaseRepl(config, **kwargs) + repl = curtsiesrepl.BaseRepl( + config, cast(CursorAwareWindow, None), **kwargs + ) os.environ["PAGER"] = "true" os.environ.pop("PYTHONSTARTUP", None) repl.width = 50 From 995747d64d03291c0180bf8c6932a96d7541696a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 6 May 2022 00:07:56 +0200 Subject: [PATCH 094/262] Fix tests --- bpython/test/test_brackets_completion.py | 2 +- bpython/test/test_curtsies_painting.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/test/test_brackets_completion.py b/bpython/test/test_brackets_completion.py index 4340ad3d..14169d6a 100644 --- a/bpython/test/test_brackets_completion.py +++ b/bpython/test/test_brackets_completion.py @@ -22,7 +22,7 @@ def create_repl(brackets_enabled=False, **kwargs): {"editor": "true", "brackets_completion": brackets_enabled} ) repl = curtsiesrepl.BaseRepl( - config, cast(None, CursorAwareWindow), **kwargs + config, cast(CursorAwareWindow, None), **kwargs ) os.environ["PAGER"] = "true" os.environ.pop("PYTHONSTARTUP", None) diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 813b4fb3..2804643c 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -58,7 +58,7 @@ class TestRepl(BaseRepl): def _request_refresh(inner_self): pass - self.repl = TestRepl(setup_config(), cast(None, CursorAwareWindow)) + self.repl = TestRepl(setup_config(), cast(CursorAwareWindow, None)) self.repl.height, self.repl.width = (5, 10) @property @@ -287,7 +287,7 @@ def _request_refresh(inner_self): self.refresh() self.repl = TestRepl( - setup_config(), cast(None, CursorAwareWindow), banner="" + setup_config(), cast(CursorAwareWindow, None), banner="" ) self.repl.height, self.repl.width = (5, 32) From 7efed26f3bd0a5f959c96d8081a464c7d42c0fdc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 11:53:23 +0200 Subject: [PATCH 095/262] Add type annotations --- bpython/curtsiesfrontend/repl.py | 61 ++++++++++++++++---------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 49d61a84..91e55b70 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -15,6 +15,7 @@ from typing import ( Dict, Any, + Iterable, List, Optional, Sequence, @@ -161,7 +162,7 @@ def process_event(self, e: Union[events.Event, str]) -> None: else: self.repl.send_to_stdin(self.current_line) - def add_input_character(self, e): + def add_input_character(self, e: str) -> None: if e in ("",): e = " " if e.startswith("<") and e.endswith(">"): @@ -190,10 +191,10 @@ def readlines(self, size=-1): def __iter__(self): return iter(self.readlines()) - def isatty(self): + def isatty(self) -> bool: return True - def flush(self): + def flush(self) -> None: """Flush the internal buffer. This is a no-op. Flushing stdin doesn't make any sense anyway.""" @@ -202,7 +203,7 @@ def write(self, value): # others, so here's a hack to keep them happy raise OSError(errno.EBADF, "sys.stdin is read-only") - def close(self): + def close(self) -> None: # hack to make closing stdin a nop # This is useful for multiprocessing.Process, which does work # for the most part, although output from other processes is @@ -210,7 +211,7 @@ def close(self): pass @property - def encoding(self): + def encoding(self) -> str: return sys.__stdin__.encoding # TODO write a read() method? @@ -1083,7 +1084,7 @@ def down_one_line(self): ) self._set_cursor_offset(len(self.current_line), reset_rl_history=False) - def process_simple_keypress(self, e): + def process_simple_keypress(self, e: str): # '\n' needed for pastes if e in ("", "", "", "\n", "\r"): self.on_enter() @@ -1980,7 +1981,7 @@ def prompt_for_undo(): greenlet.greenlet(prompt_for_undo).switch() - def redo(self): + def redo(self) -> None: if self.redo_stack: temp = self.redo_stack.pop() self.history.append(temp) @@ -2061,7 +2062,7 @@ def initialize_interp(self) -> None: del self.coderunner.interp.locals["_repl"] - def getstdout(self): + def getstdout(self) -> str: """ Returns a string of the current bpython session, wrapped, WITH prompts. """ @@ -2096,7 +2097,7 @@ def focus_on_subprocess(self, args): finally: signal.signal(signal.SIGWINCH, prev_sigwinch_handler) - def pager(self, text): + def pager(self, text: str) -> None: """Runs an external pager on text text must be a str""" @@ -2106,7 +2107,7 @@ def pager(self, text): tmp.flush() self.focus_on_subprocess(command + [tmp.name]) - def show_source(self): + def show_source(self) -> None: try: source = self.get_source_of_current_name() except SourceNotFound as e: @@ -2118,10 +2119,10 @@ def show_source(self): ) self.pager(source) - def help_text(self): + def help_text(self) -> str: return self.version_help_text() + "\n" + self.key_help_text() - def version_help_text(self): + def version_help_text(self) -> str: help_message = _( """ Thanks for using bpython! @@ -2148,7 +2149,7 @@ def version_help_text(self): return f"bpython-curtsies version {__version__} using curtsies version {curtsies_version}\n{help_message}" - def key_help_text(self): + def key_help_text(self) -> str: NOT_IMPLEMENTED = ( "suspend", "cut to buffer", @@ -2198,15 +2199,15 @@ def ps2(self): return _process_ps(super().ps2, "... ") -def is_nop(char): - return unicodedata.category(str(char)) == "Cc" +def is_nop(char: str) -> bool: + return unicodedata.category(char) == "Cc" -def tabs_to_spaces(line): +def tabs_to_spaces(line: str) -> str: return line.replace("\t", " ") -def _last_word(line): +def _last_word(line: str) -> str: split_line = line.split() return split_line.pop() if split_line else "" @@ -2230,29 +2231,29 @@ def compress_paste_event(paste_event): return None -def just_simple_events(event_list): +def just_simple_events( + event_list: Iterable[Union[str, events.Event]] +) -> List[str]: simple_events = [] for e in event_list: + if isinstance(e, events.Event): + continue # ignore events # '\n' necessary for pastes - if e in ("", "", "", "\n", "\r"): + elif e in ("", "", "", "\n", "\r"): simple_events.append("\n") - elif isinstance(e, events.Event): - pass # ignore events - elif e in ("",): + elif e == "": simple_events.append(" ") elif len(e) > 1: - pass # get rid of etc. + continue # get rid of etc. else: simple_events.append(e) return simple_events -def is_simple_event(e): +def is_simple_event(e: Union[str, events.Event]) -> bool: if isinstance(e, events.Event): return False - if e in ("", "", "", "\n", "\r", ""): - return True - if len(e) > 1: - return False - else: - return True + return ( + e in ("", "", "", "\n", "\r", "") + or len(e) <= 1 + ) From 2df70ac1eab0b17d2ec62e19b5e64fe610a19fb6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 11:53:53 +0200 Subject: [PATCH 096/262] Simplify some checks --- bpython/curtsiesfrontend/repl.py | 83 ++++++++++++++++---------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 91e55b70..d5a3eed1 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -122,40 +122,42 @@ def process_event(self, e: Union[events.Event, str]) -> None: assert self.has_focus logger.debug("fake input processing event %r", e) - if isinstance(e, events.PasteEvent): - for ee in e.events: - if ee not in self.rl_char_sequences: - self.add_input_character(ee) - elif e in self.rl_char_sequences: - self.cursor_offset, self.current_line = self.rl_char_sequences[e]( - self.cursor_offset, self.current_line - ) - elif isinstance(e, events.SigIntEvent): - self.coderunner.sigint_happened_in_main_context = True - self.has_focus = False - self.current_line = "" - self.cursor_offset = 0 - self.repl.run_code_and_maybe_finish() - elif e in ("",): - pass - elif e in ("",): - if self.current_line == "": - self.repl.send_to_stdin("\n") + if isinstance(e, events.Event): + if isinstance(e, events.PasteEvent): + for ee in e.events: + if ee not in self.rl_char_sequences: + self.add_input_character(ee) + elif isinstance(e, events.SigIntEvent): + self.coderunner.sigint_happened_in_main_context = True self.has_focus = False self.current_line = "" self.cursor_offset = 0 - self.repl.run_code_and_maybe_finish(for_code="") - else: + self.repl.run_code_and_maybe_finish() + else: + if e in self.rl_char_sequences: + self.cursor_offset, self.current_line = self.rl_char_sequences[ + e + ](self.cursor_offset, self.current_line) + elif e in ("",): pass - elif e in ("\n", "\r", "", ""): - line = self.current_line - self.repl.send_to_stdin(line + "\n") - self.has_focus = False - self.current_line = "" - self.cursor_offset = 0 - self.repl.run_code_and_maybe_finish(for_code=line + "\n") - else: # add normal character - self.add_input_character(e) + elif e in ("",): + if self.current_line == "": + self.repl.send_to_stdin("\n") + self.has_focus = False + self.current_line = "" + self.cursor_offset = 0 + self.repl.run_code_and_maybe_finish(for_code="") + else: + pass + elif e in ("\n", "\r", "", ""): + line = self.current_line + self.repl.send_to_stdin(line + "\n") + self.has_focus = False + self.current_line = "" + self.cursor_offset = 0 + self.repl.run_code_and_maybe_finish(for_code=line + "\n") + else: # add normal character + self.add_input_character(e) if self.current_line.endswith(("\n", "\r")): pass @@ -163,17 +165,16 @@ def process_event(self, e: Union[events.Event, str]) -> None: self.repl.send_to_stdin(self.current_line) def add_input_character(self, e: str) -> None: - if e in ("",): + if e == "": e = " " if e.startswith("<") and e.endswith(">"): return assert len(e) == 1, "added multiple characters: %r" % e logger.debug("adding normal char %r to current line", e) - c = e self.current_line = ( self.current_line[: self.cursor_offset] - + c + + e + self.current_line[self.cursor_offset :] ) self.cursor_offset += 1 @@ -756,11 +757,11 @@ def process_key_event(self, e: str) -> None: self.up_one_line() elif e in ("",) + key_dispatch[self.config.down_one_line_key]: self.down_one_line() - elif e in ("",): + elif e == "": self.on_control_d() - elif e in ("",): + elif e == "": self.operate_and_get_next() - elif e in ("",): + elif e == "": self.get_last_word() elif e in key_dispatch[self.config.reverse_incremental_search_key]: self.incremental_search(reverse=True) @@ -796,9 +797,9 @@ def process_key_event(self, e: str) -> None: raise SystemExit() elif e in ("\n", "\r", "", "", ""): self.on_enter() - elif e in ("",): # tab + elif e == "": # tab self.on_tab() - elif e in ("",): + elif e == "": self.on_tab(back=True) elif e in key_dispatch[self.config.undo_key]: # ctrl-r for undo self.prompt_undo() @@ -817,9 +818,9 @@ def process_key_event(self, e: str) -> None: # TODO add PAD keys hack as in bpython.cli elif e in key_dispatch[self.config.edit_current_block_key]: self.send_current_block_to_external_editor() - elif e in ("",): + elif e == "": self.incr_search_mode = SearchMode.NO_SEARCH - elif e in ("",): + elif e == "": self.add_normal_character(" ") elif e in CHARACTER_PAIR_MAP.keys(): if e in ["'", '"']: @@ -1093,7 +1094,7 @@ def process_simple_keypress(self, e: str): self.process_event(bpythonevents.RefreshRequestEvent()) elif isinstance(e, events.Event): pass # ignore events - elif e in ("",): + elif e == "": self.add_normal_character(" ") else: self.add_normal_character(e) From 7d3f3d961af070c0fb1f03dcb704ddd6b14e2440 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 6 May 2022 09:15:55 +0200 Subject: [PATCH 097/262] Remove unused import --- bpython/repl.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index e0d42b1f..06934627 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -65,10 +65,6 @@ have_pyperclip = False from . import autocomplete, inspection, simpleeval - -if TYPE_CHECKING: - from .cli import Statusbar - from .config import getpreferredencoding, Config from .formatter import Parenthesis from .history import History From b3b74b8f49fcdee8d97ac881eca18e93f0b9621a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 6 May 2022 09:17:40 +0200 Subject: [PATCH 098/262] eval wants a dict, so make sure that the Interpreter has a dict --- bpython/cli.py | 2 +- bpython/repl.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 5383a7c3..2512d6ff 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -1940,7 +1940,7 @@ def main_curses( args: List[str], config: Config, interactive: bool = True, - locals_: Optional[MutableMapping[str, str]] = None, + locals_: Optional[Dict[str, Any]] = None, banner: Optional[str] = None, ) -> Tuple[Tuple[Any, ...], str]: """main function for the curses convenience wrapper diff --git a/bpython/repl.py b/bpython/repl.py index 06934627..e41536fd 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -48,7 +48,6 @@ Optional, Type, Union, - MutableMapping, Callable, Dict, TYPE_CHECKING, @@ -109,7 +108,7 @@ class Interpreter(code.InteractiveInterpreter): def __init__( self, - locals: Optional[MutableMapping[str, Any]] = None, + locals: Optional[Dict[str, Any]] = None, ) -> None: """Constructor. From dc667b6c9cc72117588559f30c89501781052efa Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 6 May 2022 14:17:38 +0200 Subject: [PATCH 099/262] Directly initialize f_strings --- bpython/curtsiesfrontend/interpreter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 1b313345..95e4b069 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -44,9 +44,7 @@ class BPythonFormatter(Formatter): straightforward.""" def __init__(self, color_scheme, **options): - self.f_strings = {} - for k, v in color_scheme.items(): - self.f_strings[k] = f"\x01{v}" + self.f_strings = {k: f"\x01{v}" for k, v in color_scheme.items()} super().__init__(**options) def format(self, tokensource, outfile): From 492e3d5ff2643191f023e98b8959792ed4bfa0a3 Mon Sep 17 00:00:00 2001 From: rcreddyn Date: Sat, 15 Jan 2022 01:11:23 +0530 Subject: [PATCH 100/262] Added type annotations Sebastian: fixed some type annotations --- bpython/curtsiesfrontend/interpreter.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 95e4b069..80b7d5f5 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -1,11 +1,13 @@ import sys -from typing import Any, Dict, Optional +from codeop import CommandCompiler +from typing import Any, Dict, Iterable, Optional, Tuple, Union from pygments.token import Generic, Token, Keyword, Name, Comment, String from pygments.token import Error, Literal, Number, Operator, Punctuation -from pygments.token import Whitespace +from pygments.token import Whitespace, _TokenType from pygments.formatter import Formatter from pygments.lexers import get_lexer_by_name +from curtsies.formatstring import FmtStr from ..curtsiesfrontend.parse import parse from ..repl import Interpreter as ReplInterpreter @@ -43,7 +45,11 @@ class BPythonFormatter(Formatter): See the Pygments source for more info; it's pretty straightforward.""" - def __init__(self, color_scheme, **options): + def __init__( + self, + color_scheme: Dict[_TokenType, str], + **options: Union[str, bool, None], + ) -> None: self.f_strings = {k: f"\x01{v}" for k, v in color_scheme.items()} super().__init__(**options) @@ -71,7 +77,7 @@ def __init__( # typically changed after being instantiated # but used when interpreter used corresponding REPL - def write(err_line): + def write(err_line: Union[str, FmtStr]) -> None: """Default stderr handler for tracebacks Accepts FmtStrs so interpreters can output them""" @@ -80,13 +86,14 @@ def write(err_line): self.write = write # type: ignore self.outfile = self - def writetb(self, lines): + def writetb(self, lines: Iterable[str]) -> None: tbtext = "".join(lines) lexer = get_lexer_by_name("pytb") self.format(tbtext, lexer) # TODO for tracebacks get_lexer_by_name("pytb", stripall=True) - def format(self, tbtext, lexer): + def format(self, tbtext: str, lexer: Any) -> None: + # FIXME: lexer should is a Lexer traceback_informative_formatter = BPythonFormatter(default_colors) traceback_code_formatter = BPythonFormatter({Token: ("d")}) tokens = list(lexer.get_tokens(tbtext)) @@ -112,7 +119,9 @@ def format(self, tbtext, lexer): assert cur_line == [], cur_line -def code_finished_will_parse(s, compiler): +def code_finished_will_parse( + s: str, compiler: CommandCompiler +) -> Tuple[bool, bool]: """Returns a tuple of whether the buffer could be complete and whether it will parse From 711845fa99638eae4a56d699ac70ae5e5c2a7665 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 May 2022 19:05:40 +0200 Subject: [PATCH 101/262] Iterate directly over result of get_tokens --- bpython/curtsiesfrontend/interpreter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 80b7d5f5..bf3f6ad8 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -96,11 +96,10 @@ def format(self, tbtext: str, lexer: Any) -> None: # FIXME: lexer should is a Lexer traceback_informative_formatter = BPythonFormatter(default_colors) traceback_code_formatter = BPythonFormatter({Token: ("d")}) - tokens = list(lexer.get_tokens(tbtext)) no_format_mode = False cur_line = [] - for token, text in tokens: + for token, text in lexer.get_tokens(tbtext): if text.endswith("\n"): cur_line.append((token, text)) if no_format_mode: From 044bc579106181f79e0113bd2b0e7bac1c37a537 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 May 2022 19:33:55 +0200 Subject: [PATCH 102/262] Remove unused type stub --- stubs/blessings.pyi | 47 --------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 stubs/blessings.pyi diff --git a/stubs/blessings.pyi b/stubs/blessings.pyi deleted file mode 100644 index 66fd9621..00000000 --- a/stubs/blessings.pyi +++ /dev/null @@ -1,47 +0,0 @@ -from typing import ContextManager, Text, IO - -class Terminal: - def __init__(self, stream=None, force_styling=False): - # type: (IO, bool) -> None - pass - def location(self, x=None, y=None): - # type: (int, int) -> ContextManager - pass - @property - def hide_cursor(self): - # type: () -> Text - pass - @property - def normal_cursor(self): - # type: () -> Text - pass - @property - def height(self): - # type: () -> int - pass - @property - def width(self): - # type: () -> int - pass - def fullscreen(self): - # type: () -> ContextManager - pass - def move(self, y, x): - # type: (int, int) -> Text - pass - @property - def clear_eol(self): - # type: () -> Text - pass - @property - def clear_bol(self): - # type: () -> Text - pass - @property - def clear_eos(self): - # type: () -> Text - pass - @property - def clear_eos(self): - # type: () -> Text - pass From 4ec5d763a164c37f48ca18fcf584971592648366 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 May 2022 19:42:16 +0200 Subject: [PATCH 103/262] Ignore a mypy bug --- bpython/curtsiesfrontend/interpreter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index bf3f6ad8..f2a83678 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -51,7 +51,8 @@ def __init__( **options: Union[str, bool, None], ) -> None: self.f_strings = {k: f"\x01{v}" for k, v in color_scheme.items()} - super().__init__(**options) + # FIXME: mypy currently fails to handle this properly + super().__init__(**options) # type: ignore def format(self, tokensource, outfile): o = "" From df32e68b48b23d32073ecbfad36ff115d5fbade9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 May 2022 19:45:09 +0200 Subject: [PATCH 104/262] Some simplifications --- bpython/curtsiesfrontend/interpreter.py | 13 +++++-------- bpython/curtsiesfrontend/repl.py | 6 ++---- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index f2a83678..82e28091 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -94,9 +94,9 @@ def writetb(self, lines: Iterable[str]) -> None: # TODO for tracebacks get_lexer_by_name("pytb", stripall=True) def format(self, tbtext: str, lexer: Any) -> None: - # FIXME: lexer should is a Lexer + # FIXME: lexer should be "Lexer" traceback_informative_formatter = BPythonFormatter(default_colors) - traceback_code_formatter = BPythonFormatter({Token: ("d")}) + traceback_code_formatter = BPythonFormatter({Token: "d"}) no_format_mode = False cur_line = [] @@ -111,7 +111,7 @@ def format(self, tbtext: str, lexer: Any) -> None: cur_line, self.outfile ) cur_line = [] - elif text == " " and cur_line == []: + elif text == " " and len(cur_line) == 0: no_format_mode = True cur_line.append((token, text)) else: @@ -130,9 +130,6 @@ def code_finished_will_parse( False, True means code block is unfinished False, False isn't possible - an predicted error makes code block done""" try: - finished = bool(compiler(s)) - code_will_parse = True + return bool(compiler(s)), True except (ValueError, SyntaxError, OverflowError): - finished = True - code_will_parse = False - return finished, code_will_parse + return True, False diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index d5a3eed1..a29b350a 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1363,9 +1363,8 @@ def unhighlight_paren(self): def clear_current_block(self, remove_from_history=True): self.display_buffer = [] if remove_from_history: - for unused in self.buffer: - self.history.pop() - self.all_logical_lines.pop() + del self.history[-len(self.buffer) :] + del self.all_logical_lines[-len(self.buffer) :] self.buffer = [] self.cursor_offset = 0 self.saved_indent = 0 @@ -2080,7 +2079,6 @@ def focus_on_subprocess(self, args): try: signal.signal(signal.SIGWINCH, self.orig_sigwinch_handler) with Termmode(self.orig_stdin, self.orig_tcattrs): - assert self.window is not None terminal = self.window.t with terminal.fullscreen(): sys.__stdout__.write(terminal.save) From ebc920677874007ca7e6e548665cb6b312dddb30 Mon Sep 17 00:00:00 2001 From: Ulises Date: Fri, 27 May 2022 09:00:02 +0200 Subject: [PATCH 105/262] Allowing sys.stind.readline receive size parameter --- bpython/curtsiesfrontend/repl.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index a29b350a..bf723742 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -179,12 +179,17 @@ def add_input_character(self, e: str) -> None: ) self.cursor_offset += 1 - def readline(self): - self.has_focus = True - self.repl.send_to_stdin(self.current_line) - value = self.coderunner.request_from_main_context() - self.readline_results.append(value) - return value + def readline(self, size=-1): + if not isinstance(size, int): + raise TypeError(f"'{type(size).__name__}' object cannot be interpreted as an integer") + elif size == 0: + return '' + else: + self.has_focus = True + self.repl.send_to_stdin(self.current_line) + value = self.coderunner.request_from_main_context() + self.readline_results.append(value) + return value if size <= -1 else value[:size] def readlines(self, size=-1): return list(iter(self.readline, "")) From c0412f66be16cd0914d791aa4f2dfcdf85f499d4 Mon Sep 17 00:00:00 2001 From: Ulises Date: Fri, 27 May 2022 09:07:36 +0200 Subject: [PATCH 106/262] Applying black --- bpython/curtsiesfrontend/repl.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index bf723742..5b052435 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -181,9 +181,11 @@ def add_input_character(self, e: str) -> None: def readline(self, size=-1): if not isinstance(size, int): - raise TypeError(f"'{type(size).__name__}' object cannot be interpreted as an integer") + raise TypeError( + f"'{type(size).__name__}' object cannot be interpreted as an integer" + ) elif size == 0: - return '' + return "" else: self.has_focus = True self.repl.send_to_stdin(self.current_line) From 624a1e1abe9df081761f79bbaf8d9faa8843f320 Mon Sep 17 00:00:00 2001 From: Ulises Date: Sat, 28 May 2022 09:33:39 +0200 Subject: [PATCH 107/262] adding annotations and removing else statement --- bpython/curtsiesfrontend/repl.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 5b052435..16d8a8c8 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -179,19 +179,18 @@ def add_input_character(self, e: str) -> None: ) self.cursor_offset += 1 - def readline(self, size=-1): + def readline(self, size: int = -1) -> str: if not isinstance(size, int): raise TypeError( f"'{type(size).__name__}' object cannot be interpreted as an integer" ) elif size == 0: return "" - else: - self.has_focus = True - self.repl.send_to_stdin(self.current_line) - value = self.coderunner.request_from_main_context() - self.readline_results.append(value) - return value if size <= -1 else value[:size] + self.has_focus = True + self.repl.send_to_stdin(self.current_line) + value = self.coderunner.request_from_main_context() + self.readline_results.append(value) + return value if size <= -1 else value[:size] def readlines(self, size=-1): return list(iter(self.readline, "")) From c10ebb71ba54007f2665f7e86a966ed4b859729f Mon Sep 17 00:00:00 2001 From: Ulises Date: Sat, 28 May 2022 09:43:41 +0200 Subject: [PATCH 108/262] adjusting annotations --- bpython/curtsiesfrontend/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 16d8a8c8..20188f50 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -179,7 +179,7 @@ def add_input_character(self, e: str) -> None: ) self.cursor_offset += 1 - def readline(self, size: int = -1) -> str: + def readline(self, size: int = -1) -> Union[str, Any]: if not isinstance(size, int): raise TypeError( f"'{type(size).__name__}' object cannot be interpreted as an integer" From b1966098508393db0a90a6e5c32e0598c0dbfb6b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 28 May 2022 17:08:01 +0200 Subject: [PATCH 109/262] Assert that returned value is a str Anything else would be a bug. --- bpython/curtsiesfrontend/repl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 20188f50..dd7e9b68 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -179,7 +179,7 @@ def add_input_character(self, e: str) -> None: ) self.cursor_offset += 1 - def readline(self, size: int = -1) -> Union[str, Any]: + def readline(self, size: int = -1) -> str: if not isinstance(size, int): raise TypeError( f"'{type(size).__name__}' object cannot be interpreted as an integer" @@ -189,6 +189,7 @@ def readline(self, size: int = -1) -> Union[str, Any]: self.has_focus = True self.repl.send_to_stdin(self.current_line) value = self.coderunner.request_from_main_context() + assert isinstance(value, str) self.readline_results.append(value) return value if size <= -1 else value[:size] From 6bdeadde6b6705fb9f994d6f72fd591e6c7c46c4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 28 May 2022 17:21:14 +0200 Subject: [PATCH 110/262] Also handle size argument in readlines --- bpython/curtsiesfrontend/repl.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index dd7e9b68..c92bd57c 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -193,8 +193,22 @@ def readline(self, size: int = -1) -> str: self.readline_results.append(value) return value if size <= -1 else value[:size] - def readlines(self, size=-1): - return list(iter(self.readline, "")) + def readlines(self, size: Optional[int] = -1) -> List[str]: + if size is None: + # the default readlines implementation also accepts None + size = -1 + if not isinstance(size, int): + raise TypeError("argument should be integer or None, not 'str'") + if size <= 0: + # read as much as we can + return list(iter(self.readline, "")) + + lines = [] + while size > 0: + line = self.readline() + lines.append(line) + size -= len(line) + return lines def __iter__(self): return iter(self.readlines()) From b7e5d8d41dd86b9240eb835b451e6dd5747f9f4d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 28 May 2022 17:30:40 +0200 Subject: [PATCH 111/262] Call clear instead of recreating the instance --- bpython/curtsiesfrontend/filewatch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 8933e293..e70325ab 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -32,8 +32,8 @@ def __init__( super().__init__() def reset(self) -> None: - self.dirs = defaultdict(set) - del self.modules_to_add_later[:] + self.dirs.clear() + self.modules_to_add_later.clear() self.observer.unschedule_all() def _add_module(self, path: str) -> None: @@ -70,7 +70,7 @@ def activate(self) -> None: self.observer.schedule(self, dirname, recursive=False) for module in self.modules_to_add_later: self._add_module(module) - del self.modules_to_add_later[:] + self.modules_to_add_later.clear() self.activated = True def deactivate(self) -> None: From 0c1c24f210dc04efe4e4635acc76097dbeba94b4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 28 May 2022 17:31:24 +0200 Subject: [PATCH 112/262] Simplify some of the branches --- bpython/curtsiesfrontend/repl.py | 43 +++++++++++++------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index c92bd57c..3ef3f902 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -133,35 +133,28 @@ def process_event(self, e: Union[events.Event, str]) -> None: self.current_line = "" self.cursor_offset = 0 self.repl.run_code_and_maybe_finish() - else: - if e in self.rl_char_sequences: - self.cursor_offset, self.current_line = self.rl_char_sequences[ - e - ](self.cursor_offset, self.current_line) - elif e in ("",): - pass - elif e in ("",): - if self.current_line == "": - self.repl.send_to_stdin("\n") - self.has_focus = False - self.current_line = "" - self.cursor_offset = 0 - self.repl.run_code_and_maybe_finish(for_code="") - else: - pass - elif e in ("\n", "\r", "", ""): - line = self.current_line - self.repl.send_to_stdin(line + "\n") + elif e in self.rl_char_sequences: + self.cursor_offset, self.current_line = self.rl_char_sequences[e]( + self.cursor_offset, self.current_line + ) + elif e == "": + if not len(self.current_line): + self.repl.send_to_stdin("\n") self.has_focus = False self.current_line = "" self.cursor_offset = 0 - self.repl.run_code_and_maybe_finish(for_code=line + "\n") - else: # add normal character - self.add_input_character(e) + self.repl.run_code_and_maybe_finish(for_code="") + elif e in ("\n", "\r", "", ""): + line = f"{self.current_line}\n" + self.repl.send_to_stdin(line) + self.has_focus = False + self.current_line = "" + self.cursor_offset = 0 + self.repl.run_code_and_maybe_finish(for_code=line) + elif e != "": # add normal character + self.add_input_character(e) - if self.current_line.endswith(("\n", "\r")): - pass - else: + if not self.current_line.endswith(("\n", "\r")): self.repl.send_to_stdin(self.current_line) def add_input_character(self, e: str) -> None: From f076f326fe5f50454de0c0554bdce4f58d63fb61 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 28 May 2022 17:32:53 +0200 Subject: [PATCH 113/262] Add more type annotations --- bpython/curtsiesfrontend/repl.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 3ef3f902..c513d866 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -13,15 +13,15 @@ from enum import Enum from types import FrameType, TracebackType from typing import ( - Dict, Any, Iterable, + Dict, List, Optional, Sequence, Tuple, - Union, Type, + Union, ) from .._typing_compat import Literal @@ -55,7 +55,11 @@ Interp, code_finished_will_parse, ) -from .manual_readline import edit_keys, cursor_on_closing_char_pair +from .manual_readline import ( + edit_keys, + cursor_on_closing_char_pair, + AbstractEdits, +) from .parse import parse as bpythonparse, func_for_letter, color_for_letter from .preprocess import preprocess from .. import __version__ @@ -105,15 +109,20 @@ class FakeStdin: In user code, sys.stdin.read() asks the user for interactive input, so this class returns control to the UI to get that input.""" - def __init__(self, coderunner, repl, configured_edit_keys=None): + def __init__( + self, + coderunner: CodeRunner, + repl: "BaseRepl", + configured_edit_keys: Optional[AbstractEdits] = None, + ): self.coderunner = coderunner self.repl = repl self.has_focus = False # whether FakeStdin receives keypress events self.current_line = "" self.cursor_offset = 0 self.old_num_lines = 0 - self.readline_results = [] - if configured_edit_keys: + self.readline_results: List[str] = [] + if configured_edit_keys is not None: self.rl_char_sequences = configured_edit_keys else: self.rl_char_sequences = edit_keys @@ -236,7 +245,7 @@ class ReevaluateFakeStdin: """Stdin mock used during reevaluation (undo) so raw_inputs don't have to be reentered""" - def __init__(self, fakestdin, repl): + def __init__(self, fakestdin: FakeStdin, repl: "BaseRepl"): self.fakestdin = fakestdin self.repl = repl self.readline_results = fakestdin.readline_results[:] From 03f4ddb2299f052953037fd0f00d05ddefdc508c Mon Sep 17 00:00:00 2001 From: jgart <47760695+jgarte@users.noreply.github.com> Date: Tue, 21 Jun 2022 23:13:50 -0500 Subject: [PATCH 114/262] Add GNU Guix installation instructions --- README.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.rst b/README.rst index 6cfd5663..5e50e65a 100644 --- a/README.rst +++ b/README.rst @@ -146,6 +146,14 @@ Fedora users can install ``bpython`` directly from the command line using ``dnf` .. code-block:: bash $ dnf install bpython + +GNU Guix +---------- +Guix users can install ``bpython`` on any GNU/Linux distribution directly from the command line: + +.. code-block:: bash + + $ guix install bpython macOS ----- From 27665ec8679ea9f57a0100672d8ab21d354179be Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sun, 17 Jul 2022 08:41:46 +1000 Subject: [PATCH 115/262] docs: fix simple typo, blockied -> blocked There is a small typo in bpython/curtsiesfrontend/repl.py. Should read `blocked` rather than `blockied`. Signed-off-by: Tim Gates --- bpython/curtsiesfrontend/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index c513d866..ef708161 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -582,7 +582,7 @@ def request_reload(self, files_modified: Sequence[str] = ()) -> None: def schedule_refresh(self, when: float = 0) -> None: """Schedule a ScheduledRefreshRequestEvent for when. - Such a event should interrupt if blockied waiting for keyboard input""" + Such a event should interrupt if blocked waiting for keyboard input""" if self.reevaluating or self.paste_mode: self.fake_refresh_requested = True else: From 23c1945e465b5905572ebf733f64086505e5c027 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 25 Aug 2022 17:39:30 +0200 Subject: [PATCH 116/262] Add failing test for #966 --- bpython/test/test_inspection.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index fdbb959c..8c155623 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -140,6 +140,37 @@ def test_getfuncprops_numpy_array(self): # np.array(object, dtype=None, *, ...). self.assertEqual(props.argspec.args, ["object", "dtype"]) + @unittest.expectedFailure + def test_issue_966_freestanding(self): + def fun(number, lst=[]): + """ + Return a list of numbers + + Example: + ======== + C.cmethod(1337, [1, 2]) # => [1, 2, 1337] + """ + return lst + [number] + + def fun_annotations(number: int, lst: list[int] = []) -> list[int]: + """ + Return a list of numbers + + Example: + ======== + C.cmethod(1337, [1, 2]) # => [1, 2, 1337] + """ + return lst + [number] + + props = inspection.getfuncprops("fun", fun) + self.assertEqual(props.func, "fun") + self.assertEqual(props.argspec.args, ["number", "lst"]) + self.assertEqual(props.argspec.defaults[0], []) + + props = inspection.getfuncprops("fun_annotations", fun_annotations) + self.assertEqual(props.func, "fun_annotations") + self.assertEqual(props.argspec.args, ["number", "lst"]) + self.assertEqual(props.argspec.defaults[0], []) class A: a = "a" From 231adb15dc718fe10912286d360a15d14a59683c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 25 Aug 2022 17:41:29 +0200 Subject: [PATCH 117/262] Following handling of empty values according to the documentation --- bpython/inspection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 5ebfdbe8..67c7d037 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -292,12 +292,12 @@ def get_argspec_from_signature(f): signature = inspect.signature(f) for parameter in signature.parameters.values(): - if parameter.annotation is not inspect._empty: + if parameter.annotation is not parameter.empty: annotations[parameter.name] = parameter.annotation if parameter.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: args.append(parameter.name) - if parameter.default is not inspect._empty: + if parameter.default is not parameter.empty: defaults.append(parameter.default) elif parameter.kind == inspect.Parameter.POSITIONAL_ONLY: args.append(parameter.name) From 9bbb25d1125a69fe650ff36cd2201245055a60ff Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Aug 2022 23:51:59 +0200 Subject: [PATCH 118/262] Add another tests for function signatures --- bpython/test/test_inspection.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 8c155623..9110fca3 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -77,6 +77,13 @@ def spam(eggs=23, foobar="yay"): self.assertEqual(repr(defaults[0]), "23") self.assertEqual(repr(defaults[1]), "'yay'") + def test_pasekeywordpairs_annotation(self): + def spam(eggs: str = "foo, bar"): + pass + + defaults = inspection.getfuncprops("spam", spam).argspec.defaults + self.assertEqual(repr(defaults[0]), "'foo, bar'") + def test_get_encoding_ascii(self): self.assertEqual(inspection.get_encoding(encoding_ascii), "ascii") self.assertEqual(inspection.get_encoding(encoding_ascii.foo), "ascii") From c6e513c91a07a2b8ed91a73c96bff8591eaee080 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Aug 2022 23:52:46 +0200 Subject: [PATCH 119/262] Handle type annotations in function signatures --- bpython/inspection.py | 22 ++++++++++++++++------ bpython/test/test_inspection.py | 1 - 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 67c7d037..8f6773fb 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -26,7 +26,7 @@ import pydoc import re from collections import namedtuple -from typing import Any, Optional, Type +from typing import Any, Optional, Type, Dict, List from types import MemberDescriptorType, TracebackType from ._typing_compat import Literal @@ -121,15 +121,16 @@ def __repr__(self): __str__ = __repr__ -def parsekeywordpairs(signature): - tokens = Python3Lexer().get_tokens(signature) +def parsekeywordpairs(signature: str) -> Dict[str, str]: preamble = True stack = [] - substack = [] + substack: List[str] = [] parendepth = 0 - for token, value in tokens: + annotation = False + for token, value in Python3Lexer().get_tokens(signature): if preamble: if token is Token.Punctuation and value == "(": + # First "(" starts the list of arguments preamble = False continue @@ -141,14 +142,23 @@ def parsekeywordpairs(signature): elif value == ":" and parendepth == -1: # End of signature reached break + elif value == ":" and parendepth == 0: + # Start of type annotation + annotation = True + if (value == "," and parendepth == 0) or ( value == ")" and parendepth == -1 ): stack.append(substack) substack = [] + # If type annotation didn't end before, ti does now. + annotation = False continue + elif token is Token.Operator and value == "=" and parendepth == 0: + # End of type annotation + annotation = False - if value and (parendepth > 0 or value.strip()): + if value and not annotation and (parendepth > 0 or value.strip()): substack.append(value) return {item[0]: "".join(item[2:]) for item in stack if len(item) >= 3} diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 9110fca3..333abbbd 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -147,7 +147,6 @@ def test_getfuncprops_numpy_array(self): # np.array(object, dtype=None, *, ...). self.assertEqual(props.argspec.args, ["object", "dtype"]) - @unittest.expectedFailure def test_issue_966_freestanding(self): def fun(number, lst=[]): """ From c2e3faca367162dddf1e3f7cd062a5cb770db7cf Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 27 Aug 2022 09:33:49 +0200 Subject: [PATCH 120/262] Fix type annotation --- bpython/test/test_inspection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 333abbbd..ba9ffca7 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -2,6 +2,7 @@ import os import sys import unittest +from typing import List from bpython import inspection from bpython.test.fodder import encoding_ascii @@ -158,7 +159,7 @@ def fun(number, lst=[]): """ return lst + [number] - def fun_annotations(number: int, lst: list[int] = []) -> list[int]: + def fun_annotations(number: int, lst: List[int] = []) -> List[int]: """ Return a list of numbers From d42b21aaef45b9fec0519fbf044354ec81986c74 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 18:09:23 +0200 Subject: [PATCH 121/262] Turn FuncProps into a dataclass --- bpython/autocomplete.py | 6 +++--- bpython/cli.py | 6 +++--- bpython/inspection.py | 15 +++++++++++---- bpython/test/test_autocomplete.py | 6 +++--- bpython/urwid.py | 4 +++- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 81cea894..e623359e 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -570,11 +570,11 @@ def matches( matches = { f"{name}=" - for name in argspec[1][0] + for name in argspec.argspec[0] if isinstance(name, str) and name.startswith(r.word) } matches.update( - name + "=" for name in argspec[1][4] if name.startswith(r.word) + name + "=" for name in argspec.argspec[4] if name.startswith(r.word) ) return matches if matches else None @@ -711,7 +711,7 @@ def get_completer( line is a string of the current line kwargs (all optional): locals_ is a dictionary of the environment - argspec is an inspect.ArgSpec instance for the current function where + argspec is an inspect.FuncProps instance for the current function where the cursor is current_block is the possibly multiline not-yet-evaluated block of code which the current line is part of diff --git a/bpython/cli.py b/bpython/cli.py index 2512d6ff..ba85729f 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -101,7 +101,7 @@ from . import translations from .translations import _ -from . import repl +from . import repl, inspection from . import args as bpargs from .pager import page from .args import parse as argsparse @@ -726,7 +726,7 @@ def lf(self) -> None: def mkargspec( self, - topline: Any, # Named tuples don't seem to play nice with mypy + topline: inspection.FuncProps, in_arg: Union[str, int, None], down: bool, ) -> int: @@ -1298,7 +1298,7 @@ def show_list( self, items: List[str], arg_pos: Union[str, int, None], - topline: Any = None, # Named tuples don't play nice with mypy + topline: Optional[inspection.FuncProps] = None, formatter: Optional[Callable] = None, current_item: Union[str, Literal[False]] = None, ) -> None: diff --git a/bpython/inspection.py b/bpython/inspection.py index 8f6773fb..4bd67b4f 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -26,7 +26,8 @@ import pydoc import re from collections import namedtuple -from typing import Any, Optional, Type, Dict, List +from dataclasses import dataclass +from typing import Any, Callable, Optional, Type, Dict, List from types import MemberDescriptorType, TracebackType from ._typing_compat import Literal @@ -35,6 +36,7 @@ from .lazyre import LazyReCompile + ArgSpec = namedtuple( "ArgSpec", [ @@ -48,7 +50,12 @@ ], ) -FuncProps = namedtuple("FuncProps", ["func", "argspec", "is_bound_method"]) + +@dataclass +class FuncProps: + func: str + argspec: ArgSpec + is_bound_method: bool class AttrCleaner: @@ -112,10 +119,10 @@ class _Repr: Helper for `fixlongargs()`: Returns the given value in `__repr__()`. """ - def __init__(self, value): + def __init__(self, value: str) -> None: self.value = value - def __repr__(self): + def __repr__(self) -> str: return self.value __str__ = __repr__ diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 8b171d03..bfc3b830 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -11,7 +11,7 @@ except ImportError: has_jedi = False -from bpython import autocomplete +from bpython import autocomplete, inspection from bpython.line import LinePart glob_function = "glob.iglob" @@ -418,8 +418,8 @@ def test_set_of_params_returns_when_matches_found(self): def func(apple, apricot, banana, carrot): pass - argspec = list(inspect.getfullargspec(func)) - argspec = ["func", argspec, False] + argspec = inspection.ArgSpec(*inspect.getfullargspec(func)) + argspec = inspection.FuncProps("func", argspec, False) com = autocomplete.ParameterNameCompletion() self.assertSetEqual( com.matches(1, "a", argspec=argspec), {"apple=", "apricot="} diff --git a/bpython/urwid.py b/bpython/urwid.py index 0a2ffe31..6b329e65 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -762,7 +762,9 @@ def _populate_completion(self): if self.complete(): if self.funcprops: # This is mostly just stolen from the cli module. - func_name, args, is_bound = self.funcprops + func_name = self.funcprops.func + args = self.funcprops.argspec + is_bound = self.funcprops.is_bound_method in_arg = self.arg_pos args, varargs, varkw, defaults = args[:4] kwonly = self.funcprops.argspec.kwonly From ba1dac78d1623a4b9a2498a7f6b03ca7c6b4e01f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 18:09:48 +0200 Subject: [PATCH 122/262] Remove an if block that is never executed --- bpython/inspection.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 4bd67b4f..895756b5 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -270,8 +270,6 @@ def getfuncprops(func, f): try: argspec = get_argspec_from_signature(f) fixlongargs(f, argspec) - if len(argspec) == 4: - argspec = argspec + [list(), dict(), None] argspec = ArgSpec(*argspec) fprops = FuncProps(func, argspec, is_bound_method) except (TypeError, KeyError, ValueError): From 84a54677b92c6d9c124653e9c28a57ff3339312a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 18:25:49 +0200 Subject: [PATCH 123/262] Turn ArgSpec into a dataclass --- bpython/autocomplete.py | 4 +-- bpython/inspection.py | 69 ++++++++++++++++------------------------- bpython/urwid.py | 6 ++-- 3 files changed, 33 insertions(+), 46 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index e623359e..9acc95eb 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -570,11 +570,11 @@ def matches( matches = { f"{name}=" - for name in argspec.argspec[0] + for name in argspec.argspec.args if isinstance(name, str) and name.startswith(r.word) } matches.update( - name + "=" for name in argspec.argspec[4] if name.startswith(r.word) + name + "=" for name in argspec.argspec.kwonly if name.startswith(r.word) ) return matches if matches else None diff --git a/bpython/inspection.py b/bpython/inspection.py index 895756b5..676c3aa3 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -25,7 +25,6 @@ import keyword import pydoc import re -from collections import namedtuple from dataclasses import dataclass from typing import Any, Callable, Optional, Type, Dict, List from types import MemberDescriptorType, TracebackType @@ -37,18 +36,15 @@ from .lazyre import LazyReCompile -ArgSpec = namedtuple( - "ArgSpec", - [ - "args", - "varargs", - "varkwargs", - "defaults", - "kwonly", - "kwonly_defaults", - "annotations", - ], -) +@dataclass +class ArgSpec: + args: List[str] + varargs: Optional[str] + varkwargs: Optional[str] + defaults: Optional[List[Any]] + kwonly: List[str] + kwonly_defaults: Optional[Dict[str, Any]] + annotations: Optional[Dict[str, Any]] @dataclass @@ -171,24 +167,24 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: return {item[0]: "".join(item[2:]) for item in stack if len(item) >= 3} -def fixlongargs(f, argspec): +def fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: """Functions taking default arguments that are references to other objects whose str() is too big will cause breakage, so we swap out the object itself with the name it was referenced with in the source by parsing the source itself !""" - if argspec[3] is None: + if argspec.defaults is None: # No keyword args, no need to do anything - return - values = list(argspec[3]) + return argspec + values = list(argspec.defaults) if not values: - return - keys = argspec[0][-len(values) :] + return argspec + keys = argspec.args[-len(values) :] try: src = inspect.getsourcelines(f) except (OSError, IndexError): # IndexError is raised in inspect.findsource(), can happen in # some situations. See issue #94. - return + return argspec signature = "".join(src[0]) kwparsed = parsekeywordpairs(signature) @@ -196,7 +192,8 @@ def fixlongargs(f, argspec): if len(repr(value)) != len(kwparsed[key]): values[i] = _Repr(kwparsed[key]) - argspec[3] = values + argspec.defaults = values + return argspec getpydocspec_re = LazyReCompile( @@ -247,7 +244,7 @@ def getpydocspec(f, func): ) -def getfuncprops(func, f): +def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: # Check if it's a real bound method or if it's implicitly calling __init__ # (i.e. FooClass(...) and not FooClass.__init__(...) -- the former would # not take 'self', the latter would: @@ -268,9 +265,8 @@ def getfuncprops(func, f): # '__init__' throws xmlrpclib.Fault (see #202) return None try: - argspec = get_argspec_from_signature(f) - fixlongargs(f, argspec) - argspec = ArgSpec(*argspec) + argspec = _get_argspec_from_signature(f) + argspec = fixlongargs(f, argspec) fprops = FuncProps(func, argspec, is_bound_method) except (TypeError, KeyError, ValueError): argspec = getpydocspec(f, func) @@ -289,7 +285,7 @@ def is_eval_safe_name(string: str) -> bool: ) -def get_argspec_from_signature(f): +def _get_argspec_from_signature(f: Callable) -> ArgSpec: """Get callable signature from inspect.signature in argspec format. inspect.signature is a Python 3 only function that returns the signature of @@ -324,26 +320,15 @@ def get_argspec_from_signature(f): elif parameter.kind == inspect.Parameter.VAR_KEYWORD: varkwargs = parameter.name - # inspect.getfullargspec returns None for 'defaults', 'kwonly_defaults' and - # 'annotations' if there are no values for them. - if not defaults: - defaults = None - - if not kwonly_defaults: - kwonly_defaults = None - - if not annotations: - annotations = None - - return [ + return ArgSpec( args, varargs, varkwargs, - defaults, + defaults if defaults else None, kwonly, - kwonly_defaults, - annotations, - ] + kwonly_defaults if kwonly_defaults else None, + annotations if annotations else None, + ) get_encoding_line_re = LazyReCompile(r"^.*coding[:=]\s*([-\w.]+).*$") diff --git a/bpython/urwid.py b/bpython/urwid.py index 6b329e65..cc828ff0 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -763,10 +763,12 @@ def _populate_completion(self): if self.funcprops: # This is mostly just stolen from the cli module. func_name = self.funcprops.func - args = self.funcprops.argspec + args = self.funcprops.argspec.args is_bound = self.funcprops.is_bound_method in_arg = self.arg_pos - args, varargs, varkw, defaults = args[:4] + varargs = self.funcprops.argspec.varargs + varkw = self.funcprops.argspec.varkwargs + defaults = self.funcprops.argspec.defaults kwonly = self.funcprops.argspec.kwonly kwonly_defaults = self.funcprops.argspec.kwonly_defaults or {} markup = [("bold name", func_name), ("name", ": (")] From 9861730ed39b5d9c9dfa97b53e337670d471f4a5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 18:34:55 +0200 Subject: [PATCH 124/262] Some clean up --- bpython/inspection.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 676c3aa3..ed201d9a 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -185,8 +185,7 @@ def fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: # IndexError is raised in inspect.findsource(), can happen in # some situations. See issue #94. return argspec - signature = "".join(src[0]) - kwparsed = parsekeywordpairs(signature) + kwparsed = parsekeywordpairs("".join(src[0])) for i, (key, value) in enumerate(zip(keys, values)): if len(repr(value)) != len(kwparsed[key]): @@ -201,7 +200,7 @@ def fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: ) -def getpydocspec(f, func): +def _getpydocspec(f: Callable) -> Optional[ArgSpec]: try: argspec = pydoc.getdoc(f) except NameError: @@ -218,7 +217,7 @@ def getpydocspec(f, func): defaults = [] varargs = varkwargs = None kwonly_args = [] - kwonly_defaults = dict() + kwonly_defaults = {} for arg in s.group(2).split(","): arg = arg.strip() if arg.startswith("**"): @@ -266,15 +265,14 @@ def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: return None try: argspec = _get_argspec_from_signature(f) - argspec = fixlongargs(f, argspec) - fprops = FuncProps(func, argspec, is_bound_method) + fprops = FuncProps(func, fixlongargs(f, argspec), is_bound_method) except (TypeError, KeyError, ValueError): - argspec = getpydocspec(f, func) - if argspec is None: + argspec_pydoc = _getpydocspec(f) + if argspec_pydoc is None: return None if inspect.ismethoddescriptor(f): - argspec.args.insert(0, "obj") - fprops = FuncProps(func, argspec, is_bound_method) + argspec_pydoc.args.insert(0, "obj") + fprops = FuncProps(func, argspec_pydoc, is_bound_method) return fprops From b54039db00aa622d131c681fc9acfd30dc4e1362 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 22:00:46 +0200 Subject: [PATCH 125/262] Make kwargs explicit with proper type annotations Also fix handling of complete_magic_methods. --- bpython/autocomplete.py | 119 ++++++++++++++++++++---------- bpython/test/test_autocomplete.py | 14 ++-- 2 files changed, 90 insertions(+), 43 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 9acc95eb..f8e97d73 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -28,6 +28,7 @@ import __main__ import abc import glob +import itertools import keyword import logging import os @@ -383,11 +384,15 @@ class AttrCompletion(BaseCompletionType): attr_matches_re = LazyReCompile(r"(\w+(\.\w+)*)\.(\w*)") def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + locals_: Optional[Dict[str, Any]] = None, + **kwargs: Any, ) -> Optional[Set]: - if "locals_" not in kwargs: + if locals_ is None: return None - locals_ = cast(Dict[str, Any], kwargs["locals_"]) r = self.locate(cursor_offset, line) if r is None: @@ -465,11 +470,15 @@ def list_attributes(self, obj: Any) -> List[str]: class DictKeyCompletion(BaseCompletionType): def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + locals_: Optional[Dict[str, Any]] = None, + **kwargs: Any, ) -> Optional[Set]: - if "locals_" not in kwargs: + if locals_ is None: return None - locals_ = kwargs["locals_"] r = self.locate(cursor_offset, line) if r is None: @@ -500,11 +509,16 @@ def format(self, match: str) -> str: class MagicMethodCompletion(BaseCompletionType): def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + current_block: Optional[str] = None, + complete_magic_methods: Optional[bool] = None, + **kwargs: Any, ) -> Optional[Set]: - if "current_block" not in kwargs: + if current_block is None or complete_magic_methods is None or not complete_magic_methods: return None - current_block = kwargs["current_block"] r = self.locate(cursor_offset, line) if r is None: @@ -519,15 +533,19 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: class GlobalCompletion(BaseCompletionType): def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + locals_: Optional[Dict[str, Any]] = None, + **kwargs: Any, ) -> Optional[Set]: """Compute matches when text is a simple name. Return a list of all keywords, built-in functions and names currently defined in self.namespace that match. """ - if "locals_" not in kwargs: + if locals_ is None: return None - locals_ = kwargs["locals_"] r = self.locate(cursor_offset, line) if r is None: @@ -556,25 +574,29 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: class ParameterNameCompletion(BaseCompletionType): def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + funcprops: Optional[inspection.FuncProps] = None, + **kwargs: Any, ) -> Optional[Set]: - if "argspec" not in kwargs: + if funcprops is None: return None - argspec = kwargs["argspec"] - if not argspec: - return None r = self.locate(cursor_offset, line) if r is None: return None matches = { f"{name}=" - for name in argspec.argspec.args + for name in funcprops.argspec.args if isinstance(name, str) and name.startswith(r.word) } matches.update( - name + "=" for name in argspec.argspec.kwonly if name.startswith(r.word) + name + "=" + for name in funcprops.argspec.kwonly + if name.startswith(r.word) ) return matches if matches else None @@ -588,12 +610,13 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return lineparts.current_expression_attribute(cursor_offset, line) def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + locals_: Optional[Dict[str, Any]] = None, + **kwargs: Any, ) -> Optional[Set]: - if "locals_" not in kwargs: - return None - locals_ = kwargs["locals_"] - if locals_ is None: locals_ = __main__.__dict__ @@ -629,20 +652,23 @@ class JediCompletion(BaseCompletionType): _orig_start: Optional[int] def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + history: Optional[List[str]] = None, + **kwargs: Any, ) -> Optional[Set]: - if "history" not in kwargs: + if history is None: return None - history = kwargs["history"] - if not lineparts.current_word(cursor_offset, line): return None - history = "\n".join(history) + "\n" + line + combined_history = "\n".join(itertools.chain(history, (line,))) try: - script = jedi.Script(history, path="fake.py") + script = jedi.Script(combined_history, path="fake.py") completions = script.complete( - len(history.splitlines()), cursor_offset + len(combined_history.splitlines()), cursor_offset ) except (jedi.NotFoundError, IndexError, KeyError): # IndexError for #483 @@ -679,12 +705,16 @@ def locate(self, cursor_offset: int, line: str) -> LinePart: class MultilineJediCompletion(JediCompletion): # type: ignore [no-redef] def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + current_block: Optional[str] = None, + history: Optional[List[str]] = None, + **kwargs: Any, ) -> Optional[Set]: - if "current_block" not in kwargs or "history" not in kwargs: + if current_block is None or history is None: return None - current_block = kwargs["current_block"] - history = kwargs["history"] if "\n" in current_block: assert cursor_offset <= len(line), "{!r} {!r}".format( @@ -701,7 +731,12 @@ def get_completer( completers: Sequence[BaseCompletionType], cursor_offset: int, line: str, - **kwargs: Any, + *, + locals_: Optional[Dict[str, Any]] = None, + argspec: Optional[inspection.FuncProps] = None, + history: Optional[List[str]] = None, + current_block: Optional[str] = None, + complete_magic_methods: Optional[bool] = None, ) -> Tuple[List[str], Optional[BaseCompletionType]]: """Returns a list of matches and an applicable completer @@ -711,7 +746,7 @@ def get_completer( line is a string of the current line kwargs (all optional): locals_ is a dictionary of the environment - argspec is an inspect.FuncProps instance for the current function where + argspec is an inspection.FuncProps instance for the current function where the cursor is current_block is the possibly multiline not-yet-evaluated block of code which the current line is part of @@ -721,7 +756,15 @@ def get_completer( for completer in completers: try: - matches = completer.matches(cursor_offset, line, **kwargs) + matches = completer.matches( + cursor_offset, + line, + locals_=locals_, + funcprops=argspec, + history=history, + current_block=current_block, + complete_magic_methods=complete_magic_methods, + ) except Exception as e: # Instead of crashing the UI, log exceptions from autocompleters. logger = logging.getLogger(__name__) diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index bfc3b830..88e99054 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -324,7 +324,7 @@ def test_magic_methods_complete_after_double_underscores(self): com = autocomplete.MagicMethodCompletion() block = "class Something(object)\n def __" self.assertSetEqual( - com.matches(10, " def __", current_block=block), + com.matches(10, " def __", current_block=block, complete_magic_methods=True), set(autocomplete.MAGIC_METHODS), ) @@ -419,10 +419,14 @@ def func(apple, apricot, banana, carrot): pass argspec = inspection.ArgSpec(*inspect.getfullargspec(func)) - argspec = inspection.FuncProps("func", argspec, False) + funcspec = inspection.FuncProps("func", argspec, False) com = autocomplete.ParameterNameCompletion() self.assertSetEqual( - com.matches(1, "a", argspec=argspec), {"apple=", "apricot="} + com.matches(1, "a", funcprops=funcspec), {"apple=", "apricot="} + ) + self.assertSetEqual( + com.matches(2, "ba", funcprops=funcspec), {"banana="} + ) + self.assertSetEqual( + com.matches(3, "car", funcprops=funcspec), {"carrot="} ) - self.assertSetEqual(com.matches(2, "ba", argspec=argspec), {"banana="}) - self.assertSetEqual(com.matches(3, "car", argspec=argspec), {"carrot="}) From 15338a2515c9e33ea24852b66bb56e84a2dca154 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 22:06:48 +0200 Subject: [PATCH 126/262] Apply black --- bpython/autocomplete.py | 6 +++++- bpython/test/test_autocomplete.py | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index f8e97d73..36e17543 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -517,7 +517,11 @@ def matches( complete_magic_methods: Optional[bool] = None, **kwargs: Any, ) -> Optional[Set]: - if current_block is None or complete_magic_methods is None or not complete_magic_methods: + if ( + current_block is None + or complete_magic_methods is None + or not complete_magic_methods + ): return None r = self.locate(cursor_offset, line) diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 88e99054..c95328fb 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -324,7 +324,12 @@ def test_magic_methods_complete_after_double_underscores(self): com = autocomplete.MagicMethodCompletion() block = "class Something(object)\n def __" self.assertSetEqual( - com.matches(10, " def __", current_block=block, complete_magic_methods=True), + com.matches( + 10, + " def __", + current_block=block, + complete_magic_methods=True, + ), set(autocomplete.MAGIC_METHODS), ) From 12e46590e71e4039faaf7ed467b14972179d25dd Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 28 May 2022 17:44:21 +0200 Subject: [PATCH 127/262] Use super --- bpdb/debugger.py | 4 ++-- bpython/cli.py | 3 +-- bpython/urwid.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/bpdb/debugger.py b/bpdb/debugger.py index b98e9612..3e5bbc91 100644 --- a/bpdb/debugger.py +++ b/bpdb/debugger.py @@ -28,14 +28,14 @@ class BPdb(pdb.Pdb): """PDB with BPython support.""" def __init__(self, *args, **kwargs): - pdb.Pdb.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) self.prompt = "(BPdb) " self.intro = 'Use "B" to enter bpython, Ctrl-d to exit it.' def postloop(self): # We only want to show the intro message once. self.intro = None - pdb.Pdb.postloop(self) + super().postloop() # cmd.Cmd commands diff --git a/bpython/cli.py b/bpython/cli.py index ba85729f..886dc2c8 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -1128,8 +1128,7 @@ def push(self, s: str, insert_into_history: bool = True) -> bool: # curses.raw(True) prevents C-c from causing a SIGINT curses.raw(False) try: - x: bool = repl.Repl.push(self, s, insert_into_history) - return x + return super().push(s, insert_into_history) except SystemExit as e: # Avoid a traceback on e.g. quit() self.do_exit = True diff --git a/bpython/urwid.py b/bpython/urwid.py index cc828ff0..66054097 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -938,7 +938,7 @@ def push(self, s, insert_into_history=True): signal.signal(signal.SIGINT, signal.default_int_handler) # Pretty blindly adapted from bpython.cli try: - return repl.Repl.push(self, s, insert_into_history) + return super().push(s, insert_into_history) except SystemExit as e: self.exit_value = e.args raise urwid.ExitMainLoop() From df03931ad5dd516ed7ab0498d5c07024af204e1c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 22:23:12 +0200 Subject: [PATCH 128/262] Fix mypy and black regressions --- bpython/autocomplete.py | 6 +----- bpython/repl.py | 4 ++-- bpython/test/test_inspection.py | 1 + 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 36e17543..3dbf80bd 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -39,14 +39,13 @@ from enum import Enum from typing import ( Any, - cast, Dict, Iterator, List, Optional, + Sequence, Set, Tuple, - Sequence, ) from . import inspection from . import line as lineparts @@ -391,9 +390,6 @@ def matches( locals_: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Optional[Set]: - if locals_ is None: - return None - r = self.locate(cursor_offset, line) if r is None: return None diff --git a/bpython/repl.py b/bpython/repl.py index e41536fd..6e593b2b 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -588,7 +588,7 @@ def current_string(self, concatenate=False): def get_object(self, name): attributes = name.split(".") - obj = eval(attributes.pop(0), self.interp.locals) + obj = eval(attributes.pop(0), cast(Dict[str, Any], self.interp.locals)) while attributes: with inspection.AttrCleaner(obj): obj = getattr(obj, attributes.pop(0)) @@ -783,7 +783,7 @@ def complete(self, tab: bool = False) -> Optional[bool]: self.completers, cursor_offset=self.cursor_offset, line=self.current_line, - locals_=self.interp.locals, + locals_=cast(Dict[str, Any], self.interp.locals), argspec=self.funcprops, current_block="\n".join(self.buffer + [self.current_line]), complete_magic_methods=self.config.complete_magic_methods, diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index ba9ffca7..b7e2d9b5 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -179,6 +179,7 @@ def fun_annotations(number: int, lst: List[int] = []) -> List[int]: self.assertEqual(props.argspec.args, ["number", "lst"]) self.assertEqual(props.argspec.defaults[0], []) + class A: a = "a" From 4a55b9e4ffc45e4f3c33fe30ff5ce28f4d65bc9b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 23:34:35 +0200 Subject: [PATCH 129/262] Bump curtsies to 0.4.0 --- README.rst | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 5e50e65a..dd307d33 100644 --- a/README.rst +++ b/README.rst @@ -91,7 +91,7 @@ your config file as **~/.config/bpython/config** (i.e. Dependencies ============ * Pygments -* curtsies >= 0.3.5 +* curtsies >= 0.4.0 * greenlet * pyxdg * requests diff --git a/requirements.txt b/requirements.txt index 7f56dc0f..ba8b126d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Pygments backports.cached-property; python_version < "3.8" -curtsies >=0.3.5 +curtsies >=0.4.0 cwcwidth greenlet pyxdg diff --git a/setup.cfg b/setup.cfg index f5c1bc84..748ce965 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ packages = bpdb install_requires = backports.cached-property; python_version < "3.8" - curtsies >=0.3.5 + curtsies >=0.4.0 cwcwidth greenlet pygments From ddd4321d0a9b27b9a2a3efc6a23d150849f383ac Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 08:10:08 +0200 Subject: [PATCH 130/262] Hide implementation details --- bpython/inspection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index ed201d9a..3d609605 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -167,7 +167,7 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: return {item[0]: "".join(item[2:]) for item in stack if len(item) >= 3} -def fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: +def _fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: """Functions taking default arguments that are references to other objects whose str() is too big will cause breakage, so we swap out the object itself with the name it was referenced with in the source by parsing the @@ -195,7 +195,7 @@ def fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: return argspec -getpydocspec_re = LazyReCompile( +_getpydocspec_re = LazyReCompile( r"([a-zA-Z_][a-zA-Z0-9_]*?)\((.*?)\)", re.DOTALL ) @@ -206,7 +206,7 @@ def _getpydocspec(f: Callable) -> Optional[ArgSpec]: except NameError: return None - s = getpydocspec_re.search(argspec) + s = _getpydocspec_re.search(argspec) if s is None: return None @@ -265,7 +265,7 @@ def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: return None try: argspec = _get_argspec_from_signature(f) - fprops = FuncProps(func, fixlongargs(f, argspec), is_bound_method) + fprops = FuncProps(func, _fixlongargs(f, argspec), is_bound_method) except (TypeError, KeyError, ValueError): argspec_pydoc = _getpydocspec(f) if argspec_pydoc is None: From d48a3d2401c0f557a0d1d34539a542a206d1d480 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 08:14:18 +0200 Subject: [PATCH 131/262] Use getattr_safe instead of AttrCleaner This attribute lookup is covered by getattr_safe. --- bpython/repl.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 6e593b2b..8aea4e17 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -586,12 +586,11 @@ def current_string(self, concatenate=False): return "" return "".join(string) - def get_object(self, name): + def get_object(self, name: str) -> Any: attributes = name.split(".") obj = eval(attributes.pop(0), cast(Dict[str, Any], self.interp.locals)) while attributes: - with inspection.AttrCleaner(obj): - obj = getattr(obj, attributes.pop(0)) + obj = inspection.getattr_safe(obj, attributes.pop(0)) return obj @classmethod From e59e924c57c72c732180b134ba938307cf45f756 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 09:10:30 +0200 Subject: [PATCH 132/262] Add another failing test for #966 --- bpython/test/test_inspection.py | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index b7e2d9b5..0dfaab89 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -2,6 +2,7 @@ import os import sys import unittest +from collections.abc import Sequence from typing import List from bpython import inspection @@ -179,6 +180,44 @@ def fun_annotations(number: int, lst: List[int] = []) -> List[int]: self.assertEqual(props.argspec.args, ["number", "lst"]) self.assertEqual(props.argspec.defaults[0], []) + @unittest.expectedFailure + def test_issue_966_class_method(self): + class Issue966(Sequence): + @classmethod + def cmethod(cls, number: int, lst: List[int] = []): + """ + Return a list of numbers + + Example: + ======== + C.cmethod(1337, [1, 2]) # => [1, 2, 1337] + """ + return lst + [number] + + @classmethod + def bmethod(cls, number, lst): + """ + Return a list of numbers + + Example: + ======== + C.cmethod(1337, [1, 2]) # => [1, 2, 1337] + """ + return lst + [number] + + props = inspection.getfuncprops( + "bmethod", inspection.getattr_safe(Issue966, "bmethod") + ) + self.assertEqual(props.func, "bmethod") + self.assertEqual(props.argspec.args, ["number", "lst"]) + + props = inspection.getfuncprops( + "cmethod", inspection.getattr_safe(Issue966, "cmethod") + ) + self.assertEqual(props.func, "cmethod") + self.assertEqual(props.argspec.args, ["number", "lst"]) + self.assertEqual(props.argspec.defaults[0], []) + class A: a = "a" From fb923fde21b45fa96c6cc75a24034faac74837e4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 18:17:36 +0200 Subject: [PATCH 133/262] Hide implementation details --- bpython/inspection.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 3d609605..a11298e3 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -59,14 +59,12 @@ class AttrCleaner: on attribute lookup.""" def __init__(self, obj: Any) -> None: - self.obj = obj + self._obj = obj def __enter__(self) -> None: """Try to make an object not exhibit side-effects on attribute lookup.""" - type_ = type(self.obj) - __getattribute__ = None - __getattr__ = None + type_ = type(self._obj) # Dark magic: # If __getattribute__ doesn't exist on the class and __getattr__ does # then __getattr__ will be called when doing @@ -89,7 +87,7 @@ def __enter__(self) -> None: except TypeError: # XXX: This happens for e.g. built-in types __getattribute__ = None - self.attribs = (__getattribute__, __getattr__) + self._attribs = (__getattribute__, __getattr__) # /Dark magic def __exit__( @@ -99,8 +97,8 @@ def __exit__( exc_tb: Optional[TracebackType], ) -> Literal[False]: """Restore an object's magic methods.""" - type_ = type(self.obj) - __getattribute__, __getattr__ = self.attribs + type_ = type(self._obj) + __getattribute__, __getattr__ = self._attribs # Dark magic: if __getattribute__ is not None: setattr(type_, "__getattribute__", __getattribute__) @@ -329,13 +327,13 @@ def _get_argspec_from_signature(f: Callable) -> ArgSpec: ) -get_encoding_line_re = LazyReCompile(r"^.*coding[:=]\s*([-\w.]+).*$") +_get_encoding_line_re = LazyReCompile(r"^.*coding[:=]\s*([-\w.]+).*$") def get_encoding(obj) -> str: """Try to obtain encoding information of the source of an object.""" for line in inspect.findsource(obj)[0][:2]: - m = get_encoding_line_re.search(line) + m = _get_encoding_line_re.search(line) if m: return m.group(1) return "utf8" @@ -344,9 +342,9 @@ def get_encoding(obj) -> str: def get_encoding_file(fname: str) -> str: """Try to obtain encoding information from a Python source file.""" with open(fname, encoding="ascii", errors="ignore") as f: - for unused in range(2): + for _ in range(2): line = f.readline() - match = get_encoding_line_re.search(line) + match = _get_encoding_line_re.search(line) if match: return match.group(1) return "utf8" From 3ad203dc1ec9e27e2e2408dd8c297bcb3c35f52a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 18:17:47 +0200 Subject: [PATCH 134/262] Implement ContextManager for AttrCleaner Also extend some documentation. --- bpython/autocomplete.py | 5 +++-- bpython/inspection.py | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 3dbf80bd..782b8b87 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -458,8 +458,9 @@ def attr_lookup(self, obj: Any, expr: str, attr: str) -> List: return matches def list_attributes(self, obj: Any) -> List[str]: - # TODO: re-implement dir using getattr_static to avoid using - # AttrCleaner here? + # TODO: re-implement dir without AttrCleaner here + # + # Note: accessing `obj.__dir__` via `getattr_static` is not side-effect free. with inspection.AttrCleaner(obj): return dir(obj) diff --git a/bpython/inspection.py b/bpython/inspection.py index a11298e3..4268f4fb 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -26,7 +26,7 @@ import pydoc import re from dataclasses import dataclass -from typing import Any, Callable, Optional, Type, Dict, List +from typing import Any, Callable, Optional, Type, Dict, List, ContextManager from types import MemberDescriptorType, TracebackType from ._typing_compat import Literal @@ -54,9 +54,11 @@ class FuncProps: is_bound_method: bool -class AttrCleaner: +class AttrCleaner(ContextManager[None]): """A context manager that tries to make an object not exhibit side-effects - on attribute lookup.""" + on attribute lookup. + + Unless explicitely required, prefer `getattr_safe`.""" def __init__(self, obj: Any) -> None: self._obj = obj @@ -351,7 +353,7 @@ def get_encoding_file(fname: str) -> str: def getattr_safe(obj: Any, name: str) -> Any: - """side effect free getattr (calls getattr_static).""" + """Side effect free getattr (calls getattr_static).""" result = inspect.getattr_static(obj, name) # Slots are a MemberDescriptorType if isinstance(result, MemberDescriptorType): From 1332d18b53fbe99f2808febd035dbbb4647c02c9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 18:20:44 +0200 Subject: [PATCH 135/262] Test inspection with static methods This is another variant of the class from #966. --- bpython/test/test_inspection.py | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 0dfaab89..3cb87d39 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -218,6 +218,43 @@ def bmethod(cls, number, lst): self.assertEqual(props.argspec.args, ["number", "lst"]) self.assertEqual(props.argspec.defaults[0], []) + def test_issue_966_static_method(self): + class Issue966(Sequence): + @staticmethod + def cmethod(number: int, lst: List[int] = []): + """ + Return a list of numbers + + Example: + ======== + C.cmethod(1337, [1, 2]) # => [1, 2, 1337] + """ + return lst + [number] + + @staticmethod + def bmethod(number, lst): + """ + Return a list of numbers + + Example: + ======== + C.cmethod(1337, [1, 2]) # => [1, 2, 1337] + """ + return lst + [number] + + props = inspection.getfuncprops( + "bmethod", inspection.getattr_safe(Issue966, "bmethod") + ) + self.assertEqual(props.func, "bmethod") + self.assertEqual(props.argspec.args, ["number", "lst"]) + + props = inspection.getfuncprops( + "cmethod", inspection.getattr_safe(Issue966, "cmethod") + ) + self.assertEqual(props.func, "cmethod") + self.assertEqual(props.argspec.args, ["number", "lst"]) + self.assertEqual(props.argspec.defaults[0], []) + class A: a = "a" From 1bc255b87dddf78cfef204b68497ea71ba0b10f8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 18:24:02 +0200 Subject: [PATCH 136/262] Directly access class methods in introspection (fixes #966) inspect.signature is unable to process classmethod instances. So we directly access the member via its __get__. --- bpython/inspection.py | 3 +++ bpython/test/test_inspection.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 4268f4fb..6914e5a6 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -358,6 +358,9 @@ def getattr_safe(obj: Any, name: str) -> Any: # Slots are a MemberDescriptorType if isinstance(result, MemberDescriptorType): result = getattr(obj, name) + # classmethods are safe to access (see #966) + if isinstance(result, classmethod): + result = result.__get__(obj, obj) return result diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 3cb87d39..43915f3e 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -180,7 +180,6 @@ def fun_annotations(number: int, lst: List[int] = []) -> List[int]: self.assertEqual(props.argspec.args, ["number", "lst"]) self.assertEqual(props.argspec.defaults[0], []) - @unittest.expectedFailure def test_issue_966_class_method(self): class Issue966(Sequence): @classmethod From 8db689848c68796ff1d3b7cc5e82e597a6cfb2ad Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 18:39:23 +0200 Subject: [PATCH 137/262] Update changelog --- CHANGELOG.rst | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b3700a45..0c12b5fd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,13 +6,29 @@ Changelog General information: +* More and more type annotations have been added to the bpython code base. +* Some work has been performed to stop relying on blessings. + New features: -* Auto-closing brackets option added. To enable, add `brackets_completion = True` in the bpython config (press F3 to create) + +* #905: Auto-closing brackets option added. To enable, add `brackets_completion = True` in the bpython config Thanks to samuelgregorovic Fixes: -* Support for Python 3.6 has been dropped. +* Improve handling of SyntaxErrors +* #948: Fix crash on Ctrl-Z +* #952: Fix tests for Python 3.10.1 and newer +* #955: Handle optional `readline` parameters in `stdin` emulation + Thanks to thevibingcat +* #959: Fix handling of `__name__` +* #966: Fix function signature completion for `classmethod`s + +Changes to dependencies: + +* curtsies 0.4 or newer is now required + +Support for Python 3.6 has been dropped. 0.22.1 ------ From 265dd4fbb9a3996bbe5d1572ac57ff3145868587 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 18:45:45 +0200 Subject: [PATCH 138/262] Fix a typo --- bpython/inspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 6914e5a6..b45d49f9 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -58,7 +58,7 @@ class AttrCleaner(ContextManager[None]): """A context manager that tries to make an object not exhibit side-effects on attribute lookup. - Unless explicitely required, prefer `getattr_safe`.""" + Unless explicitly required, prefer `getattr_safe`.""" def __init__(self, obj: Any) -> None: self._obj = obj From c079356b426a7c6650ef45d6c2bb4cf63d6348d3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 20:04:45 +0200 Subject: [PATCH 139/262] Also directly access staticmethods --- bpython/inspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index b45d49f9..3efacd5b 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -359,7 +359,7 @@ def getattr_safe(obj: Any, name: str) -> Any: if isinstance(result, MemberDescriptorType): result = getattr(obj, name) # classmethods are safe to access (see #966) - if isinstance(result, classmethod): + if isinstance(result, (classmethod, staticmethod)): result = result.__get__(obj, obj) return result From 424345e0db2dd5024dce3def1079901f0655cccc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 20:04:55 +0200 Subject: [PATCH 140/262] Improve parenthesis checks --- bpython/inspection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 3efacd5b..6b9074c0 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -138,9 +138,9 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: continue if token is Token.Punctuation: - if value in ("(", "{", "["): + if value in "({[": parendepth += 1 - elif value in (")", "}", "]"): + elif value in ")}]": parendepth -= 1 elif value == ":" and parendepth == -1: # End of signature reached From 167fc26d4beae59d02235259e8cdec631a633970 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 21:53:39 +0200 Subject: [PATCH 141/262] Hide implementation details --- bpython/autocomplete.py | 15 +++++++-------- bpython/test/test_autocomplete.py | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 782b8b87..ddb47239 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -176,11 +176,11 @@ def from_string(cls, value: str) -> Optional["AutocompleteModes"]: KEYWORDS = frozenset(keyword.kwlist) -def after_last_dot(name: str) -> str: +def _after_last_dot(name: str) -> str: return name.rstrip(".").rsplit(".")[-1] -def few_enough_underscores(current: str, match: str) -> bool: +def _few_enough_underscores(current: str, match: str) -> bool: """Returns whether match should be shown based on current if current is _, True if match starts with 0 or 1 underscore @@ -340,7 +340,7 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return lineparts.current_word(cursor_offset, line) def format(self, word: str) -> str: - return after_last_dot(word) + return _after_last_dot(word) def _safe_glob(pathname: str) -> Iterator[str]: @@ -409,14 +409,14 @@ def matches( return { m for m in matches - if few_enough_underscores(r.word.split(".")[-1], m.split(".")[-1]) + if _few_enough_underscores(r.word.split(".")[-1], m.split(".")[-1]) } def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return lineparts.current_dotted_attribute(cursor_offset, line) def format(self, word: str) -> str: - return after_last_dot(word) + return _after_last_dot(word) def attr_matches(self, text: str, namespace: Dict[str, Any]) -> List: """Taken from rlcompleter.py and bent to my will.""" @@ -434,8 +434,7 @@ def attr_matches(self, text: str, namespace: Dict[str, Any]) -> List: obj = safe_eval(expr, namespace) except EvaluationError: return [] - matches = self.attr_lookup(obj, expr, attr) - return matches + return self.attr_lookup(obj, expr, attr) def attr_lookup(self, obj: Any, expr: str, attr: str) -> List: """Second half of attr_matches.""" @@ -631,7 +630,7 @@ def matches( # strips leading dot matches = (m[1:] for m in self.attr_lookup(obj, "", attr.word)) - return {m for m in matches if few_enough_underscores(attr.word, m)} + return {m for m in matches if _few_enough_underscores(attr.word, m)} try: diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index c95328fb..0000b0b6 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -35,7 +35,7 @@ def test_filename(self): self.assertEqual(last_part_of_filename("ab.c/e.f.g/"), "e.f.g/") def test_attribute(self): - self.assertEqual(autocomplete.after_last_dot("abc.edf"), "edf") + self.assertEqual(autocomplete._after_last_dot("abc.edf"), "edf") def completer(matches): From 0c40b67910c68a7d6bb73bb64c4b5e7235956b3b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 21:53:54 +0200 Subject: [PATCH 142/262] Refactor --- bpython/autocomplete.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index ddb47239..45ae8ffd 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -715,17 +715,15 @@ def matches( ) -> Optional[Set]: if current_block is None or history is None: return None - - if "\n" in current_block: - assert cursor_offset <= len(line), "{!r} {!r}".format( - cursor_offset, - line, - ) - results = super().matches(cursor_offset, line, history=history) - return results - else: + if "\n" not in current_block: return None + assert cursor_offset <= len(line), "{!r} {!r}".format( + cursor_offset, + line, + ) + return super().matches(cursor_offset, line, history=history) + def get_completer( completers: Sequence[BaseCompletionType], From cd20fd7ddc4f392f6655b49795cb3a39ba9383cc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 21:54:25 +0200 Subject: [PATCH 143/262] Remove unused import --- bpython/lazyre.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 8f1e7099..0ca5b9ff 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -21,7 +21,7 @@ # THE SOFTWARE. import re -from typing import Optional, Iterator, Pattern, Match, Optional +from typing import Optional, Pattern, Match, Optional try: from functools import cached_property From beb1994d0c46453a65a5989df2cadcc5fb9c5165 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 21:53:48 +0200 Subject: [PATCH 144/262] Avoid allocation of a list The list is later turned into a set. --- bpython/autocomplete.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 45ae8ffd..22a514b6 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -418,25 +418,27 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: def format(self, word: str) -> str: return _after_last_dot(word) - def attr_matches(self, text: str, namespace: Dict[str, Any]) -> List: + def attr_matches( + self, text: str, namespace: Dict[str, Any] + ) -> Iterator[str]: """Taken from rlcompleter.py and bent to my will.""" m = self.attr_matches_re.match(text) if not m: - return [] + return (_ for _ in ()) expr, attr = m.group(1, 3) if expr.isdigit(): # Special case: float literal, using attrs here will result in # a SyntaxError - return [] + return (_ for _ in ()) try: obj = safe_eval(expr, namespace) except EvaluationError: - return [] + return (_ for _ in ()) return self.attr_lookup(obj, expr, attr) - def attr_lookup(self, obj: Any, expr: str, attr: str) -> List: + def attr_lookup(self, obj: Any, expr: str, attr: str) -> Iterator[str]: """Second half of attr_matches.""" words = self.list_attributes(obj) if inspection.hasattr_safe(obj, "__class__"): @@ -449,12 +451,12 @@ def attr_lookup(self, obj: Any, expr: str, attr: str) -> List: except ValueError: pass - matches = [] n = len(attr) - for word in words: - if self.method_match(word, n, attr) and word != "__builtins__": - matches.append(f"{expr}.{word}") - return matches + return ( + f"{expr}.{word}" + for word in words + if self.method_match(word, n, attr) and word != "__builtins__" + ) def list_attributes(self, obj: Any) -> List[str]: # TODO: re-implement dir without AttrCleaner here From 65887e3c91e670523a6b6e8ac59854b4b55cd8f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 22:25:44 +0200 Subject: [PATCH 145/262] Fix return type annotations --- bpython/autocomplete.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 22a514b6..575277e7 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -308,7 +308,7 @@ def format(self, word: str) -> str: def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set]: + ) -> Optional[Set[str]]: return_value = None all_matches = set() for completer in self._completers: @@ -333,7 +333,7 @@ def __init__( def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set]: + ) -> Optional[Set[str]]: return self.module_gatherer.complete(cursor_offset, line) def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: @@ -353,7 +353,7 @@ def __init__(self, mode: AutocompleteModes = AutocompleteModes.SIMPLE): def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set]: + ) -> Optional[Set[str]]: cs = lineparts.current_string(cursor_offset, line) if cs is None: return None @@ -389,7 +389,7 @@ def matches( *, locals_: Optional[Dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: r = self.locate(cursor_offset, line) if r is None: return None @@ -474,7 +474,7 @@ def matches( *, locals_: Optional[Dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: if locals_ is None: return None @@ -514,7 +514,7 @@ def matches( current_block: Optional[str] = None, complete_magic_methods: Optional[bool] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: if ( current_block is None or complete_magic_methods is None @@ -541,7 +541,7 @@ def matches( *, locals_: Optional[Dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: """Compute matches when text is a simple name. Return a list of all keywords, built-in functions and names currently defined in self.namespace that match. @@ -582,7 +582,7 @@ def matches( *, funcprops: Optional[inspection.FuncProps] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: if funcprops is None: return None @@ -618,7 +618,7 @@ def matches( *, locals_: Optional[Dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: if locals_ is None: locals_ = __main__.__dict__ @@ -642,7 +642,7 @@ def matches( class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set]: + ) -> Optional[Set[str]]: return None def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: @@ -660,7 +660,7 @@ def matches( *, history: Optional[List[str]] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: if history is None: return None if not lineparts.current_word(cursor_offset, line): @@ -714,7 +714,7 @@ def matches( current_block: Optional[str] = None, history: Optional[List[str]] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: if current_block is None or history is None: return None if "\n" not in current_block: From d533ad2ca29587419b3763584c0b4261a46b580a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 22:39:34 +0200 Subject: [PATCH 146/262] Fix some type annotations --- bpython/curtsies.py | 10 +++++----- bpython/simpleeval.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index f7b2ef47..6d289aaa 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -72,10 +72,10 @@ def __init__( self._request_refresh_callback: Callable[ [], None ] = self.input_generator.event_trigger(events.RefreshRequestEvent) - self._schedule_refresh_callback: Callable[ - [float], None - ] = self.input_generator.scheduled_event_trigger( - events.ScheduledRefreshRequestEvent + self._schedule_refresh_callback = ( + self.input_generator.scheduled_event_trigger( + events.ScheduledRefreshRequestEvent + ) ) self._request_reload_callback = ( self.input_generator.threadsafe_event_trigger(events.ReloadEvent) @@ -252,7 +252,7 @@ def curtsies_arguments(parser: argparse._ArgumentGroup) -> None: def _combined_events( - event_provider: "SupportsEventGeneration", paste_threshold: int + event_provider: SupportsEventGeneration, paste_threshold: int ) -> Generator[Union[str, curtsies.events.Event, None], Optional[float], None]: """Combines consecutive keypress events into paste events.""" timeout = yield "nonsense_event" # so send can be used immediately diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 193a6989..251e5e7f 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -217,7 +217,7 @@ def find_attribute_with_name(node, name): def evaluate_current_expression( cursor_offset: int, line: str, namespace: Optional[Dict[str, Any]] = None -): +) -> Any: """ Return evaluated expression to the right of the dot of current attribute. From 575796983a6bd77391363b2f045af298188ee009 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 22:39:43 +0200 Subject: [PATCH 147/262] Remove unnecessary check --- bpython/simpleeval.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 251e5e7f..c5bba43d 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -227,9 +227,6 @@ def evaluate_current_expression( # Find the biggest valid ast. # Once our attribute access is found, return its .value subtree - if namespace is None: - namespace = {} - # in case attribute is blank, e.g. foo.| -> foo.xxx| temp_line = line[:cursor_offset] + "xxx" + line[cursor_offset:] temp_cursor = cursor_offset + 3 From cc9cbbf63bca474b2bc1da9f75f8c2127e1b8a76 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 22:54:55 +0200 Subject: [PATCH 148/262] Update copyright years --- doc/sphinx/source/conf.py | 121 ++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 57 deletions(-) diff --git a/doc/sphinx/source/conf.py b/doc/sphinx/source/conf.py index 2c5263d9..2ef90049 100644 --- a/doc/sphinx/source/conf.py +++ b/doc/sphinx/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # bpython documentation build configuration file, created by # sphinx-quickstart on Mon Jun 8 11:58:16 2009. @@ -16,7 +15,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.append(os.path.abspath('.')) +# sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------------- @@ -25,20 +24,20 @@ extensions = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8' +# source_encoding = 'utf-8' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'bpython' -copyright = u'2008-2021 Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al.' +project = "bpython" +copyright = "2008-2022 Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al." # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -46,172 +45,180 @@ # # The short X.Y version. -version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), - '../../../bpython/_version.py') +version_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "../../../bpython/_version.py" +) with open(version_file) as vf: - version = vf.read().strip().split('=')[-1].replace('\'', '') + version = vf.read().strip().split("=")[-1].replace("'", "") # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. -unused_docs = ['configuration-options'] +unused_docs = ["configuration-options"] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'nature' +html_theme = "nature" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = 'logo.png' +html_logo = "logo.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -html_last_updated_fmt = '%b %d, %Y' +html_last_updated_fmt = "%b %d, %Y" # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_use_modindex = True +# html_use_modindex = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -html_use_opensearch = '' +html_use_opensearch = "" # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' +# html_file_suffix = '' # Output file base name for HTML help builder. -htmlhelp_basename = 'bpythondoc' +htmlhelp_basename = "bpythondoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). -#latex_documents = [ +# latex_documents = [ # ('index', 'bpython.tex', u'bpython Documentation', # u'Robert Farrell', 'manual'), -#] +# ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +# latex_use_modindex = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('man-bpython', 'bpython', - u'a fancy {curtsies, curses, urwid} interface to the Python interactive interpreter', - [], 1), - ('man-bpython-config', 'bpython-config', - u'user configuration file for bpython', - [], 5) + ( + "man-bpython", + "bpython", + "a fancy {curtsies, curses, urwid} interface to the Python interactive interpreter", + [], + 1, + ), + ( + "man-bpython-config", + "bpython-config", + "user configuration file for bpython", + [], + 5, + ), ] # If true, show URL addresses after external links. -#man_show_urls = False - +# man_show_urls = False From 5cc2c3f8434d8427c5b574bf2e53d6840afe12cc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 30 Aug 2022 00:01:37 +0200 Subject: [PATCH 149/262] Avoid string formatting if not producing any log output --- bpython/args.py | 29 +++++++++++------------------ bpython/autocomplete.py | 7 +++---- bpython/curtsiesfrontend/repl.py | 2 +- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 1212fe3f..ec7d3b29 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -204,24 +204,17 @@ def callback(group): bpython_logger.addHandler(logging.NullHandler()) curtsies_logger.addHandler(logging.NullHandler()) - logger.info(f"Starting bpython {__version__}") - logger.info(f"Python {sys.executable}: {sys.version_info}") - logger.info(f"curtsies: {curtsies.__version__}") - logger.info(f"cwcwidth: {cwcwidth.__version__}") - logger.info(f"greenlet: {greenlet.__version__}") - logger.info(f"pygments: {pygments.__version__}") # type: ignore - logger.info(f"requests: {requests.__version__}") - logger.info( - "environment:\n{}".format( - "\n".join( - f"{key}: {value}" - for key, value in sorted(os.environ.items()) - if key.startswith("LC") - or key.startswith("LANG") - or key == "TERM" - ) - ) - ) + logger.info("Starting bpython %s", __version__) + logger.info("Python %s: %s", sys.executable, sys.version_info) + logger.info("curtsies: %s", curtsies.__version__) + logger.info("cwcwidth: %s", cwcwidth.__version__) + logger.info("greenlet: %s", greenlet.__version__) + logger.info("pygments: %s", pygments.__version__) # type: ignore + logger.info("requests: %s", requests.__version__) + logger.info("environment:") + for key, value in sorted(os.environ.items()): + if key.startswith("LC") or key.startswith("LANG") or key == "TERM": + logger.info("%s: %s", key, value) return Config(options.config), options, options.args diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 575277e7..38ffb99d 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -55,6 +55,8 @@ from .importcompletion import ModuleGatherer +logger = logging.getLogger(__name__) + # Autocomplete modes class AutocompleteModes(Enum): NONE = "none" @@ -767,11 +769,8 @@ def get_completer( ) except Exception as e: # Instead of crashing the UI, log exceptions from autocompleters. - logger = logging.getLogger(__name__) logger.debug( - "Completer {} failed with unhandled exception: {}".format( - completer, e - ) + "Completer %r failed with unhandled exception: %s", completer, e ) continue if matches is not None: diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index ef708161..b950be68 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1005,7 +1005,7 @@ def only_whitespace_left_of_cursor(): """returns true if all characters before cursor are whitespace""" return not self.current_line[: self.cursor_offset].strip() - logger.debug("self.matches_iter.matches:%r", self.matches_iter.matches) + logger.debug("self.matches_iter.matches: %r", self.matches_iter.matches) if only_whitespace_left_of_cursor(): front_ws = len(self.current_line[: self.cursor_offset]) - len( self.current_line[: self.cursor_offset].lstrip() From e5e4aef17529c1ef033652a0812b3d1bc69f964f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 30 Aug 2022 22:03:59 +0200 Subject: [PATCH 150/262] Start development of 0.24 --- CHANGELOG.rst | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0c12b5fd..b9a4c8b5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,22 @@ Changelog ========= +0.24 +---- + +General information: + + +New features: + + +Fixes: + + +Changes to dependencies: + + + 0.23 ---- @@ -22,7 +38,7 @@ Fixes: * #955: Handle optional `readline` parameters in `stdin` emulation Thanks to thevibingcat * #959: Fix handling of `__name__` -* #966: Fix function signature completion for `classmethod`s +* #966: Fix function signature completion for `classmethod` Changes to dependencies: From e710dfe6aaf8fa252e6a68ae138500fcab897828 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 31 Aug 2022 12:51:11 +0200 Subject: [PATCH 151/262] Inject subcommands into build_py --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 12d4eeec..7d1a6770 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup -from distutils.command.build import build +from distutils.command.build_py import build_py try: from babel.messages import frontend as babel @@ -122,7 +122,7 @@ def git_describe_to_python_version(version): vf.write(f'__version__ = "{version}"\n') -cmdclass = {"build": build} +cmdclass = {"build_py": build_py} from bpython import package_dir, __author__ @@ -130,7 +130,7 @@ def git_describe_to_python_version(version): # localization options if using_translations: - build.sub_commands.insert(0, ("compile_catalog", None)) + build_py.sub_commands.insert(0, ("compile_catalog", None)) cmdclass["compile_catalog"] = babel.compile_catalog cmdclass["extract_messages"] = babel.extract_messages @@ -138,7 +138,7 @@ def git_describe_to_python_version(version): cmdclass["init_catalog"] = babel.init_catalog if using_sphinx: - build.sub_commands.insert(0, ("build_sphinx_man", None)) + build_py.sub_commands.insert(0, ("build_sphinx_man", None)) cmdclass["build_sphinx_man"] = BuildDoc if platform.system() in ("FreeBSD", "OpenBSD"): From 0bce729bcc4c84b256502fd9de0bba16117187c5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 31 Aug 2022 22:01:06 +0200 Subject: [PATCH 152/262] Revert "Inject subcommands into build_py" This reverts commit e710dfe6aaf8fa252e6a68ae138500fcab897828. --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 7d1a6770..12d4eeec 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup -from distutils.command.build_py import build_py +from distutils.command.build import build try: from babel.messages import frontend as babel @@ -122,7 +122,7 @@ def git_describe_to_python_version(version): vf.write(f'__version__ = "{version}"\n') -cmdclass = {"build_py": build_py} +cmdclass = {"build": build} from bpython import package_dir, __author__ @@ -130,7 +130,7 @@ def git_describe_to_python_version(version): # localization options if using_translations: - build_py.sub_commands.insert(0, ("compile_catalog", None)) + build.sub_commands.insert(0, ("compile_catalog", None)) cmdclass["compile_catalog"] = babel.compile_catalog cmdclass["extract_messages"] = babel.extract_messages @@ -138,7 +138,7 @@ def git_describe_to_python_version(version): cmdclass["init_catalog"] = babel.init_catalog if using_sphinx: - build_py.sub_commands.insert(0, ("build_sphinx_man", None)) + build.sub_commands.insert(0, ("build_sphinx_man", None)) cmdclass["build_sphinx_man"] = BuildDoc if platform.system() in ("FreeBSD", "OpenBSD"): From e355191bcb090626e352c7bce8d1744d381e5d86 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 31 Aug 2022 22:14:03 +0200 Subject: [PATCH 153/262] Add config for readthedocs.io --- .readthedocs.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..ced748ce --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,8 @@ +version: 2 +build: + tools: + python: "3.10" + +python: + install: + method: pip From 5e46aaf78e0d68c5efc20b190191f22d882bb91a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 31 Aug 2022 22:15:43 +0200 Subject: [PATCH 154/262] Fix readthedocs config --- .readthedocs.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index ced748ce..01c356be 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,4 +5,5 @@ build: python: install: - method: pip + - method: pip + path: . From 6badea563dd27cc00a20edf0b1f6bfeaf9f904c8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 31 Aug 2022 22:18:39 +0200 Subject: [PATCH 155/262] Fix readthedocs config --- .readthedocs.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 01c356be..942c1da0 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,7 +1,4 @@ version: 2 -build: - tools: - python: "3.10" python: install: From a03e3457bef05b89b8caa5720263aac72f21c9f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 14 Sep 2022 17:49:52 +0200 Subject: [PATCH 156/262] Refactor --- bpython/importcompletion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 96936144..3496a24e 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -240,9 +240,9 @@ def find_all_modules( self.modules.add(module) yield - def find_coroutine(self) -> Optional[bool]: + def find_coroutine(self) -> bool: if self.fully_loaded: - return None + return False try: next(self.find_iterator) From f1a5cfb195987ff8b647b3066b134dbef60fc0e9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 14 Sep 2022 17:49:59 +0200 Subject: [PATCH 157/262] Fix typo --- bpython/inspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 6b9074c0..2fd8259b 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -154,7 +154,7 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: ): stack.append(substack) substack = [] - # If type annotation didn't end before, ti does now. + # If type annotation didn't end before, it does now. annotation = False continue elif token is Token.Operator and value == "=" and parendepth == 0: From 89784501732975afb9768acb9562e4e113999bd9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 14 Sep 2022 19:30:31 +0200 Subject: [PATCH 158/262] Refactor --- bpython/patch_linecache.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index 82b38dd7..d91392d2 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -1,21 +1,24 @@ import linecache -from typing import Any, List, Tuple +from typing import Any, List, Tuple, Optional class BPythonLinecache(dict): """Replaces the cache dict in the standard-library linecache module, to also remember (in an unerasable way) bpython console input.""" - def __init__(self, *args, **kwargs) -> None: + def __init__( + self, + bpython_history: Optional[ + List[Tuple[int, None, List[str], str]] + ] = None, + *args, + **kwargs, + ) -> None: super().__init__(*args, **kwargs) - self.bpython_history: List[Tuple[int, None, List[str], str]] = [] + self.bpython_history = bpython_history or [] def is_bpython_filename(self, fname: Any) -> bool: - if isinstance(fname, str): - return fname.startswith(" Tuple[int, None, List[str], str]: """Given a filename provided by remember_bpython_input, @@ -58,14 +61,13 @@ def _bpython_clear_linecache() -> None: if isinstance(linecache.cache, BPythonLinecache): bpython_history = linecache.cache.bpython_history else: - bpython_history = [] - linecache.cache = BPythonLinecache() - linecache.cache.bpython_history = bpython_history + bpython_history = None + linecache.cache = BPythonLinecache(bpython_history) -# Monkey-patch the linecache module so that we're able +# Monkey-patch the linecache module so that we are able # to hold our command history there and have it persist -linecache.cache = BPythonLinecache(linecache.cache) # type: ignore +linecache.cache = BPythonLinecache(None, linecache.cache) # type: ignore linecache.clearcache = _bpython_clear_linecache From 7e64b0f41233761a800f9a453996198361afb47b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 20:34:12 +0200 Subject: [PATCH 159/262] Use Optional --- bpython/importcompletion.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 3496a24e..3e95500e 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -155,9 +155,7 @@ def complete(self, cursor_offset: int, line: str) -> Optional[Set[str]]: else: return None - def find_modules( - self, path: Path - ) -> Generator[Union[str, None], None, None]: + def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: """Find all modules (and packages) for a given directory.""" if not path.is_dir(): # Perhaps a zip file From ba026ec19e252f2494439dc4b367f0345d09d0a2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 22:32:05 +0200 Subject: [PATCH 160/262] Store device id and inodes in a dataclass --- bpython/importcompletion.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 3e95500e..44062248 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -25,8 +25,9 @@ import importlib.machinery import sys import warnings +from dataclasses import dataclass from pathlib import Path -from typing import Optional, Set, Generator, Tuple, Sequence, Iterable, Union +from typing import Optional, Set, Generator, Sequence, Iterable, Union from .line import ( current_word, @@ -47,6 +48,16 @@ ), ) +_LOADED_INODE_DATACLASS_ARGS = {"frozen": True} +if sys.version_info[2:] >= (3, 10): + _LOADED_INODE_DATACLASS_ARGS["slots"] = True + + +@dataclass(**_LOADED_INODE_DATACLASS_ARGS) +class _LoadedInode: + dev: int + inode: int + class ModuleGatherer: def __init__( @@ -60,7 +71,7 @@ def __init__( # Cached list of all known modules self.modules: Set[str] = set() # Set of (st_dev, st_ino) to compare against so that paths are not repeated - self.paths: Set[Tuple[int, int]] = set() + self.paths: Set[_LoadedInode] = set() # Patterns to skip self.skiplist: Sequence[str] = ( skiplist if skiplist is not None else tuple() @@ -216,8 +227,9 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: stat = path_real.stat() except OSError: continue - if (stat.st_dev, stat.st_ino) not in self.paths: - self.paths.add((stat.st_dev, stat.st_ino)) + loaded_inode = _LoadedInode(stat.st_dev, stat.st_ino) + if loaded_inode not in self.paths: + self.paths.add(loaded_inode) for subname in self.find_modules(path_real): if subname is None: yield None # take a break to avoid unresponsiveness From 2868ab10e4afaf36e5d7cdd1e72c856004edece6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 22:44:12 +0200 Subject: [PATCH 161/262] Fix typo --- bpython/importcompletion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 44062248..13a77ab0 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -49,7 +49,7 @@ ) _LOADED_INODE_DATACLASS_ARGS = {"frozen": True} -if sys.version_info[2:] >= (3, 10): +if sys.version_info[:2] >= (3, 10): _LOADED_INODE_DATACLASS_ARGS["slots"] = True From 900a273ea27a72b6202a087c6d5893daa54f261f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 23:08:39 +0200 Subject: [PATCH 162/262] Refactor --- bpython/importcompletion.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 13a77ab0..39829856 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -206,23 +206,22 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: # Workaround for issue #166 continue try: - is_package = False + package_pathname = None with warnings.catch_warnings(): warnings.simplefilter("ignore", ImportWarning) spec = finder.find_spec(name) if spec is None: continue if spec.submodule_search_locations is not None: - pathname = spec.submodule_search_locations[0] - is_package = True + package_pathname = spec.submodule_search_locations[0] except (ImportError, OSError, SyntaxError): continue except UnicodeEncodeError: # Happens with Python 3 when there is a filename in some invalid encoding continue else: - if is_package: - path_real = Path(pathname).resolve() + if package_pathname is not None: + path_real = Path(package_pathname).resolve() try: stat = path_real.stat() except OSError: From 3944fa7c8a3cc5549dfb2680bb621b001d995586 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 23:09:00 +0200 Subject: [PATCH 163/262] Shortcircuit some paths --- bpython/importcompletion.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 39829856..d099c2a9 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -186,7 +186,10 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: finder = importlib.machinery.FileFinder(str(path), *LOADERS) # type: ignore for p in children: - if any(fnmatch.fnmatch(p.name, entry) for entry in self.skiplist): + if p.name.startswith(".") or p.name == "__pycache__": + # Impossible to import from names starting with . and we can skip __pycache__ + continue + elif any(fnmatch.fnmatch(p.name, entry) for entry in self.skiplist): # Path is on skiplist continue elif not any(p.name.endswith(suffix) for suffix in SUFFIXES): From 074a015fde32475916faf5454258a3e71dae0c01 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 23:09:05 +0200 Subject: [PATCH 164/262] Refactor --- bpython/importcompletion.py | 45 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index d099c2a9..c1e073f8 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -83,7 +83,7 @@ def __init__( paths = sys.path self.find_iterator = self.find_all_modules( - (Path(p).resolve() if p else Path.cwd() for p in paths) + Path(p).resolve() if p else Path.cwd() for p in paths ) def module_matches(self, cw: str, prefix: str = "") -> Set[str]: @@ -120,7 +120,7 @@ def attr_matches( matches = { name for name in dir(module) if name.startswith(name_after_dot) } - module_part, _, _ = cw.rpartition(".") + module_part = cw.rpartition(".")[0] if module_part: matches = {f"{module_part}.{m}" for m in matches} @@ -208,8 +208,9 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: if name == "badsyntax_pep3120": # Workaround for issue #166 continue + + package_pathname = None try: - package_pathname = None with warnings.catch_warnings(): warnings.simplefilter("ignore", ImportWarning) spec = finder.find_spec(name) @@ -217,27 +218,25 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: continue if spec.submodule_search_locations is not None: package_pathname = spec.submodule_search_locations[0] - except (ImportError, OSError, SyntaxError): - continue - except UnicodeEncodeError: - # Happens with Python 3 when there is a filename in some invalid encoding + except (ImportError, OSError, SyntaxError, UnicodeEncodeError): + # UnicodeEncodeError happens with Python 3 when there is a filename in some invalid encoding continue - else: - if package_pathname is not None: - path_real = Path(package_pathname).resolve() - try: - stat = path_real.stat() - except OSError: - continue - loaded_inode = _LoadedInode(stat.st_dev, stat.st_ino) - if loaded_inode not in self.paths: - self.paths.add(loaded_inode) - for subname in self.find_modules(path_real): - if subname is None: - yield None # take a break to avoid unresponsiveness - elif subname != "__init__": - yield f"{name}.{subname}" - yield name + + if package_pathname is not None: + path_real = Path(package_pathname).resolve() + try: + stat = path_real.stat() + except OSError: + continue + loaded_inode = _LoadedInode(stat.st_dev, stat.st_ino) + if loaded_inode not in self.paths: + self.paths.add(loaded_inode) + for subname in self.find_modules(path_real): + if subname is None: + yield None # take a break to avoid unresponsiveness + elif subname != "__init__": + yield f"{name}.{subname}" + yield name yield None # take a break to avoid unresponsiveness def find_all_modules( From bb8712c6185b43fd60e351b6702fb23b8d6757d0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 23:25:32 +0200 Subject: [PATCH 165/262] Remove unused import --- bpython/history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/history.py b/bpython/history.py index a870d4b2..13dbb5b7 100644 --- a/bpython/history.py +++ b/bpython/history.py @@ -25,7 +25,7 @@ from pathlib import Path import stat from itertools import islice, chain -from typing import Iterable, Optional, List, TextIO, Union +from typing import Iterable, Optional, List, TextIO from .translations import _ from .filelock import FileLock From 748ce36c3ab8f93ce80623e223ff8cde4a61b94a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 23:29:05 +0200 Subject: [PATCH 166/262] Fix exception handling --- bpython/pager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bpython/pager.py b/bpython/pager.py index 673e902b..e145e0ed 100644 --- a/bpython/pager.py +++ b/bpython/pager.py @@ -63,7 +63,6 @@ def page(data: str, use_internal: bool = False) -> None: # pager command not found, fall back to internal pager page_internal(data) return - except OSError as e: if e.errno != errno.EPIPE: raise while True: From f44b8c22eb8929267cc3599f0f84776453ce5e16 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 23:43:58 +0200 Subject: [PATCH 167/262] Refactor --- bpython/autocomplete.py | 2 +- bpython/inspection.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 38ffb99d..69e6e2a2 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -598,7 +598,7 @@ def matches( if isinstance(name, str) and name.startswith(r.word) } matches.update( - name + "=" + f"{name}=" for name in funcprops.argspec.kwonly if name.startswith(r.word) ) diff --git a/bpython/inspection.py b/bpython/inspection.py index 2fd8259b..78bbc578 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -142,16 +142,16 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: parendepth += 1 elif value in ")}]": parendepth -= 1 - elif value == ":" and parendepth == -1: - # End of signature reached - break - elif value == ":" and parendepth == 0: - # Start of type annotation - annotation = True - - if (value == "," and parendepth == 0) or ( - value == ")" and parendepth == -1 - ): + elif value == ":": + if parendepth == -1: + # End of signature reached + break + elif parendepth == 0: + # Start of type annotation + annotation = True + + if (value, parendepth) in ((",", 0), (")", -1)): + # End of current argument stack.append(substack) substack = [] # If type annotation didn't end before, it does now. From 9281dccb83c3cecfebfd557518f1b87640808d8d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 26 Sep 2022 22:21:38 +0200 Subject: [PATCH 168/262] Refactor --- bpython/autocomplete.py | 52 +++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 69e6e2a2..b97fd86f 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -555,11 +555,10 @@ def matches( if r is None: return None - matches = set() n = len(r.word) - for word in KEYWORDS: - if self.method_match(word, n, r.word): - matches.add(word) + matches = { + word for word in KEYWORDS if self.method_match(word, n, r.word) + } for nspace in (builtins.__dict__, locals_): for word, val in nspace.items(): # if identifier isn't ascii, don't complete (syntax error) @@ -652,7 +651,7 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: else: - class JediCompletion(BaseCompletionType): + class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] _orig_start: Optional[int] def matches( @@ -660,19 +659,28 @@ def matches( cursor_offset: int, line: str, *, + current_block: Optional[str] = None, history: Optional[List[str]] = None, **kwargs: Any, ) -> Optional[Set[str]]: - if history is None: - return None - if not lineparts.current_word(cursor_offset, line): + if ( + current_block is None + or history is None + or "\n" not in current_block + or not lineparts.current_word(cursor_offset, line) + ): return None + assert cursor_offset <= len(line), "{!r} {!r}".format( + cursor_offset, + line, + ) + combined_history = "\n".join(itertools.chain(history, (line,))) try: script = jedi.Script(combined_history, path="fake.py") completions = script.complete( - len(combined_history.splitlines()), cursor_offset + combined_history.count("\n") + 1, cursor_offset ) except (jedi.NotFoundError, IndexError, KeyError): # IndexError for #483 @@ -688,8 +696,6 @@ def matches( return None assert isinstance(self._orig_start, int) - first_letter = line[self._orig_start : self._orig_start + 1] - matches = [c.name for c in completions] if any( not m.lower().startswith(matches[0][0].lower()) for m in matches @@ -699,35 +705,15 @@ def matches( return None else: # case-sensitive matches only + first_letter = line[self._orig_start] return {m for m in matches if m.startswith(first_letter)} def locate(self, cursor_offset: int, line: str) -> LinePart: - assert isinstance(self._orig_start, int) + assert self._orig_start is not None start = self._orig_start end = cursor_offset return LinePart(start, end, line[start:end]) - class MultilineJediCompletion(JediCompletion): # type: ignore [no-redef] - def matches( - self, - cursor_offset: int, - line: str, - *, - current_block: Optional[str] = None, - history: Optional[List[str]] = None, - **kwargs: Any, - ) -> Optional[Set[str]]: - if current_block is None or history is None: - return None - if "\n" not in current_block: - return None - - assert cursor_offset <= len(line), "{!r} {!r}".format( - cursor_offset, - line, - ) - return super().matches(cursor_offset, line, history=history) - def get_completer( completers: Sequence[BaseCompletionType], From 1c6f1813dc8c6c8ac24daa8916a0a8d5a3aab9a1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 19 Oct 2022 19:43:54 +0200 Subject: [PATCH 169/262] Fix deprecation warning --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 748ce965..35806eca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ name = bpython long_description = file: README.rst license = MIT -license_file = LICENSE +license_files = LICENSE url = https://www.bpython-interpreter.org/ project_urls = GitHub = https://github.com/bpython/bpython From d47c2387f5d173e0175a95c782c79414c7ddd5ac Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 19 Oct 2022 20:32:02 +0200 Subject: [PATCH 170/262] GA: checkspell: ignore dedented --- .github/workflows/lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 839681b9..57aad1bd 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -25,7 +25,7 @@ jobs: - uses: codespell-project/actions-codespell@master with: skip: '*.po' - ignore_words_list: ba,te,deltion + ignore_words_list: ba,te,deltion,dedent,dedented mypy: runs-on: ubuntu-latest From 4759bcb766c633b269d75a4036c8885d45940d29 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 25 Oct 2022 15:01:41 +0200 Subject: [PATCH 171/262] GA: test with Python 3.11 --- .github/workflows/build.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 89edab7c..c16bcd70 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -14,7 +14,13 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.7, 3.8, 3.9, "3.10", "pypy-3.7"] + python-version: + - "3.7" + - "3.8" + - "3.9", + - "3.10", + - "3.11", + - "pypy-3.7" steps: - uses: actions/checkout@v2 with: From 3e495488af4a66a8be267a588919ac1722a9b9b7 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 25 Oct 2022 15:03:24 +0200 Subject: [PATCH 172/262] GA: fix syntax --- .github/workflows/build.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c16bcd70..e9a3e162 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -17,9 +17,9 @@ jobs: python-version: - "3.7" - "3.8" - - "3.9", - - "3.10", - - "3.11", + - "3.9" + - "3.10" + - "3.11" - "pypy-3.7" steps: - uses: actions/checkout@v2 From f2ad2a1e771d48e6ab1280d9c75e42381bf10993 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 15:34:09 +0200 Subject: [PATCH 173/262] GA: update actions with dependabot --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..8c139c7b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From 82389987bfb1a4fa46e18b70882e5e909a9e82a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 13:34:35 +0000 Subject: [PATCH 174/262] Bump actions/setup-python from 2 to 4 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- .github/workflows/lint.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e9a3e162..faeb9c8f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -26,7 +26,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 57aad1bd..b24cd6e5 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 - name: Install dependencies run: | python -m pip install --upgrade pip From ec2f03354e115d61d273384c3ba7bbcff366c1bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 13:36:47 +0000 Subject: [PATCH 175/262] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- .github/workflows/lint.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index faeb9c8f..052969b8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,7 +22,7 @@ jobs: - "3.11" - "pypy-3.7" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index b24cd6e5..f644f543 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -8,7 +8,7 @@ jobs: black: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 - name: Install dependencies @@ -21,7 +21,7 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: codespell-project/actions-codespell@master with: skip: '*.po' @@ -30,7 +30,7 @@ jobs: mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 - name: Install dependencies From 9e32826795944c8b97092077164bae91045ff0a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 13:34:46 +0000 Subject: [PATCH 176/262] Bump codecov/codecov-action from 1 to 3 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1 to 3. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v1...v3) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 052969b8..9cdcc1e9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -46,7 +46,7 @@ jobs: run: | pytest --cov=bpython --cov-report=xml -v - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 env: PYTHON_VERSION: ${{ matrix.python-version }} with: From 742085633868c72e5a42ce452f3deea57de442fc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 16:19:04 +0200 Subject: [PATCH 177/262] Handle changed output in Python 3.11 --- bpython/test/test_interpreter.py | 42 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 65b60a92..f53ec252 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -112,19 +112,35 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - expected = ( - "Traceback (most recent call last):\n File " - + green('""') - + ", line " - + bold(magenta("1")) - + ", in " - + cyan("") - + "\n gfunc()\n" - + bold(red("NameError")) - + ": " - + cyan(global_not_found) - + "\n" - ) + if (3, 11) <= sys.version_info[:2]: + expected = ( + "Traceback (most recent call last):\n File " + + green('""') + + ", line " + + bold(magenta("1")) + + ", in " + + cyan("") + + "\n gfunc()" + + "\n ^^^^^\n" + + bold(red("NameError")) + + ": " + + cyan(global_not_found) + + "\n" + ) + else: + expected = ( + "Traceback (most recent call last):\n File " + + green('""') + + ", line " + + bold(magenta("1")) + + ", in " + + cyan("") + + "\n gfunc()\n" + + bold(red("NameError")) + + ": " + + cyan(global_not_found) + + "\n" + ) self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) self.assertEqual(plain("").join(a), expected) From b04ad8835c6eb777393536efa4e89045a3883852 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 17:48:57 +0200 Subject: [PATCH 178/262] mypy: ignore import errors from twisted --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 35806eca..7955ee39 100644 --- a/setup.cfg +++ b/setup.cfg @@ -77,3 +77,6 @@ ignore_missing_imports = True [mypy-urwid] ignore_missing_imports = True + +[mypy-twisted.*] +ignore_missing_imports = True From 3b8927852e84fd6581c83ef974920856d5f3fb47 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 19:21:58 +0200 Subject: [PATCH 179/262] Refactor Also, as we have many small _Repr instances, make the value a slot. --- bpython/inspection.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 78bbc578..8ae5c20a 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -36,6 +36,22 @@ from .lazyre import LazyReCompile +class _Repr: + """ + Helper for `ArgSpec`: Returns the given value in `__repr__()`. + """ + + __slots__ = ("value",) + + def __init__(self, value: str) -> None: + self.value = value + + def __repr__(self) -> str: + return self.value + + __str__ = __repr__ + + @dataclass class ArgSpec: args: List[str] @@ -110,20 +126,6 @@ def __exit__( return False -class _Repr: - """ - Helper for `fixlongargs()`: Returns the given value in `__repr__()`. - """ - - def __init__(self, value: str) -> None: - self.value = value - - def __repr__(self) -> str: - return self.value - - __str__ = __repr__ - - def parsekeywordpairs(signature: str) -> Dict[str, str]: preamble = True stack = [] @@ -293,12 +295,15 @@ def _get_argspec_from_signature(f: Callable) -> ArgSpec: """ args = [] - varargs = varkwargs = None + varargs = None + varkwargs = None defaults = [] kwonly = [] kwonly_defaults = {} annotations = {} + # We use signature here instead of getfullargspec as the latter also returns + # self and cls (for class methods). signature = inspect.signature(f) for parameter in signature.parameters.values(): if parameter.annotation is not parameter.empty: From 917382530f0de38e775312cacfab5ebe498502a3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 19:56:27 +0200 Subject: [PATCH 180/262] Fix inspection of built-in functions with >= 3.11 The built-in functions no longer have their signature in the docstring, but now inspect.signature can produce results. But as we have no source for built-in functions, we cannot replace the default values. Hence, we handle built-in functions in an extra step. This commit also changes the handling of default values slightly. They are now always put into a _Repr. --- bpython/inspection.py | 60 ++++++++++++++++++++++----------- bpython/test/test_inspection.py | 36 ++++++++++---------- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 8ae5c20a..e1e4ed8d 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -57,9 +57,9 @@ class ArgSpec: args: List[str] varargs: Optional[str] varkwargs: Optional[str] - defaults: Optional[List[Any]] + defaults: Optional[List[_Repr]] kwonly: List[str] - kwonly_defaults: Optional[Dict[str, Any]] + kwonly_defaults: Optional[Dict[str, _Repr]] annotations: Optional[Dict[str, Any]] @@ -169,31 +169,51 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: return {item[0]: "".join(item[2:]) for item in stack if len(item) >= 3} -def _fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: +def _fix_default_values(f: Callable, argspec: ArgSpec) -> ArgSpec: """Functions taking default arguments that are references to other objects - whose str() is too big will cause breakage, so we swap out the object - itself with the name it was referenced with in the source by parsing the - source itself !""" - if argspec.defaults is None: + will cause breakage, so we swap out the object itself with the name it was + referenced with in the source by parsing the source itself!""" + + if argspec.defaults is None and argspec.kwonly_defaults is None: # No keyword args, no need to do anything return argspec - values = list(argspec.defaults) - if not values: - return argspec - keys = argspec.args[-len(values) :] + try: - src = inspect.getsourcelines(f) + src, _ = inspect.getsourcelines(f) except (OSError, IndexError): # IndexError is raised in inspect.findsource(), can happen in # some situations. See issue #94. return argspec - kwparsed = parsekeywordpairs("".join(src[0])) + except TypeError: + # No source code is available (for Python >= 3.11) + # + # If the function is a builtin, we replace the default values. + # Otherwise, let's bail out. + if not inspect.isbuiltin(f): + raise + + if argspec.defaults is not None: + argspec.defaults = [_Repr(str(value)) for value in argspec.defaults] + if argspec.kwonly_defaults is not None: + argspec.kwonly_defaults = { + key: _Repr(str(value)) + for key, value in argspec.kwonly_defaults.items() + } + return argspec - for i, (key, value) in enumerate(zip(keys, values)): - if len(repr(value)) != len(kwparsed[key]): + kwparsed = parsekeywordpairs("".join(src)) + + if argspec.defaults is not None: + values = list(argspec.defaults) + keys = argspec.args[-len(values) :] + for i, key in enumerate(keys): values[i] = _Repr(kwparsed[key]) - argspec.defaults = values + argspec.defaults = values + if argspec.kwonly_defaults is not None: + for key in argspec.kwonly_defaults.keys(): + argspec.kwonly_defaults[key] = _Repr(kwparsed[key]) + return argspec @@ -234,11 +254,11 @@ def _getpydocspec(f: Callable) -> Optional[ArgSpec]: if varargs is not None: kwonly_args.append(arg) if default: - kwonly_defaults[arg] = default + kwonly_defaults[arg] = _Repr(default) else: args.append(arg) if default: - defaults.append(default) + defaults.append(_Repr(default)) return ArgSpec( args, varargs, varkwargs, defaults, kwonly_args, kwonly_defaults, None @@ -267,7 +287,9 @@ def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: return None try: argspec = _get_argspec_from_signature(f) - fprops = FuncProps(func, _fixlongargs(f, argspec), is_bound_method) + fprops = FuncProps( + func, _fix_default_values(f, argspec), is_bound_method + ) except (TypeError, KeyError, ValueError): argspec_pydoc = _getpydocspec(f) if argspec_pydoc is None: diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 43915f3e..3f04222d 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -11,6 +11,7 @@ from bpython.test.fodder import encoding_utf8 pypy = "PyPy" in sys.version +_is_py311 = sys.version_info[:2] >= (3, 11) try: import numpy @@ -53,23 +54,17 @@ def test_parsekeywordpairs(self): def fails(spam=["-a", "-b"]): pass - default_arg_repr = "['-a', '-b']" - self.assertEqual( - str(["-a", "-b"]), - default_arg_repr, - "This test is broken (repr does not match), fix me.", - ) - argspec = inspection.getfuncprops("fails", fails) + self.assertIsNotNone(argspec) defaults = argspec.argspec.defaults - self.assertEqual(str(defaults[0]), default_arg_repr) + self.assertEqual(str(defaults[0]), '["-a", "-b"]') def test_pasekeywordpairs_string(self): def spam(eggs="foo, bar"): pass defaults = inspection.getfuncprops("spam", spam).argspec.defaults - self.assertEqual(repr(defaults[0]), "'foo, bar'") + self.assertEqual(repr(defaults[0]), '"foo, bar"') def test_parsekeywordpairs_multiple_keywords(self): def spam(eggs=23, foobar="yay"): @@ -77,14 +72,14 @@ def spam(eggs=23, foobar="yay"): defaults = inspection.getfuncprops("spam", spam).argspec.defaults self.assertEqual(repr(defaults[0]), "23") - self.assertEqual(repr(defaults[1]), "'yay'") + self.assertEqual(repr(defaults[1]), '"yay"') def test_pasekeywordpairs_annotation(self): def spam(eggs: str = "foo, bar"): pass defaults = inspection.getfuncprops("spam", spam).argspec.defaults - self.assertEqual(repr(defaults[0]), "'foo, bar'") + self.assertEqual(repr(defaults[0]), '"foo, bar"') def test_get_encoding_ascii(self): self.assertEqual(inspection.get_encoding(encoding_ascii), "ascii") @@ -134,8 +129,15 @@ def test_getfuncprops_print(self): self.assertIn("file", props.argspec.kwonly) self.assertIn("flush", props.argspec.kwonly) self.assertIn("sep", props.argspec.kwonly) - self.assertEqual(props.argspec.kwonly_defaults["file"], "sys.stdout") - self.assertEqual(props.argspec.kwonly_defaults["flush"], "False") + if _is_py311: + self.assertEqual( + repr(props.argspec.kwonly_defaults["file"]), "None" + ) + else: + self.assertEqual( + repr(props.argspec.kwonly_defaults["file"]), "sys.stdout" + ) + self.assertEqual(repr(props.argspec.kwonly_defaults["flush"]), "False") @unittest.skipUnless( numpy is not None and numpy.__version__ >= "1.18", @@ -173,12 +175,12 @@ def fun_annotations(number: int, lst: List[int] = []) -> List[int]: props = inspection.getfuncprops("fun", fun) self.assertEqual(props.func, "fun") self.assertEqual(props.argspec.args, ["number", "lst"]) - self.assertEqual(props.argspec.defaults[0], []) + self.assertEqual(repr(props.argspec.defaults[0]), "[]") props = inspection.getfuncprops("fun_annotations", fun_annotations) self.assertEqual(props.func, "fun_annotations") self.assertEqual(props.argspec.args, ["number", "lst"]) - self.assertEqual(props.argspec.defaults[0], []) + self.assertEqual(repr(props.argspec.defaults[0]), "[]") def test_issue_966_class_method(self): class Issue966(Sequence): @@ -215,7 +217,7 @@ def bmethod(cls, number, lst): ) self.assertEqual(props.func, "cmethod") self.assertEqual(props.argspec.args, ["number", "lst"]) - self.assertEqual(props.argspec.defaults[0], []) + self.assertEqual(repr(props.argspec.defaults[0]), "[]") def test_issue_966_static_method(self): class Issue966(Sequence): @@ -252,7 +254,7 @@ def bmethod(number, lst): ) self.assertEqual(props.func, "cmethod") self.assertEqual(props.argspec.args, ["number", "lst"]) - self.assertEqual(props.argspec.defaults[0], []) + self.assertEqual(repr(props.argspec.defaults[0]), "[]") class A: From 6b390db6ffb14a7d57a9d5458a814e41a708d123 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 22:20:38 +0200 Subject: [PATCH 181/262] Unbreak tests after 9173825 --- bpython/inspection.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index e1e4ed8d..db0a397c 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -185,13 +185,7 @@ def _fix_default_values(f: Callable, argspec: ArgSpec) -> ArgSpec: # some situations. See issue #94. return argspec except TypeError: - # No source code is available (for Python >= 3.11) - # - # If the function is a builtin, we replace the default values. - # Otherwise, let's bail out. - if not inspect.isbuiltin(f): - raise - + # No source code is available, so replace the default values with what we have. if argspec.defaults is not None: argspec.defaults = [_Repr(str(value)) for value in argspec.defaults] if argspec.kwonly_defaults is not None: From e20bf119949c445fbb1215f402d67057343476e3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 29 Oct 2022 17:54:09 +0200 Subject: [PATCH 182/262] Skip test with special unicode chars on broken Python 3.11 versions --- bpython/test/test_curtsies_painting.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 2804643c..19561efb 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -13,7 +13,7 @@ ) from curtsies.fmtfuncs import cyan, bold, green, yellow, on_magenta, red from curtsies.window import CursorAwareWindow -from unittest import mock +from unittest import mock, skipIf from bpython.curtsiesfrontend.events import RefreshRequestEvent from bpython import config, inspection @@ -311,6 +311,10 @@ def test_cursor_position_with_padding_char(self): cursor_pos = self.repl.paint()[1] self.assertEqual(cursor_pos, (1, 4)) + @skipIf( + sys.version_info[:2] >= (3, 11) and sys.version_info[:3] < (3, 11, 1), + "https://github.com/python/cpython/issues/98744", + ) def test_display_of_padding_chars(self): self.repl.width = 11 [self.repl.add_normal_character(c) for c in "width"] From abf13c57a2b54ef4c68e5bec16bcb44a11a9513e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 29 Oct 2022 18:05:02 +0200 Subject: [PATCH 183/262] Remove unused build dependency --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c7ef64f2..4dfbad88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,6 @@ [build-system] requires = [ "setuptools >= 43", - "wheel", ] [tool.black] From 7f546d932552256ad46057c81f9c8642d7a7b619 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 29 Oct 2022 18:05:11 +0200 Subject: [PATCH 184/262] Set build-backend --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4dfbad88..6526fb9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,7 @@ requires = [ "setuptools >= 43", ] +build-backend = "setuptools.build_meta" [tool.black] line-length = 80 From 33c62dac03cddd6d87547cdcbca4762b1f4c5684 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 29 Oct 2022 18:18:02 +0200 Subject: [PATCH 185/262] Update changelog --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b9a4c8b5..1d902634 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,10 +12,13 @@ New features: Fixes: +* Improve inspection of builtin functions. Changes to dependencies: +Support for Python 3.11 has been added. + 0.23 ---- From 7712e282d067b041ead41f0b514ac3e27584edc6 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Thu, 3 Nov 2022 23:51:22 -0700 Subject: [PATCH 186/262] Fix watchdog auto-reloading --- bpython/curtsies.py | 2 +- bpython/curtsiesfrontend/repl.py | 36 +++++++------------------------- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 6d289aaa..985f7085 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -106,7 +106,7 @@ def _schedule_refresh(self, when: float) -> None: return self._schedule_refresh_callback(when) def _request_reload(self, files_modified: Sequence[str]) -> None: - return self._request_reload_callback(files_modified) + return self._request_reload_callback(files_modified=files_modified) def interrupting_refresh(self) -> None: return self._interrupting_refresh_callback() diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index b950be68..cf7d9c37 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -269,54 +269,39 @@ def __init__(self, watcher, loader): def __getattr__(self, name): if name == "create_module" and hasattr(self.loader, name): return self._create_module - if name == "load_module" and hasattr(self.loader, name): - return self._load_module return getattr(self.loader, name) def _create_module(self, spec): - spec = self.loader.create_module(spec) + module_object = self.loader.create_module(spec) if ( getattr(spec, "origin", None) is not None and spec.origin != "builtin" ): self.watcher.track_module(spec.origin) - return spec - - def _load_module(self, name): - module = self.loader.load_module(name) - if hasattr(module, "__file__"): - self.watcher.track_module(module.__file__) - return module + return module_object class ImportFinder: - """Wrapper for finders in sys.meta_path to replace wrap all loaders with ImportLoader.""" + """Wrapper for finders in sys.meta_path to wrap all loaders with ImportLoader.""" - def __init__(self, finder, watcher): + def __init__(self, watcher, finder): self.watcher = watcher self.finder = finder def __getattr__(self, name): if name == "find_spec" and hasattr(self.finder, name): return self._find_spec - if name == "find_module" and hasattr(self.finder, name): - return self._find_module return getattr(self.finder, name) def _find_spec(self, fullname, path, target=None): # Attempt to find the spec spec = self.finder.find_spec(fullname, path, target) if spec is not None: - if getattr(spec, "__loader__", None) is not None: + if getattr(spec, "loader", None) is not None: # Patch the loader to enable reloading - spec.__loader__ = ImportLoader(self.watcher, spec.__loader__) + spec.loader = ImportLoader(self.watcher, spec.loader) return spec - def _find_module(self, fullname, path=None): - loader = self.finder.find_module(fullname, path) - if loader is not None: - return ImportLoader(self.watcher, loader) - def _process_ps(ps, default_ps: str): """Replace ps1/ps2 with the default if the user specified value contains control characters.""" @@ -607,14 +592,7 @@ def __enter__(self): if self.watcher: meta_path = [] for finder in sys.meta_path: - # All elements get wrapped in ImportFinder instances execepted for instances of - # _SixMetaPathImporter (from six). When importing six, it will check if the importer - # is already part of sys.meta_path and will remove instances. We do not want to - # break this feature (see also #874). - if type(finder).__name__ == "_SixMetaPathImporter": - meta_path.append(finder) - else: - meta_path.append(ImportFinder(finder, self.watcher)) + meta_path.append(ImportFinder(self.watcher, finder)) sys.meta_path = meta_path sitefix.monkeypatch_quit() From 7c312971ce37dbeddd4316ed80cb640e50f04a9d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 6 Nov 2022 19:32:40 +0100 Subject: [PATCH 187/262] Log versions of all required dependencies --- bpython/args.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bpython/args.py b/bpython/args.py index ec7d3b29..0c514c37 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -41,6 +41,7 @@ import pygments import requests import sys +import xdg from pathlib import Path from . import __version__, __copyright__ @@ -206,10 +207,12 @@ def callback(group): logger.info("Starting bpython %s", __version__) logger.info("Python %s: %s", sys.executable, sys.version_info) + # versions of required dependencies logger.info("curtsies: %s", curtsies.__version__) logger.info("cwcwidth: %s", cwcwidth.__version__) logger.info("greenlet: %s", greenlet.__version__) logger.info("pygments: %s", pygments.__version__) # type: ignore + logger.info("pyxdg: %s", xdg.__version__) # type: ignore logger.info("requests: %s", requests.__version__) logger.info("environment:") for key, value in sorted(os.environ.items()): From 84314021dd26f254a93bc3f5ac56ac584b8accd3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 6 Nov 2022 19:32:50 +0100 Subject: [PATCH 188/262] Log versions of optional dependencies --- bpython/args.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bpython/args.py b/bpython/args.py index 0c514c37..2eb910d5 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -214,6 +214,27 @@ def callback(group): logger.info("pygments: %s", pygments.__version__) # type: ignore logger.info("pyxdg: %s", xdg.__version__) # type: ignore logger.info("requests: %s", requests.__version__) + + # versions of optional dependencies + try: + import pyperclip + + logger.info("pyperclip: %s", pyperclip.__version__) # type: ignore + except ImportError: + logger.info("pyperclip: not available") + try: + import jedi + + logger.info("jedi: %s", jedi.__version__) + except ImportError: + logger.info("jedi: not available") + try: + import watchdog + + logger.info("watchdog: available") + except ImportError: + logger.info("watchdog: not available") + logger.info("environment:") for key, value in sorted(os.environ.items()): if key.startswith("LC") or key.startswith("LANG") or key == "TERM": From b387c5a666d3bec79833754b177fd9f26dfe6886 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 6 Nov 2022 19:40:24 +0100 Subject: [PATCH 189/262] Remove no longer used pycheck config skip-checks: true --- .pycheckrc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .pycheckrc diff --git a/.pycheckrc b/.pycheckrc deleted file mode 100644 index e7050fad..00000000 --- a/.pycheckrc +++ /dev/null @@ -1 +0,0 @@ -blacklist = ['pyparsing', 'code', 'pygments/lexer'] From f797aa1d5af825e7ef266ded83488c587dcc725d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 3 Dec 2022 21:12:35 +0100 Subject: [PATCH 190/262] Fix Python 3.11 version constraints --- bpython/test/test_interpreter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index f53ec252..b2fb5275 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -112,7 +112,7 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - if (3, 11) <= sys.version_info[:2]: + if (3, 11, 0) <= sys.version_info[:3] < (3, 11, 1): expected = ( "Traceback (most recent call last):\n File " + green('""') From 352de3085e2f320000d8cd8d49378c4b14d22ec3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 3 Dec 2022 21:22:12 +0100 Subject: [PATCH 191/262] Fix type annotations --- bpython/cli.py | 10 ++++++---- bpython/translations/__init__.py | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 886dc2c8..bb1429a8 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -1299,7 +1299,7 @@ def show_list( arg_pos: Union[str, int, None], topline: Optional[inspection.FuncProps] = None, formatter: Optional[Callable] = None, - current_item: Union[str, Literal[False]] = None, + current_item: Optional[str] = None, ) -> None: v_items: Collection shared = ShowListState() @@ -1315,7 +1315,7 @@ def show_list( if items and formatter: items = [formatter(x) for x in items] - if current_item: + if current_item is not None: current_item = formatter(current_item) if topline: @@ -1492,8 +1492,10 @@ def tab(self, back: bool = False) -> bool: # 4. swap current word for a match list item elif self.matches_iter.matches: - current_match: Union[str, Literal[False]] = ( - back and self.matches_iter.previous() or next(self.matches_iter) + current_match = ( + self.matches_iter.previous() + if back + else next(self.matches_iter) ) try: f = None diff --git a/bpython/translations/__init__.py b/bpython/translations/__init__.py index 13c49802..0cb4c01f 100644 --- a/bpython/translations/__init__.py +++ b/bpython/translations/__init__.py @@ -2,7 +2,7 @@ import locale import os.path import sys -from typing import cast, List +from typing import Optional, cast, List from .. import package_dir @@ -17,7 +17,9 @@ def ngettext(singular, plural, n): return translator.ngettext(singular, plural, n) -def init(locale_dir: str = None, languages: List[str] = None) -> None: +def init( + locale_dir: Optional[str] = None, languages: Optional[List[str]] = None +) -> None: try: locale.setlocale(locale.LC_ALL, "") except locale.Error: From ebefde843bce5ec1e3f180c655308d5858672531 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 17 Dec 2022 21:24:18 +0100 Subject: [PATCH 192/262] Revert "Fix Python 3.11 version constraints" This reverts commit f797aa1d5af825e7ef266ded83488c587dcc725d. --- bpython/test/test_interpreter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index b2fb5275..f53ec252 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -112,7 +112,7 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - if (3, 11, 0) <= sys.version_info[:3] < (3, 11, 1): + if (3, 11) <= sys.version_info[:2]: expected = ( "Traceback (most recent call last):\n File " + green('""') From 71320bbfa318ec7aebd7bfafe3652715087fe7d2 Mon Sep 17 00:00:00 2001 From: Eric Burgess Date: Fri, 13 Jan 2023 13:06:13 -0600 Subject: [PATCH 193/262] Add more keywords to trigger auto-deindent --- bpython/curtsiesfrontend/repl.py | 4 +++- bpython/repl.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index cf7d9c37..037cf756 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1252,7 +1252,9 @@ def predicted_indent(self, line): elif ( line and ":" not in line - and line.strip().startswith(("return", "pass", "raise", "yield")) + and line.strip().startswith( + ("return", "pass", "...", "raise", "yield", "break", "continue") + ) ): indent = max(0, indent - self.config.tab_length) logger.debug("indent we found was %s", indent) diff --git a/bpython/repl.py b/bpython/repl.py index 8aea4e17..c964f090 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -1255,7 +1255,9 @@ def next_indentation(line, tab_length) -> int: if line.rstrip().endswith(":"): indentation += 1 elif indentation >= 1: - if line.lstrip().startswith(("return", "pass", "raise", "yield")): + if line.lstrip().startswith( + ("return", "pass", "...", "raise", "yield", "break", "continue") + ): indentation -= 1 return indentation From d0da704b96b37ce4611e91d59c09f922e3908246 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 14 Jan 2023 22:10:12 +0100 Subject: [PATCH 194/262] Update changelog for 0.24 --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1d902634..4f94198d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,9 +6,12 @@ Changelog General information: +* This release is focused on Python 3.11 support. New features: +* #980: Add more keywords to trigger auto-deindent. + Thanks to Eric Burgess Fixes: @@ -16,6 +19,7 @@ Fixes: Changes to dependencies: +* wheel is no required as part of pyproject.toml's build dependencies Support for Python 3.11 has been added. From b94ccc24cf94bb6e29bd2208f462fcc9b7c49cb2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 14 Jan 2023 22:18:10 +0100 Subject: [PATCH 195/262] Update readthedocs config again --- .readthedocs.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 942c1da0..a19293da 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,5 +1,13 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3" + +sphinx: + configuration: doc/sphinx/source/conf.py + python: install: - method: pip From d6e62b372504c290fa96825535317acf067463f6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 14 Jan 2023 22:27:13 +0100 Subject: [PATCH 196/262] Avoid bpython imports in setup.py --- setup.cfg | 2 ++ setup.py | 6 +----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 7955ee39..07f90115 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,8 @@ name = bpython long_description = file: README.rst license = MIT license_files = LICENSE +author = Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al. +author_email = bpython@googlegroups.com url = https://www.bpython-interpreter.org/ project_urls = GitHub = https://github.com/bpython/bpython diff --git a/setup.py b/setup.py index 12d4eeec..6790b9d7 100755 --- a/setup.py +++ b/setup.py @@ -124,9 +124,7 @@ def git_describe_to_python_version(version): cmdclass = {"build": build} -from bpython import package_dir, __author__ - -translations_dir = os.path.join(package_dir, "translations") +translations_dir = os.path.join("bpython", "translations") # localization options if using_translations: @@ -179,8 +177,6 @@ def git_describe_to_python_version(version): setup( version=version, - author=__author__, - author_email="robertanthonyfarrell@gmail.com", data_files=data_files, package_data={ "bpython": ["sample-config"], From 406f8713915566a77c7021daf160b8a8cc9ee863 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 14 Jan 2023 22:27:23 +0100 Subject: [PATCH 197/262] Update copyright year --- bpython/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/__init__.py b/bpython/__init__.py index adc00c06..dff06c0f 100644 --- a/bpython/__init__.py +++ b/bpython/__init__.py @@ -30,7 +30,7 @@ __author__ = ( "Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al." ) -__copyright__ = f"(C) 2008-2020 {__author__}" +__copyright__ = f"(C) 2008-2023 {__author__}" __license__ = "MIT" __version__ = version package_dir = os.path.abspath(os.path.dirname(__file__)) From b38f6c7a483302651b223e13a0c478224bc6d645 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 17 Dec 2022 21:44:27 +0100 Subject: [PATCH 198/262] Remove unused code --- bpython/test/test_interpreter.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index f53ec252..4a39cfe1 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -1,37 +1,25 @@ import sys -import re import unittest from curtsies.fmtfuncs import bold, green, magenta, cyan, red, plain -from unittest import mock from bpython.curtsiesfrontend import interpreter pypy = "PyPy" in sys.version -def remove_ansi(s): - return re.sub(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]".encode("ascii"), b"", s) +class Interpreter(interpreter.Interp): + def __init__(self): + super().__init__() + self.a = [] + def write(self, data): + self.a.append(data) -class TestInterpreter(unittest.TestCase): - def interp_errlog(self): - i = interpreter.Interp() - a = [] - i.write = a.append - return i, a - - def err_lineno(self, a): - strings = [x.__unicode__() for x in a] - for line in reversed(strings): - clean_line = remove_ansi(line) - m = re.search(r"line (\d+)[,]", clean_line) - if m: - return int(m.group(1)) - return None +class TestInterpreter(unittest.TestCase): def test_syntaxerror(self): - i, a = self.interp_errlog() + i = Interpreter() i.runsource("1.1.1.1") @@ -96,11 +84,12 @@ def test_syntaxerror(self): + "\n" ) + a = i.a self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) self.assertEqual(plain("").join(a), expected) def test_traceback(self): - i, a = self.interp_errlog() + i = Interpreter() def f(): return 1 / 0 @@ -142,6 +131,7 @@ def gfunc(): + "\n" ) + a = i.a self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) self.assertEqual(plain("").join(a), expected) From e1ca4522583f06b172674c4e11303fda044bf8f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 17:25:59 +0100 Subject: [PATCH 199/262] Start development of 0.25 --- CHANGELOG.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4f94198d..7ce32c42 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,21 @@ Changelog ========= +0.25 +---- + +General information: + + +New features: + + +Fixes: + + +Changes to dependencies: + + 0.24 ---- @@ -23,7 +38,6 @@ Changes to dependencies: Support for Python 3.11 has been added. - 0.23 ---- From c1f0385c79c0096265994b0d6e0aadf6db8a2709 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 17:34:55 +0100 Subject: [PATCH 200/262] Do not fail if curtsies is not available (fixes #978) Also delay imports of dependencies only used to log the version. --- bpython/args.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 2eb910d5..b9e68e1d 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -30,19 +30,13 @@ """ import argparse -from typing import Tuple, List, Optional, NoReturn, Callable import code -import curtsies -import cwcwidth -import greenlet import importlib.util import logging import os -import pygments -import requests import sys -import xdg from pathlib import Path +from typing import Tuple, List, Optional, NoReturn, Callable from . import __version__, __copyright__ from .config import default_config_path, Config @@ -205,10 +199,22 @@ def callback(group): bpython_logger.addHandler(logging.NullHandler()) curtsies_logger.addHandler(logging.NullHandler()) + import cwcwidth + import greenlet + import pygments + import requests + import xdg + logger.info("Starting bpython %s", __version__) logger.info("Python %s: %s", sys.executable, sys.version_info) # versions of required dependencies - logger.info("curtsies: %s", curtsies.__version__) + try: + import curtsies + + logger.info("curtsies: %s", curtsies.__version__) + except ImportError: + # may happen on Windows + logger.info("curtsies: not available") logger.info("cwcwidth: %s", cwcwidth.__version__) logger.info("greenlet: %s", greenlet.__version__) logger.info("pygments: %s", pygments.__version__) # type: ignore From 29d6f87f9688610a1f96cdcc32ce84f3ce435b7f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 17:52:17 +0100 Subject: [PATCH 201/262] Fix definition of write --- bpython/test/test_interpreter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 4a39cfe1..a4a32dd0 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -12,9 +12,7 @@ class Interpreter(interpreter.Interp): def __init__(self): super().__init__() self.a = [] - - def write(self, data): - self.a.append(data) + self.write = self.a.append class TestInterpreter(unittest.TestCase): From 46dc081faa21aa957f67761a15d0fb18031de87c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 18:00:39 +0100 Subject: [PATCH 202/262] Remove support for Python 3.7 (fixes #940) --- .github/workflows/build.yaml | 5 ++-- bpython/_typing_compat.py | 33 --------------------------- bpython/cli.py | 2 +- bpython/curtsies.py | 2 +- bpython/curtsiesfrontend/_internal.py | 3 +-- bpython/curtsiesfrontend/repl.py | 2 +- bpython/filelock.py | 3 +-- bpython/inspection.py | 12 ++++++++-- bpython/paste.py | 3 +-- bpython/repl.py | 14 ++++++------ doc/sphinx/source/contributing.rst | 2 +- doc/sphinx/source/releases.rst | 2 +- pyproject.toml | 2 +- requirements.txt | 1 - setup.cfg | 4 +--- 15 files changed, 29 insertions(+), 61 deletions(-) delete mode 100644 bpython/_typing_compat.py diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9cdcc1e9..1e2a374a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -10,17 +10,16 @@ on: jobs: build: runs-on: ubuntu-latest - continue-on-error: ${{ matrix.python-version == 'pypy-3.7' }} + continue-on-error: ${{ matrix.python-version == 'pypy-3.8' }} strategy: fail-fast: false matrix: python-version: - - "3.7" - "3.8" - "3.9" - "3.10" - "3.11" - - "pypy-3.7" + - "pypy-3.8" steps: - uses: actions/checkout@v3 with: diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py deleted file mode 100644 index 31fb6428..00000000 --- a/bpython/_typing_compat.py +++ /dev/null @@ -1,33 +0,0 @@ -# The MIT License -# -# Copyright (c) 2021 Sebastian Ramacher -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -try: - # introduced in Python 3.8 - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore - -try: - # introduced in Python 3.8 - from typing import Protocol -except ImportError: - from typing_extensions import Protocol # type: ignore diff --git a/bpython/cli.py b/bpython/cli.py index bb1429a8..0a03d513 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -69,12 +69,12 @@ Collection, Dict, TYPE_CHECKING, + Literal, ) if TYPE_CHECKING: from _curses import _CursesWindow -from ._typing_compat import Literal import unicodedata from dataclasses import dataclass diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 985f7085..6dc8d1f7 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -28,11 +28,11 @@ Generator, List, Optional, + Protocol, Sequence, Tuple, Union, ) -from ._typing_compat import Protocol logger = logging.getLogger(__name__) diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index 79c5e974..0480c1b0 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -22,8 +22,7 @@ import pydoc from types import TracebackType -from typing import Optional, Type -from .._typing_compat import Literal +from typing import Optional, Type, Literal from .. import _internal diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 037cf756..a3b32d44 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -17,13 +17,13 @@ Iterable, Dict, List, + Literal, Optional, Sequence, Tuple, Type, Union, ) -from .._typing_compat import Literal import greenlet from curtsies import ( diff --git a/bpython/filelock.py b/bpython/filelock.py index 429f708b..11f575b6 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -20,8 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -from typing import Optional, Type, IO -from ._typing_compat import Literal +from typing import Optional, Type, IO, Literal from types import TracebackType has_fcntl = True diff --git a/bpython/inspection.py b/bpython/inspection.py index db0a397c..fe1e3a0a 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -26,9 +26,17 @@ import pydoc import re from dataclasses import dataclass -from typing import Any, Callable, Optional, Type, Dict, List, ContextManager +from typing import ( + Any, + Callable, + Optional, + Type, + Dict, + List, + ContextManager, + Literal, +) from types import MemberDescriptorType, TracebackType -from ._typing_compat import Literal from pygments.token import Token from pygments.lexers import Python3Lexer diff --git a/bpython/paste.py b/bpython/paste.py index ceba5938..fd140a0e 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -22,7 +22,7 @@ import errno import subprocess -from typing import Optional, Tuple +from typing import Optional, Tuple, Protocol from urllib.parse import urljoin, urlparse import requests @@ -30,7 +30,6 @@ from .config import getpreferredencoding from .translations import _ -from ._typing_compat import Protocol class PasteFailed(Exception): diff --git a/bpython/repl.py b/bpython/repl.py index c964f090..fe72a4db 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -40,19 +40,19 @@ from pathlib import Path from types import ModuleType, TracebackType from typing import ( + Any, + Callable, + Dict, Iterable, - cast, List, - Tuple, - Any, + Literal, Optional, + TYPE_CHECKING, + Tuple, Type, Union, - Callable, - Dict, - TYPE_CHECKING, + cast, ) -from ._typing_compat import Literal from pygments.lexers import Python3Lexer from pygments.token import Token, _TokenType diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 54fd56c6..9e0f6bc4 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -17,7 +17,7 @@ the time of day. Getting your development environment set up ------------------------------------------- -bpython supports Python 3.7 and newer. The code is compatible with all +bpython supports Python 3.8 and newer. The code is compatible with all supported versions. Using a virtual environment is probably a good idea. Create a virtual diff --git a/doc/sphinx/source/releases.rst b/doc/sphinx/source/releases.rst index 738c24ff..fcce5c1c 100644 --- a/doc/sphinx/source/releases.rst +++ b/doc/sphinx/source/releases.rst @@ -45,7 +45,7 @@ A checklist to perform some manual tests before a release: Check that all of the following work before a release: -* Runs under Python 3.7 - 3.9 +* Runs under Python 3.8 - 3.11 * Save * Rewind * Pastebin diff --git a/pyproject.toml b/pyproject.toml index 6526fb9e..b7bd3196 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 80 -target_version = ["py37"] +target_version = ["py38"] include = '\.pyi?$' exclude = ''' /( diff --git a/requirements.txt b/requirements.txt index ba8b126d..4c750a69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ Pygments -backports.cached-property; python_version < "3.8" curtsies >=0.4.0 cwcwidth greenlet diff --git a/setup.cfg b/setup.cfg index 07f90115..081af0bc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ classifiers = Programming Language :: Python :: 3 [options] -python_requires = >=3.7 +python_requires = >=3.8 packages = bpython bpython.curtsiesfrontend @@ -22,14 +22,12 @@ packages = bpython.translations bpdb install_requires = - backports.cached-property; python_version < "3.8" curtsies >=0.4.0 cwcwidth greenlet pygments pyxdg requests - typing-extensions; python_version < "3.8" [options.extras_require] clipboard = pyperclip From 274f423e4a1b742904a87d5cebe18258ecd095a9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 18:13:26 +0100 Subject: [PATCH 203/262] Remove < 3.8 workaround --- bpython/importcompletion.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index c1e073f8..9b0edaab 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -175,17 +175,8 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: # Path is on skiplist return - try: - # https://bugs.python.org/issue34541 - # Once we migrate to Python 3.8, we can change it back to directly iterator over - # path.iterdir(). - children = tuple(path.iterdir()) - except OSError: - # Path is not readable - return - finder = importlib.machinery.FileFinder(str(path), *LOADERS) # type: ignore - for p in children: + for p in path.iterdir(): if p.name.startswith(".") or p.name == "__pycache__": # Impossible to import from names starting with . and we can skip __pycache__ continue From 1fb4041aa641d9b06047489c487cafbeb7d3d7b2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 19:06:34 +0100 Subject: [PATCH 204/262] Handle OSError again --- bpython/importcompletion.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 9b0edaab..7833a932 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -175,8 +175,14 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: # Path is on skiplist return + try: + children = path.iterdir() + except OSError: + # Path is not readable + return + finder = importlib.machinery.FileFinder(str(path), *LOADERS) # type: ignore - for p in path.iterdir(): + for p in children: if p.name.startswith(".") or p.name == "__pycache__": # Impossible to import from names starting with . and we can skip __pycache__ continue From df3750d2a93ce69d9676f6b17a0da858df7ca983 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 19:43:34 +0100 Subject: [PATCH 205/262] Really handle OSError --- bpython/importcompletion.py | 108 ++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 7833a932..9df140c6 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -175,65 +175,67 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: # Path is on skiplist return - try: - children = path.iterdir() - except OSError: - # Path is not readable - return - finder = importlib.machinery.FileFinder(str(path), *LOADERS) # type: ignore - for p in children: - if p.name.startswith(".") or p.name == "__pycache__": - # Impossible to import from names starting with . and we can skip __pycache__ - continue - elif any(fnmatch.fnmatch(p.name, entry) for entry in self.skiplist): - # Path is on skiplist - continue - elif not any(p.name.endswith(suffix) for suffix in SUFFIXES): - # Possibly a package - if "." in p.name: + try: + for p in path.iterdir(): + if p.name.startswith(".") or p.name == "__pycache__": + # Impossible to import from names starting with . and we can skip __pycache__ continue - elif p.is_dir(): - # Unfortunately, CPython just crashes if there is a directory - # which ends with a python extension, so work around. - continue - name = p.name - for suffix in SUFFIXES: - if name.endswith(suffix): - name = name[: -len(suffix)] - break - if name == "badsyntax_pep3120": - # Workaround for issue #166 - continue - - package_pathname = None - try: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", ImportWarning) - spec = finder.find_spec(name) - if spec is None: + elif any( + fnmatch.fnmatch(p.name, entry) for entry in self.skiplist + ): + # Path is on skiplist + continue + elif not any(p.name.endswith(suffix) for suffix in SUFFIXES): + # Possibly a package + if "." in p.name: continue - if spec.submodule_search_locations is not None: - package_pathname = spec.submodule_search_locations[0] - except (ImportError, OSError, SyntaxError, UnicodeEncodeError): - # UnicodeEncodeError happens with Python 3 when there is a filename in some invalid encoding - continue + elif p.is_dir(): + # Unfortunately, CPython just crashes if there is a directory + # which ends with a python extension, so work around. + continue + name = p.name + for suffix in SUFFIXES: + if name.endswith(suffix): + name = name[: -len(suffix)] + break + if name == "badsyntax_pep3120": + # Workaround for issue #166 + continue - if package_pathname is not None: - path_real = Path(package_pathname).resolve() + package_pathname = None try: - stat = path_real.stat() - except OSError: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", ImportWarning) + spec = finder.find_spec(name) + if spec is None: + continue + if spec.submodule_search_locations is not None: + package_pathname = spec.submodule_search_locations[ + 0 + ] + except (ImportError, OSError, SyntaxError, UnicodeEncodeError): + # UnicodeEncodeError happens with Python 3 when there is a filename in some invalid encoding continue - loaded_inode = _LoadedInode(stat.st_dev, stat.st_ino) - if loaded_inode not in self.paths: - self.paths.add(loaded_inode) - for subname in self.find_modules(path_real): - if subname is None: - yield None # take a break to avoid unresponsiveness - elif subname != "__init__": - yield f"{name}.{subname}" - yield name + + if package_pathname is not None: + path_real = Path(package_pathname).resolve() + try: + stat = path_real.stat() + except OSError: + continue + loaded_inode = _LoadedInode(stat.st_dev, stat.st_ino) + if loaded_inode not in self.paths: + self.paths.add(loaded_inode) + for subname in self.find_modules(path_real): + if subname is None: + yield None # take a break to avoid unresponsiveness + elif subname != "__init__": + yield f"{name}.{subname}" + yield name + except OSError: + # Path is not readable + return yield None # take a break to avoid unresponsiveness def find_all_modules( From b537a508ede7b09eb118f20898bd3c8c0a2fe7f9 Mon Sep 17 00:00:00 2001 From: Ganden Schaffner Date: Sun, 29 Jan 2023 00:00:00 -0800 Subject: [PATCH 206/262] Fix URL typo in package metadata --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 081af0bc..8c4294d9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ author_email = bpython@googlegroups.com url = https://www.bpython-interpreter.org/ project_urls = GitHub = https://github.com/bpython/bpython - Documentation = https://doc.bpython-interpreter.org + Documentation = https://docs.bpython-interpreter.org classifiers = Programming Language :: Python :: 3 From 11a72fb11d48f8acd018acd24a538448ff57c2c2 Mon Sep 17 00:00:00 2001 From: Nitant Patel Date: Thu, 2 Mar 2023 00:48:19 -0500 Subject: [PATCH 207/262] Clarify fork instructions in contributing docs `setup.py` expects to be able to find git tags in order to properly set the package version. If a fork does not have any tags, `pip install -e .` will fail due to the default `"unknown"` being an invalid version. --- doc/sphinx/source/contributing.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 9e0f6bc4..32b1ea86 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -30,7 +30,10 @@ environment with # necessary every time you work on bpython $ source bpython-dev/bin/activate -Fork bpython in the GitHub web interface, then clone the repo: +Fork bpython in the GitHub web interface. Be sure to include the tags +in your fork by un-selecting the option to copy only the main branch. + +Then, clone the forked repo: .. code-block:: bash From 7df8e6f89036775ad659983d45deda98a175d2be Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 2 Mar 2023 09:53:04 +0100 Subject: [PATCH 208/262] Apply black --- bpython/autocomplete.py | 2 +- bpython/cli.py | 1 + bpython/config.py | 2 +- bpython/curtsiesfrontend/manual_readline.py | 1 - bpython/curtsiesfrontend/repl.py | 1 - bpython/repl.py | 10 ++++------ bpython/test/__init__.py | 1 - bpython/test/test_repl.py | 4 ++-- bpython/urwid.py | 2 -- 9 files changed, 9 insertions(+), 15 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index b97fd86f..e0849c6d 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -57,6 +57,7 @@ logger = logging.getLogger(__name__) + # Autocomplete modes class AutocompleteModes(Enum): NONE = "none" @@ -381,7 +382,6 @@ def format(self, filename: str) -> str: class AttrCompletion(BaseCompletionType): - attr_matches_re = LazyReCompile(r"(\w+(\.\w+)*)\.(\w*)") def matches( diff --git a/bpython/cli.py b/bpython/cli.py index 0a03d513..fcfd11fa 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -264,6 +264,7 @@ def readlines(self, size: int = -1) -> List[str]: # the addstr stuff to a higher level. # + # Have to ignore the return type on this one because the colors variable # is Optional[MutableMapping[str, int]] but for the purposes of this # function it can't be None diff --git a/bpython/config.py b/bpython/config.py index 29b906dd..5123ec22 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -90,7 +90,7 @@ def fill_config_with_default_values( if not config.has_section(section): config.add_section(section) - for (opt, val) in default_values[section].items(): + for opt, val in default_values[section].items(): if not config.has_option(section, opt): config.set(section, opt, str(val)) diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py index f95e66c5..206e5278 100644 --- a/bpython/curtsiesfrontend/manual_readline.py +++ b/bpython/curtsiesfrontend/manual_readline.py @@ -16,7 +16,6 @@ class AbstractEdits: - default_kwargs = { "line": "hello world", "cursor_offset": 5, diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index a3b32d44..e4819e19 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -893,7 +893,6 @@ def insert_char_pair_end(self, e): self.add_normal_character(e) def get_last_word(self): - previous_word = _last_word(self.rl_history.entry) word = _last_word(self.rl_history.back()) line = self.current_line diff --git a/bpython/repl.py b/bpython/repl.py index fe72a4db..f30cfa31 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -465,7 +465,6 @@ def cursor_offset(self, value: int) -> None: self._set_cursor_offset(value) if TYPE_CHECKING: - # not actually defined, subclasses must define cpos: int @@ -567,7 +566,7 @@ def current_string(self, concatenate=False): return "" opening = string_tokens.pop()[1] string = list() - for (token, value) in reversed(string_tokens): + for token, value in reversed(string_tokens): if token is Token.Text: continue elif opening is None: @@ -602,7 +601,7 @@ def _funcname_and_argnum( # if keyword is not None, we've encountered a keyword and so we're done counting stack = [_FuncExpr("", "", 0, "")] try: - for (token, value) in Python3Lexer().get_tokens(line): + for token, value in Python3Lexer().get_tokens(line): if token is Token.Punctuation: if value in "([{": stack.append(_FuncExpr("", "", 0, value)) @@ -692,7 +691,6 @@ def get_args(self): # py3 f.__new__.__class__ is not object.__new__.__class__ ): - class_f = f.__new__ if class_f: @@ -1117,7 +1115,7 @@ def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: line_tokens: List[Tuple[_TokenType, str]] = list() saved_tokens: List[Tuple[_TokenType, str]] = list() search_for_paren = True - for (token, value) in split_lines(all_tokens): + for token, value in split_lines(all_tokens): pos += len(value) if token is Token.Text and value == "\n": line += 1 @@ -1263,7 +1261,7 @@ def next_indentation(line, tab_length) -> int: def split_lines(tokens): - for (token, value) in tokens: + for token, value in tokens: if not value: continue while value: diff --git a/bpython/test/__init__.py b/bpython/test/__init__.py index 7722278c..4618eca4 100644 --- a/bpython/test/__init__.py +++ b/bpython/test/__init__.py @@ -13,7 +13,6 @@ def setUpClass(cls): class MagicIterMock(unittest.mock.MagicMock): - __next__ = unittest.mock.Mock(return_value=None) diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 63309364..b3e8912f 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -183,7 +183,7 @@ def set_input_line(self, line): self.repl.cursor_offset = len(line) def test_func_name(self): - for (line, expected_name) in [ + for line, expected_name in [ ("spam(", "spam"), # map pydoc has no signature in pypy ("spam(any([]", "any") if pypy else ("spam(map([]", "map"), @@ -194,7 +194,7 @@ def test_func_name(self): self.assertEqual(self.repl.current_func.__name__, expected_name) def test_func_name_method_issue_479(self): - for (line, expected_name) in [ + for line, expected_name in [ ("o.spam(", "spam"), # map pydoc has no signature in pypy ("o.spam(any([]", "any") if pypy else ("o.spam(map([]", "map"), diff --git a/bpython/urwid.py b/bpython/urwid.py index 66054097..4b41c12a 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -74,7 +74,6 @@ else: class EvalProtocol(basic.LineOnlyReceiver): - delimiter = "\n" def __init__(self, myrepl): @@ -570,7 +569,6 @@ def file_prompt(self, s: str) -> Optional[str]: class URWIDRepl(repl.Repl): - _time_between_redraws = 0.05 # seconds def __init__(self, event_loop, palette, interpreter, config): From 44c3b702037c1c1a5f7681b7f86c0246fbf0db7e Mon Sep 17 00:00:00 2001 From: supakeen Date: Tue, 9 Nov 2021 16:51:56 +0000 Subject: [PATCH 209/262] Remove deprecated curses (cli) rendering backend. --- CHANGELOG.rst | 4 +- bpython/cli.py | 2094 ----------------- bpython/test/test_repl.py | 152 +- bpython/translations/bpython.pot | 30 +- .../translations/de/LC_MESSAGES/bpython.po | 34 +- .../translations/es_ES/LC_MESSAGES/bpython.po | 31 +- .../translations/fr_FR/LC_MESSAGES/bpython.po | 31 +- .../translations/it_IT/LC_MESSAGES/bpython.po | 31 +- .../translations/nl_NL/LC_MESSAGES/bpython.po | 31 +- doc/sphinx/source/tips.rst | 2 +- doc/sphinx/source/windows.rst | 6 - setup.cfg | 1 - 12 files changed, 16 insertions(+), 2431 deletions(-) delete mode 100644 bpython/cli.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7ce32c42..d7ecb3ab 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,9 @@ Changelog General information: +* The bpython-cli rendering backend has been removed following deprecation in + version 0.19. + New features: @@ -44,7 +47,6 @@ Support for Python 3.11 has been added. General information: * More and more type annotations have been added to the bpython code base. -* Some work has been performed to stop relying on blessings. New features: diff --git a/bpython/cli.py b/bpython/cli.py deleted file mode 100644 index fcfd11fa..00000000 --- a/bpython/cli.py +++ /dev/null @@ -1,2094 +0,0 @@ -# The MIT License -# -# Copyright (c) 2008 Bob Farrell -# Copyright (c) bpython authors -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# mypy: disallow_untyped_defs=True -# mypy: disallow_untyped_calls=True - -# Modified by Brandon Navra -# Notes for Windows -# Prerequisites -# - Curses -# - pyreadline -# -# Added -# -# - Support for running on windows command prompt -# - input from numpad keys -# -# Issues -# -# - Suspend doesn't work nor does detection of resizing of screen -# - Instead the suspend key exits the program -# - View source doesn't work on windows unless you install the less program (From GnuUtils or Cygwin) - - -import curses -import errno -import functools -import math -import os -import platform -import re -import struct -import sys -import time -from typing import ( - Iterator, - NoReturn, - List, - MutableMapping, - Any, - Callable, - TypeVar, - cast, - IO, - Iterable, - Optional, - Union, - Tuple, - Collection, - Dict, - TYPE_CHECKING, - Literal, -) - -if TYPE_CHECKING: - from _curses import _CursesWindow - -import unicodedata -from dataclasses import dataclass - -if platform.system() != "Windows": - import signal # Windows does not have job control - import termios # Windows uses curses - import fcntl # Windows uses curses - - -# These are used for syntax highlighting -from pygments import format -from pygments.formatters import TerminalFormatter -from pygments.lexers import Python3Lexer -from pygments.token import Token, _TokenType -from .formatter import BPythonFormatter - -# This for config -from .config import getpreferredencoding, Config - -# This for keys -from .keys import cli_key_dispatch as key_dispatch - -# This for i18n -from . import translations -from .translations import _ - -from . import repl, inspection -from . import args as bpargs -from .pager import page -from .args import parse as argsparse - -F = TypeVar("F", bound=Callable[..., Any]) - -# --- module globals --- -stdscr = None -colors: Optional[MutableMapping[str, int]] = None - -DO_RESIZE = False -# --- - - -@dataclass -class ShowListState: - cols: int = 0 - rows: int = 0 - wl: int = 0 - - -def forward_if_not_current(func: F) -> F: - @functools.wraps(func) - def newfunc(self, *args, **kwargs): # type: ignore - dest = self.get_dest() - if self is dest: - return func(self, *args, **kwargs) - else: - return getattr(self.get_dest(), newfunc.__name__)(*args, **kwargs) - - return cast(F, newfunc) - - -class FakeStream: - """Provide a fake file object which calls functions on the interface - provided.""" - - def __init__(self, interface: "CLIRepl", get_dest: IO[str]) -> None: - self.encoding: str = getpreferredencoding() - self.interface = interface - self.get_dest = get_dest - - @forward_if_not_current - def write(self, s: str) -> None: - self.interface.write(s) - - @forward_if_not_current - def writelines(self, l: Iterable[str]) -> None: - for s in l: - self.write(s) - - def isatty(self) -> bool: - # some third party (amongst them mercurial) depend on this - return True - - def flush(self) -> None: - self.interface.flush() - - -class FakeStdin: - """Provide a fake stdin type for things like raw_input() etc.""" - - def __init__(self, interface: "CLIRepl") -> None: - """Take the curses Repl on init and assume it provides a get_key method - which, fortunately, it does.""" - - self.encoding = getpreferredencoding() - self.interface = interface - self.buffer: List[str] = list() - - def __iter__(self) -> Iterator: - return iter(self.readlines()) - - def flush(self) -> None: - """Flush the internal buffer. This is a no-op. Flushing stdin - doesn't make any sense anyway.""" - - def write(self, value: str) -> NoReturn: - # XXX IPython expects sys.stdin.write to exist, there will no doubt be - # others, so here's a hack to keep them happy - raise OSError(errno.EBADF, "sys.stdin is read-only") - - def isatty(self) -> bool: - return True - - def readline(self, size: int = -1) -> str: - """I can't think of any reason why anything other than readline would - be useful in the context of an interactive interpreter so this is the - only one I've done anything with. The others are just there in case - someone does something weird to stop it from blowing up.""" - - if not size: - return "" - elif self.buffer: - buffer = self.buffer.pop(0) - else: - buffer = "" - - curses.raw(True) - try: - while not buffer.endswith(("\n", "\r")): - key = self.interface.get_key() - if key in (curses.erasechar(), "KEY_BACKSPACE"): - y, x = self.interface.scr.getyx() - if buffer: - self.interface.scr.delch(y, x - 1) - buffer = buffer[:-1] - continue - elif key == chr(4) and not buffer: - # C-d - return "" - elif key not in ("\n", "\r") and ( - len(key) > 1 or unicodedata.category(key) == "Cc" - ): - continue - sys.stdout.write(key) - # Include the \n in the buffer - raw_input() seems to deal with trailing - # linebreaks and will break if it gets an empty string. - buffer += key - finally: - curses.raw(False) - - if size > 0: - rest = buffer[size:] - if rest: - self.buffer.append(rest) - buffer = buffer[:size] - - return buffer - - def read(self, size: Optional[int] = None) -> str: - if size == 0: - return "" - - data = list() - while size is None or size > 0: - line = self.readline(size or -1) - if not line: - break - if size is not None: - size -= len(line) - data.append(line) - - return "".join(data) - - def readlines(self, size: int = -1) -> List[str]: - return list(iter(self.readline, "")) - - -# TODO: -# -# Tab completion does not work if not at the end of the line. -# -# Numerous optimisations can be made but it seems to do all the lookup stuff -# fast enough on even my crappy server so I'm not too bothered about that -# at the moment. -# -# The popup window that displays the argspecs and completion suggestions -# needs to be an instance of a ListWin class or something so I can wrap -# the addstr stuff to a higher level. -# - - -# Have to ignore the return type on this one because the colors variable -# is Optional[MutableMapping[str, int]] but for the purposes of this -# function it can't be None -def get_color(config: Config, name: str) -> int: # type: ignore[return] - global colors - if colors: - return colors[config.color_scheme[name].lower()] - - -def get_colpair(config: Config, name: str) -> int: - return curses.color_pair(get_color(config, name) + 1) - - -def make_colors(config: Config) -> Dict[str, int]: - """Init all the colours in curses and bang them into a dictionary""" - - # blacK, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default: - c = { - "k": 0, - "r": 1, - "g": 2, - "y": 3, - "b": 4, - "m": 5, - "c": 6, - "w": 7, - "d": -1, - } - - if platform.system() == "Windows": - c = dict( - list(c.items()) - + [ - ("K", 8), - ("R", 9), - ("G", 10), - ("Y", 11), - ("B", 12), - ("M", 13), - ("C", 14), - ("W", 15), - ] - ) - - for i in range(63): - if i > 7: - j = i // 8 - else: - j = c[config.color_scheme["background"]] - curses.init_pair(i + 1, i % 8, j) - - return c - - -class CLIInteraction(repl.Interaction): - def __init__(self, config: Config, statusbar: "Statusbar"): - super().__init__(config) - self.statusbar = statusbar - - def confirm(self, q: str) -> bool: - """Ask for yes or no and return boolean""" - try: - reply = self.statusbar.prompt(q) - except ValueError: - return False - - return reply.lower() in (_("y"), _("yes")) - - def notify( - self, s: str, n: float = 10.0, wait_for_keypress: bool = False - ) -> None: - self.statusbar.message(s, n) - - def file_prompt(self, s: str) -> Optional[str]: - return self.statusbar.prompt(s) - - -class CLIRepl(repl.Repl): - def __init__( - self, - scr: "_CursesWindow", - interp: repl.Interpreter, - statusbar: "Statusbar", - config: Config, - idle: Optional[Callable] = None, - ): - super().__init__(interp, config) - # mypy doesn't quite understand the difference between a class variable with a callable type and a method. - # https://github.com/python/mypy/issues/2427 - self.interp.writetb = self.writetb # type:ignore[assignment] - self.scr: "_CursesWindow" = scr - self.stdout_hist = "" # native str (bytes in Py2, unicode in Py3) - self.list_win = newwin(get_colpair(config, "background"), 1, 1, 1, 1) - self.cpos = 0 - self.do_exit = False - self.exit_value: Tuple[Any, ...] = () - self.f_string = "" - self.idle = idle - self.in_hist = False - self.paste_mode = False - self.last_key_press = time.time() - self.s = "" - self.statusbar = statusbar - self.formatter = BPythonFormatter(config.color_scheme) - self.interact = CLIInteraction(self.config, statusbar=self.statusbar) - self.ix: int - self.iy: int - self.arg_pos: Union[str, int, None] - self.prev_block_finished: int - - if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1: - config.cli_suggestion_width = 0.8 - - def _get_cursor_offset(self) -> int: - return len(self.s) - self.cpos - - def _set_cursor_offset(self, offset: int) -> None: - self.cpos = len(self.s) - offset - - def addstr(self, s: str) -> None: - """Add a string to the current input line and figure out - where it should go, depending on the cursor position.""" - self.rl_history.reset() - if not self.cpos: - self.s += s - else: - l = len(self.s) - self.s = self.s[: l - self.cpos] + s + self.s[l - self.cpos :] - - self.complete() - - def atbol(self) -> bool: - """Return True or False accordingly if the cursor is at the beginning - of the line (whitespace is ignored). This exists so that p_key() knows - how to handle the tab key being pressed - if there is nothing but white - space before the cursor then process it as a normal tab otherwise - attempt tab completion.""" - - return not self.s.lstrip() - - # This function shouldn't return None because of pos -= self.bs() later on - def bs(self, delete_tabs: bool = True) -> int: # type: ignore[return-value] - """Process a backspace""" - - self.rl_history.reset() - y, x = self.scr.getyx() - - if not self.s: - return None # type: ignore[return-value] - - if x == self.ix and y == self.iy: - return None # type: ignore[return-value] - - n = 1 - - self.clear_wrapped_lines() - - if not self.cpos: - # I know the nested if blocks look nasty. :( - if self.atbol() and delete_tabs: - n = len(self.s) % self.config.tab_length - if not n: - n = self.config.tab_length - - self.s = self.s[:-n] - else: - self.s = self.s[: -self.cpos - 1] + self.s[-self.cpos :] - - self.print_line(self.s, clr=True) - - return n - - def bs_word(self) -> str: - self.rl_history.reset() - pos = len(self.s) - self.cpos - 1 - deleted = [] - # First we delete any space to the left of the cursor. - while pos >= 0 and self.s[pos] == " ": - deleted.append(self.s[pos]) - pos -= self.bs() - # Then we delete a full word. - while pos >= 0 and self.s[pos] != " ": - deleted.append(self.s[pos]) - pos -= self.bs() - - return "".join(reversed(deleted)) - - def check(self) -> None: - """Check if paste mode should still be active and, if not, deactivate - it and force syntax highlighting.""" - - if ( - self.paste_mode - and time.time() - self.last_key_press > self.config.paste_time - ): - self.paste_mode = False - self.print_line(self.s) - - def clear_current_line(self) -> None: - """Called when a SyntaxError occurred in the interpreter. It is - used to prevent autoindentation from occurring after a - traceback.""" - repl.Repl.clear_current_line(self) - self.s = "" - - def clear_wrapped_lines(self) -> None: - """Clear the wrapped lines of the current input.""" - # curses does not handle this on its own. Sad. - height, width = self.scr.getmaxyx() - max_y = min(self.iy + (self.ix + len(self.s)) // width + 1, height) - for y in range(self.iy + 1, max_y): - self.scr.move(y, 0) - self.scr.clrtoeol() - - def complete(self, tab: bool = False) -> None: - """Get Autocomplete list and window. - - Called whenever these should be updated, and called - with tab - """ - if self.paste_mode: - self.scr.touchwin() # TODO necessary? - return - - list_win_visible = repl.Repl.complete(self, tab) - - if list_win_visible: - try: - f = None - if self.matches_iter.completer: - f = self.matches_iter.completer.format - - self.show_list( - self.matches_iter.matches, - self.arg_pos, - topline=self.funcprops, - formatter=f, - ) - except curses.error: - # XXX: This is a massive hack, it will go away when I get - # cusswords into a good enough state that we can start - # using it. - self.list_win.border() - self.list_win.refresh() - list_win_visible = False - if not list_win_visible: - self.scr.redrawwin() - self.scr.refresh() - - def clrtobol(self) -> None: - """Clear from cursor to beginning of line; usual C-u behaviour""" - self.clear_wrapped_lines() - - if not self.cpos: - self.s = "" - else: - self.s = self.s[-self.cpos :] - - self.print_line(self.s, clr=True) - self.scr.redrawwin() - self.scr.refresh() - - def _get_current_line(self) -> str: - return self.s - - def _set_current_line(self, line: str) -> None: - self.s = line - - def cut_to_buffer(self) -> None: - """Clear from cursor to end of line, placing into cut buffer""" - self.cut_buffer = self.s[-self.cpos :] - self.s = self.s[: -self.cpos] - self.cpos = 0 - self.print_line(self.s, clr=True) - self.scr.redrawwin() - self.scr.refresh() - - def delete(self) -> None: - """Process a del""" - if not self.s: - return - - if self.mvc(-1): - self.bs(False) - - def echo(self, s: str, redraw: bool = True) -> None: - """Parse and echo a formatted string with appropriate attributes. It - uses the formatting method as defined in formatter.py to parse the - strings. It won't update the screen if it's reevaluating the code (as it - does with undo).""" - a = get_colpair(self.config, "output") - if "\x01" in s: - rx = re.search("\x01([A-Za-z])([A-Za-z]?)", s) - if rx: - fg = rx.groups()[0] - bg = rx.groups()[1] - col_num = self._C[fg.lower()] - if bg and bg != "I": - col_num *= self._C[bg.lower()] - - a = curses.color_pair(int(col_num) + 1) - if bg == "I": - a = a | curses.A_REVERSE - s = re.sub("\x01[A-Za-z][A-Za-z]?", "", s) - if fg.isupper(): - a = a | curses.A_BOLD - s = s.replace("\x03", "") - s = s.replace("\x01", "") - - # Replace NUL bytes, as addstr raises an exception otherwise - s = s.replace("\0", "") - # Replace \r\n bytes, as addstr remove the current line otherwise - s = s.replace("\r\n", "\n") - - self.scr.addstr(s, a) - - if redraw and not self.evaluating: - self.scr.refresh() - - def end(self, refresh: bool = True) -> bool: - self.cpos = 0 - h, w = gethw() - y, x = divmod(len(self.s) + self.ix, w) - y += self.iy - self.scr.move(y, x) - if refresh: - self.scr.refresh() - - return True - - def hbegin(self) -> None: - """Replace the active line with first line in history and - increment the index to keep track""" - self.cpos = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.first() - self.print_line(self.s, clr=True) - - def hend(self) -> None: - """Same as hbegin() but, well, forward""" - self.cpos = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.last() - self.print_line(self.s, clr=True) - - def back(self) -> None: - """Replace the active line with previous line in history and - increment the index to keep track""" - - self.cpos = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.back() - self.print_line(self.s, clr=True) - - def fwd(self) -> None: - """Same as back() but, well, forward""" - - self.cpos = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.forward() - self.print_line(self.s, clr=True) - - def search(self) -> None: - """Search with the partial matches from the history object.""" - - self.cpo = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.back(start=False, search=True) - self.print_line(self.s, clr=True) - - def get_key(self) -> str: - key = "" - while True: - try: - key += self.scr.getkey() - # Seems like we get a in the locale's encoding - # encoded string in Python 3 as well, but of - # type str instead of bytes, hence convert it to - # bytes first and decode then - key = key.encode("latin-1").decode(getpreferredencoding()) - self.scr.nodelay(False) - except UnicodeDecodeError: - # Yes, that actually kind of sucks, but I don't see another way to get - # input right - self.scr.nodelay(True) - except curses.error: - # I'm quite annoyed with the ambiguity of this exception handler. I previously - # caught "curses.error, x" and accessed x.message and checked that it was "no - # input", which seemed a crappy way of doing it. But then I ran it on a - # different computer and the exception seems to have entirely different - # attributes. So let's hope getkey() doesn't raise any other crazy curses - # exceptions. :) - self.scr.nodelay(False) - # XXX What to do here? Raise an exception? - if key: - return key - else: - if key != "\x00": - t = time.time() - self.paste_mode = ( - t - self.last_key_press <= self.config.paste_time - ) - self.last_key_press = t - return key - else: - key = "" - finally: - if self.idle: - self.idle(self) - - def get_line(self) -> str: - """Get a line of text and return it - This function initialises an empty string and gets the - curses cursor position on the screen and stores it - for the echo() function to use later (I think). - Then it waits for key presses and passes them to p_key(), - which returns None if Enter is pressed (that means "Return", - idiot).""" - - self.s = "" - self.rl_history.reset() - self.iy, self.ix = self.scr.getyx() - - if not self.paste_mode: - for _ in range(self.next_indentation()): - self.p_key("\t") - - self.cpos = 0 - - while True: - key = self.get_key() - if self.p_key(key) is None: - if self.config.cli_trim_prompts and self.s.startswith(">>> "): - self.s = self.s[4:] - return self.s - - def home(self, refresh: bool = True) -> bool: - self.scr.move(self.iy, self.ix) - self.cpos = len(self.s) - if refresh: - self.scr.refresh() - return True - - def lf(self) -> None: - """Process a linefeed character; it only needs to check the - cursor position and move appropriately so it doesn't clear - the current line after the cursor.""" - if self.cpos: - for _ in range(self.cpos): - self.mvc(-1) - - # Reprint the line (as there was maybe a highlighted paren in it) - self.print_line(self.s, newline=True) - self.echo("\n") - - def mkargspec( - self, - topline: inspection.FuncProps, - in_arg: Union[str, int, None], - down: bool, - ) -> int: - """This figures out what to do with the argspec and puts it nicely into - the list window. It returns the number of lines used to display the - argspec. It's also kind of messy due to it having to call so many - addstr() to get the colouring right, but it seems to be pretty - sturdy.""" - - r = 3 - fn = topline.func - args = topline.argspec.args - kwargs = topline.argspec.defaults - _args = topline.argspec.varargs - _kwargs = topline.argspec.varkwargs - is_bound_method = topline.is_bound_method - kwonly = topline.argspec.kwonly - kwonly_defaults = topline.argspec.kwonly_defaults or dict() - max_w = int(self.scr.getmaxyx()[1] * 0.6) - self.list_win.erase() - self.list_win.resize(3, max_w) - h, w = self.list_win.getmaxyx() - - self.list_win.addstr("\n ") - self.list_win.addstr( - fn, get_colpair(self.config, "name") | curses.A_BOLD - ) - self.list_win.addstr(": (", get_colpair(self.config, "name")) - maxh = self.scr.getmaxyx()[0] - - if is_bound_method and isinstance(in_arg, int): - in_arg += 1 - - punctuation_colpair = get_colpair(self.config, "punctuation") - - for k, i in enumerate(args): - y, x = self.list_win.getyx() - ln = len(str(i)) - kw = None - if kwargs and k + 1 > len(args) - len(kwargs): - kw = repr(kwargs[k - (len(args) - len(kwargs))]) - ln += len(kw) + 1 - - if ln + x >= w: - ty = self.list_win.getbegyx()[0] - if not down and ty > 0: - h += 1 - self.list_win.mvwin(ty - 1, 1) - self.list_win.resize(h, w) - elif down and h + r < maxh - ty: - h += 1 - self.list_win.resize(h, w) - else: - break - r += 1 - self.list_win.addstr("\n\t") - - if str(i) == "self" and k == 0: - color = get_colpair(self.config, "name") - else: - color = get_colpair(self.config, "token") - - if k == in_arg or i == in_arg: - color |= curses.A_BOLD - - self.list_win.addstr(str(i), color) - if kw is not None: - self.list_win.addstr("=", punctuation_colpair) - self.list_win.addstr(kw, get_colpair(self.config, "token")) - if k != len(args) - 1: - self.list_win.addstr(", ", punctuation_colpair) - - if _args: - if args: - self.list_win.addstr(", ", punctuation_colpair) - self.list_win.addstr(f"*{_args}", get_colpair(self.config, "token")) - - if kwonly: - if not _args: - if args: - self.list_win.addstr(", ", punctuation_colpair) - self.list_win.addstr("*", punctuation_colpair) - marker = object() - for arg in kwonly: - self.list_win.addstr(", ", punctuation_colpair) - color = get_colpair(self.config, "token") - if arg == in_arg: - color |= curses.A_BOLD - self.list_win.addstr(arg, color) - default = kwonly_defaults.get(arg, marker) - if default is not marker: - self.list_win.addstr("=", punctuation_colpair) - self.list_win.addstr( - repr(default), get_colpair(self.config, "token") - ) - - if _kwargs: - if args or _args or kwonly: - self.list_win.addstr(", ", punctuation_colpair) - self.list_win.addstr( - f"**{_kwargs}", get_colpair(self.config, "token") - ) - self.list_win.addstr(")", punctuation_colpair) - - return r - - def mvc(self, i: int, refresh: bool = True) -> bool: - """This method moves the cursor relatively from the current - position, where: - 0 == (right) end of current line - length of current line len(self.s) == beginning of current line - and: - current cursor position + i - for positive values of i the cursor will move towards the beginning - of the line, negative values the opposite.""" - y, x = self.scr.getyx() - - if self.cpos == 0 and i < 0: - return False - - if x == self.ix and y == self.iy and i >= 1: - return False - - h, w = gethw() - if x - i < 0: - y -= 1 - x = w - - if x - i >= w: - y += 1 - x = 0 + i - - self.cpos += i - self.scr.move(y, x - i) - if refresh: - self.scr.refresh() - - return True - - def p_key(self, key: str) -> Union[None, str, bool]: - """Process a keypress""" - - if key is None: - return "" - - config = self.config - - if platform.system() == "Windows": - C_BACK = chr(127) - BACKSP = chr(8) - else: - C_BACK = chr(8) - BACKSP = chr(127) - - if key == C_BACK: # C-Backspace (on my computer anyway!) - self.clrtobol() - key = "\n" - # Don't return; let it get handled - - if key == chr(27): # Escape Key - return "" - - if key in (BACKSP, "KEY_BACKSPACE"): - self.bs() - self.complete() - return "" - - elif key in key_dispatch[config.delete_key] and not self.s: - # Delete on empty line exits - self.do_exit = True - return None - - elif key in ("KEY_DC",) + key_dispatch[config.delete_key]: - self.delete() - self.complete() - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - return "" - - elif key in key_dispatch[config.undo_key]: # C-r - n = self.prompt_undo() - if n > 0: - self.undo(n=n) - return "" - - elif key in key_dispatch[config.search_key]: - self.search() - return "" - - elif key in ("KEY_UP",) + key_dispatch[config.up_one_line_key]: - # Cursor Up/C-p - self.back() - return "" - - elif key in ("KEY_DOWN",) + key_dispatch[config.down_one_line_key]: - # Cursor Down/C-n - self.fwd() - return "" - - elif key in ("KEY_LEFT", " ^B", chr(2)): # Cursor Left or ^B - self.mvc(1) - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - - elif key in ("KEY_RIGHT", "^F", chr(6)): # Cursor Right or ^F - self.mvc(-1) - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - - elif key in ("KEY_HOME", "^A", chr(1)): # home or ^A - self.home() - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - - elif key in ("KEY_END", "^E", chr(5)): # end or ^E - self.end() - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - - elif key in ("KEY_NPAGE",): # page_down - self.hend() - self.print_line(self.s) - - elif key in ("KEY_PPAGE",): # page_up - self.hbegin() - self.print_line(self.s) - - elif key in key_dispatch[config.cut_to_buffer_key]: # cut to buffer - self.cut_to_buffer() - return "" - - elif key in key_dispatch[config.yank_from_buffer_key]: - # yank from buffer - self.yank_from_buffer() - return "" - - elif key in key_dispatch[config.clear_word_key]: - self.cut_buffer = self.bs_word() - self.complete() - return "" - - elif key in key_dispatch[config.clear_line_key]: - self.clrtobol() - return "" - - elif key in key_dispatch[config.clear_screen_key]: - # clear all but current line - self.screen_hist: List = [self.screen_hist[-1]] - self.highlighted_paren = None - self.redraw() - return "" - - elif key in key_dispatch[config.exit_key]: - if not self.s: - self.do_exit = True - return None - else: - return "" - - elif key in key_dispatch[config.save_key]: - self.write2file() - return "" - - elif key in key_dispatch[config.pastebin_key]: - self.pastebin() - return "" - - elif key in key_dispatch[config.copy_clipboard_key]: - self.copy2clipboard() - return "" - - elif key in key_dispatch[config.last_output_key]: - page(self.stdout_hist[self.prev_block_finished : -4]) - return "" - - elif key in key_dispatch[config.show_source_key]: - try: - source = self.get_source_of_current_name() - except repl.SourceNotFound as e: - self.statusbar.message(f"{e}") - else: - if config.highlight_show_source: - source = format( - Python3Lexer().get_tokens(source), TerminalFormatter() - ) - page(source) - return "" - - elif key in ("\n", "\r", "PADENTER"): - self.lf() - return None - - elif key == "\t": - return self.tab() - - elif key == "KEY_BTAB": - return self.tab(back=True) - - elif key in key_dispatch[config.suspend_key]: - if platform.system() != "Windows": - self.suspend() - return "" - else: - self.do_exit = True - return None - - elif key == "\x18": - return self.send_current_line_to_editor() - - elif key == "\x03": - raise KeyboardInterrupt() - - elif key[0:3] == "PAD" and key not in ("PAD0", "PADSTOP"): - pad_keys = { - "PADMINUS": "-", - "PADPLUS": "+", - "PADSLASH": "/", - "PADSTAR": "*", - } - try: - self.addstr(pad_keys[key]) - self.print_line(self.s) - except KeyError: - return "" - elif len(key) == 1 and not unicodedata.category(key) == "Cc": - self.addstr(key) - self.print_line(self.s) - - else: - return "" - - return True - - def print_line( - self, s: Optional[str], clr: bool = False, newline: bool = False - ) -> None: - """Chuck a line of text through the highlighter, move the cursor - to the beginning of the line and output it to the screen.""" - - if not s: - clr = True - - if self.highlighted_paren is not None: - # Clear previous highlighted paren - - lineno = self.highlighted_paren[0] - tokens = self.highlighted_paren[1] - # mypy thinks tokens is List[Tuple[_TokenType, str]] - # but it is supposed to be MutableMapping[_TokenType, str] - self.reprint_line(lineno, tokens) - self.highlighted_paren = None - - if self.config.syntax and (not self.paste_mode or newline): - o = format(self.tokenize(s, newline), self.formatter) - else: - o = s - - self.f_string = o - self.scr.move(self.iy, self.ix) - - if clr: - self.scr.clrtoeol() - - if clr and not s: - self.scr.refresh() - - if o: - for t in o.split("\x04"): - self.echo(t.rstrip("\n")) - - if self.cpos: - t = self.cpos - for _ in range(self.cpos): - self.mvc(1) - self.cpos = t - - def prompt(self, more: Any) -> None: # I'm not sure of the type on this one - """Show the appropriate Python prompt""" - if not more: - self.echo( - "\x01{}\x03{}".format( - self.config.color_scheme["prompt"], self.ps1 - ) - ) - self.stdout_hist += self.ps1 - self.screen_hist.append( - "\x01%s\x03%s\x04" - % (self.config.color_scheme["prompt"], self.ps1) - ) - else: - prompt_more_color = self.config.color_scheme["prompt_more"] - self.echo(f"\x01{prompt_more_color}\x03{self.ps2}") - self.stdout_hist += self.ps2 - self.screen_hist.append( - f"\x01{prompt_more_color}\x03{self.ps2}\x04" - ) - - def push(self, s: str, insert_into_history: bool = True) -> bool: - # curses.raw(True) prevents C-c from causing a SIGINT - curses.raw(False) - try: - return super().push(s, insert_into_history) - except SystemExit as e: - # Avoid a traceback on e.g. quit() - self.do_exit = True - self.exit_value = e.args - return False - finally: - curses.raw(True) - - def redraw(self) -> None: - """Redraw the screen using screen_hist""" - self.scr.erase() - for k, s in enumerate(self.screen_hist): - if not s: - continue - self.iy, self.ix = self.scr.getyx() - for i in s.split("\x04"): - self.echo(i, redraw=False) - if k < len(self.screen_hist) - 1: - self.scr.addstr("\n") - self.iy, self.ix = self.scr.getyx() - self.print_line(self.s) - self.scr.refresh() - self.statusbar.refresh() - - def repl(self) -> Tuple[Any, ...]: - """Initialise the repl and jump into the loop. This method also has to - keep a stack of lines entered for the horrible "undo" feature. It also - tracks everything that would normally go to stdout in the normal Python - interpreter so it can quickly write it to stdout on exit after - curses.endwin(), as well as a history of lines entered for using - up/down to go back and forth (which has to be separate to the - evaluation history, which will be truncated when undoing.""" - - # Use our own helper function because Python's will use real stdin and - # stdout instead of our wrapped - self.push("from bpython._internal import _help as help\n", False) - - self.iy, self.ix = self.scr.getyx() - self.more = False - while not self.do_exit: - self.f_string = "" - self.prompt(self.more) - try: - inp = self.get_line() - except KeyboardInterrupt: - self.statusbar.message("KeyboardInterrupt") - self.scr.addstr("\n") - self.scr.touchwin() - self.scr.refresh() - continue - - self.scr.redrawwin() - if self.do_exit: - return self.exit_value - - self.history.append(inp) - self.screen_hist[-1] += self.f_string - self.stdout_hist += inp + "\n" - stdout_position = len(self.stdout_hist) - self.more = self.push(inp) - if not self.more: - self.prev_block_finished = stdout_position - self.s = "" - return self.exit_value - - def reprint_line( - self, lineno: int, tokens: List[Tuple[_TokenType, str]] - ) -> None: - """Helper function for paren highlighting: Reprint line at offset - `lineno` in current input buffer.""" - if not self.buffer or lineno == len(self.buffer): - return - - real_lineno = self.iy - height, width = self.scr.getmaxyx() - for i in range(lineno, len(self.buffer)): - string = self.buffer[i] - # 4 = length of prompt - length = len(string.encode(getpreferredencoding())) + 4 - real_lineno -= int(math.ceil(length / width)) - if real_lineno < 0: - return - - self.scr.move( - real_lineno, len(self.ps1) if lineno == 0 else len(self.ps2) - ) - line = format(tokens, BPythonFormatter(self.config.color_scheme)) - for string in line.split("\x04"): - self.echo(string) - - def resize(self) -> None: - """This method exists simply to keep it straight forward when - initialising a window and resizing it.""" - self.size() - self.scr.erase() - self.scr.resize(self.h, self.w) - self.scr.mvwin(self.y, self.x) - self.statusbar.resize(refresh=False) - self.redraw() - - def getstdout(self) -> str: - """This method returns the 'spoofed' stdout buffer, for writing to a - file or sending to a pastebin or whatever.""" - - return self.stdout_hist + "\n" - - def reevaluate(self) -> None: - """Clear the buffer, redraw the screen and re-evaluate the history""" - - self.evaluating = True - self.stdout_hist = "" - self.f_string = "" - self.buffer: List[str] = [] - self.scr.erase() - self.screen_hist = [] - # Set cursor position to -1 to prevent paren matching - self.cpos = -1 - - self.prompt(False) - - self.iy, self.ix = self.scr.getyx() - for line in self.history: - self.stdout_hist += line + "\n" - self.print_line(line) - self.screen_hist[-1] += self.f_string - # I decided it was easier to just do this manually - # than to make the print_line and history stuff more flexible. - self.scr.addstr("\n") - self.more = self.push(line) - self.prompt(self.more) - self.iy, self.ix = self.scr.getyx() - - self.cpos = 0 - indent = repl.next_indentation(self.s, self.config.tab_length) - self.s = "" - self.scr.refresh() - - if self.buffer: - for _ in range(indent): - self.tab() - - self.evaluating = False - # map(self.push, self.history) - # ^-- That's how simple this method was at first :( - - def write(self, s: str) -> None: - """For overriding stdout defaults""" - if "\x04" in s: - for block in s.split("\x04"): - self.write(block) - return - if s.rstrip() and "\x03" in s: - t = s.split("\x03")[1] - else: - t = s - - if not self.stdout_hist: - self.stdout_hist = t - else: - self.stdout_hist += t - - self.echo(s) - self.screen_hist.append(s.rstrip()) - - def show_list( - self, - items: List[str], - arg_pos: Union[str, int, None], - topline: Optional[inspection.FuncProps] = None, - formatter: Optional[Callable] = None, - current_item: Optional[str] = None, - ) -> None: - v_items: Collection - shared = ShowListState() - y, x = self.scr.getyx() - h, w = self.scr.getmaxyx() - down = y < h // 2 - if down: - max_h = h - y - else: - max_h = y + 1 - max_w = int(w * self.config.cli_suggestion_width) - self.list_win.erase() - - if items and formatter: - items = [formatter(x) for x in items] - if current_item is not None: - current_item = formatter(current_item) - - if topline: - height_offset = self.mkargspec(topline, arg_pos, down) + 1 - else: - height_offset = 0 - - def lsize() -> bool: - wl = max(len(i) for i in v_items) + 1 - if not wl: - wl = 1 - cols = ((max_w - 2) // wl) or 1 - rows = len(v_items) // cols - - if cols * rows < len(v_items): - rows += 1 - - if rows + 2 >= max_h: - return False - - shared.rows = rows - shared.cols = cols - shared.wl = wl - return True - - if items: - # visible items (we'll append until we can't fit any more in) - v_items = [items[0][: max_w - 3]] - lsize() - else: - v_items = [] - - for i in items[1:]: - v_items.append(i[: max_w - 3]) - if not lsize(): - del v_items[-1] - v_items[-1] = "..." - break - - rows = shared.rows - if rows + height_offset < max_h: - rows += height_offset - display_rows = rows - else: - display_rows = rows + height_offset - - cols = shared.cols - wl = shared.wl - - if topline and not v_items: - w = max_w - elif wl + 3 > max_w: - w = max_w - else: - t = (cols + 1) * wl + 3 - if t > max_w: - t = max_w - w = t - - if height_offset and display_rows + 5 >= max_h: - del v_items[-(cols * (height_offset)) :] - - if self.docstring is None: - self.list_win.resize(rows + 2, w) - else: - docstring = self.format_docstring( - self.docstring, max_w - 2, max_h - height_offset - ) - docstring_string = "".join(docstring) - rows += len(docstring) - self.list_win.resize(rows, max_w) - - if down: - self.list_win.mvwin(y + 1, 0) - else: - self.list_win.mvwin(y - rows - 2, 0) - - if v_items: - self.list_win.addstr("\n ") - - for ix, i in enumerate(v_items): - padding = (wl - len(i)) * " " - if i == current_item: - color = get_colpair(self.config, "operator") - else: - color = get_colpair(self.config, "main") - self.list_win.addstr(i + padding, color) - if (cols == 1 or (ix and not (ix + 1) % cols)) and ix + 1 < len( - v_items - ): - self.list_win.addstr("\n ") - - if self.docstring is not None: - self.list_win.addstr( - "\n" + docstring_string, get_colpair(self.config, "comment") - ) - # XXX: After all the trouble I had with sizing the list box (I'm not very good - # at that type of thing) I decided to do this bit of tidying up here just to - # make sure there's no unnecessary blank lines, it makes things look nicer. - - y = self.list_win.getyx()[0] - self.list_win.resize(y + 2, w) - - self.statusbar.win.touchwin() - self.statusbar.win.noutrefresh() - self.list_win.attron(get_colpair(self.config, "main")) - self.list_win.border() - self.scr.touchwin() - self.scr.cursyncup() - self.scr.noutrefresh() - - # This looks a little odd, but I can't figure a better way to stick the cursor - # back where it belongs (refreshing the window hides the list_win) - - self.scr.move(*self.scr.getyx()) - self.list_win.refresh() - - def size(self) -> None: - """Set instance attributes for x and y top left corner coordinates - and width and height for the window.""" - global stdscr - if stdscr: - h, w = stdscr.getmaxyx() - self.y: int = 0 - self.w: int = w - self.h: int = h - 1 - self.x: int = 0 - - def suspend(self) -> None: - """Suspend the current process for shell job control.""" - if platform.system() != "Windows": - curses.endwin() - os.kill(os.getpid(), signal.SIGSTOP) - - def tab(self, back: bool = False) -> bool: - """Process the tab key being hit. - - If there's only whitespace - in the line or the line is blank then process a normal tab, - otherwise attempt to autocomplete to the best match of possible - choices in the match list. - - If `back` is True, walk backwards through the list of suggestions - and don't indent if there are only whitespace in the line. - """ - - # 1. check if we should add a tab character - if self.atbol() and not back: - x_pos = len(self.s) - self.cpos - num_spaces = x_pos % self.config.tab_length - if not num_spaces: - num_spaces = self.config.tab_length - - self.addstr(" " * num_spaces) - self.print_line(self.s) - return True - - # 2. run complete() if we aren't already iterating through matches - if not self.matches_iter: - self.complete(tab=True) - self.print_line(self.s) - - # 3. check to see if we can expand the current word - if self.matches_iter.is_cseq(): - # TODO resolve this error-prone situation: - # can't assign at same time to self.s and self.cursor_offset - # because for cursor_offset - # property to work correctly, self.s must already be set - temp_cursor_offset, self.s = self.matches_iter.substitute_cseq() - self.cursor_offset = temp_cursor_offset - self.print_line(self.s) - if not self.matches_iter: - self.complete() - - # 4. swap current word for a match list item - elif self.matches_iter.matches: - current_match = ( - self.matches_iter.previous() - if back - else next(self.matches_iter) - ) - try: - f = None - if self.matches_iter.completer: - f = self.matches_iter.completer.format - - self.show_list( - self.matches_iter.matches, - self.arg_pos, - topline=self.funcprops, - formatter=f, - current_item=current_match, - ) - except curses.error: - # XXX: This is a massive hack, it will go away when I get - # cusswords into a good enough state that we can start - # using it. - self.list_win.border() - self.list_win.refresh() - _, self.s = self.matches_iter.cur_line() - self.print_line(self.s, True) - return True - - def undo(self, n: int = 1) -> None: - repl.Repl.undo(self, n) - - # This will unhighlight highlighted parens - self.print_line(self.s) - - def writetb(self, lines: List[str]) -> None: - for line in lines: - self.write( - "\x01{}\x03{}".format(self.config.color_scheme["error"], line) - ) - - def yank_from_buffer(self) -> None: - """Paste the text from the cut buffer at the current cursor location""" - self.addstr(self.cut_buffer) - self.print_line(self.s, clr=True) - - def send_current_line_to_editor(self) -> str: - lines = self.send_to_external_editor(self.s).split("\n") - self.s = "" - self.print_line(self.s) - while lines and not lines[-1]: - lines.pop() - if not lines: - return "" - - self.f_string = "" - self.cpos = -1 # Set cursor position to -1 to prevent paren matching - - self.iy, self.ix = self.scr.getyx() - self.evaluating = True - for line in lines: - self.stdout_hist += line + "\n" - self.history.append(line) - self.print_line(line) - self.screen_hist[-1] += self.f_string - self.scr.addstr("\n") - self.more = self.push(line) - self.prompt(self.more) - self.iy, self.ix = self.scr.getyx() - self.evaluating = False - - self.cpos = 0 - indent = repl.next_indentation(self.s, self.config.tab_length) - self.s = "" - self.scr.refresh() - - if self.buffer: - for _ in range(indent): - self.tab() - - self.print_line(self.s) - self.scr.redrawwin() - return "" - - -class Statusbar: - """This class provides the status bar at the bottom of the screen. - It has message() and prompt() methods for user interactivity, as - well as settext() and clear() methods for changing its appearance. - - The check() method needs to be called repeatedly if the statusbar is - going to be aware of when it should update its display after a message() - has been called (it'll display for a couple of seconds and then disappear). - - It should be called as: - foo = Statusbar(stdscr, scr, 'Initial text to display') - or, for a blank statusbar: - foo = Statusbar(stdscr, scr) - - It can also receive the argument 'c' which will be an integer referring - to a curses colour pair, e.g.: - foo = Statusbar(stdscr, 'Hello', c=4) - - stdscr should be a curses window object in which to put the status bar. - pwin should be the parent window. To be honest, this is only really here - so the cursor can be returned to the window properly. - - """ - - def __init__( - self, - scr: "_CursesWindow", - pwin: "_CursesWindow", - background: int, - config: Config, - s: Optional[str] = None, - c: Optional[int] = None, - ): - """Initialise the statusbar and display the initial text (if any)""" - self.size() - self.win: "_CursesWindow" = newwin( - background, self.h, self.w, self.y, self.x - ) - - self.config = config - - self.s = s or "" - self._s = self.s - self.c = c - self.timer = 0 - self.pwin = pwin - if s: - self.settext(s, c) - - def size(self) -> None: - """Set instance attributes for x and y top left corner coordinates - and width and height for the window.""" - h, w = gethw() - self.y = h - 1 - self.w = w - self.h = 1 - self.x = 0 - - def resize(self, refresh: bool = True) -> None: - """This method exists simply to keep it straight forward when - initialising a window and resizing it.""" - self.size() - self.win.mvwin(self.y, self.x) - self.win.resize(self.h, self.w) - if refresh: - self.refresh() - - def refresh(self) -> None: - """This is here to make sure the status bar text is redraw properly - after a resize.""" - self.settext(self._s) - - def check(self) -> None: - """This is the method that should be called every half second or so - to see if the status bar needs updating.""" - if not self.timer: - return - - if time.time() < self.timer: - return - - self.settext(self._s) - - def message(self, s: str, n: float = 3.0) -> None: - """Display a message for a short n seconds on the statusbar and return - it to its original state.""" - self.timer = int(time.time() + n) - self.settext(s) - - def prompt(self, s: str = "") -> str: - """Prompt the user for some input (with the optional prompt 's') and - return the input text, then restore the statusbar to its original - value.""" - - self.settext(s or "? ", p=True) - iy, ix = self.win.getyx() - - def bs(s: str) -> str: - y, x = self.win.getyx() - if x == ix: - return s - s = s[:-1] - self.win.delch(y, x - 1) - self.win.move(y, x - 1) - return s - - o = "" - while True: - c = self.win.getch() - - # '\b' - if c == 127: - o = bs(o) - # '\n' - elif c == 10: - break - # ESC - elif c == 27: - curses.flushinp() - raise ValueError - # literal - elif 0 < c < 127: - d = chr(c) - self.win.addstr(d, get_colpair(self.config, "prompt")) - o += d - - self.settext(self._s) - return o - - def settext(self, s: str, c: Optional[int] = None, p: bool = False) -> None: - """Set the text on the status bar to a new permanent value; this is the - value that will be set after a prompt or message. c is the optional - curses colour pair to use (if not specified the last specified colour - pair will be used). p is True if the cursor is expected to stay in the - status window (e.g. when prompting).""" - - self.win.erase() - if len(s) >= self.w: - s = s[: self.w - 1] - - self.s = s - if c: - self.c = c - - if s: - if self.c: - self.win.addstr(s, self.c) - else: - self.win.addstr(s) - - if not p: - self.win.noutrefresh() - self.pwin.refresh() - else: - self.win.refresh() - - def clear(self) -> None: - """Clear the status bar.""" - self.win.clear() - - -def init_wins( - scr: "_CursesWindow", config: Config -) -> Tuple["_CursesWindow", Statusbar]: - """Initialise the two windows (the main repl interface and the little - status bar at the bottom with some stuff in it)""" - # TODO: Document better what stuff is on the status bar. - - background = get_colpair(config, "background") - h, w = gethw() - - main_win = newwin(background, h - 1, w, 0, 0) - main_win.scrollok(True) - - # I think this is supposed to be True instead of 1? - main_win.keypad(1) # type:ignore[arg-type] - # Thanks to Angus Gibson for pointing out this missing line which was causing - # problems that needed dirty hackery to fix. :) - - commands = ( - (_("Rewind"), config.undo_key), - (_("Save"), config.save_key), - (_("Pastebin"), config.pastebin_key), - (_("Pager"), config.last_output_key), - (_("Show Source"), config.show_source_key), - ) - - message = " ".join( - f"<{key}> {command}" for command, key in commands if key - ) - - statusbar = Statusbar( - scr, main_win, background, config, message, get_colpair(config, "main") - ) - - return main_win, statusbar - - -def sigwinch(unused_scr: "_CursesWindow") -> None: - global DO_RESIZE - DO_RESIZE = True - - -def sigcont(unused_scr: "_CursesWindow") -> None: - sigwinch(unused_scr) - # Forces the redraw - curses.ungetch("\x00") - - -def gethw() -> Tuple[int, int]: - """I found this code on a usenet post, and snipped out the bit I needed, - so thanks to whoever wrote that, sorry I forgot your name, I'm sure you're - a great guy. - - It's unfortunately necessary (unless someone has any better ideas) in order - to allow curses and readline to work together. I looked at the code for - libreadline and noticed this comment: - - /* This is the stuff that is hard for me. I never seem to write good - display routines in C. Let's see how I do this time. */ - - So I'm not going to ask any questions. - - """ - - if platform.system() != "Windows": - h, w = struct.unpack( - "hhhh", - fcntl.ioctl( - sys.__stdout__, termios.TIOCGWINSZ, "\000" * 8 - ), # type:ignore[call-overload] - )[0:2] - else: - # Ignoring mypy's windll error because it's Windows-specific - from ctypes import ( # type:ignore[attr-defined] - windll, - create_string_buffer, - ) - - # stdin handle is -10 - # stdout handle is -11 - # stderr handle is -12 - - h = windll.kernel32.GetStdHandle(-12) - csbi = create_string_buffer(22) - res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) - - if res: - ( - bufx, - bufy, - curx, - cury, - wattr, - left, - top, - right, - bottom, - maxx, - maxy, - ) = struct.unpack("hhhhHhhhhhh", csbi.raw) - sizex = right - left + 1 - sizey = bottom - top + 1 - elif stdscr: - # can't determine actual size - return default values - sizex, sizey = stdscr.getmaxyx() - - h, w = sizey, sizex - return h, w - - -def idle(caller: CLIRepl) -> None: - """This is called once every iteration through the getkey() - loop (currently in the Repl class, see the get_line() method). - The statusbar check needs to go here to take care of timed - messages and the resize handlers need to be here to make - sure it happens conveniently.""" - global DO_RESIZE - - if caller.module_gatherer.find_coroutine() or caller.paste_mode: - caller.scr.nodelay(True) - key = caller.scr.getch() - caller.scr.nodelay(False) - if key != -1: - curses.ungetch(key) - else: - curses.ungetch("\x00") - caller.statusbar.check() - caller.check() - - if DO_RESIZE: - do_resize(caller) - - -def do_resize(caller: CLIRepl) -> None: - """This needs to hack around readline and curses not playing - nicely together. See also gethw() above.""" - global DO_RESIZE - h, w = gethw() - if not h: - # Hopefully this shouldn't happen. :) - return - - curses.endwin() - os.environ["LINES"] = str(h) - os.environ["COLUMNS"] = str(w) - curses.doupdate() - DO_RESIZE = False - - try: - caller.resize() - except curses.error: - pass - # The list win resizes itself every time it appears so no need to do it here. - - -class FakeDict: - """Very simple dict-alike that returns a constant value for any key - - used as a hacky solution to using a colours dict containing colour codes if - colour initialisation fails.""" - - def __init__(self, val: int): - self._val = val - - def __getitem__(self, k: Any) -> int: - return self._val - - -def newwin(background: int, *args: int) -> "_CursesWindow": - """Wrapper for curses.newwin to automatically set background colour on any - newly created window.""" - win = curses.newwin(*args) - win.bkgd(" ", background) - return win - - -def curses_wrapper(func: Callable, *args: Any, **kwargs: Any) -> Any: - """Like curses.wrapper(), but reuses stdscr when called again.""" - global stdscr - if stdscr is None: - stdscr = curses.initscr() - try: - curses.noecho() - curses.cbreak() - # Should this be keypad(True)? - stdscr.keypad(1) # type:ignore[arg-type] - - try: - curses.start_color() - except curses.error: - pass - - return func(stdscr, *args, **kwargs) - finally: - # Should this be keypad(False)? - stdscr.keypad(0) # type:ignore[arg-type] - curses.echo() - curses.nocbreak() - curses.endwin() - - -def main_curses( - scr: "_CursesWindow", - args: List[str], - config: Config, - interactive: bool = True, - locals_: Optional[Dict[str, Any]] = None, - banner: Optional[str] = None, -) -> Tuple[Tuple[Any, ...], str]: - """main function for the curses convenience wrapper - - Initialise the two main objects: the interpreter - and the repl. The repl does what a repl does and lots - of other cool stuff like syntax highlighting and stuff. - I've tried to keep it well factored but it needs some - tidying up, especially in separating the curses stuff - from the rest of the repl. - - Returns a tuple (exit value, output), where exit value is a tuple - with arguments passed to SystemExit. - """ - global stdscr - global DO_RESIZE - global colors - DO_RESIZE = False - - if platform.system() != "Windows": - old_sigwinch_handler = signal.signal( - signal.SIGWINCH, lambda *_: sigwinch(scr) - ) - # redraw window after being suspended - old_sigcont_handler = signal.signal( - signal.SIGCONT, lambda *_: sigcont(scr) - ) - - stdscr = scr - try: - curses.start_color() - curses.use_default_colors() - cols = make_colors(config) - except curses.error: - # Not sure what to do with the types here... - # FakeDict acts as a dictionary, but isn't actually a dictionary - cols = FakeDict(-1) # type:ignore[assignment] - - # FIXME: Gargh, bad design results in using globals without a refactor :( - colors = cols - - scr.timeout(300) - - curses.raw(True) - main_win, statusbar = init_wins(scr, config) - - interpreter = repl.Interpreter(locals_) - - clirepl = CLIRepl(main_win, interpreter, statusbar, config, idle) - clirepl._C = cols - - # Not sure how to type these Fake types - sys.stdin = FakeStdin(clirepl) # type:ignore[assignment] - sys.stdout = FakeStream(clirepl, lambda: sys.stdout) # type:ignore - sys.stderr = FakeStream(clirepl, lambda: sys.stderr) # type:ignore - - if args: - exit_value: Tuple[Any, ...] = () - try: - bpargs.exec_code(interpreter, args) - except SystemExit as e: - # The documentation of code.InteractiveInterpreter.runcode claims - # that it reraises SystemExit. However, I can't manage to trigger - # that. To be one the safe side let's catch SystemExit here anyway. - exit_value = e.args - if not interactive: - curses.raw(False) - return (exit_value, clirepl.getstdout()) - else: - sys.path.insert(0, "") - try: - clirepl.startup() - except OSError as e: - # Handle this with a proper error message. - if e.errno != errno.ENOENT: - raise - - if banner is not None: - clirepl.write(banner) - clirepl.write("\n") - - # XXX these deprecation warnings need to go at some point - clirepl.write( - _( - "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. This backend has been deprecated in version 0.19 and might disappear in a future version." - ) - ) - clirepl.write("\n") - - exit_value = clirepl.repl() - if hasattr(sys, "exitfunc"): - # Seems like the if statement should satisfy mypy, but it doesn't - sys.exitfunc() # type:ignore[attr-defined] - delattr(sys, "exitfunc") - - main_win.erase() - main_win.refresh() - statusbar.win.clear() - statusbar.win.refresh() - curses.raw(False) - - # Restore signal handlers - if platform.system() != "Windows": - signal.signal(signal.SIGWINCH, old_sigwinch_handler) - signal.signal(signal.SIGCONT, old_sigcont_handler) - - return (exit_value, clirepl.getstdout()) - - -def main( - args: Optional[List[str]] = None, - locals_: Optional[MutableMapping[str, str]] = None, - banner: Optional[str] = None, -) -> Any: - translations.init() - - config, options, exec_args = argsparse(args) - - # Save stdin, stdout and stderr for later restoration - orig_stdin = sys.stdin - orig_stdout = sys.stdout - orig_stderr = sys.stderr - - try: - (exit_value, output) = curses_wrapper( - main_curses, - exec_args, - config, - options.interactive, - locals_, - banner=banner, - ) - finally: - sys.stdin = orig_stdin - sys.stderr = orig_stderr - sys.stdout = orig_stdout - - # Fake stdout data so everything's still visible after exiting - if config.flush_output and not options.quiet: - sys.stdout.write(output) - if hasattr(sys.stdout, "flush"): - sys.stdout.flush() - return repl.extract_exit_value(exit_value) - - -if __name__ == "__main__": - sys.exit(main()) - -# vim: sw=4 ts=4 sts=4 ai et diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index b3e8912f..74f4b721 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -10,7 +10,7 @@ from pathlib import Path from unittest import mock -from bpython import config, repl, cli, autocomplete +from bpython import config, repl, autocomplete from bpython.line import LinePart from bpython.test import ( MagicIterMock, @@ -67,13 +67,6 @@ def reevaluate(self): raise NotImplementedError -class FakeCliRepl(cli.CLIRepl, FakeRepl): - def __init__(self): - self.s = "" - self.cpos = 0 - self.rl_history = FakeHistory() - - class TestMatchesIterator(unittest.TestCase): def setUp(self): self.matches = ["bobby", "bobbies", "bobberina"] @@ -539,148 +532,5 @@ def __init__(self, *args, **kwargs): self.assertEqual(self.repl.matches_iter.matches, ["apple2=", "apple="]) -class TestCliRepl(unittest.TestCase): - def setUp(self): - self.repl = FakeCliRepl() - - def test_atbol(self): - self.assertTrue(self.repl.atbol()) - - self.repl.s = "\t\t" - self.assertTrue(self.repl.atbol()) - - self.repl.s = "\t\tnot an empty line" - self.assertFalse(self.repl.atbol()) - - def test_addstr(self): - self.repl.complete = mock.Mock(True) - - self.repl.s = "foo" - self.repl.addstr("bar") - self.assertEqual(self.repl.s, "foobar") - - self.repl.cpos = 3 - self.repl.addstr("buzz") - self.assertEqual(self.repl.s, "foobuzzbar") - - -class TestCliReplTab(unittest.TestCase): - def setUp(self): - self.repl = FakeCliRepl() - - # 3 Types of tab complete - def test_simple_tab_complete(self): - self.repl.matches_iter = MagicIterMock() - self.repl.matches_iter.__bool__.return_value = False - self.repl.complete = mock.Mock() - self.repl.print_line = mock.Mock() - self.repl.matches_iter.is_cseq.return_value = False - self.repl.show_list = mock.Mock() - self.repl.funcprops = mock.Mock() - self.repl.arg_pos = mock.Mock() - self.repl.matches_iter.cur_line.return_value = (None, "foobar") - - self.repl.s = "foo" - self.repl.tab() - self.assertTrue(self.repl.complete.called) - self.repl.complete.assert_called_with(tab=True) - self.assertEqual(self.repl.s, "foobar") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_substring_tab_complete(self): - self.repl.s = "bar" - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.FUZZY - ) - self.repl.tab() - self.assertEqual(self.repl.s, "foobar") - self.repl.tab() - self.assertEqual(self.repl.s, "foofoobar") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_fuzzy_tab_complete(self): - self.repl.s = "br" - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.FUZZY - ) - self.repl.tab() - self.assertEqual(self.repl.s, "foobar") - - # Edge Cases - def test_normal_tab(self): - """make sure pressing the tab key will - still in some cases add a tab""" - self.repl.s = "" - self.repl.config = mock.Mock() - self.repl.config.tab_length = 4 - self.repl.complete = mock.Mock() - self.repl.print_line = mock.Mock() - self.repl.tab() - self.assertEqual(self.repl.s, " ") - - def test_back_parameter(self): - self.repl.matches_iter = mock.Mock() - self.repl.matches_iter.matches = True - self.repl.matches_iter.previous.return_value = "previtem" - self.repl.matches_iter.is_cseq.return_value = False - self.repl.show_list = mock.Mock() - self.repl.funcprops = mock.Mock() - self.repl.arg_pos = mock.Mock() - self.repl.matches_iter.cur_line.return_value = (None, "previtem") - self.repl.print_line = mock.Mock() - self.repl.s = "foo" - self.repl.cpos = 0 - self.repl.tab(back=True) - self.assertTrue(self.repl.matches_iter.previous.called) - self.assertTrue(self.repl.s, "previtem") - - # Attribute Tests - @unittest.skip("disabled while non-simple completion is disabled") - def test_fuzzy_attribute_tab_complete(self): - """Test fuzzy attribute with no text""" - self.repl.s = "Foo." - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.FUZZY - ) - - self.repl.tab() - self.assertEqual(self.repl.s, "Foo.foobar") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_fuzzy_attribute_tab_complete2(self): - """Test fuzzy attribute with some text""" - self.repl.s = "Foo.br" - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.FUZZY - ) - - self.repl.tab() - self.assertEqual(self.repl.s, "Foo.foobar") - - # Expand Tests - def test_simple_expand(self): - self.repl.s = "f" - self.cpos = 0 - self.repl.matches_iter = mock.Mock() - self.repl.matches_iter.is_cseq.return_value = True - self.repl.matches_iter.substitute_cseq.return_value = (3, "foo") - self.repl.print_line = mock.Mock() - self.repl.tab() - self.assertEqual(self.repl.s, "foo") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_substring_expand_forward(self): - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.SUBSTRING - ) - self.repl.s = "ba" - self.repl.tab() - self.assertEqual(self.repl.s, "bar") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_fuzzy_expand(self): - pass - - if __name__ == "__main__": unittest.main() diff --git a/bpython/translations/bpython.pot b/bpython/translations/bpython.pot index e11140ed..c6957291 100644 --- a/bpython/translations/bpython.pot +++ b/bpython/translations/bpython.pot @@ -61,42 +61,14 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/de/LC_MESSAGES/bpython.po b/bpython/translations/de/LC_MESSAGES/bpython.po index 79b3acf7..feb534f7 100644 --- a/bpython/translations/de/LC_MESSAGES/bpython.po +++ b/bpython/translations/de/LC_MESSAGES/bpython.po @@ -67,45 +67,15 @@ msgstr "" "Auszuführende Datei und zusätzliche Argumente, die an das Script " "übergeben werden sollen." -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "j" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "ja" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "Rückgängig" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "Speichern" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "Pastebin" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "Quellcode anzeigen" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" -"ACHTUNG: `bpython-cli` wird verwendet, die curses Implementierung von " -"`bpython`. Diese Implementierung wird ab Version 0.19 nicht mehr aktiv " -"unterstützt und wird in einer zukünftigen Version entfernt werden." - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/es_ES/LC_MESSAGES/bpython.po b/bpython/translations/es_ES/LC_MESSAGES/bpython.po index 5af25b37..d3487281 100644 --- a/bpython/translations/es_ES/LC_MESSAGES/bpython.po +++ b/bpython/translations/es_ES/LC_MESSAGES/bpython.po @@ -62,42 +62,15 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "s" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "si" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po index 32bbec66..ba120504 100644 --- a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po +++ b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po @@ -66,42 +66,15 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "o" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "oui" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "Rembobiner" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "Sauvegarder" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "Montrer le code source" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/it_IT/LC_MESSAGES/bpython.po b/bpython/translations/it_IT/LC_MESSAGES/bpython.po index d0076cff..46488bc3 100644 --- a/bpython/translations/it_IT/LC_MESSAGES/bpython.po +++ b/bpython/translations/it_IT/LC_MESSAGES/bpython.po @@ -62,42 +62,15 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "s" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "si" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po index d110f3ba..375f4f32 100644 --- a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po +++ b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po @@ -62,42 +62,15 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "j" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "ja" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/doc/sphinx/source/tips.rst b/doc/sphinx/source/tips.rst index f2519b40..4bfbc2e4 100644 --- a/doc/sphinx/source/tips.rst +++ b/doc/sphinx/source/tips.rst @@ -16,7 +16,7 @@ equivalent file. .. code-block:: bash - alias bpython3.5='PYTHONPATH=~/python/bpython python3.5 -m bpython.cli' + alias bpython3.5='PYTHONPATH=~/python/bpython python3.5 -m bpython.curtsies' Where the `~/python/bpython`-path is the path to where your bpython source code resides. diff --git a/doc/sphinx/source/windows.rst b/doc/sphinx/source/windows.rst index 6d2c05a0..5374f70f 100644 --- a/doc/sphinx/source/windows.rst +++ b/doc/sphinx/source/windows.rst @@ -7,9 +7,3 @@ other platforms as well. There are no official binaries for bpython on Windows (though this is something we plan on providing in the future). - -The easiest way to get `bpython.cli` (the curses frontend running) is to install -an unofficial windows binary for pdcurses from: -http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses. After this you can just -`pip install bpython` and run bpython curses frontend like you would on a Linux -system (e.g. by typing `bpython-curses` on your prompt). diff --git a/setup.cfg b/setup.cfg index 8c4294d9..c0c5d03e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,6 @@ watch = watchdog [options.entry_points] console_scripts = bpython = bpython.curtsies:main - bpython-curses = bpython.cli:main bpython-urwid = bpython.urwid:main [urwid] bpdb = bpdb:main From 5aa1989a8ed299aac155b24c4a4b4adf0542cc17 Mon Sep 17 00:00:00 2001 From: supakeen Date: Tue, 9 Nov 2021 17:46:04 +0000 Subject: [PATCH 210/262] Refer directly to the top-level package in docs. --- doc/sphinx/source/tips.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/source/tips.rst b/doc/sphinx/source/tips.rst index 4bfbc2e4..0745e3bf 100644 --- a/doc/sphinx/source/tips.rst +++ b/doc/sphinx/source/tips.rst @@ -16,7 +16,7 @@ equivalent file. .. code-block:: bash - alias bpython3.5='PYTHONPATH=~/python/bpython python3.5 -m bpython.curtsies' + alias bpython3.5='PYTHONPATH=~/python/bpython python3.5 -m bpython' Where the `~/python/bpython`-path is the path to where your bpython source code resides. From acffa531199c090baa2d512851b1cedd432fefe5 Mon Sep 17 00:00:00 2001 From: supakeen Date: Tue, 9 Nov 2021 17:47:08 +0000 Subject: [PATCH 211/262] Bit too heavy handed on the translations. --- bpython/translations/bpython.pot | 1 + 1 file changed, 1 insertion(+) diff --git a/bpython/translations/bpython.pot b/bpython/translations/bpython.pot index c6957291..9237869d 100644 --- a/bpython/translations/bpython.pot +++ b/bpython/translations/bpython.pot @@ -61,6 +61,7 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "" From 26fc2b580c774c824d1f234b32a3be0f404db6be Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 1 May 2023 10:33:26 +0200 Subject: [PATCH 212/262] Require Sphinx < 7 for now Removal of the setuptools integration from Sphinx breaks our builds. See https://github.com/sphinx-doc/sphinx/pull/11363 for the change in Sphinx. --- .github/workflows/build.yaml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1e2a374a..9254d49c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -32,7 +32,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" + pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5,<7" pip install pytest pytest-cov numpy - name: Build with Python ${{ matrix.python-version }} run: | diff --git a/setup.py b/setup.py index 6790b9d7..7ca279d3 100755 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ from sphinx.setup_command import BuildDoc # Sphinx 1.5 and newer support Python 3.6 - using_sphinx = sphinx.__version__ >= "1.5" + using_sphinx = sphinx.__version__ >= "1.5" and sphinx.__version__ < "7.0" except ImportError: using_sphinx = False From db6a559724a59713ac905d7b0533ef382fab930d Mon Sep 17 00:00:00 2001 From: Jochen Kupperschmidt Date: Tue, 11 Jul 2023 23:24:07 +0200 Subject: [PATCH 213/262] Explicitly set README content type The README, being a reStructuredText document, is not rendered as such on PyPI, just as plaintext. This *should* fix that. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index c0c5d03e..1fe4a6f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,7 @@ [metadata] name = bpython long_description = file: README.rst +long_description_content_type = text/x-rst license = MIT license_files = LICENSE author = Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al. From d58e392f188118b3f6d3c964041492dbb2cd3694 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 9 Jul 2023 16:52:27 +0200 Subject: [PATCH 214/262] Fix __signature__ support if object has a __file__ --- bpython/inspection.py | 10 +++++++--- bpython/test/test_repl.py | 19 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index fe1e3a0a..2b734cdf 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -289,9 +289,13 @@ def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: return None try: argspec = _get_argspec_from_signature(f) - fprops = FuncProps( - func, _fix_default_values(f, argspec), is_bound_method - ) + try: + argspec = _fix_default_values(f, argspec) + except KeyError as ex: + # Parsing of the source failed. If f has a __signature__, we trust it. + if not hasattr(f, "__signature__"): + raise ex + fprops = FuncProps(func, argspec, is_bound_method) except (TypeError, KeyError, ValueError): argspec_pydoc = _getpydocspec(f) if argspec_pydoc is None: diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 74f4b721..8c3b85cc 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -1,5 +1,6 @@ import collections import inspect +import os import socket import sys import tempfile @@ -523,13 +524,19 @@ def __init__(self, *args, **kwargs): inspect.Parameter("pinetree", inspect.Parameter.KEYWORD_ONLY), ]) """ - for line in code.split("\n"): - print(line[8:]) - self.repl.push(line[8:]) + code = [x[8:] for x in code.split("\n")] + for line in code: + self.repl.push(line) - self.assertTrue(self.repl.complete()) - self.assertTrue(hasattr(self.repl.matches_iter, "matches")) - self.assertEqual(self.repl.matches_iter.matches, ["apple2=", "apple="]) + with mock.patch( + "bpython.inspection.inspect.getsourcelines", + return_value=(code, None), + ): + self.assertTrue(self.repl.complete()) + self.assertTrue(hasattr(self.repl.matches_iter, "matches")) + self.assertEqual( + self.repl.matches_iter.matches, ["apple2=", "apple="] + ) if __name__ == "__main__": From 7bf93f510752e36cb72fdfd17d7db46b48e438b9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 20 Jul 2023 22:44:42 +0200 Subject: [PATCH 215/262] Fix handling of SystemExit arguments (fixes #995) --- bpython/curtsiesfrontend/coderunner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/coderunner.py b/bpython/curtsiesfrontend/coderunner.py index cddf1169..f059fab8 100644 --- a/bpython/curtsiesfrontend/coderunner.py +++ b/bpython/curtsiesfrontend/coderunner.py @@ -52,7 +52,7 @@ class Unfinished(RequestFromCodeRunner): class SystemExitRequest(RequestFromCodeRunner): """Running code raised a SystemExit""" - def __init__(self, args): + def __init__(self, *args): self.args = args From db6c9bdf6d9a1d128f04ffd053591b2e1289b08a Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 22 Jul 2023 16:48:00 +0200 Subject: [PATCH 216/262] Complete parameters without input --- bpython/autocomplete.py | 7 ++++++- bpython/test/test_autocomplete.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index e0849c6d..10f039d2 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -604,7 +604,12 @@ def matches( return matches if matches else None def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: - return lineparts.current_word(cursor_offset, line) + r = lineparts.current_word(cursor_offset, line) + if r and r.word[-1] == "(": + # if the word ends with a (, it's the parent word with an empty + # param. Return an empty word + return lineparts.LinePart(r.stop, r.stop, "") + return r class ExpressionAttributeCompletion(AttrCompletion): diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 0000b0b6..2bbd90b3 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -435,3 +435,7 @@ def func(apple, apricot, banana, carrot): self.assertSetEqual( com.matches(3, "car", funcprops=funcspec), {"carrot="} ) + self.assertSetEqual( + com.matches(5, "func(", funcprops=funcspec), + {"apple=", "apricot=", "banana=", "carrot="}, + ) From de333118a7dc89925c7e3d1f246271b814a4e37c Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 22 Jul 2023 16:49:55 +0200 Subject: [PATCH 217/262] Better completion results order --- bpython/autocomplete.py | 14 +++++++++++++- bpython/test/test_autocomplete.py | 9 +++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 10f039d2..73759992 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -747,6 +747,16 @@ def get_completer( double underscore methods like __len__ in method signatures """ + def _cmpl_sort(x: str) -> Tuple[Any, ...]: + """ + Function used to sort the matches. + """ + # put parameters above everything in completion + return ( + x[-1] != "=", + x, + ) + for completer in completers: try: matches = completer.matches( @@ -765,7 +775,9 @@ def get_completer( ) continue if matches is not None: - return sorted(matches), (completer if matches else None) + return sorted(matches, key=_cmpl_sort), ( + completer if matches else None + ) return [], None diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 2bbd90b3..da32fbb8 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -106,6 +106,15 @@ def test_two_completers_get_both(self): cumulative = autocomplete.CumulativeCompleter([a, b]) self.assertEqual(cumulative.matches(3, "abc"), {"a", "b"}) + def test_order_completer(self): + a = self.completer(["ax", "ab="]) + b = self.completer(["aa"]) + cumulative = autocomplete.CumulativeCompleter([a, b]) + self.assertEqual( + autocomplete.get_completer([cumulative], 1, "a"), + (["ab=", "aa", "ax"], cumulative), + ) + class TestFilenameCompletion(unittest.TestCase): def setUp(self): From 77bed91c9ed6b242f1efeb68047ffb43172133d3 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 26 Jul 2023 09:31:46 +0200 Subject: [PATCH 218/262] Apply suggestion --- bpython/autocomplete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 73759992..a36c7beb 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -747,7 +747,7 @@ def get_completer( double underscore methods like __len__ in method signatures """ - def _cmpl_sort(x: str) -> Tuple[Any, ...]: + def _cmpl_sort(x: str) -> Tuple[bool, ...]: """ Function used to sort the matches. """ From be21521902cda73cde331480b99c5709d757a9ae Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 13 Jul 2023 14:17:34 +0200 Subject: [PATCH 219/262] Fix argument description --- bpython/args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/args.py b/bpython/args.py index b9e68e1d..d1037e1c 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -131,7 +131,7 @@ def callback(group): "--quiet", "-q", action="store_true", - help=_("Don't flush the output to stdout."), + help=_("Don't print version banner."), ) parser.add_argument( "--version", From 1db0436babf5c964a954f875e481bc9dc686d104 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Jul 2023 09:46:46 +0200 Subject: [PATCH 220/262] Fix type annotation --- bpython/autocomplete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index a36c7beb..000fbde9 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -747,7 +747,7 @@ def get_completer( double underscore methods like __len__ in method signatures """ - def _cmpl_sort(x: str) -> Tuple[bool, ...]: + def _cmpl_sort(x: str) -> Tuple[bool, str]: """ Function used to sort the matches. """ From 590507b57f269ae33cfaadc2cafa1685f0c538e7 Mon Sep 17 00:00:00 2001 From: Mikolaj Klikowicz Date: Fri, 11 Aug 2023 13:02:07 +0200 Subject: [PATCH 221/262] Handle AttributeError Signed-off-by: Mikolaj Klikowicz --- bpython/inspection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 2b734cdf..e97a272b 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -104,13 +104,13 @@ def __enter__(self) -> None: if __getattr__ is not None: try: setattr(type_, "__getattr__", (lambda *_, **__: None)) - except TypeError: + except (TypeError, AttributeError): __getattr__ = None __getattribute__ = getattr(type_, "__getattribute__", None) if __getattribute__ is not None: try: setattr(type_, "__getattribute__", object.__getattribute__) - except TypeError: + except (TypeError, AttributeError): # XXX: This happens for e.g. built-in types __getattribute__ = None self._attribs = (__getattribute__, __getattr__) From f64677ffd9ed0446506c84e6d76e9d7272cc06ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 04:47:50 +0000 Subject: [PATCH 222/262] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- .github/workflows/lint.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9254d49c..a55ee36c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -21,7 +21,7 @@ jobs: - "3.11" - "pypy-3.8" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index f644f543..d02dd2df 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -8,7 +8,7 @@ jobs: black: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 - name: Install dependencies @@ -21,7 +21,7 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: codespell-project/actions-codespell@master with: skip: '*.po' @@ -30,7 +30,7 @@ jobs: mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 - name: Install dependencies From 7c9e8513c01b7ea77b44d5c5a8e94dcf15ccb8a7 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 Oct 2023 10:45:08 +0200 Subject: [PATCH 223/262] CI: test with Python 3.12 --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a55ee36c..ffb1c2ae 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -19,6 +19,7 @@ jobs: - "3.9" - "3.10" - "3.11" + - "3.12" - "pypy-3.8" steps: - uses: actions/checkout@v4 From cafa87c947d6e25dadd61dfa1175df953bf5445d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 11 Oct 2023 18:25:48 +0200 Subject: [PATCH 224/262] Do not fail if modules don't have __version__ (fixes #1001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … even if they should. --- bpython/args.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index d1037e1c..51cc3d25 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -37,6 +37,7 @@ import sys from pathlib import Path from typing import Tuple, List, Optional, NoReturn, Callable +from types import ModuleType from . import __version__, __copyright__ from .config import default_config_path, Config @@ -67,6 +68,13 @@ def copyright_banner() -> str: return _("{} See AUTHORS.rst for details.").format(__copyright__) +def log_version(module: ModuleType, name: str) -> None: + try: + logger.info("%s: %s", name, module.__version__) # type: ignore + except AttributeError: + logger.info("%s: unknown version", name) + + Options = Tuple[str, str, Callable[[argparse._ArgumentGroup], None]] @@ -211,27 +219,27 @@ def callback(group): try: import curtsies - logger.info("curtsies: %s", curtsies.__version__) + log_version(curtsies, "curtsies") except ImportError: # may happen on Windows logger.info("curtsies: not available") - logger.info("cwcwidth: %s", cwcwidth.__version__) - logger.info("greenlet: %s", greenlet.__version__) - logger.info("pygments: %s", pygments.__version__) # type: ignore - logger.info("pyxdg: %s", xdg.__version__) # type: ignore - logger.info("requests: %s", requests.__version__) + log_version(cwcwidth, "cwcwidth") + log_version(greenlet, "greenlet") + log_version(pygments, "pygments") + log_version(xdg, "pyxdg") + log_version(requests, "requests") # versions of optional dependencies try: import pyperclip - logger.info("pyperclip: %s", pyperclip.__version__) # type: ignore + log_version(pyperclip, "pyperclip") except ImportError: logger.info("pyperclip: not available") try: import jedi - logger.info("jedi: %s", jedi.__version__) + log_version(jedi, "jedi") except ImportError: logger.info("jedi: not available") try: From 3a440274be26338e873e3e6144e9e20116ef1d4a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 11 Oct 2023 18:42:18 +0200 Subject: [PATCH 225/262] Avoid the use of Exceptions for logic --- bpython/args.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 51cc3d25..ed0b0055 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -69,10 +69,7 @@ def copyright_banner() -> str: def log_version(module: ModuleType, name: str) -> None: - try: - logger.info("%s: %s", name, module.__version__) # type: ignore - except AttributeError: - logger.info("%s: unknown version", name) + logger.info("%s: %s", name, module.__version__ if hasattr(module, "__version__") else "unknown version") # type: ignore Options = Tuple[str, str, Callable[[argparse._ArgumentGroup], None]] From 3fda3af0df1a92c5794c6b396501c97647853222 Mon Sep 17 00:00:00 2001 From: Joan Lucas Date: Mon, 27 Nov 2023 20:59:26 -0300 Subject: [PATCH 226/262] Added functions return typing in some files: __init__.py; keys.py and lazyre.py --- bpython/__init__.py | 3 ++- bpython/keys.py | 4 ++-- bpython/lazyre.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bpython/__init__.py b/bpython/__init__.py index dff06c0f..8c7bfb37 100644 --- a/bpython/__init__.py +++ b/bpython/__init__.py @@ -21,6 +21,7 @@ # THE SOFTWARE. import os.path +from typing import Any try: from ._version import __version__ as version # type: ignore @@ -36,7 +37,7 @@ package_dir = os.path.abspath(os.path.dirname(__file__)) -def embed(locals_=None, args=None, banner=None): +def embed(locals_=None, args=None, banner=None) -> Any: if args is None: args = ["-i", "-q"] diff --git a/bpython/keys.py b/bpython/keys.py index cfcac86b..fe27dbcc 100644 --- a/bpython/keys.py +++ b/bpython/keys.py @@ -42,10 +42,10 @@ def __getitem__(self, key: str) -> T: f"Configured keymap ({key}) does not exist in bpython.keys" ) - def __delitem__(self, key: str): + def __delitem__(self, key: str) -> None: del self.map[key] - def __setitem__(self, key: str, value: T): + def __setitem__(self, key: str, value: T) -> None: self.map[key] = value diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 0ca5b9ff..8d166b74 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -21,7 +21,7 @@ # THE SOFTWARE. import re -from typing import Optional, Pattern, Match, Optional +from typing import Optional, Pattern, Match, Optional, Iterator try: from functools import cached_property @@ -43,7 +43,7 @@ def __init__(self, regex: str, flags: int = 0) -> None: def compiled(self) -> Pattern[str]: return re.compile(self.regex, self.flags) - def finditer(self, *args, **kwargs): + def finditer(self, *args, **kwargs) -> Iterator[Match[str]]: return self.compiled.finditer(*args, **kwargs) def search(self, *args, **kwargs) -> Optional[Match[str]]: From f200dac951cee8c8d0f15abd2570eb13e58f729b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 07:38:27 +0000 Subject: [PATCH 227/262] Bump actions/setup-python from 4 to 5 (#1004) --- .github/workflows/build.yaml | 2 +- .github/workflows/lint.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ffb1c2ae..864c03cc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -26,7 +26,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d02dd2df..d960c6d8 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 - name: Install dependencies run: | python -m pip install --upgrade pip From ca13bf5727d91d3208e84297c2829aa8474b4620 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 08:56:25 +0100 Subject: [PATCH 228/262] Bump codecov/codecov-action from 3 to 4 (#1006) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 864c03cc..37128f24 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -46,7 +46,7 @@ jobs: run: | pytest --cov=bpython --cov-report=xml -v - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 env: PYTHON_VERSION: ${{ matrix.python-version }} with: From 0f238b7590037c85d7b19059ee8b41cd1d1f08f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gr=C3=A4nitz?= Date: Mon, 29 Apr 2024 13:30:34 +0200 Subject: [PATCH 229/262] Fix simplerepl demo: missing arg for BaseRepl init (#1017) Default value was dropped in 8d16a71ef404db66d2c6fae6c362640da8ae240d --- doc/sphinx/source/simplerepl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/source/simplerepl.py b/doc/sphinx/source/simplerepl.py index 8a8dda74..8496f0dd 100644 --- a/doc/sphinx/source/simplerepl.py +++ b/doc/sphinx/source/simplerepl.py @@ -42,7 +42,7 @@ class SimpleRepl(BaseRepl): def __init__(self, config): self.requested_events = [] - BaseRepl.__init__(self, config) + BaseRepl.__init__(self, config, window=None) def _request_refresh(self): self.requested_events.append(bpythonevents.RefreshRequestEvent()) From 925b733e5e456daf0516f3ff9ac3877e689fd436 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 25 Apr 2024 13:57:35 +0100 Subject: [PATCH 230/262] Import build from setuptools --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7ca279d3..02194088 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup -from distutils.command.build import build +from setuptools.command.build import build try: from babel.messages import frontend as babel From ded2d7fe3fca3df64de1938c1dcf7638ec900227 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 25 Apr 2024 13:58:16 +0100 Subject: [PATCH 231/262] Avoid patching the original build class attribute --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 02194088..0cceb940 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup -from setuptools.command.build import build +from setuptools.command.build import build as _orig_build try: from babel.messages import frontend as babel @@ -122,6 +122,11 @@ def git_describe_to_python_version(version): vf.write(f'__version__ = "{version}"\n') +class build(_orig_build): + # Avoid patching the original class' attribute (more robust customisation) + sub_commands = _orig_build.sub_commands[:] + + cmdclass = {"build": build} translations_dir = os.path.join("bpython", "translations") From ac7c11ad850a8ca649a48f26eef0b2c59f204f3e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 25 Apr 2024 14:13:23 +0100 Subject: [PATCH 232/262] Bump setuptools version Reliably import `setuptools.command.build`. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7bd3196..924722b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] requires = [ - "setuptools >= 43", + "setuptools >= 62.4.0", ] build-backend = "setuptools.build_meta" From a9b1324ad535774727545896ec54515e527423ab Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 30 Apr 2024 14:49:13 +0100 Subject: [PATCH 233/262] Bump setuptools in requirement.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4c750a69..cc8fbff8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ cwcwidth greenlet pyxdg requests -setuptools \ No newline at end of file +setuptools>=62.4.0 From d54061317d767c64eb2d466fa14ca56bef5bb9eb Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 21:47:54 +0200 Subject: [PATCH 234/262] Apply black --- bpython/curtsies.py | 15 ++++++--------- bpython/curtsiesfrontend/repl.py | 8 +++++--- bpython/curtsiesfrontend/replpainter.py | 8 +++++--- bpython/paste.py | 3 +-- bpython/test/test_preprocess.py | 4 ++-- bpython/urwid.py | 1 - 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 6dc8d1f7..11b96050 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -40,14 +40,11 @@ class SupportsEventGeneration(Protocol): def send( self, timeout: Optional[float] - ) -> Union[str, curtsies.events.Event, None]: - ... + ) -> Union[str, curtsies.events.Event, None]: ... - def __iter__(self) -> "SupportsEventGeneration": - ... + def __iter__(self) -> "SupportsEventGeneration": ... - def __next__(self) -> Union[str, curtsies.events.Event, None]: - ... + def __next__(self) -> Union[str, curtsies.events.Event, None]: ... class FullCurtsiesRepl(BaseRepl): @@ -69,9 +66,9 @@ def __init__( extra_bytes_callback=self.input_generator.unget_bytes, ) - self._request_refresh_callback: Callable[ - [], None - ] = self.input_generator.event_trigger(events.RefreshRequestEvent) + self._request_refresh_callback: Callable[[], None] = ( + self.input_generator.event_trigger(events.RefreshRequestEvent) + ) self._schedule_refresh_callback = ( self.input_generator.scheduled_event_trigger( events.ScheduledRefreshRequestEvent diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index e4819e19..302e67d4 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1801,9 +1801,11 @@ def move_screen_up(current_line_start_row): self.current_match, self.docstring, self.config, - self.matches_iter.completer.format - if self.matches_iter.completer - else None, + ( + self.matches_iter.completer.format + if self.matches_iter.completer + else None + ), ) if ( diff --git a/bpython/curtsiesfrontend/replpainter.py b/bpython/curtsiesfrontend/replpainter.py index 00675451..3b63ca4c 100644 --- a/bpython/curtsiesfrontend/replpainter.py +++ b/bpython/curtsiesfrontend/replpainter.py @@ -74,9 +74,11 @@ def matches_lines(rows, columns, matches, current, config, match_format): result = [ fmtstr(" ").join( - color(m.ljust(max_match_width)) - if m != current - else highlight_color(m.ljust(max_match_width)) + ( + color(m.ljust(max_match_width)) + if m != current + else highlight_color(m.ljust(max_match_width)) + ) for m in matches[i : i + words_wide] ) for i in range(0, len(matches), words_wide) diff --git a/bpython/paste.py b/bpython/paste.py index fd140a0e..a81c0c6c 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -37,8 +37,7 @@ class PasteFailed(Exception): class Paster(Protocol): - def paste(self, s: str) -> Tuple[str, Optional[str]]: - ... + def paste(self, s: str) -> Tuple[str, Optional[str]]: ... class PastePinnwand: diff --git a/bpython/test/test_preprocess.py b/bpython/test/test_preprocess.py index ee3f2085..e9309f1e 100644 --- a/bpython/test/test_preprocess.py +++ b/bpython/test/test_preprocess.py @@ -16,10 +16,10 @@ def get_fodder_source(test_name): pattern = rf"#StartTest-{test_name}\n(.*?)#EndTest" - orig, xformed = [ + orig, xformed = ( re.search(pattern, inspect.getsource(module), re.DOTALL) for module in [original, processed] - ] + ) if not orig: raise ValueError( diff --git a/bpython/urwid.py b/bpython/urwid.py index 4b41c12a..9b061340 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -99,7 +99,6 @@ def buildProtocol(self, addr): if urwid.VERSION < (1, 0, 0) and hasattr(urwid, "TwistedEventLoop"): class TwistedEventLoop(urwid.TwistedEventLoop): - """TwistedEventLoop modified to properly stop the reactor. urwid 0.9.9 and 0.9.9.1 crash the reactor on ExitMainLoop instead From ff05288d819fb40a0a2606010abcc6c9d29d6687 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 23:17:19 +0200 Subject: [PATCH 235/262] Import BuildDoc from sphinx (fixes #987) --- LICENSE | 28 ++++++++ setup.py | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 229 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 72d02ff6..46f642f2 100644 --- a/LICENSE +++ b/LICENSE @@ -72,3 +72,31 @@ products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. + + +BuildDoc in setup.py is licensed under the BSD-2 license: + +Copyright 2007-2021 Sebastian Wiesner + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/setup.py b/setup.py index 0cceb940..e158f1a0 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ import re import subprocess -from setuptools import setup +from setuptools import setup, Command from setuptools.command.build import build as _orig_build try: @@ -17,14 +17,212 @@ try: import sphinx - from sphinx.setup_command import BuildDoc # Sphinx 1.5 and newer support Python 3.6 - using_sphinx = sphinx.__version__ >= "1.5" and sphinx.__version__ < "7.0" + using_sphinx = sphinx.__version__ >= "1.5" except ImportError: using_sphinx = False +if using_sphinx: + import sys + from io import StringIO + + from setuptools.errors import ExecError + from sphinx.application import Sphinx + from sphinx.cmd.build import handle_exception + from sphinx.util.console import color_terminal, nocolor + from sphinx.util.docutils import docutils_namespace, patch_docutils + from sphinx.util.osutil import abspath + + class BuildDoc(Command): + """ + Distutils command to build Sphinx documentation. + The Sphinx build can then be triggered from distutils, and some Sphinx + options can be set in ``setup.py`` or ``setup.cfg`` instead of Sphinx's + own configuration file. + For instance, from `setup.py`:: + # this is only necessary when not using setuptools/distribute + from sphinx.setup_command import BuildDoc + cmdclass = {'build_sphinx': BuildDoc} + name = 'My project' + version = '1.2' + release = '1.2.0' + setup( + name=name, + author='Bernard Montgomery', + version=release, + cmdclass=cmdclass, + # these are optional and override conf.py settings + command_options={ + 'build_sphinx': { + 'project': ('setup.py', name), + 'version': ('setup.py', version), + 'release': ('setup.py', release)}}, + ) + Or add this section in ``setup.cfg``:: + [build_sphinx] + project = 'My project' + version = 1.2 + release = 1.2.0 + """ + + description = "Build Sphinx documentation" + user_options = [ + ("fresh-env", "E", "discard saved environment"), + ("all-files", "a", "build all files"), + ("source-dir=", "s", "Source directory"), + ("build-dir=", None, "Build directory"), + ("config-dir=", "c", "Location of the configuration directory"), + ( + "builder=", + "b", + "The builder (or builders) to use. Can be a comma- " + 'or space-separated list. Defaults to "html"', + ), + ("warning-is-error", "W", "Turn warning into errors"), + ("project=", None, "The documented project's name"), + ("version=", None, "The short X.Y version"), + ( + "release=", + None, + "The full version, including alpha/beta/rc tags", + ), + ( + "today=", + None, + "How to format the current date, used as the " + "replacement for |today|", + ), + ("link-index", "i", "Link index.html to the master doc"), + ("copyright", None, "The copyright string"), + ("pdb", None, "Start pdb on exception"), + ("verbosity", "v", "increase verbosity (can be repeated)"), + ( + "nitpicky", + "n", + "nit-picky mode, warn about all missing references", + ), + ("keep-going", None, "With -W, keep going when getting warnings"), + ] + boolean_options = [ + "fresh-env", + "all-files", + "warning-is-error", + "link-index", + "nitpicky", + ] + + def initialize_options(self) -> None: + self.fresh_env = self.all_files = False + self.pdb = False + self.source_dir: str = None + self.build_dir: str = None + self.builder = "html" + self.warning_is_error = False + self.project = "" + self.version = "" + self.release = "" + self.today = "" + self.config_dir: str = None + self.link_index = False + self.copyright = "" + # Link verbosity to distutils' (which uses 1 by default). + self.verbosity = self.distribution.verbose - 1 # type: ignore + self.traceback = False + self.nitpicky = False + self.keep_going = False + + def _guess_source_dir(self) -> str: + for guess in ("doc", "docs"): + if not os.path.isdir(guess): + continue + for root, dirnames, filenames in os.walk(guess): + if "conf.py" in filenames: + return root + return os.curdir + + def finalize_options(self) -> None: + self.ensure_string_list("builder") + + if self.source_dir is None: + self.source_dir = self._guess_source_dir() + self.announce("Using source directory %s" % self.source_dir) + + self.ensure_dirname("source_dir") + + if self.config_dir is None: + self.config_dir = self.source_dir + + if self.build_dir is None: + build = self.get_finalized_command("build") + self.build_dir = os.path.join(abspath(build.build_base), "sphinx") # type: ignore + + self.doctree_dir = os.path.join(self.build_dir, "doctrees") + + self.builder_target_dirs = [ + (builder, os.path.join(self.build_dir, builder)) + for builder in self.builder + ] + + def run(self) -> None: + if not color_terminal(): + nocolor() + if not self.verbose: # type: ignore + status_stream = StringIO() + else: + status_stream = sys.stdout # type: ignore + confoverrides = {} + if self.project: + confoverrides["project"] = self.project + if self.version: + confoverrides["version"] = self.version + if self.release: + confoverrides["release"] = self.release + if self.today: + confoverrides["today"] = self.today + if self.copyright: + confoverrides["copyright"] = self.copyright + if self.nitpicky: + confoverrides["nitpicky"] = self.nitpicky + + for builder, builder_target_dir in self.builder_target_dirs: + app = None + + try: + confdir = self.config_dir or self.source_dir + with patch_docutils(confdir), docutils_namespace(): + app = Sphinx( + self.source_dir, + self.config_dir, + builder_target_dir, + self.doctree_dir, + builder, + confoverrides, + status_stream, + freshenv=self.fresh_env, + warningiserror=self.warning_is_error, + verbosity=self.verbosity, + keep_going=self.keep_going, + ) + app.build(force_all=self.all_files) + if app.statuscode: + raise ExecError( + "caused by %s builder." % app.builder.name + ) + except Exception as exc: + handle_exception(app, self, exc, sys.stderr) + if not self.pdb: + raise SystemExit(1) from exc + + if not self.link_index: + continue + + src = app.config.root_doc + app.builder.out_suffix # type: ignore + dst = app.builder.get_outfilename("index") # type: ignore + os.symlink(src, dst) + + # version handling From 1f2f6f5b04b52ea939d6bd64e2e8eee83ae917f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 23:17:39 +0200 Subject: [PATCH 236/262] Refactor build command overrides --- setup.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index e158f1a0..9e24203f 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup, Command -from setuptools.command.build import build as _orig_build +from setuptools.command.build import build try: from babel.messages import frontend as babel @@ -320,26 +320,27 @@ def git_describe_to_python_version(version): vf.write(f'__version__ = "{version}"\n') -class build(_orig_build): - # Avoid patching the original class' attribute (more robust customisation) - sub_commands = _orig_build.sub_commands[:] +class custom_build(build): + def run(self): + if using_translations: + self.run_command("compile_catalog") + if using_sphinx: + self.run_command("build_sphinx_man") -cmdclass = {"build": build} +cmdclass = {"build": custom_build} + translations_dir = os.path.join("bpython", "translations") # localization options if using_translations: - build.sub_commands.insert(0, ("compile_catalog", None)) - cmdclass["compile_catalog"] = babel.compile_catalog cmdclass["extract_messages"] = babel.extract_messages cmdclass["update_catalog"] = babel.update_catalog cmdclass["init_catalog"] = babel.init_catalog if using_sphinx: - build.sub_commands.insert(0, ("build_sphinx_man", None)) cmdclass["build_sphinx_man"] = BuildDoc if platform.system() in ("FreeBSD", "OpenBSD"): @@ -378,6 +379,7 @@ class build(_orig_build): if os.path.exists(os.path.join(translations_dir, mo_subpath)): mo_files.append(mo_subpath) + setup( version=version, data_files=data_files, @@ -388,6 +390,7 @@ class build(_orig_build): }, cmdclass=cmdclass, test_suite="bpython.test", + zip_safe=False, ) # vim: fileencoding=utf-8 sw=4 ts=4 sts=4 ai et sta From c085f9cc519494970e09f4a77aaeadf0bff77f87 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 23:20:39 +0200 Subject: [PATCH 237/262] CI: allow sphinx >= 7 --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 37128f24..de2f98cc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -33,7 +33,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5,<7" + pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" pip install pytest pytest-cov numpy - name: Build with Python ${{ matrix.python-version }} run: | From 7238851ca65ec0381928c1ef8ef0bde475bf6a50 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 23:24:07 +0200 Subject: [PATCH 238/262] Also register BuildDoc for build_sphinx --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 9e24203f..de10eaf4 100755 --- a/setup.py +++ b/setup.py @@ -341,6 +341,7 @@ def run(self): cmdclass["init_catalog"] = babel.init_catalog if using_sphinx: + cmdclass["build_sphinx"] = BuildDoc cmdclass["build_sphinx_man"] = BuildDoc if platform.system() in ("FreeBSD", "OpenBSD"): From 23294503c59088c5ea9c3d811d073d14272f9ff0 Mon Sep 17 00:00:00 2001 From: suman Date: Sat, 1 Jun 2024 23:43:24 +0545 Subject: [PATCH 239/262] Replace NoReturn with Never --- bpython/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index ed0b0055..e8e882a2 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -36,7 +36,7 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional, NoReturn, Callable +from typing import Tuple, List, Optional, Never, Callable from types import ModuleType from . import __version__, __copyright__ @@ -51,7 +51,7 @@ class ArgumentParserFailed(ValueError): class RaisingArgumentParser(argparse.ArgumentParser): - def error(self, msg: str) -> NoReturn: + def error(self, msg: str) -> Never: raise ArgumentParserFailed() From f8aeaaf8ef8b513473267db99dfe845d0e4b1a41 Mon Sep 17 00:00:00 2001 From: suman Date: Sat, 1 Jun 2024 23:43:46 +0545 Subject: [PATCH 240/262] Add type annotations --- bpdb/debugger.py | 8 ++++---- bpython/args.py | 4 ++-- bpython/urwid.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bpdb/debugger.py b/bpdb/debugger.py index 3e5bbc91..38469541 100644 --- a/bpdb/debugger.py +++ b/bpdb/debugger.py @@ -27,24 +27,24 @@ class BPdb(pdb.Pdb): """PDB with BPython support.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.prompt = "(BPdb) " self.intro = 'Use "B" to enter bpython, Ctrl-d to exit it.' - def postloop(self): + def postloop(self) -> None: # We only want to show the intro message once. self.intro = None super().postloop() # cmd.Cmd commands - def do_Bpython(self, arg): + def do_Bpython(self, arg: str) -> None: locals_ = self.curframe.f_globals.copy() locals_.update(self.curframe.f_locals) bpython.embed(locals_, ["-i"]) - def help_Bpython(self): + def help_Bpython(self) -> None: print("B(python)") print("") print( diff --git a/bpython/args.py b/bpython/args.py index e8e882a2..ed0b0055 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -36,7 +36,7 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional, Never, Callable +from typing import Tuple, List, Optional, NoReturn, Callable from types import ModuleType from . import __version__, __copyright__ @@ -51,7 +51,7 @@ class ArgumentParserFailed(ValueError): class RaisingArgumentParser(argparse.ArgumentParser): - def error(self, msg: str) -> Never: + def error(self, msg: str) -> NoReturn: raise ArgumentParserFailed() diff --git a/bpython/urwid.py b/bpython/urwid.py index 9b061340..3c075d93 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -76,10 +76,10 @@ class EvalProtocol(basic.LineOnlyReceiver): delimiter = "\n" - def __init__(self, myrepl): + def __init__(self, myrepl) -> None: self.repl = myrepl - def lineReceived(self, line): + def lineReceived(self, line) -> None: # HACK! # TODO: deal with encoding issues here... self.repl.main_loop.process_input(line) From a12d339e1a0bdca726d439ed1231f3f2ca993eac Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 2 Jun 2024 13:04:12 +0200 Subject: [PATCH 241/262] Use Never for Python 3.11 and newer --- bpython/_typing_compat.py | 28 ++++++++++++++++++++++++++++ bpython/args.py | 5 +++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 bpython/_typing_compat.py diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py new file mode 100644 index 00000000..83567b4f --- /dev/null +++ b/bpython/_typing_compat.py @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright (c) 2024 Sebastian Ramacher +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +try: + # introduced in Python 3.11 + from typing import Never +except ImportError: + from typing import NoReturn as Never # type: ignore + diff --git a/bpython/args.py b/bpython/args.py index ed0b0055..55691a2a 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -36,12 +36,13 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional, NoReturn, Callable +from typing import Tuple, List, Optional, Callable from types import ModuleType from . import __version__, __copyright__ from .config import default_config_path, Config from .translations import _ +from ._typing_compat import Never logger = logging.getLogger(__name__) @@ -51,7 +52,7 @@ class ArgumentParserFailed(ValueError): class RaisingArgumentParser(argparse.ArgumentParser): - def error(self, msg: str) -> NoReturn: + def error(self, msg: str) -> Never: raise ArgumentParserFailed() From c152cbf8485695bb1835aef5b58f7fc275f03307 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Jul 2024 17:31:27 +0200 Subject: [PATCH 242/262] Update changelog for 0.25 --- CHANGELOG.rst | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d7ecb3ab..67d56f88 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,18 +6,29 @@ Changelog General information: -* The bpython-cli rendering backend has been removed following deprecation in +* The `bpython-cli` rendering backend has been removed following deprecation in version 0.19. - +* This release is focused on Python 3.12 support. New features: Fixes: +* Fix __signature__ support + Thanks to gpotter2 +* #995: Fix handling of `SystemExit` +* #996: Improve order of completion results + Thanks to gpotter2 +* Fix build of documentation and manpages with Sphinx >= 7 +* #1001: Do not fail if modules don't have __version__ Changes to dependencies: +* Remove use of distutils + Thanks to Anderson Bravalheri + +Support for Python 3.12 has been added. Support for Python 3.7 has been dropped. 0.24 ---- @@ -37,7 +48,7 @@ Fixes: Changes to dependencies: -* wheel is no required as part of pyproject.toml's build dependencies +* wheel is not required as part of pyproject.toml's build dependencies Support for Python 3.11 has been added. From ce710bbdb48be7ec8325d01ee156ba666e258c63 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Jul 2024 17:57:18 +0200 Subject: [PATCH 243/262] Format with black --- bpython/_typing_compat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py index 83567b4f..486aacaf 100644 --- a/bpython/_typing_compat.py +++ b/bpython/_typing_compat.py @@ -25,4 +25,3 @@ from typing import Never except ImportError: from typing import NoReturn as Never # type: ignore - From b6318376b255be645b16aaf27c6475359e384ab9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Jul 2024 21:27:49 +0200 Subject: [PATCH 244/262] Update copyright year --- bpython/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/__init__.py b/bpython/__init__.py index 8c7bfb37..26fa3e63 100644 --- a/bpython/__init__.py +++ b/bpython/__init__.py @@ -31,7 +31,7 @@ __author__ = ( "Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al." ) -__copyright__ = f"(C) 2008-2023 {__author__}" +__copyright__ = f"(C) 2008-2024 {__author__}" __license__ = "MIT" __version__ = version package_dir = os.path.abspath(os.path.dirname(__file__)) From f0c023071a8f21b2007389ffcbe57399f35f0cac Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 25 Oct 2024 11:28:44 +0200 Subject: [PATCH 245/262] CI: test with Python 3.13 --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index de2f98cc..747d55a4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -20,6 +20,7 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" - "pypy-3.8" steps: - uses: actions/checkout@v4 From bbdff64fe37b851b6a33183a6a35739bbb4687a0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 25 Oct 2024 11:45:34 +0200 Subject: [PATCH 246/262] Accept source in showsyntaxerror --- bpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/repl.py b/bpython/repl.py index f30cfa31..b048314d 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -152,7 +152,7 @@ def runsource( with self.timer: return super().runsource(source, filename, symbol) - def showsyntaxerror(self, filename: Optional[str] = None) -> None: + def showsyntaxerror(self, filename: Optional[str] = None, source: Optional[str] = None) -> None: """Override the regular handler, the code's copied and pasted from code.py, as per showtraceback, but with the syntaxerror callback called and the text in a pretty colour.""" From 52a7a157037f1d8ef81bd0672b636b837d90bed6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 25 Oct 2024 17:42:06 +0200 Subject: [PATCH 247/262] Switch assert arguments to display correct value as expected --- bpython/test/test_interpreter.py | 4 ++-- bpython/test/test_repl.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index a4a32dd0..acad12c1 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -130,8 +130,8 @@ def gfunc(): ) a = i.a - self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) - self.assertEqual(plain("").join(a), expected) + self.assertMultiLineEqual(str(expected), str(plain("").join(a))) + self.assertEqual(expected, plain("").join(a)) def test_getsource_works_on_interactively_defined_functions(self): source = "def foo(x):\n return x + 1\n" diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 8c3b85cc..3f6b7c12 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -307,7 +307,7 @@ def assert_get_source_error_for_current_function(self, func, msg): try: self.repl.get_source_of_current_name() except repl.SourceNotFound as e: - self.assertEqual(e.args[0], msg) + self.assertEqual(msg, e.args[0]) else: self.fail("Should have raised SourceNotFound") From 45f4117b534d6827279f7b9e633f3cabe0fb37e6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 25 Oct 2024 17:42:20 +0200 Subject: [PATCH 248/262] Fix test errors with Python 3.13 --- bpython/test/test_interpreter.py | 17 ++++++++++++++++- bpython/test/test_repl.py | 11 ++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index acad12c1..b9f0a31e 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -99,7 +99,22 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - if (3, 11) <= sys.version_info[:2]: + if (3, 13) <= sys.version_info[:2]: + expected = ( + "Traceback (most recent call last):\n File " + + green('""') + + ", line " + + bold(magenta("1")) + + ", in " + + cyan("") + + "\n gfunc()" + + "\n ^^^^^\n" + + bold(red("NameError")) + + ": " + + cyan(global_not_found) + + "\n" + ) + elif (3, 11) <= sys.version_info[:2]: expected = ( "Traceback (most recent call last):\n File " + green('""') diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 3f6b7c12..5cafec94 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -332,9 +332,14 @@ def test_current_function_cpython(self): self.assert_get_source_error_for_current_function( collections.defaultdict.copy, "No source code found for INPUTLINE" ) - self.assert_get_source_error_for_current_function( - collections.defaultdict, "could not find class definition" - ) + if sys.version_info[:2] >= (3, 13): + self.assert_get_source_error_for_current_function( + collections.defaultdict, "source code not available" + ) + else: + self.assert_get_source_error_for_current_function( + collections.defaultdict, "could not find class definition" + ) def test_current_line(self): self.repl.interp.locals["a"] = socket.socket From 4605fabe156a9bf1abfee5945514cf81c1828df2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 04:28:01 +0000 Subject: [PATCH 249/262] Bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 747d55a4..7ccef9bc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -47,7 +47,7 @@ jobs: run: | pytest --cov=bpython --cov-report=xml -v - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 env: PYTHON_VERSION: ${{ matrix.python-version }} with: From c332f0d684160a612950bcf88d1a1cfec50b4ab2 Mon Sep 17 00:00:00 2001 From: Max R Date: Mon, 16 Dec 2024 10:00:04 -0500 Subject: [PATCH 250/262] black --- bpython/repl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bpython/repl.py b/bpython/repl.py index b048314d..c87d1965 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -152,7 +152,9 @@ def runsource( with self.timer: return super().runsource(source, filename, symbol) - def showsyntaxerror(self, filename: Optional[str] = None, source: Optional[str] = None) -> None: + def showsyntaxerror( + self, filename: Optional[str] = None, source: Optional[str] = None + ) -> None: """Override the regular handler, the code's copied and pasted from code.py, as per showtraceback, but with the syntaxerror callback called and the text in a pretty colour.""" From d3e7a174831c08c91d0574ae05eebe0eb9bf4cb9 Mon Sep 17 00:00:00 2001 From: Max R Date: Mon, 16 Dec 2024 10:05:38 -0500 Subject: [PATCH 251/262] codespell --- .github/workflows/lint.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d960c6d8..fbb5d996 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -24,8 +24,8 @@ jobs: - uses: actions/checkout@v4 - uses: codespell-project/actions-codespell@master with: - skip: '*.po' - ignore_words_list: ba,te,deltion,dedent,dedented + skip: '*.po',encoding_latin1.py + ignore_words_list: ba,te,deltion,dedent,dedented,assertIn mypy: runs-on: ubuntu-latest From c70fa70b8fdd57fa25d7c8890727d17e78397e27 Mon Sep 17 00:00:00 2001 From: Max R Date: Mon, 16 Dec 2024 10:39:23 -0500 Subject: [PATCH 252/262] mypy --- bpython/_typing_compat.py | 2 +- bpython/curtsiesfrontend/repl.py | 3 ++- bpython/pager.py | 3 ++- bpython/test/test_preprocess.py | 3 ++- setup.cfg | 1 + 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py index 486aacaf..5d9a3607 100644 --- a/bpython/_typing_compat.py +++ b/bpython/_typing_compat.py @@ -24,4 +24,4 @@ # introduced in Python 3.11 from typing import Never except ImportError: - from typing import NoReturn as Never # type: ignore + from typing_extensions import Never # type: ignore diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 302e67d4..69eb18c9 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -236,7 +236,8 @@ def close(self) -> None: @property def encoding(self) -> str: - return sys.__stdin__.encoding + # `encoding` is new in py39 + return sys.__stdin__.encoding # type: ignore # TODO write a read() method? diff --git a/bpython/pager.py b/bpython/pager.py index e145e0ed..65a3b223 100644 --- a/bpython/pager.py +++ b/bpython/pager.py @@ -55,7 +55,8 @@ def page(data: str, use_internal: bool = False) -> None: try: popen = subprocess.Popen(command, stdin=subprocess.PIPE) assert popen.stdin is not None - data_bytes = data.encode(sys.__stdout__.encoding, "replace") + # `encoding` is new in py39 + data_bytes = data.encode(sys.__stdout__.encoding, "replace") # type: ignore popen.stdin.write(data_bytes) popen.stdin.close() except OSError as e: diff --git a/bpython/test/test_preprocess.py b/bpython/test/test_preprocess.py index e9309f1e..a72a64b6 100644 --- a/bpython/test/test_preprocess.py +++ b/bpython/test/test_preprocess.py @@ -4,6 +4,7 @@ import unittest from code import compile_command as compiler +from codeop import CommandCompiler from functools import partial from bpython.curtsiesfrontend.interpreter import code_finished_will_parse @@ -11,7 +12,7 @@ from bpython.test.fodder import original, processed -preproc = partial(preprocess, compiler=compiler) +preproc = partial(preprocess, compiler=CommandCompiler) def get_fodder_source(test_name): diff --git a/setup.cfg b/setup.cfg index 1fe4a6f9..9a4f0bcb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,7 @@ install_requires = pygments pyxdg requests + typing_extensions ; python_version < "3.11" [options.extras_require] clipboard = pyperclip From 839145e913d72eb025f61086a09f133a0230350f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 15 Jan 2025 23:53:17 +0100 Subject: [PATCH 253/262] Handle title argument of pydoc.pager (fixes #1029) --- bpython/curtsiesfrontend/_internal.py | 4 ++-- bpython/curtsiesfrontend/repl.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index 0480c1b0..16a598fa 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -52,8 +52,8 @@ def __init__(self, repl=None): super().__init__() - def pager(self, output): - self._repl.pager(output) + def pager(self, output, title=""): + self._repl.pager(output, title) def __call__(self, *args, **kwargs): if self._repl.reevaluating: diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 69eb18c9..01b04711 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -2101,10 +2101,10 @@ def focus_on_subprocess(self, args): finally: signal.signal(signal.SIGWINCH, prev_sigwinch_handler) - def pager(self, text: str) -> None: - """Runs an external pager on text + def pager(self, text: str, title: str = "") -> None: + """Runs an external pager on text""" - text must be a str""" + # TODO: make less handle title command = get_pager_command() with tempfile.NamedTemporaryFile() as tmp: tmp.write(text.encode(getpreferredencoding())) From f5d6da985b30091c0d4dda67d35a4877200ea344 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 16 Jan 2025 00:07:33 +0100 Subject: [PATCH 254/262] Drop support for Python 3.8 --- .github/workflows/build.yaml | 81 +++++++++++++++--------------- bpython/simpleeval.py | 23 ++------- doc/sphinx/source/contributing.rst | 2 +- doc/sphinx/source/releases.rst | 2 +- pyproject.toml | 6 +-- setup.cfg | 2 +- 6 files changed, 48 insertions(+), 68 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7ccef9bc..a6e9aef0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,53 +4,52 @@ on: push: pull_request: schedule: - # run at 7:00 on the first of every month - - cron: '0 7 1 * *' + # run at 7:00 on the first of every month + - cron: "0 7 1 * *" jobs: build: runs-on: ubuntu-latest - continue-on-error: ${{ matrix.python-version == 'pypy-3.8' }} + continue-on-error: ${{ matrix.python-version == 'pypy-3.9' }} strategy: fail-fast: false matrix: python-version: - - "3.8" - - "3.9" - - "3.10" - - "3.11" - - "3.12" - - "3.13" - - "pypy-3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "pypy-3.9" steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" - pip install pytest pytest-cov numpy - - name: Build with Python ${{ matrix.python-version }} - run: | - python setup.py build - - name: Build documentation - run: | - python setup.py build_sphinx - python setup.py build_sphinx_man - - name: Test with pytest - run: | - pytest --cov=bpython --cov-report=xml -v - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - env: - PYTHON_VERSION: ${{ matrix.python-version }} - with: - file: ./coverage.xml - env_vars: PYTHON_VERSION - if: ${{ always() }} + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" + pip install pytest pytest-cov numpy + - name: Build with Python ${{ matrix.python-version }} + run: | + python setup.py build + - name: Build documentation + run: | + python setup.py build_sphinx + python setup.py build_sphinx_man + - name: Test with pytest + run: | + pytest --cov=bpython --cov-report=xml -v + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + env: + PYTHON_VERSION: ${{ matrix.python-version }} + with: + file: ./coverage.xml + env_vars: PYTHON_VERSION + if: ${{ always() }} diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index c5bba43d..3f334af4 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -33,12 +33,9 @@ from . import line as line_properties from .inspection import getattr_safe -_is_py38 = sys.version_info[:2] >= (3, 8) -_is_py39 = sys.version_info[:2] >= (3, 9) - _string_type_nodes = (ast.Str, ast.Bytes) _numeric_types = (int, float, complex) -_name_type_nodes = (ast.Name,) if _is_py38 else (ast.Name, ast.NameConstant) +_name_type_nodes = (ast.Name,) class EvaluationError(Exception): @@ -91,10 +88,6 @@ def simple_eval(node_or_string, namespace=None): def _convert(node): if isinstance(node, ast.Constant): return node.value - elif not _is_py38 and isinstance(node, _string_type_nodes): - return node.s - elif not _is_py38 and isinstance(node, ast.Num): - return node.n elif isinstance(node, ast.Tuple): return tuple(map(_convert, node.elts)) elif isinstance(node, ast.List): @@ -168,18 +161,8 @@ def _convert(node): return left - right # this is a deviation from literal_eval: we allow indexing - elif ( - not _is_py39 - and isinstance(node, ast.Subscript) - and isinstance(node.slice, ast.Index) - ): - obj = _convert(node.value) - index = _convert(node.slice.value) - return safe_getitem(obj, index) - elif ( - _is_py39 - and isinstance(node, ast.Subscript) - and isinstance(node.slice, (ast.Constant, ast.Name)) + elif isinstance(node, ast.Subscript) and isinstance( + node.slice, (ast.Constant, ast.Name) ): obj = _convert(node.value) index = _convert(node.slice) diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 32b1ea86..3b93089d 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -17,7 +17,7 @@ the time of day. Getting your development environment set up ------------------------------------------- -bpython supports Python 3.8 and newer. The code is compatible with all +bpython supports Python 3.9 and newer. The code is compatible with all supported versions. Using a virtual environment is probably a good idea. Create a virtual diff --git a/doc/sphinx/source/releases.rst b/doc/sphinx/source/releases.rst index fcce5c1c..7d789f16 100644 --- a/doc/sphinx/source/releases.rst +++ b/doc/sphinx/source/releases.rst @@ -45,7 +45,7 @@ A checklist to perform some manual tests before a release: Check that all of the following work before a release: -* Runs under Python 3.8 - 3.11 +* Runs under Python 3.9 - 3.13 * Save * Rewind * Pastebin diff --git a/pyproject.toml b/pyproject.toml index 924722b0..ca4e0450 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,10 @@ [build-system] -requires = [ - "setuptools >= 62.4.0", -] +requires = ["setuptools >= 62.4.0"] build-backend = "setuptools.build_meta" [tool.black] line-length = 80 -target_version = ["py38"] +target_version = ["py39"] include = '\.pyi?$' exclude = ''' /( diff --git a/setup.cfg b/setup.cfg index 9a4f0bcb..f8b7c325 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ classifiers = Programming Language :: Python :: 3 [options] -python_requires = >=3.8 +python_requires = >=3.9 packages = bpython bpython.curtsiesfrontend From b8923057a2aefe55c97164ff36f7766c82b7ea0b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 16 Jan 2025 00:14:17 +0100 Subject: [PATCH 255/262] Update changelog for 0.25 --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 67d56f88..114b9440 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,7 +8,7 @@ General information: * The `bpython-cli` rendering backend has been removed following deprecation in version 0.19. -* This release is focused on Python 3.12 support. +* This release is focused on Python 3.13 support. New features: @@ -28,7 +28,7 @@ Changes to dependencies: * Remove use of distutils Thanks to Anderson Bravalheri -Support for Python 3.12 has been added. Support for Python 3.7 has been dropped. +Support for Python 3.12 and 3.13 has been added. Support for Python 3.7 and 3.8 has been dropped. 0.24 ---- From 5b31cca96c951ddefba8f2c71a4abc208b2adac0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 16 Jan 2025 00:16:37 +0100 Subject: [PATCH 256/262] CI: fix yaml --- .github/workflows/lint.yaml | 54 ++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index fbb5d996..b6056159 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -8,38 +8,38 @@ jobs: black: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install black codespell - - name: Check with black - run: black --check . + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install black codespell + - name: Check with black + run: black --check . codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: codespell-project/actions-codespell@master - with: - skip: '*.po',encoding_latin1.py - ignore_words_list: ba,te,deltion,dedent,dedented,assertIn + - uses: actions/checkout@v4 + - uses: codespell-project/actions-codespell@master + with: + skip: "*.po,encoding_latin1.py" + ignore_words_list: ba,te,deltion,dedent,dedented,assertIn mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install mypy - pip install -r requirements.txt - pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" numpy - pip install types-backports types-requests types-setuptools types-toml types-pygments - - name: Check with mypy - # for now only run on a few files to avoid slipping backward - run: mypy + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install mypy + pip install -r requirements.txt + pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" numpy + pip install types-backports types-requests types-setuptools types-toml types-pygments + - name: Check with mypy + # for now only run on a few files to avoid slipping backward + run: mypy From 400f5eda1ae7859b88c81d591177b54128d0e835 Mon Sep 17 00:00:00 2001 From: Pushkar Kulkarni Date: Thu, 16 Jan 2025 11:02:07 +0530 Subject: [PATCH 257/262] More general adaptation of showsyntaxerror() to Python 3.13 Python 3.13's code.InteractiveInterpreter adds a new **kwargs argument to its showsyntaxerror() method. Currently, the only use of it is to send a named argument of name "source". Whilst the current adapation of repl.Interpreter is specific and should work in the short term, here is a more general solution. --- bpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/repl.py b/bpython/repl.py index c87d1965..0374bb6b 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -153,7 +153,7 @@ def runsource( return super().runsource(source, filename, symbol) def showsyntaxerror( - self, filename: Optional[str] = None, source: Optional[str] = None + self, filename: Optional[str] = None, **kwargs ) -> None: """Override the regular handler, the code's copied and pasted from code.py, as per showtraceback, but with the syntaxerror callback called From a4eadd750d2d3b103bb78abaac717358f7efc722 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 17 Jan 2025 21:21:40 +0100 Subject: [PATCH 258/262] Start development of 0.26 --- CHANGELOG.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 114b9440..f55fe76f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,22 @@ Changelog ========= +0.26 +---- + +General information: + + +New features: + + +Fixes: + + +Changes to dependencies: + + + 0.25 ---- From 9b344248ac8a180f4c54dc4b2eb6940596c6067b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 17 Jan 2025 21:44:57 +0100 Subject: [PATCH 259/262] Upgrade code to 3.9+ --- bpython/args.py | 8 ++-- bpython/autocomplete.py | 54 ++++++++++++------------- bpython/config.py | 5 ++- bpython/curtsies.py | 13 +++--- bpython/curtsiesfrontend/_internal.py | 2 +- bpython/curtsiesfrontend/events.py | 2 +- bpython/curtsiesfrontend/filewatch.py | 7 ++-- bpython/curtsiesfrontend/interpreter.py | 9 +++-- bpython/curtsiesfrontend/parse.py | 4 +- bpython/curtsiesfrontend/preprocess.py | 2 +- bpython/curtsiesfrontend/repl.py | 27 ++++++------- bpython/filelock.py | 2 +- bpython/formatter.py | 3 +- bpython/history.py | 13 +++--- bpython/importcompletion.py | 15 +++---- bpython/inspection.py | 16 ++++---- bpython/keys.py | 4 +- bpython/lazyre.py | 4 +- bpython/line.py | 2 +- bpython/pager.py | 2 +- bpython/paste.py | 6 +-- bpython/patch_linecache.py | 4 +- bpython/repl.py | 52 ++++++++++++------------ bpython/simpleeval.py | 4 +- bpython/test/test_curtsies_repl.py | 2 +- bpython/test/test_inspection.py | 6 +-- bpython/test/test_line_properties.py | 2 +- bpython/test/test_repl.py | 2 +- bpython/translations/__init__.py | 2 +- 29 files changed, 139 insertions(+), 135 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 55691a2a..1eb59a69 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -73,14 +73,14 @@ def log_version(module: ModuleType, name: str) -> None: logger.info("%s: %s", name, module.__version__ if hasattr(module, "__version__") else "unknown version") # type: ignore -Options = Tuple[str, str, Callable[[argparse._ArgumentGroup], None]] +Options = tuple[str, str, Callable[[argparse._ArgumentGroup], None]] def parse( - args: Optional[List[str]], + args: Optional[list[str]], extras: Optional[Options] = None, ignore_stdin: bool = False, -) -> Tuple[Config, argparse.Namespace, List[str]]: +) -> tuple[Config, argparse.Namespace, list[str]]: """Receive an argument list - if None, use sys.argv - parse all args and take appropriate action. Also receive optional extra argument: this should be a tuple of (title, description, callback) @@ -256,7 +256,7 @@ def callback(group): def exec_code( - interpreter: code.InteractiveInterpreter, args: List[str] + interpreter: code.InteractiveInterpreter, args: list[str] ) -> None: """ Helper to execute code in a given interpreter, e.g. to implement the behavior of python3 [-i] file.py diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 000fbde9..88afbe54 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -40,13 +40,13 @@ from typing import ( Any, Dict, - Iterator, List, Optional, - Sequence, Set, Tuple, ) +from collections.abc import Iterator, Sequence + from . import inspection from . import line as lineparts from .line import LinePart @@ -236,7 +236,7 @@ def __init__( @abc.abstractmethod def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: """Returns a list of possible matches given a line and cursor, or None if this completion type isn't applicable. @@ -268,7 +268,7 @@ def format(self, word: str) -> str: def substitute( self, cursor_offset: int, line: str, match: str - ) -> Tuple[int, str]: + ) -> tuple[int, str]: """Returns a cursor offset and line with match swapped in""" lpart = self.locate(cursor_offset, line) assert lpart @@ -311,7 +311,7 @@ def format(self, word: str) -> str: def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: return_value = None all_matches = set() for completer in self._completers: @@ -336,7 +336,7 @@ def __init__( def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: return self.module_gatherer.complete(cursor_offset, line) def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: @@ -356,7 +356,7 @@ def __init__(self, mode: AutocompleteModes = AutocompleteModes.SIMPLE): def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: cs = lineparts.current_string(cursor_offset, line) if cs is None: return None @@ -389,9 +389,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: r = self.locate(cursor_offset, line) if r is None: return None @@ -421,7 +421,7 @@ def format(self, word: str) -> str: return _after_last_dot(word) def attr_matches( - self, text: str, namespace: Dict[str, Any] + self, text: str, namespace: dict[str, Any] ) -> Iterator[str]: """Taken from rlcompleter.py and bent to my will.""" @@ -460,7 +460,7 @@ def attr_lookup(self, obj: Any, expr: str, attr: str) -> Iterator[str]: if self.method_match(word, n, attr) and word != "__builtins__" ) - def list_attributes(self, obj: Any) -> List[str]: + def list_attributes(self, obj: Any) -> list[str]: # TODO: re-implement dir without AttrCleaner here # # Note: accessing `obj.__dir__` via `getattr_static` is not side-effect free. @@ -474,9 +474,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: if locals_ is None: return None @@ -516,7 +516,7 @@ def matches( current_block: Optional[str] = None, complete_magic_methods: Optional[bool] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: if ( current_block is None or complete_magic_methods is None @@ -541,9 +541,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: """Compute matches when text is a simple name. Return a list of all keywords, built-in functions and names currently defined in self.namespace that match. @@ -583,7 +583,7 @@ def matches( *, funcprops: Optional[inspection.FuncProps] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: if funcprops is None: return None @@ -622,9 +622,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: if locals_ is None: locals_ = __main__.__dict__ @@ -648,7 +648,7 @@ def matches( class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: return None def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: @@ -665,9 +665,9 @@ def matches( line: str, *, current_block: Optional[str] = None, - history: Optional[List[str]] = None, + history: Optional[list[str]] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: if ( current_block is None or history is None @@ -725,12 +725,12 @@ def get_completer( cursor_offset: int, line: str, *, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, argspec: Optional[inspection.FuncProps] = None, - history: Optional[List[str]] = None, + history: Optional[list[str]] = None, current_block: Optional[str] = None, complete_magic_methods: Optional[bool] = None, -) -> Tuple[List[str], Optional[BaseCompletionType]]: +) -> tuple[list[str], Optional[BaseCompletionType]]: """Returns a list of matches and an applicable completer If no matches available, returns a tuple of an empty list and None @@ -747,7 +747,7 @@ def get_completer( double underscore methods like __len__ in method signatures """ - def _cmpl_sort(x: str) -> Tuple[bool, str]: + def _cmpl_sort(x: str) -> tuple[bool, str]: """ Function used to sort the matches. """ @@ -784,7 +784,7 @@ def _cmpl_sort(x: str) -> Tuple[bool, str]: def get_default_completer( mode: AutocompleteModes, module_gatherer: ModuleGatherer -) -> Tuple[BaseCompletionType, ...]: +) -> tuple[BaseCompletionType, ...]: return ( ( DictKeyCompletion(mode=mode), diff --git a/bpython/config.py b/bpython/config.py index 5123ec22..27af8740 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -31,7 +31,8 @@ from configparser import ConfigParser from itertools import chain from pathlib import Path -from typing import MutableMapping, Mapping, Any, Dict +from typing import Any, Dict +from collections.abc import MutableMapping, Mapping from xdg import BaseDirectory from .autocomplete import AutocompleteModes @@ -115,7 +116,7 @@ class Config: "right_arrow_suggestion": "K", } - defaults: Dict[str, Dict[str, Any]] = { + defaults: dict[str, dict[str, Any]] = { "general": { "arg_spec": True, "auto_display_list": True, diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 11b96050..547a853e 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -25,14 +25,13 @@ Any, Callable, Dict, - Generator, List, Optional, Protocol, - Sequence, Tuple, Union, ) +from collections.abc import Generator, Sequence logger = logging.getLogger(__name__) @@ -51,7 +50,7 @@ class FullCurtsiesRepl(BaseRepl): def __init__( self, config: Config, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, banner: Optional[str] = None, interp: Optional[Interp] = None, ) -> None: @@ -111,7 +110,7 @@ def interrupting_refresh(self) -> None: def request_undo(self, n: int = 1) -> None: return self._request_undo_callback(n=n) - def get_term_hw(self) -> Tuple[int, int]: + def get_term_hw(self) -> tuple[int, int]: return self.window.get_term_hw() def get_cursor_vertical_diff(self) -> int: @@ -179,8 +178,8 @@ def mainloop( def main( - args: Optional[List[str]] = None, - locals_: Optional[Dict[str, Any]] = None, + args: Optional[list[str]] = None, + locals_: Optional[dict[str, Any]] = None, banner: Optional[str] = None, welcome_message: Optional[str] = None, ) -> Any: @@ -209,7 +208,7 @@ def curtsies_arguments(parser: argparse._ArgumentGroup) -> None: interp = None paste = None - exit_value: Tuple[Any, ...] = () + exit_value: tuple[Any, ...] = () if exec_args: if not options: raise ValueError("don't pass in exec_args without options") diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index 16a598fa..cb7b8105 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -34,7 +34,7 @@ def __enter__(self): def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: diff --git a/bpython/curtsiesfrontend/events.py b/bpython/curtsiesfrontend/events.py index 26f105dc..4f9c13e5 100644 --- a/bpython/curtsiesfrontend/events.py +++ b/bpython/curtsiesfrontend/events.py @@ -1,7 +1,7 @@ """Non-keyboard events used in bpython curtsies REPL""" import time -from typing import Sequence +from collections.abc import Sequence import curtsies.events diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index e70325ab..53ae4784 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -1,6 +1,7 @@ import os from collections import defaultdict -from typing import Callable, Dict, Iterable, Sequence, Set, List +from typing import Callable, Dict, Set, List +from collections.abc import Iterable, Sequence from .. import importcompletion @@ -20,9 +21,9 @@ def __init__( paths: Iterable[str], on_change: Callable[[Sequence[str]], None], ) -> None: - self.dirs: Dict[str, Set[str]] = defaultdict(set) + self.dirs: dict[str, set[str]] = defaultdict(set) self.on_change = on_change - self.modules_to_add_later: List[str] = [] + self.modules_to_add_later: list[str] = [] self.observer = Observer() self.started = False self.activated = False diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 82e28091..6532d968 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -1,6 +1,7 @@ import sys from codeop import CommandCompiler -from typing import Any, Dict, Iterable, Optional, Tuple, Union +from typing import Any, Dict, Optional, Tuple, Union +from collections.abc import Iterable from pygments.token import Generic, Token, Keyword, Name, Comment, String from pygments.token import Error, Literal, Number, Operator, Punctuation @@ -47,7 +48,7 @@ class BPythonFormatter(Formatter): def __init__( self, - color_scheme: Dict[_TokenType, str], + color_scheme: dict[_TokenType, str], **options: Union[str, bool, None], ) -> None: self.f_strings = {k: f"\x01{v}" for k, v in color_scheme.items()} @@ -67,7 +68,7 @@ def format(self, tokensource, outfile): class Interp(ReplInterpreter): def __init__( self, - locals: Optional[Dict[str, Any]] = None, + locals: Optional[dict[str, Any]] = None, ) -> None: """Constructor. @@ -121,7 +122,7 @@ def format(self, tbtext: str, lexer: Any) -> None: def code_finished_will_parse( s: str, compiler: CommandCompiler -) -> Tuple[bool, bool]: +) -> tuple[bool, bool]: """Returns a tuple of whether the buffer could be complete and whether it will parse diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 88a149a6..96e91e55 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -60,7 +60,7 @@ def parse(s: str) -> FmtStr: ) -def fs_from_match(d: Dict[str, Any]) -> FmtStr: +def fs_from_match(d: dict[str, Any]) -> FmtStr: atts = {} color = "default" if d["fg"]: @@ -99,7 +99,7 @@ def fs_from_match(d: Dict[str, Any]) -> FmtStr: ) -def peel_off_string(s: str) -> Tuple[Dict[str, Any], str]: +def peel_off_string(s: str) -> tuple[dict[str, Any], str]: m = peel_off_string_re.match(s) assert m, repr(s) d = m.groupdict() diff --git a/bpython/curtsiesfrontend/preprocess.py b/bpython/curtsiesfrontend/preprocess.py index 5e59dd49..f48a79bf 100644 --- a/bpython/curtsiesfrontend/preprocess.py +++ b/bpython/curtsiesfrontend/preprocess.py @@ -2,7 +2,7 @@ etc)""" from codeop import CommandCompiler -from typing import Match +from re import Match from itertools import tee, islice, chain from ..lazyre import LazyReCompile diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 01b04711..09f73a82 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -14,16 +14,15 @@ from types import FrameType, TracebackType from typing import ( Any, - Iterable, Dict, List, Literal, Optional, - Sequence, Tuple, Type, Union, ) +from collections.abc import Iterable, Sequence import greenlet from curtsies import ( @@ -121,7 +120,7 @@ def __init__( self.current_line = "" self.cursor_offset = 0 self.old_num_lines = 0 - self.readline_results: List[str] = [] + self.readline_results: list[str] = [] if configured_edit_keys is not None: self.rl_char_sequences = configured_edit_keys else: @@ -195,7 +194,7 @@ def readline(self, size: int = -1) -> str: self.readline_results.append(value) return value if size <= -1 else value[:size] - def readlines(self, size: Optional[int] = -1) -> List[str]: + def readlines(self, size: Optional[int] = -1) -> list[str]: if size is None: # the default readlines implementation also accepts None size = -1 @@ -338,10 +337,10 @@ def __init__( self, config: Config, window: CursorAwareWindow, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, banner: Optional[str] = None, interp: Optional[Interp] = None, - orig_tcattrs: Optional[List[Any]] = None, + orig_tcattrs: Optional[list[Any]] = None, ): """ locals_ is a mapping of locals to pass into the interpreter @@ -404,7 +403,7 @@ def __init__( # this is every line that's been displayed (input and output) # as with formatting applied. Logical lines that exceeded the terminal width # at the time of output are split across multiple entries in this list. - self.display_lines: List[FmtStr] = [] + self.display_lines: list[FmtStr] = [] # this is every line that's been executed; it gets smaller on rewind self.history = [] @@ -415,11 +414,11 @@ def __init__( # - the first element the line (string, not fmtsr) # - the second element is one of 2 global constants: "input" or "output" # (use LineType.INPUT or LineType.OUTPUT to avoid typing these strings) - self.all_logical_lines: List[Tuple[str, LineType]] = [] + self.all_logical_lines: list[tuple[str, LineType]] = [] # formatted version of lines in the buffer kept around so we can # unhighlight parens using self.reprint_line as called by bpython.Repl - self.display_buffer: List[FmtStr] = [] + self.display_buffer: list[FmtStr] = [] # how many times display has been scrolled down # because there wasn't room to display everything @@ -428,7 +427,7 @@ def __init__( # cursor position relative to start of current_line, 0 is first char self._cursor_offset = 0 - self.orig_tcattrs: Optional[List[Any]] = orig_tcattrs + self.orig_tcattrs: Optional[list[Any]] = orig_tcattrs self.coderunner = CodeRunner(self.interp, self.request_refresh) @@ -460,7 +459,7 @@ def __init__( # some commands act differently based on the prev event # this list doesn't include instances of event.Event, # only keypress-type events (no refresh screen events etc.) - self.last_events: List[Optional[str]] = [None] * 50 + self.last_events: list[Optional[str]] = [None] * 50 # displays prev events in a column on the right hand side self.presentation_mode = False @@ -601,7 +600,7 @@ def __enter__(self): def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: @@ -1561,7 +1560,7 @@ def paint( user_quit=False, try_preserve_history_height=30, min_infobox_height=5, - ) -> Tuple[FSArray, Tuple[int, int]]: + ) -> tuple[FSArray, tuple[int, int]]: """Returns an array of min_height or more rows and width columns, plus cursor position @@ -2237,7 +2236,7 @@ def compress_paste_event(paste_event): def just_simple_events( event_list: Iterable[Union[str, events.Event]] -) -> List[str]: +) -> list[str]: simple_events = [] for e in event_list: if isinstance(e, events.Event): diff --git a/bpython/filelock.py b/bpython/filelock.py index 11f575b6..5ed8769f 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -56,7 +56,7 @@ def __enter__(self) -> "BaseLock": def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: diff --git a/bpython/formatter.py b/bpython/formatter.py index f216f213..8e74ac2c 100644 --- a/bpython/formatter.py +++ b/bpython/formatter.py @@ -28,7 +28,8 @@ # mypy: disallow_untyped_calls=True -from typing import Any, MutableMapping, Iterable, TextIO +from typing import Any, TextIO +from collections.abc import MutableMapping, Iterable from pygments.formatter import Formatter from pygments.token import ( _TokenType, diff --git a/bpython/history.py b/bpython/history.py index 13dbb5b7..386214b4 100644 --- a/bpython/history.py +++ b/bpython/history.py @@ -25,7 +25,8 @@ from pathlib import Path import stat from itertools import islice, chain -from typing import Iterable, Optional, List, TextIO +from typing import Optional, List, TextIO +from collections.abc import Iterable from .translations import _ from .filelock import FileLock @@ -55,7 +56,7 @@ def __init__( def append(self, line: str) -> None: self.append_to(self.entries, line) - def append_to(self, entries: List[str], line: str) -> None: + def append_to(self, entries: list[str], line: str) -> None: line = line.rstrip("\n") if line: if not self.duplicates: @@ -100,7 +101,7 @@ def entry(self) -> str: return self.entries[-self.index] if self.index else self.saved_line @property - def entries_by_index(self) -> List[str]: + def entries_by_index(self) -> list[str]: return list(chain((self.saved_line,), reversed(self.entries))) def find_match_backward( @@ -196,8 +197,8 @@ def load(self, filename: Path, encoding: str) -> None: with FileLock(hfile, filename=str(filename)): self.entries = self.load_from(hfile) - def load_from(self, fd: TextIO) -> List[str]: - entries: List[str] = [] + def load_from(self, fd: TextIO) -> list[str]: + entries: list[str] = [] for line in fd: self.append_to(entries, line) return entries if len(entries) else [""] @@ -213,7 +214,7 @@ def save(self, filename: Path, encoding: str, lines: int = 0) -> None: self.save_to(hfile, self.entries, lines) def save_to( - self, fd: TextIO, entries: Optional[List[str]] = None, lines: int = 0 + self, fd: TextIO, entries: Optional[list[str]] = None, lines: int = 0 ) -> None: if entries is None: entries = self.entries diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 9df140c6..da1b9140 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -27,7 +27,8 @@ import warnings from dataclasses import dataclass from pathlib import Path -from typing import Optional, Set, Generator, Sequence, Iterable, Union +from typing import Optional, Set, Union +from collections.abc import Generator, Sequence, Iterable from .line import ( current_word, @@ -69,9 +70,9 @@ def __init__( directory names. If `paths` is not given, `sys.path` will be used.""" # Cached list of all known modules - self.modules: Set[str] = set() + self.modules: set[str] = set() # Set of (st_dev, st_ino) to compare against so that paths are not repeated - self.paths: Set[_LoadedInode] = set() + self.paths: set[_LoadedInode] = set() # Patterns to skip self.skiplist: Sequence[str] = ( skiplist if skiplist is not None else tuple() @@ -86,7 +87,7 @@ def __init__( Path(p).resolve() if p else Path.cwd() for p in paths ) - def module_matches(self, cw: str, prefix: str = "") -> Set[str]: + def module_matches(self, cw: str, prefix: str = "") -> set[str]: """Modules names to replace cw with""" full = f"{prefix}.{cw}" if prefix else cw @@ -102,7 +103,7 @@ def module_matches(self, cw: str, prefix: str = "") -> Set[str]: def attr_matches( self, cw: str, prefix: str = "", only_modules: bool = False - ) -> Set[str]: + ) -> set[str]: """Attributes to replace name with""" full = f"{prefix}.{cw}" if prefix else cw module_name, _, name_after_dot = full.rpartition(".") @@ -126,11 +127,11 @@ def attr_matches( return matches - def module_attr_matches(self, name: str) -> Set[str]: + def module_attr_matches(self, name: str) -> set[str]: """Only attributes which are modules to replace name with""" return self.attr_matches(name, only_modules=True) - def complete(self, cursor_offset: int, line: str) -> Optional[Set[str]]: + def complete(self, cursor_offset: int, line: str) -> Optional[set[str]]: """Construct a full list of possibly completions for imports.""" tokens = line.split() if "from" not in tokens and "import" not in tokens: diff --git a/bpython/inspection.py b/bpython/inspection.py index e97a272b..fb1124eb 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -62,13 +62,13 @@ def __repr__(self) -> str: @dataclass class ArgSpec: - args: List[str] + args: list[str] varargs: Optional[str] varkwargs: Optional[str] - defaults: Optional[List[_Repr]] - kwonly: List[str] - kwonly_defaults: Optional[Dict[str, _Repr]] - annotations: Optional[Dict[str, Any]] + defaults: Optional[list[_Repr]] + kwonly: list[str] + kwonly_defaults: Optional[dict[str, _Repr]] + annotations: Optional[dict[str, Any]] @dataclass @@ -118,7 +118,7 @@ def __enter__(self) -> None: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: @@ -134,10 +134,10 @@ def __exit__( return False -def parsekeywordpairs(signature: str) -> Dict[str, str]: +def parsekeywordpairs(signature: str) -> dict[str, str]: preamble = True stack = [] - substack: List[str] = [] + substack: list[str] = [] parendepth = 0 annotation = False for token, value in Python3Lexer().get_tokens(signature): diff --git a/bpython/keys.py b/bpython/keys.py index fe27dbcc..1068a4f2 100644 --- a/bpython/keys.py +++ b/bpython/keys.py @@ -28,7 +28,7 @@ class KeyMap(Generic[T]): def __init__(self, default: T) -> None: - self.map: Dict[str, T] = {} + self.map: dict[str, T] = {} self.default = default def __getitem__(self, key: str) -> T: @@ -49,7 +49,7 @@ def __setitem__(self, key: str, value: T) -> None: self.map[key] = value -cli_key_dispatch: KeyMap[Tuple[str, ...]] = KeyMap(tuple()) +cli_key_dispatch: KeyMap[tuple[str, ...]] = KeyMap(tuple()) urwid_key_dispatch = KeyMap("") # fill dispatch with letters diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 8d166b74..d397f05c 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -21,7 +21,9 @@ # THE SOFTWARE. import re -from typing import Optional, Pattern, Match, Optional, Iterator +from typing import Optional, Optional +from collections.abc import Iterator +from re import Pattern, Match try: from functools import cached_property diff --git a/bpython/line.py b/bpython/line.py index cbc3bf37..363419fe 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -291,7 +291,7 @@ def current_expression_attribute( def cursor_on_closing_char_pair( cursor_offset: int, line: str, ch: Optional[str] = None -) -> Tuple[bool, bool]: +) -> tuple[bool, bool]: """Checks if cursor sits on closing character of a pair and whether its pair character is directly behind it """ diff --git a/bpython/pager.py b/bpython/pager.py index 65a3b223..2fa4846e 100644 --- a/bpython/pager.py +++ b/bpython/pager.py @@ -33,7 +33,7 @@ from typing import List -def get_pager_command(default: str = "less -rf") -> List[str]: +def get_pager_command(default: str = "less -rf") -> list[str]: command = shlex.split(os.environ.get("PAGER", default)) return command diff --git a/bpython/paste.py b/bpython/paste.py index a81c0c6c..e846aba3 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -37,7 +37,7 @@ class PasteFailed(Exception): class Paster(Protocol): - def paste(self, s: str) -> Tuple[str, Optional[str]]: ... + def paste(self, s: str) -> tuple[str, Optional[str]]: ... class PastePinnwand: @@ -45,7 +45,7 @@ def __init__(self, url: str, expiry: str) -> None: self.url = url self.expiry = expiry - def paste(self, s: str) -> Tuple[str, str]: + def paste(self, s: str) -> tuple[str, str]: """Upload to pastebin via json interface.""" url = urljoin(self.url, "/api/v1/paste") @@ -72,7 +72,7 @@ class PasteHelper: def __init__(self, executable: str) -> None: self.executable = executable - def paste(self, s: str) -> Tuple[str, None]: + def paste(self, s: str) -> tuple[str, None]: """Call out to helper program for pastebin upload.""" try: diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index d91392d2..5bf4a45b 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -9,7 +9,7 @@ class BPythonLinecache(dict): def __init__( self, bpython_history: Optional[ - List[Tuple[int, None, List[str], str]] + list[tuple[int, None, list[str], str]] ] = None, *args, **kwargs, @@ -20,7 +20,7 @@ def __init__( def is_bpython_filename(self, fname: Any) -> bool: return isinstance(fname, str) and fname.startswith(" Tuple[int, None, List[str], str]: + def get_bpython_history(self, key: str) -> tuple[int, None, list[str], str]: """Given a filename provided by remember_bpython_input, returns the associated source string.""" try: diff --git a/bpython/repl.py b/bpython/repl.py index 0374bb6b..de889031 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -43,7 +43,6 @@ Any, Callable, Dict, - Iterable, List, Literal, Optional, @@ -53,6 +52,7 @@ Union, cast, ) +from collections.abc import Iterable from pygments.lexers import Python3Lexer from pygments.token import Token, _TokenType @@ -85,7 +85,7 @@ def __enter__(self) -> None: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: @@ -108,7 +108,7 @@ class Interpreter(code.InteractiveInterpreter): def __init__( self, - locals: Optional[Dict[str, Any]] = None, + locals: Optional[dict[str, Any]] = None, ) -> None: """Constructor. @@ -152,9 +152,7 @@ def runsource( with self.timer: return super().runsource(source, filename, symbol) - def showsyntaxerror( - self, filename: Optional[str] = None, **kwargs - ) -> None: + def showsyntaxerror(self, filename: Optional[str] = None, **kwargs) -> None: """Override the regular handler, the code's copied and pasted from code.py, as per showtraceback, but with the syntaxerror callback called and the text in a pretty colour.""" @@ -221,7 +219,7 @@ def __init__(self) -> None: # word being replaced in the original line of text self.current_word = "" # possible replacements for current_word - self.matches: List[str] = [] + self.matches: list[str] = [] # which word is currently replacing the current word self.index = -1 # cursor position in the original line @@ -265,12 +263,12 @@ def previous(self) -> str: return self.matches[self.index] - def cur_line(self) -> Tuple[int, str]: + def cur_line(self) -> tuple[int, str]: """Returns a cursor offset and line with the current substitution made""" return self.substitute(self.current()) - def substitute(self, match: str) -> Tuple[int, str]: + def substitute(self, match: str) -> tuple[int, str]: """Returns a cursor offset and line with match substituted in""" assert self.completer is not None @@ -286,7 +284,7 @@ def is_cseq(self) -> bool: os.path.commonprefix(self.matches)[len(self.current_word) :] ) - def substitute_cseq(self) -> Tuple[int, str]: + def substitute_cseq(self) -> tuple[int, str]: """Returns a new line by substituting a common sequence in, and update matches""" assert self.completer is not None @@ -307,7 +305,7 @@ def update( self, cursor_offset: int, current_line: str, - matches: List[str], + matches: list[str], completer: autocomplete.BaseCompletionType, ) -> None: """Called to reset the match index and update the word being replaced @@ -428,7 +426,7 @@ def reevaluate(self): @abc.abstractmethod def reprint_line( - self, lineno: int, tokens: List[Tuple[_TokenType, str]] + self, lineno: int, tokens: list[tuple[_TokenType, str]] ) -> None: pass @@ -479,7 +477,7 @@ def __init__(self, interp: Interpreter, config: Config): """ self.config = config self.cut_buffer = "" - self.buffer: List[str] = [] + self.buffer: list[str] = [] self.interp = interp self.interp.syntaxerror_callback = self.clear_current_line self.match = False @@ -488,19 +486,19 @@ def __init__(self, interp: Interpreter, config: Config): ) # all input and output, stored as old style format strings # (\x01, \x02, ...) for cli.py - self.screen_hist: List[str] = [] + self.screen_hist: list[str] = [] # commands executed since beginning of session - self.history: List[str] = [] - self.redo_stack: List[str] = [] + self.history: list[str] = [] + self.redo_stack: list[str] = [] self.evaluating = False self.matches_iter = MatchesIterator() self.funcprops = None self.arg_pos: Union[str, int, None] = None self.current_func = None self.highlighted_paren: Optional[ - Tuple[Any, List[Tuple[_TokenType, str]]] + tuple[Any, list[tuple[_TokenType, str]]] ] = None - self._C: Dict[str, int] = {} + self._C: dict[str, int] = {} self.prev_block_finished: int = 0 self.interact: Interaction = NoInteraction(self.config) # previous pastebin content to prevent duplicate pastes, filled on call @@ -589,7 +587,7 @@ def current_string(self, concatenate=False): def get_object(self, name: str) -> Any: attributes = name.split(".") - obj = eval(attributes.pop(0), cast(Dict[str, Any], self.interp.locals)) + obj = eval(attributes.pop(0), cast(dict[str, Any], self.interp.locals)) while attributes: obj = inspection.getattr_safe(obj, attributes.pop(0)) return obj @@ -597,7 +595,7 @@ def get_object(self, name: str) -> Any: @classmethod def _funcname_and_argnum( cls, line: str - ) -> Tuple[Optional[str], Optional[Union[str, int]]]: + ) -> tuple[Optional[str], Optional[Union[str, int]]]: """Parse out the current function name and arg from a line of code.""" # each element in stack is a _FuncExpr instance # if keyword is not None, we've encountered a keyword and so we're done counting @@ -782,7 +780,7 @@ def complete(self, tab: bool = False) -> Optional[bool]: self.completers, cursor_offset=self.cursor_offset, line=self.current_line, - locals_=cast(Dict[str, Any], self.interp.locals), + locals_=cast(dict[str, Any], self.interp.locals), argspec=self.funcprops, current_block="\n".join(self.buffer + [self.current_line]), complete_magic_methods=self.config.complete_magic_methods, @@ -819,7 +817,7 @@ def complete(self, tab: bool = False) -> Optional[bool]: def format_docstring( self, docstring: str, width: int, height: int - ) -> List[str]: + ) -> list[str]: """Take a string and try to format it into a sane list of strings to be put into the suggestion box.""" @@ -1088,7 +1086,7 @@ def flush(self) -> None: def close(self): """See the flush() method docstring.""" - def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: + def tokenize(self, s, newline=False) -> list[tuple[_TokenType, str]]: """Tokenizes a line of code, returning pygments tokens with side effects/impurities: - reads self.cpos to see what parens should be highlighted @@ -1105,7 +1103,7 @@ def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: cursor = len(source) - self.cpos if self.cpos: cursor += 1 - stack: List[Any] = list() + stack: list[Any] = list() all_tokens = list(Python3Lexer().get_tokens(source)) # Unfortunately, Pygments adds a trailing newline and strings with # no size, so strip them @@ -1114,8 +1112,8 @@ def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: all_tokens[-1] = (all_tokens[-1][0], all_tokens[-1][1].rstrip("\n")) line = pos = 0 parens = dict(zip("{([", "})]")) - line_tokens: List[Tuple[_TokenType, str]] = list() - saved_tokens: List[Tuple[_TokenType, str]] = list() + line_tokens: list[tuple[_TokenType, str]] = list() + saved_tokens: list[tuple[_TokenType, str]] = list() search_for_paren = True for token, value in split_lines(all_tokens): pos += len(value) @@ -1298,7 +1296,7 @@ def token_is_any_of(token): return token_is_any_of -def extract_exit_value(args: Tuple[Any, ...]) -> Any: +def extract_exit_value(args: tuple[Any, ...]) -> Any: """Given the arguments passed to `SystemExit`, return the value that should be passed to `sys.exit`. """ diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 3f334af4..893539ea 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -42,7 +42,7 @@ class EvaluationError(Exception): """Raised if an exception occurred in safe_eval.""" -def safe_eval(expr: str, namespace: Dict[str, Any]) -> Any: +def safe_eval(expr: str, namespace: dict[str, Any]) -> Any: """Not all that safe, just catches some errors""" try: return eval(expr, namespace) @@ -199,7 +199,7 @@ def find_attribute_with_name(node, name): def evaluate_current_expression( - cursor_offset: int, line: str, namespace: Optional[Dict[str, Any]] = None + cursor_offset: int, line: str, namespace: Optional[dict[str, Any]] = None ) -> Any: """ Return evaluated expression to the right of the dot of current attribute. diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py index 5a19c6ab..59102f9e 100644 --- a/bpython/test/test_curtsies_repl.py +++ b/bpython/test/test_curtsies_repl.py @@ -435,7 +435,7 @@ def setUp(self): self.repl = create_repl() def write_startup_file(self, fname, encoding): - with open(fname, mode="wt", encoding=encoding) as f: + with open(fname, mode="w", encoding=encoding) as f: f.write("# coding: ") f.write(encoding) f.write("\n") diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 3f04222d..5089f304 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -162,7 +162,7 @@ def fun(number, lst=[]): """ return lst + [number] - def fun_annotations(number: int, lst: List[int] = []) -> List[int]: + def fun_annotations(number: int, lst: list[int] = []) -> list[int]: """ Return a list of numbers @@ -185,7 +185,7 @@ def fun_annotations(number: int, lst: List[int] = []) -> List[int]: def test_issue_966_class_method(self): class Issue966(Sequence): @classmethod - def cmethod(cls, number: int, lst: List[int] = []): + def cmethod(cls, number: int, lst: list[int] = []): """ Return a list of numbers @@ -222,7 +222,7 @@ def bmethod(cls, number, lst): def test_issue_966_static_method(self): class Issue966(Sequence): @staticmethod - def cmethod(number: int, lst: List[int] = []): + def cmethod(number: int, lst: list[int] = []): """ Return a list of numbers diff --git a/bpython/test/test_line_properties.py b/bpython/test/test_line_properties.py index 967ecbe0..5beb000b 100644 --- a/bpython/test/test_line_properties.py +++ b/bpython/test/test_line_properties.py @@ -27,7 +27,7 @@ def cursor(s): return cursor_offset, line -def decode(s: str) -> Tuple[Tuple[int, str], Optional[LinePart]]: +def decode(s: str) -> tuple[tuple[int, str], Optional[LinePart]]: """'ad' -> ((3, 'abcd'), (1, 3, 'bdc'))""" if not s.count("|") == 1: diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 5cafec94..a32ef90e 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -60,7 +60,7 @@ def getstdout(self) -> str: raise NotImplementedError def reprint_line( - self, lineno: int, tokens: List[Tuple[repl._TokenType, str]] + self, lineno: int, tokens: list[tuple[repl._TokenType, str]] ) -> None: raise NotImplementedError diff --git a/bpython/translations/__init__.py b/bpython/translations/__init__.py index 0cb4c01f..7d82dc7c 100644 --- a/bpython/translations/__init__.py +++ b/bpython/translations/__init__.py @@ -18,7 +18,7 @@ def ngettext(singular, plural, n): def init( - locale_dir: Optional[str] = None, languages: Optional[List[str]] = None + locale_dir: Optional[str] = None, languages: Optional[list[str]] = None ) -> None: try: locale.setlocale(locale.LC_ALL, "") From 3167897c7b1a81596f24cfa142708046df1027a3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 17 Jan 2025 21:48:35 +0100 Subject: [PATCH 260/262] Remove pre-3.9 fallback code --- bpython/lazyre.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/bpython/lazyre.py b/bpython/lazyre.py index d397f05c..a63bb464 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -21,14 +21,10 @@ # THE SOFTWARE. import re -from typing import Optional, Optional from collections.abc import Iterator +from functools import cached_property from re import Pattern, Match - -try: - from functools import cached_property -except ImportError: - from backports.cached_property import cached_property # type: ignore [no-redef] +from typing import Optional, Optional class LazyReCompile: From 12a65e8b57d39123d264e2217cb0551d43a93dc4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 17 Jan 2025 21:52:23 +0100 Subject: [PATCH 261/262] Fix call to preprocess --- bpython/test/test_preprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/test_preprocess.py b/bpython/test/test_preprocess.py index a72a64b6..8e8a3630 100644 --- a/bpython/test/test_preprocess.py +++ b/bpython/test/test_preprocess.py @@ -12,7 +12,7 @@ from bpython.test.fodder import original, processed -preproc = partial(preprocess, compiler=CommandCompiler) +preproc = partial(preprocess, compiler=CommandCompiler()) def get_fodder_source(test_name): From 1a919d3716b87a183006f73d47d117bc3337a522 Mon Sep 17 00:00:00 2001 From: Jochen Kupperschmidt Date: Wed, 29 Jan 2025 00:22:43 +0100 Subject: [PATCH 262/262] Add short project description Should be picked up by PyPI and others. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index f8b7c325..b3cb9a4c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,6 @@ [metadata] name = bpython +description = A fancy curses interface to the Python interactive interpreter long_description = file: README.rst long_description_content_type = text/x-rst license = MIT