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/amalg/main.py +1 -0
- omdev/cexts/importhook.py +1 -1
- omdev/interp/cli.py +44 -43
- omdev/interp/default.py +10 -0
- omdev/interp/inject.py +54 -0
- omdev/interp/inspect.py +0 -3
- omdev/interp/providers/__init__.py +0 -0
- omdev/interp/{providers.py → providers/base.py} +4 -24
- omdev/interp/providers/inject.py +26 -0
- omdev/interp/providers/running.py +27 -0
- omdev/interp/{system.py → providers/system.py} +27 -15
- omdev/interp/pyenv/__init__.py +0 -0
- omdev/interp/pyenv/inject.py +21 -0
- omdev/interp/{pyenv.py → pyenv/pyenv.py} +24 -23
- omdev/interp/resolvers.py +7 -22
- omdev/interp/types.py +12 -0
- omdev/interp/uv/__init__.py +0 -0
- omdev/interp/uv/inject.py +12 -0
- omdev/interp/{uv.py → uv/uv.py} +4 -4
- omdev/pyproject/venvs.py +2 -2
- omdev/scripts/interp.py +1968 -466
- omdev/scripts/pyproject.py +3464 -2227
- {omdev-0.0.0.dev178.dist-info → omdev-0.0.0.dev179.dist-info}/METADATA +2 -2
- {omdev-0.0.0.dev178.dist-info → omdev-0.0.0.dev179.dist-info}/RECORD +29 -20
- /omdev/interp/{standalone.py → providers/standalone.py} +0 -0
- {omdev-0.0.0.dev178.dist-info → omdev-0.0.0.dev179.dist-info}/LICENSE +0 -0
- {omdev-0.0.0.dev178.dist-info → omdev-0.0.0.dev179.dist-info}/WHEEL +0 -0
- {omdev-0.0.0.dev178.dist-info → omdev-0.0.0.dev179.dist-info}/entry_points.txt +0 -0
- {omdev-0.0.0.dev178.dist-info → omdev-0.0.0.dev179.dist-info}/top_level.txt +0 -0
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/
|
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
|
-
|
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
|
-
|
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
|
-
|
1916
|
-
|
1917
|
-
|
1918
|
-
|
1919
|
-
|
1920
|
-
|
1921
|
-
|
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
|
-
|
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
|
-
|
1948
|
-
(
|
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
|
-
|
1953
|
-
|
1954
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1973
|
-
|
1974
|
-
|
1975
|
-
|
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
|
-
|
1978
|
-
return str(self.version) + str(self.opts)
|
2069
|
+
#
|
1979
2070
|
|
1980
|
-
|
1981
|
-
|
1982
|
-
|
1983
|
-
|
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
|
-
|
1990
|
-
|
1991
|
-
|
1992
|
-
|
1993
|
-
|
1994
|
-
|
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
|
-
|
1998
|
-
|
1999
|
-
|
2000
|
-
|
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
|
-
|
2003
|
-
|
2126
|
+
else:
|
2127
|
+
raise TypeError(obj)
|
2128
|
+
|
2129
|
+
#
|
2130
|
+
|
2131
|
+
_parser: ta.ClassVar[argparse.ArgumentParser]
|
2004
2132
|
|
2005
2133
|
@classmethod
|
2006
|
-
def
|
2007
|
-
|
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
|
-
|
2018
|
-
|
2137
|
+
@property
|
2138
|
+
def argv(self) -> ta.Sequence[str]:
|
2139
|
+
return self._argv
|
2019
2140
|
|
2020
|
-
|
2021
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
2063
|
-
|
2064
|
-
|
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
|
-
|
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
|
-
|
2092
|
-
|
2093
|
-
|
2094
|
-
|
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
|
-
|
2097
|
-
|
2213
|
+
tag: ta.Any = None
|
2214
|
+
array: bool = False
|
2098
2215
|
|
2099
2216
|
|
2100
|
-
def
|
2101
|
-
|
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
|
-
|
2115
|
-
|
2116
|
-
|
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
|
-
|
2121
|
-
handler = handler_factory()
|
2122
|
-
else:
|
2123
|
-
handler = logging.StreamHandler()
|
2227
|
+
##
|
2124
2228
|
|
2125
|
-
#
|
2126
2229
|
|
2127
|
-
|
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
|
-
|
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
|
-
-
|
2804
|
-
|
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
|
2815
|
-
|
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
|
-
|
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
|
-
|
2826
|
-
|
2827
|
-
|
4395
|
+
def __init__(
|
4396
|
+
self,
|
4397
|
+
options: Options = Options(),
|
4398
|
+
*,
|
4399
|
+
inspector: ta.Optional[InterpInspector] = None,
|
4400
|
+
) -> None:
|
4401
|
+
super().__init__()
|
2828
4402
|
|
2829
|
-
|
2830
|
-
|
2831
|
-
|
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
|
-
|
2834
|
-
return []
|
4451
|
+
return out
|
2835
4452
|
|
2836
|
-
|
2837
|
-
|
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
|
-
|
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.
|
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
|
-
|
2853
|
-
|
2854
|
-
|
2855
|
-
|
2856
|
-
|
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
|
-
|
3184
|
-
|
3185
|
-
|
4824
|
+
@dc.dataclass(frozen=True)
|
4825
|
+
class Options:
|
4826
|
+
inspect: bool = False
|
3186
4827
|
|
3187
|
-
|
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
|
-
|
4834
|
+
pyenv: Pyenv,
|
4835
|
+
inspector: InterpInspector,
|
3193
4836
|
) -> None:
|
3194
4837
|
super().__init__()
|
3195
4838
|
|
3196
|
-
self.
|
4839
|
+
self._options = options
|
3197
4840
|
|
3198
|
-
self.
|
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.
|
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.
|
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
|
-
# ../
|
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
|
-
|
3394
|
-
|
3395
|
-
|
3396
|
-
|
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
|
-
|
3405
|
-
|
4959
|
+
inj.bind(SystemInterpProvider, singleton=True),
|
4960
|
+
inj.bind(InterpProvider, to_key=SystemInterpProvider, array=True),
|
4961
|
+
]
|
3406
4962
|
|
3407
|
-
|
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
|
-
# ../
|
4967
|
+
# ../pyenv/inject.py
|
3420
4968
|
|
3421
4969
|
|
3422
|
-
|
3423
|
-
|
3424
|
-
|
3425
|
-
|
4970
|
+
def bind_interp_pyenv() -> InjectorBindings:
|
4971
|
+
lst: ta.List[InjectorBindingOrBindings] = [
|
4972
|
+
inj.bind(Pyenv, singleton=True),
|
3426
4973
|
|
3427
|
-
|
3428
|
-
|
3429
|
-
|
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
|
-
|
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
|
-
|
3445
|
-
|
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
|
-
|
3453
|
-
|
3454
|
-
|
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
|
-
|
3464
|
-
return None
|
4989
|
+
bind_interp_pyenv(),
|
3465
4990
|
|
3466
|
-
|
4991
|
+
bind_interp_uv(),
|
3467
4992
|
|
3468
|
-
|
3469
|
-
|
3470
|
-
key=lambda s: s.version,
|
3471
|
-
)
|
3472
|
-
if not sv:
|
3473
|
-
return None
|
4993
|
+
inj.bind(InterpInspector, singleton=True),
|
4994
|
+
]
|
3474
4995
|
|
3475
|
-
|
3476
|
-
return await tp.install_version(bv)
|
4996
|
+
#
|
3477
4997
|
|
3478
|
-
|
3479
|
-
|
3480
|
-
|
3481
|
-
|
3482
|
-
|
3483
|
-
|
3484
|
-
|
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
|
-
|
3487
|
-
print(f' {n}')
|
3488
|
-
for si in lst:
|
3489
|
-
print(f' {si}')
|
3490
|
-
|
3491
|
-
print()
|
5007
|
+
]
|
3492
5008
|
|
3493
|
-
|
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
|
-
|
3507
|
-
|
3508
|
-
|
5013
|
+
lst.extend([
|
5014
|
+
inj.bind(InterpResolver, singleton=True),
|
5015
|
+
])
|
3509
5016
|
|
3510
|
-
|
5017
|
+
#
|
3511
5018
|
|
3512
|
-
|
3513
|
-
]])
|
5019
|
+
return inj.as_bindings(*lst)
|
3514
5020
|
|
3515
5021
|
|
3516
5022
|
########################################
|
3517
5023
|
# cli.py
|
3518
5024
|
|
3519
5025
|
|
3520
|
-
|
3521
|
-
|
3522
|
-
|
3523
|
-
|
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
|
-
|
3542
|
-
|
3543
|
-
|
3544
|
-
parser_list.set_defaults(func=_list_cmd)
|
5031
|
+
@cached_nullary
|
5032
|
+
def providers(self) -> InterpResolverProviders:
|
5033
|
+
return self.injector()[InterpResolverProviders]
|
3545
5034
|
|
3546
|
-
|
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
|
-
|
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
|
-
|
3561
|
-
|
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:
|