omlish 0.0.0.dev470__py3-none-any.whl → 0.0.0.dev471__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/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev470'
2
- __revision__ = '967906fd30a40d061ede9f7aeda9dad0ec608508'
1
+ __version__ = '0.0.0.dev471'
2
+ __revision__ = '2a1fe39e9894efdf0e28923ddd0067f441d9d964'
3
3
 
4
4
 
5
5
  #
omlish/inject/__init__.py CHANGED
@@ -57,8 +57,12 @@ with _lang.auto_proxy_init(globals()):
57
57
  Element,
58
58
  Elements,
59
59
  Elemental,
60
+
60
61
  as_elements,
61
62
  iter_elements,
63
+
64
+ CollectedElements,
65
+ collect_elements,
62
66
  )
63
67
 
64
68
  from .errors import ( # noqa
omlish/inject/elements.py CHANGED
@@ -6,6 +6,12 @@ from .. import lang
6
6
  from .impl.origins import HasOriginsImpl
7
7
 
8
8
 
9
+ if ta.TYPE_CHECKING:
10
+ from .impl import elements as _elements
11
+ else:
12
+ _elements = lang.proxy_import('.impl.elements', __package__)
13
+
14
+
9
15
  ##
10
16
 
11
17
 
@@ -74,3 +80,14 @@ def iter_elements(*args: Elemental) -> ta.Iterator[Element]:
74
80
  yield from a
75
81
  else:
76
82
  raise TypeError(a)
83
+
84
+
85
+ ##
86
+
87
+
88
+ class CollectedElements(lang.PackageSealed, lang.Abstract):
89
+ pass
90
+
91
+
92
+ def collect_elements(es: Elements | CollectedElements) -> CollectedElements:
93
+ return _elements.collect_elements(es)
@@ -27,6 +27,7 @@ from ... import collections as col
27
27
  from ... import lang
28
28
  from ..bindings import Binding
29
29
  from ..eagers import Eager
30
+ from ..elements import CollectedElements
30
31
  from ..elements import Element
31
32
  from ..elements import Elements
32
33
  from ..errors import ConflictingKeyError
@@ -47,7 +48,7 @@ from .multis import make_multi_provider_impl
47
48
  from .origins import Origins
48
49
  from .origins import set_origins
49
50
  from .providers import ProviderImpl
50
- from .providers2 import make_provider_impl
51
+ from .providersmap import make_provider_impl
51
52
  from .scopes import make_scope_impl
52
53
 
53
54
 
@@ -80,7 +81,7 @@ _NON_BINDING_ELEMENT_TYPES: tuple[type[Element], ...] = (
80
81
  )
81
82
 
82
83
 
83
- class ElementCollection(lang.Final):
84
+ class ElementCollection(CollectedElements, lang.Final):
84
85
  def __init__(self, es: Elements) -> None:
85
86
  super().__init__()
86
87
 
@@ -208,6 +209,10 @@ class ElementCollection(lang.Final):
208
209
 
209
210
  ##
210
211
 
212
+ @lang.cached_function
213
+ def scope_binding_scopes(self) -> ta.Sequence[Scope]:
214
+ return [sb.scope for sb in self.elements_of_type(ScopeBinding)]
215
+
211
216
  @lang.cached_function
212
217
  def eager_keys_by_scope(self) -> ta.Mapping[Scope, ta.Sequence[Key]]:
213
218
  bim = self.binding_impl_map()
@@ -216,3 +221,13 @@ class ElementCollection(lang.Final):
216
221
  bi = bim[e.key]
217
222
  ret.setdefault(bi.scope, []).append(e.key)
218
223
  return ret
224
+
225
+
226
+ ##
227
+
228
+
229
+ def collect_elements(es: Elements | CollectedElements) -> ElementCollection:
230
+ if isinstance(es, CollectedElements):
231
+ return check.isinstance(es, ElementCollection)
232
+ else:
233
+ return ElementCollection(es)
@@ -2,14 +2,12 @@
2
2
  TODO:
3
3
  - ** can currently bind in a child/private scope shadowing an external parent binding **
4
4
  - better source tracking
5
- - cache/export ElementCollections lol
6
5
  - scope bindings, auto in root
7
6
  - injector-internal / blacklisted bindings (Injector itself, default scopes) without rebuilding ElementCollection
8
7
  - config - proxies, impl select, etc
9
8
  - config is probably shared with ElementCollection... but not 'bound', must be shared everywhere
10
9
  - InjectorRoot object?
11
10
  - ** eagers in any scope, on scope init/open
12
- - injection listeners
13
11
  - unions - raise on ambiguous - usecase: sql.AsyncEngineLike
14
12
  - multiple live request scopes on single injector - use private injectors?
15
13
  - more listeners - UnboundKeyListener
@@ -24,7 +22,7 @@ import weakref
24
22
  from ... import check
25
23
  from ... import lang
26
24
  from ...logs import all as logs
27
- from ..elements import Elements
25
+ from ..elements import CollectedElements
28
26
  from ..errors import CyclicDependencyError
29
27
  from ..errors import UnboundKeyError
30
28
  from ..injector import AsyncInjector
@@ -33,7 +31,6 @@ from ..keys import Key
33
31
  from ..keys import as_key
34
32
  from ..listeners import ProvisionListener
35
33
  from ..listeners import ProvisionListenerBinding
36
- from ..scopes import ScopeBinding
37
34
  from ..scopes import Singleton
38
35
  from ..scopes import ThreadScope
39
36
  from ..types import Scope
@@ -61,14 +58,12 @@ DEFAULT_SCOPES: list[Scope] = [
61
58
  class AsyncInjectorImpl(AsyncInjector, lang.Final):
62
59
  def __init__(
63
60
  self,
64
- ec: ElementCollection,
61
+ ec: CollectedElements,
65
62
  p: ta.Optional['AsyncInjectorImpl'] = None,
66
63
  *,
67
64
  internal_consts: dict[Key, ta.Any] | None = None,
68
65
  ) -> None:
69
- super().__init__()
70
-
71
- self._ec = check.isinstance(ec, ElementCollection)
66
+ self._ec = (ec := check.isinstance(ec, ElementCollection))
72
67
  self._p: AsyncInjectorImpl | None = check.isinstance(p, (AsyncInjectorImpl, None))
73
68
 
74
69
  self._internal_consts: dict[Key, ta.Any] = {
@@ -77,28 +72,31 @@ class AsyncInjectorImpl(AsyncInjector, lang.Final):
77
72
  }
78
73
 
79
74
  self._bim = ec.binding_impl_map()
75
+
80
76
  self._ekbs = ec.eager_keys_by_scope()
77
+
81
78
  self._pls: tuple[ProvisionListener, ...] = tuple(
82
79
  b.listener # type: ignore[attr-defined]
83
80
  for b in itertools.chain(
84
81
  ec.elements_of_type(ProvisionListenerBinding),
85
- (p._pls if p is not None else ()), # noqa
82
+ p._pls if p is not None else (), # noqa
86
83
  )
87
84
  )
88
85
 
89
- self._cs: weakref.WeakSet[AsyncInjectorImpl] | None = None # noqa
90
86
  self._root: AsyncInjectorImpl = p._root if p is not None else self # noqa
91
87
 
92
- self.__cur_req: AsyncInjectorImpl._Request | None = None
93
-
94
- ss = [
95
- *DEFAULT_SCOPES,
96
- *[sb.scope for sb in ec.elements_of_type(ScopeBinding)],
97
- ]
98
88
  self._scopes: dict[Scope, ScopeImpl] = {
99
- s: make_scope_impl(s) for s in ss
89
+ s: make_scope_impl(s)
90
+ for s in itertools.chain(
91
+ DEFAULT_SCOPES,
92
+ ec.scope_binding_scopes(),
93
+ )
100
94
  }
101
95
 
96
+ _cs: weakref.WeakSet['AsyncInjectorImpl'] | None = None # noqa
97
+
98
+ __cur_req: ta.Optional['AsyncInjectorImpl._Request'] = None
99
+
102
100
  #
103
101
 
104
102
  _has_run_init: bool = False
@@ -259,7 +257,7 @@ class AsyncInjectorImpl(AsyncInjector, lang.Final):
259
257
  return obj(**kws)
260
258
 
261
259
 
262
- async def create_async_injector(es: Elements) -> AsyncInjector:
263
- i = AsyncInjectorImpl(ElementCollection(es))
260
+ async def create_async_injector(ce: CollectedElements) -> AsyncInjector:
261
+ i = AsyncInjectorImpl(ce)
264
262
  await i._init() # noqa
265
263
  return i
@@ -1,13 +1,12 @@
1
1
  import typing as ta
2
2
 
3
3
  from ... import lang
4
- from ..elements import Elements
4
+ from ..elements import CollectedElements
5
5
  from ..injector import AsyncInjector
6
6
  from ..inspect import KwargsTarget
7
7
  from ..keys import Key
8
8
  from ..maysync import MaysyncInjector
9
9
  from ..sync import Injector
10
- from .elements import ElementCollection
11
10
  from .injector import AsyncInjectorImpl
12
11
 
13
12
 
@@ -30,10 +29,10 @@ class MaysyncInjectorImpl(MaysyncInjector, lang.Final):
30
29
  return lang.run_maysync(self._ai.inject(obj))
31
30
 
32
31
 
33
- def create_maysync_injector(es: Elements) -> MaysyncInjector:
32
+ def create_maysync_injector(ce: CollectedElements) -> MaysyncInjector:
34
33
  si = MaysyncInjectorImpl()
35
34
  ai = AsyncInjectorImpl(
36
- ElementCollection(es),
35
+ ce,
37
36
  internal_consts={
38
37
  Key(MaysyncInjector): si,
39
38
  Key(Injector): si,
@@ -1,12 +1,11 @@
1
1
  import typing as ta
2
2
 
3
3
  from ... import lang
4
- from ..elements import Elements
4
+ from ..elements import CollectedElements
5
5
  from ..injector import AsyncInjector
6
6
  from ..inspect import KwargsTarget
7
7
  from ..keys import Key
8
8
  from ..sync import Injector
9
- from .elements import ElementCollection
10
9
  from .injector import AsyncInjectorImpl
11
10
 
12
11
 
@@ -29,10 +28,10 @@ class InjectorImpl(Injector, lang.Final):
29
28
  return lang.sync_await(self._ai.inject(obj))
30
29
 
31
30
 
32
- def create_injector(es: Elements) -> Injector:
31
+ def create_injector(ce: CollectedElements) -> Injector:
33
32
  si = InjectorImpl()
34
33
  ai = AsyncInjectorImpl(
35
- ElementCollection(es),
34
+ ce,
36
35
  internal_consts={
37
36
  Key(Injector): si,
38
37
  },
omlish/inject/injector.py CHANGED
@@ -1,9 +1,12 @@
1
1
  import abc
2
2
  import typing as ta
3
3
 
4
+ from .. import check
4
5
  from .. import lang
6
+ from .elements import CollectedElements
5
7
  from .elements import Elemental
6
8
  from .elements import as_elements
9
+ from .elements import collect_elements
7
10
  from .inspect import KwargsTarget
8
11
  from .keys import Key
9
12
 
@@ -44,5 +47,31 @@ class AsyncInjector(lang.Abstract):
44
47
  return self.provide(target)
45
48
 
46
49
 
47
- def create_async_injector(*args: Elemental) -> ta.Awaitable[AsyncInjector]:
48
- return _injector.create_async_injector(as_elements(*args))
50
+ ##
51
+
52
+
53
+ @ta.final
54
+ class _InjectorCreator(ta.Generic[T]):
55
+ def __init__(self, fac: ta.Callable[[CollectedElements], T]) -> None:
56
+ self._fac = fac
57
+
58
+ @ta.overload
59
+ def __call__(self, es: CollectedElements, /) -> T: ...
60
+
61
+ @ta.overload
62
+ def __call__(self, *es: Elemental) -> T: ...
63
+
64
+ def __call__(self, arg0, *argv):
65
+ ce: CollectedElements
66
+ if isinstance(arg0, CollectedElements):
67
+ check.arg(not argv)
68
+ ce = arg0
69
+ else:
70
+ ce = collect_elements(as_elements(arg0, *argv))
71
+ return self._fac(ce)
72
+
73
+
74
+ ##
75
+
76
+
77
+ create_async_injector = _InjectorCreator[ta.Awaitable[AsyncInjector]](lambda ce: _injector.create_async_injector(ce))
omlish/inject/maysync.py CHANGED
@@ -1,8 +1,7 @@
1
1
  import typing as ta
2
2
 
3
3
  from .. import lang
4
- from .elements import Elemental
5
- from .elements import as_elements
4
+ from .injector import _InjectorCreator
6
5
  from .sync import Injector
7
6
 
8
7
 
@@ -25,5 +24,4 @@ class MaysyncInjector(Injector, lang.Abstract):
25
24
  ##
26
25
 
27
26
 
28
- def create_maysync_injector(*args: Elemental) -> MaysyncInjector:
29
- return _maysync.create_maysync_injector(as_elements(*args))
27
+ create_maysync_injector = _InjectorCreator[MaysyncInjector](lambda ce: _maysync.create_maysync_injector(ce))
omlish/inject/sync.py CHANGED
@@ -2,8 +2,7 @@ import abc
2
2
  import typing as ta
3
3
 
4
4
  from .. import lang
5
- from .elements import Elemental
6
- from .elements import as_elements
5
+ from .injector import _InjectorCreator
7
6
  from .inspect import KwargsTarget
8
7
  from .keys import Key
9
8
 
@@ -44,5 +43,7 @@ class Injector(lang.Abstract):
44
43
  return self.provide(target)
45
44
 
46
45
 
47
- def create_injector(*args: Elemental) -> Injector:
48
- return _sync.create_injector(as_elements(*args))
46
+ ##
47
+
48
+
49
+ create_injector = _InjectorCreator[Injector](lambda ce: _sync.create_injector(ce))
@@ -342,6 +342,8 @@ def proxy_import(
342
342
  spec: str,
343
343
  package: str | None = None,
344
344
  extras: ta.Iterable[str] | None = None,
345
+ *,
346
+ no_cache: bool = False,
345
347
  ) -> types.ModuleType:
346
348
  """'Legacy' proxy import mechanism."""
347
349
 
@@ -352,12 +354,19 @@ def proxy_import(
352
354
 
353
355
  def __getattr__(att): # noqa
354
356
  nonlocal omod
357
+
355
358
  if omod is None:
356
359
  omod = importlib.import_module(spec, package=package)
357
360
  if extras:
358
361
  for x in extras:
359
362
  importlib.import_module(f'{spec}.{x}', package=package)
360
- return getattr(omod, att)
363
+
364
+ v = getattr(omod, att)
365
+
366
+ if not no_cache:
367
+ setattr(lmod, att, v)
368
+
369
+ return v
361
370
 
362
371
  lmod = types.ModuleType(spec)
363
372
  lmod.__getattr__ = __getattr__ # type: ignore[method-assign]
omlish/term/pager.py ADDED
@@ -0,0 +1,235 @@
1
+ # ruff: noqa: S602 S605
2
+ #
3
+ # Copyright 2000-2008 Michael Hudson-Doyle <micahel@gmail.com>
4
+ # Armin Rigo
5
+ #
6
+ # All Rights Reserved
7
+ #
8
+ #
9
+ # Permission to use, copy, modify, and distribute this software and its documentation for any purpose is hereby granted
10
+ # without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and
11
+ # this permission notice appear in supporting documentation.
12
+ #
13
+ # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF
14
+ # MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES
15
+ # OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
16
+ # OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
+ #
18
+ # https://github.com/python/cpython/tree/bcced02604f845b2b71d0a1dd95f95366bd7774d/Lib/_pyrepl
19
+ import io
20
+ import os
21
+ import re
22
+ import subprocess
23
+ import sys
24
+ import tempfile
25
+ import termios
26
+ import tty
27
+ import typing as ta
28
+
29
+ from .. import check
30
+
31
+
32
+ ##
33
+
34
+
35
+ class Pager(ta.Protocol):
36
+ def __call__(self, text: str, title: str = '') -> None:
37
+ ...
38
+
39
+
40
+ def get_pager() -> Pager:
41
+ """Decide what method to use for paging through text."""
42
+
43
+ if not hasattr(sys.stdin, 'isatty'):
44
+ return plain_pager
45
+
46
+ if not hasattr(sys.stdout, 'isatty'):
47
+ return plain_pager
48
+
49
+ if not sys.stdin.isatty() or not sys.stdout.isatty():
50
+ return plain_pager
51
+
52
+ if sys.platform == 'emscripten':
53
+ return plain_pager
54
+
55
+ use_pager = os.environ.get('MANPAGER') or os.environ.get('PAGER')
56
+ if use_pager:
57
+ if sys.platform == 'win32': # pipes completely broken in Windows
58
+ raise OSError
59
+
60
+ elif os.environ.get('TERM') in ('dumb', 'emacs'):
61
+ return lambda text, title='': pipe_pager(plain(text), use_pager, title)
62
+
63
+ else:
64
+ return lambda text, title='': pipe_pager(text, use_pager, title)
65
+
66
+ if os.environ.get('TERM') in ('dumb', 'emacs'):
67
+ return plain_pager
68
+
69
+ if sys.platform == 'win32':
70
+ return lambda text, title='': tempfile_pager(plain(text), 'more <')
71
+
72
+ if hasattr(os, 'system') and os.system('(pager) 2>/dev/null') == 0:
73
+ return lambda text, title='': pipe_pager(text, 'pager', title)
74
+
75
+ if hasattr(os, 'system') and os.system('(less) 2>/dev/null') == 0:
76
+ return lambda text, title='': pipe_pager(text, 'less', title)
77
+
78
+ import tempfile
79
+ (fd, filename) = tempfile.mkstemp()
80
+ os.close(fd)
81
+ try:
82
+ if hasattr(os, 'system') and os.system(f'more "{filename}"') == 0:
83
+ return lambda text, title='': pipe_pager(text, 'more', title)
84
+
85
+ else:
86
+ return tty_pager
87
+
88
+ finally:
89
+ os.unlink(filename)
90
+
91
+
92
+ def escape_stdout(text: str) -> str:
93
+ # Escape non-encodable characters to avoid encoding errors later
94
+ encoding = getattr(sys.stdout, 'encoding', None) or 'utf-8'
95
+ return text.encode(encoding, 'backslashreplace').decode(encoding)
96
+
97
+
98
+ def escape_less(s: str) -> str:
99
+ return re.sub(r'([?:.%\\])', r'\\\1', s)
100
+
101
+
102
+ def plain(text: str) -> str:
103
+ """Remove boldface formatting from text."""
104
+
105
+ return re.sub('.\b', '', text)
106
+
107
+
108
+ def tty_pager(text: str, title: str = '') -> None:
109
+ """Page through text on a text terminal."""
110
+
111
+ lines = plain(escape_stdout(text)).split('\n')
112
+ has_tty = False
113
+ try:
114
+ fd = sys.stdin.fileno()
115
+ old = termios.tcgetattr(fd)
116
+ tty.setcbreak(fd)
117
+ has_tty = True
118
+
119
+ def getchar() -> str:
120
+ return sys.stdin.read(1)
121
+
122
+ except (ImportError, AttributeError, io.UnsupportedOperation):
123
+ def getchar() -> str:
124
+ return sys.stdin.readline()[:-1][:1]
125
+
126
+ try:
127
+ try:
128
+ h = int(os.environ.get('LINES', '0'))
129
+ except ValueError:
130
+ h = 0
131
+ if h <= 1:
132
+ h = 25
133
+ r = inc = h - 1
134
+
135
+ sys.stdout.write('\n'.join(lines[:inc]) + '\n')
136
+ while lines[r:]:
137
+ sys.stdout.write('-- more --')
138
+ sys.stdout.flush()
139
+ c = getchar()
140
+
141
+ if c in ('q', 'Q'):
142
+ sys.stdout.write('\r \r')
143
+ break
144
+
145
+ elif c in ('\r', '\n'):
146
+ sys.stdout.write('\r \r' + lines[r] + '\n')
147
+ r = r + 1
148
+ continue
149
+
150
+ if c in ('b', 'B', '\x1b'):
151
+ r = r - inc - inc
152
+ if r < 0:
153
+ r = 0
154
+
155
+ sys.stdout.write('\n' + '\n'.join(lines[r:r + inc]) + '\n')
156
+ r = r + inc
157
+
158
+ finally:
159
+ if has_tty:
160
+ termios.tcsetattr(fd, termios.TCSAFLUSH, old) # noqa
161
+
162
+
163
+ def plain_pager(text: str, title: str = '') -> None:
164
+ """Simply print unformatted text. This is the ultimate fallback."""
165
+
166
+ sys.stdout.write(plain(escape_stdout(text)))
167
+
168
+
169
+ def pipe_pager(text: str, cmd: str, title: str = '') -> None:
170
+ """Page through text by feeding it to another program."""
171
+
172
+ env = os.environ.copy()
173
+
174
+ if title:
175
+ title += ' '
176
+ esc_title = escape_less(title)
177
+
178
+ prompt_string = (
179
+ f' {esc_title}'
180
+ '?ltline %lt?L/%L.'
181
+ ':byte %bB?s/%s.'
182
+ '.'
183
+ '?e (END):?pB %pB\\%..'
184
+ ' (press h for help or q to quit)'
185
+ )
186
+
187
+ env['LESS'] = f'-RmPm{prompt_string}$PM{prompt_string}$'
188
+
189
+ proc = subprocess.Popen(
190
+ cmd,
191
+ shell=True,
192
+ stdin=subprocess.PIPE,
193
+ errors='backslashreplace',
194
+ env=env,
195
+ )
196
+
197
+ try:
198
+ with check.not_none(proc.stdin) as pipe:
199
+ try:
200
+ pipe.write(text)
201
+
202
+ except KeyboardInterrupt:
203
+ # We've hereby abandoned whatever text hasn't been written, but the pager is still in control of the
204
+ # terminal.
205
+ pass
206
+
207
+ except OSError:
208
+ pass # Ignore broken pipes caused by quitting the pager program.
209
+
210
+ while True:
211
+ try:
212
+ proc.wait()
213
+ break
214
+
215
+ except KeyboardInterrupt:
216
+ # Ignore ctl-c like the pager itself does. Otherwise the pager is left running and the terminal is in raw
217
+ # mode and unusable.
218
+ pass
219
+
220
+
221
+ def tempfile_pager(text: str, cmd: str, title: str = '') -> None:
222
+ """Page through text by invoking a program on a temporary file."""
223
+
224
+ with tempfile.TemporaryDirectory() as tempdir:
225
+ filename = os.path.join(tempdir, 'pydoc.out')
226
+
227
+ with open(
228
+ filename,
229
+ 'w',
230
+ errors='backslashreplace',
231
+ encoding=os.device_encoding(0) if sys.platform == 'win32' else None,
232
+ ) as file:
233
+ file.write(text)
234
+
235
+ os.system(cmd + ' "' + filename + '"')