Repository: idaholab/MontePy

Issue:
Silent parsing error with sdef
**Describe the bug**

MontePy silently misparses an `SDEF` input leading to a silently broken input file.

**To Reproduce**

A short code snippet of what you have ran. Please change or remove any specific values or anything that can't be public. For example:

``` python
problem = montepy.read_input("foo.imcnp")
print(problem.data_inputs[-1]._tree.format())
print(problem.data_inputs[-1]._tree)
```
This leads to:

``` python
d5
(Node: data: {'start_pad': (Padding, []), 'classifier': (Classifier: mod: None, prefix: (Value, d, padding: None), number: (Value, 5, padding: None), particles: None), 'keyword': (Value, None, padding: None), 'data': (list: empty data, [])})
```

**MCNP input file snippet**


```
Title
1 1 20
         -1000  $ dollar comment
        imp:n,p=1 U=350 trcl=5

C surfaces
1000 SO 1

C data
m1 1001.80c 1.0
sdef  cel=d1 erg=d2 pos=fcel d3 ext=fcel d4 axs=0 0 1
     rad=d5
```


**Version**

 - Version 0.2.5



Discussion:

Patch:
diff --git a/doc/source/api/montepy.data_inputs.rst b/doc/source/api/montepy.data_inputs.rst
index 18fffadd..84cd93fd 100644
--- a/doc/source/api/montepy.data_inputs.rst
+++ b/doc/source/api/montepy.data_inputs.rst
@@ -25,8 +25,6 @@ montepy.data\_inputs package
    montepy.data_inputs.material_component
    montepy.data_inputs.mode
    montepy.data_inputs.thermal_scattering
-   montepy.data_inputs.tally
-   montepy.data_inputs.tally_multiplier
    montepy.data_inputs.transform
    montepy.data_inputs.universe_input
    montepy.data_inputs.volume
diff --git a/doc/source/api/montepy.data_inputs.tally.rst b/doc/source/api/montepy.data_inputs.tally.rst
deleted file mode 100644
index 0e873160..00000000
--- a/doc/source/api/montepy.data_inputs.tally.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-montepy.data\_inputs.tally module
-=================================
-
-
-.. automodule:: montepy.data_inputs.tally
-   :members:
-   :inherited-members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/doc/source/api/montepy.data_inputs.tally_multiplier.rst b/doc/source/api/montepy.data_inputs.tally_multiplier.rst
deleted file mode 100644
index 68785c7f..00000000
--- a/doc/source/api/montepy.data_inputs.tally_multiplier.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-montepy.data\_inputs.tally_multiplier module
-============================================
-
-
-.. automodule:: montepy.data_inputs.tally_multiplier
-   :members:
-   :inherited-members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst
index afad9507..a1803665 100644
--- a/doc/source/changelog.rst
+++ b/doc/source/changelog.rst
@@ -2,12 +2,17 @@ MontePy Changelog
 =================
 
 #Next Version#
----------------------
+-------------------
 
 **Features Added**
 
 * ``overwrite`` argument added to `MCNP_Problem.write_to_file` to ensure files are only overwritten if the user really wants to do so (:pull:`443`).
 
+**Bug fixes**
+
+* Fixed bug with ``SDEF`` input, and made parser more robust (:issue:`396`).
+
+
 0.2.10
 ----------------------
 
diff --git a/doc/source/developing.rst b/doc/source/developing.rst
index 459738b9..217a032c 100644
--- a/doc/source/developing.rst
+++ b/doc/source/developing.rst
@@ -126,7 +126,7 @@ For a deployment you need to:
 #. Run the deploy script : ``.github/scripts/deploy.sh``
 #. Manually merge onto main without creating a new commit. 
    This is necessary because there's no way to do a github PR that will not create a new commit, which will break setuptools_scm.
-
+#. Update the release notes on the draft release, and finalize it on GitHub.
 
 Package Structure
 -----------------
diff --git a/montepy/data_inputs/cell_modifier.py b/montepy/data_inputs/cell_modifier.py
index 87330e02..e95a1935 100644
--- a/montepy/data_inputs/cell_modifier.py
+++ b/montepy/data_inputs/cell_modifier.py
@@ -255,10 +255,10 @@ class CellModifierInput(DataInputAbstract):
         C = is_worth_pring
 
         Logic:
