omdev 0.0.0.dev147__py3-none-any.whl → 0.0.0.dev148__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.

Potentially problematic release.


This version of omdev might be problematic. Click here for more details.

@@ -25,6 +25,9 @@ See:
25
25
  """
26
26
  import abc
27
27
  import argparse
28
+ import asyncio
29
+ import asyncio.base_subprocess
30
+ import asyncio.subprocess
28
31
  import base64
29
32
  import collections
30
33
  import collections.abc
@@ -89,6 +92,9 @@ TomlParseFloat = ta.Callable[[str], ta.Any]
89
92
  TomlKey = ta.Tuple[str, ...]
90
93
  TomlPos = int # ta.TypeAlias
91
94
 
95
+ # ../../omlish/lite/asyncio/asyncio.py
96
+ AwaitableT = ta.TypeVar('AwaitableT', bound=ta.Awaitable)
97
+
92
98
  # ../../omlish/lite/cached.py
93
99
  T = ta.TypeVar('T')
94
100
  CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
@@ -1764,11 +1770,74 @@ class WheelFile(zipfile.ZipFile):
1764
1770
  super().close()
1765
1771
 
1766
1772
 
1773
+ ########################################
1774
+ # ../../../omlish/lite/asyncio/asyncio.py
1775
+
1776
+
1777
+ ##
1778
+
1779
+
1780
+ ASYNCIO_DEFAULT_BUFFER_LIMIT = 2 ** 16
1781
+
1782
+
1783
+ async def asyncio_open_stream_reader(
1784
+ f: ta.IO,
1785
+ loop: ta.Any = None,
1786
+ *,
1787
+ limit: int = ASYNCIO_DEFAULT_BUFFER_LIMIT,
1788
+ ) -> asyncio.StreamReader:
1789
+ if loop is None:
1790
+ loop = asyncio.get_running_loop()
1791
+
1792
+ reader = asyncio.StreamReader(limit=limit, loop=loop)
1793
+ await loop.connect_read_pipe(
1794
+ lambda: asyncio.StreamReaderProtocol(reader, loop=loop),
1795
+ f,
1796
+ )
1797
+
1798
+ return reader
1799
+
1800
+
1801
+ async def asyncio_open_stream_writer(
1802
+ f: ta.IO,
1803
+ loop: ta.Any = None,
1804
+ ) -> asyncio.StreamWriter:
1805
+ if loop is None:
1806
+ loop = asyncio.get_running_loop()
1807
+
1808
+ writer_transport, writer_protocol = await loop.connect_write_pipe(
1809
+ lambda: asyncio.streams.FlowControlMixin(loop=loop),
1810
+ f,
1811
+ )
1812
+
1813
+ return asyncio.streams.StreamWriter(
1814
+ writer_transport,
1815
+ writer_protocol,
1816
+ None,
1817
+ loop,
1818
+ )
1819
+
1820
+
1821
+ ##
1822
+
1823
+
1824
+ def asyncio_maybe_timeout(
1825
+ fut: AwaitableT,
1826
+ timeout: ta.Optional[float] = None,
1827
+ ) -> AwaitableT:
1828
+ if timeout is not None:
1829
+ fut = asyncio.wait_for(fut, timeout) # type: ignore
1830
+ return fut
1831
+
1832
+
1767
1833
  ########################################
1768
1834
  # ../../../omlish/lite/cached.py
1769
1835
 
1770
1836
 
1771
- class _cached_nullary: # noqa
1837
+ ##
1838
+
1839
+
1840
+ class _AbstractCachedNullary:
1772
1841
  def __init__(self, fn):
1773
1842
  super().__init__()
1774
1843
  self._fn = fn
@@ -1776,17 +1845,25 @@ class _cached_nullary: # noqa
1776
1845
  functools.update_wrapper(self, fn)
1777
1846
 
1778
1847
  def __call__(self, *args, **kwargs): # noqa
1779
- if self._value is self._missing:
1780
- self._value = self._fn()
1781
- return self._value
1848
+ raise TypeError
1782
1849
 
1783
1850
  def __get__(self, instance, owner): # noqa
1784
1851
  bound = instance.__dict__[self._fn.__name__] = self.__class__(self._fn.__get__(instance, owner))
1785
1852
  return bound
1786
1853
 
1787
1854
 
1855
+ ##
1856
+
1857
+
1858
+ class _CachedNullary(_AbstractCachedNullary):
1859
+ def __call__(self, *args, **kwargs): # noqa
1860
+ if self._value is self._missing:
1861
+ self._value = self._fn()
1862
+ return self._value
1863
+
1864
+
1788
1865
  def cached_nullary(fn): # ta.Callable[..., T]) -> ta.Callable[..., T]:
1789
- return _cached_nullary(fn)
1866
+ return _CachedNullary(fn)
1790
1867
 
1791
1868
 
1792
1869
  def static_init(fn: CallableT) -> CallableT:
@@ -1795,6 +1872,20 @@ def static_init(fn: CallableT) -> CallableT:
1795
1872
  return fn
1796
1873
 
1797
1874
 
1875
+ ##
1876
+
1877
+
1878
+ class _AsyncCachedNullary(_AbstractCachedNullary):
1879
+ async def __call__(self, *args, **kwargs):
1880
+ if self._value is self._missing:
1881
+ self._value = await self._fn()
1882
+ return self._value
1883
+
1884
+
1885
+ def async_cached_nullary(fn): # ta.Callable[..., T]) -> ta.Callable[..., T]:
1886
+ return _AsyncCachedNullary(fn)
1887
+
1888
+
1798
1889
  ########################################
1799
1890
  # ../../../omlish/lite/check.py
1800
1891
 
@@ -3601,6 +3692,8 @@ class InterpSpecifier:
3601
3692
  s, o = InterpOpts.parse_suffix(s)
3602
3693
  if not any(s.startswith(o) for o in Specifier.OPERATORS):
3603
3694
  s = '~=' + s
3695
+ if s.count('.') < 2:
3696
+ s += '.0'
3604
3697
  return cls(
3605
3698
  specifier=Specifier(s),
3606
3699
  opts=o,
@@ -3825,7 +3918,7 @@ def subprocess_maybe_shell_wrap_exec(*args: str) -> ta.Tuple[str, ...]:
3825
3918
  return args
3826
3919
 
3827
3920
 
3828
- def _prepare_subprocess_invocation(
3921
+ def prepare_subprocess_invocation(
3829
3922
  *args: str,
3830
3923
  env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
3831
3924
  extra_env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
@@ -3833,9 +3926,9 @@ def _prepare_subprocess_invocation(
3833
3926
  shell: bool = False,
3834
3927
  **kwargs: ta.Any,
3835
3928
  ) -> ta.Tuple[ta.Tuple[ta.Any, ...], ta.Dict[str, ta.Any]]:
3836
- log.debug(args)
3929
+ log.debug('prepare_subprocess_invocation: args=%r', args)
3837
3930
  if extra_env:
3838
- log.debug(extra_env)
3931
+ log.debug('prepare_subprocess_invocation: extra_env=%r', extra_env)
3839
3932
 
3840
3933
  if extra_env:
3841
3934
  env = {**(env if env is not None else os.environ), **extra_env}
@@ -3854,14 +3947,46 @@ def _prepare_subprocess_invocation(
3854
3947
  )
3855
3948
 
3856
3949
 
3857
- def subprocess_check_call(*args: str, stdout=sys.stderr, **kwargs: ta.Any) -> None:
3858
- args, kwargs = _prepare_subprocess_invocation(*args, stdout=stdout, **kwargs)
3859
- return subprocess.check_call(args, **kwargs) # type: ignore
3950
+ ##
3951
+
3952
+
3953
+ @contextlib.contextmanager
3954
+ def subprocess_common_context(*args: ta.Any, **kwargs: ta.Any) -> ta.Iterator[None]:
3955
+ start_time = time.time()
3956
+ try:
3957
+ log.debug('subprocess_common_context.try: args=%r', args)
3958
+ yield
3959
+
3960
+ except Exception as exc: # noqa
3961
+ log.debug('subprocess_common_context.except: exc=%r', exc)
3962
+ raise
3963
+
3964
+ finally:
3965
+ end_time = time.time()
3966
+ elapsed_s = end_time - start_time
3967
+ log.debug('subprocess_common_context.finally: elapsed_s=%f args=%r', elapsed_s, args)
3968
+
3969
+
3970
+ ##
3971
+
3972
+
3973
+ def subprocess_check_call(
3974
+ *args: str,
3975
+ stdout: ta.Any = sys.stderr,
3976
+ **kwargs: ta.Any,
3977
+ ) -> None:
3978
+ args, kwargs = prepare_subprocess_invocation(*args, stdout=stdout, **kwargs)
3979
+ with subprocess_common_context(*args, **kwargs):
3980
+ return subprocess.check_call(args, **kwargs) # type: ignore
3860
3981
 
3861
3982
 
3862
- def subprocess_check_output(*args: str, **kwargs: ta.Any) -> bytes:
3863
- args, kwargs = _prepare_subprocess_invocation(*args, **kwargs)
3864
- return subprocess.check_output(args, **kwargs)
3983
+ def subprocess_check_output(
3984
+ *args: str,
3985
+ **kwargs: ta.Any,
3986
+ ) -> bytes:
3987
+ args, kwargs = prepare_subprocess_invocation(*args, **kwargs)
3988
+ with subprocess_common_context(*args, **kwargs):
3989
+ return subprocess.check_output(args, **kwargs)
3865
3990
 
3866
3991
 
3867
3992
  def subprocess_check_output_str(*args: str, **kwargs: ta.Any) -> str:
@@ -3877,16 +4002,31 @@ DEFAULT_SUBPROCESS_TRY_EXCEPTIONS: ta.Tuple[ta.Type[Exception], ...] = (
3877
4002
  )
3878
4003
 
3879
4004
 
3880
- def subprocess_try_call(
3881
- *args: str,
4005
+ def _subprocess_try_run(
4006
+ fn: ta.Callable[..., T],
4007
+ *args: ta.Any,
3882
4008
  try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
3883
4009
  **kwargs: ta.Any,
3884
- ) -> bool:
4010
+ ) -> ta.Union[T, Exception]:
3885
4011
  try:
3886
- subprocess_check_call(*args, **kwargs)
4012
+ return fn(*args, **kwargs)
3887
4013
  except try_exceptions as e: # noqa
3888
4014
  if log.isEnabledFor(logging.DEBUG):
3889
4015
  log.exception('command failed')
4016
+ return e
4017
+
4018
+
4019
+ def subprocess_try_call(
4020
+ *args: str,
4021
+ try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
4022
+ **kwargs: ta.Any,
4023
+ ) -> bool:
4024
+ if isinstance(_subprocess_try_run(
4025
+ subprocess_check_call,
4026
+ *args,
4027
+ try_exceptions=try_exceptions,
4028
+ **kwargs,
4029
+ ), Exception):
3890
4030
  return False
3891
4031
  else:
3892
4032
  return True
@@ -3897,12 +4037,15 @@ def subprocess_try_output(
3897
4037
  try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
3898
4038
  **kwargs: ta.Any,
3899
4039
  ) -> ta.Optional[bytes]:
3900
- try:
3901
- return subprocess_check_output(*args, **kwargs)
3902
- except try_exceptions as e: # noqa
3903
- if log.isEnabledFor(logging.DEBUG):
3904
- log.exception('command failed')
4040
+ if isinstance(ret := _subprocess_try_run(
4041
+ subprocess_check_output,
4042
+ *args,
4043
+ try_exceptions=try_exceptions,
4044
+ **kwargs,
4045
+ ), Exception):
3905
4046
  return None
4047
+ else:
4048
+ return ret
3906
4049
 
3907
4050
 
3908
4051
  def subprocess_try_output_str(*args: str, **kwargs: ta.Any) -> ta.Optional[str]:
@@ -4329,6 +4472,285 @@ def get_git_status(
4329
4472
  return parse_git_status(proc.stdout.decode()) # noqa
4330
4473
 
4331
4474
 
4475
+ ########################################
4476
+ # ../../../omlish/lite/asyncio/subprocesses.py
4477
+
4478
+
4479
+ ##
4480
+
4481
+
4482
+ @contextlib.asynccontextmanager
4483
+ async def asyncio_subprocess_popen(
4484
+ *cmd: str,
4485
+ shell: bool = False,
4486
+ timeout: ta.Optional[float] = None,
4487
+ **kwargs: ta.Any,
4488
+ ) -> ta.AsyncGenerator[asyncio.subprocess.Process, None]:
4489
+ fac: ta.Any
4490
+ if shell:
4491
+ fac = functools.partial(
4492
+ asyncio.create_subprocess_shell,
4493
+ check_single(cmd),
4494
+ )
4495
+ else:
4496
+ fac = functools.partial(
4497
+ asyncio.create_subprocess_exec,
4498
+ *cmd,
4499
+ )
4500
+
4501
+ with subprocess_common_context(
4502
+ *cmd,
4503
+ shell=shell,
4504
+ timeout=timeout,
4505
+ **kwargs,
4506
+ ):
4507
+ proc: asyncio.subprocess.Process
4508
+ proc = await fac(**kwargs)
4509
+ try:
4510
+ yield proc
4511
+
4512
+ finally:
4513
+ await asyncio_maybe_timeout(proc.wait(), timeout)
4514
+
4515
+
4516
+ ##
4517
+
4518
+
4519
+ class AsyncioProcessCommunicator:
4520
+ def __init__(
4521
+ self,
4522
+ proc: asyncio.subprocess.Process,
4523
+ loop: ta.Optional[ta.Any] = None,
4524
+ ) -> None:
4525
+ super().__init__()
4526
+
4527
+ if loop is None:
4528
+ loop = asyncio.get_running_loop()
4529
+
4530
+ self._proc = proc
4531
+ self._loop = loop
4532
+
4533
+ self._transport: asyncio.base_subprocess.BaseSubprocessTransport = check_isinstance(
4534
+ proc._transport, # type: ignore # noqa
4535
+ asyncio.base_subprocess.BaseSubprocessTransport,
4536
+ )
4537
+
4538
+ @property
4539
+ def _debug(self) -> bool:
4540
+ return self._loop.get_debug()
4541
+
4542
+ async def _feed_stdin(self, input: bytes) -> None: # noqa
4543
+ stdin = check_not_none(self._proc.stdin)
4544
+ try:
4545
+ if input is not None:
4546
+ stdin.write(input)
4547
+ if self._debug:
4548
+ log.debug('%r communicate: feed stdin (%s bytes)', self, len(input))
4549
+
4550
+ await stdin.drain()
4551
+
4552
+ except (BrokenPipeError, ConnectionResetError) as exc:
4553
+ # communicate() ignores BrokenPipeError and ConnectionResetError. write() and drain() can raise these
4554
+ # exceptions.
4555
+ if self._debug:
4556
+ log.debug('%r communicate: stdin got %r', self, exc)
4557
+
4558
+ if self._debug:
4559
+ log.debug('%r communicate: close stdin', self)
4560
+
4561
+ stdin.close()
4562
+
4563
+ async def _noop(self) -> None:
4564
+ return None
4565
+
4566
+ async def _read_stream(self, fd: int) -> bytes:
4567
+ transport: ta.Any = check_not_none(self._transport.get_pipe_transport(fd))
4568
+
4569
+ if fd == 2:
4570
+ stream = check_not_none(self._proc.stderr)
4571
+ else:
4572
+ check_equal(fd, 1)
4573
+ stream = check_not_none(self._proc.stdout)
4574
+
4575
+ if self._debug:
4576
+ name = 'stdout' if fd == 1 else 'stderr'
4577
+ log.debug('%r communicate: read %s', self, name)
4578
+
4579
+ output = await stream.read()
4580
+
4581
+ if self._debug:
4582
+ name = 'stdout' if fd == 1 else 'stderr'
4583
+ log.debug('%r communicate: close %s', self, name)
4584
+
4585
+ transport.close()
4586
+
4587
+ return output
4588
+
4589
+ class Communication(ta.NamedTuple):
4590
+ stdout: ta.Optional[bytes]
4591
+ stderr: ta.Optional[bytes]
4592
+
4593
+ async def _communicate(
4594
+ self,
4595
+ input: ta.Any = None, # noqa
4596
+ ) -> Communication:
4597
+ stdin_fut: ta.Any
4598
+ if self._proc.stdin is not None:
4599
+ stdin_fut = self._feed_stdin(input)
4600
+ else:
4601
+ stdin_fut = self._noop()
4602
+
4603
+ stdout_fut: ta.Any
4604
+ if self._proc.stdout is not None:
4605
+ stdout_fut = self._read_stream(1)
4606
+ else:
4607
+ stdout_fut = self._noop()
4608
+
4609
+ stderr_fut: ta.Any
4610
+ if self._proc.stderr is not None:
4611
+ stderr_fut = self._read_stream(2)
4612
+ else:
4613
+ stderr_fut = self._noop()
4614
+
4615
+ stdin_res, stdout_res, stderr_res = await asyncio.gather(stdin_fut, stdout_fut, stderr_fut)
4616
+
4617
+ await self._proc.wait()
4618
+
4619
+ return AsyncioProcessCommunicator.Communication(stdout_res, stderr_res)
4620
+
4621
+ async def communicate(
4622
+ self,
4623
+ input: ta.Any = None, # noqa
4624
+ timeout: ta.Optional[float] = None,
4625
+ ) -> Communication:
4626
+ return await asyncio_maybe_timeout(self._communicate(input), timeout)
4627
+
4628
+
4629
+ async def asyncio_subprocess_communicate(
4630
+ proc: asyncio.subprocess.Process,
4631
+ input: ta.Any = None, # noqa
4632
+ timeout: ta.Optional[float] = None,
4633
+ ) -> ta.Tuple[ta.Optional[bytes], ta.Optional[bytes]]:
4634
+ return await AsyncioProcessCommunicator(proc).communicate(input, timeout) # noqa
4635
+
4636
+
4637
+ ##
4638
+
4639
+
4640
+ async def _asyncio_subprocess_check_run(
4641
+ *args: str,
4642
+ input: ta.Any = None, # noqa
4643
+ timeout: ta.Optional[float] = None,
4644
+ **kwargs: ta.Any,
4645
+ ) -> ta.Tuple[ta.Optional[bytes], ta.Optional[bytes]]:
4646
+ args, kwargs = prepare_subprocess_invocation(*args, **kwargs)
4647
+
4648
+ proc: asyncio.subprocess.Process
4649
+ async with asyncio_subprocess_popen(*args, **kwargs) as proc:
4650
+ stdout, stderr = await asyncio_subprocess_communicate(proc, input, timeout)
4651
+
4652
+ if proc.returncode:
4653
+ raise subprocess.CalledProcessError(
4654
+ proc.returncode,
4655
+ args,
4656
+ output=stdout,
4657
+ stderr=stderr,
4658
+ )
4659
+
4660
+ return stdout, stderr
4661
+
4662
+
4663
+ async def asyncio_subprocess_check_call(
4664
+ *args: str,
4665
+ stdout: ta.Any = sys.stderr,
4666
+ input: ta.Any = None, # noqa
4667
+ timeout: ta.Optional[float] = None,
4668
+ **kwargs: ta.Any,
4669
+ ) -> None:
4670
+ _, _ = await _asyncio_subprocess_check_run(
4671
+ *args,
4672
+ stdout=stdout,
4673
+ input=input,
4674
+ timeout=timeout,
4675
+ **kwargs,
4676
+ )
4677
+
4678
+
4679
+ async def asyncio_subprocess_check_output(
4680
+ *args: str,
4681
+ input: ta.Any = None, # noqa
4682
+ timeout: ta.Optional[float] = None,
4683
+ **kwargs: ta.Any,
4684
+ ) -> bytes:
4685
+ stdout, stderr = await _asyncio_subprocess_check_run(
4686
+ *args,
4687
+ stdout=asyncio.subprocess.PIPE,
4688
+ input=input,
4689
+ timeout=timeout,
4690
+ **kwargs,
4691
+ )
4692
+
4693
+ return check_not_none(stdout)
4694
+
4695
+
4696
+ async def asyncio_subprocess_check_output_str(*args: str, **kwargs: ta.Any) -> str:
4697
+ return (await asyncio_subprocess_check_output(*args, **kwargs)).decode().strip()
4698
+
4699
+
4700
+ ##
4701
+
4702
+
4703
+ async def _asyncio_subprocess_try_run(
4704
+ fn: ta.Callable[..., ta.Awaitable[T]],
4705
+ *args: ta.Any,
4706
+ try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
4707
+ **kwargs: ta.Any,
4708
+ ) -> ta.Union[T, Exception]:
4709
+ try:
4710
+ return await fn(*args, **kwargs)
4711
+ except try_exceptions as e: # noqa
4712
+ if log.isEnabledFor(logging.DEBUG):
4713
+ log.exception('command failed')
4714
+ return e
4715
+
4716
+
4717
+ async def asyncio_subprocess_try_call(
4718
+ *args: str,
4719
+ try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
4720
+ **kwargs: ta.Any,
4721
+ ) -> bool:
4722
+ if isinstance(await _asyncio_subprocess_try_run(
4723
+ asyncio_subprocess_check_call,
4724
+ *args,
4725
+ try_exceptions=try_exceptions,
4726
+ **kwargs,
4727
+ ), Exception):
4728
+ return False
4729
+ else:
4730
+ return True
4731
+
4732
+
4733
+ async def asyncio_subprocess_try_output(
4734
+ *args: str,
4735
+ try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
4736
+ **kwargs: ta.Any,
4737
+ ) -> ta.Optional[bytes]:
4738
+ if isinstance(ret := await _asyncio_subprocess_try_run(
4739
+ asyncio_subprocess_check_output,
4740
+ *args,
4741
+ try_exceptions=try_exceptions,
4742
+ **kwargs,
4743
+ ), Exception):
4744
+ return None
4745
+ else:
4746
+ return ret
4747
+
4748
+
4749
+ async def asyncio_subprocess_try_output_str(*args: str, **kwargs: ta.Any) -> ta.Optional[str]:
4750
+ out = await asyncio_subprocess_try_output(*args, **kwargs)
4751
+ return out.decode().strip() if out is not None else None
4752
+
4753
+
4332
4754
  ########################################
4333
4755
  # ../../interp/inspect.py
4334
4756
 
@@ -4363,7 +4785,6 @@ class InterpInspection:
4363
4785
 
4364
4786
 
4365
4787
  class InterpInspector:
4366
-
4367
4788
  def __init__(self) -> None:
4368
4789
  super().__init__()
4369
4790
 
@@ -4403,17 +4824,17 @@ class InterpInspector:
4403
4824
  def running(cls) -> 'InterpInspection':
4404
4825
  return cls._build_inspection(sys.executable, eval(cls._INSPECTION_CODE)) # noqa
4405
4826
 
4406
- def _inspect(self, exe: str) -> InterpInspection:
4407
- output = subprocess_check_output(exe, '-c', f'print({self._INSPECTION_CODE})', quiet=True)
4827
+ async def _inspect(self, exe: str) -> InterpInspection:
4828
+ output = await asyncio_subprocess_check_output(exe, '-c', f'print({self._INSPECTION_CODE})', quiet=True)
4408
4829
  return self._build_inspection(exe, output.decode())
4409
4830
 
4410
- def inspect(self, exe: str) -> ta.Optional[InterpInspection]:
4831
+ async def inspect(self, exe: str) -> ta.Optional[InterpInspection]:
4411
4832
  try:
4412
4833
  return self._cache[exe]
4413
4834
  except KeyError:
4414
4835
  ret: ta.Optional[InterpInspection]
4415
4836
  try:
4416
- ret = self._inspect(exe)
4837
+ ret = await self._inspect(exe)
4417
4838
  except Exception as e: # noqa
4418
4839
  if log.isEnabledFor(logging.DEBUG):
4419
4840
  log.exception('Failed to inspect interp: %s', exe)
@@ -4426,95 +4847,34 @@ INTERP_INSPECTOR = InterpInspector()
4426
4847
 
4427
4848
 
4428
4849
  ########################################
4429
- # ../../interp/providers.py
4850
+ # ../../revisions.py
4430
4851
  """
