diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c4fc82f8..9d2be93e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,13 +11,14 @@ jobs: steps: - uses: actions/checkout@v4 - name: set up python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 - run: pip install . montepy[develop] - run: python -m pytest build-pages: + needs: [last-minute-test, build-packages] environment: name: github-pages runs-on: ubuntu-latest @@ -27,7 +28,9 @@ jobs: TOKEN: ${{ secrets.ACCESS_TOKEN }} run: git config --global url."https://${TOKEN}:x-oauth-basic@github.com/".insteadOf "https://github.com/" - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + with: + ref: main + - uses: actions/setup-python@v5 with: python-version: 3.8 - run: pip install --user . montepy[doc] @@ -53,13 +56,28 @@ jobs: fetch-depth: 0 fetch-tags: true - name: set up python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 - run: pip install . montepy[build] + - uses: mathieudutour/github-tag-action@v6.2 + name: Get next version number (dry) + id: version_num + with: + dry_run: True + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Update changelog version + run: | + sed -i "s/#Next Version#/${{steps.version_num.outputs.new_version}}/w changes" doc/source/changelog.rst + [[ -s changes ]] || exit 1 + - name: Commit new changelog + uses: actions4git/add-commit-push@v1.0.0 + with: + add-pathspec: doc/source + commit-message: Update Changelog version to ${{steps.version_num.outputs.new_version}} - name: GitHub Actions Create Tag id: tag_version - uses: mathieudutour/github-tag-action@v6.1 + uses: mathieudutour/github-tag-action@v6.2 with: github_token: ${{ secrets.GITHUB_TOKEN }} # ensure tags are up to date @@ -85,7 +103,7 @@ jobs: gh release upload '${{ steps.tag_version.outputs.new_tag }}' dist/** --repo '${{ github.repository }}' - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v4.3.1 with: name: build path: | diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 10507e64..7c339d9e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: set up python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - run: pip install --upgrade pip build @@ -35,7 +35,8 @@ jobs: - run: pip install --user . montepy[develop] - run: pip freeze - name: Upload build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4.3.1 + if: ${{ matrix.python-version == '3.9' }} with: name: build path: dist/* @@ -50,35 +51,38 @@ jobs: steps: - uses: actions/checkout@v4 - name: set up python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - run: pip install --user . montepy[test] + - run: pip install --user . montepy[build] - run: coverage run -m pytest --junitxml=test_report.xml - run: coverage report + if: ${{ success() || failure() }} - run: coverage xml + if: ${{ success() || failure() }} - name: Upload test report - if: ${{ matrix.python-version == '3.9' }} + if: ${{ matrix.python-version == '3.9' && (success() || failure() )}} uses: actions/upload-artifact@v4 with: name: test path: test_report.xml - name: Upload coverage report - if: ${{ matrix.python-version == '3.9' }} + if: ${{ matrix.python-version == '3.9' && (success() || failure() )}} uses: actions/upload-artifact@v4 with: name: coverage path: coverage.xml - name: Test Reporter - if: ${{ matrix.python-version == '3.9' }} + if: ${{ matrix.python-version == '3.9' && (success() || failure() )}} uses: dorny/test-reporter@v1.7.0 with: name: CI-test-report path: test_report.xml reporter: java-junit - name: Coveralls GitHub Action - if: ${{ matrix.python-version == '3.9' }} - uses: coverallsapp/github-action@v2.2.3 + if: ${{ matrix.python-version == '3.9' && (success() || failure() )}} + uses: coverallsapp/github-action@v2 with: file: coverage.xml @@ -88,17 +92,30 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true - name: set up python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 - - run: pip install . montepy[doc] + - run: pip install . montepy[doc,build] + - uses: mathieudutour/github-tag-action@v6.2 + name: Get next version number + id: version_num + with: + dry_run: True + github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Update changelog version + run: | + sed -i "s/#Next Version#/${{steps.version_num.outputs.new_version}}/w changes" doc/source/changelog.rst + [[ -s changes ]] || exit 1 - run: sphinx-build doc/source/ doc/build/ -W --keep-going -E - - run: sphinx-build -b html doc/source/ doc/build/html - - uses: actions/upload-artifact@v3 + name: Build site strictly + - uses: actions/upload-artifact@v4 with: name: website - path: doc/build/html + path: doc/build - name: Test for missing API documentation run: | cd doc/source @@ -110,10 +127,27 @@ jobs: steps: - uses: actions/checkout@v4 - name: set up python 3.8 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.8 - run: pip install . montepy[format] - run: black --check montepy/ tests/ - - + + + changelog-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Check for changes + uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + change: + - 'doc/source/changelog.rst' + - if: steps.changes.outputs.change == 'false' + run: | + echo "Changelog not updated" + exit 1 + diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 120000 index 00000000..e0e37937 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1 @@ +doc/source/changelog.rst \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..fba47e95 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing to MontePy + +Welcome, and thank you for considering contributing to MontePy! We look forward +to welcoming new members to the community and will do our best to help you get +up to speed. + +## Code of Conduct + +Participants in the MontePy project are expected to follow and uphold the [Code +of Conduct](CODE_OF_CONDUCT.md). Please report any unacceptable behavior to +micah.gale@inl.gov. + +## Resources + +- [GitHub Repository](https://github.com/idaholab/montepy) +- [Documentation](https://idaholab.github.io/MontePy/) +- [Developer's Guide](https://idaholab.github.io/MontePy/developing.html) + +## How to Report Bugs + +MontePy is hosted on GitHub and all bugs are reported and tracked through the +[Issues](https://github.com/idaholab/MontePy/issues) listed on GitHub. + +## How to Suggest Enhancements + +If you have suggestions for new features, feel free to suggest one as a new issue. +Keep in mind that MontePy has a small development team, +and in some cases MontePy might not be the right home for the feature. +Please review the package philosophy to see if the feature would be a good fit. + +## How to Submit Changes + +All changes to OpenMC happen through pull requests. For a full overview of the +process, see the developer's guide section on [Contributing to +OpenMC](https://docs.openmc.org/en/latest/devguide/contributing.html). + +## Code Style + +Before you run off to make changes to the code, please review the [design +philosophy](https://idaholab.github.io/MontePy/developing.html#design-philosophy), +and make sure to also use [black](https://black.readthedocs.io/en/stable/index.html). diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..7c325863 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +exclude tests/test_version.py diff --git a/doc/source/_test_for_missing_docs.py b/doc/source/_test_for_missing_docs.py index f0dbf9a7..c6ad78f1 100644 --- a/doc/source/_test_for_missing_docs.py +++ b/doc/source/_test_for_missing_docs.py @@ -3,7 +3,7 @@ import sys import warnings -ignored = {"__pycache__", "_version.py", "__main__.py", "_cell_data_control.py"} +ignored = {"__pycache__", "_scripts", "_version.py", "__main__.py", "_cell_data_control.py"} base = os.path.join("..", "..") @@ -14,6 +14,8 @@ def crawl_path(rel_path): f_name = os.path.join(rel_path, f) if f in ignored: continue + if f_name == "montepy/__init__.py": + continue if os.path.isdir(os.path.join(base, f_name)): crawl_path(f_name) elif os.path.isfile(os.path.join(base, f_name)) and ".py" in f: diff --git a/doc/source/api/modules.rst b/doc/source/api/modules.rst index 7331f9c2..89af6576 100644 --- a/doc/source/api/modules.rst +++ b/doc/source/api/modules.rst @@ -1,8 +1,36 @@ -Python API -============ +MontePy API +=========== +Submodules +---------- .. toctree:: - :maxdepth: 4 + :maxdepth: 1 + + montepy.cell + montepy.cells + montepy.constants + montepy.errors + montepy.geometry_operators + montepy.materials + montepy.mcnp_object + montepy.mcnp_problem + montepy.numbered_mcnp_object + montepy.numbered_object_collection + montepy.particle + montepy.surface_collection + montepy.transforms + montepy.universe + montepy.universes + montepy.utilities + +Subpackages +----------- + +.. toctree:: + :maxdepth: 2 + + montepy.data_inputs + montepy.input_parser + montepy.surfaces - montepy diff --git a/doc/source/api/montepy.cell.rst b/doc/source/api/montepy.cell.rst index f14acb8c..5b34b566 100644 --- a/doc/source/api/montepy.cell.rst +++ b/doc/source/api/montepy.cell.rst @@ -2,7 +2,7 @@ montepy.cell module =================== -.. automodule:: montepy.cell +.. autoclass:: montepy.cell.Cell :members: :undoc-members: :show-inheritance: diff --git a/doc/source/api/montepy.data_inputs.rst b/doc/source/api/montepy.data_inputs.rst index 96bdae1e..3471a48c 100644 --- a/doc/source/api/montepy.data_inputs.rst +++ b/doc/source/api/montepy.data_inputs.rst @@ -7,8 +7,6 @@ montepy.data\_inputs package :undoc-members: :show-inheritance: -Submodules ----------- .. toctree:: :maxdepth: 4 diff --git a/doc/source/api/montepy.input_parser.rst b/doc/source/api/montepy.input_parser.rst index fc837495..3c595b97 100644 --- a/doc/source/api/montepy.input_parser.rst +++ b/doc/source/api/montepy.input_parser.rst @@ -7,11 +7,8 @@ montepy.input\_parser package :undoc-members: :show-inheritance: -Submodules ----------- - .. toctree:: - :maxdepth: 4 + :maxdepth: 2 montepy.input_parser.block_type montepy.input_parser.cell_parser diff --git a/doc/source/api/montepy.rst b/doc/source/api/montepy.rst deleted file mode 100644 index 7f788814..00000000 --- a/doc/source/api/montepy.rst +++ /dev/null @@ -1,41 +0,0 @@ -montepy package -=============== - - -.. automodule:: montepy - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - :maxdepth: 4 - - montepy.data_inputs - montepy.input_parser - montepy.surfaces - -Submodules ----------- - -.. toctree:: - :maxdepth: 4 - - montepy.cell - montepy.cells - montepy.constants - montepy.errors - montepy.geometry_operators - montepy.materials - montepy.mcnp_object - montepy.mcnp_problem - montepy.numbered_mcnp_object - montepy.numbered_object_collection - montepy.particle - montepy.surface_collection - montepy.transforms - montepy.universe - montepy.universes - montepy.utilities diff --git a/doc/source/api/montepy.surfaces.rst b/doc/source/api/montepy.surfaces.rst index e66b4112..6f07c96e 100644 --- a/doc/source/api/montepy.surfaces.rst +++ b/doc/source/api/montepy.surfaces.rst @@ -7,11 +7,9 @@ montepy.surfaces package :undoc-members: :show-inheritance: -Submodules ----------- .. toctree:: - :maxdepth: 4 + :maxdepth: 2 montepy.surfaces.axis_plane montepy.surfaces.cylinder_on_axis diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst new file mode 100644 index 00000000..9ed36f32 --- /dev/null +++ b/doc/source/changelog.rst @@ -0,0 +1,160 @@ +MontePy Changelog +================= + +#Next Version# +----------------------- + +**Documentation** + +* Added changelog +* Added code of conduct +* Added contribution guideline +* Added pull request template + + +0.2.5 +------------------- + +**Added** + +* Implemented Github actions +* Added default github issue templates + +**Changed** + +* Improved readme and documentation hyperlinks + +**Fixed** + +* bug with comments in complex geometry. + + +0.2.4 +------------------- +**Added** + +* Public release + +0.2.3 +-------------------- +**Added** + +* A license +* A logo + +**Changed** + +* Explicitly set file encoding for read/write. #159. + +**Fixed** + +* Bug with not detecting comments with no space e.g., `c\n`. #158. + +0.2.2 +-------------------- +**Fixed** + +* TODO + +0.2.1 +--------------------- +**Fixed** + +* A bug with the packaging process + +0.2.0 +---------------------- +**Added** + +* User formatting is preserved automatically +* Cell geometry is now stored in `cell.geometry` and can be set with bitwise operators. e.g., `cell.geometry = + inner_sphere & - outer_sphere`. This was heavily influenced by OpenMC. +* You can now check an input file for errors from the command line. `python -m montepy -c /path/to/inputs/*.imcnp` +* The error reporting for syntax errors should be much more intuitive now, and easy to read. +* Dollar sign comments are kept and are available in `obj.comments` +* All comments are now in a generator `.comments` + +**Deprecated** + +* `montepy.data_cards` moved to `montepy.data_inputs` +* `montepy.data_cards.data_card` is now `montepy.data_inputs.data_input` +* `Montepy.Cell.geometry_logic_string` was completely removed. +* Much of the internal functions with how objects are written to file were changed and/or deprecated. +* `montepy.data_cards.data_card.DataCard.class_prefix` was moved to `_class_prefix` as the user usually shouldn't see this. Same goes for `has_classifier` and `has_number`. +* Most of the data types inside `montepy.input_parser.mcnp_input` were deprecated or changed + +0.1.7 +----------------- + +**Added** + +* License information + +0.1.6 +------------------- + +**fixed** + +* Fixed bug that `+=` didn't work with Numbered object collections +* Updated the Documentation URL for sphinx +* Improved (and then removed) guidance on weird gitlab installation workflow. + +0.1.5 +-------------------- + +**Fixed** + +* When a `PX` style surface was `1 PZ 0` this would cause a validation error. +* Empty "cell modifiers" would be printed in the data block even if they had no useful information. E.g., `U 30J` +* Volumes couldn't start with a jump e.g., `vol j 1.0` +* "Cell modifiers" were printed both in the cell block and the data block. +* Running `problem.cells = []` would make the problem impossible to write to file. +* Support was added for tabs. + +0.1.0 +--------------------- + + +**Added** + +* Added infrastructure to support cell modifier inputs easily +* Added support for importances, and particle modes: `imp`, `mode`. +* Added support for cell volumes `vol`. +* Add support for Universes, lattices, and fills `U`, `fill`, `lat`. +* Created universal system for parsing parameters +* If you create an object from scratch and write it out to a file while it is missing, it will gracefully fail with a helpful error message. +* Added support for detecting metastable isotopes. +* Improved the experience with densities in `Cell` instead of having `cell.density` now there is `cell.mass_density` and `cell.atom_density`. + + +**Fixed** + +* Supported parameters that don't have equal signs. MCNP supports `1 0 -1 u 1` +* Now doesn't try to expand shortcuts inside of `FC` and `SC` comments. + +**Code Quality** + +* Removed magic numbers for number of characters in a line. +* Reduced the usage of regular expressions +* Made error messages related to invalid user set attributes clearer. +* Cleaned up documentation and docstrings +* Improved CI backend + + +0.0.5 +----------------------- + +**Added:** + + * `NumberedObjectCollections` which is implemented for `cells`, `surfaces`, and `materials`. This changed these collections from being a list to acting like a dict. Objects are now retrievable by their number e.g., `cells[1005]` will retrieve cell 1005. + * Implemented "pass-through" of the original inputs. If an object is not edited or mutated, the original formatting from the input file will be copied out to the output. + * Support was added for most MCNP shortcuts: (`R`, `I`, `M`, `LOG`), `J` still needs some better support. MontePy will expand these shortcuts, but will not "recompress" them. + * Added sphinx documentation website. This documents the API, has a starting guide for the users, and a guide for developers. + + +**Changed:** + +* Object numbers are now generalized: e.g., `cell.cell_number` has changed to `cell.number`. The `.number` property is standardized across all numbered objects. + +**Fixed:** + +* Comments in the middle of an input no longer breaks the input into two. diff --git a/doc/source/dev_tree.rst b/doc/source/dev_tree.rst new file mode 100644 index 00000000..df91c14d --- /dev/null +++ b/doc/source/dev_tree.rst @@ -0,0 +1,9 @@ +Developer's Resources +===================== + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + developing + scope diff --git a/doc/source/developing.rst b/doc/source/developing.rst index 5d9c2485..6f740325 100644 --- a/doc/source/developing.rst +++ b/doc/source/developing.rst @@ -15,6 +15,7 @@ Contributing Here is a getting started guide to contributing. If you have any questions Micah and Travis are available to give input and answer your questions. +Before contributing you should review the :ref:`scope` and design philosophy. Setting up and Typical Development Workflow ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -154,27 +155,6 @@ All classes need to be children of :class:`~montepy.surfaces.surface.Surface`. When possible new surface classes should combine similar planes. For example :class:`~montepy.surfaces.axis_plane.AxisPlane` covers ``PX``, ``PY``, and ``PZ``. -Design Philosophy ------------------ -#. **Do Not Repeat Yourself (DRY)** -#. Use abstraction and inheritance smartly. -#. Use ``_private`` fields mostly. Use ``__private`` for very private things that should never be touched. -#. Use ``@property`` getters, and if needed setters. Setters must verify and clean user inputs. For the most part use :func:`~montepy.utilities.make_prop_val_node`, and :func:`~montepy.utilities.make_prop_pointer`. -#. Fail early and politely. If there's something that might be bad: the user should get a helpful error as - soon as the error is apparent. -#. Test. test. test. The goal is to achieve 100% test coverage. Unit test first, then do integration testing. A new feature merge request will ideally have around a dozen new test cases. -#. Do it right the first time. -#. Document all functions. -#. Expect everything to mutate at any time. -#. Avoid relative imports when possible. Use top level ones instead: e.g., ``import montepy.cell.Cell``. -#. Defer to vanilla python, and only use the standard library. Currently the only dependencies are `numpy `_ and `sly `_. - There must be good justification for breaking from this convention and complicating things for the user. - -Style Guide ------------ -#. Use ``black`` to autoformat all code. -#. Spaces for indentation, tabs for alignment. Use spaces to build python syntax (4 spaces per level), and tabs for aligning text inside of docstrings. - Introduction to SLY and Syntax Trees ------------------------------------ diff --git a/doc/source/index.rst b/doc/source/index.rst index 31077195..c5fb49e7 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -3,26 +3,30 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to MontePy's documentation! -=================================== +MontePy: a Python library for MCNP input files. +=============================================== -.. toctree:: - :maxdepth: 1 - :caption: Contents: +MontePy is a Python library for reading, editing, and writing MCNP input files. +MontePy provides an object-oriented interface for MCNP input files. +This allows for easy automation of many different tasks for working with MCNP input files. - starting +Installing +---------- - utilities +MontePy can be installed with pip: - tricks +.. code-block:: shell - faq + pip install montepy - developing - api/modules +.. toctree:: + :maxdepth: 2 + :caption: Table of Contents: - publications + api/modules + users + dev_tree See Also ======== diff --git a/doc/source/scope.rst b/doc/source/scope.rst new file mode 100644 index 00000000..acbc964a --- /dev/null +++ b/doc/source/scope.rst @@ -0,0 +1,101 @@ +.. _scope: + +MontePy Scope +============= + +This document defines the scope of MontePy so it easier to decide if a feature should live in MontePy or possibly another repository. +This isn't meant to be fully static and is subject to change. +This is less focused on defining a strict scope and more-so on providing guidelines. + +Mission +------- + +MontePy's mission is to improve the user experience for working with MCNP input files through the power of python automation? + +Principles +---------- + +Here are the guiding principles of what MontePy should be, and what it aspires to be. + +MontePy should be: +^^^^^^^^^^^^^^^^^^ + +#. Easy to install. + + #. It should be on PyPi. + #. Install with ``pip`` for most conceivable platforms. + +#. Lightweight. It should only include the code that all users need, and only the bare essentials for dependencies. +#. Well documented. All public functions must have documentation. +#. `Pythonic `_. +#. Idiomatic. +#. Consistently designed. +#. General. New features shouldn't be only useful for a specific problem, or class of problems. +#. Quick to fail and do so in a verbose helpful manner. +#. Reliable. +#. Thorough in its validation. If it MontePy finds more issues than MCNP does, that's ok. +#. Easy to contribute to. +#. Able to support the full (public) MCNP manual, except when it doesn't. + + #. For features that aren't supported yet an UnsuportedFeature error should be raised. + #. The developers reserve the right to decide a feature is not worth ever supporting. + + +MontePy shouldn't be: +^^^^^^^^^^^^^^^^^^^^^ + +#. A collection of scripts for every use case. +#. A linking code to other software. +#. Written in other languages* + +Design Philosophy +----------------- + +#. **Do Not Repeat Yourself (DRY)** +#. If it's worth doing, it's worth doing well. +#. Use abstraction and inheritance smartly. +#. Use ``_private`` fields mostly. Use ``__private`` for very private things that should never be touched. +#. Use ``@property`` getters, and if needed setters. Setters must verify and clean user inputs. For the most part use :func:`~montepy.utilities.make_prop_val_node`, and :func:`~montepy.utilities.make_prop_pointer`. +#. Fail early and politely. If there's something that might be bad: the user should get a helpful error as + soon as the error is apparent. +#. Test. test. test. The goal is to achieve 100% test coverage. Unit test first, then do integration testing. A new feature merge request will ideally have around a dozen new test cases. +#. Do it right the first time. +#. Document all functions. +#. Expect everything to mutate at any time. +#. Avoid relative imports when possible. Use top level ones instead: e.g., ``import montepy.cell.Cell``. +#. Defer to vanilla python, and only use the standard library. Currently the only dependencies are `numpy `_ and `sly `_. + There must be good justification for breaking from this convention and complicating things for the user. + +Style Guide +----------- +#. Use ``black`` to autoformat all code. +#. Spaces for indentation, tabs for alignment. Use spaces to build python syntax (4 spaces per level), and tabs for aligning text inside of docstrings. +#. Follow `PEP 8 `_. + + +Support of MCNP Output Files +---------------------------- +This is a common question: "Will MontePy support MCNP output files?" +The short answer is no. +This is due to a few reasons: + +#. MCNP is export controlled, and none of the public manuals document the formatting of the output files. + So out of an abundance of caution we treat the format of MCNP output files as being export controlled. +#. The output format is not documented. This makes it hard to robustly handle, and also means that the format may + change. +#. `MCNPtools `_ exists and may be a better tool than what we can implement. + + +Note on use of other languages +------------------------------ + +The use of another language in the code base goes against the principle that it should be easy to contribute. +However, there could be cases where the advantages of another language could justify this violation. +Here are guidelines for when it could be appropriate: + +#. There needs to be a clear and significant benefit. +#. The rewrite needs to be limited in scope to a module that few developers will modify. + + #. Only the input parser seems to meet this criteria. + +#. There must be a dependable build system that allows deployment to PyPI. diff --git a/doc/source/users.rst b/doc/source/users.rst new file mode 100644 index 00000000..7a572473 --- /dev/null +++ b/doc/source/users.rst @@ -0,0 +1,13 @@ +User Guide +========== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + starting + utilities + tricks + faq + changelog + publications diff --git a/montepy/__init__.py b/montepy/__init__.py index 93cf0e4f..c33e7e2c 100644 --- a/montepy/__init__.py +++ b/montepy/__init__.py @@ -33,7 +33,7 @@ from setuptools_scm import get_version __version__ = get_version() - except ImportError: + except (ImportError, LookupError): __version__ = "Undefined" diff --git a/montepy/data_inputs/isotope.py b/montepy/data_inputs/isotope.py index 5a5272a5..4303198d 100644 --- a/montepy/data_inputs/isotope.py +++ b/montepy/data_inputs/isotope.py @@ -20,20 +20,22 @@ class Isotope: def __init__(self, ZAID="", node=None): if node is not None and isinstance(node, ValueNode): + if node.type == float: + node = ValueNode(node.token, str, node.padding) self._tree = node ZAID = node.value - if "." in ZAID: - parts = ZAID.split(".") - try: - assert len(parts) == 2 - int(parts[0]) - except (AssertionError, ValueError) as e: - raise ValueError(f"ZAID: {ZAID} could not be parsed as a valid isotope") - self._ZAID = parts[0] - self.__parse_zaid() + parts = ZAID.split(".") + try: + assert len(parts) <= 2 + int(parts[0]) + except (AssertionError, ValueError) as e: + raise ValueError(f"ZAID: {ZAID} could not be parsed as a valid isotope") + self._ZAID = parts[0] + self.__parse_zaid() + if len(parts) == 2: self._library = parts[1] else: - raise ValueError(f"ZAID: {ZAID} could not be parsed as a valid isotope") + self._library = "" def __parse_zaid(self): """ diff --git a/montepy/data_inputs/material.py b/montepy/data_inputs/material.py index 44516538..75cd3413 100644 --- a/montepy/data_inputs/material.py +++ b/montepy/data_inputs/material.py @@ -3,6 +3,8 @@ from montepy.data_inputs import data_input, thermal_scattering from montepy.data_inputs.isotope import Isotope from montepy.data_inputs.material_component import MaterialComponent +from montepy.input_parser import syntax_node +from montepy.input_parser.material_parser import MaterialParser from montepy import mcnp_object from montepy.numbered_mcnp_object import Numbered_MCNP_Object from montepy.errors import * @@ -26,6 +28,8 @@ class Material(data_input.DataInputAbstract, Numbered_MCNP_Object): :type input: Input """ + _parser = MaterialParser() + def __init__(self, input=None): self._material_components = {} self._thermal_scattering = None @@ -37,7 +41,24 @@ def __init__(self, input=None): self._number = num set_atom_frac = False isotope_fractions = self._tree["data"] - for isotope_node, fraction in isotope_fractions: + if isinstance(isotope_fractions, syntax_node.ListNode): + # in python 3.12 this can be replaced with itertools.batched + def batch_gen(): + it = iter(isotope_fractions) + while batch := tuple(itertools.islice(it, 2)): + yield batch + + iterator = batch_gen() + elif isinstance(isotope_fractions, syntax_node.IsotopesNode): + iterator = iter(isotope_fractions) + else: # pragma: no cover + # this is a fall through error, that should never be raised, + # but is here just in case + raise MalformedInputError( + input, + f"Material definitions for material: {self.number} is not valid.", + ) + for isotope_node, fraction in iterator: isotope = Isotope(node=isotope_node) fraction.is_negatable_float = True if not set_atom_frac: diff --git a/montepy/input_parser/__init__.py b/montepy/input_parser/__init__.py index 487d3fcb..a80864bb 100644 --- a/montepy/input_parser/__init__.py +++ b/montepy/input_parser/__init__.py @@ -4,6 +4,7 @@ from . import cell_parser from . import data_parser from . import input_reader +from . import material_parser from . import mcnp_input from . import parser_base from . import read_parser diff --git a/montepy/input_parser/input_syntax_reader.py b/montepy/input_parser/input_syntax_reader.py index e6cf1b9e..ac202831 100644 --- a/montepy/input_parser/input_syntax_reader.py +++ b/montepy/input_parser/input_syntax_reader.py @@ -9,6 +9,7 @@ from montepy.input_parser.input_file import MCNP_InputFile from montepy.input_parser.mcnp_input import Input, Message, ReadInput, Title from montepy.input_parser.read_parser import ReadParser +from montepy.utilities import is_comment import os import warnings @@ -88,20 +89,6 @@ def read_front_matters(fh, mcnp_version): break -def is_comment(line): - """""" - upper_start = line[0 : BLANK_SPACE_CONTINUE + 1].upper() - non_blank_comment = upper_start and line.lstrip().upper().startswith("C ") - if non_blank_comment: - return True - blank_comment = ( - "C\n" == upper_start.lstrip() - or "C\r\n" == upper_start.lstrip() - or ("C" == upper_start and "\n" not in line) - ) - return blank_comment or non_blank_comment - - def read_data(fh, mcnp_version, block_type=None, recursion=False): """ Reads the bulk of an MCNP file for all of the MCNP data. diff --git a/montepy/input_parser/material_parser.py b/montepy/input_parser/material_parser.py new file mode 100644 index 00000000..f53e1b4b --- /dev/null +++ b/montepy/input_parser/material_parser.py @@ -0,0 +1,31 @@ +# Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved. +from montepy.input_parser.data_parser import DataParser +from montepy.input_parser import syntax_node + + +class MaterialParser(DataParser): + debugfile = None + + @_( + "introduction isotopes", + "introduction isotopes parameters", + ) + def material(self, p): + ret = {} + for key, node in p.introduction.nodes.items(): + ret[key] = node + ret["data"] = p.isotopes + if hasattr(p, "parameters"): + ret["parameters"] = p.parameters + return syntax_node.SyntaxNode("data", ret) + + @_("isotope_fractions", "number_sequence", "isotope_hybrid_fractions") + def isotopes(self, p): + return p[0] + + @_("number_sequence isotope_fraction", "isotope_hybrid_fractions isotope_fraction") + def isotope_hybrid_fractions(self, p): + ret = p[0] + for node in p.isotope_fraction[1:]: + ret.append(node) + return ret diff --git a/montepy/input_parser/mcnp_input.py b/montepy/input_parser/mcnp_input.py index fd533892..700c034e 100644 --- a/montepy/input_parser/mcnp_input.py +++ b/montepy/input_parser/mcnp_input.py @@ -295,16 +295,28 @@ class ReadInput(Input): def __init__(self, input_lines, block_type, input_file=None, lineno=None): super().__init__(input_lines, block_type, input_file, lineno) + if not self.is_read_input(input_lines): + raise ValueError("Not a valid Read Input") parse_result = self._parser.parse(self.tokenize(), self) - first_word = input_lines[0].split()[0].lower() if not parse_result: - if first_word != "read": - raise ValueError("Not a valid Read Input") - else: - raise ParsingError(self, "", self._parser.log.clear_queue()) + raise ParsingError(self, "", self._parser.log.clear_queue()) self._tree = parse_result self._parameters = self._tree["parameters"] + @staticmethod + def is_read_input(input_lines): + first_non_comment = "" + for line in input_lines: + if not is_comment(line): + first_non_comment = line + break + words = first_non_comment.split() + if len(words) > 0: + first_word = words[0].lower() + return first_word == "read" + # this is a fall through catch that only happens for a blank input + return False # pragma: no cover + @property def file_name(self): """ diff --git a/montepy/input_parser/parser_base.py b/montepy/input_parser/parser_base.py index 900062f6..dcd844ce 100644 --- a/montepy/input_parser/parser_base.py +++ b/montepy/input_parser/parser_base.py @@ -374,6 +374,7 @@ def file_name(self, p): "SURFACE_TYPE", "THERMAL_LAW", "ZAID", + "NUMBER_WORD", ) def file_atom(self, p): return p[0] diff --git a/montepy/utilities.py b/montepy/utilities.py index feb1a3db..e167dfef 100644 --- a/montepy/utilities.py +++ b/montepy/utilities.py @@ -1,4 +1,5 @@ # Copyright 2024, Battelle Energy Alliance, LLC All Rights Reserved. +from montepy.constants import BLANK_SPACE_CONTINUE import functools import re @@ -31,6 +32,25 @@ def fortran_float(number_string): raise ValueError(f"Value Not parsable as float: {number_string}") from e +def is_comment(line): + """ + Determines if the line is a ``C comment`` style comment. + + :param line: the line to analyze + :type line: str + :returns: True if the line is a comment + :rtype: bool + """ + upper_start = line[0 : BLANK_SPACE_CONTINUE + 1].upper() + non_blank_comment = upper_start and line.lstrip().upper().startswith("C ") + if non_blank_comment: + return True + blank_comment = ("C" == upper_start.strip() and "\n" in line) or ( + "C" == upper_start and "\n" not in line + ) + return blank_comment + + def make_prop_val_node( hidden_param, types=None, base_type=None, validator=None, deletable=False ): diff --git a/pyproject.toml b/pyproject.toml index 9fab1856..c2b13e9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ version_file = "montepy/_version.py" include = ["montepy*"] [tool.coverage.run] -omit = ["montepy/_version.py","tests/*"] +omit = ["tests/*"] [tool.coverage.report] precision = 2 diff --git a/tests/constants.py b/tests/constants.py index 7398f7e4..87ec8663 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -23,6 +23,7 @@ "testReadTarget.imcnp", "bad_encoding.imcnp", "unicode.imcnp", + "file2read.imcnp", } BAD_ENCODING_FILES = { diff --git a/tests/inputs/file2read.imcnp b/tests/inputs/file2read.imcnp new file mode 100644 index 00000000..8a2c2b33 --- /dev/null +++ b/tests/inputs/file2read.imcnp @@ -0,0 +1,2 @@ +1 0 -1 +c diff --git a/tests/inputs/readEdgeCase.imcnp b/tests/inputs/readEdgeCase.imcnp new file mode 100644 index 00000000..f06c3224 --- /dev/null +++ b/tests/inputs/readEdgeCase.imcnp @@ -0,0 +1,7 @@ +Testing read card +C this is a comment +read file=file2read.imcnp + +1 so 0.5 + +mode n diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index fabcf147..ef6ac566 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -177,3 +177,8 @@ def test_geometry_comments(self): # this step caused an error for #163 cell.comments cell._tree.format() + + def test_bad_read(self): + problem = montepy.read_input( + os.path.join("tests", "inputs", "readEdgeCase.imcnp") + ) diff --git a/tests/test_material.py b/tests/test_material.py index 56adfed9..aec22336 100644 --- a/tests/test_material.py +++ b/tests/test_material.py @@ -32,6 +32,22 @@ def test_material_init(self): for component in material.material_components: self.assertEqual(material.material_components[component].fraction, 0.5) + # test implicit library with syntax tree errors + in_str = """m1 1001 0.33 + 8016 0.666667""" + input_card = Input(in_str.split("\n"), BlockType.DATA) + material = Material(input_card) + # test implicit library + in_str = "M20 1001 0.5 2001 0.5 8016.710nc 0.5" + input_card = Input([in_str], BlockType.DATA) + material = Material(input_card) + self.assertEqual(material.number, 20) + self.assertEqual(material.old_number, 20) + self.assertTrue(material.is_atom_fraction) + for component in material.material_components: + self.assertEqual(material.material_components[component].fraction, 0.5) + + # test weight fraction in_str = "M20 1001.80c -0.5 8016.80c -0.5" input_card = Input([in_str], BlockType.DATA) material = Material(input_card) @@ -164,8 +180,6 @@ def test_isotope_init(self): Isotope("1001.80c.5") with self.assertRaises(ValueError): Isotope("hi.80c") - with self.assertRaises(ValueError): - Isotope("1001") def test_isotope_metastable_init(self): isotope = Isotope("13426.02c") diff --git a/tests/test_syntax_parsing.py b/tests/test_syntax_parsing.py index 73b6f34c..a59a4e05 100644 --- a/tests/test_syntax_parsing.py +++ b/tests/test_syntax_parsing.py @@ -1101,6 +1101,10 @@ def testReadCardConfusions(self): input = ReadInput([f"Read FILE={file}"], BlockType.CELL) self.assertEqual(input.file_name, file) + def testReadCardBadSyntax(self): + with self.assertRaises(ParsingError): + card = ReadInput(["Read 1"], BlockType.CELL) + def testTitleFinder(self): test_title = "Richard Stallman writes GNU" test_string = f"""{test_title} diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 00000000..19d8114d --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,38 @@ +import importlib +import os +import sys +from unittest import TestCase + + +class TestVersion(TestCase): + def test_version(self): + self.assertTrue(os.path.exists(os.path.join("montepy", "_version.py"))) + # Test without version.py + try: + # try without setuptools_scm + old_file = os.path.join("montepy", "_version.py") + new_file = os.path.join("montepy", "_version.bak") + os.rename(old_file, new_file) + # clear out previous imports + to_delete = set() + for mod in sys.modules: + for bad_mod in ["setuptools_scm", "montepy"]: + if bad_mod in mod: + to_delete.add(mod) + for mod in to_delete: + del sys.modules[mod] + sys.modules["setuptools_scm"] = None + import montepy + + self.assertEqual(montepy.__version__, "Undefined") + # try with setuptools_scm + del sys.modules["setuptools_scm"] + importlib.reload(montepy) + print(f"From setuptools_scm: {montepy.__version__}") + self.assertTrue(len(montepy.__version__.split(".")) >= 3) + finally: + os.rename(new_file, old_file) + # do base with _version + importlib.reload(montepy) + print(f"From _version.py: {montepy.__version__}") + self.assertTrue(len(montepy.__version__.split(".")) >= 3)