omdev 0.0.0.dev180__py3-none-any.whl → 0.0.0.dev182__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.
omdev/interp/inject.py CHANGED
@@ -11,7 +11,7 @@ from .providers.inject import bind_interp_providers
11
11
  from .providers.running import RunningInterpProvider
12
12
  from .providers.system import SystemInterpProvider
13
13
  from .pyenv.inject import bind_interp_pyenv
14
- from .pyenv.pyenv import PyenvInterpProvider
14
+ from .pyenv.provider import PyenvInterpProvider
15
15
  from .resolvers import InterpResolver
16
16
  from .resolvers import InterpResolverProviders
17
17
  from .uv.inject import bind_interp_uv
omdev/interp/inspect.py CHANGED
@@ -6,7 +6,6 @@ import sys
6
6
  import typing as ta
7
7
 
8
8
  from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
9
- from omlish.lite.logs import log
10
9
 
11
10
  from ..packaging.versions import Version
12
11
  from .types import InterpOpts
@@ -43,9 +42,15 @@ class InterpInspection:
43
42
 
44
43
 
45
44
  class InterpInspector:
46
- def __init__(self) -> None:
45
+ def __init__(
46
+ self,
47
+ *,
48
+ log: ta.Optional[logging.Logger] = None,
49
+ ) -> None:
47
50
  super().__init__()
48
51
 
52
+ self._log = log
53
+
49
54
  self._cache: ta.Dict[str, ta.Optional[InterpInspection]] = {}
50
55
 
51
56
  _RAW_INSPECTION_CODE = """
@@ -94,8 +99,8 @@ class InterpInspector:
94
99
  try:
95
100
  ret = await self._inspect(exe)
96
101
  except Exception as e: # noqa
97
- if log.isEnabledFor(logging.DEBUG):
98
- log.exception('Failed to inspect interp: %s', exe)
102
+ if self._log is not None and self._log.isEnabledFor(logging.DEBUG):
103
+ self._log.exception('Failed to inspect interp: %s', exe)
99
104
  ret = None
100
105
  self._cache[exe] = ret
101
106
  return ret
@@ -5,13 +5,13 @@ TODO:
5
5
  - check if path py's are venvs: sys.prefix != sys.base_prefix
6
6
  """
7
7
  import dataclasses as dc
8
+ import logging
8
9
  import os
9
10
  import re
10
11
  import typing as ta
11
12
 
12
13
  from omlish.lite.cached import cached_nullary
13
14
  from omlish.lite.check import check
14
- from omlish.lite.logs import log
15
15
 
16
16
  from ...packaging.versions import InvalidVersion
17
17
  from ..inspect import InterpInspector
@@ -37,12 +37,14 @@ class SystemInterpProvider(InterpProvider):
37
37
  options: Options = Options(),
38
38
  *,
39
39
  inspector: ta.Optional[InterpInspector] = None,
40
+ log: ta.Optional[logging.Logger] = None,
40
41
  ) -> None:
41
42
  super().__init__()
42
43
 
43
44
  self._options = options
44
45
 
45
46
  self._inspector = inspector
47
+ self._log = log
46
48
 
47
49
  #
48
50
 
@@ -116,7 +118,8 @@ class SystemInterpProvider(InterpProvider):
116
118
  lst = []
117
119
  for e in self.exes():
118
120
  if (ev := await self.get_exe_version(e)) is None:
119
- log.debug('Invalid system version: %s', e)
121
+ if self._log is not None:
122
+ self._log.debug('Invalid system version: %s', e)
120
123
  continue
121
124
  lst.append((e, ev))
122
125
  return lst
@@ -6,8 +6,8 @@ from omlish.lite.inject import InjectorBindings
6
6
  from omlish.lite.inject import inj
7
7
 
8
8
  from ..providers.base import InterpProvider
9
+ from .provider import PyenvInterpProvider
9
10
  from .pyenv import Pyenv
10
- from .pyenv import PyenvInterpProvider
11
11
 
12
12
 
13
13
  def bind_interp_pyenv() -> InjectorBindings:
@@ -0,0 +1,251 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import abc
3
+ import dataclasses as dc
4
+ import itertools
5
+ import os.path
6
+ import shutil
7
+ import sys
8
+ import typing as ta
9
+
10
+ from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
11
+ from omlish.lite.cached import async_cached_nullary
12
+ from omlish.lite.cached import cached_nullary
13
+ from omlish.lite.check import check
14
+
15
+ from ..types import InterpOpts
16
+ from .pyenv import Pyenv
17
+
18
+
19
+ ##
20
+
21
+
22
+ @dc.dataclass(frozen=True)
23
+ class PyenvInstallOpts:
24
+ opts: ta.Sequence[str] = ()
25
+ conf_opts: ta.Sequence[str] = ()
26
+ cflags: ta.Sequence[str] = ()
27
+ ldflags: ta.Sequence[str] = ()
28
+ env: ta.Mapping[str, str] = dc.field(default_factory=dict)
29
+
30
+ def merge(self, *others: 'PyenvInstallOpts') -> 'PyenvInstallOpts':
31
+ return PyenvInstallOpts(
32
+ opts=list(itertools.chain.from_iterable(o.opts for o in [self, *others])),
33
+ conf_opts=list(itertools.chain.from_iterable(o.conf_opts for o in [self, *others])),
34
+ cflags=list(itertools.chain.from_iterable(o.cflags for o in [self, *others])),
35
+ ldflags=list(itertools.chain.from_iterable(o.ldflags for o in [self, *others])),
36
+ env=dict(itertools.chain.from_iterable(o.env.items() for o in [self, *others])),
37
+ )
38
+
39
+
40
+ # TODO: https://github.com/pyenv/pyenv/blob/master/plugins/python-build/README.md#building-for-maximum-performance
41
+ DEFAULT_PYENV_INSTALL_OPTS = PyenvInstallOpts(
42
+ opts=[
43
+ '-s',
44
+ '-v',
45
+ '-k',
46
+ ],
47
+ conf_opts=[
48
+ # FIXME: breaks on mac for older py's
49
+ '--enable-loadable-sqlite-extensions',
50
+
51
+ # '--enable-shared',
52
+
53
+ '--enable-optimizations',
54
+ '--with-lto',
55
+
56
+ # '--enable-profiling', # ?
57
+
58
+ # '--enable-ipv6', # ?
59
+ ],
60
+ cflags=[
61
+ # '-march=native',
62
+ # '-mtune=native',
63
+ ],
64
+ )
65
+
66
+ DEBUG_PYENV_INSTALL_OPTS = PyenvInstallOpts(opts=['-g'])
67
+
68
+ THREADED_PYENV_INSTALL_OPTS = PyenvInstallOpts(conf_opts=['--disable-gil'])
69
+
70
+
71
+ #
72
+
73
+
74
+ class PyenvInstallOptsProvider(abc.ABC):
75
+ @abc.abstractmethod
76
+ def opts(self) -> ta.Awaitable[PyenvInstallOpts]:
77
+ raise NotImplementedError
78
+
79
+
80
+ class LinuxPyenvInstallOpts(PyenvInstallOptsProvider):
81
+ async def opts(self) -> PyenvInstallOpts:
82
+ return PyenvInstallOpts()
83
+
84
+
85
+ class DarwinPyenvInstallOpts(PyenvInstallOptsProvider):
86
+ @cached_nullary
87
+ def framework_opts(self) -> PyenvInstallOpts:
88
+ return PyenvInstallOpts(conf_opts=['--enable-framework'])
89
+
90
+ @cached_nullary
91
+ def has_brew(self) -> bool:
92
+ return shutil.which('brew') is not None
93
+
94
+ BREW_DEPS: ta.Sequence[str] = [
95
+ 'openssl',
96
+ 'readline',
97
+ 'sqlite3',
98
+ 'zlib',
99
+ ]
100
+
101
+ @async_cached_nullary
102
+ async def brew_deps_opts(self) -> PyenvInstallOpts:
103
+ cflags = []
104
+ ldflags = []
105
+ for dep in self.BREW_DEPS:
106
+ dep_prefix = await asyncio_subprocesses.check_output_str('brew', '--prefix', dep)
107
+ cflags.append(f'-I{dep_prefix}/include')
108
+ ldflags.append(f'-L{dep_prefix}/lib')
109
+ return PyenvInstallOpts(
110
+ cflags=cflags,
111
+ ldflags=ldflags,
112
+ )
113
+
114
+ @async_cached_nullary
115
+ async def brew_tcl_opts(self) -> PyenvInstallOpts:
116
+ if await asyncio_subprocesses.try_output('brew', '--prefix', 'tcl-tk') is None:
117
+ return PyenvInstallOpts()
118
+
119
+ tcl_tk_prefix = await asyncio_subprocesses.check_output_str('brew', '--prefix', 'tcl-tk')
120
+ tcl_tk_ver_str = await asyncio_subprocesses.check_output_str('brew', 'ls', '--versions', 'tcl-tk')
121
+ tcl_tk_ver = '.'.join(tcl_tk_ver_str.split()[1].split('.')[:2])
122
+
123
+ return PyenvInstallOpts(conf_opts=[
124
+ f"--with-tcltk-includes='-I{tcl_tk_prefix}/include'",
125
+ f"--with-tcltk-libs='-L{tcl_tk_prefix}/lib -ltcl{tcl_tk_ver} -ltk{tcl_tk_ver}'",
126
+ ])
127
+
128
+ # @cached_nullary
129
+ # def brew_ssl_opts(self) -> PyenvInstallOpts:
130
+ # pkg_config_path = subprocess_check_output_str('brew', '--prefix', 'openssl')
131
+ # if 'PKG_CONFIG_PATH' in os.environ:
132
+ # pkg_config_path += ':' + os.environ['PKG_CONFIG_PATH']
133
+ # return PyenvInstallOpts(env={'PKG_CONFIG_PATH': pkg_config_path})
134
+
135
+ async def opts(self) -> PyenvInstallOpts:
136
+ return PyenvInstallOpts().merge(
137
+ self.framework_opts(),
138
+ await self.brew_deps_opts(),
139
+ await self.brew_tcl_opts(),
140
+ # self.brew_ssl_opts(),
141
+ )
142
+
143
+
144
+ PLATFORM_PYENV_INSTALL_OPTS: ta.Dict[str, PyenvInstallOptsProvider] = {
145
+ 'darwin': DarwinPyenvInstallOpts(),
146
+ 'linux': LinuxPyenvInstallOpts(),
147
+ }
148
+
149
+
150
+ ##
151
+
152
+
153
+ class PyenvVersionInstaller:
154
+ """
155
+ Messy: can install freethreaded build with a 't' suffixed version str _or_ by THREADED_PYENV_INSTALL_OPTS - need
156
+ latter to build custom interp with ft, need former to use canned / blessed interps. Muh.
157
+ """
158
+
159
+ def __init__(
160
+ self,
161
+ version: str,
162
+ opts: ta.Optional[PyenvInstallOpts] = None,
163
+ interp_opts: InterpOpts = InterpOpts(),
164
+ *,
165
+ pyenv: Pyenv,
166
+
167
+ install_name: ta.Optional[str] = None,
168
+ no_default_opts: bool = False,
169
+ ) -> None:
170
+ super().__init__()
171
+
172
+ self._version = version
173
+ self._given_opts = opts
174
+ self._interp_opts = interp_opts
175
+ self._given_install_name = install_name
176
+
177
+ self._no_default_opts = no_default_opts
178
+ self._pyenv = pyenv
179
+
180
+ @property
181
+ def version(self) -> str:
182
+ return self._version
183
+
184
+ @async_cached_nullary
185
+ async def opts(self) -> PyenvInstallOpts:
186
+ opts = self._given_opts
187
+ if self._no_default_opts:
188
+ if opts is None:
189
+ opts = PyenvInstallOpts()
190
+ else:
191
+ lst = [self._given_opts if self._given_opts is not None else DEFAULT_PYENV_INSTALL_OPTS]
192
+ if self._interp_opts.debug:
193
+ lst.append(DEBUG_PYENV_INSTALL_OPTS)
194
+ if self._interp_opts.threaded:
195
+ lst.append(THREADED_PYENV_INSTALL_OPTS)
196
+ lst.append(await PLATFORM_PYENV_INSTALL_OPTS[sys.platform].opts())
197
+ opts = PyenvInstallOpts().merge(*lst)
198
+ return opts
199
+
200
+ @cached_nullary
201
+ def install_name(self) -> str:
202
+ if self._given_install_name is not None:
203
+ return self._given_install_name
204
+ return self._version + ('-debug' if self._interp_opts.debug else '')
205
+
206
+ @async_cached_nullary
207
+ async def install_dir(self) -> str:
208
+ return str(os.path.join(check.not_none(await self._pyenv.root()), 'versions', self.install_name()))
209
+
210
+ @async_cached_nullary
211
+ async def install(self) -> str:
212
+ opts = await self.opts()
213
+ env = {**os.environ, **opts.env}
214
+ for k, l in [
215
+ ('CFLAGS', opts.cflags),
216
+ ('LDFLAGS', opts.ldflags),
217
+ ('PYTHON_CONFIGURE_OPTS', opts.conf_opts),
218
+ ]:
219
+ v = ' '.join(l)
220
+ if k in os.environ:
221
+ v += ' ' + os.environ[k]
222
+ env[k] = v
223
+
224
+ conf_args = [
225
+ *opts.opts,
226
+ self._version,
227
+ ]
228
+
229
+ full_args: ta.List[str]
230
+ if self._given_install_name is not None:
231
+ full_args = [
232
+ os.path.join(check.not_none(await self._pyenv.root()), 'plugins', 'python-build', 'bin', 'python-build'), # noqa
233
+ *conf_args,
234
+ await self.install_dir(),
235
+ ]
236
+ else:
237
+ full_args = [
238
+ await self._pyenv.exe(),
239
+ 'install',
240
+ *conf_args,
241
+ ]
242
+
243
+ await asyncio_subprocesses.check_call(
244
+ *full_args,
245
+ env=env,
246
+ )
247
+
248
+ exe = os.path.join(await self.install_dir(), 'bin', 'python')
249
+ if not os.path.isfile(exe):
250
+ raise RuntimeError(f'Interpreter not found: {exe}')
251
+ return exe
@@ -0,0 +1,144 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import dataclasses as dc
3
+ import logging
4
+ import typing as ta
5
+
6
+ from omlish.lite.check import check
7
+
8
+ from ...packaging.versions import InvalidVersion
9
+ from ...packaging.versions import Version
10
+ from ..inspect import InterpInspector
11
+ from ..providers.base import InterpProvider
12
+ from ..types import Interp
13
+ from ..types import InterpOpts
14
+ from ..types import InterpSpecifier
15
+ from ..types import InterpVersion
16
+ from .install import PyenvVersionInstaller
17
+ from .pyenv import Pyenv
18
+
19
+
20
+ class PyenvInterpProvider(InterpProvider):
21
+ @dc.dataclass(frozen=True)
22
+ class Options:
23
+ inspect: bool = False
24
+
25
+ try_update: bool = False
26
+
27
+ def __init__(
28
+ self,
29
+ options: Options = Options(),
30
+ *,
31
+ pyenv: Pyenv,
32
+ inspector: InterpInspector,
33
+ log: ta.Optional[logging.Logger] = None,
34
+ ) -> None:
35
+ super().__init__()
36
+
37
+ self._options = options
38
+
39
+ self._pyenv = pyenv
40
+ self._inspector = inspector
41
+ self._log = log
42
+
43
+ #
44
+
45
+ @staticmethod
46
+ def guess_version(s: str) -> ta.Optional[InterpVersion]:
47
+ def strip_sfx(s: str, sfx: str) -> ta.Tuple[str, bool]:
48
+ if s.endswith(sfx):
49
+ return s[:-len(sfx)], True
50
+ return s, False
51
+ ok = {}
52
+ s, ok['debug'] = strip_sfx(s, '-debug')
53
+ s, ok['threaded'] = strip_sfx(s, 't')
54
+ try:
55
+ v = Version(s)
56
+ except InvalidVersion:
57
+ return None
58
+ return InterpVersion(v, InterpOpts(**ok))
59
+
60
+ class Installed(ta.NamedTuple):
61
+ name: str
62
+ exe: str
63
+ version: InterpVersion
64
+
65
+ async def _make_installed(self, vn: str, ep: str) -> ta.Optional[Installed]:
66
+ iv: ta.Optional[InterpVersion]
67
+ if self._options.inspect:
68
+ try:
69
+ iv = check.not_none(await self._inspector.inspect(ep)).iv
70
+ except Exception as e: # noqa
71
+ return None
72
+ else:
73
+ iv = self.guess_version(vn)
74
+ if iv is None:
75
+ return None
76
+ return PyenvInterpProvider.Installed(
77
+ name=vn,
78
+ exe=ep,
79
+ version=iv,
80
+ )
81
+
82
+ async def installed(self) -> ta.Sequence[Installed]:
83
+ ret: ta.List[PyenvInterpProvider.Installed] = []
84
+ for vn, ep in await self._pyenv.version_exes():
85
+ if (i := await self._make_installed(vn, ep)) is None:
86
+ if self._log is not None:
87
+ self._log.debug('Invalid pyenv version: %s', vn)
88
+ continue
89
+ ret.append(i)
90
+ return ret
91
+
92
+ #
93
+
94
+ async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
95
+ return [i.version for i in await self.installed()]
96
+
97
+ async def get_installed_version(self, version: InterpVersion) -> Interp:
98
+ for i in await self.installed():
99
+ if i.version == version:
100
+ return Interp(
101
+ exe=i.exe,
102
+ version=i.version,
103
+ )
104
+ raise KeyError(version)
105
+
106
+ #
107
+
108
+ async def _get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
109
+ lst = []
110
+
111
+ for vs in await self._pyenv.installable_versions():
112
+ if (iv := self.guess_version(vs)) is None:
113
+ continue
114
+ if iv.opts.debug:
115
+ raise Exception('Pyenv installable versions not expected to have debug suffix')
116
+ for d in [False, True]:
117
+ lst.append(dc.replace(iv, opts=dc.replace(iv.opts, debug=d)))
118
+
119
+ return lst
120
+
121
+ async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
122
+ lst = await self._get_installable_versions(spec)
123
+
124
+ if self._options.try_update and not any(v in spec for v in lst):
125
+ if self._pyenv.update():
126
+ lst = await self._get_installable_versions(spec)
127
+
128
+ return lst
129
+
130
+ async def install_version(self, version: InterpVersion) -> Interp:
131
+ inst_version = str(version.version)
132
+ inst_opts = version.opts
133
+ if inst_opts.threaded:
134
+ inst_version += 't'
135
+ inst_opts = dc.replace(inst_opts, threaded=False)
136
+
137
+ installer = PyenvVersionInstaller(
138
+ inst_version,
139
+ interp_opts=inst_opts,
140
+ pyenv=self._pyenv,
141
+ )
142
+
143
+ exe = await installer.install()
144
+ return Interp(exe, version)