ominfra 0.0.0.dev156__py3-none-any.whl → 0.0.0.dev157__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.
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): # ta.Callable[..., T]) -> ta.Callable[..., T]:
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
 
@@ -7129,12 +8043,8 @@ class SubprocessRemoteSpawning(RemoteSpawning):
7129
8043
  ) -> ta.AsyncGenerator[RemoteSpawning.Spawned, None]:
7130
8044
  pc = self._prepare_cmd(tgt, src)
7131
8045
 
7132
- cmd = pc.cmd
7133
- if not debug:
7134
- cmd = subprocess_maybe_shell_wrap_exec(*cmd)
7135
-
7136
8046
  async with asyncio_subprocesses.popen(
7137
- *cmd,
8047
+ *pc.cmd,
7138
8048
  shell=pc.shell,
7139
8049
  stdin=subprocess.PIPE,
7140
8050
  stdout=subprocess.PIPE,
@@ -7977,6 +8887,7 @@ class PyenvVersionInstaller:
7977
8887
  self._version,
7978
8888
  ]
7979
8889
 
8890
+ full_args: ta.List[str]
7980
8891
  if self._given_install_name is not None:
7981
8892
  full_args = [
7982
8893
  os.path.join(check.not_none(await self._pyenv.root()), 'plugins', 'python-build', 'bin', 'python-build'), # noqa
@@ -8842,7 +9753,28 @@ def main_bootstrap(bs: MainBootstrap) -> Injector:
8842
9753
  # main.py
8843
9754
 
8844
9755
 
9756
+ @dc.dataclass(frozen=True)
9757
+ class ManageConfig:
9758
+ targets: ta.Optional[ta.Mapping[str, ManageTarget]] = None
9759
+
9760
+
8845
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
+
8846
9778
  @argparse_command(
8847
9779
  argparse_arg('--_payload-file'),
8848
9780
 
@@ -8894,10 +9826,13 @@ class MainCli(ArgparseCli):
8894
9826
 
8895
9827
  msh = injector[ObjMarshalerManager]
8896
9828
 
8897
- ts = self.args.target
8898
- if not ts.startswith('{'):
8899
- ts = json.dumps({ts: {}})
8900
- tgt: ManageTarget = msh.unmarshal_obj(json.loads(ts), ManageTarget)
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
+ #
8901
9836
 
8902
9837
  cmds: ta.List[Command] = []
8903
9838
  cmd: Command