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 01/56] 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 02/56] 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 03/56] 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 04/56] 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 05/56] 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 06/56] 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 07/56] 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 08/56] 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 09/56] 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 10/56] 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 11/56] 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 12/56] 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 13/56] 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 14/56] 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 15/56] 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 16/56] 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 17/56] 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 18/56] 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 19/56] 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 20/56] 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 21/56] 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 22/56] 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 23/56] 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 24/56] 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 25/56] 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 26/56] 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 27/56] 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 28/56] 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 29/56] 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 30/56] 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 31/56] 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 32/56] 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 33/56] 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 34/56] 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 35/56] 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 36/56] 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 37/56] 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 38/56] 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 39/56] 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 40/56] 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 41/56] 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 42/56] 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 43/56] 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 44/56] 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 45/56] 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 46/56] 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 47/56] 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 48/56] 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 49/56] 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): From c44d9172ea16802a1d01a36cd0b283ba712a7d80 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 26 Mar 2026 14:04:23 +0100 Subject: [PATCH 50/56] Remove unused flake8 config --- .flake8 | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index c321e71c..00000000 --- a/.flake8 +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -ignore = E203, E266, E501, W503 -max-line-length = 80 -max-complexity = 18 -select = B,C,E,F,W,T4,B9 From 3dd1034c55e15cc8238a262c2fb60b16bf27a0af Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 26 Mar 2026 14:14:57 +0100 Subject: [PATCH 51/56] Add more type annotations --- bpython/repl.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 2ced5b7a..93ce5cbc 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -337,7 +337,7 @@ def clear(self) -> None: class Interaction(metaclass=abc.ABCMeta): - def __init__(self, config: Config): + def __init__(self, config: Config) -> None: self.config = config @abc.abstractmethod @@ -356,7 +356,7 @@ def file_prompt(self, s: str) -> str | None: class NoInteraction(Interaction): - def __init__(self, config: Config): + def __init__(self, config: Config) -> None: super().__init__(config) def confirm(self, s: str) -> bool: @@ -467,7 +467,7 @@ def cursor_offset(self, value: int) -> None: # not actually defined, subclasses must define cpos: int - def __init__(self, interp: Interpreter, config: Config): + def __init__(self, interp: Interpreter, config: Config) -> None: """Initialise the repl. interp is a Python code.InteractiveInterpreter instance @@ -851,7 +851,7 @@ def next_indentation(self) -> int: ) if indentation and self.config.dedent_after > 0: - def line_is_empty(line): + def line_is_empty(line: str) -> bool: return not line.strip() empty_lines = takewhile(line_is_empty, reversed(self.buffer)) @@ -942,7 +942,7 @@ def copy2clipboard(self) -> None: else: self.interact.notify(_("Copied content to clipboard.")) - def pastebin(self, s=None) -> str | None: + def pastebin(self, s: str | None = None) -> str | None: """Upload to a pastebin and display the URL in the status bar.""" if s is None: @@ -956,9 +956,8 @@ def pastebin(self, s=None) -> str | None: else: return self.do_pastebin(s) - def do_pastebin(self, s) -> str | None: + def do_pastebin(self, s: str) -> str | None: """Actually perform the upload.""" - paste_url: str if s == self.prev_pastebin_content: self.interact.notify( _("Duplicate pastebin. Previous URL: %s. " "Removal URL: %s") @@ -989,7 +988,7 @@ def do_pastebin(self, s) -> str | None: return paste_url - def push(self, line, insert_into_history=True) -> bool: + def push(self, line: str, insert_into_history: bool = 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 From 4697efb6432e1e1de59785f73a554f7e85d59150 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 26 Mar 2026 14:24:20 +0100 Subject: [PATCH 52/56] Fix test with recent versions of Python 3.13 and 3.14 --- bpython/test/test_interpreter.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index e5bc0895..32082ff8 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -50,7 +50,24 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - if (3, 13) <= sys.version_info[:2] or pypy: + if ( + sys.version_info[:2] == (3, 13) + and sys.version_info[:3] >= (3, 13, 12) + ) or sys.version_info[:3] >= (3, 14, 3): + expected = expected = ( + "Traceback (most recent call last):\n File " + + green('""') + + ", line " + + bold(magenta("1")) + + ", in " + + cyan("") + + "\n" + + bold(red("NameError")) + + ": " + + cyan(global_not_found) + + "\n" + ) + elif (3, 13) <= sys.version_info[:2] or pypy: expected = ( "Traceback (most recent call last):\n File " + green('""') From 870e81cb5a6860f1ba15744c81b97f71467eedf9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 26 Mar 2026 14:53:38 +0100 Subject: [PATCH 53/56] Fix compatibility of linecache with Python 3.13.12 and Python 3.14.3 --- bpython/patch_linecache.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index fa8e1729..78b35684 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -36,6 +36,11 @@ def remember_bpython_input(self, source: str) -> str: ) return filename + def get(self, key: Any, default: Any | None = None) -> Any: + if self.is_bpython_filename(key): + return self.get_bpython_history(key) + return super().get(key, default) + def __getitem__(self, key: Any) -> Any: if self.is_bpython_filename(key): return self.get_bpython_history(key) From 27fc6a0fb2710dc4f4b61a98fa45dd1f62c6bba7 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 26 Mar 2026 15:45:47 +0100 Subject: [PATCH 54/56] Make test_traceback version agnostic --- bpython/test/test_interpreter.py | 56 ++------------------------------ 1 file changed, 3 insertions(+), 53 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 32082ff8..b9a636db 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -48,59 +48,9 @@ def gfunc(): i.runsource("gfunc()") - global_not_found = "name 'gfunc' is not defined" - - if ( - sys.version_info[:2] == (3, 13) - and sys.version_info[:3] >= (3, 13, 12) - ) or sys.version_info[:3] >= (3, 14, 3): - expected = expected = ( - "Traceback (most recent call last):\n File " - + green('""') - + ", line " - + bold(magenta("1")) - + ", in " - + cyan("") - + "\n" - + bold(red("NameError")) - + ": " - + cyan(global_not_found) - + "\n" - ) - elif (3, 13) <= sys.version_info[:2] or pypy: - expected = ( - "Traceback (most recent call last):\n File " - + green('""') - + ", line " - + bold(magenta("1")) - + ", in " - + cyan("") - + "\n gfunc()" - + "\n ^^^^^\n" - + bold(red("NameError")) - + ": " - + cyan(global_not_found) - + "\n" - ) - else: - expected = ( - "Traceback (most recent call last):\n File " - + green('""') - + ", line " - + bold(magenta("1")) - + ", in " - + cyan("") - + "\n gfunc()" - + "\n ^^^^^\n" - + bold(red("NameError")) - + ": " - + cyan(global_not_found) - + "\n" - ) - - a = i.a - self.assertMultiLineEqual(str(expected), str(plain("").join(a))) - self.assertEqual(expected, plain("").join(a)) + a = str(plain("").join(i.a)) + self.assertIn("name 'gfunc' is not defined", a) + self.assertIn("NameErro", a) def test_getsource_works_on_interactively_defined_functions(self): source = "def foo(x):\n return x + 1\n" From c86dbd9335445d0ac0f80db10b205d7326d4e7a4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 26 Mar 2026 15:46:17 +0100 Subject: [PATCH 55/56] Remove unused pypy check --- bpython/test/test_interpreter.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index b9a636db..3d40d198 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -1,12 +1,9 @@ -import sys import unittest from curtsies.fmtfuncs import bold, green, magenta, cyan, red, plain from bpython.curtsiesfrontend import interpreter -pypy = "PyPy" in sys.version - class Interpreter(interpreter.Interp): def __init__(self): From 795453606de4e0b6a13e132ec59a041d5a2e89fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 04:54:10 +0000 Subject: [PATCH 56/56] Bump codecov/codecov-action from 5 to 6 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6. - [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/v5...v6) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: '6' 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 8e04fae2..1b14b0cd 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -45,7 +45,7 @@ jobs: run: | pytest --cov=bpython --cov-report=xml -v - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 env: PYTHON_VERSION: ${{ matrix.python-version }} with: