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.
@@ -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]: