Skip to content

Commit d7eb31e

Browse files
Add ImmutableGeophiresInputParameters to enable caching behavior in GeophiresXClient (GeophiresInputParameters faulty hashing implementation prevents caching from working)
1 parent 370e136 commit d7eb31e

File tree

5 files changed

+688
-8
lines changed

5 files changed

+688
-8
lines changed

src/geophires_x_client/geophires_input_parameters.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import tempfile
22
import uuid
3+
from dataclasses import dataclass
4+
from dataclasses import field
35
from enum import Enum
46
from pathlib import Path
57
from types import MappingProxyType
8+
from typing import Any
9+
from typing import Mapping
610
from typing import Optional
11+
from typing import Union
712

813

914
class EndUseOption(Enum):
@@ -48,6 +53,7 @@ def __init__(self, params: Optional[MappingProxyType] = None, from_file_path: Op
4853
self._file_path = Path(tempfile.gettempdir(), f'geophires-input-params_{uuid.uuid4()!s}.txt')
4954

5055
if from_file_path is not None:
56+
# Note: This has a potential race condition if the file doesn't exist at the time of 'a',
5157
with open(from_file_path, encoding='UTF-8') as base_file:
5258
with open(self._file_path, 'a', encoding='UTF-8') as f:
5359
f.writelines(base_file.readlines())
@@ -74,5 +80,82 @@ def as_text(self):
7480
return f.read()
7581

7682
def __hash__(self):
77-
"""TODO make hashes for equivalent parameters equal"""
83+
"""
84+
Note hashes for equivalent parameters may not be equal.
85+
Use ImmutableGeophiresInputParameters instead.
86+
"""
87+
7888
return self._id
89+
90+
91+
@dataclass(frozen=True)
92+
class ImmutableGeophiresInputParameters(GeophiresInputParameters):
93+
"""
94+
An immutable, self-contained, and content-hashable set of GEOPHIRES
95+
input parameters.
96+
97+
This class is hashable based on its logical content, making it safe for
98+
caching. It generates its file representation on-demand and is designed
99+
for use cases where parameter sets must be treated as immutable values.
100+
"""
101+
102+
params: Mapping[str, Any] = field(default_factory=lambda: MappingProxyType({}))
103+
from_file_path: Union[Path, None] = None
104+
105+
# A unique ID for this instance, used for file I/O but not for hashing or equality.
106+
_instance_id: uuid.UUID = field(default_factory=uuid.uuid4, init=False, repr=False, compare=False)
107+
108+
def __post_init__(self):
109+
"""Ensures that the parameters dictionary is immutable."""
110+
if not isinstance(self.params, MappingProxyType):
111+
# object.__setattr__ is required to modify a field in a frozen dataclass
112+
object.__setattr__(self, 'params', MappingProxyType(self.params))
113+
114+
def __hash__(self) -> int:
115+
"""
116+
Computes a hash based on the content of the parameters.
117+
If a base file is used, its content is read and hashed to ensure
118+
the hash reflects a true snapshot of all inputs.
119+
"""
120+
121+
param_hash = hash(frozenset(self.params.items()))
122+
123+
if self.from_file_path is not None and self.from_file_path.exists():
124+
file_content_hash = hash(self.from_file_path.read_bytes())
125+
else:
126+
file_content_hash = hash(self.from_file_path)
127+
128+
return hash((param_hash, file_content_hash))
129+
130+
def as_file_path(self) -> Path:
131+
"""
132+
Creates a temporary file representation of the parameters on demand.
133+
The resulting file path is cached for efficiency.
134+
"""
135+
136+
# Return the cached path if the file has already been generated for this instance.
137+
if hasattr(self, '_cached_file_path'):
138+
return self._cached_file_path
139+
140+
file_path = Path(tempfile.gettempdir(), f'geophires-input-params_{self._instance_id!s}.txt')
141+
142+
with open(file_path, 'w', encoding='UTF-8') as f:
143+
if self.from_file_path is not None:
144+
with open(self.from_file_path, encoding='UTF-8') as base_file:
145+
f.write(base_file.read())
146+
147+
if self.params:
148+
# Ensure there is a newline between the base file content and appended params.
149+
if self.from_file_path is not None and f.tell() > 0:
150+
f.seek(f.tell() - 1)
151+
if f.read(1) != '\n':
152+
f.write('\n')
153+
f.writelines([f'{key}, {value}\n' for key, value in self.params.items()])
154+
155+
# Cache the path on the instance after creation.
156+
object.__setattr__(self, '_cached_file_path', file_path)
157+
return file_path
158+
159+
def get_output_file_path(self) -> Path:
160+
"""Returns a unique path for the GEOPHIRES output file."""
161+
return Path(tempfile.gettempdir(), f'geophires-result_{self._instance_id!s}.out')

0 commit comments

Comments
 (0)