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 01/70] 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 02/70] 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 03/70] 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 04/70] 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 05/70] 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 06/70] 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 07/70] 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 08/70] 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 09/70] 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 10/70] 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 11/70] 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 12/70] 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 13/70] 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 14/70] 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 15/70] 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 16/70] 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 17/70] 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 18/70] 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 19/70] 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 20/70] 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 21/70] 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 22/70] 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 23/70] 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 24/70] 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 25/70] 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 26/70] 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 27/70] 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 28/70] 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 29/70] 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 30/70] 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 31/70] 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 32/70] 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 33/70] 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 34/70] 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 35/70] Add short project description Should be picked up by PyPI and others. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index f8b7c325..b3cb9a4c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,6 @@ [metadata] name = bpython +description = A fancy curses interface to the Python interactive interpreter long_description = file: README.rst long_description_content_type = text/x-rst license = MIT From b99562370fd0ff4f5b9c2101a39263f7075252d6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 2 Jun 2025 16:39:55 +0200 Subject: [PATCH 36/70] CI: no longer test with Python 3.9 --- .github/workflows/build.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a6e9aef0..cb6d64ad 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,12 +15,11 @@ jobs: fail-fast: false matrix: python-version: - - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" - - "pypy-3.9" + - "pypy-3.10" steps: - uses: actions/checkout@v4 with: From 982bd7e20e584220f1c21805a21c345bbf893e12 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 2 Jun 2025 16:40:59 +0200 Subject: [PATCH 37/70] Upgrade to Python 3.10+ --- bpython/args.py | 7 ++- bpython/autocomplete.py | 78 ++++++++++++------------- bpython/curtsies.py | 29 +++++---- bpython/curtsiesfrontend/_internal.py | 6 +- bpython/curtsiesfrontend/filewatch.py | 4 +- bpython/curtsiesfrontend/interpreter.py | 6 +- bpython/curtsiesfrontend/parse.py | 3 +- bpython/curtsiesfrontend/repl.py | 40 ++++++------- bpython/filelock.py | 8 +-- bpython/history.py | 8 +-- bpython/importcompletion.py | 8 +-- bpython/inspection.py | 22 +++---- bpython/lazyre.py | 4 +- bpython/line.py | 34 +++++------ bpython/paste.py | 2 +- bpython/patch_linecache.py | 4 +- bpython/repl.py | 47 ++++++++------- bpython/simpleeval.py | 2 +- bpython/test/test_inspection.py | 4 +- bpython/test/test_line_properties.py | 4 +- bpython/translations/__init__.py | 2 +- bpython/urwid.py | 2 +- pyproject.toml | 2 +- 23 files changed, 157 insertions(+), 169 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 1eb59a69..35fd3e7b 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -36,7 +36,8 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional, Callable +from typing import Tuple, List, Optional +from collections.abc import Callable from types import ModuleType from . import __version__, __copyright__ @@ -77,8 +78,8 @@ def log_version(module: ModuleType, name: str) -> None: def parse( - args: Optional[list[str]], - extras: Optional[Options] = None, + args: list[str] | None, + extras: Options | None = None, ignore_stdin: bool = False, ) -> tuple[Config, argparse.Namespace, list[str]]: """Receive an argument list - if None, use sys.argv - parse all args and diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 88afbe54..4fb62f72 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -236,7 +236,7 @@ def __init__( @abc.abstractmethod def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[set[str]]: + ) -> set[str] | None: """Returns a list of possible matches given a line and cursor, or None if this completion type isn't applicable. @@ -255,7 +255,7 @@ def matches( raise NotImplementedError @abc.abstractmethod - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: """Returns a Linepart namedtuple instance or None given cursor and line A Linepart namedtuple contains a start, stop, and word. None is @@ -299,7 +299,7 @@ def __init__( super().__init__(True, mode) - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: for completer in self._completers: return_value = completer.locate(cursor_offset, line) if return_value is not None: @@ -311,7 +311,7 @@ def format(self, word: str) -> str: def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[set[str]]: + ) -> set[str] | None: return_value = None all_matches = set() for completer in self._completers: @@ -336,10 +336,10 @@ def __init__( def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[set[str]]: + ) -> set[str] | None: return self.module_gatherer.complete(cursor_offset, line) - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_word(cursor_offset, line) def format(self, word: str) -> str: @@ -356,7 +356,7 @@ def __init__(self, mode: AutocompleteModes = AutocompleteModes.SIMPLE): def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[set[str]]: + ) -> set[str] | None: cs = lineparts.current_string(cursor_offset, line) if cs is None: return None @@ -371,7 +371,7 @@ def matches( matches.add(filename) return matches - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_string(cursor_offset, line) def format(self, filename: str) -> str: @@ -389,9 +389,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[dict[str, Any]] = None, + locals_: dict[str, Any] | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: r = self.locate(cursor_offset, line) if r is None: return None @@ -414,7 +414,7 @@ def matches( if _few_enough_underscores(r.word.split(".")[-1], m.split(".")[-1]) } - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_dotted_attribute(cursor_offset, line) def format(self, word: str) -> str: @@ -474,9 +474,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[dict[str, Any]] = None, + locals_: dict[str, Any] | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: if locals_ is None: return None @@ -500,7 +500,7 @@ def matches( else: return None - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_dict_key(cursor_offset, line) def format(self, match: str) -> str: @@ -513,10 +513,10 @@ def matches( cursor_offset: int, line: str, *, - current_block: Optional[str] = None, - complete_magic_methods: Optional[bool] = None, + current_block: str | None = None, + complete_magic_methods: bool | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: if ( current_block is None or complete_magic_methods is None @@ -531,7 +531,7 @@ def matches( return None return {name for name in MAGIC_METHODS if name.startswith(r.word)} - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_method_definition_name(cursor_offset, line) @@ -541,9 +541,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[dict[str, Any]] = None, + locals_: dict[str, Any] | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: """Compute matches when text is a simple name. Return a list of all keywords, built-in functions and names currently defined in self.namespace that match. @@ -571,7 +571,7 @@ def matches( matches.add(_callable_postfix(val, word)) return matches if matches else None - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_single_word(cursor_offset, line) @@ -581,9 +581,9 @@ def matches( cursor_offset: int, line: str, *, - funcprops: Optional[inspection.FuncProps] = None, + funcprops: inspection.FuncProps | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: if funcprops is None: return None @@ -603,7 +603,7 @@ def matches( ) return matches if matches else None - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: r = lineparts.current_word(cursor_offset, line) if r and r.word[-1] == "(": # if the word ends with a (, it's the parent word with an empty @@ -614,7 +614,7 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: class ExpressionAttributeCompletion(AttrCompletion): # could replace attr completion as a more general case with some work - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return lineparts.current_expression_attribute(cursor_offset, line) def matches( @@ -622,9 +622,9 @@ def matches( cursor_offset: int, line: str, *, - locals_: Optional[dict[str, Any]] = None, + locals_: dict[str, Any] | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: if locals_ is None: locals_ = __main__.__dict__ @@ -648,26 +648,26 @@ def matches( class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[set[str]]: + ) -> set[str] | None: return None - def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + def locate(self, cursor_offset: int, line: str) -> LinePart | None: return None else: class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] - _orig_start: Optional[int] + _orig_start: int | None def matches( self, cursor_offset: int, line: str, *, - current_block: Optional[str] = None, - history: Optional[list[str]] = None, + current_block: str | None = None, + history: list[str] | None = None, **kwargs: Any, - ) -> Optional[set[str]]: + ) -> set[str] | None: if ( current_block is None or history is None @@ -725,12 +725,12 @@ def get_completer( cursor_offset: int, line: str, *, - locals_: Optional[dict[str, Any]] = None, - argspec: Optional[inspection.FuncProps] = None, - history: Optional[list[str]] = None, - current_block: Optional[str] = None, - complete_magic_methods: Optional[bool] = None, -) -> tuple[list[str], Optional[BaseCompletionType]]: + locals_: dict[str, Any] | None = None, + argspec: inspection.FuncProps | None = None, + history: list[str] | None = None, + current_block: str | None = None, + complete_magic_methods: bool | None = None, +) -> tuple[list[str], BaseCompletionType | None]: """Returns a list of matches and an applicable completer If no matches available, returns a tuple of an empty list and None diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 547a853e..b57e47a9 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -23,7 +23,6 @@ from typing import ( Any, - Callable, Dict, List, Optional, @@ -31,28 +30,28 @@ Tuple, Union, ) -from collections.abc import Generator, Sequence +from collections.abc import Callable, Generator, Sequence logger = logging.getLogger(__name__) class SupportsEventGeneration(Protocol): def send( - self, timeout: Optional[float] - ) -> Union[str, curtsies.events.Event, None]: ... + self, timeout: float | None + ) -> str | curtsies.events.Event | None: ... def __iter__(self) -> "SupportsEventGeneration": ... - def __next__(self) -> Union[str, curtsies.events.Event, None]: ... + def __next__(self) -> str | curtsies.events.Event | None: ... class FullCurtsiesRepl(BaseRepl): def __init__( self, config: Config, - locals_: Optional[dict[str, Any]] = None, - banner: Optional[str] = None, - interp: Optional[Interp] = None, + locals_: dict[str, Any] | None = None, + banner: str | None = None, + interp: Interp | None = None, ) -> None: self.input_generator = curtsies.input.Input( keynames="curtsies", sigint_event=True, paste_threshold=None @@ -129,7 +128,7 @@ def after_suspend(self) -> None: self.interrupting_refresh() def process_event_and_paint( - self, e: Union[str, curtsies.events.Event, None] + self, e: str | curtsies.events.Event | None ) -> None: """If None is passed in, just paint the screen""" try: @@ -151,7 +150,7 @@ def process_event_and_paint( def mainloop( self, interactive: bool = True, - paste: Optional[curtsies.events.PasteEvent] = None, + paste: curtsies.events.PasteEvent | None = None, ) -> None: if interactive: # Add custom help command @@ -178,10 +177,10 @@ def mainloop( def main( - args: Optional[list[str]] = None, - locals_: Optional[dict[str, Any]] = None, - banner: Optional[str] = None, - welcome_message: Optional[str] = None, + args: list[str] | None = None, + locals_: dict[str, Any] | None = None, + banner: str | None = None, + welcome_message: str | None = None, ) -> Any: """ banner is displayed directly after the version information. @@ -249,7 +248,7 @@ def curtsies_arguments(parser: argparse._ArgumentGroup) -> None: def _combined_events( event_provider: SupportsEventGeneration, paste_threshold: int -) -> Generator[Union[str, curtsies.events.Event, None], Optional[float], None]: +) -> Generator[str | curtsies.events.Event | None, float | None, None]: """Combines consecutive keypress events into paste events.""" timeout = yield "nonsense_event" # so send can be used immediately queue: collections.deque = collections.deque() diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index cb7b8105..8c070b34 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -34,9 +34,9 @@ def __enter__(self): def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> Literal[False]: pydoc.pager = self._orig_pager return False diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 53ae4784..2822db6d 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -1,7 +1,7 @@ import os from collections import defaultdict -from typing import Callable, Dict, Set, List -from collections.abc import Iterable, Sequence +from typing import Dict, Set, List +from collections.abc import Callable, Iterable, Sequence from .. import importcompletion diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 6532d968..280c56ed 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -49,7 +49,7 @@ class BPythonFormatter(Formatter): def __init__( self, color_scheme: dict[_TokenType, str], - **options: Union[str, bool, None], + **options: str | bool | None, ) -> None: self.f_strings = {k: f"\x01{v}" for k, v in color_scheme.items()} # FIXME: mypy currently fails to handle this properly @@ -68,7 +68,7 @@ def format(self, tokensource, outfile): class Interp(ReplInterpreter): def __init__( self, - locals: Optional[dict[str, Any]] = None, + locals: dict[str, Any] | None = None, ) -> None: """Constructor. @@ -79,7 +79,7 @@ def __init__( # typically changed after being instantiated # but used when interpreter used corresponding REPL - def write(err_line: Union[str, FmtStr]) -> None: + def write(err_line: str | FmtStr) -> None: """Default stderr handler for tracebacks Accepts FmtStrs so interpreters can output them""" diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 96e91e55..28b32e64 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -1,6 +1,7 @@ import re from functools import partial -from typing import Any, Callable, Dict, Tuple +from typing import Any, Dict, Tuple +from collections.abc import Callable from curtsies.formatstring import fmtstr, FmtStr from curtsies.termformatconstants import ( diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 09f73a82..2a304312 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -112,7 +112,7 @@ def __init__( self, coderunner: CodeRunner, repl: "BaseRepl", - configured_edit_keys: Optional[AbstractEdits] = None, + configured_edit_keys: AbstractEdits | None = None, ): self.coderunner = coderunner self.repl = repl @@ -126,7 +126,7 @@ def __init__( else: self.rl_char_sequences = edit_keys - def process_event(self, e: Union[events.Event, str]) -> None: + def process_event(self, e: events.Event | str) -> None: assert self.has_focus logger.debug("fake input processing event %r", e) @@ -194,7 +194,7 @@ def readline(self, size: int = -1) -> str: self.readline_results.append(value) return value if size <= -1 else value[:size] - def readlines(self, size: Optional[int] = -1) -> list[str]: + def readlines(self, size: int | None = -1) -> list[str]: if size is None: # the default readlines implementation also accepts None size = -1 @@ -337,10 +337,10 @@ def __init__( self, config: Config, window: CursorAwareWindow, - locals_: Optional[dict[str, Any]] = None, - banner: Optional[str] = None, - interp: Optional[Interp] = None, - orig_tcattrs: Optional[list[Any]] = None, + locals_: dict[str, Any] | None = None, + banner: str | None = None, + interp: Interp | None = None, + orig_tcattrs: list[Any] | None = None, ): """ locals_ is a mapping of locals to pass into the interpreter @@ -398,7 +398,7 @@ def __init__( self._current_line = "" # current line of output - stdout and stdin go here - self.current_stdouterr_line: Union[str, FmtStr] = "" + self.current_stdouterr_line: str | FmtStr = "" # this is every line that's been displayed (input and output) # as with formatting applied. Logical lines that exceeded the terminal width @@ -427,7 +427,7 @@ def __init__( # cursor position relative to start of current_line, 0 is first char self._cursor_offset = 0 - self.orig_tcattrs: Optional[list[Any]] = orig_tcattrs + self.orig_tcattrs: list[Any] | None = orig_tcattrs self.coderunner = CodeRunner(self.interp, self.request_refresh) @@ -459,7 +459,7 @@ def __init__( # some commands act differently based on the prev event # this list doesn't include instances of event.Event, # only keypress-type events (no refresh screen events etc.) - self.last_events: list[Optional[str]] = [None] * 50 + self.last_events: list[str | None] = [None] * 50 # displays prev events in a column on the right hand side self.presentation_mode = False @@ -600,9 +600,9 @@ def __enter__(self): def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> Literal[False]: sys.stdin = self.orig_stdin sys.stdout = self.orig_stdout @@ -616,7 +616,7 @@ def __exit__( sys.meta_path = self.orig_meta_path return False - def sigwinch_handler(self, signum: int, frame: Optional[FrameType]) -> None: + def sigwinch_handler(self, signum: int, frame: FrameType | None) -> None: old_rows, old_columns = self.height, self.width self.height, self.width = self.get_term_hw() cursor_dy = self.get_cursor_vertical_diff() @@ -632,7 +632,7 @@ def sigwinch_handler(self, signum: int, frame: Optional[FrameType]) -> None: self.scroll_offset, ) - def sigtstp_handler(self, signum: int, frame: Optional[FrameType]) -> None: + def sigtstp_handler(self, signum: int, frame: FrameType | None) -> None: self.scroll_offset = len(self.lines_for_display) self.__exit__(None, None, None) self.on_suspend() @@ -647,7 +647,7 @@ def clean_up_current_line_for_exit(self): self.unhighlight_paren() # Event handling - def process_event(self, e: Union[events.Event, str]) -> Optional[bool]: + def process_event(self, e: events.Event | str) -> bool | None: """Returns True if shutting down, otherwise returns None. Mostly mutates state of Repl object""" @@ -660,7 +660,7 @@ def process_event(self, e: Union[events.Event, str]) -> Optional[bool]: self.process_key_event(e) return None - def process_control_event(self, e: events.Event) -> Optional[bool]: + def process_control_event(self, e: events.Event) -> bool | None: if isinstance(e, bpythonevents.ScheduledRefreshRequestEvent): # This is a scheduled refresh - it's really just a refresh (so nop) pass @@ -2234,9 +2234,7 @@ def compress_paste_event(paste_event): return None -def just_simple_events( - event_list: Iterable[Union[str, events.Event]] -) -> list[str]: +def just_simple_events(event_list: Iterable[str | events.Event]) -> list[str]: simple_events = [] for e in event_list: if isinstance(e, events.Event): @@ -2253,7 +2251,7 @@ def just_simple_events( return simple_events -def is_simple_event(e: Union[str, events.Event]) -> bool: +def is_simple_event(e: str | events.Event) -> bool: if isinstance(e, events.Event): return False return ( diff --git a/bpython/filelock.py b/bpython/filelock.py index 5ed8769f..b8eb11ff 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -56,9 +56,9 @@ def __enter__(self) -> "BaseLock": def __exit__( self, - exc_type: Optional[type[BaseException]], - exc: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, ) -> Literal[False]: if self.locked: self.release() @@ -122,7 +122,7 @@ def release(self) -> None: def FileLock( - fileobj: IO, mode: int = 0, filename: Optional[str] = None + fileobj: IO, mode: int = 0, filename: str | None = None ) -> BaseLock: if has_fcntl: return UnixFileLock(fileobj, mode) diff --git a/bpython/history.py b/bpython/history.py index 386214b4..b58309b5 100644 --- a/bpython/history.py +++ b/bpython/history.py @@ -37,7 +37,7 @@ class History: def __init__( self, - entries: Optional[Iterable[str]] = None, + entries: Iterable[str] | None = None, duplicates: bool = True, hist_size: int = 100, ) -> None: @@ -78,7 +78,7 @@ def back( self, start: bool = True, search: bool = False, - target: Optional[str] = None, + target: str | None = None, include_current: bool = False, ) -> str: """Move one step back in the history.""" @@ -128,7 +128,7 @@ def forward( self, start: bool = True, search: bool = False, - target: Optional[str] = None, + target: str | None = None, include_current: bool = False, ) -> str: """Move one step forward in the history.""" @@ -214,7 +214,7 @@ def save(self, filename: Path, encoding: str, lines: int = 0) -> None: self.save_to(hfile, self.entries, lines) def save_to( - self, fd: TextIO, entries: Optional[list[str]] = None, lines: int = 0 + self, fd: TextIO, entries: list[str] | None = None, lines: int = 0 ) -> None: if entries is None: entries = self.entries diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index da1b9140..570996d4 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -63,8 +63,8 @@ class _LoadedInode: class ModuleGatherer: def __init__( self, - paths: Optional[Iterable[Union[str, Path]]] = None, - skiplist: Optional[Sequence[str]] = None, + paths: Iterable[str | Path] | None = None, + skiplist: Sequence[str] | None = None, ) -> None: """Initialize module gatherer with all modules in `paths`, which should be a list of directory names. If `paths` is not given, `sys.path` will be used.""" @@ -131,7 +131,7 @@ def module_attr_matches(self, name: str) -> set[str]: """Only attributes which are modules to replace name with""" return self.attr_matches(name, only_modules=True) - def complete(self, cursor_offset: int, line: str) -> Optional[set[str]]: + def complete(self, cursor_offset: int, line: str) -> set[str] | None: """Construct a full list of possibly completions for imports.""" tokens = line.split() if "from" not in tokens and "import" not in tokens: @@ -167,7 +167,7 @@ def complete(self, cursor_offset: int, line: str) -> Optional[set[str]]: else: return None - def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: + def find_modules(self, path: Path) -> Generator[str | None, None, None]: """Find all modules (and packages) for a given directory.""" if not path.is_dir(): # Perhaps a zip file diff --git a/bpython/inspection.py b/bpython/inspection.py index fb1124eb..63f0e2d3 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -28,7 +28,6 @@ from dataclasses import dataclass from typing import ( Any, - Callable, Optional, Type, Dict, @@ -36,6 +35,7 @@ ContextManager, Literal, ) +from collections.abc import Callable from types import MemberDescriptorType, TracebackType from pygments.token import Token @@ -63,12 +63,12 @@ def __repr__(self) -> str: @dataclass class ArgSpec: args: list[str] - varargs: Optional[str] - varkwargs: Optional[str] - defaults: Optional[list[_Repr]] + varargs: str | None + varkwargs: str | None + defaults: list[_Repr] | None kwonly: list[str] - kwonly_defaults: Optional[dict[str, _Repr]] - annotations: Optional[dict[str, Any]] + kwonly_defaults: dict[str, _Repr] | None + annotations: dict[str, Any] | None @dataclass @@ -118,9 +118,9 @@ def __enter__(self) -> None: def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> Literal[False]: """Restore an object's magic methods.""" type_ = type(self._obj) @@ -224,7 +224,7 @@ def _fix_default_values(f: Callable, argspec: ArgSpec) -> ArgSpec: ) -def _getpydocspec(f: Callable) -> Optional[ArgSpec]: +def _getpydocspec(f: Callable) -> ArgSpec | None: try: argspec = pydoc.getdoc(f) except NameError: @@ -267,7 +267,7 @@ def _getpydocspec(f: Callable) -> Optional[ArgSpec]: ) -def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: +def getfuncprops(func: str, f: Callable) -> FuncProps | None: # Check if it's a real bound method or if it's implicitly calling __init__ # (i.e. FooClass(...) and not FooClass.__init__(...) -- the former would # not take 'self', the latter would: diff --git a/bpython/lazyre.py b/bpython/lazyre.py index a63bb464..1d903616 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -44,10 +44,10 @@ def compiled(self) -> Pattern[str]: def finditer(self, *args, **kwargs) -> Iterator[Match[str]]: return self.compiled.finditer(*args, **kwargs) - def search(self, *args, **kwargs) -> Optional[Match[str]]: + def search(self, *args, **kwargs) -> Match[str] | None: return self.compiled.search(*args, **kwargs) - def match(self, *args, **kwargs) -> Optional[Match[str]]: + def match(self, *args, **kwargs) -> Match[str] | None: return self.compiled.match(*args, **kwargs) def sub(self, *args, **kwargs) -> str: diff --git a/bpython/line.py b/bpython/line.py index 363419fe..e64b20d9 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -24,7 +24,7 @@ class LinePart: CHARACTER_PAIR_MAP = {"(": ")", "{": "}", "[": "]", "'": "'", '"': '"'} -def current_word(cursor_offset: int, line: str) -> Optional[LinePart]: +def current_word(cursor_offset: int, line: str) -> LinePart | None: """the object.attribute.attribute just before or under the cursor""" start = cursor_offset end = cursor_offset @@ -76,7 +76,7 @@ def current_word(cursor_offset: int, line: str) -> Optional[LinePart]: ) -def current_dict_key(cursor_offset: int, line: str) -> Optional[LinePart]: +def current_dict_key(cursor_offset: int, line: str) -> LinePart | None: """If in dictionary completion, return the current key""" for m in _current_dict_key_re.finditer(line): if m.start(1) <= cursor_offset <= m.end(1): @@ -96,7 +96,7 @@ def current_dict_key(cursor_offset: int, line: str) -> Optional[LinePart]: ) -def current_dict(cursor_offset: int, line: str) -> Optional[LinePart]: +def current_dict(cursor_offset: int, line: str) -> LinePart | None: """If in dictionary completion, return the dict that should be used""" for m in _current_dict_re.finditer(line): if m.start(2) <= cursor_offset <= m.end(2): @@ -110,7 +110,7 @@ def current_dict(cursor_offset: int, line: str) -> Optional[LinePart]: ) -def current_string(cursor_offset: int, line: str) -> Optional[LinePart]: +def current_string(cursor_offset: int, line: str) -> LinePart | None: """If inside a string of nonzero length, return the string (excluding quotes) @@ -126,7 +126,7 @@ def current_string(cursor_offset: int, line: str) -> Optional[LinePart]: _current_object_re = LazyReCompile(r"([\w_][\w0-9_]*)[.]") -def current_object(cursor_offset: int, line: str) -> Optional[LinePart]: +def current_object(cursor_offset: int, line: str) -> LinePart | None: """If in attribute completion, the object on which attribute should be looked up.""" match = current_word(cursor_offset, line) @@ -145,9 +145,7 @@ def current_object(cursor_offset: int, line: str) -> Optional[LinePart]: _current_object_attribute_re = LazyReCompile(r"([\w_][\w0-9_]*)[.]?") -def current_object_attribute( - cursor_offset: int, line: str -) -> Optional[LinePart]: +def current_object_attribute(cursor_offset: int, line: str) -> LinePart | None: """If in attribute completion, the attribute being completed""" # TODO replace with more general current_expression_attribute match = current_word(cursor_offset, line) @@ -168,9 +166,7 @@ def current_object_attribute( ) -def current_from_import_from( - cursor_offset: int, line: str -) -> Optional[LinePart]: +def current_from_import_from(cursor_offset: int, line: str) -> LinePart | None: """If in from import completion, the word after from returns None if cursor not in or just after one of the two interesting @@ -194,7 +190,7 @@ def current_from_import_from( def current_from_import_import( cursor_offset: int, line: str -) -> Optional[LinePart]: +) -> LinePart | None: """If in from import completion, the word after import being completed returns None if cursor not in or just after one of these words @@ -221,7 +217,7 @@ def current_from_import_import( _current_import_re_3 = LazyReCompile(r"[,][ ]*([\w0-9_.]*)") -def current_import(cursor_offset: int, line: str) -> Optional[LinePart]: +def current_import(cursor_offset: int, line: str) -> LinePart | None: # TODO allow for multiple as's baseline = _current_import_re_1.search(line) if baseline is None: @@ -244,7 +240,7 @@ def current_import(cursor_offset: int, line: str) -> Optional[LinePart]: def current_method_definition_name( cursor_offset: int, line: str -) -> Optional[LinePart]: +) -> LinePart | None: """The name of a method being defined""" for m in _current_method_definition_name_re.finditer(line): if m.start(1) <= cursor_offset <= m.end(1): @@ -255,7 +251,7 @@ def current_method_definition_name( _current_single_word_re = LazyReCompile(r"(? Optional[LinePart]: +def current_single_word(cursor_offset: int, line: str) -> LinePart | None: """the un-dotted word just before or under the cursor""" for m in _current_single_word_re.finditer(line): if m.start(1) <= cursor_offset <= m.end(1): @@ -263,9 +259,7 @@ def current_single_word(cursor_offset: int, line: str) -> Optional[LinePart]: return None -def current_dotted_attribute( - cursor_offset: int, line: str -) -> Optional[LinePart]: +def current_dotted_attribute(cursor_offset: int, line: str) -> LinePart | None: """The dotted attribute-object pair before the cursor""" match = current_word(cursor_offset, line) if match is not None and "." in match.word[1:]: @@ -280,7 +274,7 @@ def current_dotted_attribute( def current_expression_attribute( cursor_offset: int, line: str -) -> Optional[LinePart]: +) -> LinePart | None: """If after a dot, the attribute being completed""" # TODO replace with more general current_expression_attribute for m in _current_expression_attribute_re.finditer(line): @@ -290,7 +284,7 @@ def current_expression_attribute( def cursor_on_closing_char_pair( - cursor_offset: int, line: str, ch: Optional[str] = None + cursor_offset: int, line: str, ch: str | None = None ) -> tuple[bool, bool]: """Checks if cursor sits on closing character of a pair and whether its pair character is directly behind it diff --git a/bpython/paste.py b/bpython/paste.py index e846aba3..8ca6f2df 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -37,7 +37,7 @@ class PasteFailed(Exception): class Paster(Protocol): - def paste(self, s: str) -> tuple[str, Optional[str]]: ... + def paste(self, s: str) -> tuple[str, str | None]: ... class PastePinnwand: diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index 5bf4a45b..68787e70 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -8,9 +8,7 @@ class BPythonLinecache(dict): def __init__( self, - bpython_history: Optional[ - list[tuple[int, None, list[str], str]] - ] = None, + bpython_history: None | (list[tuple[int, None, list[str], str]]) = None, *args, **kwargs, ) -> None: diff --git a/bpython/repl.py b/bpython/repl.py index de889031..50da2d46 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -41,7 +41,6 @@ from types import ModuleType, TracebackType from typing import ( Any, - Callable, Dict, List, Literal, @@ -52,7 +51,7 @@ Union, cast, ) -from collections.abc import Iterable +from collections.abc import Callable, Iterable from pygments.lexers import Python3Lexer from pygments.token import Token, _TokenType @@ -85,9 +84,9 @@ def __enter__(self) -> None: def __exit__( self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> Literal[False]: self.last_command = time.monotonic() - self.start self.running_time += self.last_command @@ -108,7 +107,7 @@ class Interpreter(code.InteractiveInterpreter): def __init__( self, - locals: Optional[dict[str, Any]] = None, + locals: dict[str, Any] | None = None, ) -> None: """Constructor. @@ -125,7 +124,7 @@ def __init__( traceback. """ - self.syntaxerror_callback: Optional[Callable] = None + self.syntaxerror_callback: Callable | None = None if locals is None: # instead of messing with sys.modules, we should modify sys.modules @@ -139,7 +138,7 @@ def __init__( def runsource( self, source: str, - filename: Optional[str] = None, + filename: str | None = None, symbol: str = "single", ) -> bool: """Execute Python code. @@ -152,7 +151,7 @@ def runsource( with self.timer: return super().runsource(source, filename, symbol) - def showsyntaxerror(self, filename: Optional[str] = None, **kwargs) -> None: + def showsyntaxerror(self, filename: str | None = None, **kwargs) -> None: """Override the regular handler, the code's copied and pasted from code.py, as per showtraceback, but with the syntaxerror callback called and the text in a pretty colour.""" @@ -227,9 +226,9 @@ def __init__(self) -> None: # original line (before match replacements) self.orig_line = "" # class describing the current type of completion - self.completer: Optional[autocomplete.BaseCompletionType] = None - self.start: Optional[int] = None - self.end: Optional[int] = None + self.completer: autocomplete.BaseCompletionType | None = None + self.start: int | None = None + self.end: int | None = None def __nonzero__(self) -> bool: """MatchesIterator is False when word hasn't been replaced yet""" @@ -352,7 +351,7 @@ def notify( pass @abc.abstractmethod - def file_prompt(self, s: str) -> Optional[str]: + def file_prompt(self, s: str) -> str | None: pass @@ -368,7 +367,7 @@ def notify( ) -> None: pass - def file_prompt(self, s: str) -> Optional[str]: + def file_prompt(self, s: str) -> str | None: return None @@ -384,7 +383,7 @@ class _FuncExpr: function_expr: str arg_number: int opening: str - keyword: Optional[str] = None + keyword: str | None = None class Repl(metaclass=abc.ABCMeta): @@ -493,11 +492,11 @@ def __init__(self, interp: Interpreter, config: Config): self.evaluating = False self.matches_iter = MatchesIterator() self.funcprops = None - self.arg_pos: Union[str, int, None] = None + self.arg_pos: str | int | None = None self.current_func = None - self.highlighted_paren: Optional[ + self.highlighted_paren: None | ( tuple[Any, list[tuple[_TokenType, str]]] - ] = None + ) = None self._C: dict[str, int] = {} self.prev_block_finished: int = 0 self.interact: Interaction = NoInteraction(self.config) @@ -509,7 +508,7 @@ def __init__(self, interp: Interpreter, config: Config): # Necessary to fix mercurial.ui.ui expecting sys.stderr to have this # attribute self.closed = False - self.paster: Union[PasteHelper, PastePinnwand] + self.paster: PasteHelper | PastePinnwand if self.config.hist_file.exists(): try: @@ -595,7 +594,7 @@ def get_object(self, name: str) -> Any: @classmethod def _funcname_and_argnum( cls, line: str - ) -> tuple[Optional[str], Optional[Union[str, int]]]: + ) -> tuple[str | None, str | int | None]: """Parse out the current function name and arg from a line of code.""" # each element in stack is a _FuncExpr instance # if keyword is not None, we've encountered a keyword and so we're done counting @@ -715,7 +714,7 @@ def get_source_of_current_name(self) -> str: current name in the current input line. Throw `SourceNotFound` if the source cannot be found.""" - obj: Optional[Callable] = self.current_func + obj: Callable | None = self.current_func try: if obj is None: line = self.current_line @@ -761,7 +760,7 @@ def set_docstring(self) -> None: # If exactly one match that is equal to current line, clear matches # If example one match and tab=True, then choose that and clear matches - def complete(self, tab: bool = False) -> Optional[bool]: + def complete(self, tab: bool = False) -> bool | None: """Construct a full list of possible completions and display them in a window. Also check if there's an available argspec (via the inspect module) and bang that on top of the completions too. @@ -937,7 +936,7 @@ def copy2clipboard(self) -> None: else: self.interact.notify(_("Copied content to clipboard.")) - def pastebin(self, s=None) -> Optional[str]: + def pastebin(self, s=None) -> str | None: """Upload to a pastebin and display the URL in the status bar.""" if s is None: @@ -951,7 +950,7 @@ def pastebin(self, s=None) -> Optional[str]: else: return self.do_pastebin(s) - def do_pastebin(self, s) -> Optional[str]: + def do_pastebin(self, s) -> str | None: """Actually perform the upload.""" paste_url: str if s == self.prev_pastebin_content: diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 893539ea..1e26ded4 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -199,7 +199,7 @@ def find_attribute_with_name(node, name): def evaluate_current_expression( - cursor_offset: int, line: str, namespace: Optional[dict[str, Any]] = None + cursor_offset: int, line: str, namespace: dict[str, Any] | None = None ) -> Any: """ Return evaluated expression to the right of the dot of current attribute. diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 5089f304..c83ca012 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -103,9 +103,7 @@ def test_get_source_latin1(self): self.assertEqual(inspect.getsource(encoding_latin1.foo), foo_non_ascii) def test_get_source_file(self): - path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "fodder" - ) + path = os.path.join(os.path.dirname(__file__), "fodder") encoding = inspection.get_encoding_file( os.path.join(path, "encoding_ascii.py") diff --git a/bpython/test/test_line_properties.py b/bpython/test/test_line_properties.py index 5beb000b..01797827 100644 --- a/bpython/test/test_line_properties.py +++ b/bpython/test/test_line_properties.py @@ -27,7 +27,7 @@ def cursor(s): return cursor_offset, line -def decode(s: str) -> tuple[tuple[int, str], Optional[LinePart]]: +def decode(s: str) -> tuple[tuple[int, str], LinePart | None]: """'ad' -> ((3, 'abcd'), (1, 3, 'bdc'))""" if not s.count("|") == 1: @@ -52,7 +52,7 @@ def line_with_cursor(cursor_offset: int, line: str) -> str: return line[:cursor_offset] + "|" + line[cursor_offset:] -def encode(cursor_offset: int, line: str, result: Optional[LinePart]) -> str: +def encode(cursor_offset: int, line: str, result: LinePart | None) -> str: """encode(3, 'abdcd', (1, 3, 'bdc')) -> ad' Written for prettier assert error messages diff --git a/bpython/translations/__init__.py b/bpython/translations/__init__.py index 7d82dc7c..069f3465 100644 --- a/bpython/translations/__init__.py +++ b/bpython/translations/__init__.py @@ -18,7 +18,7 @@ def ngettext(singular, plural, n): def init( - locale_dir: Optional[str] = None, languages: Optional[list[str]] = None + locale_dir: str | None = None, languages: list[str] | None = None ) -> None: try: locale.setlocale(locale.LC_ALL, "") diff --git a/bpython/urwid.py b/bpython/urwid.py index 3c075d93..d94fc2e7 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -563,7 +563,7 @@ def _prompt_result(self, text): self.callback = None callback(text) - def file_prompt(self, s: str) -> Optional[str]: + def file_prompt(self, s: str) -> str | None: raise NotImplementedError diff --git a/pyproject.toml b/pyproject.toml index ca4e0450..0a891d27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 80 -target_version = ["py39"] +target_version = ["py310"] include = '\.pyi?$' exclude = ''' /( From 42408f904587c19320f4805eb96ad43bedfc74f0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 2 Jun 2025 17:15:38 +0200 Subject: [PATCH 38/70] Require urwid < 3.0 --- .github/workflows/build.yaml | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index cb6d64ad..e7852728 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -32,7 +32,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" + pip install "urwid < 3.0" twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" pip install pytest pytest-cov numpy - name: Build with Python ${{ matrix.python-version }} run: | diff --git a/setup.cfg b/setup.cfg index b3cb9a4c..7d61ee1c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ install_requires = [options.extras_require] clipboard = pyperclip jedi = jedi >= 0.16 -urwid = urwid +urwid = urwid < 3.0 watch = watchdog [options.entry_points] From 4eb111d1f4c39b242ecd0968c47e3d9d4db80d96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 06:50:34 +0000 Subject: [PATCH 39/70] Bump actions/checkout from 4 to 5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [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/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' 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 e7852728..d47929d4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -21,7 +21,7 @@ jobs: - "3.13" - "pypy-3.10" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index b6056159..46506b26 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@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 - name: Install dependencies @@ -21,7 +21,7 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: codespell-project/actions-codespell@master with: skip: "*.po,encoding_latin1.py" @@ -30,7 +30,7 @@ jobs: mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python uses: actions/setup-python@v5 - name: Install dependencies From 13336953a927ed141d08f388ef40a56f1ac2b5e6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 8 Sep 2025 10:37:56 +0200 Subject: [PATCH 40/70] CI: skip codespell of tests --- .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 46506b26..ec4c156f 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v5 - uses: codespell-project/actions-codespell@master with: - skip: "*.po,encoding_latin1.py" + skip: "*.po,encoding_latin1.py,test_repl.py" ignore_words_list: ba,te,deltion,dedent,dedented,assertIn mypy: From c07b6f3e1547bac816c37faeb077c2f4d83cd826 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 07:54:47 +0000 Subject: [PATCH 41/70] Bump actions/setup-python from 5 to 6 (#1042) --- .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 d47929d4..4372d832 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -25,7 +25,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index ec4c156f..45d4c5f6 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 - name: Install dependencies run: | python -m pip install --upgrade pip From 3e430e16dd6b3bc735a4e3cb70dd04b2b11f28e4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 24 Sep 2025 09:58:30 +0200 Subject: [PATCH 42/70] Convert sys.ps1 to a string to work-around non-str sys.ps1 from vscode (fixes #1041) --- bpython/repl.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 50da2d46..9779153e 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -535,11 +535,17 @@ def __init__(self, interp: Interpreter, config: Config): @property def ps1(self) -> str: - return cast(str, getattr(sys, "ps1", ">>> ")) + if hasattr(sys, "ps1"): + # noop in most cases, but at least vscode injects a non-str ps1 + # see #1041 + return str(sys.ps1) + return ">>> " @property def ps2(self) -> str: - return cast(str, getattr(sys, "ps2", "... ")) + if hasattr(sys, "ps2"): + return str(sys.ps2) + return ">>> " def startup(self) -> None: """ From b5f428c2953a0ff107ecfca12f5c75e2ae876b85 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 24 Sep 2025 10:28:10 +0200 Subject: [PATCH 43/70] Unbreak default ps2 --- bpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/repl.py b/bpython/repl.py index 9779153e..f07494b6 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -545,7 +545,7 @@ def ps1(self) -> str: def ps2(self) -> str: if hasattr(sys, "ps2"): return str(sys.ps2) - return ">>> " + return "... " def startup(self) -> None: """ From 064ae933ee909e87c3b698a5fdf5c3062c5a318b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 16:53:47 +0200 Subject: [PATCH 44/70] Align simple_eval with Python 3.10+ (fixes #1035) --- bpython/simpleeval.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 1e26ded4..6e911590 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -26,16 +26,13 @@ """ import ast -import sys import builtins -from typing import Dict, Any, Optional +from typing import Any from . import line as line_properties from .inspection import getattr_safe -_string_type_nodes = (ast.Str, ast.Bytes) _numeric_types = (int, float, complex) -_name_type_nodes = (ast.Name,) class EvaluationError(Exception): @@ -123,7 +120,7 @@ def _convert(node): return list() # this is a deviation from literal_eval: we allow non-literals - elif isinstance(node, _name_type_nodes): + elif isinstance(node, ast.Name): try: return namespace[node.id] except KeyError: @@ -147,7 +144,9 @@ def _convert(node): elif isinstance(node, ast.BinOp) and isinstance( node.op, (ast.Add, ast.Sub) ): - # ast.literal_eval does ast typechecks here, we use type checks + # this is a deviation from literal_eval: ast.literal_eval accepts + # (+/-) int, float and complex literals as left operand, and complex + # as right operation, we evaluate as much as possible left = _convert(node.left) right = _convert(node.right) if not ( From 0a75f527ffd675d9a9a3896d12eb737492db90a4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 17:09:27 +0200 Subject: [PATCH 45/70] Make -q hide the welcome message (fixes #1036) --- bpython/curtsies.py | 6 ++++++ bpython/curtsiesfrontend/repl.py | 15 +++------------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index b57e47a9..ae48a600 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -233,6 +233,12 @@ def curtsies_arguments(parser: argparse._ArgumentGroup) -> None: print(bpargs.version_banner()) if banner is not None: print(banner) + if welcome_message is None and not options.quiet and config.help_key: + welcome_message = ( + _("Welcome to bpython!") + + " " + + _("Press <%s> for help.") % config.help_key + ) repl = FullCurtsiesRepl(config, locals_, welcome_message, interp) try: diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 2a304312..5dd8d988 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -360,15 +360,6 @@ def __init__( if interp is None: interp = Interp(locals=locals_) interp.write = self.send_to_stdouterr # type: ignore - if banner is None: - if config.help_key: - banner = ( - _("Welcome to bpython!") - + " " - + _("Press <%s> for help.") % config.help_key - ) - else: - banner = None if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1: config.cli_suggestion_width = 1 @@ -493,15 +484,15 @@ def __init__( # The methods below should be overridden, but the default implementations # below can be used as well. - def get_cursor_vertical_diff(self): + def get_cursor_vertical_diff(self) -> int: """Return how the cursor moved due to a window size change""" return 0 - def get_top_usable_line(self): + def get_top_usable_line(self) -> int: """Return the top line of display that can be rewritten""" return 0 - def get_term_hw(self): + def get_term_hw(self) -> tuple[int, int]: """Returns the current width and height of the display area.""" return (50, 10) From 410407c3be7bcaaddab9ae5baa507bbc91047a79 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 17:21:13 +0200 Subject: [PATCH 46/70] Pass correctly typed values --- bpython/test/test_config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bpython/test/test_config.py b/bpython/test/test_config.py index 2d2e5e82..c34f2dac 100644 --- a/bpython/test/test_config.py +++ b/bpython/test/test_config.py @@ -2,10 +2,11 @@ import tempfile import textwrap import unittest +from pathlib import Path from bpython import config -TEST_THEME_PATH = os.path.join(os.path.dirname(__file__), "test.theme") +TEST_THEME_PATH = Path(os.path.join(os.path.dirname(__file__), "test.theme")) class TestConfig(unittest.TestCase): @@ -16,7 +17,7 @@ def load_temp_config(self, content): f.write(content.encode("utf8")) f.flush() - return config.Config(f.name) + return config.Config(Path(f.name)) def test_load_theme(self): color_scheme = dict() From a283365bfdf2c51fe23473714b6bb5c5caf40fa6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 17:22:09 +0200 Subject: [PATCH 47/70] Handle unspecified config paths (fixes #1027) --- bpython/config.py | 9 ++++++--- bpython/repl.py | 4 ++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index 27af8740..c309403f 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -207,13 +207,14 @@ class Config: }, } - def __init__(self, config_path: Path) -> None: + def __init__(self, config_path: Path | None = None) -> None: """Loads .ini configuration file and stores its values.""" config = ConfigParser() fill_config_with_default_values(config, self.defaults) try: - config.read(config_path) + if config_path is not None: + config.read(config_path) except UnicodeDecodeError as e: sys.stderr.write( "Error: Unable to parse config file at '{}' due to an " @@ -243,7 +244,9 @@ def get_key_no_doublebind(command: str) -> str: return requested_key - self.config_path = Path(config_path).absolute() + self.config_path = ( + config_path.absolute() if config_path is not None else None + ) self.hist_file = Path(config.get("general", "hist_file")).expanduser() self.dedent_after = config.getint("general", "dedent_after") diff --git a/bpython/repl.py b/bpython/repl.py index f07494b6..8d4c305a 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -1216,6 +1216,10 @@ def open_in_external_editor(self, filename): return subprocess.call(args) == 0 def edit_config(self): + if self.config.config_path is None: + self.interact.notify(_("No config file specified.")) + return + if not self.config.config_path.is_file(): if self.interact.confirm( _("Config file does not exist - create new from default? (y/N)") From c4d5dec67e93697dbaaf17a5a4cc728cc671411f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 17:38:59 +0200 Subject: [PATCH 48/70] Remove welcome message from tests --- bpython/test/test_curtsies_painting.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 19561efb..061facf2 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -98,7 +98,7 @@ def test_history_is_cleared(self): class TestCurtsiesPaintingSimple(CurtsiesPaintingTest): def test_startup(self): - screen = fsarray([cyan(">>> "), cyan("Welcome to")]) + screen = fsarray([cyan(">>> ")]) self.assert_paint(screen, (0, 4)) def test_enter_text(self): @@ -112,8 +112,7 @@ def test_enter_text(self): + yellow("+") + cyan(" ") + green("1") - ), - cyan("Welcome to"), + ) ] ) self.assert_paint(screen, (0, 9)) @@ -124,7 +123,7 @@ def test_run_line(self): sys.stdout = self.repl.stdout [self.repl.add_normal_character(c) for c in "1 + 1"] self.repl.on_enter(new_code=False) - screen = fsarray([">>> 1 + 1", "2", "Welcome to"]) + screen = fsarray([">>> 1 + 1", "2"]) self.assert_paint_ignoring_formatting(screen, (1, 1)) finally: sys.stdout = orig_stdout @@ -135,19 +134,10 @@ def test_completion(self): self.cursor_offset = 2 screen = self.process_box_characters( [ - ">>> an", - "┌──────────────────────────────┐", - "│ and any( │", - "└──────────────────────────────┘", - "Welcome to bpython! Press f", - ] - if sys.version_info[:2] < (3, 10) - else [ ">>> an", "┌──────────────────────────────┐", "│ and anext( any( │", "└──────────────────────────────┘", - "Welcome to bpython! Press f", ] ) self.assert_paint_ignoring_formatting(screen, (0, 4)) From 290efc6d8fde2a0a481c207675671b6ffedc23f5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 17:49:37 +0200 Subject: [PATCH 49/70] Specify test width --- bpython/test/test_curtsies_painting.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 061facf2..1ce455e5 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -98,7 +98,7 @@ def test_history_is_cleared(self): class TestCurtsiesPaintingSimple(CurtsiesPaintingTest): def test_startup(self): - screen = fsarray([cyan(">>> ")]) + screen = fsarray([cyan(">>> ")], width=10) self.assert_paint(screen, (0, 4)) def test_enter_text(self): @@ -112,8 +112,9 @@ def test_enter_text(self): + yellow("+") + cyan(" ") + green("1") - ) - ] + ), + ], + width=10, ) self.assert_paint(screen, (0, 9)) From 68bee30af321a36e3ad0393fbc590e0bc27b0995 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 17:56:05 +0200 Subject: [PATCH 50/70] Fix override --- bpython/curtsiesfrontend/repl.py | 3 ++- bpython/repl.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 5dd8d988..76ee3447 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1250,7 +1250,7 @@ def predicted_indent(self, line): logger.debug("indent we found was %s", indent) return indent - def push(self, line, insert_into_history=True): + def push(self, line, insert_into_history=True) -> bool: """Push a line of code onto the buffer, start running the buffer If the interpreter successfully runs the code, clear the buffer @@ -1297,6 +1297,7 @@ def push(self, line, insert_into_history=True): self.coderunner.load_code(code_to_run) self.run_code_and_maybe_finish() + return not code_will_parse def run_code_and_maybe_finish(self, for_code=None): r = self.coderunner.run_code(for_code=for_code) diff --git a/bpython/repl.py b/bpython/repl.py index 8d4c305a..2ced5b7a 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -989,11 +989,11 @@ def do_pastebin(self, s) -> str | None: return paste_url - def push(self, s, insert_into_history=True) -> bool: + def push(self, line, insert_into_history=True) -> bool: """Push a line of code onto the buffer so it can process it all at once when a code block ends""" # This push method is used by cli and urwid, but not curtsies - s = s.rstrip("\n") + s = line.rstrip("\n") self.buffer.append(s) if insert_into_history: From 55fa6017b0d2c0a6390c11e6140e9e2021e671c2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 17:56:16 +0200 Subject: [PATCH 51/70] Fix potentially unbound variable --- bpython/test/test_curtsies_painting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 1ce455e5..fdb9dcad 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -119,8 +119,8 @@ def test_enter_text(self): self.assert_paint(screen, (0, 9)) def test_run_line(self): + orig_stdout = sys.stdout try: - orig_stdout = sys.stdout sys.stdout = self.repl.stdout [self.repl.add_normal_character(c) for c in "1 + 1"] self.repl.on_enter(new_code=False) From a5306af72cfa44d5e33ee7291a1ad57025b8eefe Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 19:34:33 +0200 Subject: [PATCH 52/70] Bump copyright years --- bpython/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/__init__.py b/bpython/__init__.py index 26fa3e63..7d7bd28e 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-2024 {__author__}" +__copyright__ = f"(C) 2008-2025 {__author__}" __license__ = "MIT" __version__ = version package_dir = os.path.abspath(os.path.dirname(__file__)) From 9b995435b2a06373aadbdb744c20fe75a86aa0f9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 19:35:17 +0200 Subject: [PATCH 53/70] Remove unused imports --- bpython/args.py | 1 - bpython/autocomplete.py | 4 ---- bpython/curtsiesfrontend/_internal.py | 2 +- bpython/curtsiesfrontend/filewatch.py | 1 - bpython/curtsiesfrontend/interpreter.py | 2 +- bpython/curtsiesfrontend/parse.py | 2 +- bpython/curtsiesfrontend/repl.py | 6 ------ bpython/filelock.py | 2 +- bpython/history.py | 2 +- bpython/importcompletion.py | 1 - bpython/inspection.py | 4 ---- bpython/keys.py | 2 +- bpython/lazyre.py | 1 - bpython/line.py | 1 - bpython/pager.py | 1 - bpython/paste.py | 2 +- bpython/patch_linecache.py | 2 +- bpython/urwid.py | 1 - 18 files changed, 8 insertions(+), 29 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 35fd3e7b..cee4bcbf 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -36,7 +36,6 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional from collections.abc import Callable from types import ModuleType diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 4fb62f72..77887ef4 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -39,11 +39,7 @@ from enum import Enum from typing import ( Any, - Dict, - List, Optional, - Set, - Tuple, ) from collections.abc import Iterator, Sequence diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index 8c070b34..72572b0b 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -22,7 +22,7 @@ import pydoc from types import TracebackType -from typing import Optional, Type, Literal +from typing import Literal from .. import _internal diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 2822db6d..b9778c97 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -1,6 +1,5 @@ import os from collections import defaultdict -from typing import Dict, Set, List from collections.abc import Callable, Iterable, Sequence from .. import importcompletion diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 280c56ed..9382db6b 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -1,6 +1,6 @@ import sys from codeop import CommandCompiler -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any from collections.abc import Iterable from pygments.token import Generic, Token, Keyword, Name, Comment, String diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 28b32e64..122f1ee9 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -1,6 +1,6 @@ import re from functools import partial -from typing import Any, Dict, Tuple +from typing import Any from collections.abc import Callable from curtsies.formatstring import fmtstr, FmtStr diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 76ee3447..928be253 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -14,13 +14,7 @@ from types import FrameType, TracebackType from typing import ( Any, - Dict, - List, Literal, - Optional, - Tuple, - Type, - Union, ) from collections.abc import Iterable, Sequence diff --git a/bpython/filelock.py b/bpython/filelock.py index b8eb11ff..add2eb81 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -20,7 +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, Literal +from typing import IO, Literal from types import TracebackType has_fcntl = True diff --git a/bpython/history.py b/bpython/history.py index b58309b5..27852e83 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 Optional, List, TextIO +from typing import TextIO from collections.abc import Iterable from .translations import _ diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 570996d4..00860c16 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -27,7 +27,6 @@ import warnings from dataclasses import dataclass from pathlib import Path -from typing import Optional, Set, Union from collections.abc import Generator, Sequence, Iterable from .line import ( diff --git a/bpython/inspection.py b/bpython/inspection.py index 63f0e2d3..d3e2d5e5 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -28,10 +28,6 @@ from dataclasses import dataclass from typing import ( Any, - Optional, - Type, - Dict, - List, ContextManager, Literal, ) diff --git a/bpython/keys.py b/bpython/keys.py index 1068a4f2..51f4c011 100644 --- a/bpython/keys.py +++ b/bpython/keys.py @@ -21,7 +21,7 @@ # THE SOFTWARE. import string -from typing import TypeVar, Generic, Tuple, Dict +from typing import TypeVar, Generic T = TypeVar("T") diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 1d903616..3d1bd372 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -24,7 +24,6 @@ from collections.abc import Iterator from functools import cached_property from re import Pattern, Match -from typing import Optional, Optional class LazyReCompile: diff --git a/bpython/line.py b/bpython/line.py index e64b20d9..83a75f09 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -8,7 +8,6 @@ from dataclasses import dataclass from itertools import chain -from typing import Optional, Tuple from .lazyre import LazyReCompile diff --git a/bpython/pager.py b/bpython/pager.py index 2fa4846e..deb144b9 100644 --- a/bpython/pager.py +++ b/bpython/pager.py @@ -30,7 +30,6 @@ import subprocess import sys import shlex -from typing import List def get_pager_command(default: str = "less -rf") -> list[str]: diff --git a/bpython/paste.py b/bpython/paste.py index 8ca6f2df..e43ce2f2 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -22,7 +22,7 @@ import errno import subprocess -from typing import Optional, Tuple, Protocol +from typing import Protocol from urllib.parse import urljoin, urlparse import requests diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index 68787e70..fa8e1729 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -1,5 +1,5 @@ import linecache -from typing import Any, List, Tuple, Optional +from typing import Any class BPythonLinecache(dict): diff --git a/bpython/urwid.py b/bpython/urwid.py index d94fc2e7..40abb421 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -38,7 +38,6 @@ import locale import signal import urwid -from typing import Optional from . import args as bpargs, repl, translations from .formatter import theme_map From 4709825cb96129d932207d437399630e5ece7796 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 20:54:37 +0200 Subject: [PATCH 54/70] Remove unused function --- bpython/curtsiesfrontend/manual_readline.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py index 206e5278..3d02c024 100644 --- a/bpython/curtsiesfrontend/manual_readline.py +++ b/bpython/curtsiesfrontend/manual_readline.py @@ -4,9 +4,9 @@ and the cursor location based on http://www.bigsmoke.us/readline/shortcuts""" -from ..lazyre import LazyReCompile import inspect +from ..lazyre import LazyReCompile from ..line import cursor_on_closing_char_pair INDENT = 4 @@ -68,12 +68,6 @@ def call(self, key, **kwargs): args = {k: v for k, v in kwargs.items() if k in params} return func(**args) - def call_without_cut(self, key, **kwargs): - """Looks up the function and calls it, returning only line and cursor - offset""" - r = self.call_for_two(key, **kwargs) - return r[:2] - def __contains__(self, key): return key in self.simple_edits or key in self.cut_buffer_edits From a04d423f253630bef8ad9d25b4c2c5be7dc354ac Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 21:27:17 +0200 Subject: [PATCH 55/70] Refactor --- bpython/filelock.py | 108 ++++++++++++++++++++++---------------------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/bpython/filelock.py b/bpython/filelock.py index add2eb81..c106c415 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -23,20 +23,6 @@ from typing import IO, Literal from types import TracebackType -has_fcntl = True -try: - import fcntl - import errno -except ImportError: - has_fcntl = False - -has_msvcrt = True -try: - import msvcrt - import os -except ImportError: - has_msvcrt = False - class BaseLock: """Base class for file locking""" @@ -69,56 +55,72 @@ def __del__(self) -> None: self.release() -class UnixFileLock(BaseLock): - """Simple file locking for Unix using fcntl""" +try: + import fcntl + import errno - def __init__(self, fileobj, mode: int = 0) -> None: - super().__init__() - self.fileobj = fileobj - self.mode = mode | fcntl.LOCK_EX + class UnixFileLock(BaseLock): + """Simple file locking for Unix using fcntl""" - def acquire(self) -> None: - try: - fcntl.flock(self.fileobj, self.mode) - self.locked = True - except OSError as e: - if e.errno != errno.ENOLCK: - raise e + def __init__(self, fileobj, mode: int = 0) -> None: + super().__init__() + self.fileobj = fileobj + self.mode = mode | fcntl.LOCK_EX - def release(self) -> None: - self.locked = False - fcntl.flock(self.fileobj, fcntl.LOCK_UN) + def acquire(self) -> None: + try: + fcntl.flock(self.fileobj, self.mode) + self.locked = True + except OSError as e: + if e.errno != errno.ENOLCK: + raise e + def release(self) -> None: + self.locked = False + fcntl.flock(self.fileobj, fcntl.LOCK_UN) -class WindowsFileLock(BaseLock): - """Simple file locking for Windows using msvcrt""" + has_fcntl = True +except ImportError: + has_fcntl = False - def __init__(self, filename: str) -> None: - super().__init__() - self.filename = f"{filename}.lock" - self.fileobj = -1 - def acquire(self) -> None: - # create a lock file and lock it - self.fileobj = os.open( - self.filename, os.O_RDWR | os.O_CREAT | os.O_TRUNC - ) - msvcrt.locking(self.fileobj, msvcrt.LK_NBLCK, 1) +try: + import msvcrt + import os - self.locked = True + class WindowsFileLock(BaseLock): + """Simple file locking for Windows using msvcrt""" - def release(self) -> None: - self.locked = False + def __init__(self, filename: str) -> None: + super().__init__() + self.filename = f"{filename}.lock" + self.fileobj = -1 + + def acquire(self) -> None: + # create a lock file and lock it + self.fileobj = os.open( + self.filename, os.O_RDWR | os.O_CREAT | os.O_TRUNC + ) + msvcrt.locking(self.fileobj, msvcrt.LK_NBLCK, 1) + + self.locked = True - # unlock lock file and remove it - msvcrt.locking(self.fileobj, msvcrt.LK_UNLCK, 1) - os.close(self.fileobj) - self.fileobj = -1 + def release(self) -> None: + self.locked = False - try: - os.remove(self.filename) - except OSError: - pass + # unlock lock file and remove it + msvcrt.locking(self.fileobj, msvcrt.LK_UNLCK, 1) + os.close(self.fileobj) + self.fileobj = -1 + + try: + os.remove(self.filename) + except OSError: + pass + + has_msvcrt = True +except ImportError: + has_msvcrt = False def FileLock( From 82416c3c023f5f2cb5ec3f3833f8c785d41022cd Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Sep 2025 21:27:23 +0200 Subject: [PATCH 56/70] Refactor --- bpython/pager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bpython/pager.py b/bpython/pager.py index deb144b9..af9370d6 100644 --- a/bpython/pager.py +++ b/bpython/pager.py @@ -33,8 +33,7 @@ def get_pager_command(default: str = "less -rf") -> list[str]: - command = shlex.split(os.environ.get("PAGER", default)) - return command + return shlex.split(os.environ.get("PAGER", default)) def page_internal(data: str) -> None: From aed3ebcd81866f4a187cf3654201ecc153ebe1ec Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 27 Oct 2025 20:55:48 +0100 Subject: [PATCH 57/70] Update changelog for 0.26 release --- CHANGELOG.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f55fe76f..a4aa42d2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,16 +6,21 @@ Changelog General information: +* This release is focused on Python 3.14 support. New features: Fixes: - +* #1027: Handle unspecified config paths +* #1035: Align simple_eval with Python 3.10+ +* #1036: Make -q hide the welcome message +* #1041: Convert sys.ps1 to a string to work-around non-str sys.ps1 from vscode Changes to dependencies: +Support for Python 3.14 has been added. Support for Python 3.9 has been dropped. 0.25 ---- From ec6603d790d81ac23c4fd54e2fa26f744651a2bb Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 28 Oct 2025 08:30:37 +0100 Subject: [PATCH 58/70] Start development of 0.27 --- CHANGELOG.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a4aa42d2..34dd4fb5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,21 @@ Changelog ========= +0.27 +---- + +General information: + + +New features: + + +Fixes: + + +Changes to dependencies: + + 0.26 ---- From c1f4c331dd35c4ffec2f92cea24b1a6860240382 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 28 Oct 2025 08:31:20 +0100 Subject: [PATCH 59/70] Update meta data to require >= 3.10 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 7d61ee1c..dee3e274 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ classifiers = Programming Language :: Python :: 3 [options] -python_requires = >=3.9 +python_requires = >=3.10 packages = bpython bpython.curtsiesfrontend From 3d6389bd877331478de913a5ae8eb8bd7cef1f01 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 28 Oct 2025 21:58:13 +0100 Subject: [PATCH 60/70] Drop support for Python 3.10 --- .github/workflows/build.yaml | 3 +-- bpython/_typing_compat.py | 27 --------------------------- bpython/args.py | 6 +++--- pyproject.toml | 2 +- setup.cfg | 2 +- 5 files changed, 6 insertions(+), 34 deletions(-) delete mode 100644 bpython/_typing_compat.py diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 4372d832..b5e05759 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,11 +15,10 @@ jobs: fail-fast: false matrix: python-version: - - "3.10" - "3.11" - "3.12" - "3.13" - - "pypy-3.10" + - "pypy-3.11" steps: - uses: actions/checkout@v5 with: diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py deleted file mode 100644 index 5d9a3607..00000000 --- a/bpython/_typing_compat.py +++ /dev/null @@ -1,27 +0,0 @@ -# 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_extensions import Never # type: ignore diff --git a/bpython/args.py b/bpython/args.py index cee4bcbf..ac78267a 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -1,7 +1,7 @@ # The MIT License # # Copyright (c) 2008 Bob Farrell -# Copyright (c) 2012-2021 Sebastian Ramacher +# Copyright (c) 2012-2025 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 @@ -38,11 +38,11 @@ from pathlib import Path from collections.abc import Callable from types import ModuleType +from typing import Never from . import __version__, __copyright__ from .config import default_config_path, Config from .translations import _ -from ._typing_compat import Never logger = logging.getLogger(__name__) @@ -52,7 +52,7 @@ class ArgumentParserFailed(ValueError): class RaisingArgumentParser(argparse.ArgumentParser): - def error(self, msg: str) -> Never: + def error(self, message: str) -> Never: raise ArgumentParserFailed() diff --git a/pyproject.toml b/pyproject.toml index 0a891d27..40efff3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 80 -target_version = ["py310"] +target_version = ["py311"] include = '\.pyi?$' exclude = ''' /( diff --git a/setup.cfg b/setup.cfg index dee3e274..07fb34e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ classifiers = Programming Language :: Python :: 3 [options] -python_requires = >=3.10 +python_requires = >=3.11 packages = bpython bpython.curtsiesfrontend From d5a8af480b2fb8aa5cecc1e932316d1b666dbae8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 28 Oct 2025 21:58:22 +0100 Subject: [PATCH 61/70] CI: test 3.14 --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b5e05759..0d297e51 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -18,6 +18,7 @@ jobs: - "3.11" - "3.12" - "3.13" + - "3.14" - "pypy-3.11" steps: - uses: actions/checkout@v5 From 20241ea355fe099e977beff40af7f2313b80c541 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 28 Oct 2025 22:03:31 +0100 Subject: [PATCH 62/70] Remove workaround for < Python 3.11 --- bpython/test/test_interpreter.py | 87 +++++--------------------------- 1 file changed, 12 insertions(+), 75 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index b9f0a31e..7245ce83 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -21,66 +21,17 @@ def test_syntaxerror(self): i.runsource("1.1.1.1") - if (3, 10, 1) <= sys.version_info[:3]: - expected = ( - " File " - + green('""') - + ", line " - + bold(magenta("1")) - + "\n 1.1.1.1\n ^^\n" - + bold(red("SyntaxError")) - + ": " - + cyan("invalid syntax") - + "\n" - ) - elif (3, 10) <= sys.version_info[:2]: - expected = ( - " File " - + green('""') - + ", line " - + bold(magenta("1")) - + "\n 1.1.1.1\n ^^^^^\n" - + bold(red("SyntaxError")) - + ": " - + cyan("invalid syntax. Perhaps you forgot a comma?") - + "\n" - ) - elif (3, 8) <= sys.version_info[:2]: - expected = ( - " File " - + green('""') - + ", line " - + bold(magenta("1")) - + "\n 1.1.1.1\n ^\n" - + bold(red("SyntaxError")) - + ": " - + cyan("invalid syntax") - + "\n" - ) - elif pypy: - expected = ( - " File " - + green('""') - + ", line " - + bold(magenta("1")) - + "\n 1.1.1.1\n ^\n" - + bold(red("SyntaxError")) - + ": " - + cyan("invalid syntax") - + "\n" - ) - else: - expected = ( - " File " - + green('""') - + ", line " - + bold(magenta("1")) - + "\n 1.1.1.1\n ^\n" - + bold(red("SyntaxError")) - + ": " - + cyan("invalid syntax") - + "\n" - ) + expected = ( + " File " + + green('""') + + ", line " + + bold(magenta("1")) + + "\n 1.1.1.1\n ^^\n" + + bold(red("SyntaxError")) + + ": " + + cyan("invalid syntax") + + "\n" + ) a = i.a self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) @@ -114,7 +65,7 @@ def gfunc(): + cyan(global_not_found) + "\n" ) - elif (3, 11) <= sys.version_info[:2]: + else: expected = ( "Traceback (most recent call last):\n File " + green('""') @@ -129,20 +80,6 @@ def gfunc(): + 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" - ) a = i.a self.assertMultiLineEqual(str(expected), str(plain("").join(a))) From b6494ffba5f0abdb4596bb2ba059a2c811438ded Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 28 Oct 2025 22:07:08 +0100 Subject: [PATCH 63/70] Fix tests with pypy --- 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 7245ce83..e5bc0895 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -50,7 +50,7 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - if (3, 13) <= sys.version_info[:2]: + if (3, 13) <= sys.version_info[:2] or pypy: expected = ( "Traceback (most recent call last):\n File " + green('""') From 11d63cc04b34753e05c7131bdcd0c8c89e3d5d1c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 28 Oct 2025 23:01:23 +0100 Subject: [PATCH 64/70] Require urwid >= 1.0 --- bpython/urwid.py | 34 +--------------------------------- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 34 deletions(-) diff --git a/bpython/urwid.py b/bpython/urwid.py index 40abb421..882641e8 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -95,39 +95,7 @@ def buildProtocol(self, addr): # If Twisted is not available urwid has no TwistedEventLoop attribute. # Code below will try to import reactor before using TwistedEventLoop. # I assume TwistedEventLoop will be available if that import succeeds. -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 - of stopping it. One obvious way this breaks is if anything used - the reactor's thread pool: that thread pool is not shut down if - the reactor is not stopped, which means python hangs on exit - (joining the non-daemon threadpool threads that never exit). And - the default resolver is the ThreadedResolver, so if we looked up - any names we hang on exit. That is bad enough that we hack up - urwid a bit here to exit properly. - """ - - def handle_exit(self, f): - def wrapper(*args, **kwargs): - try: - return f(*args, **kwargs) - except urwid.ExitMainLoop: - # This is our change. - self.reactor.stop() - except: - # This is the same as in urwid. - # We are obviously not supposed to ever hit this. - print(sys.exc_info()) - self._exc_info = sys.exc_info() - self.reactor.crash() - - return wrapper - -else: - TwistedEventLoop = getattr(urwid, "TwistedEventLoop", None) +TwistedEventLoop = getattr(urwid, "TwistedEventLoop", None) class StatusbarEdit(urwid.Edit): diff --git a/setup.cfg b/setup.cfg index 07fb34e5..f14998b7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ install_requires = [options.extras_require] clipboard = pyperclip jedi = jedi >= 0.16 -urwid = urwid < 3.0 +urwid = urwid >=1.0,< 3.0 watch = watchdog [options.entry_points] From bbc9438a2638d60f131ede56d7c21a6f96592927 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 28 Oct 2025 23:12:28 +0100 Subject: [PATCH 65/70] Fix compatibility with urwid 3.0 (fixes #1043) --- .github/workflows/build.yaml | 2 +- bpython/urwid.py | 8 ++++++-- setup.cfg | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 0d297e51..1b6fabf3 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 < 3.0" twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" + pip install "urwid >= 1.0" twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" pip install pytest pytest-cov numpy - name: Build with Python ${{ matrix.python-version }} run: | diff --git a/bpython/urwid.py b/bpython/urwid.py index 882641e8..8be00370 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -411,7 +411,7 @@ def keypress(self, size, key): return key -class Tooltip(urwid.BoxWidget): +class Tooltip(urwid.Widget): """Container inspired by Overlay to position our tooltip. bottom_w should be a BoxWidget. @@ -423,6 +423,9 @@ class Tooltip(urwid.BoxWidget): from the bottom window and hides it if there is no cursor. """ + _sizing = frozenset(['box']) + _selectable = True + def __init__(self, bottom_w, listbox): super().__init__() @@ -1322,7 +1325,8 @@ def run_find_coroutine(): run_find_coroutine() - myrepl.main_loop.screen.run_wrapper(run_with_screen_before_mainloop) + with myrepl.main_loop.screen.start(): + run_with_screen_before_mainloop() if config.flush_output and not options.quiet: sys.stdout.write(myrepl.getstdout()) diff --git a/setup.cfg b/setup.cfg index f14998b7..e1719921 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ install_requires = [options.extras_require] clipboard = pyperclip jedi = jedi >= 0.16 -urwid = urwid >=1.0,< 3.0 +urwid = urwid >=1.0 watch = watchdog [options.entry_points] From 105380ad95063a91e153dde41a198accc38917e6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 28 Oct 2025 23:15:26 +0100 Subject: [PATCH 66/70] Apply black --- bpython/urwid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/urwid.py b/bpython/urwid.py index 8be00370..123417d4 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -423,7 +423,7 @@ class Tooltip(urwid.Widget): from the bottom window and hides it if there is no cursor. """ - _sizing = frozenset(['box']) + _sizing = frozenset(["box"]) _selectable = True def __init__(self, bottom_w, listbox): From 445aff06400cad84e2482c63199dfda0a848dd59 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 28 Oct 2025 23:20:26 +0100 Subject: [PATCH 67/70] Remove unnecessary version check --- bpython/importcompletion.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 00860c16..e22b61f6 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -48,12 +48,8 @@ ), ) -_LOADED_INODE_DATACLASS_ARGS = {"frozen": True} -if sys.version_info[:2] >= (3, 10): - _LOADED_INODE_DATACLASS_ARGS["slots"] = True - -@dataclass(**_LOADED_INODE_DATACLASS_ARGS) +@dataclass(frozen=True, slots=True) class _LoadedInode: dev: int inode: int From e33828ea2c908a6716610a00f25bc2eff025adbe Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 28 Oct 2025 23:25:01 +0100 Subject: [PATCH 68/70] Remove unnecessary version checks --- bpython/test/test_inspection.py | 10 +--------- bpython/test/test_simpleeval.py | 3 --- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index c83ca012..30e91102 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -11,7 +11,6 @@ from bpython.test.fodder import encoding_utf8 pypy = "PyPy" in sys.version -_is_py311 = sys.version_info[:2] >= (3, 11) try: import numpy @@ -127,14 +126,7 @@ def test_getfuncprops_print(self): self.assertIn("file", props.argspec.kwonly) self.assertIn("flush", props.argspec.kwonly) self.assertIn("sep", props.argspec.kwonly) - 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["file"]), "None") self.assertEqual(repr(props.argspec.kwonly_defaults["flush"]), "False") @unittest.skipUnless( diff --git a/bpython/test/test_simpleeval.py b/bpython/test/test_simpleeval.py index 1d1a3f1a..8bdb1929 100644 --- a/bpython/test/test_simpleeval.py +++ b/bpython/test/test_simpleeval.py @@ -20,9 +20,6 @@ def test_matches_stdlib(self): self.assertMatchesStdlib("{(1,): [2,3,{}]}") self.assertMatchesStdlib("{1, 2}") - @unittest.skipUnless( - sys.version_info[:2] >= (3, 9), "Only Python3.9 evaluates set()" - ) def test_matches_stdlib_set_literal(self): """set() is evaluated""" self.assertMatchesStdlib("set()") From 9004accdf1cf971caccc58e9d4acdd997687b80b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 08:19:39 +0000 Subject: [PATCH 69/70] Bump actions/checkout from 5 to 6 (#1048) --- .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 1b6fabf3..8e04fae2 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -21,7 +21,7 @@ jobs: - "3.14" - "pypy-3.11" steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 45d4c5f6..8caf9562 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@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 - name: Install dependencies @@ -21,7 +21,7 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: codespell-project/actions-codespell@master with: skip: "*.po,encoding_latin1.py,test_repl.py" @@ -30,7 +30,7 @@ jobs: mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v6 - name: Install dependencies From 89e2f64050cda18f95d56141c3b9cac3a5a609f9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 28 Oct 2025 23:37:02 +0100 Subject: [PATCH 70/70] Refactor --- bpython/urwid.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/bpython/urwid.py b/bpython/urwid.py index 123417d4..d4899332 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -225,17 +225,11 @@ def _on_prompt_enter(self, edit, new_text): urwid.register_signal(Statusbar, "prompt_result") -def decoding_input_filter(keys, raw): +def decoding_input_filter(keys: list[str], _raw: list[int]) -> list[str]: """Input filter for urwid which decodes each key with the locale's preferred encoding.'""" encoding = locale.getpreferredencoding() - converted_keys = list() - for key in keys: - if isinstance(key, str): - converted_keys.append(key.decode(encoding)) - else: - converted_keys.append(key) - return converted_keys + return [key.decode(encoding) for key in keys] def format_tokens(tokensource):