omdev 0.0.0.dev178__py3-none-any.whl → 0.0.0.dev179__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.
omdev/scripts/interp.py CHANGED
@@ -17,6 +17,7 @@ import asyncio.base_subprocess
17
17
  import asyncio.subprocess
18
18
  import collections
19
19
  import contextlib
20
+ import contextvars
20
21
  import dataclasses as dc
21
22
  import datetime
22
23
  import functools
@@ -35,6 +36,7 @@ import threading
35
36
  import time
36
37
  import types
37
38
  import typing as ta
39
+ import weakref
38
40
 
39
41
 
40
42
  ########################################
@@ -75,6 +77,16 @@ UnparsedVersion = ta.Union['Version', str]
75
77
  UnparsedVersionVar = ta.TypeVar('UnparsedVersionVar', bound=UnparsedVersion)
76
78
  CallableVersionOperator = ta.Callable[['Version', str], bool]
77
79
 
80
+ # ../../omlish/argparse/cli.py
81
+ ArgparseCommandFn = ta.Callable[[], ta.Optional[int]] # ta.TypeAlias
82
+
83
+ # ../../omlish/lite/inject.py
84
+ U = ta.TypeVar('U')
85
+ InjectorKeyCls = ta.Union[type, ta.NewType]
86
+ InjectorProviderFn = ta.Callable[['Injector'], ta.Any]
87
+ InjectorProviderFnMap = ta.Mapping['InjectorKey', 'InjectorProviderFn']
88
+ InjectorBindingOrBindings = ta.Union['InjectorBinding', 'InjectorBindings']
89
+
78
90
  # ../../omlish/subprocesses.py
79
91
  SubprocessChannelOption = ta.Literal['pipe', 'stdout', 'devnull'] # ta.TypeAlias
80
92
 
