Skip to content

Commit 2cbd3be

Browse files
authored
Runtime value checking of public API methods (#707)
Add appropriate runtime errors for incorrect types and null values for public API methods and class setters in application, ensemble, experiment, and job. [ committed by @juliaputko ] [ reviewed by @amandarichardsonn, @MattToast , @mellis13 ]
1 parent dbf7b72 commit 2cbd3be

File tree

9 files changed

+690
-30
lines changed

9 files changed

+690
-30
lines changed

smartsim/_core/utils/helpers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,18 @@ def unpack(value: _NestedJobSequenceType) -> t.Generator[Job, None, None]:
6969
:param value: Sequence containing elements of type Job or other
7070
sequences that are also of type _NestedJobSequenceType
7171
:return: flattened list of Jobs"""
72+
from smartsim.launchable.job import Job
7273

7374
for item in value:
75+
7476
if isinstance(item, t.Iterable):
77+
# string are iterable of string. Avoid infinite recursion
78+
if isinstance(item, str):
79+
raise TypeError("jobs argument was not of type Job")
7580
yield from unpack(item)
7681
else:
82+
if not isinstance(item, Job):
83+
raise TypeError("jobs argument was not of type Job")
7784
yield item
7885

7986

@@ -157,10 +164,13 @@ def expand_exe_path(exe: str) -> str:
157164
"""Takes an executable and returns the full path to that executable
158165
159166
:param exe: executable or file
167+
:raises ValueError: if no executable is provided
160168
:raises TypeError: if file is not an executable
161169
:raises FileNotFoundError: if executable cannot be found
162170
"""
163171

172+
if not exe:
173+
raise ValueError("No executable provided")
164174
# which returns none if not found
165175
in_path = which(exe)
166176
if not in_path:

smartsim/builders/ensemble.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from __future__ import annotations
2828

29+
import collections
2930
import copy
3031
import itertools
3132
import os
@@ -38,6 +39,7 @@
3839
from smartsim.entity.application import Application
3940
from smartsim.entity.files import EntityFiles
4041
from smartsim.launchable.job import Job
42+
from smartsim.settings.launch_settings import LaunchSettings
4143

4244
if t.TYPE_CHECKING:
4345
from smartsim.settings.launch_settings import LaunchSettings
@@ -137,7 +139,7 @@ def __init__(
137139
copy.deepcopy(exe_arg_parameters) if exe_arg_parameters else {}
138140
)
139141
"""The parameters and values to be used when configuring entities"""
140-
self._files = copy.deepcopy(files) if files else None
142+
self._files = copy.deepcopy(files) if files else EntityFiles()
141143
"""The files to be copied, symlinked, and/or configured prior to execution"""
142144
self._file_parameters = (
143145
copy.deepcopy(file_parameters) if file_parameters else {}
@@ -163,7 +165,11 @@ def exe(self, value: str | os.PathLike[str]) -> None:
163165
"""Set the executable.
164166
165167
:param value: the executable
168+
:raises TypeError: if the exe argument is not str or PathLike str
166169
"""
170+
if not isinstance(value, (str, os.PathLike)):
171+
raise TypeError("exe argument was not of type str or PathLike str")
172+
167173
self._exe = os.fspath(value)
168174

169175
@property
@@ -179,7 +185,15 @@ def exe_args(self, value: t.Sequence[str]) -> None:
179185
"""Set the executable arguments.
180186
181187
:param value: the executable arguments
188+
:raises TypeError: if exe_args is not sequence of str
182189
"""
190+
191+
if not (
192+
isinstance(value, collections.abc.Sequence)
193+
and (all(isinstance(x, str) for x in value))
194+
):
195+
raise TypeError("exe_args argument was not of type sequence of str")
196+
183197
self._exe_args = list(value)
184198

185199
@property
@@ -197,11 +211,36 @@ def exe_arg_parameters(
197211
"""Set the executable argument parameters.
198212
199213
:param value: the executable argument parameters
214+
:raises TypeError: if exe_arg_parameters is not mapping
215+
of str and sequences of sequences of strings
200216
"""
217+
218+
if not (
219+
isinstance(value, collections.abc.Mapping)
220+
and (
221+
all(
222+
isinstance(key, str)
223+
and isinstance(val, collections.abc.Sequence)
224+
and all(
225+
isinstance(subval, collections.abc.Sequence) for subval in val
226+
)
227+
and all(
228+
isinstance(item, str)
229+
for item in itertools.chain.from_iterable(val)
230+
)
231+
for key, val in value.items()
232+
)
233+
)
234+
):
235+
raise TypeError(
236+
"exe_arg_parameters argument was not of type "
237+
"mapping of str and sequences of sequences of strings"
238+
)
239+
201240
self._exe_arg_parameters = copy.deepcopy(value)
202241

203242
@property
204-
def files(self) -> t.Union[EntityFiles, None]:
243+
def files(self) -> EntityFiles:
205244
"""Return attached EntityFiles object.
206245
207246
:return: the EntityFiles object of files to be copied, symlinked,
@@ -210,12 +249,16 @@ def files(self) -> t.Union[EntityFiles, None]:
210249
return self._files
211250

212251
@files.setter
213-
def files(self, value: t.Optional[EntityFiles]) -> None:
252+
def files(self, value: EntityFiles) -> None:
214253
"""Set the EntityFiles object.
215254
216255
:param value: the EntityFiles object of files to be copied, symlinked,
217256
and/or configured prior to execution
257+
:raises TypeError: if files is not of type EntityFiles
218258
"""
259+
260+
if not isinstance(value, EntityFiles):
261+
raise TypeError("files argument was not of type EntityFiles")
219262
self._files = copy.deepcopy(value)
220263

221264
@property
@@ -231,7 +274,26 @@ def file_parameters(self, value: t.Mapping[str, t.Sequence[str]]) -> None:
231274
"""Set the file parameters.
232275
233276
:param value: the file parameters
277+
:raises TypeError: if file_parameters is not a mapping of str and
278+
sequence of str
234279
"""
280+
281+
if not (
282+
isinstance(value, t.Mapping)
283+
and (
284+
all(
285+
isinstance(key, str)
286+
and isinstance(val, collections.abc.Sequence)
287+
and all(isinstance(subval, str) for subval in val)
288+
for key, val in value.items()
289+
)
290+
)
291+
):
292+
raise TypeError(
293+
"file_parameters argument was not of type mapping of str "
294+
"and sequence of str"
295+
)
296+
235297
self._file_parameters = dict(value)
236298

237299
@property
@@ -249,7 +311,15 @@ def permutation_strategy(
249311
"""Set the permutation strategy
250312
251313
:param value: the permutation strategy
314+
:raises TypeError: if permutation_strategy is not str or
315+
PermutationStrategyType
252316
"""
317+
318+
if not (callable(value) or isinstance(value, str)):
319+
raise TypeError(
320+
"permutation_strategy argument was not of "
321+
"type str or PermutationStrategyType"
322+
)
253323
self._permutation_strategy = value
254324

255325
@property
@@ -265,7 +335,11 @@ def max_permutations(self, value: int) -> None:
265335
"""Set the maximum permutations
266336
267337
:param value: the max permutations
338+
:raises TypeError: max_permutations argument was not of type int
268339
"""
340+
if not isinstance(value, int):
341+
raise TypeError("max_permutations argument was not of type int")
342+
269343
self._max_permutations = value
270344

271345
@property
@@ -281,7 +355,13 @@ def replicas(self, value: int) -> None:
281355
"""Set the number of replicas.
282356
283357
:return: the number of replicas
358+
:raises TypeError: replicas argument was not of type int
284359
"""
360+
if not isinstance(value, int):
361+
raise TypeError("replicas argument was not of type int")
362+
if value <= 0:
363+
raise ValueError("Number of replicas must be a positive integer")
364+
285365
self._replicas = value
286366

287367
def _create_applications(self) -> tuple[Application, ...]:
@@ -342,7 +422,11 @@ def build_jobs(self, settings: LaunchSettings) -> tuple[Job, ...]:
342422
343423
:param settings: LaunchSettings to apply to each Job
344424
:return: Sequence of Jobs with the provided LaunchSettings
425+
:raises TypeError: if the ids argument is not type LaunchSettings
426+
:raises ValueError: if the LaunchSettings provided are empty
345427
"""
428+
if not isinstance(settings, LaunchSettings):
429+
raise TypeError("ids argument was not of type LaunchSettings")
346430
apps = self._create_applications()
347431
if not apps:
348432
raise ValueError("There are no members as part of this ensemble")

smartsim/entity/application.py

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def __init__(
8888
"""The executable to run"""
8989
self._exe_args = self._build_exe_args(exe_args) or []
9090
"""The executable arguments"""
91-
self._files = copy.deepcopy(files) if files else None
91+
self._files = copy.deepcopy(files) if files else EntityFiles()
9292
"""Files to be copied, symlinked, and/or configured prior to execution"""
9393
self._file_parameters = (
9494
copy.deepcopy(file_parameters) if file_parameters else {}
@@ -112,8 +112,16 @@ def exe(self, value: str) -> None:
112112
"""Set the executable.
113113
114114
:param value: the executable
115+
:raises TypeError: exe argument is not int
116+
115117
"""
116-
self._exe = copy.deepcopy(value)
118+
if not isinstance(value, str):
119+
raise TypeError("exe argument was not of type str")
120+
121+
if value == "":
122+
raise ValueError("exe cannot be an empty str")
123+
124+
self._exe = value
117125

118126
@property
119127
def exe_args(self) -> t.MutableSequence[str]:
@@ -149,12 +157,18 @@ def files(self) -> t.Union[EntityFiles, None]:
149157
return self._files
150158

151159
@files.setter
152-
def files(self, value: t.Optional[EntityFiles]) -> None:
160+
def files(self, value: EntityFiles) -> None:
153161
"""Set the EntityFiles object.
154162
155163
:param value: the EntityFiles object of files to be copied, symlinked,
156164
and/or configured prior to execution
165+
:raises TypeError: files argument was not of type int
166+
157167
"""
168+
169+
if not isinstance(value, EntityFiles):
170+
raise TypeError("files argument was not of type EntityFiles")
171+
158172
self._files = copy.deepcopy(value)
159173

160174
@property
@@ -170,7 +184,18 @@ def file_parameters(self, value: t.Mapping[str, str]) -> None:
170184
"""Set the file parameters.
171185
172186
:param value: the file parameters
187+
:raises TypeError: file_parameters argument is not a mapping of str and str
173188
"""
189+
if not (
190+
isinstance(value, t.Mapping)
191+
and all(
192+
isinstance(key, str) and isinstance(val, str)
193+
for key, val in value.items()
194+
)
195+
):
196+
raise TypeError(
197+
"file_parameters argument was not of type mapping of str and str"
198+
)
174199
self._file_parameters = copy.deepcopy(value)
175200

176201
@property
@@ -186,7 +211,15 @@ def incoming_entities(self, value: t.List[SmartSimEntity]) -> None:
186211
"""Set the incoming entities.
187212
188213
:param value: incoming entities
214+
:raises TypeError: incoming_entities argument is not a list of SmartSimEntity
189215
"""
216+
if not isinstance(value, list) or not all(
217+
isinstance(x, SmartSimEntity) for x in value
218+
):
219+
raise TypeError(
220+
"incoming_entities argument was not of type list of SmartSimEntity"
221+
)
222+
190223
self._incoming_entities = copy.copy(value)
191224

192225
@property
@@ -202,7 +235,11 @@ def key_prefixing_enabled(self, value: bool) -> None:
202235
"""Set whether key prefixing is enabled for the application.
203236
204237
:param value: key prefixing enabled
238+
:raises TypeError: key prefixings enabled argument was not of type bool
205239
"""
240+
if not isinstance(value, bool):
241+
raise TypeError("key_prefixing_enabled argument was not of type bool")
242+
206243
self.key_prefixing_enabled = copy.deepcopy(value)
207244

208245
def as_executable_sequence(self) -> t.Sequence[str]:
@@ -264,8 +301,6 @@ def attached_files_table(self) -> str:
264301
265302
:return: String version of table
266303
"""
267-
if not self.files:
268-
return "No file attached to this application."
269304
return str(self.files)
270305

271306
@staticmethod

0 commit comments

Comments
 (0)