From e5e4aef17529c1ef033652a0812b3d1bc69f964f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 30 Aug 2022 22:03:59 +0200 Subject: [PATCH 001/113] 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 002/113] 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 003/113] 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 004/113] 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 005/113] 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 006/113] 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 007/113] 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 008/113] 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 009/113] 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 010/113] 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 011/113] 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 012/113] 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 013/113] 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 014/113] 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 015/113] 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 016/113] 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 017/113] 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 018/113] 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 019/113] 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 020/113] 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 021/113] 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 022/113] 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 023/113] 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 024/113] 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 025/113] 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 026/113] 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 027/113] Bump codecov/codecov-action from 1 to 3 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1 to 3. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v1...v3) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 052969b8..9cdcc1e9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -46,7 +46,7 @@ jobs: run: | pytest --cov=bpython --cov-report=xml -v - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 env: PYTHON_VERSION: ${{ matrix.python-version }} with: From 742085633868c72e5a42ce452f3deea57de442fc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 16:19:04 +0200 Subject: [PATCH 028/113] Handle changed output in Python 3.11 --- bpython/test/test_interpreter.py | 42 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 65b60a92..f53ec252 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -112,19 +112,35 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - expected = ( - "Traceback (most recent call last):\n File " - + green('""') - + ", line " - + bold(magenta("1")) - + ", in " - + cyan("") - + "\n gfunc()\n" - + bold(red("NameError")) - + ": " - + cyan(global_not_found) - + "\n" - ) + if (3, 11) <= sys.version_info[:2]: + expected = ( + "Traceback (most recent call last):\n File " + + green('""') + + ", line " + + bold(magenta("1")) + + ", in " + + cyan("") + + "\n gfunc()" + + "\n ^^^^^\n" + + bold(red("NameError")) + + ": " + + cyan(global_not_found) + + "\n" + ) + else: + expected = ( + "Traceback (most recent call last):\n File " + + green('""') + + ", line " + + bold(magenta("1")) + + ", in " + + cyan("") + + "\n gfunc()\n" + + bold(red("NameError")) + + ": " + + cyan(global_not_found) + + "\n" + ) self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) self.assertEqual(plain("").join(a), expected) From b04ad8835c6eb777393536efa4e89045a3883852 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 17:48:57 +0200 Subject: [PATCH 029/113] mypy: ignore import errors from twisted --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 35806eca..7955ee39 100644 --- a/setup.cfg +++ b/setup.cfg @@ -77,3 +77,6 @@ ignore_missing_imports = True [mypy-urwid] ignore_missing_imports = True + +[mypy-twisted.*] +ignore_missing_imports = True From 3b8927852e84fd6581c83ef974920856d5f3fb47 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 19:21:58 +0200 Subject: [PATCH 030/113] Refactor Also, as we have many small _Repr instances, make the value a slot. --- bpython/inspection.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 78bbc578..8ae5c20a 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -36,6 +36,22 @@ from .lazyre import LazyReCompile +class _Repr: + """ + Helper for `ArgSpec`: Returns the given value in `__repr__()`. + """ + + __slots__ = ("value",) + + def __init__(self, value: str) -> None: + self.value = value + + def __repr__(self) -> str: + return self.value + + __str__ = __repr__ + + @dataclass class ArgSpec: args: List[str] @@ -110,20 +126,6 @@ def __exit__( return False -class _Repr: - """ - Helper for `fixlongargs()`: Returns the given value in `__repr__()`. - """ - - def __init__(self, value: str) -> None: - self.value = value - - def __repr__(self) -> str: - return self.value - - __str__ = __repr__ - - def parsekeywordpairs(signature: str) -> Dict[str, str]: preamble = True stack = [] @@ -293,12 +295,15 @@ def _get_argspec_from_signature(f: Callable) -> ArgSpec: """ args = [] - varargs = varkwargs = None + varargs = None + varkwargs = None defaults = [] kwonly = [] kwonly_defaults = {} annotations = {} + # We use signature here instead of getfullargspec as the latter also returns + # self and cls (for class methods). signature = inspect.signature(f) for parameter in signature.parameters.values(): if parameter.annotation is not parameter.empty: From 917382530f0de38e775312cacfab5ebe498502a3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 19:56:27 +0200 Subject: [PATCH 031/113] Fix inspection of built-in functions with >= 3.11 The built-in functions no longer have their signature in the docstring, but now inspect.signature can produce results. But as we have no source for built-in functions, we cannot replace the default values. Hence, we handle built-in functions in an extra step. This commit also changes the handling of default values slightly. They are now always put into a _Repr. --- bpython/inspection.py | 60 ++++++++++++++++++++++----------- bpython/test/test_inspection.py | 36 ++++++++++---------- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 8ae5c20a..e1e4ed8d 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -57,9 +57,9 @@ class ArgSpec: args: List[str] varargs: Optional[str] varkwargs: Optional[str] - defaults: Optional[List[Any]] + defaults: Optional[List[_Repr]] kwonly: List[str] - kwonly_defaults: Optional[Dict[str, Any]] + kwonly_defaults: Optional[Dict[str, _Repr]] annotations: Optional[Dict[str, Any]] @@ -169,31 +169,51 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: return {item[0]: "".join(item[2:]) for item in stack if len(item) >= 3} -def _fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: +def _fix_default_values(f: Callable, argspec: ArgSpec) -> ArgSpec: """Functions taking default arguments that are references to other objects - whose str() is too big will cause breakage, so we swap out the object - itself with the name it was referenced with in the source by parsing the - source itself !""" - if argspec.defaults is None: + will cause breakage, so we swap out the object itself with the name it was + referenced with in the source by parsing the source itself!""" + + if argspec.defaults is None and argspec.kwonly_defaults is None: # No keyword args, no need to do anything return argspec - values = list(argspec.defaults) - if not values: - return argspec - keys = argspec.args[-len(values) :] + try: - src = inspect.getsourcelines(f) + src, _ = inspect.getsourcelines(f) except (OSError, IndexError): # IndexError is raised in inspect.findsource(), can happen in # some situations. See issue #94. return argspec - kwparsed = parsekeywordpairs("".join(src[0])) + except TypeError: + # No source code is available (for Python >= 3.11) + # + # If the function is a builtin, we replace the default values. + # Otherwise, let's bail out. + if not inspect.isbuiltin(f): + raise + + if argspec.defaults is not None: + argspec.defaults = [_Repr(str(value)) for value in argspec.defaults] + if argspec.kwonly_defaults is not None: + argspec.kwonly_defaults = { + key: _Repr(str(value)) + for key, value in argspec.kwonly_defaults.items() + } + return argspec - for i, (key, value) in enumerate(zip(keys, values)): - if len(repr(value)) != len(kwparsed[key]): + kwparsed = parsekeywordpairs("".join(src)) + + if argspec.defaults is not None: + values = list(argspec.defaults) + keys = argspec.args[-len(values) :] + for i, key in enumerate(keys): values[i] = _Repr(kwparsed[key]) - argspec.defaults = values + argspec.defaults = values + if argspec.kwonly_defaults is not None: + for key in argspec.kwonly_defaults.keys(): + argspec.kwonly_defaults[key] = _Repr(kwparsed[key]) + return argspec @@ -234,11 +254,11 @@ def _getpydocspec(f: Callable) -> Optional[ArgSpec]: if varargs is not None: kwonly_args.append(arg) if default: - kwonly_defaults[arg] = default + kwonly_defaults[arg] = _Repr(default) else: args.append(arg) if default: - defaults.append(default) + defaults.append(_Repr(default)) return ArgSpec( args, varargs, varkwargs, defaults, kwonly_args, kwonly_defaults, None @@ -267,7 +287,9 @@ def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: return None try: argspec = _get_argspec_from_signature(f) - fprops = FuncProps(func, _fixlongargs(f, argspec), is_bound_method) + fprops = FuncProps( + func, _fix_default_values(f, argspec), is_bound_method + ) except (TypeError, KeyError, ValueError): argspec_pydoc = _getpydocspec(f) if argspec_pydoc is None: diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 43915f3e..3f04222d 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -11,6 +11,7 @@ from bpython.test.fodder import encoding_utf8 pypy = "PyPy" in sys.version +_is_py311 = sys.version_info[:2] >= (3, 11) try: import numpy @@ -53,23 +54,17 @@ def test_parsekeywordpairs(self): def fails(spam=["-a", "-b"]): pass - default_arg_repr = "['-a', '-b']" - self.assertEqual( - str(["-a", "-b"]), - default_arg_repr, - "This test is broken (repr does not match), fix me.", - ) - argspec = inspection.getfuncprops("fails", fails) + self.assertIsNotNone(argspec) defaults = argspec.argspec.defaults - self.assertEqual(str(defaults[0]), default_arg_repr) + self.assertEqual(str(defaults[0]), '["-a", "-b"]') def test_pasekeywordpairs_string(self): def spam(eggs="foo, bar"): pass defaults = inspection.getfuncprops("spam", spam).argspec.defaults - self.assertEqual(repr(defaults[0]), "'foo, bar'") + self.assertEqual(repr(defaults[0]), '"foo, bar"') def test_parsekeywordpairs_multiple_keywords(self): def spam(eggs=23, foobar="yay"): @@ -77,14 +72,14 @@ def spam(eggs=23, foobar="yay"): defaults = inspection.getfuncprops("spam", spam).argspec.defaults self.assertEqual(repr(defaults[0]), "23") - self.assertEqual(repr(defaults[1]), "'yay'") + self.assertEqual(repr(defaults[1]), '"yay"') def test_pasekeywordpairs_annotation(self): def spam(eggs: str = "foo, bar"): pass defaults = inspection.getfuncprops("spam", spam).argspec.defaults - self.assertEqual(repr(defaults[0]), "'foo, bar'") + self.assertEqual(repr(defaults[0]), '"foo, bar"') def test_get_encoding_ascii(self): self.assertEqual(inspection.get_encoding(encoding_ascii), "ascii") @@ -134,8 +129,15 @@ def test_getfuncprops_print(self): self.assertIn("file", props.argspec.kwonly) self.assertIn("flush", props.argspec.kwonly) self.assertIn("sep", props.argspec.kwonly) - self.assertEqual(props.argspec.kwonly_defaults["file"], "sys.stdout") - self.assertEqual(props.argspec.kwonly_defaults["flush"], "False") + if _is_py311: + self.assertEqual( + repr(props.argspec.kwonly_defaults["file"]), "None" + ) + else: + self.assertEqual( + repr(props.argspec.kwonly_defaults["file"]), "sys.stdout" + ) + self.assertEqual(repr(props.argspec.kwonly_defaults["flush"]), "False") @unittest.skipUnless( numpy is not None and numpy.__version__ >= "1.18", @@ -173,12 +175,12 @@ def fun_annotations(number: int, lst: List[int] = []) -> List[int]: props = inspection.getfuncprops("fun", fun) self.assertEqual(props.func, "fun") self.assertEqual(props.argspec.args, ["number", "lst"]) - self.assertEqual(props.argspec.defaults[0], []) + self.assertEqual(repr(props.argspec.defaults[0]), "[]") props = inspection.getfuncprops("fun_annotations", fun_annotations) self.assertEqual(props.func, "fun_annotations") self.assertEqual(props.argspec.args, ["number", "lst"]) - self.assertEqual(props.argspec.defaults[0], []) + self.assertEqual(repr(props.argspec.defaults[0]), "[]") def test_issue_966_class_method(self): class Issue966(Sequence): @@ -215,7 +217,7 @@ def bmethod(cls, number, lst): ) self.assertEqual(props.func, "cmethod") self.assertEqual(props.argspec.args, ["number", "lst"]) - self.assertEqual(props.argspec.defaults[0], []) + self.assertEqual(repr(props.argspec.defaults[0]), "[]") def test_issue_966_static_method(self): class Issue966(Sequence): @@ -252,7 +254,7 @@ def bmethod(number, lst): ) self.assertEqual(props.func, "cmethod") self.assertEqual(props.argspec.args, ["number", "lst"]) - self.assertEqual(props.argspec.defaults[0], []) + self.assertEqual(repr(props.argspec.defaults[0]), "[]") class A: From 6b390db6ffb14a7d57a9d5458a814e41a708d123 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 22:20:38 +0200 Subject: [PATCH 032/113] Unbreak tests after 9173825 --- bpython/inspection.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index e1e4ed8d..db0a397c 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -185,13 +185,7 @@ def _fix_default_values(f: Callable, argspec: ArgSpec) -> ArgSpec: # some situations. See issue #94. return argspec except TypeError: - # No source code is available (for Python >= 3.11) - # - # If the function is a builtin, we replace the default values. - # Otherwise, let's bail out. - if not inspect.isbuiltin(f): - raise - + # No source code is available, so replace the default values with what we have. if argspec.defaults is not None: argspec.defaults = [_Repr(str(value)) for value in argspec.defaults] if argspec.kwonly_defaults is not None: From e20bf119949c445fbb1215f402d67057343476e3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 29 Oct 2022 17:54:09 +0200 Subject: [PATCH 033/113] Skip test with special unicode chars on broken Python 3.11 versions --- bpython/test/test_curtsies_painting.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 2804643c..19561efb 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -13,7 +13,7 @@ ) from curtsies.fmtfuncs import cyan, bold, green, yellow, on_magenta, red from curtsies.window import CursorAwareWindow -from unittest import mock +from unittest import mock, skipIf from bpython.curtsiesfrontend.events import RefreshRequestEvent from bpython import config, inspection @@ -311,6 +311,10 @@ def test_cursor_position_with_padding_char(self): cursor_pos = self.repl.paint()[1] self.assertEqual(cursor_pos, (1, 4)) + @skipIf( + sys.version_info[:2] >= (3, 11) and sys.version_info[:3] < (3, 11, 1), + "https://github.com/python/cpython/issues/98744", + ) def test_display_of_padding_chars(self): self.repl.width = 11 [self.repl.add_normal_character(c) for c in "width"] From abf13c57a2b54ef4c68e5bec16bcb44a11a9513e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 29 Oct 2022 18:05:02 +0200 Subject: [PATCH 034/113] Remove unused build dependency --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c7ef64f2..4dfbad88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,6 @@ [build-system] requires = [ "setuptools >= 43", - "wheel", ] [tool.black] From 7f546d932552256ad46057c81f9c8642d7a7b619 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 29 Oct 2022 18:05:11 +0200 Subject: [PATCH 035/113] Set build-backend --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4dfbad88..6526fb9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,7 @@ requires = [ "setuptools >= 43", ] +build-backend = "setuptools.build_meta" [tool.black] line-length = 80 From 33c62dac03cddd6d87547cdcbca4762b1f4c5684 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 29 Oct 2022 18:18:02 +0200 Subject: [PATCH 036/113] Update changelog --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b9a4c8b5..1d902634 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,10 +12,13 @@ New features: Fixes: +* Improve inspection of builtin functions. Changes to dependencies: +Support for Python 3.11 has been added. + 0.23 ---- From 7712e282d067b041ead41f0b514ac3e27584edc6 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Thu, 3 Nov 2022 23:51:22 -0700 Subject: [PATCH 037/113] Fix watchdog auto-reloading --- bpython/curtsies.py | 2 +- bpython/curtsiesfrontend/repl.py | 36 +++++++------------------------- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 6d289aaa..985f7085 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -106,7 +106,7 @@ def _schedule_refresh(self, when: float) -> None: return self._schedule_refresh_callback(when) def _request_reload(self, files_modified: Sequence[str]) -> None: - return self._request_reload_callback(files_modified) + return self._request_reload_callback(files_modified=files_modified) def interrupting_refresh(self) -> None: return self._interrupting_refresh_callback() diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index b950be68..cf7d9c37 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -269,54 +269,39 @@ def __init__(self, watcher, loader): def __getattr__(self, name): if name == "create_module" and hasattr(self.loader, name): return self._create_module - if name == "load_module" and hasattr(self.loader, name): - return self._load_module return getattr(self.loader, name) def _create_module(self, spec): - spec = self.loader.create_module(spec) + module_object = self.loader.create_module(spec) if ( getattr(spec, "origin", None) is not None and spec.origin != "builtin" ): self.watcher.track_module(spec.origin) - return spec - - def _load_module(self, name): - module = self.loader.load_module(name) - if hasattr(module, "__file__"): - self.watcher.track_module(module.__file__) - return module + return module_object class ImportFinder: - """Wrapper for finders in sys.meta_path to replace wrap all loaders with ImportLoader.""" + """Wrapper for finders in sys.meta_path to wrap all loaders with ImportLoader.""" - def __init__(self, finder, watcher): + def __init__(self, watcher, finder): self.watcher = watcher self.finder = finder def __getattr__(self, name): if name == "find_spec" and hasattr(self.finder, name): return self._find_spec - if name == "find_module" and hasattr(self.finder, name): - return self._find_module return getattr(self.finder, name) def _find_spec(self, fullname, path, target=None): # Attempt to find the spec spec = self.finder.find_spec(fullname, path, target) if spec is not None: - if getattr(spec, "__loader__", None) is not None: + if getattr(spec, "loader", None) is not None: # Patch the loader to enable reloading - spec.__loader__ = ImportLoader(self.watcher, spec.__loader__) + spec.loader = ImportLoader(self.watcher, spec.loader) return spec - def _find_module(self, fullname, path=None): - loader = self.finder.find_module(fullname, path) - if loader is not None: - return ImportLoader(self.watcher, loader) - def _process_ps(ps, default_ps: str): """Replace ps1/ps2 with the default if the user specified value contains control characters.""" @@ -607,14 +592,7 @@ def __enter__(self): if self.watcher: meta_path = [] for finder in sys.meta_path: - # All elements get wrapped in ImportFinder instances execepted for instances of - # _SixMetaPathImporter (from six). When importing six, it will check if the importer - # is already part of sys.meta_path and will remove instances. We do not want to - # break this feature (see also #874). - if type(finder).__name__ == "_SixMetaPathImporter": - meta_path.append(finder) - else: - meta_path.append(ImportFinder(finder, self.watcher)) + meta_path.append(ImportFinder(self.watcher, finder)) sys.meta_path = meta_path sitefix.monkeypatch_quit() From 7c312971ce37dbeddd4316ed80cb640e50f04a9d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 6 Nov 2022 19:32:40 +0100 Subject: [PATCH 038/113] Log versions of all required dependencies --- bpython/args.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bpython/args.py b/bpython/args.py index ec7d3b29..0c514c37 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -41,6 +41,7 @@ import pygments import requests import sys +import xdg from pathlib import Path from . import __version__, __copyright__ @@ -206,10 +207,12 @@ def callback(group): logger.info("Starting bpython %s", __version__) logger.info("Python %s: %s", sys.executable, sys.version_info) + # versions of required dependencies logger.info("curtsies: %s", curtsies.__version__) logger.info("cwcwidth: %s", cwcwidth.__version__) logger.info("greenlet: %s", greenlet.__version__) logger.info("pygments: %s", pygments.__version__) # type: ignore + logger.info("pyxdg: %s", xdg.__version__) # type: ignore logger.info("requests: %s", requests.__version__) logger.info("environment:") for key, value in sorted(os.environ.items()): From 84314021dd26f254a93bc3f5ac56ac584b8accd3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 6 Nov 2022 19:32:50 +0100 Subject: [PATCH 039/113] Log versions of optional dependencies --- bpython/args.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bpython/args.py b/bpython/args.py index 0c514c37..2eb910d5 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -214,6 +214,27 @@ def callback(group): logger.info("pygments: %s", pygments.__version__) # type: ignore logger.info("pyxdg: %s", xdg.__version__) # type: ignore logger.info("requests: %s", requests.__version__) + + # versions of optional dependencies + try: + import pyperclip + + logger.info("pyperclip: %s", pyperclip.__version__) # type: ignore + except ImportError: + logger.info("pyperclip: not available") + try: + import jedi + + logger.info("jedi: %s", jedi.__version__) + except ImportError: + logger.info("jedi: not available") + try: + import watchdog + + logger.info("watchdog: available") + except ImportError: + logger.info("watchdog: not available") + logger.info("environment:") for key, value in sorted(os.environ.items()): if key.startswith("LC") or key.startswith("LANG") or key == "TERM": From b387c5a666d3bec79833754b177fd9f26dfe6886 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 6 Nov 2022 19:40:24 +0100 Subject: [PATCH 040/113] Remove no longer used pycheck config skip-checks: true --- .pycheckrc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .pycheckrc diff --git a/.pycheckrc b/.pycheckrc deleted file mode 100644 index e7050fad..00000000 --- a/.pycheckrc +++ /dev/null @@ -1 +0,0 @@ -blacklist = ['pyparsing', 'code', 'pygments/lexer'] From f797aa1d5af825e7ef266ded83488c587dcc725d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 3 Dec 2022 21:12:35 +0100 Subject: [PATCH 041/113] Fix Python 3.11 version constraints --- bpython/test/test_interpreter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index f53ec252..b2fb5275 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -112,7 +112,7 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - if (3, 11) <= sys.version_info[:2]: + if (3, 11, 0) <= sys.version_info[:3] < (3, 11, 1): expected = ( "Traceback (most recent call last):\n File " + green('""') From 352de3085e2f320000d8cd8d49378c4b14d22ec3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 3 Dec 2022 21:22:12 +0100 Subject: [PATCH 042/113] Fix type annotations --- bpython/cli.py | 10 ++++++---- bpython/translations/__init__.py | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 886dc2c8..bb1429a8 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -1299,7 +1299,7 @@ def show_list( arg_pos: Union[str, int, None], topline: Optional[inspection.FuncProps] = None, formatter: Optional[Callable] = None, - current_item: Union[str, Literal[False]] = None, + current_item: Optional[str] = None, ) -> None: v_items: Collection shared = ShowListState() @@ -1315,7 +1315,7 @@ def show_list( if items and formatter: items = [formatter(x) for x in items] - if current_item: + if current_item is not None: current_item = formatter(current_item) if topline: @@ -1492,8 +1492,10 @@ def tab(self, back: bool = False) -> bool: # 4. swap current word for a match list item elif self.matches_iter.matches: - current_match: Union[str, Literal[False]] = ( - back and self.matches_iter.previous() or next(self.matches_iter) + current_match = ( + self.matches_iter.previous() + if back + else next(self.matches_iter) ) try: f = None diff --git a/bpython/translations/__init__.py b/bpython/translations/__init__.py index 13c49802..0cb4c01f 100644 --- a/bpython/translations/__init__.py +++ b/bpython/translations/__init__.py @@ -2,7 +2,7 @@ import locale import os.path import sys -from typing import cast, List +from typing import Optional, cast, List from .. import package_dir @@ -17,7 +17,9 @@ def ngettext(singular, plural, n): return translator.ngettext(singular, plural, n) -def init(locale_dir: str = None, languages: List[str] = None) -> None: +def init( + locale_dir: Optional[str] = None, languages: Optional[List[str]] = None +) -> None: try: locale.setlocale(locale.LC_ALL, "") except locale.Error: From ebefde843bce5ec1e3f180c655308d5858672531 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 17 Dec 2022 21:24:18 +0100 Subject: [PATCH 043/113] Revert "Fix Python 3.11 version constraints" This reverts commit f797aa1d5af825e7ef266ded83488c587dcc725d. --- bpython/test/test_interpreter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index b2fb5275..f53ec252 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -112,7 +112,7 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - if (3, 11, 0) <= sys.version_info[:3] < (3, 11, 1): + if (3, 11) <= sys.version_info[:2]: expected = ( "Traceback (most recent call last):\n File " + green('""') From 71320bbfa318ec7aebd7bfafe3652715087fe7d2 Mon Sep 17 00:00:00 2001 From: Eric Burgess Date: Fri, 13 Jan 2023 13:06:13 -0600 Subject: [PATCH 044/113] Add more keywords to trigger auto-deindent --- bpython/curtsiesfrontend/repl.py | 4 +++- bpython/repl.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index cf7d9c37..037cf756 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1252,7 +1252,9 @@ def predicted_indent(self, line): elif ( line and ":" not in line - and line.strip().startswith(("return", "pass", "raise", "yield")) + and line.strip().startswith( + ("return", "pass", "...", "raise", "yield", "break", "continue") + ) ): indent = max(0, indent - self.config.tab_length) logger.debug("indent we found was %s", indent) diff --git a/bpython/repl.py b/bpython/repl.py index 8aea4e17..c964f090 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -1255,7 +1255,9 @@ def next_indentation(line, tab_length) -> int: if line.rstrip().endswith(":"): indentation += 1 elif indentation >= 1: - if line.lstrip().startswith(("return", "pass", "raise", "yield")): + if line.lstrip().startswith( + ("return", "pass", "...", "raise", "yield", "break", "continue") + ): indentation -= 1 return indentation From d0da704b96b37ce4611e91d59c09f922e3908246 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 14 Jan 2023 22:10:12 +0100 Subject: [PATCH 045/113] Update changelog for 0.24 --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1d902634..4f94198d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,9 +6,12 @@ Changelog General information: +* This release is focused on Python 3.11 support. New features: +* #980: Add more keywords to trigger auto-deindent. + Thanks to Eric Burgess Fixes: @@ -16,6 +19,7 @@ Fixes: Changes to dependencies: +* wheel is no required as part of pyproject.toml's build dependencies Support for Python 3.11 has been added. From b94ccc24cf94bb6e29bd2208f462fcc9b7c49cb2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 14 Jan 2023 22:18:10 +0100 Subject: [PATCH 046/113] Update readthedocs config again --- .readthedocs.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 942c1da0..a19293da 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,5 +1,13 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3" + +sphinx: + configuration: doc/sphinx/source/conf.py + python: install: - method: pip From d6e62b372504c290fa96825535317acf067463f6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 14 Jan 2023 22:27:13 +0100 Subject: [PATCH 047/113] Avoid bpython imports in setup.py --- setup.cfg | 2 ++ setup.py | 6 +----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 7955ee39..07f90115 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,8 @@ name = bpython long_description = file: README.rst license = MIT license_files = LICENSE +author = Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al. +author_email = bpython@googlegroups.com url = https://www.bpython-interpreter.org/ project_urls = GitHub = https://github.com/bpython/bpython diff --git a/setup.py b/setup.py index 12d4eeec..6790b9d7 100755 --- a/setup.py +++ b/setup.py @@ -124,9 +124,7 @@ def git_describe_to_python_version(version): cmdclass = {"build": build} -from bpython import package_dir, __author__ - -translations_dir = os.path.join(package_dir, "translations") +translations_dir = os.path.join("bpython", "translations") # localization options if using_translations: @@ -179,8 +177,6 @@ def git_describe_to_python_version(version): setup( version=version, - author=__author__, - author_email="robertanthonyfarrell@gmail.com", data_files=data_files, package_data={ "bpython": ["sample-config"], From 406f8713915566a77c7021daf160b8a8cc9ee863 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 14 Jan 2023 22:27:23 +0100 Subject: [PATCH 048/113] Update copyright year --- bpython/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/__init__.py b/bpython/__init__.py index adc00c06..dff06c0f 100644 --- a/bpython/__init__.py +++ b/bpython/__init__.py @@ -30,7 +30,7 @@ __author__ = ( "Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al." ) -__copyright__ = f"(C) 2008-2020 {__author__}" +__copyright__ = f"(C) 2008-2023 {__author__}" __license__ = "MIT" __version__ = version package_dir = os.path.abspath(os.path.dirname(__file__)) From b38f6c7a483302651b223e13a0c478224bc6d645 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 17 Dec 2022 21:44:27 +0100 Subject: [PATCH 049/113] Remove unused code --- bpython/test/test_interpreter.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index f53ec252..4a39cfe1 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -1,37 +1,25 @@ import sys -import re import unittest from curtsies.fmtfuncs import bold, green, magenta, cyan, red, plain -from unittest import mock from bpython.curtsiesfrontend import interpreter pypy = "PyPy" in sys.version -def remove_ansi(s): - return re.sub(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]".encode("ascii"), b"", s) +class Interpreter(interpreter.Interp): + def __init__(self): + super().__init__() + self.a = [] + def write(self, data): + self.a.append(data) -class TestInterpreter(unittest.TestCase): - def interp_errlog(self): - i = interpreter.Interp() - a = [] - i.write = a.append - return i, a - - def err_lineno(self, a): - strings = [x.__unicode__() for x in a] - for line in reversed(strings): - clean_line = remove_ansi(line) - m = re.search(r"line (\d+)[,]", clean_line) - if m: - return int(m.group(1)) - return None +class TestInterpreter(unittest.TestCase): def test_syntaxerror(self): - i, a = self.interp_errlog() + i = Interpreter() i.runsource("1.1.1.1") @@ -96,11 +84,12 @@ def test_syntaxerror(self): + "\n" ) + a = i.a self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) self.assertEqual(plain("").join(a), expected) def test_traceback(self): - i, a = self.interp_errlog() + i = Interpreter() def f(): return 1 / 0 @@ -142,6 +131,7 @@ def gfunc(): + "\n" ) + a = i.a self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) self.assertEqual(plain("").join(a), expected) From e1ca4522583f06b172674c4e11303fda044bf8f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 17:25:59 +0100 Subject: [PATCH 050/113] Start development of 0.25 --- CHANGELOG.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4f94198d..7ce32c42 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,21 @@ Changelog ========= +0.25 +---- + +General information: + + +New features: + + +Fixes: + + +Changes to dependencies: + + 0.24 ---- @@ -23,7 +38,6 @@ Changes to dependencies: Support for Python 3.11 has been added. - 0.23 ---- From c1f0385c79c0096265994b0d6e0aadf6db8a2709 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 17:34:55 +0100 Subject: [PATCH 051/113] Do not fail if curtsies is not available (fixes #978) Also delay imports of dependencies only used to log the version. --- bpython/args.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 2eb910d5..b9e68e1d 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -30,19 +30,13 @@ """ import argparse -from typing import Tuple, List, Optional, NoReturn, Callable import code -import curtsies -import cwcwidth -import greenlet import importlib.util import logging import os -import pygments -import requests import sys -import xdg from pathlib import Path +from typing import Tuple, List, Optional, NoReturn, Callable from . import __version__, __copyright__ from .config import default_config_path, Config @@ -205,10 +199,22 @@ def callback(group): bpython_logger.addHandler(logging.NullHandler()) curtsies_logger.addHandler(logging.NullHandler()) + import cwcwidth + import greenlet + import pygments + import requests + import xdg + logger.info("Starting bpython %s", __version__) logger.info("Python %s: %s", sys.executable, sys.version_info) # versions of required dependencies - logger.info("curtsies: %s", curtsies.__version__) + try: + import curtsies + + logger.info("curtsies: %s", curtsies.__version__) + except ImportError: + # may happen on Windows + logger.info("curtsies: not available") logger.info("cwcwidth: %s", cwcwidth.__version__) logger.info("greenlet: %s", greenlet.__version__) logger.info("pygments: %s", pygments.__version__) # type: ignore From 29d6f87f9688610a1f96cdcc32ce84f3ce435b7f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 17:52:17 +0100 Subject: [PATCH 052/113] Fix definition of write --- bpython/test/test_interpreter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 4a39cfe1..a4a32dd0 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -12,9 +12,7 @@ class Interpreter(interpreter.Interp): def __init__(self): super().__init__() self.a = [] - - def write(self, data): - self.a.append(data) + self.write = self.a.append class TestInterpreter(unittest.TestCase): From 46dc081faa21aa957f67761a15d0fb18031de87c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 18:00:39 +0100 Subject: [PATCH 053/113] Remove support for Python 3.7 (fixes #940) --- .github/workflows/build.yaml | 5 ++-- bpython/_typing_compat.py | 33 --------------------------- bpython/cli.py | 2 +- bpython/curtsies.py | 2 +- bpython/curtsiesfrontend/_internal.py | 3 +-- bpython/curtsiesfrontend/repl.py | 2 +- bpython/filelock.py | 3 +-- bpython/inspection.py | 12 ++++++++-- bpython/paste.py | 3 +-- bpython/repl.py | 14 ++++++------ doc/sphinx/source/contributing.rst | 2 +- doc/sphinx/source/releases.rst | 2 +- pyproject.toml | 2 +- requirements.txt | 1 - setup.cfg | 4 +--- 15 files changed, 29 insertions(+), 61 deletions(-) delete mode 100644 bpython/_typing_compat.py diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9cdcc1e9..1e2a374a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -10,17 +10,16 @@ on: jobs: build: runs-on: ubuntu-latest - continue-on-error: ${{ matrix.python-version == 'pypy-3.7' }} + continue-on-error: ${{ matrix.python-version == 'pypy-3.8' }} strategy: fail-fast: false matrix: python-version: - - "3.7" - "3.8" - "3.9" - "3.10" - "3.11" - - "pypy-3.7" + - "pypy-3.8" steps: - uses: actions/checkout@v3 with: diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py deleted file mode 100644 index 31fb6428..00000000 --- a/bpython/_typing_compat.py +++ /dev/null @@ -1,33 +0,0 @@ -# The MIT License -# -# Copyright (c) 2021 Sebastian Ramacher -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -try: - # introduced in Python 3.8 - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore - -try: - # introduced in Python 3.8 - from typing import Protocol -except ImportError: - from typing_extensions import Protocol # type: ignore diff --git a/bpython/cli.py b/bpython/cli.py index bb1429a8..0a03d513 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -69,12 +69,12 @@ Collection, Dict, TYPE_CHECKING, + Literal, ) if TYPE_CHECKING: from _curses import _CursesWindow -from ._typing_compat import Literal import unicodedata from dataclasses import dataclass diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 985f7085..6dc8d1f7 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -28,11 +28,11 @@ Generator, List, Optional, + Protocol, Sequence, Tuple, Union, ) -from ._typing_compat import Protocol logger = logging.getLogger(__name__) diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index 79c5e974..0480c1b0 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -22,8 +22,7 @@ import pydoc from types import TracebackType -from typing import Optional, Type -from .._typing_compat import Literal +from typing import Optional, Type, Literal from .. import _internal diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 037cf756..a3b32d44 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -17,13 +17,13 @@ Iterable, Dict, List, + Literal, Optional, Sequence, Tuple, Type, Union, ) -from .._typing_compat import Literal import greenlet from curtsies import ( diff --git a/bpython/filelock.py b/bpython/filelock.py index 429f708b..11f575b6 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -20,8 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -from typing import Optional, Type, IO -from ._typing_compat import Literal +from typing import Optional, Type, IO, Literal from types import TracebackType has_fcntl = True diff --git a/bpython/inspection.py b/bpython/inspection.py index db0a397c..fe1e3a0a 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -26,9 +26,17 @@ import pydoc import re from dataclasses import dataclass -from typing import Any, Callable, Optional, Type, Dict, List, ContextManager +from typing import ( + Any, + Callable, + Optional, + Type, + Dict, + List, + ContextManager, + Literal, +) from types import MemberDescriptorType, TracebackType -from ._typing_compat import Literal from pygments.token import Token from pygments.lexers import Python3Lexer diff --git a/bpython/paste.py b/bpython/paste.py index ceba5938..fd140a0e 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -22,7 +22,7 @@ import errno import subprocess -from typing import Optional, Tuple +from typing import Optional, Tuple, Protocol from urllib.parse import urljoin, urlparse import requests @@ -30,7 +30,6 @@ from .config import getpreferredencoding from .translations import _ -from ._typing_compat import Protocol class PasteFailed(Exception): diff --git a/bpython/repl.py b/bpython/repl.py index c964f090..fe72a4db 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -40,19 +40,19 @@ from pathlib import Path from types import ModuleType, TracebackType from typing import ( + Any, + Callable, + Dict, Iterable, - cast, List, - Tuple, - Any, + Literal, Optional, + TYPE_CHECKING, + Tuple, Type, Union, - Callable, - Dict, - TYPE_CHECKING, + cast, ) -from ._typing_compat import Literal from pygments.lexers import Python3Lexer from pygments.token import Token, _TokenType diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 54fd56c6..9e0f6bc4 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -17,7 +17,7 @@ the time of day. Getting your development environment set up ------------------------------------------- -bpython supports Python 3.7 and newer. The code is compatible with all +bpython supports Python 3.8 and newer. The code is compatible with all supported versions. Using a virtual environment is probably a good idea. Create a virtual diff --git a/doc/sphinx/source/releases.rst b/doc/sphinx/source/releases.rst index 738c24ff..fcce5c1c 100644 --- a/doc/sphinx/source/releases.rst +++ b/doc/sphinx/source/releases.rst @@ -45,7 +45,7 @@ A checklist to perform some manual tests before a release: Check that all of the following work before a release: -* Runs under Python 3.7 - 3.9 +* Runs under Python 3.8 - 3.11 * Save * Rewind * Pastebin diff --git a/pyproject.toml b/pyproject.toml index 6526fb9e..b7bd3196 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 80 -target_version = ["py37"] +target_version = ["py38"] include = '\.pyi?$' exclude = ''' /( diff --git a/requirements.txt b/requirements.txt index ba8b126d..4c750a69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ Pygments -backports.cached-property; python_version < "3.8" curtsies >=0.4.0 cwcwidth greenlet diff --git a/setup.cfg b/setup.cfg index 07f90115..081af0bc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ classifiers = Programming Language :: Python :: 3 [options] -python_requires = >=3.7 +python_requires = >=3.8 packages = bpython bpython.curtsiesfrontend @@ -22,14 +22,12 @@ packages = bpython.translations bpdb install_requires = - backports.cached-property; python_version < "3.8" curtsies >=0.4.0 cwcwidth greenlet pygments pyxdg requests - typing-extensions; python_version < "3.8" [options.extras_require] clipboard = pyperclip From 274f423e4a1b742904a87d5cebe18258ecd095a9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 18:13:26 +0100 Subject: [PATCH 054/113] Remove < 3.8 workaround --- bpython/importcompletion.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index c1e073f8..9b0edaab 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -175,17 +175,8 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: # Path is on skiplist return - try: - # https://bugs.python.org/issue34541 - # Once we migrate to Python 3.8, we can change it back to directly iterator over - # path.iterdir(). - children = tuple(path.iterdir()) - except OSError: - # Path is not readable - return - finder = importlib.machinery.FileFinder(str(path), *LOADERS) # type: ignore - for p in children: + for p in path.iterdir(): if p.name.startswith(".") or p.name == "__pycache__": # Impossible to import from names starting with . and we can skip __pycache__ continue From 1fb4041aa641d9b06047489c487cafbeb7d3d7b2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 19:06:34 +0100 Subject: [PATCH 055/113] Handle OSError again --- bpython/importcompletion.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 9b0edaab..7833a932 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -175,8 +175,14 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: # Path is on skiplist return + try: + children = path.iterdir() + except OSError: + # Path is not readable + return + finder = importlib.machinery.FileFinder(str(path), *LOADERS) # type: ignore - for p in path.iterdir(): + for p in children: if p.name.startswith(".") or p.name == "__pycache__": # Impossible to import from names starting with . and we can skip __pycache__ continue From df3750d2a93ce69d9676f6b17a0da858df7ca983 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 19:43:34 +0100 Subject: [PATCH 056/113] Really handle OSError --- bpython/importcompletion.py | 108 ++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 7833a932..9df140c6 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -175,65 +175,67 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: # Path is on skiplist return - try: - children = path.iterdir() - except OSError: - # Path is not readable - return - finder = importlib.machinery.FileFinder(str(path), *LOADERS) # type: ignore - for p in children: - if p.name.startswith(".") or p.name == "__pycache__": - # Impossible to import from names starting with . and we can skip __pycache__ - continue - elif any(fnmatch.fnmatch(p.name, entry) for entry in self.skiplist): - # Path is on skiplist - continue - elif not any(p.name.endswith(suffix) for suffix in SUFFIXES): - # Possibly a package - if "." in p.name: + try: + for p in path.iterdir(): + if p.name.startswith(".") or p.name == "__pycache__": + # Impossible to import from names starting with . and we can skip __pycache__ continue - elif p.is_dir(): - # Unfortunately, CPython just crashes if there is a directory - # which ends with a python extension, so work around. - continue - name = p.name - for suffix in SUFFIXES: - if name.endswith(suffix): - name = name[: -len(suffix)] - break - if name == "badsyntax_pep3120": - # Workaround for issue #166 - continue - - package_pathname = None - try: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", ImportWarning) - spec = finder.find_spec(name) - if spec is None: + elif any( + fnmatch.fnmatch(p.name, entry) for entry in self.skiplist + ): + # Path is on skiplist + continue + elif not any(p.name.endswith(suffix) for suffix in SUFFIXES): + # Possibly a package + if "." in p.name: continue - if spec.submodule_search_locations is not None: - package_pathname = spec.submodule_search_locations[0] - except (ImportError, OSError, SyntaxError, UnicodeEncodeError): - # UnicodeEncodeError happens with Python 3 when there is a filename in some invalid encoding - continue + elif p.is_dir(): + # Unfortunately, CPython just crashes if there is a directory + # which ends with a python extension, so work around. + continue + name = p.name + for suffix in SUFFIXES: + if name.endswith(suffix): + name = name[: -len(suffix)] + break + if name == "badsyntax_pep3120": + # Workaround for issue #166 + continue - if package_pathname is not None: - path_real = Path(package_pathname).resolve() + package_pathname = None try: - stat = path_real.stat() - except OSError: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", ImportWarning) + spec = finder.find_spec(name) + if spec is None: + continue + if spec.submodule_search_locations is not None: + package_pathname = spec.submodule_search_locations[ + 0 + ] + except (ImportError, OSError, SyntaxError, UnicodeEncodeError): + # UnicodeEncodeError happens with Python 3 when there is a filename in some invalid encoding continue - loaded_inode = _LoadedInode(stat.st_dev, stat.st_ino) - if loaded_inode not in self.paths: - self.paths.add(loaded_inode) - for subname in self.find_modules(path_real): - if subname is None: - yield None # take a break to avoid unresponsiveness - elif subname != "__init__": - yield f"{name}.{subname}" - yield name + + if package_pathname is not None: + path_real = Path(package_pathname).resolve() + try: + stat = path_real.stat() + except OSError: + continue + loaded_inode = _LoadedInode(stat.st_dev, stat.st_ino) + if loaded_inode not in self.paths: + self.paths.add(loaded_inode) + for subname in self.find_modules(path_real): + if subname is None: + yield None # take a break to avoid unresponsiveness + elif subname != "__init__": + yield f"{name}.{subname}" + yield name + except OSError: + # Path is not readable + return yield None # take a break to avoid unresponsiveness def find_all_modules( From b537a508ede7b09eb118f20898bd3c8c0a2fe7f9 Mon Sep 17 00:00:00 2001 From: Ganden Schaffner Date: Sun, 29 Jan 2023 00:00:00 -0800 Subject: [PATCH 057/113] Fix URL typo in package metadata --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 081af0bc..8c4294d9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ author_email = bpython@googlegroups.com url = https://www.bpython-interpreter.org/ project_urls = GitHub = https://github.com/bpython/bpython - Documentation = https://doc.bpython-interpreter.org + Documentation = https://docs.bpython-interpreter.org classifiers = Programming Language :: Python :: 3 From 11a72fb11d48f8acd018acd24a538448ff57c2c2 Mon Sep 17 00:00:00 2001 From: Nitant Patel Date: Thu, 2 Mar 2023 00:48:19 -0500 Subject: [PATCH 058/113] Clarify fork instructions in contributing docs `setup.py` expects to be able to find git tags in order to properly set the package version. If a fork does not have any tags, `pip install -e .` will fail due to the default `"unknown"` being an invalid version. --- doc/sphinx/source/contributing.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 9e0f6bc4..32b1ea86 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -30,7 +30,10 @@ environment with # necessary every time you work on bpython $ source bpython-dev/bin/activate -Fork bpython in the GitHub web interface, then clone the repo: +Fork bpython in the GitHub web interface. Be sure to include the tags +in your fork by un-selecting the option to copy only the main branch. + +Then, clone the forked repo: .. code-block:: bash From 7df8e6f89036775ad659983d45deda98a175d2be Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 2 Mar 2023 09:53:04 +0100 Subject: [PATCH 059/113] Apply black --- bpython/autocomplete.py | 2 +- bpython/cli.py | 1 + bpython/config.py | 2 +- bpython/curtsiesfrontend/manual_readline.py | 1 - bpython/curtsiesfrontend/repl.py | 1 - bpython/repl.py | 10 ++++------ bpython/test/__init__.py | 1 - bpython/test/test_repl.py | 4 ++-- bpython/urwid.py | 2 -- 9 files changed, 9 insertions(+), 15 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index b97fd86f..e0849c6d 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -57,6 +57,7 @@ logger = logging.getLogger(__name__) + # Autocomplete modes class AutocompleteModes(Enum): NONE = "none" @@ -381,7 +382,6 @@ def format(self, filename: str) -> str: class AttrCompletion(BaseCompletionType): - attr_matches_re = LazyReCompile(r"(\w+(\.\w+)*)\.(\w*)") def matches( diff --git a/bpython/cli.py b/bpython/cli.py index 0a03d513..fcfd11fa 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -264,6 +264,7 @@ def readlines(self, size: int = -1) -> List[str]: # the addstr stuff to a higher level. # + # Have to ignore the return type on this one because the colors variable # is Optional[MutableMapping[str, int]] but for the purposes of this # function it can't be None diff --git a/bpython/config.py b/bpython/config.py index 29b906dd..5123ec22 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -90,7 +90,7 @@ def fill_config_with_default_values( if not config.has_section(section): config.add_section(section) - for (opt, val) in default_values[section].items(): + for opt, val in default_values[section].items(): if not config.has_option(section, opt): config.set(section, opt, str(val)) diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py index f95e66c5..206e5278 100644 --- a/bpython/curtsiesfrontend/manual_readline.py +++ b/bpython/curtsiesfrontend/manual_readline.py @@ -16,7 +16,6 @@ class AbstractEdits: - default_kwargs = { "line": "hello world", "cursor_offset": 5, diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index a3b32d44..e4819e19 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -893,7 +893,6 @@ def insert_char_pair_end(self, e): self.add_normal_character(e) def get_last_word(self): - previous_word = _last_word(self.rl_history.entry) word = _last_word(self.rl_history.back()) line = self.current_line diff --git a/bpython/repl.py b/bpython/repl.py index fe72a4db..f30cfa31 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -465,7 +465,6 @@ def cursor_offset(self, value: int) -> None: self._set_cursor_offset(value) if TYPE_CHECKING: - # not actually defined, subclasses must define cpos: int @@ -567,7 +566,7 @@ def current_string(self, concatenate=False): return "" opening = string_tokens.pop()[1] string = list() - for (token, value) in reversed(string_tokens): + for token, value in reversed(string_tokens): if token is Token.Text: continue elif opening is None: @@ -602,7 +601,7 @@ def _funcname_and_argnum( # if keyword is not None, we've encountered a keyword and so we're done counting stack = [_FuncExpr("", "", 0, "")] try: - for (token, value) in Python3Lexer().get_tokens(line): + for token, value in Python3Lexer().get_tokens(line): if token is Token.Punctuation: if value in "([{": stack.append(_FuncExpr("", "", 0, value)) @@ -692,7 +691,6 @@ def get_args(self): # py3 f.__new__.__class__ is not object.__new__.__class__ ): - class_f = f.__new__ if class_f: @@ -1117,7 +1115,7 @@ def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: line_tokens: List[Tuple[_TokenType, str]] = list() saved_tokens: List[Tuple[_TokenType, str]] = list() search_for_paren = True - for (token, value) in split_lines(all_tokens): + for token, value in split_lines(all_tokens): pos += len(value) if token is Token.Text and value == "\n": line += 1 @@ -1263,7 +1261,7 @@ def next_indentation(line, tab_length) -> int: def split_lines(tokens): - for (token, value) in tokens: + for token, value in tokens: if not value: continue while value: diff --git a/bpython/test/__init__.py b/bpython/test/__init__.py index 7722278c..4618eca4 100644 --- a/bpython/test/__init__.py +++ b/bpython/test/__init__.py @@ -13,7 +13,6 @@ def setUpClass(cls): class MagicIterMock(unittest.mock.MagicMock): - __next__ = unittest.mock.Mock(return_value=None) diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 63309364..b3e8912f 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -183,7 +183,7 @@ def set_input_line(self, line): self.repl.cursor_offset = len(line) def test_func_name(self): - for (line, expected_name) in [ + for line, expected_name in [ ("spam(", "spam"), # map pydoc has no signature in pypy ("spam(any([]", "any") if pypy else ("spam(map([]", "map"), @@ -194,7 +194,7 @@ def test_func_name(self): self.assertEqual(self.repl.current_func.__name__, expected_name) def test_func_name_method_issue_479(self): - for (line, expected_name) in [ + for line, expected_name in [ ("o.spam(", "spam"), # map pydoc has no signature in pypy ("o.spam(any([]", "any") if pypy else ("o.spam(map([]", "map"), diff --git a/bpython/urwid.py b/bpython/urwid.py index 66054097..4b41c12a 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -74,7 +74,6 @@ else: class EvalProtocol(basic.LineOnlyReceiver): - delimiter = "\n" def __init__(self, myrepl): @@ -570,7 +569,6 @@ def file_prompt(self, s: str) -> Optional[str]: class URWIDRepl(repl.Repl): - _time_between_redraws = 0.05 # seconds def __init__(self, event_loop, palette, interpreter, config): From 44c3b702037c1c1a5f7681b7f86c0246fbf0db7e Mon Sep 17 00:00:00 2001 From: supakeen Date: Tue, 9 Nov 2021 16:51:56 +0000 Subject: [PATCH 060/113] Remove deprecated curses (cli) rendering backend. --- CHANGELOG.rst | 4 +- bpython/cli.py | 2094 ----------------- bpython/test/test_repl.py | 152 +- bpython/translations/bpython.pot | 30 +- .../translations/de/LC_MESSAGES/bpython.po | 34 +- .../translations/es_ES/LC_MESSAGES/bpython.po | 31 +- .../translations/fr_FR/LC_MESSAGES/bpython.po | 31 +- .../translations/it_IT/LC_MESSAGES/bpython.po | 31 +- .../translations/nl_NL/LC_MESSAGES/bpython.po | 31 +- doc/sphinx/source/tips.rst | 2 +- doc/sphinx/source/windows.rst | 6 - setup.cfg | 1 - 12 files changed, 16 insertions(+), 2431 deletions(-) delete mode 100644 bpython/cli.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7ce32c42..d7ecb3ab 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,9 @@ Changelog General information: +* The bpython-cli rendering backend has been removed following deprecation in + version 0.19. + New features: @@ -44,7 +47,6 @@ Support for Python 3.11 has been added. General information: * More and more type annotations have been added to the bpython code base. -* Some work has been performed to stop relying on blessings. New features: diff --git a/bpython/cli.py b/bpython/cli.py deleted file mode 100644 index fcfd11fa..00000000 --- a/bpython/cli.py +++ /dev/null @@ -1,2094 +0,0 @@ -# The MIT License -# -# Copyright (c) 2008 Bob Farrell -# Copyright (c) bpython authors -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# mypy: disallow_untyped_defs=True -# mypy: disallow_untyped_calls=True - -# Modified by Brandon Navra -# Notes for Windows -# Prerequisites -# - Curses -# - pyreadline -# -# Added -# -# - Support for running on windows command prompt -# - input from numpad keys -# -# Issues -# -# - Suspend doesn't work nor does detection of resizing of screen -# - Instead the suspend key exits the program -# - View source doesn't work on windows unless you install the less program (From GnuUtils or Cygwin) - - -import curses -import errno -import functools -import math -import os -import platform -import re -import struct -import sys -import time -from typing import ( - Iterator, - NoReturn, - List, - MutableMapping, - Any, - Callable, - TypeVar, - cast, - IO, - Iterable, - Optional, - Union, - Tuple, - Collection, - Dict, - TYPE_CHECKING, - Literal, -) - -if TYPE_CHECKING: - from _curses import _CursesWindow - -import unicodedata -from dataclasses import dataclass - -if platform.system() != "Windows": - import signal # Windows does not have job control - import termios # Windows uses curses - import fcntl # Windows uses curses - - -# These are used for syntax highlighting -from pygments import format -from pygments.formatters import TerminalFormatter -from pygments.lexers import Python3Lexer -from pygments.token import Token, _TokenType -from .formatter import BPythonFormatter - -# This for config -from .config import getpreferredencoding, Config - -# This for keys -from .keys import cli_key_dispatch as key_dispatch - -# This for i18n -from . import translations -from .translations import _ - -from . import repl, inspection -from . import args as bpargs -from .pager import page -from .args import parse as argsparse - -F = TypeVar("F", bound=Callable[..., Any]) - -# --- module globals --- -stdscr = None -colors: Optional[MutableMapping[str, int]] = None - -DO_RESIZE = False -# --- - - -@dataclass -class ShowListState: - cols: int = 0 - rows: int = 0 - wl: int = 0 - - -def forward_if_not_current(func: F) -> F: - @functools.wraps(func) - def newfunc(self, *args, **kwargs): # type: ignore - dest = self.get_dest() - if self is dest: - return func(self, *args, **kwargs) - else: - return getattr(self.get_dest(), newfunc.__name__)(*args, **kwargs) - - return cast(F, newfunc) - - -class FakeStream: - """Provide a fake file object which calls functions on the interface - provided.""" - - def __init__(self, interface: "CLIRepl", get_dest: IO[str]) -> None: - self.encoding: str = getpreferredencoding() - self.interface = interface - self.get_dest = get_dest - - @forward_if_not_current - def write(self, s: str) -> None: - self.interface.write(s) - - @forward_if_not_current - def writelines(self, l: Iterable[str]) -> None: - for s in l: - self.write(s) - - def isatty(self) -> bool: - # some third party (amongst them mercurial) depend on this - return True - - def flush(self) -> None: - self.interface.flush() - - -class FakeStdin: - """Provide a fake stdin type for things like raw_input() etc.""" - - def __init__(self, interface: "CLIRepl") -> None: - """Take the curses Repl on init and assume it provides a get_key method - which, fortunately, it does.""" - - self.encoding = getpreferredencoding() - self.interface = interface - self.buffer: List[str] = list() - - def __iter__(self) -> Iterator: - return iter(self.readlines()) - - def flush(self) -> None: - """Flush the internal buffer. This is a no-op. Flushing stdin - doesn't make any sense anyway.""" - - def write(self, value: str) -> NoReturn: - # XXX IPython expects sys.stdin.write to exist, there will no doubt be - # others, so here's a hack to keep them happy - raise OSError(errno.EBADF, "sys.stdin is read-only") - - def isatty(self) -> bool: - return True - - def readline(self, size: int = -1) -> str: - """I can't think of any reason why anything other than readline would - be useful in the context of an interactive interpreter so this is the - only one I've done anything with. The others are just there in case - someone does something weird to stop it from blowing up.""" - - if not size: - return "" - elif self.buffer: - buffer = self.buffer.pop(0) - else: - buffer = "" - - curses.raw(True) - try: - while not buffer.endswith(("\n", "\r")): - key = self.interface.get_key() - if key in (curses.erasechar(), "KEY_BACKSPACE"): - y, x = self.interface.scr.getyx() - if buffer: - self.interface.scr.delch(y, x - 1) - buffer = buffer[:-1] - continue - elif key == chr(4) and not buffer: - # C-d - return "" - elif key not in ("\n", "\r") and ( - len(key) > 1 or unicodedata.category(key) == "Cc" - ): - continue - sys.stdout.write(key) - # Include the \n in the buffer - raw_input() seems to deal with trailing - # linebreaks and will break if it gets an empty string. - buffer += key - finally: - curses.raw(False) - - if size > 0: - rest = buffer[size:] - if rest: - self.buffer.append(rest) - buffer = buffer[:size] - - return buffer - - def read(self, size: Optional[int] = None) -> str: - if size == 0: - return "" - - data = list() - while size is None or size > 0: - line = self.readline(size or -1) - if not line: - break - if size is not None: - size -= len(line) - data.append(line) - - return "".join(data) - - def readlines(self, size: int = -1) -> List[str]: - return list(iter(self.readline, "")) - - -# TODO: -# -# Tab completion does not work if not at the end of the line. -# -# Numerous optimisations can be made but it seems to do all the lookup stuff -# fast enough on even my crappy server so I'm not too bothered about that -# at the moment. -# -# The popup window that displays the argspecs and completion suggestions -# needs to be an instance of a ListWin class or something so I can wrap -# the addstr stuff to a higher level. -# - - -# Have to ignore the return type on this one because the colors variable -# is Optional[MutableMapping[str, int]] but for the purposes of this -# function it can't be None -def get_color(config: Config, name: str) -> int: # type: ignore[return] - global colors - if colors: - return colors[config.color_scheme[name].lower()] - - -def get_colpair(config: Config, name: str) -> int: - return curses.color_pair(get_color(config, name) + 1) - - -def make_colors(config: Config) -> Dict[str, int]: - """Init all the colours in curses and bang them into a dictionary""" - - # blacK, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default: - c = { - "k": 0, - "r": 1, - "g": 2, - "y": 3, - "b": 4, - "m": 5, - "c": 6, - "w": 7, - "d": -1, - } - - if platform.system() == "Windows": - c = dict( - list(c.items()) - + [ - ("K", 8), - ("R", 9), - ("G", 10), - ("Y", 11), - ("B", 12), - ("M", 13), - ("C", 14), - ("W", 15), - ] - ) - - for i in range(63): - if i > 7: - j = i // 8 - else: - j = c[config.color_scheme["background"]] - curses.init_pair(i + 1, i % 8, j) - - return c - - -class CLIInteraction(repl.Interaction): - def __init__(self, config: Config, statusbar: "Statusbar"): - super().__init__(config) - self.statusbar = statusbar - - def confirm(self, q: str) -> bool: - """Ask for yes or no and return boolean""" - try: - reply = self.statusbar.prompt(q) - except ValueError: - return False - - return reply.lower() in (_("y"), _("yes")) - - def notify( - self, s: str, n: float = 10.0, wait_for_keypress: bool = False - ) -> None: - self.statusbar.message(s, n) - - def file_prompt(self, s: str) -> Optional[str]: - return self.statusbar.prompt(s) - - -class CLIRepl(repl.Repl): - def __init__( - self, - scr: "_CursesWindow", - interp: repl.Interpreter, - statusbar: "Statusbar", - config: Config, - idle: Optional[Callable] = None, - ): - super().__init__(interp, config) - # mypy doesn't quite understand the difference between a class variable with a callable type and a method. - # https://github.com/python/mypy/issues/2427 - self.interp.writetb = self.writetb # type:ignore[assignment] - self.scr: "_CursesWindow" = scr - self.stdout_hist = "" # native str (bytes in Py2, unicode in Py3) - self.list_win = newwin(get_colpair(config, "background"), 1, 1, 1, 1) - self.cpos = 0 - self.do_exit = False - self.exit_value: Tuple[Any, ...] = () - self.f_string = "" - self.idle = idle - self.in_hist = False - self.paste_mode = False - self.last_key_press = time.time() - self.s = "" - self.statusbar = statusbar - self.formatter = BPythonFormatter(config.color_scheme) - self.interact = CLIInteraction(self.config, statusbar=self.statusbar) - self.ix: int - self.iy: int - self.arg_pos: Union[str, int, None] - self.prev_block_finished: int - - if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1: - config.cli_suggestion_width = 0.8 - - def _get_cursor_offset(self) -> int: - return len(self.s) - self.cpos - - def _set_cursor_offset(self, offset: int) -> None: - self.cpos = len(self.s) - offset - - def addstr(self, s: str) -> None: - """Add a string to the current input line and figure out - where it should go, depending on the cursor position.""" - self.rl_history.reset() - if not self.cpos: - self.s += s - else: - l = len(self.s) - self.s = self.s[: l - self.cpos] + s + self.s[l - self.cpos :] - - self.complete() - - def atbol(self) -> bool: - """Return True or False accordingly if the cursor is at the beginning - of the line (whitespace is ignored). This exists so that p_key() knows - how to handle the tab key being pressed - if there is nothing but white - space before the cursor then process it as a normal tab otherwise - attempt tab completion.""" - - return not self.s.lstrip() - - # This function shouldn't return None because of pos -= self.bs() later on - def bs(self, delete_tabs: bool = True) -> int: # type: ignore[return-value] - """Process a backspace""" - - self.rl_history.reset() - y, x = self.scr.getyx() - - if not self.s: - return None # type: ignore[return-value] - - if x == self.ix and y == self.iy: - return None # type: ignore[return-value] - - n = 1 - - self.clear_wrapped_lines() - - if not self.cpos: - # I know the nested if blocks look nasty. :( - if self.atbol() and delete_tabs: - n = len(self.s) % self.config.tab_length - if not n: - n = self.config.tab_length - - self.s = self.s[:-n] - else: - self.s = self.s[: -self.cpos - 1] + self.s[-self.cpos :] - - self.print_line(self.s, clr=True) - - return n - - def bs_word(self) -> str: - self.rl_history.reset() - pos = len(self.s) - self.cpos - 1 - deleted = [] - # First we delete any space to the left of the cursor. - while pos >= 0 and self.s[pos] == " ": - deleted.append(self.s[pos]) - pos -= self.bs() - # Then we delete a full word. - while pos >= 0 and self.s[pos] != " ": - deleted.append(self.s[pos]) - pos -= self.bs() - - return "".join(reversed(deleted)) - - def check(self) -> None: - """Check if paste mode should still be active and, if not, deactivate - it and force syntax highlighting.""" - - if ( - self.paste_mode - and time.time() - self.last_key_press > self.config.paste_time - ): - self.paste_mode = False - self.print_line(self.s) - - def clear_current_line(self) -> None: - """Called when a SyntaxError occurred in the interpreter. It is - used to prevent autoindentation from occurring after a - traceback.""" - repl.Repl.clear_current_line(self) - self.s = "" - - def clear_wrapped_lines(self) -> None: - """Clear the wrapped lines of the current input.""" - # curses does not handle this on its own. Sad. - height, width = self.scr.getmaxyx() - max_y = min(self.iy + (self.ix + len(self.s)) // width + 1, height) - for y in range(self.iy + 1, max_y): - self.scr.move(y, 0) - self.scr.clrtoeol() - - def complete(self, tab: bool = False) -> None: - """Get Autocomplete list and window. - - Called whenever these should be updated, and called - with tab - """ - if self.paste_mode: - self.scr.touchwin() # TODO necessary? - return - - list_win_visible = repl.Repl.complete(self, tab) - - if list_win_visible: - try: - f = None - if self.matches_iter.completer: - f = self.matches_iter.completer.format - - self.show_list( - self.matches_iter.matches, - self.arg_pos, - topline=self.funcprops, - formatter=f, - ) - except curses.error: - # XXX: This is a massive hack, it will go away when I get - # cusswords into a good enough state that we can start - # using it. - self.list_win.border() - self.list_win.refresh() - list_win_visible = False - if not list_win_visible: - self.scr.redrawwin() - self.scr.refresh() - - def clrtobol(self) -> None: - """Clear from cursor to beginning of line; usual C-u behaviour""" - self.clear_wrapped_lines() - - if not self.cpos: - self.s = "" - else: - self.s = self.s[-self.cpos :] - - self.print_line(self.s, clr=True) - self.scr.redrawwin() - self.scr.refresh() - - def _get_current_line(self) -> str: - return self.s - - def _set_current_line(self, line: str) -> None: - self.s = line - - def cut_to_buffer(self) -> None: - """Clear from cursor to end of line, placing into cut buffer""" - self.cut_buffer = self.s[-self.cpos :] - self.s = self.s[: -self.cpos] - self.cpos = 0 - self.print_line(self.s, clr=True) - self.scr.redrawwin() - self.scr.refresh() - - def delete(self) -> None: - """Process a del""" - if not self.s: - return - - if self.mvc(-1): - self.bs(False) - - def echo(self, s: str, redraw: bool = True) -> None: - """Parse and echo a formatted string with appropriate attributes. It - uses the formatting method as defined in formatter.py to parse the - strings. It won't update the screen if it's reevaluating the code (as it - does with undo).""" - a = get_colpair(self.config, "output") - if "\x01" in s: - rx = re.search("\x01([A-Za-z])([A-Za-z]?)", s) - if rx: - fg = rx.groups()[0] - bg = rx.groups()[1] - col_num = self._C[fg.lower()] - if bg and bg != "I": - col_num *= self._C[bg.lower()] - - a = curses.color_pair(int(col_num) + 1) - if bg == "I": - a = a | curses.A_REVERSE - s = re.sub("\x01[A-Za-z][A-Za-z]?", "", s) - if fg.isupper(): - a = a | curses.A_BOLD - s = s.replace("\x03", "") - s = s.replace("\x01", "") - - # Replace NUL bytes, as addstr raises an exception otherwise - s = s.replace("\0", "") - # Replace \r\n bytes, as addstr remove the current line otherwise - s = s.replace("\r\n", "\n") - - self.scr.addstr(s, a) - - if redraw and not self.evaluating: - self.scr.refresh() - - def end(self, refresh: bool = True) -> bool: - self.cpos = 0 - h, w = gethw() - y, x = divmod(len(self.s) + self.ix, w) - y += self.iy - self.scr.move(y, x) - if refresh: - self.scr.refresh() - - return True - - def hbegin(self) -> None: - """Replace the active line with first line in history and - increment the index to keep track""" - self.cpos = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.first() - self.print_line(self.s, clr=True) - - def hend(self) -> None: - """Same as hbegin() but, well, forward""" - self.cpos = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.last() - self.print_line(self.s, clr=True) - - def back(self) -> None: - """Replace the active line with previous line in history and - increment the index to keep track""" - - self.cpos = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.back() - self.print_line(self.s, clr=True) - - def fwd(self) -> None: - """Same as back() but, well, forward""" - - self.cpos = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.forward() - self.print_line(self.s, clr=True) - - def search(self) -> None: - """Search with the partial matches from the history object.""" - - self.cpo = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.back(start=False, search=True) - self.print_line(self.s, clr=True) - - def get_key(self) -> str: - key = "" - while True: - try: - key += self.scr.getkey() - # Seems like we get a in the locale's encoding - # encoded string in Python 3 as well, but of - # type str instead of bytes, hence convert it to - # bytes first and decode then - key = key.encode("latin-1").decode(getpreferredencoding()) - self.scr.nodelay(False) - except UnicodeDecodeError: - # Yes, that actually kind of sucks, but I don't see another way to get - # input right - self.scr.nodelay(True) - except curses.error: - # I'm quite annoyed with the ambiguity of this exception handler. I previously - # caught "curses.error, x" and accessed x.message and checked that it was "no - # input", which seemed a crappy way of doing it. But then I ran it on a - # different computer and the exception seems to have entirely different - # attributes. So let's hope getkey() doesn't raise any other crazy curses - # exceptions. :) - self.scr.nodelay(False) - # XXX What to do here? Raise an exception? - if key: - return key - else: - if key != "\x00": - t = time.time() - self.paste_mode = ( - t - self.last_key_press <= self.config.paste_time - ) - self.last_key_press = t - return key - else: - key = "" - finally: - if self.idle: - self.idle(self) - - def get_line(self) -> str: - """Get a line of text and return it - This function initialises an empty string and gets the - curses cursor position on the screen and stores it - for the echo() function to use later (I think). - Then it waits for key presses and passes them to p_key(), - which returns None if Enter is pressed (that means "Return", - idiot).""" - - self.s = "" - self.rl_history.reset() - self.iy, self.ix = self.scr.getyx() - - if not self.paste_mode: - for _ in range(self.next_indentation()): - self.p_key("\t") - - self.cpos = 0 - - while True: - key = self.get_key() - if self.p_key(key) is None: - if self.config.cli_trim_prompts and self.s.startswith(">>> "): - self.s = self.s[4:] - return self.s - - def home(self, refresh: bool = True) -> bool: - self.scr.move(self.iy, self.ix) - self.cpos = len(self.s) - if refresh: - self.scr.refresh() - return True - - def lf(self) -> None: - """Process a linefeed character; it only needs to check the - cursor position and move appropriately so it doesn't clear - the current line after the cursor.""" - if self.cpos: - for _ in range(self.cpos): - self.mvc(-1) - - # Reprint the line (as there was maybe a highlighted paren in it) - self.print_line(self.s, newline=True) - self.echo("\n") - - def mkargspec( - self, - topline: inspection.FuncProps, - in_arg: Union[str, int, None], - down: bool, - ) -> int: - """This figures out what to do with the argspec and puts it nicely into - the list window. It returns the number of lines used to display the - argspec. It's also kind of messy due to it having to call so many - addstr() to get the colouring right, but it seems to be pretty - sturdy.""" - - r = 3 - fn = topline.func - args = topline.argspec.args - kwargs = topline.argspec.defaults - _args = topline.argspec.varargs - _kwargs = topline.argspec.varkwargs - is_bound_method = topline.is_bound_method - kwonly = topline.argspec.kwonly - kwonly_defaults = topline.argspec.kwonly_defaults or dict() - max_w = int(self.scr.getmaxyx()[1] * 0.6) - self.list_win.erase() - self.list_win.resize(3, max_w) - h, w = self.list_win.getmaxyx() - - self.list_win.addstr("\n ") - self.list_win.addstr( - fn, get_colpair(self.config, "name") | curses.A_BOLD - ) - self.list_win.addstr(": (", get_colpair(self.config, "name")) - maxh = self.scr.getmaxyx()[0] - - if is_bound_method and isinstance(in_arg, int): - in_arg += 1 - - punctuation_colpair = get_colpair(self.config, "punctuation") - - for k, i in enumerate(args): - y, x = self.list_win.getyx() - ln = len(str(i)) - kw = None - if kwargs and k + 1 > len(args) - len(kwargs): - kw = repr(kwargs[k - (len(args) - len(kwargs))]) - ln += len(kw) + 1 - - if ln + x >= w: - ty = self.list_win.getbegyx()[0] - if not down and ty > 0: - h += 1 - self.list_win.mvwin(ty - 1, 1) - self.list_win.resize(h, w) - elif down and h + r < maxh - ty: - h += 1 - self.list_win.resize(h, w) - else: - break - r += 1 - self.list_win.addstr("\n\t") - - if str(i) == "self" and k == 0: - color = get_colpair(self.config, "name") - else: - color = get_colpair(self.config, "token") - - if k == in_arg or i == in_arg: - color |= curses.A_BOLD - - self.list_win.addstr(str(i), color) - if kw is not None: - self.list_win.addstr("=", punctuation_colpair) - self.list_win.addstr(kw, get_colpair(self.config, "token")) - if k != len(args) - 1: - self.list_win.addstr(", ", punctuation_colpair) - - if _args: - if args: - self.list_win.addstr(", ", punctuation_colpair) - self.list_win.addstr(f"*{_args}", get_colpair(self.config, "token")) - - if kwonly: - if not _args: - if args: - self.list_win.addstr(", ", punctuation_colpair) - self.list_win.addstr("*", punctuation_colpair) - marker = object() - for arg in kwonly: - self.list_win.addstr(", ", punctuation_colpair) - color = get_colpair(self.config, "token") - if arg == in_arg: - color |= curses.A_BOLD - self.list_win.addstr(arg, color) - default = kwonly_defaults.get(arg, marker) - if default is not marker: - self.list_win.addstr("=", punctuation_colpair) - self.list_win.addstr( - repr(default), get_colpair(self.config, "token") - ) - - if _kwargs: - if args or _args or kwonly: - self.list_win.addstr(", ", punctuation_colpair) - self.list_win.addstr( - f"**{_kwargs}", get_colpair(self.config, "token") - ) - self.list_win.addstr(")", punctuation_colpair) - - return r - - def mvc(self, i: int, refresh: bool = True) -> bool: - """This method moves the cursor relatively from the current - position, where: - 0 == (right) end of current line - length of current line len(self.s) == beginning of current line - and: - current cursor position + i - for positive values of i the cursor will move towards the beginning - of the line, negative values the opposite.""" - y, x = self.scr.getyx() - - if self.cpos == 0 and i < 0: - return False - - if x == self.ix and y == self.iy and i >= 1: - return False - - h, w = gethw() - if x - i < 0: - y -= 1 - x = w - - if x - i >= w: - y += 1 - x = 0 + i - - self.cpos += i - self.scr.move(y, x - i) - if refresh: - self.scr.refresh() - - return True - - def p_key(self, key: str) -> Union[None, str, bool]: - """Process a keypress""" - - if key is None: - return "" - - config = self.config - - if platform.system() == "Windows": - C_BACK = chr(127) - BACKSP = chr(8) - else: - C_BACK = chr(8) - BACKSP = chr(127) - - if key == C_BACK: # C-Backspace (on my computer anyway!) - self.clrtobol() - key = "\n" - # Don't return; let it get handled - - if key == chr(27): # Escape Key - return "" - - if key in (BACKSP, "KEY_BACKSPACE"): - self.bs() - self.complete() - return "" - - elif key in key_dispatch[config.delete_key] and not self.s: - # Delete on empty line exits - self.do_exit = True - return None - - elif key in ("KEY_DC",) + key_dispatch[config.delete_key]: - self.delete() - self.complete() - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - return "" - - elif key in key_dispatch[config.undo_key]: # C-r - n = self.prompt_undo() - if n > 0: - self.undo(n=n) - return "" - - elif key in key_dispatch[config.search_key]: - self.search() - return "" - - elif key in ("KEY_UP",) + key_dispatch[config.up_one_line_key]: - # Cursor Up/C-p - self.back() - return "" - - elif key in ("KEY_DOWN",) + key_dispatch[config.down_one_line_key]: - # Cursor Down/C-n - self.fwd() - return "" - - elif key in ("KEY_LEFT", " ^B", chr(2)): # Cursor Left or ^B - self.mvc(1) - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - - elif key in ("KEY_RIGHT", "^F", chr(6)): # Cursor Right or ^F - self.mvc(-1) - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - - elif key in ("KEY_HOME", "^A", chr(1)): # home or ^A - self.home() - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - - elif key in ("KEY_END", "^E", chr(5)): # end or ^E - self.end() - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - - elif key in ("KEY_NPAGE",): # page_down - self.hend() - self.print_line(self.s) - - elif key in ("KEY_PPAGE",): # page_up - self.hbegin() - self.print_line(self.s) - - elif key in key_dispatch[config.cut_to_buffer_key]: # cut to buffer - self.cut_to_buffer() - return "" - - elif key in key_dispatch[config.yank_from_buffer_key]: - # yank from buffer - self.yank_from_buffer() - return "" - - elif key in key_dispatch[config.clear_word_key]: - self.cut_buffer = self.bs_word() - self.complete() - return "" - - elif key in key_dispatch[config.clear_line_key]: - self.clrtobol() - return "" - - elif key in key_dispatch[config.clear_screen_key]: - # clear all but current line - self.screen_hist: List = [self.screen_hist[-1]] - self.highlighted_paren = None - self.redraw() - return "" - - elif key in key_dispatch[config.exit_key]: - if not self.s: - self.do_exit = True - return None - else: - return "" - - elif key in key_dispatch[config.save_key]: - self.write2file() - return "" - - elif key in key_dispatch[config.pastebin_key]: - self.pastebin() - return "" - - elif key in key_dispatch[config.copy_clipboard_key]: - self.copy2clipboard() - return "" - - elif key in key_dispatch[config.last_output_key]: - page(self.stdout_hist[self.prev_block_finished : -4]) - return "" - - elif key in key_dispatch[config.show_source_key]: - try: - source = self.get_source_of_current_name() - except repl.SourceNotFound as e: - self.statusbar.message(f"{e}") - else: - if config.highlight_show_source: - source = format( - Python3Lexer().get_tokens(source), TerminalFormatter() - ) - page(source) - return "" - - elif key in ("\n", "\r", "PADENTER"): - self.lf() - return None - - elif key == "\t": - return self.tab() - - elif key == "KEY_BTAB": - return self.tab(back=True) - - elif key in key_dispatch[config.suspend_key]: - if platform.system() != "Windows": - self.suspend() - return "" - else: - self.do_exit = True - return None - - elif key == "\x18": - return self.send_current_line_to_editor() - - elif key == "\x03": - raise KeyboardInterrupt() - - elif key[0:3] == "PAD" and key not in ("PAD0", "PADSTOP"): - pad_keys = { - "PADMINUS": "-", - "PADPLUS": "+", - "PADSLASH": "/", - "PADSTAR": "*", - } - try: - self.addstr(pad_keys[key]) - self.print_line(self.s) - except KeyError: - return "" - elif len(key) == 1 and not unicodedata.category(key) == "Cc": - self.addstr(key) - self.print_line(self.s) - - else: - return "" - - return True - - def print_line( - self, s: Optional[str], clr: bool = False, newline: bool = False - ) -> None: - """Chuck a line of text through the highlighter, move the cursor - to the beginning of the line and output it to the screen.""" - - if not s: - clr = True - - if self.highlighted_paren is not None: - # Clear previous highlighted paren - - lineno = self.highlighted_paren[0] - tokens = self.highlighted_paren[1] - # mypy thinks tokens is List[Tuple[_TokenType, str]] - # but it is supposed to be MutableMapping[_TokenType, str] - self.reprint_line(lineno, tokens) - self.highlighted_paren = None - - if self.config.syntax and (not self.paste_mode or newline): - o = format(self.tokenize(s, newline), self.formatter) - else: - o = s - - self.f_string = o - self.scr.move(self.iy, self.ix) - - if clr: - self.scr.clrtoeol() - - if clr and not s: - self.scr.refresh() - - if o: - for t in o.split("\x04"): - self.echo(t.rstrip("\n")) - - if self.cpos: - t = self.cpos - for _ in range(self.cpos): - self.mvc(1) - self.cpos = t - - def prompt(self, more: Any) -> None: # I'm not sure of the type on this one - """Show the appropriate Python prompt""" - if not more: - self.echo( - "\x01{}\x03{}".format( - self.config.color_scheme["prompt"], self.ps1 - ) - ) - self.stdout_hist += self.ps1 - self.screen_hist.append( - "\x01%s\x03%s\x04" - % (self.config.color_scheme["prompt"], self.ps1) - ) - else: - prompt_more_color = self.config.color_scheme["prompt_more"] - self.echo(f"\x01{prompt_more_color}\x03{self.ps2}") - self.stdout_hist += self.ps2 - self.screen_hist.append( - f"\x01{prompt_more_color}\x03{self.ps2}\x04" - ) - - def push(self, s: str, insert_into_history: bool = True) -> bool: - # curses.raw(True) prevents C-c from causing a SIGINT - curses.raw(False) - try: - return super().push(s, insert_into_history) - except SystemExit as e: - # Avoid a traceback on e.g. quit() - self.do_exit = True - self.exit_value = e.args - return False - finally: - curses.raw(True) - - def redraw(self) -> None: - """Redraw the screen using screen_hist""" - self.scr.erase() - for k, s in enumerate(self.screen_hist): - if not s: - continue - self.iy, self.ix = self.scr.getyx() - for i in s.split("\x04"): - self.echo(i, redraw=False) - if k < len(self.screen_hist) - 1: - self.scr.addstr("\n") - self.iy, self.ix = self.scr.getyx() - self.print_line(self.s) - self.scr.refresh() - self.statusbar.refresh() - - def repl(self) -> Tuple[Any, ...]: - """Initialise the repl and jump into the loop. This method also has to - keep a stack of lines entered for the horrible "undo" feature. It also - tracks everything that would normally go to stdout in the normal Python - interpreter so it can quickly write it to stdout on exit after - curses.endwin(), as well as a history of lines entered for using - up/down to go back and forth (which has to be separate to the - evaluation history, which will be truncated when undoing.""" - - # Use our own helper function because Python's will use real stdin and - # stdout instead of our wrapped - self.push("from bpython._internal import _help as help\n", False) - - self.iy, self.ix = self.scr.getyx() - self.more = False - while not self.do_exit: - self.f_string = "" - self.prompt(self.more) - try: - inp = self.get_line() - except KeyboardInterrupt: - self.statusbar.message("KeyboardInterrupt") - self.scr.addstr("\n") - self.scr.touchwin() - self.scr.refresh() - continue - - self.scr.redrawwin() - if self.do_exit: - return self.exit_value - - self.history.append(inp) - self.screen_hist[-1] += self.f_string - self.stdout_hist += inp + "\n" - stdout_position = len(self.stdout_hist) - self.more = self.push(inp) - if not self.more: - self.prev_block_finished = stdout_position - self.s = "" - return self.exit_value - - def reprint_line( - self, lineno: int, tokens: List[Tuple[_TokenType, str]] - ) -> None: - """Helper function for paren highlighting: Reprint line at offset - `lineno` in current input buffer.""" - if not self.buffer or lineno == len(self.buffer): - return - - real_lineno = self.iy - height, width = self.scr.getmaxyx() - for i in range(lineno, len(self.buffer)): - string = self.buffer[i] - # 4 = length of prompt - length = len(string.encode(getpreferredencoding())) + 4 - real_lineno -= int(math.ceil(length / width)) - if real_lineno < 0: - return - - self.scr.move( - real_lineno, len(self.ps1) if lineno == 0 else len(self.ps2) - ) - line = format(tokens, BPythonFormatter(self.config.color_scheme)) - for string in line.split("\x04"): - self.echo(string) - - def resize(self) -> None: - """This method exists simply to keep it straight forward when - initialising a window and resizing it.""" - self.size() - self.scr.erase() - self.scr.resize(self.h, self.w) - self.scr.mvwin(self.y, self.x) - self.statusbar.resize(refresh=False) - self.redraw() - - def getstdout(self) -> str: - """This method returns the 'spoofed' stdout buffer, for writing to a - file or sending to a pastebin or whatever.""" - - return self.stdout_hist + "\n" - - def reevaluate(self) -> None: - """Clear the buffer, redraw the screen and re-evaluate the history""" - - self.evaluating = True - self.stdout_hist = "" - self.f_string = "" - self.buffer: List[str] = [] - self.scr.erase() - self.screen_hist = [] - # Set cursor position to -1 to prevent paren matching - self.cpos = -1 - - self.prompt(False) - - self.iy, self.ix = self.scr.getyx() - for line in self.history: - self.stdout_hist += line + "\n" - self.print_line(line) - self.screen_hist[-1] += self.f_string - # I decided it was easier to just do this manually - # than to make the print_line and history stuff more flexible. - self.scr.addstr("\n") - self.more = self.push(line) - self.prompt(self.more) - self.iy, self.ix = self.scr.getyx() - - self.cpos = 0 - indent = repl.next_indentation(self.s, self.config.tab_length) - self.s = "" - self.scr.refresh() - - if self.buffer: - for _ in range(indent): - self.tab() - - self.evaluating = False - # map(self.push, self.history) - # ^-- That's how simple this method was at first :( - - def write(self, s: str) -> None: - """For overriding stdout defaults""" - if "\x04" in s: - for block in s.split("\x04"): - self.write(block) - return - if s.rstrip() and "\x03" in s: - t = s.split("\x03")[1] - else: - t = s - - if not self.stdout_hist: - self.stdout_hist = t - else: - self.stdout_hist += t - - self.echo(s) - self.screen_hist.append(s.rstrip()) - - def show_list( - self, - items: List[str], - arg_pos: Union[str, int, None], - topline: Optional[inspection.FuncProps] = None, - formatter: Optional[Callable] = None, - current_item: Optional[str] = None, - ) -> None: - v_items: Collection - shared = ShowListState() - y, x = self.scr.getyx() - h, w = self.scr.getmaxyx() - down = y < h // 2 - if down: - max_h = h - y - else: - max_h = y + 1 - max_w = int(w * self.config.cli_suggestion_width) - self.list_win.erase() - - if items and formatter: - items = [formatter(x) for x in items] - if current_item is not None: - current_item = formatter(current_item) - - if topline: - height_offset = self.mkargspec(topline, arg_pos, down) + 1 - else: - height_offset = 0 - - def lsize() -> bool: - wl = max(len(i) for i in v_items) + 1 - if not wl: - wl = 1 - cols = ((max_w - 2) // wl) or 1 - rows = len(v_items) // cols - - if cols * rows < len(v_items): - rows += 1 - - if rows + 2 >= max_h: - return False - - shared.rows = rows - shared.cols = cols - shared.wl = wl - return True - - if items: - # visible items (we'll append until we can't fit any more in) - v_items = [items[0][: max_w - 3]] - lsize() - else: - v_items = [] - - for i in items[1:]: - v_items.append(i[: max_w - 3]) - if not lsize(): - del v_items[-1] - v_items[-1] = "..." - break - - rows = shared.rows - if rows + height_offset < max_h: - rows += height_offset - display_rows = rows - else: - display_rows = rows + height_offset - - cols = shared.cols - wl = shared.wl - - if topline and not v_items: - w = max_w - elif wl + 3 > max_w: - w = max_w - else: - t = (cols + 1) * wl + 3 - if t > max_w: - t = max_w - w = t - - if height_offset and display_rows + 5 >= max_h: - del v_items[-(cols * (height_offset)) :] - - if self.docstring is None: - self.list_win.resize(rows + 2, w) - else: - docstring = self.format_docstring( - self.docstring, max_w - 2, max_h - height_offset - ) - docstring_string = "".join(docstring) - rows += len(docstring) - self.list_win.resize(rows, max_w) - - if down: - self.list_win.mvwin(y + 1, 0) - else: - self.list_win.mvwin(y - rows - 2, 0) - - if v_items: - self.list_win.addstr("\n ") - - for ix, i in enumerate(v_items): - padding = (wl - len(i)) * " " - if i == current_item: - color = get_colpair(self.config, "operator") - else: - color = get_colpair(self.config, "main") - self.list_win.addstr(i + padding, color) - if (cols == 1 or (ix and not (ix + 1) % cols)) and ix + 1 < len( - v_items - ): - self.list_win.addstr("\n ") - - if self.docstring is not None: - self.list_win.addstr( - "\n" + docstring_string, get_colpair(self.config, "comment") - ) - # XXX: After all the trouble I had with sizing the list box (I'm not very good - # at that type of thing) I decided to do this bit of tidying up here just to - # make sure there's no unnecessary blank lines, it makes things look nicer. - - y = self.list_win.getyx()[0] - self.list_win.resize(y + 2, w) - - self.statusbar.win.touchwin() - self.statusbar.win.noutrefresh() - self.list_win.attron(get_colpair(self.config, "main")) - self.list_win.border() - self.scr.touchwin() - self.scr.cursyncup() - self.scr.noutrefresh() - - # This looks a little odd, but I can't figure a better way to stick the cursor - # back where it belongs (refreshing the window hides the list_win) - - self.scr.move(*self.scr.getyx()) - self.list_win.refresh() - - def size(self) -> None: - """Set instance attributes for x and y top left corner coordinates - and width and height for the window.""" - global stdscr - if stdscr: - h, w = stdscr.getmaxyx() - self.y: int = 0 - self.w: int = w - self.h: int = h - 1 - self.x: int = 0 - - def suspend(self) -> None: - """Suspend the current process for shell job control.""" - if platform.system() != "Windows": - curses.endwin() - os.kill(os.getpid(), signal.SIGSTOP) - - def tab(self, back: bool = False) -> bool: - """Process the tab key being hit. - - If there's only whitespace - in the line or the line is blank then process a normal tab, - otherwise attempt to autocomplete to the best match of possible - choices in the match list. - - If `back` is True, walk backwards through the list of suggestions - and don't indent if there are only whitespace in the line. - """ - - # 1. check if we should add a tab character - if self.atbol() and not back: - x_pos = len(self.s) - self.cpos - num_spaces = x_pos % self.config.tab_length - if not num_spaces: - num_spaces = self.config.tab_length - - self.addstr(" " * num_spaces) - self.print_line(self.s) - return True - - # 2. run complete() if we aren't already iterating through matches - if not self.matches_iter: - self.complete(tab=True) - self.print_line(self.s) - - # 3. check to see if we can expand the current word - if self.matches_iter.is_cseq(): - # TODO resolve this error-prone situation: - # can't assign at same time to self.s and self.cursor_offset - # because for cursor_offset - # property to work correctly, self.s must already be set - temp_cursor_offset, self.s = self.matches_iter.substitute_cseq() - self.cursor_offset = temp_cursor_offset - self.print_line(self.s) - if not self.matches_iter: - self.complete() - - # 4. swap current word for a match list item - elif self.matches_iter.matches: - current_match = ( - self.matches_iter.previous() - if back - else next(self.matches_iter) - ) - try: - f = None - if self.matches_iter.completer: - f = self.matches_iter.completer.format - - self.show_list( - self.matches_iter.matches, - self.arg_pos, - topline=self.funcprops, - formatter=f, - current_item=current_match, - ) - except curses.error: - # XXX: This is a massive hack, it will go away when I get - # cusswords into a good enough state that we can start - # using it. - self.list_win.border() - self.list_win.refresh() - _, self.s = self.matches_iter.cur_line() - self.print_line(self.s, True) - return True - - def undo(self, n: int = 1) -> None: - repl.Repl.undo(self, n) - - # This will unhighlight highlighted parens - self.print_line(self.s) - - def writetb(self, lines: List[str]) -> None: - for line in lines: - self.write( - "\x01{}\x03{}".format(self.config.color_scheme["error"], line) - ) - - def yank_from_buffer(self) -> None: - """Paste the text from the cut buffer at the current cursor location""" - self.addstr(self.cut_buffer) - self.print_line(self.s, clr=True) - - def send_current_line_to_editor(self) -> str: - lines = self.send_to_external_editor(self.s).split("\n") - self.s = "" - self.print_line(self.s) - while lines and not lines[-1]: - lines.pop() - if not lines: - return "" - - self.f_string = "" - self.cpos = -1 # Set cursor position to -1 to prevent paren matching - - self.iy, self.ix = self.scr.getyx() - self.evaluating = True - for line in lines: - self.stdout_hist += line + "\n" - self.history.append(line) - self.print_line(line) - self.screen_hist[-1] += self.f_string - self.scr.addstr("\n") - self.more = self.push(line) - self.prompt(self.more) - self.iy, self.ix = self.scr.getyx() - self.evaluating = False - - self.cpos = 0 - indent = repl.next_indentation(self.s, self.config.tab_length) - self.s = "" - self.scr.refresh() - - if self.buffer: - for _ in range(indent): - self.tab() - - self.print_line(self.s) - self.scr.redrawwin() - return "" - - -class Statusbar: - """This class provides the status bar at the bottom of the screen. - It has message() and prompt() methods for user interactivity, as - well as settext() and clear() methods for changing its appearance. - - The check() method needs to be called repeatedly if the statusbar is - going to be aware of when it should update its display after a message() - has been called (it'll display for a couple of seconds and then disappear). - - It should be called as: - foo = Statusbar(stdscr, scr, 'Initial text to display') - or, for a blank statusbar: - foo = Statusbar(stdscr, scr) - - It can also receive the argument 'c' which will be an integer referring - to a curses colour pair, e.g.: - foo = Statusbar(stdscr, 'Hello', c=4) - - stdscr should be a curses window object in which to put the status bar. - pwin should be the parent window. To be honest, this is only really here - so the cursor can be returned to the window properly. - - """ - - def __init__( - self, - scr: "_CursesWindow", - pwin: "_CursesWindow", - background: int, - config: Config, - s: Optional[str] = None, - c: Optional[int] = None, - ): - """Initialise the statusbar and display the initial text (if any)""" - self.size() - self.win: "_CursesWindow" = newwin( - background, self.h, self.w, self.y, self.x - ) - - self.config = config - - self.s = s or "" - self._s = self.s - self.c = c - self.timer = 0 - self.pwin = pwin - if s: - self.settext(s, c) - - def size(self) -> None: - """Set instance attributes for x and y top left corner coordinates - and width and height for the window.""" - h, w = gethw() - self.y = h - 1 - self.w = w - self.h = 1 - self.x = 0 - - def resize(self, refresh: bool = True) -> None: - """This method exists simply to keep it straight forward when - initialising a window and resizing it.""" - self.size() - self.win.mvwin(self.y, self.x) - self.win.resize(self.h, self.w) - if refresh: - self.refresh() - - def refresh(self) -> None: - """This is here to make sure the status bar text is redraw properly - after a resize.""" - self.settext(self._s) - - def check(self) -> None: - """This is the method that should be called every half second or so - to see if the status bar needs updating.""" - if not self.timer: - return - - if time.time() < self.timer: - return - - self.settext(self._s) - - def message(self, s: str, n: float = 3.0) -> None: - """Display a message for a short n seconds on the statusbar and return - it to its original state.""" - self.timer = int(time.time() + n) - self.settext(s) - - def prompt(self, s: str = "") -> str: - """Prompt the user for some input (with the optional prompt 's') and - return the input text, then restore the statusbar to its original - value.""" - - self.settext(s or "? ", p=True) - iy, ix = self.win.getyx() - - def bs(s: str) -> str: - y, x = self.win.getyx() - if x == ix: - return s - s = s[:-1] - self.win.delch(y, x - 1) - self.win.move(y, x - 1) - return s - - o = "" - while True: - c = self.win.getch() - - # '\b' - if c == 127: - o = bs(o) - # '\n' - elif c == 10: - break - # ESC - elif c == 27: - curses.flushinp() - raise ValueError - # literal - elif 0 < c < 127: - d = chr(c) - self.win.addstr(d, get_colpair(self.config, "prompt")) - o += d - - self.settext(self._s) - return o - - def settext(self, s: str, c: Optional[int] = None, p: bool = False) -> None: - """Set the text on the status bar to a new permanent value; this is the - value that will be set after a prompt or message. c is the optional - curses colour pair to use (if not specified the last specified colour - pair will be used). p is True if the cursor is expected to stay in the - status window (e.g. when prompting).""" - - self.win.erase() - if len(s) >= self.w: - s = s[: self.w - 1] - - self.s = s - if c: - self.c = c - - if s: - if self.c: - self.win.addstr(s, self.c) - else: - self.win.addstr(s) - - if not p: - self.win.noutrefresh() - self.pwin.refresh() - else: - self.win.refresh() - - def clear(self) -> None: - """Clear the status bar.""" - self.win.clear() - - -def init_wins( - scr: "_CursesWindow", config: Config -) -> Tuple["_CursesWindow", Statusbar]: - """Initialise the two windows (the main repl interface and the little - status bar at the bottom with some stuff in it)""" - # TODO: Document better what stuff is on the status bar. - - background = get_colpair(config, "background") - h, w = gethw() - - main_win = newwin(background, h - 1, w, 0, 0) - main_win.scrollok(True) - - # I think this is supposed to be True instead of 1? - main_win.keypad(1) # type:ignore[arg-type] - # Thanks to Angus Gibson for pointing out this missing line which was causing - # problems that needed dirty hackery to fix. :) - - commands = ( - (_("Rewind"), config.undo_key), - (_("Save"), config.save_key), - (_("Pastebin"), config.pastebin_key), - (_("Pager"), config.last_output_key), - (_("Show Source"), config.show_source_key), - ) - - message = " ".join( - f"<{key}> {command}" for command, key in commands if key - ) - - statusbar = Statusbar( - scr, main_win, background, config, message, get_colpair(config, "main") - ) - - return main_win, statusbar - - -def sigwinch(unused_scr: "_CursesWindow") -> None: - global DO_RESIZE - DO_RESIZE = True - - -def sigcont(unused_scr: "_CursesWindow") -> None: - sigwinch(unused_scr) - # Forces the redraw - curses.ungetch("\x00") - - -def gethw() -> Tuple[int, int]: - """I found this code on a usenet post, and snipped out the bit I needed, - so thanks to whoever wrote that, sorry I forgot your name, I'm sure you're - a great guy. - - It's unfortunately necessary (unless someone has any better ideas) in order - to allow curses and readline to work together. I looked at the code for - libreadline and noticed this comment: - - /* This is the stuff that is hard for me. I never seem to write good - display routines in C. Let's see how I do this time. */ - - So I'm not going to ask any questions. - - """ - - if platform.system() != "Windows": - h, w = struct.unpack( - "hhhh", - fcntl.ioctl( - sys.__stdout__, termios.TIOCGWINSZ, "\000" * 8 - ), # type:ignore[call-overload] - )[0:2] - else: - # Ignoring mypy's windll error because it's Windows-specific - from ctypes import ( # type:ignore[attr-defined] - windll, - create_string_buffer, - ) - - # stdin handle is -10 - # stdout handle is -11 - # stderr handle is -12 - - h = windll.kernel32.GetStdHandle(-12) - csbi = create_string_buffer(22) - res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) - - if res: - ( - bufx, - bufy, - curx, - cury, - wattr, - left, - top, - right, - bottom, - maxx, - maxy, - ) = struct.unpack("hhhhHhhhhhh", csbi.raw) - sizex = right - left + 1 - sizey = bottom - top + 1 - elif stdscr: - # can't determine actual size - return default values - sizex, sizey = stdscr.getmaxyx() - - h, w = sizey, sizex - return h, w - - -def idle(caller: CLIRepl) -> None: - """This is called once every iteration through the getkey() - loop (currently in the Repl class, see the get_line() method). - The statusbar check needs to go here to take care of timed - messages and the resize handlers need to be here to make - sure it happens conveniently.""" - global DO_RESIZE - - if caller.module_gatherer.find_coroutine() or caller.paste_mode: - caller.scr.nodelay(True) - key = caller.scr.getch() - caller.scr.nodelay(False) - if key != -1: - curses.ungetch(key) - else: - curses.ungetch("\x00") - caller.statusbar.check() - caller.check() - - if DO_RESIZE: - do_resize(caller) - - -def do_resize(caller: CLIRepl) -> None: - """This needs to hack around readline and curses not playing - nicely together. See also gethw() above.""" - global DO_RESIZE - h, w = gethw() - if not h: - # Hopefully this shouldn't happen. :) - return - - curses.endwin() - os.environ["LINES"] = str(h) - os.environ["COLUMNS"] = str(w) - curses.doupdate() - DO_RESIZE = False - - try: - caller.resize() - except curses.error: - pass - # The list win resizes itself every time it appears so no need to do it here. - - -class FakeDict: - """Very simple dict-alike that returns a constant value for any key - - used as a hacky solution to using a colours dict containing colour codes if - colour initialisation fails.""" - - def __init__(self, val: int): - self._val = val - - def __getitem__(self, k: Any) -> int: - return self._val - - -def newwin(background: int, *args: int) -> "_CursesWindow": - """Wrapper for curses.newwin to automatically set background colour on any - newly created window.""" - win = curses.newwin(*args) - win.bkgd(" ", background) - return win - - -def curses_wrapper(func: Callable, *args: Any, **kwargs: Any) -> Any: - """Like curses.wrapper(), but reuses stdscr when called again.""" - global stdscr - if stdscr is None: - stdscr = curses.initscr() - try: - curses.noecho() - curses.cbreak() - # Should this be keypad(True)? - stdscr.keypad(1) # type:ignore[arg-type] - - try: - curses.start_color() - except curses.error: - pass - - return func(stdscr, *args, **kwargs) - finally: - # Should this be keypad(False)? - stdscr.keypad(0) # type:ignore[arg-type] - curses.echo() - curses.nocbreak() - curses.endwin() - - -def main_curses( - scr: "_CursesWindow", - args: List[str], - config: Config, - interactive: bool = True, - locals_: Optional[Dict[str, Any]] = None, - banner: Optional[str] = None, -) -> Tuple[Tuple[Any, ...], str]: - """main function for the curses convenience wrapper - - Initialise the two main objects: the interpreter - and the repl. The repl does what a repl does and lots - of other cool stuff like syntax highlighting and stuff. - I've tried to keep it well factored but it needs some - tidying up, especially in separating the curses stuff - from the rest of the repl. - - Returns a tuple (exit value, output), where exit value is a tuple - with arguments passed to SystemExit. - """ - global stdscr - global DO_RESIZE - global colors - DO_RESIZE = False - - if platform.system() != "Windows": - old_sigwinch_handler = signal.signal( - signal.SIGWINCH, lambda *_: sigwinch(scr) - ) - # redraw window after being suspended - old_sigcont_handler = signal.signal( - signal.SIGCONT, lambda *_: sigcont(scr) - ) - - stdscr = scr - try: - curses.start_color() - curses.use_default_colors() - cols = make_colors(config) - except curses.error: - # Not sure what to do with the types here... - # FakeDict acts as a dictionary, but isn't actually a dictionary - cols = FakeDict(-1) # type:ignore[assignment] - - # FIXME: Gargh, bad design results in using globals without a refactor :( - colors = cols - - scr.timeout(300) - - curses.raw(True) - main_win, statusbar = init_wins(scr, config) - - interpreter = repl.Interpreter(locals_) - - clirepl = CLIRepl(main_win, interpreter, statusbar, config, idle) - clirepl._C = cols - - # Not sure how to type these Fake types - sys.stdin = FakeStdin(clirepl) # type:ignore[assignment] - sys.stdout = FakeStream(clirepl, lambda: sys.stdout) # type:ignore - sys.stderr = FakeStream(clirepl, lambda: sys.stderr) # type:ignore - - if args: - exit_value: Tuple[Any, ...] = () - try: - bpargs.exec_code(interpreter, args) - except SystemExit as e: - # The documentation of code.InteractiveInterpreter.runcode claims - # that it reraises SystemExit. However, I can't manage to trigger - # that. To be one the safe side let's catch SystemExit here anyway. - exit_value = e.args - if not interactive: - curses.raw(False) - return (exit_value, clirepl.getstdout()) - else: - sys.path.insert(0, "") - try: - clirepl.startup() - except OSError as e: - # Handle this with a proper error message. - if e.errno != errno.ENOENT: - raise - - if banner is not None: - clirepl.write(banner) - clirepl.write("\n") - - # XXX these deprecation warnings need to go at some point - clirepl.write( - _( - "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. This backend has been deprecated in version 0.19 and might disappear in a future version." - ) - ) - clirepl.write("\n") - - exit_value = clirepl.repl() - if hasattr(sys, "exitfunc"): - # Seems like the if statement should satisfy mypy, but it doesn't - sys.exitfunc() # type:ignore[attr-defined] - delattr(sys, "exitfunc") - - main_win.erase() - main_win.refresh() - statusbar.win.clear() - statusbar.win.refresh() - curses.raw(False) - - # Restore signal handlers - if platform.system() != "Windows": - signal.signal(signal.SIGWINCH, old_sigwinch_handler) - signal.signal(signal.SIGCONT, old_sigcont_handler) - - return (exit_value, clirepl.getstdout()) - - -def main( - args: Optional[List[str]] = None, - locals_: Optional[MutableMapping[str, str]] = None, - banner: Optional[str] = None, -) -> Any: - translations.init() - - config, options, exec_args = argsparse(args) - - # Save stdin, stdout and stderr for later restoration - orig_stdin = sys.stdin - orig_stdout = sys.stdout - orig_stderr = sys.stderr - - try: - (exit_value, output) = curses_wrapper( - main_curses, - exec_args, - config, - options.interactive, - locals_, - banner=banner, - ) - finally: - sys.stdin = orig_stdin - sys.stderr = orig_stderr - sys.stdout = orig_stdout - - # Fake stdout data so everything's still visible after exiting - if config.flush_output and not options.quiet: - sys.stdout.write(output) - if hasattr(sys.stdout, "flush"): - sys.stdout.flush() - return repl.extract_exit_value(exit_value) - - -if __name__ == "__main__": - sys.exit(main()) - -# vim: sw=4 ts=4 sts=4 ai et diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index b3e8912f..74f4b721 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -10,7 +10,7 @@ from pathlib import Path from unittest import mock -from bpython import config, repl, cli, autocomplete +from bpython import config, repl, autocomplete from bpython.line import LinePart from bpython.test import ( MagicIterMock, @@ -67,13 +67,6 @@ def reevaluate(self): raise NotImplementedError -class FakeCliRepl(cli.CLIRepl, FakeRepl): - def __init__(self): - self.s = "" - self.cpos = 0 - self.rl_history = FakeHistory() - - class TestMatchesIterator(unittest.TestCase): def setUp(self): self.matches = ["bobby", "bobbies", "bobberina"] @@ -539,148 +532,5 @@ def __init__(self, *args, **kwargs): self.assertEqual(self.repl.matches_iter.matches, ["apple2=", "apple="]) -class TestCliRepl(unittest.TestCase): - def setUp(self): - self.repl = FakeCliRepl() - - def test_atbol(self): - self.assertTrue(self.repl.atbol()) - - self.repl.s = "\t\t" - self.assertTrue(self.repl.atbol()) - - self.repl.s = "\t\tnot an empty line" - self.assertFalse(self.repl.atbol()) - - def test_addstr(self): - self.repl.complete = mock.Mock(True) - - self.repl.s = "foo" - self.repl.addstr("bar") - self.assertEqual(self.repl.s, "foobar") - - self.repl.cpos = 3 - self.repl.addstr("buzz") - self.assertEqual(self.repl.s, "foobuzzbar") - - -class TestCliReplTab(unittest.TestCase): - def setUp(self): - self.repl = FakeCliRepl() - - # 3 Types of tab complete - def test_simple_tab_complete(self): - self.repl.matches_iter = MagicIterMock() - self.repl.matches_iter.__bool__.return_value = False - self.repl.complete = mock.Mock() - self.repl.print_line = mock.Mock() - self.repl.matches_iter.is_cseq.return_value = False - self.repl.show_list = mock.Mock() - self.repl.funcprops = mock.Mock() - self.repl.arg_pos = mock.Mock() - self.repl.matches_iter.cur_line.return_value = (None, "foobar") - - self.repl.s = "foo" - self.repl.tab() - self.assertTrue(self.repl.complete.called) - self.repl.complete.assert_called_with(tab=True) - self.assertEqual(self.repl.s, "foobar") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_substring_tab_complete(self): - self.repl.s = "bar" - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.FUZZY - ) - self.repl.tab() - self.assertEqual(self.repl.s, "foobar") - self.repl.tab() - self.assertEqual(self.repl.s, "foofoobar") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_fuzzy_tab_complete(self): - self.repl.s = "br" - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.FUZZY - ) - self.repl.tab() - self.assertEqual(self.repl.s, "foobar") - - # Edge Cases - def test_normal_tab(self): - """make sure pressing the tab key will - still in some cases add a tab""" - self.repl.s = "" - self.repl.config = mock.Mock() - self.repl.config.tab_length = 4 - self.repl.complete = mock.Mock() - self.repl.print_line = mock.Mock() - self.repl.tab() - self.assertEqual(self.repl.s, " ") - - def test_back_parameter(self): - self.repl.matches_iter = mock.Mock() - self.repl.matches_iter.matches = True - self.repl.matches_iter.previous.return_value = "previtem" - self.repl.matches_iter.is_cseq.return_value = False - self.repl.show_list = mock.Mock() - self.repl.funcprops = mock.Mock() - self.repl.arg_pos = mock.Mock() - self.repl.matches_iter.cur_line.return_value = (None, "previtem") - self.repl.print_line = mock.Mock() - self.repl.s = "foo" - self.repl.cpos = 0 - self.repl.tab(back=True) - self.assertTrue(self.repl.matches_iter.previous.called) - self.assertTrue(self.repl.s, "previtem") - - # Attribute Tests - @unittest.skip("disabled while non-simple completion is disabled") - def test_fuzzy_attribute_tab_complete(self): - """Test fuzzy attribute with no text""" - self.repl.s = "Foo." - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.FUZZY - ) - - self.repl.tab() - self.assertEqual(self.repl.s, "Foo.foobar") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_fuzzy_attribute_tab_complete2(self): - """Test fuzzy attribute with some text""" - self.repl.s = "Foo.br" - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.FUZZY - ) - - self.repl.tab() - self.assertEqual(self.repl.s, "Foo.foobar") - - # Expand Tests - def test_simple_expand(self): - self.repl.s = "f" - self.cpos = 0 - self.repl.matches_iter = mock.Mock() - self.repl.matches_iter.is_cseq.return_value = True - self.repl.matches_iter.substitute_cseq.return_value = (3, "foo") - self.repl.print_line = mock.Mock() - self.repl.tab() - self.assertEqual(self.repl.s, "foo") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_substring_expand_forward(self): - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.SUBSTRING - ) - self.repl.s = "ba" - self.repl.tab() - self.assertEqual(self.repl.s, "bar") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_fuzzy_expand(self): - pass - - if __name__ == "__main__": unittest.main() diff --git a/bpython/translations/bpython.pot b/bpython/translations/bpython.pot index e11140ed..c6957291 100644 --- a/bpython/translations/bpython.pot +++ b/bpython/translations/bpython.pot @@ -61,42 +61,14 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/de/LC_MESSAGES/bpython.po b/bpython/translations/de/LC_MESSAGES/bpython.po index 79b3acf7..feb534f7 100644 --- a/bpython/translations/de/LC_MESSAGES/bpython.po +++ b/bpython/translations/de/LC_MESSAGES/bpython.po @@ -67,45 +67,15 @@ msgstr "" "Auszuführende Datei und zusätzliche Argumente, die an das Script " "übergeben werden sollen." -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "j" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "ja" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "Rückgängig" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "Speichern" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "Pastebin" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "Quellcode anzeigen" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" -"ACHTUNG: `bpython-cli` wird verwendet, die curses Implementierung von " -"`bpython`. Diese Implementierung wird ab Version 0.19 nicht mehr aktiv " -"unterstützt und wird in einer zukünftigen Version entfernt werden." - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/es_ES/LC_MESSAGES/bpython.po b/bpython/translations/es_ES/LC_MESSAGES/bpython.po index 5af25b37..d3487281 100644 --- a/bpython/translations/es_ES/LC_MESSAGES/bpython.po +++ b/bpython/translations/es_ES/LC_MESSAGES/bpython.po @@ -62,42 +62,15 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "s" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "si" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po index 32bbec66..ba120504 100644 --- a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po +++ b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po @@ -66,42 +66,15 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "o" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "oui" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "Rembobiner" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "Sauvegarder" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "Montrer le code source" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/it_IT/LC_MESSAGES/bpython.po b/bpython/translations/it_IT/LC_MESSAGES/bpython.po index d0076cff..46488bc3 100644 --- a/bpython/translations/it_IT/LC_MESSAGES/bpython.po +++ b/bpython/translations/it_IT/LC_MESSAGES/bpython.po @@ -62,42 +62,15 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "s" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "si" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po index d110f3ba..375f4f32 100644 --- a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po +++ b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po @@ -62,42 +62,15 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "j" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "ja" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/doc/sphinx/source/tips.rst b/doc/sphinx/source/tips.rst index f2519b40..4bfbc2e4 100644 --- a/doc/sphinx/source/tips.rst +++ b/doc/sphinx/source/tips.rst @@ -16,7 +16,7 @@ equivalent file. .. code-block:: bash - alias bpython3.5='PYTHONPATH=~/python/bpython python3.5 -m bpython.cli' + alias bpython3.5='PYTHONPATH=~/python/bpython python3.5 -m bpython.curtsies' Where the `~/python/bpython`-path is the path to where your bpython source code resides. diff --git a/doc/sphinx/source/windows.rst b/doc/sphinx/source/windows.rst index 6d2c05a0..5374f70f 100644 --- a/doc/sphinx/source/windows.rst +++ b/doc/sphinx/source/windows.rst @@ -7,9 +7,3 @@ other platforms as well. There are no official binaries for bpython on Windows (though this is something we plan on providing in the future). - -The easiest way to get `bpython.cli` (the curses frontend running) is to install -an unofficial windows binary for pdcurses from: -http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses. After this you can just -`pip install bpython` and run bpython curses frontend like you would on a Linux -system (e.g. by typing `bpython-curses` on your prompt). diff --git a/setup.cfg b/setup.cfg index 8c4294d9..c0c5d03e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,6 @@ watch = watchdog [options.entry_points] console_scripts = bpython = bpython.curtsies:main - bpython-curses = bpython.cli:main bpython-urwid = bpython.urwid:main [urwid] bpdb = bpdb:main From 5aa1989a8ed299aac155b24c4a4b4adf0542cc17 Mon Sep 17 00:00:00 2001 From: supakeen Date: Tue, 9 Nov 2021 17:46:04 +0000 Subject: [PATCH 061/113] Refer directly to the top-level package in docs. --- doc/sphinx/source/tips.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/source/tips.rst b/doc/sphinx/source/tips.rst index 4bfbc2e4..0745e3bf 100644 --- a/doc/sphinx/source/tips.rst +++ b/doc/sphinx/source/tips.rst @@ -16,7 +16,7 @@ equivalent file. .. code-block:: bash - alias bpython3.5='PYTHONPATH=~/python/bpython python3.5 -m bpython.curtsies' + alias bpython3.5='PYTHONPATH=~/python/bpython python3.5 -m bpython' Where the `~/python/bpython`-path is the path to where your bpython source code resides. From acffa531199c090baa2d512851b1cedd432fefe5 Mon Sep 17 00:00:00 2001 From: supakeen Date: Tue, 9 Nov 2021 17:47:08 +0000 Subject: [PATCH 062/113] Bit too heavy handed on the translations. --- bpython/translations/bpython.pot | 1 + 1 file changed, 1 insertion(+) diff --git a/bpython/translations/bpython.pot b/bpython/translations/bpython.pot index c6957291..9237869d 100644 --- a/bpython/translations/bpython.pot +++ b/bpython/translations/bpython.pot @@ -61,6 +61,7 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "" From 26fc2b580c774c824d1f234b32a3be0f404db6be Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 1 May 2023 10:33:26 +0200 Subject: [PATCH 063/113] Require Sphinx < 7 for now Removal of the setuptools integration from Sphinx breaks our builds. See https://github.com/sphinx-doc/sphinx/pull/11363 for the change in Sphinx. --- .github/workflows/build.yaml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1e2a374a..9254d49c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -32,7 +32,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" + pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5,<7" pip install pytest pytest-cov numpy - name: Build with Python ${{ matrix.python-version }} run: | diff --git a/setup.py b/setup.py index 6790b9d7..7ca279d3 100755 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ from sphinx.setup_command import BuildDoc # Sphinx 1.5 and newer support Python 3.6 - using_sphinx = sphinx.__version__ >= "1.5" + using_sphinx = sphinx.__version__ >= "1.5" and sphinx.__version__ < "7.0" except ImportError: using_sphinx = False From db6a559724a59713ac905d7b0533ef382fab930d Mon Sep 17 00:00:00 2001 From: Jochen Kupperschmidt Date: Tue, 11 Jul 2023 23:24:07 +0200 Subject: [PATCH 064/113] Explicitly set README content type The README, being a reStructuredText document, is not rendered as such on PyPI, just as plaintext. This *should* fix that. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index c0c5d03e..1fe4a6f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,7 @@ [metadata] name = bpython long_description = file: README.rst +long_description_content_type = text/x-rst license = MIT license_files = LICENSE author = Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al. From d58e392f188118b3f6d3c964041492dbb2cd3694 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 9 Jul 2023 16:52:27 +0200 Subject: [PATCH 065/113] Fix __signature__ support if object has a __file__ --- bpython/inspection.py | 10 +++++++--- bpython/test/test_repl.py | 19 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index fe1e3a0a..2b734cdf 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -289,9 +289,13 @@ def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: return None try: argspec = _get_argspec_from_signature(f) - fprops = FuncProps( - func, _fix_default_values(f, argspec), is_bound_method - ) + try: + argspec = _fix_default_values(f, argspec) + except KeyError as ex: + # Parsing of the source failed. If f has a __signature__, we trust it. + if not hasattr(f, "__signature__"): + raise ex + fprops = FuncProps(func, argspec, is_bound_method) except (TypeError, KeyError, ValueError): argspec_pydoc = _getpydocspec(f) if argspec_pydoc is None: diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 74f4b721..8c3b85cc 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -1,5 +1,6 @@ import collections import inspect +import os import socket import sys import tempfile @@ -523,13 +524,19 @@ def __init__(self, *args, **kwargs): inspect.Parameter("pinetree", inspect.Parameter.KEYWORD_ONLY), ]) """ - for line in code.split("\n"): - print(line[8:]) - self.repl.push(line[8:]) + code = [x[8:] for x in code.split("\n")] + for line in code: + self.repl.push(line) - self.assertTrue(self.repl.complete()) - self.assertTrue(hasattr(self.repl.matches_iter, "matches")) - self.assertEqual(self.repl.matches_iter.matches, ["apple2=", "apple="]) + with mock.patch( + "bpython.inspection.inspect.getsourcelines", + return_value=(code, None), + ): + self.assertTrue(self.repl.complete()) + self.assertTrue(hasattr(self.repl.matches_iter, "matches")) + self.assertEqual( + self.repl.matches_iter.matches, ["apple2=", "apple="] + ) if __name__ == "__main__": From 7bf93f510752e36cb72fdfd17d7db46b48e438b9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 20 Jul 2023 22:44:42 +0200 Subject: [PATCH 066/113] Fix handling of SystemExit arguments (fixes #995) --- bpython/curtsiesfrontend/coderunner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/coderunner.py b/bpython/curtsiesfrontend/coderunner.py index cddf1169..f059fab8 100644 --- a/bpython/curtsiesfrontend/coderunner.py +++ b/bpython/curtsiesfrontend/coderunner.py @@ -52,7 +52,7 @@ class Unfinished(RequestFromCodeRunner): class SystemExitRequest(RequestFromCodeRunner): """Running code raised a SystemExit""" - def __init__(self, args): + def __init__(self, *args): self.args = args From db6c9bdf6d9a1d128f04ffd053591b2e1289b08a Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 22 Jul 2023 16:48:00 +0200 Subject: [PATCH 067/113] Complete parameters without input --- bpython/autocomplete.py | 7 ++++++- bpython/test/test_autocomplete.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index e0849c6d..10f039d2 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -604,7 +604,12 @@ def matches( return matches if matches else None def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: - return lineparts.current_word(cursor_offset, line) + r = lineparts.current_word(cursor_offset, line) + if r and r.word[-1] == "(": + # if the word ends with a (, it's the parent word with an empty + # param. Return an empty word + return lineparts.LinePart(r.stop, r.stop, "") + return r class ExpressionAttributeCompletion(AttrCompletion): diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 0000b0b6..2bbd90b3 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -435,3 +435,7 @@ def func(apple, apricot, banana, carrot): self.assertSetEqual( com.matches(3, "car", funcprops=funcspec), {"carrot="} ) + self.assertSetEqual( + com.matches(5, "func(", funcprops=funcspec), + {"apple=", "apricot=", "banana=", "carrot="}, + ) From de333118a7dc89925c7e3d1f246271b814a4e37c Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 22 Jul 2023 16:49:55 +0200 Subject: [PATCH 068/113] Better completion results order --- bpython/autocomplete.py | 14 +++++++++++++- bpython/test/test_autocomplete.py | 9 +++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 10f039d2..73759992 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -747,6 +747,16 @@ def get_completer( double underscore methods like __len__ in method signatures """ + def _cmpl_sort(x: str) -> Tuple[Any, ...]: + """ + Function used to sort the matches. + """ + # put parameters above everything in completion + return ( + x[-1] != "=", + x, + ) + for completer in completers: try: matches = completer.matches( @@ -765,7 +775,9 @@ def get_completer( ) continue if matches is not None: - return sorted(matches), (completer if matches else None) + return sorted(matches, key=_cmpl_sort), ( + completer if matches else None + ) return [], None diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 2bbd90b3..da32fbb8 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -106,6 +106,15 @@ def test_two_completers_get_both(self): cumulative = autocomplete.CumulativeCompleter([a, b]) self.assertEqual(cumulative.matches(3, "abc"), {"a", "b"}) + def test_order_completer(self): + a = self.completer(["ax", "ab="]) + b = self.completer(["aa"]) + cumulative = autocomplete.CumulativeCompleter([a, b]) + self.assertEqual( + autocomplete.get_completer([cumulative], 1, "a"), + (["ab=", "aa", "ax"], cumulative), + ) + class TestFilenameCompletion(unittest.TestCase): def setUp(self): From 77bed91c9ed6b242f1efeb68047ffb43172133d3 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 26 Jul 2023 09:31:46 +0200 Subject: [PATCH 069/113] Apply suggestion --- bpython/autocomplete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 73759992..a36c7beb 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -747,7 +747,7 @@ def get_completer( double underscore methods like __len__ in method signatures """ - def _cmpl_sort(x: str) -> Tuple[Any, ...]: + def _cmpl_sort(x: str) -> Tuple[bool, ...]: """ Function used to sort the matches. """ From be21521902cda73cde331480b99c5709d757a9ae Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 13 Jul 2023 14:17:34 +0200 Subject: [PATCH 070/113] Fix argument description --- bpython/args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/args.py b/bpython/args.py index b9e68e1d..d1037e1c 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -131,7 +131,7 @@ def callback(group): "--quiet", "-q", action="store_true", - help=_("Don't flush the output to stdout."), + help=_("Don't print version banner."), ) parser.add_argument( "--version", From 1db0436babf5c964a954f875e481bc9dc686d104 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Jul 2023 09:46:46 +0200 Subject: [PATCH 071/113] Fix type annotation --- bpython/autocomplete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index a36c7beb..000fbde9 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -747,7 +747,7 @@ def get_completer( double underscore methods like __len__ in method signatures """ - def _cmpl_sort(x: str) -> Tuple[bool, ...]: + def _cmpl_sort(x: str) -> Tuple[bool, str]: """ Function used to sort the matches. """ From 590507b57f269ae33cfaadc2cafa1685f0c538e7 Mon Sep 17 00:00:00 2001 From: Mikolaj Klikowicz Date: Fri, 11 Aug 2023 13:02:07 +0200 Subject: [PATCH 072/113] Handle AttributeError Signed-off-by: Mikolaj Klikowicz --- bpython/inspection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 2b734cdf..e97a272b 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -104,13 +104,13 @@ def __enter__(self) -> None: if __getattr__ is not None: try: setattr(type_, "__getattr__", (lambda *_, **__: None)) - except TypeError: + except (TypeError, AttributeError): __getattr__ = None __getattribute__ = getattr(type_, "__getattribute__", None) if __getattribute__ is not None: try: setattr(type_, "__getattribute__", object.__getattribute__) - except TypeError: + except (TypeError, AttributeError): # XXX: This happens for e.g. built-in types __getattribute__ = None self._attribs = (__getattribute__, __getattr__) From f64677ffd9ed0446506c84e6d76e9d7272cc06ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 04:47:50 +0000 Subject: [PATCH 073/113] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- .github/workflows/lint.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9254d49c..a55ee36c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -21,7 +21,7 @@ jobs: - "3.11" - "pypy-3.8" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index f644f543..d02dd2df 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -8,7 +8,7 @@ jobs: black: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 - name: Install dependencies @@ -21,7 +21,7 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: codespell-project/actions-codespell@master with: skip: '*.po' @@ -30,7 +30,7 @@ jobs: mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 - name: Install dependencies From 7c9e8513c01b7ea77b44d5c5a8e94dcf15ccb8a7 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 Oct 2023 10:45:08 +0200 Subject: [PATCH 074/113] CI: test with Python 3.12 --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a55ee36c..ffb1c2ae 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -19,6 +19,7 @@ jobs: - "3.9" - "3.10" - "3.11" + - "3.12" - "pypy-3.8" steps: - uses: actions/checkout@v4 From cafa87c947d6e25dadd61dfa1175df953bf5445d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 11 Oct 2023 18:25:48 +0200 Subject: [PATCH 075/113] Do not fail if modules don't have __version__ (fixes #1001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … even if they should. --- bpython/args.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index d1037e1c..51cc3d25 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -37,6 +37,7 @@ import sys from pathlib import Path from typing import Tuple, List, Optional, NoReturn, Callable +from types import ModuleType from . import __version__, __copyright__ from .config import default_config_path, Config @@ -67,6 +68,13 @@ def copyright_banner() -> str: return _("{} See AUTHORS.rst for details.").format(__copyright__) +def log_version(module: ModuleType, name: str) -> None: + try: + logger.info("%s: %s", name, module.__version__) # type: ignore + except AttributeError: + logger.info("%s: unknown version", name) + + Options = Tuple[str, str, Callable[[argparse._ArgumentGroup], None]] @@ -211,27 +219,27 @@ def callback(group): try: import curtsies - logger.info("curtsies: %s", curtsies.__version__) + log_version(curtsies, "curtsies") except ImportError: # may happen on Windows logger.info("curtsies: not available") - logger.info("cwcwidth: %s", cwcwidth.__version__) - logger.info("greenlet: %s", greenlet.__version__) - logger.info("pygments: %s", pygments.__version__) # type: ignore - logger.info("pyxdg: %s", xdg.__version__) # type: ignore - logger.info("requests: %s", requests.__version__) + log_version(cwcwidth, "cwcwidth") + log_version(greenlet, "greenlet") + log_version(pygments, "pygments") + log_version(xdg, "pyxdg") + log_version(requests, "requests") # versions of optional dependencies try: import pyperclip - logger.info("pyperclip: %s", pyperclip.__version__) # type: ignore + log_version(pyperclip, "pyperclip") except ImportError: logger.info("pyperclip: not available") try: import jedi - logger.info("jedi: %s", jedi.__version__) + log_version(jedi, "jedi") except ImportError: logger.info("jedi: not available") try: From 3a440274be26338e873e3e6144e9e20116ef1d4a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 11 Oct 2023 18:42:18 +0200 Subject: [PATCH 076/113] Avoid the use of Exceptions for logic --- bpython/args.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 51cc3d25..ed0b0055 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -69,10 +69,7 @@ def copyright_banner() -> str: def log_version(module: ModuleType, name: str) -> None: - try: - logger.info("%s: %s", name, module.__version__) # type: ignore - except AttributeError: - logger.info("%s: unknown version", name) + logger.info("%s: %s", name, module.__version__ if hasattr(module, "__version__") else "unknown version") # type: ignore Options = Tuple[str, str, Callable[[argparse._ArgumentGroup], None]] From 3fda3af0df1a92c5794c6b396501c97647853222 Mon Sep 17 00:00:00 2001 From: Joan Lucas Date: Mon, 27 Nov 2023 20:59:26 -0300 Subject: [PATCH 077/113] Added functions return typing in some files: __init__.py; keys.py and lazyre.py --- bpython/__init__.py | 3 ++- bpython/keys.py | 4 ++-- bpython/lazyre.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bpython/__init__.py b/bpython/__init__.py index dff06c0f..8c7bfb37 100644 --- a/bpython/__init__.py +++ b/bpython/__init__.py @@ -21,6 +21,7 @@ # THE SOFTWARE. import os.path +from typing import Any try: from ._version import __version__ as version # type: ignore @@ -36,7 +37,7 @@ package_dir = os.path.abspath(os.path.dirname(__file__)) -def embed(locals_=None, args=None, banner=None): +def embed(locals_=None, args=None, banner=None) -> Any: if args is None: args = ["-i", "-q"] diff --git a/bpython/keys.py b/bpython/keys.py index cfcac86b..fe27dbcc 100644 --- a/bpython/keys.py +++ b/bpython/keys.py @@ -42,10 +42,10 @@ def __getitem__(self, key: str) -> T: f"Configured keymap ({key}) does not exist in bpython.keys" ) - def __delitem__(self, key: str): + def __delitem__(self, key: str) -> None: del self.map[key] - def __setitem__(self, key: str, value: T): + def __setitem__(self, key: str, value: T) -> None: self.map[key] = value diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 0ca5b9ff..8d166b74 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -21,7 +21,7 @@ # THE SOFTWARE. import re -from typing import Optional, Pattern, Match, Optional +from typing import Optional, Pattern, Match, Optional, Iterator try: from functools import cached_property @@ -43,7 +43,7 @@ def __init__(self, regex: str, flags: int = 0) -> None: def compiled(self) -> Pattern[str]: return re.compile(self.regex, self.flags) - def finditer(self, *args, **kwargs): + def finditer(self, *args, **kwargs) -> Iterator[Match[str]]: return self.compiled.finditer(*args, **kwargs) def search(self, *args, **kwargs) -> Optional[Match[str]]: From f200dac951cee8c8d0f15abd2570eb13e58f729b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 07:38:27 +0000 Subject: [PATCH 078/113] Bump actions/setup-python from 4 to 5 (#1004) --- .github/workflows/build.yaml | 2 +- .github/workflows/lint.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ffb1c2ae..864c03cc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -26,7 +26,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d02dd2df..d960c6d8 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 - name: Install dependencies run: | python -m pip install --upgrade pip From ca13bf5727d91d3208e84297c2829aa8474b4620 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 08:56:25 +0100 Subject: [PATCH 079/113] Bump codecov/codecov-action from 3 to 4 (#1006) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 864c03cc..37128f24 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -46,7 +46,7 @@ jobs: run: | pytest --cov=bpython --cov-report=xml -v - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 env: PYTHON_VERSION: ${{ matrix.python-version }} with: From 0f238b7590037c85d7b19059ee8b41cd1d1f08f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gr=C3=A4nitz?= Date: Mon, 29 Apr 2024 13:30:34 +0200 Subject: [PATCH 080/113] Fix simplerepl demo: missing arg for BaseRepl init (#1017) Default value was dropped in 8d16a71ef404db66d2c6fae6c362640da8ae240d --- doc/sphinx/source/simplerepl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/source/simplerepl.py b/doc/sphinx/source/simplerepl.py index 8a8dda74..8496f0dd 100644 --- a/doc/sphinx/source/simplerepl.py +++ b/doc/sphinx/source/simplerepl.py @@ -42,7 +42,7 @@ class SimpleRepl(BaseRepl): def __init__(self, config): self.requested_events = [] - BaseRepl.__init__(self, config) + BaseRepl.__init__(self, config, window=None) def _request_refresh(self): self.requested_events.append(bpythonevents.RefreshRequestEvent()) From 925b733e5e456daf0516f3ff9ac3877e689fd436 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 25 Apr 2024 13:57:35 +0100 Subject: [PATCH 081/113] Import build from setuptools --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7ca279d3..02194088 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup -from distutils.command.build import build +from setuptools.command.build import build try: from babel.messages import frontend as babel From ded2d7fe3fca3df64de1938c1dcf7638ec900227 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 25 Apr 2024 13:58:16 +0100 Subject: [PATCH 082/113] Avoid patching the original build class attribute --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 02194088..0cceb940 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup -from setuptools.command.build import build +from setuptools.command.build import build as _orig_build try: from babel.messages import frontend as babel @@ -122,6 +122,11 @@ def git_describe_to_python_version(version): vf.write(f'__version__ = "{version}"\n') +class build(_orig_build): + # Avoid patching the original class' attribute (more robust customisation) + sub_commands = _orig_build.sub_commands[:] + + cmdclass = {"build": build} translations_dir = os.path.join("bpython", "translations") From ac7c11ad850a8ca649a48f26eef0b2c59f204f3e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 25 Apr 2024 14:13:23 +0100 Subject: [PATCH 083/113] Bump setuptools version Reliably import `setuptools.command.build`. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7bd3196..924722b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] requires = [ - "setuptools >= 43", + "setuptools >= 62.4.0", ] build-backend = "setuptools.build_meta" From a9b1324ad535774727545896ec54515e527423ab Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 30 Apr 2024 14:49:13 +0100 Subject: [PATCH 084/113] Bump setuptools in requirement.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4c750a69..cc8fbff8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ cwcwidth greenlet pyxdg requests -setuptools \ No newline at end of file +setuptools>=62.4.0 From d54061317d767c64eb2d466fa14ca56bef5bb9eb Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 21:47:54 +0200 Subject: [PATCH 085/113] Apply black --- bpython/curtsies.py | 15 ++++++--------- bpython/curtsiesfrontend/repl.py | 8 +++++--- bpython/curtsiesfrontend/replpainter.py | 8 +++++--- bpython/paste.py | 3 +-- bpython/test/test_preprocess.py | 4 ++-- bpython/urwid.py | 1 - 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 6dc8d1f7..11b96050 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -40,14 +40,11 @@ class SupportsEventGeneration(Protocol): def send( self, timeout: Optional[float] - ) -> Union[str, curtsies.events.Event, None]: - ... + ) -> Union[str, curtsies.events.Event, None]: ... - def __iter__(self) -> "SupportsEventGeneration": - ... + def __iter__(self) -> "SupportsEventGeneration": ... - def __next__(self) -> Union[str, curtsies.events.Event, None]: - ... + def __next__(self) -> Union[str, curtsies.events.Event, None]: ... class FullCurtsiesRepl(BaseRepl): @@ -69,9 +66,9 @@ def __init__( extra_bytes_callback=self.input_generator.unget_bytes, ) - self._request_refresh_callback: Callable[ - [], None - ] = self.input_generator.event_trigger(events.RefreshRequestEvent) + self._request_refresh_callback: Callable[[], None] = ( + self.input_generator.event_trigger(events.RefreshRequestEvent) + ) self._schedule_refresh_callback = ( self.input_generator.scheduled_event_trigger( events.ScheduledRefreshRequestEvent diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index e4819e19..302e67d4 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1801,9 +1801,11 @@ def move_screen_up(current_line_start_row): self.current_match, self.docstring, self.config, - self.matches_iter.completer.format - if self.matches_iter.completer - else None, + ( + self.matches_iter.completer.format + if self.matches_iter.completer + else None + ), ) if ( diff --git a/bpython/curtsiesfrontend/replpainter.py b/bpython/curtsiesfrontend/replpainter.py index 00675451..3b63ca4c 100644 --- a/bpython/curtsiesfrontend/replpainter.py +++ b/bpython/curtsiesfrontend/replpainter.py @@ -74,9 +74,11 @@ def matches_lines(rows, columns, matches, current, config, match_format): result = [ fmtstr(" ").join( - color(m.ljust(max_match_width)) - if m != current - else highlight_color(m.ljust(max_match_width)) + ( + color(m.ljust(max_match_width)) + if m != current + else highlight_color(m.ljust(max_match_width)) + ) for m in matches[i : i + words_wide] ) for i in range(0, len(matches), words_wide) diff --git a/bpython/paste.py b/bpython/paste.py index fd140a0e..a81c0c6c 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -37,8 +37,7 @@ class PasteFailed(Exception): class Paster(Protocol): - def paste(self, s: str) -> Tuple[str, Optional[str]]: - ... + def paste(self, s: str) -> Tuple[str, Optional[str]]: ... class PastePinnwand: diff --git a/bpython/test/test_preprocess.py b/bpython/test/test_preprocess.py index ee3f2085..e9309f1e 100644 --- a/bpython/test/test_preprocess.py +++ b/bpython/test/test_preprocess.py @@ -16,10 +16,10 @@ def get_fodder_source(test_name): pattern = rf"#StartTest-{test_name}\n(.*?)#EndTest" - orig, xformed = [ + orig, xformed = ( re.search(pattern, inspect.getsource(module), re.DOTALL) for module in [original, processed] - ] + ) if not orig: raise ValueError( diff --git a/bpython/urwid.py b/bpython/urwid.py index 4b41c12a..9b061340 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -99,7 +99,6 @@ def buildProtocol(self, addr): if urwid.VERSION < (1, 0, 0) and hasattr(urwid, "TwistedEventLoop"): class TwistedEventLoop(urwid.TwistedEventLoop): - """TwistedEventLoop modified to properly stop the reactor. urwid 0.9.9 and 0.9.9.1 crash the reactor on ExitMainLoop instead From ff05288d819fb40a0a2606010abcc6c9d29d6687 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 23:17:19 +0200 Subject: [PATCH 086/113] Import BuildDoc from sphinx (fixes #987) --- LICENSE | 28 ++++++++ setup.py | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 229 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 72d02ff6..46f642f2 100644 --- a/LICENSE +++ b/LICENSE @@ -72,3 +72,31 @@ products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. + + +BuildDoc in setup.py is licensed under the BSD-2 license: + +Copyright 2007-2021 Sebastian Wiesner + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/setup.py b/setup.py index 0cceb940..e158f1a0 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ import re import subprocess -from setuptools import setup +from setuptools import setup, Command from setuptools.command.build import build as _orig_build try: @@ -17,14 +17,212 @@ try: import sphinx - from sphinx.setup_command import BuildDoc # Sphinx 1.5 and newer support Python 3.6 - using_sphinx = sphinx.__version__ >= "1.5" and sphinx.__version__ < "7.0" + using_sphinx = sphinx.__version__ >= "1.5" except ImportError: using_sphinx = False +if using_sphinx: + import sys + from io import StringIO + + from setuptools.errors import ExecError + from sphinx.application import Sphinx + from sphinx.cmd.build import handle_exception + from sphinx.util.console import color_terminal, nocolor + from sphinx.util.docutils import docutils_namespace, patch_docutils + from sphinx.util.osutil import abspath + + class BuildDoc(Command): + """ + Distutils command to build Sphinx documentation. + The Sphinx build can then be triggered from distutils, and some Sphinx + options can be set in ``setup.py`` or ``setup.cfg`` instead of Sphinx's + own configuration file. + For instance, from `setup.py`:: + # this is only necessary when not using setuptools/distribute + from sphinx.setup_command import BuildDoc + cmdclass = {'build_sphinx': BuildDoc} + name = 'My project' + version = '1.2' + release = '1.2.0' + setup( + name=name, + author='Bernard Montgomery', + version=release, + cmdclass=cmdclass, + # these are optional and override conf.py settings + command_options={ + 'build_sphinx': { + 'project': ('setup.py', name), + 'version': ('setup.py', version), + 'release': ('setup.py', release)}}, + ) + Or add this section in ``setup.cfg``:: + [build_sphinx] + project = 'My project' + version = 1.2 + release = 1.2.0 + """ + + description = "Build Sphinx documentation" + user_options = [ + ("fresh-env", "E", "discard saved environment"), + ("all-files", "a", "build all files"), + ("source-dir=", "s", "Source directory"), + ("build-dir=", None, "Build directory"), + ("config-dir=", "c", "Location of the configuration directory"), + ( + "builder=", + "b", + "The builder (or builders) to use. Can be a comma- " + 'or space-separated list. Defaults to "html"', + ), + ("warning-is-error", "W", "Turn warning into errors"), + ("project=", None, "The documented project's name"), + ("version=", None, "The short X.Y version"), + ( + "release=", + None, + "The full version, including alpha/beta/rc tags", + ), + ( + "today=", + None, + "How to format the current date, used as the " + "replacement for |today|", + ), + ("link-index", "i", "Link index.html to the master doc"), + ("copyright", None, "The copyright string"), + ("pdb", None, "Start pdb on exception"), + ("verbosity", "v", "increase verbosity (can be repeated)"), + ( + "nitpicky", + "n", + "nit-picky mode, warn about all missing references", + ), + ("keep-going", None, "With -W, keep going when getting warnings"), + ] + boolean_options = [ + "fresh-env", + "all-files", + "warning-is-error", + "link-index", + "nitpicky", + ] + + def initialize_options(self) -> None: + self.fresh_env = self.all_files = False + self.pdb = False + self.source_dir: str = None + self.build_dir: str = None + self.builder = "html" + self.warning_is_error = False + self.project = "" + self.version = "" + self.release = "" + self.today = "" + self.config_dir: str = None + self.link_index = False + self.copyright = "" + # Link verbosity to distutils' (which uses 1 by default). + self.verbosity = self.distribution.verbose - 1 # type: ignore + self.traceback = False + self.nitpicky = False + self.keep_going = False + + def _guess_source_dir(self) -> str: + for guess in ("doc", "docs"): + if not os.path.isdir(guess): + continue + for root, dirnames, filenames in os.walk(guess): + if "conf.py" in filenames: + return root + return os.curdir + + def finalize_options(self) -> None: + self.ensure_string_list("builder") + + if self.source_dir is None: + self.source_dir = self._guess_source_dir() + self.announce("Using source directory %s" % self.source_dir) + + self.ensure_dirname("source_dir") + + if self.config_dir is None: + self.config_dir = self.source_dir + + if self.build_dir is None: + build = self.get_finalized_command("build") + self.build_dir = os.path.join(abspath(build.build_base), "sphinx") # type: ignore + + self.doctree_dir = os.path.join(self.build_dir, "doctrees") + + self.builder_target_dirs = [ + (builder, os.path.join(self.build_dir, builder)) + for builder in self.builder + ] + + def run(self) -> None: + if not color_terminal(): + nocolor() + if not self.verbose: # type: ignore + status_stream = StringIO() + else: + status_stream = sys.stdout # type: ignore + confoverrides = {} + if self.project: + confoverrides["project"] = self.project + if self.version: + confoverrides["version"] = self.version + if self.release: + confoverrides["release"] = self.release + if self.today: + confoverrides["today"] = self.today + if self.copyright: + confoverrides["copyright"] = self.copyright + if self.nitpicky: + confoverrides["nitpicky"] = self.nitpicky + + for builder, builder_target_dir in self.builder_target_dirs: + app = None + + try: + confdir = self.config_dir or self.source_dir + with patch_docutils(confdir), docutils_namespace(): + app = Sphinx( + self.source_dir, + self.config_dir, + builder_target_dir, + self.doctree_dir, + builder, + confoverrides, + status_stream, + freshenv=self.fresh_env, + warningiserror=self.warning_is_error, + verbosity=self.verbosity, + keep_going=self.keep_going, + ) + app.build(force_all=self.all_files) + if app.statuscode: + raise ExecError( + "caused by %s builder." % app.builder.name + ) + except Exception as exc: + handle_exception(app, self, exc, sys.stderr) + if not self.pdb: + raise SystemExit(1) from exc + + if not self.link_index: + continue + + src = app.config.root_doc + app.builder.out_suffix # type: ignore + dst = app.builder.get_outfilename("index") # type: ignore + os.symlink(src, dst) + + # version handling From 1f2f6f5b04b52ea939d6bd64e2e8eee83ae917f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 23:17:39 +0200 Subject: [PATCH 087/113] Refactor build command overrides --- setup.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index e158f1a0..9e24203f 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup, Command -from setuptools.command.build import build as _orig_build +from setuptools.command.build import build try: from babel.messages import frontend as babel @@ -320,26 +320,27 @@ def git_describe_to_python_version(version): vf.write(f'__version__ = "{version}"\n') -class build(_orig_build): - # Avoid patching the original class' attribute (more robust customisation) - sub_commands = _orig_build.sub_commands[:] +class custom_build(build): + def run(self): + if using_translations: + self.run_command("compile_catalog") + if using_sphinx: + self.run_command("build_sphinx_man") -cmdclass = {"build": build} +cmdclass = {"build": custom_build} + translations_dir = os.path.join("bpython", "translations") # localization options if using_translations: - build.sub_commands.insert(0, ("compile_catalog", None)) - cmdclass["compile_catalog"] = babel.compile_catalog cmdclass["extract_messages"] = babel.extract_messages cmdclass["update_catalog"] = babel.update_catalog cmdclass["init_catalog"] = babel.init_catalog if using_sphinx: - build.sub_commands.insert(0, ("build_sphinx_man", None)) cmdclass["build_sphinx_man"] = BuildDoc if platform.system() in ("FreeBSD", "OpenBSD"): @@ -378,6 +379,7 @@ class build(_orig_build): if os.path.exists(os.path.join(translations_dir, mo_subpath)): mo_files.append(mo_subpath) + setup( version=version, data_files=data_files, @@ -388,6 +390,7 @@ class build(_orig_build): }, cmdclass=cmdclass, test_suite="bpython.test", + zip_safe=False, ) # vim: fileencoding=utf-8 sw=4 ts=4 sts=4 ai et sta From c085f9cc519494970e09f4a77aaeadf0bff77f87 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 23:20:39 +0200 Subject: [PATCH 088/113] CI: allow sphinx >= 7 --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 37128f24..de2f98cc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -33,7 +33,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5,<7" + pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" pip install pytest pytest-cov numpy - name: Build with Python ${{ matrix.python-version }} run: | From 7238851ca65ec0381928c1ef8ef0bde475bf6a50 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 23:24:07 +0200 Subject: [PATCH 089/113] Also register BuildDoc for build_sphinx --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 9e24203f..de10eaf4 100755 --- a/setup.py +++ b/setup.py @@ -341,6 +341,7 @@ def run(self): cmdclass["init_catalog"] = babel.init_catalog if using_sphinx: + cmdclass["build_sphinx"] = BuildDoc cmdclass["build_sphinx_man"] = BuildDoc if platform.system() in ("FreeBSD", "OpenBSD"): From 23294503c59088c5ea9c3d811d073d14272f9ff0 Mon Sep 17 00:00:00 2001 From: suman Date: Sat, 1 Jun 2024 23:43:24 +0545 Subject: [PATCH 090/113] Replace NoReturn with Never --- bpython/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index ed0b0055..e8e882a2 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -36,7 +36,7 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional, NoReturn, Callable +from typing import Tuple, List, Optional, Never, Callable from types import ModuleType from . import __version__, __copyright__ @@ -51,7 +51,7 @@ class ArgumentParserFailed(ValueError): class RaisingArgumentParser(argparse.ArgumentParser): - def error(self, msg: str) -> NoReturn: + def error(self, msg: str) -> Never: raise ArgumentParserFailed() From f8aeaaf8ef8b513473267db99dfe845d0e4b1a41 Mon Sep 17 00:00:00 2001 From: suman Date: Sat, 1 Jun 2024 23:43:46 +0545 Subject: [PATCH 091/113] Add type annotations --- bpdb/debugger.py | 8 ++++---- bpython/args.py | 4 ++-- bpython/urwid.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bpdb/debugger.py b/bpdb/debugger.py index 3e5bbc91..38469541 100644 --- a/bpdb/debugger.py +++ b/bpdb/debugger.py @@ -27,24 +27,24 @@ class BPdb(pdb.Pdb): """PDB with BPython support.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.prompt = "(BPdb) " self.intro = 'Use "B" to enter bpython, Ctrl-d to exit it.' - def postloop(self): + def postloop(self) -> None: # We only want to show the intro message once. self.intro = None super().postloop() # cmd.Cmd commands - def do_Bpython(self, arg): + def do_Bpython(self, arg: str) -> None: locals_ = self.curframe.f_globals.copy() locals_.update(self.curframe.f_locals) bpython.embed(locals_, ["-i"]) - def help_Bpython(self): + def help_Bpython(self) -> None: print("B(python)") print("") print( diff --git a/bpython/args.py b/bpython/args.py index e8e882a2..ed0b0055 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -36,7 +36,7 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional, Never, Callable +from typing import Tuple, List, Optional, NoReturn, Callable from types import ModuleType from . import __version__, __copyright__ @@ -51,7 +51,7 @@ class ArgumentParserFailed(ValueError): class RaisingArgumentParser(argparse.ArgumentParser): - def error(self, msg: str) -> Never: + def error(self, msg: str) -> NoReturn: raise ArgumentParserFailed() diff --git a/bpython/urwid.py b/bpython/urwid.py index 9b061340..3c075d93 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -76,10 +76,10 @@ class EvalProtocol(basic.LineOnlyReceiver): delimiter = "\n" - def __init__(self, myrepl): + def __init__(self, myrepl) -> None: self.repl = myrepl - def lineReceived(self, line): + def lineReceived(self, line) -> None: # HACK! # TODO: deal with encoding issues here... self.repl.main_loop.process_input(line) From a12d339e1a0bdca726d439ed1231f3f2ca993eac Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 2 Jun 2024 13:04:12 +0200 Subject: [PATCH 092/113] Use Never for Python 3.11 and newer --- bpython/_typing_compat.py | 28 ++++++++++++++++++++++++++++ bpython/args.py | 5 +++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 bpython/_typing_compat.py diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py new file mode 100644 index 00000000..83567b4f --- /dev/null +++ b/bpython/_typing_compat.py @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright (c) 2024 Sebastian Ramacher +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +try: + # introduced in Python 3.11 + from typing import Never +except ImportError: + from typing import NoReturn as Never # type: ignore + diff --git a/bpython/args.py b/bpython/args.py index ed0b0055..55691a2a 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -36,12 +36,13 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional, NoReturn, Callable +from typing import Tuple, List, Optional, Callable from types import ModuleType from . import __version__, __copyright__ from .config import default_config_path, Config from .translations import _ +from ._typing_compat import Never logger = logging.getLogger(__name__) @@ -51,7 +52,7 @@ class ArgumentParserFailed(ValueError): class RaisingArgumentParser(argparse.ArgumentParser): - def error(self, msg: str) -> NoReturn: + def error(self, msg: str) -> Never: raise ArgumentParserFailed() From c152cbf8485695bb1835aef5b58f7fc275f03307 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Jul 2024 17:31:27 +0200 Subject: [PATCH 093/113] Update changelog for 0.25 --- CHANGELOG.rst | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d7ecb3ab..67d56f88 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,18 +6,29 @@ Changelog General information: -* The bpython-cli rendering backend has been removed following deprecation in +* The `bpython-cli` rendering backend has been removed following deprecation in version 0.19. - +* This release is focused on Python 3.12 support. New features: Fixes: +* Fix __signature__ support + Thanks to gpotter2 +* #995: Fix handling of `SystemExit` +* #996: Improve order of completion results + Thanks to gpotter2 +* Fix build of documentation and manpages with Sphinx >= 7 +* #1001: Do not fail if modules don't have __version__ Changes to dependencies: +* Remove use of distutils + Thanks to Anderson Bravalheri + +Support for Python 3.12 has been added. Support for Python 3.7 has been dropped. 0.24 ---- @@ -37,7 +48,7 @@ Fixes: Changes to dependencies: -* wheel is no required as part of pyproject.toml's build dependencies +* wheel is not required as part of pyproject.toml's build dependencies Support for Python 3.11 has been added. From ce710bbdb48be7ec8325d01ee156ba666e258c63 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Jul 2024 17:57:18 +0200 Subject: [PATCH 094/113] Format with black --- bpython/_typing_compat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py index 83567b4f..486aacaf 100644 --- a/bpython/_typing_compat.py +++ b/bpython/_typing_compat.py @@ -25,4 +25,3 @@ from typing import Never except ImportError: from typing import NoReturn as Never # type: ignore - From b6318376b255be645b16aaf27c6475359e384ab9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Jul 2024 21:27:49 +0200 Subject: [PATCH 095/113] Update copyright year --- bpython/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/__init__.py b/bpython/__init__.py index 8c7bfb37..26fa3e63 100644 --- a/bpython/__init__.py +++ b/bpython/__init__.py @@ -31,7 +31,7 @@ __author__ = ( "Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al." ) -__copyright__ = f"(C) 2008-2023 {__author__}" +__copyright__ = f"(C) 2008-2024 {__author__}" __license__ = "MIT" __version__ = version package_dir = os.path.abspath(os.path.dirname(__file__)) From f0c023071a8f21b2007389ffcbe57399f35f0cac Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 25 Oct 2024 11:28:44 +0200 Subject: [PATCH 096/113] CI: test with Python 3.13 --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index de2f98cc..747d55a4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -20,6 +20,7 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" - "pypy-3.8" steps: - uses: actions/checkout@v4 From bbdff64fe37b851b6a33183a6a35739bbb4687a0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 25 Oct 2024 11:45:34 +0200 Subject: [PATCH 097/113] Accept source in showsyntaxerror --- bpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/repl.py b/bpython/repl.py index f30cfa31..b048314d 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -152,7 +152,7 @@ def runsource( with self.timer: return super().runsource(source, filename, symbol) - def showsyntaxerror(self, filename: Optional[str] = None) -> None: + def showsyntaxerror(self, filename: Optional[str] = None, source: Optional[str] = None) -> None: """Override the regular handler, the code's copied and pasted from code.py, as per showtraceback, but with the syntaxerror callback called and the text in a pretty colour.""" From 52a7a157037f1d8ef81bd0672b636b837d90bed6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 25 Oct 2024 17:42:06 +0200 Subject: [PATCH 098/113] Switch assert arguments to display correct value as expected --- bpython/test/test_interpreter.py | 4 ++-- bpython/test/test_repl.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index a4a32dd0..acad12c1 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -130,8 +130,8 @@ def gfunc(): ) a = i.a - self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) - self.assertEqual(plain("").join(a), expected) + self.assertMultiLineEqual(str(expected), str(plain("").join(a))) + self.assertEqual(expected, plain("").join(a)) def test_getsource_works_on_interactively_defined_functions(self): source = "def foo(x):\n return x + 1\n" diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 8c3b85cc..3f6b7c12 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -307,7 +307,7 @@ def assert_get_source_error_for_current_function(self, func, msg): try: self.repl.get_source_of_current_name() except repl.SourceNotFound as e: - self.assertEqual(e.args[0], msg) + self.assertEqual(msg, e.args[0]) else: self.fail("Should have raised SourceNotFound") From 45f4117b534d6827279f7b9e633f3cabe0fb37e6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 25 Oct 2024 17:42:20 +0200 Subject: [PATCH 099/113] Fix test errors with Python 3.13 --- bpython/test/test_interpreter.py | 17 ++++++++++++++++- bpython/test/test_repl.py | 11 ++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index acad12c1..b9f0a31e 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -99,7 +99,22 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - if (3, 11) <= sys.version_info[:2]: + if (3, 13) <= sys.version_info[:2]: + expected = ( + "Traceback (most recent call last):\n File " + + green('""') + + ", line " + + bold(magenta("1")) + + ", in " + + cyan("") + + "\n gfunc()" + + "\n ^^^^^\n" + + bold(red("NameError")) + + ": " + + cyan(global_not_found) + + "\n" + ) + elif (3, 11) <= sys.version_info[:2]: expected = ( "Traceback (most recent call last):\n File " + green('""') diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 3f6b7c12..5cafec94 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -332,9 +332,14 @@ def test_current_function_cpython(self): self.assert_get_source_error_for_current_function( collections.defaultdict.copy, "No source code found for INPUTLINE" ) - self.assert_get_source_error_for_current_function( - collections.defaultdict, "could not find class definition" - ) + if sys.version_info[:2] >= (3, 13): + self.assert_get_source_error_for_current_function( + collections.defaultdict, "source code not available" + ) + else: + self.assert_get_source_error_for_current_function( + collections.defaultdict, "could not find class definition" + ) def test_current_line(self): self.repl.interp.locals["a"] = socket.socket From 4605fabe156a9bf1abfee5945514cf81c1828df2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 04:28:01 +0000 Subject: [PATCH 100/113] Bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 747d55a4..7ccef9bc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -47,7 +47,7 @@ jobs: run: | pytest --cov=bpython --cov-report=xml -v - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 env: PYTHON_VERSION: ${{ matrix.python-version }} with: From c332f0d684160a612950bcf88d1a1cfec50b4ab2 Mon Sep 17 00:00:00 2001 From: Max R Date: Mon, 16 Dec 2024 10:00:04 -0500 Subject: [PATCH 101/113] black --- bpython/repl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bpython/repl.py b/bpython/repl.py index b048314d..c87d1965 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -152,7 +152,9 @@ def runsource( with self.timer: return super().runsource(source, filename, symbol) - def showsyntaxerror(self, filename: Optional[str] = None, source: Optional[str] = None) -> None: + def showsyntaxerror( + self, filename: Optional[str] = None, source: Optional[str] = None + ) -> None: """Override the regular handler, the code's copied and pasted from code.py, as per showtraceback, but with the syntaxerror callback called and the text in a pretty colour.""" From d3e7a174831c08c91d0574ae05eebe0eb9bf4cb9 Mon Sep 17 00:00:00 2001 From: Max R Date: Mon, 16 Dec 2024 10:05:38 -0500 Subject: [PATCH 102/113] codespell --- .github/workflows/lint.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d960c6d8..fbb5d996 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -24,8 +24,8 @@ jobs: - uses: actions/checkout@v4 - uses: codespell-project/actions-codespell@master with: - skip: '*.po' - ignore_words_list: ba,te,deltion,dedent,dedented + skip: '*.po',encoding_latin1.py + ignore_words_list: ba,te,deltion,dedent,dedented,assertIn mypy: runs-on: ubuntu-latest From c70fa70b8fdd57fa25d7c8890727d17e78397e27 Mon Sep 17 00:00:00 2001 From: Max R Date: Mon, 16 Dec 2024 10:39:23 -0500 Subject: [PATCH 103/113] mypy --- bpython/_typing_compat.py | 2 +- bpython/curtsiesfrontend/repl.py | 3 ++- bpython/pager.py | 3 ++- bpython/test/test_preprocess.py | 3 ++- setup.cfg | 1 + 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py index 486aacaf..5d9a3607 100644 --- a/bpython/_typing_compat.py +++ b/bpython/_typing_compat.py @@ -24,4 +24,4 @@ # introduced in Python 3.11 from typing import Never except ImportError: - from typing import NoReturn as Never # type: ignore + from typing_extensions import Never # type: ignore diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 302e67d4..69eb18c9 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -236,7 +236,8 @@ def close(self) -> None: @property def encoding(self) -> str: - return sys.__stdin__.encoding + # `encoding` is new in py39 + return sys.__stdin__.encoding # type: ignore # TODO write a read() method? diff --git a/bpython/pager.py b/bpython/pager.py index e145e0ed..65a3b223 100644 --- a/bpython/pager.py +++ b/bpython/pager.py @@ -55,7 +55,8 @@ def page(data: str, use_internal: bool = False) -> None: try: popen = subprocess.Popen(command, stdin=subprocess.PIPE) assert popen.stdin is not None - data_bytes = data.encode(sys.__stdout__.encoding, "replace") + # `encoding` is new in py39 + data_bytes = data.encode(sys.__stdout__.encoding, "replace") # type: ignore popen.stdin.write(data_bytes) popen.stdin.close() except OSError as e: diff --git a/bpython/test/test_preprocess.py b/bpython/test/test_preprocess.py index e9309f1e..a72a64b6 100644 --- a/bpython/test/test_preprocess.py +++ b/bpython/test/test_preprocess.py @@ -4,6 +4,7 @@ import unittest from code import compile_command as compiler +from codeop import CommandCompiler from functools import partial from bpython.curtsiesfrontend.interpreter import code_finished_will_parse @@ -11,7 +12,7 @@ from bpython.test.fodder import original, processed -preproc = partial(preprocess, compiler=compiler) +preproc = partial(preprocess, compiler=CommandCompiler) def get_fodder_source(test_name): diff --git a/setup.cfg b/setup.cfg index 1fe4a6f9..9a4f0bcb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,7 @@ install_requires = pygments pyxdg requests + typing_extensions ; python_version < "3.11" [options.extras_require] clipboard = pyperclip From 839145e913d72eb025f61086a09f133a0230350f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 15 Jan 2025 23:53:17 +0100 Subject: [PATCH 104/113] Handle title argument of pydoc.pager (fixes #1029) --- bpython/curtsiesfrontend/_internal.py | 4 ++-- bpython/curtsiesfrontend/repl.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index 0480c1b0..16a598fa 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -52,8 +52,8 @@ def __init__(self, repl=None): super().__init__() - def pager(self, output): - self._repl.pager(output) + def pager(self, output, title=""): + self._repl.pager(output, title) def __call__(self, *args, **kwargs): if self._repl.reevaluating: diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 69eb18c9..01b04711 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -2101,10 +2101,10 @@ def focus_on_subprocess(self, args): finally: signal.signal(signal.SIGWINCH, prev_sigwinch_handler) - def pager(self, text: str) -> None: - """Runs an external pager on text + def pager(self, text: str, title: str = "") -> None: + """Runs an external pager on text""" - text must be a str""" + # TODO: make less handle title command = get_pager_command() with tempfile.NamedTemporaryFile() as tmp: tmp.write(text.encode(getpreferredencoding())) From f5d6da985b30091c0d4dda67d35a4877200ea344 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 16 Jan 2025 00:07:33 +0100 Subject: [PATCH 105/113] Drop support for Python 3.8 --- .github/workflows/build.yaml | 81 +++++++++++++++--------------- bpython/simpleeval.py | 23 ++------- doc/sphinx/source/contributing.rst | 2 +- doc/sphinx/source/releases.rst | 2 +- pyproject.toml | 6 +-- setup.cfg | 2 +- 6 files changed, 48 insertions(+), 68 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 7ccef9bc..a6e9aef0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,53 +4,52 @@ on: push: pull_request: schedule: - # run at 7:00 on the first of every month - - cron: '0 7 1 * *' + # run at 7:00 on the first of every month + - cron: "0 7 1 * *" jobs: build: runs-on: ubuntu-latest - continue-on-error: ${{ matrix.python-version == 'pypy-3.8' }} + continue-on-error: ${{ matrix.python-version == 'pypy-3.9' }} strategy: fail-fast: false matrix: python-version: - - "3.8" - - "3.9" - - "3.10" - - "3.11" - - "3.12" - - "3.13" - - "pypy-3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "pypy-3.9" steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" - pip install pytest pytest-cov numpy - - name: Build with Python ${{ matrix.python-version }} - run: | - python setup.py build - - name: Build documentation - run: | - python setup.py build_sphinx - python setup.py build_sphinx_man - - name: Test with pytest - run: | - pytest --cov=bpython --cov-report=xml -v - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - env: - PYTHON_VERSION: ${{ matrix.python-version }} - with: - file: ./coverage.xml - env_vars: PYTHON_VERSION - if: ${{ always() }} + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" + pip install pytest pytest-cov numpy + - name: Build with Python ${{ matrix.python-version }} + run: | + python setup.py build + - name: Build documentation + run: | + python setup.py build_sphinx + python setup.py build_sphinx_man + - name: Test with pytest + run: | + pytest --cov=bpython --cov-report=xml -v + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + env: + PYTHON_VERSION: ${{ matrix.python-version }} + with: + file: ./coverage.xml + env_vars: PYTHON_VERSION + if: ${{ always() }} diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index c5bba43d..3f334af4 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -33,12 +33,9 @@ from . import line as line_properties from .inspection import getattr_safe -_is_py38 = sys.version_info[:2] >= (3, 8) -_is_py39 = sys.version_info[:2] >= (3, 9) - _string_type_nodes = (ast.Str, ast.Bytes) _numeric_types = (int, float, complex) -_name_type_nodes = (ast.Name,) if _is_py38 else (ast.Name, ast.NameConstant) +_name_type_nodes = (ast.Name,) class EvaluationError(Exception): @@ -91,10 +88,6 @@ def simple_eval(node_or_string, namespace=None): def _convert(node): if isinstance(node, ast.Constant): return node.value - elif not _is_py38 and isinstance(node, _string_type_nodes): - return node.s - elif not _is_py38 and isinstance(node, ast.Num): - return node.n elif isinstance(node, ast.Tuple): return tuple(map(_convert, node.elts)) elif isinstance(node, ast.List): @@ -168,18 +161,8 @@ def _convert(node): return left - right # this is a deviation from literal_eval: we allow indexing - elif ( - not _is_py39 - and isinstance(node, ast.Subscript) - and isinstance(node.slice, ast.Index) - ): - obj = _convert(node.value) - index = _convert(node.slice.value) - return safe_getitem(obj, index) - elif ( - _is_py39 - and isinstance(node, ast.Subscript) - and isinstance(node.slice, (ast.Constant, ast.Name)) + elif isinstance(node, ast.Subscript) and isinstance( + node.slice, (ast.Constant, ast.Name) ): obj = _convert(node.value) index = _convert(node.slice) diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 32b1ea86..3b93089d 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -17,7 +17,7 @@ the time of day. Getting your development environment set up ------------------------------------------- -bpython supports Python 3.8 and newer. The code is compatible with all +bpython supports Python 3.9 and newer. The code is compatible with all supported versions. Using a virtual environment is probably a good idea. Create a virtual diff --git a/doc/sphinx/source/releases.rst b/doc/sphinx/source/releases.rst index fcce5c1c..7d789f16 100644 --- a/doc/sphinx/source/releases.rst +++ b/doc/sphinx/source/releases.rst @@ -45,7 +45,7 @@ A checklist to perform some manual tests before a release: Check that all of the following work before a release: -* Runs under Python 3.8 - 3.11 +* Runs under Python 3.9 - 3.13 * Save * Rewind * Pastebin diff --git a/pyproject.toml b/pyproject.toml index 924722b0..ca4e0450 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,10 @@ [build-system] -requires = [ - "setuptools >= 62.4.0", -] +requires = ["setuptools >= 62.4.0"] build-backend = "setuptools.build_meta" [tool.black] line-length = 80 -target_version = ["py38"] +target_version = ["py39"] include = '\.pyi?$' exclude = ''' /( diff --git a/setup.cfg b/setup.cfg index 9a4f0bcb..f8b7c325 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ classifiers = Programming Language :: Python :: 3 [options] -python_requires = >=3.8 +python_requires = >=3.9 packages = bpython bpython.curtsiesfrontend From b8923057a2aefe55c97164ff36f7766c82b7ea0b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 16 Jan 2025 00:14:17 +0100 Subject: [PATCH 106/113] Update changelog for 0.25 --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 67d56f88..114b9440 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,7 +8,7 @@ General information: * The `bpython-cli` rendering backend has been removed following deprecation in version 0.19. -* This release is focused on Python 3.12 support. +* This release is focused on Python 3.13 support. New features: @@ -28,7 +28,7 @@ Changes to dependencies: * Remove use of distutils Thanks to Anderson Bravalheri -Support for Python 3.12 has been added. Support for Python 3.7 has been dropped. +Support for Python 3.12 and 3.13 has been added. Support for Python 3.7 and 3.8 has been dropped. 0.24 ---- From 5b31cca96c951ddefba8f2c71a4abc208b2adac0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 16 Jan 2025 00:16:37 +0100 Subject: [PATCH 107/113] CI: fix yaml --- .github/workflows/lint.yaml | 54 ++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index fbb5d996..b6056159 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -8,38 +8,38 @@ jobs: black: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install black codespell - - name: Check with black - run: black --check . + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install black codespell + - name: Check with black + run: black --check . codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: codespell-project/actions-codespell@master - with: - skip: '*.po',encoding_latin1.py - ignore_words_list: ba,te,deltion,dedent,dedented,assertIn + - uses: actions/checkout@v4 + - uses: codespell-project/actions-codespell@master + with: + skip: "*.po,encoding_latin1.py" + ignore_words_list: ba,te,deltion,dedent,dedented,assertIn mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install mypy - pip install -r requirements.txt - pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" numpy - pip install types-backports types-requests types-setuptools types-toml types-pygments - - name: Check with mypy - # for now only run on a few files to avoid slipping backward - run: mypy + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install mypy + pip install -r requirements.txt + pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" numpy + pip install types-backports types-requests types-setuptools types-toml types-pygments + - name: Check with mypy + # for now only run on a few files to avoid slipping backward + run: mypy From 400f5eda1ae7859b88c81d591177b54128d0e835 Mon Sep 17 00:00:00 2001 From: Pushkar Kulkarni Date: Thu, 16 Jan 2025 11:02:07 +0530 Subject: [PATCH 108/113] More general adaptation of showsyntaxerror() to Python 3.13 Python 3.13's code.InteractiveInterpreter adds a new **kwargs argument to its showsyntaxerror() method. Currently, the only use of it is to send a named argument of name "source". Whilst the current adapation of repl.Interpreter is specific and should work in the short term, here is a more general solution. --- bpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/repl.py b/bpython/repl.py index c87d1965..0374bb6b 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -153,7 +153,7 @@ def runsource( return super().runsource(source, filename, symbol) def showsyntaxerror( - self, filename: Optional[str] = None, source: Optional[str] = None + self, filename: Optional[str] = None, **kwargs ) -> None: """Override the regular handler, the code's copied and pasted from code.py, as per showtraceback, but with the syntaxerror callback called From a4eadd750d2d3b103bb78abaac717358f7efc722 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 17 Jan 2025 21:21:40 +0100 Subject: [PATCH 109/113] 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 110/113] 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 111/113] 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 112/113] 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 113/113] 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