Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/finchlite/util/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from .cache import file_cache
# AI modified: 2025-01-01T00:00:00Z parent=4f5a2e5021678965ce8d830bb9edecac1dd3fea9
# AI modified: 2025-01-01T00:01:00Z parent=4f5a2e5021678965ce8d830bb9edecac1dd3fea9
from .cache import clear_cache, file_cache
from .config import config, get_version
from .print import qual_str

__all__ = ["config", "file_cache", "get_version", "qual_str"]
__all__ = ["clear_cache", "config", "file_cache", "get_version", "qual_str"]
80 changes: 79 additions & 1 deletion src/finchlite/util/cache.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# AI modified: 2024-12-31T23:58:00Z parent=154b5aeaa66d01a2373296ba9af9705a3db73ed9
# AI modified: 2024-12-31T23:59:00Z parent=06953a764918de34b3a35c1b698198c3b74c5890
# AI modified: 2025-01-01T00:00:00Z parent=4f5a2e5021678965ce8d830bb9edecac1dd3fea9
# AI modified: 2025-01-01T00:01:00Z parent=4f5a2e5021678965ce8d830bb9edecac1dd3fea9
# AI modified: 2026-04-02T22:59:00Z parent=197d5a907823d2a53fcd3b68b674f3f4d4f50b5d
# AI modified: 2026-04-03T15:30:00Z parent=36276c257318d74488f81fa8107d2f2d0a8b804c
import atexit
import shutil
import tempfile
Expand All @@ -6,9 +12,76 @@
from pathlib import Path
from uuid import UUID

import finchlite

from .config import config, get_version

finch_uuid = UUID("ef66f312-ff6e-4b8a-bb8c-9a843f3ecdf4")
cache_timestamp_filename = ".finch_code_mtime_ns"
_finch_source_root = Path(finchlite.__path__[0])


def _latest_finch_code_mtime_ns() -> int:
latest_mtime = 0
for path in _finch_source_root.rglob("*"):
if (
"__pycache__" not in path.parts
and path.is_file()
and path.suffix not in {".pyc", ".pyo"}
):
latest_mtime = max(latest_mtime, path.stat().st_mtime_ns)
return latest_mtime


_session_finch_code_mtime_ns = _latest_finch_code_mtime_ns()
_cache_checked = False


def _clear_cache_root(cache_root: Path, *, keep_timestamp: bool = True) -> None:
for path in cache_root.iterdir():
if keep_timestamp and path.name == cache_timestamp_filename:
continue
if path.is_dir():
shutil.rmtree(path)
else:
path.unlink()


def clear_cache() -> None:
"""Clear Finch's persistent cache for the current Finch version.

This removes all cached files for the active Finch version under
``<data_path>/cache/<version>``. If the cache directory does not exist,
this function does nothing.
"""

global _cache_checked
cache_root = Path(config.get("data_path")) / "cache" / get_version()
if cache_root.exists():
_clear_cache_root(cache_root, keep_timestamp=False)
_cache_checked = False


def _ensure_cache_fresh(cache_root: Path) -> None:
cache_root.mkdir(parents=True, exist_ok=True)
timestamp_file = cache_root / cache_timestamp_filename
current_mtime = _session_finch_code_mtime_ns
should_clear = False

if timestamp_file.exists():
try:
cached_mtime = int(timestamp_file.read_text().strip())
except ValueError:
should_clear = True
else:
should_clear = current_mtime > cached_mtime
else:
should_clear = True

if should_clear:
_clear_cache_root(cache_root)

timestamp_file.write_text(str(current_mtime))


def file_cache(*, ext: str, domain: str) -> Callable:
Expand All @@ -28,7 +101,12 @@ def decorator(f: Callable) -> Callable:
nonlocal ext
ext = ext.lstrip(".")
if config.get("cache_enable"):
cache_dir = Path(config.get("data_path")) / "cache" / get_version() / domain
global _cache_checked
cache_root = Path(config.get("data_path")) / "cache" / get_version()
if not _cache_checked:
_ensure_cache_fresh(cache_root)
_cache_checked = True
cache_dir = cache_root / domain
else:
cache_dir = Path(
tempfile.mkdtemp(
Expand Down
78 changes: 78 additions & 0 deletions tests/test_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# AI modified: 2025-01-01T00:00:00Z parent=4f5a2e5021678965ce8d830bb9edecac1dd3fea9

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need an end-to-end test that runs the c_codegen, changes something about c codegen, and then runs it again in a new session and verifies the cache was invalidated and the new output is used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 9879952: replaced the cache test with a c_codegen end-to-end multi-session test that runs codegen, modifies c_codegen.py behavior, runs again in a new session, and verifies the new output is used (7 then 9). No UI changes (backend-only), so there is no screenshot.

# AI modified: 2025-01-01T00:01:00Z parent=4f5a2e5021678965ce8d830bb9edecac1dd3fea9
# AI modified: 2026-04-02T22:59:00Z parent=197d5a907823d2a53fcd3b68b674f3f4d4f50b5d
# AI modified: 2026-04-03T15:30:00Z parent=36276c257318d74488f81fa8107d2f2d0a8b804c
import os
import re
import subprocess
import sys
from pathlib import Path

import finchlite


def _run_codegen_session(
repo_root: Path,
data_path: Path,
python_code: str,
):
env = os.environ.copy()
env["FINCHLITE_DATA_PATH"] = str(data_path)
return subprocess.run(
[sys.executable, "-c", python_code],
cwd=repo_root,
env=env,
capture_output=True,
text=True,
check=True,
)


def _restore_file_state(path: Path, *, contents: str, atime_ns: int, mtime_ns: int):
path.write_text(contents)
os.utime(path, ns=(atime_ns, mtime_ns))


def test_c_codegen_cache_invalidation_end_to_end_across_sessions(tmp_path):
# finchlite.__path__[0] points to <repo>/src/finchlite, so parent.parent is repo root.
repo_root = Path(finchlite.__path__[0]).parent.parent
data_path = tmp_path / "finch_data"
c_codegen_file = repo_root / "src" / "finchlite" / "codegen" / "c_codegen.py"
stat_before = c_codegen_file.stat()
original_contents = c_codegen_file.read_text()
base_script = """
import finchlite.codegen.c_codegen as ccg

code = '''
int unique_value() {
return 7;
}
'''
ccg.load_shared_lib.cache_clear()
lib = ccg.load_shared_lib(code)
print("RESULT", int(lib.unique_value()))
"""

first = _run_codegen_session(repo_root, data_path, base_script)
first_result = int(first.stdout.strip().split("RESULT ", 1)[1])
assert first_result == 7

try:
modified_contents = re.sub(
r"c_file_path\.write_text\(c_code\)",
'c_file_path.write_text(c_code.replace("return 7;", "return 9;"))',
original_contents,
count=1,
)
c_codegen_file.write_text(modified_contents)

second = _run_codegen_session(repo_root, data_path, base_script)
second_result = int(second.stdout.strip().split("RESULT ", 1)[1])
assert second_result == 9
finally:
_restore_file_state(
c_codegen_file,
contents=original_contents,
atime_ns=stat_before.st_atime_ns,
mtime_ns=stat_before.st_mtime_ns,
)