Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-curtsies for openSUSE:Factory checked in at 2022-09-12 19:08:22 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-curtsies (Old) and /work/SRC/openSUSE:Factory/.python-curtsies.new.2083 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-curtsies" Mon Sep 12 19:08:22 2022 rev:13 rq:1002735 version:0.4.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-curtsies/python-curtsies.changes 2022-03-28 17:01:39.533058849 +0200 +++ /work/SRC/openSUSE:Factory/.python-curtsies.new.2083/python-curtsies.changes 2022-09-12 19:08:23.658560968 +0200 @@ -1,0 +2,13 @@ +Sat Sep 10 15:48:54 UTC 2022 - Arun Persaud <a...@gmx.de> + +- specfile: + * skip python 3.6 + * require python-blessed + +- update to version 0.4.0: + * Clean up both wakeup_fds + * Drop support for Python 3.6 + * Switch to blessed + * Typing: add more annotations + +------------------------------------------------------------------- Old: ---- curtsies-0.3.10.tar.gz New: ---- curtsies-0.4.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-curtsies.spec ++++++ --- /var/tmp/diff_new_pack.2tlE16/_old 2022-09-12 19:08:24.114562250 +0200 +++ /var/tmp/diff_new_pack.2tlE16/_new 2022-09-12 19:08:24.118562261 +0200 @@ -18,22 +18,23 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define skip_python2 1 +%define skip_python36 1 Name: python-curtsies -Version: 0.3.10 +Version: 0.4.0 Release: 0 Summary: Curses-like terminal wrapper, with colored strings! License: MIT Group: Development/Languages/Python URL: https://github.com/bpython/curtsies Source: https://files.pythonhosted.org/packages/source/c/curtsies/curtsies-%{version}.tar.gz -BuildRequires: %{python_module blessings} +BuildRequires: %{python_module blessed >= 1.5} BuildRequires: %{python_module cwcwidth} BuildRequires: %{python_module pytest} BuildRequires: %{python_module pyte} BuildRequires: %{python_module setuptools} BuildRequires: fdupes BuildRequires: python-rpm-macros -Requires: python-blessings +Requires: python-blessed >= 1.5 Requires: python-cwcwidth BuildArch: noarch %python_subpackages ++++++ curtsies-0.3.10.tar.gz -> curtsies-0.4.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/PKG-INFO new/curtsies-0.4.0/PKG-INFO --- old/curtsies-0.3.10/PKG-INFO 2021-10-09 04:42:20.741536100 +0200 +++ new/curtsies-0.4.0/PKG-INFO 2022-08-28 22:39:27.821959300 +0200 @@ -1,12 +1,11 @@ Metadata-Version: 2.1 Name: curtsies -Version: 0.3.10 +Version: 0.4.0 Summary: Curses-like terminal wrapper, with colored strings! Home-page: https://github.com/bpython/curtsies Author: Thomas Ballinger Author-email: thomasballin...@gmail.com License: MIT -Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Console Classifier: Intended Audience :: Developers @@ -14,7 +13,7 @@ Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Requires-Python: >=3.6 +Requires-Python: >=3.7 Description-Content-Type: text/markdown License-File: LICENSE @@ -109,5 +108,3 @@ * Thanks to the many contributors! * If all you need are colored strings, consider one of these [other libraries](http://curtsies.readthedocs.io/en/latest/FmtStr.html#fmtstr-rationale)! - - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/curtsies/__init__.py new/curtsies-0.4.0/curtsies/__init__.py --- old/curtsies-0.3.10/curtsies/__init__.py 2021-10-09 04:39:20.000000000 +0200 +++ new/curtsies-0.4.0/curtsies/__init__.py 2022-08-28 22:39:24.000000000 +0200 @@ -1,5 +1,5 @@ """Terminal-formatted strings""" -__version__ = "0.3.10" +__version__ = "0.4.0" from .window import FullscreenWindow, CursorAwareWindow from .input import Input diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/curtsies/curtsieskeys.py new/curtsies-0.4.0/curtsies/curtsieskeys.py --- old/curtsies-0.3.10/curtsies/curtsieskeys.py 2021-09-25 20:39:51.000000000 +0200 +++ new/curtsies-0.4.0/curtsies/curtsieskeys.py 2022-08-28 22:39:24.000000000 +0200 @@ -92,6 +92,10 @@ b"\x1b[3~": '<DELETE>', # delete (.), "Execute" b"\x1b[3;5~": '<Ctrl-DELETE>', + # st (simple terminal) see issue #169 + b"\x1b[4h": '<INSERT>', + b"\x1b[P": '<DELETE>', + # not fixing for back compat. # (b"\x1b[4~": u'<SELECT>', # select diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/curtsies/events.py new/curtsies-0.4.0/curtsies/events.py --- old/curtsies-0.3.10/curtsies/events.py 2021-10-08 23:51:39.000000000 +0200 +++ new/curtsies-0.4.0/curtsies/events.py 2022-08-28 22:39:24.000000000 +0200 @@ -1,9 +1,9 @@ """Events for keystrokes and other input events""" import codecs -import encodings import itertools import sys -from typing import Optional, List, Union +from enum import Enum, auto +from typing import Dict, Optional, List, Union from .termhelpers import Termmode from .curtsieskeys import CURTSIES_NAMES as special_curtsies_names @@ -12,7 +12,7 @@ chr_uni = chr -CURTSIES_NAMES = {} +CURTSIES_NAMES: Dict[bytes, str] = {} control_chars = {chr_byte(i): "<Ctrl-%s>" % chr(i + 0x60) for i in range(0x00, 0x1B)} CURTSIES_NAMES.update(control_chars) for i in range(0x00, 0x80): @@ -81,6 +81,12 @@ ) +class Keynames(Enum): + CURTSIES = auto() + CURSES = auto() + BYTES = auto() + + class Event: pass @@ -159,7 +165,10 @@ def get_key( - bytes_: List[bytes], encoding: str, keynames: str = "curtsies", full: bool = False + bytes_: List[bytes], + encoding: str, + keynames: Keynames = Keynames.CURTSIES, + full: bool = False, ) -> Optional[str]: """Return key pressed from bytes_ or None @@ -193,16 +202,14 @@ (for 'asdf', first on 'a', then on 'as', then on 'asd' - until a non-None value is returned) """ - if not all(isinstance(c, type(b"")) for c in bytes_): - raise ValueError("get key expects bytes, got %r" % bytes_) # expects raw bytes - if keynames not in ["curtsies", "curses", "bytes"]: - raise ValueError("keynames must be one of 'curtsies', 'curses' or 'bytes'") + if not all(isinstance(c, bytes) for c in bytes_): + raise TypeError("get key expects bytes, got %r" % bytes_) # expects raw bytes seq = b"".join(bytes_) if len(seq) > MAX_KEYPRESS_SIZE: raise ValueError("unable to decode bytes %r" % seq) def key_name() -> str: - if keynames == "curses": + if keynames == Keynames.CURSES: # may not be here (and still not decodable) curses names incomplete if seq in CURSES_NAMES: return CURSES_NAMES[seq] @@ -224,14 +231,14 @@ "x%02X" % ord(seq[i : i + 1]) for i in range(len(seq)) ) # TODO if this isn't possible, return multiple meta keys as a paste event if paste events enabled - elif keynames == "curtsies": + elif keynames == Keynames.CURTSIES: if seq in CURTSIES_NAMES: return CURTSIES_NAMES[seq] return seq.decode( encoding ) # assumes that curtsies names are a subset of curses ones else: - assert keynames == "bytes" + assert keynames == Keynames.BYTES return seq # type: ignore key_known = seq in CURTSIES_NAMES or seq in CURSES_NAMES or decodable(seq, encoding) @@ -306,9 +313,6 @@ print( "press a bunch of keys (not at the same time, but you can hit them pretty quickly)" ) - import tty - import termios - import fcntl import os from .termhelpers import Cbreak diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/curtsies/formatstring.py new/curtsies-0.4.0/curtsies/formatstring.py --- old/curtsies-0.3.10/curtsies/formatstring.py 2021-10-07 20:41:22.000000000 +0200 +++ new/curtsies-0.4.0/curtsies/formatstring.py 2022-08-28 22:39:24.000000000 +0200 @@ -19,24 +19,23 @@ red('hello') """ -import itertools import re -import sys from cwcwidth import wcswidth, wcwidth +from itertools import chain from typing import ( + Any, + Callable, + Dict, + Iterable, Iterator, - Tuple, List, - Union, - Optional, - Any, Mapping, - cast, MutableMapping, + Optional, + Tuple, + Union, + cast, no_type_check, - Type, - Callable, - Iterable, ) try: @@ -76,8 +75,8 @@ xforms.update(two_arg_xforms) -class FrozenDict(dict): - """Immutable dictionary class""" +class FrozenAttributes(Dict[str, Union[int, bool]]): + """Immutable dictionary class for format string attributes""" @no_type_check def __setitem__(self, key, value): @@ -87,11 +86,11 @@ def update(self, *args, **kwds): raise Exception("Cannot change value.") - def extend(self, dictlike: Mapping[str, Union[int, bool]]) -> "FrozenDict": - return FrozenDict(itertools.chain(self.items(), dictlike.items())) + def extend(self, dictlike: Mapping[str, Union[int, bool]]) -> "FrozenAttributes": + return FrozenAttributes(chain(self.items(), dictlike.items())) - def remove(self, *keys: str) -> "FrozenDict": - return FrozenDict((k, v) for k, v in self.items() if k not in keys) + def remove(self, *keys: str) -> "FrozenAttributes": + return FrozenAttributes((k, v) for k, v in self.items() if k not in keys) def stable_format_dict(d: Mapping) -> str: @@ -121,14 +120,14 @@ if not isinstance(string, str): raise ValueError("unicode string required, got %r" % string) self._s = string - self._atts = FrozenDict(atts if atts else {}) + self._atts = FrozenAttributes(atts if atts else {}) @property def s(self) -> str: return self._s @property - def atts(self) -> Mapping[str, Union[int, bool]]: + def atts(self) -> FrozenAttributes: "Attributes, e.g. {'fg': 34, 'bold': True} where 34 is the escape code for ..." return self._atts @@ -137,7 +136,7 @@ @property def width(self) -> int: - width = wcswidth(self._s, None) + width = wcswidth(self._s) if len(self._s) > 0 and width < 1: raise ValueError("Can't calculate width of string %r" % self._s) return width @@ -145,8 +144,8 @@ @cached_property def color_str(self) -> str: "Return an escape-coded string to write to the terminal." - s = self.s - for k, v in sorted(self.atts.items()): + s = self._s + for k, v in sorted(self._atts.items()): # (self.atts sorted for the sake of always acting the same.) if k not in xforms: # Unsupported SGR code @@ -156,6 +155,7 @@ elif v is True: s = one_arg_xforms[k](s) else: + # TODO: What's the purpose of this code? It will never be executed. s = two_arg_xforms[k](s, v) return s @@ -168,16 +168,16 @@ def __eq__(self, other: Any) -> bool: if not isinstance(other, Chunk): return NotImplemented - return self.s == other.s and self.atts == other.atts + return self._s == other._s and self._atts == other._atts def __hash__(self) -> int: - return hash((self.s, self.atts)) + return hash((self._s, self._atts)) def __repr__(self) -> str: return "Chunk({s}{sep}{atts})".format( - s=repr(self.s), - sep=", " if self.atts else "", - atts=stable_format_dict(self.atts) if self.atts else "", + s=repr(self._s), + sep=", " if self._atts else "", + atts=stable_format_dict(self._atts) if self._atts else "", ) def repr_part(self) -> str: @@ -191,10 +191,10 @@ else: return att - atts_out = {k: v for (k, v) in self.atts.items() if v} + atts_out = {k: v for (k, v) in self._atts.items() if v} return ( "".join(pp_att(att) + "(" for att in sorted(atts_out)) - + repr(self.s) + + repr(self._s) + ")" * len(atts_out) ) @@ -416,8 +416,9 @@ def copy_with_new_atts(self, **attributes: Union[bool, int]) -> "FmtStr": """Returns a new FmtStr with the same content but new formatting""" - result = FmtStr(*(Chunk(bfs.s, bfs.atts.extend(attributes)) for bfs in self.chunks)) # type: ignore - return result + return FmtStr( + *(Chunk(bfs.s, bfs.atts.extend(attributes)) for bfs in self.chunks) + ) def join(self, iterable: Iterable[Union[str, "FmtStr"]]) -> "FmtStr": """Joins an iterable yielding strings or FmtStrs with self as separator""" @@ -456,8 +457,8 @@ return [ self[start:end] for start, end in zip( - [0] + [m.end() for m in matches], - [m.start() for m in matches] + [len(s)], + chain((0,), (m.end() for m in matches)), + chain((m.start() for m in matches), (len(s),)), ) ] @@ -828,7 +829,7 @@ def parse_args( - args: Tuple[Union[bytes, str], ...], + args: Tuple[str, ...], kwargs: MutableMapping[str, Union[int, bool, str]], ) -> Mapping[str, Union[int, bool]]: """Returns a kwargs dictionary by turning args into kwargs""" @@ -836,9 +837,8 @@ args += (cast(str, kwargs["style"]),) del kwargs["style"] for arg in args: - arg = cast(str, arg) - if not isinstance(arg, (bytes, str)): - raise ValueError("args must be strings:" + repr(args)) + if not isinstance(arg, str): + raise ValueError(f"args must be strings: {arg!r}") if arg.lower() in FG_COLORS: if "fg" in kwargs: raise ValueError("fg specified twice") @@ -850,20 +850,20 @@ elif arg.lower() in STYLES: kwargs[arg] = True else: - raise ValueError("couldn't process arg: " + repr(arg)) + raise ValueError(f"couldn't process arg: {args!r}") for k in kwargs: - if k not in ["fg", "bg"] + list(STYLES.keys()): + if k not in ("fg", "bg") and k not in STYLES.keys(): raise ValueError("Can't apply that transformation") if "fg" in kwargs: if kwargs["fg"] in FG_COLORS: kwargs["fg"] = FG_COLORS[cast(str, kwargs["fg"])] if kwargs["fg"] not in list(FG_COLORS.values()): - raise ValueError("Bad fg value: %r" % kwargs["fg"]) + raise ValueError(f"Bad fg value: {kwargs['fg']!r}") if "bg" in kwargs: if kwargs["bg"] in BG_COLORS: kwargs["bg"] = BG_COLORS[cast(str, kwargs["bg"])] if kwargs["bg"] not in list(BG_COLORS.values()): - raise ValueError("Bad bg value: %r" % kwargs["bg"]) + raise ValueError(f"Bad bg value: {kwargs['bg']!r}") return cast(MutableMapping[str, Union[int, bool]], kwargs) @@ -877,14 +877,10 @@ on_red(bold(blue('blarg'))) """ atts = parse_args(args, kwargs) - if isinstance(string, FmtStr): - pass - elif isinstance(string, (bytes, str)): + if isinstance(string, str): string = FmtStr.from_str(string) - else: + elif not isinstance(string, FmtStr): raise ValueError( - "Bad Args: {!r} (of type {}), {!r}, {!r}".format( - string, type(string), args, kwargs - ) + f"Bad Args: {string!r} (of type {type(string)}), {args!r}, {kwargs!r}" ) return string.copy_with_new_atts(**atts) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/curtsies/formatstringarray.py new/curtsies-0.4.0/curtsies/formatstringarray.py --- old/curtsies-0.3.10/curtsies/formatstringarray.py 2021-09-25 20:39:51.000000000 +0200 +++ new/curtsies-0.4.0/curtsies/formatstringarray.py 2022-08-28 22:39:24.000000000 +0200 @@ -88,10 +88,7 @@ if isinstance(slicetuple, slice): rowslice = normalize_slice(len(self.rows), slicetuple) return self.rows[rowslice] - ( - row_slice_or_int, - col_slice_or_int, - ) = slicetuple # type: Tuple[Union[int, slice], Union[int, slice]] + row_slice_or_int, col_slice_or_int = slicetuple rowslice = normalize_slice(len(self.rows), row_slice_or_int) colslice = normalize_slice(self.num_columns, col_slice_or_int) # TODO clean up slices @@ -122,7 +119,7 @@ logger.debug("slice: %r", slicetuple) if isinstance(slicetuple, slice): rowslice, colslice = slicetuple, slice(None) - if isinstance(value, (bytes, str)): + if isinstance(value, str): raise ValueError( "if slice is 2D, value must be 2D as in of list type []" ) @@ -251,7 +248,9 @@ ) -def fsarray(strings: List[Union[FmtStr, str]], *args: Any, **kwargs: Any) -> FSArray: +def fsarray( + strings: Sequence[Union[FmtStr, str]], *args: Any, **kwargs: Any +) -> FSArray: """fsarray(list_of_FmtStrs_or_strings, width=None) -> FSArray Returns a new FSArray of width of the maximum size of the provided @@ -279,7 +278,7 @@ return arr -def simple_format(x: Union[FSArray, List[FmtStr]]) -> str: +def simple_format(x: Union[FSArray, Sequence[FmtStr]]) -> str: return "\n".join(str(l) for l in x) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/curtsies/input.py new/curtsies-0.4.0/curtsies/input.py --- old/curtsies-0.3.10/curtsies/input.py 2021-10-09 03:40:58.000000000 +0200 +++ new/curtsies-0.4.0/curtsies/input.py 2022-08-28 22:39:24.000000000 +0200 @@ -12,7 +12,18 @@ from .termhelpers import Nonblocking from . import events -from typing import Callable, Type, TextIO, Optional, List, Union, cast, Tuple, Any +from typing import ( + Callable, + ContextManager, + Type, + TextIO, + Optional, + List, + Union, + cast, + Tuple, + Any, +) from types import TracebackType, FrameType @@ -27,7 +38,7 @@ return threading.current_thread() == threading.main_thread() -class ReplacedSigIntHandler: +class ReplacedSigIntHandler(ContextManager): def __init__(self, handler: Callable) -> None: self.handler = handler @@ -43,13 +54,13 @@ signal.signal(signal.SIGINT, self.orig_sigint_handler) -class Input: +class Input(ContextManager["Input"]): """Keypress and control event generator""" def __init__( self, in_stream: Optional[TextIO] = None, - keynames: str = "curtsies", + keynames: Union[events.Keynames, str] = events.Keynames.CURTSIES, paste_threshold: Optional[int] = events.MAX_KEYPRESS_SIZE + 1, sigint_event: bool = False, disable_terminal_start_stop: bool = False, @@ -73,11 +84,24 @@ in_stream = sys.__stdin__ self.in_stream = in_stream self.unprocessed_bytes: List[bytes] = [] # leftover from stdin, unprocessed yet - self.keynames = keynames + if isinstance(keynames, str): + # TODO: Remove this block with the next API breaking release. + if keynames == "curtsies": + self.keynames = events.Keynames.CURTSIES + elif keynames == "curses": + self.keynames = events.Keynames.CURSES + elif keynames == "bytes": + self.keynames = events.Keynames.BYTES + else: + raise ValueError("keyname is invalid") + else: + self.keynames = keynames self.paste_threshold = paste_threshold self.sigint_event = sigint_event self.disable_terminal_start_stop = disable_terminal_start_stop self.sigints: List[events.SigIntEvent] = [] + self.wakeup_read_fd: Optional[int] = None + self.wakeup_write_fd: Optional[int] = None self.readers: List[int] = [] self.queued_interrupting_events: List[Union[events.Event, str]] = [] @@ -110,11 +134,11 @@ self.orig_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) - self.wakeup_read_fd, wfd = os.pipe() - os.set_blocking(wfd, False) - if sys.version_info[0] == 3 and 5 <= sys.version_info[1] < 7: - signal.set_wakeup_fd(wfd) - elif sys.version_info[0] == 3 and 7 <= sys.version_info[1]: + # Non-main threads don't receive signals + if is_main_thread(): + self.wakeup_read_fd, self.wakeup_write_fd = os.pipe() + wfd = self.wakeup_write_fd + os.set_blocking(wfd, False) signal.set_wakeup_fd(wfd, warn_on_full_buffer=False) return self @@ -131,12 +155,16 @@ and self.orig_sigint_handler is not None ): signal.signal(signal.SIGINT, self.orig_sigint_handler) - if sys.version_info[0] == 3 and sys.version_info[1] > 4: + if is_main_thread(): signal.set_wakeup_fd(-1) + if self.wakeup_read_fd is not None: + os.close(self.wakeup_read_fd) + if self.wakeup_write_fd is not None: + os.close(self.wakeup_write_fd) termios.tcsetattr(self.in_stream, termios.TCSANOW, self.original_stty) def sigint_handler( - self, signum: Union[signal.Signals, int], frame: FrameType + self, signum: Union[signal.Signals, int], frame: Optional[FrameType] ) -> None: self.sigints.append(events.SigIntEvent()) @@ -168,10 +196,8 @@ while True: try: (rs, _, _) = select.select( - [ - self.in_stream.fileno(), - self.wakeup_read_fd, - ] + [self.in_stream.fileno()] + + ([] if self.wakeup_read_fd is None else [self.wakeup_read_fd]) + self.readers, [], [], diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/curtsies/termhelpers.py new/curtsies-0.4.0/curtsies/termhelpers.py --- old/curtsies-0.3.10/curtsies/termhelpers.py 2021-09-25 20:39:51.000000000 +0200 +++ new/curtsies-0.4.0/curtsies/termhelpers.py 2022-08-28 22:39:24.000000000 +0200 @@ -3,13 +3,13 @@ import fcntl import os -from typing import IO, Type, List, Union, Optional +from typing import IO, ContextManager, Type, List, Union, Optional from types import TracebackType _Attr = List[Union[int, List[Union[bytes, int]]]] -class Nonblocking: +class Nonblocking(ContextManager): """ A context manager for making an input stream nonblocking. """ @@ -31,7 +31,7 @@ fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl) -class Termmode: +class Termmode(ContextManager): def __init__(self, stream: IO, attrs: _Attr) -> None: self.stream = stream self.attrs = attrs @@ -49,7 +49,7 @@ termios.tcsetattr(self.stream, termios.TCSANOW, self.original_stty) -class Cbreak: +class Cbreak(ContextManager[Termmode]): def __init__(self, stream: IO) -> None: self.stream = stream diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/curtsies/window.py new/curtsies-0.4.0/curtsies/window.py --- old/curtsies-0.3.10/curtsies/window.py 2021-09-25 20:39:51.000000000 +0200 +++ new/curtsies-0.4.0/curtsies/window.py 2022-08-28 22:39:24.000000000 +0200 @@ -1,11 +1,13 @@ # All windows write only unicode to the terminal - -# that's what blessings does, so we match it. +# that's what blessed does, so we match it. from typing import ( + ContextManager, Optional, IO, Dict, + Sequence, TypeVar, Type, Tuple, @@ -21,7 +23,7 @@ import re import sys -import blessings +import blessed from .formatstring import fmtstr, FmtStr from .formatstringarray import FSArray @@ -29,21 +31,18 @@ logger = logging.getLogger(__name__) -SCROLL_DOWN = "\x1bD" -FIRST_COLUMN = "\x1b[1G" - T = TypeVar("T", bound="BaseWindow") -class BaseWindow: +class BaseWindow(ContextManager): def __init__( self, out_stream: Optional[IO] = None, hide_cursor: bool = True ) -> None: logger.debug("-------initializing Window object %r------" % self) if out_stream is None: out_stream = sys.__stdout__ - self.t = blessings.Terminal(stream=out_stream, force_styling=True) + self.t = blessed.Terminal(stream=out_stream, force_styling=True) self.out_stream = out_stream self.hide_cursor = hide_cursor self._last_lines_by_row: Dict[int, Optional[FmtStr]] = {} @@ -55,7 +54,7 @@ # since scroll-down only moves the screen if cursor is at bottom with self.t.location(x=0, y=1000000): - self.write(SCROLL_DOWN) # TODO will blessings do this? + self.write(self.t.move_down) def write(self, msg: str) -> None: self.out_stream.write(msg) @@ -129,7 +128,7 @@ return for_stdout -class FullscreenWindow(BaseWindow): +class FullscreenWindow(BaseWindow, ContextManager["FullscreenWindow"]): """2D-text rendering window that disappears when its context is left FullscreenWindow will only render arrays the size of the terminal @@ -199,12 +198,9 @@ self.on_terminal_size_change(height, width) current_lines_by_row: Dict[int, Optional[FmtStr]] = {} - rows = list(range(height)) - rows_for_use = rows[: len(array)] - rest_of_rows = rows[len(array) :] # rows which we have content for and don't require scrolling - for row, line in zip(rows_for_use, array): + for row, line in enumerate(array): current_lines_by_row[row] = line if line == self._last_lines_by_row.get(row, None): continue @@ -214,7 +210,7 @@ self.write(self.t.clear_eol) # rows onscreen that we don't have content for - for row in rest_of_rows: + for row in range(len(array), height): if self._last_lines_by_row and row not in self._last_lines_by_row: continue self.write(self.t.move(row, 0)) @@ -230,7 +226,7 @@ self.write(self.t.normal_cursor) -class CursorAwareWindow(BaseWindow): +class CursorAwareWindow(BaseWindow, ContextManager["CursorAwareWindow"]): """ Renders to the normal terminal screen and can find the location of the cursor. @@ -269,10 +265,16 @@ if in_stream is None: in_stream = sys.__stdin__ self.in_stream = in_stream + # whether we can use blessed to handle some operations + self._use_blessed = ( + self.out_stream == sys.__stdout__ and self.in_stream == sys.__stdin__ + ) self._last_cursor_column: Optional[int] = None self._last_cursor_row: Optional[int] = None self.keep_last_line = keep_last_line - self.cbreak = Cbreak(self.in_stream) + self.cbreak = ( + Cbreak(self.in_stream) if not self._use_blessed else self.t.cbreak() + ) self.extra_bytes_callback = extra_bytes_callback # whether another SIGWINCH is queued up @@ -296,9 +298,9 @@ ) -> None: if self.keep_last_line: # just moves cursor down if not on last line - self.write(SCROLL_DOWN) + self.write(self.t.move_down) - self.write(FIRST_COLUMN) + self.write(self.t.move_x(0)) self.write(self.t.clear_eos) self.write(self.t.clear_eol) self.cbreak.__exit__(type, value, traceback) @@ -307,7 +309,11 @@ def get_cursor_position(self) -> Tuple[int, int]: """Returns the terminal (row, column) of the cursor - 0-indexed, like blessings cursor positions""" + 0-indexed, like blessed cursor positions""" + + if self._use_blessed: + return self.t.get_location() + # TODO would this be cleaner as a parameter? in_stream = self.in_stream @@ -419,7 +425,9 @@ return cursor_dy def render_to_terminal( - self, array: Union[FSArray, List[FmtStr]], cursor_pos: Tuple[int, int] = (0, 0) + self, + array: Union[FSArray, Sequence[FmtStr]], + cursor_pos: Tuple[int, int] = (0, 0), ) -> int: """Renders array to terminal, returns the number of lines scrolled offscreen diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/curtsies.egg-info/PKG-INFO new/curtsies-0.4.0/curtsies.egg-info/PKG-INFO --- old/curtsies-0.3.10/curtsies.egg-info/PKG-INFO 2021-10-09 04:42:20.000000000 +0200 +++ new/curtsies-0.4.0/curtsies.egg-info/PKG-INFO 2022-08-28 22:39:27.000000000 +0200 @@ -1,12 +1,11 @@ Metadata-Version: 2.1 Name: curtsies -Version: 0.3.10 +Version: 0.4.0 Summary: Curses-like terminal wrapper, with colored strings! Home-page: https://github.com/bpython/curtsies Author: Thomas Ballinger Author-email: thomasballin...@gmail.com License: MIT -Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Environment :: Console Classifier: Intended Audience :: Developers @@ -14,7 +13,7 @@ Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 -Requires-Python: >=3.6 +Requires-Python: >=3.7 Description-Content-Type: text/markdown License-File: LICENSE @@ -109,5 +108,3 @@ * Thanks to the many contributors! * If all you need are colored strings, consider one of these [other libraries](http://curtsies.readthedocs.io/en/latest/FmtStr.html#fmtstr-rationale)! - - diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/curtsies.egg-info/SOURCES.txt new/curtsies-0.4.0/curtsies.egg-info/SOURCES.txt --- old/curtsies-0.3.10/curtsies.egg-info/SOURCES.txt 2021-10-09 04:42:20.000000000 +0200 +++ new/curtsies-0.4.0/curtsies.egg-info/SOURCES.txt 2022-08-28 22:39:27.000000000 +0200 @@ -2,7 +2,6 @@ MANIFEST.in README.md pyproject.toml -readme.md setup.cfg setup.py curtsies/__init__.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/curtsies.egg-info/requires.txt new/curtsies-0.4.0/curtsies.egg-info/requires.txt --- old/curtsies-0.3.10/curtsies.egg-info/requires.txt 2021-10-09 04:42:20.000000000 +0200 +++ new/curtsies-0.4.0/curtsies.egg-info/requires.txt 2022-08-28 22:39:27.000000000 +0200 @@ -1,4 +1,4 @@ -blessings>=1.5 +blessed>=1.5 cwcwidth [:python_version < "3.8"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/setup.cfg new/curtsies-0.4.0/setup.cfg --- old/curtsies-0.3.10/setup.cfg 2021-10-09 04:42:20.743643000 +0200 +++ new/curtsies-0.4.0/setup.cfg 2022-08-28 22:39:27.821959300 +0200 @@ -18,11 +18,11 @@ Programming Language :: Python :: 3 [options] -python_requires = >=3.6 +python_requires = >=3.7 zip_safe = False packages = curtsies install_requires = - blessings>=1.5 + blessed>=1.5 cwcwidth backports.cached-property; python_version < "3.8" tests_require = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/tests/test_events.py new/curtsies-0.4.0/tests/test_events.py --- old/curtsies-0.3.10/tests/test_events.py 2021-09-25 20:39:51.000000000 +0200 +++ new/curtsies-0.4.0/tests/test_events.py 2022-08-28 22:39:24.000000000 +0200 @@ -40,25 +40,34 @@ class TestGetKey(unittest.TestCase): def test_utf8_full(self): get_utf_full = partial( - events.get_key, encoding="utf-8", keynames="curtsies", full=True + events.get_key, + encoding="utf-8", + keynames=events.Keynames.CURTSIES, + full=True, ) self.assertEqual(get_utf_full([b"h"]), "h") self.assertEqual(get_utf_full([b"\x1b", b"["]), "<Esc+[>") self.assertRaises(UnicodeDecodeError, get_utf_full, [b"\xfe\xfe"]) - self.assertRaises(ValueError, get_utf_full, "a") + self.assertRaises(TypeError, get_utf_full, "a") def test_utf8(self): get_utf = partial( - events.get_key, encoding="utf-8", keynames="curtsies", full=False + events.get_key, + encoding="utf-8", + keynames=events.Keynames.CURTSIES, + full=False, ) self.assertEqual(get_utf([b"h"]), "h") self.assertEqual(get_utf([b"\x1b", b"["]), None) self.assertEqual(get_utf([b"\xe2"]), None) - self.assertRaises(ValueError, get_utf, "a") + self.assertRaises(TypeError, get_utf, "a") def test_multibyte_utf8(self): get_utf = partial( - events.get_key, encoding="utf-8", keynames="curtsies", full=False + events.get_key, + encoding="utf-8", + keynames=events.Keynames.CURTSIES, + full=False, ) self.assertEqual(get_utf([b"\xc3"]), None) self.assertEqual(get_utf([b"\xe2"]), None) @@ -70,10 +79,15 @@ def test_sequences_without_names(self): get_utf = partial( - events.get_key, encoding="utf-8", keynames="curtsies", full=False + events.get_key, + encoding="utf-8", + keynames=events.Keynames.CURTSIES, + full=False, ) self.assertEqual(get_utf([b"\xc3"], full=True), "<Meta-C>") - self.assertEqual(get_utf([b"\xc3"], full=True, keynames="curses"), "xC3") + self.assertEqual( + get_utf([b"\xc3"], full=True, keynames=events.Keynames.CURSES), "xC3" + ) def test_key_names(self): "Every key sequence with a Curses name should have a Curtsies name too." @@ -86,22 +100,33 @@ class TestGetKeyAscii(unittest.TestCase): def test_full(self): get_ascii_full = partial( - events.get_key, encoding="ascii", keynames="curtsies", full=True + events.get_key, + encoding="ascii", + keynames=events.Keynames.CURTSIES, + full=True, ) self.assertEqual(get_ascii_full([b"a"]), "a") self.assertEqual(get_ascii_full([b"\xe1"]), "<Meta-a>") - self.assertEqual(get_ascii_full([b"\xe1"], keynames="curses"), "xE1") + self.assertEqual( + get_ascii_full([b"\xe1"], keynames=events.Keynames.CURSES), "xE1" + ) def test_simple(self): - get_ascii_full = partial(events.get_key, encoding="ascii", keynames="curtsies") + get_ascii_full = partial( + events.get_key, encoding="ascii", keynames=events.Keynames.CURTSIES + ) self.assertEqual(get_ascii_full([b"a"]), "a") self.assertEqual(get_ascii_full([b"\xe1"]), "<Meta-a>") - self.assertEqual(get_ascii_full([b"\xe1"], keynames="curses"), "xE1") + self.assertEqual( + get_ascii_full([b"\xe1"], keynames=events.Keynames.CURSES), "xE1" + ) class TestUnknownEncoding(unittest.TestCase): def test_simple(self): - get_utf16 = partial(events.get_key, encoding="utf16", keynames="curtsies") + get_utf16 = partial( + events.get_key, encoding="utf16", keynames=events.Keynames.CURTSIES + ) self.assertEqual(get_utf16([b"a"]), None) self.assertEqual(get_utf16([b"a"], full=True), None) self.assertEqual(get_utf16([b"\xe1"]), None) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/tests/test_fmtstr.py new/curtsies-0.4.0/tests/test_fmtstr.py --- old/curtsies-0.3.10/tests/test_fmtstr.py 2021-09-25 20:39:51.000000000 +0200 +++ new/curtsies-0.4.0/tests/test_fmtstr.py 2022-08-28 22:39:24.000000000 +0200 @@ -20,18 +20,18 @@ bold, ) from curtsies.termformatconstants import FG_COLORS -from curtsies.formatstringarray import fsarray, FSArray +from curtsies.formatstringarray import fsarray, FSArray, simple_format from unittest import skip -def repr_without_leading_u(s): +def repr_without_leading_u(s: str) -> str: assert isinstance(s, str) return repr(s) class TestFmtStrInitialization(unittest.TestCase): - def test_bad(self): + def test_bad(self) -> None: # Can't specify fg or bg color two ways self.assertRaises(ValueError, fmtstr, "hello", "blue", {"fg": 30}) self.assertRaises(ValueError, fmtstr, "hello", "on_blue", {"bg": 40}) @@ -41,15 +41,15 @@ # Only existing xforms can be used in kwargs self.assertRaises(ValueError, fmtstr, "hello", "make it big") - def test_actual_init(self): + def test_actual_init(self) -> None: FmtStr() class TestFmtStrParsing(unittest.TestCase): - def test_no_escapes(self): + def test_no_escapes(self) -> None: self.assertEqual(str(fmtstr("abc")), "abc") - def test_simple_escapes(self): + def test_simple_escapes(self) -> None: self.assertEqual(str(fmtstr("\x1b[33mhello\x1b[0m")), "\x1b[33mhello\x1b[39m") self.assertEqual(str(fmtstr("\x1b[33mhello\x1b[39m")), "\x1b[33mhello\x1b[39m") self.assertEqual(str(fmtstr("\x1b[33mhello")), "\x1b[33mhello\x1b[39m") @@ -68,13 +68,13 @@ "\x1b[33m\x1b[43mhello\x1b[49m\x1b[39m", ) - def test_out_of_order(self): + def test_out_of_order(self) -> None: self.assertEqual( str(fmtstr("\x1b[33m\x1b[43mhello\x1b[39m\x1b[49m")), "\x1b[33m\x1b[43mhello\x1b[49m\x1b[39m", ) - def test_noncurtsies_output(self): + def test_noncurtsies_output(self) -> None: fmtstr("\x1b[35mx\x1b[m") self.assertEqual(fmtstr("\x1b[Ahello"), "hello") self.assertEqual(fmtstr("\x1b[20Ahello"), "hello") @@ -82,7 +82,9 @@ class TestImmutability(unittest.TestCase): - def test_fmt_strings_remain_unchanged_when_used_to_construct_other_ones(self): + def test_fmt_strings_remain_unchanged_when_used_to_construct_other_ones( + self, + ) -> None: a = fmtstr("hi", "blue") b = fmtstr("there", "red") c = a + b @@ -90,7 +92,7 @@ self.assertEqual(a.shared_atts["fg"], FG_COLORS["blue"]) self.assertEqual(b.shared_atts["fg"], FG_COLORS["red"]) - def test_immutibility_of_FmtStr(self): + def test_immutibility_of_FmtStr(self) -> None: a = fmtstr("hi", "blue") b = green(a) self.assertEqual(a.shared_atts["fg"], FG_COLORS["blue"]) @@ -98,11 +100,11 @@ class TestFmtStrSplice(unittest.TestCase): - def test_simple_beginning_splice(self): + def test_simple_beginning_splice(self) -> None: self.assertEqual(fmtstr("abc").splice("d", 0), fmtstr("dabc")) self.assertEqual(fmtstr("abc").splice("d", 0), "d" + fmtstr("abc")) - def test_various_splices(self): + def test_various_splices(self) -> None: a = blue("hi") b = a + green("bye") c = b + red("!") @@ -115,10 +117,10 @@ ) self.assertEqual(c.splice("asdfg", 1, 5), blue("h") + "asdfg" + red("!")) - def test_splice_of_empty_fmtstr(self): + def test_splice_of_empty_fmtstr(self) -> None: self.assertEqual(fmtstr("ab").splice("", 1), fmtstr("ab")) - def test_splice_with_multiple_chunks(self): + def test_splice_with_multiple_chunks(self) -> None: a = fmtstr("notion") b = a.splice("te", 2, 6) c = b.splice("de", 0) @@ -128,7 +130,7 @@ self.assertEqual(c.s, "denote") self.assertEqual(len(c.chunks), 3) - def test_splice_fmtstr_with_end_without_atts(self): + def test_splice_fmtstr_with_end_without_atts(self) -> None: a = fmtstr("notion") b = a.splice("te", 2, 6) @@ -136,7 +138,7 @@ self.assertEqual(b.s, "note") self.assertEqual(len(b.chunks), 2) - def test_splice_fmtstr_with_end_with_atts(self): + def test_splice_fmtstr_with_end_with_atts(self) -> None: # Need to test with fmtstr consisting of multiple chunks # and with attributes a = fmtstr("notion", "blue") @@ -151,21 +153,21 @@ self.assertEqual(b.chunks[1].atts, {}) self.assertEqual(len(b.chunks), 2) - def test_splice_fmtstr_without_end(self): + def test_splice_fmtstr_without_end(self) -> None: a = fmtstr("notion") b = a.splice(fmtstr("ta"), 2) self.assertEqual(a.s, "notion") self.assertEqual(b.s, "notation") self.assertEqual(len(b.chunks), 3) - def test_splice_string_without_end(self): + def test_splice_string_without_end(self) -> None: a = fmtstr("notion") b = a.splice("ta", 2) self.assertEqual(a.s, "notion") self.assertEqual(b.s, "notation") self.assertEqual(len(b.chunks), 3) - def test_multiple_bfs_splice(self): + def test_multiple_bfs_splice(self) -> None: self.assertEqual( fmtstr("a") + blue("b"), on_blue(" " * 2).splice(fmtstr("a") + blue("b"), 0, 2), @@ -188,13 +190,13 @@ class TestFmtStr(unittest.TestCase): - def test_copy_with_new_atts(self): + def test_copy_with_new_atts(self) -> None: a = fmtstr("hello") b = a.copy_with_new_atts(bold=True) self.assertEqual(a.shared_atts, {}) self.assertEqual(b.shared_atts, {"bold": True}) - def test_copy_with_new_str(self): + def test_copy_with_new_str(self) -> None: # Change string but not attributes a = fmtstr("hello", "blue") b = a.copy_with_new_str("bye") @@ -202,21 +204,21 @@ self.assertEqual(b.s, "bye") self.assertEqual(a.chunks[0].atts, b.chunks[0].atts) - def test_append_without_atts(self): + def test_append_without_atts(self) -> None: a = fmtstr("no") b = a.append("te") self.assertEqual(a.s, "no") self.assertEqual(b.s, "note") self.assertEqual(len(b.chunks), 2) - def test_shared_atts(self): + def test_shared_atts(self) -> None: a = fmtstr("hi", "blue") b = fmtstr("there", "blue") c = a + b self.assertTrue("fg" in a.shared_atts) self.assertTrue("fg" in c.shared_atts) - def test_new_with_atts_removed(self): + def test_new_with_atts_removed(self) -> None: a = fmtstr("hi", "blue", "on_green") b = fmtstr("there", "blue", "on_red") c = a + b @@ -224,13 +226,13 @@ c.new_with_atts_removed("fg"), on_green("hi") + on_red("there") ) - def setUp(self): + def setUp(self) -> None: self.s = fmtstr("hello!", "on_blue", fg="red") - def test_length(self): + def test_length(self) -> None: self.assertEqual(len(self.s), len(self.s.s)) - def test_split(self): + def test_split(self) -> None: self.assertEqual(blue("hello there").split(" "), [blue("hello"), blue("there")]) s = blue("hello there") self.assertEqual(s.split(" "), [s[:5], s[6:]]) @@ -244,14 +246,14 @@ ) self.assertEqual(blue("abcbd").split("b"), [blue("a"), blue("c"), blue("d")]) - def test_split_with_spaces(self): + def test_split_with_spaces(self) -> None: self.assertEqual(blue("a\nb").split(), [blue("a"), blue("b")]) self.assertEqual(blue("a \t\n\nb").split(), [blue("a"), blue("b")]) self.assertEqual( blue("hello \t\n\nthere").split(), [blue("hello"), blue("there")] ) - def test_ljust_rjust(self): + def test_ljust_rjust(self) -> None: b = fmtstr("ab", "blue", "on_red", "bold") g = fmtstr("cd", "green", "on_red", "bold") s = b + g @@ -278,7 +280,7 @@ # formatted string passed in # preserve some non-uniform styles (bold, dark, blink) but not others (underline, invert) - def test_linessplit(self): + def test_linessplit(self) -> None: text = blue("the sum of the squares of the sideways") result = [ blue("the") + blue(" ") + blue("sum"), @@ -290,7 +292,7 @@ ] self.assertEqual(linesplit(text, 7), result) - def test_mul(self): + def test_mul(self) -> None: self.assertEqual(fmtstr("heyhey"), fmtstr("hey") * 2) pass # TODO raise common attributes when doing equality or when @@ -300,17 +302,17 @@ # bold(blue('hey')+green('there')+blue('hey')+green('there')), # bold(blue('hey')+green('there'))*2) - def test_change_color(self): + def test_change_color(self) -> None: a = blue(red("hello")) self.assertEqual(a, blue("hello")) - def test_repr(self): + def test_repr(self) -> None: self.assertEqual(fmtstr("hello", "red", bold=False), red("hello")) self.assertEqual(fmtstr("hello", "red", bold=True), bold(red("hello"))) class TestDoubleUnders(unittest.TestCase): - def test_equality(self): + def test_equality(self) -> None: x = fmtstr("adfs") self.assertEqual(x, x) self.assertTrue(fmtstr("adfs"), fmtstr("adfs")) @@ -318,28 +320,28 @@ class TestConvenience(unittest.TestCase): - def test_fg(self): + def test_fg(self) -> None: red("asdf") blue("asdf") self.assertTrue(True) - def test_bg(self): + def test_bg(self) -> None: on_red("asdf") on_blue("asdf") self.assertTrue(True) - def test_styles(self): + def test_styles(self) -> None: underline("asdf") blink("asdf") self.assertTrue(True) class TestSlicing(unittest.TestCase): - def test_index(self): + def test_index(self) -> None: self.assertEqual(fmtstr("Hi!", "blue")[0], fmtstr("H", "blue")) self.assertRaises(IndexError, fmtstr("Hi!", "blue").__getitem__, 5) - def test_slice(self): + def test_slice(self) -> None: self.assertEqual(fmtstr("Hi!", "blue")[1:2], fmtstr("i", "blue")) self.assertEqual(fmtstr("Hi!", "blue")[1:], fmtstr("i!", "blue")) s = fmtstr("imp") + " " @@ -351,7 +353,7 @@ class TestComposition(unittest.TestCase): - def test_simple_composition(self): + def test_simple_composition(self) -> None: a = fmtstr("hello ", "underline", "on_blue") b = fmtstr("there", "red", "on_blue") c = a + b @@ -360,21 +362,21 @@ class TestUnicode(unittest.TestCase): - def test_output_type(self): + def test_output_type(self) -> None: self.assertEqual(type(str(fmtstr("hello", "blue"))), str) - def test_normal_chars(self): + def test_normal_chars(self) -> None: fmtstr("a", "blue") str(fmtstr("a", "blue")) self.assertTrue(True) - def test_funny_chars(self): + def test_funny_chars(self) -> None: fmtstr("???", "blue") str(Chunk("???", {"fg": "blue"})) str(fmtstr("???", "blue")) self.assertTrue(True) - def test_right_sequence_in_py3(self): + def test_right_sequence_in_py3(self) -> None: red_on_blue = fmtstr("hello", "red", "on_blue") blue_on_red = fmtstr("there", fg="blue", bg="red") green_s = fmtstr("!", "green") @@ -387,7 +389,7 @@ "\x1b[31m\x1b[44mhello\x1b[49m\x1b[39m \x1b[34m\x1b[41mthere\x1b[49m\x1b[39m\x1b[32m!\x1b[39m", ) - def test_len_of_unicode(self): + def test_len_of_unicode(self) -> None: self.assertEqual(len(fmtstr("??????")), 2) lines = ["??????", "an", "??????"] r = fsarray(lines) @@ -398,7 +400,7 @@ # always coerce everything to unicode? # self.assertEqual(len(fmtstr('??????')), 2) - def test_len_of_unicode_in_fsarray(self): + def test_len_of_unicode_in_fsarray(self) -> None: fsa = FSArray(3, 2) fsa.rows[0] = fsa.rows[0].setslice_with_length(0, 2, "??????", 2) @@ -406,7 +408,7 @@ fsa.rows[0] = fsa.rows[0].setslice_with_length(0, 2, fmtstr("??????", "blue"), 2) self.assertEqual(fsa.shape, (3, 2)) - def test_add_unicode_to_byte(self): + def test_add_unicode_to_byte(self) -> None: fmtstr("???") + fmtstr("a") fmtstr("a") + fmtstr("???") "???" + fmtstr("???") @@ -414,30 +416,30 @@ fmtstr("???") + "???" fmtstr("a") + "???" - def test_unicode_slicing(self): + def test_unicode_slicing(self) -> None: self.assertEqual(fmtstr("???adfs", "blue")[:2], fmtstr("???a", "blue")) self.assertEqual( type(fmtstr("???adfs", "blue")[:2].s), type(fmtstr("???a", "blue").s) ) self.assertEqual(len(fmtstr("???adfs", "blue")[:2]), 2) - def test_unicode_repr(self): + def test_unicode_repr(self) -> None: repr(Chunk("???")) self.assertEqual(repr(fmtstr("???")), repr_without_leading_u("???")) class TestCharacterWidth(unittest.TestCase): - def test_doublewide_width(self): + def test_doublewide_width(self) -> None: self.assertEqual(len(fmtstr("???", "blue")), 1) self.assertEqual(fmtstr("???", "blue").width, 2) self.assertEqual(len(fmtstr("??????")), 2) self.assertEqual(fmtstr("??????").width, 4) - def test_multi_width(self): + def test_multi_width(self) -> None: self.assertEqual(len(fmtstr("a\u0300")), 2) self.assertEqual(fmtstr("a\u0300").width, 1) - def test_width_aware_slice(self): + def test_width_aware_slice(self) -> None: self.assertEqual(fmtstr("???").width_aware_slice(slice(None, 1, None)).s, " ") self.assertEqual(fmtstr("???").width_aware_slice(slice(None, 2, None)).s, "???") self.assertEqual( @@ -449,7 +451,7 @@ fmtstr("???", "blue"), ) - def test_width_aware_splitlines(self): + def test_width_aware_splitlines(self) -> None: s = fmtstr("abcd") self.assertEqual( list(s.width_aware_splitlines(2)), [fmtstr("ab"), fmtstr("cd")] @@ -469,7 +471,7 @@ with self.assertRaises(ValueError): s.width_aware_splitlines(1) - def test_width_at_offset(self): + def test_width_at_offset(self) -> None: self.assertEqual(fmtstr("ab???cdef").width_at_offset(0), 0) self.assertEqual(fmtstr("ab???cdef").width_at_offset(2), 2) self.assertEqual(fmtstr("ab???cdef").width_at_offset(3), 4) @@ -482,7 +484,7 @@ class TestWidthHelpers(unittest.TestCase): - def test_combining_char_aware_slice(self): + def test_combining_char_aware_slice(self) -> None: self.assertEqual(width_aware_slice("abc", 0, 2), "ab") self.assertEqual(width_aware_slice("abc", 1, 3), "bc") self.assertEqual(width_aware_slice("abc", 0, 3), "abc") @@ -493,7 +495,7 @@ self.assertEqual(width_aware_slice("ab\u0300\u0300c", 0, 2), "ab\u0300\u0300") self.assertEqual(width_aware_slice("ab\u0300\u0300c", 2, 3), "c") - def test_char_width_aware_slice(self): + def test_char_width_aware_slice(self) -> None: self.assertEqual(width_aware_slice("abc", 1, 2), "b") self.assertEqual(width_aware_slice("a???bc", 0, 4), "a???b") self.assertEqual(width_aware_slice("a???bc", 1, 4), "???b") @@ -502,19 +504,19 @@ class TestChunk(unittest.TestCase): - def test_repr(self): + def test_repr(self) -> None: c = Chunk("a", {"fg": 32}) self.assertEqual(repr(c), """Chunk('a', {'fg': 32})""") class TestChunkSplitter(unittest.TestCase): - def test_chunk_splitter(self): + def test_chunk_splitter(self) -> None: splitter = Chunk("asdf", {"fg": 32}).splitter() self.assertEqual(splitter.request(1), (1, Chunk("a", {"fg": 32}))) self.assertEqual(splitter.request(4), (3, Chunk("sdf", {"fg": 32}))) self.assertEqual(splitter.request(4), None) - def test_reusing_same_splitter(self): + def test_reusing_same_splitter(self) -> None: c = Chunk("asdf", {"fg": 32}) s1 = c.splitter() self.assertEqual(s1.request(3), (3, Chunk("asd", {"fg": 32}))) @@ -527,8 +529,8 @@ s1.reinit(c2) self.assertEqual(s1.request(3), (3, Chunk("abc"))) - def test_width_awareness(self): - s = Chunk("asdf") + def test_width_awareness(self) -> None: + Chunk("asdf") self.assertEqual( Chunk("ab\u0300c").splitter().request(3), (3, Chunk("ab\u0300c")) ) @@ -550,19 +552,19 @@ class TestFSArray(unittest.TestCase): - def test_no_hanging_space(self): + def test_no_hanging_space(self) -> None: a = FSArray(4, 2) self.assertEqual(len(a.rows[0]), 0) - def test_assignment_working(self): + def test_assignment_working(self) -> None: t = FSArray(10, 10) t[2, 2] = "a" # TODO: is this supposed to check something? t[2, 2] == "a" - def test_normalize_slice(self): + def test_normalize_slice(self) -> None: class SliceBuilder: - def __getitem__(self, slice): + def __getitem__(self, slice: slice) -> slice: return slice Slice = SliceBuilder() @@ -571,14 +573,13 @@ self.assertEqual(normalize_slice(11, Slice[3:]), slice(3, 11, None)) @skip("TODO") - def test_oomerror(self): + def test_oomerror(self) -> None: a = FSArray(10, 40) a[2:-2, 2:-2] = fsarray(["asdf", "zxcv"]) class FormatStringTest(unittest.TestCase): - def assertFSArraysEqual(self, a, b): - # type: (FSArray, FSArray) -> None + def assertFSArraysEqual(self, a: FSArray, b: FSArray) -> None: self.assertIsInstance(a, FSArray) self.assertIsInstance(b, FSArray) self.assertEqual( @@ -593,8 +594,7 @@ "FSArrays differ first on line {}:\n{}".format(i, FSArray.diff(a, b)), ) - def assertFSArraysEqualIgnoringFormatting(self, a, b): - # type: (FSArray, FSArray) -> None + def assertFSArraysEqualIgnoringFormatting(self, a: FSArray, b: FSArray) -> None: """Also accepts arrays of strings""" self.assertEqual( len(a), @@ -614,7 +614,7 @@ class TestFSArrayWithDiff(FormatStringTest): - def test_diff_testing(self): + def test_diff_testing(self) -> None: a = fsarray(["abc", "def"]) b = fsarray(["abc", "dqf"]) self.assertRaises(AssertionError, self.assertFSArraysEqual, a, b) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/tests/test_input.py new/curtsies-0.4.0/tests/test_input.py --- old/curtsies-0.3.10/tests/test_input.py 2021-09-25 20:39:51.000000000 +0200 +++ new/curtsies-0.4.0/tests/test_input.py 2022-08-28 22:39:24.000000000 +0200 @@ -141,3 +141,9 @@ t = threading.Thread(target=use) t.start() t.join() + + def test_cleanup(self): + input_generator = Input() + for i in range(1000): + with input_generator: + pass diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/tests/test_terminal.py new/curtsies-0.4.0/tests/test_terminal.py --- old/curtsies-0.3.10/tests/test_terminal.py 2021-09-25 20:39:51.000000000 +0200 +++ new/curtsies-0.4.0/tests/test_terminal.py 2022-08-28 22:39:24.000000000 +0200 @@ -7,7 +7,7 @@ from io import StringIO from unittest import skipUnless, skipIf, expectedFailure -import blessings +import blessed import pyte from pyte import control as ctrl, Stream, Screen @@ -88,7 +88,7 @@ pass -@skipUnless(sys.stdin.isatty(), "blessings Terminal needs streams open") +@skipUnless(sys.stdin.isatty(), "blessed Terminal needs streams open") class TestFullscreenWindow(unittest.TestCase): def setUp(self): self.screen = pyte.Screen(10, 3) @@ -121,7 +121,7 @@ pass -@skipUnless(sys.stdin.isatty(), "blessings Terminal needs streams open") +@skipUnless(sys.stdin.isatty(), "blessed Terminal needs streams open") class TestCursorAwareWindow(unittest.TestCase): def setUp(self): self.screen = ReportingScreen(6, 3) @@ -133,8 +133,8 @@ out_stream=stdout, in_stream=self.screen._report_file ) self.window.cbreak = NopContext() - blessings.Terminal.height = 3 - blessings.Terminal.width = 6 + blessed.Terminal.height = 3 + blessed.Terminal.width = 6 # This isn't passing locally for me anymore :/ @expectedFailure @@ -162,7 +162,7 @@ self.assertEqual(self.screen.display, [" ", "hi ", "there "]) -@skipUnless(sys.stdin.isatty(), "blessings Terminal needs streams open") +@skipUnless(sys.stdin.isatty(), "blessed Terminal needs streams open") class TestCursorAwareWindowWithExtraInput(unittest.TestCase): def setUp(self): self.screen = ReportingScreenWithExtra(6, 3) @@ -177,8 +177,8 @@ extra_bytes_callback=self.extra_bytes_callback, ) self.window.cbreak = NopContext() - blessings.Terminal.height = 3 - blessings.Terminal.width = 6 + blessed.Terminal.height = 3 + blessed.Terminal.width = 6 def extra_bytes_callback(self, bytes): self.extra_bytes.append(bytes) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/curtsies-0.3.10/tests/test_window.py new/curtsies-0.4.0/tests/test_window.py --- old/curtsies-0.3.10/tests/test_window.py 2021-09-25 20:39:51.000000000 +0200 +++ new/curtsies-0.4.0/tests/test_window.py 2022-08-28 22:39:24.000000000 +0200 @@ -14,7 +14,7 @@ height = property(lambda self: 4) -@skipIf(fds_closed, "blessings Terminal needs streams open") +@skipIf(fds_closed, "blessed Terminal needs streams open") class TestBaseWindow(unittest.TestCase): """Pretty pathetic tests for window"""