From 83ca33e02fb54640ddd0ff6b956059225b05586c Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 17 Nov 2021 19:36:13 -0500 Subject: [PATCH 001/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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/305] 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