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]