omlish 0.0.0.dev219__py3-none-any.whl → 0.0.0.dev221__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.
Files changed (66) hide show
  1. omlish/__about__.py +2 -2
  2. omlish/algorithm/__init__.py +0 -0
  3. omlish/algorithm/all.py +13 -0
  4. omlish/algorithm/distribute.py +46 -0
  5. omlish/algorithm/toposort.py +26 -0
  6. omlish/algorithm/unify.py +31 -0
  7. omlish/antlr/dot.py +13 -6
  8. omlish/collections/__init__.py +0 -2
  9. omlish/collections/utils.py +0 -46
  10. omlish/dataclasses/__init__.py +1 -0
  11. omlish/dataclasses/impl/api.py +10 -0
  12. omlish/dataclasses/impl/fields.py +8 -1
  13. omlish/dataclasses/impl/init.py +1 -1
  14. omlish/dataclasses/impl/main.py +1 -1
  15. omlish/dataclasses/impl/metaclass.py +112 -29
  16. omlish/dataclasses/impl/overrides.py +53 -0
  17. omlish/dataclasses/impl/params.py +3 -0
  18. omlish/dataclasses/impl/reflect.py +17 -5
  19. omlish/dataclasses/impl/simple.py +0 -42
  20. omlish/docker/oci/building.py +122 -0
  21. omlish/docker/oci/data.py +62 -8
  22. omlish/docker/oci/datarefs.py +98 -0
  23. omlish/docker/oci/loading.py +120 -0
  24. omlish/docker/oci/media.py +44 -14
  25. omlish/docker/oci/repositories.py +72 -0
  26. omlish/graphs/trees.py +2 -1
  27. omlish/http/coro/server.py +53 -24
  28. omlish/http/{simple.py → coro/simple.py} +17 -17
  29. omlish/http/handlers.py +8 -0
  30. omlish/io/fileno.py +11 -0
  31. omlish/lang/__init__.py +4 -1
  32. omlish/lang/cached.py +0 -1
  33. omlish/lang/classes/__init__.py +3 -1
  34. omlish/lang/classes/abstract.py +14 -1
  35. omlish/lang/classes/restrict.py +5 -5
  36. omlish/lang/classes/virtual.py +0 -1
  37. omlish/lang/clsdct.py +0 -1
  38. omlish/lang/contextmanagers.py +0 -8
  39. omlish/lang/descriptors.py +0 -1
  40. omlish/lang/maybes.py +0 -1
  41. omlish/lang/objects.py +0 -2
  42. omlish/secrets/ssl.py +9 -0
  43. omlish/secrets/tempssl.py +50 -0
  44. omlish/sockets/bind.py +6 -1
  45. omlish/sockets/server/server.py +18 -5
  46. omlish/specs/irc/__init__.py +0 -0
  47. omlish/specs/irc/format/LICENSE +11 -0
  48. omlish/specs/irc/format/__init__.py +61 -0
  49. omlish/specs/irc/format/consts.py +6 -0
  50. omlish/specs/irc/format/errors.py +30 -0
  51. omlish/specs/irc/format/message.py +18 -0
  52. omlish/specs/irc/format/nuh.py +52 -0
  53. omlish/specs/irc/format/parsing.py +155 -0
  54. omlish/specs/irc/format/rendering.py +150 -0
  55. omlish/specs/irc/format/tags.py +99 -0
  56. omlish/specs/irc/format/utils.py +27 -0
  57. omlish/specs/irc/numerics/__init__.py +0 -0
  58. omlish/specs/irc/numerics/formats.py +94 -0
  59. omlish/specs/irc/numerics/numerics.py +808 -0
  60. omlish/specs/irc/numerics/types.py +59 -0
  61. {omlish-0.0.0.dev219.dist-info → omlish-0.0.0.dev221.dist-info}/METADATA +1 -1
  62. {omlish-0.0.0.dev219.dist-info → omlish-0.0.0.dev221.dist-info}/RECORD +66 -38
  63. {omlish-0.0.0.dev219.dist-info → omlish-0.0.0.dev221.dist-info}/LICENSE +0 -0
  64. {omlish-0.0.0.dev219.dist-info → omlish-0.0.0.dev221.dist-info}/WHEEL +0 -0
  65. {omlish-0.0.0.dev219.dist-info → omlish-0.0.0.dev221.dist-info}/entry_points.txt +0 -0
  66. {omlish-0.0.0.dev219.dist-info → omlish-0.0.0.dev221.dist-info}/top_level.txt +0 -0
