1
1
import tempfile
2
2
import uuid
3
+ from dataclasses import dataclass
4
+ from dataclasses import field
3
5
from enum import Enum
4
6
from pathlib import Path
5
7
from types import MappingProxyType
8
+ from typing import Any
9
+ from typing import Mapping
6
10
from typing import Optional
11
+ from typing import Union
7
12
8
13
9
14
class EndUseOption (Enum ):
@@ -48,6 +53,7 @@ def __init__(self, params: Optional[MappingProxyType] = None, from_file_path: Op
48
53
self ._file_path = Path (tempfile .gettempdir (), f'geophires-input-params_{ uuid .uuid4 ()!s} .txt' )
49
54
50
55
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',
51
57
with open (from_file_path , encoding = 'UTF-8' ) as base_file :
52
58
with open (self ._file_path , 'a' , encoding = 'UTF-8' ) as f :
53
59
f .writelines (base_file .readlines ())
@@ -74,5 +80,82 @@ def as_text(self):
74
80
return f .read ()
75
81
76
82
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
+
78
88
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