Skip to content

Commit 92fdf16

Browse files
committed
Cache dependency version fetches per invocation
Memoize the requests-cache session factory and each dependency's _fetch_versions helper with functools.cache so the per-process backend init and JSON fetch+parse happen exactly once regardless of how many DependencyConstraint instances ask for them. Previously each PythonDependencyConstraint, RDependencyConstraint, etc. opened a fresh CachedSession and walked through the local HTTP cache on every resolve_versions call, producing repeated "Initializing backend" and "Cache directives" DEBUG lines and redundant parse work. Closes #330
1 parent 27f2bae commit 92fdf16

6 files changed

Lines changed: 58 additions & 24 deletions

File tree

posit-bakery/posit_bakery/config/dependencies/positron.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import abc
2+
from functools import cache
23
from typing import Literal, ClassVar
34

45
from pydantic import ConfigDict
@@ -31,18 +32,21 @@ def releases_url(target_arch: str = _DEFAULT_ARCH) -> str:
3132
arch = _ARCH_MAP[target_arch]
3233
return POSITRON_RELEASES_URL_TEMPLATE.format(arch=arch)
3334

34-
def _fetch_versions(self) -> list[DependencyVersion]:
35+
@staticmethod
36+
@cache
37+
def _fetch_versions() -> list[DependencyVersion]:
3538
"""Fetch available Positron versions from Posit CDN.
3639
3740
Uses the default architecture for version discovery since the version
3841
list is identical across architectures.
3942
40-
This method uses caching to avoid repeated network requests.
43+
Memoized so the fetch+parse runs once per bakery invocation regardless
44+
of how many constraint instances ask for it.
4145
4246
:return: A sorted list of available Positron versions.
4347
"""
4448
session = cached_session()
45-
response = session.get(self.releases_url())
49+
response = session.get(PositronDependency.releases_url())
4650
response.raise_for_status()
4751

4852
releases = response.json().get("releases", [])

posit-bakery/posit_bakery/config/dependencies/python.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import abc
2+
from functools import cache
23
from typing import Literal, ClassVar
34

45
from pydantic import ConfigDict
@@ -17,10 +18,13 @@ class PythonDependency(BakeryYAMLModel, abc.ABC):
1718

1819
dependency: Literal[SupportedDependencies.PYTHON] = SupportedDependencies.PYTHON
1920

20-
def _fetch_versions(self) -> list[DependencyVersion]:
21+
@staticmethod
22+
@cache
23+
def _fetch_versions() -> list[DependencyVersion]:
2124
"""Fetch available Python versions from astral-sh/python-build-standalone.
2225
23-
This method uses caching to avoid repeated network requests.
26+
Memoized so the fetch+parse runs once per bakery invocation regardless
27+
of how many constraint instances ask for it.
2428
2529
The results only include cpython builds for linux.
2630
Prerelease versions are excluded.

posit-bakery/posit_bakery/config/dependencies/quarto.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import abc
2+
from functools import cache
23
from typing import Annotated, Literal, ClassVar
34

45
from pydantic import ConfigDict, Field, field_validator
@@ -28,11 +29,14 @@ class QuartoDependency(BakeryYAMLModel, abc.ABC):
2829
),
2930
]
3031

31-
def _fetch_versions(self) -> list[DependencyVersion]:
32+
@staticmethod
33+
@cache
34+
def _fetch_versions(prerelease: bool = False) -> list[DependencyVersion]:
3235
"""Fetch available Quarto versions.
3336
Only the latest patch version for each minor version is included.
3437
35-
This method uses caching to avoid repeated network requests.
38+
Memoized so the fetch+parse runs once per bakery invocation per
39+
``prerelease`` value (at most two entries).
3640
3741
:return: A sorted list of available Quarto versions.
3842
"""
@@ -44,7 +48,7 @@ def _fetch_versions(self) -> list[DependencyVersion]:
4448
response.raise_for_status()
4549
versions.append(DependencyVersion(response.json().get("version")))
4650

47-
if self.prerelease:
51+
if prerelease:
4852
# Fetch prerelease version
4953
response = session.get(QUARTO_PRERELEASE_URL)
5054
response.raise_for_status()
@@ -64,7 +68,7 @@ def available_versions(self) -> list[DependencyVersion]:
6468
6569
:return: A sorted list of available Quarto versions.
6670
"""
67-
return self._fetch_versions()
71+
return self._fetch_versions(self.prerelease)
6872

6973

7074
class QuartoDependencyVersions(DependencyVersions, QuartoDependency):

posit-bakery/posit_bakery/config/dependencies/r.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import abc
2+
from functools import cache
23
from typing import Literal, ClassVar
34

45
from pydantic import ConfigDict
@@ -17,10 +18,13 @@ class RDependency(BakeryYAMLModel, abc.ABC):
1718

1819
dependency: Literal[SupportedDependencies.R] = SupportedDependencies.R
1920

20-
def _fetch_versions(self) -> list[DependencyVersion]:
21+
@staticmethod
22+
@cache
23+
def _fetch_versions() -> list[DependencyVersion]:
2124
"""Fetch available R versions from Posit.
2225
23-
This method uses caching to avoid repeated network requests.
26+
Memoized so the fetch+parse runs once per bakery invocation regardless
27+
of how many constraint instances ask for it.
2428
2529
The results exclude "devel" and "next" versions.
2630

posit-bakery/posit_bakery/util.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import os
3+
from functools import cache
34
from pathlib import Path
45
from shutil import which
56
from typing import Union
@@ -63,17 +64,19 @@ def auto_path() -> Path:
6364
return context
6465

6566

66-
def cached_session(**kwargs) -> CachedSession:
67-
"""Create a cached requests session with default settings."""
68-
session_kwargs = {
69-
"cache_name": "bakery_cache",
70-
"expire_after": 3600,
71-
"backend": "filesystem",
72-
"use_temp": True,
73-
"allowable_methods": ["GET"],
74-
"allowable_codes": [200],
75-
"stale_if_error": True,
76-
}
77-
session_kwargs.update(kwargs)
67+
@cache
68+
def cached_session() -> CachedSession:
69+
"""Return a process-wide cached requests session.
7870
79-
return CachedSession(**session_kwargs)
71+
Memoized so backend initialization and the accompanying log chatter only
72+
happen once per bakery invocation.
73+
"""
74+
return CachedSession(
75+
cache_name="bakery_cache",
76+
expire_after=3600,
77+
backend="filesystem",
78+
use_temp=True,
79+
allowable_methods=["GET"],
80+
allowable_codes=[200],
81+
stale_if_error=True,
82+
)

posit-bakery/test/config/conftest.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@ def patch_testdata_response(url: str):
9292

9393
@pytest.fixture(scope="function")
9494
def disable_requests_caching(mocker):
95+
# The session factory and fetch helpers are decorated with @functools.cache,
96+
# so values returned by an earlier test would otherwise survive into the
97+
# next one. Clear the per-process caches so each test's patches take effect.
98+
from posit_bakery.config.dependencies.positron import PositronDependency
99+
from posit_bakery.config.dependencies.python import PythonDependency
100+
from posit_bakery.config.dependencies.quarto import QuartoDependency
101+
from posit_bakery.config.dependencies.r import RDependency
102+
from posit_bakery.util import cached_session
103+
104+
cached_session.cache_clear()
105+
PythonDependency._fetch_versions.cache_clear()
106+
RDependency._fetch_versions.cache_clear()
107+
QuartoDependency._fetch_versions.cache_clear()
108+
PositronDependency._fetch_versions.cache_clear()
109+
95110
return mocker.patch("posit_bakery.util.CachedSession", spec=requests.Session)
96111

97112

0 commit comments

Comments
 (0)