omdev 0.0.0.dev180__py3-none-any.whl → 0.0.0.dev182__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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)