• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmc-dev / openmc / 23503641174

24 Mar 2026 05:36PM UTC coverage: 81.328% (-0.1%) from 81.447%
23503641174

Pull #3886

github

web-flow
Merge 63fd9c86f into 6cd39073b
Pull Request #3886: Implement python tally types

17611 of 25453 branches covered (69.19%)

Branch coverage included in aggregate %.

244 of 297 new or added lines in 12 files covered. (82.15%)

238 existing lines in 4 files now uncovered.

58250 of 67825 relevant lines covered (85.88%)

44553638.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.79
/openmc/checkvalue.py
1
import copy
11✔
2
import inspect
11✔
3
import os
11✔
4
from collections.abc import Iterable
11✔
5
import sys
11✔
6
import types
11✔
7
import typing
11✔
8

9
import numpy as np
11✔
10

11
# Type for arguments that accept file paths
12
PathLike = str | os.PathLike
11✔
13

14

15

16
def _isinstance(obj, expected_type, _memo=None):
11✔
17
    from openmc.tallies import (_SCORE_CLASSES, _FILTER_CLASSES, _NUCLIDE_CLASSES, _EMPTY_NUCLIDE_CLASSES, 
11✔
18
                                _VOLUME_SCORE_CLASSES, _VOLUME_FILTER_CLASSES, 
19
                                _SURFACE_SCORE_CLASSES, _SURFACE_FILTER_CLASSES, 
20
                                _PULSEHEIGHT_SCORE_CLASSES, _PULSEHEIGHT_FILTER_CLASSES)
21
    if _memo is None:
11✔
22
        _memo = set()
11✔
23
        
24
    if isinstance(expected_type, str):
11✔
NEW
25
        expected_type = typing.ForwardRef(expected_type)
×
26
        
27
    if isinstance(expected_type, typing.ForwardRef):
11✔
28
        # Recursion Guard: If we've seen this (object, type) pair, assume True to break cycle
29
        memo_key = (id(obj), id(expected_type))
11✔
30
        if memo_key in _memo:
11✔
31
            return True
11✔
32
        _memo.add(memo_key)
11✔
33
        
34
        # Resolve ForwardRef using caller's scope
35
        for frame, *_ in inspect.stack()[1:]:
11✔
36
            try:
11✔
37
                expected_type = expected_type._evaluate(frame.f_globals, frame.f_locals, recursive_guard=frozenset())
11✔
38
            except NameError:
11✔
39
                pass
11✔
40
            else:
41
                break    
11✔
42
        
43
    origin = typing.get_origin(expected_type)
11✔
44
    
45
    # Handle Union (e.g., Union[int, str] or int | str)
46
    if origin in (typing.Union, types.UnionType):
11✔
47
        return any(_isinstance(obj, t, _memo) for t in typing.get_args(expected_type))
11✔
48
        
49
    elif origin is typing.Literal:
11✔
50
        return any(obj == t for t in typing.get_args(expected_type))
11✔
51

52
    # Handle Generic Alias (e.g., list[int])
53
    if origin is not None:
11✔
54
        if not _isinstance(obj, origin, _memo):
11✔
55
            return False
11✔
56
        
57
        # Check inner types (e.g., the int in list[int])
58
        args = typing.get_args(expected_type)
11✔
59
        if not args:
11✔
NEW
60
            return True
×
61
        
62
        if origin is dict:
11✔
NEW
63
            k_type, v_type = args
×
NEW
64
            return all(_isinstance(k, k_type, _memo) and _isinstance(v, v_type, _memo) for k, v in obj.items())            
×
65
        
66
        if issubclass(origin, Iterable):
11✔
67
            return all(_isinstance(item, args[0], _memo) for item in obj)
11✔
68
            
69
    return isinstance(obj, expected_type)
11✔
70
    
71
def check_type(name, value, expected_type, expected_iter_type=None, *, none_ok=False):
11✔
72
    """Ensure that an object is of an expected type. Optionally, if the object is
73
    iterable, check that each element is of a particular type.
74

75
    Parameters
76
    ----------
77
    name : str
78
        Description of value being checked
79
    value : object
80
        Object to check type of
81
    expected_type : type or Iterable of type
82
        type to check object against
83
    expected_iter_type : type or Iterable of type or None, optional
84
        Expected type of each element in value, assuming it is iterable. If
85
        None, no check will be performed.
86
    none_ok : bool, optional
87
        Whether None is allowed as a value
88

89
    """
90
    if none_ok and value is None:
11✔
91
        return
11✔
92

93
    if not _isinstance(value, expected_type):
11✔
94
        if _isinstance(expected_type, Iterable):
11✔
95
            msg = 'Unable to set "{}" to "{}" which is not one of the ' \
11✔
96
                  'following types: "{}"'.format(name, value, ', '.join(
97
                      [t.__name__ for t in expected_type]))
98
        else:
99
            msg = (f'Unable to set "{name}" to "{value}" which is not of type "'
11✔
100
                   f'{expected_type}"')
101
        raise TypeError(msg)
11✔
102

103
    if expected_iter_type:
11✔
104
        if _isinstance(value, np.ndarray):
11✔
105
            if not issubclass(value.dtype.type, expected_iter_type):
11✔
106
                msg = (f'Unable to set "{name}" to "{value}" since each item '
×
107
                       f'must be of type "{expected_iter_type.__name__}"')
108
                raise TypeError(msg)
×
109
            else:
110
                return
11✔
111

112
        for item in value:
11✔
113
            if not _isinstance(item, expected_iter_type):
11✔
114
                if _isinstance(expected_iter_type, Iterable):
11✔
115
                    msg = 'Unable to set "{}" to "{}" since each item must be ' \
11✔
116
                          'one of the following types: "{}"'.format(
117
                              name, value, ', '.join([t.__name__ for t in
118
                                                      expected_iter_type]))
119
                else:
120
                    msg = (f'Unable to set "{name}" to "{value}" since each '
11✔
121
                           f'item must be of type "{expected_iter_type.__name__}"')
122
                raise TypeError(msg)
11✔
123

124

125
def check_iterable_type(name, value, expected_type, min_depth=1, max_depth=1):
11✔
126
    """Ensure that an object is an iterable containing an expected type.
127

128
    Parameters
129
    ----------
130
    name : str
131
        Description of value being checked
132
    value : Iterable
133
        Iterable, possibly of other iterables, that should ultimately contain
134
        the expected type
135
    expected_type : type
136
        type that the iterable should contain
137
    min_depth : int
138
        The minimum number of layers of nested iterables there should be before
139
        reaching the ultimately contained items
140
    max_depth : int
141
        The maximum number of layers of nested iterables there should be before
142
        reaching the ultimately contained items
143
    """
144
    # Initialize the tree at the very first item.
145
    tree = [value]
11✔
146
    index = [0]
11✔
147

148
    # Traverse the tree.
149
    while index[0] != len(tree[0]):
11✔
150
        # If we are done with this level of the tree, go to the next branch on
151
        # the level above this one.
152
        if index[-1] == len(tree[-1]):
11✔
153
            del index[-1]
11✔
154
            del tree[-1]
11✔
155
            index[-1] += 1
11✔
156
            continue
11✔
157

158
        # Get a string representation of the current index in case we raise an
159
        # exception.
160
        form = '[' + '{:d}, ' * (len(index)-1) + '{:d}]'
11✔
161
        ind_str = form.format(*index)
11✔
162

163
        # What is the current item we are looking at?
164
        current_item = tree[-1][index[-1]]
11✔
165

166
        # If this item is of the expected type, then we've reached the bottom
167
        # level of this branch.
168
        if _isinstance(current_item, expected_type):
11✔
169
            # Is this deep enough?
170
            if len(tree) < min_depth:
11✔
171
                msg = (f'Error setting "{name}": The item at {ind_str} does not '
×
172
                       f'meet the minimum depth of {min_depth}')
173
                raise TypeError(msg)
×
174

175
            # This item is okay.  Move on to the next item.
176
            index[-1] += 1
11✔
177

178
        # If this item is not of the expected type, then it's either an error or
179
        # on a deeper level of the tree.
180
        else:
181
            if _isinstance(current_item, Iterable):
11✔
182
                # The tree goes deeper here, let's explore it.
183
                tree.append(current_item)
11✔
184
                index.append(0)
11✔
185

186
                # But first, have we exceeded the max depth?
187
                if len(tree) > max_depth:
