omdev 0.0.0.dev7__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.
Files changed (67) hide show
  1. omdev/__about__.py +35 -0
  2. omdev/__init__.py +0 -0
  3. omdev/amalg/__init__.py +0 -0
  4. omdev/amalg/__main__.py +4 -0
  5. omdev/amalg/amalg.py +513 -0
  6. omdev/classdot.py +61 -0
  7. omdev/cmake.py +164 -0
  8. omdev/exts/__init__.py +0 -0
  9. omdev/exts/_distutils/__init__.py +10 -0
  10. omdev/exts/_distutils/build_ext.py +367 -0
  11. omdev/exts/_distutils/compilers/__init__.py +3 -0
  12. omdev/exts/_distutils/compilers/ccompiler.py +1032 -0
  13. omdev/exts/_distutils/compilers/options.py +80 -0
  14. omdev/exts/_distutils/compilers/unixccompiler.py +385 -0
  15. omdev/exts/_distutils/dir_util.py +76 -0
  16. omdev/exts/_distutils/errors.py +62 -0
  17. omdev/exts/_distutils/extension.py +107 -0
  18. omdev/exts/_distutils/file_util.py +216 -0
  19. omdev/exts/_distutils/modified.py +47 -0
  20. omdev/exts/_distutils/spawn.py +103 -0
  21. omdev/exts/_distutils/sysconfig.py +349 -0
  22. omdev/exts/_distutils/util.py +201 -0
  23. omdev/exts/_distutils/version.py +308 -0
  24. omdev/exts/build.py +43 -0
  25. omdev/exts/cmake.py +195 -0
  26. omdev/exts/importhook.py +88 -0
  27. omdev/exts/scan.py +74 -0
  28. omdev/interp/__init__.py +1 -0
  29. omdev/interp/__main__.py +4 -0
  30. omdev/interp/cli.py +63 -0
  31. omdev/interp/inspect.py +105 -0
  32. omdev/interp/providers.py +67 -0
  33. omdev/interp/pyenv.py +353 -0
  34. omdev/interp/resolvers.py +76 -0
  35. omdev/interp/standalone.py +187 -0
  36. omdev/interp/system.py +125 -0
  37. omdev/interp/types.py +92 -0
  38. omdev/mypy/__init__.py +0 -0
  39. omdev/mypy/debug.py +86 -0
  40. omdev/pyproject/__init__.py +1 -0
  41. omdev/pyproject/__main__.py +4 -0
  42. omdev/pyproject/cli.py +319 -0
  43. omdev/pyproject/configs.py +97 -0
  44. omdev/pyproject/ext.py +107 -0
  45. omdev/pyproject/pkg.py +196 -0
  46. omdev/scripts/__init__.py +0 -0
  47. omdev/scripts/execrss.py +19 -0
  48. omdev/scripts/findimports.py +62 -0
  49. omdev/scripts/findmagic.py +70 -0
  50. omdev/scripts/interp.py +2118 -0
  51. omdev/scripts/pyproject.py +3584 -0
  52. omdev/scripts/traceimport.py +502 -0
  53. omdev/tokens.py +42 -0
  54. omdev/toml/__init__.py +1 -0
  55. omdev/toml/parser.py +823 -0
  56. omdev/toml/writer.py +104 -0
  57. omdev/tools/__init__.py +0 -0
  58. omdev/tools/dockertools.py +81 -0
  59. omdev/tools/sqlrepl.py +193 -0
  60. omdev/versioning/__init__.py +1 -0
  61. omdev/versioning/specifiers.py +531 -0
  62. omdev/versioning/versions.py +416 -0
  63. omdev-0.0.0.dev7.dist-info/LICENSE +21 -0
  64. omdev-0.0.0.dev7.dist-info/METADATA +24 -0
  65. omdev-0.0.0.dev7.dist-info/RECORD +67 -0
  66. omdev-0.0.0.dev7.dist-info/WHEEL +5 -0
  67. omdev-0.0.0.dev7.dist-info/top_level.txt +1 -0
omdev/exts/scan.py ADDED
@@ -0,0 +1,74 @@
1
+ import argparse
2
+ import logging
3
+ import os.path
4
+ import typing as ta
5
+
6
+ from omlish import logs
7
+
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+
12
+ SCAN_COMMENT = '// @omdev-ext'
13
+
14
+
15
+ def _scan_one(
16
+ input_path: str,
17
+ **kwargs: ta.Any,
18
+ ) -> None:
19
+ if not any(input_path.endswith(fx) for fx in ('.c', '.cc', '.cpp')):
20
+ return
21
+
22
+ with open(input_path, 'rb') as f:
23
+ srcb = f.read()
24
+
25
+ try:
26
+ src = srcb.decode('utf-8')
27
+ except UnicodeDecodeError:
28
+ return
29
+
30
+ sls = [l for l in src.splitlines() if l.startswith(SCAN_COMMENT)]
31
+ for sl in sls:
32
+ sas = sl[len(SCAN_COMMENT):].split() # noqa
33
+
34
+ log.info('Found ext: %s', input_path)
35
+
36
+
37
+ def _scan_cmd(args) -> None:
38
+ for i in args.inputs:
39
+ if not os.path.isdir(i):
40
+ raise Exception(f'Not a directory: {i}')
41
+
42
+ log.info('Scanning %s', i)
43
+ for we_dirpath, we_dirnames, we_filenames in os.walk(i): # noqa
44
+ for fname in we_filenames:
45
+ _scan_one(
46
+ os.path.abspath(os.path.join(we_dirpath, fname)),
47
+ )
48
+
49
+
50
+ def _build_parser() -> argparse.ArgumentParser:
51
+ parser = argparse.ArgumentParser()
52
+
53
+ subparsers = parser.add_subparsers()
54
+
55
+ parser_scan = subparsers.add_parser('scan')
56
+ parser_scan.add_argument('inputs', nargs='+')
57
+ parser_scan.set_defaults(func=_scan_cmd)
58
+
59
+ return parser
60
+
61
+
62
+ def _main() -> None:
63
+ logs.configure_standard_logging('INFO')
64
+
65
+ parser = _build_parser()
66
+ args = parser.parse_args()
67
+ if not getattr(args, 'func', None):
68
+ parser.print_help()
69
+ else:
70
+ args.func(args)
71
+
72
+
73
+ if __name__ == '__main__':
74
+ _main()
@@ -0,0 +1 @@
1
+ # @omlish-lite
@@ -0,0 +1,4 @@
1
+ if __name__ == '__main__':
2
+ from .cli import _main
3
+
4
+ _main()
omdev/interp/cli.py ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+ # @omdev-amalg ../scripts/interp.py
3
+ # ruff: noqa: UP007
4
+ """
5
+ TODO:
6
+ - partial best-matches - '3.12'
7
+ - https://github.com/asdf-vm/asdf support (instead of pyenv) ?
8
+ - colon sep provider name prefix - pyenv:3.12
9
+ """
10
+ import argparse
11
+ import typing as ta
12
+
13
+ from omlish.lite.logs import configure_standard_logging
14
+ from omlish.lite.runtime import check_runtime_version
15
+
16
+ from .resolvers import DEFAULT_INTERP_RESOLVER
17
+ from .types import InterpSpecifier
18
+
19
+
20
+ def _list_cmd(args) -> None:
21
+ r = DEFAULT_INTERP_RESOLVER
22
+ s = InterpSpecifier.parse(args.version)
23
+ r.list(s)
24
+
25
+
26
+ def _resolve_cmd(args) -> None:
27
+ r = DEFAULT_INTERP_RESOLVER
28
+ s = InterpSpecifier.parse(args.version)
29
+ print(r.resolve(s).exe)
30
+
31
+
32
+ def _build_parser() -> argparse.ArgumentParser:
33
+ parser = argparse.ArgumentParser()
34
+
35
+ subparsers = parser.add_subparsers()
36
+
37
+ parser_list = subparsers.add_parser('list')
38
+ parser_list.add_argument('version')
39
+ parser_list.add_argument('--debug', action='store_true')
40
+ parser_list.set_defaults(func=_list_cmd)
41
+
42
+ parser_resolve = subparsers.add_parser('resolve')
43
+ parser_resolve.add_argument('version')
44
+ parser_resolve.add_argument('--debug', action='store_true')
45
+ parser_resolve.set_defaults(func=_resolve_cmd)
46
+
47
+ return parser
48
+
49
+
50
+ def _main(argv: ta.Optional[ta.Sequence[str]] = None) -> None:
51
+ check_runtime_version()
52
+ configure_standard_logging()
53
+
54
+ parser = _build_parser()
55
+ args = parser.parse_args(argv)
56
+ if not getattr(args, 'func', None):
57
+ parser.print_help()
58
+ else:
59
+ args.func(args)
60
+
61
+
62
+ if __name__ == '__main__':
63
+ _main()
@@ -0,0 +1,105 @@
1
+ # ruff: noqa: UP006 UP007
2
+ import dataclasses as dc
3
+ import json
4
+ import logging
5
+ import sys
6
+ import typing as ta
7
+
8
+ from omlish.lite.logs import log
9
+ from omlish.lite.subprocesses import subprocess_check_output
10
+
11
+ from ..versioning.versions import Version
12
+ from .types import InterpOpts
13
+ from .types import InterpVersion
14
+
15
+
16
+ @dc.dataclass(frozen=True)
17
+ class InterpInspection:
18
+ exe: str
19
+ version: Version
20
+
21
+ version_str: str
22
+ config_vars: ta.Mapping[str, str]
23
+ prefix: str
24
+ base_prefix: str
25
+
26
+ @property
27
+ def opts(self) -> InterpOpts:
28
+ return InterpOpts(
29
+ threaded=bool(self.config_vars.get('Py_GIL_DISABLED')),
30
+ debug=bool(self.config_vars.get('Py_DEBUG')),
31
+ )
32
+
33
+ @property
34
+ def iv(self) -> InterpVersion:
35
+ return InterpVersion(
36
+ version=self.version,
37
+ opts=self.opts,
38
+ )
39
+
40
+ @property
41
+ def is_venv(self) -> bool:
42
+ return self.prefix != self.base_prefix
43
+
44
+
45
+ class InterpInspector:
46
+
47
+ def __init__(self) -> None:
48
+ super().__init__()
49
+
50
+ self._cache: ta.Dict[str, ta.Optional[InterpInspection]] = {}
51
+
52
+ _RAW_INSPECTION_CODE = """
53
+ __import__('json').dumps(dict(
54
+ version_str=__import__('sys').version,
55
+ prefix=__import__('sys').prefix,
56
+ base_prefix=__import__('sys').base_prefix,
57
+ config_vars=__import__('sysconfig').get_config_vars(),
58
+ ))"""
59
+
60
+ _INSPECTION_CODE = ''.join(l.strip() for l in _RAW_INSPECTION_CODE.splitlines())
61
+
62
+ @staticmethod
63
+ def _build_inspection(
64
+ exe: str,
65
+ output: str,
66
+ ) -> InterpInspection:
67
+ dct = json.loads(output)
68
+
69
+ version = Version(dct['version_str'].split()[0])
70
+
71
+ return InterpInspection(
72
+ exe=exe,
73
+ version=version,
74
+ **{k: dct[k] for k in (
75
+ 'version_str',
76
+ 'prefix',
77
+ 'base_prefix',
78
+ 'config_vars',
79
+ )},
80
+ )
81
+
82
+ @classmethod
83
+ def running(cls) -> 'InterpInspection':
84
+ return cls._build_inspection(sys.executable, eval(cls._INSPECTION_CODE)) # noqa
85
+
86
+ def _inspect(self, exe: str) -> InterpInspection:
87
+ output = subprocess_check_output(exe, '-c', f'print({self._INSPECTION_CODE})', quiet=True)
88
+ return self._build_inspection(exe, output.decode())
89
+
90
+ def inspect(self, exe: str) -> ta.Optional[InterpInspection]:
91
+ try:
92
+ return self._cache[exe]
93
+ except KeyError:
94
+ ret: ta.Optional[InterpInspection]
95
+ try:
96
+ ret = self._inspect(exe)
97
+ except Exception as e: # noqa
98
+ if log.isEnabledFor(logging.DEBUG):
99
+ log.exception('Failed to inspect interp: %s', exe)
100
+ ret = None
101
+ self._cache[exe] = ret
102
+ return ret
103
+
104
+
105
+ INTERP_INSPECTOR = InterpInspector()
@@ -0,0 +1,67 @@
1
+ """
2
+ TODO:
3
+ - backends
4
+ - local builds
5
+ - deadsnakes?
6
+ - loose versions
7
+ """
8
+ import abc
9
+ import sys
10
+ import typing as ta
11
+
12
+ from omlish.lite.cached import cached_nullary
13
+ from omlish.lite.strings import snake_case
14
+
15
+ from .inspect import InterpInspector
16
+ from .types import Interp
17
+ from .types import InterpSpecifier
18
+ from .types import InterpVersion
19
+
20
+
21
+ ##
22
+
23
+
24
+ class InterpProvider(abc.ABC):
25
+ name: ta.ClassVar[str]
26
+
27
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
28
+ super().__init_subclass__(**kwargs)
29
+ if abc.ABC not in cls.__bases__ and 'name' not in cls.__dict__:
30
+ sfx = 'InterpProvider'
31
+ if not cls.__name__.endswith(sfx):
32
+ raise NameError(cls)
33
+ setattr(cls, 'name', snake_case(cls.__name__[:-len(sfx)]))
34
+
35
+ @abc.abstractmethod
36
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
37
+ raise NotImplementedError
38
+
39
+ @abc.abstractmethod
40
+ def get_installed_version(self, version: InterpVersion) -> Interp:
41
+ raise NotImplementedError
42
+
43
+ def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
44
+ return []
45
+
46
+ def install_version(self, version: InterpVersion) -> Interp:
47
+ raise TypeError
48
+
49
+
50
+ ##
51
+
52
+
53
+ class RunningInterpProvider(InterpProvider):
54
+ @cached_nullary
55
+ def version(self) -> InterpVersion:
56
+ return InterpInspector.running().iv
57
+
58
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
59
+ return [self.version()]
60
+
61
+ def get_installed_version(self, version: InterpVersion) -> Interp:
62
+ if version != self.version():
63
+ raise KeyError(version)
64
+ return Interp(
65
+ exe=sys.executable,
66
+ version=self.version(),
67
+ )
omdev/interp/pyenv.py ADDED
@@ -0,0 +1,353 @@
1
+ """
2
+ TODO:
3
+ - custom tags
4
+ - optionally install / upgrade pyenv itself
5
+ - new vers dont need these custom mac opts, only run on old vers
6
+ """
7
+ # ruff: noqa: UP006 UP007
8
+ import abc
9
+ import dataclasses as dc
10
+ import itertools
11
+ import os.path
12
+ import shutil
13
+ import sys
14
+ import typing as ta
15
+
16
+ from omlish.lite.cached import cached_nullary
17
+ from omlish.lite.check import check_not_none
18
+ from omlish.lite.logs import log
19
+ from omlish.lite.subprocesses import subprocess_check_call
20
+ from omlish.lite.subprocesses import subprocess_check_output_str
21
+ from omlish.lite.subprocesses import subprocess_try_output
22
+
23
+ from ..versioning.versions import InvalidVersion
24
+ from ..versioning.versions import Version
25
+ from .inspect import INTERP_INSPECTOR
26
+ from .inspect import InterpInspector
27
+ from .providers import InterpProvider
28
+ from .types import Interp
29
+ from .types import InterpOpts
30
+ from .types import InterpSpecifier
31
+ from .types import InterpVersion
32
+
33
+
34
+ ##
35
+
36
+
37
+ class Pyenv:
38
+
39
+ def __init__(
40
+ self,
41
+ *,
42
+ root: ta.Optional[str] = None,
43
+ ) -> None:
44
+ if root is not None and not (isinstance(root, str) and root):
45
+ raise ValueError(f'pyenv_root: {root!r}')
46
+
47
+ super().__init__()
48
+
49
+ self._root_kw = root
50
+
51
+ @cached_nullary
52
+ def root(self) -> ta.Optional[str]:
53
+ if self._root_kw is not None:
54
+ return self._root_kw
55
+
56
+ if shutil.which('pyenv'):
57
+ return subprocess_check_output_str('pyenv', 'root')
58
+
59
+ d = os.path.expanduser('~/.pyenv')
60
+ if os.path.isdir(d) and os.path.isfile(os.path.join(d, 'bin', 'pyenv')):
61
+ return d
62
+
63
+ return None
64
+
65
+ @cached_nullary
66
+ def exe(self) -> str:
67
+ return os.path.join(check_not_none(self.root()), 'bin', 'pyenv')
68
+
69
+ def version_exes(self) -> ta.List[ta.Tuple[str, str]]:
70
+ ret = []
71
+ vp = os.path.join(self.root(), 'versions')
72
+ for dn in os.listdir(vp):
73
+ ep = os.path.join(vp, dn, 'bin', 'python')
74
+ if not os.path.isfile(ep):
75
+ continue
76
+ ret.append((dn, ep))
77
+ return ret
78
+
79
+ def installable_versions(self) -> ta.List[str]:
80
+ ret = []
81
+ s = subprocess_check_output_str(self.exe(), 'install', '--list')
82
+ for l in s.splitlines():
83
+ if not l.startswith(' '):
84
+ continue
85
+ l = l.strip()
86
+ if not l:
87
+ continue
88
+ ret.append(l)
89
+ return ret
90
+
91
+
92
+ ##
93
+
94
+
95
+ @dc.dataclass(frozen=True)
96
+ class PyenvInstallOpts:
97
+ opts: ta.Sequence[str] = ()
98
+ conf_opts: ta.Sequence[str] = ()
99
+ cflags: ta.Sequence[str] = ()
100
+ ldflags: ta.Sequence[str] = ()
101
+ env: ta.Mapping[str, str] = dc.field(default_factory=dict)
102
+
103
+ def merge(self, *others: 'PyenvInstallOpts') -> 'PyenvInstallOpts':
104
+ return PyenvInstallOpts(
105
+ opts=list(itertools.chain.from_iterable(o.opts for o in [self, *others])),
106
+ conf_opts=list(itertools.chain.from_iterable(o.conf_opts for o in [self, *others])),
107
+ cflags=list(itertools.chain.from_iterable(o.cflags for o in [self, *others])),
108
+ ldflags=list(itertools.chain.from_iterable(o.ldflags for o in [self, *others])),
109
+ env=dict(itertools.chain.from_iterable(o.env.items() for o in [self, *others])),
110
+ )
111
+
112
+
113
+ DEFAULT_PYENV_INSTALL_OPTS = PyenvInstallOpts(opts=['-s', '-v'])
114
+ DEBUG_PYENV_INSTALL_OPTS = PyenvInstallOpts(opts=['-g'])
115
+
116
+
117
+ #
118
+
119
+
120
+ class PyenvInstallOptsProvider(abc.ABC):
121
+ @abc.abstractmethod
122
+ def opts(self) -> PyenvInstallOpts:
123
+ raise NotImplementedError
124
+
125
+
126
+ class LinuxPyenvInstallOpts(PyenvInstallOptsProvider):
127
+ def opts(self) -> PyenvInstallOpts:
128
+ return PyenvInstallOpts()
129
+
130
+
131
+ class DarwinPyenvInstallOpts(PyenvInstallOptsProvider):
132
+
133
+ @cached_nullary
134
+ def framework_opts(self) -> PyenvInstallOpts:
135
+ return PyenvInstallOpts(conf_opts=['--enable-framework'])
136
+
137
+ @cached_nullary
138
+ def has_brew(self) -> bool:
139
+ return shutil.which('brew') is not None
140
+
141
+ BREW_DEPS: ta.Sequence[str] = [
142
+ 'openssl',
143
+ 'readline',
144
+ 'sqlite3',
145
+ 'zlib',
146
+ ]
147
+
148
+ @cached_nullary
149
+ def brew_deps_opts(self) -> PyenvInstallOpts:
150
+ cflags = []
151
+ ldflags = []
152
+ for dep in self.BREW_DEPS:
153
+ dep_prefix = subprocess_check_output_str('brew', '--prefix', dep)
154
+ cflags.append(f'-I{dep_prefix}/include')
155
+ ldflags.append(f'-L{dep_prefix}/lib')
156
+ return PyenvInstallOpts(
157
+ cflags=cflags,
158
+ ldflags=ldflags,
159
+ )
160
+
161
+ @cached_nullary
162
+ def brew_tcl_opts(self) -> PyenvInstallOpts:
163
+ if subprocess_try_output('brew', '--prefix', 'tcl-tk') is None:
164
+ return PyenvInstallOpts()
165
+
166
+ tcl_tk_prefix = subprocess_check_output_str('brew', '--prefix', 'tcl-tk')
167
+ tcl_tk_ver_str = subprocess_check_output_str('brew', 'ls', '--versions', 'tcl-tk')
168
+ tcl_tk_ver = '.'.join(tcl_tk_ver_str.split()[1].split('.')[:2])
169
+
170
+ return PyenvInstallOpts(conf_opts=[
171
+ f"--with-tcltk-includes='-I{tcl_tk_prefix}/include'",
172
+ f"--with-tcltk-libs='-L{tcl_tk_prefix}/lib -ltcl{tcl_tk_ver} -ltk{tcl_tk_ver}'",
173
+ ])
174
+
175
+ @cached_nullary
176
+ def brew_ssl_opts(self) -> PyenvInstallOpts:
177
+ pkg_config_path = subprocess_check_output_str('brew', '--prefix', 'openssl')
178
+ if 'PKG_CONFIG_PATH' in os.environ:
179
+ pkg_config_path += ':' + os.environ['PKG_CONFIG_PATH']
180
+ return PyenvInstallOpts(env={'PKG_CONFIG_PATH': pkg_config_path})
181
+
182
+ def opts(self) -> PyenvInstallOpts:
183
+ return PyenvInstallOpts().merge(
184
+ self.framework_opts(),
185
+ self.brew_deps_opts(),
186
+ self.brew_tcl_opts(),
187
+ self.brew_ssl_opts(),
188
+ )
189
+
190
+
191
+ PLATFORM_PYENV_INSTALL_OPTS: ta.Dict[str, PyenvInstallOptsProvider] = {
192
+ 'darwin': DarwinPyenvInstallOpts(),
193
+ 'linux': LinuxPyenvInstallOpts(),
194
+ }
195
+
196
+
197
+ ##
198
+
199
+
200
+ class PyenvVersionInstaller:
201
+
202
+ def __init__(
203
+ self,
204
+ version: str,
205
+ opts: ta.Optional[PyenvInstallOpts] = None,
206
+ *,
207
+ debug: bool = False,
208
+ pyenv: Pyenv = Pyenv(),
209
+ ) -> None:
210
+ super().__init__()
211
+
212
+ if opts is None:
213
+ lst = [DEFAULT_PYENV_INSTALL_OPTS]
214
+ if debug:
215
+ lst.append(DEBUG_PYENV_INSTALL_OPTS)
216
+ lst.append(PLATFORM_PYENV_INSTALL_OPTS[sys.platform].opts())
217
+ opts = PyenvInstallOpts().merge(*lst)
218
+
219
+ self._version = version
220
+ self._opts = opts
221
+ self._debug = debug
222
+ self._pyenv = pyenv
223
+
224
+ @property
225
+ def version(self) -> str:
226
+ return self._version
227
+
228
+ @property
229
+ def opts(self) -> PyenvInstallOpts:
230
+ return self._opts
231
+
232
+ @cached_nullary
233
+ def install_name(self) -> str:
234
+ return self._version + ('-debug' if self._debug else '')
235
+
236
+ @cached_nullary
237
+ def install_dir(self) -> str:
238
+ return str(os.path.join(check_not_none(self._pyenv.root()), 'versions', self.install_name()))
239
+
240
+ @cached_nullary
241
+ def install(self) -> str:
242
+ env = dict(self._opts.env)
243
+ for k, l in [
244
+ ('CFLAGS', self._opts.cflags),
245
+ ('LDFLAGS', self._opts.ldflags),
246
+ ('PYTHON_CONFIGURE_OPTS', self._opts.conf_opts),
247
+ ]:
248
+ v = ' '.join(l)
249
+ if k in os.environ:
250
+ v += ' ' + os.environ[k]
251
+ env[k] = v
252
+
253
+ subprocess_check_call(self._pyenv.exe(), 'install', *self._opts.opts, self._version, env=env)
254
+
255
+ exe = os.path.join(self.install_dir(), 'bin', 'python')
256
+ if not os.path.isfile(exe):
257
+ raise RuntimeError(f'Interpreter not found: {exe}')
258
+ return exe
259
+
260
+
261
+ ##
262
+
263
+
264
+ class PyenvInterpProvider(InterpProvider):
265
+
266
+ def __init__(
267
+ self,
268
+ pyenv: Pyenv = Pyenv(),
269
+
270
+ inspect: bool = False,
271
+ inspector: InterpInspector = INTERP_INSPECTOR,
272
+ ) -> None:
273
+ super().__init__()
274
+
275
+ self._pyenv = pyenv
276
+
277
+ self._inspect = inspect
278
+ self._inspector = inspector
279
+
280
+ #
281
+
282
+ @staticmethod
283
+ def guess_version(s: str) -> ta.Optional[InterpVersion]:
284
+ def strip_sfx(s: str, sfx: str) -> ta.Tuple[str, bool]:
285
+ if s.endswith(sfx):
286
+ return s[:-len(sfx)], True
287
+ return s, False
288
+ ok = {}
289
+ s, ok['debug'] = strip_sfx(s, '-debug')
290
+ s, ok['threaded'] = strip_sfx(s, 't')
291
+ try:
292
+ v = Version(s)
293
+ except InvalidVersion:
294
+ return None
295
+ return InterpVersion(v, InterpOpts(**ok))
296
+
297
+ class Installed(ta.NamedTuple):
298
+ name: str
299
+ exe: str
300
+ version: InterpVersion
301
+
302
+ def _make_installed(self, vn: str, ep: str) -> ta.Optional[Installed]:
303
+ iv: ta.Optional[InterpVersion]
304
+ if self._inspect:
305
+ try:
306
+ iv = check_not_none(self._inspector.inspect(ep)).iv
307
+ except Exception as e: # noqa
308
+ return None
309
+ else:
310
+ iv = self.guess_version(vn)
311
+ if iv is None:
312
+ return None
313
+ return PyenvInterpProvider.Installed(
314
+ name=vn,
315
+ exe=ep,
316
+ version=iv,
317
+ )
318
+
319
+ def installed(self) -> ta.Sequence[Installed]:
320
+ ret: ta.List[PyenvInterpProvider.Installed] = []
321
+ for vn, ep in self._pyenv.version_exes():
322
+ if (i := self._make_installed(vn, ep)) is None:
323
+ log.debug('Invalid pyenv version: %s', vn)
324
+ continue
325
+ ret.append(i)
326
+ return ret
327
+
328
+ #
329
+
330
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
331
+ return [i.version for i in self.installed()]
332
+
333
+ def get_installed_version(self, version: InterpVersion) -> Interp:
334
+ for i in self.installed():
335
+ if i.version == version:
336
+ return Interp(
337
+ exe=i.exe,
338
+ version=i.version,
339
+ )
340
+ raise KeyError(version)
341
+
342
+ #
343
+
344
+ def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
345
+ lst = []
346
+ for vs in self._pyenv.installable_versions():
347
+ if (iv := self.guess_version(vs)) is None:
348
+ continue
349
+ if iv.opts.debug:
350
+ raise Exception('Pyenv installable versions not expected to have debug suffix')
351
+ for d in [False, True]:
352
+ lst.append(dc.replace(iv, opts=dc.replace(iv.opts, debug=d)))
353
+ return lst