diff options
Diffstat (limited to 'lib/ansible/galaxy/dependency_resolution/providers.py')
-rw-r--r-- | lib/ansible/galaxy/dependency_resolution/providers.py | 134 |
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 ( |