11✔
188
                    msg = (f'Error setting {name}: Found an iterable at '
×
189
                           f'{ind_str}, items in that iterable exceed the '
190
                           f'maximum depth of {max_depth}')
191
                    raise TypeError(msg)
×
192

193
            else:
194
                # This item is completely unexpected.
195
                msg = (f'Error setting {name}: Items must be of type '
11✔
196
                       f'"{expected_type.__name__}", but item at {ind_str} is '
197
                       f'of type "{type(current_item).__name__}"')
198
                raise TypeError(msg)
11✔
199

200

201
def check_length(name, value, length_min, length_max=None):
11✔
202
    """Ensure that a sized object has length within a given range.
203

204
    Parameters
205
    ----------
206
    name : str
207
        Description of value being checked
208
    value : collections.Sized
209
        Object to check length of
210
    length_min : int
211
        Minimum length of object
212
    length_max : int or None, optional
213
        Maximum length of object. If None, it is assumed object must be of
214
        length length_min.
215

216
    """
217

218
    if length_max is None:
11✔
219
        if len(value) < length_min:
11✔
220
            msg = (f'Unable to set "{name}" to "{value}" since it must be at '
11✔
221
                   f'least of length "{length_min}"')
222
            raise ValueError(msg)
11✔
223
    elif not length_min <= len(value) <= length_max:
11✔
224
        if length_min == length_max:
11✔
225
            msg = (f'Unable to set "{name}" to "{value}" since it must be of '
11✔
226
                  f'length "{length_min}"')
227
        else:
228
            msg = (f'Unable to set "{name}" to "{value}" since it must have '
×
229
                   f'length between "{length_min}" and "{length_max}"')
230
        raise ValueError(msg)
11✔
231

232

233
def check_increasing(name: str, value, equality: bool = False):
11✔
234
    """Ensure that a list's elements are strictly or loosely increasing.
235

236
    Parameters
237
    ----------
238
    name : str
239
        Description of value being checked
240
    value : iterable
241
        Object to check if increasing
242
    equality : bool, optional
243
        Whether equality is allowed. Defaults to False.
244

245
    """
246
    if equality:
11✔
247
        if not np.all(np.diff(value) >= 0.0):
×
248
            raise ValueError(f'Unable to set "{name}" to "{value}" since its '
×
249
                             'elements must be increasing.')
250
    elif not equality:
11✔
251
        if not np.all(np.diff(value) > 0.0):
11✔
252
            raise ValueError(f'Unable to set "{name}" to "{value}" since its '
11✔
253
                             'elements must be strictly increasing.')
254

255

256
def check_value(name, value, accepted_values):
11✔
257
    """Ensure that an object's value is contained in a set of acceptable values.
258

259
    Parameters
260
    ----------
261
    name : str
262
        Description of value being checked
263
    value : collections.Iterable
264
        Object to check
265
    accepted_values : collections.Container
266
        Container of acceptable values
267

268
    """
269

270
    if value not in accepted_values:
11✔
271
        msg = (f'Unable to set "{name}" to "{value}" since it is not in '
11✔
272
               f'"{accepted_values}"')
273
        raise ValueError(msg)
11✔
274

275

276
def check_less_than(name, value, maximum, equality=False):
11✔
277
    """Ensure that an object's value is less than a given value.
278

279
    Parameters
280
    ----------
281
    name : str
282
        Description of the value being checked
283
    value : object
284
        Object to check
285
    maximum : object
286
        Maximum value to check against
287
    equality : bool, optional
288
        Whether equality is allowed. Defaults to False.
289

290
    """
291

292
    if equality:
11✔
293
        if value > maximum:
11✔
294
            msg = (f'Unable to set "{name}" to "{value}" since it is greater '
11✔
295
                   f'than "{maximum}"')
296
            raise ValueError(msg)
11✔
297
    else:
298
        if value >= maximum:
11✔
299
            msg = (f'Unable to set "{name}" to "{value}" since it is greater '
11✔
300
                   f'than or equal to "{maximum}"')
301
            raise ValueError(msg)
11✔
302

303

304
def check_greater_than(name, value, minimum, equality=False):
11✔
305
    """Ensure that an object's value is greater than a given value.
306

307
    Parameters
308
    ----------
309
    name : str
310
        Description of the value being checked
311
    value : object
312
        Object to check
313
    minimum : object
314
        Minimum value to check against
315
    equality : bool, optional
316
        Whether equality is allowed. Defaults to False.
317

318
    """
319

320
    if equality:
11✔
321
        if value < minimum:
11✔
322
            msg = (f'Unable to set "{name}" to "{value}" since it is less than '
11✔
323
                   f'"{minimum}"')
324
            raise ValueError(msg)
11✔
325
    else:
326
        if value <= minimum:
11✔
327
            msg = (f'Unable to set "{name}" to "{value}" since it is less than '
11✔
328
                   f'or equal to "{minimum}"')
329
            raise ValueError(msg)
11✔
330

331

332
def check_filetype_version(obj, expected_type, expected_version):
11✔
333
    """Check filetype and version of an HDF5 file.
334

335
    Parameters
336
    ----------
337
    obj : h5py.File
338
        HDF5 file to check
339
    expected_type : str
340
        Expected file type, e.g. 'statepoint'
341
    expected_version : int
342
        Expected major version number.
343

344
    """
345
    try:
11✔
346
        this_filetype = obj.attrs['filetype'].decode()
11✔
347
        this_version = obj.attrs['version']
11✔
348

349
        # Check filetype
350
        if this_filetype != expected_type:
11✔
351
            raise IOError(f'{obj.filename} is not a {expected_type} file.')
×
352

353
        # Check version
354
        if this_version[0] != expected_version:
11✔
355
            raise IOError('{} file has a version of {} which is not '
×
356
                          'consistent with the version expected by OpenMC, {}'
357
                          .format(this_filetype,
358
                                  '.'.join(str(v) for v in this_version),
359
                                  expected_version))
360
    except AttributeError:
×
361
        raise IOError(f'Could not read {obj.filename} file. This most likely '
×
362
                      'means the file was produced by a different version of '
363
                      'OpenMC than the one you are using.')
364

365

366
class CheckedList(list):
11✔
367
    """A list for which each element is type-checked as it's added
368

369
    Parameters
370
    ----------
371
    expected_type : type or Iterable of type
372
        Type(s) which each element should be
373
    name : str
374
        Name of data being checked
375
    items : Iterable, optional
376
        Items to initialize the list with
377

378
    """
379

380
    def __init__(self, expected_type, name, items=None):
11✔
381
        super().__init__()
11✔
382
        self.expected_type = expected_type
11✔
383
        self.name = name
11✔
384
        if items is not None:
11✔
385
            for item in items:
11✔
386
                self.append(item)
11✔
387
                
388
    def __deepcopy__(self, memo):
11✔
389
        import copy
11✔
390
        cls = self.__class__
11✔
391
        result = cls.__new__(cls)
11✔
392
        memo[id(self)] = result
11✔
393
        for k, v in self.__dict__.items():
11✔
394
            if k != 'expected_type':
11✔
395
                setattr(result, k, copy.deepcopy(v, memo))
11✔
396
            else:
397
                setattr(result, k, v)
11✔
398
        for item in self:
11✔
399
            result.append(copy.deepcopy(item, memo))
11✔
400
        return result                
11✔
401

402
    def __add__(self, other):
11✔
403
        new_instance = copy.copy(self)
×
404
        new_instance += other
×
405
        return new_instance
×
406

407
    def __radd__(self, other):
11✔
408
        return self + other
×
409

410
    def __iadd__(self, other):
11✔
411
        check_type('CheckedList add operand', other, Iterable,
11✔
412
                   self.expected_type)
413
        for item in other:
11✔
414
            self.append(item)
11✔
415
        return self
11✔
416

417
    def append(self, item):
11✔
418
        """Append item to list
419

420
        Parameters
421
        ----------
422
        item : object
423
            Item to append
424

425
        """
426
        check_type(self.name, item, self.expected_type)
11✔
427
        super().append(item)
11✔
428

429
    def insert(self, index, item):
11✔
430
        """Insert item before index
431

432
        Parameters
433
        ----------
434
        index : int
435
            Index in list
436
        item : object
437
            Item to insert
438

439
        """
440
        check_type(self.name, item, self.expected_type)
11✔
441
        super().insert(index, item)
11✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc