jetpytools 1.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of jetpytools might be problematic. Click here for more details.

@@ -0,0 +1,254 @@
1
+ from __future__ import annotations
2
+
3
+ from fractions import Fraction
4
+ from typing import Any, Iterable, Iterator, Sequence, overload
5
+
6
+ from ..types import F, SupportsString, T, SoftRange, SoftRangeN, SoftRangesN, StrictRange
7
+
8
+ __all__ = [
9
+ 'normalize_seq',
10
+ 'to_arr',
11
+ 'flatten',
12
+ 'normalize_list_to_ranges',
13
+ 'normalize_ranges_to_list',
14
+ 'normalize_range',
15
+ 'normalize_ranges',
16
+ 'invert_ranges',
17
+ 'norm_func_name', 'norm_display_name'
18
+ ]
19
+
20
+
21
+ @overload
22
+ def normalize_seq(val: Sequence[T], length: int) -> list[T]:
23
+ ...
24
+
25
+
26
+ @overload
27
+ def normalize_seq(val: T | Sequence[T], length: int) -> list[T]:
28
+ ...
29
+
30
+
31
+ def normalize_seq(val: T | Sequence[T], length: int) -> list[T]:
32
+ """
33
+ Normalize a sequence of values.
34
+
35
+ :param val: Input value.
36
+ :param length: Amount of items in the output.
37
+ If original sequence length is less that this,
38
+ the last item will be repeated.
39
+
40
+ :return: List of normalized values with a set amount of items.
41
+ """
42
+
43
+ val = to_arr(val)
44
+
45
+ val += [val[-1]] * (length - len(val))
46
+
47
+ return val[:length]
48
+
49
+
50
+ _iterables_t = (list, tuple, range, zip, set, map, enumerate)
51
+
52
+
53
+ @overload
54
+ def to_arr(val: list[T], *, sub: bool = False) -> list[T]:
55
+ ...
56
+
57
+
58
+ @overload
59
+ def to_arr(val: T | Sequence[T], *, sub: bool = False) -> list[T]:
60
+ ...
61
+
62
+
63
+ def to_arr(val: T | Sequence[T], *, sub: bool = False) -> list[T]:
64
+ """Normalize any value into an iterable."""
65
+
66
+ if sub:
67
+ return list(val) if any(isinstance(val, x) for x in _iterables_t) else [val] # type: ignore
68
+
69
+ return list(val) if type(val) in _iterables_t else [val] # type: ignore
70
+
71
+
72
+ @overload
73
+ def flatten(items: T | Iterable[T | Iterable[T | Iterable[T]]]) -> Iterable[T]:
74
+ ...
75
+
76
+
77
+ @overload
78
+ def flatten(items: T | Iterable[T | Iterable[T]]) -> Iterable[T]: # type: ignore
79
+ ...
80
+
81
+
82
+ @overload
83
+ def flatten(items: T | Iterable[T]) -> Iterable[T]: # type: ignore
84
+ ...
85
+
86
+
87
+ def flatten(items: Any) -> Any:
88
+ """Flatten an array of values."""
89
+
90
+ for val in items:
91
+ if isinstance(val, Iterable) and not isinstance(val, (str, bytes)):
92
+ for sub_x in flatten(val):
93
+ yield sub_x
94
+ else:
95
+ yield val
96
+
97
+
98
+ def normalize_range(ranges: SoftRange, /) -> Iterable[int]:
99
+ """
100
+ Normalize ranges represented by a tuple to an iterable of frame numbers.
101
+
102
+ :param ranges: Ranges to normalize.
103
+
104
+ :return: List of positive frame ranges.
105
+ """
106
+
107
+ if isinstance(ranges, int):
108
+ return [ranges]
109
+
110
+ if isinstance(ranges, tuple):
111
+ start, stop = ranges
112
+ step = -1 if stop < start else 1
113
+
114
+ return range(start, stop + step, step)
115
+
116
+ return ranges
117
+
118
+
119
+ def normalize_list_to_ranges(flist: Iterable[int], min_length: int = 0) -> list[StrictRange]:
120
+ flist2 = list[list[int]]()
121
+ flist3 = list[int]()
122
+
123
+ prev_n = -1
124
+
125
+ for n in sorted(set(flist)):
126
+ if prev_n + 1 != n:
127
+ if flist3:
128
+ flist2.append(flist3)
129
+ flist3 = []
130
+ flist3.append(n)
131
+ prev_n = n
132
+
133
+ if flist3:
134
+ flist2.append(flist3)
135
+
136
+ flist4 = [i for i in flist2 if len(i) > min_length]
137
+
138
+ return list(zip(
139
+ [i[0] for i in flist4],
140
+ [i[-1] for j, i in enumerate(flist4)]
141
+ ))
142
+
143
+
144
+ def normalize_ranges_to_list(ranges: Iterable[SoftRange]) -> list[int]:
145
+ out = list[int]()
146
+
147
+ for srange in ranges:
148
+ out.extend(normalize_range(srange))
149
+
150
+ return out
151
+
152
+
153
+ def normalize_ranges(ranges: SoftRangeN | SoftRangesN, end: int) -> list[StrictRange]:
154
+ """
155
+ Normalize ranges to a list of positive ranges.
156
+
157
+ Frame ranges can include None and negative values.
158
+ None will be converted to either 0 if it's the first value in a SoftRange, or the end if it's the second item.
159
+ Negative values will be subtracted from the end.
160
+
161
+ Examples:
162
+
163
+ .. code-block:: python
164
+
165
+ >>> normalize_ranges((None, None), end=1000)
166
+ [(0, 999)]
167
+ >>> normalize_ranges((24, -24), end=1000)
168
+ [(24, 975)]
169
+ >>> normalize_ranges([(24, 100), (80, 150)], end=1000)
170
+ [(24, 150)]
171
+
172
+
173
+ :param clip: Input clip.
174
+ :param franges: Frame range or list of frame ranges.
175
+
176
+ :return: List of positive frame ranges.
177
+ """
178
+
179
+ ranges = ranges if isinstance(ranges, list) else [ranges] # type:ignore
180
+
181
+ out = []
182
+
183
+ for r in ranges:
184
+ if r is None:
185
+ r = (None, None)
186
+
187
+ if isinstance(r, tuple):
188
+ start, endd = r
189
+ if start is None:
190
+ start = 0
191
+ if endd is None:
192
+ endd = end - 1
193
+ else:
194
+ start = r
195
+ endd = r
196
+
197
+ if start < 0:
198
+ start = end - 1 + start
199
+
200
+ if endd < 0:
201
+ endd = end - 1 + endd
202
+
203
+ out.append((start, endd))
204
+
205
+ return normalize_list_to_ranges([
206
+ x for start, endd in out for x in range(start, endd + 1)
207
+ ])
208
+
209
+
210
+ def invert_ranges(ranges: SoftRangeN | SoftRangesN, enda: int, endb: int | None) -> list[StrictRange]:
211
+ norm_ranges = normalize_ranges(ranges, enda if endb is None else endb)
212
+
213
+ b_frames = {*normalize_ranges_to_list(norm_ranges)}
214
+
215
+ return normalize_list_to_ranges({*range(enda)} - b_frames)
216
+
217
+
218
+ def norm_func_name(func_name: SupportsString | F) -> str:
219
+ """Normalize a class, function, or other object to obtain its name"""
220
+
221
+ if isinstance(func_name, str):
222
+ return func_name.strip()
223
+
224
+ if not isinstance(func_name, type) and not callable(func_name):
225
+ return str(func_name).strip()
226
+
227
+ func = func_name
228
+
229
+ if hasattr(func_name, '__name__'):
230
+ func_name = func.__name__
231
+ elif hasattr(func_name, '__qualname__'):
232
+ func_name = func.__qualname__
233
+
234
+ if callable(func):
235
+ if hasattr(func, '__self__'):
236
+ func = func.__self__ if isinstance(func.__self__, type) else func.__self__.__class__
237
+ func_name = f'{func.__name__}.{func_name}'
238
+
239
+ return str(func_name).strip()
240
+
241
+
242
+ def norm_display_name(obj: object) -> str:
243
+ """Get a fancy name from any object."""
244
+
245
+ if isinstance(obj, Iterator):
246
+ return ', '.join(norm_display_name(v) for v in obj).strip()
247
+
248
+ if isinstance(obj, Fraction):
249
+ return f'{obj.numerator}/{obj.denominator}'
250
+
251
+ if isinstance(obj, dict):
252
+ return '(' + ', '.join(f'{k}={v}' for k, v in obj.items()) + ')'
253
+
254
+ return norm_func_name(obj)
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ __all__ = [
6
+ 'deepmerge'
7
+ ]
8
+
9
+
10
+ def deepmerge(source: dict[Any, Any], destination: dict[Any, Any]) -> dict[Any, Any]:
11
+ for key, value in source.items():
12
+ if isinstance(value, dict):
13
+ node = destination.setdefault(key, {})
14
+ deepmerge(value, node)
15
+ else:
16
+ destination[key] = value
17
+
18
+ return destination
jetpytools/py.typed ADDED
File without changes
@@ -0,0 +1,6 @@
1
+ from .builtins import * # noqa: F401, F403
2
+ from .file import * # noqa: F401, F403
3
+ from .funcs import * # noqa: F401, F403
4
+ from .generic import * # noqa: F401, F403
5
+ from .supports import * # noqa: F401, F403
6
+ from .utils import * # noqa: F401, F403
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import (
4
+ TYPE_CHECKING, Any, Callable, ParamSpec, Sequence, SupportsFloat, SupportsIndex, TypeAlias, TypeVar, Union
5
+ )
6
+
7
+ __all__ = [
8
+ 'T', 'T0', 'T1', 'T2', 'T_contra',
9
+
10
+ 'F', 'F0', 'F1', 'F2',
11
+
12
+ 'P', 'P0', 'P1', 'P2',
13
+ 'R', 'R0', 'R1', 'R2', 'R_contra',
14
+
15
+ 'Nb',
16
+
17
+ 'StrictRange', 'SoftRange', 'SoftRangeN', 'SoftRangesN',
18
+
19
+ 'Self',
20
+
21
+ 'SingleOrArr', 'SingleOrArrOpt',
22
+ 'SingleOrSeq', 'SingleOrSeqOpt',
23
+
24
+ 'SimpleByteData', 'SimpleByteDataArray',
25
+ 'ByteData',
26
+
27
+ 'KwargsT'
28
+ ]
29
+
30
+ Nb = TypeVar('Nb', float, int)
31
+
32
+ T = TypeVar('T')
33
+ T0 = TypeVar('T0')
34
+ T1 = TypeVar('T1')
35
+ T2 = TypeVar('T2')
36
+
37
+ F = TypeVar('F', bound=Callable[..., Any])
38
+ F0 = TypeVar('F0', bound=Callable[..., Any])
39
+ F1 = TypeVar('F1', bound=Callable[..., Any])
40
+ F2 = TypeVar('F2', bound=Callable[..., Any])
41
+
42
+ P = ParamSpec('P')
43
+ P0 = ParamSpec('P0')
44
+ P1 = ParamSpec('P1')
45
+ P2 = ParamSpec('P2')
46
+
47
+ R = TypeVar('R')
48
+ R0 = TypeVar('R0')
49
+ R1 = TypeVar('R1')
50
+ R2 = TypeVar('R2')
51
+
52
+ T_contra = TypeVar('T_contra', contravariant=True)
53
+ R_contra = TypeVar('R_contra', contravariant=True)
54
+
55
+ Self = TypeVar('Self')
56
+
57
+ StrictRange: TypeAlias = tuple[int, int]
58
+ SoftRange: TypeAlias = int | StrictRange | Sequence[int]
59
+
60
+ SoftRangeN: TypeAlias = int | tuple[int | None, int | None] | None
61
+
62
+ if TYPE_CHECKING:
63
+ SoftRangesN: TypeAlias = Sequence[SoftRangeN]
64
+ else:
65
+ SoftRangesN: TypeAlias = list[SoftRangeN]
66
+
67
+ SingleOrArr = Union[T, list[T]]
68
+ SingleOrSeq = Union[T, Sequence[T]]
69
+ SingleOrArrOpt = Union[SingleOrArr[T], None]
70
+ SingleOrSeqOpt = Union[SingleOrSeq[T], None]
71
+
72
+ SimpleByteData: TypeAlias = str | bytes | bytearray
73
+ SimpleByteDataArray = Union[SimpleByteData, Sequence[SimpleByteData]]
74
+
75
+ ByteData: TypeAlias = SupportsFloat | SupportsIndex | SimpleByteData | memoryview
76
+
77
+ KwargsT = dict[str, Any]
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ import fnmatch
4
+ import shutil
5
+ from os import PathLike, listdir, path, walk
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal, TypeAlias, Union
8
+
9
+ __all__ = [
10
+ 'FilePathType', 'FileDescriptor',
11
+ 'FileOpener',
12
+
13
+ 'OpenTextModeUpdating',
14
+ 'OpenTextModeWriting',
15
+ 'OpenTextModeReading',
16
+
17
+ 'OpenBinaryModeUpdating',
18
+ 'OpenBinaryModeWriting',
19
+ 'OpenBinaryModeReading',
20
+
21
+ 'OpenTextMode',
22
+ 'OpenBinaryMode',
23
+
24
+ 'SPath', 'SPathLike'
25
+ ]
26
+
27
+ FileDescriptor: TypeAlias = int
28
+
29
+ FilePathType: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes]
30
+
31
+ FileOpener: TypeAlias = Callable[[str, int], int]
32
+
33
+ OpenTextModeUpdating: TypeAlias = Literal[
34
+ 'r+', '+r', 'rt+', 'r+t', '+rt', 'tr+', 't+r', '+tr', 'w+', '+w', 'wt+', 'w+t', '+wt', 'tw+', 't+w', '+tw',
35
+ 'a+', '+a', 'at+', 'a+t', '+at', 'ta+', 't+a', '+ta', 'x+', '+x', 'xt+', 'x+t', '+xt', 'tx+', 't+x', '+tx',
36
+ ]
37
+ OpenTextModeWriting: TypeAlias = Literal[
38
+ 'w', 'wt', 'tw', 'a', 'at', 'ta', 'x', 'xt', 'tx'
39
+ ]
40
+ OpenTextModeReading: TypeAlias = Literal[
41
+ 'r', 'rt', 'tr', 'U', 'rU', 'Ur', 'rtU', 'rUt', 'Urt', 'trU', 'tUr', 'Utr'
42
+ ]
43
+
44
+ OpenBinaryModeUpdating: TypeAlias = Literal[
45
+ 'rb+', 'r+b', '+rb', 'br+', 'b+r', '+br', 'wb+', 'w+b', '+wb', 'bw+', 'b+w', '+bw',
46
+ 'ab+', 'a+b', '+ab', 'ba+', 'b+a', '+ba', 'xb+', 'x+b', '+xb', 'bx+', 'b+x', '+bx'
47
+ ]
48
+ OpenBinaryModeWriting: TypeAlias = Literal[
49
+ 'wb', 'bw', 'ab', 'ba', 'xb', 'bx'
50
+ ]
51
+ OpenBinaryModeReading: TypeAlias = Literal[
52
+ 'rb', 'br', 'rbU', 'rUb', 'Urb', 'brU', 'bUr', 'Ubr'
53
+ ]
54
+
55
+ OpenTextMode: TypeAlias = OpenTextModeUpdating | OpenTextModeWriting | OpenTextModeReading
56
+ OpenBinaryMode: TypeAlias = OpenBinaryModeUpdating | OpenBinaryModeReading | OpenBinaryModeWriting
57
+
58
+
59
+ class SPath(Path):
60
+ """Modified version of pathlib.Path"""
61
+
62
+ if TYPE_CHECKING:
63
+ def __new__(cls, *args: SPathLike, **kwargs: Any) -> SPath:
64
+ ...
65
+
66
+ def format(self, *args: Any, **kwargs: Any) -> SPath:
67
+ """Format the path with the given arguments."""
68
+
69
+ return SPath(self.to_str().format(*args, **kwargs))
70
+
71
+ def to_str(self) -> str:
72
+ """Cast the path to a string."""
73
+
74
+ return str(self)
75
+
76
+ def get_folder(self) -> SPath:
77
+ """Get the folder of the path."""
78
+
79
+ folder_path = self.resolve()
80
+
81
+ if folder_path.is_dir():
82
+ return folder_path
83
+
84
+ return SPath(path.dirname(folder_path))
85
+
86
+ def mkdirp(self, mode: int = 0o777) -> None:
87
+ """Make the dir path with its parents."""
88
+
89
+ return self.get_folder().mkdir(mode, True, True)
90
+
91
+ def rmdirs(self, missing_ok: bool = False, ignore_errors: bool = True) -> None:
92
+ """Remove the dir path with its contents."""
93
+
94
+ try:
95
+ return shutil.rmtree(str(self.get_folder()), ignore_errors)
96
+ except FileNotFoundError:
97
+ if not missing_ok:
98
+ raise
99
+
100
+ def read_lines(
101
+ self, encoding: str | None = None, errors: str | None = None, keepends: bool = False
102
+ ) -> list[str]:
103
+ """Read the file and return its lines."""
104
+
105
+ return super().read_text(encoding, errors).splitlines(keepends)
106
+
107
+ def write_lines(
108
+ self, data: Iterable[str], encoding: str | None = None,
109
+ errors: str | None = None, newline: str | None = None
110
+ ) -> int:
111
+ """Open the file and write the given lines."""
112
+
113
+ return super().write_text('\n'.join(data), encoding, errors, newline)
114
+
115
+ def append_to_stem(self, suffixes: str | Iterable[str], sep: str = '_') -> SPath:
116
+ """Append a suffix to the stem of the path"""
117
+
118
+ from ..functions import to_arr
119
+
120
+ return self.with_stem(sep.join([self.stem, *to_arr(suffixes)])) # type:ignore[list-item]
121
+
122
+ def is_empty_dir(self) -> bool:
123
+ """Check if the directory is empty."""
124
+
125
+ return self.is_dir() and not any(self.iterdir())
126
+
127
+ def move_dir(self, dst: SPath, *, mode: int = 0o777) -> None:
128
+ """Move the directory to the specified destination."""
129
+
130
+ dst.mkdir(mode, True, True)
131
+
132
+ for file in listdir(self):
133
+ src_file = self / file
134
+ dst_file = dst / file
135
+
136
+ if dst_file.exists():
137
+ src_file.unlink()
138
+ else:
139
+ src_file.rename(dst_file)
140
+
141
+ self.rmdir()
142
+
143
+ def copy_dir(self, dst: SPath) -> SPath:
144
+ """Copy the directory to the specified destination."""
145
+
146
+ if not self.is_dir():
147
+ from ..exceptions import PathIsNotADirectoryError
148
+ raise PathIsNotADirectoryError('The given path, \"{self}\" is not a directory!', self.copy_dir)
149
+
150
+ dst.mkdirp()
151
+ shutil.copytree(self, dst, dirs_exist_ok=True)
152
+
153
+ return SPath(dst)
154
+
155
+ def lglob(self, pattern: str = '*') -> list[SPath]:
156
+ """Glob the path and return the list of paths."""
157
+
158
+ return list(map(SPath, self.glob(pattern)))
159
+
160
+ def fglob(self, pattern: str = '*') -> SPath | None:
161
+ """Glob the path and return the first match."""
162
+
163
+ for root, dirs, files in walk(self):
164
+ for name in dirs + files:
165
+ if fnmatch.fnmatch(name, pattern):
166
+ return SPath(path.join(root, name))
167
+
168
+ return None
169
+
170
+ def find_newest_file(self, pattern: str = '*') -> SPath | None:
171
+ """Find the most recently modified file matching the given pattern in the directory."""
172
+
173
+ matching_files = self.get_folder().glob(pattern)
174
+
175
+ if not matching_files:
176
+ return None
177
+
178
+ return max(matching_files, key=lambda p: p.stat().st_mtime, default=None) # type:ignore
179
+
180
+ def get_size(self) -> int:
181
+ """Get the size of the file or directory in bytes."""
182
+
183
+ if not self.exists():
184
+ from ..exceptions import FileNotExistsError
185
+ raise FileNotExistsError('The given path, \"{self}\" is not a file or directory!', self.get_size)
186
+
187
+ if self.is_file():
188
+ return self.stat().st_size
189
+
190
+ return sum(f.stat().st_size for f in self.rglob('*') if f.is_file())
191
+
192
+
193
+ SPathLike = Union[str, Path, SPath]
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+ from functools import wraps
3
+
4
+ from typing import TYPE_CHECKING, Any, Callable, Iterable, List, SupportsIndex, TypeAlias, TypeVar, overload
5
+
6
+ from .builtins import T, P
7
+ from .supports import SupportsString
8
+
9
+ __all__ = [
10
+ 'StrList',
11
+ 'Sentinel'
12
+ ]
13
+
14
+
15
+ class StrList(List[SupportsString]):
16
+ """Custom class for representing a recursively "stringable" list."""
17
+
18
+ if TYPE_CHECKING:
19
+ @overload
20
+ def __init__(self, __iterable: Iterable[SupportsString | None] = []) -> None:
21
+ ...
22
+
23
+ @overload
24
+ def __init__(self, __iterable: Iterable[Iterable[SupportsString | None] | None] = []) -> None:
25
+ ...
26
+
27
+ def __init__(self, __iterable: Any = []) -> None:
28
+ ...
29
+
30
+ @property
31
+ def string(self) -> str:
32
+ return self.to_str()
33
+
34
+ def to_str(self) -> str:
35
+ return str(self)
36
+
37
+ def __str__(self) -> str:
38
+ from ..functions import flatten
39
+
40
+ return ' '.join(
41
+ filter(
42
+ None,
43
+ (str(x).strip() for x in flatten(self) if x is not None) # type: ignore[var-annotated,arg-type]
44
+ )
45
+ )
46
+
47
+ def __add__(self, __x: list[SupportsString]) -> StrList: # type: ignore[override]
48
+ return StrList(super().__add__(__x))
49
+
50
+ def __mul__(self, __n: SupportsIndex) -> StrList:
51
+ return StrList(super().__mul__(__n))
52
+
53
+ def __rmul__(self, __n: SupportsIndex) -> StrList:
54
+ return StrList(super().__rmul__(__n))
55
+
56
+ @property
57
+ def mlength(self) -> int:
58
+ return len(self) - 1
59
+
60
+ def append(self, *__object: SupportsString) -> None:
61
+ for __obj in __object:
62
+ super().append(__obj)
63
+
64
+
65
+ class SentinelDispatcher:
66
+ def check(self, ret_value: T, cond: bool) -> T | SentinelDispatcher:
67
+ return ret_value if cond else self
68
+
69
+ def check_cb(self, callback: Callable[P, tuple[T, bool]]) -> Callable[P, T | SentinelDispatcher]:
70
+ @wraps(callback)
71
+ def _wrap(*args: P.args, **kwargs: P.kwargs) -> T | SentinelDispatcher:
72
+ return self.check(*callback(*args, **kwargs))
73
+
74
+ return _wrap
75
+
76
+ def filter(self: SelfSentinel, items: Iterable[T | SelfSentinel]) -> Iterable[T]:
77
+ for item in items:
78
+ if item is self:
79
+ continue
80
+
81
+ yield item # type: ignore
82
+
83
+ @classmethod
84
+ def filter_multi(cls, items: Iterable[T | SelfSentinel], *sentinels: SelfSentinel) -> Iterable[T]:
85
+ for item in items:
86
+ if item in sentinels:
87
+ continue
88
+
89
+ yield item # type: ignore
90
+
91
+ def __getattr__(self, name: str) -> SentinelDispatcher:
92
+ if name not in _sentinels:
93
+ _sentinels[name] = SentinelDispatcher()
94
+ return _sentinels[name]
95
+
96
+ def __setattr__(self, __name: str, __value: Any) -> None:
97
+ raise NameError
98
+
99
+ def __call__(self) -> SentinelDispatcher:
100
+ return SentinelDispatcher()
101
+
102
+ Type: TypeAlias = 'SentinelDispatcher'
103
+
104
+
105
+ Sentinel = SentinelDispatcher()
106
+
107
+ _sentinels = dict[str, SentinelDispatcher]()
108
+
109
+ SelfSentinel = TypeVar('SelfSentinel', bound=SentinelDispatcher)