omdev 0.0.0.dev151__py3-none-any.whl → 0.0.0.dev153__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.

@@ -92,7 +92,7 @@ TomlParseFloat = ta.Callable[[str], ta.Any]
92
92
  TomlKey = ta.Tuple[str, ...]
93
93
  TomlPos = int # ta.TypeAlias
94
94
 
95
- # ../../omlish/lite/asyncio/asyncio.py
95
+ # ../../omlish/asyncs/asyncio/timeouts.py
96
96
  AwaitableT = ta.TypeVar('AwaitableT', bound=ta.Awaitable)
97
97
 
98
98
  # ../../omlish/lite/cached.py
@@ -112,8 +112,11 @@ UnparsedVersion = ta.Union['Version', str]
112
112
  UnparsedVersionVar = ta.TypeVar('UnparsedVersionVar', bound=UnparsedVersion)
113
113
  CallableVersionOperator = ta.Callable[['Version', str], bool]
114
114
 
115
+ # ../../omlish/argparse/cli.py
116
+ ArgparseCommandFn = ta.Callable[[], ta.Optional[int]] # ta.TypeAlias
117
+
115
118
  # ../../omlish/lite/subprocesses.py
116
- SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull']
119
+ SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
117
120
 
118
121
 
119
122
  ########################################
@@ -1776,54 +1779,7 @@ class WheelFile(zipfile.ZipFile):
1776
1779
 
1777
1780
 
1778
1781
  ########################################
1779
- # ../../../omlish/lite/asyncio/asyncio.py
1780
-
1781
-
1782
- ##
1783
-
1784
-
1785
- ASYNCIO_DEFAULT_BUFFER_LIMIT = 2 ** 16
1786
-
1787
-
1788
- async def asyncio_open_stream_reader(
1789
- f: ta.IO,
1790
- loop: ta.Any = None,
1791
- *,
1792
- limit: int = ASYNCIO_DEFAULT_BUFFER_LIMIT,
1793
- ) -> asyncio.StreamReader:
1794
- if loop is None:
1795
- loop = asyncio.get_running_loop()
1796
-
1797
- reader = asyncio.StreamReader(limit=limit, loop=loop)
1798
- await loop.connect_read_pipe(
1799
- lambda: asyncio.StreamReaderProtocol(reader, loop=loop),
1800
- f,
1801
- )
1802
-
1803
- return reader
1804
-
1805
-
1806
- async def asyncio_open_stream_writer(
1807
- f: ta.IO,
1808
- loop: ta.Any = None,
1809
- ) -> asyncio.StreamWriter:
1810
- if loop is None:
1811
- loop = asyncio.get_running_loop()
1812
-
1813
- writer_transport, writer_protocol = await loop.connect_write_pipe(
1814
- lambda: asyncio.streams.FlowControlMixin(loop=loop),
1815
- f,
1816
- )
1817
-
1818
- return asyncio.streams.StreamWriter(
1819
- writer_transport,
1820
- writer_protocol,
1821
- None,
1822
- loop,
1823
- )
1824
-
1825
-
1826
- ##
1782
+ # ../../../omlish/asyncs/asyncio/timeouts.py
1827
1783
 
1828
1784
 
