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 @@ def format_for_mcnp_input(self, mcnp_version): 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 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 @@ def _has_number(self): # pragma: no cover @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 @@ lattice_input, material, mode, - tally, - tally_segment, - tally_multiplier, thermal_scattering, universe_input, volume, @@ -22,9 +19,6 @@ 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 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 @@ def zaid_phrase(self, p): 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 @@ def text_phrase(self, p): 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 @@ def kitchen_sink(self, p): 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 @@ def data_classifier(self, p): 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 @@ def _flatten_rules(classname, basis, attributes): for par_basis in parent: if par_basis != Parser: return - MetaBuilder._flatten_rules(classname, par_basis, attributes) class SLY_Supressor: @@ -105,6 +104,9 @@ def clear_queue(self): 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 @@ def parse(self, token_generator, input=None): :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 @@ def error(self, token): ) 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 @@ def is_space(self, i): 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 @@ def value(self): """ 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 @@ def check_for_orphan_jump(value): 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 @@ def format(self): 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 @@ def _expand_jump(self, p): 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 @@ 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 @@ 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"} ] 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 @@ def test_universe_data_formatter(self): 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 @@ def test_blank_dollar_comment(self): 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")