diff --git a/docs/concepts/resolver-architecture.rst b/docs/concepts/resolver-architecture.rst index 67fefa0e1..219d52cd1 100644 --- a/docs/concepts/resolver-architecture.rst +++ b/docs/concepts/resolver-architecture.rst @@ -30,10 +30,6 @@ priority order and accumulates results in a thread-safe session cache: │ ▼ │ │ 3. Network (PyPI / GitHub / GitLab) │ │ (per-package configurable) │ - │ │ age filter empties result │ - │ ▼ │ - │ 4. Cache server fallback │ - │ (multiple_versions mode only) │ └───────────────────────────────────────────────────────┘ Git URL requirements (top-level only) bypass this @@ -44,6 +40,10 @@ Versions discovered from any source are merged into the session cache. Subsequent requests for the same package skip the network entirely if the cache already contains a matching version. +The configured wheel cache server is not a version-resolution fallback. +After a version has been selected, the bootstrapper checks the cache +server separately for a previously built wheel. + Provider Hierarchy ------------------ @@ -73,8 +73,8 @@ CLI commands interact with providers through a common Filters by platform tags, Python version, and yanked status. * - ``PyPICacheProvider`` - Subclass of ``PyPIProvider`` pointing at fromager's own wheel - server. No cooldown applied. Used as a fallback when age - filtering eliminates all candidates. + server. No cooldown applied. Used by the bootstrapper's + previously-built-wheel lookup, not by source version resolution. * - ``GenericProvider`` - Callback-based provider that pairs a version source function with a configurable match function (plain parse or regex). @@ -117,10 +117,11 @@ Two age-based filters can narrow the set of acceptable versions: ``multiple_versions`` mode to limit the range of versions built. When both are active, only versions published within the window are -considered. If all candidates are filtered out, the behavior depends -on the mode: in single-version mode a warning is logged and all -candidates are kept; in ``multiple_versions`` mode the cache server -fallback is tried instead. +considered. If all source candidates are filtered out, the behavior +depends on the mode: in single-version mode a warning is logged and all +candidates are kept; in ``multiple_versions`` mode only the newest +candidate is retained. A source provider with no matching candidates +raises a resolution error; it is not replaced by a cache-server lookup. Flat Resolution by Design ------------------------- diff --git a/src/fromager/bootstrap_requirement_resolver.py b/src/fromager/bootstrap_requirement_resolver.py index c4943096a..8ee267511 100644 --- a/src/fromager/bootstrap_requirement_resolver.py +++ b/src/fromager/bootstrap_requirement_resolver.py @@ -14,7 +14,7 @@ from packaging.utils import NormalizedName, canonicalize_name from packaging.version import Version -from . import finders, resolver, sources, wheels +from . import resolver, sources, wheels from .dependency_graph import DependencyGraph from .requirements_file import RequirementType @@ -43,24 +43,21 @@ def __init__( ctx: context.WorkContext, prev_graph: DependencyGraph | None = None, multiple_versions: bool = False, - cache_wheel_server_url: str = "", ) -> None: """Initialize requirement resolver. Args: ctx: Work context with constraints and settings prev_graph: Optional previous dependency graph for caching - multiple_versions: If ``True`` and no results are found through - any other approach, takes the latest candidate from the - cache server, ignoring the age filters. In all other - cases, returns an empty list when no candidates are found. - cache_wheel_server_url: URL of the remote wheel cache server. - Used as a fallback when age filtering produces no candidates. + multiple_versions: If ``True``, keep the newest matching source + version when the age filter removes all candidates. If + ``False``, keep all matching versions in that case. If no + source version matches the requirement, resolution raises an + error. """ self.ctx = ctx self.prev_graph = prev_graph self.multiple_versions = multiple_versions - self.cache_wheel_server_url = cache_wheel_server_url # All known versions for a package, accumulated across resolution # contexts. Versions discovered via different specifiers or req_types # are merged so that later lookups see the widest set. @@ -211,19 +208,9 @@ def _resolve_and_extend( age_fallback=age_fallback, ) - if not results and self.multiple_versions and self.cache_wheel_server_url: - logger.info( - "no results found with normal resolution, " - "falling back to the cache server %s", - self.cache_wheel_server_url, - ) - results = self._resolve_from_cache_server(req) - if not results: logger.warning( - "resolver returned no results " - "(wheel server URL %s, %s version mode)", - self.cache_wheel_server_url or "(none)", + "resolver returned no results (%s version mode)", "multiple" if self.multiple_versions else "single", ) @@ -235,43 +222,6 @@ def _resolve_and_extend( versions[version] = url self._resolved_rules.add((str(req), pre_built)) - def _resolve_from_cache_server(self, req: Requirement) -> list[tuple[str, Version]]: - """Fall back to the remote wheel cache server for a cached version. - - When age filtering removes all candidates in multi-version mode, - queries the remote cache server for the newest available wheel. - Returns at most one version so that transitive dependencies are - re-processed without rebuilding every old version. - """ - logger.info( - "checking cache server %s for existing build", - self.cache_wheel_server_url, - ) - best: tuple[str, Version] | None = None - for include_sdists, include_wheels in [(False, True), (True, False)]: - try: - provider = finders.PyPICacheProvider( - cache_server_url=self.cache_wheel_server_url, - constraints=self.ctx.constraints, - include_sdists=include_sdists, - include_wheels=include_wheels, - ) - results = resolver.find_all_matching_from_provider(provider, req) - if results: - url, version = results[0] - if best is None or version > best[1]: - best = (url, version) - except Exception as err: - logger.warning( - "error checking cache server %s: %s", - self.cache_wheel_server_url, - err, - ) - if best is not None: - logger.info("found version %s on cache server", best[1]) - return [best] - return [] - def get_matching_versions( self, req: Requirement, diff --git a/src/fromager/bootstrapper/_bootstrapper.py b/src/fromager/bootstrapper/_bootstrapper.py index af5df5424..8b1ac16f6 100644 --- a/src/fromager/bootstrapper/_bootstrapper.py +++ b/src/fromager/bootstrapper/_bootstrapper.py @@ -91,7 +91,6 @@ def __init__( ctx=ctx, prev_graph=prev_graph, multiple_versions=multiple_versions, - cache_wheel_server_url=self.cache_wheel_server_url, ) # Push items onto the stack as we start to resolve their # dependencies so at the end we have a list of items that need to diff --git a/tests/test_bootstrap_requirement_resolver.py b/tests/test_bootstrap_requirement_resolver.py index 7222a6fbc..a0de1ffa7 100644 --- a/tests/test_bootstrap_requirement_resolver.py +++ b/tests/test_bootstrap_requirement_resolver.py @@ -658,173 +658,8 @@ def test_resolve_prebuilt_after_source_uses_separate_cache( assert version2 == Version("1.5") -class TestResolveFromCacheServer: - """Tests for the cache server fallback in _resolve_from_cache_server.""" - - def test_returns_newest_version_from_cache(self, tmp_context: WorkContext) -> None: - """Falls back to cache server and returns only the newest version.""" - brr = BootstrapRequirementResolver( - tmp_context, - multiple_versions=True, - cache_wheel_server_url="http://cache.test/simple", - ) - req = Requirement("testpkg") - - with patch( - "fromager.bootstrap_requirement_resolver.resolver" - ".find_all_matching_from_provider", - return_value=[ - ("http://cache.test/testpkg-3.0.whl", Version("3.0")), - ("http://cache.test/testpkg-2.0.whl", Version("2.0")), - ], - ): - result = brr._resolve_from_cache_server(req) - - assert len(result) == 1 - assert result[0][1] == Version("3.0") - - def test_returns_empty_when_cache_has_no_match( - self, tmp_context: WorkContext - ) -> None: - """Returns empty list when cache server has nothing.""" - brr = BootstrapRequirementResolver( - tmp_context, - multiple_versions=True, - cache_wheel_server_url="http://cache.test/simple", - ) - req = Requirement("testpkg") - - with patch( - "fromager.bootstrap_requirement_resolver.resolver" - ".find_all_matching_from_provider", - return_value=[], - ): - result = brr._resolve_from_cache_server(req) - - assert result == [] - - def test_returns_empty_on_exception(self, tmp_context: WorkContext) -> None: - """Returns empty list when cache server query fails.""" - brr = BootstrapRequirementResolver( - tmp_context, - multiple_versions=True, - cache_wheel_server_url="http://cache.test/simple", - ) - req = Requirement("testpkg") - - with patch( - "fromager.bootstrap_requirement_resolver.resolver" - ".find_all_matching_from_provider", - side_effect=RuntimeError("connection refused"), - ): - result = brr._resolve_from_cache_server(req) - - assert result == [] - - def test_resolve_uses_cache_fallback_when_age_filter_empties( - self, tmp_context: WorkContext - ) -> None: - """resolve() falls back to cache server when age filter produces empty result.""" - brr = BootstrapRequirementResolver( - tmp_context, - multiple_versions=True, - cache_wheel_server_url="http://cache.test/simple", - ) - req = Requirement("testpkg") - - with ( - patch.object(brr, "_resolve_from_graph", return_value=None), - patch( - "fromager.bootstrap_requirement_resolver.sources.get_source_provider", - ), - patch( - "fromager.bootstrap_requirement_resolver.resolver" - ".find_all_matching_from_provider", - return_value=[], - ) as mock_pypi, - patch.object( - brr, - "_resolve_from_cache_server", - return_value=[("http://cache.test/testpkg-1.0.whl", Version("1.0"))], - ) as mock_cache, - ): - result = brr.resolve( - req, - RequirementType.INSTALL, - parent_req=None, - pre_built=False, - return_all_versions=True, - ) - - mock_pypi.assert_called_once() - mock_cache.assert_called_once_with(req) - assert len(result) == 1 - assert result[0][1] == Version("1.0") - - def test_resolve_skips_cache_fallback_in_single_version_mode( - self, tmp_context: WorkContext - ) -> None: - """resolve() does not fall back to cache server in single-version mode.""" - brr = BootstrapRequirementResolver( - tmp_context, - multiple_versions=False, - cache_wheel_server_url="http://cache.test/simple", - ) - req = Requirement("testpkg") - - with ( - patch.object(brr, "_resolve_from_graph", return_value=None), - patch( - "fromager.bootstrap_requirement_resolver.sources.get_source_provider", - ), - patch( - "fromager.bootstrap_requirement_resolver.resolver" - ".find_all_matching_from_provider", - return_value=[("url", Version("1.0"))], - ), - patch.object(brr, "_resolve_from_cache_server") as mock_cache, - ): - brr.resolve( - req, - RequirementType.INSTALL, - parent_req=None, - pre_built=False, - ) - - mock_cache.assert_not_called() - - def test_resolve_skips_cache_fallback_when_no_server_url( - self, tmp_context: WorkContext - ) -> None: - """resolve() does not fall back when no cache_wheel_server_url is set.""" - brr = BootstrapRequirementResolver( - tmp_context, - multiple_versions=True, - cache_wheel_server_url="", - ) - req = Requirement("testpkg") - - with ( - patch.object(brr, "_resolve_from_graph", return_value=None), - patch( - "fromager.bootstrap_requirement_resolver.sources.get_source_provider", - ), - patch( - "fromager.bootstrap_requirement_resolver.resolver" - ".find_all_matching_from_provider", - return_value=[], - ), - patch.object(brr, "_resolve_from_cache_server") as mock_cache, - ): - brr.resolve( - req, - RequirementType.INSTALL, - parent_req=None, - pre_built=False, - return_all_versions=True, - ) - - mock_cache.assert_not_called() +class TestAgeFallback: + """Tests for age filtering fallback behavior.""" def test_multi_version_passes_newest_age_fallback( self, tmp_context: WorkContext @@ -833,7 +668,6 @@ def test_multi_version_passes_newest_age_fallback( brr = BootstrapRequirementResolver( tmp_context, multiple_versions=True, - cache_wheel_server_url="http://cache.test/simple", ) req = Requirement("testpkg")