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

idaholab / MontePy / 18238605364

04 Oct 2025 02:54AM UTC coverage: 97.865% (-0.1%) from 97.963%
18238605364

Pull #814

github

web-flow
Merge dad679e43 into 3d190b7b0
Pull Request #814: Full test suite re-org

7974 of 8148 relevant lines covered (97.86%)

0.98 hits per line

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

98.31
/montepy/input_parser/syntax_node.py
1
# Copyright 2024-2025, Battelle Energy Alliance, LLC All Rights Reserved.
2
from abc import ABC, abstractmethod
1✔
3
import re
1✔
4
import warnings
1✔
5
import collections
1✔
6
import copy
1✔
7
import itertools as it
1✔
8
import enum
1✔
9
import math
1✔
10
from numbers import Integral, Real
1✔
11

12
from montepy import input_parser
1✔
13
from montepy import constants
1✔
14
from montepy.constants import rel_tol, abs_tol
1✔
15
from montepy.exceptions import *
1✔
16
from montepy.input_parser.shortcuts import Shortcuts
1✔
17
from montepy.geometry_operators import Operator
1✔
18
from montepy.particle import Particle
1✔
19
from montepy.utilities import fortran_float
1✔
20

21

22
class SyntaxNodeBase(ABC):
1✔
23
    """A base class for all syntax nodes.
24

25
    A syntax node is any component of the syntax tree
26
    for a parsed input.
27

28
    Parameters
29
    ----------
30
    name : str
31
        a name for labeling this node.
32
    """
33

34
    def __init__(self, name):
1✔
35
        self._name = name
1✔
36
        self._nodes = []
1✔
37

38
    def append(self, node):
1✔
39
        """Append the node to this node.
40

41
        Parameters
42
        ----------
43
        node : SyntaxNodeBase, str, None
44
            node
45
        """
46
        self._nodes.append(node)
1✔
47

48
    @property
1✔
49
    def nodes(self):
1✔
50
        """The children nodes of this node.
51

52
        Returns
53
        -------
54
        list
55
            a list of the nodes.
56
        """
57
        return self._nodes
1✔
58

59
    def __len__(self):
1✔
60
        return len(self.nodes)
1✔
61

62
    @property
1✔
63
    def name(self):
1✔
64
        """The name for the node.
65

66
        Returns
67
        -------
68
        str
69
            the node's name.
70
        """
71
        return self._name
1✔
72

73
    @name.setter
1✔
74
    def name(self, name):
1✔
75
        if not isinstance(name, str):
1✔
76
            raise TypeError("Name must be a string")
1✔
77
        self._name = name
78

79
    @abstractmethod
80
    def format(self):
81
        """Generate a string representing the tree's current state.
82

83
        Returns
84
        -------
85
        str
86
            the MCNP representation of the tree's current state.
87
        """
88
        pass
89

90
    @property
91
    @abstractmethod
92
    def comments(self):
93
        """A generator of all comments contained in this tree.
94

95
        Returns
96
        -------
97
        Generator
98
            the comments in the tree.
99
        """
100
        pass
101

102
    def get_trailing_comment(self):
1✔
103
        """Get the trailing ``c`` style  comments if any.
104

105
        Returns
106
        -------
107
        list
108
            The trailing comments of this tree.
109
        """
110
        if len(self.nodes) == 0:
1✔
111
            return
1✔
112
        tail = self.nodes[-1]
1✔
113
        if isinstance(tail, SyntaxNodeBase):
1✔
114
            return tail.get_trailing_comment()
1✔
115

116
    def _delete_trailing_comment(self):
1✔
117
        """Deletes the trailing comment if any."""
118
        if len(self.nodes) == 0:
1✔
119
            return
1✔
120
        tail = self.nodes[-1]
1✔
121
        if isinstance(tail, SyntaxNodeBase):
1✔
122
            tail._delete_trailing_comment()
1✔
123

124
    def _grab_beginning_comment(self, extra_padding):
1✔
125
        """Consumes the provided comment, and moves it to the beginning of this node.
126

127
        Parameters
128
        ----------
129
        extra_padding : list
130
            the padding comment to add to the beginning of this padding.
131
        """
132
        if len(self.nodes) == 0 or extra_padding is None:
×
133
            return
×
134
        head = self.nodes[0]
×
135
        if isinstance(head, SyntaxNodeBase):
×
136
            head._grab_beginning_comment(extra_padding)
×
137

138
    def check_for_graveyard_comments(self, has_following_input=False):
1✔
139
        """Checks if there is a graveyard comment that is preventing information from being part of the tree, and handles
140
        them.
141

142
        A graveyard comment is one that accidentally suppresses important information in the syntax tree.
143

144
        For example::
145

146
            imp:n=1 $ grave yard Vol=1
147

148
        Should be::
149

150
            imp:n=1 $ grave yard
151
            Vol=1
152

153
        These graveyards are handled by appending a new line, and the required number of continue spaces to the
154
        comment.
155

156
        .. versionadded:: 0.4.0
157

158
        Parameters
159
        ----------
160
        has_following_input : bool
161
            Whether there is another input (cell modifier) after this
162
            tree that should be continued.
163

164
        Returns
165
        -------
166
        None
167
        """
168
        flatpack = self.flatten()
1✔
169
        if len(flatpack) == 0:
1✔
170
            return
×
171
        first = flatpack[0]
1✔
172
        if has_following_input:
1✔
173
            flatpack.append("")
1✔
174
        for second in flatpack[1:]:
1✔
175
            if isinstance(first, ValueNode):
1✔
176
                padding = first.padding
1✔
177
            elif isinstance(first, PaddingNode):
1✔
178
                padding = first
1✔
179
            else:
180
                padding = None
1✔
181
            if padding:
1✔
182
                if padding.has_graveyard_comment() and not isinstance(
1✔
183
                    second, PaddingNode
184
                ):
185
                    padding.append("\n")
1✔
186
                    padding.append(" " * constants.BLANK_SPACE_CONTINUE)
1✔
187
            first = second
1✔
188

189
    def flatten(self):
1✔
190
        """Flattens this tree structure into a list of leaves.
191

192
        .. versionadded:: 0.4.0
193

194
        Returns
195
        -------
196
        list
197
            a list of ValueNode and PaddingNode objects from this tree.
198
        """
199
        ret = []
1✔
200
        for node in self.nodes:
1✔
201
            if node is None:
1✔
202
                continue
1✔
203
            if isinstance(node, (ValueNode, PaddingNode, CommentNode, str)):
1✔
204
                ret.append(node)
1✔
205
            else:
206
                ret += node.flatten()
1✔
207
        return ret
1✔
208

209
    def _pretty_str(self):
1✔
210
        INDENT = 2
1✔
211
        if not self.nodes:
1✔
212
            return f"<Node: {self.name}: []>"
×
213
        ret = f"<Node: {self.name}: [\n"
1✔
214
        for val in self.nodes:
1✔
215
            child_strs = val._pretty_str().split("\n")
1✔
216
            ret += "\n".join([" " * 2 * INDENT + s for s in child_strs[:-1]])
1✔
217
            ret += " " * 2 * INDENT + child_strs[-1] + ",\n"
1✔
218
        ret += " " * INDENT + "]\n"
1✔
219
        ret += ">"
1✔
220
        return ret
1✔
221

222

223
class SyntaxNode(SyntaxNodeBase):
1✔
224
    """A general syntax node for handling inner tree nodes.
225

226
    This is a generalized wrapper for a dictionary.
227
    The order of the dictionary is significant.
228

229
    This does behave like a dict for collecting items. e.g.,
230

231
    .. code-block:: python
232

233
        value = syntax_node["start_pad"]
234
        if key in syntax_node:
235
            pass
236

237
    Parameters
238
    ----------
239
    name : str
240
        a name for labeling this node.
241
    parse_dict : dict
242
        the dictionary of the syntax tree nodes.
243
    """
244

245
    def __init__(self, name, parse_dict):
1✔
246
        super().__init__(name)
1✔
247
        self._name = name
1✔
248
        self._nodes = parse_dict
1✔
249

250
    def __getitem__(self, key):
1✔
251
        return self.nodes[key]
1✔
252

253
    def __contains__(self, key):
1✔
254
        return key in self.nodes
1✔
255

256
    def get_value(self, key):
1✔
257
        """Get a value from the syntax tree.
258

259
        Parameters
260
        ----------
261
        key : str
262
            the key for the item to get.
263

264
        Returns
265
        -------
266
        SyntaxNodeBase
267
            the node in the syntax tree.
268

269
        Raises
270
        ------
271
        KeyError
272
            if key is not in SyntaxNode
273
        """
274
        temp = self.nodes[key]
1✔
275
        if isinstance(temp, ValueNode):
1✔
276
            return temp.value
1✔
277
        else:
278
            raise KeyError(f"{key} is not a value leaf node")
1✔
279

280
    def __str__(self):
1✔
281
        return f"<Node: {self.name}: {self.nodes}>"
1✔
282

283
    def __repr__(self):
1✔
284
        return str(self)
1✔
285

286
    def format(self):
1✔
287
        ret = ""
1✔
288
        for node in self.nodes.values():
1✔
289
            if isinstance(node, ValueNode):
1✔
290
                if node.value is not None:
1✔
291
                    ret += node.format()
1✔
292
            else:
293
                ret += node.format()
1✔
294
        return ret
1✔
295

296
    @property
1✔
297
    def comments(self):
1✔
298
        for node in self.nodes.values():
1✔
299
            yield from node.comments
1✔
300

301
    def get_trailing_comment(self):
1✔
302
        node = self._get_trailing_node()
1✔
303
        if node:
1✔
304
            return node.get_trailing_comment()
1✔
305

306
    def _grab_beginning_comment(self, extra_padding):
1✔
307
        """Consumes the provided comment, and moves it to the beginning of this node.
308

309
        Parameters
310
        ----------
311
        extra_padding : list
312
            the padding comment to add to the beginning of this padding.
313
        """
314
        if len(self.nodes) == 0 or extra_padding is None:
×
315
            return
×
316
        head = next(iter(self.nodes.values()))
×
317
        if isinstance(head, SyntaxNodeBase):
×
318
            head._grab_beginning_comment(extra_padding)
×
319

320
    def _get_trailing_node(self):
1✔
321
        if len(self.nodes) == 0:
1✔
322
            return
×
323
        for node in reversed(self.nodes.values()):
1✔
324
            if node is not None:
1✔
325
                if isinstance(node, ValueNode):
1✔
326
                    if node.value is not None:
1✔
327
                        return node
1✔
328
                elif len(node) > 0:
1✔
329
                    return node
1✔
330

331
    def _delete_trailing_comment(self):
1✔
332
        node = self._get_trailing_node()
1✔
333
        node._delete_trailing_comment()
1✔
334

335
    def flatten(self):
1✔
336
        ret = []
1✔
337
        for node in self.nodes.values():
1✔
338
            if isinstance(node, (ValueNode, PaddingNode)):
1✔
339
                ret.append(node)
1✔
340
            else:
341
                ret += node.flatten()
1✔
342
        return ret
1✔
343

344
    def _pretty_str(self):
1✔
345
        INDENT = 2
1✔
346
        ret = f"<Node: {self.name}: {{\n"
1✔
347
        for key, val in self.nodes.items():
1✔
348
            child_strs = val._pretty_str().split("\n")
1✔
349
            ret += " " * INDENT + f"{key}: {child_strs[0]}\n"
1✔
350
            ret += "\n".join([" " * 2 * INDENT + s for s in child_strs[1:-1]])
1✔
351
            ret += " " * 2 * INDENT + child_strs[-1] + ",\n"
1✔
352
        ret += " " * INDENT + "}\n"
1✔
353
        ret += ">"
1✔
354
        return ret
1✔
355

356

357
class GeometryTree(SyntaxNodeBase):
1✔
358
    """A syntax tree that is a binary tree for representing CSG geometry logic.
359

360
    .. versionchanged:: 0.4.1
361
        Added left/right_short_type
362

363
    Parameters
364
    ----------
365
    name : str
366
        a name for labeling this node.
367
    tokens : dict
368
        The nodes that are in the tree.
369
    op : str
370
        The string representation of the Operator to use.
371
    left : GeometryTree, ValueNode
372
        the node of the left side of the binary tree.
373
    right : GeometryTree, ValueNode
374
        the node of the right side of the binary tree.
375
    left_short_type : Shortcuts
376
        The type of Shortcut that right left leaf is involved in.
377
    right_short_type : Shortcuts
378
        The type of Shortcut that the right leaf is involved in.
379
    """
380

381
    def __init__(
1✔
382
        self,
383
        name,
384
        tokens,
385
        op,
386
        left,
387
        right=None,
388
        left_short_type=None,
389
        right_short_type=None,
390
    ):
391
        super().__init__(name)
1✔
392
        assert all(list(map(lambda v: isinstance(v, SyntaxNodeBase), tokens.values())))
1✔
393
        self._nodes = tokens
1✔
394
        self._operator = Operator(op)
1✔
395
        self._left_side = left
1✔
396
        self._right_side = right
1✔
397
        self._left_short_type = left_short_type
1✔
398
        self._right_short_type = right_short_type
1✔
399

400
    def __str__(self):
1✔
401
        return (
1✔
402
            f"Geometry: < {self._left_side}"
403
            f" {f'Short:{self._left_short_type.value}' if self._left_short_type else ''}"
404
            f" {self._operator} {self._right_side} "
405
            f"{f'Short:{self._right_short_type.value}' if self._right_short_type else ''}>"
406
        )
407

408
    def _pretty_str(self):
1✔
409
        INDENT = 2
1✔
410
        ret = f"<Geometry: {self.name}: [\n"
1✔
411
        for key, val in [
1✔
412
            ("left", self._left_side._pretty_str()),
413
            ("operator", self._operator),
414
            ("right", self._right_side),
415
        ]:
416
            if val is None:
1✔
417
                continue
×
418
            if isinstance(val, SyntaxNodeBase):
1✔
419
                child_strs = val._pretty_str().split("\n")
1✔
420
            else:
421
                child_strs = [str(val)]
1✔
422
            ret += " " * INDENT + f"{key}: {child_strs[0]}\n"
1✔
423
            ret += "\n".join([" " * 2 * INDENT + s for s in child_strs[1:-1]])
1✔
424
            ret += " " * 2 * INDENT + child_strs[-1] + ",\n"
1✔
425
        ret += " " * INDENT + "}\n"
1✔
426
        ret += ">"
1✔
427
        return ret
1✔
428

429
    def __repr__(self):
1✔
430
        return str(self)
1✔
431

432
    def format(self):
1✔
433
        if self._left_short_type or self._right_short_type:
1✔
434
            return self._format_shortcut()
1✔
435
        ret = ""
1✔
436
        for node in self.nodes.values():
1✔
437
            ret += node.format()
1✔
438
        return ret
1✔
439

440
    def mark_last_leaf_shortcut(self, short_type):
1✔
441
        """Mark the final (rightmost) leaf node in this tree as being a shortcut.
442

443
        Parameters
444
        ----------
445
        short_type : Shortcuts
446
            the type of shortcut that this leaf is.
447
        """
448
        if self.right is not None:
1✔
449
            node = self.right
1✔
450
            if self._right_short_type:
1✔
451
                return
1✔
452
        else:
453
            node = self.left
1✔
454
            if self._left_short_type:
1✔
455
                return
1✔
456
        if isinstance(node, type(self)):
1✔
457
            return node.mark_last_leaf_shortcut(short_type)
1✔
458
        if self.right is not None:
1✔
459
            self._right_short_type = short_type
1✔
460
        else:
461
            self._left_short_type = short_type
1✔
462

463
    def _flatten_shortcut(self):
1✔
464
        """Flattens this tree into a ListNode.
465

466
        This will add ShortcutNodes as well.
467

468
        Returns
469
        -------
470
        ListNode
471
        """
472

473
        def add_leaf(list_node, leaf, short_type):
1✔
474
            end = list_node.nodes[-1] if len(list_node) > 0 else None
1✔
475

476
            def flush_shortcut():
1✔
477
                end.load_nodes(end.nodes)
1✔
478

479
            def start_shortcut():
1✔
480
                short = ShortcutNode(short_type=short_type)
1✔
481
                # give an interpolate it's old beginning to give it right
482
                # start value
483
                if short_type in {
1✔
484
                    Shortcuts.LOG_INTERPOLATE,
485
                    Shortcuts.INTERPOLATE,
486
                } and isinstance(end, ShortcutNode):
487
                    short.append(end.nodes[-1])
1✔
488
                    short._has_pseudo_start = True
1✔
489

490
                short.append(leaf)
1✔
491
                if not leaf.padding:
1✔
492
                    leaf.padding = PaddingNode(" ")
1✔
493
                list_node.append(short)
1✔
494

495
            if short_type:
1✔
496
                if isinstance(end, ShortcutNode):
1✔
497
                    if end.type == short_type:
1✔
498
                        end.append(leaf)
1✔
499
                    else:
500
                        flush_shortcut()
1✔
501
                        start_shortcut()
1✔
502
                else:
503
                    start_shortcut()
1✔
504
            else:
505
                if isinstance(end, ShortcutNode):
1✔
506
                    flush_shortcut()
1✔
507
                    list_node.append(leaf)
1✔
508
                else:
509
                    list_node.append(leaf)
1✔
510

511
        if isinstance(self.left, ValueNode):
1✔
512
            ret = ListNode("list wrapper")
1✔
513
            add_leaf(ret, self.left, self._left_short_type)
1✔
514
        else:
515
            ret = self.left._flatten_shortcut()
1✔
516
        if self.right is not None:
1✔
517
            if isinstance(self.right, ValueNode):
1✔
518
                add_leaf(ret, self.right, self._right_short_type)
1✔
519
            else:
520
                [ret.append(n) for n in self.right._flatten_shortcut()]
1✔
521
        return ret
1✔
522

523
    def _format_shortcut(self):
1✔
524
        """Handles formatting a subset of tree that has shortcuts in it."""
525
        list_wrap = self._flatten_shortcut()
1✔
526
        if isinstance(list_wrap.nodes[-1], ShortcutNode):
1✔
527
            list_wrap.nodes[-1].load_nodes(list_wrap.nodes[-1].nodes)
1✔
528
        return list_wrap.format()
1✔
529

530
    @property
1✔
531
    def comments(self):
1✔
532
        for node in self.nodes.values():
1✔
533
            yield from node.comments
1✔
534

535
    @property
1✔
536
    def left(self):
1✔
537
        """The left side of the binary tree.
538

539
        Returns
540
        -------
541
        GeometryTree, ValueNode
542
            the left node of the syntax tree.
543
        """
544
        return self._left_side
1✔
545

546
    @property
1✔
547
    def right(self):
1✔
548
        """The right side of the binary tree.
549

550
        Returns
551
        -------
552
        GeometryTree, ValueNode
553
            the right node of the syntax tree.
554
        """
555
        return self._right_side
1✔
556

557
    @property
1✔
558
    def operator(self):
1✔
559
        """The operator used for the binary tree.
560

561
        Returns
562
        -------
563
        Operator
564
            the operator used.
565
        """
566
        return self._operator
1✔
567

568
    def __iter__(self):
1✔
569
        """Iterates over the leafs"""
570
        self._iter_l_r = False
1✔
571
        self._iter_complete = False
1✔
572
        self._sub_iter = None
1✔
573
        return self
1✔
574

575
    def __next__(self):
1✔
576
        if self._iter_complete:
1✔
577
            raise StopIteration
1✔
578
        if not self._iter_l_r:
1✔
579
            node = self.left
1✔
580
        if self._iter_l_r and self.right is not None:
1✔
581
            node = self.right
1✔
582
        if isinstance(node, ValueNode):
1✔
583
            if not self._iter_l_r:
1✔
584
                if self.right is not None:
1✔
585
                    self._iter_l_r = True
1✔
586
                else:
587
                    self._iter_complete = True
1✔
588
            else:
589
                self._iter_complete = True
1✔
590
            return node
1✔
591
        if self._sub_iter is None:
1✔
592
            self._sub_iter = iter(node)
1✔
593
        try:
1✔
594
            return next(self._sub_iter)
1✔
595
        except StopIteration:
1✔
596
            self._sub_iter = None
1✔
597
            if not self._iter_l_r:
1✔
598
                if self.right is not None:
1✔
599
                    self._iter_l_r = True
1✔
600
                else:
601
                    self._iter_complete = True
1✔
602
            else:
603
                raise StopIteration
1✔
604
            return next(self)
1✔
605

606
    def flatten(self):
1✔
607
        ret = []
1✔
608
        for node in self.nodes.values():
1✔
609
            if isinstance(node, (ValueNode, PaddingNode)):
1✔
610
                ret.append(node)
1✔
611
            else:
612
                ret += node.flatten()
1✔
613
        return ret
1✔
614

615

616
class PaddingNode(SyntaxNodeBase):
1✔
617
    """A syntax tree node to represent a collection of sequential padding elements.
618

619
    Parameters
620
    ----------
621
    token : str
622
        The first padding token for this node.
623
    is_comment : bool
624
        If the token provided is a comment.
625
    """
626

627
    def __init__(self, token=None, is_comment=False):
1✔
628
        super().__init__("padding")
1✔
629
        if token is not None:
1✔
630
            self.append(token, is_comment)
1✔
631

632
    def __str__(self):
1✔
633
        return f"<Padding, {self._nodes}>"
1✔
634

635
    def __repr__(self):
1✔
636
        return str(self)
1✔
637

638
    def __iadd__(self, other):
1✔
639
        if not isinstance(other, type(self)):
1✔
640
            raise TypeError(f"Can only combine with PaddingNodes. {other} given.")
×
641
        self._nodes += other.nodes
1✔
642
        return self
1✔
643

644
    @property
1✔
645
    def value(self):
1✔
646
        """A string representation of the contents of this node.
647

648
        All of the padding will be combined into a single string.
649

650
        Returns
651
        -------
652
        str
653
            a string sequence of the padding.
654
        """
655
        return "".join([val.format() for val in self.nodes])
1✔
656

657
    def is_space(self, i):
1✔
658
        """Determine if the value at i is a space or not.
659

660
        Notes
661
        -----
662
        the newline, ``\\n``, by itself is not considered a space.
663

664
        Parameters
665
        ----------
666
        i : int
667
            the index of the element to check.
668

669
        Returns
670
        -------
671
        unknown
672
            true iff the padding at that node is only spaces that are
673
            not ``\\n``.
674

675
        Raises
676
        ------
677
        IndexError
678
            if the index i is not in ``self.nodes``.
679
        """
680
        val = self.nodes[i]
1✔
681
        if not isinstance(val, str):
1✔
682
            return False
1✔
683
        return len(val.strip()) == 0 and val != "\n"
1✔
684

685
    def has_space(self):
1✔
686
        """Determines if there is syntactically significant space anywhere in this node.
687

688
        Returns
689
        -------
690
        bool
691
            True if there is syntactically significant (not in a
692
            comment) space.
693
        """
694
        return any([self.is_space(i) for i in range(len(self))])
1✔
695

696
    def append(self, val, is_comment=False):
1✔
697
        """Append the node to this node.
698

699
        Parameters
700
        ----------
701
        node : str, CommentNode
702
            node
703
        is_comment : bool
704
            whether or not the node is a comment.
705
        """
706
        if is_comment and not isinstance(val, CommentNode):
1✔
707
            val = CommentNode(val)
1✔
708
        if isinstance(val, CommentNode):
1✔
709
            self.nodes.append(val)
1✔
710
            return
1✔
711
        parts = val.split("\n")
1✔
712
        if len(parts) > 1:
1✔
713
            for part in parts[:-1]:
1✔
714
                if part:
1✔
715
                    self._nodes += [part, "\n"]
1✔
716
                else:
717
                    self._nodes.append("\n")
1✔
718
            if parts[-1]:
1✔
719
                self._nodes.append(parts[-1])
1✔
720
        else:
721
            self._nodes.append(val)
1✔
722

723
    def format(self):
1✔
724
        ret = ""
1✔
725
        for node in self.nodes:
1✔
726
            if isinstance(node, str):
1✔
727
                ret += node
1✔
728
            else:
729
                ret += node.format()
1✔
730
        return ret
1✔
731

732
    @property
1✔
733
    def comments(self):
1✔
734
        for node in self.nodes:
1✔
735
            if isinstance(node, CommentNode):
1✔
736
                yield node
1✔
737

738
    def _get_first_comment(self):
1✔
739
        """Get the first index that is a ``c`` style comment.
740

741
        Returns
742
        -------
743
        int, None
744
            the index of the first comment, if there is no comment then
745
            None.
746
        """
747
        for i, item in enumerate(self.nodes):
1✔
748
            if isinstance(item, CommentNode) and not item.is_dollar:
1✔
749
                return i
1✔
750
        return None
1✔
751

752
    def get_trailing_comment(self):
1✔
753
        i = self._get_first_comment()
1✔
754
        if i is not None:
1✔
755
            return self.nodes[i:]
1✔
756
        return None
1✔
757

758
    def _delete_trailing_comment(self):
1✔
759
        i = self._get_first_comment()
1✔
760
        if i is not None:
1✔
761
            del self._nodes[i:]
1✔
762

763
    def _grab_beginning_comment(self, extra_padding):
1✔
764
        """Consumes the provided comment, and moves it to the beginning of this node.
765

766
        Parameters
767
        ----------
768
        extra_padding : list
769
            the padding comment to add to the beginning of this padding.
770
        """
771
        if extra_padding[-1] != "\n":
1✔
772
            extra_padding.append("\n")
1✔
773
        self._nodes = extra_padding + self.nodes
1✔
774

775
    def __eq__(self, other):
1✔
776
        if not isinstance(other, (type(self), str)):
1✔
777
            return False
1✔
778
        if isinstance(other, type(self)):
1✔
779
            other = other.format()
1✔
780
        return self.format() == other
1✔
781

782
    def has_graveyard_comment(self):
1✔
783
        """Checks if there is a graveyard comment that is preventing information from being part of the tree.
784

785
        A graveyard comment is one that accidentally suppresses important information in the syntax tree.
786

787
        For example::
788

789
            imp:n=1 $ grave yard Vol=1
790

791
        Should be::
792

793
            imp:n=1 $ grave yard
794
            Vol=1
795

796
        .. versionadded:: 0.4.0
797

798
        Returns
799
        -------
800
        bool
801
            True if this PaddingNode contains a graveyard comment.
802
        """
803
        found = False
1✔
804
        for i, item in reversed(list(enumerate(self.nodes))):
1✔
805
            if isinstance(item, CommentNode):
1✔
806
                found = True
1✔
807
                break
1✔
808
        if not found:
1✔
809
            return False
1✔
810
        trail = self.nodes[i:]
1✔
811
        if len(trail) == 1:
1✔
812
            if trail[0].format().endswith("\n"):
1✔
813
                return False
1✔
814
            return True
1✔
815
        for node in trail[1:]:
1✔
816
            if node == "\n":
1✔
817
                return False
1✔
818
        return True
1✔
819

820

821
class CommentNode(SyntaxNodeBase):
1✔
822
    """Object to represent a comment in an MCNP problem.
823

824
    Parameters
825
    ----------
826
    input : Token
827
        the token from the lexer
828
    """
829

830
    _MATCHER = re.compile(
1✔
831
        rf"""(?P<delim>
832
                (\s{{0,{constants.BLANK_SPACE_CONTINUE-1}}}C\s?)
833
                |(\$\s?)
834
             )
835
            (?P<contents>.*)""",
836
        re.I | re.VERBOSE,
837
    )
838
    """A re matcher to confirm this is a C style comment."""
1✔
839

840
    def __init__(self, input):
1✔
841
        super().__init__("comment")
1✔
842
        is_dollar, node = self._convert_to_node(input)
1✔
843
        self._is_dollar = is_dollar
1✔
844
        self._nodes = [node]
1✔
845

846
    def _convert_to_node(self, token):
1✔
847
        """Converts the token to a Syntax Node to store.
848

849
        Parameters
850
        ----------
851
        token : str
852
            the token to convert.
853

854
        Returns
855
        -------
856
        SyntaxNode
857
            the SyntaxNode of the Comment.
858
        """
859
        if match := self._MATCHER.match(token):
1✔
860
            start = match["delim"]
1✔
861
            comment_line = match["contents"]
1✔
862
            is_dollar = "$" in start
1✔
863
        else:
864
            start = token
1✔
865
            comment_line = ""
1✔
866
            is_dollar = "$" in start
1✔
867
        return (
1✔
868
            is_dollar,
869
            SyntaxNode(
870
                "comment",
871
                {
872
                    "delimiter": ValueNode(start, str),
873
                    "data": ValueNode(comment_line, str),
874
                },
875
            ),
876
        )
877

878
    def append(self, token):
1✔
879
        """Append the comment token to this node.
880

881
        Parameters
882
        ----------
883
        token : str
884
            the comment token
885
        """
886
        is_dollar, node = self._convert_to_node(token)
1✔
887
        if is_dollar or self._is_dollar:
1✔
888
            raise TypeError(
1✔
889
                f"Cannot append multiple comments to a dollar comment. {token} given."
890
            )
891
        self._nodes.append(node)
1✔
892

893
    @property
1✔
894
    def is_dollar(self):
1✔
895
        """Whether or not this CommentNode is a dollar sign ($) comment.
896

897
        Returns
898
        -------
899
        bool
900
            True iff this is a dollar sign comment.
901
        """
902
        return self._is_dollar
1✔
903

904
    @property
1✔
905
    def contents(self):
1✔
906
        """The contents of the comments without delimiters (i.e., $/C).
907

908
        Returns
909
        -------
910
        str
911
            String of the contents
912
        """
913
        return "\n".join([node["data"].value for node in self.nodes])
1✔
914

915
    def format(self):
1✔
916
        ret = ""
1✔
917
        for node in self.nodes:
1✔
918
            ret += node.format()
1✔
919
        return ret
1✔
920

921
    @property
1✔
922
    def comments(self):
1✔
923
        yield from [self]
1✔
924

925
    def __str__(self):
1✔
926
        return self.format()
1✔
927

928
    def _pretty_str(self):
1✔
929
        return str(self)
×
930

931
    def __repr__(self):
1✔
932
        ret = f"COMMENT: "
1✔
933
        for node in self.nodes:
1✔
934
            ret += node.format()
1✔
935
        return ret
1✔
936

937
    def __eq__(self, other):
1✔
938
        return str(self) == str(other)
1✔
939

940

941
class ValueNode(SyntaxNodeBase):
1✔
942
    """A syntax node to represent the leaf node.
943

944
    This stores the original input token, the current value,
945
    and the possible associated padding.
946

947
    Parameters
948
    ----------
949
    token : str
950
        the original token for the ValueNode.
951
    token_type : class
952
        the type for the ValueNode.
953
    padding : PaddingNode
954
        the padding for this node.
955
    never_pad : bool
956
        If true an ending space will never be added to this.
957
    """
958

959
    _FORMATTERS = {
1✔
960
        float: {
961
            "value_length": 0,
962
            "precision": 5,
963
            "zero_padding": 0,
964
            "sign": "-",
965
            "divider": "e",
966
            "exponent_length": 0,
967
            "exponent_zero_pad": 0,
968
            "as_int": False,
969
            "int_tolerance": 1e-6,
970
            "is_scientific": True,
971
            "rel_eps": 1e-6,
972
            "abs_eps": 1e-9,
973
        },
974
        int: {"value_length": 0, "zero_padding": 0, "sign": "-"},
975
        str: {"value_length": 0},
976
    }
977
    """The default formatters for each type."""
1✔
978

979
    _SCIENTIFIC_FINDER = re.compile(
1✔
980
        r"""
981
            [+\-]?                      # leading sign if any
982
            (?P<significand>\d+\.*\d*)  # the actual number
983
            ((?P<e>[eE])                 # non-optional e with +/-
984
            [+\-]?|
985
            [+\-])                  #non-optional +/- if fortran float is used
986
            (?P<exponent>\d+)                    #exponent
987
        """,
988
        re.VERBOSE,
989
    )
990
    """A regex for finding scientific notation."""
1✔
991

992
    def __init__(self, token, token_type, padding=None, never_pad=False):
1✔
993
        super().__init__("")
1✔
994
        self._token = token
1✔
995
        self._type = token_type
1✔
996
        self._formatter = self._FORMATTERS[token_type].copy()
1✔
997
        self._is_neg_id = False
1✔
998
        self._is_neg_val = False
1✔
999
        self._og_value = None
1✔
1000
        self._never_pad = never_pad
1✔
1001
        if token is None:
1✔
1002
            self._value = None
1✔
1003
        elif isinstance(token, input_parser.mcnp_input.Jump):
1✔
1004
            self._value = None
1✔
1005
        elif token_type == float:
1✔
1006
            self._value = fortran_float(token)
1✔
1007
        elif token_type == int:
1✔
1008
            self._value = int(token)
1✔
1009
        else:
1010
            self._value = token
1✔
1011
        self._og_value = self.value
1✔
1012
        self._padding = padding
1✔
1013
        self._nodes = [self]
1✔
1014
        self._is_reversed = False
1✔
1015

1016
    def convert_to_int(self):
1✔
1017
        """Converts a float ValueNode to an int ValueNode."""
1018
        if self._type not in {float, int}:
1✔
1019
            raise ValueError(f"ValueNode must be a float to convert to int")
1✔
1020
        self._type = int
1✔
1021
        if self._token is not None and not isinstance(
1✔
1022
            self._token, input_parser.mcnp_input.Jump
1023
        ):
1024
            try:
1✔
1025
                self._value = int(self._token)
1✔
1026
            except ValueError as e:
1✔
1027
                parts = self._token.split(".")
1✔
1028
                if len(parts) > 1 and int(parts[1]) == 0:
1✔
1029
                    self._value = int(parts[0])
1✔
1030
                else:
1031
                    raise e
1✔
1032
        self._formatter = self._FORMATTERS[int].copy()
1✔
1033

1034
    def convert_to_enum(
1✔
1035
        self, enum_class, allow_none=False, format_type=str, switch_to_upper=False
1036
    ):
1037
        """Converts the ValueNode to an Enum for allowed values.
1038

1039
        Parameters
1040
        ----------
1041
        enum_class : Class
1042
            the class for the enum to use.
1043
        allow_none : bool
1044
            Whether or not to allow None as a value.
1045
        format_type : Class
1046
            the base data type to format this ValueNode as.
1047
        switch_to_upper : bool
1048
            Whether or not to convert a string to upper case before
1049
            convert to enum.
1050
        """
1051
        self._type = enum_class
1✔
1052
        if switch_to_upper and self._value is not None:
1✔
1053
            value = self._value.upper()
1✔
1054
        else:
1055
            value = self._value
1✔
1056
        if not (allow_none and self._value is None):
1✔
1057
            self._value = enum_class(value)
1✔
1058
        self._formatter = self._FORMATTERS[format_type].copy()
1✔
1059

1060
    def convert_to_str(self):
1✔
1061
        """Converts this ValueNode to being a string type.
1062

1063
        .. versionadded:: 1.0.0
1064

1065
        """
1066
        self._type = str
1✔
1067
        self._value = str(self._token)
1✔
1068
        self._og_value = self._token
1✔
1069
        self._formatter = self._FORMATTERS[str].copy()
1✔
1070

1071
    @property
1✔
1072
    def is_negatable_identifier(self):
1✔
1073
        """Whether or not this value is a negatable identifier.
1074

1075
        Example use: the surface transform or periodic surface is switched based on positive
1076
        or negative.
1077

1078
        This means:
1079
            1. the ValueNode is an int.
1080
            2. The ``value`` will always be positive.
1081
            3. The ``is_negative`` property will be available.
1082

1083
        Returns
1084
        -------
1085
        bool
1086
            the state of this marker.
1087
        """
1088
        return self._is_neg_id
1✔
1089

1090
    @is_negatable_identifier.setter
1✔
1091
    def is_negatable_identifier(self, val):
1✔
1092
        if val == True:
1✔
1093
            self.convert_to_int()
1✔
1094
            if self.value is not None:
1✔
1095
                self._is_neg = self.value < 0
1✔
1096
                self._value = abs(self._value)
1✔
1097
            else:
1098
                self._is_neg = None
1✔
1099
        self._is_neg_id = val
1✔
1100

1101
    @property
1✔
1102
    def is_negatable_float(self):
1✔
1103
        """Whether or not this value is a negatable float.
1104

1105
        Example use: cell density.
1106

1107
        This means:
1108
            1. the ValueNode is an int.
1109
            2. The ``value`` will always be positive.
1110
            3. The ``is_negative`` property will be available.
1111

1112
        Returns
1113
        -------
1114
        bool
1115
            the state of this marker.
1116
        """
1117
        return self._is_neg_val
1✔
1118

1119
    @is_negatable_float.setter
1✔
1120
    def is_negatable_float(self, val):
1✔
1121
        if val == True:
1✔
1122
            if self.value is not None:
1✔
1123
                self._is_neg = self.value < 0
1✔
1124
                self._value = abs(self._value)
1✔
1125
            else:
1126
                self._is_neg = None
1✔
1127
        self._is_neg_val = val
1✔
1128

1129
    @property
1✔
1130
    def is_negative(self):
1✔
1131
        """Whether or not this value is negative.
1132

1133
        If neither :func:`is_negatable_float` or :func:`is_negatable_identifier` is true
1134
        then this will return ``None``.
1135

1136
        Returns
1137
        -------
1138
        bool, None
1139
            true if this value is negative (either in input or through
1140
            state).
1141
        """
1142
        if self.is_negatable_identifier or self.is_negatable_float:
1✔
1143
            return self._is_neg
1✔
1144

1145
    @is_negative.setter
1✔
1146
    def is_negative(self, val):
1✔
1147
        if self.is_negatable_identifier or self.is_negatable_float:
1✔
1148
            self._is_neg = val
1✔
1149

1150
    def _reverse_engineer_formatting(self):
1✔
1151
        """Tries its best to figure out and update the formatter based on the token's format."""
1152
        if not self._is_reversed and self._token is not None:
1✔
1153
            self._is_reversed = True
1✔
1154
            token = self._token
1✔
1155
            if isinstance(token, input_parser.mcnp_input.Jump):
1✔
1156
                token = "J"
1✔
1157
            if isinstance(token, (Integral, Real)):
1✔
1158
                token = str(token)
1✔
1159
            self._formatter["value_length"] = len(token)
1✔
1160
            if self.padding:
1✔
1161
                if self.padding.is_space(0):
1✔
1162
                    self._formatter["value_length"] += len(self.padding.nodes[0])
1✔
1163

1164
            if self._type == float or self._type == int:
1✔
1165
                no_zero_pad = token.lstrip("0+-")
1✔
1166
                length = len(token)
1✔
1167
                delta = length - len(no_zero_pad)
1✔
1168
                if token.startswith("+") or token.startswith("-"):
1✔
1169
                    delta -= 1
1✔
1170
                    if token.startswith("+"):
1✔
1171
                        self._formatter["sign"] = "+"
1✔
1172
                    if token.startswith("-") and not self.never_pad:
1✔
1173
                        self._formatter["sign"] = " "
1✔
1174
                if delta > 0:
1✔
1175
                    self._formatter["zero_padding"] = length
1✔
1176
                if self._type == float:
1✔
1177
                    self._reverse_engineer_float()
1✔
1178

1179
    def _reverse_engineer_float(self):
1✔
1180
        token = self._token
1✔
1181
        if isinstance(token, Real):
1✔
1182
            token = str(token)
1✔
1183
        if isinstance(token, input_parser.mcnp_input.Jump):
1✔
1184
            token = "J"
1✔
1185
        if match := self._SCIENTIFIC_FINDER.match(token):
1✔
1186
            groups = match.groupdict(default="")
1✔
1187
            self._formatter["is_scientific"] = True
1✔
1188
            significand = groups["significand"]
1✔
1189
            self._formatter["divider"] = groups["e"]
1✔
1190
            # extra space for the "e" in scientific and... stuff
1191
            self._formatter["zero_padding"] += 4
1✔
1192
            exponent = groups["exponent"]
1✔
1193
            temp_exp = exponent.lstrip("0")
1✔
1194
            if exponent != temp_exp:
1✔
1195
                self._formatter["exponent_length"] = len(exponent)
1✔
1196
                self._formatter["exponent_zero_pad"] = len(exponent)
1✔
1197
        else:
1198
            self._formatter["is_scientific"] = False
1✔
1199
            significand = token
1✔
1200
        parts = significand.split(".")
1✔
1201
        if len(parts) == 2:
1✔
1202
            precision = len(parts[1])
1✔
1203
        else:
1204
            precision = self._FORMATTERS[float]["precision"]
1✔
1205
            self._formatter["as_int"] = True
1✔
1206

1207
        self._formatter["precision"] = precision
1✔
1208

1209
    def _can_float_to_int_happen(self):
1✔
1210
        """Checks if you can format a floating point as an int.
1211

1212
        E.g., 1.0 -> 1
1213

1214
        Considers if this was done in the input, and if the value is close to the int value.
1215

1216
        Returns
1217
        -------
1218
        bool.
1219
        """
1220
        if self._type != float or not self._formatter["as_int"]:
1✔
1221
            return False
1✔
1222
        nearest_int = round(self.value)
1✔
1223
        if not math.isclose(nearest_int, self.value, rel_tol=rel_tol, abs_tol=abs_tol):
1✔
1224
            return False
1✔
1225
        return True
1✔
1226

1227
    @property
1✔
1228
    def _print_value(self):
1✔
1229
        """The print version of the value.
1230

1231
        This takes a float/int that is negatable, and negates it
1232
        based on the ``is_negative`` value.
1233

1234
        Returns
1235
        -------
1236
        int, float
1237
        """
1238
        if self._type in {int, float} and self.is_negative:
1✔
1239
            return -self.value
1✔
1240
        return self.value
1✔
1241

1242
    @property
1✔
1243
    def _value_changed(self):
1✔
1244
        """Checks if the value has changed at all from first parsing.
1245

1246
        Used to shortcut formatting and reverse engineering.
1247

1248
        Returns
1249
        -------
1250
        bool
1251
        """
1252
        if self.value is None and self._og_value is None:
1✔
1253
            return False
1✔
1254
        if self.value is None or self._og_value is None:
1✔
1255
            return True
1✔
1256
        if self._type in {float, int}:
1✔
1257
            return not math.isclose(
1✔
1258
                self._print_value, self._og_value, rel_tol=rel_tol, abs_tol=abs_tol
1259
            )
1260
        return self.value != self._og_value
1✔
1261

1262
    def _avoid_rounding_truncation(self):
1✔
1263
        """
1264
        Detects when not enough digits are in original input to preserve precision.
1265

1266
        This will update the precision in the formatter to the necessary
1267
        value to preserve the precision.
1268
        """
1269
        precision = self._formatter["precision"]
1✔
1270
        if self._formatter["is_scientific"]:
1✔
1271
            # Remember that you can test for equality to 0 with floats safely
1272
            if self.value != 0:
1✔
1273
                exp = math.floor(math.log10(abs(self.value)))
1✔
1274
            else:
1275
                exp = 0
×
1276
            val = self.value / 10**exp
1✔
1277
        else:
1278
            val = self.value
1✔
1279
        while not math.isclose(
1✔
1280
            val,
1281
            round(val, precision),
1282
            rel_tol=self._formatter["rel_eps"],
1283
            abs_tol=self._formatter["abs_eps"],
1284
        ):
1285
            precision += 1
1✔
1286
        self._formatter["precision"] = precision
1✔
1287

1288
    def format(self):
1✔
1289
        if not self._value_changed:
1✔
1290
            return f"{self._token}{self.padding.format() if self.padding else ''}"
1✔
1291
        if self.value is None:
1✔
1292
            return ""
1✔
1293
        self._reverse_engineer_formatting()
1✔
1294
        if issubclass(self.type, enum.Enum):
1✔
1295
            value = self.value.value
1✔
1296
        else:
1297
            value = self._print_value
1✔
1298
        if self._type == int or self._can_float_to_int_happen():
1✔
1299
            temp = "{value:0={sign}{zero_padding}d}".format(
1✔
1300
                value=int(value), **self._formatter
1301
            )
1302
        elif self._type == float:
1✔
1303
            # default to python general if new value
1304
            self._avoid_rounding_truncation()
1✔
1305
            if not self._is_reversed:
1✔
1306
                temp = "{value:0={sign}{zero_padding}.{precision}g}".format(
1✔
1307
                    value=value, **self._formatter
1308
                )
1309
            elif self._formatter["is_scientific"]:
1✔
1310
                temp = "{value:0={sign}{zero_padding}.{precision}e}".format(
1✔
1311
                    value=value, **self._formatter
1312
                )
1313
                temp = temp.replace("e", self._formatter["divider"])
1✔
1314
                temp_match = self._SCIENTIFIC_FINDER.match(temp)
1✔
1315
                exponent = temp_match.group("exponent")
1✔
1316
                start, end = temp_match.span("exponent")
1✔
1317
                new_exp_temp = "{value:0={zero_padding}d}".format(
1✔
1318
                    value=int(exponent),
1319
                    zero_padding=self._formatter["exponent_zero_pad"],
1320
                )
1321
                new_exp = "{temp:<{value_length}}".format(
1✔
1322
                    temp=new_exp_temp, value_length=self._formatter["exponent_length"]
1323
                )
1324
                temp = temp[0:start] + new_exp + temp[end:]
1✔
1325
            elif self._formatter["as_int"]:
1✔
1326
                temp = "{value:0={sign}0{zero_padding}g}".format(
1✔
1327
                    value=value, **self._formatter
1328
                )
1329
            else:
1330
                temp = "{value:0={sign}0{zero_padding}.{precision}f}".format(
1✔
1331
                    value=value, **self._formatter
1332
                )
1333
        else:
1334
            temp = str(value)
1✔
1335
        end_line_padding = False
1✔
1336
        if self.padding:
1✔
1337
            for node in self.padding.nodes:
1✔
1338
                if node == "\n":
1✔
1339
                    end_line_padding = True
1✔
1340
                    break
1✔
1341
                if isinstance(node, CommentNode):
1✔
1342
                    break
1✔
1343
            if self.padding.is_space(0):
1✔
1344
                # if there was and end space, and we ran out of space, and there isn't
1345
                # a saving space later on
1346
                if len(temp) >= self._formatter["value_length"] and not (
1✔
1347
                    len(self.padding) > 1
1348
                    and (self.padding.is_space(1) or self.padding.nodes[1] == "\n")
1349
                ):
1350
                    pad_str = " "
1✔
1351
                else:
1352
                    pad_str = ""
1✔
1353
                extra_pad_str = "".join([x.format() for x in self.padding.nodes[1:]])
1✔
1354
            else:
1355
                pad_str = ""
1✔
1356
                extra_pad_str = "".join([x.format() for x in self.padding.nodes])
1✔
1357
        else:
1358
            pad_str = ""
1✔
1359
            extra_pad_str = ""
1✔
1360
        if not self.never_pad:
1✔
1361
            buffer = "{temp:<{value_length}}{padding}".format(
1✔
1362
                temp=temp, padding=pad_str, **self._formatter
1363
            )
1364
        else:
1365
            buffer = "{temp}{padding}".format(
1✔
1366
                temp=temp, padding=pad_str, **self._formatter
1367
            )
1368
        """
1✔
1369
        If:
1370
            1. expanded
1371
            2. had an original value
1372
            3. and value doesn't end in a new line (without a comment)
1373
        """
1374
        if (
1✔
1375
            len(buffer) > self._formatter["value_length"]
1376
            and self._token is not None
1377
            and not end_line_padding
1378
        ):
1379
            warning = LineExpansionWarning("")
1✔
1380
            warning.cause = "value"
1✔
1381
            warning.og_value = self._token
1✔
1382
            warning.new_value = temp
1✔
1383
            warnings.warn(
1✔
1384
                warning,
1385
                category=LineExpansionWarning,
1386
            )
1387
        return buffer + extra_pad_str
1✔
1388

1389
    @property
1✔
1390
    def comments(self):
1✔
1391
        if self.padding is not None:
1✔
1392
            yield from self.padding.comments
1✔
1393
        else:
1394
            yield from []
1✔
1395

1396
    def get_trailing_comment(self):
1✔
1397
        if self.padding is None:
1✔
1398
            return
1✔
1399
        return self.padding.get_trailing_comment()
1✔
1400

1401
    def _delete_trailing_comment(self):
1✔
1402
        if self.padding is None:
1✔
1403
            return
1✔
1404
        self.padding._delete_trailing_comment()
1✔
1405

1406
    @property
1✔
1407
    def padding(self):
1✔
1408
        """The padding if any for this ValueNode.
1409

1410
        Returns
1411
        -------
1412
        PaddingNode
1413
            the padding if any.
1414
        """
1415
        return self._padding
1✔
1416

1417
    @padding.setter
1✔
1418
    def padding(self, pad):
1✔
1419
        self._padding = pad
1✔
1420

1421
    @property
1✔
1422
    def type(self):
1✔
1423
        """The data type for this ValueNode.
1424

1425
        Examples: float, int, str, Lattice
1426

1427
        Returns
1428
        -------
1429
        Class
1430
            the class for the value of this node.
1431
        """
1432
        return self._type
1✔
1433

1434
    @property
1✔
1435
    def token(self):
1✔
1436
        """The original text (token) for this ValueNode.
1437

1438
        Returns
1439
        -------
1440
        str
1441
            the original input.
1442
        """
1443
        return self._token
1✔
1444

1445
    def __str__(self):
1✔
1446
        return f"<Value, {self._value}, padding: {self._padding}>"
1✔
1447

1448
    def _pretty_str(self):
1✔
1449
        return str(self)
1✔
1450

1451
    def __repr__(self):
1✔
1452
        return str(self)
1✔
1453

1454
    @property
1✔
1455
    def value(self):
1✔
1456
        """The current semantic value of this ValueNode.
1457

1458
        This is the parsed meaning in the type of ``self.type``,
1459
        that can be updated. When this value is updated, next time format()
1460
        is ran this value will be used.
1461

1462
        Returns
1463
        -------
1464
        float, int, str, enum
1465
            the node's value in type ``type``.
1466
        """
1467
        return self._value
1✔
1468

1469
    @property
1✔
1470
    def never_pad(self):
1✔
1471
        """Whether or not this value node will not have extra spaces added.
1472

1473
        Returns
1474
        -------
1475
        bool
1476
            true if extra padding is not adding at the end if missing.
1477
        """
1478
        return self._never_pad
1✔
1479

1480
    @never_pad.setter
1✔
1481
    def never_pad(self, never_pad):
1✔
1482
        self._never_pad = never_pad
1✔
1483

1484
    @value.setter
1✔
1485
    def value(self, value):
1✔
1486
        if self.is_negative is not None and value is not None:
1✔
1487
            value = abs(value)
1✔
1488
        self._check_if_needs_end_padding(value)
1✔
1489
        self._value = value
1✔
1490

1491
    def _check_if_needs_end_padding(self, value):
1✔
1492
        if value is None or self.value is not None or self._never_pad:
1✔
1493
            return
1✔
1494
        # if not followed by a trailing space
1495
        if self.padding is None:
1✔
1496
            self.padding = PaddingNode(" ")
1✔
1497

1498
    def __eq__(self, other):
1✔
1499
        if not isinstance(other, (type(self), str, Real)):
1✔
1500
            return False
1✔
1501
        if isinstance(other, ValueNode):
1✔
1502
            other_val = other.value
1✔
1503
            if self.type != other.type:
1✔
1504
                return False
1✔
1505
        else:
1506
            other_val = other
1✔
1507
            if self.type != type(other):
1✔
1508
                return False
1✔
1509

1510
        if self.type == float and self.value is not None and other_val is not None:
1✔
1511
            return math.isclose(self.value, other_val, rel_tol=rel_tol, abs_tol=abs_tol)
1✔
1512
        return self.value == other_val
1✔
1513

1514

1515
class ParticleNode(SyntaxNodeBase):
1✔
1516
    """A node to hold particles information in a :class:`ClassifierNode`.
1517

1518
    Parameters
1519
    ----------
1520
    name : str
1521
        the name for the node.
1522
    token : str
1523
        the original token from parsing
1524
    """
1525

1526
    _letter_finder = re.compile(r"([a-zA-Z])")
1✔
1527

1528
    def __init__(self, name, token):
1✔
1529
        super().__init__(name)
1✔
1530
        self._nodes = [self]
1✔
1531
        self._token = token
1✔
1532
        self._order = []
1✔
1533
        classifier_chunks = token.replace(":", "").split(",")
1✔
1534
        self._particles = set()
1✔
1535
        self._formatter = {"upper": False}
1✔
1536
        for chunk in classifier_chunks:
1✔
1537
            part = Particle(chunk.upper())
1✔
1538
            self._particles.add(part)
1✔
1539
            self._order.append(part)
1✔
1540

1541
    @property
1✔
1542
    def token(self):
1✔
1543
        """The original text (token) for this ParticleNode.
1544

1545
        Returns
1546
        -------
1547
        str
1548
            the original input.
1549
        """
1550
        return self._token
1✔
1551

1552
    @property
1✔
1553
    def particles(self):
1✔
1554
        """The particles included in this node.
1555

1556
        Returns
1557
        -------
1558
        set
1559
            a set of the particles being used.
1560
        """
1561
        return self._particles
1✔
1562

1563
    @particles.setter
1✔
1564
    def particles(self, values):
1✔
1565
        if not isinstance(values, (list, set)):
1✔
1566
            raise TypeError(f"Particles must be a set. {values} given.")
1✔
1567
        for value in values:
1✔
1568
            if not isinstance(value, Particle):
1✔
1569
                raise TypeError(f"All particles must be a Particle. {value} given")
1✔
1570
        if isinstance(values, list):
1✔
1571
            self._order = values
1✔
1572
            values = set(values)
1✔
1573
        self._particles = values
1✔
1574

1575
    def add(self, value):
1✔
1576
        """Add a particle to this node.
1577

1578
        Parameters
1579
        ----------
1580
        value : Particle
1581
            the particle to add.
1582
        """
1583
        if not isinstance(value, Particle):
1✔
1584
            raise TypeError(f"All particles must be a Particle. {value} given")
1✔
1585
        self._order.append(value)
1✔
1586
        self._particles.add(value)
1✔
1587

1588
    def remove(self, value):
1✔
1589
        """Remove a particle from this node.
1590

1591
        Parameters
1592
        ----------
1593
        value : Particle
1594
            the particle to remove.
1595
        """
1596
        if not isinstance(value, Particle):
1✔
1597
            raise TypeError(f"All particles must be a Particle. {value} given")
1✔
1598
        self._particles.remove(value)
1✔
1599
        self._order.remove(value)
1✔
1600

1601
    @property
1✔
1602
    def _particles_sorted(self):
1✔
1603
        """The particles in this node ordered in a nice-ish way.
1604

1605
        Ordering:
1606
            1. User input.
1607
            2. Order of particles appended
1608
            3. randomly at the end if all else fails.
1609

1610
        Returns
1611
        -------
1612
        list
1613
        """
1614
        ret = self._order
1✔
1615
        ret_set = set(ret)
1✔
1616
        remainder = self.particles - ret_set
1✔
1617
        extras = ret_set - self.particles
1✔
1618
        for straggler in sorted(remainder):
1✔
1619
            ret.append(straggler)
1✔
1620
        for useless in extras:
1✔
1621
            ret.remove(useless)
1✔
1622
        return ret
1✔
1623

1624
    def format(self):
1✔
1625
        self._reverse_engineer_format()
1✔
1626
        if self._formatter["upper"]:
1✔
1627
            parts = [p.value.upper() for p in self._particles_sorted]
1✔
1628
        else:
1629
            parts = [p.value.lower() for p in self._particles_sorted]
1✔
1630
        return f":{','.join(parts)}"
1✔
1631

1632
    def _reverse_engineer_format(self):
1✔
1633
        total_match = 0
1✔
1634
        upper_match = 0
1✔
1635
        for match in self._letter_finder.finditer(self._token):
1✔
1636
            if match:
1✔
1637
                if match.group(0).isupper():
1✔
1638
                    upper_match += 1
1✔
1639
                total_match += 1
1✔
1640
        if total_match and upper_match / total_match >= 0.5:
1✔
1641
            self._formatter["upper"] = True
1✔
1642

1643
    @property
1✔
1644
    def comments(self):
1✔
1645
        yield from []
1✔
1646

1647
    def __repr__(self):
1✔
1648
        return self.format()
1✔
1649

1650
    def __iter__(self):
1✔
1651
        return iter(self.particles)
1✔
1652

1653

1654
class ListNode(SyntaxNodeBase):
1✔
1655
    """A node to represent a list of values.
1656

1657
    Parameters
1658
    ----------
1659
    name : str
1660
        the name of this node.
1661
    """
1662

1663
    def __init__(self, name):
1✔
1664
        super().__init__(name)
1✔
1665
        self._shortcuts = []
1✔
1666

1667
    def __repr__(self):
1✔
1668
        return f"(list: {self.name}, {self.nodes})"
1✔
1669

1670
    def update_with_new_values(self, new_vals):
1✔
1671
        """Update this list node with new values.
1672

1673
        This will first try to find if any shortcuts in the original input match up with
1674
        the new values. If so it will then "zip" out those shortcuts to consume
1675
        as many neighbor nodes as possible.
1676
        Finally, the internal shortcuts, and list will be updated to reflect the new state.
1677

1678
        Parameters
1679
        ----------
1680
        new_vals : list
1681
            the new values (a list of ValueNodes)
1682
        """
1683
        if not new_vals:
1✔
1684
            self._nodes = []
1✔
1685
            return
1✔
1686
        new_vals_cache = {id(v): v for v in new_vals}
1✔
1687
        # bind shortcuts to single site in new values
1688
        for shortcut in self._shortcuts:
1✔
1689
            for node in shortcut.nodes:
1✔
1690
                if id(node) in new_vals_cache:
1✔
1691
                    new_vals_cache[id(node)] = shortcut
1✔
1692
                    shortcut.nodes.clear()
1✔
1693
                    break
1✔
1694
        self._expand_shortcuts(new_vals, new_vals_cache)
1✔
1695
        self._shortcuts = []
1✔
1696
        self._nodes = []
1✔
1697
        for key, node in new_vals_cache.items():
1✔
1698
            if isinstance(node, ShortcutNode):
1✔
1699
                if (
1✔
1700
                    len(self._shortcuts) > 0 and node is not self._shortcuts[-1]
1701
                ) or len(self._shortcuts) == 0:
1702
                    self._shortcuts.append(node)
1✔
1703
                    self._nodes.append(node)
1✔
1704
            else:
1705
                self._nodes.append(node)
1✔
1706
        end = self._nodes[-1]
1✔
1707
        # pop off final shortcut if it's a jump the user left off
1708
        if (
1✔
1709
            isinstance(end, ShortcutNode)
1710
            and end._type == Shortcuts.JUMP
1711
            and len(end._original) == 0
1712
        ):
1713
            self._nodes.pop()
1✔
1714
            self._shortcuts.pop()
1✔
1715

1716
    def _expand_shortcuts(self, new_vals, new_vals_cache):
1✔
1717
        """Expands the existing shortcuts, and tries to "zip out" and consume their neighbors.
1718

1719
        Parameters
1720
        ----------
1721
        new_vals : list
1722
            the new values.
1723
        new_vals_cache : dict
1724
            a dictionary mapping the id of the ValueNode to the
1725
            ValueNode or ShortcutNode. This is ordered the same as
1726
            ``new_vals``.
1727
        """
1728

1729
        def try_expansion(shortcut, value):
1✔
1730
            status = shortcut.consume_edge_node(
1✔
1731
                value, 1, i == last_end + 1 and last_end != 0
1732
            )
1733
            if status:
1✔
1734
                new_vals_cache[id(value)] = shortcut
1✔
1735
            else:
1736
                new_vals_cache[id(value)] = value
1✔
1737
            return status
1✔
1738

1739
        def try_reverse_expansion(shortcut, i, last_end):
1✔
1740
            if i > 1:
1✔
1741
                for value in new_vals[i - 1 : last_end : -1]:
1✔
1742
                    if shortcut.consume_edge_node(value, -1):
1✔
1743
                        new_vals_cache[id(value)] = shortcut
1✔
1744
                    else:
1745
                        new_vals_cache[id(value)] = value
1✔
1746
                        return
1✔
1747

1748
        def check_for_orphan_jump(value):
1✔
1749
            """Checks if the current Jump is not tied to an existing Shortcut"""
1750
            nonlocal shortcut
1751
            if value.value is None and shortcut is None:
1✔
1752
                shortcut = ShortcutNode(p=None, short_type=Shortcuts.JUMP)
1✔
1753
                if shortcut.consume_edge_node(value, 1):
1✔
1754
                    new_vals_cache[id(value)] = shortcut
1✔
1755

1756
        shortcut = None
1✔
1757
        last_end = 0
1✔
1758
        for i, value in enumerate(new_vals_cache.values()):
1✔
1759
            # found a new shortcut
1760
            if isinstance(value, ShortcutNode):
1✔
1761
                # shortcuts bumped up against each other
1762
                if shortcut is not None:
1✔
1763
                    last_end = i - 1
1✔
1764
                shortcut = value
1✔
1765
                if try_expansion(shortcut, new_vals[i]):
1✔
1766
                    try_reverse_expansion(shortcut, i, last_end)
1✔
1767
                else:
1768
                    shortcut = None
×
1769
            # otherwise it is actually a value to expand as well
1770
            else:
1771
                if shortcut is not None:
1✔
1772
                    if not try_expansion(shortcut, new_vals[i]):
1✔
1773
                        last_end = i - 1
1✔
1774
                        shortcut = None
1✔
1775
                        check_for_orphan_jump(new_vals[i])
1✔
1776
                else:
1777
                    check_for_orphan_jump(new_vals[i])
1✔
1778

1779
    def append(self, val, from_parsing=False):
1✔
1780
        """Append the node to this node.
1781

1782
        Parameters
1783
        ----------
1784
        node : ValueNode, ShortcutNode
1785
            node
1786
        from_parsing : bool
1787
            If this is being append from the parsers, and not elsewhere.
1788
        """
1789
        if isinstance(val, ShortcutNode):
1✔
1790
            self._shortcuts.append(val)
