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

idaholab / MontePy / 13821729733

12 Mar 2025 09:18PM UTC coverage: 98.082%. First build
13821729733

Pull #668

github

web-flow
Merge 3c814c82f into f26e57899
Pull Request #668: Implemented clear for material

115 of 118 new or added lines in 9 files covered. (97.46%)

7721 of 7872 relevant lines covered (98.08%)

0.98 hits per line

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

95.65
/montepy/input_parser/parser_base.py
1
# Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved.
2
from montepy.input_parser.tokens import MCNP_Lexer
1✔
3
from montepy.input_parser import syntax_node
1✔
4
from sly import Parser
1✔
5
import sly
1✔
6

7
_dec = sly.yacc._decorator
1✔
8

9

10
class MetaBuilder(sly.yacc.ParserMeta):
1✔
11
    """Custom MetaClass for allowing subclassing of MCNP_Parser.
12

13
    Note: overloading functions is not allowed.
14
    """
15

16
    protected_names = {
1✔
17
        "debugfile",
18
        "errok",
19
        "error",
20
        "index_position",
21
        "line_position",
22
        "log",
23
        "parse",
24
        "restart",
25
        "tokens",
26
        "dont_copy",
27
    }
28

29
    def __new__(meta, classname, bases, attributes):
1✔
30
        if classname != "MCNP_Parser":
1✔
31
            for basis in bases:
1✔
32
                MetaBuilder._flatten_rules(classname, basis, attributes)
1✔
33
        cls = super().__new__(meta, classname, bases, attributes)
1✔
34
        return cls
1✔
35

36
    @staticmethod
1✔
37
    def _flatten_rules(classname, basis, attributes):
1✔
38
        for attr_name in dir(basis):
1✔
39
            if (
1✔
40
                not attr_name.startswith("_")
41
                and attr_name not in MetaBuilder.protected_names
42
                and attr_name not in attributes.get("dont_copy", set())
43
            ):
44
                func = getattr(basis, attr_name)
1✔
45
                attributes[attr_name] = func
1✔
46
        parent = basis.__bases__
1✔
47
        for par_basis in parent:
1✔
48
            if par_basis != Parser:
1✔
49
                return
1✔
50

51

52
class SLY_Supressor:
1✔
53
    """This is a fake logger meant to mostly make warnings dissapear."""
54

55
    def __init__(self):
1✔
56
        self._parse_fail_queue = []
1✔
57

58
    def debug(self, msg, *args, **kwargs):
1✔
59
        pass
60

61
    info = debug
1✔
62

63
    warning = debug
1✔
64

65
    error = debug
1✔
66

67
    critical = debug
1✔
68

69
    def parse_error(self, msg, token=None, lineno=0, index=0):
1✔
70
        """Adds a SLY parsing error to the error queue for being dumped later.
71

72
        Parameters
73
        ----------
74
        msg : str
75
            The message to display.
76
        token : Token
77
            the token that caused the error if any.
78
        lineno : int
79
            the current lineno of the error (from SLY not the file), if
80
            any.
81
        """
82
        self._parse_fail_queue.append(
1✔
83
            {"message": msg, "token": token, "line": lineno, "index": index}
84
        )
85

86
    def clear_queue(self):
1✔
87
        """Clears the error queue and returns all errors.
88

89
        Returns a list of dictionaries. The dictionary has the keys: "message", "token", "line.
90

91
        Returns
92
        -------
93
        list
94
            A list of the errors since the queue was last cleared.
95
        """
96
        ret = self._parse_fail_queue
1✔
97
        self._parse_fail_queue = []
1✔
98
        return ret
1✔
99

100
    def __len__(self):
1✔
101
        return len(self._parse_fail_queue)
1✔
102

103

104
class MCNP_Parser(Parser, metaclass=MetaBuilder):
1✔
105
    """Base class for all MCNP parsers that provides basics."""
106

107
    # Remove this if trying to see issues with parser
108
    log = SLY_Supressor()
1✔
109
    tokens = MCNP_Lexer.tokens
1✔
110
    debugfile = None
1✔
111

112
    def restart(self):
1✔
113
        """Clears internal state information about the current parse.
114

115
        Should be ran before a new object is parsed.
116
        """
117
        self.log.clear_queue()
1✔
118
        super().restart()
1✔
119

120
    def parse(self, token_generator, input=None):
1✔
121
        """Parses the token stream and returns a syntax tree.
122

123
        If the parsing fails None will be returned.
124
        The error queue can be retrieved from ``parser.log.clear_queue()``.
125

126
        Parameters
127
        ----------
128
        token_generator : generator
129
            the token generator from ``lexer.tokenize``.
130
        input : Input
131
            the input that is being lexed and parsed.
132

133
        Returns
134
        -------
135
        SyntaxNode
136
        """
137
        self._input = input
1✔
138

139
        # debug every time a token is taken
140
        def gen_wrapper():
1✔
141
            while True:
×
142
                token = next(token_generator, None)
×
143
                self._debug_parsing_error(token)
×
144
                yield token
×
145

146
        # change to using `gen_wrapper()` to debug
147
        tree = super().parse(token_generator)
1✔
148
        # treat any previous errors as being fatal even if it recovered.
149
        if len(self.log) > 0:
1✔
150
            return None
1✔
151
        self.tokens = {}
1✔
152
        return tree
1✔
153

154
    precedence = (("left", SPACE), ("left", TEXT))
1✔
155

156
    @_("NUMBER", "NUMBER padding")
1✔
157
    def number_phrase(self, p):
1✔
158
        """A non-zero number with or without padding.
159

160
        Returns
161
        -------
162
        ValueNode
163
            a float ValueNode
164
        """
165
        return self._flush_phrase(p, float)
1✔
166

167
    @_("NUMBER", "NUMBER padding")
1✔
168
    def identifier_phrase(self, p):
1✔
169
        """A non-zero number with or without padding converted to int.
170

171
        Returns
172
        -------
173
        ValueNode
174
            an int ValueNode
175
        """
176
        return self._flush_phrase(p, int)
1✔
177

178
    @_(
1✔
179
        "numerical_phrase",
180
        "shortcut_phrase",
181
        "number_sequence numerical_phrase",
182
        "number_sequence shortcut_phrase",
183
    )
184
    def number_sequence(self, p):
1✔
185
        """A list of numbers.
186

187
        Returns
188
        -------
189
        ListNode
190
        """
191
        if len(p) == 1:
1✔
192
            sequence = syntax_node.ListNode("number sequence")
1✔
193
            if type(p[0]) == syntax_node.ListNode:
1✔
194
                return p[0]
1✔
195
            sequence.append(p[0])
1✔
196
        else:
197
            sequence = p[0]
1✔
198
            if type(p[1]) == syntax_node.ListNode:
1✔
199
                for node in p[1].nodes:
×
200
                    sequence.append(node)
×
201
            else:
202
                sequence.append(p[1])
1✔
203
        return sequence
1✔
204

205
    @_(
1✔
206
        "numerical_phrase numerical_phrase",
207
        "shortcut_phrase",
208
        "even_number_sequence numerical_phrase numerical_phrase",
209
        "even_number_sequence shortcut_phrase",
210
    )
211
    def even_number_sequence(self, p):
1✔
212
        """
213
        A list of numbers with an even number of elements*.
214

215
        * shortcuts will break this.
216
        """
217
        if not hasattr(p, "even_number_sequence"):
1✔
218
            sequence = syntax_node.ListNode("number sequence")
1✔
219
            if type(p[0]) == syntax_node.ListNode:
1✔
NEW
220
                return p[0]
×
221
            sequence.append(p[0])
1✔
222
        else:
223
            sequence = p[0]
1✔
224
        if len(p) > 1:
1✔
225
            if type(p[1]) == syntax_node.ListNode:
1✔
NEW
226
                for node in p[1].nodes:
×
NEW
227
                    sequence.append(node)
×
228
            else:
229
                for idx in range(1, len(p)):
1✔
230
                    sequence.append(p[idx])
1✔
231
        return sequence
1✔
232

233
    @_("number_phrase", "null_phrase")
1✔
234
    def numerical_phrase(self, p):
1✔
235
        """Any number, including 0, with its padding.
236

237
        Returns
238
        -------
239
        ValueNode
240
            a float ValueNode
241
        """
242
        return p[0]
1✔
243

244
    @_("numerical_phrase", "shortcut_phrase")
1✔
245
    def shortcut_start(self, p):
1✔
246
        return p[0]
1✔
247

248
    @_(
1✔
249
        "shortcut_start NUM_REPEAT",
250
        "shortcut_start REPEAT",
251
        "shortcut_start NUM_MULTIPLY",
252
        "shortcut_start MULTIPLY",
253
        "shortcut_start NUM_INTERPOLATE padding number_phrase",
254
        "shortcut_start INTERPOLATE padding number_phrase",
255
        "shortcut_start NUM_LOG_INTERPOLATE padding number_phrase",
256
        "shortcut_start LOG_INTERPOLATE padding number_phrase",
257
        "NUM_JUMP",
258
        "JUMP",
259
    )
260
    def shortcut_sequence(self, p):
1✔
261
        """A shortcut (repeat, multiply, interpolate, or jump).
262

263
        Returns
264
        -------
265
        ShortcutNode
266
            the parsed shortcut.
267
        """
268
        short_cut = syntax_node.ShortcutNode(p)
1✔
269
        if isinstance(p[0], syntax_node.ShortcutNode):
1✔
270
            list_node = syntax_node.ListNode("next_shortcuts")
1✔
271
            list_node.append(p[0])
1✔
272
            list_node.append(short_cut)
1✔
273
            return list_node
1✔
274
        return short_cut
1✔
275

276
    @_("shortcut_sequence", "shortcut_sequence padding")
1✔
277
    def shortcut_phrase(self, p):
1✔
278
        """A complete shortcut, which should be used, and not shortcut_sequence.
279

280
        Returns
281
        -------
282
        ShortcutNode
283
            the parsed shortcut.
284
        """
285
        sequence = p.shortcut_sequence
1✔
286
        if len(p) == 2:
1✔
287
            sequence.end_padding = p.padding
1✔
288
        return sequence
1✔
289

290
    @_("NULL", "NULL padding")
1✔
291
    def null_phrase(self, p):
1✔
292
        """A zero number with or without its padding.
293

294
        Returns
295
        -------
296
        ValueNode
297
            a float ValueNode
298
        """
299
        return self._flush_phrase(p, float)
1✔
300

301
    @_("NULL", "NULL padding")
1✔
302
    def null_ident_phrase(self, p):
1✔
303
        """A zero number with or without its padding, for identification.
304

305
        Returns
306
        -------
307
        ValueNode
308
            an int ValueNode
309
        """
310
        return self._flush_phrase(p, int)
1✔
311

312
    @_("TEXT", "TEXT padding")
1✔
313
    def text_phrase(self, p):
1✔
314
        """A string with or without its padding.
315

316
        Returns
317
        -------
318
        ValueNode
319
            a str ValueNode.
320
        """
321
        return self._flush_phrase(p, str)
1✔
322

323
    def _flush_phrase(self, p, token_type):
1✔
324
        """Creates a ValueNode."""
325
        if len(p) > 1:
1✔
326
            padding = p[1]
1✔
327
        else:
328
            padding = None
1✔
329
        return syntax_node.ValueNode(p[0], token_type, padding)
1✔
330

331
    @_("SPACE", "DOLLAR_COMMENT", "COMMENT")
1✔
332
    def padding(self, p):
1✔
333
        """Anything that is not semantically significant: white space, and comments.
334

335
        Returns
336
        -------
337
        PaddingNode
338
            All sequential padding.
339
        """
340
        if hasattr(p, "DOLLAR_COMMENT") or hasattr(p, "COMMENT"):
1✔
341
            is_comment = True
1✔
342
        else:
343
            is_comment = False
1✔
344
        return syntax_node.PaddingNode(p[0], is_comment)
1✔
345

346
    @_("padding SPACE", "padding DOLLAR_COMMENT", "padding COMMENT", 'padding "&"')
1✔
347
    def padding(self, p):
1✔
348
        """Anything that is not semantically significant: white space, and comments.
349

350
        Returns
351
        -------
352
        PaddingNode
353
            All sequential padding.
354
        """
355
        if hasattr(p, "DOLLAR_COMMENT") or hasattr(p, "COMMENT"):
1✔
356
            is_comment = True
1✔
357
        else:
358
            is_comment = False
1✔
359
        p[0].append(p[1], is_comment)
1✔
360
        return p[0]
1✔
361

362
    @_("parameter", "parameters parameter")
1✔
363
    def parameters(self, p):
1✔
364
        """A list of the parameters (key, value pairs) for this input.
365

366
        Returns
367
        -------
368
        ParametersNode
369
            all parameters
370
        """
371
        if len(p) == 1:
1✔
372
            params = syntax_node.ParametersNode()
1✔
373
            param = p[0]
1✔
374
        else:
375
            params = p[0]
1✔
376
            param = p[1]
1✔
377
        params.append(param)
1✔
378
        return params
1✔
379

380
    @_(
1✔
381
        "classifier param_seperator number_sequence",
382
        "classifier param_seperator text_phrase",
383
    )
384
    def parameter(self, p):
1✔
385
        """A singular Key-value pair.
386

387
        Returns
388
        -------
389
        SyntaxNode
390
            the parameter.
391
        """
392
        return syntax_node.SyntaxNode(
1✔
393
            p.classifier.prefix.value,
394
            {"classifier": p.classifier, "seperator": p.param_seperator, "data": p[2]},
395
        )
396

397
    @_("file_atom", "file_name file_atom")
1✔
398
    def file_name(self, p):
1✔
399
        """A file name.
400

401
        Returns
402
        -------
403
        str
404
        """
405
        ret = p[0]
1✔
406
        if len(p) > 1:
1✔
407
            ret += p[1]
1✔
408
        return ret
1✔
409

410
    @_(
1✔
411
        "TEXT",
412
        "FILE_PATH",
413
        "NUMBER",
414
        "PARTICLE",
415
        "INTERPOLATE",
416
        "JUMP",
417
        "KEYWORD",
418
        "LOG_INTERPOLATE",
419
        "NULL",
420
        "REPEAT",
421
        "SURFACE_TYPE",
422
        "THERMAL_LAW",
423
        "ZAID",
424
        "NUMBER_WORD",
425
    )
426
    def file_atom(self, p):
1✔
427
        return p[0]
1✔
428

429
    @_("file_name", "file_name padding")
1✔
430
    def file_phrase(self, p):
1✔
431
        """A file name with or without its padding.
432

433
        Returns
434
        -------
435
        ValueNode
436
            a str ValueNode.
437
        """
438
        return self._flush_phrase(p, str)
1✔
439

440
    @_("padding", "equals_sign", "padding equals_sign")
1✔
441
    def param_seperator(self, p):
1✔
442
        """The seperation between a key and value for a parameter.
443

444
        Returns
445
        -------
446
        ValueNode
447
            a str ValueNode
448
        """
449
        padding = p[0]
1✔
450
        if len(p) > 1:
1✔
451
            padding += p[1]
1✔
452
        return padding
1✔
453

454
    @_('"="', '"=" padding')
1✔
455
    def equals_sign(self, p):
1✔
456
        """The seperation between a key and value for a parameter.
457

458
        Returns
459
        -------
460
        ValueNode
461
            a str ValueNode
462
        """
463
        padding = syntax_node.PaddingNode(p[0])
1✔
464
        if hasattr(p, "padding"):
1✔
465
            padding += p.padding
1✔
466
        return padding
1✔
467

468
    @_('":" part', 'particle_type "," part')
1✔
469
    def particle_type(self, p):
1✔
470
        if hasattr(p, "particle_type"):
1✔
471
            token = p.particle_type.token + "".join(list(p)[1:])
1✔
472
            particle_node = syntax_node.ParticleNode("data particles", token)
1✔
473
        else:
474
            particle_node = syntax_node.ParticleNode("data particles", "".join(list(p)))
1✔
475

476
        return particle_node
1✔
477

478
    @_("PARTICLE", "PARTICLE_SPECIAL")
1✔
479
    def part(self, p):
1✔
480
        return p[0]
1✔
481

