Skip to content

Commit 27b6c69

Browse files
Merge pull request #1322 from linsword13/ws-cache
Add a decorator for Ramble object to cache command results
2 parents b3837e7 + 93b9959 commit 27b6c69

File tree

5 files changed

+99
-2
lines changed

5 files changed

+99
-2
lines changed

lib/ramble/ramble/workspace/workspace.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,9 @@ def __init__(self, root, dry_run=False, read_default_template=True):
478478
# This is currently used as a cache for reading per-object template contents.
479479
self._inmem_file_cache = {}
480480

481+
# A workspace-level cache that's used by objects to cache command returns.
482+
self.object_command_cache = {}
483+
481484
self.results = self.default_results()
482485

483486
# A cache structured as {pkg_man: {env_name: pkg_list}}.

var/ramble/repos/builtin/base_classes/object-mixin/base_class.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
66
# option. This file may not be copied, modified, or distributed
77
# except according to those terms.
8+
import functools
89
import os
910
from html import escape
1011
from typing import List
@@ -210,3 +211,26 @@ def to_html_docs(self, out, obj_def):
210211

211212
def _format_docs_details(self, _out):
212213
"""Hook for objects to add extra documentation."""
214+
215+
@staticmethod
216+
def workspace_cache(method):
217+
"""
218+
A decorator that caches the result of a ramble object's instance method into the workspace.
219+
"""
220+
221+
@functools.wraps(method)
222+
def wrapper(obj, *args, **kwargs):
223+
# Precondition: the method should be called with a `workspace=` argument.
224+
ws = kwargs["workspace"]
225+
key = (
226+
obj.origin_type,
227+
obj.name,
228+
method.__name__,
229+
args,
230+
frozenset(kwargs.items()),
231+
)
232+
if key not in ws.object_command_cache:
233+
ws.object_command_cache[key] = method(obj, *args, **kwargs)
234+
return ws.object_command_cache[key]
235+
236+
return wrapper
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Copyright 2022-2025 The Ramble Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4+
# https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5+
# <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6+
# option. This file may not be copied, modified, or distributed
7+
# except according to those terms.
8+
import unittest.mock as mock
9+
10+
import pytest
11+
12+
from ramble.repository import get_base_class
13+
14+
ObjectMixin = get_base_class("object-mixin")
15+
16+
17+
class TestObject(ObjectMixin):
18+
"""A test class for the ObjectMixin's workspace_cache decorator."""
19+
20+
origin_type = "test-object-type"
21+
_name = "test-object"
22+
23+
def __init__(self):
24+
self.call_count = 0
25+
26+
@ObjectMixin.workspace_cache
27+
def cached_method(self, *args, **kwargs):
28+
self.call_count += 1
29+
return self.call_count
30+
31+
32+
def test_workspace_cache():
33+
obj = TestObject()
34+
workspace = mock.Mock()
35+
workspace.object_command_cache = {}
36+
37+
# First call
38+
result1 = obj.cached_method(1, 2, key="value", workspace=workspace)
39+
assert result1 == 1
40+
assert obj.call_count == 1
41+
42+
# Second call with same arguments
43+
result2 = obj.cached_method(1, 2, key="value", workspace=workspace)
44+
assert result2 == 1
45+
assert obj.call_count == 1
46+
47+
# Call with different args
48+
result3 = obj.cached_method(3, 4, key="value", workspace=workspace)
49+
assert result3 == 2
50+
assert obj.call_count == 2
51+
52+
# Call with different kwargs
53+
result4 = obj.cached_method(1, 2, key="new_value", workspace=workspace)
54+
assert result4 == 3
55+
assert obj.call_count == 3
56+
57+
# Verify cache contents
58+
assert len(workspace.object_command_cache) == 3
59+
60+
# The caching assumes workspace= argument is present
61+
with pytest.raises(KeyError, match="'workspace'"):
62+
obj.cached_method(1, 2)

var/ramble/repos/builtin/package_managers/pip/package_manager.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ def get_spec_str(self, pkg, all_pkgs, compiler):
214214
"""
215215
return pkg.spec
216216

217+
@PackageManagerBase.workspace_cache
218+
def get_version(self, workspace=None):
219+
return self.runner.get_version()
220+
217221
def populate_inventory(
218222
self, workspace, force_compute=False, require_exist=False
219223
):
@@ -223,7 +227,7 @@ def populate_inventory(
223227
self.runner.set_dry_run(workspace.dry_run)
224228
self.runner.configure_env(env_path)
225229

226-
pkgman_version = self.runner.get_version()
230+
pkgman_version = self.get_version(workspace=workspace)
227231

228232
self.app_inst.hash_inventory["package_manager"].append(
229233
{

var/ramble/repos/builtin/package_managers/spack-lightweight/package_manager.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,10 @@ def _push_to_spack_cache(self, workspace, app_inst=None):
400400
logger.die(e)
401401
pass
402402

403+
@PackageManagerBase.workspace_cache
404+
def get_version(self, workspace=None):
405+
return self.runner.get_version()
406+
403407
def populate_inventory(
404408
self, workspace, force_compute=False, require_exist=False
405409
):
@@ -411,7 +415,7 @@ def populate_inventory(
411415
self.runner.activate()
412416

413417
try:
414-
pkgman_version = self.runner.get_version()
418+
pkgman_version = self.get_version(workspace=workspace)
415419
except RunnerError:
416420
pkgman_version = "unknown"
417421

0 commit comments

Comments
 (0)