1✔
1791
        if len(self) > 0 and from_parsing:
1✔
1792
            last = self[-1]
1✔
1793
            if isinstance(last, ValueNode) and (
1✔
1794
                (last.padding and not last.padding.has_space) or last.padding is None
1795
            ):
1796
                self[-1].never_pad = True
×
1797
        super().append(val)
1✔
1798

1799
    @property
1✔
1800
    def comments(self):
1✔
1801
        for node in self.nodes:
1✔
1802
            yield from node.comments
1✔
1803

1804
    def format(self):
1✔
1805
        ret = ""
1✔
1806
        length = len(self.nodes)
1✔
1807
        last_node = None
1✔
1808
        for i, node in enumerate(self.nodes):
1✔
1809
            # adds extra padding
1810
            if (
1✔
1811
                isinstance(node, ValueNode)
1812
                and node.padding is None
1813
                and i < length - 1
1814
                and not isinstance(self.nodes[i + 1], PaddingNode)
1815
                and not node.never_pad
1816
            ):
1817
                node.padding = PaddingNode(" ")
1✔
1818
            if isinstance(last_node, ShortcutNode) and isinstance(node, ShortcutNode):
1✔
1819
                ret += node.format(last_node)
1✔
1820
            else:
1821
                ret += node.format()
1✔
1822
            last_node = node
1✔
1823
        return ret
1✔
1824

1825
    def __iter__(self):
1✔
1826
        for node in self.nodes:
1✔
1827
            if isinstance(node, ShortcutNode):
1✔
1828
                yield from node.nodes
1✔
1829
            else:
1830
                yield node
1✔
1831

1832
    def __contains__(self, value):
1✔
1833
        for node in self:
1✔
1834
            if node == value:
1✔
1835
                return True
1✔
1836
        return False
1✔
1837

1838
    def __getitem__(self, indx):
1✔
1839
        if isinstance(indx, slice):
1✔
1840
            return self.__get_slice(indx)
1✔
1841
        if indx >= 0:
1✔
1842
            for i, item in enumerate(self):
1✔
1843
                if i == indx:
1✔
1844
                    return item
1✔
1845
        else:
1846
            items = list(self)
1✔
1847
            return items[indx]
1✔
1848
        raise IndexError(f"{indx} not in ListNode")
1✔
1849

1850
    def __get_slice(self, i: slice):
1✔
1851
        """Helper function for __getitem__ with slices."""
1852
        rstep = i.step if i.step is not None else 1
1✔
1853
        rstart = i.start
1✔
1854
        rstop = i.stop
1✔
1855
        if rstep < 0:  # Backwards
1✔
1856
            if rstart is None:
1✔
1857
                rstart = len(self) - 1
1✔
1858
            if rstop is None:
1✔
1859
                rstop = 0
1✔
1860
        else:  # Forwards
1861
            if rstart is None:
1✔
1862
                rstart = 0
1✔
1863
            if rstop is None:
1✔
1864
                rstop = len(self.nodes) - 1
1✔
1865
            if rstop < 0:
1✔
1866
                rstop += len(self)
1✔
1867
        buffer = []
1✔
1868
        allowed_indices = range(rstart, rstop, rstep)
1✔
1869
        for i, item in enumerate(self):
1✔
1870
            if i in allowed_indices:
1✔
1871
                buffer.append(item)
1✔
1872
        ret = ListNode(f"{self.name}_slice")
1✔
1873
        if rstep < 0:
1✔
1874
            buffer.reverse()
1✔
1875
        for val in buffer:
1✔
1876
            ret.append(val)
1✔
1877
        return ret
1✔
1878

1879
    def remove(self, obj):
1✔
1880
        """Removes the given object from this list.
1881

1882
        Parameters
1883
        ----------
1884
        obj : ValueNode
1885
            the object to remove.
1886
        """
1887
        self.nodes.remove(obj)
1✔
1888

1889
    def __eq__(self, other):
1✔
1890
        if not isinstance(other, (type(self), list)):
1✔
1891
            return False
1✔
1892
        if len(self) != len(other):
1✔
1893
            return False
1✔
1894
        for lhs, rhs in zip(self, other):
1✔
1895
            if lhs != rhs:
1✔
1896
                return False
1✔
1897
        return True
1✔
1898

1899

1900
class MaterialsNode(SyntaxNodeBase):
1✔
1901
    """A node for representing isotopes and their concentration,
1902
    and the material parameters.
1903

1904
    This stores a list of tuples of ZAIDs and concentrations,
1905
    or a tuple of a parameter.
1906

1907
    .. versionadded:: 1.0.0
1908

1909
        This was added as a more general version of ``IsotopesNodes``.
1910

1911
    Parameters
1912
    ----------
1913
    name : str
1914
        a name for labeling this node.
1915
    """
1916

1917
    def __init__(self, name):
1✔
1918
        super().__init__(name)
1✔
1919

1920
    def append_nuclide(self, isotope_fraction):
1✔
1921
        """Append the isotope fraction to this node.
1922

1923
        .. versionadded:: 1.0.0
1924

1925
            Added to replace ``append``
1926

1927
        Parameters
1928
        ----------
1929
        isotope_fraction : tuple
1930
            the isotope_fraction to add. This must be a tuple from A
1931
            Yacc production. This will consist of: the string
1932
            identifying the Yacc production, a ValueNode that is the
1933
            ZAID, and a ValueNode of the concentration.
1934
        """
1935
        isotope, concentration = isotope_fraction[1:3]
1✔
1936
        self._nodes.append((isotope, concentration))
1✔
1937

1938
    def append(self):  # pragma: no cover
1939
        raise DeprecationWarning("Deprecated. Use append_param or append_nuclide")
1940

1941
    def append_param(self, param):
1✔
1942
        """Append the parameter to this node.
1943

1944
        .. versionadded:: 1.0.0
1945

1946
            Added to replace ``append``
1947

1948
        Parameters
1949
        ----------
1950
        param : ParametersNode
1951
            the parameter to add to this node.
1952
        """
1953
        self._nodes.append((param,))
1✔
1954

1955
    def format(self):
1✔
1956
        ret = ""
1✔
1957
        for node in it.chain(*self.nodes):
1✔
1958
            ret += node.format()
1✔
1959
        return ret
1✔
1960

1961
    def __repr__(self):
1✔
1962
        return f"(Materials: {self.nodes})"
1✔
1963

1964
    def _pretty_str(self):
1✔
1965
        INDENT = 2
1✔
1966
        ret = f"<Node: {self.name}: [\n"
1✔
1967
        for val in self.nodes:
1✔
1968
            child_strs = [f"({', '.join([str(v) for v in val])})"]
1✔
1969
            ret += "\n".join([" " * 2 * INDENT + s for s in child_strs[:-1]])
1✔
1970
            ret += " " * 2 * INDENT + child_strs[-1] + ",\n"
1✔
1971
        ret += " " * INDENT + "]\n"
1✔
1972
        ret += ">"
1✔
1973
        return ret
1✔
1974

1975
    def __iter__(self):
1✔
1976
        return iter(self.nodes)
1✔
1977

1978
    @property
1✔
1979
    def comments(self):
1✔
1980
        for node in self.nodes:
1✔
1981
            for value in node:
1✔
1982
                yield from value.comments
1✔
1983

1984
    def get_trailing_comment(self):
1✔
1985
        tail = self.nodes[-1]
1✔
1986
        tail = tail[-1]
1✔
1987
        return tail.get_trailing_comment()
1✔
1988

1989
    def _delete_trailing_comment(self):
1✔
1990
        tail = self.nodes[-1]
1✔
1991
        tail = tail[-1]
1✔
1992
        tail._delete_trailing_comment()
1✔
1993

1994
    def flatten(self):
1✔
1995
        ret = []
1✔
1996
        for node_group in self.nodes:
1✔
1997
            ret += node_group
1✔
1998
        return ret
1✔
1999

2000

2001
class ShortcutNode(ListNode):
1✔
2002
    """A node that pretends to be a :class:`ListNode` but is actually representing a shortcut.
2003

2004
    This takes the shortcut tokens, and expands it into their "virtual" values.
2005

2006
    Parameters
2007
    ----------
2008
    p : sly.yacc.YaccProduction
2009
        the parsing object to parse.
2010
    short_type : Shortcuts
2011
        the type of the shortcut.
2012
    """
2013

2014
    _shortcut_names = {
1✔
2015
        ("REPEAT", "NUM_REPEAT"): Shortcuts.REPEAT,
2016
        ("JUMP", "NUM_JUMP"): Shortcuts.JUMP,
2017
        ("INTERPOLATE", "NUM_INTERPOLATE"): Shortcuts.INTERPOLATE,
2018
        ("LOG_INTERPOLATE", "NUM_LOG_INTERPOLATE"): Shortcuts.LOG_INTERPOLATE,
2019
        ("MULTIPLY", "NUM_MULTIPLY"): Shortcuts.MULTIPLY,
2020
    }
2021
    _num_finder = re.compile(r"\d+")
1✔
2022

2023
    def __init__(self, p=None, short_type=None, data_type=float):
1✔
2024
        self._type = None
1✔
2025
        self._end_pad = None
1✔
2026
        self._nodes = collections.deque()
1✔
2027
        self._original = []
1✔
2028
        self._full = False
1✔
2029
        self._num_node = ValueNode(None, float, never_pad=True)
1✔
2030
        self._data_type = data_type
1✔
2031
        if p is not None:
1✔
2032
            for search_strs, shortcut in self._shortcut_names.items():
1✔
2033
                for search_str in search_strs:
1✔
2034
                    if hasattr(p, search_str):
1✔
2035
                        super().__init__(search_str.lower())
1✔
2036
                        self._type = shortcut
1✔
2037
            if self._type is None:
1✔
2038
                raise ValueError("must use a valid shortcut")
1✔
2039
            self._original = list(p)
1✔
2040
            if self._type == Shortcuts.REPEAT:
1✔
2041
                self._expand_repeat(p)
1✔
2042
            elif self._type == Shortcuts.MULTIPLY:
1✔
2043
                self._expand_multiply(p)
1✔
2044
            elif self._type == Shortcuts.JUMP:
1✔
2045
                self._expand_jump(p)
1✔
2046
            elif self._type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
1✔
2047
                self._expand_interpolate(p)
1✔
2048
        elif short_type is not None:
1✔
2049
            if not isinstance(short_type, Shortcuts):
1✔
2050
                raise TypeError(f"Shortcut type must be Shortcuts. {short_type} given.")
1✔
2051
            self._type = short_type
1✔
2052
            if self._type in {
1✔
2053
                Shortcuts.INTERPOLATE,
2054
                Shortcuts.LOG_INTERPOLATE,
2055
                Shortcuts.JUMP,
2056
                Shortcuts.JUMP,
2057
            }:
2058
                self._num_node = ValueNode(None, int, never_pad=True)
1✔
2059
            self._end_pad = PaddingNode(" ")
1✔
2060

2061
    def load_nodes(self, nodes):
1✔
2062
        """Loads the given nodes into this shortcut, and update needed information.
2063

2064
        For interpolate nodes should start and end with the beginning/end of
2065
        the interpolation.
2066

2067
        Parameters
2068
        ----------
2069
        nodes : list
2070
            the nodes to be loaded.
2071
        """
2072
        self._nodes = collections.deque(nodes)
1✔
2073
        if self.type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
1✔
2074
            self._begin = nodes[0].value
1✔
2075
            self._end = nodes[-1].value
1✔
2076
            if self.type == Shortcuts.LOG_INTERPOLATE:
1✔
2077
                self._begin = math.log10(self._begin)
1✔
2078
                self._end = math.log10(self._end)
1✔
2079
            self._spacing = (self._end - self._begin) / (len(nodes) - 1)
1✔
2080

2081
    @property
1✔
2082
    def end_padding(self):
1✔
2083
        """The padding at the end of this shortcut.
2084

2085
        Returns
2086
        -------
2087
        PaddingNode
2088
        """
2089
        return self._end_pad
1✔
2090

2091
    @end_padding.setter
1✔
2092
    def end_padding(self, padding):
1✔
2093
        if not isinstance(padding, PaddingNode):
1✔
2094
            raise TypeError(
1✔
2095
                f"End padding must be of type PaddingNode. {padding} given."
2096
            )
2097
        self._end_pad = padding
1✔
2098

2099
    @property
1✔
2100
    def type(self):
1✔
2101
        """The Type of shortcut this ShortcutNode represents.
2102

2103
        Returns
2104
        -------
2105
        Shortcuts
2106
        """
2107
        return self._type
1✔
2108

2109
    def __repr__(self):
1✔
2110
        return f"(shortcut:{self._type}: {self.nodes})"
1✔
2111

2112
    def _get_last_node(self, p):
1✔
2113
        last = p[0]
1✔
2114
        if isinstance(last, ValueNode):
1✔
2115
            return collections.deque([last])
1✔
2116
        return collections.deque()
1✔
2117

2118
    def _expand_repeat(self, p):
1✔
2119
        self._nodes = self._get_last_node(p)
1✔
2120
        repeat = p[1]
1✔
2121
        try:
1✔
2122
            repeat_num_str = repeat.lower().replace("r", "")
1✔
2123
            repeat_num = int(repeat_num_str)
1✔
2124
            self._num_node = ValueNode(repeat_num, int, never_pad=True)
1✔
2125
        except ValueError:
1✔
2126
            repeat_num = 1
1✔
2127
            self._num_node = ValueNode(None, int, never_pad=True)
1✔
2128
        if isinstance(p[0], ValueNode):
1✔
2129
            last_val = p[0]
1✔
2130
        else:
2131
            if isinstance(p[0], GeometryTree):
1✔
2132
                last_val = list(p[0])[-1]
1✔
2133
            else:
2134
                last_val = p[0].nodes[-1]
1✔
2135
        if last_val.value is None:
1✔
2136
            raise ValueError(f"Repeat cannot follow a jump. Given: {list(p)}")
1✔
2137
        self._nodes += [copy.deepcopy(last_val) for i in range(repeat_num)]
1✔
2138

2139
    def _expand_multiply(self, p):
1✔
2140
        self._nodes = self._get_last_node(p)
1✔
2141
        mult_str = p[1].lower().replace("m", "")
1✔
2142
        mult_val = fortran_float(mult_str)
1✔
2143
        self._num_node = ValueNode(mult_str, float, never_pad=True)
1✔
2144
        if isinstance(p[0], ValueNode):
1✔
2145
            last_val = self.nodes[-1]
1✔
2146
        elif isinstance(p[0], GeometryTree):
1✔
2147
            if "right" in p[0].nodes:
1✔
2148
                last_val = p[0].nodes["right"]
1✔
2149
            else:
2150
                last_val = p[0].nodes["left"]
×
2151
        else:
2152
            last_val = p[0].nodes[-1]
1✔
2153
        if last_val.value is None:
1✔
2154
            raise ValueError(f"Multiply cannot follow a jump. Given: {list(p)}")
1✔
2155
        self._nodes.append(copy.deepcopy(last_val))
1✔
2156
        self.nodes[-1].value *= mult_val
1✔
2157

2158
    def _expand_jump(self, p):
1✔
2159
        try:
1✔
2160
            jump_str = p[0].lower().replace("j", "")
1✔
2161
            jump_num = int(jump_str)
1✔
2162
            self._num_node = ValueNode(jump_str, int, never_pad=True)
1✔
2163
        except ValueError:
1✔
2164
            jump_num = 1
1✔
2165
            self._num_node = ValueNode(None, int, never_pad=True)
1✔
2166
        for i in range(jump_num):
1✔
2167
            self._nodes.append(ValueNode(input_parser.mcnp_input.Jump(), float))
1✔
2168

2169
    def _expand_interpolate(self, p):
1✔
2170
        if self._type == Shortcuts.LOG_INTERPOLATE:
1✔
2171
            is_log = True
1✔
2172
        else:
2173
            is_log = False
1✔
2174
        if hasattr(p, "geometry_term"):
1✔
2175
            term = p.geometry_term
1✔
2176
            if isinstance(term, GeometryTree):
1✔
2177
                begin = list(term)[-1].value
1✔
2178
            else:
2179
                begin = term.value
1✔
2180
            end = p.number_phrase.value
1✔
2181
        else:
2182
            if isinstance(p[0], ListNode):
1✔
2183
                begin = p[0].nodes[-1].value
1✔
2184
            else:
2185
                begin = p[0].value
1✔
2186
            end = p.number_phrase.value
1✔
2187
        self._nodes = self._get_last_node(p)
1✔
2188
        if begin is None:
1✔
2189
            raise ValueError(f"Interpolates cannot follow a jump. Given: {list(p)}")
1✔
2190
        match = self._num_finder.search(p[1])
1✔
2191
        if match:
1✔
2192
            number = int(match.group(0))
1✔
2193
            self._num_node = ValueNode(match.group(0), int, never_pad=True)
1✔
2194
        else:
2195
            number = 1
1✔
2196
            self._num_node = ValueNode(None, int, never_pad=True)
1✔
2197
        if is_log:
1✔
2198
            begin = math.log(begin, 10)
1✔
2199
            end = math.log(end, 10)
1✔
2200
        spacing = (end - begin) / (number + 1)
1✔
2201
        for i in range(number):
1✔
2202
            if is_log:
1✔
2203
                new_val = 10 ** (begin + spacing * (i + 1))
1✔
2204
            else:
2205
                new_val = begin + spacing * (i + 1)
1✔
2206
            self.append(
1✔
2207
                ValueNode(
2208
                    str(self._data_type(new_val)), self._data_type, never_pad=True
2209
                )
2210
            )
2211
        self._begin = begin
1✔
2212
        self._end = end
1✔
2213
        self._spacing = spacing
1✔
2214
        self.append(p.number_phrase)
1✔
2215

2216
    def _can_consume_node(self, node, direction, last_edge_shortcut=False):
1✔
2217
        """If it's possible to consume this node.
2218

2219
        Parameters
2220
        ----------
2221
        node : ValueNode
2222
            the node to consume
2223
        direction : int
2224
            the direct to go in. Must be in {-1, 1}
2225
        last_edge_shortcut : bool
2226
            Whether the previous node in the list was part of a
2227
            different shortcut
2228

2229
        Returns
2230
        -------
2231
        bool
2232
            true it can be consumed.
2233
        """
2234
        if self._type == Shortcuts.JUMP:
1✔
2235
            if node.value is None:
1✔
2236
                return True
1✔
2237

2238
        # REPEAT
2239
        elif self._type == Shortcuts.REPEAT:
1✔
2240
            if len(self.nodes) == 0 and node.value is not None:
1✔
2241
                return True
1✔
2242
            if direction == 1:
1✔
2243
                edge = self.nodes[-1]
1✔
2244
            else:
2245
                edge = self.nodes[0]
1✔
2246
            if edge.type != node.type or edge.value is None or node.value is None:
1✔
2247
                return False
1✔
2248
            if edge.type in {int, float} and math.isclose(
1✔
2249
                edge.value, node.value, rel_tol=rel_tol, abs_tol=abs_tol
2250
            ):
2251
                return True
1✔
2252
            elif edge.value == node.value:
1✔
2253
                return True
×
2254

2255
        # INTERPOLATE
2256
        elif self._type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
1✔
2257
            return self._is_valid_interpolate_edge(node, direction)
1✔
2258
        # Multiply can only ever have 1 value
2259
        elif self._type == Shortcuts.MULTIPLY:
1✔
2260
            # can't do a multiply with a Jump
2261
            if node.value is None:
1✔
2262
                return False
1✔
2263
            if len(self.nodes) == 0:
1✔
2264
                # clear out old state if needed
2265
                self._full = False
1✔
2266
                if last_edge_shortcut:
1✔
2267
                    self._full = True
1✔
2268
                return True
1✔
2269
            if len(self.nodes) == 1 and not self._full:
1✔
2270
                return True
1✔
2271
        return False
1✔
2272

2273
    def _is_valid_interpolate_edge(self, node, direction):
1✔
2274
        """Is a valid interpolation edge.
2275

2276
        Parameters
2277
        ----------
2278
        node : ValueNode
2279
            the node to consume
2280
        direction : int
2281
            the direct to go in. Must be in {-1, 1}
2282

2283
        Returns
2284
        -------
2285
        bool
2286
            true it can be consumed.
2287
        """
2288
        # kill jumps immediately
2289
        if node.value is None:
1✔
2290
            return False
1✔
2291
        if len(self.nodes) == 0:
1✔
2292
            new_val = self._begin if direction == 1 else self._end
1✔
2293
            if self._type == Shortcuts.LOG_INTERPOLATE:
1✔
2294
                new_val = 10**new_val
1✔
2295
        else:
2296
            edge = self.nodes[-1] if direction == 1 else self.nodes[0]
1✔
2297
            edge = edge.value
1✔
2298
            if self._type == Shortcuts.LOG_INTERPOLATE:
1✔
2299
                edge = math.log(edge, 10)
1✔
2300
                new_val = 10 ** (edge + direction * self._spacing)
1✔
2301
            else:
2302
                new_val = edge + direction * self._spacing
1✔
2303
        return math.isclose(new_val, node.value, rel_tol=rel_tol, abs_tol=abs_tol)
1✔
2304

2305
    def consume_edge_node(self, node, direction, last_edge_shortcut=False):
1✔
2306
        """Tries to consume the given edge.
2307

2308
        If it can be consumed the node is appended to the internal nodes.
2309

2310
        Parameters
2311
        ----------
2312
        node : ValueNode
2313
            the node to consume
2314
        direction : int
2315
            the direct to go in. Must be in {-1, 1}
2316
        last_edge_shortcut : bool
2317
            Whether or the previous node in the list was part of a
2318
            different shortcut
2319

2320
        Returns
2321
        -------
2322
        bool
2323
            True if the node was consumed.
2324
        """
2325
        if self._can_consume_node(node, direction, last_edge_shortcut):
1✔
2326
            if direction == 1:
1✔
2327
                self._nodes.append(node)
1✔
2328
            else:
2329
                self._nodes.appendleft(node)
1✔
2330
            return True
1✔
2331
        return False
1✔
2332

2333
    def format(self, leading_node=None):
1✔
2334
        if self._type == Shortcuts.JUMP:
1✔
2335
            temp = self._format_jump()
1✔
2336
        # repeat
2337
        elif self._type == Shortcuts.REPEAT:
1✔
2338
            temp = self._format_repeat(leading_node)
1✔
2339
        # multiply
2340
        elif self._type == Shortcuts.MULTIPLY:
1✔
2341
            temp = self._format_multiply(leading_node)
1✔
2342
        # interpolate
2343
        elif self._type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
1✔
2344
            temp = self._format_interpolate(leading_node)
1✔
2345
        if self.end_padding:
1✔
2346
            pad_str = self.end_padding.format()
1✔
2347
        else:
2348
            pad_str = ""
1✔
2349
        return f"{temp}{pad_str}"
1✔
2350

2351
    def _format_jump(self):
1✔
2352
        num_jumps = len(self.nodes)
1✔
2353
        if num_jumps == 0:
1✔
2354
            return ""
×
2355
        if len(self._original) > 0 and "j" in self._original[0]:
1✔
2356
            j = "j"
1✔
2357
        else:
2358
            j = "J"
1✔
2359
        length = len(self._original)
1✔
2360
        self._num_node.value = num_jumps
1✔
2361
        if num_jumps == 1 and (
1✔
2362
            length == 0 or (length > 0 and "1" not in self._original[0])
2363
        ):
2364
            num_jumps = ""
1✔
2365
        else:
2366
            num_jumps = self._num_node
1✔
2367

2368
        return f"{num_jumps.format()}{j}"
1✔
2369

2370
    def _can_use_last_node(self, node, start=None):
1✔
2371
        """Determine if the previous node can be used as the start to this node
2372
        (and therefore skip the start of this one).
2373

2374
        Last node can be used if
2375
        - it's a basic ValueNode that matches this repeat
2376
        - it's also a shortcut, with the same edge values.
2377

2378
        Parameters
2379
        ----------
2380
        node : ValueNode, ShortcutNode
2381
            the previous node to test.
2382
        start : float
2383
            the starting value for this node (specifically for
2384
            interpolation)
2385

2386
        Returns
2387
        -------
2388
        bool
2389
            True if the node given can be used.
2390
        """
2391
        if isinstance(node, ValueNode):
1✔
2392
            value = node.value
×
2393
        elif isinstance(node, ShortcutNode):
1✔
2394
            value = node.nodes[-1].value
1✔
2395
        else:
2396
            return False
1✔
2397
        if value is None:
1✔
2398
            return False
1✔
2399
        if start is None:
1✔
2400
            start = self.nodes[0].value
1✔
2401
        return math.isclose(start, value)
1✔
2402

2403
    def _format_repeat(self, leading_node=None):
1✔
2404

2405
        if self._can_use_last_node(leading_node):
1✔
2406
            first_val = ""
1✔
2407
            num_extra = 0
1✔
2408
        else:
2409
            first_val = self.nodes[0].format()
1✔
2410
            num_extra = 1
1✔
2411
        num_repeats = len(self.nodes) - num_extra