1829
1785
  def asyncio_maybe_timeout(
@@ -3253,6 +3209,278 @@ class SpecifierSet(BaseSpecifier):
3253
3209
  return iter(filtered)
3254
3210
 
3255
3211
 
3212
+ ########################################
3213
+ # ../../../omlish/argparse/cli.py
3214
+ """
3215
+ TODO:
3216
+ - default command
3217
+ - auto match all underscores to hyphens
3218
+ - pre-run, post-run hooks
3219
+ - exitstack?
3220
+ """
3221
+
3222
+
3223
+ ##
3224
+
3225
+
3226
+ @dc.dataclass(eq=False)
3227
+ class ArgparseArg:
3228
+ args: ta.Sequence[ta.Any]
3229
+ kwargs: ta.Mapping[str, ta.Any]
3230
+ dest: ta.Optional[str] = None
3231
+
3232
+ def __get__(self, instance, owner=None):
3233
+ if instance is None:
3234
+ return self
3235
+ return getattr(instance.args, self.dest) # type: ignore
3236
+
3237
+
3238
+ def argparse_arg(*args, **kwargs) -> ArgparseArg:
3239
+ return ArgparseArg(args, kwargs)
3240
+
3241
+
3242
+ #
3243
+
3244
+
3245
+ @dc.dataclass(eq=False)
3246
+ class ArgparseCommand:
3247
+ name: str
3248
+ fn: ArgparseCommandFn
3249
+ args: ta.Sequence[ArgparseArg] = () # noqa
3250
+
3251
+ # _: dc.KW_ONLY
3252
+
3253
+ aliases: ta.Optional[ta.Sequence[str]] = None
3254
+ parent: ta.Optional['ArgparseCommand'] = None
3255
+ accepts_unknown: bool = False
3256
+
3257
+ def __post_init__(self) -> None:
3258
+ def check_name(s: str) -> None:
3259
+ check.isinstance(s, str)
3260
+ check.not_in('_', s)
3261
+ check.not_empty(s)
3262
+ check_name(self.name)
3263
+ check.not_isinstance(self.aliases, str)
3264
+ for a in self.aliases or []:
3265
+ check_name(a)
3266
+
3267
+ check.arg(callable(self.fn))
3268
+ check.arg(all(isinstance(a, ArgparseArg) for a in self.args))
3269
+ check.isinstance(self.parent, (ArgparseCommand, type(None)))
3270
+ check.isinstance(self.accepts_unknown, bool)
3271
+
3272
+ functools.update_wrapper(self, self.fn)
3273
+
3274
+ def __get__(self, instance, owner=None):
3275
+ if instance is None:
3276
+ return self
3277
+ return dc.replace(self, fn=self.fn.__get__(instance, owner)) # noqa
3278
+
3279
+ def __call__(self, *args, **kwargs) -> ta.Optional[int]:
3280
+ return self.fn(*args, **kwargs)
3281
+
3282
+
3283
+ def argparse_command(
3284
+ *args: ArgparseArg,
3285
+ name: ta.Optional[str] = None,
3286
+ aliases: ta.Optional[ta.Iterable[str]] = None,
3287
+ parent: ta.Optional[ArgparseCommand] = None,
3288
+ accepts_unknown: bool = False,
3289
+ ) -> ta.Any: # ta.Callable[[ArgparseCommandFn], ArgparseCommand]: # FIXME
3290
+ for arg in args:
3291
+ check.isinstance(arg, ArgparseArg)
3292
+ check.isinstance(name, (str, type(None)))
3293
+ check.isinstance(parent, (ArgparseCommand, type(None)))
3294
+ check.not_isinstance(aliases, str)
3295
+
3296
+ def inner(fn):
3297
+ return ArgparseCommand(
3298
+ (name if name is not None else fn.__name__).replace('_', '-'),
3299
+ fn,
3300
+ args,
3301
+ aliases=tuple(aliases) if aliases is not None else None,
3302
+ parent=parent,
3303
+ accepts_unknown=accepts_unknown,
3304
+ )
3305
+
3306
+ return inner
3307
+
3308
+
3309
+ ##
3310
+
3311
+
3312
+ def _get_argparse_arg_ann_kwargs(ann: ta.Any) -> ta.Mapping[str, ta.Any]:
3313
+ if ann is str:
3314
+ return {}
3315
+ elif ann is int:
3316
+ return {'type': int}
3317
+ elif ann is bool:
3318
+ return {'action': 'store_true'}
3319
+ elif ann is list:
3320
+ return {'action': 'append'}
3321
+ else:
3322
+ raise TypeError(ann)
3323
+
3324
+
3325
+ class _ArgparseCliAnnotationBox:
3326
+ def __init__(self, annotations: ta.Mapping[str, ta.Any]) -> None:
3327
+ super().__init__()
3328
+ self.__annotations__ = annotations # type: ignore
3329
+
3330
+
3331
+ class ArgparseCli:
3332
+ def __init__(self, argv: ta.Optional[ta.Sequence[str]] = None) -> None:
3333
+ super().__init__()
3334
+
3335
+ self._argv = argv if argv is not None else sys.argv[1:]
3336
+
3337
+ self._args, self._unknown_args = self.get_parser().parse_known_args(self._argv)
3338
+
3339
+ #
3340
+
3341
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
3342
+ super().__init_subclass__(**kwargs)
3343
+
3344
+ ns = cls.__dict__
3345
+ objs = {}
3346
+ mro = cls.__mro__[::-1]
3347
+ for bns in [bcls.__dict__ for bcls in reversed(mro)] + [ns]:
3348
+ bseen = set() # type: ignore
3349
+ for k, v in bns.items():
3350
+ if isinstance(v, (ArgparseCommand, ArgparseArg)):
3351
+ check.not_in(v, bseen)
3352
+ bseen.add(v)
3353
+ objs[k] = v
3354
+ elif k in objs:
3355
+ del [k]
3356
+
3357
+ #
3358
+
3359
+ anns = ta.get_type_hints(_ArgparseCliAnnotationBox({
3360
+ **{k: v for bcls in reversed(mro) for k, v in getattr(bcls, '__annotations__', {}).items()},
3361
+ **ns.get('__annotations__', {}),
3362
+ }), globalns=ns.get('__globals__', {}))
3363
+
3364
+ #
3365
+
3366
+ if '_parser' in ns:
3367
+ parser = check.isinstance(ns['_parser'], argparse.ArgumentParser)
3368
+ else:
3369
+ parser = argparse.ArgumentParser()
3370
+ setattr(cls, '_parser', parser)
3371
+
3372
+ #
3373
+
3374
+ subparsers = parser.add_subparsers()
3375
+
3376
+ for att, obj in objs.items():
3377
+ if isinstance(obj, ArgparseCommand):
3378
+ if obj.parent is not None:
3379
+ raise NotImplementedError
3380
+
3381
+ for cn in [obj.name, *(obj.aliases or [])]:
3382
+ subparser = subparsers.add_parser(cn)
3383
+
3384
+ for arg in (obj.args or []):
3385
+ if (
3386
+ len(arg.args) == 1 and
3387
+ isinstance(arg.args[0], str) and
3388
+ not (n := check.isinstance(arg.args[0], str)).startswith('-') and
3389
+ 'metavar' not in arg.kwargs
3390
+ ):
3391
+ subparser.add_argument(
3392
+ n.replace('-', '_'),
3393
+ **arg.kwargs,
3394
+ metavar=n,
3395
+ )
3396
+ else:
3397
+ subparser.add_argument(*arg.args, **arg.kwargs)
3398
+
3399
+ subparser.set_defaults(_cmd=obj)
3400
+
3401
+ elif isinstance(obj, ArgparseArg):
3402
+ if att in anns:
3403
+ ann_kwargs = _get_argparse_arg_ann_kwargs(anns[att])
3404
+ obj.kwargs = {**ann_kwargs, **obj.kwargs}
3405
+
3406
+ if not obj.dest:
3407
+ if 'dest' in obj.kwargs:
3408
+ obj.dest = obj.kwargs['dest']
3409
+ else:
3410
+ obj.dest = obj.kwargs['dest'] = att # type: ignore
3411
+
3412
+ parser.add_argument(*obj.args, **obj.kwargs)
3413
+
3414
+ else:
3415
+ raise TypeError(obj)
3416
+
3417
+ #
3418
+
3419
+ _parser: ta.ClassVar[argparse.ArgumentParser]
3420
+
3421
+ @classmethod
3422
+ def get_parser(cls) -> argparse.ArgumentParser:
3423
+ return cls._parser
3424
+
3425
+ @property
3426
+ def argv(self) -> ta.Sequence[str]:
3427
+ return self._argv
3428
+
3429
+ @property
3430
+ def args(self) -> argparse.Namespace:
3431
+ return self._args
3432
+
3433
+ @property
3434
+ def unknown_args(self) -> ta.Sequence[str]:
3435
+ return self._unknown_args
3436
+
3437
+ #
3438
+
3439
+ def _bind_cli_cmd(self, cmd: ArgparseCommand) -> ta.Callable:
3440
+ return cmd.__get__(self, type(self))
3441
+
3442
+ def prepare_cli_run(self) -> ta.Optional[ta.Callable]:
3443
+ cmd = getattr(self.args, '_cmd', None)
3444
+
3445
+ if self._unknown_args and not (cmd is not None and cmd.accepts_unknown):
3446
+ msg = f'unrecognized arguments: {" ".join(self._unknown_args)}'
3447
+ if (parser := self.get_parser()).exit_on_error: # type: ignore
3448
+ parser.error(msg)
3449
+ else:
3450
+ raise argparse.ArgumentError(None, msg)
3451
+
3452
+ if cmd is None:
3453
+ self.get_parser().print_help()
3454
+ return None
3455
+
3456
+ return self._bind_cli_cmd(cmd)
3457
+
3458
+ #
3459
+
3460
+ def cli_run(self) -> ta.Optional[int]:
3461
+ if (fn := self.prepare_cli_run()) is None:
3462
+ return 0
3463
+
3464
+ return fn()
3465
+
3466
+ def cli_run_and_exit(self) -> ta.NoReturn:
3467
+ sys.exit(rc if isinstance(rc := self.cli_run(), int) else 0)
3468
+
3469
+ def __call__(self, *, exit: bool = False) -> ta.Optional[int]: # noqa
3470
+ if exit:
3471
+ return self.cli_run_and_exit()
3472
+ else:
3473
+ return self.cli_run()
3474
+
3475
+ #
3476
+
3477
+ async def async_cli_run(self) -> ta.Optional[int]:
3478
+ if (fn := self.prepare_cli_run()) is None:
3479
+ return 0
3480
+
3481
+ return await fn()
3482
+
3483
+
3256
3484
  ########################################
3257
3485
  # ../../../omlish/lite/logs.py
3258
3486
  """
@@ -4994,22 +5222,25 @@ async def asyncio_subprocess_communicate(
4994
5222
  return await AsyncioProcessCommunicator(proc).communicate(input, timeout) # noqa
4995
5223
 
4996
5224
 
4997
- ##
4998
-
4999
-
5000
- async def _asyncio_subprocess_check_run(
5225
+ async def asyncio_subprocess_run(
5001
5226
  *args: str,
5002
5227
  input: ta.Any = None, # noqa
5003
5228
  timeout: ta.Optional[float] = None,
5229
+ check: bool = False, # noqa
5230
+ capture_output: ta.Optional[bool] = None,
5004
5231
  **kwargs: ta.Any,
5005
5232
  ) -> ta.Tuple[ta.Optional[bytes], ta.Optional[bytes]]:
5233
+ if capture_output:
5234
+ kwargs.setdefault('stdout', subprocess.PIPE)
5235
+ kwargs.setdefault('stderr', subprocess.PIPE)
5236
+
5006
5237
  args, kwargs = prepare_subprocess_invocation(*args, **kwargs)
5007
5238
 
5008
5239
  proc: asyncio.subprocess.Process
5009
5240
  async with asyncio_subprocess_popen(*args, **kwargs) as proc:
5010
5241
  stdout, stderr = await asyncio_subprocess_communicate(proc, input, timeout)
5011
5242
 
5012
- if proc.returncode:
5243
+ if check and proc.returncode:
5013
5244
  raise subprocess.CalledProcessError(
5014
5245
  proc.returncode,
5015
5246
  args,
@@ -5020,6 +5251,9 @@ async def _asyncio_subprocess_check_run(
5020
5251
  return stdout, stderr
5021
5252
 
5022
5253
 
5254
+ ##
5255
+
5256
+
5023
5257
  async def asyncio_subprocess_check_call(
5024
5258
  *args: str,
5025
5259
  stdout: ta.Any = sys.stderr,
@@ -5027,11 +5261,12 @@ async def asyncio_subprocess_check_call(
5027
5261
  timeout: ta.Optional[float] = None,
5028
5262
  **kwargs: ta.Any,
5029
5263
  ) -> None:
5030
- _, _ = await _asyncio_subprocess_check_run(
5264
+ _, _ = await asyncio_subprocess_run(
5031
5265
  *args,
5032
5266
  stdout=stdout,
5033
5267
  input=input,
5034
5268
  timeout=timeout,
5269
+ check=True,
5035
5270
  **kwargs,
5036
5271
  )
5037
5272
 
@@ -5042,11 +5277,12 @@ async def asyncio_subprocess_check_output(
5042
5277
  timeout: ta.Optional[float] = None,
5043
5278
  **kwargs: ta.Any,
5044
5279
  ) -> bytes:
5045
- stdout, stderr = await _asyncio_subprocess_check_run(
5280
+ stdout, stderr = await asyncio_subprocess_run(
5046
5281
  *args,
5047
5282
  stdout=asyncio.subprocess.PIPE,
5048
5283
  input=input,
5049
5284
  timeout=timeout,
5285
+ check=True,
5050
5286
  **kwargs,
5051
5287
  )
5052
5288
 
@@ -6601,52 +6837,7 @@ DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
6601
6837
 
6602
6838
 
6603
6839
  ########################################
6604
- # cli.py
6605
-
6606
-
6607
- ##
6608
-
6609
-
6610
- @dc.dataclass(frozen=True)
6611
- class VersionsFile:
6612
- name: ta.Optional[str] = '.versions'
6613
-
6614
- @staticmethod
6615
- def parse(s: str) -> ta.Mapping[str, str]:
6616
- return {
6617
- k: v
6618
- for l in s.splitlines()
6619
- if (sl := l.split('#')[0].strip())
6620
- for k, _, v in (sl.partition('='),)
6621
- }
6622
-
6623
- @cached_nullary
6624
- def contents(self) -> ta.Mapping[str, str]:
6625
- if not self.name or not os.path.exists(self.name):
6626
- return {}
6627
- with open(self.name) as f:
6628
- s = f.read()
6629
- return self.parse(s)
6630
-
6631
- @staticmethod
6632
- def get_pythons(d: ta.Mapping[str, str]) -> ta.Mapping[str, str]:
6633
- pfx = 'PYTHON_'
6634
- return {k[len(pfx):].lower(): v for k, v in d.items() if k.startswith(pfx)}
6635
-
6636
- @cached_nullary
6637
- def pythons(self) -> ta.Mapping[str, str]:
6638
- return self.get_pythons(self.contents())
6639
-
6640
-
6641
- ##
6642
-
6643
-
6644
- @cached_nullary
6645
- def _script_rel_path() -> str:
6646
- cwd = os.getcwd()
6647
- if not (f := __file__).startswith(cwd):
6648
- raise OSError(f'file {f} not in {cwd}')
6649
- return f[len(cwd):].lstrip(os.sep)
6840
+ # ../venvs.py
6650
6841
 
6651
6842
 
6652
6843
  ##
@@ -6692,12 +6883,12 @@ class Venv:
6692
6883
  return False
6693
6884
 
6694
6885
  log.info('Using interpreter %s', (ie := await self.interp_exe()))
6695
- subprocess_check_call(ie, '-m', 'venv', dn)
6886
+ await asyncio_subprocess_check_call(ie, '-m', 'venv', dn)
6696
6887
 
6697
6888
  ve = self.exe()
6698
6889
  uv = self._cfg.use_uv
6699
6890
 
6700
- subprocess_check_call(
6891
+ await asyncio_subprocess_check_call(
6701
6892
  ve,
6702
6893
  '-m', 'pip',
6703
6894
  'install', '-v', '--upgrade',
@@ -6715,7 +6906,7 @@ class Venv:
6715
6906
  # Caused by: Failed to download distribution due to network timeout. Try increasing UV_HTTP_TIMEOUT (current value: 30s). # noqa
6716
6907
  # UV_CONCURRENT_DOWNLOADS=4 UV_HTTP_TIMEOUT=3600
6717
6908
 
6718
- subprocess_check_call(
6909
+ await asyncio_subprocess_check_call(
6719
6910
  ve,
6720
6911
  '-m',
6721
6912
  *(['uv'] if uv else []),
@@ -6748,6 +6939,55 @@ class Venv:
6748
6939
  return self._resolve_srcs(self._cfg.srcs or [])
6749
6940
 
6750
6941
 
6942
+ ########################################
6943
+ # cli.py
6944
+
6945
+
6946
+ ##
6947
+
6948
+
6949
+ @dc.dataclass(frozen=True)
6950
+ class VersionsFile:
6951
+ name: ta.Optional[str] = '.versions'
6952
+
6953
+ @staticmethod
6954
+ def parse(s: str) -> ta.Mapping[str, str]:
6955
+ return {
6956
+ k: v
6957
+ for l in s.splitlines()
6958
+ if (sl := l.split('#')[0].strip())
6959
+ for k, _, v in (sl.partition('='),)
6960
+ }
6961
+
6962
+ @cached_nullary
6963
+ def contents(self) -> ta.Mapping[str, str]:
6964
+ if not self.name or not os.path.exists(self.name):
6965
+ return {}
6966
+ with open(self.name) as f:
6967
+ s = f.read()
6968
+ return self.parse(s)
6969
+
6970
+ @staticmethod
6971
+ def get_pythons(d: ta.Mapping[str, str]) -> ta.Mapping[str, str]:
6972
+ pfx = 'PYTHON_'
6973
+ return {k[len(pfx):].lower(): v for k, v in d.items() if k.startswith(pfx)}
6974
+
6975
+ @cached_nullary
6976
+ def pythons(self) -> ta.Mapping[str, str]:
6977
+ return self.get_pythons(self.contents())
6978
+
6979
+
6980
+ ##
6981
+
6982
+
6983
+ @cached_nullary
6984
+ def _script_rel_path() -> str:
6985
+ cwd = os.getcwd()
6986
+ if not (f := __file__).startswith(cwd):
6987
+ raise OSError(f'file {f} not in {cwd}')
6988
+ return f[len(cwd):].lstrip(os.sep)
6989
+
6990
+
6751
6991
  ##
6752
6992
 
6753
6993
 
@@ -6791,183 +7031,160 @@ class Run:
6791
7031
  ##
6792
7032
 
6793
7033
 
6794
- async def _venv_cmd(args) -> None:
6795
- venv = Run().venvs()[args.name]
6796
- if (sd := venv.cfg.docker) is not None and sd != (cd := args._docker_container): # noqa
6797
- script = ' '.join([
6798
- 'python3',
6799
- shlex.quote(_script_rel_path()),
6800
- f'--_docker_container={shlex.quote(sd)}',
6801
- *map(shlex.quote, sys.argv[1:]),
6802
- ])
6803
-
6804
- docker_env = {
6805
- 'DOCKER_HOST_PLATFORM': os.environ.get('DOCKER_HOST_PLATFORM', sys.platform),
6806
- }
6807
- for e in args.docker_env or []:
6808
- if '=' in e:
6809
- k, _, v = e.split('=')
6810
- docker_env[k] = v
6811
- else:
6812
- docker_env[e] = os.environ.get(e, '')
6813
-
6814
- subprocess_check_call(
6815
- 'docker',
6816
- 'compose',
6817
- '-f', 'docker/compose.yml',
6818
- 'exec',
6819
- *itertools.chain.from_iterable(
6820
- ('-e', f'{k}={v}')
6821
- for k, v in docker_env.items()
6822
- ),
6823
- '-it', sd,
6824
- 'bash', '--login', '-c', script,
6825
- )
6826
-
6827
- return
6828
-
6829
- cmd = args.cmd
6830
- if not cmd:
6831
- await venv.create()
6832
-
6833
- elif cmd == 'python':
6834
- await venv.create()
6835
- os.execl(
6836
- (exe := venv.exe()),
6837
- exe,
6838
- *args.args,
6839
- )
7034
+ class PyprojectCli(ArgparseCli):
7035
+ _docker_container = argparse_arg('--_docker_container', help=argparse.SUPPRESS)
6840
7036
 
6841
- elif cmd == 'exe':
6842
- await venv.create()
6843
- check.arg(not args.args)
6844
- print(venv.exe())
6845
-
6846
- elif cmd == 'run':
6847
- await venv.create()
6848
- sh = check.not_none(shutil.which('bash'))
6849
- script = ' '.join(args.args)
6850
- if not script:
6851
- script = sh
6852
- os.execl(
6853
- (bash := check.not_none(sh)),
6854
- bash,
6855
- '-c',
6856
- f'. {venv.dir_name}/bin/activate && ' + script,
6857
- )
7037
+ @argparse_command(
7038
+ argparse_arg('name'),
7039
+ argparse_arg('-e', '--docker-env', action='append'),
7040
+ argparse_arg('cmd', nargs='?'),
7041
+ argparse_arg('args', nargs=argparse.REMAINDER),
7042
+ )
7043
+ async def venv(self) -> None:
7044
+ venv = Run().venvs()[self.args.name]
7045
+ if (sd := venv.cfg.docker) is not None and sd != (cd := self.args._docker_container): # noqa
7046
+ script = ' '.join([
7047
+ 'python3',
7048
+ shlex.quote(_script_rel_path()),
7049
+ f'--_docker_container={shlex.quote(sd)}',
7050
+ *map(shlex.quote, sys.argv[1:]),
7051
+ ])
6858
7052
 
6859
- elif cmd == 'srcs':
6860
- check.arg(not args.args)
6861
- print('\n'.join(venv.srcs()))
7053
+ docker_env = {
7054
+ 'DOCKER_HOST_PLATFORM': os.environ.get('DOCKER_HOST_PLATFORM', sys.platform),
7055
+ }
7056
+ for e in self.args.docker_env or []:
7057
+ if '=' in e:
7058
+ k, _, v = e.split('=')
7059
+ docker_env[k] = v
7060
+ else:
7061
+ docker_env[e] = os.environ.get(e, '')
7062
+
7063
+ await asyncio_subprocess_check_call(
7064
+ 'docker',
7065
+ 'compose',
7066
+ '-f', 'docker/compose.yml',
7067
+ 'exec',
7068
+ *itertools.chain.from_iterable(
7069
+ ('-e', f'{k}={v}')
7070
+ for k, v in docker_env.items()
7071
+ ),
7072
+ '-it', sd,
7073
+ 'bash', '--login', '-c', script,
7074
+ )
6862
7075
 
6863
- elif cmd == 'test':
6864
- await venv.create()
6865
- subprocess_check_call(venv.exe(), '-m', 'pytest', *(args.args or []), *venv.srcs())
7076
+ return
6866
7077
 
6867
- else:
6868
- raise Exception(f'unknown subcommand: {cmd}')
7078
+ cmd = self.args.cmd
7079
+ if not cmd:
7080
+ await venv.create()
6869
7081
 
7082
+ elif cmd == 'python':
7083
+ await venv.create()
7084
+ os.execl(
7085
+ (exe := venv.exe()),
7086
+ exe,
7087
+ *self.args.args,
7088
+ )
6870
7089
 
6871
- ##
7090
+ elif cmd == 'exe':
7091
+ await venv.create()
7092
+ check.arg(not self.args.args)
7093
+ print(venv.exe())
7094
+
7095
+ elif cmd == 'run':
7096
+ await venv.create()
7097
+ sh = check.not_none(shutil.which('bash'))
7098
+ script = ' '.join(self.args.args)
7099
+ if not script:
7100
+ script = sh
7101
+ os.execl(
7102
+ (bash := check.not_none(sh)),
7103
+ bash,
7104
+ '-c',
7105
+ f'. {venv.dir_name}/bin/activate && ' + script,
7106
+ )
6872
7107
 
7108
+ elif cmd == 'srcs':
7109
+ check.arg(not self.args.args)
7110
+ print('\n'.join(venv.srcs()))
6873
7111
 
6874
- async def _pkg_cmd(args) -> None:
6875
- run = Run()
7112
+ elif cmd == 'test':
7113
+ await venv.create()
7114
+ await asyncio_subprocess_check_call(venv.exe(), '-m', 'pytest', *(self.args.args or []), *venv.srcs())
6876
7115
 
6877
- cmd = args.cmd
6878
- if not cmd:
6879
- raise Exception('must specify command')
7116
+ else:
7117
+ raise Exception(f'unknown subcommand: {cmd}')
7118
+
7119
+ @argparse_command(
7120
+ argparse_arg('-b', '--build', action='store_true'),
7121
+ argparse_arg('-r', '--revision', action='store_true'),
7122
+ argparse_arg('-j', '--jobs', type=int),
7123
+ argparse_arg('cmd', nargs='?'),
7124
+ argparse_arg('args', nargs=argparse.REMAINDER),
7125
+ )
7126
+ async def pkg(self) -> None:
7127
+ run = Run()
6880
7128
 
6881
- elif cmd == 'gen':
6882
- pkgs_root = os.path.join('.pkg')
7129
+ cmd = self.args.cmd
7130
+ if not cmd:
7131
+ raise Exception('must specify command')
6883
7132
 
6884
- if os.path.exists(pkgs_root):
6885
- shutil.rmtree(pkgs_root)
7133
+ elif cmd == 'gen':
7134
+ pkgs_root = os.path.join('.pkg')
6886
7135
 
6887
- build_output_dir = 'dist'
6888
- run_build = bool(args.build)
6889
- add_revision = bool(args.revision)
7136
+ if os.path.exists(pkgs_root):
7137
+ shutil.rmtree(pkgs_root)
6890
7138
 
6891
- if run_build:
6892
- os.makedirs(build_output_dir, exist_ok=True)
7139
+ build_output_dir = 'dist'
7140
+ run_build = bool(self.args.build)
7141
+ add_revision = bool(self.args.revision)
6893
7142
 
6894
- pgs: ta.List[BasePyprojectPackageGenerator] = [
6895
- PyprojectPackageGenerator(
6896
- dir_name,
6897
- pkgs_root,
6898
- )
6899
- for dir_name in run.cfg().pkgs
6900
- ]
6901
- pgs = list(itertools.chain.from_iterable([pg, *pg.children()] for pg in pgs))
7143
+ if run_build:
7144
+ os.makedirs(build_output_dir, exist_ok=True)
6902
7145
 
6903
- num_threads = args.jobs or int(max(mp.cpu_count() // 1.5, 1))
6904
- futs: ta.List[cf.Future]
6905
- with cf.ThreadPoolExecutor(num_threads) as ex:
6906
- futs = [ex.submit(pg.gen) for pg in pgs]
6907
- for fut in futs:
6908
- fut.result()
7146
+ pgs: ta.List[BasePyprojectPackageGenerator] = [
7147
+ PyprojectPackageGenerator(
7148
+ dir_name,
7149
+ pkgs_root,
7150
+ )
7151
+ for dir_name in run.cfg().pkgs
7152
+ ]
7153
+ pgs = list(itertools.chain.from_iterable([pg, *pg.children()] for pg in pgs))
6909
7154
 
6910
- if run_build:
6911
- futs = [
6912
- ex.submit(functools.partial(
6913
- pg.build,
6914
- build_output_dir,
6915
- BasePyprojectPackageGenerator.BuildOpts(
6916
- add_revision=add_revision,
6917
- ),
6918
- ))
6919
- for pg in pgs
6920
- ]
7155
+ num_threads = self.args.jobs or int(max(mp.cpu_count() // 1.5, 1))
7156
+ futs: ta.List[cf.Future]
7157
+ with cf.ThreadPoolExecutor(num_threads) as ex:
7158
+ futs = [ex.submit(pg.gen) for pg in pgs]
6921
7159
  for fut in futs:
6922
7160
  fut.result()
6923
7161
 
6924
- else:
6925
- raise Exception(f'unknown subcommand: {cmd}')
6926
-
6927
-
6928
- ##
6929
-
6930
-
6931
- def _build_parser() -> argparse.ArgumentParser:
6932
- parser = argparse.ArgumentParser()
6933
- parser.add_argument('--_docker_container', help=argparse.SUPPRESS)
6934
-
6935
- subparsers = parser.add_subparsers()
6936
-
6937
- #
6938
-
6939
- parser_venv = subparsers.add_parser('venv')
6940
- parser_venv.add_argument('name')
6941
- parser_venv.add_argument('-e', '--docker-env', action='append')
6942
- parser_venv.add_argument('cmd', nargs='?')
6943
- parser_venv.add_argument('args', nargs=argparse.REMAINDER)
6944
- parser_venv.set_defaults(func=_venv_cmd)
6945
-
6946
- #
7162
+ if run_build:
7163
+ futs = [
7164
+ ex.submit(functools.partial(
7165
+ pg.build,
7166
+ build_output_dir,
7167
+ BasePyprojectPackageGenerator.BuildOpts(
7168
+ add_revision=add_revision,
7169
+ ),
7170
+ ))
7171
+ for pg in pgs
7172
+ ]
7173
+ for fut in futs:
7174
+ fut.result()
6947
7175
 
6948
- parser_pkg = subparsers.add_parser('pkg')
6949
- parser_pkg.add_argument('-b', '--build', action='store_true')
6950
- parser_pkg.add_argument('-r', '--revision', action='store_true')
6951
- parser_pkg.add_argument('-j', '--jobs', type=int)
6952
- parser_pkg.add_argument('cmd', nargs='?')
6953
- parser_pkg.add_argument('args', nargs=argparse.REMAINDER)
6954
- parser_pkg.set_defaults(func=_pkg_cmd)
7176
+ else:
7177
+ raise Exception(f'unknown subcommand: {cmd}')
6955
7178
 
6956
- #
6957
7179
 
6958
- return parser
7180
+ ##
6959
7181
 
6960
7182
 
6961
7183
  async def _async_main(argv: ta.Optional[ta.Sequence[str]] = None) -> None:
6962
7184
  check_runtime_version()
6963
7185
  configure_standard_logging()
6964
7186
 
6965
- parser = _build_parser()
6966
- args = parser.parse_args(argv)
6967
- if not getattr(args, 'func', None):
6968
- parser.print_help()
6969
- else:
6970
- await args.func(args)
7187
+ await PyprojectCli(argv).async_cli_run()
6971
7188
 
6972
7189
 
6973
7190
  def _main(argv: ta.Optional[ta.Sequence[str]] = None) -> None: