ominfra 0.0.0.dev142__py3-none-any.whl → 0.0.0.dev143__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
@@ -31,6 +31,7 @@ import subprocess
31
31
  import sys
32
32
  import threading
33
33
  import time
34
+ import traceback
34
35
  import types
35
36
  import typing as ta
36
37
  import uuid
@@ -48,10 +49,6 @@ if sys.version_info < (3, 8):
48
49
  ########################################
49
50
 
50
51
 
51
- # commands/base.py
52
- CommandT = ta.TypeVar('CommandT', bound='Command')
53
- CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
54
-
55
52
  # ../../omlish/lite/cached.py
56
53
  T = ta.TypeVar('T')
57
54
  CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
@@ -59,37 +56,39 @@ CallableT = ta.TypeVar('CallableT', bound=ta.Callable)
59
56
  # ../../omlish/lite/check.py
60
57
  SizedT = ta.TypeVar('SizedT', bound=ta.Sized)
61
58
 
59
+ # commands/base.py
60
+ CommandT = ta.TypeVar('CommandT', bound='Command')
61
+ CommandOutputT = ta.TypeVar('CommandOutputT', bound='Command.Output')
62
+
63
+ # ../../omlish/lite/inject.py
64
+ U = ta.TypeVar('U')
65
+ InjectorKeyCls = ta.Union[type, ta.NewType]
66
+ InjectorProviderFn = ta.Callable[['Injector'], ta.Any]
67
+ InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
68
+ InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
69
+
62
70
  # ../../omlish/lite/subprocesses.py
63
71
  SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull']
64
72
 
65
73
 
66
74
  ########################################
67
- # ../commands/base.py
68
-
69
-
70
- ##
75
+ # ../config.py
71
76
 
72
77
 
73
78
  @dc.dataclass(frozen=True)
74
- class Command(abc.ABC, ta.Generic[CommandOutputT]):
75
- @dc.dataclass(frozen=True)
76
- class Output(abc.ABC): # noqa
77
- pass
78
-
79
-
80
- ##
79
+ class MainConfig:
80
+ log_level: ta.Optional[str] = 'INFO'
81
81
 
82
-
83
- class CommandExecutor(abc.ABC, ta.Generic[CommandT, CommandOutputT]):
84
- @abc.abstractmethod
85
- def execute(self, i: CommandT) -> CommandOutputT:
86
- raise NotImplementedError
82
+ debug: bool = False
87
83
 
88
84
 
89
85
  ########################################
90
86
  # ../../pyremote.py
91
87
  """
92
88
  Basically this: https://mitogen.networkgenomics.com/howitworks.html
89
+
90
+ TODO:
91
+ - log: ta.Optional[logging.Logger] = None + log.debug's
93
92
  """
94
93
 
95
94
 
@@ -100,6 +99,9 @@ Basically this: https://mitogen.networkgenomics.com/howitworks.html
100
99
  class PyremoteBootstrapOptions:
101
100
  debug: bool = False
102
101
 
102
+ DEFAULT_MAIN_NAME_OVERRIDE: ta.ClassVar[str] = '__pyremote__'
103
+ main_name_override: ta.Optional[str] = DEFAULT_MAIN_NAME_OVERRIDE
104
+
103
105
 
104
106
  ##
105
107
 
@@ -416,6 +418,10 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
416
418
  os.dup2(nfd := os.open('/dev/null', os.O_WRONLY), 1)
417
419
  os.close(nfd)
418
420
 
421
+ if (mn := options.main_name_override) is not None:
422
+ # Inspections like typing.get_type_hints need an entry in sys.modules.
423
+ sys.modules[mn] = sys.modules['__main__']
424
+
419
425
  # Write fourth ack
420
426
  output.write(_PYREMOTE_BOOTSTRAP_ACK3)
421
427
 
@@ -434,14 +440,41 @@ def pyremote_bootstrap_finalize() -> PyremotePayloadRuntime:
434
440
 
435
441
 
436
442
  class PyremoteBootstrapDriver:
437
- def __init__(self, main_src: str, options: PyremoteBootstrapOptions = PyremoteBootstrapOptions()) -> None:
443
+ def __init__(
444
+ self,
445
+ main_src: ta.Union[str, ta.Sequence[str]],
446
+ options: PyremoteBootstrapOptions = PyremoteBootstrapOptions(),
447
+ ) -> None:
438
448
  super().__init__()
439
449
 
440
450
  self._main_src = main_src
441
- self._main_z = zlib.compress(main_src.encode('utf-8'))
442
-
443
451
  self._options = options
452
+
453
+ self._prepared_main_src = self._prepare_main_src(main_src, options)
454
+ self._main_z = zlib.compress(self._prepared_main_src.encode('utf-8'))
455
+
444
456
  self._options_json = json.dumps(dc.asdict(options), indent=None, separators=(',', ':')).encode('utf-8') # noqa
457
+ #
458
+
459
+ @classmethod
460
+ def _prepare_main_src(
461
+ cls,
462
+ main_src: ta.Union[str, ta.Sequence[str]],
463
+ options: PyremoteBootstrapOptions,
464
+ ) -> str:
465
+ parts: ta.List[str]
466
+ if isinstance(main_src, str):
467
+ parts = [main_src]
468
+ else:
469
+ parts = list(main_src)
470
+
471
+ if (mn := options.main_name_override) is not None:
472
+ parts.insert(0, f'__name__ = {mn!r}')
473
+
474
+ if len(parts) == 1:
475
+ return parts[0]
476
+ else:
477
+ return '\n\n'.join(parts)
445
478
 
446
479
  #
447
480
 
@@ -702,6 +735,99 @@ json_dump_compact: ta.Callable[..., bytes] = functools.partial(json.dump, **JSON
702
735
  json_dumps_compact: ta.Callable[..., str] = functools.partial(json.dumps, **JSON_COMPACT_KWARGS)
703
736
 
704
737
 
738
+ ########################################
739
+ # ../../../omlish/lite/maybes.py
740
+
741
+
742
+ class Maybe(ta.Generic[T]):
743
+ @property
744
+ @abc.abstractmethod
745
+ def present(self) -> bool:
746
+ raise NotImplementedError
747
+
748
+ @abc.abstractmethod
749
+ def must(self) -> T:
750
+ raise NotImplementedError
751
+
752
+ @classmethod
753
+ def just(cls, v: T) -> 'Maybe[T]':
754
+ return tuple.__new__(_Maybe, (v,)) # noqa
755
+
756
+ _empty: ta.ClassVar['Maybe']
757
+
758
+ @classmethod
759
+ def empty(cls) -> 'Maybe[T]':
760
+ return Maybe._empty
761
+
762
+
763
+ class _Maybe(Maybe[T], tuple):
764
+ __slots__ = ()
765
+
766
+ def __init_subclass__(cls, **kwargs):
767
+ raise TypeError
768
+
769
+ @property
770
+ def present(self) -> bool:
771
+ return bool(self)
772
+
773
+ def must(self) -> T:
774
+ if not self:
775
+ raise ValueError
776
+ return self[0]
777
+
778
+
779
+ Maybe._empty = tuple.__new__(_Maybe, ()) # noqa
780
+
781
+
782
+ ########################################
783
+ # ../../../omlish/lite/pycharm.py
784
+
785
+
786
+ DEFAULT_PYCHARM_VERSION = '242.23726.102'
787
+
788
+
789
+ @dc.dataclass(frozen=True)
790
+ class PycharmRemoteDebug:
791
+ port: int
792
+ host: ta.Optional[str] = 'localhost'
793
+ install_version: ta.Optional[str] = DEFAULT_PYCHARM_VERSION
794
+
795
+
796
+ def pycharm_debug_connect(prd: PycharmRemoteDebug) -> None:
797
+ if prd.install_version is not None:
798
+ import subprocess
799
+ import sys
800
+ subprocess.check_call([
801
+ sys.executable,
802
+ '-mpip',
803
+ 'install',
804
+ f'pydevd-pycharm~={prd.install_version}',
805
+ ])
806
+
807
+ pydevd_pycharm = __import__('pydevd_pycharm') # noqa
808
+ pydevd_pycharm.settrace(
809
+ prd.host,
810
+ port=prd.port,
811
+ stdoutToServer=True,
812
+ stderrToServer=True,
813
+ )
814
+
815
+
816
+ def pycharm_debug_preamble(prd: PycharmRemoteDebug) -> str:
817
+ import inspect
818
+ import textwrap
819
+
820
+ return textwrap.dedent(f"""
821
+ {inspect.getsource(pycharm_debug_connect)}
822
+
823
+ pycharm_debug_connect(PycharmRemoteDebug(
824
+ {prd.port!r},
825
+ host={prd.host!r},
826
+ install_version={prd.install_version!r},
827
+ ))
828
+ """)
829
+
830
+
705
831
  ########################################
706
832
  # ../../../omlish/lite/reflect.py
707
833
 
@@ -758,7 +884,238 @@ def deep_subclasses(cls: ta.Type[T]) -> ta.Iterator[ta.Type[T]]:
758
884
 
759
885
 
760
886
  ########################################
761
- # ../payload.py
887
+ # ../../../omlish/lite/strings.py
888
+
889
+
890
+ ##
891
+
892
+
893
+ def camel_case(name: str, lower: bool = False) -> str:
894
+ if not name:
895
+ return ''
896
+ s = ''.join(map(str.capitalize, name.split('_'))) # noqa
897
+ if lower:
898
+ s = s[0].lower() + s[1:]
899
+ return s
900
+
901
+
902
+ def snake_case(name: str) -> str:
903
+ uppers: list[int | None] = [i for i, c in enumerate(name) if c.isupper()]
904
+ return '_'.join([name[l:r].lower() for l, r in zip([None, *uppers], [*uppers, None])]).strip('_')
905
+
906
+
907
+ ##
908
+
909
+
910
+ def is_dunder(name: str) -> bool:
911
+ return (
912
+ name[:2] == name[-2:] == '__' and
913
+ name[2:3] != '_' and
914
+ name[-3:-2] != '_' and
915
+ len(name) > 4
916
+ )
917
+
918
+
919
+ def is_sunder(name: str) -> bool:
920
+ return (
921
+ name[0] == name[-1] == '_' and
922
+ name[1:2] != '_' and
923
+ name[-2:-1] != '_' and
924
+ len(name) > 2
925
+ )
926
+
927
+
928
+ ##
929
+
930
+
931
+ def attr_repr(obj: ta.Any, *attrs: str) -> str:
932
+ return f'{type(obj).__name__}({", ".join(f"{attr}={getattr(obj, attr)!r}" for attr in attrs)})'
933
+
934
+
935
+ ##
936
+
937
+
938
+ FORMAT_NUM_BYTES_SUFFIXES: ta.Sequence[str] = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB']
939
+
940
+
941
+ def format_num_bytes(num_bytes: int) -> str:
942
+ for i, suffix in enumerate(FORMAT_NUM_BYTES_SUFFIXES):
943
+ value = num_bytes / 1024 ** i
944
+ if num_bytes < 1024 ** (i + 1):
945
+ if value.is_integer():
946
+ return f'{int(value)}{suffix}'
947
+ else:
948
+ return f'{value:.2f}{suffix}'
949
+
950
+ return f'{num_bytes / 1024 ** (len(FORMAT_NUM_BYTES_SUFFIXES) - 1):.2f}{FORMAT_NUM_BYTES_SUFFIXES[-1]}'
951
+
952
+
953
+ ########################################
954
+ # ../commands/base.py
955
+
956
+
957
+ ##
958
+
959
+
960
+ @dc.dataclass(frozen=True)
961
+ class Command(abc.ABC, ta.Generic[CommandOutputT]):
962
+ @dc.dataclass(frozen=True)
963
+ class Output(abc.ABC): # noqa
964
+ pass
965
+
966
+ @ta.final
967
+ def execute(self, executor: 'CommandExecutor') -> CommandOutputT:
968
+ return check_isinstance(executor.execute(self), self.Output) # type: ignore[return-value]
969
+
970
+
971
+ ##
972
+
973
+
974
+ @dc.dataclass(frozen=True)
975
+ class CommandException:
976
+ name: str
977
+ repr: str
978
+
979
+ traceback: ta.Optional[str] = None
980
+
981
+ exc: ta.Optional[ta.Any] = None # Exception
982
+
983
+ cmd: ta.Optional[Command] = None
984
+
985
+ @classmethod
986
+ def of(
987
+ cls,
988
+ exc: Exception,
989
+ *,
990
+ omit_exc_object: bool = False,
991
+
992
+ cmd: ta.Optional[Command] = None,
993
+ ) -> 'CommandException':
994
+ return CommandException(
995
+ name=type(exc).__qualname__,
996
+ repr=repr(exc),
997
+
998
+ traceback=(
999
+ ''.join(traceback.format_tb(exc.__traceback__))
1000
+ if getattr(exc, '__traceback__', None) is not None else None
1001
+ ),
1002
+
1003
+ exc=None if omit_exc_object else exc,
1004
+
1005
+ cmd=cmd,
1006
+ )
1007
+
1008
+
1009
+ class CommandOutputOrException(abc.ABC, ta.Generic[CommandOutputT]):
1010
+ @property
1011
+ @abc.abstractmethod
1012
+ def output(self) -> ta.Optional[CommandOutputT]:
1013
+ raise NotImplementedError
1014
+
1015
+ @property
1016
+ @abc.abstractmethod
1017
+ def exception(self) -> ta.Optional[CommandException]:
1018
+ raise NotImplementedError
1019
+
1020
+
1021
+ @dc.dataclass(frozen=True)
1022
+ class CommandOutputOrExceptionData(CommandOutputOrException):
1023
+ output: ta.Optional[Command.Output] = None
1024
+ exception: ta.Optional[CommandException] = None
1025
+
1026
+
1027
+ class CommandExecutor(abc.ABC, ta.Generic[CommandT, CommandOutputT]):
1028
+ @abc.abstractmethod
1029
+ def execute(self, cmd: CommandT) -> CommandOutputT:
1030
+ raise NotImplementedError
1031
+
1032
+ def try_execute(
1033
+ self,
1034
+ cmd: CommandT,
1035
+ *,
1036
+ log: ta.Optional[logging.Logger] = None,
1037
+ omit_exc_object: bool = False,
1038
+ ) -> CommandOutputOrException[CommandOutputT]:
1039
+ try:
1040
+ o = self.execute(cmd)
1041
+
1042
+ except Exception as e: # noqa
1043
+ if log is not None:
1044
+ log.exception('Exception executing command: %r', type(cmd))
1045
+
1046
+ return CommandOutputOrExceptionData(exception=CommandException.of(
1047
+ e,
1048
+ omit_exc_object=omit_exc_object,
1049
+ cmd=cmd,
1050
+ ))
1051
+
1052
+ else:
1053
+ return CommandOutputOrExceptionData(output=o)
1054
+
1055
+
1056
+ ##
1057
+
1058
+
1059
+ @dc.dataclass(frozen=True)
1060
+ class CommandRegistration:
1061
+ command_cls: ta.Type[Command]
1062
+
1063
+ name: ta.Optional[str] = None
1064
+
1065
+ @property
1066
+ def name_or_default(self) -> str:
1067
+ if not (cls_name := self.command_cls.__name__).endswith('Command'):
1068
+ raise NameError(cls_name)
1069
+ return snake_case(cls_name[:-len('Command')])
1070
+
1071
+
1072
+ CommandRegistrations = ta.NewType('CommandRegistrations', ta.Sequence[CommandRegistration])
1073
+
1074
+
1075
+ ##
1076
+
1077
+
1078
+ @dc.dataclass(frozen=True)
1079
+ class CommandExecutorRegistration:
1080
+ command_cls: ta.Type[Command]
1081
+ executor_cls: ta.Type[CommandExecutor]
1082
+
1083
+
1084
+ CommandExecutorRegistrations = ta.NewType('CommandExecutorRegistrations', ta.Sequence[CommandExecutorRegistration])
1085
+
1086
+
1087
+ ##
1088
+
1089
+
1090
+ CommandNameMap = ta.NewType('CommandNameMap', ta.Mapping[str, ta.Type[Command]])
1091
+
1092
+
1093
+ def build_command_name_map(crs: CommandRegistrations) -> CommandNameMap:
1094
+ dct: ta.Dict[str, ta.Type[Command]] = {}
1095
+ cr: CommandRegistration
1096
+ for cr in crs:
1097
+ if (name := cr.name_or_default) in dct:
1098
+ raise NameError(name)
1099
+ dct[name] = cr.command_cls
1100
+ return CommandNameMap(dct)
1101
+
1102
+
1103
+ ########################################
1104
+ # ../remote/config.py
1105
+
1106
+
1107
+ @dc.dataclass(frozen=True)
1108
+ class RemoteConfig:
1109
+ payload_file: ta.Optional[str] = None
1110
+
1111
+ pycharm_remote_debug: ta.Optional[PycharmRemoteDebug] = None
1112
+
1113
+
1114
+ ########################################
1115
+ # ../remote/payload.py
1116
+
1117
+
1118
+ RemoteExecutionPayloadFile = ta.NewType('RemoteExecutionPayloadFile', str)
762
1119
 
763
1120
 
764
1121
  @cached_nullary
@@ -773,21 +1130,917 @@ def _is_src_amalg(src: str) -> bool:
773
1130
  return False
774
1131
 
775
1132
 
776
- @cached_nullary
777
- def _is_self_amalg() -> bool:
778
- return _is_src_amalg(_get_self_src())
1133
+ @cached_nullary
1134
+ def _is_self_amalg() -> bool:
1135
+ return _is_src_amalg(_get_self_src())
1136
+
1137
+
1138
+ def get_remote_payload_src(
1139
+ *,
1140
+ file: ta.Optional[RemoteExecutionPayloadFile],
1141
+ ) -> str:
1142
+ if file is not None:
1143
+ with open(file) as f:
1144
+ return f.read()
1145
+
1146
+ if _is_self_amalg():
1147
+ return _get_self_src()
1148
+
1149
+ import importlib.resources
1150
+ return importlib.resources.files(__package__.split('.')[0] + '.scripts').joinpath('manage.py').read_text()
1151
+
1152
+
1153
+ ########################################
1154
+ # ../../../omlish/lite/inject.py
1155
+
1156
+
1157
+ ###
1158
+ # types
1159
+
1160
+
1161
+ @dc.dataclass(frozen=True)
1162
+ class InjectorKey(ta.Generic[T]):
1163
+ # Before PEP-560 typing.Generic was a metaclass with a __new__ that takes a 'cls' arg, so instantiating a dataclass
1164
+ # with kwargs (such as through dc.replace) causes `TypeError: __new__() got multiple values for argument 'cls'`.
1165
+ # See:
1166
+ # - https://github.com/python/cpython/commit/d911e40e788fb679723d78b6ea11cabf46caed5a
1167
+ # - https://gist.github.com/wrmsr/4468b86efe9f373b6b114bfe85b98fd3
1168
+ cls_: InjectorKeyCls
1169
+
1170
+ tag: ta.Any = None
1171
+ array: bool = False
1172
+
1173
+
1174
+ def is_valid_injector_key_cls(cls: ta.Any) -> bool:
1175
+ return isinstance(cls, type) or is_new_type(cls)
1176
+
1177
+
1178
+ def check_valid_injector_key_cls(cls: T) -> T:
1179
+ if not is_valid_injector_key_cls(cls):
1180
+ raise TypeError(cls)
1181
+ return cls
1182
+
1183
+
1184
+ ##
1185
+
1186
+
1187
+ class InjectorProvider(abc.ABC):
1188
+ @abc.abstractmethod
1189
+ def provider_fn(self) -> InjectorProviderFn:
1190
+ raise NotImplementedError
1191
+
1192
+
1193
+ ##
1194
+
1195
+
1196
+ @dc.dataclass(frozen=True)
1197
+ class InjectorBinding:
1198
+ key: InjectorKey
1199
+ provider: InjectorProvider
1200
+
1201
+
1202
+ class InjectorBindings(abc.ABC):
1203
+ @abc.abstractmethod
1204
+ def bindings(self) -> ta.Iterator[InjectorBinding]:
1205
+ raise NotImplementedError
1206
+
1207
+ ##
1208
+
1209
+
1210
+ class Injector(abc.ABC):
1211
+ @abc.abstractmethod
1212
+ def try_provide(self, key: ta.Any) -> Maybe[ta.Any]:
1213
+ raise NotImplementedError
1214
+
1215
+ @abc.abstractmethod
1216
+ def provide(self, key: ta.Any) -> ta.Any:
1217
+ raise NotImplementedError
1218
+
1219
+ @abc.abstractmethod
1220
+ def provide_kwargs(
1221
+ self,
1222
+ obj: ta.Any,
1223
+ *,
1224
+ skip_args: int = 0,
1225
+ skip_kwargs: ta.Optional[ta.Iterable[ta.Any]] = None,
1226
+ ) -> ta.Mapping[str, ta.Any]:
1227
+ raise NotImplementedError
1228
+
1229
+ @abc.abstractmethod
1230
+ def inject(
1231
+ self,
1232
+ obj: ta.Any,
1233
+ *,
1234
+ args: ta.Optional[ta.Sequence[ta.Any]] = None,
1235
+ kwargs: ta.Optional[ta.Mapping[str, ta.Any]] = None,
1236
+ ) -> ta.Any:
1237
+ raise NotImplementedError
1238
+
1239
+ def __getitem__(
1240
+ self,
1241
+ target: ta.Union[InjectorKey[T], ta.Type[T]],
1242
+ ) -> T:
1243
+ return self.provide(target)
1244
+
1245
+
1246
+ ###
1247
+ # exceptions
1248
+
1249
+
1250
+ class InjectorError(Exception):
1251
+ pass
1252
+
1253
+
1254
+ @dc.dataclass()
1255
+ class InjectorKeyError(InjectorError):
1256
+ key: InjectorKey
1257
+
1258
+ source: ta.Any = None
1259
+ name: ta.Optional[str] = None
1260
+
1261
+
1262
+ class UnboundInjectorKeyError(InjectorKeyError):
1263
+ pass
1264
+
1265
+
1266
+ class DuplicateInjectorKeyError(InjectorKeyError):
1267
+ pass
1268
+
1269
+
1270
+ class CyclicDependencyInjectorKeyError(InjectorKeyError):
1271
+ pass
1272
+
1273
+
1274
+ ###
1275
+ # keys
1276
+
1277
+
1278
+ def as_injector_key(o: ta.Any) -> InjectorKey:
1279
+ if o is inspect.Parameter.empty:
1280
+ raise TypeError(o)
1281
+ if isinstance(o, InjectorKey):
1282
+ return o
1283
+ if is_valid_injector_key_cls(o):
1284
+ return InjectorKey(o)
1285
+ raise TypeError(o)
1286
+
1287
+
1288
+ ###
1289
+ # providers
1290
+
1291
+
1292
+ @dc.dataclass(frozen=True)
1293
+ class FnInjectorProvider(InjectorProvider):
1294
+ fn: ta.Any
1295
+
1296
+ def __post_init__(self) -> None:
1297
+ check_not_isinstance(self.fn, type)
1298
+
1299
+ def provider_fn(self) -> InjectorProviderFn:
1300
+ def pfn(i: Injector) -> ta.Any:
1301
+ return i.inject(self.fn)
1302
+
1303
+ return pfn
1304
+
1305
+
1306
+ @dc.dataclass(frozen=True)
1307
+ class CtorInjectorProvider(InjectorProvider):
1308
+ cls_: type
1309
+
1310
+ def __post_init__(self) -> None:
1311
+ check_isinstance(self.cls_, type)
1312
+
1313
+ def provider_fn(self) -> InjectorProviderFn:
1314
+ def pfn(i: Injector) -> ta.Any:
1315
+ return i.inject(self.cls_)
1316
+
1317
+ return pfn
1318
+
1319
+
1320
+ @dc.dataclass(frozen=True)
1321
+ class ConstInjectorProvider(InjectorProvider):
1322
+ v: ta.Any
1323
+
1324
+ def provider_fn(self) -> InjectorProviderFn:
1325
+ return lambda _: self.v
1326
+
1327
+
1328
+ @dc.dataclass(frozen=True)
1329
+ class SingletonInjectorProvider(InjectorProvider):
1330
+ p: InjectorProvider
1331
+
1332
+ def __post_init__(self) -> None:
1333
+ check_isinstance(self.p, InjectorProvider)
1334
+
1335
+ def provider_fn(self) -> InjectorProviderFn:
1336
+ v = not_set = object()
1337
+
1338
+ def pfn(i: Injector) -> ta.Any:
1339
+ nonlocal v
1340
+ if v is not_set:
1341
+ v = ufn(i)
1342
+ return v
1343
+
1344
+ ufn = self.p.provider_fn()
1345
+ return pfn
1346
+
1347
+
1348
+ @dc.dataclass(frozen=True)
1349
+ class LinkInjectorProvider(InjectorProvider):
1350
+ k: InjectorKey
1351
+
1352
+ def __post_init__(self) -> None:
1353
+ check_isinstance(self.k, InjectorKey)
1354
+
1355
+ def provider_fn(self) -> InjectorProviderFn:
1356
+ def pfn(i: Injector) -> ta.Any:
1357
+ return i.provide(self.k)
1358
+
1359
+ return pfn
1360
+
1361
+
1362
+ @dc.dataclass(frozen=True)
1363
+ class ArrayInjectorProvider(InjectorProvider):
1364
+ ps: ta.Sequence[InjectorProvider]
1365
+
1366
+ def provider_fn(self) -> InjectorProviderFn:
1367
+ ps = [p.provider_fn() for p in self.ps]
1368
+
1369
+ def pfn(i: Injector) -> ta.Any:
1370
+ rv = []
1371
+ for ep in ps:
1372
+ o = ep(i)
1373
+ rv.append(o)
1374
+ return rv
1375
+
1376
+ return pfn
1377
+
1378
+
1379
+ ###
1380
+ # bindings
1381
+
1382
+
1383
+ @dc.dataclass(frozen=True)
1384
+ class _InjectorBindings(InjectorBindings):
1385
+ bs: ta.Optional[ta.Sequence[InjectorBinding]] = None
1386
+ ps: ta.Optional[ta.Sequence[InjectorBindings]] = None
1387
+
1388
+ def bindings(self) -> ta.Iterator[InjectorBinding]:
1389
+ if self.bs is not None:
1390
+ yield from self.bs
1391
+ if self.ps is not None:
1392
+ for p in self.ps:
1393
+ yield from p.bindings()
1394
+
1395
+
1396
+ def as_injector_bindings(*args: InjectorBindingOrBindings) -> InjectorBindings:
1397
+ bs: ta.List[InjectorBinding] = []
1398
+ ps: ta.List[InjectorBindings] = []
1399
+
1400
+ for a in args:
1401
+ if isinstance(a, InjectorBindings):
1402
+ ps.append(a)
1403
+ elif isinstance(a, InjectorBinding):
1404
+ bs.append(a)
1405
+ else:
1406
+ raise TypeError(a)
1407
+
1408
+ return _InjectorBindings(
1409
+ bs or None,
1410
+ ps or None,
1411
+ )
1412
+
1413
+
1414
+ ##
1415
+
1416
+
1417
+ @dc.dataclass(frozen=True)
1418
+ class OverridesInjectorBindings(InjectorBindings):
1419
+ p: InjectorBindings
1420
+ m: ta.Mapping[InjectorKey, InjectorBinding]
1421
+
1422
+ def bindings(self) -> ta.Iterator[InjectorBinding]:
1423
+ for b in self.p.bindings():
1424
+ yield self.m.get(b.key, b)
1425
+
1426
+
1427
+ def injector_override(p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
1428
+ m: ta.Dict[InjectorKey, InjectorBinding] = {}
1429
+
1430
+ for b in as_injector_bindings(*args).bindings():
1431
+ if b.key in m:
1432
+ raise DuplicateInjectorKeyError(b.key)
1433
+ m[b.key] = b
1434
+
1435
+ return OverridesInjectorBindings(p, m)
1436
+
1437
+
1438
+ ##
1439
+
1440
+
1441
+ def build_injector_provider_map(bs: InjectorBindings) -> ta.Mapping[InjectorKey, InjectorProvider]:
1442
+ pm: ta.Dict[InjectorKey, InjectorProvider] = {}
1443
+ am: ta.Dict[InjectorKey, ta.List[InjectorProvider]] = {}
1444
+
1445
+ for b in bs.bindings():
1446
+ if b.key.array:
1447
+ al = am.setdefault(b.key, [])
1448
+ if isinstance(b.provider, ArrayInjectorProvider):
1449
+ al.extend(b.provider.ps)
1450
+ else:
1451
+ al.append(b.provider)
1452
+ else:
1453
+ if b.key in pm:
1454
+ raise KeyError(b.key)
1455
+ pm[b.key] = b.provider
1456
+
1457
+ if am:
1458
+ for k, aps in am.items():
1459
+ pm[k] = ArrayInjectorProvider(aps)
1460
+
1461
+ return pm
1462
+
1463
+
1464
+ ###
1465
+ # inspection
1466
+
1467
+
1468
+ class _InjectionInspection(ta.NamedTuple):
1469
+ signature: inspect.Signature
1470
+ type_hints: ta.Mapping[str, ta.Any]
1471
+ args_offset: int
1472
+
1473
+
1474
+ _INJECTION_INSPECTION_CACHE: ta.MutableMapping[ta.Any, _InjectionInspection] = weakref.WeakKeyDictionary()
1475
+
1476
+
1477
+ def _do_injection_inspect(obj: ta.Any) -> _InjectionInspection:
1478
+ tgt = obj
1479
+ if isinstance(tgt, type) and tgt.__init__ is not object.__init__: # type: ignore[misc]
1480
+ # Python 3.8's inspect.signature can't handle subclasses overriding __new__, always generating *args/**kwargs.
1481
+ # - https://bugs.python.org/issue40897
1482
+ # - https://github.com/python/cpython/commit/df7c62980d15acd3125dfbd81546dad359f7add7
1483
+ tgt = tgt.__init__ # type: ignore[misc]
1484
+ has_generic_base = True
1485
+ else:
1486
+ has_generic_base = False
1487
+
1488
+ # inspect.signature(eval_str=True) was added in 3.10 and we have to support 3.8, so we have to get_type_hints to
1489
+ # eval str annotations *in addition to* getting the signature for parameter information.
1490
+ uw = tgt
1491
+ has_partial = False
1492
+ while True:
1493
+ if isinstance(uw, functools.partial):
1494
+ has_partial = True
1495
+ uw = uw.func
1496
+ else:
1497
+ if (uw2 := inspect.unwrap(uw)) is uw:
1498
+ break
1499
+ uw = uw2
1500
+
1501
+ if has_generic_base and has_partial:
1502
+ raise InjectorError(
1503
+ 'Injector inspection does not currently support both a typing.Generic base and a functools.partial: '
1504
+ f'{obj}',
1505
+ )
1506
+
1507
+ return _InjectionInspection(
1508
+ inspect.signature(tgt),
1509
+ ta.get_type_hints(uw),
1510
+ 1 if has_generic_base else 0,
1511
+ )
1512
+
1513
+
1514
+ def _injection_inspect(obj: ta.Any) -> _InjectionInspection:
1515
+ try:
1516
+ return _INJECTION_INSPECTION_CACHE[obj]
1517
+ except TypeError:
1518
+ return _do_injection_inspect(obj)
1519
+ except KeyError:
1520
+ pass
1521
+ insp = _do_injection_inspect(obj)
1522
+ _INJECTION_INSPECTION_CACHE[obj] = insp
1523
+ return insp
1524
+
1525
+
1526
+ class InjectionKwarg(ta.NamedTuple):
1527
+ name: str
1528
+ key: InjectorKey
1529
+ has_default: bool
1530
+
1531
+
1532
+ class InjectionKwargsTarget(ta.NamedTuple):
1533
+ obj: ta.Any
1534
+ kwargs: ta.Sequence[InjectionKwarg]
1535
+
1536
+
1537
+ def build_injection_kwargs_target(
1538
+ obj: ta.Any,
1539
+ *,
1540
+ skip_args: int = 0,
1541
+ skip_kwargs: ta.Optional[ta.Iterable[str]] = None,
1542
+ raw_optional: bool = False,
1543
+ ) -> InjectionKwargsTarget:
1544
+ insp = _injection_inspect(obj)
1545
+
1546
+ params = list(insp.signature.parameters.values())
1547
+
1548
+ skip_names: ta.Set[str] = set()
1549
+ if skip_kwargs is not None:
1550
+ skip_names.update(check_not_isinstance(skip_kwargs, str))
1551
+
1552
+ seen: ta.Set[InjectorKey] = set()
1553
+ kws: ta.List[InjectionKwarg] = []
1554
+ for p in params[insp.args_offset + skip_args:]:
1555
+ if p.name in skip_names:
1556
+ continue
1557
+
1558
+ if p.annotation is inspect.Signature.empty:
1559
+ if p.default is not inspect.Parameter.empty:
1560
+ raise KeyError(f'{obj}, {p.name}')
1561
+ continue
1562
+
1563
+ if p.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):
1564
+ raise TypeError(insp)
1565
+
1566
+ # 3.8 inspect.signature doesn't eval_str but typing.get_type_hints does, so prefer that.
1567
+ ann = insp.type_hints.get(p.name, p.annotation)
1568
+ if (
1569
+ not raw_optional and
1570
+ is_optional_alias(ann)
1571
+ ):
1572
+ ann = get_optional_alias_arg(ann)
1573
+
1574
+ k = as_injector_key(ann)
1575
+
1576
+ if k in seen:
1577
+ raise DuplicateInjectorKeyError(k)
1578
+ seen.add(k)
1579
+
1580
+ kws.append(InjectionKwarg(
1581
+ p.name,
1582
+ k,
1583
+ p.default is not inspect.Parameter.empty,
1584
+ ))
1585
+
1586
+ return InjectionKwargsTarget(
1587
+ obj,
1588
+ kws,
1589
+ )
1590
+
1591
+
1592
+ ###
1593
+ # injector
1594
+
1595
+
1596
+ _INJECTOR_INJECTOR_KEY: InjectorKey[Injector] = InjectorKey(Injector)
1597
+
1598
+
1599
+ @dc.dataclass(frozen=True)
1600
+ class _InjectorEager:
1601
+ key: InjectorKey
1602
+
1603
+
1604
+ _INJECTOR_EAGER_ARRAY_KEY: InjectorKey[_InjectorEager] = InjectorKey(_InjectorEager, array=True)
1605
+
1606
+
1607
+ class _Injector(Injector):
1608
+ def __init__(self, bs: InjectorBindings, p: ta.Optional[Injector] = None) -> None:
1609
+ super().__init__()
1610
+
1611
+ self._bs = check_isinstance(bs, InjectorBindings)
1612
+ self._p: ta.Optional[Injector] = check_isinstance(p, (Injector, type(None)))
1613
+
1614
+ self._pfm = {k: v.provider_fn() for k, v in build_injector_provider_map(bs).items()}
1615
+
1616
+ if _INJECTOR_INJECTOR_KEY in self._pfm:
1617
+ raise DuplicateInjectorKeyError(_INJECTOR_INJECTOR_KEY)
1618
+
1619
+ self.__cur_req: ta.Optional[_Injector._Request] = None
1620
+
1621
+ if _INJECTOR_EAGER_ARRAY_KEY in self._pfm:
1622
+ for e in self.provide(_INJECTOR_EAGER_ARRAY_KEY):
1623
+ self.provide(e.key)
1624
+
1625
+ class _Request:
1626
+ def __init__(self, injector: '_Injector') -> None:
1627
+ super().__init__()
1628
+ self._injector = injector
1629
+ self._provisions: ta.Dict[InjectorKey, Maybe] = {}
1630
+ self._seen_keys: ta.Set[InjectorKey] = set()
1631
+
1632
+ def handle_key(self, key: InjectorKey) -> Maybe[Maybe]:
1633
+ try:
1634
+ return Maybe.just(self._provisions[key])
1635
+ except KeyError:
1636
+ pass
1637
+ if key in self._seen_keys:
1638
+ raise CyclicDependencyInjectorKeyError(key)
1639
+ self._seen_keys.add(key)
1640
+ return Maybe.empty()
1641
+
1642
+ def handle_provision(self, key: InjectorKey, mv: Maybe) -> Maybe:
1643
+ check_in(key, self._seen_keys)
1644
+ check_not_in(key, self._provisions)
1645
+ self._provisions[key] = mv
1646
+ return mv
1647
+
1648
+ @contextlib.contextmanager
1649
+ def _current_request(self) -> ta.Generator[_Request, None, None]:
1650
+ if (cr := self.__cur_req) is not None:
1651
+ yield cr
1652
+ return
1653
+
1654
+ cr = self._Request(self)
1655
+ try:
1656
+ self.__cur_req = cr
1657
+ yield cr
1658
+ finally:
1659
+ self.__cur_req = None
1660
+
1661
+ def try_provide(self, key: ta.Any) -> Maybe[ta.Any]:
1662
+ key = as_injector_key(key)
1663
+
1664
+ cr: _Injector._Request
1665
+ with self._current_request() as cr:
1666
+ if (rv := cr.handle_key(key)).present:
1667
+ return rv.must()
1668
+
1669
+ if key == _INJECTOR_INJECTOR_KEY:
1670
+ return cr.handle_provision(key, Maybe.just(self))
1671
+
1672
+ fn = self._pfm.get(key)
1673
+ if fn is not None:
1674
+ return cr.handle_provision(key, Maybe.just(fn(self)))
1675
+
1676
+ if self._p is not None:
1677
+ pv = self._p.try_provide(key)
1678
+ if pv is not None:
1679
+ return cr.handle_provision(key, Maybe.empty())
1680
+
1681
+ return cr.handle_provision(key, Maybe.empty())
1682
+
1683
+ def provide(self, key: ta.Any) -> ta.Any:
1684
+ v = self.try_provide(key)
1685
+ if v.present:
1686
+ return v.must()
1687
+ raise UnboundInjectorKeyError(key)
1688
+
1689
+ def provide_kwargs(
1690
+ self,
1691
+ obj: ta.Any,
1692
+ *,
1693
+ skip_args: int = 0,
1694
+ skip_kwargs: ta.Optional[ta.Iterable[ta.Any]] = None,
1695
+ ) -> ta.Mapping[str, ta.Any]:
1696
+ kt = build_injection_kwargs_target(
1697
+ obj,
1698
+ skip_args=skip_args,
1699
+ skip_kwargs=skip_kwargs,
1700
+ )
1701
+
1702
+ ret: ta.Dict[str, ta.Any] = {}
1703
+ for kw in kt.kwargs:
1704
+ if kw.has_default:
1705
+ if not (mv := self.try_provide(kw.key)).present:
1706
+ continue
1707
+ v = mv.must()
1708
+ else:
1709
+ v = self.provide(kw.key)
1710
+ ret[kw.name] = v
1711
+ return ret
1712
+
1713
+ def inject(
1714
+ self,
1715
+ obj: ta.Any,
1716
+ *,
1717
+ args: ta.Optional[ta.Sequence[ta.Any]] = None,
1718
+ kwargs: ta.Optional[ta.Mapping[str, ta.Any]] = None,
1719
+ ) -> ta.Any:
1720
+ provided = self.provide_kwargs(
1721
+ obj,
1722
+ skip_args=len(args) if args is not None else 0,
1723
+ skip_kwargs=kwargs if kwargs is not None else None,
1724
+ )
1725
+
1726
+ return obj(
1727
+ *(args if args is not None else ()),
1728
+ **(kwargs if kwargs is not None else {}),
1729
+ **provided,
1730
+ )
1731
+
1732
+
1733
+ ###
1734
+ # binder
1735
+
1736
+
1737
+ class InjectorBinder:
1738
+ def __new__(cls, *args, **kwargs): # noqa
1739
+ raise TypeError
1740
+
1741
+ _FN_TYPES: ta.Tuple[type, ...] = (
1742
+ types.FunctionType,
1743
+ types.MethodType,
1744
+
1745
+ classmethod,
1746
+ staticmethod,
1747
+
1748
+ functools.partial,
1749
+ functools.partialmethod,
1750
+ )
1751
+
1752
+ @classmethod
1753
+ def _is_fn(cls, obj: ta.Any) -> bool:
1754
+ return isinstance(obj, cls._FN_TYPES)
1755
+
1756
+ @classmethod
1757
+ def bind_as_fn(cls, icls: ta.Type[T]) -> ta.Type[T]:
1758
+ check_isinstance(icls, type)
1759
+ if icls not in cls._FN_TYPES:
1760
+ cls._FN_TYPES = (*cls._FN_TYPES, icls)
1761
+ return icls
1762
+
1763
+ _BANNED_BIND_TYPES: ta.Tuple[type, ...] = (
1764
+ InjectorProvider,
1765
+ )
1766
+
1767
+ @classmethod
1768
+ def bind(
1769
+ cls,
1770
+ obj: ta.Any,
1771
+ *,
1772
+ key: ta.Any = None,
1773
+ tag: ta.Any = None,
1774
+ array: ta.Optional[bool] = None, # noqa
1775
+
1776
+ to_fn: ta.Any = None,
1777
+ to_ctor: ta.Any = None,
1778
+ to_const: ta.Any = None,
1779
+ to_key: ta.Any = None,
1780
+
1781
+ singleton: bool = False,
1782
+
1783
+ eager: bool = False,
1784
+ ) -> InjectorBindingOrBindings:
1785
+ if obj is None or obj is inspect.Parameter.empty:
1786
+ raise TypeError(obj)
1787
+ if isinstance(obj, cls._BANNED_BIND_TYPES):
1788
+ raise TypeError(obj)
1789
+
1790
+ ##
1791
+
1792
+ if key is not None:
1793
+ key = as_injector_key(key)
1794
+
1795
+ ##
1796
+
1797
+ has_to = (
1798
+ to_fn is not None or
1799
+ to_ctor is not None or
1800
+ to_const is not None or
1801
+ to_key is not None
1802
+ )
1803
+ if isinstance(obj, InjectorKey):
1804
+ if key is None:
1805
+ key = obj
1806
+ elif isinstance(obj, type):
1807
+ if not has_to:
1808
+ to_ctor = obj
1809
+ if key is None:
1810
+ key = InjectorKey(obj)
1811
+ elif cls._is_fn(obj) and not has_to:
1812
+ to_fn = obj
1813
+ if key is None:
1814
+ insp = _injection_inspect(obj)
1815
+ key_cls: ta.Any = check_valid_injector_key_cls(check_not_none(insp.type_hints.get('return')))
1816
+ key = InjectorKey(key_cls)
1817
+ else:
1818
+ if to_const is not None:
1819
+ raise TypeError('Cannot bind instance with to_const')
1820
+ to_const = obj
1821
+ if key is None:
1822
+ key = InjectorKey(type(obj))
1823
+ del has_to
1824
+
1825
+ ##
1826
+
1827
+ if tag is not None:
1828
+ if key.tag is not None:
1829
+ raise TypeError('Tag already set')
1830
+ key = dc.replace(key, tag=tag)
1831
+
1832
+ if array is not None:
1833
+ key = dc.replace(key, array=array)
1834
+
1835
+ ##
1836
+
1837
+ providers: ta.List[InjectorProvider] = []
1838
+ if to_fn is not None:
1839
+ providers.append(FnInjectorProvider(to_fn))
1840
+ if to_ctor is not None:
1841
+ providers.append(CtorInjectorProvider(to_ctor))
1842
+ if to_const is not None:
1843
+ providers.append(ConstInjectorProvider(to_const))
1844
+ if to_key is not None:
1845
+ providers.append(LinkInjectorProvider(as_injector_key(to_key)))
1846
+ if not providers:
1847
+ raise TypeError('Must specify provider')
1848
+ if len(providers) > 1:
1849
+ raise TypeError('May not specify multiple providers')
1850
+ provider, = providers
1851
+
1852
+ ##
1853
+
1854
+ if singleton:
1855
+ provider = SingletonInjectorProvider(provider)
1856
+
1857
+ binding = InjectorBinding(key, provider)
1858
+
1859
+ ##
1860
+
1861
+ extras: ta.List[InjectorBinding] = []
1862
+
1863
+ if eager:
1864
+ extras.append(bind_injector_eager_key(key))
1865
+
1866
+ ##
1867
+
1868
+ if extras:
1869
+ return as_injector_bindings(binding, *extras)
1870
+ else:
1871
+ return binding
1872
+
1873
+
1874
+ ###
1875
+ # injection helpers
1876
+
1877
+
1878
+ def make_injector_factory(
1879
+ fn: ta.Callable[..., T],
1880
+ cls: U,
1881
+ ann: ta.Any = None,
1882
+ ) -> ta.Callable[..., U]:
1883
+ if ann is None:
1884
+ ann = cls
1885
+
1886
+ def outer(injector: Injector) -> ann:
1887
+ def inner(*args, **kwargs):
1888
+ return injector.inject(fn, args=args, kwargs=kwargs)
1889
+ return cls(inner) # type: ignore
1890
+
1891
+ return outer
1892
+
1893
+
1894
+ def bind_injector_array(
1895
+ obj: ta.Any = None,
1896
+ *,
1897
+ tag: ta.Any = None,
1898
+ ) -> InjectorBindingOrBindings:
1899
+ key = as_injector_key(obj)
1900
+ if tag is not None:
1901
+ if key.tag is not None:
1902
+ raise ValueError('Must not specify multiple tags')
1903
+ key = dc.replace(key, tag=tag)
1904
+
1905
+ if key.array:
1906
+ raise ValueError('Key must not be array')
1907
+
1908
+ return InjectorBinding(
1909
+ dc.replace(key, array=True),
1910
+ ArrayInjectorProvider([]),
1911
+ )
1912
+
1913
+
1914
+ def make_injector_array_type(
1915
+ ele: ta.Union[InjectorKey, InjectorKeyCls],
1916
+ cls: U,
1917
+ ann: ta.Any = None,
1918
+ ) -> ta.Callable[..., U]:
1919
+ if isinstance(ele, InjectorKey):
1920
+ if not ele.array:
1921
+ raise InjectorError('Provided key must be array', ele)
1922
+ key = ele
1923
+ else:
1924
+ key = dc.replace(as_injector_key(ele), array=True)
1925
+
1926
+ if ann is None:
1927
+ ann = cls
1928
+
1929
+ def inner(injector: Injector) -> ann:
1930
+ return cls(injector.provide(key)) # type: ignore[operator]
1931
+
1932
+ return inner
1933
+
1934
+
1935
+ def bind_injector_eager_key(key: ta.Any) -> InjectorBinding:
1936
+ return InjectorBinding(_INJECTOR_EAGER_ARRAY_KEY, ConstInjectorProvider(_InjectorEager(as_injector_key(key))))
1937
+
1938
+
1939
+ ##
1940
+
1941
+
1942
+ class Injection:
1943
+ def __new__(cls, *args, **kwargs): # noqa
1944
+ raise TypeError
1945
+
1946
+ # keys
1947
+
1948
+ @classmethod
1949
+ def as_key(cls, o: ta.Any) -> InjectorKey:
1950
+ return as_injector_key(o)
1951
+
1952
+ @classmethod
1953
+ def array(cls, o: ta.Any) -> InjectorKey:
1954
+ return dc.replace(as_injector_key(o), array=True)
1955
+
1956
+ @classmethod
1957
+ def tag(cls, o: ta.Any, t: ta.Any) -> InjectorKey:
1958
+ return dc.replace(as_injector_key(o), tag=t)
1959
+
1960
+ # bindings
1961
+
1962
+ @classmethod
1963
+ def as_bindings(cls, *args: InjectorBindingOrBindings) -> InjectorBindings:
1964
+ return as_injector_bindings(*args)
1965
+
1966
+ @classmethod
1967
+ def override(cls, p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
1968
+ return injector_override(p, *args)
1969
+
1970
+ # injector
1971
+
1972
+ @classmethod
1973
+ def create_injector(cls, *args: InjectorBindingOrBindings, parent: ta.Optional[Injector] = None) -> Injector:
1974
+ return _Injector(as_injector_bindings(*args), parent)
1975
+
1976
+ # binder
1977
+
1978
+ @classmethod
1979
+ def bind(
1980
+ cls,
1981
+ obj: ta.Any,
1982
+ *,
1983
+ key: ta.Any = None,
1984
+ tag: ta.Any = None,
1985
+ array: ta.Optional[bool] = None, # noqa
1986
+
1987
+ to_fn: ta.Any = None,
1988
+ to_ctor: ta.Any = None,
1989
+ to_const: ta.Any = None,
1990
+ to_key: ta.Any = None,
1991
+
1992
+ singleton: bool = False,
1993
+
1994
+ eager: bool = False,
1995
+ ) -> InjectorBindingOrBindings:
1996
+ return InjectorBinder.bind(
1997
+ obj,
1998
+
1999
+ key=key,
2000
+ tag=tag,
2001
+ array=array,
2002
+
2003
+ to_fn=to_fn,
2004
+ to_ctor=to_ctor,
2005
+ to_const=to_const,
2006
+ to_key=to_key,
2007
+
2008
+ singleton=singleton,
779
2009
 
2010
+ eager=eager,
2011
+ )
780
2012
 
781
- def get_payload_src(*, file: ta.Optional[str]) -> str:
782
- if file is not None:
783
- with open(file) as f:
784
- return f.read()
2013
+ # helpers
785
2014
 
786
- if _is_self_amalg():
787
- return _get_self_src()
2015
+ @classmethod
2016
+ def bind_factory(
2017
+ cls,
2018
+ fn: ta.Callable[..., T],
2019
+ cls_: U,
2020
+ ann: ta.Any = None,
2021
+ ) -> InjectorBindingOrBindings:
2022
+ return cls.bind(make_injector_factory(fn, cls_, ann))
788
2023
 
789
- import importlib.resources
790
- return importlib.resources.files(__package__.split('.')[0] + '.scripts').joinpath('manage.py').read_text()
2024
+ @classmethod
2025
+ def bind_array(
2026
+ cls,
2027
+ obj: ta.Any = None,
2028
+ *,
2029
+ tag: ta.Any = None,
2030
+ ) -> InjectorBindingOrBindings:
2031
+ return bind_injector_array(obj, tag=tag)
2032
+
2033
+ @classmethod
2034
+ def bind_array_type(
2035
+ cls,
2036
+ ele: ta.Union[InjectorKey, InjectorKeyCls],
2037
+ cls_: U,
2038
+ ann: ta.Any = None,
2039
+ ) -> InjectorBindingOrBindings:
2040
+ return cls.bind(make_injector_array_type(ele, cls_, ann))
2041
+
2042
+
2043
+ inj = Injection
791
2044
 
792
2045
 
793
2046
  ########################################
@@ -1072,21 +2325,26 @@ TODO:
1072
2325
  ##
1073
2326
 
1074
2327
 
2328
+ @dc.dataclass(frozen=True)
2329
+ class ObjMarshalOptions:
2330
+ raw_bytes: bool = False
2331
+
2332
+
1075
2333
  class ObjMarshaler(abc.ABC):
1076
2334
  @abc.abstractmethod
1077
- def marshal(self, o: ta.Any) -> ta.Any:
2335
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1078
2336
  raise NotImplementedError
1079
2337
 
1080
2338
  @abc.abstractmethod
1081
- def unmarshal(self, o: ta.Any) -> ta.Any:
2339
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1082
2340
  raise NotImplementedError
1083
2341
 
1084
2342
 
1085
2343
  class NopObjMarshaler(ObjMarshaler):
1086
- def marshal(self, o: ta.Any) -> ta.Any:
2344
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1087
2345
  return o
1088
2346
 
1089
- def unmarshal(self, o: ta.Any) -> ta.Any:
2347
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1090
2348
  return o
1091
2349
 
1092
2350
 
@@ -1094,29 +2352,29 @@ class NopObjMarshaler(ObjMarshaler):
1094
2352
  class ProxyObjMarshaler(ObjMarshaler):
1095
2353
  m: ta.Optional[ObjMarshaler] = None
1096
2354
 
1097
- def marshal(self, o: ta.Any) -> ta.Any:
1098
- return check_not_none(self.m).marshal(o)
2355
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2356
+ return check_not_none(self.m).marshal(o, opts)
1099
2357
 
1100
- def unmarshal(self, o: ta.Any) -> ta.Any:
1101
- return check_not_none(self.m).unmarshal(o)
2358
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2359
+ return check_not_none(self.m).unmarshal(o, opts)
1102
2360
 
1103
2361
 
1104
2362
  @dc.dataclass(frozen=True)
1105
2363
  class CastObjMarshaler(ObjMarshaler):
1106
2364
  ty: type
1107
2365
 
1108
- def marshal(self, o: ta.Any) -> ta.Any:
2366
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1109
2367
  return o
1110
2368
 
1111
- def unmarshal(self, o: ta.Any) -> ta.Any:
2369
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1112
2370
  return self.ty(o)
1113
2371
 
1114
2372
 
1115
2373
  class DynamicObjMarshaler(ObjMarshaler):
1116
- def marshal(self, o: ta.Any) -> ta.Any:
2374
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1117
2375
  return marshal_obj(o)
1118
2376
 
1119
- def unmarshal(self, o: ta.Any) -> ta.Any:
2377
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1120
2378
  return o
1121
2379
 
1122
2380
 
@@ -1124,21 +2382,36 @@ class DynamicObjMarshaler(ObjMarshaler):
1124
2382
  class Base64ObjMarshaler(ObjMarshaler):
1125
2383
  ty: type
1126
2384
 
1127
- def marshal(self, o: ta.Any) -> ta.Any:
2385
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1128
2386
  return base64.b64encode(o).decode('ascii')
1129
2387
 
1130
- def unmarshal(self, o: ta.Any) -> ta.Any:
2388
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1131
2389
  return self.ty(base64.b64decode(o))
1132
2390
 
1133
2391
 
2392
+ @dc.dataclass(frozen=True)
2393
+ class BytesSwitchedObjMarshaler(ObjMarshaler):
2394
+ m: ObjMarshaler
2395
+
2396
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2397
+ if opts.raw_bytes:
2398
+ return o
2399
+ return self.m.marshal(o, opts)
2400
+
2401
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2402
+ if opts.raw_bytes:
2403
+ return o
2404
+ return self.m.unmarshal(o, opts)
2405
+
2406
+
1134
2407
  @dc.dataclass(frozen=True)
1135
2408
  class EnumObjMarshaler(ObjMarshaler):
1136
2409
  ty: type
1137
2410
 
1138
- def marshal(self, o: ta.Any) -> ta.Any:
2411
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1139
2412
  return o.name
1140
2413
 
1141
- def unmarshal(self, o: ta.Any) -> ta.Any:
2414
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1142
2415
  return self.ty.__members__[o] # type: ignore
1143
2416
 
1144
2417
 
@@ -1146,15 +2419,15 @@ class EnumObjMarshaler(ObjMarshaler):
1146
2419
  class OptionalObjMarshaler(ObjMarshaler):
1147
2420
  item: ObjMarshaler
1148
2421
 
1149
- def marshal(self, o: ta.Any) -> ta.Any:
2422
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1150
2423
  if o is None:
1151
2424
  return None
1152
- return self.item.marshal(o)
2425
+ return self.item.marshal(o, opts)
1153
2426
 
1154
- def unmarshal(self, o: ta.Any) -> ta.Any:
2427
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1155
2428
  if o is None:
1156
2429
  return None
1157
- return self.item.unmarshal(o)
2430
+ return self.item.unmarshal(o, opts)
1158
2431
 
1159
2432
 
1160
2433
  @dc.dataclass(frozen=True)
@@ -1163,11 +2436,11 @@ class MappingObjMarshaler(ObjMarshaler):
1163
2436
  km: ObjMarshaler
1164
2437
  vm: ObjMarshaler
1165
2438
 
1166
- def marshal(self, o: ta.Any) -> ta.Any:
1167
- return {self.km.marshal(k): self.vm.marshal(v) for k, v in o.items()}
2439
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2440
+ return {self.km.marshal(k, opts): self.vm.marshal(v, opts) for k, v in o.items()}
1168
2441
 
1169
- def unmarshal(self, o: ta.Any) -> ta.Any:
1170
- return self.ty((self.km.unmarshal(k), self.vm.unmarshal(v)) for k, v in o.items())
2442
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2443
+ return self.ty((self.km.unmarshal(k, opts), self.vm.unmarshal(v, opts)) for k, v in o.items())
1171
2444
 
1172
2445
 
1173
2446
  @dc.dataclass(frozen=True)
@@ -1175,11 +2448,11 @@ class IterableObjMarshaler(ObjMarshaler):
1175
2448
  ty: type
1176
2449
  item: ObjMarshaler
1177
2450
 
1178
- def marshal(self, o: ta.Any) -> ta.Any:
1179
- return [self.item.marshal(e) for e in o]
2451
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2452
+ return [self.item.marshal(e, opts) for e in o]
1180
2453
 
1181
- def unmarshal(self, o: ta.Any) -> ta.Any:
1182
- return self.ty(self.item.unmarshal(e) for e in o)
2454
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2455
+ return self.ty(self.item.unmarshal(e, opts) for e in o)
1183
2456
 
1184
2457
 
1185
2458
  @dc.dataclass(frozen=True)
@@ -1188,11 +2461,11 @@ class DataclassObjMarshaler(ObjMarshaler):
1188
2461
  fs: ta.Mapping[str, ObjMarshaler]
1189
2462
  nonstrict: bool = False
1190
2463
 
1191
- def marshal(self, o: ta.Any) -> ta.Any:
1192
- return {k: m.marshal(getattr(o, k)) for k, m in self.fs.items()}
2464
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2465
+ return {k: m.marshal(getattr(o, k), opts) for k, m in self.fs.items()}
1193
2466
 
1194
- def unmarshal(self, o: ta.Any) -> ta.Any:
1195
- return self.ty(**{k: self.fs[k].unmarshal(v) for k, v in o.items() if not self.nonstrict or k in self.fs})
2467
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
2468
+ return self.ty(**{k: self.fs[k].unmarshal(v, opts) for k, v in o.items() if not self.nonstrict or k in self.fs})
1196
2469
 
1197
2470
 
1198
2471
  @dc.dataclass(frozen=True)
@@ -1212,50 +2485,50 @@ class PolymorphicObjMarshaler(ObjMarshaler):
1212
2485
  {i.tag: i for i in impls},
1213
2486
  )
1214
2487
 
1215
- def marshal(self, o: ta.Any) -> ta.Any:
2488
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1216
2489
  impl = self.impls_by_ty[type(o)]
1217
- return {impl.tag: impl.m.marshal(o)}
2490
+ return {impl.tag: impl.m.marshal(o, opts)}
1218
2491
 
1219
- def unmarshal(self, o: ta.Any) -> ta.Any:
2492
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1220
2493
  [(t, v)] = o.items()
1221
2494
  impl = self.impls_by_tag[t]
1222
- return impl.m.unmarshal(v)
2495
+ return impl.m.unmarshal(v, opts)
1223
2496
 
1224
2497
 
1225
2498
  @dc.dataclass(frozen=True)
1226
2499
  class DatetimeObjMarshaler(ObjMarshaler):
1227
2500
  ty: type
1228
2501
 
1229
- def marshal(self, o: ta.Any) -> ta.Any:
2502
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1230
2503
  return o.isoformat()
1231
2504
 
1232
- def unmarshal(self, o: ta.Any) -> ta.Any:
2505
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1233
2506
  return self.ty.fromisoformat(o) # type: ignore
1234
2507
 
1235
2508
 
1236
2509
  class DecimalObjMarshaler(ObjMarshaler):
1237
- def marshal(self, o: ta.Any) -> ta.Any:
2510
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1238
2511
  return str(check_isinstance(o, decimal.Decimal))
1239
2512
 
1240
- def unmarshal(self, v: ta.Any) -> ta.Any:
2513
+ def unmarshal(self, v: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1241
2514
  return decimal.Decimal(check_isinstance(v, str))
1242
2515
 
1243
2516
 
1244
2517
  class FractionObjMarshaler(ObjMarshaler):
1245
- def marshal(self, o: ta.Any) -> ta.Any:
2518
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1246
2519
  fr = check_isinstance(o, fractions.Fraction)
1247
2520
  return [fr.numerator, fr.denominator]
1248
2521
 
1249
- def unmarshal(self, v: ta.Any) -> ta.Any:
2522
+ def unmarshal(self, v: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1250
2523
  num, denom = check_isinstance(v, list)
1251
2524
  return fractions.Fraction(num, denom)
1252
2525
 
1253
2526
 
1254
2527
  class UuidObjMarshaler(ObjMarshaler):
1255
- def marshal(self, o: ta.Any) -> ta.Any:
2528
+ def marshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1256
2529
  return str(o)
1257
2530
 
1258
- def unmarshal(self, o: ta.Any) -> ta.Any:
2531
+ def unmarshal(self, o: ta.Any, opts: ObjMarshalOptions) -> ta.Any:
1259
2532
  return uuid.UUID(o)
1260
2533
 
1261
2534
 
@@ -1265,7 +2538,7 @@ class UuidObjMarshaler(ObjMarshaler):
1265
2538
  _DEFAULT_OBJ_MARSHALERS: ta.Dict[ta.Any, ObjMarshaler] = {
1266
2539
  **{t: NopObjMarshaler() for t in (type(None),)},
1267
2540
  **{t: CastObjMarshaler(t) for t in (int, float, str, bool)},
1268
- **{t: Base64ObjMarshaler(t) for t in (bytes, bytearray)},
2541
+ **{t: BytesSwitchedObjMarshaler(Base64ObjMarshaler(t)) for t in (bytes, bytearray)},
1269
2542
  **{t: IterableObjMarshaler(t, DynamicObjMarshaler()) for t in (list, tuple, set, frozenset)},
1270
2543
  **{t: MappingObjMarshaler(t, DynamicObjMarshaler(), DynamicObjMarshaler()) for t in (dict,)},
1271
2544
 
@@ -1298,12 +2571,16 @@ class ObjMarshalerManager:
1298
2571
  def __init__(
1299
2572
  self,
1300
2573
  *,
2574
+ default_options: ObjMarshalOptions = ObjMarshalOptions(),
2575
+
1301
2576
  default_obj_marshalers: ta.Dict[ta.Any, ObjMarshaler] = _DEFAULT_OBJ_MARSHALERS, # noqa
1302
2577
  generic_mapping_types: ta.Dict[ta.Any, type] = _OBJ_MARSHALER_GENERIC_MAPPING_TYPES, # noqa
1303
2578
  generic_iterable_types: ta.Dict[ta.Any, type] = _OBJ_MARSHALER_GENERIC_ITERABLE_TYPES, # noqa
1304
2579
  ) -> None:
1305
2580
  super().__init__()
1306
2581
 
2582
+ self._default_options = default_options
2583
+
1307
2584
  self._obj_marshalers = dict(default_obj_marshalers)
1308
2585
  self._generic_mapping_types = generic_mapping_types
1309
2586
  self._generic_iterable_types = generic_iterable_types
@@ -1412,11 +2689,35 @@ class ObjMarshalerManager:
1412
2689
 
1413
2690
  #
1414
2691
 
1415
- def marshal_obj(self, o: ta.Any, ty: ta.Any = None) -> ta.Any:
1416
- return self.get_obj_marshaler(ty if ty is not None else type(o)).marshal(o)
1417
-
1418
- def unmarshal_obj(self, o: ta.Any, ty: ta.Union[ta.Type[T], ta.Any]) -> T:
1419
- return self.get_obj_marshaler(ty).unmarshal(o)
2692
+ def marshal_obj(
2693
+ self,
2694
+ o: ta.Any,
2695
+ ty: ta.Any = None,
2696
+ opts: ta.Optional[ObjMarshalOptions] = None,
2697
+ ) -> ta.Any:
2698
+ m = self.get_obj_marshaler(ty if ty is not None else type(o))
2699
+ return m.marshal(o, opts or self._default_options)
2700
+
2701
+ def unmarshal_obj(
2702
+ self,
2703
+ o: ta.Any,
2704
+ ty: ta.Union[ta.Type[T], ta.Any],
2705
+ opts: ta.Optional[ObjMarshalOptions] = None,
2706
+ ) -> T:
2707
+ m = self.get_obj_marshaler(ty)
2708
+ return m.unmarshal(o, opts or self._default_options)
2709
+
2710
+ def roundtrip_obj(
2711
+ self,
2712
+ o: ta.Any,
2713
+ ty: ta.Any = None,
2714
+ opts: ta.Optional[ObjMarshalOptions] = None,
2715
+ ) -> ta.Any:
2716
+ if ty is None:
2717
+ ty = type(o)
2718
+ m: ta.Any = self.marshal_obj(o, ty, opts)
2719
+ u: ta.Any = self.unmarshal_obj(m, ty, opts)
2720
+ return u
1420
2721
 
1421
2722
 
1422
2723
  ##
@@ -1449,10 +2750,80 @@ def check_runtime_version() -> None:
1449
2750
 
1450
2751
 
1451
2752
  ########################################
1452
- # ../protocol.py
2753
+ # ../bootstrap.py
2754
+
2755
+
2756
+ @dc.dataclass(frozen=True)
2757
+ class MainBootstrap:
2758
+ main_config: MainConfig = MainConfig()
2759
+
2760
+ remote_config: RemoteConfig = RemoteConfig()
2761
+
2762
+
2763
+ ########################################
2764
+ # ../commands/execution.py
2765
+
2766
+
2767
+ CommandExecutorMap = ta.NewType('CommandExecutorMap', ta.Mapping[ta.Type[Command], CommandExecutor])
2768
+
2769
+
2770
+ class CommandExecutionService(CommandExecutor):
2771
+ def __init__(
2772
+ self,
2773
+ *,
2774
+ command_executors: CommandExecutorMap,
2775
+ ) -> None:
2776
+ super().__init__()
2777
+
2778
+ self._command_executors = command_executors
2779
+
2780
+ def execute(self, cmd: Command) -> Command.Output:
2781
+ ce: CommandExecutor = self._command_executors[type(cmd)]
2782
+ return ce.execute(cmd)
2783
+
2784
+
2785
+ ########################################
2786
+ # ../commands/marshal.py
2787
+
2788
+
2789
+ def install_command_marshaling(
2790
+ cmds: CommandNameMap,
2791
+ msh: ObjMarshalerManager,
2792
+ ) -> None:
2793
+ for fn in [
2794
+ lambda c: c,
2795
+ lambda c: c.Output,
2796
+ ]:
2797
+ msh.register_opj_marshaler(
2798
+ fn(Command),
2799
+ PolymorphicObjMarshaler.of([
2800
+ PolymorphicObjMarshaler.Impl(
2801
+ fn(cmd),
2802
+ name,
2803
+ msh.get_obj_marshaler(fn(cmd)),
2804
+ )
2805
+ for name, cmd in cmds.items()
2806
+ ]),
2807
+ )
2808
+
2809
+
2810
+ ########################################
2811
+ # ../marshal.py
2812
+
2813
+
2814
+ @dc.dataclass(frozen=True)
2815
+ class ObjMarshalerInstaller:
2816
+ fn: ta.Callable[[ObjMarshalerManager], None]
2817
+
2818
+
2819
+ ObjMarshalerInstallers = ta.NewType('ObjMarshalerInstallers', ta.Sequence[ObjMarshalerInstaller])
2820
+
2821
+
2822
+ ########################################
2823
+ # ../remote/channel.py
1453
2824
 
1454
2825
 
1455
- class Channel:
2826
+ class RemoteChannel:
1456
2827
  def __init__(
1457
2828
  self,
1458
2829
  input: ta.IO, # noqa
@@ -1466,6 +2837,9 @@ class Channel:
1466
2837
  self._output = output
1467
2838
  self._msh = msh
1468
2839
 
2840
+ def set_marshaler(self, msh: ObjMarshalerManager) -> None:
2841
+ self._msh = msh
2842
+
1469
2843
  def send_obj(self, o: ta.Any, ty: ta.Any = None) -> None:
1470
2844
  j = json_dumps_compact(self._msh.marshal_obj(o, ty))
1471
2845
  d = j.encode('utf-8')
@@ -1474,7 +2848,7 @@ class Channel:
1474
2848
  self._output.write(d)
1475
2849
  self._output.flush()
1476
2850
 
1477
- def recv_obj(self, ty: ta.Any) -> ta.Any:
2851
+ def recv_obj(self, ty: ta.Type[T]) -> ta.Optional[T]:
1478
2852
  d = self._input.read(4)
1479
2853
  if not d:
1480
2854
  return None
@@ -1695,28 +3069,19 @@ class SubprocessCommandExecutor(CommandExecutor[SubprocessCommand, SubprocessCom
1695
3069
 
1696
3070
 
1697
3071
  ########################################
1698
- # ../spawning.py
3072
+ # ../remote/spawning.py
1699
3073
 
1700
3074
 
1701
- class PySpawner:
1702
- DEFAULT_PYTHON = 'python3'
3075
+ class RemoteSpawning:
3076
+ @dc.dataclass(frozen=True)
3077
+ class Target:
3078
+ shell: ta.Optional[str] = None
3079
+ shell_quote: bool = False
1703
3080
 
1704
- def __init__(
1705
- self,
1706
- src: str,
1707
- *,
1708
- shell: ta.Optional[str] = None,
1709
- shell_quote: bool = False,
1710
- python: str = DEFAULT_PYTHON,
1711
- stderr: ta.Optional[SubprocessChannelOption] = None,
1712
- ) -> None:
1713
- super().__init__()
3081
+ DEFAULT_PYTHON: ta.ClassVar[str] = 'python3'
3082
+ python: str = DEFAULT_PYTHON
1714
3083
 
1715
- self._src = src
1716
- self._shell = shell
1717
- self._shell_quote = shell_quote
1718
- self._python = python
1719
- self._stderr = stderr
3084
+ stderr: ta.Optional[str] = None # SubprocessChannelOption
1720
3085
 
1721
3086
  #
1722
3087
 
@@ -1724,20 +3089,24 @@ class PySpawner:
1724
3089
  cmd: ta.Sequence[str]
1725
3090
  shell: bool
1726
3091
 
1727
- def _prepare_cmd(self) -> _PreparedCmd:
1728
- if self._shell is not None:
1729
- sh_src = f'{self._python} -c {shlex.quote(self._src)}'
1730
- if self._shell_quote:
3092
+ def _prepare_cmd(
3093
+ self,
3094
+ tgt: Target,
3095
+ src: str,
3096
+ ) -> _PreparedCmd:
3097
+ if tgt.shell is not None:
3098
+ sh_src = f'{tgt.python} -c {shlex.quote(src)}'
3099
+ if tgt.shell_quote:
1731
3100
  sh_src = shlex.quote(sh_src)
1732
- sh_cmd = f'{self._shell} {sh_src}'
1733
- return PySpawner._PreparedCmd(
3101
+ sh_cmd = f'{tgt.shell} {sh_src}'
3102
+ return RemoteSpawning._PreparedCmd(
1734
3103
  cmd=[sh_cmd],
1735
3104
  shell=True,
1736
3105
  )
1737
3106
 
1738
3107
  else:
1739
- return PySpawner._PreparedCmd(
1740
- cmd=[self._python, '-c', self._src],
3108
+ return RemoteSpawning._PreparedCmd(
3109
+ cmd=[tgt.python, '-c', src],
1741
3110
  shell=False,
1742
3111
  )
1743
3112
 
@@ -1752,23 +3121,28 @@ class PySpawner:
1752
3121
  @contextlib.contextmanager
1753
3122
  def spawn(
1754
3123
  self,
3124
+ tgt: Target,
3125
+ src: str,
1755
3126
  *,
1756
3127
  timeout: ta.Optional[float] = None,
1757
3128
  ) -> ta.Generator[Spawned, None, None]:
1758
- pc = self._prepare_cmd()
3129
+ pc = self._prepare_cmd(tgt, src)
1759
3130
 
1760
3131
  with subprocess.Popen(
1761
3132
  subprocess_maybe_shell_wrap_exec(*pc.cmd),
1762
3133
  shell=pc.shell,
1763
3134
  stdin=subprocess.PIPE,
1764
3135
  stdout=subprocess.PIPE,
1765
- stderr=SUBPROCESS_CHANNEL_OPTION_VALUES[self._stderr] if self._stderr is not None else None,
3136
+ stderr=(
3137
+ SUBPROCESS_CHANNEL_OPTION_VALUES[ta.cast(SubprocessChannelOption, tgt.stderr)]
3138
+ if tgt.stderr is not None else None
3139
+ ),
1766
3140
  ) as proc:
1767
3141
  stdin = check_not_none(proc.stdin)
1768
3142
  stdout = check_not_none(proc.stdout)
1769
3143
 
1770
3144
  try:
1771
- yield PySpawner.Spawned(
3145
+ yield RemoteSpawning.Spawned(
1772
3146
  stdin=stdin,
1773
3147
  stdout=stdout,
1774
3148
  stderr=proc.stderr,
@@ -1784,59 +3158,356 @@ class PySpawner:
1784
3158
 
1785
3159
 
1786
3160
  ########################################
1787
- # main.py
3161
+ # ../commands/inject.py
1788
3162
 
1789
3163
 
1790
3164
  ##
1791
3165
 
1792
3166
 
1793
- _COMMAND_TYPES = {
1794
- 'subprocess': SubprocessCommand,
1795
- }
3167
+ def bind_command(
3168
+ command_cls: ta.Type[Command],
3169
+ executor_cls: ta.Optional[ta.Type[CommandExecutor]],
3170
+ ) -> InjectorBindings:
3171
+ lst: ta.List[InjectorBindingOrBindings] = [
3172
+ inj.bind(CommandRegistration(command_cls), array=True),
3173
+ ]
3174
+
3175
+ if executor_cls is not None:
3176
+ lst.extend([
3177
+ inj.bind(executor_cls, singleton=True),
3178
+ inj.bind(CommandExecutorRegistration(command_cls, executor_cls), array=True),
3179
+ ])
3180
+
3181
+ return inj.as_bindings(*lst)
1796
3182
 
1797
3183
 
1798
3184
  ##
1799
3185
 
1800
3186
 
1801
- def register_command_marshaling(msh: ObjMarshalerManager) -> None:
1802
- for fn in [
1803
- lambda c: c,
1804
- lambda c: c.Output,
3187
+ @dc.dataclass(frozen=True)
3188
+ class _FactoryCommandExecutor(CommandExecutor):
3189
+ factory: ta.Callable[[], CommandExecutor]
3190
+
3191
+ def execute(self, i: Command) -> Command.Output:
3192
+ return self.factory().execute(i)
3193
+
3194
+
3195
+ ##
3196
+
3197
+
3198
+ def bind_commands(
3199
+ *,
3200
+ main_config: MainConfig,
3201
+ ) -> InjectorBindings:
3202
+ lst: ta.List[InjectorBindingOrBindings] = [
3203
+ inj.bind_array(CommandRegistration),
3204
+ inj.bind_array_type(CommandRegistration, CommandRegistrations),
3205
+
3206
+ inj.bind_array(CommandExecutorRegistration),
3207
+ inj.bind_array_type(CommandExecutorRegistration, CommandExecutorRegistrations),
3208
+
3209
+ inj.bind(build_command_name_map, singleton=True),
3210
+ ]
3211
+
3212
+ #
3213
+
3214
+ def provide_obj_marshaler_installer(cmds: CommandNameMap) -> ObjMarshalerInstaller:
3215
+ return ObjMarshalerInstaller(functools.partial(install_command_marshaling, cmds))
3216
+
3217
+ lst.append(inj.bind(provide_obj_marshaler_installer, array=True))
3218
+
3219
+ #
3220
+
3221
+ def provide_command_executor_map(
3222
+ injector: Injector,
3223
+ crs: CommandExecutorRegistrations,
3224
+ ) -> CommandExecutorMap:
3225
+ dct: ta.Dict[ta.Type[Command], CommandExecutor] = {}
3226
+
3227
+ cr: CommandExecutorRegistration
3228
+ for cr in crs:
3229
+ if cr.command_cls in dct:
3230
+ raise KeyError(cr.command_cls)
3231
+
3232
+ factory = functools.partial(injector.provide, cr.executor_cls)
3233
+ if main_config.debug:
3234
+ ce = factory()
3235
+ else:
3236
+ ce = _FactoryCommandExecutor(factory)
3237
+
3238
+ dct[cr.command_cls] = ce
3239
+
3240
+ return CommandExecutorMap(dct)
3241
+
3242
+ lst.extend([
3243
+ inj.bind(provide_command_executor_map, singleton=True),
3244
+
3245
+ inj.bind(CommandExecutionService, singleton=True, eager=main_config.debug),
3246
+ inj.bind(CommandExecutor, to_key=CommandExecutionService),
3247
+ ])
3248
+
3249
+ #
3250
+
3251
+ for command_cls, executor_cls in [
3252
+ (SubprocessCommand, SubprocessCommandExecutor),
1805
3253
  ]:
1806
- msh.register_opj_marshaler(
1807
- fn(Command),
1808
- PolymorphicObjMarshaler.of([
1809
- PolymorphicObjMarshaler.Impl(
1810
- fn(cty),
1811
- k,
1812
- msh.get_obj_marshaler(fn(cty)),
1813
- )
1814
- for k, cty in _COMMAND_TYPES.items()
1815
- ]),
1816
- )
3254
+ lst.append(bind_command(command_cls, executor_cls))
3255
+
3256
+ #
3257
+
3258
+ return inj.as_bindings(*lst)
1817
3259
 
1818
3260
 
1819
- register_command_marshaling(OBJ_MARSHALER_MANAGER)
3261
+ ########################################
3262
+ # ../remote/execution.py
1820
3263
 
1821
3264
 
1822
3265
  ##
1823
3266
 
1824
3267
 
1825
- def _remote_main() -> None:
3268
+ def _remote_execution_main() -> None:
1826
3269
  rt = pyremote_bootstrap_finalize() # noqa
1827
- chan = Channel(rt.input, rt.output)
3270
+
3271
+ chan = RemoteChannel(
3272
+ rt.input,
3273
+ rt.output,
3274
+ )
3275
+
3276
+ bs = check_not_none(chan.recv_obj(MainBootstrap))
3277
+
3278
+ if (prd := bs.remote_config.pycharm_remote_debug) is not None:
3279
+ pycharm_debug_connect(prd)
3280
+
3281
+ injector = main_bootstrap(bs)
3282
+
3283
+ chan.set_marshaler(injector[ObjMarshalerManager])
3284
+
3285
+ ce = injector[CommandExecutor]
1828
3286
 
1829
3287
  while True:
1830
3288
  i = chan.recv_obj(Command)
1831
3289
  if i is None:
1832
3290
  break
1833
3291
 
1834
- if isinstance(i, SubprocessCommand):
1835
- o = SubprocessCommandExecutor().execute(i) # noqa
3292
+ r = ce.try_execute(
3293
+ i,
3294
+ log=log,
3295
+ omit_exc_object=True,
3296
+ )
3297
+
3298
+ chan.send_obj(r)
3299
+
3300
+
3301
+ ##
3302
+
3303
+
3304
+ @dc.dataclass()
3305
+ class RemoteCommandError(Exception):
3306
+ e: CommandException
3307
+
3308
+
3309
+ class RemoteCommandExecutor(CommandExecutor):
3310
+ def __init__(self, chan: RemoteChannel) -> None:
3311
+ super().__init__()
3312
+
3313
+ self._chan = chan
3314
+
3315
+ def _remote_execute(self, cmd: Command) -> CommandOutputOrException:
3316
+ self._chan.send_obj(cmd, Command)
3317
+
3318
+ if (r := self._chan.recv_obj(CommandOutputOrExceptionData)) is None:
3319
+ raise EOFError
3320
+
3321
+ return r
3322
+
3323
+ # @ta.override
3324
+ def execute(self, cmd: Command) -> Command.Output:
3325
+ r = self._remote_execute(cmd)
3326
+ if (e := r.exception) is not None:
3327
+ raise RemoteCommandError(e)
3328
+ else:
3329
+ return check_not_none(r.output)
3330
+
3331
+ # @ta.override
3332
+ def try_execute(
3333
+ self,
3334
+ cmd: Command,
3335
+ *,
3336
+ log: ta.Optional[logging.Logger] = None,
3337
+ omit_exc_object: bool = False,
3338
+ ) -> CommandOutputOrException:
3339
+ try:
3340
+ r = self._remote_execute(cmd)
3341
+
3342
+ except Exception as e: # noqa
3343
+ if log is not None:
3344
+ log.exception('Exception executing remote command: %r', type(cmd))
3345
+
3346
+ return CommandOutputOrExceptionData(exception=CommandException.of(
3347
+ e,
3348
+ omit_exc_object=omit_exc_object,
3349
+ cmd=cmd,
3350
+ ))
3351
+
1836
3352
  else:
1837
- raise TypeError(i)
3353
+ return r
3354
+
3355
+
3356
+ ##
3357
+
3358
+
3359
+ class RemoteExecution:
3360
+ def __init__(
3361
+ self,
3362
+ *,
3363
+ spawning: RemoteSpawning,
3364
+ msh: ObjMarshalerManager,
3365
+ payload_file: ta.Optional[RemoteExecutionPayloadFile] = None,
3366
+ ) -> None:
3367
+ super().__init__()
3368
+
3369
+ self._spawning = spawning
3370
+ self._msh = msh
3371
+ self._payload_file = payload_file
3372
+
3373
+ #
3374
+
3375
+ @cached_nullary
3376
+ def _payload_src(self) -> str:
3377
+ return get_remote_payload_src(file=self._payload_file)
3378
+
3379
+ @cached_nullary
3380
+ def _remote_src(self) -> ta.Sequence[str]:
3381
+ return [
3382
+ self._payload_src(),
3383
+ '_remote_execution_main()',
3384
+ ]
3385
+
3386
+ @cached_nullary
3387
+ def _spawn_src(self) -> str:
3388
+ return pyremote_build_bootstrap_cmd(__package__ or 'manage')
3389
+
3390
+ #
3391
+
3392
+ @contextlib.contextmanager
3393
+ def connect(
3394
+ self,
3395
+ tgt: RemoteSpawning.Target,
3396
+ bs: MainBootstrap,
3397
+ ) -> ta.Generator[RemoteCommandExecutor, None, None]:
3398
+ spawn_src = self._spawn_src()
3399
+ remote_src = self._remote_src()
3400
+
3401
+ with self._spawning.spawn(
3402
+ tgt,
3403
+ spawn_src,
3404
+ ) as proc:
3405
+ res = PyremoteBootstrapDriver( # noqa
3406
+ remote_src,
3407
+ PyremoteBootstrapOptions(
3408
+ debug=bs.main_config.debug,
3409
+ ),
3410
+ ).run(
3411
+ proc.stdout,
3412
+ proc.stdin,
3413
+ )
3414
+
3415
+ chan = RemoteChannel(
3416
+ proc.stdout,
3417
+ proc.stdin,
3418
+ msh=self._msh,
3419
+ )
3420
+
3421
+ chan.send_obj(bs)
3422
+
3423
+ yield RemoteCommandExecutor(chan)
3424
+
3425
+
3426
+ ########################################
3427
+ # ../remote/inject.py
3428
+
3429
+
3430
+ def bind_remote(
3431
+ *,
3432
+ remote_config: RemoteConfig,
3433
+ ) -> InjectorBindings:
3434
+ lst: ta.List[InjectorBindingOrBindings] = [
3435
+ inj.bind(remote_config),
3436
+
3437
+ inj.bind(RemoteSpawning, singleton=True),
3438
+
3439
+ inj.bind(RemoteExecution, singleton=True),
3440
+ ]
3441
+
3442
+ if (pf := remote_config.payload_file) is not None:
3443
+ lst.append(inj.bind(pf, to_key=RemoteExecutionPayloadFile))
3444
+
3445
+ return inj.as_bindings(*lst)
3446
+
3447
+
3448
+ ########################################
3449
+ # ../inject.py
3450
+
3451
+
3452
+ ##
3453
+
3454
+
3455
+ def bind_main(
3456
+ *,
3457
+ main_config: MainConfig,
3458
+ remote_config: RemoteConfig,
3459
+ ) -> InjectorBindings:
3460
+ lst: ta.List[InjectorBindingOrBindings] = [
3461
+ inj.bind(main_config),
3462
+
3463
+ bind_commands(
3464
+ main_config=main_config,
3465
+ ),
3466
+
3467
+ bind_remote(
3468
+ remote_config=remote_config,
3469
+ ),
3470
+ ]
3471
+
3472
+ #
3473
+
3474
+ def build_obj_marshaler_manager(insts: ObjMarshalerInstallers) -> ObjMarshalerManager:
3475
+ msh = ObjMarshalerManager()
3476
+ inst: ObjMarshalerInstaller
3477
+ for inst in insts:
3478
+ inst.fn(msh)
3479
+ return msh
3480
+
3481
+ lst.extend([
3482
+ inj.bind(build_obj_marshaler_manager, singleton=True),
3483
+
3484
+ inj.bind_array(ObjMarshalerInstaller),
3485
+ inj.bind_array_type(ObjMarshalerInstaller, ObjMarshalerInstallers),
3486
+ ])
3487
+
3488
+ #
3489
+
3490
+ return inj.as_bindings(*lst)
3491
+
3492
+
3493
+ ########################################
3494
+ # ../bootstrap_.py
1838
3495
 
1839
- chan.send_obj(o, Command.Output)
3496
+
3497
+ def main_bootstrap(bs: MainBootstrap) -> Injector:
3498
+ if (log_level := bs.main_config.log_level) is not None:
3499
+ configure_standard_logging(log_level)
3500
+
3501
+ injector = inj.create_injector(bind_main( # noqa
3502
+ main_config=bs.main_config,
3503
+ remote_config=bs.remote_config,
3504
+ ))
3505
+
3506
+ return injector
3507
+
3508
+
3509
+ ########################################
3510
+ # main.py
1840
3511
 
1841
3512
 
1842
3513
  ##
@@ -1853,50 +3524,64 @@ def _main() -> None:
1853
3524
  parser.add_argument('-q', '--shell-quote', action='store_true')
1854
3525
  parser.add_argument('--python', default='python3')
1855
3526
 
3527
+ parser.add_argument('--pycharm-debug-port', type=int)
3528
+ parser.add_argument('--pycharm-debug-host')
3529
+ parser.add_argument('--pycharm-debug-version')
3530
+
1856
3531
  parser.add_argument('--debug', action='store_true')
1857
3532
 
3533
+ parser.add_argument('command', nargs='+')
3534
+
1858
3535
  args = parser.parse_args()
1859
3536
 
1860
3537
  #
1861
3538
 
1862
- payload_src = get_payload_src(file=args._payload_file) # noqa
3539
+ bs = MainBootstrap(
3540
+ main_config=MainConfig(
3541
+ log_level='DEBUG' if args.debug else 'INFO',
1863
3542
 
1864
- remote_src = '\n\n'.join([
1865
- '__name__ = "__remote__"',
1866
- payload_src,
1867
- '_remote_main()',
1868
- ])
3543
+ debug=bool(args.debug),
3544
+ ),
3545
+
3546
+ remote_config=RemoteConfig(
3547
+ payload_file=args._payload_file, # noqa
3548
+
3549
+ pycharm_remote_debug=PycharmRemoteDebug(
3550
+ port=args.pycharm_debug_port,
3551
+ host=args.pycharm_debug_host,
3552
+ install_version=args.pycharm_debug_version,
3553
+ ) if args.pycharm_debug_port is not None else None,
3554
+ ),
3555
+ )
3556
+
3557
+ injector = main_bootstrap(
3558
+ bs,
3559
+ )
3560
+
3561
+ #
3562
+
3563
+ cmds = [
3564
+ SubprocessCommand([c])
3565
+ for c in args.command
3566
+ ]
1869
3567
 
1870
3568
  #
1871
3569
 
1872
- spawner = PySpawner(
1873
- pyremote_build_bootstrap_cmd(__package__ or 'manage'),
3570
+ tgt = RemoteSpawning.Target(
1874
3571
  shell=args.shell,
1875
3572
  shell_quote=args.shell_quote,
1876
3573
  python=args.python,
1877
3574
  )
1878
3575
 
1879
- with spawner.spawn() as proc:
1880
- res = PyremoteBootstrapDriver( # noqa
1881
- remote_src,
1882
- PyremoteBootstrapOptions(
1883
- debug=args.debug,
1884
- ),
1885
- ).run(proc.stdout, proc.stdin)
1886
-
1887
- chan = Channel(proc.stdout, proc.stdin)
1888
-
1889
- #
3576
+ with injector[RemoteExecution].connect(tgt, bs) as rce:
3577
+ for cmd in cmds:
3578
+ r = rce.try_execute(cmd)
1890
3579
 
1891
- for ci in [
1892
- SubprocessCommand(['python3', '-'], input=b'print(1)\n'),
1893
- SubprocessCommand(['uname']),
1894
- ]:
1895
- chan.send_obj(ci, Command)
3580
+ print(injector[ObjMarshalerManager].marshal_obj(r, opts=ObjMarshalOptions(raw_bytes=True)))
1896
3581
 
1897
- o = chan.recv_obj(Command.Output)
3582
+ #
1898
3583
 
1899
- print(o)
3584
+ print('Success')
1900
3585
 
1901
3586
 
1902
3587
  if __name__ == '__main__':