11import tempfile
22import uuid
3+ from dataclasses import dataclass
4+ from dataclasses import field
35from enum import Enum
46from pathlib import Path
57from types import MappingProxyType
8+ from typing import Any
9+ from typing import Mapping
610from typing import Optional
11+ from typing import Union
712
813
914class 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