fix(python): report an unsatisfiable version floor instead of recursing

camoufox_path() ended in `return camoufox_path()` after a fetch. When the
newest published build is still below CONSTRAINTS.MIN_VERSION, install() is a
no-op ("already installed") and that tail recursed ~1000 times -- each
iteration firing another GitHub API call, which exhausts the unauthenticated
rate limit (60/hr) long before the RecursionError lands.

That is precisely the state a library published ahead of its browser release
puts every user in, and it is reachable now that the floor is raised. It also
hits permanently for anyone using a repos.yml source that does not carry the
required build.

Re-check after the fetch instead, and raise UnsupportedVersion naming the
required minimum.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jake Writer
2026-08-31 15:06:48 -06:00
co-authored by Claude Opus 5
parent da67775257
commit ce87cf7dab
2 changed files with 42 additions and 1 deletions
+18 -1
View File
@@ -811,7 +811,24 @@ def camoufox_path(download_if_missing: bool = True) -> Path:
raise UnsupportedVersion("Camoufox executable is outdated.")
CamoufoxFetcher().install()
return camoufox_path()
# Re-check rather than recurse.
#
# If the newest published build is still below the floor -- a library
# published ahead of its browser release, or a repos.yml source that does
# not carry it -- install() is a no-op ("already installed") and recursing
# here spun ~1000 fetch attempts into a RecursionError, having hammered the
# GitHub API into a rate limit on the way. Say what is actually wrong.
active = get_active_path()
if active and Version.from_path(active).is_supported():
return active
if os.path.exists(INSTALL_DIR) and _root_install_supported():
return INSTALL_DIR
raise UnsupportedVersion(
f"No available Camoufox build satisfies this library's minimum "
f"({CONSTRAINTS.MIN_VERSION}). The matching browser release may not be "
f"published yet; wait for it, or install an older camoufox release."
)
def get_path(file: str) -> str:
@@ -95,3 +95,27 @@ def test_root_probe_tolerates_the_versioned_layout(tmp_path, monkeypatch):
monkeypatch.setattr(pkgman, "INSTALL_DIR", root)
assert pkgman._root_install_supported() is False
def test_unsatisfiable_floor_reports_instead_of_recursing(tmp_path, monkeypatch):
"""A floor no published build satisfies must report, not spin.
install() is a no-op when the newest release is already installed, so the
old `return camoufox_path()` tail recursed ~1000 times -- each iteration
firing another GitHub API call, which exhausts the unauthenticated rate
limit long before the RecursionError lands. That is the state a library
published ahead of its browser release puts every user in.
"""
_install(tmp_path, monkeypatch, "versioned", build="beta.29", floor="beta.30")
attempts = []
class StubFetcher:
def install(self):
attempts.append(True) # newest published build is still below the floor
monkeypatch.setattr(pkgman, "CamoufoxFetcher", StubFetcher)
with pytest.raises(UnsupportedVersion):
pkgman.camoufox_path()
assert attempts == [True], "should fetch once, not spin"