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

idaholab / MontePy / 14137241351

28 Mar 2025 08:22PM UTC coverage: 98.142% (+0.3%) from 97.83%
14137241351

push

github

web-flow
Merge pull request #608 from idaholab/alpha-test-dev

1.0.0 alpha to develop staging: YOLO merge.

1780 of 1796 new or added lines in 52 files covered. (99.11%)

13 existing lines in 8 files now uncovered.

7766 of 7913 relevant lines covered (98.14%)

0.98 hits per line

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

98.3
/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.errors 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✔
NEW
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✔
NEW
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✔
NEW
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
        },
972
        int: {"value_length": 0, "zero_padding": 0, "sign": "-"},
973
        str: {"value_length": 0},
974
    }
975
    """The default formatters for each type."""
1✔
976

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

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

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

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

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

1058
    def _convert_to_str(self):
1✔
1059
        """Converts this ValueNode to being a string type.
1060

1061
        .. versionadded:: 1.0.0
1062

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

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

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

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

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

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

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

1103
        Example use: cell density.
1104

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

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

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

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

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

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

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

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

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

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

1205
        self._formatter["precision"] = precision
1✔
1206

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

1210
        E.g., 1.0 -> 1
1211

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

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

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

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

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

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

1244
        Used to shortcut formatting and reverse engineering.
1245

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

1260
    def format(self):
1✔
1261
        if not self._value_changed:
1✔
1262
            return f"{self._token}{self.padding.format() if self.padding else ''}"
1✔
1263
        if self.value is None:
1✔
1264
            return ""
1✔
1265
        self._reverse_engineer_formatting()
1✔
1266
        if issubclass(self.type, enum.Enum):
1✔
1267
            value = self.value.value
1✔
1268
        else:
1269
            value = self._print_value
1✔
1270
        if self._type == int or self._can_float_to_int_happen():
1✔
1271
            temp = "{value:0={sign}{zero_padding}d}".format(
1✔
1272
                value=int(value), **self._formatter
1273
            )
1274
        elif self._type == float:
1✔
1275
            # default to python general if new value
1276
            if not self._is_reversed:
1✔
1277
                temp = "{value:0={sign}{zero_padding}.{precision}g}".format(
1✔
1278
                    value=value, **self._formatter
1279
                )
1280
            elif self._formatter["is_scientific"]:
1✔
1281
                temp = "{value:0={sign}{zero_padding}.{precision}e}".format(
1✔
1282
                    value=value, **self._formatter
1283
                )
1284
                temp = temp.replace("e", self._formatter["divider"])
1✔
1285
                temp_match = self._SCIENTIFIC_FINDER.match(temp)
1✔
1286
                exponent = temp_match.group("exponent")
1✔
1287
                start, end = temp_match.span("exponent")
1✔
1288
                new_exp_temp = "{value:0={zero_padding}d}".format(
1✔
1289
                    value=int(exponent),
1290
                    zero_padding=self._formatter["exponent_zero_pad"],
1291
                )
1292
                new_exp = "{temp:<{value_length}}".format(
1✔
1293
                    temp=new_exp_temp, value_length=self._formatter["exponent_length"]
1294
                )
1295
                temp = temp[0:start] + new_exp + temp[end:]
1✔
1296
            elif self._formatter["as_int"]:
1✔
1297
                temp = "{value:0={sign}0{zero_padding}g}".format(
1✔
1298
                    value=value, **self._formatter
1299
                )
1300
            else:
1301
                temp = "{value:0={sign}0{zero_padding}.{precision}f}".format(
1✔
1302
                    value=value, **self._formatter
1303
                )
1304
        else:
1305
            temp = str(value)
1✔
1306
        end_line_padding = False
1✔
1307
        if self.padding:
1✔
1308
            for node in self.padding.nodes:
1✔
1309
                if node == "\n":
1✔
1310
                    end_line_padding = True
1✔
1311
                    break
1✔
1312
                if isinstance(node, CommentNode):
1✔
1313
                    break
1✔
1314
            if self.padding.is_space(0):
1✔
1315
                # if there was and end space, and we ran out of space, and there isn't
1316
                # a saving space later on
1317
                if len(temp) >= self._formatter["value_length"] and not (
1✔
1318
                    len(self.padding) > 1
1319
                    and (self.padding.is_space(1) or self.padding.nodes[1] == "\n")
1320
                ):
1321
                    pad_str = " "
1✔
1322
                else:
1323
                    pad_str = ""
1✔
1324
                extra_pad_str = "".join([x.format() for x in self.padding.nodes[1:]])
1✔
1325
            else:
1326
                pad_str = ""
1✔
1327
                extra_pad_str = "".join([x.format() for x in self.padding.nodes])
1✔
1328
        else:
1329
            pad_str = ""
1✔
1330
            extra_pad_str = ""
1✔
1331
        if not self.never_pad:
1✔
1332
            buffer = "{temp:<{value_length}}{padding}".format(
1✔
1333
                temp=temp, padding=pad_str, **self._formatter
1334
            )
1335
        else:
1336
            buffer = "{temp}{padding}".format(
1✔
1337
                temp=temp, padding=pad_str, **self._formatter
1338
            )
1339
        """
1✔
1340
        If:
1341
            1. expanded
1342
            2. had an original value
1343
            3. and value doesn't end in a new line (without a comment)
1344
        """
1345
        if (
1✔
1346
            len(buffer) > self._formatter["value_length"]
1347
            and self._token is not None
1348
            and not end_line_padding
1349
        ):
1350
            warning = LineExpansionWarning("")
1✔
1351
            warning.cause = "value"
1✔
1352
            warning.og_value = self._token
1✔
1353
            warning.new_value = temp
1✔
1354
            warnings.warn(
1✔
1355
                warning,
1356
                category=LineExpansionWarning,
1357
            )
1358
        return buffer + extra_pad_str
1✔
1359

1360
    @property
1✔
1361
    def comments(self):
1✔
1362
        if self.padding is not None:
1✔
1363
            yield from self.padding.comments
1✔
1364
        else:
1365
            yield from []
1✔
1366

1367
    def get_trailing_comment(self):
1✔
1368
        if self.padding is None:
1✔
1369
            return
1✔
1370
        return self.padding.get_trailing_comment()
1✔
1371

1372
    def _delete_trailing_comment(self):
1✔
1373
        if self.padding is None:
1✔
1374
            return
1✔
1375
        self.padding._delete_trailing_comment()
1✔
1376

1377
    @property
1✔
1378
    def padding(self):
1✔
1379
        """The padding if any for this ValueNode.
1380

1381
        Returns
1382
        -------
1383
        PaddingNode
1384
            the padding if any.
1385
        """
1386
        return self._padding
1✔
1387

1388
    @padding.setter
1✔
1389
    def padding(self, pad):
1✔
1390
        self._padding = pad
1✔
1391

1392
    @property
1✔
1393
    def type(self):
1✔
1394
        """The data type for this ValueNode.
1395

1396
        Examples: float, int, str, Lattice
1397

1398
        Returns
1399
        -------
1400
        Class
1401
            the class for the value of this node.
1402
        """
1403
        return self._type
1✔
1404

1405
    @property
1✔
1406
    def token(self):
1✔
1407
        """The original text (token) for this ValueNode.
1408

1409
        Returns
1410
        -------
1411
        str
1412
            the original input.
1413
        """
1414
        return self._token
1✔
1415

1416
    def __str__(self):
1✔
1417
        return f"<Value, {self._value}, padding: {self._padding}>"
1✔
1418

1419
    def _pretty_str(self):
1✔
1420
        return str(self)
1✔
1421

1422
    def __repr__(self):
1✔
1423
        return str(self)
1✔
1424

1425
    @property
1✔
1426
    def value(self):
1✔
1427
        """The current semantic value of this ValueNode.
1428

1429
        This is the parsed meaning in the type of ``self.type``,
1430
        that can be updated. When this value is updated, next time format()
1431
        is ran this value will be used.
1432

1433
        Returns
1434
        -------
1435
        float, int, str, enum
1436
            the node's value in type ``type``.
1437
        """
1438
        return self._value
1✔
1439

1440
    @property
1✔
1441
    def never_pad(self):
1✔
1442
        """Whether or not this value node will not have extra spaces added.
1443

1444
        Returns
1445
        -------
1446
        bool
1447
            true if extra padding is not adding at the end if missing.
1448
        """
1449
        return self._never_pad
1✔
1450

1451
    @never_pad.setter
1✔
1452
    def never_pad(self, never_pad):
1✔
1453
        self._never_pad = never_pad
1✔
1454

1455
    @value.setter
1✔
1456
    def value(self, value):
1✔
1457
        if self.is_negative is not None and value is not None:
1✔
1458
            value = abs(value)
1✔
1459
        self._check_if_needs_end_padding(value)
1✔
1460
        self._value = value
1✔
1461

1462
    def _check_if_needs_end_padding(self, value):
1✔
1463
        if value is None or self.value is not None or self._never_pad:
1✔
1464
            return
1✔
1465
        # if not followed by a trailing space
1466
        if self.padding is None:
1✔
1467
            self.padding = PaddingNode(" ")
1✔
1468

1469
    def __eq__(self, other):
1✔
1470
        if not isinstance(other, (type(self), str, Real)):
1✔
1471
            return False
1✔
1472
        if isinstance(other, ValueNode):
1✔
1473
            other_val = other.value
1✔
1474
            if self.type != other.type:
1✔
1475
                return False
1✔
1476
        else:
1477
            other_val = other
1✔
1478
            if self.type != type(other):
1✔
1479
                return False
1✔
1480

1481
        if self.type == float and self.value is not None and other_val is not None:
1✔
1482
            return math.isclose(self.value, other_val, rel_tol=rel_tol, abs_tol=abs_tol)
1✔
1483
        return self.value == other_val
1✔
1484

1485

1486
class ParticleNode(SyntaxNodeBase):
1✔
1487
    """A node to hold particles information in a :class:`ClassifierNode`.
1488

1489
    Parameters
1490
    ----------
1491
    name : str
1492
        the name for the node.
1493
    token : str
1494
        the original token from parsing
1495
    """
1496

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

1499
    def __init__(self, name, token):
1✔
1500
        super().__init__(name)
1✔
1501
        self._nodes = [self]
1✔
1502
        self._token = token
1✔
1503
        self._order = []
1✔
1504
        classifier_chunks = token.replace(":", "").split(",")
1✔
1505
        self._particles = set()
1✔
1506
        self._formatter = {"upper": False}
1✔
1507
        for chunk in classifier_chunks:
1✔
1508
            part = Particle(chunk.upper())
1✔
1509
            self._particles.add(part)
1✔
1510
            self._order.append(part)
1✔
1511

1512
    @property
1✔
1513
    def token(self):
1✔
1514
        """The original text (token) for this ParticleNode.
1515

1516
        Returns
1517
        -------
1518
        str
1519
            the original input.
1520
        """
1521
        return self._token
1✔
1522

1523
    @property
1✔
1524
    def particles(self):
1✔
1525
        """The particles included in this node.
1526

1527
        Returns
1528
        -------
1529
        set
1530
            a set of the particles being used.
1531
        """
1532
        return self._particles
1✔
1533

1534
    @particles.setter
1✔
1535
    def particles(self, values):
1✔
1536
        if not isinstance(values, (list, set)):
1✔
1537
            raise TypeError(f"Particles must be a set. {values} given.")
1✔
1538
        for value in values:
1✔
1539
            if not isinstance(value, Particle):
1✔
1540
                raise TypeError(f"All particles must be a Particle. {value} given")
1✔
1541
        if isinstance(values, list):
1✔
1542
            self._order = values
1✔
1543
            values = set(values)
1✔
1544
        self._particles = values
1✔
1545

1546
    def add(self, value):
1✔
1547
        """Add a particle to this node.
1548

1549
        Parameters
1550
        ----------
1551
        value : Particle
1552
            the particle to add.
1553
        """
1554
        if not isinstance(value, Particle):
1✔
1555
            raise TypeError(f"All particles must be a Particle. {value} given")
1✔
1556
        self._order.append(value)
1✔
1557
        self._particles.add(value)
1✔
1558

1559
    def remove(self, value):
1✔
1560
        """Remove a particle from this node.
1561

1562
        Parameters
1563
        ----------
1564
        value : Particle
1565
            the particle to remove.
1566
        """
1567
        if not isinstance(value, Particle):
1✔
1568
            raise TypeError(f"All particles must be a Particle. {value} given")
1✔
1569
        self._particles.remove(value)
1✔
1570
        self._order.remove(value)
1✔
1571

1572
    @property
1✔
1573
    def _particles_sorted(self):
1✔
1574
        """The particles in this node ordered in a nice-ish way.
1575

1576
        Ordering:
1577
            1. User input.
1578
            2. Order of particles appended
1579
            3. randomly at the end if all else fails.
1580

1581
        Returns
1582
        -------
1583
        list
1584
        """
1585
        ret = self._order
1✔
1586
        ret_set = set(ret)
1✔
1587
        remainder = self.particles - ret_set
1✔
1588
        extras = ret_set - self.particles
1✔
1589
        for straggler in sorted(remainder):
1✔
1590
            ret.append(straggler)
1✔
1591
        for useless in extras:
1✔
1592
            ret.remove(useless)
1✔
1593
        return ret
1✔
1594

1595
    def format(self):
1✔
1596
        self._reverse_engineer_format()
1✔
1597
        if self._formatter["upper"]:
1✔
1598
            parts = [p.value.upper() for p in self._particles_sorted]
1✔
1599
        else:
1600
            parts = [p.value.lower() for p in self._particles_sorted]
1✔
1601
        return f":{','.join(parts)}"
1✔
1602

1603
    def _reverse_engineer_format(self):
1✔
1604
        total_match = 0
1✔
1605
        upper_match = 0
1✔
1606
        for match in self._letter_finder.finditer(self._token):
1✔
1607
            if match:
1✔
1608
                if match.group(0).isupper():
1✔
1609
                    upper_match += 1
1✔
1610
                total_match += 1
1✔
1611
        if total_match and upper_match / total_match >= 0.5:
1✔
1612
            self._formatter["upper"] = True
1✔
1613

1614
    @property
1✔
1615
    def comments(self):
1✔
1616
        yield from []
1✔
1617

1618
    def __repr__(self):
1✔
1619
        return self.format()
1✔
1620

1621
    def __iter__(self):
1✔
1622
        return iter(self.particles)
1✔
1623

1624

1625
class ListNode(SyntaxNodeBase):
1✔
1626
    """A node to represent a list of values.
1627

1628
    Parameters
1629
    ----------
1630
    name : str
1631
        the name of this node.
1632
    """
1633

1634
    def __init__(self, name):
1✔
1635
        super().__init__(name)
1✔
1636
        self._shortcuts = []
1✔
1637

1638
    def __repr__(self):
1✔
1639
        return f"(list: {self.name}, {self.nodes})"
1✔
1640

1641
    def update_with_new_values(self, new_vals):
1✔
1642
        """Update this list node with new values.
1643

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

1649
        Parameters
1650
        ----------
1651
        new_vals : list
1652
            the new values (a list of ValueNodes)
1653
        """
1654
        if not new_vals:
1✔
1655
            self._nodes = []
×
1656
            return
×
1657
        new_vals_cache = {id(v): v for v in new_vals}
1✔
1658
        # bind shortcuts to single site in new values
1659
        for shortcut in self._shortcuts:
1✔
1660
            for node in shortcut.nodes:
1✔
1661
                if id(node) in new_vals_cache:
1✔
1662
                    new_vals_cache[id(node)] = shortcut
1✔
1663
                    shortcut.nodes.clear()
1✔
1664
                    break
1✔
1665
        self._expand_shortcuts(new_vals, new_vals_cache)
1✔
1666
        self._shortcuts = []
1✔
1667
        self._nodes = []
1✔
1668
        for key, node in new_vals_cache.items():
1✔
1669
            if isinstance(node, ShortcutNode):
1✔
1670
                if (
1✔
1671
                    len(self._shortcuts) > 0 and node is not self._shortcuts[-1]
1672
                ) or len(self._shortcuts) == 0:
1673
                    self._shortcuts.append(node)
1✔
1674
                    self._nodes.append(node)
1✔
1675
            else:
1676
                self._nodes.append(node)
1✔
1677
        end = self._nodes[-1]
1✔
1678
        # pop off final shortcut if it's a jump the user left off
1679
        if (
1✔
1680
            isinstance(end, ShortcutNode)
1681
            and end._type == Shortcuts.JUMP
1682
            and len(end._original) == 0
1683
        ):
1684
            self._nodes.pop()
1✔
1685
            self._shortcuts.pop()
1✔
1686

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

1690
        Parameters
1691
        ----------
1692
        new_vals : list
1693
            the new values.
1694
        new_vals_cache : dict
1695
            a dictionary mapping the id of the ValueNode to the
1696
            ValueNode or ShortcutNode. This is ordered the same as
1697
            ``new_vals``.
1698
        """
1699

1700
        def try_expansion(shortcut, value):
1✔
1701
            status = shortcut.consume_edge_node(
1✔
1702
                value, 1, i == last_end + 1 and last_end != 0
1703
            )
1704
            if status:
1✔
1705
                new_vals_cache[id(value)] = shortcut
1✔
1706
            else:
1707
                new_vals_cache[id(value)] = value
1✔
1708
            return status
1✔
1709

1710
        def try_reverse_expansion(shortcut, i, last_end):
1✔
1711
            if i > 1:
1✔
1712
                for value in new_vals[i - 1 : last_end : -1]:
1✔
1713
                    if shortcut.consume_edge_node(value, -1):
1✔
1714
                        new_vals_cache[id(value)] = shortcut
1✔
1715
                    else:
1716
                        new_vals_cache[id(value)] = value
1✔
1717
                        return
1✔
1718

1719
        def check_for_orphan_jump(value):
1✔
1720
            """Checks if the current Jump is not tied to an existing Shortcut"""
1721
            nonlocal shortcut
1722
            if value.value is None and shortcut is None:
1✔
1723
                shortcut = ShortcutNode(p=None, short_type=Shortcuts.JUMP)
1✔
1724
                if shortcut.consume_edge_node(value, 1):
1✔
1725
                    new_vals_cache[id(value)] = shortcut
1✔
1726

1727
        shortcut = None
1✔
1728
        last_end = 0
1✔
1729
        for i, value in enumerate(new_vals_cache.values()):
1✔
1730
            # found a new shortcut
1731
            if isinstance(value, ShortcutNode):
1✔
1732
                # shortcuts bumped up against each other
1733
                if shortcut is not None:
1✔
1734
                    last_end = i - 1
1✔
1735
                shortcut = value
1✔
1736
                if try_expansion(shortcut, new_vals[i]):
1✔
1737
                    try_reverse_expansion(shortcut, i, last_end)
1✔
1738
                else:
1739
                    shortcut = None
×
1740
            # otherwise it is actually a value to expand as well
1741
            else:
1742
                if shortcut is not None:
1✔
1743
                    if not try_expansion(shortcut, new_vals[i]):
1✔
1744
                        last_end = i - 1
1✔
1745
                        shortcut = None
1✔
1746
                        check_for_orphan_jump(new_vals[i])
1✔
1747
                else:
1748
                    check_for_orphan_jump(new_vals[i])
1✔
1749

1750
    def append(self, val, from_parsing=False):
1✔
1751
        """Append the node to this node.
1752

1753
        Parameters
1754
        ----------
1755
        node : ValueNode, ShortcutNode
1756
            node
1757
        from_parsing : bool
1758
            If this is being append from the parsers, and not elsewhere.
1759
        """
1760
        if isinstance(val, ShortcutNode):
1✔
1761
            self._shortcuts.append(val)
1✔
1762
        if len(self) > 0 and from_parsing:
1✔
1763
            last = self[-1]
1✔
1764
            if isinstance(last, ValueNode) and (
1✔
1765
                (last.padding and not last.padding.has_space) or last.padding is None
1766
            ):
1767
                self[-1].never_pad = True
1✔
1768
        super().append(val)
1✔
1769

1770
    @property
1✔
1771
    def comments(self):
1✔
1772
        for node in self.nodes:
1✔
1773
            yield from node.comments
1✔
1774

1775
    def format(self):
1✔
1776
        ret = ""
1✔
1777
        length = len(self.nodes)
1✔
1778
        last_node = None
1✔
1779
        for i, node in enumerate(self.nodes):
1✔
1780
            # adds extra padding
1781
            if (
1✔
1782
                isinstance(node, ValueNode)
1783
                and node.padding is None
1784
                and i < length - 1
1785
                and not isinstance(self.nodes[i + 1], PaddingNode)
1786
                and not node.never_pad
1787
            ):
1788
                node.padding = PaddingNode(" ")
1✔
1789
            if isinstance(last_node, ShortcutNode) and isinstance(node, ShortcutNode):
1✔
1790
                ret += node.format(last_node)
1✔
1791
            else:
1792
                ret += node.format()
1✔
1793
            last_node = node
1✔
1794
        return ret
1✔
1795

1796
    def __iter__(self):
1✔
1797
        for node in self.nodes:
1✔
1798
            if isinstance(node, ShortcutNode):
1✔
1799
                yield from node.nodes
1✔
1800
            else:
1801
                yield node
1✔
1802

1803
    def __contains__(self, value):
1✔
1804
        for node in self:
1✔
1805
            if node == value:
1✔
1806
                return True
1✔
1807
        return False
1✔
1808

1809
    def __getitem__(self, indx):
1✔
1810
        if isinstance(indx, slice):
1✔
1811
            return self.__get_slice(indx)
1✔
1812
        if indx >= 0:
1✔
1813
            for i, item in enumerate(self):
1✔
1814
                if i == indx:
1✔
1815
                    return item
1✔
1816
        else:
1817
            items = list(self)
1✔
1818
            return items[indx]
1✔
1819
        raise IndexError(f"{indx} not in ListNode")
1✔
1820

1821
    def __get_slice(self, i: slice):
1✔
1822
        """Helper function for __getitem__ with slices."""
1823
        rstep = i.step if i.step is not None else 1
1✔
1824
        rstart = i.start
1✔
1825
        rstop = i.stop
1✔
1826
        if rstep < 0:  # Backwards
1✔
1827
            if rstart is None:
1✔
1828
                rstart = len(self.nodes) - 1
1✔
1829
            if rstop is None:
1✔
1830
                rstop = 0
1✔
1831
            rstop -= 1
1✔
1832
        else:  # Forwards
1833
            if rstart is None:
1✔
1834
                rstart = 0
1✔
1835
            if rstop is None:
1✔
1836
                rstop = len(self.nodes) - 1
1✔
1837
            rstop += 1
1✔
1838
        buffer = []
1✔
1839
        allowed_indices = range(rstart, rstop, rstep)
1✔
1840
        for i, item in enumerate(self):
1✔
1841
            if i in allowed_indices:
1✔
1842
                buffer.append(item)
1✔
1843
        ret = ListNode(f"{self.name}_slice")
1✔
1844
        if rstep < 0:
1✔
1845
            buffer.reverse()
1✔
1846
        for val in buffer:
1✔
1847
            ret.append(val)
1✔
1848
        return ret
1✔
1849

1850
    def remove(self, obj):
1✔
1851
        """Removes the given object from this list.
1852

1853
        Parameters
1854
        ----------
1855
        obj : ValueNode
1856
            the object to remove.
1857
        """
1858
        self.nodes.remove(obj)
1✔
1859

1860
    def __eq__(self, other):
1✔
1861
        if not isinstance(other, (type(self), list)):
1✔
1862
            return False
1✔
1863
        if len(self) != len(other):
1✔
1864
            return False
1✔
1865
        for lhs, rhs in zip(self, other):
1✔
1866
            if lhs != rhs:
1✔
1867
                return False
1✔
1868
        return True
1✔
1869

1870

1871
class MaterialsNode(SyntaxNodeBase):
1✔
1872
    """A node for representing isotopes and their concentration,
1873
    and the material parameters.
1874

1875
    This stores a list of tuples of ZAIDs and concentrations,
1876
    or a tuple of a parameter.
1877

1878
    .. versionadded:: 1.0.0
1879

1880
        This was added as a more general version of ``IsotopesNodes``.
1881

1882
    Parameters
1883
    ----------
1884
    name : str
1885
        a name for labeling this node.
1886
    """
1887

1888
    def __init__(self, name):
1✔
1889
        super().__init__(name)
1✔
1890

1891
    def append_nuclide(self, isotope_fraction):
1✔
1892
        """Append the isotope fraction to this node.
1893

1894
        .. versionadded:: 1.0.0
1895

1896
            Added to replace ``append``
1897

1898
        Parameters
1899
        ----------
1900
        isotope_fraction : tuple
1901
            the isotope_fraction to add. This must be a tuple from A
1902
            Yacc production. This will consist of: the string
1903
            identifying the Yacc production, a ValueNode that is the
1904
            ZAID, and a ValueNode of the concentration.
1905
        """
1906
        isotope, concentration = isotope_fraction[1:3]
1✔
1907
        self._nodes.append((isotope, concentration))
1✔
1908

1909
    def append(self):  # pragma: no cover
1910
        raise DeprecationWarning("Deprecated. Use append_param or append_nuclide")
1911

1912
    def append_param(self, param):
1✔
1913
        """Append the parameter to this node.
1914

1915
        .. versionadded:: 1.0.0
1916

1917
            Added to replace ``append``
1918

1919
        Parameters
1920
        ----------
1921
        param : ParametersNode
1922
            the parameter to add to this node.
1923
        """
1924
        self._nodes.append((param,))
1✔
1925

1926
    def format(self):
1✔
1927
        ret = ""
1✔
1928
        for node in it.chain(*self.nodes):
1✔
1929
            ret += node.format()
1✔
1930
        return ret
1✔
1931

1932
    def __repr__(self):
1✔
1933
        return f"(Materials: {self.nodes})"
1✔
1934

1935
    def _pretty_str(self):
1✔
1936
        INDENT = 2
1✔
1937
        ret = f"<Node: {self.name}: [\n"
1✔
1938
        for val in self.nodes:
1✔
1939
            child_strs = [f"({', '.join([str(v) for v in val])})"]
1✔
1940
            ret += "\n".join([" " * 2 * INDENT + s for s in child_strs[:-1]])
1✔
1941
            ret += " " * 2 * INDENT + child_strs[-1] + ",\n"
1✔
1942
        ret += " " * INDENT + "]\n"
1✔
1943
        ret += ">"
1✔
1944
        return ret
1✔
1945

1946
    def __iter__(self):
1✔
1947
        return iter(self.nodes)
1✔
1948

1949
    @property
1✔
1950
    def comments(self):
1✔
1951
        for node in self.nodes:
1✔
1952
            for value in node:
1✔
1953
                yield from value.comments
1✔
1954

1955
    def get_trailing_comment(self):
1✔
1956
        tail = self.nodes[-1]
1✔
1957
        tail = tail[-1]
1✔
1958
        return tail.get_trailing_comment()
1✔
1959

1960
    def _delete_trailing_comment(self):
1✔
1961
        tail = self.nodes[-1]
1✔
1962
        tail = tail[-1]
1✔
1963
        tail._delete_trailing_comment()
1✔
1964

1965
    def flatten(self):
1✔
1966
        ret = []
1✔
1967
        for node_group in self.nodes:
1✔
1968
            ret += node_group
1✔
1969
        return ret
1✔
1970

1971

1972
class ShortcutNode(ListNode):
1✔
1973
    """A node that pretends to be a :class:`ListNode` but is actually representing a shortcut.
1974

1975
    This takes the shortcut tokens, and expands it into their "virtual" values.
1976

1977
    Parameters
1978
    ----------
1979
    p : sly.yacc.YaccProduction
1980
        the parsing object to parse.
1981
    short_type : Shortcuts
1982
        the type of the shortcut.
1983
    """
1984

1985
    _shortcut_names = {
1✔
1986
        ("REPEAT", "NUM_REPEAT"): Shortcuts.REPEAT,
1987
        ("JUMP", "NUM_JUMP"): Shortcuts.JUMP,
1988
        ("INTERPOLATE", "NUM_INTERPOLATE"): Shortcuts.INTERPOLATE,
1989
        ("LOG_INTERPOLATE", "NUM_LOG_INTERPOLATE"): Shortcuts.LOG_INTERPOLATE,
1990
        ("MULTIPLY", "NUM_MULTIPLY"): Shortcuts.MULTIPLY,
1991
    }