omlish/graphs/trees.py CHANGED
@@ -10,6 +10,7 @@ from .. import cached
10
10
  from .. import check
11
11
  from .. import collections as col
12
12
  from .. import lang
13
+ from ..algorithm import all as alg
13
14
 
14
15
 
15
16
  T = ta.TypeVar('T')
@@ -194,7 +195,7 @@ class BasicTreeAnalysis(ta.Generic[NodeT]):
194
195
  else:
195
196
  e, d = lang.identity, lang.identity
196
197
  tsd = {e(n): {e(p)} for n, p in parents_by_node.items()}
197
- ts = list(col.mut_toposort(tsd))
198
+ ts = list(alg.mut_toposort(tsd))
198
199
  root = d(check.single(ts[0]))
199
200
 
200
201
  return cls(
@@ -205,6 +205,10 @@ class CoroHttpServer:
205
205
  return h
206
206
  return None
207
207
 
208
+ def close(self) -> None:
209
+ if isinstance(d := self.data, HttpHandlerResponseStreamedData):
210
+ d.close()
211
+
208
212
  #
209
213
 
210
214
  def _build_response_head_bytes(self, a: _Response) -> bytes:
@@ -417,12 +421,20 @@ class CoroHttpServer:
417
421
  #
418
422
 
419
423
  def coro_handle(self) -> ta.Generator[Io, ta.Optional[bytes], None]:
420
- while True:
421
- gen = self.coro_handle_one()
424
+ return self._coro_run_handler(self._coro_handle_one())
422
425
 
423
- o = next(gen)
424
- i: ta.Optional[bytes]
425
- while True:
426
+ def _coro_run_handler(
427
+ self,
428
+ gen: ta.Generator[
429
+ ta.Union[AnyLogIo, AnyReadIo, _Response],
430
+ ta.Optional[bytes],
431
+ None,
432
+ ],
433
+ ) -> ta.Generator[Io, ta.Optional[bytes], None]:
434
+ i: ta.Optional[bytes]
435
+ o: ta.Any = next(gen)
436
+ while True:
437
+ try:
426
438
  if isinstance(o, self.AnyLogIo):
427
439
  i = None
428
440
  yield o
@@ -440,8 +452,13 @@ class CoroHttpServer:
440
452
  for b in self._yield_response_data(r):
441
453
  yield self.WriteIo(b)
442
454
 
455
+ o.close()
456
+ if o.close_connection:
457
+ break
458
+ o = None
459
+
443
460
  else:
444
- raise TypeError(o)
461
+ raise TypeError(o) # noqa
445
462
 
446
463
  try:
447
464
  o = gen.send(i)
@@ -450,7 +467,13 @@ class CoroHttpServer:
450
467
  except StopIteration:
451
468
  break
452
469
 
453
- def coro_handle_one(self) -> ta.Generator[
470
+ except Exception: # noqa
471
+ if hasattr(o, 'close'):
472
+ o.close()
473
+
474
+ raise
475
+
476
+ def _coro_handle_one(self) -> ta.Generator[
454
477
  ta.Union[AnyLogIo, AnyReadIo, _Response],
455
478
  ta.Optional[bytes],
456
479
  None,
@@ -530,28 +553,34 @@ class CoroHttpServer:
530
553
  yield self._build_error_response(err)
531
554
  return
532
555
 
533
- # Build internal response
556
+ try:
557
+ # Build internal response
534
558
 
535
- response_headers = handler_response.headers or {}
536
- response_data = handler_response.data
559
+ response_headers = handler_response.headers or {}
560
+ response_data = handler_response.data
537
561
 
538
- headers: ta.List[CoroHttpServer._Header] = [
539
- *self._make_default_headers(),
540
- ]
562
+ headers: ta.List[CoroHttpServer._Header] = [
563
+ *self._make_default_headers(),
564
+ ]
541
565
 
542
- for k, v in response_headers.items():
543
- headers.append(self._Header(k, v))
566
+ for k, v in response_headers.items():
567
+ headers.append(self._Header(k, v))
544
568
 
545
- if handler_response.close_connection and 'Connection' not in headers:
546
- headers.append(self._Header('Connection', 'close'))
569
+ if handler_response.close_connection and 'Connection' not in headers:
570
+ headers.append(self._Header('Connection', 'close'))
547
571
 
548
- yield self._Response(
549
- version=parsed.version,
550
- code=http.HTTPStatus(handler_response.status),
551
- headers=headers,
552
- data=response_data,
553
- close_connection=handler_response.close_connection,
554
- )
572
+ yield self._Response(
573
+ version=parsed.version,
574
+ code=http.HTTPStatus(handler_response.status),
575
+ headers=headers,
576
+ data=response_data,
577
+ close_connection=handler_response.close_connection,
578
+ )
579
+
580
+ except Exception: # noqa
581
+ handler_response.close()
582
+
583
+ raise
555
584
 
556
585
 
557
586
  ##
@@ -5,23 +5,23 @@ import contextlib
5
5
  import functools
6
6
  import typing as ta
7
7
 
8
- from ..lite.check import check
9
- from ..sockets.addresses import SocketAndAddress
10
- from ..sockets.bind import CanSocketBinder
11
- from ..sockets.bind import SocketBinder
12
- from ..sockets.server.handlers import ExecutorSocketServerHandler
13
- from ..sockets.server.handlers import SocketHandlerSocketServerHandler
14
- from ..sockets.server.handlers import SocketServerHandler
15
- from ..sockets.server.handlers import SocketWrappingSocketServerHandler
16
- from ..sockets.server.handlers import StandardSocketServerHandler
17
- from ..sockets.server.server import SocketServer
18
- from ..sockets.server.threading import ThreadingSocketServerHandler
19
- from .coro.server import CoroHttpServer
20
- from .coro.server import CoroHttpServerSocketHandler
21
- from .handlers import HttpHandler
22
- from .parsing import HttpRequestParser
23
- from .versions import HttpProtocolVersion
24
- from .versions import HttpProtocolVersions
8
+ from ...lite.check import check
9
+ from ...sockets.addresses import SocketAndAddress
10
+ from ...sockets.bind import CanSocketBinder
11
+ from ...sockets.bind import SocketBinder
12
+ from ...sockets.server.handlers import ExecutorSocketServerHandler
13
+ from ...sockets.server.handlers import SocketHandlerSocketServerHandler
14
+ from ...sockets.server.handlers import SocketServerHandler
15
+ from ...sockets.server.handlers import SocketWrappingSocketServerHandler
16
+ from ...sockets.server.handlers import StandardSocketServerHandler
17
+ from ...sockets.server.server import SocketServer
18
+ from ...sockets.server.threading import ThreadingSocketServerHandler
19
+ from ..handlers import HttpHandler
20
+ from ..parsing import HttpRequestParser
21
+ from ..versions import HttpProtocolVersion
22
+ from ..versions import HttpProtocolVersions
23
+ from .server import CoroHttpServer
24
+ from .server import CoroHttpServerSocketHandler
25
25
 
26
26
 
27
27
  if ta.TYPE_CHECKING:
omlish/http/handlers.py CHANGED
@@ -33,12 +33,20 @@ class HttpHandlerResponse:
33
33
  data: ta.Optional[HttpHandlerResponseData] = None
34
34
  close_connection: ta.Optional[bool] = None
35
35
 
36
+ def close(self) -> None:
37
+ if isinstance(d := self.data, HttpHandlerResponseStreamedData):
38
+ d.close()
39
+
36
40
 
37
41
  @dc.dataclass(frozen=True)
38
42
  class HttpHandlerResponseStreamedData:
39
43
  iter: ta.Iterable[bytes]
40
44
  length: ta.Optional[int] = None
41
45
 
46
+ def close(self) -> None:
47
+ if hasattr(d := self.iter, 'close'):
48
+ d.close() # noqa
49
+
42
50
 
43
51
  class HttpHandlerError(Exception):
44
52
  pass
omlish/io/fileno.py ADDED
@@ -0,0 +1,11 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import dataclasses as dc
4
+
5
+
6
+ @dc.dataclass(frozen=True)
7
+ class Fileno:
8
+ fd: int
9
+
10
+ def fileno(self) -> int:
11
+ return self.fd
omlish/lang/__init__.py CHANGED
@@ -6,11 +6,12 @@ from .cached import ( # noqa
6
6
 
7
7
  from .classes import ( # noqa
8
8
  Abstract,
9
+ AbstractTypeError,
9
10
  AnySensitive,
10
11
  Callable,
11
12
  Descriptor,
12
13
  Final,
13
- FinalError,
14
+ FinalTypeError,
14
15
  LazySingleton,
15
16
  Marker,
16
17
  Namespace,
@@ -26,6 +27,7 @@ from .classes import ( # noqa
26
27
  SimpleMetaDict,
27
28
  Singleton,
28
29
  Virtual,
30
+ get_abstract_methods,
29
31
  is_abstract,
30
32
  is_abstract_class,
31
33
  is_abstract_method,
@@ -52,6 +54,7 @@ from .cmp import ( # noqa
52
54
 
53
55
  from .contextmanagers import ( # noqa
54
56
  AsyncContextManager,
57
+ AsyncExitStacked,
55
58
  ContextManaged,
56
59
  ContextManager,
57
60
  ContextWrapped,
omlish/lang/cached.py CHANGED
@@ -158,7 +158,6 @@ class _CachedFunction(ta.Generic[T]):
158
158
 
159
159
 
160
160
  class _CachedFunctionDescriptor(_CachedFunction[T]):
161
-
162
161
  def __init__(
163
162
  self,
164
163
  fn: ta.Callable[P, T],
@@ -1,5 +1,7 @@
1
1
  from .abstract import ( # noqa
2
2
  Abstract,
3
+ AbstractTypeError,
4
+ get_abstract_methods,
3
5
  is_abstract,
4
6
  is_abstract_class,
5
7
  is_abstract_method,
@@ -10,7 +12,7 @@ from .abstract import ( # noqa
10
12
  from .restrict import ( # noqa
11
13
  AnySensitive,
12
14
  Final,
13
- FinalError,
15
+ FinalTypeError,
14
16
  NoBool,
15
17
  NotInstantiable,
16
18
  NotPicklable,
@@ -11,6 +11,8 @@ _ABSTRACT_METHODS_ATTR = '__abstractmethods__'
11
11
  _IS_ABSTRACT_METHOD_ATTR = '__isabstractmethod__'
12
12
  _FORCE_ABSTRACT_ATTR = '__forceabstract__'
13
13
 
14
+ _INTERNAL_ABSTRACT_ATTRS = frozenset([_FORCE_ABSTRACT_ATTR])
15
+
14
16
 
15
17
  def make_abstract(obj: T) -> T:
16
18
  if callable(obj):
@@ -27,6 +29,10 @@ def make_abstract(obj: T) -> T:
27
29
  return obj
28
30
 
29
31
 
32
+ class AbstractTypeError(TypeError):
33
+ pass
34
+
35
+
30
36
  class Abstract(abc.ABC): # noqa
31
37
  __slots__ = ()
32
38
 
@@ -50,7 +56,7 @@ class Abstract(abc.ABC): # noqa
50
56
  ams.update(set(getattr(b, _ABSTRACT_METHODS_ATTR, [])) - seen)
51
57
  seen.update(dir(b))
52
58
  if ams:
53
- raise TypeError(
59
+ raise AbstractTypeError(
54
60
  f'Cannot subclass abstract class {cls.__name__} with abstract methods: '
55
61
  f'{", ".join(map(str, sorted(ams)))}',
56
62
  )
@@ -78,6 +84,13 @@ def is_abstract(obj: ta.Any) -> bool:
78
84
  return is_abstract_method(obj) or is_abstract_class(obj)
79
85
 
80
86
 
87
+ def get_abstract_methods(cls: type, *, include_internal: bool = False) -> frozenset[str]:
88
+ ms = frozenset(getattr(cls, _ABSTRACT_METHODS_ATTR))
89
+ if not include_internal:
90
+ ms -= _INTERNAL_ABSTRACT_ATTRS
91
+ return ms
92
+
93
+
81
94
  def unabstract_class(
82
95
  members: ta.Iterable[str | tuple[str, ta.Any]],
83
96
  ): # -> ta.Callable[[type[T]], type[T]]:
@@ -9,7 +9,7 @@ from .abstract import is_abstract
9
9
  ##
10
10
 
11
11
 
12
- class FinalError(TypeError):
12
+ class FinalTypeError(TypeError):
13
13
  def __init__(self, _type: type) -> None:
14
14
  super().__init__()
15
15
 
@@ -28,11 +28,11 @@ class Final(Abstract):
28
28
  abstracts: set[ta.Any] = set()
29
29
  for base in cls.__bases__:
30
30
  if base is Abstract:
31
- raise FinalError(base)
31
+ raise FinalTypeError(base)
32
32
  elif base is Final:
33
33
  continue
34
34
  elif Final in base.__mro__:
35
- raise FinalError(base)
35
+ raise FinalTypeError(base)
36
36
  else:
37
37
  abstracts.update(getattr(base, '__abstractmethods__', []))
38
38
 
@@ -40,9 +40,9 @@ class Final(Abstract):
40
40
  try:
41
41
  v = cls.__dict__[a]
42
42
  except KeyError:
43
- raise FinalError(a) from None
43
+ raise FinalTypeError(a) from None
44
44
  if is_abstract(v):
45
- raise FinalError(a)
45
+ raise FinalTypeError(a)
46
46
 
47
47
 
48
48
  ##
@@ -28,7 +28,6 @@ def _make_not_instantiable():
28
28
 
29
29
 
30
30
  class _VirtualMeta(abc.ABCMeta):
31
-
32
31
  def __new__(mcls, name, bases, namespace):
33
32
  if 'Virtual' not in globals():
34
33
  return super().__new__(mcls, name, bases, namespace)
omlish/lang/clsdct.py CHANGED
@@ -36,7 +36,6 @@ def get_caller_cls_dct(offset: int = 0) -> ta.MutableMapping[str, ta.Any]:
36
36
 
37
37
 
38
38
  class ClassDctFn:
39
-
40
39
  def __init__(self, fn: ta.Callable, offset: int | None = None, *, wrap=True) -> None:
41
40
  super().__init__()
42
41
 
@@ -19,7 +19,6 @@ T = ta.TypeVar('T')
19
19
 
20
20
 
21
21
  class ContextManaged:
22
-
23
22
  def __enter__(self) -> ta.Self:
24
23
  return self
25
24
 
@@ -33,7 +32,6 @@ class ContextManaged:
33
32
 
34
33
 
35
34
  class NopContextManaged(ContextManaged):
36
-
37
35
  def __init_subclass__(cls, **kwargs: ta.Any) -> None:
38
36
  raise TypeError
39
37
 
@@ -42,7 +40,6 @@ NOP_CONTEXT_MANAGED = NopContextManaged()
42
40
 
43
41
 
44
42
  class NopContextManager:
45
-
46
43
  def __init_subclass__(cls, **kwargs: ta.Any) -> None:
47
44
  raise TypeError
48
45
 
@@ -57,7 +54,6 @@ NOP_CONTEXT_MANAGER = NopContextManager()
57
54
 
58
55
 
59
56
  class ContextManager(abc.ABC, ta.Generic[T]):
60
-
61
57
  def __init_subclass__(cls, **kwargs: ta.Any) -> None:
62
58
  super().__init_subclass__(**kwargs)
63
59
 
@@ -87,7 +83,6 @@ class ContextManager(abc.ABC, ta.Generic[T]):
87
83
 
88
84
 
89
85
  class AsyncContextManager(abc.ABC, ta.Generic[T]):
90
-
91
86
  def __init_subclass__(cls, **kwargs: ta.Any) -> None:
92
87
  super().__init_subclass__(**kwargs)
93
88
 
@@ -191,7 +186,6 @@ def attr_setting(obj, attr, val, *, default=None): # noqa
191
186
 
192
187
 
193
188
  class ExitStacked:
194
-
195
189
  @property
196
190
  def _exit_stack(self) -> contextlib.ExitStack:
197
191
  try:
@@ -229,7 +223,6 @@ class ExitStacked:
229
223
 
230
224
 
231
225
  class AsyncExitStacked:
232
-
233
226
  @property
234
227
  def _exit_stack(self) -> contextlib.AsyncExitStack:
235
228
  try:
@@ -276,7 +269,6 @@ ContextWrappable: ta.TypeAlias = ta.ContextManager | str | ta.Callable[..., ta.C
276
269
 
277
270
 
278
271
  class ContextWrapped:
279
-
280
272
  def __init__(self, fn: ta.Callable, cm: str | ContextWrappable) -> None:
281
273
  super().__init__()
282
274
 
@@ -197,7 +197,6 @@ decorator = _decorator
197
197
 
198
198
 
199
199
  class AccessForbiddenError(Exception):
200
-
201
200
  def __init__(self, name: str | None = None, *args: ta.Any, **kwargs: ta.Any) -> None:
202
201
  super().__init__(*((name,) if name is not None else ()), *args, **kwargs) # noqa
203
202
  self.name = name
omlish/lang/maybes.py CHANGED
@@ -11,7 +11,6 @@ class ValueNotPresentException(BaseException):
11
11
 
12
12
 
13
13
  class Maybe(abc.ABC, ta.Generic[T]):
14
-
15
14
  @property
16
15
  @abc.abstractmethod
17
16
  def present(self) -> bool:
omlish/lang/objects.py CHANGED
@@ -140,9 +140,7 @@ def dir_dict(o: ta.Any) -> dict[str, ta.Any]:
140
140
 
141
141
 
142
142
  class SimpleProxy(ta.Generic[T]):
143
-
144
143
  class Descriptor:
145
-
146
144
  def __init__(self, attr: str) -> None:
147
145
  super().__init__()
148
146
  self._attr = attr
omlish/secrets/ssl.py ADDED
@@ -0,0 +1,9 @@
1
+ # @omlish-lite
2
+ # ruff: noqa: UP006 UP007
3
+ import dataclasses as dc
4
+
5
+
6
+ @dc.dataclass(frozen=True)
7
+ class SslCert:
8
+ key_file: str
9
+ cert_file: str
@@ -0,0 +1,50 @@
1
+ # @omlish-lite
2
+ # ruff: noqa: UP006 UP007
3
+ import os.path
4
+ import subprocess
5
+ import tempfile
6
+ import typing as ta
7
+
8
+ from .ssl import SslCert
9
+
10
+
11
+ class TempSslCert(ta.NamedTuple):
12
+ cert: SslCert
13
+ temp_dir: str
14
+
15
+
16
+ def generate_temp_localhost_ssl_cert() -> TempSslCert:
17
+ temp_dir = tempfile.mkdtemp()
18
+
19
+ proc = subprocess.run(
20
+ [
21
+ 'openssl',
22
+ 'req',
23
+ '-x509',
24
+ '-newkey', 'rsa:2048',
25
+
26
+ '-keyout', 'key.pem',
27
+ '-out', 'cert.pem',
28
+
29
+ '-days', '365',
30
+
31
+ '-nodes',
32
+
33
+ '-subj', '/CN=localhost',
34
+ '-addext', 'subjectAltName = DNS:localhost,IP:127.0.0.1',
35
+ ],
36
+ cwd=temp_dir,
37
+ capture_output=True,
38
+ check=False,
39
+ )
40
+
41
+ if proc.returncode:
42
+ raise RuntimeError(f'Failed to generate temp ssl cert: {proc.stderr=}')
43
+
44
+ return TempSslCert(
45
+ SslCert(
46
+ key_file=os.path.join(temp_dir, 'key.pem'),
47
+ cert_file=os.path.join(temp_dir, 'cert.pem'),
48
+ ),
49
+ temp_dir,
50
+ )
omlish/sockets/bind.py CHANGED
@@ -2,7 +2,10 @@
2
2
  # @omlish-lite
3
3
  """
4
4
  TODO:
5
+ - def parse: (<bind>)?:<port>, unix://, fd://
6
+ - unix chown/chgrp
5
7
  - DupSocketBinder
8
+ - udp
6
9
  """
7
10
  import abc
8
11
  import dataclasses as dc
@@ -20,6 +23,7 @@ from omlish.sockets.addresses import SocketAndAddress
20
23
 
21
24
  SocketBinderT = ta.TypeVar('SocketBinderT', bound='SocketBinder')
22
25
  SocketBinderConfigT = ta.TypeVar('SocketBinderConfigT', bound='SocketBinder.Config')
26
+
23
27
  CanSocketBinderConfig = ta.Union['SocketBinder.Config', int, ta.Tuple[str, int], str] # ta.TypeAlias
24
28
  CanSocketBinder = ta.Union['SocketBinder', CanSocketBinderConfig] # ta.TypeAlias
25
29
 
@@ -308,7 +312,8 @@ class UnixSocketBinder(SocketBinder):
308
312
 
309
313
  if self._config.unlink:
310
314
  try:
311
- os.unlink(self._config.file)
315
+ if stat.S_ISSOCK(os.stat(self._config.file).st_mode):
316
+ os.unlink(self._config.file)
312
317
  except FileNotFoundError:
313
318
  pass
314
319
 
@@ -2,10 +2,12 @@
2
2
  # ruff: noqa: UP006 UP007
3
3
  import abc
4
4
  import contextlib
5
+ import logging
5
6
  import selectors
6
7
  import threading
7
8
  import typing as ta
8
9
 
10
+ from ..addresses import SocketAndAddress
9
11
  from ..bind import SocketBinder
10
12
  from ..io import close_socket_immediately
11
13
  from .handlers import SocketServerHandler
@@ -15,12 +17,15 @@ from .handlers import SocketServerHandler
15
17
 
16
18
 
17
19
  class SocketServer(abc.ABC):
20
+ _DEFAULT_LOGGER = logging.getLogger('.'.join([__name__, 'SocketServer']))
21
+
18
22
  def __init__(
19
23
  self,
20
24
  binder: SocketBinder,
21
25
  handler: SocketServerHandler,
22
26
  *,
23
- on_error: ta.Optional[ta.Callable[[BaseException], None]] = None,
27
+ on_error: ta.Optional[ta.Callable[[BaseException, ta.Optional[SocketAndAddress]], None]] = None,
28
+ error_logger: ta.Optional[logging.Logger] = _DEFAULT_LOGGER,
24
29
  poll_interval: float = .5,
25
30
  shutdown_timeout: ta.Optional[float] = None,
26
31
  ) -> None:
@@ -29,6 +34,7 @@ class SocketServer(abc.ABC):
29
34
  self._binder = binder
30
35
  self._handler = handler
31
36
  self._on_error = on_error
37
+ self._error_logger = error_logger
32
38
  self._poll_interval = poll_interval
33
39
  self._shutdown_timeout = shutdown_timeout
34
40
 
@@ -46,6 +52,15 @@ class SocketServer(abc.ABC):
46
52
 
47
53
  #
48
54
 
55
+ def _handle_error(self, exc: BaseException, conn: ta.Optional[SocketAndAddress] = None) -> None:
56
+ if (error_logger := self._error_logger) is not None:
57
+ error_logger.exception('Error in socket server: %r', conn)
58
+
59
+ if (on_error := self._on_error) is not None:
60
+ on_error(exc, conn)
61
+
62
+ #
63
+
49
64
  class SelectorProtocol(ta.Protocol):
50
65
  def register(self, *args, **kwargs) -> None:
51
66
  raise NotImplementedError
@@ -100,8 +115,7 @@ class SocketServer(abc.ABC):
100
115
  conn = self._binder.accept()
101
116
 
102
117
  except OSError as exc:
103
- if (on_error := self._on_error) is not None:
104
- on_error(exc)
118
+ self._handle_error(exc)
105
119
 
106
120
  return
107
121
 
@@ -109,8 +123,7 @@ class SocketServer(abc.ABC):
109
123
  self._handler(conn)
110
124
 
111
125
  except Exception as exc: # noqa
112
- if (on_error := self._on_error) is not None:
113
- on_error(exc)
126
+ self._handle_error(exc, conn)
114
127
 
115
128
  close_socket_immediately(conn.socket)
116
129
 
File without changes
@@ -0,0 +1,11 @@
1
+ Copyright (c) 2016-2021 Daniel Oaks
2
+ Copyright (c) 2018-2021 Shivaram Lingamneni
3
+
4
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted,
5
+ provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
8
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
9
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
10
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
11
+ THIS SOFTWARE.