From a4eadd750d2d3b103bb78abaac717358f7efc722 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 17 Jan 2025 21:21:40 +0100 Subject: [PATCH 1/8] Start development of 0.26 --- CHANGELOG.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 114b9440..f55fe76f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,22 @@ Changelog ========= +0.26 +---- + +General information: + + +New features: + + +Fixes: + + +Changes to dependencies: + + + 0.25 ---- From 9b344248ac8a180f4c54dc4b2eb6940596c6067b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 17 Jan 2025 21:44:57 +0100 Subject: [PATCH 2/8] Upgrade code to 3.9+ --- bpython/args.py | 8 ++-- bpython/autocomplete.py | 54 ++++++++++++------------- bpython/config.py | 5 ++- bpython/curtsies.py | 13 +++--- bpython/curtsiesfrontend/_internal.py | 2 +- bpython/curtsiesfrontend/events.py | 2 +- bpython/curtsiesfrontend/filewatch.py | 7 ++-- bpython/curtsiesfrontend/interpreter.py | 9 +++-- bpython/curtsiesfrontend/parse.py | 4 +- bpython/curtsiesfrontend/preprocess.py | 2 +- bpython/curtsiesfrontend/repl.py | 27 ++++++------- bpython/filelock.py | 2 +- bpython/formatter.py | 3 +- bpython/history.py | 13 +++--- bpython/importcompletion.py | 15 +++---- bpython/inspection.py | 16 ++++---- bpython/keys.py | 4 +- bpython/lazyre.py | 4 +- bpython/line.py | 2 +- bpython/pager.py | 2 +- bpython/paste.py | 6 +-- bpython/patch_linecache.py | 4 +- bpython/repl.py | 52 ++++++++++++------------ bpython/simpleeval.py | 4 +- bpython/test/test_curtsies_repl.py | 2 +- bpython/test/test_inspection.py | 6 +-- bpython/test/test_line_properties.py | 2 +- bpython/test/test_repl.py | 2 +- bpython/translations/__init__.py | 2 +- 29 files changed, 139 insertions(+), 135 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 55691a2a..1eb59a69 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -73,14 +73,14 @@ def log_version(module: ModuleType, name: str) -> None: logger.info("%s: %s", name, module.__version__ if hasattr(module, "__version__") else "unknown version") # type: ignore -Options = Tuple[str, str, Callable[[argparse._ArgumentGroup], None]] +Options = tuple[str, str, Callable[[argparse._ArgumentGroup], None]] def parse( - args: Optional[List[str]], + args: Optional[list[str]], extras: Optional[Options] = None, ignore_stdin: bool = False, -) -> Tuple[Config, argparse.Namespace, List[str]]: +) -> tuple[Config, argparse.Namespace, list[str]]: """Receive an argument list - if None, use sys.argv - parse all args and take appropriate action. Also receive optional extra argument: this should be a tuple of (title, description, callback) @@ -256,7 +256,7 @@ def callback(group): def exec_code( - interpreter: code.InteractiveInterpreter, args: List[str] + interpreter: code.InteractiveInterpreter, args: list[str] ) -> None: """ Helper to execute code in a given interpreter, e.g. to implement the behavior of python3 [-i] file.py diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 000fbde9..88afbe54 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -40,13 +40,13 @@ from typing import ( Any, Dict, - Iterator, List, Optional, - Sequence, Set, Tuple, ) +from collections.abc import Iterator, Sequence + from . import inspection from . import line as lineparts from .line import LinePart @@ -236,7 +236,7 @@ def __init__( @abc.abstractmethod def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: """Returns a list of possible matches given a line and cursor, or None if this completion type isn't applicable. @@ -268,7 +268,7 @@ def format(self, word: str) -> str: def substitute( self, cursor_offset: int, line: str, match: str - ) -> Tuple[int, str]: + ) -> tuple[int, str]: """Returns a cursor offset and line with match swapped in""" lpart = self.locate(cursor_offset, line) assert lpart @@ -311,7 +311,7 @@ def format(self, word: str) -> str: def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: return_value = None all_matches = set() for completer in self._completers: @@ -336,7 +336,7 @@ def __init__( def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: return self.module_gatherer.complete(cursor_offset, line) def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: @@ -356,7 +356,7 @@ def __init__(self, mode: AutocompleteModes = AutocompleteModes.SIMPLE): def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: cs = lineparts.current_string(cursor_offset, line) if cs is None: return None @@ -389,9 +389,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: r = self.locate(cursor_offset, line) if r is None: return None @@ -421,7 +421,7 @@ def format(self, word: str) -> str: return _after_last_dot(word) def attr_matches( - self, text: str, namespace: Dict[str, Any] + self, text: str, namespace: dict[str, Any] ) -> Iterator[str]: """Taken from rlcompleter.py and bent to my will.""" @@ -460,7 +460,7 @@ def attr_lookup(self, obj: Any, expr: str, attr: str) -> Iterator[str]: if self.method_match(word, n, attr) and word != "__builtins__" ) - def list_attributes(self, obj: Any) -> List[str]: + def list_attributes(self, obj: Any) -> list[str]: # TODO: re-implement dir without AttrCleaner here # # Note: accessing `obj.__dir__` via `getattr_static` is not side-effect free. @@ -474,9 +474,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: if locals_ is None: return None @@ -516,7 +516,7 @@ def matches( current_block: Optional[str] = None, complete_magic_methods: Optional[bool] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: if ( current_block is None or complete_magic_methods is None @@ -541,9 +541,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: """Compute matches when text is a simple name. Return a list of all keywords, built-in functions and names currently defined in self.namespace that match. @@ -583,7 +583,7 @@ def matches( *, funcprops: Optional[inspection.FuncProps] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: if funcprops is None: return None @@ -622,9 +622,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: if locals_ is None: locals_ = __main__.__dict__ @@ -648,7 +648,7 @@ def matches( class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: return None def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: @@ -665,9 +665,9 @@ def matches( line: str, *, current_block: Optional[str] = None, - history: Optional[List[str]] = None, + history: Optional[list[str]] = None, **kwargs: Any, - ) -> Optional[Set[str]]: + ) -> Optional[set[str]]: if ( current_block is None or history is None @@ -725,12 +725,12 @@ def get_completer( cursor_offset: int, line: str, *, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, argspec: Optional[inspection.FuncProps] = None, - history: Optional[List[str]] = None, + history: Optional[list[str]] = None, current_block: Optional[str] = None, complete_magic_methods: Optional[bool] = None, -) -> Tuple[List[str], Optional[BaseCompletionType]]: +) -> tuple[list[str], Optional[BaseCompletionType]]: """Returns a list of matches and an applicable completer If no matches available, returns a tuple of an empty list and None @@ -747,7 +747,7 @@ def get_completer( double underscore methods like __len__ in method signatures """ - def _cmpl_sort(x: str) -> Tuple[bool, str]: + def _cmpl_sort(x: str) -> tuple[bool, str]: """ Function used to sort the matches. """ @@ -784,7 +784,7 @@ def _cmpl_sort(x: str) -> Tuple[bool, str]: def get_default_completer( mode: AutocompleteModes, module_gatherer: ModuleGatherer -) -> Tuple[BaseCompletionType, ...]: +) -> tuple[BaseCompletionType, ...]: return ( ( DictKeyCompletion(mode=mode), diff --git a/bpython/config.py b/bpython/config.py index 5123ec22..27af8740 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -31,7 +31,8 @@ from configparser import ConfigParser from itertools import chain from pathlib import Path -from typing import MutableMapping, Mapping, Any, Dict +from typing import Any, Dict +from collections.abc import MutableMapping, Mapping from xdg import BaseDirectory from .autocomplete import AutocompleteModes @@ -115,7 +116,7 @@ class Config: "right_arrow_suggestion": "K", } - defaults: Dict[str, Dict[str, Any]] = { + defaults: dict[str, dict[str, Any]] = { "general": { "arg_spec": True, "auto_display_list": True, diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 11b96050..547a853e 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -25,14 +25,13 @@ Any, Callable, Dict, - Generator, List, Optional, Protocol, - Sequence, Tuple, Union, ) +from collections.abc import Generator, Sequence logger = logging.getLogger(__name__) @@ -51,7 +50,7 @@ class FullCurtsiesRepl(BaseRepl): def __init__( self, config: Config, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, banner: Optional[str] = None, interp: Optional[Interp] = None, ) -> None: @@ -111,7 +110,7 @@ def interrupting_refresh(self) -> None: def request_undo(self, n: int = 1) -> None: return self._request_undo_callback(n=n) - def get_term_hw(self) -> Tuple[int, int]: + def get_term_hw(self) -> tuple[int, int]: return self.window.get_term_hw() def get_cursor_vertical_diff(self) -> int: @@ -179,8 +178,8 @@ def mainloop( def main( - args: Optional[List[str]] = None, - locals_: Optional[Dict[str, Any]] = None, + args: Optional[list[str]] = None, + locals_: Optional[dict[str, Any]] = None, banner: Optional[str] = None, welcome_message: Optional[str] = None, ) -> Any: @@ -209,7 +208,7 @@ def curtsies_arguments(parser: argparse._ArgumentGroup) -> None: interp = None paste = None - exit_value: Tuple[Any, ...] = () + exit_value: tuple[Any, ...] = () if exec_args: if not options: raise ValueError("don't pass in exec_args without options") diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index 16a598fa..cb7b8105 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -34,7 +34,7 @@ def __enter__(self): def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: diff --git a/bpython/curtsiesfrontend/events.py b/bpython/curtsiesfrontend/events.py index 26f105dc..4f9c13e5 100644 --- a/bpython/curtsiesfrontend/events.py +++ b/bpython/curtsiesfrontend/events.py @@ -1,7 +1,7 @@ """Non-keyboard events used in bpython curtsies REPL""" import time -from typing import Sequence +from collections.abc import Sequence import curtsies.events diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index e70325ab..53ae4784 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -1,6 +1,7 @@ import os from collections import defaultdict -from typing import Callable, Dict, Iterable, Sequence, Set, List +from typing import Callable, Dict, Set, List +from collections.abc import Iterable, Sequence from .. import importcompletion @@ -20,9 +21,9 @@ def __init__( paths: Iterable[str], on_change: Callable[[Sequence[str]], None], ) -> None: - self.dirs: Dict[str, Set[str]] = defaultdict(set) + self.dirs: dict[str, set[str]] = defaultdict(set) self.on_change = on_change - self.modules_to_add_later: List[str] = [] + self.modules_to_add_later: list[str] = [] self.observer = Observer() self.started = False self.activated = False diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 82e28091..6532d968 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -1,6 +1,7 @@ import sys from codeop import CommandCompiler -from typing import Any, Dict, Iterable, Optional, Tuple, Union +from typing import Any, Dict, Optional, Tuple, Union +from collections.abc import Iterable from pygments.token import Generic, Token, Keyword, Name, Comment, String from pygments.token import Error, Literal, Number, Operator, Punctuation @@ -47,7 +48,7 @@ class BPythonFormatter(Formatter): def __init__( self, - color_scheme: Dict[_TokenType, str], + color_scheme: dict[_TokenType, str], **options: Union[str, bool, None], ) -> None: self.f_strings = {k: f"\x01{v}" for k, v in color_scheme.items()} @@ -67,7 +68,7 @@ def format(self, tokensource, outfile): class Interp(ReplInterpreter): def __init__( self, - locals: Optional[Dict[str, Any]] = None, + locals: Optional[dict[str, Any]] = None, ) -> None: """Constructor. @@ -121,7 +122,7 @@ def format(self, tbtext: str, lexer: Any) -> None: def code_finished_will_parse( s: str, compiler: CommandCompiler -) -> Tuple[bool, bool]: +) -> tuple[bool, bool]: """Returns a tuple of whether the buffer could be complete and whether it will parse diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 88a149a6..96e91e55 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -60,7 +60,7 @@ def parse(s: str) -> FmtStr: ) -def fs_from_match(d: Dict[str, Any]) -> FmtStr: +def fs_from_match(d: dict[str, Any]) -> FmtStr: atts = {} color = "default" if d["fg"]: @@ -99,7 +99,7 @@ def fs_from_match(d: Dict[str, Any]) -> FmtStr: ) -def peel_off_string(s: str) -> Tuple[Dict[str, Any], str]: +def peel_off_string(s: str) -> tuple[dict[str, Any], str]: m = peel_off_string_re.match(s) assert m, repr(s) d = m.groupdict() diff --git a/bpython/curtsiesfrontend/preprocess.py b/bpython/curtsiesfrontend/preprocess.py index 5e59dd49..f48a79bf 100644 --- a/bpython/curtsiesfrontend/preprocess.py +++ b/bpython/curtsiesfrontend/preprocess.py @@ -2,7 +2,7 @@ etc)""" from codeop import CommandCompiler -from typing import Match +from re import Match from itertools import tee, islice, chain from ..lazyre import LazyReCompile diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 01b04711..09f73a82 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -14,16 +14,15 @@ from types import FrameType, TracebackType from typing import ( Any, - Iterable, Dict, List, Literal, Optional, - Sequence, Tuple, Type, Union, ) +from collections.abc import Iterable, Sequence import greenlet from curtsies import ( @@ -121,7 +120,7 @@ def __init__( self.current_line = "" self.cursor_offset = 0 self.old_num_lines = 0 - self.readline_results: List[str] = [] + self.readline_results: list[str] = [] if configured_edit_keys is not None: self.rl_char_sequences = configured_edit_keys else: @@ -195,7 +194,7 @@ def readline(self, size: int = -1) -> str: self.readline_results.append(value) return value if size <= -1 else value[:size] - def readlines(self, size: Optional[int] = -1) -> List[str]: + def readlines(self, size: Optional[int] = -1) -> list[str]: if size is None: # the default readlines implementation also accepts None size = -1 @@ -338,10 +337,10 @@ def __init__( self, config: Config, window: CursorAwareWindow, - locals_: Optional[Dict[str, Any]] = None, + locals_: Optional[dict[str, Any]] = None, banner: Optional[str] = None, interp: Optional[Interp] = None, - orig_tcattrs: Optional[List[Any]] = None, + orig_tcattrs: Optional[list[Any]] = None, ): """ locals_ is a mapping of locals to pass into the interpreter @@ -404,7 +403,7 @@ def __init__( # this is every line that's been displayed (input and output) # as with formatting applied. Logical lines that exceeded the terminal width # at the time of output are split across multiple entries in this list. - self.display_lines: List[FmtStr] = [] + self.display_lines: list[FmtStr] = [] # this is every line that's been executed; it gets smaller on rewind self.history = [] @@ -415,11 +414,11 @@ def __init__( # - the first element the line (string, not fmtsr) # - the second element is one of 2 global constants: "input" or "output" # (use LineType.INPUT or LineType.OUTPUT to avoid typing these strings) - self.all_logical_lines: List[Tuple[str, LineType]] = [] + self.all_logical_lines: list[tuple[str, LineType]] = [] # formatted version of lines in the buffer kept around so we can # unhighlight parens using self.reprint_line as called by bpython.Repl - self.display_buffer: List[FmtStr] = [] + self.display_buffer: list[FmtStr] = [] # how many times display has been scrolled down # because there wasn't room to display everything @@ -428,7 +427,7 @@ def __init__( # cursor position relative to start of current_line, 0 is first char self._cursor_offset = 0 - self.orig_tcattrs: Optional[List[Any]] = orig_tcattrs + self.orig_tcattrs: Optional[list[Any]] = orig_tcattrs self.coderunner = CodeRunner(self.interp, self.request_refresh) @@ -460,7 +459,7 @@ def __init__( # some commands act differently based on the prev event # this list doesn't include instances of event.Event, # only keypress-type events (no refresh screen events etc.) - self.last_events: List[Optional[str]] = [None] * 50 + self.last_events: list[Optional[str]] = [None] * 50 # displays prev events in a column on the right hand side self.presentation_mode = False @@ -601,7 +600,7 @@ def __enter__(self): def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: @@ -1561,7 +1560,7 @@ def paint( user_quit=False, try_preserve_history_height=30, min_infobox_height=5, - ) -> Tuple[FSArray, Tuple[int, int]]: + ) -> tuple[FSArray, tuple[int, int]]: """Returns an array of min_height or more rows and width columns, plus cursor position @@ -2237,7 +2236,7 @@ def compress_paste_event(paste_event): def just_simple_events( event_list: Iterable[Union[str, events.Event]] -) -> List[str]: +) -> list[str]: simple_events = [] for e in event_list: if isinstance(e, events.Event): diff --git a/bpython/filelock.py b/bpython/filelock.py index 11f575b6..5ed8769f 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -56,7 +56,7 @@ def __enter__(self) -> "BaseLock": def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: diff --git a/bpython/formatter.py b/bpython/formatter.py index f216f213..8e74ac2c 100644 --- a/bpython/formatter.py +++ b/bpython/formatter.py @@ -28,7 +28,8 @@ # mypy: disallow_untyped_calls=True -from typing import Any, MutableMapping, Iterable, TextIO +from typing import Any, TextIO +from collections.abc import MutableMapping, Iterable from pygments.formatter import Formatter from pygments.token import ( _TokenType, diff --git a/bpython/history.py b/bpython/history.py index 13dbb5b7..386214b4 100644 --- a/bpython/history.py +++ b/bpython/history.py @@ -25,7 +25,8 @@ from pathlib import Path import stat from itertools import islice, chain -from typing import Iterable, Optional, List, TextIO +from typing import Optional, List, TextIO +from collections.abc import Iterable from .translations import _ from .filelock import FileLock @@ -55,7 +56,7 @@ def __init__( def append(self, line: str) -> None: self.append_to(self.entries, line) - def append_to(self, entries: List[str], line: str) -> None: + def append_to(self, entries: list[str], line: str) -> None: line = line.rstrip("\n") if line: if not self.duplicates: @@ -100,7 +101,7 @@ def entry(self) -> str: return self.entries[-self.index] if self.index else self.saved_line @property - def entries_by_index(self) -> List[str]: + def entries_by_index(self) -> list[str]: return list(chain((self.saved_line,), reversed(self.entries))) def find_match_backward( @@ -196,8 +197,8 @@ def load(self, filename: Path, encoding: str) -> None: with FileLock(hfile, filename=str(filename)): self.entries = self.load_from(hfile) - def load_from(self, fd: TextIO) -> List[str]: - entries: List[str] = [] + def load_from(self, fd: TextIO) -> list[str]: + entries: list[str] = [] for line in fd: self.append_to(entries, line) return entries if len(entries) else [""] @@ -213,7 +214,7 @@ def save(self, filename: Path, encoding: str, lines: int = 0) -> None: self.save_to(hfile, self.entries, lines) def save_to( - self, fd: TextIO, entries: Optional[List[str]] = None, lines: int = 0 + self, fd: TextIO, entries: Optional[list[str]] = None, lines: int = 0 ) -> None: if entries is None: entries = self.entries diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 9df140c6..da1b9140 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -27,7 +27,8 @@ import warnings from dataclasses import dataclass from pathlib import Path -from typing import Optional, Set, Generator, Sequence, Iterable, Union +from typing import Optional, Set, Union +from collections.abc import Generator, Sequence, Iterable from .line import ( current_word, @@ -69,9 +70,9 @@ def __init__( directory names. If `paths` is not given, `sys.path` will be used.""" # Cached list of all known modules - self.modules: Set[str] = set() + self.modules: set[str] = set() # Set of (st_dev, st_ino) to compare against so that paths are not repeated - self.paths: Set[_LoadedInode] = set() + self.paths: set[_LoadedInode] = set() # Patterns to skip self.skiplist: Sequence[str] = ( skiplist if skiplist is not None else tuple() @@ -86,7 +87,7 @@ def __init__( Path(p).resolve() if p else Path.cwd() for p in paths ) - def module_matches(self, cw: str, prefix: str = "") -> Set[str]: + def module_matches(self, cw: str, prefix: str = "") -> set[str]: """Modules names to replace cw with""" full = f"{prefix}.{cw}" if prefix else cw @@ -102,7 +103,7 @@ def module_matches(self, cw: str, prefix: str = "") -> Set[str]: def attr_matches( self, cw: str, prefix: str = "", only_modules: bool = False - ) -> Set[str]: + ) -> set[str]: """Attributes to replace name with""" full = f"{prefix}.{cw}" if prefix else cw module_name, _, name_after_dot = full.rpartition(".") @@ -126,11 +127,11 @@ def attr_matches( return matches - def module_attr_matches(self, name: str) -> Set[str]: + def module_attr_matches(self, name: str) -> set[str]: """Only attributes which are modules to replace name with""" return self.attr_matches(name, only_modules=True) - def complete(self, cursor_offset: int, line: str) -> Optional[Set[str]]: + def complete(self, cursor_offset: int, line: str) -> Optional[set[str]]: """Construct a full list of possibly completions for imports.""" tokens = line.split() if "from" not in tokens and "import" not in tokens: diff --git a/bpython/inspection.py b/bpython/inspection.py index e97a272b..fb1124eb 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -62,13 +62,13 @@ def __repr__(self) -> str: @dataclass class ArgSpec: - args: List[str] + args: list[str] varargs: Optional[str] varkwargs: Optional[str] - defaults: Optional[List[_Repr]] - kwonly: List[str] - kwonly_defaults: Optional[Dict[str, _Repr]] - annotations: Optional[Dict[str, Any]] + defaults: Optional[list[_Repr]] + kwonly: list[str] + kwonly_defaults: Optional[dict[str, _Repr]] + annotations: Optional[dict[str, Any]] @dataclass @@ -118,7 +118,7 @@ def __enter__(self) -> None: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: @@ -134,10 +134,10 @@ def __exit__( return False -def parsekeywordpairs(signature: str) -> Dict[str, str]: +def parsekeywordpairs(signature: str) -> dict[str, str]: preamble = True stack = [] - substack: List[str] = [] + substack: list[str] = [] parendepth = 0 annotation = False for token, value in Python3Lexer().get_tokens(signature): diff --git a/bpython/keys.py b/bpython/keys.py index fe27dbcc..1068a4f2 100644 --- a/bpython/keys.py +++ b/bpython/keys.py @@ -28,7 +28,7 @@ class KeyMap(Generic[T]): def __init__(self, default: T) -> None: - self.map: Dict[str, T] = {} + self.map: dict[str, T] = {} self.default = default def __getitem__(self, key: str) -> T: @@ -49,7 +49,7 @@ def __setitem__(self, key: str, value: T) -> None: self.map[key] = value -cli_key_dispatch: KeyMap[Tuple[str, ...]] = KeyMap(tuple()) +cli_key_dispatch: KeyMap[tuple[str, ...]] = KeyMap(tuple()) urwid_key_dispatch = KeyMap("") # fill dispatch with letters diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 8d166b74..d397f05c 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -21,7 +21,9 @@ # THE SOFTWARE. import re -from typing import Optional, Pattern, Match, Optional, Iterator +from typing import Optional, Optional +from collections.abc import Iterator +from re import Pattern, Match try: from functools import cached_property diff --git a/bpython/line.py b/bpython/line.py index cbc3bf37..363419fe 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -291,7 +291,7 @@ def current_expression_attribute( def cursor_on_closing_char_pair( cursor_offset: int, line: str, ch: Optional[str] = None -) -> Tuple[bool, bool]: +) -> tuple[bool, bool]: """Checks if cursor sits on closing character of a pair and whether its pair character is directly behind it """ diff --git a/bpython/pager.py b/bpython/pager.py index 65a3b223..2fa4846e 100644 --- a/bpython/pager.py +++ b/bpython/pager.py @@ -33,7 +33,7 @@ from typing import List -def get_pager_command(default: str = "less -rf") -> List[str]: +def get_pager_command(default: str = "less -rf") -> list[str]: command = shlex.split(os.environ.get("PAGER", default)) return command diff --git a/bpython/paste.py b/bpython/paste.py index a81c0c6c..e846aba3 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -37,7 +37,7 @@ class PasteFailed(Exception): class Paster(Protocol): - def paste(self, s: str) -> Tuple[str, Optional[str]]: ... + def paste(self, s: str) -> tuple[str, Optional[str]]: ... class PastePinnwand: @@ -45,7 +45,7 @@ def __init__(self, url: str, expiry: str) -> None: self.url = url self.expiry = expiry - def paste(self, s: str) -> Tuple[str, str]: + def paste(self, s: str) -> tuple[str, str]: """Upload to pastebin via json interface.""" url = urljoin(self.url, "/api/v1/paste") @@ -72,7 +72,7 @@ class PasteHelper: def __init__(self, executable: str) -> None: self.executable = executable - def paste(self, s: str) -> Tuple[str, None]: + def paste(self, s: str) -> tuple[str, None]: """Call out to helper program for pastebin upload.""" try: diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index d91392d2..5bf4a45b 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -9,7 +9,7 @@ class BPythonLinecache(dict): def __init__( self, bpython_history: Optional[ - List[Tuple[int, None, List[str], str]] + list[tuple[int, None, list[str], str]] ] = None, *args, **kwargs, @@ -20,7 +20,7 @@ def __init__( def is_bpython_filename(self, fname: Any) -> bool: return isinstance(fname, str) and fname.startswith(" Tuple[int, None, List[str], str]: + def get_bpython_history(self, key: str) -> tuple[int, None, list[str], str]: """Given a filename provided by remember_bpython_input, returns the associated source string.""" try: diff --git a/bpython/repl.py b/bpython/repl.py index 0374bb6b..de889031 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -43,7 +43,6 @@ Any, Callable, Dict, - Iterable, List, Literal, Optional, @@ -53,6 +52,7 @@ Union, cast, ) +from collections.abc import Iterable from pygments.lexers import Python3Lexer from pygments.token import Token, _TokenType @@ -85,7 +85,7 @@ def __enter__(self) -> None: def __exit__( self, - exc_type: Optional[Type[BaseException]], + exc_type: Optional[type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: @@ -108,7 +108,7 @@ class Interpreter(code.InteractiveInterpreter): def __init__( self, - locals: Optional[Dict[str, Any]] = None, + locals: Optional[dict[str, Any]] = None, ) -> None: """Constructor. @@ -152,9 +152,7 @@ def runsource( with self.timer: return super().runsource(source, filename, symbol) - def showsyntaxerror( - self, filename: Optional[str] = None, **kwargs - ) -> None: + def showsyntaxerror(self, filename: Optional[str] = None, **kwargs) -> None: """Override the regular handler, the code's copied and pasted from code.py, as per showtraceback, but with the syntaxerror callback called and the text in a pretty colour.""" @@ -221,7 +219,7 @@ def __init__(self) -> None: # word being replaced in the original line of text self.current_word = "" # possible replacements for current_word - self.matches: List[str] = [] + self.matches: list[str] = [] # which word is currently replacing the current word self.index = -1 # cursor position in the original line @@ -265,12 +263,12 @@ def previous(self) -> str: return self.matches[self.index] - def cur_line(self) -> Tuple[int, str]: + def cur_line(self) -> tuple[int, str]: """Returns a cursor offset and line with the current substitution made""" return self.substitute(self.current()) - def substitute(self, match: str) -> Tuple[int, str]: + def substitute(self, match: str) -> tuple[int, str]: """Returns a cursor offset and line with match substituted in""" assert self.completer is not None @@ -286,7 +284,7 @@ def is_cseq(self) -> bool: os.path.commonprefix(self.matches)[len(self.current_word) :] ) - def substitute_cseq(self) -> Tuple[int, str]: + def substitute_cseq(self) -> tuple[int, str]: """Returns a new line by substituting a common sequence in, and update matches""" assert self.completer is not None @@ -307,7 +305,7 @@ def update( self, cursor_offset: int, current_line: str, - matches: List[str], + matches: list[str], completer: autocomplete.BaseCompletionType, ) -> None: """Called to reset the match index and update the word being replaced @@ -428,7 +426,7 @@ def reevaluate(self): @abc.abstractmethod def reprint_line( - self, lineno: int, tokens: List[Tuple[_TokenType, str]] + self, lineno: int, tokens: list[tuple[_TokenType, str]] ) -> None: pass @@ -479,7 +477,7 @@ def __init__(self, interp: Interpreter, config: Config): """ self.config = config self.cut_buffer = "" - self.buffer: List[str] = [] + self.buffer: list[str] = [] self.interp = interp self.interp.syntaxerror_callback = self.clear_current_line self.match = False @@ -488,19 +486,19 @@ def __init__(self, interp: Interpreter, config: Config): ) # all input and output, stored as old style format strings # (\x01, \x02, ...) for cli.py - self.screen_hist: List[str] = [] + self.screen_hist: list[str] = [] # commands executed since beginning of session - self.history: List[str] = [] - self.redo_stack: List[str] = [] + self.history: list[str] = [] + self.redo_stack: list[str] = [] self.evaluating = False self.matches_iter = MatchesIterator() self.funcprops = None self.arg_pos: Union[str, int, None] = None self.current_func = None self.highlighted_paren: Optional[ - Tuple[Any, List[Tuple[_TokenType, str]]] + tuple[Any, list[tuple[_TokenType, str]]] ] = None - self._C: Dict[str, int] = {} + self._C: dict[str, int] = {} self.prev_block_finished: int = 0 self.interact: Interaction = NoInteraction(self.config) # previous pastebin content to prevent duplicate pastes, filled on call @@ -589,7 +587,7 @@ def current_string(self, concatenate=False): def get_object(self, name: str) -> Any: attributes = name.split(".") - obj = eval(attributes.pop(0), cast(Dict[str, Any], self.interp.locals)) + obj = eval(attributes.pop(0), cast(dict[str, Any], self.interp.locals)) while attributes: obj = inspection.getattr_safe(obj, attributes.pop(0)) return obj @@ -597,7 +595,7 @@ def get_object(self, name: str) -> Any: @classmethod def _funcname_and_argnum( cls, line: str - ) -> Tuple[Optional[str], Optional[Union[str, int]]]: + ) -> tuple[Optional[str], Optional[Union[str, int]]]: """Parse out the current function name and arg from a line of code.""" # each element in stack is a _FuncExpr instance # if keyword is not None, we've encountered a keyword and so we're done counting @@ -782,7 +780,7 @@ def complete(self, tab: bool = False) -> Optional[bool]: self.completers, cursor_offset=self.cursor_offset, line=self.current_line, - locals_=cast(Dict[str, Any], self.interp.locals), + locals_=cast(dict[str, Any], self.interp.locals), argspec=self.funcprops, current_block="\n".join(self.buffer + [self.current_line]), complete_magic_methods=self.config.complete_magic_methods, @@ -819,7 +817,7 @@ def complete(self, tab: bool = False) -> Optional[bool]: def format_docstring( self, docstring: str, width: int, height: int - ) -> List[str]: + ) -> list[str]: """Take a string and try to format it into a sane list of strings to be put into the suggestion box.""" @@ -1088,7 +1086,7 @@ def flush(self) -> None: def close(self): """See the flush() method docstring.""" - def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: + def tokenize(self, s, newline=False) -> list[tuple[_TokenType, str]]: """Tokenizes a line of code, returning pygments tokens with side effects/impurities: - reads self.cpos to see what parens should be highlighted @@ -1105,7 +1103,7 @@ def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: cursor = len(source) - self.cpos if self.cpos: cursor += 1 - stack: List[Any] = list() + stack: list[Any] = list() all_tokens = list(Python3Lexer().get_tokens(source)) # Unfortunately, Pygments adds a trailing newline and strings with # no size, so strip them @@ -1114,8 +1112,8 @@ def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: all_tokens[-1] = (all_tokens[-1][0], all_tokens[-1][1].rstrip("\n")) line = pos = 0 parens = dict(zip("{([", "})]")) - line_tokens: List[Tuple[_TokenType, str]] = list() - saved_tokens: List[Tuple[_TokenType, str]] = list() + line_tokens: list[tuple[_TokenType, str]] = list() + saved_tokens: list[tuple[_TokenType, str]] = list() search_for_paren = True for token, value in split_lines(all_tokens): pos += len(value) @@ -1298,7 +1296,7 @@ def token_is_any_of(token): return token_is_any_of -def extract_exit_value(args: Tuple[Any, ...]) -> Any: +def extract_exit_value(args: tuple[Any, ...]) -> Any: """Given the arguments passed to `SystemExit`, return the value that should be passed to `sys.exit`. """ diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 3f334af4..893539ea 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -42,7 +42,7 @@ class EvaluationError(Exception): """Raised if an exception occurred in safe_eval.""" -def safe_eval(expr: str, namespace: Dict[str, Any]) -> Any: +def safe_eval(expr: str, namespace: dict[str, Any]) -> Any: """Not all that safe, just catches some errors""" try: return eval(expr, namespace) @@ -199,7 +199,7 @@ def find_attribute_with_name(node, name): def evaluate_current_expression( - cursor_offset: int, line: str, namespace: Optional[Dict[str, Any]] = None + cursor_offset: int, line: str, namespace: Optional[dict[str, Any]] = None ) -> Any: """ Return evaluated expression to the right of the dot of current attribute. diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py index 5a19c6ab..59102f9e 100644 --- a/bpython/test/test_curtsies_repl.py +++ b/bpython/test/test_curtsies_repl.py @@ -435,7 +435,7 @@ def setUp(self): self.repl = create_repl() def write_startup_file(self, fname, encoding): - with open(fname, mode="wt", encoding=encoding) as f: + with open(fname, mode="w", encoding=encoding) as f: f.write("# coding: ") f.write(encoding) f.write("\n") diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 3f04222d..5089f304 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -162,7 +162,7 @@ def fun(number, lst=[]): """ return lst + [number] - def fun_annotations(number: int, lst: List[int] = []) -> List[int]: + def fun_annotations(number: int, lst: list[int] = []) -> list[int]: """ Return a list of numbers @@ -185,7 +185,7 @@ def fun_annotations(number: int, lst: List[int] = []) -> List[int]: def test_issue_966_class_method(self): class Issue966(Sequence): @classmethod - def cmethod(cls, number: int, lst: List[int] = []): + def cmethod(cls, number: int, lst: list[int] = []): """ Return a list of numbers @@ -222,7 +222,7 @@ def bmethod(cls, number, lst): def test_issue_966_static_method(self): class Issue966(Sequence): @staticmethod - def cmethod(number: int, lst: List[int] = []): + def cmethod(number: int, lst: list[int] = []): """ Return a list of numbers diff --git a/bpython/test/test_line_properties.py b/bpython/test/test_line_properties.py index 967ecbe0..5beb000b 100644 --- a/bpython/test/test_line_properties.py +++ b/bpython/test/test_line_properties.py @@ -27,7 +27,7 @@ def cursor(s): return cursor_offset, line -def decode(s: str) -> Tuple[Tuple[int, str], Optional[LinePart]]: +def decode(s: str) -> tuple[tuple[int, str], Optional[LinePart]]: """'ad' -> ((3, 'abcd'), (1, 3, 'bdc'))""" if not s.count("|") == 1: diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 5cafec94..a32ef90e 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -60,7 +60,7 @@ def getstdout(self) -> str: raise NotImplementedError def reprint_line( - self, lineno: int, tokens: List[Tuple[repl._TokenType, str]] + self, lineno: int, tokens: list[tuple[repl._TokenType, str]] ) -> None: raise NotImplementedError diff --git a/bpython/translations/__init__.py b/bpython/translations/__init__.py index 0cb4c01f..7d82dc7c 100644 --- a/bpython/translations/__init__.py +++ b/bpython/translations/__init__.py @@ -18,7 +18,7 @@ def ngettext(singular, plural, n): def init( - locale_dir: Optional[str] = None, languages: Optional[List[str]] = None + locale_dir: Optional[str] = None, languages: Optional[list[str]] = None ) -> None: try: locale.setlocale(locale.LC_ALL, "") From 3167897c7b1a81596f24cfa142708046df1027a3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 17 Jan 2025 21:48:35 +0100 Subject: [PATCH 3/8] Remove pre-3.9 fallback code --- bpython/lazyre.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/bpython/lazyre.py b/bpython/lazyre.py index d397f05c..a63bb464 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -21,14 +21,10 @@ # THE SOFTWARE. import re -from typing import Optional, Optional from collections.abc import Iterator +from functools import cached_property from re import Pattern, Match - -try: - from functools import cached_property -except ImportError: - from backports.cached_property import cached_property # type: ignore [no-redef] +from typing import Optional, Optional class LazyReCompile: From 12a65e8b57d39123d264e2217cb0551d43a93dc4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 17 Jan 2025 21:52:23 +0100 Subject: [PATCH 4/8] Fix call to preprocess --- bpython/test/test_preprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/test_preprocess.py b/bpython/test/test_preprocess.py index a72a64b6..8e8a3630 100644 --- a/bpython/test/test_preprocess.py +++ b/bpython/test/test_preprocess.py @@ -12,7 +12,7 @@ from bpython.test.fodder import original, processed -preproc = partial(preprocess, compiler=CommandCompiler) +preproc = partial(preprocess, compiler=CommandCompiler()) def get_fodder_source(test_name): From 1a919d3716b87a183006f73d47d117bc3337a522 Mon Sep 17 00:00:00 2001 From: Jochen Kupperschmidt Date: Wed, 29 Jan 2025 00:22:43 +0100 Subject: [PATCH 5/8] Add short project description Should be picked up by PyPI and others. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index f8b7c325..b3cb9a4c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,6 @@ [metadata] name = bpython +description = A fancy curses interface to the Python interactive interpreter long_description = file: README.rst long_description_content_type = text/x-rst license = MIT From b99562370fd0ff4f5b9c2101a39263f7075252d6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 2 Jun 2025 16:39:55 +0200 Subject: [PATCH 6/8] CI: no longer test with Python 3.9 --- .github/workflows/build.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a6e9aef0..cb6d64ad 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,12 +15,11 @@ jobs: fail-fast: false matrix: python-version: - - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" - - "pypy-3.9" + - "pypy-3.10" steps: - uses: actions/checkout@v4 with: From 982bd7e20e584220f1c21805a21c345bbf893e12 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 2 Jun 2025 16:40:59 +0200 Subject: [PATCH 7/8] Upgrade to Python 3.10+ --- bpython/args.py | 7 ++- bpython/autocomplete.py | 78 ++++++++++++------------- bpython/curtsies.py | 29 +++++---- bpython/curtsiesfrontend/_internal.py | 6 +- bpython/curtsiesfrontend/filewatch.py | 4 +- bpython/curtsiesfrontend/interpreter.py | 6 +- bpython/curtsiesfrontend/parse.py | 3 +- bpython/curtsiesfrontend/repl.py | 40 ++++++------- bpython/filelock.py | 8 +-- bpython/history.py | 8 +-- bpython/importcompletion.py | 8 +-- bpython/inspection.py | 22 +++---- bpython/lazyre.py | 4 +- bpython/line.py | 34 +++++------ bpython/paste.py | 2 +- bpython/patch_linecache.py | 4 +- bpython/repl.py | 47 ++++++++------- bpython/simpleeval.py | 2 +- bpython/test/test_inspection.py | 4 +- bpython/test/test_line_properties.py | 4 +- bpython/translations/__init__.py | 2 +- bpython/urwid.py | 2 +- pyproject.toml | 2 +- 23 files changed, 157 insertions(+), 169 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 1eb59a69..35fd3e7b 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -36,7 +36,8 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional, Callable +from typing import Tuple, List, Optional +from collections.abc import Callable from types import ModuleType from . import __version__, __copyright__ @@ -77,8 +78,8 @@ def log_version(module: ModuleType, name: str) -> None: def parse( - args: Optional[list[str]], - extras: Optional[Options] = None, + args: list[str] | None, + extras: Options | None = None, ignore_stdin: bool = False, ) -> tuple[Config, argparse.Namespace, list[str]]: """Receive an argument list - if None, use sys.argv - parse all args and diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 88afbe54..4fb62f72 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -236,7 +236,7 @@ def __init__( @abc.abstractmethod def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[set[str]]: + ) -> set[str] | None: """Returns a list of possible matches given a line and cursor, or None if this completion type isn't applicable. @@ -255,7 +255,7 @@ def matches( raise NotImplementedError @abc.abstractmethod - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: """Returns a Linepart namedtuple instance or None given cursor and line A Linepart namedtuple contains a start, stop, and word. None is @@ -299,7 +299,7 @@ def __init__( super().__init__(True, mode) - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: for completer in self._completers: return_value = completer.locate(cursor_offset, line) if return_value is not None: @@ -311,7 +311,7 @@ def format(self, word: str) -> str: def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[set[str]]: + ) -> set[str] | None: return_value = None all_matches = set() for completer in self._completers: @@ -336,10 +336,10 @@ def __init__( def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[set[str]]: + ) -> set[str] | None: return self.module_gatherer.complete(cursor_offset, line) - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_word(cursor_offset, line) def format(self, word: str) -> str: @@ -356,7 +356,7 @@ def __init__(self, mode: AutocompleteModes = AutocompleteModes.SIMPLE): def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[set[str]]: + ) -> set[str] | None: cs = lineparts.current_string(cursor_offset, line) if cs is None: return None @@ -371,7 +371,7 @@ def matches( matches.add(filename) return matches - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_string(cursor_offset, line) def format(self, filename: str) -> str: @@ -389,9 +389,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[dict[str, Any]] = None, + locals_: dict[str, Any] | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: r = self.locate(cursor_offset, line) if r is None: return None @@ -414,7 +414,7 @@ def matches( if _few_enough_underscores(r.word.split(".")[-1], m.split(".")[-1]) } - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_dotted_attribute(cursor_offset, line) def format(self, word: str) -> str: @@ -474,9 +474,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[dict[str, Any]] = None, + locals_: dict[str, Any] | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: if locals_ is None: return None @@ -500,7 +500,7 @@ def matches( else: return None - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_dict_key(cursor_offset, line) def format(self, match: str) -> str: @@ -513,10 +513,10 @@ def matches( cursor_offset: int, line: str, *, - current_block: Optional[str] = None, - complete_magic_methods: Optional[bool] = None, + current_block: str | None = None, + complete_magic_methods: bool | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: if ( current_block is None or complete_magic_methods is None @@ -531,7 +531,7 @@ def matches( return None return {name for name in MAGIC_METHODS if name.startswith(r.word)} - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_method_definition_name(cursor_offset, line) @@ -541,9 +541,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[dict[str, Any]] = None, + locals_: dict[str, Any] | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: """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. @@ -571,7 +571,7 @@ def matches( matches.add(_callable_postfix(val, word)) return matches if matches else None - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_single_word(cursor_offset, line) @@ -581,9 +581,9 @@ def matches( cursor_offset: int, line: str, *, - funcprops: Optional[inspection.FuncProps] = None, + funcprops: inspection.FuncProps | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: if funcprops is None: return None @@ -603,7 +603,7 @@ def matches( ) return matches if matches else None - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: r = lineparts.current_word(cursor_offset, line) if r and r.word[-1] == "(": # if the word ends with a (, it's the parent word with an empty @@ -614,7 +614,7 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: class ExpressionAttributeCompletion(AttrCompletion): # could replace attr completion as a more general case with some work - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_expression_attribute(cursor_offset, line) def matches( @@ -622,9 +622,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[dict[str, Any]] = None, + locals_: dict[str, Any] | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: if locals_ is None: locals_ = __main__.__dict__ @@ -648,26 +648,26 @@ def matches( class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[set[str]]: + ) -> set[str] | None: return None - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return None else: class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] - _orig_start: Optional[int] + _orig_start: int | None def matches( self, cursor_offset: int, line: str, *, - current_block: Optional[str] = None, - history: Optional[list[str]] = None, + current_block: str | None = None, + history: list[str] | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: if ( current_block is None or history is None @@ -725,12 +725,12 @@ def get_completer( cursor_offset: int, line: str, *, - locals_: Optional[dict[str, Any]] = None, - 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]]: + locals_: dict[str, Any] | None = None, + argspec: inspection.FuncProps | None = None, + history: list[str] | None = None, + current_block: str | None = None, + complete_magic_methods: bool | None = None, +) -> tuple[list[str], BaseCompletionType | None]: """Returns a list of matches and an applicable completer If no matches available, returns a tuple of an empty list and None diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 547a853e..b57e47a9 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -23,7 +23,6 @@ from typing import ( Any, - Callable, Dict, List, Optional, @@ -31,28 +30,28 @@ Tuple, Union, ) -from collections.abc import Generator, Sequence +from collections.abc import Callable, Generator, Sequence logger = logging.getLogger(__name__) class SupportsEventGeneration(Protocol): def send( - self, timeout: Optional[float] - ) -> Union[str, curtsies.events.Event, None]: ... + self, timeout: float | None + ) -> str | curtsies.events.Event | None: ... def __iter__(self) -> "SupportsEventGeneration": ... - def __next__(self) -> Union[str, curtsies.events.Event, None]: ... + def __next__(self) -> str | curtsies.events.Event | None: ... class FullCurtsiesRepl(BaseRepl): def __init__( self, config: Config, - locals_: Optional[dict[str, Any]] = None, - banner: Optional[str] = None, - interp: Optional[Interp] = None, + locals_: dict[str, Any] | None = None, + banner: str | None = None, + interp: Interp | None = None, ) -> None: self.input_generator = curtsies.input.Input( keynames="curtsies", sigint_event=True, paste_threshold=None @@ -129,7 +128,7 @@ def after_suspend(self) -> None: self.interrupting_refresh() def process_event_and_paint( - self, e: Union[str, curtsies.events.Event, None] + self, e: str | curtsies.events.Event | None ) -> None: """If None is passed in, just paint the screen""" try: @@ -151,7 +150,7 @@ def process_event_and_paint( def mainloop( self, interactive: bool = True, - paste: Optional[curtsies.events.PasteEvent] = None, + paste: curtsies.events.PasteEvent | None = None, ) -> None: if interactive: # Add custom help command @@ -178,10 +177,10 @@ def mainloop( def main( - args: Optional[list[str]] = None, - locals_: Optional[dict[str, Any]] = None, - banner: Optional[str] = None, - welcome_message: Optional[str] = None, + args: list[str] | None = None, + locals_: dict[str, Any] | None = None, + banner: str | None = None, + welcome_message: str | None = None, ) -> Any: """ banner is displayed directly after the version information. @@ -249,7 +248,7 @@ def curtsies_arguments(parser: argparse._ArgumentGroup) -> None: def _combined_events( event_provider: SupportsEventGeneration, paste_threshold: int -) -> Generator[Union[str, curtsies.events.Event, None], Optional[float], None]: +) -> Generator[str | curtsies.events.Event | None, float | None, None]: """Combines consecutive keypress events into paste events.""" timeout = yield "nonsense_event" # so send can be used immediately queue: collections.deque = collections.deque() diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index cb7b8105..8c070b34 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -34,9 +34,9 @@ def __enter__(self): def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> Literal[False]: pydoc.pager = self._orig_pager return False diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 53ae4784..2822db6d 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -1,7 +1,7 @@ import os from collections import defaultdict -from typing import Callable, Dict, Set, List -from collections.abc import Iterable, Sequence +from typing import Dict, Set, List +from collections.abc import Callable, Iterable, Sequence from .. import importcompletion diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 6532d968..280c56ed 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -49,7 +49,7 @@ class BPythonFormatter(Formatter): def __init__( self, color_scheme: dict[_TokenType, str], - **options: Union[str, bool, None], + **options: str | bool | None, ) -> None: self.f_strings = {k: f"\x01{v}" for k, v in color_scheme.items()} # FIXME: mypy currently fails to handle this properly @@ -68,7 +68,7 @@ def format(self, tokensource, outfile): class Interp(ReplInterpreter): def __init__( self, - locals: Optional[dict[str, Any]] = None, + locals: dict[str, Any] | None = None, ) -> None: """Constructor. @@ -79,7 +79,7 @@ def __init__( # typically changed after being instantiated # but used when interpreter used corresponding REPL - def write(err_line: Union[str, FmtStr]) -> None: + def write(err_line: str | FmtStr) -> None: """Default stderr handler for tracebacks Accepts FmtStrs so interpreters can output them""" diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 96e91e55..28b32e64 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -1,6 +1,7 @@ import re from functools import partial -from typing import Any, Callable, Dict, Tuple +from typing import Any, Dict, Tuple +from collections.abc import Callable from curtsies.formatstring import fmtstr, FmtStr from curtsies.termformatconstants import ( diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 09f73a82..2a304312 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -112,7 +112,7 @@ def __init__( self, coderunner: CodeRunner, repl: "BaseRepl", - configured_edit_keys: Optional[AbstractEdits] = None, + configured_edit_keys: AbstractEdits | None = None, ): self.coderunner = coderunner self.repl = repl @@ -126,7 +126,7 @@ def __init__( else: self.rl_char_sequences = edit_keys - def process_event(self, e: Union[events.Event, str]) -> None: + def process_event(self, e: events.Event | str) -> None: assert self.has_focus logger.debug("fake input processing event %r", e) @@ -194,7 +194,7 @@ def readline(self, size: int = -1) -> str: self.readline_results.append(value) return value if size <= -1 else value[:size] - def readlines(self, size: Optional[int] = -1) -> list[str]: + def readlines(self, size: int | None = -1) -> list[str]: if size is None: # the default readlines implementation also accepts None size = -1 @@ -337,10 +337,10 @@ def __init__( self, config: Config, window: CursorAwareWindow, - locals_: Optional[dict[str, Any]] = None, - banner: Optional[str] = None, - interp: Optional[Interp] = None, - orig_tcattrs: Optional[list[Any]] = None, + locals_: dict[str, Any] | None = None, + banner: str | None = None, + interp: Interp | None = None, + orig_tcattrs: list[Any] | None = None, ): """ locals_ is a mapping of locals to pass into the interpreter @@ -398,7 +398,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: str | FmtStr = "" # this is every line that's been displayed (input and output) # as with formatting applied. Logical lines that exceeded the terminal width @@ -427,7 +427,7 @@ def __init__( # cursor position relative to start of current_line, 0 is first char self._cursor_offset = 0 - self.orig_tcattrs: Optional[list[Any]] = orig_tcattrs + self.orig_tcattrs: list[Any] | None = orig_tcattrs self.coderunner = CodeRunner(self.interp, self.request_refresh) @@ -459,7 +459,7 @@ def __init__( # some commands act differently based on the prev event # this list doesn't include instances of event.Event, # only keypress-type events (no refresh screen events etc.) - self.last_events: list[Optional[str]] = [None] * 50 + self.last_events: list[str | None] = [None] * 50 # displays prev events in a column on the right hand side self.presentation_mode = False @@ -600,9 +600,9 @@ def __enter__(self): def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> Literal[False]: sys.stdin = self.orig_stdin sys.stdout = self.orig_stdout @@ -616,7 +616,7 @@ def __exit__( sys.meta_path = self.orig_meta_path return False - def sigwinch_handler(self, signum: int, frame: Optional[FrameType]) -> None: + def sigwinch_handler(self, signum: int, frame: FrameType | None) -> None: old_rows, old_columns = self.height, self.width self.height, self.width = self.get_term_hw() cursor_dy = self.get_cursor_vertical_diff() @@ -632,7 +632,7 @@ def sigwinch_handler(self, signum: int, frame: Optional[FrameType]) -> None: self.scroll_offset, ) - def sigtstp_handler(self, signum: int, frame: Optional[FrameType]) -> None: + def sigtstp_handler(self, signum: int, frame: FrameType | None) -> None: self.scroll_offset = len(self.lines_for_display) self.__exit__(None, None, None) self.on_suspend() @@ -647,7 +647,7 @@ def clean_up_current_line_for_exit(self): self.unhighlight_paren() # Event handling - def process_event(self, e: Union[events.Event, str]) -> Optional[bool]: + def process_event(self, e: events.Event | str) -> bool | None: """Returns True if shutting down, otherwise returns None. Mostly mutates state of Repl object""" @@ -660,7 +660,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: events.Event) -> Optional[bool]: + def process_control_event(self, e: events.Event) -> bool | None: if isinstance(e, bpythonevents.ScheduledRefreshRequestEvent): # This is a scheduled refresh - it's really just a refresh (so nop) pass @@ -2234,9 +2234,7 @@ def compress_paste_event(paste_event): return None -def just_simple_events( - event_list: Iterable[Union[str, events.Event]] -) -> list[str]: +def just_simple_events(event_list: Iterable[str | events.Event]) -> list[str]: simple_events = [] for e in event_list: if isinstance(e, events.Event): @@ -2253,7 +2251,7 @@ def just_simple_events( return simple_events -def is_simple_event(e: Union[str, events.Event]) -> bool: +def is_simple_event(e: str | events.Event) -> bool: if isinstance(e, events.Event): return False return ( diff --git a/bpython/filelock.py b/bpython/filelock.py index 5ed8769f..b8eb11ff 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -56,9 +56,9 @@ def __enter__(self) -> "BaseLock": def __exit__( self, - exc_type: Optional[type[BaseException]], - exc: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, ) -> Literal[False]: if self.locked: self.release() @@ -122,7 +122,7 @@ def release(self) -> None: def FileLock( - fileobj: IO, mode: int = 0, filename: Optional[str] = None + fileobj: IO, mode: int = 0, filename: str | None = None ) -> BaseLock: if has_fcntl: return UnixFileLock(fileobj, mode) diff --git a/bpython/history.py b/bpython/history.py index 386214b4..b58309b5 100644 --- a/bpython/history.py +++ b/bpython/history.py @@ -37,7 +37,7 @@ class History: def __init__( self, - entries: Optional[Iterable[str]] = None, + entries: Iterable[str] | None = None, duplicates: bool = True, hist_size: int = 100, ) -> None: @@ -78,7 +78,7 @@ def back( self, start: bool = True, search: bool = False, - target: Optional[str] = None, + target: str | None = None, include_current: bool = False, ) -> str: """Move one step back in the history.""" @@ -128,7 +128,7 @@ def forward( self, start: bool = True, search: bool = False, - target: Optional[str] = None, + target: str | None = None, include_current: bool = False, ) -> str: """Move one step forward in the history.""" @@ -214,7 +214,7 @@ def save(self, filename: Path, encoding: str, lines: int = 0) -> None: self.save_to(hfile, self.entries, lines) def save_to( - self, fd: TextIO, entries: Optional[list[str]] = None, lines: int = 0 + self, fd: TextIO, entries: list[str] | None = None, lines: int = 0 ) -> None: if entries is None: entries = self.entries diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index da1b9140..570996d4 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -63,8 +63,8 @@ class _LoadedInode: class ModuleGatherer: def __init__( self, - paths: Optional[Iterable[Union[str, Path]]] = None, - skiplist: Optional[Sequence[str]] = None, + paths: Iterable[str | Path] | None = None, + skiplist: Sequence[str] | None = None, ) -> None: """Initialize module gatherer with all modules in `paths`, which should be a list of directory names. If `paths` is not given, `sys.path` will be used.""" @@ -131,7 +131,7 @@ def module_attr_matches(self, name: str) -> set[str]: """Only attributes which are modules to replace name with""" return self.attr_matches(name, only_modules=True) - def complete(self, cursor_offset: int, line: str) -> Optional[set[str]]: + def complete(self, cursor_offset: int, line: str) -> set[str] | None: """Construct a full list of possibly completions for imports.""" tokens = line.split() if "from" not in tokens and "import" not in tokens: @@ -167,7 +167,7 @@ def complete(self, cursor_offset: int, line: str) -> Optional[set[str]]: else: return None - def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: + def find_modules(self, path: Path) -> Generator[str | None, None, None]: """Find all modules (and packages) for a given directory.""" if not path.is_dir(): # Perhaps a zip file diff --git a/bpython/inspection.py b/bpython/inspection.py index fb1124eb..63f0e2d3 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -28,7 +28,6 @@ from dataclasses import dataclass from typing import ( Any, - Callable, Optional, Type, Dict, @@ -36,6 +35,7 @@ ContextManager, Literal, ) +from collections.abc import Callable from types import MemberDescriptorType, TracebackType from pygments.token import Token @@ -63,12 +63,12 @@ def __repr__(self) -> str: @dataclass class ArgSpec: args: list[str] - varargs: Optional[str] - varkwargs: Optional[str] - defaults: Optional[list[_Repr]] + varargs: str | None + varkwargs: str | None + defaults: list[_Repr] | None kwonly: list[str] - kwonly_defaults: Optional[dict[str, _Repr]] - annotations: Optional[dict[str, Any]] + kwonly_defaults: dict[str, _Repr] | None + annotations: dict[str, Any] | None @dataclass @@ -118,9 +118,9 @@ def __enter__(self) -> None: def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> Literal[False]: """Restore an object's magic methods.""" type_ = type(self._obj) @@ -224,7 +224,7 @@ def _fix_default_values(f: Callable, argspec: ArgSpec) -> ArgSpec: ) -def _getpydocspec(f: Callable) -> Optional[ArgSpec]: +def _getpydocspec(f: Callable) -> ArgSpec | None: try: argspec = pydoc.getdoc(f) except NameError: @@ -267,7 +267,7 @@ def _getpydocspec(f: Callable) -> Optional[ArgSpec]: ) -def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: +def getfuncprops(func: str, f: Callable) -> FuncProps | None: # 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: diff --git a/bpython/lazyre.py b/bpython/lazyre.py index a63bb464..1d903616 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -44,10 +44,10 @@ def compiled(self) -> Pattern[str]: def finditer(self, *args, **kwargs) -> Iterator[Match[str]]: return self.compiled.finditer(*args, **kwargs) - def search(self, *args, **kwargs) -> Optional[Match[str]]: + def search(self, *args, **kwargs) -> Match[str] | None: return self.compiled.search(*args, **kwargs) - def match(self, *args, **kwargs) -> Optional[Match[str]]: + def match(self, *args, **kwargs) -> Match[str] | None: return self.compiled.match(*args, **kwargs) def sub(self, *args, **kwargs) -> str: diff --git a/bpython/line.py b/bpython/line.py index 363419fe..e64b20d9 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -24,7 +24,7 @@ class LinePart: CHARACTER_PAIR_MAP = {"(": ")", "{": "}", "[": "]", "'": "'", '"': '"'} -def current_word(cursor_offset: int, line: str) -> Optional[LinePart]: +def current_word(cursor_offset: int, line: str) -> LinePart | None: """the object.attribute.attribute just before or under the cursor""" start = cursor_offset end = cursor_offset @@ -76,7 +76,7 @@ def current_word(cursor_offset: int, line: str) -> Optional[LinePart]: ) -def current_dict_key(cursor_offset: int, line: str) -> Optional[LinePart]: +def current_dict_key(cursor_offset: int, line: str) -> LinePart | None: """If in dictionary completion, return the current key""" for m in _current_dict_key_re.finditer(line): if m.start(1) <= cursor_offset <= m.end(1): @@ -96,7 +96,7 @@ def current_dict_key(cursor_offset: int, line: str) -> Optional[LinePart]: ) -def current_dict(cursor_offset: int, line: str) -> Optional[LinePart]: +def current_dict(cursor_offset: int, line: str) -> LinePart | None: """If in dictionary completion, return the dict that should be used""" for m in _current_dict_re.finditer(line): if m.start(2) <= cursor_offset <= m.end(2): @@ -110,7 +110,7 @@ def current_dict(cursor_offset: int, line: str) -> Optional[LinePart]: ) -def current_string(cursor_offset: int, line: str) -> Optional[LinePart]: +def current_string(cursor_offset: int, line: str) -> LinePart | None: """If inside a string of nonzero length, return the string (excluding quotes) @@ -126,7 +126,7 @@ def current_string(cursor_offset: int, line: str) -> Optional[LinePart]: _current_object_re = LazyReCompile(r"([\w_][\w0-9_]*)[.]") -def current_object(cursor_offset: int, line: str) -> Optional[LinePart]: +def current_object(cursor_offset: int, line: str) -> LinePart | None: """If in attribute completion, the object on which attribute should be looked up.""" match = current_word(cursor_offset, line) @@ -145,9 +145,7 @@ def current_object(cursor_offset: int, line: str) -> Optional[LinePart]: _current_object_attribute_re = LazyReCompile(r"([\w_][\w0-9_]*)[.]?") -def current_object_attribute( - cursor_offset: int, line: str -) -> Optional[LinePart]: +def current_object_attribute(cursor_offset: int, line: str) -> LinePart | None: """If in attribute completion, the attribute being completed""" # TODO replace with more general current_expression_attribute match = current_word(cursor_offset, line) @@ -168,9 +166,7 @@ def current_object_attribute( ) -def current_from_import_from( - cursor_offset: int, line: str -) -> Optional[LinePart]: +def current_from_import_from(cursor_offset: int, line: str) -> LinePart | None: """If in from import completion, the word after from returns None if cursor not in or just after one of the two interesting @@ -194,7 +190,7 @@ def current_from_import_from( def current_from_import_import( cursor_offset: int, line: str -) -> Optional[LinePart]: +) -> LinePart | None: """If in from import completion, the word after import being completed returns None if cursor not in or just after one of these words @@ -221,7 +217,7 @@ def current_from_import_import( _current_import_re_3 = LazyReCompile(r"[,][ ]*([\w0-9_.]*)") -def current_import(cursor_offset: int, line: str) -> Optional[LinePart]: +def current_import(cursor_offset: int, line: str) -> LinePart | None: # TODO allow for multiple as's baseline = _current_import_re_1.search(line) if baseline is None: @@ -244,7 +240,7 @@ def current_import(cursor_offset: int, line: str) -> Optional[LinePart]: def current_method_definition_name( cursor_offset: int, line: str -) -> Optional[LinePart]: +) -> LinePart | None: """The name of a method being defined""" for m in _current_method_definition_name_re.finditer(line): if m.start(1) <= cursor_offset <= m.end(1): @@ -255,7 +251,7 @@ def current_method_definition_name( _current_single_word_re = LazyReCompile(r"(? Optional[LinePart]: +def current_single_word(cursor_offset: int, line: str) -> LinePart | None: """the un-dotted word just before or under the cursor""" for m in _current_single_word_re.finditer(line): if m.start(1) <= cursor_offset <= m.end(1): @@ -263,9 +259,7 @@ def current_single_word(cursor_offset: int, line: str) -> Optional[LinePart]: return None -def current_dotted_attribute( - cursor_offset: int, line: str -) -> Optional[LinePart]: +def current_dotted_attribute(cursor_offset: int, line: str) -> LinePart | None: """The dotted attribute-object pair before the cursor""" match = current_word(cursor_offset, line) if match is not None and "." in match.word[1:]: @@ -280,7 +274,7 @@ def current_dotted_attribute( def current_expression_attribute( cursor_offset: int, line: str -) -> Optional[LinePart]: +) -> LinePart | None: """If after a dot, the attribute being completed""" # TODO replace with more general current_expression_attribute for m in _current_expression_attribute_re.finditer(line): @@ -290,7 +284,7 @@ def current_expression_attribute( def cursor_on_closing_char_pair( - cursor_offset: int, line: str, ch: Optional[str] = None + cursor_offset: int, line: str, ch: str | None = None ) -> tuple[bool, bool]: """Checks if cursor sits on closing character of a pair and whether its pair character is directly behind it diff --git a/bpython/paste.py b/bpython/paste.py index e846aba3..8ca6f2df 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -37,7 +37,7 @@ class PasteFailed(Exception): class Paster(Protocol): - def paste(self, s: str) -> tuple[str, Optional[str]]: ... + def paste(self, s: str) -> tuple[str, str | None]: ... class PastePinnwand: diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index 5bf4a45b..68787e70 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -8,9 +8,7 @@ class BPythonLinecache(dict): def __init__( self, - bpython_history: Optional[ - list[tuple[int, None, list[str], str]] - ] = None, + bpython_history: None | (list[tuple[int, None, list[str], str]]) = None, *args, **kwargs, ) -> None: diff --git a/bpython/repl.py b/bpython/repl.py index de889031..50da2d46 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -41,7 +41,6 @@ from types import ModuleType, TracebackType from typing import ( Any, - Callable, Dict, List, Literal, @@ -52,7 +51,7 @@ Union, cast, ) -from collections.abc import Iterable +from collections.abc import Callable, Iterable from pygments.lexers import Python3Lexer from pygments.token import Token, _TokenType @@ -85,9 +84,9 @@ def __enter__(self) -> None: def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> Literal[False]: self.last_command = time.monotonic() - self.start self.running_time += self.last_command @@ -108,7 +107,7 @@ class Interpreter(code.InteractiveInterpreter): def __init__( self, - locals: Optional[dict[str, Any]] = None, + locals: dict[str, Any] | None = None, ) -> None: """Constructor. @@ -125,7 +124,7 @@ def __init__( traceback. """ - self.syntaxerror_callback: Optional[Callable] = None + self.syntaxerror_callback: Callable | None = None if locals is None: # instead of messing with sys.modules, we should modify sys.modules @@ -139,7 +138,7 @@ def __init__( def runsource( self, source: str, - filename: Optional[str] = None, + filename: str | None = None, symbol: str = "single", ) -> bool: """Execute Python code. @@ -152,7 +151,7 @@ def runsource( with self.timer: return super().runsource(source, filename, symbol) - def showsyntaxerror(self, filename: Optional[str] = None, **kwargs) -> None: + def showsyntaxerror(self, filename: str | None = None, **kwargs) -> None: """Override the regular handler, the code's copied and pasted from code.py, as per showtraceback, but with the syntaxerror callback called and the text in a pretty colour.""" @@ -227,9 +226,9 @@ def __init__(self) -> None: # original line (before match replacements) 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 + self.completer: autocomplete.BaseCompletionType | None = None + self.start: int | None = None + self.end: int | None = None def __nonzero__(self) -> bool: """MatchesIterator is False when word hasn't been replaced yet""" @@ -352,7 +351,7 @@ def notify( pass @abc.abstractmethod - def file_prompt(self, s: str) -> Optional[str]: + def file_prompt(self, s: str) -> str | None: pass @@ -368,7 +367,7 @@ def notify( ) -> None: pass - def file_prompt(self, s: str) -> Optional[str]: + def file_prompt(self, s: str) -> str | None: return None @@ -384,7 +383,7 @@ class _FuncExpr: function_expr: str arg_number: int opening: str - keyword: Optional[str] = None + keyword: str | None = None class Repl(metaclass=abc.ABCMeta): @@ -493,11 +492,11 @@ def __init__(self, interp: Interpreter, config: Config): self.evaluating = False self.matches_iter = MatchesIterator() self.funcprops = None - self.arg_pos: Union[str, int, None] = None + self.arg_pos: str | int | None = None self.current_func = None - self.highlighted_paren: Optional[ + self.highlighted_paren: None | ( tuple[Any, list[tuple[_TokenType, str]]] - ] = None + ) = None self._C: dict[str, int] = {} self.prev_block_finished: int = 0 self.interact: Interaction = NoInteraction(self.config) @@ -509,7 +508,7 @@ def __init__(self, interp: Interpreter, config: Config): # Necessary to fix mercurial.ui.ui expecting sys.stderr to have this # attribute self.closed = False - self.paster: Union[PasteHelper, PastePinnwand] + self.paster: PasteHelper | PastePinnwand if self.config.hist_file.exists(): try: @@ -595,7 +594,7 @@ def get_object(self, name: str) -> Any: @classmethod def _funcname_and_argnum( cls, line: str - ) -> tuple[Optional[str], Optional[Union[str, int]]]: + ) -> tuple[str | None, str | int | None]: """Parse out the current function name and arg from a line of code.""" # each element in stack is a _FuncExpr instance # if keyword is not None, we've encountered a keyword and so we're done counting @@ -715,7 +714,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: Optional[Callable] = self.current_func + obj: Callable | None = self.current_func try: if obj is None: line = self.current_line @@ -761,7 +760,7 @@ def set_docstring(self) -> None: # 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: bool = False) -> Optional[bool]: + def complete(self, tab: bool = False) -> bool | None: """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. @@ -937,7 +936,7 @@ def copy2clipboard(self) -> None: else: self.interact.notify(_("Copied content to clipboard.")) - def pastebin(self, s=None) -> Optional[str]: + def pastebin(self, s=None) -> str | None: """Upload to a pastebin and display the URL in the status bar.""" if s is None: @@ -951,7 +950,7 @@ def pastebin(self, s=None) -> Optional[str]: else: return self.do_pastebin(s) - def do_pastebin(self, s) -> Optional[str]: + def do_pastebin(self, s) -> str | None: """Actually perform the upload.""" paste_url: str if s == self.prev_pastebin_content: diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 893539ea..1e26ded4 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -199,7 +199,7 @@ def find_attribute_with_name(node, name): def evaluate_current_expression( - cursor_offset: int, line: str, namespace: Optional[dict[str, Any]] = None + cursor_offset: int, line: str, namespace: dict[str, Any] | None = None ) -> Any: """ Return evaluated expression to the right of the dot of current attribute. diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 5089f304..c83ca012 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -103,9 +103,7 @@ def test_get_source_latin1(self): self.assertEqual(inspect.getsource(encoding_latin1.foo), foo_non_ascii) def test_get_source_file(self): - path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "fodder" - ) + path = os.path.join(os.path.dirname(__file__), "fodder") encoding = inspection.get_encoding_file( os.path.join(path, "encoding_ascii.py") diff --git a/bpython/test/test_line_properties.py b/bpython/test/test_line_properties.py index 5beb000b..01797827 100644 --- a/bpython/test/test_line_properties.py +++ b/bpython/test/test_line_properties.py @@ -27,7 +27,7 @@ def cursor(s): return cursor_offset, line -def decode(s: str) -> tuple[tuple[int, str], Optional[LinePart]]: +def decode(s: str) -> tuple[tuple[int, str], LinePart | None]: """'ad' -> ((3, 'abcd'), (1, 3, 'bdc'))""" if not s.count("|") == 1: @@ -52,7 +52,7 @@ def line_with_cursor(cursor_offset: int, line: str) -> str: return line[:cursor_offset] + "|" + line[cursor_offset:] -def encode(cursor_offset: int, line: str, result: Optional[LinePart]) -> str: +def encode(cursor_offset: int, line: str, result: LinePart | None) -> str: """encode(3, 'abdcd', (1, 3, 'bdc')) -> ad' Written for prettier assert error messages diff --git a/bpython/translations/__init__.py b/bpython/translations/__init__.py index 7d82dc7c..069f3465 100644 --- a/bpython/translations/__init__.py +++ b/bpython/translations/__init__.py @@ -18,7 +18,7 @@ def ngettext(singular, plural, n): def init( - locale_dir: Optional[str] = None, languages: Optional[list[str]] = None + locale_dir: str | None = None, languages: list[str] | None = None ) -> None: try: locale.setlocale(locale.LC_ALL, "") diff --git a/bpython/urwid.py b/bpython/urwid.py index 3c075d93..d94fc2e7 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -563,7 +563,7 @@ def _prompt_result(self, text): self.callback = None callback(text) - def file_prompt(self, s: str) -> Optional[str]: + def file_prompt(self, s: str) -> str | None: raise NotImplementedError diff --git a/pyproject.toml b/pyproject.toml index ca4e0450..0a891d27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 80 -target_version = ["py39"] +target_version = ["py310"] include = '\.pyi?$' exclude = ''' /( From 42408f904587c19320f4805eb96ad43bedfc74f0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 2 Jun 2025 17:15:38 +0200 Subject: [PATCH 8/8] Require urwid < 3.0 --- .github/workflows/build.yaml | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index cb6d64ad..e7852728 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -32,7 +32,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" + pip install "urwid < 3.0" twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" pip install pytest pytest-cov numpy - name: Build with Python ${{ matrix.python-version }} run: | diff --git a/setup.cfg b/setup.cfg index b3cb9a4c..7d61ee1c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ install_requires = [options.extras_require] clipboard = pyperclip jedi = jedi >= 0.16 -urwid = urwid +urwid = urwid < 3.0 watch = watchdog [options.entry_points]