ominfra 0.0.0.dev155__py3-none-any.whl → 0.0.0.dev157__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- ominfra/manage/commands/subprocess.py +3 -4
- ominfra/manage/deploy/git.py +3 -3
- ominfra/manage/deploy/venvs.py +4 -4
- ominfra/manage/main.py +33 -4
- ominfra/manage/remote/spawning.py +3 -8
- ominfra/manage/system/packages.py +13 -15
- ominfra/scripts/journald2aws.py +184 -129
- ominfra/scripts/manage.py +1276 -325
- ominfra/scripts/supervisor.py +44 -31
- ominfra/supervisor/configs.py +2 -0
- ominfra/supervisor/supervisor.py +2 -33
- ominfra/supervisor/utils/os.py +41 -0
- {ominfra-0.0.0.dev155.dist-info → ominfra-0.0.0.dev157.dist-info}/METADATA +3 -3
- {ominfra-0.0.0.dev155.dist-info → ominfra-0.0.0.dev157.dist-info}/RECORD +18 -18
- {ominfra-0.0.0.dev155.dist-info → ominfra-0.0.0.dev157.dist-info}/LICENSE +0 -0
- {ominfra-0.0.0.dev155.dist-info → ominfra-0.0.0.dev157.dist-info}/WHEEL +0 -0
- {ominfra-0.0.0.dev155.dist-info → ominfra-0.0.0.dev157.dist-info}/entry_points.txt +0 -0
- {ominfra-0.0.0.dev155.dist-info → ominfra-0.0.0.dev157.dist-info}/top_level.txt +0 -0
ominfra/scripts/manage.py
CHANGED
@@ -37,6 +37,7 @@ import shlex
|
|
37
37
|
import shutil
|
38
38
|
import signal
|
39
39
|
import site
|
40
|
+
import string
|
40
41
|
import struct
|
41
42
|
import subprocess
|
42
43
|
import sys
|
@@ -68,6 +69,11 @@ VersionCmpLocalType = ta.Union['NegativeInfinityVersionType', _VersionCmpLocalTy
|
|
68
69
|
VersionCmpKey = ta.Tuple[int, ta.Tuple[int, ...], VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpPrePostDevType, VersionCmpLocalType] # noqa
|
69
70
|
VersionComparisonMethod = ta.Callable[[VersionCmpKey, VersionCmpKey], bool]
|
70
71
|
|
72
|
+
# ../../omdev/toml/parser.py
|
73
|
+
TomlParseFloat = ta.Callable[[str], ta.Any]
|
74
|
+
TomlKey = ta.Tuple[str, ...]
|
75
|
+
TomlPos = int # ta.TypeAlias
|
76
|
+
|
71
77
|
# ../../omlish/asyncs/asyncio/timeouts.py
|
72
78
|
AwaitableT = ta.TypeVar('AwaitableT', bound=ta.Awaitable)
|
73
79
|
|
@@ -109,6 +115,9 @@ InjectorProviderFn = ta.Callable[['Injector'], ta.Any]
|
|
109
115
|
InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
|
110
116
|
InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
|
111
117
|
|
118
|
+
# ../configs.py
|
119
|
+
ConfigMapping = ta.Mapping[str, ta.Any]
|
120
|
+
|
112
121
|
# ../../omlish/lite/subprocesses.py
|
113
122
|
SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
|
114
123
|
|
@@ -523,6 +532,824 @@ def canonicalize_version(
|
|
523
532
|
return ''.join(parts)
|
524
533
|
|
525
534
|
|
535
|
+
########################################
|
536
|
+
# ../../../omdev/toml/parser.py
|
537
|
+
# SPDX-License-Identifier: MIT
|
538
|
+
# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
|
539
|
+
# Licensed to PSF under a Contributor Agreement.
|
540
|
+
#
|
541
|
+
# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
542
|
+
# --------------------------------------------
|
543
|
+
#
|
544
|
+
# 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization
|
545
|
+
# ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated
|
546
|
+
# documentation.
|
547
|
+
#
|
548
|
+
# 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive,
|
549
|
+
# royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative
|
550
|
+
# works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License
|
551
|
+
# Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
|
552
|
+
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; All
|
553
|
+
# Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee.
|
554
|
+
#
|
555
|
+
# 3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and
|
556
|
+
# wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in
|
557
|
+
# any such work a brief summary of the changes made to Python.
|
558
|
+
#
|
559
|
+
# 4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES,
|
560
|
+
# EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY
|
561
|
+
# OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY
|
562
|
+
# RIGHTS.
|
563
|
+
#
|
564
|
+
# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
|
565
|
+
# DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF
|
566
|
+
# ADVISED OF THE POSSIBILITY THEREOF.
|
567
|
+
#
|
568
|
+
# 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions.
|
569
|
+
#
|
570
|
+
# 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint
|
571
|
+
# venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade
|
572
|
+
# name in a trademark sense to endorse or promote products or services of Licensee, or any third party.
|
573
|
+
#
|
574
|
+
# 8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this
|
575
|
+
# License Agreement.
|
576
|
+
#
|
577
|
+
# https://github.com/python/cpython/blob/9ce90206b7a4649600218cf0bd4826db79c9a312/Lib/tomllib/_parser.py
|
578
|
+
|
579
|
+
|
580
|
+
##
|
581
|
+
|
582
|
+
|
583
|
+
_TOML_TIME_RE_STR = r'([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?'
|
584
|
+
|
585
|
+
TOML_RE_NUMBER = re.compile(
|
586
|
+
r"""
|
587
|
+
0
|
588
|
+
(?:
|
589
|
+
x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex
|
590
|
+
|
|
591
|
+
b[01](?:_?[01])* # bin
|
592
|
+
|
|
593
|
+
o[0-7](?:_?[0-7])* # oct
|
594
|
+
)
|
595
|
+
|
|
596
|
+
[+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part
|
597
|
+
(?P<floatpart>
|
598
|
+
(?:\.[0-9](?:_?[0-9])*)? # optional fractional part
|
599
|
+
(?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part
|
600
|
+
)
|
601
|
+
""",
|
602
|
+
flags=re.VERBOSE,
|
603
|
+
)
|
604
|
+
TOML_RE_LOCALTIME = re.compile(_TOML_TIME_RE_STR)
|
605
|
+
TOML_RE_DATETIME = re.compile(
|
606
|
+
rf"""
|
607
|
+
([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27
|
608
|
+
(?:
|
609
|
+
[Tt ]
|
610
|
+
{_TOML_TIME_RE_STR}
|
611
|
+
(?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset
|
612
|
+
)?
|
613
|
+
""",
|
614
|
+
flags=re.VERBOSE,
|
615
|
+
)
|
616
|
+
|
617
|
+
|
618
|
+
def toml_match_to_datetime(match: re.Match) -> ta.Union[datetime.datetime, datetime.date]:
|
619
|
+
"""Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
|
620
|
+
|
621
|
+
Raises ValueError if the match does not correspond to a valid date or datetime.
|
622
|
+
"""
|
623
|
+
(
|
624
|
+
year_str,
|
625
|
+
month_str,
|
626
|
+
day_str,
|
627
|
+
hour_str,
|
628
|
+
minute_str,
|
629
|
+
sec_str,
|
630
|
+
micros_str,
|
631
|
+
zulu_time,
|
632
|
+
offset_sign_str,
|
633
|
+
offset_hour_str,
|
634
|
+
offset_minute_str,
|
635
|
+
) = match.groups()
|
636
|
+
year, month, day = int(year_str), int(month_str), int(day_str)
|
637
|
+
if hour_str is None:
|
638
|
+
return datetime.date(year, month, day)
|
639
|
+
hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
|
640
|
+
micros = int(micros_str.ljust(6, '0')) if micros_str else 0
|
641
|
+
if offset_sign_str:
|
642
|
+
tz: ta.Optional[datetime.tzinfo] = toml_cached_tz(
|
643
|
+
offset_hour_str, offset_minute_str, offset_sign_str,
|
644
|
+
)
|
645
|
+
elif zulu_time:
|
646
|
+
tz = datetime.UTC
|
647
|
+
else: # local date-time
|
648
|
+
tz = None
|
649
|
+
return datetime.datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz)
|
650
|
+
|
651
|
+
|
652
|
+
@functools.lru_cache() # noqa
|
653
|
+
def toml_cached_tz(hour_str: str, minute_str: str, sign_str: str) -> datetime.timezone:
|
654
|
+
sign = 1 if sign_str == '+' else -1
|
655
|
+
return datetime.timezone(
|
656
|
+
datetime.timedelta(
|
657
|
+
hours=sign * int(hour_str),
|
658
|
+
minutes=sign * int(minute_str),
|
659
|
+
),
|
660
|
+
)
|
661
|
+
|
662
|
+
|
663
|
+
def toml_match_to_localtime(match: re.Match) -> datetime.time:
|
664
|
+
hour_str, minute_str, sec_str, micros_str = match.groups()
|
665
|
+
micros = int(micros_str.ljust(6, '0')) if micros_str else 0
|
666
|
+
return datetime.time(int(hour_str), int(minute_str), int(sec_str), micros)
|
667
|
+
|
668
|
+
|
669
|
+
def toml_match_to_number(match: re.Match, parse_float: TomlParseFloat) -> ta.Any:
|
670
|
+
if match.group('floatpart'):
|
671
|
+
return parse_float(match.group())
|
672
|
+
return int(match.group(), 0)
|
673
|
+
|
674
|
+
|
675
|
+
TOML_ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
|
676
|
+
|
677
|
+
# Neither of these sets include quotation mark or backslash. They are currently handled as separate cases in the parser
|
678
|
+
# functions.
|
679
|
+
TOML_ILLEGAL_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t')
|
680
|
+
TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS = TOML_ASCII_CTRL - frozenset('\t\n')
|
681
|
+
|
682
|
+
TOML_ILLEGAL_LITERAL_STR_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
|
683
|
+
TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
|
684
|
+
|
685
|
+
TOML_ILLEGAL_COMMENT_CHARS = TOML_ILLEGAL_BASIC_STR_CHARS
|
686
|
+
|
687
|
+
TOML_WS = frozenset(' \t')
|
688
|
+
TOML_WS_AND_NEWLINE = TOML_WS | frozenset('\n')
|
689
|
+
TOML_BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + '-_')
|
690
|
+
TOML_KEY_INITIAL_CHARS = TOML_BARE_KEY_CHARS | frozenset("\"'")
|
691
|
+
TOML_HEXDIGIT_CHARS = frozenset(string.hexdigits)
|
692
|
+
|
693
|
+
TOML_BASIC_STR_ESCAPE_REPLACEMENTS = types.MappingProxyType(
|
694
|
+
{
|
695
|
+
'\\b': '\u0008', # backspace
|
696
|
+
'\\t': '\u0009', # tab
|
697
|
+
'\\n': '\u000A', # linefeed
|
698
|
+
'\\f': '\u000C', # form feed
|
699
|
+
'\\r': '\u000D', # carriage return
|
700
|
+
'\\"': '\u0022', # quote
|
701
|
+
'\\\\': '\u005C', # backslash
|
702
|
+
},
|
703
|
+
)
|
704
|
+
|
705
|
+
|
706
|
+
class TomlDecodeError(ValueError):
|
707
|
+
"""An error raised if a document is not valid TOML."""
|
708
|
+
|
709
|
+
|
710
|
+
def toml_load(fp: ta.BinaryIO, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]:
|
711
|
+
"""Parse TOML from a binary file object."""
|
712
|
+
b = fp.read()
|
713
|
+
try:
|
714
|
+
s = b.decode()
|
715
|
+
except AttributeError:
|
716
|
+
raise TypeError("File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`") from None
|
717
|
+
return toml_loads(s, parse_float=parse_float)
|
718
|
+
|
719
|
+
|
720
|
+
def toml_loads(s: str, /, *, parse_float: TomlParseFloat = float) -> ta.Dict[str, ta.Any]: # noqa: C901
|
721
|
+
"""Parse TOML from a string."""
|
722
|
+
|
723
|
+
# The spec allows converting "\r\n" to "\n", even in string literals. Let's do so to simplify parsing.
|
724
|
+
try:
|
725
|
+
src = s.replace('\r\n', '\n')
|
726
|
+
except (AttributeError, TypeError):
|
727
|
+
raise TypeError(f"Expected str object, not '{type(s).__qualname__}'") from None
|
728
|
+
pos = 0
|
729
|
+
out = TomlOutput(TomlNestedDict(), TomlFlags())
|
730
|
+
header: TomlKey = ()
|
731
|
+
parse_float = toml_make_safe_parse_float(parse_float)
|
732
|
+
|
733
|
+
# Parse one statement at a time (typically means one line in TOML source)
|
734
|
+
while True:
|
735
|
+
# 1. Skip line leading whitespace
|
736
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
737
|
+
|
738
|
+
# 2. Parse rules. Expect one of the following:
|
739
|
+
# - end of file
|
740
|
+
# - end of line
|
741
|
+
# - comment
|
742
|
+
# - key/value pair
|
743
|
+
# - append dict to list (and move to its namespace)
|
744
|
+
# - create dict (and move to its namespace)
|
745
|
+
# Skip trailing whitespace when applicable.
|
746
|
+
try:
|
747
|
+
char = src[pos]
|
748
|
+
except IndexError:
|
749
|
+
break
|
750
|
+
if char == '\n':
|
751
|
+
pos += 1
|
752
|
+
continue
|
753
|
+
if char in TOML_KEY_INITIAL_CHARS:
|
754
|
+
pos = toml_key_value_rule(src, pos, out, header, parse_float)
|
755
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
756
|
+
elif char == '[':
|
757
|
+
try:
|
758
|
+
second_char: ta.Optional[str] = src[pos + 1]
|
759
|
+
except IndexError:
|
760
|
+
second_char = None
|
761
|
+
out.flags.finalize_pending()
|
762
|
+
if second_char == '[':
|
763
|
+
pos, header = toml_create_list_rule(src, pos, out)
|
764
|
+
else:
|
765
|
+
pos, header = toml_create_dict_rule(src, pos, out)
|
766
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
767
|
+
elif char != '#':
|
768
|
+
raise toml_suffixed_err(src, pos, 'Invalid statement')
|
769
|
+
|
770
|
+
# 3. Skip comment
|
771
|
+
pos = toml_skip_comment(src, pos)
|
772
|
+
|
773
|
+
# 4. Expect end of line or end of file
|
774
|
+
try:
|
775
|
+
char = src[pos]
|
776
|
+
except IndexError:
|
777
|
+
break
|
778
|
+
if char != '\n':
|
779
|
+
raise toml_suffixed_err(
|
780
|
+
src, pos, 'Expected newline or end of document after a statement',
|
781
|
+
)
|
782
|
+
pos += 1
|
783
|
+
|
784
|
+
return out.data.dict
|
785
|
+
|
786
|
+
|
787
|
+
class TomlFlags:
|
788
|
+
"""Flags that map to parsed keys/namespaces."""
|
789
|
+
|
790
|
+
# Marks an immutable namespace (inline array or inline table).
|
791
|
+
FROZEN = 0
|
792
|
+
# Marks a nest that has been explicitly created and can no longer be opened using the "[table]" syntax.
|
793
|
+
EXPLICIT_NEST = 1
|
794
|
+
|
795
|
+
def __init__(self) -> None:
|
796
|
+
self._flags: ta.Dict[str, dict] = {}
|
797
|
+
self._pending_flags: ta.Set[ta.Tuple[TomlKey, int]] = set()
|
798
|
+
|
799
|
+
def add_pending(self, key: TomlKey, flag: int) -> None:
|
800
|
+
self._pending_flags.add((key, flag))
|
801
|
+
|
802
|
+
def finalize_pending(self) -> None:
|
803
|
+
for key, flag in self._pending_flags:
|
804
|
+
self.set(key, flag, recursive=False)
|
805
|
+
self._pending_flags.clear()
|
806
|
+
|
807
|
+
def unset_all(self, key: TomlKey) -> None:
|
808
|
+
cont = self._flags
|
809
|
+
for k in key[:-1]:
|
810
|
+
if k not in cont:
|
811
|
+
return
|
812
|
+
cont = cont[k]['nested']
|
813
|
+
cont.pop(key[-1], None)
|
814
|
+
|
815
|
+
def set(self, key: TomlKey, flag: int, *, recursive: bool) -> None: # noqa: A003
|
816
|
+
cont = self._flags
|
817
|
+
key_parent, key_stem = key[:-1], key[-1]
|
818
|
+
for k in key_parent:
|
819
|
+
if k not in cont:
|
820
|
+
cont[k] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
|
821
|
+
cont = cont[k]['nested']
|
822
|
+
if key_stem not in cont:
|
823
|
+
cont[key_stem] = {'flags': set(), 'recursive_flags': set(), 'nested': {}}
|
824
|
+
cont[key_stem]['recursive_flags' if recursive else 'flags'].add(flag)
|
825
|
+
|
826
|
+
def is_(self, key: TomlKey, flag: int) -> bool:
|
827
|
+
if not key:
|
828
|
+
return False # document root has no flags
|
829
|
+
cont = self._flags
|
830
|
+
for k in key[:-1]:
|
831
|
+
if k not in cont:
|
832
|
+
return False
|
833
|
+
inner_cont = cont[k]
|
834
|
+
if flag in inner_cont['recursive_flags']:
|
835
|
+
return True
|
836
|
+
cont = inner_cont['nested']
|
837
|
+
key_stem = key[-1]
|
838
|
+
if key_stem in cont:
|
839
|
+
cont = cont[key_stem]
|
840
|
+
return flag in cont['flags'] or flag in cont['recursive_flags']
|
841
|
+
return False
|
842
|
+
|
843
|
+
|
844
|
+
class TomlNestedDict:
|
845
|
+
def __init__(self) -> None:
|
846
|
+
# The parsed content of the TOML document
|
847
|
+
self.dict: ta.Dict[str, ta.Any] = {}
|
848
|
+
|
849
|
+
def get_or_create_nest(
|
850
|
+
self,
|
851
|
+
key: TomlKey,
|
852
|
+
*,
|
853
|
+
access_lists: bool = True,
|
854
|
+
) -> dict:
|
855
|
+
cont: ta.Any = self.dict
|
856
|
+
for k in key:
|
857
|
+
if k not in cont:
|
858
|
+
cont[k] = {}
|
859
|
+
cont = cont[k]
|
860
|
+
if access_lists and isinstance(cont, list):
|
861
|
+
cont = cont[-1]
|
862
|
+
if not isinstance(cont, dict):
|
863
|
+
raise KeyError('There is no nest behind this key')
|
864
|
+
return cont
|
865
|
+
|
866
|
+
def append_nest_to_list(self, key: TomlKey) -> None:
|
867
|
+
cont = self.get_or_create_nest(key[:-1])
|
868
|
+
last_key = key[-1]
|
869
|
+
if last_key in cont:
|
870
|
+
list_ = cont[last_key]
|
871
|
+
if not isinstance(list_, list):
|
872
|
+
raise KeyError('An object other than list found behind this key')
|
873
|
+
list_.append({})
|
874
|
+
else:
|
875
|
+
cont[last_key] = [{}]
|
876
|
+
|
877
|
+
|
878
|
+
class TomlOutput(ta.NamedTuple):
|
879
|
+
data: TomlNestedDict
|
880
|
+
flags: TomlFlags
|
881
|
+
|
882
|
+
|
883
|
+
def toml_skip_chars(src: str, pos: TomlPos, chars: ta.Iterable[str]) -> TomlPos:
|
884
|
+
try:
|
885
|
+
while src[pos] in chars:
|
886
|
+
pos += 1
|
887
|
+
except IndexError:
|
888
|
+
pass
|
889
|
+
return pos
|
890
|
+
|
891
|
+
|
892
|
+
def toml_skip_until(
|
893
|
+
src: str,
|
894
|
+
pos: TomlPos,
|
895
|
+
expect: str,
|
896
|
+
*,
|
897
|
+
error_on: ta.FrozenSet[str],
|
898
|
+
error_on_eof: bool,
|
899
|
+
) -> TomlPos:
|
900
|
+
try:
|
901
|
+
new_pos = src.index(expect, pos)
|
902
|
+
except ValueError:
|
903
|
+
new_pos = len(src)
|
904
|
+
if error_on_eof:
|
905
|
+
raise toml_suffixed_err(src, new_pos, f'Expected {expect!r}') from None
|
906
|
+
|
907
|
+
if not error_on.isdisjoint(src[pos:new_pos]):
|
908
|
+
while src[pos] not in error_on:
|
909
|
+
pos += 1
|
910
|
+
raise toml_suffixed_err(src, pos, f'Found invalid character {src[pos]!r}')
|
911
|
+
return new_pos
|
912
|
+
|
913
|
+
|
914
|
+
def toml_skip_comment(src: str, pos: TomlPos) -> TomlPos:
|
915
|
+
try:
|
916
|
+
char: ta.Optional[str] = src[pos]
|
917
|
+
except IndexError:
|
918
|
+
char = None
|
919
|
+
if char == '#':
|
920
|
+
return toml_skip_until(
|
921
|
+
src, pos + 1, '\n', error_on=TOML_ILLEGAL_COMMENT_CHARS, error_on_eof=False,
|
922
|
+
)
|
923
|
+
return pos
|
924
|
+
|
925
|
+
|
926
|
+
def toml_skip_comments_and_array_ws(src: str, pos: TomlPos) -> TomlPos:
|
927
|
+
while True:
|
928
|
+
pos_before_skip = pos
|
929
|
+
pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
|
930
|
+
pos = toml_skip_comment(src, pos)
|
931
|
+
if pos == pos_before_skip:
|
932
|
+
return pos
|
933
|
+
|
934
|
+
|
935
|
+
def toml_create_dict_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
|
936
|
+
pos += 1 # Skip "["
|
937
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
938
|
+
pos, key = toml_parse_key(src, pos)
|
939
|
+
|
940
|
+
if out.flags.is_(key, TomlFlags.EXPLICIT_NEST) or out.flags.is_(key, TomlFlags.FROZEN):
|
941
|
+
raise toml_suffixed_err(src, pos, f'Cannot declare {key} twice')
|
942
|
+
out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
|
943
|
+
try:
|
944
|
+
out.data.get_or_create_nest(key)
|
945
|
+
except KeyError:
|
946
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
947
|
+
|
948
|
+
if not src.startswith(']', pos):
|
949
|
+
raise toml_suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
|
950
|
+
return pos + 1, key
|
951
|
+
|
952
|
+
|
953
|
+
def toml_create_list_rule(src: str, pos: TomlPos, out: TomlOutput) -> ta.Tuple[TomlPos, TomlKey]:
|
954
|
+
pos += 2 # Skip "[["
|
955
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
956
|
+
pos, key = toml_parse_key(src, pos)
|
957
|
+
|
958
|
+
if out.flags.is_(key, TomlFlags.FROZEN):
|
959
|
+
raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
|
960
|
+
# Free the namespace now that it points to another empty list item...
|
961
|
+
out.flags.unset_all(key)
|
962
|
+
# ...but this key precisely is still prohibited from table declaration
|
963
|
+
out.flags.set(key, TomlFlags.EXPLICIT_NEST, recursive=False)
|
964
|
+
try:
|
965
|
+
out.data.append_nest_to_list(key)
|
966
|
+
except KeyError:
|
967
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
968
|
+
|
969
|
+
if not src.startswith(']]', pos):
|
970
|
+
raise toml_suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
|
971
|
+
return pos + 2, key
|
972
|
+
|
973
|
+
|
974
|
+
def toml_key_value_rule(
|
975
|
+
src: str,
|
976
|
+
pos: TomlPos,
|
977
|
+
out: TomlOutput,
|
978
|
+
header: TomlKey,
|
979
|
+
parse_float: TomlParseFloat,
|
980
|
+
) -> TomlPos:
|
981
|
+
pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
|
982
|
+
key_parent, key_stem = key[:-1], key[-1]
|
983
|
+
abs_key_parent = header + key_parent
|
984
|
+
|
985
|
+
relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
|
986
|
+
for cont_key in relative_path_cont_keys:
|
987
|
+
# Check that dotted key syntax does not redefine an existing table
|
988
|
+
if out.flags.is_(cont_key, TomlFlags.EXPLICIT_NEST):
|
989
|
+
raise toml_suffixed_err(src, pos, f'Cannot redefine namespace {cont_key}')
|
990
|
+
# Containers in the relative path can't be opened with the table syntax or dotted key/value syntax in following
|
991
|
+
# table sections.
|
992
|
+
out.flags.add_pending(cont_key, TomlFlags.EXPLICIT_NEST)
|
993
|
+
|
994
|
+
if out.flags.is_(abs_key_parent, TomlFlags.FROZEN):
|
995
|
+
raise toml_suffixed_err(
|
996
|
+
src,
|
997
|
+
pos,
|
998
|
+
f'Cannot mutate immutable namespace {abs_key_parent}',
|
999
|
+
)
|
1000
|
+
|
1001
|
+
try:
|
1002
|
+
nest = out.data.get_or_create_nest(abs_key_parent)
|
1003
|
+
except KeyError:
|
1004
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
1005
|
+
if key_stem in nest:
|
1006
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value')
|
1007
|
+
# Mark inline table and array namespaces recursively immutable
|
1008
|
+
if isinstance(value, (dict, list)):
|
1009
|
+
out.flags.set(header + key, TomlFlags.FROZEN, recursive=True)
|
1010
|
+
nest[key_stem] = value
|
1011
|
+
return pos
|
1012
|
+
|
1013
|
+
|
1014
|
+
def toml_parse_key_value_pair(
|
1015
|
+
src: str,
|
1016
|
+
pos: TomlPos,
|
1017
|
+
parse_float: TomlParseFloat,
|
1018
|
+
) -> ta.Tuple[TomlPos, TomlKey, ta.Any]:
|
1019
|
+
pos, key = toml_parse_key(src, pos)
|
1020
|
+
try:
|
1021
|
+
char: ta.Optional[str] = src[pos]
|
1022
|
+
except IndexError:
|
1023
|
+
char = None
|
1024
|
+
if char != '=':
|
1025
|
+
raise toml_suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
|
1026
|
+
pos += 1
|
1027
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1028
|
+
pos, value = toml_parse_value(src, pos, parse_float)
|
1029
|
+
return pos, key, value
|
1030
|
+
|
1031
|
+
|
1032
|
+
def toml_parse_key(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, TomlKey]:
|
1033
|
+
pos, key_part = toml_parse_key_part(src, pos)
|
1034
|
+
key: TomlKey = (key_part,)
|
1035
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1036
|
+
while True:
|
1037
|
+
try:
|
1038
|
+
char: ta.Optional[str] = src[pos]
|
1039
|
+
except IndexError:
|
1040
|
+
char = None
|
1041
|
+
if char != '.':
|
1042
|
+
return pos, key
|
1043
|
+
pos += 1
|
1044
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1045
|
+
pos, key_part = toml_parse_key_part(src, pos)
|
1046
|
+
key += (key_part,)
|
1047
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1048
|
+
|
1049
|
+
|
1050
|
+
def toml_parse_key_part(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
1051
|
+
try:
|
1052
|
+
char: ta.Optional[str] = src[pos]
|
1053
|
+
except IndexError:
|
1054
|
+
char = None
|
1055
|
+
if char in TOML_BARE_KEY_CHARS:
|
1056
|
+
start_pos = pos
|
1057
|
+
pos = toml_skip_chars(src, pos, TOML_BARE_KEY_CHARS)
|
1058
|
+
return pos, src[start_pos:pos]
|
1059
|
+
if char == "'":
|
1060
|
+
return toml_parse_literal_str(src, pos)
|
1061
|
+
if char == '"':
|
1062
|
+
return toml_parse_one_line_basic_str(src, pos)
|
1063
|
+
raise toml_suffixed_err(src, pos, 'Invalid initial character for a key part')
|
1064
|
+
|
1065
|
+
|
1066
|
+
def toml_parse_one_line_basic_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
1067
|
+
pos += 1
|
1068
|
+
return toml_parse_basic_str(src, pos, multiline=False)
|
1069
|
+
|
1070
|
+
|
1071
|
+
def toml_parse_array(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, list]:
|
1072
|
+
pos += 1
|
1073
|
+
array: list = []
|
1074
|
+
|
1075
|
+
pos = toml_skip_comments_and_array_ws(src, pos)
|
1076
|
+
if src.startswith(']', pos):
|
1077
|
+
return pos + 1, array
|
1078
|
+
while True:
|
1079
|
+
pos, val = toml_parse_value(src, pos, parse_float)
|
1080
|
+
array.append(val)
|
1081
|
+
pos = toml_skip_comments_and_array_ws(src, pos)
|
1082
|
+
|
1083
|
+
c = src[pos:pos + 1]
|
1084
|
+
if c == ']':
|
1085
|
+
return pos + 1, array
|
1086
|
+
if c != ',':
|
1087
|
+
raise toml_suffixed_err(src, pos, 'Unclosed array')
|
1088
|
+
pos += 1
|
1089
|
+
|
1090
|
+
pos = toml_skip_comments_and_array_ws(src, pos)
|
1091
|
+
if src.startswith(']', pos):
|
1092
|
+
return pos + 1, array
|
1093
|
+
|
1094
|
+
|
1095
|
+
def toml_parse_inline_table(src: str, pos: TomlPos, parse_float: TomlParseFloat) -> ta.Tuple[TomlPos, dict]:
|
1096
|
+
pos += 1
|
1097
|
+
nested_dict = TomlNestedDict()
|
1098
|
+
flags = TomlFlags()
|
1099
|
+
|
1100
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1101
|
+
if src.startswith('}', pos):
|
1102
|
+
return pos + 1, nested_dict.dict
|
1103
|
+
while True:
|
1104
|
+
pos, key, value = toml_parse_key_value_pair(src, pos, parse_float)
|
1105
|
+
key_parent, key_stem = key[:-1], key[-1]
|
1106
|
+
if flags.is_(key, TomlFlags.FROZEN):
|
1107
|
+
raise toml_suffixed_err(src, pos, f'Cannot mutate immutable namespace {key}')
|
1108
|
+
try:
|
1109
|
+
nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
|
1110
|
+
except KeyError:
|
1111
|
+
raise toml_suffixed_err(src, pos, 'Cannot overwrite a value') from None
|
1112
|
+
if key_stem in nest:
|
1113
|
+
raise toml_suffixed_err(src, pos, f'Duplicate inline table key {key_stem!r}')
|
1114
|
+
nest[key_stem] = value
|
1115
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1116
|
+
c = src[pos:pos + 1]
|
1117
|
+
if c == '}':
|
1118
|
+
return pos + 1, nested_dict.dict
|
1119
|
+
if c != ',':
|
1120
|
+
raise toml_suffixed_err(src, pos, 'Unclosed inline table')
|
1121
|
+
if isinstance(value, (dict, list)):
|
1122
|
+
flags.set(key, TomlFlags.FROZEN, recursive=True)
|
1123
|
+
pos += 1
|
1124
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1125
|
+
|
1126
|
+
|
1127
|
+
def toml_parse_basic_str_escape(
|
1128
|
+
src: str,
|
1129
|
+
pos: TomlPos,
|
1130
|
+
*,
|
1131
|
+
multiline: bool = False,
|
1132
|
+
) -> ta.Tuple[TomlPos, str]:
|
1133
|
+
escape_id = src[pos:pos + 2]
|
1134
|
+
pos += 2
|
1135
|
+
if multiline and escape_id in {'\\ ', '\\\t', '\\\n'}:
|
1136
|
+
# Skip whitespace until next non-whitespace character or end of the doc. Error if non-whitespace is found before
|
1137
|
+
# newline.
|
1138
|
+
if escape_id != '\\\n':
|
1139
|
+
pos = toml_skip_chars(src, pos, TOML_WS)
|
1140
|
+
try:
|
1141
|
+
char = src[pos]
|
1142
|
+
except IndexError:
|
1143
|
+
return pos, ''
|
1144
|
+
if char != '\n':
|
1145
|
+
raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string")
|
1146
|
+
pos += 1
|
1147
|
+
pos = toml_skip_chars(src, pos, TOML_WS_AND_NEWLINE)
|
1148
|
+
return pos, ''
|
1149
|
+
if escape_id == '\\u':
|
1150
|
+
return toml_parse_hex_char(src, pos, 4)
|
1151
|
+
if escape_id == '\\U':
|
1152
|
+
return toml_parse_hex_char(src, pos, 8)
|
1153
|
+
try:
|
1154
|
+
return pos, TOML_BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
|
1155
|
+
except KeyError:
|
1156
|
+
raise toml_suffixed_err(src, pos, "Unescaped '\\' in a string") from None
|
1157
|
+
|
1158
|
+
|
1159
|
+
def toml_parse_basic_str_escape_multiline(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
1160
|
+
return toml_parse_basic_str_escape(src, pos, multiline=True)
|
1161
|
+
|
1162
|
+
|
1163
|
+
def toml_parse_hex_char(src: str, pos: TomlPos, hex_len: int) -> ta.Tuple[TomlPos, str]:
|
1164
|
+
hex_str = src[pos:pos + hex_len]
|
1165
|
+
if len(hex_str) != hex_len or not TOML_HEXDIGIT_CHARS.issuperset(hex_str):
|
1166
|
+
raise toml_suffixed_err(src, pos, 'Invalid hex value')
|
1167
|
+
pos += hex_len
|
1168
|
+
hex_int = int(hex_str, 16)
|
1169
|
+
if not toml_is_unicode_scalar_value(hex_int):
|
1170
|
+
raise toml_suffixed_err(src, pos, 'Escaped character is not a Unicode scalar value')
|
1171
|
+
return pos, chr(hex_int)
|
1172
|
+
|
1173
|
+
|
1174
|
+
def toml_parse_literal_str(src: str, pos: TomlPos) -> ta.Tuple[TomlPos, str]:
|
1175
|
+
pos += 1 # Skip starting apostrophe
|
1176
|
+
start_pos = pos
|
1177
|
+
pos = toml_skip_until(
|
1178
|
+
src, pos, "'", error_on=TOML_ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True,
|
1179
|
+
)
|
1180
|
+
return pos + 1, src[start_pos:pos] # Skip ending apostrophe
|
1181
|
+
|
1182
|
+
|
1183
|
+
def toml_parse_multiline_str(src: str, pos: TomlPos, *, literal: bool) -> ta.Tuple[TomlPos, str]:
|
1184
|
+
pos += 3
|
1185
|
+
if src.startswith('\n', pos):
|
1186
|
+
pos += 1
|
1187
|
+
|
1188
|
+
if literal:
|
1189
|
+
delim = "'"
|
1190
|
+
end_pos = toml_skip_until(
|
1191
|
+
src,
|
1192
|
+
pos,
|
1193
|
+
"'''",
|
1194
|
+
error_on=TOML_ILLEGAL_MULTILINE_LITERAL_STR_CHARS,
|
1195
|
+
error_on_eof=True,
|
1196
|
+
)
|
1197
|
+
result = src[pos:end_pos]
|
1198
|
+
pos = end_pos + 3
|
1199
|
+
else:
|
1200
|
+
delim = '"'
|
1201
|
+
pos, result = toml_parse_basic_str(src, pos, multiline=True)
|
1202
|
+
|
1203
|
+
# Add at maximum two extra apostrophes/quotes if the end sequence is 4 or 5 chars long instead of just 3.
|
1204
|
+
if not src.startswith(delim, pos):
|
1205
|
+
return pos, result
|
1206
|
+
pos += 1
|
1207
|
+
if not src.startswith(delim, pos):
|
1208
|
+
return pos, result + delim
|
1209
|
+
pos += 1
|
1210
|
+
return pos, result + (delim * 2)
|
1211
|
+
|
1212
|
+
|
1213
|
+
def toml_parse_basic_str(src: str, pos: TomlPos, *, multiline: bool) -> ta.Tuple[TomlPos, str]:
|
1214
|
+
if multiline:
|
1215
|
+
error_on = TOML_ILLEGAL_MULTILINE_BASIC_STR_CHARS
|
1216
|
+
parse_escapes = toml_parse_basic_str_escape_multiline
|
1217
|
+
else:
|
1218
|
+
error_on = TOML_ILLEGAL_BASIC_STR_CHARS
|
1219
|
+
parse_escapes = toml_parse_basic_str_escape
|
1220
|
+
result = ''
|
1221
|
+
start_pos = pos
|
1222
|
+
while True:
|
1223
|
+
try:
|
1224
|
+
char = src[pos]
|
1225
|
+
except IndexError:
|
1226
|
+
raise toml_suffixed_err(src, pos, 'Unterminated string') from None
|
1227
|
+
if char == '"':
|
1228
|
+
if not multiline:
|
1229
|
+
return pos + 1, result + src[start_pos:pos]
|
1230
|
+
if src.startswith('"""', pos):
|
1231
|
+
return pos + 3, result + src[start_pos:pos]
|
1232
|
+
pos += 1
|
1233
|
+
continue
|
1234
|
+
if char == '\\':
|
1235
|
+
result += src[start_pos:pos]
|
1236
|
+
pos, parsed_escape = parse_escapes(src, pos)
|
1237
|
+
result += parsed_escape
|
1238
|
+
start_pos = pos
|
1239
|
+
continue
|
1240
|
+
if char in error_on:
|
1241
|
+
raise toml_suffixed_err(src, pos, f'Illegal character {char!r}')
|
1242
|
+
pos += 1
|
1243
|
+
|
1244
|
+
|
1245
|
+
def toml_parse_value( # noqa: C901
|
1246
|
+
src: str,
|
1247
|
+
pos: TomlPos,
|
1248
|
+
parse_float: TomlParseFloat,
|
1249
|
+
) -> ta.Tuple[TomlPos, ta.Any]:
|
1250
|
+
try:
|
1251
|
+
char: ta.Optional[str] = src[pos]
|
1252
|
+
except IndexError:
|
1253
|
+
char = None
|
1254
|
+
|
1255
|
+
# IMPORTANT: order conditions based on speed of checking and likelihood
|
1256
|
+
|
1257
|
+
# Basic strings
|
1258
|
+
if char == '"':
|
1259
|
+
if src.startswith('"""', pos):
|
1260
|
+
return toml_parse_multiline_str(src, pos, literal=False)
|
1261
|
+
return toml_parse_one_line_basic_str(src, pos)
|
1262
|
+
|
1263
|
+
# Literal strings
|
1264
|
+
if char == "'":
|
1265
|
+
if src.startswith("'''", pos):
|
1266
|
+
return toml_parse_multiline_str(src, pos, literal=True)
|
1267
|
+
return toml_parse_literal_str(src, pos)
|
1268
|
+
|
1269
|
+
# Booleans
|
1270
|
+
if char == 't':
|
1271
|
+
if src.startswith('true', pos):
|
1272
|
+
return pos + 4, True
|
1273
|
+
if char == 'f':
|
1274
|
+
if src.startswith('false', pos):
|
1275
|
+
return pos + 5, False
|
1276
|
+
|
1277
|
+
# Arrays
|
1278
|
+
if char == '[':
|
1279
|
+
return toml_parse_array(src, pos, parse_float)
|
1280
|
+
|
1281
|
+
# Inline tables
|
1282
|
+
if char == '{':
|
1283
|
+
return toml_parse_inline_table(src, pos, parse_float)
|
1284
|
+
|
1285
|
+
# Dates and times
|
1286
|
+
datetime_match = TOML_RE_DATETIME.match(src, pos)
|
1287
|
+
if datetime_match:
|
1288
|
+
try:
|
1289
|
+
datetime_obj = toml_match_to_datetime(datetime_match)
|
1290
|
+
except ValueError as e:
|
1291
|
+
raise toml_suffixed_err(src, pos, 'Invalid date or datetime') from e
|
1292
|
+
return datetime_match.end(), datetime_obj
|
1293
|
+
localtime_match = TOML_RE_LOCALTIME.match(src, pos)
|
1294
|
+
if localtime_match:
|
1295
|
+
return localtime_match.end(), toml_match_to_localtime(localtime_match)
|
1296
|
+
|
1297
|
+
# Integers and "normal" floats. The regex will greedily match any type starting with a decimal char, so needs to be
|
1298
|
+
# located after handling of dates and times.
|
1299
|
+
number_match = TOML_RE_NUMBER.match(src, pos)
|
1300
|
+
if number_match:
|
1301
|
+
return number_match.end(), toml_match_to_number(number_match, parse_float)
|
1302
|
+
|
1303
|
+
# Special floats
|
1304
|
+
first_three = src[pos:pos + 3]
|
1305
|
+
if first_three in {'inf', 'nan'}:
|
1306
|
+
return pos + 3, parse_float(first_three)
|
1307
|
+
first_four = src[pos:pos + 4]
|
1308
|
+
if first_four in {'-inf', '+inf', '-nan', '+nan'}:
|
1309
|
+
return pos + 4, parse_float(first_four)
|
1310
|
+
|
1311
|
+
raise toml_suffixed_err(src, pos, 'Invalid value')
|
1312
|
+
|
1313
|
+
|
1314
|
+
def toml_suffixed_err(src: str, pos: TomlPos, msg: str) -> TomlDecodeError:
|
1315
|
+
"""Return a `TomlDecodeError` where error message is suffixed with coordinates in source."""
|
1316
|
+
|
1317
|
+
def coord_repr(src: str, pos: TomlPos) -> str:
|
1318
|
+
if pos >= len(src):
|
1319
|
+
return 'end of document'
|
1320
|
+
line = src.count('\n', 0, pos) + 1
|
1321
|
+
if line == 1:
|
1322
|
+
column = pos + 1
|
1323
|
+
else:
|
1324
|
+
column = pos - src.rindex('\n', 0, pos)
|
1325
|
+
return f'line {line}, column {column}'
|
1326
|
+
|
1327
|
+
return TomlDecodeError(f'{msg} (at {coord_repr(src, pos)})')
|
1328
|
+
|
1329
|
+
|
1330
|
+
def toml_is_unicode_scalar_value(codepoint: int) -> bool:
|
1331
|
+
return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
|
1332
|
+
|
1333
|
+
|
1334
|
+
def toml_make_safe_parse_float(parse_float: TomlParseFloat) -> TomlParseFloat:
|
1335
|
+
"""A decorator to make `parse_float` safe.
|
1336
|
+
|
1337
|
+
`parse_float` must not return dicts or lists, because these types would be mixed with parsed TOML tables and arrays,
|
1338
|
+
thus confusing the parser. The returned decorated callable raises `ValueError` instead of returning illegal types.
|
1339
|
+
"""
|
1340
|
+
# The default `float` callable never returns illegal types. Optimize it.
|
1341
|
+
if parse_float is float:
|
1342
|
+
return float
|
1343
|
+
|
1344
|
+
def safe_parse_float(float_str: str) -> ta.Any:
|
1345
|
+
float_value = parse_float(float_str)
|
1346
|
+
if isinstance(float_value, (dict, list)):
|
1347
|
+
raise ValueError('parse_float must not return dicts or lists') # noqa
|
1348
|
+
return float_value
|
1349
|
+
|
1350
|
+
return safe_parse_float
|
1351
|
+
|
1352
|
+
|
526
1353
|
########################################
|
527
1354
|
# ../config.py
|
528
1355
|
|
@@ -1208,8 +2035,8 @@ class _CachedNullary(_AbstractCachedNullary):
|
|
1208
2035
|
return self._value
|
1209
2036
|
|
1210
2037
|
|
1211
|
-
def cached_nullary(fn
|
1212
|
-
return _CachedNullary(fn)
|
2038
|
+
def cached_nullary(fn: CallableT) -> CallableT:
|
2039
|
+
return _CachedNullary(fn) # type: ignore
|
1213
2040
|
|
1214
2041
|
|
1215
2042
|
def static_init(fn: CallableT) -> CallableT:
|
@@ -3582,6 +4409,8 @@ def _get_argparse_arg_ann_kwargs(ann: ta.Any) -> ta.Mapping[str, ta.Any]:
|
|
3582
4409
|
return {'action': 'store_true'}
|
3583
4410
|
elif ann is list:
|
3584
4411
|
return {'action': 'append'}
|
4412
|
+
elif is_optional_alias(ann):
|
4413
|
+
return _get_argparse_arg_ann_kwargs(get_optional_alias_arg(ann))
|
3585
4414
|
else:
|
3586
4415
|
raise TypeError(ann)
|
3587
4416
|
|
@@ -4987,6 +5816,7 @@ TODO:
|
|
4987
5816
|
- pickle stdlib objs? have to pin to 3.8 pickle protocol, will be cross-version
|
4988
5817
|
- namedtuple
|
4989
5818
|
- literals
|
5819
|
+
- newtypes?
|
4990
5820
|
"""
|
4991
5821
|
|
4992
5822
|
|
@@ -5534,6 +6364,90 @@ class Interp:
|
|
5534
6364
|
version: InterpVersion
|
5535
6365
|
|
5536
6366
|
|
6367
|
+
########################################
|
6368
|
+
# ../../configs.py
|
6369
|
+
|
6370
|
+
|
6371
|
+
def parse_config_file(
|
6372
|
+
name: str,
|
6373
|
+
f: ta.TextIO,
|
6374
|
+
) -> ConfigMapping:
|
6375
|
+
if name.endswith('.toml'):
|
6376
|
+
return toml_loads(f.read())
|
6377
|
+
|
6378
|
+
elif any(name.endswith(e) for e in ('.yml', '.yaml')):
|
6379
|
+
yaml = __import__('yaml')
|
6380
|
+
return yaml.safe_load(f)
|
6381
|
+
|
6382
|
+
elif name.endswith('.ini'):
|
6383
|
+
import configparser
|
6384
|
+
cp = configparser.ConfigParser()
|
6385
|
+
cp.read_file(f)
|
6386
|
+
config_dct: ta.Dict[str, ta.Any] = {}
|
6387
|
+
for sec in cp.sections():
|
6388
|
+
cd = config_dct
|
6389
|
+
for k in sec.split('.'):
|
6390
|
+
cd = cd.setdefault(k, {})
|
6391
|
+
cd.update(cp.items(sec))
|
6392
|
+
return config_dct
|
6393
|
+
|
6394
|
+
else:
|
6395
|
+
return json.loads(f.read())
|
6396
|
+
|
6397
|
+
|
6398
|
+
def read_config_file(
|
6399
|
+
path: str,
|
6400
|
+
cls: ta.Type[T],
|
6401
|
+
*,
|
6402
|
+
prepare: ta.Optional[ta.Callable[[ConfigMapping], ConfigMapping]] = None,
|
6403
|
+
) -> T:
|
6404
|
+
with open(path) as cf:
|
6405
|
+
config_dct = parse_config_file(os.path.basename(path), cf)
|
6406
|
+
|
6407
|
+
if prepare is not None:
|
6408
|
+
config_dct = prepare(config_dct)
|
6409
|
+
|
6410
|
+
return unmarshal_obj(config_dct, cls)
|
6411
|
+
|
6412
|
+
|
6413
|
+
def build_config_named_children(
|
6414
|
+
o: ta.Union[
|
6415
|
+
ta.Sequence[ConfigMapping],
|
6416
|
+
ta.Mapping[str, ConfigMapping],
|
6417
|
+
None,
|
6418
|
+
],
|
6419
|
+
*,
|
6420
|
+
name_key: str = 'name',
|
6421
|
+
) -> ta.Optional[ta.Sequence[ConfigMapping]]:
|
6422
|
+
if o is None:
|
6423
|
+
return None
|
6424
|
+
|
6425
|
+
lst: ta.List[ConfigMapping] = []
|
6426
|
+
if isinstance(o, ta.Mapping):
|
6427
|
+
for k, v in o.items():
|
6428
|
+
check.isinstance(v, ta.Mapping)
|
6429
|
+
if name_key in v:
|
6430
|
+
n = v[name_key]
|
6431
|
+
if k != n:
|
6432
|
+
raise KeyError(f'Given names do not match: {n} != {k}')
|
6433
|
+
lst.append(v)
|
6434
|
+
else:
|
6435
|
+
lst.append({name_key: k, **v})
|
6436
|
+
|
6437
|
+
else:
|
6438
|
+
check.not_isinstance(o, str)
|
6439
|
+
lst.extend(o)
|
6440
|
+
|
6441
|
+
seen = set()
|
6442
|
+
for d in lst:
|
6443
|
+
n = d['name']
|
6444
|
+
if n in d:
|
6445
|
+
raise KeyError(f'Duplicate name: {n}')
|
6446
|
+
seen.add(n)
|
6447
|
+
|
6448
|
+
return lst
|
6449
|
+
|
6450
|
+
|
5537
6451
|
########################################
|
5538
6452
|
# ../commands/marshal.py
|
5539
6453
|
|
@@ -5788,168 +6702,222 @@ SUBPROCESS_CHANNEL_OPTION_VALUES: ta.Mapping[SubprocessChannelOption, int] = {
|
|
5788
6702
|
_SUBPROCESS_SHELL_WRAP_EXECS = False
|
5789
6703
|
|
5790
6704
|
|
5791
|
-
def subprocess_shell_wrap_exec(*
|
5792
|
-
return ('sh', '-c', ' '.join(map(shlex.quote,
|
6705
|
+
def subprocess_shell_wrap_exec(*cmd: str) -> ta.Tuple[str, ...]:
|
6706
|
+
return ('sh', '-c', ' '.join(map(shlex.quote, cmd)))
|
5793
6707
|
|
5794
6708
|
|
5795
|
-
def subprocess_maybe_shell_wrap_exec(*
|
6709
|
+
def subprocess_maybe_shell_wrap_exec(*cmd: str) -> ta.Tuple[str, ...]:
|
5796
6710
|
if _SUBPROCESS_SHELL_WRAP_EXECS or is_debugger_attached():
|
5797
|
-
return subprocess_shell_wrap_exec(*
|
6711
|
+
return subprocess_shell_wrap_exec(*cmd)
|
5798
6712
|
else:
|
5799
|
-
return
|
5800
|
-
|
5801
|
-
|
5802
|
-
def prepare_subprocess_invocation(
|
5803
|
-
*args: str,
|
5804
|
-
env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
|
5805
|
-
extra_env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
|
5806
|
-
quiet: bool = False,
|
5807
|
-
shell: bool = False,
|
5808
|
-
**kwargs: ta.Any,
|
5809
|
-
) -> ta.Tuple[ta.Tuple[ta.Any, ...], ta.Dict[str, ta.Any]]:
|
5810
|
-
log.debug('prepare_subprocess_invocation: args=%r', args)
|
5811
|
-
if extra_env:
|
5812
|
-
log.debug('prepare_subprocess_invocation: extra_env=%r', extra_env)
|
5813
|
-
|
5814
|
-
if extra_env:
|
5815
|
-
env = {**(env if env is not None else os.environ), **extra_env}
|
5816
|
-
|
5817
|
-
if quiet and 'stderr' not in kwargs:
|
5818
|
-
if not log.isEnabledFor(logging.DEBUG):
|
5819
|
-
kwargs['stderr'] = subprocess.DEVNULL
|
5820
|
-
|
5821
|
-
if not shell:
|
5822
|
-
args = subprocess_maybe_shell_wrap_exec(*args)
|
5823
|
-
|
5824
|
-
return args, dict(
|
5825
|
-
env=env,
|
5826
|
-
shell=shell,
|
5827
|
-
**kwargs,
|
5828
|
-
)
|
6713
|
+
return cmd
|
5829
6714
|
|
5830
6715
|
|
5831
6716
|
##
|
5832
6717
|
|
5833
6718
|
|
5834
|
-
|
5835
|
-
|
5836
|
-
|
5837
|
-
|
5838
|
-
|
5839
|
-
|
5840
|
-
|
5841
|
-
|
5842
|
-
|
5843
|
-
|
6719
|
+
def subprocess_close(
|
6720
|
+
proc: subprocess.Popen,
|
6721
|
+
timeout: ta.Optional[float] = None,
|
6722
|
+
) -> None:
|
6723
|
+
# TODO: terminate, sleep, kill
|
6724
|
+
if proc.stdout:
|
6725
|
+
proc.stdout.close()
|
6726
|
+
if proc.stderr:
|
6727
|
+
proc.stderr.close()
|
6728
|
+
if proc.stdin:
|
6729
|
+
proc.stdin.close()
|
5844
6730
|
|
5845
|
-
|
5846
|
-
end_time = time.time()
|
5847
|
-
elapsed_s = end_time - start_time
|
5848
|
-
log.debug('subprocess_common_context.finally: elapsed_s=%f args=%r', elapsed_s, args)
|
6731
|
+
proc.wait(timeout)
|
5849
6732
|
|
5850
6733
|
|
5851
6734
|
##
|
5852
6735
|
|
5853
6736
|
|
5854
|
-
|
5855
|
-
|
5856
|
-
stdout: ta.Any = sys.stderr,
|
5857
|
-
**kwargs: ta.Any,
|
5858
|
-
) -> None:
|
5859
|
-
args, kwargs = prepare_subprocess_invocation(*args, stdout=stdout, **kwargs)
|
5860
|
-
with subprocess_common_context(*args, **kwargs):
|
5861
|
-
return subprocess.check_call(args, **kwargs) # type: ignore
|
6737
|
+
class AbstractSubprocesses(abc.ABC): # noqa
|
6738
|
+
DEFAULT_LOGGER: ta.ClassVar[ta.Optional[logging.Logger]] = log
|
5862
6739
|
|
6740
|
+
def __init__(
|
6741
|
+
self,
|
6742
|
+
*,
|
6743
|
+
log: ta.Optional[logging.Logger] = None,
|
6744
|
+
try_exceptions: ta.Optional[ta.Tuple[ta.Type[Exception], ...]] = None,
|
6745
|
+
) -> None:
|
6746
|
+
super().__init__()
|
5863
6747
|
|
5864
|
-
|
5865
|
-
|
5866
|
-
**kwargs: ta.Any,
|
5867
|
-
) -> bytes:
|
5868
|
-
args, kwargs = prepare_subprocess_invocation(*args, **kwargs)
|
5869
|
-
with subprocess_common_context(*args, **kwargs):
|
5870
|
-
return subprocess.check_output(args, **kwargs)
|
6748
|
+
self._log = log if log is not None else self.DEFAULT_LOGGER
|
6749
|
+
self._try_exceptions = try_exceptions if try_exceptions is not None else self.DEFAULT_TRY_EXCEPTIONS
|
5871
6750
|
|
6751
|
+
#
|
5872
6752
|
|
5873
|
-
def
|
5874
|
-
|
6753
|
+
def prepare_args(
|
6754
|
+
self,
|
6755
|
+
*cmd: str,
|
6756
|
+
env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
|
6757
|
+
extra_env: ta.Optional[ta.Mapping[str, ta.Any]] = None,
|
6758
|
+
quiet: bool = False,
|
6759
|
+
shell: bool = False,
|
6760
|
+
**kwargs: ta.Any,
|
6761
|
+
) -> ta.Tuple[ta.Tuple[ta.Any, ...], ta.Dict[str, ta.Any]]:
|
6762
|
+
if self._log:
|
6763
|
+
self._log.debug('Subprocesses.prepare_args: cmd=%r', cmd)
|
6764
|
+
if extra_env:
|
6765
|
+
self._log.debug('Subprocesses.prepare_args: extra_env=%r', extra_env)
|
5875
6766
|
|
6767
|
+
if extra_env:
|
6768
|
+
env = {**(env if env is not None else os.environ), **extra_env}
|
5876
6769
|
|
5877
|
-
|
6770
|
+
if quiet and 'stderr' not in kwargs:
|
6771
|
+
if self._log and not self._log.isEnabledFor(logging.DEBUG):
|
6772
|
+
kwargs['stderr'] = subprocess.DEVNULL
|
5878
6773
|
|
6774
|
+
if not shell:
|
6775
|
+
cmd = subprocess_maybe_shell_wrap_exec(*cmd)
|
5879
6776
|
|
5880
|
-
|
5881
|
-
|
5882
|
-
|
5883
|
-
|
6777
|
+
return cmd, dict(
|
6778
|
+
env=env,
|
6779
|
+
shell=shell,
|
6780
|
+
**kwargs,
|
6781
|
+
)
|
5884
6782
|
|
6783
|
+
@contextlib.contextmanager
|
6784
|
+
def wrap_call(self, *cmd: ta.Any, **kwargs: ta.Any) -> ta.Iterator[None]:
|
6785
|
+
start_time = time.time()
|
6786
|
+
try:
|
6787
|
+
if self._log:
|
6788
|
+
self._log.debug('Subprocesses.wrap_call.try: cmd=%r', cmd)
|
6789
|
+
yield
|
5885
6790
|
|
5886
|
-
|
5887
|
-
|
5888
|
-
|
5889
|
-
|
5890
|
-
**kwargs: ta.Any,
|
5891
|
-
) -> ta.Union[T, Exception]:
|
5892
|
-
try:
|
5893
|
-
return fn(*args, **kwargs)
|
5894
|
-
except try_exceptions as e: # noqa
|
5895
|
-
if log.isEnabledFor(logging.DEBUG):
|
5896
|
-
log.exception('command failed')
|
5897
|
-
return e
|
5898
|
-
|
5899
|
-
|
5900
|
-
def subprocess_try_call(
|
5901
|
-
*args: str,
|
5902
|
-
try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
|
5903
|
-
**kwargs: ta.Any,
|
5904
|
-
) -> bool:
|
5905
|
-
if isinstance(_subprocess_try_run(
|
5906
|
-
subprocess_check_call,
|
5907
|
-
*args,
|
5908
|
-
try_exceptions=try_exceptions,
|
5909
|
-
**kwargs,
|
5910
|
-
), Exception):
|
5911
|
-
return False
|
5912
|
-
else:
|
5913
|
-
return True
|
6791
|
+
except Exception as exc: # noqa
|
6792
|
+
if self._log:
|
6793
|
+
self._log.debug('Subprocesses.wrap_call.except: exc=%r', exc)
|
6794
|
+
raise
|
5914
6795
|
|
6796
|
+
finally:
|
6797
|
+
end_time = time.time()
|
6798
|
+
elapsed_s = end_time - start_time
|
6799
|
+
if self._log:
|
6800
|
+
self._log.debug('sSubprocesses.wrap_call.finally: elapsed_s=%f cmd=%r', elapsed_s, cmd)
|
5915
6801
|
|
5916
|
-
|
5917
|
-
|
5918
|
-
|
5919
|
-
|
5920
|
-
|
5921
|
-
|
5922
|
-
|
5923
|
-
|
5924
|
-
|
5925
|
-
|
5926
|
-
|
5927
|
-
|
5928
|
-
else:
|
5929
|
-
return ret
|
6802
|
+
@contextlib.contextmanager
|
6803
|
+
def prepare_and_wrap(
|
6804
|
+
self,
|
6805
|
+
*cmd: ta.Any,
|
6806
|
+
**kwargs: ta.Any,
|
6807
|
+
) -> ta.Iterator[ta.Tuple[
|
6808
|
+
ta.Tuple[ta.Any, ...],
|
6809
|
+
ta.Dict[str, ta.Any],
|
6810
|
+
]]:
|
6811
|
+
cmd, kwargs = self.prepare_args(*cmd, **kwargs)
|
6812
|
+
with self.wrap_call(*cmd, **kwargs):
|
6813
|
+
yield cmd, kwargs
|
5930
6814
|
|
6815
|
+
#
|
5931
6816
|
|
5932
|
-
|
5933
|
-
|
5934
|
-
|
6817
|
+
DEFAULT_TRY_EXCEPTIONS: ta.Tuple[ta.Type[Exception], ...] = (
|
6818
|
+
FileNotFoundError,
|
6819
|
+
subprocess.CalledProcessError,
|
6820
|
+
)
|
6821
|
+
|
6822
|
+
def try_fn(
|
6823
|
+
self,
|
6824
|
+
fn: ta.Callable[..., T],
|
6825
|
+
*cmd: str,
|
6826
|
+
try_exceptions: ta.Optional[ta.Tuple[ta.Type[Exception], ...]] = None,
|
6827
|
+
**kwargs: ta.Any,
|
6828
|
+
) -> ta.Union[T, Exception]:
|
6829
|
+
if try_exceptions is None:
|
6830
|
+
try_exceptions = self._try_exceptions
|
6831
|
+
|
6832
|
+
try:
|
6833
|
+
return fn(*cmd, **kwargs)
|
6834
|
+
|
6835
|
+
except try_exceptions as e: # noqa
|
6836
|
+
if self._log and self._log.isEnabledFor(logging.DEBUG):
|
6837
|
+
self._log.exception('command failed')
|
6838
|
+
return e
|
6839
|
+
|
6840
|
+
async def async_try_fn(
|
6841
|
+
self,
|
6842
|
+
fn: ta.Callable[..., ta.Awaitable[T]],
|
6843
|
+
*cmd: ta.Any,
|
6844
|
+
try_exceptions: ta.Optional[ta.Tuple[ta.Type[Exception], ...]] = None,
|
6845
|
+
**kwargs: ta.Any,
|
6846
|
+
) -> ta.Union[T, Exception]:
|
6847
|
+
if try_exceptions is None:
|
6848
|
+
try_exceptions = self._try_exceptions
|
6849
|
+
|
6850
|
+
try:
|
6851
|
+
return await fn(*cmd, **kwargs)
|
6852
|
+
|
6853
|
+
except try_exceptions as e: # noqa
|
6854
|
+
if self._log and self._log.isEnabledFor(logging.DEBUG):
|
6855
|
+
self._log.exception('command failed')
|
6856
|
+
return e
|
5935
6857
|
|
5936
6858
|
|
5937
6859
|
##
|
5938
6860
|
|
5939
6861
|
|
5940
|
-
|
5941
|
-
|
5942
|
-
|
5943
|
-
|
5944
|
-
|
5945
|
-
|
5946
|
-
|
5947
|
-
|
5948
|
-
|
5949
|
-
if proc.stdin:
|
5950
|
-
proc.stdin.close()
|
6862
|
+
class Subprocesses(AbstractSubprocesses):
|
6863
|
+
def check_call(
|
6864
|
+
self,
|
6865
|
+
*cmd: str,
|
6866
|
+
stdout: ta.Any = sys.stderr,
|
6867
|
+
**kwargs: ta.Any,
|
6868
|
+
) -> None:
|
6869
|
+
with self.prepare_and_wrap(*cmd, stdout=stdout, **kwargs) as (cmd, kwargs): # noqa
|
6870
|
+
subprocess.check_call(cmd, **kwargs)
|
5951
6871
|
|
5952
|
-
|
6872
|
+
def check_output(
|
6873
|
+
self,
|
6874
|
+
*cmd: str,
|
6875
|
+
**kwargs: ta.Any,
|
6876
|
+
) -> bytes:
|
6877
|
+
with self.prepare_and_wrap(*cmd, **kwargs) as (cmd, kwargs): # noqa
|
6878
|
+
return subprocess.check_output(cmd, **kwargs)
|
6879
|
+
|
6880
|
+
def check_output_str(
|
6881
|
+
self,
|
6882
|
+
*cmd: str,
|
6883
|
+
**kwargs: ta.Any,
|
6884
|
+
) -> str:
|
6885
|
+
return self.check_output(*cmd, **kwargs).decode().strip()
|
6886
|
+
|
6887
|
+
#
|
6888
|
+
|
6889
|
+
def try_call(
|
6890
|
+
self,
|
6891
|
+
*cmd: str,
|
6892
|
+
**kwargs: ta.Any,
|
6893
|
+
) -> bool:
|
6894
|
+
if isinstance(self.try_fn(self.check_call, *cmd, **kwargs), Exception):
|
6895
|
+
return False
|
6896
|
+
else:
|
6897
|
+
return True
|
6898
|
+
|
6899
|
+
def try_output(
|
6900
|
+
self,
|
6901
|
+
*cmd: str,
|
6902
|
+
**kwargs: ta.Any,
|
6903
|
+
) -> ta.Optional[bytes]:
|
6904
|
+
if isinstance(ret := self.try_fn(self.check_output, *cmd, **kwargs), Exception):
|
6905
|
+
return None
|
6906
|
+
else:
|
6907
|
+
return ret
|
6908
|
+
|
6909
|
+
def try_output_str(
|
6910
|
+
self,
|
6911
|
+
*cmd: str,
|
6912
|
+
**kwargs: ta.Any,
|
6913
|
+
) -> ta.Optional[str]:
|
6914
|
+
if (ret := self.try_output(*cmd, **kwargs)) is None:
|
6915
|
+
return None
|
6916
|
+
else:
|
6917
|
+
return ret.decode().strip()
|
6918
|
+
|
6919
|
+
|
6920
|
+
subprocesses = Subprocesses()
|
5953
6921
|
|
5954
6922
|
|
5955
6923
|
########################################
|
@@ -6384,43 +7352,6 @@ class SystemConfig:
|
|
6384
7352
|
##
|
6385
7353
|
|
6386
7354
|
|
6387
|
-
@contextlib.asynccontextmanager
|
6388
|
-
async def asyncio_subprocess_popen(
|
6389
|
-
*cmd: str,
|
6390
|
-
shell: bool = False,
|
6391
|
-
timeout: ta.Optional[float] = None,
|
6392
|
-
**kwargs: ta.Any,
|
6393
|
-
) -> ta.AsyncGenerator[asyncio.subprocess.Process, None]:
|
6394
|
-
fac: ta.Any
|
6395
|
-
if shell:
|
6396
|
-
fac = functools.partial(
|
6397
|
-
asyncio.create_subprocess_shell,
|
6398
|
-
check.single(cmd),
|
6399
|
-
)
|
6400
|
-
else:
|
6401
|
-
fac = functools.partial(
|
6402
|
-
asyncio.create_subprocess_exec,
|
6403
|
-
*cmd,
|
6404
|
-
)
|
6405
|
-
|
6406
|
-
with subprocess_common_context(
|
6407
|
-
*cmd,
|
6408
|
-
shell=shell,
|
6409
|
-
timeout=timeout,
|
6410
|
-
**kwargs,
|
6411
|
-
):
|
6412
|
-
proc: asyncio.subprocess.Process
|
6413
|
-
proc = await fac(**kwargs)
|
6414
|
-
try:
|
6415
|
-
yield proc
|
6416
|
-
|
6417
|
-
finally:
|
6418
|
-
await asyncio_maybe_timeout(proc.wait(), timeout)
|
6419
|
-
|
6420
|
-
|
6421
|
-
##
|
6422
|
-
|
6423
|
-
|
6424
7355
|
class AsyncioProcessCommunicator:
|
6425
7356
|
def __init__(
|
6426
7357
|
self,
|
@@ -6531,148 +7462,147 @@ class AsyncioProcessCommunicator:
|
|
6531
7462
|
return await asyncio_maybe_timeout(self._communicate(input), timeout)
|
6532
7463
|
|
6533
7464
|
|
6534
|
-
|
6535
|
-
proc: asyncio.subprocess.Process,
|
6536
|
-
input: ta.Any = None, # noqa
|
6537
|
-
timeout: ta.Optional[float] = None,
|
6538
|
-
) -> ta.Tuple[ta.Optional[bytes], ta.Optional[bytes]]:
|
6539
|
-
return await AsyncioProcessCommunicator(proc).communicate(input, timeout) # noqa
|
6540
|
-
|
7465
|
+
##
|
6541
7466
|
|
6542
|
-
@dc.dataclass(frozen=True)
|
6543
|
-
class AsyncioSubprocessOutput:
|
6544
|
-
proc: asyncio.subprocess.Process
|
6545
|
-
stdout: ta.Optional[bytes]
|
6546
|
-
stderr: ta.Optional[bytes]
|
6547
7467
|
|
7468
|
+
class AsyncioSubprocesses(AbstractSubprocesses):
|
7469
|
+
async def communicate(
|
7470
|
+
self,
|
7471
|
+
proc: asyncio.subprocess.Process,
|
7472
|
+
input: ta.Any = None, # noqa
|
7473
|
+
timeout: ta.Optional[float] = None,
|
7474
|
+
) -> ta.Tuple[ta.Optional[bytes], ta.Optional[bytes]]:
|
7475
|
+
return await AsyncioProcessCommunicator(proc).communicate(input, timeout) # noqa
|
6548
7476
|
|
6549
|
-
|
6550
|
-
*args: str,
|
6551
|
-
input: ta.Any = None, # noqa
|
6552
|
-
timeout: ta.Optional[float] = None,
|
6553
|
-
check: bool = False, # noqa
|
6554
|
-
capture_output: ta.Optional[bool] = None,
|
6555
|
-
**kwargs: ta.Any,
|
6556
|
-
) -> AsyncioSubprocessOutput:
|
6557
|
-
if capture_output:
|
6558
|
-
kwargs.setdefault('stdout', subprocess.PIPE)
|
6559
|
-
kwargs.setdefault('stderr', subprocess.PIPE)
|
6560
|
-
|
6561
|
-
args, kwargs = prepare_subprocess_invocation(*args, **kwargs)
|
6562
|
-
|
6563
|
-
proc: asyncio.subprocess.Process
|
6564
|
-
async with asyncio_subprocess_popen(*args, **kwargs) as proc:
|
6565
|
-
stdout, stderr = await asyncio_subprocess_communicate(proc, input, timeout)
|
6566
|
-
|
6567
|
-
if check and proc.returncode:
|
6568
|
-
raise subprocess.CalledProcessError(
|
6569
|
-
proc.returncode,
|
6570
|
-
args,
|
6571
|
-
output=stdout,
|
6572
|
-
stderr=stderr,
|
6573
|
-
)
|
7477
|
+
#
|
6574
7478
|
|
6575
|
-
|
6576
|
-
|
6577
|
-
|
6578
|
-
|
6579
|
-
|
7479
|
+
@contextlib.asynccontextmanager
|
7480
|
+
async def popen(
|
7481
|
+
self,
|
7482
|
+
*cmd: str,
|
7483
|
+
shell: bool = False,
|
7484
|
+
timeout: ta.Optional[float] = None,
|
7485
|
+
**kwargs: ta.Any,
|
7486
|
+
) -> ta.AsyncGenerator[asyncio.subprocess.Process, None]:
|
7487
|
+
fac: ta.Any
|
7488
|
+
if shell:
|
7489
|
+
fac = functools.partial(
|
7490
|
+
asyncio.create_subprocess_shell,
|
7491
|
+
check.single(cmd),
|
7492
|
+
)
|
7493
|
+
else:
|
7494
|
+
fac = functools.partial(
|
7495
|
+
asyncio.create_subprocess_exec,
|
7496
|
+
*cmd,
|
7497
|
+
)
|
6580
7498
|
|
7499
|
+
with self.prepare_and_wrap( *cmd, shell=shell, **kwargs) as (cmd, kwargs): # noqa
|
7500
|
+
proc: asyncio.subprocess.Process = await fac(**kwargs)
|
7501
|
+
try:
|
7502
|
+
yield proc
|
6581
7503
|
|
6582
|
-
|
7504
|
+
finally:
|
7505
|
+
await asyncio_maybe_timeout(proc.wait(), timeout)
|
6583
7506
|
|
7507
|
+
#
|
6584
7508
|
|
6585
|
-
|
6586
|
-
|
6587
|
-
|
6588
|
-
|
6589
|
-
|
6590
|
-
**kwargs: ta.Any,
|
6591
|
-
) -> None:
|
6592
|
-
await asyncio_subprocess_run(
|
6593
|
-
*args,
|
6594
|
-
stdout=stdout,
|
6595
|
-
input=input,
|
6596
|
-
timeout=timeout,
|
6597
|
-
check=True,
|
6598
|
-
**kwargs,
|
6599
|
-
)
|
7509
|
+
@dc.dataclass(frozen=True)
|
7510
|
+
class RunOutput:
|
7511
|
+
proc: asyncio.subprocess.Process
|
7512
|
+
stdout: ta.Optional[bytes]
|
7513
|
+
stderr: ta.Optional[bytes]
|
6600
7514
|
|
7515
|
+
async def run(
|
7516
|
+
self,
|
7517
|
+
*cmd: str,
|
7518
|
+
input: ta.Any = None, # noqa
|
7519
|
+
timeout: ta.Optional[float] = None,
|
7520
|
+
check: bool = False, # noqa
|
7521
|
+
capture_output: ta.Optional[bool] = None,
|
7522
|
+
**kwargs: ta.Any,
|
7523
|
+
) -> RunOutput:
|
7524
|
+
if capture_output:
|
7525
|
+
kwargs.setdefault('stdout', subprocess.PIPE)
|
7526
|
+
kwargs.setdefault('stderr', subprocess.PIPE)
|
6601
7527
|
|
6602
|
-
|
6603
|
-
*
|
6604
|
-
|
6605
|
-
|
6606
|
-
|
6607
|
-
|
6608
|
-
|
6609
|
-
|
6610
|
-
|
6611
|
-
|
6612
|
-
|
6613
|
-
check=True,
|
6614
|
-
**kwargs,
|
6615
|
-
)
|
7528
|
+
proc: asyncio.subprocess.Process
|
7529
|
+
async with self.popen(*cmd, **kwargs) as proc:
|
7530
|
+
stdout, stderr = await self.communicate(proc, input, timeout)
|
7531
|
+
|
7532
|
+
if check and proc.returncode:
|
7533
|
+
raise subprocess.CalledProcessError(
|
7534
|
+
proc.returncode,
|
7535
|
+
cmd,
|
7536
|
+
output=stdout,
|
7537
|
+
stderr=stderr,
|
7538
|
+
)
|
6616
7539
|
|
6617
|
-
|
7540
|
+
return self.RunOutput(
|
7541
|
+
proc,
|
7542
|
+
stdout,
|
7543
|
+
stderr,
|
7544
|
+
)
|
6618
7545
|
|
7546
|
+
#
|
6619
7547
|
|
6620
|
-
async def
|
6621
|
-
|
7548
|
+
async def check_call(
|
7549
|
+
self,
|
7550
|
+
*cmd: str,
|
7551
|
+
stdout: ta.Any = sys.stderr,
|
7552
|
+
**kwargs: ta.Any,
|
7553
|
+
) -> None:
|
7554
|
+
with self.prepare_and_wrap(*cmd, stdout=stdout, check=True, **kwargs) as (cmd, kwargs): # noqa
|
7555
|
+
await self.run(*cmd, **kwargs)
|
6622
7556
|
|
7557
|
+
async def check_output(
|
7558
|
+
self,
|
7559
|
+
*cmd: str,
|
7560
|
+
**kwargs: ta.Any,
|
7561
|
+
) -> bytes:
|
7562
|
+
with self.prepare_and_wrap(*cmd, stdout=subprocess.PIPE, check=True, **kwargs) as (cmd, kwargs): # noqa
|
7563
|
+
return check.not_none((await self.run(*cmd, **kwargs)).stdout)
|
6623
7564
|
|
6624
|
-
|
7565
|
+
async def check_output_str(
|
7566
|
+
self,
|
7567
|
+
*cmd: str,
|
7568
|
+
**kwargs: ta.Any,
|
7569
|
+
) -> str:
|
7570
|
+
return (await self.check_output(*cmd, **kwargs)).decode().strip()
|
6625
7571
|
|
7572
|
+
#
|
6626
7573
|
|
6627
|
-
async def
|
6628
|
-
|
6629
|
-
|
6630
|
-
|
6631
|
-
|
6632
|
-
|
6633
|
-
|
6634
|
-
|
6635
|
-
|
6636
|
-
if log.isEnabledFor(logging.DEBUG):
|
6637
|
-
log.exception('command failed')
|
6638
|
-
return e
|
6639
|
-
|
6640
|
-
|
6641
|
-
async def asyncio_subprocess_try_call(
|
6642
|
-
*args: str,
|
6643
|
-
try_exceptions: ta.Tuple[ta.Type[Exception], ...] = DEFAULT_SUBPROCESS_TRY_EXCEPTIONS,
|
6644
|
-
**kwargs: ta.Any,
|
6645
|
-
) -> bool:
|
6646
|
-
if isinstance(await _asyncio_subprocess_try_run(
|
6647
|
-
asyncio_subprocess_check_call,
|
6648
|
-
*args,
|
6649
|
-
try_exceptions=try_exceptions,
|
6650
|
-
**kwargs,
|
6651
|
-
), Exception):
|
6652
|
-
return False
|
6653
|
-
else:
|
6654
|
-
return True
|
7574
|
+
async def try_call(
|
7575
|
+
self,
|
7576
|
+
*cmd: str,
|
7577
|
+
**kwargs: ta.Any,
|
7578
|
+
) -> bool:
|
7579
|
+
if isinstance(await self.async_try_fn(self.check_call, *cmd, **kwargs), Exception):
|
7580
|
+
return False
|
7581
|
+
else:
|
7582
|
+
return True
|
6655
7583
|
|
7584
|
+
async def try_output(
|
7585
|
+
self,
|
7586
|
+
*cmd: str,
|
7587
|
+
**kwargs: ta.Any,
|
7588
|
+
) -> ta.Optional[bytes]:
|
7589
|
+
if isinstance(ret := await self.async_try_fn(self.check_output, *cmd, **kwargs), Exception):
|
7590
|
+
return None
|
7591
|
+
else:
|
7592
|
+
return ret
|
6656
7593
|
|
6657
|
-
async def
|
6658
|
-
|
6659
|
-
|
6660
|
-
|
6661
|
-
) -> ta.Optional[
|
6662
|
-
|
6663
|
-
|
6664
|
-
|
6665
|
-
|
6666
|
-
**kwargs,
|
6667
|
-
), Exception):
|
6668
|
-
return None
|
6669
|
-
else:
|
6670
|
-
return ret
|
7594
|
+
async def try_output_str(
|
7595
|
+
self,
|
7596
|
+
*cmd: str,
|
7597
|
+
**kwargs: ta.Any,
|
7598
|
+
) -> ta.Optional[str]:
|
7599
|
+
if (ret := await self.try_output(*cmd, **kwargs)) is None:
|
7600
|
+
return None
|
7601
|
+
else:
|
7602
|
+
return ret.decode().strip()
|
6671
7603
|
|
6672
7604
|
|
6673
|
-
|
6674
|
-
out = await asyncio_subprocess_try_output(*args, **kwargs)
|
6675
|
-
return out.decode().strip() if out is not None else None
|
7605
|
+
asyncio_subprocesses = AsyncioSubprocesses()
|
6676
7606
|
|
6677
7607
|
|
6678
7608
|
########################################
|
@@ -6749,7 +7679,7 @@ class InterpInspector:
|
|
6749
7679
|
return cls._build_inspection(sys.executable, eval(cls._INSPECTION_CODE)) # noqa
|
6750
7680
|
|
6751
7681
|
async def _inspect(self, exe: str) -> InterpInspection:
|
6752
|
-
output = await
|
7682
|
+
output = await asyncio_subprocesses.check_output(exe, '-c', f'print({self._INSPECTION_CODE})', quiet=True)
|
6753
7683
|
return self._build_inspection(exe, output.decode())
|
6754
7684
|
|
6755
7685
|
async def inspect(self, exe: str) -> ta.Optional[InterpInspection]:
|
@@ -6823,7 +7753,7 @@ class SubprocessCommand(Command['SubprocessCommand.Output']):
|
|
6823
7753
|
class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCommand.Output]):
|
6824
7754
|
async def execute(self, cmd: SubprocessCommand) -> SubprocessCommand.Output:
|
6825
7755
|
proc: asyncio.subprocess.Process
|
6826
|
-
async with
|
7756
|
+
async with asyncio_subprocesses.popen(
|
6827
7757
|
*subprocess_maybe_shell_wrap_exec(*cmd.cmd),
|
6828
7758
|
|
6829
7759
|
shell=cmd.shell,
|
@@ -6837,7 +7767,7 @@ class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCom
|
|
6837
7767
|
timeout=cmd.timeout,
|
6838
7768
|
) as proc:
|
6839
7769
|
start_time = time.time()
|
6840
|
-
stdout, stderr = await
|
7770
|
+
stdout, stderr = await asyncio_subprocesses.communicate(
|
6841
7771
|
proc,
|
6842
7772
|
input=cmd.input,
|
6843
7773
|
timeout=cmd.timeout,
|
@@ -6937,7 +7867,7 @@ class DeployGitManager(DeployPathOwner):
|
|
6937
7867
|
return f'https://{self._repo.host}/{self._repo.path}'
|
6938
7868
|
|
6939
7869
|
async def _call(self, *cmd: str) -> None:
|
6940
|
-
await
|
7870
|
+
await asyncio_subprocesses.check_call(
|
6941
7871
|
*cmd,
|
6942
7872
|
cwd=self._dir,
|
6943
7873
|
)
|
@@ -6963,7 +7893,7 @@ class DeployGitManager(DeployPathOwner):
|
|
6963
7893
|
# FIXME: temp dir swap
|
6964
7894
|
os.makedirs(dst_dir)
|
6965
7895
|
|
6966
|
-
dst_call = functools.partial(
|
7896
|
+
dst_call = functools.partial(asyncio_subprocesses.check_call, cwd=dst_dir)
|
6967
7897
|
await dst_call('git', 'init')
|
6968
7898
|
|
6969
7899
|
await dst_call('git', 'remote', 'add', 'local', self._dir)
|
@@ -7015,7 +7945,7 @@ class DeployVenvManager(DeployPathOwner):
|
|
7015
7945
|
) -> None:
|
7016
7946
|
sys_exe = 'python3'
|
7017
7947
|
|
7018
|
-
await
|
7948
|
+
await asyncio_subprocesses.check_call(sys_exe, '-m', 'venv', venv_dir)
|
7019
7949
|
|
7020
7950
|
#
|
7021
7951
|
|
@@ -7027,12 +7957,12 @@ class DeployVenvManager(DeployPathOwner):
|
|
7027
7957
|
|
7028
7958
|
if os.path.isfile(reqs_txt):
|
7029
7959
|
if use_uv:
|
7030
|
-
await
|
7960
|
+
await asyncio_subprocesses.check_call(venv_exe, '-m', 'pip', 'install', 'uv')
|
7031
7961
|
pip_cmd = ['-m', 'uv', 'pip']
|
7032
7962
|
else:
|
7033
7963
|
pip_cmd = ['-m', 'pip']
|
7034
7964
|
|
7035
|
-
await
|
7965
|
+
await asyncio_subprocesses.check_call(venv_exe, *pip_cmd,'install', '-r', reqs_txt)
|
7036
7966
|
|
7037
7967
|
async def setup_app_venv(self, app_tag: DeployAppTag) -> None:
|
7038
7968
|
await self.setup_venv(
|
@@ -7113,12 +8043,8 @@ class SubprocessRemoteSpawning(RemoteSpawning):
|
|
7113
8043
|
) -> ta.AsyncGenerator[RemoteSpawning.Spawned, None]:
|
7114
8044
|
pc = self._prepare_cmd(tgt, src)
|
7115
8045
|
|
7116
|
-
|
7117
|
-
|
7118
|
-
cmd = subprocess_maybe_shell_wrap_exec(*cmd)
|
7119
|
-
|
7120
|
-
async with asyncio_subprocess_popen(
|
7121
|
-
*cmd,
|
8046
|
+
async with asyncio_subprocesses.popen(
|
8047
|
+
*pc.cmd,
|
7122
8048
|
shell=pc.shell,
|
7123
8049
|
stdin=subprocess.PIPE,
|
7124
8050
|
stdout=subprocess.PIPE,
|
@@ -7179,10 +8105,10 @@ class SystemPackageManager(abc.ABC):
|
|
7179
8105
|
|
7180
8106
|
class BrewSystemPackageManager(SystemPackageManager):
|
7181
8107
|
async def update(self) -> None:
|
7182
|
-
await
|
8108
|
+
await asyncio_subprocesses.check_call('brew', 'update')
|
7183
8109
|
|
7184
8110
|
async def upgrade(self) -> None:
|
7185
|
-
await
|
8111
|
+
await asyncio_subprocesses.check_call('brew', 'upgrade')
|
7186
8112
|
|
7187
8113
|
async def install(self, *packages: SystemPackageOrStr) -> None:
|
7188
8114
|
es: ta.List[str] = []
|
@@ -7191,11 +8117,11 @@ class BrewSystemPackageManager(SystemPackageManager):
|
|
7191
8117
|
es.append(p.name + (f'@{p.version}' if p.version is not None else ''))
|
7192
8118
|
else:
|
7193
8119
|
es.append(p)
|
7194
|
-
await
|
8120
|
+
await asyncio_subprocesses.check_call('brew', 'install', *es)
|
7195
8121
|
|
7196
8122
|
async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
|
7197
8123
|
pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
|
7198
|
-
o = await
|
8124
|
+
o = await asyncio_subprocesses.check_output('brew', 'info', '--json', *pns)
|
7199
8125
|
j = json.loads(o.decode())
|
7200
8126
|
d: ta.Dict[str, SystemPackage] = {}
|
7201
8127
|
for e in j:
|
@@ -7214,18 +8140,18 @@ class AptSystemPackageManager(SystemPackageManager):
|
|
7214
8140
|
}
|
7215
8141
|
|
7216
8142
|
async def update(self) -> None:
|
7217
|
-
await
|
8143
|
+
await asyncio_subprocesses.check_call('sudo', 'apt', 'update', env={**os.environ, **self._APT_ENV})
|
7218
8144
|
|
7219
8145
|
async def upgrade(self) -> None:
|
7220
|
-
await
|
8146
|
+
await asyncio_subprocesses.check_call('sudo', 'apt', 'upgrade', '-y', env={**os.environ, **self._APT_ENV})
|
7221
8147
|
|
7222
8148
|
async def install(self, *packages: SystemPackageOrStr) -> None:
|
7223
8149
|
pns = [p.name if isinstance(p, SystemPackage) else p for p in packages] # FIXME: versions
|
7224
|
-
await
|
8150
|
+
await asyncio_subprocesses.check_call('sudo', 'apt', 'install', '-y', *pns, env={**os.environ, **self._APT_ENV})
|
7225
8151
|
|
7226
8152
|
async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
|
7227
8153
|
pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
|
7228
|
-
out = await
|
8154
|
+
out = await asyncio_subprocesses.run(
|
7229
8155
|
'dpkg-query', '-W', '-f=${Package}=${Version}\n', *pns,
|
7230
8156
|
capture_output=True,
|
7231
8157
|
check=False,
|
@@ -7242,20 +8168,20 @@ class AptSystemPackageManager(SystemPackageManager):
|
|
7242
8168
|
|
7243
8169
|
class YumSystemPackageManager(SystemPackageManager):
|
7244
8170
|
async def update(self) -> None:
|
7245
|
-
await
|
8171
|
+
await asyncio_subprocesses.check_call('sudo', 'yum', 'check-update')
|
7246
8172
|
|
7247
8173
|
async def upgrade(self) -> None:
|
7248
|
-
await
|
8174
|
+
await asyncio_subprocesses.check_call('sudo', 'yum', 'update')
|
7249
8175
|
|
7250
8176
|
async def install(self, *packages: SystemPackageOrStr) -> None:
|
7251
8177
|
pns = [p.name if isinstance(p, SystemPackage) else p for p in packages] # FIXME: versions
|
7252
|
-
await
|
8178
|
+
await asyncio_subprocesses.check_call('sudo', 'yum', 'install', *pns)
|
7253
8179
|
|
7254
8180
|
async def query(self, *packages: SystemPackageOrStr) -> ta.Mapping[str, SystemPackage]:
|
7255
8181
|
pns = [p.name if isinstance(p, SystemPackage) else p for p in packages]
|
7256
8182
|
d: ta.Dict[str, SystemPackage] = {}
|
7257
8183
|
for pn in pns:
|
7258
|
-
out = await
|
8184
|
+
out = await asyncio_subprocesses.run(
|
7259
8185
|
'rpm', '-q', pn,
|
7260
8186
|
capture_output=True,
|
7261
8187
|
)
|
@@ -7704,7 +8630,7 @@ class Pyenv:
|
|
7704
8630
|
return self._root_kw
|
7705
8631
|
|
7706
8632
|
if shutil.which('pyenv'):
|
7707
|
-
return await
|
8633
|
+
return await asyncio_subprocesses.check_output_str('pyenv', 'root')
|
7708
8634
|
|
7709
8635
|
d = os.path.expanduser('~/.pyenv')
|
7710
8636
|
if os.path.isdir(d) and os.path.isfile(os.path.join(d, 'bin', 'pyenv')):
|
@@ -7733,7 +8659,7 @@ class Pyenv:
|
|
7733
8659
|
if await self.root() is None:
|
7734
8660
|
return []
|
7735
8661
|
ret = []
|
7736
|
-
s = await
|
8662
|
+
s = await asyncio_subprocesses.check_output_str(await self.exe(), 'install', '--list')
|
7737
8663
|
for l in s.splitlines():
|
7738
8664
|
if not l.startswith(' '):
|
7739
8665
|
continue
|
@@ -7748,7 +8674,7 @@ class Pyenv:
|
|
7748
8674
|
return False
|
7749
8675
|
if not os.path.isdir(os.path.join(root, '.git')):
|
7750
8676
|
return False
|
7751
|
-
await
|
8677
|
+
await asyncio_subprocesses.check_call('git', 'pull', cwd=root)
|
7752
8678
|
return True
|
7753
8679
|
|
7754
8680
|
|
@@ -7839,7 +8765,7 @@ class DarwinPyenvInstallOpts(PyenvInstallOptsProvider):
|
|
7839
8765
|
cflags = []
|
7840
8766
|
ldflags = []
|
7841
8767
|
for dep in self.BREW_DEPS:
|
7842
|
-
dep_prefix = await
|
8768
|
+
dep_prefix = await asyncio_subprocesses.check_output_str('brew', '--prefix', dep)
|
7843
8769
|
cflags.append(f'-I{dep_prefix}/include')
|
7844
8770
|
ldflags.append(f'-L{dep_prefix}/lib')
|
7845
8771
|
return PyenvInstallOpts(
|
@@ -7849,11 +8775,11 @@ class DarwinPyenvInstallOpts(PyenvInstallOptsProvider):
|
|
7849
8775
|
|
7850
8776
|
@async_cached_nullary
|
7851
8777
|
async def brew_tcl_opts(self) -> PyenvInstallOpts:
|
7852
|
-
if await
|
8778
|
+
if await asyncio_subprocesses.try_output('brew', '--prefix', 'tcl-tk') is None:
|
7853
8779
|
return PyenvInstallOpts()
|
7854
8780
|
|
7855
|
-
tcl_tk_prefix = await
|
7856
|
-
tcl_tk_ver_str = await
|
8781
|
+
tcl_tk_prefix = await asyncio_subprocesses.check_output_str('brew', '--prefix', 'tcl-tk')
|
8782
|
+
tcl_tk_ver_str = await asyncio_subprocesses.check_output_str('brew', 'ls', '--versions', 'tcl-tk')
|
7857
8783
|
tcl_tk_ver = '.'.join(tcl_tk_ver_str.split()[1].split('.')[:2])
|
7858
8784
|
|
7859
8785
|
return PyenvInstallOpts(conf_opts=[
|
@@ -7961,6 +8887,7 @@ class PyenvVersionInstaller:
|
|
7961
8887
|
self._version,
|
7962
8888
|
]
|
7963
8889
|
|
8890
|
+
full_args: ta.List[str]
|
7964
8891
|
if self._given_install_name is not None:
|
7965
8892
|
full_args = [
|
7966
8893
|
os.path.join(check.not_none(await self._pyenv.root()), 'plugins', 'python-build', 'bin', 'python-build'), # noqa
|
@@ -7974,7 +8901,7 @@ class PyenvVersionInstaller:
|
|
7974
8901
|
*conf_args,
|
7975
8902
|
]
|
7976
8903
|
|
7977
|
-
await
|
8904
|
+
await asyncio_subprocesses.check_call(
|
7978
8905
|
*full_args,
|
7979
8906
|
env=env,
|
7980
8907
|
)
|
@@ -8826,7 +9753,28 @@ def main_bootstrap(bs: MainBootstrap) -> Injector:
|
|
8826
9753
|
# main.py
|
8827
9754
|
|
8828
9755
|
|
9756
|
+
@dc.dataclass(frozen=True)
|
9757
|
+
class ManageConfig:
|
9758
|
+
targets: ta.Optional[ta.Mapping[str, ManageTarget]] = None
|
9759
|
+
|
9760
|
+
|
8829
9761
|
class MainCli(ArgparseCli):
|
9762
|
+
config_file: ta.Optional[str] = argparse_arg('--config-file', help='Config file path') # type: ignore
|
9763
|
+
|
9764
|
+
@cached_nullary
|
9765
|
+
def config(self) -> ManageConfig:
|
9766
|
+
if (cf := self.config_file) is None:
|
9767
|
+
cf = os.path.expanduser('~/.omlish/manage.yml')
|
9768
|
+
if not os.path.isfile(cf):
|
9769
|
+
cf = None
|
9770
|
+
|
9771
|
+
if cf is None:
|
9772
|
+
return ManageConfig()
|
9773
|
+
else:
|
9774
|
+
return read_config_file(cf, ManageConfig)
|
9775
|
+
|
9776
|
+
#
|
9777
|
+
|
8830
9778
|
@argparse_command(
|
8831
9779
|
argparse_arg('--_payload-file'),
|
8832
9780
|
|
@@ -8878,10 +9826,13 @@ class MainCli(ArgparseCli):
|
|
8878
9826
|
|
8879
9827
|
msh = injector[ObjMarshalerManager]
|
8880
9828
|
|
8881
|
-
|
8882
|
-
if not ts.startswith('{'):
|
8883
|
-
|
8884
|
-
|
9829
|
+
tgt: ManageTarget
|
9830
|
+
if not (ts := self.args.target).startswith('{'):
|
9831
|
+
tgt = check.not_none(self.config().targets)[ts]
|
9832
|
+
else:
|
9833
|
+
tgt = msh.unmarshal_obj(json.loads(ts), ManageTarget)
|
9834
|
+
|
9835
|
+
#
|
8885
9836
|
|
8886
9837
|
cmds: ta.List[Command] = []
|
8887
9838
|
cmd: Command
|