split-dex 0.1.0__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.
- split_dex/__init__.py +5 -0
- split_dex/class_sets.py +135 -0
- split_dex/cli.py +227 -0
- split_dex/partition.py +229 -0
- split_dex/py.typed +0 -0
- split_dex/registry.py +214 -0
- split_dex/splitter.py +584 -0
- split_dex/strategies/__init__.py +1 -0
- split_dex/strategies/original.py +59 -0
- split_dex/strategies/package_affinity.py +40 -0
- split_dex/strategies/package_order.py +56 -0
- split_dex/strategies/single.py +23 -0
- split_dex/strategy.py +233 -0
- split_dex-0.1.0.dist-info/METADATA +253 -0
- split_dex-0.1.0.dist-info/RECORD +18 -0
- split_dex-0.1.0.dist-info/WHEEL +4 -0
- split_dex-0.1.0.dist-info/entry_points.txt +2 -0
- split_dex-0.1.0.dist-info/licenses/LICENSE +201 -0
split_dex/registry.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Find and create strategies: built-in, from installed plugins, or from a user's file or module.
|
|
2
|
+
|
|
3
|
+
A strategy is named by one of:
|
|
4
|
+
|
|
5
|
+
- the name of a built-in strategy, listed in `STRATEGIES`;
|
|
6
|
+
- the name of a plugin: an installed package that declares the strategy as an entry point in
|
|
7
|
+
the group `split_dex.strategies`;
|
|
8
|
+
- `MODULE:CLASS`, for a class in a module that Python can import;
|
|
9
|
+
- `FILE.py:CLASS`, for a class in a Python file.
|
|
10
|
+
|
|
11
|
+
To add a built-in strategy, write its module in `split_dex/strategies/` and add its class to
|
|
12
|
+
`STRATEGIES`. The keyword-only parameters of a strategy's constructor are its settings: each
|
|
13
|
+
`KEY=VALUE` of the command line is converted to the parameter's type (int, float, bool, str or
|
|
14
|
+
Path, optionally `| None`).
|
|
15
|
+
|
|
16
|
+
A strategy from a plugin, module or file is ordinary Python code that runs inside split-dex, so it
|
|
17
|
+
must be trusted.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import collections.abc
|
|
21
|
+
import hashlib
|
|
22
|
+
import importlib.metadata
|
|
23
|
+
import importlib.util
|
|
24
|
+
import inspect
|
|
25
|
+
import pathlib
|
|
26
|
+
import sys
|
|
27
|
+
import types
|
|
28
|
+
import typing
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from split_dex import strategy
|
|
32
|
+
from split_dex.strategies import original
|
|
33
|
+
from split_dex.strategies import package_affinity
|
|
34
|
+
from split_dex.strategies import package_order
|
|
35
|
+
from split_dex.strategies import single
|
|
36
|
+
|
|
37
|
+
STRATEGIES: collections.abc.Mapping[str, collections.abc.Callable[..., strategy.Strategy]] = {
|
|
38
|
+
"package-affinity": package_affinity.PackageAffinity,
|
|
39
|
+
"package-order": package_order.PackageOrder,
|
|
40
|
+
"original": original.Original,
|
|
41
|
+
"single": single.Single,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
PLUGIN_GROUP = "split_dex.strategies"
|
|
45
|
+
|
|
46
|
+
_SCALAR_TYPES = (int, float, str, pathlib.Path)
|
|
47
|
+
_TRUE = ("true", "yes", "1")
|
|
48
|
+
_FALSE = ("false", "no", "0")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create(spec: str, settings: collections.abc.Mapping[str, str]) -> strategy.Strategy:
|
|
52
|
+
"""Create a strategy from its name and settings given as text.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
spec: The strategy: a built-in or plugin name such as `package-affinity`,
|
|
56
|
+
`MODULE:CLASS`, or `FILE.py:CLASS`.
|
|
57
|
+
settings: The value of each setting, as text, by parameter name. A dash in a name counts
|
|
58
|
+
as an underscore, so `max-files` sets `max_files`.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The strategy.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
StrategyError: If the strategy cannot be found or loaded, a setting does not exist, a
|
|
65
|
+
value is invalid, or the created object has no `plan` method.
|
|
66
|
+
"""
|
|
67
|
+
factory = _find(spec)
|
|
68
|
+
parameters = inspect.signature(factory).parameters
|
|
69
|
+
arguments: dict[str, Any] = {}
|
|
70
|
+
for key, text in settings.items():
|
|
71
|
+
parameter = parameters.get(key.replace("-", "_"))
|
|
72
|
+
if parameter is None:
|
|
73
|
+
known = ", ".join(parameters) if len(parameters) > 0 else "none"
|
|
74
|
+
raise strategy.StrategyError(
|
|
75
|
+
f"strategy {spec} has no setting {key!r}; its settings: {known}"
|
|
76
|
+
)
|
|
77
|
+
arguments[parameter.name] = _convert(spec, parameter, text)
|
|
78
|
+
try:
|
|
79
|
+
created = factory(**arguments)
|
|
80
|
+
except ValueError as error:
|
|
81
|
+
raise strategy.StrategyError(f"strategy {spec}: {error}") from error
|
|
82
|
+
if isinstance(created, strategy.Strategy) is False:
|
|
83
|
+
raise strategy.StrategyError(f"strategy {spec} has no plan method")
|
|
84
|
+
return created
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def describe() -> list[str]:
|
|
88
|
+
"""Describe every built-in and plugin strategy: its name, what it does, and its settings.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The lines to print, one per strategy and one per setting.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
StrategyError: If a plugin cannot be loaded.
|
|
95
|
+
"""
|
|
96
|
+
lines: list[str] = []
|
|
97
|
+
for name, factory in STRATEGIES.items():
|
|
98
|
+
lines.extend(_describe(name, factory))
|
|
99
|
+
for name, entry_point in sorted(_plugins().items()):
|
|
100
|
+
source = "" if entry_point.dist is None else f" (plugin from {entry_point.dist.name})"
|
|
101
|
+
lines.extend(_describe(f"{name}{source}", _load(entry_point, name)))
|
|
102
|
+
return lines
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _find(spec: str) -> collections.abc.Callable[..., strategy.Strategy]:
|
|
106
|
+
built_in = STRATEGIES.get(spec)
|
|
107
|
+
if built_in is not None:
|
|
108
|
+
return built_in
|
|
109
|
+
location, separator, attribute = spec.rpartition(":")
|
|
110
|
+
if separator == "":
|
|
111
|
+
plugin = _plugins().get(spec)
|
|
112
|
+
if plugin is None:
|
|
113
|
+
known = ", ".join([*STRATEGIES, *sorted(_plugins())])
|
|
114
|
+
raise strategy.StrategyError(
|
|
115
|
+
f"unknown strategy {spec!r}; known: {known}; or give MODULE:CLASS or FILE.py:CLASS"
|
|
116
|
+
)
|
|
117
|
+
return _load(plugin, spec)
|
|
118
|
+
if location == "" or attribute == "":
|
|
119
|
+
raise strategy.StrategyError(f"strategy {spec}: expected MODULE:CLASS or FILE.py:CLASS")
|
|
120
|
+
if location.endswith(".py"):
|
|
121
|
+
location = _import_file(pathlib.Path(location))
|
|
122
|
+
entry_point = importlib.metadata.EntryPoint(
|
|
123
|
+
name=spec, value=f"{location}:{attribute}", group=PLUGIN_GROUP
|
|
124
|
+
)
|
|
125
|
+
return _load(entry_point, spec)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _plugins() -> dict[str, importlib.metadata.EntryPoint]:
|
|
129
|
+
"""The strategies of installed plugins, by name. A built-in strategy hides a plugin's."""
|
|
130
|
+
return {
|
|
131
|
+
entry_point.name: entry_point
|
|
132
|
+
for entry_point in importlib.metadata.entry_points(group=PLUGIN_GROUP)
|
|
133
|
+
if entry_point.name not in STRATEGIES
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _load(
|
|
138
|
+
entry_point: importlib.metadata.EntryPoint, spec: str
|
|
139
|
+
) -> collections.abc.Callable[..., strategy.Strategy]:
|
|
140
|
+
try:
|
|
141
|
+
loaded = entry_point.load()
|
|
142
|
+
except (ImportError, AttributeError) as error:
|
|
143
|
+
raise strategy.StrategyError(f"cannot load strategy {spec}: {error}") from error
|
|
144
|
+
if callable(loaded) is False:
|
|
145
|
+
raise strategy.StrategyError(f"strategy {spec} is not a class")
|
|
146
|
+
factory: collections.abc.Callable[..., strategy.Strategy] = loaded
|
|
147
|
+
return factory
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _import_file(path: pathlib.Path) -> str:
|
|
151
|
+
"""Import a Python file as a module, once, and return the module's name."""
|
|
152
|
+
if path.is_file() is False:
|
|
153
|
+
raise strategy.StrategyError(f"strategy file not found: {path}")
|
|
154
|
+
digest = hashlib.sha256(str(path.resolve()).encode()).hexdigest()[:16]
|
|
155
|
+
module_name = f"split_dex_user_strategy_{digest}"
|
|
156
|
+
if module_name in sys.modules:
|
|
157
|
+
return module_name
|
|
158
|
+
module_spec = importlib.util.spec_from_file_location(module_name, path)
|
|
159
|
+
if module_spec is None or module_spec.loader is None:
|
|
160
|
+
raise strategy.StrategyError(f"cannot load strategy file {path}")
|
|
161
|
+
module = importlib.util.module_from_spec(module_spec)
|
|
162
|
+
sys.modules[module_name] = module
|
|
163
|
+
try:
|
|
164
|
+
module_spec.loader.exec_module(module)
|
|
165
|
+
except (OSError, SyntaxError, ImportError) as error:
|
|
166
|
+
del sys.modules[module_name]
|
|
167
|
+
raise strategy.StrategyError(f"cannot load strategy file {path}: {error}") from error
|
|
168
|
+
return module_name
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _describe(title: str, factory: collections.abc.Callable[..., strategy.Strategy]) -> list[str]:
|
|
172
|
+
summary = (inspect.getdoc(factory) or "").split("\n", 1)[0]
|
|
173
|
+
lines = [f"{title}: {summary}"]
|
|
174
|
+
for parameter in inspect.signature(factory).parameters.values():
|
|
175
|
+
lines.append(
|
|
176
|
+
f" {parameter.name}: {_type_name(parameter.annotation)} = {parameter.default!r}"
|
|
177
|
+
)
|
|
178
|
+
return lines
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _convert(spec: str, parameter: inspect.Parameter, text: str) -> object:
|
|
182
|
+
target = _value_type(parameter.annotation)
|
|
183
|
+
if target is bool:
|
|
184
|
+
if text.lower() in _TRUE:
|
|
185
|
+
return True
|
|
186
|
+
if text.lower() in _FALSE:
|
|
187
|
+
return False
|
|
188
|
+
elif target in _SCALAR_TYPES:
|
|
189
|
+
try:
|
|
190
|
+
return target(text)
|
|
191
|
+
except ValueError:
|
|
192
|
+
pass
|
|
193
|
+
else:
|
|
194
|
+
raise strategy.StrategyError(
|
|
195
|
+
f"strategy {spec}: setting {parameter.name} cannot be given on the command line"
|
|
196
|
+
)
|
|
197
|
+
raise strategy.StrategyError(
|
|
198
|
+
f"strategy {spec}: {parameter.name} must be {_type_name(target)}, not {text!r}"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _value_type(annotation: Any) -> Any:
|
|
203
|
+
"""The type a value converts to: `X` for both `X` and `X | None`."""
|
|
204
|
+
if isinstance(annotation, types.UnionType):
|
|
205
|
+
members = [member for member in typing.get_args(annotation) if member is not type(None)]
|
|
206
|
+
if len(members) == 1:
|
|
207
|
+
return members[0]
|
|
208
|
+
return annotation
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _type_name(annotation: Any) -> str:
|
|
212
|
+
if isinstance(annotation, type):
|
|
213
|
+
return annotation.__name__
|
|
214
|
+
return str(annotation)
|