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