diff --git a/.travis.yml b/.travis.yml index 38fbbcdb..0d5b02e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ matrix: fast_finish: true include: - python: 3.8 - env: TOXENV=about,pydocstyle,pylint,flake8,flake8_tests,sphinx COVERAGE_ID=travis-ci + env: TOXENV=about,pydocstyle,pylint,flake8,flake8_tests,mypy,sphinx COVERAGE_ID=travis-ci - python: 2.6 env: TOXENV=py26,codecov TEST_QUICK=1 COVERAGE_ID=travis-ci dist: trusty diff --git a/MANIFEST.in b/MANIFEST.in index 1ee1b0b5..3c89951b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,5 +4,7 @@ global-exclude *.py[cod] __pycache__ include LICENSE include version.json include *.txt +include mypy.ini include tox.ini +recursive-include blessed *.pyi py.typed recursive-include tests *.py *.ans diff --git a/blessed/__init__.py b/blessed/__init__.py index 3391c216..5477d20d 100644 --- a/blessed/__init__.py +++ b/blessed/__init__.py @@ -10,7 +10,7 @@ if _platform.system() == 'Windows': from blessed.win_terminal import Terminal else: - from blessed.terminal import Terminal + from blessed.terminal import Terminal # type: ignore if (3, 0, 0) <= _sys.version_info[:3] < (3, 2, 3): # Good till 3.2.10 diff --git a/blessed/_capabilities.py b/blessed/_capabilities.py index 8b109735..05cc005a 100644 --- a/blessed/_capabilities.py +++ b/blessed/_capabilities.py @@ -6,7 +6,7 @@ from collections import OrderedDict except ImportError: # python 2.6 requires 3rd party library (backport) - from ordereddict import OrderedDict # pylint: disable=import-error + from ordereddict import OrderedDict # type: ignore # pylint: disable=import-error __all__ = ( 'CAPABILITY_DATABASE', diff --git a/blessed/_capabilities.pyi b/blessed/_capabilities.pyi new file mode 100644 index 00000000..7fd25ff5 --- /dev/null +++ b/blessed/_capabilities.pyi @@ -0,0 +1,6 @@ +from typing import Any, Dict, OrderedDict, Tuple + +CAPABILITY_DATABASE: OrderedDict[str, Tuple[str, Dict[str, Any]]] +CAPABILITIES_RAW_MIXIN: Dict[str, str] +CAPABILITIES_ADDITIVES: Dict[str, Tuple[str, str]] +CAPABILITIES_CAUSE_MOVEMENT: Tuple[str, ...] diff --git a/blessed/color.pyi b/blessed/color.pyi new file mode 100644 index 00000000..b04be034 --- /dev/null +++ b/blessed/color.pyi @@ -0,0 +1,16 @@ +from typing import Callable, Dict, Tuple + +_RGB = Tuple[int, int, int] + +def rgb_to_xyz(red: int, green: int, blue: int) -> Tuple[float, float, float]: ... +def xyz_to_lab( + x_val: float, y_val: float, z_val: float +) -> Tuple[float, float, float]: ... +def rgb_to_lab(red: int, green: int, blue: int) -> Tuple[float, float, float]: ... +def dist_rgb(rgb1: _RGB, rgb2: _RGB) -> float: ... +def dist_rgb_weighted(rgb1: _RGB, rgb2: _RGB) -> float: ... +def dist_cie76(rgb1: _RGB, rgb2: _RGB) -> float: ... +def dist_cie94(rgb1: _RGB, rgb2: _RGB) -> float: ... +def dist_cie2000(rgb1: _RGB, rgb2: _RGB) -> float: ... + +COLOR_DISTANCE_ALGORITHMS: Dict[str, Callable[[_RGB, _RGB], float]] diff --git a/blessed/colorspace.pyi b/blessed/colorspace.pyi new file mode 100644 index 00000000..a59899cb --- /dev/null +++ b/blessed/colorspace.pyi @@ -0,0 +1,11 @@ +from typing import Dict, NamedTuple, Set, Tuple + +CGA_COLORS: Set[str] + +class RGBColor(NamedTuple): + red: int + green: int + blue: int + +X11_COLORNAMES_TO_RGB: Dict[str, RGBColor] +RGB_256TABLE: Tuple[RGBColor, ...] diff --git a/blessed/formatters.pyi b/blessed/formatters.pyi new file mode 100644 index 00000000..bf5c7c9d --- /dev/null +++ b/blessed/formatters.pyi @@ -0,0 +1,70 @@ +from typing import ( + Any, + Callable, + List, + NoReturn, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, + overload, +) + +from .terminal import Terminal + +COLORS = Set[str] +COMPOUNDABLES = Set[str] + +_T = TypeVar("_T") + +class ParameterizingString(str): + def __new__(cls: Type[_T], cap: str, normal: str = ..., name: str = ...) -> _T: ... + @overload + def __call__( + self, *args: int + ) -> Union["FormattingString", "NullCallableString"]: ... + @overload + def __call__(self, *args: str) -> NoReturn: ... + +class ParameterizingProxyString(str): + def __new__( + cls: Type[_T], + fmt_pair: Tuple[str, Callable[..., Tuple[object, ...]]], + normal: str = ..., + name: str = ..., + ) -> _T: ... + def __call__(self, *args: Any) -> "FormattingString": ... + +class FormattingString(str): + def __new__(cls: Type[_T], sequence: str, normal: str = ...) -> _T: ... + @overload + def __call__(self, *args: int) -> NoReturn: ... + @overload + def __call__(self, *args: str) -> str: ... + +class FormattingOtherString(str): + def __new__( + cls: Type[_T], direct: ParameterizingString, target: ParameterizingString = ... + ) -> _T: ... + def __call__(self, *args: Union[int, str]) -> str: ... + +class NullCallableString(str): + def __new__(cls: Type[_T]) -> _T: ... + @overload + def __call__(self, *args: int) -> "NullCallableString": ... + @overload + def __call__(self, *args: str) -> str: ... + +def get_proxy_string( + term: Terminal, attr: str +) -> Optional[ParameterizingProxyString]: ... +def split_compound(compound: str) -> List[str]: ... +def resolve_capability(term: Terminal, attr: str) -> str: ... +def resolve_color( + term: Terminal, color: str +) -> Union[NullCallableString, FormattingString]: ... +def resolve_attribute( + term: Terminal, attr: str +) -> Union[ParameterizingString, FormattingString]: ... diff --git a/blessed/keyboard.pyi b/blessed/keyboard.pyi new file mode 100644 index 00000000..cbd035e4 --- /dev/null +++ b/blessed/keyboard.pyi @@ -0,0 +1,26 @@ +from typing import Dict, Iterable, Mapping, OrderedDict, Optional, Set, Type, TypeVar + +from .terminal import Terminal + +_T = TypeVar("_T") + +class Keystroke(str): + def __new__( + cls: Type[_T], + ucs: str = ..., + code: Optional[int] = ..., + name: Optional[str] = ..., + ) -> _T: ... + @property + def is_sequence(self) -> bool: ... + @property + def name(self) -> Optional[str]: ... + @property + def code(self) -> Optional[int]: ... + +def get_keyboard_codes() -> Dict[int, str]: ... +def get_keyboard_sequences(term: Terminal) -> OrderedDict[str, int]: ... +def get_leading_prefixes(sequences: Iterable[str]) -> Set[str]: ... +def resolve_sequence( + text: str, mapper: Mapping[str, int], codes: Mapping[int, str] +) -> Keystroke: ... diff --git a/blessed/py.typed b/blessed/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/blessed/sequences.pyi b/blessed/sequences.pyi new file mode 100644 index 00000000..f9225c0a --- /dev/null +++ b/blessed/sequences.pyi @@ -0,0 +1,52 @@ +import textwrap +from typing import Any, Iterator, Optional, Pattern, Tuple, Type, TypeVar + +from .terminal import Terminal + +_T = TypeVar("_T") + +class Termcap: + name: str = ... + pattern: str = ... + attribute: str = ... + def __init__(self, name: str, pattern: str, attribute: str) -> None: ... + @property + def named_pattern(self) -> str: ... + @property + def re_compiled(self) -> Pattern[str]: ... + @property + def will_move(self) -> bool: ... + def horizontal_distance(self, text: str) -> int: ... + @classmethod + def build( + cls, + name: str, + capability: str, + attribute: str, + nparams: int = ..., + numeric: int = ..., + match_grouped: bool = ..., + match_any: bool = ..., + match_optional: bool = ..., + ) -> "Termcap": ... + +class SequenceTextWrapper(textwrap.TextWrapper): + term: Terminal = ... + def __init__(self, width: int, term: Terminal, **kwargs: Any) -> None: ... + +class Sequence(str): + def __new__(cls: Type[_T], sequence_text: str, term: Terminal) -> _T: ... + def ljust(self, width: int, fillchar: str = ...) -> str: ... + def rjust(self, width: int, fillchar: str = ...) -> str: ... + def center(self, width: int, fillchar: str = ...) -> str: ... + def length(self) -> int: ... + def strip(self, chars: Optional[str] = ...) -> str: ... + def lstrip(self, chars: Optional[str] = ...) -> str: ... + def rstrip(self, chars: Optional[str] = ...) -> str: ... + def strip_seqs(self) -> str: ... + def padd(self, strip: bool = ...) -> str: ... + +def iter_parse( + term: Terminal, text: str +) -> Iterator[Tuple[str, Optional[Termcap]]]: ... +def measure_length(text: str, term: Terminal) -> int: ... diff --git a/blessed/terminal.pyi b/blessed/terminal.pyi new file mode 100644 index 00000000..bb4488e9 --- /dev/null +++ b/blessed/terminal.pyi @@ -0,0 +1,105 @@ +from typing import Any, ContextManager, IO, List, Optional, OrderedDict, Tuple, Union + +from .formatters import ( + FormattingOtherString, + FormattingString, + NullCallableString, + ParameterizingString, +) +from .keyboard import Keystroke +from .sequences import Termcap + +HAS_TTY: bool + +class Terminal: + caps: OrderedDict[str, Termcap] + errors: List[str] = ... + def __init__( + self, + kind: Optional[str] = ..., + stream: Optional[IO[str]] = ..., + force_styling: bool = ..., + ) -> None: ... + def __getattr__( + self, attr: str + ) -> Union[NullCallableString, ParameterizingString, FormattingString]: ... + @property + def kind(self) -> str: ... + @property + def does_styling(self) -> bool: ... + @property + def is_a_tty(self) -> bool: ... + @property + def height(self) -> int: ... + @property + def width(self) -> int: ... + @property + def pixel_height(self) -> int: ... + @property + def pixel_width(self) -> int: ... + def location( + self, x: Optional[int] = ..., y: Optional[int] = ... + ) -> ContextManager[None]: ... + def get_location(self, timeout: Optional[float] = ...) -> Tuple[int, int]: ... + def fullscreen(self) -> ContextManager[None]: ... + def hidden_cursor(self) -> ContextManager[None]: ... + def move_xy(self, x: int, y: int) -> ParameterizingString: ... + def move_yx(self, y: int, x: int) -> ParameterizingString: ... + @property + def move_left(self) -> FormattingOtherString: ... + @property + def move_right(self) -> FormattingOtherString: ... + @property + def move_up(self) -> FormattingOtherString: ... + @property + def move_down(self) -> FormattingOtherString: ... + @property + def color(self) -> Union[NullCallableString, ParameterizingString]: ... + def color_rgb(self, red: int, green: int, blue: int) -> FormattingString: ... + @property + def on_color(self) -> Union[NullCallableString, ParameterizingString]: ... + def on_color_rgb(self, red: int, green: int, blue: int) -> FormattingString: ... + def formatter(self, value: str) -> Union[NullCallableString, FormattingString]: ... + def rgb_downconvert(self, red: int, green: int, blue: int) -> int: ... + @property + def normal(self) -> str: ... + def link(self, url: str, text: str, url_id: str = ...) -> str: ... + @property + def stream(self) -> IO[str]: ... + @property + def number_of_colors(self) -> int: ... + @number_of_colors.setter + def number_of_colors(self, value: int) -> None: ... + @property + def color_distance_algorithm(self) -> str: ... + @color_distance_algorithm.setter + def color_distance_algorithm(self, value: str) -> None: ... + def ljust( + self, text: str, width: Optional[int] = ..., fillchar: str = ... + ) -> str: ... + def rjust( + self, text: str, width: Optional[int] = ..., fillchar: str = ... + ) -> str: ... + def center( + self, text: str, width: Optional[int] = ..., fillchar: str = ... + ) -> str: ... + def length(self, text: str) -> int: ... + def strip(self, text: str, chars: Optional[str] = ...) -> str: ... + def rstrip(self, text: str, chars: Optional[str] = ...) -> str: ... + def lstrip(self, text: str, chars: Optional[str] = ...) -> str: ... + def strip_seqs(self, text: str) -> str: ... + def split_seqs(self, text: str, **kwds: Any) -> List[str]: ... + def wrap( + self, text: str, width: Optional[int] = ..., **kwargs: Any + ) -> List[str]: ... + def getch(self) -> str: ... + def ungetch(self, text: str) -> None: ... + def kbhit(self, timeout: Optional[float] = ...) -> bool: ... + def cbreak(self) -> ContextManager[None]: ... + def raw(self) -> ContextManager[None]: ... + def keypad(self) -> ContextManager[None]: ... + def inkey( + self, timeout: Optional[float] = ..., esc_delay: float = ... + ) -> Keystroke: ... + +class WINSZ: ... diff --git a/blessed/win_terminal.pyi b/blessed/win_terminal.pyi new file mode 100644 index 00000000..cd0368b0 --- /dev/null +++ b/blessed/win_terminal.pyi @@ -0,0 +1,8 @@ +from typing import ContextManager, Optional +from .terminal import Terminal as _Terminal + +class Terminal(_Terminal): + def getch(self) -> str: ... + def kbhit(self, timeout: Optional[float] = ...) -> bool: ... + def cbreak(self) -> ContextManager[None]: ... + def raw(self) -> ContextManager[None]: ... diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..58653a51 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,13 @@ +[mypy] + +[mypy-curses.has_key.*] +ignore_missing_imports = True + +[mypy-jinxed.*] +ignore_missing_imports = True + +[mypy-ordereddict.*] +ignore_missing_imports = True + +[mypy-wcwidth.*] +ignore_missing_imports = True diff --git a/setup.py b/setup.py index 9288dec1..b0c6d0f9 100755 --- a/setup.py +++ b/setup.py @@ -41,10 +41,23 @@ def _get_long_description(fname): author_email='contact@jeffquast.com', license='MIT', packages=['blessed', ], + package_data={ + 'blessed': [ + 'py.typed', + '_capabilities.pyi', + 'color.pyi', + 'colorspace.pyi', + 'formatters.pyi', + 'keyboard.pyi', + 'sequences.pyi', + 'terminal.pyi', + 'win_terminal.pyi', + ], + }, url='https://github.com/jquast/blessed', project_urls={'Documentation': 'https://blessed.readthedocs.io'}, include_package_data=True, - zip_safe=True, + zip_safe=False, classifiers=[ 'Intended Audience :: Developers', 'Natural Language :: English', diff --git a/tests/accessories.py b/tests/accessories.py index 73a6eedd..d31591d2 100644 --- a/tests/accessories.py +++ b/tests/accessories.py @@ -12,6 +12,11 @@ import traceback import contextlib import subprocess +try: + from typing import Callable +except ImportError: # py2 + pass + # 3rd party import six @@ -31,7 +36,7 @@ test_kind = 'xterm-256color' if platform.system() == 'Windows': test_kind = 'vtwin10' -TestTerminal = functools.partial(Terminal, kind=test_kind) +TestTerminal = functools.partial(Terminal, kind=test_kind) # type: Callable[..., Terminal] SEND_SEMAPHORE = SEMAPHORE = b'SEMAPHORE\n' RECV_SEMAPHORE = b'SEMAPHORE\r\n' many_lines_params = [40, 80] diff --git a/tests/test_core.py b/tests/test_core.py index 21b309a0..b94fd544 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -191,8 +191,7 @@ def child(): try: # a second instantiation raises UserWarning term = TestTerminal(kind=next_kind, force_styling=True) - except UserWarning: - err = sys.exc_info()[1] + except UserWarning as err: assert (err.args[0].startswith( 'A terminal of kind "' + next_kind + '" has been requested') ), err.args[0] @@ -219,8 +218,7 @@ def child(): try: term = TestTerminal(kind='unknown', force_styling=True) - except UserWarning: - err = sys.exc_info()[1] + except UserWarning as err: assert err.args[0] in ( "Failed to setupterm(kind='unknown'): " "setupterm: could not find terminal", @@ -430,8 +428,7 @@ def __import__(name, *args, **kwargs): try: import blessed.terminal reload_module(blessed.terminal) - except UserWarning: - err = sys.exc_info()[1] + except UserWarning as err: assert err.args[0] == blessed.terminal._MSG_NOSUPPORT warnings.filterwarnings("ignore", category=UserWarning) diff --git a/tests/test_formatters.py b/tests/test_formatters.py index 35ac3c8d..44f70dd2 100644 --- a/tests/test_formatters.py +++ b/tests/test_formatters.py @@ -479,7 +479,7 @@ def tparm(*args): pstr = ParameterizingString(u'cap', u'norm', u'seq-name') - value = pstr(u'x') + value = pstr(0) assert isinstance(value, NullCallableString) diff --git a/tests/test_sequences.py b/tests/test_sequences.py index 6da0b612..01881238 100644 --- a/tests/test_sequences.py +++ b/tests/test_sequences.py @@ -471,21 +471,18 @@ def child(kind): try: t.bold_misspelled('hey') assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError: - e = sys.exc_info()[1] + except TypeError as e: assert 'Unknown terminal capability,' in e.args[0] try: t.bold_misspelled(u'hey') # unicode assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError: - e = sys.exc_info()[1] + except TypeError as e: assert 'Unknown terminal capability,' in e.args[0] try: t.bold_misspelled(None) # an arbitrary non-string assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError: - e = sys.exc_info()[1] + except TypeError as e: assert 'Unknown terminal capability,' not in e.args[0] if platform.python_implementation() != 'PyPy': @@ -493,8 +490,7 @@ def child(kind): try: t.bold_misspelled('a', 'b') # >1 string arg assert not t.is_a_tty or False, 'Should have thrown exception' - except TypeError: - e = sys.exc_info()[1] + except TypeError as e: assert 'Unknown terminal capability,' in e.args[0], e.args child(all_terms) diff --git a/tox.ini b/tox.ini index 76b19f7b..77052352 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ envlist = about flake8 flake8_tests pydocstyle + mypy sphinx py{26,27,34,35,36,37,38} skip_missing_interpreters = true @@ -206,6 +207,10 @@ commands = {envbindir}/pydocstyle --source --explain {toxinidir}/blessed {envbindir}/rst-lint README.rst {envbindir}/doc8 --ignore-path docs/_build --ignore D000 docs +[testenv:mypy] +deps = mypy +commands = {envpython} -m mypy --strict {toxinidir}/blessed + [testenv:sphinx] deps = -r {toxinidir}/docs/requirements.txt commands = {envbindir}/sphinx-build {posargs:-v -W -d {toxinidir}/docs/_build/doctrees -b html docs {toxinidir}/docs/_build/html}