summaryrefslogtreecommitdiff
path: root/lib/ansible/galaxy/dependency_resolution/providers.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/galaxy/dependency_resolution/providers.py')
-rw-r--r--lib/ansible/galaxy/dependency_resolution/providers.py134
1 files changed, 71 insertions, 63 deletions
diff --git a/lib/ansible/galaxy/dependency_resolution/providers.py b/lib/ansible/galaxy/dependency_resolution/providers.py
index f13d3ecf..6ad1de84 100644
--- a/lib/ansible/galaxy/dependency_resolution/providers.py
+++ b/lib/ansible/galaxy/dependency_resolution/providers.py
@@ -40,7 +40,7 @@ except ImportError:
# TODO: add python requirements to ansible-test's ansible-core distribution info and remove the hardcoded lowerbound/upperbound fallback
RESOLVELIB_LOWERBOUND = SemanticVersion("0.5.3")
-RESOLVELIB_UPPERBOUND = SemanticVersion("1.1.0")
+RESOLVELIB_UPPERBOUND = SemanticVersion("0.9.0")
RESOLVELIB_VERSION = SemanticVersion.from_loose_version(LooseVersion(resolvelib_version))
@@ -51,6 +51,7 @@ class CollectionDependencyProviderBase(AbstractProvider):
self, # type: CollectionDependencyProviderBase
apis, # type: MultiGalaxyAPIProxy
concrete_artifacts_manager=None, # type: ConcreteArtifactsManager
+ user_requirements=None, # type: t.Iterable[Requirement]
preferred_candidates=None, # type: t.Iterable[Candidate]
with_deps=True, # type: bool
with_pre_releases=False, # type: bool
@@ -86,12 +87,58 @@ class CollectionDependencyProviderBase(AbstractProvider):
Requirement.from_requirement_dict,
art_mgr=concrete_artifacts_manager,
)
+ self._pinned_candidate_requests = set(
+ # NOTE: User-provided signatures are supplemental, so signatures
+ # NOTE: are not used to determine if a candidate is user-requested
+ Candidate(req.fqcn, req.ver, req.src, req.type, None)
+ for req in (user_requirements or ())
+ if req.is_concrete_artifact or (
+ req.ver != '*' and
+ not req.ver.startswith(('<', '>', '!='))
+ )
+ )
self._preferred_candidates = set(preferred_candidates or ())
self._with_deps = with_deps
self._with_pre_releases = with_pre_releases
self._upgrade = upgrade
self._include_signatures = include_signatures
+ def _is_user_requested(self, candidate): # type: (Candidate) -> bool
+ """Check if the candidate is requested by the user."""
+ if candidate in self._pinned_candidate_requests:
+ return True
+
+ if candidate.is_online_index_pointer and candidate.src is not None:
+ # NOTE: Candidate is a namedtuple, it has a source server set
+ # NOTE: to a specific GalaxyAPI instance or `None`. When the
+ # NOTE: user runs
+ # NOTE:
+ # NOTE: $ ansible-galaxy collection install ns.coll
+ # NOTE:
+ # NOTE: then it's saved in `self._pinned_candidate_requests`
+ # NOTE: as `('ns.coll', '*', None, 'galaxy')` but then
+ # NOTE: `self.find_matches()` calls `self.is_satisfied_by()`
+ # NOTE: with Candidate instances bound to each specific
+ # NOTE: server available, those look like
+ # NOTE: `('ns.coll', '*', GalaxyAPI(...), 'galaxy')` and
+ # NOTE: wouldn't match the user requests saved in
+ # NOTE: `self._pinned_candidate_requests`. This is why we
+ # NOTE: normalize the collection to have `src=None` and try
+ # NOTE: again.
+ # NOTE:
+ # NOTE: When the user request comes from `requirements.yml`
+ # NOTE: with the `source:` set, it'll match the first check
+ # NOTE: but it still can have entries with `src=None` so this
+ # NOTE: normalized check is still necessary.
+ # NOTE:
+ # NOTE: User-provided signatures are supplemental, so signatures
+ # NOTE: are not used to determine if a candidate is user-requested
+ return Candidate(
+ candidate.fqcn, candidate.ver, None, candidate.type, None
+ ) in self._pinned_candidate_requests
+
+ return False
+
def identify(self, requirement_or_candidate):
# type: (t.Union[Candidate, Requirement]) -> str
"""Given requirement or candidate, return an identifier for it.
@@ -143,7 +190,7 @@ class CollectionDependencyProviderBase(AbstractProvider):
Mapping of identifier, list of named tuple pairs.
The named tuples have the entries ``requirement`` and ``parent``.
- resolvelib >=0.8.0, <= 1.0.1
+ resolvelib >=0.8.0, <= 0.8.1
:param identifier: The value returned by ``identify()``.
@@ -295,79 +342,25 @@ class CollectionDependencyProviderBase(AbstractProvider):
latest_matches = []
signatures = []
extra_signature_sources = [] # type: list[str]
-
- discarding_pre_releases_acceptable = any(
- not is_pre_release(candidate_version)
- for candidate_version, _src_server in coll_versions
- )
-
- # NOTE: The optimization of conditionally looping over the requirements
- # NOTE: is used to skip having to compute the pinned status of all
- # NOTE: requirements and apply version normalization to the found ones.
- all_pinned_requirement_version_numbers = {
- # NOTE: Pinned versions can start with a number, but also with an
- # NOTE: equals sign. Stripping it at the beginning should be
- # NOTE: enough. If there's a space after equals, the second strip
- # NOTE: will take care of it.
- # NOTE: Without this conversion, requirements versions like
- # NOTE: '1.2.3-alpha.4' work, but '=1.2.3-alpha.4' don't.
- requirement.ver.lstrip('=').strip()
- for requirement in requirements
- if requirement.is_pinned
- } if discarding_pre_releases_acceptable else set()
-
for version, src_server in coll_versions:
tmp_candidate = Candidate(fqcn, version, src_server, 'galaxy', None)
+ unsatisfied = False
for requirement in requirements:
- candidate_satisfies_requirement = self.is_satisfied_by(
- requirement, tmp_candidate,
- )
- if not candidate_satisfies_requirement:
- break
-
- should_disregard_pre_release_candidate = (
- # NOTE: Do not discard pre-release candidates in the
- # NOTE: following cases:
- # NOTE: * the end-user requested pre-releases explicitly;
- # NOTE: * the candidate is a concrete artifact (e.g. a
- # NOTE: Git repository, subdirs, a tarball URL, or a
- # NOTE: local dir or file etc.);
- # NOTE: * the candidate's pre-release version exactly
- # NOTE: matches a version specifically requested by one
- # NOTE: of the requirements in the current match
- # NOTE: discovery round (i.e. matching a requirement
- # NOTE: that is not a range but an explicit specific
- # NOTE: version pin). This works when some requirements
- # NOTE: request version ranges but others (possibly on
- # NOTE: different dependency tree level depths) demand
- # NOTE: pre-release dependency versions, even if those
- # NOTE: dependencies are transitive.
- is_pre_release(tmp_candidate.ver)
- and discarding_pre_releases_acceptable
- and not (
- self._with_pre_releases
- or tmp_candidate.is_concrete_artifact
- or version in all_pinned_requirement_version_numbers
- )
- )
- if should_disregard_pre_release_candidate:
- break
-
+ unsatisfied |= not self.is_satisfied_by(requirement, tmp_candidate)
# FIXME
- # candidate_is_from_requested_source = (
- # requirement.src is None # if this is true for some candidates but not all it will break key param - Nonetype can't be compared to str
+ # unsatisfied |= not self.is_satisfied_by(requirement, tmp_candidate) or not (
+ # requirement.src is None or # if this is true for some candidates but not all it will break key param - Nonetype can't be compared to str
# or requirement.src == candidate.src
# )
- # if not candidate_is_from_requested_source:
- # break
-
+ if unsatisfied:
+ break
if not self._include_signatures:
continue
extra_signature_sources.extend(requirement.signature_sources or [])
- else: # candidate satisfies requirements, `break` never happened
+ if not unsatisfied:
if self._include_signatures:
for extra_source in extra_signature_sources:
signatures.append(get_signature_from_source(extra_source))
@@ -412,6 +405,21 @@ class CollectionDependencyProviderBase(AbstractProvider):
:returns: Indication whether the `candidate` is a viable \
solution to the `requirement`.
"""
+ # NOTE: Only allow pre-release candidates if we want pre-releases
+ # NOTE: or the req ver was an exact match with the pre-release
+ # NOTE: version. Another case where we'd want to allow
+ # NOTE: pre-releases is when there are several user requirements
+ # NOTE: and one of them is a pre-release that also matches a
+ # NOTE: transitive dependency of another requirement.
+ allow_pre_release = self._with_pre_releases or not (
+ requirement.ver == '*' or
+ requirement.ver.startswith('<') or
+ requirement.ver.startswith('>') or
+ requirement.ver.startswith('!=')
+ ) or self._is_user_requested(candidate)
+ if is_pre_release(candidate.ver) and not allow_pre_release:
+ return False
+
# NOTE: This is a set of Pipenv-inspired optimizations. Ref:
# https://github.com/sarugaku/passa/blob/2ac00f1/src/passa/models/providers.py#L58-L74
if (