Mercurial > repos > shellac > sam_consensus_v3
comparison env/lib/python3.9/site-packages/galaxy/tool_util/deps/resolvers/__init__.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author | shellac |
---|---|
date | Mon, 22 Mar 2021 18:12:50 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4f3585e2f14b |
---|---|
1 """The module defines the abstract interface for dealing tool dependency resolution plugins.""" | |
2 import errno | |
3 import os.path | |
4 from abc import ( | |
5 ABCMeta, | |
6 abstractmethod, | |
7 abstractproperty, | |
8 ) | |
9 from typing import Any, Dict | |
10 | |
11 import yaml | |
12 | |
13 from galaxy.util import listify | |
14 from galaxy.util.dictifiable import Dictifiable | |
15 from ..requirements import ToolRequirement | |
16 | |
17 | |
18 class DependencyResolver(Dictifiable, metaclass=ABCMeta): | |
19 """Abstract description of a technique for resolving container images for tool execution.""" | |
20 | |
21 # Keys for dictification. | |
22 dict_collection_visible_keys = ['resolver_type', 'resolves_simple_dependencies', 'can_uninstall_dependencies', 'read_only'] | |
23 # A "simple" dependency is one that does not depend on the the tool | |
24 # resolving the dependency. Classic tool shed dependencies are non-simple | |
25 # because the repository install context is used in dependency resolution | |
26 # so the same requirement tags in different tools will have very different | |
27 # resolution. | |
28 disabled = False | |
29 resolves_simple_dependencies = True | |
30 config_options: Dict[str, Any] = {} | |
31 read_only = True | |
32 | |
33 @abstractmethod | |
34 def resolve(self, requirement, **kwds): | |
35 """Given inputs describing dependency in the abstract yield a Dependency object. | |
36 | |
37 The Dependency object describes various attributes (script, bin, | |
38 version) used to build scripts with the dependency availble. Here | |
39 script is the env.sh file to source before running a job, if that is | |
40 not found the bin directory will be appended to the path (if it is | |
41 not ``None``). Finally, version is the resolved tool dependency | |
42 version (which may differ from requested version for instance if the | |
43 request version is 'default'.) | |
44 """ | |
45 | |
46 def install_dependency(self, name, version, type, **kwds): | |
47 if self.read_only: | |
48 return False | |
49 else: | |
50 return self._install_dependency(name, version, type, **kwds) | |
51 | |
52 def _install_dependency(self, name, version, type, **kwds): | |
53 """ Attempt to install this dependency if a recipe to do so | |
54 has been registered in some way. | |
55 """ | |
56 return False | |
57 | |
58 @property | |
59 def can_uninstall_dependencies(self): | |
60 return not self.read_only | |
61 | |
62 | |
63 class MultipleDependencyResolver: | |
64 """Variant of DependencyResolver that can optionally resolve multiple dependencies together.""" | |
65 | |
66 @abstractmethod | |
67 def resolve_all(self, requirements, **kwds): | |
68 """ | |
69 Given multiple requirements yields a list of Dependency objects if and only if they may all be resolved together. | |
70 | |
71 Unsuccessfull attempts should return an empty list. | |
72 | |
73 :param requirements: list of tool requirements | |
74 :param type: [ToolRequirement] or ToolRequirements | |
75 | |
76 :returns: list of resolved dependencies | |
77 :rtype: [Dependency] | |
78 """ | |
79 | |
80 | |
81 class ListableDependencyResolver(metaclass=ABCMeta): | |
82 """ Mix this into a ``DependencyResolver`` and implement to indicate | |
83 the dependency resolver can iterate over its dependencies and generate | |
84 requirements. | |
85 """ | |
86 | |
87 @abstractmethod | |
88 def list_dependencies(self): | |
89 """ List the "simple" requirements that may be resolved "exact"-ly | |
90 by this dependency resolver. | |
91 """ | |
92 | |
93 def _to_requirement(self, name, version=None): | |
94 return ToolRequirement(name=name, type="package", version=version) | |
95 | |
96 | |
97 class MappableDependencyResolver: | |
98 """Mix this into a ``DependencyResolver`` to allow mapping files. | |
99 | |
100 Mapping files allow adapting generic requirements to specific local implementations. | |
101 """ | |
102 | |
103 def _setup_mapping(self, dependency_manager, **kwds): | |
104 mapping_files = dependency_manager.get_resolver_option(self, "mapping_files", explicit_resolver_options=kwds) | |
105 mappings = [] | |
106 if mapping_files: | |
107 search_dirs = [os.getcwd()] | |
108 if isinstance(dependency_manager.default_base_path, str): | |
109 search_dirs.append(dependency_manager.default_base_path) | |
110 | |
111 def candidates(path): | |
112 if os.path.isabs(path): | |
113 yield path | |
114 else: | |
115 for search_dir in search_dirs: | |
116 yield os.path.join(search_dir, path) | |
117 | |
118 mapping_files = listify(mapping_files) | |
119 for mapping_file in mapping_files: | |
120 for full_path in candidates(mapping_file): | |
121 if os.path.exists(full_path): | |
122 mappings.extend(MappableDependencyResolver._mapping_file_to_list(full_path)) | |
123 break | |
124 self._mappings = mappings | |
125 | |
126 @staticmethod | |
127 def _mapping_file_to_list(mapping_file): | |
128 raw_mapping = [] | |
129 try: | |
130 with open(mapping_file) as f: | |
131 raw_mapping = yaml.safe_load(f) | |
132 except OSError as exc: | |
133 if exc.errno != errno.ENOENT: | |
134 raise | |
135 return list(map(RequirementMapping.from_dict, raw_mapping)) | |
136 | |
137 def _expand_mappings(self, requirement): | |
138 for mapping in self._mappings: | |
139 if mapping.matches_requirement(requirement): | |
140 requirement = mapping.apply(requirement) | |
141 break | |
142 | |
143 return requirement | |
144 | |
145 | |
146 FROM_UNVERSIONED = object() | |
147 | |
148 | |
149 class RequirementMapping: | |
150 | |
151 def __init__(self, from_name, from_version, to_name, to_version): | |
152 self.from_name = from_name | |
153 self.from_version = from_version | |
154 self.to_name = to_name | |
155 self.to_version = to_version | |
156 | |
157 def matches_requirement(self, requirement): | |
158 """Check if supplied ToolRequirement matches this mapping description. | |
159 | |
160 For it to match - the names must match. Additionally if the | |
161 requirement is created with a version or with unversioned being set to | |
162 True additional checks are needed. If a version is specified, it must | |
163 match the supplied version exactly. If ``unversioned`` is True, then | |
164 the supplied requirement must be unversioned (i.e. its version must be | |
165 set to ``None``). | |
166 """ | |
167 | |
168 if requirement.name != self.from_name: | |
169 return False | |
170 elif self.from_version is None: | |
171 return True | |
172 elif self.from_version is FROM_UNVERSIONED: | |
173 return requirement.version is None | |
174 else: | |
175 return requirement.version == self.from_version | |
176 | |
177 def apply(self, requirement): | |
178 requirement = requirement.copy() | |
179 requirement.name = self.to_name | |
180 if self.to_version is not None: | |
181 requirement.version = self.to_version | |
182 return requirement | |
183 | |
184 @staticmethod | |
185 def from_dict(raw_mapping): | |
186 from_raw = raw_mapping.get("from") | |
187 if isinstance(from_raw, dict): | |
188 from_name = from_raw.get("name") | |
189 raw_version = from_raw.get("version", None) | |
190 unversioned = from_raw.get("unversioned", False) | |
191 if unversioned and raw_version: | |
192 raise Exception("Cannot define both version and set unversioned to True.") | |
193 | |
194 if unversioned: | |
195 from_version = FROM_UNVERSIONED | |
196 else: | |
197 from_version = str(raw_version) if raw_version is not None else raw_version | |
198 else: | |
199 from_name = from_raw | |
200 from_version = None | |
201 | |
202 to_raw = raw_mapping.get("to") | |
203 if isinstance(to_raw, dict): | |
204 to_name = to_raw.get("name", from_name) | |
205 to_version = str(to_raw.get("version")) | |
206 else: | |
207 to_name = to_raw | |
208 to_version = None | |
209 | |
210 return RequirementMapping(from_name, from_version, to_name, to_version) | |
211 | |
212 | |
213 class SpecificationAwareDependencyResolver(metaclass=ABCMeta): | |
214 """Mix this into a :class:`DependencyResolver` to implement URI specification matching. | |
215 | |
216 Allows adapting generic requirements to more specific URIs - to tailor name | |
217 or version to specified resolution system. | |
218 """ | |
219 | |
220 @abstractmethod | |
221 def _expand_specs(self, requirement): | |
222 """Find closest matching specification for discovered resolver and return new concrete requirement.""" | |
223 | |
224 | |
225 class SpecificationPatternDependencyResolver(SpecificationAwareDependencyResolver): | |
226 """Implement the :class:`SpecificationAwareDependencyResolver` with a regex pattern.""" | |
227 | |
228 @abstractproperty | |
229 def _specification_pattern(self): | |
230 """Pattern of URI to match against.""" | |
231 | |
232 def _find_specification(self, specs): | |
233 pattern = self._specification_pattern | |
234 for spec in specs: | |
235 if pattern.match(spec.uri): | |
236 return spec | |
237 return None | |
238 | |
239 def _expand_specs(self, requirement): | |
240 name = requirement.name | |
241 version = requirement.version | |
242 specs = requirement.specs | |
243 | |
244 spec = self._find_specification(specs) | |
245 if spec is not None: | |
246 name = spec.short_name | |
247 version = spec.version or version | |
248 | |
249 requirement = requirement.copy() | |
250 requirement.name = name | |
251 requirement.version = version | |
252 | |
253 return requirement | |
254 | |
255 | |
256 class Dependency(Dictifiable, metaclass=ABCMeta): | |
257 dict_collection_visible_keys = ['dependency_type', 'exact', 'name', 'version', 'cacheable'] | |
258 cacheable = False | |
259 | |
260 @abstractmethod | |
261 def shell_commands(self): | |
262 """ | |
263 Return shell commands to enable this dependency. | |
264 """ | |
265 | |
266 @abstractproperty | |
267 def exact(self): | |
268 """ Return true if version information wasn't discarded to resolve | |
269 the dependency. | |
270 """ | |
271 | |
272 @property | |
273 def resolver_msg(self): | |
274 """ | |
275 Return a message describing this dependency | |
276 """ | |
277 return f"Using dependency {self.name} version {self.version} of type {self.dependency_type}" | |
278 | |
279 | |
280 class ContainerDependency(Dependency): | |
281 | |
282 dict_collection_visible_keys = Dependency.dict_collection_visible_keys + ['environment_path', 'container_description', 'container_resolver'] | |
283 | |
284 def __init__(self, container_description, name=None, version=None, container_resolver=None): | |
285 self.container_description = container_description | |
286 self.dependency_type = container_description.type | |
287 self._name = name | |
288 self._version = version | |
289 self.environment_path = container_description.identifier | |
290 self.container_resolver = container_resolver | |
291 | |
292 @property | |
293 def name(self): | |
294 return self._name | |
295 | |
296 @property | |
297 def version(self): | |
298 return self._version | |
299 | |
300 @property | |
301 def exact(self): | |
302 return True | |
303 | |
304 @property | |
305 def shell_commands(self): | |
306 return None | |
307 | |
308 | |
309 class NullDependency(Dependency): | |
310 dependency_type = None | |
311 exact = True | |
312 | |
313 def __init__(self, version=None, name=None): | |
314 self.version = version | |
315 self.name = name | |
316 | |
317 @property | |
318 def resolver_msg(self): | |
319 """ | |
320 Return a message describing this dependency | |
321 """ | |
322 return "Dependency %s not found." % self.name | |
323 | |
324 def shell_commands(self): | |
325 return None | |
326 | |
327 | |
328 class DependencyException(Exception): | |
329 pass |