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