-            A!BC + !ABC
-            C *(A!B + !AB)
-            C * (A xor B)
-            C * (A != B)
+            1. A!BC + !ABC
+            2. C *(A!B + !AB)
+            3. C * (A xor B)
+            4. C * (A != B)
         """
         # print in either block
         if (self.in_cell_block != print_in_data_block) and self._is_worth_printing:
diff --git a/montepy/data_inputs/data_input.py b/montepy/data_inputs/data_input.py
index 732e193e..80b0fd59 100644
--- a/montepy/data_inputs/data_input.py
+++ b/montepy/data_inputs/data_input.py
@@ -4,7 +4,11 @@ import copy
 
 import montepy
 from montepy.errors import *
-from montepy.input_parser.data_parser import ClassifierParser, DataParser
+from montepy.input_parser.data_parser import (
+    ClassifierParser,
+    DataParser,
+    ParamOnlyDataParser,
+)
 from montepy.input_parser.mcnp_input import Input
 from montepy.particle import Particle
 from montepy.mcnp_object import MCNP_Object
@@ -322,8 +326,17 @@ class DataInput(DataInputAbstract):
 
     :param input: the Input object representing this data input
     :type input: Input
+    :param fast_parse: Whether or not to only parse the first word for the type of data.
+    :type fast_parse: bool
+    :param prefix: The input prefix found during parsing (internal use only)
+    :type prefix: str
     """
 
+    def __init__(self, input=None, fast_parse=False, prefix=None):
+        if prefix:
+            self._load_correct_parser(prefix)
+        super().__init__(input, fast_parse)
+
     @property
     def _class_prefix(self):
         return None
@@ -335,3 +348,21 @@ class DataInput(DataInputAbstract):
     @property
     def _has_classifier(self):  # pragma: no cover
         return None
+
+    def _load_correct_parser(self, prefix):
+        """
+        Decides if a specialized parser needs to be loaded for barebone
+        special cases.
+
+        .. versionadded:: 0.3.0
+        """
+        PARAM_PARSER = ParamOnlyDataParser
+        TALLY_PARSER = montepy.input_parser.tally_parser.TallyParser
+        PARSER_PREFIX_MAP = {
+            "f": TALLY_PARSER,
+            "fm": TALLY_PARSER,
+            "fs": montepy.input_parser.tally_seg_parser.TallySegmentParser,
+            "sdef": PARAM_PARSER,
+        }
+        if prefix.lower() in PARSER_PREFIX_MAP:
+            self._parser = PARSER_PREFIX_MAP[prefix.lower()]()
diff --git a/montepy/data_inputs/data_parser.py b/montepy/data_inputs/data_parser.py
index 5e8b9b1b..7d0ca9a2 100644
--- a/montepy/data_inputs/data_parser.py
+++ b/montepy/data_inputs/data_parser.py
@@ -6,9 +6,6 @@ from montepy.data_inputs import (
     lattice_input,
     material,
     mode,
-    tally,
-    tally_segment,
-    tally_multiplier,
     thermal_scattering,
     universe_input,
     volume,
@@ -22,9 +19,6 @@ PREFIX_MATCHES = {
     lattice_input.LatticeInput,
     material.Material,
     mode.Mode,
-    tally.Tally,
-    tally_multiplier.TallyMultiplier,
-    tally_segment.TallySegment,
     thermal_scattering.ThermalScatteringLaw,
     transform.Transform,
     volume.Volume,
@@ -47,8 +41,7 @@ def parse_data(input):
 
     base_input = data_input.DataInput(input, fast_parse=True)
     prefix = base_input.prefix
-
     for data_class in PREFIX_MATCHES:
         if prefix == data_class._class_prefix():
             return data_class(input)
-    return data_input.DataInput(input)
+    return data_input.DataInput(input, prefix=prefix)
diff --git a/montepy/data_inputs/tally.py b/montepy/data_inputs/tally.py
deleted file mode 100644
index ecfc6429..00000000
--- a/montepy/data_inputs/tally.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved.
-import montepy
-from montepy.data_inputs.data_input import DataInputAbstract
-from montepy.input_parser.tally_parser import TallyParser
-
-
-class Tally(DataInputAbstract):
-    """ """
-
-    _parser = TallyParser()
-
-    @staticmethod
-    def _class_prefix():
-        return "f"
-
-    @staticmethod
-    def _has_number():
-        return True
-
-    @staticmethod
-    def _has_classifier():
-        return 1
diff --git a/montepy/data_inputs/tally_multiplier.py b/montepy/data_inputs/tally_multiplier.py
deleted file mode 100644
index 3c954a6f..00000000
--- a/montepy/data_inputs/tally_multiplier.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved.
-from montepy.data_inputs.data_input import DataInputAbstract
-from montepy.input_parser.tally_parser import TallyParser
-
-
-class TallyMultiplier(DataInputAbstract):
-    _parser = TallyParser()
-
-    @staticmethod
-    def _class_prefix():
-        return "fm"
-
-    @staticmethod
-    def _has_number():
-        return True
-
-    @staticmethod
-    def _has_classifier():
-        return 0
diff --git a/montepy/data_inputs/tally_segment.py b/montepy/data_inputs/tally_segment.py
deleted file mode 100644
index 9a78cea2..00000000
--- a/montepy/data_inputs/tally_segment.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved.
-import montepy
-from montepy.data_inputs.data_input import DataInputAbstract
-from montepy.input_parser.tally_seg_parser import TallySegmentParser
-
-
-class TallySegment(DataInputAbstract):
-    """ """
-
-    _parser = TallySegmentParser()
-
-    @staticmethod
-    def _class_prefix():
-        return "fs"
-
-    @staticmethod
-    def _has_number():
-        return True
-
-    @staticmethod
-    def _has_classifier():
-        return 0
diff --git a/montepy/input_parser/__init__.py b/montepy/input_parser/__init__.py
index a80864bb..e6a83a8f 100644
--- a/montepy/input_parser/__init__.py
+++ b/montepy/input_parser/__init__.py
@@ -10,4 +10,6 @@ from . import parser_base
 from . import read_parser
 from . import shortcuts
 from . import surface_parser
+from . import tally_parser
+from . import tally_seg_parser
 from . import tokens
diff --git a/montepy/input_parser/data_parser.py b/montepy/input_parser/data_parser.py
index f54b178f..57635454 100644
--- a/montepy/input_parser/data_parser.py
+++ b/montepy/input_parser/data_parser.py
@@ -86,10 +86,10 @@ class DataParser(MCNP_Parser):
     def particle_sequence(self, p):
         if len(p) == 1:
             sequence = syntax_node.ListNode("particle sequence")
-            sequence.append(p[0])
+            sequence.append(p[0], True)
         else:
             sequence = p[0]
-            sequence.append(p[1])
+            sequence.append(p[1], True)
         return sequence
 
     @_("PARTICLE", "SURFACE_TYPE", "PARTICLE_SPECIAL")
@@ -114,10 +114,10 @@ class DataParser(MCNP_Parser):
     def text_sequence(self, p):
         if len(p) == 1:
             sequence = syntax_node.ListNode("text sequence")
-            sequence.append(p[0])
+            sequence.append(p[0], True)
         else:
             sequence = p[0]
-            sequence.append(p[1])
+            sequence.append(p[1], True)
         return sequence
 
     @_("kitchen_junk", "kitchen_sink kitchen_junk")
@@ -125,7 +125,7 @@ class DataParser(MCNP_Parser):
         sequence = p[0]
         if len(p) != 1:
             for node in p[1].nodes:
-                sequence.append(node)
+                sequence.append(node, True)
         return sequence
 
     @_("number_sequence", "text_sequence", "particle_sequence")
@@ -166,3 +166,107 @@ class ClassifierParser(DataParser):
         return syntax_node.SyntaxNode(
             "data input classifier", {"start_pad": padding, "classifier": p.classifier}
         )
+
+
+class ParamOnlyDataParser(DataParser):
+    """
+    A parser for parsing parameter (key-value pair) only data inputs.
+
+    .e.g., SDEF
+
+    .. versionadded:: 0.3.0
+
+    :returns: a syntax tree for the data input.
+    :rtype: SyntaxNode
+    """
+
+    debugfile = None
+
+    @_(
+        "param_introduction spec_parameters",
+    )
+    def param_data_input(self, p):
+        ret = {}
+        for key, node in p.param_introduction.nodes.items():
+            ret[key] = node
+        if hasattr(p, "spec_parameters"):
+            ret["parameters"] = p.spec_parameters
+        return syntax_node.SyntaxNode("data", ret)
+
+    @_(
+        "classifier_phrase",
+        "padding classifier_phrase",
+    )
+    def param_introduction(self, p):
+        ret = {}
+        if isinstance(p[0], syntax_node.PaddingNode):
+            ret["start_pad"] = p[0]
+        else:
+            ret["start_pad"] = syntax_node.PaddingNode()
+        ret["classifier"] = p.classifier_phrase
+        ret["keyword"] = syntax_node.ValueNode(None, str, padding=None)
+        return syntax_node.SyntaxNode("data intro", ret)
+
+    @_("spec_parameter", "spec_parameters spec_parameter")
+    def spec_parameters(self, p):
+        """
+        A list of the parameters (key, value pairs) for this input.
+
+        :returns: all parameters
+        :rtype: ParametersNode
+        """
+        if len(p) == 1:
+            params = syntax_node.ParametersNode()
+            param = p[0]
+        else:
+            params = p[0]
+            param = p[1]
+        params.append(param)
+        return params
+
+    @_("spec_classifier param_seperator data")
+    def spec_parameter(self, p):
+        return syntax_node.SyntaxNode(
+            p.spec_classifier.prefix.value,
+            {
+                "classifier": p.spec_classifier,
+                "seperator": p.param_seperator,
+                "data": p.data,
+            },
+        )
+
+    @_(
+        "KEYWORD",
+    )
+    def spec_data_prefix(self, p):
+        return syntax_node.ValueNode(p[0], str)
+
+    @_(
+        "modifier spec_data_prefix",
+        "spec_data_prefix",
+        "spec_classifier NUMBER",
+        "spec_classifier particle_type",
+    )
+    def spec_classifier(self, p):
+        """
+        The classifier of a data input.
+
+        This represents the first word of the data input.
+        E.g.: ``M4``, `IMP:N`, ``F104:p``
+
+        :rtype: ClassifierNode
+        """
+        if hasattr(p, "spec_classifier"):
+            classifier = p.spec_classifier
+        else:
+            classifier = syntax_node.ClassifierNode()
+
+        if hasattr(p, "modifier"):
+            classifier.modifier = syntax_node.ValueNode(p.modifier, str)
+        if hasattr(p, "spec_data_prefix"):
+            classifier.prefix = p.spec_data_prefix
+        if hasattr(p, "NUMBER"):
+            classifier.number = syntax_node.ValueNode(p.NUMBER, int)
+        if hasattr(p, "particle_type"):
+            classifier.particles = p.particle_type
+        return classifier
diff --git a/montepy/input_parser/parser_base.py b/montepy/input_parser/parser_base.py
index dcd844ce..75846269 100644
--- a/montepy/input_parser/parser_base.py
+++ b/montepy/input_parser/parser_base.py
@@ -52,7 +52,6 @@ class MetaBuilder(sly.yacc.ParserMeta):
         for par_basis in parent:
             if par_basis != Parser:
                 return
-                MetaBuilder._flatten_rules(classname, par_basis, attributes)
 
 
 class SLY_Supressor:
@@ -105,6 +104,9 @@ class SLY_Supressor:
         self._parse_fail_queue = []
         return ret
 
+    def __len__(self):
+        return len(self._parse_fail_queue)
+
 
 class MCNP_Parser(Parser, metaclass=MetaBuilder):
     """
@@ -142,7 +144,20 @@ class MCNP_Parser(Parser, metaclass=MetaBuilder):
         :rtype: SyntaxNode
         """
         self._input = input
-        return super().parse(token_generator)
+
+        # debug every time a token is taken
+        def gen_wrapper():
+            while True:
+                token = next(token_generator, None)
+                self._debug_parsing_error(token)
+                yield token
+
+        # change to using `gen_wrapper()` to debug
+        tree = super().parse(token_generator)
+        # treat any previous errors as being fatal even if it recovered.
+        if len(self.log) > 0:
+            return None
+        return tree
 
     precedence = (("left", SPACE), ("left", TEXT))
 
@@ -523,3 +538,18 @@ class MCNP_Parser(Parser, metaclass=MetaBuilder):
                 )
         else:
             self.log.parse_error("sly: Parse error in input. EOF\n")
+
+    def _debug_parsing_error(self, token):  # pragma: no cover
+        """
+        A function that should be called from error when debugging a parsing error.
+
+        Call this from the method error. Also you will need the relevant debugfile to be set and saving the parser
+        tables to file. e.g.,
+
+        debugfile = 'parser.out'
+        """
+        print("********* New Parsing Error ************ ")
+        print(f"Token: {token}")
+        print(f"State: {self.state}, statestack: {self.statestack}")
+        print(f"Symstack: {self.symstack}")
+        print()
diff --git a/montepy/input_parser/syntax_node.py b/montepy/input_parser/syntax_node.py
index 496a7280..437b065a 100644
--- a/montepy/input_parser/syntax_node.py
+++ b/montepy/input_parser/syntax_node.py
@@ -375,6 +375,15 @@ class PaddingNode(SyntaxNodeBase):
             return False
         return len(val.strip()) == 0 and val != "\n"
 
+    def has_space(self):
+        """
+        Determines if there is syntactically significant space anywhere in this node.
+
+        :returns: True if there is syntactically significant (not in a comment) space.
+        :rtype: bool
+        """
+        return any([self.is_space(i) for i in range(len(self))])
+
     def append(self, val, is_comment=False):
         """
         Append the node to this node.
@@ -1031,6 +1040,20 @@ class ValueNode(SyntaxNodeBase):
         """
         return self._value
 
+    @property
+    def never_pad(self):
+        """
+        Whether or not this value node will not have extra spaces added.
+
+        :returns: true if extra padding is not adding at the end if missing.
+        :rtype: bool
+        """
+        return self._never_pad
+
+    @never_pad.setter
+    def never_pad(self, never_pad):
+        self._never_pad = never_pad
+
     @value.setter
     def value(self, value):
         if self.is_negative is not None and value is not None:
@@ -1326,15 +1349,23 @@ class ListNode(SyntaxNodeBase):
                 else:
                     check_for_orphan_jump(new_vals[i])
 
-    def append(self, val):
+    def append(self, val, from_parsing=False):
         """
         Append the node to this node.
 
         :param node: node
         :type node: ValueNode, ShortcutNode
+        :param from_parsing: If this is being append from the parsers, and not elsewhere.
+        :type from_parsing: bool
         """
         if isinstance(val, ShortcutNode):
             self._shortcuts.append(val)
+        if len(self) > 0 and from_parsing:
+            last = self[-1]
+            if isinstance(last, ValueNode) and (
+                (last.padding and not last.padding.has_space) or last.padding is None
+            ):
+                self[-1].never_pad = True
         super().append(val)
 
     @property
@@ -1353,6 +1384,7 @@ class ListNode(SyntaxNodeBase):
                 and node.padding is None
                 and i < length - 1
                 and not isinstance(self.nodes[i + 1], PaddingNode)
+                and not node.never_pad
             ):
                 node.padding = PaddingNode(" ")
             if isinstance(last_node, ShortcutNode) and isinstance(node, ShortcutNode):
