Skip to content

Commit f331b22

Browse files
authored
Improve define_guard typing (#205)
* Add ParamSpec to `define_guard` The intention is to eventually silence a couple warnings in mypy 1.19 about missing `self` arguments in the methods annotated by `define_guard`, but this iteration does not work. Instead, it exposed another error in `ViewStream` that is addressed in a later commit. error: Self argument missing for a non-static method (or an invalid type for self) [misc] * Remove unnecessary type-ignore comments * ViewStream should implement/inherit a protocol It is not possible to satisfy the type signature of `TextIOBase.readline` due to the conflict with `IOBase.readline` returning a different type while still being an inherited class. Instead, we claim that `ViewStream` implements the protocol (which is semantically equivalent). * Define guards as static functions Addresses the mypy 1.19 errors that complain about the first argument of the wrapped guard function not being `self`. Self argument missing for a non-static method (or an invalid type for self) Arguably this also makes the classes easier to read.
1 parent 89ce2d1 commit f331b22

File tree

4 files changed

+66
-56
lines changed

4 files changed

+66
-56
lines changed

sublime_lib/_util/enum.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def decorator(cls: Any) -> EnumMeta:
2121
def __new__(cls: EnumMeta, *args: Any, **kwargs: Any) -> Enum:
2222
return constructor(next_constructor, cls, *args, **kwargs)
2323

24-
cls.__new__ = __new__ # type: ignore
24+
cls.__new__ = __new__
2525
return cls
2626

2727
return decorator

sublime_lib/_util/guard.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
11
from __future__ import annotations
22
from functools import wraps
3-
from typing import Any, Callable, ContextManager, TypeVar
3+
from typing import TYPE_CHECKING
44

5-
6-
_Self = TypeVar('_Self')
7-
_WrappedType = Callable[..., Any]
5+
if TYPE_CHECKING:
6+
from typing import Any, Callable, ContextManager, TypeVar
7+
from typing_extensions import ParamSpec, Concatenate
8+
_Self = TypeVar('_Self')
9+
_T = TypeVar('_T')
10+
_P = ParamSpec('_P')
811

912

1013
def define_guard(
11-
guard_fn: Callable[[_Self], ContextManager | None]
12-
) -> Callable[[_WrappedType], _WrappedType]:
13-
def decorator(wrapped: _WrappedType) -> _WrappedType:
14+
guard_fn: Callable[[_Self], ContextManager[Any] | None]
15+
) -> Callable[[Callable[Concatenate[_Self, _P], _T]], Callable[Concatenate[_Self, _P], _T]]:
16+
def decorator(
17+
wrapped: Callable[Concatenate[_Self, _P], _T]
18+
) -> Callable[Concatenate[_Self, _P], _T]:
1419
@wraps(wrapped)
15-
def wrapper_guards(self: _Self, *args: Any, **kwargs: Any) -> Any:
20+
def wrapper(self: _Self, /, *args: _P.args, **kwargs: _P.kwargs) -> _T:
1621
ret_val = guard_fn(self)
1722
if ret_val is not None:
1823
with ret_val:
1924
return wrapped(self, *args, **kwargs)
2025
else:
2126
return wrapped(self, *args, **kwargs)
2227

23-
return wrapper_guards
28+
return wrapper
2429

2530
return decorator

sublime_lib/panel.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
__all__ = ['Panel', 'OutputPanel']
1111

1212

13+
@define_guard
14+
def guard_exists(panel: Panel) -> None:
15+
panel._checkExists()
16+
17+
1318
class Panel():
1419
"""An abstraction of a panel, such as the console or an output panel.
1520
@@ -36,10 +41,6 @@ def _checkExists(self) -> None:
3641
if not self.exists():
3742
raise ValueError(f"Panel {self.panel_name} does not exist.")
3843

39-
@define_guard
40-
def guard_exists(self) -> None:
41-
self._checkExists()
42-
4344
def exists(self) -> bool:
4445
"""Return ``True`` if the panel exists, or ``False`` otherwise.
4546

sublime_lib/view_stream.py

