From a65b4c70a74242cc9bc02687a0b8ee20fd2c0fad Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 4 Jul 2026 09:16:05 +0530 Subject: [PATCH] Migrate type checker to ty Much faster than mypy. Matches usage of ruff from same developer. --- .github/workflows/ci.yml | 12 +++++------ bypy/rsync.conf | 2 +- docs/conf.py | 8 +++---- gen/config.py | 3 ++- gen/go_code.py | 2 +- kittens/broadcast/main.py | 4 ++-- kittens/hints/main.py | 5 +++-- kittens/tui/handler.py | 11 +++++----- kittens/tui/images.py | 6 ++++-- kittens/tui/path_completer.py | 2 +- kitty/actions.py | 4 ++-- kitty/boss.py | 9 +++++--- kitty/cli.py | 2 ++ kitty/clipboard.py | 4 ++-- kitty/colors.py | 8 ++++--- kitty/conf/generate.py | 8 +++++++ kitty/conf/utils.py | 2 +- kitty/config.py | 4 ++-- kitty/constants.py | 22 ++++--------------- kitty/debug_config.py | 20 ++++++++++++------ kitty/file_transmission.py | 10 ++++----- kitty/fonts/__init__.py | 7 +++--- kitty/fonts/common.py | 25 +++++++++++----------- kitty/fonts/core_text.py | 6 +++--- kitty/fonts/fontconfig.py | 6 +++--- kitty/guess_mime_type.py | 2 +- kitty/layout/base.py | 5 +++-- kitty/layout/grid.py | 32 ++++++++++++++-------------- kitty/layout/splits.py | 26 +++++++++++------------ kitty/layout/stack.py | 12 +++++------ kitty/layout/tall.py | 40 +++++++++++++++++------------------ kitty/layout/vertical.py | 26 +++++++++++------------ kitty/main.py | 1 - kitty/multiprocessing.py | 20 +++--------------- kitty/notifications.py | 5 +++-- kitty/options/types.py | 2 +- kitty/rc/scroll_window.py | 12 ++++++----- kitty/rc/set_spacing.py | 4 ++-- kitty/rc/set_window_title.py | 7 +++--- kitty/search_query_parser.py | 6 +++--- kitty/session.py | 4 ++-- kitty/types.py | 8 ++----- kitty_tests/main.py | 10 +-------- pyproject.toml | 35 +++++++++--------------------- setup.py | 7 ++++-- 45 files changed, 218 insertions(+), 238 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5439ec9d..f04ecf280 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,15 +23,15 @@ jobs: cc: [gcc, clang] include: - python: a - pyver: "3.11" + pyver: "3.12" sanitize: 0 - python: b - pyver: "3.12" + pyver: "3.13" sanitize: 1 - python: c - pyver: "3.13" + pyver: "3.14" sanitize: 1 @@ -106,7 +106,7 @@ jobs: ${{ runner.os }}-golang-static- - name: Install build-only deps - run: python -m pip install -r docs/requirements.txt ruff mypy types-requests types-docutils types-Pygments + run: python -m pip install -r docs/requirements.txt ruff ty types-requests types-docutils types-Pygments - name: Run ruff run: ruff check . @@ -120,8 +120,8 @@ jobs: - name: Build kitty run: python setup.py build --debug - - name: Run mypy - run: which python && python -m mypy --version && ./test.py mypy + - name: Run type checking + run: which python && ./test.py type-check - name: Run go vet run: go version && go vet -tags testing ./... diff --git a/bypy/rsync.conf b/bypy/rsync.conf index 71603af93..a3005ce8d 100644 --- a/bypy/rsync.conf +++ b/bypy/rsync.conf @@ -1 +1 @@ -to_vm_excludes '/dependencies /build /dist /kitty/launcher/kitty* /.build-cache /.cache /.mypy_cache /.ruff_cache /tags __pycache__ /*_commands.json *.so *.pyd *.pyc *_generated.go' +to_vm_excludes '/dependencies /build /dist /kitty/launcher/kitty* /.build-cache /.cache /.ruff_cache /tags __pycache__ /*_commands.json *.so *.pyd *.pyc *_generated.go' diff --git a/docs/conf.py b/docs/conf.py index 016fa5d3a..41d8cf706 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,7 +17,7 @@ from functools import lru_cache, partial from typing import Any, Callable, Dict, Iterable, Iterator, List, Tuple from docutils import nodes -from docutils.parsers.rst.roles import set_classes +from docutils.parsers.rst.roles import normalize_options from pygments.lexer import RegexLexer from pygments.lexer import bygroups as untyped_bygroups from pygments.token import Comment, Error, Keyword, Literal, Name, Number, String, Whitespace @@ -221,7 +221,7 @@ def commit_role( prb = inliner.problematic(rawtext, rawtext, msg) return [prb], [msg] url = f'https://github.com/kovidgoyal/kitty/commit/{commit_id}' - set_classes(options) + normalize_options(options) short_id = subprocess.check_output( f'git rev-list --max-count=1 --abbrev-commit {commit_id}'.split()).decode('utf-8').strip() node = nodes.reference(rawtext, f'commit: {short_id}', refuri=url, **options) @@ -498,7 +498,7 @@ def link_role( return [prb], [msg] text, url = m.group(1, 2) url = url.replace(' ', '') - set_classes(options) + normalize_options(options) node = nodes.reference(rawtext, text, refuri=url, **options) return [node], [] @@ -672,7 +672,7 @@ def monkeypatch_man_writer() -> None: th += "\n" sh_tmpl: str = (".SH Name\n" "%(ktitle)s \\- %(subtitle)s\n") - return th + sh_tmpl % di # type: ignore + return th + sh_tmpl % di setattr(ManualPageTranslator, 'header', header) diff --git a/gen/config.py b/gen/config.py index 7feefd6ef..4659cf4b5 100755 --- a/gen/config.py +++ b/gen/config.py @@ -6,6 +6,7 @@ import os import re import subprocess import sys +import types from kitty.conf.generate import write_output @@ -45,7 +46,7 @@ def main(args: list[str]=sys.argv) -> None: all_colors = [] special_colors = [] for opt in definition.iter_all_options(): - if callable(opt.parser_func): + if isinstance(opt.parser_func, types.FunctionType): match opt.parser_func.__name__: case 'to_color': all_colors.append(opt.name) diff --git a/gen/go_code.py b/gen/go_code.py index 325502a5e..f391edab0 100755 --- a/gen/go_code.py +++ b/gen/go_code.py @@ -605,7 +605,7 @@ def load_ref_map() -> dict[str, dict[str, str]]: raw = f.read() raw = raw.split('{', 1)[1].split('}', 1)[0] data = json.loads(bytes(bytearray(json.loads(f'[{raw}]')))) - return data # type: ignore + return data def generate_constants() -> str: diff --git a/kittens/broadcast/main.py b/kittens/broadcast/main.py index c76f54b0f..57f112c60 100644 --- a/kittens/broadcast/main.py +++ b/kittens/broadcast/main.py @@ -51,8 +51,8 @@ class Broadcast(Handler): self.cmd.clear_to_end_of_screen() self.line_edit.write(self.write, screen_cols=self.screen_size.cols) - def on_resize(self, screen_size: ScreenSize) -> None: - super().on_resize(screen_size) + def on_resize(self, new_size: ScreenSize) -> None: + super().on_resize(new_size) self.commit_line() def on_text(self, text: str, in_bracketed_paste: bool = False) -> None: diff --git a/kittens/hints/main.py b/kittens/hints/main.py index da69fa898..887698d23 100644 --- a/kittens/hints/main.py +++ b/kittens/hints/main.py @@ -297,7 +297,7 @@ def linenum_handle_result(args: list[str], data: dict[str, Any], target_window_i def is_copy_action(s: str) -> bool: return s in ('-', '@', '*') or s.startswith('@') - programs = list(filter(is_copy_action, data['programs'] or ())) + programs = list(filter(is_copy_action, data['programs'] or [])) # keep for backward compatibility, previously option `--program` does not need to be specified to perform copy actions if is_copy_action(cmd[0]): programs.append(cmd.pop(0)) @@ -412,7 +412,8 @@ def handle_result(args: list[str], data: dict[str, Any], target_window_id: int, w = boss.window_id_map.get(target_window_id) boss.call_remote_control(self_window=w, args=tuple(launch_args + ([m] if isinstance(m, str) else m))) else: - boss.open_url(m, program, cwd=cwd) + if isinstance(m, str): + boss.open_url(m, program, cwd=cwd) if __name__ == '__main__': diff --git a/kittens/tui/handler.py b/kittens/tui/handler.py index 9f271a636..3cca6c089 100644 --- a/kittens/tui/handler.py +++ b/kittens/tui/handler.py @@ -102,7 +102,7 @@ class KittenUI: raise ValueError('Remote control is not enabled, remember to use allow_remote_control=True') prefix = [kitten_exe(), '@'] r = -1 - pass_fds = list(kw.get('pass_fds') or ()) + pass_fds = list(kw.get('pass_fds') or []) try: if self.rc_fd > -1: pass_fds.append(self.rc_fd) @@ -200,12 +200,13 @@ class Handler: self.debug.fobj = self self.initialize() - def __exit__(self, etype: type, value: Exception, tb: TracebackType) -> None: + def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, tb: TracebackType | None) -> bool | None: del self.debug.fobj with suppress(Exception): self.finalize() if self._image_manager is not None: - self._image_manager.__exit__(etype, value, tb) + self._image_manager.__exit__(exc_type, exc_val, tb) + return None def initialize(self) -> None: pass @@ -213,8 +214,8 @@ class Handler: def finalize(self) -> None: pass - def on_resize(self, screen_size: ScreenSize) -> None: - self.screen_size = screen_size + def on_resize(self, new_size: ScreenSize) -> None: + self.screen_size = new_size def quit_loop(self, return_code: int | None = None) -> None: self._tui_loop.quit(return_code) diff --git a/kittens/tui/images.py b/kittens/tui/images.py index 007e63d3e..183cf2b14 100644 --- a/kittens/tui/images.py +++ b/kittens/tui/images.py @@ -10,7 +10,7 @@ from collections.abc import Callable, Iterator, Sequence from contextlib import suppress from enum import IntEnum from itertools import count -from typing import Any, ClassVar, DefaultDict, Deque, Generic, Optional, TypeVar, Union, cast +from typing import Any, ClassVar, DefaultDict, Deque, Generic, Optional, TypeVar, Union, cast, final from kitty.conf.utils import positive_float, positive_int from kitty.fast_data_types import create_canvas @@ -343,6 +343,7 @@ class Alias(Generic[T]): self.name = Alias.currently_processing +@final class GraphicsCommand: a = action = Alias('t') q = quiet = Alias(0) @@ -467,12 +468,13 @@ class ImageManager: gc.t = 'f' self.handler.cmd.gr_command(gc, standard_b64encode(f.name.encode(fsenc))) - def __exit__(self, *a: Any) -> None: + def __exit__(self, *a: Any) -> bool | None: import shutil shutil.rmtree(self.tdir, ignore_errors=True) self.handler.cmd.clear_images_on_screen(delete_data=True) self.delete_all_sent_images() del self.handler + return None def delete_all_sent_images(self) -> None: gc = GraphicsCommand() diff --git a/kittens/tui/path_completer.py b/kittens/tui/path_completer.py index a19208b4d..ad5f1f1ed 100644 --- a/kittens/tui/path_completer.py +++ b/kittens/tui/path_completer.py @@ -84,7 +84,7 @@ class PathCompleter: import readline from .dircolors import Dircolors - if 'libedit' in readline.__doc__: + if 'libedit' in getattr(readline, '__doc__', '') or '': readline.parse_and_bind("bind -e") readline.parse_and_bind("bind '\t' rl_complete") else: diff --git a/kitty/actions.py b/kitty/actions.py index ab3f9acbd..b04f2648a 100644 --- a/kitty/actions.py +++ b/kitty/actions.py @@ -2,7 +2,7 @@ # License: GPLv3 Copyright: 2021, Kovid Goyal import inspect -from typing import NamedTuple +from typing import NamedTuple, cast from .boss import Boss from .tabs import Tab @@ -50,7 +50,7 @@ def get_all_actions() -> dict[ActionGroup, list[Action]]: short_help = first long_help = '\n'.join(lines).strip() assert spec.group in groups - return Action(getattr(x, '__name__'), spec.group, short_help, long_help) + return Action(getattr(x, '__name__'), cast(ActionGroup, spec.group), short_help, long_help) seen = set() for cls in (Window, Tab, Boss): diff --git a/kitty/boss.py b/kitty/boss.py index 8ce021a4b..c1b7b4796 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -22,6 +22,7 @@ from typing import ( Literal, Optional, Union, + cast, ) from weakref import WeakValueDictionary @@ -899,7 +900,8 @@ class Boss: for x in payload: c.response_from_kitty(self, self_window, PayloadGetter(c, x if isinstance(x, dict) else {})) return None - return c.response_from_kitty(self, self_window, PayloadGetter(c, payload if isinstance(payload, dict) else {})) + pd = cast(dict[str, Any], payload if isinstance(payload, dict) else {}) + return c.response_from_kitty(self, self_window, PayloadGetter(c, pd)) except Exception as e: if silent: log_error(f'Failed to run remote_control mapping: {aa} with error: {e}') @@ -3169,7 +3171,7 @@ class Boss: if theme_colors.has_applied_theme: theme_colors.refresh() if theme_colors.has_applied_theme: # in case the theme file was deleted - assert theme_colors.applied_theme # to make mypy happy + assert theme_colors.applied_theme # to make type check happy theme_colors.apply_theme(theme_colors.applied_theme, notify_on_bg_change=False) for w in self.all_windows: if w.screen.color_profile.default_bg != bg_colors_before.get(w.id): @@ -3302,7 +3304,8 @@ class Boss: except (Exception, SystemExit) as err: self.show_error('Failed to set colors', str(err)) return - c.response_from_kitty(self, self.window_for_dispatch or self.active_window, PayloadGetter(c, payload if isinstance(payload, dict) else {})) + pd = cast(dict[str, Any], payload if isinstance(payload, dict) else {}) + c.response_from_kitty(self, self.window_for_dispatch or self.active_window, PayloadGetter(c, pd)) def _move_window_to( self, diff --git a/kitty/cli.py b/kitty/cli.py index 4bf8ef8ee..a4c72ea88 100644 --- a/kitty/cli.py +++ b/kitty/cli.py @@ -4,6 +4,7 @@ import os import re import sys +import types from collections.abc import Callable, Iterator, Sequence from re import Match from typing import Any, NoReturn, TypeVar, cast @@ -134,6 +135,7 @@ role_map: dict[str, Callable[[str], str]] = {} def role(func: Callable[[str], str]) -> Callable[[str], str]: + assert isinstance(func, types.FunctionType) role_map[func.__name__] = func return func diff --git a/kitty/clipboard.py b/kitty/clipboard.py index e0f73c3d1..2e137e1c0 100644 --- a/kitty/clipboard.py +++ b/kitty/clipboard.py @@ -102,7 +102,7 @@ class Clipboard: def set_mime(self, data: Mapping[str, DataType]) -> None: if self.enabled and isinstance(data, dict): - self.data = data + self.data = dict(data) set_clipboard_data_types(self.clipboard_type, tuple(self.data)) def get_text(self) -> str: @@ -147,7 +147,7 @@ class Clipboard: def __call__(self, mime: str) -> Callable[[], bytes]: data = self.data.get(mime, b'') if isinstance(data, str): - data = data.encode('utf-8') # type: ignore + data = data.encode('utf-8') if isinstance(data, bytes): def chunker() -> bytes: nonlocal data diff --git a/kitty/colors.py b/kitty/colors.py index edcb0be03..87d20b986 100644 --- a/kitty/colors.py +++ b/kitty/colors.py @@ -5,7 +5,7 @@ import os from collections.abc import Iterable, Sequence from contextlib import suppress from enum import Enum -from typing import Any, Literal, Optional, TypedDict +from typing import Any, Literal, Optional, TypedDict, cast from .config import parse_config from .constants import config_dir @@ -75,13 +75,14 @@ class ThemeColors: def refresh(self) -> bool: found = False + d: BackgroundImageOptions with suppress(FileNotFoundError): for x in os.scandir(config_dir): if x.name == ThemeFile.dark.value: mtime = x.stat().st_mtime_ns if mtime > self.dark_mtime: with open(x.path) as f: - d: BackgroundImageOptions = {} + d = {} self.dark_spec, self.dark_tbc = self.parse_colors(f, d) self.dark_background_image_options = d self.dark_mtime = mtime @@ -222,9 +223,10 @@ def parse_colors( conf = parse_config(spec) transparent_background_colors = conf.pop('transparent_background_colors', ()) if background_image_options is not None: + bio: dict[str, Any] = cast(dict[str, Any], background_image_options) for key in BackgroundImageOptions.__optional_keys__: if key in conf: - background_image_options.__setitem__(key, conf[key]) + bio[key] = conf[key] colors.update(conf) for k in nullable_colors: q = colors.pop(k, False) diff --git a/kitty/conf/generate.py b/kitty/conf/generate.py index c73ee9830..5dfeb1389 100644 --- a/kitty/conf/generate.py +++ b/kitty/conf/generate.py @@ -6,6 +6,7 @@ import inspect import os import re import textwrap +import types from collections.abc import Callable, Iterator from typing import Any, get_type_hints @@ -56,6 +57,7 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]: def option_type_data(option: Option | MultiOption) -> tuple[Callable[[Any], Any], str]: func = option.parser_func if func.__module__ == 'builtins': + assert isinstance(func, types.FunctionType) return func, func.__name__ th = get_type_hints(func) rettype = th['return'] @@ -86,6 +88,7 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]: if isinstance(option, MultiOption): mval: dict[str, dict[str, Any]] = {'macos': {}, 'linux': {}, '': {}} func, typ = option_type_data(option) + assert isinstance(func, types.FunctionType) for val in option: if val.add_to_default: gr = mval[val.only] @@ -114,6 +117,7 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]: func = str elif defn.has_color_table and option.is_color_table_color: func, typ = option_type_data(option) + assert isinstance(func, types.FunctionType) t(f' ans[{option.name!r}] = {func.__name__}(val)') tc_imports.add((func.__module__, func.__name__)) cnum = int(option.name[5:]) @@ -128,6 +132,7 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]: params = dict(inspect.signature(func).parameters) except Exception: params = {} + assert isinstance(func, types.FunctionType) if 'dict_with_parse_results' in params: t(f' {func.__name__}(val, ans)') else: @@ -163,6 +168,7 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]: a(f' {option_name}: {typ} = ' '{}') for parser, aliases in defn.deprecations.items(): + assert isinstance(parser, types.FunctionType) for alias in aliases: parser_function_declaration(alias) tc_imports.add((parser.__module__, parser.__name__)) @@ -183,6 +189,7 @@ def generate_class(defn: Definition, loc: str) -> tuple[str, str]: for aname, action in defn.actions.items(): option_names.add(aname) action_parsers[aname] = func = action.parser_func + assert isinstance(func, types.FunctionType) th = get_type_hints(func) rettype = th['return'] typ = option_type_as_str(rettype) @@ -487,6 +494,7 @@ def go_type_data(parser_func: ParserFuncType, ctype: str, is_multiple: bool = Fa _, rsep, fsep = ctype.split('_', 2) return 'map[string]string', f'config.ParseStrDict(val, `{rsep}`, `{fsep}`)' return f'*{ctype}', f'Parse{ctype}(val)' + assert isinstance(parser_func, types.FunctionType) p = parser_func.__name__ if p == 'int': return 'int64', 'strconv.ParseInt(val, 10, 64)' diff --git a/kitty/conf/utils.py b/kitty/conf/utils.py index 479df9d1e..4c831a226 100644 --- a/kitty/conf/utils.py +++ b/kitty/conf/utils.py @@ -558,7 +558,7 @@ def uniq(vals: Iterable[T]) -> list[T]: def save_type_stub(text: str, fpath: str) -> None: fpath += 'i' - preamble = '# Update this file by running: ./test.py mypy\n\n' + preamble = '# Update this file by running: ./test.py type-check\n\n' try: with open(fpath) as fs: existing = fs.read() diff --git a/kitty/config.py b/kitty/config.py index f63dfdb5b..5c6f19975 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -82,7 +82,7 @@ def finalize_keys(opts: Options, accumulate_bad_lines: list[BadLine] | None = No defns: list[KeyDefinition] = [] for d in opts.map: if d is None: # clear_all_shortcuts - defns = [] # type: ignore + defns = [] else: try: defns.append(d.resolve_and_copy(opts.kitty_mod)) @@ -122,7 +122,7 @@ def finalize_mouse_mappings(opts: Options, accumulate_bad_lines: list[BadLine] | defns: list[MouseMapping] = [] for d in opts.mouse_map: if d is None: # clear_all_mouse_actions - defns = [] # type: ignore + defns = [] else: try: defns.append(d.resolve_and_copy(opts.kitty_mod)) diff --git a/kitty/constants.py b/kitty/constants.py index 9cb05de14..7c8d44d96 100644 --- a/kitty/constants.py +++ b/kitty/constants.py @@ -233,27 +233,13 @@ def running_in_kitty(set_val: bool | None = None) -> bool: def list_kitty_resources(package: str = 'kitty') -> Iterator[str]: - try: - if sys.version_info[:2] < (3, 10): - raise ImportError("importlib.resources.files() doesn't work with frozen builds on python 3.9") - from importlib.resources import files - except ImportError: - from importlib.resources import contents - return iter(contents(package)) - else: - return (path.name for path in files(package).iterdir()) + from importlib.resources import files + return (path.name for path in files(package).iterdir()) def read_kitty_resource(name: str, package_name: str = 'kitty') -> bytes: - try: - if sys.version_info[:2] < (3, 10): - raise ImportError("importlib.resources.files() doesn't work with frozen builds on python 3.9") - from importlib.resources import files - except ImportError: - from importlib.resources import read_binary - return read_binary(package_name, name) - else: - return (files(package_name) / name).read_bytes() + from importlib.resources import files + return (files(package_name) / name).read_bytes() def website_url(doc_name: str = '', website: str = website_base_url) -> str: diff --git a/kitty/debug_config.py b/kitty/debug_config.py index ba9a724aa..dc5712d87 100644 --- a/kitty/debug_config.py +++ b/kitty/debug_config.py @@ -11,7 +11,7 @@ from collections.abc import Callable, Iterator, Sequence from contextlib import suppress from functools import partial from pprint import pformat -from typing import IO, TypeVar +from typing import IO, TYPE_CHECKING, Protocol, cast from kittens.tui.operations import colored, styled @@ -24,12 +24,15 @@ from .options.types import Options as KittyOpts from .options.types import defaults, secret_options from .options.utils import KeyboardMode, KeyDefinition from .rgb import color_as_sharp, color_from_int -from .types import MouseEvent, Shortcut, mod_to_names +from .types import Shortcut, mod_to_names from .utils import shlex_split -AnyEvent = TypeVar('AnyEvent', MouseEvent, Shortcut) Print = Callable[..., None] ShortcutMap = dict[Shortcut, str] +if TYPE_CHECKING: + from .fast_data_types import FontFace +else: + FontFace = object def green(x: str) -> str: @@ -54,9 +57,13 @@ def print_mapping_changes(defns: dict[str, str], changes: set[str], text: str, p for k in sorted(changes): print_event(k, defns[k], print) +class AnyEvent(Protocol): + def human_repr(self, kitty_mod: int = 0) -> str: ... -def compare_maps( - final: dict[AnyEvent, str], final_kitty_mod: int, initial: dict[AnyEvent, str], initial_kitty_mod: int, print: Print, mode_name: str = '' + +def compare_maps[T: AnyEvent]( + final: dict[T, str], final_kitty_mod: int, initial: dict[T, str], + initial_kitty_mod: int, print: Print, mode_name: str = '' ) -> None: ei = {k.human_repr(initial_kitty_mod): v for k, v in initial.items()} ef = {k.human_repr(final_kitty_mod): v for k, v in final.items()} @@ -308,7 +315,8 @@ def debug_config(opts: KittyOpts | None = None, global_shortcuts: dict[str, Sing p(green('Fonts:')) for k, font in current_fonts().items(): if hasattr(font, 'identify_for_debug'): - flines = font.identify_for_debug().splitlines() + face: FontFace = cast(FontFace, font) + flines = face.identify_for_debug().splitlines() p(yellow(f'{k.rjust(8)}:'), flines[0]) for fl in flines[1:]: p(' ' * 9, fl) diff --git a/kitty/file_transmission.py b/kitty/file_transmission.py index d953d24d5..d059cca4d 100644 --- a/kitty/file_transmission.py +++ b/kitty/file_transmission.py @@ -1087,7 +1087,7 @@ class FileTransmission: q = self.active_receives.get(receive_id) if q is None: return - ar = q # for mypy + ar = q # for type check while ar.signature_pending_chunks: if self.write_ftc_to_child(ar.signature_pending_chunks[0], use_pending=False): ar.signature_pending_chunks.popleft() @@ -1244,11 +1244,11 @@ class TestFileTransmission(FileTransmission): self.test_responses.append(payload.asdict()) return True - def start_receive(self, aid: str) -> None: - self.handle_send_confirmation(self.allow, aid) + def start_receive(self, ar_id: str) -> None: + self.handle_send_confirmation(self.allow, ar_id) - def start_send(self, aid: str) -> None: - self.handle_receive_confirmation(self.allow, aid) + def start_send(self, asd_id: str) -> None: + self.handle_receive_confirmation(self.allow, asd_id) def callback_after(self, callback: Callable[[int | None], None], timeout: float = 0) -> int | None: callback(None) diff --git a/kitty/fonts/__init__.py b/kitty/fonts/__init__.py index d1ff7afa1..9eed5866f 100644 --- a/kitty/fonts/__init__.py +++ b/kitty/fonts/__init__.py @@ -1,6 +1,6 @@ from collections.abc import Sequence from enum import Enum, IntEnum, auto -from typing import TYPE_CHECKING, Literal, NamedTuple, TypedDict, TypeVar, Union +from typing import TYPE_CHECKING, Literal, NamedTuple, TypedDict, Union from kitty.fast_data_types import ParsedFontFeature from kitty.types import run_once @@ -205,7 +205,6 @@ class FontSpec(NamedTuple): Descriptor = Union[FontConfigPattern, CoreTextFont] -DescriptorVar = TypeVar('DescriptorVar', FontConfigPattern, CoreTextFont, Descriptor) class Score(NamedTuple): @@ -215,7 +214,7 @@ class Score(NamedTuple): width_score: int -class Scorer: +class Scorer[T]: def __init__(self, bold: bool = False, italic: bool = False, monospaced: bool = True, prefer_variable: bool = False) -> None: self.bold = bold @@ -223,7 +222,7 @@ class Scorer: self.monospaced = monospaced self.prefer_variable = prefer_variable - def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> list[DescriptorVar]: + def sorted_candidates(self, candidates: Sequence[T], dump: bool = False) -> list[T]: raise NotImplementedError() def __repr__(self) -> str: diff --git a/kitty/fonts/common.py b/kitty/fonts/common.py index d6e873990..3a0edc291 100644 --- a/kitty/fonts/common.py +++ b/kitty/fonts/common.py @@ -1,15 +1,16 @@ #!/usr/bin/env python # License: GPLv3 Copyright: 2024, Kovid Goyal -from typing import TYPE_CHECKING, Any, Literal, TypedDict, Union +import copy +from typing import TYPE_CHECKING, Any, Literal, TypedDict, Union, cast from kitty.constants import is_macos from kitty.fast_data_types import ParsedFontFeature -from kitty.fonts import Descriptor, DescriptorVar, DesignAxis, FontSpec, NamedStyle, Scorer, VariableAxis, VariableData, family_name_to_key +from kitty.fonts import Descriptor, DesignAxis, FontSpec, NamedStyle, Scorer, VariableAxis, VariableData, family_name_to_key from kitty.options.types import Options if TYPE_CHECKING: - from kitty.fast_data_types import CTFace + from kitty.fast_data_types import CoreTextFont, CTFace, FontConfigPattern from kitty.fast_data_types import Face as FT_Face FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map', 'variable_map'] @@ -139,9 +140,9 @@ def get_variable_data_for_face(d: Face) -> VariableData: return ans -def find_best_match_in_candidates( - candidates: list[DescriptorVar], scorer: Scorer, is_medium_face: bool, ignore_face: DescriptorVar | None = None -) -> DescriptorVar | None: +def find_best_match_in_candidates[T]( + candidates: list[T], scorer: Scorer[T], is_medium_face: bool, ignore_face: T | None = None +) -> T | None: if candidates: for x in scorer.sorted_candidates(candidates): if ignore_face is None or x != ignore_face: @@ -154,8 +155,8 @@ def pprint(*a: Any, **kw: Any) -> None: pprint(*a, **kw) -def find_medium_variant(font: DescriptorVar) -> DescriptorVar: - font = font.copy() +def find_medium_variant[T: (CoreTextFont | FontConfigPattern)](font: T) -> T: + font = copy.copy(font) vd = get_variable_data_for_descriptor(font) for i, ns in enumerate(vd['named_styles']): if ns['name'] == 'Regular': @@ -452,7 +453,7 @@ def _get_named_style(axis_map: dict[str, float], vd: VariableData) -> NamedStyle def get_named_style(face_or_descriptor: Face | Descriptor) -> NamedStyle | None: if isinstance(face_or_descriptor, dict): - d: Descriptor = face_or_descriptor + d = cast(Descriptor, face_or_descriptor) vd = get_variable_data_for_descriptor(d) if d['descriptor_type'] == 'fontconfig': ns = d.get('named_style', -1) @@ -466,7 +467,7 @@ def get_named_style(face_or_descriptor: Face | Descriptor) -> NamedStyle | None: else: axis_map = d.get('axis_map', {}).copy() else: - face: Face = face_or_descriptor + face = cast(Face, face_or_descriptor) vd = get_variable_data_for_face(face) q = face.get_variation() if q is None: @@ -479,7 +480,7 @@ def get_axis_map(face_or_descriptor: Face | Descriptor) -> dict[str, float]: base_axis_map = {} axis_map: dict[str, float] = {} if isinstance(face_or_descriptor, dict): - d: Descriptor = face_or_descriptor + d = cast(Descriptor, face_or_descriptor) vd = get_variable_data_for_descriptor(d) if d['descriptor_type'] == 'fontconfig': ns = d.get('named_style', -1) @@ -494,7 +495,7 @@ def get_axis_map(face_or_descriptor: Face | Descriptor) -> dict[str, float]: else: axis_map = d.get('axis_map', {}).copy() else: - face: Face = face_or_descriptor + face = cast(Face, face_or_descriptor) q = face.get_variation() if q is not None: axis_map = q diff --git a/kitty/fonts/core_text.py b/kitty/fonts/core_text.py index 1b9944f7f..b67a8bcb5 100644 --- a/kitty/fonts/core_text.py +++ b/kitty/fonts/core_text.py @@ -12,7 +12,7 @@ from kitty.fast_data_types import CTFace, coretext_all_fonts from kitty.typing_compat import CoreTextFont from kitty.utils import log_error -from . import Descriptor, DescriptorVar, ListedFont, Score, Scorer, VariableData, family_name_to_key +from . import Descriptor, ListedFont, Score, Scorer, VariableData, family_name_to_key attr_map = {(False, False): 'font_family', (True, False): 'bold_font', @@ -113,7 +113,7 @@ def weight_range_for_family(family: str) -> WeightRange: return WeightRange(mini, maxi, medium, bold) -class CTScorer(Scorer): +class CTScorer(Scorer[CoreTextFont]): weight_range: WeightRange | None = None def score(self, candidate: Descriptor) -> Score: @@ -140,7 +140,7 @@ class CTScorer(Scorer): is_regular_width = not candidate['expanded'] and not candidate['condensed'] return Score(variable_score, bold_score + italic_score, monospace_match, 0 if is_regular_width else 1) - def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> list[DescriptorVar]: + def sorted_candidates(self, candidates: Sequence[CoreTextFont], dump: bool = False) -> list[CoreTextFont]: self.weight_range = None families = {x['family'] for x in candidates} if len(families) == 1: diff --git a/kitty/fonts/fontconfig.py b/kitty/fonts/fontconfig.py index 544ef063e..d61a67e58 100644 --- a/kitty/fonts/fontconfig.py +++ b/kitty/fonts/fontconfig.py @@ -22,7 +22,7 @@ from kitty.fast_data_types import ( from kitty.fast_data_types import fc_match as fc_match_impl from kitty.typing_compat import FontConfigPattern -from . import Descriptor, DescriptorVar, ListedFont, Score, Scorer, VariableData, family_name_to_key +from . import Descriptor, ListedFont, Score, Scorer, VariableData, family_name_to_key FontCollectionMapType = Literal['family_map', 'ps_map', 'full_map', 'variable_map'] FontMap = dict[FontCollectionMapType, dict[str, list[FontConfigPattern]]] @@ -130,7 +130,7 @@ def weight_range_for_family(family: str) -> WeightRange: -class FCScorer(Scorer): +class FCScorer(Scorer[FontConfigPattern]): weight_range: WeightRange | None = None @@ -148,7 +148,7 @@ class FCScorer(Scorer): width_score = abs(candidate['width'] - FC_WIDTH_NORMAL) return Score(variable_score, bold_score / 1000 + italic_score / 110, monospace_match, width_score) - def sorted_candidates(self, candidates: Sequence[DescriptorVar], dump: bool = False) -> list[DescriptorVar]: + def sorted_candidates(self, candidates: Sequence[FontConfigPattern], dump: bool = False) -> list[FontConfigPattern]: self.weight_range = None families = {x['family'] for x in candidates} if len(families) == 1: diff --git a/kitty/guess_mime_type.py b/kitty/guess_mime_type.py index b4604c8bb..cf38c8c1f 100644 --- a/kitty/guess_mime_type.py +++ b/kitty/guess_mime_type.py @@ -100,7 +100,7 @@ def guess_type(path: str, allow_filesystem_access: bool = False) -> str | None: mt = mt or is_special_file(path) if not mt: if is_dir: - mt = 'inode/directory' # type: ignore + mt = 'inode/directory' elif is_exe: mt = 'inode/executable' return mt diff --git a/kitty/layout/base.py b/kitty/layout/base.py index 62baed205..6d9be87c8 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Generator, Iterable, Iterator, Sequence from enum import Enum from functools import partial from itertools import repeat -from typing import Any, ClassVar, NamedTuple +from typing import Any, ClassVar, NamedTuple, cast from kitty.borders import BorderColor from kitty.fast_data_types import BOTTOM_EDGE, RIGHT_EDGE, Region, get_options, set_active_window, viewport_for_window @@ -92,7 +92,8 @@ def calculate_cells_map( number_of_windows: int, number_of_cells: int ) -> list[int]: if isinstance(bias, dict): - bias = convert_bias_map(bias, number_of_windows, number_of_cells) + b: dict[int, float] = cast(dict[int, float], bias) + bias = convert_bias_map(b, number_of_windows, number_of_cells) cells_per_window = number_of_cells // number_of_windows if bias is not None and number_of_windows > 1 and number_of_windows == len(bias) and cells_per_window > 5: cells_map = [int(b * number_of_cells) for b in bias] diff --git a/kitty/layout/grid.py b/kitty/layout/grid.py index 3060f0b43..e0ef3f024 100644 --- a/kitty/layout/grid.py +++ b/kitty/layout/grid.py @@ -93,10 +93,10 @@ class Grid(Layout): b[bias_idx] = increment return tuple(self.variable_layout(layout_func, num_windows, b)) == before_layout - def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool: + def apply_bias(self, window_id: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool: num_windows = all_windows.num_groups ncols, nrows, special_rows, special_col = calc_grid_size(num_windows) - row_num, col_num = self.position_for_window_idx(idx, num_windows, ncols, nrows, special_rows, special_col) + row_num, col_num = self.position_for_window_idx(window_id, num_windows, ncols, nrows, special_rows, special_col) if is_horizontal: b = self.biased_cols @@ -151,13 +151,13 @@ class Grid(Layout): pos += rows on_col_done(col_windows) - def do_layout(self, all_windows: WindowList) -> None: - n = all_windows.num_groups + def do_layout(self, windows: WindowList) -> None: + n = windows.num_groups if n == 1: - self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups())) + self.layout_single_window_group(next(windows.iter_all_layoutable_groups())) return ncols, nrows, special_rows, special_col = calc_grid_size(n) - groups = tuple(all_windows.iter_all_layoutable_groups()) + groups = tuple(windows.iter_all_layoutable_groups()) win_col_map: list[list[WindowGroup]] = [] def on_col_done(col_windows: list[int]) -> None: @@ -193,17 +193,17 @@ class Grid(Layout): n, nrows, ncols, special_rows, special_col, on_col_done): position_window_in_grid_cell(window_idx, xl, yl) - def minimal_borders(self, all_windows: WindowList) -> Iterator[BorderLine]: - n = all_windows.num_groups + def minimal_borders(self, windows: WindowList) -> Iterator[BorderLine]: + n = windows.num_groups if not lgd.draw_minimal_borders or n < 2: return - needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders) + needs_borders_map = windows.compute_needs_borders_map(lgd.draw_active_borders) ncols, nrows, special_rows, special_col = calc_grid_size(n) is_first_row: set[int] = set() is_last_row: set[int] = set() is_first_column: set[int] = set() is_last_column: set[int] = set() - groups = tuple(all_windows.iter_all_layoutable_groups()) + groups = tuple(windows.iter_all_layoutable_groups()) bw = groups[0].effective_border() if not bw: return @@ -227,7 +227,7 @@ class Grid(Layout): all_groups_in_order.append(wg) layout_data_map[wg.id] = xl, yl is_last_column = {groups[x].id for x in prev_col_windows} - active_group = all_windows.active_group + active_group = windows.active_group def ends(yl: LayoutData) -> tuple[int, int]: return yl.content_pos - yl.space_before, yl.content_pos + yl.content_size + yl.space_after @@ -258,17 +258,17 @@ class Grid(Layout): wid = wg.active_window_id yield from borders_for_window(wg.id, color, wid) - def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap: - n = all_windows.num_groups + def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap: + n = windows.num_groups if n < 4: - return neighbors_for_tall_window(1, window, all_windows) + return neighbors_for_tall_window(1, window, windows) - wg = all_windows.group_for_window(window) + wg = windows.group_for_window(window) assert wg is not None ncols, nrows, special_rows, special_col = calc_grid_size(n) blank_row: list[int | None] = [None for i in range(ncols)] matrix = tuple(blank_row[:] for j in range(max(nrows, special_rows))) - wi = all_windows.iter_all_layoutable_groups() + wi = windows.iter_all_layoutable_groups() pos_map: dict[int, tuple[int, int]] = {} col_counts: list[int] = [] for col in range(ncols): diff --git a/kitty/layout/splits.py b/kitty/layout/splits.py index ff487915c..4ff755f51 100644 --- a/kitty/layout/splits.py +++ b/kitty/layout/splits.py @@ -576,7 +576,7 @@ class SplitsLayoutOpts(LayoutOpts): class Splits(Layout): name = 'splits' needs_all_windows = True - layout_opts = SplitsLayoutOpts({}) + layout_opts: SplitsLayoutOpts = SplitsLayoutOpts({}) no_minimal_window_borders = True drag_overlay_mode = DragOverlayMode.free @@ -608,8 +608,8 @@ class Splits(Layout): if isinstance(q, Pair): self.pairs_root = q - def do_layout(self, all_windows: WindowList) -> None: - groups = tuple(all_windows.iter_all_layoutable_groups()) + def do_layout(self, windows: WindowList) -> None: + groups = tuple(windows.iter_all_layoutable_groups()) root = self.pairs_root all_present_group_ids = {g.id for g in groups} already_placed_group_ids = frozenset(root.all_window_ids()) @@ -700,19 +700,19 @@ class Splits(Layout): return self.equalize_biases() return False - def minimal_borders(self, all_windows: WindowList) -> Iterator[BorderLine]: - groups = tuple(all_windows.iter_all_layoutable_groups()) + def minimal_borders(self, windows: WindowList) -> Iterator[BorderLine]: + groups = tuple(windows.iter_all_layoutable_groups()) window_count = len(groups) if not lgd.draw_minimal_borders or window_count < 2: return - needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders) - ag = all_windows.active_group + needs_borders_map = windows.compute_needs_borders_map(lgd.draw_active_borders) + ag = windows.active_group active_group_id = -1 if ag is None else ag.id border_color_map = {} for grp_id, needs_borders in needs_borders_map.items(): if needs_borders: - wid = g.active_window_id if (g := all_windows.group_for_id(grp_id)) else 0 + wid = g.active_window_id if (g := windows.group_for_id(grp_id)) else 0 if wid: color = BorderColor.active if grp_id is active_group_id else BorderColor.bell border_color_map[wid] = color @@ -723,13 +723,13 @@ class Splits(Layout): for bb in which: yield bb._replace(color=border_color_map.get(abs(bb.window_id), BorderColor.inactive)) - def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap: - wg = all_windows.group_for_window(window) + def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap: + wg = windows.group_for_window(window) assert wg is not None pair = self.pairs_root.pair_for_window(wg.id) ans: NeighborsMap = {} if pair is not None: - pair.neighbors_for_window(wg.id, ans, self, all_windows) + pair.neighbors_for_window(wg.id, ans, self, windows) return ans def move_window(self, all_windows: WindowList, delta: int = 1) -> bool: @@ -886,9 +886,9 @@ class Splits(Layout): return None - def drag_resize_window(self, all_windows: WindowList, pair_id: int, increment: float, is_horizontal: bool = True) -> bool: + def drag_resize_window(self, all_windows: WindowList, window_id: int, increment: float, is_horizontal: bool = True) -> bool: for pair in self.pairs_root.self_and_descendants(): - if id(pair) == pair_id: + if id(pair) == window_id: new_bias = max(0, min(pair.bias + increment, 1)) if new_bias != pair.bias: pair.bias = new_bias diff --git a/kitty/layout/stack.py b/kitty/layout/stack.py index c855b2c64..7498de75e 100644 --- a/kitty/layout/stack.py +++ b/kitty/layout/stack.py @@ -14,15 +14,15 @@ class Stack(Layout): needs_window_borders = False only_active_window_visible = True - def do_layout(self, all_windows: WindowList) -> None: - active_group = all_windows.active_group - for group in all_windows.iter_all_layoutable_groups(): + def do_layout(self, windows: WindowList) -> None: + active_group = windows.active_group + for group in windows.iter_all_layoutable_groups(): self.layout_single_window_group(group, add_blank_rects=group is active_group) - def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap: - wg = all_windows.group_for_window(window) + def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap: + wg = windows.group_for_window(window) assert wg is not None - groups = tuple(all_windows.iter_all_layoutable_groups()) + groups = tuple(windows.iter_all_layoutable_groups()) idx = groups.index(wg) before = [] if wg is groups[0] else [groups[idx-1].id] after = [] if wg is groups[-1] else [groups[idx+1].id] diff --git a/kitty/layout/tall.py b/kitty/layout/tall.py index 3c8b95dd7..136444bf4 100644 --- a/kitty/layout/tall.py +++ b/kitty/layout/tall.py @@ -138,7 +138,7 @@ class Tall(Layout): main_is_horizontal = True no_minimal_window_borders = True drag_overlay_mode = DragOverlayMode.axis_y - layout_opts = TallLayoutOpts({}) + layout_opts: TallLayoutOpts = TallLayoutOpts({}) main_axis_layout = Layout.xlayout perp_axis_layout = Layout.ylayout @@ -167,12 +167,12 @@ class Tall(Layout): after_layout = tuple(self.variable_layout(all_windows, self.biased_map)) return before_layout == after_layout - def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool: + def apply_bias(self, window_id: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool: num_windows = all_windows.num_groups if self.main_is_horizontal == is_horizontal: before_main_bias = self.main_bias ncols = self.num_full_size_windows + 1 - biased_col = idx if idx < self.num_full_size_windows else (ncols - 1) + biased_col = window_id if window_id < self.num_full_size_windows else (ncols - 1) self.main_bias = [ safe_increment_bias(self.main_bias[i], increment * (1 if i == biased_col else -1)) for i in range(ncols) ] @@ -180,13 +180,13 @@ class Tall(Layout): return self.main_bias != before_main_bias num_of_short_windows = num_windows - self.num_full_size_windows - if idx < self.num_full_size_windows or num_of_short_windows < 2: + if window_id < self.num_full_size_windows or num_of_short_windows < 2: return False - idx -= self.num_full_size_windows + window_id -= self.num_full_size_windows before_layout = tuple(self.variable_layout(all_windows, self.biased_map)) - before = self.biased_map.get(idx, 0.) + before = self.biased_map.get(window_id, 0.) candidate = self.biased_map.copy() - candidate[idx] = after = before + increment + candidate[window_id] = after = before + increment if before_layout == tuple(self.variable_layout(all_windows, candidate)): return False self.biased_map = candidate @@ -251,12 +251,12 @@ class Tall(Layout): xl, yl = yl, xl yield wg, xl, yl, False - def do_layout(self, all_windows: WindowList) -> None: - num = all_windows.num_groups + def do_layout(self, windows: WindowList) -> None: + num = windows.num_groups if num == 1: - self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups())) + self.layout_single_window_group(next(windows.iter_all_layoutable_groups())) return - layouts = (self.simple_layout if num <= self.num_full_size_windows + 1 else self.full_layout)(all_windows) + layouts = (self.simple_layout if num <= self.num_full_size_windows + 1 else self.full_layout)(windows) for wg, xl, yl, is_full_size in layouts: self.set_window_group_geometry(wg, xl, yl) @@ -303,25 +303,25 @@ class Tall(Layout): return False return None - def minimal_borders(self, all_windows: WindowList) -> Iterator[BorderLine]: - num = all_windows.num_groups + def minimal_borders(self, windows: WindowList) -> Iterator[BorderLine]: + num = windows.num_groups if num < 2 or not lgd.draw_minimal_borders: return try: - bw = next(all_windows.iter_all_layoutable_groups()).effective_border() + bw = next(windows.iter_all_layoutable_groups()).effective_border() except StopIteration: bw = 0 if not bw: return if num <= self.num_full_size_windows + 1: - layout = (x[:3] for x in self.simple_layout(all_windows)) - yield from borders(layout, self.main_is_horizontal, all_windows) + layout = (x[:3] for x in self.simple_layout(windows)) + yield from borders(layout, self.main_is_horizontal, windows) return main_layouts: list[tuple[WindowGroup, LayoutData, LayoutData]] = [] perp_borders: list[BorderLine] = [] - layouts = (self.simple_layout if num <= self.num_full_size_windows else self.full_layout)(all_windows) - needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders) - active_group = all_windows.active_group + layouts = (self.simple_layout if num <= self.num_full_size_windows else self.full_layout)(windows) + needs_borders_map = windows.compute_needs_borders_map(lgd.draw_active_borders) + active_group = windows.active_group mirrored = self.layout_opts.mirrored for wg, xl, yl, is_full_size in layouts: if is_full_size: @@ -356,7 +356,7 @@ class Tall(Layout): mirrored = self.layout_opts.mirrored yield from borders( - main_layouts, self.main_is_horizontal, all_windows, + main_layouts, self.main_is_horizontal, windows, start_offset=int(not mirrored), end_offset=int(mirrored) ) yield from perp_borders[1:-1] diff --git a/kitty/layout/vertical.py b/kitty/layout/vertical.py index 7a3159565..e9f835bd8 100644 --- a/kitty/layout/vertical.py +++ b/kitty/layout/vertical.py @@ -80,7 +80,7 @@ class Vertical(Layout): self.biased_map: dict[int, float] = {} return True - def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool: + def apply_bias(self, window_id: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool: if self.main_is_horizontal != is_horizontal: return False num_windows = all_windows.num_groups @@ -88,8 +88,8 @@ class Vertical(Layout): return False before_layout = list(self.variable_layout(all_windows, self.biased_map)) candidate = self.biased_map.copy() - before = candidate.get(idx, 0) - candidate[idx] = before + increment + before = candidate.get(window_id, 0) + candidate[window_id] = before + increment if before_layout == list(self.variable_layout(all_windows, candidate)): return False self.biased_map = candidate @@ -109,24 +109,24 @@ class Vertical(Layout): xl, yl = yl, xl yield wg, xl, yl - def do_layout(self, all_windows: WindowList) -> None: - window_count = all_windows.num_groups + def do_layout(self, windows: WindowList) -> None: + window_count = windows.num_groups if window_count == 1: - self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups())) + self.layout_single_window_group(next(windows.iter_all_layoutable_groups())) return - for wg, xl, yl in self.generate_layout_data(all_windows): + for wg, xl, yl in self.generate_layout_data(windows): self.set_window_group_geometry(wg, xl, yl) - def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]: - window_count = all_windows.num_groups + def minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]: + window_count = windows.num_groups if window_count < 2 or not lgd.draw_minimal_borders: return - yield from borders(self.generate_layout_data(all_windows), self.main_is_horizontal, all_windows) + yield from borders(self.generate_layout_data(windows), self.main_is_horizontal, windows) - def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap: - wg = all_windows.group_for_window(window) + def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap: + wg = windows.group_for_window(window) assert wg is not None - groups = tuple(all_windows.iter_all_layoutable_groups()) + groups = tuple(windows.iter_all_layoutable_groups()) idx = groups.index(wg) lg = len(groups) if lg > 1: diff --git a/kitty/main.py b/kitty/main.py index e0e56a73d..95c75d6a6 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -606,7 +606,6 @@ def kitty_main(called_from_panel: bool = False) -> None: def main(called_from_panel: bool = False) -> None: - global redirected_for_quick_access try: if is_macos and launched_by_launch_services and not called_from_panel: with suppress(OSError): diff --git a/kitty/multiprocessing.py b/kitty/multiprocessing.py index fde5715cf..ec9bddcfe 100644 --- a/kitty/multiprocessing.py +++ b/kitty/multiprocessing.py @@ -6,31 +6,17 @@ import os -import sys from collections.abc import Callable, Sequence from concurrent.futures import ProcessPoolExecutor from multiprocessing import context, get_all_start_methods, get_context, spawn, util -from typing import TYPE_CHECKING, Any, Union +from typing import Any from .constants import kitty_exe orig_spawn_passfds = util.spawnv_passfds orig_executable = spawn.get_executable() -if TYPE_CHECKING: - from collections.abc import Buffer - from typing import SupportsIndex, SupportsInt - - if sys.version_info[:2] >= (3, 14): - ArgsType = Sequence[Union[str, Buffer, SupportsInt, SupportsIndex]] - else: - from _typeshed import ReadableBuffer, SupportsTrunc - ArgsType = Sequence[Union[str, ReadableBuffer, SupportsInt, SupportsIndex, SupportsTrunc]] -else: - ArgsType = Sequence[str] - - -def spawnv_passfds(path: bytes, args: ArgsType, passfds: Sequence[int]) -> int: +def spawnv_passfds(path: bytes, args: list[str], passfds: Sequence[int]) -> int: if '-c' in args: idx = args.index('-c') patched_args = [spawn.get_executable(), '+runpy'] + list(args)[idx + 1:] @@ -45,7 +31,7 @@ def spawnv_passfds(path: bytes, args: ArgsType, passfds: Sequence[int]) -> int: def monkey_patch_multiprocessing() -> None: # Use kitty to run the worker process used by multiprocessing spawn.set_executable(kitty_exe()) - util.spawnv_passfds = spawnv_passfds + util.spawnv_passfds = spawnv_passfds # type: ignore def unmonkey_patch_multiprocessing() -> None: diff --git a/kitty/notifications.py b/kitty/notifications.py index a2df28947..63260e45b 100644 --- a/kitty/notifications.py +++ b/kitty/notifications.py @@ -456,8 +456,9 @@ class NotificationCommand: from .search_query_parser import search def get_matches(location: str, query: str, candidates: set['NotificationCommand']) -> set['NotificationCommand']: return {x for x in candidates if x.matches_rule_item(location, query)} + universal_set: set['NotificationCommand'] = {self} try: - return self in search(rule, ('title', 'body', 'app', 'type'), {self}, get_matches) + return self in search(rule, ('title', 'body', 'app', 'type'), universal_set, get_matches) except Exception as e: self.log(f'Ignoring invalid filter_notification rule: {rule} with error: {e}') return False @@ -992,7 +993,7 @@ class NotificationManager: self.send_closed_response(channel_id, cmd.identifier, untracked=True) return desktop_notification_id - def expire_notification(self, desktop_notification_id: int, command_id: int, timer_id: int) -> None: + def expire_notification(self, desktop_notification_id: int, command_id: int, timer_id: int | None) -> None: if n := self.in_progress_notification_commands.get(desktop_notification_id): if id(n) == command_id: self.desktop_integration.close_notification(desktop_notification_id) diff --git a/kitty/options/types.py b/kitty/options/types.py index a271ca046..3d036ef5d 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -681,7 +681,7 @@ class Options: tab_separator: str = ' ┇' tab_switch_strategy: choices_for_tab_switch_strategy = 'previous' tab_title_max_length: int = 0 - tab_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{fmt.fg.tab}{tab.last_focused_progress_percent}{title}' + tab_title_template: str = '{fmt.fg.red}{bell_symbol}{activity_symbol}{secure_input_symbol}{fmt.fg.tab}{tab.last_focused_progress_percent}{title}' term: str = 'xterm-kitty' terminfo_type: choices_for_terminfo_type = 'path' text_composition_strategy: str = 'platform' diff --git a/kitty/rc/scroll_window.py b/kitty/rc/scroll_window.py index f857ef6a8..5cc86befa 100644 --- a/kitty/rc/scroll_window.py +++ b/kitty/rc/scroll_window.py @@ -2,7 +2,7 @@ # License: GPLv3 Copyright: 2020, Kovid Goyal -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal from .base import MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, Window @@ -60,13 +60,14 @@ using this option means that you will not be notified of failures. return {'match': opts.match, 'amount': amount, 'self': True} def response_from_kitty(self, boss: Boss, window: Window | None, payload_get: PayloadGetType) -> ResponseType: - amt = payload_get('amount') + vamt: tuple[int | float | Literal['start', 'end'], str] = payload_get('amount') for window in self.windows_for_match_payload(boss, window, payload_get): if window: - if amt[0] in ('start', 'end'): - (window.scroll_home if amt[0] == 'start' else window.scroll_end)() + if vamt[0] in ('start', 'end'): + (window.scroll_home if vamt[0] == 'start' else window.scroll_end)() else: - amt, unit = amt + amt, unit = vamt + assert isinstance(amt, (int, float)) match unit: case 'u': window.screen.reverse_scroll(int(abs(amt)), True) @@ -78,6 +79,7 @@ using this option means that you will not be notified of failures. if not isinstance(amt, int) and not amt.is_integer(): amt = round(window.screen.lines * amt) unit = 'line' + assert isinstance(amt, int) direction = 'up' if amt < 0 else 'down' func = getattr(window, f'scroll_{unit}_{direction}') for i in range(int(abs(amt))): diff --git a/kitty/rc/set_spacing.py b/kitty/rc/set_spacing.py index 381d4081d..1e5a26936 100644 --- a/kitty/rc/set_spacing.py +++ b/kitty/rc/set_spacing.py @@ -3,7 +3,7 @@ from collections.abc import Iterable -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, LiteralString from .base import MATCH_TAB_OPTION, MATCH_WINDOW_OPTION, ArgsType, Boss, PayloadGetType, PayloadType, RCOptions, RemoteCommand, ResponseType, Window @@ -36,7 +36,7 @@ def patch_configured_edges(opts: 'Options', s: dict[str, float | None]) -> None: def parse_spacing_settings(args: Iterable[str]) -> dict[str, float | None]: - mapper: dict[str, list[str]] = {} + mapper: dict[str, list[LiteralString]] = {} for q in ('margin', 'padding'): mapper[q] = f'{q}-left {q}-top {q}-right {q}-bottom'.split() mapper[f'{q}-h'] = mapper[f'{q}-horizontal'] = f'{q}-left {q}-right'.split() diff --git a/kitty/rc/set_window_title.py b/kitty/rc/set_window_title.py index 33d55f386..1a7bfd09f 100644 --- a/kitty/rc/set_window_title.py +++ b/kitty/rc/set_window_title.py @@ -42,14 +42,15 @@ again. If you want to allow other programs to change it afterwards, use this opt return ans def response_from_kitty(self, boss: Boss, window: Window | None, payload_get: PayloadGetType) -> ResponseType: - title = payload_get('title') + title: str | None = payload_get('title') + btitle: memoryview | None = None if payload_get('temporary') and title is not None: - title = memoryview(title.encode('utf-8')) + btitle = memoryview(title.encode('utf-8')) for window in self.windows_for_match_payload(boss, window, payload_get): if window: if payload_get('temporary'): window.override_title = None - window.title_changed(title) + window.title_changed(btitle) else: window.set_title(title) return None diff --git a/kitty/search_query_parser.py b/kitty/search_query_parser.py index 51a6f7cc7..dfdc7cfa2 100644 --- a/kitty/search_query_parser.py +++ b/kitty/search_query_parser.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Iterator, Sequence from enum import Enum from functools import lru_cache from gettext import gettext as _ -from typing import NamedTuple, TypeVar +from typing import NamedTuple, TypeVar, cast from .types import run_once @@ -121,13 +121,13 @@ class Token(NamedTuple): @run_once def lex_scanner() -> Callable[[str], tuple[list[Token], str]]: - return getattr(re, 'Scanner')([ # type: ignore + return cast(Callable[[str], tuple[list[Token], str]], getattr(re, 'Scanner')([ (r'[()]', lambda x, t: Token(TokenType.OPCODE, t)), (r'@.+?:[^")\s]+', lambda x, t: Token(TokenType.WORD, str(t))), (r'[^"()\s]+', lambda x, t: Token(TokenType.WORD, str(t))), (r'".*?((? None: from .launch import parse_launch_args needs_expandvars = False - if isinstance(cmd, str) and cmd: + if isinstance(cmd, str): needs_expandvars = True - cmd = list(shlex_split(cmd)) + cmd = list(shlex_split(cmd)) if cmd else [] serialize_data: dict[str, Any] = {'id': 0, 'cmd_at_shell_startup': ()} if cmd and cmd[0].startswith(unserialize_launch_flag): serialize_data = json.loads(cmd[0][len(unserialize_launch_flag):]) diff --git a/kitty/types.py b/kitty/types.py index d3fb4603d..6b4e04d76 100644 --- a/kitty/types.py +++ b/kitty/types.py @@ -4,7 +4,7 @@ from collections.abc import Callable, Iterator, Mapping, Sequence from enum import Enum from functools import update_wrapper -from typing import TYPE_CHECKING, Any, Generic, NamedTuple, TypedDict, TypeVar, Union +from typing import TYPE_CHECKING, Any, Generic, Literal, NamedTuple, TypedDict, TypeVar, Union if TYPE_CHECKING: from kitty.fast_data_types import SingleKey @@ -213,11 +213,7 @@ def modmap() -> dict[str, int]: 'caps_lock': GLFW_MOD_CAPS_LOCK, 'num_lock': GLFW_MOD_NUM_LOCK} -if TYPE_CHECKING: - from typing import Literal - ActionGroup = Literal['cp', 'sc', 'win', 'tab', 'fs', 'mouse', 'mk', 'lay', 'misc', 'debug', 'session'] -else: - ActionGroup = str +ActionGroup = Literal['cp', 'sc', 'win', 'tab', 'fs', 'mouse', 'mk', 'lay', 'misc', 'debug', 'session'] class ActionSpec(NamedTuple): diff --git a/kitty_tests/main.py b/kitty_tests/main.py index 9315e1746..030197874 100644 --- a/kitty_tests/main.py +++ b/kitty_tests/main.py @@ -88,11 +88,6 @@ def filter_tests_by_module(suite: unittest.TestSuite, *names: str) -> unittest.T return filter_tests(suite, q) -@lru_cache -def python_for_type_check() -> str: - return shutil.which('python') or shutil.which('python3') or 'python' - - def type_check() -> NoReturn: from kitty.cli_stub import generate_stub # type:ignore @@ -100,8 +95,7 @@ def type_check() -> NoReturn: from kittens.tui.operations_stub import generate_stub # type: ignore generate_stub() - py = python_for_type_check() - os.execlp(py, py, '-m', 'mypy', '--pretty') + os.execlp('ty', 'ty', 'check') def run_cli(suite: unittest.TestSuite, verbosity: int = 4) -> bool: @@ -309,11 +303,9 @@ def env_for_python_tests(report_env: bool = False) -> Iterator[None]: path = os.pathsep.join(x for x in paths if not x.startswith(current_home)) launcher_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'kitty', 'launcher') path = f'{launcher_dir}{os.pathsep}{path}' - python_for_type_check() print('Running under CI:', BaseTest.is_ci) if report_env: print('Using PATH in test environment:', path) - print('Python:', python_for_type_check()) from kitty.fast_data_types import has_avx2, has_sse4_2 print(f'Intrinsics: {has_avx2=} {has_sse4_2=}') # we need fonts installed in the user home directory as well, so initialize diff --git a/pyproject.toml b/pyproject.toml index 46cd5ca11..9a7b994bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -requires-python = ">=3.11" +requires-python = ">=3.12" license = "GPL-3.0-only" [build-system] @@ -10,30 +10,15 @@ requires = [ "packaging == 23.1", ] -[tool.mypy] -files = 'kitty,kittens,glfw,*.py,docs/conf.py,gen' -no_implicit_optional = true -sqlite_cache = true -cache_fine_grained = true -warn_redundant_casts = true -warn_unused_ignores = true -warn_return_any = true -warn_unreachable = true -warn_unused_configs = true -check_untyped_defs = true -disallow_untyped_defs = true -disallow_untyped_decorators = true -disallow_untyped_calls = true -disallow_incomplete_defs = true -strict = true -strict_bytes = true -no_implicit_reexport = true - -[tool.pylsp-mypy] -enabled = true -dmypy = true -exclude = ['kitty_tests/*'] -report_progress = true +[tool.ty.src] +include = [ + "kitty", + "kittens", + "glfw", + "*.py", + "docs/conf.py", + "gen" +] [tool.ruff] line-length = 160 diff --git a/setup.py b/setup.py index 0f0a7edd4..e41507df2 100755 --- a/setup.py +++ b/setup.py @@ -1369,8 +1369,11 @@ def read_bool_options(path: str = 'kitty/cli.py') -> Tuple[str, ...]: def build_launcher(args: Options, launcher_dir: str = '.', bundle_type: str = 'source') -> str: - werror = '' if args.ignore_compiler_warnings else '-pedantic-errors -Werror' - cflags = f'-Wall {werror} -fpie {c_std}'.strip().split() + cflags = ['-Wall'] + if not args.ignore_compiler_warnings: + cflags.extend(('-pedantic-errors', '-Werror')) + if c_std: + cflags.append(c_std) cppflags = [define(f'WRAPPED_KITTENS=" {wrapped_kittens()} "')] ldflags = shlex.split(os.environ.get('LDFLAGS', '')) xxhash = xxhash_flags()