4431
4852
  TODO:
4432
- - backends
4433
- - local builds
4434
- - deadsnakes?
4435
- - uv
4436
- - loose versions
4853
+ - omlish-lite, move to pyproject/
4854
+ - vendor-lite wheel.wheelfile
4437
4855
  """
4438
4856
 
4439
4857
 
4440
4858
  ##
4441
4859
 
4442
4860
 
4443
- class InterpProvider(abc.ABC):
4444
- name: ta.ClassVar[str]
4445
-
4446
- def __init_subclass__(cls, **kwargs: ta.Any) -> None:
4447
- super().__init_subclass__(**kwargs)
4448
- if abc.ABC not in cls.__bases__ and 'name' not in cls.__dict__:
4449
- sfx = 'InterpProvider'
4450
- if not cls.__name__.endswith(sfx):
4451
- raise NameError(cls)
4452
- setattr(cls, 'name', snake_case(cls.__name__[:-len(sfx)]))
4453
-
4454
- @abc.abstractmethod
4455
- def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
4456
- raise NotImplementedError
4457
-
4458
- @abc.abstractmethod
4459
- def get_installed_version(self, version: InterpVersion) -> Interp:
4460
- raise NotImplementedError
4461
-
4462
- def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
4463
- return []
4464
-
4465
- def install_version(self, version: InterpVersion) -> Interp:
4466
- raise TypeError
4467
-
4861
+ class GitRevisionAdder:
4862
+ def __init__(
4863
+ self,
4864
+ revision: ta.Optional[str] = None,
4865
+ output_suffix: ta.Optional[str] = None,
4866
+ ) -> None:
4867
+ super().__init__()
4868
+ self._given_revision = revision
4869
+ self._output_suffix = output_suffix
4468
4870
 
4469
- ##
4871
+ @cached_nullary
4872
+ def revision(self) -> str:
4873
+ if self._given_revision is not None:
4874
+ return self._given_revision
4875
+ return check_non_empty_str(get_git_revision())
4470
4876
 
4471
-
4472
- class RunningInterpProvider(InterpProvider):
4473
- @cached_nullary
4474
- def version(self) -> InterpVersion:
4475
- return InterpInspector.running().iv
4476
-
4477
- def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
4478
- return [self.version()]
4479
-
4480
- def get_installed_version(self, version: InterpVersion) -> Interp:
4481
- if version != self.version():
4482
- raise KeyError(version)
4483
- return Interp(
4484
- exe=sys.executable,
4485
- version=self.version(),
4486
- )
4487
-
4488
-
4489
- ########################################
4490
- # ../../revisions.py
4491
- """
4492
- TODO:
4493
- - omlish-lite, move to pyproject/
4494
- - vendor-lite wheel.wheelfile
4495
- """
4496
-
4497
-
4498
- ##
4499
-
4500
-
4501
- class GitRevisionAdder:
4502
- def __init__(
4503
- self,
4504
- revision: ta.Optional[str] = None,
4505
- output_suffix: ta.Optional[str] = None,
4506
- ) -> None:
4507
- super().__init__()
4508
- self._given_revision = revision
4509
- self._output_suffix = output_suffix
4510
-
4511
- @cached_nullary
4512
- def revision(self) -> str:
4513
- if self._given_revision is not None:
4514
- return self._given_revision
4515
- return check_non_empty_str(get_git_revision())
4516
-
4517
- REVISION_ATTR = '__revision__'
4877
+ REVISION_ATTR = '__revision__'
4518
4878
 
4519
4879
  def add_to_contents(self, dct: ta.Dict[str, bytes]) -> bool:
4520
4880
  changed = False
@@ -4607,1120 +4967,1179 @@ class GitRevisionAdder:
4607
4967
 
4608
4968
 
4609
4969
  ########################################
4610
- # ../../interp/pyenv.py
4970
+ # ../../interp/providers.py
4611
4971
  """
4612
4972
  TODO:
4613
- - custom tags
4614
- - 'aliases'
4615
- - https://github.com/pyenv/pyenv/pull/2966
4616
- - https://github.com/pyenv/pyenv/issues/218 (lol)
4617
- - probably need custom (temp?) definition file
4618
- - *or* python-build directly just into the versions dir?
4619
- - optionally install / upgrade pyenv itself
4620
- - new vers dont need these custom mac opts, only run on old vers
4973
+ - backends
4974
+ - local builds
4975
+ - deadsnakes?
4976
+ - uv
4977
+ - loose versions
4621
4978
  """
4622
4979
 
4623
4980
 
4624
4981
  ##
4625
4982
 
4626
4983
 
4627
- class Pyenv:
4984
+ class InterpProvider(abc.ABC):
4985
+ name: ta.ClassVar[str]
4628
4986
 
4629
- def __init__(
4630
- self,
4631
- *,
4632
- root: ta.Optional[str] = None,
4633
- ) -> None:
4634
- if root is not None and not (isinstance(root, str) and root):
4635
- raise ValueError(f'pyenv_root: {root!r}')
4987
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
4988
+ super().__init_subclass__(**kwargs)
4989
+ if abc.ABC not in cls.__bases__ and 'name' not in cls.__dict__:
4990
+ sfx = 'InterpProvider'
4991
+ if not cls.__name__.endswith(sfx):
4992
+ raise NameError(cls)
4993
+ setattr(cls, 'name', snake_case(cls.__name__[:-len(sfx)]))
4636
4994
 
4637
- super().__init__()
4995
+ @abc.abstractmethod
4996
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Awaitable[ta.Sequence[InterpVersion]]:
4997
+ raise NotImplementedError
4638
4998
 
4639
- self._root_kw = root
4999
+ @abc.abstractmethod
5000
+ def get_installed_version(self, version: InterpVersion) -> ta.Awaitable[Interp]:
5001
+ raise NotImplementedError
4640
5002
 
4641
- @cached_nullary
4642
- def root(self) -> ta.Optional[str]:
4643
- if self._root_kw is not None:
4644
- return self._root_kw
5003
+ async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5004
+ return []
4645
5005
 
4646
- if shutil.which('pyenv'):
4647
- return subprocess_check_output_str('pyenv', 'root')
5006
+ async def install_version(self, version: InterpVersion) -> Interp:
5007
+ raise TypeError
4648
5008
 
4649
- d = os.path.expanduser('~/.pyenv')
4650
- if os.path.isdir(d) and os.path.isfile(os.path.join(d, 'bin', 'pyenv')):
4651
- return d
4652
5009
 
4653
- return None
5010
+ ##
4654
5011
 
5012
+
5013
+ class RunningInterpProvider(InterpProvider):
4655
5014
  @cached_nullary
4656
- def exe(self) -> str:
4657
- return os.path.join(check_not_none(self.root()), 'bin', 'pyenv')
5015
+ def version(self) -> InterpVersion:
5016
+ return InterpInspector.running().iv
4658
5017
 
4659
- def version_exes(self) -> ta.List[ta.Tuple[str, str]]:
4660
- if (root := self.root()) is None:
4661
- return []
4662
- ret = []
4663
- vp = os.path.join(root, 'versions')
4664
- if os.path.isdir(vp):
4665
- for dn in os.listdir(vp):
4666
- ep = os.path.join(vp, dn, 'bin', 'python')
4667
- if not os.path.isfile(ep):
4668
- continue
4669
- ret.append((dn, ep))
4670
- return ret
5018
+ async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5019
+ return [self.version()]
4671
5020
 
4672
- def installable_versions(self) -> ta.List[str]:
4673
- if self.root() is None:
4674
- return []
4675
- ret = []
4676
- s = subprocess_check_output_str(self.exe(), 'install', '--list')
4677
- for l in s.splitlines():
4678
- if not l.startswith(' '):
4679
- continue
4680
- l = l.strip()
4681
- if not l:
4682
- continue
4683
- ret.append(l)
4684
- return ret
5021
+ async def get_installed_version(self, version: InterpVersion) -> Interp:
5022
+ if version != self.version():
5023
+ raise KeyError(version)
5024
+ return Interp(
5025
+ exe=sys.executable,
5026
+ version=self.version(),
5027
+ )
4685
5028
 
4686
- def update(self) -> bool:
4687
- if (root := self.root()) is None:
4688
- return False
4689
- if not os.path.isdir(os.path.join(root, '.git')):
4690
- return False
4691
- subprocess_check_call('git', 'pull', cwd=root)
4692
- return True
4693
5029
 
5030
+ ########################################
5031
+ # ../pkg.py
5032
+ """
5033
+ TODO:
5034
+ - ext scanning
5035
+ - __revision__
5036
+ - entry_points
4694
5037
 
4695
- ##
5038
+ ** NOTE **
5039
+ setuptools now (2024/09/02) has experimental support for extensions in pure pyproject.toml - but we still want a
5040
+ separate '-cext' package
5041
+ https://setuptools.pypa.io/en/latest/userguide/ext_modules.html
5042
+ https://github.com/pypa/setuptools/commit/1a9d87308dc0d8aabeaae0dce989b35dfb7699f0#diff-61d113525e9cc93565799a4bb8b34a68e2945b8a3f7d90c81380614a4ea39542R7-R8
4696
5043
 
5044
+ --
4697
5045
 
4698
- @dc.dataclass(frozen=True)
4699
- class PyenvInstallOpts:
4700
- opts: ta.Sequence[str] = ()
4701
- conf_opts: ta.Sequence[str] = ()
4702
- cflags: ta.Sequence[str] = ()
4703
- ldflags: ta.Sequence[str] = ()
4704
- env: ta.Mapping[str, str] = dc.field(default_factory=dict)
5046
+ https://setuptools.pypa.io/en/latest/references/keywords.html
5047
+ https://packaging.python.org/en/latest/specifications/pyproject-toml
4705
5048
 
4706
- def merge(self, *others: 'PyenvInstallOpts') -> 'PyenvInstallOpts':
4707
- return PyenvInstallOpts(
4708
- opts=list(itertools.chain.from_iterable(o.opts for o in [self, *others])),
4709
- conf_opts=list(itertools.chain.from_iterable(o.conf_opts for o in [self, *others])),
4710
- cflags=list(itertools.chain.from_iterable(o.cflags for o in [self, *others])),
4711
- ldflags=list(itertools.chain.from_iterable(o.ldflags for o in [self, *others])),
4712
- env=dict(itertools.chain.from_iterable(o.env.items() for o in [self, *others])),
4713
- )
5049
+ How to build a C extension in keeping with PEP 517, i.e. with pyproject.toml instead of setup.py?
5050
+ https://stackoverflow.com/a/66479252
4714
5051
 
5052
+ https://github.com/pypa/sampleproject/blob/db5806e0a3204034c51b1c00dde7d5eb3fa2532e/setup.py
4715
5053
 
4716
- # TODO: https://github.com/pyenv/pyenv/blob/master/plugins/python-build/README.md#building-for-maximum-performance
4717
- DEFAULT_PYENV_INSTALL_OPTS = PyenvInstallOpts(
4718
- opts=[
4719
- '-s',
4720
- '-v',
4721
- '-k',
4722
- ],
4723
- conf_opts=[
4724
- # FIXME: breaks on mac for older py's
4725
- '--enable-loadable-sqlite-extensions',
5054
+ https://pip.pypa.io/en/stable/cli/pip_install/#vcs-support
5055
+ vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir
5056
+ 'git+https://github.com/wrmsr/omlish@master#subdirectory=.pip/omlish'
5057
+ """ # noqa
4726
5058
 
4727
- # '--enable-shared',
4728
5059
 
4729
- '--enable-optimizations',
4730
- '--with-lto',
5060
+ #
4731
5061
 
4732
- # '--enable-profiling', # ?
4733
5062
 
4734
- # '--enable-ipv6', # ?
4735
- ],
4736
- cflags=[
4737
- # '-march=native',
4738
- # '-mtune=native',
4739
- ],
4740
- )
5063
+ class BasePyprojectPackageGenerator(abc.ABC):
5064
+ def __init__(
5065
+ self,
5066
+ dir_name: str,
5067
+ pkgs_root: str,
5068
+ *,
5069
+ pkg_suffix: str = '',
5070
+ ) -> None:
5071
+ super().__init__()
5072
+ self._dir_name = dir_name
5073
+ self._pkgs_root = pkgs_root
5074
+ self._pkg_suffix = pkg_suffix
4741
5075
 
4742
- DEBUG_PYENV_INSTALL_OPTS = PyenvInstallOpts(opts=['-g'])
5076
+ #
4743
5077
 
4744
- THREADED_PYENV_INSTALL_OPTS = PyenvInstallOpts(conf_opts=['--disable-gil'])
5078
+ @cached_nullary
5079
+ def about(self) -> types.ModuleType:
5080
+ return importlib.import_module(f'{self._dir_name}.__about__')
4745
5081
 
5082
+ #
4746
5083
 
4747
- #
5084
+ @cached_nullary
5085
+ def _pkg_dir(self) -> str:
5086
+ pkg_dir: str = os.path.join(self._pkgs_root, self._dir_name + self._pkg_suffix)
5087
+ if os.path.isdir(pkg_dir):
5088
+ shutil.rmtree(pkg_dir)
5089
+ os.makedirs(pkg_dir)
5090
+ return pkg_dir
4748
5091
 
5092
+ #
4749
5093
 
4750
- class PyenvInstallOptsProvider(abc.ABC):
4751
- @abc.abstractmethod
4752
- def opts(self) -> PyenvInstallOpts:
4753
- raise NotImplementedError
5094
+ _GIT_IGNORE: ta.Sequence[str] = [
5095
+ '/*.egg-info/',
5096
+ '/dist',
5097
+ ]
4754
5098
 
5099
+ def _write_git_ignore(self) -> None:
5100
+ with open(os.path.join(self._pkg_dir(), '.gitignore'), 'w') as f:
5101
+ f.write('\n'.join(self._GIT_IGNORE))
4755
5102
 
4756
- class LinuxPyenvInstallOpts(PyenvInstallOptsProvider):
4757
- def opts(self) -> PyenvInstallOpts:
4758
- return PyenvInstallOpts()
5103
+ #
4759
5104
 
5105
+ def _symlink_source_dir(self) -> None:
5106
+ os.symlink(
5107
+ os.path.relpath(self._dir_name, self._pkg_dir()),
5108
+ os.path.join(self._pkg_dir(), self._dir_name),
5109
+ )
4760
5110
 
4761
- class DarwinPyenvInstallOpts(PyenvInstallOptsProvider):
5111
+ #
4762
5112
 
4763
5113
  @cached_nullary
4764
- def framework_opts(self) -> PyenvInstallOpts:
4765
- return PyenvInstallOpts(conf_opts=['--enable-framework'])
5114
+ def project_cls(self) -> type:
5115
+ return self.about().Project
4766
5116
 
4767
5117
  @cached_nullary
4768
- def has_brew(self) -> bool:
4769
- return shutil.which('brew') is not None
4770
-
4771
- BREW_DEPS: ta.Sequence[str] = [
4772
- 'openssl',
4773
- 'readline',
4774
- 'sqlite3',
4775
- 'zlib',
4776
- ]
5118
+ def setuptools_cls(self) -> type:
5119
+ return self.about().Setuptools
4777
5120
 
4778
- @cached_nullary
4779
- def brew_deps_opts(self) -> PyenvInstallOpts:
4780
- cflags = []
4781
- ldflags = []
4782
- for dep in self.BREW_DEPS:
4783
- dep_prefix = subprocess_check_output_str('brew', '--prefix', dep)
4784
- cflags.append(f'-I{dep_prefix}/include')
4785
- ldflags.append(f'-L{dep_prefix}/lib')
4786
- return PyenvInstallOpts(
4787
- cflags=cflags,
4788
- ldflags=ldflags,
5121
+ @staticmethod
5122
+ def _build_cls_dct(cls: type) -> ta.Dict[str, ta.Any]: # noqa
5123
+ dct = {}
5124
+ for b in reversed(cls.__mro__):
5125
+ for k, v in b.__dict__.items():
5126
+ if k.startswith('_'):
5127
+ continue
5128
+ dct[k] = v
5129
+ return dct
5130
+
5131
+ @staticmethod
5132
+ def _move_dict_key(
5133
+ sd: ta.Dict[str, ta.Any],
5134
+ sk: str,
5135
+ dd: ta.Dict[str, ta.Any],
5136
+ dk: str,
5137
+ ) -> None:
5138
+ if sk in sd:
5139
+ dd[dk] = sd.pop(sk)
5140
+
5141
+ @dc.dataclass(frozen=True)
5142
+ class Specs:
5143
+ pyproject: ta.Dict[str, ta.Any]
5144
+ setuptools: ta.Dict[str, ta.Any]
5145
+
5146
+ def build_specs(self) -> Specs:
5147
+ return self.Specs(
5148
+ self._build_cls_dct(self.project_cls()),
5149
+ self._build_cls_dct(self.setuptools_cls()),
4789
5150
  )
4790
5151
 
5152
+ #
5153
+
5154
+ class _PkgData(ta.NamedTuple):
5155
+ inc: ta.List[str]
5156
+ exc: ta.List[str]
5157
+
4791
5158
  @cached_nullary
4792
- def brew_tcl_opts(self) -> PyenvInstallOpts:
4793
- if subprocess_try_output('brew', '--prefix', 'tcl-tk') is None:
4794
- return PyenvInstallOpts()
5159
+ def _collect_pkg_data(self) -> _PkgData:
5160
+ inc: ta.List[str] = []
5161
+ exc: ta.List[str] = []
4795
5162
 
4796
- tcl_tk_prefix = subprocess_check_output_str('brew', '--prefix', 'tcl-tk')
4797
- tcl_tk_ver_str = subprocess_check_output_str('brew', 'ls', '--versions', 'tcl-tk')
4798
- tcl_tk_ver = '.'.join(tcl_tk_ver_str.split()[1].split('.')[:2])
5163
+ for p, ds, fs in os.walk(self._dir_name): # noqa
5164
+ for f in fs:
5165
+ if f != '.pkgdata':
5166
+ continue
5167
+ rp = os.path.relpath(p, self._dir_name)
5168
+ log.info('Found pkgdata %s for pkg %s', rp, self._dir_name)
5169
+ with open(os.path.join(p, f)) as fo:
5170
+ src = fo.read()
5171
+ for l in src.splitlines():
5172
+ if not (l := l.strip()):
5173
+ continue
5174
+ if l.startswith('!'):
5175
+ exc.append(os.path.join(rp, l[1:]))
5176
+ else:
5177
+ inc.append(os.path.join(rp, l))
4799
5178
 
4800
- return PyenvInstallOpts(conf_opts=[
4801
- f"--with-tcltk-includes='-I{tcl_tk_prefix}/include'",
4802
- f"--with-tcltk-libs='-L{tcl_tk_prefix}/lib -ltcl{tcl_tk_ver} -ltk{tcl_tk_ver}'",
4803
- ])
5179
+ return self._PkgData(inc, exc)
4804
5180
 
4805
- # @cached_nullary
4806
- # def brew_ssl_opts(self) -> PyenvInstallOpts:
4807
- # pkg_config_path = subprocess_check_output_str('brew', '--prefix', 'openssl')
4808
- # if 'PKG_CONFIG_PATH' in os.environ:
4809
- # pkg_config_path += ':' + os.environ['PKG_CONFIG_PATH']
4810
- # return PyenvInstallOpts(env={'PKG_CONFIG_PATH': pkg_config_path})
5181
+ #
4811
5182
 
4812
- def opts(self) -> PyenvInstallOpts:
4813
- return PyenvInstallOpts().merge(
4814
- self.framework_opts(),
4815
- self.brew_deps_opts(),
4816
- self.brew_tcl_opts(),
4817
- # self.brew_ssl_opts(),
4818
- )
5183
+ @abc.abstractmethod
5184
+ def _write_file_contents(self) -> None:
5185
+ raise NotImplementedError
4819
5186
 
5187
+ #
4820
5188
 
4821
- PLATFORM_PYENV_INSTALL_OPTS: ta.Dict[str, PyenvInstallOptsProvider] = {
4822
- 'darwin': DarwinPyenvInstallOpts(),
4823
- 'linux': LinuxPyenvInstallOpts(),
4824
- }
5189
+ _STANDARD_FILES: ta.Sequence[str] = [
5190
+ 'LICENSE',
5191
+ 'README.rst',
5192
+ ]
5193
+
5194
+ def _symlink_standard_files(self) -> None:
5195
+ for fn in self._STANDARD_FILES:
5196
+ if os.path.exists(fn):
5197
+ os.symlink(os.path.relpath(fn, self._pkg_dir()), os.path.join(self._pkg_dir(), fn))
4825
5198
 
5199
+ #
4826
5200
 
4827
- ##
5201
+ def children(self) -> ta.Sequence['BasePyprojectPackageGenerator']:
5202
+ return []
4828
5203
 
5204
+ #
4829
5205
 
4830
- class PyenvVersionInstaller:
4831
- """
4832
- Messy: can install freethreaded build with a 't' suffixed version str _or_ by THREADED_PYENV_INSTALL_OPTS - need
4833
- latter to build custom interp with ft, need former to use canned / blessed interps. Muh.
4834
- """
5206
+ def gen(self) -> str:
5207
+ log.info('Generating pyproject package: %s -> %s (%s)', self._dir_name, self._pkgs_root, self._pkg_suffix)
4835
5208
 
4836
- def __init__(
5209
+ self._pkg_dir()
5210
+ self._write_git_ignore()
5211
+ self._symlink_source_dir()
5212
+ self._write_file_contents()
5213
+ self._symlink_standard_files()
5214
+
5215
+ return self._pkg_dir()
5216
+
5217
+ #
5218
+
5219
+ @dc.dataclass(frozen=True)
5220
+ class BuildOpts:
5221
+ add_revision: bool = False
5222
+ test: bool = False
5223
+
5224
+ def build(
4837
5225
  self,
4838
- version: str,
4839
- opts: ta.Optional[PyenvInstallOpts] = None,
4840
- interp_opts: InterpOpts = InterpOpts(),
4841
- *,
4842
- install_name: ta.Optional[str] = None,
4843
- no_default_opts: bool = False,
4844
- pyenv: Pyenv = Pyenv(),
5226
+ output_dir: ta.Optional[str] = None,
5227
+ opts: BuildOpts = BuildOpts(),
4845
5228
  ) -> None:
4846
- super().__init__()
5229
+ subprocess_check_call(
5230
+ sys.executable,
5231
+ '-m',
5232
+ 'build',
5233
+ cwd=self._pkg_dir(),
5234
+ )
4847
5235
 
4848
- if no_default_opts:
4849
- if opts is None:
4850
- opts = PyenvInstallOpts()
4851
- else:
4852
- lst = [opts if opts is not None else DEFAULT_PYENV_INSTALL_OPTS]
4853
- if interp_opts.debug:
4854
- lst.append(DEBUG_PYENV_INSTALL_OPTS)
4855
- if interp_opts.threaded:
4856
- lst.append(THREADED_PYENV_INSTALL_OPTS)
4857
- lst.append(PLATFORM_PYENV_INSTALL_OPTS[sys.platform].opts())
4858
- opts = PyenvInstallOpts().merge(*lst)
5236
+ dist_dir = os.path.join(self._pkg_dir(), 'dist')
4859
5237
 
4860
- self._version = version
4861
- self._opts = opts
4862
- self._interp_opts = interp_opts
4863
- self._given_install_name = install_name
5238
+ if opts.add_revision:
5239
+ GitRevisionAdder().add_to(dist_dir)
4864
5240
 
4865
- self._no_default_opts = no_default_opts
4866
- self._pyenv = pyenv
5241
+ if opts.test:
5242
+ for fn in os.listdir(dist_dir):
5243
+ tmp_dir = tempfile.mkdtemp()
4867
5244
 
4868
- @property
4869
- def version(self) -> str:
4870
- return self._version
5245
+ subprocess_check_call(
5246
+ sys.executable,
5247
+ '-m', 'venv',
5248
+ 'test-install',
5249
+ cwd=tmp_dir,
5250
+ )
4871
5251
 
4872
- @property
4873
- def opts(self) -> PyenvInstallOpts:
4874
- return self._opts
5252
+ subprocess_check_call(
5253
+ os.path.join(tmp_dir, 'test-install', 'bin', 'python3'),
5254
+ '-m', 'pip',
5255
+ 'install',
5256
+ os.path.abspath(os.path.join(dist_dir, fn)),
5257
+ cwd=tmp_dir,
5258
+ )
4875
5259
 
4876
- @cached_nullary
4877
- def install_name(self) -> str:
4878
- if self._given_install_name is not None:
4879
- return self._given_install_name
4880
- return self._version + ('-debug' if self._interp_opts.debug else '')
5260
+ if output_dir is not None:
5261
+ for fn in os.listdir(dist_dir):
5262
+ shutil.copyfile(os.path.join(dist_dir, fn), os.path.join(output_dir, fn))
4881
5263
 
4882
- @cached_nullary
4883
- def install_dir(self) -> str:
4884
- return str(os.path.join(check_not_none(self._pyenv.root()), 'versions', self.install_name()))
4885
5264
 
4886
- @cached_nullary
4887
- def install(self) -> str:
4888
- env = {**os.environ, **self._opts.env}
4889
- for k, l in [
4890
- ('CFLAGS', self._opts.cflags),
4891
- ('LDFLAGS', self._opts.ldflags),
4892
- ('PYTHON_CONFIGURE_OPTS', self._opts.conf_opts),
4893
- ]:
4894
- v = ' '.join(l)
4895
- if k in os.environ:
4896
- v += ' ' + os.environ[k]
4897
- env[k] = v
5265
+ #
4898
5266
 
4899
- conf_args = [
4900
- *self._opts.opts,
4901
- self._version,
4902
- ]
4903
5267
 
4904
- if self._given_install_name is not None:
4905
- full_args = [
4906
- os.path.join(check_not_none(self._pyenv.root()), 'plugins', 'python-build', 'bin', 'python-build'),
4907
- *conf_args,
4908
- self.install_dir(),
4909
- ]
4910
- else:
4911
- full_args = [
4912
- self._pyenv.exe(),
4913
- 'install',
4914
- *conf_args,
4915
- ]
5268
+ class PyprojectPackageGenerator(BasePyprojectPackageGenerator):
4916
5269
 
4917
- subprocess_check_call(
4918
- *full_args,
4919
- env=env,
4920
- )
5270
+ #
4921
5271
 
4922
- exe = os.path.join(self.install_dir(), 'bin', 'python')
4923
- if not os.path.isfile(exe):
4924
- raise RuntimeError(f'Interpreter not found: {exe}')
4925
- return exe
5272
+ @dc.dataclass(frozen=True)
5273
+ class FileContents:
5274
+ pyproject_dct: ta.Mapping[str, ta.Any]
5275
+ manifest_in: ta.Optional[ta.Sequence[str]]
4926
5276
 
5277
+ @cached_nullary
5278
+ def file_contents(self) -> FileContents:
5279
+ specs = self.build_specs()
4927
5280
 
4928
- ##
5281
+ #
4929
5282
 
5283
+ pyp_dct = {}
4930
5284
 
4931
- class PyenvInterpProvider(InterpProvider):
5285
+ pyp_dct['build-system'] = {
5286
+ 'requires': ['setuptools'],
5287
+ 'build-backend': 'setuptools.build_meta',
5288
+ }
4932
5289
 
4933
- def __init__(
4934
- self,
4935
- pyenv: Pyenv = Pyenv(),
5290
+ prj = specs.pyproject
5291
+ prj['name'] += self._pkg_suffix
4936
5292
 
4937
- inspect: bool = False,
4938
- inspector: InterpInspector = INTERP_INSPECTOR,
5293
+ pyp_dct['project'] = prj
4939
5294
 
4940
- *,
5295
+ self._move_dict_key(prj, 'optional_dependencies', pyp_dct, extrask := 'project.optional-dependencies')
5296
+ if (extras := pyp_dct.get(extrask)):
5297
+ pyp_dct[extrask] = {
5298
+ 'all': [
5299
+ e
5300
+ for lst in extras.values()
5301
+ for e in lst
5302
+ ],
5303
+ **extras,
5304
+ }
4941
5305
 
4942
- try_update: bool = False,
4943
- ) -> None:
4944
- super().__init__()
5306
+ if (eps := prj.pop('entry_points', None)):
5307
+ pyp_dct['project.entry-points'] = {TomlWriter.Literal(f"'{k}'"): v for k, v in eps.items()} # type: ignore # noqa
4945
5308
 
4946
- self._pyenv = pyenv
5309
+ if (scs := prj.pop('scripts', None)):
5310
+ pyp_dct['project.scripts'] = scs
4947
5311
 
4948
- self._inspect = inspect
4949
- self._inspector = inspector
5312
+ prj.pop('cli_scripts', None)
4950
5313
 
4951
- self._try_update = try_update
5314
+ ##
4952
5315
 
4953
- #
5316
+ st = dict(specs.setuptools)
5317
+ pyp_dct['tool.setuptools'] = st
4954
5318
 
4955
- @staticmethod
4956
- def guess_version(s: str) -> ta.Optional[InterpVersion]:
4957
- def strip_sfx(s: str, sfx: str) -> ta.Tuple[str, bool]:
4958
- if s.endswith(sfx):
4959
- return s[:-len(sfx)], True
4960
- return s, False
4961
- ok = {}
4962
- s, ok['debug'] = strip_sfx(s, '-debug')
4963
- s, ok['threaded'] = strip_sfx(s, 't')
4964
- try:
4965
- v = Version(s)
4966
- except InvalidVersion:
4967
- return None
4968
- return InterpVersion(v, InterpOpts(**ok))
5319
+ st.pop('cexts', None)
4969
5320
 
4970
- class Installed(ta.NamedTuple):
4971
- name: str
4972
- exe: str
4973
- version: InterpVersion
5321
+ #
4974
5322
 
4975
- def _make_installed(self, vn: str, ep: str) -> ta.Optional[Installed]:
4976
- iv: ta.Optional[InterpVersion]
4977
- if self._inspect:
4978
- try:
4979
- iv = check_not_none(self._inspector.inspect(ep)).iv
4980
- except Exception as e: # noqa
4981
- return None
4982
- else:
4983
- iv = self.guess_version(vn)
4984
- if iv is None:
4985
- return None
4986
- return PyenvInterpProvider.Installed(
4987
- name=vn,
4988
- exe=ep,
4989
- version=iv,
4990
- )
5323
+ # TODO: default
5324
+ # find_packages = {
5325
+ # 'include': [Project.name, f'{Project.name}.*'],
5326
+ # 'exclude': [*SetuptoolsBase.find_packages['exclude']],
5327
+ # }
4991
5328
 
4992
- def installed(self) -> ta.Sequence[Installed]:
4993
- ret: ta.List[PyenvInterpProvider.Installed] = []
4994
- for vn, ep in self._pyenv.version_exes():
4995
- if (i := self._make_installed(vn, ep)) is None:
4996
- log.debug('Invalid pyenv version: %s', vn)
4997
- continue
4998
- ret.append(i)
4999
- return ret
5329
+ fp = dict(st.pop('find_packages', {}))
5000
5330
 
5001
- #
5331
+ pyp_dct['tool.setuptools.packages.find'] = fp
5002
5332
 
5003
- def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5004
- return [i.version for i in self.installed()]
5333
+ #
5005
5334
 
5006
- def get_installed_version(self, version: InterpVersion) -> Interp:
5007
- for i in self.installed():
5008
- if i.version == version:
5009
- return Interp(
5010
- exe=i.exe,
5011
- version=i.version,
5012
- )
5013
- raise KeyError(version)
5335
+ # TODO: default
5336
+ # package_data = {
5337
+ # '*': [
5338
+ # '*.c',
5339
+ # '*.cc',
5340
+ # '*.h',
5341
+ # '.manifests.json',
5342
+ # 'LICENSE',
5343
+ # ],
5344
+ # }
5014
5345
 
5015
- #
5346
+ pd = dict(st.pop('package_data', {}))
5347
+ epd = dict(st.pop('exclude_package_data', {}))
5016
5348
 
5017
- def _get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5018
- lst = []
5349
+ cpd = self._collect_pkg_data()
5350
+ if cpd.inc:
5351
+ pd['*'] = [*pd.get('*', []), *sorted(set(cpd.inc))]
5352
+ if cpd.exc:
5353
+ epd['*'] = [*epd.get('*', []), *sorted(set(cpd.exc))]
5019
5354
 
5020
- for vs in self._pyenv.installable_versions():
5021
- if (iv := self.guess_version(vs)) is None:
5022
- continue
5023
- if iv.opts.debug:
5024
- raise Exception('Pyenv installable versions not expected to have debug suffix')
5025
- for d in [False, True]:
5026
- lst.append(dc.replace(iv, opts=dc.replace(iv.opts, debug=d)))
5355
+ if pd:
5356
+ pyp_dct['tool.setuptools.package-data'] = pd
5357
+ if epd:
5358
+ pyp_dct['tool.setuptools.exclude-package-data'] = epd
5027
5359
 
5028
- return lst
5360
+ #
5361
+
5362
+ # TODO: default
5363
+ # manifest_in = [
5364
+ # 'global-exclude **/conftest.py',
5365
+ # ]
5029
5366
 
5030
- def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5031
- lst = self._get_installable_versions(spec)
5367
+ mani_in = st.pop('manifest_in', None)
5032
5368
 
5033
- if self._try_update and not any(v in spec for v in lst):
5034
- if self._pyenv.update():
5035
- lst = self._get_installable_versions(spec)
5369
+ #
5036
5370
 
5037
- return lst
5371
+ return self.FileContents(
5372
+ pyp_dct,
5373
+ mani_in,
5374
+ )
5038
5375
 
5039
- def install_version(self, version: InterpVersion) -> Interp:
5040
- inst_version = str(version.version)
5041
- inst_opts = version.opts
5042
- if inst_opts.threaded:
5043
- inst_version += 't'
5044
- inst_opts = dc.replace(inst_opts, threaded=False)
5376
+ def _write_file_contents(self) -> None:
5377
+ fc = self.file_contents()
5378
+
5379
+ with open(os.path.join(self._pkg_dir(), 'pyproject.toml'), 'w') as f:
5380
+ TomlWriter(f).write_root(fc.pyproject_dct)
5045
5381
 
5046
- installer = PyenvVersionInstaller(
5047
- inst_version,
5048
- interp_opts=inst_opts,
5049
- )
5382
+ if fc.manifest_in:
5383
+ with open(os.path.join(self._pkg_dir(), 'MANIFEST.in'), 'w') as f:
5384
+ f.write('\n'.join(fc.manifest_in)) # noqa
5050
5385
 
5051
- exe = installer.install()
5052
- return Interp(exe, version)
5386
+ #
5053
5387
 
5388
+ @cached_nullary
5389
+ def children(self) -> ta.Sequence[BasePyprojectPackageGenerator]:
5390
+ out: ta.List[BasePyprojectPackageGenerator] = []
5054
5391
 
5055
- ########################################
5056
- # ../../interp/system.py
5057
- """
5058
- TODO:
5059
- - python, python3, python3.12, ...
5060
- - check if path py's are venvs: sys.prefix != sys.base_prefix
5061
- """
5392
+ if self.build_specs().setuptools.get('cexts'):
5393
+ out.append(_PyprojectCextPackageGenerator(
5394
+ self._dir_name,
5395
+ self._pkgs_root,
5396
+ pkg_suffix='-cext',
5397
+ ))
5062
5398
 
5399
+ if self.build_specs().pyproject.get('cli_scripts'):
5400
+ out.append(_PyprojectCliPackageGenerator(
5401
+ self._dir_name,
5402
+ self._pkgs_root,
5403
+ pkg_suffix='-cli',
5404
+ ))
5063
5405
 
5064
- ##
5406
+ return out
5065
5407
 
5066
5408
 
5067
- @dc.dataclass(frozen=True)
5068
- class SystemInterpProvider(InterpProvider):
5069
- cmd: str = 'python3'
5070
- path: ta.Optional[str] = None
5409
+ #
5071
5410
 
5072
- inspect: bool = False
5073
- inspector: InterpInspector = INTERP_INSPECTOR
5411
+
5412
+ class _PyprojectCextPackageGenerator(BasePyprojectPackageGenerator):
5074
5413
 
5075
5414
  #
5076
5415
 
5077
- @staticmethod
5078
- def _re_which(
5079
- pat: re.Pattern,
5080
- *,
5081
- mode: int = os.F_OK | os.X_OK,
5082
- path: ta.Optional[str] = None,
5083
- ) -> ta.List[str]:
5084
- if path is None:
5085
- path = os.environ.get('PATH', None)
5086
- if path is None:
5087
- try:
5088
- path = os.confstr('CS_PATH')
5089
- except (AttributeError, ValueError):
5090
- path = os.defpath
5416
+ @cached_nullary
5417
+ def find_cext_srcs(self) -> ta.Sequence[str]:
5418
+ return sorted(find_magic_files(
5419
+ CextMagic.STYLE,
5420
+ [self._dir_name],
5421
+ keys=[CextMagic.KEY],
5422
+ ))
5091
5423
 
5092
- if not path:
5093
- return []
5424
+ #
5094
5425
 
5095
- path = os.fsdecode(path)
5096
- pathlst = path.split(os.pathsep)
5426
+ @dc.dataclass(frozen=True)
5427
+ class FileContents:
5428
+ pyproject_dct: ta.Mapping[str, ta.Any]
5429
+ setup_py: str
5097
5430
 
5098
- def _access_check(fn: str, mode: int) -> bool:
5099
- return os.path.exists(fn) and os.access(fn, mode)
5431
+ @cached_nullary
5432
+ def file_contents(self) -> FileContents:
5433
+ specs = self.build_specs()
5100
5434
 
5101
- out = []
5102
- seen = set()
5103
- for d in pathlst:
5104
- normdir = os.path.normcase(d)
5105
- if normdir not in seen:
5106
- seen.add(normdir)
5107
- if not _access_check(normdir, mode):
5108
- continue
5109
- for thefile in os.listdir(d):
5110
- name = os.path.join(d, thefile)
5111
- if not (
5112
- os.path.isfile(name) and
5113
- pat.fullmatch(thefile) and
5114
- _access_check(name, mode)
5115
- ):
5116
- continue
5117
- out.append(name)
5435
+ #
5118
5436
 
5119
- return out
5437
+ pyp_dct = {}
5120
5438
 
5121
- @cached_nullary
5122
- def exes(self) -> ta.List[str]:
5123
- return self._re_which(
5124
- re.compile(r'python3(\.\d+)?'),
5125
- path=self.path,
5126
- )
5439
+ pyp_dct['build-system'] = {
5440
+ 'requires': ['setuptools'],
5441
+ 'build-backend': 'setuptools.build_meta',
5442
+ }
5127
5443
 
5128
- #
5444
+ prj = specs.pyproject
5445
+ prj['dependencies'] = [f'{prj["name"]} == {prj["version"]}']
5446
+ prj['name'] += self._pkg_suffix
5447
+ for k in [
5448
+ 'optional_dependencies',
5449
+ 'entry_points',
5450
+ 'scripts',
5451
+ 'cli_scripts',
5452
+ ]:
5453
+ prj.pop(k, None)
5129
5454
 
5130
- def get_exe_version(self, exe: str) -> ta.Optional[InterpVersion]:
5131
- if not self.inspect:
5132
- s = os.path.basename(exe)
5133
- if s.startswith('python'):
5134
- s = s[len('python'):]
5135
- if '.' in s:
5136
- try:
5137
- return InterpVersion.parse(s)
5138
- except InvalidVersion:
5139
- pass
5140
- ii = self.inspector.inspect(exe)
5141
- return ii.iv if ii is not None else None
5455
+ pyp_dct['project'] = prj
5142
5456
 
5143
- def exe_versions(self) -> ta.Sequence[ta.Tuple[str, InterpVersion]]:
5144
- lst = []
5145
- for e in self.exes():
5146
- if (ev := self.get_exe_version(e)) is None:
5147
- log.debug('Invalid system version: %s', e)
5148
- continue
5149
- lst.append((e, ev))
5150
- return lst
5457
+ #
5151
5458
 
5152
- #
5459
+ st = dict(specs.setuptools)
5460
+ pyp_dct['tool.setuptools'] = st
5153
5461
 
5154
- def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5155
- return [ev for e, ev in self.exe_versions()]
5462
+ for k in [
5463
+ 'cexts',
5156
5464
 
5157
- def get_installed_version(self, version: InterpVersion) -> Interp:
5158
- for e, ev in self.exe_versions():
5159
- if ev != version:
5160
- continue
5161
- return Interp(
5162
- exe=e,
5163
- version=ev,
5164
- )
5165
- raise KeyError(version)
5465
+ 'find_packages',
5466
+ 'package_data',
5467
+ 'manifest_in',
5468
+ ]:
5469
+ st.pop(k, None)
5166
5470
 
5471
+ pyp_dct['tool.setuptools.packages.find'] = {
5472
+ 'include': [],
5473
+ }
5167
5474
 
5168
- ########################################
5169
- # ../pkg.py
5170
- """
5171
- TODO:
5172
- - ext scanning
5173
- - __revision__
5174
- - entry_points
5475
+ #
5175
5476
 
5176
- ** NOTE **
5177
- setuptools now (2024/09/02) has experimental support for extensions in pure pyproject.toml - but we still want a
5178
- separate '-cext' package
5179
- https://setuptools.pypa.io/en/latest/userguide/ext_modules.html
5180
- https://github.com/pypa/setuptools/commit/1a9d87308dc0d8aabeaae0dce989b35dfb7699f0#diff-61d113525e9cc93565799a4bb8b34a68e2945b8a3f7d90c81380614a4ea39542R7-R8
5477
+ ext_lines = []
5181
5478
 
5182
- --
5479
+ for ext_src in self.find_cext_srcs():
5480
+ ext_name = ext_src.rpartition('.')[0].replace(os.sep, '.')
5481
+ ext_lines.extend([
5482
+ 'st.Extension(',
5483
+ f" name='{ext_name}',",
5484
+ f" sources=['{ext_src}'],",
5485
+ " extra_compile_args=['-std=c++20'],",
5486
+ '),',
5487
+ ])
5183
5488
 
5184
- https://setuptools.pypa.io/en/latest/references/keywords.html
5185
- https://packaging.python.org/en/latest/specifications/pyproject-toml
5489
+ src = '\n'.join([
5490
+ 'import setuptools as st',
5491
+ '',
5492
+ '',
5493
+ 'st.setup(',
5494
+ ' ext_modules=[',
5495
+ *[' ' + l for l in ext_lines],
5496
+ ' ]',
5497
+ ')',
5498
+ '',
5499
+ ])
5186
5500
 
5187
- How to build a C extension in keeping with PEP 517, i.e. with pyproject.toml instead of setup.py?
5188
- https://stackoverflow.com/a/66479252
5501
+ #
5189
5502
 
5190
- https://github.com/pypa/sampleproject/blob/db5806e0a3204034c51b1c00dde7d5eb3fa2532e/setup.py
5503
+ return self.FileContents(
5504
+ pyp_dct,
5505
+ src,
5506
+ )
5191
5507
 
5192
- https://pip.pypa.io/en/stable/cli/pip_install/#vcs-support
5193
- vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir
5194
- 'git+https://github.com/wrmsr/omlish@master#subdirectory=.pip/omlish'
5195
- """ # noqa
5508
+ def _write_file_contents(self) -> None:
5509
+ fc = self.file_contents()
5196
5510
 
5511
+ with open(os.path.join(self._pkg_dir(), 'pyproject.toml'), 'w') as f:
5512
+ TomlWriter(f).write_root(fc.pyproject_dct)
5197
5513
 
5198
- #
5514
+ with open(os.path.join(self._pkg_dir(), 'setup.py'), 'w') as f:
5515
+ f.write(fc.setup_py)
5199
5516
 
5200
5517
 
5201
- class BasePyprojectPackageGenerator(abc.ABC):
5202
- def __init__(
5203
- self,
5204
- dir_name: str,
5205
- pkgs_root: str,
5206
- *,
5207
- pkg_suffix: str = '',
5208
- ) -> None:
5209
- super().__init__()
5210
- self._dir_name = dir_name
5211
- self._pkgs_root = pkgs_root
5212
- self._pkg_suffix = pkg_suffix
5518
+ ##
5213
5519
 
5214
- #
5215
5520
 
5216
- @cached_nullary
5217
- def about(self) -> types.ModuleType:
5218
- return importlib.import_module(f'{self._dir_name}.__about__')
5521
+ class _PyprojectCliPackageGenerator(BasePyprojectPackageGenerator):
5219
5522
 
5220
5523
  #
5221
5524
 
5525
+ @dc.dataclass(frozen=True)
5526
+ class FileContents:
5527
+ pyproject_dct: ta.Mapping[str, ta.Any]
5528
+
5222
5529
  @cached_nullary
5223
- def _pkg_dir(self) -> str:
5224
- pkg_dir: str = os.path.join(self._pkgs_root, self._dir_name + self._pkg_suffix)
5225
- if os.path.isdir(pkg_dir):
5226
- shutil.rmtree(pkg_dir)
5227
- os.makedirs(pkg_dir)
5228
- return pkg_dir
5530
+ def file_contents(self) -> FileContents:
5531
+ specs = self.build_specs()
5229
5532
 
5230
- #
5533
+ #
5231
5534
 
5232
- _GIT_IGNORE: ta.Sequence[str] = [
5233
- '/*.egg-info/',
5234
- '/dist',
5235
- ]
5535
+ pyp_dct = {}
5236
5536
 
5237
- def _write_git_ignore(self) -> None:
5238
- with open(os.path.join(self._pkg_dir(), '.gitignore'), 'w') as f:
5239
- f.write('\n'.join(self._GIT_IGNORE))
5537
+ pyp_dct['build-system'] = {
5538
+ 'requires': ['setuptools'],
5539
+ 'build-backend': 'setuptools.build_meta',
5540
+ }
5240
5541
 
5241
- #
5542
+ prj = specs.pyproject
5543
+ prj['dependencies'] = [f'{prj["name"]} == {prj["version"]}']
5544
+ prj['name'] += self._pkg_suffix
5545
+ for k in [
5546
+ 'optional_dependencies',
5547
+ 'entry_points',
5548
+ 'scripts',
5549
+ ]:
5550
+ prj.pop(k, None)
5242
5551
 
5243
- def _symlink_source_dir(self) -> None:
5244
- os.symlink(
5245
- os.path.relpath(self._dir_name, self._pkg_dir()),
5246
- os.path.join(self._pkg_dir(), self._dir_name),
5247
- )
5552
+ pyp_dct['project'] = prj
5248
5553
 
5249
- #
5554
+ if (scs := prj.pop('cli_scripts', None)):
5555
+ pyp_dct['project.scripts'] = scs
5250
5556
 
5251
- @cached_nullary
5252
- def project_cls(self) -> type:
5253
- return self.about().Project
5557
+ #
5254
5558
 
5255
- @cached_nullary
5256
- def setuptools_cls(self) -> type:
5257
- return self.about().Setuptools
5559
+ st = dict(specs.setuptools)
5560
+ pyp_dct['tool.setuptools'] = st
5258
5561
 
5259
- @staticmethod
5260
- def _build_cls_dct(cls: type) -> ta.Dict[str, ta.Any]: # noqa
5261
- dct = {}
5262
- for b in reversed(cls.__mro__):
5263
- for k, v in b.__dict__.items():
5264
- if k.startswith('_'):
5265
- continue
5266
- dct[k] = v
5267
- return dct
5562
+ for k in [
5563
+ 'cexts',
5268
5564
 
5269
- @staticmethod
5270
- def _move_dict_key(
5271
- sd: ta.Dict[str, ta.Any],
5272
- sk: str,
5273
- dd: ta.Dict[str, ta.Any],
5274
- dk: str,
5275
- ) -> None:
5276
- if sk in sd:
5277
- dd[dk] = sd.pop(sk)
5565
+ 'find_packages',
5566
+ 'package_data',
5567
+ 'manifest_in',
5568
+ ]:
5569
+ st.pop(k, None)
5278
5570
 
5279
- @dc.dataclass(frozen=True)
5280
- class Specs:
5281
- pyproject: ta.Dict[str, ta.Any]
5282
- setuptools: ta.Dict[str, ta.Any]
5571
+ pyp_dct['tool.setuptools.packages.find'] = {
5572
+ 'include': [],
5573
+ }
5283
5574
 
5284
- def build_specs(self) -> Specs:
5285
- return self.Specs(
5286
- self._build_cls_dct(self.project_cls()),
5287
- self._build_cls_dct(self.setuptools_cls()),
5575
+ #
5576
+
5577
+ return self.FileContents(
5578
+ pyp_dct,
5288
5579
  )
5289
5580
 
5290
- #
5581
+ def _write_file_contents(self) -> None:
5582
+ fc = self.file_contents()
5291
5583
 
5292
- class _PkgData(ta.NamedTuple):
5293
- inc: ta.List[str]
5294
- exc: ta.List[str]
5584
+ with open(os.path.join(self._pkg_dir(), 'pyproject.toml'), 'w') as f:
5585
+ TomlWriter(f).write_root(fc.pyproject_dct)
5295
5586
 
5296
- @cached_nullary
5297
- def _collect_pkg_data(self) -> _PkgData:
5298
- inc: ta.List[str] = []
5299
- exc: ta.List[str] = []
5300
5587
 
5301
- for p, ds, fs in os.walk(self._dir_name): # noqa
5302
- for f in fs:
5303
- if f != '.pkgdata':
5304
- continue
5305
- rp = os.path.relpath(p, self._dir_name)
5306
- log.info('Found pkgdata %s for pkg %s', rp, self._dir_name)
5307
- with open(os.path.join(p, f)) as fo:
5308
- src = fo.read()
5309
- for l in src.splitlines():
5310
- if not (l := l.strip()):
5311
- continue
5312
- if l.startswith('!'):
5313
- exc.append(os.path.join(rp, l[1:]))
5314
- else:
5315
- inc.append(os.path.join(rp, l))
5588
+ ########################################
5589
+ # ../../interp/pyenv.py
5590
+ """
5591
+ TODO:
5592
+ - custom tags
5593
+ - 'aliases'
5594
+ - https://github.com/pyenv/pyenv/pull/2966
5595
+ - https://github.com/pyenv/pyenv/issues/218 (lol)
5596
+ - probably need custom (temp?) definition file
5597
+ - *or* python-build directly just into the versions dir?
5598
+ - optionally install / upgrade pyenv itself
5599
+ - new vers dont need these custom mac opts, only run on old vers
5600
+ """
5316
5601
 
5317
- return self._PkgData(inc, exc)
5318
5602
 
5319
- #
5603
+ ##
5320
5604
 
5321
- @abc.abstractmethod
5322
- def _write_file_contents(self) -> None:
5323
- raise NotImplementedError
5324
5605
 
5325
- #
5606
+ class Pyenv:
5607
+ def __init__(
5608
+ self,
5609
+ *,
5610
+ root: ta.Optional[str] = None,
5611
+ ) -> None:
5612
+ if root is not None and not (isinstance(root, str) and root):
5613
+ raise ValueError(f'pyenv_root: {root!r}')
5326
5614
 
5327
- _STANDARD_FILES: ta.Sequence[str] = [
5328
- 'LICENSE',
5329
- 'README.rst',
5330
- ]
5615
+ super().__init__()
5331
5616
 
5332
- def _symlink_standard_files(self) -> None:
5333
- for fn in self._STANDARD_FILES:
5334
- if os.path.exists(fn):
5335
- os.symlink(os.path.relpath(fn, self._pkg_dir()), os.path.join(self._pkg_dir(), fn))
5617
+ self._root_kw = root
5336
5618
 
5337
- #
5619
+ @async_cached_nullary
5620
+ async def root(self) -> ta.Optional[str]:
5621
+ if self._root_kw is not None:
5622
+ return self._root_kw
5338
5623
 
5339
- def children(self) -> ta.Sequence['BasePyprojectPackageGenerator']:
5340
- return []
5624
+ if shutil.which('pyenv'):
5625
+ return await asyncio_subprocess_check_output_str('pyenv', 'root')
5341
5626
 
5342
- #
5627
+ d = os.path.expanduser('~/.pyenv')
5628
+ if os.path.isdir(d) and os.path.isfile(os.path.join(d, 'bin', 'pyenv')):
5629
+ return d
5343
5630
 
5344
- def gen(self) -> str:
5345
- log.info('Generating pyproject package: %s -> %s (%s)', self._dir_name, self._pkgs_root, self._pkg_suffix)
5631
+ return None
5346
5632
 
5347
- self._pkg_dir()
5348
- self._write_git_ignore()
5349
- self._symlink_source_dir()
5350
- self._write_file_contents()
5351
- self._symlink_standard_files()
5633
+ @async_cached_nullary
5634
+ async def exe(self) -> str:
5635
+ return os.path.join(check_not_none(await self.root()), 'bin', 'pyenv')
5352
5636
 
5353
- return self._pkg_dir()
5637
+ async def version_exes(self) -> ta.List[ta.Tuple[str, str]]:
5638
+ if (root := await self.root()) is None:
5639
+ return []
5640
+ ret = []
5641
+ vp = os.path.join(root, 'versions')
5642
+ if os.path.isdir(vp):
5643
+ for dn in os.listdir(vp):
5644
+ ep = os.path.join(vp, dn, 'bin', 'python')
5645
+ if not os.path.isfile(ep):
5646
+ continue
5647
+ ret.append((dn, ep))
5648
+ return ret
5354
5649
 
5355
- #
5650
+ async def installable_versions(self) -> ta.List[str]:
5651
+ if await self.root() is None:
5652
+ return []
5653
+ ret = []
5654
+ s = await asyncio_subprocess_check_output_str(await self.exe(), 'install', '--list')
5655
+ for l in s.splitlines():
5656
+ if not l.startswith(' '):
5657
+ continue
5658
+ l = l.strip()
5659
+ if not l:
5660
+ continue
5661
+ ret.append(l)
5662
+ return ret
5356
5663
 
5357
- @dc.dataclass(frozen=True)
5358
- class BuildOpts:
5359
- add_revision: bool = False
5360
- test: bool = False
5664
+ async def update(self) -> bool:
5665
+ if (root := await self.root()) is None:
5666
+ return False
5667
+ if not os.path.isdir(os.path.join(root, '.git')):
5668
+ return False
5669
+ await asyncio_subprocess_check_call('git', 'pull', cwd=root)
5670
+ return True
5361
5671
 
5362
- def build(
5363
- self,
5364
- output_dir: ta.Optional[str] = None,
5365
- opts: BuildOpts = BuildOpts(),
5366
- ) -> None:
5367
- subprocess_check_call(
5368
- sys.executable,
5369
- '-m',
5370
- 'build',
5371
- cwd=self._pkg_dir(),
5372
- )
5373
5672
 
5374
- dist_dir = os.path.join(self._pkg_dir(), 'dist')
5673
+ ##
5375
5674
 
5376
- if opts.add_revision:
5377
- GitRevisionAdder().add_to(dist_dir)
5378
5675
 
5379
- if opts.test:
5380
- for fn in os.listdir(dist_dir):
5381
- tmp_dir = tempfile.mkdtemp()
5676
+ @dc.dataclass(frozen=True)
5677
+ class PyenvInstallOpts:
5678
+ opts: ta.Sequence[str] = ()
5679
+ conf_opts: ta.Sequence[str] = ()
5680
+ cflags: ta.Sequence[str] = ()
5681
+ ldflags: ta.Sequence[str] = ()
5682
+ env: ta.Mapping[str, str] = dc.field(default_factory=dict)
5382
5683
 
5383
- subprocess_check_call(
5384
- sys.executable,
5385
- '-m', 'venv',
5386
- 'test-install',
5387
- cwd=tmp_dir,
5388
- )
5684
+ def merge(self, *others: 'PyenvInstallOpts') -> 'PyenvInstallOpts':
5685
+ return PyenvInstallOpts(
5686
+ opts=list(itertools.chain.from_iterable(o.opts for o in [self, *others])),
5687
+ conf_opts=list(itertools.chain.from_iterable(o.conf_opts for o in [self, *others])),
5688
+ cflags=list(itertools.chain.from_iterable(o.cflags for o in [self, *others])),
5689
+ ldflags=list(itertools.chain.from_iterable(o.ldflags for o in [self, *others])),
5690
+ env=dict(itertools.chain.from_iterable(o.env.items() for o in [self, *others])),
5691
+ )
5389
5692
 
5390
- subprocess_check_call(
5391
- os.path.join(tmp_dir, 'test-install', 'bin', 'python3'),
5392
- '-m', 'pip',
5393
- 'install',
5394
- os.path.abspath(os.path.join(dist_dir, fn)),
5395
- cwd=tmp_dir,
5396
- )
5397
5693
 
5398
- if output_dir is not None:
5399
- for fn in os.listdir(dist_dir):
5400
- shutil.copyfile(os.path.join(dist_dir, fn), os.path.join(output_dir, fn))
5694
+ # TODO: https://github.com/pyenv/pyenv/blob/master/plugins/python-build/README.md#building-for-maximum-performance
5695
+ DEFAULT_PYENV_INSTALL_OPTS = PyenvInstallOpts(
5696
+ opts=[
5697
+ '-s',
5698
+ '-v',
5699
+ '-k',
5700
+ ],
5701
+ conf_opts=[
5702
+ # FIXME: breaks on mac for older py's
5703
+ '--enable-loadable-sqlite-extensions',
5401
5704
 
5705
+ # '--enable-shared',
5402
5706
 
5403
- #
5707
+ '--enable-optimizations',
5708
+ '--with-lto',
5404
5709
 
5710
+ # '--enable-profiling', # ?
5405
5711
 
5406
- class PyprojectPackageGenerator(BasePyprojectPackageGenerator):
5712
+ # '--enable-ipv6', # ?
5713
+ ],
5714
+ cflags=[
5715
+ # '-march=native',
5716
+ # '-mtune=native',
5717
+ ],
5718
+ )
5407
5719
 
5408
- #
5720
+ DEBUG_PYENV_INSTALL_OPTS = PyenvInstallOpts(opts=['-g'])
5721
+
5722
+ THREADED_PYENV_INSTALL_OPTS = PyenvInstallOpts(conf_opts=['--disable-gil'])
5409
5723
 
5410
- @dc.dataclass(frozen=True)
5411
- class FileContents:
5412
- pyproject_dct: ta.Mapping[str, ta.Any]
5413
- manifest_in: ta.Optional[ta.Sequence[str]]
5414
5724
 
5415
- @cached_nullary
5416
- def file_contents(self) -> FileContents:
5417
- specs = self.build_specs()
5725
+ #
5418
5726
 
5419
- #
5420
5727
 
5421
- pyp_dct = {}
5728
+ class PyenvInstallOptsProvider(abc.ABC):
5729
+ @abc.abstractmethod
5730
+ def opts(self) -> ta.Awaitable[PyenvInstallOpts]:
5731
+ raise NotImplementedError
5422
5732
 
5423
- pyp_dct['build-system'] = {
5424
- 'requires': ['setuptools'],
5425
- 'build-backend': 'setuptools.build_meta',
5426
- }
5427
5733
 
5428
- prj = specs.pyproject
5429
- prj['name'] += self._pkg_suffix
5734
+ class LinuxPyenvInstallOpts(PyenvInstallOptsProvider):
5735
+ async def opts(self) -> PyenvInstallOpts:
5736
+ return PyenvInstallOpts()
5430
5737
 
5431
- pyp_dct['project'] = prj
5432
5738
 
5433
- self._move_dict_key(prj, 'optional_dependencies', pyp_dct, extrask := 'project.optional-dependencies')
5434
- if (extras := pyp_dct.get(extrask)):
5435
- pyp_dct[extrask] = {
5436
- 'all': [
5437
- e
5438
- for lst in extras.values()
5439
- for e in lst
5440
- ],
5441
- **extras,
5442
- }
5739
+ class DarwinPyenvInstallOpts(PyenvInstallOptsProvider):
5740
+ @cached_nullary
5741
+ def framework_opts(self) -> PyenvInstallOpts:
5742
+ return PyenvInstallOpts(conf_opts=['--enable-framework'])
5443
5743
 
5444
- if (eps := prj.pop('entry_points', None)):
5445
- pyp_dct['project.entry-points'] = {TomlWriter.Literal(f"'{k}'"): v for k, v in eps.items()} # type: ignore # noqa
5744
+ @cached_nullary
5745
+ def has_brew(self) -> bool:
5746
+ return shutil.which('brew') is not None
5446
5747
 
5447
- if (scs := prj.pop('scripts', None)):
5448
- pyp_dct['project.scripts'] = scs
5748
+ BREW_DEPS: ta.Sequence[str] = [
5749
+ 'openssl',
5750
+ 'readline',
5751
+ 'sqlite3',
5752
+ 'zlib',
5753
+ ]
5449
5754
 
5450
- prj.pop('cli_scripts', None)
5755
+ @async_cached_nullary
5756
+ async def brew_deps_opts(self) -> PyenvInstallOpts:
5757
+ cflags = []
5758
+ ldflags = []
5759
+ for dep in self.BREW_DEPS:
5760
+ dep_prefix = await asyncio_subprocess_check_output_str('brew', '--prefix', dep)
5761
+ cflags.append(f'-I{dep_prefix}/include')
5762
+ ldflags.append(f'-L{dep_prefix}/lib')
5763
+ return PyenvInstallOpts(
5764
+ cflags=cflags,
5765
+ ldflags=ldflags,
5766
+ )
5451
5767
 
5452
- ##
5768
+ @async_cached_nullary
5769
+ async def brew_tcl_opts(self) -> PyenvInstallOpts:
5770
+ if await asyncio_subprocess_try_output('brew', '--prefix', 'tcl-tk') is None:
5771
+ return PyenvInstallOpts()
5453
5772
 
5454
- st = dict(specs.setuptools)
5455
- pyp_dct['tool.setuptools'] = st
5773
+ tcl_tk_prefix = await asyncio_subprocess_check_output_str('brew', '--prefix', 'tcl-tk')
5774
+ tcl_tk_ver_str = await asyncio_subprocess_check_output_str('brew', 'ls', '--versions', 'tcl-tk')
5775
+ tcl_tk_ver = '.'.join(tcl_tk_ver_str.split()[1].split('.')[:2])
5456
5776
 
5457
- st.pop('cexts', None)
5777
+ return PyenvInstallOpts(conf_opts=[
5778
+ f"--with-tcltk-includes='-I{tcl_tk_prefix}/include'",
5779
+ f"--with-tcltk-libs='-L{tcl_tk_prefix}/lib -ltcl{tcl_tk_ver} -ltk{tcl_tk_ver}'",
5780
+ ])
5458
5781
 
5459
- #
5782
+ # @cached_nullary
5783
+ # def brew_ssl_opts(self) -> PyenvInstallOpts:
5784
+ # pkg_config_path = subprocess_check_output_str('brew', '--prefix', 'openssl')
5785
+ # if 'PKG_CONFIG_PATH' in os.environ:
5786
+ # pkg_config_path += ':' + os.environ['PKG_CONFIG_PATH']
5787
+ # return PyenvInstallOpts(env={'PKG_CONFIG_PATH': pkg_config_path})
5460
5788
 
5461
- # TODO: default
5462
- # find_packages = {
5463
- # 'include': [Project.name, f'{Project.name}.*'],
5464
- # 'exclude': [*SetuptoolsBase.find_packages['exclude']],
5465
- # }
5789
+ async def opts(self) -> PyenvInstallOpts:
5790
+ return PyenvInstallOpts().merge(
5791
+ self.framework_opts(),
5792
+ await self.brew_deps_opts(),
5793
+ await self.brew_tcl_opts(),
5794
+ # self.brew_ssl_opts(),
5795
+ )
5466
5796
 
5467
- fp = dict(st.pop('find_packages', {}))
5468
5797
 
5469
- pyp_dct['tool.setuptools.packages.find'] = fp
5798
+ PLATFORM_PYENV_INSTALL_OPTS: ta.Dict[str, PyenvInstallOptsProvider] = {
5799
+ 'darwin': DarwinPyenvInstallOpts(),
5800
+ 'linux': LinuxPyenvInstallOpts(),
5801
+ }
5470
5802
 
5471
- #
5472
5803
 
5473
- # TODO: default
5474
- # package_data = {
5475
- # '*': [
5476
- # '*.c',
5477
- # '*.cc',
5478
- # '*.h',
5479
- # '.manifests.json',
5480
- # 'LICENSE',
5481
- # ],
5482
- # }
5804
+ ##
5483
5805
 
5484
- pd = dict(st.pop('package_data', {}))
5485
- epd = dict(st.pop('exclude_package_data', {}))
5486
5806
 
5487
- cpd = self._collect_pkg_data()
5488
- if cpd.inc:
5489
- pd['*'] = [*pd.get('*', []), *sorted(set(cpd.inc))]
5490
- if cpd.exc:
5491
- epd['*'] = [*epd.get('*', []), *sorted(set(cpd.exc))]
5807
+ class PyenvVersionInstaller:
5808
+ """
5809
+ Messy: can install freethreaded build with a 't' suffixed version str _or_ by THREADED_PYENV_INSTALL_OPTS - need
5810
+ latter to build custom interp with ft, need former to use canned / blessed interps. Muh.
5811
+ """
5492
5812
 
5493
- if pd:
5494
- pyp_dct['tool.setuptools.package-data'] = pd
5495
- if epd:
5496
- pyp_dct['tool.setuptools.exclude-package-data'] = epd
5813
+ def __init__(
5814
+ self,
5815
+ version: str,
5816
+ opts: ta.Optional[PyenvInstallOpts] = None,
5817
+ interp_opts: InterpOpts = InterpOpts(),
5818
+ *,
5819
+ install_name: ta.Optional[str] = None,
5820
+ no_default_opts: bool = False,
5821
+ pyenv: Pyenv = Pyenv(),
5822
+ ) -> None:
5823
+ super().__init__()
5497
5824
 
5498
- #
5825
+ self._version = version
5826
+ self._given_opts = opts
5827
+ self._interp_opts = interp_opts
5828
+ self._given_install_name = install_name
5499
5829
 
5500
- # TODO: default
5501
- # manifest_in = [
5502
- # 'global-exclude **/conftest.py',
5503
- # ]
5830
+ self._no_default_opts = no_default_opts
5831
+ self._pyenv = pyenv
5504
5832
 
5505
- mani_in = st.pop('manifest_in', None)
5833
+ @property
5834
+ def version(self) -> str:
5835
+ return self._version
5506
5836
 
5507
- #
5837
+ @async_cached_nullary
5838
+ async def opts(self) -> PyenvInstallOpts:
5839
+ opts = self._given_opts
5840
+ if self._no_default_opts:
5841
+ if opts is None:
5842
+ opts = PyenvInstallOpts()
5843
+ else:
5844
+ lst = [self._given_opts if self._given_opts is not None else DEFAULT_PYENV_INSTALL_OPTS]
5845
+ if self._interp_opts.debug:
5846
+ lst.append(DEBUG_PYENV_INSTALL_OPTS)
5847
+ if self._interp_opts.threaded:
5848
+ lst.append(THREADED_PYENV_INSTALL_OPTS)
5849
+ lst.append(await PLATFORM_PYENV_INSTALL_OPTS[sys.platform].opts())
5850
+ opts = PyenvInstallOpts().merge(*lst)
5851
+ return opts
5508
5852
 
5509
- return self.FileContents(
5510
- pyp_dct,
5511
- mani_in,
5512
- )
5853
+ @cached_nullary
5854
+ def install_name(self) -> str:
5855
+ if self._given_install_name is not None:
5856
+ return self._given_install_name
5857
+ return self._version + ('-debug' if self._interp_opts.debug else '')
5513
5858
 
5514
- def _write_file_contents(self) -> None:
5515
- fc = self.file_contents()
5859
+ @async_cached_nullary
5860
+ async def install_dir(self) -> str:
5861
+ return str(os.path.join(check_not_none(await self._pyenv.root()), 'versions', self.install_name()))
5516
5862
 
5517
- with open(os.path.join(self._pkg_dir(), 'pyproject.toml'), 'w') as f:
5518
- TomlWriter(f).write_root(fc.pyproject_dct)
5863
+ @async_cached_nullary
5864
+ async def install(self) -> str:
5865
+ opts = await self.opts()
5866
+ env = {**os.environ, **opts.env}
5867
+ for k, l in [
5868
+ ('CFLAGS', opts.cflags),
5869
+ ('LDFLAGS', opts.ldflags),
5870
+ ('PYTHON_CONFIGURE_OPTS', opts.conf_opts),
5871
+ ]:
5872
+ v = ' '.join(l)
5873
+ if k in os.environ:
5874
+ v += ' ' + os.environ[k]
5875
+ env[k] = v
5519
5876
 
5520
- if fc.manifest_in:
5521
- with open(os.path.join(self._pkg_dir(), 'MANIFEST.in'), 'w') as f:
5522
- f.write('\n'.join(fc.manifest_in)) # noqa
5877
+ conf_args = [
5878
+ *opts.opts,
5879
+ self._version,
5880
+ ]
5523
5881
 
5524
- #
5882
+ if self._given_install_name is not None:
5883
+ full_args = [
5884
+ os.path.join(check_not_none(await self._pyenv.root()), 'plugins', 'python-build', 'bin', 'python-build'), # noqa
5885
+ *conf_args,
5886
+ self.install_dir(),
5887
+ ]
5888
+ else:
5889
+ full_args = [
5890
+ self._pyenv.exe(),
5891
+ 'install',
5892
+ *conf_args,
5893
+ ]
5525
5894
 
5526
- @cached_nullary
5527
- def children(self) -> ta.Sequence[BasePyprojectPackageGenerator]:
5528
- out: ta.List[BasePyprojectPackageGenerator] = []
5895
+ await asyncio_subprocess_check_call(
5896
+ *full_args,
5897
+ env=env,
5898
+ )
5529
5899
 
5530
- if self.build_specs().setuptools.get('cexts'):
5531
- out.append(_PyprojectCextPackageGenerator(
5532
- self._dir_name,
5533
- self._pkgs_root,
5534
- pkg_suffix='-cext',
5535
- ))
5900
+ exe = os.path.join(await self.install_dir(), 'bin', 'python')
5901
+ if not os.path.isfile(exe):
5902
+ raise RuntimeError(f'Interpreter not found: {exe}')
5903
+ return exe
5536
5904
 
5537
- if self.build_specs().pyproject.get('cli_scripts'):
5538
- out.append(_PyprojectCliPackageGenerator(
5539
- self._dir_name,
5540
- self._pkgs_root,
5541
- pkg_suffix='-cli',
5542
- ))
5543
5905
 
5544
- return out
5906
+ ##
5545
5907
 
5546
5908
 
5547
- #
5909
+ class PyenvInterpProvider(InterpProvider):
5910
+ def __init__(
5911
+ self,
5912
+ pyenv: Pyenv = Pyenv(),
5548
5913
 
5914
+ inspect: bool = False,
5915
+ inspector: InterpInspector = INTERP_INSPECTOR,
5549
5916
 
5550
- class _PyprojectCextPackageGenerator(BasePyprojectPackageGenerator):
5917
+ *,
5551
5918
 
5552
- #
5919
+ try_update: bool = False,
5920
+ ) -> None:
5921
+ super().__init__()
5553
5922
 
5554
- @cached_nullary
5555
- def find_cext_srcs(self) -> ta.Sequence[str]:
5556
- return sorted(find_magic_files(
5557
- CextMagic.STYLE,
5558
- [self._dir_name],
5559
- keys=[CextMagic.KEY],
5560
- ))
5923
+ self._pyenv = pyenv
5561
5924
 
5562
- #
5925
+ self._inspect = inspect
5926
+ self._inspector = inspector
5563
5927
 
5564
- @dc.dataclass(frozen=True)
5565
- class FileContents:
5566
- pyproject_dct: ta.Mapping[str, ta.Any]
5567
- setup_py: str
5928
+ self._try_update = try_update
5568
5929
 
5569
- @cached_nullary
5570
- def file_contents(self) -> FileContents:
5571
- specs = self.build_specs()
5930
+ #
5572
5931
 
5573
- #
5932
+ @staticmethod
5933
+ def guess_version(s: str) -> ta.Optional[InterpVersion]:
5934
+ def strip_sfx(s: str, sfx: str) -> ta.Tuple[str, bool]:
5935
+ if s.endswith(sfx):
5936
+ return s[:-len(sfx)], True
5937
+ return s, False
5938
+ ok = {}
5939
+ s, ok['debug'] = strip_sfx(s, '-debug')
5940
+ s, ok['threaded'] = strip_sfx(s, 't')
5941
+ try:
5942
+ v = Version(s)
5943
+ except InvalidVersion:
5944
+ return None
5945
+ return InterpVersion(v, InterpOpts(**ok))
5574
5946
 
5575
- pyp_dct = {}
5947
+ class Installed(ta.NamedTuple):
5948
+ name: str
5949
+ exe: str
5950
+ version: InterpVersion
5576
5951
 
5577
- pyp_dct['build-system'] = {
5578
- 'requires': ['setuptools'],
5579
- 'build-backend': 'setuptools.build_meta',
5580
- }
5952
+ async def _make_installed(self, vn: str, ep: str) -> ta.Optional[Installed]:
5953
+ iv: ta.Optional[InterpVersion]
5954
+ if self._inspect:
5955
+ try:
5956
+ iv = check_not_none(await self._inspector.inspect(ep)).iv
5957
+ except Exception as e: # noqa
5958
+ return None
5959
+ else:
5960
+ iv = self.guess_version(vn)
5961
+ if iv is None:
5962
+ return None
5963
+ return PyenvInterpProvider.Installed(
5964
+ name=vn,
5965
+ exe=ep,
5966
+ version=iv,
5967
+ )
5581
5968
 
5582
- prj = specs.pyproject
5583
- prj['dependencies'] = [f'{prj["name"]} == {prj["version"]}']
5584
- prj['name'] += self._pkg_suffix
5585
- for k in [
5586
- 'optional_dependencies',
5587
- 'entry_points',
5588
- 'scripts',
5589
- 'cli_scripts',
5590
- ]:
5591
- prj.pop(k, None)
5969
+ async def installed(self) -> ta.Sequence[Installed]:
5970
+ ret: ta.List[PyenvInterpProvider.Installed] = []
5971
+ for vn, ep in await self._pyenv.version_exes():
5972
+ if (i := await self._make_installed(vn, ep)) is None:
5973
+ log.debug('Invalid pyenv version: %s', vn)
5974
+ continue
5975
+ ret.append(i)
5976
+ return ret
5592
5977
 
5593
- pyp_dct['project'] = prj
5978
+ #
5594
5979
 
5595
- #
5980
+ async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5981
+ return [i.version for i in await self.installed()]
5596
5982
 
5597
- st = dict(specs.setuptools)
5598
- pyp_dct['tool.setuptools'] = st
5983
+ async def get_installed_version(self, version: InterpVersion) -> Interp:
5984
+ for i in await self.installed():
5985
+ if i.version == version:
5986
+ return Interp(
5987
+ exe=i.exe,
5988
+ version=i.version,
5989
+ )
5990
+ raise KeyError(version)
5599
5991
 
5600
- for k in [
5601
- 'cexts',
5992
+ #
5602
5993
 
5603
- 'find_packages',
5604
- 'package_data',
5605
- 'manifest_in',
5606
- ]:
5607
- st.pop(k, None)
5994
+ async def _get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
5995
+ lst = []
5608
5996
 
5609
- pyp_dct['tool.setuptools.packages.find'] = {
5610
- 'include': [],
5611
- }
5997
+ for vs in await self._pyenv.installable_versions():
5998
+ if (iv := self.guess_version(vs)) is None:
5999
+ continue
6000
+ if iv.opts.debug:
6001
+ raise Exception('Pyenv installable versions not expected to have debug suffix')
6002
+ for d in [False, True]:
6003
+ lst.append(dc.replace(iv, opts=dc.replace(iv.opts, debug=d)))
5612
6004
 
5613
- #
6005
+ return lst
5614
6006
 
5615
- ext_lines = []
6007
+ async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
6008
+ lst = await self._get_installable_versions(spec)
5616
6009
 
5617
- for ext_src in self.find_cext_srcs():
5618
- ext_name = ext_src.rpartition('.')[0].replace(os.sep, '.')
5619
- ext_lines.extend([
5620
- 'st.Extension(',
5621
- f" name='{ext_name}',",
5622
- f" sources=['{ext_src}'],",
5623
- " extra_compile_args=['-std=c++20'],",
5624
- '),',
5625
- ])
6010
+ if self._try_update and not any(v in spec for v in lst):
6011
+ if self._pyenv.update():
6012
+ lst = await self._get_installable_versions(spec)
5626
6013
 
5627
- src = '\n'.join([
5628
- 'import setuptools as st',
5629
- '',
5630
- '',
5631
- 'st.setup(',
5632
- ' ext_modules=[',
5633
- *[' ' + l for l in ext_lines],
5634
- ' ]',
5635
- ')',
5636
- '',
5637
- ])
6014
+ return lst
5638
6015
 
5639
- #
6016
+ async def install_version(self, version: InterpVersion) -> Interp:
6017
+ inst_version = str(version.version)
6018
+ inst_opts = version.opts
6019
+ if inst_opts.threaded:
6020
+ inst_version += 't'
6021
+ inst_opts = dc.replace(inst_opts, threaded=False)
5640
6022
 
5641
- return self.FileContents(
5642
- pyp_dct,
5643
- src,
6023
+ installer = PyenvVersionInstaller(
6024
+ inst_version,
6025
+ interp_opts=inst_opts,
5644
6026
  )
5645
6027
 
5646
- def _write_file_contents(self) -> None:
5647
- fc = self.file_contents()
6028
+ exe = await installer.install()
6029
+ return Interp(exe, version)
5648
6030
 
5649
- with open(os.path.join(self._pkg_dir(), 'pyproject.toml'), 'w') as f:
5650
- TomlWriter(f).write_root(fc.pyproject_dct)
5651
6031
 
5652
- with open(os.path.join(self._pkg_dir(), 'setup.py'), 'w') as f:
5653
- f.write(fc.setup_py)
6032
+ ########################################
6033
+ # ../../interp/system.py
6034
+ """
6035
+ TODO:
6036
+ - python, python3, python3.12, ...
6037
+ - check if path py's are venvs: sys.prefix != sys.base_prefix
6038
+ """
5654
6039
 
5655
6040
 
5656
6041
  ##
5657
6042
 
5658
6043
 
5659
- class _PyprojectCliPackageGenerator(BasePyprojectPackageGenerator):
5660
-
5661
- #
5662
-
5663
- @dc.dataclass(frozen=True)
5664
- class FileContents:
5665
- pyproject_dct: ta.Mapping[str, ta.Any]
5666
-
5667
- @cached_nullary
5668
- def file_contents(self) -> FileContents:
5669
- specs = self.build_specs()
6044
+ @dc.dataclass(frozen=True)
6045
+ class SystemInterpProvider(InterpProvider):
6046
+ cmd: str = 'python3'
6047
+ path: ta.Optional[str] = None
5670
6048
 
5671
- #
6049
+ inspect: bool = False
6050
+ inspector: InterpInspector = INTERP_INSPECTOR
5672
6051
 
5673
- pyp_dct = {}
6052
+ #
5674
6053
 
5675
- pyp_dct['build-system'] = {
5676
- 'requires': ['setuptools'],
5677
- 'build-backend': 'setuptools.build_meta',
5678
- }
6054
+ @staticmethod
6055
+ def _re_which(
6056
+ pat: re.Pattern,
6057
+ *,
6058
+ mode: int = os.F_OK | os.X_OK,
6059
+ path: ta.Optional[str] = None,
6060
+ ) -> ta.List[str]:
6061
+ if path is None:
6062
+ path = os.environ.get('PATH', None)
6063
+ if path is None:
6064
+ try:
6065
+ path = os.confstr('CS_PATH')
6066
+ except (AttributeError, ValueError):
6067
+ path = os.defpath
5679
6068
 
5680
- prj = specs.pyproject
5681
- prj['dependencies'] = [f'{prj["name"]} == {prj["version"]}']
5682
- prj['name'] += self._pkg_suffix
5683
- for k in [
5684
- 'optional_dependencies',
5685
- 'entry_points',
5686
- 'scripts',
5687
- ]:
5688
- prj.pop(k, None)
6069
+ if not path:
6070
+ return []
5689
6071
 
5690
- pyp_dct['project'] = prj
6072
+ path = os.fsdecode(path)
6073
+ pathlst = path.split(os.pathsep)
5691
6074
 
5692
- if (scs := prj.pop('cli_scripts', None)):
5693
- pyp_dct['project.scripts'] = scs
6075
+ def _access_check(fn: str, mode: int) -> bool:
6076
+ return os.path.exists(fn) and os.access(fn, mode)
5694
6077
 
5695
- #
6078
+ out = []
6079
+ seen = set()
6080
+ for d in pathlst:
6081
+ normdir = os.path.normcase(d)
6082
+ if normdir not in seen:
6083
+ seen.add(normdir)
6084
+ if not _access_check(normdir, mode):
6085
+ continue
6086
+ for thefile in os.listdir(d):
6087
+ name = os.path.join(d, thefile)
6088
+ if not (
6089
+ os.path.isfile(name) and
6090
+ pat.fullmatch(thefile) and
6091
+ _access_check(name, mode)
6092
+ ):
6093
+ continue
6094
+ out.append(name)
5696
6095
 
5697
- st = dict(specs.setuptools)
5698
- pyp_dct['tool.setuptools'] = st
6096
+ return out
5699
6097
 
5700
- for k in [
5701
- 'cexts',
6098
+ @cached_nullary
6099
+ def exes(self) -> ta.List[str]:
6100
+ return self._re_which(
6101
+ re.compile(r'python3(\.\d+)?'),
6102
+ path=self.path,
6103
+ )
5702
6104
 
5703
- 'find_packages',
5704
- 'package_data',
5705
- 'manifest_in',
5706
- ]:
5707
- st.pop(k, None)
6105
+ #
5708
6106
 
5709
- pyp_dct['tool.setuptools.packages.find'] = {
5710
- 'include': [],
5711
- }
6107
+ async def get_exe_version(self, exe: str) -> ta.Optional[InterpVersion]:
6108
+ if not self.inspect:
6109
+ s = os.path.basename(exe)
6110
+ if s.startswith('python'):
6111
+ s = s[len('python'):]
6112
+ if '.' in s:
6113
+ try:
6114
+ return InterpVersion.parse(s)
6115
+ except InvalidVersion:
6116
+ pass
6117
+ ii = await self.inspector.inspect(exe)
6118
+ return ii.iv if ii is not None else None
5712
6119
 
5713
- #
6120
+ async def exe_versions(self) -> ta.Sequence[ta.Tuple[str, InterpVersion]]:
6121
+ lst = []
6122
+ for e in self.exes():
6123
+ if (ev := await self.get_exe_version(e)) is None:
6124
+ log.debug('Invalid system version: %s', e)
6125
+ continue
6126
+ lst.append((e, ev))
6127
+ return lst
5714
6128
 
5715
- return self.FileContents(
5716
- pyp_dct,
5717
- )
6129
+ #
5718
6130
 
5719
- def _write_file_contents(self) -> None:
5720
- fc = self.file_contents()
6131
+ async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
6132
+ return [ev for e, ev in await self.exe_versions()]
5721
6133
 
5722
- with open(os.path.join(self._pkg_dir(), 'pyproject.toml'), 'w') as f:
5723
- TomlWriter(f).write_root(fc.pyproject_dct)
6134
+ async def get_installed_version(self, version: InterpVersion) -> Interp:
6135
+ for e, ev in await self.exe_versions():
6136
+ if ev != version:
6137
+ continue
6138
+ return Interp(
6139
+ exe=e,
6140
+ version=ev,
6141
+ )
6142
+ raise KeyError(version)
5724
6143
 
5725
6144
 
5726
6145
  ########################################
@@ -5738,13 +6157,14 @@ class InterpResolver:
5738
6157
  providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
5739
6158
  ) -> None:
5740
6159
  super().__init__()
6160
+
5741
6161
  self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers)
5742
6162
 
5743
- def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
6163
+ async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
5744
6164
  lst = [
5745
6165
  (i, si)
5746
6166
  for i, p in enumerate(self._providers.values())
5747
- for si in p.get_installed_versions(spec)
6167
+ for si in await p.get_installed_versions(spec)
5748
6168
  if spec.contains(si)
5749
6169
  ]
5750
6170
 
@@ -5756,16 +6176,16 @@ class InterpResolver:
5756
6176
  bp = list(self._providers.values())[bi]
5757
6177
  return (bp, bv)
5758
6178
 
5759
- def resolve(
6179
+ async def resolve(
5760
6180
  self,
5761
6181
  spec: InterpSpecifier,
5762
6182
  *,
5763
6183
  install: bool = False,
5764
6184
  ) -> ta.Optional[Interp]:
5765
- tup = self._resolve_installed(spec)
6185
+ tup = await self._resolve_installed(spec)
5766
6186
  if tup is not None:
5767
6187
  bp, bv = tup
5768
- return bp.get_installed_version(bv)
6188
+ return await bp.get_installed_version(bv)
5769
6189
 
5770
6190
  if not install:
5771
6191
  return None
@@ -5773,21 +6193,21 @@ class InterpResolver:
5773
6193
  tp = list(self._providers.values())[0] # noqa
5774
6194
 
5775
6195
  sv = sorted(
5776
- [s for s in tp.get_installable_versions(spec) if s in spec],
6196
+ [s for s in await tp.get_installable_versions(spec) if s in spec],
5777
6197
  key=lambda s: s.version,
5778
6198
  )
5779
6199
  if not sv:
5780
6200
  return None
5781
6201
 
5782
6202
  bv = sv[-1]
5783
- return tp.install_version(bv)
6203
+ return await tp.install_version(bv)
5784
6204
 
5785
- def list(self, spec: InterpSpecifier) -> None:
6205
+ async def list(self, spec: InterpSpecifier) -> None:
5786
6206
  print('installed:')
5787
6207
  for n, p in self._providers.items():
5788
6208
  lst = [
5789
6209
  si
5790
- for si in p.get_installed_versions(spec)
6210
+ for si in await p.get_installed_versions(spec)
5791
6211
  if spec.contains(si)
5792
6212
  ]
5793
6213
  if lst:
@@ -5801,7 +6221,7 @@ class InterpResolver:
5801
6221
  for n, p in self._providers.items():
5802
6222
  lst = [
5803
6223
  si
5804
- for si in p.get_installable_versions(spec)
6224
+ for si in await p.get_installable_versions(spec)
5805
6225
  if spec.contains(si)
5806
6226
  ]
5807
6227
  if lst:
@@ -5892,10 +6312,10 @@ class Venv:
5892
6312
  def dir_name(self) -> str:
5893
6313
  return os.path.join(self.DIR_NAME, self._name)
5894
6314
 
5895
- @cached_nullary
5896
- def interp_exe(self) -> str:
6315
+ @async_cached_nullary
6316
+ async def interp_exe(self) -> str:
5897
6317
  i = InterpSpecifier.parse(check_not_none(self._cfg.interp))
5898
- return check_not_none(DEFAULT_INTERP_RESOLVER.resolve(i, install=True)).exe
6318
+ return check_not_none(await DEFAULT_INTERP_RESOLVER.resolve(i, install=True)).exe
5899
6319
 
5900
6320
  @cached_nullary
5901
6321
  def exe(self) -> str:
@@ -5904,14 +6324,14 @@ class Venv:
5904
6324
  raise Exception(f'venv exe {ve} does not exist or is not a file!')
5905
6325
  return ve
5906
6326
 
5907
- @cached_nullary
5908
- def create(self) -> bool:
6327
+ @async_cached_nullary
6328
+ async def create(self) -> bool:
5909
6329
  if os.path.exists(dn := self.dir_name):
5910
6330
  if not os.path.isdir(dn):
5911
6331
  raise Exception(f'{dn} exists but is not a directory!')
5912
6332
  return False
5913
6333
 
5914
- log.info('Using interpreter %s', (ie := self.interp_exe()))
6334
+ log.info('Using interpreter %s', (ie := await self.interp_exe()))
5915
6335
  subprocess_check_call(ie, '-m', 'venv', dn)
5916
6336
 
5917
6337
  ve = self.exe()
@@ -6011,7 +6431,7 @@ class Run:
6011
6431
  ##
6012
6432
 
6013
6433
 
6014
- def _venv_cmd(args) -> None:
6434
+ async def _venv_cmd(args) -> None:
6015
6435
  venv = Run().venvs()[args.name]
6016
6436
  if (sd := venv.cfg.docker) is not None and sd != (cd := args._docker_container): # noqa
6017
6437
  script = ' '.join([
@@ -6048,10 +6468,10 @@ def _venv_cmd(args) -> None:
6048
6468
 
6049
6469
  cmd = args.cmd
6050
6470
  if not cmd:
6051
- venv.create()
6471
+ await venv.create()
6052
6472
 
6053
6473
  elif cmd == 'python':
6054
- venv.create()
6474
+ await venv.create()
6055
6475
  os.execl(
6056
6476
  (exe := venv.exe()),
6057
6477
  exe,
@@ -6059,12 +6479,12 @@ def _venv_cmd(args) -> None:
6059
6479
  )
6060
6480
 
6061
6481
  elif cmd == 'exe':
6062
- venv.create()
6482
+ await venv.create()
6063
6483
  check_not(args.args)
6064
6484
  print(venv.exe())
6065
6485
 
6066
6486
  elif cmd == 'run':
6067
- venv.create()
6487
+ await venv.create()
6068
6488
  sh = check_not_none(shutil.which('bash'))
6069
6489
  script = ' '.join(args.args)
6070
6490
  if not script:
@@ -6081,7 +6501,7 @@ def _venv_cmd(args) -> None:
6081
6501
  print('\n'.join(venv.srcs()))
6082
6502
 
6083
6503
  elif cmd == 'test':
6084
- venv.create()
6504
+ await venv.create()
6085
6505
  subprocess_check_call(venv.exe(), '-m', 'pytest', *(args.args or []), *venv.srcs())
6086
6506
 
6087
6507
  else:
@@ -6091,7 +6511,7 @@ def _venv_cmd(args) -> None:
6091
6511
  ##
6092
6512
 
6093
6513
 
6094
- def _pkg_cmd(args) -> None:
6514
+ async def _pkg_cmd(args) -> None:
6095
6515
  run = Run()
6096
6516
 
6097
6517
  cmd = args.cmd
@@ -6178,7 +6598,7 @@ def _build_parser() -> argparse.ArgumentParser:
6178
6598
  return parser
6179
6599
 
6180
6600
 
6181
- def _main(argv: ta.Optional[ta.Sequence[str]] = None) -> None:
6601
+ async def _async_main(argv: ta.Optional[ta.Sequence[str]] = None) -> None:
6182
6602
  check_runtime_version()
6183
6603
  configure_standard_logging()
6184
6604
 
@@ -6187,7 +6607,11 @@ def _main(argv: ta.Optional[ta.Sequence[str]] = None) -> None:
6187
6607
  if not getattr(args, 'func', None):
6188
6608
  parser.print_help()
6189
6609
  else:
6190
- args.func(args)
6610
+ await args.func(args)
6611
+
6612
+
6613
+ def _main(argv: ta.Optional[ta.Sequence[str]] = None) -> None:
6614
+ asyncio.run(_async_main(argv))
6191
6615
 
6192
6616
 
6193
6617
  if __name__ == '__main__':