diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 4372d832..8e04fae2 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -15,13 +15,13 @@ jobs:
fail-fast: false
matrix:
python-version:
- - "3.10"
- "3.11"
- "3.12"
- "3.13"
- - "pypy-3.10"
+ - "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 }}
@@ -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/.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
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
----
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/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
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_interpreter.py b/bpython/test/test_interpreter.py
index b9f0a31e..e5bc0895 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))
@@ -99,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('""')
@@ -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)))
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()")
diff --git a/bpython/urwid.py b/bpython/urwid.py
index 40abb421..d4899332 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):
@@ -257,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):
@@ -443,7 +405,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.
@@ -455,6 +417,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__()
@@ -1354,7 +1319,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/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 7d61ee1c..e1719921 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -15,7 +15,7 @@ classifiers =
Programming Language :: Python :: 3
[options]
-python_requires = >=3.9
+python_requires = >=3.11
packages =
bpython
bpython.curtsiesfrontend
@@ -35,7 +35,7 @@ install_requires =
[options.extras_require]
clipboard = pyperclip
jedi = jedi >= 0.16
-urwid = urwid < 3.0
+urwid = urwid >=1.0
watch = watchdog
[options.entry_points]