482
    @_(
1✔
483
        "TEXT",
484
        "KEYWORD",
485
        "PARTICLE",
486
        "SOURCE_COMMENT",
487
        "TALLY_COMMENT",
488
    )
489
    def data_prefix(self, p):
1✔
490
        return syntax_node.ValueNode(p[0], str)
1✔
491

492
    @_(
1✔
493
        "modifier data_prefix",
494
        "data_prefix",
495
        "classifier NUMBER",
496
        "classifier NULL",
497
        "classifier particle_type",
498
    )
499
    def classifier(self, p):
1✔
500
        """The classifier of a data input.
501

502
        This represents the first word of the data input.
503
        E.g.: ``M4``, `IMP:N`, ``F104:p``
504

505
        Returns
506
        -------
507
        ClassifierNode
508
        """
509
        if hasattr(p, "classifier"):
1✔
510
            classifier = p.classifier
1✔
511
        else:
512
            classifier = syntax_node.ClassifierNode()
1✔
513

514
        if hasattr(p, "modifier"):
1✔
515
            classifier.modifier = syntax_node.ValueNode(p.modifier, str)
1✔
516
        if hasattr(p, "data_prefix"):
1✔
517
            classifier.prefix = p.data_prefix
1✔
518
        if hasattr(p, "NUMBER") or hasattr(p, "NULL"):
1✔
519
            if hasattr(p, "NUMBER"):
1✔
520
                num = p.NUMBER
1✔
521
            else:
522
                num = p.NULL
1✔
523
            classifier.number = syntax_node.ValueNode(num, int)
1✔
524
        if hasattr(p, "particle_type"):
1✔
525
            classifier.particles = p.particle_type
1✔
526
        return classifier
1✔
527

528
    @_("classifier padding", "classifier")
1✔
529
    def classifier_phrase(self, p):
1✔
530
        """A classifier with its padding.
531

532
        Returns
533
        -------
534
        ClassifierNode
535
        """
536
        classifier = p.classifier
1✔
537
        if len(p) > 1:
1✔
538
            classifier.padding = p.padding
1✔
539
        return classifier
1✔
540

541
    @_('"*"', "PARTICLE_SPECIAL")
1✔
542
    def modifier(self, p):
1✔
543
        """A character that modifies a classifier, e.g., ``*TR``.
544

545
        Returns
546
        -------
547
        str
548
            the modifier
549
        """
550
        if hasattr(p, "PARTICLE_SPECIAL"):
1✔
551
            if p.PARTICLE_SPECIAL == "*":
1✔
552
                return "*"
1✔
553
        return p[0]
1✔
554

555
    def error(self, token):
1✔
556
        """Default error handling.
557

558
        Puts the data into a queue that can be pulled out later for one final clear debug.
559

560
        Parameters
561
        ----------
562
        token : Token
563
            the token that broke the parsing rules.
564
        """
565
        if token:
1✔
566
            lineno = getattr(token, "lineno", 0)
1✔
567
            if self._input and self._input.lexer:
1✔
568
                lexer = self._input.lexer
1✔
569
                index = lexer.find_column(lexer.text, token)
1✔
570
            else:
571
                index = 0
1✔
572
            if lineno:
1✔
573
                self.log.parse_error(
1✔
574
                    f"sly: Syntax error at line {lineno}, token={token.type}\n",
575
                    token,
576
                    lineno,
577
                    index,
578
                )
579
            else:
580
                self.log.parse_error(
×
581
                    f"sly: Syntax error, token={token.type}", token, lineno
582
                )
583
        else:
584
            self.log.parse_error("sly: Parse error in input. EOF\n")
1✔
585

586
    def _debug_parsing_error(self, token):  # pragma: no cover
587
        """A function that should be called from error when debugging a parsing error.
588

589
        Call this from the method error. Also you will need the relevant debugfile to be set and saving the parser
590
        tables to file. e.g.,
591

592
        debugfile = 'parser.out'
593
        """
594
        print(f"********* New Parsing Error from: {type(self)} ************ ")
595
        print(f"Token: {token}")
596
        print(f"State: {self.state}, statestack: {self.statestack}")
597
        print(f"Symstack: {self.symstack}")
598
        print(f"Log length: {len(self.log)}")
599
        print()
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