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
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 4372d832..1b14b0cd 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: |
@@ -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:
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/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)
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
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..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):
@@ -21,66 +18,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))
@@ -97,56 +45,9 @@ def gfunc():
i.runsource("gfunc()")
- global_not_found = "name 'gfunc' is not defined"
-
- if (3, 13) <= sys.version_info[:2]:
- expected = (
- "Traceback (most recent call last):\n File "
- + green('""')
- + ", line "
- + bold(magenta("1"))
- + ", in "
- + cyan("")
- + "\n gfunc()"
- + "\n ^^^^^\n"
- + bold(red("NameError"))
- + ": "
- + cyan(global_not_found)
- + "\n"
- )
- elif (3, 11) <= sys.version_info[:2]:
- expected = (
- "Traceback (most recent call last):\n File "
- + green('""')
- + ", line "
- + bold(magenta("1"))
- + ", in "
- + cyan("")
- + "\n gfunc()"
- + "\n ^^^^^\n"
- + bold(red("NameError"))
- + ": "
- + cyan(global_not_found)
- + "\n"
- )
- else:
- expected = (
- "Traceback (most recent call last):\n File "
- + green('""')
- + ", line "
- + bold(magenta("1"))
- + ", in "
- + cyan("")
- + "\n gfunc()\n"
- + bold(red("NameError"))
- + ": "
- + cyan(global_not_found)
- + "\n"
- )
-
- 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"
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]