diff --git a/src/fromager/bootstrapper.py b/src/fromager/bootstrapper.py index 11440b5d..2ff36342 100644 --- a/src/fromager/bootstrapper.py +++ b/src/fromager/bootstrapper.py @@ -176,7 +176,7 @@ def resolve_version( ) -> tuple[str, Version]: """Resolve the version of a requirement. - Returns the source URL and the version of the requirement. + Returns the source URL and the version of the requirement (highest matching version). Git URL resolution stays in Bootstrapper because it requires build orchestration (BuildEnvironment, build dependencies). @@ -193,19 +193,22 @@ def resolve_version( cached_result = self._resolver.get_cached_resolution(req, pre_built=False) if cached_result is not None: logger.debug(f"resolved {req} from cache") - return cached_result + # Pick highest version from cached list + return cached_result[0] logger.info("resolving source via URL, ignoring any plugins") source_url, resolved_version = self._resolve_version_from_git_url(req=req) # Cache the git URL resolution (always source, not prebuilt) + # Store as list for consistency with cache structure self._resolver.cache_resolution( - req, pre_built=False, result=(source_url, resolved_version) + req, pre_built=False, result=[(source_url, resolved_version)] ) return source_url, resolved_version # Delegate to RequirementResolver parent_req = self.why[-1][1] if self.why else None + # Returns the highest matching version return self._resolver.resolve( req=req, req_type=req_type, diff --git a/src/fromager/requirement_resolver.py b/src/fromager/requirement_resolver.py index fbee977e..7ec54a21 100644 --- a/src/fromager/requirement_resolver.py +++ b/src/fromager/requirement_resolver.py @@ -51,16 +51,19 @@ def __init__( self.prev_graph = prev_graph # Session-level resolution cache to avoid re-resolving same requirements # Key: (requirement_string, pre_built) to distinguish source vs prebuilt - self._resolved_requirements: dict[tuple[str, bool], tuple[str, Version]] = {} + # Value: list of (url, version) tuples sorted by version (highest first) + self._resolved_requirements: dict[ + tuple[str, bool], list[tuple[str, Version]] + ] = {} - def resolve( + def resolve_all( self, req: Requirement, req_type: RequirementType, parent_req: Requirement | None = None, pre_built: bool | None = None, - ) -> tuple[str, Version]: - """Resolve package requirement. + ) -> list[tuple[str, Version]]: + """Resolve package requirement to all matching versions. Tries resolution strategies in order: 1. Session cache (if previously resolved) @@ -75,7 +78,7 @@ def resolve( If None (default), uses package build info to determine. Returns: - Tuple of (url, resolved_version) + List of (url, version) tuples sorted by version (highest first) Raises: ValueError: If req contains a git URL and pre_built is False @@ -101,12 +104,42 @@ def resolve( return cached_result # Resolve using strategies - url, resolved_version = self._resolve(req, req_type, parent_req, pre_built) + results = self._resolve(req, req_type, parent_req, pre_built) # Cache the result - result = (url, resolved_version) - self.cache_resolution(req, pre_built, result) - return url, resolved_version + self.cache_resolution(req, pre_built, results) + return results + + def resolve( + self, + req: Requirement, + req_type: RequirementType, + parent_req: Requirement | None = None, + pre_built: bool | None = None, + ) -> tuple[str, Version]: + """Resolve package requirement to the best matching version. + + Tries resolution strategies in order: + 1. Session cache (if previously resolved) + 2. Previous dependency graph + 3. PyPI resolution (source or prebuilt based on package build info) + + Args: + req: Package requirement + req_type: Type of requirement + parent_req: Parent requirement from dependency chain + pre_built: Optional override to force prebuilt (True) or source (False). + If None (default), uses package build info to determine. + + Returns: + (url, version) tuple for the highest matching version + + Raises: + ValueError: If req contains a git URL and pre_built is False + (git URL source resolution must be handled by Bootstrapper) + """ + results = self.resolve_all(req, req_type, parent_req, pre_built) + return results[0] def _resolve( self, @@ -114,7 +147,7 @@ def _resolve( req_type: RequirementType, parent_req: Requirement | None, pre_built: bool, - ) -> tuple[str, Version]: + ) -> list[tuple[str, Version]]: """Internal resolution logic without caching. Tries resolution strategies in order: @@ -128,7 +161,7 @@ def _resolve( pre_built: Whether to resolve prebuilt (True) or source (False) Returns: - Tuple of (url, resolved_version) + List of (url, version) tuples sorted by version (highest first) """ # Try graph cached_resolution = self._resolve_from_graph( @@ -139,35 +172,36 @@ def _resolve( ) if cached_resolution and not req.url: - url, resolved_version = cached_resolution - logger.debug(f"resolved from previous bootstrap to {resolved_version}") - return url, resolved_version + logger.debug( + f"resolved from previous bootstrap: {len(cached_resolution)} version(s)" + ) + return cached_resolution # Fallback to PyPI + result: list[tuple[str, Version]] if pre_built: # Resolve prebuilt wheel servers = wheels.get_wheel_server_urls( self.ctx, req, cache_wheel_server_url=resolver.PYPI_SERVER_URL ) - url, resolved_version = wheels.resolve_prebuilt_wheel( + result = wheels.resolve_prebuilt_wheel_all( ctx=self.ctx, req=req, wheel_server_urls=servers, req_type=req_type ) else: # Resolve source (sdist) - url, resolved_version = sources.resolve_source( + result = sources.resolve_source_all( ctx=self.ctx, req=req, sdist_server_url=resolver.PYPI_SERVER_URL, req_type=req_type, ) - - return url, resolved_version + return result def get_cached_resolution( self, req: Requirement, pre_built: bool, - ) -> tuple[str, Version] | None: + ) -> list[tuple[str, Version]] | None: """Get a cached resolution result if it exists. Args: @@ -175,7 +209,7 @@ def get_cached_resolution( pre_built: Whether looking for prebuilt or source resolution Returns: - Tuple of (source_url, resolved_version) if cached, None otherwise + List of (url, version) tuples if cached, None otherwise """ cache_key = (str(req), pre_built) return self._resolved_requirements.get(cache_key) @@ -184,7 +218,7 @@ def cache_resolution( self, req: Requirement, pre_built: bool, - result: tuple[str, Version], + result: list[tuple[str, Version]], ) -> None: """Cache a resolution result. @@ -194,7 +228,7 @@ def cache_resolution( Args: req: Package requirement to cache pre_built: Whether this is a prebuilt or source resolution - result: Tuple of (source_url, resolved_version) + result: List of (url, version) tuples """ cache_key = (str(req), pre_built) self._resolved_requirements[cache_key] = result @@ -205,7 +239,7 @@ def _resolve_from_graph( req_type: RequirementType, pre_built: bool, parent_req: Requirement | None, - ) -> tuple[str, Version] | None: + ) -> list[tuple[str, Version]] | None: """Resolve from previous dependency graph. Extracted from Bootstrapper._resolve_from_graph(). @@ -217,7 +251,7 @@ def _resolve_from_graph( parent_req: Parent requirement for graph traversal Returns: - Tuple of (url, version) if found in graph, None otherwise + List of (url, version) tuples if found in graph, None otherwise """ if not self.prev_graph: return None @@ -307,8 +341,8 @@ def _resolve_from_version_source( self, version_source: list[tuple[str, Version]], req: Requirement, - ) -> tuple[str, Version] | None: - """Select best version from candidates. + ) -> list[tuple[str, Version]] | None: + """Filter and return all matching versions from candidates. Extracted from Bootstrapper._resolve_from_version_source(). @@ -317,7 +351,7 @@ def _resolve_from_version_source( req: Package requirement with version specifier Returns: - Tuple of (url, version) for best match, None if no match + List of (url, version) tuples for all matches, None if no matches """ if not version_source: return None @@ -329,6 +363,7 @@ def _resolve_from_version_source( constraints=self.ctx.constraints, use_resolver_cache=False, ) + # resolve_from_provider now returns all matching candidates return resolver.resolve_from_provider(provider, req) except Exception as err: logger.debug(f"could not resolve {req} from {version_source}: {err}") diff --git a/src/fromager/resolver.py b/src/fromager/resolver.py index 2fc100b2..c3ea58de 100644 --- a/src/fromager/resolver.py +++ b/src/fromager/resolver.py @@ -75,7 +75,7 @@ def match_py_req(py_req: str, *, python_version: Version = PYTHON_VERSION) -> bo return python_version in SpecifierSet(py_req) -def resolve( +def resolve_all( *, ctx: context.WorkContext, req: Requirement, @@ -84,7 +84,11 @@ def resolve( include_wheels: bool = True, req_type: RequirementType | None = None, ignore_platform: bool = False, -) -> tuple[str, Version]: +) -> list[tuple[str, Version]]: + """Resolve requirement and return all matching versions. + + Returns list of (url, version) tuples sorted by version (highest first). + """ # Create the (reusable) resolver. provider = overrides.find_and_invoke( req.name, @@ -101,6 +105,32 @@ def resolve( return resolve_from_provider(provider, req) +def resolve( + *, + ctx: context.WorkContext, + req: Requirement, + sdist_server_url: str, + include_sdists: bool = True, + include_wheels: bool = True, + req_type: RequirementType | None = None, + ignore_platform: bool = False, +) -> tuple[str, Version]: + """Resolve requirement and return the best matching version. + + Returns (url, version) tuple for the highest matching version. + """ + results = resolve_all( + ctx=ctx, + req=req, + sdist_server_url=sdist_server_url, + include_sdists=include_sdists, + include_wheels=include_wheels, + req_type=req_type, + ignore_platform=ignore_platform, + ) + return results[0] + + def default_resolver_provider( ctx: context.WorkContext, req: Requirement, @@ -158,26 +188,31 @@ def ending(self, state: typing.Any) -> None: def resolve_from_provider( provider: BaseProvider, req: Requirement -) -> tuple[str, Version]: - reporter = LogReporter(req) - rslvr: resolvelib.Resolver = resolvelib.Resolver(provider, reporter) +) -> list[tuple[str, Version]]: + """Resolve requirement and return all matching candidates. + + Returns list of (url, version) tuples sorted by version (highest first). + """ + # Get all matching candidates directly from provider + # instead of using resolvelib's resolver which picks just one + identifier = provider.identify(req) try: - result = rslvr.resolve([req]) + candidates = provider.find_matches( + identifier=identifier, + requirements={identifier: [req]}, + incompatibilities={}, + ) except resolvelib.resolvers.ResolverException as err: constraint = provider.constraints.get_constraint(req.name) provider_desc = provider.get_provider_description() - # Include the original error message to preserve detailed information - # (e.g., file types, pre-release info from PyPIProvider) original_msg = str(err) raise resolvelib.resolvers.ResolverException( f"Unable to resolve requirement specifier {req} with constraint {constraint} using {provider_desc}: {original_msg}" ) from err - # resolvelib actually just returns one candidate per requirement. - # result.mapping is map from an identifier to its resolved candidate - candidate: Candidate - for candidate in result.mapping.values(): - return candidate.url, candidate.version - raise ValueError(f"Unable to resolve {req}") + + # Convert candidates to list of (url, version) tuples + # Candidates are already sorted by version (highest first) + return [(candidate.url, candidate.version) for candidate in candidates] def get_project_from_pypi( @@ -454,8 +489,8 @@ def validate_candidate( incompatibilities: CandidatesMap, candidate: Candidate, ) -> bool: - identifier_reqs = list(requirements[identifier]) - bad_versions = {c.version for c in incompatibilities[identifier]} + identifier_reqs = list(requirements.get(identifier, [])) + bad_versions = {c.version for c in incompatibilities.get(identifier, [])} # Skip versions that are known bad if candidate.version in bad_versions: if DEBUG_RESOLVER: @@ -559,8 +594,11 @@ def _get_no_match_error_message( Subclasses should override this to provide provider-specific error details. """ - r = next(iter(requirements[identifier])) - return f"found no match for {r} using {self.get_provider_description()}" + reqs = requirements.get(identifier, []) + if reqs: + r = next(iter(reqs)) + return f"found no match for {r} using {self.get_provider_description()}" + return f"found no match for identifier {identifier} using {self.get_provider_description()}" def find_matches( self, diff --git a/src/fromager/sources.py b/src/fromager/sources.py index 65b097aa..f4cc675c 100644 --- a/src/fromager/sources.py +++ b/src/fromager/sources.py @@ -125,15 +125,17 @@ def download_source( @metrics.timeit(description="resolve source") -def resolve_source( +def resolve_source_all( *, ctx: context.WorkContext, req: Requirement, sdist_server_url: str, req_type: RequirementType | None = None, -) -> tuple[str, Version]: - "Return URL to source and its version." +) -> list[tuple[str, Version]]: + """Return list of (URL, version) for all matching source versions. + Returns list sorted by version (highest first). + """ constraint = ctx.constraints.get_constraint(req.name) logger.debug( f"resolving requirement {req} using {sdist_server_url} with constraint {constraint}" @@ -143,7 +145,7 @@ def resolve_source( resolver_results = overrides.find_and_invoke( req.name, "resolve_source", - default_resolve_source, + default_resolve_source_all, ctx=ctx, req=req, sdist_server_url=sdist_server_url, @@ -157,31 +159,54 @@ def resolve_source( logger.debug(f"could not resolve {req} with {constraint}: {err}") raise - if len(resolver_results) == 2: - url, version = resolver_results - else: + if not isinstance(resolver_results, list): raise ValueError( - f"do not know how to unpack {resolver_results}, expected 2 members" + f"expected list of (url, version) tuples, got {type(resolver_results)}" ) - if not isinstance(version, Version): - raise ValueError(f"expected 2nd member to be of type Version, got {version}") + # Validate each tuple in the list + for _url, version in resolver_results: + if not isinstance(version, Version): + raise ValueError(f"expected Version, got {type(version)}: {version}") - return str(url), version + return [(str(url), version) for url, version in resolver_results] -def default_resolve_source( +def resolve_source( + *, ctx: context.WorkContext, req: Requirement, sdist_server_url: str, req_type: RequirementType | None = None, ) -> tuple[str, Version]: - "Return URL to source and its version." + """Return (URL, version) for the best matching source version. + + Returns the highest matching version. + """ + results = resolve_source_all( + ctx=ctx, + req=req, + sdist_server_url=sdist_server_url, + req_type=req_type, + ) + result: tuple[str, Version] = results[0] + return result + + +def default_resolve_source_all( + ctx: context.WorkContext, + req: Requirement, + sdist_server_url: str, + req_type: RequirementType | None = None, +) -> list[tuple[str, Version]]: + """Return list of (URL, version) for all matching source versions. + Returns list sorted by version (highest first). + """ pbi = ctx.package_build_info(req) override_sdist_server_url = pbi.resolver_sdist_server_url(sdist_server_url) - url, version = resolver.resolve( + return resolver.resolve_all( ctx=ctx, req=req, sdist_server_url=override_sdist_server_url, @@ -190,7 +215,21 @@ def default_resolve_source( req_type=req_type, ignore_platform=pbi.resolver_ignore_platform, ) - return url, version + + +def default_resolve_source( + ctx: context.WorkContext, + req: Requirement, + sdist_server_url: str, + req_type: RequirementType | None = None, +) -> tuple[str, Version]: + """Return (URL, version) for the best matching source version. + + Returns the highest matching version. + """ + results = default_resolve_source_all(ctx, req, sdist_server_url, req_type) + result: tuple[str, Version] = results[0] + return result def default_download_source( diff --git a/src/fromager/wheels.py b/src/fromager/wheels.py index 88a2a829..991a0f19 100644 --- a/src/fromager/wheels.py +++ b/src/fromager/wheels.py @@ -456,18 +456,22 @@ def get_wheel_server_urls( @metrics.timeit(description="resolve wheel") -def resolve_prebuilt_wheel( +def resolve_prebuilt_wheel_all( *, ctx: context.WorkContext, req: Requirement, wheel_server_urls: list[str], req_type: requirements_file.RequirementType | None = None, -) -> tuple[str, Version]: - "Return URL to wheel and its version." +) -> list[tuple[str, Version]]: + """Return list of (URL, version) for all matching wheel versions. + + Tries wheel servers in order and returns results from the first that succeeds. + Returns list sorted by version (highest first). + """ excs: list[Exception] = [] for url in wheel_server_urls: try: - wheel_url, resolved_version = resolver.resolve( + wheel_results = resolver.resolve_all( ctx=ctx, req=req, sdist_server_url=url, @@ -480,15 +484,33 @@ def resolve_prebuilt_wheel( except Exception as e: excs.append(e) else: - if wheel_url and resolved_version: - return (wheel_url, resolved_version) + if wheel_results: + return wheel_results else: - excs.append( - ValueError( - f"no result for {url}: {wheel_url=}, {resolved_version=}" - ) - ) + excs.append(ValueError(f"no results for {url}: {wheel_results=}")) raise ExceptionGroup( f"Could not find a prebuilt wheel for {req} on {' or '.join(wheel_server_urls)}", excs, ) + + +def resolve_prebuilt_wheel( + *, + ctx: context.WorkContext, + req: Requirement, + wheel_server_urls: list[str], + req_type: requirements_file.RequirementType | None = None, +) -> tuple[str, Version]: + """Return (URL, version) for the best matching wheel version. + + Tries wheel servers in order and returns result from the first that succeeds. + Returns the highest matching version. + """ + results = resolve_prebuilt_wheel_all( + ctx=ctx, + req=req, + wheel_server_urls=wheel_server_urls, + req_type=req_type, + ) + result: tuple[str, Version] = results[0] + return result diff --git a/tests/test_requirement_resolver.py b/tests/test_requirement_resolver.py index 62957e76..e3dcfa2b 100644 --- a/tests/test_requirement_resolver.py +++ b/tests/test_requirement_resolver.py @@ -85,7 +85,7 @@ def test_resolve_from_graph_no_changes(tmp_context: WorkContext) -> None: req_type=RequirementType.INSTALL, pre_built=False, parent_req=Requirement("foo"), - ) == ("", Version("7")) + ) == [("", Version("7"))] # Resolving pbr dependency of bar assert resolver._resolve_from_graph( @@ -93,7 +93,7 @@ def test_resolve_from_graph_no_changes(tmp_context: WorkContext) -> None: req_type=RequirementType.INSTALL, pre_built=False, parent_req=Requirement("bar"), - ) == ("", Version("6")) + ) == [("", Version("6"))] # Resolving pbr dependency of blah assert resolver._resolve_from_graph( @@ -101,7 +101,7 @@ def test_resolve_from_graph_no_changes(tmp_context: WorkContext) -> None: req_type=RequirementType.INSTALL, pre_built=False, parent_req=Requirement("blah"), - ) == ("", Version("5")) + ) == [("", Version("5"))] def test_resolve_from_graph_install_dep_upgrade(tmp_context: WorkContext) -> None: @@ -123,7 +123,7 @@ def test_resolve_from_graph_install_dep_upgrade(tmp_context: WorkContext) -> Non req_type=RequirementType.INSTALL, pre_built=False, parent_req=Requirement("foo"), - ) == ("", Version("8")) + ) == [("", Version("8"))] # Resolving pbr dependency of bar - constraint prevents upgrade assert resolver._resolve_from_graph( @@ -131,7 +131,7 @@ def test_resolve_from_graph_install_dep_upgrade(tmp_context: WorkContext) -> Non req_type=RequirementType.INSTALL, pre_built=False, parent_req=Requirement("bar"), - ) == ("", Version("6")) + ) == [("", Version("6"))] # Resolving pbr dependency of blah - exact version requirement assert resolver._resolve_from_graph( @@ -139,7 +139,7 @@ def test_resolve_from_graph_install_dep_upgrade(tmp_context: WorkContext) -> Non req_type=RequirementType.INSTALL, pre_built=False, parent_req=Requirement("blah"), - ) == ("", Version("5")) + ) == [("", Version("5"))] def test_resolve_from_graph_install_dep_downgrade(tmp_context: WorkContext) -> None: @@ -161,7 +161,7 @@ def test_resolve_from_graph_install_dep_downgrade(tmp_context: WorkContext) -> N req_type=RequirementType.INSTALL, pre_built=False, parent_req=Requirement("foo"), - ) == ("", Version("6")) + ) == [("", Version("6"))] # Resolving pbr dependency of bar - already at 6 assert resolver._resolve_from_graph( @@ -169,7 +169,7 @@ def test_resolve_from_graph_install_dep_downgrade(tmp_context: WorkContext) -> N req_type=RequirementType.INSTALL, pre_built=False, parent_req=Requirement("bar"), - ) == ("", Version("6")) + ) == [("", Version("6"))] # Resolving pbr dependency of blah - exact version requirement assert resolver._resolve_from_graph( @@ -177,7 +177,7 @@ def test_resolve_from_graph_install_dep_downgrade(tmp_context: WorkContext) -> N req_type=RequirementType.INSTALL, pre_built=False, parent_req=Requirement("blah"), - ) == ("", Version("5")) + ) == [("", Version("5"))] def test_resolve_from_graph_toplevel_dep(tmp_context: WorkContext) -> None: @@ -208,7 +208,7 @@ def test_resolve_from_graph_toplevel_dep(tmp_context: WorkContext) -> None: req_type=RequirementType.TOP_LEVEL, pre_built=False, parent_req=None, - ) == ("", Version("2")) + ) == [("", Version("2"))] # Resolving pbr dependency of foo even if foo version changed assert resolver._resolve_from_graph( @@ -216,7 +216,7 @@ def test_resolve_from_graph_toplevel_dep(tmp_context: WorkContext) -> None: req_type=RequirementType.INSTALL, pre_built=False, parent_req=Requirement("foo"), - ) == ("", Version("7")) + ) == [("", Version("7"))] # Resolving bar assert resolver._resolve_from_graph( @@ -224,7 +224,7 @@ def test_resolve_from_graph_toplevel_dep(tmp_context: WorkContext) -> None: req_type=RequirementType.TOP_LEVEL, pre_built=False, parent_req=None, - ) == ("", Version("1.0.0")) + ) == [("", Version("1.0.0"))] # Resolving pbr dependency of bar assert resolver._resolve_from_graph( @@ -232,7 +232,7 @@ def test_resolve_from_graph_toplevel_dep(tmp_context: WorkContext) -> None: req_type=RequirementType.INSTALL, pre_built=False, parent_req=Requirement("bar"), - ) == ("", Version("6")) + ) == [("", Version("6"))] def test_resolve_from_graph_no_previous_graph(tmp_context: WorkContext) -> None: @@ -292,7 +292,7 @@ def test_resolve_from_graph_new_parent_reuses_existing_version( assert result is not None, ( "Expected packaging==25.0 from prev_graph but got None (would fall back to PyPI)" ) - assert result == ("", Version("25.0")) + assert result == [("", Version("25.0"))] def test_resolve_from_graph_different_req_type_reuses_existing_version( @@ -336,7 +336,7 @@ def test_resolve_from_graph_different_req_type_reuses_existing_version( assert result is not None, ( "Expected bar==2.0 from prev_graph but got None (would fall back to PyPI)" ) - assert result == ("", Version("2.0")) + assert result == [("", Version("2.0"))] def test_resolve_from_graph_parent_specific_preferred_over_name_fallback( @@ -392,7 +392,7 @@ def test_resolve_from_graph_parent_specific_preferred_over_name_fallback( parent_req=Requirement("foo"), ) assert result is not None - assert result == ("", Version("2.0")) + assert result == [("", Version("2.0"))] def test_resolve_from_graph_name_fallback_returns_none_for_missing_package( @@ -448,7 +448,7 @@ def test_resolve_rejects_git_urls_for_source(tmp_context: WorkContext) -> None: ) -@patch("fromager.requirement_resolver.wheels.resolve_prebuilt_wheel") +@patch("fromager.requirement_resolver.wheels.resolve_prebuilt_wheel_all") @patch("fromager.requirement_resolver.wheels.get_wheel_server_urls") def test_resolve_allows_git_urls_for_prebuilt( mock_get_servers: MagicMock, @@ -459,12 +459,11 @@ def test_resolve_allows_git_urls_for_prebuilt( resolver = RequirementResolver(tmp_context) req = Requirement("mypkg @ git+https://github.com/example/repo.git") - # Mock wheel resolution to return expected result + # Mock wheel resolution to return expected result (as list) mock_get_servers.return_value = ["https://pypi.org/simple"] - mock_resolve_wheel.return_value = ( - "https://files.pythonhosted.org/mypkg-1.0-py3-none-any.whl", - Version("1.0"), - ) + mock_resolve_wheel.return_value = [ + ("https://files.pythonhosted.org/mypkg-1.0-py3-none-any.whl", Version("1.0")) + ] # Should NOT raise - git URLs are allowed when explicitly requesting prebuilt url, version = resolver.resolve( @@ -480,14 +479,14 @@ def test_resolve_allows_git_urls_for_prebuilt( assert version == Version("1.0") -@patch("fromager.requirement_resolver.wheels.resolve_prebuilt_wheel") +@patch("fromager.requirement_resolver.wheels.resolve_prebuilt_wheel_all") @patch("fromager.requirement_resolver.wheels.get_wheel_server_urls") def test_resolve_auto_routes_to_prebuilt( mock_get_servers: MagicMock, mock_resolve_wheel: MagicMock, tmp_context: WorkContext, ) -> None: - """resolve(pre_built=None) with pbi.pre_built=True routes to wheels.resolve_prebuilt_wheel.""" + """resolve(pre_built=None) with pbi.pre_built=True routes to wheels.resolve_prebuilt_wheel_all.""" req = Requirement("setuptools>=40") # Mock package build info to return pre_built=True @@ -497,12 +496,14 @@ def test_resolve_auto_routes_to_prebuilt( with patch.object(tmp_context, "package_build_info", return_value=mock_pbi): resolver = RequirementResolver(tmp_context) - # Mock wheel resolution to return expected result + # Mock wheel resolution to return expected result (as list) mock_get_servers.return_value = ["https://pypi.org/simple"] - mock_resolve_wheel.return_value = ( - "https://files.pythonhosted.org/setuptools-1.0-py3-none-any.whl", - Version("1.0"), - ) + mock_resolve_wheel.return_value = [ + ( + "https://files.pythonhosted.org/setuptools-1.0-py3-none-any.whl", + Version("1.0"), + ) + ] # Call resolve with pre_built=None (should auto-detect) url, version = resolver.resolve( @@ -518,12 +519,12 @@ def test_resolve_auto_routes_to_prebuilt( assert version == Version("1.0") -@patch("fromager.requirement_resolver.sources.resolve_source") +@patch("fromager.requirement_resolver.sources.resolve_source_all") def test_resolve_auto_routes_to_source( mock_resolve_source: MagicMock, tmp_context: WorkContext, ) -> None: - """resolve(pre_built=None) with pbi.pre_built=False routes to sources.resolve_source.""" + """resolve(pre_built=None) with pbi.pre_built=False routes to sources.resolve_source_all.""" req = Requirement("mypackage>=1.0") # Mock package build info to return pre_built=False @@ -533,11 +534,10 @@ def test_resolve_auto_routes_to_source( with patch.object(tmp_context, "package_build_info", return_value=mock_pbi): resolver = RequirementResolver(tmp_context) - # Mock source resolution to return expected result - mock_resolve_source.return_value = ( - "https://files.pythonhosted.org/mypackage-2.0.tar.gz", - Version("2.0"), - ) + # Mock source resolution to return expected result (as list) + mock_resolve_source.return_value = [ + ("https://files.pythonhosted.org/mypackage-2.0.tar.gz", Version("2.0")) + ] # Call resolve with pre_built=None (should auto-detect) url, version = resolver.resolve( @@ -553,9 +553,9 @@ def test_resolve_auto_routes_to_source( assert version == Version("2.0") -@patch("fromager.requirement_resolver.wheels.resolve_prebuilt_wheel") +@patch("fromager.requirement_resolver.wheels.resolve_prebuilt_wheel_all") @patch("fromager.requirement_resolver.wheels.get_wheel_server_urls") -@patch("fromager.requirement_resolver.sources.resolve_source") +@patch("fromager.requirement_resolver.sources.resolve_source_all") def test_resolve_prebuilt_after_source_uses_separate_cache( mock_resolve_source: MagicMock, mock_get_servers: MagicMock, @@ -572,11 +572,10 @@ def test_resolve_prebuilt_after_source_uses_separate_cache( with patch.object(tmp_context, "package_build_info", return_value=mock_pbi): resolver = RequirementResolver(tmp_context) - # Mock source resolution - mock_resolve_source.return_value = ( - "https://files.pythonhosted.org/testpkg-1.5.tar.gz", - Version("1.5"), - ) + # Mock source resolution (as list) + mock_resolve_source.return_value = [ + ("https://files.pythonhosted.org/testpkg-1.5.tar.gz", Version("1.5")) + ] # First call: resolve as source (pre_built=None, auto-detects to False) url1, version1 = resolver.resolve( @@ -590,12 +589,14 @@ def test_resolve_prebuilt_after_source_uses_separate_cache( assert version1 == Version("1.5") assert mock_resolve_source.call_count == 1 - # Mock wheel resolution for second call + # Mock wheel resolution for second call (as list) mock_get_servers.return_value = ["https://pypi.org/simple"] - mock_resolve_wheel.return_value = ( - "https://files.pythonhosted.org/testpkg-1.5-py3-none-any.whl", - Version("1.5"), - ) + mock_resolve_wheel.return_value = [ + ( + "https://files.pythonhosted.org/testpkg-1.5-py3-none-any.whl", + Version("1.5"), + ) + ] # Second call: resolve same req as prebuilt (explicit pre_built=True) # This should NOT return the cached source result diff --git a/tests/test_sources.py b/tests/test_sources.py index 4a6c35e7..2a4c2c1a 100644 --- a/tests/test_sources.py +++ b/tests/test_sources.py @@ -22,14 +22,14 @@ def test_invalid_tarfile(mock_download_url: typing.Any, tmp_path: pathlib.Path) sources._download_source_check(req=req, destination_dir=fake_dir, url=fake_url) -@patch("fromager.resolver.resolve") +@patch("fromager.resolver.resolve_all") @patch("fromager.sources._download_source_check") def test_default_download_source_from_settings( download_source_check: Mock, resolve: Mock, testdata_context: context.WorkContext, ) -> None: - resolve.return_value = ("url", Version("42.1.2")) + resolve.return_value = [("url", Version("42.1.2"))] download_source_check.return_value = pathlib.Path("filename.zip") req = Requirement("test_pkg==42.1.2") sdist_server_url = "https://sdist.test/egg" @@ -62,7 +62,7 @@ def test_default_download_source_from_settings( ) -@patch("fromager.resolver.resolve") +@patch("fromager.resolver.resolve_all") @patch("fromager.sources._download_source_check") @patch.multiple( packagesettings.PackageBuildInfo, @@ -75,7 +75,7 @@ def test_default_download_source_with_predefined_resolve_dist( resolve: Mock, tmp_context: context.WorkContext, ) -> None: - resolve.return_value = ("url", Version("1.0")) + resolve.return_value = [("url", Version("1.0"))] download_source_check.return_value = pathlib.Path("filename") req = Requirement("foo==1.0") @@ -92,16 +92,18 @@ def test_default_download_source_with_predefined_resolve_dist( ) -@patch("fromager.sources.default_resolve_source") +@patch("fromager.sources.default_resolve_source_all") def test_invalid_version( mock_default_resolve_source: typing.Any, tmp_context: context.WorkContext ) -> None: req = Requirement("fake==1.0") sdist_server_url = resolver.PYPI_SERVER_URL - mock_default_resolve_source.return_value = ( - "fakesdisturl.com", - "fake version 1.0", - ) + mock_default_resolve_source.return_value = [ + ( + "fakesdisturl.com", + "fake version 1.0", + ) + ] mock_default_resolve_source.__name__ = "mock_default_resolve_source" with pytest.raises(ValueError): sources.resolve_source(