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.
@@ -10,31 +10,13 @@ TODO:
10
10
  - optionally install / upgrade pyenv itself
11
11
  - new vers dont need these custom mac opts, only run on old vers
12
12
  """
13
- import abc
14
- import dataclasses as dc
15
- import itertools
16
13
  import os.path
17
14
  import shutil
18
- import sys
19
15
  import typing as ta
20
16
 
21
17
  from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
22
18
  from omlish.lite.cached import async_cached_nullary
23
- from omlish.lite.cached import cached_nullary
24
19
  from omlish.lite.check import check
25
- from omlish.lite.logs import log
26
-
27
- from ...packaging.versions import InvalidVersion
28
- from ...packaging.versions import Version
29
- from ..inspect import InterpInspector
30
- from ..providers.base import InterpProvider
31
- from ..types import Interp
32
- from ..types import InterpOpts
33
- from ..types import InterpSpecifier
34
- from ..types import InterpVersion
35
-
36
-
37
- ##
38
20
 
39
21
 
40
22
  class Pyenv:
@@ -102,365 +84,3 @@ class Pyenv:
102
84
  return False
103
85
  await asyncio_subprocesses.check_call('git', 'pull', cwd=root)
104
86
  return True
105
-
106
-
107
- ##
108
-
109
-
110
- @dc.dataclass(frozen=True)
111
- class PyenvInstallOpts:
112
- opts: ta.Sequence[str] = ()
113
- conf_opts: ta.Sequence[str] = ()
114
- cflags: ta.Sequence[str] = ()
115
- ldflags: ta.Sequence[str] = ()
116
- env: ta.Mapping[str, str] = dc.field(default_factory=dict)
117
-
118
- def merge(self, *others: 'PyenvInstallOpts') -> 'PyenvInstallOpts':
119
- return PyenvInstallOpts(
120
- opts=list(itertools.chain.from_iterable(o.opts for o in [self, *others])),
121
- conf_opts=list(itertools.chain.from_iterable(o.conf_opts for o in [self, *others])),
122
- cflags=list(itertools.chain.from_iterable(o.cflags for o in [self, *others])),
123
- ldflags=list(itertools.chain.from_iterable(o.ldflags for o in [self, *others])),
124
- env=dict(itertools.chain.from_iterable(o.env.items() for o in [self, *others])),
125
- )
126
-
127
-
128
- # TODO: https://github.com/pyenv/pyenv/blob/master/plugins/python-build/README.md#building-for-maximum-performance
129
- DEFAULT_PYENV_INSTALL_OPTS = PyenvInstallOpts(
130
- opts=[
131
- '-s',
132
- '-v',
133
- '-k',
134
- ],
135
- conf_opts=[
136
- # FIXME: breaks on mac for older py's
137
- '--enable-loadable-sqlite-extensions',
138
-
139
- # '--enable-shared',
140
-
141
- '--enable-optimizations',
142
- '--with-lto',
143
-
144
- # '--enable-profiling', # ?
145
-
146
- # '--enable-ipv6', # ?
147
- ],
148
- cflags=[
149
- # '-march=native',
150
- # '-mtune=native',
151
- ],
152
- )
153
-
154
- DEBUG_PYENV_INSTALL_OPTS = PyenvInstallOpts(opts=['-g'])
155
-
156
- THREADED_PYENV_INSTALL_OPTS = PyenvInstallOpts(conf_opts=['--disable-gil'])
157
-
158
-
159
- #
160
-
161
-
162
- class PyenvInstallOptsProvider(abc.ABC):
163
- @abc.abstractmethod
164
- def opts(self) -> ta.Awaitable[PyenvInstallOpts]:
165
- raise NotImplementedError
166
-
167
-
168
- class LinuxPyenvInstallOpts(PyenvInstallOptsProvider):
169
- async def opts(self) -> PyenvInstallOpts:
170
- return PyenvInstallOpts()
171
-
172
-
173
- class DarwinPyenvInstallOpts(PyenvInstallOptsProvider):
174
- @cached_nullary
175
- def framework_opts(self) -> PyenvInstallOpts:
176
- return PyenvInstallOpts(conf_opts=['--enable-framework'])
177
-
178
- @cached_nullary
179
- def has_brew(self) -> bool:
180
- return shutil.which('brew') is not None
181
-
182
- BREW_DEPS: ta.Sequence[str] = [
183
- 'openssl',
184
- 'readline',
185
- 'sqlite3',
186
- 'zlib',
187
- ]
188
-
189
- @async_cached_nullary
190
- async def brew_deps_opts(self) -> PyenvInstallOpts:
191
- cflags = []
192
- ldflags = []
193
- for dep in self.BREW_DEPS:
194
- dep_prefix = await asyncio_subprocesses.check_output_str('brew', '--prefix', dep)
195
- cflags.append(f'-I{dep_prefix}/include')
196
- ldflags.append(f'-L{dep_prefix}/lib')
197
- return PyenvInstallOpts(
198
- cflags=cflags,
199
- ldflags=ldflags,
200
- )
201
-
202
- @async_cached_nullary
203
- async def brew_tcl_opts(self) -> PyenvInstallOpts:
204
- if await asyncio_subprocesses.try_output('brew', '--prefix', 'tcl-tk') is None:
205
- return PyenvInstallOpts()
206
-
207
- tcl_tk_prefix = await asyncio_subprocesses.check_output_str('brew', '--prefix', 'tcl-tk')
208
- tcl_tk_ver_str = await asyncio_subprocesses.check_output_str('brew', 'ls', '--versions', 'tcl-tk')
209
- tcl_tk_ver = '.'.join(tcl_tk_ver_str.split()[1].split('.')[:2])
210
-
211
- return PyenvInstallOpts(conf_opts=[
212
- f"--with-tcltk-includes='-I{tcl_tk_prefix}/include'",
213
- f"--with-tcltk-libs='-L{tcl_tk_prefix}/lib -ltcl{tcl_tk_ver} -ltk{tcl_tk_ver}'",
214
- ])
215
-
216
- # @cached_nullary
217
- # def brew_ssl_opts(self) -> PyenvInstallOpts:
218
- # pkg_config_path = subprocess_check_output_str('brew', '--prefix', 'openssl')
219
- # if 'PKG_CONFIG_PATH' in os.environ:
220
- # pkg_config_path += ':' + os.environ['PKG_CONFIG_PATH']
221
- # return PyenvInstallOpts(env={'PKG_CONFIG_PATH': pkg_config_path})
222
-
223
- async def opts(self) -> PyenvInstallOpts:
224
- return PyenvInstallOpts().merge(
225
- self.framework_opts(),
226
- await self.brew_deps_opts(),
227
- await self.brew_tcl_opts(),
228
- # self.brew_ssl_opts(),
229
- )
230
-
231
-
232
- PLATFORM_PYENV_INSTALL_OPTS: ta.Dict[str, PyenvInstallOptsProvider] = {
233
- 'darwin': DarwinPyenvInstallOpts(),
234
- 'linux': LinuxPyenvInstallOpts(),
235
- }
236
-
237
-
238
- ##
239
-
240
-
241
- class PyenvVersionInstaller:
242
- """
243
- Messy: can install freethreaded build with a 't' suffixed version str _or_ by THREADED_PYENV_INSTALL_OPTS - need
244
- latter to build custom interp with ft, need former to use canned / blessed interps. Muh.
245
- """
246
-
247
- def __init__(
248
- self,
249
- version: str,
250
- opts: ta.Optional[PyenvInstallOpts] = None,
251
- interp_opts: InterpOpts = InterpOpts(),
252
- *,
253
- pyenv: Pyenv,
254
-
255
- install_name: ta.Optional[str] = None,
256
- no_default_opts: bool = False,
257
- ) -> None:
258
- super().__init__()
259
-
260
- self._version = version
261
- self._given_opts = opts
262
- self._interp_opts = interp_opts
263
- self._given_install_name = install_name
264
-
265
- self._no_default_opts = no_default_opts
266
- self._pyenv = pyenv
267
-
268
- @property
269
- def version(self) -> str:
270
- return self._version
271
-
272
- @async_cached_nullary
273
- async def opts(self) -> PyenvInstallOpts:
274
- opts = self._given_opts
275
- if self._no_default_opts:
276
- if opts is None:
277
- opts = PyenvInstallOpts()
278
- else:
279
- lst = [self._given_opts if self._given_opts is not None else DEFAULT_PYENV_INSTALL_OPTS]
280
- if self._interp_opts.debug:
281
- lst.append(DEBUG_PYENV_INSTALL_OPTS)
282
- if self._interp_opts.threaded:
283
- lst.append(THREADED_PYENV_INSTALL_OPTS)
284
- lst.append(await PLATFORM_PYENV_INSTALL_OPTS[sys.platform].opts())
285
- opts = PyenvInstallOpts().merge(*lst)
286
- return opts
287
-
288
- @cached_nullary
289
- def install_name(self) -> str:
290
- if self._given_install_name is not None:
291
- return self._given_install_name
292
- return self._version + ('-debug' if self._interp_opts.debug else '')
293
-
294
- @async_cached_nullary
295
- async def install_dir(self) -> str:
296
- return str(os.path.join(check.not_none(await self._pyenv.root()), 'versions', self.install_name()))
297
-
298
- @async_cached_nullary
299
- async def install(self) -> str:
300
- opts = await self.opts()
301
- env = {**os.environ, **opts.env}
302
- for k, l in [
303
- ('CFLAGS', opts.cflags),
304
- ('LDFLAGS', opts.ldflags),
305
- ('PYTHON_CONFIGURE_OPTS', opts.conf_opts),
306
- ]:
307
- v = ' '.join(l)
308
- if k in os.environ:
309
- v += ' ' + os.environ[k]
310
- env[k] = v
311
-
312
- conf_args = [
313
- *opts.opts,
314
- self._version,
315
- ]
316
-
317
- full_args: ta.List[str]
318
- if self._given_install_name is not None:
319
- full_args = [
320
- os.path.join(check.not_none(await self._pyenv.root()), 'plugins', 'python-build', 'bin', 'python-build'), # noqa
321
- *conf_args,
322
- await self.install_dir(),
323
- ]
324
- else:
325
- full_args = [
326
- await self._pyenv.exe(),
327
- 'install',
328
- *conf_args,
329
- ]
330
-
331
- await asyncio_subprocesses.check_call(
332
- *full_args,
333
- env=env,
334
- )
335
-
336
- exe = os.path.join(await self.install_dir(), 'bin', 'python')
337
- if not os.path.isfile(exe):
338
- raise RuntimeError(f'Interpreter not found: {exe}')
339
- return exe
340
-
341
-
342
- ##
343
-
344
-
345
- class PyenvInterpProvider(InterpProvider):
346
- @dc.dataclass(frozen=True)
347
- class Options:
348
- inspect: bool = False
349
-
350
- try_update: bool = False
351
-
352
- def __init__(
353
- self,
354
- options: Options = Options(),
355
- *,
356
- pyenv: Pyenv,
357
- inspector: InterpInspector,
358
- ) -> None:
359
- super().__init__()
360
-
361
- self._options = options
362
-
363
- self._pyenv = pyenv
364
- self._inspector = inspector
365
-
366
- #
367
-
368
- @staticmethod
369
- def guess_version(s: str) -> ta.Optional[InterpVersion]:
370
- def strip_sfx(s: str, sfx: str) -> ta.Tuple[str, bool]:
371
- if s.endswith(sfx):
372
- return s[:-len(sfx)], True
373
- return s, False
374
- ok = {}
375
- s, ok['debug'] = strip_sfx(s, '-debug')
376
- s, ok['threaded'] = strip_sfx(s, 't')
377
- try:
378
- v = Version(s)
379
- except InvalidVersion:
380
- return None
381
- return InterpVersion(v, InterpOpts(**ok))
382
-
383
- class Installed(ta.NamedTuple):
384
- name: str
385
- exe: str
386
- version: InterpVersion
387
-
388
- async def _make_installed(self, vn: str, ep: str) -> ta.Optional[Installed]:
389
- iv: ta.Optional[InterpVersion]
390
- if self._options.inspect:
391
- try:
392
- iv = check.not_none(await self._inspector.inspect(ep)).iv
393
- except Exception as e: # noqa
394
- return None
395
- else:
396
- iv = self.guess_version(vn)
397
- if iv is None:
398
- return None
399
- return PyenvInterpProvider.Installed(
400
- name=vn,
401
- exe=ep,
402
- version=iv,
403
- )
404
-
405
- async def installed(self) -> ta.Sequence[Installed]:
406
- ret: ta.List[PyenvInterpProvider.Installed] = []
407
- for vn, ep in await self._pyenv.version_exes():
408
- if (i := await self._make_installed(vn, ep)) is None:
409
- log.debug('Invalid pyenv version: %s', vn)
410
- continue
411
- ret.append(i)
412
- return ret
413
-
414
- #
415
-
416
- async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
417
- return [i.version for i in await self.installed()]
418
-
419
- async def get_installed_version(self, version: InterpVersion) -> Interp:
420
- for i in await self.installed():
421
- if i.version == version:
422
- return Interp(
423
- exe=i.exe,
424
- version=i.version,
425
- )
426
- raise KeyError(version)
427
-
428
- #
429
-
430
- async def _get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
431
- lst = []
432
-
433
- for vs in await self._pyenv.installable_versions():
434
- if (iv := self.guess_version(vs)) is None:
435
- continue
436
- if iv.opts.debug:
437
- raise Exception('Pyenv installable versions not expected to have debug suffix')
438
- for d in [False, True]:
439
- lst.append(dc.replace(iv, opts=dc.replace(iv.opts, debug=d)))
440
-
441
- return lst
442
-
443
- async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
444
- lst = await self._get_installable_versions(spec)
445
-
446
- if self._options.try_update and not any(v in spec for v in lst):
447
- if self._pyenv.update():
448
- lst = await self._get_installable_versions(spec)
449
-
450
- return lst
451
-
452
- async def install_version(self, version: InterpVersion) -> Interp:
453
- inst_version = str(version.version)
454
- inst_opts = version.opts
455
- if inst_opts.threaded:
456
- inst_version += 't'
457
- inst_opts = dc.replace(inst_opts, threaded=False)
458
-
459
- installer = PyenvVersionInstaller(
460
- inst_version,
461
- interp_opts=inst_opts,
462
- pyenv=self._pyenv,
463
- )
464
-
465
- exe = await installer.install()
466
- return Interp(exe, version)
omdev/interp/venvs.py ADDED
@@ -0,0 +1,114 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import dataclasses as dc
3
+ import logging
4
+ import os.path
5
+ import typing as ta
6
+
7
+ from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
8
+ from omlish.lite.cached import async_cached_nullary
9
+ from omlish.lite.cached import cached_nullary
10
+ from omlish.lite.check import check
11
+ from omlish.lite.typing import Func2
12
+
13
+ from .default import get_default_interp_resolver
14
+ from .types import InterpSpecifier
15
+
16
+
17
+ ##
18
+
19
+
20
+ @dc.dataclass(frozen=True)
21
+ class InterpVenvConfig:
22
+ interp: ta.Optional[str] = None
23
+ requires: ta.Optional[ta.Sequence[str]] = None
24
+ use_uv: ta.Optional[bool] = None
25
+
26
+
27
+ class InterpVenvRequirementsProcessor(Func2['InterpVenv', ta.Sequence[str], ta.Sequence[str]]):
28
+ pass
29
+
30
+
31
+ class InterpVenv:
32
+ def __init__(
33
+ self,
34
+ path: str,
35
+ cfg: InterpVenvConfig,
36
+ *,
37
+ requirements_processor: ta.Optional[InterpVenvRequirementsProcessor] = None,
38
+ log: ta.Optional[logging.Logger] = None,
39
+ ) -> None:
40
+ super().__init__()
41
+
42
+ self._path = path
43
+ self._cfg = cfg
44
+
45
+ self._requirements_processor = requirements_processor
46
+ self._log = log
47
+
48
+ @property
49
+ def path(self) -> str:
50
+ return self._path
51
+
52
+ @property
53
+ def cfg(self) -> InterpVenvConfig:
54
+ return self._cfg
55
+
56
+ @async_cached_nullary
57
+ async def interp_exe(self) -> str:
58
+ i = InterpSpecifier.parse(check.not_none(self._cfg.interp))
59
+ return check.not_none(await get_default_interp_resolver().resolve(i, install=True)).exe
60
+
61
+ @cached_nullary
62
+ def exe(self) -> str:
63
+ ve = os.path.join(self._path, 'bin/python')
64
+ if not os.path.isfile(ve):
65
+ raise Exception(f'venv exe {ve} does not exist or is not a file!')
66
+ return ve
67
+
68
+ @async_cached_nullary
69
+ async def create(self) -> bool:
70
+ if os.path.exists(dn := self._path):
71
+ if not os.path.isdir(dn):
72
+ raise Exception(f'{dn} exists but is not a directory!')
73
+ return False
74
+
75
+ ie = await self.interp_exe()
76
+
77
+ if self._log is not None:
78
+ self._log.info('Using interpreter %s', ie)
79
+
80
+ await asyncio_subprocesses.check_call(ie, '-m', 'venv', dn)
81
+
82
+ ve = self.exe()
83
+ uv = self._cfg.use_uv
84
+
85
+ await asyncio_subprocesses.check_call(
86
+ ve,
87
+ '-m', 'pip',
88
+ 'install', '-v', '--upgrade',
89
+ 'pip',
90
+ 'setuptools',
91
+ 'wheel',
92
+ *(['uv'] if uv else []),
93
+ )
94
+
95
+ if sr := self._cfg.requires:
96
+ reqs = list(sr)
97
+ if self._requirements_processor is not None:
98
+ reqs = list(self._requirements_processor(self, reqs))
99
+
100
+ # TODO: automatically try slower uv download when it fails? lol
101
+ # Caused by: Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: 30s). # noqa
102
+ # UV_CONCURRENT_DOWNLOADS=4 UV_HTTP_TIMEOUT=3600
103
+
104
+ await asyncio_subprocesses.check_call(
105
+ ve,
106
+ '-m',
107
+ *(['uv'] if uv else []),
108
+ 'pip',
109
+ 'install',
110
+ *([] if uv else ['-v']),
111
+ *reqs,
112
+ )
113
+
114
+ return True
@@ -4,15 +4,14 @@ import typing as ta
4
4
 
5
5
  from omlish.lite.marshal import unmarshal_obj
6
6
 
7
+ from ..interp.venvs import InterpVenvConfig
8
+
7
9
 
8
10
  @dc.dataclass(frozen=True)
9
- class VenvConfig:
11
+ class VenvConfig(InterpVenvConfig):
10
12
  inherits: ta.Optional[ta.Sequence[str]] = None
11
- interp: ta.Optional[str] = None
12
- requires: ta.Optional[ta.List[str]] = None
13
13
  docker: ta.Optional[str] = None
14
14
  srcs: ta.Optional[ta.List[str]] = None
15
- use_uv: ta.Optional[bool] = None
16
15
 
17
16
 
18
17
  @dc.dataclass(frozen=True)
@@ -0,0 +1,12 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import typing as ta
3
+
4
+ from omlish.lite.inject import InjectorBindingOrBindings
5
+ from omlish.lite.inject import InjectorBindings
6
+ from omlish.lite.inject import inj
7
+
8
+
9
+ def bind_pyproject() -> InjectorBindings:
10
+ lst: ta.List[InjectorBindingOrBindings] = []
11
+
12
+ return inj.as_bindings(*lst)
omdev/pyproject/venvs.py CHANGED
@@ -3,14 +3,12 @@ import glob
3
3
  import os.path
4
4
  import typing as ta
5
5
 
6
- from omlish.asyncs.asyncio.subprocesses import asyncio_subprocesses
7
6
  from omlish.lite.cached import async_cached_nullary
8
7
  from omlish.lite.cached import cached_nullary
9
- from omlish.lite.check import check
10
8
  from omlish.lite.logs import log
11
9
 
12
- from ..interp.default import get_default_interp_resolver
13
- from ..interp.types import InterpSpecifier
10
+ from ..interp.venvs import InterpVenv
11
+ from ..interp.venvs import InterpVenvRequirementsProcessor
14
12
  from .configs import VenvConfig
15
13
  from .reqs import RequirementsRewriter
16
14
 
@@ -38,60 +36,26 @@ class Venv:
38
36
  def dir_name(self) -> str:
39
37
  return os.path.join(self.DIR_NAME, self._name)
40
38
 
41
- @async_cached_nullary
42
- async def interp_exe(self) -> str:
43
- i = InterpSpecifier.parse(check.not_none(self._cfg.interp))
44
- return check.not_none(await get_default_interp_resolver().resolve(i, install=True)).exe
39
+ @cached_nullary
40
+ def _iv(self) -> InterpVenv:
41
+ rr = RequirementsRewriter(self._name)
42
+
43
+ return InterpVenv(
44
+ self.dir_name,
45
+ self._cfg,
46
+ requirements_processor=InterpVenvRequirementsProcessor(
47
+ lambda iv, reqs: [rr.rewrite(req) for req in reqs] # noqa
48
+ ),
49
+ log=log,
50
+ )
45
51
 
46
52
  @cached_nullary
47
53
  def exe(self) -> str:
48
- ve = os.path.join(self.dir_name, 'bin/python')
49
- if not os.path.isfile(ve):
50
- raise Exception(f'venv exe {ve} does not exist or is not a file!')
51
- return ve
54
+ return self._iv().exe()
52
55
 
53
56
  @async_cached_nullary
54
57
  async def create(self) -> bool:
55
- if os.path.exists(dn := self.dir_name):
56
- if not os.path.isdir(dn):
57
- raise Exception(f'{dn} exists but is not a directory!')
58
- return False
59
-
60
- log.info('Using interpreter %s', (ie := await self.interp_exe()))
61
- await asyncio_subprocesses.check_call(ie, '-m', 'venv', dn)
62
-
63
- ve = self.exe()
64
- uv = self._cfg.use_uv
65
-
66
- await asyncio_subprocesses.check_call(
67
- ve,
68
- '-m', 'pip',
69
- 'install', '-v', '--upgrade',
70
- 'pip',
71
- 'setuptools',
72
- 'wheel',
73
- *(['uv'] if uv else []),
74
- )
75
-
76
- if sr := self._cfg.requires:
77
- rr = RequirementsRewriter(self._name)
78
- reqs = [rr.rewrite(req) for req in sr]
79
-
80
- # TODO: automatically try slower uv download when it fails? lol
81
- # Caused by: Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: 30s). # noqa
82
- # UV_CONCURRENT_DOWNLOADS=4 UV_HTTP_TIMEOUT=3600
83
-
84
- await asyncio_subprocesses.check_call(
85
- ve,
86
- '-m',
87
- *(['uv'] if uv else []),
88
- 'pip',
89
- 'install',
90
- *([] if uv else ['-v']),
91
- *reqs,
92
- )
93
-
94
- return True
58
+ return await self._iv().create()
95
59
 
96
60
  @staticmethod
97
61
  def _resolve_srcs(raw: ta.List[str]) -> ta.List[str]: