From ef5267253b67edd9aad7749e4349172784ef3ab9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 19 Oct 2020 22:50:25 +0200 Subject: [PATCH 001/555] Generate black-compatible _version.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c5e37c0f0..ec71c2fdd 100755 --- a/setup.py +++ b/setup.py @@ -119,7 +119,7 @@ def git_describe_to_python_version(version): with open(version_file, "w") as vf: vf.write("# Auto-generated file, do not edit!\n") - vf.write(f"__version__ = '{version}'\n") + vf.write(f"__version__ = \"{version}\"\n") class install(_install): From 093d8445ab2617cf6b31ea191eead8135716be16 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 19 Oct 2020 23:30:34 +0200 Subject: [PATCH 002/555] Provide more meta data in bpython and bpdb modules --- bpdb/__init__.py | 5 ++++- bpython/__init__.py | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/bpdb/__init__.py b/bpdb/__init__.py index 7c2be1717..a6cea15c0 100644 --- a/bpdb/__init__.py +++ b/bpdb/__init__.py @@ -1,7 +1,7 @@ # The MIT License # # Copyright (c) 2008 Bob Farrell -# Copyright (c) 2013 Sebastian Ramacher +# Copyright (c) 2013-2020 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 @@ -30,6 +30,9 @@ from optparse import OptionParser from pdb import Restart +__author__ = bpython.__author__ +__copyright__ = bpython.__copyright__ +__license__ = bpython.__license__ __version__ = bpython.__version__ diff --git a/bpython/__init__.py b/bpython/__init__.py index 04e84e7b3..f9048afa8 100644 --- a/bpython/__init__.py +++ b/bpython/__init__.py @@ -20,7 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. - import os.path try: @@ -28,6 +27,11 @@ except ImportError: version = "unknown" +__author__ = ( + "Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al." +) +__copyright__ = f"(C) 2008-2020 {__author__}" +__license__ = "MIT" __version__ = version package_dir = os.path.abspath(os.path.dirname(__file__)) From ff122520bf08bae097cb171cae99df426536391d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 19 Oct 2020 23:32:09 +0200 Subject: [PATCH 003/555] Refactor copyright banner --- bpdb/__init__.py | 9 +++------ bpython/args.py | 40 ++++++++++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/bpdb/__init__.py b/bpdb/__init__.py index a6cea15c0..ccb3a4d91 100644 --- a/bpdb/__init__.py +++ b/bpdb/__init__.py @@ -26,6 +26,7 @@ import traceback import bpython +from bpython.args import version_banner, copyright_banner from .debugger import BPdb from optparse import OptionParser from pdb import Restart @@ -74,12 +75,8 @@ def main(): ) options, args = parser.parse_args(sys.argv) if options.version: - print("bpdb on top of bpython version", __version__, end="") - print("on top of Python", sys.version.split()[0]) - print( - "(C) 2008-2013 Bob Farrell, Andreas Stuehrk et al. " - "See AUTHORS for detail." - ) + print(version_banner(base="bpdb")) + print(copyright_banner()) return 0 if len(args) < 2: diff --git a/bpython/args.py b/bpython/args.py index 6eef37759..479590ffc 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -1,15 +1,37 @@ +# The MIT License +# +# Copyright (c) 2008 Bob Farrell +# Copyright (c) 2012-2020 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. + """ Module to handle command line argument parsing, for all front-ends. """ - import code import importlib.util import os import sys from optparse import OptionParser, OptionGroup -from . import __version__ +from . import __version__, __copyright__ from .config import default_config_path, loadini, Struct from .translations import _ @@ -23,14 +45,19 @@ def error(self, msg): raise OptionParserFailed() -def version_banner(): - return "bpython version {} on top of Python {} {}".format( +def version_banner(base="bpython"): + return "{} version {} on top of Python {} {}".format( + base, __version__, sys.version.split()[0], sys.executable, ) +def copyright_banner(): + return "{} See AUTHORS for details.".format(__copyright__) + + def parse(args, extras=None, ignore_stdin=False): """Receive an argument list - if None, use sys.argv - parse all args and take appropriate action. Also receive optional extra options: this should @@ -110,10 +137,7 @@ def parse(args, extras=None, ignore_stdin=False): if options.version: print(version_banner()) - print( - "(C) 2008-2020 Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al. " - "See AUTHORS for detail." - ) + print(copyright_banner()) raise SystemExit if not ignore_stdin and not (sys.stdin.isatty() and sys.stdout.isatty()): From 8c9f37ce561187f35829864421eb14e14f418df3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 19 Oct 2020 23:37:44 +0200 Subject: [PATCH 004/555] Update user facing strings --- bpdb/__init__.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/bpdb/__init__.py b/bpdb/__init__.py index ccb3a4d91..9ce932d38 100644 --- a/bpdb/__init__.py +++ b/bpdb/__init__.py @@ -55,8 +55,7 @@ def post_mortem(t=None): t = sys.exc_info()[2] if t is None: raise ValueError( - "A valid traceback must be passed if no " - "exception is being handled" + "A valid traceback must be passed if no exception is being handled." ) p = BPdb() @@ -86,7 +85,7 @@ def main(): # The following code is based on Python's pdb.py. mainpyfile = args[1] if not os.path.exists(mainpyfile): - print("Error:", mainpyfile, "does not exist") + print(f"Error: {mainpyfile} does not exist.") return 1 # Hide bpdb from argument list. @@ -101,22 +100,22 @@ def main(): pdb._runscript(mainpyfile) if pdb._user_requested_quit: break - print("The program finished and will be restarted") + print("The program finished and will be restarted.") except Restart: - print("Restarting", mainpyfile, "with arguments:") + print(f"Restarting {mainpyfile} with arguments:") print("\t" + " ".join(sys.argv[1:])) except SystemExit: # In most cases SystemExit does not warrant a post-mortem session. - print("The program exited via sys.exit(). Exit status: ",) + print( + "The program exited via sys.exit(). Exit status: ", + ) print(sys.exc_info()[1]) except: traceback.print_exc() - print("Uncaught exception. Entering post mortem debugging") - print("Running 'cont' or 'step' will restart the program") + print("Uncaught exception. Entering post mortem debugging.") + print("Running 'cont' or 'step' will restart the program.") t = sys.exc_info()[2] pdb.interaction(None, t) print( - "Post mortem debugger finished. The " - + mainpyfile - + " will be restarted" + f"Post mortem debugger finished. The {mainpyfile} will be restarted." ) From ea44aec13ef33cc14e52f7bfaa4d7086d0ab7179 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 19 Oct 2020 23:48:04 +0200 Subject: [PATCH 005/555] Update doc string --- bpython/simpleeval.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 229e3423d..2b414ee25 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -74,10 +74,10 @@ def simple_eval(node_or_string, namespace=None): * variable names causing lookups in the passed in namespace or builtins * getitem calls using the [] syntax on objects of the types above - Like the Python 3 (and unlike the Python 2) literal_eval, unary and binary - + and - operations are allowed on all builtin numeric types. + Like Python 3's literal_eval, unary and binary + and - operations are + allowed on all builtin numeric types. - The optional namespace dict-like ought not to cause side effects on lookup + The optional namespace dict-like ought not to cause side effects on lookup. """ if namespace is None: namespace = {} From be8737c1dd507439d78c3f9b2c41967e544343e1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 21 Oct 2020 00:58:35 +0200 Subject: [PATCH 006/555] Use tuples instead of lists --- bpython/cli.py | 2 +- bpython/curtsiesfrontend/interaction.py | 2 +- bpython/curtsiesfrontend/repl.py | 23 +++++++++++------------ bpython/lazyre.py | 1 - bpython/repl.py | 8 ++------ bpython/urwid.py | 4 ++-- 6 files changed, 17 insertions(+), 23 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index cd9a20904..4cedab8d8 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -190,7 +190,7 @@ def readline(self, size=-1): try: while not buffer.endswith(("\n", "\r")): key = self.interface.get_key() - if key in [curses.erasechar(), "KEY_BACKSPACE"]: + if key in (curses.erasechar(), "KEY_BACKSPACE"): y, x = self.interface.scr.getyx() if buffer: self.interface.scr.delch(y, x - 1) diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index abd6009c6..7820c7c87 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -87,7 +87,7 @@ def process_event(self, e): for ee in e.events: # strip control seq self.add_normal_character(ee if len(ee) == 1 else ee[-1]) - elif e in [""] or isinstance(e, events.SigIntEvent): + elif e == "" or isinstance(e, events.SigIntEvent): self.request_context.switch(False) self.escape() elif e in edit_keys: diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 7c30143be..32ddd8362 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -136,10 +136,9 @@ def process_event(self, e): self.repl.run_code_and_maybe_finish() elif e in ("",): self.get_last_word() - - elif e in [""]: + elif e in ("",): pass - elif e in [""]: + elif e in ("",): if self.current_line == "": self.repl.send_to_stdin("\n") self.has_focus = False @@ -148,7 +147,7 @@ def process_event(self, e): self.repl.run_code_and_maybe_finish(for_code="") else: pass - elif e in ["\n", "\r", "", ""]: + elif e in ("\n", "\r", "", ""): line = self.current_line self.repl.send_to_stdin(line + "\n") self.has_focus = False @@ -164,7 +163,7 @@ def process_event(self, e): self.repl.send_to_stdin(self.current_line) def add_input_character(self, e): - if e == "": + if e in ("",): e = " " if e.startswith("<") and e.endswith(">"): return @@ -763,7 +762,7 @@ def process_key_event(self, e): raise SystemExit() elif e in ("\n", "\r", "", "", ""): self.on_enter() - elif e == "": # tab + elif e in ("",): # tab self.on_tab() elif e in ("",): self.on_tab(back=True) @@ -784,9 +783,9 @@ def process_key_event(self, e): # TODO add PAD keys hack as in bpython.cli elif e in key_dispatch[self.config.edit_current_block_key]: self.send_current_block_to_external_editor() - elif e in [""]: + elif e in ("",): self.incr_search_mode = None - elif e in [""]: + elif e in ("",): self.add_normal_character(" ") else: self.add_normal_character(e) @@ -969,7 +968,7 @@ def process_simple_keypress(self, e): self.process_event(bpythonevents.RefreshRequestEvent()) elif isinstance(e, events.Event): pass # ignore events - elif e == "": + elif e in ("",): self.add_normal_character(" ") else: self.add_normal_character(e) @@ -2013,11 +2012,11 @@ def key_help_text(self): ["complete history suggestion", "right arrow at end of line"] ) pairs.append(["previous match with current line", "up arrow"]) - for functionality, key in [ + for functionality, key in ( (attr[:-4].replace("_", " "), getattr(self.config, attr)) for attr in self.config.__dict__ if attr.endswith("key") - ]: + ): if functionality in NOT_IMPLEMENTED: key = "Not Implemented" if key == "": @@ -2070,7 +2069,7 @@ def just_simple_events(event_list): simple_events.append("\n") elif isinstance(e, events.Event): pass # ignore events - elif e == "": + elif e in ("",): simple_events.append(" ") elif len(e) > 1: pass # get rid of etc. diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 588558b85..01747f819 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -20,7 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. - import re diff --git a/bpython/repl.py b/bpython/repl.py index 5857dc9dd..9b23221ad 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -21,7 +21,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. - import code import inspect import os @@ -40,9 +39,7 @@ from pygments.token import Token from pygments.lexers import Python3Lexer - -from . import autocomplete -from . import inspection +from . import autocomplete, inspection, simpleeval from .clipboard import get_clipboard, CopyFailed from .config import getpreferredencoding from .formatter import Parenthesis @@ -51,7 +48,6 @@ from .paste import PasteHelper, PastePinnwand, PasteFailed from .patch_linecache import filename_for_console_input from .translations import _, ngettext -from . import simpleeval class RuntimeTimer: @@ -1206,7 +1202,7 @@ def next_token_inside_string(code_string, inside_string): for token, value in Python3Lexer().get_tokens(code_string): if token is Token.String: value = value.lstrip("bBrRuU") - if value in ['"""', "'''", '"', "'"]: + if value in ('"""', "'''", '"', "'"): if not inside_string: inside_string = value elif value == inside_string: diff --git a/bpython/urwid.py b/bpython/urwid.py index 019a77c7c..27f1ef116 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -393,7 +393,7 @@ def move_cursor_to_coords(self, *args): return False def keypress(self, size, key): - if urwid.command_map[key] in ["cursor up", "cursor down"]: + if urwid.command_map[key] in ("cursor up", "cursor down"): # Do not handle up/down arrow, leave them for the repl. return key @@ -452,7 +452,7 @@ class BPythonListBox(urwid.ListBox): """ def keypress(self, size, key): - if key not in ["up", "down"]: + if key not in ("up", "down"): return urwid.ListBox.keypress(self, size, key) return key From 4e11d5ed6f4df3faab5446c4300f98cd564c8d9a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 21 Oct 2020 01:03:58 +0200 Subject: [PATCH 007/555] Remove __future__ imports --- bpython/test/test_curtsies_repl.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py index b1eb38997..de5fdb96c 100644 --- a/bpython/test/test_curtsies_repl.py +++ b/bpython/test/test_curtsies_repl.py @@ -244,7 +244,6 @@ class TestFutureImports(TestCase): def test_repl(self): repl = create_repl() with captured_output() as (out, err): - repl.push("from __future__ import division") repl.push("1 / 2") self.assertEqual(out.getvalue(), "0.5\n") @@ -252,7 +251,6 @@ def test_interactive(self): interp = code.InteractiveInterpreter(locals={}) with captured_output() as (out, err): with tempfile.NamedTemporaryFile(mode="w", suffix=".py") as f: - f.write("from __future__ import division\n") f.write("print(1/2)\n") f.flush() args.exec_code(interp, [f.name]) @@ -438,7 +436,6 @@ def write_startup_file(self, fname, encoding): f.write("# coding: ") f.write(encoding) f.write("\n") - f.write("from __future__ import unicode_literals\n") f.write('a = "äöü"\n') def test_startup_event_utf8(self): From 7c58ca03143a6d05429e5e98d44e4497cbf61cf7 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 24 Oct 2020 17:46:33 +0200 Subject: [PATCH 008/555] Bump Sphinx version requirement --- .travis.install.sh | 2 +- README.rst | 2 +- setup.py | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.travis.install.sh b/.travis.install.sh index 43392c336..58a021274 100755 --- a/.travis.install.sh +++ b/.travis.install.sh @@ -17,5 +17,5 @@ if [[ $RUN == nosetests ]]; then python setup.py install elif [[ $RUN == build_sphinx ]]; then # documentation specific dependencies - pip install 'sphinx >=1.1.3' + pip install 'sphinx >=1.5' fi diff --git a/README.rst b/README.rst index 88924e7da..ad9000781 100644 --- a/README.rst +++ b/README.rst @@ -99,7 +99,7 @@ Dependencies * requests * curtsies >= 0.3.0 * greenlet -* Sphinx != 1.1.2 (optional, for the documentation) +* Sphinx >= 1.5 (optional, for the documentation) * babel (optional, for internationalization) * watchdog (optional, for monitoring imported modules for changes) * jedi (optional, for experimental multiline completion) diff --git a/setup.py b/setup.py index ec71c2fdd..6a10f8296 100755 --- a/setup.py +++ b/setup.py @@ -24,9 +24,8 @@ import sphinx from sphinx.setup_command import BuildDoc - # Sphinx 1.1.2 is buggy and building bpython with that version fails. - # See #241. - using_sphinx = sphinx.__version__ >= "1.1.3" + # Sphinx 1.5 and newer support Python 3.6 + using_sphinx = sphinx.__version__ >= "1.5" except ImportError: using_sphinx = False From d55c640f05979671dfedeb03ac1bf7a6e04d75d1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 24 Oct 2020 22:32:36 +0200 Subject: [PATCH 009/555] Fix version parsing --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6a10f8296..d31cbaeea 100755 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ def git_describe_to_python_version(version): try: # get version from existing version file with open(version_file) as vf: - version = vf.read().strip().split("=")[-1].replace("'", "") + version = vf.read().strip().split("=")[-1].replace("'", "").replace("\"", "") version = version.strip() except OSError: pass From 4270a7b7fc6155728a0e99a06193bf10301e268d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Oct 2020 15:31:52 +0100 Subject: [PATCH 010/555] Remove unused import --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index d31cbaeea..33913d2d1 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,6 @@ import platform import re import subprocess -import sys from distutils.command.build import build from setuptools import setup From 71c419d5bb33e1dc518bf228f1c8645eb427e054 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Oct 2020 16:00:31 +0100 Subject: [PATCH 011/555] Replace list of options with a callback This will make it easier to later change to argparse. --- bpython/args.py | 13 ++++---- bpython/curtsies.py | 30 +++++++++---------- bpython/urwid.py | 72 ++++++++++++++++++++++----------------------- 3 files changed, 58 insertions(+), 57 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 479590ffc..586c73bda 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -64,17 +64,19 @@ def parse(args, extras=None, ignore_stdin=False): be a tuple of (title, description, options) title: The title for the option group description: A full description of the option group - options: A list of optparse.Option objects to be added to the - group + callback: A callback that adds options to the option group e.g.: + def callback(group): + group.add_option('-f', action='store_true', dest='f', help='Explode') + group.add_option('-l', action='store_true', dest='l', help='Love') + parse( ['-i', '-m', 'foo.py'], ('Front end-specific options', 'A full description of what these options are for', - [optparse.Option('-f', action='store_true', dest='f', help='Explode'), - optparse.Option('-l', action='store_true', dest='l', help='Love')])) + callback)) Return a tuple of (config, options, exec_args) wherein "config" is the @@ -125,8 +127,7 @@ def parse(args, extras=None, ignore_stdin=False): if extras is not None: extras_group = OptionGroup(parser, extras[0], extras[1]) - for option in extras[2]: - extras_group.add_option(option) + extras[2](extras_group) parser.add_option_group(extras_group) try: diff --git a/bpython/curtsies.py b/bpython/curtsies.py index dd74fd13a..94676edb8 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -1,7 +1,6 @@ import collections import logging import sys -from optparse import Option import curtsies import curtsies.window @@ -134,25 +133,26 @@ def main(args=None, locals_=None, banner=None, welcome_message=None): """ translations.init() + def curtsies_arguments(parser): + parser.add_argument( + "--log", + "-L", + action="count", + help=_("log debug messages to bpython.log"), + ) + parser.add_argument( + "--paste", + "-p", + action="store_true", + help=_("start by pasting lines of a file into session"), + ) + config, options, exec_args = bpargs.parse( args, ( "curtsies options", None, - [ - Option( - "--log", - "-L", - action="count", - help=_("log debug messages to bpython.log"), - ), - Option( - "--paste", - "-p", - action="store_true", - help=_("start by pasting lines of a file into session"), - ), - ], + curtsies_arguments, ), ) if options.log is None: diff --git a/bpython/urwid.py b/bpython/urwid.py index 27f1ef116..0e2570e88 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -37,7 +37,6 @@ import time import locale import signal -from optparse import Option from pygments.token import Token @@ -1120,47 +1119,48 @@ def tab(self, back=False): def main(args=None, locals_=None, banner=None): translations.init() + def options_callback(group): + group.add_option( + "--twisted", + "-T", + action="store_true", + help=_("Run twisted reactor."), + ) + group.add_option( + "--reactor", + "-r", + help=_( + "Select specific reactor (see --help-reactors). " + "Implies --twisted." + ), + ) + group.add_option( + "--help-reactors", + action="store_true", + help=_("List available reactors for -r."), + ) + group.add_option( + "--plugin", + "-p", + help=_( + "twistd plugin to run (use twistd for a list). " + 'Use "--" to pass further options to the plugin.' + ), + ) + group.add_option( + "--server", + "-s", + type="int", + help=_("Port to run an eval server on (forces Twisted)."), + ) + # TODO: maybe support displays other than raw_display? config, options, exec_args = bpargs.parse( args, ( "Urwid options", None, - [ - Option( - "--twisted", - "-T", - action="store_true", - help=_("Run twisted reactor."), - ), - Option( - "--reactor", - "-r", - help=_( - "Select specific reactor (see --help-reactors). " - "Implies --twisted." - ), - ), - Option( - "--help-reactors", - action="store_true", - help=_("List available reactors for -r."), - ), - Option( - "--plugin", - "-p", - help=_( - "twistd plugin to run (use twistd for a list). " - 'Use "--" to pass further options to the plugin.' - ), - ), - Option( - "--server", - "-s", - type="int", - help=_("Port to run an eval server on (forces Twisted)."), - ), - ], + options_callback, ), ) From 0327893bb397c36b129a5bbd2f162461920ce251 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Oct 2020 16:08:40 +0100 Subject: [PATCH 012/555] Fix add_option calls --- bpython/curtsies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 94676edb8..b80a52ce3 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -134,13 +134,13 @@ def main(args=None, locals_=None, banner=None, welcome_message=None): translations.init() def curtsies_arguments(parser): - parser.add_argument( + parser.add_option( "--log", "-L", action="count", help=_("log debug messages to bpython.log"), ) - parser.add_argument( + parser.add_option( "--paste", "-p", action="store_true", From ce29d8b391bf6083448ea834e13638de685ea44d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Oct 2020 16:18:08 +0100 Subject: [PATCH 013/555] Replace deprecated optparse with argparse --- bpython/args.py | 51 +++++++++++++++++++++------------------------ bpython/curtsies.py | 4 ++-- bpython/urwid.py | 12 +++++------ 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 586c73bda..6e64901d7 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -29,20 +29,20 @@ import importlib.util import os import sys -from optparse import OptionParser, OptionGroup +import argparse from . import __version__, __copyright__ from .config import default_config_path, loadini, Struct from .translations import _ -class OptionParserFailed(ValueError): +class ArgumentParserFailed(ValueError): """Raised by the RaisingOptionParser for a bogus commandline.""" -class RaisingOptionParser(OptionParser): +class RaisingArgumentParser(argparse.ArgumentParser): def error(self, msg): - raise OptionParserFailed() + raise ArgumentParserFailed() def version_banner(base="bpython"): @@ -60,17 +60,17 @@ def copyright_banner(): def parse(args, extras=None, ignore_stdin=False): """Receive an argument list - if None, use sys.argv - parse all args and - take appropriate action. Also receive optional extra options: this should - be a tuple of (title, description, options) - title: The title for the option group - description: A full description of the option group - callback: A callback that adds options to the option group + take appropriate action. Also receive optional extra argument: this should + be a tuple of (title, description, callback) + title: The title for the argument group + description: A full description of the argument group + callback: A callback that adds argument to the argument group e.g.: def callback(group): - group.add_option('-f', action='store_true', dest='f', help='Explode') - group.add_option('-l', action='store_true', dest='l', help='Love') + group.add_argument('-f', action='store_true', dest='f', help='Explode') + group.add_argument('-l', action='store_true', dest='l', help='Love') parse( ['-i', '-m', 'foo.py'], @@ -82,43 +82,38 @@ def callback(group): Return a tuple of (config, options, exec_args) wherein "config" is the config object either parsed from a default/specified config file or default config options, "options" is the parsed options from - OptionParser.parse_args, and "exec_args" are the args (if any) to be parsed + ArgumentParser.parse_args, and "exec_args" are the args (if any) to be parsed to the executed file (if any). """ if args is None: args = sys.argv[1:] - parser = RaisingOptionParser( + parser = RaisingArgumentParser( usage=_( - "Usage: %prog [options] [file [args]]\n" + "Usage: %(prog)s [options] [file [args]]\n" "NOTE: If bpython sees an argument it does " "not know, execution falls back to the " "regular Python interpreter." ) ) - # This is not sufficient if bpython gains its own -m support - # (instead of falling back to Python itself for that). - # That's probably fixable though, for example by having that - # option swallow all remaining arguments in a callback. - parser.disable_interspersed_args() - parser.add_option( + parser.add_argument( "--config", default=default_config_path(), help=_("Use CONFIG instead of default config file."), ) - parser.add_option( + parser.add_argument( "--interactive", "-i", action="store_true", help=_("Drop to bpython shell after running file instead of exiting."), ) - parser.add_option( + parser.add_argument( "--quiet", "-q", action="store_true", help=_("Don't flush the output to stdout."), ) - parser.add_option( + parser.add_argument( "--version", "-V", action="store_true", @@ -126,12 +121,14 @@ def callback(group): ) if extras is not None: - extras_group = OptionGroup(parser, extras[0], extras[1]) + extras_group = parser.add_argument_group(extras[0], extras[1]) extras[2](extras_group) - parser.add_option_group(extras_group) + + # collect all the remaining arguments into a list + parser.add_argument('args', nargs=argparse.REMAINDER) try: - options, args = parser.parse_args(args) + options = parser.parse_args(args) except OptionParserFailed: # Just let Python handle this os.execv(sys.executable, [sys.executable] + args) @@ -149,7 +146,7 @@ def callback(group): config = Struct() loadini(config, options.config) - return config, options, args + return config, options, options.args def exec_code(interpreter, args): diff --git a/bpython/curtsies.py b/bpython/curtsies.py index b80a52ce3..94676edb8 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -134,13 +134,13 @@ def main(args=None, locals_=None, banner=None, welcome_message=None): translations.init() def curtsies_arguments(parser): - parser.add_option( + parser.add_argument( "--log", "-L", action="count", help=_("log debug messages to bpython.log"), ) - parser.add_option( + parser.add_argument( "--paste", "-p", action="store_true", diff --git a/bpython/urwid.py b/bpython/urwid.py index 0e2570e88..e3e14d37e 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -1120,13 +1120,13 @@ def main(args=None, locals_=None, banner=None): translations.init() def options_callback(group): - group.add_option( + group.add_argument( "--twisted", "-T", action="store_true", help=_("Run twisted reactor."), ) - group.add_option( + group.add_argument( "--reactor", "-r", help=_( @@ -1134,12 +1134,12 @@ def options_callback(group): "Implies --twisted." ), ) - group.add_option( + group.add_argument( "--help-reactors", action="store_true", help=_("List available reactors for -r."), ) - group.add_option( + group.add_argument( "--plugin", "-p", help=_( @@ -1147,10 +1147,10 @@ def options_callback(group): 'Use "--" to pass further options to the plugin.' ), ) - group.add_option( + group.add_argument( "--server", "-s", - type="int", + type=int, help=_("Port to run an eval server on (forces Twisted)."), ) From d10e33593b8a5f3c42e56f3ac764d3cb7412e733 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 27 Oct 2020 11:31:04 +0100 Subject: [PATCH 014/555] Remove unused import --- bpython/simpleeval.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 2b414ee25..37562c933 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -28,7 +28,6 @@ import ast -import inspect import sys import builtins From 5617a438c01adeca9eebacb0f76baaaff0336d67 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 27 Oct 2020 11:34:01 +0100 Subject: [PATCH 015/555] Use isinstance for type checking --- bpython/simpleeval.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 37562c933..ca8f5d51b 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -139,7 +139,8 @@ def _convert(node): left = _convert(node.left) right = _convert(node.right) if not ( - type(left) in _numeric_types and type(right) in _numeric_types + isinstance(left, _numeric_types) + and isinstance(right, _numeric_types) ): raise ValueError("binary + and - only allowed on builtin nums") if isinstance(node.op, ast.Add): From cf6c550362f7a47da546f00995f0c63423770b79 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 27 Oct 2020 11:42:44 +0100 Subject: [PATCH 016/555] Remove unused imports --- bpython/curtsiesfrontend/_internal.py | 2 -- bpython/curtsiesfrontend/coderunner.py | 4 +--- bpython/paste.py | 9 ++++----- bpython/urwid.py | 6 +----- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index b52a47542..d5e8cb458 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -21,9 +21,7 @@ # THE SOFTWARE. import pydoc - import bpython._internal -from bpython.repl import getpreferredencoding class NopPydocPager: diff --git a/bpython/curtsiesfrontend/coderunner.py b/bpython/curtsiesfrontend/coderunner.py index ee62e7c6e..8baabf35c 100644 --- a/bpython/curtsiesfrontend/coderunner.py +++ b/bpython/curtsiesfrontend/coderunner.py @@ -11,13 +11,11 @@ """ import code -import signal import greenlet import logging +import signal import threading -from bpython.config import getpreferredencoding - logger = logging.getLogger(__name__) diff --git a/bpython/paste.py b/bpython/paste.py index 736adea3f..ef27e0295 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2014-2015 Sebastian Ramacher +# Copyright (c) 2014-2020 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 @@ -20,15 +20,14 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. - -from locale import getpreferredencoding -from urllib.parse import quote as urlquote, urljoin, urlparse -from string import Template import errno import requests import subprocess import unicodedata +from locale import getpreferredencoding +from urllib.parse import urljoin, urlparse + from .translations import _ diff --git a/bpython/urwid.py b/bpython/urwid.py index e3e14d37e..cc7d22f56 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -31,25 +31,21 @@ This is still *VERY* rough. """ -import builtins import sys import os import time import locale import signal +import urwid from pygments.token import Token from . import args as bpargs, repl, translations -from .config import getpreferredencoding from .formatter import theme_map from .importcompletion import find_coroutine from .translations import _ - from .keys import urwid_key_dispatch as key_dispatch -import urwid - Parenthesis = Token.Punctuation.Parenthesis # Urwid colors are: From fb372e37f1e4526d76ef988a561f12b21e0efcdd Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 27 Oct 2020 11:42:53 +0100 Subject: [PATCH 017/555] Refactor --- bpython/paste.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bpython/paste.py b/bpython/paste.py index ef27e0295..4d118cfc2 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -96,16 +96,16 @@ def paste(self, s): if not paste_url: raise PasteFailed(_("No output from helper program.")) - else: - parsed_url = urlparse(paste_url) - if not parsed_url.scheme or any( - unicodedata.category(c) == "Cc" for c in paste_url - ): - raise PasteFailed( - _( - "Failed to recognize the helper " - "program's output as an URL." - ) + + parsed_url = urlparse(paste_url) + if not parsed_url.scheme or any( + unicodedata.category(c) == "Cc" for c in paste_url + ): + raise PasteFailed( + _( + "Failed to recognize the helper " + "program's output as an URL." ) + ) return paste_url, None From dee4d23c54c015a2e678123658f86e2cdf557d82 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 27 Oct 2020 14:17:51 +0100 Subject: [PATCH 018/555] Remove more unused imports --- bpython/importcompletion.py | 11 +++++------ bpython/test/test_autocomplete.py | 9 ++------- bpython/test/test_curtsies_painting.py | 1 - bpython/test/test_import_not_cyclical.py | 1 - bpython/test/test_interpreter.py | 1 - 5 files changed, 7 insertions(+), 16 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 6e30861a2..91972a6b5 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -20,6 +20,11 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import fnmatch +import os +import sys +import warnings +import importlib.machinery from .line import ( current_word, @@ -28,12 +33,6 @@ current_from_import_import, ) -import fnmatch -import os -import sys -import warnings -import importlib.machinery - SUFFIXES = importlib.machinery.all_suffixes() # The cached list of all known modules diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index bee638d51..5b7ccd6ca 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -1,12 +1,7 @@ -from collections import namedtuple import inspect import keyword -import sys - -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest +from collections import namedtuple try: import jedi diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 720b5966e..a5d958a05 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -1,5 +1,4 @@ import itertools -import os import pydoc import string import sys diff --git a/bpython/test/test_import_not_cyclical.py b/bpython/test/test_import_not_cyclical.py index 830a23117..a1001365c 100644 --- a/bpython/test/test_import_not_cyclical.py +++ b/bpython/test/test_import_not_cyclical.py @@ -1,5 +1,4 @@ import os -import sys import tempfile from bpython.test import unittest diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 56d0c8ad7..afa71aa79 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -1,6 +1,5 @@ import sys import re -from textwrap import dedent from curtsies.fmtfuncs import bold, green, magenta, cyan, red, plain From 6a1bc38d0201bf3b74a74908f86993017a28cb9e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 27 Oct 2020 14:39:46 +0100 Subject: [PATCH 019/555] travis: install urwid and twisted --- .travis.install.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.install.sh b/.travis.install.sh index 58a021274..40c0a67bb 100755 --- a/.travis.install.sh +++ b/.travis.install.sh @@ -7,6 +7,8 @@ pip install setuptools if [[ $RUN == nosetests ]]; then # core dependencies pip install -r requirements.txt + # urwid specific dependencies + pip install urwid twisted # filewatch specific dependencies pip install watchdog # jedi specific dependencies From cf81346df833e5a43ff12f4f999a598aaad932e7 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 27 Oct 2020 14:59:28 +0100 Subject: [PATCH 020/555] Fix type --- bpython/args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/args.py b/bpython/args.py index 6e64901d7..b83844e20 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -129,7 +129,7 @@ def callback(group): try: options = parser.parse_args(args) - except OptionParserFailed: + except ArgumentParserFailed: # Just let Python handle this os.execv(sys.executable, [sys.executable] + args) From 810364fd5103208ea7c21643b6ca206aa488abe3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 27 Oct 2020 15:02:51 +0100 Subject: [PATCH 021/555] Improve description of arguments --- bpython/args.py | 8 +++++++- bpython/curtsies.py | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index b83844e20..915169c22 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -125,7 +125,13 @@ def callback(group): extras[2](extras_group) # collect all the remaining arguments into a list - parser.add_argument('args', nargs=argparse.REMAINDER) + parser.add_argument( + "args", + nargs=argparse.REMAINDER, + help=_( + "File to extecute and additional arguments passed on to the executed script." + ), + ) try: options = parser.parse_args(args) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 94676edb8..ae780f7d7 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -150,14 +150,14 @@ def curtsies_arguments(parser): config, options, exec_args = bpargs.parse( args, ( - "curtsies options", - None, + _("curtsies arguments"), + _("Additional arguments specific to the curtsies-based REPL."), curtsies_arguments, ), ) if options.log is None: options.log = 0 - logging_levels = [logging.ERROR, logging.INFO, logging.DEBUG] + logging_levels = (logging.ERROR, logging.INFO, logging.DEBUG) level = logging_levels[min(len(logging_levels) - 1, options.log)] logging.getLogger("curtsies").setLevel(level) logging.getLogger("bpython").setLevel(level) From e50ce9d1b278730b0b03c601a3b69b7f57c4d9c4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 27 Oct 2020 15:15:04 +0100 Subject: [PATCH 022/555] Remove unused global repl --- bpython/curtsies.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index ae780f7d7..98c064bd2 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -21,10 +21,6 @@ logger = logging.getLogger(__name__) -repl = None # global for `from bpython.curtsies import repl` -# WARNING Will be a problem if more than one repl is ever instantiated this way - - class FullCurtsiesRepl(BaseRepl): def __init__(self, config, locals_, banner, interp=None): self.input_generator = curtsies.input.Input( @@ -197,7 +193,6 @@ def curtsies_arguments(parser): if banner is not None: print(banner) - global repl repl = FullCurtsiesRepl(config, locals_, welcome_message, interp) try: with repl.input_generator: From 0c6864be1fcdbc17a9790f15e24ed6e36ed0121a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 27 Oct 2020 15:21:13 +0100 Subject: [PATCH 023/555] Use super() where possible --- bpython/urwid.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/bpython/urwid.py b/bpython/urwid.py index cc7d22f56..d9e0a388c 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -1,4 +1,3 @@ -# # The MIT License # # Copyright (c) 2010-2011 Marien Zwart @@ -38,16 +37,12 @@ import signal import urwid -from pygments.token import Token - from . import args as bpargs, repl, translations from .formatter import theme_map from .importcompletion import find_coroutine from .translations import _ from .keys import urwid_key_dispatch as key_dispatch -Parenthesis = Token.Punctuation.Parenthesis - # Urwid colors are: # 'black', 'dark red', 'dark green', 'brown', 'dark blue', # 'dark magenta', 'dark cyan', 'light gray', 'dark gray', @@ -147,7 +142,7 @@ class StatusbarEdit(urwid.Edit): def __init__(self, *args, **kwargs): self.single = False - urwid.Edit.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) def keypress(self, size, key): if self.single: @@ -155,14 +150,13 @@ def keypress(self, size, key): elif key == "enter": urwid.emit_signal(self, "prompt_enter", self, self.get_edit_text()) else: - return urwid.Edit.keypress(self, size, key) + return super().keypress(size, key) urwid.register_signal(StatusbarEdit, "prompt_enter") class Statusbar: - """Statusbar object, ripped off from bpython.cli. This class provides the status bar at the bottom of the screen. @@ -290,7 +284,6 @@ def format_tokens(tokensource): class BPythonEdit(urwid.Edit): - """Customized editor *very* tightly interwoven with URWIDRepl. Changes include: @@ -322,10 +315,10 @@ def __init__(self, config, *args, **kwargs): self._bpy_may_move_cursor = False self.config = config self.tab_length = config.tab_length - urwid.Edit.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) def set_edit_pos(self, pos): - urwid.Edit.set_edit_pos(self, pos) + super().set_edit_pos(pos) self._emit("edit-pos-changed", self.edit_pos) def get_edit_pos(self): @@ -366,7 +359,7 @@ def get_cursor_coords(self, *args, **kwargs): # urwid gets confused if a nonselectable widget has a cursor position. if not self._bpy_selectable: return None - return urwid.Edit.get_cursor_coords(self, *args, **kwargs) + return super().get_cursor_coords(*args, **kwargs) def render(self, size, focus=False): # XXX I do not want to have to do this, but listbox gets confused @@ -374,17 +367,17 @@ def render(self, size, focus=False): # we just became unselectable, then having this render a cursor) if not self._bpy_selectable: focus = False - return urwid.Edit.render(self, size, focus=focus) + return super().render(size, focus=focus) def get_pref_col(self, size): # Need to make this deal with us being nonselectable if not self._bpy_selectable: return "left" - return urwid.Edit.get_pref_col(self, size) + return super().get_pref_col(size) def move_cursor_to_coords(self, *args): if self._bpy_may_move_cursor: - return urwid.Edit.move_cursor_to_coords(self, *args) + return super().move_cursor_to_coords(*args) return False def keypress(self, size, key): @@ -425,10 +418,10 @@ def keypress(self, size, key): if not (cpos or len(line) % self.tab_length or line.strip()): self.set_edit_text(line[: -self.tab_length]) else: - return urwid.Edit.keypress(self, size, key) + return super().keypress(size, key) else: # TODO: Add in specific keypress fetching code here - return urwid.Edit.keypress(self, size, key) + return super().keypress(size, key) return None finally: self._bpy_may_move_cursor = False @@ -436,7 +429,7 @@ def keypress(self, size, key): def mouse_event(self, *args): self._bpy_may_move_cursor = True try: - return urwid.Edit.mouse_event(self, *args) + return super().mouse_event(*args) finally: self._bpy_may_move_cursor = False @@ -453,7 +446,6 @@ def keypress(self, size, key): class Tooltip(urwid.BoxWidget): - """Container inspired by Overlay to position our tooltip. bottom_w should be a BoxWidget. @@ -466,7 +458,7 @@ class Tooltip(urwid.BoxWidget): """ def __init__(self, bottom_w, listbox): - self.__super.__init__() + super().__init__() self.bottom_w = bottom_w self.listbox = listbox @@ -534,7 +526,7 @@ def render(self, size, focus=False): class URWIDInteraction(repl.Interaction): def __init__(self, config, statusbar, frame): - repl.Interaction.__init__(self, config, statusbar) + super().__init__(config, statusbar) self.frame = frame urwid.connect_signal(statusbar, "prompt_result", self._prompt_result) self.callback = None @@ -577,7 +569,7 @@ class URWIDRepl(repl.Repl): _time_between_redraws = 0.05 # seconds def __init__(self, event_loop, palette, interpreter, config): - repl.Repl.__init__(self, interpreter, config) + super().__init__(interpreter, config) self._redraw_handle = None self._redraw_pending = False From 794bfa993102fd810fdd81750b629ce3e69d7f73 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 29 Oct 2020 12:17:59 +0100 Subject: [PATCH 024/555] Move babel integration to setup.cfg --- setup.cfg | 19 +++++++++++++++++++ setup.py | 53 +++++------------------------------------------------ 2 files changed, 24 insertions(+), 48 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8183238ab..06135236a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,21 @@ [metadata] license_files = LICENSE + +[init_catalog] +domain = bpython +input_file = bpython/translations/bpython.pot +output_dir = bpython/translations + +[compile_catalog] +domain = bpython +directory = bpython/translations +use_fuzzy = true + +[update_catalog] +domain = bpython +input_file = bpython/translations/bpython.pot +output_dir = bpython/translations + +[extract_messages] +output_file = bpython/translations/bpython.pot +msgid_bugs_address = https://github.com/bpython/bpython/issues diff --git a/setup.py b/setup.py index 33913d2d1..2d50bed2b 100755 --- a/setup.py +++ b/setup.py @@ -10,10 +10,7 @@ from setuptools.command.install import install as _install try: - from babel.messages.frontend import compile_catalog as _compile_catalog - from babel.messages.frontend import extract_messages as _extract_messages - from babel.messages.frontend import update_catalog as _update_catalog - from babel.messages.frontend import init_catalog as _init_catalog + from babel.messages import frontend as babel using_translations = True except ImportError: @@ -136,52 +133,12 @@ def run(self): # localization options if using_translations: - - class compile_catalog(_compile_catalog): - def initialize_options(self): - """Simply set default domain and directory attributes to the - correct path for bpython.""" - _compile_catalog.initialize_options(self) - - self.domain = "bpython" - self.directory = translations_dir - self.use_fuzzy = True - - class update_catalog(_update_catalog): - def initialize_options(self): - """Simply set default domain and directory attributes to the - correct path for bpython.""" - _update_catalog.initialize_options(self) - - self.domain = "bpython" - self.output_dir = translations_dir - self.input_file = os.path.join(translations_dir, "bpython.pot") - - class extract_messages(_extract_messages): - def initialize_options(self): - """Simply set default domain and output file attributes to the - correct values for bpython.""" - _extract_messages.initialize_options(self) - - self.domain = "bpython" - self.output_file = os.path.join(translations_dir, "bpython.pot") - - class init_catalog(_init_catalog): - def initialize_options(self): - """Simply set default domain, input file and output directory - attributes to the correct values for bpython.""" - _init_catalog.initialize_options(self) - - self.domain = "bpython" - self.output_dir = translations_dir - self.input_file = os.path.join(translations_dir, "bpython.pot") - build.sub_commands.insert(0, ("compile_catalog", None)) - cmdclass["compile_catalog"] = compile_catalog - cmdclass["extract_messages"] = extract_messages - cmdclass["update_catalog"] = update_catalog - cmdclass["init_catalog"] = init_catalog + cmdclass["compile_catalog"] = babel.compile_catalog + cmdclass["extract_messages"] = babel.extract_messages + cmdclass["update_catalog"] = babel.update_catalog + cmdclass["init_catalog"] = babel.init_catalog if using_sphinx: From 9d8f414ff7cf566233f88ad5c44b756d21b2cf8b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 29 Oct 2020 12:23:14 +0100 Subject: [PATCH 025/555] Update translations --- bpython/translations/bpython.pot | 143 +++++++++-------- .../translations/de/LC_MESSAGES/bpython.po | 147 ++++++++++-------- .../translations/es_ES/LC_MESSAGES/bpython.po | 144 +++++++++-------- .../translations/fr_FR/LC_MESSAGES/bpython.po | 143 +++++++++-------- .../translations/it_IT/LC_MESSAGES/bpython.po | 139 +++++++++-------- .../translations/nl_NL/LC_MESSAGES/bpython.po | 144 +++++++++-------- 6 files changed, 472 insertions(+), 388 deletions(-) diff --git a/bpython/translations/bpython.pot b/bpython/translations/bpython.pot index 443101f32..e8dc384fa 100644 --- a/bpython/translations/bpython.pot +++ b/bpython/translations/bpython.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: bpython 0.21.dev33\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-10-13 21:53+0200\n" +"Project-Id-Version: bpython 0.21.dev78\n" +"Report-Msgid-Bugs-To: https://github.com/bpython/bpython/issues\n" +"POT-Creation-Date: 2020-10-29 12:28+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,34 +17,41 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:63 +#: bpython/args.py:92 +#, python-format msgid "" -"Usage: %prog [options] [file [args]]\n" +"Usage: %(prog)s [options] [file [args]]\n" "NOTE: If bpython sees an argument it does not know, execution falls back " "to the regular Python interpreter." msgstr "" -#: bpython/args.py:78 +#: bpython/args.py:102 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:84 +#: bpython/args.py:108 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:92 +#: bpython/args.py:114 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:98 +#: bpython/args.py:120 msgid "Print version and exit." msgstr "" -#: bpython/cli.py:315 bpython/urwid.py:551 +#: bpython/args.py:131 +msgid "" +"File to extecute and additional arguments passed on to the executed " +"script." +msgstr "" + +#: bpython/cli.py:315 bpython/urwid.py:538 msgid "y" msgstr "" -#: bpython/cli.py:315 bpython/urwid.py:551 +#: bpython/cli.py:315 bpython/urwid.py:538 msgid "yes" msgstr "" @@ -75,242 +82,250 @@ msgid "" " future version." msgstr "" -#: bpython/curtsies.py:147 +#: bpython/curtsies.py:137 msgid "log debug messages to bpython.log" msgstr "" -#: bpython/curtsies.py:153 +#: bpython/curtsies.py:143 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/history.py:226 +#: bpython/curtsies.py:149 +msgid "curtsies arguments" +msgstr "" + +#: bpython/curtsies.py:150 +msgid "Additional arguments specific to the curtsies-based REPL." +msgstr "" + +#: bpython/history.py:224 #, python-format msgid "Error occurred while writing to file %s (%s)" msgstr "" -#: bpython/paste.py:86 +#: bpython/paste.py:85 msgid "Helper program not found." msgstr "" -#: bpython/paste.py:88 +#: bpython/paste.py:87 msgid "Helper program could not be run." msgstr "" -#: bpython/paste.py:94 +#: bpython/paste.py:93 #, python-format msgid "Helper program returned non-zero exit status %d." msgstr "" -#: bpython/paste.py:99 +#: bpython/paste.py:98 msgid "No output from helper program." msgstr "" -#: bpython/paste.py:106 +#: bpython/paste.py:105 msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:657 +#: bpython/repl.py:653 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:662 +#: bpython/repl.py:658 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:667 +#: bpython/repl.py:663 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:669 +#: bpython/repl.py:665 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:823 +#: bpython/repl.py:820 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:825 bpython/repl.py:828 bpython/repl.py:852 +#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:839 +#: bpython/repl.py:836 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:847 +#: bpython/repl.py:844 msgid "overwrite" msgstr "" -#: bpython/repl.py:849 +#: bpython/repl.py:846 msgid "append" msgstr "" -#: bpython/repl.py:861 bpython/repl.py:1171 +#: bpython/repl.py:858 bpython/repl.py:1168 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:863 +#: bpython/repl.py:860 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:869 +#: bpython/repl.py:866 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:876 +#: bpython/repl.py:873 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:878 +#: bpython/repl.py:875 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:887 +#: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:889 +#: bpython/repl.py:886 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:897 +#: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:903 +#: bpython/repl.py:900 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:907 +#: bpython/repl.py:904 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:916 +#: bpython/repl.py:913 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:921 +#: bpython/repl.py:918 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:960 +#: bpython/repl.py:957 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:968 bpython/repl.py:972 +#: bpython/repl.py:965 bpython/repl.py:969 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:975 +#: bpython/repl.py:972 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1151 +#: bpython/repl.py:1148 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1181 +#: bpython/repl.py:1178 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1187 +#: bpython/repl.py:1184 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:618 +#: bpython/urwid.py:605 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" -#: bpython/urwid.py:1136 +#: bpython/urwid.py:1115 msgid "Run twisted reactor." msgstr "" -#: bpython/urwid.py:1141 +#: bpython/urwid.py:1120 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "" -#: bpython/urwid.py:1149 +#: bpython/urwid.py:1128 msgid "List available reactors for -r." msgstr "" -#: bpython/urwid.py:1154 +#: bpython/urwid.py:1133 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." msgstr "" -#: bpython/urwid.py:1163 +#: bpython/urwid.py:1142 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1350 +#: bpython/urwid.py:1336 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:340 +#: bpython/curtsiesfrontend/repl.py:343 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:342 +#: bpython/curtsiesfrontend/repl.py:345 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:671 +#: bpython/curtsiesfrontend/repl.py:674 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:689 +#: bpython/curtsiesfrontend/repl.py:692 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:998 +#: bpython/curtsiesfrontend/repl.py:1001 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1013 +#: bpython/curtsiesfrontend/repl.py:1016 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1023 +#: bpython/curtsiesfrontend/repl.py:1026 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1034 +#: bpython/curtsiesfrontend/repl.py:1037 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1040 +#: bpython/curtsiesfrontend/repl.py:1043 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1045 +#: bpython/curtsiesfrontend/repl.py:1048 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1051 +#: bpython/curtsiesfrontend/repl.py:1054 msgid "Auto-reloading not available because watchdog not installed." msgstr "" diff --git a/bpython/translations/de/LC_MESSAGES/bpython.po b/bpython/translations/de/LC_MESSAGES/bpython.po index 7023785e6..951c1cc0b 100644 --- a/bpython/translations/de/LC_MESSAGES/bpython.po +++ b/bpython/translations/de/LC_MESSAGES/bpython.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: bpython mercurial\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2020-10-13 21:53+0200\n" -"PO-Revision-Date: 2020-10-19 20:59+0200\n" +"POT-Creation-Date: 2020-10-29 12:28+0100\n" +"PO-Revision-Date: 2020-10-29 12:26+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: de\n" "Language-Team: de \n" @@ -18,37 +18,44 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:63 +#: bpython/args.py:92 +#, python-format msgid "" -"Usage: %prog [options] [file [args]]\n" +"Usage: %(prog)s [options] [file [args]]\n" "NOTE: If bpython sees an argument it does not know, execution falls back " "to the regular Python interpreter." msgstr "" -"Verwendung: %prog [Optionen] [Datei [Argumente]]\n" +"Verwendung: %(prog)s [Optionen] [Datei [Argumente]]\n" "Hinweis: Wenn bpython Argumente übergeben bekommt, die nicht verstanden " "werden, wird der normale Python Interpreter ausgeführt." -#: bpython/args.py:78 +#: bpython/args.py:102 msgid "Use CONFIG instead of default config file." msgstr "Verwende CONFIG antatt der standardmäßigen Konfigurationsdatei." -#: bpython/args.py:84 +#: bpython/args.py:108 msgid "Drop to bpython shell after running file instead of exiting." msgstr "Verbleibe in bpython nach dem Ausführen der Datei." -#: bpython/args.py:92 +#: bpython/args.py:114 msgid "Don't flush the output to stdout." msgstr "Gib Ausgabe beim Beenden nicht ernaut auf stdout aus." -#: bpython/args.py:98 +#: bpython/args.py:120 msgid "Print version and exit." msgstr "Zeige Versionsinformationen an und beende." -#: bpython/cli.py:315 bpython/urwid.py:551 +#: bpython/args.py:131 +msgid "" +"File to extecute and additional arguments passed on to the executed " +"script." +msgstr "" + +#: bpython/cli.py:315 bpython/urwid.py:538 msgid "y" msgstr "j" -#: bpython/cli.py:315 bpython/urwid.py:551 +#: bpython/cli.py:315 bpython/urwid.py:538 msgid "yes" msgstr "ja" @@ -82,189 +89,197 @@ msgstr "" "`bpython`. Diese Implementierung wird ab Version 0.19 nicht mehr aktiv " "unterstützt und wird in einer zukünftigen Version entfernt werden." -#: bpython/curtsies.py:147 +#: bpython/curtsies.py:137 msgid "log debug messages to bpython.log" msgstr "Zeichne debug Nachrichten in bpython.log auf" -#: bpython/curtsies.py:153 +#: bpython/curtsies.py:143 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/history.py:226 +#: bpython/curtsies.py:149 +msgid "curtsies arguments" +msgstr "" + +#: bpython/curtsies.py:150 +msgid "Additional arguments specific to the curtsies-based REPL." +msgstr "Zusätzliche Argumente spezifisch für die curtsies-basierte REPL." + +#: bpython/history.py:224 #, python-format msgid "Error occurred while writing to file %s (%s)" msgstr "Fehler beim Schreiben in Datei %s aufgetreten (%s)" -#: bpython/paste.py:86 +#: bpython/paste.py:85 msgid "Helper program not found." msgstr "Hilfsprogramm konnte nicht gefunden werden." -#: bpython/paste.py:88 +#: bpython/paste.py:87 msgid "Helper program could not be run." msgstr "Hilfsprogramm konnte nicht ausgeführt werden." -#: bpython/paste.py:94 +#: bpython/paste.py:93 #, python-format msgid "Helper program returned non-zero exit status %d." msgstr "Hilfsprogramm beendete mit Status %d." -#: bpython/paste.py:99 +#: bpython/paste.py:98 msgid "No output from helper program." msgstr "Keine Ausgabe von Hilfsprogramm vorhanden." -#: bpython/paste.py:106 +#: bpython/paste.py:105 msgid "Failed to recognize the helper program's output as an URL." msgstr "Konnte Ausgabe von Hilfsprogramm nicht verarbeiten." -#: bpython/repl.py:657 +#: bpython/repl.py:653 msgid "Nothing to get source of" msgstr "Nichts um Quellcode abzurufen" -#: bpython/repl.py:662 +#: bpython/repl.py:658 #, python-format msgid "Cannot get source: %s" msgstr "Kann Quellcode nicht finden: %s" -#: bpython/repl.py:667 +#: bpython/repl.py:663 #, python-format msgid "Cannot access source of %r" msgstr "Kann auf Quellcode nicht zugreifen: %r" -#: bpython/repl.py:669 +#: bpython/repl.py:665 #, python-format msgid "No source code found for %s" msgstr "Quellcode für %s nicht gefunden" -#: bpython/repl.py:823 +#: bpython/repl.py:820 msgid "Save to file (Esc to cancel): " msgstr "In Datei speichern (Esc um abzubrechen): " -#: bpython/repl.py:825 bpython/repl.py:828 bpython/repl.py:852 +#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 msgid "Save cancelled." msgstr "Speichern abgebrochen." -#: bpython/repl.py:839 +#: bpython/repl.py:836 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " -msgstr "%s existiert bereit. (C) abbrechen, (o) überschrieben oder (a) anhängen?" +msgstr "%s existiert bereit. (C) abbrechen, (o) überschrieben oder (a) anhängen? " -#: bpython/repl.py:847 +#: bpython/repl.py:844 msgid "overwrite" msgstr "überschreiben" -#: bpython/repl.py:849 +#: bpython/repl.py:846 msgid "append" msgstr "anhängen" -#: bpython/repl.py:861 bpython/repl.py:1171 +#: bpython/repl.py:858 bpython/repl.py:1168 #, python-format msgid "Error writing file '%s': %s" msgstr "Fehler beim Schreiben in Datei '%s': %s" -#: bpython/repl.py:863 +#: bpython/repl.py:860 #, python-format msgid "Saved to %s." msgstr "Nach %s gespeichert." -#: bpython/repl.py:869 +#: bpython/repl.py:866 msgid "No clipboard available." msgstr "Zwischenablage ist nicht verfügbar." -#: bpython/repl.py:876 +#: bpython/repl.py:873 msgid "Could not copy to clipboard." msgstr "Konnte nicht in Zwischenablage kopieren." -#: bpython/repl.py:878 +#: bpython/repl.py:875 msgid "Copied content to clipboard." msgstr "Inhalt wurde in Zwischenablage kopiert." -#: bpython/repl.py:887 +#: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:889 +#: bpython/repl.py:886 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:897 +#: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:903 +#: bpython/repl.py:900 msgid "Posting data to pastebin..." msgstr "Lade Daten hoch..." -#: bpython/repl.py:907 +#: bpython/repl.py:904 #, python-format msgid "Upload failed: %s" msgstr "Hochladen ist fehlgeschlagen: %s" -#: bpython/repl.py:916 +#: bpython/repl.py:913 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:921 +#: bpython/repl.py:918 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:960 +#: bpython/repl.py:957 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -"Wie viele Zeilen rückgängig machen? (Rückgängigmachen wird bis zu ~%1.f " +"Wie viele Zeilen rückgängig machen? (Rückgängigmachen wird bis zu ~%.1f " "Sekunden brauchen) [1]" -#: bpython/repl.py:968 bpython/repl.py:972 +#: bpython/repl.py:965 bpython/repl.py:969 msgid "Undo canceled" msgstr "Rückgängigmachen abgebrochen" -#: bpython/repl.py:975 +#: bpython/repl.py:972 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "Mache %d Zeile rückgängig... (ungefähr %.1f Sekunden)" msgstr[1] "Mache %d Zeilen rückgängig... (ungefähr %.1f Sekunden)" -#: bpython/repl.py:1151 +#: bpython/repl.py:1148 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" "Konfigurationsdatei existiert nicht. Soll eine neue Datei erstellt " "werden? (j/N)" -#: bpython/repl.py:1181 +#: bpython/repl.py:1178 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" "bpython Konfigurationsdatei bearbeitet. Starte bpython neu damit die " "Änderungen übernommen werden." -#: bpython/repl.py:1187 +#: bpython/repl.py:1184 #, python-format msgid "Error editing config file: %s" msgstr "Fehler beim Bearbeiten der Konfigurationsdatei: %s" -#: bpython/urwid.py:618 +#: bpython/urwid.py:605 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" " <%s> Rückgängigmachen <%s> Speichern <%s> Pastebin <%s> Pager <%s> " "Quellcode anzeigen " -#: bpython/urwid.py:1136 +#: bpython/urwid.py:1115 msgid "Run twisted reactor." msgstr "Führe twisted reactor aus." -#: bpython/urwid.py:1141 +#: bpython/urwid.py:1120 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "Wähle reactor aus (siehe --help-reactors). Impliziert --twisted." -#: bpython/urwid.py:1149 +#: bpython/urwid.py:1128 msgid "List available reactors for -r." msgstr "Liste verfügbare reactors für -r auf." -#: bpython/urwid.py:1154 +#: bpython/urwid.py:1133 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." @@ -272,11 +287,11 @@ msgstr "" "Auszuführendes twistd Plugin (starte twistd für eine Liste). Verwende " "\"--\" um Optionen an das Plugin zu übergeben." -#: bpython/urwid.py:1163 +#: bpython/urwid.py:1142 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1350 +#: bpython/urwid.py:1336 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" @@ -286,51 +301,51 @@ msgstr "" "`bpython`. Diese Implementierung wird ab Version 0.19 nicht mehr aktiv " "unterstützt und wird in einer zukünftigen Version entfernt werden." -#: bpython/curtsiesfrontend/repl.py:340 +#: bpython/curtsiesfrontend/repl.py:343 msgid "Welcome to bpython!" msgstr "Willkommen by bpython!" -#: bpython/curtsiesfrontend/repl.py:342 +#: bpython/curtsiesfrontend/repl.py:345 #, python-format msgid "Press <%s> for help." msgstr "Drücke <%s> für Hilfe." -#: bpython/curtsiesfrontend/repl.py:671 +#: bpython/curtsiesfrontend/repl.py:674 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "Fehler beim Ausführen von PYTHONSTARTUP: %s" -#: bpython/curtsiesfrontend/repl.py:689 +#: bpython/curtsiesfrontend/repl.py:692 #, python-format msgid "Reloaded at %s because %s modified." msgstr "Bei %s neugeladen, da %s modifiziert wurde." -#: bpython/curtsiesfrontend/repl.py:998 +#: bpython/curtsiesfrontend/repl.py:1001 msgid "Session not reevaluated because it was not edited" msgstr "Die Sitzung wurde nicht neu ausgeführt, da sie nicht berabeitet wurde" -#: bpython/curtsiesfrontend/repl.py:1013 +#: bpython/curtsiesfrontend/repl.py:1016 msgid "Session not reevaluated because saved file was blank" msgstr "Die Sitzung wurde nicht neu ausgeführt, da die gespeicherte Datei leer war" -#: bpython/curtsiesfrontend/repl.py:1023 +#: bpython/curtsiesfrontend/repl.py:1026 msgid "Session edited and reevaluated" msgstr "Sitzung bearbeitet und neu ausgeführt" -#: bpython/curtsiesfrontend/repl.py:1034 +#: bpython/curtsiesfrontend/repl.py:1037 #, python-format msgid "Reloaded at %s by user." msgstr "Bei %s vom Benutzer neu geladen." -#: bpython/curtsiesfrontend/repl.py:1040 +#: bpython/curtsiesfrontend/repl.py:1043 msgid "Auto-reloading deactivated." msgstr "Automatisches Neuladen deaktiviert." -#: bpython/curtsiesfrontend/repl.py:1045 +#: bpython/curtsiesfrontend/repl.py:1048 msgid "Auto-reloading active, watching for file changes..." msgstr "Automatisches Neuladen ist aktiv; beobachte Dateiänderungen..." -#: bpython/curtsiesfrontend/repl.py:1051 +#: bpython/curtsiesfrontend/repl.py:1054 msgid "Auto-reloading not available because watchdog not installed." msgstr "" "Automatisches Neuladen ist nicht verfügbar da watchdog nicht installiert " diff --git a/bpython/translations/es_ES/LC_MESSAGES/bpython.po b/bpython/translations/es_ES/LC_MESSAGES/bpython.po index 57057a420..7bb910396 100644 --- a/bpython/translations/es_ES/LC_MESSAGES/bpython.po +++ b/bpython/translations/es_ES/LC_MESSAGES/bpython.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2020-10-13 21:53+0200\n" -"PO-Revision-Date: 2015-02-02 00:34+0100\n" +"POT-Creation-Date: 2020-10-29 12:28+0100\n" +"PO-Revision-Date: 2020-10-29 12:22+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: es_ES\n" "Language-Team: bpython developers\n" @@ -18,34 +18,41 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:63 +#: bpython/args.py:92 +#, python-format msgid "" -"Usage: %prog [options] [file [args]]\n" +"Usage: %(prog)s [options] [file [args]]\n" "NOTE: If bpython sees an argument it does not know, execution falls back " "to the regular Python interpreter." msgstr "" -#: bpython/args.py:78 +#: bpython/args.py:102 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:84 +#: bpython/args.py:108 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:92 +#: bpython/args.py:114 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:98 +#: bpython/args.py:120 msgid "Print version and exit." msgstr "" -#: bpython/cli.py:315 bpython/urwid.py:551 +#: bpython/args.py:131 +msgid "" +"File to extecute and additional arguments passed on to the executed " +"script." +msgstr "" + +#: bpython/cli.py:315 bpython/urwid.py:538 msgid "y" msgstr "s" -#: bpython/cli.py:315 bpython/urwid.py:551 +#: bpython/cli.py:315 bpython/urwid.py:538 msgid "yes" msgstr "si" @@ -76,247 +83,252 @@ msgid "" " future version." msgstr "" -#: bpython/curtsies.py:147 +#: bpython/curtsies.py:137 msgid "log debug messages to bpython.log" msgstr "" -#: bpython/curtsies.py:153 +#: bpython/curtsies.py:143 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/history.py:226 +#: bpython/curtsies.py:149 +msgid "curtsies arguments" +msgstr "" + +#: bpython/curtsies.py:150 +msgid "Additional arguments specific to the curtsies-based REPL." +msgstr "" + +#: bpython/history.py:224 #, python-format msgid "Error occurred while writing to file %s (%s)" msgstr "" -#: bpython/paste.py:86 +#: bpython/paste.py:85 msgid "Helper program not found." msgstr "" -#: bpython/paste.py:88 +#: bpython/paste.py:87 msgid "Helper program could not be run." msgstr "" -#: bpython/paste.py:94 +#: bpython/paste.py:93 #, python-format msgid "Helper program returned non-zero exit status %d." msgstr "" -#: bpython/paste.py:99 +#: bpython/paste.py:98 msgid "No output from helper program." msgstr "" -#: bpython/paste.py:106 +#: bpython/paste.py:105 msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:657 +#: bpython/repl.py:653 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:662 +#: bpython/repl.py:658 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:667 +#: bpython/repl.py:663 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:669 +#: bpython/repl.py:665 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:823 +#: bpython/repl.py:820 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:825 bpython/repl.py:828 bpython/repl.py:852 +#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:839 +#: bpython/repl.py:836 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:847 +#: bpython/repl.py:844 msgid "overwrite" msgstr "" -#: bpython/repl.py:849 +#: bpython/repl.py:846 msgid "append" msgstr "" -#: bpython/repl.py:861 bpython/repl.py:1171 +#: bpython/repl.py:858 bpython/repl.py:1168 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:863 +#: bpython/repl.py:860 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:869 +#: bpython/repl.py:866 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:876 +#: bpython/repl.py:873 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:878 +#: bpython/repl.py:875 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:887 +#: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:889 +#: bpython/repl.py:886 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:897 +#: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:903 +#: bpython/repl.py:900 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:907 +#: bpython/repl.py:904 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:916 +#: bpython/repl.py:913 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:921 +#: bpython/repl.py:918 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:960 +#: bpython/repl.py:957 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:968 bpython/repl.py:972 +#: bpython/repl.py:965 bpython/repl.py:969 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:975 +#: bpython/repl.py:972 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1151 +#: bpython/repl.py:1148 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1181 +#: bpython/repl.py:1178 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1187 +#: bpython/repl.py:1184 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:618 +#: bpython/urwid.py:605 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" " <%s> Rewind <%s> Salva <%s> Pastebin <%s> Pager <%s> Mostra el " "código fuente" -#: bpython/urwid.py:1136 +#: bpython/urwid.py:1115 msgid "Run twisted reactor." msgstr "" -#: bpython/urwid.py:1141 +#: bpython/urwid.py:1120 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "" -#: bpython/urwid.py:1149 +#: bpython/urwid.py:1128 msgid "List available reactors for -r." msgstr "" -#: bpython/urwid.py:1154 +#: bpython/urwid.py:1133 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." msgstr "" -#: bpython/urwid.py:1163 +#: bpython/urwid.py:1142 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1350 +#: bpython/urwid.py:1336 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:340 +#: bpython/curtsiesfrontend/repl.py:343 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:342 +#: bpython/curtsiesfrontend/repl.py:345 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:671 +#: bpython/curtsiesfrontend/repl.py:674 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:689 +#: bpython/curtsiesfrontend/repl.py:692 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:998 +#: bpython/curtsiesfrontend/repl.py:1001 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1013 +#: bpython/curtsiesfrontend/repl.py:1016 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1023 +#: bpython/curtsiesfrontend/repl.py:1026 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1034 +#: bpython/curtsiesfrontend/repl.py:1037 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1040 +#: bpython/curtsiesfrontend/repl.py:1043 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1045 +#: bpython/curtsiesfrontend/repl.py:1048 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1051 +#: bpython/curtsiesfrontend/repl.py:1054 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#~ msgid "Error editing config file." -#~ msgstr "" - diff --git a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po index 73330837f..d3d00f08a 100644 --- a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po +++ b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.13-442\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2020-10-13 21:53+0200\n" -"PO-Revision-Date: 2019-09-22 22:58+0200\n" +"POT-Creation-Date: 2020-10-29 12:28+0100\n" +"PO-Revision-Date: 2020-10-29 12:20+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: fr_FR\n" "Language-Team: bpython developers\n" @@ -17,39 +17,46 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:63 +#: bpython/args.py:92 +#, python-format msgid "" -"Usage: %prog [options] [file [args]]\n" +"Usage: %(prog)s [options] [file [args]]\n" "NOTE: If bpython sees an argument it does not know, execution falls back " "to the regular Python interpreter." msgstr "" -"Utilisation: %prog [options] [fichier [arguments]]\n" +"Utilisation: %(prog)s [options] [fichier [arguments]]\n" "NOTE: Si bpython ne reconnaît pas un des arguments fournis, " "l'interpréteur Python classique sera lancé" -#: bpython/args.py:78 +#: bpython/args.py:102 msgid "Use CONFIG instead of default config file." msgstr "Utiliser CONFIG à la place du fichier de configuration par défaut." -#: bpython/args.py:84 +#: bpython/args.py:108 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" "Aller dans le shell bpython après l'exécution du fichier au lieu de " "quitter." -#: bpython/args.py:92 +#: bpython/args.py:114 msgid "Don't flush the output to stdout." msgstr "Ne pas purger la sortie vers stdout." -#: bpython/args.py:98 +#: bpython/args.py:120 msgid "Print version and exit." msgstr "Afficher la version et quitter." -#: bpython/cli.py:315 bpython/urwid.py:551 +#: bpython/args.py:131 +msgid "" +"File to extecute and additional arguments passed on to the executed " +"script." +msgstr "" + +#: bpython/cli.py:315 bpython/urwid.py:538 msgid "y" msgstr "o" -#: bpython/cli.py:315 bpython/urwid.py:551 +#: bpython/cli.py:315 bpython/urwid.py:538 msgid "yes" msgstr "oui" @@ -80,183 +87,191 @@ msgid "" " future version." msgstr "" -#: bpython/curtsies.py:147 +#: bpython/curtsies.py:137 msgid "log debug messages to bpython.log" msgstr "logger les messages de debug dans bpython.log" -#: bpython/curtsies.py:153 +#: bpython/curtsies.py:143 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/history.py:226 +#: bpython/curtsies.py:149 +msgid "curtsies arguments" +msgstr "" + +#: bpython/curtsies.py:150 +msgid "Additional arguments specific to the curtsies-based REPL." +msgstr "" + +#: bpython/history.py:224 #, python-format msgid "Error occurred while writing to file %s (%s)" msgstr "Une erreur s'est produite pendant l'écriture du fichier %s (%s)" -#: bpython/paste.py:86 +#: bpython/paste.py:85 msgid "Helper program not found." msgstr "programme externe non trouvé." -#: bpython/paste.py:88 +#: bpython/paste.py:87 msgid "Helper program could not be run." msgstr "impossible de lancer le programme externe." -#: bpython/paste.py:94 +#: bpython/paste.py:93 #, python-format msgid "Helper program returned non-zero exit status %d." msgstr "le programme externe a renvoyé un statut de sortie différent de zéro %d." -#: bpython/paste.py:99 +#: bpython/paste.py:98 msgid "No output from helper program." msgstr "pas de sortie du programme externe." -#: bpython/paste.py:106 +#: bpython/paste.py:105 msgid "Failed to recognize the helper program's output as an URL." msgstr "la sortie du programme externe ne correspond pas à une URL." -#: bpython/repl.py:657 +#: bpython/repl.py:653 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:662 +#: bpython/repl.py:658 #, python-format msgid "Cannot get source: %s" msgstr "Impossible de récupérer le source: %s" -#: bpython/repl.py:667 +#: bpython/repl.py:663 #, python-format msgid "Cannot access source of %r" msgstr "Impossible d'accéder au source de %r" -#: bpython/repl.py:669 +#: bpython/repl.py:665 #, python-format msgid "No source code found for %s" msgstr "Pas de code source trouvé pour %s" -#: bpython/repl.py:823 +#: bpython/repl.py:820 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:825 bpython/repl.py:828 bpython/repl.py:852 +#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:839 +#: bpython/repl.py:836 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:847 +#: bpython/repl.py:844 msgid "overwrite" msgstr "" -#: bpython/repl.py:849 +#: bpython/repl.py:846 msgid "append" msgstr "" -#: bpython/repl.py:861 bpython/repl.py:1171 +#: bpython/repl.py:858 bpython/repl.py:1168 #, python-format msgid "Error writing file '%s': %s" msgstr "Une erreur s'est produite pendant l'écriture du fichier '%s': %s" -#: bpython/repl.py:863 +#: bpython/repl.py:860 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:869 +#: bpython/repl.py:866 msgid "No clipboard available." msgstr "Pas de presse-papier disponible." -#: bpython/repl.py:876 +#: bpython/repl.py:873 msgid "Could not copy to clipboard." msgstr "Impossible de copier vers le presse-papier." -#: bpython/repl.py:878 +#: bpython/repl.py:875 msgid "Copied content to clipboard." msgstr "Contenu copié vers le presse-papier." -#: bpython/repl.py:887 +#: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " msgstr "Tampon Pastebin ? (o/N) " -#: bpython/repl.py:889 +#: bpython/repl.py:886 msgid "Pastebin aborted." msgstr "Pastebin abandonné." -#: bpython/repl.py:897 +#: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "Pastebin dupliqué. URL précédente: %s. URL de suppression: %s" -#: bpython/repl.py:903 +#: bpython/repl.py:900 msgid "Posting data to pastebin..." msgstr "Envoi des donnés à pastebin..." -#: bpython/repl.py:907 +#: bpython/repl.py:904 #, python-format msgid "Upload failed: %s" msgstr "Echec du téléchargement: %s" -#: bpython/repl.py:916 +#: bpython/repl.py:913 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "URL Pastebin: %s - URL de suppression: %s" -#: bpython/repl.py:921 +#: bpython/repl.py:918 #, python-format msgid "Pastebin URL: %s" msgstr "URL Pastebin: %s" -#: bpython/repl.py:960 +#: bpython/repl.py:957 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:968 bpython/repl.py:972 +#: bpython/repl.py:965 bpython/repl.py:969 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:975 +#: bpython/repl.py:972 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1151 +#: bpython/repl.py:1148 msgid "Config file does not exist - create new from default? (y/N)" msgstr "Le fichier de configuration n'existe pas - en créér un par défaut? (o/N)" -#: bpython/repl.py:1181 +#: bpython/repl.py:1178 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1187 +#: bpython/repl.py:1184 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:618 +#: bpython/urwid.py:605 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" " <%s> Rebobiner <%s> Sauvegarder <%s> Pastebin <%s> Pager <%s> " "Montrer Source " -#: bpython/urwid.py:1136 +#: bpython/urwid.py:1115 msgid "Run twisted reactor." msgstr "Lancer le reactor twisted." -#: bpython/urwid.py:1141 +#: bpython/urwid.py:1120 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "Choisir un reactor spécifique (voir --help-reactors). Nécessite --twisted." -#: bpython/urwid.py:1149 +#: bpython/urwid.py:1128 msgid "List available reactors for -r." msgstr "Lister les reactors disponibles pour -r." -#: bpython/urwid.py:1154 +#: bpython/urwid.py:1133 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." @@ -264,62 +279,62 @@ msgstr "" "plugin twistd à lancer (utiliser twistd pour une list). Utiliser \"--\" " "pour donner plus d'options au plugin." -#: bpython/urwid.py:1163 +#: bpython/urwid.py:1142 msgid "Port to run an eval server on (forces Twisted)." msgstr "Port pour lancer un server eval (force Twisted)." -#: bpython/urwid.py:1350 +#: bpython/urwid.py:1336 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:340 +#: bpython/curtsiesfrontend/repl.py:343 msgid "Welcome to bpython!" msgstr "Bienvenue dans bpython!" -#: bpython/curtsiesfrontend/repl.py:342 +#: bpython/curtsiesfrontend/repl.py:345 #, python-format msgid "Press <%s> for help." msgstr "Appuyer sur <%s> pour de l'aide." -#: bpython/curtsiesfrontend/repl.py:671 +#: bpython/curtsiesfrontend/repl.py:674 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "L'exécution de PYTHONSTARTUP a échoué: %s" -#: bpython/curtsiesfrontend/repl.py:689 +#: bpython/curtsiesfrontend/repl.py:692 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:998 +#: bpython/curtsiesfrontend/repl.py:1001 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1013 +#: bpython/curtsiesfrontend/repl.py:1016 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1023 +#: bpython/curtsiesfrontend/repl.py:1026 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1034 +#: bpython/curtsiesfrontend/repl.py:1037 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1040 +#: bpython/curtsiesfrontend/repl.py:1043 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1045 +#: bpython/curtsiesfrontend/repl.py:1048 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1051 +#: bpython/curtsiesfrontend/repl.py:1054 msgid "Auto-reloading not available because watchdog not installed." msgstr "" diff --git a/bpython/translations/it_IT/LC_MESSAGES/bpython.po b/bpython/translations/it_IT/LC_MESSAGES/bpython.po index 7c94712da..7d0a39783 100644 --- a/bpython/translations/it_IT/LC_MESSAGES/bpython.po +++ b/bpython/translations/it_IT/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2020-10-13 21:53+0200\n" +"POT-Creation-Date: 2020-10-29 12:28+0100\n" "PO-Revision-Date: 2015-02-02 00:34+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: it_IT\n" @@ -18,34 +18,41 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:63 +#: bpython/args.py:92 +#, python-format msgid "" -"Usage: %prog [options] [file [args]]\n" +"Usage: %(prog)s [options] [file [args]]\n" "NOTE: If bpython sees an argument it does not know, execution falls back " "to the regular Python interpreter." msgstr "" -#: bpython/args.py:78 +#: bpython/args.py:102 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:84 +#: bpython/args.py:108 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:92 +#: bpython/args.py:114 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:98 +#: bpython/args.py:120 msgid "Print version and exit." msgstr "" -#: bpython/cli.py:315 bpython/urwid.py:551 +#: bpython/args.py:131 +msgid "" +"File to extecute and additional arguments passed on to the executed " +"script." +msgstr "" + +#: bpython/cli.py:315 bpython/urwid.py:538 msgid "y" msgstr "s" -#: bpython/cli.py:315 bpython/urwid.py:551 +#: bpython/cli.py:315 bpython/urwid.py:538 msgid "yes" msgstr "si" @@ -76,242 +83,250 @@ msgid "" " future version." msgstr "" -#: bpython/curtsies.py:147 +#: bpython/curtsies.py:137 msgid "log debug messages to bpython.log" msgstr "" -#: bpython/curtsies.py:153 +#: bpython/curtsies.py:143 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/history.py:226 +#: bpython/curtsies.py:149 +msgid "curtsies arguments" +msgstr "" + +#: bpython/curtsies.py:150 +msgid "Additional arguments specific to the curtsies-based REPL." +msgstr "" + +#: bpython/history.py:224 #, python-format msgid "Error occurred while writing to file %s (%s)" msgstr "" -#: bpython/paste.py:86 +#: bpython/paste.py:85 msgid "Helper program not found." msgstr "" -#: bpython/paste.py:88 +#: bpython/paste.py:87 msgid "Helper program could not be run." msgstr "" -#: bpython/paste.py:94 +#: bpython/paste.py:93 #, python-format msgid "Helper program returned non-zero exit status %d." msgstr "" -#: bpython/paste.py:99 +#: bpython/paste.py:98 msgid "No output from helper program." msgstr "" -#: bpython/paste.py:106 +#: bpython/paste.py:105 msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:657 +#: bpython/repl.py:653 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:662 +#: bpython/repl.py:658 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:667 +#: bpython/repl.py:663 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:669 +#: bpython/repl.py:665 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:823 +#: bpython/repl.py:820 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:825 bpython/repl.py:828 bpython/repl.py:852 +#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:839 +#: bpython/repl.py:836 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:847 +#: bpython/repl.py:844 msgid "overwrite" msgstr "" -#: bpython/repl.py:849 +#: bpython/repl.py:846 msgid "append" msgstr "" -#: bpython/repl.py:861 bpython/repl.py:1171 +#: bpython/repl.py:858 bpython/repl.py:1168 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:863 +#: bpython/repl.py:860 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:869 +#: bpython/repl.py:866 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:876 +#: bpython/repl.py:873 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:878 +#: bpython/repl.py:875 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:887 +#: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:889 +#: bpython/repl.py:886 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:897 +#: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:903 +#: bpython/repl.py:900 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:907 +#: bpython/repl.py:904 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:916 +#: bpython/repl.py:913 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:921 +#: bpython/repl.py:918 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:960 +#: bpython/repl.py:957 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:968 bpython/repl.py:972 +#: bpython/repl.py:965 bpython/repl.py:969 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:975 +#: bpython/repl.py:972 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1151 +#: bpython/repl.py:1148 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1181 +#: bpython/repl.py:1178 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1187 +#: bpython/repl.py:1184 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:618 +#: bpython/urwid.py:605 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr " <%s> Rewind <%s> Salva <%s> Pastebin <%s> Pager <%s> Mostra Sorgente" -#: bpython/urwid.py:1136 +#: bpython/urwid.py:1115 msgid "Run twisted reactor." msgstr "" -#: bpython/urwid.py:1141 +#: bpython/urwid.py:1120 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "" -#: bpython/urwid.py:1149 +#: bpython/urwid.py:1128 msgid "List available reactors for -r." msgstr "" -#: bpython/urwid.py:1154 +#: bpython/urwid.py:1133 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." msgstr "" -#: bpython/urwid.py:1163 +#: bpython/urwid.py:1142 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1350 +#: bpython/urwid.py:1336 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:340 +#: bpython/curtsiesfrontend/repl.py:343 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:342 +#: bpython/curtsiesfrontend/repl.py:345 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:671 +#: bpython/curtsiesfrontend/repl.py:674 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:689 +#: bpython/curtsiesfrontend/repl.py:692 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:998 +#: bpython/curtsiesfrontend/repl.py:1001 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1013 +#: bpython/curtsiesfrontend/repl.py:1016 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1023 +#: bpython/curtsiesfrontend/repl.py:1026 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1034 +#: bpython/curtsiesfrontend/repl.py:1037 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1040 +#: bpython/curtsiesfrontend/repl.py:1043 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1045 +#: bpython/curtsiesfrontend/repl.py:1048 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1051 +#: bpython/curtsiesfrontend/repl.py:1054 msgid "Auto-reloading not available because watchdog not installed." msgstr "" diff --git a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po index eab83d1ae..44f047281 100644 --- a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po +++ b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7.1\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2020-10-13 21:53+0200\n" -"PO-Revision-Date: 2015-02-02 00:34+0100\n" +"POT-Creation-Date: 2020-10-29 12:28+0100\n" +"PO-Revision-Date: 2020-10-29 12:20+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: nl_NL\n" "Language-Team: bpython developers\n" @@ -18,34 +18,41 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:63 +#: bpython/args.py:92 +#, python-format msgid "" -"Usage: %prog [options] [file [args]]\n" +"Usage: %(prog)s [options] [file [args]]\n" "NOTE: If bpython sees an argument it does not know, execution falls back " "to the regular Python interpreter." msgstr "" -#: bpython/args.py:78 +#: bpython/args.py:102 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:84 +#: bpython/args.py:108 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:92 +#: bpython/args.py:114 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:98 +#: bpython/args.py:120 msgid "Print version and exit." msgstr "" -#: bpython/cli.py:315 bpython/urwid.py:551 +#: bpython/args.py:131 +msgid "" +"File to extecute and additional arguments passed on to the executed " +"script." +msgstr "" + +#: bpython/cli.py:315 bpython/urwid.py:538 msgid "y" msgstr "j" -#: bpython/cli.py:315 bpython/urwid.py:551 +#: bpython/cli.py:315 bpython/urwid.py:538 msgid "yes" msgstr "ja" @@ -76,245 +83,250 @@ msgid "" " future version." msgstr "" -#: bpython/curtsies.py:147 +#: bpython/curtsies.py:137 msgid "log debug messages to bpython.log" msgstr "" -#: bpython/curtsies.py:153 +#: bpython/curtsies.py:143 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/history.py:226 +#: bpython/curtsies.py:149 +msgid "curtsies arguments" +msgstr "" + +#: bpython/curtsies.py:150 +msgid "Additional arguments specific to the curtsies-based REPL." +msgstr "" + +#: bpython/history.py:224 #, python-format msgid "Error occurred while writing to file %s (%s)" msgstr "" -#: bpython/paste.py:86 +#: bpython/paste.py:85 msgid "Helper program not found." msgstr "" -#: bpython/paste.py:88 +#: bpython/paste.py:87 msgid "Helper program could not be run." msgstr "" -#: bpython/paste.py:94 +#: bpython/paste.py:93 #, python-format msgid "Helper program returned non-zero exit status %d." msgstr "" -#: bpython/paste.py:99 +#: bpython/paste.py:98 msgid "No output from helper program." msgstr "" -#: bpython/paste.py:106 +#: bpython/paste.py:105 msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:657 +#: bpython/repl.py:653 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:662 +#: bpython/repl.py:658 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:667 +#: bpython/repl.py:663 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:669 +#: bpython/repl.py:665 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:823 +#: bpython/repl.py:820 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:825 bpython/repl.py:828 bpython/repl.py:852 +#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:839 +#: bpython/repl.py:836 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:847 +#: bpython/repl.py:844 msgid "overwrite" msgstr "" -#: bpython/repl.py:849 +#: bpython/repl.py:846 msgid "append" msgstr "" -#: bpython/repl.py:861 bpython/repl.py:1171 +#: bpython/repl.py:858 bpython/repl.py:1168 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:863 +#: bpython/repl.py:860 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:869 +#: bpython/repl.py:866 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:876 +#: bpython/repl.py:873 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:878 +#: bpython/repl.py:875 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:887 +#: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:889 +#: bpython/repl.py:886 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:897 +#: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:903 +#: bpython/repl.py:900 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:907 +#: bpython/repl.py:904 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:916 +#: bpython/repl.py:913 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:921 +#: bpython/repl.py:918 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:960 +#: bpython/repl.py:957 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:968 bpython/repl.py:972 +#: bpython/repl.py:965 bpython/repl.py:969 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:975 +#: bpython/repl.py:972 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1151 +#: bpython/repl.py:1148 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1181 +#: bpython/repl.py:1178 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1187 +#: bpython/repl.py:1184 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:618 +#: bpython/urwid.py:605 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr " <%s> Rewind <%s> Opslaan <%s> Pastebin <%s> Pager <%s> Toon broncode" -#: bpython/urwid.py:1136 +#: bpython/urwid.py:1115 msgid "Run twisted reactor." msgstr "" -#: bpython/urwid.py:1141 +#: bpython/urwid.py:1120 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "" -#: bpython/urwid.py:1149 +#: bpython/urwid.py:1128 msgid "List available reactors for -r." msgstr "" -#: bpython/urwid.py:1154 +#: bpython/urwid.py:1133 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." msgstr "" -#: bpython/urwid.py:1163 +#: bpython/urwid.py:1142 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1350 +#: bpython/urwid.py:1336 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:340 +#: bpython/curtsiesfrontend/repl.py:343 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:342 +#: bpython/curtsiesfrontend/repl.py:345 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:671 +#: bpython/curtsiesfrontend/repl.py:674 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:689 +#: bpython/curtsiesfrontend/repl.py:692 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:998 +#: bpython/curtsiesfrontend/repl.py:1001 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1013 +#: bpython/curtsiesfrontend/repl.py:1016 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1023 +#: bpython/curtsiesfrontend/repl.py:1026 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1034 +#: bpython/curtsiesfrontend/repl.py:1037 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1040 +#: bpython/curtsiesfrontend/repl.py:1043 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1045 +#: bpython/curtsiesfrontend/repl.py:1048 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1051 +#: bpython/curtsiesfrontend/repl.py:1054 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#~ msgid "Error editing config file." -#~ msgstr "" - From 9c143fa43bfd556e94d3f3775f5de5e1af4525e2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 29 Oct 2020 12:18:46 +0100 Subject: [PATCH 026/555] Re-use authors from bpython --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 2d50bed2b..78b6b1a21 100755 --- a/setup.py +++ b/setup.py @@ -127,7 +127,7 @@ def run(self): cmdclass = {"build": build, "install": install} -from bpython import package_dir +from bpython import package_dir, __author__ translations_dir = os.path.join(package_dir, "translations") @@ -227,7 +227,7 @@ def initialize_options(self): setup( name="bpython", version=version, - author="Bob Farrell, Andreas Stuehrk et al.", + author=__author__, author_email="robertanthonyfarrell@gmail.com", description="Fancy Interface to the Python Interpreter", license="MIT/X", From 29b1baeb4fd68a724627bfcf6b6db4e8b892c472 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 29 Oct 2020 17:33:12 +0100 Subject: [PATCH 027/555] Use super --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 78b6b1a21..4f47d5779 100755 --- a/setup.py +++ b/setup.py @@ -122,7 +122,7 @@ class install(_install): def run(self): self.run_command("build") - _install.run(self) + super().run() cmdclass = {"build": build, "install": install} @@ -144,7 +144,7 @@ def run(self): class BuildDocMan(BuildDoc): def initialize_options(self): - BuildDoc.initialize_options(self) + super().initialize_options() self.builder = "man" self.source_dir = "doc/sphinx/source" self.build_dir = "build" From 2ead56df21c0c8b73815c94313ec66285bf4f2e4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 29 Oct 2020 17:35:55 +0100 Subject: [PATCH 028/555] Remove obsolete install target fix up --- setup.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 4f47d5779..a2328643c 100755 --- a/setup.py +++ b/setup.py @@ -7,7 +7,6 @@ from distutils.command.build import build from setuptools import setup -from setuptools.command.install import install as _install try: from babel.messages import frontend as babel @@ -117,15 +116,7 @@ def git_describe_to_python_version(version): vf.write(f"__version__ = \"{version}\"\n") -class install(_install): - """Force install to run build target.""" - - def run(self): - self.run_command("build") - super().run() - - -cmdclass = {"build": build, "install": install} +cmdclass = {"build": build} from bpython import package_dir, __author__ From 627ebc9f6a02d8ca04b7b4241483e98e8e42e408 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 29 Oct 2020 17:38:17 +0100 Subject: [PATCH 029/555] Move build_sphinx_man configuration to setup.cfg --- setup.cfg | 5 +++++ setup.py | 12 ++---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/setup.cfg b/setup.cfg index 06135236a..bfed3932c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,3 +19,8 @@ output_dir = bpython/translations [extract_messages] output_file = bpython/translations/bpython.pot msgid_bugs_address = https://github.com/bpython/bpython/issues + +[build_sphinx_man] +builder = man +source_dir = doc/sphinx/source +build_dir = build diff --git a/setup.py b/setup.py index a2328643c..4650fd663 100755 --- a/setup.py +++ b/setup.py @@ -132,18 +132,10 @@ def git_describe_to_python_version(version): cmdclass["init_catalog"] = babel.init_catalog if using_sphinx: - - class BuildDocMan(BuildDoc): - def initialize_options(self): - super().initialize_options() - self.builder = "man" - self.source_dir = "doc/sphinx/source" - self.build_dir = "build" - build.sub_commands.insert(0, ("build_sphinx_man", None)) - cmdclass["build_sphinx_man"] = BuildDocMan + cmdclass["build_sphinx_man"] = BuildDoc - if platform.system() in ["FreeBSD", "OpenBSD"]: + if platform.system() in ("FreeBSD", "OpenBSD"): man_dir = "man" else: man_dir = "share/man" From 081e3f2d8dbdf08e3ea53bdfea115914c48cc002 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 1 Nov 2020 10:26:47 +0100 Subject: [PATCH 030/555] Replace type("") with str --- bpython/test/test_autocomplete.py | 2 +- bpython/test/test_curtsies_coderunner.py | 2 +- bpython/test/test_curtsies_repl.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 5b7ccd6ca..852a6ef7b 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -409,7 +409,7 @@ def function(): def test_completions_are_unicode(self): for m in self.com.matches(1, "a", locals_={"abc": 10}): - self.assertIsInstance(m, type("")) + self.assertIsInstance(m, str) def test_mock_kwlist(self): with mock.patch.object(keyword, "kwlist", new=["abcd"]): diff --git a/bpython/test/test_curtsies_coderunner.py b/bpython/test/test_curtsies_coderunner.py index ec0dbfe0b..3a677b561 100644 --- a/bpython/test/test_curtsies_coderunner.py +++ b/bpython/test/test_curtsies_coderunner.py @@ -46,7 +46,7 @@ def ctrlc(): class TestFakeOutput(unittest.TestCase): def assert_unicode(self, s): - self.assertIsInstance(s, type("")) + self.assertIsInstance(s, str) def test_bytes(self): out = FakeOutput(mock.Mock(), self.assert_unicode, None) diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py index de5fdb96c..824c166e0 100644 --- a/bpython/test/test_curtsies_repl.py +++ b/bpython/test/test_curtsies_repl.py @@ -405,7 +405,7 @@ def setUp(self): self.repl.pager = self.assert_pager_gets_unicode def assert_pager_gets_unicode(self, text): - self.assertIsInstance(text, type("")) + self.assertIsInstance(text, str) def test_help(self): self.repl.pager(self.repl.help_text()) From e66dc0ac766ad38bba41dea9a1612dcda2b2fb5e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 8 Nov 2020 22:04:17 +0100 Subject: [PATCH 031/555] Fix check of key code (fixes #859) --- bpython/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 4cedab8d8..218949aab 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -916,11 +916,11 @@ def p_key(self, key): # Redraw (as there might have been highlighted parens) self.print_line(self.s) - elif key in ("KEY_NPAGE"): # page_down + elif key in ("KEY_NPAGE",): # page_down self.hend() self.print_line(self.s) - elif key in ("KEY_PPAGE"): # page_up + elif key in ("KEY_PPAGE",): # page_up self.hbegin() self.print_line(self.s) From 23f681ed65e54bfe9aa494f4be9638081c696834 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 8 Nov 2020 22:04:17 +0100 Subject: [PATCH 032/555] Fix check of key code (fixes #859) --- bpython/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 68d7f8566..33563bf6b 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -939,11 +939,11 @@ def p_key(self, key): # Redraw (as there might have been highlighted parens) self.print_line(self.s) - elif key in ("KEY_NPAGE"): # page_down + elif key in ("KEY_NPAGE",): # page_down self.hend() self.print_line(self.s) - elif key in ("KEY_PPAGE"): # page_up + elif key in ("KEY_PPAGE",): # page_up self.hbegin() self.print_line(self.s) From 15580483e5fa587d31396bb516767d8c359bb025 Mon Sep 17 00:00:00 2001 From: supakeen Date: Tue, 10 Nov 2020 20:38:33 +0100 Subject: [PATCH 033/555] Prepare 0.20.1 release. --- CHANGELOG | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 3606bbadb..28805a1a6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,12 @@ Changelog ========= +0.20.1 +------ + +Fixes: +* Fix check of key code (fixes #859) + 0.20 ---- From fc9e6fc6ace58dc2b9aa7ad9e3d68a3038974330 Mon Sep 17 00:00:00 2001 From: niloct Date: Wed, 11 Nov 2020 12:06:49 -0300 Subject: [PATCH 034/555] Docs state wrong default config file directory Fixing correct default directory for bpython config file (especially useful for color customization). --- doc/sphinx/source/configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/source/configuration.rst b/doc/sphinx/source/configuration.rst index 6b07f8a6e..1f559e15f 100644 --- a/doc/sphinx/source/configuration.rst +++ b/doc/sphinx/source/configuration.rst @@ -4,7 +4,7 @@ Configuration ============= You can edit the config file by pressing F3 (default). If a config file does not exist you will asked if you would like to create a file. By default it will be -saved to ``$XDG_CONFIG_HOME/.config/bpython/config`` [#f1]_. +saved to ``$XDG_CONFIG_HOME/bpython/config`` [#f1]_. .. include:: configuration-options.rst From 4880684e34307d260760caccb521a46fe94f46e1 Mon Sep 17 00:00:00 2001 From: Kelsey Blair Date: Tue, 10 Nov 2020 15:39:09 -0800 Subject: [PATCH 035/555] upgrading curtsies version to include changes from PR 111 to fix typing bug --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 397da7e93..16b3574fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ Pygments -curtsies >=0.3.0 +curtsies >=0.3.2 greenlet requests setuptools From ff2acf1c02bd392cd292d22ae930e5f5f6348d3b Mon Sep 17 00:00:00 2001 From: Kelsey Blair Date: Tue, 10 Nov 2020 15:43:38 -0800 Subject: [PATCH 036/555] upgrade to 0.3.3 (latest) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 16b3574fd..5cb3eba12 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ Pygments -curtsies >=0.3.2 +curtsies >=0.3.3 greenlet requests setuptools From 16d84e4cadc67f7f3d81dcdb75dc2fde1905e37a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 13 Nov 2020 19:16:01 +0100 Subject: [PATCH 037/555] Also bump curtsies in install_requires and README --- README.rst | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index ad9000781..58e4d77b9 100644 --- a/README.rst +++ b/README.rst @@ -97,7 +97,7 @@ Dependencies ============ * Pygments * requests -* curtsies >= 0.3.0 +* curtsies >= 0.3.3 * greenlet * Sphinx >= 1.5 (optional, for the documentation) * babel (optional, for internationalization) diff --git a/setup.py b/setup.py index 4650fd663..c49251f9b 100755 --- a/setup.py +++ b/setup.py @@ -171,7 +171,7 @@ def git_describe_to_python_version(version): install_requires = [ "pygments", "requests", - "curtsies >=0.3.0", + "curtsies >=0.3.3", "greenlet", "wcwidth", ] From b6b66fe648384940afc5d8bcf37c2d9595aa5b34 Mon Sep 17 00:00:00 2001 From: supakeen Date: Sat, 14 Nov 2020 13:59:32 +0100 Subject: [PATCH 038/555] Add extension to CHANGELOG and AUTHORS. This makes it easier to find when there's a reference to a file. I've also updated the docs to not be a symlink to changelog but instead an explicit include. --- AUTHORS => AUTHORS.rst | 0 CHANGELOG => CHANGELOG.rst | 0 MANIFEST.in | 4 ++-- bpython/args.py | 2 +- doc/sphinx/source/authors.rst | 2 +- doc/sphinx/source/changelog.rst | 4 +++- 6 files changed, 7 insertions(+), 5 deletions(-) rename AUTHORS => AUTHORS.rst (100%) rename CHANGELOG => CHANGELOG.rst (100%) mode change 120000 => 100644 doc/sphinx/source/changelog.rst diff --git a/AUTHORS b/AUTHORS.rst similarity index 100% rename from AUTHORS rename to AUTHORS.rst diff --git a/CHANGELOG b/CHANGELOG.rst similarity index 100% rename from CHANGELOG rename to CHANGELOG.rst diff --git a/MANIFEST.in b/MANIFEST.in index badb93f1e..8694b9e27 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,6 @@ include .pycheckrc -include AUTHORS -include CHANGELOG +include AUTHORS.rst +include CHANGELOG.rst include LICENSE include data/bpython.png include data/org.bpython-interpreter.bpython.desktop diff --git a/bpython/args.py b/bpython/args.py index 915169c22..0636dad9f 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -55,7 +55,7 @@ def version_banner(base="bpython"): def copyright_banner(): - return "{} See AUTHORS for details.".format(__copyright__) + return "{} See AUTHORS.rst for details.".format(__copyright__) def parse(args, extras=None, ignore_stdin=False): diff --git a/doc/sphinx/source/authors.rst b/doc/sphinx/source/authors.rst index c83e6aefb..d475229e0 100644 --- a/doc/sphinx/source/authors.rst +++ b/doc/sphinx/source/authors.rst @@ -5,4 +5,4 @@ Authors If you contributed to bpython and want to be on this list please find us (:ref:`community`) and let us know! -.. include:: ../../../AUTHORS +.. include:: ../../../AUTHORS.rst diff --git a/doc/sphinx/source/changelog.rst b/doc/sphinx/source/changelog.rst deleted file mode 120000 index b6b15a7d0..000000000 --- a/doc/sphinx/source/changelog.rst +++ /dev/null @@ -1 +0,0 @@ -../../../CHANGELOG \ No newline at end of file diff --git a/doc/sphinx/source/changelog.rst b/doc/sphinx/source/changelog.rst new file mode 100644 index 000000000..29e651cab --- /dev/null +++ b/doc/sphinx/source/changelog.rst @@ -0,0 +1,3 @@ +.. _changelog: + +.. include:: ../../../CHANGELOG.rst From 165f3cc4d8413e396a11d33df226cd63e12a3746 Mon Sep 17 00:00:00 2001 From: supakeen Date: Sat, 14 Nov 2020 14:01:16 +0100 Subject: [PATCH 039/555] Move themes to sub directory. --- light.theme => theme/light.theme | 0 sample.theme => theme/sample.theme | 0 windows.theme => theme/windows.theme | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename light.theme => theme/light.theme (100%) rename sample.theme => theme/sample.theme (100%) rename windows.theme => theme/windows.theme (100%) diff --git a/light.theme b/theme/light.theme similarity index 100% rename from light.theme rename to theme/light.theme diff --git a/sample.theme b/theme/sample.theme similarity index 100% rename from sample.theme rename to theme/sample.theme diff --git a/windows.theme b/theme/windows.theme similarity index 100% rename from windows.theme rename to theme/windows.theme From d7c59621c27ab43f6b399e510cd7b79b13294326 Mon Sep 17 00:00:00 2001 From: supakeen Date: Sat, 14 Nov 2020 14:08:06 +0100 Subject: [PATCH 040/555] Refer to `bpython` not `pinnwand`. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 58e4d77b9..13f2a75cb 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,8 @@ .. image:: https://travis-ci.org/bpython/bpython.svg?branch=master :target: https://travis-ci.org/bpython/bpython -.. image:: https://readthedocs.org/projects/pinnwand/badge/?version=latest - :target: https://pinnwand.readthedocs.io/en/latest/ +.. image:: https://readthedocs.org/projects/bpython/badge/?version=latest + :target: https://docs.bpython-interpreter.org/en/latest/ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/ambv/black From 8d91453918236c57c5fc7dbeacff9b81295f6df9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 10 Dec 2020 12:06:24 +0100 Subject: [PATCH 041/555] Blacklist more folders for import completion (fixes #866) --- bpython/importcompletion.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 91972a6b5..a753932fb 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -42,7 +42,18 @@ paths = set() # Patterns to skip # TODO: This skiplist should be configurable. -skiplist = (".git", ".config", ".local", ".share", "node_modules") +skiplist = ( + # version tracking + ".git", ".svn", ".hg" + # XDG + ".config", ".local", ".share", + # nodejs + "node_modules", + # PlayOnLinux + "PlayOnLinux's virtual drives", + # wine + "dosdevices", +) fully_loaded = False From 0f313e4104b14b293159d6832055555551fdec98 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 11 Dec 2020 13:15:45 +0100 Subject: [PATCH 042/555] Drop slow attribute for tests --- bpython/test/test_args.py | 11 ----------- bpython/test/test_crashers.py | 12 ------------ 2 files changed, 23 deletions(-) diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py index 3e8c5e9db..78242a32e 100644 --- a/bpython/test/test_args.py +++ b/bpython/test/test_args.py @@ -7,18 +7,7 @@ from bpython import args from bpython.test import FixLanguageTestCase as TestCase, unittest -try: - from nose.plugins.attrib import attr -except ImportError: - def attr(*args, **kwargs): - def identity(func): - return func - - return identity - - -@attr(speed="slow") class TestExecArgs(unittest.TestCase): def test_exec_dunder_file(self): with tempfile.NamedTemporaryFile(mode="w") as f: diff --git a/bpython/test/test_crashers.py b/bpython/test/test_crashers.py index 39fce35cb..7a92f4f87 100644 --- a/bpython/test/test_crashers.py +++ b/bpython/test/test_crashers.py @@ -28,16 +28,6 @@ class TrialTestCase: except ImportError: have_urwid = False -try: - from nose.plugins.attrib import attr -except ImportError: - - def attr(*args, **kwargs): - def identity(func): - return func - - return identity - def set_win_size(fd, rows, columns): s = struct.pack("HHHH", rows, columns, 0, 0) @@ -109,7 +99,6 @@ def processExited(self, reason): ) return result - @attr(speed="slow") def test_issue108(self): input = textwrap.dedent( """\ @@ -121,7 +110,6 @@ def spam(): deferred = self.run_bpython(input) return deferred.addCallback(self.check_no_traceback) - @attr(speed="slow") def test_issue133(self): input = textwrap.dedent( """\ From 68bba09672a48d64eeec318f8d084112d10d1b12 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 14 Dec 2020 16:56:12 +0100 Subject: [PATCH 043/555] Replace travis with Github Actions --- .github/workflows/build.yaml | 44 ++++++++++++++++++++++++++++++++++++ .travis.install.sh | 23 ------------------- .travis.script.sh | 11 --------- .travis.yml | 26 --------------------- 4 files changed, 44 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/build.yaml delete mode 100755 .travis.install.sh delete mode 100755 .travis.script.sh delete mode 100644 .travis.yml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 000000000..ee822e154 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + pull_request: + schedule: + # run at 7:00 on the first of every month + - cron: '0 7 1 * *' + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-cov urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" + - 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 + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + env: + PYTHON_VERSION: ${{ matrix.python-version }} + with: + file: ./coverage.xml + env_vars: PYTHON_VERSION + if: ${{ always() }} diff --git a/.travis.install.sh b/.travis.install.sh deleted file mode 100755 index 40c0a67bb..000000000 --- a/.travis.install.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -e -set -x - -pip install setuptools - -if [[ $RUN == nosetests ]]; then - # core dependencies - pip install -r requirements.txt - # urwid specific dependencies - pip install urwid twisted - # filewatch specific dependencies - pip install watchdog - # jedi specific dependencies - pip install 'jedi >= 0.16' - # translation specific dependencies - pip install babel - # build and install - python setup.py install -elif [[ $RUN == build_sphinx ]]; then - # documentation specific dependencies - pip install 'sphinx >=1.5' -fi diff --git a/.travis.script.sh b/.travis.script.sh deleted file mode 100755 index f8619bb48..000000000 --- a/.travis.script.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -set -e -set -x - -if [[ $RUN == build_sphinx ]]; then - python setup.py build_sphinx - python setup.py build_sphinx_man -elif [[ $RUN == nosetests ]]; then - cd build/lib/ - nosetests -v bpython/test -fi diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 04b7750f3..000000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -language: python -sudo: false -dist: bionic - -python: - - "3.9-dev" - - "3.8" - - "3.7" - - "3.6" - - "nightly" - - "pypy3" - -env: - - RUN=nosetests - - RUN=build_sphinx - -matrix: - allow_failures: - - python: "nightly" - - python: "pypy3" - -install: - - ./.travis.install.sh - -script: - - ./.travis.script.sh From 1d930fd280b5ff4586edcae2787ef9ee691fa374 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 14 Dec 2020 17:10:49 +0100 Subject: [PATCH 044/555] Skip tests known to fail with pytest --- bpython/test/test_args.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py index 78242a32e..e71a9ddfd 100644 --- a/bpython/test/test_args.py +++ b/bpython/test/test_args.py @@ -9,6 +9,7 @@ class TestExecArgs(unittest.TestCase): + @unittest.skip("test broken under pytest") def test_exec_dunder_file(self): with tempfile.NamedTemporaryFile(mode="w") as f: f.write( @@ -48,6 +49,7 @@ def test_exec_nonascii_file(self): except subprocess.CalledProcessError: self.fail("Error running module with nonascii characters") + @unittest.skip("test broken under pytest") def test_exec_nonascii_file_linenums(self): with tempfile.NamedTemporaryFile(mode="w") as f: f.write( From 4d0190b5233209706f6982790c762ae6e57ea7cb Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 14 Dec 2020 17:12:17 +0100 Subject: [PATCH 045/555] Document testing with pytest --- doc/sphinx/source/contributing.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 06e656d49..ade107476 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -47,7 +47,7 @@ Next install your development copy of bpython and its dependencies: # install optional dependencies $ pip install watchdog urwid # development dependencies - $ pip install sphinx nose + $ pip install sphinx pytest # this runs your modified copy of bpython! $ bpython @@ -62,7 +62,7 @@ Next install your development copy of bpython and its dependencies: $ sudp apt install python3-greenlet python3-pygments python3-requests $ sudo apt install python3-watchdog python3-urwid - $ sudo apt install python3-sphinx python3-nose + $ sudo apt install python3-sphinx python3-pytest You also need to run `virtualenv` with `--system-site-packages` packages, if you want to use the packages provided by your distribution. @@ -83,14 +83,8 @@ To run tests from the bpython directory: .. code-block:: bash - $ nosetests + $ pytest -If you want to skip test cases that are known to be slow, run `nosetests` in -the following way: - -.. code-block:: bash - - $ nosetests -A "speed != 'slow'" Building the documentation -------------------------- From 1846449e22e69948a3abe2ca6f5afa69c5acc375 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 14 Dec 2020 18:01:47 +0100 Subject: [PATCH 046/555] Remove travis badge --- README.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.rst b/README.rst index 13f2a75cb..da947d55a 100644 --- a/README.rst +++ b/README.rst @@ -1,9 +1,6 @@ .. image:: https://img.shields.io/pypi/v/bpython :target: https://pypi.org/project/bpython -.. image:: https://travis-ci.org/bpython/bpython.svg?branch=master - :target: https://travis-ci.org/bpython/bpython - .. image:: https://readthedocs.org/projects/bpython/badge/?version=latest :target: https://docs.bpython-interpreter.org/en/latest/ From 3e04ddaedb22ded1070ae716a1a414564b93f341 Mon Sep 17 00:00:00 2001 From: Yu Fan Date: Thu, 17 Dec 2020 19:28:44 -0800 Subject: [PATCH 047/555] Add installation method using system package managers --- README.rst | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/README.rst b/README.rst index da947d55a..d36342729 100644 --- a/README.rst +++ b/README.rst @@ -42,6 +42,10 @@ You can find more about bpython - including `full documentation`_ - at our ========================== Installation & Basic Usage ========================== + +Installation using Pip +---------------------- + If you have `pip`_ installed, you can simply run: .. code-block:: bash @@ -52,6 +56,64 @@ Start bpython by typing ``bpython`` in your terminal. You can exit bpython by using the ``exit()`` command or by pressing control-D like regular interactive Python. +Installation via OS Package Manager +----------------------------------- + +The majority of operating system of desktop computers comes with a package +manager system, if you are any user of them, you can install ``bpython`` +using the package manager. + +Ubuntu/Debian +~~~~~~~~~~~~~ +Ubuntu/Debian family Linux users and install bpython using the apt package manager, using the +command with sudo priviledge: + +.. code-block:: bash + + $ apt install bpython + +In case you are using an older version, run + +.. code-block:: bash + + $ apt-get install bpython + +Arch Linux +~~~~~~~~~ +Arch linux uses pacman as the default package manager, and you can use it to install bpython: + +.. code-block:: bash + + $ pacman -S bpython + + +Windows +~~~~~~~ +**Caveats:** As ``bpython`` makes use of the ncurses library of \*nix-family operating systems, +bpython on Windows is not officially supported and tested. + +However, you can still use bpython on Windows using a somewhat work around. Briefly, you should install +these two packages using pip: + +.. code-block:: bash + + $ pip install bpython windows-curses + +Then you should invoke a program called ``bpython-curses.exe`` instead of ``bpython.exe`` to use bpython: + +.. code-block:: bash + + $ bpython-curses + +Mac OS +~~~~~~ +Like Windows, Mac OS does not include a package manager by default. If you have installed any +third-party pacakge manager like MacPorts, you can install it via + +.. code-block:: bash + + $ sudo port install py-bpython + =================== Features & Examples =================== From 9d05fc52f423320daad204e0ccf9229fc05e671c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 18 Dec 2020 12:51:02 +0100 Subject: [PATCH 048/555] Add missing tilde --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d36342729..2bbb777e7 100644 --- a/README.rst +++ b/README.rst @@ -79,7 +79,7 @@ In case you are using an older version, run $ apt-get install bpython Arch Linux -~~~~~~~~~ +~~~~~~~~~~ Arch linux uses pacman as the default package manager, and you can use it to install bpython: .. code-block:: bash From eef933f1a2422f507eaa675027a0a6283f8fcfe4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 30 Dec 2020 18:22:41 +0100 Subject: [PATCH 049/555] Update list of magic methods --- bpython/autocomplete.py | 65 +++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 9b4f72dd2..a93d123ba 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -48,35 +48,51 @@ ALL_MODES = (SIMPLE, SUBSTRING, FUZZY) MAGIC_METHODS = tuple( - "__%s__" % s + f"__{s}__" for s in ( + "new", "init", + "del", "repr", "str", + "bytes", + "format", "lt", "le", "eq", "ne", "gt", "ge", - "cmp", "hash", - "nonzero", - "unicode", + "bool", "getattr", + "getattribute", "setattr", + "delattr", + "dir", "get", "set", + "delete", + "set_name", + "init_subclass", + "instancecheck", + "subclasscheck", + "class_getitem", "call", "len", + "length_hint", "getitem", "setitem", + "delitem", + "missing", "iter", "reversed", "contains", "add", "sub", "mul", + "matmul", + "truediv", "floordiv", "mod", "divmod", @@ -86,8 +102,33 @@ "and", "xor", "or", - "div", - "truediv", + "radd", + "rsub", + "rmul", + "rmatmul", + "rtruediv", + "rfloordiv", + "rmod", + "rdivmod", + "rpow", + "rlshift", + "rrshift", + "rand", + "rxor", + "ror", + "iadd", + "isub", + "imul", + "imatmul", + "itruediv", + "ifloordiv", + "imod", + "ipow", + "ilshift", + "irshift", + "iand", + "ixor", + "ixor", "neg", "pos", "abs", @@ -95,12 +136,18 @@ "complex", "int", "float", - "oct", - "hex", "index", - "coerce", + "round", + "trunc", + "floor", + "ceil", "enter", "exit", + "await", + "aiter", + "anext", + "aenter", + "aexit", ) ) From fab1d4efd6d1449adee55f060fda7379915d5319 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 30 Dec 2020 18:33:24 +0100 Subject: [PATCH 050/555] Turn auto completion modes into an Enum --- bpython/autocomplete.py | 29 ++++++++++------- bpython/config.py | 10 ++++-- bpython/curtsiesfrontend/repl.py | 2 +- bpython/test/test_repl.py | 56 ++++++++++++++++++++++++-------- 4 files changed, 68 insertions(+), 29 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index a93d123ba..5a273424c 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -32,6 +32,7 @@ import rlcompleter import builtins +from enum import Enum from . import inspection from . import importcompletion from . import line as lineparts @@ -41,11 +42,17 @@ # Autocomplete modes -SIMPLE = "simple" -SUBSTRING = "substring" -FUZZY = "fuzzy" +class AutocompleteModes(Enum): + SIMPLE = "simple" + SUBSTRING = "substring" + FUZZY = "fuzzy" + + @classmethod + def from_string(cls, value): + if value in cls.__members__: + return cls.__members__[value] + return None -ALL_MODES = (SIMPLE, SUBSTRING, FUZZY) MAGIC_METHODS = tuple( f"__{s}__" @@ -189,16 +196,16 @@ def method_match_fuzzy(word, size, text): MODES_MAP = { - SIMPLE: method_match_simple, - SUBSTRING: method_match_substring, - FUZZY: method_match_fuzzy, + AutocompleteModes.SIMPLE: method_match_simple, + AutocompleteModes.SUBSTRING: method_match_substring, + AutocompleteModes.FUZZY: method_match_fuzzy, } class BaseCompletionType: """Describes different completion types""" - def __init__(self, shown_before_tab=True, mode=SIMPLE): + def __init__(self, shown_before_tab=True, mode=AutocompleteModes.SIMPLE): self._shown_before_tab = shown_before_tab self.method_match = MODES_MAP[mode] @@ -248,7 +255,7 @@ def shown_before_tab(self): class CumulativeCompleter(BaseCompletionType): """Returns combined matches from several completers""" - def __init__(self, completers, mode=SIMPLE): + def __init__(self, completers, mode=AutocompleteModes.SIMPLE): if not completers: raise ValueError( "CumulativeCompleter requires at least one completer" @@ -289,7 +296,7 @@ def format(self, word): class FilenameCompletion(BaseCompletionType): - def __init__(self, mode=SIMPLE): + def __init__(self, mode=AutocompleteModes.SIMPLE): super().__init__(False, mode) def safe_glob(self, pathname): @@ -649,7 +656,7 @@ def get_completer(completers, cursor_offset, line, **kwargs): return [], None -def get_default_completer(mode=SIMPLE): +def get_default_completer(mode=AutocompleteModes.SIMPLE): return ( DictKeyCompletion(mode=mode), ImportCompletion(mode=mode), diff --git a/bpython/config.py b/bpython/config.py index 643aca76a..59b12c44e 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -4,7 +4,9 @@ from itertools import chain from configparser import ConfigParser -from .autocomplete import SIMPLE as default_completion, ALL_MODES +from .autocomplete import AutocompleteModes + +default_completion = AutocompleteModes.SIMPLE class Struct: @@ -234,7 +236,9 @@ def get_key_no_doublebind(command): struct.complete_magic_methods = config.getboolean( "general", "complete_magic_methods" ) - struct.autocomplete_mode = config.get("general", "autocomplete_mode") + struct.autocomplete_mode = AutocompleteModes.from_string( + config.get("general", "autocomplete_mode") + ) struct.save_append_py = config.getboolean("general", "save_append_py") struct.curtsies_list_above = config.getboolean("curtsies", "list_above") @@ -282,7 +286,7 @@ def get_key_no_doublebind(command): struct.hist_file = os.path.expanduser(struct.hist_file) # verify completion mode - if struct.autocomplete_mode not in ALL_MODES: + if struct.autocomplete_mode is None: struct.autocomplete_mode = default_completion # set box drawing characters diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 32ddd8362..9af2c4bbc 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -347,7 +347,7 @@ def __init__( else: banner = None # only one implemented currently - config.autocomplete_mode = autocomplete.SIMPLE + config.autocomplete_mode = autocomplete.AutocompleteModes.SIMPLE if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1: config.cli_suggestion_width = 1 diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 868489061..7182416e1 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -364,7 +364,9 @@ def test_push(self): # COMPLETE TESTS # 1. Global tests def test_simple_global_complete(self): - self.repl = FakeRepl({"autocomplete_mode": autocomplete.SIMPLE}) + self.repl = FakeRepl( + {"autocomplete_mode": autocomplete.AutocompleteModes.SIMPLE} + ) self.set_input_line("d") self.assertTrue(self.repl.complete()) @@ -375,7 +377,9 @@ def test_simple_global_complete(self): ) def test_substring_global_complete(self): - self.repl = FakeRepl({"autocomplete_mode": autocomplete.SUBSTRING}) + self.repl = FakeRepl( + {"autocomplete_mode": autocomplete.AutocompleteModes.SUBSTRING} + ) self.set_input_line("time") self.assertTrue(self.repl.complete()) @@ -385,7 +389,9 @@ def test_substring_global_complete(self): ) def test_fuzzy_global_complete(self): - self.repl = FakeRepl({"autocomplete_mode": autocomplete.FUZZY}) + self.repl = FakeRepl( + {"autocomplete_mode": autocomplete.AutocompleteModes.FUZZY} + ) self.set_input_line("doc") self.assertTrue(self.repl.complete()) @@ -397,7 +403,9 @@ def test_fuzzy_global_complete(self): # 2. Attribute tests def test_simple_attribute_complete(self): - self.repl = FakeRepl({"autocomplete_mode": autocomplete.SIMPLE}) + self.repl = FakeRepl( + {"autocomplete_mode": autocomplete.AutocompleteModes.SIMPLE} + ) self.set_input_line("Foo.b") code = "class Foo():\n\tdef bar(self):\n\t\tpass\n" @@ -409,7 +417,9 @@ def test_simple_attribute_complete(self): self.assertEqual(self.repl.matches_iter.matches, ["Foo.bar"]) def test_substring_attribute_complete(self): - self.repl = FakeRepl({"autocomplete_mode": autocomplete.SUBSTRING}) + self.repl = FakeRepl( + {"autocomplete_mode": autocomplete.AutocompleteModes.SUBSTRING} + ) self.set_input_line("Foo.az") code = "class Foo():\n\tdef baz(self):\n\t\tpass\n" @@ -421,7 +431,9 @@ def test_substring_attribute_complete(self): self.assertEqual(self.repl.matches_iter.matches, ["Foo.baz"]) def test_fuzzy_attribute_complete(self): - self.repl = FakeRepl({"autocomplete_mode": autocomplete.FUZZY}) + self.repl = FakeRepl( + {"autocomplete_mode": autocomplete.AutocompleteModes.FUZZY} + ) self.set_input_line("Foo.br") code = "class Foo():\n\tdef bar(self):\n\t\tpass\n" @@ -434,7 +446,9 @@ def test_fuzzy_attribute_complete(self): # 3. Edge cases def test_updating_namespace_complete(self): - self.repl = FakeRepl({"autocomplete_mode": autocomplete.SIMPLE}) + self.repl = FakeRepl( + {"autocomplete_mode": autocomplete.AutocompleteModes.SIMPLE} + ) self.set_input_line("foo") self.repl.push("foobar = 2") @@ -443,7 +457,9 @@ def test_updating_namespace_complete(self): self.assertEqual(self.repl.matches_iter.matches, ["foobar"]) def test_file_should_not_appear_in_complete(self): - self.repl = FakeRepl({"autocomplete_mode": autocomplete.SIMPLE}) + self.repl = FakeRepl( + {"autocomplete_mode": autocomplete.AutocompleteModes.SIMPLE} + ) self.set_input_line("_") self.assertTrue(self.repl.complete()) self.assertTrue(hasattr(self.repl.matches_iter, "matches")) @@ -451,7 +467,9 @@ def test_file_should_not_appear_in_complete(self): # 4. Parameter names def test_paremeter_name_completion(self): - self.repl = FakeRepl({"autocomplete_mode": autocomplete.SIMPLE}) + self.repl = FakeRepl( + {"autocomplete_mode": autocomplete.AutocompleteModes.SIMPLE} + ) self.set_input_line("foo(ab") code = "def foo(abc=1, abd=2, xyz=3):\n\tpass\n" @@ -515,7 +533,9 @@ def test_simple_tab_complete(self): @unittest.skip("disabled while non-simple completion is disabled") def test_substring_tab_complete(self): self.repl.s = "bar" - self.repl.config.autocomplete_mode = autocomplete.FUZZY + self.repl.config.autocomplete_mode = ( + autocomplete.AutocompleteModes.FUZZY + ) self.repl.tab() self.assertEqual(self.repl.s, "foobar") self.repl.tab() @@ -524,7 +544,9 @@ def test_substring_tab_complete(self): @unittest.skip("disabled while non-simple completion is disabled") def test_fuzzy_tab_complete(self): self.repl.s = "br" - self.repl.config.autocomplete_mode = autocomplete.FUZZY + self.repl.config.autocomplete_mode = ( + autocomplete.AutocompleteModes.FUZZY + ) self.repl.tab() self.assertEqual(self.repl.s, "foobar") @@ -561,7 +583,9 @@ def test_back_parameter(self): def test_fuzzy_attribute_tab_complete(self): """Test fuzzy attribute with no text""" self.repl.s = "Foo." - self.repl.config.autocomplete_mode = autocomplete.FUZZY + self.repl.config.autocomplete_mode = ( + autocomplete.AutocompleteModes.FUZZY + ) self.repl.tab() self.assertEqual(self.repl.s, "Foo.foobar") @@ -570,7 +594,9 @@ def test_fuzzy_attribute_tab_complete(self): def test_fuzzy_attribute_tab_complete2(self): """Test fuzzy attribute with some text""" self.repl.s = "Foo.br" - self.repl.config.autocomplete_mode = autocomplete.FUZZY + self.repl.config.autocomplete_mode = ( + autocomplete.AutocompleteModes.FUZZY + ) self.repl.tab() self.assertEqual(self.repl.s, "Foo.foobar") @@ -588,7 +614,9 @@ def test_simple_expand(self): @unittest.skip("disabled while non-simple completion is disabled") def test_substring_expand_forward(self): - self.repl.config.autocomplete_mode = autocomplete.SUBSTRING + self.repl.config.autocomplete_mode = ( + autocomplete.AutocompleteModes.SUBSTRING + ) self.repl.s = "ba" self.repl.tab() self.assertEqual(self.repl.s, "bar") From 79e60136e13392c5f8f750ec5e7cc6a182da0a4a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 30 Dec 2020 18:49:14 +0100 Subject: [PATCH 051/555] Use find_all_modules and reload instead of find_iterator --- bpython/curtsies.py | 4 ++-- bpython/test/test_importcompletion.py | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 98c064bd2..3e94a71f5 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -13,7 +13,7 @@ from . import args as bpargs from . import translations from .translations import _ -from .importcompletion import find_iterator +from .importcompletion import find_all_modules from .curtsiesfrontend import events as bpythonevents from . import inspection from .repl import extract_exit_value @@ -113,7 +113,7 @@ def mainloop(self, interactive=True, paste=None): # do a display before waiting for first event self.process_event_and_paint(None) inputs = combined_events(self.input_generator) - for unused in find_iterator: + for unused in find_all_modules(): e = inputs.send(0) if e is not None: self.process_event_and_paint(e) diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index af710b2ef..e585b8a4c 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -31,16 +31,10 @@ def test_package_completion(self): class TestRealComplete(unittest.TestCase): @classmethod def setUpClass(cls): - for _ in importcompletion.find_iterator: - pass + importcompletion.reload() __import__("sys") __import__("os") - @classmethod - def tearDownClass(cls): - importcompletion.find_iterator = importcompletion.find_all_modules() - importcompletion.modules = set() - def test_from_attribute(self): self.assertSetEqual( importcompletion.complete(19, "from sys import arg"), {"argv"} From cea4db1c915484d5aa793aa3b06bf197d88384f0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 30 Dec 2020 19:00:33 +0100 Subject: [PATCH 052/555] Use find_coroutine instead of find_all_modules --- bpython/curtsies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 3e94a71f5..06ae85f94 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -13,7 +13,7 @@ from . import args as bpargs from . import translations from .translations import _ -from .importcompletion import find_all_modules +from .importcompletion import find_coroutine from .curtsiesfrontend import events as bpythonevents from . import inspection from .repl import extract_exit_value @@ -113,7 +113,7 @@ def mainloop(self, interactive=True, paste=None): # do a display before waiting for first event self.process_event_and_paint(None) inputs = combined_events(self.input_generator) - for unused in find_all_modules(): + while find_coroutine(): e = inputs.send(0) if e is not None: self.process_event_and_paint(e) From 400d7e2203a13eb8f4b118c4d8c32dada8f6e077 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 30 Dec 2020 19:13:56 +0100 Subject: [PATCH 053/555] Remove unused get_completer_bpython --- bpython/autocomplete.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 5a273424c..24a207369 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -672,11 +672,6 @@ def get_default_completer(mode=AutocompleteModes.SIMPLE): ) -def get_completer_bpython(cursor_offset, line, **kwargs): - """""" - return get_completer(get_default_completer(), cursor_offset, line, **kwargs) - - def _callable_postfix(value, word): """rlcompleter's _callable_postfix done right.""" if callable(value): From ec79bd833d4cfa24bdb8f0c506e8035784e395bb Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 30 Dec 2020 21:46:54 +0100 Subject: [PATCH 054/555] Remove global state of bpython.importcompletion Repls now have a ModuleGartherer instance that performs the job. --- bpython/autocomplete.py | 12 +- bpython/cli.py | 5 +- bpython/curtsies.py | 3 +- bpython/importcompletion.py | 378 +++++++++++------------ bpython/repl.py | 4 +- bpython/simplerepl.py | 9 +- bpython/test/test_import_not_cyclical.py | 10 +- bpython/test/test_importcompletion.py | 26 +- bpython/urwid.py | 3 +- 9 files changed, 225 insertions(+), 225 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 24a207369..6b6698b13 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -1,6 +1,7 @@ # The MIT License # # Copyright (c) 2009-2015 the bpython authors. +# Copyright (c) 2015-2020 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 @@ -34,7 +35,6 @@ from enum import Enum from . import inspection -from . import importcompletion from . import line as lineparts from .line import LinePart from .lazyre import LazyReCompile @@ -285,8 +285,12 @@ def matches(self, cursor_offset, line, **kwargs): class ImportCompletion(BaseCompletionType): + def __init__(self, module_gatherer, mode=AutocompleteModes.SIMPLE): + super().__init__(False, mode) + self.module_gatherer = module_gatherer + def matches(self, cursor_offset, line, **kwargs): - return importcompletion.complete(cursor_offset, line) + return self.module_gatherer.complete(cursor_offset, line) def locate(self, current_offset, line): return lineparts.current_word(current_offset, line) @@ -656,10 +660,10 @@ def get_completer(completers, cursor_offset, line, **kwargs): return [], None -def get_default_completer(mode=AutocompleteModes.SIMPLE): +def get_default_completer(mode=AutocompleteModes.SIMPLE, module_gatherer=None): return ( DictKeyCompletion(mode=mode), - ImportCompletion(mode=mode), + ImportCompletion(module_gatherer, mode=mode), FilenameCompletion(mode=mode), MagicMethodCompletion(mode=mode), MultilineJediCompletion(mode=mode), diff --git a/bpython/cli.py b/bpython/cli.py index 218949aab..43180bb0b 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -66,9 +66,6 @@ from pygments.token import Token from .formatter import BPythonFormatter -# This for completion -from . import importcompletion - # This for config from .config import Struct, getpreferredencoding @@ -1784,7 +1781,7 @@ def idle(caller): sure it happens conveniently.""" global DO_RESIZE - if importcompletion.find_coroutine() or caller.paste_mode: + if caller.module_gatherer.find_coroutine() or caller.paste_mode: caller.scr.nodelay(True) key = caller.scr.getch() caller.scr.nodelay(False) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 06ae85f94..0c0b44789 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -13,7 +13,6 @@ from . import args as bpargs from . import translations from .translations import _ -from .importcompletion import find_coroutine from .curtsiesfrontend import events as bpythonevents from . import inspection from .repl import extract_exit_value @@ -113,7 +112,7 @@ def mainloop(self, interactive=True, paste=None): # do a display before waiting for first event self.process_event_and_paint(None) inputs = combined_events(self.input_generator) - while find_coroutine(): + while self.module_gatherer.find_coroutine(): e = inputs.send(0) if e is not None: self.process_event_and_paint(e) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index a753932fb..920e4234a 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -1,6 +1,7 @@ # The MIT License # # Copyright (c) 2009-2011 Andreas Stuehrk +# Copyright (c) 2020 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 @@ -21,10 +22,10 @@ # THE SOFTWARE. import fnmatch +import importlib.machinery import os import sys import warnings -import importlib.machinery from .line import ( current_word, @@ -35,204 +36,201 @@ SUFFIXES = importlib.machinery.all_suffixes() -# The cached list of all known modules -modules = set() -# List of stored paths to compare against so that real paths are not repeated -# handles symlinks not mount points -paths = set() -# Patterns to skip -# TODO: This skiplist should be configurable. -skiplist = ( - # version tracking - ".git", ".svn", ".hg" - # XDG - ".config", ".local", ".share", - # nodejs - "node_modules", - # PlayOnLinux - "PlayOnLinux's virtual drives", - # wine - "dosdevices", -) -fully_loaded = False - - -def module_matches(cw, prefix=""): - """Modules names to replace cw with""" - full = f"{prefix}.{cw}" if prefix else cw - matches = ( - name - for name in modules - if (name.startswith(full) and name.find(".", len(full)) == -1) - ) - if prefix: - return {match[len(prefix) + 1 :] for match in matches} - else: - return set(matches) - - -def attr_matches(cw, prefix="", only_modules=False): - """Attributes to replace name with""" - full = f"{prefix}.{cw}" if prefix else cw - module_name, _, name_after_dot = full.rpartition(".") - if module_name not in sys.modules: - return set() - module = sys.modules[module_name] - if only_modules: - matches = { + +class ModuleGatherer: + def __init__(self, path=None): + # The cached list of all known modules + self.modules = set() + # List of stored paths to compare against so that real paths are not repeated + # handles symlinks not mount points + self.paths = set() + # Patterns to skip + # TODO: This skiplist should be configurable. + self.skiplist = ( + # version tracking + ".git", + ".svn", + ".hg" + # XDG + ".config", + ".local", + ".share", + # nodejs + "node_modules", + # PlayOnLinux + "PlayOnLinux's virtual drives", + # wine + "dosdevices", + # cache + "__pycache__", + ) + self.fully_loaded = False + self.find_iterator = self.find_all_modules(path) + + def module_matches(self, cw, prefix=""): + """Modules names to replace cw with""" + + full = f"{prefix}.{cw}" if prefix else cw + matches = ( name - for name in dir(module) - if name.startswith(name_after_dot) - and f"{module_name}.{name}" in sys.modules - } - else: - matches = { - name for name in dir(module) if name.startswith(name_after_dot) - } - module_part, _, _ = cw.rpartition(".") - if module_part: - matches = {f"{module_part}.{m}" for m in matches} - - return matches - - -def module_attr_matches(name): - """Only attributes which are modules to replace name with""" - return attr_matches(name, prefix="", only_modules=True) - - -def complete(cursor_offset, line): - """Construct a full list of possibly completions for imports.""" - tokens = line.split() - if "from" not in tokens and "import" not in tokens: - return None - - result = current_word(cursor_offset, line) - if result is None: - return None - - from_import_from = current_from_import_from(cursor_offset, line) - if from_import_from is not None: - import_import = current_from_import_import(cursor_offset, line) - if import_import is not None: - # `from a import ` completion - matches = module_matches(import_import[2], from_import_from[2]) - matches.update(attr_matches(import_import[2], from_import_from[2])) + for name in self.modules + if (name.startswith(full) and name.find(".", len(full)) == -1) + ) + if prefix: + return {match[len(prefix) + 1 :] for match in matches} else: - # `from ` completion - matches = module_attr_matches(from_import_from[2]) - matches.update(module_matches(from_import_from[2])) - return matches + return set(matches) + + def attr_matches(self, cw, prefix="", only_modules=False): + """Attributes to replace name with""" + full = f"{prefix}.{cw}" if prefix else cw + module_name, _, name_after_dot = full.rpartition(".") + if module_name not in sys.modules: + return set() + module = sys.modules[module_name] + if only_modules: + matches = { + name + for name in dir(module) + if name.startswith(name_after_dot) + and f"{module_name}.{name}" in sys.modules + } + else: + matches = { + name for name in dir(module) if name.startswith(name_after_dot) + } + module_part, _, _ = cw.rpartition(".") + if module_part: + matches = {f"{module_part}.{m}" for m in matches} - cur_import = current_import(cursor_offset, line) - if cur_import is not None: - # `import ` completion - matches = module_matches(cur_import[2]) - matches.update(module_attr_matches(cur_import[2])) return matches - else: - return None - -def find_modules(path): - """Find all modules (and packages) for a given directory.""" - if not os.path.isdir(path): - # Perhaps a zip file - return - basepath = os.path.basename(path) - if any(fnmatch.fnmatch(basepath, entry) for entry in skiplist): - # Path is on skiplist - return + def module_attr_matches(self, name): + """Only attributes which are modules to replace name with""" + return self.attr_matches(name, prefix="", only_modules=True) + + def complete(self, cursor_offset, line): + """Construct a full list of possibly completions for imports.""" + tokens = line.split() + if "from" not in tokens and "import" not in tokens: + return None + + result = current_word(cursor_offset, line) + if result is None: + return None + + from_import_from = current_from_import_from(cursor_offset, line) + if from_import_from is not None: + import_import = current_from_import_import(cursor_offset, line) + if import_import is not None: + # `from a import ` completion + matches = self.module_matches( + import_import[2], from_import_from[2] + ) + matches.update( + self.attr_matches(import_import[2], from_import_from[2]) + ) + else: + # `from ` completion + matches = self.module_attr_matches(from_import_from[2]) + matches.update(self.module_matches(from_import_from[2])) + return matches + + cur_import = current_import(cursor_offset, line) + if cur_import is not None: + # `import ` completion + matches = self.module_matches(cur_import[2]) + matches.update(self.module_attr_matches(cur_import[2])) + return matches + else: + return None + + def find_modules(self, path): + """Find all modules (and packages) for a given directory.""" + if not os.path.isdir(path): + # Perhaps a zip file + return + basepath = os.path.basename(path) + if any(fnmatch.fnmatch(basepath, entry) for entry in self.skiplist): + # Path is on skiplist + return - try: - filenames = os.listdir(path) - except OSError: - filenames = [] + try: + filenames = os.listdir(path) + except OSError: + filenames = [] - finder = importlib.machinery.FileFinder(path) + finder = importlib.machinery.FileFinder(path) - for name in filenames: - if any(fnmatch.fnmatch(name, entry) for entry in skiplist): - # Path is on skiplist - continue - elif not any(name.endswith(suffix) for suffix in SUFFIXES): - # Possibly a package - if "." in name: + for name in filenames: + if any(fnmatch.fnmatch(name, entry) for entry in self.skiplist): + # Path is on skiplist continue - elif os.path.isdir(os.path.join(path, name)): - # Unfortunately, CPython just crashes if there is a directory - # which ends with a python extension, so work around. - continue - for suffix in SUFFIXES: - if name.endswith(suffix): - name = name[: -len(suffix)] - break - if name == "badsyntax_pep3120": - # Workaround for issue #166 - continue - try: - is_package = False - with warnings.catch_warnings(): - warnings.simplefilter("ignore", ImportWarning) - spec = finder.find_spec(name) - if spec is None: + elif not any(name.endswith(suffix) for suffix in SUFFIXES): + # Possibly a package + if "." in name: continue - if spec.submodule_search_locations is not None: - pathname = spec.submodule_search_locations[0] - is_package = True - else: - pathname = spec.origin - except (ImportError, OSError, SyntaxError): - continue - except UnicodeEncodeError: - # Happens with Python 3 when there is a filename in some - # invalid encoding - continue - else: - if is_package: - path_real = os.path.realpath(pathname) - if path_real not in paths: - paths.add(path_real) - for subname in find_modules(pathname): - if subname != "__init__": - yield f"{name}.{subname}" - yield name - - -def find_all_modules(path=None): - """Return a list with all modules in `path`, which should be a list of - directory names. If path is not given, sys.path will be used.""" - if path is None: - modules.update(sys.builtin_module_names) - path = sys.path - - for p in path: - if not p: - p = os.curdir - for module in find_modules(p): - modules.add(module) - yield - - -def find_coroutine(): - global fully_loaded - - if fully_loaded: - return None - - try: - next(find_iterator) - except StopIteration: - fully_loaded = True - - return True - - -def reload(): - """Refresh the list of known modules.""" - modules.clear() - for _ in find_all_modules(): - pass + elif os.path.isdir(os.path.join(path, name)): + # Unfortunately, CPython just crashes if there is a directory + # which ends with a python extension, so work around. + continue + for suffix in SUFFIXES: + if name.endswith(suffix): + name = name[: -len(suffix)] + break + if name == "badsyntax_pep3120": + # Workaround for issue #166 + continue + try: + is_package = False + with warnings.catch_warnings(): + warnings.simplefilter("ignore", ImportWarning) + spec = finder.find_spec(name) + if spec is None: + continue + if spec.submodule_search_locations is not None: + pathname = spec.submodule_search_locations[0] + is_package = True + else: + pathname = spec.origin + except (ImportError, OSError, SyntaxError): + continue + except UnicodeEncodeError: + # Happens with Python 3 when there is a filename in some + # invalid encoding + continue + else: + if is_package: + path_real = os.path.realpath(pathname) + if path_real not in self.paths: + self.paths.add(path_real) + for subname in self.find_modules(pathname): + if subname != "__init__": + yield f"{name}.{subname}" + yield name + + def find_all_modules(self, path=None): + """Return a list with all modules in `path`, which should be a list of + directory names. If path is not given, sys.path will be used.""" + + if path is None: + self.modules.update(sys.builtin_module_names) + path = sys.path + + for p in path: + if not p: + p = os.curdir + for module in self.find_modules(p): + self.modules.add(module) + yield + + def find_coroutine(self): + if self.fully_loaded: + return None + try: + next(self.find_iterator) + except StopIteration: + self.fully_loaded = True -find_iterator = find_all_modules() + return True diff --git a/bpython/repl.py b/bpython/repl.py index 9b23221ad..35a48c16a 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -48,6 +48,7 @@ from .paste import PasteHelper, PastePinnwand, PasteFailed from .patch_linecache import filename_for_console_input from .translations import _, ngettext +from .importcompletion import ModuleGatherer class RuntimeTimer: @@ -443,8 +444,9 @@ def __init__(self, interp, config): except OSError: pass + self.module_gatherer = ModuleGatherer() self.completers = autocomplete.get_default_completer( - config.autocomplete_mode + config.autocomplete_mode, self.module_gatherer ) if self.config.pastebin_helper: self.paster = PasteHelper(self.config.pastebin_helper) diff --git a/bpython/simplerepl.py b/bpython/simplerepl.py index 9ec6bea86..24f7380e8 100644 --- a/bpython/simplerepl.py +++ b/bpython/simplerepl.py @@ -27,10 +27,10 @@ import time import logging -from .curtsiesfrontend.repl import BaseRepl -from .curtsiesfrontend import events as bpythonevents from . import translations -from . import importcompletion +from .curtsiesfrontend import events as bpythonevents +from .curtsiesfrontend.repl import BaseRepl +from .importcompletion import ModuleGatherer from curtsies.configfile_keynames import keymap as key_dispatch @@ -116,7 +116,8 @@ def get_input(self): def main(args=None, locals_=None, banner=None): translations.init() - while importcompletion.find_coroutine(): + module_gatherer = ModuleGatherer() + while module_gatherer.find_coroutine(): pass with SimpleRepl() as r: r.width = 50 diff --git a/bpython/test/test_import_not_cyclical.py b/bpython/test/test_import_not_cyclical.py index a1001365c..ea1a28b1c 100644 --- a/bpython/test/test_import_not_cyclical.py +++ b/bpython/test/test_import_not_cyclical.py @@ -2,7 +2,7 @@ import tempfile from bpython.test import unittest -from bpython.importcompletion import find_modules +from bpython.importcompletion import ModuleGatherer class TestAvoidSymbolicLinks(unittest.TestCase): @@ -62,9 +62,11 @@ def setUp(self): True, ) - self.modules = list( - find_modules(os.path.abspath(import_test_folder)) + self.module_gatherer = ModuleGatherer( + [os.path.abspath(import_test_folder)] ) + while self.module_gatherer.find_coroutine(): + pass self.filepaths = [ "Left.toRight.toLeft", "Left.toRight", @@ -79,7 +81,7 @@ def setUp(self): ] def test_simple_symbolic_link_loop(self): - for thing in self.modules: + for thing in self.module_gatherer.modules: self.assertTrue(thing in self.filepaths) if thing == "Left.toRight.toLeft": self.filepaths.remove("Right.toLeft") diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index e585b8a4c..d7aac54ce 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -1,11 +1,11 @@ -from bpython import importcompletion +from bpython.importcompletion import ModuleGatherer from bpython.test import unittest class TestSimpleComplete(unittest.TestCase): def setUp(self): - self.original_modules = importcompletion.modules - importcompletion.modules = [ + self.module_gatherer = ModuleGatherer() + self.module_gatherer.modules = [ "zzabc", "zzabd", "zzefg", @@ -13,39 +13,37 @@ def setUp(self): "zzabc.f", ] - def tearDown(self): - importcompletion.modules = self.original_modules - def test_simple_completion(self): self.assertSetEqual( - importcompletion.complete(10, "import zza"), {"zzabc", "zzabd"} + self.module_gatherer.complete(10, "import zza"), {"zzabc", "zzabd"} ) def test_package_completion(self): self.assertSetEqual( - importcompletion.complete(13, "import zzabc."), + self.module_gatherer.complete(13, "import zzabc."), {"zzabc.e", "zzabc.f"}, ) class TestRealComplete(unittest.TestCase): - @classmethod - def setUpClass(cls): - importcompletion.reload() + def setUp(self): + self.module_gatherer = ModuleGatherer() + while self.module_gatherer.find_coroutine(): + pass __import__("sys") __import__("os") def test_from_attribute(self): self.assertSetEqual( - importcompletion.complete(19, "from sys import arg"), {"argv"} + self.module_gatherer.complete(19, "from sys import arg"), {"argv"} ) def test_from_attr_module(self): self.assertSetEqual( - importcompletion.complete(9, "from os.p"), {"os.path"} + self.module_gatherer.complete(9, "from os.p"), {"os.path"} ) def test_from_package(self): self.assertSetEqual( - importcompletion.complete(17, "from xml import d"), {"dom"} + self.module_gatherer.complete(17, "from xml import d"), {"dom"} ) diff --git a/bpython/urwid.py b/bpython/urwid.py index d9e0a388c..1f63b1370 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -39,7 +39,6 @@ from . import args as bpargs, repl, translations from .formatter import theme_map -from .importcompletion import find_coroutine from .translations import _ from .keys import urwid_key_dispatch as key_dispatch @@ -1344,7 +1343,7 @@ def start(main_loop, user_data): # This bypasses main_loop.set_alarm_in because we must *not* # hit the draw_screen call (it's unnecessary and slow). def run_find_coroutine(): - if find_coroutine(): + if myrepl.module_gatherer.find_coroutine(): main_loop.event_loop.alarm(0, run_find_coroutine) run_find_coroutine() From cf5763eca1ca18c6b4b8c580a53183a4c3f8248a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 30 Dec 2020 22:22:02 +0100 Subject: [PATCH 055/555] Make importcompletion skiplist configurable (fixes #849) --- bpython/config.py | 23 +++++++++++++++++++++ bpython/importcompletion.py | 22 ++------------------ bpython/repl.py | 4 +++- doc/sphinx/source/configuration-options.rst | 6 ++++++ 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index 59b12c44e..68e700915 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -70,6 +70,26 @@ def loadini(struct, configfile): "default_autoreload": False, "editor": os.environ.get("VISUAL", os.environ.get("EDITOR", "vi")), "flush_output": True, + "import_completion_skiplist": ":".join( + ( + # version tracking + ".git", + ".svn", + ".hg" + # XDG + ".config", + ".local", + ".share", + # nodejs + "node_modules", + # PlayOnLinux + "PlayOnLinux's virtual drives", + # wine + "dosdevices", + # Python byte code cache + "__pycache__", + ) + ), "highlight_show_source": True, "hist_duplicates": True, "hist_file": "~/.pythonhist", @@ -188,6 +208,9 @@ def get_key_no_doublebind(command): struct.default_autoreload = config.getboolean( "general", "default_autoreload" ) + struct.import_completion_skiplist = config.get( + "general", "import_completion_skiplist" + ).split(":") struct.pastebin_key = get_key_no_doublebind("pastebin") struct.copy_clipboard_key = get_key_no_doublebind("copy_clipboard") diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 920e4234a..aca7e6602 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -38,32 +38,14 @@ class ModuleGatherer: - def __init__(self, path=None): + def __init__(self, path=None, skiplist=None): # The cached list of all known modules self.modules = set() # List of stored paths to compare against so that real paths are not repeated # handles symlinks not mount points self.paths = set() # Patterns to skip - # TODO: This skiplist should be configurable. - self.skiplist = ( - # version tracking - ".git", - ".svn", - ".hg" - # XDG - ".config", - ".local", - ".share", - # nodejs - "node_modules", - # PlayOnLinux - "PlayOnLinux's virtual drives", - # wine - "dosdevices", - # cache - "__pycache__", - ) + self.skiplist = skiplist if skiplist is not None else tuple() self.fully_loaded = False self.find_iterator = self.find_all_modules(path) diff --git a/bpython/repl.py b/bpython/repl.py index 35a48c16a..5fb75f04a 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -444,7 +444,9 @@ def __init__(self, interp, config): except OSError: pass - self.module_gatherer = ModuleGatherer() + self.module_gatherer = ModuleGatherer( + skiplist=self.config.import_completion_skiplist + ) self.completers = autocomplete.get_default_completer( config.autocomplete_mode, self.module_gatherer ) diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst index b1eeb068a..4bc688207 100644 --- a/doc/sphinx/source/configuration-options.rst +++ b/doc/sphinx/source/configuration-options.rst @@ -174,6 +174,12 @@ Whether to use Unicode characters to draw boxes. .. versionadded:: 0.14 +import_completion_skiplist +^^^^^^^^^^^^^^^^^^^^^^^^^^ +A `:`-seperated list of patterns to skip when processing modules for import completion. + +.. versionadded:: 0.21 + Keyboard -------- This section refers to the ``[keyboard]`` section in your From bd277b645417e82f7740ef6a1824676611748109 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 30 Dec 2020 23:14:18 +0100 Subject: [PATCH 056/555] Import unittest and mock directly --- bpython/test/__init__.py | 12 +++--------- bpython/test/test_args.py | 5 +++-- bpython/test/test_autocomplete.py | 2 +- bpython/test/test_config.py | 2 +- bpython/test/test_crashers.py | 3 ++- bpython/test/test_curtsies.py | 5 +++-- bpython/test/test_curtsies_coderunner.py | 3 ++- bpython/test/test_curtsies_painting.py | 4 ++-- bpython/test/test_curtsies_repl.py | 7 ++++--- bpython/test/test_filewatch.py | 3 ++- bpython/test/test_history.py | 3 +-- bpython/test/test_import_not_cyclical.py | 2 +- bpython/test/test_importcompletion.py | 3 ++- bpython/test/test_inspection.py | 2 +- bpython/test/test_interpreter.py | 3 ++- bpython/test/test_keys.py | 3 ++- bpython/test/test_line_properties.py | 2 +- bpython/test/test_manual_readline.py | 3 ++- bpython/test/test_preprocess.py | 12 ++++++------ bpython/test/test_repl.py | 9 ++++++--- bpython/test/test_simpleeval.py | 2 +- 21 files changed, 48 insertions(+), 42 deletions(-) diff --git a/bpython/test/__init__.py b/bpython/test/__init__.py index 896f856fe..ca62a204d 100644 --- a/bpython/test/__init__.py +++ b/bpython/test/__init__.py @@ -1,9 +1,8 @@ import unittest -import builtins -from unittest import mock +import unittest.mock +import os from bpython.translations import init -import os class FixLanguageTestCase(unittest.TestCase): @@ -12,14 +11,9 @@ def setUpClass(cls): init(languages=["en"]) -class MagicIterMock(mock.MagicMock): +class MagicIterMock(unittest.mock.MagicMock): __next__ = mock.Mock(return_value=None) -def builtin_target(obj): - """Returns mock target string of a builtin""" - return f"{builtins.__name__}.{obj.__name__}" - - TEST_CONFIG = os.path.join(os.path.dirname(__file__), "test.config") diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py index e71a9ddfd..2115bc088 100644 --- a/bpython/test/test_args.py +++ b/bpython/test/test_args.py @@ -2,10 +2,11 @@ import subprocess import sys import tempfile -from textwrap import dedent +import unittest +from textwrap import dedent from bpython import args -from bpython.test import FixLanguageTestCase as TestCase, unittest +from bpython.test import FixLanguageTestCase as TestCase class TestExecArgs(unittest.TestCase): diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 852a6ef7b..5787744c0 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -2,6 +2,7 @@ import keyword import unittest from collections import namedtuple +from unittest import mock try: import jedi @@ -11,7 +12,6 @@ has_jedi = False from bpython import autocomplete -from bpython.test import mock glob_function = "glob.iglob" diff --git a/bpython/test/test_config.py b/bpython/test/test_config.py index a4d081da9..76db5d293 100644 --- a/bpython/test/test_config.py +++ b/bpython/test/test_config.py @@ -1,8 +1,8 @@ import os import tempfile import textwrap +import unittest -from bpython.test import unittest from bpython import config TEST_THEME_PATH = os.path.join(os.path.dirname(__file__), "test.theme") diff --git a/bpython/test/test_crashers.py b/bpython/test/test_crashers.py index 7a92f4f87..3315f418e 100644 --- a/bpython/test/test_crashers.py +++ b/bpython/test/test_crashers.py @@ -5,8 +5,9 @@ import sys import termios import textwrap +import unittest -from bpython.test import unittest, TEST_CONFIG +from bpython.test import TEST_CONFIG from bpython.config import getpreferredencoding try: diff --git a/bpython/test/test_curtsies.py b/bpython/test/test_curtsies.py index d27e53dac..fde2b1037 100644 --- a/bpython/test/test_curtsies.py +++ b/bpython/test/test_curtsies.py @@ -1,7 +1,8 @@ -from collections import namedtuple +import unittest +from collections import namedtuple from bpython.curtsies import combined_events -from bpython.test import FixLanguageTestCase as TestCase, unittest +from bpython.test import FixLanguageTestCase as TestCase import curtsies.events diff --git a/bpython/test/test_curtsies_coderunner.py b/bpython/test/test_curtsies_coderunner.py index 3a677b561..bb5cec423 100644 --- a/bpython/test/test_curtsies_coderunner.py +++ b/bpython/test/test_curtsies_coderunner.py @@ -1,6 +1,7 @@ import sys +import unittest -from bpython.test import mock, unittest +from unittest import mock from bpython.curtsiesfrontend.coderunner import CodeRunner, FakeOutput diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index a5d958a05..fb82627ee 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -2,13 +2,13 @@ import pydoc import string import sys -from contextlib import contextmanager +from contextlib import contextmanager from curtsies.formatstringarray import FormatStringTest, fsarray from curtsies.fmtfuncs import cyan, bold, green, yellow, on_magenta, red +from unittest import mock from bpython.curtsiesfrontend.events import RefreshRequestEvent -from bpython.test import mock from bpython import config, inspection from bpython.curtsiesfrontend.repl import BaseRepl from bpython.curtsiesfrontend import replpainter diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py index 824c166e0..4a7544d1b 100644 --- a/bpython/test/test_curtsies_repl.py +++ b/bpython/test/test_curtsies_repl.py @@ -3,8 +3,11 @@ import sys import tempfile import io -from functools import partial +import unittest + from contextlib import contextmanager +from functools import partial +from unittest import mock from bpython.curtsiesfrontend import repl as curtsiesrepl from bpython.curtsiesfrontend import interpreter @@ -16,8 +19,6 @@ from bpython.test import ( FixLanguageTestCase as TestCase, MagicIterMock, - mock, - unittest, TEST_CONFIG, ) diff --git a/bpython/test/test_filewatch.py b/bpython/test/test_filewatch.py index 9f2790b28..67b29f943 100644 --- a/bpython/test/test_filewatch.py +++ b/bpython/test/test_filewatch.py @@ -1,4 +1,5 @@ import os +import unittest try: import watchdog @@ -8,7 +9,7 @@ except ImportError: has_watchdog = False -from bpython.test import mock, unittest +from unittest import mock @unittest.skipUnless(has_watchdog, "watchdog required") diff --git a/bpython/test/test_history.py b/bpython/test/test_history.py index 6e8756a5c..71d7847ce 100644 --- a/bpython/test/test_history.py +++ b/bpython/test/test_history.py @@ -1,9 +1,8 @@ import os - +import unittest from bpython.config import getpreferredencoding from bpython.history import History -from bpython.test import unittest class TestHistory(unittest.TestCase): diff --git a/bpython/test/test_import_not_cyclical.py b/bpython/test/test_import_not_cyclical.py index ea1a28b1c..25633182f 100644 --- a/bpython/test/test_import_not_cyclical.py +++ b/bpython/test/test_import_not_cyclical.py @@ -1,7 +1,7 @@ import os import tempfile +import unittest -from bpython.test import unittest from bpython.importcompletion import ModuleGatherer diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index d7aac54ce..59f83e16a 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -1,5 +1,6 @@ +import unittest + from bpython.importcompletion import ModuleGatherer -from bpython.test import unittest class TestSimpleComplete(unittest.TestCase): diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 6bf6e7793..885c0dbe1 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -1,7 +1,7 @@ import os +import unittest from bpython import inspection -from bpython.test import unittest from bpython.test.fodder import encoding_ascii from bpython.test.fodder import encoding_latin1 from bpython.test.fodder import encoding_utf8 diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index afa71aa79..9b93672c0 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -1,10 +1,11 @@ import sys import re +import unittest from curtsies.fmtfuncs import bold, green, magenta, cyan, red, plain +from unittest import mock from bpython.curtsiesfrontend import interpreter -from bpython.test import mock, unittest pypy = "PyPy" in sys.version diff --git a/bpython/test/test_keys.py b/bpython/test/test_keys.py index 671182bc3..23e8798cc 100644 --- a/bpython/test/test_keys.py +++ b/bpython/test/test_keys.py @@ -1,5 +1,6 @@ +import unittest + from bpython import keys -from bpython.test import unittest class TestCLIKeys(unittest.TestCase): diff --git a/bpython/test/test_line_properties.py b/bpython/test/test_line_properties.py index f139b3103..fe1b0813e 100644 --- a/bpython/test/test_line_properties.py +++ b/bpython/test/test_line_properties.py @@ -1,6 +1,6 @@ import re +import unittest -from bpython.test import unittest from bpython.line import ( current_word, current_dict_key, diff --git a/bpython/test/test_manual_readline.py b/bpython/test/test_manual_readline.py index 7b0936ae1..445e78b30 100644 --- a/bpython/test/test_manual_readline.py +++ b/bpython/test/test_manual_readline.py @@ -1,3 +1,5 @@ +import unittest + from bpython.curtsiesfrontend.manual_readline import ( left_arrow, right_arrow, @@ -16,7 +18,6 @@ UnconfiguredEdits, delete_word_from_cursor_back, ) -from bpython.test import unittest class TestManualReadline(unittest.TestCase): diff --git a/bpython/test/test_preprocess.py b/bpython/test/test_preprocess.py index 143dfd3e7..03e9a3b8e 100644 --- a/bpython/test/test_preprocess.py +++ b/bpython/test/test_preprocess.py @@ -1,16 +1,16 @@ -from code import compile_command as compiler -from functools import partial import difflib import inspect import re +import unittest + +from code import compile_command as compiler +from functools import partial from bpython.curtsiesfrontend.interpreter import code_finished_will_parse from bpython.curtsiesfrontend.preprocess import preprocess -from bpython.test import unittest from bpython.test.fodder import original, processed -skip = unittest.skip preproc = partial(preprocess, compiler=compiler) @@ -85,14 +85,14 @@ def test_empty_line_within_class(self): def test_blank_lines_in_for_loop(self): self.assertIndented("blank_lines_in_for_loop") - @skip( + @unittest.skip( "More advanced technique required: need to try compiling and " "backtracking" ) def test_blank_line_in_try_catch(self): self.assertIndented("blank_line_in_try_catch") - @skip( + @unittest.skip( "More advanced technique required: need to try compiling and " "backtracking" ) diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 7182416e1..1c56f8444 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -1,4 +1,3 @@ -from itertools import islice import collections import inspect import os @@ -6,10 +5,14 @@ import socket import sys import tempfile +import unittest + +from itertools import islice +from unittest import mock from bpython import config, repl, cli, autocomplete -from bpython.test import MagicIterMock, mock, FixLanguageTestCase as TestCase -from bpython.test import unittest, TEST_CONFIG +from bpython.test import MagicIterMock, FixLanguageTestCase as TestCase +from bpython.test import TEST_CONFIG pypy = "PyPy" in sys.version diff --git a/bpython/test/test_simpleeval.py b/bpython/test/test_simpleeval.py index 2865ed68f..1d1a3f1a3 100644 --- a/bpython/test/test_simpleeval.py +++ b/bpython/test/test_simpleeval.py @@ -1,13 +1,13 @@ import ast import numbers import sys +import unittest from bpython.simpleeval import ( simple_eval, evaluate_current_expression, EvaluationError, ) -from bpython.test import unittest class TestSimpleEval(unittest.TestCase): From 18fc6ff2082b9297b3e4f8a09132a1f33537a043 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 30 Dec 2020 23:20:40 +0100 Subject: [PATCH 057/555] Fix use of mock --- bpython/test/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/__init__.py b/bpython/test/__init__.py index ca62a204d..cb9caef5f 100644 --- a/bpython/test/__init__.py +++ b/bpython/test/__init__.py @@ -13,7 +13,7 @@ def setUpClass(cls): class MagicIterMock(unittest.mock.MagicMock): - __next__ = mock.Mock(return_value=None) + __next__ = unittest.mock.Mock(return_value=None) TEST_CONFIG = os.path.join(os.path.dirname(__file__), "test.config") From 7fca6d79001c3a035f206a9807b54aee46af3fcd Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 30 Dec 2020 23:25:31 +0100 Subject: [PATCH 058/555] Consolidate importcompletion tests --- bpython/test/test_import_not_cyclical.py | 97 ------------------------ bpython/test/test_importcompletion.py | 90 ++++++++++++++++++++++ 2 files changed, 90 insertions(+), 97 deletions(-) delete mode 100644 bpython/test/test_import_not_cyclical.py diff --git a/bpython/test/test_import_not_cyclical.py b/bpython/test/test_import_not_cyclical.py deleted file mode 100644 index 25633182f..000000000 --- a/bpython/test/test_import_not_cyclical.py +++ /dev/null @@ -1,97 +0,0 @@ -import os -import tempfile -import unittest - -from bpython.importcompletion import ModuleGatherer - - -class TestAvoidSymbolicLinks(unittest.TestCase): - def setUp(self): - with tempfile.TemporaryDirectory() as import_test_folder: - os.mkdir(os.path.join(import_test_folder, "Level0")) - os.mkdir(os.path.join(import_test_folder, "Right")) - os.mkdir(os.path.join(import_test_folder, "Left")) - - current_path = os.path.join(import_test_folder, "Level0") - with open( - os.path.join(current_path, "__init__.py"), "x" - ) as init_file: - pass - - current_path = os.path.join(current_path, "Level1") - os.mkdir(current_path) - with open( - os.path.join(current_path, "__init__.py"), "x" - ) as init_file: - pass - - current_path = os.path.join(current_path, "Level2") - os.mkdir(current_path) - with open( - os.path.join(current_path, "__init__.py"), "x" - ) as init_file: - pass - - os.symlink( - os.path.join(import_test_folder, "Level0/Level1"), - os.path.join(current_path, "Level3"), - True, - ) - - current_path = os.path.join(import_test_folder, "Right") - with open( - os.path.join(current_path, "__init__.py"), "x" - ) as init_file: - pass - - os.symlink( - os.path.join(import_test_folder, "Left"), - os.path.join(current_path, "toLeft"), - True, - ) - - current_path = os.path.join(import_test_folder, "Left") - with open( - os.path.join(current_path, "__init__.py"), "x" - ) as init_file: - pass - - os.symlink( - os.path.join(import_test_folder, "Right"), - os.path.join(current_path, "toRight"), - True, - ) - - self.module_gatherer = ModuleGatherer( - [os.path.abspath(import_test_folder)] - ) - while self.module_gatherer.find_coroutine(): - pass - self.filepaths = [ - "Left.toRight.toLeft", - "Left.toRight", - "Left", - "Level0.Level1.Level2.Level3", - "Level0.Level1.Level2", - "Level0.Level1", - "Level0", - "Right", - "Right.toLeft", - "Right.toLeft.toRight", - ] - - def test_simple_symbolic_link_loop(self): - for thing in self.module_gatherer.modules: - self.assertTrue(thing in self.filepaths) - if thing == "Left.toRight.toLeft": - self.filepaths.remove("Right.toLeft") - self.filepaths.remove("Right.toLeft.toRight") - if thing == "Right.toLeft.toRight": - self.filepaths.remove("Left.toRight.toLeft") - self.filepaths.remove("Left.toRight") - self.filepaths.remove(thing) - self.assertFalse(self.filepaths) - - -if __name__ == "__main__": - unittest.main() diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index 59f83e16a..00700aadb 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -1,3 +1,5 @@ +import os +import tempfile import unittest from bpython.importcompletion import ModuleGatherer @@ -48,3 +50,91 @@ def test_from_package(self): self.assertSetEqual( self.module_gatherer.complete(17, "from xml import d"), {"dom"} ) + + +class TestAvoidSymbolicLinks(unittest.TestCase): + def setUp(self): + with tempfile.TemporaryDirectory() as import_test_folder: + os.mkdir(os.path.join(import_test_folder, "Level0")) + os.mkdir(os.path.join(import_test_folder, "Right")) + os.mkdir(os.path.join(import_test_folder, "Left")) + + current_path = os.path.join(import_test_folder, "Level0") + with open( + os.path.join(current_path, "__init__.py"), "x" + ) as init_file: + pass + + current_path = os.path.join(current_path, "Level1") + os.mkdir(current_path) + with open( + os.path.join(current_path, "__init__.py"), "x" + ) as init_file: + pass + + current_path = os.path.join(current_path, "Level2") + os.mkdir(current_path) + with open( + os.path.join(current_path, "__init__.py"), "x" + ) as init_file: + pass + + os.symlink( + os.path.join(import_test_folder, "Level0/Level1"), + os.path.join(current_path, "Level3"), + True, + ) + + current_path = os.path.join(import_test_folder, "Right") + with open( + os.path.join(current_path, "__init__.py"), "x" + ) as init_file: + pass + + os.symlink( + os.path.join(import_test_folder, "Left"), + os.path.join(current_path, "toLeft"), + True, + ) + + current_path = os.path.join(import_test_folder, "Left") + with open( + os.path.join(current_path, "__init__.py"), "x" + ) as init_file: + pass + + os.symlink( + os.path.join(import_test_folder, "Right"), + os.path.join(current_path, "toRight"), + True, + ) + + self.module_gatherer = ModuleGatherer( + [os.path.abspath(import_test_folder)] + ) + while self.module_gatherer.find_coroutine(): + pass + self.filepaths = [ + "Left.toRight.toLeft", + "Left.toRight", + "Left", + "Level0.Level1.Level2.Level3", + "Level0.Level1.Level2", + "Level0.Level1", + "Level0", + "Right", + "Right.toLeft", + "Right.toLeft.toRight", + ] + + def test_simple_symbolic_link_loop(self): + for thing in self.module_gatherer.modules: + self.assertTrue(thing in self.filepaths) + if thing == "Left.toRight.toLeft": + self.filepaths.remove("Right.toLeft") + self.filepaths.remove("Right.toLeft.toRight") + if thing == "Right.toLeft.toRight": + self.filepaths.remove("Left.toRight.toLeft") + self.filepaths.remove("Left.toRight") + self.filepaths.remove(thing) + self.assertFalse(self.filepaths) From f2298211a8f410376283604040f62a8153dee073 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 30 Dec 2020 23:29:06 +0100 Subject: [PATCH 059/555] Simplify using pathlib --- bpython/test/test_importcompletion.py | 28 +++++++-------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index 00700aadb..b21207e11 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -2,6 +2,7 @@ import tempfile import unittest +from pathlib import Path from bpython.importcompletion import ModuleGatherer @@ -60,36 +61,24 @@ def setUp(self): os.mkdir(os.path.join(import_test_folder, "Left")) current_path = os.path.join(import_test_folder, "Level0") - with open( - os.path.join(current_path, "__init__.py"), "x" - ) as init_file: - pass + Path(os.path.join(current_path, "__init__.py")).touch() current_path = os.path.join(current_path, "Level1") os.mkdir(current_path) - with open( - os.path.join(current_path, "__init__.py"), "x" - ) as init_file: - pass + Path(os.path.join(current_path, "__init__.py")).touch() current_path = os.path.join(current_path, "Level2") os.mkdir(current_path) - with open( - os.path.join(current_path, "__init__.py"), "x" - ) as init_file: - pass + Path(os.path.join(current_path, "__init__.py")).touch() os.symlink( - os.path.join(import_test_folder, "Level0/Level1"), + os.path.join(import_test_folder, "Level0", "Level1"), os.path.join(current_path, "Level3"), True, ) current_path = os.path.join(import_test_folder, "Right") - with open( - os.path.join(current_path, "__init__.py"), "x" - ) as init_file: - pass + Path(os.path.join(current_path, "__init__.py")).touch() os.symlink( os.path.join(import_test_folder, "Left"), @@ -98,10 +87,7 @@ def setUp(self): ) current_path = os.path.join(import_test_folder, "Left") - with open( - os.path.join(current_path, "__init__.py"), "x" - ) as init_file: - pass + Path(os.path.join(current_path, "__init__.py")).touch() os.symlink( os.path.join(import_test_folder, "Right"), From 40b300cd1c67a90c10fa0371e05f2b869ff8fe13 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 31 Dec 2020 00:24:30 +0100 Subject: [PATCH 060/555] Fix formatting --- CHANGELOG.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0f762ca20..a9da60571 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,35 +5,42 @@ Changelog ---- General information: + * Support for Python 2 has been dropped. New features: + * #643: Provide bpython._version if built from Github tarballs Fixes: + * #857: Replace remaining use of deprecated imp with importlib 0.20.1 ------ Fixes: + * Fix check of key code (fixes #859) 0.20 ---- General information: + * The next release of bpython (0.20) will drop support for Python 2. * Support for Python 3.9 has been added. Support for Python 3.5 has been dropped. New features: + * #802: Provide redo. Thanks to Evan. * #835: Add support for importing namespace packages. Thanks to Thomas Babej. Fixes: + * #622: Provide encoding attribute for FakeOutput. * #806: Prevent symbolic link loops in import completion. Thanks to Etienne Richart. @@ -48,6 +55,7 @@ Fixes: ---- General information: + * The bpython-cli and bpython-urwid rendering backends have been deprecated and will show a warning that they'll be removed in a future release when started. * Usage in combination with Python 2 has been deprecated. This does not mean that @@ -61,6 +69,7 @@ General information: New features: Fixes: + * #765: Display correct signature for decorated functions. Thanks to Benedikt Rascher-Friesenhausen. * #776: Protect get_args from user code exceptions @@ -73,10 +82,12 @@ Support for Python 3.8 has been added. Support for Python 3.4 has been dropped. ---- New features: + * #713 expose globals in bpdb debugging. Thanks to toejough. Fixes: + * Fix file locking on Windows. * Exit gracefully if config file fails to be loaded due to encoding errors. * #744: Fix newline handling. @@ -98,11 +109,13 @@ Fixes: ---- New features: + * #641: Implement Ctrl+O. * Add default_autoreload config option. Thanks to Alex Frieder. Fixes: + * Fix deprecation warnings. * Do not call signal outside of main thread. Thanks to Max Nordlund. @@ -122,9 +135,11 @@ Fixes: ---- New features: + * #466: Improve handling of completion box height. Fixes: + * Fix various spelling mistakes. Thanks to Josh Soref and Simeon Visser. * #601: Fix Python 2 issues on Windows. From c68d210d2f75e9318b1f0605563bcd64f978fb1c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 31 Dec 2020 12:04:48 +0100 Subject: [PATCH 061/555] No longer complain about old config files --- bpython/config.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index 68e700915..4189993bc 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -155,15 +155,7 @@ def loadini(struct, configfile): fill_config_with_default_values(config, defaults) try: - if not config.read(config_path): - # No config file. If the user has it in the old place then complain - if os.path.isfile(os.path.expanduser("~/.bpython.ini")): - sys.stderr.write( - "Error: It seems that you have a config file at " - "~/.bpython.ini. Please move your config file to " - "%s\n" % default_config_path() - ) - sys.exit(1) + config.read(config_path) except UnicodeDecodeError as e: sys.stderr.write( "Error: Unable to parse config file at '{}' due to an " From 8bc9df74ceb1982c3bfa24ba2805783cdf9c0a06 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 31 Dec 2020 13:19:49 +0100 Subject: [PATCH 062/555] Use xdg module instead of custom XDG logic --- README.rst | 5 +++-- bpython/config.py | 4 ++-- requirements.txt | 1 + setup.py | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 2bbb777e7..059e78d77 100644 --- a/README.rst +++ b/README.rst @@ -155,13 +155,14 @@ your config file as **~/.config/bpython/config** (i.e. Dependencies ============ * Pygments -* requests * curtsies >= 0.3.3 * greenlet +* requests +* xdg * Sphinx >= 1.5 (optional, for the documentation) * babel (optional, for internationalization) -* watchdog (optional, for monitoring imported modules for changes) * jedi (optional, for experimental multiline completion) +* watchdog (optional, for monitoring imported modules for changes) bpython-urwid ------------- diff --git a/bpython/config.py b/bpython/config.py index 4189993bc..08eeec920 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -3,6 +3,7 @@ import locale from itertools import chain from configparser import ConfigParser +from xdg import BaseDirectory from .autocomplete import AutocompleteModes @@ -34,8 +35,7 @@ def supports_box_chars(): def get_config_home(): """Returns the base directory for bpython's configuration files.""" - xdg_config_home = os.environ.get("XDG_CONFIG_HOME", "~/.config") - return os.path.join(xdg_config_home, "bpython") + return os.path.join(BaseDirectory.xdg_config_home, "bpython") def default_config_path(): diff --git a/requirements.txt b/requirements.txt index 5cb3eba12..bfbb71cdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ curtsies >=0.3.3 greenlet requests setuptools +xdg diff --git a/setup.py b/setup.py index c49251f9b..e20a0be80 100755 --- a/setup.py +++ b/setup.py @@ -174,6 +174,7 @@ def git_describe_to_python_version(version): "curtsies >=0.3.3", "greenlet", "wcwidth", + "xdg", ] extras_require = { From 57a97d8fc3edf10276fc5ef4987234a45eaf2515 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 31 Dec 2020 13:38:27 +0100 Subject: [PATCH 063/555] Simplify path handling using pathlib --- bpython/config.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index 08eeec920..9cafa3baf 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -1,8 +1,9 @@ import os import sys import locale -from itertools import chain from configparser import ConfigParser +from itertools import chain +from pathlib import Path from xdg import BaseDirectory from .autocomplete import AutocompleteModes @@ -35,12 +36,12 @@ def supports_box_chars(): def get_config_home(): """Returns the base directory for bpython's configuration files.""" - return os.path.join(BaseDirectory.xdg_config_home, "bpython") + return Path(BaseDirectory.xdg_config_home) / "bpython" def default_config_path(): """Returns bpython's default configuration file path.""" - return os.path.join(get_config_home(), "config") + return get_config_home() / "config" def fill_config_with_default_values(config, default_values): @@ -53,11 +54,9 @@ def fill_config_with_default_values(config, default_values): config.set(section, opt, f"{val}") -def loadini(struct, configfile): +def loadini(struct, config_path): """Loads .ini configuration file and stores its values in struct""" - config_path = os.path.expanduser(configfile) - config = ConfigParser() defaults = { "general": { @@ -287,18 +286,15 @@ def get_key_no_doublebind(command): else: struct.color_scheme = dict() - theme_filename = color_scheme_name + ".theme" - path = os.path.expanduser( - os.path.join(get_config_home(), theme_filename) - ) + path = get_config_home() / f"{color_scheme_name}.theme" try: load_theme(struct, path, struct.color_scheme, default_colors) except OSError: - sys.stderr.write(f"Could not load theme '{color_scheme_name}'.\n") + sys.stderr.write(f"Could not load theme '{color_scheme_name}' from {path}.\n") sys.exit(1) # expand path of history file - struct.hist_file = os.path.expanduser(struct.hist_file) + struct.hist_file = Path(struct.hist_file).expanduser() # verify completion mode if struct.autocomplete_mode is None: From 68c953871d5a8205de75abdef2ec4d41fcfa3b64 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 31 Dec 2020 13:43:27 +0100 Subject: [PATCH 064/555] Fix (py)xdg package name --- README.rst | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 059e78d77..0ff6279e5 100644 --- a/README.rst +++ b/README.rst @@ -157,8 +157,8 @@ Dependencies * Pygments * curtsies >= 0.3.3 * greenlet +* pyxdg * requests -* xdg * Sphinx >= 1.5 (optional, for the documentation) * babel (optional, for internationalization) * jedi (optional, for experimental multiline completion) diff --git a/requirements.txt b/requirements.txt index bfbb71cdc..4a9e91bca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Pygments curtsies >=0.3.3 greenlet +pyxdg requests setuptools -xdg diff --git a/setup.py b/setup.py index e20a0be80..64421fb31 100755 --- a/setup.py +++ b/setup.py @@ -174,7 +174,7 @@ def git_describe_to_python_version(version): "curtsies >=0.3.3", "greenlet", "wcwidth", - "xdg", + "pyxdg", ] extras_require = { From a3179a9754da2106831b2e17f6d4019183df3fc7 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 31 Dec 2020 13:44:01 +0100 Subject: [PATCH 065/555] Add license header --- bpython/autocomplete.py | 1 - bpython/config.py | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 6b6698b13..94841e044 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -20,7 +20,6 @@ # 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. -# import __main__ diff --git a/bpython/config.py b/bpython/config.py index 9cafa3baf..70708fd77 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -1,3 +1,27 @@ +# The MIT License +# +# Copyright (c) 2009-2015 the bpython authors. +# Copyright (c) 2015-2020 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. + + import os import sys import locale From 3a1c1b09ff4affa140ed2c4fcf32dda07d3f966e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 31 Dec 2020 13:51:50 +0100 Subject: [PATCH 066/555] Fix Python 3 compatibility --- bpython/simplerepl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/simplerepl.py b/bpython/simplerepl.py index 24f7380e8..9723ace5c 100644 --- a/bpython/simplerepl.py +++ b/bpython/simplerepl.py @@ -64,7 +64,7 @@ def request_undo(self, n=1): def out(self, msg): if hasattr(self, "orig_stdout"): - self.orig_stdout.write((msg + "\n").encode("utf8")) + self.orig_stdout.write(f"{msg}\n") self.orig_stdout.flush() else: print(msg) @@ -89,7 +89,7 @@ def print_padded(s): print_padded("") self.out("X``" + ("`" * (self.width + 2)) + "``X") for line in arr: - self.out("X```" + unicode(line.ljust(self.width)) + "```X") + self.out("X```" + line.ljust(self.width) + "```X") logger.debug("line:") logger.debug(repr(line)) self.out("X``" + ("`" * (self.width + 2)) + "``X") From 248f2e2966331277d81606b1fa2da8ade5af3f22 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 31 Dec 2020 14:05:01 +0100 Subject: [PATCH 067/555] Move simplerepl.py to the documentation (fixes #870) --- doc/sphinx/source/index.rst | 1 + {bpython => doc/sphinx/source}/simplerepl.py | 0 doc/sphinx/source/simplerepl.rst | 9 +++++++++ 3 files changed, 10 insertions(+) rename {bpython => doc/sphinx/source}/simplerepl.py (100%) create mode 100644 doc/sphinx/source/simplerepl.rst diff --git a/doc/sphinx/source/index.rst b/doc/sphinx/source/index.rst index 3322f2c6c..f209d2971 100644 --- a/doc/sphinx/source/index.rst +++ b/doc/sphinx/source/index.rst @@ -23,3 +23,4 @@ Contents: bpaste tips bpdb + simplerepl diff --git a/bpython/simplerepl.py b/doc/sphinx/source/simplerepl.py similarity index 100% rename from bpython/simplerepl.py rename to doc/sphinx/source/simplerepl.py diff --git a/doc/sphinx/source/simplerepl.rst b/doc/sphinx/source/simplerepl.rst new file mode 100644 index 000000000..8a088ad73 --- /dev/null +++ b/doc/sphinx/source/simplerepl.rst @@ -0,0 +1,9 @@ +.. _simplerepl: + +A Simple REPL +============= + +The following code listing shows a simple example REPL implemented using `bpython` and `curtsies`. + +.. literalinclude:: simplerepl.py + :language: python From 2d56c5c8035b7771d0801031d01065b4e84cc8cf Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 1 Jan 2021 12:56:09 +0100 Subject: [PATCH 068/555] Use more f-strings --- bpython/config.py | 4 +++- bpython/keys.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index 70708fd77..2d96e0310 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -314,7 +314,9 @@ def get_key_no_doublebind(command): try: load_theme(struct, path, struct.color_scheme, default_colors) except OSError: - sys.stderr.write(f"Could not load theme '{color_scheme_name}' from {path}.\n") + sys.stderr.write( + f"Could not load theme '{color_scheme_name}' from {path}.\n" + ) sys.exit(1) # expand path of history file diff --git a/bpython/keys.py b/bpython/keys.py index 86984db6b..3da060d2c 100644 --- a/bpython/keys.py +++ b/bpython/keys.py @@ -37,8 +37,7 @@ def __getitem__(self, key): return self.map[key] else: raise KeyError( - "Configured keymap (%s)" % key - + " does not exist in bpython.keys" + f"Configured keymap ({key}) does not exist in bpython.keys" ) def __delitem__(self, key): From ea58d89c2f27da98f0d27cf919acb2b9a21fcd0f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 1 Jan 2021 13:12:55 +0100 Subject: [PATCH 069/555] Use pathlib instead of low-level os module --- bpython/config.py | 2 +- bpython/repl.py | 33 +++++++++++++++------------------ bpython/test/test_repl.py | 3 ++- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index 2d96e0310..e1bc04419 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -203,7 +203,7 @@ def get_key_no_doublebind(command): return requested_key - struct.config_path = config_path + struct.config_path = Path(config_path).absolute() struct.dedent_after = config.getint("general", "dedent_after") struct.tab_length = config.getint("general", "tab_length") diff --git a/bpython/repl.py b/bpython/repl.py index 5fb75f04a..9939d7808 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -33,9 +33,10 @@ import textwrap import time import traceback +from enum import Enum from itertools import takewhile +from pathlib import Path from types import ModuleType -from enum import Enum from pygments.token import Token from pygments.lexers import Python3Lexer @@ -435,11 +436,10 @@ def __init__(self, interp, config): self.closed = False self.clipboard = get_clipboard() - pythonhist = os.path.expanduser(self.config.hist_file) - if os.path.exists(pythonhist): + if self.config.hist_file.exists(): try: self.rl_history.load( - pythonhist, getpreferredencoding() or "ascii" + self.config.hist_file, getpreferredencoding() or "ascii" ) except OSError: pass @@ -829,13 +829,13 @@ def write2file(self): self.interact.notify(_("Save cancelled.")) return - if fn.startswith("~"): - fn = os.path.expanduser(fn) - if not fn.endswith(".py") and self.config.save_append_py: - fn = fn + ".py" + fn = Path(fn).expanduser() + if fn.suffix != ".py" and self.config.save_append_py: + # fn.with_suffix(".py") does not append if fn has a non-empty suffix + fn = Path(f"{fn}.py") mode = "w" - if os.path.exists(fn): + if fn.exists(): mode = self.interact.file_prompt( _( "%s already exists. Do you " @@ -941,10 +941,9 @@ def push(self, s, insert_into_history=True): return more def insert_into_history(self, s): - pythonhist = os.path.expanduser(self.config.hist_file) try: self.rl_history.append_reload_and_write( - s, pythonhist, getpreferredencoding() + s, self.config.hist_file, getpreferredencoding() ) except RuntimeError as e: self.interact.notify(f"{e}") @@ -1147,7 +1146,7 @@ def open_in_external_editor(self, filename): return subprocess.call(args) == 0 def edit_config(self): - if not os.path.isfile(self.config.config_path): + if not self.config.config_path.is_file(): if self.interact.confirm( _( "Config file does not exist - create " @@ -1160,17 +1159,15 @@ def edit_config(self): ) # Py3 files need unicode default_config = default_config.decode("ascii") - containing_dir = os.path.dirname( - os.path.abspath(self.config.config_path) - ) - if not os.path.exists(containing_dir): - os.makedirs(containing_dir) + containing_dir = self.config.config_path.parent + if not containing_dir.exists(): + containing_dir.mkdir(parents=True) with open(self.config.config_path, "w") as f: f.write(default_config) except OSError as e: self.interact.notify( _("Error writing file '%s': %s") - % (self.config.config.path, e) + % (self.config.config_path, e) ) return False else: diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 1c56f8444..250dbf241 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -8,6 +8,7 @@ import unittest from itertools import islice +from pathlib import Path from unittest import mock from bpython import config, repl, cli, autocomplete @@ -332,7 +333,7 @@ def setUp(self): def test_create_config(self): tmp_dir = tempfile.mkdtemp() try: - config_path = os.path.join(tmp_dir, "newdir", "config") + config_path = Path(tmp_dir) / "newdir" / "config" self.repl.config.config_path = config_path self.repl.edit_config() self.assertTrue(os.path.exists(config_path)) From 2aed6b325fdd752d6a21e853e26d96278b3dc94d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 1 Jan 2021 13:50:08 +0100 Subject: [PATCH 070/555] Use temporary directory --- bpython/test/test_repl.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 250dbf241..4eb05c4b9 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -1,7 +1,6 @@ import collections import inspect import os -import shutil import socket import sys import tempfile @@ -331,15 +330,11 @@ def setUp(self): self.repl.config.editor = "true" def test_create_config(self): - tmp_dir = tempfile.mkdtemp() - try: + with tempfile.TemporaryDirectory() as tmp_dir: config_path = Path(tmp_dir) / "newdir" / "config" self.repl.config.config_path = config_path self.repl.edit_config() self.assertTrue(os.path.exists(config_path)) - finally: - shutil.rmtree(tmp_dir) - self.assertFalse(os.path.exists(config_path)) class TestRepl(unittest.TestCase): From b7a6753fc06abcad03e6331de712e364c30d52af Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 1 Jan 2021 14:05:27 +0100 Subject: [PATCH 071/555] Use pathlib instead of low-level os module --- bpython/test/__init__.py | 3 +- bpython/test/test_crashers.py | 2 +- bpython/test/test_importcompletion.py | 98 +++++++++++++-------------- bpython/test/test_repl.py | 3 +- 4 files changed, 50 insertions(+), 56 deletions(-) diff --git a/bpython/test/__init__.py b/bpython/test/__init__.py index cb9caef5f..7722278cc 100644 --- a/bpython/test/__init__.py +++ b/bpython/test/__init__.py @@ -1,6 +1,7 @@ import unittest import unittest.mock import os +from pathlib import Path from bpython.translations import init @@ -16,4 +17,4 @@ class MagicIterMock(unittest.mock.MagicMock): __next__ = unittest.mock.Mock(return_value=None) -TEST_CONFIG = os.path.join(os.path.dirname(__file__), "test.config") +TEST_CONFIG = Path(__file__).parent / "test.config" diff --git a/bpython/test/test_crashers.py b/bpython/test/test_crashers.py index 3315f418e..4dd80e569 100644 --- a/bpython/test/test_crashers.py +++ b/bpython/test/test_crashers.py @@ -93,7 +93,7 @@ def processExited(self, reason): "-m", "bpython." + self.backend, "--config", - TEST_CONFIG, + str(TEST_CONFIG), ), env=dict(TERM="vt100", LANG=os.environ.get("LANG", "")), usePTY=(master, slave, os.ttyname(slave)), diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index b21207e11..ee9651a7b 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -56,43 +56,36 @@ def test_from_package(self): class TestAvoidSymbolicLinks(unittest.TestCase): def setUp(self): with tempfile.TemporaryDirectory() as import_test_folder: - os.mkdir(os.path.join(import_test_folder, "Level0")) - os.mkdir(os.path.join(import_test_folder, "Right")) - os.mkdir(os.path.join(import_test_folder, "Left")) - - current_path = os.path.join(import_test_folder, "Level0") - Path(os.path.join(current_path, "__init__.py")).touch() - - current_path = os.path.join(current_path, "Level1") - os.mkdir(current_path) - Path(os.path.join(current_path, "__init__.py")).touch() - - current_path = os.path.join(current_path, "Level2") - os.mkdir(current_path) - Path(os.path.join(current_path, "__init__.py")).touch() - - os.symlink( - os.path.join(import_test_folder, "Level0", "Level1"), - os.path.join(current_path, "Level3"), - True, + base_path = Path(import_test_folder) + (base_path / "Level0" / "Level1" / "Level2").mkdir(parents=True) + (base_path / "Left").mkdir(parents=True) + (base_path / "Right").mkdir(parents=True) + + current_path = base_path / "Level0" + (current_path / "__init__.py").touch() + + current_path = current_path / "Level1" + (current_path / "__init__.py").touch() + + current_path = current_path / "Level2" + (current_path / "__init__.py").touch() + # Level0/Level1/Level2/Level3 -> Level0/Level1 + (current_path / "Level3").symlink_to( + base_path / "Level0" / "Level1", target_is_directory=True ) - current_path = os.path.join(import_test_folder, "Right") - Path(os.path.join(current_path, "__init__.py")).touch() - - os.symlink( - os.path.join(import_test_folder, "Left"), - os.path.join(current_path, "toLeft"), - True, + current_path = base_path / "Right" + (current_path / "__init__.py").touch() + # Right/toLeft -> Left + (current_path / "toLeft").symlink_to( + base_path / "Left", target_is_directory=True ) - current_path = os.path.join(import_test_folder, "Left") - Path(os.path.join(current_path, "__init__.py")).touch() - - os.symlink( - os.path.join(import_test_folder, "Right"), - os.path.join(current_path, "toRight"), - True, + current_path = base_path / "Left" + (current_path / "__init__.py").touch() + # Left/toRight -> Right + (current_path / "toRight").symlink_to( + base_path / "Right", target_is_directory=True ) self.module_gatherer = ModuleGatherer( @@ -100,27 +93,28 @@ def setUp(self): ) while self.module_gatherer.find_coroutine(): pass - self.filepaths = [ - "Left.toRight.toLeft", - "Left.toRight", - "Left", - "Level0.Level1.Level2.Level3", - "Level0.Level1.Level2", - "Level0.Level1", - "Level0", - "Right", - "Right.toLeft", - "Right.toLeft.toRight", - ] def test_simple_symbolic_link_loop(self): + filepaths = [ + "Left.toRight.toLeft", + "Left.toRight", + "Left", + "Level0.Level1.Level2.Level3", + "Level0.Level1.Level2", + "Level0.Level1", + "Level0", + "Right", + "Right.toLeft", + "Right.toLeft.toRight", + ] + for thing in self.module_gatherer.modules: - self.assertTrue(thing in self.filepaths) + self.assertIn(thing, filepaths) if thing == "Left.toRight.toLeft": - self.filepaths.remove("Right.toLeft") - self.filepaths.remove("Right.toLeft.toRight") + filepaths.remove("Right.toLeft") + filepaths.remove("Right.toLeft.toRight") if thing == "Right.toLeft.toRight": - self.filepaths.remove("Left.toRight.toLeft") - self.filepaths.remove("Left.toRight") - self.filepaths.remove(thing) - self.assertFalse(self.filepaths) + filepaths.remove("Left.toRight.toLeft") + filepaths.remove("Left.toRight") + filepaths.remove(thing) + self.assertFalse(filepaths) diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 4eb05c4b9..2d33430fe 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -1,6 +1,5 @@ import collections import inspect -import os import socket import sys import tempfile @@ -334,7 +333,7 @@ def test_create_config(self): config_path = Path(tmp_dir) / "newdir" / "config" self.repl.config.config_path = config_path self.repl.edit_config() - self.assertTrue(os.path.exists(config_path)) + self.assertTrue(config_path.exists()) class TestRepl(unittest.TestCase): From 163b64fe5a9743208e9ba77cd1a3fd6c492b8a9e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 1 Jan 2021 14:36:09 +0100 Subject: [PATCH 072/555] Refactor --- bpython/config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bpython/config.py b/bpython/config.py index e1bc04419..4b7f33a45 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -68,6 +68,11 @@ def default_config_path(): return get_config_home() / "config" +def default_editor(): + """Returns the default editor.""" + return os.environ.get("VISUAL", os.environ.get("EDITOR", "vi")) + + def fill_config_with_default_values(config, default_values): for section in default_values.keys(): if not config.has_section(section): @@ -91,7 +96,7 @@ def loadini(struct, config_path): "complete_magic_methods": True, "dedent_after": 1, "default_autoreload": False, - "editor": os.environ.get("VISUAL", os.environ.get("EDITOR", "vi")), + "editor": default_editor(), "flush_output": True, "import_completion_skiplist": ":".join( ( From 39e75dc5174205fa8f9893fd989e8f496001c5cf Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 1 Jan 2021 14:45:06 +0100 Subject: [PATCH 073/555] Make config non-optional --- bpython/curtsies.py | 2 +- bpython/curtsiesfrontend/repl.py | 13 ++----------- bpython/test/test_curtsies_repl.py | 2 +- doc/sphinx/source/simplerepl.py | 17 ++++++++++------- 4 files changed, 14 insertions(+), 20 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 0c0b44789..f9950997e 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -53,8 +53,8 @@ def __init__(self, config, locals_, banner, interp=None): pass # temp hack to get .original_stty super().__init__( + config, locals_=locals_, - config=config, banner=banner, interp=interp, orig_tcattrs=self.input_generator.original_stty, diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 9af2c4bbc..9e47ebb18 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -27,12 +27,7 @@ import bpython from bpython.repl import Repl as BpythonRepl, SourceNotFound from bpython.repl import LineTypeTranslator as LineType -from bpython.config import ( - Struct, - loadini, - default_config_path, - getpreferredencoding, -) +from bpython.config import getpreferredencoding, default_config_path from bpython.formatter import BPythonFormatter from bpython import autocomplete from bpython.translations import _ @@ -310,8 +305,8 @@ class BaseRepl(BpythonRepl): def __init__( self, + config, locals_=None, - config=None, banner=None, interp=None, orig_tcattrs=None, @@ -326,10 +321,6 @@ def __init__( logger.debug("starting init") - if config is None: - config = Struct() - loadini(config, default_config_path()) - # If creating a new interpreter on undo would be unsafe because initial # state was passed in self.weak_rewind = bool(locals_ or interp) diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py index 4a7544d1b..a95990279 100644 --- a/bpython/test/test_curtsies_repl.py +++ b/bpython/test/test_curtsies_repl.py @@ -233,7 +233,7 @@ def captured_output(): def create_repl(**kwargs): config = setup_config({"editor": "true"}) - repl = curtsiesrepl.BaseRepl(config=config, **kwargs) + repl = curtsiesrepl.BaseRepl(config, **kwargs) os.environ["PAGER"] = "true" os.environ.pop("PYTHONSTARTUP", None) repl.width = 50 diff --git a/doc/sphinx/source/simplerepl.py b/doc/sphinx/source/simplerepl.py index 9723ace5c..e0bddb480 100644 --- a/doc/sphinx/source/simplerepl.py +++ b/doc/sphinx/source/simplerepl.py @@ -27,10 +27,11 @@ import time import logging -from . import translations -from .curtsiesfrontend import events as bpythonevents -from .curtsiesfrontend.repl import BaseRepl -from .importcompletion import ModuleGatherer +from bpython import translations +from bpython.config import Struct, loadini, default_config_path +from bpython.curtsiesfrontend import events as bpythonevents +from bpython.curtsiesfrontend.repl import BaseRepl +from bpython.importcompletion import ModuleGatherer from curtsies.configfile_keynames import keymap as key_dispatch @@ -39,9 +40,9 @@ class SimpleRepl(BaseRepl): - def __init__(self): + def __init__(self, config): self.requested_events = [] - BaseRepl.__init__(self) + BaseRepl.__init__(self, config) def _request_refresh(self): self.requested_events.append(bpythonevents.RefreshRequestEvent()) @@ -116,10 +117,12 @@ def get_input(self): def main(args=None, locals_=None, banner=None): translations.init() + config = Struct() + loadini(config, default_config_path()) module_gatherer = ModuleGatherer() while module_gatherer.find_coroutine(): pass - with SimpleRepl() as r: + with SimpleRepl(config) as r: r.width = 50 r.height = 10 while True: From 7049f1c5c2964f6851420c7540a53b91faaa9751 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 1 Jan 2021 14:56:24 +0100 Subject: [PATCH 074/555] Display actual config file in help message --- bpython/curtsiesfrontend/repl.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 9e47ebb18..8a7a7ff04 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -27,7 +27,7 @@ import bpython from bpython.repl import Repl as BpythonRepl, SourceNotFound from bpython.repl import LineTypeTranslator as LineType -from bpython.config import getpreferredencoding, default_config_path +from bpython.config import getpreferredencoding from bpython.formatter import BPythonFormatter from bpython import autocomplete from bpython.translations import _ @@ -61,8 +61,7 @@ HELP_MESSAGE = """ Thanks for using bpython! -See http://bpython-interpreter.org/ for more information and -http://docs.bpython-interpreter.org/ for docs. +See http://bpython-interpreter.org/ for more information and http://docs.bpython-interpreter.org/ for docs. Please report issues at https://github.com/bpython/bpython/issues Features: @@ -75,7 +74,7 @@ bpython -i your_script.py runs a file in interactive mode bpython -t your_script.py pastes the contents of a file into the session -A config file at {config_file_location} customizes keys and behavior of bpython. +A config file at {config.config_path} customizes keys and behavior of bpython. You can also set which pastebin helper and which external editor to use. See {example_config_url} for an example config file. Press {config.edit_config_key} to edit this config file. @@ -1983,7 +1982,6 @@ def version_help_text(self): + ("using curtsies version %s" % curtsies.__version__) + "\n" + HELP_MESSAGE.format( - config_file_location=default_config_path(), example_config_url=EXAMPLE_CONFIG_URL, config=self.config, ) From d2600fca8a04ad80d6776e6d7a67672fcbb4f022 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 1 Jan 2021 15:21:34 +0100 Subject: [PATCH 075/555] Make help message translatable --- bpython/curtsiesfrontend/repl.py | 54 +++--- bpython/translations/bpython.pot | 154 +++++++++++------- .../translations/de/LC_MESSAGES/bpython.po | 148 ++++++++++------- .../translations/es_ES/LC_MESSAGES/bpython.po | 148 ++++++++++------- .../translations/fr_FR/LC_MESSAGES/bpython.po | 148 ++++++++++------- .../translations/it_IT/LC_MESSAGES/bpython.po | 148 ++++++++++------- .../translations/nl_NL/LC_MESSAGES/bpython.po | 148 ++++++++++------- 7 files changed, 566 insertions(+), 382 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 8a7a7ff04..2d58877f4 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -58,27 +58,6 @@ INCONSISTENT_HISTORY_MSG = "#<---History inconsistent with output shown--->" CONTIGUITY_BROKEN_MSG = "#<---History contiguity broken by rewind--->" -HELP_MESSAGE = """ -Thanks for using bpython! - -See http://bpython-interpreter.org/ for more information and http://docs.bpython-interpreter.org/ for docs. -Please report issues at https://github.com/bpython/bpython/issues - -Features: -Try using undo ({config.undo_key})! -Edit the current line ({config.edit_current_block_key}) or the entire session ({config.external_editor_key}) in an external editor. (currently {config.editor}) -Save sessions ({config.save_key}) or post them to pastebins ({config.pastebin_key})! Current pastebin helper: {config.pastebin_helper} -Reload all modules and rerun session ({config.reimport_key}) to test out changes to a module. -Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute the current session when a module you've imported is modified. - -bpython -i your_script.py runs a file in interactive mode -bpython -t your_script.py pastes the contents of a file into the session - -A config file at {config.config_path} customizes keys and behavior of bpython. -You can also set which pastebin helper and which external editor to use. -See {example_config_url} for an example config file. -Press {config.edit_config_key} to edit this config file. -""" EXAMPLE_CONFIG_URL = "https://raw.githubusercontent.com/bpython/bpython/master/bpython/sample-config" EDIT_SESSION_HEADER = """### current bpython session - make changes and save to reevaluate session. ### lines beginning with ### will be ignored. @@ -1976,16 +1955,29 @@ def help_text(self): return self.version_help_text() + "\n" + self.key_help_text() def version_help_text(self): - return ( - ("bpython-curtsies version %s" % bpython.__version__) - + " " - + ("using curtsies version %s" % curtsies.__version__) - + "\n" - + HELP_MESSAGE.format( - example_config_url=EXAMPLE_CONFIG_URL, - config=self.config, - ) - ) + help_message = _(""" +Thanks for using bpython! + +See http://bpython-interpreter.org/ for more information and http://docs.bpython-interpreter.org/ for docs. +Please report issues at https://github.com/bpython/bpython/issues + +Features: +Try using undo ({config.undo_key})! +Edit the current line ({config.edit_current_block_key}) or the entire session ({config.external_editor_key}) in an external editor. (currently {config.editor}) +Save sessions ({config.save_key}) or post them to pastebins ({config.pastebin_key})! Current pastebin helper: {config.pastebin_helper} +Reload all modules and rerun session ({config.reimport_key}) to test out changes to a module. +Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute the current session when a module you've imported is modified. + +bpython -i your_script.py runs a file in interactive mode +bpython -t your_script.py pastes the contents of a file into the session + +A config file at {config.config_path} customizes keys and behavior of bpython. +You can also set which pastebin helper and which external editor to use. +See {example_config_url} for an example config file. +Press {config.edit_config_key} to edit this config file. +""").format(example_config_url=EXAMPLE_CONFIG_URL, config=self.config) + + return f"bpython-curtsies version {bpython.__version__} using curtsies version {curtsies.__version__}\n{help_message}" def key_help_text(self): NOT_IMPLEMENTED = ( diff --git a/bpython/translations/bpython.pot b/bpython/translations/bpython.pot index e8dc384fa..df60dee43 100644 --- a/bpython/translations/bpython.pot +++ b/bpython/translations/bpython.pot @@ -1,14 +1,14 @@ # Translations template for bpython. -# Copyright (C) 2020 ORGANIZATION +# Copyright (C) 2021 ORGANIZATION # This file is distributed under the same license as the bpython project. -# FIRST AUTHOR , 2020. +# FIRST AUTHOR , 2021. # #, fuzzy msgid "" msgstr "" -"Project-Id-Version: bpython 0.21.dev78\n" +"Project-Id-Version: bpython 0.20.1.post128\n" "Report-Msgid-Bugs-To: https://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2020-10-29 12:28+0100\n" +"POT-Creation-Date: 2021-01-01 15:21+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -47,54 +47,54 @@ msgid "" "script." msgstr "" -#: bpython/cli.py:315 bpython/urwid.py:538 +#: bpython/cli.py:312 bpython/urwid.py:537 msgid "y" msgstr "" -#: bpython/cli.py:315 bpython/urwid.py:538 +#: bpython/cli.py:312 bpython/urwid.py:537 msgid "yes" msgstr "" -#: bpython/cli.py:1695 +#: bpython/cli.py:1692 msgid "Rewind" msgstr "" -#: bpython/cli.py:1696 +#: bpython/cli.py:1693 msgid "Save" msgstr "" -#: bpython/cli.py:1697 +#: bpython/cli.py:1694 msgid "Pastebin" msgstr "" -#: bpython/cli.py:1698 +#: bpython/cli.py:1695 msgid "Pager" msgstr "" -#: bpython/cli.py:1699 +#: bpython/cli.py:1696 msgid "Show Source" msgstr "" -#: bpython/cli.py:1946 +#: bpython/cli.py:1943 msgid "" "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsies.py:137 +#: bpython/curtsies.py:136 msgid "log debug messages to bpython.log" msgstr "" -#: bpython/curtsies.py:143 +#: bpython/curtsies.py:142 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:149 +#: bpython/curtsies.py:148 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:150 +#: bpython/curtsies.py:149 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" @@ -124,208 +124,240 @@ msgstr "" msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:653 +#: bpython/repl.py:657 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:658 +#: bpython/repl.py:662 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:663 +#: bpython/repl.py:667 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:665 +#: bpython/repl.py:669 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:820 +#: bpython/repl.py:824 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 +#: bpython/repl.py:826 bpython/repl.py:829 bpython/repl.py:853 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:836 +#: bpython/repl.py:840 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:844 +#: bpython/repl.py:848 msgid "overwrite" msgstr "" -#: bpython/repl.py:846 +#: bpython/repl.py:850 msgid "append" msgstr "" -#: bpython/repl.py:858 bpython/repl.py:1168 +#: bpython/repl.py:862 bpython/repl.py:1169 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:860 +#: bpython/repl.py:864 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:866 +#: bpython/repl.py:870 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:873 +#: bpython/repl.py:877 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:875 +#: bpython/repl.py:879 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:884 +#: bpython/repl.py:888 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:886 +#: bpython/repl.py:890 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:894 +#: bpython/repl.py:898 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:900 +#: bpython/repl.py:904 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:908 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:913 +#: bpython/repl.py:917 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:918 +#: bpython/repl.py:922 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:957 +#: bpython/repl.py:960 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:965 bpython/repl.py:969 +#: bpython/repl.py:968 bpython/repl.py:972 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:972 +#: bpython/repl.py:975 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1148 +#: bpython/repl.py:1151 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1178 +#: bpython/repl.py:1179 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1184 +#: bpython/repl.py:1185 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:605 +#: bpython/urwid.py:604 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" -#: bpython/urwid.py:1115 +#: bpython/urwid.py:1114 msgid "Run twisted reactor." msgstr "" -#: bpython/urwid.py:1120 +#: bpython/urwid.py:1119 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "" -#: bpython/urwid.py:1128 +#: bpython/urwid.py:1127 msgid "List available reactors for -r." msgstr "" -#: bpython/urwid.py:1133 +#: bpython/urwid.py:1132 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." msgstr "" -#: bpython/urwid.py:1142 +#: bpython/urwid.py:1141 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1336 +#: bpython/urwid.py:1335 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:343 +#: bpython/curtsiesfrontend/repl.py:312 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:345 +#: bpython/curtsiesfrontend/repl.py:314 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:674 +#: bpython/curtsiesfrontend/repl.py:643 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:692 +#: bpython/curtsiesfrontend/repl.py:661 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1001 +#: bpython/curtsiesfrontend/repl.py:970 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1016 +#: bpython/curtsiesfrontend/repl.py:985 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1026 +#: bpython/curtsiesfrontend/repl.py:995 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1037 +#: bpython/curtsiesfrontend/repl.py:1006 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1043 +#: bpython/curtsiesfrontend/repl.py:1012 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1048 +#: bpython/curtsiesfrontend/repl.py:1017 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1054 +#: bpython/curtsiesfrontend/repl.py:1023 msgid "Auto-reloading not available because watchdog not installed." msgstr "" +#: bpython/curtsiesfrontend/repl.py:1958 +msgid "" +"\n" +"Thanks for using bpython!\n" +"\n" +"See http://bpython-interpreter.org/ for more information and http://docs" +".bpython-interpreter.org/ for docs.\n" +"Please report issues at https://github.com/bpython/bpython/issues\n" +"\n" +"Features:\n" +"Try using undo ({config.undo_key})!\n" +"Edit the current line ({config.edit_current_block_key}) or the entire " +"session ({config.external_editor_key}) in an external editor. (currently " +"{config.editor})\n" +"Save sessions ({config.save_key}) or post them to pastebins " +"({config.pastebin_key})! Current pastebin helper: " +"{config.pastebin_helper}\n" +"Reload all modules and rerun session ({config.reimport_key}) to test out " +"changes to a module.\n" +"Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute " +"the current session when a module you've imported is modified.\n" +"\n" +"bpython -i your_script.py runs a file in interactive mode\n" +"bpython -t your_script.py pastes the contents of a file into the session\n" +"\n" +"A config file at {config.config_path} customizes keys and behavior of " +"bpython.\n" +"You can also set which pastebin helper and which external editor to use.\n" +"See {example_config_url} for an example config file.\n" +"Press {config.edit_config_key} to edit this config file.\n" +msgstr "" + diff --git a/bpython/translations/de/LC_MESSAGES/bpython.po b/bpython/translations/de/LC_MESSAGES/bpython.po index 951c1cc0b..e903a3ee7 100644 --- a/bpython/translations/de/LC_MESSAGES/bpython.po +++ b/bpython/translations/de/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython mercurial\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2020-10-29 12:28+0100\n" +"POT-Creation-Date: 2021-01-01 15:21+0100\n" "PO-Revision-Date: 2020-10-29 12:26+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: de\n" @@ -51,35 +51,35 @@ msgid "" "script." msgstr "" -#: bpython/cli.py:315 bpython/urwid.py:538 +#: bpython/cli.py:312 bpython/urwid.py:537 msgid "y" msgstr "j" -#: bpython/cli.py:315 bpython/urwid.py:538 +#: bpython/cli.py:312 bpython/urwid.py:537 msgid "yes" msgstr "ja" -#: bpython/cli.py:1695 +#: bpython/cli.py:1692 msgid "Rewind" msgstr "Rückgängig" -#: bpython/cli.py:1696 +#: bpython/cli.py:1693 msgid "Save" msgstr "Speichern" -#: bpython/cli.py:1697 +#: bpython/cli.py:1694 msgid "Pastebin" msgstr "" -#: bpython/cli.py:1698 +#: bpython/cli.py:1695 msgid "Pager" msgstr "" -#: bpython/cli.py:1699 +#: bpython/cli.py:1696 msgid "Show Source" msgstr "Quellcode anzeigen" -#: bpython/cli.py:1946 +#: bpython/cli.py:1943 msgid "" "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" @@ -89,19 +89,19 @@ msgstr "" "`bpython`. Diese Implementierung wird ab Version 0.19 nicht mehr aktiv " "unterstützt und wird in einer zukünftigen Version entfernt werden." -#: bpython/curtsies.py:137 +#: bpython/curtsies.py:136 msgid "log debug messages to bpython.log" msgstr "Zeichne debug Nachrichten in bpython.log auf" -#: bpython/curtsies.py:143 +#: bpython/curtsies.py:142 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:149 +#: bpython/curtsies.py:148 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:150 +#: bpython/curtsies.py:149 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "Zusätzliche Argumente spezifisch für die curtsies-basierte REPL." @@ -131,155 +131,155 @@ msgstr "Keine Ausgabe von Hilfsprogramm vorhanden." msgid "Failed to recognize the helper program's output as an URL." msgstr "Konnte Ausgabe von Hilfsprogramm nicht verarbeiten." -#: bpython/repl.py:653 +#: bpython/repl.py:657 msgid "Nothing to get source of" msgstr "Nichts um Quellcode abzurufen" -#: bpython/repl.py:658 +#: bpython/repl.py:662 #, python-format msgid "Cannot get source: %s" msgstr "Kann Quellcode nicht finden: %s" -#: bpython/repl.py:663 +#: bpython/repl.py:667 #, python-format msgid "Cannot access source of %r" msgstr "Kann auf Quellcode nicht zugreifen: %r" -#: bpython/repl.py:665 +#: bpython/repl.py:669 #, python-format msgid "No source code found for %s" msgstr "Quellcode für %s nicht gefunden" -#: bpython/repl.py:820 +#: bpython/repl.py:824 msgid "Save to file (Esc to cancel): " msgstr "In Datei speichern (Esc um abzubrechen): " -#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 +#: bpython/repl.py:826 bpython/repl.py:829 bpython/repl.py:853 msgid "Save cancelled." msgstr "Speichern abgebrochen." -#: bpython/repl.py:836 +#: bpython/repl.py:840 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "%s existiert bereit. (C) abbrechen, (o) überschrieben oder (a) anhängen? " -#: bpython/repl.py:844 +#: bpython/repl.py:848 msgid "overwrite" msgstr "überschreiben" -#: bpython/repl.py:846 +#: bpython/repl.py:850 msgid "append" msgstr "anhängen" -#: bpython/repl.py:858 bpython/repl.py:1168 +#: bpython/repl.py:862 bpython/repl.py:1169 #, python-format msgid "Error writing file '%s': %s" msgstr "Fehler beim Schreiben in Datei '%s': %s" -#: bpython/repl.py:860 +#: bpython/repl.py:864 #, python-format msgid "Saved to %s." msgstr "Nach %s gespeichert." -#: bpython/repl.py:866 +#: bpython/repl.py:870 msgid "No clipboard available." msgstr "Zwischenablage ist nicht verfügbar." -#: bpython/repl.py:873 +#: bpython/repl.py:877 msgid "Could not copy to clipboard." msgstr "Konnte nicht in Zwischenablage kopieren." -#: bpython/repl.py:875 +#: bpython/repl.py:879 msgid "Copied content to clipboard." msgstr "Inhalt wurde in Zwischenablage kopiert." -#: bpython/repl.py:884 +#: bpython/repl.py:888 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:886 +#: bpython/repl.py:890 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:894 +#: bpython/repl.py:898 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:900 +#: bpython/repl.py:904 msgid "Posting data to pastebin..." msgstr "Lade Daten hoch..." -#: bpython/repl.py:904 +#: bpython/repl.py:908 #, python-format msgid "Upload failed: %s" msgstr "Hochladen ist fehlgeschlagen: %s" -#: bpython/repl.py:913 +#: bpython/repl.py:917 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:918 +#: bpython/repl.py:922 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:957 +#: bpython/repl.py:960 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" "Wie viele Zeilen rückgängig machen? (Rückgängigmachen wird bis zu ~%.1f " "Sekunden brauchen) [1]" -#: bpython/repl.py:965 bpython/repl.py:969 +#: bpython/repl.py:968 bpython/repl.py:972 msgid "Undo canceled" msgstr "Rückgängigmachen abgebrochen" -#: bpython/repl.py:972 +#: bpython/repl.py:975 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "Mache %d Zeile rückgängig... (ungefähr %.1f Sekunden)" msgstr[1] "Mache %d Zeilen rückgängig... (ungefähr %.1f Sekunden)" -#: bpython/repl.py:1148 +#: bpython/repl.py:1151 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" "Konfigurationsdatei existiert nicht. Soll eine neue Datei erstellt " "werden? (j/N)" -#: bpython/repl.py:1178 +#: bpython/repl.py:1179 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" "bpython Konfigurationsdatei bearbeitet. Starte bpython neu damit die " "Änderungen übernommen werden." -#: bpython/repl.py:1184 +#: bpython/repl.py:1185 #, python-format msgid "Error editing config file: %s" msgstr "Fehler beim Bearbeiten der Konfigurationsdatei: %s" -#: bpython/urwid.py:605 +#: bpython/urwid.py:604 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" " <%s> Rückgängigmachen <%s> Speichern <%s> Pastebin <%s> Pager <%s> " "Quellcode anzeigen " -#: bpython/urwid.py:1115 +#: bpython/urwid.py:1114 msgid "Run twisted reactor." msgstr "Führe twisted reactor aus." -#: bpython/urwid.py:1120 +#: bpython/urwid.py:1119 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "Wähle reactor aus (siehe --help-reactors). Impliziert --twisted." -#: bpython/urwid.py:1128 +#: bpython/urwid.py:1127 msgid "List available reactors for -r." msgstr "Liste verfügbare reactors für -r auf." -#: bpython/urwid.py:1133 +#: bpython/urwid.py:1132 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." @@ -287,11 +287,11 @@ msgstr "" "Auszuführendes twistd Plugin (starte twistd für eine Liste). Verwende " "\"--\" um Optionen an das Plugin zu übergeben." -#: bpython/urwid.py:1142 +#: bpython/urwid.py:1141 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1336 +#: bpython/urwid.py:1335 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" @@ -301,53 +301,85 @@ msgstr "" "`bpython`. Diese Implementierung wird ab Version 0.19 nicht mehr aktiv " "unterstützt und wird in einer zukünftigen Version entfernt werden." -#: bpython/curtsiesfrontend/repl.py:343 +#: bpython/curtsiesfrontend/repl.py:312 msgid "Welcome to bpython!" msgstr "Willkommen by bpython!" -#: bpython/curtsiesfrontend/repl.py:345 +#: bpython/curtsiesfrontend/repl.py:314 #, python-format msgid "Press <%s> for help." msgstr "Drücke <%s> für Hilfe." -#: bpython/curtsiesfrontend/repl.py:674 +#: bpython/curtsiesfrontend/repl.py:643 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "Fehler beim Ausführen von PYTHONSTARTUP: %s" -#: bpython/curtsiesfrontend/repl.py:692 +#: bpython/curtsiesfrontend/repl.py:661 #, python-format msgid "Reloaded at %s because %s modified." msgstr "Bei %s neugeladen, da %s modifiziert wurde." -#: bpython/curtsiesfrontend/repl.py:1001 +#: bpython/curtsiesfrontend/repl.py:970 msgid "Session not reevaluated because it was not edited" msgstr "Die Sitzung wurde nicht neu ausgeführt, da sie nicht berabeitet wurde" -#: bpython/curtsiesfrontend/repl.py:1016 +#: bpython/curtsiesfrontend/repl.py:985 msgid "Session not reevaluated because saved file was blank" msgstr "Die Sitzung wurde nicht neu ausgeführt, da die gespeicherte Datei leer war" -#: bpython/curtsiesfrontend/repl.py:1026 +#: bpython/curtsiesfrontend/repl.py:995 msgid "Session edited and reevaluated" msgstr "Sitzung bearbeitet und neu ausgeführt" -#: bpython/curtsiesfrontend/repl.py:1037 +#: bpython/curtsiesfrontend/repl.py:1006 #, python-format msgid "Reloaded at %s by user." msgstr "Bei %s vom Benutzer neu geladen." -#: bpython/curtsiesfrontend/repl.py:1043 +#: bpython/curtsiesfrontend/repl.py:1012 msgid "Auto-reloading deactivated." msgstr "Automatisches Neuladen deaktiviert." -#: bpython/curtsiesfrontend/repl.py:1048 +#: bpython/curtsiesfrontend/repl.py:1017 msgid "Auto-reloading active, watching for file changes..." msgstr "Automatisches Neuladen ist aktiv; beobachte Dateiänderungen..." -#: bpython/curtsiesfrontend/repl.py:1054 +#: bpython/curtsiesfrontend/repl.py:1023 msgid "Auto-reloading not available because watchdog not installed." msgstr "" "Automatisches Neuladen ist nicht verfügbar da watchdog nicht installiert " "ist." +#: bpython/curtsiesfrontend/repl.py:1958 +msgid "" +"\n" +"Thanks for using bpython!\n" +"\n" +"See http://bpython-interpreter.org/ for more information and http://docs" +".bpython-interpreter.org/ for docs.\n" +"Please report issues at https://github.com/bpython/bpython/issues\n" +"\n" +"Features:\n" +"Try using undo ({config.undo_key})!\n" +"Edit the current line ({config.edit_current_block_key}) or the entire " +"session ({config.external_editor_key}) in an external editor. (currently " +"{config.editor})\n" +"Save sessions ({config.save_key}) or post them to pastebins " +"({config.pastebin_key})! Current pastebin helper: " +"{config.pastebin_helper}\n" +"Reload all modules and rerun session ({config.reimport_key}) to test out " +"changes to a module.\n" +"Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute " +"the current session when a module you've imported is modified.\n" +"\n" +"bpython -i your_script.py runs a file in interactive mode\n" +"bpython -t your_script.py pastes the contents of a file into the session\n" +"\n" +"A config file at {config.config_path} customizes keys and behavior of " +"bpython.\n" +"You can also set which pastebin helper and which external editor to use.\n" +"See {example_config_url} for an example config file.\n" +"Press {config.edit_config_key} to edit this config file.\n" +msgstr "" + diff --git a/bpython/translations/es_ES/LC_MESSAGES/bpython.po b/bpython/translations/es_ES/LC_MESSAGES/bpython.po index 7bb910396..526fb57ce 100644 --- a/bpython/translations/es_ES/LC_MESSAGES/bpython.po +++ b/bpython/translations/es_ES/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2020-10-29 12:28+0100\n" +"POT-Creation-Date: 2021-01-01 15:21+0100\n" "PO-Revision-Date: 2020-10-29 12:22+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: es_ES\n" @@ -48,54 +48,54 @@ msgid "" "script." msgstr "" -#: bpython/cli.py:315 bpython/urwid.py:538 +#: bpython/cli.py:312 bpython/urwid.py:537 msgid "y" msgstr "s" -#: bpython/cli.py:315 bpython/urwid.py:538 +#: bpython/cli.py:312 bpython/urwid.py:537 msgid "yes" msgstr "si" -#: bpython/cli.py:1695 +#: bpython/cli.py:1692 msgid "Rewind" msgstr "" -#: bpython/cli.py:1696 +#: bpython/cli.py:1693 msgid "Save" msgstr "" -#: bpython/cli.py:1697 +#: bpython/cli.py:1694 msgid "Pastebin" msgstr "" -#: bpython/cli.py:1698 +#: bpython/cli.py:1695 msgid "Pager" msgstr "" -#: bpython/cli.py:1699 +#: bpython/cli.py:1696 msgid "Show Source" msgstr "" -#: bpython/cli.py:1946 +#: bpython/cli.py:1943 msgid "" "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsies.py:137 +#: bpython/curtsies.py:136 msgid "log debug messages to bpython.log" msgstr "" -#: bpython/curtsies.py:143 +#: bpython/curtsies.py:142 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:149 +#: bpython/curtsies.py:148 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:150 +#: bpython/curtsies.py:149 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" @@ -125,210 +125,242 @@ msgstr "" msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:653 +#: bpython/repl.py:657 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:658 +#: bpython/repl.py:662 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:663 +#: bpython/repl.py:667 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:665 +#: bpython/repl.py:669 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:820 +#: bpython/repl.py:824 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 +#: bpython/repl.py:826 bpython/repl.py:829 bpython/repl.py:853 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:836 +#: bpython/repl.py:840 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:844 +#: bpython/repl.py:848 msgid "overwrite" msgstr "" -#: bpython/repl.py:846 +#: bpython/repl.py:850 msgid "append" msgstr "" -#: bpython/repl.py:858 bpython/repl.py:1168 +#: bpython/repl.py:862 bpython/repl.py:1169 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:860 +#: bpython/repl.py:864 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:866 +#: bpython/repl.py:870 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:873 +#: bpython/repl.py:877 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:875 +#: bpython/repl.py:879 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:884 +#: bpython/repl.py:888 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:886 +#: bpython/repl.py:890 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:894 +#: bpython/repl.py:898 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:900 +#: bpython/repl.py:904 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:908 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:913 +#: bpython/repl.py:917 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:918 +#: bpython/repl.py:922 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:957 +#: bpython/repl.py:960 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:965 bpython/repl.py:969 +#: bpython/repl.py:968 bpython/repl.py:972 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:972 +#: bpython/repl.py:975 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1148 +#: bpython/repl.py:1151 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1178 +#: bpython/repl.py:1179 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1184 +#: bpython/repl.py:1185 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:605 +#: bpython/urwid.py:604 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" " <%s> Rewind <%s> Salva <%s> Pastebin <%s> Pager <%s> Mostra el " "código fuente" -#: bpython/urwid.py:1115 +#: bpython/urwid.py:1114 msgid "Run twisted reactor." msgstr "" -#: bpython/urwid.py:1120 +#: bpython/urwid.py:1119 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "" -#: bpython/urwid.py:1128 +#: bpython/urwid.py:1127 msgid "List available reactors for -r." msgstr "" -#: bpython/urwid.py:1133 +#: bpython/urwid.py:1132 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." msgstr "" -#: bpython/urwid.py:1142 +#: bpython/urwid.py:1141 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1336 +#: bpython/urwid.py:1335 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:343 +#: bpython/curtsiesfrontend/repl.py:312 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:345 +#: bpython/curtsiesfrontend/repl.py:314 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:674 +#: bpython/curtsiesfrontend/repl.py:643 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:692 +#: bpython/curtsiesfrontend/repl.py:661 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1001 +#: bpython/curtsiesfrontend/repl.py:970 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1016 +#: bpython/curtsiesfrontend/repl.py:985 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1026 +#: bpython/curtsiesfrontend/repl.py:995 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1037 +#: bpython/curtsiesfrontend/repl.py:1006 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1043 +#: bpython/curtsiesfrontend/repl.py:1012 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1048 +#: bpython/curtsiesfrontend/repl.py:1017 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1054 +#: bpython/curtsiesfrontend/repl.py:1023 msgid "Auto-reloading not available because watchdog not installed." msgstr "" +#: bpython/curtsiesfrontend/repl.py:1958 +msgid "" +"\n" +"Thanks for using bpython!\n" +"\n" +"See http://bpython-interpreter.org/ for more information and http://docs" +".bpython-interpreter.org/ for docs.\n" +"Please report issues at https://github.com/bpython/bpython/issues\n" +"\n" +"Features:\n" +"Try using undo ({config.undo_key})!\n" +"Edit the current line ({config.edit_current_block_key}) or the entire " +"session ({config.external_editor_key}) in an external editor. (currently " +"{config.editor})\n" +"Save sessions ({config.save_key}) or post them to pastebins " +"({config.pastebin_key})! Current pastebin helper: " +"{config.pastebin_helper}\n" +"Reload all modules and rerun session ({config.reimport_key}) to test out " +"changes to a module.\n" +"Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute " +"the current session when a module you've imported is modified.\n" +"\n" +"bpython -i your_script.py runs a file in interactive mode\n" +"bpython -t your_script.py pastes the contents of a file into the session\n" +"\n" +"A config file at {config.config_path} customizes keys and behavior of " +"bpython.\n" +"You can also set which pastebin helper and which external editor to use.\n" +"See {example_config_url} for an example config file.\n" +"Press {config.edit_config_key} to edit this config file.\n" +msgstr "" + diff --git a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po index d3d00f08a..b62785548 100644 --- a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po +++ b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.13-442\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2020-10-29 12:28+0100\n" +"POT-Creation-Date: 2021-01-01 15:21+0100\n" "PO-Revision-Date: 2020-10-29 12:20+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: fr_FR\n" @@ -52,54 +52,54 @@ msgid "" "script." msgstr "" -#: bpython/cli.py:315 bpython/urwid.py:538 +#: bpython/cli.py:312 bpython/urwid.py:537 msgid "y" msgstr "o" -#: bpython/cli.py:315 bpython/urwid.py:538 +#: bpython/cli.py:312 bpython/urwid.py:537 msgid "yes" msgstr "oui" -#: bpython/cli.py:1695 +#: bpython/cli.py:1692 msgid "Rewind" msgstr "Rembobiner" -#: bpython/cli.py:1696 +#: bpython/cli.py:1693 msgid "Save" msgstr "Sauvegarder" -#: bpython/cli.py:1697 +#: bpython/cli.py:1694 msgid "Pastebin" msgstr "" -#: bpython/cli.py:1698 +#: bpython/cli.py:1695 msgid "Pager" msgstr "" -#: bpython/cli.py:1699 +#: bpython/cli.py:1696 msgid "Show Source" msgstr "Montrer le code source" -#: bpython/cli.py:1946 +#: bpython/cli.py:1943 msgid "" "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsies.py:137 +#: bpython/curtsies.py:136 msgid "log debug messages to bpython.log" msgstr "logger les messages de debug dans bpython.log" -#: bpython/curtsies.py:143 +#: bpython/curtsies.py:142 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:149 +#: bpython/curtsies.py:148 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:150 +#: bpython/curtsies.py:149 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" @@ -129,149 +129,149 @@ msgstr "pas de sortie du programme externe." msgid "Failed to recognize the helper program's output as an URL." msgstr "la sortie du programme externe ne correspond pas à une URL." -#: bpython/repl.py:653 +#: bpython/repl.py:657 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:658 +#: bpython/repl.py:662 #, python-format msgid "Cannot get source: %s" msgstr "Impossible de récupérer le source: %s" -#: bpython/repl.py:663 +#: bpython/repl.py:667 #, python-format msgid "Cannot access source of %r" msgstr "Impossible d'accéder au source de %r" -#: bpython/repl.py:665 +#: bpython/repl.py:669 #, python-format msgid "No source code found for %s" msgstr "Pas de code source trouvé pour %s" -#: bpython/repl.py:820 +#: bpython/repl.py:824 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 +#: bpython/repl.py:826 bpython/repl.py:829 bpython/repl.py:853 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:836 +#: bpython/repl.py:840 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:844 +#: bpython/repl.py:848 msgid "overwrite" msgstr "" -#: bpython/repl.py:846 +#: bpython/repl.py:850 msgid "append" msgstr "" -#: bpython/repl.py:858 bpython/repl.py:1168 +#: bpython/repl.py:862 bpython/repl.py:1169 #, python-format msgid "Error writing file '%s': %s" msgstr "Une erreur s'est produite pendant l'écriture du fichier '%s': %s" -#: bpython/repl.py:860 +#: bpython/repl.py:864 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:866 +#: bpython/repl.py:870 msgid "No clipboard available." msgstr "Pas de presse-papier disponible." -#: bpython/repl.py:873 +#: bpython/repl.py:877 msgid "Could not copy to clipboard." msgstr "Impossible de copier vers le presse-papier." -#: bpython/repl.py:875 +#: bpython/repl.py:879 msgid "Copied content to clipboard." msgstr "Contenu copié vers le presse-papier." -#: bpython/repl.py:884 +#: bpython/repl.py:888 msgid "Pastebin buffer? (y/N) " msgstr "Tampon Pastebin ? (o/N) " -#: bpython/repl.py:886 +#: bpython/repl.py:890 msgid "Pastebin aborted." msgstr "Pastebin abandonné." -#: bpython/repl.py:894 +#: bpython/repl.py:898 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "Pastebin dupliqué. URL précédente: %s. URL de suppression: %s" -#: bpython/repl.py:900 +#: bpython/repl.py:904 msgid "Posting data to pastebin..." msgstr "Envoi des donnés à pastebin..." -#: bpython/repl.py:904 +#: bpython/repl.py:908 #, python-format msgid "Upload failed: %s" msgstr "Echec du téléchargement: %s" -#: bpython/repl.py:913 +#: bpython/repl.py:917 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "URL Pastebin: %s - URL de suppression: %s" -#: bpython/repl.py:918 +#: bpython/repl.py:922 #, python-format msgid "Pastebin URL: %s" msgstr "URL Pastebin: %s" -#: bpython/repl.py:957 +#: bpython/repl.py:960 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:965 bpython/repl.py:969 +#: bpython/repl.py:968 bpython/repl.py:972 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:972 +#: bpython/repl.py:975 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1148 +#: bpython/repl.py:1151 msgid "Config file does not exist - create new from default? (y/N)" msgstr "Le fichier de configuration n'existe pas - en créér un par défaut? (o/N)" -#: bpython/repl.py:1178 +#: bpython/repl.py:1179 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1184 +#: bpython/repl.py:1185 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:605 +#: bpython/urwid.py:604 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" " <%s> Rebobiner <%s> Sauvegarder <%s> Pastebin <%s> Pager <%s> " "Montrer Source " -#: bpython/urwid.py:1115 +#: bpython/urwid.py:1114 msgid "Run twisted reactor." msgstr "Lancer le reactor twisted." -#: bpython/urwid.py:1120 +#: bpython/urwid.py:1119 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "Choisir un reactor spécifique (voir --help-reactors). Nécessite --twisted." -#: bpython/urwid.py:1128 +#: bpython/urwid.py:1127 msgid "List available reactors for -r." msgstr "Lister les reactors disponibles pour -r." -#: bpython/urwid.py:1133 +#: bpython/urwid.py:1132 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." @@ -279,62 +279,94 @@ msgstr "" "plugin twistd à lancer (utiliser twistd pour une list). Utiliser \"--\" " "pour donner plus d'options au plugin." -#: bpython/urwid.py:1142 +#: bpython/urwid.py:1141 msgid "Port to run an eval server on (forces Twisted)." msgstr "Port pour lancer un server eval (force Twisted)." -#: bpython/urwid.py:1336 +#: bpython/urwid.py:1335 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:343 +#: bpython/curtsiesfrontend/repl.py:312 msgid "Welcome to bpython!" msgstr "Bienvenue dans bpython!" -#: bpython/curtsiesfrontend/repl.py:345 +#: bpython/curtsiesfrontend/repl.py:314 #, python-format msgid "Press <%s> for help." msgstr "Appuyer sur <%s> pour de l'aide." -#: bpython/curtsiesfrontend/repl.py:674 +#: bpython/curtsiesfrontend/repl.py:643 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "L'exécution de PYTHONSTARTUP a échoué: %s" -#: bpython/curtsiesfrontend/repl.py:692 +#: bpython/curtsiesfrontend/repl.py:661 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1001 +#: bpython/curtsiesfrontend/repl.py:970 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1016 +#: bpython/curtsiesfrontend/repl.py:985 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1026 +#: bpython/curtsiesfrontend/repl.py:995 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1037 +#: bpython/curtsiesfrontend/repl.py:1006 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1043 +#: bpython/curtsiesfrontend/repl.py:1012 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1048 +#: bpython/curtsiesfrontend/repl.py:1017 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1054 +#: bpython/curtsiesfrontend/repl.py:1023 msgid "Auto-reloading not available because watchdog not installed." msgstr "" +#: bpython/curtsiesfrontend/repl.py:1958 +msgid "" +"\n" +"Thanks for using bpython!\n" +"\n" +"See http://bpython-interpreter.org/ for more information and http://docs" +".bpython-interpreter.org/ for docs.\n" +"Please report issues at https://github.com/bpython/bpython/issues\n" +"\n" +"Features:\n" +"Try using undo ({config.undo_key})!\n" +"Edit the current line ({config.edit_current_block_key}) or the entire " +"session ({config.external_editor_key}) in an external editor. (currently " +"{config.editor})\n" +"Save sessions ({config.save_key}) or post them to pastebins " +"({config.pastebin_key})! Current pastebin helper: " +"{config.pastebin_helper}\n" +"Reload all modules and rerun session ({config.reimport_key}) to test out " +"changes to a module.\n" +"Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute " +"the current session when a module you've imported is modified.\n" +"\n" +"bpython -i your_script.py runs a file in interactive mode\n" +"bpython -t your_script.py pastes the contents of a file into the session\n" +"\n" +"A config file at {config.config_path} customizes keys and behavior of " +"bpython.\n" +"You can also set which pastebin helper and which external editor to use.\n" +"See {example_config_url} for an example config file.\n" +"Press {config.edit_config_key} to edit this config file.\n" +msgstr "" + diff --git a/bpython/translations/it_IT/LC_MESSAGES/bpython.po b/bpython/translations/it_IT/LC_MESSAGES/bpython.po index 7d0a39783..46ae2a155 100644 --- a/bpython/translations/it_IT/LC_MESSAGES/bpython.po +++ b/bpython/translations/it_IT/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2020-10-29 12:28+0100\n" +"POT-Creation-Date: 2021-01-01 15:21+0100\n" "PO-Revision-Date: 2015-02-02 00:34+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: it_IT\n" @@ -48,54 +48,54 @@ msgid "" "script." msgstr "" -#: bpython/cli.py:315 bpython/urwid.py:538 +#: bpython/cli.py:312 bpython/urwid.py:537 msgid "y" msgstr "s" -#: bpython/cli.py:315 bpython/urwid.py:538 +#: bpython/cli.py:312 bpython/urwid.py:537 msgid "yes" msgstr "si" -#: bpython/cli.py:1695 +#: bpython/cli.py:1692 msgid "Rewind" msgstr "" -#: bpython/cli.py:1696 +#: bpython/cli.py:1693 msgid "Save" msgstr "" -#: bpython/cli.py:1697 +#: bpython/cli.py:1694 msgid "Pastebin" msgstr "" -#: bpython/cli.py:1698 +#: bpython/cli.py:1695 msgid "Pager" msgstr "" -#: bpython/cli.py:1699 +#: bpython/cli.py:1696 msgid "Show Source" msgstr "" -#: bpython/cli.py:1946 +#: bpython/cli.py:1943 msgid "" "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsies.py:137 +#: bpython/curtsies.py:136 msgid "log debug messages to bpython.log" msgstr "" -#: bpython/curtsies.py:143 +#: bpython/curtsies.py:142 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:149 +#: bpython/curtsies.py:148 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:150 +#: bpython/curtsies.py:149 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" @@ -125,211 +125,243 @@ msgstr "" msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:653 +#: bpython/repl.py:657 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:658 +#: bpython/repl.py:662 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:663 +#: bpython/repl.py:667 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:665 +#: bpython/repl.py:669 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:820 +#: bpython/repl.py:824 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 +#: bpython/repl.py:826 bpython/repl.py:829 bpython/repl.py:853 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:836 +#: bpython/repl.py:840 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:844 +#: bpython/repl.py:848 msgid "overwrite" msgstr "" -#: bpython/repl.py:846 +#: bpython/repl.py:850 msgid "append" msgstr "" -#: bpython/repl.py:858 bpython/repl.py:1168 +#: bpython/repl.py:862 bpython/repl.py:1169 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:860 +#: bpython/repl.py:864 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:866 +#: bpython/repl.py:870 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:873 +#: bpython/repl.py:877 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:875 +#: bpython/repl.py:879 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:884 +#: bpython/repl.py:888 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:886 +#: bpython/repl.py:890 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:894 +#: bpython/repl.py:898 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:900 +#: bpython/repl.py:904 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:908 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:913 +#: bpython/repl.py:917 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:918 +#: bpython/repl.py:922 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:957 +#: bpython/repl.py:960 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:965 bpython/repl.py:969 +#: bpython/repl.py:968 bpython/repl.py:972 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:972 +#: bpython/repl.py:975 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1148 +#: bpython/repl.py:1151 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1178 +#: bpython/repl.py:1179 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1184 +#: bpython/repl.py:1185 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:605 +#: bpython/urwid.py:604 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr " <%s> Rewind <%s> Salva <%s> Pastebin <%s> Pager <%s> Mostra Sorgente" -#: bpython/urwid.py:1115 +#: bpython/urwid.py:1114 msgid "Run twisted reactor." msgstr "" -#: bpython/urwid.py:1120 +#: bpython/urwid.py:1119 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "" -#: bpython/urwid.py:1128 +#: bpython/urwid.py:1127 msgid "List available reactors for -r." msgstr "" -#: bpython/urwid.py:1133 +#: bpython/urwid.py:1132 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." msgstr "" -#: bpython/urwid.py:1142 +#: bpython/urwid.py:1141 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1336 +#: bpython/urwid.py:1335 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:343 +#: bpython/curtsiesfrontend/repl.py:312 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:345 +#: bpython/curtsiesfrontend/repl.py:314 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:674 +#: bpython/curtsiesfrontend/repl.py:643 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:692 +#: bpython/curtsiesfrontend/repl.py:661 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1001 +#: bpython/curtsiesfrontend/repl.py:970 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1016 +#: bpython/curtsiesfrontend/repl.py:985 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1026 +#: bpython/curtsiesfrontend/repl.py:995 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1037 +#: bpython/curtsiesfrontend/repl.py:1006 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1043 +#: bpython/curtsiesfrontend/repl.py:1012 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1048 +#: bpython/curtsiesfrontend/repl.py:1017 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1054 +#: bpython/curtsiesfrontend/repl.py:1023 msgid "Auto-reloading not available because watchdog not installed." msgstr "" +#: bpython/curtsiesfrontend/repl.py:1958 +msgid "" +"\n" +"Thanks for using bpython!\n" +"\n" +"See http://bpython-interpreter.org/ for more information and http://docs" +".bpython-interpreter.org/ for docs.\n" +"Please report issues at https://github.com/bpython/bpython/issues\n" +"\n" +"Features:\n" +"Try using undo ({config.undo_key})!\n" +"Edit the current line ({config.edit_current_block_key}) or the entire " +"session ({config.external_editor_key}) in an external editor. (currently " +"{config.editor})\n" +"Save sessions ({config.save_key}) or post them to pastebins " +"({config.pastebin_key})! Current pastebin helper: " +"{config.pastebin_helper}\n" +"Reload all modules and rerun session ({config.reimport_key}) to test out " +"changes to a module.\n" +"Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute " +"the current session when a module you've imported is modified.\n" +"\n" +"bpython -i your_script.py runs a file in interactive mode\n" +"bpython -t your_script.py pastes the contents of a file into the session\n" +"\n" +"A config file at {config.config_path} customizes keys and behavior of " +"bpython.\n" +"You can also set which pastebin helper and which external editor to use.\n" +"See {example_config_url} for an example config file.\n" +"Press {config.edit_config_key} to edit this config file.\n" +msgstr "" + #~ msgid "Error editing config file." #~ msgstr "" diff --git a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po index 44f047281..be858262f 100644 --- a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po +++ b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7.1\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2020-10-29 12:28+0100\n" +"POT-Creation-Date: 2021-01-01 15:21+0100\n" "PO-Revision-Date: 2020-10-29 12:20+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: nl_NL\n" @@ -48,54 +48,54 @@ msgid "" "script." msgstr "" -#: bpython/cli.py:315 bpython/urwid.py:538 +#: bpython/cli.py:312 bpython/urwid.py:537 msgid "y" msgstr "j" -#: bpython/cli.py:315 bpython/urwid.py:538 +#: bpython/cli.py:312 bpython/urwid.py:537 msgid "yes" msgstr "ja" -#: bpython/cli.py:1695 +#: bpython/cli.py:1692 msgid "Rewind" msgstr "" -#: bpython/cli.py:1696 +#: bpython/cli.py:1693 msgid "Save" msgstr "" -#: bpython/cli.py:1697 +#: bpython/cli.py:1694 msgid "Pastebin" msgstr "" -#: bpython/cli.py:1698 +#: bpython/cli.py:1695 msgid "Pager" msgstr "" -#: bpython/cli.py:1699 +#: bpython/cli.py:1696 msgid "Show Source" msgstr "" -#: bpython/cli.py:1946 +#: bpython/cli.py:1943 msgid "" "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsies.py:137 +#: bpython/curtsies.py:136 msgid "log debug messages to bpython.log" msgstr "" -#: bpython/curtsies.py:143 +#: bpython/curtsies.py:142 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:149 +#: bpython/curtsies.py:148 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:150 +#: bpython/curtsies.py:149 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" @@ -125,208 +125,240 @@ msgstr "" msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:653 +#: bpython/repl.py:657 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:658 +#: bpython/repl.py:662 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:663 +#: bpython/repl.py:667 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:665 +#: bpython/repl.py:669 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:820 +#: bpython/repl.py:824 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 +#: bpython/repl.py:826 bpython/repl.py:829 bpython/repl.py:853 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:836 +#: bpython/repl.py:840 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:844 +#: bpython/repl.py:848 msgid "overwrite" msgstr "" -#: bpython/repl.py:846 +#: bpython/repl.py:850 msgid "append" msgstr "" -#: bpython/repl.py:858 bpython/repl.py:1168 +#: bpython/repl.py:862 bpython/repl.py:1169 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:860 +#: bpython/repl.py:864 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:866 +#: bpython/repl.py:870 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:873 +#: bpython/repl.py:877 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:875 +#: bpython/repl.py:879 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:884 +#: bpython/repl.py:888 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:886 +#: bpython/repl.py:890 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:894 +#: bpython/repl.py:898 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:900 +#: bpython/repl.py:904 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:908 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:913 +#: bpython/repl.py:917 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:918 +#: bpython/repl.py:922 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:957 +#: bpython/repl.py:960 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:965 bpython/repl.py:969 +#: bpython/repl.py:968 bpython/repl.py:972 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:972 +#: bpython/repl.py:975 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1148 +#: bpython/repl.py:1151 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1178 +#: bpython/repl.py:1179 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1184 +#: bpython/repl.py:1185 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:605 +#: bpython/urwid.py:604 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr " <%s> Rewind <%s> Opslaan <%s> Pastebin <%s> Pager <%s> Toon broncode" -#: bpython/urwid.py:1115 +#: bpython/urwid.py:1114 msgid "Run twisted reactor." msgstr "" -#: bpython/urwid.py:1120 +#: bpython/urwid.py:1119 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "" -#: bpython/urwid.py:1128 +#: bpython/urwid.py:1127 msgid "List available reactors for -r." msgstr "" -#: bpython/urwid.py:1133 +#: bpython/urwid.py:1132 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." msgstr "" -#: bpython/urwid.py:1142 +#: bpython/urwid.py:1141 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1336 +#: bpython/urwid.py:1335 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:343 +#: bpython/curtsiesfrontend/repl.py:312 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:345 +#: bpython/curtsiesfrontend/repl.py:314 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:674 +#: bpython/curtsiesfrontend/repl.py:643 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:692 +#: bpython/curtsiesfrontend/repl.py:661 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1001 +#: bpython/curtsiesfrontend/repl.py:970 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1016 +#: bpython/curtsiesfrontend/repl.py:985 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1026 +#: bpython/curtsiesfrontend/repl.py:995 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1037 +#: bpython/curtsiesfrontend/repl.py:1006 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1043 +#: bpython/curtsiesfrontend/repl.py:1012 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1048 +#: bpython/curtsiesfrontend/repl.py:1017 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1054 +#: bpython/curtsiesfrontend/repl.py:1023 msgid "Auto-reloading not available because watchdog not installed." msgstr "" +#: bpython/curtsiesfrontend/repl.py:1958 +msgid "" +"\n" +"Thanks for using bpython!\n" +"\n" +"See http://bpython-interpreter.org/ for more information and http://docs" +".bpython-interpreter.org/ for docs.\n" +"Please report issues at https://github.com/bpython/bpython/issues\n" +"\n" +"Features:\n" +"Try using undo ({config.undo_key})!\n" +"Edit the current line ({config.edit_current_block_key}) or the entire " +"session ({config.external_editor_key}) in an external editor. (currently " +"{config.editor})\n" +"Save sessions ({config.save_key}) or post them to pastebins " +"({config.pastebin_key})! Current pastebin helper: " +"{config.pastebin_helper}\n" +"Reload all modules and rerun session ({config.reimport_key}) to test out " +"changes to a module.\n" +"Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute " +"the current session when a module you've imported is modified.\n" +"\n" +"bpython -i your_script.py runs a file in interactive mode\n" +"bpython -t your_script.py pastes the contents of a file into the session\n" +"\n" +"A config file at {config.config_path} customizes keys and behavior of " +"bpython.\n" +"You can also set which pastebin helper and which external editor to use.\n" +"See {example_config_url} for an example config file.\n" +"Press {config.edit_config_key} to edit this config file.\n" +msgstr "" + From 79ff92192b18326ac3bd5441a4f7ef0aff8f7e9a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 2 Jan 2021 16:03:22 +0100 Subject: [PATCH 076/555] Use pathlib instead of low-level os module --- bpython/importcompletion.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index aca7e6602..c45cd0093 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -23,9 +23,9 @@ import fnmatch import importlib.machinery -import os import sys import warnings +from pathlib import Path from .line import ( current_word, @@ -129,33 +129,27 @@ def complete(self, cursor_offset, line): def find_modules(self, path): """Find all modules (and packages) for a given directory.""" - if not os.path.isdir(path): + if not path.is_dir(): # Perhaps a zip file return - basepath = os.path.basename(path) - if any(fnmatch.fnmatch(basepath, entry) for entry in self.skiplist): + if any(fnmatch.fnmatch(path.name, entry) for entry in self.skiplist): # Path is on skiplist return - try: - filenames = os.listdir(path) - except OSError: - filenames = [] - - finder = importlib.machinery.FileFinder(path) - - for name in filenames: - if any(fnmatch.fnmatch(name, entry) for entry in self.skiplist): + finder = importlib.machinery.FileFinder(str(path)) + for p in path.iterdir(): + if any(fnmatch.fnmatch(p.name, entry) for entry in self.skiplist): # Path is on skiplist continue - elif not any(name.endswith(suffix) for suffix in SUFFIXES): + elif not any(p.name.endswith(suffix) for suffix in SUFFIXES): # Possibly a package - if "." in name: + if "." in p.name: continue - elif os.path.isdir(os.path.join(path, name)): + elif p.is_dir(): # Unfortunately, CPython just crashes if there is a directory # which ends with a python extension, so work around. continue + name = p.name for suffix in SUFFIXES: if name.endswith(suffix): name = name[: -len(suffix)] @@ -183,10 +177,10 @@ def find_modules(self, path): continue else: if is_package: - path_real = os.path.realpath(pathname) + path_real = Path(pathname).resolve() if path_real not in self.paths: self.paths.add(path_real) - for subname in self.find_modules(pathname): + for subname in self.find_modules(path_real): if subname != "__init__": yield f"{name}.{subname}" yield name @@ -200,8 +194,7 @@ def find_all_modules(self, path=None): path = sys.path for p in path: - if not p: - p = os.curdir + p = Path(p).resolve() if p else Path.cwd() for module in self.find_modules(p): self.modules.add(module) yield From 38cb0b954d3baf49ea115ad5237355f37df2c417 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 2 Jan 2021 17:48:06 +0100 Subject: [PATCH 077/555] Exclude doc from black --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a4df32f85..2ea7bf3e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,5 +15,6 @@ exclude = ''' | build | dist | bpython/test/fodder + | doc )/ ''' From 463b247bad2f0262870cd8b1ae750d56a37f563c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 2 Jan 2021 17:48:24 +0100 Subject: [PATCH 078/555] Check with black --- .github/workflows/lint.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/lint.yaml diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 000000000..db0e1ebd2 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,20 @@ +name: Linters + +on: + push: + pull_request: + +jobs: + black: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install black + - name: Check with black + run: | + black --check . From 542d6c59cecbc2059525630f8720764d091e01d1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 2 Jan 2021 17:54:30 +0100 Subject: [PATCH 079/555] Apply black --- bpython/curtsiesfrontend/repl.py | 6 ++++-- setup.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 2d58877f4..e9965b638 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1955,7 +1955,8 @@ def help_text(self): return self.version_help_text() + "\n" + self.key_help_text() def version_help_text(self): - help_message = _(""" + help_message = _( + """ Thanks for using bpython! See http://bpython-interpreter.org/ for more information and http://docs.bpython-interpreter.org/ for docs. @@ -1975,7 +1976,8 @@ def version_help_text(self): You can also set which pastebin helper and which external editor to use. See {example_config_url} for an example config file. Press {config.edit_config_key} to edit this config file. -""").format(example_config_url=EXAMPLE_CONFIG_URL, config=self.config) +""" + ).format(example_config_url=EXAMPLE_CONFIG_URL, config=self.config) return f"bpython-curtsies version {bpython.__version__} using curtsies version {curtsies.__version__}\n{help_message}" diff --git a/setup.py b/setup.py index 64421fb31..be76c05d7 100755 --- a/setup.py +++ b/setup.py @@ -94,7 +94,13 @@ def git_describe_to_python_version(version): try: # get version from existing version file with open(version_file) as vf: - version = vf.read().strip().split("=")[-1].replace("'", "").replace("\"", "") + version = ( + vf.read() + .strip() + .split("=")[-1] + .replace("'", "") + .replace('"', "") + ) version = version.strip() except OSError: pass @@ -113,7 +119,7 @@ def git_describe_to_python_version(version): with open(version_file, "w") as vf: vf.write("# Auto-generated file, do not edit!\n") - vf.write(f"__version__ = \"{version}\"\n") + vf.write(f'__version__ = "{version}"\n') cmdclass = {"build": build} From 3f7eb13c7c20ddf796f19f4619327e19acfdf612 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 2 Jan 2021 19:24:01 +0100 Subject: [PATCH 080/555] Add more tests for import completion Related to #844 --- bpython/test/test_importcompletion.py | 33 ++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index ee9651a7b..ccd1ff4ba 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -15,6 +15,8 @@ def setUp(self): "zzefg", "zzabc.e", "zzabc.f", + "zzefg.a1", + "zzefg.a2" ] def test_simple_completion(self): @@ -22,12 +24,36 @@ def test_simple_completion(self): self.module_gatherer.complete(10, "import zza"), {"zzabc", "zzabd"} ) - def test_package_completion(self): + def test_import_empty(self): self.assertSetEqual( self.module_gatherer.complete(13, "import zzabc."), {"zzabc.e", "zzabc.f"}, ) + def test_import(self): + self.assertSetEqual( + self.module_gatherer.complete(14, "import zzefg.a"), + {"zzefg.a1", "zzefg.a2"}, + ) + + + @unittest.expectedFailure + def test_import_empty(self): + self.assertSetEqual( + self.module_gatherer.complete(7, "import "), {"zzabc", "zzabd", "zzefg"} + ) + + @unittest.expectedFailure + def test_from_import_empty(self): + self.assertSetEqual( + self.module_gatherer.complete(18, "from zzabc import "), {"e", "f"} + ) + + def test_from_import(self): + self.assertSetEqual( + self.module_gatherer.complete(19, "from zzefg import a"), {"a1", "a2"} + ) + class TestRealComplete(unittest.TestCase): def setUp(self): @@ -52,6 +78,11 @@ def test_from_package(self): self.module_gatherer.complete(17, "from xml import d"), {"dom"} ) + def test_from_package(self): + self.assertSetEqual( + self.module_gatherer.complete(17, "from xml import d"), {"dom"} + ) + class TestAvoidSymbolicLinks(unittest.TestCase): def setUp(self): From ee0e64b976d3665ee7591d9ac0b491c3df0c5cb9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 2 Jan 2021 19:25:04 +0100 Subject: [PATCH 081/555] Apply black --- bpython/test/test_importcompletion.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index ccd1ff4ba..3d3cf7fea 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -16,7 +16,7 @@ def setUp(self): "zzabc.e", "zzabc.f", "zzefg.a1", - "zzefg.a2" + "zzefg.a2", ] def test_simple_completion(self): @@ -36,11 +36,11 @@ def test_import(self): {"zzefg.a1", "zzefg.a2"}, ) - @unittest.expectedFailure def test_import_empty(self): self.assertSetEqual( - self.module_gatherer.complete(7, "import "), {"zzabc", "zzabd", "zzefg"} + self.module_gatherer.complete(7, "import "), + {"zzabc", "zzabd", "zzefg"}, ) @unittest.expectedFailure @@ -51,7 +51,8 @@ def test_from_import_empty(self): def test_from_import(self): self.assertSetEqual( - self.module_gatherer.complete(19, "from zzefg import a"), {"a1", "a2"} + self.module_gatherer.complete(19, "from zzefg import a"), + {"a1", "a2"}, ) From 8e73c8b6fd3343d6056cb6fc43b834997f4bddad Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 2 Jan 2021 22:53:40 +0100 Subject: [PATCH 082/555] Remove some temporary variables --- bpython/line.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index 6f1f75e16..be040d070 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -17,11 +17,10 @@ def current_word(cursor_offset, line): """the object.attribute.attribute just before or under the cursor""" pos = cursor_offset - matches = current_word_re.finditer(line) start = pos end = pos word = None - for m in matches: + for m in current_word_re.finditer(line): if m.start(1) < pos and m.end(1) >= pos: start = m.start(1) end = m.end(1) @@ -36,8 +35,7 @@ def current_word(cursor_offset, line): def current_dict_key(cursor_offset, line): """If in dictionary completion, return the current key""" - matches = current_dict_key_re.finditer(line) - for m in matches: + for m in current_dict_key_re.finditer(line): if m.start(1) <= cursor_offset and m.end(1) >= cursor_offset: return LinePart(m.start(1), m.end(1), m.group(1)) return None @@ -48,8 +46,7 @@ def current_dict_key(cursor_offset, line): def current_dict(cursor_offset, line): """If in dictionary completion, return the dict that should be used""" - matches = current_dict_re.finditer(line) - for m in matches: + for m in current_dict_re.finditer(line): if m.start(2) <= cursor_offset and m.end(2) >= cursor_offset: return LinePart(m.start(1), m.end(1), m.group(1)) return None @@ -84,9 +81,8 @@ def current_object(cursor_offset, line): if match is None: return None start, end, word = match - matches = current_object_re.finditer(word) s = "" - for m in matches: + for m in current_object_re.finditer(word): if m.end(1) + start < cursor_offset: if s: s += "." @@ -132,8 +128,7 @@ def current_from_import_from(cursor_offset, line): tokens = line.split() if not ("from" in tokens or "import" in tokens): return None - matches = current_from_import_from_re.finditer(line) - for m in matches: + for m in current_from_import_from_re.finditer(line): if (m.start(1) < cursor_offset and m.end(1) >= cursor_offset) or ( m.start(2) < cursor_offset and m.end(2) >= cursor_offset ): @@ -157,8 +152,10 @@ def current_from_import_import(cursor_offset, line): match1 = current_from_import_import_re_2.search(line[baseline.end() :]) if match1 is None: return None - matches = current_from_import_import_re_3.finditer(line[baseline.end() :]) - for m in chain((match1,), matches): + for m in chain( + (match1,), + current_from_import_import_re_3.finditer(line[baseline.end() :]), + ): start = baseline.end() + m.start(1) end = baseline.end() + m.end(1) if start < cursor_offset and end >= cursor_offset: @@ -179,8 +176,9 @@ def current_import(cursor_offset, line): match1 = current_import_re_2.search(line[baseline.end() :]) if match1 is None: return None - matches = current_import_re_3.finditer(line[baseline.end() :]) - for m in chain((match1,), matches): + for m in chain( + (match1,), current_import_re_3.finditer(line[baseline.end() :]) + ): start = baseline.end() + m.start(1) end = baseline.end() + m.end(1) if start < cursor_offset and end >= cursor_offset: @@ -192,8 +190,7 @@ def current_import(cursor_offset, line): def current_method_definition_name(cursor_offset, line): """The name of a method being defined""" - matches = current_method_definition_name_re.finditer(line) - for m in matches: + for m in current_method_definition_name_re.finditer(line): if m.start(1) <= cursor_offset and m.end(1) >= cursor_offset: return LinePart(m.start(1), m.end(1), m.group(1)) return None @@ -204,8 +201,7 @@ def current_method_definition_name(cursor_offset, line): def current_single_word(cursor_offset, line): """the un-dotted word just before or under the cursor""" - matches = current_single_word_re.finditer(line) - for m in matches: + for m in current_single_word_re.finditer(line): if m.start(1) <= cursor_offset and m.end(1) >= cursor_offset: return LinePart(m.start(1), m.end(1), m.group(1)) return None @@ -229,8 +225,7 @@ def current_dotted_attribute(cursor_offset, line): def current_expression_attribute(cursor_offset, line): """If after a dot, the attribute being completed""" # TODO replace with more general current_expression_attribute - matches = current_expression_attribute_re.finditer(line) - for m in matches: + for m in current_expression_attribute_re.finditer(line): if m.start(1) <= cursor_offset and m.end(1) >= cursor_offset: return LinePart(m.start(1), m.end(1), m.group(1)) return None From 337173156c8899b0a8516d4de3d827be0dee88a1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 3 Jan 2021 00:10:07 +0100 Subject: [PATCH 083/555] Extend importcompletion tests --- bpython/test/test_importcompletion.py | 51 ++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index 3d3cf7fea..9ac7d876b 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -23,18 +23,29 @@ def test_simple_completion(self): self.assertSetEqual( self.module_gatherer.complete(10, "import zza"), {"zzabc", "zzabd"} ) + self.assertSetEqual( + self.module_gatherer.complete(11, "import zza"), {"zzabc", "zzabd"} + ) def test_import_empty(self): self.assertSetEqual( self.module_gatherer.complete(13, "import zzabc."), {"zzabc.e", "zzabc.f"}, ) + self.assertSetEqual( + self.module_gatherer.complete(14, "import zzabc."), + {"zzabc.e", "zzabc.f"}, + ) def test_import(self): self.assertSetEqual( self.module_gatherer.complete(14, "import zzefg.a"), {"zzefg.a1", "zzefg.a2"}, ) + self.assertSetEqual( + self.module_gatherer.complete(15, "import zzefg.a"), + {"zzefg.a1", "zzefg.a2"}, + ) @unittest.expectedFailure def test_import_empty(self): @@ -42,18 +53,52 @@ def test_import_empty(self): self.module_gatherer.complete(7, "import "), {"zzabc", "zzabd", "zzefg"}, ) + self.assertSetEqual( + self.module_gatherer.complete(8, "import "), + {"zzabc", "zzabd", "zzefg"}, + ) @unittest.expectedFailure def test_from_import_empty(self): + self.assertSetEqual( + self.module_gatherer.complete(5, "from "), {"zzabc", "zzabd", "zzefg"} + ) + self.assertSetEqual( + self.module_gatherer.complete(6, "from "), {"zzabc", "zzabd", "zzefg"} + ) + + @unittest.expectedFailure + def test_from_module_import_empty(self): self.assertSetEqual( self.module_gatherer.complete(18, "from zzabc import "), {"e", "f"} ) + self.assertSetEqual( + self.module_gatherer.complete(19, "from zzabc import "), {"e", "f"} + ) + self.assertSetEqual( + self.module_gatherer.complete(19, "from zzabc import "), {"e", "f"} + ) + self.assertSetEqual( + self.module_gatherer.complete(19, "from zzabc import "), {"e", "f"} + ) - def test_from_import(self): + def test_from_module_import(self): self.assertSetEqual( self.module_gatherer.complete(19, "from zzefg import a"), {"a1", "a2"}, ) + self.assertSetEqual( + self.module_gatherer.complete(20, "from zzefg import a"), + {"a1", "a2"}, + ) + self.assertSetEqual( + self.module_gatherer.complete(20, "from zzefg import a"), + {"a1", "a2"}, + ) + self.assertSetEqual( + self.module_gatherer.complete(20, "from zzefg import a"), + {"a1", "a2"}, + ) class TestRealComplete(unittest.TestCase): @@ -150,3 +195,7 @@ def test_simple_symbolic_link_loop(self): filepaths.remove("Left.toRight") filepaths.remove(thing) self.assertFalse(filepaths) + + +if __name__ == "__main__": + unittest.main() From 2f956d6d542733af687ba1e23eb091d5d9d9641c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 3 Jan 2021 00:14:31 +0100 Subject: [PATCH 084/555] Handle more white spaces --- bpython/line.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index be040d070..2e4ac6167 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -114,7 +114,7 @@ def current_object_attribute(cursor_offset, line): current_from_import_from_re = LazyReCompile( - r"from ([\w0-9_.]*)(?:\s+import\s+([\w0-9_]+[,]?\s*)+)*" + r"from +([\w0-9_.]*)(?:\s+import\s+([\w0-9_]+[,]?\s*)+)*" ) @@ -136,9 +136,9 @@ def current_from_import_from(cursor_offset, line): return None -current_from_import_import_re_1 = LazyReCompile(r"from\s([\w0-9_.]*)\s+import") +current_from_import_import_re_1 = LazyReCompile(r"from\s+([\w0-9_.]*)\s+import") current_from_import_import_re_2 = LazyReCompile(r"([\w0-9_]+)") -current_from_import_import_re_3 = LazyReCompile(r"[,][ ]([\w0-9_]*)") +current_from_import_import_re_3 = LazyReCompile(r", *([\w0-9_]*)") def current_from_import_import(cursor_offset, line): @@ -165,7 +165,7 @@ def current_from_import_import(cursor_offset, line): current_import_re_1 = LazyReCompile(r"import") current_import_re_2 = LazyReCompile(r"([\w0-9_.]+)") -current_import_re_3 = LazyReCompile(r"[,][ ]([\w0-9_.]*)") +current_import_re_3 = LazyReCompile(r"[,][ ]*([\w0-9_.]*)") def current_import(cursor_offset, line): From 5f992edc2cef5f7bfb8a09a05326e0f78dddebdf Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 3 Jan 2021 00:26:12 +0100 Subject: [PATCH 085/555] Apply black --- bpython/test/test_importcompletion.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index 9ac7d876b..9c01766ea 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -61,10 +61,12 @@ def test_import_empty(self): @unittest.expectedFailure def test_from_import_empty(self): self.assertSetEqual( - self.module_gatherer.complete(5, "from "), {"zzabc", "zzabd", "zzefg"} + self.module_gatherer.complete(5, "from "), + {"zzabc", "zzabd", "zzefg"}, ) self.assertSetEqual( - self.module_gatherer.complete(6, "from "), {"zzabc", "zzabd", "zzefg"} + self.module_gatherer.complete(6, "from "), + {"zzabc", "zzabd", "zzefg"}, ) @unittest.expectedFailure From 6442deaa91c33252394597724abdb8b9146be77e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 3 Jan 2021 16:23:51 +0100 Subject: [PATCH 086/555] Remove useless condition --- bpython/line.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index 2e4ac6167..8d62d7b01 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -125,9 +125,6 @@ def current_from_import_from(cursor_offset, line): parts of an import: from (module) import (name1, name2) """ # TODO allow for as's - tokens = line.split() - if not ("from" in tokens or "import" in tokens): - return None for m in current_from_import_from_re.finditer(line): if (m.start(1) < cursor_offset and m.end(1) >= cursor_offset) or ( m.start(2) < cursor_offset and m.end(2) >= cursor_offset From cf98ab5152dd4c4ee6a597b13bb483de5688ec5a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 5 Jan 2021 22:39:11 +0100 Subject: [PATCH 087/555] Handle 'd' when mapping to colors (fixes #873) --- bpython/curtsiesfrontend/parse.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 5d5e661af..0c363de0e 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -3,11 +3,13 @@ from bpython.lazyre import LazyReCompile -from curtsies.termformatconstants import FG_COLORS, BG_COLORS, colors +import curtsies.termformatconstants +from curtsies.termformatconstants import FG_COLORS, BG_COLORS from curtsies.formatstring import fmtstr, FmtStr -cnames = dict(zip("krgybmcwd", colors + ("default",))) +colors = curtsies.termformatconstants.colors + ("default", ) +cnames = dict(zip("krgybmcwd", colors)) def func_for_letter(l, default="k"): From 285aa97db0542f76f3d2ae05e3be03109f2a8c4e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 5 Jan 2021 22:42:19 +0100 Subject: [PATCH 088/555] Reformat --- bpython/curtsiesfrontend/parse.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 0c363de0e..c783c39c6 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -1,15 +1,15 @@ -from functools import partial +import curtsies.termformatconstants import re -from bpython.lazyre import LazyReCompile - -import curtsies.termformatconstants -from curtsies.termformatconstants import FG_COLORS, BG_COLORS from curtsies.formatstring import fmtstr, FmtStr +from curtsies.termformatconstants import FG_COLORS, BG_COLORS +from functools import partial +from bpython.lazyre import LazyReCompile -colors = curtsies.termformatconstants.colors + ("default", ) -cnames = dict(zip("krgybmcwd", colors)) + +COLORS = curtsies.termformatconstants.colors + ("default",) +CNAMES = dict(zip("krgybmcwd", COLORS)) def func_for_letter(l, default="k"): @@ -18,13 +18,13 @@ def func_for_letter(l, default="k"): l = default elif l == "D": l = default.upper() - return partial(fmtstr, fg=cnames[l.lower()], bold=l.isupper()) + return partial(fmtstr, fg=CNAMES[l.lower()], bold=l.isupper()) def color_for_letter(l, default="k"): if l == "d": l = default - return cnames[l.lower()] + return CNAMES[l.lower()] def parse(s): @@ -46,23 +46,22 @@ def parse(s): def fs_from_match(d): atts = {} if d["fg"]: - # this isn't according to spec as I understand it if d["fg"].isupper(): d["bold"] = True # TODO figure out why boldness isn't based on presence of \x02 - color = cnames[d["fg"].lower()] + color = CNAMES[d["fg"].lower()] if color != "default": atts["fg"] = FG_COLORS[color] if d["bg"]: if d["bg"] == "I": # hack for finding the "inverse" - color = colors[ - (colors.index(color) + (len(colors) // 2)) % len(colors) + color = COLORS[ + (COLORS.index(color) + (len(COLORS) // 2)) % len(COLORS) ] else: - color = cnames[d["bg"].lower()] + color = CNAMES[d["bg"].lower()] if color != "default": atts["bg"] = BG_COLORS[color] if d["bold"]: From 12e9ce12f84e5b33ec94002812fb019cd6a253b3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 6 Jan 2021 09:38:31 +0100 Subject: [PATCH 089/555] Fix calculation of inverse color after cf98ab5152dd4c4ee6a597b13bb483de5688ec5a --- bpython/curtsiesfrontend/parse.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index c783c39c6..dacb5d6a0 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -1,15 +1,26 @@ -import curtsies.termformatconstants import re from curtsies.formatstring import fmtstr, FmtStr -from curtsies.termformatconstants import FG_COLORS, BG_COLORS +from curtsies.termformatconstants import ( + FG_COLORS, + BG_COLORS, + colors as CURTSIES_COLORS, +) from functools import partial from bpython.lazyre import LazyReCompile -COLORS = curtsies.termformatconstants.colors + ("default",) +COLORS = CURTSIES_COLORS + ("default",) CNAMES = dict(zip("krgybmcwd", COLORS)) +# hack for finding the "inverse" +INVERSE_COLORS = { + CURTSIES_COLORS[idx]: CURTSIES_COLORS[ + (idx + (len(CURTSIES_COLORS) // 2)) % len(CURTSIES_COLORS) + ] + for idx in range(len(CURTSIES_COLORS)) +} +INVERSE_COLORS["default"] = INVERSE_COLORS[CURTSIES_COLORS[0]] def func_for_letter(l, default="k"): @@ -57,9 +68,7 @@ def fs_from_match(d): if d["bg"]: if d["bg"] == "I": # hack for finding the "inverse" - color = COLORS[ - (COLORS.index(color) + (len(COLORS) // 2)) % len(COLORS) - ] + color = INVERSE_COLORS[color] else: color = CNAMES[d["bg"].lower()] if color != "default": From a82623e5a4f268c706508b72da2e7de154e91768 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 9 Jan 2021 10:50:10 +0100 Subject: [PATCH 090/555] Handle OSError again when iterating directories --- bpython/importcompletion.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index c45cd0093..fa2314479 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -136,8 +136,17 @@ def find_modules(self, path): # Path is on skiplist return + try: + # https://bugs.python.org/issue34541 + # Once we migrate to Python 3.8, we can change it back to directly iterator over + # path.iterdir(). + children = tuple(path.iterdir()) + except OSError: + # Path is not readable + return + finder = importlib.machinery.FileFinder(str(path)) - for p in path.iterdir(): + for p in children: if any(fnmatch.fnmatch(p.name, entry) for entry in self.skiplist): # Path is on skiplist continue From ee9b8256eb513533718fd14b8e3b2dc6733f30cf Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 9 Jan 2021 11:19:38 +0100 Subject: [PATCH 091/555] Store list of device ids and inodes This allows us to detect repeating paths in a more general way. --- bpython/importcompletion.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index fa2314479..3e90720f6 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -41,8 +41,7 @@ class ModuleGatherer: def __init__(self, path=None, skiplist=None): # The cached list of all known modules self.modules = set() - # List of stored paths to compare against so that real paths are not repeated - # handles symlinks not mount points + # List of (st_dev, st_ino) to compare against so that paths are not repeated self.paths = set() # Patterns to skip self.skiplist = skiplist if skiplist is not None else tuple() @@ -187,8 +186,9 @@ def find_modules(self, path): else: if is_package: path_real = Path(pathname).resolve() - if path_real not in self.paths: - self.paths.add(path_real) + stat = path_real.stat() + if (stat.st_dev, stat.st_ino) not in self.paths: + self.paths.add((stat.st_dev, stat.st_ino)) for subname in self.find_modules(path_real): if subname != "__init__": yield f"{name}.{subname}" From e4c1bd0b5a9fec2029cb36172cf9a64322de5174 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 9 Jan 2021 11:22:03 +0100 Subject: [PATCH 092/555] Test with pypy3 again --- .github/workflows/build.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ee822e154..ec62bca26 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -10,9 +10,10 @@ on: jobs: build: runs-on: ubuntu-latest + continue-on-error: ${{ matrix.python-version == 'pypy3' }} strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9, pypy3] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} From 66d9840d2d242efc6ebec70517c480824904eecb Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 9 Jan 2021 11:36:16 +0100 Subject: [PATCH 093/555] Remove unused class --- bpython/test/test_autocomplete.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 5787744c0..9b0a4e86d 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -214,16 +214,6 @@ def method(self, x): pass -class OldStyleFoo: - a = 10 - - def __init__(self): - self.b = 20 - - def method(self, x): - pass - - class Properties(Foo): @property def asserts_when_called(self): From 6438d27868927895b1767fd9073675615464fa37 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 9 Jan 2021 23:42:57 +0100 Subject: [PATCH 094/555] Add test for #847 --- bpython/test/test_importcompletion.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index 9c01766ea..1bf40e0d3 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -199,5 +199,32 @@ def test_simple_symbolic_link_loop(self): self.assertFalse(filepaths) +class TestBugReports(unittest.TestCase): + def test_issue_847(self): + with tempfile.TemporaryDirectory() as import_test_folder: + # ./xyzzy + # ./xyzzy/__init__.py + # ./xyzzy/plugh + # ./xyzzy/plugh/__init__.py + # ./xyzzy/plugh/bar.py + # ./xyzzy/plugh/foo.py + + base_path = Path(import_test_folder) + (base_path / "xyzzy" / "plugh").mkdir(parents=True) + (base_path / "xyzzy" / "__init__.py").touch() + (base_path / "xyzzy" / "plugh" / "__init__.py").touch() + (base_path / "xyzzy" / "plugh" / "bar.py").touch() + (base_path / "xyzzy" / "plugh" / "foo.py").touch() + + module_gatherer = ModuleGatherer([base_path.absolute()]) + while module_gatherer.find_coroutine(): + pass + + self.assertSetEqual( + module_gatherer.complete(17, "from xyzzy.plugh."), + {"xyzzy.plugh.bar", "xyzzy.plugh.foo"}, + ) + + if __name__ == "__main__": unittest.main() From 2c3cbd7e73c4f7be64168816be996a5daa1433e4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 10 Jan 2021 00:16:04 +0100 Subject: [PATCH 095/555] Specify loaders for FileFinder (fixes #847) Otherwise the FileFinder.find_spec does not know how to handle the file extensions. --- bpython/importcompletion.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 3e90720f6..077fac3e5 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -1,7 +1,7 @@ # The MIT License # # Copyright (c) 2009-2011 Andreas Stuehrk -# Copyright (c) 2020 Sebastian Ramacher +# Copyright (c) 2020-2021 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 @@ -35,6 +35,16 @@ ) SUFFIXES = importlib.machinery.all_suffixes() +LOADERS = ( + ( + importlib.machinery.ExtensionFileLoader, + importlib.machinery.EXTENSION_SUFFIXES, + ), + ( + importlib.machinery.SourceFileLoader, + importlib.machinery.SOURCE_SUFFIXES + ), +) class ModuleGatherer: @@ -144,7 +154,7 @@ def find_modules(self, path): # Path is not readable return - finder = importlib.machinery.FileFinder(str(path)) + finder = importlib.machinery.FileFinder(str(path), *LOADERS) for p in children: if any(fnmatch.fnmatch(p.name, entry) for entry in self.skiplist): # Path is on skiplist From 864b772be1e55c7eac5e3646bd6262f4480546c6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 10 Jan 2021 00:19:14 +0100 Subject: [PATCH 096/555] Apply black --- bpython/importcompletion.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 077fac3e5..8b0ff7bdb 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -40,10 +40,7 @@ importlib.machinery.ExtensionFileLoader, importlib.machinery.EXTENSION_SUFFIXES, ), - ( - importlib.machinery.SourceFileLoader, - importlib.machinery.SOURCE_SUFFIXES - ), + (importlib.machinery.SourceFileLoader, importlib.machinery.SOURCE_SUFFIXES), ) From fbcf8a14d3cf37ebe0e22a8ae70244bf3b447cbf Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 13 Jan 2021 22:01:54 +0100 Subject: [PATCH 097/555] GitHub Actions: Find typos with codespell --- .github/workflows/lint.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index db0e1ebd2..0be8a9a37 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -14,7 +14,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install black + pip install black codespell - name: Check with black - run: | - black --check . + run: black --check . + - name: Find typos with codespell + run: codespell --ignore-words-list="ba,te" --quiet-level=2 --skip="*.po" || true From a5011297f92ca294bab7d46db2f8bd5ceaa3fcbb Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 10 Jan 2021 22:23:34 +0100 Subject: [PATCH 098/555] Use fstring --- bpython/test/test_crashers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/test_crashers.py b/bpython/test/test_crashers.py index 4dd80e569..f6c13c790 100644 --- a/bpython/test/test_crashers.py +++ b/bpython/test/test_crashers.py @@ -91,7 +91,7 @@ def processExited(self, reason): ( sys.executable, "-m", - "bpython." + self.backend, + f"bpython.{self.backend}", "--config", str(TEST_CONFIG), ), From 16531125e54b0be0b807034543af7810d1262879 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 10 Jan 2021 22:23:41 +0100 Subject: [PATCH 099/555] Reformat --- bpython/importcompletion.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 8b0ff7bdb..f298efdbd 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -40,7 +40,10 @@ importlib.machinery.ExtensionFileLoader, importlib.machinery.EXTENSION_SUFFIXES, ), - (importlib.machinery.SourceFileLoader, importlib.machinery.SOURCE_SUFFIXES), + ( + importlib.machinery.SourceFileLoader, + importlib.machinery.SOURCE_SUFFIXES, + ), ) From 1e450492d15c902ab9fbf07ab2466f27621dcc77 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 13 Jan 2021 22:46:22 +0100 Subject: [PATCH 100/555] Turn codespell check into a seperate job and fail on errors --- .github/workflows/lint.yaml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 0be8a9a37..d856a9004 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -17,5 +17,16 @@ jobs: pip install black codespell - name: Check with black run: black --check . + + codespell: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install codespell - name: Find typos with codespell - run: codespell --ignore-words-list="ba,te" --quiet-level=2 --skip="*.po" || true + run: codespell --ignore-words-list="ba,te" --quiet-level=2 --skip="*.po" From 15447b53f97626b672ded44563e45474dc2f2839 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 13 Jan 2021 23:04:08 +0100 Subject: [PATCH 101/555] Fix spelling errors found by codespell --- .github/workflows/lint.yaml | 2 +- CHANGELOG.rst | 2 +- README.rst | 4 ++-- bpython/cli.py | 2 +- bpython/curtsiesfrontend/interaction.py | 2 +- bpython/sample-config | 2 +- doc/sphinx/source/configuration-options.rst | 2 +- doc/sphinx/source/django.rst | 6 +++--- doc/sphinx/source/man-bpython.rst | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d856a9004..ebc7c90f8 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -29,4 +29,4 @@ jobs: python -m pip install --upgrade pip pip install codespell - name: Find typos with codespell - run: codespell --ignore-words-list="ba,te" --quiet-level=2 --skip="*.po" + run: codespell --ignore-words-list="ba,te,deltion" --quiet-level=2 --skip="*.po" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a9da60571..4f46096d0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -773,7 +773,7 @@ Suggestions/bug reports/patches are welcome regarding this. ----- Well, hopefully we're one step closer to making the list sizing stuff work. I really hate doing code for that kind of thing as I -never get it quite right, but with perseverence it should end up +never get it quite right, but with perseverance it should end up being completely stable; it's not the hardest thing in the world. Various cosmetic fixes have been put in at the request of a bunch diff --git a/README.rst b/README.rst index 0ff6279e5..fdb66dd88 100644 --- a/README.rst +++ b/README.rst @@ -66,7 +66,7 @@ using the package manager. Ubuntu/Debian ~~~~~~~~~~~~~ Ubuntu/Debian family Linux users and install bpython using the apt package manager, using the -command with sudo priviledge: +command with sudo privilege: .. code-block:: bash @@ -108,7 +108,7 @@ Then you should invoke a program called ``bpython-curses.exe`` instead of ``bpyt Mac OS ~~~~~~ Like Windows, Mac OS does not include a package manager by default. If you have installed any -third-party pacakge manager like MacPorts, you can install it via +third-party package manager like MacPorts, you can install it via .. code-block:: bash diff --git a/bpython/cli.py b/bpython/cli.py index 43180bb0b..6d570c613 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -524,7 +524,7 @@ def delete(self): def echo(self, s, redraw=True): """Parse and echo a formatted string with appropriate attributes. It uses the formatting method as defined in formatter.py to parse the - srings. It won't update the screen if it's reevaluating the code (as it + strings. It won't update the screen if it's reevaluating the code (as it does with undo).""" a = get_colpair(self.config, "output") if "\x01" in s: diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index 7820c7c87..f7265dcc8 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -57,7 +57,7 @@ def pop_permanent_message(self, msg): if msg in self.permanent_stack: self.permanent_stack.remove(msg) else: - raise ValueError("Messsage %r was not in permanent_stack" % msg) + raise ValueError("Message %r was not in permanent_stack" % msg) @property def has_focus(self): diff --git a/bpython/sample-config b/bpython/sample-config index af11e4172..b61dce5b4 100644 --- a/bpython/sample-config +++ b/bpython/sample-config @@ -37,7 +37,7 @@ # External editor to use for editing the current line, block, or full history # Examples: vi (vim) -# code --wait (VS Code) - in VS Code use the command pallete to: +# code --wait (VS Code) - in VS Code use the command palette to: # Shell Command: Install 'code' command in PATH # atom -nw (Atom) # Default is to try $EDITOR and $VISUAL, then vi - but if you uncomment diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst index 4bc688207..2a1bbbe6c 100644 --- a/doc/sphinx/source/configuration-options.rst +++ b/doc/sphinx/source/configuration-options.rst @@ -402,7 +402,7 @@ yank_from_buffer ^^^^^^^^^^^^^^^^ Default: C-y -Pastes the current line from the buffer (the one you previously cutted) +Pastes the current line from the buffer (the one you previously cut) CLI --- diff --git a/doc/sphinx/source/django.rst b/doc/sphinx/source/django.rst index 443649505..c9535c4a8 100644 --- a/doc/sphinx/source/django.rst +++ b/doc/sphinx/source/django.rst @@ -12,15 +12,15 @@ out of the box models and views for a lot of stuff. For those people wanting to use bpython with their Django installation you can follow the following steps. Written by Chanita Siridechkun. The following instructions make bpython try to import a setting module in the current folder -and let django set up its enviroment with the settings module (if found) if -bpython can't find the settings module nothing happens and no enviroment gets +and let django set up its environment with the settings module (if found) if +bpython can't find the settings module nothing happens and no environment gets set up. The addition also checks if settings contains a PINAX_ROOT (if you use Pinax), if it finds this key it will do some additional Pinax setup. The Pinax addition was written by Skylar Saveland. -bpython uses something called the PYTHONSTARTUP enviroment variable. This is +bpython uses something called the PYTHONSTARTUP environment variable. This is also used by the vanilla Python REPL. Add the following lines to your ``.profile`` or equivalent file on your operating diff --git a/doc/sphinx/source/man-bpython.rst b/doc/sphinx/source/man-bpython.rst index 951300c6c..463aa6018 100644 --- a/doc/sphinx/source/man-bpython.rst +++ b/doc/sphinx/source/man-bpython.rst @@ -19,7 +19,7 @@ The idea is to provide the user with all the features in-line, much like modern IDEs, but in a simple, lightweight package that can be run in a terminal window. In-line syntax highlighting. - Hilights commands as you type! + Highlights commands as you type! Readline-like autocomplete with suggestions displayed as you type. Press tab to complete expressions when there's only one suggestion. From dfdecfc3bcc95fbd160b1c1a951b678ff51037d2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 13 Jan 2021 23:21:16 +0100 Subject: [PATCH 102/555] Use codespell-project/actions-codespell --- .github/workflows/lint.yaml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index ebc7c90f8..adddd5d5a 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -22,11 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install codespell - - name: Find typos with codespell - run: codespell --ignore-words-list="ba,te,deltion" --quiet-level=2 --skip="*.po" + - uses: codespell-project/actions-codespell@master + with: + skip: '*.po' + ignore_words_list: ba,te,deltion From e38c4785476df93c6400a462d68cb903f4b8525f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 16 Jan 2021 22:33:26 +0100 Subject: [PATCH 103/555] Sort imports --- bpython/curtsiesfrontend/repl.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index e9965b638..01307770c 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1,3 +1,4 @@ +import blessings import contextlib import errno import greenlet @@ -17,8 +18,6 @@ from wcwidth import wcswidth -import blessings - import curtsies from curtsies import FSArray, fmtstr, FmtStr, Termmode from curtsies import fmtfuncs From 275a6e7dd4b547f401c16138c0d1fc0dd2e03592 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 16 Jan 2021 23:03:04 +0100 Subject: [PATCH 104/555] Only replace importers not from six (fixes #874) This avoids the breakage seen in #874. Changes to six won't trigger a reload, however not breaking six is more important. The code now also only provides some functions of the underling finder/importer also does. --- bpython/curtsiesfrontend/repl.py | 85 ++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 01307770c..8f5d9ddc1 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -212,7 +212,20 @@ def __init__(self, watcher, loader): self.watcher = watcher self.loader = loader - def load_module(self, name): + def __getattr__(self, name): + if name == "create_module" and hasattr(self.loader, name): + return self._create_module + if name == "load_module" and hasattr(self.loader, name): + return self._load_module + return getattr(self.loader, name) + + def _create_module(self, spec): + spec = self.loader.create_module(spec) + if getattr(spec, "origin", None) is not None and spec.origin != "builtin": + self.watcher.track_module(spec.origin) + return spec + + def _load_module(self, name): module = self.loader.load_module(name) if hasattr(module, "__file__"): self.watcher.track_module(module.__file__) @@ -220,42 +233,30 @@ def load_module(self, name): class ImportFinder: - def __init__(self, watcher, old_meta_path): + def __init__(self, finder, watcher): self.watcher = watcher - self.old_meta_path = old_meta_path - - def find_distributions(self, context): - for finder in self.old_meta_path: - distribution_finder = getattr(finder, "find_distributions", None) - if distribution_finder is not None: - loader = finder.find_distributions(context) - if loader is not None: - return loader - - return None - - def find_spec(self, fullname, path, target=None): - for finder in self.old_meta_path: - # Consider the finder only if it implements find_spec - if getattr(finder, "find_spec", None) is None: - continue - # Attempt to find the spec - spec = finder.find_spec(fullname, path, target) - if spec is not None: - if getattr(spec, "__loader__", None) is not None: - # Patch the loader to enable reloading - spec.__loader__ = ImportLoader( - self.watcher, spec.__loader__ - ) - return spec - - def find_module(self, fullname, path=None): - for finder in self.old_meta_path: - loader = finder.find_module(fullname, path) - if loader is not None: - return ImportLoader(self.watcher, loader) - - return None + self.finder = finder + + def __getattr__(self, name): + if name == "find_spec" and hasattr(self.finder, name): + return self._find_spec + if name == "find_module" and hasattr(self.finder, name): + return self._find_module + return getattr(self.finder, name) + + def _find_spec(self, fullname, path, target=None): + # Attempt to find the spec + spec = self.finder.find_spec(fullname, path, target) + if spec is not None: + if getattr(spec, "__loader__", None) is not None: + # Patch the loader to enable reloading + spec.__loader__ = ImportLoader(self.watcher, spec.__loader__) + return spec + + def _find_module(self, fullname, path=None): + loader = self.finder.find_module(fullname, path) + if loader is not None: + return ImportLoader(self.watcher, loader) class BaseRepl(BpythonRepl): @@ -531,7 +532,17 @@ def __enter__(self): self.orig_meta_path = sys.meta_path if self.watcher: - sys.meta_path = [ImportFinder(self.watcher, self.orig_meta_path)] + meta_path = [] + for finder in sys.meta_path: + # All elements get wrapped in ImportFinder instances execepted for instances of + # _SixMetaPathImporter (from six). When importing six, it will check if the importer + # is already part of sys.meta_path and will remove instances. We do not want to + # break this feature (see also #874). + if type(finder).__name__ == "_SixMetaPathImporter": + meta_path.append(finder) + else: + meta_path.append(ImportFinder(finder, self.watcher)) + sys.meta_path = meta_path sitefix.monkeypatch_quit() return self From f74ad98d641f986b35f43be1d972b4e1b76b8cba Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 16 Jan 2021 23:06:58 +0100 Subject: [PATCH 105/555] Formatting --- bpython/curtsiesfrontend/repl.py | 5 ++++- bpython/repl.py | 4 +--- bpython/test/test_crashers.py | 5 ++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 8f5d9ddc1..9bb9edc94 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -221,7 +221,10 @@ def __getattr__(self, name): def _create_module(self, spec): spec = self.loader.create_module(spec) - if getattr(spec, "origin", None) is not None and spec.origin != "builtin": + if ( + getattr(spec, "origin", None) is not None + and spec.origin != "builtin" + ): self.watcher.track_module(spec.origin) return spec diff --git a/bpython/repl.py b/bpython/repl.py index 9939d7808..21f5942e7 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -130,9 +130,7 @@ def runsource(self, source, filename=None, symbol="single", encode="auto"): files should always already have an encoding comment or be ASCII. By default an encoding line will be added if no filename is given. - In Python 3, source must be a unicode string - In Python 2, source may be latin-1 bytestring or unicode string, - following the interface of code.InteractiveInterpreter. + source must be a string Because adding an encoding comment to a unicode string in Python 2 would cause a syntax error to be thrown which would reference code diff --git a/bpython/test/test_crashers.py b/bpython/test/test_crashers.py index f6c13c790..8988518e2 100644 --- a/bpython/test/test_crashers.py +++ b/bpython/test/test_crashers.py @@ -95,7 +95,10 @@ def processExited(self, reason): "--config", str(TEST_CONFIG), ), - env=dict(TERM="vt100", LANG=os.environ.get("LANG", "")), + env={ + "TERM": "vt100", + "LANG": os.environ.get("LANG", ""), + }, usePTY=(master, slave, os.ttyname(slave)), ) return result From be1a6de5ea5e3a06d8f01487d8809aaa24ee9c0c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 16 Jan 2021 23:16:44 +0100 Subject: [PATCH 106/555] Add some docstrings --- bpython/curtsiesfrontend/repl.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 9bb9edc94..c5b12ef25 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -208,6 +208,8 @@ def readline(self): class ImportLoader: + """Wrapper for module loaders to watch their paths with watchdog.""" + def __init__(self, watcher, loader): self.watcher = watcher self.loader = loader @@ -236,6 +238,8 @@ def _load_module(self, name): class ImportFinder: + """Wrapper for finders in sys.meta_path to replace wrap all loaders with ImportLoader.""" + def __init__(self, finder, watcher): self.watcher = watcher self.finder = finder From e8b8c686182bd7e75a7f54a92c818344327b5254 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 16 Jan 2021 23:27:23 +0100 Subject: [PATCH 107/555] Re-arrange imports --- bpython/curtsiesfrontend/repl.py | 47 ++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index c5b12ef25..1da6f5ebe 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -18,40 +18,45 @@ from wcwidth import wcswidth -import curtsies -from curtsies import FSArray, fmtstr, FmtStr, Termmode -from curtsies import fmtfuncs -from curtsies import events - -import bpython -from bpython.repl import Repl as BpythonRepl, SourceNotFound -from bpython.repl import LineTypeTranslator as LineType +from curtsies import ( + FSArray, + fmtstr, + FmtStr, + Termmode, + fmtfuncs, + events, + __version__ as curtsies_version, +) +from curtsies.configfile_keynames import keymap as key_dispatch + +from bpython import __version__ +from bpython.repl import ( + Repl as BpythonRepl, + SourceNotFound, + LineTypeTranslator as LineType, +) from bpython.config import getpreferredencoding from bpython.formatter import BPythonFormatter from bpython import autocomplete from bpython.translations import _ from bpython.pager import get_pager_command -from bpython.curtsiesfrontend import replpainter as paint -from bpython.curtsiesfrontend import sitefix -from bpython.curtsiesfrontend.coderunner import ( +from . import events as bpythonevents, sitefix, replpainter as paint +from .coderunner import ( CodeRunner, FakeOutput, is_main_thread, ) -from bpython.curtsiesfrontend.filewatch import ModuleChangedEventHandler -from bpython.curtsiesfrontend.interaction import StatusBar -from bpython.curtsiesfrontend.manual_readline import edit_keys -from bpython.curtsiesfrontend import events as bpythonevents -from bpython.curtsiesfrontend.parse import parse as bpythonparse -from bpython.curtsiesfrontend.parse import func_for_letter, color_for_letter -from bpython.curtsiesfrontend.preprocess import preprocess -from bpython.curtsiesfrontend.interpreter import ( +from .filewatch import ModuleChangedEventHandler +from .interaction import StatusBar +from .manual_readline import edit_keys +from .parse import parse as bpythonparse, func_for_letter, color_for_letter +from .preprocess import preprocess +from .interpreter import ( Interp, code_finished_will_parse, ) -from curtsies.configfile_keynames import keymap as key_dispatch logger = logging.getLogger(__name__) @@ -1996,7 +2001,7 @@ def version_help_text(self): """ ).format(example_config_url=EXAMPLE_CONFIG_URL, config=self.config) - return f"bpython-curtsies version {bpython.__version__} using curtsies version {curtsies.__version__}\n{help_message}" + return f"bpython-curtsies version {__version__} using curtsies version {curtsies_version}\n{help_message}" def key_help_text(self): NOT_IMPLEMENTED = ( From d412b5e9a790e10fce8bb1267efb9301e1aad439 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 16 Jan 2021 23:35:36 +0100 Subject: [PATCH 108/555] Update changelog --- CHANGELOG.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4f46096d0..bc6b3a921 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,10 +11,16 @@ General information: New features: * #643: Provide bpython._version if built from Github tarballs +* #849: Make import completion skip list configurable Fixes: +* #847: Fix import completion of modles * #857: Replace remaining use of deprecated imp with importlib +* #866: Add more directories to the default import completion skip list +* #873: Handle 'd' when mapping colors +* #874: Avoid breakage with six's importer + 0.20.1 ------ From 93b813326fe5465384b8d7930f9a5bc8e88517d8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 16 Jan 2021 23:39:53 +0100 Subject: [PATCH 109/555] List more fixes and new features --- CHANGELOG.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bc6b3a921..d6e3fe88c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,16 +12,21 @@ New features: * #643: Provide bpython._version if built from Github tarballs * #849: Make import completion skip list configurable +* #876: Check spelling with codespell + Thanks to Christian Clauss Fixes: * #847: Fix import completion of modles * #857: Replace remaining use of deprecated imp with importlib +* #862: Upgrade curtsies version requirements + Thanks to Kelsey Blair +* #863: State correct default config file directory + Thanks to niloct * #866: Add more directories to the default import completion skip list * #873: Handle 'd' when mapping colors * #874: Avoid breakage with six's importer - 0.20.1 ------ From 1e80189b68d22a48e549b1256653b3e1f19e5e96 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 22 Jan 2021 16:10:54 +0100 Subject: [PATCH 110/555] Set supported Python versions in setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index be76c05d7..9a657fcba 100755 --- a/setup.py +++ b/setup.py @@ -225,6 +225,7 @@ def git_describe_to_python_version(version): long_description="""bpython is a fancy interface to the Python interpreter for Unix-like operating systems.""", classifiers=classifiers, + python_requires=">=3.6", install_requires=install_requires, extras_require=extras_require, packages=packages, From e248821bc249a0c845f1e1cb80c0d37b42d80b85 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 17 Jan 2021 10:21:41 +0100 Subject: [PATCH 111/555] Use is_main_thread from curtsies.input --- bpython/curtsiesfrontend/coderunner.py | 7 ++----- bpython/curtsiesfrontend/repl.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/bpython/curtsiesfrontend/coderunner.py b/bpython/curtsiesfrontend/coderunner.py index 8baabf35c..cddf1169d 100644 --- a/bpython/curtsiesfrontend/coderunner.py +++ b/bpython/curtsiesfrontend/coderunner.py @@ -14,13 +14,10 @@ import greenlet import logging import signal -import threading - -logger = logging.getLogger(__name__) +from curtsies.input import is_main_thread -def is_main_thread(): - return threading.main_thread() == threading.current_thread() +logger = logging.getLogger(__name__) class SigintHappened: diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 1da6f5ebe..24ce7cc87 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -28,6 +28,7 @@ __version__ as curtsies_version, ) from curtsies.configfile_keynames import keymap as key_dispatch +from curtsies.input import is_main_thread from bpython import __version__ from bpython.repl import ( @@ -45,7 +46,6 @@ from .coderunner import ( CodeRunner, FakeOutput, - is_main_thread, ) from .filewatch import ModuleChangedEventHandler from .interaction import StatusBar From 3510efcd599ccca066a6a9c0a84f5504d54608a9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 17 Jan 2021 12:00:13 +0100 Subject: [PATCH 112/555] Use super --- bpython/repl.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 21f5942e7..7c827602a 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -110,9 +110,7 @@ def __init__(self, locals=None, encoding=None): sys.modules["__main__"] = main_mod = ModuleType("__main__") locals = main_mod.__dict__ - # Unfortunately code.InteractiveInterpreter is a classic class, so no - # super() - code.InteractiveInterpreter.__init__(self, locals) + super().__init__(locals) self.timer = RuntimeTimer() def reset_running_time(self): From 7a6752343950011e18303e26bb52769679d33c74 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 14:59:20 +0100 Subject: [PATCH 113/555] Store setting of unicode_box --- bpython/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/config.py b/bpython/config.py index 4b7f33a45..5af10d1dd 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -288,6 +288,7 @@ def get_key_no_doublebind(command): struct.curtsies_right_arrow_completion = config.getboolean( "curtsies", "right_arrow_completion" ) + struct.unicode_box = config.getboolean("general", "unicode_box") color_scheme_name = config.get("general", "color_scheme") @@ -332,7 +333,7 @@ def get_key_no_doublebind(command): struct.autocomplete_mode = default_completion # set box drawing characters - if config.getboolean("general", "unicode_box") and supports_box_chars(): + if struct.unicode_box and supports_box_chars(): struct.left_border = "│" struct.right_border = "│" struct.top_border = "─" From dc23149d4270dc5187d7121f2ffc831cdd5597b8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 14:59:48 +0100 Subject: [PATCH 114/555] Assert cursor_row >= 0 --- bpython/curtsiesfrontend/repl.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 24ce7cc87..17ecc7419 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1576,7 +1576,7 @@ def move_screen_up(current_line_start_row): + wcswidth(self.stdin.current_line[: self.stdin.cursor_offset]), width, ) - assert cursor_column >= 0, cursor_column + assert cursor_row >= 0 and cursor_column >= 0, (cursor_row, cursor_column) elif self.coderunner.running: # TODO does this ever happen? cursor_row, cursor_column = divmod( ( @@ -1585,7 +1585,8 @@ def move_screen_up(current_line_start_row): ), width, ) - assert cursor_column >= 0, ( + assert cursor_row >= 0 and cursor_column >= 0, ( + cursor_row, cursor_column, len(self.current_cursor_line), len(self.current_line), @@ -1601,7 +1602,8 @@ def move_screen_up(current_line_start_row): + self.number_of_padding_chars_on_current_cursor_line(), width, ) - assert cursor_column >= 0, ( + assert cursor_row >= 0 and cursor_column >= 0, ( + cursor_row, cursor_column, len(self.current_cursor_line), len(self.current_line), From 4364fc9c7aacd16e9f4fada884c72beea4823fc6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 15:01:00 +0100 Subject: [PATCH 115/555] Fix handling of box characters in tests --- bpython/test/test_curtsies_painting.py | 187 ++++++++++++++----------- 1 file changed, 102 insertions(+), 85 deletions(-) diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index fb82627ee..6cb978f5c 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -65,6 +65,18 @@ def assert_paint_ignoring_formatting( if cursor_row_col is not None: self.assertEqual(cursor_pos, cursor_row_col) + def process_box_characters(self, screen): + if not self.repl.config.unicode_box or not config.supports_box_chars(): + return [ + line.replace("┌", "+") + .replace("└", "+") + .replace("┘", "+") + .replace("┐", "+") + .replace("─", "-") + for line in screen + ] + return screen + class TestCurtsiesPaintingTest(CurtsiesPaintingTest): def test_history_is_cleared(self): @@ -108,22 +120,15 @@ def test_completion(self): self.repl.height, self.repl.width = (5, 32) self.repl.current_line = "an" self.cursor_offset = 2 - if config.supports_box_chars(): - screen = [ + screen = self.process_box_characters( + [ ">>> an", "┌──────────────────────────────┐", "│ and any( │", "└──────────────────────────────┘", "Welcome to bpython! Press f", ] - else: - screen = [ - ">>> an", - "+------------------------------+", - "| and any( |", - "+------------------------------+", - "Welcome to bpython! Press f", - ] + ) self.assert_paint_ignoring_formatting(screen, (0, 4)) def test_argspec(self): @@ -729,14 +734,16 @@ def test_simple(self): self.repl.current_line = "abc" self.repl.cursor_offset = 3 self.repl.process_event(".") - screen = [ - ">>> abc.", - "+------------------+", - "| aaaaaaaaaaaaaaaa |", - "| b |", - "| c |", - "+------------------+", - ] + screen = self.process_box_characters( + [ + ">>> abc.", + "┌──────────────────┐", + "│ aaaaaaaaaaaaaaaa │", + "│ b │", + "│ c │", + "└──────────────────┘", + ] + ) self.assert_paint_ignoring_formatting(screen, (0, 8)) def test_fill_screen(self): @@ -745,23 +752,25 @@ def test_fill_screen(self): self.repl.current_line = "abc" self.repl.cursor_offset = 3 self.repl.process_event(".") - screen = [ - ">>> abc.", - "+------------------+", - "| aaaaaaaaaaaaaaaa |", - "| b |", - "| c |", - "| d |", - "| e |", - "| f |", - "| g |", - "| h |", - "| i |", - "| j |", - "| k |", - "| l |", - "+------------------+", - ] + screen = self.process_box_characters( + [ + ">>> abc.", + "┌──────────────────┐", + "│ aaaaaaaaaaaaaaaa │", + "│ b │", + "│ c │", + "│ d │", + "│ e │", + "│ f │", + "│ g │", + "│ h │", + "│ i │", + "│ j │", + "│ k │", + "│ l │", + "└──────────────────┘", + ] + ) self.assert_paint_ignoring_formatting(screen, (0, 8)) def test_lower_on_screen(self): @@ -771,37 +780,41 @@ def test_lower_on_screen(self): self.repl.current_line = "abc" self.repl.cursor_offset = 3 self.repl.process_event(".") - screen = [ - ">>> abc.", - "+------------------+", - "| aaaaaaaaaaaaaaaa |", - "| b |", - "| c |", - "| d |", - "| e |", - "| f |", - "| g |", - "| h |", - "| i |", - "| j |", - "| k |", - "| l |", - "+------------------+", - ] + screen = self.process_box_characters( + [ + ">>> abc.", + "┌──────────────────┐", + "│ aaaaaaaaaaaaaaaa │", + "│ b │", + "│ c │", + "│ d │", + "│ e │", + "│ f │", + "│ g │", + "│ h │", + "│ i │", + "│ j │", + "│ k │", + "│ l │", + "└──────────────────┘", + ] + ) # behavior before issue #466 self.assert_paint_ignoring_formatting( screen, try_preserve_history_height=0 ) self.assert_paint_ignoring_formatting(screen, min_infobox_height=100) # behavior after issue #466 - screen = [ - ">>> abc.", - "+------------------+", - "| aaaaaaaaaaaaaaaa |", - "| b |", - "| c |", - "+------------------+", - ] + screen = self.process_box_characters( + [ + ">>> abc.", + "┌──────────────────┐", + "│ aaaaaaaaaaaaaaaa │", + "│ b │", + "│ c │", + "└──────────────────┘", + ] + ) self.assert_paint_ignoring_formatting(screen) def test_at_bottom_of_screen(self): @@ -811,35 +824,39 @@ def test_at_bottom_of_screen(self): self.repl.current_line = "abc" self.repl.cursor_offset = 3 self.repl.process_event(".") - screen = [ - ">>> abc.", - "+------------------+", - "| aaaaaaaaaaaaaaaa |", - "| b |", - "| c |", - "| d |", - "| e |", - "| f |", - "| g |", - "| h |", - "| i |", - "| j |", - "| k |", - "| l |", - "+------------------+", - ] + screen = self.process_box_characters( + [ + ">>> abc.", + "┌──────────────────┐", + "│ aaaaaaaaaaaaaaaa │", + "│ b │", + "│ c │", + "│ d │", + "│ e │", + "│ f │", + "│ g │", + "│ h │", + "│ i │", + "│ j │", + "│ k │", + "│ l │", + "└──────────────────┘", + ] + ) # behavior before issue #466 self.assert_paint_ignoring_formatting( screen, try_preserve_history_height=0 ) self.assert_paint_ignoring_formatting(screen, min_infobox_height=100) # behavior after issue #466 - screen = [ - ">>> abc.", - "+------------------+", - "| aaaaaaaaaaaaaaaa |", - "| b |", - "| c |", - "+------------------+", - ] + screen = self.process_box_characters( + [ + ">>> abc.", + "┌──────────────────┐", + "│ aaaaaaaaaaaaaaaa │", + "│ b │", + "│ c │", + "└──────────────────┘", + ] + ) self.assert_paint_ignoring_formatting(screen) From fa4377734bbd4e9529a49fc0d60cb6266e9133cc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 15:01:25 +0100 Subject: [PATCH 116/555] The tests require unicode, so set LANG and LC_ALL to C.UTF-8 --- bpython/test/test_curtsies_painting.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 6cb978f5c..ead71178f 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -29,7 +29,9 @@ def setup_config(): class ClearEnviron(TestCase): @classmethod def setUpClass(cls): - cls.mock_environ = mock.patch.dict("os.environ", {}, clear=True) + cls.mock_environ = mock.patch.dict( + "os.environ", {"LC_LANG": "C.UTF-8", "LANG": "C.UTF-8"}, clear=True + ) cls.mock_environ.start() TestCase.setUpClass() From 5f86b6bf9d409dfeb39569e03bd6e53746ab3b1a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 23 Jan 2021 16:29:45 +0100 Subject: [PATCH 117/555] Use cwcwidth --- bpython/curtsiesfrontend/repl.py | 2 +- requirements.txt | 1 + setup.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 17ecc7419..92f3ed2e3 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -16,7 +16,7 @@ from pygments.lexers import Python3Lexer from pygments.formatters import TerminalFormatter -from wcwidth import wcswidth +from cwcwidth import wcswidth from curtsies import ( FSArray, diff --git a/requirements.txt b/requirements.txt index 4a9e91bca..7ec4a6113 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ Pygments curtsies >=0.3.3 +cwcwidth greenlet pyxdg requests diff --git a/setup.py b/setup.py index 9a657fcba..70a6bf6b4 100755 --- a/setup.py +++ b/setup.py @@ -179,7 +179,7 @@ def git_describe_to_python_version(version): "requests", "curtsies >=0.3.3", "greenlet", - "wcwidth", + "cwcwidth", "pyxdg", ] From ad65967de3b82d8d21b0917276d62d6b83f45a45 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 15:10:08 +0100 Subject: [PATCH 118/555] Specify max length instead of splicing --- bpython/curtsiesfrontend/repl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 92f3ed2e3..4d6328433 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1573,7 +1573,7 @@ def move_screen_up(current_line_start_row): if self.stdin.has_focus: cursor_row, cursor_column = divmod( wcswidth(self.current_stdouterr_line) - + wcswidth(self.stdin.current_line[: self.stdin.cursor_offset]), + + wcswidth(self.stdin.current_line, self.stdin.cursor_offset), width, ) assert cursor_row >= 0 and cursor_column >= 0, (cursor_row, cursor_column) @@ -1597,7 +1597,7 @@ def move_screen_up(current_line_start_row): ( wcswidth(self.current_cursor_line_without_suggestion.s) - wcswidth(self.current_line) - + wcswidth(self.current_line[: self.cursor_offset]) + + wcswidth(self.current_line, self.cursor_offset) ) + self.number_of_padding_chars_on_current_cursor_line(), width, From b522be41edf9591b64411466975ec43d1aae3799 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 15:19:03 +0100 Subject: [PATCH 119/555] Apply black --- bpython/curtsiesfrontend/repl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 4d6328433..143b916a5 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1576,7 +1576,10 @@ def move_screen_up(current_line_start_row): + wcswidth(self.stdin.current_line, self.stdin.cursor_offset), width, ) - assert cursor_row >= 0 and cursor_column >= 0, (cursor_row, cursor_column) + assert cursor_row >= 0 and cursor_column >= 0, ( + cursor_row, + cursor_column, + ) elif self.coderunner.running: # TODO does this ever happen? cursor_row, cursor_column = divmod( ( From 80d7d9407259525915dad86d79d079de0d9fc052 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 16:32:20 +0100 Subject: [PATCH 120/555] Use new assert helper functions from curtsies --- README.rst | 2 +- bpython/test/test_curtsies_painting.py | 22 +++++++++++++--------- requirements.txt | 2 +- setup.py | 2 +- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index fdb66dd88..54d2e7d86 100644 --- a/README.rst +++ b/README.rst @@ -155,7 +155,7 @@ your config file as **~/.config/bpython/config** (i.e. Dependencies ============ * Pygments -* curtsies >= 0.3.3 +* curtsies >= 0.3.5 * greenlet * pyxdg * requests diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index ead71178f..c8152a3cd 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -4,7 +4,11 @@ import sys from contextlib import contextmanager -from curtsies.formatstringarray import FormatStringTest, fsarray +from curtsies.formatstringarray import ( + fsarray, + assertFSArraysEqual, + assertFSArraysEqualIgnoringFormatting, +) from curtsies.fmtfuncs import cyan, bold, green, yellow, on_magenta, red from unittest import mock @@ -41,7 +45,7 @@ def tearDownClass(cls): TestCase.tearDownClass() -class CurtsiesPaintingTest(FormatStringTest, ClearEnviron): +class CurtsiesPaintingTest(ClearEnviron): def setUp(self): class TestRepl(BaseRepl): def _request_refresh(inner_self): @@ -56,14 +60,14 @@ def locals(self): def assert_paint(self, screen, cursor_row_col): array, cursor_pos = self.repl.paint() - self.assertFSArraysEqual(array, screen) + assertFSArraysEqual(array, screen) self.assertEqual(cursor_pos, cursor_row_col) def assert_paint_ignoring_formatting( self, screen, cursor_row_col=None, **paint_kwargs ): array, cursor_pos = self.repl.paint(**paint_kwargs) - self.assertFSArraysEqualIgnoringFormatting(array, screen) + assertFSArraysEqualIgnoringFormatting(array, screen) if cursor_row_col is not None: self.assertEqual(cursor_pos, cursor_row_col) @@ -156,7 +160,7 @@ def foo(x, y, z=10): + bold(cyan("10")) + yellow(")") ] - self.assertFSArraysEqual(fsarray(array), fsarray(screen)) + assertFSArraysEqual(fsarray(array), fsarray(screen)) def test_formatted_docstring(self): actual = replpainter.formatted_docstring( @@ -165,7 +169,7 @@ def test_formatted_docstring(self): config=setup_config(), ) expected = fsarray(["Returns the results", "", "Also has side effects"]) - self.assertFSArraysEqualIgnoringFormatting(actual, expected) + assertFSArraysEqualIgnoringFormatting(actual, expected) def test_unicode_docstrings(self): "A bit of a special case in Python 2" @@ -178,7 +182,7 @@ def foo(): foo.__doc__, 40, config=setup_config() ) expected = fsarray(["åß∂ƒ"]) - self.assertFSArraysEqualIgnoringFormatting(actual, expected) + assertFSArraysEqualIgnoringFormatting(actual, expected) def test_nonsense_docstrings(self): for docstring in [ @@ -208,7 +212,7 @@ def foo(): wd = pydoc.getdoc(foo) actual = replpainter.formatted_docstring(wd, 40, config=setup_config()) expected = fsarray(["asdfåß∂ƒ"]) - self.assertFSArraysEqualIgnoringFormatting(actual, expected) + assertFSArraysEqualIgnoringFormatting(actual, expected) def test_paint_lasts_events(self): actual = replpainter.paint_last_events( @@ -219,7 +223,7 @@ def test_paint_lasts_events(self): else: expected = fsarray(["+-+", "|c|", "|b|", "+-+"]) - self.assertFSArraysEqualIgnoringFormatting(actual, expected) + assertFSArraysEqualIgnoringFormatting(actual, expected) @contextmanager diff --git a/requirements.txt b/requirements.txt index 7ec4a6113..bcdb090be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ Pygments -curtsies >=0.3.3 +curtsies >=0.3.5 cwcwidth greenlet pyxdg diff --git a/setup.py b/setup.py index 70a6bf6b4..f73ecf904 100755 --- a/setup.py +++ b/setup.py @@ -177,7 +177,7 @@ def git_describe_to_python_version(version): install_requires = [ "pygments", "requests", - "curtsies >=0.3.3", + "curtsies >=0.3.5", "greenlet", "cwcwidth", "pyxdg", From 1b0f48688486b880e1d06fa5ddfcd026c70915b3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 17:11:05 +0100 Subject: [PATCH 121/555] Specify width --- bpython/test/test_curtsies_painting.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index c8152a3cd..d5a9f3b33 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -641,6 +641,7 @@ def test_cursor_stays_at_bottom_of_screen(self): def test_unhighlight_paren_bugs(self): """two previous bugs, parent didn't highlight until next render and paren didn't unhighlight until enter""" + self.repl.width = 32 self.assertEqual(self.repl.rl_history.entries, [""]) self.enter("(") self.assertEqual(self.repl.rl_history.entries, [""]) @@ -657,7 +658,8 @@ def test_unhighlight_paren_bugs(self): [ cyan(">>> ") + on_magenta(bold(red("("))), green("... ") + on_magenta(bold(red(")"))), - ] + ], + width=32 ) self.assert_paint(screen, (1, 5)) @@ -667,7 +669,8 @@ def test_unhighlight_paren_bugs(self): [ cyan(">>> ") + yellow("("), green("... ") + yellow(")") + bold(cyan(" ")), - ] + ], + width=32 ) self.assert_paint(screen, (1, 6)) From 08024defa4ffc479e0298dfbe5d08b85e5d4e96b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 17:13:28 +0100 Subject: [PATCH 122/555] Update CHANGELOG --- CHANGELOG.rst | 6 ++++++ bpython/test/test_curtsies_painting.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d6e3fe88c..4a0448347 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,12 @@ Fixes: * #873: Handle 'd' when mapping colors * #874: Avoid breakage with six's importer +Changes to dependencies: + +* curtsies >= 0.3.5 is now required +* pyxdg is now required +* wcwidth has been replaced with cwcwidth + 0.20.1 ------ diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index d5a9f3b33..e3970a089 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -659,7 +659,7 @@ def test_unhighlight_paren_bugs(self): cyan(">>> ") + on_magenta(bold(red("("))), green("... ") + on_magenta(bold(red(")"))), ], - width=32 + width=32, ) self.assert_paint(screen, (1, 5)) @@ -670,7 +670,7 @@ def test_unhighlight_paren_bugs(self): cyan(">>> ") + yellow("("), green("... ") + yellow(")") + bold(cyan(" ")), ], - width=32 + width=32, ) self.assert_paint(screen, (1, 6)) From b1082cfb7adfa022b9d38bed68496064a60e874e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 18:23:33 +0100 Subject: [PATCH 123/555] Only set LC_ALL and LANG in test suite if not set --- bpython/test/test_crashers.py | 2 +- bpython/test/test_curtsies_painting.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/bpython/test/test_crashers.py b/bpython/test/test_crashers.py index 8988518e2..f9afe7947 100644 --- a/bpython/test/test_crashers.py +++ b/bpython/test/test_crashers.py @@ -97,7 +97,7 @@ def processExited(self, reason): ), env={ "TERM": "vt100", - "LANG": os.environ.get("LANG", ""), + "LANG": os.environ.get("LANG", "C.UTF-8"), }, usePTY=(master, slave, os.ttyname(slave)), ) diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index e3970a089..7ce91d67a 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -1,4 +1,5 @@ import itertools +import os import pydoc import string import sys @@ -34,7 +35,12 @@ class ClearEnviron(TestCase): @classmethod def setUpClass(cls): cls.mock_environ = mock.patch.dict( - "os.environ", {"LC_LANG": "C.UTF-8", "LANG": "C.UTF-8"}, clear=True + "os.environ", + { + "LC_ALL": os.environ.get("LC_ALL", "C.UTF-8"), + "LANG": os.environ.get("LANG", "C.UTF-8"), + }, + clear=True, ) cls.mock_environ.start() TestCase.setUpClass() From cb66a009bc7de7c3f3bf68ab93023432e5b28f9b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 21:25:00 +0100 Subject: [PATCH 124/555] Ensure that passed length is non-negative --- bpython/curtsiesfrontend/repl.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 143b916a5..86c2cb1d0 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1573,7 +1573,9 @@ def move_screen_up(current_line_start_row): if self.stdin.has_focus: cursor_row, cursor_column = divmod( wcswidth(self.current_stdouterr_line) - + wcswidth(self.stdin.current_line, self.stdin.cursor_offset), + + wcswidth( + self.stdin.current_line, max(0, self.stdin.cursor_offset) + ), width, ) assert cursor_row >= 0 and cursor_column >= 0, ( @@ -1600,7 +1602,7 @@ def move_screen_up(current_line_start_row): ( wcswidth(self.current_cursor_line_without_suggestion.s) - wcswidth(self.current_line) - + wcswidth(self.current_line, self.cursor_offset) + + wcswidth(self.current_line, max(0, self.cursor_offset)) ) + self.number_of_padding_chars_on_current_cursor_line(), width, From f4c0d3428731e48cf84d145c3e8f72212eab323b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 21:39:51 +0100 Subject: [PATCH 125/555] Run code directly via Python instead of InteractiveInterpreter We cannot do anything reasonable here anyway, so just let Python handle this case. --- bpython/args.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 0636dad9f..e877ac1c1 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -25,7 +25,6 @@ Module to handle command line argument parsing, for all front-ends. """ -import code import importlib.util import os import sys @@ -145,9 +144,8 @@ def callback(group): raise SystemExit if not ignore_stdin and not (sys.stdin.isatty() and sys.stdout.isatty()): - interpreter = code.InteractiveInterpreter() - interpreter.runsource(sys.stdin.read()) - raise SystemExit + # Just let Python handle this + os.execv(sys.executable, [sys.executable] + args) config = Struct() loadini(config, options.config) From 35ed73aebb2aa4b6fc8db5392da0359adb50938b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 21:46:35 +0100 Subject: [PATCH 126/555] Reenable TestExecArgs tests --- bpython/test/test_args.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py index 2115bc088..b536292f1 100644 --- a/bpython/test/test_args.py +++ b/bpython/test/test_args.py @@ -10,7 +10,9 @@ class TestExecArgs(unittest.TestCase): - @unittest.skip("test broken under pytest") + # These tests are currently not very useful. Under pytest neither stdout nor stdin are ttys, + # hence bpython.args.parse will just exectute code by falling back to Python. + def test_exec_dunder_file(self): with tempfile.NamedTemporaryFile(mode="w") as f: f.write( @@ -25,7 +27,7 @@ def test_exec_dunder_file(self): p = subprocess.Popen( [sys.executable] + ["-m", "bpython.curtsies", f.name], stderr=subprocess.PIPE, - universal_newlines=True, + text=True, ) (_, stderr) = p.communicate() @@ -36,7 +38,6 @@ def test_exec_nonascii_file(self): f.write( dedent( """\ - #!/usr/bin/env python # coding: utf-8 "你好 # nonascii" """ @@ -50,7 +51,6 @@ def test_exec_nonascii_file(self): except subprocess.CalledProcessError: self.fail("Error running module with nonascii characters") - @unittest.skip("test broken under pytest") def test_exec_nonascii_file_linenums(self): with tempfile.NamedTemporaryFile(mode="w") as f: f.write( @@ -66,7 +66,7 @@ def test_exec_nonascii_file_linenums(self): p = subprocess.Popen( [sys.executable, "-m", "bpython.curtsies", f.name], stderr=subprocess.PIPE, - universal_newlines=True, + text=True, ) (_, stderr) = p.communicate() From ee3b0e992e69f8489c2fbec019790c1f3b0761e4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 21:53:58 +0100 Subject: [PATCH 127/555] Revert to universal_newlines text was only introduced in Python 3.7. --- bpython/test/test_args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py index b536292f1..45f48de70 100644 --- a/bpython/test/test_args.py +++ b/bpython/test/test_args.py @@ -27,7 +27,7 @@ def test_exec_dunder_file(self): p = subprocess.Popen( [sys.executable] + ["-m", "bpython.curtsies", f.name], stderr=subprocess.PIPE, - text=True, + universal_newlines=True, ) (_, stderr) = p.communicate() @@ -66,7 +66,7 @@ def test_exec_nonascii_file_linenums(self): p = subprocess.Popen( [sys.executable, "-m", "bpython.curtsies", f.name], stderr=subprocess.PIPE, - text=True, + universal_newlines=True, ) (_, stderr) = p.communicate() From b333f28111844c32e2774e77999ede737d9fe60d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 22:23:28 +0100 Subject: [PATCH 128/555] Run tests with tty (fixes #869) --- bpython/test/test_args.py | 81 +++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py index 45f48de70..11eaf7322 100644 --- a/bpython/test/test_args.py +++ b/bpython/test/test_args.py @@ -1,4 +1,8 @@ +import errno +import os +import pty import re +import select import subprocess import sys import tempfile @@ -6,9 +10,58 @@ from textwrap import dedent from bpython import args +from bpython.config import getpreferredencoding from bpython.test import FixLanguageTestCase as TestCase +def run_with_tty(command): + # based on https://stackoverflow.com/questions/52954248/capture-output-as-a-tty-in-python + master_stdout, slave_stdout = pty.openpty() + master_stderr, slave_stderr = pty.openpty() + master_stdin, slave_stdin = pty.openpty() + + p = subprocess.Popen( + command, + stdout=slave_stdout, + stderr=slave_stderr, + stdin=slave_stdin, + close_fds=True, + ) + for fd in (slave_stdout, slave_stderr, slave_stdin): + os.close(fd) + + readable = [master_stdout, master_stderr] + result = {master_stdout: b"", master_stderr: b""} + try: + while readable: + ready, _, _ = select.select(readable, [], [], 1) + for fd in ready: + try: + data = os.read(fd, 512) + except OSError as e: + if e.errno != errno.EIO: + raise + # EIO means EOF on some systems + readable.remove(fd) + else: + if not data: # EOF + readable.remove(fd) + result[fd] += data + finally: + for fd in (master_stdout, master_stderr, master_stdin): + os.close(fd) + if p.poll() is None: + p.kill() + p.wait() + if p.returncode: + raise RuntimeError(f"Subprocess exited with {p.returncode}") + + return ( + result[master_stdout].decode(getpreferredencoding()), + result[master_stderr].decode(getpreferredencoding()), + ) + + class TestExecArgs(unittest.TestCase): # These tests are currently not very useful. Under pytest neither stdout nor stdin are ttys, # hence bpython.args.parse will just exectute code by falling back to Python. @@ -24,13 +77,9 @@ def test_exec_dunder_file(self): ) ) f.flush() - p = subprocess.Popen( - [sys.executable] + ["-m", "bpython.curtsies", f.name], - stderr=subprocess.PIPE, - universal_newlines=True, + _, stderr = run_with_tty( + [sys.executable] + ["-m", "bpython.curtsies", f.name] ) - (_, stderr) = p.communicate() - self.assertEqual(stderr.strip(), f.name) def test_exec_nonascii_file(self): @@ -44,33 +93,25 @@ def test_exec_nonascii_file(self): ) ) f.flush() - try: - subprocess.check_call( - [sys.executable, "-m", "bpython.curtsies", f.name] - ) - except subprocess.CalledProcessError: - self.fail("Error running module with nonascii characters") + _, stderr = run_with_tty( + [sys.executable, "-m", "bpython.curtsies", f.name], + ) + self.assertEqual(len(stderr), 0) def test_exec_nonascii_file_linenums(self): with tempfile.NamedTemporaryFile(mode="w") as f: f.write( dedent( """\ - #!/usr/bin/env python - # coding: utf-8 1/0 """ ) ) f.flush() - p = subprocess.Popen( + _, stderr = run_with_tty( [sys.executable, "-m", "bpython.curtsies", f.name], - stderr=subprocess.PIPE, - universal_newlines=True, ) - (_, stderr) = p.communicate() - - self.assertIn("line 3", clean_colors(stderr)) + self.assertIn("line 1", clean_colors(stderr)) def clean_colors(s): From f0048eb71142deea2d5ef8bdbb412f9fe8ab0e1f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 22:49:05 +0100 Subject: [PATCH 129/555] Only check exit code if no other exception was raised --- bpython/test/test_args.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py index 11eaf7322..febf28eab 100644 --- a/bpython/test/test_args.py +++ b/bpython/test/test_args.py @@ -53,8 +53,9 @@ def run_with_tty(command): if p.poll() is None: p.kill() p.wait() - if p.returncode: - raise RuntimeError(f"Subprocess exited with {p.returncode}") + + if p.returncode: + raise RuntimeError(f"Subprocess exited with {p.returncode}") return ( result[master_stdout].decode(getpreferredencoding()), From 26fc7cf03a39a34abe4c2d58ddcaf161633d96f5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 24 Jan 2021 22:50:27 +0100 Subject: [PATCH 130/555] Remove comment --- bpython/test/test_args.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bpython/test/test_args.py b/bpython/test/test_args.py index febf28eab..36f233ebc 100644 --- a/bpython/test/test_args.py +++ b/bpython/test/test_args.py @@ -64,9 +64,6 @@ def run_with_tty(command): class TestExecArgs(unittest.TestCase): - # These tests are currently not very useful. Under pytest neither stdout nor stdin are ttys, - # hence bpython.args.parse will just exectute code by falling back to Python. - def test_exec_dunder_file(self): with tempfile.NamedTemporaryFile(mode="w") as f: f.write( From bdbd814f16fab7774b72cfb57e233bff6f7a0026 Mon Sep 17 00:00:00 2001 From: supakeen Date: Mon, 25 Jan 2021 09:14:16 +0100 Subject: [PATCH 131/555] Minor changelog typo. --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4a0448347..4d4d2d396 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,7 +17,7 @@ New features: Fixes: -* #847: Fix import completion of modles +* #847: Fix import completion of modules * #857: Replace remaining use of deprecated imp with importlib * #862: Upgrade curtsies version requirements Thanks to Kelsey Blair From f8a628c222a892799fe9b35245d038a712303740 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Jan 2021 11:13:09 +0100 Subject: [PATCH 132/555] Start development of 0.22 --- CHANGELOG.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4d4d2d396..e4d2c6be8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,17 @@ Changelog ========= +0.22 +---- + +General information: + +New features: + +Fixes: + +Changes to dependencies: + 0.21 ---- From 5c45d9463f5fe49f9acb837a5ff304fac0952463 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 27 Jan 2021 23:26:11 +0100 Subject: [PATCH 133/555] Fix inclusion of themes in sdist --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 8694b9e27..a569201d3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,9 +8,9 @@ include data/org.bpython-interpreter.bpython.appdata.xml include doc/sphinx/source/conf.py include doc/sphinx/source/*.rst include doc/sphinx/source/logo.png -include *.theme include bpython/test/*.py include bpython/test/*.theme include bpython/translations/*/LC_MESSAGES/bpython.po include bpython/translations/*/LC_MESSAGES/bpython.mo include bpython/sample-config +include theme/*.theme From 49aab4118e14b8cba6231ea48d95bd657703c6f3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 28 Jan 2021 00:07:28 +0100 Subject: [PATCH 134/555] Also make sure that simplerepl.py is contained in sdist --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index a569201d3..eb5d7f2fb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,7 +5,7 @@ include LICENSE include data/bpython.png include data/org.bpython-interpreter.bpython.desktop include data/org.bpython-interpreter.bpython.appdata.xml -include doc/sphinx/source/conf.py +include doc/sphinx/source/*.py include doc/sphinx/source/*.rst include doc/sphinx/source/logo.png include bpython/test/*.py From 0a83c13661bba8b5f16e17a8e20aab133f47c27b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 14:28:00 +0100 Subject: [PATCH 135/555] Import setuptools before distutils --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f73ecf904..0aac54bdc 100755 --- a/setup.py +++ b/setup.py @@ -5,8 +5,8 @@ import re import subprocess -from distutils.command.build import build from setuptools import setup +from distutils.command.build import build try: from babel.messages import frontend as babel From 36d471c12d04e8b9731e41ecd8f40f0e34a4e24d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 14:38:31 +0100 Subject: [PATCH 136/555] Fetch all tags to avoid 'unknown' version warning during build --- .github/workflows/build.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index ec62bca26..9af5bb048 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -16,6 +16,8 @@ jobs: python-version: [3.6, 3.7, 3.8, 3.9, pypy3] steps: - uses: actions/checkout@v2 + with: + fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: From 2c5c3498fbab231f6acf2691b2f5d8814d44536a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 22:05:39 +0100 Subject: [PATCH 137/555] Call functools.wraps on method --- bpython/lazyre.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 01747f819..67a9de3dd 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import functools import re @@ -35,6 +36,7 @@ def __init__(self, regex, flags=0): self.compiled = None def compile_regex(method): + @functools.wraps(method) def _impl(self, *args, **kwargs): if self.compiled is None: self.compiled = re.compile(self.regex, self.flags) From 40460d1a8d5de1b0f697533e0fbc1da7d9e7cb4d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 22:21:47 +0100 Subject: [PATCH 138/555] Use f-string --- bpython/autocomplete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 94841e044..5eae9f7ac 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -511,7 +511,7 @@ def matches(self, cursor_offset, line, **kwargs): return None if argspec: matches = { - name + "=" + f"{name}=" for name in argspec[1][0] if isinstance(name, str) and name.startswith(r.word) } From a837cb25db4a57c9638b4ba8dd33728953f68cfd Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 22:21:56 +0100 Subject: [PATCH 139/555] Avoid a list comprehension --- bpython/autocomplete.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 5eae9f7ac..89b4abf94 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -544,8 +544,8 @@ def matches(self, cursor_offset, line, **kwargs): except EvaluationError: return set() - # strips leading dot - matches = [m[1:] for m in self.attr_lookup(obj, "", attr.word)] + # strips leading dot + matches = (m[1:] for m in self.attr_lookup(obj, "", attr.word)) return {m for m in matches if few_enough_underscores(attr.word, m)} From 23874727accfeba1f0f4e0ec23b983eace72a57e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 22:59:24 +0100 Subject: [PATCH 140/555] Fix typo --- bpython/args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/args.py b/bpython/args.py index e877ac1c1..3d5396625 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -128,7 +128,7 @@ def callback(group): "args", nargs=argparse.REMAINDER, help=_( - "File to extecute and additional arguments passed on to the executed script." + "File to execute and additional arguments passed on to the executed script." ), ) From e638475ccca615d83c0946ed48023c1a0fc8e612 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 29 Jan 2021 23:01:43 +0100 Subject: [PATCH 141/555] Update translations --- bpython/translations/bpython.pot | 98 ++++++++-------- .../translations/de/LC_MESSAGES/bpython.po | 106 +++++++++--------- .../translations/es_ES/LC_MESSAGES/bpython.po | 96 ++++++++-------- .../translations/fr_FR/LC_MESSAGES/bpython.po | 96 ++++++++-------- .../translations/it_IT/LC_MESSAGES/bpython.po | 96 ++++++++-------- .../translations/nl_NL/LC_MESSAGES/bpython.po | 96 ++++++++-------- 6 files changed, 288 insertions(+), 300 deletions(-) diff --git a/bpython/translations/bpython.pot b/bpython/translations/bpython.pot index df60dee43..7b23845d7 100644 --- a/bpython/translations/bpython.pot +++ b/bpython/translations/bpython.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: bpython 0.20.1.post128\n" +"Project-Id-Version: bpython 0.22.dev8\n" "Report-Msgid-Bugs-To: https://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-01-01 15:21+0100\n" +"POT-Creation-Date: 2021-01-29 22:59+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:92 +#: bpython/args.py:91 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -25,26 +25,24 @@ msgid "" "to the regular Python interpreter." msgstr "" -#: bpython/args.py:102 +#: bpython/args.py:101 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:108 +#: bpython/args.py:107 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:114 +#: bpython/args.py:113 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:120 +#: bpython/args.py:119 msgid "Print version and exit." msgstr "" -#: bpython/args.py:131 -msgid "" -"File to extecute and additional arguments passed on to the executed " -"script." +#: bpython/args.py:130 +msgid "File to execute and additional arguments passed on to the executed script." msgstr "" #: bpython/cli.py:312 bpython/urwid.py:537 @@ -124,125 +122,125 @@ msgstr "" msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:657 +#: bpython/repl.py:653 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:662 +#: bpython/repl.py:658 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:667 +#: bpython/repl.py:663 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:669 +#: bpython/repl.py:665 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:824 +#: bpython/repl.py:820 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:826 bpython/repl.py:829 bpython/repl.py:853 +#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:840 +#: bpython/repl.py:836 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:848 +#: bpython/repl.py:844 msgid "overwrite" msgstr "" -#: bpython/repl.py:850 +#: bpython/repl.py:846 msgid "append" msgstr "" -#: bpython/repl.py:862 bpython/repl.py:1169 +#: bpython/repl.py:858 bpython/repl.py:1165 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:864 +#: bpython/repl.py:860 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:870 +#: bpython/repl.py:866 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:877 +#: bpython/repl.py:873 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:879 +#: bpython/repl.py:875 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:888 +#: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:890 +#: bpython/repl.py:886 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:898 +#: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:900 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:908 +#: bpython/repl.py:904 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:917 +#: bpython/repl.py:913 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:922 +#: bpython/repl.py:918 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:960 +#: bpython/repl.py:956 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:968 bpython/repl.py:972 +#: bpython/repl.py:964 bpython/repl.py:968 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:975 +#: bpython/repl.py:971 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1151 +#: bpython/repl.py:1147 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1179 +#: bpython/repl.py:1175 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1185 +#: bpython/repl.py:1181 #, python-format msgid "Error editing config file: %s" msgstr "" @@ -281,55 +279,55 @@ msgid "" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:312 +#: bpython/curtsiesfrontend/repl.py:324 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:314 +#: bpython/curtsiesfrontend/repl.py:326 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:643 +#: bpython/curtsiesfrontend/repl.py:663 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:661 +#: bpython/curtsiesfrontend/repl.py:681 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:970 +#: bpython/curtsiesfrontend/repl.py:990 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:985 +#: bpython/curtsiesfrontend/repl.py:1005 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:995 +#: bpython/curtsiesfrontend/repl.py:1015 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1006 +#: bpython/curtsiesfrontend/repl.py:1026 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1012 +#: bpython/curtsiesfrontend/repl.py:1032 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1017 +#: bpython/curtsiesfrontend/repl.py:1037 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1023 +#: bpython/curtsiesfrontend/repl.py:1043 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1958 +#: bpython/curtsiesfrontend/repl.py:1985 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/de/LC_MESSAGES/bpython.po b/bpython/translations/de/LC_MESSAGES/bpython.po index e903a3ee7..70684700b 100644 --- a/bpython/translations/de/LC_MESSAGES/bpython.po +++ b/bpython/translations/de/LC_MESSAGES/bpython.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: bpython mercurial\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-01-01 15:21+0100\n" -"PO-Revision-Date: 2020-10-29 12:26+0100\n" +"POT-Creation-Date: 2021-01-29 22:59+0100\n" +"PO-Revision-Date: 2021-01-29 22:58+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: de\n" "Language-Team: de \n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:92 +#: bpython/args.py:91 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -29,26 +29,24 @@ msgstr "" "Hinweis: Wenn bpython Argumente übergeben bekommt, die nicht verstanden " "werden, wird der normale Python Interpreter ausgeführt." -#: bpython/args.py:102 +#: bpython/args.py:101 msgid "Use CONFIG instead of default config file." msgstr "Verwende CONFIG antatt der standardmäßigen Konfigurationsdatei." -#: bpython/args.py:108 +#: bpython/args.py:107 msgid "Drop to bpython shell after running file instead of exiting." msgstr "Verbleibe in bpython nach dem Ausführen der Datei." -#: bpython/args.py:114 +#: bpython/args.py:113 msgid "Don't flush the output to stdout." msgstr "Gib Ausgabe beim Beenden nicht ernaut auf stdout aus." -#: bpython/args.py:120 +#: bpython/args.py:119 msgid "Print version and exit." msgstr "Zeige Versionsinformationen an und beende." -#: bpython/args.py:131 -msgid "" -"File to extecute and additional arguments passed on to the executed " -"script." +#: bpython/args.py:130 +msgid "File to execute and additional arguments passed on to the executed script." msgstr "" #: bpython/cli.py:312 bpython/urwid.py:537 @@ -69,7 +67,7 @@ msgstr "Speichern" #: bpython/cli.py:1694 msgid "Pastebin" -msgstr "" +msgstr "Pastebin" #: bpython/cli.py:1695 msgid "Pager" @@ -99,7 +97,7 @@ msgstr "" #: bpython/curtsies.py:148 msgid "curtsies arguments" -msgstr "" +msgstr "Argumente für curtsies" #: bpython/curtsies.py:149 msgid "Additional arguments specific to the curtsies-based REPL." @@ -131,131 +129,131 @@ msgstr "Keine Ausgabe von Hilfsprogramm vorhanden." msgid "Failed to recognize the helper program's output as an URL." msgstr "Konnte Ausgabe von Hilfsprogramm nicht verarbeiten." -#: bpython/repl.py:657 +#: bpython/repl.py:653 msgid "Nothing to get source of" msgstr "Nichts um Quellcode abzurufen" -#: bpython/repl.py:662 +#: bpython/repl.py:658 #, python-format msgid "Cannot get source: %s" msgstr "Kann Quellcode nicht finden: %s" -#: bpython/repl.py:667 +#: bpython/repl.py:663 #, python-format msgid "Cannot access source of %r" msgstr "Kann auf Quellcode nicht zugreifen: %r" -#: bpython/repl.py:669 +#: bpython/repl.py:665 #, python-format msgid "No source code found for %s" msgstr "Quellcode für %s nicht gefunden" -#: bpython/repl.py:824 +#: bpython/repl.py:820 msgid "Save to file (Esc to cancel): " msgstr "In Datei speichern (Esc um abzubrechen): " -#: bpython/repl.py:826 bpython/repl.py:829 bpython/repl.py:853 +#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 msgid "Save cancelled." msgstr "Speichern abgebrochen." -#: bpython/repl.py:840 +#: bpython/repl.py:836 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "%s existiert bereit. (C) abbrechen, (o) überschrieben oder (a) anhängen? " -#: bpython/repl.py:848 +#: bpython/repl.py:844 msgid "overwrite" msgstr "überschreiben" -#: bpython/repl.py:850 +#: bpython/repl.py:846 msgid "append" msgstr "anhängen" -#: bpython/repl.py:862 bpython/repl.py:1169 +#: bpython/repl.py:858 bpython/repl.py:1165 #, python-format msgid "Error writing file '%s': %s" msgstr "Fehler beim Schreiben in Datei '%s': %s" -#: bpython/repl.py:864 +#: bpython/repl.py:860 #, python-format msgid "Saved to %s." msgstr "Nach %s gespeichert." -#: bpython/repl.py:870 +#: bpython/repl.py:866 msgid "No clipboard available." msgstr "Zwischenablage ist nicht verfügbar." -#: bpython/repl.py:877 +#: bpython/repl.py:873 msgid "Could not copy to clipboard." msgstr "Konnte nicht in Zwischenablage kopieren." -#: bpython/repl.py:879 +#: bpython/repl.py:875 msgid "Copied content to clipboard." msgstr "Inhalt wurde in Zwischenablage kopiert." -#: bpython/repl.py:888 +#: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:890 +#: bpython/repl.py:886 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:898 +#: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:900 msgid "Posting data to pastebin..." msgstr "Lade Daten hoch..." -#: bpython/repl.py:908 +#: bpython/repl.py:904 #, python-format msgid "Upload failed: %s" msgstr "Hochladen ist fehlgeschlagen: %s" -#: bpython/repl.py:917 +#: bpython/repl.py:913 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" -msgstr "" +msgstr "Pastebin URL: %s - URL zum Löschen: %s" -#: bpython/repl.py:922 +#: bpython/repl.py:918 #, python-format msgid "Pastebin URL: %s" -msgstr "" +msgstr "Pastebin URL: %s" -#: bpython/repl.py:960 +#: bpython/repl.py:956 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" "Wie viele Zeilen rückgängig machen? (Rückgängigmachen wird bis zu ~%.1f " "Sekunden brauchen) [1]" -#: bpython/repl.py:968 bpython/repl.py:972 +#: bpython/repl.py:964 bpython/repl.py:968 msgid "Undo canceled" msgstr "Rückgängigmachen abgebrochen" -#: bpython/repl.py:975 +#: bpython/repl.py:971 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "Mache %d Zeile rückgängig... (ungefähr %.1f Sekunden)" msgstr[1] "Mache %d Zeilen rückgängig... (ungefähr %.1f Sekunden)" -#: bpython/repl.py:1151 +#: bpython/repl.py:1147 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" "Konfigurationsdatei existiert nicht. Soll eine neue Datei erstellt " "werden? (j/N)" -#: bpython/repl.py:1179 +#: bpython/repl.py:1175 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" "bpython Konfigurationsdatei bearbeitet. Starte bpython neu damit die " "Änderungen übernommen werden." -#: bpython/repl.py:1185 +#: bpython/repl.py:1181 #, python-format msgid "Error editing config file: %s" msgstr "Fehler beim Bearbeiten der Konfigurationsdatei: %s" @@ -301,57 +299,57 @@ msgstr "" "`bpython`. Diese Implementierung wird ab Version 0.19 nicht mehr aktiv " "unterstützt und wird in einer zukünftigen Version entfernt werden." -#: bpython/curtsiesfrontend/repl.py:312 +#: bpython/curtsiesfrontend/repl.py:324 msgid "Welcome to bpython!" msgstr "Willkommen by bpython!" -#: bpython/curtsiesfrontend/repl.py:314 +#: bpython/curtsiesfrontend/repl.py:326 #, python-format msgid "Press <%s> for help." msgstr "Drücke <%s> für Hilfe." -#: bpython/curtsiesfrontend/repl.py:643 +#: bpython/curtsiesfrontend/repl.py:663 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "Fehler beim Ausführen von PYTHONSTARTUP: %s" -#: bpython/curtsiesfrontend/repl.py:661 +#: bpython/curtsiesfrontend/repl.py:681 #, python-format msgid "Reloaded at %s because %s modified." msgstr "Bei %s neugeladen, da %s modifiziert wurde." -#: bpython/curtsiesfrontend/repl.py:970 +#: bpython/curtsiesfrontend/repl.py:990 msgid "Session not reevaluated because it was not edited" msgstr "Die Sitzung wurde nicht neu ausgeführt, da sie nicht berabeitet wurde" -#: bpython/curtsiesfrontend/repl.py:985 +#: bpython/curtsiesfrontend/repl.py:1005 msgid "Session not reevaluated because saved file was blank" msgstr "Die Sitzung wurde nicht neu ausgeführt, da die gespeicherte Datei leer war" -#: bpython/curtsiesfrontend/repl.py:995 +#: bpython/curtsiesfrontend/repl.py:1015 msgid "Session edited and reevaluated" msgstr "Sitzung bearbeitet und neu ausgeführt" -#: bpython/curtsiesfrontend/repl.py:1006 +#: bpython/curtsiesfrontend/repl.py:1026 #, python-format msgid "Reloaded at %s by user." msgstr "Bei %s vom Benutzer neu geladen." -#: bpython/curtsiesfrontend/repl.py:1012 +#: bpython/curtsiesfrontend/repl.py:1032 msgid "Auto-reloading deactivated." msgstr "Automatisches Neuladen deaktiviert." -#: bpython/curtsiesfrontend/repl.py:1017 +#: bpython/curtsiesfrontend/repl.py:1037 msgid "Auto-reloading active, watching for file changes..." msgstr "Automatisches Neuladen ist aktiv; beobachte Dateiänderungen..." -#: bpython/curtsiesfrontend/repl.py:1023 +#: bpython/curtsiesfrontend/repl.py:1043 msgid "Auto-reloading not available because watchdog not installed." msgstr "" "Automatisches Neuladen ist nicht verfügbar da watchdog nicht installiert " "ist." -#: bpython/curtsiesfrontend/repl.py:1958 +#: bpython/curtsiesfrontend/repl.py:1985 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/es_ES/LC_MESSAGES/bpython.po b/bpython/translations/es_ES/LC_MESSAGES/bpython.po index 526fb57ce..3d5704ae7 100644 --- a/bpython/translations/es_ES/LC_MESSAGES/bpython.po +++ b/bpython/translations/es_ES/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-01-01 15:21+0100\n" +"POT-Creation-Date: 2021-01-29 22:59+0100\n" "PO-Revision-Date: 2020-10-29 12:22+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: es_ES\n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:92 +#: bpython/args.py:91 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -26,26 +26,24 @@ msgid "" "to the regular Python interpreter." msgstr "" -#: bpython/args.py:102 +#: bpython/args.py:101 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:108 +#: bpython/args.py:107 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:114 +#: bpython/args.py:113 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:120 +#: bpython/args.py:119 msgid "Print version and exit." msgstr "" -#: bpython/args.py:131 -msgid "" -"File to extecute and additional arguments passed on to the executed " -"script." +#: bpython/args.py:130 +msgid "File to execute and additional arguments passed on to the executed script." msgstr "" #: bpython/cli.py:312 bpython/urwid.py:537 @@ -125,125 +123,125 @@ msgstr "" msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:657 +#: bpython/repl.py:653 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:662 +#: bpython/repl.py:658 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:667 +#: bpython/repl.py:663 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:669 +#: bpython/repl.py:665 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:824 +#: bpython/repl.py:820 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:826 bpython/repl.py:829 bpython/repl.py:853 +#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:840 +#: bpython/repl.py:836 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:848 +#: bpython/repl.py:844 msgid "overwrite" msgstr "" -#: bpython/repl.py:850 +#: bpython/repl.py:846 msgid "append" msgstr "" -#: bpython/repl.py:862 bpython/repl.py:1169 +#: bpython/repl.py:858 bpython/repl.py:1165 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:864 +#: bpython/repl.py:860 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:870 +#: bpython/repl.py:866 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:877 +#: bpython/repl.py:873 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:879 +#: bpython/repl.py:875 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:888 +#: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:890 +#: bpython/repl.py:886 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:898 +#: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:900 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:908 +#: bpython/repl.py:904 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:917 +#: bpython/repl.py:913 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:922 +#: bpython/repl.py:918 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:960 +#: bpython/repl.py:956 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:968 bpython/repl.py:972 +#: bpython/repl.py:964 bpython/repl.py:968 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:975 +#: bpython/repl.py:971 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1151 +#: bpython/repl.py:1147 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1179 +#: bpython/repl.py:1175 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1185 +#: bpython/repl.py:1181 #, python-format msgid "Error editing config file: %s" msgstr "" @@ -284,55 +282,55 @@ msgid "" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:312 +#: bpython/curtsiesfrontend/repl.py:324 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:314 +#: bpython/curtsiesfrontend/repl.py:326 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:643 +#: bpython/curtsiesfrontend/repl.py:663 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:661 +#: bpython/curtsiesfrontend/repl.py:681 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:970 +#: bpython/curtsiesfrontend/repl.py:990 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:985 +#: bpython/curtsiesfrontend/repl.py:1005 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:995 +#: bpython/curtsiesfrontend/repl.py:1015 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1006 +#: bpython/curtsiesfrontend/repl.py:1026 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1012 +#: bpython/curtsiesfrontend/repl.py:1032 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1017 +#: bpython/curtsiesfrontend/repl.py:1037 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1023 +#: bpython/curtsiesfrontend/repl.py:1043 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1958 +#: bpython/curtsiesfrontend/repl.py:1985 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po index b62785548..b2b4888d8 100644 --- a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po +++ b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.13-442\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-01-01 15:21+0100\n" +"POT-Creation-Date: 2021-01-29 22:59+0100\n" "PO-Revision-Date: 2020-10-29 12:20+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: fr_FR\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:92 +#: bpython/args.py:91 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -28,28 +28,26 @@ msgstr "" "NOTE: Si bpython ne reconnaît pas un des arguments fournis, " "l'interpréteur Python classique sera lancé" -#: bpython/args.py:102 +#: bpython/args.py:101 msgid "Use CONFIG instead of default config file." msgstr "Utiliser CONFIG à la place du fichier de configuration par défaut." -#: bpython/args.py:108 +#: bpython/args.py:107 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" "Aller dans le shell bpython après l'exécution du fichier au lieu de " "quitter." -#: bpython/args.py:114 +#: bpython/args.py:113 msgid "Don't flush the output to stdout." msgstr "Ne pas purger la sortie vers stdout." -#: bpython/args.py:120 +#: bpython/args.py:119 msgid "Print version and exit." msgstr "Afficher la version et quitter." -#: bpython/args.py:131 -msgid "" -"File to extecute and additional arguments passed on to the executed " -"script." +#: bpython/args.py:130 +msgid "File to execute and additional arguments passed on to the executed script." msgstr "" #: bpython/cli.py:312 bpython/urwid.py:537 @@ -129,125 +127,125 @@ msgstr "pas de sortie du programme externe." msgid "Failed to recognize the helper program's output as an URL." msgstr "la sortie du programme externe ne correspond pas à une URL." -#: bpython/repl.py:657 +#: bpython/repl.py:653 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:662 +#: bpython/repl.py:658 #, python-format msgid "Cannot get source: %s" msgstr "Impossible de récupérer le source: %s" -#: bpython/repl.py:667 +#: bpython/repl.py:663 #, python-format msgid "Cannot access source of %r" msgstr "Impossible d'accéder au source de %r" -#: bpython/repl.py:669 +#: bpython/repl.py:665 #, python-format msgid "No source code found for %s" msgstr "Pas de code source trouvé pour %s" -#: bpython/repl.py:824 +#: bpython/repl.py:820 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:826 bpython/repl.py:829 bpython/repl.py:853 +#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:840 +#: bpython/repl.py:836 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:848 +#: bpython/repl.py:844 msgid "overwrite" msgstr "" -#: bpython/repl.py:850 +#: bpython/repl.py:846 msgid "append" msgstr "" -#: bpython/repl.py:862 bpython/repl.py:1169 +#: bpython/repl.py:858 bpython/repl.py:1165 #, python-format msgid "Error writing file '%s': %s" msgstr "Une erreur s'est produite pendant l'écriture du fichier '%s': %s" -#: bpython/repl.py:864 +#: bpython/repl.py:860 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:870 +#: bpython/repl.py:866 msgid "No clipboard available." msgstr "Pas de presse-papier disponible." -#: bpython/repl.py:877 +#: bpython/repl.py:873 msgid "Could not copy to clipboard." msgstr "Impossible de copier vers le presse-papier." -#: bpython/repl.py:879 +#: bpython/repl.py:875 msgid "Copied content to clipboard." msgstr "Contenu copié vers le presse-papier." -#: bpython/repl.py:888 +#: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " msgstr "Tampon Pastebin ? (o/N) " -#: bpython/repl.py:890 +#: bpython/repl.py:886 msgid "Pastebin aborted." msgstr "Pastebin abandonné." -#: bpython/repl.py:898 +#: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "Pastebin dupliqué. URL précédente: %s. URL de suppression: %s" -#: bpython/repl.py:904 +#: bpython/repl.py:900 msgid "Posting data to pastebin..." msgstr "Envoi des donnés à pastebin..." -#: bpython/repl.py:908 +#: bpython/repl.py:904 #, python-format msgid "Upload failed: %s" msgstr "Echec du téléchargement: %s" -#: bpython/repl.py:917 +#: bpython/repl.py:913 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "URL Pastebin: %s - URL de suppression: %s" -#: bpython/repl.py:922 +#: bpython/repl.py:918 #, python-format msgid "Pastebin URL: %s" msgstr "URL Pastebin: %s" -#: bpython/repl.py:960 +#: bpython/repl.py:956 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:968 bpython/repl.py:972 +#: bpython/repl.py:964 bpython/repl.py:968 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:975 +#: bpython/repl.py:971 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1151 +#: bpython/repl.py:1147 msgid "Config file does not exist - create new from default? (y/N)" msgstr "Le fichier de configuration n'existe pas - en créér un par défaut? (o/N)" -#: bpython/repl.py:1179 +#: bpython/repl.py:1175 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1185 +#: bpython/repl.py:1181 #, python-format msgid "Error editing config file: %s" msgstr "" @@ -290,55 +288,55 @@ msgid "" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:312 +#: bpython/curtsiesfrontend/repl.py:324 msgid "Welcome to bpython!" msgstr "Bienvenue dans bpython!" -#: bpython/curtsiesfrontend/repl.py:314 +#: bpython/curtsiesfrontend/repl.py:326 #, python-format msgid "Press <%s> for help." msgstr "Appuyer sur <%s> pour de l'aide." -#: bpython/curtsiesfrontend/repl.py:643 +#: bpython/curtsiesfrontend/repl.py:663 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "L'exécution de PYTHONSTARTUP a échoué: %s" -#: bpython/curtsiesfrontend/repl.py:661 +#: bpython/curtsiesfrontend/repl.py:681 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:970 +#: bpython/curtsiesfrontend/repl.py:990 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:985 +#: bpython/curtsiesfrontend/repl.py:1005 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:995 +#: bpython/curtsiesfrontend/repl.py:1015 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1006 +#: bpython/curtsiesfrontend/repl.py:1026 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1012 +#: bpython/curtsiesfrontend/repl.py:1032 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1017 +#: bpython/curtsiesfrontend/repl.py:1037 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1023 +#: bpython/curtsiesfrontend/repl.py:1043 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1958 +#: bpython/curtsiesfrontend/repl.py:1985 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/it_IT/LC_MESSAGES/bpython.po b/bpython/translations/it_IT/LC_MESSAGES/bpython.po index 46ae2a155..c4e76797b 100644 --- a/bpython/translations/it_IT/LC_MESSAGES/bpython.po +++ b/bpython/translations/it_IT/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-01-01 15:21+0100\n" +"POT-Creation-Date: 2021-01-29 22:59+0100\n" "PO-Revision-Date: 2015-02-02 00:34+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: it_IT\n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:92 +#: bpython/args.py:91 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -26,26 +26,24 @@ msgid "" "to the regular Python interpreter." msgstr "" -#: bpython/args.py:102 +#: bpython/args.py:101 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:108 +#: bpython/args.py:107 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:114 +#: bpython/args.py:113 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:120 +#: bpython/args.py:119 msgid "Print version and exit." msgstr "" -#: bpython/args.py:131 -msgid "" -"File to extecute and additional arguments passed on to the executed " -"script." +#: bpython/args.py:130 +msgid "File to execute and additional arguments passed on to the executed script." msgstr "" #: bpython/cli.py:312 bpython/urwid.py:537 @@ -125,125 +123,125 @@ msgstr "" msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:657 +#: bpython/repl.py:653 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:662 +#: bpython/repl.py:658 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:667 +#: bpython/repl.py:663 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:669 +#: bpython/repl.py:665 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:824 +#: bpython/repl.py:820 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:826 bpython/repl.py:829 bpython/repl.py:853 +#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:840 +#: bpython/repl.py:836 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:848 +#: bpython/repl.py:844 msgid "overwrite" msgstr "" -#: bpython/repl.py:850 +#: bpython/repl.py:846 msgid "append" msgstr "" -#: bpython/repl.py:862 bpython/repl.py:1169 +#: bpython/repl.py:858 bpython/repl.py:1165 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:864 +#: bpython/repl.py:860 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:870 +#: bpython/repl.py:866 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:877 +#: bpython/repl.py:873 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:879 +#: bpython/repl.py:875 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:888 +#: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:890 +#: bpython/repl.py:886 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:898 +#: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:900 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:908 +#: bpython/repl.py:904 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:917 +#: bpython/repl.py:913 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:922 +#: bpython/repl.py:918 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:960 +#: bpython/repl.py:956 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:968 bpython/repl.py:972 +#: bpython/repl.py:964 bpython/repl.py:968 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:975 +#: bpython/repl.py:971 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1151 +#: bpython/repl.py:1147 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1179 +#: bpython/repl.py:1175 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1185 +#: bpython/repl.py:1181 #, python-format msgid "Error editing config file: %s" msgstr "" @@ -282,55 +280,55 @@ msgid "" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:312 +#: bpython/curtsiesfrontend/repl.py:324 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:314 +#: bpython/curtsiesfrontend/repl.py:326 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:643 +#: bpython/curtsiesfrontend/repl.py:663 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:661 +#: bpython/curtsiesfrontend/repl.py:681 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:970 +#: bpython/curtsiesfrontend/repl.py:990 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:985 +#: bpython/curtsiesfrontend/repl.py:1005 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:995 +#: bpython/curtsiesfrontend/repl.py:1015 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1006 +#: bpython/curtsiesfrontend/repl.py:1026 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1012 +#: bpython/curtsiesfrontend/repl.py:1032 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1017 +#: bpython/curtsiesfrontend/repl.py:1037 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1023 +#: bpython/curtsiesfrontend/repl.py:1043 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1958 +#: bpython/curtsiesfrontend/repl.py:1985 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po index be858262f..87c957390 100644 --- a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po +++ b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7.1\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-01-01 15:21+0100\n" +"POT-Creation-Date: 2021-01-29 22:59+0100\n" "PO-Revision-Date: 2020-10-29 12:20+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: nl_NL\n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:92 +#: bpython/args.py:91 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -26,26 +26,24 @@ msgid "" "to the regular Python interpreter." msgstr "" -#: bpython/args.py:102 +#: bpython/args.py:101 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:108 +#: bpython/args.py:107 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:114 +#: bpython/args.py:113 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:120 +#: bpython/args.py:119 msgid "Print version and exit." msgstr "" -#: bpython/args.py:131 -msgid "" -"File to extecute and additional arguments passed on to the executed " -"script." +#: bpython/args.py:130 +msgid "File to execute and additional arguments passed on to the executed script." msgstr "" #: bpython/cli.py:312 bpython/urwid.py:537 @@ -125,125 +123,125 @@ msgstr "" msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:657 +#: bpython/repl.py:653 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:662 +#: bpython/repl.py:658 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:667 +#: bpython/repl.py:663 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:669 +#: bpython/repl.py:665 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:824 +#: bpython/repl.py:820 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:826 bpython/repl.py:829 bpython/repl.py:853 +#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:840 +#: bpython/repl.py:836 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:848 +#: bpython/repl.py:844 msgid "overwrite" msgstr "" -#: bpython/repl.py:850 +#: bpython/repl.py:846 msgid "append" msgstr "" -#: bpython/repl.py:862 bpython/repl.py:1169 +#: bpython/repl.py:858 bpython/repl.py:1165 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:864 +#: bpython/repl.py:860 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:870 +#: bpython/repl.py:866 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:877 +#: bpython/repl.py:873 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:879 +#: bpython/repl.py:875 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:888 +#: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:890 +#: bpython/repl.py:886 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:898 +#: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:900 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:908 +#: bpython/repl.py:904 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:917 +#: bpython/repl.py:913 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:922 +#: bpython/repl.py:918 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:960 +#: bpython/repl.py:956 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:968 bpython/repl.py:972 +#: bpython/repl.py:964 bpython/repl.py:968 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:975 +#: bpython/repl.py:971 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1151 +#: bpython/repl.py:1147 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1179 +#: bpython/repl.py:1175 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1185 +#: bpython/repl.py:1181 #, python-format msgid "Error editing config file: %s" msgstr "" @@ -282,55 +280,55 @@ msgid "" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:312 +#: bpython/curtsiesfrontend/repl.py:324 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:314 +#: bpython/curtsiesfrontend/repl.py:326 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:643 +#: bpython/curtsiesfrontend/repl.py:663 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:661 +#: bpython/curtsiesfrontend/repl.py:681 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:970 +#: bpython/curtsiesfrontend/repl.py:990 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:985 +#: bpython/curtsiesfrontend/repl.py:1005 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:995 +#: bpython/curtsiesfrontend/repl.py:1015 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1006 +#: bpython/curtsiesfrontend/repl.py:1026 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1012 +#: bpython/curtsiesfrontend/repl.py:1032 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1017 +#: bpython/curtsiesfrontend/repl.py:1037 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1023 +#: bpython/curtsiesfrontend/repl.py:1043 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1958 +#: bpython/curtsiesfrontend/repl.py:1985 msgid "" "\n" "Thanks for using bpython!\n" From f924f2c7867ed0816b0314d4702a7805ad6014ca Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 30 Jan 2021 09:47:29 +0100 Subject: [PATCH 142/555] Check colors in custom theme config and error out on unknown codes --- bpython/config.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/bpython/config.py b/bpython/config.py index 5af10d1dd..cdd8ca35d 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -31,10 +31,17 @@ from xdg import BaseDirectory from .autocomplete import AutocompleteModes +from .curtsiesfrontend.parse import CNAMES default_completion = AutocompleteModes.SIMPLE +class UnknownColorCode(Exception): + def __init__(self, key, color): + self.key = key + self.color = color + + class Struct: """Simple class for instantiating objects we can add arbitrary attributes to and use for various arbitrary things.""" @@ -324,6 +331,11 @@ def get_key_no_doublebind(command): f"Could not load theme '{color_scheme_name}' from {path}.\n" ) sys.exit(1) + except UnknownColorCode as ucc: + sys.stderr.write( + f"Theme '{color_scheme_name}' contains invalid color: {ucc.key} = {ucc.color}.\n" + ) + sys.exit(1) # expand path of history file struct.hist_file = Path(struct.hist_file).expanduser() @@ -362,6 +374,8 @@ def load_theme(struct, path, colors, default_colors): colors[k] = theme.get("syntax", k) else: colors[k] = theme.get("interface", k) + if colors[k].lower() not in CNAMES: + raise UnknownColorCode(k, colors[k]) # Check against default theme to see if all values are defined for k, v in default_colors.items(): From 8d217395e900f75a404cb632cb3dec7a7b02e121 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 30 Jan 2021 09:49:34 +0100 Subject: [PATCH 143/555] Remove a branch --- bpython/autocomplete.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 89b4abf94..15eda6a9c 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -175,10 +175,7 @@ def few_enough_underscores(current, match): return True elif current.startswith("_") and not match.startswith("__"): return True - elif match.startswith("_"): - return False - else: - return True + return not match.startswith("_") def method_match_simple(word, size, text): From 3d315dcd6a4e65963184ddbb933a0bab2a2e5d8c Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sat, 30 Jan 2021 07:59:51 -0800 Subject: [PATCH 144/555] Enable crashers tests for Curtsies (#881) Enable crashers tests for Curtsies --- bpython/test/test_crashers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bpython/test/test_crashers.py b/bpython/test/test_crashers.py index f9afe7947..64abff3e3 100644 --- a/bpython/test/test_crashers.py +++ b/bpython/test/test_crashers.py @@ -72,6 +72,10 @@ def next(self): self.data = self.data[index + 4 :] self.transport.write(input.encode(encoding)) self.state = next(self.states) + elif self.data == "\x1b[6n": + # this is a cursor position query + # respond that cursor is on row 2, column 1 + self.transport.write("\x1b[2;1R".encode(encoding)) else: self.transport.closeStdin() if self.transport.pid is not None: @@ -94,6 +98,7 @@ def processExited(self, reason): f"bpython.{self.backend}", "--config", str(TEST_CONFIG), + "-q", # prevents version greeting ), env={ "TERM": "vt100", @@ -128,6 +133,11 @@ def check_no_traceback(self, data): self.assertNotIn("Traceback", data) +@unittest.skipIf(reactor is None, "twisted is not available") +class CurtsiesCrashersTest(TrialTestCase, CrashersTest): + backend = "curtsies" + + @unittest.skipIf(reactor is None, "twisted is not available") class CursesCrashersTest(TrialTestCase, CrashersTest): backend = "cli" From 76aad2fa5ea34ace62271b2bcf52f5dd5b7da9ad Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 3 Feb 2021 09:41:35 +0100 Subject: [PATCH 145/555] Turn Appdata into metainfo --- ...pdata.xml => org.bpython-interpreter.bpython.metainfo.xml} | 0 setup.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename data/{org.bpython-interpreter.bpython.appdata.xml => org.bpython-interpreter.bpython.metainfo.xml} (100%) diff --git a/data/org.bpython-interpreter.bpython.appdata.xml b/data/org.bpython-interpreter.bpython.metainfo.xml similarity index 100% rename from data/org.bpython-interpreter.bpython.appdata.xml rename to data/org.bpython-interpreter.bpython.metainfo.xml diff --git a/setup.py b/setup.py index 0aac54bdc..994e50855 100755 --- a/setup.py +++ b/setup.py @@ -162,8 +162,8 @@ def git_describe_to_python_version(version): ), # AppData ( - os.path.join("share", "appinfo"), - ["data/org.bpython-interpreter.bpython.appdata.xml"], + os.path.join("share", "metainfo"), + ["data/org.bpython-interpreter.bpython.metainfo.xml"], ), # icon (os.path.join("share", "pixmaps"), ["data/bpython.png"]), From ee87010964ee00d8fc1ee9882f374abc9c1978ea Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 4 Feb 2021 22:04:23 +0100 Subject: [PATCH 146/555] Fix autocompletion mode lookup --- bpython/autocomplete.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 15eda6a9c..c700d97ce 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -48,8 +48,8 @@ class AutocompleteModes(Enum): @classmethod def from_string(cls, value): - if value in cls.__members__: - return cls.__members__[value] + if value.upper() in cls.__members__: + return cls.__members__[value.upper()] return None From 5fac24d31819091a2626d98dac408b30dc7feb67 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 4 Feb 2021 22:05:22 +0100 Subject: [PATCH 147/555] No longer override autocompletion_mode setting --- bpython/curtsiesfrontend/repl.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 86c2cb1d0..5637b9d57 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -327,8 +327,6 @@ def __init__( ) else: banner = None - # only one implemented currently - config.autocomplete_mode = autocomplete.AutocompleteModes.SIMPLE if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1: config.cli_suggestion_width = 1 From a625d69fb55d427595846e086596911428756b7d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 4 Feb 2021 22:10:27 +0100 Subject: [PATCH 148/555] Make it possible to disable auto completion (fixes #883) --- bpython/autocomplete.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index c700d97ce..6b5645157 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -42,6 +42,7 @@ # Autocomplete modes class AutocompleteModes(Enum): + NONE = "none" SIMPLE = "simple" SUBSTRING = "substring" FUZZY = "fuzzy" @@ -178,6 +179,10 @@ def few_enough_underscores(current, match): return not match.startswith("_") +def method_match_none(word, size, text): + return False + + def method_match_simple(word, size, text): return word[:size] == text @@ -192,6 +197,7 @@ def method_match_fuzzy(word, size, text): MODES_MAP = { + AutocompleteModes.NONE: method_match_none, AutocompleteModes.SIMPLE: method_match_simple, AutocompleteModes.SUBSTRING: method_match_substring, AutocompleteModes.FUZZY: method_match_fuzzy, @@ -669,7 +675,7 @@ def get_default_completer(mode=AutocompleteModes.SIMPLE, module_gatherer=None): ), AttrCompletion(mode=mode), ExpressionAttributeCompletion(mode=mode), - ) + ) if mode != AutocompleteModes.NONE else tuple() def _callable_postfix(value, word): From 3ae41ef04963eba575d62f7db7d87d6be4d39349 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 4 Feb 2021 22:15:01 +0100 Subject: [PATCH 149/555] Update autocomplete_mode documentation --- doc/sphinx/source/configuration-options.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst index 2a1bbbe6c..c5b3ccfb0 100644 --- a/doc/sphinx/source/configuration-options.rst +++ b/doc/sphinx/source/configuration-options.rst @@ -15,11 +15,10 @@ When this is off, you can hit tab to see the suggestions. autocomplete_mode ^^^^^^^^^^^^^^^^^ -There are three modes for autocomplete. simple, substring, and fuzzy. Simple -matches methods with a common prefix, substring matches methods with a common -subsequence, and fuzzy matches methods with common characters (default: simple). - -As of version 0.14 this option has no effect, but is reserved for later use. +There are four modes for autocomplete: ``none``, ``simple``, ``substring``, and +``fuzzy``. Simple matches methods with a common prefix, substring matches +methods with a common subsequence, and fuzzy matches methods with common +characters (default: simple). None disables autocompletion. .. versionadded:: 0.12 From e940a37ff677b7c137a6f3d1e89c37a2eb0d4f2e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 4 Feb 2021 22:17:47 +0100 Subject: [PATCH 150/555] Apply black --- bpython/autocomplete.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 6b5645157..477b9651c 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -664,18 +664,25 @@ def get_completer(completers, cursor_offset, line, **kwargs): def get_default_completer(mode=AutocompleteModes.SIMPLE, module_gatherer=None): return ( - DictKeyCompletion(mode=mode), - ImportCompletion(module_gatherer, mode=mode), - FilenameCompletion(mode=mode), - MagicMethodCompletion(mode=mode), - MultilineJediCompletion(mode=mode), - CumulativeCompleter( - (GlobalCompletion(mode=mode), ParameterNameCompletion(mode=mode)), - mode=mode, - ), - AttrCompletion(mode=mode), - ExpressionAttributeCompletion(mode=mode), - ) if mode != AutocompleteModes.NONE else tuple() + ( + DictKeyCompletion(mode=mode), + ImportCompletion(module_gatherer, mode=mode), + FilenameCompletion(mode=mode), + MagicMethodCompletion(mode=mode), + MultilineJediCompletion(mode=mode), + CumulativeCompleter( + ( + GlobalCompletion(mode=mode), + ParameterNameCompletion(mode=mode), + ), + mode=mode, + ), + AttrCompletion(mode=mode), + ExpressionAttributeCompletion(mode=mode), + ) + if mode != AutocompleteModes.NONE + else tuple() + ) def _callable_postfix(value, word): From b63051e1c7de400d05fb34f55d38eec86398116b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 7 Feb 2021 20:40:04 +0100 Subject: [PATCH 151/555] Do not abort early if nothing to output (fixes #884) Otherwise no TypeError is raised on b"" which is used by click to check if an output stream is a binary stream. --- bpython/curtsiesfrontend/repl.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 5637b9d57..3abb689bb 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1240,8 +1240,6 @@ def send_to_stdouterr(self, output): Must be able to handle FmtStrs because interpreter pass in tracebacks already formatted.""" - if not output: - return lines = output.split("\n") logger.debug("display_lines: %r", self.display_lines) self.current_stdouterr_line += lines[0] From 10072cb7afdffc60296a854ad27bc62d9e663958 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 7 Feb 2021 21:18:24 +0100 Subject: [PATCH 152/555] Use itertools.chain instead of list addition --- bpython/curtsiesfrontend/repl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 3abb689bb..9a7ee0e08 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -2,6 +2,7 @@ import contextlib import errno import greenlet +import itertools import logging import os import re @@ -1259,7 +1260,9 @@ def send_to_stdouterr(self, output): ) ) # These can be FmtStrs, but self.all_logical_lines only wants strings - for line in [self.current_stdouterr_line] + lines[1:-1]: + for line in itertools.chain( + (self.current_stdouterr_line,), lines[1:-1] + ): if isinstance(line, FmtStr): self.all_logical_lines.append((line.s, LineType.OUTPUT)) else: From 13dc7067910039b4c9fd534a4ad3535cb598db4f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 7 Feb 2021 21:25:41 +0100 Subject: [PATCH 153/555] Use relative import --- bpython/curtsiesfrontend/preprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/preprocess.py b/bpython/curtsiesfrontend/preprocess.py index 714d9538a..8bf2924d8 100644 --- a/bpython/curtsiesfrontend/preprocess.py +++ b/bpython/curtsiesfrontend/preprocess.py @@ -1,7 +1,7 @@ """Tools for preparing code to be run in the REPL (removing blank lines, etc)""" -from bpython.lazyre import LazyReCompile +from ..lazyre import LazyReCompile # TODO specifically catch IndentationErrors instead of any syntax errors From b2927110f705ccdc43ffca7c1e1be059cc60b600 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 7 Feb 2021 21:33:14 +0100 Subject: [PATCH 154/555] Use iterators instead of allocating extra lists --- bpython/curtsiesfrontend/preprocess.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/preprocess.py b/bpython/curtsiesfrontend/preprocess.py index 8bf2924d8..e0d15f4ec 100644 --- a/bpython/curtsiesfrontend/preprocess.py +++ b/bpython/curtsiesfrontend/preprocess.py @@ -2,10 +2,10 @@ etc)""" from ..lazyre import LazyReCompile +from itertools import tee, islice, chain # TODO specifically catch IndentationErrors instead of any syntax errors - indent_empty_lines_re = LazyReCompile(r"\s*") tabs_to_spaces_re = LazyReCompile(r"^\t+") @@ -21,7 +21,11 @@ def indent_empty_lines(s, compiler): lines.pop() result_lines = [] - for p_line, line, n_line in zip([""] + lines[:-1], lines, lines[1:] + [""]): + prevs, lines, nexts = tee(lines, 3) + prevs = chain(("",), prevs) + nexts = chain(islice(nexts, 1, None), ("",)) + + for p_line, line, n_line in zip(prevs, lines, nexts): if len(line) == 0: p_indent = indent_empty_lines_re.match(p_line).group() n_indent = indent_empty_lines_re.match(n_line).group() From ac60146d7cd23e30004d64c8163ee15862d24937 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 9 Feb 2021 00:24:47 +0100 Subject: [PATCH 155/555] Update years and authors --- doc/sphinx/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/source/conf.py b/doc/sphinx/source/conf.py index 99f63344e..2c5263d90 100644 --- a/doc/sphinx/source/conf.py +++ b/doc/sphinx/source/conf.py @@ -38,7 +38,7 @@ # General information about the project. project = u'bpython' -copyright = u'2008-2015 Bob Farrell, Andreas Stuehrk et al.' +copyright = u'2008-2021 Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 55dbd77d5aa2b38d2640d7088c9fce62a0a424f2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 9 Feb 2021 00:26:26 +0100 Subject: [PATCH 156/555] Fix indentation --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e4d2c6be8..28cd66af2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -130,6 +130,7 @@ Support for Python 3.3 has been dropped. ------ Fixes: + * Reverted #670 temporarily due to performance impact on large strings being output. From c115cb956f9cc393d979d7c025ee3d47a006ab6d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 9 Feb 2021 00:37:10 +0100 Subject: [PATCH 157/555] Update pastebin example --- doc/sphinx/source/configuration-options.rst | 46 +++++++++++---------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst index c5b3ccfb0..6d347761d 100644 --- a/doc/sphinx/source/configuration-options.rst +++ b/doc/sphinx/source/configuration-options.rst @@ -103,38 +103,42 @@ following helper program can be used to create `gists #!/usr/bin/env python import sys - import urllib2 + import requests import json def do_gist_json(s): """ Use json to post to github. """ gist_public = False - gist_url = 'https://api.github.com/gists' - - data = {'description': None, - 'public': None, - 'files' : { - 'sample': { 'content': None } - }} - data['description'] = 'Gist from BPython' - data['public'] = gist_public - data['files']['sample']['content'] = s - - req = urllib2.Request(gist_url, json.dumps(data), {'Content-Type': 'application/json'}) - try: - res = urllib2.urlopen(req) - except HTTPError, e: - return e + gist_url = "https://api.github.com/gists" + + data = { + "description": "Gist from bpython", + "public": gist_public, + "files": { + "sample": { + "content": s + }, + }, + } + + headers = { + "Content-Type": "application/json", + "X-Github-Username": "YOUR_USERNAME", + "Authorization": "token YOUR_TOKEN", + } try: + res = requests.post(gist_url, data=json.dumps(payload), headers=headers) + res.raise_for_status() json_res = json.loads(res.read()) - return json_res['html_url'] - except HTTPError, e: - return e + return json_res["html_url"] + except requests.exceptions.HTTPError as err: + return err + if __name__ == "__main__": s = sys.stdin.read() - print do_gist_json(s) + print(do_gist_json(s)) .. versionadded:: 0.12 From f3cc2204b87d2bf890e85af04eb2a774277bdf7a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Feb 2021 15:45:13 +0100 Subject: [PATCH 158/555] Use declarative build config as much as possible --- pyproject.toml | 6 ++++++ setup.cfg | 40 +++++++++++++++++++++++++++++++++++++++- setup.py | 49 ------------------------------------------------- 3 files changed, 45 insertions(+), 50 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2ea7bf3e9..79a558e88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,9 @@ +[build-system] +requires = [ + "setuptools >= 43", + "wheel", +] + [tool.black] line-length = 80 target_version = ["py36"] diff --git a/setup.cfg b/setup.cfg index bfed3932c..7dfb1d58c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,43 @@ [metadata] -license_files = LICENSE +name = bpython +long_description = file: README.rst +license = MIT +license_file = LICENSE +url = https://www.bpython-interpreter.org/ +project_urls = + GitHub = https://github.com/bpython/bpython + Documentation = https://doc.bpython-interpreter.org +classifiers = + Programming Language :: Python :: 3 + +[options] +python_requires = >=3.6 +packages = + bpython + bpython.curtsiesfrontend + bpython.test + bpython.test.fodder + bpython.translations + bpdb +install_requires = + pygments + requests + curtsies >=0.3.5 + greenlet + cwcwidth + pyxdg + +[options.extras_require] +urwid = urwid +watch = watchdog +jedi = jedi >= 0.16 + +[options.entry_points] +console_scripts = + bpython = bpython.curtsies:main + bpython-curses = bpython.cli:main + bpython-urwid = bpython.urwid:main [urwid] + bpdb = bpdb:main [init_catalog] domain = bpython diff --git a/setup.py b/setup.py index 994e50855..42dd2a309 100755 --- a/setup.py +++ b/setup.py @@ -170,43 +170,6 @@ def git_describe_to_python_version(version): ] data_files.extend(man_pages) -classifiers = [ - "Programming Language :: Python :: 3", -] - -install_requires = [ - "pygments", - "requests", - "curtsies >=0.3.5", - "greenlet", - "cwcwidth", - "pyxdg", -] - -extras_require = { - "urwid": ["urwid"], - "watch": ["watchdog"], - "jedi": ["jedi >=0.16"], -} - -packages = [ - "bpython", - "bpython.curtsiesfrontend", - "bpython.test", - "bpython.test.fodder", - "bpython.translations", - "bpdb", -] - -entry_points = { - "console_scripts": [ - "bpython = bpython.curtsies:main", - "bpython-curses = bpython.cli:main", - "bpython-urwid = bpython.urwid:main [urwid]", - "bpdb = bpdb:main", - ] -} - # translations mo_files = [] for language in os.listdir(translations_dir): @@ -215,27 +178,15 @@ def git_describe_to_python_version(version): mo_files.append(mo_subpath) setup( - name="bpython", version=version, author=__author__, author_email="robertanthonyfarrell@gmail.com", - description="Fancy Interface to the Python Interpreter", - license="MIT/X", - url="https://www.bpython-interpreter.org/", - long_description="""bpython is a fancy interface to the Python - interpreter for Unix-like operating systems.""", - classifiers=classifiers, - python_requires=">=3.6", - install_requires=install_requires, - extras_require=extras_require, - packages=packages, data_files=data_files, package_data={ "bpython": ["sample-config"], "bpython.translations": mo_files, "bpython.test": ["test.config", "test.theme"], }, - entry_points=entry_points, cmdclass=cmdclass, test_suite="bpython.test", ) From 8badb8e596fcab7362b906caacd067660ba11ec5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 14 Feb 2021 17:22:09 +0100 Subject: [PATCH 159/555] Improve logging configuration via command line options --- bpython/args.py | 34 ++++++++++++++++++++++++++++++- bpython/curtsies.py | 18 ---------------- doc/sphinx/source/man-bpython.rst | 4 ++-- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 3d5396625..9618cc0c2 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -25,10 +25,11 @@ Module to handle command line argument parsing, for all front-ends. """ +import argparse import importlib.util +import logging import os import sys -import argparse from . import __version__, __copyright__ from .config import default_config_path, loadini, Struct @@ -118,6 +119,18 @@ def callback(group): action="store_true", help=_("Print version and exit."), ) + parser.add_argument( + "--log-level", + "-l", + choices=("debug", "info", "warning", "error", "critical"), + default="error", + help=_("Set log level for logging"), + ) + parser.add_argument( + "--log-output", + "-L", + help=_("Log output file"), + ) if extras is not None: extras_group = parser.add_argument_group(extras[0], extras[1]) @@ -147,6 +160,25 @@ def callback(group): # Just let Python handle this os.execv(sys.executable, [sys.executable] + args) + # Configure logging handler + bpython_logger = logging.getLogger("bpython") + curtsies_logger = logging.getLogger("curtsies") + bpython_logger.setLevel(options.log_level.upper()) + curtsies_logger.setLevel(options.log_level.upper()) + if options.log_output: + handler = logging.FileHandler(filename=options.log_output) + handler.setFormatter( + logging.Formatter( + "%(asctime)s: %(name)s: %(levelname)s: %(message)s" + ) + ) + bpython_logger.addHandler(handler) + curtsies_logger.addHandler(handler) + bpython_logger.propagate = curtsies_logger.propagate = False + else: + bpython_logger.addHandler(logging.NullHandler()) + curtsies_logger.addHandler(logging.NullHandler()) + config = Struct() loadini(config, options.config) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index f9950997e..69d3f175f 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -129,12 +129,6 @@ def main(args=None, locals_=None, banner=None, welcome_message=None): translations.init() def curtsies_arguments(parser): - parser.add_argument( - "--log", - "-L", - action="count", - help=_("log debug messages to bpython.log"), - ) parser.add_argument( "--paste", "-p", @@ -150,18 +144,6 @@ def curtsies_arguments(parser): curtsies_arguments, ), ) - if options.log is None: - options.log = 0 - logging_levels = (logging.ERROR, logging.INFO, logging.DEBUG) - level = logging_levels[min(len(logging_levels) - 1, options.log)] - logging.getLogger("curtsies").setLevel(level) - logging.getLogger("bpython").setLevel(level) - if options.log: - handler = logging.FileHandler(filename="bpython.log") - logging.getLogger("curtsies").addHandler(handler) - logging.getLogger("curtsies").propagate = False - logging.getLogger("bpython").addHandler(handler) - logging.getLogger("bpython").propagate = False interp = None paste = None diff --git a/doc/sphinx/source/man-bpython.rst b/doc/sphinx/source/man-bpython.rst index 463aa6018..fe37a25fe 100644 --- a/doc/sphinx/source/man-bpython.rst +++ b/doc/sphinx/source/man-bpython.rst @@ -57,12 +57,12 @@ The following options are supported by all frontends: exiting. The PYTHONSTARTUP file is not read. -q, --quiet Do not flush the output to stdout. -V, --version Print :program:`bpython`'s version and exit. +-l , --log-level= Set logging level +-L , --log-output= Set log output file In addition to the above options, :program:`bpython` also supports the following options: --L, --log Write debugging messages to the file bpython.log. Use - -LL for more verbose logging. -p file, --paste=file Paste in the contents of a file at startup. In addition to the common options, :program:`bpython-urwid` also supports the From 7204eb5f580f275254c10a550d846a992a63c348 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 14 Feb 2021 17:28:31 +0100 Subject: [PATCH 160/555] Improve docstring --- bpython/args.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 9618cc0c2..7cba9a6d9 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -74,9 +74,12 @@ def callback(group): parse( ['-i', '-m', 'foo.py'], - ('Front end-specific options', - 'A full description of what these options are for', - callback)) + ( + 'Front end-specific options', + 'A full description of what these options are for', + callback + ), + ) Return a tuple of (config, options, exec_args) wherein "config" is the From 964f42c160145e89d04a05887a4285b82ddf7c74 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 14 Feb 2021 17:29:36 +0100 Subject: [PATCH 161/555] Translate version and copyright banner --- bpython/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 7cba9a6d9..f670f8dae 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -46,7 +46,7 @@ def error(self, msg): def version_banner(base="bpython"): - return "{} version {} on top of Python {} {}".format( + return _("{} version {} on top of Python {} {}").format( base, __version__, sys.version.split()[0], @@ -55,7 +55,7 @@ def version_banner(base="bpython"): def copyright_banner(): - return "{} See AUTHORS.rst for details.".format(__copyright__) + return _("{} See AUTHORS.rst for details.").format(__copyright__) def parse(args, extras=None, ignore_stdin=False): From 9a0ad312c964e40ff830ed9e4a136f7d5d5405ee Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 14 Feb 2021 17:33:53 +0100 Subject: [PATCH 162/555] Update translations --- bpython/translations/bpython.pot | 62 +++++--- .../translations/de/LC_MESSAGES/bpython.po | 143 ++++++++++-------- .../translations/es_ES/LC_MESSAGES/bpython.po | 60 +++++--- .../translations/fr_FR/LC_MESSAGES/bpython.po | 60 +++++--- .../translations/it_IT/LC_MESSAGES/bpython.po | 60 +++++--- .../translations/nl_NL/LC_MESSAGES/bpython.po | 60 +++++--- 6 files changed, 257 insertions(+), 188 deletions(-) diff --git a/bpython/translations/bpython.pot b/bpython/translations/bpython.pot index 7b23845d7..24c9cdd8a 100644 --- a/bpython/translations/bpython.pot +++ b/bpython/translations/bpython.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: bpython 0.22.dev8\n" +"Project-Id-Version: bpython 0.22.dev28\n" "Report-Msgid-Bugs-To: https://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-01-29 22:59+0100\n" +"POT-Creation-Date: 2021-02-14 17:29+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,7 +17,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:91 +#: bpython/args.py:49 +msgid "{} version {} on top of Python {} {}" +msgstr "" + +#: bpython/args.py:58 +msgid "{} See AUTHORS.rst for details." +msgstr "" + +#: bpython/args.py:95 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -25,23 +33,31 @@ msgid "" "to the regular Python interpreter." msgstr "" -#: bpython/args.py:101 +#: bpython/args.py:105 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:107 +#: bpython/args.py:111 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:113 +#: bpython/args.py:117 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:119 +#: bpython/args.py:123 msgid "Print version and exit." msgstr "" #: bpython/args.py:130 +msgid "Set log level for logging" +msgstr "" + +#: bpython/args.py:135 +msgid "Log output file" +msgstr "" + +#: bpython/args.py:146 msgid "File to execute and additional arguments passed on to the executed script." msgstr "" @@ -81,18 +97,14 @@ msgid "" msgstr "" #: bpython/curtsies.py:136 -msgid "log debug messages to bpython.log" -msgstr "" - -#: bpython/curtsies.py:142 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:148 +#: bpython/curtsies.py:142 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:149 +#: bpython/curtsies.py:143 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" @@ -279,55 +291,55 @@ msgid "" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:324 +#: bpython/curtsiesfrontend/repl.py:325 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:326 +#: bpython/curtsiesfrontend/repl.py:327 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:663 +#: bpython/curtsiesfrontend/repl.py:664 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:681 +#: bpython/curtsiesfrontend/repl.py:682 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:990 +#: bpython/curtsiesfrontend/repl.py:991 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1005 +#: bpython/curtsiesfrontend/repl.py:1006 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1015 +#: bpython/curtsiesfrontend/repl.py:1016 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1026 +#: bpython/curtsiesfrontend/repl.py:1027 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1032 +#: bpython/curtsiesfrontend/repl.py:1033 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1037 +#: bpython/curtsiesfrontend/repl.py:1038 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1043 +#: bpython/curtsiesfrontend/repl.py:1044 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1985 +#: bpython/curtsiesfrontend/repl.py:1986 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/de/LC_MESSAGES/bpython.po b/bpython/translations/de/LC_MESSAGES/bpython.po index 70684700b..95bd6cc84 100644 --- a/bpython/translations/de/LC_MESSAGES/bpython.po +++ b/bpython/translations/de/LC_MESSAGES/bpython.po @@ -7,45 +7,62 @@ msgid "" msgstr "" "Project-Id-Version: bpython mercurial\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-01-29 22:59+0100\n" -"PO-Revision-Date: 2021-01-29 22:58+0100\n" +"POT-Creation-Date: 2021-02-14 17:29+0100\n" +"PO-Revision-Date: 2021-02-14 17:31+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: de\n" "Language-Team: de \n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" +"X-Generator: Poedit 2.4.2\n" -#: bpython/args.py:91 +#: bpython/args.py:49 +msgid "{} version {} on top of Python {} {}" +msgstr "{} Version {} mit Python {} {}" + +#: bpython/args.py:58 +msgid "{} See AUTHORS.rst for details." +msgstr "{} Siehe AUTHORS.rst für mehr Details." + +#: bpython/args.py:95 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" -"NOTE: If bpython sees an argument it does not know, execution falls back " -"to the regular Python interpreter." +"NOTE: If bpython sees an argument it does not know, execution falls back to the " +"regular Python interpreter." msgstr "" "Verwendung: %(prog)s [Optionen] [Datei [Argumente]]\n" -"Hinweis: Wenn bpython Argumente übergeben bekommt, die nicht verstanden " -"werden, wird der normale Python Interpreter ausgeführt." +"Hinweis: Wenn bpython Argumente übergeben bekommt, die nicht verstanden werden, " +"wird der normale Python Interpreter ausgeführt." -#: bpython/args.py:101 +#: bpython/args.py:105 msgid "Use CONFIG instead of default config file." msgstr "Verwende CONFIG antatt der standardmäßigen Konfigurationsdatei." -#: bpython/args.py:107 +#: bpython/args.py:111 msgid "Drop to bpython shell after running file instead of exiting." msgstr "Verbleibe in bpython nach dem Ausführen der Datei." -#: bpython/args.py:113 +#: bpython/args.py:117 msgid "Don't flush the output to stdout." msgstr "Gib Ausgabe beim Beenden nicht ernaut auf stdout aus." -#: bpython/args.py:119 +#: bpython/args.py:123 msgid "Print version and exit." msgstr "Zeige Versionsinformationen an und beende." #: bpython/args.py:130 +msgid "Set log level for logging" +msgstr "Log-Stufe" + +#: bpython/args.py:135 +msgid "Log output file" +msgstr "Datei für Ausgabe von Log-Nachrichten" + +#: bpython/args.py:146 msgid "File to execute and additional arguments passed on to the executed script." msgstr "" @@ -79,27 +96,23 @@ msgstr "Quellcode anzeigen" #: bpython/cli.py:1943 msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." +"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. This " +"backend has been deprecated in version 0.19 and might disappear in a future " +"version." msgstr "" -"ACHTUNG: `bpython-cli` wird verwendet, die curses Implementierung von " -"`bpython`. Diese Implementierung wird ab Version 0.19 nicht mehr aktiv " -"unterstützt und wird in einer zukünftigen Version entfernt werden." +"ACHTUNG: `bpython-cli` wird verwendet, die curses Implementierung von `bpython`. " +"Diese Implementierung wird ab Version 0.19 nicht mehr aktiv unterstützt und wird " +"in einer zukünftigen Version entfernt werden." #: bpython/curtsies.py:136 -msgid "log debug messages to bpython.log" -msgstr "Zeichne debug Nachrichten in bpython.log auf" - -#: bpython/curtsies.py:142 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:148 +#: bpython/curtsies.py:142 msgid "curtsies arguments" msgstr "Argumente für curtsies" -#: bpython/curtsies.py:149 +#: bpython/curtsies.py:143 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "Zusätzliche Argumente spezifisch für die curtsies-basierte REPL." @@ -227,8 +240,8 @@ msgstr "Pastebin URL: %s" #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -"Wie viele Zeilen rückgängig machen? (Rückgängigmachen wird bis zu ~%.1f " -"Sekunden brauchen) [1]" +"Wie viele Zeilen rückgängig machen? (Rückgängigmachen wird bis zu ~%.1f Sekunden " +"brauchen) [1]" #: bpython/repl.py:964 bpython/repl.py:968 msgid "Undo canceled" @@ -244,14 +257,13 @@ msgstr[1] "Mache %d Zeilen rückgängig... (ungefähr %.1f Sekunden)" #: bpython/repl.py:1147 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -"Konfigurationsdatei existiert nicht. Soll eine neue Datei erstellt " -"werden? (j/N)" +"Konfigurationsdatei existiert nicht. Soll eine neue Datei erstellt werden? (j/N)" #: bpython/repl.py:1175 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -"bpython Konfigurationsdatei bearbeitet. Starte bpython neu damit die " -"Änderungen übernommen werden." +"bpython Konfigurationsdatei bearbeitet. Starte bpython neu damit die Änderungen " +"übernommen werden." #: bpython/repl.py:1181 #, python-format @@ -262,8 +274,8 @@ msgstr "Fehler beim Bearbeiten der Konfigurationsdatei: %s" #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" -" <%s> Rückgängigmachen <%s> Speichern <%s> Pastebin <%s> Pager <%s> " -"Quellcode anzeigen " +" <%s> Rückgängigmachen <%s> Speichern <%s> Pastebin <%s> Pager <%s> Quellcode " +"anzeigen " #: bpython/urwid.py:1114 msgid "Run twisted reactor." @@ -279,11 +291,11 @@ msgstr "Liste verfügbare reactors für -r auf." #: bpython/urwid.py:1132 msgid "" -"twistd plugin to run (use twistd for a list). Use \"--\" to pass further " -"options to the plugin." +"twistd plugin to run (use twistd for a list). Use \"--\" to pass further options " +"to the plugin." msgstr "" -"Auszuführendes twistd Plugin (starte twistd für eine Liste). Verwende " -"\"--\" um Optionen an das Plugin zu übergeben." +"Auszuführendes twistd Plugin (starte twistd für eine Liste). Verwende \"--\" um " +"Optionen an das Plugin zu übergeben." #: bpython/urwid.py:1141 msgid "Port to run an eval server on (forces Twisted)." @@ -291,91 +303,88 @@ msgstr "" #: bpython/urwid.py:1335 msgid "" -"WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." +"WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. This " +"backend has been deprecated in version 0.19 and might disappear in a future " +"version." msgstr "" "ACHTUNG: `bpython-urwid` wird verwendet, die curses Implementierung von " "`bpython`. Diese Implementierung wird ab Version 0.19 nicht mehr aktiv " "unterstützt und wird in einer zukünftigen Version entfernt werden." -#: bpython/curtsiesfrontend/repl.py:324 +#: bpython/curtsiesfrontend/repl.py:325 msgid "Welcome to bpython!" msgstr "Willkommen by bpython!" -#: bpython/curtsiesfrontend/repl.py:326 +#: bpython/curtsiesfrontend/repl.py:327 #, python-format msgid "Press <%s> for help." msgstr "Drücke <%s> für Hilfe." -#: bpython/curtsiesfrontend/repl.py:663 +#: bpython/curtsiesfrontend/repl.py:664 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "Fehler beim Ausführen von PYTHONSTARTUP: %s" -#: bpython/curtsiesfrontend/repl.py:681 +#: bpython/curtsiesfrontend/repl.py:682 #, python-format msgid "Reloaded at %s because %s modified." msgstr "Bei %s neugeladen, da %s modifiziert wurde." -#: bpython/curtsiesfrontend/repl.py:990 +#: bpython/curtsiesfrontend/repl.py:991 msgid "Session not reevaluated because it was not edited" msgstr "Die Sitzung wurde nicht neu ausgeführt, da sie nicht berabeitet wurde" -#: bpython/curtsiesfrontend/repl.py:1005 +#: bpython/curtsiesfrontend/repl.py:1006 msgid "Session not reevaluated because saved file was blank" msgstr "Die Sitzung wurde nicht neu ausgeführt, da die gespeicherte Datei leer war" -#: bpython/curtsiesfrontend/repl.py:1015 +#: bpython/curtsiesfrontend/repl.py:1016 msgid "Session edited and reevaluated" msgstr "Sitzung bearbeitet und neu ausgeführt" -#: bpython/curtsiesfrontend/repl.py:1026 +#: bpython/curtsiesfrontend/repl.py:1027 #, python-format msgid "Reloaded at %s by user." msgstr "Bei %s vom Benutzer neu geladen." -#: bpython/curtsiesfrontend/repl.py:1032 +#: bpython/curtsiesfrontend/repl.py:1033 msgid "Auto-reloading deactivated." msgstr "Automatisches Neuladen deaktiviert." -#: bpython/curtsiesfrontend/repl.py:1037 +#: bpython/curtsiesfrontend/repl.py:1038 msgid "Auto-reloading active, watching for file changes..." msgstr "Automatisches Neuladen ist aktiv; beobachte Dateiänderungen..." -#: bpython/curtsiesfrontend/repl.py:1043 +#: bpython/curtsiesfrontend/repl.py:1044 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -"Automatisches Neuladen ist nicht verfügbar da watchdog nicht installiert " -"ist." +"Automatisches Neuladen ist nicht verfügbar da watchdog nicht installiert ist." -#: bpython/curtsiesfrontend/repl.py:1985 +#: bpython/curtsiesfrontend/repl.py:1986 msgid "" "\n" "Thanks for using bpython!\n" "\n" -"See http://bpython-interpreter.org/ for more information and http://docs" -".bpython-interpreter.org/ for docs.\n" +"See http://bpython-interpreter.org/ for more information and http://docs.bpython-" +"interpreter.org/ for docs.\n" "Please report issues at https://github.com/bpython/bpython/issues\n" "\n" "Features:\n" "Try using undo ({config.undo_key})!\n" -"Edit the current line ({config.edit_current_block_key}) or the entire " -"session ({config.external_editor_key}) in an external editor. (currently " -"{config.editor})\n" -"Save sessions ({config.save_key}) or post them to pastebins " -"({config.pastebin_key})! Current pastebin helper: " -"{config.pastebin_helper}\n" -"Reload all modules and rerun session ({config.reimport_key}) to test out " -"changes to a module.\n" -"Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute " -"the current session when a module you've imported is modified.\n" +"Edit the current line ({config.edit_current_block_key}) or the entire session " +"({config.external_editor_key}) in an external editor. (currently {config." +"editor})\n" +"Save sessions ({config.save_key}) or post them to pastebins ({config." +"pastebin_key})! Current pastebin helper: {config.pastebin_helper}\n" +"Reload all modules and rerun session ({config.reimport_key}) to test out changes " +"to a module.\n" +"Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute the " +"current session when a module you've imported is modified.\n" "\n" "bpython -i your_script.py runs a file in interactive mode\n" "bpython -t your_script.py pastes the contents of a file into the session\n" "\n" -"A config file at {config.config_path} customizes keys and behavior of " -"bpython.\n" +"A config file at {config.config_path} customizes keys and behavior of bpython.\n" "You can also set which pastebin helper and which external editor to use.\n" "See {example_config_url} for an example config file.\n" "Press {config.edit_config_key} to edit this config file.\n" diff --git a/bpython/translations/es_ES/LC_MESSAGES/bpython.po b/bpython/translations/es_ES/LC_MESSAGES/bpython.po index 3d5704ae7..b877fd1bd 100644 --- a/bpython/translations/es_ES/LC_MESSAGES/bpython.po +++ b/bpython/translations/es_ES/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-01-29 22:59+0100\n" +"POT-Creation-Date: 2021-02-14 17:29+0100\n" "PO-Revision-Date: 2020-10-29 12:22+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: es_ES\n" @@ -18,7 +18,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:91 +#: bpython/args.py:49 +msgid "{} version {} on top of Python {} {}" +msgstr "" + +#: bpython/args.py:58 +msgid "{} See AUTHORS.rst for details." +msgstr "" + +#: bpython/args.py:95 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -26,23 +34,31 @@ msgid "" "to the regular Python interpreter." msgstr "" -#: bpython/args.py:101 +#: bpython/args.py:105 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:107 +#: bpython/args.py:111 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:113 +#: bpython/args.py:117 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:119 +#: bpython/args.py:123 msgid "Print version and exit." msgstr "" #: bpython/args.py:130 +msgid "Set log level for logging" +msgstr "" + +#: bpython/args.py:135 +msgid "Log output file" +msgstr "" + +#: bpython/args.py:146 msgid "File to execute and additional arguments passed on to the executed script." msgstr "" @@ -82,18 +98,14 @@ msgid "" msgstr "" #: bpython/curtsies.py:136 -msgid "log debug messages to bpython.log" -msgstr "" - -#: bpython/curtsies.py:142 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:148 +#: bpython/curtsies.py:142 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:149 +#: bpython/curtsies.py:143 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" @@ -282,55 +294,55 @@ msgid "" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:324 +#: bpython/curtsiesfrontend/repl.py:325 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:326 +#: bpython/curtsiesfrontend/repl.py:327 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:663 +#: bpython/curtsiesfrontend/repl.py:664 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:681 +#: bpython/curtsiesfrontend/repl.py:682 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:990 +#: bpython/curtsiesfrontend/repl.py:991 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1005 +#: bpython/curtsiesfrontend/repl.py:1006 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1015 +#: bpython/curtsiesfrontend/repl.py:1016 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1026 +#: bpython/curtsiesfrontend/repl.py:1027 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1032 +#: bpython/curtsiesfrontend/repl.py:1033 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1037 +#: bpython/curtsiesfrontend/repl.py:1038 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1043 +#: bpython/curtsiesfrontend/repl.py:1044 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1985 +#: bpython/curtsiesfrontend/repl.py:1986 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po index b2b4888d8..d4c4d7758 100644 --- a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po +++ b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.13-442\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-01-29 22:59+0100\n" +"POT-Creation-Date: 2021-02-14 17:29+0100\n" "PO-Revision-Date: 2020-10-29 12:20+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: fr_FR\n" @@ -17,7 +17,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:91 +#: bpython/args.py:49 +msgid "{} version {} on top of Python {} {}" +msgstr "" + +#: bpython/args.py:58 +msgid "{} See AUTHORS.rst for details." +msgstr "" + +#: bpython/args.py:95 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -28,25 +36,33 @@ msgstr "" "NOTE: Si bpython ne reconnaît pas un des arguments fournis, " "l'interpréteur Python classique sera lancé" -#: bpython/args.py:101 +#: bpython/args.py:105 msgid "Use CONFIG instead of default config file." msgstr "Utiliser CONFIG à la place du fichier de configuration par défaut." -#: bpython/args.py:107 +#: bpython/args.py:111 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" "Aller dans le shell bpython après l'exécution du fichier au lieu de " "quitter." -#: bpython/args.py:113 +#: bpython/args.py:117 msgid "Don't flush the output to stdout." msgstr "Ne pas purger la sortie vers stdout." -#: bpython/args.py:119 +#: bpython/args.py:123 msgid "Print version and exit." msgstr "Afficher la version et quitter." #: bpython/args.py:130 +msgid "Set log level for logging" +msgstr "" + +#: bpython/args.py:135 +msgid "Log output file" +msgstr "" + +#: bpython/args.py:146 msgid "File to execute and additional arguments passed on to the executed script." msgstr "" @@ -86,18 +102,14 @@ msgid "" msgstr "" #: bpython/curtsies.py:136 -msgid "log debug messages to bpython.log" -msgstr "logger les messages de debug dans bpython.log" - -#: bpython/curtsies.py:142 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:148 +#: bpython/curtsies.py:142 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:149 +#: bpython/curtsies.py:143 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" @@ -288,55 +300,55 @@ msgid "" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:324 +#: bpython/curtsiesfrontend/repl.py:325 msgid "Welcome to bpython!" msgstr "Bienvenue dans bpython!" -#: bpython/curtsiesfrontend/repl.py:326 +#: bpython/curtsiesfrontend/repl.py:327 #, python-format msgid "Press <%s> for help." msgstr "Appuyer sur <%s> pour de l'aide." -#: bpython/curtsiesfrontend/repl.py:663 +#: bpython/curtsiesfrontend/repl.py:664 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "L'exécution de PYTHONSTARTUP a échoué: %s" -#: bpython/curtsiesfrontend/repl.py:681 +#: bpython/curtsiesfrontend/repl.py:682 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:990 +#: bpython/curtsiesfrontend/repl.py:991 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1005 +#: bpython/curtsiesfrontend/repl.py:1006 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1015 +#: bpython/curtsiesfrontend/repl.py:1016 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1026 +#: bpython/curtsiesfrontend/repl.py:1027 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1032 +#: bpython/curtsiesfrontend/repl.py:1033 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1037 +#: bpython/curtsiesfrontend/repl.py:1038 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1043 +#: bpython/curtsiesfrontend/repl.py:1044 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1985 +#: bpython/curtsiesfrontend/repl.py:1986 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/it_IT/LC_MESSAGES/bpython.po b/bpython/translations/it_IT/LC_MESSAGES/bpython.po index c4e76797b..ddb93cc0c 100644 --- a/bpython/translations/it_IT/LC_MESSAGES/bpython.po +++ b/bpython/translations/it_IT/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-01-29 22:59+0100\n" +"POT-Creation-Date: 2021-02-14 17:29+0100\n" "PO-Revision-Date: 2015-02-02 00:34+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: it_IT\n" @@ -18,7 +18,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:91 +#: bpython/args.py:49 +msgid "{} version {} on top of Python {} {}" +msgstr "" + +#: bpython/args.py:58 +msgid "{} See AUTHORS.rst for details." +msgstr "" + +#: bpython/args.py:95 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -26,23 +34,31 @@ msgid "" "to the regular Python interpreter." msgstr "" -#: bpython/args.py:101 +#: bpython/args.py:105 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:107 +#: bpython/args.py:111 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:113 +#: bpython/args.py:117 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:119 +#: bpython/args.py:123 msgid "Print version and exit." msgstr "" #: bpython/args.py:130 +msgid "Set log level for logging" +msgstr "" + +#: bpython/args.py:135 +msgid "Log output file" +msgstr "" + +#: bpython/args.py:146 msgid "File to execute and additional arguments passed on to the executed script." msgstr "" @@ -82,18 +98,14 @@ msgid "" msgstr "" #: bpython/curtsies.py:136 -msgid "log debug messages to bpython.log" -msgstr "" - -#: bpython/curtsies.py:142 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:148 +#: bpython/curtsies.py:142 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:149 +#: bpython/curtsies.py:143 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" @@ -280,55 +292,55 @@ msgid "" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:324 +#: bpython/curtsiesfrontend/repl.py:325 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:326 +#: bpython/curtsiesfrontend/repl.py:327 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:663 +#: bpython/curtsiesfrontend/repl.py:664 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:681 +#: bpython/curtsiesfrontend/repl.py:682 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:990 +#: bpython/curtsiesfrontend/repl.py:991 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1005 +#: bpython/curtsiesfrontend/repl.py:1006 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1015 +#: bpython/curtsiesfrontend/repl.py:1016 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1026 +#: bpython/curtsiesfrontend/repl.py:1027 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1032 +#: bpython/curtsiesfrontend/repl.py:1033 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1037 +#: bpython/curtsiesfrontend/repl.py:1038 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1043 +#: bpython/curtsiesfrontend/repl.py:1044 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1985 +#: bpython/curtsiesfrontend/repl.py:1986 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po index 87c957390..582442406 100644 --- a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po +++ b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7.1\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-01-29 22:59+0100\n" +"POT-Creation-Date: 2021-02-14 17:29+0100\n" "PO-Revision-Date: 2020-10-29 12:20+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: nl_NL\n" @@ -18,7 +18,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:91 +#: bpython/args.py:49 +msgid "{} version {} on top of Python {} {}" +msgstr "" + +#: bpython/args.py:58 +msgid "{} See AUTHORS.rst for details." +msgstr "" + +#: bpython/args.py:95 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -26,23 +34,31 @@ msgid "" "to the regular Python interpreter." msgstr "" -#: bpython/args.py:101 +#: bpython/args.py:105 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:107 +#: bpython/args.py:111 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:113 +#: bpython/args.py:117 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:119 +#: bpython/args.py:123 msgid "Print version and exit." msgstr "" #: bpython/args.py:130 +msgid "Set log level for logging" +msgstr "" + +#: bpython/args.py:135 +msgid "Log output file" +msgstr "" + +#: bpython/args.py:146 msgid "File to execute and additional arguments passed on to the executed script." msgstr "" @@ -82,18 +98,14 @@ msgid "" msgstr "" #: bpython/curtsies.py:136 -msgid "log debug messages to bpython.log" -msgstr "" - -#: bpython/curtsies.py:142 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:148 +#: bpython/curtsies.py:142 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:149 +#: bpython/curtsies.py:143 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" @@ -280,55 +292,55 @@ msgid "" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:324 +#: bpython/curtsiesfrontend/repl.py:325 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:326 +#: bpython/curtsiesfrontend/repl.py:327 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:663 +#: bpython/curtsiesfrontend/repl.py:664 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:681 +#: bpython/curtsiesfrontend/repl.py:682 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:990 +#: bpython/curtsiesfrontend/repl.py:991 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1005 +#: bpython/curtsiesfrontend/repl.py:1006 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1015 +#: bpython/curtsiesfrontend/repl.py:1016 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1026 +#: bpython/curtsiesfrontend/repl.py:1027 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1032 +#: bpython/curtsiesfrontend/repl.py:1033 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1037 +#: bpython/curtsiesfrontend/repl.py:1038 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1043 +#: bpython/curtsiesfrontend/repl.py:1044 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1985 +#: bpython/curtsiesfrontend/repl.py:1986 msgid "" "\n" "Thanks for using bpython!\n" From 8ba24d1a88ace7bbc73b375d71a737c36932d164 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 14 Feb 2021 18:12:56 +0100 Subject: [PATCH 163/555] Replace custom clipboard code with pyperclip pyperclip supports Wayland and Windows --- README.rst | 1 + bpython/clipboard.py | 78 -------------------------------------------- bpython/repl.py | 18 ++++++---- setup.cfg | 3 +- 4 files changed, 14 insertions(+), 86 deletions(-) delete mode 100644 bpython/clipboard.py diff --git a/README.rst b/README.rst index 54d2e7d86..eb9112463 100644 --- a/README.rst +++ b/README.rst @@ -163,6 +163,7 @@ Dependencies * babel (optional, for internationalization) * jedi (optional, for experimental multiline completion) * watchdog (optional, for monitoring imported modules for changes) +* pyperclip (optional, for copying to the clipboard) bpython-urwid ------------- diff --git a/bpython/clipboard.py b/bpython/clipboard.py deleted file mode 100644 index be17566f4..000000000 --- a/bpython/clipboard.py +++ /dev/null @@ -1,78 +0,0 @@ -# The MIT License -# -# Copyright (c) 2015 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. - - -import subprocess -import os -import platform -from locale import getpreferredencoding - - -class CopyFailed(Exception): - pass - - -class XClipboard: - """Manage clipboard with xclip.""" - - def copy(self, content): - process = subprocess.Popen( - ["xclip", "-i", "-selection", "clipboard"], stdin=subprocess.PIPE - ) - process.communicate(content.encode(getpreferredencoding())) - if process.returncode != 0: - raise CopyFailed() - - -class OSXClipboard: - """Manage clipboard with pbcopy.""" - - def copy(self, content): - process = subprocess.Popen(["pbcopy", "w"], stdin=subprocess.PIPE) - process.communicate(content.encode(getpreferredencoding())) - if process.returncode != 0: - raise CopyFailed() - - -def command_exists(command): - process = subprocess.Popen( - ["which", command], stderr=subprocess.STDOUT, stdout=subprocess.PIPE - ) - process.communicate() - - return process.returncode == 0 - - -def get_clipboard(): - """Get best clipboard handling implementation for current system.""" - - if platform.system() == "Darwin": - if command_exists("pbcopy"): - return OSXClipboard() - if ( - platform.system() in ("Linux", "FreeBSD", "OpenBSD") - and os.getenv("DISPLAY") is not None - ): - if command_exists("xclip"): - return XClipboard() - - return None diff --git a/bpython/repl.py b/bpython/repl.py index 7c827602a..62e008101 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -36,12 +36,17 @@ from enum import Enum from itertools import takewhile from pathlib import Path +from pygments.lexers import Python3Lexer +from pygments.token import Token from types import ModuleType -from pygments.token import Token -from pygments.lexers import Python3Lexer +have_pyperclip = True +try: + import pyperclip +except ImportError: + have_pyperclip = False + from . import autocomplete, inspection, simpleeval -from .clipboard import get_clipboard, CopyFailed from .config import getpreferredencoding from .formatter import Parenthesis from .history import History @@ -430,7 +435,6 @@ def __init__(self, interp, config): # Necessary to fix mercurial.ui.ui expecting sys.stderr to have this # attribute self.closed = False - self.clipboard = get_clipboard() if self.config.hist_file.exists(): try: @@ -862,14 +866,14 @@ def write2file(self): def copy2clipboard(self): """Copy current content to clipboard.""" - if self.clipboard is None: + if not have_pyperclip: self.interact.notify(_("No clipboard available.")) return content = self.get_session_formatted_for_file() try: - self.clipboard.copy(content) - except CopyFailed: + pyperclip.copy(content) + except pyperclip.PyperclipException: self.interact.notify(_("Could not copy to clipboard.")) else: self.interact.notify(_("Copied content to clipboard.")) diff --git a/setup.cfg b/setup.cfg index 7dfb1d58c..8b6177591 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,9 +28,10 @@ install_requires = pyxdg [options.extras_require] +clipboard = pyperclip +jedi = jedi >= 0.16 urwid = urwid watch = watchdog -jedi = jedi >= 0.16 [options.entry_points] console_scripts = From 624116ebe3e768f6935ac65d5bab0f495c80a65b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 14 Feb 2021 21:16:49 +0100 Subject: [PATCH 164/555] Turn FileLock into a wrapper function Also make sure that for UnixFileLock fcntl.LOCK_EX is always set. --- bpython/filelock.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/bpython/filelock.py b/bpython/filelock.py index 72018f832..5a931739e 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2015-2019 Sebastian Ramacher +# Copyright (c) 2015-2021 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 @@ -20,20 +20,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. - +has_fcntl = True try: import fcntl import errno - - has_fcntl = True except ImportError: has_fcntl = False +has_msvcrt = True try: import msvcrt import os - - has_msvcrt = True except ImportError: has_msvcrt = False @@ -41,7 +38,7 @@ class BaseLock: """Base class for file locking""" - def __init__(self, fileobj, mode=None, filename=None): + def __init__(self, fileobj=None): self.fileobj = fileobj self.locked = False @@ -67,12 +64,9 @@ def __del__(self): class UnixFileLock(BaseLock): """Simple file locking for Unix using fcntl""" - def __init__(self, fileobj, mode=None, filename=None): + def __init__(self, fileobj, mode=0): super().__init__(fileobj) - - if mode is None: - mode = fcntl.LOCK_EX - self.mode = mode + self.mode = mode | fcntl.LOCK_EX def acquire(self): try: @@ -90,8 +84,8 @@ def release(self): class WindowsFileLock(BaseLock): """Simple file locking for Windows using msvcrt""" - def __init__(self, fileobj, mode=None, filename=None): - super().__init__(None) + def __init__(self, filename): + super().__init__() self.filename = f"{filename}.lock" def acquire(self): @@ -117,11 +111,12 @@ def release(self): pass -if has_fcntl: - FileLock = UnixFileLock -elif has_msvcrt: - FileLock = WindowsFileLock -else: - FileLock = BaseLock +def FileLock(fileobj, mode=0, filename=None): + if has_fcntl: + return UnixFileLock(fileobj, mode) + elif has_msvcrt: + return WindowsFileLock(filename) + return BaseLock(fileobj) + # vim: sw=4 ts=4 sts=4 ai et From 34f0e700082bf79223689ff120dddf4e46442129 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 10 Mar 2021 23:32:58 +0100 Subject: [PATCH 165/555] Iterate over all completers until the first one with succeeding locate is found (fixes #879) --- bpython/autocomplete.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 477b9651c..f4424f7c8 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -267,7 +267,10 @@ def __init__(self, completers, mode=AutocompleteModes.SIMPLE): super().__init__(True, mode) def locate(self, current_offset, line): - return self._completers[0].locate(current_offset, line) + for completer in self._completers: + return_value = completer.locate(current_offset, line) + if return_value is not None: + return return_value def format(self, word): return self._completers[0].format(word) From 2f4e69a92595321e6ab17ae8e24ddc1f6691e9f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 11 Mar 2021 23:48:01 +0100 Subject: [PATCH 166/555] Assume utf8 encoding (fixes #888) If there is no encoding specified, the default encoding in Python 3 is utf8. --- bpython/inspection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index bf039e612..e4d8abe06 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -326,7 +326,7 @@ def get_encoding(obj): m = get_encoding_line_re.search(line) if m: return m.group(1) - return "ascii" + return "utf8" def get_encoding_file(fname): @@ -337,7 +337,7 @@ def get_encoding_file(fname): match = get_encoding_line_re.search(line) if match: return match.group(1) - return "ascii" + return "utf8" def getattr_safe(obj, name): From 72e1c4b4bbb759f00d62472f241748193a1cd968 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 16 Mar 2021 22:56:49 +0100 Subject: [PATCH 167/555] Also handle function __doc__ where the signature is split over two or more lines --- .github/workflows/build.yaml | 3 ++- bpython/inspection.py | 11 +++++++---- bpython/test/test_inspection.py | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9af5bb048..f01bcb236 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -26,7 +26,8 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install pytest pytest-cov urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" + 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 diff --git a/bpython/inspection.py b/bpython/inspection.py index e4d8abe06..480f59275 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -24,6 +24,7 @@ import inspect import keyword import pydoc +import re from collections import namedtuple from pygments.token import Token @@ -173,7 +174,9 @@ def fixlongargs(f, argspec): argspec[3] = values -getpydocspec_re = LazyReCompile(r"([a-zA-Z_][a-zA-Z0-9_]*?)\((.*?)\)") +getpydocspec_re = LazyReCompile( + r"([a-zA-Z_][a-zA-Z0-9_]*?)\((.*?)\)", re.DOTALL +) def getpydocspec(f, func): @@ -189,10 +192,10 @@ def getpydocspec(f, func): if not hasattr_safe(f, "__name__") or s.groups()[0] != f.__name__: return None - args = list() - defaults = list() + args = [] + defaults = [] varargs = varkwargs = None - kwonly_args = list() + kwonly_args = [] kwonly_defaults = dict() for arg in s.group(2).split(","): arg = arg.strip() diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 885c0dbe1..c34d43f60 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -6,6 +6,11 @@ from bpython.test.fodder import encoding_latin1 from bpython.test.fodder import encoding_utf8 +try: + import numpy +except ImportError: + numpy = None + foo_ascii_only = '''def foo(): """Test""" @@ -113,6 +118,15 @@ def test_get_source_file(self): ) self.assertEqual(encoding, "utf-8") + @unittest.skipUnless(numpy is not None, "requires numpy") + def test_getfuncprops_numpy_array(self): + props = inspection.getfuncprops("array", numpy.array) + + self.assertEqual(props.func, "array") + # This check might need an update in the future, but at least numpy >= 1.18 has + # np.array(object, dtype=None, *, ...). + self.assertEqual(props.argspec.args, ["object", "dtype"]) + class A: a = "a" From 7ed578e16a60bdd58db2dfcca675ba4e14bc8eea Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 16 Mar 2021 23:21:54 +0100 Subject: [PATCH 168/555] Run pytest with -v --- .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 f01bcb236..b877487a7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -37,7 +37,7 @@ jobs: python setup.py build_sphinx_man - name: Test with pytest run: | - pytest --cov=bpython --cov-report=xml + pytest --cov=bpython --cov-report=xml -v - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 env: From 30018e4274122ed5b272eec44e319a49eeb33e36 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 16 Mar 2021 23:39:43 +0100 Subject: [PATCH 169/555] Treat ... in __doc__ as separator between varargs and kwonly args --- bpython/inspection.py | 3 +++ bpython/test/test_inspection.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/bpython/inspection.py b/bpython/inspection.py index 480f59275..222def51c 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -203,6 +203,9 @@ def getpydocspec(f, func): varkwargs = arg[2:] elif arg.startswith("*"): varargs = arg[1:] + elif arg == "...": + # At least print denotes "..." as separator between varargs and kwonly args. + varargs = "" else: arg, _, default = arg.partition("=") if varargs is not None: diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index c34d43f60..bb9d24c60 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -118,6 +118,17 @@ def test_get_source_file(self): ) self.assertEqual(encoding, "utf-8") + def test_getfuncprops_print(self): + props = inspection.getfuncprops("print", print) + + self.assertEqual(props.func, "print") + self.assertIn("end", props.argspec.kwonly) + self.assertIn("file", props.argspec.kwonly) + self.assertIn("flush", props.argspec.kwonly) + self.assertIn("sep", props.argspec.kwonly) + self.assertEqual(props.argspec.kwonly_defaults["file"], "sys.stdout") + self.assertEqual(props.argspec.kwonly_defaults["flush"], "False") + @unittest.skipUnless(numpy is not None, "requires numpy") def test_getfuncprops_numpy_array(self): props = inspection.getfuncprops("array", numpy.array) From d4577d383b124535f1b155ff32bf60e57bcbf94d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 17 Mar 2021 11:46:14 +0100 Subject: [PATCH 170/555] Handle locale specific confirmation --- bpython/curtsiesfrontend/interaction.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index f7265dcc8..a6ad866e6 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -2,6 +2,7 @@ import time import curtsies.events as events +from bpython.translations import _ from bpython.repl import Interaction as BpythonInteraction from bpython.curtsiesfrontend.events import RefreshRequestEvent from bpython.curtsiesfrontend.manual_readline import edit_keys @@ -103,7 +104,7 @@ def process_event(self, e): self.escape() self.request_context.switch(line) elif self.in_confirm: - if e in ("y", "Y"): + if e.lower() == _("y"): self.request_context.switch(True) else: self.request_context.switch(False) From a257b3f98b2e2c0a53d4fa4474ea77714bc014ed Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 17 Mar 2021 11:52:26 +0100 Subject: [PATCH 171/555] Check numpy version --- bpython/test/test_inspection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index bb9d24c60..7c04c521e 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -129,7 +129,10 @@ def test_getfuncprops_print(self): self.assertEqual(props.argspec.kwonly_defaults["file"], "sys.stdout") self.assertEqual(props.argspec.kwonly_defaults["flush"], "False") - @unittest.skipUnless(numpy is not None, "requires numpy") + @unittest.skipUnless( + numpy is not None and numpy.__version__ >= "1.18", + "requires numpy >= 1.18", + ) def test_getfuncprops_numpy_array(self): props = inspection.getfuncprops("array", numpy.array) From 0badb7320048fb69f6c74b9f93ef362752a855c9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 17 Mar 2021 12:04:33 +0100 Subject: [PATCH 172/555] Mention that comments start with # --- bpython/sample-config | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bpython/sample-config b/bpython/sample-config index b61dce5b4..287e8a2fc 100644 --- a/bpython/sample-config +++ b/bpython/sample-config @@ -1,11 +1,13 @@ -# This is a standard python config file -# Valid values can be True, False, integer numbers, strings +# This is a standard Python config file. +# Valid values can be True, False, integer numbers, and strings. +# Lines starting with # are treated as comments. +# # By default bpython will look for $XDG_CONFIG_HOME/bpython/config # ($XDG_CONFIG_HOME defaults to ~/.config) or you can specify a file with the -# --config option on the command line +# --config option on the command line. # -# see http://docs.bpython-interpreter.org/configuration.html -# for all configurable options +# See http://docs.bpython-interpreter.org/configuration.html for all +# configurable options. # General section tag [general] From cb05a2de7c69f880820c812acca8c93f4d4d26e2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 20 Mar 2021 15:49:26 +0100 Subject: [PATCH 173/555] Turn --config into a Path --- bpython/args.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bpython/args.py b/bpython/args.py index f670f8dae..0a60c86ee 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -30,6 +30,7 @@ import logging import os import sys +from pathlib import Path from . import __version__, __copyright__ from .config import default_config_path, loadini, Struct @@ -102,6 +103,7 @@ def callback(group): parser.add_argument( "--config", default=default_config_path(), + type=Path, help=_("Use CONFIG instead of default config file."), ) parser.add_argument( From 447bede5ab7b51458cacffef001097cc23195737 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 20 Mar 2021 16:44:15 +0100 Subject: [PATCH 174/555] Use relative imports --- bpython/curtsiesfrontend/_internal.py | 4 ++-- bpython/curtsiesfrontend/filewatch.py | 2 +- bpython/curtsiesfrontend/interaction.py | 8 ++++---- bpython/curtsiesfrontend/interpreter.py | 4 ++-- bpython/curtsiesfrontend/manual_readline.py | 2 +- bpython/curtsiesfrontend/parse.py | 2 +- bpython/curtsiesfrontend/repl.py | 13 ++++++------- bpython/curtsiesfrontend/replpainter.py | 2 +- 8 files changed, 18 insertions(+), 19 deletions(-) diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index d5e8cb458..f918f6fc5 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -21,7 +21,7 @@ # THE SOFTWARE. import pydoc -import bpython._internal +from .. import _internal class NopPydocPager: @@ -36,7 +36,7 @@ def __call__(self, text): return None -class _Helper(bpython._internal._Helper): +class _Helper(_internal._Helper): def __init__(self, repl=None): self._repl = repl pydoc.pager = self.pager diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 676736737..6657fac17 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -1,7 +1,7 @@ import os from collections import defaultdict -from bpython import importcompletion +from .. import importcompletion try: from watchdog.observers import Observer diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index a6ad866e6..dd9b13a92 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -2,10 +2,10 @@ import time import curtsies.events as events -from bpython.translations import _ -from bpython.repl import Interaction as BpythonInteraction -from bpython.curtsiesfrontend.events import RefreshRequestEvent -from bpython.curtsiesfrontend.manual_readline import edit_keys +from ..translations import _ +from ..repl import Interaction as BpythonInteraction +from ..curtsiesfrontend.events import RefreshRequestEvent +from ..curtsiesfrontend.manual_readline import edit_keys class StatusBar(BpythonInteraction): diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 6d62c65a7..a48bc429a 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -6,8 +6,8 @@ from pygments.formatter import Formatter from pygments.lexers import get_lexer_by_name -from bpython.curtsiesfrontend.parse import parse -from bpython.repl import Interpreter as ReplInterpreter +from ..curtsiesfrontend.parse import parse +from ..repl import Interpreter as ReplInterpreter default_colors = { diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py index d2efe77bb..15aea35c2 100644 --- a/bpython/curtsiesfrontend/manual_readline.py +++ b/bpython/curtsiesfrontend/manual_readline.py @@ -4,7 +4,7 @@ and the cursor location based on http://www.bigsmoke.us/readline/shortcuts""" -from bpython.lazyre import LazyReCompile +from ..lazyre import LazyReCompile import inspect diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index dacb5d6a0..6076a027a 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -8,7 +8,7 @@ ) from functools import partial -from bpython.lazyre import LazyReCompile +from ..lazyre import LazyReCompile COLORS = CURTSIES_COLORS + ("default",) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 9a7ee0e08..2a8c499cb 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -31,17 +31,16 @@ from curtsies.configfile_keynames import keymap as key_dispatch from curtsies.input import is_main_thread -from bpython import __version__ -from bpython.repl import ( +from .. import __version__, autocomplete +from ..repl import ( Repl as BpythonRepl, SourceNotFound, LineTypeTranslator as LineType, ) -from bpython.config import getpreferredencoding -from bpython.formatter import BPythonFormatter -from bpython import autocomplete -from bpython.translations import _ -from bpython.pager import get_pager_command +from ..config import getpreferredencoding +from ..formatter import BPythonFormatter +from ..translations import _ +from ..pager import get_pager_command from . import events as bpythonevents, sitefix, replpainter as paint from .coderunner import ( diff --git a/bpython/curtsiesfrontend/replpainter.py b/bpython/curtsiesfrontend/replpainter.py index d7de5fd0e..00675451d 100644 --- a/bpython/curtsiesfrontend/replpainter.py +++ b/bpython/curtsiesfrontend/replpainter.py @@ -5,7 +5,7 @@ from curtsies.formatstring import linesplit from curtsies.fmtfuncs import bold -from bpython.curtsiesfrontend.parse import func_for_letter +from .parse import func_for_letter logger = logging.getLogger(__name__) From ae67b5a0b46604edc480b88b318685092eecdc37 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Mar 2021 18:45:10 +0200 Subject: [PATCH 175/555] Add type annotations --- bpython/filelock.py | 47 ++++++++++++++++----------- bpython/history.py | 78 ++++++++++++++++++++++++++++++--------------- 2 files changed, 81 insertions(+), 44 deletions(-) diff --git a/bpython/filelock.py b/bpython/filelock.py index 5a931739e..3d77c439c 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -20,6 +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 +from types import TracebackType + has_fcntl = True try: import fcntl @@ -38,25 +41,29 @@ class BaseLock: """Base class for file locking""" - def __init__(self, fileobj=None): - self.fileobj = fileobj + def __init__(self) -> None: self.locked = False - def acquire(self): + def acquire(self) -> None: pass - def release(self): + def release(self) -> None: pass - def __enter__(self): + def __enter__(self) -> "BaseLock": self.acquire() return self - def __exit__(self, *args): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: if self.locked: self.release() - def __del__(self): + def __del__(self) -> None: if self.locked: self.release() @@ -64,11 +71,12 @@ def __del__(self): class UnixFileLock(BaseLock): """Simple file locking for Unix using fcntl""" - def __init__(self, fileobj, mode=0): - super().__init__(fileobj) + def __init__(self, fileobj, mode: int = 0) -> None: + super().__init__() + self.fileobj = fileobj self.mode = mode | fcntl.LOCK_EX - def acquire(self): + def acquire(self) -> None: try: fcntl.flock(self.fileobj, self.mode) self.locked = True @@ -76,7 +84,7 @@ def acquire(self): if e.errno != errno.ENOLCK: raise e - def release(self): + def release(self) -> None: self.locked = False fcntl.flock(self.fileobj, fcntl.LOCK_UN) @@ -84,11 +92,12 @@ def release(self): class WindowsFileLock(BaseLock): """Simple file locking for Windows using msvcrt""" - def __init__(self, filename): + def __init__(self, filename: str) -> None: super().__init__() self.filename = f"{filename}.lock" + self.fileobj = -1 - def acquire(self): + 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 @@ -97,13 +106,13 @@ def acquire(self): self.locked = True - def release(self): + def release(self) -> None: self.locked = False # unlock lock file and remove it msvcrt.locking(self.fileobj, msvcrt.LK_UNLCK, 1) os.close(self.fileobj) - self.fileobj = None + self.fileobj = -1 try: os.remove(self.filename) @@ -111,12 +120,14 @@ def release(self): pass -def FileLock(fileobj, mode=0, filename=None): +def FileLock( + fileobj: IO, mode: int = 0, filename: Optional[str] = None +) -> BaseLock: if has_fcntl: return UnixFileLock(fileobj, mode) - elif has_msvcrt: + elif has_msvcrt and filename is not None: return WindowsFileLock(filename) - return BaseLock(fileobj) + return BaseLock() # vim: sw=4 ts=4 sts=4 ai et diff --git a/bpython/history.py b/bpython/history.py index 683a062f6..4779eb456 100644 --- a/bpython/history.py +++ b/bpython/history.py @@ -1,7 +1,7 @@ # The MIT License # # Copyright (c) 2009 the bpython authors. -# Copyright (c) 2012,2015 Sebastian Ramacher +# Copyright (c) 2012-2021 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 @@ -24,6 +24,7 @@ import os import stat from itertools import islice +from typing import Iterable, Optional, List, TextIO from .translations import _ from .filelock import FileLock @@ -32,7 +33,12 @@ class History: """Stores readline-style history and current place in it""" - def __init__(self, entries=None, duplicates=True, hist_size=100): + def __init__( + self, + entries: Optional[Iterable[str]] = None, + duplicates: bool = True, + hist_size: int = 100, + ) -> None: if entries is None: self.entries = [""] else: @@ -45,10 +51,10 @@ def __init__(self, entries=None, duplicates=True, hist_size=100): self.duplicates = duplicates self.hist_size = hist_size - def append(self, line): + def append(self, line: str) -> None: self.append_to(self.entries, line) - def append_to(self, entries, line): + def append_to(self, entries: List[str], line: str) -> None: line = line.rstrip("\n") if line: if not self.duplicates: @@ -60,15 +66,19 @@ def append_to(self, entries, line): pass entries.append(line) - def first(self): + def first(self) -> str: """Move back to the beginning of the history.""" if not self.is_at_end: self.index = len(self.entries) return self.entries[-self.index] def back( - self, start=True, search=False, target=None, include_current=False - ): + self, + start: bool = True, + search: bool = False, + target: Optional[str] = None, + include_current: bool = False, + ) -> str: """Move one step back in the history.""" if target is None: target = self.saved_line @@ -84,15 +94,17 @@ def back( return self.entry @property - def entry(self): + def entry(self) -> str: """The current entry, which may be the saved line""" return self.entries[-self.index] if self.index else self.saved_line @property - def entries_by_index(self): + def entries_by_index(self) -> List[str]: return list(reversed(self.entries + [self.saved_line])) - def find_match_backward(self, search_term, include_current=False): + def find_match_backward( + self, search_term: str, include_current: bool = False + ) -> int: add = 0 if include_current else 1 start = self.index + add for idx, val in enumerate(islice(self.entries_by_index, start, None)): @@ -100,7 +112,9 @@ def find_match_backward(self, search_term, include_current=False): return idx + add return 0 - def find_partial_match_backward(self, search_term, include_current=False): + def find_partial_match_backward( + self, search_term: str, include_current: bool = False + ) -> int: add = 0 if include_current else 1 start = self.index + add for idx, val in enumerate(islice(self.entries_by_index, start, None)): @@ -109,8 +123,12 @@ def find_partial_match_backward(self, search_term, include_current=False): return 0 def forward( - self, start=True, search=False, target=None, include_current=False - ): + self, + start: bool = True, + search: bool = False, + target: Optional[str] = None, + include_current: bool = False, + ) -> str: """Move one step forward in the history.""" if target is None: target = self.saved_line @@ -128,7 +146,9 @@ def forward( self.index = 0 return self.saved_line - def find_match_forward(self, search_term, include_current=False): + def find_match_forward( + self, search_term: str, include_current: bool = False + ) -> int: add = 0 if include_current else 1 end = max(0, self.index - (1 - add)) for idx in range(end): @@ -137,7 +157,9 @@ def find_match_forward(self, search_term, include_current=False): return idx + (0 if include_current else 1) return self.index - def find_partial_match_forward(self, search_term, include_current=False): + def find_partial_match_forward( + self, search_term: str, include_current: bool = False + ) -> int: add = 0 if include_current else 1 end = max(0, self.index - (1 - add)) for idx in range(end): @@ -146,40 +168,40 @@ def find_partial_match_forward(self, search_term, include_current=False): return idx + add return self.index - def last(self): + def last(self) -> str: """Move forward to the end of the history.""" if not self.is_at_start: self.index = 0 return self.entries[0] @property - def is_at_end(self): + def is_at_end(self) -> bool: return self.index >= len(self.entries) or self.index == -1 @property - def is_at_start(self): + def is_at_start(self) -> bool: return self.index == 0 - def enter(self, line): + def enter(self, line: str) -> None: if self.index == 0: self.saved_line = line - def reset(self): + def reset(self) -> None: self.index = 0 self.saved_line = "" - def load(self, filename, encoding): + def load(self, filename: str, encoding: str) -> None: with open(filename, encoding=encoding, errors="ignore") as hfile: with FileLock(hfile, filename=filename): self.entries = self.load_from(hfile) - def load_from(self, fd): - entries = [] + 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 [""] - def save(self, filename, encoding, lines=0): + def save(self, filename: str, encoding: str, lines: int = 0) -> None: fd = os.open( filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, @@ -189,14 +211,18 @@ def save(self, filename, encoding, lines=0): with FileLock(hfile, filename=filename): self.save_to(hfile, self.entries, lines) - def save_to(self, fd, entries=None, lines=0): + def save_to( + self, fd: TextIO, entries: Optional[List[str]] = None, lines: int = 0 + ) -> None: if entries is None: entries = self.entries for line in entries[-lines:]: fd.write(line) fd.write("\n") - def append_reload_and_write(self, s, filename, encoding): + def append_reload_and_write( + self, s: str, filename: str, encoding: str + ) -> None: if not self.hist_size: return self.append(s) From d89793e365da873edaea5ef9eb9c5d4cca59eb07 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Mar 2021 22:20:57 +0200 Subject: [PATCH 176/555] Use f-strings --- bpython/keys.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bpython/keys.py b/bpython/keys.py index 3da060d2c..8956ba648 100644 --- a/bpython/keys.py +++ b/bpython/keys.py @@ -52,14 +52,14 @@ def __setitem__(self, key, value): # fill dispatch with letters for c in string.ascii_lowercase: - cli_key_dispatch["C-%s" % c] = ( + cli_key_dispatch[f"C-{c}"] = ( chr(string.ascii_lowercase.index(c) + 1), - "^%s" % c.upper(), + f"^{c.upper()}", ) for c in string.ascii_lowercase: - urwid_key_dispatch["C-%s" % c] = "ctrl %s" % c - urwid_key_dispatch["M-%s" % c] = "meta %s" % c + urwid_key_dispatch[f"C-{c}"] = f"ctrl {c}" + urwid_key_dispatch[f"M-{c}"] = f"meta {c}" # fill dispatch with cool characters cli_key_dispatch["C-["] = (chr(27), "^[") @@ -70,7 +70,7 @@ def __setitem__(self, key, value): # fill dispatch with function keys for x in range(1, 13): - cli_key_dispatch["F%d" % x] = ("KEY_F(%d)" % x,) + cli_key_dispatch[f"F{x}"] = (f"KEY_F({x})",) for x in range(1, 13): - urwid_key_dispatch["F%d" % x] = "f%d" % x + urwid_key_dispatch[f"F{x}"] = f"f{x}" From dc032650e8660cd80f0ce1c3f39bce17b98d9574 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Mar 2021 22:22:33 +0200 Subject: [PATCH 177/555] Add type annotations --- bpython/keys.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bpython/keys.py b/bpython/keys.py index 8956ba648..cfcac86be 100644 --- a/bpython/keys.py +++ b/bpython/keys.py @@ -20,16 +20,18 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. - import string +from typing import TypeVar, Generic, Tuple, Dict + +T = TypeVar("T") -class KeyMap: - def __init__(self, default=""): - self.map = {} +class KeyMap(Generic[T]): + def __init__(self, default: T) -> None: + self.map: Dict[str, T] = {} self.default = default - def __getitem__(self, key): + def __getitem__(self, key: str) -> T: if not key: # Unbound key return self.default @@ -40,14 +42,14 @@ def __getitem__(self, key): f"Configured keymap ({key}) does not exist in bpython.keys" ) - def __delitem__(self, key): + def __delitem__(self, key: str): del self.map[key] - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: T): self.map[key] = value -cli_key_dispatch = KeyMap(tuple()) +cli_key_dispatch: KeyMap[Tuple[str, ...]] = KeyMap(tuple()) urwid_key_dispatch = KeyMap("") # fill dispatch with letters From f8d38892258dcbed61450e9006f4937b6567dcc3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Mar 2021 22:34:25 +0200 Subject: [PATCH 178/555] Add type annotations --- bpython/line.py | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index 8d62d7b01..14a2cf8d7 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -6,6 +6,7 @@ from itertools import chain from collections import namedtuple +from typing import Optional from .lazyre import LazyReCompile @@ -14,7 +15,7 @@ current_word_re = LazyReCompile(r"(? Optional[LinePart]: """the object.attribute.attribute just before or under the cursor""" pos = cursor_offset start = pos @@ -33,7 +34,7 @@ def current_word(cursor_offset, line): current_dict_key_re = LazyReCompile(r"""[\w_][\w0-9._]*\[([\w0-9._(), '"]*)""") -def current_dict_key(cursor_offset, line): +def current_dict_key(cursor_offset: int, line: str) -> Optional[LinePart]: """If in dictionary completion, return the current key""" for m in current_dict_key_re.finditer(line): if m.start(1) <= cursor_offset and m.end(1) >= cursor_offset: @@ -44,7 +45,7 @@ def current_dict_key(cursor_offset, line): current_dict_re = LazyReCompile(r"""([\w_][\w0-9._]*)\[([\w0-9._(), '"]*)""") -def current_dict(cursor_offset, line): +def current_dict(cursor_offset: int, line: str) -> Optional[LinePart]: """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 and m.end(2) >= cursor_offset: @@ -58,7 +59,7 @@ def current_dict(cursor_offset, line): ) -def current_string(cursor_offset, line): +def current_string(cursor_offset: int, line: str) -> Optional[LinePart]: """If inside a string of nonzero length, return the string (excluding quotes) @@ -74,7 +75,7 @@ def current_string(cursor_offset, line): current_object_re = LazyReCompile(r"([\w_][\w0-9_]*)[.]") -def current_object(cursor_offset, line): +def current_object(cursor_offset: int, line: str) -> Optional[LinePart]: """If in attribute completion, the object on which attribute should be looked up.""" match = current_word(cursor_offset, line) @@ -95,7 +96,9 @@ def current_object(cursor_offset, line): current_object_attribute_re = LazyReCompile(r"([\w_][\w0-9_]*)[.]?") -def current_object_attribute(cursor_offset, line): +def current_object_attribute( + cursor_offset: int, line: str +) -> Optional[LinePart]: """If in attribute completion, the attribute being completed""" # TODO replace with more general current_expression_attribute match = current_word(cursor_offset, line) @@ -118,7 +121,9 @@ def current_object_attribute(cursor_offset, line): ) -def current_from_import_from(cursor_offset, line): +def current_from_import_from( + cursor_offset: int, line: str +) -> Optional[LinePart]: """If in from import completion, the word after from returns None if cursor not in or just after one of the two interesting @@ -138,7 +143,9 @@ def current_from_import_from(cursor_offset, line): current_from_import_import_re_3 = LazyReCompile(r", *([\w0-9_]*)") -def current_from_import_import(cursor_offset, line): +def current_from_import_import( + cursor_offset: int, line: str +) -> Optional[LinePart]: """If in from import completion, the word after import being completed returns None if cursor not in or just after one of these words @@ -165,7 +172,7 @@ def current_from_import_import(cursor_offset, line): current_import_re_3 = LazyReCompile(r"[,][ ]*([\w0-9_.]*)") -def current_import(cursor_offset, line): +def current_import(cursor_offset: int, line: str) -> Optional[LinePart]: # TODO allow for multiple as's baseline = current_import_re_1.search(line) if baseline is None: @@ -180,12 +187,15 @@ def current_import(cursor_offset, line): end = baseline.end() + m.end(1) if start < cursor_offset and end >= cursor_offset: return LinePart(start, end, m.group(1)) + return None current_method_definition_name_re = LazyReCompile(r"def\s+([a-zA-Z_][\w]*)") -def current_method_definition_name(cursor_offset, line): +def current_method_definition_name( + cursor_offset: int, line: str +) -> Optional[LinePart]: """The name of a method being defined""" for m in current_method_definition_name_re.finditer(line): if m.start(1) <= cursor_offset and m.end(1) >= cursor_offset: @@ -196,7 +206,7 @@ def current_method_definition_name(cursor_offset, line): current_single_word_re = LazyReCompile(r"(? Optional[LinePart]: """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 and m.end(1) >= cursor_offset: @@ -204,7 +214,9 @@ def current_single_word(cursor_offset, line): return None -def current_dotted_attribute(cursor_offset, line): +def current_dotted_attribute( + cursor_offset: int, line: str +) -> Optional[LinePart]: """The dotted attribute-object pair before the cursor""" match = current_word(cursor_offset, line) if match is None: @@ -212,6 +224,7 @@ def current_dotted_attribute(cursor_offset, line): start, end, word = match if "." in word[1:]: return LinePart(start, end, word) + return None current_expression_attribute_re = LazyReCompile( @@ -219,7 +232,9 @@ def current_dotted_attribute(cursor_offset, line): ) -def current_expression_attribute(cursor_offset, line): +def current_expression_attribute( + cursor_offset: int, line: str +) -> Optional[LinePart]: """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): From 79de0c65927b4b27fbb45e95485ac0aeb499ab53 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Mar 2021 22:40:14 +0200 Subject: [PATCH 179/555] Rename duplicated test name --- bpython/test/test_importcompletion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index 1bf40e0d3..1d2b6b68a 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -48,7 +48,7 @@ def test_import(self): ) @unittest.expectedFailure - def test_import_empty(self): + def test_import_blank(self): self.assertSetEqual( self.module_gatherer.complete(7, "import "), {"zzabc", "zzabd", "zzefg"}, From 65fff8655bdc21cb13c9c769f645a690f75acacd Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Mar 2021 22:40:25 +0200 Subject: [PATCH 180/555] Remove duplicated test --- bpython/test/test_importcompletion.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index 1d2b6b68a..3016daf2c 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -126,11 +126,6 @@ def test_from_package(self): self.module_gatherer.complete(17, "from xml import d"), {"dom"} ) - def test_from_package(self): - self.assertSetEqual( - self.module_gatherer.complete(17, "from xml import d"), {"dom"} - ) - class TestAvoidSymbolicLinks(unittest.TestCase): def setUp(self): From 3ded87aad2b8ff227a495812fa633c63f53428c4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Mar 2021 22:42:10 +0200 Subject: [PATCH 181/555] Remove useless branch pathname is never used if is_package is False. --- bpython/importcompletion.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index f298efdbd..b59c17c81 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -185,8 +185,6 @@ def find_modules(self, path): if spec.submodule_search_locations is not None: pathname = spec.submodule_search_locations[0] is_package = True - else: - pathname = spec.origin except (ImportError, OSError, SyntaxError): continue except UnicodeEncodeError: From 2a3cde4ec747c9be1d9666389f93917b9d5df214 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Mar 2021 22:45:36 +0200 Subject: [PATCH 182/555] Add type annotations --- bpython/importcompletion.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index b59c17c81..039335351 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -26,6 +26,7 @@ import sys import warnings from pathlib import Path +from typing import Optional, Set, Generator, Tuple, List from .line import ( current_word, @@ -48,17 +49,17 @@ class ModuleGatherer: - def __init__(self, path=None, skiplist=None): + def __init__(self, path: Optional[Path] = None, skiplist=None) -> None: # The cached list of all known modules - self.modules = set() + self.modules: Set[str] = set() # List of (st_dev, st_ino) to compare against so that paths are not repeated - self.paths = set() + self.paths: Set[Tuple[int, int]] = set() # Patterns to skip self.skiplist = skiplist if skiplist is not None else tuple() self.fully_loaded = False self.find_iterator = self.find_all_modules(path) - def module_matches(self, cw, prefix=""): + def module_matches(self, cw: str, prefix: str = "") -> Set[str]: """Modules names to replace cw with""" full = f"{prefix}.{cw}" if prefix else cw @@ -72,7 +73,9 @@ def module_matches(self, cw, prefix=""): else: return set(matches) - def attr_matches(self, cw, prefix="", only_modules=False): + def attr_matches( + self, cw: str, prefix: str = "", only_modules: bool = False + ) -> Set[str]: """Attributes to replace name with""" full = f"{prefix}.{cw}" if prefix else cw module_name, _, name_after_dot = full.rpartition(".") @@ -96,11 +99,11 @@ def attr_matches(self, cw, prefix="", only_modules=False): return matches - def module_attr_matches(self, name): + def module_attr_matches(self, name: str) -> Set[str]: """Only attributes which are modules to replace name with""" return self.attr_matches(name, prefix="", only_modules=True) - def complete(self, cursor_offset, line): + 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: @@ -136,7 +139,7 @@ def complete(self, cursor_offset, line): else: return None - def find_modules(self, path): + def find_modules(self, path: Path) -> Generator[str, None, None]: """Find all modules (and packages) for a given directory.""" if not path.is_dir(): # Perhaps a zip file @@ -154,7 +157,7 @@ def find_modules(self, path): # Path is not readable return - finder = importlib.machinery.FileFinder(str(path), *LOADERS) + finder = importlib.machinery.FileFinder(str(path), *LOADERS) # type: ignore for p in children: if any(fnmatch.fnmatch(p.name, entry) for entry in self.skiplist): # Path is on skiplist From 8b728d9b3c078fffb21bb014ae68b89bbc953cd3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 7 Apr 2021 21:23:09 +0200 Subject: [PATCH 183/555] Be verbose about Python versions --- bpython/simpleeval.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index ca8f5d51b..d0d1a9d6f 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -23,10 +23,8 @@ """simple evaluation of side-effect free code In order to provide fancy completion, some code can be executed safely. - """ - import ast import sys import builtins @@ -34,9 +32,12 @@ 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, ast.NameConstant) +_name_type_nodes = (ast.Name,) if _is_py38 else (ast.Name, ast.NameConstant) class EvaluationError(Exception): @@ -88,9 +89,9 @@ def simple_eval(node_or_string, namespace=None): def _convert(node): if isinstance(node, ast.Constant): return node.value - elif isinstance(node, _string_type_nodes): + elif not _is_py38 and isinstance(node, _string_type_nodes): return node.s - elif isinstance(node, ast.Num): + elif not _is_py38 and isinstance(node, ast.Num): return node.n elif isinstance(node, ast.Tuple): return tuple(map(_convert, node.elts)) @@ -149,14 +150,16 @@ def _convert(node): return left - right # this is a deviation from literal_eval: we allow indexing - elif isinstance(node, ast.Subscript) and isinstance( - node.slice, ast.Index + 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 ( - sys.version_info[:2] >= (3, 9) + _is_py39 and isinstance(node, ast.Subscript) and isinstance(node.slice, (ast.Constant, ast.Name)) ): From bc1916b91d496f41b72e290e01d7d8cf563674e9 Mon Sep 17 00:00:00 2001 From: oscar Date: Sat, 17 Apr 2021 14:24:48 +0200 Subject: [PATCH 184/555] fixes #841 --- bpython/args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/args.py b/bpython/args.py index 0a60c86ee..429ffff45 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -202,7 +202,7 @@ def exec_code(interpreter, args): spec = importlib.util.spec_from_loader("__console__", loader=None) mod = importlib.util.module_from_spec(spec) sys.modules["__console__"] = mod - interpreter.locals = mod.__dict__ + interpreter.locals.update(mod.__dict__) interpreter.locals["__file__"] = args[0] interpreter.runsource(source, args[0], "exec") sys.argv = old_argv From 2bb62657f9769e301d01f98c53fe929500c85113 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 11 May 2021 23:58:10 +0200 Subject: [PATCH 185/555] Add type hints --- bpython/curtsiesfrontend/events.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bpython/curtsiesfrontend/events.py b/bpython/curtsiesfrontend/events.py index fc0ef53c4..26f105dc9 100644 --- a/bpython/curtsiesfrontend/events.py +++ b/bpython/curtsiesfrontend/events.py @@ -1,5 +1,7 @@ """Non-keyboard events used in bpython curtsies REPL""" + import time +from typing import Sequence import curtsies.events @@ -7,17 +9,17 @@ class ReloadEvent(curtsies.events.Event): """Request to rerun REPL session ASAP because imported modules changed""" - def __init__(self, files_modified=("?",)): + def __init__(self, files_modified: Sequence[str] = ("?",)) -> None: self.files_modified = files_modified - def __repr__(self): - return "" % (" & ".join(self.files_modified)) + def __repr__(self) -> str: + return "".format(" & ".join(self.files_modified)) class RefreshRequestEvent(curtsies.events.Event): """Request to refresh REPL display ASAP""" - def __repr__(self): + def __repr__(self) -> str: return "" @@ -27,11 +29,11 @@ class ScheduledRefreshRequestEvent(curtsies.events.ScheduledEvent): Used to schedule the disappearance of status bar message that only shows for a few seconds""" - def __init__(self, when): + def __init__(self, when: float) -> None: super().__init__(when) - def __repr__(self): - return "" % ( + def __repr__(self) -> str: + return "".format( self.when - time.time() ) @@ -43,5 +45,5 @@ class RunStartupFileEvent(curtsies.events.Event): class UndoEvent(curtsies.events.Event): """Request to undo.""" - def __init__(self, n=1): + def __init__(self, n: int = 1) -> None: self.n = n From 6e402e2cb996946e1f77a2962a45d77e64991e05 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 11 May 2021 23:59:38 +0200 Subject: [PATCH 186/555] Add strings that cause wcwidth to return -1 to assertion message --- bpython/curtsiesfrontend/repl.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 2a8c499cb..bd355943e 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1579,13 +1579,13 @@ def move_screen_up(current_line_start_row): assert cursor_row >= 0 and cursor_column >= 0, ( cursor_row, cursor_column, + self.current_stdouterr_line, + self.stdin.current_line, ) elif self.coderunner.running: # TODO does this ever happen? cursor_row, cursor_column = divmod( - ( - len(self.current_cursor_line_without_suggestion) - + self.cursor_offset - ), + len(self.current_cursor_line_without_suggestion) + + self.cursor_offset, width, ) assert cursor_row >= 0 and cursor_column >= 0, ( @@ -1597,19 +1597,17 @@ def move_screen_up(current_line_start_row): ) else: # Common case for determining cursor position cursor_row, cursor_column = divmod( - ( - wcswidth(self.current_cursor_line_without_suggestion.s) - - wcswidth(self.current_line) - + wcswidth(self.current_line, max(0, self.cursor_offset)) - ) + wcswidth(self.current_cursor_line_without_suggestion.s) + - wcswidth(self.current_line) + + wcswidth(self.current_line, max(0, self.cursor_offset)) + self.number_of_padding_chars_on_current_cursor_line(), width, ) assert cursor_row >= 0 and cursor_column >= 0, ( cursor_row, cursor_column, - len(self.current_cursor_line), - len(self.current_line), + self.current_cursor_line_without_suggestion.s, + self.current_line, self.cursor_offset, ) cursor_row += current_line_start_row From 37d5fddf31161d61b2f6ecee9fdef0be2110f3e8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 12 May 2021 21:33:13 +0200 Subject: [PATCH 187/555] Prepend element instead of appending and reversing --- bpython/history.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/history.py b/bpython/history.py index 4779eb456..1eef7d546 100644 --- a/bpython/history.py +++ b/bpython/history.py @@ -23,7 +23,7 @@ import os import stat -from itertools import islice +from itertools import islice, chain from typing import Iterable, Optional, List, TextIO from .translations import _ @@ -100,7 +100,7 @@ def entry(self) -> str: @property def entries_by_index(self) -> List[str]: - return list(reversed(self.entries + [self.saved_line])) + return list(chain((self.saved_line, ), reversed(self.entries))) def find_match_backward( self, search_term: str, include_current: bool = False From 3193adfd39b1b16ea0a020cc53c225f335f3af7e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 12 May 2021 21:45:44 +0200 Subject: [PATCH 188/555] Replace compiled_regex with cached_property --- bpython/lazyre.py | 35 +++++++++++++++-------------------- requirements.txt | 3 ++- setup.cfg | 7 ++++--- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 67a9de3dd..03708f3d4 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2015 Sebastian Ramacher +# Copyright (c) 2015-2021 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 @@ -20,8 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import functools import re +from typing import Optional, Iterator + +try: + from functools import cached_property +except ImportError: + from backports.cached_property import cached_property # type: ignore class LazyReCompile: @@ -30,32 +35,22 @@ class LazyReCompile: This class allows one to store regular expressions and compiles them on first use.""" - def __init__(self, regex, flags=0): + def __init__(self, regex: str, flags: int = 0) -> None: self.regex = regex self.flags = flags - self.compiled = None - - def compile_regex(method): - @functools.wraps(method) - def _impl(self, *args, **kwargs): - if self.compiled is None: - self.compiled = re.compile(self.regex, self.flags) - return method(self, *args, **kwargs) - return _impl + @cached_property + def compiled(self) -> re.Pattern: + return re.compile(self.regex, self.flags) - @compile_regex - def finditer(self, *args, **kwargs): + def finditer(self, *args, **kwargs) -> Iterator[re.Match]: return self.compiled.finditer(*args, **kwargs) - @compile_regex - def search(self, *args, **kwargs): + def search(self, *args, **kwargs) -> Optional[re.Match]: return self.compiled.search(*args, **kwargs) - @compile_regex - def match(self, *args, **kwargs): + def match(self, *args, **kwargs) -> Optional[re.Match]: return self.compiled.match(*args, **kwargs) - @compile_regex - def sub(self, *args, **kwargs): + def sub(self, *args, **kwargs) -> str: return self.compiled.sub(*args, **kwargs) diff --git a/requirements.txt b/requirements.txt index bcdb090be..41127a240 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ Pygments +backports.cached-property; python_version < "3.9" curtsies >=0.3.5 cwcwidth greenlet pyxdg requests -setuptools +setuptools \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 8b6177591..afde695ad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,12 +20,13 @@ packages = bpython.translations bpdb install_requires = - pygments - requests + backports.cached-property; python_version < "3.9" curtsies >=0.3.5 - greenlet cwcwidth + greenlet + pygments pyxdg + requests [options.extras_require] clipboard = pyperclip From 84a54df6a28c2d0335be5fe96e3f6ba837806462 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 12 May 2021 22:02:54 +0200 Subject: [PATCH 189/555] Python 3.6 does not yet know about re.Pattern --- bpython/lazyre.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 03708f3d4..536275326 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -40,7 +40,7 @@ def __init__(self, regex: str, flags: int = 0) -> None: self.flags = flags @cached_property - def compiled(self) -> re.Pattern: + def compiled(self): return re.compile(self.regex, self.flags) def finditer(self, *args, **kwargs) -> Iterator[re.Match]: From 55a2b5a46ef30f1b3a184dd7ae3a7fe7b8638ff8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 12 May 2021 22:03:09 +0200 Subject: [PATCH 190/555] Apply black --- bpython/history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/history.py b/bpython/history.py index 1eef7d546..dfbab2ada 100644 --- a/bpython/history.py +++ b/bpython/history.py @@ -100,7 +100,7 @@ def entry(self) -> str: @property def entries_by_index(self) -> List[str]: - return list(chain((self.saved_line, ), reversed(self.entries))) + return list(chain((self.saved_line,), reversed(self.entries))) def find_match_backward( self, search_term: str, include_current: bool = False From bf2b32f8bab148b6c8690c4bed94f435f387f4b3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 12 May 2021 22:06:35 +0200 Subject: [PATCH 191/555] Apply black --- bpdb/debugger.py | 2 +- bpython/curtsiesfrontend/interaction.py | 2 +- bpython/simpleeval.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bpdb/debugger.py b/bpdb/debugger.py index d575b2671..b98e9612a 100644 --- a/bpdb/debugger.py +++ b/bpdb/debugger.py @@ -25,7 +25,7 @@ class BPdb(pdb.Pdb): - """ PDB with BPython support. """ + """PDB with BPython support.""" def __init__(self, *args, **kwargs): pdb.Pdb.__init__(self, *args, **kwargs) diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index dd9b13a92..ac92e3fb0 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -167,7 +167,7 @@ def confirm(self, q): return self.main_context.switch(q) def file_prompt(self, s): - """Expected to return a file name, given """ + """Expected to return a file name, given""" self.request_context = greenlet.getcurrent() self.prompt = s self.in_prompt = True diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index d0d1a9d6f..801da318a 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -179,7 +179,7 @@ def _convert(node): def safe_getitem(obj, index): - """ Safely tries to access obj[index] """ + """Safely tries to access obj[index]""" if type(obj) in (list, tuple, dict, bytes, str): try: return obj[index] From 3237223a69fb2acbd526ebf1e7ebed56c39e670c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 12 May 2021 22:11:52 +0200 Subject: [PATCH 192/555] Remove more type annotations that do not work with Python 3.6 --- bpython/lazyre.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 536275326..fbbdd38d8 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -43,13 +43,13 @@ def __init__(self, regex: str, flags: int = 0) -> None: def compiled(self): return re.compile(self.regex, self.flags) - def finditer(self, *args, **kwargs) -> Iterator[re.Match]: + def finditer(self, *args, **kwargs): return self.compiled.finditer(*args, **kwargs) - def search(self, *args, **kwargs) -> Optional[re.Match]: + def search(self, *args, **kwargs): return self.compiled.search(*args, **kwargs) - def match(self, *args, **kwargs) -> Optional[re.Match]: + def match(self, *args, **kwargs): return self.compiled.match(*args, **kwargs) def sub(self, *args, **kwargs) -> str: From 5de943b4ec832596ee5464662aa956a7961775bf Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 12 May 2021 22:30:49 +0200 Subject: [PATCH 193/555] Record relevant versions in log output --- bpython/args.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/bpython/args.py b/bpython/args.py index 429ffff45..25a8d95f7 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -1,7 +1,7 @@ # The MIT License # # Copyright (c) 2008 Bob Farrell -# Copyright (c) 2012-2020 Sebastian Ramacher +# Copyright (c) 2012-2021 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 @@ -26,9 +26,14 @@ """ import argparse +import curtsies +import cwcwidth +import greenlet import importlib.util import logging import os +import pygments +import requests import sys from pathlib import Path @@ -36,6 +41,8 @@ from .config import default_config_path, loadini, Struct from .translations import _ +logger = logging.getLogger(__name__) + class ArgumentParserFailed(ValueError): """Raised by the RaisingOptionParser for a bogus commandline.""" @@ -184,6 +191,25 @@ def callback(group): bpython_logger.addHandler(logging.NullHandler()) curtsies_logger.addHandler(logging.NullHandler()) + logger.info(f"Starting bpython {__version__}") + logger.info(f"Python {sys.executable}: {sys.version_info}") + logger.info(f"curtsies: {curtsies.__version__}") + logger.info(f"cwcwidth: {cwcwidth.__version__}") + logger.info(f"greenlet: {greenlet.__version__}") + logger.info(f"pygments: {pygments.__version__}") + logger.info(f"requests: {requests.__version__}") + logger.info( + "environment:\n{}".format( + "\n".join( + f"{key}: {value}" + for key, value in sorted(os.environ.items()) + if key.startswith("LC") + or key.startswith("LANG") + or key == "TERM" + ) + ) + ) + config = Struct() loadini(config, options.config) From da150e4284d464d5b6e2e6dd605c02d16f0876b1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 12 May 2021 23:30:30 +0200 Subject: [PATCH 194/555] Implement more efficient __contains__ --- bpython/curtsiesfrontend/manual_readline.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py index 15aea35c2..b8e334e8a 100644 --- a/bpython/curtsiesfrontend/manual_readline.py +++ b/bpython/curtsiesfrontend/manual_readline.py @@ -22,13 +22,6 @@ class AbstractEdits: "cut_buffer": "there", } - def __contains__(self, key): - try: - self[key] - except KeyError: - return False - else: - return True def add(self, key, func, overwrite=False): if key in self: @@ -75,6 +68,9 @@ def call_without_cut(self, key, **kwargs): 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 + def __getitem__(self, key): if key in self.simple_edits: return self.simple_edits[key] From c0a783752f70fb0157c7d3cd7554b95e941c4372 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 12 May 2021 23:30:51 +0200 Subject: [PATCH 195/555] Fix function signature --- bpython/curtsiesfrontend/manual_readline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py index b8e334e8a..48f96b097 100644 --- a/bpython/curtsiesfrontend/manual_readline.py +++ b/bpython/curtsiesfrontend/manual_readline.py @@ -156,7 +156,7 @@ def __init__( def add_config_attr(self, config_attr, func): raise NotImplementedError("Config already set on this mapping") - def add(self, key, func): + def add(self, key, func, overwrite=False): raise NotImplementedError("Config already set on this mapping") From 517ae805c3f5f99990267477a424612551b38625 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 12 May 2021 23:31:11 +0200 Subject: [PATCH 196/555] Initialize attributes in base class --- bpython/curtsiesfrontend/manual_readline.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py index 48f96b097..afeb55531 100644 --- a/bpython/curtsiesfrontend/manual_readline.py +++ b/bpython/curtsiesfrontend/manual_readline.py @@ -22,6 +22,12 @@ class AbstractEdits: "cut_buffer": "there", } + def __init__(self, simple_edits=None, cut_buffer_edits=None): + self.simple_edits = {} if simple_edits is None else simple_edits + self.cut_buffer_edits = ( + {} if cut_buffer_edits is None else cut_buffer_edits + ) + self.awaiting_config = {} def add(self, key, func, overwrite=False): if key in self: @@ -104,11 +110,6 @@ class UnconfiguredEdits(AbstractEdits): Keys can't be added twice, config attributes can't be added twice. """ - def __init__(self): - self.simple_edits = {} - self.cut_buffer_edits = {} - self.awaiting_config = {} - def mapping_with_config(self, config, key_dispatch): """Creates a new mapping object by applying a config object""" return ConfiguredEdits( @@ -147,8 +148,7 @@ def __init__( config, key_dispatch, ): - self.simple_edits = dict(simple_edits) - self.cut_buffer_edits = dict(cut_buffer_edits) + super().__init__(dict(simple_edits), dict(cut_buffer_edits)) for attr, func in awaiting_config.items(): for key in key_dispatch[getattr(config, attr)]: super().add(key, func, overwrite=True) From 6527442a2526de96f8463e381edab4fb9d80f554 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 15:13:35 +0200 Subject: [PATCH 197/555] Fix version constraint for backports.cached-property --- requirements.txt | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 41127a240..7f56dc0fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ Pygments -backports.cached-property; python_version < "3.9" +backports.cached-property; python_version < "3.8" curtsies >=0.3.5 cwcwidth greenlet diff --git a/setup.cfg b/setup.cfg index afde695ad..7c41b4788 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ packages = bpython.translations bpdb install_requires = - backports.cached-property; python_version < "3.9" + backports.cached-property; python_version < "3.8" curtsies >=0.3.5 cwcwidth greenlet From dac3dd72bf339d4e8df76cd94e473c8fc43c2e93 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 15:28:19 +0200 Subject: [PATCH 198/555] Add type annotations --- bpython/curtsiesfrontend/parse.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 6076a027a..6a42b3764 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -23,19 +23,23 @@ INVERSE_COLORS["default"] = INVERSE_COLORS[CURTSIES_COLORS[0]] -def func_for_letter(l, default="k"): +def func_for_letter(letter_color_code: str, default: str = "k"): """Returns FmtStr constructor for a bpython-style color code""" - if l == "d": - l = default - elif l == "D": - l = default.upper() - return partial(fmtstr, fg=CNAMES[l.lower()], bold=l.isupper()) + if letter_color_code == "d": + letter_color_code = default + elif letter_color_code == "D": + letter_color_code = default.upper() + return partial( + fmtstr, + fg=CNAMES[letter_color_code.lower()], + bold=letter_color_code.isupper(), + ) -def color_for_letter(l, default="k"): - if l == "d": - l = default - return CNAMES[l.lower()] +def color_for_letter(letter_color_code: str, default: str = "k"): + if letter_color_code == "d": + letter_color_code = default + return CNAMES[letter_color_code.lower()] def parse(s): From 93b1cdc8d23cca8581080ae336c7f2c6c29d2376 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 15:40:15 +0200 Subject: [PATCH 199/555] Add type annotations --- bpython/config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index cdd8ca35d..3e796056a 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -47,12 +47,12 @@ class Struct: to and use for various arbitrary things.""" -def getpreferredencoding(): +def getpreferredencoding() -> str: """Get the user's preferred encoding.""" return locale.getpreferredencoding() or sys.getdefaultencoding() -def can_encode(c): +def can_encode(c: str) -> bool: try: c.encode(getpreferredencoding()) return True @@ -60,22 +60,22 @@ def can_encode(c): return False -def supports_box_chars(): +def supports_box_chars() -> bool: """Check if the encoding supports Unicode box characters.""" return all(map(can_encode, "│─└┘┌┐")) -def get_config_home(): +def get_config_home() -> Path: """Returns the base directory for bpython's configuration files.""" return Path(BaseDirectory.xdg_config_home) / "bpython" -def default_config_path(): +def default_config_path() -> Path: """Returns bpython's default configuration file path.""" return get_config_home() / "config" -def default_editor(): +def default_editor() -> str: """Returns the default editor.""" return os.environ.get("VISUAL", os.environ.get("EDITOR", "vi")) From a34f77f68fdb3f6eae7b1786fc2b62b29cdc77b1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 15:40:26 +0200 Subject: [PATCH 200/555] USe str instead of f-string --- bpython/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/config.py b/bpython/config.py index 3e796056a..6dd471d98 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -87,7 +87,7 @@ def fill_config_with_default_values(config, default_values): for (opt, val) in default_values[section].items(): if not config.has_option(section, opt): - config.set(section, opt, f"{val}") + config.set(section, opt, str(val)) def loadini(struct, config_path): From faa65fc4c5c82e97b83891724aa3bdbde7b13b61 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 15:40:36 +0200 Subject: [PATCH 201/555] Extend error message --- bpython/config.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index 6dd471d98..411aec139 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -194,8 +194,10 @@ def loadini(struct, config_path): except UnicodeDecodeError as e: sys.stderr.write( "Error: Unable to parse config file at '{}' due to an " - "encoding issue. Please make sure to fix the encoding " - "of the file or remove it and then try again.\n".format(config_path) + "encoding issue ({}). Please make sure to fix the encoding " + "of the file or remove it and then try again.\n".format( + config_path, e + ) ) sys.exit(1) From 0acf2688fafb709ab7ca767a95a66d2f4ef7dd0c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 15:59:41 +0200 Subject: [PATCH 202/555] Simplify ps1/ps2 handling --- bpython/repl.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 62e008101..39388eedf 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -459,18 +459,12 @@ def __init__(self, interp, config): ) @property - def ps1(self): - try: - return sys.ps1 - except AttributeError: - return ">>> " + def ps1(self) -> str: + return getattr(sys, "ps1", ">>> ") @property - def ps2(self): - try: - return sys.ps2 - except AttributeError: - return "... " + def ps2(self) -> str: + return getattr(sys, "ps2", "... ") def startup(self): """ From 0ac6f137f28695673859d6d0b611dd0c7bc8a01d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 22:09:27 +0200 Subject: [PATCH 203/555] Use super --- bpython/curtsiesfrontend/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index bd355943e..9b845420f 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1833,7 +1833,7 @@ def prompt_undo(self): return self.take_back_empty_line() def prompt_for_undo(): - n = BpythonRepl.prompt_undo(self) + n = super(BaseRepl, self).prompt_undo() if n > 0: self.request_undo(n=n) From 7f4abb2de9218603a352afac7fd82740504cd617 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 22:09:43 +0200 Subject: [PATCH 204/555] Use list literal --- bpython/curtsiesfrontend/repl.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 9b845420f..c5af8db16 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -2015,11 +2015,10 @@ def key_help_text(self): "yank from buffer", "cut to buffer", ) - pairs = [] - pairs.append( - ["complete history suggestion", "right arrow at end of line"] - ) - pairs.append(["previous match with current line", "up arrow"]) + pairs = [ + ["complete history suggestion", "right arrow at end of line"], + ["previous match with current line", "up arrow"], + ] for functionality, key in ( (attr[:-4].replace("_", " "), getattr(self.config, attr)) for attr in self.config.__dict__ From de90c373930f35fdc32b8f89459001d4fe9605be Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 22:10:16 +0200 Subject: [PATCH 205/555] Add type annotations --- bpython/config.py | 2 +- bpython/inspection.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index 411aec139..693d59904 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -37,7 +37,7 @@ class UnknownColorCode(Exception): - def __init__(self, key, color): + def __init__(self, key: str, color: str) -> None: self.key = key self.color = color diff --git a/bpython/inspection.py b/bpython/inspection.py index 222def51c..4c0dbada4 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -29,6 +29,7 @@ from pygments.token import Token from pygments.lexers import Python3Lexer +from typing import Any from types import MemberDescriptorType from .lazyre import LazyReCompile @@ -53,7 +54,7 @@ class AttrCleaner: """A context manager that tries to make an object not exhibit side-effects on attribute lookup.""" - def __init__(self, obj): + def __init__(self, obj: Any) -> None: self.obj = obj def __enter__(self): From 159e1530ef3264bf20f79b6af396285bea32b373 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 22:28:57 +0200 Subject: [PATCH 206/555] Remove some renames --- bpython/curtsiesfrontend/interaction.py | 6 ++-- bpython/curtsiesfrontend/repl.py | 44 +++++++++++-------------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index ac92e3fb0..71a3ef3eb 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -3,12 +3,12 @@ import curtsies.events as events from ..translations import _ -from ..repl import Interaction as BpythonInteraction +from ..repl import Interaction from ..curtsiesfrontend.events import RefreshRequestEvent from ..curtsiesfrontend.manual_readline import edit_keys -class StatusBar(BpythonInteraction): +class StatusBar(Interaction): """StatusBar and Interaction for Repl Passing of control back and forth between calls that use interact api @@ -157,7 +157,7 @@ def notify(self, msg, n=3, wait_for_keypress=False): self.request_refresh() self.main_context.switch(msg) - # below Really ought to be called from greenlets other than main because + # below really ought to be called from greenlets other than main because # they block def confirm(self, q): """Expected to return True or False, given question prompt q""" diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index c5af8db16..9ed039bef 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1,7 +1,5 @@ -import blessings import contextlib import errno -import greenlet import itertools import logging import os @@ -13,12 +11,8 @@ import time import unicodedata -from pygments import format as pygformat -from pygments.lexers import Python3Lexer -from pygments.formatters import TerminalFormatter - -from cwcwidth import wcswidth - +import blessings +import greenlet from curtsies import ( FSArray, fmtstr, @@ -30,17 +24,10 @@ ) from curtsies.configfile_keynames import keymap as key_dispatch from curtsies.input import is_main_thread - -from .. import __version__, autocomplete -from ..repl import ( - Repl as BpythonRepl, - SourceNotFound, - LineTypeTranslator as LineType, -) -from ..config import getpreferredencoding -from ..formatter import BPythonFormatter -from ..translations import _ -from ..pager import get_pager_command +from cwcwidth import wcswidth +from pygments import format as pygformat +from pygments.formatters import TerminalFormatter +from pygments.lexers import Python3Lexer from . import events as bpythonevents, sitefix, replpainter as paint from .coderunner import ( @@ -49,14 +36,23 @@ ) from .filewatch import ModuleChangedEventHandler from .interaction import StatusBar -from .manual_readline import edit_keys -from .parse import parse as bpythonparse, func_for_letter, color_for_letter -from .preprocess import preprocess from .interpreter import ( Interp, code_finished_will_parse, ) - +from .manual_readline import edit_keys +from .parse import parse as bpythonparse, func_for_letter, color_for_letter +from .preprocess import preprocess +from .. import __version__ +from ..config import getpreferredencoding +from ..formatter import BPythonFormatter +from ..pager import get_pager_command +from ..repl import ( + Repl, + SourceNotFound, + LineTypeTranslator as LineType, +) +from ..translations import _ logger = logging.getLogger(__name__) @@ -271,7 +267,7 @@ def _find_module(self, fullname, path=None): return ImportLoader(self.watcher, loader) -class BaseRepl(BpythonRepl): +class BaseRepl(Repl): """Python Repl Reacts to events like From 8bd20fc9aa96784de011b4c190cb5a19077dba6d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 22:32:57 +0200 Subject: [PATCH 207/555] Remove useless assignment --- bpython/curtsiesfrontend/interaction.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index 71a3ef3eb..2ba225f12 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -171,5 +171,4 @@ def file_prompt(self, s): self.request_context = greenlet.getcurrent() self.prompt = s self.in_prompt = True - result = self.main_context.switch(s) - return result + return self.main_context.switch(s) From bae3fd05a44d1ca22368c25814729ed64f8104c9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 22:49:01 +0200 Subject: [PATCH 208/555] Override get_session_formatted_for_file in curtsies Repl --- bpython/curtsiesfrontend/repl.py | 23 ++++++++++++++-- bpython/repl.py | 45 ++++++++------------------------ 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 9ed039bef..d35317303 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -10,6 +10,7 @@ import tempfile import time import unicodedata +from enum import Enum import blessings import greenlet @@ -50,7 +51,6 @@ from ..repl import ( Repl, SourceNotFound, - LineTypeTranslator as LineType, ) from ..translations import _ @@ -70,6 +70,14 @@ MAX_EVENTS_POSSIBLY_NOT_PASTE = 20 +class LineType(Enum): + """Used when adding a tuple to all_logical_lines, to get input / output values + having to actually type/know the strings""" + + INPUT = "input" + OUTPUT = "output" + + class FakeStdin: """The stdin object user code will reference @@ -365,7 +373,7 @@ def __init__( # Entries are tuples, where # - 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) + # (use LineTypeTranslator.INPUT or LineTypeTranslator.OUTPUT to avoid typing these strings) self.all_logical_lines = [] # formatted version of lines in the buffer kept around so we can @@ -2032,6 +2040,17 @@ def key_help_text(self): "{} : {}".format(func.rjust(max_func), key) for func, key in pairs ) + def get_session_formatted_for_file(self) -> str: + def process(): + for line, lineType in self.all_logical_lines: + if lineType == LineType.INPUT: + yield line + elif line.rstrip(): + yield "# OUT: %s" % line + yield "### %s" % self.current_line + + return "\n".join(process()) + def is_nop(char): return unicodedata.category(str(char)) == "Cc" diff --git a/bpython/repl.py b/bpython/repl.py index 39388eedf..c1ba03617 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -33,7 +33,6 @@ import textwrap import time import traceback -from enum import Enum from itertools import takewhile from pathlib import Path from pygments.lexers import Python3Lexer @@ -355,14 +354,6 @@ class SourceNotFound(Exception): """Exception raised when the requested source could not be found.""" -class LineTypeTranslator(Enum): - """Used when adding a tuple to all_logical_lines, to get input / output values - having to actually type/know the strings""" - - INPUT = "input" - OUTPUT = "output" - - class Repl: """Implements the necessary guff for a Python-repl-alike interface @@ -778,37 +769,23 @@ def line_is_empty(line): indentation = 0 return indentation - def get_session_formatted_for_file(self): + def get_session_formatted_for_file(self) -> str: """Format the stdout buffer to something suitable for writing to disk, i.e. without >>> and ... at input lines and with "# OUT: " prepended to output lines and "### " prepended to current line""" - if hasattr(self, "all_logical_lines"): - # Curtsies - - def process(): - for line, lineType in self.all_logical_lines: - if lineType == LineTypeTranslator.INPUT: - yield line - elif line.rstrip(): - yield "# OUT: %s" % line - yield "### %s" % self.current_line - - return "\n".join(process()) - - else: # cli and Urwid - session_output = self.getstdout() + session_output = self.getstdout() - def process(): - for line in session_output.split("\n"): - if line.startswith(self.ps1): - yield line[len(self.ps1) :] - elif line.startswith(self.ps2): - yield line[len(self.ps2) :] - elif line.rstrip(): - yield f"# OUT: {line}" + def process(): + for line in session_output.split("\n"): + if line.startswith(self.ps1): + yield line[len(self.ps1) :] + elif line.startswith(self.ps2): + yield line[len(self.ps2) :] + elif line.rstrip(): + yield f"# OUT: {line}" - return "\n".join(process()) + return "\n".join(process()) def write2file(self): """Prompt for a filename and write the current contents of the stdout From 626dcce3e9c4f13c4ff1614d0bff9ec6cb620de9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 23:08:53 +0200 Subject: [PATCH 209/555] Fix import --- bpython/test/test_curtsies_repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py index a95990279..f7798ae6f 100644 --- a/bpython/test/test_curtsies_repl.py +++ b/bpython/test/test_curtsies_repl.py @@ -12,7 +12,7 @@ from bpython.curtsiesfrontend import repl as curtsiesrepl from bpython.curtsiesfrontend import interpreter from bpython.curtsiesfrontend import events as bpythonevents -from bpython.repl import LineTypeTranslator as LineType +from bpython.curtsiesfrontend.repl import LineType from bpython import autocomplete from bpython import config from bpython import args From 979f2391af9fae0cc11fd135945cdf69c4af241f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 23:21:40 +0200 Subject: [PATCH 210/555] Fix import --- bpython/curtsiesfrontend/interaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index 2ba225f12..51fd28d35 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -1,6 +1,6 @@ import greenlet import time -import curtsies.events as events +from curtsies import events from ..translations import _ from ..repl import Interaction From dd596b443f9574912ae5618a1536f5859439d0c1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 16 May 2021 23:32:47 +0200 Subject: [PATCH 211/555] Put history files in temporary directory --- bpython/test/test_history.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/bpython/test/test_history.py b/bpython/test/test_history.py index 71d7847ce..544a644eb 100644 --- a/bpython/test/test_history.py +++ b/bpython/test/test_history.py @@ -1,5 +1,6 @@ -import os +import tempfile import unittest +from pathlib import Path from bpython.config import getpreferredencoding from bpython.history import History @@ -7,7 +8,7 @@ class TestHistory(unittest.TestCase): def setUp(self): - self.history = History("#%d" % x for x in range(1000)) + self.history = History(f"#{x}" for x in range(1000)) def test_is_at_start(self): self.history.first() @@ -84,7 +85,8 @@ def test_reset(self): class TestHistoryFileAccess(unittest.TestCase): def setUp(self): - self.filename = "history_temp_file" + self.tempdir = tempfile.TemporaryDirectory() + self.filename = str(Path(self.tempdir.name) / "history_temp_file") self.encoding = getpreferredencoding() with open( @@ -109,21 +111,17 @@ def test_append_reload_and_write(self): def test_save(self): history = History() - history.entries = [] - for line in ["#1", "#2", "#3", "#4"]: + for line in ("#1", "#2", "#3", "#4"): history.append_to(history.entries, line) # save only last 2 lines history.save(self.filename, self.encoding, lines=2) - # empty the list of entries and load again from the file - history.entries = [""] + # load again from the file + history = History() history.load(self.filename, self.encoding) self.assertEqual(history.entries, ["#3", "#4"]) def tearDown(self): - try: - os.remove(self.filename) - except OSError: - pass + self.tempdir = None From 2240883457342242f3b7f08699e913ece7471ef6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 20 May 2021 13:37:11 +0200 Subject: [PATCH 212/555] Move to OFTC channel --- doc/sphinx/source/community.rst | 4 ++-- doc/sphinx/source/contributing.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/sphinx/source/community.rst b/doc/sphinx/source/community.rst index 00b8c4093..911bbc4f6 100644 --- a/doc/sphinx/source/community.rst +++ b/doc/sphinx/source/community.rst @@ -10,8 +10,8 @@ These are the places where you can find us. IRC --- -You can find us in `#bpython `_ on the `Freenode -`_ network. Don't worry when you get no response (this does +You can find us in `#bpython `_ on the `OFTC +`_ network. Don't worry when you get no response (this does not usually happen) but we are all from Europe and when you get to the channel during our nighttime you might have to wait a while for a response. diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index ade107476..84fb2da55 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -10,7 +10,7 @@ these are particularly good ones to start out with. See our section about the :ref:`community` for a list of resources. -`#bpython `_ on Freenode is particularly useful, +`#bpython `_ on OFTC is particularly useful, but you might have to wait for a while to get a question answered depending on the time of day. From 81eb0a5fff60cdb15cefb46b18889fdc1dc51a6b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 18 May 2021 00:07:37 +0200 Subject: [PATCH 213/555] Replace loadini with a Config object --- bpython/args.py | 7 +- bpython/config.py | 551 +++++++++++++------------ bpython/test/test_config.py | 22 +- bpython/test/test_curtsies_painting.py | 3 +- bpython/test/test_curtsies_repl.py | 3 +- bpython/test/test_repl.py | 3 +- doc/sphinx/source/simplerepl.py | 5 +- 7 files changed, 293 insertions(+), 301 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 25a8d95f7..e3095e17c 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -38,7 +38,7 @@ from pathlib import Path from . import __version__, __copyright__ -from .config import default_config_path, loadini, Struct +from .config import default_config_path, Config from .translations import _ logger = logging.getLogger(__name__) @@ -210,10 +210,7 @@ def callback(group): ) ) - config = Struct() - loadini(config, options.config) - - return config, options, options.args + return Config(options.config), options, options.args def exec_code(interpreter, args): diff --git a/bpython/config.py b/bpython/config.py index 693d59904..399782de2 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -21,13 +21,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. - import os import sys import locale from configparser import ConfigParser from itertools import chain from pathlib import Path +from typing import MutableMapping, Mapping, Any, Dict from xdg import BaseDirectory from .autocomplete import AutocompleteModes @@ -90,284 +90,291 @@ def fill_config_with_default_values(config, default_values): config.set(section, opt, str(val)) -def loadini(struct, config_path): - """Loads .ini configuration file and stores its values in struct""" - - config = ConfigParser() - defaults = { - "general": { - "arg_spec": True, - "auto_display_list": True, - "autocomplete_mode": default_completion, - "color_scheme": "default", - "complete_magic_methods": True, - "dedent_after": 1, - "default_autoreload": False, - "editor": default_editor(), - "flush_output": True, - "import_completion_skiplist": ":".join( - ( - # version tracking - ".git", - ".svn", - ".hg" - # XDG - ".config", - ".local", - ".share", - # nodejs - "node_modules", - # PlayOnLinux - "PlayOnLinux's virtual drives", - # wine - "dosdevices", - # Python byte code cache - "__pycache__", +class Config: + def __init__(self, config_path: Path): + """Loads .ini configuration file and stores its values.""" + + config = ConfigParser() + defaults: Dict[str, Dict[str, Any]] = { + "general": { + "arg_spec": True, + "auto_display_list": True, + "autocomplete_mode": default_completion, + "color_scheme": "default", + "complete_magic_methods": True, + "dedent_after": 1, + "default_autoreload": False, + "editor": default_editor(), + "flush_output": True, + "import_completion_skiplist": ":".join( + ( + # version tracking + ".git", + ".svn", + ".hg" + # XDG + ".config", + ".local", + ".share", + # nodejs + "node_modules", + # PlayOnLinux + "PlayOnLinux's virtual drives", + # wine + "dosdevices", + # Python byte code cache + "__pycache__", + ) + ), + "highlight_show_source": True, + "hist_duplicates": True, + "hist_file": "~/.pythonhist", + "hist_length": 1000, + "paste_time": 0.02, + "pastebin_confirm": True, + "pastebin_expiry": "1week", + "pastebin_helper": "", + "pastebin_url": "https://bpaste.net", + "save_append_py": False, + "single_undo_time": 1.0, + "syntax": True, + "tab_length": 4, + "unicode_box": True, + }, + "keyboard": { + "backspace": "C-h", + "beginning_of_line": "C-a", + "clear_line": "C-u", + "clear_screen": "C-l", + "clear_word": "C-w", + "copy_clipboard": "F10", + "cut_to_buffer": "C-k", + "delete": "C-d", + "down_one_line": "C-n", + "edit_config": "F3", + "edit_current_block": "C-x", + "end_of_line": "C-e", + "exit": "", + "external_editor": "F7", + "help": "F1", + "incremental_search": "M-s", + "last_output": "F9", + "left": "C-b", + "pastebin": "F8", + "redo": "C-g", + "reimport": "F6", + "reverse_incremental_search": "M-r", + "right": "C-f", + "save": "C-s", + "search": "C-o", + "show_source": "F2", + "suspend": "C-z", + "toggle_file_watch": "F5", + "transpose_chars": "C-t", + "undo": "C-r", + "up_one_line": "C-p", + "yank_from_buffer": "C-y", + }, + "cli": { + "suggestion_width": 0.8, + "trim_prompts": False, + }, + "curtsies": { + "list_above": False, + "right_arrow_completion": True, + }, + } + + default_keys_to_commands = { + value: key for (key, value) in defaults["keyboard"].items() + } + + fill_config_with_default_values(config, defaults) + try: + config.read(config_path) + except UnicodeDecodeError as e: + sys.stderr.write( + "Error: Unable to parse config file at '{}' due to an " + "encoding issue ({}). Please make sure to fix the encoding " + "of the file or remove it and then try again.\n".format( + config_path, e ) - ), - "highlight_show_source": True, - "hist_duplicates": True, - "hist_file": "~/.pythonhist", - "hist_length": 1000, - "paste_time": 0.02, - "pastebin_confirm": True, - "pastebin_expiry": "1week", - "pastebin_helper": "", - "pastebin_url": "https://bpaste.net", - "save_append_py": False, - "single_undo_time": 1.0, - "syntax": True, - "tab_length": 4, - "unicode_box": True, - }, - "keyboard": { - "backspace": "C-h", - "beginning_of_line": "C-a", - "clear_line": "C-u", - "clear_screen": "C-l", - "clear_word": "C-w", - "copy_clipboard": "F10", - "cut_to_buffer": "C-k", - "delete": "C-d", - "down_one_line": "C-n", - "edit_config": "F3", - "edit_current_block": "C-x", - "end_of_line": "C-e", - "exit": "", - "external_editor": "F7", - "help": "F1", - "incremental_search": "M-s", - "last_output": "F9", - "left": "C-b", - "pastebin": "F8", - "redo": "C-g", - "reimport": "F6", - "reverse_incremental_search": "M-r", - "right": "C-f", - "save": "C-s", - "search": "C-o", - "show_source": "F2", - "suspend": "C-z", - "toggle_file_watch": "F5", - "transpose_chars": "C-t", - "undo": "C-r", - "up_one_line": "C-p", - "yank_from_buffer": "C-y", - }, - "cli": { - "suggestion_width": 0.8, - "trim_prompts": False, - }, - "curtsies": { - "list_above": False, - "right_arrow_completion": True, - }, - } - - default_keys_to_commands = { - value: key for (key, value) in defaults["keyboard"].items() - } - - fill_config_with_default_values(config, defaults) - try: - config.read(config_path) - except UnicodeDecodeError as e: - sys.stderr.write( - "Error: Unable to parse config file at '{}' due to an " - "encoding issue ({}). Please make sure to fix the encoding " - "of the file or remove it and then try again.\n".format( - config_path, e ) + sys.exit(1) + + def get_key_no_doublebind(command): + default_commands_to_keys = defaults["keyboard"] + requested_key = config.get("keyboard", command) + + try: + default_command = default_keys_to_commands[requested_key] + + if default_commands_to_keys[default_command] == config.get( + "keyboard", default_command + ): + setattr(self, "%s_key" % default_command, "") + except KeyError: + pass + + return requested_key + + self.config_path = Path(config_path).absolute() + self.hist_file = Path(config.get("general", "hist_file")).expanduser() + + self.dedent_after = config.getint("general", "dedent_after") + self.tab_length = config.getint("general", "tab_length") + self.auto_display_list = config.getboolean( + "general", "auto_display_list" + ) + self.syntax = config.getboolean("general", "syntax") + self.arg_spec = config.getboolean("general", "arg_spec") + self.paste_time = config.getfloat("general", "paste_time") + self.single_undo_time = config.getfloat("general", "single_undo_time") + self.highlight_show_source = config.getboolean( + "general", "highlight_show_source" + ) + self.editor = config.get("general", "editor") + self.hist_length = config.getint("general", "hist_length") + self.hist_duplicates = config.getboolean("general", "hist_duplicates") + self.flush_output = config.getboolean("general", "flush_output") + self.default_autoreload = config.getboolean( + "general", "default_autoreload" + ) + self.import_completion_skiplist = config.get( + "general", "import_completion_skiplist" + ).split(":") + + self.pastebin_key = get_key_no_doublebind("pastebin") + self.copy_clipboard_key = get_key_no_doublebind("copy_clipboard") + self.save_key = get_key_no_doublebind("save") + self.search_key = get_key_no_doublebind("search") + self.show_source_key = get_key_no_doublebind("show_source") + self.suspend_key = get_key_no_doublebind("suspend") + self.toggle_file_watch_key = get_key_no_doublebind("toggle_file_watch") + self.undo_key = get_key_no_doublebind("undo") + self.redo_key = get_key_no_doublebind("redo") + self.reimport_key = get_key_no_doublebind("reimport") + self.reverse_incremental_search_key = get_key_no_doublebind( + "reverse_incremental_search" + ) + self.incremental_search_key = get_key_no_doublebind( + "incremental_search" + ) + self.up_one_line_key = get_key_no_doublebind("up_one_line") + self.down_one_line_key = get_key_no_doublebind("down_one_line") + self.cut_to_buffer_key = get_key_no_doublebind("cut_to_buffer") + self.yank_from_buffer_key = get_key_no_doublebind("yank_from_buffer") + self.clear_word_key = get_key_no_doublebind("clear_word") + self.backspace_key = get_key_no_doublebind("backspace") + self.clear_line_key = get_key_no_doublebind("clear_line") + self.clear_screen_key = get_key_no_doublebind("clear_screen") + self.delete_key = get_key_no_doublebind("delete") + + self.left_key = get_key_no_doublebind("left") + self.right_key = get_key_no_doublebind("right") + self.end_of_line_key = get_key_no_doublebind("end_of_line") + self.beginning_of_line_key = get_key_no_doublebind("beginning_of_line") + self.transpose_chars_key = get_key_no_doublebind("transpose_chars") + self.exit_key = get_key_no_doublebind("exit") + self.last_output_key = get_key_no_doublebind("last_output") + self.edit_config_key = get_key_no_doublebind("edit_config") + self.edit_current_block_key = get_key_no_doublebind( + "edit_current_block" ) - sys.exit(1) + self.external_editor_key = get_key_no_doublebind("external_editor") + self.help_key = get_key_no_doublebind("help") - def get_key_no_doublebind(command): - default_commands_to_keys = defaults["keyboard"] - requested_key = config.get("keyboard", command) + self.pastebin_confirm = config.getboolean("general", "pastebin_confirm") + self.pastebin_url = config.get("general", "pastebin_url") + self.pastebin_expiry = config.get("general", "pastebin_expiry") + self.pastebin_helper = config.get("general", "pastebin_helper") - try: - default_command = default_keys_to_commands[requested_key] - - if default_commands_to_keys[default_command] == config.get( - "keyboard", default_command - ): - setattr(struct, "%s_key" % default_command, "") - except KeyError: - pass - - return requested_key - - struct.config_path = Path(config_path).absolute() - - struct.dedent_after = config.getint("general", "dedent_after") - struct.tab_length = config.getint("general", "tab_length") - struct.auto_display_list = config.getboolean("general", "auto_display_list") - struct.syntax = config.getboolean("general", "syntax") - struct.arg_spec = config.getboolean("general", "arg_spec") - struct.paste_time = config.getfloat("general", "paste_time") - struct.single_undo_time = config.getfloat("general", "single_undo_time") - struct.highlight_show_source = config.getboolean( - "general", "highlight_show_source" - ) - struct.hist_file = config.get("general", "hist_file") - struct.editor = config.get("general", "editor") - struct.hist_length = config.getint("general", "hist_length") - struct.hist_duplicates = config.getboolean("general", "hist_duplicates") - struct.flush_output = config.getboolean("general", "flush_output") - struct.default_autoreload = config.getboolean( - "general", "default_autoreload" - ) - struct.import_completion_skiplist = config.get( - "general", "import_completion_skiplist" - ).split(":") - - struct.pastebin_key = get_key_no_doublebind("pastebin") - struct.copy_clipboard_key = get_key_no_doublebind("copy_clipboard") - struct.save_key = get_key_no_doublebind("save") - struct.search_key = get_key_no_doublebind("search") - struct.show_source_key = get_key_no_doublebind("show_source") - struct.suspend_key = get_key_no_doublebind("suspend") - struct.toggle_file_watch_key = get_key_no_doublebind("toggle_file_watch") - struct.undo_key = get_key_no_doublebind("undo") - struct.redo_key = get_key_no_doublebind("redo") - struct.reimport_key = get_key_no_doublebind("reimport") - struct.reverse_incremental_search_key = get_key_no_doublebind( - "reverse_incremental_search" - ) - struct.incremental_search_key = get_key_no_doublebind("incremental_search") - struct.up_one_line_key = get_key_no_doublebind("up_one_line") - struct.down_one_line_key = get_key_no_doublebind("down_one_line") - struct.cut_to_buffer_key = get_key_no_doublebind("cut_to_buffer") - struct.yank_from_buffer_key = get_key_no_doublebind("yank_from_buffer") - struct.clear_word_key = get_key_no_doublebind("clear_word") - struct.backspace_key = get_key_no_doublebind("backspace") - struct.clear_line_key = get_key_no_doublebind("clear_line") - struct.clear_screen_key = get_key_no_doublebind("clear_screen") - struct.delete_key = get_key_no_doublebind("delete") - - struct.left_key = get_key_no_doublebind("left") - struct.right_key = get_key_no_doublebind("right") - struct.end_of_line_key = get_key_no_doublebind("end_of_line") - struct.beginning_of_line_key = get_key_no_doublebind("beginning_of_line") - struct.transpose_chars_key = get_key_no_doublebind("transpose_chars") - struct.exit_key = get_key_no_doublebind("exit") - struct.last_output_key = get_key_no_doublebind("last_output") - struct.edit_config_key = get_key_no_doublebind("edit_config") - struct.edit_current_block_key = get_key_no_doublebind("edit_current_block") - struct.external_editor_key = get_key_no_doublebind("external_editor") - struct.help_key = get_key_no_doublebind("help") - - struct.pastebin_confirm = config.getboolean("general", "pastebin_confirm") - struct.pastebin_url = config.get("general", "pastebin_url") - struct.pastebin_expiry = config.get("general", "pastebin_expiry") - struct.pastebin_helper = config.get("general", "pastebin_helper") - - struct.cli_suggestion_width = config.getfloat("cli", "suggestion_width") - struct.cli_trim_prompts = config.getboolean("cli", "trim_prompts") - - struct.complete_magic_methods = config.getboolean( - "general", "complete_magic_methods" - ) - struct.autocomplete_mode = AutocompleteModes.from_string( - config.get("general", "autocomplete_mode") - ) - struct.save_append_py = config.getboolean("general", "save_append_py") - - struct.curtsies_list_above = config.getboolean("curtsies", "list_above") - struct.curtsies_right_arrow_completion = config.getboolean( - "curtsies", "right_arrow_completion" - ) - struct.unicode_box = config.getboolean("general", "unicode_box") - - color_scheme_name = config.get("general", "color_scheme") - - default_colors = { - "keyword": "y", - "name": "c", - "comment": "b", - "string": "m", - "error": "r", - "number": "G", - "operator": "Y", - "punctuation": "y", - "token": "C", - "background": "d", - "output": "w", - "main": "c", - "paren": "R", - "prompt": "c", - "prompt_more": "g", - "right_arrow_suggestion": "K", - } - - if color_scheme_name == "default": - struct.color_scheme = default_colors - else: - struct.color_scheme = dict() - - path = get_config_home() / f"{color_scheme_name}.theme" - try: - load_theme(struct, path, struct.color_scheme, default_colors) - except OSError: - sys.stderr.write( - f"Could not load theme '{color_scheme_name}' from {path}.\n" - ) - sys.exit(1) - except UnknownColorCode as ucc: - sys.stderr.write( - f"Theme '{color_scheme_name}' contains invalid color: {ucc.key} = {ucc.color}.\n" + self.cli_suggestion_width = config.getfloat("cli", "suggestion_width") + self.cli_trim_prompts = config.getboolean("cli", "trim_prompts") + + self.complete_magic_methods = config.getboolean( + "general", "complete_magic_methods" + ) + self.autocomplete_mode = ( + AutocompleteModes.from_string( + config.get("general", "autocomplete_mode") ) - sys.exit(1) + or default_completion + ) + self.save_append_py = config.getboolean("general", "save_append_py") - # expand path of history file - struct.hist_file = Path(struct.hist_file).expanduser() - - # verify completion mode - if struct.autocomplete_mode is None: - struct.autocomplete_mode = default_completion - - # set box drawing characters - if struct.unicode_box and supports_box_chars(): - struct.left_border = "│" - struct.right_border = "│" - struct.top_border = "─" - struct.bottom_border = "─" - struct.left_bottom_corner = "└" - struct.right_bottom_corner = "┘" - struct.left_top_corner = "┌" - struct.right_top_corner = "┐" - else: - struct.left_border = "|" - struct.right_border = "|" - struct.top_border = "-" - struct.bottom_border = "-" - struct.left_bottom_corner = "+" - struct.right_bottom_corner = "+" - struct.left_top_corner = "+" - struct.right_top_corner = "+" - - -def load_theme(struct, path, colors, default_colors): + self.curtsies_list_above = config.getboolean("curtsies", "list_above") + self.curtsies_right_arrow_completion = config.getboolean( + "curtsies", "right_arrow_completion" + ) + self.unicode_box = config.getboolean("general", "unicode_box") + + color_scheme_name = config.get("general", "color_scheme") + + default_colors = { + "keyword": "y", + "name": "c", + "comment": "b", + "string": "m", + "error": "r", + "number": "G", + "operator": "Y", + "punctuation": "y", + "token": "C", + "background": "d", + "output": "w", + "main": "c", + "paren": "R", + "prompt": "c", + "prompt_more": "g", + "right_arrow_suggestion": "K", + } + + if color_scheme_name == "default": + self.color_scheme = default_colors + else: + self.color_scheme = dict() + + path = get_config_home() / f"{color_scheme_name}.theme" + try: + load_theme(path, self.color_scheme, default_colors) + except OSError: + sys.stderr.write( + f"Could not load theme '{color_scheme_name}' from {path}.\n" + ) + sys.exit(1) + except UnknownColorCode as ucc: + sys.stderr.write( + f"Theme '{color_scheme_name}' contains invalid color: {ucc.key} = {ucc.color}.\n" + ) + sys.exit(1) + + # set box drawing characters + if self.unicode_box and supports_box_chars(): + self.left_border = "│" + self.right_border = "│" + self.top_border = "─" + self.bottom_border = "─" + self.left_bottom_corner = "└" + self.right_bottom_corner = "┘" + self.left_top_corner = "┌" + self.right_top_corner = "┐" + else: + self.left_border = "|" + self.right_border = "|" + self.top_border = "-" + self.bottom_border = "-" + self.left_bottom_corner = "+" + self.right_bottom_corner = "+" + self.left_top_corner = "+" + self.right_top_corner = "+" + + +def load_theme( + path: Path, + colors: MutableMapping[str, str], + default_colors: Mapping[str, str], +) -> None: theme = ConfigParser() with open(path) as f: theme.read_file(f) diff --git a/bpython/test/test_config.py b/bpython/test/test_config.py index 76db5d293..2d2e5e820 100644 --- a/bpython/test/test_config.py +++ b/bpython/test/test_config.py @@ -9,33 +9,25 @@ class TestConfig(unittest.TestCase): - def load_temp_config(self, content, struct=None): + def load_temp_config(self, content): """Write config to a temporary file and load it.""" - if struct is None: - struct = config.Struct() - with tempfile.NamedTemporaryFile() as f: f.write(content.encode("utf8")) f.flush() - config.loadini(struct, f.name) - - return struct + return config.Config(f.name) def test_load_theme(self): - struct = config.Struct() - struct.color_scheme = dict() - config.load_theme(struct, TEST_THEME_PATH, struct.color_scheme, dict()) + color_scheme = dict() + config.load_theme(TEST_THEME_PATH, color_scheme, dict()) expected = {"keyword": "y"} - self.assertEqual(struct.color_scheme, expected) + self.assertEqual(color_scheme, expected) defaults = {"name": "c"} expected.update(defaults) - config.load_theme( - struct, TEST_THEME_PATH, struct.color_scheme, defaults - ) - self.assertEqual(struct.color_scheme, expected) + config.load_theme(TEST_THEME_PATH, color_scheme, defaults) + self.assertEqual(color_scheme, expected) def test_keybindings_default_contains_no_duplicates(self): struct = self.load_temp_config("") diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 7ce91d67a..adc63f155 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -25,8 +25,7 @@ def setup_config(): - config_struct = config.Struct() - config.loadini(config_struct, TEST_CONFIG) + config_struct = config.Config(TEST_CONFIG) config_struct.cli_suggestion_width = 1 return config_struct diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py index f7798ae6f..2ed33097b 100644 --- a/bpython/test/test_curtsies_repl.py +++ b/bpython/test/test_curtsies_repl.py @@ -27,8 +27,7 @@ def setup_config(conf): - config_struct = config.Struct() - config.loadini(config_struct, TEST_CONFIG) + config_struct = config.Config(TEST_CONFIG) for key, value in conf.items(): if not hasattr(config_struct, key): raise ValueError(f"{key!r} is not a valid config attribute") diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 2d33430fe..12275e227 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -18,8 +18,7 @@ def setup_config(conf): - config_struct = config.Struct() - config.loadini(config_struct, TEST_CONFIG) + config_struct = config.Config(TEST_CONFIG) if conf is not None and "autocomplete_mode" in conf: config_struct.autocomplete_mode = conf["autocomplete_mode"] return config_struct diff --git a/doc/sphinx/source/simplerepl.py b/doc/sphinx/source/simplerepl.py index e0bddb480..8a8dda74e 100644 --- a/doc/sphinx/source/simplerepl.py +++ b/doc/sphinx/source/simplerepl.py @@ -28,7 +28,7 @@ import logging from bpython import translations -from bpython.config import Struct, loadini, default_config_path +from bpython.config import Config, default_config_path from bpython.curtsiesfrontend import events as bpythonevents from bpython.curtsiesfrontend.repl import BaseRepl from bpython.importcompletion import ModuleGatherer @@ -117,8 +117,7 @@ def get_input(self): def main(args=None, locals_=None, banner=None): translations.init() - config = Struct() - loadini(config, default_config_path()) + config = Config(default_config_path()) module_gatherer = ModuleGatherer() while module_gatherer.find_coroutine(): pass From dec96ef8ba0b49eb59af57ed78376fa300b3b874 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 18 May 2021 00:08:02 +0200 Subject: [PATCH 214/555] Replace Struct with a dataclass --- bpython/cli.py | 31 +++++++++++++++++-------------- bpython/config.py | 5 ----- requirements.txt | 1 + setup.cfg | 1 + 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 6d570c613..8add9f743 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -40,23 +40,23 @@ # - View source doesn't work on windows unless you install the less program (From GnuUtils or Cygwin) -import platform -import os -import sys import curses +import errno +import functools import math +import os +import platform import re -import time -import functools - import struct +import sys +import time +import unicodedata +from dataclasses import dataclass if platform.system() != "Windows": import signal # Windows does not have job control import termios # Windows uses curses import fcntl # Windows uses curses -import unicodedata -import errno # These are used for syntax highlighting @@ -67,7 +67,7 @@ from .formatter import BPythonFormatter # This for config -from .config import Struct, getpreferredencoding +from .config import getpreferredencoding # This for keys from .keys import cli_key_dispatch as key_dispatch @@ -90,6 +90,13 @@ # --- +@dataclass +class ShowListState: + cols: int = 0 + rows: int = 0 + wl: int = 0 + + def calculate_screen_lines(tokens, width, cursor=0): """Given a stream of tokens and a screen width plus an optional initial cursor position, return the amount of needed lines on the @@ -1254,11 +1261,7 @@ def write(self, s): def show_list( self, items, arg_pos, topline=None, formatter=None, current_item=None ): - - shared = Struct() - shared.cols = 0 - shared.rows = 0 - shared.wl = 0 + shared = ShowListState() y, x = self.scr.getyx() h, w = self.scr.getmaxyx() down = y < h // 2 diff --git a/bpython/config.py b/bpython/config.py index 399782de2..3e0aac997 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -42,11 +42,6 @@ def __init__(self, key: str, color: str) -> None: self.color = color -class Struct: - """Simple class for instantiating objects we can add arbitrary attributes - to and use for various arbitrary things.""" - - def getpreferredencoding() -> str: """Get the user's preferred encoding.""" return locale.getpreferredencoding() or sys.getdefaultencoding() diff --git a/requirements.txt b/requirements.txt index 7f56dc0fd..a989bdf40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ Pygments backports.cached-property; python_version < "3.8" curtsies >=0.3.5 cwcwidth +dataclasses; python_version < "3.7" greenlet pyxdg requests diff --git a/setup.cfg b/setup.cfg index 7c41b4788..89b6cde61 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,7 @@ packages = install_requires = backports.cached-property; python_version < "3.8" curtsies >=0.3.5 + dataclasses; python_version < "3.7" cwcwidth greenlet pygments From 1916d28761e863479c5ec168dde932324bebe18e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 18 May 2021 00:08:35 +0200 Subject: [PATCH 215/555] Add type annotations --- bpython/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bpython/config.py b/bpython/config.py index 3e0aac997..b9e739957 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -75,7 +75,9 @@ def default_editor() -> str: return os.environ.get("VISUAL", os.environ.get("EDITOR", "vi")) -def fill_config_with_default_values(config, default_values): +def fill_config_with_default_values( + config: ConfigParser, default_values: Mapping[str, Mapping[str, Any]] +) -> None: for section in default_values.keys(): if not config.has_section(section): config.add_section(section) From 41561936d54e1b5ab2a879141dd61a09802f42f9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 18 May 2021 11:01:39 +0200 Subject: [PATCH 216/555] Make defaults a class variable --- bpython/config.py | 241 +++++++++++++++++++++++----------------------- 1 file changed, 120 insertions(+), 121 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index b9e739957..0fc60f562 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -88,105 +88,120 @@ def fill_config_with_default_values( class Config: - def __init__(self, config_path: Path): + default_colors = { + "keyword": "y", + "name": "c", + "comment": "b", + "string": "m", + "error": "r", + "number": "G", + "operator": "Y", + "punctuation": "y", + "token": "C", + "background": "d", + "output": "w", + "main": "c", + "paren": "R", + "prompt": "c", + "prompt_more": "g", + "right_arrow_suggestion": "K", + } + + defaults: Dict[str, Dict[str, Any]] = { + "general": { + "arg_spec": True, + "auto_display_list": True, + "autocomplete_mode": default_completion, + "color_scheme": "default", + "complete_magic_methods": True, + "dedent_after": 1, + "default_autoreload": False, + "editor": default_editor(), + "flush_output": True, + "import_completion_skiplist": ":".join( + ( + # version tracking + ".git", + ".svn", + ".hg" + # XDG + ".config", + ".local", + ".share", + # nodejs + "node_modules", + # PlayOnLinux + "PlayOnLinux's virtual drives", + # wine + "dosdevices", + # Python byte code cache + "__pycache__", + ) + ), + "highlight_show_source": True, + "hist_duplicates": True, + "hist_file": "~/.pythonhist", + "hist_length": 1000, + "paste_time": 0.02, + "pastebin_confirm": True, + "pastebin_expiry": "1week", + "pastebin_helper": "", + "pastebin_url": "https://bpaste.net", + "save_append_py": False, + "single_undo_time": 1.0, + "syntax": True, + "tab_length": 4, + "unicode_box": True, + }, + "keyboard": { + "backspace": "C-h", + "beginning_of_line": "C-a", + "clear_line": "C-u", + "clear_screen": "C-l", + "clear_word": "C-w", + "copy_clipboard": "F10", + "cut_to_buffer": "C-k", + "delete": "C-d", + "down_one_line": "C-n", + "edit_config": "F3", + "edit_current_block": "C-x", + "end_of_line": "C-e", + "exit": "", + "external_editor": "F7", + "help": "F1", + "incremental_search": "M-s", + "last_output": "F9", + "left": "C-b", + "pastebin": "F8", + "redo": "C-g", + "reimport": "F6", + "reverse_incremental_search": "M-r", + "right": "C-f", + "save": "C-s", + "search": "C-o", + "show_source": "F2", + "suspend": "C-z", + "toggle_file_watch": "F5", + "transpose_chars": "C-t", + "undo": "C-r", + "up_one_line": "C-p", + "yank_from_buffer": "C-y", + }, + "cli": { + "suggestion_width": 0.8, + "trim_prompts": False, + }, + "curtsies": { + "list_above": False, + "right_arrow_completion": True, + }, + } + + def __init__(self, config_path: Path) -> None: """Loads .ini configuration file and stores its values.""" config = ConfigParser() - defaults: Dict[str, Dict[str, Any]] = { - "general": { - "arg_spec": True, - "auto_display_list": True, - "autocomplete_mode": default_completion, - "color_scheme": "default", - "complete_magic_methods": True, - "dedent_after": 1, - "default_autoreload": False, - "editor": default_editor(), - "flush_output": True, - "import_completion_skiplist": ":".join( - ( - # version tracking - ".git", - ".svn", - ".hg" - # XDG - ".config", - ".local", - ".share", - # nodejs - "node_modules", - # PlayOnLinux - "PlayOnLinux's virtual drives", - # wine - "dosdevices", - # Python byte code cache - "__pycache__", - ) - ), - "highlight_show_source": True, - "hist_duplicates": True, - "hist_file": "~/.pythonhist", - "hist_length": 1000, - "paste_time": 0.02, - "pastebin_confirm": True, - "pastebin_expiry": "1week", - "pastebin_helper": "", - "pastebin_url": "https://bpaste.net", - "save_append_py": False, - "single_undo_time": 1.0, - "syntax": True, - "tab_length": 4, - "unicode_box": True, - }, - "keyboard": { - "backspace": "C-h", - "beginning_of_line": "C-a", - "clear_line": "C-u", - "clear_screen": "C-l", - "clear_word": "C-w", - "copy_clipboard": "F10", - "cut_to_buffer": "C-k", - "delete": "C-d", - "down_one_line": "C-n", - "edit_config": "F3", - "edit_current_block": "C-x", - "end_of_line": "C-e", - "exit": "", - "external_editor": "F7", - "help": "F1", - "incremental_search": "M-s", - "last_output": "F9", - "left": "C-b", - "pastebin": "F8", - "redo": "C-g", - "reimport": "F6", - "reverse_incremental_search": "M-r", - "right": "C-f", - "save": "C-s", - "search": "C-o", - "show_source": "F2", - "suspend": "C-z", - "toggle_file_watch": "F5", - "transpose_chars": "C-t", - "undo": "C-r", - "up_one_line": "C-p", - "yank_from_buffer": "C-y", - }, - "cli": { - "suggestion_width": 0.8, - "trim_prompts": False, - }, - "curtsies": { - "list_above": False, - "right_arrow_completion": True, - }, - } - - default_keys_to_commands = { - value: key for (key, value) in defaults["keyboard"].items() - } - - fill_config_with_default_values(config, defaults) + fill_config_with_default_values(config, self.defaults) try: config.read(config_path) except UnicodeDecodeError as e: @@ -199,8 +214,12 @@ def __init__(self, config_path: Path): ) sys.exit(1) - def get_key_no_doublebind(command): - default_commands_to_keys = defaults["keyboard"] + default_keys_to_commands = { + value: key for (key, value) in self.defaults["keyboard"].items() + } + + def get_key_no_doublebind(command: str) -> str: + default_commands_to_keys = self.defaults["keyboard"] requested_key = config.get("keyboard", command) try: @@ -209,7 +228,7 @@ def get_key_no_doublebind(command): if default_commands_to_keys[default_command] == config.get( "keyboard", default_command ): - setattr(self, "%s_key" % default_command, "") + setattr(self, f"{default_command}_key", "") except KeyError: pass @@ -307,34 +326,14 @@ def get_key_no_doublebind(command): self.unicode_box = config.getboolean("general", "unicode_box") color_scheme_name = config.get("general", "color_scheme") - - default_colors = { - "keyword": "y", - "name": "c", - "comment": "b", - "string": "m", - "error": "r", - "number": "G", - "operator": "Y", - "punctuation": "y", - "token": "C", - "background": "d", - "output": "w", - "main": "c", - "paren": "R", - "prompt": "c", - "prompt_more": "g", - "right_arrow_suggestion": "K", - } - if color_scheme_name == "default": - self.color_scheme = default_colors + self.color_scheme = self.default_colors else: self.color_scheme = dict() path = get_config_home() / f"{color_scheme_name}.theme" try: - load_theme(path, self.color_scheme, default_colors) + load_theme(path, self.color_scheme, self.default_colors) except OSError: sys.stderr.write( f"Could not load theme '{color_scheme_name}' from {path}.\n" From 2dfe4ff9dc72474317792f8efb110e755a268818 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 18 May 2021 11:02:30 +0200 Subject: [PATCH 217/555] Reduce verbosity --- bpython/config.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index 0fc60f562..2aee538d9 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -346,24 +346,20 @@ def get_key_no_doublebind(command: str) -> str: sys.exit(1) # set box drawing characters - if self.unicode_box and supports_box_chars(): - self.left_border = "│" - self.right_border = "│" - self.top_border = "─" - self.bottom_border = "─" - self.left_bottom_corner = "└" - self.right_bottom_corner = "┘" - self.left_top_corner = "┌" - self.right_top_corner = "┐" - else: - self.left_border = "|" - self.right_border = "|" - self.top_border = "-" - self.bottom_border = "-" - self.left_bottom_corner = "+" - self.right_bottom_corner = "+" - self.left_top_corner = "+" - self.right_top_corner = "+" + ( + self.left_border, + self.right_border, + self.top_border, + self.bottom_border, + self.left_bottom_corner, + self.right_bottom_corner, + self.left_top_corner, + self.right_top_corner, + ) = ( + ("│", "│", "─", "─", "└", "┘", "┌", "┐") + if self.unicode_box and supports_box_chars() + else ("|", "|", "-", "-", "+", "+", "+", "+") + ) def load_theme( From a7609fd0ebc527491273b9d7a4dca3eba15bfab4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 21 May 2021 09:20:35 +0200 Subject: [PATCH 218/555] Clean up --- bpython/config.py | 1 - bpython/repl.py | 12 ++++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index 2aee538d9..f4de762d6 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -224,7 +224,6 @@ def get_key_no_doublebind(command: str) -> str: try: default_command = default_keys_to_commands[requested_key] - if default_commands_to_keys[default_command] == config.get( "keyboard", default_command ): diff --git a/bpython/repl.py b/bpython/repl.py index c1ba03617..ba9acf4d8 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -859,8 +859,8 @@ def pastebin(self, s=None): _("Pastebin buffer? (y/N) ") ): self.interact.notify(_("Pastebin aborted.")) - return - return self.do_pastebin(s) + else: + return self.do_pastebin(s) def do_pastebin(self, s): """Actually perform the upload.""" @@ -1119,10 +1119,7 @@ def open_in_external_editor(self, filename): def edit_config(self): if not self.config.config_path.is_file(): if self.interact.confirm( - _( - "Config file does not exist - create " - "new from default? (y/N)" - ) + _("Config file does not exist - create new from default? (y/N)") ): try: default_config = pkgutil.get_data( @@ -1148,8 +1145,7 @@ def edit_config(self): if self.open_in_external_editor(self.config.config_path): self.interact.notify( _( - "bpython config file edited. Restart " - "bpython for changes to take effect." + "bpython config file edited. Restart bpython for changes to take effect." ) ) except OSError as e: From feae0f15a3165cc5c02821fafe8c5f574c16f78b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 21 May 2021 09:22:12 +0200 Subject: [PATCH 219/555] Evaluate tuple() and list() --- bpython/simpleeval.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 801da318a..ef6dd53b9 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -63,6 +63,7 @@ def safe_eval(expr, namespace): # * new docstring describing different functionality # * looks up names from namespace # * indexing syntax is allowed +# * evaluates tuple() and list() def simple_eval(node_or_string, namespace=None): """ Safely evaluate an expression node or a string containing a Python @@ -111,6 +112,22 @@ def _convert(node): ): return set() + # this is a deviation from literal_eval: we evaluate tuple() and list() + elif ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "tuple" + and node.args == node.keywords == [] + ): + return tuple() + elif ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "list" + and node.args == node.keywords == [] + ): + return list() + # this is a deviation from literal_eval: we allow non-literals elif isinstance(node, _name_type_nodes): try: From bc7ea75f21f11c14c0615008494eab4b7403af38 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 21 May 2021 21:13:04 +0200 Subject: [PATCH 220/555] Replace ps1/ps2 with the default if values are not usable (fixes #896) This is a workaround and should be replaced with a better solution in the future. --- bpython/curtsiesfrontend/repl.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index d35317303..faacc9c4f 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -13,6 +13,7 @@ from enum import Enum import blessings +import cwcwidth import greenlet from curtsies import ( FSArray, @@ -275,6 +276,14 @@ def _find_module(self, fullname, path=None): return ImportLoader(self.watcher, loader) +def _process_ps(ps, default_ps: str): + """Replace ps1/ps2 with the default if the user specified value contains control characters.""" + if not isinstance(ps, str): + return ps + + return ps if cwcwidth.wcswidth(ps) >= 0 else default_ps + + class BaseRepl(Repl): """Python Repl @@ -2051,6 +2060,14 @@ def process(): return "\n".join(process()) + @property + def ps1(self): + return _process_ps(super().ps1, ">>> ") + + @property + def ps2(self): + return _process_ps(super().ps2, "... ") + def is_nop(char): return unicodedata.category(str(char)) == "Cc" From 7b6ab950402495454ef7ebf5877589a4c26f897e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 21 May 2021 21:17:09 +0200 Subject: [PATCH 221/555] Use f-string --- bpython/curtsiesfrontend/repl.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index faacc9c4f..ba205ad27 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1729,13 +1729,11 @@ def in_paste_mode(self): self.update_completion() def __repr__(self): - s = "" - s += "<" + repr(type(self)) + "\n" - s += " cursor_offset:" + repr(self.cursor_offset) + "\n" - s += " num display lines:" + repr(len(self.display_lines)) + "\n" - s += " lines scrolled down:" + repr(self.scroll_offset) + "\n" - s += ">" - return s + return f"""<{type(self)} + cursor_offset: {self.cursor_offset} + num display lines: {len(self.display_lines)} + lines scrolled down: {self.scroll_offset} +>""" def _get_current_line(self): return self._current_line From fc3e65f0a6f8ae21f41038abfe651905c9e79193 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 21 May 2021 21:27:55 +0200 Subject: [PATCH 222/555] Use property decorator --- bpython/curtsiesfrontend/repl.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index ba205ad27..1c09a196c 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1735,9 +1735,15 @@ def __repr__(self): lines scrolled down: {self.scroll_offset} >""" - def _get_current_line(self): + @property + def current_line(self): + """The current line""" return self._current_line + @current_line.setter + def current_line(self, value): + self._set_current_line(value) + def _set_current_line( self, line, @@ -1758,13 +1764,15 @@ def _set_current_line( self.special_mode = None self.unhighlight_paren() - current_line = property( - _get_current_line, _set_current_line, None, "The current line" - ) - - def _get_cursor_offset(self): + @property + def cursor_offset(self): + """The current cursor offset from the front of the "line".""" return self._cursor_offset + @cursor_offset.setter + def cursor_offset(self, value): + self._set_cursor_offset(value) + def _set_cursor_offset( self, offset, @@ -1787,13 +1795,6 @@ def _set_cursor_offset( self.update_completion() self.unhighlight_paren() - cursor_offset = property( - _get_cursor_offset, - _set_cursor_offset, - None, - "The current cursor offset from the front of the " "line", - ) - def echo(self, msg, redraw=True): """ Notification that redrawing the current line is necessary (we don't From 62833ff51e4c7167d0c84e8605ef584294959c41 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 21 May 2021 21:36:31 +0200 Subject: [PATCH 223/555] Assign color_scheme only once --- bpython/config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index f4de762d6..48686610e 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -324,12 +324,11 @@ def get_key_no_doublebind(command: str) -> str: ) self.unicode_box = config.getboolean("general", "unicode_box") + self.color_scheme = dict() color_scheme_name = config.get("general", "color_scheme") if color_scheme_name == "default": - self.color_scheme = self.default_colors + self.color_scheme.update(self.default_colors) else: - self.color_scheme = dict() - path = get_config_home() / f"{color_scheme_name}.theme" try: load_theme(path, self.color_scheme, self.default_colors) From 3e8711c1290ad619e413ac4dc74d27928e3a5acb Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 21 May 2021 21:42:44 +0200 Subject: [PATCH 224/555] Simplify comparisons --- bpython/line.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index 14a2cf8d7..7ad42d63e 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -22,7 +22,7 @@ def current_word(cursor_offset: int, line: str) -> Optional[LinePart]: end = pos word = None for m in current_word_re.finditer(line): - if m.start(1) < pos and m.end(1) >= pos: + if m.start(1) < pos <= m.end(1): start = m.start(1) end = m.end(1) word = m.group(1) @@ -37,7 +37,7 @@ def current_word(cursor_offset: int, line: str) -> Optional[LinePart]: def current_dict_key(cursor_offset: int, line: str) -> Optional[LinePart]: """If in dictionary completion, return the current key""" for m in current_dict_key_re.finditer(line): - if m.start(1) <= cursor_offset and m.end(1) >= cursor_offset: + if m.start(1) <= cursor_offset <= m.end(1): return LinePart(m.start(1), m.end(1), m.group(1)) return None @@ -48,7 +48,7 @@ def current_dict_key(cursor_offset: int, line: str) -> Optional[LinePart]: def current_dict(cursor_offset: int, line: str) -> Optional[LinePart]: """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 and m.end(2) >= cursor_offset: + if m.start(2) <= cursor_offset <= m.end(2): return LinePart(m.start(1), m.end(1), m.group(1)) return None @@ -67,7 +67,7 @@ def current_string(cursor_offset: int, line: str) -> Optional[LinePart]: string is a string based on previous lines in the buffer.""" for m in current_string_re.finditer(line): i = 3 if m.group(3) else 4 - if m.start(i) <= cursor_offset and m.end(i) >= cursor_offset: + if m.start(i) <= cursor_offset <= m.end(i): return LinePart(m.start(i), m.end(i), m.group(i)) return None @@ -108,10 +108,7 @@ def current_object_attribute( matches = current_object_attribute_re.finditer(word) next(matches) for m in matches: - if ( - m.start(1) + start <= cursor_offset - and m.end(1) + start >= cursor_offset - ): + if m.start(1) + start <= cursor_offset <= m.end(1) + start: return LinePart(m.start(1) + start, m.end(1) + start, m.group(1)) return None @@ -131,8 +128,8 @@ def current_from_import_from( """ # TODO allow for as's for m in current_from_import_from_re.finditer(line): - if (m.start(1) < cursor_offset and m.end(1) >= cursor_offset) or ( - m.start(2) < cursor_offset and m.end(2) >= cursor_offset + if (m.start(1) < cursor_offset <= m.end(1)) or ( + m.start(2) < cursor_offset <= m.end(2) ): return LinePart(m.start(1), m.end(1), m.group(1)) return None @@ -162,7 +159,7 @@ def current_from_import_import( ): start = baseline.end() + m.start(1) end = baseline.end() + m.end(1) - if start < cursor_offset and end >= cursor_offset: + if start < cursor_offset <= end: return LinePart(start, end, m.group(1)) return None @@ -185,7 +182,7 @@ def current_import(cursor_offset: int, line: str) -> Optional[LinePart]: ): start = baseline.end() + m.start(1) end = baseline.end() + m.end(1) - if start < cursor_offset and end >= cursor_offset: + if start < cursor_offset <= end: return LinePart(start, end, m.group(1)) return None @@ -198,7 +195,7 @@ def current_method_definition_name( ) -> Optional[LinePart]: """The name of a method being defined""" for m in current_method_definition_name_re.finditer(line): - if m.start(1) <= cursor_offset and m.end(1) >= cursor_offset: + if m.start(1) <= cursor_offset <= m.end(1): return LinePart(m.start(1), m.end(1), m.group(1)) return None @@ -209,7 +206,7 @@ def current_method_definition_name( def current_single_word(cursor_offset: int, line: str) -> Optional[LinePart]: """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 and m.end(1) >= cursor_offset: + if m.start(1) <= cursor_offset <= m.end(1): return LinePart(m.start(1), m.end(1), m.group(1)) return None @@ -238,6 +235,6 @@ def current_expression_attribute( """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): - if m.start(1) <= cursor_offset and m.end(1) >= cursor_offset: + if m.start(1) <= cursor_offset <= m.end(1): return LinePart(m.start(1), m.end(1), m.group(1)) return None From 9d33cf7136a26b89f51c2050a094d12bf6365dcd Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 21 May 2021 21:53:23 +0200 Subject: [PATCH 225/555] Call super's __init__ --- bpython/curtsiesfrontend/filewatch.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 6657fac17..314767a53 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -26,6 +26,8 @@ def __init__(self, paths, on_change): for path in paths: self._add_module(path) + super().__init__() + def reset(self): self.dirs = defaultdict(set) del self.modules_to_add_later[:] From f7a7181c820b8e8d0061f8c37bf19d577fdddebd Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 21 May 2021 21:53:34 +0200 Subject: [PATCH 226/555] Split only once --- bpython/curtsiesfrontend/repl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 1c09a196c..f38304e9a 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -2077,7 +2077,8 @@ def tabs_to_spaces(line): def _last_word(line): - return line.split().pop() if line.split() else "" + split_line = line.split() + return split_line.pop() if split_line else "" def compress_paste_event(paste_event): From 639c6494bef44a2546784eb657dae63579e61688 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 21 May 2021 21:58:45 +0200 Subject: [PATCH 227/555] Re-organize imports --- bpython/curtsies.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 69d3f175f..b33691a58 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -3,19 +3,18 @@ import sys import curtsies -import curtsies.window -import curtsies.input import curtsies.events +import curtsies.input +import curtsies.window -from .curtsiesfrontend.repl import BaseRepl +from . import args as bpargs, translations, inspection +from .config import Config +from .curtsiesfrontend import events from .curtsiesfrontend.coderunner import SystemExitFromCodeRunner from .curtsiesfrontend.interpreter import Interp -from . import args as bpargs -from . import translations -from .translations import _ -from .curtsiesfrontend import events as bpythonevents -from . import inspection +from .curtsiesfrontend.repl import BaseRepl from .repl import extract_exit_value +from .translations import _ logger = logging.getLogger(__name__) @@ -34,20 +33,18 @@ def __init__(self, config, locals_, banner, interp=None): ) self._request_refresh = self.input_generator.event_trigger( - bpythonevents.RefreshRequestEvent + events.RefreshRequestEvent ) self._schedule_refresh = self.input_generator.scheduled_event_trigger( - bpythonevents.ScheduledRefreshRequestEvent + events.ScheduledRefreshRequestEvent ) self._request_reload = self.input_generator.threadsafe_event_trigger( - bpythonevents.ReloadEvent + events.ReloadEvent ) self.interrupting_refresh = ( self.input_generator.threadsafe_event_trigger(lambda: None) ) - self.request_undo = self.input_generator.event_trigger( - bpythonevents.UndoEvent - ) + self.request_undo = self.input_generator.event_trigger(events.UndoEvent) with self.input_generator: pass # temp hack to get .original_stty @@ -103,7 +100,7 @@ def mainloop(self, interactive=True, paste=None): self.initialize_interp() # run startup file - self.process_event(bpythonevents.RunStartupFileEvent()) + self.process_event(events.RunStartupFileEvent()) # handle paste if paste: From 08d32834e31fe40624c191ba28d6117902af661a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 12:10:39 +0200 Subject: [PATCH 228/555] Remove argument which is the same as the default --- bpython/importcompletion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 039335351..02255a6b3 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -101,7 +101,7 @@ def attr_matches( def module_attr_matches(self, name: str) -> Set[str]: """Only attributes which are modules to replace name with""" - return self.attr_matches(name, prefix="", only_modules=True) + return self.attr_matches(name, only_modules=True) def complete(self, cursor_offset: int, line: str) -> Optional[Set[str]]: """Construct a full list of possibly completions for imports.""" From 213000739176348a992f00467a469c2da656a1b0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 12:11:25 +0200 Subject: [PATCH 229/555] Move str->Path converstion to __init__ --- bpython/importcompletion.py | 42 +++++++++++++++++---------- bpython/test/test_importcompletion.py | 6 ++-- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 02255a6b3..8cb333e89 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -26,7 +26,7 @@ import sys import warnings from pathlib import Path -from typing import Optional, Set, Generator, Tuple, List +from typing import Optional, Set, Generator, Tuple, Sequence, Iterable, Union from .line import ( current_word, @@ -49,15 +49,31 @@ class ModuleGatherer: - def __init__(self, path: Optional[Path] = None, skiplist=None) -> None: - # The cached list of all known modules + def __init__( + self, + paths: Optional[Iterable[Union[str, Path]]] = None, + skiplist: Optional[Sequence[str]] = 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.""" + + # Cached list of all known modules self.modules: Set[str] = set() - # List of (st_dev, st_ino) to compare against so that paths are not repeated + # Set of (st_dev, st_ino) to compare against so that paths are not repeated self.paths: Set[Tuple[int, int]] = set() # Patterns to skip - self.skiplist = skiplist if skiplist is not None else tuple() + self.skiplist: Sequence[str] = ( + skiplist if skiplist is not None else tuple() + ) self.fully_loaded = False - self.find_iterator = self.find_all_modules(path) + + if paths is None: + self.modules.update(sys.builtin_module_names) + paths = sys.path + + self.find_iterator = self.find_all_modules( + (Path(p).resolve() if p else Path.cwd() for p in sys.path) + ) def module_matches(self, cw: str, prefix: str = "") -> Set[str]: """Modules names to replace cw with""" @@ -191,8 +207,7 @@ def find_modules(self, path: Path) -> Generator[str, None, None]: except (ImportError, OSError, SyntaxError): continue except UnicodeEncodeError: - # Happens with Python 3 when there is a filename in some - # invalid encoding + # Happens with Python 3 when there is a filename in some invalid encoding continue else: if is_package: @@ -205,16 +220,13 @@ def find_modules(self, path: Path) -> Generator[str, None, None]: yield f"{name}.{subname}" yield name - def find_all_modules(self, path=None): + def find_all_modules( + self, paths: Iterable[Path] + ) -> Generator[None, None, None]: """Return a list with all modules in `path`, which should be a list of directory names. If path is not given, sys.path will be used.""" - if path is None: - self.modules.update(sys.builtin_module_names) - path = sys.path - - for p in path: - p = Path(p).resolve() if p else Path.cwd() + for p in paths: for module in self.find_modules(p): self.modules.add(module) yield diff --git a/bpython/test/test_importcompletion.py b/bpython/test/test_importcompletion.py index 3016daf2c..814d3c312 100644 --- a/bpython/test/test_importcompletion.py +++ b/bpython/test/test_importcompletion.py @@ -162,9 +162,7 @@ def setUp(self): base_path / "Right", target_is_directory=True ) - self.module_gatherer = ModuleGatherer( - [os.path.abspath(import_test_folder)] - ) + self.module_gatherer = ModuleGatherer((base_path.absolute(),)) while self.module_gatherer.find_coroutine(): pass @@ -211,7 +209,7 @@ def test_issue_847(self): (base_path / "xyzzy" / "plugh" / "bar.py").touch() (base_path / "xyzzy" / "plugh" / "foo.py").touch() - module_gatherer = ModuleGatherer([base_path.absolute()]) + module_gatherer = ModuleGatherer((base_path.absolute(),)) while module_gatherer.find_coroutine(): pass From eb8664d537322701e74dd5fcb68864778e4bc1af Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 12:11:38 +0200 Subject: [PATCH 230/555] Add return type --- bpython/importcompletion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 8cb333e89..eba24ba92 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -231,7 +231,7 @@ def find_all_modules( self.modules.add(module) yield - def find_coroutine(self): + def find_coroutine(self) -> Optional[bool]: if self.fully_loaded: return None From d77dd9866c2968c0f328cccf6c48adc64e090719 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 12:21:23 +0200 Subject: [PATCH 231/555] Fix argument --- bpython/importcompletion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index eba24ba92..14262db4d 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -72,7 +72,7 @@ def __init__( paths = sys.path self.find_iterator = self.find_all_modules( - (Path(p).resolve() if p else Path.cwd() for p in sys.path) + (Path(p).resolve() if p else Path.cwd() for p in paths) ) def module_matches(self, cw: str, prefix: str = "") -> Set[str]: From 6b8c0499e8951205f97f4f45fd560fe351b22deb Mon Sep 17 00:00:00 2001 From: Kadermiyanyedi Date: Sun, 23 May 2021 21:40:59 +0300 Subject: [PATCH 232/555] Fedora installation method added --- README.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.rst b/README.rst index eb9112463..8a26442be 100644 --- a/README.rst +++ b/README.rst @@ -86,6 +86,13 @@ Arch linux uses pacman as the default package manager, and you can use it to ins $ pacman -S bpython +Fedora +~~~~~~~~~~ +Fedora users can install bpython directly from the command line using ``dnf``. + +.. code-block:: bash + + $ dnf install bpython Windows ~~~~~~~ From a905b77cf5ba2193d16786cc42cd55cf5d3132aa Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 21:35:53 +0200 Subject: [PATCH 233/555] Fix type name --- bpython/test/test_autocomplete.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 9b0a4e86d..ad991abe4 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -326,7 +326,7 @@ def test_magic_methods_complete_after_double_underscores(self): ) -Comp = namedtuple("Completion", ["name", "complete"]) +Completion = namedtuple("Completion", ["name", "complete"]) @unittest.skipUnless(has_jedi, "jedi required") @@ -363,7 +363,7 @@ def test_completions_starting_with_different_letters(self): " a", "class Foo:\n a", ["adsf"], - [Comp("Abc", "bc"), Comp("Cbc", "bc")], + [Completion("Abc", "bc"), Completion("Cbc", "bc")], ) self.assertEqual(matches, None) @@ -373,7 +373,7 @@ def test_completions_starting_with_different_cases(self): " a", "class Foo:\n a", ["adsf"], - [Comp("Abc", "bc"), Comp("ade", "de")], + [Completion("Abc", "bc"), Completion("ade", "de")], ) self.assertSetEqual(matches, {"ade"}) From 66e999cac35f5e721a60e69400ab7659e02eeeac Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 24 May 2021 21:48:37 +0200 Subject: [PATCH 234/555] Properly override methods --- bpython/curtsies.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index b33691a58..2bd348335 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -32,19 +32,23 @@ def __init__(self, config, locals_, banner, interp=None): extra_bytes_callback=self.input_generator.unget_bytes, ) - self._request_refresh = self.input_generator.event_trigger( + self._request_refresh_callback = self.input_generator.event_trigger( events.RefreshRequestEvent ) - self._schedule_refresh = self.input_generator.scheduled_event_trigger( - events.ScheduledRefreshRequestEvent + self._schedule_refresh_callback = ( + self.input_generator.scheduled_event_trigger( + events.ScheduledRefreshRequestEvent + ) ) - self._request_reload = self.input_generator.threadsafe_event_trigger( - events.ReloadEvent + self._request_reload_callback = ( + self.input_generator.threadsafe_event_trigger(events.ReloadEvent) ) - self.interrupting_refresh = ( + self._interrupting_refresh_callback = ( self.input_generator.threadsafe_event_trigger(lambda: None) ) - self.request_undo = self.input_generator.event_trigger(events.UndoEvent) + self._request_undo_callback = self.input_generator.event_trigger( + events.UndoEvent + ) with self.input_generator: pass # temp hack to get .original_stty @@ -57,6 +61,21 @@ def __init__(self, config, locals_, banner, interp=None): orig_tcattrs=self.input_generator.original_stty, ) + def _request_refresh(self): + return self._request_refresh_callback() + + def _schedule_refresh(self, when="now"): + return self._schedule_refresh_callback(when) + + def _request_reload(self, files_modified=("?",)): + return self._request_reload_callback(files_modified) + + def interrupting_refresh(self): + return self._interrupting_refresh_callback() + + def request_undo(self, n=1): + return self._request_undo_callback(n) + def get_term_hw(self): return self.window.get_term_hw() From 22ebf12b5b2072664c95486db7e47727fb7e8853 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 25 May 2021 13:29:16 +0200 Subject: [PATCH 235/555] Clean up --- bpython/line.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index 7ad42d63e..e103b9d97 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -11,18 +11,16 @@ from .lazyre import LazyReCompile LinePart = namedtuple("LinePart", ["start", "stop", "word"]) - current_word_re = LazyReCompile(r"(? Optional[LinePart]: """the object.attribute.attribute just before or under the cursor""" - pos = cursor_offset - start = pos - end = pos + start = cursor_offset + end = cursor_offset word = None for m in current_word_re.finditer(line): - if m.start(1) < pos <= m.end(1): + if m.start(1) < cursor_offset <= m.end(1): start = m.start(1) end = m.end(1) word = m.group(1) @@ -82,12 +80,11 @@ def current_object(cursor_offset: int, line: str) -> Optional[LinePart]: if match is None: return None start, end, word = match - s = "" - for m in current_object_re.finditer(word): - if m.end(1) + start < cursor_offset: - if s: - s += "." - s += m.group(1) + s = ".".join( + m.group(1) + for m in current_object_re.finditer(word) + if m.end(1) + start < cursor_offset + ) if not s: return None return LinePart(start, start + len(s), s) From c21753bf74e29343fbfd08af7a36180936e99c27 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 25 May 2021 17:56:28 +0200 Subject: [PATCH 236/555] Mark regexes as "private" --- bpython/line.py | 66 +++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index e103b9d97..431f0de2d 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -11,7 +11,7 @@ from .lazyre import LazyReCompile LinePart = namedtuple("LinePart", ["start", "stop", "word"]) -current_word_re = LazyReCompile(r"(? Optional[LinePart]: @@ -19,7 +19,7 @@ def current_word(cursor_offset: int, line: str) -> Optional[LinePart]: start = cursor_offset end = cursor_offset word = None - for m in current_word_re.finditer(line): + for m in _current_word_re.finditer(line): if m.start(1) < cursor_offset <= m.end(1): start = m.start(1) end = m.end(1) @@ -29,29 +29,29 @@ def current_word(cursor_offset: int, line: str) -> Optional[LinePart]: return LinePart(start, end, word) -current_dict_key_re = LazyReCompile(r"""[\w_][\w0-9._]*\[([\w0-9._(), '"]*)""") +_current_dict_key_re = LazyReCompile(r"""[\w_][\w0-9._]*\[([\w0-9._(), '"]*)""") def current_dict_key(cursor_offset: int, line: str) -> Optional[LinePart]: """If in dictionary completion, return the current key""" - for m in current_dict_key_re.finditer(line): + for m in _current_dict_key_re.finditer(line): if m.start(1) <= cursor_offset <= m.end(1): return LinePart(m.start(1), m.end(1), m.group(1)) return None -current_dict_re = LazyReCompile(r"""([\w_][\w0-9._]*)\[([\w0-9._(), '"]*)""") +_current_dict_re = LazyReCompile(r"""([\w_][\w0-9._]*)\[([\w0-9._(), '"]*)""") def current_dict(cursor_offset: int, line: str) -> Optional[LinePart]: """If in dictionary completion, return the dict that should be used""" - for m in current_dict_re.finditer(line): + for m in _current_dict_re.finditer(line): if m.start(2) <= cursor_offset <= m.end(2): return LinePart(m.start(1), m.end(1), m.group(1)) return None -current_string_re = LazyReCompile( +_current_string_re = LazyReCompile( '''(?P(?:""")|"|(?:''\')|')(?:((?P.+?)(?P=open))|''' """(?P.+))""" ) @@ -63,14 +63,14 @@ def current_string(cursor_offset: int, line: str) -> Optional[LinePart]: Weaker than bpython.Repl's current_string, because that checks that a string is a string based on previous lines in the buffer.""" - for m in current_string_re.finditer(line): + for m in _current_string_re.finditer(line): i = 3 if m.group(3) else 4 if m.start(i) <= cursor_offset <= m.end(i): return LinePart(m.start(i), m.end(i), m.group(i)) return None -current_object_re = LazyReCompile(r"([\w_][\w0-9_]*)[.]") +_current_object_re = LazyReCompile(r"([\w_][\w0-9_]*)[.]") def current_object(cursor_offset: int, line: str) -> Optional[LinePart]: @@ -82,7 +82,7 @@ def current_object(cursor_offset: int, line: str) -> Optional[LinePart]: start, end, word = match s = ".".join( m.group(1) - for m in current_object_re.finditer(word) + for m in _current_object_re.finditer(word) if m.end(1) + start < cursor_offset ) if not s: @@ -90,7 +90,7 @@ def current_object(cursor_offset: int, line: str) -> Optional[LinePart]: return LinePart(start, start + len(s), s) -current_object_attribute_re = LazyReCompile(r"([\w_][\w0-9_]*)[.]?") +_current_object_attribute_re = LazyReCompile(r"([\w_][\w0-9_]*)[.]?") def current_object_attribute( @@ -102,7 +102,7 @@ def current_object_attribute( if match is None: return None start, end, word = match - matches = current_object_attribute_re.finditer(word) + matches = _current_object_attribute_re.finditer(word) next(matches) for m in matches: if m.start(1) + start <= cursor_offset <= m.end(1) + start: @@ -110,7 +110,7 @@ def current_object_attribute( return None -current_from_import_from_re = LazyReCompile( +_current_from_import_from_re = LazyReCompile( r"from +([\w0-9_.]*)(?:\s+import\s+([\w0-9_]+[,]?\s*)+)*" ) @@ -124,7 +124,7 @@ def current_from_import_from( parts of an import: from (module) import (name1, name2) """ # TODO allow for as's - for m in current_from_import_from_re.finditer(line): + for m in _current_from_import_from_re.finditer(line): if (m.start(1) < cursor_offset <= m.end(1)) or ( m.start(2) < cursor_offset <= m.end(2) ): @@ -132,9 +132,11 @@ def current_from_import_from( return None -current_from_import_import_re_1 = LazyReCompile(r"from\s+([\w0-9_.]*)\s+import") -current_from_import_import_re_2 = LazyReCompile(r"([\w0-9_]+)") -current_from_import_import_re_3 = LazyReCompile(r", *([\w0-9_]*)") +_current_from_import_import_re_1 = LazyReCompile( + r"from\s+([\w0-9_.]*)\s+import" +) +_current_from_import_import_re_2 = LazyReCompile(r"([\w0-9_]+)") +_current_from_import_import_re_3 = LazyReCompile(r", *([\w0-9_]*)") def current_from_import_import( @@ -144,15 +146,15 @@ def current_from_import_import( returns None if cursor not in or just after one of these words """ - baseline = current_from_import_import_re_1.search(line) + baseline = _current_from_import_import_re_1.search(line) if baseline is None: return None - match1 = current_from_import_import_re_2.search(line[baseline.end() :]) + match1 = _current_from_import_import_re_2.search(line[baseline.end() :]) if match1 is None: return None for m in chain( (match1,), - current_from_import_import_re_3.finditer(line[baseline.end() :]), + _current_from_import_import_re_3.finditer(line[baseline.end() :]), ): start = baseline.end() + m.start(1) end = baseline.end() + m.end(1) @@ -161,21 +163,21 @@ def current_from_import_import( return None -current_import_re_1 = LazyReCompile(r"import") -current_import_re_2 = LazyReCompile(r"([\w0-9_.]+)") -current_import_re_3 = LazyReCompile(r"[,][ ]*([\w0-9_.]*)") +_current_import_re_1 = LazyReCompile(r"import") +_current_import_re_2 = LazyReCompile(r"([\w0-9_.]+)") +_current_import_re_3 = LazyReCompile(r"[,][ ]*([\w0-9_.]*)") def current_import(cursor_offset: int, line: str) -> Optional[LinePart]: # TODO allow for multiple as's - baseline = current_import_re_1.search(line) + baseline = _current_import_re_1.search(line) if baseline is None: return None - match1 = current_import_re_2.search(line[baseline.end() :]) + match1 = _current_import_re_2.search(line[baseline.end() :]) if match1 is None: return None for m in chain( - (match1,), current_import_re_3.finditer(line[baseline.end() :]) + (match1,), _current_import_re_3.finditer(line[baseline.end() :]) ): start = baseline.end() + m.start(1) end = baseline.end() + m.end(1) @@ -184,25 +186,25 @@ def current_import(cursor_offset: int, line: str) -> Optional[LinePart]: return None -current_method_definition_name_re = LazyReCompile(r"def\s+([a-zA-Z_][\w]*)") +_current_method_definition_name_re = LazyReCompile(r"def\s+([a-zA-Z_][\w]*)") def current_method_definition_name( cursor_offset: int, line: str ) -> Optional[LinePart]: """The name of a method being defined""" - for m in current_method_definition_name_re.finditer(line): + for m in _current_method_definition_name_re.finditer(line): if m.start(1) <= cursor_offset <= m.end(1): return LinePart(m.start(1), m.end(1), m.group(1)) return None -current_single_word_re = LazyReCompile(r"(? Optional[LinePart]: """the un-dotted word just before or under the cursor""" - for m in current_single_word_re.finditer(line): + for m in _current_single_word_re.finditer(line): if m.start(1) <= cursor_offset <= m.end(1): return LinePart(m.start(1), m.end(1), m.group(1)) return None @@ -221,7 +223,7 @@ def current_dotted_attribute( return None -current_expression_attribute_re = LazyReCompile( +_current_expression_attribute_re = LazyReCompile( r"[.]\s*((?:[\w_][\w0-9_]*)|(?:))" ) @@ -231,7 +233,7 @@ def current_expression_attribute( ) -> Optional[LinePart]: """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): + for m in _current_expression_attribute_re.finditer(line): if m.start(1) <= cursor_offset <= m.end(1): return LinePart(m.start(1), m.end(1), m.group(1)) return None From 90ab512547fd154fbc0cdd7b7c57970ddb25d2b8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 30 May 2021 22:59:59 +0200 Subject: [PATCH 237/555] Replace namedtuple with typing.NamedTuple --- bpython/line.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index 431f0de2d..7ced3bf1d 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -5,12 +5,17 @@ word.""" from itertools import chain -from collections import namedtuple -from typing import Optional +from typing import Optional, NamedTuple from .lazyre import LazyReCompile -LinePart = namedtuple("LinePart", ["start", "stop", "word"]) + +class LinePart(NamedTuple): + start: int + stop: int + word: str + + _current_word_re = LazyReCompile(r"(? Date: Sun, 30 May 2021 23:01:39 +0200 Subject: [PATCH 238/555] Fix name of LinePart member --- bpython/autocomplete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index f4424f7c8..975a2622c 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -244,7 +244,7 @@ def substitute(self, cursor_offset, line, match): """Returns a cursor offset and line with match swapped in""" lpart = self.locate(cursor_offset, line) offset = lpart.start + len(match) - changed_line = line[: lpart.start] + match + line[lpart.end :] + changed_line = line[: lpart.start] + match + line[lpart.stop :] return offset, changed_line @property From 7f325dfd45ee60765f4346054fd23de29da31044 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 1 Aug 2021 17:13:23 +0200 Subject: [PATCH 239/555] Handle OSError from stat (fixes #902) --- bpython/importcompletion.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 14262db4d..00bf99f31 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -212,7 +212,10 @@ def find_modules(self, path: Path) -> Generator[str, None, None]: else: if is_package: path_real = Path(pathname).resolve() - stat = path_real.stat() + try: + stat = path_real.stat() + except OSError: + continue if (stat.st_dev, stat.st_ino) not in self.paths: self.paths.add((stat.st_dev, stat.st_ino)) for subname in self.find_modules(path_real): From 5eb31eb64ea02b9fdfae5a468d3f01b574352dd4 Mon Sep 17 00:00:00 2001 From: Geoff Maciolek Date: Thu, 5 Aug 2021 10:18:26 -0400 Subject: [PATCH 240/555] Grammatical & formatting improvements: README.rst A bit of editing to move closer to standard U.S. English grammar & conventions. (Also, wrapping some terms in backticks for clarity & consistency.) --- README.rst | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 8a26442be..539d036bd 100644 --- a/README.rst +++ b/README.rst @@ -29,7 +29,7 @@ few ideas in a practical, useful, and lightweight manner. bpython is a great replacement to any occasion where you would normally use the vanilla Python interpreter - testing out solutions to people's problems on IRC, quickly testing a method of doing something without creating a temporary file, -etc.. +etc. You can find more about bpython - including `full documentation`_ - at our `homepage`_. @@ -59,14 +59,14 @@ Python. Installation via OS Package Manager ----------------------------------- -The majority of operating system of desktop computers comes with a package -manager system, if you are any user of them, you can install ``bpython`` -using the package manager. +The majority of desktop computer operating systems come with package management +systems. If you use one of these OSes, you can install ``bpython`` using the +package manager. Ubuntu/Debian ~~~~~~~~~~~~~ -Ubuntu/Debian family Linux users and install bpython using the apt package manager, using the -command with sudo privilege: +Ubuntu/Debian family Linux users can install ``bpython`` using the ``apt`` +package manager, using the command with ``sudo`` privileges: .. code-block:: bash @@ -80,7 +80,7 @@ In case you are using an older version, run Arch Linux ~~~~~~~~~~ -Arch linux uses pacman as the default package manager, and you can use it to install bpython: +Arch Linux uses ``pacman`` as the default package manager; you can use it to install ``bpython``: .. code-block:: bash @@ -88,7 +88,7 @@ Arch linux uses pacman as the default package manager, and you can use it to ins Fedora ~~~~~~~~~~ -Fedora users can install bpython directly from the command line using ``dnf``. +Fedora users can install ``bpython`` directly from the command line using ``dnf``. .. code-block:: bash @@ -99,8 +99,8 @@ Windows **Caveats:** As ``bpython`` makes use of the ncurses library of \*nix-family operating systems, bpython on Windows is not officially supported and tested. -However, you can still use bpython on Windows using a somewhat work around. Briefly, you should install -these two packages using pip: +However, you may still use bpython on Windows using a workaround. In brief, you should install +these two packages using ``pip``: .. code-block:: bash @@ -130,9 +130,8 @@ Features & Examples type, and colours appropriately. * Expected parameter list. As in a lot of modern IDEs, bpython will attempt to - display a list of parameters for any function you call. The inspect module is - tried first, which works with any Python function, and then pydoc if that - fails. + display a list of parameters for any function you call. The inspect module (which + works with any Python function) is tried first, and then pydoc if that fails. * Rewind. This isn't called "Undo" because it would be misleading, but "Rewind" is probably as bad. The idea is that the code entered is kept in memory and From 98617816e59f2c1a0ad874c1a270e177dafde364 Mon Sep 17 00:00:00 2001 From: supremestdoggo <83146042+supremestdoggo@users.noreply.github.com> Date: Fri, 6 Aug 2021 21:42:02 -0400 Subject: [PATCH 241/555] Add type annotations to a few files --- bpython/_internal.py | 6 ++-- bpython/args.py | 7 ++-- bpython/autocomplete.py | 71 +++++++++++++++++++++-------------------- bpython/cli.py | 21 ++++++------ bpython/curtsies.py | 2 +- 5 files changed, 55 insertions(+), 52 deletions(-) diff --git a/bpython/_internal.py b/bpython/_internal.py index 4545862c4..35cd0bf80 100644 --- a/bpython/_internal.py +++ b/bpython/_internal.py @@ -8,20 +8,20 @@ class _Helper: - def __init__(self): + def __init__(self) -> None: if hasattr(pydoc.Helper, "output"): # See issue #228 self.helper = pydoc.Helper(sys.stdin, None) else: self.helper = pydoc.Helper(sys.stdin, sys.stdout) - def __repr__(self): + def __repr__(self) -> str: return ( "Type help() for interactive help, " "or help(object) for help about object." ) - def __call__(self, *args, **kwargs): + def __call__(self, *args, **kwargs) -> None: self.helper(*args, **kwargs) diff --git a/bpython/args.py b/bpython/args.py index e3095e17c..7895d84ad 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -26,6 +26,7 @@ """ import argparse +from typing import Tuple import curtsies import cwcwidth import greenlet @@ -53,7 +54,7 @@ def error(self, msg): raise ArgumentParserFailed() -def version_banner(base="bpython"): +def version_banner(base="bpython") -> str: return _("{} version {} on top of Python {} {}").format( base, __version__, @@ -62,11 +63,11 @@ def version_banner(base="bpython"): ) -def copyright_banner(): +def copyright_banner() -> str: return _("{} See AUTHORS.rst for details.").format(__copyright__) -def parse(args, extras=None, ignore_stdin=False): +def parse(args, extras=None, ignore_stdin=False) -> Tuple: """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) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 975a2622c..0697e4906 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -33,6 +33,7 @@ import builtins from enum import Enum +from typing import Any, Dict, Iterator, List, Match, NoReturn, Set, Union from . import inspection from . import line as lineparts from .line import LinePart @@ -48,7 +49,7 @@ class AutocompleteModes(Enum): FUZZY = "fuzzy" @classmethod - def from_string(cls, value): + def from_string(cls, value) -> Union[Any, None]: if value.upper() in cls.__members__: return cls.__members__[value.upper()] return None @@ -161,11 +162,11 @@ def from_string(cls, value): KEYWORDS = frozenset(keyword.kwlist) -def after_last_dot(name): +def after_last_dot(name: str) -> str: return name.rstrip(".").rsplit(".")[-1] -def few_enough_underscores(current, match): +def few_enough_underscores(current, match) -> bool: """Returns whether match should be shown based on current if current is _, True if match starts with 0 or 1 underscore @@ -179,19 +180,19 @@ def few_enough_underscores(current, match): return not match.startswith("_") -def method_match_none(word, size, text): +def method_match_none(word, size, text) -> False: return False -def method_match_simple(word, size, text): +def method_match_simple(word, size, text) -> bool: return word[:size] == text -def method_match_substring(word, size, text): +def method_match_substring(word, size, text) -> bool: return text in word -def method_match_fuzzy(word, size, text): +def method_match_fuzzy(word, size, text) -> Union[Match, None]: s = r".*%s.*" % ".*".join(list(text)) return re.search(s, word) @@ -207,11 +208,11 @@ def method_match_fuzzy(word, size, text): class BaseCompletionType: """Describes different completion types""" - def __init__(self, shown_before_tab=True, mode=AutocompleteModes.SIMPLE): + def __init__(self, shown_before_tab: bool=True, mode=AutocompleteModes.SIMPLE) -> None: self._shown_before_tab = shown_before_tab self.method_match = MODES_MAP[mode] - def matches(self, cursor_offset, line, **kwargs): + def matches(self, cursor_offset, line, **kwargs) -> NoReturn: """Returns a list of possible matches given a line and cursor, or None if this completion type isn't applicable. @@ -229,7 +230,7 @@ def matches(self, cursor_offset, line, **kwargs): """ raise NotImplementedError - def locate(self, cursor_offset, line): + def locate(self, cursor_offset, line) -> NoReturn: """Returns a Linepart namedtuple instance or None given cursor and line A Linepart namedtuple contains a start, stop, and word. None is @@ -240,7 +241,7 @@ def locate(self, cursor_offset, line): def format(self, word): return word - def substitute(self, cursor_offset, line, match): + def substitute(self, cursor_offset, line, match) -> NoReturn: """Returns a cursor offset and line with match swapped in""" lpart = self.locate(cursor_offset, line) offset = lpart.start + len(match) @@ -248,7 +249,7 @@ def substitute(self, cursor_offset, line, match): return offset, changed_line @property - def shown_before_tab(self): + def shown_before_tab(self) -> bool: """Whether suggestions should be shown before the user hits tab, or only once that has happened.""" return self._shown_before_tab @@ -257,7 +258,7 @@ def shown_before_tab(self): class CumulativeCompleter(BaseCompletionType): """Returns combined matches from several completers""" - def __init__(self, completers, mode=AutocompleteModes.SIMPLE): + def __init__(self, completers, mode=AutocompleteModes.SIMPLE) -> None: if not completers: raise ValueError( "CumulativeCompleter requires at least one completer" @@ -266,7 +267,7 @@ def __init__(self, completers, mode=AutocompleteModes.SIMPLE): super().__init__(True, mode) - def locate(self, current_offset, line): + def locate(self, current_offset, line) -> Union[None, NoReturn]: for completer in self._completers: return_value = completer.locate(current_offset, line) if return_value is not None: @@ -275,7 +276,7 @@ def locate(self, current_offset, line): def format(self, word): return self._completers[0].format(word) - def matches(self, cursor_offset, line, **kwargs): + def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: return_value = None all_matches = set() for completer in self._completers: @@ -308,10 +309,10 @@ class FilenameCompletion(BaseCompletionType): def __init__(self, mode=AutocompleteModes.SIMPLE): super().__init__(False, mode) - def safe_glob(self, pathname): + def safe_glob(self, pathname) -> Iterator: return glob.iglob(glob.escape(pathname) + "*") - def matches(self, cursor_offset, line, **kwargs): + def matches(self, cursor_offset, line, **kwargs) -> Union[None, set]: cs = lineparts.current_string(cursor_offset, line) if cs is None: return None @@ -341,7 +342,7 @@ class AttrCompletion(BaseCompletionType): attr_matches_re = LazyReCompile(r"(\w+(\.\w+)*)\.(\w*)") - def matches(self, cursor_offset, line, **kwargs): + def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -377,7 +378,7 @@ def locate(self, current_offset, line): def format(self, word): return after_last_dot(word) - def attr_matches(self, text, namespace): + def attr_matches(self, text, namespace) -> List: """Taken from rlcompleter.py and bent to my will.""" m = self.attr_matches_re.match(text) @@ -396,7 +397,7 @@ def attr_matches(self, text, namespace): matches = self.attr_lookup(obj, expr, attr) return matches - def attr_lookup(self, obj, expr, attr): + def attr_lookup(self, obj, expr, attr) -> List: """Second half of attr_matches.""" words = self.list_attributes(obj) if inspection.hasattr_safe(obj, "__class__"): @@ -416,7 +417,7 @@ def attr_lookup(self, obj, expr, attr): matches.append(f"{expr}.{word}") return matches - def list_attributes(self, obj): + def list_attributes(self, obj) -> List[str]: # TODO: re-implement dir using getattr_static to avoid using # AttrCleaner here? with inspection.AttrCleaner(obj): @@ -424,7 +425,7 @@ def list_attributes(self, obj): class DictKeyCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs): + def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -445,7 +446,7 @@ def matches(self, cursor_offset, line, **kwargs): else: return None - def locate(self, current_offset, line): + def locate(self, current_offset, line) -> Union[LinePart, None]: return lineparts.current_dict_key(current_offset, line) def format(self, match): @@ -453,7 +454,7 @@ def format(self, match): class MagicMethodCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs): + def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: if "current_block" not in kwargs: return None current_block = kwargs["current_block"] @@ -465,12 +466,12 @@ def matches(self, cursor_offset, line, **kwargs): return None return {name for name in MAGIC_METHODS if name.startswith(r.word)} - def locate(self, current_offset, line): + def locate(self, current_offset, line) -> Union[LinePart, None]: return lineparts.current_method_definition_name(current_offset, line) class GlobalCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs): + def matches(self, cursor_offset, line, **kwargs) -> Union[Set, 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. @@ -500,12 +501,12 @@ def matches(self, cursor_offset, line, **kwargs): matches.add(_callable_postfix(val, word)) return matches if matches else None - def locate(self, current_offset, line): + def locate(self, current_offset, line) -> Union[LinePart, None]: return lineparts.current_single_word(current_offset, line) class ParameterNameCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs): + def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: if "argspec" not in kwargs: return None argspec = kwargs["argspec"] @@ -526,16 +527,16 @@ def matches(self, cursor_offset, line, **kwargs): ) return matches if matches else None - def locate(self, current_offset, line): + def locate(self, current_offset, line) -> Union[LinePart, None]: return lineparts.current_word(current_offset, line) class ExpressionAttributeCompletion(AttrCompletion): # could replace attr completion as a more general case with some work - def locate(self, current_offset, line): + def locate(self, current_offset, line) -> Union[LinePart, None]: return lineparts.current_expression_attribute(current_offset, line) - def matches(self, cursor_offset, line, **kwargs): + def matches(self, cursor_offset, line, **kwargs) -> Union[Set, Dict, None]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -560,14 +561,14 @@ def matches(self, cursor_offset, line, **kwargs): except ImportError: class MultilineJediCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs): + def matches(self, cursor_offset, line, **kwargs) -> None: return None else: class JediCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs): + def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: if "history" not in kwargs: return None history = kwargs["history"] @@ -607,13 +608,13 @@ def matches(self, cursor_offset, line, **kwargs): # case-sensitive matches only return {m for m in matches if m.startswith(first_letter)} - def locate(self, cursor_offset, line): + def locate(self, cursor_offset, line) -> LinePart: start = self._orig_start end = cursor_offset return LinePart(start, end, line[start:end]) class MultilineJediCompletion(JediCompletion): - def matches(self, cursor_offset, line, **kwargs): + def matches(self, cursor_offset, line, **kwargs) -> Union[Dict, None]: if "current_block" not in kwargs or "history" not in kwargs: return None current_block = kwargs["current_block"] diff --git a/bpython/cli.py b/bpython/cli.py index 8add9f743..baa2059da 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -50,6 +50,7 @@ import struct import sys import time +from typing import Iterator, NoReturn import unicodedata from dataclasses import dataclass @@ -97,7 +98,7 @@ class ShowListState: wl: int = 0 -def calculate_screen_lines(tokens, width, cursor=0): +def calculate_screen_lines(tokens, width, cursor=0) -> int: """Given a stream of tokens and a screen width plus an optional initial cursor position, return the amount of needed lines on the screen.""" @@ -130,31 +131,31 @@ class FakeStream: provided.""" def __init__(self, interface, get_dest): - self.encoding = getpreferredencoding() + self.encoding: str = getpreferredencoding() self.interface = interface self.get_dest = get_dest @forward_if_not_current - def write(self, s): + def write(self, s) -> None: self.interface.write(s) @forward_if_not_current - def writelines(self, l): + def writelines(self, l) -> None: for s in l: self.write(s) - def isatty(self): + def isatty(self) -> True: # some third party (amongst them mercurial) depend on this return True - def flush(self): + def flush(self) -> None: self.interface.flush() class FakeStdin: """Provide a fake stdin type for things like raw_input() etc.""" - def __init__(self, interface): + def __init__(self, interface) -> None: """Take the curses Repl on init and assume it provides a get_key method which, fortunately, it does.""" @@ -162,19 +163,19 @@ def __init__(self, interface): self.interface = interface self.buffer = list() - def __iter__(self): + def __iter__(self) -> Iterator: return iter(self.readlines()) def flush(self): """Flush the internal buffer. This is a no-op. Flushing stdin doesn't make any sense anyway.""" - def write(self, value): + def write(self, value) -> NoReturn: # XXX IPython expects sys.stdin.write to exist, there will no doubt be # others, so here's a hack to keep them happy raise OSError(errno.EBADF, "sys.stdin is read-only") - def isatty(self): + def isatty(self) -> True: return True def readline(self, size=-1): diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 2bd348335..139bbae36 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -20,7 +20,7 @@ class FullCurtsiesRepl(BaseRepl): - def __init__(self, config, locals_, banner, interp=None): + def __init__(self, config, locals_, banner, interp=None) -> None: self.input_generator = curtsies.input.Input( keynames="curtsies", sigint_event=True, paste_threshold=None ) From f7d8b775d7f82db0196dad0a96f1b0c9b4c544ee Mon Sep 17 00:00:00 2001 From: supremestdoggo <83146042+supremestdoggo@users.noreply.github.com> Date: Thu, 12 Aug 2021 19:45:18 -0400 Subject: [PATCH 242/555] Reformat via black --- bpython/autocomplete.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 0697e4906..d8f04f64a 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -208,7 +208,9 @@ def method_match_fuzzy(word, size, text) -> Union[Match, None]: class BaseCompletionType: """Describes different completion types""" - def __init__(self, shown_before_tab: bool=True, mode=AutocompleteModes.SIMPLE) -> None: + def __init__( + self, shown_before_tab: bool = True, mode=AutocompleteModes.SIMPLE + ) -> None: self._shown_before_tab = shown_before_tab self.method_match = MODES_MAP[mode] From 87a85dff224b50f28ac7558f728afe9902f67457 Mon Sep 17 00:00:00 2001 From: Ulises Ojeda Ogando Date: Sat, 25 Sep 2021 10:06:02 +0200 Subject: [PATCH 243/555] checking lines array length --- bpython/curtsiesfrontend/repl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index f38304e9a..30fbbb14e 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1004,9 +1004,9 @@ def send_session_to_external_editor(self, filename=None): ) return lines = text.split("\n") - if not lines[-1].strip(): + if len(lines) and not lines[-1].strip(): lines.pop() # strip last line if empty - if lines[-1].startswith("### "): + if len(lines) and lines[-1].startswith("### "): current_line = lines[-1][4:] else: current_line = "" From b66a29fde838f98a39c08ce1b94f6cc8b8c3e86d Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sat, 25 Sep 2021 10:08:36 -0700 Subject: [PATCH 244/555] Use kwarg for request_undo_callback to fix undo. --- bpython/curtsies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 139bbae36..10fab77f4 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -74,7 +74,7 @@ def interrupting_refresh(self): return self._interrupting_refresh_callback() def request_undo(self, n=1): - return self._request_undo_callback(n) + return self._request_undo_callback(n=n) def get_term_hw(self): return self.window.get_term_hw() From e69cfe3eb164ad0ddcc87c6998442b42596fba49 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 1 Oct 2021 22:21:26 +0200 Subject: [PATCH 245/555] Behave like python3 when executing files with -i (fixes #919) --- bpython/args.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 7895d84ad..79ddcc679 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -216,11 +216,17 @@ def callback(group): def exec_code(interpreter, args): """ - Helper to execute code in a given interpreter. args should be a [faked] - sys.argv + Helper to execute code in a given interpreter, e.g. to implement the behavior of python3 [-i] file.py + + args should be a [faked] sys.argv. """ - with open(args[0]) as sourcefile: - source = sourcefile.read() + try: + with open(args[0]) as sourcefile: + source = sourcefile.read() + except OSError as e: + # print an error and exit (if -i is specified the calling code will continue) + print(f"bpython: can't open file '{args[0]}: {e}", file=sys.stderr) + raise SystemExit(e.errno) old_argv, sys.argv = sys.argv, args sys.path.insert(0, os.path.abspath(os.path.dirname(args[0]))) spec = importlib.util.spec_from_loader("__console__", loader=None) From 18f6901b8bb6b530112381003b70f10792be616e Mon Sep 17 00:00:00 2001 From: arian-deimling <71411328+arian-deimling@users.noreply.github.com> Date: Wed, 6 Oct 2021 12:17:20 -0400 Subject: [PATCH 246/555] Changed dict key-matching regex to capture any valid dict key (#920) * Changed dict key-matching regex to capture any valid dict key * dict key matching regex no longer matches beyond the end of a key * Updated regex to handle str, bytes, int, float, tuple dict keys * Added comments to regex using re.VERBOSE flag * added test case for string dict key being typed inside [] --- bpython/line.py | 49 ++++++++++++++++++++++++++-- bpython/test/test_line_properties.py | 14 ++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index 7ced3bf1d..b98302dd2 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -4,6 +4,8 @@ Python code, and return None, or a tuple of the start index, end index, and the word.""" +import re + from itertools import chain from typing import Optional, NamedTuple @@ -34,7 +36,41 @@ def current_word(cursor_offset: int, line: str) -> Optional[LinePart]: return LinePart(start, end, word) -_current_dict_key_re = LazyReCompile(r"""[\w_][\w0-9._]*\[([\w0-9._(), '"]*)""") +# pieces of regex to match repr() of several hashable built-in types +_match_all_dict_keys = r"""[^\]]*""" + +# https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals +_match_single_quote_str_bytes = r""" + # bytes repr() begins with `b` character; bytes and str begin with `'` + b?' + # match escape sequence; this handles `\'` in the string repr() + (?:\\['"nabfrtvxuU\\]| + # or match any non-`\` and non-single-quote character (most of the string) + [^'\\])* + # matches hanging `\` or ending `'` if one is present + [\\']? +""" + +# bytes and str repr() only uses double quotes if the string contains 1 or more +# `'` character and exactly 0 `"` characters +_match_double_quote_str_bytes = r""" + # bytes repr() begins with `b` character + b?" + # string continues until a `"` character is reached + [^"]* + # end matching at closing double-quote if one is present + "?""" + +# match valid identifier name followed by `[` character +_match_dict_before_key = r"""[\w_][\w0-9._]*\[""" + +_current_dict_key_re = LazyReCompile( + f"{_match_dict_before_key}((?:" + f"{_match_single_quote_str_bytes}|" + f"{_match_double_quote_str_bytes}|" + f"{_match_all_dict_keys}|)*)", + re.VERBOSE, +) def current_dict_key(cursor_offset: int, line: str) -> Optional[LinePart]: @@ -45,7 +81,16 @@ def current_dict_key(cursor_offset: int, line: str) -> Optional[LinePart]: return None -_current_dict_re = LazyReCompile(r"""([\w_][\w0-9._]*)\[([\w0-9._(), '"]*)""") +# capture valid identifier name if followed by `[` character +_capture_dict_name = r"""([\w_][\w0-9._]*)\[""" + +_current_dict_re = LazyReCompile( + f"{_capture_dict_name}((?:" + f"{_match_single_quote_str_bytes}|" + f"{_match_double_quote_str_bytes}|" + f"{_match_all_dict_keys}|)*)", + re.VERBOSE, +) def current_dict(cursor_offset: int, line: str) -> Optional[LinePart]: diff --git a/bpython/test/test_line_properties.py b/bpython/test/test_line_properties.py index fe1b0813e..592a61765 100644 --- a/bpython/test/test_line_properties.py +++ b/bpython/test/test_line_properties.py @@ -178,10 +178,24 @@ def test_simple(self): self.assertAccess("asdf[<(>|]") self.assertAccess("asdf[<(1>|]") self.assertAccess("asdf[<(1,>|]") + self.assertAccess("asdf[<(1,)>|]") self.assertAccess("asdf[<(1, >|]") self.assertAccess("asdf[<(1, 2)>|]") # TODO self.assertAccess('d[d[<12|>') self.assertAccess("d[<'a>|") + self.assertAccess("object.dict['a'bcd'], object.dict[<'abc>|") + self.assertAccess("object.dict[<'a'bcd'>|], object.dict['abc") + self.assertAccess(r"object.dict[<'a\'\\\"\n\\'>|") + self.assertAccess("object.dict[<\"abc'>|") + self.assertAccess("object.dict[<(1, 'apple', 2.134>|]") + self.assertAccess("object.dict[<(1, 'apple', 2.134)>|]") + self.assertAccess("object.dict[<-1000>|") + self.assertAccess("object.dict[<-0.23948>|") + self.assertAccess("object.dict[<'\U0001ffff>|") + self.assertAccess(r"object.dict[<'a\'\\\"\n\\'>|]") + self.assertAccess(r"object.dict[<'a\'\\\"\n\\|[[]'>") + self.assertAccess('object.dict[<"a]bc[|]">]') + self.assertAccess("object.dict[<'abcd[]>|") class TestCurrentDict(LineTestCase): From 5ab308763f72371f80cbe33f93bbaaa70620c17d Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 6 Oct 2021 18:45:08 -0700 Subject: [PATCH 247/555] disable some tests for pypy --- bpython/test/test_inspection.py | 4 ++++ bpython/test/test_interpreter.py | 2 +- bpython/test/test_repl.py | 7 +++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 7c04c521e..fecb848b3 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -1,4 +1,5 @@ import os +import sys import unittest from bpython import inspection @@ -6,6 +7,8 @@ from bpython.test.fodder import encoding_latin1 from bpython.test.fodder import encoding_utf8 +pypy = "PyPy" in sys.version + try: import numpy except ImportError: @@ -118,6 +121,7 @@ def test_get_source_file(self): ) self.assertEqual(encoding, "utf-8") + @unittest.skipIf(pypy, "pypy builtin signatures aren't complete") def test_getfuncprops_print(self): props = inspection.getfuncprops("print", print) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 9b93672c0..36e82c0eb 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -53,7 +53,7 @@ def test_syntaxerror(self): + green('""') + ", line " + bold(magenta("1")) - + "\n 1.1.1.1\n ^\n" + + "\n 1.1.1.1\n ^\n" + bold(red("SyntaxError")) + ": " + cyan("invalid syntax") diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 12275e227..65a2fb813 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -157,7 +157,8 @@ def set_input_line(self, line): def test_func_name(self): for (line, expected_name) in [ ("spam(", "spam"), - ("spam(map([]", "map"), + # map pydoc has no signature in pypy + ("spam(any([]", "any") if pypy else ("spam(map([]", "map"), ("spam((), ", "spam"), ]: self.set_input_line(line) @@ -167,7 +168,8 @@ def test_func_name(self): def test_func_name_method_issue_479(self): for (line, expected_name) in [ ("o.spam(", "spam"), - ("o.spam(map([]", "map"), + # map pydoc has no signature in pypy + ("o.spam(any([]", "any") if pypy else ("o.spam(map([]", "map"), ("o.spam((), ", "spam"), ]: self.set_input_line(line) @@ -200,6 +202,7 @@ def test_lambda_position(self): # Argument position self.assertEqual(self.repl.arg_pos, 1) + @unittest.skipIf(pypy, "range pydoc has no signature in pypy") def test_issue127(self): self.set_input_line("x=range(") self.assertTrue(self.repl.get_args()) From 78ead4c83d7c47c35ab075dd08114f51c24a709d Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 6 Oct 2021 19:59:38 -0700 Subject: [PATCH 248/555] Test Python 3.10 (#924) * Test Python 3.10 * Update tests for Python 3.10 --- .github/workflows/build.yaml | 2 +- bpython/test/test_curtsies_painting.py | 8 ++++++++ bpython/test/test_interpreter.py | 14 +++++++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b877487a7..dc1fc9ed2 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -13,7 +13,7 @@ jobs: continue-on-error: ${{ matrix.python-version == 'pypy3' }} strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9, pypy3] + python-version: [3.6, 3.7, 3.8, 3.9, "3.10", pypy3] steps: - uses: actions/checkout@v2 with: diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index adc63f155..9f98bf066 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -139,6 +139,14 @@ def test_completion(self): "└──────────────────────────────┘", "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_interpreter.py b/bpython/test/test_interpreter.py index 36e82c0eb..ca64de77b 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -35,7 +35,19 @@ def test_syntaxerror(self): i.runsource("1.1.1.1") - if sys.version_info[:2] >= (3, 8): + if sys.version_info[:2] >= (3, 10): + 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] <= (3, 9): expected = ( " File " + green('""') From 2308a61669bd90bb18cfefe31afa7cee6518867e Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 6 Oct 2021 22:45:44 -0700 Subject: [PATCH 249/555] Fix #909 Static types would have helped! I'll add some. --- bpython/curtsiesfrontend/repl.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 30fbbb14e..21b606e67 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -367,7 +367,7 @@ def __init__( self._current_line = "" # current line of output - stdout and stdin go here - self.current_stdouterr_line = "" + self.current_stdouterr_line = "" # Union[str, FmtStr] # this is every line that's been displayed (input and output) # as with formatting applied. Logical lines that exceeded the terminal width @@ -1582,8 +1582,18 @@ def move_screen_up(current_line_start_row): current_line_height = current_line_end_row - current_line_start_row if self.stdin.has_focus: + logger.debug( + "stdouterr when self.stdin has focus: %r %r", + type(self.current_stdouterr_line), + self.current_stdouterr_line, + ) + stdouterr_width = ( + self.current_stdouterr_line.width + if isinstance(self.current_stdouterr_line, FmtStr) + else wcswidth(self.current_stdouterr_line) + ) cursor_row, cursor_column = divmod( - wcswidth(self.current_stdouterr_line) + stdouterr_width + wcswidth( self.stdin.current_line, max(0, self.stdin.cursor_offset) ), From d9305d7a88beb8536c798767df5ddecfb36d9780 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Thu, 7 Oct 2021 00:50:14 -0700 Subject: [PATCH 250/555] Mypy types for completion (#928) * Mypy types for completion We're not checking these in CI nor are we providing a config file yet. I used mypy 0.910 in Python 3.6 run directly on bpython/autocomplete.py and still had 3 errors after these changes. --- bpython/autocomplete.py | 47 +++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index d8f04f64a..3ec2a4578 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -33,7 +33,7 @@ import builtins from enum import Enum -from typing import Any, Dict, Iterator, List, Match, NoReturn, Set, Union +from typing import Any, Dict, Iterator, List, Match, NoReturn, Set, Union, Tuple from . import inspection from . import line as lineparts from .line import LinePart @@ -180,7 +180,7 @@ def few_enough_underscores(current, match) -> bool: return not match.startswith("_") -def method_match_none(word, size, text) -> False: +def method_match_none(word, size, text) -> bool: return False @@ -214,7 +214,10 @@ def __init__( self._shown_before_tab = shown_before_tab self.method_match = MODES_MAP[mode] - def matches(self, cursor_offset, line, **kwargs) -> NoReturn: + @abc.abstractmethod + def matches( + self, cursor_offset: int, line: str, **kwargs + ) -> Union[Set[str], None]: """Returns a list of possible matches given a line and cursor, or None if this completion type isn't applicable. @@ -232,7 +235,8 @@ def matches(self, cursor_offset, line, **kwargs) -> NoReturn: """ raise NotImplementedError - def locate(self, cursor_offset, line) -> NoReturn: + @abc.abstractmethod + def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: """Returns a Linepart namedtuple instance or None given cursor and line A Linepart namedtuple contains a start, stop, and word. None is @@ -243,9 +247,10 @@ def locate(self, cursor_offset, line) -> NoReturn: def format(self, word): return word - def substitute(self, cursor_offset, line, match) -> NoReturn: + def substitute(self, cursor_offset, line, match) -> Tuple[int, str]: """Returns a cursor offset and line with match swapped in""" lpart = self.locate(cursor_offset, line) + assert lpart offset = lpart.start + len(match) changed_line = line[: lpart.start] + match + line[lpart.stop :] return offset, changed_line @@ -269,16 +274,19 @@ def __init__(self, completers, mode=AutocompleteModes.SIMPLE) -> None: super().__init__(True, mode) - def locate(self, current_offset, line) -> Union[None, NoReturn]: + def locate(self, current_offset: int, line: str) -> Union[None, NoReturn]: for completer in self._completers: return_value = completer.locate(current_offset, line) if return_value is not None: return return_value + return None def format(self, word): return self._completers[0].format(word) - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: + def matches( + self, cursor_offset: int, line: str, **kwargs + ) -> Union[None, Set]: return_value = None all_matches = set() for completer in self._completers: @@ -344,7 +352,7 @@ class AttrCompletion(BaseCompletionType): attr_matches_re = LazyReCompile(r"(\w+(\.\w+)*)\.(\w*)") - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: + def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -427,7 +435,7 @@ def list_attributes(self, obj) -> List[str]: class DictKeyCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: + def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -435,7 +443,9 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: r = self.locate(cursor_offset, line) if r is None: return None - _, _, dexpr = lineparts.current_dict(cursor_offset, line) + curDictParts = lineparts.current_dict(cursor_offset, line) + assert curDictParts, "current_dict when .locate() truthy" + _, _, dexpr = curDictParts try: obj = safe_eval(dexpr, locals_) except EvaluationError: @@ -456,7 +466,7 @@ def format(self, match): class MagicMethodCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: + def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: if "current_block" not in kwargs: return None current_block = kwargs["current_block"] @@ -508,7 +518,7 @@ def locate(self, current_offset, line) -> Union[LinePart, None]: class ParameterNameCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: + def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: if "argspec" not in kwargs: return None argspec = kwargs["argspec"] @@ -538,7 +548,7 @@ class ExpressionAttributeCompletion(AttrCompletion): def locate(self, current_offset, line) -> Union[LinePart, None]: return lineparts.current_expression_attribute(current_offset, line) - def matches(self, cursor_offset, line, **kwargs) -> Union[Set, Dict, None]: + def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -547,6 +557,7 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[Set, Dict, None]: locals_ = __main__.__dict__ attr = self.locate(cursor_offset, line) + assert attr, "locate was already truthy for the same call" try: obj = evaluate_current_expression(cursor_offset, line, locals_) @@ -570,7 +581,9 @@ def matches(self, cursor_offset, line, **kwargs) -> None: else: class JediCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: + _orig_start: Union[int, None] + + def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: if "history" not in kwargs: return None history = kwargs["history"] @@ -596,6 +609,7 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: else: self._orig_start = None return None + assert isinstance(self._orig_start, int) first_letter = line[self._orig_start : self._orig_start + 1] @@ -610,13 +624,14 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, Dict]: # case-sensitive matches only return {m for m in matches if m.startswith(first_letter)} - def locate(self, cursor_offset, line) -> LinePart: + def locate(self, cursor_offset: int, line: str) -> LinePart: + assert isinstance(self._orig_start, int) start = self._orig_start end = cursor_offset return LinePart(start, end, line[start:end]) class MultilineJediCompletion(JediCompletion): - def matches(self, cursor_offset, line, **kwargs) -> Union[Dict, None]: + def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: if "current_block" not in kwargs or "history" not in kwargs: return None current_block = kwargs["current_block"] From 76d2ae6d3f424b8a36abbbc9f01c4b7b6287f94a Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 8 Oct 2021 15:02:37 -0700 Subject: [PATCH 251/555] Remove call of nonexistent method. --- bpython/curtsiesfrontend/repl.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 21b606e67..58aa6127d 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -116,8 +116,6 @@ def process_event(self, e): self.current_line = "" self.cursor_offset = 0 self.repl.run_code_and_maybe_finish() - elif e in ("",): - self.get_last_word() elif e in ("",): pass elif e in ("",): From 4ffa123951e46259388e8f13b66164630bc33135 Mon Sep 17 00:00:00 2001 From: arian-deimling <71411328+arian-deimling@users.noreply.github.com> Date: Fri, 8 Oct 2021 18:23:57 -0400 Subject: [PATCH 252/555] Fixed typo (#930) --- doc/sphinx/source/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 84fb2da55..76d3b9402 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -60,7 +60,7 @@ Next install your development copy of bpython and its dependencies: .. code-block:: bash - $ sudp apt install python3-greenlet python3-pygments python3-requests + $ sudo apt install python3-greenlet python3-pygments python3-requests $ sudo apt install python3-watchdog python3-urwid $ sudo apt install python3-sphinx python3-pytest From 4d744ca4c43e86c89a47b8ff1d031781eff7ec0c Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sun, 10 Oct 2021 09:32:59 -0700 Subject: [PATCH 253/555] Mypy types for autocomplete.py, args.py, config.py and curtsies.py (#929) --- .github/workflows/lint.yaml | 17 +++ bpython/__init__.py | 2 +- bpython/args.py | 31 +++- bpython/autocomplete.py | 189 ++++++++++++++++-------- bpython/cli.py | 8 +- bpython/config.py | 4 + bpython/curtsies.py | 107 ++++++++++---- bpython/curtsiesfrontend/filewatch.py | 2 +- bpython/curtsiesfrontend/interaction.py | 2 +- bpython/curtsiesfrontend/interpreter.py | 5 +- bpython/curtsiesfrontend/repl.py | 78 +++++----- bpython/inspection.py | 6 +- bpython/lazyre.py | 10 +- bpython/patch_linecache.py | 2 +- bpython/repl.py | 12 +- bpython/simpleeval.py | 7 +- bpython/test/test_crashers.py | 4 +- bpython/test/test_curtsies_repl.py | 5 +- bpython/test/test_inspection.py | 4 +- bpython/translations/__init__.py | 7 +- bpython/urwid.py | 2 + setup.cfg | 12 ++ stubs/blessings.pyi | 47 ++++++ stubs/greenlet.pyi | 9 ++ stubs/msvcrt.pyi | 7 + stubs/pyperclip.pyi | 3 + stubs/rlcompleter.pyi | 3 + stubs/watchdog/__init__.pyi | 0 stubs/watchdog/events.pyi | 1 + stubs/watchdog/observers.pyi | 4 + stubs/xdg.pyi | 4 + 31 files changed, 427 insertions(+), 167 deletions(-) create mode 100644 stubs/blessings.pyi create mode 100644 stubs/greenlet.pyi create mode 100644 stubs/msvcrt.pyi create mode 100644 stubs/pyperclip.pyi create mode 100644 stubs/rlcompleter.pyi create mode 100644 stubs/watchdog/__init__.pyi create mode 100644 stubs/watchdog/events.pyi create mode 100644 stubs/watchdog/observers.pyi create mode 100644 stubs/xdg.pyi diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index adddd5d5a..839681b92 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -26,3 +26,20 @@ jobs: with: skip: '*.po' ignore_words_list: ba,te,deltion + + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + - 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 diff --git a/bpython/__init__.py b/bpython/__init__.py index f9048afa8..adc00c06b 100644 --- a/bpython/__init__.py +++ b/bpython/__init__.py @@ -23,7 +23,7 @@ import os.path try: - from ._version import __version__ as version + from ._version import __version__ as version # type: ignore except ImportError: version = "unknown" diff --git a/bpython/args.py b/bpython/args.py index 79ddcc679..1ab61d260 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -21,12 +21,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# To gradually migrate to mypy we aren't setting these globally yet +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True + """ Module to handle command line argument parsing, for all front-ends. """ import argparse -from typing import Tuple +from typing import Tuple, List, Optional, NoReturn, Callable +import code import curtsies import cwcwidth import greenlet @@ -50,11 +55,11 @@ class ArgumentParserFailed(ValueError): class RaisingArgumentParser(argparse.ArgumentParser): - def error(self, msg): + def error(self, msg: str) -> NoReturn: raise ArgumentParserFailed() -def version_banner(base="bpython") -> str: +def version_banner(base: str = "bpython") -> str: return _("{} version {} on top of Python {} {}").format( base, __version__, @@ -67,7 +72,14 @@ def copyright_banner() -> str: return _("{} See AUTHORS.rst for details.").format(__copyright__) -def parse(args, extras=None, ignore_stdin=False) -> Tuple: +Options = Tuple[str, str, Callable[[argparse._ArgumentGroup], None]] + + +def parse( + args: Optional[List[str]], + extras: Options = None, + ignore_stdin: bool = False, +) -> Tuple: """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) @@ -197,7 +209,7 @@ def callback(group): logger.info(f"curtsies: {curtsies.__version__}") logger.info(f"cwcwidth: {cwcwidth.__version__}") logger.info(f"greenlet: {greenlet.__version__}") - logger.info(f"pygments: {pygments.__version__}") + logger.info(f"pygments: {pygments.__version__}") # type: ignore logger.info(f"requests: {requests.__version__}") logger.info( "environment:\n{}".format( @@ -214,7 +226,9 @@ def callback(group): return Config(options.config), options, options.args -def exec_code(interpreter, args): +def exec_code( + 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 @@ -230,9 +244,10 @@ def exec_code(interpreter, args): old_argv, sys.argv = sys.argv, args sys.path.insert(0, os.path.abspath(os.path.dirname(args[0]))) spec = importlib.util.spec_from_loader("__console__", loader=None) + assert spec mod = importlib.util.module_from_spec(spec) sys.modules["__console__"] = mod - interpreter.locals.update(mod.__dict__) - interpreter.locals["__file__"] = args[0] + interpreter.locals.update(mod.__dict__) # type: ignore # TODO use a more specific type that has a .locals attribute + interpreter.locals["__file__"] = args[0] # type: ignore # TODO use a more specific type that has a .locals attribute interpreter.runsource(source, args[0], "exec") sys.argv = old_argv diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 3ec2a4578..fd91ea4af 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -21,6 +21,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# To gradually migrate to mypy we aren't setting these globally yet +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True import __main__ import abc @@ -33,12 +36,26 @@ import builtins from enum import Enum -from typing import Any, Dict, Iterator, List, Match, NoReturn, Set, Union, Tuple +from typing import ( + Any, + cast, + Dict, + Iterator, + List, + Match, + Optional, + Set, + Union, + Tuple, + Type, + Sequence, +) from . import inspection from . import line as lineparts from .line import LinePart from .lazyre import LazyReCompile from .simpleeval import safe_eval, evaluate_current_expression, EvaluationError +from .importcompletion import ModuleGatherer # Autocomplete modes @@ -49,7 +66,7 @@ class AutocompleteModes(Enum): FUZZY = "fuzzy" @classmethod - def from_string(cls, value) -> Union[Any, None]: + def from_string(cls, value: str) -> Optional[Any]: if value.upper() in cls.__members__: return cls.__members__[value.upper()] return None @@ -166,7 +183,7 @@ def after_last_dot(name: str) -> str: return name.rstrip(".").rsplit(".")[-1] -def few_enough_underscores(current, match) -> bool: +def few_enough_underscores(current: str, match: str) -> bool: """Returns whether match should be shown based on current if current is _, True if match starts with 0 or 1 underscore @@ -180,19 +197,19 @@ def few_enough_underscores(current, match) -> bool: return not match.startswith("_") -def method_match_none(word, size, text) -> bool: +def method_match_none(word: str, size: int, text: str) -> bool: return False -def method_match_simple(word, size, text) -> bool: +def method_match_simple(word: str, size: int, text: str) -> bool: return word[:size] == text -def method_match_substring(word, size, text) -> bool: +def method_match_substring(word: str, size: int, text: str) -> bool: return text in word -def method_match_fuzzy(word, size, text) -> Union[Match, None]: +def method_match_fuzzy(word: str, size: int, text: str) -> Optional[Match]: s = r".*%s.*" % ".*".join(list(text)) return re.search(s, word) @@ -209,15 +226,17 @@ class BaseCompletionType: """Describes different completion types""" def __init__( - self, shown_before_tab: bool = True, mode=AutocompleteModes.SIMPLE + self, + shown_before_tab: bool = True, + mode: AutocompleteModes = AutocompleteModes.SIMPLE, ) -> None: self._shown_before_tab = shown_before_tab self.method_match = MODES_MAP[mode] @abc.abstractmethod def matches( - self, cursor_offset: int, line: str, **kwargs - ) -> Union[Set[str], None]: + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set[str]]: """Returns a list of possible matches given a line and cursor, or None if this completion type isn't applicable. @@ -236,7 +255,7 @@ def matches( raise NotImplementedError @abc.abstractmethod - def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: """Returns a Linepart namedtuple instance or None given cursor and line A Linepart namedtuple contains a start, stop, and word. None is @@ -244,10 +263,12 @@ def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: the cursor.""" raise NotImplementedError - def format(self, word): + def format(self, word: str) -> str: return word - def substitute(self, cursor_offset, line, match) -> Tuple[int, str]: + def substitute( + self, cursor_offset: int, line: str, match: str + ) -> Tuple[int, str]: """Returns a cursor offset and line with match swapped in""" lpart = self.locate(cursor_offset, line) assert lpart @@ -265,28 +286,32 @@ def shown_before_tab(self) -> bool: class CumulativeCompleter(BaseCompletionType): """Returns combined matches from several completers""" - def __init__(self, completers, mode=AutocompleteModes.SIMPLE) -> None: + def __init__( + self, + completers: Sequence[BaseCompletionType], + mode: AutocompleteModes = AutocompleteModes.SIMPLE, + ) -> None: if not completers: raise ValueError( "CumulativeCompleter requires at least one completer" ) - self._completers = completers + self._completers: Sequence[BaseCompletionType] = completers super().__init__(True, mode) - def locate(self, current_offset: int, line: str) -> Union[None, NoReturn]: + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: for completer in self._completers: - return_value = completer.locate(current_offset, line) + return_value = completer.locate(cursor_offset, line) if return_value is not None: return return_value return None - def format(self, word): + def format(self, word: str) -> str: return self._completers[0].format(word) def matches( - self, cursor_offset: int, line: str, **kwargs - ) -> Union[None, Set]: + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: return_value = None all_matches = set() for completer in self._completers: @@ -301,28 +326,36 @@ def matches( class ImportCompletion(BaseCompletionType): - def __init__(self, module_gatherer, mode=AutocompleteModes.SIMPLE): + def __init__( + self, + module_gatherer: ModuleGatherer, + mode: AutocompleteModes = AutocompleteModes.SIMPLE, + ): super().__init__(False, mode) self.module_gatherer = module_gatherer - def matches(self, cursor_offset, line, **kwargs): + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: return self.module_gatherer.complete(cursor_offset, line) - def locate(self, current_offset, line): - return lineparts.current_word(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_word(cursor_offset, line) - def format(self, word): + def format(self, word: str) -> str: return after_last_dot(word) class FilenameCompletion(BaseCompletionType): - def __init__(self, mode=AutocompleteModes.SIMPLE): + def __init__(self, mode: AutocompleteModes = AutocompleteModes.SIMPLE): super().__init__(False, mode) - def safe_glob(self, pathname) -> Iterator: + def safe_glob(self, pathname: str) -> Iterator[str]: return glob.iglob(glob.escape(pathname) + "*") - def matches(self, cursor_offset, line, **kwargs) -> Union[None, set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: cs = lineparts.current_string(cursor_offset, line) if cs is None: return None @@ -337,10 +370,10 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, set]: matches.add(filename) return matches - def locate(self, current_offset, line): - return lineparts.current_string(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_string(cursor_offset, line) - def format(self, filename): + def format(self, filename: str) -> str: filename.rstrip(os.sep).rsplit(os.sep)[-1] if os.sep in filename[:-1]: return filename[filename.rindex(os.sep, 0, -1) + 1 :] @@ -352,10 +385,12 @@ class AttrCompletion(BaseCompletionType): attr_matches_re = LazyReCompile(r"(\w+(\.\w+)*)\.(\w*)") - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "locals_" not in kwargs: return None - locals_ = kwargs["locals_"] + locals_ = cast(Dict[str, Any], kwargs["locals_"]) r = self.locate(cursor_offset, line) if r is None: @@ -382,13 +417,13 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: if few_enough_underscores(r.word.split(".")[-1], m.split(".")[-1]) } - def locate(self, current_offset, line): - return lineparts.current_dotted_attribute(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_dotted_attribute(cursor_offset, line) - def format(self, word): + def format(self, word: str) -> str: return after_last_dot(word) - def attr_matches(self, text, namespace) -> List: + def attr_matches(self, text: str, namespace: Dict[str, Any]) -> List: """Taken from rlcompleter.py and bent to my will.""" m = self.attr_matches_re.match(text) @@ -407,7 +442,7 @@ def attr_matches(self, text, namespace) -> List: matches = self.attr_lookup(obj, expr, attr) return matches - def attr_lookup(self, obj, expr, attr) -> List: + def attr_lookup(self, obj: Any, expr: str, attr: str) -> List: """Second half of attr_matches.""" words = self.list_attributes(obj) if inspection.hasattr_safe(obj, "__class__"): @@ -427,7 +462,7 @@ def attr_lookup(self, obj, expr, attr) -> List: matches.append(f"{expr}.{word}") return matches - def list_attributes(self, obj) -> List[str]: + def list_attributes(self, obj: Any) -> List[str]: # TODO: re-implement dir using getattr_static to avoid using # AttrCleaner here? with inspection.AttrCleaner(obj): @@ -435,7 +470,9 @@ def list_attributes(self, obj) -> List[str]: class DictKeyCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -458,15 +495,17 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: else: return None - def locate(self, current_offset, line) -> Union[LinePart, None]: - return lineparts.current_dict_key(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_dict_key(cursor_offset, line) - def format(self, match): + def format(self, match: str) -> str: return match[:-1] class MagicMethodCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "current_block" not in kwargs: return None current_block = kwargs["current_block"] @@ -478,12 +517,14 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: return None return {name for name in MAGIC_METHODS if name.startswith(r.word)} - def locate(self, current_offset, line) -> Union[LinePart, None]: - return lineparts.current_method_definition_name(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_method_definition_name(cursor_offset, line) class GlobalCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: """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. @@ -513,12 +554,14 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: matches.add(_callable_postfix(val, word)) return matches if matches else None - def locate(self, current_offset, line) -> Union[LinePart, None]: - return lineparts.current_single_word(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_single_word(cursor_offset, line) class ParameterNameCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "argspec" not in kwargs: return None argspec = kwargs["argspec"] @@ -539,16 +582,18 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: ) return matches if matches else None - def locate(self, current_offset, line) -> Union[LinePart, None]: - return lineparts.current_word(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_word(cursor_offset, line) class ExpressionAttributeCompletion(AttrCompletion): # could replace attr completion as a more general case with some work - def locate(self, current_offset, line) -> Union[LinePart, None]: - return lineparts.current_expression_attribute(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_expression_attribute(cursor_offset, line) - def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -573,17 +618,24 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: import jedi except ImportError: - class MultilineJediCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> None: + class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: + return None + + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return None else: class JediCompletion(BaseCompletionType): - _orig_start: Union[int, None] + _orig_start: Optional[int] - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "history" not in kwargs: return None history = kwargs["history"] @@ -630,8 +682,10 @@ def locate(self, cursor_offset: int, line: str) -> LinePart: end = cursor_offset return LinePart(start, end, line[start:end]) - class MultilineJediCompletion(JediCompletion): - def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: + class MultilineJediCompletion(JediCompletion): # type: ignore [no-redef] + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "current_block" not in kwargs or "history" not in kwargs: return None current_block = kwargs["current_block"] @@ -648,7 +702,12 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: return None -def get_completer(completers, cursor_offset, line, **kwargs): +def get_completer( + completers: Sequence[BaseCompletionType], + cursor_offset: int, + line: str, + **kwargs: Any, +) -> 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 @@ -683,7 +742,9 @@ def get_completer(completers, cursor_offset, line, **kwargs): return [], None -def get_default_completer(mode=AutocompleteModes.SIMPLE, module_gatherer=None): +def get_default_completer( + mode: AutocompleteModes, module_gatherer: ModuleGatherer +) -> Tuple[BaseCompletionType, ...]: return ( ( DictKeyCompletion(mode=mode), @@ -706,7 +767,7 @@ def get_default_completer(mode=AutocompleteModes.SIMPLE, module_gatherer=None): ) -def _callable_postfix(value, word): +def _callable_postfix(value: Any, word: str) -> str: """rlcompleter's _callable_postfix done right.""" if callable(value): word += "(" diff --git a/bpython/cli.py b/bpython/cli.py index baa2059da..28cc67c71 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -50,7 +50,7 @@ import struct import sys import time -from typing import Iterator, NoReturn +from typing import Iterator, NoReturn, List import unicodedata from dataclasses import dataclass @@ -144,7 +144,7 @@ def writelines(self, l) -> None: for s in l: self.write(s) - def isatty(self) -> True: + def isatty(self) -> bool: # some third party (amongst them mercurial) depend on this return True @@ -161,7 +161,7 @@ def __init__(self, interface) -> None: self.encoding = getpreferredencoding() self.interface = interface - self.buffer = list() + self.buffer: List[str] = list() def __iter__(self) -> Iterator: return iter(self.readlines()) @@ -175,7 +175,7 @@ def write(self, value) -> NoReturn: # others, so here's a hack to keep them happy raise OSError(errno.EBADF, "sys.stdin is read-only") - def isatty(self) -> True: + def isatty(self) -> bool: return True def readline(self, size=-1): diff --git a/bpython/config.py b/bpython/config.py index 48686610e..0127b0cbc 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -21,6 +21,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# To gradually migrate to mypy we aren't setting these globally yet +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True + import os import sys import locale diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 10fab77f4..86d33cf3f 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -1,3 +1,9 @@ +# To gradually migrate to mypy we aren't setting these globally yet +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True + +import argparse +import code import collections import logging import sys @@ -16,11 +22,43 @@ from .repl import extract_exit_value from .translations import _ +from typing import ( + Any, + Dict, + List, + Callable, + Union, + Sequence, + Tuple, + Optional, + Generator, +) +from typing_extensions import Literal, Protocol + logger = logging.getLogger(__name__) +class SupportsEventGeneration(Protocol): + def send( + self, timeout: Optional[float] + ) -> Union[str, curtsies.events.Event, None]: + ... + + def __iter__(self) -> "SupportsEventGeneration": + ... + + def __next__(self) -> Union[str, curtsies.events.Event, None]: + ... + + class FullCurtsiesRepl(BaseRepl): - def __init__(self, config, locals_, banner, interp=None) -> None: + def __init__( + self, + config: Config, + locals_: Optional[Dict[str, Any]], + banner: Optional[str], + interp: code.InteractiveInterpreter = None, + ) -> None: self.input_generator = curtsies.input.Input( keynames="curtsies", sigint_event=True, paste_threshold=None ) @@ -32,13 +70,13 @@ def __init__(self, config, locals_, banner, interp=None) -> None: extra_bytes_callback=self.input_generator.unget_bytes, ) - self._request_refresh_callback = self.input_generator.event_trigger( - events.RefreshRequestEvent - ) - self._schedule_refresh_callback = ( - self.input_generator.scheduled_event_trigger( - events.ScheduledRefreshRequestEvent - ) + self._request_refresh_callback: Callable[ + [], None + ] = self.input_generator.event_trigger(events.RefreshRequestEvent) + self._schedule_refresh_callback: Callable[ + [float], None + ] = self.input_generator.scheduled_event_trigger( + events.ScheduledRefreshRequestEvent ) self._request_reload_callback = ( self.input_generator.threadsafe_event_trigger(events.ReloadEvent) @@ -61,40 +99,42 @@ def __init__(self, config, locals_, banner, interp=None) -> None: orig_tcattrs=self.input_generator.original_stty, ) - def _request_refresh(self): + def _request_refresh(self) -> None: return self._request_refresh_callback() - def _schedule_refresh(self, when="now"): + def _schedule_refresh(self, when: float) -> None: return self._schedule_refresh_callback(when) - def _request_reload(self, files_modified=("?",)): + def _request_reload(self, files_modified: Sequence[str] = ("?",)) -> None: return self._request_reload_callback(files_modified) - def interrupting_refresh(self): + def interrupting_refresh(self) -> None: return self._interrupting_refresh_callback() - def request_undo(self, n=1): + def request_undo(self, n: int = 1) -> None: return self._request_undo_callback(n=n) - def get_term_hw(self): + def get_term_hw(self) -> Tuple[int, int]: return self.window.get_term_hw() - def get_cursor_vertical_diff(self): + def get_cursor_vertical_diff(self) -> int: return self.window.get_cursor_vertical_diff() - def get_top_usable_line(self): + def get_top_usable_line(self) -> int: return self.window.top_usable_row - def on_suspend(self): + def on_suspend(self) -> None: self.window.__exit__(None, None, None) self.input_generator.__exit__(None, None, None) - def after_suspend(self): + def after_suspend(self) -> None: self.input_generator.__enter__() self.window.__enter__() self.interrupting_refresh() - def process_event_and_paint(self, e): + def process_event_and_paint( + self, e: Union[str, curtsies.events.Event, None] + ) -> None: """If None is passed in, just paint the screen""" try: if e is not None: @@ -112,7 +152,11 @@ def process_event_and_paint(self, e): scrolled = self.window.render_to_terminal(array, cursor_pos) self.scroll_offset += scrolled - def mainloop(self, interactive=True, paste=None): + def mainloop( + self, + interactive: bool = True, + paste: Optional[curtsies.events.PasteEvent] = None, + ) -> None: if interactive: # Add custom help command # TODO: add methods to run the code @@ -137,14 +181,19 @@ def mainloop(self, interactive=True, paste=None): self.process_event_and_paint(e) -def main(args=None, locals_=None, banner=None, welcome_message=None): +def main( + args: List[str] = None, + locals_: Dict[str, Any] = None, + banner: str = None, + welcome_message: str = None, +) -> Any: """ banner is displayed directly after the version information. welcome_message is passed on to Repl and displayed in the statusbar. """ translations.init() - def curtsies_arguments(parser): + def curtsies_arguments(parser: argparse._ArgumentGroup) -> None: parser.add_argument( "--paste", "-p", @@ -163,10 +212,10 @@ def curtsies_arguments(parser): interp = None paste = None + exit_value: Tuple[Any, ...] = () if exec_args: if not options: raise ValueError("don't pass in exec_args without options") - exit_value = () if options.paste: paste = curtsies.events.PasteEvent() encoding = inspection.get_encoding_file(exec_args[0]) @@ -196,16 +245,18 @@ def curtsies_arguments(parser): with repl.window as win: with repl: repl.height, repl.width = win.t.height, win.t.width - exit_value = repl.mainloop(True, paste) + repl.mainloop(True, paste) except (SystemExitFromCodeRunner, SystemExit) as e: exit_value = e.args return extract_exit_value(exit_value) -def _combined_events(event_provider, paste_threshold): +def _combined_events( + event_provider: "SupportsEventGeneration", paste_threshold: int +) -> Generator[Union[str, curtsies.events.Event, None], Optional[float], None]: """Combines consecutive keypress events into paste events.""" timeout = yield "nonsense_event" # so send can be used immediately - queue = collections.deque() + queue: collections.deque = collections.deque() while True: e = event_provider.send(timeout) if isinstance(e, curtsies.events.Event): @@ -230,7 +281,9 @@ def _combined_events(event_provider, paste_threshold): timeout = yield queue.popleft() -def combined_events(event_provider, paste_threshold=3): +def combined_events( + event_provider: SupportsEventGeneration, paste_threshold: int = 3 +) -> SupportsEventGeneration: g = _combined_events(event_provider, paste_threshold) next(g) return g diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 314767a53..e3607180c 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -14,7 +14,7 @@ def ModuleChangedEventHandler(*args): else: - class ModuleChangedEventHandler(FileSystemEventHandler): + class ModuleChangedEventHandler(FileSystemEventHandler): # type: ignore [no-redef] def __init__(self, paths, on_change): self.dirs = defaultdict(set) self.on_change = on_change diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index 51fd28d35..79622d149 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -78,7 +78,7 @@ def _check_for_expired_message(self): ): self._message = "" - def process_event(self, e): + def process_event(self, e) -> None: """Returns True if shutting down""" assert self.in_prompt or self.in_confirm or self.waiting_for_refresh if isinstance(e, RefreshRequestEvent): diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index a48bc429a..7f7c2fcc7 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -1,4 +1,5 @@ import sys +from typing import Any, Dict from pygments.token import Generic, Token, Keyword, Name, Comment, String from pygments.token import Error, Literal, Number, Operator, Punctuation @@ -59,7 +60,7 @@ def format(self, tokensource, outfile): class Interp(ReplInterpreter): - def __init__(self, locals=None, encoding=None): + def __init__(self, locals: Dict[str, Any] = None, encoding=None): """Constructor. We include an argument for the outfile to pass to the formatter for it @@ -75,7 +76,7 @@ def write(err_line): Accepts FmtStrs so interpreters can output them""" sys.stderr.write(str(err_line)) - self.write = write + self.write = write # type: ignore self.outfile = self def writetb(self, lines): diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 58aa6127d..85965f4f6 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1,3 +1,4 @@ +import code import contextlib import errno import itertools @@ -12,6 +13,8 @@ import unicodedata from enum import Enum +from typing import Dict, Any, List, Optional, Tuple, Union, cast + import blessings import cwcwidth import greenlet @@ -32,6 +35,7 @@ from pygments.lexers import Python3Lexer from . import events as bpythonevents, sitefix, replpainter as paint +from ..config import Config from .coderunner import ( CodeRunner, FakeOutput, @@ -98,7 +102,7 @@ def __init__(self, coderunner, repl, configured_edit_keys=None): else: self.rl_char_sequences = edit_keys - def process_event(self, e): + def process_event(self, e: Union[events.Event, str]) -> None: assert self.has_focus logger.debug("fake input processing event %r", e) @@ -279,7 +283,7 @@ def _process_ps(ps, default_ps: str): if not isinstance(ps, str): return ps - return ps if cwcwidth.wcswidth(ps) >= 0 else default_ps + return ps if cwcwidth.wcswidth(ps, None) >= 0 else default_ps class BaseRepl(Repl): @@ -306,11 +310,11 @@ class BaseRepl(Repl): def __init__( self, - config, - locals_=None, - banner=None, - interp=None, - orig_tcattrs=None, + config: Config, + locals_: Dict[str, Any] = None, + banner: str = None, + interp: code.InteractiveInterpreter = None, + orig_tcattrs: List[Any] = None, ): """ locals_ is a mapping of locals to pass into the interpreter @@ -328,7 +332,7 @@ def __init__( if interp is None: interp = Interp(locals=locals_) - interp.write = self.send_to_stdouterr + interp.write = self.send_to_stdouterr # type: ignore if banner is None: if config.help_key: banner = ( @@ -370,7 +374,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 = [] + self.display_lines: List[FmtStr] = [] # this is every line that's been executed; it gets smaller on rewind self.history = [] @@ -380,12 +384,12 @@ def __init__( # Entries are tuples, where # - the first element the line (string, not fmtsr) # - the second element is one of 2 global constants: "input" or "output" - # (use LineTypeTranslator.INPUT or LineTypeTranslator.OUTPUT to avoid typing these strings) - self.all_logical_lines = [] + # (use LineType.INPUT or LineType.OUTPUT to avoid typing these strings) + 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 = [] + self.display_buffer: List[FmtStr] = [] # how many times display has been scrolled down # because there wasn't room to display everything @@ -394,7 +398,7 @@ def __init__( # cursor position relative to start of current_line, 0 is first char self._cursor_offset = 0 - self.orig_tcattrs = orig_tcattrs + self.orig_tcattrs: Optional[List[Any]] = orig_tcattrs self.coderunner = CodeRunner(self.interp, self.request_refresh) @@ -426,7 +430,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 = [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 @@ -444,8 +448,10 @@ def __init__( self.original_modules = set(sys.modules.keys()) - self.width = None - self.height = None + # as long as the first event received is a window resize event, + # this works fine... + self.width: int = cast(int, None) + self.height: int = cast(int, None) self.status_bar.message(banner) @@ -468,7 +474,7 @@ def get_term_hw(self): """Returns the current width and height of the display area.""" return (50, 10) - def _schedule_refresh(self, when="now"): + def _schedule_refresh(self, when: float): """Arrange for the bpython display to be refreshed soon. This method will be called when the Repl wants the display to be @@ -611,7 +617,7 @@ def clean_up_current_line_for_exit(self): self.unhighlight_paren() # Event handling - def process_event(self, e): + def process_event(self, e: Union[events.Event, str]) -> Optional[bool]: """Returns True if shutting down, otherwise returns None. Mostly mutates state of Repl object""" @@ -621,9 +627,10 @@ def process_event(self, e): else: self.last_events.append(e) self.last_events.pop(0) - return self.process_key_event(e) + self.process_key_event(e) + return None - def process_control_event(self, e): + def process_control_event(self, e) -> Optional[bool]: if isinstance(e, bpythonevents.ScheduledRefreshRequestEvent): # This is a scheduled refresh - it's really just a refresh (so nop) @@ -638,7 +645,7 @@ def process_control_event(self, e): self.run_code_and_maybe_finish() elif self.status_bar.has_focus: - return self.status_bar.process_event(e) + self.status_bar.process_event(e) # handles paste events for both stdin and repl elif isinstance(e, events.PasteEvent): @@ -678,12 +685,11 @@ def process_control_event(self, e): self.undo(n=e.n) elif self.stdin.has_focus: - return self.stdin.process_event(e) + self.stdin.process_event(e) elif isinstance(e, events.SigIntEvent): logger.debug("received sigint event") self.keyboard_interrupt() - return elif isinstance(e, bpythonevents.ReloadEvent): if self.watching_files: @@ -695,8 +701,9 @@ def process_control_event(self, e): else: raise ValueError("Don't know how to handle event type: %r" % e) + return None - def process_key_event(self, e): + def process_key_event(self, e: str) -> None: # To find the curtsies name for a keypress, try # python -m curtsies.events if self.status_bar.has_focus: @@ -751,7 +758,7 @@ def process_key_event(self, e): elif e in key_dispatch[self.config.reimport_key]: self.clear_modules_and_reevaluate() elif e in key_dispatch[self.config.toggle_file_watch_key]: - return self.toggle_file_watch() + self.toggle_file_watch() elif e in key_dispatch[self.config.clear_screen_key]: self.request_paint_to_clear_screen = True elif e in key_dispatch[self.config.show_source_key]: @@ -1427,7 +1434,7 @@ def paint( user_quit=False, try_preserve_history_height=30, min_infobox_height=5, - ): + ) -> Tuple[FSArray, Tuple[int, int]]: """Returns an array of min_height or more rows and width columns, plus cursor position @@ -1585,11 +1592,12 @@ def move_screen_up(current_line_start_row): type(self.current_stdouterr_line), self.current_stdouterr_line, ) - stdouterr_width = ( - self.current_stdouterr_line.width - if isinstance(self.current_stdouterr_line, FmtStr) - else wcswidth(self.current_stdouterr_line) - ) + # mypy can't do ternary type guards yet + stdouterr = self.current_stdouterr_line + if isinstance(stdouterr, FmtStr): + stdouterr_width = stdouterr.width + else: + stdouterr_width = len(stdouterr) cursor_row, cursor_column = divmod( stdouterr_width + wcswidth( @@ -1618,8 +1626,8 @@ def move_screen_up(current_line_start_row): ) else: # Common case for determining cursor position cursor_row, cursor_column = divmod( - wcswidth(self.current_cursor_line_without_suggestion.s) - - wcswidth(self.current_line) + wcswidth(self.current_cursor_line_without_suggestion.s, None) + - wcswidth(self.current_line, None) + wcswidth(self.current_line, max(0, self.cursor_offset)) + self.number_of_padding_chars_on_current_cursor_line(), width, @@ -1815,7 +1823,7 @@ def echo(self, msg, redraw=True): @property def cpos(self): - "many WATs were had - it's the pos from the end of the line back" "" + "many WATs were had - it's the pos from the end of the line back" return len(self.current_line) - self.cursor_offset def reprint_line(self, lineno, tokens): @@ -1930,7 +1938,7 @@ def reevaluate(self, new_code=False): self._cursor_offset = 0 self.current_line = "" - def initialize_interp(self): + def initialize_interp(self) -> None: self.coderunner.interp.locals["_repl"] = self self.coderunner.interp.runsource( "from bpython.curtsiesfrontend._internal import _Helper\n" diff --git a/bpython/inspection.py b/bpython/inspection.py index 4c0dbada4..7f95fbd20 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -336,7 +336,7 @@ def get_encoding(obj): return "utf8" -def get_encoding_file(fname): +def get_encoding_file(fname: str) -> str: """Try to obtain encoding information from a Python source file.""" with open(fname, encoding="ascii", errors="ignore") as f: for unused in range(2): @@ -347,7 +347,7 @@ def get_encoding_file(fname): return "utf8" -def getattr_safe(obj, name): +def getattr_safe(obj: Any, name: str): """side effect free getattr (calls getattr_static).""" result = inspect.getattr_static(obj, name) # Slots are a MemberDescriptorType @@ -356,7 +356,7 @@ def getattr_safe(obj, name): return result -def hasattr_safe(obj, name): +def hasattr_safe(obj: Any, name: str) -> bool: try: getattr_safe(obj, name) return True diff --git a/bpython/lazyre.py b/bpython/lazyre.py index fbbdd38d8..8f1e70995 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -21,12 +21,12 @@ # THE SOFTWARE. import re -from typing import Optional, Iterator +from typing import Optional, Iterator, Pattern, Match, Optional try: from functools import cached_property except ImportError: - from backports.cached_property import cached_property # type: ignore + from backports.cached_property import cached_property # type: ignore [no-redef] class LazyReCompile: @@ -40,16 +40,16 @@ def __init__(self, regex: str, flags: int = 0) -> None: self.flags = flags @cached_property - def compiled(self): + def compiled(self) -> Pattern[str]: return re.compile(self.regex, self.flags) def finditer(self, *args, **kwargs): return self.compiled.finditer(*args, **kwargs) - def search(self, *args, **kwargs): + def search(self, *args, **kwargs) -> Optional[Match[str]]: return self.compiled.search(*args, **kwargs) - def match(self, *args, **kwargs): + def match(self, *args, **kwargs) -> Optional[Match[str]]: return self.compiled.match(*args, **kwargs) def sub(self, *args, **kwargs) -> str: diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index daf7251d9..e1d94a157 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -64,7 +64,7 @@ def _bpython_clear_linecache(): # Monkey-patch the linecache module so that we're able # to hold our command history there and have it persist -linecache.cache = BPythonLinecache(linecache.cache) +linecache.cache = BPythonLinecache(linecache.cache) # type: ignore linecache.clearcache = _bpython_clear_linecache diff --git a/bpython/repl.py b/bpython/repl.py index ba9acf4d8..e261e61ad 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -33,11 +33,13 @@ import textwrap import time import traceback +from abc import abstractmethod from itertools import takewhile from pathlib import Path from pygments.lexers import Python3Lexer from pygments.token import Token from types import ModuleType +from typing import cast, Tuple, Any have_pyperclip = True try: @@ -451,11 +453,11 @@ def __init__(self, interp, config): @property def ps1(self) -> str: - return getattr(sys, "ps1", ">>> ") + return cast(str, getattr(sys, "ps1", ">>> ")) @property def ps2(self) -> str: - return getattr(sys, "ps2", "... ") + return cast(str, getattr(sys, "ps2", "... ")) def startup(self): """ @@ -769,6 +771,10 @@ def line_is_empty(line): indentation = 0 return indentation + @abstractmethod + def getstdout(self) -> str: + raise NotImplementedError() + def get_session_formatted_for_file(self) -> str: """Format the stdout buffer to something suitable for writing to disk, i.e. without >>> and ... at input lines and with "# OUT: " prepended to @@ -1214,7 +1220,7 @@ def token_is_any_of(token): return token_is_any_of -def extract_exit_value(args): +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 ef6dd53b9..c5e14cf5d 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -28,6 +28,7 @@ import ast import sys import builtins +from typing import Dict, Any from . import line as line_properties from .inspection import getattr_safe @@ -44,7 +45,7 @@ class EvaluationError(Exception): """Raised if an exception occurred in safe_eval.""" -def safe_eval(expr, namespace): +def safe_eval(expr: str, namespace: Dict[str, Any]) -> Any: """Not all that safe, just catches some errors""" try: return eval(expr, namespace) @@ -214,7 +215,9 @@ def find_attribute_with_name(node, name): return r -def evaluate_current_expression(cursor_offset, line, namespace=None): +def evaluate_current_expression( + cursor_offset: int, line: str, namespace: Dict[str, Any] = None +): """ Return evaluated expression to the right of the dot of current attribute. diff --git a/bpython/test/test_crashers.py b/bpython/test/test_crashers.py index 64abff3e3..051e5b691 100644 --- a/bpython/test/test_crashers.py +++ b/bpython/test/test_crashers.py @@ -17,10 +17,10 @@ from twisted.trial.unittest import TestCase as TrialTestCase except ImportError: - class TrialTestCase: + class TrialTestCase: # type: ignore [no-redef] pass - reactor = None + reactor = None # type: ignore try: import urwid diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py index 2ed33097b..a6e4c7866 100644 --- a/bpython/test/test_curtsies_repl.py +++ b/bpython/test/test_curtsies_repl.py @@ -86,8 +86,7 @@ def test_last_word(self): self.assertEqual(curtsiesrepl._last_word("a"), "a") self.assertEqual(curtsiesrepl._last_word("a b"), "b") - # this is the behavior of bash - not currently implemented - @unittest.skip + @unittest.skip("this is the behavior of bash - not currently implemented") def test_get_last_word_with_prev_line(self): self.repl.rl_history.entries = ["1", "2 3", "4 5 6"] self.repl._set_current_line("abcde") @@ -300,7 +299,7 @@ def test_simple(self): self.assertEqual(self.repl.predicted_indent("def asdf():"), 4) self.assertEqual(self.repl.predicted_indent("def asdf(): return 7"), 0) - @unittest.skip + @unittest.skip("This would be interesting") def test_complex(self): self.assertEqual(self.repl.predicted_indent("[a, "), 1) self.assertEqual(self.repl.predicted_indent("reduce(asdfasdf, "), 7) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index fecb848b3..50be0c3a2 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -12,7 +12,7 @@ try: import numpy except ImportError: - numpy = None + numpy = None # type: ignore foo_ascii_only = '''def foo(): @@ -191,7 +191,7 @@ def __mro__(self): a = 1 -member_descriptor = type(Slots.s1) +member_descriptor = type(Slots.s1) # type: ignore class TestSafeGetAttribute(unittest.TestCase): diff --git a/bpython/translations/__init__.py b/bpython/translations/__init__.py index c2e23f806..13c498025 100644 --- a/bpython/translations/__init__.py +++ b/bpython/translations/__init__.py @@ -2,13 +2,14 @@ import locale import os.path import sys +from typing import cast, List from .. import package_dir -translator = None +translator: gettext.NullTranslations = cast(gettext.NullTranslations, None) -def _(message): +def _(message) -> str: return translator.gettext(message) @@ -16,7 +17,7 @@ def ngettext(singular, plural, n): return translator.ngettext(singular, plural, n) -def init(locale_dir=None, languages=None): +def init(locale_dir: str = None, languages: List[str] = None) -> None: try: locale.setlocale(locale.LC_ALL, "") except locale.Error: diff --git a/bpython/urwid.py b/bpython/urwid.py index 1f63b1370..e3aab75c2 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -20,6 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# This whole file typing TODO +# type: ignore """bpython backend based on Urwid. diff --git a/setup.cfg b/setup.cfg index 89b6cde61..1efed779c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,3 +65,15 @@ msgid_bugs_address = https://github.com/bpython/bpython/issues builder = man source_dir = doc/sphinx/source build_dir = build + +[mypy] +warn_return_any = True +warn_unused_configs = True +mypy_path=stubs +files=bpython + +[mypy-jedi] +ignore_missing_imports = True + +[mypy-urwid] +ignore_missing_imports = True diff --git a/stubs/blessings.pyi b/stubs/blessings.pyi new file mode 100644 index 000000000..66fd96216 --- /dev/null +++ b/stubs/blessings.pyi @@ -0,0 +1,47 @@ +from typing import ContextManager, Text, IO + +class Terminal: + def __init__(self, stream=None, force_styling=False): + # type: (IO, bool) -> None + pass + def location(self, x=None, y=None): + # type: (int, int) -> ContextManager + pass + @property + def hide_cursor(self): + # type: () -> Text + pass + @property + def normal_cursor(self): + # type: () -> Text + pass + @property + def height(self): + # type: () -> int + pass + @property + def width(self): + # type: () -> int + pass + def fullscreen(self): + # type: () -> ContextManager + pass + def move(self, y, x): + # type: (int, int) -> Text + pass + @property + def clear_eol(self): + # type: () -> Text + pass + @property + def clear_bol(self): + # type: () -> Text + pass + @property + def clear_eos(self): + # type: () -> Text + pass + @property + def clear_eos(self): + # type: () -> Text + pass diff --git a/stubs/greenlet.pyi b/stubs/greenlet.pyi new file mode 100644 index 000000000..778c827ef --- /dev/null +++ b/stubs/greenlet.pyi @@ -0,0 +1,9 @@ +from typing import Any, Callable + +__version__: str + +def getcurrent() -> None: ... + +class greenlet: + def __init__(self, func: Callable[[], Any]): ... + def switch(self, value: Any = None) -> Any: ... diff --git a/stubs/msvcrt.pyi b/stubs/msvcrt.pyi new file mode 100644 index 000000000..2e99c9008 --- /dev/null +++ b/stubs/msvcrt.pyi @@ -0,0 +1,7 @@ +# The real types seem only available on the Windows platform, +# but it seems annoying to need to run typechecking once per platform +# https://github.com/python/typeshed/blob/master/stdlib/msvcrt.pyi +def locking(__fd: int, __mode: int, __nbytes: int) -> None: ... + +LK_NBLCK: int +LK_UNLCK: int diff --git a/stubs/pyperclip.pyi b/stubs/pyperclip.pyi new file mode 100644 index 000000000..3968c20a6 --- /dev/null +++ b/stubs/pyperclip.pyi @@ -0,0 +1,3 @@ +def copy(content: str): ... + +class PyperclipException(Exception): ... diff --git a/stubs/rlcompleter.pyi b/stubs/rlcompleter.pyi new file mode 100644 index 000000000..bbc871ada --- /dev/null +++ b/stubs/rlcompleter.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def get_class_members(class_: Any): ... diff --git a/stubs/watchdog/__init__.pyi b/stubs/watchdog/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/stubs/watchdog/events.pyi b/stubs/watchdog/events.pyi new file mode 100644 index 000000000..6e17bd6df --- /dev/null +++ b/stubs/watchdog/events.pyi @@ -0,0 +1 @@ +class FileSystemEventHandler: ... diff --git a/stubs/watchdog/observers.pyi b/stubs/watchdog/observers.pyi new file mode 100644 index 000000000..7db3099fb --- /dev/null +++ b/stubs/watchdog/observers.pyi @@ -0,0 +1,4 @@ +class Observer: + def start(self): ... + def schedule(self, dirname: str, recursive: bool): ... + def unschedule_all(self): ... diff --git a/stubs/xdg.pyi b/stubs/xdg.pyi new file mode 100644 index 000000000..db7d63e03 --- /dev/null +++ b/stubs/xdg.pyi @@ -0,0 +1,4 @@ +from typing import ClassVar + +class BaseDirectory: + xdg_config_home: ClassVar[str] From 459753e97e724eeeba4c950c54b7a9d6977e4163 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 12 Oct 2021 21:31:39 +0200 Subject: [PATCH 254/555] Update changelog --- CHANGELOG.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 28cd66af2..5f956a1ec 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,12 +6,36 @@ Changelog General information: +* The #bpython channel has moved to OFTC. +* Type annotations have been added to the bpython code base. +* Declarative build configuration is used as much as possible. + New features: +* #883: Allow auto-completion to be disabled +* #841: Respect locals when using bpython.embed +* Use pyperclip for better clipboard handling + Fixes: +* #879: Iterate over all completers until a successful one is found +* #882: Handle errors in theme configuration without crashing +* #884: Fix writing of b"" on fake stdout +* #888: Read PYTHONSTARTUP with utf8 as encoding +* #896: Use default sys.ps1 and sys.ps2 if user specified ones are not usable +* #902: Do not crash when encountering unreadable files while processing modules for import completion +* #909: Fix sys.stdin.readlin +* #917: Fix tab completion for dict keys +* #919: Replicate python behavior when running with -i and a non-existing file + Changes to dependencies: +* pyperclip is a new optional dependency for clipboard support +* backports.cached-property is now required for Python < 3.8 +* dataclasses is now required for Python < 3.7 + +Support for Python 3.10 has been added. + 0.21 ---- From 03e2ead4edf09af8e2928732a2427caababb53ad Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 12 Oct 2021 21:56:29 +0200 Subject: [PATCH 255/555] Update German translation --- bpython/translations/de/LC_MESSAGES/bpython.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bpython/translations/de/LC_MESSAGES/bpython.po b/bpython/translations/de/LC_MESSAGES/bpython.po index 95bd6cc84..718acbb7f 100644 --- a/bpython/translations/de/LC_MESSAGES/bpython.po +++ b/bpython/translations/de/LC_MESSAGES/bpython.po @@ -1,7 +1,7 @@ # German translations for bpython. -# Copyright (C) 2012-2013 bpython developers +# Copyright (C) 2012-2021 bpython developers # This file is distributed under the same license as the bpython project. -# Sebastian Ramacher , 2012-2013. +# Sebastian Ramacher , 2012-2021. # msgid "" msgstr "" @@ -64,7 +64,7 @@ msgstr "Datei für Ausgabe von Log-Nachrichten" #: bpython/args.py:146 msgid "File to execute and additional arguments passed on to the executed script." -msgstr "" +msgstr "Auszuführende Datei und zusätzliche Argumente, die an das Script übergeben werden sollen." #: bpython/cli.py:312 bpython/urwid.py:537 msgid "y" @@ -206,20 +206,20 @@ msgstr "Inhalt wurde in Zwischenablage kopiert." #: bpython/repl.py:884 msgid "Pastebin buffer? (y/N) " -msgstr "" +msgstr "Buffer bei Pastebin hochladen? (j/N)" #: bpython/repl.py:886 msgid "Pastebin aborted." -msgstr "" +msgstr "Hochladen zu Pastebin abgebrochen." #: bpython/repl.py:894 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" -msgstr "" +msgstr "Duplizierte Daten zu Pastebin hochgeladen. Vorherige URL: %s. URL zum Löschen: %s" #: bpython/repl.py:900 msgid "Posting data to pastebin..." -msgstr "Lade Daten hoch..." +msgstr "Lade Daten hoch zu Pastebin..." #: bpython/repl.py:904 #, python-format From f591116efa8ff4139bb72b4e9d10cc4c25c6b9a9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 12 Oct 2021 21:59:07 +0200 Subject: [PATCH 256/555] Update translations --- bpython/translations/bpython.pot | 141 +++++------ .../translations/de/LC_MESSAGES/bpython.po | 228 +++++++++--------- .../translations/es_ES/LC_MESSAGES/bpython.po | 139 +++++------ .../translations/fr_FR/LC_MESSAGES/bpython.po | 139 +++++------ .../translations/it_IT/LC_MESSAGES/bpython.po | 139 +++++------ .../translations/nl_NL/LC_MESSAGES/bpython.po | 139 +++++------ 6 files changed, 469 insertions(+), 456 deletions(-) diff --git a/bpython/translations/bpython.pot b/bpython/translations/bpython.pot index 24c9cdd8a..e11140ed2 100644 --- a/bpython/translations/bpython.pot +++ b/bpython/translations/bpython.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: bpython 0.22.dev28\n" +"Project-Id-Version: bpython 0.22.dev123\n" "Report-Msgid-Bugs-To: https://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-02-14 17:29+0100\n" +"POT-Creation-Date: 2021-10-12 21:58+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,15 +17,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:49 +#: bpython/args.py:63 msgid "{} version {} on top of Python {} {}" msgstr "" -#: bpython/args.py:58 +#: bpython/args.py:72 msgid "{} See AUTHORS.rst for details." msgstr "" -#: bpython/args.py:95 +#: bpython/args.py:116 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -33,82 +33,83 @@ msgid "" "to the regular Python interpreter." msgstr "" -#: bpython/args.py:105 +#: bpython/args.py:127 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:111 +#: bpython/args.py:133 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:117 +#: bpython/args.py:139 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:123 +#: bpython/args.py:145 msgid "Print version and exit." msgstr "" -#: bpython/args.py:130 +#: bpython/args.py:152 msgid "Set log level for logging" msgstr "" -#: bpython/args.py:135 +#: bpython/args.py:157 msgid "Log output file" msgstr "" -#: bpython/args.py:146 +#: bpython/args.py:168 msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:312 bpython/urwid.py:537 +#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/urwid.py:539 msgid "y" msgstr "" -#: bpython/cli.py:312 bpython/urwid.py:537 +#: bpython/cli.py:320 bpython/urwid.py:539 msgid "yes" msgstr "" -#: bpython/cli.py:1692 +#: bpython/cli.py:1696 msgid "Rewind" msgstr "" -#: bpython/cli.py:1693 +#: bpython/cli.py:1697 msgid "Save" msgstr "" -#: bpython/cli.py:1694 +#: bpython/cli.py:1698 msgid "Pastebin" msgstr "" -#: bpython/cli.py:1695 +#: bpython/cli.py:1699 msgid "Pager" msgstr "" -#: bpython/cli.py:1696 +#: bpython/cli.py:1700 msgid "Show Source" msgstr "" -#: bpython/cli.py:1943 +#: bpython/cli.py:1947 msgid "" "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsies.py:136 +#: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:142 +#: bpython/curtsies.py:207 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:143 +#: bpython/curtsies.py:208 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" -#: bpython/history.py:224 +#: bpython/history.py:250 #, python-format msgid "Error occurred while writing to file %s (%s)" msgstr "" @@ -134,212 +135,212 @@ msgstr "" msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:653 +#: bpython/repl.py:644 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:658 +#: bpython/repl.py:649 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:663 +#: bpython/repl.py:654 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:665 +#: bpython/repl.py:656 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:820 +#: bpython/repl.py:801 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 +#: bpython/repl.py:803 bpython/repl.py:806 bpython/repl.py:830 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:836 +#: bpython/repl.py:817 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:844 +#: bpython/repl.py:825 msgid "overwrite" msgstr "" -#: bpython/repl.py:846 +#: bpython/repl.py:827 msgid "append" msgstr "" -#: bpython/repl.py:858 bpython/repl.py:1165 +#: bpython/repl.py:839 bpython/repl.py:1143 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:860 +#: bpython/repl.py:841 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:866 +#: bpython/repl.py:847 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:873 +#: bpython/repl.py:854 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:875 +#: bpython/repl.py:856 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:884 +#: bpython/repl.py:865 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:886 +#: bpython/repl.py:867 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:894 +#: bpython/repl.py:875 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:900 +#: bpython/repl.py:881 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:885 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:913 +#: bpython/repl.py:894 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:918 +#: bpython/repl.py:899 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:956 +#: bpython/repl.py:937 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:964 bpython/repl.py:968 +#: bpython/repl.py:945 bpython/repl.py:949 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:971 +#: bpython/repl.py:952 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1147 +#: bpython/repl.py:1128 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1175 +#: bpython/repl.py:1153 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1181 +#: bpython/repl.py:1158 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:604 +#: bpython/urwid.py:606 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" -#: bpython/urwid.py:1114 +#: bpython/urwid.py:1116 msgid "Run twisted reactor." msgstr "" -#: bpython/urwid.py:1119 +#: bpython/urwid.py:1121 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "" -#: bpython/urwid.py:1127 +#: bpython/urwid.py:1129 msgid "List available reactors for -r." msgstr "" -#: bpython/urwid.py:1132 +#: bpython/urwid.py:1134 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." msgstr "" -#: bpython/urwid.py:1141 +#: bpython/urwid.py:1143 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1335 +#: bpython/urwid.py:1337 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:325 +#: bpython/curtsiesfrontend/repl.py:339 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:327 +#: bpython/curtsiesfrontend/repl.py:341 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:664 +#: bpython/curtsiesfrontend/repl.py:681 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:682 +#: bpython/curtsiesfrontend/repl.py:698 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:991 +#: bpython/curtsiesfrontend/repl.py:1008 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1006 +#: bpython/curtsiesfrontend/repl.py:1023 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1016 +#: bpython/curtsiesfrontend/repl.py:1033 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1027 +#: bpython/curtsiesfrontend/repl.py:1044 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1033 +#: bpython/curtsiesfrontend/repl.py:1050 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1038 +#: bpython/curtsiesfrontend/repl.py:1055 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1044 +#: bpython/curtsiesfrontend/repl.py:1061 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1986 +#: bpython/curtsiesfrontend/repl.py:2011 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/de/LC_MESSAGES/bpython.po b/bpython/translations/de/LC_MESSAGES/bpython.po index 718acbb7f..79b3acf71 100644 --- a/bpython/translations/de/LC_MESSAGES/bpython.po +++ b/bpython/translations/de/LC_MESSAGES/bpython.po @@ -7,116 +7,118 @@ msgid "" msgstr "" "Project-Id-Version: bpython mercurial\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-02-14 17:29+0100\n" +"POT-Creation-Date: 2021-10-12 21:58+0200\n" "PO-Revision-Date: 2021-02-14 17:31+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: de\n" "Language-Team: de \n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -"X-Generator: Poedit 2.4.2\n" -#: bpython/args.py:49 +#: bpython/args.py:63 msgid "{} version {} on top of Python {} {}" msgstr "{} Version {} mit Python {} {}" -#: bpython/args.py:58 +#: bpython/args.py:72 msgid "{} See AUTHORS.rst for details." msgstr "{} Siehe AUTHORS.rst für mehr Details." -#: bpython/args.py:95 +#: bpython/args.py:116 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" -"NOTE: If bpython sees an argument it does not know, execution falls back to the " -"regular Python interpreter." +"NOTE: If bpython sees an argument it does not know, execution falls back " +"to the regular Python interpreter." msgstr "" "Verwendung: %(prog)s [Optionen] [Datei [Argumente]]\n" -"Hinweis: Wenn bpython Argumente übergeben bekommt, die nicht verstanden werden, " -"wird der normale Python Interpreter ausgeführt." +"Hinweis: Wenn bpython Argumente übergeben bekommt, die nicht verstanden " +"werden, wird der normale Python Interpreter ausgeführt." -#: bpython/args.py:105 +#: bpython/args.py:127 msgid "Use CONFIG instead of default config file." msgstr "Verwende CONFIG antatt der standardmäßigen Konfigurationsdatei." -#: bpython/args.py:111 +#: bpython/args.py:133 msgid "Drop to bpython shell after running file instead of exiting." msgstr "Verbleibe in bpython nach dem Ausführen der Datei." -#: bpython/args.py:117 +#: bpython/args.py:139 msgid "Don't flush the output to stdout." msgstr "Gib Ausgabe beim Beenden nicht ernaut auf stdout aus." -#: bpython/args.py:123 +#: bpython/args.py:145 msgid "Print version and exit." msgstr "Zeige Versionsinformationen an und beende." -#: bpython/args.py:130 +#: bpython/args.py:152 msgid "Set log level for logging" msgstr "Log-Stufe" -#: bpython/args.py:135 +#: bpython/args.py:157 msgid "Log output file" msgstr "Datei für Ausgabe von Log-Nachrichten" -#: bpython/args.py:146 +#: bpython/args.py:168 msgid "File to execute and additional arguments passed on to the executed script." -msgstr "Auszuführende Datei und zusätzliche Argumente, die an das Script übergeben werden sollen." +msgstr "" +"Auszuführende Datei und zusätzliche Argumente, die an das Script " +"übergeben werden sollen." -#: bpython/cli.py:312 bpython/urwid.py:537 +#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/urwid.py:539 msgid "y" msgstr "j" -#: bpython/cli.py:312 bpython/urwid.py:537 +#: bpython/cli.py:320 bpython/urwid.py:539 msgid "yes" msgstr "ja" -#: bpython/cli.py:1692 +#: bpython/cli.py:1696 msgid "Rewind" msgstr "Rückgängig" -#: bpython/cli.py:1693 +#: bpython/cli.py:1697 msgid "Save" msgstr "Speichern" -#: bpython/cli.py:1694 +#: bpython/cli.py:1698 msgid "Pastebin" msgstr "Pastebin" -#: bpython/cli.py:1695 +#: bpython/cli.py:1699 msgid "Pager" msgstr "" -#: bpython/cli.py:1696 +#: bpython/cli.py:1700 msgid "Show Source" msgstr "Quellcode anzeigen" -#: bpython/cli.py:1943 +#: bpython/cli.py:1947 msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. This " -"backend has been deprecated in version 0.19 and might disappear in a future " -"version." +"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " +"This backend has been deprecated in version 0.19 and might disappear in a" +" future version." msgstr "" -"ACHTUNG: `bpython-cli` wird verwendet, die curses Implementierung von `bpython`. " -"Diese Implementierung wird ab Version 0.19 nicht mehr aktiv unterstützt und wird " -"in einer zukünftigen Version entfernt werden." +"ACHTUNG: `bpython-cli` wird verwendet, die curses Implementierung von " +"`bpython`. Diese Implementierung wird ab Version 0.19 nicht mehr aktiv " +"unterstützt und wird in einer zukünftigen Version entfernt werden." -#: bpython/curtsies.py:136 +#: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:142 +#: bpython/curtsies.py:207 msgid "curtsies arguments" msgstr "Argumente für curtsies" -#: bpython/curtsies.py:143 +#: bpython/curtsies.py:208 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "Zusätzliche Argumente spezifisch für die curtsies-basierte REPL." -#: bpython/history.py:224 +#: bpython/history.py:250 #, python-format msgid "Error occurred while writing to file %s (%s)" msgstr "Fehler beim Schreiben in Datei %s aufgetreten (%s)" @@ -142,249 +144,255 @@ msgstr "Keine Ausgabe von Hilfsprogramm vorhanden." msgid "Failed to recognize the helper program's output as an URL." msgstr "Konnte Ausgabe von Hilfsprogramm nicht verarbeiten." -#: bpython/repl.py:653 +#: bpython/repl.py:644 msgid "Nothing to get source of" msgstr "Nichts um Quellcode abzurufen" -#: bpython/repl.py:658 +#: bpython/repl.py:649 #, python-format msgid "Cannot get source: %s" msgstr "Kann Quellcode nicht finden: %s" -#: bpython/repl.py:663 +#: bpython/repl.py:654 #, python-format msgid "Cannot access source of %r" msgstr "Kann auf Quellcode nicht zugreifen: %r" -#: bpython/repl.py:665 +#: bpython/repl.py:656 #, python-format msgid "No source code found for %s" msgstr "Quellcode für %s nicht gefunden" -#: bpython/repl.py:820 +#: bpython/repl.py:801 msgid "Save to file (Esc to cancel): " msgstr "In Datei speichern (Esc um abzubrechen): " -#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 +#: bpython/repl.py:803 bpython/repl.py:806 bpython/repl.py:830 msgid "Save cancelled." msgstr "Speichern abgebrochen." -#: bpython/repl.py:836 +#: bpython/repl.py:817 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "%s existiert bereit. (C) abbrechen, (o) überschrieben oder (a) anhängen? " -#: bpython/repl.py:844 +#: bpython/repl.py:825 msgid "overwrite" msgstr "überschreiben" -#: bpython/repl.py:846 +#: bpython/repl.py:827 msgid "append" msgstr "anhängen" -#: bpython/repl.py:858 bpython/repl.py:1165 +#: bpython/repl.py:839 bpython/repl.py:1143 #, python-format msgid "Error writing file '%s': %s" msgstr "Fehler beim Schreiben in Datei '%s': %s" -#: bpython/repl.py:860 +#: bpython/repl.py:841 #, python-format msgid "Saved to %s." msgstr "Nach %s gespeichert." -#: bpython/repl.py:866 +#: bpython/repl.py:847 msgid "No clipboard available." msgstr "Zwischenablage ist nicht verfügbar." -#: bpython/repl.py:873 +#: bpython/repl.py:854 msgid "Could not copy to clipboard." msgstr "Konnte nicht in Zwischenablage kopieren." -#: bpython/repl.py:875 +#: bpython/repl.py:856 msgid "Copied content to clipboard." msgstr "Inhalt wurde in Zwischenablage kopiert." -#: bpython/repl.py:884 +#: bpython/repl.py:865 msgid "Pastebin buffer? (y/N) " msgstr "Buffer bei Pastebin hochladen? (j/N)" -#: bpython/repl.py:886 +#: bpython/repl.py:867 msgid "Pastebin aborted." msgstr "Hochladen zu Pastebin abgebrochen." -#: bpython/repl.py:894 +#: bpython/repl.py:875 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" -msgstr "Duplizierte Daten zu Pastebin hochgeladen. Vorherige URL: %s. URL zum Löschen: %s" +msgstr "" +"Duplizierte Daten zu Pastebin hochgeladen. Vorherige URL: %s. URL zum " +"Löschen: %s" -#: bpython/repl.py:900 +#: bpython/repl.py:881 msgid "Posting data to pastebin..." msgstr "Lade Daten hoch zu Pastebin..." -#: bpython/repl.py:904 +#: bpython/repl.py:885 #, python-format msgid "Upload failed: %s" msgstr "Hochladen ist fehlgeschlagen: %s" -#: bpython/repl.py:913 +#: bpython/repl.py:894 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "Pastebin URL: %s - URL zum Löschen: %s" -#: bpython/repl.py:918 +#: bpython/repl.py:899 #, python-format msgid "Pastebin URL: %s" msgstr "Pastebin URL: %s" -#: bpython/repl.py:956 +#: bpython/repl.py:937 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -"Wie viele Zeilen rückgängig machen? (Rückgängigmachen wird bis zu ~%.1f Sekunden " -"brauchen) [1]" +"Wie viele Zeilen rückgängig machen? (Rückgängigmachen wird bis zu ~%.1f " +"Sekunden brauchen) [1]" -#: bpython/repl.py:964 bpython/repl.py:968 +#: bpython/repl.py:945 bpython/repl.py:949 msgid "Undo canceled" msgstr "Rückgängigmachen abgebrochen" -#: bpython/repl.py:971 +#: bpython/repl.py:952 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "Mache %d Zeile rückgängig... (ungefähr %.1f Sekunden)" msgstr[1] "Mache %d Zeilen rückgängig... (ungefähr %.1f Sekunden)" -#: bpython/repl.py:1147 +#: bpython/repl.py:1128 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -"Konfigurationsdatei existiert nicht. Soll eine neue Datei erstellt werden? (j/N)" +"Konfigurationsdatei existiert nicht. Soll eine neue Datei erstellt " +"werden? (j/N)" -#: bpython/repl.py:1175 +#: bpython/repl.py:1153 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -"bpython Konfigurationsdatei bearbeitet. Starte bpython neu damit die Änderungen " -"übernommen werden." +"bpython Konfigurationsdatei bearbeitet. Starte bpython neu damit die " +"Änderungen übernommen werden." -#: bpython/repl.py:1181 +#: bpython/repl.py:1158 #, python-format msgid "Error editing config file: %s" msgstr "Fehler beim Bearbeiten der Konfigurationsdatei: %s" -#: bpython/urwid.py:604 +#: bpython/urwid.py:606 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" -" <%s> Rückgängigmachen <%s> Speichern <%s> Pastebin <%s> Pager <%s> Quellcode " -"anzeigen " +" <%s> Rückgängigmachen <%s> Speichern <%s> Pastebin <%s> Pager <%s> " +"Quellcode anzeigen " -#: bpython/urwid.py:1114 +#: bpython/urwid.py:1116 msgid "Run twisted reactor." msgstr "Führe twisted reactor aus." -#: bpython/urwid.py:1119 +#: bpython/urwid.py:1121 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "Wähle reactor aus (siehe --help-reactors). Impliziert --twisted." -#: bpython/urwid.py:1127 +#: bpython/urwid.py:1129 msgid "List available reactors for -r." msgstr "Liste verfügbare reactors für -r auf." -#: bpython/urwid.py:1132 +#: bpython/urwid.py:1134 msgid "" -"twistd plugin to run (use twistd for a list). Use \"--\" to pass further options " -"to the plugin." +"twistd plugin to run (use twistd for a list). Use \"--\" to pass further " +"options to the plugin." msgstr "" -"Auszuführendes twistd Plugin (starte twistd für eine Liste). Verwende \"--\" um " -"Optionen an das Plugin zu übergeben." +"Auszuführendes twistd Plugin (starte twistd für eine Liste). Verwende " +"\"--\" um Optionen an das Plugin zu übergeben." -#: bpython/urwid.py:1141 +#: bpython/urwid.py:1143 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1335 +#: bpython/urwid.py:1337 msgid "" -"WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. This " -"backend has been deprecated in version 0.19 and might disappear in a future " -"version." +"WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " +"This backend has been deprecated in version 0.19 and might disappear in a" +" future version." msgstr "" "ACHTUNG: `bpython-urwid` wird verwendet, die curses Implementierung von " "`bpython`. Diese Implementierung wird ab Version 0.19 nicht mehr aktiv " "unterstützt und wird in einer zukünftigen Version entfernt werden." -#: bpython/curtsiesfrontend/repl.py:325 +#: bpython/curtsiesfrontend/repl.py:339 msgid "Welcome to bpython!" msgstr "Willkommen by bpython!" -#: bpython/curtsiesfrontend/repl.py:327 +#: bpython/curtsiesfrontend/repl.py:341 #, python-format msgid "Press <%s> for help." msgstr "Drücke <%s> für Hilfe." -#: bpython/curtsiesfrontend/repl.py:664 +#: bpython/curtsiesfrontend/repl.py:681 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "Fehler beim Ausführen von PYTHONSTARTUP: %s" -#: bpython/curtsiesfrontend/repl.py:682 +#: bpython/curtsiesfrontend/repl.py:698 #, python-format msgid "Reloaded at %s because %s modified." msgstr "Bei %s neugeladen, da %s modifiziert wurde." -#: bpython/curtsiesfrontend/repl.py:991 +#: bpython/curtsiesfrontend/repl.py:1008 msgid "Session not reevaluated because it was not edited" msgstr "Die Sitzung wurde nicht neu ausgeführt, da sie nicht berabeitet wurde" -#: bpython/curtsiesfrontend/repl.py:1006 +#: bpython/curtsiesfrontend/repl.py:1023 msgid "Session not reevaluated because saved file was blank" msgstr "Die Sitzung wurde nicht neu ausgeführt, da die gespeicherte Datei leer war" -#: bpython/curtsiesfrontend/repl.py:1016 +#: bpython/curtsiesfrontend/repl.py:1033 msgid "Session edited and reevaluated" msgstr "Sitzung bearbeitet und neu ausgeführt" -#: bpython/curtsiesfrontend/repl.py:1027 +#: bpython/curtsiesfrontend/repl.py:1044 #, python-format msgid "Reloaded at %s by user." msgstr "Bei %s vom Benutzer neu geladen." -#: bpython/curtsiesfrontend/repl.py:1033 +#: bpython/curtsiesfrontend/repl.py:1050 msgid "Auto-reloading deactivated." msgstr "Automatisches Neuladen deaktiviert." -#: bpython/curtsiesfrontend/repl.py:1038 +#: bpython/curtsiesfrontend/repl.py:1055 msgid "Auto-reloading active, watching for file changes..." msgstr "Automatisches Neuladen ist aktiv; beobachte Dateiänderungen..." -#: bpython/curtsiesfrontend/repl.py:1044 +#: bpython/curtsiesfrontend/repl.py:1061 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -"Automatisches Neuladen ist nicht verfügbar da watchdog nicht installiert ist." +"Automatisches Neuladen ist nicht verfügbar da watchdog nicht installiert " +"ist." -#: bpython/curtsiesfrontend/repl.py:1986 +#: bpython/curtsiesfrontend/repl.py:2011 msgid "" "\n" "Thanks for using bpython!\n" "\n" -"See http://bpython-interpreter.org/ for more information and http://docs.bpython-" -"interpreter.org/ for docs.\n" +"See http://bpython-interpreter.org/ for more information and http://docs" +".bpython-interpreter.org/ for docs.\n" "Please report issues at https://github.com/bpython/bpython/issues\n" "\n" "Features:\n" "Try using undo ({config.undo_key})!\n" -"Edit the current line ({config.edit_current_block_key}) or the entire session " -"({config.external_editor_key}) in an external editor. (currently {config." -"editor})\n" -"Save sessions ({config.save_key}) or post them to pastebins ({config." -"pastebin_key})! Current pastebin helper: {config.pastebin_helper}\n" -"Reload all modules and rerun session ({config.reimport_key}) to test out changes " -"to a module.\n" -"Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute the " -"current session when a module you've imported is modified.\n" +"Edit the current line ({config.edit_current_block_key}) or the entire " +"session ({config.external_editor_key}) in an external editor. (currently " +"{config.editor})\n" +"Save sessions ({config.save_key}) or post them to pastebins " +"({config.pastebin_key})! Current pastebin helper: " +"{config.pastebin_helper}\n" +"Reload all modules and rerun session ({config.reimport_key}) to test out " +"changes to a module.\n" +"Toggle auto-reload mode ({config.toggle_file_watch_key}) to re-execute " +"the current session when a module you've imported is modified.\n" "\n" "bpython -i your_script.py runs a file in interactive mode\n" "bpython -t your_script.py pastes the contents of a file into the session\n" "\n" -"A config file at {config.config_path} customizes keys and behavior of bpython.\n" +"A config file at {config.config_path} customizes keys and behavior of " +"bpython.\n" "You can also set which pastebin helper and which external editor to use.\n" "See {example_config_url} for an example config file.\n" "Press {config.edit_config_key} to edit this config file.\n" diff --git a/bpython/translations/es_ES/LC_MESSAGES/bpython.po b/bpython/translations/es_ES/LC_MESSAGES/bpython.po index b877fd1bd..5af25b37a 100644 --- a/bpython/translations/es_ES/LC_MESSAGES/bpython.po +++ b/bpython/translations/es_ES/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-02-14 17:29+0100\n" +"POT-Creation-Date: 2021-10-12 21:58+0200\n" "PO-Revision-Date: 2020-10-29 12:22+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: es_ES\n" @@ -18,15 +18,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:49 +#: bpython/args.py:63 msgid "{} version {} on top of Python {} {}" msgstr "" -#: bpython/args.py:58 +#: bpython/args.py:72 msgid "{} See AUTHORS.rst for details." msgstr "" -#: bpython/args.py:95 +#: bpython/args.py:116 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -34,82 +34,83 @@ msgid "" "to the regular Python interpreter." msgstr "" -#: bpython/args.py:105 +#: bpython/args.py:127 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:111 +#: bpython/args.py:133 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:117 +#: bpython/args.py:139 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:123 +#: bpython/args.py:145 msgid "Print version and exit." msgstr "" -#: bpython/args.py:130 +#: bpython/args.py:152 msgid "Set log level for logging" msgstr "" -#: bpython/args.py:135 +#: bpython/args.py:157 msgid "Log output file" msgstr "" -#: bpython/args.py:146 +#: bpython/args.py:168 msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:312 bpython/urwid.py:537 +#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/urwid.py:539 msgid "y" msgstr "s" -#: bpython/cli.py:312 bpython/urwid.py:537 +#: bpython/cli.py:320 bpython/urwid.py:539 msgid "yes" msgstr "si" -#: bpython/cli.py:1692 +#: bpython/cli.py:1696 msgid "Rewind" msgstr "" -#: bpython/cli.py:1693 +#: bpython/cli.py:1697 msgid "Save" msgstr "" -#: bpython/cli.py:1694 +#: bpython/cli.py:1698 msgid "Pastebin" msgstr "" -#: bpython/cli.py:1695 +#: bpython/cli.py:1699 msgid "Pager" msgstr "" -#: bpython/cli.py:1696 +#: bpython/cli.py:1700 msgid "Show Source" msgstr "" -#: bpython/cli.py:1943 +#: bpython/cli.py:1947 msgid "" "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsies.py:136 +#: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:142 +#: bpython/curtsies.py:207 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:143 +#: bpython/curtsies.py:208 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" -#: bpython/history.py:224 +#: bpython/history.py:250 #, python-format msgid "Error occurred while writing to file %s (%s)" msgstr "" @@ -135,214 +136,214 @@ msgstr "" msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:653 +#: bpython/repl.py:644 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:658 +#: bpython/repl.py:649 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:663 +#: bpython/repl.py:654 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:665 +#: bpython/repl.py:656 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:820 +#: bpython/repl.py:801 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 +#: bpython/repl.py:803 bpython/repl.py:806 bpython/repl.py:830 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:836 +#: bpython/repl.py:817 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:844 +#: bpython/repl.py:825 msgid "overwrite" msgstr "" -#: bpython/repl.py:846 +#: bpython/repl.py:827 msgid "append" msgstr "" -#: bpython/repl.py:858 bpython/repl.py:1165 +#: bpython/repl.py:839 bpython/repl.py:1143 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:860 +#: bpython/repl.py:841 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:866 +#: bpython/repl.py:847 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:873 +#: bpython/repl.py:854 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:875 +#: bpython/repl.py:856 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:884 +#: bpython/repl.py:865 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:886 +#: bpython/repl.py:867 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:894 +#: bpython/repl.py:875 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:900 +#: bpython/repl.py:881 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:885 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:913 +#: bpython/repl.py:894 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:918 +#: bpython/repl.py:899 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:956 +#: bpython/repl.py:937 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:964 bpython/repl.py:968 +#: bpython/repl.py:945 bpython/repl.py:949 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:971 +#: bpython/repl.py:952 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1147 +#: bpython/repl.py:1128 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1175 +#: bpython/repl.py:1153 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1181 +#: bpython/repl.py:1158 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:604 +#: bpython/urwid.py:606 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" " <%s> Rewind <%s> Salva <%s> Pastebin <%s> Pager <%s> Mostra el " "código fuente" -#: bpython/urwid.py:1114 +#: bpython/urwid.py:1116 msgid "Run twisted reactor." msgstr "" -#: bpython/urwid.py:1119 +#: bpython/urwid.py:1121 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "" -#: bpython/urwid.py:1127 +#: bpython/urwid.py:1129 msgid "List available reactors for -r." msgstr "" -#: bpython/urwid.py:1132 +#: bpython/urwid.py:1134 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." msgstr "" -#: bpython/urwid.py:1141 +#: bpython/urwid.py:1143 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1335 +#: bpython/urwid.py:1337 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:325 +#: bpython/curtsiesfrontend/repl.py:339 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:327 +#: bpython/curtsiesfrontend/repl.py:341 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:664 +#: bpython/curtsiesfrontend/repl.py:681 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:682 +#: bpython/curtsiesfrontend/repl.py:698 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:991 +#: bpython/curtsiesfrontend/repl.py:1008 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1006 +#: bpython/curtsiesfrontend/repl.py:1023 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1016 +#: bpython/curtsiesfrontend/repl.py:1033 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1027 +#: bpython/curtsiesfrontend/repl.py:1044 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1033 +#: bpython/curtsiesfrontend/repl.py:1050 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1038 +#: bpython/curtsiesfrontend/repl.py:1055 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1044 +#: bpython/curtsiesfrontend/repl.py:1061 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1986 +#: bpython/curtsiesfrontend/repl.py:2011 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po index d4c4d7758..32bbec662 100644 --- a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po +++ b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.13-442\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-02-14 17:29+0100\n" +"POT-Creation-Date: 2021-10-12 21:58+0200\n" "PO-Revision-Date: 2020-10-29 12:20+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: fr_FR\n" @@ -17,15 +17,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:49 +#: bpython/args.py:63 msgid "{} version {} on top of Python {} {}" msgstr "" -#: bpython/args.py:58 +#: bpython/args.py:72 msgid "{} See AUTHORS.rst for details." msgstr "" -#: bpython/args.py:95 +#: bpython/args.py:116 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -36,84 +36,85 @@ msgstr "" "NOTE: Si bpython ne reconnaît pas un des arguments fournis, " "l'interpréteur Python classique sera lancé" -#: bpython/args.py:105 +#: bpython/args.py:127 msgid "Use CONFIG instead of default config file." msgstr "Utiliser CONFIG à la place du fichier de configuration par défaut." -#: bpython/args.py:111 +#: bpython/args.py:133 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" "Aller dans le shell bpython après l'exécution du fichier au lieu de " "quitter." -#: bpython/args.py:117 +#: bpython/args.py:139 msgid "Don't flush the output to stdout." msgstr "Ne pas purger la sortie vers stdout." -#: bpython/args.py:123 +#: bpython/args.py:145 msgid "Print version and exit." msgstr "Afficher la version et quitter." -#: bpython/args.py:130 +#: bpython/args.py:152 msgid "Set log level for logging" msgstr "" -#: bpython/args.py:135 +#: bpython/args.py:157 msgid "Log output file" msgstr "" -#: bpython/args.py:146 +#: bpython/args.py:168 msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:312 bpython/urwid.py:537 +#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/urwid.py:539 msgid "y" msgstr "o" -#: bpython/cli.py:312 bpython/urwid.py:537 +#: bpython/cli.py:320 bpython/urwid.py:539 msgid "yes" msgstr "oui" -#: bpython/cli.py:1692 +#: bpython/cli.py:1696 msgid "Rewind" msgstr "Rembobiner" -#: bpython/cli.py:1693 +#: bpython/cli.py:1697 msgid "Save" msgstr "Sauvegarder" -#: bpython/cli.py:1694 +#: bpython/cli.py:1698 msgid "Pastebin" msgstr "" -#: bpython/cli.py:1695 +#: bpython/cli.py:1699 msgid "Pager" msgstr "" -#: bpython/cli.py:1696 +#: bpython/cli.py:1700 msgid "Show Source" msgstr "Montrer le code source" -#: bpython/cli.py:1943 +#: bpython/cli.py:1947 msgid "" "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsies.py:136 +#: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:142 +#: bpython/curtsies.py:207 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:143 +#: bpython/curtsies.py:208 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" -#: bpython/history.py:224 +#: bpython/history.py:250 #, python-format msgid "Error occurred while writing to file %s (%s)" msgstr "Une erreur s'est produite pendant l'écriture du fichier %s (%s)" @@ -139,149 +140,149 @@ msgstr "pas de sortie du programme externe." msgid "Failed to recognize the helper program's output as an URL." msgstr "la sortie du programme externe ne correspond pas à une URL." -#: bpython/repl.py:653 +#: bpython/repl.py:644 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:658 +#: bpython/repl.py:649 #, python-format msgid "Cannot get source: %s" msgstr "Impossible de récupérer le source: %s" -#: bpython/repl.py:663 +#: bpython/repl.py:654 #, python-format msgid "Cannot access source of %r" msgstr "Impossible d'accéder au source de %r" -#: bpython/repl.py:665 +#: bpython/repl.py:656 #, python-format msgid "No source code found for %s" msgstr "Pas de code source trouvé pour %s" -#: bpython/repl.py:820 +#: bpython/repl.py:801 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 +#: bpython/repl.py:803 bpython/repl.py:806 bpython/repl.py:830 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:836 +#: bpython/repl.py:817 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:844 +#: bpython/repl.py:825 msgid "overwrite" msgstr "" -#: bpython/repl.py:846 +#: bpython/repl.py:827 msgid "append" msgstr "" -#: bpython/repl.py:858 bpython/repl.py:1165 +#: bpython/repl.py:839 bpython/repl.py:1143 #, python-format msgid "Error writing file '%s': %s" msgstr "Une erreur s'est produite pendant l'écriture du fichier '%s': %s" -#: bpython/repl.py:860 +#: bpython/repl.py:841 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:866 +#: bpython/repl.py:847 msgid "No clipboard available." msgstr "Pas de presse-papier disponible." -#: bpython/repl.py:873 +#: bpython/repl.py:854 msgid "Could not copy to clipboard." msgstr "Impossible de copier vers le presse-papier." -#: bpython/repl.py:875 +#: bpython/repl.py:856 msgid "Copied content to clipboard." msgstr "Contenu copié vers le presse-papier." -#: bpython/repl.py:884 +#: bpython/repl.py:865 msgid "Pastebin buffer? (y/N) " msgstr "Tampon Pastebin ? (o/N) " -#: bpython/repl.py:886 +#: bpython/repl.py:867 msgid "Pastebin aborted." msgstr "Pastebin abandonné." -#: bpython/repl.py:894 +#: bpython/repl.py:875 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "Pastebin dupliqué. URL précédente: %s. URL de suppression: %s" -#: bpython/repl.py:900 +#: bpython/repl.py:881 msgid "Posting data to pastebin..." msgstr "Envoi des donnés à pastebin..." -#: bpython/repl.py:904 +#: bpython/repl.py:885 #, python-format msgid "Upload failed: %s" msgstr "Echec du téléchargement: %s" -#: bpython/repl.py:913 +#: bpython/repl.py:894 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "URL Pastebin: %s - URL de suppression: %s" -#: bpython/repl.py:918 +#: bpython/repl.py:899 #, python-format msgid "Pastebin URL: %s" msgstr "URL Pastebin: %s" -#: bpython/repl.py:956 +#: bpython/repl.py:937 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:964 bpython/repl.py:968 +#: bpython/repl.py:945 bpython/repl.py:949 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:971 +#: bpython/repl.py:952 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1147 +#: bpython/repl.py:1128 msgid "Config file does not exist - create new from default? (y/N)" msgstr "Le fichier de configuration n'existe pas - en créér un par défaut? (o/N)" -#: bpython/repl.py:1175 +#: bpython/repl.py:1153 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1181 +#: bpython/repl.py:1158 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:604 +#: bpython/urwid.py:606 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr "" " <%s> Rebobiner <%s> Sauvegarder <%s> Pastebin <%s> Pager <%s> " "Montrer Source " -#: bpython/urwid.py:1114 +#: bpython/urwid.py:1116 msgid "Run twisted reactor." msgstr "Lancer le reactor twisted." -#: bpython/urwid.py:1119 +#: bpython/urwid.py:1121 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "Choisir un reactor spécifique (voir --help-reactors). Nécessite --twisted." -#: bpython/urwid.py:1127 +#: bpython/urwid.py:1129 msgid "List available reactors for -r." msgstr "Lister les reactors disponibles pour -r." -#: bpython/urwid.py:1132 +#: bpython/urwid.py:1134 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." @@ -289,66 +290,66 @@ msgstr "" "plugin twistd à lancer (utiliser twistd pour une list). Utiliser \"--\" " "pour donner plus d'options au plugin." -#: bpython/urwid.py:1141 +#: bpython/urwid.py:1143 msgid "Port to run an eval server on (forces Twisted)." msgstr "Port pour lancer un server eval (force Twisted)." -#: bpython/urwid.py:1335 +#: bpython/urwid.py:1337 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:325 +#: bpython/curtsiesfrontend/repl.py:339 msgid "Welcome to bpython!" msgstr "Bienvenue dans bpython!" -#: bpython/curtsiesfrontend/repl.py:327 +#: bpython/curtsiesfrontend/repl.py:341 #, python-format msgid "Press <%s> for help." msgstr "Appuyer sur <%s> pour de l'aide." -#: bpython/curtsiesfrontend/repl.py:664 +#: bpython/curtsiesfrontend/repl.py:681 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "L'exécution de PYTHONSTARTUP a échoué: %s" -#: bpython/curtsiesfrontend/repl.py:682 +#: bpython/curtsiesfrontend/repl.py:698 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:991 +#: bpython/curtsiesfrontend/repl.py:1008 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1006 +#: bpython/curtsiesfrontend/repl.py:1023 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1016 +#: bpython/curtsiesfrontend/repl.py:1033 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1027 +#: bpython/curtsiesfrontend/repl.py:1044 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1033 +#: bpython/curtsiesfrontend/repl.py:1050 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1038 +#: bpython/curtsiesfrontend/repl.py:1055 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1044 +#: bpython/curtsiesfrontend/repl.py:1061 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1986 +#: bpython/curtsiesfrontend/repl.py:2011 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/it_IT/LC_MESSAGES/bpython.po b/bpython/translations/it_IT/LC_MESSAGES/bpython.po index ddb93cc0c..d0076cffd 100644 --- a/bpython/translations/it_IT/LC_MESSAGES/bpython.po +++ b/bpython/translations/it_IT/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-02-14 17:29+0100\n" +"POT-Creation-Date: 2021-10-12 21:58+0200\n" "PO-Revision-Date: 2015-02-02 00:34+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: it_IT\n" @@ -18,15 +18,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:49 +#: bpython/args.py:63 msgid "{} version {} on top of Python {} {}" msgstr "" -#: bpython/args.py:58 +#: bpython/args.py:72 msgid "{} See AUTHORS.rst for details." msgstr "" -#: bpython/args.py:95 +#: bpython/args.py:116 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -34,82 +34,83 @@ msgid "" "to the regular Python interpreter." msgstr "" -#: bpython/args.py:105 +#: bpython/args.py:127 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:111 +#: bpython/args.py:133 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:117 +#: bpython/args.py:139 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:123 +#: bpython/args.py:145 msgid "Print version and exit." msgstr "" -#: bpython/args.py:130 +#: bpython/args.py:152 msgid "Set log level for logging" msgstr "" -#: bpython/args.py:135 +#: bpython/args.py:157 msgid "Log output file" msgstr "" -#: bpython/args.py:146 +#: bpython/args.py:168 msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:312 bpython/urwid.py:537 +#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/urwid.py:539 msgid "y" msgstr "s" -#: bpython/cli.py:312 bpython/urwid.py:537 +#: bpython/cli.py:320 bpython/urwid.py:539 msgid "yes" msgstr "si" -#: bpython/cli.py:1692 +#: bpython/cli.py:1696 msgid "Rewind" msgstr "" -#: bpython/cli.py:1693 +#: bpython/cli.py:1697 msgid "Save" msgstr "" -#: bpython/cli.py:1694 +#: bpython/cli.py:1698 msgid "Pastebin" msgstr "" -#: bpython/cli.py:1695 +#: bpython/cli.py:1699 msgid "Pager" msgstr "" -#: bpython/cli.py:1696 +#: bpython/cli.py:1700 msgid "Show Source" msgstr "" -#: bpython/cli.py:1943 +#: bpython/cli.py:1947 msgid "" "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsies.py:136 +#: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:142 +#: bpython/curtsies.py:207 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:143 +#: bpython/curtsies.py:208 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" -#: bpython/history.py:224 +#: bpython/history.py:250 #, python-format msgid "Error occurred while writing to file %s (%s)" msgstr "" @@ -135,212 +136,212 @@ msgstr "" msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:653 +#: bpython/repl.py:644 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:658 +#: bpython/repl.py:649 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:663 +#: bpython/repl.py:654 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:665 +#: bpython/repl.py:656 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:820 +#: bpython/repl.py:801 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 +#: bpython/repl.py:803 bpython/repl.py:806 bpython/repl.py:830 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:836 +#: bpython/repl.py:817 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:844 +#: bpython/repl.py:825 msgid "overwrite" msgstr "" -#: bpython/repl.py:846 +#: bpython/repl.py:827 msgid "append" msgstr "" -#: bpython/repl.py:858 bpython/repl.py:1165 +#: bpython/repl.py:839 bpython/repl.py:1143 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:860 +#: bpython/repl.py:841 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:866 +#: bpython/repl.py:847 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:873 +#: bpython/repl.py:854 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:875 +#: bpython/repl.py:856 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:884 +#: bpython/repl.py:865 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:886 +#: bpython/repl.py:867 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:894 +#: bpython/repl.py:875 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:900 +#: bpython/repl.py:881 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:885 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:913 +#: bpython/repl.py:894 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:918 +#: bpython/repl.py:899 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:956 +#: bpython/repl.py:937 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:964 bpython/repl.py:968 +#: bpython/repl.py:945 bpython/repl.py:949 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:971 +#: bpython/repl.py:952 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1147 +#: bpython/repl.py:1128 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1175 +#: bpython/repl.py:1153 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1181 +#: bpython/repl.py:1158 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:604 +#: bpython/urwid.py:606 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr " <%s> Rewind <%s> Salva <%s> Pastebin <%s> Pager <%s> Mostra Sorgente" -#: bpython/urwid.py:1114 +#: bpython/urwid.py:1116 msgid "Run twisted reactor." msgstr "" -#: bpython/urwid.py:1119 +#: bpython/urwid.py:1121 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "" -#: bpython/urwid.py:1127 +#: bpython/urwid.py:1129 msgid "List available reactors for -r." msgstr "" -#: bpython/urwid.py:1132 +#: bpython/urwid.py:1134 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." msgstr "" -#: bpython/urwid.py:1141 +#: bpython/urwid.py:1143 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1335 +#: bpython/urwid.py:1337 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:325 +#: bpython/curtsiesfrontend/repl.py:339 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:327 +#: bpython/curtsiesfrontend/repl.py:341 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:664 +#: bpython/curtsiesfrontend/repl.py:681 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:682 +#: bpython/curtsiesfrontend/repl.py:698 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:991 +#: bpython/curtsiesfrontend/repl.py:1008 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1006 +#: bpython/curtsiesfrontend/repl.py:1023 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1016 +#: bpython/curtsiesfrontend/repl.py:1033 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1027 +#: bpython/curtsiesfrontend/repl.py:1044 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1033 +#: bpython/curtsiesfrontend/repl.py:1050 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1038 +#: bpython/curtsiesfrontend/repl.py:1055 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1044 +#: bpython/curtsiesfrontend/repl.py:1061 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1986 +#: bpython/curtsiesfrontend/repl.py:2011 msgid "" "\n" "Thanks for using bpython!\n" diff --git a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po index 582442406..d110f3ba8 100644 --- a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po +++ b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: bpython 0.9.7.1\n" "Report-Msgid-Bugs-To: http://github.com/bpython/bpython/issues\n" -"POT-Creation-Date: 2021-02-14 17:29+0100\n" +"POT-Creation-Date: 2021-10-12 21:58+0200\n" "PO-Revision-Date: 2020-10-29 12:20+0100\n" "Last-Translator: Sebastian Ramacher \n" "Language: nl_NL\n" @@ -18,15 +18,15 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.8.0\n" -#: bpython/args.py:49 +#: bpython/args.py:63 msgid "{} version {} on top of Python {} {}" msgstr "" -#: bpython/args.py:58 +#: bpython/args.py:72 msgid "{} See AUTHORS.rst for details." msgstr "" -#: bpython/args.py:95 +#: bpython/args.py:116 #, python-format msgid "" "Usage: %(prog)s [options] [file [args]]\n" @@ -34,82 +34,83 @@ msgid "" "to the regular Python interpreter." msgstr "" -#: bpython/args.py:105 +#: bpython/args.py:127 msgid "Use CONFIG instead of default config file." msgstr "" -#: bpython/args.py:111 +#: bpython/args.py:133 msgid "Drop to bpython shell after running file instead of exiting." msgstr "" -#: bpython/args.py:117 +#: bpython/args.py:139 msgid "Don't flush the output to stdout." msgstr "" -#: bpython/args.py:123 +#: bpython/args.py:145 msgid "Print version and exit." msgstr "" -#: bpython/args.py:130 +#: bpython/args.py:152 msgid "Set log level for logging" msgstr "" -#: bpython/args.py:135 +#: bpython/args.py:157 msgid "Log output file" msgstr "" -#: bpython/args.py:146 +#: bpython/args.py:168 msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:312 bpython/urwid.py:537 +#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/urwid.py:539 msgid "y" msgstr "j" -#: bpython/cli.py:312 bpython/urwid.py:537 +#: bpython/cli.py:320 bpython/urwid.py:539 msgid "yes" msgstr "ja" -#: bpython/cli.py:1692 +#: bpython/cli.py:1696 msgid "Rewind" msgstr "" -#: bpython/cli.py:1693 +#: bpython/cli.py:1697 msgid "Save" msgstr "" -#: bpython/cli.py:1694 +#: bpython/cli.py:1698 msgid "Pastebin" msgstr "" -#: bpython/cli.py:1695 +#: bpython/cli.py:1699 msgid "Pager" msgstr "" -#: bpython/cli.py:1696 +#: bpython/cli.py:1700 msgid "Show Source" msgstr "" -#: bpython/cli.py:1943 +#: bpython/cli.py:1947 msgid "" "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsies.py:136 +#: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" -#: bpython/curtsies.py:142 +#: bpython/curtsies.py:207 msgid "curtsies arguments" msgstr "" -#: bpython/curtsies.py:143 +#: bpython/curtsies.py:208 msgid "Additional arguments specific to the curtsies-based REPL." msgstr "" -#: bpython/history.py:224 +#: bpython/history.py:250 #, python-format msgid "Error occurred while writing to file %s (%s)" msgstr "" @@ -135,212 +136,212 @@ msgstr "" msgid "Failed to recognize the helper program's output as an URL." msgstr "" -#: bpython/repl.py:653 +#: bpython/repl.py:644 msgid "Nothing to get source of" msgstr "" -#: bpython/repl.py:658 +#: bpython/repl.py:649 #, python-format msgid "Cannot get source: %s" msgstr "" -#: bpython/repl.py:663 +#: bpython/repl.py:654 #, python-format msgid "Cannot access source of %r" msgstr "" -#: bpython/repl.py:665 +#: bpython/repl.py:656 #, python-format msgid "No source code found for %s" msgstr "" -#: bpython/repl.py:820 +#: bpython/repl.py:801 msgid "Save to file (Esc to cancel): " msgstr "" -#: bpython/repl.py:822 bpython/repl.py:825 bpython/repl.py:849 +#: bpython/repl.py:803 bpython/repl.py:806 bpython/repl.py:830 msgid "Save cancelled." msgstr "" -#: bpython/repl.py:836 +#: bpython/repl.py:817 #, python-format msgid "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " msgstr "" -#: bpython/repl.py:844 +#: bpython/repl.py:825 msgid "overwrite" msgstr "" -#: bpython/repl.py:846 +#: bpython/repl.py:827 msgid "append" msgstr "" -#: bpython/repl.py:858 bpython/repl.py:1165 +#: bpython/repl.py:839 bpython/repl.py:1143 #, python-format msgid "Error writing file '%s': %s" msgstr "" -#: bpython/repl.py:860 +#: bpython/repl.py:841 #, python-format msgid "Saved to %s." msgstr "" -#: bpython/repl.py:866 +#: bpython/repl.py:847 msgid "No clipboard available." msgstr "" -#: bpython/repl.py:873 +#: bpython/repl.py:854 msgid "Could not copy to clipboard." msgstr "" -#: bpython/repl.py:875 +#: bpython/repl.py:856 msgid "Copied content to clipboard." msgstr "" -#: bpython/repl.py:884 +#: bpython/repl.py:865 msgid "Pastebin buffer? (y/N) " msgstr "" -#: bpython/repl.py:886 +#: bpython/repl.py:867 msgid "Pastebin aborted." msgstr "" -#: bpython/repl.py:894 +#: bpython/repl.py:875 #, python-format msgid "Duplicate pastebin. Previous URL: %s. Removal URL: %s" msgstr "" -#: bpython/repl.py:900 +#: bpython/repl.py:881 msgid "Posting data to pastebin..." msgstr "" -#: bpython/repl.py:904 +#: bpython/repl.py:885 #, python-format msgid "Upload failed: %s" msgstr "" -#: bpython/repl.py:913 +#: bpython/repl.py:894 #, python-format msgid "Pastebin URL: %s - Removal URL: %s" msgstr "" -#: bpython/repl.py:918 +#: bpython/repl.py:899 #, python-format msgid "Pastebin URL: %s" msgstr "" -#: bpython/repl.py:956 +#: bpython/repl.py:937 #, python-format msgid "Undo how many lines? (Undo will take up to ~%.1f seconds) [1]" msgstr "" -#: bpython/repl.py:964 bpython/repl.py:968 +#: bpython/repl.py:945 bpython/repl.py:949 msgid "Undo canceled" msgstr "" -#: bpython/repl.py:971 +#: bpython/repl.py:952 #, python-format msgid "Undoing %d line... (est. %.1f seconds)" msgid_plural "Undoing %d lines... (est. %.1f seconds)" msgstr[0] "" msgstr[1] "" -#: bpython/repl.py:1147 +#: bpython/repl.py:1128 msgid "Config file does not exist - create new from default? (y/N)" msgstr "" -#: bpython/repl.py:1175 +#: bpython/repl.py:1153 msgid "bpython config file edited. Restart bpython for changes to take effect." msgstr "" -#: bpython/repl.py:1181 +#: bpython/repl.py:1158 #, python-format msgid "Error editing config file: %s" msgstr "" -#: bpython/urwid.py:604 +#: bpython/urwid.py:606 #, python-format msgid " <%s> Rewind <%s> Save <%s> Pastebin <%s> Pager <%s> Show Source " msgstr " <%s> Rewind <%s> Opslaan <%s> Pastebin <%s> Pager <%s> Toon broncode" -#: bpython/urwid.py:1114 +#: bpython/urwid.py:1116 msgid "Run twisted reactor." msgstr "" -#: bpython/urwid.py:1119 +#: bpython/urwid.py:1121 msgid "Select specific reactor (see --help-reactors). Implies --twisted." msgstr "" -#: bpython/urwid.py:1127 +#: bpython/urwid.py:1129 msgid "List available reactors for -r." msgstr "" -#: bpython/urwid.py:1132 +#: bpython/urwid.py:1134 msgid "" "twistd plugin to run (use twistd for a list). Use \"--\" to pass further " "options to the plugin." msgstr "" -#: bpython/urwid.py:1141 +#: bpython/urwid.py:1143 msgid "Port to run an eval server on (forces Twisted)." msgstr "" -#: bpython/urwid.py:1335 +#: bpython/urwid.py:1337 msgid "" "WARNING: You are using `bpython-urwid`, the urwid backend for `bpython`. " "This backend has been deprecated in version 0.19 and might disappear in a" " future version." msgstr "" -#: bpython/curtsiesfrontend/repl.py:325 +#: bpython/curtsiesfrontend/repl.py:339 msgid "Welcome to bpython!" msgstr "" -#: bpython/curtsiesfrontend/repl.py:327 +#: bpython/curtsiesfrontend/repl.py:341 #, python-format msgid "Press <%s> for help." msgstr "" -#: bpython/curtsiesfrontend/repl.py:664 +#: bpython/curtsiesfrontend/repl.py:681 #, python-format msgid "Executing PYTHONSTARTUP failed: %s" msgstr "" -#: bpython/curtsiesfrontend/repl.py:682 +#: bpython/curtsiesfrontend/repl.py:698 #, python-format msgid "Reloaded at %s because %s modified." msgstr "" -#: bpython/curtsiesfrontend/repl.py:991 +#: bpython/curtsiesfrontend/repl.py:1008 msgid "Session not reevaluated because it was not edited" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1006 +#: bpython/curtsiesfrontend/repl.py:1023 msgid "Session not reevaluated because saved file was blank" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1016 +#: bpython/curtsiesfrontend/repl.py:1033 msgid "Session edited and reevaluated" msgstr "" -#: bpython/curtsiesfrontend/repl.py:1027 +#: bpython/curtsiesfrontend/repl.py:1044 #, python-format msgid "Reloaded at %s by user." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1033 +#: bpython/curtsiesfrontend/repl.py:1050 msgid "Auto-reloading deactivated." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1038 +#: bpython/curtsiesfrontend/repl.py:1055 msgid "Auto-reloading active, watching for file changes..." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1044 +#: bpython/curtsiesfrontend/repl.py:1061 msgid "Auto-reloading not available because watchdog not installed." msgstr "" -#: bpython/curtsiesfrontend/repl.py:1986 +#: bpython/curtsiesfrontend/repl.py:2011 msgid "" "\n" "Thanks for using bpython!\n" From 262f65c319a84ec01d70d0db62f3c8657c813698 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 12 Oct 2021 21:59:11 +0200 Subject: [PATCH 257/555] Fix --- doc/sphinx/source/configuration-options.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst index 6d347761d..4d13cbae5 100644 --- a/doc/sphinx/source/configuration-options.rst +++ b/doc/sphinx/source/configuration-options.rst @@ -305,7 +305,7 @@ Brings up sincerely cheerful description of bpython features and current key bin .. versionadded:: 0.14 incremental_search -^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^ Default: M-s Perform incremental search on all stored lines in the history. From 0746e559c71009cfc3dafd718caecf640b6258c6 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sun, 10 Oct 2021 13:09:50 -0700 Subject: [PATCH 258/555] remove optional arg of cwcswidth --- bpython/curtsiesfrontend/repl.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 85965f4f6..b25a321ff 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -16,7 +16,6 @@ from typing import Dict, Any, List, Optional, Tuple, Union, cast import blessings -import cwcwidth import greenlet from curtsies import ( FSArray, @@ -283,7 +282,7 @@ def _process_ps(ps, default_ps: str): if not isinstance(ps, str): return ps - return ps if cwcwidth.wcswidth(ps, None) >= 0 else default_ps + return ps if wcswidth(ps) >= 0 else default_ps class BaseRepl(Repl): @@ -1626,8 +1625,8 @@ def move_screen_up(current_line_start_row): ) else: # Common case for determining cursor position cursor_row, cursor_column = divmod( - wcswidth(self.current_cursor_line_without_suggestion.s, None) - - wcswidth(self.current_line, None) + wcswidth(self.current_cursor_line_without_suggestion.s) + - wcswidth(self.current_line) + wcswidth(self.current_line, max(0, self.cursor_offset)) + self.number_of_padding_chars_on_current_cursor_line(), width, From 643a091079f12abacbb34e02490b95c4221414ca Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 15 Oct 2021 22:18:01 +0200 Subject: [PATCH 259/555] Return None on error instead of asserting --- bpython/autocomplete.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index fd91ea4af..e9ad61114 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -480,9 +480,11 @@ def matches( r = self.locate(cursor_offset, line) if r is None: return None - curDictParts = lineparts.current_dict(cursor_offset, line) - assert curDictParts, "current_dict when .locate() truthy" - _, _, dexpr = curDictParts + current_dict_parts = lineparts.current_dict(cursor_offset, line) + if current_dict_parts is None: + return None + + _, _, dexpr = current_dict_parts try: obj = safe_eval(dexpr, locals_) except EvaluationError: From 03334d38da1e5ecf6676580c460cbc6d7519711a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 15 Oct 2021 22:22:48 +0200 Subject: [PATCH 260/555] Mark match functions as private --- bpython/autocomplete.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index e9ad61114..c422c2a75 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -197,28 +197,28 @@ def few_enough_underscores(current: str, match: str) -> bool: return not match.startswith("_") -def method_match_none(word: str, size: int, text: str) -> bool: +def _method_match_none(word: str, size: int, text: str) -> bool: return False -def method_match_simple(word: str, size: int, text: str) -> bool: +def _method_match_simple(word: str, size: int, text: str) -> bool: return word[:size] == text -def method_match_substring(word: str, size: int, text: str) -> bool: +def _method_match_substring(word: str, size: int, text: str) -> bool: return text in word -def method_match_fuzzy(word: str, size: int, text: str) -> Optional[Match]: +def _method_match_fuzzy(word: str, size: int, text: str) -> Optional[Match]: s = r".*%s.*" % ".*".join(list(text)) return re.search(s, word) -MODES_MAP = { - AutocompleteModes.NONE: method_match_none, - AutocompleteModes.SIMPLE: method_match_simple, - AutocompleteModes.SUBSTRING: method_match_substring, - AutocompleteModes.FUZZY: method_match_fuzzy, +_MODES_MAP = { + AutocompleteModes.NONE: _method_match_none, + AutocompleteModes.SIMPLE: _method_match_simple, + AutocompleteModes.SUBSTRING: _method_match_substring, + AutocompleteModes.FUZZY: _method_match_fuzzy, } @@ -231,7 +231,7 @@ def __init__( mode: AutocompleteModes = AutocompleteModes.SIMPLE, ) -> None: self._shown_before_tab = shown_before_tab - self.method_match = MODES_MAP[mode] + self.method_match = _MODES_MAP[mode] @abc.abstractmethod def matches( From 0cfbd99333b23313d514be76b8c267fdccb94ae0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 15 Oct 2021 22:25:43 +0200 Subject: [PATCH 261/555] Make return value and type of _method_match_fuzzy consistent with other match functions --- bpython/autocomplete.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index c422c2a75..f45516c14 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -42,12 +42,9 @@ Dict, Iterator, List, - Match, Optional, Set, - Union, Tuple, - Type, Sequence, ) from . import inspection @@ -209,9 +206,9 @@ def _method_match_substring(word: str, size: int, text: str) -> bool: return text in word -def _method_match_fuzzy(word: str, size: int, text: str) -> Optional[Match]: +def _method_match_fuzzy(word: str, size: int, text: str) -> bool: s = r".*%s.*" % ".*".join(list(text)) - return re.search(s, word) + return re.search(s, word) is not None _MODES_MAP = { From f87fc960a221a00276aa8ab15db79a00c16e755c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 15 Oct 2021 22:34:39 +0200 Subject: [PATCH 262/555] Use generator instead of a list --- bpython/autocomplete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index f45516c14..911dbb842 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -207,7 +207,7 @@ def _method_match_substring(word: str, size: int, text: str) -> bool: def _method_match_fuzzy(word: str, size: int, text: str) -> bool: - s = r".*%s.*" % ".*".join(list(text)) + s = r".*{}.*".format(".*".join(c for c in text)) return re.search(s, word) is not None From f9b21cafc87f343b861d458c3bc438b0e419c1cc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 15 Oct 2021 22:46:17 +0200 Subject: [PATCH 263/555] Also mention #700 --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5f956a1ec..679cfc128 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,13 +18,13 @@ New features: Fixes: +* #700, #884: Fix writing of b"" on fake stdout * #879: Iterate over all completers until a successful one is found * #882: Handle errors in theme configuration without crashing -* #884: Fix writing of b"" on fake stdout * #888: Read PYTHONSTARTUP with utf8 as encoding * #896: Use default sys.ps1 and sys.ps2 if user specified ones are not usable * #902: Do not crash when encountering unreadable files while processing modules for import completion -* #909: Fix sys.stdin.readlin +* #909: Fix sys.stdin.readline * #917: Fix tab completion for dict keys * #919: Replicate python behavior when running with -i and a non-existing file From 126b059ea620ed5c7ef6324bd8c3c0950cc54b3e Mon Sep 17 00:00:00 2001 From: gpotter2 Date: Thu, 21 Oct 2021 14:07:09 +0200 Subject: [PATCH 264/555] Fix advanced class completion --- bpython/repl.py | 2 -- bpython/test/test_repl.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index e261e61ad..5d664e659 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -601,8 +601,6 @@ def get_args(self): if inspect.isclass(f): class_f = None - if hasattr(f, "__init__") and f.__init__ is not object.__init__: - class_f = f.__init__ if ( (not class_f or not inspection.getfuncprops(func, class_f)) and hasattr(f, "__new__") diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 65a2fb813..e29c5a4e5 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -482,6 +482,34 @@ def test_paremeter_name_completion(self): self.repl.matches_iter.matches, ["abc=", "abd=", "abs("] ) + def test_parameter_advanced_on_class(self): + self.repl = FakeRepl( + {"autocomplete_mode": autocomplete.AutocompleteModes.SIMPLE} + ) + self.set_input_line("TestCls(app") + + code = """ + import inspect + + class TestCls: + # A class with boring __init__ typing + def __init__(self, *args, **kwargs): + pass + # But that uses super exotic typings recognized by inspect.signature + __signature__ = inspect.Signature([ + inspect.Parameter("apple", inspect.Parameter.POSITIONAL_ONLY), + inspect.Parameter("apple2", inspect.Parameter.KEYWORD_ONLY), + inspect.Parameter("pinetree", inspect.Parameter.KEYWORD_ONLY), + ]) + """ + for line in code.split("\n"): + print(line[8:]) + self.repl.push(line[8:]) + + self.assertTrue(self.repl.complete()) + self.assertTrue(hasattr(self.repl.matches_iter, "matches")) + self.assertEqual(self.repl.matches_iter.matches, ["apple2=", "apple="]) + class TestCliRepl(unittest.TestCase): def setUp(self): From bb6eda80796cc9d8197f5c5514b9e2c2c54aea84 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 22 Oct 2021 12:27:42 +0200 Subject: [PATCH 265/555] Mention #970 in the changelog [skip ci] --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 679cfc128..256e5d855 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,8 @@ Fixes: * #909: Fix sys.stdin.readline * #917: Fix tab completion for dict keys * #919: Replicate python behavior when running with -i and a non-existing file +* #932: Fix handling of __signature__ for completion. + Thanks to gpotter2 Changes to dependencies: From f51d61bb4eb25cc0eb74302de90b5c7fefc21803 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 30 Oct 2021 16:04:13 +0200 Subject: [PATCH 266/555] Improve signature of __exit__ --- bpython/curtsiesfrontend/_internal.py | 11 ++++++++++- bpython/curtsiesfrontend/repl.py | 12 +++++++++--- bpython/filelock.py | 5 +++-- bpython/inspection.py | 12 +++++++++--- bpython/repl.py | 12 +++++++++--- 5 files changed, 40 insertions(+), 12 deletions(-) diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index f918f6fc5..bc8e02c70 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -21,6 +21,9 @@ # THE SOFTWARE. import pydoc +from types import TracebackType +from typing import Optional, Literal, Type + from .. import _internal @@ -29,8 +32,14 @@ def __enter__(self): self._orig_pager = pydoc.pager pydoc.pager = self - def __exit__(self, *args): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Literal[False]: pydoc.pager = self._orig_pager + return False def __call__(self, text): return None diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index b25a321ff..a9f911e3f 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -12,8 +12,8 @@ import time import unicodedata from enum import Enum - -from typing import Dict, Any, List, Optional, Tuple, Union, cast +from types import TracebackType +from typing import Dict, Any, List, Optional, Tuple, Union, cast, Literal, Type import blessings import greenlet @@ -573,7 +573,12 @@ def __enter__(self): sitefix.monkeypatch_quit() return self - def __exit__(self, *args): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Literal[False]: sys.stdin = self.orig_stdin sys.stdout = self.orig_stdout sys.stderr = self.orig_stderr @@ -584,6 +589,7 @@ def __exit__(self, *args): signal.signal(signal.SIGTSTP, self.orig_sigtstp_handler) sys.meta_path = self.orig_meta_path + return False def sigwinch_handler(self, signum, frame): old_rows, old_columns = self.height, self.width diff --git a/bpython/filelock.py b/bpython/filelock.py index 3d77c439c..11f575b6e 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 +from typing import Optional, Type, IO, Literal from types import TracebackType has_fcntl = True @@ -59,9 +59,10 @@ def __exit__( exc_type: Optional[Type[BaseException]], exc: Optional[BaseException], exc_tb: Optional[TracebackType], - ) -> None: + ) -> Literal[False]: if self.locked: self.release() + return False def __del__(self) -> None: if self.locked: diff --git a/bpython/inspection.py b/bpython/inspection.py index 7f95fbd20..e3f8aba26 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -26,11 +26,11 @@ import pydoc import re from collections import namedtuple +from typing import Any, Optional, Literal, Type +from types import MemberDescriptorType, TracebackType from pygments.token import Token from pygments.lexers import Python3Lexer -from typing import Any -from types import MemberDescriptorType from .lazyre import LazyReCompile @@ -88,7 +88,12 @@ def __enter__(self): self.attribs = (__getattribute__, __getattr__) # /Dark magic - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Literal[False]: """Restore an object's magic methods.""" type_ = type(self.obj) __getattribute__, __getattr__ = self.attribs @@ -98,6 +103,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): if __getattr__ is not None: setattr(type_, "__getattr__", __getattr__) # /Dark magic + return False class _Repr: diff --git a/bpython/repl.py b/bpython/repl.py index 5d664e659..a21593e9c 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -36,10 +36,11 @@ from abc import abstractmethod from itertools import takewhile from pathlib import Path +from types import ModuleType, TracebackType +from typing import cast, Tuple, Any, Optional, Literal, Type + from pygments.lexers import Python3Lexer from pygments.token import Token -from types import ModuleType -from typing import cast, Tuple, Any have_pyperclip = True try: @@ -68,7 +69,12 @@ def __init__(self): def __enter__(self): self.start = self.time() - def __exit__(self, ty, val, tb): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Literal[False]: self.last_command = self.time() - self.start self.running_time += self.last_command return False From 194a00094f20f5747bc25fac564c0931b997ba6c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 31 Oct 2021 15:42:06 +0100 Subject: [PATCH 267/555] Fix import of Literal --- bpython/curtsiesfrontend/_internal.py | 3 ++- bpython/curtsiesfrontend/repl.py | 3 ++- bpython/filelock.py | 3 ++- bpython/inspection.py | 3 ++- bpython/repl.py | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index bc8e02c70..633174a88 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -22,7 +22,8 @@ import pydoc from types import TracebackType -from typing import Optional, Literal, Type +from typing import Optional, Type +from typing_extensions import Literal from .. import _internal diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index a9f911e3f..6007f6157 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -13,7 +13,8 @@ import unicodedata from enum import Enum from types import TracebackType -from typing import Dict, Any, List, Optional, Tuple, Union, cast, Literal, Type +from typing import Dict, Any, List, Optional, Tuple, Union, cast, Type +from typing_extensions import Literal import blessings import greenlet diff --git a/bpython/filelock.py b/bpython/filelock.py index 11f575b6e..6558fc583 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -20,7 +20,8 @@ # 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 Optional, Type, IO +from typing_extensions import Literal from types import TracebackType has_fcntl = True diff --git a/bpython/inspection.py b/bpython/inspection.py index e3f8aba26..08ad47c95 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -26,8 +26,9 @@ import pydoc import re from collections import namedtuple -from typing import Any, Optional, Literal, Type +from typing import Any, Optional, Type from types import MemberDescriptorType, TracebackType +from typing_extensions import Literal from pygments.token import Token from pygments.lexers import Python3Lexer diff --git a/bpython/repl.py b/bpython/repl.py index a21593e9c..32d08dbf2 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -37,7 +37,8 @@ from itertools import takewhile from pathlib import Path from types import ModuleType, TracebackType -from typing import cast, Tuple, Any, Optional, Literal, Type +from typing import cast, Tuple, Any, Optional, Type +from typing_extensions import Literal from pygments.lexers import Python3Lexer from pygments.token import Token From 29fc2a221c69d33f3366e36124df41f3c041af75 Mon Sep 17 00:00:00 2001 From: supakeen Date: Sun, 7 Nov 2021 19:30:52 +0100 Subject: [PATCH 268/555] The contents can't be rendered by PyPi and block release. --- README.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.rst b/README.rst index 539d036bd..976314c55 100644 --- a/README.rst +++ b/README.rst @@ -34,11 +34,6 @@ etc. You can find more about bpython - including `full documentation`_ - at our `homepage`_. -.. contents:: - :local: - :depth: 1 - :backlinks: none - ========================== Installation & Basic Usage ========================== From 2ba5de04ef8675ae5287cb0bcfa19741582667f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 15 Oct 2021 23:28:41 +0200 Subject: [PATCH 269/555] Add some return types --- bpython/inspection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 08ad47c95..e7f705147 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -267,7 +267,7 @@ def getfuncprops(func, f): return fprops -def is_eval_safe_name(string): +def is_eval_safe_name(string: str) -> bool: return all( part.isidentifier() and not keyword.iskeyword(part) for part in string.split(".") @@ -334,7 +334,7 @@ def get_argspec_from_signature(f): get_encoding_line_re = LazyReCompile(r"^.*coding[:=]\s*([-\w.]+).*$") -def get_encoding(obj): +def get_encoding(obj) -> str: """Try to obtain encoding information of the source of an object.""" for line in inspect.findsource(obj)[0][:2]: m = get_encoding_line_re.search(line) @@ -354,7 +354,7 @@ def get_encoding_file(fname: str) -> str: return "utf8" -def getattr_safe(obj: Any, name: str): +def getattr_safe(obj: Any, name: str) -> Any: """side effect free getattr (calls getattr_static).""" result = inspect.getattr_static(obj, name) # Slots are a MemberDescriptorType @@ -371,6 +371,6 @@ def hasattr_safe(obj: Any, name: str) -> bool: return False -def get_source_unicode(obj): +def get_source_unicode(obj) -> str: """Returns a decoded source of object""" return inspect.getsource(obj) From 87f305c023641ea6fbc7aed0eaa0cf3ea87f3965 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 7 Nov 2021 21:01:04 +0100 Subject: [PATCH 270/555] Bump required Python version to >=3.7 By the time bpython 0.23 is released, Python 3.6 will be EOL. --- .github/workflows/build.yaml | 2 +- doc/sphinx/source/contributing.rst | 2 +- doc/sphinx/source/releases.rst | 2 +- pyproject.toml | 2 +- requirements.txt | 1 - setup.cfg | 3 +-- 6 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index dc1fc9ed2..05e3f65d4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -13,7 +13,7 @@ jobs: continue-on-error: ${{ matrix.python-version == 'pypy3' }} strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9, "3.10", pypy3] + python-version: [3.7, 3.8, 3.9, "3.10", pypy3] steps: - uses: actions/checkout@v2 with: diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 76d3b9402..54fd56c6b 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.6 and newer. The code is compatible with all +bpython supports Python 3.7 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 1f723f592..738c24ff2 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.6 - 3.9 +* Runs under Python 3.7 - 3.9 * Save * Rewind * Pastebin diff --git a/pyproject.toml b/pyproject.toml index 79a558e88..c7ef64f28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ requires = [ [tool.black] line-length = 80 -target_version = ["py36"] +target_version = ["py37"] include = '\.pyi?$' exclude = ''' /( diff --git a/requirements.txt b/requirements.txt index a989bdf40..7f56dc0fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ Pygments backports.cached-property; python_version < "3.8" curtsies >=0.3.5 cwcwidth -dataclasses; python_version < "3.7" greenlet pyxdg requests diff --git a/setup.cfg b/setup.cfg index 1efed779c..806c01c89 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,7 @@ classifiers = Programming Language :: Python :: 3 [options] -python_requires = >=3.6 +python_requires = >=3.7 packages = bpython bpython.curtsiesfrontend @@ -22,7 +22,6 @@ packages = install_requires = backports.cached-property; python_version < "3.8" curtsies >=0.3.5 - dataclasses; python_version < "3.7" cwcwidth greenlet pygments From f8b5962027719beb761b20f1b46ee839072c2060 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 7 Nov 2021 21:22:47 +0100 Subject: [PATCH 271/555] Remove get_source_unicode It's just a call to inspect.getsource anyway. --- bpython/inspection.py | 5 ----- bpython/repl.py | 2 +- bpython/test/test_inspection.py | 7 ++++--- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index e7f705147..e7ab0a155 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -369,8 +369,3 @@ def hasattr_safe(obj: Any, name: str) -> bool: return True except AttributeError: return False - - -def get_source_unicode(obj) -> str: - """Returns a decoded source of object""" - return inspect.getsource(obj) diff --git a/bpython/repl.py b/bpython/repl.py index 32d08dbf2..9ffeba328 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -649,7 +649,7 @@ def get_source_of_current_name(self): raise SourceNotFound(_("Nothing to get source of")) if inspection.is_eval_safe_name(line): obj = self.get_object(line) - return inspection.get_source_unicode(obj) + return inspect.getsource(obj) except (AttributeError, NameError) as e: msg = _("Cannot get source: %s") % (e,) except OSError as e: diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 50be0c3a2..550f8731d 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -1,3 +1,4 @@ +import inspect import os import sys import unittest @@ -90,17 +91,17 @@ def test_get_encoding_utf8(self): def test_get_source_ascii(self): self.assertEqual( - inspection.get_source_unicode(encoding_ascii.foo), foo_ascii_only + inspect.getsource(encoding_ascii.foo), foo_ascii_only ) def test_get_source_utf8(self): self.assertEqual( - inspection.get_source_unicode(encoding_utf8.foo), foo_non_ascii + inspect.getsource(encoding_utf8.foo), foo_non_ascii ) def test_get_source_latin1(self): self.assertEqual( - inspection.get_source_unicode(encoding_latin1.foo), foo_non_ascii + inspect.getsource(encoding_latin1.foo), foo_non_ascii ) def test_get_source_file(self): From 72ee717a04368c48e3233d545e5cf517fbaf6a0c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 7 Nov 2021 21:42:53 +0100 Subject: [PATCH 272/555] Turn safe_glob into a function --- bpython/autocomplete.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 911dbb842..29a02141a 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -343,13 +343,14 @@ def format(self, word: str) -> str: return after_last_dot(word) +def _safe_glob(pathname: str) -> Iterator[str]: + return glob.iglob(glob.escape(pathname) + "*") + + class FilenameCompletion(BaseCompletionType): def __init__(self, mode: AutocompleteModes = AutocompleteModes.SIMPLE): super().__init__(False, mode) - def safe_glob(self, pathname: str) -> Iterator[str]: - return glob.iglob(glob.escape(pathname) + "*") - def matches( self, cursor_offset: int, line: str, **kwargs: Any ) -> Optional[Set]: @@ -359,7 +360,7 @@ def matches( matches = set() username = cs.word.split(os.path.sep, 1)[0] user_dir = os.path.expanduser(username) - for filename in self.safe_glob(os.path.expanduser(cs.word)): + for filename in _safe_glob(os.path.expanduser(cs.word)): if os.path.isdir(filename): filename += os.path.sep if cs.word.startswith("~"): From 88de05a8fdfc16cadf4cb286d0a9090b128c1647 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 7 Nov 2021 21:43:07 +0100 Subject: [PATCH 273/555] Remove useless if --- bpython/autocomplete.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 29a02141a..40d655b03 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -571,15 +571,15 @@ def matches( r = self.locate(cursor_offset, line) if r is None: return None - if argspec: - matches = { - f"{name}=" - for name in argspec[1][0] - if isinstance(name, str) and name.startswith(r.word) - } - matches.update( - name + "=" for name in argspec[1][4] if name.startswith(r.word) - ) + + matches = { + f"{name}=" + for name in argspec[1][0] + if isinstance(name, str) and name.startswith(r.word) + } + matches.update( + name + "=" for name in argspec[1][4] if name.startswith(r.word) + ) return matches if matches else None def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: From 535d4e9452bd0ebdcc5d715b3d825f60208f143a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 7 Nov 2021 21:53:50 +0100 Subject: [PATCH 274/555] Apply black --- bpython/test/test_inspection.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 550f8731d..fdbb959c9 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -90,19 +90,13 @@ def test_get_encoding_utf8(self): self.assertEqual(inspection.get_encoding(encoding_utf8.foo), "utf-8") def test_get_source_ascii(self): - self.assertEqual( - inspect.getsource(encoding_ascii.foo), foo_ascii_only - ) + self.assertEqual(inspect.getsource(encoding_ascii.foo), foo_ascii_only) def test_get_source_utf8(self): - self.assertEqual( - inspect.getsource(encoding_utf8.foo), foo_non_ascii - ) + self.assertEqual(inspect.getsource(encoding_utf8.foo), foo_non_ascii) def test_get_source_latin1(self): - self.assertEqual( - inspect.getsource(encoding_latin1.foo), foo_non_ascii - ) + self.assertEqual(inspect.getsource(encoding_latin1.foo), foo_non_ascii) def test_get_source_file(self): path = os.path.join( From 2f7899bb9bf28fd820df0707fd2722830254bbc5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 7 Nov 2021 22:07:50 +0100 Subject: [PATCH 275/555] Use more f-strings --- bpython/curtsiesfrontend/interpreter.py | 2 +- bpython/curtsiesfrontend/repl.py | 2 +- bpython/formatter.py | 4 ++-- bpython/simpleeval.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 7f7c2fcc7..48ee15bae 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -55,7 +55,7 @@ def format(self, tokensource, outfile): for token, text in tokensource: while token not in self.f_strings: token = token.parent - o += "{}\x03{}\x04".format(self.f_strings[token], text) + o += f"{self.f_strings[token]}\x03{text}\x04" outfile.write(parse(o.rstrip())) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 6007f6157..54b707806 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -2067,7 +2067,7 @@ def key_help_text(self): max_func = max(len(func) for func, key in pairs) return "\n".join( - "{} : {}".format(func.rjust(max_func), key) for func, key in pairs + f"{func.rjust(max_func)} : {key}" for func, key in pairs ) def get_session_formatted_for_file(self) -> str: diff --git a/bpython/formatter.py b/bpython/formatter.py index 9fdd43a57..9618979aa 100644 --- a/bpython/formatter.py +++ b/bpython/formatter.py @@ -99,7 +99,7 @@ class BPythonFormatter(Formatter): def __init__(self, color_scheme, **options): self.f_strings = {} for k, v in theme_map.items(): - self.f_strings[k] = "\x01{}".format(color_scheme[v]) + self.f_strings[k] = f"\x01{color_scheme[v]}" if k is Parenthesis: # FIXME: Find a way to make this the inverse of the current # background colour @@ -114,7 +114,7 @@ def format(self, tokensource, outfile): while token not in self.f_strings: token = token.parent - o += "{}\x03{}\x04".format(self.f_strings[token], text) + o += f"{self.f_strings[token]}\x03{text}\x04" outfile.write(o.rstrip()) diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index c5e14cf5d..3992a70fc 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -203,7 +203,7 @@ def safe_getitem(obj, index): return obj[index] except (KeyError, IndexError): raise EvaluationError(f"can't lookup key {index!r} on {obj!r}") - raise ValueError("unsafe to lookup on object of type {}".format(type(obj))) + raise ValueError(f"unsafe to lookup on object of type {type(obj)}") def find_attribute_with_name(node, name): From 5c6aa63a39d5666511dd4b693a0107809894cc7f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 7 Nov 2021 22:11:13 +0100 Subject: [PATCH 276/555] Remove useless statement --- bpython/autocomplete.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 40d655b03..7a65d1b30 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -372,7 +372,6 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return lineparts.current_string(cursor_offset, line) def format(self, filename: str) -> str: - filename.rstrip(os.sep).rsplit(os.sep)[-1] if os.sep in filename[:-1]: return filename[filename.rindex(os.sep, 0, -1) + 1 :] else: From 143e4e55d8f5227149528a5880a32a516a40f14d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 7 Nov 2021 22:12:34 +0100 Subject: [PATCH 277/555] Start development of 0.23 --- CHANGELOG.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 256e5d855..062a711b4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,17 @@ Changelog ========= +0.23 +---- + +General information: + +New features: + +Fixes: + +Support for Python 3.6 has been dropped. + 0.22 ---- From 47c75308298be8099c8f0c4db4d67c2b1bd7c7ec Mon Sep 17 00:00:00 2001 From: Dustin Rodrigues Date: Sun, 7 Nov 2021 23:42:17 -0500 Subject: [PATCH 278/555] add typing-extensions requirement --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 806c01c89..96ef47be1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,7 @@ install_requires = pygments pyxdg requests + typing-extensions [options.extras_require] clipboard = pyperclip From 532dac009bc1c7f64012c7c66c75ffdd4da49275 Mon Sep 17 00:00:00 2001 From: Dustin Rodrigues Date: Sun, 7 Nov 2021 23:42:17 -0500 Subject: [PATCH 279/555] add typing-extensions requirement --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 1efed779c..001fa1334 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ install_requires = pygments pyxdg requests + typing-extensions [options.extras_require] clipboard = pyperclip From 429749f8ee90af60455af099dd0871975ccace71 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 8 Nov 2021 09:54:27 +0100 Subject: [PATCH 280/555] Remove unused import --- bpython/curtsies.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 86d33cf3f..1d41c3b5c 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -24,16 +24,16 @@ from typing import ( Any, + Callable, Dict, + Generator, List, - Callable, - Union, + Optional, Sequence, Tuple, - Optional, - Generator, + Union, ) -from typing_extensions import Literal, Protocol +from typing_extensions import Protocol logger = logging.getLogger(__name__) From 2d6a0b6125494d3be49cf021da7926dae71afe90 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 8 Nov 2021 10:32:35 +0100 Subject: [PATCH 281/555] Add changelog entry for 0.22.1 --- CHANGELOG.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 256e5d855..00244b1c3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog ========= +0.22.1 +------ + +Fixes: + +* #938: Fix missing dependency on typing_extensions. + Thanks to Dustin Rodrigues + 0.22 ---- From 8411460a9cdc75166ca1935e3acb9646c4218109 Mon Sep 17 00:00:00 2001 From: supakeen Date: Mon, 8 Nov 2021 14:02:48 +0100 Subject: [PATCH 282/555] Re-order installation, usage, and OS installation. - Removal of the Windows subsection (these will move to docs). - Rename of "Mac OS" to "macOS". - Remove 'curses' from the title. - Remove 'alternatives' section. --- README.rst | 165 +++++++++++++++++------------------------------------ 1 file changed, 52 insertions(+), 113 deletions(-) diff --git a/README.rst b/README.rst index 976314c55..9eedbd45c 100644 --- a/README.rst +++ b/README.rst @@ -8,9 +8,9 @@ :target: https://github.com/ambv/black -*********************************************************************** -bpython: A fancy curses interface to the Python interactive interpreter -*********************************************************************** +**************************************************************** +bpython: A fancy interface to the Python interactive interpreter +**************************************************************** `bpython`_ is a lightweight Python interpreter that adds several features common to IDEs. These features include **syntax highlighting**, **expected parameter @@ -51,71 +51,6 @@ Start bpython by typing ``bpython`` in your terminal. You can exit bpython by using the ``exit()`` command or by pressing control-D like regular interactive Python. -Installation via OS Package Manager ------------------------------------ - -The majority of desktop computer operating systems come with package management -systems. If you use one of these OSes, you can install ``bpython`` using the -package manager. - -Ubuntu/Debian -~~~~~~~~~~~~~ -Ubuntu/Debian family Linux users can install ``bpython`` using the ``apt`` -package manager, using the command with ``sudo`` privileges: - -.. code-block:: bash - - $ apt install bpython - -In case you are using an older version, run - -.. code-block:: bash - - $ apt-get install bpython - -Arch Linux -~~~~~~~~~~ -Arch Linux uses ``pacman`` as the default package manager; you can use it to install ``bpython``: - -.. code-block:: bash - - $ pacman -S bpython - -Fedora -~~~~~~~~~~ -Fedora users can install ``bpython`` directly from the command line using ``dnf``. - -.. code-block:: bash - - $ dnf install bpython - -Windows -~~~~~~~ -**Caveats:** As ``bpython`` makes use of the ncurses library of \*nix-family operating systems, -bpython on Windows is not officially supported and tested. - -However, you may still use bpython on Windows using a workaround. In brief, you should install -these two packages using ``pip``: - -.. code-block:: bash - - $ pip install bpython windows-curses - -Then you should invoke a program called ``bpython-curses.exe`` instead of ``bpython.exe`` to use bpython: - -.. code-block:: bash - - $ bpython-curses - -Mac OS -~~~~~~ -Like Windows, Mac OS does not include a package manager by default. If you have installed any -third-party package manager like MacPorts, you can install it via - -.. code-block:: bash - - $ sudo port install py-bpython - =================== Features & Examples =================== @@ -172,69 +107,73 @@ bpython-urwid * urwid -========== -Known Bugs -========== -For known bugs please see bpython's `known issues and FAQ`_ page. -====================== -Contact & Contributing -====================== -I hope you find it useful and please feel free to submit any bugs/patches -suggestions to `Robert`_ or place them on the GitHub -`issues tracker`_. +=================================== +Installation via OS Package Manager +=================================== -For any other ways of communicating with bpython users and devs you can find us -at the community page on the `project homepage`_, or in the `community`_. +The majority of desktop computer operating systems come with package management +systems. If you use one of these OSes, you can install ``bpython`` using the +package manager. -Hope to see you there! +Ubuntu/Debian +~~~~~~~~~~~~~ +Ubuntu/Debian family Linux users can install ``bpython`` using the ``apt`` +package manager, using the command with ``sudo`` privileges: -=================== -CLI Windows Support -=================== +.. code-block:: bash -Dependencies ------------- -`Curses`_ Use the appropriate version compiled by Christoph Gohlke. + $ apt install bpython -`pyreadline`_ Use the version in the cheeseshop. +In case you are using an older version, run -Recommended ------------ -Obtain the less program from GnuUtils. This makes the pager work as intended. -It can be obtained from cygwin or GnuWin32 or msys +.. code-block:: bash -Current version is tested with ------------------------------- -* Curses 2.2 -* pyreadline 1.7 + $ apt-get install bpython -Curses Notes ------------- -The curses used has a bug where the colours are displayed incorrectly: +Arch Linux +~~~~~~~~~~ +Arch Linux uses ``pacman`` as the default package manager; you can use it to install ``bpython``: -* red is swapped with blue -* cyan is swapped with yellow +.. code-block:: bash -To correct this I have provided a windows.theme file. + $ pacman -S bpython -This curses implementation has 16 colors (dark and light versions of the -colours) +Fedora +~~~~~~~~~~ +Fedora users can install ``bpython`` directly from the command line using ``dnf``. +.. code-block:: bash + + $ dnf install bpython + +macOS +~~~~~ +macOS does not include a package manager by default. If you have installed any +third-party package manager like MacPorts, you can install it via + +.. code-block:: bash + + $ sudo port install py-bpython -============ -Alternatives -============ -`ptpython`_ +========== +Known Bugs +========== +For known bugs please see bpython's `known issues and FAQ`_ page. + +====================== +Contact & Contributing +====================== +I hope you find it useful and please feel free to submit any bugs/patches +suggestions to `Robert`_ or place them on the GitHub +`issues tracker`_. -`IPython`_ +For any other ways of communicating with bpython users and devs you can find us +at the community page on the `project homepage`_, or in the `community`_. -Feel free to get in touch if you know of any other alternatives that people -may be interested to try. +Hope to see you there! -.. _ptpython: https://github.com/jonathanslenders/ptpython -.. _ipython: https://ipython.org/ .. _homepage: http://www.bpython-interpreter.org .. _full documentation: http://docs.bpython-interpreter.org/ .. _issues tracker: http://github.com/bpython/bpython/issues/ From 58da471da995637e586b5274cad61470617dbf52 Mon Sep 17 00:00:00 2001 From: supakeen Date: Mon, 8 Nov 2021 14:16:15 +0100 Subject: [PATCH 283/555] Change title levels to match tree. --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 9eedbd45c..6cfd5663e 100644 --- a/README.rst +++ b/README.rst @@ -117,7 +117,7 @@ systems. If you use one of these OSes, you can install ``bpython`` using the package manager. Ubuntu/Debian -~~~~~~~~~~~~~ +------------- Ubuntu/Debian family Linux users can install ``bpython`` using the ``apt`` package manager, using the command with ``sudo`` privileges: @@ -132,7 +132,7 @@ In case you are using an older version, run $ apt-get install bpython Arch Linux -~~~~~~~~~~ +---------- Arch Linux uses ``pacman`` as the default package manager; you can use it to install ``bpython``: .. code-block:: bash @@ -140,7 +140,7 @@ Arch Linux uses ``pacman`` as the default package manager; you can use it to ins $ pacman -S bpython Fedora -~~~~~~~~~~ +------ Fedora users can install ``bpython`` directly from the command line using ``dnf``. .. code-block:: bash @@ -148,7 +148,7 @@ Fedora users can install ``bpython`` directly from the command line using ``dnf` $ dnf install bpython macOS -~~~~~ +----- macOS does not include a package manager by default. If you have installed any third-party package manager like MacPorts, you can install it via From b4578ba50bc12cc0e28a7fc0f996e30cf66ca366 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 8 Nov 2021 21:11:14 +0100 Subject: [PATCH 284/555] Remove unused next method next was used in Python 2.x. --- bpython/repl.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 9ffeba328..e1a5429d5 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -265,9 +265,6 @@ def current(self): raise ValueError("No current match.") return self.matches[self.index] - def next(self): - return self.__next__() - def __next__(self): self.index = (self.index + 1) % len(self.matches) return self.matches[self.index] From 3ba6e169f4b6531d6d99619c978eb6174140d336 Mon Sep 17 00:00:00 2001 From: samuelgregorovic <47627184+samuelgregorovic@users.noreply.github.com> Date: Tue, 9 Nov 2021 14:50:21 +0100 Subject: [PATCH 285/555] Brackets auto-close (#934) Auto-close for charcter pairs such as (), [], "", and ''. Not enabled by default, can be enabled in the bpython config file. --- bpython/config.py | 4 + bpython/curtsiesfrontend/manual_readline.py | 15 +- bpython/curtsiesfrontend/repl.py | 109 +++++++++++++- bpython/line.py | 23 +++ bpython/sample-config | 2 + bpython/test/test_brackets_completion.py | 149 ++++++++++++++++++++ 6 files changed, 295 insertions(+), 7 deletions(-) create mode 100644 bpython/test/test_brackets_completion.py diff --git a/bpython/config.py b/bpython/config.py index 0127b0cbc..c6cadf85d 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -156,6 +156,7 @@ class Config: "syntax": True, "tab_length": 4, "unicode_box": True, + "brackets_completion": False, }, "keyboard": { "backspace": "C-h", @@ -362,6 +363,9 @@ def get_key_no_doublebind(command: str) -> str: if self.unicode_box and supports_box_chars() else ("|", "|", "-", "-", "+", "+", "+", "+") ) + self.brackets_completion = config.getboolean( + "general", "brackets_completion" + ) def load_theme( diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py index afeb55531..f95e66c59 100644 --- a/bpython/curtsiesfrontend/manual_readline.py +++ b/bpython/curtsiesfrontend/manual_readline.py @@ -5,9 +5,10 @@ based on http://www.bigsmoke.us/readline/shortcuts""" from ..lazyre import LazyReCompile - import inspect +from ..line import cursor_on_closing_char_pair + INDENT = 4 # TODO Allow user config of keybindings for these actions @@ -244,6 +245,18 @@ def backspace(cursor_offset, line): cursor_offset - to_delete, line[: cursor_offset - to_delete] + line[cursor_offset:], ) + # removes opening bracket along with closing bracket + # if there is nothing between them + # TODO: could not get config value here, works even without -B option + on_closing_char, pair_close = cursor_on_closing_char_pair( + cursor_offset, line + ) + if on_closing_char and pair_close: + return ( + cursor_offset - 1, + line[: cursor_offset - 1] + line[cursor_offset + 1 :], + ) + return (cursor_offset - 1, line[: cursor_offset - 1] + line[cursor_offset:]) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 54b707806..6693c8517 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -46,7 +46,7 @@ Interp, code_finished_will_parse, ) -from .manual_readline import edit_keys +from .manual_readline import edit_keys, cursor_on_closing_char_pair from .parse import parse as bpythonparse, func_for_letter, color_for_letter from .preprocess import preprocess from .. import __version__ @@ -58,6 +58,7 @@ SourceNotFound, ) from ..translations import _ +from ..line import CHARACTER_PAIR_MAP logger = logging.getLogger(__name__) @@ -800,9 +801,74 @@ def process_key_event(self, e: str) -> None: self.incr_search_mode = None elif e in ("",): self.add_normal_character(" ") + elif e in CHARACTER_PAIR_MAP.keys(): + if e in ["'", '"']: + if self.is_closing_quote(e): + self.insert_char_pair_end(e) + else: + self.insert_char_pair_start(e) + else: + self.insert_char_pair_start(e) + elif e in CHARACTER_PAIR_MAP.values(): + self.insert_char_pair_end(e) else: self.add_normal_character(e) + def is_closing_quote(self, e): + char_count = self._current_line.count(e) + if ( + char_count % 2 == 0 + and cursor_on_closing_char_pair( + self._cursor_offset, self._current_line, e + )[0] + ): + return True + return False + + def insert_char_pair_start(self, e): + """Accepts character which is a part of CHARACTER_PAIR_MAP + like brackets and quotes, and appends it to the line with + an appropriate character pair ending. Closing character can only be inserted + when the next character is either a closing character or a space + + e.x. if you type "(" (lparen) , this will insert "()" + into the line + """ + self.add_normal_character(e) + if self.config.brackets_completion: + allowed_chars = ["}", ")", "]", " "] + start_of_line = len(self._current_line) == 1 + end_of_line = len(self._current_line) == self._cursor_offset + can_lookup_next = len(self._current_line) > self._cursor_offset + next_char = ( + None + if not can_lookup_next + else self._current_line[self._cursor_offset] + ) + next_char_allowed = next_char in allowed_chars + if start_of_line or end_of_line or next_char_allowed: + closing_char = CHARACTER_PAIR_MAP[e] + self.add_normal_character(closing_char, narrow_search=False) + self._cursor_offset -= 1 + + def insert_char_pair_end(self, e): + """Accepts character which is a part of CHARACTER_PAIR_MAP + like brackets and quotes, and checks whether it should be + inserted to the line or overwritten + + e.x. if you type ")" (rparen) , and your cursor is directly + above another ")" (rparen) in the cmd, this will just skip + it and move the cursor. + If there is no same character underneath the cursor, the + character will be printed/appended to the line + """ + if self.config.brackets_completion: + if self.cursor_offset < len(self._current_line): + if self._current_line[self.cursor_offset] == e: + self.cursor_offset += 1 + return + self.add_normal_character(e) + def get_last_word(self): previous_word = _last_word(self.rl_history.entry) @@ -903,7 +969,15 @@ def only_whitespace_left_of_cursor(): for unused in range(to_add): self.add_normal_character(" ") return - + # if cursor on closing character from pair, + # moves cursor behind it on tab + # ? should we leave it here as default? + if self.config.brackets_completion: + on_closing_char, _ = cursor_on_closing_char_pair( + self._cursor_offset, self._current_line + ) + if on_closing_char: + self._cursor_offset += 1 # run complete() if we don't already have matches if len(self.matches_iter.matches) == 0: self.list_win_visible = self.complete(tab=True) @@ -915,7 +989,6 @@ def only_whitespace_left_of_cursor(): # using _current_line so we don't trigger a completion reset if not self.matches_iter.matches: self.list_win_visible = self.complete() - elif self.matches_iter.matches: self.current_match = ( back and self.matches_iter.previous() or next(self.matches_iter) @@ -924,6 +997,24 @@ def only_whitespace_left_of_cursor(): self._cursor_offset, self._current_line = cursor_and_line # using _current_line so we don't trigger a completion reset self.list_win_visible = True + if self.config.brackets_completion: + # appends closing char pair if completion is a callable + if self.is_completion_callable(self._current_line): + self._current_line = self.append_closing_character( + self._current_line + ) + + def is_completion_callable(self, completion): + """Checks whether given completion is callable (e.x. function)""" + completion_end = completion[-1] + return completion_end in CHARACTER_PAIR_MAP + + def append_closing_character(self, completion): + """Appends closing character/bracket to the completion""" + completion_end = completion[-1] + if completion_end in CHARACTER_PAIR_MAP: + completion = f"{completion}{CHARACTER_PAIR_MAP[completion_end]}" + return completion def on_control_d(self): if self.current_line == "": @@ -1071,7 +1162,7 @@ def toggle_file_watch(self): ) # Handler Helpers - def add_normal_character(self, char): + def add_normal_character(self, char, narrow_search=True): if len(char) > 1 or is_nop(char): return if self.incr_search_mode: @@ -1087,12 +1178,18 @@ def add_normal_character(self, char): reset_rl_history=False, clear_special_mode=False, ) - self.cursor_offset += 1 + if narrow_search: + self.cursor_offset += 1 + else: + self._cursor_offset += 1 if self.config.cli_trim_prompts and self.current_line.startswith( self.ps1 ): self.current_line = self.current_line[4:] - self.cursor_offset = max(0, self.cursor_offset - 4) + if narrow_search: + self.cursor_offset = max(0, self.cursor_offset - 4) + else: + self._cursor_offset += max(0, self.cursor_offset - 4) def add_to_incremental_search(self, char=None, backspace=False): """Modify the current search term while in incremental search. diff --git a/bpython/line.py b/bpython/line.py index b98302dd2..cbfa682de 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -19,6 +19,7 @@ class LinePart(NamedTuple): _current_word_re = LazyReCompile(r"(? Optional[LinePart]: @@ -287,3 +288,25 @@ def current_expression_attribute( if m.start(1) <= cursor_offset <= m.end(1): return LinePart(m.start(1), m.end(1), m.group(1)) return None + + +def cursor_on_closing_char_pair(cursor_offset, line, ch=None): + """Checks if cursor sits on closing character of a pair + and whether its pair character is directly behind it + """ + on_closing_char, pair_close = False, False + if line is None: + return on_closing_char, pair_close + if cursor_offset < len(line): + cur_char = line[cursor_offset] + if cur_char in CHARACTER_PAIR_MAP.values(): + on_closing_char = True if not ch else cur_char == ch + if cursor_offset > 0: + prev_char = line[cursor_offset - 1] + if ( + on_closing_char + and prev_char in CHARACTER_PAIR_MAP + and CHARACTER_PAIR_MAP[prev_char] == cur_char + ): + pair_close = True if not ch else prev_char == ch + return on_closing_char, pair_close diff --git a/bpython/sample-config b/bpython/sample-config index 287e8a2fc..03127f1b2 100644 --- a/bpython/sample-config +++ b/bpython/sample-config @@ -61,6 +61,8 @@ # Enable autoreload feature by default (default: False). # default_autoreload = False +# Enable autocompletion of brackets and quotes (default: False) +# brackets_completion = False [keyboard] diff --git a/bpython/test/test_brackets_completion.py b/bpython/test/test_brackets_completion.py new file mode 100644 index 000000000..fd9836650 --- /dev/null +++ b/bpython/test/test_brackets_completion.py @@ -0,0 +1,149 @@ +import os + +from bpython.test import FixLanguageTestCase as TestCase, TEST_CONFIG +from bpython.curtsiesfrontend import repl as curtsiesrepl +from bpython import config + + +def setup_config(conf): + config_struct = config.Config(TEST_CONFIG) + for key, value in conf.items(): + if not hasattr(config_struct, key): + raise ValueError(f"{key!r} is not a valid config attribute") + setattr(config_struct, key, value) + return config_struct + + +def create_repl(brackets_enabled=False, **kwargs): + config = setup_config( + {"editor": "true", "brackets_completion": brackets_enabled} + ) + repl = curtsiesrepl.BaseRepl(config, **kwargs) + os.environ["PAGER"] = "true" + os.environ.pop("PYTHONSTARTUP", None) + repl.width = 50 + repl.height = 20 + return repl + + +class TestBracketCompletionEnabled(TestCase): + def setUp(self): + self.repl = create_repl(brackets_enabled=True) + + def process_multiple_events(self, event_list): + for event in event_list: + self.repl.process_event(event) + + def test_start_line(self): + self.repl.process_event("(") + self.assertEqual(self.repl._current_line, "()") + self.assertEqual(self.repl._cursor_offset, 1) + + def test_nested_brackets(self): + self.process_multiple_events(["(", "[", "{"]) + self.assertEqual(self.repl._current_line, """([{}])""") + self.assertEqual(self.repl._cursor_offset, 3) + + def test_quotes(self): + self.process_multiple_events(["(", "'", "x", "", ","]) + self.process_multiple_events(["[", '"', "y", "", "", ""]) + self.assertEqual(self.repl._current_line, """('x',["y"])""") + self.assertEqual(self.repl._cursor_offset, 11) + + def test_bracket_overwrite_closing_char(self): + self.process_multiple_events(["(", "[", "{"]) + self.assertEqual(self.repl._current_line, """([{}])""") + self.assertEqual(self.repl._cursor_offset, 3) + self.process_multiple_events(["}", "]", ")"]) + self.assertEqual(self.repl._current_line, """([{}])""") + self.assertEqual(self.repl._cursor_offset, 6) + + def test_brackets_move_cursor_on_tab(self): + self.process_multiple_events(["(", "[", "{"]) + self.assertEqual(self.repl._current_line, """([{}])""") + self.assertEqual(self.repl._cursor_offset, 3) + self.repl.process_event("") + self.assertEqual(self.repl._current_line, """([{}])""") + self.assertEqual(self.repl._cursor_offset, 4) + self.repl.process_event("") + self.assertEqual(self.repl._current_line, """([{}])""") + self.assertEqual(self.repl._cursor_offset, 5) + self.repl.process_event("") + self.assertEqual(self.repl._current_line, """([{}])""") + self.assertEqual(self.repl._cursor_offset, 6) + + def test_brackets_non_whitespace_following_char(self): + self.repl.current_line = "s = s.connect('localhost', 8080)" + self.repl.cursor_offset = 14 + self.repl.process_event("(") + self.assertEqual( + self.repl._current_line, "s = s.connect(('localhost', 8080)" + ) + self.assertEqual(self.repl._cursor_offset, 15) + + def test_brackets_deletion_on_backspace(self): + self.repl.current_line = "def foo()" + self.repl.cursor_offset = 8 + self.repl.process_event("") + self.assertEqual(self.repl._current_line, "def foo") + self.assertEqual(self.repl.cursor_offset, 7) + + def test_brackets_deletion_on_backspace_nested(self): + self.repl.current_line = '([{""}])' + self.repl.cursor_offset = 4 + self.process_multiple_events( + ["", "", ""] + ) + self.assertEqual(self.repl._current_line, "()") + self.assertEqual(self.repl.cursor_offset, 1) + + +class TestBracketCompletionDisabled(TestCase): + def setUp(self): + self.repl = create_repl(brackets_enabled=False) + + def process_multiple_events(self, event_list): + for event in event_list: + self.repl.process_event(event) + + def test_start_line(self): + self.repl.process_event("(") + self.assertEqual(self.repl._current_line, "(") + self.assertEqual(self.repl._cursor_offset, 1) + + def test_nested_brackets(self): + self.process_multiple_events(["(", "[", "{"]) + self.assertEqual(self.repl._current_line, "([{") + self.assertEqual(self.repl._cursor_offset, 3) + + def test_bracket_overwrite_closing_char(self): + self.process_multiple_events(["(", "[", "{"]) + self.assertEqual(self.repl._current_line, """([{""") + self.assertEqual(self.repl._cursor_offset, 3) + self.process_multiple_events(["}", "]", ")"]) + self.assertEqual(self.repl._current_line, """([{}])""") + self.assertEqual(self.repl._cursor_offset, 6) + + def test_brackets_move_cursor_on_tab(self): + self.process_multiple_events(["(", "[", "{"]) + self.assertEqual(self.repl._current_line, """([{""") + self.assertEqual(self.repl._cursor_offset, 3) + self.repl.process_event("") + self.assertEqual(self.repl._current_line, """([{""") + self.assertEqual(self.repl._cursor_offset, 3) + + def test_brackets_deletion_on_backspace(self): + self.repl.current_line = "def foo()" + self.repl.cursor_offset = 8 + self.repl.process_event("") + self.assertEqual(self.repl._current_line, "def foo") + self.assertEqual(self.repl.cursor_offset, 7) + + def test_brackets_deletion_on_backspace_nested(self): + self.repl.current_line = '([{""}])' + self.repl.cursor_offset = 4 + self.process_multiple_events( + ["", "", ""] + ) + self.assertEqual(self.repl._current_line, "()") + self.assertEqual(self.repl.cursor_offset, 1) From bdd3ab287ac1d2229944000286b689ab48614f6b Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Tue, 9 Nov 2021 11:05:07 -0500 Subject: [PATCH 286/555] Add brackets auto-close to CHANGELOG --- CHANGELOG.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 42043ea0b..b3700a45f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,8 @@ Changelog General information: New features: +* Auto-closing brackets option added. To enable, add `brackets_completion = True` in the bpython config (press F3 to create) + Thanks to samuelgregorovic Fixes: From 7702ebd0a632f1b0e0afdc83598f48446c893b20 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 9 Nov 2021 17:09:29 +0100 Subject: [PATCH 287/555] Follow first parent on merge commits If a bugfix release is merged into main, git describe might use the release tag instead of the X-dev tag. During development, we always want it to use the X-dev tag, however. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 42dd2a309..12d4eeec5 100755 --- a/setup.py +++ b/setup.py @@ -78,7 +78,7 @@ def git_describe_to_python_version(version): try: # get version from git describe proc = subprocess.Popen( - ["git", "describe", "--tags"], + ["git", "describe", "--tags", "--first-parent"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) From adf19b18a5b8814db04c66dfce671957831f2cf9 Mon Sep 17 00:00:00 2001 From: Frostmaine <52504177+Frostmaine@users.noreply.github.com> Date: Tue, 9 Nov 2021 11:16:51 -0500 Subject: [PATCH 288/555] added type references to pager.py (#936) --- bpython/_internal.py | 2 +- bpython/pager.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/bpython/_internal.py b/bpython/_internal.py index 35cd0bf80..bfcfce46f 100644 --- a/bpython/_internal.py +++ b/bpython/_internal.py @@ -4,7 +4,7 @@ from .pager import page # Ugly monkeypatching -pydoc.pager = page +pydoc.pager = page # type: ignore class _Helper: diff --git a/bpython/pager.py b/bpython/pager.py index e481e7936..673e902bc 100644 --- a/bpython/pager.py +++ b/bpython/pager.py @@ -20,6 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True import curses import errno @@ -28,14 +30,15 @@ import subprocess import sys import shlex +from typing import List -def get_pager_command(default="less -rf"): +def get_pager_command(default: str = "less -rf") -> List[str]: command = shlex.split(os.environ.get("PAGER", default)) return command -def page_internal(data): +def page_internal(data: str) -> None: """A more than dumb pager function.""" if hasattr(pydoc, "ttypager"): pydoc.ttypager(data) @@ -43,7 +46,7 @@ def page_internal(data): sys.stdout.write(data) -def page(data, use_internal=False): +def page(data: str, use_internal: bool = False) -> None: command = get_pager_command() if not command or use_internal: page_internal(data) @@ -51,8 +54,9 @@ def page(data, use_internal=False): curses.endwin() try: popen = subprocess.Popen(command, stdin=subprocess.PIPE) - data = data.encode(sys.__stdout__.encoding, "replace") - popen.stdin.write(data) + assert popen.stdin is not None + data_bytes = data.encode(sys.__stdout__.encoding, "replace") + popen.stdin.write(data_bytes) popen.stdin.close() except OSError as e: if e.errno == errno.ENOENT: From 699ced03e8e58c918870ece1af164c10aeb30bb0 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Tue, 9 Nov 2021 11:29:02 -0500 Subject: [PATCH 289/555] Use pypy3.7 in CI 'pypy3' was using pypy3.6 --- .github/workflows/build.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 05e3f65d4..306565fbf 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -10,10 +10,10 @@ on: jobs: build: runs-on: ubuntu-latest - continue-on-error: ${{ matrix.python-version == 'pypy3' }} + continue-on-error: ${{ matrix.python-version == 'pypy3.7' }} strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", pypy3] + python-version: [3.7, 3.8, 3.9, "3.10", pypy3.7] steps: - uses: actions/checkout@v2 with: From a9db8370ffe55c5461b488e311914eaa2b6f2cbc Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Tue, 9 Nov 2021 11:34:40 -0500 Subject: [PATCH 290/555] use pypy-3.7 --- .github/workflows/build.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 306565fbf..62ebac0f7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -10,10 +10,10 @@ on: jobs: build: runs-on: ubuntu-latest - continue-on-error: ${{ matrix.python-version == 'pypy3.7' }} + continue-on-error: ${{ matrix.python-version == 'pypy-3.7' }} strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", pypy3.7] + python-version: [3.7, 3.8, 3.9, "3.10", "pypy-3.7"] steps: - uses: actions/checkout@v2 with: From 83ca33e02fb54640ddd0ff6b956059225b05586c Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 17 Nov 2021 19:36:13 -0500 Subject: [PATCH 291/555] Yield from import module discovery Avoid interruptions by yielding more frequently. This prevent unresponsiveness while working through portions of directory trees without any Python modules. --- bpython/importcompletion.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 00bf99f31..c5dc28f00 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -155,7 +155,9 @@ def complete(self, cursor_offset: int, line: str) -> Optional[Set[str]]: else: return None - def find_modules(self, path: Path) -> Generator[str, None, None]: + def find_modules( + self, path: Path + ) -> Generator[Union[str, None], None, None]: """Find all modules (and packages) for a given directory.""" if not path.is_dir(): # Perhaps a zip file @@ -219,9 +221,12 @@ def find_modules(self, path: Path) -> Generator[str, None, None]: if (stat.st_dev, stat.st_ino) not in self.paths: self.paths.add((stat.st_dev, stat.st_ino)) for subname in self.find_modules(path_real): - if subname != "__init__": + if subname is None: + yield None # take a break to avoid unresponsiveness + elif subname != "__init__": yield f"{name}.{subname}" yield name + yield None # take a break to avoid unresponsiveness def find_all_modules( self, paths: Iterable[Path] @@ -231,7 +236,8 @@ def find_all_modules( for p in paths: for module in self.find_modules(p): - self.modules.add(module) + if module is not None: + self.modules.add(module) yield def find_coroutine(self) -> Optional[bool]: From f3f9e420a9c61b19dbb7aba6274bb43185f8ddd3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 21 Nov 2021 17:38:37 +0100 Subject: [PATCH 292/555] Add brackets_completion documentation to configuration-options --- doc/sphinx/source/configuration-options.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/sphinx/source/configuration-options.rst b/doc/sphinx/source/configuration-options.rst index 4d13cbae5..1521542ed 100644 --- a/doc/sphinx/source/configuration-options.rst +++ b/doc/sphinx/source/configuration-options.rst @@ -22,6 +22,13 @@ characters (default: simple). None disables autocompletion. .. versionadded:: 0.12 +brackets_completion +^^^^^^^^^^^^^^^^^^^ +Whether opening character of the pairs ``()``, ``[]``, ``""``, and ``''`` should be auto-closed +(default: False). + +.. versionadded:: 0.23 + .. _configuration_color_scheme: color_scheme @@ -173,7 +180,7 @@ Soft tab size (default 4, see PEP-8). unicode_box ^^^^^^^^^^^ -Whether to use Unicode characters to draw boxes. +Whether to use Unicode characters to draw boxes (default: True). .. versionadded:: 0.14 From 51ebc86070c7a49abe78ba87a0e8268a09f141a6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 18:18:17 +0100 Subject: [PATCH 293/555] Add a typing compat module to avoid a dependency on typing-extensions for Py >= 3.8 --- bpython/_typing_compat.py | 33 +++++++++++++++++++++++++++ bpython/curtsies.py | 2 +- bpython/curtsiesfrontend/_internal.py | 2 +- bpython/curtsiesfrontend/repl.py | 2 +- bpython/filelock.py | 2 +- bpython/inspection.py | 2 +- bpython/repl.py | 2 +- setup.cfg | 2 +- 8 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 bpython/_typing_compat.py diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py new file mode 100644 index 000000000..31fb64287 --- /dev/null +++ b/bpython/_typing_compat.py @@ -0,0 +1,33 @@ +# The MIT License +# +# Copyright (c) 2021 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.8 + from typing import Literal +except ImportError: + from typing_extensions import Literal # type: ignore + +try: + # introduced in Python 3.8 + from typing import Protocol +except ImportError: + from typing_extensions import Protocol # type: ignore diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 1d41c3b5c..7ffe90109 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -33,7 +33,7 @@ Tuple, Union, ) -from typing_extensions import Protocol +from ._typing_compat import Protocol logger = logging.getLogger(__name__) diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index 633174a88..79c5e974c 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -23,7 +23,7 @@ import pydoc from types import TracebackType from typing import Optional, Type -from typing_extensions import Literal +from .._typing_compat import Literal from .. import _internal diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 6693c8517..892dc5289 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -14,7 +14,7 @@ from enum import Enum from types import TracebackType from typing import Dict, Any, List, Optional, Tuple, Union, cast, Type -from typing_extensions import Literal +from .._typing_compat import Literal import blessings import greenlet diff --git a/bpython/filelock.py b/bpython/filelock.py index 6558fc583..429f708b6 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -21,7 +21,7 @@ # THE SOFTWARE. from typing import Optional, Type, IO -from typing_extensions import Literal +from ._typing_compat import Literal from types import TracebackType has_fcntl = True diff --git a/bpython/inspection.py b/bpython/inspection.py index e7ab0a155..21193ef32 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -28,7 +28,7 @@ from collections import namedtuple from typing import Any, Optional, Type from types import MemberDescriptorType, TracebackType -from typing_extensions import Literal +from ._typing_compat import Literal from pygments.token import Token from pygments.lexers import Python3Lexer diff --git a/bpython/repl.py b/bpython/repl.py index e1a5429d5..a071d8b5c 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -38,7 +38,7 @@ from pathlib import Path from types import ModuleType, TracebackType from typing import cast, Tuple, Any, Optional, Type -from typing_extensions import Literal +from ._typing_compat import Literal from pygments.lexers import Python3Lexer from pygments.token import Token diff --git a/setup.cfg b/setup.cfg index 96ef47be1..f5c1bc849 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ install_requires = pygments pyxdg requests - typing-extensions + typing-extensions; python_version < "3.8" [options.extras_require] clipboard = pyperclip From edb8391f3c48e6ad76cb972362d1032cdb354d82 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 18:54:36 +0100 Subject: [PATCH 294/555] Remove unused function --- bpython/repl.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index a071d8b5c..719baec51 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -126,9 +126,6 @@ def __init__(self, locals=None, encoding=None): super().__init__(locals) self.timer = RuntimeTimer() - def reset_running_time(self): - self.running_time = 0 - def runsource(self, source, filename=None, symbol="single", encode="auto"): """Execute Python code. From 1b3c87eade6d7e75ae37c699bcd99424068cc762 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 18:54:50 +0100 Subject: [PATCH 295/555] Add type annotations for RuntimeTimer --- bpython/repl.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 719baec51..5802d7d7f 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -63,11 +63,11 @@ class RuntimeTimer: """Calculate running time""" - def __init__(self): + def __init__(self) -> None: self.reset_timer() self.time = time.monotonic if hasattr(time, "monotonic") else time.time - def __enter__(self): + def __enter__(self) -> None: self.start = self.time() def __exit__( @@ -80,11 +80,11 @@ def __exit__( self.running_time += self.last_command return False - def reset_timer(self): + def reset_timer(self) -> None: self.running_time = 0.0 self.last_command = 0.0 - def estimate(self): + def estimate(self) -> float: return self.running_time - self.last_command From d0cdeb4926317af49fc791ff5a83efc0382fa242 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 20:24:25 +0100 Subject: [PATCH 296/555] Directly use time.monotonic It's always available starting with Python 3.5. --- bpython/repl.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 5802d7d7f..cbfc987e6 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -65,10 +65,9 @@ class RuntimeTimer: def __init__(self) -> None: self.reset_timer() - self.time = time.monotonic if hasattr(time, "monotonic") else time.time def __enter__(self) -> None: - self.start = self.time() + self.start = time.monotonic() def __exit__( self, @@ -76,7 +75,7 @@ def __exit__( exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: - self.last_command = self.time() - self.start + self.last_command = time.monotonic() - self.start self.running_time += self.last_command return False From a96d6e3483e7743fd8e0453532b93154fe0772f6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 19:20:49 +0100 Subject: [PATCH 297/555] Reset correct members --- bpython/repl.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index cbfc987e6..9a60300cc 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -234,9 +234,9 @@ def __init__(self): # which word is currently replacing the current word self.index = -1 # cursor position in the original line - self.orig_cursor_offset = None + self.orig_cursor_offset = -1 # original line (before match replacements) - self.orig_line = None + self.orig_line = "" # class describing the current type of completion self.completer = None @@ -327,8 +327,8 @@ def update(self, cursor_offset, current_line, matches, completer): def clear(self): self.matches = [] - self.cursor_offset = -1 - self.current_line = "" + self.orig_cursor_offset = -1 + self.orig_line = "" self.current_word = "" self.start = None self.end = None From 9284640d4064c2fcb207e7fc35a5fc4041ba729e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 19:23:41 +0100 Subject: [PATCH 298/555] Add type annotations to MatchesIterator --- bpython/repl.py | 50 +++++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 9a60300cc..689115ffc 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -37,7 +37,7 @@ from itertools import takewhile from pathlib import Path from types import ModuleType, TracebackType -from typing import cast, Tuple, Any, Optional, Type +from typing import cast, List, Tuple, Any, Optional, Type from ._typing_compat import Literal from pygments.lexers import Python3Lexer @@ -226,11 +226,11 @@ class MatchesIterator: A MatchesIterator can be `clear`ed to reset match iteration, and `update`ed to set what matches will be iterated over.""" - def __init__(self): + def __init__(self) -> None: # word being replaced in the original line of text self.current_word = "" # possible replacements for current_word - self.matches = None + self.matches: List[str] = [] # which word is currently replacing the current word self.index = -1 # cursor position in the original line @@ -238,63 +238,67 @@ def __init__(self): # original line (before match replacements) self.orig_line = "" # class describing the current type of completion - self.completer = None + self.completer: Optional[autocomplete.BaseCompletionType] = None - def __nonzero__(self): + def __nonzero__(self) -> bool: """MatchesIterator is False when word hasn't been replaced yet""" return self.index != -1 - def __bool__(self): + def __bool__(self) -> bool: return self.index != -1 @property - def candidate_selected(self): + def candidate_selected(self) -> bool: """True when word selected/replaced, False when word hasn't been replaced yet""" return bool(self) - def __iter__(self): + def __iter__(self) -> "MatchesIterator": return self - def current(self): + def current(self) -> str: if self.index == -1: raise ValueError("No current match.") return self.matches[self.index] - def __next__(self): + def __next__(self) -> str: self.index = (self.index + 1) % len(self.matches) return self.matches[self.index] - def previous(self): + def previous(self) -> str: if self.index <= 0: self.index = len(self.matches) self.index -= 1 return self.matches[self.index] - def cur_line(self): + 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): + def substitute(self, match) -> Tuple[int, str]: """Returns a cursor offset and line with match substituted in""" - start, end, word = self.completer.locate( + assert self.completer is not None + + start, end, _ = self.completer.locate( self.orig_cursor_offset, self.orig_line - ) + ) # type: ignore return ( start + len(match), self.orig_line[:start] + match + self.orig_line[end:], ) - def is_cseq(self): + def is_cseq(self) -> bool: return bool( os.path.commonprefix(self.matches)[len(self.current_word) :] ) - def substitute_cseq(self): + 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 + cseq = os.path.commonprefix(self.matches) new_cursor_offset, new_line = self.substitute(cseq) if len(self.matches) == 1: @@ -307,7 +311,13 @@ def substitute_cseq(self): self.clear() return new_cursor_offset, new_line - def update(self, cursor_offset, current_line, matches, completer): + def update( + self, + cursor_offset: int, + current_line: str, + matches: List[str], + completer: autocomplete.BaseCompletionType, + ) -> None: """Called to reset the match index and update the word being replaced Should only be called if there's a target to update - otherwise, call @@ -323,9 +333,9 @@ def update(self, cursor_offset, current_line, matches, completer): self.index = -1 self.start, self.end, self.current_word = self.completer.locate( self.orig_cursor_offset, self.orig_line - ) + ) # type: ignore - def clear(self): + def clear(self) -> None: self.matches = [] self.orig_cursor_offset = -1 self.orig_line = "" From 8bf810d6142f4f3e30a18b7ca8912d181ae52322 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 20:24:05 +0100 Subject: [PATCH 299/555] Remove unused function --- bpython/repl.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 689115ffc..8cf88962d 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -1178,20 +1178,6 @@ def next_indentation(line, tab_length): return indentation -def next_token_inside_string(code_string, inside_string): - """Given a code string s and an initial state inside_string, return - whether the next token will be inside a string or not.""" - for token, value in Python3Lexer().get_tokens(code_string): - if token is Token.String: - value = value.lstrip("bBrRuU") - if value in ('"""', "'''", '"', "'"): - if not inside_string: - inside_string = value - elif value == inside_string: - inside_string = False - return inside_string - - def split_lines(tokens): for (token, value) in tokens: if not value: From 3c9e35dc6c38e807116b0b1406df0406fd04f5c0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 8 Dec 2021 22:54:12 +0100 Subject: [PATCH 300/555] Add return type for __enter__ --- bpython/inspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 21193ef32..5ebfdbe85 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -58,7 +58,7 @@ class AttrCleaner: def __init__(self, obj: Any) -> None: self.obj = obj - def __enter__(self): + def __enter__(self) -> None: """Try to make an object not exhibit side-effects on attribute lookup.""" type_ = type(self.obj) From fdd4ad960351e5cb7e4b77bd891cecc0d638d199 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 31 Dec 2021 13:59:15 +0100 Subject: [PATCH 301/555] Fix tests with Python 3.10.1 --- bpython/test/test_interpreter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index ca64de77b..1a5eed393 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -35,7 +35,7 @@ def test_syntaxerror(self): i.runsource("1.1.1.1") - if sys.version_info[:2] >= (3, 10): + if (3, 10, 0) <= sys.version_info[:3] < (3, 10, 1): expected = ( " File " + green('""') @@ -47,7 +47,7 @@ def test_syntaxerror(self): + cyan("invalid syntax. Perhaps you forgot a comma?") + "\n" ) - elif (3, 8) <= sys.version_info[:2] <= (3, 9): + elif (3, 8) <= sys.version_info[:2]: expected = ( " File " + green('""') From b46afa3bb1ab783c96dc80c5184090a171ab70d4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 31 Dec 2021 14:00:46 +0100 Subject: [PATCH 302/555] Apply black --- bpython/autocomplete.py | 1 - bpython/urwid.py | 1 - 2 files changed, 2 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 7a65d1b30..71e7fe094 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -626,7 +626,6 @@ def matches( def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return None - else: class JediCompletion(BaseCompletionType): diff --git a/bpython/urwid.py b/bpython/urwid.py index e3aab75c2..33d1f4b8c 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -128,7 +128,6 @@ def wrapper(*args, **kwargs): return wrapper - else: TwistedEventLoop = getattr(urwid, "TwistedEventLoop", None) From 4510162c67e9b54b1258bb4c47bca0797d51ba3f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 31 Dec 2021 14:07:44 +0100 Subject: [PATCH 303/555] Apply black --- bpython/curtsiesfrontend/filewatch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index e3607180c..7616d4845 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -11,7 +11,6 @@ def ModuleChangedEventHandler(*args): return None - else: class ModuleChangedEventHandler(FileSystemEventHandler): # type: ignore [no-redef] From 4d33cc6ef6114fb173452ade774b8995ffc54783 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 31 Dec 2021 14:08:35 +0100 Subject: [PATCH 304/555] Really fix tests with Python 3.10.1 --- bpython/test/test_interpreter.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 1a5eed393..45ffa66d6 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -35,7 +35,19 @@ def test_syntaxerror(self): i.runsource("1.1.1.1") - if (3, 10, 0) <= sys.version_info[:3] < (3, 10, 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('""') From 60baf5eee9c107df5cce2319227fb129ff64bb77 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 8 Jan 2022 20:27:26 +0100 Subject: [PATCH 305/555] Fix call of __exit__ (fixes #948) --- bpython/curtsiesfrontend/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 892dc5289..0d45593fb 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -611,7 +611,7 @@ def sigwinch_handler(self, signum, frame): def sigtstp_handler(self, signum, frame): self.scroll_offset = len(self.lines_for_display) - self.__exit__() + self.__exit__(None, None, None) self.on_suspend() os.kill(os.getpid(), signal.SIGTSTP) self.after_suspend() From 9465700bc899eda0c7d71c7baf47ec1c06414511 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 8 Jan 2022 20:33:56 +0100 Subject: [PATCH 306/555] Simplify is_closing_quote --- bpython/curtsiesfrontend/repl.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 0d45593fb..758c9745d 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -814,16 +814,14 @@ def process_key_event(self, e: str) -> None: else: self.add_normal_character(e) - def is_closing_quote(self, e): + def is_closing_quote(self, e: str) -> bool: char_count = self._current_line.count(e) - if ( + return ( char_count % 2 == 0 and cursor_on_closing_char_pair( self._cursor_offset, self._current_line, e )[0] - ): - return True - return False + ) def insert_char_pair_start(self, e): """Accepts character which is a part of CHARACTER_PAIR_MAP From 30f8dbaa8d38f7b1f3652c21e1cbc6b1ae969f82 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 8 Jan 2022 20:43:03 +0100 Subject: [PATCH 307/555] Check for None --- bpython/line.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index cbfa682de..3572e45da 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -300,7 +300,7 @@ def cursor_on_closing_char_pair(cursor_offset, line, ch=None): if cursor_offset < len(line): cur_char = line[cursor_offset] if cur_char in CHARACTER_PAIR_MAP.values(): - on_closing_char = True if not ch else cur_char == ch + on_closing_char = True if ch is None else cur_char == ch if cursor_offset > 0: prev_char = line[cursor_offset - 1] if ( @@ -308,5 +308,5 @@ def cursor_on_closing_char_pair(cursor_offset, line, ch=None): and prev_char in CHARACTER_PAIR_MAP and CHARACTER_PAIR_MAP[prev_char] == cur_char ): - pair_close = True if not ch else prev_char == ch + pair_close = True if ch is None else prev_char == ch return on_closing_char, pair_close From 562ad3f68a25ed805e81755ef26fcb6507614520 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 8 Jan 2022 20:44:01 +0100 Subject: [PATCH 308/555] Simplify insert_char_pair_start --- bpython/curtsiesfrontend/repl.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 758c9745d..973654218 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -834,7 +834,6 @@ def insert_char_pair_start(self, e): """ self.add_normal_character(e) if self.config.brackets_completion: - allowed_chars = ["}", ")", "]", " "] start_of_line = len(self._current_line) == 1 end_of_line = len(self._current_line) == self._cursor_offset can_lookup_next = len(self._current_line) > self._cursor_offset @@ -843,10 +842,8 @@ def insert_char_pair_start(self, e): if not can_lookup_next else self._current_line[self._cursor_offset] ) - next_char_allowed = next_char in allowed_chars - if start_of_line or end_of_line or next_char_allowed: - closing_char = CHARACTER_PAIR_MAP[e] - self.add_normal_character(closing_char, narrow_search=False) + if start_of_line or end_of_line or next_char in "})] ": + self.add_normal_character(CHARACTER_PAIR_MAP[e], narrow_search=False) self._cursor_offset -= 1 def insert_char_pair_end(self, e): From 94bda61004598cea8b988ecc4067c494f8901c69 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 9 Jan 2022 00:19:29 +0100 Subject: [PATCH 309/555] Apply black --- bpython/curtsiesfrontend/repl.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 973654218..018904527 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -816,7 +816,7 @@ def process_key_event(self, e: str) -> None: def is_closing_quote(self, e: str) -> bool: char_count = self._current_line.count(e) - return ( + return ( char_count % 2 == 0 and cursor_on_closing_char_pair( self._cursor_offset, self._current_line, e @@ -843,7 +843,9 @@ def insert_char_pair_start(self, e): else self._current_line[self._cursor_offset] ) if start_of_line or end_of_line or next_char in "})] ": - self.add_normal_character(CHARACTER_PAIR_MAP[e], narrow_search=False) + self.add_normal_character( + CHARACTER_PAIR_MAP[e], narrow_search=False + ) self._cursor_offset -= 1 def insert_char_pair_end(self, e): From 189da3ecbaa30212b8ba73aeb321b6a6a324348b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 9 Jan 2022 00:22:52 +0100 Subject: [PATCH 310/555] Add type annotations --- bpython/line.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index 3572e45da..ee964a088 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -7,7 +7,7 @@ import re from itertools import chain -from typing import Optional, NamedTuple +from typing import Optional, NamedTuple, Tuple from .lazyre import LazyReCompile @@ -290,7 +290,9 @@ def current_expression_attribute( return None -def cursor_on_closing_char_pair(cursor_offset, line, ch=None): +def cursor_on_closing_char_pair( + cursor_offset: int, line: str, ch: Optional[str] = None +) -> Tuple[bool, bool]: """Checks if cursor sits on closing character of a pair and whether its pair character is directly behind it """ From 95539ccdf8ccc1b614a62a757044c08a308a5375 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 9 Jan 2022 16:11:05 +0100 Subject: [PATCH 311/555] Avoid indexing and tuple unpacking of LinePart instances --- bpython/autocomplete.py | 2 +- bpython/importcompletion.py | 12 +++++----- bpython/line.py | 23 ++++++++---------- bpython/repl.py | 21 +++++++++------- bpython/test/test_autocomplete.py | 5 +++- bpython/test/test_line_properties.py | 36 ++++++++++++++++++---------- bpython/test/test_repl.py | 12 ++++++---- 7 files changed, 64 insertions(+), 47 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 71e7fe094..bb8a8b0c7 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -481,7 +481,7 @@ def matches( if current_dict_parts is None: return None - _, _, dexpr = current_dict_parts + dexpr = current_dict_parts.word try: obj = safe_eval(dexpr, locals_) except EvaluationError: diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index c5dc28f00..969361447 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -135,22 +135,22 @@ def complete(self, cursor_offset: int, line: str) -> Optional[Set[str]]: if import_import is not None: # `from a import ` completion matches = self.module_matches( - import_import[2], from_import_from[2] + import_import.word, from_import_from.word ) matches.update( - self.attr_matches(import_import[2], from_import_from[2]) + self.attr_matches(import_import.word, from_import_from.word) ) else: # `from ` completion - matches = self.module_attr_matches(from_import_from[2]) - matches.update(self.module_matches(from_import_from[2])) + matches = self.module_attr_matches(from_import_from.word) + matches.update(self.module_matches(from_import_from.word)) return matches cur_import = current_import(cursor_offset, line) if cur_import is not None: # `import ` completion - matches = self.module_matches(cur_import[2]) - matches.update(self.module_attr_matches(cur_import[2])) + matches = self.module_matches(cur_import.word) + matches.update(self.module_attr_matches(cur_import.word)) return matches else: return None diff --git a/bpython/line.py b/bpython/line.py index ee964a088..99615efe5 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -130,15 +130,14 @@ def current_object(cursor_offset: int, line: str) -> Optional[LinePart]: match = current_word(cursor_offset, line) if match is None: return None - start, end, word = match s = ".".join( m.group(1) - for m in _current_object_re.finditer(word) - if m.end(1) + start < cursor_offset + for m in _current_object_re.finditer(match.word) + if m.end(1) + match.start < cursor_offset ) if not s: return None - return LinePart(start, start + len(s), s) + return LinePart(match.start, match.start + len(s), s) _current_object_attribute_re = LazyReCompile(r"([\w_][\w0-9_]*)[.]?") @@ -152,12 +151,13 @@ def current_object_attribute( match = current_word(cursor_offset, line) if match is None: return None - start, end, word = match - matches = _current_object_attribute_re.finditer(word) + matches = _current_object_attribute_re.finditer(match.word) next(matches) for m in matches: - if m.start(1) + start <= cursor_offset <= m.end(1) + start: - return LinePart(m.start(1) + start, m.end(1) + start, m.group(1)) + if m.start(1) + match.start <= cursor_offset <= m.end(1) + match.start: + return LinePart( + m.start(1) + match.start, m.end(1) + match.start, m.group(1) + ) return None @@ -266,11 +266,8 @@ def current_dotted_attribute( ) -> Optional[LinePart]: """The dotted attribute-object pair before the cursor""" match = current_word(cursor_offset, line) - if match is None: - return None - start, end, word = match - if "." in word[1:]: - return LinePart(start, end, word) + if match is not None and "." in match.word[1:]: + return match return None diff --git a/bpython/repl.py b/bpython/repl.py index 8cf88962d..4ee4eebae 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -239,6 +239,8 @@ def __init__(self) -> None: 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 def __nonzero__(self) -> bool: """MatchesIterator is False when word hasn't been replaced yet""" @@ -277,16 +279,15 @@ def cur_line(self) -> Tuple[int, str]: made""" return self.substitute(self.current()) - def substitute(self, match) -> 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 - start, end, _ = self.completer.locate( - self.orig_cursor_offset, self.orig_line - ) # type: ignore + lp = self.completer.locate(self.orig_cursor_offset, self.orig_line) + assert lp is not None return ( - start + len(match), - self.orig_line[:start] + match + self.orig_line[end:], + lp.start + len(match), + self.orig_line[: lp.start] + match + self.orig_line[lp.stop :], ) def is_cseq(self) -> bool: @@ -331,9 +332,11 @@ def update( self.matches = matches self.completer = completer self.index = -1 - self.start, self.end, self.current_word = self.completer.locate( - self.orig_cursor_offset, self.orig_line - ) # type: ignore + lp = self.completer.locate(self.orig_cursor_offset, self.orig_line) + assert lp is not None + self.start = lp.start + self.end = lp.stop + self.current_word = lp.word def clear(self) -> None: self.matches = [] diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index ad991abe4..8b171d035 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -12,6 +12,7 @@ has_jedi = False from bpython import autocomplete +from bpython.line import LinePart glob_function = "glob.iglob" @@ -114,7 +115,9 @@ def test_locate_fails_when_not_in_string(self): self.assertEqual(self.completer.locate(4, "abcd"), None) def test_locate_succeeds_when_in_string(self): - self.assertEqual(self.completer.locate(4, "a'bc'd"), (2, 4, "bc")) + self.assertEqual( + self.completer.locate(4, "a'bc'd"), LinePart(2, 4, "bc") + ) def test_issue_491(self): self.assertNotEqual(self.completer.matches(9, '"a[a.l-1]'), None) diff --git a/bpython/test/test_line_properties.py b/bpython/test/test_line_properties.py index 592a61765..967ecbe01 100644 --- a/bpython/test/test_line_properties.py +++ b/bpython/test/test_line_properties.py @@ -1,7 +1,9 @@ import re +from typing import Optional, Tuple import unittest from bpython.line import ( + LinePart, current_word, current_dict_key, current_dict, @@ -25,7 +27,7 @@ def cursor(s): return cursor_offset, line -def decode(s): +def decode(s: str) -> Tuple[Tuple[int, str], Optional[LinePart]]: """'ad' -> ((3, 'abcd'), (1, 3, 'bdc'))""" if not s.count("|") == 1: @@ -41,16 +43,16 @@ def decode(s): assert len(d) in [1, 3], "need all the parts just once! %r" % d if "<" in d: - return (d["|"], s), (d["<"], d[">"], s[d["<"] : d[">"]]) + return (d["|"], s), LinePart(d["<"], d[">"], s[d["<"] : d[">"]]) else: return (d["|"], s), None -def line_with_cursor(cursor_offset, line): +def line_with_cursor(cursor_offset: int, line: str) -> str: return line[:cursor_offset] + "|" + line[cursor_offset:] -def encode(cursor_offset, line, result): +def encode(cursor_offset: int, line: str, result: Optional[LinePart]) -> str: """encode(3, 'abdcd', (1, 3, 'bdc')) -> ad' Written for prettier assert error messages @@ -58,7 +60,9 @@ def encode(cursor_offset, line, result): encoded_line = line_with_cursor(cursor_offset, line) if result is None: return encoded_line - start, end, value = result + start = result.start + end = result.stop + value = result.word assert line[start:end] == value if start < cursor_offset: encoded_line = encoded_line[:start] + "<" + encoded_line[start:] @@ -107,19 +111,25 @@ def test_I(self): self.assertEqual(cursor("asd|fgh"), (3, "asdfgh")) def test_decode(self): - self.assertEqual(decode("ad"), ((3, "abdcd"), (1, 4, "bdc"))) - self.assertEqual(decode("a|d"), ((1, "abdcd"), (1, 4, "bdc"))) - self.assertEqual(decode("ad|"), ((5, "abdcd"), (1, 4, "bdc"))) + self.assertEqual( + decode("ad"), ((3, "abdcd"), LinePart(1, 4, "bdc")) + ) + self.assertEqual( + decode("a|d"), ((1, "abdcd"), LinePart(1, 4, "bdc")) + ) + self.assertEqual( + decode("ad|"), ((5, "abdcd"), LinePart(1, 4, "bdc")) + ) def test_encode(self): - self.assertEqual(encode(3, "abdcd", (1, 4, "bdc")), "ad") - self.assertEqual(encode(1, "abdcd", (1, 4, "bdc")), "a|d") - self.assertEqual(encode(4, "abdcd", (1, 4, "bdc")), "ad") - self.assertEqual(encode(5, "abdcd", (1, 4, "bdc")), "ad|") + self.assertEqual(encode(3, "abdcd", LinePart(1, 4, "bdc")), "ad") + self.assertEqual(encode(1, "abdcd", LinePart(1, 4, "bdc")), "a|d") + self.assertEqual(encode(4, "abdcd", LinePart(1, 4, "bdc")), "ad") + self.assertEqual(encode(5, "abdcd", LinePart(1, 4, "bdc")), "ad|") def test_assert_access(self): def dumb_func(cursor_offset, line): - return (0, 2, "ab") + return LinePart(0, 2, "ab") self.func = dumb_func self.assertAccess("d") diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index e29c5a4e5..a4241087d 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -10,8 +10,12 @@ from unittest import mock from bpython import config, repl, cli, autocomplete -from bpython.test import MagicIterMock, FixLanguageTestCase as TestCase -from bpython.test import TEST_CONFIG +from bpython.line import LinePart +from bpython.test import ( + MagicIterMock, + FixLanguageTestCase as TestCase, + TEST_CONFIG, +) pypy = "PyPy" in sys.version @@ -99,7 +103,7 @@ def test_update(self): newmatches = ["string", "str", "set"] completer = mock.Mock() - completer.locate.return_value = (0, 1, "s") + completer.locate.return_value = LinePart(0, 1, "s") self.matches_iterator.update(1, "s", newmatches, completer) newslice = islice(newmatches, 0, 3) @@ -108,7 +112,7 @@ def test_update(self): def test_cur_line(self): completer = mock.Mock() - completer.locate.return_value = ( + completer.locate.return_value = LinePart( 0, self.matches_iterator.orig_cursor_offset, self.matches_iterator.orig_line, From 809003c24bfe0577a936670355e3856388eb77b4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 9 Jan 2022 15:28:33 +0100 Subject: [PATCH 312/555] Replace NamedTuple with dataclass --- bpython/line.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/line.py b/bpython/line.py index 99615efe5..cbc3bf37e 100644 --- a/bpython/line.py +++ b/bpython/line.py @@ -6,13 +6,15 @@ import re +from dataclasses import dataclass from itertools import chain -from typing import Optional, NamedTuple, Tuple +from typing import Optional, Tuple from .lazyre import LazyReCompile -class LinePart(NamedTuple): +@dataclass +class LinePart: start: int stop: int word: str From a60ae154152c4199336be70a53df0091a9061832 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 9 Jan 2022 23:22:38 +0100 Subject: [PATCH 313/555] Fix type annotation --- bpython/autocomplete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index bb8a8b0c7..916a62291 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -63,7 +63,7 @@ class AutocompleteModes(Enum): FUZZY = "fuzzy" @classmethod - def from_string(cls, value: str) -> Optional[Any]: + def from_string(cls, value: str) -> Optional["AutocompleteModes"]: if value.upper() in cls.__members__: return cls.__members__[value.upper()] return None From 5629bbe2225976defd628b4063fe61ec8595909b Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Mon, 3 Jan 2022 22:11:08 -0600 Subject: [PATCH 314/555] Adding type hints to formatter.py --- bpython/formatter.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/bpython/formatter.py b/bpython/formatter.py index 9618979aa..f216f213f 100644 --- a/bpython/formatter.py +++ b/bpython/formatter.py @@ -24,9 +24,14 @@ # Pygments really kicks ass, it made it really easy to # get the exact behaviour I wanted, thanks Pygments.:) +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True + +from typing import Any, MutableMapping, Iterable, TextIO from pygments.formatter import Formatter from pygments.token import ( + _TokenType, Keyword, Name, Comment, @@ -96,7 +101,9 @@ class BPythonFormatter(Formatter): See the Pygments source for more info; it's pretty straightforward.""" - def __init__(self, color_scheme, **options): + def __init__( + self, color_scheme: MutableMapping[str, str], **options: Any + ) -> None: self.f_strings = {} for k, v in theme_map.items(): self.f_strings[k] = f"\x01{color_scheme[v]}" @@ -106,14 +113,21 @@ def __init__(self, color_scheme, **options): self.f_strings[k] += "I" super().__init__(**options) - def format(self, tokensource, outfile): - o = "" + def format( + self, + tokensource: Iterable[MutableMapping[_TokenType, str]], + outfile: TextIO, + ) -> None: + o: str = "" for token, text in tokensource: if text == "\n": continue while token not in self.f_strings: - token = token.parent + if token.parent is None: + break + else: + token = token.parent o += f"{self.f_strings[token]}\x03{text}\x04" outfile.write(o.rstrip()) From f8ce9162bfccc20b978536c581192984cf43f87f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 2 Feb 2022 14:41:22 +0100 Subject: [PATCH 315/555] Mark optional arguments as optional --- bpython/curtsiesfrontend/interpreter.py | 8 ++++++-- bpython/curtsiesfrontend/repl.py | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 48ee15bae..91dba96ae 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -1,5 +1,5 @@ import sys -from typing import Any, Dict +from typing import Any, Dict, Optional from pygments.token import Generic, Token, Keyword, Name, Comment, String from pygments.token import Error, Literal, Number, Operator, Punctuation @@ -60,7 +60,11 @@ def format(self, tokensource, outfile): class Interp(ReplInterpreter): - def __init__(self, locals: Dict[str, Any] = None, encoding=None): + def __init__( + self, + locals: Optional[Dict[str, Any]] = None, + encoding: Optional[str] = None, + ) -> None: """Constructor. We include an argument for the outfile to pass to the formatter for it diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 018904527..59dcd481d 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -312,10 +312,10 @@ class BaseRepl(Repl): def __init__( self, config: Config, - locals_: Dict[str, Any] = None, - banner: str = None, - interp: code.InteractiveInterpreter = None, - orig_tcattrs: List[Any] = None, + locals_: Optional[Dict[str, Any]] = None, + banner: Optional[str] = None, + interp: Optional[code.InteractiveInterpreter] = None, + orig_tcattrs: Optional[List[Any]] = None, ): """ locals_ is a mapping of locals to pass into the interpreter From 6005b72b1065f71290008b6e880c56e7256ce0f2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 2 Feb 2022 14:54:49 +0100 Subject: [PATCH 316/555] Replace a loop with an rfind --- bpython/autocomplete.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 916a62291..81cea8945 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -398,13 +398,10 @@ def matches( assert "." in r.word - for i in range(1, len(r.word) + 1): - if r.word[-i] == "[": - i -= 1 - break - methodtext = r.word[-i:] + i = r.word.rfind("[") + 1 + methodtext = r.word[i:] matches = { - "".join([r.word[:-i], m]) + "".join([r.word[:i], m]) for m in self.attr_matches(methodtext, locals_) } From 2dd27f9eacf893042fee0a6789c10241460e3915 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 3 Feb 2022 23:03:03 +0100 Subject: [PATCH 317/555] Add type check instead of using exceptions --- bpython/patch_linecache.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index e1d94a157..f947f7c4f 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -53,10 +53,10 @@ def __delitem__(self, key): return super().__delitem__(key) -def _bpython_clear_linecache(): - try: +def _bpython_clear_linecache() -> None: + if isinstance(linecache.cache, BPythonLinecache): bpython_history = linecache.cache.bpython_history - except AttributeError: + else: bpython_history = [] linecache.cache = BPythonLinecache() linecache.cache.bpython_history = bpython_history @@ -68,12 +68,12 @@ def _bpython_clear_linecache(): linecache.clearcache = _bpython_clear_linecache -def filename_for_console_input(code_string): +def filename_for_console_input(code_string: str) -> str: """Remembers a string of source code, and returns a fake filename to use to retrieve it later.""" - try: + if isinstance(linecache.cache, BPythonLinecache): return linecache.cache.remember_bpython_input(code_string) - except AttributeError: + else: # If someone else has patched linecache.cache, better for code to # simply be unavailable to inspect.getsource() than to raise # an exception. From 9984c2391f1621c280dd32361f8ea0f712cee8b6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 3 Feb 2022 23:03:15 +0100 Subject: [PATCH 318/555] Add type annotations for line cache --- bpython/patch_linecache.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index f947f7c4f..4a89f23a9 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -1,22 +1,23 @@ import linecache +from typing import Any, List, Tuple class BPythonLinecache(dict): """Replaces the cache dict in the standard-library linecache module, to also remember (in an unerasable way) bpython console input.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.bpython_history = [] - def is_bpython_filename(self, fname): + def is_bpython_filename(self, fname: Any) -> bool: try: return fname.startswith(" Tuple[int, None, List[str], str]: """Given a filename provided by remember_bpython_input, returns the associated source string.""" try: @@ -25,21 +26,21 @@ def get_bpython_history(self, key): except (IndexError, ValueError): raise KeyError - def remember_bpython_input(self, source): + def remember_bpython_input(self, source: str) -> str: """Remembers a string of source code, and returns a fake filename to use to retrieve it later.""" - filename = "" % len(self.bpython_history) + filename = f"" self.bpython_history.append( (len(source), None, source.splitlines(True), filename) ) return filename - def __getitem__(self, key): + def __getitem__(self, key: Any) -> Any: if self.is_bpython_filename(key): return self.get_bpython_history(key) return super().__getitem__(key) - def __contains__(self, key): + def __contains__(self, key: Any) -> bool: if self.is_bpython_filename(key): try: self.get_bpython_history(key) @@ -48,9 +49,9 @@ def __contains__(self, key): return False return super().__contains__(key) - def __delitem__(self, key): + def __delitem__(self, key: Any) -> None: if not self.is_bpython_filename(key): - return super().__delitem__(key) + super().__delitem__(key) def _bpython_clear_linecache() -> None: From 92c3d1f6ff64b09ba1a161ea75e8738b4286de8c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 3 Feb 2022 23:20:16 +0100 Subject: [PATCH 319/555] Fix mypy regression --- bpython/patch_linecache.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index 4a89f23a9..82b38dd74 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -8,12 +8,12 @@ class BPythonLinecache(dict): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self.bpython_history = [] + self.bpython_history: List[Tuple[int, None, List[str], str]] = [] def is_bpython_filename(self, fname: Any) -> bool: - try: + if isinstance(fname, str): return fname.startswith(" Date: Mon, 25 Apr 2022 22:59:29 +0200 Subject: [PATCH 320/555] Avoid a potential import of fcntl --- bpython/config.py | 10 +++++++--- bpython/curtsiesfrontend/parse.py | 3 ++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/bpython/config.py b/bpython/config.py index c6cadf85d..29b906ddb 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -1,7 +1,7 @@ # The MIT License # # Copyright (c) 2009-2015 the bpython authors. -# Copyright (c) 2015-2020 Sebastian Ramacher +# Copyright (c) 2015-2022 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 @@ -35,9 +35,13 @@ from xdg import BaseDirectory from .autocomplete import AutocompleteModes -from .curtsiesfrontend.parse import CNAMES default_completion = AutocompleteModes.SIMPLE +# All supported letters for colors for themes +# +# Instead of importing it from .curtsiesfrontend.parse, we define them here to +# avoid a potential import of fcntl on Windows. +COLOR_LETTERS = tuple("krgybmcwd") class UnknownColorCode(Exception): @@ -381,7 +385,7 @@ def load_theme( colors[k] = theme.get("syntax", k) else: colors[k] = theme.get("interface", k) - if colors[k].lower() not in CNAMES: + if colors[k].lower() not in COLOR_LETTERS: raise UnknownColorCode(k, colors[k]) # Check against default theme to see if all values are defined diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 6a42b3764..d10a0f5c2 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -8,11 +8,12 @@ ) from functools import partial +from ..config import COLOR_LETTERS from ..lazyre import LazyReCompile COLORS = CURTSIES_COLORS + ("default",) -CNAMES = dict(zip("krgybmcwd", COLORS)) +CNAMES = dict(zip(COLOR_LETTERS, COLORS)) # hack for finding the "inverse" INVERSE_COLORS = { CURTSIES_COLORS[idx]: CURTSIES_COLORS[ From 8c4fd2304da4f4858ce87621d80aebb8c0104072 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Apr 2022 23:11:50 +0200 Subject: [PATCH 321/555] Add type annotations --- bpython/curtsiesfrontend/parse.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index d10a0f5c2..13a200ec5 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -1,4 +1,6 @@ import re +from functools import partial +from typing import Any, Callable, Dict, Tuple from curtsies.formatstring import fmtstr, FmtStr from curtsies.termformatconstants import ( @@ -6,7 +8,6 @@ BG_COLORS, colors as CURTSIES_COLORS, ) -from functools import partial from ..config import COLOR_LETTERS from ..lazyre import LazyReCompile @@ -24,7 +25,9 @@ INVERSE_COLORS["default"] = INVERSE_COLORS[CURTSIES_COLORS[0]] -def func_for_letter(letter_color_code: str, default: str = "k"): +def func_for_letter( + letter_color_code: str, default: str = "k" +) -> Callable[..., FmtStr]: """Returns FmtStr constructor for a bpython-style color code""" if letter_color_code == "d": letter_color_code = default @@ -37,13 +40,13 @@ def func_for_letter(letter_color_code: str, default: str = "k"): ) -def color_for_letter(letter_color_code: str, default: str = "k"): +def color_for_letter(letter_color_code: str, default: str = "k") -> str: if letter_color_code == "d": letter_color_code = default return CNAMES[letter_color_code.lower()] -def parse(s): +def parse(s: str) -> FmtStr: """Returns a FmtStr object from a bpython-formatted colored string""" rest = s stuff = [] @@ -59,7 +62,7 @@ def parse(s): ) -def fs_from_match(d): +def fs_from_match(d: Dict[str, Any]) -> FmtStr: atts = {} if d["fg"]: # this isn't according to spec as I understand it @@ -97,7 +100,7 @@ def fs_from_match(d): ) -def peel_off_string(s): +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() From f38ced3cb299b43430e3be21d065cb7c6626d648 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Apr 2022 23:12:01 +0200 Subject: [PATCH 322/555] Simplify loop condition --- bpython/curtsiesfrontend/parse.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 13a200ec5..8eb1fddd1 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -50,9 +50,7 @@ def parse(s: str) -> FmtStr: """Returns a FmtStr object from a bpython-formatted colored string""" rest = s stuff = [] - while True: - if not rest: - break + while rest: start, rest = peel_off_string(rest) stuff.append(start) return ( From 4c5e18b7c244cad8e21600fae61d6faa3a8ef54b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Apr 2022 23:12:26 +0200 Subject: [PATCH 323/555] Apply black --- 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 03e9a3b8e..ee3f20857 100644 --- a/bpython/test/test_preprocess.py +++ b/bpython/test/test_preprocess.py @@ -15,7 +15,7 @@ def get_fodder_source(test_name): - pattern = fr"#StartTest-{test_name}\n(.*?)#EndTest" + pattern = rf"#StartTest-{test_name}\n(.*?)#EndTest" orig, xformed = [ re.search(pattern, inspect.getsource(module), re.DOTALL) for module in [original, processed] From ace2d066a1e70a710807740739ab325cbe39212c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Apr 2022 23:19:16 +0200 Subject: [PATCH 324/555] Ensure that color is always initialized --- bpython/curtsiesfrontend/parse.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bpython/curtsiesfrontend/parse.py b/bpython/curtsiesfrontend/parse.py index 8eb1fddd1..88a149a65 100644 --- a/bpython/curtsiesfrontend/parse.py +++ b/bpython/curtsiesfrontend/parse.py @@ -62,6 +62,7 @@ def parse(s: str) -> FmtStr: def fs_from_match(d: Dict[str, Any]) -> FmtStr: atts = {} + color = "default" if d["fg"]: # this isn't according to spec as I understand it if d["fg"].isupper(): From 2784b2f781becb8e36531377b851ed6148936e3f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Apr 2022 23:27:06 +0200 Subject: [PATCH 325/555] Fix type annotations --- bpython/args.py | 4 ++-- bpython/simpleeval.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 1ab61d260..04cb8fc3e 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -77,9 +77,9 @@ def copyright_banner() -> str: def parse( args: Optional[List[str]], - extras: Options = None, + extras: Optional[Options] = None, ignore_stdin: bool = False, -) -> Tuple: +) -> 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) diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 3992a70fc..193a69899 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -28,7 +28,7 @@ import ast import sys import builtins -from typing import Dict, Any +from typing import Dict, Any, Optional from . import line as line_properties from .inspection import getattr_safe @@ -216,7 +216,7 @@ def find_attribute_with_name(node, name): def evaluate_current_expression( - cursor_offset: int, line: str, namespace: Dict[str, Any] = None + cursor_offset: int, line: str, namespace: Optional[Dict[str, Any]] = None ): """ Return evaluated expression to the right of the dot of current attribute. From ee7e295477357abce1f730c6b05e0787a7864313 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 25 Apr 2022 23:53:11 +0200 Subject: [PATCH 326/555] Fix type --- 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 45ffa66d6..4c18f8fda 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -130,7 +130,7 @@ def gfunc(): self.assertEqual(plain("").join(a), expected) def test_runsource_bytes_over_128_syntax_error_py3(self): - i = interpreter.Interp(encoding=b"latin-1") + i = interpreter.Interp(encoding="latin-1") i.showsyntaxerror = mock.Mock(return_value=None) i.runsource("a = b'\xfe'") From 10c8b37077d825ce8c4f40b2bd2f8bb3647cd2f3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 26 Apr 2022 23:40:48 +0200 Subject: [PATCH 327/555] Add type annotations --- bpython/paste.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/bpython/paste.py b/bpython/paste.py index 4d118cfc2..9daca01ad 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -1,6 +1,6 @@ # The MIT License # -# Copyright (c) 2014-2020 Sebastian Ramacher +# Copyright (c) 2014-2022 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 @@ -21,13 +21,14 @@ # THE SOFTWARE. import errno -import requests import subprocess -import unicodedata - -from locale import getpreferredencoding +from typing import Tuple from urllib.parse import urljoin, urlparse +import requests +import unicodedata + +from .config import getpreferredencoding from .translations import _ @@ -36,11 +37,11 @@ class PasteFailed(Exception): class PastePinnwand: - def __init__(self, url, expiry): + def __init__(self, url: str, expiry: str) -> None: self.url = url self.expiry = expiry - def paste(self, s): + def paste(self, s: str) -> Tuple[str, str]: """Upload to pastebin via json interface.""" url = urljoin(self.url, "/api/v1/paste") @@ -64,10 +65,10 @@ def paste(self, s): class PasteHelper: - def __init__(self, executable): + def __init__(self, executable: str) -> None: self.executable = executable - def paste(self, s): + def paste(self, s: str) -> Tuple[str, None]: """Call out to helper program for pastebin upload.""" try: @@ -77,6 +78,7 @@ def paste(self, s): stdin=subprocess.PIPE, stdout=subprocess.PIPE, ) + assert helper.stdin is not None helper.stdin.write(s.encode(getpreferredencoding())) output = helper.communicate()[0].decode(getpreferredencoding()) paste_url = output.split()[0] @@ -89,8 +91,8 @@ def paste(self, s): if helper.returncode != 0: raise PasteFailed( _( - "Helper program returned non-zero exit " - "status %d." % (helper.returncode,) + "Helper program returned non-zero exit status %d." + % (helper.returncode,) ) ) From 8aef6a7f2474cf82f015d065dfda7d4b3ecf1427 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 26 Apr 2022 23:40:56 +0200 Subject: [PATCH 328/555] Fix exception handling --- bpython/paste.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/paste.py b/bpython/paste.py index 9daca01ad..ebea9e28e 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -54,7 +54,7 @@ def paste(self, s: str) -> Tuple[str, str]: response = requests.post(url, json=payload, verify=True) response.raise_for_status() except requests.exceptions.RequestException as exc: - raise PasteFailed(exc.message) + raise PasteFailed(str(exc)) data = response.json() From 229961238d6cb783067eae3c7bd07f68a4b49927 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 26 Apr 2022 23:50:08 +0200 Subject: [PATCH 329/555] Cache encoding --- bpython/paste.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bpython/paste.py b/bpython/paste.py index ebea9e28e..126f5a43a 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -79,8 +79,9 @@ def paste(self, s: str) -> Tuple[str, None]: stdout=subprocess.PIPE, ) assert helper.stdin is not None - helper.stdin.write(s.encode(getpreferredencoding())) - output = helper.communicate()[0].decode(getpreferredencoding()) + encoding = getpreferredencoding() + helper.stdin.write(s.encode(encoding)) + output = helper.communicate()[0].decode(encoding) paste_url = output.split()[0] except OSError as e: if e.errno == errno.ENOENT: From deb64165ad23b99dc1c6f54d63fefed41fb69d48 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 26 Apr 2022 23:50:23 +0200 Subject: [PATCH 330/555] Define a protocol for paster implementations --- bpython/paste.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bpython/paste.py b/bpython/paste.py index 126f5a43a..fd140a0ec 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -22,7 +22,7 @@ import errno import subprocess -from typing import Tuple +from typing import Optional, Tuple, Protocol from urllib.parse import urljoin, urlparse import requests @@ -36,6 +36,11 @@ class PasteFailed(Exception): pass +class Paster(Protocol): + def paste(self, s: str) -> Tuple[str, Optional[str]]: + ... + + class PastePinnwand: def __init__(self, url: str, expiry: str) -> None: self.url = url From af5e90ab270956d45b9c9399fc2929ab996d22b6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 27 Apr 2022 00:11:34 +0200 Subject: [PATCH 331/555] Fix import of Protocol --- bpython/paste.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/paste.py b/bpython/paste.py index fd140a0ec..ceba59386 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 Optional, Tuple from urllib.parse import urljoin, urlparse import requests @@ -30,6 +30,7 @@ from .config import getpreferredencoding from .translations import _ +from ._typing_compat import Protocol class PasteFailed(Exception): From 14669c08e28dcb0ad1a186b486e50595bad952f5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 3 May 2022 00:19:15 +0200 Subject: [PATCH 332/555] Set up __main__ module (fixes #959, #868) --- bpython/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 04cb8fc3e..1212fe3f6 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -243,10 +243,10 @@ def exec_code( raise SystemExit(e.errno) old_argv, sys.argv = sys.argv, args sys.path.insert(0, os.path.abspath(os.path.dirname(args[0]))) - spec = importlib.util.spec_from_loader("__console__", loader=None) + spec = importlib.util.spec_from_loader("__main__", loader=None) assert spec mod = importlib.util.module_from_spec(spec) - sys.modules["__console__"] = mod + sys.modules["__main__"] = mod interpreter.locals.update(mod.__dict__) # type: ignore # TODO use a more specific type that has a .locals attribute interpreter.locals["__file__"] = args[0] # type: ignore # TODO use a more specific type that has a .locals attribute interpreter.runsource(source, args[0], "exec") From 3d3a6633394c69404e53aa6dc1d61b6126489b51 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Sun, 6 Feb 2022 17:22:15 -0600 Subject: [PATCH 333/555] Just starting out --- bpython/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bpython/cli.py b/bpython/cli.py index 28cc67c71..03987d072 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -21,6 +21,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True # Modified by Brandon Navra # Notes for Windows From 8a292ed6aafaca98b9532d5f343070956daf89a9 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Tue, 8 Feb 2022 23:08:21 -0600 Subject: [PATCH 334/555] Done through line 120 --- bpython/cli.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 03987d072..7194cde3f 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -52,7 +52,7 @@ import struct import sys import time -from typing import Iterator, NoReturn, List +from typing import Iterator, NoReturn, List, MutableMapping, Any import unicodedata from dataclasses import dataclass @@ -66,7 +66,7 @@ from pygments import format from pygments.formatters import TerminalFormatter from pygments.lexers import Python3Lexer -from pygments.token import Token +from pygments.token import Token, _TokenType from .formatter import BPythonFormatter # This for config @@ -100,7 +100,9 @@ class ShowListState: wl: int = 0 -def calculate_screen_lines(tokens, width, cursor=0) -> int: +def calculate_screen_lines( + tokens: MutableMapping[_TokenType, str], width: int, cursor: int = 0 +) -> int: """Given a stream of tokens and a screen width plus an optional initial cursor position, return the amount of needed lines on the screen.""" From 7ae4c794dfd54b803bdf241c963aaa6201d44ab7 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Wed, 9 Feb 2022 20:39:08 -0600 Subject: [PATCH 335/555] Not sure about forward_if_not_current() typing. --- bpython/cli.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 7194cde3f..9720e76d8 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -52,7 +52,7 @@ import struct import sys import time -from typing import Iterator, NoReturn, List, MutableMapping, Any +from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable import unicodedata from dataclasses import dataclass @@ -118,9 +118,9 @@ def calculate_screen_lines( return lines -def forward_if_not_current(func): +def forward_if_not_current(func: Callable) -> Callable: @functools.wraps(func) - def newfunc(self, *args, **kwargs): + def newfunc(self, *args: Any, **kwargs: Any) -> Any: dest = self.get_dest() if self is dest: return func(self, *args, **kwargs) From 6fecebdcbe084bf88aa884e6987c327e7694989d Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Thu, 17 Feb 2022 08:55:35 -0600 Subject: [PATCH 336/555] Did the forward_if_not_current decorator function. Not 100% sure about the types. --- bpython/cli.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 9720e76d8..2ad20f293 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -52,7 +52,7 @@ import struct import sys import time -from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable +from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable, TypeVar, cast import unicodedata from dataclasses import dataclass @@ -84,6 +84,7 @@ from .pager import page from .args import parse as argsparse +F = TypeVar('F', bound=Callable[..., Any]) # --- module globals --- stdscr = None @@ -118,16 +119,16 @@ def calculate_screen_lines( return lines -def forward_if_not_current(func: Callable) -> Callable: +def forward_if_not_current(func: F) -> F: @functools.wraps(func) - def newfunc(self, *args: Any, **kwargs: Any) -> Any: + def newfunc(self, *args, **kwargs): # type: ignore dest = self.get_dest() if self is dest: return func(self, *args, **kwargs) else: return getattr(self.get_dest(), newfunc.__name__)(*args, **kwargs) - return newfunc + return cast(F, newfunc) class FakeStream: From 9c56f05775cd599279caa340d2a0d66e5ea8daa2 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 18 Feb 2022 20:12:34 -0600 Subject: [PATCH 337/555] Had to punt on get_color() --- bpython/cli.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 2ad20f293..15f93bb8a 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -52,7 +52,7 @@ import struct import sys import time -from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable, TypeVar, cast +from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable, TypeVar, cast, IO, Iterable, Optional import unicodedata from dataclasses import dataclass @@ -70,7 +70,7 @@ from .formatter import BPythonFormatter # This for config -from .config import getpreferredencoding +from .config import getpreferredencoding, Config # This for keys from .keys import cli_key_dispatch as key_dispatch @@ -88,7 +88,7 @@ # --- module globals --- stdscr = None -colors = None +colors: Optional[MutableMapping[str, int]] = None DO_RESIZE = False # --- @@ -135,17 +135,17 @@ class FakeStream: """Provide a fake file object which calls functions on the interface provided.""" - def __init__(self, interface, get_dest): + def __init__(self, interface: 'CLIRepl', get_dest: IO[str]) -> None: self.encoding: str = getpreferredencoding() self.interface = interface self.get_dest = get_dest @forward_if_not_current - def write(self, s) -> None: + def write(self, s: str) -> None: self.interface.write(s) @forward_if_not_current - def writelines(self, l) -> None: + def writelines(self, l: Iterable[str]) -> None: for s in l: self.write(s) @@ -160,7 +160,7 @@ def flush(self) -> None: class FakeStdin: """Provide a fake stdin type for things like raw_input() etc.""" - def __init__(self, interface) -> None: + def __init__(self, interface: 'CLIRepl') -> None: """Take the curses Repl on init and assume it provides a get_key method which, fortunately, it does.""" @@ -171,11 +171,11 @@ def __init__(self, interface) -> None: def __iter__(self) -> Iterator: return iter(self.readlines()) - def flush(self): + def flush(self) -> None: """Flush the internal buffer. This is a no-op. Flushing stdin doesn't make any sense anyway.""" - def write(self, value) -> NoReturn: + def write(self, value: str) -> NoReturn: # XXX IPython expects sys.stdin.write to exist, there will no doubt be # others, so here's a hack to keep them happy raise OSError(errno.EBADF, "sys.stdin is read-only") @@ -183,7 +183,7 @@ def write(self, value) -> NoReturn: def isatty(self) -> bool: return True - def readline(self, size=-1): + def readline(self, size: int = -1) -> str: """I can't think of any reason why anything other than readline would be useful in the context of an interactive interpreter so this is the only one I've done anything with. The others are just there in case @@ -228,7 +228,7 @@ def readline(self, size=-1): return buffer - def read(self, size=None): + def read(self, size: Optional[int] = None) -> str: if size == 0: return "" @@ -243,7 +243,7 @@ def read(self, size=None): return "".join(data) - def readlines(self, size=-1): + def readlines(self, size: int = -1) -> List[str]: return list(iter(self.readline, "")) @@ -260,17 +260,20 @@ def readlines(self, size=-1): # the addstr stuff to a higher level. # - -def get_color(config, name): +# Have to ignore the return type on this one because the colors variable +# is Optional[MutableMapping[str, int]] but for the purposes of this +# function it can't be None +def get_color(config: Config, name: str) -> int: # type: ignore[return] global colors - return colors[config.color_scheme[name].lower()] + if colors: + return colors[config.color_scheme[name].lower()] -def get_colpair(config, name): +def get_colpair(config: Config, name: str) -> int: return curses.color_pair(get_color(config, name) + 1) -def make_colors(config): +def make_colors(config: Config) -> MutableMapping[str, int]: """Init all the colours in curses and bang them into a dictionary""" # blacK, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default: From 3c3a81d0056f272aeeb7c3ec16deb285f1265947 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 18 Feb 2022 23:58:16 -0600 Subject: [PATCH 338/555] Still working my way down. --- bpython/cli.py | 63 +++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 15f93bb8a..812d4b609 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -315,10 +315,10 @@ def make_colors(config: Config) -> MutableMapping[str, int]: class CLIInteraction(repl.Interaction): - def __init__(self, config, statusbar=None): + def __init__(self, config: Config, statusbar: 'Statusbar' = None): super().__init__(config, statusbar) - def confirm(self, q): + def confirm(self, q: str) -> bool: """Ask for yes or no and return boolean""" try: reply = self.statusbar.prompt(q) @@ -327,15 +327,15 @@ def confirm(self, q): return reply.lower() in (_("y"), _("yes")) - def notify(self, s, n=10, wait_for_keypress=False): + def notify(self, s: str, n: int = 10, wait_for_keypress: bool = False) -> None: return self.statusbar.message(s, n) - def file_prompt(self, s): + def file_prompt(self, s: int) -> str: return self.statusbar.prompt(s) class CLIRepl(repl.Repl): - def __init__(self, scr, interp, statusbar, config, idle=None): + def __init__(self, scr: curses.window, interp: repl.Interpreter, statusbar: 'Statusbar', config: Config, idle: None = None): super().__init__(interp, config) self.interp.writetb = self.writetb self.scr = scr @@ -357,10 +357,10 @@ def __init__(self, scr, interp, statusbar, config, idle=None): if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1: config.cli_suggestion_width = 0.8 - def _get_cursor_offset(self): + def _get_cursor_offset(self) -> int: return len(self.s) - self.cpos - def _set_cursor_offset(self, offset): + def _set_cursor_offset(self, offset: int) -> None: self.cpos = len(self.s) - offset cursor_offset = property( @@ -370,7 +370,7 @@ def _set_cursor_offset(self, offset): "The cursor offset from the beginning of the line", ) - def addstr(self, s): + def addstr(self, s: str) -> None: """Add a string to the current input line and figure out where it should go, depending on the cursor position.""" self.rl_history.reset() @@ -382,7 +382,7 @@ def addstr(self, s): self.complete() - def atbol(self): + def atbol(self) -> bool: """Return True or False accordingly if the cursor is at the beginning of the line (whitespace is ignored). This exists so that p_key() knows how to handle the tab key being pressed - if there is nothing but white @@ -391,17 +391,18 @@ def atbol(self): return not self.s.lstrip() - def bs(self, delete_tabs=True): + # This function shouldn't return None because of pos -= self.bs() later on + def bs(self, delete_tabs: bool = True) -> int: # type: ignore[return-value] """Process a backspace""" self.rl_history.reset() y, x = self.scr.getyx() if not self.s: - return + return None # type: ignore[return-value] if x == self.ix and y == self.iy: - return + return None # type: ignore[return-value] n = 1 @@ -422,7 +423,7 @@ def bs(self, delete_tabs=True): return n - def bs_word(self): + def bs_word(self) -> str: self.rl_history.reset() pos = len(self.s) - self.cpos - 1 deleted = [] @@ -437,7 +438,7 @@ def bs_word(self): return "".join(reversed(deleted)) - def check(self): + def check(self) -> None: """Check if paste mode should still be active and, if not, deactivate it and force syntax highlighting.""" @@ -448,14 +449,14 @@ def check(self): self.paste_mode = False self.print_line(self.s) - def clear_current_line(self): + def clear_current_line(self) -> None: """Called when a SyntaxError occurred in the interpreter. It is used to prevent autoindentation from occurring after a traceback.""" repl.Repl.clear_current_line(self) self.s = "" - def clear_wrapped_lines(self): + def clear_wrapped_lines(self) -> None: """Clear the wrapped lines of the current input.""" # curses does not handle this on its own. Sad. height, width = self.scr.getmaxyx() @@ -464,7 +465,7 @@ def clear_wrapped_lines(self): self.scr.move(y, 0) self.scr.clrtoeol() - def complete(self, tab=False): + def complete(self, tab: bool = False) -> None: """Get Autocomplete list and window. Called whenever these should be updated, and called @@ -494,7 +495,7 @@ def complete(self, tab=False): self.scr.redrawwin() self.scr.refresh() - def clrtobol(self): + def clrtobol(self) -> None: """Clear from cursor to beginning of line; usual C-u behaviour""" self.clear_wrapped_lines() @@ -507,10 +508,10 @@ def clrtobol(self): self.scr.redrawwin() self.scr.refresh() - def _get_current_line(self): + def _get_current_line(self) -> str: return self.s - def _set_current_line(self, line): + def _set_current_line(self, line: str) -> None: self.s = line current_line = property( @@ -520,7 +521,7 @@ def _set_current_line(self, line): "The characters of the current line", ) - def cut_to_buffer(self): + def cut_to_buffer(self) -> None: """Clear from cursor to end of line, placing into cut buffer""" self.cut_buffer = self.s[-self.cpos :] self.s = self.s[: -self.cpos] @@ -529,7 +530,7 @@ def cut_to_buffer(self): self.scr.redrawwin() self.scr.refresh() - def delete(self): + def delete(self) -> None: """Process a del""" if not self.s: return @@ -537,7 +538,7 @@ def delete(self): if self.mvc(-1): self.bs(False) - def echo(self, s, redraw=True): + def echo(self, s: str, redraw: bool = True) -> None: """Parse and echo a formatted string with appropriate attributes. It uses the formatting method as defined in formatter.py to parse the strings. It won't update the screen if it's reevaluating the code (as it @@ -571,7 +572,7 @@ def echo(self, s, redraw=True): if redraw and not self.evaluating: self.scr.refresh() - def end(self, refresh=True): + def end(self, refresh: bool = True) -> bool: self.cpos = 0 h, w = gethw() y, x = divmod(len(self.s) + self.ix, w) @@ -582,7 +583,7 @@ def end(self, refresh=True): return True - def hbegin(self): + def hbegin(self) -> None: """Replace the active line with first line in history and increment the index to keep track""" self.cpos = 0 @@ -591,7 +592,7 @@ def hbegin(self): self.s = self.rl_history.first() self.print_line(self.s, clr=True) - def hend(self): + def hend(self) -> None: """Same as hbegin() but, well, forward""" self.cpos = 0 self.clear_wrapped_lines() @@ -599,7 +600,7 @@ def hend(self): self.s = self.rl_history.last() self.print_line(self.s, clr=True) - def back(self): + def back(self) -> None: """Replace the active line with previous line in history and increment the index to keep track""" @@ -609,7 +610,7 @@ def back(self): self.s = self.rl_history.back() self.print_line(self.s, clr=True) - def fwd(self): + def fwd(self) -> None: """Same as back() but, well, forward""" self.cpos = 0 @@ -618,7 +619,7 @@ def fwd(self): self.s = self.rl_history.forward() self.print_line(self.s, clr=True) - def search(self): + def search(self) -> None: """Search with the partial matches from the history object.""" self.cpo = 0 @@ -627,7 +628,7 @@ def search(self): self.s = self.rl_history.back(start=False, search=True) self.print_line(self.s, clr=True) - def get_key(self): + def get_key(self) -> str: key = "" while True: try: @@ -667,7 +668,7 @@ def get_key(self): if self.idle: self.idle(self) - def get_line(self): + def get_line(self) -> Optional[str]: """Get a line of text and return it This function initialises an empty string and gets the curses cursor position on the screen and stores it From f42d1beca1dbae44906943ecf7e299f10e4b11fa Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Sun, 20 Feb 2022 17:26:33 -0600 Subject: [PATCH 339/555] Saving progress --- bpython/cli.py | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 812d4b609..cfb1fa254 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -52,7 +52,7 @@ import struct import sys import time -from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable, TypeVar, cast, IO, Iterable, Optional +from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable, TypeVar, cast, IO, Iterable, Optional, Union, Tuple import unicodedata from dataclasses import dataclass @@ -330,7 +330,7 @@ def confirm(self, q: str) -> bool: def notify(self, s: str, n: int = 10, wait_for_keypress: bool = False) -> None: return self.statusbar.message(s, n) - def file_prompt(self, s: int) -> str: + def file_prompt(self, s: str) -> str: return self.statusbar.prompt(s) @@ -668,7 +668,7 @@ def get_key(self) -> str: if self.idle: self.idle(self) - def get_line(self) -> Optional[str]: + def get_line(self) -> str: """Get a line of text and return it This function initialises an empty string and gets the curses cursor position on the screen and stores it @@ -694,14 +694,14 @@ def get_line(self) -> Optional[str]: self.s = self.s[4:] return self.s - def home(self, refresh=True): + def home(self, refresh: bool = True) -> bool: self.scr.move(self.iy, self.ix) self.cpos = len(self.s) if refresh: self.scr.refresh() return True - def lf(self): + def lf(self) -> None: """Process a linefeed character; it only needs to check the cursor position and move appropriately so it doesn't clear the current line after the cursor.""" @@ -713,7 +713,12 @@ def lf(self): self.print_line(self.s, newline=True) self.echo("\n") - def mkargspec(self, topline, in_arg, down): + def mkargspec( + self, + topline: Any, # Named tuples don't seem to play nice with mypy + in_arg: Union[str, int], + down: bool + ) -> int: """This figures out what to do with the argspec and puts it nicely into the list window. It returns the number of lines used to display the argspec. It's also kind of messy due to it having to call so many @@ -817,7 +822,7 @@ def mkargspec(self, topline, in_arg, down): return r - def mvc(self, i, refresh=True): + def mvc(self, i: int, refresh: bool = True) -> bool: """This method moves the cursor relatively from the current position, where: 0 == (right) end of current line @@ -850,7 +855,7 @@ def mvc(self, i, refresh=True): return True - def p_key(self, key): + def p_key(self, key: str) -> Union[None, str, bool]: """Process a keypress""" if key is None: @@ -1044,7 +1049,7 @@ def p_key(self, key): return True - def print_line(self, s, clr=False, newline=False): + def print_line(self, s: Optional[str], clr: bool = False, newline: bool = False) -> None: """Chuck a line of text through the highlighter, move the cursor to the beginning of the line and output it to the screen.""" @@ -1080,7 +1085,10 @@ def print_line(self, s, clr=False, newline=False): self.mvc(1) self.cpos = t - def prompt(self, more): + def prompt( + self, + more: Any # I'm not sure of the type on this one + ) -> None: """Show the appropriate Python prompt""" if not more: self.echo( @@ -1101,7 +1109,7 @@ def prompt(self, more): f"\x01{prompt_more_color}\x03{self.ps2}\x04" ) - def push(self, s, insert_into_history=True): + def push(self, s: str, insert_into_history: bool = True) -> bool: # curses.raw(True) prevents C-c from causing a SIGINT curses.raw(False) try: @@ -1114,7 +1122,7 @@ def push(self, s, insert_into_history=True): finally: curses.raw(True) - def redraw(self): + def redraw(self) -> None: """Redraw the screen using screen_hist""" self.scr.erase() for k, s in enumerate(self.screen_hist): @@ -1130,7 +1138,7 @@ def redraw(self): self.scr.refresh() self.statusbar.refresh() - def repl(self): + def repl(self) -> Tuple: """Initialise the repl and jump into the loop. This method also has to keep a stack of lines entered for the horrible "undo" feature. It also tracks everything that would normally go to stdout in the normal Python @@ -1171,7 +1179,7 @@ def repl(self): self.s = "" return self.exit_value - def reprint_line(self, lineno, tokens): + def reprint_line(self, lineno: int, tokens: MutableMapping[_TokenType, str]) -> None: """Helper function for paren highlighting: Reprint line at offset `lineno` in current input buffer.""" if not self.buffer or lineno == len(self.buffer): @@ -1194,7 +1202,7 @@ def reprint_line(self, lineno, tokens): for string in line.split("\x04"): self.echo(string) - def resize(self): + def resize(self) -> None: """This method exists simply to keep it straight forward when initialising a window and resizing it.""" self.size() @@ -1204,13 +1212,13 @@ def resize(self): self.statusbar.resize(refresh=False) self.redraw() - def getstdout(self): + def getstdout(self) -> str: """This method returns the 'spoofed' stdout buffer, for writing to a file or sending to a pastebin or whatever.""" return self.stdout_hist + "\n" - def reevaluate(self): + def reevaluate(self) -> None: """Clear the buffer, redraw the screen and re-evaluate the history""" self.evaluating = True @@ -1249,7 +1257,7 @@ def reevaluate(self): # map(self.push, self.history) # ^-- That's how simple this method was at first :( - def write(self, s): + def write(self, s: str) -> None: """For overriding stdout defaults""" if "\x04" in s: for block in s.split("\x04"): @@ -1418,7 +1426,7 @@ def suspend(self): curses.endwin() os.kill(os.getpid(), signal.SIGSTOP) - def tab(self, back=False): + def tab(self, back: bool = False) -> bool: """Process the tab key being hit. If there's only whitespace @@ -1498,7 +1506,7 @@ def yank_from_buffer(self): self.addstr(self.cut_buffer) self.print_line(self.s, clr=True) - def send_current_line_to_editor(self): + def send_current_line_to_editor(self) -> str: lines = self.send_to_external_editor(self.s).split("\n") self.s = "" self.print_line(self.s) From b47b91852ebca009a99e1364fbb174b4b1065760 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Wed, 23 Feb 2022 16:21:01 -0600 Subject: [PATCH 340/555] Progress --- bpython/cli.py | 124 ++++++++++++++++++++++++++++++------------------- 1 file changed, 75 insertions(+), 49 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index cfb1fa254..88c2b026f 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -335,7 +335,14 @@ def file_prompt(self, s: str) -> str: class CLIRepl(repl.Repl): - def __init__(self, scr: curses.window, interp: repl.Interpreter, statusbar: 'Statusbar', config: Config, idle: None = None): + def __init__( + self, + scr: curses.window, + interp: repl.Interpreter, + statusbar: 'Statusbar', + config: Config, + idle: Optional[Callable] = None + ): super().__init__(interp, config) self.interp.writetb = self.writetb self.scr = scr @@ -343,7 +350,7 @@ def __init__(self, scr: curses.window, interp: repl.Interpreter, statusbar: 'Sta self.list_win = newwin(get_colpair(config, "background"), 1, 1, 1, 1) self.cpos = 0 self.do_exit = False - self.exit_value = () + self.exit_value: Tuple[Any, ...] = () self.f_string = "" self.idle = idle self.in_hist = False @@ -1138,7 +1145,7 @@ def redraw(self) -> None: self.scr.refresh() self.statusbar.refresh() - def repl(self) -> Tuple: + def repl(self) -> Tuple[Any, ...]: """Initialise the repl and jump into the loop. This method also has to keep a stack of lines entered for the horrible "undo" feature. It also tracks everything that would normally go to stdout in the normal Python @@ -1277,8 +1284,13 @@ def write(self, s: str) -> None: self.screen_hist.append(s.rstrip()) def show_list( - self, items, arg_pos, topline=None, formatter=None, current_item=None - ): + self, + items: List[str], + arg_pos: Union[str, int], + topline: Any = None, # Named tuples don't play nice with mypy + formatter: Optional[Callable] = None, + current_item: Optional[bool] = None + ) -> None: shared = ShowListState() y, x = self.scr.getyx() h, w = self.scr.getmaxyx() @@ -1290,7 +1302,7 @@ def show_list( max_w = int(w * self.config.cli_suggestion_width) self.list_win.erase() - if items: + if items and formatter: items = [formatter(x) for x in items] if current_item: current_item = formatter(current_item) @@ -1300,7 +1312,7 @@ def show_list( else: height_offset = 0 - def lsize(): + def lsize() -> bool: wl = max(len(i) for i in v_items) + 1 if not wl: wl = 1 @@ -1410,17 +1422,18 @@ def lsize(): self.scr.move(*self.scr.getyx()) self.list_win.refresh() - def size(self): + def size(self) -> None: """Set instance attributes for x and y top left corner coordinates and width and height for the window.""" global stdscr - h, w = stdscr.getmaxyx() - self.y = 0 - self.w = w - self.h = h - 1 - self.x = 0 - - def suspend(self): + if stdscr: + h, w = stdscr.getmaxyx() + self.y: int = 0 + self.w: int = w + self.h: int = h - 1 + self.x: int = 0 + + def suspend(self) -> None: """Suspend the current process for shell job control.""" if platform.system() != "Windows": curses.endwin() @@ -1489,19 +1502,19 @@ def tab(self, back: bool = False) -> bool: self.print_line(self.s, True) return True - def undo(self, n=1): + def undo(self, n: int = 1) -> None: repl.Repl.undo(self, n) # This will unhighlight highlighted parens self.print_line(self.s) - def writetb(self, lines): + def writetb(self, lines: List[str]) -> None: for line in lines: self.write( "\x01{}\x03{}".format(self.config.color_scheme["error"], line) ) - def yank_from_buffer(self): + def yank_from_buffer(self) -> None: """Paste the text from the cut buffer at the current cursor location""" self.addstr(self.cut_buffer) self.print_line(self.s, clr=True) @@ -1569,7 +1582,15 @@ class Statusbar: """ - def __init__(self, scr, pwin, background, config, s=None, c=None): + def __init__( + self, + scr: curses.window, + pwin: curses.window, + background: int, + config: Config, + s: Optional[str] = None, + c: Optional[int] = None + ): """Initialise the statusbar and display the initial text (if any)""" self.size() self.win = newwin(background, self.h, self.w, self.y, self.x) @@ -1581,9 +1602,10 @@ def __init__(self, scr, pwin, background, config, s=None, c=None): self.c = c self.timer = 0 self.pwin = pwin - self.settext(s, c) + if s: + self.settext(s, c) - def size(self): + def size(self) -> None: """Set instance attributes for x and y top left corner coordinates and width and height for the window.""" h, w = gethw() @@ -1592,7 +1614,7 @@ def size(self): self.h = 1 self.x = 0 - def resize(self, refresh=True): + def resize(self, refresh: bool = True) -> None: """This method exists simply to keep it straight forward when initialising a window and resizing it.""" self.size() @@ -1601,12 +1623,12 @@ def resize(self, refresh=True): if refresh: self.refresh() - def refresh(self): + def refresh(self) -> None: """This is here to make sure the status bar text is redraw properly after a resize.""" self.settext(self._s) - def check(self): + def check(self) -> None: """This is the method that should be called every half second or so to see if the status bar needs updating.""" if not self.timer: @@ -1617,13 +1639,13 @@ def check(self): self.settext(self._s) - def message(self, s, n=3): + def message(self, s: str, n: int = 3) -> None: """Display a message for a short n seconds on the statusbar and return it to its original state.""" - self.timer = time.time() + n + self.timer = int(time.time() + n) self.settext(s) - def prompt(self, s=""): + def prompt(self, s: str = "") -> str: """Prompt the user for some input (with the optional prompt 's') and return the input text, then restore the statusbar to its original value.""" @@ -1631,7 +1653,7 @@ def prompt(self, s=""): self.settext(s or "? ", p=True) iy, ix = self.win.getyx() - def bs(s): + def bs(s: str) -> str: y, x = self.win.getyx() if x == ix: return s @@ -1656,14 +1678,14 @@ def bs(s): raise ValueError # literal elif 0 < c < 127: - c = chr(c) - self.win.addstr(c, get_colpair(self.config, "prompt")) - o += c + d = chr(c) + self.win.addstr(d, get_colpair(self.config, "prompt")) + o += d self.settext(self._s) return o - def settext(self, s, c=None, p=False): + def settext(self, s: str, c: Optional[int] = None, p: bool = False) -> None: """Set the text on the status bar to a new permanent value; this is the value that will be set after a prompt or message. c is the optional curses colour pair to use (if not specified the last specified colour @@ -1690,12 +1712,12 @@ def settext(self, s, c=None, p=False): else: self.win.refresh() - def clear(self): + def clear(self) -> None: """Clear the status bar.""" self.win.clear() -def init_wins(scr, config): +def init_wins(scr: curses.window, config: Config) -> Tuple[curses.window, Statusbar]: """Initialise the two windows (the main repl interface and the little status bar at the bottom with some stuff in it)""" # TODO: Document better what stuff is on the status bar. @@ -1705,7 +1727,9 @@ def init_wins(scr, config): main_win = newwin(background, h - 1, w, 0, 0) main_win.scrollok(True) - main_win.keypad(1) + + # I think this is supposed to be True instead of 1? + main_win.keypad(1) # type:ignore[arg-type] # Thanks to Angus Gibson for pointing out this missing line which was causing # problems that needed dirty hackery to fix. :) @@ -1728,18 +1752,18 @@ def init_wins(scr, config): return main_win, statusbar -def sigwinch(unused_scr): +def sigwinch(unused_scr: curses.window) -> None: global DO_RESIZE DO_RESIZE = True -def sigcont(unused_scr): +def sigcont(unused_scr: curses.window) -> None: sigwinch(unused_scr) # Forces the redraw curses.ungetch("\x00") -def gethw(): +def gethw() -> Tuple[int, int]: """I found this code on a usenet post, and snipped out the bit I needed, so thanks to whoever wrote that, sorry I forgot your name, I'm sure you're a great guy. @@ -1757,10 +1781,10 @@ def gethw(): if platform.system() != "Windows": h, w = struct.unpack( - "hhhh", fcntl.ioctl(sys.__stdout__, termios.TIOCGWINSZ, "\000" * 8) + "hhhh", fcntl.ioctl(sys.__stdout__, termios.TIOCGWINSZ, "\000" * 8) # type:ignore[call-overload] )[0:2] else: - from ctypes import windll, create_string_buffer + from ctypes import windll, create_string_buffer # type:ignore[attr-defined] # stdin handle is -10 # stdout handle is -11 @@ -1786,7 +1810,7 @@ def gethw(): ) = struct.unpack("hhhhHhhhhhh", csbi.raw) sizex = right - left + 1 sizey = bottom - top + 1 - else: + elif stdscr: # can't determine actual size - return default values sizex, sizey = stdscr.getmaxyx() @@ -1794,7 +1818,7 @@ def gethw(): return h, w -def idle(caller): +def idle(caller: CLIRepl) -> None: """This is called once every iteration through the getkey() loop (currently in the Repl class, see the get_line() method). The statusbar check needs to go here to take care of timed @@ -1817,7 +1841,7 @@ def idle(caller): do_resize(caller) -def do_resize(caller): +def do_resize(caller: CLIRepl) -> None: """This needs to hack around readline and curses not playing nicely together. See also gethw() above.""" global DO_RESIZE @@ -1844,14 +1868,14 @@ class FakeDict: used as a hacky solution to using a colours dict containing colour codes if colour initialisation fails.""" - def __init__(self, val): + def __init__(self, val: int): self._val = val - def __getitem__(self, k): + def __getitem__(self, k: Any) -> int: return self._val -def newwin(background, *args): +def newwin(background: int, *args: int) -> curses.window: """Wrapper for curses.newwin to automatically set background colour on any newly created window.""" win = curses.newwin(*args) @@ -1859,7 +1883,7 @@ def newwin(background, *args): return win -def curses_wrapper(func, *args, **kwargs): +def curses_wrapper(func: Callable, *args: Any, **kwargs: Any) -> Any: """Like curses.wrapper(), but reuses stdscr when called again.""" global stdscr if stdscr is None: @@ -1867,7 +1891,8 @@ def curses_wrapper(func, *args, **kwargs): try: curses.noecho() curses.cbreak() - stdscr.keypad(1) + # Should this be keypad(True)? + stdscr.keypad(1) # type:ignore[arg-type] try: curses.start_color() @@ -1876,7 +1901,8 @@ def curses_wrapper(func, *args, **kwargs): return func(stdscr, *args, **kwargs) finally: - stdscr.keypad(0) + # Should this be keypad(False)? + stdscr.keypad(0) # type:ignore[arg-type] curses.echo() curses.nocbreak() curses.endwin() From 4e55dde2e2582158cf8d6301b1af292003f3a150 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Sun, 6 Mar 2022 21:06:40 -0600 Subject: [PATCH 341/555] Finished squashing the mypy errors, use --follow-imports=error to ignore errors from imports that haven't been typed yet. --- bpython/cli.py | 56 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 88c2b026f..29658f7ee 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -315,23 +315,29 @@ def make_colors(config: Config) -> MutableMapping[str, int]: class CLIInteraction(repl.Interaction): - def __init__(self, config: Config, statusbar: 'Statusbar' = None): + def __init__(self, config: Config, statusbar: Optional['Statusbar'] = None): super().__init__(config, statusbar) def confirm(self, q: str) -> bool: """Ask for yes or no and return boolean""" try: - reply = self.statusbar.prompt(q) + if self.statusbar: + reply = self.statusbar.prompt(q) except ValueError: return False return reply.lower() in (_("y"), _("yes")) def notify(self, s: str, n: int = 10, wait_for_keypress: bool = False) -> None: - return self.statusbar.message(s, n) + if self.statusbar: + self.statusbar.message(s, n) - def file_prompt(self, s: str) -> str: - return self.statusbar.prompt(s) + def file_prompt(self, s: str) -> Optional[str]: + if self.statusbar: + # This thows a mypy error because repl.py isn't typed yet + return self.statusbar.prompt(s) # type:ignore[no-any-return] + else: + return None class CLIRepl(repl.Repl): @@ -970,7 +976,7 @@ def p_key(self, key: str) -> Union[None, str, bool]: elif key in key_dispatch[config.clear_screen_key]: # clear all but current line - self.screen_hist = [self.screen_hist[-1]] + self.screen_hist: List = [self.screen_hist[-1]] self.highlighted_paren = None self.redraw() return "" @@ -1120,7 +1126,8 @@ def push(self, s: str, insert_into_history: bool = True) -> bool: # curses.raw(True) prevents C-c from causing a SIGINT curses.raw(False) try: - return repl.Repl.push(self, s, insert_into_history) + x: bool = repl.Repl.push(self, s, insert_into_history) + return x except SystemExit as e: # Avoid a traceback on e.g. quit() self.do_exit = True @@ -1231,7 +1238,7 @@ def reevaluate(self) -> None: self.evaluating = True self.stdout_hist = "" self.f_string = "" - self.buffer = [] + self.buffer: List[str] = [] self.scr.erase() self.screen_hist = [] # Set cursor position to -1 to prevent paren matching @@ -1593,7 +1600,7 @@ def __init__( ): """Initialise the statusbar and display the initial text (if any)""" self.size() - self.win = newwin(background, self.h, self.w, self.y, self.x) + self.win: curses.window = newwin(background, self.h, self.w, self.y, self.x) self.config = config @@ -1908,7 +1915,14 @@ def curses_wrapper(func: Callable, *args: Any, **kwargs: Any) -> Any: curses.endwin() -def main_curses(scr, args, config, interactive=True, locals_=None, banner=None): +def main_curses( + scr: curses.window, + args: List[str], + config: Config, + interactive: bool = True, + locals_: Optional[MutableMapping[str, str]] = None, + banner: Optional[str] = None +) -> Tuple[Tuple[Any, ...], str]: """main function for the curses convenience wrapper Initialise the two main objects: the interpreter @@ -1941,7 +1955,9 @@ def main_curses(scr, args, config, interactive=True, locals_=None, banner=None): curses.use_default_colors() cols = make_colors(config) except curses.error: - cols = FakeDict(-1) + # Not sure what to do with the types here... + # FakeDict acts as a dictionary, but isn't actually a dictionary + cols = FakeDict(-1) # type:ignore[assignment] # FIXME: Gargh, bad design results in using globals without a refactor :( colors = cols @@ -1956,12 +1972,13 @@ def main_curses(scr, args, config, interactive=True, locals_=None, banner=None): clirepl = CLIRepl(main_win, interpreter, statusbar, config, idle) clirepl._C = cols - sys.stdin = FakeStdin(clirepl) - sys.stdout = FakeStream(clirepl, lambda: sys.stdout) - sys.stderr = FakeStream(clirepl, lambda: sys.stderr) + # Not sure how to type these Fake types + sys.stdin = FakeStdin(clirepl) # type:ignore[assignment] + sys.stdout = FakeStream(clirepl, lambda: sys.stdout) # type:ignore + sys.stderr = FakeStream(clirepl, lambda: sys.stderr) # type:ignore if args: - exit_value = () + exit_value: Tuple[Any, ...] = () try: bpargs.exec_code(interpreter, args) except SystemExit as e: @@ -1995,7 +2012,8 @@ def main_curses(scr, args, config, interactive=True, locals_=None, banner=None): exit_value = clirepl.repl() if hasattr(sys, "exitfunc"): - sys.exitfunc() + # Seems like the if statment should satisfy mypy, but it doesn't + sys.exitfunc() # type:ignore[attr-defined] delattr(sys, "exitfunc") main_win.erase() @@ -2012,7 +2030,11 @@ def main_curses(scr, args, config, interactive=True, locals_=None, banner=None): return (exit_value, clirepl.getstdout()) -def main(args=None, locals_=None, banner=None): +def main( + args: Optional[List[str]] = None, + locals_: Optional[MutableMapping[str, str]] = None, + banner: Optional[str] = None +) -> Any: translations.init() config, options, exec_args = argsparse(args) From 136631d4547effee53b7eba8aaeb75e904598497 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Sun, 6 Mar 2022 21:13:23 -0600 Subject: [PATCH 342/555] Formatted with Black --- bpython/cli.py | 127 ++++++++++++++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 50 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 29658f7ee..ed885669b 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -52,7 +52,21 @@ import struct import sys import time -from typing import Iterator, NoReturn, List, MutableMapping, Any, Callable, TypeVar, cast, IO, Iterable, Optional, Union, Tuple +from typing import ( + Iterator, + NoReturn, + List, + MutableMapping, + Any, + Callable, + TypeVar, + cast, + IO, + Iterable, + Optional, + Union, + Tuple, +) import unicodedata from dataclasses import dataclass @@ -84,7 +98,7 @@ from .pager import page from .args import parse as argsparse -F = TypeVar('F', bound=Callable[..., Any]) +F = TypeVar("F", bound=Callable[..., Any]) # --- module globals --- stdscr = None @@ -135,7 +149,7 @@ class FakeStream: """Provide a fake file object which calls functions on the interface provided.""" - def __init__(self, interface: 'CLIRepl', get_dest: IO[str]) -> None: + def __init__(self, interface: "CLIRepl", get_dest: IO[str]) -> None: self.encoding: str = getpreferredencoding() self.interface = interface self.get_dest = get_dest @@ -160,7 +174,7 @@ def flush(self) -> None: class FakeStdin: """Provide a fake stdin type for things like raw_input() etc.""" - def __init__(self, interface: 'CLIRepl') -> None: + def __init__(self, interface: "CLIRepl") -> None: """Take the curses Repl on init and assume it provides a get_key method which, fortunately, it does.""" @@ -263,7 +277,7 @@ def readlines(self, size: int = -1) -> List[str]: # Have to ignore the return type on this one because the colors variable # is Optional[MutableMapping[str, int]] but for the purposes of this # function it can't be None -def get_color(config: Config, name: str) -> int: # type: ignore[return] +def get_color(config: Config, name: str) -> int: # type: ignore[return] global colors if colors: return colors[config.color_scheme[name].lower()] @@ -315,7 +329,7 @@ def make_colors(config: Config) -> MutableMapping[str, int]: class CLIInteraction(repl.Interaction): - def __init__(self, config: Config, statusbar: Optional['Statusbar'] = None): + def __init__(self, config: Config, statusbar: Optional["Statusbar"] = None): super().__init__(config, statusbar) def confirm(self, q: str) -> bool: @@ -328,7 +342,9 @@ def confirm(self, q: str) -> bool: return reply.lower() in (_("y"), _("yes")) - def notify(self, s: str, n: int = 10, wait_for_keypress: bool = False) -> None: + def notify( + self, s: str, n: int = 10, wait_for_keypress: bool = False + ) -> None: if self.statusbar: self.statusbar.message(s, n) @@ -342,12 +358,12 @@ def file_prompt(self, s: str) -> Optional[str]: class CLIRepl(repl.Repl): def __init__( - self, - scr: curses.window, - interp: repl.Interpreter, - statusbar: 'Statusbar', - config: Config, - idle: Optional[Callable] = None + self, + scr: curses.window, + interp: repl.Interpreter, + statusbar: "Statusbar", + config: Config, + idle: Optional[Callable] = None, ): super().__init__(interp, config) self.interp.writetb = self.writetb @@ -412,10 +428,10 @@ def bs(self, delete_tabs: bool = True) -> int: # type: ignore[return-value] y, x = self.scr.getyx() if not self.s: - return None # type: ignore[return-value] + return None # type: ignore[return-value] if x == self.ix and y == self.iy: - return None # type: ignore[return-value] + return None # type: ignore[return-value] n = 1 @@ -727,10 +743,10 @@ def lf(self) -> None: self.echo("\n") def mkargspec( - self, - topline: Any, # Named tuples don't seem to play nice with mypy - in_arg: Union[str, int], - down: bool + self, + topline: Any, # Named tuples don't seem to play nice with mypy + in_arg: Union[str, int], + down: bool, ) -> int: """This figures out what to do with the argspec and puts it nicely into the list window. It returns the number of lines used to display the @@ -1062,7 +1078,9 @@ def p_key(self, key: str) -> Union[None, str, bool]: return True - def print_line(self, s: Optional[str], clr: bool = False, newline: bool = False) -> None: + def print_line( + self, s: Optional[str], clr: bool = False, newline: bool = False + ) -> None: """Chuck a line of text through the highlighter, move the cursor to the beginning of the line and output it to the screen.""" @@ -1098,10 +1116,7 @@ def print_line(self, s: Optional[str], clr: bool = False, newline: bool = False) self.mvc(1) self.cpos = t - def prompt( - self, - more: Any # I'm not sure of the type on this one - ) -> None: + def prompt(self, more: Any) -> None: # I'm not sure of the type on this one """Show the appropriate Python prompt""" if not more: self.echo( @@ -1193,7 +1208,9 @@ def repl(self) -> Tuple[Any, ...]: self.s = "" return self.exit_value - def reprint_line(self, lineno: int, tokens: MutableMapping[_TokenType, str]) -> None: + def reprint_line( + self, lineno: int, tokens: MutableMapping[_TokenType, str] + ) -> None: """Helper function for paren highlighting: Reprint line at offset `lineno` in current input buffer.""" if not self.buffer or lineno == len(self.buffer): @@ -1291,12 +1308,12 @@ def write(self, s: str) -> None: self.screen_hist.append(s.rstrip()) def show_list( - self, - items: List[str], - arg_pos: Union[str, int], - topline: Any = None, # Named tuples don't play nice with mypy - formatter: Optional[Callable] = None, - current_item: Optional[bool] = None + self, + items: List[str], + arg_pos: Union[str, int], + topline: Any = None, # Named tuples don't play nice with mypy + formatter: Optional[Callable] = None, + current_item: Optional[bool] = None, ) -> None: shared = ShowListState() y, x = self.scr.getyx() @@ -1591,16 +1608,18 @@ class Statusbar: def __init__( self, - scr: curses.window, - pwin: curses.window, - background: int, - config: Config, - s: Optional[str] = None, - c: Optional[int] = None + scr: curses.window, + pwin: curses.window, + background: int, + config: Config, + s: Optional[str] = None, + c: Optional[int] = None, ): """Initialise the statusbar and display the initial text (if any)""" self.size() - self.win: curses.window = newwin(background, self.h, self.w, self.y, self.x) + self.win: curses.window = newwin( + background, self.h, self.w, self.y, self.x + ) self.config = config @@ -1724,7 +1743,9 @@ def clear(self) -> None: self.win.clear() -def init_wins(scr: curses.window, config: Config) -> Tuple[curses.window, Statusbar]: +def init_wins( + scr: curses.window, config: Config +) -> Tuple[curses.window, Statusbar]: """Initialise the two windows (the main repl interface and the little status bar at the bottom with some stuff in it)""" # TODO: Document better what stuff is on the status bar. @@ -1788,10 +1809,16 @@ def gethw() -> Tuple[int, int]: if platform.system() != "Windows": h, w = struct.unpack( - "hhhh", fcntl.ioctl(sys.__stdout__, termios.TIOCGWINSZ, "\000" * 8) # type:ignore[call-overload] + "hhhh", + fcntl.ioctl( + sys.__stdout__, termios.TIOCGWINSZ, "\000" * 8 + ), # type:ignore[call-overload] )[0:2] else: - from ctypes import windll, create_string_buffer # type:ignore[attr-defined] + from ctypes import ( + windll, + create_string_buffer, + ) # type:ignore[attr-defined] # stdin handle is -10 # stdout handle is -11 @@ -1916,12 +1943,12 @@ def curses_wrapper(func: Callable, *args: Any, **kwargs: Any) -> Any: def main_curses( - scr: curses.window, - args: List[str], - config: Config, - interactive: bool = True, - locals_: Optional[MutableMapping[str, str]] = None, - banner: Optional[str] = None + scr: curses.window, + args: List[str], + config: Config, + interactive: bool = True, + locals_: Optional[MutableMapping[str, str]] = None, + banner: Optional[str] = None, ) -> Tuple[Tuple[Any, ...], str]: """main function for the curses convenience wrapper @@ -2031,9 +2058,9 @@ def main_curses( def main( - args: Optional[List[str]] = None, - locals_: Optional[MutableMapping[str, str]] = None, - banner: Optional[str] = None + args: Optional[List[str]] = None, + locals_: Optional[MutableMapping[str, str]] = None, + banner: Optional[str] = None, ) -> Any: translations.init() From 2ae5c4510ddfd7e6852a429c33fa149e38957754 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Wed, 16 Mar 2022 21:54:27 -0500 Subject: [PATCH 343/555] Done with cli.py, working on repl.py --- bpython/cli.py | 44 +++++++++++++++++++++-------- bpython/repl.py | 74 +++++++++++++++++++++++++------------------------ 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index ed885669b..c88dac970 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -66,6 +66,8 @@ Optional, Union, Tuple, + Collection, + Dict ) import unicodedata from dataclasses import dataclass @@ -287,7 +289,7 @@ def get_colpair(config: Config, name: str) -> int: return curses.color_pair(get_color(config, name) + 1) -def make_colors(config: Config) -> MutableMapping[str, int]: +def make_colors(config: Config) -> Dict[str, int]: """Init all the colours in curses and bang them into a dictionary""" # blacK, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default: @@ -366,8 +368,10 @@ def __init__( idle: Optional[Callable] = None, ): super().__init__(interp, config) - self.interp.writetb = self.writetb - self.scr = scr + # mypy doesn't quite understand the difference between a class variable with a callable type and a method. + # https://github.com/python/mypy/issues/2427 + self.interp.writetb = self.writetb # type:ignore[assignment] + self.scr: curses.window = scr self.stdout_hist = "" # native str (bytes in Py2, unicode in Py3) self.list_win = newwin(get_colpair(config, "background"), 1, 1, 1, 1) self.cpos = 0 @@ -382,6 +386,10 @@ def __init__( self.statusbar = statusbar self.formatter = BPythonFormatter(config.color_scheme) self.interact = CLIInteraction(self.config, statusbar=self.statusbar) + self.ix: int + self.iy: int + self.arg_pos: Union[str, int, None] + self.prev_block_finished: int if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1: config.cli_suggestion_width = 0.8 @@ -505,13 +513,18 @@ def complete(self, tab: bool = False) -> None: return list_win_visible = repl.Repl.complete(self, tab) + + f = None + if self.matches_iter.completer: + f = self.matches_iter.completer.format + if list_win_visible: try: self.show_list( self.matches_iter.matches, self.arg_pos, topline=self.funcprops, - formatter=self.matches_iter.completer.format, + formatter=f, ) except curses.error: # XXX: This is a massive hack, it will go away when I get @@ -745,7 +758,7 @@ def lf(self) -> None: def mkargspec( self, topline: Any, # Named tuples don't seem to play nice with mypy - in_arg: Union[str, int], + in_arg: Union[str, int, None], down: bool, ) -> int: """This figures out what to do with the argspec and puts it nicely into @@ -1310,11 +1323,12 @@ def write(self, s: str) -> None: def show_list( self, items: List[str], - arg_pos: Union[str, int], + arg_pos: Union[str, int, None], topline: Any = None, # Named tuples don't play nice with mypy formatter: Optional[Callable] = None, - current_item: Optional[bool] = None, + current_item: Optional[str] = None, ) -> None: + v_items: Collection shared = ShowListState() y, x = self.scr.getyx() h, w = self.scr.getmaxyx() @@ -1475,6 +1489,10 @@ def tab(self, back: bool = False) -> bool: and don't indent if there are only whitespace in the line. """ + f = None + if self.matches_iter.completer: + f = self.matches_iter.completer.format + # 1. check if we should add a tab character if self.atbol() and not back: x_pos = len(self.s) - self.cpos @@ -1505,15 +1523,16 @@ def tab(self, back: bool = False) -> bool: # 4. swap current word for a match list item elif self.matches_iter.matches: - current_match = ( - back and self.matches_iter.previous() or next(self.matches_iter) + n: str = next(self.matches_iter) + current_match: Optional[str] = ( + back and self.matches_iter.previous() or n ) try: self.show_list( self.matches_iter.matches, self.arg_pos, topline=self.funcprops, - formatter=self.matches_iter.completer.format, + formatter=f, current_item=current_match, ) except curses.error: @@ -1815,10 +1834,11 @@ def gethw() -> Tuple[int, int]: ), # type:ignore[call-overload] )[0:2] else: - from ctypes import ( + # Ignoring mypy's windll error because it's Windows-specific + from ctypes import ( # type:ignore[attr-defined] windll, create_string_buffer, - ) # type:ignore[attr-defined] + ) # stdin handle is -10 # stdout handle is -11 diff --git a/bpython/repl.py b/bpython/repl.py index 4ee4eebae..f2e599edd 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -37,11 +37,11 @@ from itertools import takewhile from pathlib import Path from types import ModuleType, TracebackType -from typing import cast, List, Tuple, Any, Optional, Type +from typing import cast, List, Tuple, Any, Optional, Type, Union, MutableMapping, Callable, Dict from ._typing_compat import Literal from pygments.lexers import Python3Lexer -from pygments.token import Token +from pygments.token import Token, _TokenType have_pyperclip = True try: @@ -50,7 +50,8 @@ have_pyperclip = False from . import autocomplete, inspection, simpleeval -from .config import getpreferredencoding +from .cli import Statusbar +from .config import getpreferredencoding, Config from .formatter import Parenthesis from .history import History from .lazyre import LazyReCompile @@ -92,7 +93,7 @@ class Interpreter(code.InteractiveInterpreter): bpython_input_re = LazyReCompile(r"") - def __init__(self, locals=None, encoding=None): + def __init__(self, locals: Optional[MutableMapping[str, str]] = None, encoding: Optional[str] = None): """Constructor. The optional 'locals' argument specifies the dictionary in which code @@ -114,7 +115,7 @@ def __init__(self, locals=None, encoding=None): """ self.encoding = encoding or getpreferredencoding() - self.syntaxerror_callback = None + self.syntaxerror_callback: Optional[Callable] = None if locals is None: # instead of messing with sys.modules, we should modify sys.modules @@ -349,7 +350,7 @@ def clear(self) -> None: class Interaction: - def __init__(self, config, statusbar=None): + def __init__(self, config: Config, statusbar: Optional[Statusbar] = None): self.config = config if statusbar: @@ -402,7 +403,7 @@ class Repl: XXX Subclasses should implement echo, current_line, cw """ - def __init__(self, interp, config): + def __init__(self, interp: Interpreter, config: Config): """Initialise the repl. interp is a Python code.InteractiveInterpreter instance @@ -412,7 +413,7 @@ def __init__(self, interp, config): self.config = config self.cut_buffer = "" - self.buffer = [] + self.buffer: List[str] = [] self.interp = interp self.interp.syntaxerror_callback = self.clear_current_line self.match = False @@ -421,17 +422,17 @@ def __init__(self, interp, config): ) # all input and output, stored as old style format strings # (\x01, \x02, ...) for cli.py - self.screen_hist = [] - self.history = [] # commands executed since beginning of session - self.redo_stack = [] + self.screen_hist: List[str] = [] + self.history: List[str] = [] # commands executed since beginning of session + self.redo_stack: List[str] = [] self.evaluating = False self.matches_iter = MatchesIterator() self.funcprops = None - self.arg_pos = None + self.arg_pos: Union[str, int, None] = None self.current_func = None self.highlighted_paren = None - self._C = {} - self.prev_block_finished = 0 + self._C: Dict[str, int] = {} + self.prev_block_finished: int = 0 self.interact = Interaction(self.config) # previous pastebin content to prevent duplicate pastes, filled on call # to repl.pastebin @@ -441,11 +442,12 @@ def __init__(self, interp, config): # Necessary to fix mercurial.ui.ui expecting sys.stderr to have this # attribute self.closed = False + self.paster: Union[PasteHelper, PastePinnwand] if self.config.hist_file.exists(): try: self.rl_history.load( - self.config.hist_file, getpreferredencoding() or "ascii" + str(self.config.hist_file), getpreferredencoding() or "ascii" ) except OSError: pass @@ -472,7 +474,7 @@ def ps1(self) -> str: def ps2(self) -> str: return cast(str, getattr(sys, "ps2", "... ")) - def startup(self): + def startup(self) -> None: """ Execute PYTHONSTARTUP file if it exits. Call this after front end-specific initialisation. @@ -642,7 +644,7 @@ def get_args(self): self.arg_pos = None return False - def get_source_of_current_name(self): + def get_source_of_current_name(self) -> str: """Return the unicode source code of the object which is bound to the current name in the current input line. Throw `SourceNotFound` if the source cannot be found.""" @@ -692,7 +694,7 @@ def set_docstring(self): # 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=False): + def complete(self, tab: bool = False) -> Optional[bool]: """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. @@ -743,7 +745,7 @@ def complete(self, tab=False): else: return tab or completer.shown_before_tab - def format_docstring(self, docstring, width, height): + def format_docstring(self, docstring: str, width: int, height: int) -> str: """Take a string and try to format it into a sane list of strings to be put into the suggestion box.""" @@ -763,7 +765,7 @@ def format_docstring(self, docstring, width, height): out[-1] = out[-1].rstrip() return out - def next_indentation(self): + def next_indentation(self) -> int: """Return the indentation of the next line based on the current input buffer.""" if self.buffer: @@ -804,7 +806,7 @@ def process(): return "\n".join(process()) - def write2file(self): + def write2file(self) -> None: """Prompt for a filename and write the current contents of the stdout buffer to disk.""" @@ -851,7 +853,7 @@ def write2file(self): else: self.interact.notify(_("Saved to %s.") % (fn,)) - def copy2clipboard(self): + def copy2clipboard(self) -> None: """Copy current content to clipboard.""" if not have_pyperclip: @@ -866,7 +868,7 @@ def copy2clipboard(self): else: self.interact.notify(_("Copied content to clipboard.")) - def pastebin(self, s=None): + def pastebin(self, s=None) -> Optional[str]: """Upload to a pastebin and display the URL in the status bar.""" if s is None: @@ -879,7 +881,7 @@ def pastebin(self, s=None): else: return self.do_pastebin(s) - def do_pastebin(self, s): + def do_pastebin(self, s) -> Optional[str]: """Actually perform the upload.""" if s == self.prev_pastebin_content: self.interact.notify( @@ -911,7 +913,7 @@ def do_pastebin(self, s): return paste_url - def push(self, s, insert_into_history=True): + def push(self, s, 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 @@ -936,7 +938,7 @@ def insert_into_history(self, s): except RuntimeError as e: self.interact.notify(f"{e}") - def prompt_undo(self): + def prompt_undo(self) -> int: """Returns how many lines to undo, 0 means don't undo""" if ( self.config.single_undo_time < 0 @@ -944,14 +946,14 @@ def prompt_undo(self): ): return 1 est = self.interp.timer.estimate() - n = self.interact.file_prompt( + m = self.interact.file_prompt( _("Undo how many lines? (Undo will take up to ~%.1f seconds) [1]") % (est,) ) try: - if n == "": - n = "1" - n = int(n) + if m == "": + m = "1" + n = int(m) except ValueError: self.interact.notify(_("Undo canceled"), 0.1) return 0 @@ -968,7 +970,7 @@ def prompt_undo(self): self.interact.notify(message % (n, est), 0.1) return n - def undo(self, n=1): + def undo(self, n: int = 1) -> None: """Go back in the undo history n steps and call reevaluate() Note that in the program this is called "Rewind" because I want it to be clear that this is by no means a true undo @@ -992,7 +994,7 @@ def undo(self, n=1): self.rl_history.entries = entries - def flush(self): + def flush(self) -> None: """Olivier Grisel brought it to my attention that the logging module tries to call this method, since it makes assumptions about stdout that may not necessarily be true. The docs for @@ -1009,7 +1011,7 @@ def flush(self): def close(self): """See the flush() method docstring.""" - def tokenize(self, s, newline=False): + 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 @@ -1107,11 +1109,11 @@ def tokenize(self, s, newline=False): return list() return line_tokens - def clear_current_line(self): + def clear_current_line(self) -> None: """This is used as the exception callback for the Interpreter instance. It prevents autoindentation from occurring after a traceback.""" - def send_to_external_editor(self, text): + def send_to_external_editor(self, text: str) -> str: """Returns modified text from an editor, or the original text if editor exited with non-zero""" @@ -1169,7 +1171,7 @@ def edit_config(self): self.interact.notify(_("Error editing config file: %s") % e) -def next_indentation(line, tab_length): +def next_indentation(line, tab_length) -> int: """Given a code line, return the indentation of the next line.""" line = line.expandtabs(tab_length) indentation = (len(line) - len(line.lstrip(" "))) // tab_length From c2f1c3919606e80d5376865a4cb1beefe69d0080 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Wed, 23 Mar 2022 20:49:33 -0500 Subject: [PATCH 344/555] Mostly done, but have some questions about the missing attributes in repl.py --- bpython/cli.py | 3 +-- bpython/repl.py | 60 +++++++++++++++++++++++++++---------------------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index c88dac970..f129ba761 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -352,8 +352,7 @@ def notify( def file_prompt(self, s: str) -> Optional[str]: if self.statusbar: - # This thows a mypy error because repl.py isn't typed yet - return self.statusbar.prompt(s) # type:ignore[no-any-return] + return self.statusbar.prompt(s) else: return None diff --git a/bpython/repl.py b/bpython/repl.py index f2e599edd..f13d487a6 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -430,7 +430,7 @@ def __init__(self, interp: Interpreter, config: Config): self.funcprops = None self.arg_pos: Union[str, int, None] = None self.current_func = None - self.highlighted_paren = None + self.highlighted_paren: Optional[Tuple[Any, List[Tuple[_TokenType, str]]]] = None self._C: Dict[str, int] = {} self.prev_block_finished: int = 0 self.interact = Interaction(self.config) @@ -649,7 +649,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 = self.current_func + obj: Optional[Callable] = self.current_func try: if obj is None: line = self.current_line @@ -657,7 +657,8 @@ def get_source_of_current_name(self) -> str: raise SourceNotFound(_("Nothing to get source of")) if inspection.is_eval_safe_name(line): obj = self.get_object(line) - return inspect.getsource(obj) + # Ignoring the next mypy error because we want this to fail if obj is None + return inspect.getsource(obj) # type:ignore[arg-type] except (AttributeError, NameError) as e: msg = _("Cannot get source: %s") % (e,) except OSError as e: @@ -724,28 +725,31 @@ def complete(self, tab: bool = False) -> Optional[bool]: self.matches_iter.clear() return bool(self.funcprops) - self.matches_iter.update( - self.cursor_offset, self.current_line, matches, completer - ) + if completer: + self.matches_iter.update( + self.cursor_offset, self.current_line, matches, completer + ) - if len(matches) == 1: - if tab: - # if this complete is being run for a tab key press, substitute - # common sequence - ( - self._cursor_offset, - self._current_line, - ) = self.matches_iter.substitute_cseq() - return Repl.complete(self) # again for - elif self.matches_iter.current_word == matches[0]: - self.matches_iter.clear() - return False - return completer.shown_before_tab + if len(matches) == 1: + if tab: + # if this complete is being run for a tab key press, substitute + # common sequence + ( + self._cursor_offset, + self._current_line, + ) = self.matches_iter.substitute_cseq() + return Repl.complete(self) # again for + elif self.matches_iter.current_word == matches[0]: + self.matches_iter.clear() + return False + return completer.shown_before_tab + else: + return tab or completer.shown_before_tab else: - return tab or completer.shown_before_tab + return False - def format_docstring(self, docstring: str, width: int, height: int) -> str: + def format_docstring(self, docstring: str, width: int, height: int) -> List[str]: """Take a string and try to format it into a sane list of strings to be put into the suggestion box.""" @@ -878,11 +882,13 @@ def pastebin(self, s=None) -> Optional[str]: _("Pastebin buffer? (y/N) ") ): self.interact.notify(_("Pastebin aborted.")) + return None else: return self.do_pastebin(s) def do_pastebin(self, s) -> Optional[str]: """Actually perform the upload.""" + paste_url: str if s == self.prev_pastebin_content: self.interact.notify( _("Duplicate pastebin. Previous URL: %s. " "Removal URL: %s") @@ -896,7 +902,7 @@ def do_pastebin(self, s) -> Optional[str]: paste_url, removal_url = self.paster.paste(s) except PasteFailed as e: self.interact.notify(_("Upload failed: %s") % e) - return + return None self.prev_pastebin_content = s self.prev_pastebin_url = paste_url @@ -923,7 +929,7 @@ def push(self, s, insert_into_history=True) -> bool: if insert_into_history: self.insert_into_history(s) - more = self.interp.runsource("\n".join(self.buffer)) + more: bool = self.interp.runsource("\n".join(self.buffer)) if not more: self.buffer = [] @@ -1028,7 +1034,7 @@ def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: cursor = len(source) - self.cpos if self.cpos: cursor += 1 - stack = 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 @@ -1037,8 +1043,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() - saved_tokens = 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) @@ -1174,7 +1180,7 @@ def edit_config(self): def next_indentation(line, tab_length) -> int: """Given a code line, return the indentation of the next line.""" line = line.expandtabs(tab_length) - indentation = (len(line) - len(line.lstrip(" "))) // tab_length + indentation: int = (len(line) - len(line.lstrip(" "))) // tab_length if line.rstrip().endswith(":"): indentation += 1 elif indentation >= 1: From 2de72b30131ced90bc11e818fb20725632969269 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Wed, 23 Mar 2022 21:02:28 -0500 Subject: [PATCH 345/555] Ran black and codespell --- bpython/cli.py | 4 ++-- bpython/repl.py | 34 ++++++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index f129ba761..d081e9405 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -67,7 +67,7 @@ Union, Tuple, Collection, - Dict + Dict, ) import unicodedata from dataclasses import dataclass @@ -2058,7 +2058,7 @@ def main_curses( exit_value = clirepl.repl() if hasattr(sys, "exitfunc"): - # Seems like the if statment should satisfy mypy, but it doesn't + # Seems like the if statement should satisfy mypy, but it doesn't sys.exitfunc() # type:ignore[attr-defined] delattr(sys, "exitfunc") diff --git a/bpython/repl.py b/bpython/repl.py index f13d487a6..31294bde7 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -37,7 +37,18 @@ from itertools import takewhile from pathlib import Path from types import ModuleType, TracebackType -from typing import cast, List, Tuple, Any, Optional, Type, Union, MutableMapping, Callable, Dict +from typing import ( + cast, + List, + Tuple, + Any, + Optional, + Type, + Union, + MutableMapping, + Callable, + Dict, +) from ._typing_compat import Literal from pygments.lexers import Python3Lexer @@ -93,7 +104,11 @@ class Interpreter(code.InteractiveInterpreter): bpython_input_re = LazyReCompile(r"") - def __init__(self, locals: Optional[MutableMapping[str, str]] = None, encoding: Optional[str] = None): + def __init__( + self, + locals: Optional[MutableMapping[str, str]] = None, + encoding: Optional[str] = None, + ): """Constructor. The optional 'locals' argument specifies the dictionary in which code @@ -423,14 +438,18 @@ 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.history: List[str] = [] # commands executed since beginning of session + self.history: List[ + str + ] = [] # commands executed since beginning of session 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]]]] = None + self.highlighted_paren: Optional[ + Tuple[Any, List[Tuple[_TokenType, str]]] + ] = None self._C: Dict[str, int] = {} self.prev_block_finished: int = 0 self.interact = Interaction(self.config) @@ -447,7 +466,8 @@ def __init__(self, interp: Interpreter, config: Config): if self.config.hist_file.exists(): try: self.rl_history.load( - str(self.config.hist_file), getpreferredencoding() or "ascii" + str(self.config.hist_file), + getpreferredencoding() or "ascii", ) except OSError: pass @@ -749,7 +769,9 @@ def complete(self, tab: bool = False) -> Optional[bool]: else: return False - def format_docstring(self, docstring: str, width: int, height: int) -> List[str]: + def format_docstring( + self, docstring: str, width: int, height: int + ) -> List[str]: """Take a string and try to format it into a sane list of strings to be put into the suggestion box.""" From 7d4f80d6a37519a34cdcc3d591dc0d37280bda21 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 15 Apr 2022 13:14:41 -0400 Subject: [PATCH 346/555] good-enough solutions --- bpython/cli.py | 9 +++++++-- bpython/curtsiesfrontend/repl.py | 9 +++++++-- bpython/repl.py | 32 +++++++++++++++++++++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index d081e9405..6c15f5e2e 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -1101,7 +1101,12 @@ def print_line( if self.highlighted_paren is not None: # Clear previous highlighted paren - self.reprint_line(*self.highlighted_paren) + + lineno = self.highlighted_paren[0] + tokens = self.highlighted_paren[1] + # mypy thinks tokens is List[Tuple[_TokenType, str]] + # but it is supposed to be MutableMapping[_TokenType, str] + self.reprint_line(lineno, tokens) self.highlighted_paren = None if self.config.syntax and (not self.paste_mode or newline): @@ -1221,7 +1226,7 @@ def repl(self) -> Tuple[Any, ...]: return self.exit_value def reprint_line( - self, lineno: int, tokens: MutableMapping[_TokenType, str] + self, lineno: int, tokens: List[Tuple[_TokenType, str]] ) -> None: """Helper function for paren highlighting: Reprint line at offset `lineno` in current input buffer.""" diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 59dcd481d..0d318fd1b 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -13,9 +13,12 @@ import unicodedata from enum import Enum from types import TracebackType -from typing import Dict, Any, List, Optional, Tuple, Union, cast, Type +from typing import Dict, Any, List, Optional, Tuple, Union, cast, Type, TYPE_CHECKING from .._typing_compat import Literal +if TYPE_CHECKING: + from ..repl import Interpreter + import blessings import greenlet from curtsies import ( @@ -357,7 +360,9 @@ def __init__( ) self.edit_keys = edit_keys.mapping_with_config(config, key_dispatch) logger.debug("starting parent init") - super().__init__(interp, config) + # interp is a subclass of repl.Interpreter, so it definitely, + # implements the methods of Interpreter! + super().__init__(cast('Interpreter', interp), config) self.formatter = BPythonFormatter(config.color_scheme) diff --git a/bpython/repl.py b/bpython/repl.py index 31294bde7..1f4b6fc6e 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -48,6 +48,7 @@ MutableMapping, Callable, Dict, + TYPE_CHECKING, ) from ._typing_compat import Literal @@ -61,7 +62,10 @@ have_pyperclip = False from . import autocomplete, inspection, simpleeval -from .cli import Statusbar + +if TYPE_CHECKING: + from .cli import Statusbar + from .config import getpreferredencoding, Config from .formatter import Parenthesis from .history import History @@ -365,7 +369,7 @@ def clear(self) -> None: class Interaction: - def __init__(self, config: Config, statusbar: Optional[Statusbar] = None): + def __init__(self, config: Config, statusbar: Optional['Statusbar'] = None): self.config = config if statusbar: @@ -418,6 +422,29 @@ class Repl: XXX Subclasses should implement echo, current_line, cw """ + @abstractmethod + @property + def current_line(self): + pass + + @abstractmethod + @property + def cursor_offset(self): + pass + + @abstractmethod + def reevaluate(self): + pass + + @abstractmethod + def reprint_line( + self, lineno: int, tokens: List[Tuple[_TokenType, str]] + ) -> None: + pass + + # not actually defined, subclasses must define + cpos: int + def __init__(self, interp: Interpreter, config: Config): """Initialise the repl. @@ -425,7 +452,6 @@ def __init__(self, interp: Interpreter, config: Config): config is a populated bpython.config.Struct. """ - self.config = config self.cut_buffer = "" self.buffer: List[str] = [] From d1891d279879c48b84498bead4e78f8629a45e7e Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 15 Apr 2022 12:23:37 -0500 Subject: [PATCH 347/555] formatted with black --- bpython/curtsiesfrontend/repl.py | 14 ++++++++++++-- bpython/repl.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 0d318fd1b..696e84bf7 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -13,7 +13,17 @@ import unicodedata from enum import Enum from types import TracebackType -from typing import Dict, Any, List, Optional, Tuple, Union, cast, Type, TYPE_CHECKING +from typing import ( + Dict, + Any, + List, + Optional, + Tuple, + Union, + cast, + Type, + TYPE_CHECKING, +) from .._typing_compat import Literal if TYPE_CHECKING: @@ -362,7 +372,7 @@ def __init__( logger.debug("starting parent init") # interp is a subclass of repl.Interpreter, so it definitely, # implements the methods of Interpreter! - super().__init__(cast('Interpreter', interp), config) + super().__init__(cast("Interpreter", interp), config) self.formatter = BPythonFormatter(config.color_scheme) diff --git a/bpython/repl.py b/bpython/repl.py index 1f4b6fc6e..19062a1ef 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -369,7 +369,7 @@ def clear(self) -> None: class Interaction: - def __init__(self, config: Config, statusbar: Optional['Statusbar'] = None): + def __init__(self, config: Config, statusbar: Optional["Statusbar"] = None): self.config = config if statusbar: From 1745ad5e2845751ad7fddd4b8327cfff1c015fc8 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 15 Apr 2022 12:39:57 -0500 Subject: [PATCH 348/555] hide properties under type checking --- bpython/repl.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 19062a1ef..bf9ee82c1 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -422,28 +422,29 @@ class Repl: XXX Subclasses should implement echo, current_line, cw """ - @abstractmethod - @property - def current_line(self): - pass + if TYPE_CHECKING: + @property + @abstractmethod + def current_line(self): + pass - @abstractmethod - @property - def cursor_offset(self): - pass + @property + @abstractmethod + def cursor_offset(self): + pass - @abstractmethod - def reevaluate(self): - pass + @abstractmethod + def reevaluate(self): + pass - @abstractmethod - def reprint_line( - self, lineno: int, tokens: List[Tuple[_TokenType, str]] - ) -> None: - pass + @abstractmethod + def reprint_line( + self, lineno: int, tokens: List[Tuple[_TokenType, str]] + ) -> None: + pass - # not actually defined, subclasses must define - cpos: int + # not actually defined, subclasses must define + cpos: int def __init__(self, interp: Interpreter, config: Config): """Initialise the repl. From a832ea35de4de1e00b8de6903b722c27df1b3def Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 15 Apr 2022 16:58:44 -0500 Subject: [PATCH 349/555] Found and fixed the mock error, still need to address the matches_iter error --- bpython/cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 6c15f5e2e..b1bff1269 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -68,6 +68,7 @@ Tuple, Collection, Dict, + Literal, ) import unicodedata from dataclasses import dataclass @@ -1330,7 +1331,7 @@ def show_list( arg_pos: Union[str, int, None], topline: Any = None, # Named tuples don't play nice with mypy formatter: Optional[Callable] = None, - current_item: Optional[str] = None, + current_item: Union[str, Literal[False]] = None, ) -> None: v_items: Collection shared = ShowListState() @@ -1527,9 +1528,8 @@ def tab(self, back: bool = False) -> bool: # 4. swap current word for a match list item elif self.matches_iter.matches: - n: str = next(self.matches_iter) - current_match: Optional[str] = ( - back and self.matches_iter.previous() or n + current_match: Union[str, Literal[False]] = ( + back and self.matches_iter.previous() or next(self.matches_iter) ) try: self.show_list( From 6f4b3c2e5a6f8eb0659d44e9f31578147d975dc7 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 15 Apr 2022 20:18:02 -0500 Subject: [PATCH 350/555] Fixed pytest errors --- bpython/cli.py | 16 ++++++++-------- bpython/repl.py | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index b1bff1269..db090cca2 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -514,12 +514,12 @@ def complete(self, tab: bool = False) -> None: list_win_visible = repl.Repl.complete(self, tab) - f = None - if self.matches_iter.completer: - f = self.matches_iter.completer.format - if list_win_visible: try: + f = None + if self.matches_iter.completer: + f = self.matches_iter.completer.format + self.show_list( self.matches_iter.matches, self.arg_pos, @@ -1494,10 +1494,6 @@ def tab(self, back: bool = False) -> bool: and don't indent if there are only whitespace in the line. """ - f = None - if self.matches_iter.completer: - f = self.matches_iter.completer.format - # 1. check if we should add a tab character if self.atbol() and not back: x_pos = len(self.s) - self.cpos @@ -1532,6 +1528,10 @@ def tab(self, back: bool = False) -> bool: back and self.matches_iter.previous() or next(self.matches_iter) ) try: + f = None + if self.matches_iter.completer: + f = self.matches_iter.completer.format + self.show_list( self.matches_iter.matches, self.arg_pos, diff --git a/bpython/repl.py b/bpython/repl.py index bf9ee82c1..2da6a9a13 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -423,6 +423,7 @@ class Repl: """ if TYPE_CHECKING: + @property @abstractmethod def current_line(self): From bcd3c31a22ee1e73d89c9f1117b14ec979312170 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Fri, 15 Apr 2022 20:25:57 -0500 Subject: [PATCH 351/555] Imported Literal using _typing_compat instead of typing --- bpython/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/cli.py b/bpython/cli.py index db090cca2..4f9b50c83 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -68,8 +68,8 @@ Tuple, Collection, Dict, - Literal, ) +from ._typing_compat import Literal import unicodedata from dataclasses import dataclass From d8d68d20018a55c8062e8d5a0468a2e6c0980fd0 Mon Sep 17 00:00:00 2001 From: Ben-Reg <3612364+Ben-Reg@users.noreply.github.com> Date: Mon, 18 Apr 2022 12:16:59 -0500 Subject: [PATCH 352/555] Changed type hinting on curses windows to be compatible with python 3.7 --- bpython/cli.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 4f9b50c83..8bace509a 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -68,7 +68,12 @@ Tuple, Collection, Dict, + TYPE_CHECKING, ) + +if TYPE_CHECKING: + from _curses import _CursesWindow + from ._typing_compat import Literal import unicodedata from dataclasses import dataclass @@ -361,7 +366,7 @@ def file_prompt(self, s: str) -> Optional[str]: class CLIRepl(repl.Repl): def __init__( self, - scr: curses.window, + scr: "_CursesWindow", interp: repl.Interpreter, statusbar: "Statusbar", config: Config, @@ -371,7 +376,7 @@ def __init__( # mypy doesn't quite understand the difference between a class variable with a callable type and a method. # https://github.com/python/mypy/issues/2427 self.interp.writetb = self.writetb # type:ignore[assignment] - self.scr: curses.window = scr + self.scr: "_CursesWindow" = scr self.stdout_hist = "" # native str (bytes in Py2, unicode in Py3) self.list_win = newwin(get_colpair(config, "background"), 1, 1, 1, 1) self.cpos = 0 @@ -1631,8 +1636,8 @@ class Statusbar: def __init__( self, - scr: curses.window, - pwin: curses.window, + scr: "_CursesWindow", + pwin: "_CursesWindow", background: int, config: Config, s: Optional[str] = None, @@ -1640,7 +1645,7 @@ def __init__( ): """Initialise the statusbar and display the initial text (if any)""" self.size() - self.win: curses.window = newwin( + self.win: "_CursesWindow" = newwin( background, self.h, self.w, self.y, self.x ) @@ -1767,8 +1772,8 @@ def clear(self) -> None: def init_wins( - scr: curses.window, config: Config -) -> Tuple[curses.window, Statusbar]: + scr: "_CursesWindow", config: Config +) -> Tuple["_CursesWindow", Statusbar]: """Initialise the two windows (the main repl interface and the little status bar at the bottom with some stuff in it)""" # TODO: Document better what stuff is on the status bar. @@ -1803,12 +1808,12 @@ def init_wins( return main_win, statusbar -def sigwinch(unused_scr: curses.window) -> None: +def sigwinch(unused_scr: "_CursesWindow") -> None: global DO_RESIZE DO_RESIZE = True -def sigcont(unused_scr: curses.window) -> None: +def sigcont(unused_scr: "_CursesWindow") -> None: sigwinch(unused_scr) # Forces the redraw curses.ungetch("\x00") @@ -1933,7 +1938,7 @@ def __getitem__(self, k: Any) -> int: return self._val -def newwin(background: int, *args: int) -> curses.window: +def newwin(background: int, *args: int) -> "_CursesWindow": """Wrapper for curses.newwin to automatically set background colour on any newly created window.""" win = curses.newwin(*args) @@ -1967,7 +1972,7 @@ def curses_wrapper(func: Callable, *args: Any, **kwargs: Any) -> Any: def main_curses( - scr: curses.window, + scr: "_CursesWindow", args: List[str], config: Config, interactive: bool = True, From 9e0feb33fc5c1626ba19dbfba552bb669203c548 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 3 May 2022 00:32:15 +0200 Subject: [PATCH 353/555] Fix some type annotatations --- bpython/curtsies.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 7ffe90109..82adc15f0 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -55,9 +55,9 @@ class FullCurtsiesRepl(BaseRepl): def __init__( self, config: Config, - locals_: Optional[Dict[str, Any]], - banner: Optional[str], - interp: code.InteractiveInterpreter = None, + locals_: Optional[Dict[str, Any]] = None, + banner: Optional[str] = None, + interp: Optional[Interp] = None, ) -> None: self.input_generator = curtsies.input.Input( keynames="curtsies", sigint_event=True, paste_threshold=None @@ -182,10 +182,10 @@ def mainloop( def main( - args: List[str] = None, - locals_: Dict[str, Any] = None, - banner: str = None, - welcome_message: str = None, + args: Optional[List[str]] = None, + locals_: Optional[Dict[str, Any]] = None, + banner: Optional[str] = None, + welcome_message: Optional[str] = None, ) -> Any: """ banner is displayed directly after the version information. From cca075a6346fda5177146ca6dbbbcde0a29ea3a9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 3 May 2022 00:42:06 +0200 Subject: [PATCH 354/555] Remove unused function --- bpython/cli.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 8bace509a..21d79ba31 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -123,24 +123,6 @@ class ShowListState: wl: int = 0 -def calculate_screen_lines( - tokens: MutableMapping[_TokenType, str], width: int, cursor: int = 0 -) -> int: - """Given a stream of tokens and a screen width plus an optional - initial cursor position, return the amount of needed lines on the - screen.""" - lines = 1 - pos = cursor - for (token, value) in tokens: - if token is Token.Text and value == "\n": - lines += 1 - else: - pos += len(value) - lines += pos // width - pos %= width - return lines - - def forward_if_not_current(func: F) -> F: @functools.wraps(func) def newfunc(self, *args, **kwargs): # type: ignore From 069a0a655facba02b74362da2684a2504d1d7fa6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 3 May 2022 00:42:18 +0200 Subject: [PATCH 355/555] Set to empty string if no removal URL is available --- bpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/repl.py b/bpython/repl.py index 2da6a9a13..a74fa206d 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -956,7 +956,7 @@ def do_pastebin(self, s) -> Optional[str]: self.prev_pastebin_content = s self.prev_pastebin_url = paste_url - self.prev_removal_url = removal_url + self.prev_removal_url = removal_url if removal_url is not None else "" if removal_url is not None: self.interact.notify( From a808521ad8301c237bde708d161d32daa49a7ec9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 00:11:32 +0200 Subject: [PATCH 356/555] Remove no longer used encoding --- bpython/cli.py | 2 +- bpython/curtsiesfrontend/interpreter.py | 3 +- bpython/repl.py | 44 +++++++------------------ bpython/test/test_interpreter.py | 11 ++----- bpython/urwid.py | 2 +- 5 files changed, 16 insertions(+), 46 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 21d79ba31..3291be090 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -2005,7 +2005,7 @@ def main_curses( curses.raw(True) main_win, statusbar = init_wins(scr, config) - interpreter = repl.Interpreter(locals_, getpreferredencoding()) + interpreter = repl.Interpreter(locals_) clirepl = CLIRepl(main_win, interpreter, statusbar, config, idle) clirepl._C = cols diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 91dba96ae..1b3133459 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -63,14 +63,13 @@ class Interp(ReplInterpreter): def __init__( self, locals: Optional[Dict[str, Any]] = None, - encoding: Optional[str] = None, ) -> None: """Constructor. We include an argument for the outfile to pass to the formatter for it to write to. """ - super().__init__(locals, encoding) + super().__init__(locals) # typically changed after being instantiated # but used when interpreter used corresponding REPL diff --git a/bpython/repl.py b/bpython/repl.py index a74fa206d..1f318f8f4 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -110,9 +110,8 @@ class Interpreter(code.InteractiveInterpreter): def __init__( self, - locals: Optional[MutableMapping[str, str]] = None, - encoding: Optional[str] = None, - ): + locals: Optional[MutableMapping[str, Any]] = None, + ) -> None: """Constructor. The optional 'locals' argument specifies the dictionary in which code @@ -126,14 +125,8 @@ def __init__( callback can be added to the Interpreter instance afterwards - more specifically, this is so that autoindentation does not occur after a traceback. - - encoding is only used in Python 2, where it may be necessary to add an - encoding comment to a source bytestring before running it. - encoding must be a bytestring in Python 2 because it will be templated - into a bytestring source as part of an encoding comment. """ - self.encoding = encoding or getpreferredencoding() self.syntaxerror_callback: Optional[Callable] = None if locals is None: @@ -145,36 +138,21 @@ def __init__( super().__init__(locals) self.timer = RuntimeTimer() - def runsource(self, source, filename=None, symbol="single", encode="auto"): + def runsource( + self, + source: str, + filename: Optional[str] = None, + symbol: str = "single", + ) -> bool: """Execute Python code. source, filename and symbol are passed on to - code.InteractiveInterpreter.runsource. If encode is True, - an encoding comment will be added to the source. - On Python 3.X, encode will be ignored. - - encode should only be used for interactive interpreter input, - files should always already have an encoding comment or be ASCII. - By default an encoding line will be added if no filename is given. - - source must be a string - - Because adding an encoding comment to a unicode string in Python 2 - would cause a syntax error to be thrown which would reference code - the user did not write, setting encoding to True when source is a - unicode string in Python 2 will throw a ValueError.""" - if encode and filename is not None: - # files have encoding comments or implicit encoding of ASCII - if encode != "auto": - raise ValueError("shouldn't add encoding line to file contents") - encode = False + code.InteractiveInterpreter.runsource.""" if filename is None: filename = filename_for_console_input(source) with self.timer: - return code.InteractiveInterpreter.runsource( - self, source, filename, symbol - ) + return super().runsource(source, filename, symbol) def showsyntaxerror(self, filename=None): """Override the regular handler, the code's copied and pasted from @@ -532,7 +510,7 @@ def startup(self) -> None: encoding = inspection.get_encoding_file(filename) with open(filename, encoding=encoding) as f: source = f.read() - self.interp.runsource(source, filename, "exec", encode=False) + self.interp.runsource(source, filename, "exec") def current_string(self, concatenate=False): """If the line ends in a string get it, otherwise return ''""" diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 4c18f8fda..281ad5fa5 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -38,7 +38,7 @@ def test_syntaxerror(self): if (3, 10, 1) <= sys.version_info[:3]: expected = ( " File " - + green('""') + + green('""') + ", line " + bold(magenta("1")) + "\n 1.1.1.1\n ^^\n" @@ -50,7 +50,7 @@ def test_syntaxerror(self): elif (3, 10) <= sys.version_info[:2]: expected = ( " File " - + green('""') + + green('""') + ", line " + bold(magenta("1")) + "\n 1.1.1.1\n ^^^^^\n" @@ -129,13 +129,6 @@ def gfunc(): self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) self.assertEqual(plain("").join(a), expected) - def test_runsource_bytes_over_128_syntax_error_py3(self): - i = interpreter.Interp(encoding="latin-1") - i.showsyntaxerror = mock.Mock(return_value=None) - - i.runsource("a = b'\xfe'") - i.showsyntaxerror.assert_called_with(mock.ANY) - def test_getsource_works_on_interactively_defined_functions(self): source = "def foo(x):\n return x + 1\n" i = interpreter.Interp() diff --git a/bpython/urwid.py b/bpython/urwid.py index 33d1f4b8c..ec7038e80 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -1243,7 +1243,7 @@ def options_callback(group): extend_locals["service"] = serv reactor.callWhenRunning(serv.startService) exec_args = [] - interpreter = repl.Interpreter(locals_, locale.getpreferredencoding()) + interpreter = repl.Interpreter(locals_) # TODO: replace with something less hack-ish interpreter.locals.update(extend_locals) From 8caa915e2f0f7e96b5945c01443e418e1fb3494e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 00:27:08 +0200 Subject: [PATCH 357/555] Make Interaction an abstract base class --- bpython/cli.py | 18 +++++------ bpython/curtsiesfrontend/interaction.py | 4 +-- bpython/repl.py | 41 ++++++++++++++++++------- bpython/urwid.py | 7 ++++- 4 files changed, 45 insertions(+), 25 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 3291be090..5d8b0ed4b 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -319,14 +319,14 @@ def make_colors(config: Config) -> Dict[str, int]: class CLIInteraction(repl.Interaction): - def __init__(self, config: Config, statusbar: Optional["Statusbar"] = None): - super().__init__(config, statusbar) + def __init__(self, config: Config, statusbar: "Statusbar"): + super().__init__(config) + self.statusbar = statusbar def confirm(self, q: str) -> bool: """Ask for yes or no and return boolean""" try: - if self.statusbar: - reply = self.statusbar.prompt(q) + reply = self.statusbar.prompt(q) except ValueError: return False @@ -335,14 +335,10 @@ def confirm(self, q: str) -> bool: def notify( self, s: str, n: int = 10, wait_for_keypress: bool = False ) -> None: - if self.statusbar: - self.statusbar.message(s, n) + self.statusbar.message(s, n) def file_prompt(self, s: str) -> Optional[str]: - if self.statusbar: - return self.statusbar.prompt(s) - else: - return None + return self.statusbar.prompt(s) class CLIRepl(repl.Repl): @@ -1675,7 +1671,7 @@ def check(self) -> None: self.settext(self._s) - def message(self, s: str, n: int = 3) -> None: + def message(self, s: str, n: float = 3.0) -> None: """Display a message for a short n seconds on the statusbar and return it to its original state.""" self.timer = int(time.time() + n) diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index 79622d149..17b178de6 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -39,7 +39,7 @@ def __init__( self.prompt = "" self._message = "" self.message_start_time = time.time() - self.message_time = 3 + self.message_time = 3.0 self.permanent_stack = [] if permanent_text: self.permanent_stack.append(permanent_text) @@ -149,7 +149,7 @@ def should_show_message(self): return bool(self.current_line) # interaction interface - should be called from other greenlets - def notify(self, msg, n=3, wait_for_keypress=False): + def notify(self, msg, n=3.0, wait_for_keypress=False): self.request_context = greenlet.getcurrent() self.message_time = n self.message(msg, schedule_refresh=wait_for_keypress) diff --git a/bpython/repl.py b/bpython/repl.py index 1f318f8f4..f5c21def1 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -21,6 +21,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import abc import code import inspect import os @@ -346,21 +347,39 @@ def clear(self) -> None: self.index = -1 -class Interaction: - def __init__(self, config: Config, statusbar: Optional["Statusbar"] = None): +class Interaction(metaclass=abc.ABCMeta): + def __init__(self, config: Config): self.config = config - if statusbar: - self.statusbar = statusbar + @abc.abstractmethod + def confirm(self, s: str) -> bool: + pass - def confirm(self, s): - raise NotImplementedError + @abc.abstractmethod + def notify( + self, s: str, n: float = 10.0, wait_for_keypress: bool = False + ) -> None: + pass + + @abc.abstractmethod + def file_prompt(self, s: str) -> Optional[str]: + pass + + +class NoInteraction(Interaction): + def __init__(self, config: Config): + super().__init__(config) - def notify(self, s, n=10, wait_for_keypress=False): - raise NotImplementedError + def confirm(self, s: str) -> bool: + return False + + def notify( + self, s: str, n: float = 10.0, wait_for_keypress: bool = False + ) -> None: + pass - def file_prompt(self, s): - raise NotImplementedError + def file_prompt(self, s: str) -> Optional[str]: + return None class SourceNotFound(Exception): @@ -458,7 +477,7 @@ def __init__(self, interp: Interpreter, config: Config): ] = None self._C: Dict[str, int] = {} self.prev_block_finished: int = 0 - self.interact = Interaction(self.config) + self.interact: Interaction = NoInteraction(self.config) # previous pastebin content to prevent duplicate pastes, filled on call # to repl.pastebin self.prev_pastebin_content = "" diff --git a/bpython/urwid.py b/bpython/urwid.py index ec7038e80..0a2ffe31b 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -38,6 +38,7 @@ import locale import signal import urwid +from typing import Optional from . import args as bpargs, repl, translations from .formatter import theme_map @@ -526,7 +527,8 @@ def render(self, size, focus=False): class URWIDInteraction(repl.Interaction): def __init__(self, config, statusbar, frame): - super().__init__(config, statusbar) + super().__init__(config) + self.statusbar = statusbar self.frame = frame urwid.connect_signal(statusbar, "prompt_result", self._prompt_result) self.callback = None @@ -563,6 +565,9 @@ def _prompt_result(self, text): self.callback = None callback(text) + def file_prompt(self, s: str) -> Optional[str]: + raise NotImplementedError + class URWIDRepl(repl.Repl): From deb757d880791d56421216991cc949e40511b669 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 00:27:53 +0200 Subject: [PATCH 358/555] Add more type annotations and fix some mypy errors --- bpython/cli.py | 2 +- bpython/repl.py | 52 ++++++++++++++++++++++++------------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 5d8b0ed4b..559b63e7b 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -333,7 +333,7 @@ def confirm(self, q: str) -> bool: return reply.lower() in (_("y"), _("yes")) def notify( - self, s: str, n: int = 10, wait_for_keypress: bool = False + self, s: str, n: float = 10.0, wait_for_keypress: bool = False ) -> None: self.statusbar.message(s, n) diff --git a/bpython/repl.py b/bpython/repl.py index f5c21def1..8294f2c8e 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -39,6 +39,7 @@ from pathlib import Path from types import ModuleType, TracebackType from typing import ( + Iterable, cast, List, Tuple, @@ -155,7 +156,7 @@ def runsource( with self.timer: return super().runsource(source, filename, symbol) - def showsyntaxerror(self, filename=None): + def showsyntaxerror(self, filename: 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.""" @@ -182,7 +183,7 @@ def showsyntaxerror(self, filename=None): exc_formatted = traceback.format_exception_only(exc_type, value) self.writetb(exc_formatted) - def showtraceback(self): + def showtraceback(self) -> None: """This needs to override the default traceback thing so it can put it into a pretty colour and maybe other stuff, I don't know""" @@ -194,11 +195,10 @@ def showtraceback(self): tblist = traceback.extract_tb(tb) del tblist[:1] - for i, (fname, lineno, module, something) in enumerate(tblist): - # strip linecache line number - if self.bpython_input_re.match(fname): - fname = "" - tblist[i] = (fname, lineno, module, something) + for frame in tblist: + if self.bpython_input_re.match(frame.filename): + # strip linecache line number + frame.filename = "" l = traceback.format_list(tblist) if l: @@ -209,7 +209,7 @@ def showtraceback(self): self.writetb(l) - def writetb(self, lines): + def writetb(self, lines: Iterable[str]) -> None: """This outputs the traceback and should be overridden for anything fancy.""" for line in lines: @@ -463,9 +463,8 @@ 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.history: List[ - str - ] = [] # commands executed since beginning of session + # commands executed since beginning of session + self.history: List[str] = [] self.redo_stack: List[str] = [] self.evaluating = False self.matches_iter = MatchesIterator() @@ -870,25 +869,22 @@ def write2file(self) -> None: self.interact.notify(_("Save cancelled.")) return - fn = Path(fn).expanduser() - if fn.suffix != ".py" and self.config.save_append_py: + path = Path(fn).expanduser() + if path.suffix != ".py" and self.config.save_append_py: # fn.with_suffix(".py") does not append if fn has a non-empty suffix - fn = Path(f"{fn}.py") + path = Path(f"{path}.py") mode = "w" - if fn.exists(): - mode = self.interact.file_prompt( + if path.exists(): + new_mode = self.interact.file_prompt( _( - "%s already exists. Do you " - "want to (c)ancel, " - " (o)verwrite or " - "(a)ppend? " + "%s already exists. Do you want to (c)ancel, (o)verwrite or (a)ppend? " ) - % (fn,) + % (path,) ) - if mode in ("o", "overwrite", _("overwrite")): + if new_mode in ("o", "overwrite", _("overwrite")): mode = "w" - elif mode in ("a", "append", _("append")): + elif new_mode in ("a", "append", _("append")): mode = "a" else: self.interact.notify(_("Save cancelled.")) @@ -897,12 +893,12 @@ def write2file(self) -> None: stdout_text = self.get_session_formatted_for_file() try: - with open(fn, mode) as f: + with open(path, mode) as f: f.write(stdout_text) except OSError as e: - self.interact.notify(_("Error writing file '%s': %s") % (fn, e)) + self.interact.notify(_("Error writing file '%s': %s") % (path, e)) else: - self.interact.notify(_("Saved to %s.") % (fn,)) + self.interact.notify(_("Saved to %s.") % (path,)) def copy2clipboard(self) -> None: """Copy current content to clipboard.""" @@ -1003,6 +999,10 @@ def prompt_undo(self) -> int: _("Undo how many lines? (Undo will take up to ~%.1f seconds) [1]") % (est,) ) + if m is None: + self.interact.notify(_("Undo canceled"), 0.1) + return 0 + try: if m == "": m = "1" From 59a8095a962cebe9a17783e872ca15a19a326fe0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 01:05:55 +0200 Subject: [PATCH 359/555] Handle end_lineno and end_offset in SyntaxError --- bpython/repl.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 8294f2c8e..d3b646766 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -166,20 +166,21 @@ def showsyntaxerror(self, filename: Optional[str] = None) -> None: exc_type, value, sys.last_traceback = sys.exc_info() sys.last_type = exc_type sys.last_value = value - if filename and exc_type is SyntaxError: - # Work hard to stuff the correct filename in the exception - try: - msg, (dummy_filename, lineno, offset, line) = value.args - except: - # Not the format we expect; leave it alone - pass - else: - # Stuff in the right filename and right lineno - # strip linecache line number - if self.bpython_input_re.match(filename): - filename = "" - value = SyntaxError(msg, (filename, lineno, offset, line)) - sys.last_value = value + if ( + filename + and exc_type is SyntaxError + and value is not None + and len(value.args) >= 4 + ): + msg = str(value) + lineno = value.args[1] + offset = value.args[2] + line = value.args[3] + # strip linechache line number + if self.bpython_input_re.match(filename): + filename = "" + value = SyntaxError(msg, (filename, lineno, offset, line)) + sys.last_value = value exc_formatted = traceback.format_exception_only(exc_type, value) self.writetb(exc_formatted) From 937626dd46abcc50cd8192e22c8185955a5bf780 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 01:25:42 +0200 Subject: [PATCH 360/555] Improve handling of SyntaxErrors is now detected and replaced with in more cases. --- bpython/repl.py | 17 +++++------------ bpython/test/test_interpreter.py | 4 ++-- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index d3b646766..fda799280 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -166,20 +166,13 @@ def showsyntaxerror(self, filename: Optional[str] = None) -> None: exc_type, value, sys.last_traceback = sys.exc_info() sys.last_type = exc_type sys.last_value = value - if ( - filename - and exc_type is SyntaxError - and value is not None - and len(value.args) >= 4 - ): - msg = str(value) - lineno = value.args[1] - offset = value.args[2] - line = value.args[3] + if filename and exc_type is SyntaxError and value is not None: + msg = value.args[0] + args = list(value.args[1]) # strip linechache line number if self.bpython_input_re.match(filename): - filename = "" - value = SyntaxError(msg, (filename, lineno, offset, line)) + args[0] = "" + value = SyntaxError(msg, tuple(args)) sys.last_value = value exc_formatted = traceback.format_exception_only(exc_type, value) self.writetb(exc_formatted) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 281ad5fa5..65b60a925 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -38,7 +38,7 @@ def test_syntaxerror(self): if (3, 10, 1) <= sys.version_info[:3]: expected = ( " File " - + green('""') + + green('""') + ", line " + bold(magenta("1")) + "\n 1.1.1.1\n ^^\n" @@ -50,7 +50,7 @@ def test_syntaxerror(self): elif (3, 10) <= sys.version_info[:2]: expected = ( " File " - + green('""') + + green('""') + ", line " + bold(magenta("1")) + "\n 1.1.1.1\n ^^^^^\n" From cec06780ef158a14fb3fc6b372c30717821cd6cc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 01:25:51 +0200 Subject: [PATCH 361/555] GA: do not fail fast --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 62ebac0f7..89edab7c8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,6 +12,7 @@ jobs: runs-on: ubuntu-latest continue-on-error: ${{ matrix.python-version == 'pypy-3.7' }} strategy: + fail-fast: false matrix: python-version: [3.7, 3.8, 3.9, "3.10", "pypy-3.7"] steps: From 0ada13d6645d80c18399c738d01e75380b6c431f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 09:52:41 +0200 Subject: [PATCH 362/555] Provide more type annotations --- bpython/repl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index fda799280..41c28d5a8 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -417,12 +417,12 @@ class Repl: @property @abstractmethod - def current_line(self): + def current_line(self) -> str: pass @property @abstractmethod - def cursor_offset(self): + def cursor_offset(self) -> int: pass @abstractmethod From e906df7c2c437970b95fad75dca925cb41895f0d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 10:31:42 +0200 Subject: [PATCH 363/555] Refactor _funcname_and_argnum to avoid Exception-based control flow --- bpython/repl.py | 86 ++++++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 37 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 41c28d5a8..6e8278225 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -35,6 +35,7 @@ import time import traceback from abc import abstractmethod +from dataclasses import dataclass from itertools import takewhile from pathlib import Path from types import ModuleType, TracebackType @@ -380,6 +381,17 @@ class SourceNotFound(Exception): """Exception raised when the requested source could not be found.""" +@dataclass +class _FuncExpr: + """Stack element in Repl._funcname_and_argnum""" + + full_expr: str + function_expr: str + arg_number: int + opening: str + keyword: Optional[str] = None + + class Repl: """Implements the necessary guff for a Python-repl-alike interface @@ -564,37 +576,37 @@ def get_object(self, name): return obj @classmethod - def _funcname_and_argnum(cls, line): + def _funcname_and_argnum( + cls, line: str + ) -> Tuple[Optional[str], Optional[Union[str, int]]]: """Parse out the current function name and arg from a line of code.""" - # each list in stack: - # [full_expr, function_expr, arg_number, opening] - # arg_number may be a string if we've encountered a keyword - # argument so we're done counting - stack = [["", "", 0, ""]] + # each element in stack is a _FuncExpr instance + # if keyword is not None, we've encountered a keyword and so we're done counting + stack = [_FuncExpr("", "", 0, "")] try: for (token, value) in Python3Lexer().get_tokens(line): if token is Token.Punctuation: if value in "([{": - stack.append(["", "", 0, value]) + stack.append(_FuncExpr("", "", 0, value)) elif value in ")]}": - full, _, _, start = stack.pop() - expr = start + full + value - stack[-1][1] += expr - stack[-1][0] += expr + element = stack.pop() + expr = element.opening + element.full_expr + value + stack[-1].function_expr += expr + stack[-1].full_expr += expr elif value == ",": - try: - stack[-1][2] += 1 - except TypeError: - stack[-1][2] = "" - stack[-1][1] = "" - stack[-1][0] += value - elif value == ":" and stack[-1][3] == "lambda": - expr = stack.pop()[0] + ":" - stack[-1][1] += expr - stack[-1][0] += expr + if stack[-1].keyword is None: + stack[-1].arg_number += 1 + else: + stack[-1].keyword = "" + stack[-1].function_expr = "" + stack[-1].full_expr += value + elif value == ":" and stack[-1].opening == "lambda": + expr = stack.pop().full_expr + ":" + stack[-1].function_expr += expr + stack[-1].full_expr += expr else: - stack[-1][1] = "" - stack[-1][0] += value + stack[-1].function_expr = "" + stack[-1].full_expr += value elif ( token is Token.Number or token in Token.Number.subtypes @@ -603,25 +615,25 @@ def _funcname_and_argnum(cls, line): or token is Token.Operator and value == "." ): - stack[-1][1] += value - stack[-1][0] += value + stack[-1].function_expr += value + stack[-1].full_expr += value elif token is Token.Operator and value == "=": - stack[-1][2] = stack[-1][1] - stack[-1][1] = "" - stack[-1][0] += value + stack[-1].keyword = stack[-1].function_expr + stack[-1].function_expr = "" + stack[-1].full_expr += value elif token is Token.Number or token in Token.Number.subtypes: - stack[-1][1] = value - stack[-1][0] += value + stack[-1].function_expr = value + stack[-1].full_expr += value elif token is Token.Keyword and value == "lambda": - stack.append([value, "", 0, value]) + stack.append(_FuncExpr(value, "", 0, value)) else: - stack[-1][1] = "" - stack[-1][0] += value - while stack[-1][3] in "[{": + stack[-1].function_expr = "" + stack[-1].full_expr += value + while stack[-1].opening in "[{": stack.pop() - _, _, arg_number, _ = stack.pop() - _, func, _, _ = stack.pop() - return func, arg_number + elem1 = stack.pop() + elem2 = stack.pop() + return elem2.function_expr, elem1.keyword or elem1.arg_number except IndexError: return None, None From b86de33c17413b6fa7cbb0597bd491605dc85412 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 10:59:00 +0200 Subject: [PATCH 364/555] Make implementations of current_line and cursor_offset consistent --- bpython/cli.py | 14 -------- bpython/curtsiesfrontend/repl.py | 18 +++------- bpython/repl.py | 60 ++++++++++++++++++++++---------- bpython/test/test_repl.py | 30 ++++++++++++++-- 4 files changed, 73 insertions(+), 49 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 559b63e7b..5383a7c38 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -383,13 +383,6 @@ def _get_cursor_offset(self) -> int: def _set_cursor_offset(self, offset: int) -> None: self.cpos = len(self.s) - offset - cursor_offset = property( - _get_cursor_offset, - _set_cursor_offset, - None, - "The cursor offset from the beginning of the line", - ) - def addstr(self, s: str) -> None: """Add a string to the current input line and figure out where it should go, depending on the cursor position.""" @@ -539,13 +532,6 @@ def _get_current_line(self) -> str: def _set_current_line(self, line: str) -> None: self.s = line - current_line = property( - _get_current_line, - _set_current_line, - None, - "The characters of the current line", - ) - def cut_to_buffer(self) -> None: """Clear from cursor to end of line, placing into cut buffer""" self.cut_buffer = self.s[-self.cpos :] diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 696e84bf7..b8c7f0164 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1866,18 +1866,13 @@ def __repr__(self): lines scrolled down: {self.scroll_offset} >""" - @property - def current_line(self): + def _get_current_line(self) -> str: """The current line""" return self._current_line - @current_line.setter - def current_line(self, value): - self._set_current_line(value) - def _set_current_line( self, - line, + line: str, update_completion=True, reset_rl_history=True, clear_special_mode=True, @@ -1895,18 +1890,13 @@ def _set_current_line( self.special_mode = None self.unhighlight_paren() - @property - def cursor_offset(self): + def _get_cursor_offset(self) -> int: """The current cursor offset from the front of the "line".""" return self._cursor_offset - @cursor_offset.setter - def cursor_offset(self, value): - self._set_cursor_offset(value) - def _set_cursor_offset( self, - offset, + offset: int, update_completion=True, reset_rl_history=False, clear_special_mode=True, diff --git a/bpython/repl.py b/bpython/repl.py index 6e8278225..6420a1780 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -392,7 +392,7 @@ class _FuncExpr: keyword: Optional[str] = None -class Repl: +class Repl(metaclass=abc.ABCMeta): """Implements the necessary guff for a Python-repl-alike interface The execution of the code entered and all that stuff was taken from the @@ -425,27 +425,51 @@ class Repl: XXX Subclasses should implement echo, current_line, cw """ - if TYPE_CHECKING: + @abc.abstractmethod + def reevaluate(self): + pass - @property - @abstractmethod - def current_line(self) -> str: - pass + @abc.abstractmethod + def reprint_line( + self, lineno: int, tokens: List[Tuple[_TokenType, str]] + ) -> None: + pass - @property - @abstractmethod - def cursor_offset(self) -> int: - pass + @abc.abstractmethod + def _get_current_line(self) -> str: + pass - @abstractmethod - def reevaluate(self): - pass + @abc.abstractmethod + def _set_current_line(self, val: str) -> None: + pass - @abstractmethod - def reprint_line( - self, lineno: int, tokens: List[Tuple[_TokenType, str]] - ) -> None: - pass + @property + def current_line(self) -> str: + """The current line""" + return self._get_current_line() + + @current_line.setter + def current_line(self, value: str) -> None: + self._set_current_line(value) + + @abc.abstractmethod + def _get_cursor_offset(self) -> int: + pass + + @abc.abstractmethod + def _set_cursor_offset(self, val: int) -> None: + pass + + @property + def cursor_offset(self) -> int: + """The current cursor offset from the front of the "line".""" + return self._get_cursor_offset() + + @cursor_offset.setter + def cursor_offset(self, value: int) -> None: + self._set_cursor_offset(value) + + if TYPE_CHECKING: # not actually defined, subclasses must define cpos: int diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index a4241087d..63309364c 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -5,6 +5,7 @@ import tempfile import unittest +from typing import List, Tuple from itertools import islice from pathlib import Path from unittest import mock @@ -38,9 +39,32 @@ def reset(self): class FakeRepl(repl.Repl): def __init__(self, conf=None): - repl.Repl.__init__(self, repl.Interpreter(), setup_config(conf)) - self.current_line = "" - self.cursor_offset = 0 + super().__init__(repl.Interpreter(), setup_config(conf)) + self._current_line = "" + self._cursor_offset = 0 + + def _get_current_line(self) -> str: + return self._current_line + + def _set_current_line(self, val: str) -> None: + self._current_line = val + + def _get_cursor_offset(self) -> int: + return self._cursor_offset + + def _set_cursor_offset(self, val: int) -> None: + self._cursor_offset = val + + def getstdout(self) -> str: + raise NotImplementedError + + def reprint_line( + self, lineno: int, tokens: List[Tuple[repl._TokenType, str]] + ) -> None: + raise NotImplementedError + + def reevaluate(self): + raise NotImplementedError class FakeCliRepl(cli.CLIRepl, FakeRepl): From cc3bac7e552cb7d88d25bac0011caf045714126c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 11:09:05 +0200 Subject: [PATCH 365/555] Change filenames in History to Path --- bpython/history.py | 15 ++++++++------- bpython/repl.py | 6 +++--- bpython/test/test_history.py | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/bpython/history.py b/bpython/history.py index dfbab2ada..a870d4b29 100644 --- a/bpython/history.py +++ b/bpython/history.py @@ -22,9 +22,10 @@ # THE SOFTWARE. import os +from pathlib import Path import stat from itertools import islice, chain -from typing import Iterable, Optional, List, TextIO +from typing import Iterable, Optional, List, TextIO, Union from .translations import _ from .filelock import FileLock @@ -190,9 +191,9 @@ def reset(self) -> None: self.index = 0 self.saved_line = "" - def load(self, filename: str, encoding: str) -> None: + def load(self, filename: Path, encoding: str) -> None: with open(filename, encoding=encoding, errors="ignore") as hfile: - with FileLock(hfile, filename=filename): + with FileLock(hfile, filename=str(filename)): self.entries = self.load_from(hfile) def load_from(self, fd: TextIO) -> List[str]: @@ -201,14 +202,14 @@ def load_from(self, fd: TextIO) -> List[str]: self.append_to(entries, line) return entries if len(entries) else [""] - def save(self, filename: str, encoding: str, lines: int = 0) -> None: + def save(self, filename: Path, encoding: str, lines: int = 0) -> None: fd = os.open( filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, stat.S_IRUSR | stat.S_IWUSR, ) with open(fd, "w", encoding=encoding, errors="ignore") as hfile: - with FileLock(hfile, filename=filename): + with FileLock(hfile, filename=str(filename)): self.save_to(hfile, self.entries, lines) def save_to( @@ -221,7 +222,7 @@ def save_to( fd.write("\n") def append_reload_and_write( - self, s: str, filename: str, encoding: str + self, s: str, filename: Path, encoding: str ) -> None: if not self.hist_size: return self.append(s) @@ -233,7 +234,7 @@ def append_reload_and_write( stat.S_IRUSR | stat.S_IWUSR, ) with open(fd, "a+", encoding=encoding, errors="ignore") as hfile: - with FileLock(hfile, filename=filename): + with FileLock(hfile, filename=str(filename)): # read entries hfile.seek(0, os.SEEK_SET) entries = self.load_from(hfile) diff --git a/bpython/repl.py b/bpython/repl.py index 6420a1780..e0d42b1f9 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -520,7 +520,7 @@ def __init__(self, interp: Interpreter, config: Config): if self.config.hist_file.exists(): try: self.rl_history.load( - str(self.config.hist_file), + self.config.hist_file, getpreferredencoding() or "ascii", ) except OSError: @@ -744,7 +744,7 @@ def get_source_of_current_name(self) -> str: msg = _("No source code found for %s") % (self.current_line,) raise SourceNotFound(msg) - def set_docstring(self): + def set_docstring(self) -> None: self.docstring = None if not self.get_args(): self.funcprops = None @@ -1009,7 +1009,7 @@ def push(self, s, insert_into_history=True) -> bool: return more - def insert_into_history(self, s): + def insert_into_history(self, s: str): try: self.rl_history.append_reload_and_write( s, self.config.hist_file, getpreferredencoding() diff --git a/bpython/test/test_history.py b/bpython/test/test_history.py index 544a644eb..d810cf6be 100644 --- a/bpython/test/test_history.py +++ b/bpython/test/test_history.py @@ -86,7 +86,7 @@ def test_reset(self): class TestHistoryFileAccess(unittest.TestCase): def setUp(self): self.tempdir = tempfile.TemporaryDirectory() - self.filename = str(Path(self.tempdir.name) / "history_temp_file") + self.filename = Path(self.tempdir.name) / "history_temp_file" self.encoding = getpreferredencoding() with open( From 167712230a803edd7b34a1cb127926632c964a01 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 23:50:02 +0200 Subject: [PATCH 366/555] Simplify type annotations --- bpython/curtsiesfrontend/repl.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index b8c7f0164..c2b502fe6 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1,4 +1,3 @@ -import code import contextlib import errno import itertools @@ -22,13 +21,9 @@ Union, cast, Type, - TYPE_CHECKING, ) from .._typing_compat import Literal -if TYPE_CHECKING: - from ..repl import Interpreter - import blessings import greenlet from curtsies import ( @@ -327,7 +322,7 @@ def __init__( config: Config, locals_: Optional[Dict[str, Any]] = None, banner: Optional[str] = None, - interp: Optional[code.InteractiveInterpreter] = None, + interp: Optional[Interp] = None, orig_tcattrs: Optional[List[Any]] = None, ): """ @@ -372,7 +367,7 @@ def __init__( logger.debug("starting parent init") # interp is a subclass of repl.Interpreter, so it definitely, # implements the methods of Interpreter! - super().__init__(cast("Interpreter", interp), config) + super().__init__(interp, config) self.formatter = BPythonFormatter(config.color_scheme) From 2a95aa790aabe21dab4860b30e69f954627c5ed3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 23:50:11 +0200 Subject: [PATCH 367/555] Remove unused import --- bpython/curtsies.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 82adc15f0..102252b2f 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -3,7 +3,6 @@ # mypy: disallow_untyped_calls=True import argparse -import code import collections import logging import sys From 741015e1c786999f66fd49af08bf28419a5ab478 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 23:50:22 +0200 Subject: [PATCH 368/555] Fix default argument --- bpython/curtsiesfrontend/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index c2b502fe6..8cf1467d5 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -544,7 +544,7 @@ def request_reload(self, files_modified=()): if self.watching_files: self._request_reload(files_modified=files_modified) - def schedule_refresh(self, when="now"): + def schedule_refresh(self, when: float = 0) -> None: """Schedule a ScheduledRefreshRequestEvent for when. Such a event should interrupt if blockied waiting for keyboard input""" From 44c7d42fb8a6fda96529f454715ae7628e54e5c8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 May 2022 23:59:52 +0200 Subject: [PATCH 369/555] Turn search modes into an Enum --- bpython/curtsiesfrontend/repl.py | 40 +++++++++++++++++++------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 8cf1467d5..258a937b1 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -84,6 +84,12 @@ MAX_EVENTS_POSSIBLY_NOT_PASTE = 20 +class SearchMode(Enum): + NO_SEARCH = 0 + INCREMENTAL_SEARCH = 1 + REVERSE_INCREMENTAL_SEARCH = 2 + + class LineType(Enum): """Used when adding a tuple to all_logical_lines, to get input / output values having to actually type/know the strings""" @@ -452,9 +458,7 @@ def __init__( # whether auto reloading active self.watching_files = config.default_autoreload - # 'reverse_incremental_search', 'incremental_search' or None - self.incr_search_mode = None - + self.incr_search_mode = SearchMode.NO_SEARCH self.incr_search_target = "" self.original_modules = set(sys.modules.keys()) @@ -758,7 +762,7 @@ def process_key_event(self, e: str) -> None: self.incremental_search() elif ( e in (("",) + key_dispatch[self.config.backspace_key]) - and self.incr_search_mode + and self.incr_search_mode != SearchMode.NO_SEARCH ): self.add_to_incremental_search(self, backspace=True) elif e in self.edit_keys.cut_buffer_edits: @@ -808,7 +812,7 @@ def process_key_event(self, e: str) -> None: elif e in key_dispatch[self.config.edit_current_block_key]: self.send_current_block_to_external_editor() elif e in ("",): - self.incr_search_mode = None + self.incr_search_mode = SearchMode.NO_SEARCH elif e in ("",): self.add_normal_character(" ") elif e in CHARACTER_PAIR_MAP.keys(): @@ -852,7 +856,11 @@ def insert_char_pair_start(self, e): if not can_lookup_next else self._current_line[self._cursor_offset] ) - if start_of_line or end_of_line or next_char in "})] ": + if ( + start_of_line + or end_of_line + or (next_char is not None and next_char in "})] ") + ): self.add_normal_character( CHARACTER_PAIR_MAP[e], narrow_search=False ) @@ -891,7 +899,7 @@ def get_last_word(self): ) def incremental_search(self, reverse=False, include_current=False): - if self.incr_search_mode is None: + if self.incr_search_mode == SearchMode.NO_SEARCH: self.rl_history.enter(self.current_line) self.incr_search_target = "" else: @@ -920,9 +928,9 @@ def incremental_search(self, reverse=False, include_current=False): clear_special_mode=False, ) if reverse: - self.incr_search_mode = "reverse_incremental_search" + self.incr_search_mode = SearchMode.REVERSE_INCREMENTAL_SEARCH else: - self.incr_search_mode = "incremental_search" + self.incr_search_mode = SearchMode.INCREMENTAL_SEARCH def readline_kill(self, e): func = self.edit_keys[e] @@ -1172,7 +1180,7 @@ def toggle_file_watch(self): def add_normal_character(self, char, narrow_search=True): if len(char) > 1 or is_nop(char): return - if self.incr_search_mode: + if self.incr_search_mode != SearchMode.NO_SEARCH: self.add_to_incremental_search(char) else: self._set_current_line( @@ -1209,9 +1217,9 @@ def add_to_incremental_search(self, char=None, backspace=False): self.incr_search_target = self.incr_search_target[:-1] else: self.incr_search_target += char - if self.incr_search_mode == "reverse_incremental_search": + if self.incr_search_mode == SearchMode.REVERSE_INCREMENTAL_SEARCH: self.incremental_search(reverse=True, include_current=True) - elif self.incr_search_mode == "incremental_search": + elif self.incr_search_mode == SearchMode.INCREMENTAL_SEARCH: self.incremental_search(include_current=True) else: raise ValueError("add_to_incremental_search not in a special mode") @@ -1419,7 +1427,7 @@ def current_line_formatted(self): fs = bpythonparse( pygformat(self.tokenize(self.current_line), self.formatter) ) - if self.incr_search_mode: + if self.incr_search_mode != SearchMode.NO_SEARCH: if self.incr_search_target in self.current_line: fs = fmtfuncs.on_magenta(self.incr_search_target).join( fs.split(self.incr_search_target) @@ -1467,12 +1475,12 @@ def display_line_with_prompt(self): """colored line with prompt""" prompt = func_for_letter(self.config.color_scheme["prompt"]) more = func_for_letter(self.config.color_scheme["prompt_more"]) - if self.incr_search_mode == "reverse_incremental_search": + if self.incr_search_mode == SearchMode.REVERSE_INCREMENTAL_SEARCH: return ( prompt(f"(reverse-i-search)`{self.incr_search_target}': ") + self.current_line_formatted ) - elif self.incr_search_mode == "incremental_search": + elif self.incr_search_mode == SearchMode.INCREMENTAL_SEARCH: return prompt(f"(i-search)`%s': ") + self.current_line_formatted return ( prompt(self.ps1) if self.done else more(self.ps2) @@ -1905,7 +1913,7 @@ def _set_cursor_offset( if reset_rl_history: self.rl_history.reset() if clear_special_mode: - self.incr_search_mode = None + self.incr_search_mode = SearchMode.NO_SEARCH self._cursor_offset = offset if update_completion: self.update_completion() From 49255d6bfcc69465fe1a2c5d82d015a2fdf4488f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 00:10:13 +0200 Subject: [PATCH 370/555] Re-arrange checks --- bpython/curtsiesfrontend/repl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 258a937b1..98b78d05c 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1211,12 +1211,12 @@ def add_to_incremental_search(self, char=None, backspace=False): The only operations allowed in incremental search mode are adding characters and backspacing.""" - if char is None and not backspace: - raise ValueError("must provide a char or set backspace to True") if backspace: self.incr_search_target = self.incr_search_target[:-1] - else: + elif char is not None: self.incr_search_target += char + else: + raise ValueError("must provide a char or set backspace to True") if self.incr_search_mode == SearchMode.REVERSE_INCREMENTAL_SEARCH: self.incremental_search(reverse=True, include_current=True) elif self.incr_search_mode == SearchMode.INCREMENTAL_SEARCH: From 2776156c535368e723582ca1835efd6881e7e8e1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 00:21:35 +0200 Subject: [PATCH 371/555] Directly sum up lengths There is no need to build the complete string. --- bpython/curtsiesfrontend/repl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 98b78d05c..7020b678b 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1536,15 +1536,15 @@ def number_of_padding_chars_on_current_cursor_line(self): Should return zero unless there are fullwidth characters.""" full_line = self.current_cursor_line_without_suggestion - line_with_padding = "".join( - line.s + line_with_padding_len = sum( + len(line.s) for line in paint.display_linize( self.current_cursor_line_without_suggestion.s, self.width ) ) # the difference in length here is how much padding there is - return len(line_with_padding) - len(full_line) + return line_with_padding_len - len(full_line) def paint( self, From 2c80e03655c79527f05abb5a2e12a733bd50d11a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 00:52:30 +0200 Subject: [PATCH 372/555] Add type annotations --- bpython/curtsiesfrontend/preprocess.py | 35 +++++++++++++------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/bpython/curtsiesfrontend/preprocess.py b/bpython/curtsiesfrontend/preprocess.py index e0d15f4ec..5e59dd499 100644 --- a/bpython/curtsiesfrontend/preprocess.py +++ b/bpython/curtsiesfrontend/preprocess.py @@ -1,34 +1,38 @@ """Tools for preparing code to be run in the REPL (removing blank lines, etc)""" -from ..lazyre import LazyReCompile +from codeop import CommandCompiler +from typing import Match from itertools import tee, islice, chain +from ..lazyre import LazyReCompile + # TODO specifically catch IndentationErrors instead of any syntax errors indent_empty_lines_re = LazyReCompile(r"\s*") tabs_to_spaces_re = LazyReCompile(r"^\t+") -def indent_empty_lines(s, compiler): +def indent_empty_lines(s: str, compiler: CommandCompiler) -> str: """Indents blank lines that would otherwise cause early compilation Only really works if starting on a new line""" - lines = s.split("\n") + initial_lines = s.split("\n") ends_with_newline = False - if lines and not lines[-1]: + if initial_lines and not initial_lines[-1]: ends_with_newline = True - lines.pop() + initial_lines.pop() result_lines = [] - prevs, lines, nexts = tee(lines, 3) + prevs, lines, nexts = tee(initial_lines, 3) prevs = chain(("",), prevs) nexts = chain(islice(nexts, 1, None), ("",)) for p_line, line, n_line in zip(prevs, lines, nexts): if len(line) == 0: - p_indent = indent_empty_lines_re.match(p_line).group() - n_indent = indent_empty_lines_re.match(n_line).group() + # "\s*" always matches + p_indent = indent_empty_lines_re.match(p_line).group() # type: ignore + n_indent = indent_empty_lines_re.match(n_line).group() # type: ignore result_lines.append(min([p_indent, n_indent], key=len) + line) else: result_lines.append(line) @@ -36,17 +40,14 @@ def indent_empty_lines(s, compiler): return "\n".join(result_lines) + ("\n" if ends_with_newline else "") -def leading_tabs_to_spaces(s): - lines = s.split("\n") - result_lines = [] - - def tab_to_space(m): +def leading_tabs_to_spaces(s: str) -> str: + def tab_to_space(m: Match[str]) -> str: return len(m.group()) * 4 * " " - for line in lines: - result_lines.append(tabs_to_spaces_re.sub(tab_to_space, line)) - return "\n".join(result_lines) + return "\n".join( + tabs_to_spaces_re.sub(tab_to_space, line) for line in s.split("\n") + ) -def preprocess(s, compiler): +def preprocess(s: str, compiler: CommandCompiler) -> str: return indent_empty_lines(leading_tabs_to_spaces(s), compiler) From 9ffa5f3eef565fed62a7c7976ec856970249fd14 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 01:13:50 +0200 Subject: [PATCH 373/555] Add type annotations --- bpython/curtsiesfrontend/filewatch.py | 25 +++++++++++++------------ stubs/watchdog/events.pyi | 4 ++++ stubs/watchdog/observers.pyi | 6 +++++- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 7616d4845..8f64fd3db 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -1,11 +1,12 @@ import os from collections import defaultdict +from typing import Dict, Iterable, Set, List from .. import importcompletion try: from watchdog.observers import Observer - from watchdog.events import FileSystemEventHandler + from watchdog.events import FileSystemEventHandler, FileSystemEvent except ImportError: def ModuleChangedEventHandler(*args): @@ -14,12 +15,12 @@ def ModuleChangedEventHandler(*args): else: class ModuleChangedEventHandler(FileSystemEventHandler): # type: ignore [no-redef] - def __init__(self, paths, on_change): - self.dirs = defaultdict(set) + def __init__(self, paths: Iterable[str], on_change) -> None: + self.dirs: Dict[str, Set[str]] = defaultdict(set) self.on_change = on_change - self.modules_to_add_later = [] + self.modules_to_add_later: List[str] = [] self.observer = Observer() - self.old_dirs = defaultdict(set) + self.old_dirs: Dict[str, Set[str]] = defaultdict(set) self.started = False self.activated = False for path in paths: @@ -27,13 +28,13 @@ def __init__(self, paths, on_change): super().__init__() - def reset(self): + def reset(self) -> None: self.dirs = defaultdict(set) del self.modules_to_add_later[:] self.old_dirs = defaultdict(set) self.observer.unschedule_all() - def _add_module(self, path): + def _add_module(self, path: str) -> None: """Add a python module to track changes""" path = os.path.abspath(path) for suff in importcompletion.SUFFIXES: @@ -45,10 +46,10 @@ def _add_module(self, path): self.observer.schedule(self, dirname, recursive=False) self.dirs[dirname].add(path) - def _add_module_later(self, path): + def _add_module_later(self, path: str) -> None: self.modules_to_add_later.append(path) - def track_module(self, path): + def track_module(self, path: str) -> None: """ Begins tracking this if activated, or remembers to track later. """ @@ -57,7 +58,7 @@ def track_module(self, path): else: self._add_module_later(path) - def activate(self): + def activate(self) -> None: if self.activated: raise ValueError(f"{self!r} is already activated.") if not self.started: @@ -70,13 +71,13 @@ def activate(self): del self.modules_to_add_later[:] self.activated = True - def deactivate(self): + def deactivate(self) -> None: if not self.activated: raise ValueError(f"{self!r} is not activated.") self.observer.unschedule_all() self.activated = False - def on_any_event(self, event): + def on_any_event(self, event: FileSystemEvent) -> None: dirpath = os.path.dirname(event.src_path) paths = [path + ".py" for path in self.dirs[dirpath]] if event.src_path in paths: diff --git a/stubs/watchdog/events.pyi b/stubs/watchdog/events.pyi index 6e17bd6df..ded1fe942 100644 --- a/stubs/watchdog/events.pyi +++ b/stubs/watchdog/events.pyi @@ -1 +1,5 @@ +class FileSystemEvent: + @property + def src_path(self) -> str: ... + class FileSystemEventHandler: ... diff --git a/stubs/watchdog/observers.pyi b/stubs/watchdog/observers.pyi index 7db3099fb..c4596f2d9 100644 --- a/stubs/watchdog/observers.pyi +++ b/stubs/watchdog/observers.pyi @@ -1,4 +1,8 @@ +from .events import FileSystemEventHandler + class Observer: def start(self): ... - def schedule(self, dirname: str, recursive: bool): ... + def schedule( + self, observer: FileSystemEventHandler, dirname: str, recursive: bool + ): ... def unschedule_all(self): ... From 43778e8dc955ca0163fc846eaa0400dee096b9e9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 01:21:54 +0200 Subject: [PATCH 374/555] Remove unused member --- bpython/curtsiesfrontend/filewatch.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 8f64fd3db..33054c4cc 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -20,7 +20,6 @@ def __init__(self, paths: Iterable[str], on_change) -> None: self.on_change = on_change self.modules_to_add_later: List[str] = [] self.observer = Observer() - self.old_dirs: Dict[str, Set[str]] = defaultdict(set) self.started = False self.activated = False for path in paths: @@ -31,7 +30,6 @@ def __init__(self, paths: Iterable[str], on_change) -> None: def reset(self) -> None: self.dirs = defaultdict(set) del self.modules_to_add_later[:] - self.old_dirs = defaultdict(set) self.observer.unschedule_all() def _add_module(self, path: str) -> None: From 5f070f474e5a3a0761c29621a72829edf32d6734 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 09:06:08 +0200 Subject: [PATCH 375/555] Initialize width/height using os.get_terminal_size --- bpython/curtsiesfrontend/repl.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 7020b678b..8ba7545da 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -19,7 +19,6 @@ Optional, Tuple, Union, - cast, Type, ) from .._typing_compat import Literal @@ -465,8 +464,7 @@ def __init__( # as long as the first event received is a window resize event, # this works fine... - self.width: int = cast(int, None) - self.height: int = cast(int, None) + self.width, self.height = os.get_terminal_size() self.status_bar.message(banner) From e82b4fc5279974598a1c0650ce9b2df35333df3a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 09:29:37 +0200 Subject: [PATCH 376/555] Handle OSError from unit tests --- bpython/curtsiesfrontend/repl.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 8ba7545da..2ea4269cf 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -464,7 +464,12 @@ def __init__( # as long as the first event received is a window resize event, # this works fine... - self.width, self.height = os.get_terminal_size() + try: + self.width, self.height = os.get_terminal_size() + except OSError: + # this case will trigger during unit tests when stdout is redirected + self.width = -1 + self.height = -1 self.status_bar.message(banner) From 592725a487a9da0fd5523676a99993314a974d8e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 09:35:32 +0200 Subject: [PATCH 377/555] Improve on_any_event and add type annotations --- bpython/curtsies.py | 2 +- bpython/curtsiesfrontend/filewatch.py | 15 ++++++++++----- bpython/curtsiesfrontend/repl.py | 7 ++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 102252b2f..caa6e08e7 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -104,7 +104,7 @@ def _request_refresh(self) -> None: def _schedule_refresh(self, when: float) -> None: return self._schedule_refresh_callback(when) - def _request_reload(self, files_modified: Sequence[str] = ("?",)) -> None: + def _request_reload(self, files_modified: Sequence[str]) -> None: return self._request_reload_callback(files_modified) def interrupting_refresh(self) -> None: diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 33054c4cc..8933e2930 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -1,6 +1,6 @@ import os from collections import defaultdict -from typing import Dict, Iterable, Set, List +from typing import Callable, Dict, Iterable, Sequence, Set, List from .. import importcompletion @@ -15,7 +15,11 @@ def ModuleChangedEventHandler(*args): else: class ModuleChangedEventHandler(FileSystemEventHandler): # type: ignore [no-redef] - def __init__(self, paths: Iterable[str], on_change) -> None: + def __init__( + self, + paths: Iterable[str], + on_change: Callable[[Sequence[str]], None], + ) -> None: self.dirs: Dict[str, Set[str]] = defaultdict(set) self.on_change = on_change self.modules_to_add_later: List[str] = [] @@ -77,6 +81,7 @@ def deactivate(self) -> None: def on_any_event(self, event: FileSystemEvent) -> None: dirpath = os.path.dirname(event.src_path) - paths = [path + ".py" for path in self.dirs[dirpath]] - if event.src_path in paths: - self.on_change(files_modified=[event.src_path]) + if any( + event.src_path == f"{path}.py" for path in self.dirs[dirpath] + ): + self.on_change((event.src_path,)) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 2ea4269cf..2226cae32 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -17,6 +17,7 @@ Any, List, Optional, + Sequence, Tuple, Union, Type, @@ -516,7 +517,7 @@ def _request_refresh(self): RefreshRequestEvent.""" raise NotImplementedError - def _request_reload(self, files_modified=("?",)): + def _request_reload(self, files_modified: Sequence[str]) -> None: """Like request_refresh, but for reload requests events.""" raise NotImplementedError @@ -546,10 +547,10 @@ def request_refresh(self): else: self._request_refresh() - def request_reload(self, files_modified=()): + def request_reload(self, files_modified: Sequence[str] = ()) -> None: """Request that a ReloadEvent be passed next into process_event""" if self.watching_files: - self._request_reload(files_modified=files_modified) + self._request_reload(files_modified) def schedule_refresh(self, when: float = 0) -> None: """Schedule a ScheduledRefreshRequestEvent for when. From 56dfd88d11212a1834222a52a981b4a7bb0713f1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 09:36:13 +0200 Subject: [PATCH 378/555] Ignore mypy cache --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1bda0f251..7a81cbfe2 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ doc/sphinx/build/* bpython/_version.py venv/ .venv/ +.mypy_cache/ From e987ad32baf45f0bb89e4a2fd3f9f6d36abfd1d6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 09:36:45 +0200 Subject: [PATCH 379/555] Add type annotations --- bpython/curtsiesfrontend/repl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 2226cae32..7a591d416 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -11,7 +11,7 @@ import time import unicodedata from enum import Enum -from types import TracebackType +from types import FrameType, TracebackType from typing import ( Dict, Any, @@ -611,7 +611,7 @@ def __exit__( sys.meta_path = self.orig_meta_path return False - def sigwinch_handler(self, signum, frame): + def sigwinch_handler(self, signum: int, frame: Optional[FrameType]) -> None: old_rows, old_columns = self.height, self.width self.height, self.width = self.get_term_hw() cursor_dy = self.get_cursor_vertical_diff() @@ -627,7 +627,7 @@ def sigwinch_handler(self, signum, frame): self.scroll_offset, ) - def sigtstp_handler(self, signum, frame): + def sigtstp_handler(self, signum: int, frame: Optional[FrameType]) -> None: self.scroll_offset = len(self.lines_for_display) self.__exit__(None, None, None) self.on_suspend() From 28ea475bbafa85864c476a129725b3bf4d006cf5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 23:18:52 +0200 Subject: [PATCH 380/555] Store window to avoid implicit dependency on blessing/blessed --- bpython/curtsies.py | 3 ++- bpython/curtsiesfrontend/repl.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index caa6e08e7..f7b2ef472 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -61,7 +61,7 @@ def __init__( self.input_generator = curtsies.input.Input( keynames="curtsies", sigint_event=True, paste_threshold=None ) - self.window = curtsies.window.CursorAwareWindow( + window = curtsies.window.CursorAwareWindow( sys.stdout, sys.stdin, keep_last_line=True, @@ -92,6 +92,7 @@ def __init__( super().__init__( config, + window, locals_=locals_, banner=banner, interp=interp, diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 7a591d416..3a44f4e5b 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -24,7 +24,6 @@ ) from .._typing_compat import Literal -import blessings import greenlet from curtsies import ( FSArray, @@ -37,6 +36,7 @@ ) from curtsies.configfile_keynames import keymap as key_dispatch from curtsies.input import is_main_thread +from curtsies.window import BaseWindow from cwcwidth import wcswidth from pygments import format as pygformat from pygments.formatters import TerminalFormatter @@ -326,6 +326,7 @@ class BaseRepl(Repl): def __init__( self, config: Config, + window: BaseWindow, locals_: Optional[Dict[str, Any]] = None, banner: Optional[str] = None, interp: Optional[Interp] = None, @@ -340,6 +341,7 @@ def __init__( """ logger.debug("starting init") + self.window = window # If creating a new interpreter on undo would be unsafe because initial # state was passed in @@ -2077,7 +2079,7 @@ def focus_on_subprocess(self, args): try: signal.signal(signal.SIGWINCH, self.orig_sigwinch_handler) with Termmode(self.orig_stdin, self.orig_tcattrs): - terminal = blessings.Terminal(stream=sys.__stdout__) + terminal = self.window.t with terminal.fullscreen(): sys.__stdout__.write(terminal.save) sys.__stdout__.write(terminal.move(0, 0)) From 0d16fe6e3a896a4fa759e1cb08605b0c7bf25c83 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 23:32:56 +0200 Subject: [PATCH 381/555] Add type annotations --- bpython/curtsiesfrontend/repl.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 3a44f4e5b..b5956ba7b 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -388,7 +388,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: Union[str, FmtStr] = "" # this is every line that's been displayed (input and output) # as with formatting applied. Logical lines that exceeded the terminal width @@ -657,8 +657,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) -> Optional[bool]: - + def process_control_event(self, e: events.Event) -> Optional[bool]: if isinstance(e, bpythonevents.ScheduledRefreshRequestEvent): # This is a scheduled refresh - it's really just a refresh (so nop) pass @@ -703,9 +702,9 @@ def process_control_event(self, e) -> Optional[bool]: elif isinstance(e, bpythonevents.RunStartupFileEvent): try: self.startup() - except OSError as e: + except OSError as err: self.status_bar.message( - _("Executing PYTHONSTARTUP failed: %s") % (e,) + _("Executing PYTHONSTARTUP failed: %s") % (err,) ) elif isinstance(e, bpythonevents.UndoEvent): From 80bd3e0ef05ac40abe98aa9f3b84db1e25687c28 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 23:47:30 +0200 Subject: [PATCH 382/555] Make window optional for tests Where required, assert that window is not None. --- bpython/curtsiesfrontend/repl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index b5956ba7b..2c8e93e1f 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -326,7 +326,7 @@ class BaseRepl(Repl): def __init__( self, config: Config, - window: BaseWindow, + window: Optional[BaseWindow] = None, locals_: Optional[Dict[str, Any]] = None, banner: Optional[str] = None, interp: Optional[Interp] = None, @@ -2078,6 +2078,7 @@ def focus_on_subprocess(self, args): try: signal.signal(signal.SIGWINCH, self.orig_sigwinch_handler) with Termmode(self.orig_stdin, self.orig_tcattrs): + assert self.window is not None terminal = self.window.t with terminal.fullscreen(): sys.__stdout__.write(terminal.save) From 8d16a71ef404db66d2c6fae6c362640da8ae240d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 6 May 2022 00:02:30 +0200 Subject: [PATCH 383/555] Change the type to CursorAwareWindow Pass None in the tests. This is good enough. --- bpython/curtsiesfrontend/repl.py | 4 ++-- bpython/test/test_brackets_completion.py | 7 ++++++- bpython/test/test_curtsies_painting.py | 8 ++++++-- bpython/test/test_curtsies_repl.py | 6 +++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 2c8e93e1f..49d61a84b 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -36,7 +36,7 @@ ) from curtsies.configfile_keynames import keymap as key_dispatch from curtsies.input import is_main_thread -from curtsies.window import BaseWindow +from curtsies.window import CursorAwareWindow from cwcwidth import wcswidth from pygments import format as pygformat from pygments.formatters import TerminalFormatter @@ -326,7 +326,7 @@ class BaseRepl(Repl): def __init__( self, config: Config, - window: Optional[BaseWindow] = None, + window: CursorAwareWindow, locals_: Optional[Dict[str, Any]] = None, banner: Optional[str] = None, interp: Optional[Interp] = None, diff --git a/bpython/test/test_brackets_completion.py b/bpython/test/test_brackets_completion.py index fd9836650..4340ad3d0 100644 --- a/bpython/test/test_brackets_completion.py +++ b/bpython/test/test_brackets_completion.py @@ -1,9 +1,12 @@ import os +from typing import cast from bpython.test import FixLanguageTestCase as TestCase, TEST_CONFIG from bpython.curtsiesfrontend import repl as curtsiesrepl from bpython import config +from curtsies.window import CursorAwareWindow + def setup_config(conf): config_struct = config.Config(TEST_CONFIG) @@ -18,7 +21,9 @@ def create_repl(brackets_enabled=False, **kwargs): config = setup_config( {"editor": "true", "brackets_completion": brackets_enabled} ) - repl = curtsiesrepl.BaseRepl(config, **kwargs) + repl = curtsiesrepl.BaseRepl( + config, cast(None, CursorAwareWindow), **kwargs + ) os.environ["PAGER"] = "true" os.environ.pop("PYTHONSTARTUP", None) repl.width = 50 diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 9f98bf066..813b4fb3d 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -5,12 +5,14 @@ import sys from contextlib import contextmanager +from typing import cast from curtsies.formatstringarray import ( fsarray, assertFSArraysEqual, assertFSArraysEqualIgnoringFormatting, ) from curtsies.fmtfuncs import cyan, bold, green, yellow, on_magenta, red +from curtsies.window import CursorAwareWindow from unittest import mock from bpython.curtsiesfrontend.events import RefreshRequestEvent @@ -56,7 +58,7 @@ class TestRepl(BaseRepl): def _request_refresh(inner_self): pass - self.repl = TestRepl(config=setup_config()) + self.repl = TestRepl(setup_config(), cast(None, CursorAwareWindow)) self.repl.height, self.repl.width = (5, 10) @property @@ -284,7 +286,9 @@ class TestRepl(BaseRepl): def _request_refresh(inner_self): self.refresh() - self.repl = TestRepl(banner="", config=setup_config()) + self.repl = TestRepl( + setup_config(), cast(None, CursorAwareWindow), banner="" + ) self.repl.height, self.repl.width = (5, 32) def send_key(self, key): diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py index a6e4c7866..5a19c6abb 100644 --- a/bpython/test/test_curtsies_repl.py +++ b/bpython/test/test_curtsies_repl.py @@ -3,6 +3,7 @@ import sys import tempfile import io +from typing import cast import unittest from contextlib import contextmanager @@ -23,6 +24,7 @@ ) from curtsies import events +from curtsies.window import CursorAwareWindow from importlib import invalidate_caches @@ -231,7 +233,9 @@ def captured_output(): def create_repl(**kwargs): config = setup_config({"editor": "true"}) - repl = curtsiesrepl.BaseRepl(config, **kwargs) + repl = curtsiesrepl.BaseRepl( + config, cast(CursorAwareWindow, None), **kwargs + ) os.environ["PAGER"] = "true" os.environ.pop("PYTHONSTARTUP", None) repl.width = 50 From 995747d64d03291c0180bf8c6932a96d7541696a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 6 May 2022 00:07:56 +0200 Subject: [PATCH 384/555] Fix tests --- bpython/test/test_brackets_completion.py | 2 +- bpython/test/test_curtsies_painting.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/test/test_brackets_completion.py b/bpython/test/test_brackets_completion.py index 4340ad3d0..14169d6a8 100644 --- a/bpython/test/test_brackets_completion.py +++ b/bpython/test/test_brackets_completion.py @@ -22,7 +22,7 @@ def create_repl(brackets_enabled=False, **kwargs): {"editor": "true", "brackets_completion": brackets_enabled} ) repl = curtsiesrepl.BaseRepl( - config, cast(None, CursorAwareWindow), **kwargs + config, cast(CursorAwareWindow, None), **kwargs ) os.environ["PAGER"] = "true" os.environ.pop("PYTHONSTARTUP", None) diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 813b4fb3d..2804643ce 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -58,7 +58,7 @@ class TestRepl(BaseRepl): def _request_refresh(inner_self): pass - self.repl = TestRepl(setup_config(), cast(None, CursorAwareWindow)) + self.repl = TestRepl(setup_config(), cast(CursorAwareWindow, None)) self.repl.height, self.repl.width = (5, 10) @property @@ -287,7 +287,7 @@ def _request_refresh(inner_self): self.refresh() self.repl = TestRepl( - setup_config(), cast(None, CursorAwareWindow), banner="" + setup_config(), cast(CursorAwareWindow, None), banner="" ) self.repl.height, self.repl.width = (5, 32) From 7efed26f3bd0a5f959c96d8081a464c7d42c0fdc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 11:53:23 +0200 Subject: [PATCH 385/555] Add type annotations --- bpython/curtsiesfrontend/repl.py | 61 ++++++++++++++++---------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 49d61a84b..91e55b708 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -15,6 +15,7 @@ from typing import ( Dict, Any, + Iterable, List, Optional, Sequence, @@ -161,7 +162,7 @@ def process_event(self, e: Union[events.Event, str]) -> None: else: self.repl.send_to_stdin(self.current_line) - def add_input_character(self, e): + def add_input_character(self, e: str) -> None: if e in ("",): e = " " if e.startswith("<") and e.endswith(">"): @@ -190,10 +191,10 @@ def readlines(self, size=-1): def __iter__(self): return iter(self.readlines()) - def isatty(self): + def isatty(self) -> bool: return True - def flush(self): + def flush(self) -> None: """Flush the internal buffer. This is a no-op. Flushing stdin doesn't make any sense anyway.""" @@ -202,7 +203,7 @@ def write(self, value): # others, so here's a hack to keep them happy raise OSError(errno.EBADF, "sys.stdin is read-only") - def close(self): + def close(self) -> None: # hack to make closing stdin a nop # This is useful for multiprocessing.Process, which does work # for the most part, although output from other processes is @@ -210,7 +211,7 @@ def close(self): pass @property - def encoding(self): + def encoding(self) -> str: return sys.__stdin__.encoding # TODO write a read() method? @@ -1083,7 +1084,7 @@ def down_one_line(self): ) self._set_cursor_offset(len(self.current_line), reset_rl_history=False) - def process_simple_keypress(self, e): + def process_simple_keypress(self, e: str): # '\n' needed for pastes if e in ("", "", "", "\n", "\r"): self.on_enter() @@ -1980,7 +1981,7 @@ def prompt_for_undo(): greenlet.greenlet(prompt_for_undo).switch() - def redo(self): + def redo(self) -> None: if self.redo_stack: temp = self.redo_stack.pop() self.history.append(temp) @@ -2061,7 +2062,7 @@ def initialize_interp(self) -> None: del self.coderunner.interp.locals["_repl"] - def getstdout(self): + def getstdout(self) -> str: """ Returns a string of the current bpython session, wrapped, WITH prompts. """ @@ -2096,7 +2097,7 @@ def focus_on_subprocess(self, args): finally: signal.signal(signal.SIGWINCH, prev_sigwinch_handler) - def pager(self, text): + def pager(self, text: str) -> None: """Runs an external pager on text text must be a str""" @@ -2106,7 +2107,7 @@ def pager(self, text): tmp.flush() self.focus_on_subprocess(command + [tmp.name]) - def show_source(self): + def show_source(self) -> None: try: source = self.get_source_of_current_name() except SourceNotFound as e: @@ -2118,10 +2119,10 @@ def show_source(self): ) self.pager(source) - def help_text(self): + def help_text(self) -> str: return self.version_help_text() + "\n" + self.key_help_text() - def version_help_text(self): + def version_help_text(self) -> str: help_message = _( """ Thanks for using bpython! @@ -2148,7 +2149,7 @@ def version_help_text(self): return f"bpython-curtsies version {__version__} using curtsies version {curtsies_version}\n{help_message}" - def key_help_text(self): + def key_help_text(self) -> str: NOT_IMPLEMENTED = ( "suspend", "cut to buffer", @@ -2198,15 +2199,15 @@ def ps2(self): return _process_ps(super().ps2, "... ") -def is_nop(char): - return unicodedata.category(str(char)) == "Cc" +def is_nop(char: str) -> bool: + return unicodedata.category(char) == "Cc" -def tabs_to_spaces(line): +def tabs_to_spaces(line: str) -> str: return line.replace("\t", " ") -def _last_word(line): +def _last_word(line: str) -> str: split_line = line.split() return split_line.pop() if split_line else "" @@ -2230,29 +2231,29 @@ def compress_paste_event(paste_event): return None -def just_simple_events(event_list): +def just_simple_events( + event_list: Iterable[Union[str, events.Event]] +) -> List[str]: simple_events = [] for e in event_list: + if isinstance(e, events.Event): + continue # ignore events # '\n' necessary for pastes - if e in ("", "", "", "\n", "\r"): + elif e in ("", "", "", "\n", "\r"): simple_events.append("\n") - elif isinstance(e, events.Event): - pass # ignore events - elif e in ("",): + elif e == "": simple_events.append(" ") elif len(e) > 1: - pass # get rid of etc. + continue # get rid of etc. else: simple_events.append(e) return simple_events -def is_simple_event(e): +def is_simple_event(e: Union[str, events.Event]) -> bool: if isinstance(e, events.Event): return False - if e in ("", "", "", "\n", "\r", ""): - return True - if len(e) > 1: - return False - else: - return True + return ( + e in ("", "", "", "\n", "\r", "") + or len(e) <= 1 + ) From 2df70ac1eab0b17d2ec62e19b5e64fe610a19fb6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 5 May 2022 11:53:53 +0200 Subject: [PATCH 386/555] Simplify some checks --- bpython/curtsiesfrontend/repl.py | 83 ++++++++++++++++---------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 91e55b708..d5a3eed15 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -122,40 +122,42 @@ def process_event(self, e: Union[events.Event, str]) -> None: assert self.has_focus logger.debug("fake input processing event %r", e) - if isinstance(e, events.PasteEvent): - for ee in e.events: - if ee not in self.rl_char_sequences: - self.add_input_character(ee) - elif e in self.rl_char_sequences: - self.cursor_offset, self.current_line = self.rl_char_sequences[e]( - self.cursor_offset, self.current_line - ) - elif isinstance(e, events.SigIntEvent): - self.coderunner.sigint_happened_in_main_context = True - self.has_focus = False - self.current_line = "" - self.cursor_offset = 0 - self.repl.run_code_and_maybe_finish() - elif e in ("",): - pass - elif e in ("",): - if self.current_line == "": - self.repl.send_to_stdin("\n") + if isinstance(e, events.Event): + if isinstance(e, events.PasteEvent): + for ee in e.events: + if ee not in self.rl_char_sequences: + self.add_input_character(ee) + elif isinstance(e, events.SigIntEvent): + self.coderunner.sigint_happened_in_main_context = True self.has_focus = False self.current_line = "" self.cursor_offset = 0 - self.repl.run_code_and_maybe_finish(for_code="") - else: + self.repl.run_code_and_maybe_finish() + else: + if e in self.rl_char_sequences: + self.cursor_offset, self.current_line = self.rl_char_sequences[ + e + ](self.cursor_offset, self.current_line) + elif e in ("",): pass - elif e in ("\n", "\r", "", ""): - line = self.current_line - self.repl.send_to_stdin(line + "\n") - self.has_focus = False - self.current_line = "" - self.cursor_offset = 0 - self.repl.run_code_and_maybe_finish(for_code=line + "\n") - else: # add normal character - self.add_input_character(e) + elif e in ("",): + if self.current_line == "": + self.repl.send_to_stdin("\n") + self.has_focus = False + self.current_line = "" + self.cursor_offset = 0 + self.repl.run_code_and_maybe_finish(for_code="") + else: + pass + elif e in ("\n", "\r", "", ""): + line = self.current_line + self.repl.send_to_stdin(line + "\n") + self.has_focus = False + self.current_line = "" + self.cursor_offset = 0 + self.repl.run_code_and_maybe_finish(for_code=line + "\n") + else: # add normal character + self.add_input_character(e) if self.current_line.endswith(("\n", "\r")): pass @@ -163,17 +165,16 @@ def process_event(self, e: Union[events.Event, str]) -> None: self.repl.send_to_stdin(self.current_line) def add_input_character(self, e: str) -> None: - if e in ("",): + if e == "": e = " " if e.startswith("<") and e.endswith(">"): return assert len(e) == 1, "added multiple characters: %r" % e logger.debug("adding normal char %r to current line", e) - c = e self.current_line = ( self.current_line[: self.cursor_offset] - + c + + e + self.current_line[self.cursor_offset :] ) self.cursor_offset += 1 @@ -756,11 +757,11 @@ def process_key_event(self, e: str) -> None: self.up_one_line() elif e in ("",) + key_dispatch[self.config.down_one_line_key]: self.down_one_line() - elif e in ("",): + elif e == "": self.on_control_d() - elif e in ("",): + elif e == "": self.operate_and_get_next() - elif e in ("",): + elif e == "": self.get_last_word() elif e in key_dispatch[self.config.reverse_incremental_search_key]: self.incremental_search(reverse=True) @@ -796,9 +797,9 @@ def process_key_event(self, e: str) -> None: raise SystemExit() elif e in ("\n", "\r", "", "", ""): self.on_enter() - elif e in ("",): # tab + elif e == "": # tab self.on_tab() - elif e in ("",): + elif e == "": self.on_tab(back=True) elif e in key_dispatch[self.config.undo_key]: # ctrl-r for undo self.prompt_undo() @@ -817,9 +818,9 @@ def process_key_event(self, e: str) -> None: # TODO add PAD keys hack as in bpython.cli elif e in key_dispatch[self.config.edit_current_block_key]: self.send_current_block_to_external_editor() - elif e in ("",): + elif e == "": self.incr_search_mode = SearchMode.NO_SEARCH - elif e in ("",): + elif e == "": self.add_normal_character(" ") elif e in CHARACTER_PAIR_MAP.keys(): if e in ["'", '"']: @@ -1093,7 +1094,7 @@ def process_simple_keypress(self, e: str): self.process_event(bpythonevents.RefreshRequestEvent()) elif isinstance(e, events.Event): pass # ignore events - elif e in ("",): + elif e == "": self.add_normal_character(" ") else: self.add_normal_character(e) From 7d3f3d961af070c0fb1f03dcb704ddd6b14e2440 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 6 May 2022 09:15:55 +0200 Subject: [PATCH 387/555] Remove unused import --- bpython/repl.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index e0d42b1f9..06934627e 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -65,10 +65,6 @@ have_pyperclip = False from . import autocomplete, inspection, simpleeval - -if TYPE_CHECKING: - from .cli import Statusbar - from .config import getpreferredencoding, Config from .formatter import Parenthesis from .history import History From b3b74b8f49fcdee8d97ac881eca18e93f0b9621a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 6 May 2022 09:17:40 +0200 Subject: [PATCH 388/555] eval wants a dict, so make sure that the Interpreter has a dict --- bpython/cli.py | 2 +- bpython/repl.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 5383a7c38..2512d6ffe 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -1940,7 +1940,7 @@ def main_curses( args: List[str], config: Config, interactive: bool = True, - locals_: Optional[MutableMapping[str, str]] = None, + locals_: Optional[Dict[str, Any]] = None, banner: Optional[str] = None, ) -> Tuple[Tuple[Any, ...], str]: """main function for the curses convenience wrapper diff --git a/bpython/repl.py b/bpython/repl.py index 06934627e..e41536fd0 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -48,7 +48,6 @@ Optional, Type, Union, - MutableMapping, Callable, Dict, TYPE_CHECKING, @@ -109,7 +108,7 @@ class Interpreter(code.InteractiveInterpreter): def __init__( self, - locals: Optional[MutableMapping[str, Any]] = None, + locals: Optional[Dict[str, Any]] = None, ) -> None: """Constructor. From dc667b6c9cc72117588559f30c89501781052efa Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 6 May 2022 14:17:38 +0200 Subject: [PATCH 389/555] Directly initialize f_strings --- bpython/curtsiesfrontend/interpreter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 1b3133459..95e4b0691 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -44,9 +44,7 @@ class BPythonFormatter(Formatter): straightforward.""" def __init__(self, color_scheme, **options): - self.f_strings = {} - for k, v in color_scheme.items(): - self.f_strings[k] = f"\x01{v}" + self.f_strings = {k: f"\x01{v}" for k, v in color_scheme.items()} super().__init__(**options) def format(self, tokensource, outfile): From 492e3d5ff2643191f023e98b8959792ed4bfa0a3 Mon Sep 17 00:00:00 2001 From: rcreddyn Date: Sat, 15 Jan 2022 01:11:23 +0530 Subject: [PATCH 390/555] Added type annotations Sebastian: fixed some type annotations --- bpython/curtsiesfrontend/interpreter.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 95e4b0691..80b7d5f5b 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -1,11 +1,13 @@ import sys -from typing import Any, Dict, Optional +from codeop import CommandCompiler +from typing import Any, Dict, Iterable, Optional, Tuple, Union from pygments.token import Generic, Token, Keyword, Name, Comment, String from pygments.token import Error, Literal, Number, Operator, Punctuation -from pygments.token import Whitespace +from pygments.token import Whitespace, _TokenType from pygments.formatter import Formatter from pygments.lexers import get_lexer_by_name +from curtsies.formatstring import FmtStr from ..curtsiesfrontend.parse import parse from ..repl import Interpreter as ReplInterpreter @@ -43,7 +45,11 @@ class BPythonFormatter(Formatter): See the Pygments source for more info; it's pretty straightforward.""" - def __init__(self, color_scheme, **options): + def __init__( + self, + 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()} super().__init__(**options) @@ -71,7 +77,7 @@ def __init__( # typically changed after being instantiated # but used when interpreter used corresponding REPL - def write(err_line): + def write(err_line: Union[str, FmtStr]) -> None: """Default stderr handler for tracebacks Accepts FmtStrs so interpreters can output them""" @@ -80,13 +86,14 @@ def write(err_line): self.write = write # type: ignore self.outfile = self - def writetb(self, lines): + def writetb(self, lines: Iterable[str]) -> None: tbtext = "".join(lines) lexer = get_lexer_by_name("pytb") self.format(tbtext, lexer) # TODO for tracebacks get_lexer_by_name("pytb", stripall=True) - def format(self, tbtext, lexer): + def format(self, tbtext: str, lexer: Any) -> None: + # FIXME: lexer should is a Lexer traceback_informative_formatter = BPythonFormatter(default_colors) traceback_code_formatter = BPythonFormatter({Token: ("d")}) tokens = list(lexer.get_tokens(tbtext)) @@ -112,7 +119,9 @@ def format(self, tbtext, lexer): assert cur_line == [], cur_line -def code_finished_will_parse(s, compiler): +def code_finished_will_parse( + s: str, compiler: CommandCompiler +) -> Tuple[bool, bool]: """Returns a tuple of whether the buffer could be complete and whether it will parse From 711845fa99638eae4a56d699ac70ae5e5c2a7665 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 May 2022 19:05:40 +0200 Subject: [PATCH 391/555] Iterate directly over result of get_tokens --- bpython/curtsiesfrontend/interpreter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index 80b7d5f5b..bf3f6ad80 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -96,11 +96,10 @@ def format(self, tbtext: str, lexer: Any) -> None: # FIXME: lexer should is a Lexer traceback_informative_formatter = BPythonFormatter(default_colors) traceback_code_formatter = BPythonFormatter({Token: ("d")}) - tokens = list(lexer.get_tokens(tbtext)) no_format_mode = False cur_line = [] - for token, text in tokens: + for token, text in lexer.get_tokens(tbtext): if text.endswith("\n"): cur_line.append((token, text)) if no_format_mode: From 044bc579106181f79e0113bd2b0e7bac1c37a537 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 May 2022 19:33:55 +0200 Subject: [PATCH 392/555] Remove unused type stub --- stubs/blessings.pyi | 47 --------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 stubs/blessings.pyi diff --git a/stubs/blessings.pyi b/stubs/blessings.pyi deleted file mode 100644 index 66fd96216..000000000 --- a/stubs/blessings.pyi +++ /dev/null @@ -1,47 +0,0 @@ -from typing import ContextManager, Text, IO - -class Terminal: - def __init__(self, stream=None, force_styling=False): - # type: (IO, bool) -> None - pass - def location(self, x=None, y=None): - # type: (int, int) -> ContextManager - pass - @property - def hide_cursor(self): - # type: () -> Text - pass - @property - def normal_cursor(self): - # type: () -> Text - pass - @property - def height(self): - # type: () -> int - pass - @property - def width(self): - # type: () -> int - pass - def fullscreen(self): - # type: () -> ContextManager - pass - def move(self, y, x): - # type: (int, int) -> Text - pass - @property - def clear_eol(self): - # type: () -> Text - pass - @property - def clear_bol(self): - # type: () -> Text - pass - @property - def clear_eos(self): - # type: () -> Text - pass - @property - def clear_eos(self): - # type: () -> Text - pass From 4ec5d763a164c37f48ca18fcf584971592648366 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 May 2022 19:42:16 +0200 Subject: [PATCH 393/555] Ignore a mypy bug --- bpython/curtsiesfrontend/interpreter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index bf3f6ad80..f2a83678e 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -51,7 +51,8 @@ def __init__( **options: Union[str, bool, None], ) -> None: self.f_strings = {k: f"\x01{v}" for k, v in color_scheme.items()} - super().__init__(**options) + # FIXME: mypy currently fails to handle this properly + super().__init__(**options) # type: ignore def format(self, tokensource, outfile): o = "" From df32e68b48b23d32073ecbfad36ff115d5fbade9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 May 2022 19:45:09 +0200 Subject: [PATCH 394/555] Some simplifications --- bpython/curtsiesfrontend/interpreter.py | 13 +++++-------- bpython/curtsiesfrontend/repl.py | 6 ++---- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index f2a83678e..82e28091c 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -94,9 +94,9 @@ def writetb(self, lines: Iterable[str]) -> None: # TODO for tracebacks get_lexer_by_name("pytb", stripall=True) def format(self, tbtext: str, lexer: Any) -> None: - # FIXME: lexer should is a Lexer + # FIXME: lexer should be "Lexer" traceback_informative_formatter = BPythonFormatter(default_colors) - traceback_code_formatter = BPythonFormatter({Token: ("d")}) + traceback_code_formatter = BPythonFormatter({Token: "d"}) no_format_mode = False cur_line = [] @@ -111,7 +111,7 @@ def format(self, tbtext: str, lexer: Any) -> None: cur_line, self.outfile ) cur_line = [] - elif text == " " and cur_line == []: + elif text == " " and len(cur_line) == 0: no_format_mode = True cur_line.append((token, text)) else: @@ -130,9 +130,6 @@ def code_finished_will_parse( False, True means code block is unfinished False, False isn't possible - an predicted error makes code block done""" try: - finished = bool(compiler(s)) - code_will_parse = True + return bool(compiler(s)), True except (ValueError, SyntaxError, OverflowError): - finished = True - code_will_parse = False - return finished, code_will_parse + return True, False diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index d5a3eed15..a29b350a4 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1363,9 +1363,8 @@ def unhighlight_paren(self): def clear_current_block(self, remove_from_history=True): self.display_buffer = [] if remove_from_history: - for unused in self.buffer: - self.history.pop() - self.all_logical_lines.pop() + del self.history[-len(self.buffer) :] + del self.all_logical_lines[-len(self.buffer) :] self.buffer = [] self.cursor_offset = 0 self.saved_indent = 0 @@ -2080,7 +2079,6 @@ def focus_on_subprocess(self, args): try: signal.signal(signal.SIGWINCH, self.orig_sigwinch_handler) with Termmode(self.orig_stdin, self.orig_tcattrs): - assert self.window is not None terminal = self.window.t with terminal.fullscreen(): sys.__stdout__.write(terminal.save) From ebc920677874007ca7e6e548665cb6b312dddb30 Mon Sep 17 00:00:00 2001 From: Ulises Date: Fri, 27 May 2022 09:00:02 +0200 Subject: [PATCH 395/555] Allowing sys.stind.readline receive size parameter --- bpython/curtsiesfrontend/repl.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index a29b350a4..bf723742c 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -179,12 +179,17 @@ def add_input_character(self, e: str) -> None: ) self.cursor_offset += 1 - def readline(self): - self.has_focus = True - self.repl.send_to_stdin(self.current_line) - value = self.coderunner.request_from_main_context() - self.readline_results.append(value) - return value + def readline(self, size=-1): + if not isinstance(size, int): + raise TypeError(f"'{type(size).__name__}' object cannot be interpreted as an integer") + elif size == 0: + return '' + else: + self.has_focus = True + self.repl.send_to_stdin(self.current_line) + value = self.coderunner.request_from_main_context() + self.readline_results.append(value) + return value if size <= -1 else value[:size] def readlines(self, size=-1): return list(iter(self.readline, "")) From c0412f66be16cd0914d791aa4f2dfcdf85f499d4 Mon Sep 17 00:00:00 2001 From: Ulises Date: Fri, 27 May 2022 09:07:36 +0200 Subject: [PATCH 396/555] Applying black --- bpython/curtsiesfrontend/repl.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index bf723742c..5b052435a 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -181,9 +181,11 @@ def add_input_character(self, e: str) -> None: def readline(self, size=-1): if not isinstance(size, int): - raise TypeError(f"'{type(size).__name__}' object cannot be interpreted as an integer") + raise TypeError( + f"'{type(size).__name__}' object cannot be interpreted as an integer" + ) elif size == 0: - return '' + return "" else: self.has_focus = True self.repl.send_to_stdin(self.current_line) From 624a1e1abe9df081761f79bbaf8d9faa8843f320 Mon Sep 17 00:00:00 2001 From: Ulises Date: Sat, 28 May 2022 09:33:39 +0200 Subject: [PATCH 397/555] adding annotations and removing else statement --- bpython/curtsiesfrontend/repl.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 5b052435a..16d8a8c83 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -179,19 +179,18 @@ def add_input_character(self, e: str) -> None: ) self.cursor_offset += 1 - def readline(self, size=-1): + def readline(self, size: int = -1) -> str: if not isinstance(size, int): raise TypeError( f"'{type(size).__name__}' object cannot be interpreted as an integer" ) elif size == 0: return "" - else: - self.has_focus = True - self.repl.send_to_stdin(self.current_line) - value = self.coderunner.request_from_main_context() - self.readline_results.append(value) - return value if size <= -1 else value[:size] + self.has_focus = True + self.repl.send_to_stdin(self.current_line) + value = self.coderunner.request_from_main_context() + self.readline_results.append(value) + return value if size <= -1 else value[:size] def readlines(self, size=-1): return list(iter(self.readline, "")) From c10ebb71ba54007f2665f7e86a966ed4b859729f Mon Sep 17 00:00:00 2001 From: Ulises Date: Sat, 28 May 2022 09:43:41 +0200 Subject: [PATCH 398/555] adjusting annotations --- bpython/curtsiesfrontend/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 16d8a8c83..20188f506 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -179,7 +179,7 @@ def add_input_character(self, e: str) -> None: ) self.cursor_offset += 1 - def readline(self, size: int = -1) -> str: + def readline(self, size: int = -1) -> Union[str, Any]: if not isinstance(size, int): raise TypeError( f"'{type(size).__name__}' object cannot be interpreted as an integer" From b1966098508393db0a90a6e5c32e0598c0dbfb6b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 28 May 2022 17:08:01 +0200 Subject: [PATCH 399/555] Assert that returned value is a str Anything else would be a bug. --- bpython/curtsiesfrontend/repl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 20188f506..dd7e9b68b 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -179,7 +179,7 @@ def add_input_character(self, e: str) -> None: ) self.cursor_offset += 1 - def readline(self, size: int = -1) -> Union[str, Any]: + def readline(self, size: int = -1) -> str: if not isinstance(size, int): raise TypeError( f"'{type(size).__name__}' object cannot be interpreted as an integer" @@ -189,6 +189,7 @@ def readline(self, size: int = -1) -> Union[str, Any]: self.has_focus = True self.repl.send_to_stdin(self.current_line) value = self.coderunner.request_from_main_context() + assert isinstance(value, str) self.readline_results.append(value) return value if size <= -1 else value[:size] From 6bdeadde6b6705fb9f994d6f72fd591e6c7c46c4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 28 May 2022 17:21:14 +0200 Subject: [PATCH 400/555] Also handle size argument in readlines --- bpython/curtsiesfrontend/repl.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index dd7e9b68b..c92bd57c9 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -193,8 +193,22 @@ def readline(self, size: int = -1) -> str: self.readline_results.append(value) return value if size <= -1 else value[:size] - def readlines(self, size=-1): - return list(iter(self.readline, "")) + def readlines(self, size: Optional[int] = -1) -> List[str]: + if size is None: + # the default readlines implementation also accepts None + size = -1 + if not isinstance(size, int): + raise TypeError("argument should be integer or None, not 'str'") + if size <= 0: + # read as much as we can + return list(iter(self.readline, "")) + + lines = [] + while size > 0: + line = self.readline() + lines.append(line) + size -= len(line) + return lines def __iter__(self): return iter(self.readlines()) From b7e5d8d41dd86b9240eb835b451e6dd5747f9f4d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 28 May 2022 17:30:40 +0200 Subject: [PATCH 401/555] Call clear instead of recreating the instance --- bpython/curtsiesfrontend/filewatch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 8933e2930..e70325ab5 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -32,8 +32,8 @@ def __init__( super().__init__() def reset(self) -> None: - self.dirs = defaultdict(set) - del self.modules_to_add_later[:] + self.dirs.clear() + self.modules_to_add_later.clear() self.observer.unschedule_all() def _add_module(self, path: str) -> None: @@ -70,7 +70,7 @@ def activate(self) -> None: self.observer.schedule(self, dirname, recursive=False) for module in self.modules_to_add_later: self._add_module(module) - del self.modules_to_add_later[:] + self.modules_to_add_later.clear() self.activated = True def deactivate(self) -> None: From 0c1c24f210dc04efe4e4635acc76097dbeba94b4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 28 May 2022 17:31:24 +0200 Subject: [PATCH 402/555] Simplify some of the branches --- bpython/curtsiesfrontend/repl.py | 43 +++++++++++++------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index c92bd57c9..3ef3f902d 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -133,35 +133,28 @@ def process_event(self, e: Union[events.Event, str]) -> None: self.current_line = "" self.cursor_offset = 0 self.repl.run_code_and_maybe_finish() - else: - if e in self.rl_char_sequences: - self.cursor_offset, self.current_line = self.rl_char_sequences[ - e - ](self.cursor_offset, self.current_line) - elif e in ("",): - pass - elif e in ("",): - if self.current_line == "": - self.repl.send_to_stdin("\n") - self.has_focus = False - self.current_line = "" - self.cursor_offset = 0 - self.repl.run_code_and_maybe_finish(for_code="") - else: - pass - elif e in ("\n", "\r", "", ""): - line = self.current_line - self.repl.send_to_stdin(line + "\n") + elif e in self.rl_char_sequences: + self.cursor_offset, self.current_line = self.rl_char_sequences[e]( + self.cursor_offset, self.current_line + ) + elif e == "": + if not len(self.current_line): + self.repl.send_to_stdin("\n") self.has_focus = False self.current_line = "" self.cursor_offset = 0 - self.repl.run_code_and_maybe_finish(for_code=line + "\n") - else: # add normal character - self.add_input_character(e) + self.repl.run_code_and_maybe_finish(for_code="") + elif e in ("\n", "\r", "", ""): + line = f"{self.current_line}\n" + self.repl.send_to_stdin(line) + self.has_focus = False + self.current_line = "" + self.cursor_offset = 0 + self.repl.run_code_and_maybe_finish(for_code=line) + elif e != "": # add normal character + self.add_input_character(e) - if self.current_line.endswith(("\n", "\r")): - pass - else: + if not self.current_line.endswith(("\n", "\r")): self.repl.send_to_stdin(self.current_line) def add_input_character(self, e: str) -> None: From f076f326fe5f50454de0c0554bdce4f58d63fb61 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 28 May 2022 17:32:53 +0200 Subject: [PATCH 403/555] Add more type annotations --- bpython/curtsiesfrontend/repl.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 3ef3f902d..c513d8661 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -13,15 +13,15 @@ from enum import Enum from types import FrameType, TracebackType from typing import ( - Dict, Any, Iterable, + Dict, List, Optional, Sequence, Tuple, - Union, Type, + Union, ) from .._typing_compat import Literal @@ -55,7 +55,11 @@ Interp, code_finished_will_parse, ) -from .manual_readline import edit_keys, cursor_on_closing_char_pair +from .manual_readline import ( + edit_keys, + cursor_on_closing_char_pair, + AbstractEdits, +) from .parse import parse as bpythonparse, func_for_letter, color_for_letter from .preprocess import preprocess from .. import __version__ @@ -105,15 +109,20 @@ class FakeStdin: In user code, sys.stdin.read() asks the user for interactive input, so this class returns control to the UI to get that input.""" - def __init__(self, coderunner, repl, configured_edit_keys=None): + def __init__( + self, + coderunner: CodeRunner, + repl: "BaseRepl", + configured_edit_keys: Optional[AbstractEdits] = None, + ): self.coderunner = coderunner self.repl = repl self.has_focus = False # whether FakeStdin receives keypress events self.current_line = "" self.cursor_offset = 0 self.old_num_lines = 0 - self.readline_results = [] - if configured_edit_keys: + self.readline_results: List[str] = [] + if configured_edit_keys is not None: self.rl_char_sequences = configured_edit_keys else: self.rl_char_sequences = edit_keys @@ -236,7 +245,7 @@ class ReevaluateFakeStdin: """Stdin mock used during reevaluation (undo) so raw_inputs don't have to be reentered""" - def __init__(self, fakestdin, repl): + def __init__(self, fakestdin: FakeStdin, repl: "BaseRepl"): self.fakestdin = fakestdin self.repl = repl self.readline_results = fakestdin.readline_results[:] From 03f4ddb2299f052953037fd0f00d05ddefdc508c Mon Sep 17 00:00:00 2001 From: jgart <47760695+jgarte@users.noreply.github.com> Date: Tue, 21 Jun 2022 23:13:50 -0500 Subject: [PATCH 404/555] Add GNU Guix installation instructions --- README.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.rst b/README.rst index 6cfd5663e..5e50e65a9 100644 --- a/README.rst +++ b/README.rst @@ -146,6 +146,14 @@ Fedora users can install ``bpython`` directly from the command line using ``dnf` .. code-block:: bash $ dnf install bpython + +GNU Guix +---------- +Guix users can install ``bpython`` on any GNU/Linux distribution directly from the command line: + +.. code-block:: bash + + $ guix install bpython macOS ----- From 27665ec8679ea9f57a0100672d8ab21d354179be Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sun, 17 Jul 2022 08:41:46 +1000 Subject: [PATCH 405/555] docs: fix simple typo, blockied -> blocked There is a small typo in bpython/curtsiesfrontend/repl.py. Should read `blocked` rather than `blockied`. Signed-off-by: Tim Gates --- bpython/curtsiesfrontend/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index c513d8661..ef7081617 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -582,7 +582,7 @@ def request_reload(self, files_modified: Sequence[str] = ()) -> None: def schedule_refresh(self, when: float = 0) -> None: """Schedule a ScheduledRefreshRequestEvent for when. - Such a event should interrupt if blockied waiting for keyboard input""" + Such a event should interrupt if blocked waiting for keyboard input""" if self.reevaluating or self.paste_mode: self.fake_refresh_requested = True else: From 23c1945e465b5905572ebf733f64086505e5c027 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 25 Aug 2022 17:39:30 +0200 Subject: [PATCH 406/555] Add failing test for #966 --- bpython/test/test_inspection.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index fdbb959c9..8c1556232 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -140,6 +140,37 @@ def test_getfuncprops_numpy_array(self): # np.array(object, dtype=None, *, ...). self.assertEqual(props.argspec.args, ["object", "dtype"]) + @unittest.expectedFailure + def test_issue_966_freestanding(self): + def fun(number, lst=[]): + """ + Return a list of numbers + + Example: + ======== + C.cmethod(1337, [1, 2]) # => [1, 2, 1337] + """ + return lst + [number] + + def fun_annotations(number: int, lst: list[int] = []) -> list[int]: + """ + Return a list of numbers + + Example: + ======== + C.cmethod(1337, [1, 2]) # => [1, 2, 1337] + """ + return lst + [number] + + props = inspection.getfuncprops("fun", fun) + self.assertEqual(props.func, "fun") + self.assertEqual(props.argspec.args, ["number", "lst"]) + self.assertEqual(props.argspec.defaults[0], []) + + props = inspection.getfuncprops("fun_annotations", fun_annotations) + self.assertEqual(props.func, "fun_annotations") + self.assertEqual(props.argspec.args, ["number", "lst"]) + self.assertEqual(props.argspec.defaults[0], []) class A: a = "a" From 231adb15dc718fe10912286d360a15d14a59683c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 25 Aug 2022 17:41:29 +0200 Subject: [PATCH 407/555] Following handling of empty values according to the documentation --- bpython/inspection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 5ebfdbe85..67c7d0377 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -292,12 +292,12 @@ def get_argspec_from_signature(f): signature = inspect.signature(f) for parameter in signature.parameters.values(): - if parameter.annotation is not inspect._empty: + if parameter.annotation is not parameter.empty: annotations[parameter.name] = parameter.annotation if parameter.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: args.append(parameter.name) - if parameter.default is not inspect._empty: + if parameter.default is not parameter.empty: defaults.append(parameter.default) elif parameter.kind == inspect.Parameter.POSITIONAL_ONLY: args.append(parameter.name) From 9bbb25d1125a69fe650ff36cd2201245055a60ff Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Aug 2022 23:51:59 +0200 Subject: [PATCH 408/555] Add another tests for function signatures --- bpython/test/test_inspection.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 8c1556232..9110fca30 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -77,6 +77,13 @@ def spam(eggs=23, foobar="yay"): self.assertEqual(repr(defaults[0]), "23") self.assertEqual(repr(defaults[1]), "'yay'") + def test_pasekeywordpairs_annotation(self): + def spam(eggs: str = "foo, bar"): + pass + + defaults = inspection.getfuncprops("spam", spam).argspec.defaults + self.assertEqual(repr(defaults[0]), "'foo, bar'") + def test_get_encoding_ascii(self): self.assertEqual(inspection.get_encoding(encoding_ascii), "ascii") self.assertEqual(inspection.get_encoding(encoding_ascii.foo), "ascii") From c6e513c91a07a2b8ed91a73c96bff8591eaee080 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 26 Aug 2022 23:52:46 +0200 Subject: [PATCH 409/555] Handle type annotations in function signatures --- bpython/inspection.py | 22 ++++++++++++++++------ bpython/test/test_inspection.py | 1 - 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 67c7d0377..8f6773fbc 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -26,7 +26,7 @@ import pydoc import re from collections import namedtuple -from typing import Any, Optional, Type +from typing import Any, Optional, Type, Dict, List from types import MemberDescriptorType, TracebackType from ._typing_compat import Literal @@ -121,15 +121,16 @@ def __repr__(self): __str__ = __repr__ -def parsekeywordpairs(signature): - tokens = Python3Lexer().get_tokens(signature) +def parsekeywordpairs(signature: str) -> Dict[str, str]: preamble = True stack = [] - substack = [] + substack: List[str] = [] parendepth = 0 - for token, value in tokens: + annotation = False + for token, value in Python3Lexer().get_tokens(signature): if preamble: if token is Token.Punctuation and value == "(": + # First "(" starts the list of arguments preamble = False continue @@ -141,14 +142,23 @@ def parsekeywordpairs(signature): elif value == ":" and parendepth == -1: # End of signature reached break + elif value == ":" and parendepth == 0: + # Start of type annotation + annotation = True + if (value == "," and parendepth == 0) or ( value == ")" and parendepth == -1 ): stack.append(substack) substack = [] + # If type annotation didn't end before, ti does now. + annotation = False continue + elif token is Token.Operator and value == "=" and parendepth == 0: + # End of type annotation + annotation = False - if value and (parendepth > 0 or value.strip()): + if value and not annotation and (parendepth > 0 or value.strip()): substack.append(value) return {item[0]: "".join(item[2:]) for item in stack if len(item) >= 3} diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 9110fca30..333abbbd4 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -147,7 +147,6 @@ def test_getfuncprops_numpy_array(self): # np.array(object, dtype=None, *, ...). self.assertEqual(props.argspec.args, ["object", "dtype"]) - @unittest.expectedFailure def test_issue_966_freestanding(self): def fun(number, lst=[]): """ From c2e3faca367162dddf1e3f7cd062a5cb770db7cf Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 27 Aug 2022 09:33:49 +0200 Subject: [PATCH 410/555] Fix type annotation --- bpython/test/test_inspection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 333abbbd4..ba9ffca7a 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -2,6 +2,7 @@ import os import sys import unittest +from typing import List from bpython import inspection from bpython.test.fodder import encoding_ascii @@ -158,7 +159,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 From d42b21aaef45b9fec0519fbf044354ec81986c74 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 18:09:23 +0200 Subject: [PATCH 411/555] Turn FuncProps into a dataclass --- bpython/autocomplete.py | 6 +++--- bpython/cli.py | 6 +++--- bpython/inspection.py | 15 +++++++++++---- bpython/test/test_autocomplete.py | 6 +++--- bpython/urwid.py | 4 +++- 5 files changed, 23 insertions(+), 14 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 81cea8945..e623359ef 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -570,11 +570,11 @@ def matches( matches = { f"{name}=" - for name in argspec[1][0] + for name in argspec.argspec[0] if isinstance(name, str) and name.startswith(r.word) } matches.update( - name + "=" for name in argspec[1][4] if name.startswith(r.word) + name + "=" for name in argspec.argspec[4] if name.startswith(r.word) ) return matches if matches else None @@ -711,7 +711,7 @@ def get_completer( line is a string of the current line kwargs (all optional): locals_ is a dictionary of the environment - argspec is an inspect.ArgSpec instance for the current function where + argspec is an inspect.FuncProps instance for the current function where the cursor is current_block is the possibly multiline not-yet-evaluated block of code which the current line is part of diff --git a/bpython/cli.py b/bpython/cli.py index 2512d6ffe..ba85729f0 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -101,7 +101,7 @@ from . import translations from .translations import _ -from . import repl +from . import repl, inspection from . import args as bpargs from .pager import page from .args import parse as argsparse @@ -726,7 +726,7 @@ def lf(self) -> None: def mkargspec( self, - topline: Any, # Named tuples don't seem to play nice with mypy + topline: inspection.FuncProps, in_arg: Union[str, int, None], down: bool, ) -> int: @@ -1298,7 +1298,7 @@ def show_list( self, items: List[str], arg_pos: Union[str, int, None], - topline: Any = None, # Named tuples don't play nice with mypy + topline: Optional[inspection.FuncProps] = None, formatter: Optional[Callable] = None, current_item: Union[str, Literal[False]] = None, ) -> None: diff --git a/bpython/inspection.py b/bpython/inspection.py index 8f6773fbc..4bd67b4fd 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -26,7 +26,8 @@ import pydoc import re from collections import namedtuple -from typing import Any, Optional, Type, Dict, List +from dataclasses import dataclass +from typing import Any, Callable, Optional, Type, Dict, List from types import MemberDescriptorType, TracebackType from ._typing_compat import Literal @@ -35,6 +36,7 @@ from .lazyre import LazyReCompile + ArgSpec = namedtuple( "ArgSpec", [ @@ -48,7 +50,12 @@ ], ) -FuncProps = namedtuple("FuncProps", ["func", "argspec", "is_bound_method"]) + +@dataclass +class FuncProps: + func: str + argspec: ArgSpec + is_bound_method: bool class AttrCleaner: @@ -112,10 +119,10 @@ class _Repr: Helper for `fixlongargs()`: Returns the given value in `__repr__()`. """ - def __init__(self, value): + def __init__(self, value: str) -> None: self.value = value - def __repr__(self): + def __repr__(self) -> str: return self.value __str__ = __repr__ diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 8b171d035..bfc3b8308 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -11,7 +11,7 @@ except ImportError: has_jedi = False -from bpython import autocomplete +from bpython import autocomplete, inspection from bpython.line import LinePart glob_function = "glob.iglob" @@ -418,8 +418,8 @@ def test_set_of_params_returns_when_matches_found(self): def func(apple, apricot, banana, carrot): pass - argspec = list(inspect.getfullargspec(func)) - argspec = ["func", argspec, False] + argspec = inspection.ArgSpec(*inspect.getfullargspec(func)) + argspec = inspection.FuncProps("func", argspec, False) com = autocomplete.ParameterNameCompletion() self.assertSetEqual( com.matches(1, "a", argspec=argspec), {"apple=", "apricot="} diff --git a/bpython/urwid.py b/bpython/urwid.py index 0a2ffe31b..6b329e651 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -762,7 +762,9 @@ def _populate_completion(self): if self.complete(): if self.funcprops: # This is mostly just stolen from the cli module. - func_name, args, is_bound = self.funcprops + func_name = self.funcprops.func + args = self.funcprops.argspec + is_bound = self.funcprops.is_bound_method in_arg = self.arg_pos args, varargs, varkw, defaults = args[:4] kwonly = self.funcprops.argspec.kwonly From ba1dac78d1623a4b9a2498a7f6b03ca7c6b4e01f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 18:09:48 +0200 Subject: [PATCH 412/555] Remove an if block that is never executed --- bpython/inspection.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 4bd67b4fd..895756b52 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -270,8 +270,6 @@ def getfuncprops(func, f): try: argspec = get_argspec_from_signature(f) fixlongargs(f, argspec) - if len(argspec) == 4: - argspec = argspec + [list(), dict(), None] argspec = ArgSpec(*argspec) fprops = FuncProps(func, argspec, is_bound_method) except (TypeError, KeyError, ValueError): From 84a54677b92c6d9c124653e9c28a57ff3339312a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 18:25:49 +0200 Subject: [PATCH 413/555] Turn ArgSpec into a dataclass --- bpython/autocomplete.py | 4 +-- bpython/inspection.py | 69 ++++++++++++++++------------------------- bpython/urwid.py | 6 ++-- 3 files changed, 33 insertions(+), 46 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index e623359ef..9acc95ebf 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -570,11 +570,11 @@ def matches( matches = { f"{name}=" - for name in argspec.argspec[0] + for name in argspec.argspec.args if isinstance(name, str) and name.startswith(r.word) } matches.update( - name + "=" for name in argspec.argspec[4] if name.startswith(r.word) + name + "=" for name in argspec.argspec.kwonly if name.startswith(r.word) ) return matches if matches else None diff --git a/bpython/inspection.py b/bpython/inspection.py index 895756b52..676c3aa34 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -25,7 +25,6 @@ import keyword import pydoc import re -from collections import namedtuple from dataclasses import dataclass from typing import Any, Callable, Optional, Type, Dict, List from types import MemberDescriptorType, TracebackType @@ -37,18 +36,15 @@ from .lazyre import LazyReCompile -ArgSpec = namedtuple( - "ArgSpec", - [ - "args", - "varargs", - "varkwargs", - "defaults", - "kwonly", - "kwonly_defaults", - "annotations", - ], -) +@dataclass +class ArgSpec: + args: List[str] + varargs: Optional[str] + varkwargs: Optional[str] + defaults: Optional[List[Any]] + kwonly: List[str] + kwonly_defaults: Optional[Dict[str, Any]] + annotations: Optional[Dict[str, Any]] @dataclass @@ -171,24 +167,24 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: return {item[0]: "".join(item[2:]) for item in stack if len(item) >= 3} -def fixlongargs(f, argspec): +def fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: """Functions taking default arguments that are references to other objects whose str() is too big will cause breakage, so we swap out the object itself with the name it was referenced with in the source by parsing the source itself !""" - if argspec[3] is None: + if argspec.defaults is None: # No keyword args, no need to do anything - return - values = list(argspec[3]) + return argspec + values = list(argspec.defaults) if not values: - return - keys = argspec[0][-len(values) :] + return argspec + keys = argspec.args[-len(values) :] try: src = inspect.getsourcelines(f) except (OSError, IndexError): # IndexError is raised in inspect.findsource(), can happen in # some situations. See issue #94. - return + return argspec signature = "".join(src[0]) kwparsed = parsekeywordpairs(signature) @@ -196,7 +192,8 @@ def fixlongargs(f, argspec): if len(repr(value)) != len(kwparsed[key]): values[i] = _Repr(kwparsed[key]) - argspec[3] = values + argspec.defaults = values + return argspec getpydocspec_re = LazyReCompile( @@ -247,7 +244,7 @@ def getpydocspec(f, func): ) -def getfuncprops(func, f): +def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: # 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: @@ -268,9 +265,8 @@ def getfuncprops(func, f): # '__init__' throws xmlrpclib.Fault (see #202) return None try: - argspec = get_argspec_from_signature(f) - fixlongargs(f, argspec) - argspec = ArgSpec(*argspec) + argspec = _get_argspec_from_signature(f) + argspec = fixlongargs(f, argspec) fprops = FuncProps(func, argspec, is_bound_method) except (TypeError, KeyError, ValueError): argspec = getpydocspec(f, func) @@ -289,7 +285,7 @@ def is_eval_safe_name(string: str) -> bool: ) -def get_argspec_from_signature(f): +def _get_argspec_from_signature(f: Callable) -> ArgSpec: """Get callable signature from inspect.signature in argspec format. inspect.signature is a Python 3 only function that returns the signature of @@ -324,26 +320,15 @@ def get_argspec_from_signature(f): elif parameter.kind == inspect.Parameter.VAR_KEYWORD: varkwargs = parameter.name - # inspect.getfullargspec returns None for 'defaults', 'kwonly_defaults' and - # 'annotations' if there are no values for them. - if not defaults: - defaults = None - - if not kwonly_defaults: - kwonly_defaults = None - - if not annotations: - annotations = None - - return [ + return ArgSpec( args, varargs, varkwargs, - defaults, + defaults if defaults else None, kwonly, - kwonly_defaults, - annotations, - ] + kwonly_defaults if kwonly_defaults else None, + annotations if annotations else None, + ) get_encoding_line_re = LazyReCompile(r"^.*coding[:=]\s*([-\w.]+).*$") diff --git a/bpython/urwid.py b/bpython/urwid.py index 6b329e651..cc828ff06 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -763,10 +763,12 @@ def _populate_completion(self): if self.funcprops: # This is mostly just stolen from the cli module. func_name = self.funcprops.func - args = self.funcprops.argspec + args = self.funcprops.argspec.args is_bound = self.funcprops.is_bound_method in_arg = self.arg_pos - args, varargs, varkw, defaults = args[:4] + varargs = self.funcprops.argspec.varargs + varkw = self.funcprops.argspec.varkwargs + defaults = self.funcprops.argspec.defaults kwonly = self.funcprops.argspec.kwonly kwonly_defaults = self.funcprops.argspec.kwonly_defaults or {} markup = [("bold name", func_name), ("name", ": (")] From 9861730ed39b5d9c9dfa97b53e337670d471f4a5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 18:34:55 +0200 Subject: [PATCH 414/555] Some clean up --- bpython/inspection.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 676c3aa34..ed201d9a7 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -185,8 +185,7 @@ def fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: # IndexError is raised in inspect.findsource(), can happen in # some situations. See issue #94. return argspec - signature = "".join(src[0]) - kwparsed = parsekeywordpairs(signature) + kwparsed = parsekeywordpairs("".join(src[0])) for i, (key, value) in enumerate(zip(keys, values)): if len(repr(value)) != len(kwparsed[key]): @@ -201,7 +200,7 @@ def fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: ) -def getpydocspec(f, func): +def _getpydocspec(f: Callable) -> Optional[ArgSpec]: try: argspec = pydoc.getdoc(f) except NameError: @@ -218,7 +217,7 @@ def getpydocspec(f, func): defaults = [] varargs = varkwargs = None kwonly_args = [] - kwonly_defaults = dict() + kwonly_defaults = {} for arg in s.group(2).split(","): arg = arg.strip() if arg.startswith("**"): @@ -266,15 +265,14 @@ def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: return None try: argspec = _get_argspec_from_signature(f) - argspec = fixlongargs(f, argspec) - fprops = FuncProps(func, argspec, is_bound_method) + fprops = FuncProps(func, fixlongargs(f, argspec), is_bound_method) except (TypeError, KeyError, ValueError): - argspec = getpydocspec(f, func) - if argspec is None: + argspec_pydoc = _getpydocspec(f) + if argspec_pydoc is None: return None if inspect.ismethoddescriptor(f): - argspec.args.insert(0, "obj") - fprops = FuncProps(func, argspec, is_bound_method) + argspec_pydoc.args.insert(0, "obj") + fprops = FuncProps(func, argspec_pydoc, is_bound_method) return fprops From b54039db00aa622d131c681fc9acfd30dc4e1362 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 22:00:46 +0200 Subject: [PATCH 415/555] Make kwargs explicit with proper type annotations Also fix handling of complete_magic_methods. --- bpython/autocomplete.py | 119 ++++++++++++++++++++---------- bpython/test/test_autocomplete.py | 14 ++-- 2 files changed, 90 insertions(+), 43 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 9acc95ebf..f8e97d73a 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -28,6 +28,7 @@ import __main__ import abc import glob +import itertools import keyword import logging import os @@ -383,11 +384,15 @@ class AttrCompletion(BaseCompletionType): attr_matches_re = LazyReCompile(r"(\w+(\.\w+)*)\.(\w*)") def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + locals_: Optional[Dict[str, Any]] = None, + **kwargs: Any, ) -> Optional[Set]: - if "locals_" not in kwargs: + if locals_ is None: return None - locals_ = cast(Dict[str, Any], kwargs["locals_"]) r = self.locate(cursor_offset, line) if r is None: @@ -465,11 +470,15 @@ def list_attributes(self, obj: Any) -> List[str]: class DictKeyCompletion(BaseCompletionType): def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + locals_: Optional[Dict[str, Any]] = None, + **kwargs: Any, ) -> Optional[Set]: - if "locals_" not in kwargs: + if locals_ is None: return None - locals_ = kwargs["locals_"] r = self.locate(cursor_offset, line) if r is None: @@ -500,11 +509,16 @@ def format(self, match: str) -> str: class MagicMethodCompletion(BaseCompletionType): def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + current_block: Optional[str] = None, + complete_magic_methods: Optional[bool] = None, + **kwargs: Any, ) -> Optional[Set]: - if "current_block" not in kwargs: + if current_block is None or complete_magic_methods is None or not complete_magic_methods: return None - current_block = kwargs["current_block"] r = self.locate(cursor_offset, line) if r is None: @@ -519,15 +533,19 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: class GlobalCompletion(BaseCompletionType): def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + locals_: Optional[Dict[str, Any]] = None, + **kwargs: Any, ) -> Optional[Set]: """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. """ - if "locals_" not in kwargs: + if locals_ is None: return None - locals_ = kwargs["locals_"] r = self.locate(cursor_offset, line) if r is None: @@ -556,25 +574,29 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: class ParameterNameCompletion(BaseCompletionType): def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + funcprops: Optional[inspection.FuncProps] = None, + **kwargs: Any, ) -> Optional[Set]: - if "argspec" not in kwargs: + if funcprops is None: return None - argspec = kwargs["argspec"] - if not argspec: - return None r = self.locate(cursor_offset, line) if r is None: return None matches = { f"{name}=" - for name in argspec.argspec.args + for name in funcprops.argspec.args if isinstance(name, str) and name.startswith(r.word) } matches.update( - name + "=" for name in argspec.argspec.kwonly if name.startswith(r.word) + name + "=" + for name in funcprops.argspec.kwonly + if name.startswith(r.word) ) return matches if matches else None @@ -588,12 +610,13 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return lineparts.current_expression_attribute(cursor_offset, line) def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + locals_: Optional[Dict[str, Any]] = None, + **kwargs: Any, ) -> Optional[Set]: - if "locals_" not in kwargs: - return None - locals_ = kwargs["locals_"] - if locals_ is None: locals_ = __main__.__dict__ @@ -629,20 +652,23 @@ class JediCompletion(BaseCompletionType): _orig_start: Optional[int] def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + history: Optional[List[str]] = None, + **kwargs: Any, ) -> Optional[Set]: - if "history" not in kwargs: + if history is None: return None - history = kwargs["history"] - if not lineparts.current_word(cursor_offset, line): return None - history = "\n".join(history) + "\n" + line + combined_history = "\n".join(itertools.chain(history, (line,))) try: - script = jedi.Script(history, path="fake.py") + script = jedi.Script(combined_history, path="fake.py") completions = script.complete( - len(history.splitlines()), cursor_offset + len(combined_history.splitlines()), cursor_offset ) except (jedi.NotFoundError, IndexError, KeyError): # IndexError for #483 @@ -679,12 +705,16 @@ def locate(self, cursor_offset: int, line: str) -> LinePart: class MultilineJediCompletion(JediCompletion): # type: ignore [no-redef] def matches( - self, cursor_offset: int, line: str, **kwargs: Any + self, + cursor_offset: int, + line: str, + *, + current_block: Optional[str] = None, + history: Optional[List[str]] = None, + **kwargs: Any, ) -> Optional[Set]: - if "current_block" not in kwargs or "history" not in kwargs: + if current_block is None or history is None: return None - current_block = kwargs["current_block"] - history = kwargs["history"] if "\n" in current_block: assert cursor_offset <= len(line), "{!r} {!r}".format( @@ -701,7 +731,12 @@ def get_completer( completers: Sequence[BaseCompletionType], cursor_offset: int, line: str, - **kwargs: Any, + *, + 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]]: """Returns a list of matches and an applicable completer @@ -711,7 +746,7 @@ def get_completer( line is a string of the current line kwargs (all optional): locals_ is a dictionary of the environment - argspec is an inspect.FuncProps instance for the current function where + argspec is an inspection.FuncProps instance for the current function where the cursor is current_block is the possibly multiline not-yet-evaluated block of code which the current line is part of @@ -721,7 +756,15 @@ def get_completer( for completer in completers: try: - matches = completer.matches(cursor_offset, line, **kwargs) + matches = completer.matches( + cursor_offset, + line, + locals_=locals_, + funcprops=argspec, + history=history, + current_block=current_block, + complete_magic_methods=complete_magic_methods, + ) except Exception as e: # Instead of crashing the UI, log exceptions from autocompleters. logger = logging.getLogger(__name__) diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index bfc3b8308..88e990542 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -324,7 +324,7 @@ def test_magic_methods_complete_after_double_underscores(self): com = autocomplete.MagicMethodCompletion() block = "class Something(object)\n def __" self.assertSetEqual( - com.matches(10, " def __", current_block=block), + com.matches(10, " def __", current_block=block, complete_magic_methods=True), set(autocomplete.MAGIC_METHODS), ) @@ -419,10 +419,14 @@ def func(apple, apricot, banana, carrot): pass argspec = inspection.ArgSpec(*inspect.getfullargspec(func)) - argspec = inspection.FuncProps("func", argspec, False) + funcspec = inspection.FuncProps("func", argspec, False) com = autocomplete.ParameterNameCompletion() self.assertSetEqual( - com.matches(1, "a", argspec=argspec), {"apple=", "apricot="} + com.matches(1, "a", funcprops=funcspec), {"apple=", "apricot="} + ) + self.assertSetEqual( + com.matches(2, "ba", funcprops=funcspec), {"banana="} + ) + self.assertSetEqual( + com.matches(3, "car", funcprops=funcspec), {"carrot="} ) - self.assertSetEqual(com.matches(2, "ba", argspec=argspec), {"banana="}) - self.assertSetEqual(com.matches(3, "car", argspec=argspec), {"carrot="}) From 15338a2515c9e33ea24852b66bb56e84a2dca154 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 22:06:48 +0200 Subject: [PATCH 416/555] Apply black --- bpython/autocomplete.py | 6 +++++- bpython/test/test_autocomplete.py | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index f8e97d73a..36e175433 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -517,7 +517,11 @@ def matches( complete_magic_methods: Optional[bool] = None, **kwargs: Any, ) -> Optional[Set]: - if current_block is None or complete_magic_methods is None or not complete_magic_methods: + if ( + current_block is None + or complete_magic_methods is None + or not complete_magic_methods + ): return None r = self.locate(cursor_offset, line) diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 88e990542..c95328fbb 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -324,7 +324,12 @@ def test_magic_methods_complete_after_double_underscores(self): com = autocomplete.MagicMethodCompletion() block = "class Something(object)\n def __" self.assertSetEqual( - com.matches(10, " def __", current_block=block, complete_magic_methods=True), + com.matches( + 10, + " def __", + current_block=block, + complete_magic_methods=True, + ), set(autocomplete.MAGIC_METHODS), ) From 12e46590e71e4039faaf7ed467b14972179d25dd Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 28 May 2022 17:44:21 +0200 Subject: [PATCH 417/555] Use super --- bpdb/debugger.py | 4 ++-- bpython/cli.py | 3 +-- bpython/urwid.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/bpdb/debugger.py b/bpdb/debugger.py index b98e9612a..3e5bbc91b 100644 --- a/bpdb/debugger.py +++ b/bpdb/debugger.py @@ -28,14 +28,14 @@ class BPdb(pdb.Pdb): """PDB with BPython support.""" def __init__(self, *args, **kwargs): - pdb.Pdb.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) self.prompt = "(BPdb) " self.intro = 'Use "B" to enter bpython, Ctrl-d to exit it.' def postloop(self): # We only want to show the intro message once. self.intro = None - pdb.Pdb.postloop(self) + super().postloop() # cmd.Cmd commands diff --git a/bpython/cli.py b/bpython/cli.py index ba85729f0..886dc2c85 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -1128,8 +1128,7 @@ def push(self, s: str, insert_into_history: bool = True) -> bool: # curses.raw(True) prevents C-c from causing a SIGINT curses.raw(False) try: - x: bool = repl.Repl.push(self, s, insert_into_history) - return x + return super().push(s, insert_into_history) except SystemExit as e: # Avoid a traceback on e.g. quit() self.do_exit = True diff --git a/bpython/urwid.py b/bpython/urwid.py index cc828ff06..66054097e 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -938,7 +938,7 @@ def push(self, s, insert_into_history=True): signal.signal(signal.SIGINT, signal.default_int_handler) # Pretty blindly adapted from bpython.cli try: - return repl.Repl.push(self, s, insert_into_history) + return super().push(s, insert_into_history) except SystemExit as e: self.exit_value = e.args raise urwid.ExitMainLoop() From df03931ad5dd516ed7ab0498d5c07024af204e1c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 22:23:12 +0200 Subject: [PATCH 418/555] Fix mypy and black regressions --- bpython/autocomplete.py | 6 +----- bpython/repl.py | 4 ++-- bpython/test/test_inspection.py | 1 + 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 36e175433..3dbf80bd9 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -39,14 +39,13 @@ from enum import Enum from typing import ( Any, - cast, Dict, Iterator, List, Optional, + Sequence, Set, Tuple, - Sequence, ) from . import inspection from . import line as lineparts @@ -391,9 +390,6 @@ def matches( locals_: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> Optional[Set]: - if locals_ is None: - return None - r = self.locate(cursor_offset, line) if r is None: return None diff --git a/bpython/repl.py b/bpython/repl.py index e41536fd0..6e593b2b5 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -588,7 +588,7 @@ def current_string(self, concatenate=False): def get_object(self, name): attributes = name.split(".") - obj = eval(attributes.pop(0), self.interp.locals) + obj = eval(attributes.pop(0), cast(Dict[str, Any], self.interp.locals)) while attributes: with inspection.AttrCleaner(obj): obj = getattr(obj, attributes.pop(0)) @@ -783,7 +783,7 @@ def complete(self, tab: bool = False) -> Optional[bool]: self.completers, cursor_offset=self.cursor_offset, line=self.current_line, - locals_=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, diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index ba9ffca7a..b7e2d9b5c 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -179,6 +179,7 @@ def fun_annotations(number: int, lst: List[int] = []) -> List[int]: self.assertEqual(props.argspec.args, ["number", "lst"]) self.assertEqual(props.argspec.defaults[0], []) + class A: a = "a" From 4a55b9e4ffc45e4f3c33fe30ff5ce28f4d65bc9b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 28 Aug 2022 23:34:35 +0200 Subject: [PATCH 419/555] Bump curtsies to 0.4.0 --- README.rst | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 5e50e65a9..dd307d33b 100644 --- a/README.rst +++ b/README.rst @@ -91,7 +91,7 @@ your config file as **~/.config/bpython/config** (i.e. Dependencies ============ * Pygments -* curtsies >= 0.3.5 +* curtsies >= 0.4.0 * greenlet * pyxdg * requests diff --git a/requirements.txt b/requirements.txt index 7f56dc0fd..ba8b126d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Pygments backports.cached-property; python_version < "3.8" -curtsies >=0.3.5 +curtsies >=0.4.0 cwcwidth greenlet pyxdg diff --git a/setup.cfg b/setup.cfg index f5c1bc849..748ce965e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ packages = bpdb install_requires = backports.cached-property; python_version < "3.8" - curtsies >=0.3.5 + curtsies >=0.4.0 cwcwidth greenlet pygments From ddd4321d0a9b27b9a2a3efc6a23d150849f383ac Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 08:10:08 +0200 Subject: [PATCH 420/555] Hide implementation details --- bpython/inspection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index ed201d9a7..3d6096058 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -167,7 +167,7 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: return {item[0]: "".join(item[2:]) for item in stack if len(item) >= 3} -def fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: +def _fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: """Functions taking default arguments that are references to other objects whose str() is too big will cause breakage, so we swap out the object itself with the name it was referenced with in the source by parsing the @@ -195,7 +195,7 @@ def fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: return argspec -getpydocspec_re = LazyReCompile( +_getpydocspec_re = LazyReCompile( r"([a-zA-Z_][a-zA-Z0-9_]*?)\((.*?)\)", re.DOTALL ) @@ -206,7 +206,7 @@ def _getpydocspec(f: Callable) -> Optional[ArgSpec]: except NameError: return None - s = getpydocspec_re.search(argspec) + s = _getpydocspec_re.search(argspec) if s is None: return None @@ -265,7 +265,7 @@ def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: return None try: argspec = _get_argspec_from_signature(f) - fprops = FuncProps(func, fixlongargs(f, argspec), is_bound_method) + fprops = FuncProps(func, _fixlongargs(f, argspec), is_bound_method) except (TypeError, KeyError, ValueError): argspec_pydoc = _getpydocspec(f) if argspec_pydoc is None: From d48a3d2401c0f557a0d1d34539a542a206d1d480 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 08:14:18 +0200 Subject: [PATCH 421/555] Use getattr_safe instead of AttrCleaner This attribute lookup is covered by getattr_safe. --- bpython/repl.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bpython/repl.py b/bpython/repl.py index 6e593b2b5..8aea4e172 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -586,12 +586,11 @@ def current_string(self, concatenate=False): return "" return "".join(string) - def get_object(self, name): + def get_object(self, name: str) -> Any: attributes = name.split(".") obj = eval(attributes.pop(0), cast(Dict[str, Any], self.interp.locals)) while attributes: - with inspection.AttrCleaner(obj): - obj = getattr(obj, attributes.pop(0)) + obj = inspection.getattr_safe(obj, attributes.pop(0)) return obj @classmethod From e59e924c57c72c732180b134ba938307cf45f756 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 09:10:30 +0200 Subject: [PATCH 422/555] Add another failing test for #966 --- bpython/test/test_inspection.py | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index b7e2d9b5c..0dfaab896 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -2,6 +2,7 @@ import os import sys import unittest +from collections.abc import Sequence from typing import List from bpython import inspection @@ -179,6 +180,44 @@ def fun_annotations(number: int, lst: List[int] = []) -> List[int]: self.assertEqual(props.argspec.args, ["number", "lst"]) self.assertEqual(props.argspec.defaults[0], []) + @unittest.expectedFailure + def test_issue_966_class_method(self): + class Issue966(Sequence): + @classmethod + def cmethod(cls, number: int, lst: List[int] = []): + """ + Return a list of numbers + + Example: + ======== + C.cmethod(1337, [1, 2]) # => [1, 2, 1337] + """ + return lst + [number] + + @classmethod + def bmethod(cls, number, lst): + """ + Return a list of numbers + + Example: + ======== + C.cmethod(1337, [1, 2]) # => [1, 2, 1337] + """ + return lst + [number] + + props = inspection.getfuncprops( + "bmethod", inspection.getattr_safe(Issue966, "bmethod") + ) + self.assertEqual(props.func, "bmethod") + self.assertEqual(props.argspec.args, ["number", "lst"]) + + props = inspection.getfuncprops( + "cmethod", inspection.getattr_safe(Issue966, "cmethod") + ) + self.assertEqual(props.func, "cmethod") + self.assertEqual(props.argspec.args, ["number", "lst"]) + self.assertEqual(props.argspec.defaults[0], []) + class A: a = "a" From fb923fde21b45fa96c6cc75a24034faac74837e4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 18:17:36 +0200 Subject: [PATCH 423/555] Hide implementation details --- bpython/inspection.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 3d6096058..a11298e30 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -59,14 +59,12 @@ class AttrCleaner: on attribute lookup.""" def __init__(self, obj: Any) -> None: - self.obj = obj + self._obj = obj def __enter__(self) -> None: """Try to make an object not exhibit side-effects on attribute lookup.""" - type_ = type(self.obj) - __getattribute__ = None - __getattr__ = None + type_ = type(self._obj) # Dark magic: # If __getattribute__ doesn't exist on the class and __getattr__ does # then __getattr__ will be called when doing @@ -89,7 +87,7 @@ def __enter__(self) -> None: except TypeError: # XXX: This happens for e.g. built-in types __getattribute__ = None - self.attribs = (__getattribute__, __getattr__) + self._attribs = (__getattribute__, __getattr__) # /Dark magic def __exit__( @@ -99,8 +97,8 @@ def __exit__( exc_tb: Optional[TracebackType], ) -> Literal[False]: """Restore an object's magic methods.""" - type_ = type(self.obj) - __getattribute__, __getattr__ = self.attribs + type_ = type(self._obj) + __getattribute__, __getattr__ = self._attribs # Dark magic: if __getattribute__ is not None: setattr(type_, "__getattribute__", __getattribute__) @@ -329,13 +327,13 @@ def _get_argspec_from_signature(f: Callable) -> ArgSpec: ) -get_encoding_line_re = LazyReCompile(r"^.*coding[:=]\s*([-\w.]+).*$") +_get_encoding_line_re = LazyReCompile(r"^.*coding[:=]\s*([-\w.]+).*$") def get_encoding(obj) -> str: """Try to obtain encoding information of the source of an object.""" for line in inspect.findsource(obj)[0][:2]: - m = get_encoding_line_re.search(line) + m = _get_encoding_line_re.search(line) if m: return m.group(1) return "utf8" @@ -344,9 +342,9 @@ def get_encoding(obj) -> str: def get_encoding_file(fname: str) -> str: """Try to obtain encoding information from a Python source file.""" with open(fname, encoding="ascii", errors="ignore") as f: - for unused in range(2): + for _ in range(2): line = f.readline() - match = get_encoding_line_re.search(line) + match = _get_encoding_line_re.search(line) if match: return match.group(1) return "utf8" From 3ad203dc1ec9e27e2e2408dd8c297bcb3c35f52a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 18:17:47 +0200 Subject: [PATCH 424/555] Implement ContextManager for AttrCleaner Also extend some documentation. --- bpython/autocomplete.py | 5 +++-- bpython/inspection.py | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 3dbf80bd9..782b8b87a 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -458,8 +458,9 @@ def attr_lookup(self, obj: Any, expr: str, attr: str) -> List: return matches def list_attributes(self, obj: Any) -> List[str]: - # TODO: re-implement dir using getattr_static to avoid using - # AttrCleaner here? + # TODO: re-implement dir without AttrCleaner here + # + # Note: accessing `obj.__dir__` via `getattr_static` is not side-effect free. with inspection.AttrCleaner(obj): return dir(obj) diff --git a/bpython/inspection.py b/bpython/inspection.py index a11298e30..4268f4fbf 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -26,7 +26,7 @@ import pydoc import re from dataclasses import dataclass -from typing import Any, Callable, Optional, Type, Dict, List +from typing import Any, Callable, Optional, Type, Dict, List, ContextManager from types import MemberDescriptorType, TracebackType from ._typing_compat import Literal @@ -54,9 +54,11 @@ class FuncProps: is_bound_method: bool -class AttrCleaner: +class AttrCleaner(ContextManager[None]): """A context manager that tries to make an object not exhibit side-effects - on attribute lookup.""" + on attribute lookup. + + Unless explicitely required, prefer `getattr_safe`.""" def __init__(self, obj: Any) -> None: self._obj = obj @@ -351,7 +353,7 @@ def get_encoding_file(fname: str) -> str: def getattr_safe(obj: Any, name: str) -> Any: - """side effect free getattr (calls getattr_static).""" + """Side effect free getattr (calls getattr_static).""" result = inspect.getattr_static(obj, name) # Slots are a MemberDescriptorType if isinstance(result, MemberDescriptorType): From 1332d18b53fbe99f2808febd035dbbb4647c02c9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 18:20:44 +0200 Subject: [PATCH 425/555] Test inspection with static methods This is another variant of the class from #966. --- bpython/test/test_inspection.py | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 0dfaab896..3cb87d396 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -218,6 +218,43 @@ def bmethod(cls, number, lst): self.assertEqual(props.argspec.args, ["number", "lst"]) self.assertEqual(props.argspec.defaults[0], []) + def test_issue_966_static_method(self): + class Issue966(Sequence): + @staticmethod + def cmethod(number: int, lst: List[int] = []): + """ + Return a list of numbers + + Example: + ======== + C.cmethod(1337, [1, 2]) # => [1, 2, 1337] + """ + return lst + [number] + + @staticmethod + def bmethod(number, lst): + """ + Return a list of numbers + + Example: + ======== + C.cmethod(1337, [1, 2]) # => [1, 2, 1337] + """ + return lst + [number] + + props = inspection.getfuncprops( + "bmethod", inspection.getattr_safe(Issue966, "bmethod") + ) + self.assertEqual(props.func, "bmethod") + self.assertEqual(props.argspec.args, ["number", "lst"]) + + props = inspection.getfuncprops( + "cmethod", inspection.getattr_safe(Issue966, "cmethod") + ) + self.assertEqual(props.func, "cmethod") + self.assertEqual(props.argspec.args, ["number", "lst"]) + self.assertEqual(props.argspec.defaults[0], []) + class A: a = "a" From 1bc255b87dddf78cfef204b68497ea71ba0b10f8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 18:24:02 +0200 Subject: [PATCH 426/555] Directly access class methods in introspection (fixes #966) inspect.signature is unable to process classmethod instances. So we directly access the member via its __get__. --- bpython/inspection.py | 3 +++ bpython/test/test_inspection.py | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 4268f4fbf..6914e5a6e 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -358,6 +358,9 @@ def getattr_safe(obj: Any, name: str) -> Any: # Slots are a MemberDescriptorType if isinstance(result, MemberDescriptorType): result = getattr(obj, name) + # classmethods are safe to access (see #966) + if isinstance(result, classmethod): + result = result.__get__(obj, obj) return result diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 3cb87d396..43915f3e2 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -180,7 +180,6 @@ def fun_annotations(number: int, lst: List[int] = []) -> List[int]: self.assertEqual(props.argspec.args, ["number", "lst"]) self.assertEqual(props.argspec.defaults[0], []) - @unittest.expectedFailure def test_issue_966_class_method(self): class Issue966(Sequence): @classmethod From 8db689848c68796ff1d3b7cc5e82e597a6cfb2ad Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 18:39:23 +0200 Subject: [PATCH 427/555] Update changelog --- CHANGELOG.rst | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b3700a45f..0c12b5fd3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,13 +6,29 @@ Changelog General information: +* More and more type annotations have been added to the bpython code base. +* Some work has been performed to stop relying on blessings. + New features: -* Auto-closing brackets option added. To enable, add `brackets_completion = True` in the bpython config (press F3 to create) + +* #905: Auto-closing brackets option added. To enable, add `brackets_completion = True` in the bpython config Thanks to samuelgregorovic Fixes: -* Support for Python 3.6 has been dropped. +* Improve handling of SyntaxErrors +* #948: Fix crash on Ctrl-Z +* #952: Fix tests for Python 3.10.1 and newer +* #955: Handle optional `readline` parameters in `stdin` emulation + Thanks to thevibingcat +* #959: Fix handling of `__name__` +* #966: Fix function signature completion for `classmethod`s + +Changes to dependencies: + +* curtsies 0.4 or newer is now required + +Support for Python 3.6 has been dropped. 0.22.1 ------ From 265dd4fbb9a3996bbe5d1572ac57ff3145868587 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 18:45:45 +0200 Subject: [PATCH 428/555] Fix a typo --- bpython/inspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 6914e5a6e..b45d49f9d 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -58,7 +58,7 @@ class AttrCleaner(ContextManager[None]): """A context manager that tries to make an object not exhibit side-effects on attribute lookup. - Unless explicitely required, prefer `getattr_safe`.""" + Unless explicitly required, prefer `getattr_safe`.""" def __init__(self, obj: Any) -> None: self._obj = obj From c079356b426a7c6650ef45d6c2bb4cf63d6348d3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 20:04:45 +0200 Subject: [PATCH 429/555] Also directly access staticmethods --- bpython/inspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index b45d49f9d..3efacd5b9 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -359,7 +359,7 @@ def getattr_safe(obj: Any, name: str) -> Any: if isinstance(result, MemberDescriptorType): result = getattr(obj, name) # classmethods are safe to access (see #966) - if isinstance(result, classmethod): + if isinstance(result, (classmethod, staticmethod)): result = result.__get__(obj, obj) return result From 424345e0db2dd5024dce3def1079901f0655cccc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 20:04:55 +0200 Subject: [PATCH 430/555] Improve parenthesis checks --- bpython/inspection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 3efacd5b9..6b9074c0c 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -138,9 +138,9 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: continue if token is Token.Punctuation: - if value in ("(", "{", "["): + if value in "({[": parendepth += 1 - elif value in (")", "}", "]"): + elif value in ")}]": parendepth -= 1 elif value == ":" and parendepth == -1: # End of signature reached From 167fc26d4beae59d02235259e8cdec631a633970 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 21:53:39 +0200 Subject: [PATCH 431/555] Hide implementation details --- bpython/autocomplete.py | 15 +++++++-------- bpython/test/test_autocomplete.py | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 782b8b87a..ddb47239e 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -176,11 +176,11 @@ def from_string(cls, value: str) -> Optional["AutocompleteModes"]: KEYWORDS = frozenset(keyword.kwlist) -def after_last_dot(name: str) -> str: +def _after_last_dot(name: str) -> str: return name.rstrip(".").rsplit(".")[-1] -def few_enough_underscores(current: str, match: str) -> bool: +def _few_enough_underscores(current: str, match: str) -> bool: """Returns whether match should be shown based on current if current is _, True if match starts with 0 or 1 underscore @@ -340,7 +340,7 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return lineparts.current_word(cursor_offset, line) def format(self, word: str) -> str: - return after_last_dot(word) + return _after_last_dot(word) def _safe_glob(pathname: str) -> Iterator[str]: @@ -409,14 +409,14 @@ def matches( return { m for m in matches - if few_enough_underscores(r.word.split(".")[-1], m.split(".")[-1]) + if _few_enough_underscores(r.word.split(".")[-1], m.split(".")[-1]) } def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return lineparts.current_dotted_attribute(cursor_offset, line) def format(self, word: str) -> str: - return after_last_dot(word) + return _after_last_dot(word) def attr_matches(self, text: str, namespace: Dict[str, Any]) -> List: """Taken from rlcompleter.py and bent to my will.""" @@ -434,8 +434,7 @@ def attr_matches(self, text: str, namespace: Dict[str, Any]) -> List: obj = safe_eval(expr, namespace) except EvaluationError: return [] - matches = self.attr_lookup(obj, expr, attr) - return matches + return self.attr_lookup(obj, expr, attr) def attr_lookup(self, obj: Any, expr: str, attr: str) -> List: """Second half of attr_matches.""" @@ -631,7 +630,7 @@ def matches( # strips leading dot matches = (m[1:] for m in self.attr_lookup(obj, "", attr.word)) - return {m for m in matches if few_enough_underscores(attr.word, m)} + return {m for m in matches if _few_enough_underscores(attr.word, m)} try: diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index c95328fbb..0000b0b60 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -35,7 +35,7 @@ def test_filename(self): self.assertEqual(last_part_of_filename("ab.c/e.f.g/"), "e.f.g/") def test_attribute(self): - self.assertEqual(autocomplete.after_last_dot("abc.edf"), "edf") + self.assertEqual(autocomplete._after_last_dot("abc.edf"), "edf") def completer(matches): From 0c40b67910c68a7d6bb73bb64c4b5e7235956b3b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 21:53:54 +0200 Subject: [PATCH 432/555] Refactor --- bpython/autocomplete.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index ddb47239e..45ae8ffd8 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -715,17 +715,15 @@ def matches( ) -> Optional[Set]: if current_block is None or history is None: return None - - if "\n" in current_block: - assert cursor_offset <= len(line), "{!r} {!r}".format( - cursor_offset, - line, - ) - results = super().matches(cursor_offset, line, history=history) - return results - else: + if "\n" not in current_block: return None + assert cursor_offset <= len(line), "{!r} {!r}".format( + cursor_offset, + line, + ) + return super().matches(cursor_offset, line, history=history) + def get_completer( completers: Sequence[BaseCompletionType], From cd20fd7ddc4f392f6655b49795cb3a39ba9383cc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 21:54:25 +0200 Subject: [PATCH 433/555] Remove unused import --- bpython/lazyre.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 8f1e70995..0ca5b9ffa 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -21,7 +21,7 @@ # THE SOFTWARE. import re -from typing import Optional, Iterator, Pattern, Match, Optional +from typing import Optional, Pattern, Match, Optional try: from functools import cached_property From beb1994d0c46453a65a5989df2cadcc5fb9c5165 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 21:53:48 +0200 Subject: [PATCH 434/555] Avoid allocation of a list The list is later turned into a set. --- bpython/autocomplete.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 45ae8ffd8..22a514b69 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -418,25 +418,27 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: def format(self, word: str) -> str: return _after_last_dot(word) - def attr_matches(self, text: str, namespace: Dict[str, Any]) -> List: + def attr_matches( + self, text: str, namespace: Dict[str, Any] + ) -> Iterator[str]: """Taken from rlcompleter.py and bent to my will.""" m = self.attr_matches_re.match(text) if not m: - return [] + return (_ for _ in ()) expr, attr = m.group(1, 3) if expr.isdigit(): # Special case: float literal, using attrs here will result in # a SyntaxError - return [] + return (_ for _ in ()) try: obj = safe_eval(expr, namespace) except EvaluationError: - return [] + return (_ for _ in ()) return self.attr_lookup(obj, expr, attr) - def attr_lookup(self, obj: Any, expr: str, attr: str) -> List: + def attr_lookup(self, obj: Any, expr: str, attr: str) -> Iterator[str]: """Second half of attr_matches.""" words = self.list_attributes(obj) if inspection.hasattr_safe(obj, "__class__"): @@ -449,12 +451,12 @@ def attr_lookup(self, obj: Any, expr: str, attr: str) -> List: except ValueError: pass - matches = [] n = len(attr) - for word in words: - if self.method_match(word, n, attr) and word != "__builtins__": - matches.append(f"{expr}.{word}") - return matches + return ( + f"{expr}.{word}" + for word in words + if self.method_match(word, n, attr) and word != "__builtins__" + ) def list_attributes(self, obj: Any) -> List[str]: # TODO: re-implement dir without AttrCleaner here From 65887e3c91e670523a6b6e8ac59854b4b55cd8f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 22:25:44 +0200 Subject: [PATCH 435/555] Fix return type annotations --- bpython/autocomplete.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 22a514b69..575277e7e 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -308,7 +308,7 @@ def format(self, word: str) -> str: def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set]: + ) -> Optional[Set[str]]: return_value = None all_matches = set() for completer in self._completers: @@ -333,7 +333,7 @@ def __init__( def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set]: + ) -> Optional[Set[str]]: return self.module_gatherer.complete(cursor_offset, line) def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: @@ -353,7 +353,7 @@ def __init__(self, mode: AutocompleteModes = AutocompleteModes.SIMPLE): def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set]: + ) -> Optional[Set[str]]: cs = lineparts.current_string(cursor_offset, line) if cs is None: return None @@ -389,7 +389,7 @@ def matches( *, locals_: Optional[Dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: r = self.locate(cursor_offset, line) if r is None: return None @@ -474,7 +474,7 @@ def matches( *, locals_: Optional[Dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: if locals_ is None: return None @@ -514,7 +514,7 @@ def matches( current_block: Optional[str] = None, complete_magic_methods: Optional[bool] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: if ( current_block is None or complete_magic_methods is None @@ -541,7 +541,7 @@ def matches( *, locals_: Optional[Dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> 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. @@ -582,7 +582,7 @@ def matches( *, funcprops: Optional[inspection.FuncProps] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: if funcprops is None: return None @@ -618,7 +618,7 @@ def matches( *, locals_: Optional[Dict[str, Any]] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: if locals_ is None: locals_ = __main__.__dict__ @@ -642,7 +642,7 @@ def matches( class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Optional[Set]: + ) -> Optional[Set[str]]: return None def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: @@ -660,7 +660,7 @@ def matches( *, history: Optional[List[str]] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: if history is None: return None if not lineparts.current_word(cursor_offset, line): @@ -714,7 +714,7 @@ def matches( current_block: Optional[str] = None, history: Optional[List[str]] = None, **kwargs: Any, - ) -> Optional[Set]: + ) -> Optional[Set[str]]: if current_block is None or history is None: return None if "\n" not in current_block: From d533ad2ca29587419b3763584c0b4261a46b580a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 22:39:34 +0200 Subject: [PATCH 436/555] Fix some type annotations --- bpython/curtsies.py | 10 +++++----- bpython/simpleeval.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index f7b2ef472..6d289aaa6 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -72,10 +72,10 @@ def __init__( self._request_refresh_callback: Callable[ [], None ] = self.input_generator.event_trigger(events.RefreshRequestEvent) - self._schedule_refresh_callback: Callable[ - [float], None - ] = self.input_generator.scheduled_event_trigger( - events.ScheduledRefreshRequestEvent + self._schedule_refresh_callback = ( + self.input_generator.scheduled_event_trigger( + events.ScheduledRefreshRequestEvent + ) ) self._request_reload_callback = ( self.input_generator.threadsafe_event_trigger(events.ReloadEvent) @@ -252,7 +252,7 @@ def curtsies_arguments(parser: argparse._ArgumentGroup) -> None: def _combined_events( - event_provider: "SupportsEventGeneration", paste_threshold: int + event_provider: SupportsEventGeneration, paste_threshold: int ) -> Generator[Union[str, curtsies.events.Event, None], Optional[float], None]: """Combines consecutive keypress events into paste events.""" timeout = yield "nonsense_event" # so send can be used immediately diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 193a69899..251e5e7f4 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -217,7 +217,7 @@ def find_attribute_with_name(node, name): def evaluate_current_expression( cursor_offset: int, line: str, namespace: Optional[Dict[str, Any]] = None -): +) -> Any: """ Return evaluated expression to the right of the dot of current attribute. From 575796983a6bd77391363b2f045af298188ee009 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 22:39:43 +0200 Subject: [PATCH 437/555] Remove unnecessary check --- bpython/simpleeval.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index 251e5e7f4..c5bba43db 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -227,9 +227,6 @@ def evaluate_current_expression( # Find the biggest valid ast. # Once our attribute access is found, return its .value subtree - if namespace is None: - namespace = {} - # in case attribute is blank, e.g. foo.| -> foo.xxx| temp_line = line[:cursor_offset] + "xxx" + line[cursor_offset:] temp_cursor = cursor_offset + 3 From cc9cbbf63bca474b2bc1da9f75f8c2127e1b8a76 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 29 Aug 2022 22:54:55 +0200 Subject: [PATCH 438/555] Update copyright years --- doc/sphinx/source/conf.py | 121 ++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 57 deletions(-) diff --git a/doc/sphinx/source/conf.py b/doc/sphinx/source/conf.py index 2c5263d90..2ef900498 100644 --- a/doc/sphinx/source/conf.py +++ b/doc/sphinx/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # bpython documentation build configuration file, created by # sphinx-quickstart on Mon Jun 8 11:58:16 2009. @@ -16,7 +15,7 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.append(os.path.abspath('.')) +# sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------------- @@ -25,20 +24,20 @@ extensions = [] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8' +# source_encoding = 'utf-8' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'bpython' -copyright = u'2008-2021 Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al.' +project = "bpython" +copyright = "2008-2022 Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al." # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -46,172 +45,180 @@ # # The short X.Y version. -version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), - '../../../bpython/_version.py') +version_file = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "../../../bpython/_version.py" +) with open(version_file) as vf: - version = vf.read().strip().split('=')[-1].replace('\'', '') + version = vf.read().strip().split("=")[-1].replace("'", "") # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. -unused_docs = ['configuration-options'] +unused_docs = ["configuration-options"] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'nature' +html_theme = "nature" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = 'logo.png' +html_logo = "logo.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -html_last_updated_fmt = '%b %d, %Y' +html_last_updated_fmt = "%b %d, %Y" # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_use_modindex = True +# html_use_modindex = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -html_use_opensearch = '' +html_use_opensearch = "" # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '' +# html_file_suffix = '' # Output file base name for HTML help builder. -htmlhelp_basename = 'bpythondoc' +htmlhelp_basename = "bpythondoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). -#latex_documents = [ +# latex_documents = [ # ('index', 'bpython.tex', u'bpython Documentation', # u'Robert Farrell', 'manual'), -#] +# ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +# latex_use_modindex = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('man-bpython', 'bpython', - u'a fancy {curtsies, curses, urwid} interface to the Python interactive interpreter', - [], 1), - ('man-bpython-config', 'bpython-config', - u'user configuration file for bpython', - [], 5) + ( + "man-bpython", + "bpython", + "a fancy {curtsies, curses, urwid} interface to the Python interactive interpreter", + [], + 1, + ), + ( + "man-bpython-config", + "bpython-config", + "user configuration file for bpython", + [], + 5, + ), ] # If true, show URL addresses after external links. -#man_show_urls = False - +# man_show_urls = False From 5cc2c3f8434d8427c5b574bf2e53d6840afe12cc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 30 Aug 2022 00:01:37 +0200 Subject: [PATCH 439/555] Avoid string formatting if not producing any log output --- bpython/args.py | 29 +++++++++++------------------ bpython/autocomplete.py | 7 +++---- bpython/curtsiesfrontend/repl.py | 2 +- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 1212fe3f6..ec7d3b299 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -204,24 +204,17 @@ def callback(group): bpython_logger.addHandler(logging.NullHandler()) curtsies_logger.addHandler(logging.NullHandler()) - logger.info(f"Starting bpython {__version__}") - logger.info(f"Python {sys.executable}: {sys.version_info}") - logger.info(f"curtsies: {curtsies.__version__}") - logger.info(f"cwcwidth: {cwcwidth.__version__}") - logger.info(f"greenlet: {greenlet.__version__}") - logger.info(f"pygments: {pygments.__version__}") # type: ignore - logger.info(f"requests: {requests.__version__}") - logger.info( - "environment:\n{}".format( - "\n".join( - f"{key}: {value}" - for key, value in sorted(os.environ.items()) - if key.startswith("LC") - or key.startswith("LANG") - or key == "TERM" - ) - ) - ) + logger.info("Starting bpython %s", __version__) + logger.info("Python %s: %s", sys.executable, sys.version_info) + logger.info("curtsies: %s", curtsies.__version__) + logger.info("cwcwidth: %s", cwcwidth.__version__) + logger.info("greenlet: %s", greenlet.__version__) + logger.info("pygments: %s", pygments.__version__) # type: ignore + logger.info("requests: %s", requests.__version__) + logger.info("environment:") + for key, value in sorted(os.environ.items()): + if key.startswith("LC") or key.startswith("LANG") or key == "TERM": + logger.info("%s: %s", key, value) return Config(options.config), options, options.args diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 575277e7e..38ffb99d2 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -55,6 +55,8 @@ from .importcompletion import ModuleGatherer +logger = logging.getLogger(__name__) + # Autocomplete modes class AutocompleteModes(Enum): NONE = "none" @@ -767,11 +769,8 @@ def get_completer( ) except Exception as e: # Instead of crashing the UI, log exceptions from autocompleters. - logger = logging.getLogger(__name__) logger.debug( - "Completer {} failed with unhandled exception: {}".format( - completer, e - ) + "Completer %r failed with unhandled exception: %s", completer, e ) continue if matches is not None: diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index ef7081617..b950be68c 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1005,7 +1005,7 @@ def only_whitespace_left_of_cursor(): """returns true if all characters before cursor are whitespace""" return not self.current_line[: self.cursor_offset].strip() - logger.debug("self.matches_iter.matches:%r", self.matches_iter.matches) + logger.debug("self.matches_iter.matches: %r", self.matches_iter.matches) if only_whitespace_left_of_cursor(): front_ws = len(self.current_line[: self.cursor_offset]) - len( self.current_line[: self.cursor_offset].lstrip() From e5e4aef17529c1ef033652a0812b3d1bc69f964f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 30 Aug 2022 22:03:59 +0200 Subject: [PATCH 440/555] Start development of 0.24 --- CHANGELOG.rst | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0c12b5fd3..b9a4c8b55 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,22 @@ Changelog ========= +0.24 +---- + +General information: + + +New features: + + +Fixes: + + +Changes to dependencies: + + + 0.23 ---- @@ -22,7 +38,7 @@ Fixes: * #955: Handle optional `readline` parameters in `stdin` emulation Thanks to thevibingcat * #959: Fix handling of `__name__` -* #966: Fix function signature completion for `classmethod`s +* #966: Fix function signature completion for `classmethod` Changes to dependencies: From e710dfe6aaf8fa252e6a68ae138500fcab897828 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 31 Aug 2022 12:51:11 +0200 Subject: [PATCH 441/555] Inject subcommands into build_py --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 12d4eeec5..7d1a67703 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup -from distutils.command.build import build +from distutils.command.build_py import build_py try: from babel.messages import frontend as babel @@ -122,7 +122,7 @@ def git_describe_to_python_version(version): vf.write(f'__version__ = "{version}"\n') -cmdclass = {"build": build} +cmdclass = {"build_py": build_py} from bpython import package_dir, __author__ @@ -130,7 +130,7 @@ def git_describe_to_python_version(version): # localization options if using_translations: - build.sub_commands.insert(0, ("compile_catalog", None)) + build_py.sub_commands.insert(0, ("compile_catalog", None)) cmdclass["compile_catalog"] = babel.compile_catalog cmdclass["extract_messages"] = babel.extract_messages @@ -138,7 +138,7 @@ def git_describe_to_python_version(version): cmdclass["init_catalog"] = babel.init_catalog if using_sphinx: - build.sub_commands.insert(0, ("build_sphinx_man", None)) + build_py.sub_commands.insert(0, ("build_sphinx_man", None)) cmdclass["build_sphinx_man"] = BuildDoc if platform.system() in ("FreeBSD", "OpenBSD"): From 0bce729bcc4c84b256502fd9de0bba16117187c5 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 31 Aug 2022 22:01:06 +0200 Subject: [PATCH 442/555] Revert "Inject subcommands into build_py" This reverts commit e710dfe6aaf8fa252e6a68ae138500fcab897828. --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 7d1a67703..12d4eeec5 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup -from distutils.command.build_py import build_py +from distutils.command.build import build try: from babel.messages import frontend as babel @@ -122,7 +122,7 @@ def git_describe_to_python_version(version): vf.write(f'__version__ = "{version}"\n') -cmdclass = {"build_py": build_py} +cmdclass = {"build": build} from bpython import package_dir, __author__ @@ -130,7 +130,7 @@ def git_describe_to_python_version(version): # localization options if using_translations: - build_py.sub_commands.insert(0, ("compile_catalog", None)) + build.sub_commands.insert(0, ("compile_catalog", None)) cmdclass["compile_catalog"] = babel.compile_catalog cmdclass["extract_messages"] = babel.extract_messages @@ -138,7 +138,7 @@ def git_describe_to_python_version(version): cmdclass["init_catalog"] = babel.init_catalog if using_sphinx: - build_py.sub_commands.insert(0, ("build_sphinx_man", None)) + build.sub_commands.insert(0, ("build_sphinx_man", None)) cmdclass["build_sphinx_man"] = BuildDoc if platform.system() in ("FreeBSD", "OpenBSD"): From e355191bcb090626e352c7bce8d1744d381e5d86 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 31 Aug 2022 22:14:03 +0200 Subject: [PATCH 443/555] Add config for readthedocs.io --- .readthedocs.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 000000000..ced748ce7 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,8 @@ +version: 2 +build: + tools: + python: "3.10" + +python: + install: + method: pip From 5e46aaf78e0d68c5efc20b190191f22d882bb91a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 31 Aug 2022 22:15:43 +0200 Subject: [PATCH 444/555] Fix readthedocs config --- .readthedocs.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index ced748ce7..01c356be4 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,4 +5,5 @@ build: python: install: - method: pip + - method: pip + path: . From 6badea563dd27cc00a20edf0b1f6bfeaf9f904c8 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 31 Aug 2022 22:18:39 +0200 Subject: [PATCH 445/555] Fix readthedocs config --- .readthedocs.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 01c356be4..942c1da0b 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,7 +1,4 @@ version: 2 -build: - tools: - python: "3.10" python: install: From a03e3457bef05b89b8caa5720263aac72f21c9f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 14 Sep 2022 17:49:52 +0200 Subject: [PATCH 446/555] Refactor --- bpython/importcompletion.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 969361447..3496a24e5 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -240,9 +240,9 @@ def find_all_modules( self.modules.add(module) yield - def find_coroutine(self) -> Optional[bool]: + def find_coroutine(self) -> bool: if self.fully_loaded: - return None + return False try: next(self.find_iterator) From f1a5cfb195987ff8b647b3066b134dbef60fc0e9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 14 Sep 2022 17:49:59 +0200 Subject: [PATCH 447/555] Fix typo --- bpython/inspection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 6b9074c0c..2fd8259b7 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -154,7 +154,7 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: ): stack.append(substack) substack = [] - # If type annotation didn't end before, ti does now. + # If type annotation didn't end before, it does now. annotation = False continue elif token is Token.Operator and value == "=" and parendepth == 0: From 89784501732975afb9768acb9562e4e113999bd9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 14 Sep 2022 19:30:31 +0200 Subject: [PATCH 448/555] Refactor --- bpython/patch_linecache.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index 82b38dd74..d91392d24 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -1,21 +1,24 @@ import linecache -from typing import Any, List, Tuple +from typing import Any, List, Tuple, Optional class BPythonLinecache(dict): """Replaces the cache dict in the standard-library linecache module, to also remember (in an unerasable way) bpython console input.""" - def __init__(self, *args, **kwargs) -> None: + def __init__( + self, + bpython_history: Optional[ + List[Tuple[int, None, List[str], str]] + ] = None, + *args, + **kwargs, + ) -> None: super().__init__(*args, **kwargs) - self.bpython_history: List[Tuple[int, None, List[str], str]] = [] + self.bpython_history = bpython_history or [] def is_bpython_filename(self, fname: Any) -> bool: - if isinstance(fname, str): - return fname.startswith(" Tuple[int, None, List[str], str]: """Given a filename provided by remember_bpython_input, @@ -58,14 +61,13 @@ def _bpython_clear_linecache() -> None: if isinstance(linecache.cache, BPythonLinecache): bpython_history = linecache.cache.bpython_history else: - bpython_history = [] - linecache.cache = BPythonLinecache() - linecache.cache.bpython_history = bpython_history + bpython_history = None + linecache.cache = BPythonLinecache(bpython_history) -# Monkey-patch the linecache module so that we're able +# Monkey-patch the linecache module so that we are able # to hold our command history there and have it persist -linecache.cache = BPythonLinecache(linecache.cache) # type: ignore +linecache.cache = BPythonLinecache(None, linecache.cache) # type: ignore linecache.clearcache = _bpython_clear_linecache From 7e64b0f41233761a800f9a453996198361afb47b Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 20:34:12 +0200 Subject: [PATCH 449/555] Use Optional --- bpython/importcompletion.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 3496a24e5..3e95500eb 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -155,9 +155,7 @@ def complete(self, cursor_offset: int, line: str) -> Optional[Set[str]]: else: return None - def find_modules( - self, path: Path - ) -> Generator[Union[str, None], None, None]: + def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: """Find all modules (and packages) for a given directory.""" if not path.is_dir(): # Perhaps a zip file From ba026ec19e252f2494439dc4b367f0345d09d0a2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 22:32:05 +0200 Subject: [PATCH 450/555] Store device id and inodes in a dataclass --- bpython/importcompletion.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 3e95500eb..44062248b 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -25,8 +25,9 @@ import importlib.machinery import sys import warnings +from dataclasses import dataclass from pathlib import Path -from typing import Optional, Set, Generator, Tuple, Sequence, Iterable, Union +from typing import Optional, Set, Generator, Sequence, Iterable, Union from .line import ( current_word, @@ -47,6 +48,16 @@ ), ) +_LOADED_INODE_DATACLASS_ARGS = {"frozen": True} +if sys.version_info[2:] >= (3, 10): + _LOADED_INODE_DATACLASS_ARGS["slots"] = True + + +@dataclass(**_LOADED_INODE_DATACLASS_ARGS) +class _LoadedInode: + dev: int + inode: int + class ModuleGatherer: def __init__( @@ -60,7 +71,7 @@ def __init__( # Cached list of all known modules self.modules: Set[str] = set() # Set of (st_dev, st_ino) to compare against so that paths are not repeated - self.paths: Set[Tuple[int, int]] = set() + self.paths: Set[_LoadedInode] = set() # Patterns to skip self.skiplist: Sequence[str] = ( skiplist if skiplist is not None else tuple() @@ -216,8 +227,9 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: stat = path_real.stat() except OSError: continue - if (stat.st_dev, stat.st_ino) not in self.paths: - self.paths.add((stat.st_dev, stat.st_ino)) + loaded_inode = _LoadedInode(stat.st_dev, stat.st_ino) + if loaded_inode not in self.paths: + self.paths.add(loaded_inode) for subname in self.find_modules(path_real): if subname is None: yield None # take a break to avoid unresponsiveness From 2868ab10e4afaf36e5d7cdd1e72c856004edece6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 22:44:12 +0200 Subject: [PATCH 451/555] Fix typo --- bpython/importcompletion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 44062248b..13a77ab0c 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -49,7 +49,7 @@ ) _LOADED_INODE_DATACLASS_ARGS = {"frozen": True} -if sys.version_info[2:] >= (3, 10): +if sys.version_info[:2] >= (3, 10): _LOADED_INODE_DATACLASS_ARGS["slots"] = True From 900a273ea27a72b6202a087c6d5893daa54f261f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 23:08:39 +0200 Subject: [PATCH 452/555] Refactor --- bpython/importcompletion.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 13a77ab0c..398298566 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -206,23 +206,22 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: # Workaround for issue #166 continue try: - is_package = False + package_pathname = None with warnings.catch_warnings(): warnings.simplefilter("ignore", ImportWarning) spec = finder.find_spec(name) if spec is None: continue if spec.submodule_search_locations is not None: - pathname = spec.submodule_search_locations[0] - is_package = True + package_pathname = spec.submodule_search_locations[0] except (ImportError, OSError, SyntaxError): continue except UnicodeEncodeError: # Happens with Python 3 when there is a filename in some invalid encoding continue else: - if is_package: - path_real = Path(pathname).resolve() + if package_pathname is not None: + path_real = Path(package_pathname).resolve() try: stat = path_real.stat() except OSError: From 3944fa7c8a3cc5549dfb2680bb621b001d995586 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 23:09:00 +0200 Subject: [PATCH 453/555] Shortcircuit some paths --- bpython/importcompletion.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 398298566..d099c2a9b 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -186,7 +186,10 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: finder = importlib.machinery.FileFinder(str(path), *LOADERS) # type: ignore for p in children: - if any(fnmatch.fnmatch(p.name, entry) for entry in self.skiplist): + if p.name.startswith(".") or p.name == "__pycache__": + # Impossible to import from names starting with . and we can skip __pycache__ + continue + elif any(fnmatch.fnmatch(p.name, entry) for entry in self.skiplist): # Path is on skiplist continue elif not any(p.name.endswith(suffix) for suffix in SUFFIXES): From 074a015fde32475916faf5454258a3e71dae0c01 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 23:09:05 +0200 Subject: [PATCH 454/555] Refactor --- bpython/importcompletion.py | 45 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index d099c2a9b..c1e073f8e 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -83,7 +83,7 @@ def __init__( paths = sys.path self.find_iterator = self.find_all_modules( - (Path(p).resolve() if p else Path.cwd() for p in paths) + Path(p).resolve() if p else Path.cwd() for p in paths ) def module_matches(self, cw: str, prefix: str = "") -> Set[str]: @@ -120,7 +120,7 @@ def attr_matches( matches = { name for name in dir(module) if name.startswith(name_after_dot) } - module_part, _, _ = cw.rpartition(".") + module_part = cw.rpartition(".")[0] if module_part: matches = {f"{module_part}.{m}" for m in matches} @@ -208,8 +208,9 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: if name == "badsyntax_pep3120": # Workaround for issue #166 continue + + package_pathname = None try: - package_pathname = None with warnings.catch_warnings(): warnings.simplefilter("ignore", ImportWarning) spec = finder.find_spec(name) @@ -217,27 +218,25 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: continue if spec.submodule_search_locations is not None: package_pathname = spec.submodule_search_locations[0] - except (ImportError, OSError, SyntaxError): - continue - except UnicodeEncodeError: - # Happens with Python 3 when there is a filename in some invalid encoding + except (ImportError, OSError, SyntaxError, UnicodeEncodeError): + # UnicodeEncodeError happens with Python 3 when there is a filename in some invalid encoding continue - else: - if package_pathname is not None: - path_real = Path(package_pathname).resolve() - try: - stat = path_real.stat() - except OSError: - continue - loaded_inode = _LoadedInode(stat.st_dev, stat.st_ino) - if loaded_inode not in self.paths: - self.paths.add(loaded_inode) - for subname in self.find_modules(path_real): - if subname is None: - yield None # take a break to avoid unresponsiveness - elif subname != "__init__": - yield f"{name}.{subname}" - yield name + + if package_pathname is not None: + path_real = Path(package_pathname).resolve() + try: + stat = path_real.stat() + except OSError: + continue + loaded_inode = _LoadedInode(stat.st_dev, stat.st_ino) + if loaded_inode not in self.paths: + self.paths.add(loaded_inode) + for subname in self.find_modules(path_real): + if subname is None: + yield None # take a break to avoid unresponsiveness + elif subname != "__init__": + yield f"{name}.{subname}" + yield name yield None # take a break to avoid unresponsiveness def find_all_modules( From bb8712c6185b43fd60e351b6702fb23b8d6757d0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 23:25:32 +0200 Subject: [PATCH 455/555] Remove unused import --- bpython/history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/history.py b/bpython/history.py index a870d4b29..13dbb5b7f 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 Iterable, Optional, List, TextIO, Union +from typing import Iterable, Optional, List, TextIO from .translations import _ from .filelock import FileLock From 748ce36c3ab8f93ce80623e223ff8cde4a61b94a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 23:29:05 +0200 Subject: [PATCH 456/555] Fix exception handling --- bpython/pager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bpython/pager.py b/bpython/pager.py index 673e902bc..e145e0ed8 100644 --- a/bpython/pager.py +++ b/bpython/pager.py @@ -63,7 +63,6 @@ def page(data: str, use_internal: bool = False) -> None: # pager command not found, fall back to internal pager page_internal(data) return - except OSError as e: if e.errno != errno.EPIPE: raise while True: From f44b8c22eb8929267cc3599f0f84776453ce5e16 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 25 Sep 2022 23:43:58 +0200 Subject: [PATCH 457/555] Refactor --- bpython/autocomplete.py | 2 +- bpython/inspection.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 38ffb99d2..69e6e2a2d 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -598,7 +598,7 @@ def matches( if isinstance(name, str) and name.startswith(r.word) } matches.update( - name + "=" + f"{name}=" for name in funcprops.argspec.kwonly if name.startswith(r.word) ) diff --git a/bpython/inspection.py b/bpython/inspection.py index 2fd8259b7..78bbc5782 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -142,16 +142,16 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: parendepth += 1 elif value in ")}]": parendepth -= 1 - elif value == ":" and parendepth == -1: - # End of signature reached - break - elif value == ":" and parendepth == 0: - # Start of type annotation - annotation = True - - if (value == "," and parendepth == 0) or ( - value == ")" and parendepth == -1 - ): + elif value == ":": + if parendepth == -1: + # End of signature reached + break + elif parendepth == 0: + # Start of type annotation + annotation = True + + if (value, parendepth) in ((",", 0), (")", -1)): + # End of current argument stack.append(substack) substack = [] # If type annotation didn't end before, it does now. From 9281dccb83c3cecfebfd557518f1b87640808d8d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 26 Sep 2022 22:21:38 +0200 Subject: [PATCH 458/555] Refactor --- bpython/autocomplete.py | 52 +++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 69e6e2a2d..b97fd86f7 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -555,11 +555,10 @@ def matches( if r is None: return None - matches = set() n = len(r.word) - for word in KEYWORDS: - if self.method_match(word, n, r.word): - matches.add(word) + matches = { + word for word in KEYWORDS if self.method_match(word, n, r.word) + } for nspace in (builtins.__dict__, locals_): for word, val in nspace.items(): # if identifier isn't ascii, don't complete (syntax error) @@ -652,7 +651,7 @@ def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: else: - class JediCompletion(BaseCompletionType): + class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] _orig_start: Optional[int] def matches( @@ -660,19 +659,28 @@ def matches( cursor_offset: int, line: str, *, + current_block: Optional[str] = None, history: Optional[List[str]] = None, **kwargs: Any, ) -> Optional[Set[str]]: - if history is None: - return None - if not lineparts.current_word(cursor_offset, line): + if ( + current_block is None + or history is None + or "\n" not in current_block + or not lineparts.current_word(cursor_offset, line) + ): return None + assert cursor_offset <= len(line), "{!r} {!r}".format( + cursor_offset, + line, + ) + combined_history = "\n".join(itertools.chain(history, (line,))) try: script = jedi.Script(combined_history, path="fake.py") completions = script.complete( - len(combined_history.splitlines()), cursor_offset + combined_history.count("\n") + 1, cursor_offset ) except (jedi.NotFoundError, IndexError, KeyError): # IndexError for #483 @@ -688,8 +696,6 @@ def matches( return None assert isinstance(self._orig_start, int) - first_letter = line[self._orig_start : self._orig_start + 1] - matches = [c.name for c in completions] if any( not m.lower().startswith(matches[0][0].lower()) for m in matches @@ -699,35 +705,15 @@ def matches( return None else: # case-sensitive matches only + first_letter = line[self._orig_start] return {m for m in matches if m.startswith(first_letter)} def locate(self, cursor_offset: int, line: str) -> LinePart: - assert isinstance(self._orig_start, int) + assert self._orig_start is not None start = self._orig_start end = cursor_offset return LinePart(start, end, line[start:end]) - class MultilineJediCompletion(JediCompletion): # type: ignore [no-redef] - def matches( - self, - cursor_offset: int, - line: str, - *, - current_block: Optional[str] = None, - history: Optional[List[str]] = None, - **kwargs: Any, - ) -> Optional[Set[str]]: - if current_block is None or history is None: - return None - if "\n" not in current_block: - return None - - assert cursor_offset <= len(line), "{!r} {!r}".format( - cursor_offset, - line, - ) - return super().matches(cursor_offset, line, history=history) - def get_completer( completers: Sequence[BaseCompletionType], From 1c6f1813dc8c6c8ac24daa8916a0a8d5a3aab9a1 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 19 Oct 2022 19:43:54 +0200 Subject: [PATCH 459/555] Fix deprecation warning --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 748ce965e..35806eca1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ name = bpython long_description = file: README.rst license = MIT -license_file = LICENSE +license_files = LICENSE url = https://www.bpython-interpreter.org/ project_urls = GitHub = https://github.com/bpython/bpython From d47c2387f5d173e0175a95c782c79414c7ddd5ac Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 19 Oct 2022 20:32:02 +0200 Subject: [PATCH 460/555] GA: checkspell: ignore dedented --- .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 839681b92..57aad1bda 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -25,7 +25,7 @@ jobs: - uses: codespell-project/actions-codespell@master with: skip: '*.po' - ignore_words_list: ba,te,deltion + ignore_words_list: ba,te,deltion,dedent,dedented mypy: runs-on: ubuntu-latest From 4759bcb766c633b269d75a4036c8885d45940d29 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 25 Oct 2022 15:01:41 +0200 Subject: [PATCH 461/555] GA: test with Python 3.11 --- .github/workflows/build.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 89edab7c8..c16bcd70e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -14,7 +14,13 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.7, 3.8, 3.9, "3.10", "pypy-3.7"] + python-version: + - "3.7" + - "3.8" + - "3.9", + - "3.10", + - "3.11", + - "pypy-3.7" steps: - uses: actions/checkout@v2 with: From 3e495488af4a66a8be267a588919ac1722a9b9b7 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Tue, 25 Oct 2022 15:03:24 +0200 Subject: [PATCH 462/555] GA: fix syntax --- .github/workflows/build.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c16bcd70e..e9a3e1626 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -17,9 +17,9 @@ jobs: python-version: - "3.7" - "3.8" - - "3.9", - - "3.10", - - "3.11", + - "3.9" + - "3.10" + - "3.11" - "pypy-3.7" steps: - uses: actions/checkout@v2 From f2ad2a1e771d48e6ab1280d9c75e42381bf10993 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 15:34:09 +0200 Subject: [PATCH 463/555] GA: update actions with dependabot --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..8c139c7be --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From 82389987bfb1a4fa46e18b70882e5e909a9e82a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 13:34:35 +0000 Subject: [PATCH 464/555] Bump actions/setup-python from 2 to 4 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .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 e9a3e1626..faeb9c8f3 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -26,7 +26,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 57aad1bda..b24cd6e56 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 - name: Install dependencies run: | python -m pip install --upgrade pip From ec2f03354e115d61d273384c3ba7bbcff366c1bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 13:36:47 +0000 Subject: [PATCH 465/555] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [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/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout 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 faeb9c8f3..052969b8e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,7 +22,7 @@ jobs: - "3.11" - "pypy-3.7" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index b24cd6e56..f644f5434 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@v2 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 - name: Install dependencies @@ -21,7 +21,7 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: codespell-project/actions-codespell@master with: skip: '*.po' @@ -30,7 +30,7 @@ jobs: mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 - name: Install dependencies From 9e32826795944c8b97092077164bae91045ff0a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Oct 2022 13:34:46 +0000 Subject: [PATCH 466/555] Bump codecov/codecov-action from 1 to 3 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 1 to 3. - [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/v1...v3) --- 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 052969b8e..9cdcc1e92 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -46,7 +46,7 @@ jobs: run: | pytest --cov=bpython --cov-report=xml -v - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 env: PYTHON_VERSION: ${{ matrix.python-version }} with: From 742085633868c72e5a42ce452f3deea57de442fc Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 16:19:04 +0200 Subject: [PATCH 467/555] Handle changed output in Python 3.11 --- bpython/test/test_interpreter.py | 42 ++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 65b60a925..f53ec2529 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -112,19 +112,35 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - 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" - ) + if (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" + ) self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) self.assertEqual(plain("").join(a), expected) From b04ad8835c6eb777393536efa4e89045a3883852 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 17:48:57 +0200 Subject: [PATCH 468/555] mypy: ignore import errors from twisted --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 35806eca1..7955ee390 100644 --- a/setup.cfg +++ b/setup.cfg @@ -77,3 +77,6 @@ ignore_missing_imports = True [mypy-urwid] ignore_missing_imports = True + +[mypy-twisted.*] +ignore_missing_imports = True From 3b8927852e84fd6581c83ef974920856d5f3fb47 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 19:21:58 +0200 Subject: [PATCH 469/555] Refactor Also, as we have many small _Repr instances, make the value a slot. --- bpython/inspection.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 78bbc5782..8ae5c20a0 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -36,6 +36,22 @@ from .lazyre import LazyReCompile +class _Repr: + """ + Helper for `ArgSpec`: Returns the given value in `__repr__()`. + """ + + __slots__ = ("value",) + + def __init__(self, value: str) -> None: + self.value = value + + def __repr__(self) -> str: + return self.value + + __str__ = __repr__ + + @dataclass class ArgSpec: args: List[str] @@ -110,20 +126,6 @@ def __exit__( return False -class _Repr: - """ - Helper for `fixlongargs()`: Returns the given value in `__repr__()`. - """ - - def __init__(self, value: str) -> None: - self.value = value - - def __repr__(self) -> str: - return self.value - - __str__ = __repr__ - - def parsekeywordpairs(signature: str) -> Dict[str, str]: preamble = True stack = [] @@ -293,12 +295,15 @@ def _get_argspec_from_signature(f: Callable) -> ArgSpec: """ args = [] - varargs = varkwargs = None + varargs = None + varkwargs = None defaults = [] kwonly = [] kwonly_defaults = {} annotations = {} + # We use signature here instead of getfullargspec as the latter also returns + # self and cls (for class methods). signature = inspect.signature(f) for parameter in signature.parameters.values(): if parameter.annotation is not parameter.empty: From 917382530f0de38e775312cacfab5ebe498502a3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 19:56:27 +0200 Subject: [PATCH 470/555] Fix inspection of built-in functions with >= 3.11 The built-in functions no longer have their signature in the docstring, but now inspect.signature can produce results. But as we have no source for built-in functions, we cannot replace the default values. Hence, we handle built-in functions in an extra step. This commit also changes the handling of default values slightly. They are now always put into a _Repr. --- bpython/inspection.py | 60 ++++++++++++++++++++++----------- bpython/test/test_inspection.py | 36 ++++++++++---------- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 8ae5c20a0..e1e4ed8db 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -57,9 +57,9 @@ class ArgSpec: args: List[str] varargs: Optional[str] varkwargs: Optional[str] - defaults: Optional[List[Any]] + defaults: Optional[List[_Repr]] kwonly: List[str] - kwonly_defaults: Optional[Dict[str, Any]] + kwonly_defaults: Optional[Dict[str, _Repr]] annotations: Optional[Dict[str, Any]] @@ -169,31 +169,51 @@ def parsekeywordpairs(signature: str) -> Dict[str, str]: return {item[0]: "".join(item[2:]) for item in stack if len(item) >= 3} -def _fixlongargs(f: Callable, argspec: ArgSpec) -> ArgSpec: +def _fix_default_values(f: Callable, argspec: ArgSpec) -> ArgSpec: """Functions taking default arguments that are references to other objects - whose str() is too big will cause breakage, so we swap out the object - itself with the name it was referenced with in the source by parsing the - source itself !""" - if argspec.defaults is None: + will cause breakage, so we swap out the object itself with the name it was + referenced with in the source by parsing the source itself!""" + + if argspec.defaults is None and argspec.kwonly_defaults is None: # No keyword args, no need to do anything return argspec - values = list(argspec.defaults) - if not values: - return argspec - keys = argspec.args[-len(values) :] + try: - src = inspect.getsourcelines(f) + src, _ = inspect.getsourcelines(f) except (OSError, IndexError): # IndexError is raised in inspect.findsource(), can happen in # some situations. See issue #94. return argspec - kwparsed = parsekeywordpairs("".join(src[0])) + except TypeError: + # No source code is available (for Python >= 3.11) + # + # If the function is a builtin, we replace the default values. + # Otherwise, let's bail out. + if not inspect.isbuiltin(f): + raise + + if argspec.defaults is not None: + argspec.defaults = [_Repr(str(value)) for value in argspec.defaults] + if argspec.kwonly_defaults is not None: + argspec.kwonly_defaults = { + key: _Repr(str(value)) + for key, value in argspec.kwonly_defaults.items() + } + return argspec - for i, (key, value) in enumerate(zip(keys, values)): - if len(repr(value)) != len(kwparsed[key]): + kwparsed = parsekeywordpairs("".join(src)) + + if argspec.defaults is not None: + values = list(argspec.defaults) + keys = argspec.args[-len(values) :] + for i, key in enumerate(keys): values[i] = _Repr(kwparsed[key]) - argspec.defaults = values + argspec.defaults = values + if argspec.kwonly_defaults is not None: + for key in argspec.kwonly_defaults.keys(): + argspec.kwonly_defaults[key] = _Repr(kwparsed[key]) + return argspec @@ -234,11 +254,11 @@ def _getpydocspec(f: Callable) -> Optional[ArgSpec]: if varargs is not None: kwonly_args.append(arg) if default: - kwonly_defaults[arg] = default + kwonly_defaults[arg] = _Repr(default) else: args.append(arg) if default: - defaults.append(default) + defaults.append(_Repr(default)) return ArgSpec( args, varargs, varkwargs, defaults, kwonly_args, kwonly_defaults, None @@ -267,7 +287,9 @@ def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: return None try: argspec = _get_argspec_from_signature(f) - fprops = FuncProps(func, _fixlongargs(f, argspec), is_bound_method) + fprops = FuncProps( + func, _fix_default_values(f, argspec), is_bound_method + ) except (TypeError, KeyError, ValueError): argspec_pydoc = _getpydocspec(f) if argspec_pydoc is None: diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 43915f3e2..3f04222de 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -11,6 +11,7 @@ from bpython.test.fodder import encoding_utf8 pypy = "PyPy" in sys.version +_is_py311 = sys.version_info[:2] >= (3, 11) try: import numpy @@ -53,23 +54,17 @@ def test_parsekeywordpairs(self): def fails(spam=["-a", "-b"]): pass - default_arg_repr = "['-a', '-b']" - self.assertEqual( - str(["-a", "-b"]), - default_arg_repr, - "This test is broken (repr does not match), fix me.", - ) - argspec = inspection.getfuncprops("fails", fails) + self.assertIsNotNone(argspec) defaults = argspec.argspec.defaults - self.assertEqual(str(defaults[0]), default_arg_repr) + self.assertEqual(str(defaults[0]), '["-a", "-b"]') def test_pasekeywordpairs_string(self): def spam(eggs="foo, bar"): pass defaults = inspection.getfuncprops("spam", spam).argspec.defaults - self.assertEqual(repr(defaults[0]), "'foo, bar'") + self.assertEqual(repr(defaults[0]), '"foo, bar"') def test_parsekeywordpairs_multiple_keywords(self): def spam(eggs=23, foobar="yay"): @@ -77,14 +72,14 @@ def spam(eggs=23, foobar="yay"): defaults = inspection.getfuncprops("spam", spam).argspec.defaults self.assertEqual(repr(defaults[0]), "23") - self.assertEqual(repr(defaults[1]), "'yay'") + self.assertEqual(repr(defaults[1]), '"yay"') def test_pasekeywordpairs_annotation(self): def spam(eggs: str = "foo, bar"): pass defaults = inspection.getfuncprops("spam", spam).argspec.defaults - self.assertEqual(repr(defaults[0]), "'foo, bar'") + self.assertEqual(repr(defaults[0]), '"foo, bar"') def test_get_encoding_ascii(self): self.assertEqual(inspection.get_encoding(encoding_ascii), "ascii") @@ -134,8 +129,15 @@ def test_getfuncprops_print(self): self.assertIn("file", props.argspec.kwonly) self.assertIn("flush", props.argspec.kwonly) self.assertIn("sep", props.argspec.kwonly) - self.assertEqual(props.argspec.kwonly_defaults["file"], "sys.stdout") - self.assertEqual(props.argspec.kwonly_defaults["flush"], "False") + 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["flush"]), "False") @unittest.skipUnless( numpy is not None and numpy.__version__ >= "1.18", @@ -173,12 +175,12 @@ def fun_annotations(number: int, lst: List[int] = []) -> List[int]: props = inspection.getfuncprops("fun", fun) self.assertEqual(props.func, "fun") self.assertEqual(props.argspec.args, ["number", "lst"]) - self.assertEqual(props.argspec.defaults[0], []) + self.assertEqual(repr(props.argspec.defaults[0]), "[]") props = inspection.getfuncprops("fun_annotations", fun_annotations) self.assertEqual(props.func, "fun_annotations") self.assertEqual(props.argspec.args, ["number", "lst"]) - self.assertEqual(props.argspec.defaults[0], []) + self.assertEqual(repr(props.argspec.defaults[0]), "[]") def test_issue_966_class_method(self): class Issue966(Sequence): @@ -215,7 +217,7 @@ def bmethod(cls, number, lst): ) self.assertEqual(props.func, "cmethod") self.assertEqual(props.argspec.args, ["number", "lst"]) - self.assertEqual(props.argspec.defaults[0], []) + self.assertEqual(repr(props.argspec.defaults[0]), "[]") def test_issue_966_static_method(self): class Issue966(Sequence): @@ -252,7 +254,7 @@ def bmethod(number, lst): ) self.assertEqual(props.func, "cmethod") self.assertEqual(props.argspec.args, ["number", "lst"]) - self.assertEqual(props.argspec.defaults[0], []) + self.assertEqual(repr(props.argspec.defaults[0]), "[]") class A: From 6b390db6ffb14a7d57a9d5458a814e41a708d123 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Oct 2022 22:20:38 +0200 Subject: [PATCH 471/555] Unbreak tests after 9173825 --- bpython/inspection.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index e1e4ed8db..db0a397c0 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -185,13 +185,7 @@ def _fix_default_values(f: Callable, argspec: ArgSpec) -> ArgSpec: # some situations. See issue #94. return argspec except TypeError: - # No source code is available (for Python >= 3.11) - # - # If the function is a builtin, we replace the default values. - # Otherwise, let's bail out. - if not inspect.isbuiltin(f): - raise - + # No source code is available, so replace the default values with what we have. if argspec.defaults is not None: argspec.defaults = [_Repr(str(value)) for value in argspec.defaults] if argspec.kwonly_defaults is not None: From e20bf119949c445fbb1215f402d67057343476e3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 29 Oct 2022 17:54:09 +0200 Subject: [PATCH 472/555] Skip test with special unicode chars on broken Python 3.11 versions --- bpython/test/test_curtsies_painting.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 2804643ce..19561efb9 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -13,7 +13,7 @@ ) from curtsies.fmtfuncs import cyan, bold, green, yellow, on_magenta, red from curtsies.window import CursorAwareWindow -from unittest import mock +from unittest import mock, skipIf from bpython.curtsiesfrontend.events import RefreshRequestEvent from bpython import config, inspection @@ -311,6 +311,10 @@ def test_cursor_position_with_padding_char(self): cursor_pos = self.repl.paint()[1] self.assertEqual(cursor_pos, (1, 4)) + @skipIf( + sys.version_info[:2] >= (3, 11) and sys.version_info[:3] < (3, 11, 1), + "https://github.com/python/cpython/issues/98744", + ) def test_display_of_padding_chars(self): self.repl.width = 11 [self.repl.add_normal_character(c) for c in "width"] From abf13c57a2b54ef4c68e5bec16bcb44a11a9513e Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 29 Oct 2022 18:05:02 +0200 Subject: [PATCH 473/555] Remove unused build dependency --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c7ef64f28..4dfbad888 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,6 @@ [build-system] requires = [ "setuptools >= 43", - "wheel", ] [tool.black] From 7f546d932552256ad46057c81f9c8642d7a7b619 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 29 Oct 2022 18:05:11 +0200 Subject: [PATCH 474/555] Set build-backend --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 4dfbad888..6526fb9ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,7 @@ requires = [ "setuptools >= 43", ] +build-backend = "setuptools.build_meta" [tool.black] line-length = 80 From 33c62dac03cddd6d87547cdcbca4762b1f4c5684 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 29 Oct 2022 18:18:02 +0200 Subject: [PATCH 475/555] Update changelog --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b9a4c8b55..1d9026348 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,10 +12,13 @@ New features: Fixes: +* Improve inspection of builtin functions. Changes to dependencies: +Support for Python 3.11 has been added. + 0.23 ---- From 7712e282d067b041ead41f0b514ac3e27584edc6 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Thu, 3 Nov 2022 23:51:22 -0700 Subject: [PATCH 476/555] Fix watchdog auto-reloading --- bpython/curtsies.py | 2 +- bpython/curtsiesfrontend/repl.py | 36 +++++++------------------------- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 6d289aaa6..985f70853 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -106,7 +106,7 @@ def _schedule_refresh(self, when: float) -> None: return self._schedule_refresh_callback(when) def _request_reload(self, files_modified: Sequence[str]) -> None: - return self._request_reload_callback(files_modified) + return self._request_reload_callback(files_modified=files_modified) def interrupting_refresh(self) -> None: return self._interrupting_refresh_callback() diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index b950be68c..cf7d9c37c 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -269,54 +269,39 @@ def __init__(self, watcher, loader): def __getattr__(self, name): if name == "create_module" and hasattr(self.loader, name): return self._create_module - if name == "load_module" and hasattr(self.loader, name): - return self._load_module return getattr(self.loader, name) def _create_module(self, spec): - spec = self.loader.create_module(spec) + module_object = self.loader.create_module(spec) if ( getattr(spec, "origin", None) is not None and spec.origin != "builtin" ): self.watcher.track_module(spec.origin) - return spec - - def _load_module(self, name): - module = self.loader.load_module(name) - if hasattr(module, "__file__"): - self.watcher.track_module(module.__file__) - return module + return module_object class ImportFinder: - """Wrapper for finders in sys.meta_path to replace wrap all loaders with ImportLoader.""" + """Wrapper for finders in sys.meta_path to wrap all loaders with ImportLoader.""" - def __init__(self, finder, watcher): + def __init__(self, watcher, finder): self.watcher = watcher self.finder = finder def __getattr__(self, name): if name == "find_spec" and hasattr(self.finder, name): return self._find_spec - if name == "find_module" and hasattr(self.finder, name): - return self._find_module return getattr(self.finder, name) def _find_spec(self, fullname, path, target=None): # Attempt to find the spec spec = self.finder.find_spec(fullname, path, target) if spec is not None: - if getattr(spec, "__loader__", None) is not None: + if getattr(spec, "loader", None) is not None: # Patch the loader to enable reloading - spec.__loader__ = ImportLoader(self.watcher, spec.__loader__) + spec.loader = ImportLoader(self.watcher, spec.loader) return spec - def _find_module(self, fullname, path=None): - loader = self.finder.find_module(fullname, path) - if loader is not None: - return ImportLoader(self.watcher, loader) - def _process_ps(ps, default_ps: str): """Replace ps1/ps2 with the default if the user specified value contains control characters.""" @@ -607,14 +592,7 @@ def __enter__(self): if self.watcher: meta_path = [] for finder in sys.meta_path: - # All elements get wrapped in ImportFinder instances execepted for instances of - # _SixMetaPathImporter (from six). When importing six, it will check if the importer - # is already part of sys.meta_path and will remove instances. We do not want to - # break this feature (see also #874). - if type(finder).__name__ == "_SixMetaPathImporter": - meta_path.append(finder) - else: - meta_path.append(ImportFinder(finder, self.watcher)) + meta_path.append(ImportFinder(self.watcher, finder)) sys.meta_path = meta_path sitefix.monkeypatch_quit() From 7c312971ce37dbeddd4316ed80cb640e50f04a9d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 6 Nov 2022 19:32:40 +0100 Subject: [PATCH 477/555] Log versions of all required dependencies --- bpython/args.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bpython/args.py b/bpython/args.py index ec7d3b299..0c514c37b 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -41,6 +41,7 @@ import pygments import requests import sys +import xdg from pathlib import Path from . import __version__, __copyright__ @@ -206,10 +207,12 @@ def callback(group): logger.info("Starting bpython %s", __version__) logger.info("Python %s: %s", sys.executable, sys.version_info) + # versions of required dependencies logger.info("curtsies: %s", curtsies.__version__) logger.info("cwcwidth: %s", cwcwidth.__version__) logger.info("greenlet: %s", greenlet.__version__) logger.info("pygments: %s", pygments.__version__) # type: ignore + logger.info("pyxdg: %s", xdg.__version__) # type: ignore logger.info("requests: %s", requests.__version__) logger.info("environment:") for key, value in sorted(os.environ.items()): From 84314021dd26f254a93bc3f5ac56ac584b8accd3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 6 Nov 2022 19:32:50 +0100 Subject: [PATCH 478/555] Log versions of optional dependencies --- bpython/args.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bpython/args.py b/bpython/args.py index 0c514c37b..2eb910d5b 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -214,6 +214,27 @@ def callback(group): logger.info("pygments: %s", pygments.__version__) # type: ignore logger.info("pyxdg: %s", xdg.__version__) # type: ignore logger.info("requests: %s", requests.__version__) + + # versions of optional dependencies + try: + import pyperclip + + logger.info("pyperclip: %s", pyperclip.__version__) # type: ignore + except ImportError: + logger.info("pyperclip: not available") + try: + import jedi + + logger.info("jedi: %s", jedi.__version__) + except ImportError: + logger.info("jedi: not available") + try: + import watchdog + + logger.info("watchdog: available") + except ImportError: + logger.info("watchdog: not available") + logger.info("environment:") for key, value in sorted(os.environ.items()): if key.startswith("LC") or key.startswith("LANG") or key == "TERM": From b387c5a666d3bec79833754b177fd9f26dfe6886 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 6 Nov 2022 19:40:24 +0100 Subject: [PATCH 479/555] Remove no longer used pycheck config skip-checks: true --- .pycheckrc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .pycheckrc diff --git a/.pycheckrc b/.pycheckrc deleted file mode 100644 index e7050fad1..000000000 --- a/.pycheckrc +++ /dev/null @@ -1 +0,0 @@ -blacklist = ['pyparsing', 'code', 'pygments/lexer'] From f797aa1d5af825e7ef266ded83488c587dcc725d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 3 Dec 2022 21:12:35 +0100 Subject: [PATCH 480/555] Fix Python 3.11 version constraints --- 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 f53ec2529..b2fb52758 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -112,7 +112,7 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - if (3, 11) <= sys.version_info[:2]: + if (3, 11, 0) <= sys.version_info[:3] < (3, 11, 1): expected = ( "Traceback (most recent call last):\n File " + green('""') From 352de3085e2f320000d8cd8d49378c4b14d22ec3 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 3 Dec 2022 21:22:12 +0100 Subject: [PATCH 481/555] Fix type annotations --- bpython/cli.py | 10 ++++++---- bpython/translations/__init__.py | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/bpython/cli.py b/bpython/cli.py index 886dc2c85..bb1429a8e 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -1299,7 +1299,7 @@ def show_list( arg_pos: Union[str, int, None], topline: Optional[inspection.FuncProps] = None, formatter: Optional[Callable] = None, - current_item: Union[str, Literal[False]] = None, + current_item: Optional[str] = None, ) -> None: v_items: Collection shared = ShowListState() @@ -1315,7 +1315,7 @@ def show_list( if items and formatter: items = [formatter(x) for x in items] - if current_item: + if current_item is not None: current_item = formatter(current_item) if topline: @@ -1492,8 +1492,10 @@ def tab(self, back: bool = False) -> bool: # 4. swap current word for a match list item elif self.matches_iter.matches: - current_match: Union[str, Literal[False]] = ( - back and self.matches_iter.previous() or next(self.matches_iter) + current_match = ( + self.matches_iter.previous() + if back + else next(self.matches_iter) ) try: f = None diff --git a/bpython/translations/__init__.py b/bpython/translations/__init__.py index 13c498025..0cb4c01f6 100644 --- a/bpython/translations/__init__.py +++ b/bpython/translations/__init__.py @@ -2,7 +2,7 @@ import locale import os.path import sys -from typing import cast, List +from typing import Optional, cast, List from .. import package_dir @@ -17,7 +17,9 @@ def ngettext(singular, plural, n): return translator.ngettext(singular, plural, n) -def init(locale_dir: str = None, languages: List[str] = None) -> None: +def init( + locale_dir: Optional[str] = None, languages: Optional[List[str]] = None +) -> None: try: locale.setlocale(locale.LC_ALL, "") except locale.Error: From ebefde843bce5ec1e3f180c655308d5858672531 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 17 Dec 2022 21:24:18 +0100 Subject: [PATCH 482/555] Revert "Fix Python 3.11 version constraints" This reverts commit f797aa1d5af825e7ef266ded83488c587dcc725d. --- 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 b2fb52758..f53ec2529 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -112,7 +112,7 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - if (3, 11, 0) <= sys.version_info[:3] < (3, 11, 1): + if (3, 11) <= sys.version_info[:2]: expected = ( "Traceback (most recent call last):\n File " + green('""') From 71320bbfa318ec7aebd7bfafe3652715087fe7d2 Mon Sep 17 00:00:00 2001 From: Eric Burgess Date: Fri, 13 Jan 2023 13:06:13 -0600 Subject: [PATCH 483/555] Add more keywords to trigger auto-deindent --- bpython/curtsiesfrontend/repl.py | 4 +++- bpython/repl.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index cf7d9c37c..037cf7567 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1252,7 +1252,9 @@ def predicted_indent(self, line): elif ( line and ":" not in line - and line.strip().startswith(("return", "pass", "raise", "yield")) + and line.strip().startswith( + ("return", "pass", "...", "raise", "yield", "break", "continue") + ) ): indent = max(0, indent - self.config.tab_length) logger.debug("indent we found was %s", indent) diff --git a/bpython/repl.py b/bpython/repl.py index 8aea4e172..c964f0902 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -1255,7 +1255,9 @@ def next_indentation(line, tab_length) -> int: if line.rstrip().endswith(":"): indentation += 1 elif indentation >= 1: - if line.lstrip().startswith(("return", "pass", "raise", "yield")): + if line.lstrip().startswith( + ("return", "pass", "...", "raise", "yield", "break", "continue") + ): indentation -= 1 return indentation From d0da704b96b37ce4611e91d59c09f922e3908246 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 14 Jan 2023 22:10:12 +0100 Subject: [PATCH 484/555] Update changelog for 0.24 --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1d9026348..4f94198d2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,9 +6,12 @@ Changelog General information: +* This release is focused on Python 3.11 support. New features: +* #980: Add more keywords to trigger auto-deindent. + Thanks to Eric Burgess Fixes: @@ -16,6 +19,7 @@ Fixes: Changes to dependencies: +* wheel is no required as part of pyproject.toml's build dependencies Support for Python 3.11 has been added. From b94ccc24cf94bb6e29bd2208f462fcc9b7c49cb2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 14 Jan 2023 22:18:10 +0100 Subject: [PATCH 485/555] Update readthedocs config again --- .readthedocs.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 942c1da0b..a19293daa 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,5 +1,13 @@ version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3" + +sphinx: + configuration: doc/sphinx/source/conf.py + python: install: - method: pip From d6e62b372504c290fa96825535317acf067463f6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 14 Jan 2023 22:27:13 +0100 Subject: [PATCH 486/555] Avoid bpython imports in setup.py --- setup.cfg | 2 ++ setup.py | 6 +----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 7955ee390..07f90115a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,8 @@ name = bpython long_description = file: README.rst license = MIT license_files = LICENSE +author = Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al. +author_email = bpython@googlegroups.com url = https://www.bpython-interpreter.org/ project_urls = GitHub = https://github.com/bpython/bpython diff --git a/setup.py b/setup.py index 12d4eeec5..6790b9d78 100755 --- a/setup.py +++ b/setup.py @@ -124,9 +124,7 @@ def git_describe_to_python_version(version): cmdclass = {"build": build} -from bpython import package_dir, __author__ - -translations_dir = os.path.join(package_dir, "translations") +translations_dir = os.path.join("bpython", "translations") # localization options if using_translations: @@ -179,8 +177,6 @@ def git_describe_to_python_version(version): setup( version=version, - author=__author__, - author_email="robertanthonyfarrell@gmail.com", data_files=data_files, package_data={ "bpython": ["sample-config"], From 406f8713915566a77c7021daf160b8a8cc9ee863 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 14 Jan 2023 22:27:23 +0100 Subject: [PATCH 487/555] Update copyright year --- bpython/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/__init__.py b/bpython/__init__.py index adc00c06b..dff06c0fa 100644 --- a/bpython/__init__.py +++ b/bpython/__init__.py @@ -30,7 +30,7 @@ __author__ = ( "Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al." ) -__copyright__ = f"(C) 2008-2020 {__author__}" +__copyright__ = f"(C) 2008-2023 {__author__}" __license__ = "MIT" __version__ = version package_dir = os.path.abspath(os.path.dirname(__file__)) From b38f6c7a483302651b223e13a0c478224bc6d645 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 17 Dec 2022 21:44:27 +0100 Subject: [PATCH 488/555] Remove unused code --- bpython/test/test_interpreter.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index f53ec2529..4a39cfe14 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -1,37 +1,25 @@ import sys -import re import unittest from curtsies.fmtfuncs import bold, green, magenta, cyan, red, plain -from unittest import mock from bpython.curtsiesfrontend import interpreter pypy = "PyPy" in sys.version -def remove_ansi(s): - return re.sub(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]".encode("ascii"), b"", s) +class Interpreter(interpreter.Interp): + def __init__(self): + super().__init__() + self.a = [] + def write(self, data): + self.a.append(data) -class TestInterpreter(unittest.TestCase): - def interp_errlog(self): - i = interpreter.Interp() - a = [] - i.write = a.append - return i, a - - def err_lineno(self, a): - strings = [x.__unicode__() for x in a] - for line in reversed(strings): - clean_line = remove_ansi(line) - m = re.search(r"line (\d+)[,]", clean_line) - if m: - return int(m.group(1)) - return None +class TestInterpreter(unittest.TestCase): def test_syntaxerror(self): - i, a = self.interp_errlog() + i = Interpreter() i.runsource("1.1.1.1") @@ -96,11 +84,12 @@ def test_syntaxerror(self): + "\n" ) + a = i.a self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) self.assertEqual(plain("").join(a), expected) def test_traceback(self): - i, a = self.interp_errlog() + i = Interpreter() def f(): return 1 / 0 @@ -142,6 +131,7 @@ def gfunc(): + "\n" ) + a = i.a self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) self.assertEqual(plain("").join(a), expected) From e1ca4522583f06b172674c4e11303fda044bf8f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 17:25:59 +0100 Subject: [PATCH 489/555] Start development of 0.25 --- CHANGELOG.rst | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4f94198d2..7ce32c42c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,21 @@ Changelog ========= +0.25 +---- + +General information: + + +New features: + + +Fixes: + + +Changes to dependencies: + + 0.24 ---- @@ -23,7 +38,6 @@ Changes to dependencies: Support for Python 3.11 has been added. - 0.23 ---- From c1f0385c79c0096265994b0d6e0aadf6db8a2709 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 17:34:55 +0100 Subject: [PATCH 490/555] Do not fail if curtsies is not available (fixes #978) Also delay imports of dependencies only used to log the version. --- bpython/args.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 2eb910d5b..b9e68e1d8 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -30,19 +30,13 @@ """ import argparse -from typing import Tuple, List, Optional, NoReturn, Callable import code -import curtsies -import cwcwidth -import greenlet import importlib.util import logging import os -import pygments -import requests import sys -import xdg from pathlib import Path +from typing import Tuple, List, Optional, NoReturn, Callable from . import __version__, __copyright__ from .config import default_config_path, Config @@ -205,10 +199,22 @@ def callback(group): bpython_logger.addHandler(logging.NullHandler()) curtsies_logger.addHandler(logging.NullHandler()) + import cwcwidth + import greenlet + import pygments + import requests + import xdg + logger.info("Starting bpython %s", __version__) logger.info("Python %s: %s", sys.executable, sys.version_info) # versions of required dependencies - logger.info("curtsies: %s", curtsies.__version__) + try: + import curtsies + + logger.info("curtsies: %s", curtsies.__version__) + except ImportError: + # may happen on Windows + logger.info("curtsies: not available") logger.info("cwcwidth: %s", cwcwidth.__version__) logger.info("greenlet: %s", greenlet.__version__) logger.info("pygments: %s", pygments.__version__) # type: ignore From 29d6f87f9688610a1f96cdcc32ce84f3ce435b7f Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 17:52:17 +0100 Subject: [PATCH 491/555] Fix definition of write --- bpython/test/test_interpreter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index 4a39cfe14..a4a32dd09 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -12,9 +12,7 @@ class Interpreter(interpreter.Interp): def __init__(self): super().__init__() self.a = [] - - def write(self, data): - self.a.append(data) + self.write = self.a.append class TestInterpreter(unittest.TestCase): From 46dc081faa21aa957f67761a15d0fb18031de87c Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 18:00:39 +0100 Subject: [PATCH 492/555] Remove support for Python 3.7 (fixes #940) --- .github/workflows/build.yaml | 5 ++-- bpython/_typing_compat.py | 33 --------------------------- bpython/cli.py | 2 +- bpython/curtsies.py | 2 +- bpython/curtsiesfrontend/_internal.py | 3 +-- bpython/curtsiesfrontend/repl.py | 2 +- bpython/filelock.py | 3 +-- bpython/inspection.py | 12 ++++++++-- bpython/paste.py | 3 +-- bpython/repl.py | 14 ++++++------ doc/sphinx/source/contributing.rst | 2 +- doc/sphinx/source/releases.rst | 2 +- pyproject.toml | 2 +- requirements.txt | 1 - setup.cfg | 4 +--- 15 files changed, 29 insertions(+), 61 deletions(-) delete mode 100644 bpython/_typing_compat.py diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 9cdcc1e92..1e2a374a5 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -10,17 +10,16 @@ on: jobs: build: runs-on: ubuntu-latest - continue-on-error: ${{ matrix.python-version == 'pypy-3.7' }} + continue-on-error: ${{ matrix.python-version == 'pypy-3.8' }} strategy: fail-fast: false matrix: python-version: - - "3.7" - "3.8" - "3.9" - "3.10" - "3.11" - - "pypy-3.7" + - "pypy-3.8" steps: - uses: actions/checkout@v3 with: diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py deleted file mode 100644 index 31fb64287..000000000 --- a/bpython/_typing_compat.py +++ /dev/null @@ -1,33 +0,0 @@ -# The MIT License -# -# Copyright (c) 2021 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.8 - from typing import Literal -except ImportError: - from typing_extensions import Literal # type: ignore - -try: - # introduced in Python 3.8 - from typing import Protocol -except ImportError: - from typing_extensions import Protocol # type: ignore diff --git a/bpython/cli.py b/bpython/cli.py index bb1429a8e..0a03d5138 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -69,12 +69,12 @@ Collection, Dict, TYPE_CHECKING, + Literal, ) if TYPE_CHECKING: from _curses import _CursesWindow -from ._typing_compat import Literal import unicodedata from dataclasses import dataclass diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 985f70853..6dc8d1f78 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -28,11 +28,11 @@ Generator, List, Optional, + Protocol, Sequence, Tuple, Union, ) -from ._typing_compat import Protocol logger = logging.getLogger(__name__) diff --git a/bpython/curtsiesfrontend/_internal.py b/bpython/curtsiesfrontend/_internal.py index 79c5e974c..0480c1b06 100644 --- a/bpython/curtsiesfrontend/_internal.py +++ b/bpython/curtsiesfrontend/_internal.py @@ -22,8 +22,7 @@ import pydoc from types import TracebackType -from typing import Optional, Type -from .._typing_compat import Literal +from typing import Optional, Type, Literal from .. import _internal diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 037cf7567..a3b32d443 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -17,13 +17,13 @@ Iterable, Dict, List, + Literal, Optional, Sequence, Tuple, Type, Union, ) -from .._typing_compat import Literal import greenlet from curtsies import ( diff --git a/bpython/filelock.py b/bpython/filelock.py index 429f708b6..11f575b6e 100644 --- a/bpython/filelock.py +++ b/bpython/filelock.py @@ -20,8 +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 -from ._typing_compat import Literal +from typing import Optional, Type, IO, Literal from types import TracebackType has_fcntl = True diff --git a/bpython/inspection.py b/bpython/inspection.py index db0a397c0..fe1e3a0a2 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -26,9 +26,17 @@ import pydoc import re from dataclasses import dataclass -from typing import Any, Callable, Optional, Type, Dict, List, ContextManager +from typing import ( + Any, + Callable, + Optional, + Type, + Dict, + List, + ContextManager, + Literal, +) from types import MemberDescriptorType, TracebackType -from ._typing_compat import Literal from pygments.token import Token from pygments.lexers import Python3Lexer diff --git a/bpython/paste.py b/bpython/paste.py index ceba59386..fd140a0ec 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -22,7 +22,7 @@ import errno import subprocess -from typing import Optional, Tuple +from typing import Optional, Tuple, Protocol from urllib.parse import urljoin, urlparse import requests @@ -30,7 +30,6 @@ from .config import getpreferredencoding from .translations import _ -from ._typing_compat import Protocol class PasteFailed(Exception): diff --git a/bpython/repl.py b/bpython/repl.py index c964f0902..fe72a4db8 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -40,19 +40,19 @@ from pathlib import Path from types import ModuleType, TracebackType from typing import ( + Any, + Callable, + Dict, Iterable, - cast, List, - Tuple, - Any, + Literal, Optional, + TYPE_CHECKING, + Tuple, Type, Union, - Callable, - Dict, - TYPE_CHECKING, + cast, ) -from ._typing_compat import Literal from pygments.lexers import Python3Lexer from pygments.token import Token, _TokenType diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 54fd56c6b..9e0f6bc44 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.7 and newer. The code is compatible with all +bpython supports Python 3.8 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 738c24ff2..fcce5c1cf 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.7 - 3.9 +* Runs under Python 3.8 - 3.11 * Save * Rewind * Pastebin diff --git a/pyproject.toml b/pyproject.toml index 6526fb9ed..b7bd3196a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 80 -target_version = ["py37"] +target_version = ["py38"] include = '\.pyi?$' exclude = ''' /( diff --git a/requirements.txt b/requirements.txt index ba8b126d1..4c750a694 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ Pygments -backports.cached-property; python_version < "3.8" curtsies >=0.4.0 cwcwidth greenlet diff --git a/setup.cfg b/setup.cfg index 07f90115a..081af0bc2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ classifiers = Programming Language :: Python :: 3 [options] -python_requires = >=3.7 +python_requires = >=3.8 packages = bpython bpython.curtsiesfrontend @@ -22,14 +22,12 @@ packages = bpython.translations bpdb install_requires = - backports.cached-property; python_version < "3.8" curtsies >=0.4.0 cwcwidth greenlet pygments pyxdg requests - typing-extensions; python_version < "3.8" [options.extras_require] clipboard = pyperclip From 274f423e4a1b742904a87d5cebe18258ecd095a9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 18:13:26 +0100 Subject: [PATCH 493/555] Remove < 3.8 workaround --- bpython/importcompletion.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index c1e073f8e..9b0edaab6 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -175,17 +175,8 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: # Path is on skiplist return - try: - # https://bugs.python.org/issue34541 - # Once we migrate to Python 3.8, we can change it back to directly iterator over - # path.iterdir(). - children = tuple(path.iterdir()) - except OSError: - # Path is not readable - return - finder = importlib.machinery.FileFinder(str(path), *LOADERS) # type: ignore - for p in children: + for p in path.iterdir(): if p.name.startswith(".") or p.name == "__pycache__": # Impossible to import from names starting with . and we can skip __pycache__ continue From 1fb4041aa641d9b06047489c487cafbeb7d3d7b2 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 19:06:34 +0100 Subject: [PATCH 494/555] Handle OSError again --- bpython/importcompletion.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 9b0edaab6..7833a9328 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -175,8 +175,14 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: # Path is on skiplist return + try: + children = path.iterdir() + except OSError: + # Path is not readable + return + finder = importlib.machinery.FileFinder(str(path), *LOADERS) # type: ignore - for p in path.iterdir(): + for p in children: if p.name.startswith(".") or p.name == "__pycache__": # Impossible to import from names starting with . and we can skip __pycache__ continue From df3750d2a93ce69d9676f6b17a0da858df7ca983 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 15 Jan 2023 19:43:34 +0100 Subject: [PATCH 495/555] Really handle OSError --- bpython/importcompletion.py | 108 ++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/bpython/importcompletion.py b/bpython/importcompletion.py index 7833a9328..9df140c64 100644 --- a/bpython/importcompletion.py +++ b/bpython/importcompletion.py @@ -175,65 +175,67 @@ def find_modules(self, path: Path) -> Generator[Optional[str], None, None]: # Path is on skiplist return - try: - children = path.iterdir() - except OSError: - # Path is not readable - return - finder = importlib.machinery.FileFinder(str(path), *LOADERS) # type: ignore - for p in children: - if p.name.startswith(".") or p.name == "__pycache__": - # Impossible to import from names starting with . and we can skip __pycache__ - continue - elif any(fnmatch.fnmatch(p.name, entry) for entry in self.skiplist): - # Path is on skiplist - continue - elif not any(p.name.endswith(suffix) for suffix in SUFFIXES): - # Possibly a package - if "." in p.name: + try: + for p in path.iterdir(): + if p.name.startswith(".") or p.name == "__pycache__": + # Impossible to import from names starting with . and we can skip __pycache__ continue - elif p.is_dir(): - # Unfortunately, CPython just crashes if there is a directory - # which ends with a python extension, so work around. - continue - name = p.name - for suffix in SUFFIXES: - if name.endswith(suffix): - name = name[: -len(suffix)] - break - if name == "badsyntax_pep3120": - # Workaround for issue #166 - continue - - package_pathname = None - try: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", ImportWarning) - spec = finder.find_spec(name) - if spec is None: + elif any( + fnmatch.fnmatch(p.name, entry) for entry in self.skiplist + ): + # Path is on skiplist + continue + elif not any(p.name.endswith(suffix) for suffix in SUFFIXES): + # Possibly a package + if "." in p.name: continue - if spec.submodule_search_locations is not None: - package_pathname = spec.submodule_search_locations[0] - except (ImportError, OSError, SyntaxError, UnicodeEncodeError): - # UnicodeEncodeError happens with Python 3 when there is a filename in some invalid encoding - continue + elif p.is_dir(): + # Unfortunately, CPython just crashes if there is a directory + # which ends with a python extension, so work around. + continue + name = p.name + for suffix in SUFFIXES: + if name.endswith(suffix): + name = name[: -len(suffix)] + break + if name == "badsyntax_pep3120": + # Workaround for issue #166 + continue - if package_pathname is not None: - path_real = Path(package_pathname).resolve() + package_pathname = None try: - stat = path_real.stat() - except OSError: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", ImportWarning) + spec = finder.find_spec(name) + if spec is None: + continue + if spec.submodule_search_locations is not None: + package_pathname = spec.submodule_search_locations[ + 0 + ] + except (ImportError, OSError, SyntaxError, UnicodeEncodeError): + # UnicodeEncodeError happens with Python 3 when there is a filename in some invalid encoding continue - loaded_inode = _LoadedInode(stat.st_dev, stat.st_ino) - if loaded_inode not in self.paths: - self.paths.add(loaded_inode) - for subname in self.find_modules(path_real): - if subname is None: - yield None # take a break to avoid unresponsiveness - elif subname != "__init__": - yield f"{name}.{subname}" - yield name + + if package_pathname is not None: + path_real = Path(package_pathname).resolve() + try: + stat = path_real.stat() + except OSError: + continue + loaded_inode = _LoadedInode(stat.st_dev, stat.st_ino) + if loaded_inode not in self.paths: + self.paths.add(loaded_inode) + for subname in self.find_modules(path_real): + if subname is None: + yield None # take a break to avoid unresponsiveness + elif subname != "__init__": + yield f"{name}.{subname}" + yield name + except OSError: + # Path is not readable + return yield None # take a break to avoid unresponsiveness def find_all_modules( From b537a508ede7b09eb118f20898bd3c8c0a2fe7f9 Mon Sep 17 00:00:00 2001 From: Ganden Schaffner Date: Sun, 29 Jan 2023 00:00:00 -0800 Subject: [PATCH 496/555] Fix URL typo in package metadata --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 081af0bc2..8c4294d9c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ author_email = bpython@googlegroups.com url = https://www.bpython-interpreter.org/ project_urls = GitHub = https://github.com/bpython/bpython - Documentation = https://doc.bpython-interpreter.org + Documentation = https://docs.bpython-interpreter.org classifiers = Programming Language :: Python :: 3 From 11a72fb11d48f8acd018acd24a538448ff57c2c2 Mon Sep 17 00:00:00 2001 From: Nitant Patel Date: Thu, 2 Mar 2023 00:48:19 -0500 Subject: [PATCH 497/555] Clarify fork instructions in contributing docs `setup.py` expects to be able to find git tags in order to properly set the package version. If a fork does not have any tags, `pip install -e .` will fail due to the default `"unknown"` being an invalid version. --- doc/sphinx/source/contributing.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 9e0f6bc44..32b1ea869 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -30,7 +30,10 @@ environment with # necessary every time you work on bpython $ source bpython-dev/bin/activate -Fork bpython in the GitHub web interface, then clone the repo: +Fork bpython in the GitHub web interface. Be sure to include the tags +in your fork by un-selecting the option to copy only the main branch. + +Then, clone the forked repo: .. code-block:: bash From 7df8e6f89036775ad659983d45deda98a175d2be Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 2 Mar 2023 09:53:04 +0100 Subject: [PATCH 498/555] Apply black --- bpython/autocomplete.py | 2 +- bpython/cli.py | 1 + bpython/config.py | 2 +- bpython/curtsiesfrontend/manual_readline.py | 1 - bpython/curtsiesfrontend/repl.py | 1 - bpython/repl.py | 10 ++++------ bpython/test/__init__.py | 1 - bpython/test/test_repl.py | 4 ++-- bpython/urwid.py | 2 -- 9 files changed, 9 insertions(+), 15 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index b97fd86f7..e0849c6d2 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -57,6 +57,7 @@ logger = logging.getLogger(__name__) + # Autocomplete modes class AutocompleteModes(Enum): NONE = "none" @@ -381,7 +382,6 @@ def format(self, filename: str) -> str: class AttrCompletion(BaseCompletionType): - attr_matches_re = LazyReCompile(r"(\w+(\.\w+)*)\.(\w*)") def matches( diff --git a/bpython/cli.py b/bpython/cli.py index 0a03d5138..fcfd11fa5 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -264,6 +264,7 @@ def readlines(self, size: int = -1) -> List[str]: # the addstr stuff to a higher level. # + # Have to ignore the return type on this one because the colors variable # is Optional[MutableMapping[str, int]] but for the purposes of this # function it can't be None diff --git a/bpython/config.py b/bpython/config.py index 29b906ddb..5123ec226 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -90,7 +90,7 @@ def fill_config_with_default_values( if not config.has_section(section): config.add_section(section) - for (opt, val) in default_values[section].items(): + for opt, val in default_values[section].items(): if not config.has_option(section, opt): config.set(section, opt, str(val)) diff --git a/bpython/curtsiesfrontend/manual_readline.py b/bpython/curtsiesfrontend/manual_readline.py index f95e66c59..206e5278b 100644 --- a/bpython/curtsiesfrontend/manual_readline.py +++ b/bpython/curtsiesfrontend/manual_readline.py @@ -16,7 +16,6 @@ class AbstractEdits: - default_kwargs = { "line": "hello world", "cursor_offset": 5, diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index a3b32d443..e4819e193 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -893,7 +893,6 @@ def insert_char_pair_end(self, e): self.add_normal_character(e) def get_last_word(self): - previous_word = _last_word(self.rl_history.entry) word = _last_word(self.rl_history.back()) line = self.current_line diff --git a/bpython/repl.py b/bpython/repl.py index fe72a4db8..f30cfa31d 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -465,7 +465,6 @@ def cursor_offset(self, value: int) -> None: self._set_cursor_offset(value) if TYPE_CHECKING: - # not actually defined, subclasses must define cpos: int @@ -567,7 +566,7 @@ def current_string(self, concatenate=False): return "" opening = string_tokens.pop()[1] string = list() - for (token, value) in reversed(string_tokens): + for token, value in reversed(string_tokens): if token is Token.Text: continue elif opening is None: @@ -602,7 +601,7 @@ def _funcname_and_argnum( # if keyword is not None, we've encountered a keyword and so we're done counting stack = [_FuncExpr("", "", 0, "")] try: - for (token, value) in Python3Lexer().get_tokens(line): + for token, value in Python3Lexer().get_tokens(line): if token is Token.Punctuation: if value in "([{": stack.append(_FuncExpr("", "", 0, value)) @@ -692,7 +691,6 @@ def get_args(self): # py3 f.__new__.__class__ is not object.__new__.__class__ ): - class_f = f.__new__ if class_f: @@ -1117,7 +1115,7 @@ def tokenize(self, s, newline=False) -> List[Tuple[_TokenType, str]]: 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): + for token, value in split_lines(all_tokens): pos += len(value) if token is Token.Text and value == "\n": line += 1 @@ -1263,7 +1261,7 @@ def next_indentation(line, tab_length) -> int: def split_lines(tokens): - for (token, value) in tokens: + for token, value in tokens: if not value: continue while value: diff --git a/bpython/test/__init__.py b/bpython/test/__init__.py index 7722278cc..4618eca4d 100644 --- a/bpython/test/__init__.py +++ b/bpython/test/__init__.py @@ -13,7 +13,6 @@ def setUpClass(cls): class MagicIterMock(unittest.mock.MagicMock): - __next__ = unittest.mock.Mock(return_value=None) diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 63309364c..b3e8912fd 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -183,7 +183,7 @@ def set_input_line(self, line): self.repl.cursor_offset = len(line) def test_func_name(self): - for (line, expected_name) in [ + for line, expected_name in [ ("spam(", "spam"), # map pydoc has no signature in pypy ("spam(any([]", "any") if pypy else ("spam(map([]", "map"), @@ -194,7 +194,7 @@ def test_func_name(self): self.assertEqual(self.repl.current_func.__name__, expected_name) def test_func_name_method_issue_479(self): - for (line, expected_name) in [ + for line, expected_name in [ ("o.spam(", "spam"), # map pydoc has no signature in pypy ("o.spam(any([]", "any") if pypy else ("o.spam(map([]", "map"), diff --git a/bpython/urwid.py b/bpython/urwid.py index 66054097e..4b41c12aa 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -74,7 +74,6 @@ else: class EvalProtocol(basic.LineOnlyReceiver): - delimiter = "\n" def __init__(self, myrepl): @@ -570,7 +569,6 @@ def file_prompt(self, s: str) -> Optional[str]: class URWIDRepl(repl.Repl): - _time_between_redraws = 0.05 # seconds def __init__(self, event_loop, palette, interpreter, config): From 44c3b702037c1c1a5f7681b7f86c0246fbf0db7e Mon Sep 17 00:00:00 2001 From: supakeen Date: Tue, 9 Nov 2021 16:51:56 +0000 Subject: [PATCH 499/555] Remove deprecated curses (cli) rendering backend. --- CHANGELOG.rst | 4 +- bpython/cli.py | 2094 ----------------- bpython/test/test_repl.py | 152 +- bpython/translations/bpython.pot | 30 +- .../translations/de/LC_MESSAGES/bpython.po | 34 +- .../translations/es_ES/LC_MESSAGES/bpython.po | 31 +- .../translations/fr_FR/LC_MESSAGES/bpython.po | 31 +- .../translations/it_IT/LC_MESSAGES/bpython.po | 31 +- .../translations/nl_NL/LC_MESSAGES/bpython.po | 31 +- doc/sphinx/source/tips.rst | 2 +- doc/sphinx/source/windows.rst | 6 - setup.cfg | 1 - 12 files changed, 16 insertions(+), 2431 deletions(-) delete mode 100644 bpython/cli.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7ce32c42c..d7ecb3ab7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,9 @@ Changelog General information: +* The bpython-cli rendering backend has been removed following deprecation in + version 0.19. + New features: @@ -44,7 +47,6 @@ Support for Python 3.11 has been added. General information: * More and more type annotations have been added to the bpython code base. -* Some work has been performed to stop relying on blessings. New features: diff --git a/bpython/cli.py b/bpython/cli.py deleted file mode 100644 index fcfd11fa5..000000000 --- a/bpython/cli.py +++ /dev/null @@ -1,2094 +0,0 @@ -# The MIT License -# -# Copyright (c) 2008 Bob Farrell -# Copyright (c) bpython authors -# -# 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. -# -# mypy: disallow_untyped_defs=True -# mypy: disallow_untyped_calls=True - -# Modified by Brandon Navra -# Notes for Windows -# Prerequisites -# - Curses -# - pyreadline -# -# Added -# -# - Support for running on windows command prompt -# - input from numpad keys -# -# Issues -# -# - Suspend doesn't work nor does detection of resizing of screen -# - Instead the suspend key exits the program -# - View source doesn't work on windows unless you install the less program (From GnuUtils or Cygwin) - - -import curses -import errno -import functools -import math -import os -import platform -import re -import struct -import sys -import time -from typing import ( - Iterator, - NoReturn, - List, - MutableMapping, - Any, - Callable, - TypeVar, - cast, - IO, - Iterable, - Optional, - Union, - Tuple, - Collection, - Dict, - TYPE_CHECKING, - Literal, -) - -if TYPE_CHECKING: - from _curses import _CursesWindow - -import unicodedata -from dataclasses import dataclass - -if platform.system() != "Windows": - import signal # Windows does not have job control - import termios # Windows uses curses - import fcntl # Windows uses curses - - -# These are used for syntax highlighting -from pygments import format -from pygments.formatters import TerminalFormatter -from pygments.lexers import Python3Lexer -from pygments.token import Token, _TokenType -from .formatter import BPythonFormatter - -# This for config -from .config import getpreferredencoding, Config - -# This for keys -from .keys import cli_key_dispatch as key_dispatch - -# This for i18n -from . import translations -from .translations import _ - -from . import repl, inspection -from . import args as bpargs -from .pager import page -from .args import parse as argsparse - -F = TypeVar("F", bound=Callable[..., Any]) - -# --- module globals --- -stdscr = None -colors: Optional[MutableMapping[str, int]] = None - -DO_RESIZE = False -# --- - - -@dataclass -class ShowListState: - cols: int = 0 - rows: int = 0 - wl: int = 0 - - -def forward_if_not_current(func: F) -> F: - @functools.wraps(func) - def newfunc(self, *args, **kwargs): # type: ignore - dest = self.get_dest() - if self is dest: - return func(self, *args, **kwargs) - else: - return getattr(self.get_dest(), newfunc.__name__)(*args, **kwargs) - - return cast(F, newfunc) - - -class FakeStream: - """Provide a fake file object which calls functions on the interface - provided.""" - - def __init__(self, interface: "CLIRepl", get_dest: IO[str]) -> None: - self.encoding: str = getpreferredencoding() - self.interface = interface - self.get_dest = get_dest - - @forward_if_not_current - def write(self, s: str) -> None: - self.interface.write(s) - - @forward_if_not_current - def writelines(self, l: Iterable[str]) -> None: - for s in l: - self.write(s) - - def isatty(self) -> bool: - # some third party (amongst them mercurial) depend on this - return True - - def flush(self) -> None: - self.interface.flush() - - -class FakeStdin: - """Provide a fake stdin type for things like raw_input() etc.""" - - def __init__(self, interface: "CLIRepl") -> None: - """Take the curses Repl on init and assume it provides a get_key method - which, fortunately, it does.""" - - self.encoding = getpreferredencoding() - self.interface = interface - self.buffer: List[str] = list() - - def __iter__(self) -> Iterator: - return iter(self.readlines()) - - def flush(self) -> None: - """Flush the internal buffer. This is a no-op. Flushing stdin - doesn't make any sense anyway.""" - - def write(self, value: str) -> NoReturn: - # XXX IPython expects sys.stdin.write to exist, there will no doubt be - # others, so here's a hack to keep them happy - raise OSError(errno.EBADF, "sys.stdin is read-only") - - def isatty(self) -> bool: - return True - - def readline(self, size: int = -1) -> str: - """I can't think of any reason why anything other than readline would - be useful in the context of an interactive interpreter so this is the - only one I've done anything with. The others are just there in case - someone does something weird to stop it from blowing up.""" - - if not size: - return "" - elif self.buffer: - buffer = self.buffer.pop(0) - else: - buffer = "" - - curses.raw(True) - try: - while not buffer.endswith(("\n", "\r")): - key = self.interface.get_key() - if key in (curses.erasechar(), "KEY_BACKSPACE"): - y, x = self.interface.scr.getyx() - if buffer: - self.interface.scr.delch(y, x - 1) - buffer = buffer[:-1] - continue - elif key == chr(4) and not buffer: - # C-d - return "" - elif key not in ("\n", "\r") and ( - len(key) > 1 or unicodedata.category(key) == "Cc" - ): - continue - sys.stdout.write(key) - # Include the \n in the buffer - raw_input() seems to deal with trailing - # linebreaks and will break if it gets an empty string. - buffer += key - finally: - curses.raw(False) - - if size > 0: - rest = buffer[size:] - if rest: - self.buffer.append(rest) - buffer = buffer[:size] - - return buffer - - def read(self, size: Optional[int] = None) -> str: - if size == 0: - return "" - - data = list() - while size is None or size > 0: - line = self.readline(size or -1) - if not line: - break - if size is not None: - size -= len(line) - data.append(line) - - return "".join(data) - - def readlines(self, size: int = -1) -> List[str]: - return list(iter(self.readline, "")) - - -# TODO: -# -# Tab completion does not work if not at the end of the line. -# -# Numerous optimisations can be made but it seems to do all the lookup stuff -# fast enough on even my crappy server so I'm not too bothered about that -# at the moment. -# -# The popup window that displays the argspecs and completion suggestions -# needs to be an instance of a ListWin class or something so I can wrap -# the addstr stuff to a higher level. -# - - -# Have to ignore the return type on this one because the colors variable -# is Optional[MutableMapping[str, int]] but for the purposes of this -# function it can't be None -def get_color(config: Config, name: str) -> int: # type: ignore[return] - global colors - if colors: - return colors[config.color_scheme[name].lower()] - - -def get_colpair(config: Config, name: str) -> int: - return curses.color_pair(get_color(config, name) + 1) - - -def make_colors(config: Config) -> Dict[str, int]: - """Init all the colours in curses and bang them into a dictionary""" - - # blacK, Red, Green, Yellow, Blue, Magenta, Cyan, White, Default: - c = { - "k": 0, - "r": 1, - "g": 2, - "y": 3, - "b": 4, - "m": 5, - "c": 6, - "w": 7, - "d": -1, - } - - if platform.system() == "Windows": - c = dict( - list(c.items()) - + [ - ("K", 8), - ("R", 9), - ("G", 10), - ("Y", 11), - ("B", 12), - ("M", 13), - ("C", 14), - ("W", 15), - ] - ) - - for i in range(63): - if i > 7: - j = i // 8 - else: - j = c[config.color_scheme["background"]] - curses.init_pair(i + 1, i % 8, j) - - return c - - -class CLIInteraction(repl.Interaction): - def __init__(self, config: Config, statusbar: "Statusbar"): - super().__init__(config) - self.statusbar = statusbar - - def confirm(self, q: str) -> bool: - """Ask for yes or no and return boolean""" - try: - reply = self.statusbar.prompt(q) - except ValueError: - return False - - return reply.lower() in (_("y"), _("yes")) - - def notify( - self, s: str, n: float = 10.0, wait_for_keypress: bool = False - ) -> None: - self.statusbar.message(s, n) - - def file_prompt(self, s: str) -> Optional[str]: - return self.statusbar.prompt(s) - - -class CLIRepl(repl.Repl): - def __init__( - self, - scr: "_CursesWindow", - interp: repl.Interpreter, - statusbar: "Statusbar", - config: Config, - idle: Optional[Callable] = None, - ): - super().__init__(interp, config) - # mypy doesn't quite understand the difference between a class variable with a callable type and a method. - # https://github.com/python/mypy/issues/2427 - self.interp.writetb = self.writetb # type:ignore[assignment] - self.scr: "_CursesWindow" = scr - self.stdout_hist = "" # native str (bytes in Py2, unicode in Py3) - self.list_win = newwin(get_colpair(config, "background"), 1, 1, 1, 1) - self.cpos = 0 - self.do_exit = False - self.exit_value: Tuple[Any, ...] = () - self.f_string = "" - self.idle = idle - self.in_hist = False - self.paste_mode = False - self.last_key_press = time.time() - self.s = "" - self.statusbar = statusbar - self.formatter = BPythonFormatter(config.color_scheme) - self.interact = CLIInteraction(self.config, statusbar=self.statusbar) - self.ix: int - self.iy: int - self.arg_pos: Union[str, int, None] - self.prev_block_finished: int - - if config.cli_suggestion_width <= 0 or config.cli_suggestion_width > 1: - config.cli_suggestion_width = 0.8 - - def _get_cursor_offset(self) -> int: - return len(self.s) - self.cpos - - def _set_cursor_offset(self, offset: int) -> None: - self.cpos = len(self.s) - offset - - def addstr(self, s: str) -> None: - """Add a string to the current input line and figure out - where it should go, depending on the cursor position.""" - self.rl_history.reset() - if not self.cpos: - self.s += s - else: - l = len(self.s) - self.s = self.s[: l - self.cpos] + s + self.s[l - self.cpos :] - - self.complete() - - def atbol(self) -> bool: - """Return True or False accordingly if the cursor is at the beginning - of the line (whitespace is ignored). This exists so that p_key() knows - how to handle the tab key being pressed - if there is nothing but white - space before the cursor then process it as a normal tab otherwise - attempt tab completion.""" - - return not self.s.lstrip() - - # This function shouldn't return None because of pos -= self.bs() later on - def bs(self, delete_tabs: bool = True) -> int: # type: ignore[return-value] - """Process a backspace""" - - self.rl_history.reset() - y, x = self.scr.getyx() - - if not self.s: - return None # type: ignore[return-value] - - if x == self.ix and y == self.iy: - return None # type: ignore[return-value] - - n = 1 - - self.clear_wrapped_lines() - - if not self.cpos: - # I know the nested if blocks look nasty. :( - if self.atbol() and delete_tabs: - n = len(self.s) % self.config.tab_length - if not n: - n = self.config.tab_length - - self.s = self.s[:-n] - else: - self.s = self.s[: -self.cpos - 1] + self.s[-self.cpos :] - - self.print_line(self.s, clr=True) - - return n - - def bs_word(self) -> str: - self.rl_history.reset() - pos = len(self.s) - self.cpos - 1 - deleted = [] - # First we delete any space to the left of the cursor. - while pos >= 0 and self.s[pos] == " ": - deleted.append(self.s[pos]) - pos -= self.bs() - # Then we delete a full word. - while pos >= 0 and self.s[pos] != " ": - deleted.append(self.s[pos]) - pos -= self.bs() - - return "".join(reversed(deleted)) - - def check(self) -> None: - """Check if paste mode should still be active and, if not, deactivate - it and force syntax highlighting.""" - - if ( - self.paste_mode - and time.time() - self.last_key_press > self.config.paste_time - ): - self.paste_mode = False - self.print_line(self.s) - - def clear_current_line(self) -> None: - """Called when a SyntaxError occurred in the interpreter. It is - used to prevent autoindentation from occurring after a - traceback.""" - repl.Repl.clear_current_line(self) - self.s = "" - - def clear_wrapped_lines(self) -> None: - """Clear the wrapped lines of the current input.""" - # curses does not handle this on its own. Sad. - height, width = self.scr.getmaxyx() - max_y = min(self.iy + (self.ix + len(self.s)) // width + 1, height) - for y in range(self.iy + 1, max_y): - self.scr.move(y, 0) - self.scr.clrtoeol() - - def complete(self, tab: bool = False) -> None: - """Get Autocomplete list and window. - - Called whenever these should be updated, and called - with tab - """ - if self.paste_mode: - self.scr.touchwin() # TODO necessary? - return - - list_win_visible = repl.Repl.complete(self, tab) - - if list_win_visible: - try: - f = None - if self.matches_iter.completer: - f = self.matches_iter.completer.format - - self.show_list( - self.matches_iter.matches, - self.arg_pos, - topline=self.funcprops, - formatter=f, - ) - except curses.error: - # XXX: This is a massive hack, it will go away when I get - # cusswords into a good enough state that we can start - # using it. - self.list_win.border() - self.list_win.refresh() - list_win_visible = False - if not list_win_visible: - self.scr.redrawwin() - self.scr.refresh() - - def clrtobol(self) -> None: - """Clear from cursor to beginning of line; usual C-u behaviour""" - self.clear_wrapped_lines() - - if not self.cpos: - self.s = "" - else: - self.s = self.s[-self.cpos :] - - self.print_line(self.s, clr=True) - self.scr.redrawwin() - self.scr.refresh() - - def _get_current_line(self) -> str: - return self.s - - def _set_current_line(self, line: str) -> None: - self.s = line - - def cut_to_buffer(self) -> None: - """Clear from cursor to end of line, placing into cut buffer""" - self.cut_buffer = self.s[-self.cpos :] - self.s = self.s[: -self.cpos] - self.cpos = 0 - self.print_line(self.s, clr=True) - self.scr.redrawwin() - self.scr.refresh() - - def delete(self) -> None: - """Process a del""" - if not self.s: - return - - if self.mvc(-1): - self.bs(False) - - def echo(self, s: str, redraw: bool = True) -> None: - """Parse and echo a formatted string with appropriate attributes. It - uses the formatting method as defined in formatter.py to parse the - strings. It won't update the screen if it's reevaluating the code (as it - does with undo).""" - a = get_colpair(self.config, "output") - if "\x01" in s: - rx = re.search("\x01([A-Za-z])([A-Za-z]?)", s) - if rx: - fg = rx.groups()[0] - bg = rx.groups()[1] - col_num = self._C[fg.lower()] - if bg and bg != "I": - col_num *= self._C[bg.lower()] - - a = curses.color_pair(int(col_num) + 1) - if bg == "I": - a = a | curses.A_REVERSE - s = re.sub("\x01[A-Za-z][A-Za-z]?", "", s) - if fg.isupper(): - a = a | curses.A_BOLD - s = s.replace("\x03", "") - s = s.replace("\x01", "") - - # Replace NUL bytes, as addstr raises an exception otherwise - s = s.replace("\0", "") - # Replace \r\n bytes, as addstr remove the current line otherwise - s = s.replace("\r\n", "\n") - - self.scr.addstr(s, a) - - if redraw and not self.evaluating: - self.scr.refresh() - - def end(self, refresh: bool = True) -> bool: - self.cpos = 0 - h, w = gethw() - y, x = divmod(len(self.s) + self.ix, w) - y += self.iy - self.scr.move(y, x) - if refresh: - self.scr.refresh() - - return True - - def hbegin(self) -> None: - """Replace the active line with first line in history and - increment the index to keep track""" - self.cpos = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.first() - self.print_line(self.s, clr=True) - - def hend(self) -> None: - """Same as hbegin() but, well, forward""" - self.cpos = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.last() - self.print_line(self.s, clr=True) - - def back(self) -> None: - """Replace the active line with previous line in history and - increment the index to keep track""" - - self.cpos = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.back() - self.print_line(self.s, clr=True) - - def fwd(self) -> None: - """Same as back() but, well, forward""" - - self.cpos = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.forward() - self.print_line(self.s, clr=True) - - def search(self) -> None: - """Search with the partial matches from the history object.""" - - self.cpo = 0 - self.clear_wrapped_lines() - self.rl_history.enter(self.s) - self.s = self.rl_history.back(start=False, search=True) - self.print_line(self.s, clr=True) - - def get_key(self) -> str: - key = "" - while True: - try: - key += self.scr.getkey() - # Seems like we get a in the locale's encoding - # encoded string in Python 3 as well, but of - # type str instead of bytes, hence convert it to - # bytes first and decode then - key = key.encode("latin-1").decode(getpreferredencoding()) - self.scr.nodelay(False) - except UnicodeDecodeError: - # Yes, that actually kind of sucks, but I don't see another way to get - # input right - self.scr.nodelay(True) - except curses.error: - # I'm quite annoyed with the ambiguity of this exception handler. I previously - # caught "curses.error, x" and accessed x.message and checked that it was "no - # input", which seemed a crappy way of doing it. But then I ran it on a - # different computer and the exception seems to have entirely different - # attributes. So let's hope getkey() doesn't raise any other crazy curses - # exceptions. :) - self.scr.nodelay(False) - # XXX What to do here? Raise an exception? - if key: - return key - else: - if key != "\x00": - t = time.time() - self.paste_mode = ( - t - self.last_key_press <= self.config.paste_time - ) - self.last_key_press = t - return key - else: - key = "" - finally: - if self.idle: - self.idle(self) - - def get_line(self) -> str: - """Get a line of text and return it - This function initialises an empty string and gets the - curses cursor position on the screen and stores it - for the echo() function to use later (I think). - Then it waits for key presses and passes them to p_key(), - which returns None if Enter is pressed (that means "Return", - idiot).""" - - self.s = "" - self.rl_history.reset() - self.iy, self.ix = self.scr.getyx() - - if not self.paste_mode: - for _ in range(self.next_indentation()): - self.p_key("\t") - - self.cpos = 0 - - while True: - key = self.get_key() - if self.p_key(key) is None: - if self.config.cli_trim_prompts and self.s.startswith(">>> "): - self.s = self.s[4:] - return self.s - - def home(self, refresh: bool = True) -> bool: - self.scr.move(self.iy, self.ix) - self.cpos = len(self.s) - if refresh: - self.scr.refresh() - return True - - def lf(self) -> None: - """Process a linefeed character; it only needs to check the - cursor position and move appropriately so it doesn't clear - the current line after the cursor.""" - if self.cpos: - for _ in range(self.cpos): - self.mvc(-1) - - # Reprint the line (as there was maybe a highlighted paren in it) - self.print_line(self.s, newline=True) - self.echo("\n") - - def mkargspec( - self, - topline: inspection.FuncProps, - in_arg: Union[str, int, None], - down: bool, - ) -> int: - """This figures out what to do with the argspec and puts it nicely into - the list window. It returns the number of lines used to display the - argspec. It's also kind of messy due to it having to call so many - addstr() to get the colouring right, but it seems to be pretty - sturdy.""" - - r = 3 - fn = topline.func - args = topline.argspec.args - kwargs = topline.argspec.defaults - _args = topline.argspec.varargs - _kwargs = topline.argspec.varkwargs - is_bound_method = topline.is_bound_method - kwonly = topline.argspec.kwonly - kwonly_defaults = topline.argspec.kwonly_defaults or dict() - max_w = int(self.scr.getmaxyx()[1] * 0.6) - self.list_win.erase() - self.list_win.resize(3, max_w) - h, w = self.list_win.getmaxyx() - - self.list_win.addstr("\n ") - self.list_win.addstr( - fn, get_colpair(self.config, "name") | curses.A_BOLD - ) - self.list_win.addstr(": (", get_colpair(self.config, "name")) - maxh = self.scr.getmaxyx()[0] - - if is_bound_method and isinstance(in_arg, int): - in_arg += 1 - - punctuation_colpair = get_colpair(self.config, "punctuation") - - for k, i in enumerate(args): - y, x = self.list_win.getyx() - ln = len(str(i)) - kw = None - if kwargs and k + 1 > len(args) - len(kwargs): - kw = repr(kwargs[k - (len(args) - len(kwargs))]) - ln += len(kw) + 1 - - if ln + x >= w: - ty = self.list_win.getbegyx()[0] - if not down and ty > 0: - h += 1 - self.list_win.mvwin(ty - 1, 1) - self.list_win.resize(h, w) - elif down and h + r < maxh - ty: - h += 1 - self.list_win.resize(h, w) - else: - break - r += 1 - self.list_win.addstr("\n\t") - - if str(i) == "self" and k == 0: - color = get_colpair(self.config, "name") - else: - color = get_colpair(self.config, "token") - - if k == in_arg or i == in_arg: - color |= curses.A_BOLD - - self.list_win.addstr(str(i), color) - if kw is not None: - self.list_win.addstr("=", punctuation_colpair) - self.list_win.addstr(kw, get_colpair(self.config, "token")) - if k != len(args) - 1: - self.list_win.addstr(", ", punctuation_colpair) - - if _args: - if args: - self.list_win.addstr(", ", punctuation_colpair) - self.list_win.addstr(f"*{_args}", get_colpair(self.config, "token")) - - if kwonly: - if not _args: - if args: - self.list_win.addstr(", ", punctuation_colpair) - self.list_win.addstr("*", punctuation_colpair) - marker = object() - for arg in kwonly: - self.list_win.addstr(", ", punctuation_colpair) - color = get_colpair(self.config, "token") - if arg == in_arg: - color |= curses.A_BOLD - self.list_win.addstr(arg, color) - default = kwonly_defaults.get(arg, marker) - if default is not marker: - self.list_win.addstr("=", punctuation_colpair) - self.list_win.addstr( - repr(default), get_colpair(self.config, "token") - ) - - if _kwargs: - if args or _args or kwonly: - self.list_win.addstr(", ", punctuation_colpair) - self.list_win.addstr( - f"**{_kwargs}", get_colpair(self.config, "token") - ) - self.list_win.addstr(")", punctuation_colpair) - - return r - - def mvc(self, i: int, refresh: bool = True) -> bool: - """This method moves the cursor relatively from the current - position, where: - 0 == (right) end of current line - length of current line len(self.s) == beginning of current line - and: - current cursor position + i - for positive values of i the cursor will move towards the beginning - of the line, negative values the opposite.""" - y, x = self.scr.getyx() - - if self.cpos == 0 and i < 0: - return False - - if x == self.ix and y == self.iy and i >= 1: - return False - - h, w = gethw() - if x - i < 0: - y -= 1 - x = w - - if x - i >= w: - y += 1 - x = 0 + i - - self.cpos += i - self.scr.move(y, x - i) - if refresh: - self.scr.refresh() - - return True - - def p_key(self, key: str) -> Union[None, str, bool]: - """Process a keypress""" - - if key is None: - return "" - - config = self.config - - if platform.system() == "Windows": - C_BACK = chr(127) - BACKSP = chr(8) - else: - C_BACK = chr(8) - BACKSP = chr(127) - - if key == C_BACK: # C-Backspace (on my computer anyway!) - self.clrtobol() - key = "\n" - # Don't return; let it get handled - - if key == chr(27): # Escape Key - return "" - - if key in (BACKSP, "KEY_BACKSPACE"): - self.bs() - self.complete() - return "" - - elif key in key_dispatch[config.delete_key] and not self.s: - # Delete on empty line exits - self.do_exit = True - return None - - elif key in ("KEY_DC",) + key_dispatch[config.delete_key]: - self.delete() - self.complete() - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - return "" - - elif key in key_dispatch[config.undo_key]: # C-r - n = self.prompt_undo() - if n > 0: - self.undo(n=n) - return "" - - elif key in key_dispatch[config.search_key]: - self.search() - return "" - - elif key in ("KEY_UP",) + key_dispatch[config.up_one_line_key]: - # Cursor Up/C-p - self.back() - return "" - - elif key in ("KEY_DOWN",) + key_dispatch[config.down_one_line_key]: - # Cursor Down/C-n - self.fwd() - return "" - - elif key in ("KEY_LEFT", " ^B", chr(2)): # Cursor Left or ^B - self.mvc(1) - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - - elif key in ("KEY_RIGHT", "^F", chr(6)): # Cursor Right or ^F - self.mvc(-1) - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - - elif key in ("KEY_HOME", "^A", chr(1)): # home or ^A - self.home() - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - - elif key in ("KEY_END", "^E", chr(5)): # end or ^E - self.end() - # Redraw (as there might have been highlighted parens) - self.print_line(self.s) - - elif key in ("KEY_NPAGE",): # page_down - self.hend() - self.print_line(self.s) - - elif key in ("KEY_PPAGE",): # page_up - self.hbegin() - self.print_line(self.s) - - elif key in key_dispatch[config.cut_to_buffer_key]: # cut to buffer - self.cut_to_buffer() - return "" - - elif key in key_dispatch[config.yank_from_buffer_key]: - # yank from buffer - self.yank_from_buffer() - return "" - - elif key in key_dispatch[config.clear_word_key]: - self.cut_buffer = self.bs_word() - self.complete() - return "" - - elif key in key_dispatch[config.clear_line_key]: - self.clrtobol() - return "" - - elif key in key_dispatch[config.clear_screen_key]: - # clear all but current line - self.screen_hist: List = [self.screen_hist[-1]] - self.highlighted_paren = None - self.redraw() - return "" - - elif key in key_dispatch[config.exit_key]: - if not self.s: - self.do_exit = True - return None - else: - return "" - - elif key in key_dispatch[config.save_key]: - self.write2file() - return "" - - elif key in key_dispatch[config.pastebin_key]: - self.pastebin() - return "" - - elif key in key_dispatch[config.copy_clipboard_key]: - self.copy2clipboard() - return "" - - elif key in key_dispatch[config.last_output_key]: - page(self.stdout_hist[self.prev_block_finished : -4]) - return "" - - elif key in key_dispatch[config.show_source_key]: - try: - source = self.get_source_of_current_name() - except repl.SourceNotFound as e: - self.statusbar.message(f"{e}") - else: - if config.highlight_show_source: - source = format( - Python3Lexer().get_tokens(source), TerminalFormatter() - ) - page(source) - return "" - - elif key in ("\n", "\r", "PADENTER"): - self.lf() - return None - - elif key == "\t": - return self.tab() - - elif key == "KEY_BTAB": - return self.tab(back=True) - - elif key in key_dispatch[config.suspend_key]: - if platform.system() != "Windows": - self.suspend() - return "" - else: - self.do_exit = True - return None - - elif key == "\x18": - return self.send_current_line_to_editor() - - elif key == "\x03": - raise KeyboardInterrupt() - - elif key[0:3] == "PAD" and key not in ("PAD0", "PADSTOP"): - pad_keys = { - "PADMINUS": "-", - "PADPLUS": "+", - "PADSLASH": "/", - "PADSTAR": "*", - } - try: - self.addstr(pad_keys[key]) - self.print_line(self.s) - except KeyError: - return "" - elif len(key) == 1 and not unicodedata.category(key) == "Cc": - self.addstr(key) - self.print_line(self.s) - - else: - return "" - - return True - - def print_line( - self, s: Optional[str], clr: bool = False, newline: bool = False - ) -> None: - """Chuck a line of text through the highlighter, move the cursor - to the beginning of the line and output it to the screen.""" - - if not s: - clr = True - - if self.highlighted_paren is not None: - # Clear previous highlighted paren - - lineno = self.highlighted_paren[0] - tokens = self.highlighted_paren[1] - # mypy thinks tokens is List[Tuple[_TokenType, str]] - # but it is supposed to be MutableMapping[_TokenType, str] - self.reprint_line(lineno, tokens) - self.highlighted_paren = None - - if self.config.syntax and (not self.paste_mode or newline): - o = format(self.tokenize(s, newline), self.formatter) - else: - o = s - - self.f_string = o - self.scr.move(self.iy, self.ix) - - if clr: - self.scr.clrtoeol() - - if clr and not s: - self.scr.refresh() - - if o: - for t in o.split("\x04"): - self.echo(t.rstrip("\n")) - - if self.cpos: - t = self.cpos - for _ in range(self.cpos): - self.mvc(1) - self.cpos = t - - def prompt(self, more: Any) -> None: # I'm not sure of the type on this one - """Show the appropriate Python prompt""" - if not more: - self.echo( - "\x01{}\x03{}".format( - self.config.color_scheme["prompt"], self.ps1 - ) - ) - self.stdout_hist += self.ps1 - self.screen_hist.append( - "\x01%s\x03%s\x04" - % (self.config.color_scheme["prompt"], self.ps1) - ) - else: - prompt_more_color = self.config.color_scheme["prompt_more"] - self.echo(f"\x01{prompt_more_color}\x03{self.ps2}") - self.stdout_hist += self.ps2 - self.screen_hist.append( - f"\x01{prompt_more_color}\x03{self.ps2}\x04" - ) - - def push(self, s: str, insert_into_history: bool = True) -> bool: - # curses.raw(True) prevents C-c from causing a SIGINT - curses.raw(False) - try: - return super().push(s, insert_into_history) - except SystemExit as e: - # Avoid a traceback on e.g. quit() - self.do_exit = True - self.exit_value = e.args - return False - finally: - curses.raw(True) - - def redraw(self) -> None: - """Redraw the screen using screen_hist""" - self.scr.erase() - for k, s in enumerate(self.screen_hist): - if not s: - continue - self.iy, self.ix = self.scr.getyx() - for i in s.split("\x04"): - self.echo(i, redraw=False) - if k < len(self.screen_hist) - 1: - self.scr.addstr("\n") - self.iy, self.ix = self.scr.getyx() - self.print_line(self.s) - self.scr.refresh() - self.statusbar.refresh() - - def repl(self) -> Tuple[Any, ...]: - """Initialise the repl and jump into the loop. This method also has to - keep a stack of lines entered for the horrible "undo" feature. It also - tracks everything that would normally go to stdout in the normal Python - interpreter so it can quickly write it to stdout on exit after - curses.endwin(), as well as a history of lines entered for using - up/down to go back and forth (which has to be separate to the - evaluation history, which will be truncated when undoing.""" - - # Use our own helper function because Python's will use real stdin and - # stdout instead of our wrapped - self.push("from bpython._internal import _help as help\n", False) - - self.iy, self.ix = self.scr.getyx() - self.more = False - while not self.do_exit: - self.f_string = "" - self.prompt(self.more) - try: - inp = self.get_line() - except KeyboardInterrupt: - self.statusbar.message("KeyboardInterrupt") - self.scr.addstr("\n") - self.scr.touchwin() - self.scr.refresh() - continue - - self.scr.redrawwin() - if self.do_exit: - return self.exit_value - - self.history.append(inp) - self.screen_hist[-1] += self.f_string - self.stdout_hist += inp + "\n" - stdout_position = len(self.stdout_hist) - self.more = self.push(inp) - if not self.more: - self.prev_block_finished = stdout_position - self.s = "" - return self.exit_value - - def reprint_line( - self, lineno: int, tokens: List[Tuple[_TokenType, str]] - ) -> None: - """Helper function for paren highlighting: Reprint line at offset - `lineno` in current input buffer.""" - if not self.buffer or lineno == len(self.buffer): - return - - real_lineno = self.iy - height, width = self.scr.getmaxyx() - for i in range(lineno, len(self.buffer)): - string = self.buffer[i] - # 4 = length of prompt - length = len(string.encode(getpreferredencoding())) + 4 - real_lineno -= int(math.ceil(length / width)) - if real_lineno < 0: - return - - self.scr.move( - real_lineno, len(self.ps1) if lineno == 0 else len(self.ps2) - ) - line = format(tokens, BPythonFormatter(self.config.color_scheme)) - for string in line.split("\x04"): - self.echo(string) - - def resize(self) -> None: - """This method exists simply to keep it straight forward when - initialising a window and resizing it.""" - self.size() - self.scr.erase() - self.scr.resize(self.h, self.w) - self.scr.mvwin(self.y, self.x) - self.statusbar.resize(refresh=False) - self.redraw() - - def getstdout(self) -> str: - """This method returns the 'spoofed' stdout buffer, for writing to a - file or sending to a pastebin or whatever.""" - - return self.stdout_hist + "\n" - - def reevaluate(self) -> None: - """Clear the buffer, redraw the screen and re-evaluate the history""" - - self.evaluating = True - self.stdout_hist = "" - self.f_string = "" - self.buffer: List[str] = [] - self.scr.erase() - self.screen_hist = [] - # Set cursor position to -1 to prevent paren matching - self.cpos = -1 - - self.prompt(False) - - self.iy, self.ix = self.scr.getyx() - for line in self.history: - self.stdout_hist += line + "\n" - self.print_line(line) - self.screen_hist[-1] += self.f_string - # I decided it was easier to just do this manually - # than to make the print_line and history stuff more flexible. - self.scr.addstr("\n") - self.more = self.push(line) - self.prompt(self.more) - self.iy, self.ix = self.scr.getyx() - - self.cpos = 0 - indent = repl.next_indentation(self.s, self.config.tab_length) - self.s = "" - self.scr.refresh() - - if self.buffer: - for _ in range(indent): - self.tab() - - self.evaluating = False - # map(self.push, self.history) - # ^-- That's how simple this method was at first :( - - def write(self, s: str) -> None: - """For overriding stdout defaults""" - if "\x04" in s: - for block in s.split("\x04"): - self.write(block) - return - if s.rstrip() and "\x03" in s: - t = s.split("\x03")[1] - else: - t = s - - if not self.stdout_hist: - self.stdout_hist = t - else: - self.stdout_hist += t - - self.echo(s) - self.screen_hist.append(s.rstrip()) - - def show_list( - self, - items: List[str], - arg_pos: Union[str, int, None], - topline: Optional[inspection.FuncProps] = None, - formatter: Optional[Callable] = None, - current_item: Optional[str] = None, - ) -> None: - v_items: Collection - shared = ShowListState() - y, x = self.scr.getyx() - h, w = self.scr.getmaxyx() - down = y < h // 2 - if down: - max_h = h - y - else: - max_h = y + 1 - max_w = int(w * self.config.cli_suggestion_width) - self.list_win.erase() - - if items and formatter: - items = [formatter(x) for x in items] - if current_item is not None: - current_item = formatter(current_item) - - if topline: - height_offset = self.mkargspec(topline, arg_pos, down) + 1 - else: - height_offset = 0 - - def lsize() -> bool: - wl = max(len(i) for i in v_items) + 1 - if not wl: - wl = 1 - cols = ((max_w - 2) // wl) or 1 - rows = len(v_items) // cols - - if cols * rows < len(v_items): - rows += 1 - - if rows + 2 >= max_h: - return False - - shared.rows = rows - shared.cols = cols - shared.wl = wl - return True - - if items: - # visible items (we'll append until we can't fit any more in) - v_items = [items[0][: max_w - 3]] - lsize() - else: - v_items = [] - - for i in items[1:]: - v_items.append(i[: max_w - 3]) - if not lsize(): - del v_items[-1] - v_items[-1] = "..." - break - - rows = shared.rows - if rows + height_offset < max_h: - rows += height_offset - display_rows = rows - else: - display_rows = rows + height_offset - - cols = shared.cols - wl = shared.wl - - if topline and not v_items: - w = max_w - elif wl + 3 > max_w: - w = max_w - else: - t = (cols + 1) * wl + 3 - if t > max_w: - t = max_w - w = t - - if height_offset and display_rows + 5 >= max_h: - del v_items[-(cols * (height_offset)) :] - - if self.docstring is None: - self.list_win.resize(rows + 2, w) - else: - docstring = self.format_docstring( - self.docstring, max_w - 2, max_h - height_offset - ) - docstring_string = "".join(docstring) - rows += len(docstring) - self.list_win.resize(rows, max_w) - - if down: - self.list_win.mvwin(y + 1, 0) - else: - self.list_win.mvwin(y - rows - 2, 0) - - if v_items: - self.list_win.addstr("\n ") - - for ix, i in enumerate(v_items): - padding = (wl - len(i)) * " " - if i == current_item: - color = get_colpair(self.config, "operator") - else: - color = get_colpair(self.config, "main") - self.list_win.addstr(i + padding, color) - if (cols == 1 or (ix and not (ix + 1) % cols)) and ix + 1 < len( - v_items - ): - self.list_win.addstr("\n ") - - if self.docstring is not None: - self.list_win.addstr( - "\n" + docstring_string, get_colpair(self.config, "comment") - ) - # XXX: After all the trouble I had with sizing the list box (I'm not very good - # at that type of thing) I decided to do this bit of tidying up here just to - # make sure there's no unnecessary blank lines, it makes things look nicer. - - y = self.list_win.getyx()[0] - self.list_win.resize(y + 2, w) - - self.statusbar.win.touchwin() - self.statusbar.win.noutrefresh() - self.list_win.attron(get_colpair(self.config, "main")) - self.list_win.border() - self.scr.touchwin() - self.scr.cursyncup() - self.scr.noutrefresh() - - # This looks a little odd, but I can't figure a better way to stick the cursor - # back where it belongs (refreshing the window hides the list_win) - - self.scr.move(*self.scr.getyx()) - self.list_win.refresh() - - def size(self) -> None: - """Set instance attributes for x and y top left corner coordinates - and width and height for the window.""" - global stdscr - if stdscr: - h, w = stdscr.getmaxyx() - self.y: int = 0 - self.w: int = w - self.h: int = h - 1 - self.x: int = 0 - - def suspend(self) -> None: - """Suspend the current process for shell job control.""" - if platform.system() != "Windows": - curses.endwin() - os.kill(os.getpid(), signal.SIGSTOP) - - def tab(self, back: bool = False) -> bool: - """Process the tab key being hit. - - If there's only whitespace - in the line or the line is blank then process a normal tab, - otherwise attempt to autocomplete to the best match of possible - choices in the match list. - - If `back` is True, walk backwards through the list of suggestions - and don't indent if there are only whitespace in the line. - """ - - # 1. check if we should add a tab character - if self.atbol() and not back: - x_pos = len(self.s) - self.cpos - num_spaces = x_pos % self.config.tab_length - if not num_spaces: - num_spaces = self.config.tab_length - - self.addstr(" " * num_spaces) - self.print_line(self.s) - return True - - # 2. run complete() if we aren't already iterating through matches - if not self.matches_iter: - self.complete(tab=True) - self.print_line(self.s) - - # 3. check to see if we can expand the current word - if self.matches_iter.is_cseq(): - # TODO resolve this error-prone situation: - # can't assign at same time to self.s and self.cursor_offset - # because for cursor_offset - # property to work correctly, self.s must already be set - temp_cursor_offset, self.s = self.matches_iter.substitute_cseq() - self.cursor_offset = temp_cursor_offset - self.print_line(self.s) - if not self.matches_iter: - self.complete() - - # 4. swap current word for a match list item - elif self.matches_iter.matches: - current_match = ( - self.matches_iter.previous() - if back - else next(self.matches_iter) - ) - try: - f = None - if self.matches_iter.completer: - f = self.matches_iter.completer.format - - self.show_list( - self.matches_iter.matches, - self.arg_pos, - topline=self.funcprops, - formatter=f, - current_item=current_match, - ) - except curses.error: - # XXX: This is a massive hack, it will go away when I get - # cusswords into a good enough state that we can start - # using it. - self.list_win.border() - self.list_win.refresh() - _, self.s = self.matches_iter.cur_line() - self.print_line(self.s, True) - return True - - def undo(self, n: int = 1) -> None: - repl.Repl.undo(self, n) - - # This will unhighlight highlighted parens - self.print_line(self.s) - - def writetb(self, lines: List[str]) -> None: - for line in lines: - self.write( - "\x01{}\x03{}".format(self.config.color_scheme["error"], line) - ) - - def yank_from_buffer(self) -> None: - """Paste the text from the cut buffer at the current cursor location""" - self.addstr(self.cut_buffer) - self.print_line(self.s, clr=True) - - def send_current_line_to_editor(self) -> str: - lines = self.send_to_external_editor(self.s).split("\n") - self.s = "" - self.print_line(self.s) - while lines and not lines[-1]: - lines.pop() - if not lines: - return "" - - self.f_string = "" - self.cpos = -1 # Set cursor position to -1 to prevent paren matching - - self.iy, self.ix = self.scr.getyx() - self.evaluating = True - for line in lines: - self.stdout_hist += line + "\n" - self.history.append(line) - self.print_line(line) - self.screen_hist[-1] += self.f_string - self.scr.addstr("\n") - self.more = self.push(line) - self.prompt(self.more) - self.iy, self.ix = self.scr.getyx() - self.evaluating = False - - self.cpos = 0 - indent = repl.next_indentation(self.s, self.config.tab_length) - self.s = "" - self.scr.refresh() - - if self.buffer: - for _ in range(indent): - self.tab() - - self.print_line(self.s) - self.scr.redrawwin() - return "" - - -class Statusbar: - """This class provides the status bar at the bottom of the screen. - It has message() and prompt() methods for user interactivity, as - well as settext() and clear() methods for changing its appearance. - - The check() method needs to be called repeatedly if the statusbar is - going to be aware of when it should update its display after a message() - has been called (it'll display for a couple of seconds and then disappear). - - It should be called as: - foo = Statusbar(stdscr, scr, 'Initial text to display') - or, for a blank statusbar: - foo = Statusbar(stdscr, scr) - - It can also receive the argument 'c' which will be an integer referring - to a curses colour pair, e.g.: - foo = Statusbar(stdscr, 'Hello', c=4) - - stdscr should be a curses window object in which to put the status bar. - pwin should be the parent window. To be honest, this is only really here - so the cursor can be returned to the window properly. - - """ - - def __init__( - self, - scr: "_CursesWindow", - pwin: "_CursesWindow", - background: int, - config: Config, - s: Optional[str] = None, - c: Optional[int] = None, - ): - """Initialise the statusbar and display the initial text (if any)""" - self.size() - self.win: "_CursesWindow" = newwin( - background, self.h, self.w, self.y, self.x - ) - - self.config = config - - self.s = s or "" - self._s = self.s - self.c = c - self.timer = 0 - self.pwin = pwin - if s: - self.settext(s, c) - - def size(self) -> None: - """Set instance attributes for x and y top left corner coordinates - and width and height for the window.""" - h, w = gethw() - self.y = h - 1 - self.w = w - self.h = 1 - self.x = 0 - - def resize(self, refresh: bool = True) -> None: - """This method exists simply to keep it straight forward when - initialising a window and resizing it.""" - self.size() - self.win.mvwin(self.y, self.x) - self.win.resize(self.h, self.w) - if refresh: - self.refresh() - - def refresh(self) -> None: - """This is here to make sure the status bar text is redraw properly - after a resize.""" - self.settext(self._s) - - def check(self) -> None: - """This is the method that should be called every half second or so - to see if the status bar needs updating.""" - if not self.timer: - return - - if time.time() < self.timer: - return - - self.settext(self._s) - - def message(self, s: str, n: float = 3.0) -> None: - """Display a message for a short n seconds on the statusbar and return - it to its original state.""" - self.timer = int(time.time() + n) - self.settext(s) - - def prompt(self, s: str = "") -> str: - """Prompt the user for some input (with the optional prompt 's') and - return the input text, then restore the statusbar to its original - value.""" - - self.settext(s or "? ", p=True) - iy, ix = self.win.getyx() - - def bs(s: str) -> str: - y, x = self.win.getyx() - if x == ix: - return s - s = s[:-1] - self.win.delch(y, x - 1) - self.win.move(y, x - 1) - return s - - o = "" - while True: - c = self.win.getch() - - # '\b' - if c == 127: - o = bs(o) - # '\n' - elif c == 10: - break - # ESC - elif c == 27: - curses.flushinp() - raise ValueError - # literal - elif 0 < c < 127: - d = chr(c) - self.win.addstr(d, get_colpair(self.config, "prompt")) - o += d - - self.settext(self._s) - return o - - def settext(self, s: str, c: Optional[int] = None, p: bool = False) -> None: - """Set the text on the status bar to a new permanent value; this is the - value that will be set after a prompt or message. c is the optional - curses colour pair to use (if not specified the last specified colour - pair will be used). p is True if the cursor is expected to stay in the - status window (e.g. when prompting).""" - - self.win.erase() - if len(s) >= self.w: - s = s[: self.w - 1] - - self.s = s - if c: - self.c = c - - if s: - if self.c: - self.win.addstr(s, self.c) - else: - self.win.addstr(s) - - if not p: - self.win.noutrefresh() - self.pwin.refresh() - else: - self.win.refresh() - - def clear(self) -> None: - """Clear the status bar.""" - self.win.clear() - - -def init_wins( - scr: "_CursesWindow", config: Config -) -> Tuple["_CursesWindow", Statusbar]: - """Initialise the two windows (the main repl interface and the little - status bar at the bottom with some stuff in it)""" - # TODO: Document better what stuff is on the status bar. - - background = get_colpair(config, "background") - h, w = gethw() - - main_win = newwin(background, h - 1, w, 0, 0) - main_win.scrollok(True) - - # I think this is supposed to be True instead of 1? - main_win.keypad(1) # type:ignore[arg-type] - # Thanks to Angus Gibson for pointing out this missing line which was causing - # problems that needed dirty hackery to fix. :) - - commands = ( - (_("Rewind"), config.undo_key), - (_("Save"), config.save_key), - (_("Pastebin"), config.pastebin_key), - (_("Pager"), config.last_output_key), - (_("Show Source"), config.show_source_key), - ) - - message = " ".join( - f"<{key}> {command}" for command, key in commands if key - ) - - statusbar = Statusbar( - scr, main_win, background, config, message, get_colpair(config, "main") - ) - - return main_win, statusbar - - -def sigwinch(unused_scr: "_CursesWindow") -> None: - global DO_RESIZE - DO_RESIZE = True - - -def sigcont(unused_scr: "_CursesWindow") -> None: - sigwinch(unused_scr) - # Forces the redraw - curses.ungetch("\x00") - - -def gethw() -> Tuple[int, int]: - """I found this code on a usenet post, and snipped out the bit I needed, - so thanks to whoever wrote that, sorry I forgot your name, I'm sure you're - a great guy. - - It's unfortunately necessary (unless someone has any better ideas) in order - to allow curses and readline to work together. I looked at the code for - libreadline and noticed this comment: - - /* This is the stuff that is hard for me. I never seem to write good - display routines in C. Let's see how I do this time. */ - - So I'm not going to ask any questions. - - """ - - if platform.system() != "Windows": - h, w = struct.unpack( - "hhhh", - fcntl.ioctl( - sys.__stdout__, termios.TIOCGWINSZ, "\000" * 8 - ), # type:ignore[call-overload] - )[0:2] - else: - # Ignoring mypy's windll error because it's Windows-specific - from ctypes import ( # type:ignore[attr-defined] - windll, - create_string_buffer, - ) - - # stdin handle is -10 - # stdout handle is -11 - # stderr handle is -12 - - h = windll.kernel32.GetStdHandle(-12) - csbi = create_string_buffer(22) - res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) - - if res: - ( - bufx, - bufy, - curx, - cury, - wattr, - left, - top, - right, - bottom, - maxx, - maxy, - ) = struct.unpack("hhhhHhhhhhh", csbi.raw) - sizex = right - left + 1 - sizey = bottom - top + 1 - elif stdscr: - # can't determine actual size - return default values - sizex, sizey = stdscr.getmaxyx() - - h, w = sizey, sizex - return h, w - - -def idle(caller: CLIRepl) -> None: - """This is called once every iteration through the getkey() - loop (currently in the Repl class, see the get_line() method). - The statusbar check needs to go here to take care of timed - messages and the resize handlers need to be here to make - sure it happens conveniently.""" - global DO_RESIZE - - if caller.module_gatherer.find_coroutine() or caller.paste_mode: - caller.scr.nodelay(True) - key = caller.scr.getch() - caller.scr.nodelay(False) - if key != -1: - curses.ungetch(key) - else: - curses.ungetch("\x00") - caller.statusbar.check() - caller.check() - - if DO_RESIZE: - do_resize(caller) - - -def do_resize(caller: CLIRepl) -> None: - """This needs to hack around readline and curses not playing - nicely together. See also gethw() above.""" - global DO_RESIZE - h, w = gethw() - if not h: - # Hopefully this shouldn't happen. :) - return - - curses.endwin() - os.environ["LINES"] = str(h) - os.environ["COLUMNS"] = str(w) - curses.doupdate() - DO_RESIZE = False - - try: - caller.resize() - except curses.error: - pass - # The list win resizes itself every time it appears so no need to do it here. - - -class FakeDict: - """Very simple dict-alike that returns a constant value for any key - - used as a hacky solution to using a colours dict containing colour codes if - colour initialisation fails.""" - - def __init__(self, val: int): - self._val = val - - def __getitem__(self, k: Any) -> int: - return self._val - - -def newwin(background: int, *args: int) -> "_CursesWindow": - """Wrapper for curses.newwin to automatically set background colour on any - newly created window.""" - win = curses.newwin(*args) - win.bkgd(" ", background) - return win - - -def curses_wrapper(func: Callable, *args: Any, **kwargs: Any) -> Any: - """Like curses.wrapper(), but reuses stdscr when called again.""" - global stdscr - if stdscr is None: - stdscr = curses.initscr() - try: - curses.noecho() - curses.cbreak() - # Should this be keypad(True)? - stdscr.keypad(1) # type:ignore[arg-type] - - try: - curses.start_color() - except curses.error: - pass - - return func(stdscr, *args, **kwargs) - finally: - # Should this be keypad(False)? - stdscr.keypad(0) # type:ignore[arg-type] - curses.echo() - curses.nocbreak() - curses.endwin() - - -def main_curses( - scr: "_CursesWindow", - args: List[str], - config: Config, - interactive: bool = True, - locals_: Optional[Dict[str, Any]] = None, - banner: Optional[str] = None, -) -> Tuple[Tuple[Any, ...], str]: - """main function for the curses convenience wrapper - - Initialise the two main objects: the interpreter - and the repl. The repl does what a repl does and lots - of other cool stuff like syntax highlighting and stuff. - I've tried to keep it well factored but it needs some - tidying up, especially in separating the curses stuff - from the rest of the repl. - - Returns a tuple (exit value, output), where exit value is a tuple - with arguments passed to SystemExit. - """ - global stdscr - global DO_RESIZE - global colors - DO_RESIZE = False - - if platform.system() != "Windows": - old_sigwinch_handler = signal.signal( - signal.SIGWINCH, lambda *_: sigwinch(scr) - ) - # redraw window after being suspended - old_sigcont_handler = signal.signal( - signal.SIGCONT, lambda *_: sigcont(scr) - ) - - stdscr = scr - try: - curses.start_color() - curses.use_default_colors() - cols = make_colors(config) - except curses.error: - # Not sure what to do with the types here... - # FakeDict acts as a dictionary, but isn't actually a dictionary - cols = FakeDict(-1) # type:ignore[assignment] - - # FIXME: Gargh, bad design results in using globals without a refactor :( - colors = cols - - scr.timeout(300) - - curses.raw(True) - main_win, statusbar = init_wins(scr, config) - - interpreter = repl.Interpreter(locals_) - - clirepl = CLIRepl(main_win, interpreter, statusbar, config, idle) - clirepl._C = cols - - # Not sure how to type these Fake types - sys.stdin = FakeStdin(clirepl) # type:ignore[assignment] - sys.stdout = FakeStream(clirepl, lambda: sys.stdout) # type:ignore - sys.stderr = FakeStream(clirepl, lambda: sys.stderr) # type:ignore - - if args: - exit_value: Tuple[Any, ...] = () - try: - bpargs.exec_code(interpreter, args) - except SystemExit as e: - # The documentation of code.InteractiveInterpreter.runcode claims - # that it reraises SystemExit. However, I can't manage to trigger - # that. To be one the safe side let's catch SystemExit here anyway. - exit_value = e.args - if not interactive: - curses.raw(False) - return (exit_value, clirepl.getstdout()) - else: - sys.path.insert(0, "") - try: - clirepl.startup() - except OSError as e: - # Handle this with a proper error message. - if e.errno != errno.ENOENT: - raise - - if banner is not None: - clirepl.write(banner) - clirepl.write("\n") - - # XXX these deprecation warnings need to go at some point - clirepl.write( - _( - "WARNING: You are using `bpython-cli`, the curses backend for `bpython`. This backend has been deprecated in version 0.19 and might disappear in a future version." - ) - ) - clirepl.write("\n") - - exit_value = clirepl.repl() - if hasattr(sys, "exitfunc"): - # Seems like the if statement should satisfy mypy, but it doesn't - sys.exitfunc() # type:ignore[attr-defined] - delattr(sys, "exitfunc") - - main_win.erase() - main_win.refresh() - statusbar.win.clear() - statusbar.win.refresh() - curses.raw(False) - - # Restore signal handlers - if platform.system() != "Windows": - signal.signal(signal.SIGWINCH, old_sigwinch_handler) - signal.signal(signal.SIGCONT, old_sigcont_handler) - - return (exit_value, clirepl.getstdout()) - - -def main( - args: Optional[List[str]] = None, - locals_: Optional[MutableMapping[str, str]] = None, - banner: Optional[str] = None, -) -> Any: - translations.init() - - config, options, exec_args = argsparse(args) - - # Save stdin, stdout and stderr for later restoration - orig_stdin = sys.stdin - orig_stdout = sys.stdout - orig_stderr = sys.stderr - - try: - (exit_value, output) = curses_wrapper( - main_curses, - exec_args, - config, - options.interactive, - locals_, - banner=banner, - ) - finally: - sys.stdin = orig_stdin - sys.stderr = orig_stderr - sys.stdout = orig_stdout - - # Fake stdout data so everything's still visible after exiting - if config.flush_output and not options.quiet: - sys.stdout.write(output) - if hasattr(sys.stdout, "flush"): - sys.stdout.flush() - return repl.extract_exit_value(exit_value) - - -if __name__ == "__main__": - sys.exit(main()) - -# vim: sw=4 ts=4 sts=4 ai et diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index b3e8912fd..74f4b7217 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -10,7 +10,7 @@ from pathlib import Path from unittest import mock -from bpython import config, repl, cli, autocomplete +from bpython import config, repl, autocomplete from bpython.line import LinePart from bpython.test import ( MagicIterMock, @@ -67,13 +67,6 @@ def reevaluate(self): raise NotImplementedError -class FakeCliRepl(cli.CLIRepl, FakeRepl): - def __init__(self): - self.s = "" - self.cpos = 0 - self.rl_history = FakeHistory() - - class TestMatchesIterator(unittest.TestCase): def setUp(self): self.matches = ["bobby", "bobbies", "bobberina"] @@ -539,148 +532,5 @@ def __init__(self, *args, **kwargs): self.assertEqual(self.repl.matches_iter.matches, ["apple2=", "apple="]) -class TestCliRepl(unittest.TestCase): - def setUp(self): - self.repl = FakeCliRepl() - - def test_atbol(self): - self.assertTrue(self.repl.atbol()) - - self.repl.s = "\t\t" - self.assertTrue(self.repl.atbol()) - - self.repl.s = "\t\tnot an empty line" - self.assertFalse(self.repl.atbol()) - - def test_addstr(self): - self.repl.complete = mock.Mock(True) - - self.repl.s = "foo" - self.repl.addstr("bar") - self.assertEqual(self.repl.s, "foobar") - - self.repl.cpos = 3 - self.repl.addstr("buzz") - self.assertEqual(self.repl.s, "foobuzzbar") - - -class TestCliReplTab(unittest.TestCase): - def setUp(self): - self.repl = FakeCliRepl() - - # 3 Types of tab complete - def test_simple_tab_complete(self): - self.repl.matches_iter = MagicIterMock() - self.repl.matches_iter.__bool__.return_value = False - self.repl.complete = mock.Mock() - self.repl.print_line = mock.Mock() - self.repl.matches_iter.is_cseq.return_value = False - self.repl.show_list = mock.Mock() - self.repl.funcprops = mock.Mock() - self.repl.arg_pos = mock.Mock() - self.repl.matches_iter.cur_line.return_value = (None, "foobar") - - self.repl.s = "foo" - self.repl.tab() - self.assertTrue(self.repl.complete.called) - self.repl.complete.assert_called_with(tab=True) - self.assertEqual(self.repl.s, "foobar") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_substring_tab_complete(self): - self.repl.s = "bar" - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.FUZZY - ) - self.repl.tab() - self.assertEqual(self.repl.s, "foobar") - self.repl.tab() - self.assertEqual(self.repl.s, "foofoobar") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_fuzzy_tab_complete(self): - self.repl.s = "br" - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.FUZZY - ) - self.repl.tab() - self.assertEqual(self.repl.s, "foobar") - - # Edge Cases - def test_normal_tab(self): - """make sure pressing the tab key will - still in some cases add a tab""" - self.repl.s = "" - self.repl.config = mock.Mock() - self.repl.config.tab_length = 4 - self.repl.complete = mock.Mock() - self.repl.print_line = mock.Mock() - self.repl.tab() - self.assertEqual(self.repl.s, " ") - - def test_back_parameter(self): - self.repl.matches_iter = mock.Mock() - self.repl.matches_iter.matches = True - self.repl.matches_iter.previous.return_value = "previtem" - self.repl.matches_iter.is_cseq.return_value = False - self.repl.show_list = mock.Mock() - self.repl.funcprops = mock.Mock() - self.repl.arg_pos = mock.Mock() - self.repl.matches_iter.cur_line.return_value = (None, "previtem") - self.repl.print_line = mock.Mock() - self.repl.s = "foo" - self.repl.cpos = 0 - self.repl.tab(back=True) - self.assertTrue(self.repl.matches_iter.previous.called) - self.assertTrue(self.repl.s, "previtem") - - # Attribute Tests - @unittest.skip("disabled while non-simple completion is disabled") - def test_fuzzy_attribute_tab_complete(self): - """Test fuzzy attribute with no text""" - self.repl.s = "Foo." - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.FUZZY - ) - - self.repl.tab() - self.assertEqual(self.repl.s, "Foo.foobar") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_fuzzy_attribute_tab_complete2(self): - """Test fuzzy attribute with some text""" - self.repl.s = "Foo.br" - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.FUZZY - ) - - self.repl.tab() - self.assertEqual(self.repl.s, "Foo.foobar") - - # Expand Tests - def test_simple_expand(self): - self.repl.s = "f" - self.cpos = 0 - self.repl.matches_iter = mock.Mock() - self.repl.matches_iter.is_cseq.return_value = True - self.repl.matches_iter.substitute_cseq.return_value = (3, "foo") - self.repl.print_line = mock.Mock() - self.repl.tab() - self.assertEqual(self.repl.s, "foo") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_substring_expand_forward(self): - self.repl.config.autocomplete_mode = ( - autocomplete.AutocompleteModes.SUBSTRING - ) - self.repl.s = "ba" - self.repl.tab() - self.assertEqual(self.repl.s, "bar") - - @unittest.skip("disabled while non-simple completion is disabled") - def test_fuzzy_expand(self): - pass - - if __name__ == "__main__": unittest.main() diff --git a/bpython/translations/bpython.pot b/bpython/translations/bpython.pot index e11140ed2..c69572910 100644 --- a/bpython/translations/bpython.pot +++ b/bpython/translations/bpython.pot @@ -61,42 +61,14 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/de/LC_MESSAGES/bpython.po b/bpython/translations/de/LC_MESSAGES/bpython.po index 79b3acf71..feb534f7f 100644 --- a/bpython/translations/de/LC_MESSAGES/bpython.po +++ b/bpython/translations/de/LC_MESSAGES/bpython.po @@ -67,45 +67,15 @@ msgstr "" "Auszuführende Datei und zusätzliche Argumente, die an das Script " "übergeben werden sollen." -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "j" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "ja" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "Rückgängig" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "Speichern" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "Pastebin" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "Quellcode anzeigen" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" -"ACHTUNG: `bpython-cli` wird verwendet, die curses Implementierung von " -"`bpython`. Diese Implementierung wird ab Version 0.19 nicht mehr aktiv " -"unterstützt und wird in einer zukünftigen Version entfernt werden." - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/es_ES/LC_MESSAGES/bpython.po b/bpython/translations/es_ES/LC_MESSAGES/bpython.po index 5af25b37a..d34872816 100644 --- a/bpython/translations/es_ES/LC_MESSAGES/bpython.po +++ b/bpython/translations/es_ES/LC_MESSAGES/bpython.po @@ -62,42 +62,15 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "s" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "si" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po index 32bbec662..ba1205048 100644 --- a/bpython/translations/fr_FR/LC_MESSAGES/bpython.po +++ b/bpython/translations/fr_FR/LC_MESSAGES/bpython.po @@ -66,42 +66,15 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "o" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "oui" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "Rembobiner" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "Sauvegarder" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "Montrer le code source" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/it_IT/LC_MESSAGES/bpython.po b/bpython/translations/it_IT/LC_MESSAGES/bpython.po index d0076cffd..46488bc3c 100644 --- a/bpython/translations/it_IT/LC_MESSAGES/bpython.po +++ b/bpython/translations/it_IT/LC_MESSAGES/bpython.po @@ -62,42 +62,15 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "s" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "si" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po index d110f3ba8..375f4f32e 100644 --- a/bpython/translations/nl_NL/LC_MESSAGES/bpython.po +++ b/bpython/translations/nl_NL/LC_MESSAGES/bpython.po @@ -62,42 +62,15 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" -#: bpython/cli.py:320 bpython/curtsiesfrontend/interaction.py:107 +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "j" -#: bpython/cli.py:320 bpython/urwid.py:539 +#: bpython/urwid.py:539 msgid "yes" msgstr "ja" -#: bpython/cli.py:1696 -msgid "Rewind" -msgstr "" - -#: bpython/cli.py:1697 -msgid "Save" -msgstr "" - -#: bpython/cli.py:1698 -msgid "Pastebin" -msgstr "" - -#: bpython/cli.py:1699 -msgid "Pager" -msgstr "" - -#: bpython/cli.py:1700 -msgid "Show Source" -msgstr "" - -#: bpython/cli.py:1947 -msgid "" -"WARNING: You are using `bpython-cli`, the curses backend for `bpython`. " -"This backend has been deprecated in version 0.19 and might disappear in a" -" future version." -msgstr "" - #: bpython/curtsies.py:201 msgid "start by pasting lines of a file into session" msgstr "" diff --git a/doc/sphinx/source/tips.rst b/doc/sphinx/source/tips.rst index f2519b405..4bfbc2e43 100644 --- a/doc/sphinx/source/tips.rst +++ b/doc/sphinx/source/tips.rst @@ -16,7 +16,7 @@ equivalent file. .. code-block:: bash - alias bpython3.5='PYTHONPATH=~/python/bpython python3.5 -m bpython.cli' + alias bpython3.5='PYTHONPATH=~/python/bpython python3.5 -m bpython.curtsies' Where the `~/python/bpython`-path is the path to where your bpython source code resides. diff --git a/doc/sphinx/source/windows.rst b/doc/sphinx/source/windows.rst index 6d2c05a0b..5374f70fb 100644 --- a/doc/sphinx/source/windows.rst +++ b/doc/sphinx/source/windows.rst @@ -7,9 +7,3 @@ other platforms as well. There are no official binaries for bpython on Windows (though this is something we plan on providing in the future). - -The easiest way to get `bpython.cli` (the curses frontend running) is to install -an unofficial windows binary for pdcurses from: -http://www.lfd.uci.edu/~gohlke/pythonlibs/#curses. After this you can just -`pip install bpython` and run bpython curses frontend like you would on a Linux -system (e.g. by typing `bpython-curses` on your prompt). diff --git a/setup.cfg b/setup.cfg index 8c4294d9c..c0c5d03e7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,6 @@ watch = watchdog [options.entry_points] console_scripts = bpython = bpython.curtsies:main - bpython-curses = bpython.cli:main bpython-urwid = bpython.urwid:main [urwid] bpdb = bpdb:main From 5aa1989a8ed299aac155b24c4a4b4adf0542cc17 Mon Sep 17 00:00:00 2001 From: supakeen Date: Tue, 9 Nov 2021 17:46:04 +0000 Subject: [PATCH 500/555] Refer directly to the top-level package in docs. --- doc/sphinx/source/tips.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/source/tips.rst b/doc/sphinx/source/tips.rst index 4bfbc2e43..0745e3bfa 100644 --- a/doc/sphinx/source/tips.rst +++ b/doc/sphinx/source/tips.rst @@ -16,7 +16,7 @@ equivalent file. .. code-block:: bash - alias bpython3.5='PYTHONPATH=~/python/bpython python3.5 -m bpython.curtsies' + alias bpython3.5='PYTHONPATH=~/python/bpython python3.5 -m bpython' Where the `~/python/bpython`-path is the path to where your bpython source code resides. From acffa531199c090baa2d512851b1cedd432fefe5 Mon Sep 17 00:00:00 2001 From: supakeen Date: Tue, 9 Nov 2021 17:47:08 +0000 Subject: [PATCH 501/555] Bit too heavy handed on the translations. --- bpython/translations/bpython.pot | 1 + 1 file changed, 1 insertion(+) diff --git a/bpython/translations/bpython.pot b/bpython/translations/bpython.pot index c69572910..9237869da 100644 --- a/bpython/translations/bpython.pot +++ b/bpython/translations/bpython.pot @@ -61,6 +61,7 @@ msgstr "" msgid "File to execute and additional arguments passed on to the executed script." msgstr "" +#: bpython/curtsiesfrontend/interaction.py:107 #: bpython/urwid.py:539 msgid "y" msgstr "" From 26fc2b580c774c824d1f234b32a3be0f404db6be Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Mon, 1 May 2023 10:33:26 +0200 Subject: [PATCH 502/555] Require Sphinx < 7 for now Removal of the setuptools integration from Sphinx breaks our builds. See https://github.com/sphinx-doc/sphinx/pull/11363 for the change in Sphinx. --- .github/workflows/build.yaml | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1e2a374a5..9254d49cd 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 twisted watchdog "jedi >=0.16" babel "sphinx >=1.5,<7" pip install pytest pytest-cov numpy - name: Build with Python ${{ matrix.python-version }} run: | diff --git a/setup.py b/setup.py index 6790b9d78..7ca279d35 100755 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ from sphinx.setup_command import BuildDoc # Sphinx 1.5 and newer support Python 3.6 - using_sphinx = sphinx.__version__ >= "1.5" + using_sphinx = sphinx.__version__ >= "1.5" and sphinx.__version__ < "7.0" except ImportError: using_sphinx = False From db6a559724a59713ac905d7b0533ef382fab930d Mon Sep 17 00:00:00 2001 From: Jochen Kupperschmidt Date: Tue, 11 Jul 2023 23:24:07 +0200 Subject: [PATCH 503/555] Explicitly set README content type The README, being a reStructuredText document, is not rendered as such on PyPI, just as plaintext. This *should* fix that. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index c0c5d03e7..1fe4a6f99 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,7 @@ [metadata] name = bpython long_description = file: README.rst +long_description_content_type = text/x-rst license = MIT license_files = LICENSE author = Bob Farrell, Andreas Stuehrk, Sebastian Ramacher, Thomas Ballinger, et al. From d58e392f188118b3f6d3c964041492dbb2cd3694 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sun, 9 Jul 2023 16:52:27 +0200 Subject: [PATCH 504/555] Fix __signature__ support if object has a __file__ --- bpython/inspection.py | 10 +++++++--- bpython/test/test_repl.py | 19 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index fe1e3a0a2..2b734cdfb 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -289,9 +289,13 @@ def getfuncprops(func: str, f: Callable) -> Optional[FuncProps]: return None try: argspec = _get_argspec_from_signature(f) - fprops = FuncProps( - func, _fix_default_values(f, argspec), is_bound_method - ) + try: + argspec = _fix_default_values(f, argspec) + except KeyError as ex: + # Parsing of the source failed. If f has a __signature__, we trust it. + if not hasattr(f, "__signature__"): + raise ex + fprops = FuncProps(func, argspec, is_bound_method) except (TypeError, KeyError, ValueError): argspec_pydoc = _getpydocspec(f) if argspec_pydoc is None: diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 74f4b7217..8c3b85cc8 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -1,5 +1,6 @@ import collections import inspect +import os import socket import sys import tempfile @@ -523,13 +524,19 @@ def __init__(self, *args, **kwargs): inspect.Parameter("pinetree", inspect.Parameter.KEYWORD_ONLY), ]) """ - for line in code.split("\n"): - print(line[8:]) - self.repl.push(line[8:]) + code = [x[8:] for x in code.split("\n")] + for line in code: + self.repl.push(line) - self.assertTrue(self.repl.complete()) - self.assertTrue(hasattr(self.repl.matches_iter, "matches")) - self.assertEqual(self.repl.matches_iter.matches, ["apple2=", "apple="]) + with mock.patch( + "bpython.inspection.inspect.getsourcelines", + return_value=(code, None), + ): + self.assertTrue(self.repl.complete()) + self.assertTrue(hasattr(self.repl.matches_iter, "matches")) + self.assertEqual( + self.repl.matches_iter.matches, ["apple2=", "apple="] + ) if __name__ == "__main__": From 7bf93f510752e36cb72fdfd17d7db46b48e438b9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 20 Jul 2023 22:44:42 +0200 Subject: [PATCH 505/555] Fix handling of SystemExit arguments (fixes #995) --- bpython/curtsiesfrontend/coderunner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/curtsiesfrontend/coderunner.py b/bpython/curtsiesfrontend/coderunner.py index cddf1169d..f059fab88 100644 --- a/bpython/curtsiesfrontend/coderunner.py +++ b/bpython/curtsiesfrontend/coderunner.py @@ -52,7 +52,7 @@ class Unfinished(RequestFromCodeRunner): class SystemExitRequest(RequestFromCodeRunner): """Running code raised a SystemExit""" - def __init__(self, args): + def __init__(self, *args): self.args = args From db6c9bdf6d9a1d128f04ffd053591b2e1289b08a Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 22 Jul 2023 16:48:00 +0200 Subject: [PATCH 506/555] Complete parameters without input --- bpython/autocomplete.py | 7 ++++++- bpython/test/test_autocomplete.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index e0849c6d2..10f039d22 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -604,7 +604,12 @@ def matches( return matches if matches else None def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: - return lineparts.current_word(cursor_offset, line) + 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 + # param. Return an empty word + return lineparts.LinePart(r.stop, r.stop, "") + return r class ExpressionAttributeCompletion(AttrCompletion): diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 0000b0b60..2bbd90b33 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -435,3 +435,7 @@ def func(apple, apricot, banana, carrot): self.assertSetEqual( com.matches(3, "car", funcprops=funcspec), {"carrot="} ) + self.assertSetEqual( + com.matches(5, "func(", funcprops=funcspec), + {"apple=", "apricot=", "banana=", "carrot="}, + ) From de333118a7dc89925c7e3d1f246271b814a4e37c Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Sat, 22 Jul 2023 16:49:55 +0200 Subject: [PATCH 507/555] Better completion results order --- bpython/autocomplete.py | 14 +++++++++++++- bpython/test/test_autocomplete.py | 9 +++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 10f039d22..737599923 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -747,6 +747,16 @@ def get_completer( double underscore methods like __len__ in method signatures """ + def _cmpl_sort(x: str) -> Tuple[Any, ...]: + """ + Function used to sort the matches. + """ + # put parameters above everything in completion + return ( + x[-1] != "=", + x, + ) + for completer in completers: try: matches = completer.matches( @@ -765,7 +775,9 @@ def get_completer( ) continue if matches is not None: - return sorted(matches), (completer if matches else None) + return sorted(matches, key=_cmpl_sort), ( + completer if matches else None + ) return [], None diff --git a/bpython/test/test_autocomplete.py b/bpython/test/test_autocomplete.py index 2bbd90b33..da32fbb8c 100644 --- a/bpython/test/test_autocomplete.py +++ b/bpython/test/test_autocomplete.py @@ -106,6 +106,15 @@ def test_two_completers_get_both(self): cumulative = autocomplete.CumulativeCompleter([a, b]) self.assertEqual(cumulative.matches(3, "abc"), {"a", "b"}) + def test_order_completer(self): + a = self.completer(["ax", "ab="]) + b = self.completer(["aa"]) + cumulative = autocomplete.CumulativeCompleter([a, b]) + self.assertEqual( + autocomplete.get_completer([cumulative], 1, "a"), + (["ab=", "aa", "ax"], cumulative), + ) + class TestFilenameCompletion(unittest.TestCase): def setUp(self): From 77bed91c9ed6b242f1efeb68047ffb43172133d3 Mon Sep 17 00:00:00 2001 From: gpotter2 <10530980+gpotter2@users.noreply.github.com> Date: Wed, 26 Jul 2023 09:31:46 +0200 Subject: [PATCH 508/555] Apply suggestion --- bpython/autocomplete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 737599923..a36c7beb4 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -747,7 +747,7 @@ def get_completer( double underscore methods like __len__ in method signatures """ - def _cmpl_sort(x: str) -> Tuple[Any, ...]: + def _cmpl_sort(x: str) -> Tuple[bool, ...]: """ Function used to sort the matches. """ From be21521902cda73cde331480b99c5709d757a9ae Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Thu, 13 Jul 2023 14:17:34 +0200 Subject: [PATCH 509/555] Fix argument description --- bpython/args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/args.py b/bpython/args.py index b9e68e1d8..d1037e1ca 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -131,7 +131,7 @@ def callback(group): "--quiet", "-q", action="store_true", - help=_("Don't flush the output to stdout."), + help=_("Don't print version banner."), ) parser.add_argument( "--version", From 1db0436babf5c964a954f875e481bc9dc686d104 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 26 Jul 2023 09:46:46 +0200 Subject: [PATCH 510/555] Fix type annotation --- bpython/autocomplete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index a36c7beb4..000fbde9f 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -747,7 +747,7 @@ def get_completer( double underscore methods like __len__ in method signatures """ - def _cmpl_sort(x: str) -> Tuple[bool, ...]: + def _cmpl_sort(x: str) -> Tuple[bool, str]: """ Function used to sort the matches. """ From 590507b57f269ae33cfaadc2cafa1685f0c538e7 Mon Sep 17 00:00:00 2001 From: Mikolaj Klikowicz Date: Fri, 11 Aug 2023 13:02:07 +0200 Subject: [PATCH 511/555] Handle AttributeError Signed-off-by: Mikolaj Klikowicz --- bpython/inspection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/inspection.py b/bpython/inspection.py index 2b734cdfb..e97a272bc 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -104,13 +104,13 @@ def __enter__(self) -> None: if __getattr__ is not None: try: setattr(type_, "__getattr__", (lambda *_, **__: None)) - except TypeError: + except (TypeError, AttributeError): __getattr__ = None __getattribute__ = getattr(type_, "__getattribute__", None) if __getattribute__ is not None: try: setattr(type_, "__getattribute__", object.__getattribute__) - except TypeError: + except (TypeError, AttributeError): # XXX: This happens for e.g. built-in types __getattribute__ = None self._attribs = (__getattribute__, __getattr__) From f64677ffd9ed0446506c84e6d76e9d7272cc06ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 04:47:50 +0000 Subject: [PATCH 512/555] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [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/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout 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 9254d49cd..a55ee36c3 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -21,7 +21,7 @@ jobs: - "3.11" - "pypy-3.8" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index f644f5434..d02dd2dfb 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@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 - name: Install dependencies @@ -21,7 +21,7 @@ jobs: codespell: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: codespell-project/actions-codespell@master with: skip: '*.po' @@ -30,7 +30,7 @@ jobs: mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 - name: Install dependencies From 7c9e8513c01b7ea77b44d5c5a8e94dcf15ccb8a7 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 4 Oct 2023 10:45:08 +0200 Subject: [PATCH 513/555] CI: test with Python 3.12 --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a55ee36c3..ffb1c2ae0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -19,6 +19,7 @@ jobs: - "3.9" - "3.10" - "3.11" + - "3.12" - "pypy-3.8" steps: - uses: actions/checkout@v4 From cafa87c947d6e25dadd61dfa1175df953bf5445d Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 11 Oct 2023 18:25:48 +0200 Subject: [PATCH 514/555] Do not fail if modules don't have __version__ (fixes #1001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … even if they should. --- bpython/args.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index d1037e1ca..51cc3d25c 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -37,6 +37,7 @@ import sys from pathlib import Path from typing import Tuple, List, Optional, NoReturn, Callable +from types import ModuleType from . import __version__, __copyright__ from .config import default_config_path, Config @@ -67,6 +68,13 @@ def copyright_banner() -> str: return _("{} See AUTHORS.rst for details.").format(__copyright__) +def log_version(module: ModuleType, name: str) -> None: + try: + logger.info("%s: %s", name, module.__version__) # type: ignore + except AttributeError: + logger.info("%s: unknown version", name) + + Options = Tuple[str, str, Callable[[argparse._ArgumentGroup], None]] @@ -211,27 +219,27 @@ def callback(group): try: import curtsies - logger.info("curtsies: %s", curtsies.__version__) + log_version(curtsies, "curtsies") except ImportError: # may happen on Windows logger.info("curtsies: not available") - logger.info("cwcwidth: %s", cwcwidth.__version__) - logger.info("greenlet: %s", greenlet.__version__) - logger.info("pygments: %s", pygments.__version__) # type: ignore - logger.info("pyxdg: %s", xdg.__version__) # type: ignore - logger.info("requests: %s", requests.__version__) + log_version(cwcwidth, "cwcwidth") + log_version(greenlet, "greenlet") + log_version(pygments, "pygments") + log_version(xdg, "pyxdg") + log_version(requests, "requests") # versions of optional dependencies try: import pyperclip - logger.info("pyperclip: %s", pyperclip.__version__) # type: ignore + log_version(pyperclip, "pyperclip") except ImportError: logger.info("pyperclip: not available") try: import jedi - logger.info("jedi: %s", jedi.__version__) + log_version(jedi, "jedi") except ImportError: logger.info("jedi: not available") try: From 3a440274be26338e873e3e6144e9e20116ef1d4a Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Wed, 11 Oct 2023 18:42:18 +0200 Subject: [PATCH 515/555] Avoid the use of Exceptions for logic --- bpython/args.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 51cc3d25c..ed0b0055b 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -69,10 +69,7 @@ def copyright_banner() -> str: def log_version(module: ModuleType, name: str) -> None: - try: - logger.info("%s: %s", name, module.__version__) # type: ignore - except AttributeError: - logger.info("%s: unknown version", name) + logger.info("%s: %s", name, module.__version__ if hasattr(module, "__version__") else "unknown version") # type: ignore Options = Tuple[str, str, Callable[[argparse._ArgumentGroup], None]] From 3fda3af0df1a92c5794c6b396501c97647853222 Mon Sep 17 00:00:00 2001 From: Joan Lucas Date: Mon, 27 Nov 2023 20:59:26 -0300 Subject: [PATCH 516/555] Added functions return typing in some files: __init__.py; keys.py and lazyre.py --- bpython/__init__.py | 3 ++- bpython/keys.py | 4 ++-- bpython/lazyre.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/bpython/__init__.py b/bpython/__init__.py index dff06c0fa..8c7bfb37a 100644 --- a/bpython/__init__.py +++ b/bpython/__init__.py @@ -21,6 +21,7 @@ # THE SOFTWARE. import os.path +from typing import Any try: from ._version import __version__ as version # type: ignore @@ -36,7 +37,7 @@ package_dir = os.path.abspath(os.path.dirname(__file__)) -def embed(locals_=None, args=None, banner=None): +def embed(locals_=None, args=None, banner=None) -> Any: if args is None: args = ["-i", "-q"] diff --git a/bpython/keys.py b/bpython/keys.py index cfcac86be..fe27dbcc4 100644 --- a/bpython/keys.py +++ b/bpython/keys.py @@ -42,10 +42,10 @@ def __getitem__(self, key: str) -> T: f"Configured keymap ({key}) does not exist in bpython.keys" ) - def __delitem__(self, key: str): + def __delitem__(self, key: str) -> None: del self.map[key] - def __setitem__(self, key: str, value: T): + def __setitem__(self, key: str, value: T) -> None: self.map[key] = value diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 0ca5b9ffa..8d166b74f 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -21,7 +21,7 @@ # THE SOFTWARE. import re -from typing import Optional, Pattern, Match, Optional +from typing import Optional, Pattern, Match, Optional, Iterator try: from functools import cached_property @@ -43,7 +43,7 @@ def __init__(self, regex: str, flags: int = 0) -> None: def compiled(self) -> Pattern[str]: return re.compile(self.regex, self.flags) - def finditer(self, *args, **kwargs): + def finditer(self, *args, **kwargs) -> Iterator[Match[str]]: return self.compiled.finditer(*args, **kwargs) def search(self, *args, **kwargs) -> Optional[Match[str]]: From f200dac951cee8c8d0f15abd2570eb13e58f729b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 07:38:27 +0000 Subject: [PATCH 517/555] Bump actions/setup-python from 4 to 5 (#1004) --- .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 ffb1c2ae0..864c03cc8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -26,7 +26,7 @@ jobs: with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d02dd2dfb..d960c6d89 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 - name: Install dependencies run: | python -m pip install --upgrade pip From ca13bf5727d91d3208e84297c2829aa8474b4620 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Feb 2024 08:56:25 +0100 Subject: [PATCH 518/555] Bump codecov/codecov-action from 3 to 4 (#1006) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. - [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/v3...v4) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .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 864c03cc8..37128f249 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -46,7 +46,7 @@ jobs: run: | pytest --cov=bpython --cov-report=xml -v - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 env: PYTHON_VERSION: ${{ matrix.python-version }} with: From 0f238b7590037c85d7b19059ee8b41cd1d1f08f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Gr=C3=A4nitz?= Date: Mon, 29 Apr 2024 13:30:34 +0200 Subject: [PATCH 519/555] Fix simplerepl demo: missing arg for BaseRepl init (#1017) Default value was dropped in 8d16a71ef404db66d2c6fae6c362640da8ae240d --- doc/sphinx/source/simplerepl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/source/simplerepl.py b/doc/sphinx/source/simplerepl.py index 8a8dda74e..8496f0dd6 100644 --- a/doc/sphinx/source/simplerepl.py +++ b/doc/sphinx/source/simplerepl.py @@ -42,7 +42,7 @@ class SimpleRepl(BaseRepl): def __init__(self, config): self.requested_events = [] - BaseRepl.__init__(self, config) + BaseRepl.__init__(self, config, window=None) def _request_refresh(self): self.requested_events.append(bpythonevents.RefreshRequestEvent()) From 925b733e5e456daf0516f3ff9ac3877e689fd436 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 25 Apr 2024 13:57:35 +0100 Subject: [PATCH 520/555] Import build from setuptools --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7ca279d35..021940883 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup -from distutils.command.build import build +from setuptools.command.build import build try: from babel.messages import frontend as babel From ded2d7fe3fca3df64de1938c1dcf7638ec900227 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 25 Apr 2024 13:58:16 +0100 Subject: [PATCH 521/555] Avoid patching the original build class attribute --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 021940883..0cceb9406 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup -from setuptools.command.build import build +from setuptools.command.build import build as _orig_build try: from babel.messages import frontend as babel @@ -122,6 +122,11 @@ def git_describe_to_python_version(version): vf.write(f'__version__ = "{version}"\n') +class build(_orig_build): + # Avoid patching the original class' attribute (more robust customisation) + sub_commands = _orig_build.sub_commands[:] + + cmdclass = {"build": build} translations_dir = os.path.join("bpython", "translations") From ac7c11ad850a8ca649a48f26eef0b2c59f204f3e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 25 Apr 2024 14:13:23 +0100 Subject: [PATCH 522/555] Bump setuptools version Reliably import `setuptools.command.build`. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7bd3196a..924722b0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] requires = [ - "setuptools >= 43", + "setuptools >= 62.4.0", ] build-backend = "setuptools.build_meta" From a9b1324ad535774727545896ec54515e527423ab Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 30 Apr 2024 14:49:13 +0100 Subject: [PATCH 523/555] Bump setuptools in requirement.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4c750a694..cc8fbff84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ cwcwidth greenlet pyxdg requests -setuptools \ No newline at end of file +setuptools>=62.4.0 From d54061317d767c64eb2d466fa14ca56bef5bb9eb Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 21:47:54 +0200 Subject: [PATCH 524/555] Apply black --- bpython/curtsies.py | 15 ++++++--------- bpython/curtsiesfrontend/repl.py | 8 +++++--- bpython/curtsiesfrontend/replpainter.py | 8 +++++--- bpython/paste.py | 3 +-- bpython/test/test_preprocess.py | 4 ++-- bpython/urwid.py | 1 - 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 6dc8d1f78..11b960503 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -40,14 +40,11 @@ class SupportsEventGeneration(Protocol): def send( self, timeout: Optional[float] - ) -> Union[str, curtsies.events.Event, None]: - ... + ) -> Union[str, curtsies.events.Event, None]: ... - def __iter__(self) -> "SupportsEventGeneration": - ... + def __iter__(self) -> "SupportsEventGeneration": ... - def __next__(self) -> Union[str, curtsies.events.Event, None]: - ... + def __next__(self) -> Union[str, curtsies.events.Event, None]: ... class FullCurtsiesRepl(BaseRepl): @@ -69,9 +66,9 @@ def __init__( extra_bytes_callback=self.input_generator.unget_bytes, ) - self._request_refresh_callback: Callable[ - [], None - ] = self.input_generator.event_trigger(events.RefreshRequestEvent) + self._request_refresh_callback: Callable[[], None] = ( + self.input_generator.event_trigger(events.RefreshRequestEvent) + ) self._schedule_refresh_callback = ( self.input_generator.scheduled_event_trigger( events.ScheduledRefreshRequestEvent diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index e4819e193..302e67d4c 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1801,9 +1801,11 @@ def move_screen_up(current_line_start_row): self.current_match, self.docstring, self.config, - self.matches_iter.completer.format - if self.matches_iter.completer - else None, + ( + self.matches_iter.completer.format + if self.matches_iter.completer + else None + ), ) if ( diff --git a/bpython/curtsiesfrontend/replpainter.py b/bpython/curtsiesfrontend/replpainter.py index 00675451d..3b63ca4c9 100644 --- a/bpython/curtsiesfrontend/replpainter.py +++ b/bpython/curtsiesfrontend/replpainter.py @@ -74,9 +74,11 @@ def matches_lines(rows, columns, matches, current, config, match_format): result = [ fmtstr(" ").join( - color(m.ljust(max_match_width)) - if m != current - else highlight_color(m.ljust(max_match_width)) + ( + color(m.ljust(max_match_width)) + if m != current + else highlight_color(m.ljust(max_match_width)) + ) for m in matches[i : i + words_wide] ) for i in range(0, len(matches), words_wide) diff --git a/bpython/paste.py b/bpython/paste.py index fd140a0ec..a81c0c6c9 100644 --- a/bpython/paste.py +++ b/bpython/paste.py @@ -37,8 +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: diff --git a/bpython/test/test_preprocess.py b/bpython/test/test_preprocess.py index ee3f20857..e9309f1e1 100644 --- a/bpython/test/test_preprocess.py +++ b/bpython/test/test_preprocess.py @@ -16,10 +16,10 @@ def get_fodder_source(test_name): pattern = rf"#StartTest-{test_name}\n(.*?)#EndTest" - orig, xformed = [ + orig, xformed = ( re.search(pattern, inspect.getsource(module), re.DOTALL) for module in [original, processed] - ] + ) if not orig: raise ValueError( diff --git a/bpython/urwid.py b/bpython/urwid.py index 4b41c12aa..9b061340f 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -99,7 +99,6 @@ def buildProtocol(self, addr): 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 From ff05288d819fb40a0a2606010abcc6c9d29d6687 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 23:17:19 +0200 Subject: [PATCH 525/555] Import BuildDoc from sphinx (fixes #987) --- LICENSE | 28 ++++++++ setup.py | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 229 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 72d02ff63..46f642f27 100644 --- a/LICENSE +++ b/LICENSE @@ -72,3 +72,31 @@ products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. + + +BuildDoc in setup.py is licensed under the BSD-2 license: + +Copyright 2007-2021 Sebastian Wiesner + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/setup.py b/setup.py index 0cceb9406..e158f1a0d 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ import re import subprocess -from setuptools import setup +from setuptools import setup, Command from setuptools.command.build import build as _orig_build try: @@ -17,14 +17,212 @@ try: import sphinx - from sphinx.setup_command import BuildDoc # Sphinx 1.5 and newer support Python 3.6 - using_sphinx = sphinx.__version__ >= "1.5" and sphinx.__version__ < "7.0" + using_sphinx = sphinx.__version__ >= "1.5" except ImportError: using_sphinx = False +if using_sphinx: + import sys + from io import StringIO + + from setuptools.errors import ExecError + from sphinx.application import Sphinx + from sphinx.cmd.build import handle_exception + from sphinx.util.console import color_terminal, nocolor + from sphinx.util.docutils import docutils_namespace, patch_docutils + from sphinx.util.osutil import abspath + + class BuildDoc(Command): + """ + Distutils command to build Sphinx documentation. + The Sphinx build can then be triggered from distutils, and some Sphinx + options can be set in ``setup.py`` or ``setup.cfg`` instead of Sphinx's + own configuration file. + For instance, from `setup.py`:: + # this is only necessary when not using setuptools/distribute + from sphinx.setup_command import BuildDoc + cmdclass = {'build_sphinx': BuildDoc} + name = 'My project' + version = '1.2' + release = '1.2.0' + setup( + name=name, + author='Bernard Montgomery', + version=release, + cmdclass=cmdclass, + # these are optional and override conf.py settings + command_options={ + 'build_sphinx': { + 'project': ('setup.py', name), + 'version': ('setup.py', version), + 'release': ('setup.py', release)}}, + ) + Or add this section in ``setup.cfg``:: + [build_sphinx] + project = 'My project' + version = 1.2 + release = 1.2.0 + """ + + description = "Build Sphinx documentation" + user_options = [ + ("fresh-env", "E", "discard saved environment"), + ("all-files", "a", "build all files"), + ("source-dir=", "s", "Source directory"), + ("build-dir=", None, "Build directory"), + ("config-dir=", "c", "Location of the configuration directory"), + ( + "builder=", + "b", + "The builder (or builders) to use. Can be a comma- " + 'or space-separated list. Defaults to "html"', + ), + ("warning-is-error", "W", "Turn warning into errors"), + ("project=", None, "The documented project's name"), + ("version=", None, "The short X.Y version"), + ( + "release=", + None, + "The full version, including alpha/beta/rc tags", + ), + ( + "today=", + None, + "How to format the current date, used as the " + "replacement for |today|", + ), + ("link-index", "i", "Link index.html to the master doc"), + ("copyright", None, "The copyright string"), + ("pdb", None, "Start pdb on exception"), + ("verbosity", "v", "increase verbosity (can be repeated)"), + ( + "nitpicky", + "n", + "nit-picky mode, warn about all missing references", + ), + ("keep-going", None, "With -W, keep going when getting warnings"), + ] + boolean_options = [ + "fresh-env", + "all-files", + "warning-is-error", + "link-index", + "nitpicky", + ] + + def initialize_options(self) -> None: + self.fresh_env = self.all_files = False + self.pdb = False + self.source_dir: str = None + self.build_dir: str = None + self.builder = "html" + self.warning_is_error = False + self.project = "" + self.version = "" + self.release = "" + self.today = "" + self.config_dir: str = None + self.link_index = False + self.copyright = "" + # Link verbosity to distutils' (which uses 1 by default). + self.verbosity = self.distribution.verbose - 1 # type: ignore + self.traceback = False + self.nitpicky = False + self.keep_going = False + + def _guess_source_dir(self) -> str: + for guess in ("doc", "docs"): + if not os.path.isdir(guess): + continue + for root, dirnames, filenames in os.walk(guess): + if "conf.py" in filenames: + return root + return os.curdir + + def finalize_options(self) -> None: + self.ensure_string_list("builder") + + if self.source_dir is None: + self.source_dir = self._guess_source_dir() + self.announce("Using source directory %s" % self.source_dir) + + self.ensure_dirname("source_dir") + + if self.config_dir is None: + self.config_dir = self.source_dir + + if self.build_dir is None: + build = self.get_finalized_command("build") + self.build_dir = os.path.join(abspath(build.build_base), "sphinx") # type: ignore + + self.doctree_dir = os.path.join(self.build_dir, "doctrees") + + self.builder_target_dirs = [ + (builder, os.path.join(self.build_dir, builder)) + for builder in self.builder + ] + + def run(self) -> None: + if not color_terminal(): + nocolor() + if not self.verbose: # type: ignore + status_stream = StringIO() + else: + status_stream = sys.stdout # type: ignore + confoverrides = {} + if self.project: + confoverrides["project"] = self.project + if self.version: + confoverrides["version"] = self.version + if self.release: + confoverrides["release"] = self.release + if self.today: + confoverrides["today"] = self.today + if self.copyright: + confoverrides["copyright"] = self.copyright + if self.nitpicky: + confoverrides["nitpicky"] = self.nitpicky + + for builder, builder_target_dir in self.builder_target_dirs: + app = None + + try: + confdir = self.config_dir or self.source_dir + with patch_docutils(confdir), docutils_namespace(): + app = Sphinx( + self.source_dir, + self.config_dir, + builder_target_dir, + self.doctree_dir, + builder, + confoverrides, + status_stream, + freshenv=self.fresh_env, + warningiserror=self.warning_is_error, + verbosity=self.verbosity, + keep_going=self.keep_going, + ) + app.build(force_all=self.all_files) + if app.statuscode: + raise ExecError( + "caused by %s builder." % app.builder.name + ) + except Exception as exc: + handle_exception(app, self, exc, sys.stderr) + if not self.pdb: + raise SystemExit(1) from exc + + if not self.link_index: + continue + + src = app.config.root_doc + app.builder.out_suffix # type: ignore + dst = app.builder.get_outfilename("index") # type: ignore + os.symlink(src, dst) + + # version handling From 1f2f6f5b04b52ea939d6bd64e2e8eee83ae917f4 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 23:17:39 +0200 Subject: [PATCH 526/555] Refactor build command overrides --- setup.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index e158f1a0d..9e24203fd 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ import subprocess from setuptools import setup, Command -from setuptools.command.build import build as _orig_build +from setuptools.command.build import build try: from babel.messages import frontend as babel @@ -320,26 +320,27 @@ def git_describe_to_python_version(version): vf.write(f'__version__ = "{version}"\n') -class build(_orig_build): - # Avoid patching the original class' attribute (more robust customisation) - sub_commands = _orig_build.sub_commands[:] +class custom_build(build): + def run(self): + if using_translations: + self.run_command("compile_catalog") + if using_sphinx: + self.run_command("build_sphinx_man") -cmdclass = {"build": build} +cmdclass = {"build": custom_build} + translations_dir = os.path.join("bpython", "translations") # localization options if using_translations: - build.sub_commands.insert(0, ("compile_catalog", None)) - cmdclass["compile_catalog"] = babel.compile_catalog cmdclass["extract_messages"] = babel.extract_messages cmdclass["update_catalog"] = babel.update_catalog cmdclass["init_catalog"] = babel.init_catalog if using_sphinx: - build.sub_commands.insert(0, ("build_sphinx_man", None)) cmdclass["build_sphinx_man"] = BuildDoc if platform.system() in ("FreeBSD", "OpenBSD"): @@ -378,6 +379,7 @@ class build(_orig_build): if os.path.exists(os.path.join(translations_dir, mo_subpath)): mo_files.append(mo_subpath) + setup( version=version, data_files=data_files, @@ -388,6 +390,7 @@ class build(_orig_build): }, cmdclass=cmdclass, test_suite="bpython.test", + zip_safe=False, ) # vim: fileencoding=utf-8 sw=4 ts=4 sts=4 ai et sta From c085f9cc519494970e09f4a77aaeadf0bff77f87 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 23:20:39 +0200 Subject: [PATCH 527/555] CI: allow sphinx >= 7 --- .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 37128f249..de2f98ccc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -33,7 +33,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,<7" + 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: | From 7238851ca65ec0381928c1ef8ef0bde475bf6a50 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 1 Jun 2024 23:24:07 +0200 Subject: [PATCH 528/555] Also register BuildDoc for build_sphinx --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 9e24203fd..de10eaf44 100755 --- a/setup.py +++ b/setup.py @@ -341,6 +341,7 @@ def run(self): cmdclass["init_catalog"] = babel.init_catalog if using_sphinx: + cmdclass["build_sphinx"] = BuildDoc cmdclass["build_sphinx_man"] = BuildDoc if platform.system() in ("FreeBSD", "OpenBSD"): From 23294503c59088c5ea9c3d811d073d14272f9ff0 Mon Sep 17 00:00:00 2001 From: suman Date: Sat, 1 Jun 2024 23:43:24 +0545 Subject: [PATCH 529/555] Replace NoReturn with Never --- bpython/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index ed0b0055b..e8e882a22 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -36,7 +36,7 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional, NoReturn, Callable +from typing import Tuple, List, Optional, Never, Callable from types import ModuleType from . import __version__, __copyright__ @@ -51,7 +51,7 @@ class ArgumentParserFailed(ValueError): class RaisingArgumentParser(argparse.ArgumentParser): - def error(self, msg: str) -> NoReturn: + def error(self, msg: str) -> Never: raise ArgumentParserFailed() From f8aeaaf8ef8b513473267db99dfe845d0e4b1a41 Mon Sep 17 00:00:00 2001 From: suman Date: Sat, 1 Jun 2024 23:43:46 +0545 Subject: [PATCH 530/555] Add type annotations --- bpdb/debugger.py | 8 ++++---- bpython/args.py | 4 ++-- bpython/urwid.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bpdb/debugger.py b/bpdb/debugger.py index 3e5bbc91b..38469541a 100644 --- a/bpdb/debugger.py +++ b/bpdb/debugger.py @@ -27,24 +27,24 @@ class BPdb(pdb.Pdb): """PDB with BPython support.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.prompt = "(BPdb) " self.intro = 'Use "B" to enter bpython, Ctrl-d to exit it.' - def postloop(self): + def postloop(self) -> None: # We only want to show the intro message once. self.intro = None super().postloop() # cmd.Cmd commands - def do_Bpython(self, arg): + def do_Bpython(self, arg: str) -> None: locals_ = self.curframe.f_globals.copy() locals_.update(self.curframe.f_locals) bpython.embed(locals_, ["-i"]) - def help_Bpython(self): + def help_Bpython(self) -> None: print("B(python)") print("") print( diff --git a/bpython/args.py b/bpython/args.py index e8e882a22..ed0b0055b 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -36,7 +36,7 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional, Never, Callable +from typing import Tuple, List, Optional, NoReturn, Callable from types import ModuleType from . import __version__, __copyright__ @@ -51,7 +51,7 @@ class ArgumentParserFailed(ValueError): class RaisingArgumentParser(argparse.ArgumentParser): - def error(self, msg: str) -> Never: + def error(self, msg: str) -> NoReturn: raise ArgumentParserFailed() diff --git a/bpython/urwid.py b/bpython/urwid.py index 9b061340f..3c075d937 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -76,10 +76,10 @@ class EvalProtocol(basic.LineOnlyReceiver): delimiter = "\n" - def __init__(self, myrepl): + def __init__(self, myrepl) -> None: self.repl = myrepl - def lineReceived(self, line): + def lineReceived(self, line) -> None: # HACK! # TODO: deal with encoding issues here... self.repl.main_loop.process_input(line) From a12d339e1a0bdca726d439ed1231f3f2ca993eac Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sun, 2 Jun 2024 13:04:12 +0200 Subject: [PATCH 531/555] Use Never for Python 3.11 and newer --- bpython/_typing_compat.py | 28 ++++++++++++++++++++++++++++ bpython/args.py | 5 +++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 bpython/_typing_compat.py diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py new file mode 100644 index 000000000..83567b4f7 --- /dev/null +++ b/bpython/_typing_compat.py @@ -0,0 +1,28 @@ +# 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 import NoReturn as Never # type: ignore + diff --git a/bpython/args.py b/bpython/args.py index ed0b0055b..55691a2a0 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -36,12 +36,13 @@ import os import sys from pathlib import Path -from typing import Tuple, List, Optional, NoReturn, Callable +from typing import Tuple, List, Optional, Callable from types import ModuleType from . import __version__, __copyright__ from .config import default_config_path, Config from .translations import _ +from ._typing_compat import Never logger = logging.getLogger(__name__) @@ -51,7 +52,7 @@ class ArgumentParserFailed(ValueError): class RaisingArgumentParser(argparse.ArgumentParser): - def error(self, msg: str) -> NoReturn: + def error(self, msg: str) -> Never: raise ArgumentParserFailed() From c152cbf8485695bb1835aef5b58f7fc275f03307 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Jul 2024 17:31:27 +0200 Subject: [PATCH 532/555] Update changelog for 0.25 --- CHANGELOG.rst | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d7ecb3ab7..67d56f883 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,18 +6,29 @@ Changelog General information: -* The bpython-cli rendering backend has been removed following deprecation in +* The `bpython-cli` rendering backend has been removed following deprecation in version 0.19. - +* This release is focused on Python 3.12 support. New features: Fixes: +* Fix __signature__ support + Thanks to gpotter2 +* #995: Fix handling of `SystemExit` +* #996: Improve order of completion results + Thanks to gpotter2 +* Fix build of documentation and manpages with Sphinx >= 7 +* #1001: Do not fail if modules don't have __version__ 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. 0.24 ---- @@ -37,7 +48,7 @@ Fixes: Changes to dependencies: -* wheel is no required as part of pyproject.toml's build dependencies +* wheel is not required as part of pyproject.toml's build dependencies Support for Python 3.11 has been added. From ce710bbdb48be7ec8325d01ee156ba666e258c63 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Jul 2024 17:57:18 +0200 Subject: [PATCH 533/555] Format with black --- bpython/_typing_compat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bpython/_typing_compat.py b/bpython/_typing_compat.py index 83567b4f7..486aacafc 100644 --- a/bpython/_typing_compat.py +++ b/bpython/_typing_compat.py @@ -25,4 +25,3 @@ from typing import Never except ImportError: from typing import NoReturn as Never # type: ignore - From b6318376b255be645b16aaf27c6475359e384ab9 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Sat, 13 Jul 2024 21:27:49 +0200 Subject: [PATCH 534/555] Update copyright year --- bpython/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/__init__.py b/bpython/__init__.py index 8c7bfb37a..26fa3e63d 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-2023 {__author__}" +__copyright__ = f"(C) 2008-2024 {__author__}" __license__ = "MIT" __version__ = version package_dir = os.path.abspath(os.path.dirname(__file__)) From f0c023071a8f21b2007389ffcbe57399f35f0cac Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 25 Oct 2024 11:28:44 +0200 Subject: [PATCH 535/555] CI: test with Python 3.13 --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index de2f98ccc..747d55a44 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -20,6 +20,7 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" - "pypy-3.8" steps: - uses: actions/checkout@v4 From bbdff64fe37b851b6a33183a6a35739bbb4687a0 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 25 Oct 2024 11:45:34 +0200 Subject: [PATCH 536/555] Accept source in showsyntaxerror --- bpython/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpython/repl.py b/bpython/repl.py index f30cfa31d..b048314d6 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -152,7 +152,7 @@ def runsource( with self.timer: return super().runsource(source, filename, symbol) - def showsyntaxerror(self, filename: 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 52a7a157037f1d8ef81bd0672b636b837d90bed6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 25 Oct 2024 17:42:06 +0200 Subject: [PATCH 537/555] Switch assert arguments to display correct value as expected --- bpython/test/test_interpreter.py | 4 ++-- bpython/test/test_repl.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index a4a32dd09..acad12c1d 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -130,8 +130,8 @@ def gfunc(): ) a = i.a - self.assertMultiLineEqual(str(plain("").join(a)), str(expected)) - self.assertEqual(plain("").join(a), expected) + self.assertMultiLineEqual(str(expected), str(plain("").join(a))) + self.assertEqual(expected, plain("").join(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_repl.py b/bpython/test/test_repl.py index 8c3b85cc8..3f6b7c124 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -307,7 +307,7 @@ def assert_get_source_error_for_current_function(self, func, msg): try: self.repl.get_source_of_current_name() except repl.SourceNotFound as e: - self.assertEqual(e.args[0], msg) + self.assertEqual(msg, e.args[0]) else: self.fail("Should have raised SourceNotFound") From 45f4117b534d6827279f7b9e633f3cabe0fb37e6 Mon Sep 17 00:00:00 2001 From: Sebastian Ramacher Date: Fri, 25 Oct 2024 17:42:20 +0200 Subject: [PATCH 538/555] Fix test errors with Python 3.13 --- bpython/test/test_interpreter.py | 17 ++++++++++++++++- bpython/test/test_repl.py | 11 ++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/bpython/test/test_interpreter.py b/bpython/test/test_interpreter.py index acad12c1d..b9f0a31e2 100644 --- a/bpython/test/test_interpreter.py +++ b/bpython/test/test_interpreter.py @@ -99,7 +99,22 @@ def gfunc(): global_not_found = "name 'gfunc' is not defined" - if (3, 11) <= sys.version_info[:2]: + 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('""') diff --git a/bpython/test/test_repl.py b/bpython/test/test_repl.py index 3f6b7c124..5cafec946 100644 --- a/bpython/test/test_repl.py +++ b/bpython/test/test_repl.py @@ -332,9 +332,14 @@ def test_current_function_cpython(self): self.assert_get_source_error_for_current_function( collections.defaultdict.copy, "No source code found for INPUTLINE" ) - self.assert_get_source_error_for_current_function( - collections.defaultdict, "could not find class definition" - ) + if sys.version_info[:2] >= (3, 13): + self.assert_get_source_error_for_current_function( + collections.defaultdict, "source code not available" + ) + else: + self.assert_get_source_error_for_current_function( + collections.defaultdict, "could not find class definition" + ) def test_current_line(self): self.repl.interp.locals["a"] = socket.socket 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 539/555] 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 747d55a44..7ccef9bce 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 540/555] black --- bpython/repl.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bpython/repl.py b/bpython/repl.py index b048314d6..c87d1965b 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 541/555] 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 d960c6d89..fbb5d996b 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 542/555] 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 486aacafc..5d9a36079 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 302e67d4c..69eb18c9b 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 e145e0ed8..65a3b223c 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 e9309f1e1..a72a64b63 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 1fe4a6f99..9a4f0bcbe 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 543/555] 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 0480c1b06..16a598fa3 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 69eb18c9b..01b047118 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 544/555] 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 7ccef9bce..a6e9aef0a 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 c5bba43db..3f334af4a 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 32b1ea869..3b93089df 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 fcce5c1cf..7d789f166 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 924722b0b..ca4e04508 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 9a4f0bcbe..f8b7c3258 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 545/555] Update changelog for 0.25 --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 67d56f883..114b94401 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 546/555] 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 fbb5d996b..b60561592 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 547/555] 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 c87d1965b..0374bb6bb 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 548/555] Start development of 0.26 --- CHANGELOG.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 114b94401..f55fe76fd 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 549/555] 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 55691a2a0..1eb59a691 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 000fbde9f..88afbe54f 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 5123ec226..27af87402 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 11b960503..547a853ee 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 16a598fa3..cb7b81057 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 26f105dc9..4f9c13e55 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 e70325ab5..53ae47844 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 82e28091c..6532d9688 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 88a149a65..96e91e55e 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 5e59dd499..f48a79bf7 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 01b047118..09f73a82f 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 11f575b6e..5ed8769fd 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 f216f213f..8e74ac2c2 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 13dbb5b7f..386214b44 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 9df140c64..da1b91405 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 e97a272bc..fb1124ebf 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 fe27dbcc4..1068a4f26 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 8d166b74f..d397f05cf 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 cbc3bf37e..363419fe0 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 65a3b223c..2fa4846e0 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 a81c0c6c9..e846aba3f 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 d91392d24..5bf4a45b8 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 0374bb6bb..de8890310 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 3f334af4a..893539ea7 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 5a19c6abb..59102f9e1 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 3f04222de..5089f3048 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 967ecbe01..5beb000bd 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 5cafec946..a32ef90e8 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 0cb4c01f6..7d82dc7ce 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 550/555] 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 d397f05cf..a63bb4646 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 551/555] 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 a72a64b63..8e8a36304 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 552/555] 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 f8b7c3258..b3cb9a4c7 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 553/555] 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 a6e9aef0a..cb6d64ad2 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 554/555] 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 1eb59a691..35fd3e7bf 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 88afbe54f..4fb62f720 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 547a853ee..b57e47a9d 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 cb7b81057..8c070b34c 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 53ae47844..2822db6df 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 6532d9688..280c56ede 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 96e91e55e..28b32e649 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 09f73a82f..2a304312d 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 5ed8769fd..b8eb11ff2 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 386214b44..b58309b53 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 da1b91405..570996d44 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 fb1124ebf..63f0e2d38 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 a63bb4646..1d9036164 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 363419fe0..e64b20d90 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 e846aba3f..8ca6f2df5 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 5bf4a45b8..68787e709 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 de8890310..50da2d462 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 893539ea7..1e26ded4c 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 5089f3048..c83ca0128 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 5beb000bd..017978277 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 7d82dc7ce..069f34653 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 3c075d937..d94fc2e77 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 ca4e04508..0a891d27c 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 555/555] 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 cb6d64ad2..e7852728e 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 b3cb9a4c7..7d61ee1ce 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]