omlish 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev3__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 omlish might be problematic. Click here for more details.

Files changed (147) hide show
  1. omlish/__about__.py +2 -3
  2. omlish/argparse.py +8 -8
  3. omlish/asyncs/__init__.py +2 -2
  4. omlish/asyncs/anyio.py +64 -1
  5. omlish/asyncs/asyncs.py +1 -3
  6. omlish/asyncs/futures.py +16 -15
  7. omlish/c3.py +5 -5
  8. omlish/check.py +8 -8
  9. omlish/collections/__init__.py +98 -63
  10. omlish/collections/_abc.py +2 -0
  11. omlish/collections/_io_abc.py +4 -2
  12. omlish/collections/cache/__init__.py +1 -1
  13. omlish/collections/cache/descriptor.py +12 -12
  14. omlish/collections/cache/impl.py +27 -20
  15. omlish/collections/cache/types.py +1 -1
  16. omlish/collections/coerce.py +44 -44
  17. omlish/collections/frozen.py +9 -9
  18. omlish/collections/identity.py +4 -5
  19. omlish/collections/mappings.py +5 -5
  20. omlish/collections/ordered.py +8 -8
  21. omlish/collections/skiplist.py +7 -7
  22. omlish/collections/sorted.py +4 -4
  23. omlish/collections/treap.py +42 -17
  24. omlish/collections/treapmap.py +59 -7
  25. omlish/collections/unmodifiable.py +25 -24
  26. omlish/collections/utils.py +1 -1
  27. omlish/configs/flattening.py +8 -7
  28. omlish/configs/props.py +3 -3
  29. omlish/dataclasses/__init__.py +1 -1
  30. omlish/dataclasses/impl/__init__.py +18 -0
  31. omlish/dataclasses/impl/api.py +15 -24
  32. omlish/dataclasses/impl/as_.py +4 -4
  33. omlish/dataclasses/impl/exceptions.py +1 -1
  34. omlish/dataclasses/impl/fields.py +8 -8
  35. omlish/dataclasses/impl/frozen.py +2 -2
  36. omlish/dataclasses/impl/init.py +6 -6
  37. omlish/dataclasses/impl/internals.py +16 -1
  38. omlish/dataclasses/impl/main.py +4 -4
  39. omlish/dataclasses/impl/metaclass.py +2 -2
  40. omlish/dataclasses/impl/metadata.py +1 -1
  41. omlish/dataclasses/impl/order.py +2 -2
  42. omlish/dataclasses/impl/params.py +4 -38
  43. omlish/dataclasses/impl/reflect.py +1 -7
  44. omlish/dataclasses/impl/replace.py +1 -1
  45. omlish/dataclasses/impl/repr.py +24 -6
  46. omlish/dataclasses/impl/simple.py +2 -2
  47. omlish/dataclasses/impl/slots.py +2 -2
  48. omlish/dataclasses/impl/utils.py +7 -7
  49. omlish/defs.py +13 -17
  50. omlish/diag/procfs.py +334 -0
  51. omlish/diag/ps.py +47 -0
  52. omlish/{replserver → diag/replserver}/console.py +26 -28
  53. omlish/{replserver → diag/replserver}/server.py +12 -12
  54. omlish/dispatch/dispatch.py +14 -16
  55. omlish/dispatch/functions.py +1 -1
  56. omlish/dispatch/methods.py +6 -7
  57. omlish/docker.py +8 -6
  58. omlish/dynamic.py +13 -13
  59. omlish/fnpairs.py +311 -0
  60. omlish/graphs/dot/items.py +1 -1
  61. omlish/graphs/trees.py +25 -31
  62. omlish/inject/__init__.py +7 -7
  63. omlish/inject/elements.py +2 -2
  64. omlish/inject/exceptions.py +8 -8
  65. omlish/inject/impl/elements.py +4 -4
  66. omlish/inject/impl/injector.py +6 -6
  67. omlish/inject/impl/inspect.py +3 -3
  68. omlish/inject/impl/scopes.py +9 -9
  69. omlish/inject/injector.py +1 -1
  70. omlish/inject/providers.py +2 -2
  71. omlish/inject/proxy.py +5 -5
  72. omlish/iterators.py +62 -26
  73. omlish/json.py +7 -6
  74. omlish/lang/__init__.py +172 -112
  75. omlish/lang/cached.py +15 -10
  76. omlish/lang/classes/__init__.py +35 -24
  77. omlish/lang/classes/abstract.py +3 -3
  78. omlish/lang/classes/restrict.py +14 -14
  79. omlish/lang/classes/simple.py +2 -2
  80. omlish/lang/classes/virtual.py +5 -5
  81. omlish/lang/clsdct.py +2 -2
  82. omlish/lang/cmp.py +2 -2
  83. omlish/lang/contextmanagers.py +31 -25
  84. omlish/lang/datetimes.py +1 -1
  85. omlish/lang/descriptors.py +51 -6
  86. omlish/lang/exceptions.py +2 -0
  87. omlish/lang/functions.py +101 -35
  88. omlish/lang/imports.py +25 -30
  89. omlish/lang/iterables.py +2 -2
  90. omlish/lang/maybes.py +2 -1
  91. omlish/lang/objects.py +17 -11
  92. omlish/lang/resolving.py +1 -1
  93. omlish/lang/strings.py +1 -1
  94. omlish/lang/timeouts.py +53 -0
  95. omlish/lang/typing.py +5 -5
  96. omlish/libc.py +15 -11
  97. omlish/logs/_abc.py +5 -1
  98. omlish/logs/filters.py +2 -0
  99. omlish/logs/formatters.py +6 -2
  100. omlish/logs/utils.py +1 -1
  101. omlish/marshal/base.py +9 -9
  102. omlish/marshal/dataclasses.py +2 -2
  103. omlish/marshal/enums.py +2 -2
  104. omlish/marshal/exceptions.py +1 -1
  105. omlish/marshal/factories.py +10 -10
  106. omlish/marshal/global_.py +10 -4
  107. omlish/marshal/iterables.py +2 -2
  108. omlish/marshal/mappings.py +2 -2
  109. omlish/marshal/objects.py +1 -2
  110. omlish/marshal/optionals.py +4 -4
  111. omlish/marshal/polymorphism.py +4 -4
  112. omlish/marshal/registries.py +3 -3
  113. omlish/marshal/standard.py +6 -6
  114. omlish/marshal/utils.py +3 -3
  115. omlish/marshal/values.py +1 -1
  116. omlish/math.py +9 -9
  117. omlish/os.py +13 -4
  118. omlish/reflect.py +5 -15
  119. omlish/sql/__init__.py +0 -0
  120. omlish/sql/_abc.py +65 -0
  121. omlish/sql/dbs.py +90 -0
  122. omlish/stats.py +7 -8
  123. omlish/term.py +1 -1
  124. omlish/testing/pydevd.py +30 -12
  125. omlish/testing/pytest/inject/__init__.py +7 -0
  126. omlish/testing/pytest/inject/harness.py +24 -2
  127. omlish/testing/pytest/plugins/__init__.py +1 -1
  128. omlish/testing/pytest/plugins/pydevd.py +12 -0
  129. omlish/testing/pytest/plugins/switches.py +3 -3
  130. omlish/testing/testing.py +5 -5
  131. omlish/text/delimit.py +3 -6
  132. omlish/text/parts.py +3 -3
  133. omlish-0.0.0.dev3.dist-info/METADATA +31 -0
  134. omlish-0.0.0.dev3.dist-info/RECORD +191 -0
  135. {omlish-0.0.0.dev1.dist-info → omlish-0.0.0.dev3.dist-info}/WHEEL +1 -1
  136. omlish/lang/classes/test/test_abstract.py +0 -89
  137. omlish/lang/classes/test/test_restrict.py +0 -71
  138. omlish/lang/classes/test/test_simple.py +0 -58
  139. omlish/lang/classes/test/test_virtual.py +0 -72
  140. omlish/testing/pytest/plugins/pycharm.py +0 -54
  141. omlish-0.0.0.dev1.dist-info/METADATA +0 -17
  142. omlish-0.0.0.dev1.dist-info/RECORD +0 -187
  143. /omlish/{lang/classes/test → diag}/__init__.py +0 -0
  144. /omlish/{replserver → diag/replserver}/__init__.py +0 -0
  145. /omlish/{replserver → diag/replserver}/__main__.py +0 -0
  146. {omlish-0.0.0.dev1.dist-info → omlish-0.0.0.dev3.dist-info}/LICENSE +0 -0
  147. {omlish-0.0.0.dev1.dist-info → omlish-0.0.0.dev3.dist-info}/top_level.txt +0 -0
omlish/diag/procfs.py ADDED
@@ -0,0 +1,334 @@
1
+ """
2
+ TODO:
3
+ - dataclasses
4
+ """
5
+ import argparse
6
+ import logging
7
+ import os
8
+ import re
9
+ import resource
10
+ import struct
11
+ import sys
12
+ import typing as ta
13
+
14
+ from .. import iterators as it
15
+ from .. import json
16
+ from .. import lang
17
+ from .. import os as oos
18
+
19
+
20
+ log = logging.getLogger(__name__)
21
+
22
+
23
+ PidLike = int | str
24
+
25
+
26
+ RLIMIT_RESOURCES = {
27
+ getattr(resource, k): k
28
+ for k in dir(resource)
29
+ if k.startswith('RLIMIT_')
30
+ }
31
+
32
+
33
+ def parse_size(s: str) -> int:
34
+ if ' ' not in s:
35
+ return int(s)
36
+ us = {'kB': 1024, 'mB': 1024 * 1024}
37
+ v, u = s.split()
38
+ return int(v) * us[u]
39
+
40
+
41
+ class ProcStat(lang.Namespace):
42
+ PID = 0
43
+ COMM = 1
44
+ STATE = 2
45
+ PPID = 3
46
+ PGRP = 4
47
+ SESSION = 5
48
+ TTY_NR = 6
49
+ TPGID = 7
50
+ FLAGS = 8
51
+ MINFLT = 9
52
+ CMINFLT = 10
53
+ MAJFLT = 11
54
+ CMAJFLT = 12
55
+ UTIME = 13
56
+ STIME = 14
57
+ CUTIME = 15
58
+ CSTIME = 16
59
+ PRIORITY = 17
60
+ NICE = 18
61
+ NUM_THREADS = 19
62
+ ITREALVALUE = 20
63
+ STARTTIME = 21
64
+ VSIZE = 22
65
+ RSS = 23
66
+ RSSLIM = 24
67
+ STARTCODE = 25
68
+ ENDCODE = 26
69
+ STARTSTACK = 27
70
+ KSTKESP = 28
71
+ KSTKEIP = 29
72
+ SIGNAL = 30
73
+ BLOCKED = 31
74
+ SIGIGNORE = 32
75
+ SIGCATCH = 33
76
+ WCHAN = 34
77
+ NSWAP = 35
78
+ CNSWAP = 36
79
+ EXIT_SIGNAL = 37
80
+ PROCESSOR = 38
81
+ RT_PRIORITY = 39
82
+ POLICY = 40
83
+ DELAYACCT_BLKIO_TICKS = 41
84
+ GUEST_TIME = 42
85
+ CGUEST_TIME = 43
86
+ START_DATA = 44
87
+ END_DATA = 45
88
+ START_BRK = 46
89
+ ARG_START = 47
90
+ ARG_END = 48
91
+ ENV_START = 49
92
+ ENV_END = 50
93
+ EXIT_CODE = 51
94
+
95
+
96
+ def _check_linux() -> None:
97
+ if sys.platform != 'linux':
98
+ raise OSError
99
+
100
+
101
+ def get_process_stats(pid: PidLike = 'self') -> list[str]:
102
+ """http://man7.org/linux/man-pages/man5/proc.5.html -> /proc/[pid]/stat"""
103
+
104
+ _check_linux()
105
+ with open(f'/proc/{pid}/stat') as f:
106
+ buf = f.read()
107
+ l, _, r = buf.rpartition(')')
108
+ pid, _, comm = l.partition('(')
109
+ return [pid.strip(), comm, *r.strip().split(' ')]
110
+
111
+
112
+ def get_process_chain(pid: PidLike = 'self') -> list[tuple[int, str]]:
113
+ _check_linux()
114
+ lst = []
115
+ while pid:
116
+ process_stats = get_process_stats(pid)
117
+ lst.append((int(process_stats[ProcStat.PID]), process_stats[ProcStat.COMM]))
118
+ pid = int(process_stats[ProcStat.PPID])
119
+ return lst
120
+
121
+
122
+ def get_process_start_time(pid: PidLike = 'self') -> int:
123
+ """https://stackoverflow.com/questions/2598145/how-to-retrieve-the-process-start-time-or-uptime-in-python"""
124
+
125
+ _check_linux()
126
+ hz = os.sysconf(os.sysconf_names['SC_CLK_TCK'])
127
+ with open('/proc/stat') as f:
128
+ system_stats = f.readlines()
129
+ for line in system_stats:
130
+ if line.startswith('btime'):
131
+ boot_timestamp = int(line.split()[1])
132
+ break
133
+ else:
134
+ raise ValueError
135
+ process_stats = get_process_stats(pid)
136
+ age_from_boot_jiffies = int(process_stats[ProcStat.STARTTIME])
137
+ age_from_boot_timestamp = age_from_boot_jiffies // hz
138
+ return boot_timestamp + age_from_boot_timestamp
139
+
140
+
141
+ def get_process_rss(pid: PidLike = 'self') -> int:
142
+ return int(get_process_stats(pid)[ProcStat.RSS])
143
+
144
+
145
+ def set_process_oom_score_adj(score: str, pid: PidLike = 'self') -> None:
146
+ _check_linux()
147
+ with open(f'/proc/{pid}/oom_score_adj', 'w') as f:
148
+ f.write(str(score))
149
+
150
+
151
+ MAP_LINE_RX = re.compile(
152
+ r'^'
153
+ r'(?P<address>[A-Fa-f0-9]+)-(?P<end_address>[A-Fa-f0-9]+)\s+'
154
+ r'(?P<permissions>\S+)\s+'
155
+ r'(?P<offset>[A-Fa-f0-9]+)\s+'
156
+ r'(?P<device>\S+)\s+'
157
+ r'(?P<inode>\d+)\s+'
158
+ r'(?P<path>.*)'
159
+ r'$'
160
+ )
161
+
162
+
163
+ def get_process_maps(pid: PidLike = 'self', sharing: bool = False) -> ta.Iterator[dict[str, ta.Any]]:
164
+ """http://man7.org/linux/man-pages/man5/proc.5.html -> /proc/[pid]/maps"""
165
+
166
+ _check_linux()
167
+ with open(f'/proc/{pid}/{"smaps" if sharing else "maps"}') as map_file:
168
+ while True:
169
+ line = map_file.readline()
170
+ if not line:
171
+ break
172
+ m = MAP_LINE_RX.match(line)
173
+ if not m:
174
+ raise ValueError(line)
175
+ address = int(m.group('address'), 16)
176
+ end_address = int(m.group('end_address'), 16)
177
+ d = {
178
+ 'address': address,
179
+ 'end_address': end_address,
180
+ 'size': end_address - address,
181
+ 'permissions': [x for x in m.group('permissions') if x != '-'],
182
+ 'offset': int(m.group('offset'), 16),
183
+ 'device': m.group('device'),
184
+ 'inode': int(m.group('inode')),
185
+ 'path': m.group('path'),
186
+ }
187
+ if sharing:
188
+ s: dict[str, ta.Any] = {}
189
+ while True:
190
+ line = map_file.readline()
191
+ k, v = line.split(':')
192
+ if k.lower() == 'vmflags':
193
+ break
194
+ s[k.lower()] = parse_size(v.strip())
195
+ _, v = line.split(':')
196
+ s['vmflags'] = [p for p in [j.strip() for j in v.split(' ')] if p]
197
+ d['sharing'] = s
198
+ yield d
199
+
200
+
201
+ PAGEMAP_KEYS = (
202
+ 'address',
203
+ 'pfn',
204
+ 'swap_type',
205
+ 'swap_offset',
206
+ 'pte_soft_dirty',
207
+ 'file_page_or_shared_anon',
208
+ 'page_swapped',
209
+ 'page_present',
210
+ )
211
+
212
+
213
+ def get_process_range_pagemaps(start: int, end: int, pid: PidLike = 'self') -> ta.Iterable[dict[str, int]]:
214
+ """https://www.kernel.org/doc/Documentation/vm/pagemap.txt"""
215
+
216
+ _check_linux()
217
+ offset = (start // oos.PAGE_SIZE) * 8
218
+ npages = ((end - start) // oos.PAGE_SIZE)
219
+ size = npages * 8
220
+ with open(f'/proc/{pid}/pagemap', 'rb') as pagemap_file:
221
+ pagemap_file.seek(offset)
222
+ pagemap_buf = pagemap_file.read(size)
223
+ if not pagemap_buf:
224
+ return
225
+ _struct_unpack = struct.unpack
226
+ for pagenum in range(npages):
227
+ [packed] = _struct_unpack('Q', pagemap_buf[pagenum * 8:(pagenum + 1) * 8])
228
+ yield {
229
+ 'address': start + (pagenum * oos.PAGE_SIZE),
230
+ 'pfn': (packed & ((1 << (54 + 1)) - 1)),
231
+ 'swap_type': (packed & ((1 << (4 + 1)) - 1)),
232
+ 'swap_offset': (packed & ((1 << (54 + 1)) - 1)) >> 5,
233
+ 'pte_soft_dirty': ((packed >> 55) & 1) > 0,
234
+ 'file_page_or_shared_anon': ((packed >> 61) & 1) > 0,
235
+ 'page_swapped': ((packed >> 62) & 1) > 0,
236
+ 'page_present': ((packed >> 63) & 1) > 0,
237
+ }
238
+
239
+
240
+ def get_process_pagemaps(pid: PidLike = 'self') -> ta.Iterable[dict[str, int]]:
241
+ _check_linux()
242
+ for m in get_process_maps(pid):
243
+ yield from get_process_range_pagemaps(m['address'], m['end_address'], pid)
244
+
245
+
246
+ def _dump_cmd(args: ta.Any) -> None:
247
+ total = 0
248
+ dirty_total = 0
249
+ for m in get_process_maps(args.pid, sharing=True):
250
+ total += m['sharing']['rss']
251
+ sys.stdout.write(json.dumps({'map': m}))
252
+ sys.stdout.write('\n')
253
+ for pm in get_process_range_pagemaps(m['address'], m['end_address'], args.pid):
254
+ if pm['pte_soft_dirty']:
255
+ dirty_total += oos.PAGE_SIZE
256
+ sys.stdout.write(json.dumps({'page': tuple(pm[k] for k in PAGEMAP_KEYS)}))
257
+ sys.stdout.write('\n')
258
+ dct = {
259
+ 'total': total,
260
+ 'dirty_total': dirty_total,
261
+ }
262
+ sys.stdout.write(json.dumps(dct))
263
+ sys.stdout.write('\n')
264
+
265
+
266
+ def _cmp_cmd(args: ta.Any) -> None:
267
+ if len(args.pids) == 1:
268
+ [rpid] = args.pids
269
+ lpid = get_process_chain(rpid)[1][0]
270
+ elif len(args.pids) == 2:
271
+ lpid, rpid = args.pids
272
+ else:
273
+ raise TypeError('Invalid arguments')
274
+
275
+ def g(pid: int) -> ta.Iterator[dict[str, int]]:
276
+ for m in get_process_maps(pid, sharing=True):
277
+ yield from get_process_range_pagemaps(m['address'], m['end_address'], pid)
278
+
279
+ lpms, rpms = (g(pid) for pid in (lpid, rpid))
280
+
281
+ l_pages = 0
282
+ r_pages = 0
283
+ c_pages = 0
284
+ for _, ps in it.merge_on(lambda pm: pm['address'], lpms, rpms):
285
+ l, r = it.expand_indexed_pairs(ps, None, width=2)
286
+ if l is not None and r is None:
287
+ l_pages += 1
288
+ elif l is None and r is not None:
289
+ r_pages += 1
290
+ elif l['pfn'] != r['pfn']: # type: ignore
291
+ c_pages += 1
292
+ else:
293
+ continue
294
+ if not args.quiet:
295
+ sys.stdout.write(json.dumps([l, r]))
296
+ sys.stdout.write('\n')
297
+ l_pages += c_pages
298
+ r_pages += c_pages
299
+ dct = {
300
+ 'l_pages': l_pages,
301
+ 'l_bytes': l_pages * oos.PAGE_SIZE,
302
+ 'r_pages': r_pages,
303
+ 'r_bytes': r_pages * oos.PAGE_SIZE,
304
+ 'c_pages': c_pages,
305
+ 'c_bytes': c_pages * oos.PAGE_SIZE,
306
+ }
307
+ sys.stdout.write(json.dumps(dct))
308
+ sys.stdout.write('\n')
309
+
310
+
311
+ def _main() -> None:
312
+ _check_linux()
313
+
314
+ arg_parser = argparse.ArgumentParser()
315
+ arg_parser.add_argument('-q', '--quiet', action='store_true')
316
+ arg_subparsers = arg_parser.add_subparsers()
317
+
318
+ dump_arg_parser = arg_subparsers.add_parser('dump')
319
+ dump_arg_parser.add_argument('pid', type=int)
320
+ dump_arg_parser.set_defaults(func=_dump_cmd)
321
+
322
+ cmp_arg_parser = arg_subparsers.add_parser('cmp')
323
+ cmp_arg_parser.add_argument('pids', type=int, nargs='*')
324
+ cmp_arg_parser.set_defaults(func=_cmp_cmd)
325
+
326
+ args = arg_parser.parse_args()
327
+ if not hasattr(args, 'func'):
328
+ arg_parser.print_help()
329
+ else:
330
+ args.func(args)
331
+
332
+
333
+ if __name__ == '__main__':
334
+ _main()
omlish/diag/ps.py ADDED
@@ -0,0 +1,47 @@
1
+ import dataclasses as dc
2
+ import os
3
+ import subprocess
4
+
5
+ from .. import lang
6
+
7
+
8
+ @dc.dataclass(frozen=True)
9
+ class PsItem:
10
+ pid: int
11
+ ppid: int
12
+ cmd: str
13
+
14
+
15
+ def get_ps_item(pid: int, timeout: lang.Timeout | None = None) -> PsItem:
16
+ timeout = lang.timeout(timeout)
17
+ out = subprocess.check_output(
18
+ ['ps', '-o', 'pid=,ppid=,command=', str(int(pid))],
19
+ timeout=timeout.or_(None),
20
+ ).decode().strip()
21
+ opid, _, rest = out.partition(' ')
22
+ ppid, _, cmd = rest.strip().partition(' ')
23
+ return PsItem(
24
+ int(opid),
25
+ int(ppid),
26
+ cmd.strip(),
27
+ )
28
+
29
+
30
+ def get_ps_lineage(pid: int, timeout: lang.Timeout | None = None) -> list[PsItem]:
31
+ timeout = lang.timeout(timeout)
32
+ ret: list[PsItem] = []
33
+ while True:
34
+ cur = get_ps_item(pid, timeout)
35
+ if cur.ppid < 1:
36
+ break
37
+ ret.append(cur)
38
+ pid = cur.ppid
39
+ return ret
40
+
41
+
42
+ def _main() -> None:
43
+ print(get_ps_lineage(os.getpid()))
44
+
45
+
46
+ if __name__ == '__main__':
47
+ _main()
@@ -25,13 +25,13 @@ import traceback
25
25
  import types
26
26
  import typing as ta
27
27
 
28
- from .. import check
28
+ from ... import check
29
29
 
30
30
 
31
31
  log = logging.getLogger(__name__)
32
32
 
33
33
 
34
- class DisconnectException(Exception):
34
+ class DisconnectError(Exception):
35
35
  pass
36
36
 
37
37
 
@@ -43,13 +43,13 @@ class InteractiveSocketConsole:
43
43
  def __init__(
44
44
  self,
45
45
  conn: sock.socket,
46
- locals: ta.Optional[dict[str, ta.Any]] = None,
47
- filename: str = '<console>'
46
+ locals: dict[str, ta.Any] | None = None, # noqa
47
+ filename: str = '<console>',
48
48
  ) -> None:
49
49
  super().__init__()
50
50
 
51
51
  if locals is None:
52
- locals = {
52
+ locals = { # noqa
53
53
  '__name__': '__console__',
54
54
  '__doc__': None,
55
55
  '__console__': self,
@@ -73,7 +73,7 @@ class InteractiveSocketConsole:
73
73
 
74
74
  CPRT = 'Type "help", "copyright", "credits" or "license" for more information.'
75
75
 
76
- def interact(self, banner: ta.Optional[str] = None, exitmsg: ta.Optional[str] = None) -> None:
76
+ def interact(self, banner: str | None = None, exitmsg: str | None = None) -> None:
77
77
  log.info(f'Console {id(self)} on thread {threading.current_thread().ident} interacting')
78
78
 
79
79
  try:
@@ -81,11 +81,9 @@ class InteractiveSocketConsole:
81
81
  ps2 = getattr(sys, 'ps2', '... ')
82
82
 
83
83
  if banner is None:
84
- self.write(
85
- 'Python %s on %s\n%s\n(%s)\n' %
86
- (sys.version, sys.platform, self.CPRT, self.__class__.__name__))
84
+ self.write(f'Python {sys.version} on {sys.platform}\n{self.CPRT}\n({self.__class__.__name__})\n')
87
85
  elif banner:
88
- self.write('%s\n' % (str(banner),))
86
+ self.write(f'{banner!s}\n')
89
87
 
90
88
  more = False
91
89
  while True:
@@ -104,12 +102,12 @@ class InteractiveSocketConsole:
104
102
  more = False
105
103
 
106
104
  if exitmsg is None:
107
- self.write('now exiting %s...\n' % self.__class__.__name__)
105
+ self.write(f'now exiting {self.__class__.__name__}...\n')
108
106
 
109
107
  elif exitmsg != '':
110
- self.write('%s\n' % exitmsg)
108
+ self.write(f'{exitmsg}\n')
111
109
 
112
- except DisconnectException:
110
+ except DisconnectError:
113
111
  pass
114
112
 
115
113
  except OSError as oe:
@@ -133,7 +131,7 @@ class InteractiveSocketConsole:
133
131
  while True:
134
132
  b = self._conn.recv(1)
135
133
  if not b:
136
- raise DisconnectException
134
+ raise DisconnectError
137
135
  if b == b'\n':
138
136
  break
139
137
  buf += b
@@ -144,10 +142,10 @@ class InteractiveSocketConsole:
144
142
 
145
143
  def compile(
146
144
  self,
147
- source: ta.Union[str, ast.AST],
145
+ source: str | ast.AST,
148
146
  filename: str = '<input>',
149
- symbol: str = 'single'
150
- ) -> ta.Optional[types.CodeType]:
147
+ symbol: str = 'single',
148
+ ) -> types.CodeType | None:
151
149
  if isinstance(source, ast.AST):
152
150
  return self._compiler.compiler(source, filename, symbol) # type: ignore
153
151
  else:
@@ -155,7 +153,7 @@ class InteractiveSocketConsole:
155
153
 
156
154
  def run_source(
157
155
  self,
158
- source: ta.Union[str, ast.AST],
156
+ source: str | ast.AST,
159
157
  filename: str = '<input>',
160
158
  symbol: str = 'single',
161
159
  ) -> bool:
@@ -195,7 +193,7 @@ class InteractiveSocketConsole:
195
193
  expr.value,
196
194
  lineno=expr.lineno,
197
195
  col_offset=expr.col_offset,
198
- )
196
+ ),
199
197
  ],