@@ -1622,9 +1654,7 @@ class ShortcutNode(ListNode):
             jump_num = 1
             self._num_node = ValueNode(None, int, never_pad=True)
         for i in range(jump_num):
-            self._nodes.append(
-                ValueNode(input_parser.mcnp_input.Jump(), float, never_pad=True)
-            )
+            self._nodes.append(ValueNode(input_parser.mcnp_input.Jump(), float))
 
     def _expand_interpolate(self, p):
         if self._type == Shortcuts.LOG_INTERPOLATE:
diff --git a/montepy/input_parser/tally_parser.py b/montepy/input_parser/tally_parser.py
index 50a8c917..a5468863 100644
--- a/montepy/input_parser/tally_parser.py
+++ b/montepy/input_parser/tally_parser.py
@@ -4,7 +4,14 @@ from montepy.input_parser import syntax_node
 
 
 class TallyParser(DataParser):
-    """ """
+    """
+    A barebone parser for parsing tallies before they are fully implemented.
+
+    .. versionadded:: 0.2.0
+
+    :returns: a syntax tree for the data input.
+    :rtype: SyntaxNode
+    """
 
     debugfile = None
 
diff --git a/montepy/input_parser/tally_seg_parser.py b/montepy/input_parser/tally_seg_parser.py
index 25ae0856..bef8e01c 100644
--- a/montepy/input_parser/tally_seg_parser.py
+++ b/montepy/input_parser/tally_seg_parser.py
@@ -4,7 +4,14 @@ from montepy.input_parser import syntax_node
 
 
 class TallySegmentParser(DataParser):