@@ -1045,6 +1057,50 @@ json_dumps_compact: ta.Callable[..., str] = functools.partial(json.dumps, **JSON
1045
1057
  log = logging.getLogger(__name__)
1046
1058
 
1047
1059
 
1060
+ ########################################
1061
+ # ../../../omlish/lite/maybes.py
1062
+
1063
+
1064
+ class Maybe(ta.Generic[T]):
1065
+ @property
1066
+ @abc.abstractmethod
1067
+ def present(self) -> bool:
1068
+ raise NotImplementedError
1069
+
1070
+ @abc.abstractmethod
1071
+ def must(self) -> T:
1072
+ raise NotImplementedError
1073
+
1074
+ @classmethod
1075
+ def just(cls, v: T) -> 'Maybe[T]':
1076
+ return tuple.__new__(_Maybe, (v,)) # noqa
1077
+
1078
+ _empty: ta.ClassVar['Maybe']
1079
+
1080
+ @classmethod
1081
+ def empty(cls) -> 'Maybe[T]':
1082
+ return Maybe._empty
1083
+
1084
+
1085
+ class _Maybe(Maybe[T], tuple):
1086
+ __slots__ = ()
1087
+
1088
+ def __init_subclass__(cls, **kwargs):
1089
+ raise TypeError
1090
+
1091
+ @property
1092
+ def present(self) -> bool:
1093
+ return bool(self)
1094
+
1095
+ def must(self) -> T:
1096
+ if not self:
1097
+ raise ValueError
1098
+ return self[0]
1099
+
1100
+
1101
+ Maybe._empty = tuple.__new__(_Maybe, ()) # noqa
1102
+
1103
+
1048
1104
  ########################################
1049
1105
  # ../../../omlish/lite/reflect.py
1050
1106
 
@@ -1864,267 +1920,1637 @@ class SpecifierSet(BaseSpecifier):
1864
1920
 
1865
1921
 
1866
1922
  ########################################
1867
- # ../../../omlish/lite/runtime.py
1923
+ # ../../../omlish/argparse/cli.py
1924
+ """
1925
+ TODO:
1926
+ - default command
1927
+ - auto match all underscores to hyphens
1928
+ - pre-run, post-run hooks
1929
+ - exitstack?
1930
+ """
1868
1931
 
1869
1932
 
1870
- @cached_nullary
1871
- def is_debugger_attached() -> bool:
1872
- return any(frame[1].endswith('pydevd.py') for frame in inspect.stack())
1933
+ ##
1873
1934
 
1874
1935
 
1875
- LITE_REQUIRED_PYTHON_VERSION = (3, 8)
1936
+ @dc.dataclass(eq=False)
1937
+ class ArgparseArg:
1938
+ args: ta.Sequence[ta.Any]
1939
+ kwargs: ta.Mapping[str, ta.Any]
1940
+ dest: ta.Optional[str] = None
1876
1941
 
1942
+ def __get__(self, instance, owner=None):
1943
+ if instance is None:
1944
+ return self
1945
+ return getattr(instance.args, self.dest) # type: ignore
1877
1946
 
1878
- def check_lite_runtime_version() -> None:
1879
- if sys.version_info < LITE_REQUIRED_PYTHON_VERSION:
1880
- raise OSError(f'Requires python {LITE_REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
1881
1947
 
1948
+ def argparse_arg(*args, **kwargs) -> ArgparseArg:
1949
+ return ArgparseArg(args, kwargs)
1882
1950
 
1883
- ########################################
1884
- # ../../../omlish/logs/json.py
1885
- """
1886
- TODO:
1887
- - translate json keys
1888
- """
1889
1951
 
1952
+ #
1890
1953
 
1891
- class JsonLogFormatter(logging.Formatter):
1892
- KEYS: ta.Mapping[str, bool] = {
1893
- 'name': False,
1894
- 'msg': False,
1895
- 'args': False,
1896
- 'levelname': False,
1897
- 'levelno': False,
1898
- 'pathname': False,
1899
- 'filename': False,
1900
- 'module': False,
1901
- 'exc_info': True,
1902
- 'exc_text': True,
1903
- 'stack_info': True,
1904
- 'lineno': False,
1905
- 'funcName': False,
1906
- 'created': False,
1907
- 'msecs': False,
1908
- 'relativeCreated': False,
1909
- 'thread': False,
1910
- 'threadName': False,
1911
- 'processName': False,
1912
- 'process': False,
1913
- }
1914
1954
 
1915
- def __init__(
1916
- self,
1917
- *args: ta.Any,
1918
- json_dumps: ta.Optional[ta.Callable[[ta.Any], str]] = None,
1919
- **kwargs: ta.Any,
1920
- ) -> None:
1921
- super().__init__(*args, **kwargs)
1955
+ @dc.dataclass(eq=False)
1956
+ class ArgparseCommand:
1957
+ name: str
1958
+ fn: ArgparseCommandFn
1959
+ args: ta.Sequence[ArgparseArg] = () # noqa
1960
+
1961
+ # _: dc.KW_ONLY
1962
+
1963
+ aliases: ta.Optional[ta.Sequence[str]] = None
1964
+ parent: ta.Optional['ArgparseCommand'] = None
1965
+ accepts_unknown: bool = False
1966
+
1967
+ def __post_init__(self) -> None:
1968
+ def check_name(s: str) -> None:
1969
+ check.isinstance(s, str)
1970
+ check.not_in('_', s)
1971
+ check.not_empty(s)
1972
+ check_name(self.name)
1973
+ check.not_isinstance(self.aliases, str)
1974
+ for a in self.aliases or []:
1975
+ check_name(a)
1976
+
1977
+ check.arg(callable(self.fn))
1978
+ check.arg(all(isinstance(a, ArgparseArg) for a in self.args))
1979
+ check.isinstance(self.parent, (ArgparseCommand, type(None)))
1980
+ check.isinstance(self.accepts_unknown, bool)
1981
+
1982
+ functools.update_wrapper(self, self.fn)
1983
+
1984
+ def __get__(self, instance, owner=None):
1985
+ if instance is None:
1986
+ return self
1987
+ return dc.replace(self, fn=self.fn.__get__(instance, owner)) # noqa
1988
+
1989
+ def __call__(self, *args, **kwargs) -> ta.Optional[int]:
1990
+ return self.fn(*args, **kwargs)
1991
+
1992
+
1993
+ def argparse_command(
1994
+ *args: ArgparseArg,
1995
+ name: ta.Optional[str] = None,
1996
+ aliases: ta.Optional[ta.Iterable[str]] = None,
1997
+ parent: ta.Optional[ArgparseCommand] = None,
1998
+ accepts_unknown: bool = False,
1999
+ ) -> ta.Any: # ta.Callable[[ArgparseCommandFn], ArgparseCommand]: # FIXME
2000
+ for arg in args:
2001
+ check.isinstance(arg, ArgparseArg)
2002
+ check.isinstance(name, (str, type(None)))
2003
+ check.isinstance(parent, (ArgparseCommand, type(None)))
2004
+ check.not_isinstance(aliases, str)
2005
+
2006
+ def inner(fn):
2007
+ return ArgparseCommand(
2008
+ (name if name is not None else fn.__name__).replace('_', '-'),
2009
+ fn,
2010
+ args,
2011
+ aliases=tuple(aliases) if aliases is not None else None,
2012
+ parent=parent,
2013
+ accepts_unknown=accepts_unknown,
2014
+ )
1922
2015
 
1923
- if json_dumps is None:
1924
- json_dumps = json_dumps_compact
1925
- self._json_dumps = json_dumps
2016
+ return inner
1926
2017
 
1927
- def format(self, record: logging.LogRecord) -> str:
1928
- dct = {
1929
- k: v
1930
- for k, o in self.KEYS.items()
1931
- for v in [getattr(record, k)]
1932
- if not (o and v is None)
1933
- }
1934
- return self._json_dumps(dct)
1935
2018
 
2019
+ ##
1936
2020
 
1937
- ########################################
1938
- # ../types.py
1939
2021
 
2022
+ def _get_argparse_arg_ann_kwargs(ann: ta.Any) -> ta.Mapping[str, ta.Any]:
2023
+ if ann is str:
2024
+ return {}
2025
+ elif ann is int:
2026
+ return {'type': int}
2027
+ elif ann is bool:
2028
+ return {'action': 'store_true'}
2029
+ elif ann is list:
2030
+ return {'action': 'append'}
2031
+ elif is_optional_alias(ann):
2032
+ return _get_argparse_arg_ann_kwargs(get_optional_alias_arg(ann))
2033
+ else:
2034
+ raise TypeError(ann)
1940
2035
 
1941
- # See https://peps.python.org/pep-3149/
1942
- INTERP_OPT_GLYPHS_BY_ATTR: ta.Mapping[str, str] = collections.OrderedDict([
1943
- ('debug', 'd'),
1944
- ('threaded', 't'),
1945
- ])
1946
2036
 
1947
- INTERP_OPT_ATTRS_BY_GLYPH: ta.Mapping[str, str] = collections.OrderedDict(
1948
- (g, a) for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items()
1949
- )
2037
+ class _ArgparseCliAnnotationBox:
2038
+ def __init__(self, annotations: ta.Mapping[str, ta.Any]) -> None:
2039
+ super().__init__()
2040
+ self.__annotations__ = annotations # type: ignore
1950
2041
 
1951
2042
 
1952
- @dc.dataclass(frozen=True)
1953
- class InterpOpts:
1954
- threaded: bool = False
1955
- debug: bool = False
2043
+ class ArgparseCli:
2044
+ def __init__(self, argv: ta.Optional[ta.Sequence[str]] = None) -> None:
2045
+ super().__init__()
1956
2046
 
1957
- def __str__(self) -> str:
1958
- return ''.join(g for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items() if getattr(self, a))
2047
+ self._argv = argv if argv is not None else sys.argv[1:]
1959
2048
 
1960
- @classmethod
1961
- def parse(cls, s: str) -> 'InterpOpts':
1962
- return cls(**{INTERP_OPT_ATTRS_BY_GLYPH[g]: True for g in s})
2049
+ self._args, self._unknown_args = self.get_parser().parse_known_args(self._argv)
1963
2050
 
1964
- @classmethod
1965
- def parse_suffix(cls, s: str) -> ta.Tuple[str, 'InterpOpts']:
1966
- kw = {}
1967
- while s and (a := INTERP_OPT_ATTRS_BY_GLYPH.get(s[-1])):
1968
- s, kw[a] = s[:-1], True
1969
- return s, cls(**kw)
2051
+ #
1970
2052
 
2053
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
2054
+ super().__init_subclass__(**kwargs)
1971
2055
 
1972
- @dc.dataclass(frozen=True)
1973
- class InterpVersion:
1974
- version: Version
1975
- opts: InterpOpts
2056
+ ns = cls.__dict__
2057
+ objs = {}
2058
+ mro = cls.__mro__[::-1]
2059
+ for bns in [bcls.__dict__ for bcls in reversed(mro)] + [ns]:
2060
+ bseen = set() # type: ignore
2061
+ for k, v in bns.items():
2062
+ if isinstance(v, (ArgparseCommand, ArgparseArg)):
2063
+ check.not_in(v, bseen)
2064
+ bseen.add(v)
2065
+ objs[k] = v
2066
+ elif k in objs:
2067
+ del [k]
1976
2068
 
1977
- def __str__(self) -> str:
1978
- return str(self.version) + str(self.opts)
2069
+ #
1979
2070
 
1980
- @classmethod
1981
- def parse(cls, s: str) -> 'InterpVersion':
1982
- s, o = InterpOpts.parse_suffix(s)
1983
- v = Version(s)
1984
- return cls(
1985
- version=v,
1986
- opts=o,
1987
- )
2071
+ anns = ta.get_type_hints(_ArgparseCliAnnotationBox({
2072
+ **{k: v for bcls in reversed(mro) for k, v in getattr(bcls, '__annotations__', {}).items()},
2073
+ **ns.get('__annotations__', {}),
2074
+ }), globalns=ns.get('__globals__', {}))
1988
2075
 
1989
- @classmethod
1990
- def try_parse(cls, s: str) -> ta.Optional['InterpVersion']:
1991
- try:
1992
- return cls.parse(s)
1993
- except (KeyError, InvalidVersion):
1994
- return None
2076
+ #
2077
+
2078
+ if '_parser' in ns:
2079
+ parser = check.isinstance(ns['_parser'], argparse.ArgumentParser)
2080
+ else:
2081
+ parser = argparse.ArgumentParser()
2082
+ setattr(cls, '_parser', parser)
1995
2083
 
2084
+ #
1996
2085
 
1997
- @dc.dataclass(frozen=True)
1998
- class InterpSpecifier:
1999
- specifier: Specifier
2000
- opts: InterpOpts
2086
+ subparsers = parser.add_subparsers()
2087
+
2088
+ for att, obj in objs.items():
2089
+ if isinstance(obj, ArgparseCommand):
2090
+ if obj.parent is not None:
2091
+ raise NotImplementedError
2092
+
2093
+ for cn in [obj.name, *(obj.aliases or [])]:
2094
+ subparser = subparsers.add_parser(cn)
2095
+
2096
+ for arg in (obj.args or []):
2097
+ if (
2098
+ len(arg.args) == 1 and
2099
+ isinstance(arg.args[0], str) and
2100
+ not (n := check.isinstance(arg.args[0], str)).startswith('-') and
2101
+ 'metavar' not in arg.kwargs
2102
+ ):
2103
+ subparser.add_argument(
2104
+ n.replace('-', '_'),
2105
+ **arg.kwargs,
2106
+ metavar=n,
2107
+ )
2108
+ else:
2109
+ subparser.add_argument(*arg.args, **arg.kwargs)
2110
+
2111
+ subparser.set_defaults(_cmd=obj)
2112
+
2113
+ elif isinstance(obj, ArgparseArg):
2114
+ if att in anns:
2115
+ ann_kwargs = _get_argparse_arg_ann_kwargs(anns[att])
2116
+ obj.kwargs = {**ann_kwargs, **obj.kwargs}
2117
+
2118
+ if not obj.dest:
2119
+ if 'dest' in obj.kwargs:
2120
+ obj.dest = obj.kwargs['dest']
2121
+ else:
2122
+ obj.dest = obj.kwargs['dest'] = att # type: ignore
2123
+
2124
+ parser.add_argument(*obj.args, **obj.kwargs)
2001
2125
 
2002
- def __str__(self) -> str:
2003
- return str(self.specifier) + str(self.opts)
2126
+ else:
2127
+ raise TypeError(obj)
2128
+
2129
+ #
2130
+
2131
+ _parser: ta.ClassVar[argparse.ArgumentParser]
2004
2132
 
2005
2133
  @classmethod
2006
- def parse(cls, s: str) -> 'InterpSpecifier':
2007
- s, o = InterpOpts.parse_suffix(s)
2008
- if not any(s.startswith(o) for o in Specifier.OPERATORS):
2009
- s = '~=' + s
2010
- if s.count('.') < 2:
2011
- s += '.0'
2012
- return cls(
2013
- specifier=Specifier(s),
2014
- opts=o,
2015
- )
2134
+ def get_parser(cls) -> argparse.ArgumentParser:
2135
+ return cls._parser
2016
2136
 
2017
- def contains(self, iv: InterpVersion) -> bool:
2018
- return self.specifier.contains(iv.version) and self.opts == iv.opts
2137
+ @property
2138
+ def argv(self) -> ta.Sequence[str]:
2139
+ return self._argv
2019
2140
 
2020
- def __contains__(self, iv: InterpVersion) -> bool:
2021
- return self.contains(iv)
2141
+ @property
2142
+ def args(self) -> argparse.Namespace:
2143
+ return self._args
2022
2144
 
2145
+ @property
2146
+ def unknown_args(self) -> ta.Sequence[str]:
2147
+ return self._unknown_args
2023
2148
 
2024
- @dc.dataclass(frozen=True)
2025
- class Interp:
2026
- exe: str
2027
- version: InterpVersion
2149
+ #
2028
2150
 
2151
+ def _bind_cli_cmd(self, cmd: ArgparseCommand) -> ta.Callable:
2152
+ return cmd.__get__(self, type(self))
2029
2153
 
2030
- ########################################
2031
- # ../../../omlish/logs/standard.py
2032
- """
2033
- TODO:
2034
- - structured
2035
- - prefixed
2036
- - debug
2037
- - optional noisy? noisy will never be lite - some kinda configure_standard callback mechanism?
2038
- """
2154
+ def prepare_cli_run(self) -> ta.Optional[ta.Callable]:
2155
+ cmd = getattr(self.args, '_cmd', None)
2039
2156
 
2157
+ if self._unknown_args and not (cmd is not None and cmd.accepts_unknown):
2158
+ msg = f'unrecognized arguments: {" ".join(self._unknown_args)}'
2159
+ if (parser := self.get_parser()).exit_on_error: # type: ignore
2160
+ parser.error(msg)
2161
+ else:
2162
+ raise argparse.ArgumentError(None, msg)
2040
2163
 
2041
- ##
2164
+ if cmd is None:
2165
+ self.get_parser().print_help()
2166
+ return None
2042
2167
 
2168
+ return self._bind_cli_cmd(cmd)
2043
2169
 
2044
- STANDARD_LOG_FORMAT_PARTS = [
2045
- ('asctime', '%(asctime)-15s'),
2046
- ('process', 'pid=%(process)-6s'),
2047
- ('thread', 'tid=%(thread)x'),
2048
- ('levelname', '%(levelname)s'),
2049
- ('name', '%(name)s'),
2050
- ('separator', '::'),
2051
- ('message', '%(message)s'),
2052
- ]
2170
+ #
2053
2171
 
2172
+ def cli_run(self) -> ta.Optional[int]:
2173
+ if (fn := self.prepare_cli_run()) is None:
2174
+ return 0
2054
2175
 
2055
- class StandardLogFormatter(logging.Formatter):
2056
- @staticmethod
2057
- def build_log_format(parts: ta.Iterable[ta.Tuple[str, str]]) -> str:
2058
- return ' '.join(v for k, v in parts)
2176
+ return fn()
2059
2177
 
2060
- converter = datetime.datetime.fromtimestamp # type: ignore
2178
+ def cli_run_and_exit(self) -> ta.NoReturn:
2179
+ sys.exit(rc if isinstance(rc := self.cli_run(), int) else 0)
2061
2180
 
2062
- def formatTime(self, record, datefmt=None):
2063
- ct = self.converter(record.created) # type: ignore
2064
- if datefmt:
2065
- return ct.strftime(datefmt) # noqa
2181
+ def __call__(self, *, exit: bool = False) -> ta.Optional[int]: # noqa
2182
+ if exit:
2183
+ return self.cli_run_and_exit()
2066
2184
  else:
2067
- t = ct.strftime('%Y-%m-%d %H:%M:%S')
2068
- return '%s.%03d' % (t, record.msecs) # noqa
2185
+ return self.cli_run()
2069
2186
 
2187
+ #
2070
2188
 
2071
- ##
2189
+ async def async_cli_run(self) -> ta.Optional[int]:
2190
+ if (fn := self.prepare_cli_run()) is None:
2191
+ return 0
2072
2192
 
2193
+ return await fn()
2073
2194
 
2074
- class StandardConfiguredLogHandler(ProxyLogHandler):
2075
- def __init_subclass__(cls, **kwargs):
2076
- raise TypeError('This class serves only as a marker and should not be subclassed.')
2077
2195
 
2196
+ ########################################
2197
+ # ../../../omlish/lite/inject.py
2078
2198
 
2079
- ##
2080
2199
 
2200
+ ###
2201
+ # types
2081
2202
 
2082
- @contextlib.contextmanager
2083
- def _locking_logging_module_lock() -> ta.Iterator[None]:
2084
- if hasattr(logging, '_acquireLock'):
2085
- logging._acquireLock() # noqa
2086
- try:
2087
- yield
2088
- finally:
2089
- logging._releaseLock() # type: ignore # noqa
2090
2203
 
2091
- elif hasattr(logging, '_lock'):
2092
- # https://github.com/python/cpython/commit/74723e11109a320e628898817ab449b3dad9ee96
2093
- with logging._lock: # noqa
2094
- yield
2204
+ @dc.dataclass(frozen=True)
2205
+ class InjectorKey(ta.Generic[T]):
2206
+ # Before PEP-560 typing.Generic was a metaclass with a __new__ that takes a 'cls' arg, so instantiating a dataclass
2207
+ # with kwargs (such as through dc.replace) causes `TypeError: __new__() got multiple values for argument 'cls'`.
2208
+ # See:
2209
+ # - https://github.com/python/cpython/commit/d911e40e788fb679723d78b6ea11cabf46caed5a
2210
+ # - https://gist.github.com/wrmsr/4468b86efe9f373b6b114bfe85b98fd3
2211
+ cls_: InjectorKeyCls
2095
2212
 
2096
- else:
2097
- raise Exception("Can't find lock in logging module")
2213
+ tag: ta.Any = None
2214
+ array: bool = False
2098
2215
 
2099
2216
 
2100
- def configure_standard_logging(
2101
- level: ta.Union[int, str] = logging.INFO,
2102
- *,
2103
- json: bool = False,
2104
- target: ta.Optional[logging.Logger] = None,
2105
- force: bool = False,
2106
- handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
2107
- ) -> ta.Optional[StandardConfiguredLogHandler]:
2108
- with _locking_logging_module_lock():
2109
- if target is None:
2110
- target = logging.root
2217
+ def is_valid_injector_key_cls(cls: ta.Any) -> bool:
2218
+ return isinstance(cls, type) or is_new_type(cls)
2111
2219
 
2112
- #
2113
2220
 
2114
- if not force:
2115
- if any(isinstance(h, StandardConfiguredLogHandler) for h in list(target.handlers)):
2116
- return None
2221
+ def check_valid_injector_key_cls(cls: T) -> T:
2222
+ if not is_valid_injector_key_cls(cls):
2223
+ raise TypeError(cls)
2224
+ return cls
2117
2225
 
2118
- #
2119
2226
 
2120
- if handler_factory is not None:
2121
- handler = handler_factory()
2122
- else:
2123
- handler = logging.StreamHandler()
2227
+ ##
2124
2228
 
2125
- #
2126
2229
 
2127
- formatter: logging.Formatter
2230
+ class InjectorProvider(abc.ABC):
2231
+ @abc.abstractmethod
2232
+ def provider_fn(self) -> InjectorProviderFn:
2233
+ raise NotImplementedError
2234
+
2235
+
2236
+ ##
2237
+
2238
+
2239
+ @dc.dataclass(frozen=True)
2240
+ class InjectorBinding:
2241
+ key: InjectorKey
2242
+ provider: InjectorProvider
2243
+
2244
+ def __post_init__(self) -> None:
2245
+ check.isinstance(self.key, InjectorKey)
2246
+ check.isinstance(self.provider, InjectorProvider)
2247
+
2248
+
2249
+ class InjectorBindings(abc.ABC):
2250
+ @abc.abstractmethod
2251
+ def bindings(self) -> ta.Iterator[InjectorBinding]:
2252
+ raise NotImplementedError
2253
+
2254
+ ##
2255
+
2256
+
2257
+ class Injector(abc.ABC):
2258
+ @abc.abstractmethod
2259
+ def try_provide(self, key: ta.Any) -> Maybe[ta.Any]:
2260
+ raise NotImplementedError
2261
+
2262
+ @abc.abstractmethod
2263
+ def provide(self, key: ta.Any) -> ta.Any:
2264
+ raise NotImplementedError
2265
+
2266
+ @abc.abstractmethod
2267
+ def provide_kwargs(
2268
+ self,
2269
+ obj: ta.Any,
2270
+ *,
2271
+ skip_args: int = 0,
2272
+ skip_kwargs: ta.Optional[ta.Iterable[ta.Any]] = None,
2273
+ ) -> ta.Mapping[str, ta.Any]:
2274
+ raise NotImplementedError
2275
+
2276
+ @abc.abstractmethod
2277
+ def inject(
2278
+ self,
2279
+ obj: ta.Any,
2280
+ *,
2281
+ args: ta.Optional[ta.Sequence[ta.Any]] = None,
2282
+ kwargs: ta.Optional[ta.Mapping[str, ta.Any]] = None,
2283
+ ) -> ta.Any:
2284
+ raise NotImplementedError
2285
+
2286
+ def __getitem__(
2287
+ self,
2288
+ target: ta.Union[InjectorKey[T], ta.Type[T]],
2289
+ ) -> T:
2290
+ return self.provide(target)
2291
+
2292
+
2293
+ ###
2294
+ # exceptions
2295
+
2296
+
2297
+ class InjectorError(Exception):
2298
+ pass
2299
+
2300
+
2301
+ @dc.dataclass()
2302
+ class InjectorKeyError(InjectorError):
2303
+ key: InjectorKey
2304
+
2305
+ source: ta.Any = None
2306
+ name: ta.Optional[str] = None
2307
+
2308
+
2309
+ class UnboundInjectorKeyError(InjectorKeyError):
2310
+ pass
2311
+
2312
+
2313
+ class DuplicateInjectorKeyError(InjectorKeyError):
2314
+ pass
2315
+
2316
+
2317
+ class CyclicDependencyInjectorKeyError(InjectorKeyError):
2318
+ pass
2319
+
2320
+
2321
+ ###
2322
+ # keys
2323
+
2324
+
2325
+ def as_injector_key(o: ta.Any) -> InjectorKey:
2326
+ if o is inspect.Parameter.empty:
2327
+ raise TypeError(o)
2328
+ if isinstance(o, InjectorKey):
2329
+ return o
2330
+ if is_valid_injector_key_cls(o):
2331
+ return InjectorKey(o)
2332
+ raise TypeError(o)
2333
+
2334
+
2335
+ ###
2336
+ # providers
2337
+
2338
+
2339
+ @dc.dataclass(frozen=True)
2340
+ class FnInjectorProvider(InjectorProvider):
2341
+ fn: ta.Any
2342
+
2343
+ def __post_init__(self) -> None:
2344
+ check.not_isinstance(self.fn, type)
2345
+
2346
+ def provider_fn(self) -> InjectorProviderFn:
2347
+ def pfn(i: Injector) -> ta.Any:
2348
+ return i.inject(self.fn)
2349
+
2350
+ return pfn
2351
+
2352
+
2353
+ @dc.dataclass(frozen=True)
2354
+ class CtorInjectorProvider(InjectorProvider):
2355
+ cls_: type
2356
+
2357
+ def __post_init__(self) -> None:
2358
+ check.isinstance(self.cls_, type)
2359
+
2360
+ def provider_fn(self) -> InjectorProviderFn:
2361
+ def pfn(i: Injector) -> ta.Any:
2362
+ return i.inject(self.cls_)
2363
+
2364
+ return pfn
2365
+
2366
+
2367
+ @dc.dataclass(frozen=True)
2368
+ class ConstInjectorProvider(InjectorProvider):
2369
+ v: ta.Any
2370
+
2371
+ def provider_fn(self) -> InjectorProviderFn:
2372
+ return lambda _: self.v
2373
+
2374
+
2375
+ @dc.dataclass(frozen=True)
2376
+ class SingletonInjectorProvider(InjectorProvider):
2377
+ p: InjectorProvider
2378
+
2379
+ def __post_init__(self) -> None:
2380
+ check.isinstance(self.p, InjectorProvider)
2381
+
2382
+ def provider_fn(self) -> InjectorProviderFn:
2383
+ v = not_set = object()
2384
+
2385
+ def pfn(i: Injector) -> ta.Any:
2386
+ nonlocal v
2387
+ if v is not_set:
2388
+ v = ufn(i)
2389
+ return v
2390
+
2391
+ ufn = self.p.provider_fn()
2392
+ return pfn
2393
+
2394
+
2395
+ @dc.dataclass(frozen=True)
2396
+ class LinkInjectorProvider(InjectorProvider):
2397
+ k: InjectorKey
2398
+
2399
+ def __post_init__(self) -> None:
2400
+ check.isinstance(self.k, InjectorKey)
2401
+
2402
+ def provider_fn(self) -> InjectorProviderFn:
2403
+ def pfn(i: Injector) -> ta.Any:
2404
+ return i.provide(self.k)
2405
+
2406
+ return pfn
2407
+
2408
+
2409
+ @dc.dataclass(frozen=True)
2410
+ class ArrayInjectorProvider(InjectorProvider):
2411
+ ps: ta.Sequence[InjectorProvider]
2412
+
2413
+ def provider_fn(self) -> InjectorProviderFn:
2414
+ ps = [p.provider_fn() for p in self.ps]
2415
+
2416
+ def pfn(i: Injector) -> ta.Any:
2417
+ rv = []
2418
+ for ep in ps:
2419
+ o = ep(i)
2420
+ rv.append(o)
2421
+ return rv
2422
+
2423
+ return pfn
2424
+
2425
+
2426
+ ###
2427
+ # bindings
2428
+
2429
+
2430
+ @dc.dataclass(frozen=True)
2431
+ class _InjectorBindings(InjectorBindings):
2432
+ bs: ta.Optional[ta.Sequence[InjectorBinding]] = None
2433
+ ps: ta.Optional[ta.Sequence[InjectorBindings]] = None
2434
+
2435
+ def bindings(self) -> ta.Iterator[InjectorBinding]:
2436
+ if self.bs is not None:
2437
+ yield from self.bs
2438
+ if self.ps is not None:
2439
+ for p in self.ps:
2440
+ yield from p.bindings()
2441
+
2442
+
2443
+ def as_injector_bindings(*args: InjectorBindingOrBindings) -> InjectorBindings:
2444
+ bs: ta.List[InjectorBinding] = []
2445
+ ps: ta.List[InjectorBindings] = []
2446
+
2447
+ for a in args:
2448
+ if isinstance(a, InjectorBindings):
2449
+ ps.append(a)
2450
+ elif isinstance(a, InjectorBinding):
2451
+ bs.append(a)
2452
+ else:
2453
+ raise TypeError(a)
2454
+
2455
+ return _InjectorBindings(
2456
+ bs or None,
2457
+ ps or None,
2458
+ )
2459
+
2460
+
2461
+ ##
2462
+
2463
+
2464
+ def build_injector_provider_map(bs: InjectorBindings) -> ta.Mapping[InjectorKey, InjectorProvider]:
2465
+ pm: ta.Dict[InjectorKey, InjectorProvider] = {}
2466
+ am: ta.Dict[InjectorKey, ta.List[InjectorProvider]] = {}
2467
+
2468
+ for b in bs.bindings():
2469
+ if b.key.array:
2470
+ al = am.setdefault(b.key, [])
2471
+ if isinstance(b.provider, ArrayInjectorProvider):
2472
+ al.extend(b.provider.ps)
2473
+ else:
2474
+ al.append(b.provider)
2475
+ else:
2476
+ if b.key in pm:
2477
+ raise KeyError(b.key)
2478
+ pm[b.key] = b.provider
2479
+
2480
+ if am:
2481
+ for k, aps in am.items():
2482
+ pm[k] = ArrayInjectorProvider(aps)
2483
+
2484
+ return pm
2485
+
2486
+
2487
+ ###
2488
+ # overrides
2489
+
2490
+
2491
+ @dc.dataclass(frozen=True)
2492
+ class OverridesInjectorBindings(InjectorBindings):
2493
+ p: InjectorBindings
2494
+ m: ta.Mapping[InjectorKey, InjectorBinding]
2495
+
2496
+ def bindings(self) -> ta.Iterator[InjectorBinding]:
2497
+ for b in self.p.bindings():
2498
+ yield self.m.get(b.key, b)
2499
+
2500
+
2501
+ def injector_override(p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
2502
+ m: ta.Dict[InjectorKey, InjectorBinding] = {}
2503
+
2504
+ for b in as_injector_bindings(*args).bindings():
2505
+ if b.key in m:
2506
+ raise DuplicateInjectorKeyError(b.key)
2507
+ m[b.key] = b
2508
+
2509
+ return OverridesInjectorBindings(p, m)
2510
+
2511
+
2512
+ ###
2513
+ # scopes
2514
+
2515
+
2516
+ class InjectorScope(abc.ABC): # noqa
2517
+ def __init__(
2518
+ self,
2519
+ *,
2520
+ _i: Injector,
2521
+ ) -> None:
2522
+ check.not_in(abc.ABC, type(self).__bases__)
2523
+
2524
+ super().__init__()
2525
+
2526
+ self._i = _i
2527
+
2528
+ all_seeds: ta.Iterable[_InjectorScopeSeed] = self._i.provide(InjectorKey(_InjectorScopeSeed, array=True))
2529
+ self._sks = {s.k for s in all_seeds if s.sc is type(self)}
2530
+
2531
+ #
2532
+
2533
+ @dc.dataclass(frozen=True)
2534
+ class State:
2535
+ seeds: ta.Dict[InjectorKey, ta.Any]
2536
+ provisions: ta.Dict[InjectorKey, ta.Any] = dc.field(default_factory=dict)
2537
+
2538
+ def new_state(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> State:
2539
+ vs = dict(vs)
2540
+ check.equal(set(vs.keys()), self._sks)
2541
+ return InjectorScope.State(vs)
2542
+
2543
+ #
2544
+
2545
+ @abc.abstractmethod
2546
+ def state(self) -> State:
2547
+ raise NotImplementedError
2548
+
2549
+ @abc.abstractmethod
2550
+ def enter(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> ta.ContextManager[None]:
2551
+ raise NotImplementedError
2552
+
2553
+
2554
+ class ExclusiveInjectorScope(InjectorScope, abc.ABC):
2555
+ _st: ta.Optional[InjectorScope.State] = None
2556
+
2557
+ def state(self) -> InjectorScope.State:
2558
+ return check.not_none(self._st)
2559
+
2560
+ @contextlib.contextmanager
2561
+ def enter(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> ta.Iterator[None]:
2562
+ check.none(self._st)
2563
+ self._st = self.new_state(vs)
2564
+ try:
2565
+ yield
2566
+ finally:
2567
+ self._st = None
2568
+
2569
+
2570
+ class ContextvarInjectorScope(InjectorScope, abc.ABC):
2571
+ _cv: contextvars.ContextVar
2572
+
2573
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
2574
+ super().__init_subclass__(**kwargs)
2575
+ check.not_in(abc.ABC, cls.__bases__)
2576
+ check.state(not hasattr(cls, '_cv'))
2577
+ cls._cv = contextvars.ContextVar(f'{cls.__name__}_cv')
2578
+
2579
+ def state(self) -> InjectorScope.State:
2580
+ return self._cv.get()
2581
+
2582
+ @contextlib.contextmanager
2583
+ def enter(self, vs: ta.Mapping[InjectorKey, ta.Any]) -> ta.Iterator[None]:
2584
+ try:
2585
+ self._cv.get()
2586
+ except LookupError:
2587
+ pass
2588
+ else:
2589
+ raise RuntimeError(f'Scope already entered: {self}')
2590
+ st = self.new_state(vs)
2591
+ tok = self._cv.set(st)
2592
+ try:
2593
+ yield
2594
+ finally:
2595
+ self._cv.reset(tok)
2596
+
2597
+
2598
+ #
2599
+
2600
+
2601
+ @dc.dataclass(frozen=True)
2602
+ class ScopedInjectorProvider(InjectorProvider):
2603
+ p: InjectorProvider
2604
+ k: InjectorKey
2605
+ sc: ta.Type[InjectorScope]
2606
+
2607
+ def __post_init__(self) -> None:
2608
+ check.isinstance(self.p, InjectorProvider)
2609
+ check.isinstance(self.k, InjectorKey)
2610
+ check.issubclass(self.sc, InjectorScope)
2611
+
2612
+ def provider_fn(self) -> InjectorProviderFn:
2613
+ def pfn(i: Injector) -> ta.Any:
2614
+ st = i[self.sc].state()
2615
+ try:
2616
+ return st.provisions[self.k]
2617
+ except KeyError:
2618
+ pass
2619
+ v = ufn(i)
2620
+ st.provisions[self.k] = v
2621
+ return v
2622
+
2623
+ ufn = self.p.provider_fn()
2624
+ return pfn
2625
+
2626
+
2627
+ @dc.dataclass(frozen=True)
2628
+ class _ScopeSeedInjectorProvider(InjectorProvider):
2629
+ k: InjectorKey
2630
+ sc: ta.Type[InjectorScope]
2631
+
2632
+ def __post_init__(self) -> None:
2633
+ check.isinstance(self.k, InjectorKey)
2634
+ check.issubclass(self.sc, InjectorScope)
2635
+
2636
+ def provider_fn(self) -> InjectorProviderFn:
2637
+ def pfn(i: Injector) -> ta.Any:
2638
+ st = i[self.sc].state()
2639
+ return st.seeds[self.k]
2640
+ return pfn
2641
+
2642
+
2643
+ def bind_injector_scope(sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
2644
+ return InjectorBinder.bind(sc, singleton=True)
2645
+
2646
+
2647
+ #
2648
+
2649
+
2650
+ @dc.dataclass(frozen=True)
2651
+ class _InjectorScopeSeed:
2652
+ sc: ta.Type['InjectorScope']
2653
+ k: InjectorKey
2654
+
2655
+ def __post_init__(self) -> None:
2656
+ check.issubclass(self.sc, InjectorScope)
2657
+ check.isinstance(self.k, InjectorKey)
2658
+
2659
+
2660
+ def bind_injector_scope_seed(k: ta.Any, sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
2661
+ kk = as_injector_key(k)
2662
+ return as_injector_bindings(
2663
+ InjectorBinding(kk, _ScopeSeedInjectorProvider(kk, sc)),
2664
+ InjectorBinder.bind(_InjectorScopeSeed(sc, kk), array=True),
2665
+ )
2666
+
2667
+
2668
+ ###
2669
+ # inspection
2670
+
2671
+
2672
+ class _InjectionInspection(ta.NamedTuple):
2673
+ signature: inspect.Signature
2674
+ type_hints: ta.Mapping[str, ta.Any]
2675
+ args_offset: int
2676
+
2677
+
2678
+ _INJECTION_INSPECTION_CACHE: ta.MutableMapping[ta.Any, _InjectionInspection] = weakref.WeakKeyDictionary()
2679
+
2680
+
2681
+ def _do_injection_inspect(obj: ta.Any) -> _InjectionInspection:
2682
+ tgt = obj
2683
+ if isinstance(tgt, type) and tgt.__init__ is not object.__init__: # type: ignore[misc]
2684
+ # Python 3.8's inspect.signature can't handle subclasses overriding __new__, always generating *args/**kwargs.
2685
+ # - https://bugs.python.org/issue40897
2686
+ # - https://github.com/python/cpython/commit/df7c62980d15acd3125dfbd81546dad359f7add7
2687
+ tgt = tgt.__init__ # type: ignore[misc]
2688
+ has_generic_base = True
2689
+ else:
2690
+ has_generic_base = False
2691
+
2692
+ # 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
2693
+ # eval str annotations *in addition to* getting the signature for parameter information.
2694
+ uw = tgt
2695
+ has_partial = False
2696
+ while True:
2697
+ if isinstance(uw, functools.partial):
2698
+ has_partial = True
2699
+ uw = uw.func
2700
+ else:
2701
+ if (uw2 := inspect.unwrap(uw)) is uw:
2702
+ break
2703
+ uw = uw2
2704
+
2705
+ if has_generic_base and has_partial:
2706
+ raise InjectorError(
2707
+ 'Injector inspection does not currently support both a typing.Generic base and a functools.partial: '
2708
+ f'{obj}',
2709
+ )
2710
+
2711
+ return _InjectionInspection(
2712
+ inspect.signature(tgt),
2713
+ ta.get_type_hints(uw),
2714
+ 1 if has_generic_base else 0,
2715
+ )
2716
+
2717
+
2718
+ def _injection_inspect(obj: ta.Any) -> _InjectionInspection:
2719
+ try:
2720
+ return _INJECTION_INSPECTION_CACHE[obj]
2721
+ except TypeError:
2722
+ return _do_injection_inspect(obj)
2723
+ except KeyError:
2724
+ pass
2725
+ insp = _do_injection_inspect(obj)
2726
+ _INJECTION_INSPECTION_CACHE[obj] = insp
2727
+ return insp
2728
+
2729
+
2730
+ class InjectionKwarg(ta.NamedTuple):
2731
+ name: str
2732
+ key: InjectorKey
2733
+ has_default: bool
2734
+
2735
+
2736
+ class InjectionKwargsTarget(ta.NamedTuple):
2737
+ obj: ta.Any
2738
+ kwargs: ta.Sequence[InjectionKwarg]
2739
+
2740
+
2741
+ def build_injection_kwargs_target(
2742
+ obj: ta.Any,
2743
+ *,
2744
+ skip_args: int = 0,
2745
+ skip_kwargs: ta.Optional[ta.Iterable[str]] = None,
2746
+ raw_optional: bool = False,
2747
+ ) -> InjectionKwargsTarget:
2748
+ insp = _injection_inspect(obj)
2749
+
2750
+ params = list(insp.signature.parameters.values())
2751
+
2752
+ skip_names: ta.Set[str] = set()
2753
+ if skip_kwargs is not None:
2754
+ skip_names.update(check.not_isinstance(skip_kwargs, str))
2755
+
2756
+ seen: ta.Set[InjectorKey] = set()
2757
+ kws: ta.List[InjectionKwarg] = []
2758
+ for p in params[insp.args_offset + skip_args:]:
2759
+ if p.name in skip_names:
2760
+ continue
2761
+
2762
+ if p.annotation is inspect.Signature.empty:
2763
+ if p.default is not inspect.Parameter.empty:
2764
+ raise KeyError(f'{obj}, {p.name}')
2765
+ continue
2766
+
2767
+ if p.kind not in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY):
2768
+ raise TypeError(insp)
2769
+
2770
+ # 3.8 inspect.signature doesn't eval_str but typing.get_type_hints does, so prefer that.
2771
+ ann = insp.type_hints.get(p.name, p.annotation)
2772
+ if (
2773
+ not raw_optional and
2774
+ is_optional_alias(ann)
2775
+ ):
2776
+ ann = get_optional_alias_arg(ann)
2777
+
2778
+ k = as_injector_key(ann)
2779
+
2780
+ if k in seen:
2781
+ raise DuplicateInjectorKeyError(k)
2782
+ seen.add(k)
2783
+
2784
+ kws.append(InjectionKwarg(
2785
+ p.name,
2786
+ k,
2787
+ p.default is not inspect.Parameter.empty,
2788
+ ))
2789
+
2790
+ return InjectionKwargsTarget(
2791
+ obj,
2792
+ kws,
2793
+ )
2794
+
2795
+
2796
+ ###
2797
+ # injector
2798
+
2799
+
2800
+ _INJECTOR_INJECTOR_KEY: InjectorKey[Injector] = InjectorKey(Injector)
2801
+
2802
+
2803
+ @dc.dataclass(frozen=True)
2804
+ class _InjectorEager:
2805
+ key: InjectorKey
2806
+
2807
+
2808
+ _INJECTOR_EAGER_ARRAY_KEY: InjectorKey[_InjectorEager] = InjectorKey(_InjectorEager, array=True)
2809
+
2810
+
2811
+ class _Injector(Injector):
2812
+ _DEFAULT_BINDINGS: ta.ClassVar[ta.List[InjectorBinding]] = []
2813
+
2814
+ def __init__(self, bs: InjectorBindings, p: ta.Optional[Injector] = None) -> None:
2815
+ super().__init__()
2816
+
2817
+ self._bs = check.isinstance(bs, InjectorBindings)
2818
+ self._p: ta.Optional[Injector] = check.isinstance(p, (Injector, type(None)))
2819
+
2820
+ self._pfm = {
2821
+ k: v.provider_fn()
2822
+ for k, v in build_injector_provider_map(as_injector_bindings(
2823
+ *self._DEFAULT_BINDINGS,
2824
+ bs,
2825
+ )).items()
2826
+ }
2827
+
2828
+ if _INJECTOR_INJECTOR_KEY in self._pfm:
2829
+ raise DuplicateInjectorKeyError(_INJECTOR_INJECTOR_KEY)
2830
+
2831
+ self.__cur_req: ta.Optional[_Injector._Request] = None
2832
+
2833
+ if _INJECTOR_EAGER_ARRAY_KEY in self._pfm:
2834
+ for e in self.provide(_INJECTOR_EAGER_ARRAY_KEY):
2835
+ self.provide(e.key)
2836
+
2837
+ class _Request:
2838
+ def __init__(self, injector: '_Injector') -> None:
2839
+ super().__init__()
2840
+ self._injector = injector
2841
+ self._provisions: ta.Dict[InjectorKey, Maybe] = {}
2842
+ self._seen_keys: ta.Set[InjectorKey] = set()
2843
+
2844
+ def handle_key(self, key: InjectorKey) -> Maybe[Maybe]:
2845
+ try:
2846
+ return Maybe.just(self._provisions[key])
2847
+ except KeyError:
2848
+ pass
2849
+ if key in self._seen_keys:
2850
+ raise CyclicDependencyInjectorKeyError(key)
2851
+ self._seen_keys.add(key)
2852
+ return Maybe.empty()
2853
+
2854
+ def handle_provision(self, key: InjectorKey, mv: Maybe) -> Maybe:
2855
+ check.in_(key, self._seen_keys)
2856
+ check.not_in(key, self._provisions)
2857
+ self._provisions[key] = mv
2858
+ return mv
2859
+
2860
+ @contextlib.contextmanager
2861
+ def _current_request(self) -> ta.Generator[_Request, None, None]:
2862
+ if (cr := self.__cur_req) is not None:
2863
+ yield cr
2864
+ return
2865
+
2866
+ cr = self._Request(self)
2867
+ try:
2868
+ self.__cur_req = cr
2869
+ yield cr
2870
+ finally:
2871
+ self.__cur_req = None
2872
+
2873
+ def try_provide(self, key: ta.Any) -> Maybe[ta.Any]:
2874
+ key = as_injector_key(key)
2875
+
2876
+ cr: _Injector._Request
2877
+ with self._current_request() as cr:
2878
+ if (rv := cr.handle_key(key)).present:
2879
+ return rv.must()
2880
+
2881
+ if key == _INJECTOR_INJECTOR_KEY:
2882
+ return cr.handle_provision(key, Maybe.just(self))
2883
+
2884
+ fn = self._pfm.get(key)
2885
+ if fn is not None:
2886
+ return cr.handle_provision(key, Maybe.just(fn(self)))
2887
+
2888
+ if self._p is not None:
2889
+ pv = self._p.try_provide(key)
2890
+ if pv is not None:
2891
+ return cr.handle_provision(key, Maybe.empty())
2892
+
2893
+ return cr.handle_provision(key, Maybe.empty())
2894
+
2895
+ def provide(self, key: ta.Any) -> ta.Any:
2896
+ v = self.try_provide(key)
2897
+ if v.present:
2898
+ return v.must()
2899
+ raise UnboundInjectorKeyError(key)
2900
+
2901
+ def provide_kwargs(
2902
+ self,
2903
+ obj: ta.Any,
2904
+ *,
2905
+ skip_args: int = 0,
2906
+ skip_kwargs: ta.Optional[ta.Iterable[ta.Any]] = None,
2907
+ ) -> ta.Mapping[str, ta.Any]:
2908
+ kt = build_injection_kwargs_target(
2909
+ obj,
2910
+ skip_args=skip_args,
2911
+ skip_kwargs=skip_kwargs,
2912
+ )
2913
+
2914
+ ret: ta.Dict[str, ta.Any] = {}
2915
+ for kw in kt.kwargs:
2916
+ if kw.has_default:
2917
+ if not (mv := self.try_provide(kw.key)).present:
2918
+ continue
2919
+ v = mv.must()
2920
+ else:
2921
+ v = self.provide(kw.key)
2922
+ ret[kw.name] = v
2923
+ return ret
2924
+
2925
+ def inject(
2926
+ self,
2927
+ obj: ta.Any,
2928
+ *,
2929
+ args: ta.Optional[ta.Sequence[ta.Any]] = None,
2930
+ kwargs: ta.Optional[ta.Mapping[str, ta.Any]] = None,
2931
+ ) -> ta.Any:
2932
+ provided = self.provide_kwargs(
2933
+ obj,
2934
+ skip_args=len(args) if args is not None else 0,
2935
+ skip_kwargs=kwargs if kwargs is not None else None,
2936
+ )
2937
+
2938
+ return obj(
2939
+ *(args if args is not None else ()),
2940
+ **(kwargs if kwargs is not None else {}),
2941
+ **provided,
2942
+ )
2943
+
2944
+
2945
+ ###
2946
+ # binder
2947
+
2948
+
2949
+ class InjectorBinder:
2950
+ def __new__(cls, *args, **kwargs): # noqa
2951
+ raise TypeError
2952
+
2953
+ _FN_TYPES: ta.ClassVar[ta.Tuple[type, ...]] = (
2954
+ types.FunctionType,
2955
+ types.MethodType,
2956
+
2957
+ classmethod,
2958
+ staticmethod,
2959
+
2960
+ functools.partial,
2961
+ functools.partialmethod,
2962
+ )
2963
+
2964
+ @classmethod
2965
+ def _is_fn(cls, obj: ta.Any) -> bool:
2966
+ return isinstance(obj, cls._FN_TYPES)
2967
+
2968
+ @classmethod
2969
+ def bind_as_fn(cls, icls: ta.Type[T]) -> ta.Type[T]:
2970
+ check.isinstance(icls, type)
2971
+ if icls not in cls._FN_TYPES:
2972
+ cls._FN_TYPES = (*cls._FN_TYPES, icls)
2973
+ return icls
2974
+
2975
+ _BANNED_BIND_TYPES: ta.ClassVar[ta.Tuple[type, ...]] = (
2976
+ InjectorProvider,
2977
+ )
2978
+
2979
+ @classmethod
2980
+ def bind(
2981
+ cls,
2982
+ obj: ta.Any,
2983
+ *,
2984
+ key: ta.Any = None,
2985
+ tag: ta.Any = None,
2986
+ array: ta.Optional[bool] = None, # noqa
2987
+
2988
+ to_fn: ta.Any = None,
2989
+ to_ctor: ta.Any = None,
2990
+ to_const: ta.Any = None,
2991
+ to_key: ta.Any = None,
2992
+
2993
+ in_: ta.Optional[ta.Type[InjectorScope]] = None,
2994
+ singleton: bool = False,
2995
+
2996
+ eager: bool = False,
2997
+ ) -> InjectorBindingOrBindings:
2998
+ if obj is None or obj is inspect.Parameter.empty:
2999
+ raise TypeError(obj)
3000
+ if isinstance(obj, cls._BANNED_BIND_TYPES):
3001
+ raise TypeError(obj)
3002
+
3003
+ #
3004
+
3005
+ if key is not None:
3006
+ key = as_injector_key(key)
3007
+
3008
+ #
3009
+
3010
+ has_to = (
3011
+ to_fn is not None or
3012
+ to_ctor is not None or
3013
+ to_const is not None or
3014
+ to_key is not None
3015
+ )
3016
+ if isinstance(obj, InjectorKey):
3017
+ if key is None:
3018
+ key = obj
3019
+ elif isinstance(obj, type):
3020
+ if not has_to:
3021
+ to_ctor = obj
3022
+ if key is None:
3023
+ key = InjectorKey(obj)
3024
+ elif cls._is_fn(obj) and not has_to:
3025
+ to_fn = obj
3026
+ if key is None:
3027
+ insp = _injection_inspect(obj)
3028
+ key_cls: ta.Any = check_valid_injector_key_cls(check.not_none(insp.type_hints.get('return')))
3029
+ key = InjectorKey(key_cls)
3030
+ else:
3031
+ if to_const is not None:
3032
+ raise TypeError('Cannot bind instance with to_const')
3033
+ to_const = obj
3034
+ if key is None:
3035
+ key = InjectorKey(type(obj))
3036
+ del has_to
3037
+
3038
+ #
3039
+
3040
+ if tag is not None:
3041
+ if key.tag is not None:
3042
+ raise TypeError('Tag already set')
3043
+ key = dc.replace(key, tag=tag)
3044
+
3045
+ if array is not None:
3046
+ key = dc.replace(key, array=array)
3047
+
3048
+ #
3049
+
3050
+ providers: ta.List[InjectorProvider] = []
3051
+ if to_fn is not None:
3052
+ providers.append(FnInjectorProvider(to_fn))
3053
+ if to_ctor is not None:
3054
+ providers.append(CtorInjectorProvider(to_ctor))
3055
+ if to_const is not None:
3056
+ providers.append(ConstInjectorProvider(to_const))
3057
+ if to_key is not None:
3058
+ providers.append(LinkInjectorProvider(as_injector_key(to_key)))
3059
+ if not providers:
3060
+ raise TypeError('Must specify provider')
3061
+ if len(providers) > 1:
3062
+ raise TypeError('May not specify multiple providers')
3063
+ provider = check.single(providers)
3064
+
3065
+ #
3066
+
3067
+ pws: ta.List[ta.Any] = []
3068
+ if in_ is not None:
3069
+ check.issubclass(in_, InjectorScope)
3070
+ check.not_in(abc.ABC, in_.__bases__)
3071
+ pws.append(functools.partial(ScopedInjectorProvider, k=key, sc=in_))
3072
+ if singleton:
3073
+ pws.append(SingletonInjectorProvider)
3074
+ if len(pws) > 1:
3075
+ raise TypeError('May not specify multiple provider wrappers')
3076
+ elif pws:
3077
+ provider = check.single(pws)(provider)
3078
+
3079
+ #
3080
+
3081
+ binding = InjectorBinding(key, provider)
3082
+
3083
+ #
3084
+
3085
+ extras: ta.List[InjectorBinding] = []
3086
+
3087
+ if eager:
3088
+ extras.append(bind_injector_eager_key(key))
3089
+
3090
+ #
3091
+
3092
+ if extras:
3093
+ return as_injector_bindings(binding, *extras)
3094
+ else:
3095
+ return binding
3096
+
3097
+
3098
+ ###
3099
+ # injection helpers
3100
+
3101
+
3102
+ def make_injector_factory(
3103
+ fn: ta.Callable[..., T],
3104
+ cls: U,
3105
+ ann: ta.Any = None,
3106
+ ) -> ta.Callable[..., U]:
3107
+ if ann is None:
3108
+ ann = cls
3109
+
3110
+ def outer(injector: Injector) -> ann:
3111
+ def inner(*args, **kwargs):
3112
+ return injector.inject(fn, args=args, kwargs=kwargs)
3113
+ return cls(inner) # type: ignore
3114
+
3115
+ return outer
3116
+
3117
+
3118
+ def bind_injector_array(
3119
+ obj: ta.Any = None,
3120
+ *,
3121
+ tag: ta.Any = None,
3122
+ ) -> InjectorBindingOrBindings:
3123
+ key = as_injector_key(obj)
3124
+ if tag is not None:
3125
+ if key.tag is not None:
3126
+ raise ValueError('Must not specify multiple tags')
3127
+ key = dc.replace(key, tag=tag)
3128
+
3129
+ if key.array:
3130
+ raise ValueError('Key must not be array')
3131
+
3132
+ return InjectorBinding(
3133
+ dc.replace(key, array=True),
3134
+ ArrayInjectorProvider([]),
3135
+ )
3136
+
3137
+
3138
+ def make_injector_array_type(
3139
+ ele: ta.Union[InjectorKey, InjectorKeyCls],
3140
+ cls: U,
3141
+ ann: ta.Any = None,
3142
+ ) -> ta.Callable[..., U]:
3143
+ if isinstance(ele, InjectorKey):
3144
+ if not ele.array:
3145
+ raise InjectorError('Provided key must be array', ele)
3146
+ key = ele
3147
+ else:
3148
+ key = dc.replace(as_injector_key(ele), array=True)
3149
+
3150
+ if ann is None:
3151
+ ann = cls
3152
+
3153
+ def inner(injector: Injector) -> ann:
3154
+ return cls(injector.provide(key)) # type: ignore[operator]
3155
+
3156
+ return inner
3157
+
3158
+
3159
+ def bind_injector_eager_key(key: ta.Any) -> InjectorBinding:
3160
+ return InjectorBinding(_INJECTOR_EAGER_ARRAY_KEY, ConstInjectorProvider(_InjectorEager(as_injector_key(key))))
3161
+
3162
+
3163
+ ###
3164
+ # api
3165
+
3166
+
3167
+ class InjectionApi:
3168
+ # keys
3169
+
3170
+ def as_key(self, o: ta.Any) -> InjectorKey:
3171
+ return as_injector_key(o)
3172
+
3173
+ def array(self, o: ta.Any) -> InjectorKey:
3174
+ return dc.replace(as_injector_key(o), array=True)
3175
+
3176
+ def tag(self, o: ta.Any, t: ta.Any) -> InjectorKey:
3177
+ return dc.replace(as_injector_key(o), tag=t)
3178
+
3179
+ # bindings
3180
+
3181
+ def as_bindings(self, *args: InjectorBindingOrBindings) -> InjectorBindings:
3182
+ return as_injector_bindings(*args)
3183
+
3184
+ # overrides
3185
+
3186
+ def override(self, p: InjectorBindings, *args: InjectorBindingOrBindings) -> InjectorBindings:
3187
+ return injector_override(p, *args)
3188
+
3189
+ # scopes
3190
+
3191
+ def bind_scope(self, sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
3192
+ return bind_injector_scope(sc)
3193
+
3194
+ def bind_scope_seed(self, k: ta.Any, sc: ta.Type[InjectorScope]) -> InjectorBindingOrBindings:
3195
+ return bind_injector_scope_seed(k, sc)
3196
+
3197
+ # injector
3198
+
3199
+ def create_injector(self, *args: InjectorBindingOrBindings, parent: ta.Optional[Injector] = None) -> Injector:
3200
+ return _Injector(as_injector_bindings(*args), parent)
3201
+
3202
+ # binder
3203
+
3204
+ def bind(
3205
+ self,
3206
+ obj: ta.Any,
3207
+ *,
3208
+ key: ta.Any = None,
3209
+ tag: ta.Any = None,
3210
+ array: ta.Optional[bool] = None, # noqa
3211
+
3212
+ to_fn: ta.Any = None,
3213
+ to_ctor: ta.Any = None,
3214
+ to_const: ta.Any = None,
3215
+ to_key: ta.Any = None,
3216
+
3217
+ in_: ta.Optional[ta.Type[InjectorScope]] = None,
3218
+ singleton: bool = False,
3219
+
3220
+ eager: bool = False,
3221
+ ) -> InjectorBindingOrBindings:
3222
+ return InjectorBinder.bind(
3223
+ obj,
3224
+
3225
+ key=key,
3226
+ tag=tag,
3227
+ array=array,
3228
+
3229
+ to_fn=to_fn,
3230
+ to_ctor=to_ctor,
3231
+ to_const=to_const,
3232
+ to_key=to_key,
3233
+
3234
+ in_=in_,
3235
+ singleton=singleton,
3236
+
3237
+ eager=eager,
3238
+ )
3239
+
3240
+ # helpers
3241
+
3242
+ def bind_factory(
3243
+ self,
3244
+ fn: ta.Callable[..., T],
3245
+ cls_: U,
3246
+ ann: ta.Any = None,
3247
+ ) -> InjectorBindingOrBindings:
3248
+ return self.bind(make_injector_factory(fn, cls_, ann))
3249
+
3250
+ def bind_array(
3251
+ self,
3252
+ obj: ta.Any = None,
3253
+ *,
3254
+ tag: ta.Any = None,
3255
+ ) -> InjectorBindingOrBindings:
3256
+ return bind_injector_array(obj, tag=tag)
3257
+
3258
+ def bind_array_type(
3259
+ self,
3260
+ ele: ta.Union[InjectorKey, InjectorKeyCls],
3261
+ cls_: U,
3262
+ ann: ta.Any = None,
3263
+ ) -> InjectorBindingOrBindings:
3264
+ return self.bind(make_injector_array_type(ele, cls_, ann))
3265
+
3266
+
3267
+ inj = InjectionApi()
3268
+
3269
+
3270
+ ########################################
3271
+ # ../../../omlish/lite/runtime.py
3272
+
3273
+
3274
+ @cached_nullary
3275
+ def is_debugger_attached() -> bool:
3276
+ return any(frame[1].endswith('pydevd.py') for frame in inspect.stack())
3277
+
3278
+
3279
+ LITE_REQUIRED_PYTHON_VERSION = (3, 8)
3280
+
3281
+
3282
+ def check_lite_runtime_version() -> None:
3283
+ if sys.version_info < LITE_REQUIRED_PYTHON_VERSION:
3284
+ raise OSError(f'Requires python {LITE_REQUIRED_PYTHON_VERSION}, got {sys.version_info} from {sys.executable}') # noqa
3285
+
3286
+
3287
+ ########################################
3288
+ # ../../../omlish/logs/json.py
3289
+ """
3290
+ TODO:
3291
+ - translate json keys
3292
+ """
3293
+
3294
+
3295
+ class JsonLogFormatter(logging.Formatter):
3296
+ KEYS: ta.Mapping[str, bool] = {
3297
+ 'name': False,
3298
+ 'msg': False,
3299
+ 'args': False,
3300
+ 'levelname': False,
3301
+ 'levelno': False,
3302
+ 'pathname': False,
3303
+ 'filename': False,
3304
+ 'module': False,
3305
+ 'exc_info': True,
3306
+ 'exc_text': True,
3307
+ 'stack_info': True,
3308
+ 'lineno': False,
3309
+ 'funcName': False,
3310
+ 'created': False,
3311
+ 'msecs': False,
3312
+ 'relativeCreated': False,
3313
+ 'thread': False,
3314
+ 'threadName': False,
3315
+ 'processName': False,
3316
+ 'process': False,
3317
+ }
3318
+
3319
+ def __init__(
3320
+ self,
3321
+ *args: ta.Any,
3322
+ json_dumps: ta.Optional[ta.Callable[[ta.Any], str]] = None,
3323
+ **kwargs: ta.Any,
3324
+ ) -> None:
3325
+ super().__init__(*args, **kwargs)
3326
+
3327
+ if json_dumps is None:
3328
+ json_dumps = json_dumps_compact
3329
+ self._json_dumps = json_dumps
3330
+
3331
+ def format(self, record: logging.LogRecord) -> str:
3332
+ dct = {
3333
+ k: v
3334
+ for k, o in self.KEYS.items()
3335
+ for v in [getattr(record, k)]
3336
+ if not (o and v is None)
3337
+ }
3338
+ return self._json_dumps(dct)
3339
+
3340
+
3341
+ ########################################
3342
+ # ../types.py
3343
+
3344
+
3345
+ ##
3346
+
3347
+
3348
+ # See https://peps.python.org/pep-3149/
3349
+ INTERP_OPT_GLYPHS_BY_ATTR: ta.Mapping[str, str] = collections.OrderedDict([
3350
+ ('debug', 'd'),
3351
+ ('threaded', 't'),
3352
+ ])
3353
+
3354
+ INTERP_OPT_ATTRS_BY_GLYPH: ta.Mapping[str, str] = collections.OrderedDict(
3355
+ (g, a) for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items()
3356
+ )
3357
+
3358
+
3359
+ @dc.dataclass(frozen=True)
3360
+ class InterpOpts:
3361
+ threaded: bool = False
3362
+ debug: bool = False
3363
+
3364
+ def __str__(self) -> str:
3365
+ return ''.join(g for a, g in INTERP_OPT_GLYPHS_BY_ATTR.items() if getattr(self, a))
3366
+
3367
+ @classmethod
3368
+ def parse(cls, s: str) -> 'InterpOpts':
3369
+ return cls(**{INTERP_OPT_ATTRS_BY_GLYPH[g]: True for g in s})
3370
+
3371
+ @classmethod
3372
+ def parse_suffix(cls, s: str) -> ta.Tuple[str, 'InterpOpts']:
3373
+ kw = {}
3374
+ while s and (a := INTERP_OPT_ATTRS_BY_GLYPH.get(s[-1])):
3375
+ s, kw[a] = s[:-1], True
3376
+ return s, cls(**kw)
3377
+
3378
+
3379
+ ##
3380
+
3381
+
3382
+ @dc.dataclass(frozen=True)
3383
+ class InterpVersion:
3384
+ version: Version
3385
+ opts: InterpOpts
3386
+
3387
+ def __str__(self) -> str:
3388
+ return str(self.version) + str(self.opts)
3389
+
3390
+ @classmethod
3391
+ def parse(cls, s: str) -> 'InterpVersion':
3392
+ s, o = InterpOpts.parse_suffix(s)
3393
+ v = Version(s)
3394
+ return cls(
3395
+ version=v,
3396
+ opts=o,
3397
+ )
3398
+
3399
+ @classmethod
3400
+ def try_parse(cls, s: str) -> ta.Optional['InterpVersion']:
3401
+ try:
3402
+ return cls.parse(s)
3403
+ except (KeyError, InvalidVersion):
3404
+ return None
3405
+
3406
+
3407
+ ##
3408
+
3409
+
3410
+ @dc.dataclass(frozen=True)
3411
+ class InterpSpecifier:
3412
+ specifier: Specifier
3413
+ opts: InterpOpts
3414
+
3415
+ def __str__(self) -> str:
3416
+ return str(self.specifier) + str(self.opts)
3417
+
3418
+ @classmethod
3419
+ def parse(cls, s: str) -> 'InterpSpecifier':
3420
+ s, o = InterpOpts.parse_suffix(s)
3421
+ if not any(s.startswith(o) for o in Specifier.OPERATORS):
3422
+ s = '~=' + s
3423
+ if s.count('.') < 2:
3424
+ s += '.0'
3425
+ return cls(
3426
+ specifier=Specifier(s),
3427
+ opts=o,
3428
+ )
3429
+
3430
+ def contains(self, iv: InterpVersion) -> bool:
3431
+ return self.specifier.contains(iv.version) and self.opts == iv.opts
3432
+
3433
+ def __contains__(self, iv: InterpVersion) -> bool:
3434
+ return self.contains(iv)
3435
+
3436
+
3437
+ ##
3438
+
3439
+
3440
+ @dc.dataclass(frozen=True)
3441
+ class Interp:
3442
+ exe: str
3443
+ version: InterpVersion
3444
+
3445
+
3446
+ ########################################
3447
+ # ../uv/inject.py
3448
+
3449
+
3450
+ def bind_interp_uv() -> InjectorBindings:
3451
+ lst: ta.List[InjectorBindingOrBindings] = []
3452
+
3453
+ return inj.as_bindings(*lst)
3454
+
3455
+
3456
+ ########################################
3457
+ # ../../../omlish/logs/standard.py
3458
+ """
3459
+ TODO:
3460
+ - structured
3461
+ - prefixed
3462
+ - debug
3463
+ - optional noisy? noisy will never be lite - some kinda configure_standard callback mechanism?
3464
+ """
3465
+
3466
+
3467
+ ##
3468
+
3469
+
3470
+ STANDARD_LOG_FORMAT_PARTS = [
3471
+ ('asctime', '%(asctime)-15s'),
3472
+ ('process', 'pid=%(process)-6s'),
3473
+ ('thread', 'tid=%(thread)x'),
3474
+ ('levelname', '%(levelname)s'),
3475
+ ('name', '%(name)s'),
3476
+ ('separator', '::'),
3477
+ ('message', '%(message)s'),
3478
+ ]
3479
+
3480
+
3481
+ class StandardLogFormatter(logging.Formatter):
3482
+ @staticmethod
3483
+ def build_log_format(parts: ta.Iterable[ta.Tuple[str, str]]) -> str:
3484
+ return ' '.join(v for k, v in parts)
3485
+
3486
+ converter = datetime.datetime.fromtimestamp # type: ignore
3487
+
3488
+ def formatTime(self, record, datefmt=None):
3489
+ ct = self.converter(record.created) # type: ignore
3490
+ if datefmt:
3491
+ return ct.strftime(datefmt) # noqa
3492
+ else:
3493
+ t = ct.strftime('%Y-%m-%d %H:%M:%S')
3494
+ return '%s.%03d' % (t, record.msecs) # noqa
3495
+
3496
+
3497
+ ##
3498
+
3499
+
3500
+ class StandardConfiguredLogHandler(ProxyLogHandler):
3501
+ def __init_subclass__(cls, **kwargs):
3502
+ raise TypeError('This class serves only as a marker and should not be subclassed.')
3503
+
3504
+
3505
+ ##
3506
+
3507
+
3508
+ @contextlib.contextmanager
3509
+ def _locking_logging_module_lock() -> ta.Iterator[None]:
3510
+ if hasattr(logging, '_acquireLock'):
3511
+ logging._acquireLock() # noqa
3512
+ try:
3513
+ yield
3514
+ finally:
3515
+ logging._releaseLock() # type: ignore # noqa
3516
+
3517
+ elif hasattr(logging, '_lock'):
3518
+ # https://github.com/python/cpython/commit/74723e11109a320e628898817ab449b3dad9ee96
3519
+ with logging._lock: # noqa
3520
+ yield
3521
+
3522
+ else:
3523
+ raise Exception("Can't find lock in logging module")
3524
+
3525
+
3526
+ def configure_standard_logging(
3527
+ level: ta.Union[int, str] = logging.INFO,
3528
+ *,
3529
+ json: bool = False,
3530
+ target: ta.Optional[logging.Logger] = None,
3531
+ force: bool = False,
3532
+ handler_factory: ta.Optional[ta.Callable[[], logging.Handler]] = None,
3533
+ ) -> ta.Optional[StandardConfiguredLogHandler]:
3534
+ with _locking_logging_module_lock():
3535
+ if target is None:
3536
+ target = logging.root
3537
+
3538
+ #
3539
+
3540
+ if not force:
3541
+ if any(isinstance(h, StandardConfiguredLogHandler) for h in list(target.handlers)):
3542
+ return None
3543
+
3544
+ #
3545
+
3546
+ if handler_factory is not None:
3547
+ handler = handler_factory()
3548
+ else:
3549
+ handler = logging.StreamHandler()
3550
+
3551
+ #
3552
+
3553
+ formatter: logging.Formatter
2128
3554
  if json:
2129
3555
  formatter = JsonLogFormatter()
2130
3556
  else:
@@ -2477,6 +3903,50 @@ class AbstractAsyncSubprocesses(BaseSubprocesses):
2477
3903
  return ret.decode().strip()
2478
3904
 
2479
3905
 
3906
+ ########################################
3907
+ # ../providers/base.py
3908
+ """
3909
+ TODO:
3910
+ - backends
3911
+ - local builds
3912
+ - deadsnakes?
3913
+ - uv
3914
+ - loose versions
3915
+ """
3916
+
3917
+
3918
+ ##
3919
+
3920
+
3921
+ class InterpProvider(abc.ABC):
3922
+ name: ta.ClassVar[str]
3923
+
3924
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
3925
+ super().__init_subclass__(**kwargs)
3926
+ if abc.ABC not in cls.__bases__ and 'name' not in cls.__dict__:
3927
+ sfx = 'InterpProvider'
3928
+ if not cls.__name__.endswith(sfx):
3929
+ raise NameError(cls)
3930
+ setattr(cls, 'name', snake_case(cls.__name__[:-len(sfx)]))
3931
+
3932
+ @abc.abstractmethod
3933
+ def get_installed_versions(self, spec: InterpSpecifier) -> ta.Awaitable[ta.Sequence[InterpVersion]]:
3934
+ raise NotImplementedError
3935
+
3936
+ @abc.abstractmethod
3937
+ def get_installed_version(self, version: InterpVersion) -> ta.Awaitable[Interp]:
3938
+ raise NotImplementedError
3939
+
3940
+ async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
3941
+ return []
3942
+
3943
+ async def install_version(self, version: InterpVersion) -> Interp:
3944
+ raise TypeError
3945
+
3946
+
3947
+ InterpProviders = ta.NewType('InterpProviders', ta.Sequence[InterpProvider])
3948
+
3949
+
2480
3950
  ########################################
2481
3951
  # ../../../omlish/asyncs/asyncio/subprocesses.py
2482
3952
 
@@ -2793,72 +4263,242 @@ class InterpInspector:
2793
4263
  return ret
2794
4264
 
2795
4265
 
2796
- INTERP_INSPECTOR = InterpInspector()
4266
+ ########################################
4267
+ # ../resolvers.py
4268
+
4269
+
4270
+ @dc.dataclass(frozen=True)
4271
+ class InterpResolverProviders:
4272
+ providers: ta.Sequence[ta.Tuple[str, InterpProvider]]
4273
+
4274
+
4275
+ class InterpResolver:
4276
+ def __init__(
4277
+ self,
4278
+ providers: InterpResolverProviders,
4279
+ ) -> None:
4280
+ super().__init__()
4281
+
4282
+ self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers.providers)
4283
+
4284
+ async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
4285
+ lst = [
4286
+ (i, si)
4287
+ for i, p in enumerate(self._providers.values())
4288
+ for si in await p.get_installed_versions(spec)
4289
+ if spec.contains(si)
4290
+ ]
4291
+
4292
+ slst = sorted(lst, key=lambda t: (-t[0], t[1].version))
4293
+ if not slst:
4294
+ return None
4295
+
4296
+ bi, bv = slst[-1]
4297
+ bp = list(self._providers.values())[bi]
4298
+ return (bp, bv)
4299
+
4300
+ async def resolve(
4301
+ self,
4302
+ spec: InterpSpecifier,
4303
+ *,
4304
+ install: bool = False,
4305
+ ) -> ta.Optional[Interp]:
4306
+ tup = await self._resolve_installed(spec)
4307
+ if tup is not None:
4308
+ bp, bv = tup
4309
+ return await bp.get_installed_version(bv)
4310
+
4311
+ if not install:
4312
+ return None
4313
+
4314
+ tp = list(self._providers.values())[0] # noqa
4315
+
4316
+ sv = sorted(
4317
+ [s for s in await tp.get_installable_versions(spec) if s in spec],
4318
+ key=lambda s: s.version,
4319
+ )
4320
+ if not sv:
4321
+ return None
4322
+
4323
+ bv = sv[-1]
4324
+ return await tp.install_version(bv)
4325
+
4326
+ async def list(self, spec: InterpSpecifier) -> None:
4327
+ print('installed:')
4328
+ for n, p in self._providers.items():
4329
+ lst = [
4330
+ si
4331
+ for si in await p.get_installed_versions(spec)
4332
+ if spec.contains(si)
4333
+ ]
4334
+ if lst:
4335
+ print(f' {n}')
4336
+ for si in lst:
4337
+ print(f' {si}')
4338
+
4339
+ print()
4340
+
4341
+ print('installable:')
4342
+ for n, p in self._providers.items():
4343
+ lst = [
4344
+ si
4345
+ for si in await p.get_installable_versions(spec)
4346
+ if spec.contains(si)
4347
+ ]
4348
+ if lst:
4349
+ print(f' {n}')
4350
+ for si in lst:
4351
+ print(f' {si}')
4352
+
4353
+
4354
+ ########################################
4355
+ # ../providers/running.py
4356
+
4357
+
4358
+ class RunningInterpProvider(InterpProvider):
4359
+ @cached_nullary
4360
+ def version(self) -> InterpVersion:
4361
+ return InterpInspector.running().iv
4362
+
4363
+ async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
4364
+ return [self.version()]
4365
+
4366
+ async def get_installed_version(self, version: InterpVersion) -> Interp:
4367
+ if version != self.version():
4368
+ raise KeyError(version)
4369
+ return Interp(
4370
+ exe=sys.executable,
4371
+ version=self.version(),
4372
+ )
2797
4373
 
2798
4374
 
2799
4375
  ########################################
2800
- # ../providers.py
4376
+ # ../providers/system.py
2801
4377
  """
2802
4378
  TODO:
2803
- - backends
2804
- - local builds
2805
- - deadsnakes?
2806
- - uv
2807
- - loose versions
4379
+ - python, python3, python3.12, ...
4380
+ - check if path py's are venvs: sys.prefix != sys.base_prefix
2808
4381
  """
2809
4382
 
2810
4383
 
2811
4384
  ##
2812
4385
 
2813
4386
 
2814
- class InterpProvider(abc.ABC):
2815
- name: ta.ClassVar[str]
4387
+ class SystemInterpProvider(InterpProvider):
4388
+ @dc.dataclass(frozen=True)
4389
+ class Options:
4390
+ cmd: str = 'python3' # FIXME: unused lol
4391
+ path: ta.Optional[str] = None
2816
4392
 
2817
- def __init_subclass__(cls, **kwargs: ta.Any) -> None:
2818
- super().__init_subclass__(**kwargs)
2819
- if abc.ABC not in cls.__bases__ and 'name' not in cls.__dict__:
2820
- sfx = 'InterpProvider'
2821
- if not cls.__name__.endswith(sfx):
2822
- raise NameError(cls)
2823
- setattr(cls, 'name', snake_case(cls.__name__[:-len(sfx)]))
4393
+ inspect: bool = False
2824
4394
 
2825
- @abc.abstractmethod
2826
- def get_installed_versions(self, spec: InterpSpecifier) -> ta.Awaitable[ta.Sequence[InterpVersion]]:
2827
- raise NotImplementedError
4395
+ def __init__(
4396
+ self,
4397
+ options: Options = Options(),
4398
+ *,
4399
+ inspector: ta.Optional[InterpInspector] = None,
4400
+ ) -> None:
4401
+ super().__init__()
2828
4402
 
2829
- @abc.abstractmethod
2830
- def get_installed_version(self, version: InterpVersion) -> ta.Awaitable[Interp]:
2831
- raise NotImplementedError
4403
+ self._options = options
4404
+
4405
+ self._inspector = inspector
4406
+
4407
+ #
4408
+
4409
+ @staticmethod
4410
+ def _re_which(
4411
+ pat: re.Pattern,
4412
+ *,
4413
+ mode: int = os.F_OK | os.X_OK,
4414
+ path: ta.Optional[str] = None,
4415
+ ) -> ta.List[str]:
4416
+ if path is None:
4417
+ path = os.environ.get('PATH', None)
4418
+ if path is None:
4419
+ try:
4420
+ path = os.confstr('CS_PATH')
4421
+ except (AttributeError, ValueError):
4422
+ path = os.defpath
4423
+
4424
+ if not path:
4425
+ return []
4426
+
4427
+ path = os.fsdecode(path)
4428
+ pathlst = path.split(os.pathsep)
4429
+
4430
+ def _access_check(fn: str, mode: int) -> bool:
4431
+ return os.path.exists(fn) and os.access(fn, mode)
4432
+
4433
+ out = []
4434
+ seen = set()
4435
+ for d in pathlst:
4436
+ normdir = os.path.normcase(d)
4437
+ if normdir not in seen:
4438
+ seen.add(normdir)
4439
+ if not _access_check(normdir, mode):
4440
+ continue
4441
+ for thefile in os.listdir(d):
4442
+ name = os.path.join(d, thefile)
4443
+ if not (
4444
+ os.path.isfile(name) and
4445
+ pat.fullmatch(thefile) and
4446
+ _access_check(name, mode)
4447
+ ):
4448
+ continue
4449
+ out.append(name)
2832
4450
 
2833
- async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
2834
- return []
4451
+ return out
2835
4452
 
2836
- async def install_version(self, version: InterpVersion) -> Interp:
2837
- raise TypeError
4453
+ @cached_nullary
4454
+ def exes(self) -> ta.List[str]:
4455
+ return self._re_which(
4456
+ re.compile(r'python3(\.\d+)?'),
4457
+ path=self._options.path,
4458
+ )
2838
4459
 
4460
+ #
2839
4461
 
2840
- ##
4462
+ async def get_exe_version(self, exe: str) -> ta.Optional[InterpVersion]:
4463
+ if not self._options.inspect:
4464
+ s = os.path.basename(exe)
4465
+ if s.startswith('python'):
4466
+ s = s[len('python'):]
4467
+ if '.' in s:
4468
+ try:
4469
+ return InterpVersion.parse(s)
4470
+ except InvalidVersion:
4471
+ pass
4472
+ ii = await check.not_none(self._inspector).inspect(exe)
4473
+ return ii.iv if ii is not None else None
2841
4474
 
4475
+ async def exe_versions(self) -> ta.Sequence[ta.Tuple[str, InterpVersion]]:
4476
+ lst = []
4477
+ for e in self.exes():
4478
+ if (ev := await self.get_exe_version(e)) is None:
4479
+ log.debug('Invalid system version: %s', e)
4480
+ continue
4481
+ lst.append((e, ev))
4482
+ return lst
2842
4483
 
2843
- class RunningInterpProvider(InterpProvider):
2844
- @cached_nullary
2845
- def version(self) -> InterpVersion:
2846
- return InterpInspector.running().iv
4484
+ #
2847
4485
 
2848
4486
  async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
2849
- return [self.version()]
4487
+ return [ev for e, ev in await self.exe_versions()]
2850
4488
 
2851
4489
  async def get_installed_version(self, version: InterpVersion) -> Interp:
2852
- if version != self.version():
2853
- raise KeyError(version)
2854
- return Interp(
2855
- exe=sys.executable,
2856
- version=self.version(),
2857
- )
4490
+ for e, ev in await self.exe_versions():
4491
+ if ev != version:
4492
+ continue
4493
+ return Interp(
4494
+ exe=e,
4495
+ version=ev,
4496
+ )
4497
+ raise KeyError(version)
2858
4498
 
2859
4499
 
2860
4500
  ########################################
2861
- # ../pyenv.py
4501
+ # ../pyenv/pyenv.py
2862
4502
  """
2863
4503
  TODO:
2864
4504
  - custom tags
@@ -3088,9 +4728,10 @@ class PyenvVersionInstaller:
3088
4728
  opts: ta.Optional[PyenvInstallOpts] = None,
3089
4729
  interp_opts: InterpOpts = InterpOpts(),
3090
4730
  *,
4731
+ pyenv: Pyenv,
4732
+
3091
4733
  install_name: ta.Optional[str] = None,
3092
4734
  no_default_opts: bool = False,
3093
- pyenv: Pyenv = Pyenv(),
3094
4735
  ) -> None:
3095
4736
  super().__init__()
3096
4737
 
@@ -3180,26 +4821,26 @@ class PyenvVersionInstaller:
3180
4821
 
3181
4822
 
3182
4823
  class PyenvInterpProvider(InterpProvider):
3183
- def __init__(
3184
- self,
3185
- pyenv: Pyenv = Pyenv(),
4824
+ @dc.dataclass(frozen=True)
4825
+ class Options:
4826
+ inspect: bool = False
3186
4827
 
3187
- inspect: bool = False,
3188
- inspector: InterpInspector = INTERP_INSPECTOR,
4828
+ try_update: bool = False
3189
4829
 
4830
+ def __init__(
4831
+ self,
4832
+ options: Options = Options(),
3190
4833
  *,
3191
-
3192
- try_update: bool = False,
4834
+ pyenv: Pyenv,
4835
+ inspector: InterpInspector,
3193
4836
  ) -> None:
3194
4837
  super().__init__()
3195
4838
 
3196
- self._pyenv = pyenv
4839
+ self._options = options
3197
4840
 
3198
- self._inspect = inspect
4841
+ self._pyenv = pyenv
3199
4842
  self._inspector = inspector
3200
4843
 
3201
- self._try_update = try_update
3202
-
3203
4844
  #
3204
4845
 
3205
4846
  @staticmethod
@@ -3224,7 +4865,7 @@ class PyenvInterpProvider(InterpProvider):
3224
4865
 
3225
4866
  async def _make_installed(self, vn: str, ep: str) -> ta.Optional[Installed]:
3226
4867
  iv: ta.Optional[InterpVersion]
3227
- if self._inspect:
4868
+ if self._options.inspect:
3228
4869
  try:
3229
4870
  iv = check.not_none(await self._inspector.inspect(ep)).iv
3230
4871
  except Exception as e: # noqa
@@ -3280,7 +4921,7 @@ class PyenvInterpProvider(InterpProvider):
3280
4921
  async def get_installable_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
3281
4922
  lst = await self._get_installable_versions(spec)
3282
4923
 
3283
- if self._try_update and not any(v in spec for v in lst):
4924
+ if self._options.try_update and not any(v in spec for v in lst):
3284
4925
  if self._pyenv.update():
3285
4926
  lst = await self._get_installable_versions(spec)
3286
4927
 
@@ -3296,6 +4937,7 @@ class PyenvInterpProvider(InterpProvider):
3296
4937
  installer = PyenvVersionInstaller(
3297
4938
  inst_version,
3298
4939
  interp_opts=inst_opts,
4940
+ pyenv=self._pyenv,
3299
4941
  )
3300
4942
 
3301
4943
  exe = await installer.install()
@@ -3303,266 +4945,126 @@ class PyenvInterpProvider(InterpProvider):
3303
4945
 
3304
4946
 
3305
4947
  ########################################
3306
- # ../system.py
3307
- """
3308
- TODO:
3309
- - python, python3, python3.12, ...
3310
- - check if path py's are venvs: sys.prefix != sys.base_prefix
3311
- """
3312
-
3313
-
3314
- ##
3315
-
3316
-
3317
- @dc.dataclass(frozen=True)
3318
- class SystemInterpProvider(InterpProvider):
3319
- cmd: str = 'python3'
3320
- path: ta.Optional[str] = None
3321
-
3322
- inspect: bool = False
3323
- inspector: InterpInspector = INTERP_INSPECTOR
3324
-
3325
- #
3326
-
3327
- @staticmethod
3328
- def _re_which(
3329
- pat: re.Pattern,
3330
- *,
3331
- mode: int = os.F_OK | os.X_OK,
3332
- path: ta.Optional[str] = None,
3333
- ) -> ta.List[str]:
3334
- if path is None:
3335
- path = os.environ.get('PATH', None)
3336
- if path is None:
3337
- try:
3338
- path = os.confstr('CS_PATH')
3339
- except (AttributeError, ValueError):
3340
- path = os.defpath
3341
-
3342
- if not path:
3343
- return []
3344
-
3345
- path = os.fsdecode(path)
3346
- pathlst = path.split(os.pathsep)
3347
-
3348
- def _access_check(fn: str, mode: int) -> bool:
3349
- return os.path.exists(fn) and os.access(fn, mode)
3350
-
3351
- out = []
3352
- seen = set()
3353
- for d in pathlst:
3354
- normdir = os.path.normcase(d)
3355
- if normdir not in seen:
3356
- seen.add(normdir)
3357
- if not _access_check(normdir, mode):
3358
- continue
3359
- for thefile in os.listdir(d):
3360
- name = os.path.join(d, thefile)
3361
- if not (
3362
- os.path.isfile(name) and
3363
- pat.fullmatch(thefile) and
3364
- _access_check(name, mode)
3365
- ):
3366
- continue
3367
- out.append(name)
3368
-
3369
- return out
3370
-
3371
- @cached_nullary
3372
- def exes(self) -> ta.List[str]:
3373
- return self._re_which(
3374
- re.compile(r'python3(\.\d+)?'),
3375
- path=self.path,
3376
- )
3377
-
3378
- #
4948
+ # ../providers/inject.py
3379
4949
 
3380
- async def get_exe_version(self, exe: str) -> ta.Optional[InterpVersion]:
3381
- if not self.inspect:
3382
- s = os.path.basename(exe)
3383
- if s.startswith('python'):
3384
- s = s[len('python'):]
3385
- if '.' in s:
3386
- try:
3387
- return InterpVersion.parse(s)
3388
- except InvalidVersion:
3389
- pass
3390
- ii = await self.inspector.inspect(exe)
3391
- return ii.iv if ii is not None else None
3392
4950
 
3393
- async def exe_versions(self) -> ta.Sequence[ta.Tuple[str, InterpVersion]]:
3394
- lst = []
3395
- for e in self.exes():
3396
- if (ev := await self.get_exe_version(e)) is None:
3397
- log.debug('Invalid system version: %s', e)
3398
- continue
3399
- lst.append((e, ev))
3400
- return lst
4951
+ def bind_interp_providers() -> InjectorBindings:
4952
+ lst: ta.List[InjectorBindingOrBindings] = [
4953
+ inj.bind_array(InterpProvider),
4954
+ inj.bind_array_type(InterpProvider, InterpProviders),
3401
4955
 
3402
- #
4956
+ inj.bind(RunningInterpProvider, singleton=True),
4957
+ inj.bind(InterpProvider, to_key=RunningInterpProvider, array=True),
3403
4958
 
3404
- async def get_installed_versions(self, spec: InterpSpecifier) -> ta.Sequence[InterpVersion]:
3405
- return [ev for e, ev in await self.exe_versions()]
4959
+ inj.bind(SystemInterpProvider, singleton=True),
4960
+ inj.bind(InterpProvider, to_key=SystemInterpProvider, array=True),
4961
+ ]
3406
4962
 
3407
- async def get_installed_version(self, version: InterpVersion) -> Interp:
3408
- for e, ev in await self.exe_versions():
3409
- if ev != version:
3410
- continue
3411
- return Interp(
3412
- exe=e,
3413
- version=ev,
3414
- )
3415
- raise KeyError(version)
4963
+ return inj.as_bindings(*lst)
3416
4964
 
3417
4965
 
3418
4966
  ########################################
3419
- # ../resolvers.py
4967
+ # ../pyenv/inject.py
3420
4968
 
3421
4969
 
3422
- INTERP_PROVIDER_TYPES_BY_NAME: ta.Mapping[str, ta.Type[InterpProvider]] = {
3423
- cls.name: cls for cls in deep_subclasses(InterpProvider) if abc.ABC not in cls.__bases__ # type: ignore
3424
- }
3425
-
4970
+ def bind_interp_pyenv() -> InjectorBindings:
4971
+ lst: ta.List[InjectorBindingOrBindings] = [
4972
+ inj.bind(Pyenv, singleton=True),
3426
4973
 
3427
- class InterpResolver:
3428
- def __init__(
3429
- self,
3430
- providers: ta.Sequence[ta.Tuple[str, InterpProvider]],
3431
- ) -> None:
3432
- super().__init__()
4974
+ inj.bind(PyenvInterpProvider, singleton=True),
4975
+ inj.bind(InterpProvider, to_key=PyenvInterpProvider, array=True),
4976
+ ]
3433
4977
 
3434
- self._providers: ta.Mapping[str, InterpProvider] = collections.OrderedDict(providers)
4978
+ return inj.as_bindings(*lst)
3435
4979
 
3436
- async def _resolve_installed(self, spec: InterpSpecifier) -> ta.Optional[ta.Tuple[InterpProvider, InterpVersion]]:
3437
- lst = [
3438
- (i, si)
3439
- for i, p in enumerate(self._providers.values())
3440
- for si in await p.get_installed_versions(spec)
3441
- if spec.contains(si)
3442
- ]
3443
4980
 
3444
- slst = sorted(lst, key=lambda t: (-t[0], t[1].version))
3445
- if not slst:
3446
- return None
4981
+ ########################################
4982
+ # ../inject.py
3447
4983
 
3448
- bi, bv = slst[-1]
3449
- bp = list(self._providers.values())[bi]
3450
- return (bp, bv)
3451
4984
 
3452
- async def resolve(
3453
- self,
3454
- spec: InterpSpecifier,
3455
- *,
3456
- install: bool = False,
3457
- ) -> ta.Optional[Interp]:
3458
- tup = await self._resolve_installed(spec)
3459
- if tup is not None:
3460
- bp, bv = tup
3461
- return await bp.get_installed_version(bv)
4985
+ def bind_interp() -> InjectorBindings:
4986
+ lst: ta.List[InjectorBindingOrBindings] = [
4987
+ bind_interp_providers(),
3462
4988
 
3463
- if not install:
3464
- return None
4989
+ bind_interp_pyenv(),
3465
4990
 
3466
- tp = list(self._providers.values())[0] # noqa
4991
+ bind_interp_uv(),
3467
4992
 
3468
- sv = sorted(
3469
- [s for s in await tp.get_installable_versions(spec) if s in spec],
3470
- key=lambda s: s.version,
3471
- )
3472
- if not sv:
3473
- return None
4993
+ inj.bind(InterpInspector, singleton=True),
4994
+ ]
3474
4995
 
3475
- bv = sv[-1]
3476
- return await tp.install_version(bv)
4996
+ #
3477
4997
 
3478
- async def list(self, spec: InterpSpecifier) -> None:
3479
- print('installed:')
3480
- for n, p in self._providers.items():
3481
- lst = [
3482
- si
3483
- for si in await p.get_installed_versions(spec)
3484
- if spec.contains(si)
4998
+ def provide_interp_resolver_providers(injector: Injector) -> InterpResolverProviders:
4999
+ # FIXME: lol
5000
+ rps: ta.List[ta.Any] = [
5001
+ injector.provide(c)
5002
+ for c in [
5003
+ PyenvInterpProvider,
5004
+ RunningInterpProvider,
5005
+ SystemInterpProvider,
3485
5006
  ]
3486
- if lst:
3487
- print(f' {n}')
3488
- for si in lst:
3489
- print(f' {si}')
3490
-
3491
- print()
5007
+ ]
3492
5008
 
3493
- print('installable:')
3494
- for n, p in self._providers.items():
3495
- lst = [
3496
- si
3497
- for si in await p.get_installable_versions(spec)
3498
- if spec.contains(si)
3499
- ]
3500
- if lst:
3501
- print(f' {n}')
3502
- for si in lst:
3503
- print(f' {si}')
5009
+ return InterpResolverProviders([(rp.name, rp) for rp in rps])
3504
5010
 
5011
+ lst.append(inj.bind(provide_interp_resolver_providers, singleton=True))
3505
5012
 
3506
- DEFAULT_INTERP_RESOLVER = InterpResolver([(p.name, p) for p in [
3507
- # pyenv is preferred to system interpreters as it tends to have more support for things like tkinter
3508
- PyenvInterpProvider(try_update=True),
5013
+ lst.extend([
5014
+ inj.bind(InterpResolver, singleton=True),
5015
+ ])
3509
5016
 
3510
- RunningInterpProvider(),
5017
+ #
3511
5018
 
3512
- SystemInterpProvider(),
3513
- ]])
5019
+ return inj.as_bindings(*lst)
3514
5020
 
3515
5021
 
3516
5022
  ########################################
3517
5023
  # cli.py
3518
5024
 
3519
5025
 
3520
- async def _list_cmd(args) -> None:
3521
- r = DEFAULT_INTERP_RESOLVER
3522
- s = InterpSpecifier.parse(args.version)
3523
- await r.list(s)
3524
-
3525
-
3526
- async def _resolve_cmd(args) -> None:
3527
- if args.provider:
3528
- p = INTERP_PROVIDER_TYPES_BY_NAME[args.provider]()
3529
- r = InterpResolver([(p.name, p)])
3530
- else:
3531
- r = DEFAULT_INTERP_RESOLVER
3532
- s = InterpSpecifier.parse(args.version)
3533
- print(check.not_none(await r.resolve(s, install=bool(args.install))).exe)
3534
-
3535
-
3536
- def _build_parser() -> argparse.ArgumentParser:
3537
- parser = argparse.ArgumentParser()
3538
-
3539
- subparsers = parser.add_subparsers()
5026
+ class InterpCli(ArgparseCli):
5027
+ @cached_nullary
5028
+ def injector(self) -> Injector:
5029
+ return inj.create_injector(bind_interp())
3540
5030
 
3541
- parser_list = subparsers.add_parser('list')
3542
- parser_list.add_argument('version')
3543
- parser_list.add_argument('-d', '--debug', action='store_true')
3544
- parser_list.set_defaults(func=_list_cmd)
5031
+ @cached_nullary
5032
+ def providers(self) -> InterpResolverProviders:
5033
+ return self.injector()[InterpResolverProviders]
3545
5034
 
3546
- parser_resolve = subparsers.add_parser('resolve')
3547
- parser_resolve.add_argument('version')
3548
- parser_resolve.add_argument('-p', '--provider')
3549
- parser_resolve.add_argument('-d', '--debug', action='store_true')
3550
- parser_resolve.add_argument('-i', '--install', action='store_true')
3551
- parser_resolve.set_defaults(func=_resolve_cmd)
5035
+ #
3552
5036
 
3553
- return parser
5037
+ @argparse_command(
5038
+ argparse_arg('version'),
5039
+ argparse_arg('-d', '--debug', action='store_true'),
5040
+ )
5041
+ async def list(self) -> None:
5042
+ r = InterpResolver(self.providers())
5043
+ s = InterpSpecifier.parse(self.args.version)
5044
+ await r.list(s)
5045
+
5046
+ @argparse_command(
5047
+ argparse_arg('version'),
5048
+ argparse_arg('-p', '--provider'),
5049
+ argparse_arg('-d', '--debug', action='store_true'),
5050
+ argparse_arg('-i', '--install', action='store_true'),
5051
+ )
5052
+ async def resolve(self) -> None:
5053
+ if self.args.provider:
5054
+ p = check.single([p for n, p in self.providers().providers if n == self.args.provider])
5055
+ r = InterpResolver(InterpResolverProviders([(p.name, p)]))
5056
+ else:
5057
+ r = InterpResolver(self.providers())
5058
+ s = InterpSpecifier.parse(self.args.version)
5059
+ print(check.not_none(await r.resolve(s, install=bool(self.args.install))).exe)
3554
5060
 
3555
5061
 
3556
5062
  async def _async_main(argv: ta.Optional[ta.Sequence[str]] = None) -> None:
3557
5063
  check_lite_runtime_version()
3558
5064
  configure_standard_logging()
3559
5065
 
3560
- parser = _build_parser()
3561
- args = parser.parse_args(argv)
3562
- if not getattr(args, 'func', None):
3563
- parser.print_help()
3564
- else:
3565
- await args.func(args)
5066
+ cli = ArgparseCli(argv)
5067
+ await cli.async_cli_run()
3566
5068
 
3567
5069
 
3568
5070
  def _main(argv: ta.Optional[ta.Sequence[str]] = None) -> None: