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 e7852728..ec63d6ff 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,24 +15,24 @@ 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@v4 + - uses: actions/checkout@v6 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 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@v7 env: PYTHON_VERSION: ${{ matrix.python-version }} with: diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index b6056159..8caf9562 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -8,9 +8,9 @@ jobs: black: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -21,18 +21,18 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - 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: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f55fe76f..34dd4fb5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -0.26 +0.27 ---- General information: @@ -16,6 +16,26 @@ Fixes: Changes to dependencies: +0.26 +---- + +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 ---- 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__)) 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 35fd3e7b..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 @@ -36,14 +36,13 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional 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__) @@ -53,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/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/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/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/_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/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 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 2a304312..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 @@ -360,15 +354,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 +478,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) @@ -1259,7 +1244,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 @@ -1306,6 +1291,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/filelock.py b/bpython/filelock.py index b8eb11ff..c106c415 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -20,23 +20,9 @@ # 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 -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( 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..e22b61f6 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 ( @@ -49,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/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..af9370d6 100644 --- a/bpython/pager.py +++ b/bpython/pager.py @@ -30,12 +30,10 @@ import subprocess import sys import shlex -from typing import List 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: 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..78b35684 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): @@ -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 50da2d46..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 @@ -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: """ @@ -845,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)) @@ -936,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: @@ -950,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") @@ -983,11 +988,11 @@ def do_pastebin(self, s) -> str | None: return paste_url - def push(self, s, 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 - s = s.rstrip("\n") + s = line.rstrip("\n") self.buffer.append(s) if insert_into_history: @@ -1210,6 +1215,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)") 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 ( 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() diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 19561efb..fdb9dcad 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(">>> ")], width=10) self.assert_paint(screen, (0, 4)) def test_enter_text(self): @@ -113,18 +113,18 @@ def test_enter_text(self): + cyan(" ") + green("1") ), - cyan("Welcome to"), - ] + ], + width=10, ) 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) - 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 +135,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)) 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 d94fc2e7..d4899332 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 @@ -96,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): @@ -258,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): @@ -444,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. @@ -456,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__() @@ -1355,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]