-    """ """
+    """
+    A barebone parser for parsing tally segment inputs before they are fully implemented.
+
+    .. versionadded:: 0.2.10
+
+    :returns: a syntax tree for the data input.
+    :rtype: SyntaxNode
+    """
 
     debugfile = None
 
diff --git a/montepy/input_parser/tokens.py b/montepy/input_parser/tokens.py
index 136e7c6b..a62856c2 100644
--- a/montepy/input_parser/tokens.py
+++ b/montepy/input_parser/tokens.py
@@ -94,6 +94,31 @@ class MCNP_Lexer(Lexer):
         "refs",
         # volume
         "no",
+        # sdef
+        "cel",
+        "sur",
+        "erg",
+        "tme",
+        "dir",
+        "vec",
+        "nrm",
+        "pos",
+        "rad",
+        "ext",
+        "axs",
+        "x",
+        "y",
+        "z",
+        "ccc",
+        "ara",
+        "wgt",
+        "tr",
+        "eff",
+        "par",
+        "dat",
+        "loc",
+        "bem",
+        "bap",
     }
     """
     Defines allowed keywords in MCNP.
diff --git a/pyproject.toml b/pyproject.toml
index bea43920..0f9cffcd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,10 +7,10 @@ description = "A library for reading, editing, and writing MCNP input files"
 readme = "README.md"
 requires-python = ">=3.8"
 maintainers = [
-	{name = "Micah Gale", email = "micah.gale@inl.gov"}
+	{name = "Micah Gale", email = "mgale@montepy.org"}
 ]
 authors = [
-	{name = "Micah Gale", email = "micah.gale@inl.gov"},
+	{name = "Micah Gale", email = "mgale@montepy.org"},
 	{name = "Travis Labossiere-Hickman", email = "Travis.LabossiereHickman@inl.gov"},
 	{name = "Brenna Carbno", email="brenna.carbno@inl.gov"}
 ]


Test patch:
diff --git a/tests/test_integration.py b/tests/test_integration.py
index ba79da99..af50a3a0 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -783,7 +783,7 @@ class testFullFileIntegration(TestCase):
         with self.assertWarns(LineExpansionWarning):
             output = problem.cells._universe.format_for_mcnp_input((6, 2, 0))
         print(output)
-        self.assertIn("u 350 2J -1 J 350", output)
+        self.assertIn("u 350 2J -1 J 350 ", output)
 
     def test_universe_number_collision(self):
         problem = montepy.read_input(
diff --git a/tests/test_source.py b/tests/test_source.py
new file mode 100644
index 00000000..ef50f009
--- /dev/null
+++ b/tests/test_source.py
@@ -0,0 +1,16 @@
+import montepy
+from montepy.data_inputs.data_parser import parse_data
+from montepy.input_parser.block_type import BlockType
+from montepy.input_parser.mcnp_input import Input
+
+import pytest
+
+
+@pytest.mark.parametrize(
+    "line", ["sdef  cel=d1 erg=d2 pos=fcel d3 ext=fcel d4 axs=0 0 1 rad=d5"]
+)
+def test_source_parse_and_parrot(line):
+    input = Input([line], BlockType.DATA)
+    data = parse_data(input)
+    print(repr(data._tree))
+    assert data._tree.format() == line
diff --git a/tests/test_syntax_parsing.py b/tests/test_syntax_parsing.py
index a59a4e05..9bd42fe1 100644
--- a/tests/test_syntax_parsing.py
+++ b/tests/test_syntax_parsing.py
@@ -1,6 +1,7 @@
 # Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved.
 import copy
 from io import StringIO
+import pytest
 from unittest import TestCase
 
 import montepy
@@ -567,6 +568,22 @@ class TestPaddingNode(TestCase):
         self.assertEqual(len(comment.contents), 0)
 
 
+@pytest.mark.parametrize(
+    "padding,expect",
+    [
+        (["$ hi"], False),
+        ([" c style comment"], False),
+        ([" "], True),
+        (["$ hi", "    ", "c hi"], True),
+    ],
+)
+def test_padding_has_space(padding, expect):
+    node = syntax_node.PaddingNode(padding[0])
+    for pad in padding[1:]:
+        node.append(pad)
+    assert node.has_space() == expect
+
+
 class TestParticlesNode(TestCase):
     def test_particle_init(self):
         parts = syntax_node.ParticleNode("test", ":n,p,e")