1992
    _num_finder = re.compile(r"\d+")
1✔
1993

1994
    def __init__(self, p=None, short_type=None, data_type=float):
1✔
1995
        self._type = None
1✔
1996
        self._end_pad = None
1✔
1997
        self._nodes = collections.deque()
1✔
1998
        self._original = []
1✔
1999
        self._full = False
1✔
2000
        self._num_node = ValueNode(None, float, never_pad=True)
1✔
2001
        self._data_type = data_type
1✔
2002
        if p is not None:
1✔
2003
            for search_strs, shortcut in self._shortcut_names.items():
1✔
2004
                for search_str in search_strs:
1✔
2005
                    if hasattr(p, search_str):
1✔
2006
                        super().__init__(search_str.lower())
1✔
2007
                        self._type = shortcut
1✔
2008
            if self._type is None:
1✔
2009
                raise ValueError("must use a valid shortcut")
1✔
2010
            self._original = list(p)
1✔
2011
            if self._type == Shortcuts.REPEAT:
1✔
2012
                self._expand_repeat(p)
1✔
2013
            elif self._type == Shortcuts.MULTIPLY:
1✔
2014
                self._expand_multiply(p)
1✔
2015
            elif self._type == Shortcuts.JUMP:
1✔
2016
                self._expand_jump(p)
1✔
2017
            elif self._type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
1✔
2018
                self._expand_interpolate(p)
1✔
2019
        elif short_type is not None:
1✔
2020
            if not isinstance(short_type, Shortcuts):
1✔
2021
                raise TypeError(f"Shortcut type must be Shortcuts. {short_type} given.")
1✔
2022
            self._type = short_type
1✔
2023
            if self._type in {
1✔
2024
                Shortcuts.INTERPOLATE,
2025
                Shortcuts.LOG_INTERPOLATE,
2026
                Shortcuts.JUMP,
2027
                Shortcuts.JUMP,
2028
            }:
2029
                self._num_node = ValueNode(None, int, never_pad=True)
1✔
2030
            self._end_pad = PaddingNode(" ")
1✔
2031

2032
    def load_nodes(self, nodes):
1✔
2033
        """Loads the given nodes into this shortcut, and update needed information.
2034

2035
        For interpolate nodes should start and end with the beginning/end of
2036
        the interpolation.
2037

2038
        Parameters
2039
        ----------
2040
        nodes : list
2041
            the nodes to be loaded.
2042
        """
2043
        self._nodes = collections.deque(nodes)
1✔
2044
        if self.type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
1✔
2045
            self._begin = nodes[0].value
1✔
2046
            self._end = nodes[-1].value
1✔
2047
            if self.type == Shortcuts.LOG_INTERPOLATE:
1✔
2048
                self._begin = math.log10(self._begin)
1✔
2049
                self._end = math.log10(self._end)
1✔
2050
            self._spacing = (self._end - self._begin) / (len(nodes) - 1)
1✔
2051

2052
    @property
1✔
2053
    def end_padding(self):
1✔
2054
        """The padding at the end of this shortcut.
2055

2056
        Returns
2057
        -------
2058
        PaddingNode
2059
        """
2060
        return self._end_pad
1✔
2061

2062
    @end_padding.setter
1✔
2063
    def end_padding(self, padding):
1✔
2064
        if not isinstance(padding, PaddingNode):
1✔
2065
            raise TypeError(
1✔
2066
                f"End padding must be of type PaddingNode. {padding} given."
2067
            )
2068
        self._end_pad = padding
1✔
2069

2070
    @property
1✔
2071
    def type(self):
1✔
2072
        """The Type of shortcut this ShortcutNode represents.
2073

2074
        Returns
2075
        -------
2076
        Shortcuts
2077
        """
2078
        return self._type
1✔
2079

2080
    def __repr__(self):
1✔
2081
        return f"(shortcut:{self._type}: {self.nodes})"
1✔
2082

2083
    def _get_last_node(self, p):
1✔
2084
        last = p[0]
1✔
2085
        if isinstance(last, ValueNode):
1✔
2086
            return collections.deque([last])
1✔
2087
        return collections.deque()
1✔
2088

2089
    def _expand_repeat(self, p):
1✔
2090
        self._nodes = self._get_last_node(p)
1✔
2091
        repeat = p[1]
1✔
2092
        try:
1✔
2093
            repeat_num_str = repeat.lower().replace("r", "")
1✔
2094
            repeat_num = int(repeat_num_str)
1✔
2095
            self._num_node = ValueNode(repeat_num, int, never_pad=True)
1✔
2096
        except ValueError:
1✔
2097
            repeat_num = 1
1✔
2098
            self._num_node = ValueNode(None, int, never_pad=True)
1✔
2099
        if isinstance(p[0], ValueNode):
1✔
2100
            last_val = p[0]
1✔
2101
        else:
2102
            if isinstance(p[0], GeometryTree):
1✔
2103
                last_val = list(p[0])[-1]
1✔
2104
            else:
2105
                last_val = p[0].nodes[-1]
1✔
2106
        if last_val.value is None:
1✔
2107
            raise ValueError(f"Repeat cannot follow a jump. Given: {list(p)}")
1✔
2108
        self._nodes += [copy.deepcopy(last_val) for i in range(repeat_num)]
1✔
2109

2110
    def _expand_multiply(self, p):
1✔
2111
        self._nodes = self._get_last_node(p)
1✔
2112
        mult_str = p[1].lower().replace("m", "")
1✔
2113
        mult_val = fortran_float(mult_str)
1✔
2114
        self._num_node = ValueNode(mult_str, float, never_pad=True)
1✔
2115
        if isinstance(p[0], ValueNode):
1✔
2116
            last_val = self.nodes[-1]
1✔
2117
        elif isinstance(p[0], GeometryTree):
1✔
2118
            if "right" in p[0].nodes:
1✔
2119
                last_val = p[0].nodes["right"]
1✔
2120
            else:
2121
                last_val = p[0].nodes["left"]
×
2122
        else:
2123
            last_val = p[0].nodes[-1]
1✔
2124
        if last_val.value is None:
1✔
2125
            raise ValueError(f"Multiply cannot follow a jump. Given: {list(p)}")
1✔
2126
        self._nodes.append(copy.deepcopy(last_val))
1✔
2127
        self.nodes[-1].value *= mult_val
1✔
2128

2129
    def _expand_jump(self, p):
1✔
2130
        try:
1✔
2131
            jump_str = p[0].lower().replace("j", "")
1✔
2132
            jump_num = int(jump_str)
1✔
2133
            self._num_node = ValueNode(jump_str, int, never_pad=True)
1✔
2134
        except ValueError:
1✔
2135
            jump_num = 1
1✔
2136
            self._num_node = ValueNode(None, int, never_pad=True)
1✔
2137
        for i in range(jump_num):
1✔
2138
            self._nodes.append(ValueNode(input_parser.mcnp_input.Jump(), float))
1✔
2139

2140
    def _expand_interpolate(self, p):
1✔
2141
        if self._type == Shortcuts.LOG_INTERPOLATE:
1✔
2142
            is_log = True
1✔
2143
        else:
2144
            is_log = False
1✔
2145
        if hasattr(p, "geometry_term"):
1✔
2146
            term = p.geometry_term
1✔
2147
            if isinstance(term, GeometryTree):
1✔
2148
                begin = list(term)[-1].value
1✔
2149
            else:
2150
                begin = term.value
1✔
2151
            end = p.number_phrase.value
1✔
2152
        else:
2153
            if isinstance(p[0], ListNode):
1✔
2154
                begin = p[0].nodes[-1].value
1✔
2155
            else:
2156
                begin = p[0].value
1✔
2157
            end = p.number_phrase.value
1✔
2158
        self._nodes = self._get_last_node(p)
1✔
2159
        if begin is None:
1✔
2160
            raise ValueError(f"Interpolates cannot follow a jump. Given: {list(p)}")
1✔
2161
        match = self._num_finder.search(p[1])
1✔
2162
        if match:
1✔
2163
            number = int(match.group(0))
1✔
2164
            self._num_node = ValueNode(match.group(0), int, never_pad=True)
1✔
2165
        else:
2166
            number = 1
1✔
2167
            self._num_node = ValueNode(None, int, never_pad=True)
1✔
2168
        if is_log:
1✔
2169
            begin = math.log(begin, 10)
1✔
2170
            end = math.log(end, 10)
1✔
2171
        spacing = (end - begin) / (number + 1)
1✔
2172
        for i in range(number):
1✔
2173
            if is_log:
1✔
2174
                new_val = 10 ** (begin + spacing * (i + 1))
1✔
2175
            else:
2176
                new_val = begin + spacing * (i + 1)
1✔
2177
            self.append(
1✔
2178
                ValueNode(
2179
                    str(self._data_type(new_val)), self._data_type, never_pad=True
2180
                )
2181
            )
2182
        self._begin = begin
1✔
2183
        self._end = end
1✔
2184
        self._spacing = spacing
1✔
2185
        self.append(p.number_phrase)
1✔
2186

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

2190
        Parameters
2191
        ----------
2192
        node : ValueNode
2193
            the node to consume
2194
        direction : int
2195
            the direct to go in. Must be in {-1, 1}
2196
        last_edge_shortcut : bool
2197
            Whether the previous node in the list was part of a
2198
            different shortcut
2199

2200
        Returns
2201
        -------
2202
        bool
2203
            true it can be consumed.
2204
        """
2205
        if self._type == Shortcuts.JUMP:
1✔
2206
            if node.value is None:
1✔
2207
                return True
1✔
2208

2209
        # REPEAT
2210
        elif self._type == Shortcuts.REPEAT:
1✔
2211
            if len(self.nodes) == 0 and node.value is not None:
1✔
2212
                return True
1✔
2213
            if direction == 1:
1✔
2214
                edge = self.nodes[-1]
1✔
2215
            else:
2216
                edge = self.nodes[0]
1✔
2217
            if edge.type != node.type or edge.value is None or node.value is None:
1✔
2218
                return False
1✔
2219
            if edge.type in {int, float} and math.isclose(
1✔
2220
                edge.value, node.value, rel_tol=rel_tol, abs_tol=abs_tol
2221
            ):
2222
                return True
1✔
2223
            elif edge.value == node.value:
1✔
2224
                return True
×
2225

2226
        # INTERPOLATE
2227
        elif self._type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
1✔
2228
            return self._is_valid_interpolate_edge(node, direction)
1✔
2229
        # Multiply can only ever have 1 value
2230
        elif self._type == Shortcuts.MULTIPLY:
1✔
2231
            # can't do a multiply with a Jump
2232
            if node.value is None:
1✔
2233
                return False
1✔
2234
            if len(self.nodes) == 0:
1✔
2235
                # clear out old state if needed
2236
                self._full = False
1✔
2237
                if last_edge_shortcut:
1✔
2238
                    self._full = True
1✔
2239
                return True
1✔
2240
            if len(self.nodes) == 1 and not self._full:
1✔
2241
                return True
1✔
2242
        return False
1✔
2243

2244
    def _is_valid_interpolate_edge(self, node, direction):
1✔
2245
        """Is a valid interpolation edge.
2246

2247
        Parameters
2248
        ----------
2249
        node : ValueNode
2250
            the node to consume
2251
        direction : int
2252
            the direct to go in. Must be in {-1, 1}
2253

2254
        Returns
2255
        -------
2256
        bool
2257
            true it can be consumed.
2258
        """
2259
        # kill jumps immediately
2260
        if node.value is None:
1✔
2261
            return False
1✔
2262
        if len(self.nodes) == 0:
1✔
2263
            new_val = self._begin if direction == 1 else self._end
1✔
2264
            if self._type == Shortcuts.LOG_INTERPOLATE:
1✔
2265
                new_val = 10**new_val
1✔
2266
        else:
2267
            edge = self.nodes[-1] if direction == 1 else self.nodes[0]
1✔
2268
            edge = edge.value
1✔
2269
            if self._type == Shortcuts.LOG_INTERPOLATE:
1✔
2270
                edge = math.log(edge, 10)
1✔
2271
                new_val = 10 ** (edge + direction * self._spacing)
1✔
2272
            else:
2273
                new_val = edge + direction * self._spacing
1✔
2274
        return math.isclose(new_val, node.value, rel_tol=rel_tol, abs_tol=abs_tol)
1✔
2275

2276
    def consume_edge_node(self, node, direction, last_edge_shortcut=False):
1✔
2277
        """Tries to consume the given edge.
2278

2279
        If it can be consumed the node is appended to the internal nodes.
2280

2281
        Parameters
2282
        ----------
2283
        node : ValueNode
2284
            the node to consume
2285
        direction : int
2286
            the direct to go in. Must be in {-1, 1}
2287
        last_edge_shortcut : bool
2288
            Whether or the previous node in the list was part of a
2289
            different shortcut
2290

2291
        Returns
2292
        -------
2293
        bool
2294
            True if the node was consumed.
2295
        """
2296
        if self._can_consume_node(node, direction, last_edge_shortcut):
1✔
2297
            if direction == 1:
1✔
2298
                self._nodes.append(node)
1✔
2299
            else:
2300
                self._nodes.appendleft(node)
1✔
2301
            return True
1✔
2302
        return False
1✔
2303

2304
    def format(self, leading_node=None):
1✔
2305
        if self._type == Shortcuts.JUMP:
1✔
2306
            temp = self._format_jump()
1✔
2307
        # repeat
2308
        elif self._type == Shortcuts.REPEAT:
1✔
2309
            temp = self._format_repeat(leading_node)
1✔
2310
        # multiply
2311
        elif self._type == Shortcuts.MULTIPLY:
1✔
2312
            temp = self._format_multiply(leading_node)
1✔
2313
        # interpolate
2314
        elif self._type in {Shortcuts.INTERPOLATE, Shortcuts.LOG_INTERPOLATE}:
1✔
2315
            temp = self._format_interpolate(leading_node)
1✔
2316
        if self.end_padding:
1✔
2317
            pad_str = self.end_padding.format()
1✔
2318
        else:
2319
            pad_str = ""
1✔
2320
        return f"{temp}{pad_str}"
1✔
2321

2322
    def _format_jump(self):
1✔
2323
        num_jumps = len(self.nodes)
1✔
2324
        if num_jumps == 0:
1✔
2325
            return ""
×
2326
        if len(self._original) > 0 and "j" in self._original[0]:
1✔
2327
            j = "j"
1✔
2328
        else:
2329
            j = "J"
1✔
2330
        length = len(self._original)
1✔
2331
        self._num_node.value = num_jumps
1✔
2332
        if num_jumps == 1 and (
1✔
2333
            length == 0 or (length > 0 and "1" not in self._original[0])
2334
        ):
2335
            num_jumps = ""
1✔
2336
        else:
2337
            num_jumps = self._num_node
1✔
2338

2339
        return f"{num_jumps.format()}{j}"
1✔
2340

2341
    def _can_use_last_node(self, node, start=None):
1✔
2342
        """Determine if the previous node can be used as the start to this node
2343
        (and therefore skip the start of this one).
2344

2345
        Last node can be used if
2346
        - it's a basic ValueNode that matches this repeat
2347
        - it's also a shortcut, with the same edge values.
2348

2349
        Parameters
2350
        ----------
2351
        node : ValueNode, ShortcutNode
2352
            the previous node to test.
2353
        start : float
2354
            the starting value for this node (specifically for
2355
            interpolation)
2356

2357
        Returns
2358
        -------
2359
        bool
2360
            True if the node given can be used.
2361
        """
2362
        if isinstance(node, ValueNode):
1✔
2363
            value = node.value
×
2364
        elif isinstance(node, ShortcutNode):
1✔
2365
            value = node.nodes[-1].value
1✔
2366
        else:
2367
            return False
1✔
2368
        if value is None:
1✔
2369
            return False
1✔
2370
        if start is None:
1✔
2371
            start = self.nodes[0].value
1✔
2372
        return math.isclose(start, value)
1✔
2373

2374
    def _format_repeat(self, leading_node=None):
1✔
2375

2376
        if self._can_use_last_node(leading_node):
1✔
2377
            first_val = ""
1✔
2378
            num_extra = 0
1✔
2379
        else:
2380
            first_val = self.nodes[0].format()
1✔
2381
            num_extra = 1
1✔
2382
        num_repeats = len(self.nodes) - num_extra
1✔
2383
        self._num_node.value = num_repeats
1✔
2384
        if len(self._original) >= 2 and "r" in self._original[1]:
1✔
2385
            r = "r"
1✔
2386
        else:
2387
            r = "R"
1✔
2388
        if (
1✔
2389
            num_repeats == 1
2390
            and len(self._original) >= 2
2391
            and "1" not in self._original[1]
2392
        ):
2393
            num_repeats = ""
1✔
2394
        else:
2395
            num_repeats = self._num_node
1✔
2396
        return f"{first_val}{num_repeats.format()}{r}"
1✔
2397

2398
    def _format_multiply(self, leading_node=None):
1✔
2399
        # Multiply doesn't usually consume other nodes
2400
        if leading_node is not None and len(self) == 1:
1✔
2401
            first_val = leading_node.nodes[-1]
1✔
2402
            first_val_str = ""
1✔
2403
        else:
2404
            first_val = self.nodes[0]
1✔
2405
            first_val_str = first_val
1✔
2406
        if self._original and "m" in self._original[-1]:
1✔
2407
            m = "m"
1✔
2408
        else:
2409
            m = "M"
1✔
2410
        self._num_node.value = self.nodes[-1].value / first_val.value
1✔
2411
        return f"{first_val_str.format()}{self._num_node.format()}{m}"
1✔
2412

2413
    def _format_interpolate(self, leading_node=None):
1✔
2414
        begin = self._begin
1✔
2415
        if self.type == Shortcuts.LOG_INTERPOLATE:
1✔
2416
            begin = 10**begin
1✔
2417
        if self._can_use_last_node(leading_node, begin):
1✔
2418
            start = ""
1✔
2419
            num_extra_nodes = 1
1✔
2420
            if hasattr(self, "_has_pseudo_start"):
1✔
2421
                num_extra_nodes += 1
1✔
2422
        else:
2423
            start = self.nodes[0]
1✔
2424
            num_extra_nodes = 2
1✔
2425
        end = self.nodes[-1]
1✔
2426
        num_interp = len(self.nodes) - num_extra_nodes
1✔
2427
        self._num_node.value = num_interp
1✔
2428
        interp = "I"
1✔
2429
        can_match = False
1✔
2430
        if len(self._original) > 0:
1✔
2431
            if match := re.match(r"\d*(\w+)", self._original[1]):
1✔
2432
                can_match = True
1✔
2433
                interp = match.group(1)
1✔
2434
        if not can_match:
1✔
2435
            if self._type == Shortcuts.LOG_INTERPOLATE:
1✔
2436
                interp = "ILOG"
1✔
2437
        if (
1✔
2438
            num_interp == 1
2439
            and len(self._original) >= 2
2440
            and "1" not in self._original[1]
2441
        ):
2442
            num_interp = ""
1✔
2443
        else:
2444
            num_interp = self._num_node
1✔
2445
        if len(self._original) >= 3:
1✔
2446
            padding = self._original[2]
1✔
2447
        else:
2448
            padding = PaddingNode(" ")
1✔
2449
        return f"{start.format()}{num_interp.format()}{interp}{padding.format()}{end.format()}"
1✔
2450

2451

2452
class ClassifierNode(SyntaxNodeBase):
1✔
2453
    """A node to represent the classifier for a :class:`montepy.data_input.DataInput`
2454

2455
    e.g., represents ``M4``, ``F104:n,p``, ``IMP:n,e``.
2456
    """
2457

2458
    def __init__(self):
1✔
2459
        super().__init__("classifier")
1✔
2460
        self._prefix = None
1✔
2461
        self._number = None
1✔
2462
        self._particles = None
1✔
2463
        self._modifier = None
1✔
2464
        self._padding = None
1✔
2465
        self._nodes = []
1✔
2466

2467
    @property
1✔
2468
    def prefix(self):
1✔
2469
        """The prefix for the classifier.
2470

2471
        That is the string that tells what type of input this is.
2472

2473
        E.g.: ``M`` in ``M4`` or ``IMP`` in ``IMP:n``.
2474

2475
        Returns
2476
        -------
2477
        ValueNode
2478
            the prefix
2479
        """
2480
        return self._prefix
1✔
2481

2482
    @prefix.setter
1✔
2483
    def prefix(self, pref):
1✔
2484
        self.append(pref)
1✔
2485
        self._prefix = pref
1✔
2486

2487
    @property
1✔
2488
    def number(self):
1✔
2489
        """The number if any for the classifier.
2490

2491
        Returns
2492
        -------
2493
        ValueNode
2494
            the number holder for this classifier.
2495
        """
2496
        return self._number
1✔
2497

2498
    @number.setter
1✔
2499
    def number(self, number):
1✔
2500
        self.append(number)
1✔
2501
        self._number = number
1✔
2502

2503
    @property
1✔
2504
    def particles(self):
1✔
2505
        """The particles if any tied to this classifier.
2506

2507
        Returns
2508
        -------
2509
        ParticleNode
2510
            the particles used.
2511
        """
2512
        return self._particles
1✔
2513

2514
    @particles.setter
1✔
2515
    def particles(self, part):
1✔
2516
        self.append(part)
1✔
2517
        self._particles = part
1✔
2518

2519
    @property
1✔
2520
    def modifier(self):
1✔
2521
        """The modifier for this classifier if any.
2522

2523
        A modifier is a prefix character that changes the inputs behavior,
2524
        e.g.: ``*`` or ``+``.
2525

2526
        Returns
2527
        -------
2528
        ValueNode
2529
            the modifier
2530
        """
2531
        return self._modifier
1✔
2532

2533
    @modifier.setter
1✔
2534
    def modifier(self, mod):
1✔
2535
        self.append(mod)
1✔
2536
        self._modifier = mod
1✔
2537

2538
    @property
1✔
2539
    def padding(self):
1✔
2540
        """The padding for this classifier.
2541

2542
        .. Note::
2543
            None of the ValueNodes in this object should have padding.
2544

2545
        Returns
2546
        -------
2547
        PaddingNode
2548
            the padding after the classifier.
2549
        """
2550
        return self._padding
1✔
2551

2552
    @padding.setter
1✔
2553
    def padding(self, val):
1✔
2554
        self.append(val)
1✔
2555
        self._padding = val
1✔
2556

2557
    def format(self):
1✔
2558
        if self.modifier:
1✔
2559
            ret = self.modifier.format()
1✔
2560
        else:
2561
            ret = ""
1✔
2562
        ret += self.prefix.format()
1✔
2563
        if self.number:
1✔
2564
            ret += self.number.format()
1✔
2565
        if self.particles:
1✔
2566
            ret += self.particles.format()
1✔
2567
        if self.padding:
1✔
2568
            ret += self.padding.format()
1✔
2569
        return ret
1✔
2570

2571
    def __str__(self):
1✔
2572
        return self.format()
1✔
2573

2574
    def __repr__(self):
1✔
2575
        return (
1✔
2576
            f"(Classifier: mod: {self.modifier}, prefix: {self.prefix}, "
2577
            f"number: {self.number}, particles: {self.particles},"
2578
            f" padding: {self.padding})"
2579
        )
2580

2581
    def _pretty_str(self):
1✔
2582
        return f"""<Classifier: {{ 
1✔
2583
    mod: {self.modifier}, 
2584
    prefix: {self.prefix}, 
2585
    number: {self.number}, 
2586
    particles: {self.particles},
2587
    padding: {self.padding} 
2588
  }}
2589
>
2590
"""
2591

2592
    @property
1✔
2593
    def comments(self):
1✔
2594
        if self.padding is not None:
1✔
2595
            yield from self.padding.comments
1✔
2596
        else:
2597
            yield from []
1✔
2598

2599
    def get_trailing_comment(self):
1✔
2600
        if self.padding:
1✔
2601
            return self.padding.get_trailing_comment()
1✔
2602

2603
    def _delete_trailing_comment(self):
1✔
2604
        if self.padding:
1✔
2605
            self.padding._delete_trailing_comment()
1✔
2606

2607
    def flatten(self):
1✔
2608
        ret = []
1✔
2609
        if self.modifier:
1✔
2610
            ret.append(self.modifier)
1✔
2611
        ret.append(self.prefix)
1✔
2612
        if self.number:
1✔
2613
            ret.append(self.number)
1✔
2614
        if self.particles:
1✔
2615
            ret.append(self.particles)
1✔
2616
        if self.padding:
1✔
2617
            ret.append(self.padding)
1✔
2618
        return ret
1✔
2619

2620

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

2624
    This behaves like a dictionary and is accessible by their key*
2625

2626
    .. Note::
2627
        How to access values.
2628

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

2634
        So to access a cell's fill information you would run:
2635

2636
        .. code-block:: python
2637

2638
            parameters["fill"]
2639

2640
        And to access the n,p importance:
2641

2642
        .. code-block:: python
2643

2644
            parameters["imp:n,p"]
2645
    """
2646

2647
    def __init__(self):
1✔
2648
        super().__init__("parameters")
1✔
2649
        self._nodes = {}
1✔
2650

2651
    def append(self, val, is_default=False):
1✔
2652
        """Append the node to this node.
2653

2654
        This takes a syntax node, which requires the keys:
2655
            ``["classifier", "seperator", "data"]``
2656

2657
        Parameters
2658
        ----------
2659
        val : SyntaxNode
2660
            the parameter to append.
2661
        is_default : bool
2662
            whether this parameter was added as a default tree not from
2663
            the user.
2664
        """
2665
        classifier = val["classifier"]
1✔
2666
        key = (
1✔
2667
            classifier.prefix.value
2668
            + (str(classifier.particles) if classifier.particles else "")
2669
        ).lower()
2670
        if key in self._nodes:
1✔
2671
            raise RedundantParameterSpecification(key, val)
1✔
2672
        if is_default:
1✔
2673
            val._is_default = True
1✔
2674
        self._nodes[key] = val
1✔
2675

2676
    def __str__(self):
1✔
2677
        return f"<Parameters, {self.nodes}>"
1✔
2678

2679
    def __repr__(self):
1✔
2680
        return str(self)
1✔
2681

2682
    def _pretty_str(self):
1✔
2683
        INDENT = 2
1✔
2684
        ret = f"<Node: {self.name}: {{\n"
1✔
2685
        for key, val in self.nodes.items():
1✔
2686
            child_strs = val._pretty_str().split("\n")
1✔
2687
            ret += " " * INDENT + f"{key}: {child_strs[0]}\n"
1✔
2688
            ret += "\n".join([" " * 2 * INDENT + s for s in child_strs[1:-1]])
1✔
2689
            ret += " " * 2 * INDENT + child_strs[-1] + ",\n"
1✔
2690
        ret += " " * INDENT + "}\n"
1✔
2691
        ret += ">"
1✔
2692
        return ret
1✔
2693

2694
    def __getitem__(self, key):
1✔
2695
        return self.nodes[key.lower()]
1✔
2696

2697
    def __contains__(self, key):
1✔
2698
        return key.lower() in self.nodes
1✔
2699

2700
    def format(self):
1✔
2701
        ret = ""
1✔
2702
        for node in self.nodes.values():
1✔
2703
            ret += node.format()
1✔
2704
        return ret
1✔
2705

2706
    def get_trailing_comment(self):
1✔
2707
        for node in reversed(self.nodes.values()):
1✔
2708
            if hasattr(node, "_is_default"):
1✔
2709
                continue
1✔
2710
            return node.get_trailing_comment()
1✔
2711

2712
    def _delete_trailing_comment(self):
1✔
2713
        for node in reversed(self.nodes.values()):
1✔
2714
            if hasattr(node, "_is_default"):
1✔
2715
                continue
1✔
2716
            node._delete_trailing_comment()
1✔
2717

2718
    @property
1✔
2719
    def comments(self):
1✔
2720
        for node in self.nodes.values():
1✔
2721
            if isinstance(node, SyntaxNodeBase):
1✔
2722
                yield from node.comments
1✔
2723

2724
    def flatten(self):
1✔
2725
        ret = []
1✔
2726
        for node in self.nodes.values():
1✔
2727
            if isinstance(node, (ValueNode, PaddingNode)):
1✔
2728
                ret.append(node)
×
2729
            else:
2730
                ret += node.flatten()
1✔
2731
        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