From 5835bbfc443d11824583fb8ca94d55fdb5c24543 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Thu, 7 Oct 2021 00:18:57 -0700 Subject: [PATCH 01/10] Mypy types for autocomplete.py Running mypy in CI on this one file. --- .github/workflows/lint.yaml | 17 ++++++++++ bpython/args.py | 2 +- bpython/autocomplete.py | 33 ++++++++++++++----- bpython/curtsiesfrontend/filewatch.py | 2 +- bpython/lazyre.py | 8 ++--- bpython/patch_linecache.py | 2 +- bpython/repl.py | 10 ++++-- bpython/translations/__init__.py | 5 +-- setup.cfg | 9 +++++ stubs/blessings.pyi | 47 +++++++++++++++++++++++++++ stubs/cwcwidth.pyi | 4 +++ stubs/msvcrt.pyi | 7 ++++ stubs/rlcompleter.pyi | 3 ++ 13 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 stubs/blessings.pyi create mode 100644 stubs/cwcwidth.pyi create mode 100644 stubs/msvcrt.pyi create mode 100644 stubs/rlcompleter.pyi diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index adddd5d5a..593f64a98 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" + pip install types-backports types-requests types-setuptools types-toml + - name: Check with mypy + # for now only run on a few files to avoid slipping backward + run: mypy --ignore-missing-imports bpython/autocomplete.py diff --git a/bpython/args.py b/bpython/args.py index 79ddcc679..4983be3f0 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -197,7 +197,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( diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 3ec2a4578..d1612e4ee 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -33,7 +33,18 @@ import builtins from enum import Enum -from typing import Any, Dict, Iterator, List, Match, NoReturn, Set, Union, Tuple +from typing import ( + Any, + Dict, + Iterator, + List, + Match, + Set, + Union, + Tuple, + Type, + Sequence, +) from . import inspection from . import line as lineparts from .line import LinePart @@ -180,15 +191,15 @@ 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 @@ -265,16 +276,20 @@ 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, current_offset, line): for completer in self._completers: return_value = completer.locate(current_offset, line) if return_value is not None: @@ -573,7 +588,7 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: import jedi except ImportError: - class MultilineJediCompletion(BaseCompletionType): + class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] def matches(self, cursor_offset, line, **kwargs) -> None: return None @@ -630,7 +645,7 @@ def locate(self, cursor_offset: int, line: str) -> LinePart: end = cursor_offset return LinePart(start, end, line[start:end]) - class MultilineJediCompletion(JediCompletion): + class MultilineJediCompletion(JediCompletion): # type: ignore [no-redef] def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: if "current_block" not in kwargs or "history" not in kwargs: return None 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/lazyre.py b/bpython/lazyre.py index fbbdd38d8..8ca971a97 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,7 +40,7 @@ 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): @@ -49,7 +49,7 @@ def finditer(self, *args, **kwargs): def search(self, *args, **kwargs): 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..6e8743532 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 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 diff --git a/bpython/translations/__init__.py b/bpython/translations/__init__.py index c2e23f806..ff831f48b 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 from .. import package_dir -translator = None +translator: gettext.GNUTranslations = cast(gettext.GNUTranslations, None) -def _(message): +def _(message) -> str: return translator.gettext(message) diff --git a/setup.cfg b/setup.cfg index 89b6cde61..41b47a93f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,3 +65,12 @@ 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 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/cwcwidth.pyi b/stubs/cwcwidth.pyi new file mode 100644 index 000000000..8d047b628 --- /dev/null +++ b/stubs/cwcwidth.pyi @@ -0,0 +1,4 @@ +# official typing seems to require a second arg +def wcswidth(s: str, n=100000000): ... + +__version__: str 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/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): ... From 70cd0185411140203b8c40e62db2beee980cd3d8 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 8 Oct 2021 09:59:03 -0700 Subject: [PATCH 02/10] fix missing imports (mostly with stub files) --- .github/workflows/lint.yaml | 4 ++-- stubs/greenlet.pyi | 6 ++++++ stubs/pyperclip.pyi | 2 ++ stubs/watchdog/__init__.pyi | 0 stubs/watchdog/events.pyi | 1 + stubs/watchdog/observers.pyi | 5 +++++ stubs/xdg.pyi | 3 +++ 7 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 stubs/greenlet.pyi create mode 100644 stubs/pyperclip.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 593f64a98..d19996575 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -39,7 +39,7 @@ jobs: pip install mypy pip install -r requirements.txt pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" - pip install types-backports types-requests types-setuptools types-toml + 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 --ignore-missing-imports bpython/autocomplete.py + run: mypy bpython/autocomplete.py diff --git a/stubs/greenlet.pyi b/stubs/greenlet.pyi new file mode 100644 index 000000000..35bb28f71 --- /dev/null +++ b/stubs/greenlet.pyi @@ -0,0 +1,6 @@ +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/pyperclip.pyi b/stubs/pyperclip.pyi new file mode 100644 index 000000000..8edd97773 --- /dev/null +++ b/stubs/pyperclip.pyi @@ -0,0 +1,2 @@ +def copy(content: str): ... +class PyperclipException(Exception): ... 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..e72cb41d6 --- /dev/null +++ b/stubs/watchdog/observers.pyi @@ -0,0 +1,5 @@ + +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..2d5d6d755 --- /dev/null +++ b/stubs/xdg.pyi @@ -0,0 +1,3 @@ +from typing import ClassVar +class BaseDirectory: + xdg_config_home: ClassVar[str] From a8a128f67cb317743eec8c9317f5d69ac7c00ca1 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 8 Oct 2021 10:01:33 -0700 Subject: [PATCH 03/10] reformat --- stubs/greenlet.pyi | 5 ++++- stubs/pyperclip.pyi | 1 + stubs/watchdog/observers.pyi | 3 +-- stubs/xdg.pyi | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/stubs/greenlet.pyi b/stubs/greenlet.pyi index 35bb28f71..778c827ef 100644 --- a/stubs/greenlet.pyi +++ b/stubs/greenlet.pyi @@ -1,6 +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: ... + def switch(self, value: Any = None) -> Any: ... diff --git a/stubs/pyperclip.pyi b/stubs/pyperclip.pyi index 8edd97773..3968c20a6 100644 --- a/stubs/pyperclip.pyi +++ b/stubs/pyperclip.pyi @@ -1,2 +1,3 @@ def copy(content: str): ... + class PyperclipException(Exception): ... diff --git a/stubs/watchdog/observers.pyi b/stubs/watchdog/observers.pyi index e72cb41d6..7db3099fb 100644 --- a/stubs/watchdog/observers.pyi +++ b/stubs/watchdog/observers.pyi @@ -1,5 +1,4 @@ - class Observer: def start(self): ... - def schedule(self, dirname: str, recursive:bool): ... + def schedule(self, dirname: str, recursive: bool): ... def unschedule_all(self): ... diff --git a/stubs/xdg.pyi b/stubs/xdg.pyi index 2d5d6d755..db7d63e03 100644 --- a/stubs/xdg.pyi +++ b/stubs/xdg.pyi @@ -1,3 +1,4 @@ from typing import ClassVar + class BaseDirectory: xdg_config_home: ClassVar[str] From 3356afc0c8a0283a3335dc2cc76bbe8205bf4a28 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 8 Oct 2021 10:04:47 -0700 Subject: [PATCH 04/10] ignore missing _version.py --- bpython/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From 55e482ad64f1142f72d8f206829534031809bb8a Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 8 Oct 2021 10:40:10 -0700 Subject: [PATCH 05/10] disallow untyped defs in autocomplete.py --- bpython/autocomplete.py | 151 ++++++++++++++++++++++++++-------------- 1 file changed, 99 insertions(+), 52 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index d1612e4ee..c61877128 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -21,6 +21,8 @@ # 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 import __main__ import abc @@ -35,10 +37,12 @@ from enum import Enum from typing import ( Any, + cast, Dict, Iterator, List, Match, + Optional, Set, Union, Tuple, @@ -50,6 +54,7 @@ from .line import LinePart from .lazyre import LazyReCompile from .simpleeval import safe_eval, evaluate_current_expression, EvaluationError +from .importcompletion import ModuleGatherer # Autocomplete modes @@ -60,7 +65,7 @@ class AutocompleteModes(Enum): FUZZY = "fuzzy" @classmethod - def from_string(cls, value) -> Union[Any, None]: + def from_string(cls, value: str) -> Union[Any, None]: if value.upper() in cls.__members__: return cls.__members__[value.upper()] return None @@ -177,7 +182,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 @@ -203,7 +208,7 @@ 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) -> Union[Match, None]: s = r".*%s.*" % ".*".join(list(text)) return re.search(s, word) @@ -220,14 +225,16 @@ 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 + self, cursor_offset: int, line: str, **kwargs: Any ) -> Union[Set[str], None]: """Returns a list of possible matches given a line and cursor, or None if this completion type isn't applicable. @@ -255,10 +262,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 @@ -289,18 +298,18 @@ def __init__( super().__init__(True, mode) - def locate(self, current_offset, line): + def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: 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 + self, cursor_offset: int, line: str, **kwargs: Any ) -> Union[None, Set]: return_value = None all_matches = set() @@ -316,28 +325,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 + ) -> Union[None, 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) -> Union[LinePart, None]: + 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 + ) -> Union[None, Set]: cs = lineparts.current_string(cursor_offset, line) if cs is None: return None @@ -352,10 +369,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) -> Union[LinePart, None]: + 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 :] @@ -367,10 +384,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 + ) -> Union[None, 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: @@ -397,13 +416,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) -> Union[LinePart, None]: + 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) @@ -422,7 +441,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__"): @@ -442,7 +461,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): @@ -450,7 +469,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 + ) -> Union[None, Set]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -473,15 +494,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) -> Union[LinePart, None]: + 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 + ) -> Union[None, Set]: if "current_block" not in kwargs: return None current_block = kwargs["current_block"] @@ -493,12 +516,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) -> Union[LinePart, None]: + 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 + ) -> Union[None, 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. @@ -528,12 +553,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) -> Union[LinePart, None]: + 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 + ) -> Union[None, Set]: if "argspec" not in kwargs: return None argspec = kwargs["argspec"] @@ -554,16 +581,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) -> Union[LinePart, None]: + 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) -> Union[LinePart, None]: + 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 + ) -> Union[None, Set]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -589,7 +618,14 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: except ImportError: class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] - def matches(self, cursor_offset, line, **kwargs) -> None: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Union[None, Set]: + return None + + def locate( + self, cursor_offset: int, line: str + ) -> Union[LinePart, None]: return None @@ -598,7 +634,9 @@ def matches(self, cursor_offset, line, **kwargs) -> None: class JediCompletion(BaseCompletionType): _orig_start: Union[int, None] - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Union[None, Set]: if "history" not in kwargs: return None history = kwargs["history"] @@ -646,7 +684,9 @@ def locate(self, cursor_offset: int, line: str) -> LinePart: return LinePart(start, end, line[start:end]) class MultilineJediCompletion(JediCompletion): # type: ignore [no-redef] - def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Union[None, Set]: if "current_block" not in kwargs or "history" not in kwargs: return None current_block = kwargs["current_block"] @@ -663,7 +703,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 @@ -698,7 +743,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), @@ -721,7 +768,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 += "(" From 1ad2ba457f0ff01875fcf3e1275e84f73873f34f Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 8 Oct 2021 10:44:25 -0700 Subject: [PATCH 06/10] disallow untyped calls in autocomplete.py --- bpython/autocomplete.py | 1 + bpython/inspection.py | 4 ++-- bpython/simpleeval.py | 7 +++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index c61877128..c87a39c4b 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -23,6 +23,7 @@ # 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 diff --git a/bpython/inspection.py b/bpython/inspection.py index 4c0dbada4..ff02d712a 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -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/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. From b1aa26373f827fa828b29c77dcc91647c149a06c Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 8 Oct 2021 13:29:21 -0700 Subject: [PATCH 07/10] type args.py --- bpython/args.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/bpython/args.py b/bpython/args.py index 4983be3f0..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) @@ -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 From 55564ff8624ba56debc43b9b817a12dc04d84396 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 8 Oct 2021 15:01:26 -0700 Subject: [PATCH 08/10] Type curtsies.py --- .github/workflows/lint.yaml | 2 +- bpython/config.py | 4 + bpython/curtsies.py | 109 ++++++++++++++++++------ bpython/curtsiesfrontend/interaction.py | 3 +- bpython/curtsiesfrontend/interpreter.py | 5 +- bpython/curtsiesfrontend/repl.py | 75 +++++++++------- bpython/inspection.py | 2 +- bpython/lazyre.py | 2 +- bpython/repl.py | 4 +- bpython/translations/__init__.py | 6 +- 10 files changed, 141 insertions(+), 71 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d19996575..aeb11721b 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -42,4 +42,4 @@ jobs: 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 bpython/autocomplete.py + run: mypy bpython/autocomplete.py bpython/args.py bpython/config.py bpython/curtsies.py 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..9776a5863 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: Union[float, None] + ) -> 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,20 @@ 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], Union[float, None], 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 +283,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/interaction.py b/bpython/curtsiesfrontend/interaction.py index 51fd28d35..3cec9d5ea 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -1,5 +1,6 @@ import greenlet import time +from typing import Optional from curtsies import events from ..translations import _ @@ -78,7 +79,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 21b606e67..04e60ed5a 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,9 @@ import unicodedata from enum import Enum +from typing import Dict, Any, List, Optional, Tuple, Union, cast +from typing_extensions import Literal + import blessings import cwcwidth import greenlet @@ -32,6 +36,7 @@ from pygments.lexers import Python3Lexer from . import events as bpythonevents, sitefix, replpainter as paint +from ..config import Config from .coderunner import ( CodeRunner, FakeOutput, @@ -55,6 +60,8 @@ ) from ..translations import _ +InputOrOutput = Union[Literal["input"], Literal["output"]] + logger = logging.getLogger(__name__) INCONSISTENT_HISTORY_MSG = "#<---History inconsistent with output shown--->" @@ -98,7 +105,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) @@ -116,8 +123,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 ("",): @@ -308,11 +313,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 @@ -330,7 +335,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 = ( @@ -372,7 +377,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 = [] @@ -383,11 +388,11 @@ def __init__( # - 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 = [] + self.all_logical_lines: List[Tuple[str, InputOrOutput]] = [] # 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 @@ -396,7 +401,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) @@ -428,7 +433,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 @@ -446,8 +451,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) @@ -470,7 +477,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 @@ -613,7 +620,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""" @@ -623,9 +630,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) @@ -640,7 +648,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): @@ -680,12 +688,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: @@ -697,8 +704,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: @@ -753,7 +761,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]: @@ -1429,7 +1437,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 @@ -1587,11 +1595,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( @@ -1817,7 +1826,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): @@ -1932,7 +1941,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 ff02d712a..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): diff --git a/bpython/lazyre.py b/bpython/lazyre.py index 8ca971a97..8f1e70995 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -46,7 +46,7 @@ def compiled(self) -> Pattern[str]: 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) -> Optional[Match[str]]: diff --git a/bpython/repl.py b/bpython/repl.py index 6e8743532..e261e61ad 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -39,7 +39,7 @@ from pygments.lexers import Python3Lexer from pygments.token import Token from types import ModuleType -from typing import cast +from typing import cast, Tuple, Any have_pyperclip = True try: @@ -1220,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/translations/__init__.py b/bpython/translations/__init__.py index ff831f48b..13c498025 100644 --- a/bpython/translations/__init__.py +++ b/bpython/translations/__init__.py @@ -2,11 +2,11 @@ import locale import os.path import sys -from typing import cast +from typing import cast, List from .. import package_dir -translator: gettext.GNUTranslations = cast(gettext.GNUTranslations, None) +translator: gettext.NullTranslations = cast(gettext.NullTranslations, None) def _(message) -> str: @@ -17,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: From 4ef513d3c785b3c9bc1786df6adc6ae676eb6efc Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Fri, 8 Oct 2021 19:37:08 -0700 Subject: [PATCH 09/10] Basic type checking in all files --- .github/workflows/lint.yaml | 4 ++-- bpython/cli.py | 9 +++++---- bpython/curtsies.py | 4 ++-- bpython/test/test_crashers.py | 4 ++-- bpython/test/test_curtsies_repl.py | 5 ++--- bpython/test/test_inspection.py | 7 +++++-- bpython/urwid.py | 2 ++ setup.cfg | 3 +++ 8 files changed, 23 insertions(+), 15 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index aeb11721b..839681b92 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -38,8 +38,8 @@ jobs: 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" + 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 bpython/autocomplete.py bpython/args.py bpython/config.py bpython/curtsies.py + run: mypy diff --git a/bpython/cli.py b/bpython/cli.py index baa2059da..c5684c732 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -50,7 +50,8 @@ import struct import sys import time -from typing import Iterator, NoReturn +from typing import Iterator, NoReturn, List +from typing_extensions import Literal import unicodedata from dataclasses import dataclass @@ -144,7 +145,7 @@ def writelines(self, l) -> None: for s in l: self.write(s) - def isatty(self) -> True: + def isatty(self) -> Literal[True]: # some third party (amongst them mercurial) depend on this return True @@ -161,7 +162,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 +176,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) -> Literal[True]: return True def readline(self, size=-1): diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 9776a5863..b95b8fa53 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -44,7 +44,7 @@ def send( ) -> Union[str, curtsies.events.Event, None]: ... - def __iter__(self) -> SupportsEventGeneration: + def __iter__(self) -> "SupportsEventGeneration": ... def __next__(self) -> Union[str, curtsies.events.Event, None]: @@ -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], Union[float, None], None ]: 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..04ec8c84d 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -2,6 +2,9 @@ import sys import unittest +from typing import Optional +from types import ModuleType + from bpython import inspection from bpython.test.fodder import encoding_ascii from bpython.test.fodder import encoding_latin1 @@ -12,7 +15,7 @@ try: import numpy except ImportError: - numpy = None + numpy = None # type: ignore foo_ascii_only = '''def foo(): @@ -191,7 +194,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/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 41b47a93f..1efed779c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -74,3 +74,6 @@ files=bpython [mypy-jedi] ignore_missing_imports = True + +[mypy-urwid] +ignore_missing_imports = True From 3514a7d58ed34ad1ef59bec81c0178aeac055f6c Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sun, 10 Oct 2021 08:24:23 -0700 Subject: [PATCH 10/10] Use Optional, CR --- bpython/autocomplete.py | 56 ++++++++++++------------- bpython/cli.py | 5 +-- bpython/curtsies.py | 6 +-- bpython/curtsiesfrontend/interaction.py | 1 - bpython/curtsiesfrontend/repl.py | 13 +++--- bpython/test/test_inspection.py | 3 -- stubs/cwcwidth.pyi | 4 -- 7 files changed, 36 insertions(+), 52 deletions(-) delete mode 100644 stubs/cwcwidth.pyi diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index c87a39c4b..fd91ea4af 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -66,7 +66,7 @@ class AutocompleteModes(Enum): FUZZY = "fuzzy" @classmethod - def from_string(cls, value: str) -> Union[Any, None]: + def from_string(cls, value: str) -> Optional[Any]: if value.upper() in cls.__members__: return cls.__members__[value.upper()] return None @@ -209,7 +209,7 @@ 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) -> 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) @@ -236,7 +236,7 @@ def __init__( @abc.abstractmethod def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Union[Set[str], None]: + ) -> Optional[Set[str]]: """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) -> 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 @@ -299,7 +299,7 @@ def __init__( super().__init__(True, mode) - def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: 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 - ) -> Union[None, Set]: + ) -> Optional[Set]: 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 - ) -> Union[None, Set]: + ) -> Optional[Set]: return self.module_gatherer.complete(cursor_offset, line) - def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return lineparts.current_word(cursor_offset, line) def format(self, word: str) -> str: @@ -355,7 +355,7 @@ def safe_glob(self, pathname: str) -> Iterator[str]: def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Union[None, Set]: + ) -> Optional[Set]: cs = lineparts.current_string(cursor_offset, line) if cs is None: return None @@ -370,7 +370,7 @@ def matches( matches.add(filename) return matches - def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return lineparts.current_string(cursor_offset, line) def format(self, filename: str) -> str: @@ -387,7 +387,7 @@ class AttrCompletion(BaseCompletionType): def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Union[None, Set]: + ) -> Optional[Set]: if "locals_" not in kwargs: return None locals_ = cast(Dict[str, Any], kwargs["locals_"]) @@ -417,7 +417,7 @@ def matches( if few_enough_underscores(r.word.split(".")[-1], m.split(".")[-1]) } - def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return lineparts.current_dotted_attribute(cursor_offset, line) def format(self, word: str) -> str: @@ -472,7 +472,7 @@ def list_attributes(self, obj: Any) -> List[str]: class DictKeyCompletion(BaseCompletionType): def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Union[None, Set]: + ) -> Optional[Set]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -495,7 +495,7 @@ def matches( else: return None - def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return lineparts.current_dict_key(cursor_offset, line) def format(self, match: str) -> str: @@ -505,7 +505,7 @@ def format(self, match: str) -> str: class MagicMethodCompletion(BaseCompletionType): def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Union[None, Set]: + ) -> Optional[Set]: if "current_block" not in kwargs: return None current_block = kwargs["current_block"] @@ -517,14 +517,14 @@ def matches( return None return {name for name in MAGIC_METHODS if name.startswith(r.word)} - def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: + 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: int, line: str, **kwargs: Any - ) -> Union[None, Set]: + ) -> 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. @@ -554,14 +554,14 @@ def matches( matches.add(_callable_postfix(val, word)) return matches if matches else None - def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: + 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: int, line: str, **kwargs: Any - ) -> Union[None, Set]: + ) -> Optional[Set]: if "argspec" not in kwargs: return None argspec = kwargs["argspec"] @@ -582,18 +582,18 @@ def matches( ) return matches if matches else None - def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: + 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, cursor_offset: int, line: str) -> Union[LinePart, None]: + 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 - ) -> Union[None, Set]: + ) -> Optional[Set]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -621,23 +621,21 @@ def matches( class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] def matches( self, cursor_offset: int, line: str, **kwargs: Any - ) -> Union[None, Set]: + ) -> Optional[Set]: return None - def locate( - self, cursor_offset: int, line: str - ) -> Union[LinePart, 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: int, line: str, **kwargs: Any - ) -> Union[None, Set]: + ) -> Optional[Set]: if "history" not in kwargs: return None history = kwargs["history"] @@ -687,7 +685,7 @@ 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 - ) -> Union[None, Set]: + ) -> Optional[Set]: 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 c5684c732..28cc67c71 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -51,7 +51,6 @@ import sys import time from typing import Iterator, NoReturn, List -from typing_extensions import Literal import unicodedata from dataclasses import dataclass @@ -145,7 +144,7 @@ def writelines(self, l) -> None: for s in l: self.write(s) - def isatty(self) -> Literal[True]: + def isatty(self) -> bool: # some third party (amongst them mercurial) depend on this return True @@ -176,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) -> Literal[True]: + def isatty(self) -> bool: return True def readline(self, size=-1): diff --git a/bpython/curtsies.py b/bpython/curtsies.py index b95b8fa53..86d33cf3f 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -40,7 +40,7 @@ class SupportsEventGeneration(Protocol): def send( - self, timeout: Union[float, None] + self, timeout: Optional[float] ) -> Union[str, curtsies.events.Event, None]: ... @@ -253,9 +253,7 @@ def curtsies_arguments(parser: argparse._ArgumentGroup) -> None: def _combined_events( event_provider: "SupportsEventGeneration", paste_threshold: int -) -> Generator[ - Union[str, curtsies.events.Event, None], Union[float, None], None -]: +) -> 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 = collections.deque() diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index 3cec9d5ea..79622d149 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -1,6 +1,5 @@ import greenlet import time -from typing import Optional from curtsies import events from ..translations import _ diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 04e60ed5a..85965f4f6 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -14,7 +14,6 @@ from enum import Enum from typing import Dict, Any, List, Optional, Tuple, Union, cast -from typing_extensions import Literal import blessings import cwcwidth @@ -60,8 +59,6 @@ ) from ..translations import _ -InputOrOutput = Union[Literal["input"], Literal["output"]] - logger = logging.getLogger(__name__) INCONSISTENT_HISTORY_MSG = "#<---History inconsistent with output shown--->" @@ -286,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): @@ -387,8 +384,8 @@ 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: List[Tuple[str, InputOrOutput]] = [] + # (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 @@ -1629,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, diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index 04ec8c84d..50be0c3a2 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -2,9 +2,6 @@ import sys import unittest -from typing import Optional -from types import ModuleType - from bpython import inspection from bpython.test.fodder import encoding_ascii from bpython.test.fodder import encoding_latin1 diff --git a/stubs/cwcwidth.pyi b/stubs/cwcwidth.pyi deleted file mode 100644 index 8d047b628..000000000 --- a/stubs/cwcwidth.pyi +++ /dev/null @@ -1,4 +0,0 @@ -# official typing seems to require a second arg -def wcswidth(s: str, n=100000000): ... - -__version__: str