Lines changed: 46 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,58 @@
11
from __future__ import annotations
22
from collections.abc import Generator
33
from contextlib import contextmanager
4-
from io import SEEK_SET, SEEK_CUR, SEEK_END, TextIOBase
5-
from typing import Any
4+
from io import SEEK_SET, SEEK_CUR, SEEK_END
5+
from typing import Any, TextIO
66

77
import sublime
88
from sublime import Region
99

1010
from ._util.guard import define_guard
1111

1212

13-
class ViewStream(TextIOBase):
13+
@define_guard
14+
@contextmanager
15+
def guard_read_only(vs: ViewStream) -> Generator[Any, None, None]:
16+
if vs.view.is_read_only():
17+
if vs.force_writes:
18+
vs.view.set_read_only(False)
19+
yield
20+
vs.view.set_read_only(True)
21+
else:
22+
raise ValueError("The underlying view is read-only.")
23+
else:
24+
yield
25+
26+
27+
@define_guard
28+
@contextmanager
29+
def guard_auto_indent(vs: ViewStream) -> Generator[Any, None, None]:
30+
settings = vs.view.settings()
31+
if settings.get('auto_indent'):
32+
settings.set('auto_indent', False)
33+
yield
34+
settings.set('auto_indent', True)
35+
else:
36+
yield
37+
38+
39+
@define_guard
40+
def guard_validity(vs: ViewStream) -> None:
41+
if not vs.view.is_valid():
42+
raise ValueError("The underlying view is invalid.")
43+
44+
45+
@define_guard
46+
def guard_selection(vs: ViewStream) -> None:
47+
if len(vs.view.sel()) == 0:
48+
raise ValueError("The underlying view has no selection.")
49+
elif len(vs.view.sel()) > 1:
50+
raise ValueError("The underlying view has multiple selections.")
51+
elif not vs.view.sel()[0].empty():
52+
raise ValueError("The underlying view's selection is not empty.")
53+
54+
55+
class ViewStream(TextIO):
1456
"""A :class:`~io.TextIOBase` encapsulating a :class:`~sublime.View` object.
1557
1658
All public methods (except :meth:`flush`) require
@@ -35,44 +77,6 @@ class ViewStream(TextIOBase):
3577
Added the `follow_cursor` option.
3678
"""
3779

38-
@define_guard
39-
@contextmanager
40-
def guard_read_only(self) -> Generator[Any, None, None]:
41-
if self.view.is_read_only():
42-
if self.force_writes:
43-
self.view.set_read_only(False)
44-
yield
45-
self.view.set_read_only(True)
46-
else:
47-
raise ValueError("The underlying view is read-only.")
48-
else:
49-
yield
50-
51-
@define_guard
52-
@contextmanager
53-
def guard_auto_indent(self) -> Generator[Any, None, None]:
54-
settings = self.view.settings()
55-
if settings.get('auto_indent'):
56-
settings.set('auto_indent', False)
57-
yield
58-
settings.set('auto_indent', True)
59-
else:
60-
yield
61-
62-
@define_guard
63-
def guard_validity(self) -> None:
64-
if not self.view.is_valid():
65-
raise ValueError("The underlying view is invalid.")
66-
67-
@define_guard
68-
def guard_selection(self) -> None:
69-
if len(self.view.sel()) == 0:
70-
raise ValueError("The underlying view has no selection.")
71-
elif len(self.view.sel()) > 1:
72-
raise ValueError("The underlying view has multiple selections.")
73-
elif not self.view.sel()[0].empty():
74-
raise ValueError("The underlying view's selection is not empty.")
75-
7680
def __init__(
7781
self, view: sublime.View, *, force_writes: bool = False, follow_cursor: bool = False
7882
):
@@ -136,7 +140,7 @@ def write(self, s: str) -> int:
136140

137141
def print(self, *objects: object, sep: str = ' ', end: str = '\n') -> None:
138142
"""Shorthand for :func:`print()` passing this ViewStream as the `file` argument."""
139-
print(*objects, file=self, sep=sep, end=end) # type: ignore
143+
print(*objects, file=self, sep=sep, end=end)
140144

141145
def flush(self) -> None:
142146
"""Do nothing. (The stream is not buffered.)"""

0 commit comments

Comments
 (0)