omlish 0.0.0.dev315__py3-none-any.whl → 0.0.0.dev317__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.
omlish/lite/maybes.py CHANGED
@@ -1,15 +1,23 @@
1
+ # ruff: noqa: UP007
1
2
  import abc
2
3
  import functools
3
4
  import typing as ta
4
5
 
5
6
 
6
7
  T = ta.TypeVar('T')
8
+ U = ta.TypeVar('U')
7
9
 
8
10
 
9
11
  ##
10
12
 
11
13
 
14
+ @functools.total_ordering
12
15
  class Maybe(ta.Generic[T]):
16
+ class ValueNotPresentError(BaseException):
17
+ pass
18
+
19
+ #
20
+
13
21
  @property
14
22
  @abc.abstractmethod
15
23
  def present(self) -> bool:
@@ -19,9 +27,102 @@ class Maybe(ta.Generic[T]):
19
27
  def must(self) -> T:
20
28
  raise NotImplementedError
21
29
 
30
+ #
31
+
32
+ @abc.abstractmethod
33
+ def __repr__(self) -> str:
34
+ raise NotImplementedError
35
+
36
+ @abc.abstractmethod
37
+ def __hash__(self) -> int:
38
+ raise NotImplementedError
39
+
40
+ @abc.abstractmethod
41
+ def __eq__(self, other) -> bool:
42
+ raise NotImplementedError
43
+
44
+ @abc.abstractmethod
45
+ def __lt__(self, other) -> bool:
46
+ raise NotImplementedError
47
+
48
+ #
49
+
50
+ @ta.final
51
+ def __ne__(self, other):
52
+ return not (self == other)
53
+
54
+ @ta.final
55
+ def __iter__(self) -> ta.Iterator[T]:
56
+ if self.present:
57
+ yield self.must()
58
+
59
+ @ta.final
60
+ def __bool__(self) -> ta.NoReturn:
61
+ raise TypeError
62
+
63
+ #
64
+
65
+ @ta.final
66
+ def if_present(self, consumer: ta.Callable[[T], None]) -> None:
67
+ if self.present:
68
+ consumer(self.must())
69
+
70
+ @ta.final
71
+ def filter(self, predicate: ta.Callable[[T], bool]) -> 'Maybe[T]':
72
+ if self.present and predicate(self.must()):
73
+ return self
74
+ else:
75
+ return Maybe.empty()
76
+
77
+ @ta.final
78
+ def map(self, mapper: ta.Callable[[T], U]) -> 'Maybe[U]':
79
+ if self.present:
80
+ return Maybe.just(mapper(self.must()))
81
+ else:
82
+ return Maybe.empty()
83
+
84
+ @ta.final
85
+ def flat_map(self, mapper: ta.Callable[[T], 'Maybe[U]']) -> 'Maybe[U]':
86
+ if self.present:
87
+ if not isinstance(v := mapper(self.must()), Maybe):
88
+ raise TypeError(v)
89
+ return v
90
+ else:
91
+ return Maybe.empty()
92
+
93
+ @ta.final
94
+ def or_else(self, other: ta.Union[T, U]) -> ta.Union[T, U]:
95
+ if self.present:
96
+ return self.must()
97
+ else:
98
+ return other
99
+
100
+ @ta.final
101
+ def or_else_get(self, supplier: ta.Callable[[], ta.Union[T, U]]) -> ta.Union[T, U]:
102
+ if self.present:
103
+ return self.must()
104
+ else:
105
+ return supplier()
106
+
107
+ @ta.final
108
+ def or_else_raise(self, exception_supplier: ta.Callable[[], Exception]) -> T:
109
+ if self.present:
110
+ return self.must()
111
+ else:
112
+ raise exception_supplier()
113
+
114
+ #
115
+
116
+ @classmethod
117
+ def of_optional(cls, v: ta.Optional[T]) -> 'Maybe[T]':
118
+ if v is not None:
119
+ return cls.just(v)
120
+ else:
121
+ return cls.empty()
122
+
22
123
  @classmethod
23
124
  def just(cls, v: T) -> 'Maybe[T]':
24
- return tuple.__new__(_Maybe, (v,)) # noqa
125
+ return _JustMaybe(v)
25
126
 
26
127
  _empty: ta.ClassVar['Maybe']
27
128
 
@@ -30,33 +131,78 @@ class Maybe(ta.Generic[T]):
30
131
  return Maybe._empty
31
132
 
32
133
 
33
- class _Maybe(Maybe[T], tuple):
34
- __slots__ = ()
134
+ ##
135
+
136
+
137
+ class _Maybe(Maybe[T], abc.ABC):
138
+ def __lt__(self, other):
139
+ if not isinstance(other, _Maybe):
140
+ return NotImplemented
141
+ sp = self.present
142
+ op = other.present
143
+ if self.present and other.present:
144
+ return self.must() < other.must()
145
+ else:
146
+ return op and not sp
35
147
 
36
- def __init_subclass__(cls, **kwargs):
37
- raise TypeError
148
+
149
+ class _JustMaybe(_Maybe[T]):
150
+ __slots__ = ('_v', '_hash')
151
+
152
+ def __init__(self, v: T) -> None:
153
+ super().__init__()
154
+
155
+ self._v = v
38
156
 
39
157
  @property
40
158
  def present(self) -> bool:
41
- return bool(self)
159
+ return True
42
160
 
43
161
  def must(self) -> T:
44
- if not self:
45
- raise ValueError
46
- return self[0]
162
+ return self._v
47
163
 
164
+ #
48
165
 
49
- Maybe._empty = tuple.__new__(_Maybe, ()) # noqa
166
+ def __repr__(self) -> str:
167
+ return f'just({self._v!r})'
50
168
 
169
+ _hash: int
51
170
 
52
- ##
171
+ def __hash__(self) -> int:
172
+ try:
173
+ return self._hash
174
+ except AttributeError:
175
+ pass
176
+ h = self._hash = hash((_JustMaybe, self._v))
177
+ return h
178
+
179
+ def __eq__(self, other):
180
+ return (
181
+ self.__class__ is other.__class__ and
182
+ self._v == other._v # noqa
183
+ )
184
+
185
+
186
+ class _EmptyMaybe(_Maybe[T]):
187
+ __slots__ = ()
188
+
189
+ @property
190
+ def present(self) -> bool:
191
+ return False
192
+
193
+ def must(self) -> T:
194
+ raise Maybe.ValueNotPresentError
195
+
196
+ #
197
+
198
+ def __repr__(self) -> str:
199
+ return 'empty()'
53
200
 
201
+ def __hash__(self) -> int:
202
+ return hash(_EmptyMaybe)
54
203
 
55
- @functools.singledispatch
56
- def as_maybe(obj: ta.Any) -> Maybe:
57
- raise TypeError(obj)
204
+ def __eq__(self, other):
205
+ return self.__class__ is other.__class__
58
206
 
59
207
 
60
- @as_maybe.register
61
- def _(obj: Maybe) -> Maybe:
62
- return obj
208
+ Maybe._empty = _EmptyMaybe() # noqa
omlish/logs/callers.py CHANGED
@@ -7,6 +7,9 @@ import types
7
7
  import typing as ta
8
8
 
9
9
 
10
+ ##
11
+
12
+
10
13
  class LoggingCaller(ta.NamedTuple):
11
14
  filename: str
12
15
  lineno: int
omlish/logs/color.py CHANGED
@@ -6,6 +6,9 @@ from ..term import codes as tc
6
6
  from .standard import StandardLogFormatter
7
7
 
8
8
 
9
+ ##
10
+
11
+
9
12
  class ColorLogFormatter(StandardLogFormatter):
10
13
  LEVEL_COLORS: ta.Mapping[int, tc.SGRs.FG] = {
11
14
  logging.WARNING: tc.SGRs.FG.BRIGHT_YELLOW,
omlish/logs/filters.py CHANGED
@@ -4,7 +4,14 @@ import logging
4
4
  import threading
5
5
 
6
6
 
7
+ ##
8
+
9
+
7
10
  class TidLogFilter(logging.Filter):
8
11
  def filter(self, record):
9
- record.tid = threading.get_native_id()
12
+ # FIXME: handle better - missing from wasm and cosmos
13
+ if hasattr(threading, 'get_native_id'):
14
+ record.tid = threading.get_native_id()
15
+ else:
16
+ record.tid = '?'
10
17
  return True
omlish/logs/handlers.py CHANGED
@@ -4,6 +4,9 @@ import logging
4
4
  import typing as ta
5
5
 
6
6
 
7
+ ##
8
+
9
+
7
10
  class ListHandler(logging.Handler):
8
11
  def __init__(self) -> None:
9
12
  super().__init__()
omlish/logs/json.py CHANGED
@@ -10,6 +10,9 @@ import typing as ta
10
10
  from ..lite.json import json_dumps_compact
11
11
 
12
12
 
13
+ ##
14
+
15
+
13
16
  class JsonLogFormatter(logging.Formatter):
14
17
  KEYS: ta.Mapping[str, bool] = {
15
18
  'name': False,
omlish/logs/noisy.py CHANGED
@@ -1,6 +1,9 @@
1
1
  import logging
2
2
 
3
3
 
4
+ ##
5
+
6
+
4
7
  NOISY_LOGGERS: set[str] = {
5
8
  'boto3.resources.action',
6
9
  'datadog.dogstatsd',
omlish/logs/proxy.py CHANGED
@@ -3,6 +3,9 @@
3
3
  import logging
4
4
 
5
5
 
6
+ ##
7
+
8
+
6
9
  class ProxyLogFilterer(logging.Filterer):
7
10
  def __init__(self, underlying: logging.Filterer) -> None: # noqa
8
11
  self._underlying = underlying
omlish/logs/utils.py CHANGED
@@ -2,6 +2,9 @@ import functools
2
2
  import logging
3
3
 
4
4
 
5
+ ##
6
+
7
+
5
8
  _log = logging.getLogger(__name__)
6
9
 
7
10
 
omlish/sockets/ports.py CHANGED
@@ -16,21 +16,38 @@ DEFAULT_AVAILABLE_PORT_HOST: str = '127.0.0.1'
16
16
 
17
17
 
18
18
  @contextlib.contextmanager
19
- def get_available_port_context(host: ta.Optional[str] = None) -> ta.Iterator[int]:
19
+ def get_available_port_context(
20
+ host: ta.Optional[str] = None,
21
+ family: int = socket.AF_INET,
22
+ type: int = socket.SOCK_STREAM, # noqa
23
+ *,
24
+ listen: ta.Union[bool, int, None] = False,
25
+ ) -> ta.Iterator[int]:
20
26
  if host is None:
21
27
  host = DEFAULT_AVAILABLE_PORT_HOST
22
28
 
23
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
29
+ if listen is not None:
30
+ if listen is True:
31
+ listen = 1
32
+ elif listen is False:
33
+ listen = None
34
+ else:
35
+ listen = check.isinstance(listen, int)
36
+
37
+ with socket.socket(family, type) as sock:
24
38
  sock.bind((host, 0))
25
- sock.listen(1)
39
+
40
+ if listen is not None:
41
+ sock.listen(listen)
42
+
26
43
  port = sock.getsockname()[1]
44
+
27
45
  yield port
28
46
 
29
47
 
30
- def get_available_port(host: ta.Optional[str] = None) -> int:
31
- with get_available_port_context(host) as port:
32
- pass
33
- return port
48
+ def get_available_port(*args: ta.Any, **kwargs: ta.Any) -> int:
49
+ with get_available_port_context(*args, **kwargs) as port:
50
+ return port
34
51
 
35
52
 
36
53
  def get_available_ports(
@@ -4,29 +4,87 @@ TODO:
4
4
  - dynamic registration
5
5
  - dynamic switching (skip docker if not running, skip online if not online, ...)
6
6
  """
7
+ import dataclasses as dc
7
8
  import typing as ta
8
9
 
9
10
  import pytest
10
11
 
11
12
  from .... import check
12
13
  from .... import collections as col
13
- from .... import lang
14
14
  from ....docker import all as docker
15
15
  from ._registry import register
16
16
 
17
17
 
18
- Configable = pytest.FixtureRequest | pytest.Config
18
+ Configable: ta.TypeAlias = pytest.FixtureRequest | pytest.Config
19
19
 
20
20
 
21
- SWITCHES: ta.Mapping[str, bool | ta.Callable[[], bool]] = {
22
- 'docker': docker.has_cli,
23
- 'docker-guest': docker.is_likely_in_docker,
24
- 'online': True,
25
- 'integration': True,
26
- 'slow': False,
27
- }
21
+ ##
22
+
23
+
24
+ @dc.dataclass(frozen=True, eq=False)
25
+ class Switch:
26
+ name: str
27
+ _default_enabled: bool | ta.Callable[[], bool]
28
+
29
+ _: dc.KW_ONLY
30
+
31
+ add_marks: ta.Sequence[ta.Any] | None = None
32
+
33
+ def default_enabled(self) -> bool:
34
+ if isinstance(e := self._default_enabled, bool):
35
+ return e
36
+ elif callable(e):
37
+ return check.isinstance(e(), bool)
38
+ else:
39
+ raise TypeError(e)
40
+
41
+ @property
42
+ def attr(self) -> str:
43
+ return self.name.replace('-', '_')
44
+
45
+
46
+ SWITCHES: ta.Sequence[Switch] = [
47
+ Switch(
48
+ 'name',
49
+ docker.has_cli,
50
+ ),
51
+
52
+ Switch(
53
+ 'docker-guest',
54
+ docker.is_likely_in_docker,
55
+ ),
56
+
57
+ Switch(
58
+ 'online',
59
+ True,
60
+ ),
28
61
 
29
- SWITCH_ATTRS = {k.replace('-', '_'): k for k in SWITCHES}
62
+ Switch(
63
+ 'integration',
64
+ True,
65
+ ),
66
+
67
+ Switch(
68
+ 'high-mem',
69
+ True,
70
+ add_marks=[
71
+ # https://pytest-xdist.readthedocs.io/en/latest/distribution.html
72
+ pytest.mark.xdist_group('high-mem'),
73
+ ],
74
+ ),
75
+
76
+ Switch(
77
+ 'slow',
78
+ False,
79
+ ),
80
+ ]
81
+
82
+
83
+ SWITCHES_BY_NAME: ta.Mapping[str, Switch] = col.make_map_by(lambda sw: sw.name, SWITCHES, strict=True)
84
+ SWITCHES_BY_ATTR: ta.Mapping[str, Switch] = col.make_map_by(lambda sw: sw.attr, SWITCHES, strict=True)
85
+
86
+
87
+ ##
30
88
 
31
89
 
32
90
  SwitchState: ta.TypeAlias = bool | ta.Literal['only']
@@ -49,7 +107,7 @@ def _get_obj_config(obj: Configable) -> pytest.Config:
49
107
 
50
108
  def is_disabled(obj: Configable | None, name: str) -> bool:
51
109
  check.isinstance(name, str)
52
- check.in_(name, SWITCHES)
110
+ check.in_(name, SWITCHES_BY_NAME)
53
111
  return obj is not None and _get_obj_config(obj).getoption(f'--no-{name}')
54
112
 
55
113
 
@@ -58,17 +116,17 @@ def skip_if_disabled(obj: Configable | None, name: str) -> None:
58
116
  pytest.skip(f'{name} disabled')
59
117
 
60
118
 
61
- def get_specified_switches(obj: Configable) -> ta.Mapping[str, SwitchState]:
62
- ret: dict[str, SwitchState] = {}
119
+ def get_specified_switches(obj: Configable) -> dict[Switch, SwitchState]:
120
+ ret: dict[Switch, SwitchState] = {}
63
121
  for sw in SWITCHES:
64
122
  sts = {
65
123
  st
66
124
  for st, pfx in SWITCH_STATE_OPT_PREFIXES.items()
67
- if _get_obj_config(obj).getoption(pfx + sw)
125
+ if _get_obj_config(obj).getoption(pfx + sw.name)
68
126
  }
69
127
  if sts:
70
128
  if len(sts) > 1:
71
- raise Exception(f'Multiple switches specified for {sw}')
129
+ raise Exception(f'Multiple switches specified for {sw.name}')
72
130
  ret[sw] = check.single(sts)
73
131
  return ret
74
132
 
@@ -76,49 +134,50 @@ def get_specified_switches(obj: Configable) -> ta.Mapping[str, SwitchState]:
76
134
  @register
77
135
  class SwitchesPlugin:
78
136
  def pytest_configure(self, config):
79
- for sw in SWITCH_ATTRS:
80
- config.addinivalue_line('markers', f'{sw}: mark test as {sw}')
81
- config.addinivalue_line('markers', f'not_{sw}: mark test as not {sw}')
137
+ for sw in SWITCHES:
138
+ config.addinivalue_line('markers', f'{sw.attr}: mark test as {sw.attr}')
139
+ config.addinivalue_line('markers', f'not_{sw.attr}: mark test as not {sw.attr}')
82
140
 
83
141
  def pytest_addoption(self, parser):
84
142
  for sw in SWITCHES:
85
- parser.addoption(f'--no-{sw}', action='store_true', default=False, help=f'disable {sw} tests')
86
- parser.addoption(f'--{sw}', action='store_true', default=False, help=f'enables {sw} tests')
87
- parser.addoption(f'--only-{sw}', action='store_true', default=False, help=f'enables only {sw} tests')
88
-
89
- @lang.cached_function
90
- def get_switches(self) -> ta.Mapping[str, SwitchState]:
91
- return {
92
- k: v() if callable(v) else v
93
- for k, v in SWITCHES.items()
94
- }
143
+ parser.addoption(f'--no-{sw.name}', action='store_true', default=False, help=f'disable {sw.name} tests')
144
+ parser.addoption(f'--{sw.name}', action='store_true', default=False, help=f'enables {sw.name} tests')
145
+ parser.addoption(f'--only-{sw.name}', action='store_true', default=False, help=f'enables only {sw.name} tests') # noqa
95
146
 
96
147
  def pytest_collection_modifyitems(self, config, items):
97
- sts = {
98
- **self.get_switches(),
148
+ switch_states: dict[Switch, SwitchState] = {
149
+ **{
150
+ sw: sw.default_enabled()
151
+ for sw in SWITCHES
152
+ },
99
153
  **get_specified_switches(config),
100
154
  }
101
155
 
102
- stx = col.multi_map(map(reversed, sts.items())) # type: ignore
103
- ts, fs, onlys = (stx.get(k, ()) for k in (True, False, 'only'))
156
+ inv_switch_states: dict[SwitchState, list[Switch]] = col.multi_map((st, sw) for sw, st in switch_states.items())
157
+ true_switches = frozenset(inv_switch_states.get(True, ()))
158
+ false_switches = frozenset(inv_switch_states.get(False, ()))
159
+ only_switches = frozenset(inv_switch_states.get('only', ()))
104
160
 
105
161
  def process(item):
106
- sws = {sw for swa, sw in SWITCH_ATTRS.items() if swa in item.keywords}
107
- nsws = {sw for swa, sw in SWITCH_ATTRS.items() if ('not_' + swa) in item.keywords}
162
+ item_switches = {sw for sw in SWITCHES if sw.attr in item.keywords}
163
+ item_not_switches = {sw for sw in SWITCHES if ('not_' + sw.attr) in item.keywords}
164
+
165
+ for sw in item_switches:
166
+ for mk in sw.add_marks or []:
167
+ item.add_marker(mk)
108
168
 
109
- if onlys:
110
- if not any(sw in onlys for sw in sws):
111
- item.add_marker(pytest.mark.skip(reason=f'skipping switches {sws}'))
169
+ if only_switches:
170
+ if not any(sw in only_switches for sw in item_switches):
171
+ item.add_marker(pytest.mark.skip(reason=f'skipping switches {item_switches}'))
112
172
  return
113
173
 
114
- else:
115
- for sw in sws:
116
- if sw in fs:
117
- item.add_marker(pytest.mark.skip(reason=f'skipping switches {sw}'))
174
+ for sw in item_switches:
175
+ if sw in false_switches:
176
+ item.add_marker(pytest.mark.skip(reason=f'skipping switches {sw}'))
118
177
 
119
- for nsw in nsws:
120
- if nsw in ts:
121
- item.add_marker(pytest.mark.skip(reason=f'skipping switches {nsw}'))
178
+ for sw in item_not_switches:
179
+ if sw in true_switches:
180
+ item.add_marker(pytest.mark.skip(reason=f'skipping switches {sw}'))
122
181
 
123
182
  for item in items:
124
183
  process(item)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omlish
3
- Version: 0.0.0.dev315
3
+ Version: 0.0.0.dev317
4
4
  Summary: omlish
5
5
  Author: wrmsr
6
6
  License: BSD-3-Clause