200
198
  )
201
199
  ast.fix_missing_locations(source)
@@ -227,21 +225,21 @@ class InteractiveSocketConsole:
227
225
  finally:
228
226
  last_tb = ei = None # type: ignore # noqa
229
227
 
230
- def show_syntax_error(self, filename: ta.Optional[str] = None) -> None:
231
- type, value, tb = sys.exc_info()
232
- sys.last_type = type
233
- sys.last_value = value
228
+ def show_syntax_error(self, filename: str | None = None) -> None:
229
+ et, e, tb = sys.exc_info()
230
+ sys.last_type = et
231
+ sys.last_value = e
234
232
  sys.last_traceback = tb
235
- if filename and type is SyntaxError:
233
+ if filename and et is SyntaxError:
236
234
  # Work hard to stuff the correct filename in the exception
237
235
  try:
238
- msg, (dummy_filename, lineno, offset, line) = value.args # type: ignore
236
+ msg, (dummy_filename, lineno, offset, line) = e.args # type: ignore
239
237
  except ValueError:
240
238
  # Not the format we expect; leave it alone
241
239
  pass
242
240
  else:
243
241
  # Stuff in the right filename
244
- value = SyntaxError(msg, (filename, lineno, offset, line))
245
- sys.last_value = value
246
- lines = traceback.format_exception_only(type, value)
242
+ e = SyntaxError(msg, (filename, lineno, offset, line))
243
+ sys.last_value = e
244
+ lines = traceback.format_exception_only(et, e)
247
245
  self.write(''.join(lines))
@@ -23,8 +23,8 @@ import threading
23
23
  import typing as ta
24
24
  import weakref
25
25
 
26
- from .. import check
27
- from .. import dataclasses as dc
26
+ from ... import check
27
+ from ... import dataclasses as dc
28
28
  from .console import InteractiveSocketConsole
29
29
 
30
30
 
@@ -38,7 +38,7 @@ class ReplServer:
38
38
  @dc.dataclass(frozen=True)
39
39
  class Config:
40
40
  path: str
41
- file_mode: ta.Optional[int] = None
41
+ file_mode: int | None = None
42
42
  poll_interval: float = 0.5
43
43
  exit_timeout: float = 10.0
44
44
 
@@ -51,7 +51,7 @@ class ReplServer:
51
51
  check.not_empty(config.path)
52
52
  self._config = check.isinstance(config, ReplServer.Config)
53
53
 
54
- self._socket: ta.Optional[sock.socket] = None
54
+ self._socket: sock.socket | None = None
55
55
  self._is_running = False
56
56
  self._consoles_by_threads: ta.MutableMapping[threading.Thread, InteractiveSocketConsole] = \
57
57
  weakref.WeakKeyDictionary() # noqa
@@ -62,12 +62,12 @@ class ReplServer:
62
62
  def path(self) -> str:
63
63
  return self._config.path
64
64
 
65
- def __enter__(self):
65
+ def __enter__(self) -> ta.Self:
66
66
  check.state(not self._is_running)
67
67
  check.state(not self._is_shutdown.is_set())
68
68
  return self
69
69
 
70
- def __exit__(self, exc_type, exc_val, exc_tb):
70
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
71
71
  if not self._is_shutdown.is_set():
72
72
  self.shutdown(True, self._config.exit_timeout)
73
73
 
@@ -91,7 +91,7 @@ class ReplServer:
91
91
  while not self._should_shutdown:
92
92
  try:
93
93
  conn, _ = self._socket.accept()
94
- except sock.timeout:
94
+ except TimeoutError:
95
95
  continue
96
96
 
97
97
  log.info(f'Got repl server connection on file {self._config.path}')
@@ -106,7 +106,7 @@ class ReplServer:
106
106
  log.info(
107
107
  f'Starting console {id(console)} repl server connection '
108
108
  f'on file {self._config.path} '
109
- f'on thread {threading.current_thread().ident}'
109
+ f'on thread {threading.current_thread().ident}',
110
110
  )
111
111
  self._consoles_by_threads[threading.current_thread()] = console
112
112
  console.interact()
@@ -117,13 +117,13 @@ class ReplServer:
117
117
  name=self.CONNECTION_THREAD_NAME)
