diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0639d2c..fc89a53 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: rev: v2.6.0 hooks: - id: reorder-python-imports - args: [--py3-plus] + args: [--py3-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v2.1.0 hooks: @@ -33,7 +33,7 @@ repos: rev: v2.25.0 hooks: - id: pyupgrade - args: [--py36-plus] + args: [--py37-plus] - repo: https://github.com/asottile/setup-cfg-fmt rev: v1.17.0 hooks: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 0678a20..8e078c5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -15,5 +15,5 @@ resources: jobs: - template: job--python-tox.yml@asottile parameters: - toxenvs: [pypy3, py36, py37, py38, py39] + toxenvs: [py37, py38, py39] os: linux diff --git a/babi/__main__.py b/babi/__main__.py index 056a5f3..ff09674 100644 --- a/babi/__main__.py +++ b/babi/__main__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from babi.main import main if __name__ == '__main__': diff --git a/babi/_types.py b/babi/_types.py index b3f97e0..aa879df 100644 --- a/babi/_types.py +++ b/babi/_types.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import TYPE_CHECKING if TYPE_CHECKING: diff --git a/babi/buf.py b/babi/buf.py index 9b1cec4..d2df483 100644 --- a/babi/buf.py +++ b/babi/buf.py @@ -1,12 +1,11 @@ +from __future__ import annotations + import bisect import contextlib from typing import Callable from typing import Generator from typing import Iterator -from typing import List from typing import NamedTuple -from typing import Optional -from typing import Tuple from babi._types import Protocol from babi.horizontal_scrolling import line_x @@ -19,7 +18,7 @@ DelCallback = Callable[['Buf', int, str], None] InsCallback = Callable[['Buf', int], None] -def _offsets(s: str, tab_size: int) -> Tuple[int, ...]: +def _offsets(s: str, tab_size: int) -> tuple[int, ...]: ret = [0] for c in s: if c == '\t': @@ -30,14 +29,14 @@ def _offsets(s: str, tab_size: int) -> Tuple[int, ...]: class Modification(Protocol): - def __call__(self, buf: 'Buf') -> None: ... + def __call__(self, buf: Buf) -> None: ... class SetModification(NamedTuple): idx: int s: str - def __call__(self, buf: 'Buf') -> None: + def __call__(self, buf: Buf) -> None: buf[self.idx] = self.s @@ -45,29 +44,29 @@ class InsModification(NamedTuple): idx: int s: str - def __call__(self, buf: 'Buf') -> None: + def __call__(self, buf: Buf) -> None: buf.insert(self.idx, self.s) class DelModification(NamedTuple): idx: int - def __call__(self, buf: 'Buf') -> None: + def __call__(self, buf: Buf) -> None: del buf[self.idx] class Buf: - def __init__(self, lines: List[str], tab_size: int = 4) -> None: + def __init__(self, lines: list[str], tab_size: int = 4) -> None: self._lines = lines self.expandtabs = True self.tab_size = tab_size self.file_y = self.y = self._x = self._x_hint = 0 - self._set_callbacks: List[SetCallback] = [self._set_cb] - self._del_callbacks: List[DelCallback] = [self._del_cb] - self._ins_callbacks: List[InsCallback] = [self._ins_cb] + self._set_callbacks: list[SetCallback] = [self._set_cb] + self._del_callbacks: list[DelCallback] = [self._del_cb] + self._ins_callbacks: list[InsCallback] = [self._ins_cb] - self._positions: List[Optional[Tuple[int, ...]]] = [] + self._positions: list[tuple[int, ...] | None] = [] # read only interface @@ -163,16 +162,16 @@ class Buf: self._ins_callbacks.remove(cb) @contextlib.contextmanager - def record(self) -> Generator[List[Modification], None, None]: - modifications: List[Modification] = [] + def record(self) -> Generator[list[Modification], None, None]: + modifications: list[Modification] = [] - def set_cb(buf: 'Buf', idx: int, victim: str) -> None: + def set_cb(buf: Buf, idx: int, victim: str) -> None: modifications.append(SetModification(idx, victim)) - def del_cb(buf: 'Buf', idx: int, victim: str) -> None: + def del_cb(buf: Buf, idx: int, victim: str) -> None: modifications.append(InsModification(idx, victim)) - def ins_cb(buf: 'Buf', idx: int) -> None: + def ins_cb(buf: Buf, idx: int) -> None: modifications.append(DelModification(idx)) self.add_set_callback(set_cb) @@ -185,7 +184,7 @@ class Buf: self.remove_del_callback(del_cb) self.remove_set_callback(set_cb) - def apply(self, modifications: List[Modification]) -> List[Modification]: + def apply(self, modifications: list[Modification]) -> list[Modification]: with self.record() as ret_modifications: for modification in reversed(modifications): modification(self) @@ -209,19 +208,19 @@ class Buf: def _extend_positions(self, idx: int) -> None: self._positions.extend([None] * (1 + idx - len(self._positions))) - def _set_cb(self, buf: 'Buf', idx: int, victim: str) -> None: + def _set_cb(self, buf: Buf, idx: int, victim: str) -> None: self._extend_positions(idx) self._positions[idx] = None - def _del_cb(self, buf: 'Buf', idx: int, victim: str) -> None: + def _del_cb(self, buf: Buf, idx: int, victim: str) -> None: self._extend_positions(idx) del self._positions[idx] - def _ins_cb(self, buf: 'Buf', idx: int) -> None: + def _ins_cb(self, buf: Buf, idx: int) -> None: self._extend_positions(idx) self._positions.insert(idx, None) - def line_positions(self, idx: int) -> Tuple[int, ...]: + def line_positions(self, idx: int) -> tuple[int, ...]: self._extend_positions(idx) value = self._positions[idx] if value is None: @@ -236,7 +235,7 @@ class Buf: def _cursor_x(self) -> int: return self.line_positions(self.y)[self.x] - def cursor_position(self, margin: Margin) -> Tuple[int, int]: + def cursor_position(self, margin: Margin) -> tuple[int, int]: y = self.y - self.file_y + margin.header x = self._cursor_x - self.line_x(margin) return y, x diff --git a/babi/cached_property.py b/babi/cached_property.py index 7ce276e..78764f4 100644 --- a/babi/cached_property.py +++ b/babi/cached_property.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys if sys.version_info >= (3, 8): # pragma: no cover (>=py38) @@ -5,8 +7,6 @@ if sys.version_info >= (3, 8): # pragma: no cover (>=py38) else: # pragma: no cover ( TRet: assert instance is not None ret = instance.__dict__[self._func.__name__] = self._func(instance) diff --git a/babi/color.py b/babi/color.py index 02fa120..d549652 100644 --- a/babi/color.py +++ b/babi/color.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import NamedTuple # TODO: find a standard which defines these @@ -11,7 +13,7 @@ class Color(NamedTuple): b: int @classmethod - def parse(cls, s: str) -> 'Color': + def parse(cls, s: str) -> Color: if s.startswith('#') and len(s) >= 7: return cls(r=int(s[1:3], 16), g=int(s[3:5], 16), b=int(s[5:7], 16)) elif s.startswith('#'): diff --git a/babi/color_kd.py b/babi/color_kd.py index e5b1631..4bfd0e7 100644 --- a/babi/color_kd.py +++ b/babi/color_kd.py @@ -1,9 +1,8 @@ +from __future__ import annotations + import functools import itertools -from typing import List from typing import NamedTuple -from typing import Optional -from typing import Tuple from babi._types import Protocol from babi.color import Color @@ -19,19 +18,19 @@ class KD(Protocol): @property def n(self) -> int: ... @property - def left(self) -> Optional['KD']: ... + def left(self) -> KD | None: ... @property - def right(self) -> Optional['KD']: ... + def right(self) -> KD | None: ... class _KD(NamedTuple): color: Color n: int - left: Optional[KD] - right: Optional[KD] + left: KD | None + right: KD | None -def _build(colors: List[Tuple[Color, int]], depth: int = 0) -> Optional[KD]: +def _build(colors: list[tuple[Color, int]], depth: int = 0) -> KD | None: if not colors: return None @@ -46,11 +45,11 @@ def _build(colors: List[Tuple[Color, int]], depth: int = 0) -> Optional[KD]: ) -def nearest(color: Color, colors: Optional[KD]) -> int: +def nearest(color: Color, colors: KD | None) -> int: best = 0 dist = 2 ** 32 - def _search(kd: Optional[KD], *, depth: int) -> None: + def _search(kd: KD | None, *, depth: int) -> None: nonlocal best nonlocal dist @@ -77,7 +76,7 @@ def nearest(color: Color, colors: Optional[KD]) -> int: @functools.lru_cache(maxsize=1) -def make_256() -> Optional[KD]: +def make_256() -> KD | None: vals = (0, 95, 135, 175, 215, 255) colors = [ (Color(r, g, b), i) diff --git a/babi/color_manager.py b/babi/color_manager.py index 6970c39..614b2ea 100644 --- a/babi/color_manager.py +++ b/babi/color_manager.py @@ -1,21 +1,20 @@ +from __future__ import annotations + import curses -from typing import Dict from typing import NamedTuple -from typing import Optional -from typing import Tuple from babi import color_kd from babi.color import Color -def _color_to_curses(color: Color) -> Tuple[int, int, int]: +def _color_to_curses(color: Color) -> tuple[int, int, int]: factor = 1000 / 255 return int(color.r * factor), int(color.g * factor), int(color.b * factor) class ColorManager(NamedTuple): - colors: Dict[Color, int] - raw_pairs: Dict[Tuple[int, int], int] + colors: dict[Color, int] + raw_pairs: dict[tuple[int, int], int] def init_color(self, color: Color) -> None: if curses.can_change_color(): @@ -27,7 +26,7 @@ class ColorManager(NamedTuple): else: self.colors[color] = -1 - def color_pair(self, fg: Optional[Color], bg: Optional[Color]) -> int: + def color_pair(self, fg: Color | None, bg: Color | None) -> int: fg_i = self.colors[fg] if fg is not None else -1 bg_i = self.colors[bg] if bg is not None else -1 return self.raw_color_pair(fg_i, bg_i) @@ -46,5 +45,5 @@ class ColorManager(NamedTuple): return 0 @classmethod - def make(cls) -> 'ColorManager': + def make(cls) -> ColorManager: return cls({}, {}) diff --git a/babi/fdict.py b/babi/fdict.py index 0613e83..f3ba9dd 100644 --- a/babi/fdict.py +++ b/babi/fdict.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Generic from typing import Iterable from typing import Mapping diff --git a/babi/file.py b/babi/file.py index 0a3d05c..0f3e9f2 100644 --- a/babi/file.py +++ b/babi/file.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import collections import contextlib import curses @@ -12,15 +14,11 @@ from typing import Callable from typing import cast from typing import Generator from typing import IO -from typing import List from typing import Match from typing import NamedTuple -from typing import Optional from typing import Pattern -from typing import Tuple from typing import TYPE_CHECKING from typing import TypeVar -from typing import Union from babi.buf import Buf from babi.buf import Modification @@ -42,7 +40,7 @@ TCallable = TypeVar('TCallable', bound=Callable[..., Any]) WS_RE = re.compile(r'^\s*') -def get_lines(sio: IO[str]) -> Tuple[List[str], str, bool, str]: +def get_lines(sio: IO[str]) -> tuple[list[str], str, bool, str]: sha256 = hashlib.sha256() lines = [] newlines = collections.Counter({'\n': 0}) # default to `\n` @@ -64,7 +62,7 @@ def get_lines(sio: IO[str]) -> Tuple[List[str], str, bool, str]: class Action: def __init__( - self, *, name: str, modifications: List[Modification], + self, *, name: str, modifications: list[Modification], start_x: int, start_y: int, start_modified: bool, end_x: int, end_y: int, end_modified: bool, final: bool, @@ -79,7 +77,7 @@ class Action: self.end_modified = end_modified self.final = final - def apply(self, file: 'File') -> 'Action': + def apply(self, file: File) -> Action: action = Action( name=self.name, modifications=file.buf.apply(self.modifications), start_x=self.end_x, start_y=self.end_y, @@ -98,7 +96,7 @@ class Action: def action(func: TCallable) -> TCallable: @functools.wraps(func) - def action_inner(self: 'File', *args: Any, **kwargs: Any) -> Any: + def action_inner(self: File, *args: Any, **kwargs: Any) -> Any: self.finalize_previous_action() return func(self, *args, **kwargs) return cast(TCallable, action_inner) @@ -111,7 +109,7 @@ def edit_action( ) -> Callable[[TCallable], TCallable]: def edit_action_decorator(func: TCallable) -> TCallable: @functools.wraps(func) - def edit_action_inner(self: 'File', *args: Any, **kwargs: Any) -> Any: + def edit_action_inner(self: File, *args: Any, **kwargs: Any) -> Any: with self.edit_action_context(name, final=final): return func(self, *args, **kwargs) return cast(TCallable, edit_action_inner) @@ -120,7 +118,7 @@ def edit_action( def keep_selection(func: TCallable) -> TCallable: @functools.wraps(func) - def keep_selection_inner(self: 'File', *args: Any, **kwargs: Any) -> Any: + def keep_selection_inner(self: File, *args: Any, **kwargs: Any) -> Any: with self.select(): return func(self, *args, **kwargs) return cast(TCallable, keep_selection_inner) @@ -128,7 +126,7 @@ def keep_selection(func: TCallable) -> TCallable: def clear_selection(func: TCallable) -> TCallable: @functools.wraps(func) - def clear_selection_inner(self: 'File', *args: Any, **kwargs: Any) -> Any: + def clear_selection_inner(self: File, *args: Any, **kwargs: Any) -> Any: ret = func(self, *args, **kwargs) self.selection.clear() return ret @@ -143,7 +141,7 @@ class Found(NamedTuple): class _SearchIter: def __init__( self, - file: 'File', + file: File, reg: Pattern[str], *, offset: int, @@ -155,7 +153,7 @@ class _SearchIter: self._start_x = file.buf.x + offset self._start_y = file.buf.y - def __iter__(self) -> '_SearchIter': + def __iter__(self) -> _SearchIter: return self def _stop_if_past_original(self, y: int, match: Match[str]) -> Found: @@ -168,7 +166,7 @@ class _SearchIter: raise StopIteration() return Found(y, match) - def __next__(self) -> Tuple[int, Match[str]]: + def __next__(self) -> tuple[int, Match[str]]: x = self.file.buf.x + self.offset y = self.file.buf.y @@ -200,25 +198,25 @@ class _SearchIter: class File: def __init__( self, - filename: Optional[str], + filename: str | None, initial_line: int, color_manager: ColorManager, - hl_factories: Tuple[HLFactory, ...], + hl_factories: tuple[HLFactory, ...], ) -> None: self.filename = filename self.initial_line = initial_line self.modified = False self.buf = Buf([]) self.nl = '\n' - self.sha256: Optional[str] = None + self.sha256: str | None = None self._in_edit_action = False - self.undo_stack: List[Action] = [] - self.redo_stack: List[Action] = [] + self.undo_stack: list[Action] = [] + self.redo_stack: list[Action] = [] self._hl_factories = hl_factories self._trailing_whitespace = TrailingWhitespace(color_manager) self._replace_hl = Replace() self.selection = Selection() - self._file_hls: Tuple[FileHL, ...] = () + self._file_hls: tuple[FileHL, ...] = () def ensure_loaded( self, @@ -395,14 +393,14 @@ class File: @clear_selection def replace( self, - screen: 'Screen', + screen: Screen, reg: Pattern[str], replace: str, ) -> None: self.finalize_previous_action() count = 0 - res: Union[str, PromptResult] = '' + res: str | PromptResult = '' search = _SearchIter(self, reg, offset=0) for line_y, match in search: end = match.end() @@ -597,7 +595,7 @@ class File: @edit_action('cut selection', final=True) @clear_selection - def cut_selection(self, margin: Margin) -> Tuple[str, ...]: + def cut_selection(self, margin: Margin) -> tuple[str, ...]: ret = [] (s_y, s_x), (e_y, e_x) = self.selection.get() if s_y == e_y: @@ -617,7 +615,7 @@ class File: self.buf.scroll_screen_if_needed(margin) return tuple(ret) - def cut(self, cut_buffer: Tuple[str, ...]) -> Tuple[str, ...]: + def cut(self, cut_buffer: tuple[str, ...]) -> tuple[str, ...]: # only continue a cut if the last action is a non-final cut if not self._continue_last_action('cut'): cut_buffer = () @@ -630,7 +628,7 @@ class File: self.buf.x = 0 return cut_buffer + (victim,) - def _uncut(self, cut_buffer: Tuple[str, ...], margin: Margin) -> None: + def _uncut(self, cut_buffer: tuple[str, ...], margin: Margin) -> None: for cut_line in cut_buffer: line = self.buf[self.buf.y] before, after = line[:self.buf.x], line[self.buf.x:] @@ -641,14 +639,14 @@ class File: @edit_action('uncut', final=True) @clear_selection - def uncut(self, cut_buffer: Tuple[str, ...], margin: Margin) -> None: + def uncut(self, cut_buffer: tuple[str, ...], margin: Margin) -> None: self._uncut(cut_buffer, margin) @edit_action('uncut selection', final=True) @clear_selection def uncut_selection( self, - cut_buffer: Tuple[str, ...], margin: Margin, + cut_buffer: tuple[str, ...], margin: Margin, ) -> None: self._uncut(cut_buffer, margin) self.buf.up(margin) @@ -666,7 +664,7 @@ class File: self.buf.x = 0 self.buf.scroll_screen_if_needed(margin) - def _selection_lines(self) -> Tuple[int, int]: + def _selection_lines(self) -> tuple[int, int]: (s_y, _), (e_y, _) = self.selection.get() e_y = min(e_y + 1, len(self.buf) - 1) if self.buf[e_y - 1] == '': @@ -852,12 +850,12 @@ class File: def move_cursor( self, - stdscr: 'curses._CursesWindow', + stdscr: curses._CursesWindow, margin: Margin, ) -> None: stdscr.move(*self.buf.cursor_position(margin)) - def draw(self, stdscr: 'curses._CursesWindow', margin: Margin) -> None: + def draw(self, stdscr: curses._CursesWindow, margin: Margin) -> None: to_display = min(self.buf.displayable_count, margin.body_lines) for file_hl in self._file_hls: diff --git a/babi/highlight.py b/babi/highlight.py index faa530e..cbd2f75 100644 --- a/babi/highlight.py +++ b/babi/highlight.py @@ -1,13 +1,11 @@ +from __future__ import annotations + import functools import json import os.path from typing import Any -from typing import Dict -from typing import FrozenSet -from typing import List from typing import Match from typing import NamedTuple -from typing import Optional from typing import Tuple from typing import TypeVar @@ -34,7 +32,7 @@ def uniquely_constructed(t: T) -> T: return t -def _split_name(s: Optional[str]) -> Tuple[str, ...]: +def _split_name(s: str | None) -> tuple[str, ...]: if s is None: return () else: @@ -44,17 +42,17 @@ def _split_name(s: Optional[str]) -> Tuple[str, ...]: class _Rule(Protocol): """hax for recursive types python/mypy#731""" @property - def name(self) -> Tuple[str, ...]: ... + def name(self) -> tuple[str, ...]: ... @property - def match(self) -> Optional[str]: ... + def match(self) -> str | None: ... @property - def begin(self) -> Optional[str]: ... + def begin(self) -> str | None: ... @property - def end(self) -> Optional[str]: ... + def end(self) -> str | None: ... @property - def while_(self) -> Optional[str]: ... + def while_(self) -> str | None: ... @property - def content_name(self) -> Tuple[str, ...]: ... + def content_name(self) -> tuple[str, ...]: ... @property def captures(self) -> Captures: ... @property @@ -64,39 +62,39 @@ class _Rule(Protocol): @property def while_captures(self) -> Captures: ... @property - def include(self) -> Optional[str]: ... + def include(self) -> str | None: ... @property - def patterns(self) -> 'Tuple[_Rule, ...]': ... + def patterns(self) -> tuple[_Rule, ...]: ... @property - def repository(self) -> 'FChainMap[str, _Rule]': ... + def repository(self) -> FChainMap[str, _Rule]: ... @uniquely_constructed class Rule(NamedTuple): - name: Tuple[str, ...] - match: Optional[str] - begin: Optional[str] - end: Optional[str] - while_: Optional[str] - content_name: Tuple[str, ...] + name: tuple[str, ...] + match: str | None + begin: str | None + end: str | None + while_: str | None + content_name: tuple[str, ...] captures: Captures begin_captures: Captures end_captures: Captures while_captures: Captures - include: Optional[str] - patterns: Tuple[_Rule, ...] + include: str | None + patterns: tuple[_Rule, ...] repository: FChainMap[str, _Rule] @classmethod def make( cls, - dct: Dict[str, Any], + dct: dict[str, Any], parent_repository: FChainMap[str, _Rule], ) -> _Rule: if 'repository' in dct: # this looks odd, but it's so we can have a self-referential # immutable-after-construction chain map - repository_dct: Dict[str, _Rule] = {} + repository_dct: dict[str, _Rule] = {} repository = FChainMap(parent_repository, repository_dct) for k, sub_dct in dct['repository'].items(): repository_dct[k] = Rule.make(sub_dct, repository) @@ -183,15 +181,15 @@ class Rule(NamedTuple): class Grammar(NamedTuple): scope_name: str repository: FChainMap[str, _Rule] - patterns: Tuple[_Rule, ...] + patterns: tuple[_Rule, ...] @classmethod - def make(cls, data: Dict[str, Any]) -> 'Grammar': + def make(cls, data: dict[str, Any]) -> Grammar: scope_name = data['scopeName'] if 'repository' in data: # this looks odd, but it's so we can have a self-referential # immutable-after-construction chain map - repository_dct: Dict[str, _Rule] = {} + repository_dct: dict[str, _Rule] = {} repository = FChainMap(repository_dct) for k, dct in data['repository'].items(): repository_dct[k] = Rule.make(dct, repository) @@ -212,54 +210,54 @@ class Region(NamedTuple): class State(NamedTuple): - entries: Tuple['Entry', ...] - while_stack: Tuple[Tuple['WhileRule', int], ...] + entries: tuple[Entry, ...] + while_stack: tuple[tuple[WhileRule, int], ...] @classmethod - def root(cls, entry: 'Entry') -> 'State': + def root(cls, entry: Entry) -> State: return cls((entry,), ()) @property - def cur(self) -> 'Entry': + def cur(self) -> Entry: return self.entries[-1] - def push(self, entry: 'Entry') -> 'State': + def push(self, entry: Entry) -> State: return self._replace(entries=(*self.entries, entry)) - def pop(self) -> 'State': + def pop(self) -> State: return self._replace(entries=self.entries[:-1]) - def push_while(self, rule: 'WhileRule', entry: 'Entry') -> 'State': + def push_while(self, rule: WhileRule, entry: Entry) -> State: entries = (*self.entries, entry) while_stack = (*self.while_stack, (rule, len(entries))) return self._replace(entries=entries, while_stack=while_stack) - def pop_while(self) -> 'State': + def pop_while(self) -> State: entries, while_stack = self.entries[:-1], self.while_stack[:-1] return self._replace(entries=entries, while_stack=while_stack) class CompiledRule(Protocol): @property - def name(self) -> Tuple[str, ...]: ... + def name(self) -> tuple[str, ...]: ... def start( self, - compiler: 'Compiler', + compiler: Compiler, match: Match[str], state: State, - ) -> Tuple[State, bool, Regions]: + ) -> tuple[State, bool, Regions]: ... def search( self, - compiler: 'Compiler', + compiler: Compiler, state: State, line: str, pos: int, first_line: bool, boundary: bool, - ) -> Optional[Tuple[State, int, bool, Regions]]: + ) -> tuple[State, int, bool, Regions] | None: ... @@ -267,19 +265,19 @@ class CompiledRegsetRule(CompiledRule, Protocol): @property def regset(self) -> _RegSet: ... @property - def u_rules(self) -> Tuple[_Rule, ...]: ... + def u_rules(self) -> tuple[_Rule, ...]: ... class Entry(NamedTuple): - scope: Tuple[str, ...] + scope: tuple[str, ...] rule: CompiledRule - start: Tuple[str, int] + start: tuple[str, int] reg: _Reg = ERR_REG boundary: bool = False def _inner_capture_parse( - compiler: 'Compiler', + compiler: Compiler, start: int, s: str, scope: Scope, @@ -293,12 +291,12 @@ def _inner_capture_parse( def _captures( - compiler: 'Compiler', + compiler: Compiler, scope: Scope, match: Match[str], captures: Captures, ) -> Regions: - ret: List[Region] = [] + ret: list[Region] = [] pos, pos_end = match.span() for i, u_rule in captures: try: @@ -347,12 +345,12 @@ def _captures( def _do_regset( idx: int, - match: Optional[Match[str]], + match: Match[str] | None, rule: CompiledRegsetRule, - compiler: 'Compiler', + compiler: Compiler, state: State, pos: int, -) -> Optional[Tuple[State, int, bool, Regions]]: +) -> tuple[State, int, bool, Regions] | None: if match is None: return None @@ -369,73 +367,73 @@ def _do_regset( @uniquely_constructed class PatternRule(NamedTuple): - name: Tuple[str, ...] + name: tuple[str, ...] regset: _RegSet - u_rules: Tuple[_Rule, ...] + u_rules: tuple[_Rule, ...] def start( self, - compiler: 'Compiler', + compiler: Compiler, match: Match[str], state: State, - ) -> Tuple[State, bool, Regions]: + ) -> tuple[State, bool, Regions]: raise AssertionError(f'unreachable {self}') def search( self, - compiler: 'Compiler', + compiler: Compiler, state: State, line: str, pos: int, first_line: bool, boundary: bool, - ) -> Optional[Tuple[State, int, bool, Regions]]: + ) -> tuple[State, int, bool, Regions] | None: idx, match = self.regset.search(line, pos, first_line, boundary) return _do_regset(idx, match, self, compiler, state, pos) @uniquely_constructed class MatchRule(NamedTuple): - name: Tuple[str, ...] + name: tuple[str, ...] captures: Captures def start( self, - compiler: 'Compiler', + compiler: Compiler, match: Match[str], state: State, - ) -> Tuple[State, bool, Regions]: + ) -> tuple[State, bool, Regions]: scope = state.cur.scope + self.name return state, False, _captures(compiler, scope, match, self.captures) def search( self, - compiler: 'Compiler', + compiler: Compiler, state: State, line: str, pos: int, first_line: bool, boundary: bool, - ) -> Optional[Tuple[State, int, bool, Regions]]: + ) -> tuple[State, int, bool, Regions] | None: raise AssertionError(f'unreachable {self}') @uniquely_constructed class EndRule(NamedTuple): - name: Tuple[str, ...] - content_name: Tuple[str, ...] + name: tuple[str, ...] + content_name: tuple[str, ...] begin_captures: Captures end_captures: Captures end: str regset: _RegSet - u_rules: Tuple[_Rule, ...] + u_rules: tuple[_Rule, ...] def start( self, - compiler: 'Compiler', + compiler: Compiler, match: Match[str], state: State, - ) -> Tuple[State, bool, Regions]: + ) -> tuple[State, bool, Regions]: scope = state.cur.scope + self.name next_scope = scope + self.content_name @@ -448,11 +446,11 @@ class EndRule(NamedTuple): def _end_ret( self, - compiler: 'Compiler', + compiler: Compiler, state: State, pos: int, m: Match[str], - ) -> Tuple[State, int, bool, Regions]: + ) -> tuple[State, int, bool, Regions]: ret = [] if m.start() > pos: ret.append(Region(pos, m.start(), state.cur.scope)) @@ -470,13 +468,13 @@ class EndRule(NamedTuple): def search( self, - compiler: 'Compiler', + compiler: Compiler, state: State, line: str, pos: int, first_line: bool, boundary: bool, - ) -> Optional[Tuple[State, int, bool, Regions]]: + ) -> tuple[State, int, bool, Regions] | None: end_match = state.cur.reg.search(line, pos, first_line, boundary) if end_match is not None and end_match.start() == pos: return self._end_ret(compiler, state, pos, end_match) @@ -493,20 +491,20 @@ class EndRule(NamedTuple): @uniquely_constructed class WhileRule(NamedTuple): - name: Tuple[str, ...] - content_name: Tuple[str, ...] + name: tuple[str, ...] + content_name: tuple[str, ...] begin_captures: Captures while_captures: Captures while_: str regset: _RegSet - u_rules: Tuple[_Rule, ...] + u_rules: tuple[_Rule, ...] def start( self, - compiler: 'Compiler', + compiler: Compiler, match: Match[str], state: State, - ) -> Tuple[State, bool, Regions]: + ) -> tuple[State, bool, Regions]: scope = state.cur.scope + self.name next_scope = scope + self.content_name @@ -520,13 +518,13 @@ class WhileRule(NamedTuple): def continues( self, - compiler: 'Compiler', + compiler: Compiler, state: State, line: str, pos: int, first_line: bool, boundary: bool, - ) -> Optional[Tuple[int, bool, Regions]]: + ) -> tuple[int, bool, Regions] | None: match = state.cur.reg.match(line, pos, first_line, boundary) if match is None: return None @@ -536,23 +534,23 @@ class WhileRule(NamedTuple): def search( self, - compiler: 'Compiler', + compiler: Compiler, state: State, line: str, pos: int, first_line: bool, boundary: bool, - ) -> Optional[Tuple[State, int, bool, Regions]]: + ) -> tuple[State, int, bool, Regions] | None: idx, match = self.regset.search(line, pos, first_line, boundary) return _do_regset(idx, match, self, compiler, state, pos) class Compiler: - def __init__(self, grammar: Grammar, grammars: 'Grammars') -> None: + def __init__(self, grammar: Grammar, grammars: Grammars) -> None: self._root_scope = grammar.scope_name self._grammars = grammars - self._rule_to_grammar: Dict[_Rule, Grammar] = {} - self._c_rules: Dict[_Rule, CompiledRule] = {} + self._rule_to_grammar: dict[_Rule, Grammar] = {} + self._c_rules: dict[_Rule, CompiledRule] = {} root = self._compile_root(grammar) self.root_state = State.root(Entry(root.name, root, ('', 0))) @@ -566,7 +564,7 @@ class Compiler: grammar: Grammar, repository: FChainMap[str, _Rule], s: str, - ) -> Tuple[List[str], Tuple[_Rule, ...]]: + ) -> tuple[list[str], tuple[_Rule, ...]]: if s == '$self': return self._patterns(grammar, grammar.patterns) elif s == '$base': @@ -586,10 +584,10 @@ class Compiler: def _patterns( self, grammar: Grammar, - rules: Tuple[_Rule, ...], - ) -> Tuple[List[str], Tuple[_Rule, ...]]: + rules: tuple[_Rule, ...], + ) -> tuple[list[str], tuple[_Rule, ...]]: ret_regs = [] - ret_rules: List[_Rule] = [] + ret_rules: list[_Rule] = [] for rule in rules: if rule.include is not None: tmp_regs, tmp_rules = self._include( @@ -676,12 +674,12 @@ class Grammars: unknown_grammar = {'scopeName': 'source.unknown', 'patterns': []} self._raw = {'source.unknown': unknown_grammar} - self._file_types: List[Tuple[FrozenSet[str], str]] = [] - self._first_line: List[Tuple[_Reg, str]] = [] - self._parsed: Dict[str, Grammar] = {} - self._compiled: Dict[str, Compiler] = {} + self._file_types: list[tuple[frozenset[str], str]] = [] + self._first_line: list[tuple[_Reg, str]] = [] + self._parsed: dict[str, Grammar] = {} + self._compiled: dict[str, Compiler] = {} - def _raw_for_scope(self, scope: str) -> Dict[str, Any]: + def _raw_for_scope(self, scope: str) -> dict[str, Any]: try: return self._raw[scope] except KeyError: @@ -747,12 +745,12 @@ class Grammars: def highlight_line( - compiler: 'Compiler', + compiler: Compiler, state: State, line: str, first_line: bool, -) -> Tuple[State, Regions]: - ret: List[Region] = [] +) -> tuple[State, Regions]: + ret: list[Region] = [] pos = 0 boundary = state.cur.boundary diff --git a/babi/history.py b/babi/history.py index 1d7204c..a4fceb5 100644 --- a/babi/history.py +++ b/babi/history.py @@ -1,18 +1,18 @@ +from __future__ import annotations + import collections import contextlib import os.path -from typing import Dict from typing import Generator -from typing import List from babi.user_data import xdg_data class History: def __init__(self) -> None: - self._orig_len: Dict[str, int] = collections.defaultdict(int) - self.data: Dict[str, List[str]] = collections.defaultdict(list) - self.prev: Dict[str, str] = {} + self._orig_len: dict[str, int] = collections.defaultdict(int) + self.data: dict[str, list[str]] = collections.defaultdict(list) + self.prev: dict[str, str] = {} @contextlib.contextmanager def save(self) -> Generator[None, None, None]: diff --git a/babi/hl/interface.py b/babi/hl/interface.py index 9e73cfa..3bd7f0b 100644 --- a/babi/hl/interface.py +++ b/babi/hl/interface.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import NamedTuple from typing import Tuple diff --git a/babi/hl/replace.py b/babi/hl/replace.py index b8918fb..a52e7f8 100644 --- a/babi/hl/replace.py +++ b/babi/hl/replace.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import collections import contextlib import curses -from typing import Dict from typing import Generator from babi.buf import Buf @@ -13,7 +14,7 @@ class Replace: include_edge = True def __init__(self) -> None: - self.regions: Dict[int, HLs] = collections.defaultdict(tuple) + self.regions: dict[int, HLs] = collections.defaultdict(tuple) def highlight_until(self, lines: Buf, idx: int) -> None: """our highlight regions are populated in other ways""" diff --git a/babi/hl/selection.py b/babi/hl/selection.py index 0b91acc..6a8bd12 100644 --- a/babi/hl/selection.py +++ b/babi/hl/selection.py @@ -1,8 +1,7 @@ +from __future__ import annotations + import collections import curses -from typing import Dict -from typing import Optional -from typing import Tuple from babi.buf import Buf from babi.hl.interface import HL @@ -13,9 +12,9 @@ class Selection: include_edge = True def __init__(self) -> None: - self.regions: Dict[int, HLs] = collections.defaultdict(tuple) - self.start: Optional[Tuple[int, int]] = None - self.end: Optional[Tuple[int, int]] = None + self.regions: dict[int, HLs] = collections.defaultdict(tuple) + self.start: tuple[int, int] | None = None + self.end: tuple[int, int] | None = None def register_callbacks(self, buf: Buf) -> None: """our highlight regions are populated in other ways""" @@ -39,7 +38,7 @@ class Selection: ) self.regions[e_y] = (HL(x=0, end=e_x, attr=attr),) - def get(self) -> Tuple[Tuple[int, int], Tuple[int, int]]: + def get(self) -> tuple[tuple[int, int], tuple[int, int]]: assert self.start is not None and self.end is not None if self.start < self.end: return self.start, self.end diff --git a/babi/hl/syntax.py b/babi/hl/syntax.py index ba0b3cc..c0e962d 100644 --- a/babi/hl/syntax.py +++ b/babi/hl/syntax.py @@ -1,11 +1,10 @@ +from __future__ import annotations + import curses import functools import math from typing import Callable -from typing import List from typing import NamedTuple -from typing import Optional -from typing import Tuple from babi.buf import Buf from babi.color_manager import ColorManager @@ -21,8 +20,6 @@ from babi.user_data import prefix_data from babi.user_data import xdg_config from babi.user_data import xdg_data -A_ITALIC = getattr(curses, 'A_ITALIC', 0x80000000) # new in py37 - class FileSyntax: include_edge = False @@ -37,12 +34,12 @@ class FileSyntax: self._theme = theme self._color_manager = color_manager - self.regions: List[HLs] = [] - self._states: List[State] = [] + self.regions: list[HLs] = [] + self._states: list[State] = [] # this will be assigned a functools.lru_cache per instance for # better hit rate and memory usage - self._hl: Optional[Callable[[State, str, bool], Tuple[State, HLs]]] + self._hl: Callable[[State, str, bool], tuple[State, HLs]] | None self._hl = None def attr(self, style: Style) -> int: @@ -50,7 +47,7 @@ class FileSyntax: return ( curses.color_pair(pair) | curses.A_BOLD * style.b | - A_ITALIC * style.i | + curses.A_ITALIC * style.i | curses.A_UNDERLINE * style.u ) @@ -59,7 +56,7 @@ class FileSyntax: state: State, line: str, first_line: bool, - ) -> Tuple[State, HLs]: + ) -> tuple[State, HLs]: new_state, regions = highlight_line( self._compiler, state, f'{line}\n', first_line=first_line, ) @@ -68,7 +65,7 @@ class FileSyntax: new_end = regions[-1]._replace(end=regions[-1].end - 1) regions = regions[:-1] + (new_end,) - regs: List[HL] = [] + regs: list[HL] = [] for r in regions: style = self._theme.select(r.scope) if style == self._theme.default: @@ -133,7 +130,7 @@ class Syntax(NamedTuple): compiler = self.grammars.blank_compiler() return FileSyntax(compiler, self.theme, self.color_manager) - def _init_screen(self, stdscr: 'curses._CursesWindow') -> None: + def _init_screen(self, stdscr: curses._CursesWindow) -> None: default_fg, default_bg = self.theme.default.fg, self.theme.default.bg all_colors = {c for c in (default_fg, default_bg) if c is not None} todo = list(self.theme.rules.children.values()) @@ -154,9 +151,9 @@ class Syntax(NamedTuple): @classmethod def from_screen( cls, - stdscr: 'curses._CursesWindow', + stdscr: curses._CursesWindow, color_manager: ColorManager, - ) -> 'Syntax': + ) -> Syntax: grammars = Grammars(prefix_data('grammar_v1'), xdg_data('grammar_v1')) theme = Theme.from_filename(xdg_config('theme.json')) ret = cls(grammars, theme, color_manager) diff --git a/babi/hl/trailing_whitespace.py b/babi/hl/trailing_whitespace.py index 383241a..73d058f 100644 --- a/babi/hl/trailing_whitespace.py +++ b/babi/hl/trailing_whitespace.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import curses -from typing import List from babi.buf import Buf from babi.color_manager import ColorManager @@ -13,7 +14,7 @@ class TrailingWhitespace: def __init__(self, color_manager: ColorManager) -> None: self._color_manager = color_manager - self.regions: List[HLs] = [] + self.regions: list[HLs] = [] def _trailing_ws(self, line: str) -> HLs: if not line: diff --git a/babi/horizontal_scrolling.py b/babi/horizontal_scrolling.py index ad6c7f4..acd3820 100644 --- a/babi/horizontal_scrolling.py +++ b/babi/horizontal_scrolling.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import curses from babi.cached_property import cached_property @@ -34,7 +36,7 @@ def scrolled_line(s: str, x: int, width: int) -> str: class _CalcWidth: @cached_property - def _window(self) -> 'curses._CursesWindow': + def _window(self) -> curses._CursesWindow: return curses.newwin(1, 10) def wcwidth(self, c: str) -> int: diff --git a/babi/main.py b/babi/main.py index 8d81505..959c9a6 100644 --- a/babi/main.py +++ b/babi/main.py @@ -1,13 +1,12 @@ +from __future__ import annotations + import argparse import curses import os import re import signal import sys -from typing import List -from typing import Optional from typing import Sequence -from typing import Tuple from babi.buf import Buf from babi.file import File @@ -44,9 +43,9 @@ def _edit(screen: Screen, stdin: str) -> EditResult: def c_main( - stdscr: 'curses._CursesWindow', - filenames: List[Optional[str]], - positions: List[int], + stdscr: curses._CursesWindow, + filenames: list[str | None], + positions: list[int], stdin: str, perf: Perf, ) -> int: @@ -73,7 +72,7 @@ def c_main( return 0 -def _key_debug(stdscr: 'curses._CursesWindow', perf: Perf) -> int: +def _key_debug(stdscr: curses._CursesWindow, perf: Perf) -> int: screen = Screen(stdscr, ['<>'], [0], perf) screen.file.buf = Buf(['']) @@ -91,11 +90,11 @@ def _key_debug(stdscr: 'curses._CursesWindow', perf: Perf) -> int: return 0 -def _filenames(filenames: List[str]) -> Tuple[List[Optional[str]], List[int]]: +def _filenames(filenames: list[str]) -> tuple[list[str | None], list[int]]: if not filenames: return [None], [0] - ret_filenames: List[Optional[str]] = [] + ret_filenames: list[str | None] = [] ret_positions = [] filenames_iter = iter(filenames) @@ -122,7 +121,7 @@ def _filenames(filenames: List[str]) -> Tuple[List[Optional[str]], List[int]]: return ret_filenames, ret_positions -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', metavar='filename', nargs='*') parser.add_argument('--perf-log') diff --git a/babi/margin.py b/babi/margin.py index fd4e9cf..b956226 100644 --- a/babi/margin.py +++ b/babi/margin.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import curses from typing import NamedTuple @@ -31,5 +33,5 @@ class Margin(NamedTuple): return int(self.lines / 2 + .5) @classmethod - def from_current_screen(cls) -> 'Margin': + def from_current_screen(cls) -> Margin: return cls(curses.LINES, curses.COLS) diff --git a/babi/perf.py b/babi/perf.py index d4002e4..e2d8056 100644 --- a/babi/perf.py +++ b/babi/perf.py @@ -1,18 +1,17 @@ +from __future__ import annotations + import contextlib import cProfile import time from typing import Generator -from typing import List -from typing import Optional -from typing import Tuple class Perf: def __init__(self) -> None: - self._prof: Optional[cProfile.Profile] = None - self._records: List[Tuple[str, float]] = [] - self._name: Optional[str] = None - self._time: Optional[float] = None + self._prof: cProfile.Profile | None = None + self._records: list[tuple[str, float]] = [] + self._name: str | None = None + self._time: float | None = None def start(self, name: str) -> None: if self._prof: @@ -43,7 +42,7 @@ class Perf: @contextlib.contextmanager -def perf_log(filename: Optional[str]) -> Generator[Perf, None, None]: +def perf_log(filename: str | None) -> Generator[Perf, None, None]: perf = Perf() if filename is None: yield perf diff --git a/babi/prompt.py b/babi/prompt.py index 23856a8..36d282f 100644 --- a/babi/prompt.py +++ b/babi/prompt.py @@ -1,10 +1,8 @@ +from __future__ import annotations + import curses import enum -from typing import List -from typing import Optional -from typing import Tuple from typing import TYPE_CHECKING -from typing import Union from babi.horizontal_scrolling import line_x from babi.horizontal_scrolling import scrolled_line @@ -16,7 +14,7 @@ PromptResult = enum.Enum('PromptResult', 'CANCELLED') class Prompt: - def __init__(self, screen: 'Screen', prompt: str, lst: List[str]) -> None: + def __init__(self, screen: Screen, prompt: str, lst: list[str]) -> None: self._screen = screen self._prompt = prompt self._lst = lst @@ -31,7 +29,7 @@ class Prompt: def _s(self, s: str) -> None: self._lst[self._y] = s - def _render_prompt(self, *, base: Optional[str] = None) -> None: + def _render_prompt(self, *, base: str | None = None) -> None: base = base or self._prompt if not base or self._screen.margin.cols < 7: prompt_s = '' @@ -100,7 +98,7 @@ class Prompt: def _resize(self) -> None: self._screen.resize() - def _check_failed(self, idx: int, s: str) -> Tuple[bool, int]: + def _check_failed(self, idx: int, s: str) -> tuple[bool, int]: failed = False for search_idx in range(idx, -1, -1): if s in self._lst[search_idx]: @@ -111,7 +109,7 @@ class Prompt: failed = True return failed, idx - def _reverse_search(self) -> Union[None, str, PromptResult]: + def _reverse_search(self) -> None | str | PromptResult: reverse_s = '' idx = self._y while True: @@ -177,7 +175,7 @@ class Prompt: self._s = self._s[:self._x] + c + self._s[self._x:] self._x += len(c) - def run(self) -> Union[PromptResult, str]: + def run(self) -> PromptResult | str: while True: self._render_prompt() diff --git a/babi/reg.py b/babi/reg.py index 9b59396..aa68bac 100644 --- a/babi/reg.py +++ b/babi/reg.py @@ -1,8 +1,8 @@ +from __future__ import annotations + import functools import re from typing import Match -from typing import Optional -from typing import Tuple import onigurumacffi @@ -42,7 +42,7 @@ class _Reg: pos: int, first_line: bool, boundary: bool, - ) -> Optional[Match[str]]: + ) -> Match[str] | None: return self._reg.search(line, pos, flags=_FLAGS[first_line, boundary]) def match( @@ -51,7 +51,7 @@ class _Reg: pos: int, first_line: bool, boundary: bool, - ) -> Optional[Match[str]]: + ) -> Match[str] | None: return self._reg.match(line, pos, flags=_FLAGS[first_line, boundary]) @@ -70,7 +70,7 @@ class _RegSet: pos: int, first_line: bool, boundary: bool, - ) -> Tuple[int, Optional[Match[str]]]: + ) -> tuple[int, Match[str] | None]: return self._set.search(line, pos, flags=_FLAGS[first_line, boundary]) diff --git a/babi/screen.py b/babi/screen.py index 146eb3e..9d725d4 100644 --- a/babi/screen.py +++ b/babi/screen.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import curses import enum @@ -7,12 +9,8 @@ import re import signal import sys from typing import Generator -from typing import List from typing import NamedTuple -from typing import Optional from typing import Pattern -from typing import Tuple -from typing import Union from babi.color_manager import ColorManager from babi.file import Action @@ -102,16 +100,16 @@ KEYNAME_REWRITE = { class Key(NamedTuple): - wch: Union[int, str] + wch: int | str keyname: bytes class Screen: def __init__( self, - stdscr: 'curses._CursesWindow', - filenames: List[Optional[str]], - initial_lines: List[int], + stdscr: curses._CursesWindow, + filenames: list[str | None], + initial_lines: list[int], perf: Perf, ) -> None: self.stdscr = stdscr @@ -126,9 +124,9 @@ class Screen: self.perf = perf self.status = Status() self.margin = Margin.from_current_screen() - self.cut_buffer: Tuple[str, ...] = () + self.cut_buffer: tuple[str, ...] = () self.cut_selection = False - self._buffered_input: Union[int, str, None] = None + self._buffered_input: int | str | None = None @property def file(self) -> File: @@ -271,8 +269,8 @@ class Screen: def quick_prompt( self, prompt: str, - opt_strs: Tuple[str, ...], - ) -> Union[str, PromptResult]: + opt_strs: tuple[str, ...], + ) -> str | PromptResult: opts = {opt[0] for opt in opt_strs} while True: x = 0 @@ -318,10 +316,10 @@ class Screen: prompt: str, *, allow_empty: bool = False, - history: Optional[str] = None, + history: str | None = None, default_prev: bool = False, - default: Optional[str] = None, - ) -> Union[str, PromptResult]: + default: str | None = None, + ) -> str | PromptResult: default = default or '' self.status.clear() if history is not None: @@ -378,7 +376,7 @@ class Screen: else: self.file.uncut(self.cut_buffer, self.margin) - def _get_search_re(self, prompt: str) -> Union[Pattern[str], PromptResult]: + def _get_search_re(self, prompt: str) -> Pattern[str] | PromptResult: response = self.prompt(prompt, history='search', default_prev=True) if response is PromptResult.CANCELLED: return response @@ -391,8 +389,8 @@ class Screen: def _undo_redo( self, op: str, - from_stack: List[Action], - to_stack: List[Action], + from_stack: list[Action], + to_stack: list[Action], ) -> None: if not from_stack: self.status.update(f'nothing to {op}!') @@ -423,7 +421,7 @@ class Screen: if response is not PromptResult.CANCELLED: self.file.replace(self, search_response, response) - def command(self) -> Optional[EditResult]: + def command(self) -> EditResult | None: response = self.prompt('', history='command') if response is PromptResult.CANCELLED: pass @@ -480,7 +478,7 @@ class Screen: self.status.update(f'invalid command: {response}') return None - def save(self) -> Optional[PromptResult]: + def save(self) -> PromptResult | None: self.file.finalize_previous_action() # TODO: make directories if they don't exist @@ -495,7 +493,7 @@ class Screen: self.file.filename = filename if not os.path.isfile(self.file.filename): - sha256: Optional[str] = None + sha256: str | None = None else: with open(self.file.filename, encoding='UTF-8', newline='') as f: *_, sha256 = get_lines(f) @@ -532,7 +530,7 @@ class Screen: first = False return None - def save_filename(self) -> Optional[PromptResult]: + def save_filename(self) -> PromptResult | None: response = self.prompt('enter filename', default=self.file.filename) if response is PromptResult.CANCELLED: return PromptResult.CANCELLED @@ -540,7 +538,7 @@ class Screen: self.file.filename = response return self.save() - def open_file(self) -> Optional[EditResult]: + def open_file(self) -> EditResult | None: response = self.prompt('enter filename', history='open') if response is not PromptResult.CANCELLED: opened = File(response, 0, self.color_manager, self.hl_factories) @@ -549,7 +547,7 @@ class Screen: else: return None - def quit_save_modified(self) -> Optional[EditResult]: + def quit_save_modified(self) -> EditResult | None: if self.file.modified: response = self.quick_prompt( 'file is modified - save', ('yes', 'no'), @@ -597,7 +595,7 @@ class Screen: } -def _init_screen() -> 'curses._CursesWindow': +def _init_screen() -> curses._CursesWindow: # set the escape delay so curses does not pause waiting for sequences if ( sys.version_info >= (3, 9) and @@ -623,7 +621,7 @@ def _init_screen() -> 'curses._CursesWindow': @contextlib.contextmanager -def make_stdscr() -> Generator['curses._CursesWindow', None, None]: +def make_stdscr() -> Generator[curses._CursesWindow, None, None]: """essentially `curses.wrapper` but split out to implement ^Z""" try: yield _init_screen() diff --git a/babi/status.py b/babi/status.py index 3b6410e..268b1d3 100644 --- a/babi/status.py +++ b/babi/status.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import curses from babi.margin import Margin @@ -16,7 +18,7 @@ class Status: def clear(self) -> None: self._status = '' - def draw(self, stdscr: 'curses._CursesWindow', margin: Margin) -> None: + def draw(self, stdscr: curses._CursesWindow, margin: Margin) -> None: if margin.footer or self._status: stdscr.insstr(margin.lines - 1, 0, ' ' * margin.cols) if self._status: diff --git a/babi/textmate_demo.py b/babi/textmate_demo.py index c997697..2945ff6 100644 --- a/babi/textmate_demo.py +++ b/babi/textmate_demo.py @@ -1,5 +1,6 @@ +from __future__ import annotations + import argparse -from typing import Optional from typing import Sequence from babi.highlight import Compiler @@ -47,7 +48,7 @@ def _highlight_output(theme: Theme, compiler: Compiler, filename: str) -> int: return 0 -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('--theme', default=xdg_config('theme.json')) parser.add_argument('--grammar-dir', default=prefix_data('grammar_v1')) diff --git a/babi/theme.py b/babi/theme.py index eac61de..2ded326 100644 --- a/babi/theme.py +++ b/babi/theme.py @@ -1,11 +1,10 @@ +from __future__ import annotations + import functools import json import os.path from typing import Any -from typing import Dict from typing import NamedTuple -from typing import Optional -from typing import Tuple from babi._types import Protocol from babi.color import Color @@ -13,32 +12,32 @@ from babi.fdict import FDict class Style(NamedTuple): - fg: Optional[Color] - bg: Optional[Color] + fg: Color | None + bg: Color | None b: bool i: bool u: bool @classmethod - def blank(cls) -> 'Style': + def blank(cls) -> Style: return cls(fg=None, bg=None, b=False, i=False, u=False) class PartialStyle(NamedTuple): - fg: Optional[Color] = None - bg: Optional[Color] = None - b: Optional[bool] = None - i: Optional[bool] = None - u: Optional[bool] = None + fg: Color | None = None + bg: Color | None = None + b: bool | None = None + i: bool | None = None + u: bool | None = None - def overlay_on(self, dct: Dict[str, Any]) -> None: + def overlay_on(self, dct: dict[str, Any]) -> None: for attr in self._fields: value = getattr(self, attr) if value is not None: dct[attr] = value @classmethod - def from_dct(cls, dct: Dict[str, Any]) -> 'PartialStyle': + def from_dct(cls, dct: dict[str, Any]) -> PartialStyle: kv = cls()._asdict() if 'foreground' in dct: kv['fg'] = Color.parse(dct['foreground']) @@ -57,7 +56,7 @@ class _TrieNode(Protocol): @property def style(self) -> PartialStyle: ... @property - def children(self) -> FDict[str, '_TrieNode']: ... + def children(self) -> FDict[str, _TrieNode]: ... class TrieNode(NamedTuple): @@ -65,7 +64,7 @@ class TrieNode(NamedTuple): children: FDict[str, _TrieNode] @classmethod - def from_dct(cls, dct: Dict[str, Any]) -> _TrieNode: + def from_dct(cls, dct: dict[str, Any]) -> _TrieNode: children = FDict({ k: TrieNode.from_dct(v) for k, v in dct['children'].items() }) @@ -77,7 +76,7 @@ class Theme(NamedTuple): rules: _TrieNode @functools.lru_cache(maxsize=None) - def select(self, scope: Tuple[str, ...]) -> Style: + def select(self, scope: tuple[str, ...]) -> Style: if not scope: return self.default else: @@ -92,7 +91,7 @@ class Theme(NamedTuple): return Style(**style) @classmethod - def from_dct(cls, data: Dict[str, Any]) -> 'Theme': + def from_dct(cls, data: dict[str, Any]) -> Theme: default = Style.blank()._asdict() for k in ('foreground', 'editor.foreground'): @@ -105,7 +104,7 @@ class Theme(NamedTuple): default['bg'] = Color.parse(data['colors'][k]) break - root: Dict[str, Any] = {'children': {}} + root: dict[str, Any] = {'children': {}} rules = data.get('tokenColors', []) + data.get('settings', []) for rule in rules: if 'scope' not in rule: @@ -139,11 +138,11 @@ class Theme(NamedTuple): return cls(Style(**default), TrieNode.from_dct(root)) @classmethod - def blank(cls) -> 'Theme': + def blank(cls) -> Theme: return cls(Style.blank(), TrieNode.from_dct({'children': {}})) @classmethod - def from_filename(cls, filename: str) -> 'Theme': + def from_filename(cls, filename: str) -> Theme: if not os.path.exists(filename): return cls.blank() else: diff --git a/babi/user_data.py b/babi/user_data.py index f5c3082..87d9786 100644 --- a/babi/user_data.py +++ b/babi/user_data.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import sys diff --git a/bin/download-theme b/bin/download-theme index dccb36c..42abedf 100755 --- a/bin/download-theme +++ b/bin/download-theme @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from __future__ import annotations + import argparse import io import json diff --git a/setup.cfg b/setup.cfg index 7043e03..e86b68b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,6 @@ classifiers = License :: OSI Approved :: MIT License Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -28,7 +27,7 @@ install_requires = onigurumacffi>=0.0.18 importlib_metadata>=1;python_version<"3.8" windows-curses;sys_platform=="win32" -python_requires = >=3.6.1 +python_requires = >=3.7 [options.packages.find] exclude = diff --git a/setup.py b/setup.py index 8bf1ba9..3d93aef 100644 --- a/setup.py +++ b/setup.py @@ -1,2 +1,4 @@ +from __future__ import annotations + from setuptools import setup setup() diff --git a/testing/runner.py b/testing/runner.py index c7a3e79..3d265d9 100644 --- a/testing/runner.py +++ b/testing/runner.py @@ -1,9 +1,9 @@ +from __future__ import annotations + import contextlib import curses import enum import re -from typing import List -from typing import Tuple from hecate import Runner @@ -34,7 +34,7 @@ def to_attrs(screen, width): fg = bg = -1 attr = 0 idx = 0 - ret: List[List[Tuple[int, int, int]]] + ret: list[list[tuple[int, int, int]]] ret = [[] for _ in range(len(screen.splitlines()))] for tp, match in tokenize_colors(screen): diff --git a/tests/buf_test.py b/tests/buf_test.py index 3d496b1..d56eb14 100644 --- a/tests/buf_test.py +++ b/tests/buf_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from babi.buf import Buf diff --git a/tests/color_kd_test.py b/tests/color_kd_test.py index b91ea0f..8b2604b 100644 --- a/tests/color_kd_test.py +++ b/tests/color_kd_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from babi import color_kd from babi.color import Color diff --git a/tests/color_manager_test.py b/tests/color_manager_test.py index a83fb50..c1e6b22 100644 --- a/tests/color_manager_test.py +++ b/tests/color_manager_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from babi.color import Color diff --git a/tests/color_test.py b/tests/color_test.py index a3938df..08542d4 100644 --- a/tests/color_test.py +++ b/tests/color_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from babi.color import Color diff --git a/tests/conftest.py b/tests/conftest.py index 2ea3f8a..ae82b0f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import pytest diff --git a/tests/fdict_test.py b/tests/fdict_test.py index ab2df6d..cd4569a 100644 --- a/tests/fdict_test.py +++ b/tests/fdict_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from babi.fdict import FChainMap diff --git a/tests/features/command_mode_test.py b/tests/features/command_mode_test.py index 3e8433d..c744e11 100644 --- a/tests/features/command_mode_test.py +++ b/tests/features/command_mode_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from testing.runner import and_exit diff --git a/tests/features/comment_test.py b/tests/features/comment_test.py index c5e38e2..b93ab40 100644 --- a/tests/features/comment_test.py +++ b/tests/features/comment_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from testing.runner import and_exit diff --git a/tests/features/conftest.py b/tests/features/conftest.py index 256ca03..78b41c3 100644 --- a/tests/features/conftest.py +++ b/tests/features/conftest.py @@ -1,11 +1,10 @@ +from __future__ import annotations + import contextlib import curses import os import sys -from typing import List from typing import NamedTuple -from typing import Tuple -from typing import Union from unittest import mock import pytest @@ -148,7 +147,7 @@ class AssertScreenLineEquals(NamedTuple): class AssertScreenAttrEquals(NamedTuple): n: int - attr: List[Tuple[int, int, int]] + attr: list[tuple[int, int, int]] def __call__(self, screen: Screen) -> None: assert screen.attrs[self.n] == self.attr @@ -170,7 +169,7 @@ class Resize(NamedTuple): class KeyPress(NamedTuple): - wch: Union[int, str] + wch: int | str def __call__(self, screen: Screen) -> None: raise AssertionError('unreachable') @@ -236,7 +235,7 @@ class CursesScreen: class Key(NamedTuple): tmux: str curses: bytes - wch: Union[int, str] + wch: int | str @property def value(self) -> int: @@ -300,7 +299,7 @@ class DeferredRunner: def __init__(self, command, width=80, height=24, term='screen'): self.command = command self._i = 0 - self._ops: List[Op] = [] + self._ops: list[Op] = [] self.color_pairs = {0: (7, 0)} self.screen = Screen(width, height) self._n_colors, self._can_change_color = { diff --git a/tests/features/current_position_test.py b/tests/features/current_position_test.py index 247d742..277f327 100644 --- a/tests/features/current_position_test.py +++ b/tests/features/current_position_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from testing.runner import and_exit diff --git a/tests/features/cut_uncut_test.py b/tests/features/cut_uncut_test.py index 886107d..8cf7ea2 100644 --- a/tests/features/cut_uncut_test.py +++ b/tests/features/cut_uncut_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from testing.runner import and_exit diff --git a/tests/features/expandtabs_test.py b/tests/features/expandtabs_test.py index 01d4297..abdceed 100644 --- a/tests/features/expandtabs_test.py +++ b/tests/features/expandtabs_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from testing.runner import and_exit from testing.runner import trigger_command_mode diff --git a/tests/features/go_to_line_test.py b/tests/features/go_to_line_test.py index 6ab36ab..5fcbf7f 100644 --- a/tests/features/go_to_line_test.py +++ b/tests/features/go_to_line_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from testing.runner import and_exit diff --git a/tests/features/indent_test.py b/tests/features/indent_test.py index f81ab6c..7352d16 100644 --- a/tests/features/indent_test.py +++ b/tests/features/indent_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from testing.runner import and_exit from testing.runner import trigger_command_mode diff --git a/tests/features/initial_position_test.py b/tests/features/initial_position_test.py index 130ea3a..10cef01 100644 --- a/tests/features/initial_position_test.py +++ b/tests/features/initial_position_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from testing.runner import and_exit diff --git a/tests/features/key_debug_test.py b/tests/features/key_debug_test.py index bc6324b..2af3526 100644 --- a/tests/features/key_debug_test.py +++ b/tests/features/key_debug_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import curses from babi.screen import VERSION_STR diff --git a/tests/features/movement_test.py b/tests/features/movement_test.py index f207e73..083c802 100644 --- a/tests/features/movement_test.py +++ b/tests/features/movement_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from testing.runner import and_exit diff --git a/tests/features/multiple_files_test.py b/tests/features/multiple_files_test.py index cc533b1..b8a36e2 100644 --- a/tests/features/multiple_files_test.py +++ b/tests/features/multiple_files_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/tests/features/open_test.py b/tests/features/open_test.py index c6e2a40..10e38b6 100644 --- a/tests/features/open_test.py +++ b/tests/features/open_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from testing.runner import and_exit diff --git a/tests/features/perf_log_test.py b/tests/features/perf_log_test.py index e4ae821..d4c5ae7 100644 --- a/tests/features/perf_log_test.py +++ b/tests/features/perf_log_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from testing.runner import and_exit diff --git a/tests/features/replace_test.py b/tests/features/replace_test.py index b4953e4..f42cf10 100644 --- a/tests/features/replace_test.py +++ b/tests/features/replace_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from testing.runner import and_exit diff --git a/tests/features/resize_test.py b/tests/features/resize_test.py index 14c0023..eeb9aa6 100644 --- a/tests/features/resize_test.py +++ b/tests/features/resize_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from babi.screen import VERSION_STR from testing.runner import and_exit diff --git a/tests/features/save_test.py b/tests/features/save_test.py index c74e079..78bcde8 100644 --- a/tests/features/save_test.py +++ b/tests/features/save_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from testing.runner import and_exit diff --git a/tests/features/search_test.py b/tests/features/search_test.py index 649c266..a3c9e8e 100644 --- a/tests/features/search_test.py +++ b/tests/features/search_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from testing.runner import and_exit diff --git a/tests/features/sort_test.py b/tests/features/sort_test.py index e83d4a4..b36e7ad 100644 --- a/tests/features/sort_test.py +++ b/tests/features/sort_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from testing.runner import and_exit diff --git a/tests/features/status_test.py b/tests/features/status_test.py index d54d264..e1c61ec 100644 --- a/tests/features/status_test.py +++ b/tests/features/status_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from testing.runner import and_exit diff --git a/tests/features/stdin_test.py b/tests/features/stdin_test.py index 169388c..7e7f1b4 100644 --- a/tests/features/stdin_test.py +++ b/tests/features/stdin_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import shlex import sys diff --git a/tests/features/suspend_test.py b/tests/features/suspend_test.py index 56f1175..5a7b854 100644 --- a/tests/features/suspend_test.py +++ b/tests/features/suspend_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import shlex import sys diff --git a/tests/features/syntax_highlight_test.py b/tests/features/syntax_highlight_test.py index eed5a76..9664d31 100644 --- a/tests/features/syntax_highlight_test.py +++ b/tests/features/syntax_highlight_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import curses import json diff --git a/tests/features/tabsize_test.py b/tests/features/tabsize_test.py index f1d74db..eba810a 100644 --- a/tests/features/tabsize_test.py +++ b/tests/features/tabsize_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from testing.runner import and_exit diff --git a/tests/features/text_editing_test.py b/tests/features/text_editing_test.py index 6a0adc9..02b2dba 100644 --- a/tests/features/text_editing_test.py +++ b/tests/features/text_editing_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from testing.runner import and_exit diff --git a/tests/features/trailing_whitespace_test.py b/tests/features/trailing_whitespace_test.py index 715fb84..13b137c 100644 --- a/tests/features/trailing_whitespace_test.py +++ b/tests/features/trailing_whitespace_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import curses from testing.runner import and_exit diff --git a/tests/features/undo_redo_test.py b/tests/features/undo_redo_test.py index e4d835b..5693d8e 100644 --- a/tests/features/undo_redo_test.py +++ b/tests/features/undo_redo_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from testing.runner import and_exit diff --git a/tests/file_test.py b/tests/file_test.py index 0affbac..7060fb3 100644 --- a/tests/file_test.py +++ b/tests/file_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import io import pytest diff --git a/tests/highlight_test.py b/tests/highlight_test.py index 8c825ef..db8d2b7 100644 --- a/tests/highlight_test.py +++ b/tests/highlight_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from babi.highlight import highlight_line diff --git a/tests/hl/syntax_test.py b/tests/hl/syntax_test.py index fec4dc8..976a7eb 100644 --- a/tests/hl/syntax_test.py +++ b/tests/hl/syntax_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import contextlib import curses from unittest import mock diff --git a/tests/main_test.py b/tests/main_test.py index 14ea172..5e7dab6 100644 --- a/tests/main_test.py +++ b/tests/main_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from babi import main diff --git a/tests/reg_test.py b/tests/reg_test.py index 4af6074..1f79543 100644 --- a/tests/reg_test.py +++ b/tests/reg_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import onigurumacffi import pytest diff --git a/tests/textmate_demo_test.py b/tests/textmate_demo_test.py index 93a753b..89639ab 100644 --- a/tests/textmate_demo_test.py +++ b/tests/textmate_demo_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import pytest diff --git a/tests/theme_test.py b/tests/theme_test.py index 6cd00f8..9519ed1 100644 --- a/tests/theme_test.py +++ b/tests/theme_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from babi.color import Color diff --git a/tests/user_data_test.py b/tests/user_data_test.py index 0994896..d8bdd3a 100644 --- a/tests/user_data_test.py +++ b/tests/user_data_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from unittest import mock diff --git a/tox.ini b/tox.ini index 2ee817e..fccbf4a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,pre-commit +envlist = py37,pre-commit [testenv] deps = -rrequirements-dev.txt