1✔
2412
        self._num_node.value = num_repeats
1✔
2413
        if len(self._original) >= 2 and "r" in self._original[1]:
1✔
2414
            r = "r"
1✔
2415
        else:
2416
            r = "R"
1✔
2417
        if (
1✔
2418
            num_repeats == 1
2419
            and len(self._original) >= 2
2420
            and "1" not in self._original[1]
2421
        ):
2422
            num_repeats = ""
1✔
2423
        else:
2424
            num_repeats = self._num_node
1✔
2425
        return f"{first_val}{num_repeats.format()}{r}"
1✔
2426

2427
    def _format_multiply(self, leading_node=None):
1✔
2428
        # Multiply doesn't usually consume other nodes
2429
        if leading_node is not None and len(self) == 1:
1✔
2430
            first_val = leading_node.nodes[-1]
1✔
2431
            first_val_str = ""
1✔
2432
        else:
2433
            first_val = self.nodes[0]
1✔
2434
            first_val_str = first_val
1✔
2435
        if self._original and "m" in self._original[-1]:
1✔
2436
            m = "m"
1✔
2437
        else:
2438
            m = "M"
1✔
2439
        self._num_node.value = self.nodes[-1].value / first_val.value
1✔
2440
        return f"{first_val_str.format()}{self._num_node.format()}{m}"
1✔
2441

2442
    def _format_interpolate(self, leading_node=None):
1✔
2443
        begin = self._begin
1✔
2444
        if self.type == Shortcuts.LOG_INTERPOLATE:
1✔
2445
            begin = 10**begin
1✔
2446
        if self._can_use_last_node(leading_node, begin):
1✔
2447
            start = ""
1✔
2448
            num_extra_nodes = 1
1✔
2449
            if hasattr(self, "_has_pseudo_start"):
1✔
2450
                num_extra_nodes += 1
1✔
2451
        else:
2452
            start = self.nodes[0]
1✔
2453
            num_extra_nodes = 2
1✔
2454
        end = self.nodes[-1]
1✔
2455
        num_interp = len(self.nodes) - num_extra_nodes
1✔
2456
        self._num_node.value = num_interp
1✔
2457
        interp = "I"
1✔
2458
        can_match = False
1✔
2459
        if len(self._original) > 0:
1✔
2460
            if match := re.match(r"\d*(\w+)", self._original[1]):
1✔
2461
                can_match = True
1✔
2462
                interp = match.group(1)
1✔
2463
        if not can_match:
1✔
2464
            if self._type == Shortcuts.LOG_INTERPOLATE:
1✔
2465
                interp = "ILOG"
1✔
2466
        if (
1✔
2467
            num_interp == 1
2468
            and len(self._original) >= 2
2469
            and "1" not in self._original[1]
2470
        ):
2471
            num_interp = ""
1✔
2472
        else:
2473
            num_interp = self._num_node
1✔
2474
        if len(self._original) >= 3:
1✔
2475
            padding = self._original[2]
1✔
2476
        else:
2477
            padding = PaddingNode(" ")
1✔
2478
        return f"{start.format()}{num_interp.format()}{interp}{padding.format()}{end.format()}"
1✔
2479

2480

2481
class ClassifierNode(SyntaxNodeBase):
1✔
2482
    """A node to represent the classifier for a :class:`montepy.data_input.DataInput`
2483

2484
    e.g., represents ``M4``, ``F104:n,p``, ``IMP:n,e``.
2485
    """
2486

2487
    def __init__(self):
1✔
2488
        super().__init__("classifier")
1✔
2489
        self._prefix = None
1✔
2490
        self._number = None
1✔
2491
        self._particles = None
1✔
2492
        self._modifier = None
1✔
2493
        self._padding = None
1✔
2494
        self._nodes = []
1✔
2495

2496
    @property
1✔
2497
    def prefix(self):
1✔
2498
        """The prefix for the classifier.
2499

2500
        That is the string that tells what type of input this is.
2501

2502
        E.g.: ``M`` in ``M4`` or ``IMP`` in ``IMP:n``.
2503

2504
        Returns
2505
        -------
2506
        ValueNode
2507
            the prefix
2508
        """
2509
        return self._prefix
1✔
2510

2511
    @prefix.setter
1✔
2512
    def prefix(self, pref):
1✔
2513
        self.append(pref)
1✔
2514
        self._prefix = pref
1✔
2515

2516
    @property
1✔
2517
    def number(self):
1✔
2518
        """The number if any for the classifier.
2519

2520
        Returns
2521
        -------
2522
        ValueNode
2523
            the number holder for this classifier.
2524
        """
2525
        return self._number
1✔
2526

2527
    @number.setter
1✔
2528
    def number(self, number):
1✔
2529
        self.append(number)
1✔
2530
        self._number = number
1✔
2531

2532
    @property
1✔
2533
    def particles(self):
1✔
2534
        """The particles if any tied to this classifier.
2535

2536
        Returns
2537
        -------
2538
        ParticleNode
2539
            the particles used.
2540
        """
2541
        return self._particles
1✔
2542

2543
    @particles.setter
1✔
2544
    def particles(self, part):
1✔
2545
        self.append(part)
1✔
2546
        self._particles = part
1✔
2547

2548
    @property
1✔
2549
    def modifier(self):
1✔
2550
        """The modifier for this classifier if any.
2551

2552
        A modifier is a prefix character that changes the inputs behavior,
2553
        e.g.: ``*`` or ``+``.
2554

2555
        Returns
2556
        -------
2557
        ValueNode
2558
            the modifier
2559
        """
2560
        return self._modifier
1✔
2561

2562
    @modifier.setter
1✔
2563
    def modifier(self, mod):
1✔
2564
        self.append(mod)
1✔
2565
        self._modifier = mod
1✔
2566

2567
    @property
1✔
2568
    def padding(self):
1✔
2569
        """The padding for this classifier.
2570

2571
        .. Note::
2572
            None of the ValueNodes in this object should have padding.
2573

2574
        Returns
2575
        -------
2576
        PaddingNode
2577
            the padding after the classifier.
2578
        """
2579
        return self._padding
1✔
2580

2581
    @padding.setter
1✔
2582
    def padding(self, val):
1✔
2583
        self.append(val)
1✔
2584
        self._padding = val
1✔
2585

2586
    def format(self):
1✔
2587
        if self.modifier:
1✔
2588
            ret = self.modifier.format()
1✔
2589
        else:
2590
            ret = ""
1✔
2591
        ret += self.prefix.format()
1✔
2592
        if self.number:
1✔
2593
            ret += self.number.format()
1✔
2594
        if self.particles:
1✔
2595
            ret += self.particles.format()
1✔
2596
        if self.padding:
1✔
2597
            ret += self.padding.format()
1✔
2598
        return ret
1✔
2599

2600
    def __str__(self):
1✔
2601
        return self.format()
1✔
2602

2603
    def __repr__(self):
1✔
2604
        return (
1✔
2605
            f"(Classifier: mod: {self.modifier}, prefix: {self.prefix}, "
2606
            f"number: {self.number}, particles: {self.particles},"
2607
            f" padding: {self.padding})"
2608
        )
2609

2610
    def _pretty_str(self):
1✔
2611
        return f"""<Classifier: {{ 
1✔
2612
    mod: {self.modifier}, 
2613
    prefix: {self.prefix}, 
2614
    number: {self.number}, 
2615
    particles: {self.particles},
2616
    padding: {self.padding} 
2617
  }}
2618
>
2619
"""
2620

2621
    @property
1✔
2622
    def comments(self):
1✔
2623
        if self.padding is not None:
1✔
2624
            yield from self.padding.comments
1✔
2625
        else:
2626
            yield from []
1✔
2627

2628
    def get_trailing_comment(self):
1✔
2629
        if self.padding:
1✔
2630
            return self.padding.get_trailing_comment()
1✔
2631

2632
    def _delete_trailing_comment(self):
1✔
2633
        if self.padding:
1✔
2634
            self.padding._delete_trailing_comment()
1✔
2635

2636
    def flatten(self):
1✔
2637
        ret = []
1✔
2638
        if self.modifier:
1✔
2639
            ret.append(self.modifier)
1✔
2640
        ret.append(self.prefix)
1✔
2641
        if self.number:
1✔
2642
            ret.append(self.number)
1✔
2643
        if self.particles:
1✔
2644
            ret.append(self.particles)
1✔
2645
        if self.padding:
1✔
2646
            ret.append(self.padding)
1✔
2647
        return ret
1✔
2648

2649

2650
class ParametersNode(SyntaxNodeBase):
1✔
2651
    """A node to hold the parameters, key-value pairs, for this input.
2652

2653
    This behaves like a dictionary and is accessible by their key*
2654

2655
    .. Note::
2656
        How to access values.
2657

2658
        The internal dictionary doesn't use the full classifier directly,
2659
        because some parameters should not be both allowed: e.g., ``fill`` and ``*fill``.
2660
        The key is a string that is all lower case, and only uses the classifiers prefix,
2661
        and particles.
2662

2663
        So to access a cell's fill information you would run:
2664

2665
        .. code-block:: python
2666

2667
            parameters["fill"]
2668

2669
        And to access the n,p importance:
2670

2671
        .. code-block:: python
2672

2673
            parameters["imp:n,p"]
2674
    """
2675

2676
    def __init__(self):
1✔
2677
        super().__init__("parameters")
1✔
2678
        self._nodes = {}
1✔
2679

2680
    def append(self, val, is_default=False):
1✔
2681
        """Append the node to this node.
2682

2683
        This takes a syntax node, which requires the keys:
2684
            ``["classifier", "seperator", "data"]``
2685

2686
        Parameters
2687
        ----------
2688
        val : SyntaxNode
2689
            the parameter to append.
2690
        is_default : bool
2691
            whether this parameter was added as a default tree not from
2692
            the user.
2693
        """
2694
        classifier = val["classifier"]
1✔
2695
        key = (
1✔
2696
            classifier.prefix.value
2697
            + (str(classifier.particles) if classifier.particles else "")
2698
        ).lower()
2699
        if key in self._nodes:
1✔
2700
            raise RedundantParameterSpecification(key, val)
1✔
2701
        if is_default:
1✔
2702
            val._is_default = True
1✔
2703
        self._nodes[key] = val
1✔
2704

2705
    def __str__(self):
1✔
2706
        return f"<Parameters, {self.nodes}>"
1✔
2707

2708
    def __repr__(self):
1✔
2709
        return str(self)
1✔
2710

2711
    def _pretty_str(self):
1✔
2712
        INDENT = 2
1✔
2713
        ret = f"<Node: {self.name}: {{\n"
1✔
2714
        for key, val in self.nodes.items():
1✔
2715
            child_strs = val._pretty_str().split("\n")
1✔
2716
            ret += " " * INDENT + f"{key}: {child_strs[0]}\n"
1✔
2717
            ret += "\n".join([" " * 2 * INDENT + s for s in child_strs[1:-1]])
1✔
2718
            ret += " " * 2 * INDENT + child_strs[-1] + ",\n"
1✔
2719
        ret += " " * INDENT + "}\n"
1✔
2720
        ret += ">"
1✔
2721
        return ret
1✔
2722

2723
    def __getitem__(self, key):
1✔
2724
        return self.nodes[key.lower()]
1✔
2725

2726
    def __contains__(self, key):
1✔
2727
        return key.lower() in self.nodes
1✔
2728

2729
    def format(self):
1✔
2730
        ret = ""
1✔
2731
        for node in self.nodes.values():
1✔
2732
            ret += node.format()
1✔
2733
        return ret
1✔
2734

2735
    def get_trailing_comment(self):
1✔
2736
        for node in reversed(self.nodes.values()):
1✔
2737
            if hasattr(node, "_is_default"):
1✔
2738
                continue
1✔
2739
            return node.get_trailing_comment()
1✔
2740

2741
    def _delete_trailing_comment(self):
1✔
2742
        for node in reversed(self.nodes.values()):
1✔
2743
            if hasattr(node, "_is_default"):
1✔
2744
                continue
1✔
2745
            node._delete_trailing_comment()
1✔
2746

2747
    @property
1✔
2748
    def comments(self):
1✔
2749
        for node in self.nodes.values():
1✔
2750
            if isinstance(node, SyntaxNodeBase):
1✔
2751
                yield from node.comments
1✔
2752

2753
    def flatten(self):
1✔
2754
        ret = []
1✔
2755
        for node in self.nodes.values():
1✔
2756
            if isinstance(node, (ValueNode, PaddingNode)):
1✔
2757
                ret.append(node)
×
2758
            else:
2759
                ret += node.flatten()
1✔
2760
        return ret
1✔
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