118
118
  thread.start()
119
119
 
120
- for thread, console in self._consoles_by_threads.items():
120
+ for console in self._consoles_by_threads.values():
121
121
  try:
122
122
  console.conn.close()
123
123
  except Exception:
124
124
  log.exception('Error shutting down')
125
125
 
126
- for thread in self._consoles_by_threads.keys():
126
+ for thread in self._consoles_by_threads:
127
127
  try:
128
128
  thread.join(self._config.exit_timeout)
129
129
  except Exception:
@@ -135,12 +135,12 @@ class ReplServer:
135
135
  self._is_shutdown.set()
136
136
  self._is_running = False
137
137
 
138
- def shutdown(self, block: bool = False, timeout: ta.Optional[float] = None) -> None:
138
+ def shutdown(self, block: bool = False, timeout: float | None = None) -> None:
139
139
  self._should_shutdown = True
140
140
  if block:
141
141
  self._is_shutdown.wait(timeout=timeout)
142
142
 
143
143
 
144
- def run():
144
+ def run() -> None:
145
145
  with ReplServer(ReplServer.Config('repl.sock')) as repl_server:
146
146
  repl_server.run()
@@ -1,4 +1,5 @@
1
1
  import abc
2
+ import contextlib
2
3
  import typing as ta
3
4
  import weakref
4
5
 
@@ -13,33 +14,32 @@ T = ta.TypeVar('T')
13
14
  ##
14
15
 
15
16
 
16
- _IMPL_FUNC_CLS_SET_CACHE: ta.MutableMapping[ta.Callable, ta.FrozenSet[type]] = weakref.WeakKeyDictionary()
17
+ _IMPL_FUNC_CLS_SET_CACHE: ta.MutableMapping[ta.Callable, frozenset[type]] = weakref.WeakKeyDictionary()
17
18
 
18
19
 
19
- def get_impl_func_cls_set(func: ta.Callable) -> ta.FrozenSet[type]:
20
- try:
20
+ def get_impl_func_cls_set(func: ta.Callable) -> frozenset[type]:
21
+ with contextlib.suppress(KeyError):
21
22
  return _IMPL_FUNC_CLS_SET_CACHE[func]
22
- except KeyError:
23
- pass
24
23
 
25
24
  ann = getattr(func, '__annotations__', {})
26
25
  if not ann:
27
26
  raise TypeError(f'Invalid impl func: {func!r}')
28
27
 
29
28
  _, cls = next(iter(ta.get_type_hints(func).items()))
30
- if rfl.is_union_type(cls):
31
- ret = frozenset(check.isinstance(arg, type) for arg in ta.get_args(cls))
29
+ rty = rfl.type_(cls)
30
+ if isinstance(rty, rfl.Union):
31
+ ret = frozenset(check.isinstance(arg, type) for arg in rty.args)
32
32
  else:
33
- ret = frozenset([check.isinstance(cls, type)])
33
+ ret = frozenset([check.isinstance(rty, type)])
34
34
 
35
35
  _IMPL_FUNC_CLS_SET_CACHE[func] = ret
36
36
  return ret
37
37
 
38
38
 
39
- def find_impl(cls: type, registry: ta.Mapping[type, T]) -> ta.Optional[T]:
39
+ def find_impl(cls: type, registry: ta.Mapping[type, T]) -> T | None:
40
40
  mro = c3.compose_mro(cls, registry.keys())
41
41
 
42
- match: ta.Optional[type] = None
42
+ match: type | None = None
43
43
  for t in mro:
44
44
  if match is not None:
45
45
  # If *match* is an implicit ABC but there is another unrelated, equally matching implicit ABC, refuse the
@@ -71,23 +71,21 @@ class Dispatcher(ta.Generic[T]):
71
71
  impls_by_arg_cls: dict[type, T] = {}
72
72
  self._impls_by_arg_cls = impls_by_arg_cls
73
73
 
74
- dispatch_cache: dict[ta.Any, ta.Optional[T]] = {}
74
+ dispatch_cache: dict[ta.Any, T | None] = {}
75
75
  self._get_dispatch_cache = lambda: dispatch_cache
76
76
 
77
77
  def cache_remove(k, self_ref=weakref.ref(self)):
78
78
  if (ref_self := self_ref()) is not None:
79
79
  cache = ref_self._get_dispatch_cache() # noqa
80
- try:
80
+ with contextlib.suppress(KeyError):
81
81
  del cache[k]
82
- except KeyError:
83
- pass
84
82
 
85
83
  cache_token: ta.Any = None
86
84
  self._get_cache_token = lambda: cache_token
87
85
 
88
86
  weakref_ref_ = weakref.ref
89
87
 
90
- def dispatch(cls: type) -> ta.Optional[T]:
88
+ def dispatch(cls: type) -> T | None:
91
89
  nonlocal cache_token
92
90
 
93
91
  if cache_token is not None and (current_token := abc.get_cache_token()) != cache_token:
@@ -132,6 +130,6 @@ class Dispatcher(ta.Generic[T]):
132
130
  def cache_size(self) -> int:
133
131
  return len(self._get_dispatch_cache())
134
132
 
135
- dispatch: ta.Callable[[type], ta.Optional[T]]
133
+ dispatch: ta.Callable[[type], T | None]
136
134
 
137
135
  register: ta.Callable[[T, ta.Iterable[type]], T]