From 07f1982ae7ec3b2c19a659e0385e900b70183c19 Mon Sep 17 00:00:00 2001 From: "Zachary M. Prince" Date: Mon, 6 Oct 2025 07:43:36 -0600 Subject: [PATCH 1/3] Adding moose-python conda package #31668 --- conda/python/conda_build_config.yaml | 11 ++++ conda/python/conda_build_config.yaml.template | 11 ++++ conda/python/meta.yaml | 59 +++++++++++++++++++ conda/python/meta.yaml.template | 59 +++++++++++++++++++ conda/python/pyproject.toml | 9 +++ conda/python/pyproject.toml.template | 9 +++ scripts/versioner.yaml | 21 +++++++ 7 files changed, 179 insertions(+) create mode 100644 conda/python/conda_build_config.yaml create mode 100644 conda/python/conda_build_config.yaml.template create mode 100644 conda/python/meta.yaml create mode 100644 conda/python/meta.yaml.template create mode 100644 conda/python/pyproject.toml create mode 100644 conda/python/pyproject.toml.template diff --git a/conda/python/conda_build_config.yaml b/conda/python/conda_build_config.yaml new file mode 100644 index 000000000000..2e81129eeda6 --- /dev/null +++ b/conda/python/conda_build_config.yaml @@ -0,0 +1,11 @@ +moose_python: + - python 3.13 + - python 3.12 + - python 3.11 + - python 3.10 + +moose_pyhit: + - moose-pyhit 2025.09.18 + +moose_tools: + - moose-tools 2025.06.26 diff --git a/conda/python/conda_build_config.yaml.template b/conda/python/conda_build_config.yaml.template new file mode 100644 index 000000000000..3df7331f5d72 --- /dev/null +++ b/conda/python/conda_build_config.yaml.template @@ -0,0 +1,11 @@ +moose_python: + - python 3.13 + - python 3.12 + - python 3.11 + - python 3.10 + +moose_pyhit: + - moose-pyhit __VERSIONER_WASP_VERSION__ + +moose_tools: + - moose-tools __VERSIONER_TOOLS_VERSION__ diff --git a/conda/python/meta.yaml b/conda/python/meta.yaml new file mode 100644 index 000000000000..1078611bfa44 --- /dev/null +++ b/conda/python/meta.yaml @@ -0,0 +1,59 @@ +# Making a Change to this package? +# REMEMBER TO UPDATE the .yaml files for the following packages: +# moose/conda_build_config.yaml +# As well as any directions pertaining to modifying those files. +{% set version = "2025.10.02" %} + +package: + name: moose-python + version: {{ version }} + +source: + - path: setup.py + - path: ../../python/mooseutils + folder: src/mooseutils + - path: ../../python/MooseControl + folder: src/MooseControl + - path: ../../python/mms + folder: src/mms + - path: ../../modules/geochemistry/python + folder: src + - path: ../../modules/level_set/python + folder: src + - path: ../../modules/navier_stokes/python + folder: src + - path: ../../modules/stochastic_tools/python + folder: src + - path: ../../modules/thermal_hydraulics/python/moose_thm/__init__.py + folder: src/moose_thm + - path: ../../modules/thermal_hydraulics/python/moose_thm/thm_utilities.py + folder: src/moose_thm + +build: + number: 0 + script_env: + - REQUESTS_CA_BUNDLE + - SSL_CERT_FILE + - CURL_CA_BUNDLE + - NODE_EXTRA_CA_CERTS + script: MOOSE_PYTHON_VERSION={{ version }} python -m pip install . + +requirements: + build: + - {{ moose_python }} + - pip + - setuptools + run: + - {{ moose_pyhit }} + - {{ moose_tools }} + +test: + imports: + - mooseutils + - MooseControl + - mms + - moose_geochemistry + - moose_level_set + - moose_navier_stokes + - moose_stochastic_tools + - moose_thm diff --git a/conda/python/meta.yaml.template b/conda/python/meta.yaml.template new file mode 100644 index 000000000000..f0e6b9cbd5d8 --- /dev/null +++ b/conda/python/meta.yaml.template @@ -0,0 +1,59 @@ +# Making a Change to this package? +# REMEMBER TO UPDATE the .yaml files for the following packages: +# moose/conda_build_config.yaml +# As well as any directions pertaining to modifying those files. +{% set version = "__VERSIONER_PYTHON_VERSION__" %} + +package: + name: moose-python + version: {{ version }} + +source: + - path: pyproject.toml + - path: ../../python/mooseutils + folder: src/mooseutils + - path: ../../python/MooseControl + folder: src/MooseControl + - path: ../../python/mms + folder: src/mms + - path: ../../modules/geochemistry/python + folder: src + - path: ../../modules/level_set/python + folder: src + - path: ../../modules/navier_stokes/python + folder: src + - path: ../../modules/stochastic_tools/python + folder: src + - path: ../../modules/thermal_hydraulics/python/moose_thm/__init__.py + folder: src/moose_thm + - path: ../../modules/thermal_hydraulics/python/moose_thm/thm_utilities.py + folder: src/moose_thm + +build: + number: 0 + script_env: + - REQUESTS_CA_BUNDLE + - SSL_CERT_FILE + - CURL_CA_BUNDLE + - NODE_EXTRA_CA_CERTS + script: MOOSE_PYTHON_VERSION={{ version }} python -m pip install . + +requirements: + build: + - {{ moose_python }} + - pip + - setuptools + run: + - {{ moose_pyhit }} + - {{ moose_tools }} + +test: + imports: + - mooseutils + - MooseControl + - mms + - moose_geochemistry + - moose_level_set + - moose_navier_stokes + - moose_stochastic_tools + - moose_thm diff --git a/conda/python/pyproject.toml b/conda/python/pyproject.toml new file mode 100644 index 000000000000..e3fc224d9161 --- /dev/null +++ b/conda/python/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + + +[project] +name = "moose-python" +descriptions = "Python utilities from MOOSE" +version = "2025.10.02" diff --git a/conda/python/pyproject.toml.template b/conda/python/pyproject.toml.template new file mode 100644 index 000000000000..73cbe423a406 --- /dev/null +++ b/conda/python/pyproject.toml.template @@ -0,0 +1,9 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + + +[project] +name = "moose-python" +descriptions = "Python utilities from MOOSE" +version = "__VERSIONER_PYTHON_VERSION__" diff --git a/scripts/versioner.yaml b/scripts/versioner.yaml index 9662e182482e..8277c5bf7f0b 100644 --- a/scripts/versioner.yaml +++ b/scripts/versioner.yaml @@ -148,3 +148,24 @@ packages: - moose-dev apptainer: from: moose-dev + python: + version: 2025.10.02 + conda: conda/python + dependencies: + - wasp + - tools + templates: + conda/python/conda_build_config.yaml.template: conda/python/conda_build_config.yaml + conda/python/meta.yaml.template: conda/python/meta.yaml + conda/python/pyproject.toml.template: conda/python/pyproject.toml + influential: + - conda/python/pyproject.toml + - python/mooseutils + - python/MooseControl + - python/mms + - modules/geochemistry/python + - modules/level_set/python + - modules/navier_stokes/python + - modules/stochastic_tools/python + - modules/thermal_hydraulics/python/moose_thm/__init__.py + - modules/thermal_hydraulics/python/moose_thm/thm_utilities.py From a89ce56b95ddcf9ba9c6425a55339659b301054d Mon Sep 17 00:00:00 2001 From: "Zachary M. Prince" Date: Mon, 6 Oct 2025 07:51:14 -0600 Subject: [PATCH 2/3] Changes to versioner.py for adding a new package #31668 --- scripts/versioner.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/scripts/versioner.py b/scripts/versioner.py index 0de6a4ec4be1..88379efd1e85 100755 --- a/scripts/versioner.py +++ b/scripts/versioner.py @@ -170,7 +170,7 @@ class AppInfo: ### Unittest Tracking Libraries (versioner_hashes.yaml) # note: Order is important -TRACKING_LIBRARIES = ['tools', 'mpi', 'petsc', 'libmesh', 'wasp', 'moose-dev', 'app'] +TRACKING_LIBRARIES = ['tools', 'mpi', 'petsc', 'libmesh', 'wasp', 'moose-dev', 'app', 'python'] ### Additional libraries for tracking_libraries, necessary to build the moose-dev Conda stack # This allows returning a proper verification @@ -243,7 +243,7 @@ def print_result(data, header, keys, summary): tablefmt = 'github' if brief else 'rounded_grid' contents = '' length = 0 - if brief: + if brief or not data: contents = f'{summary}\n' if data: table = tabulate(data, headers=keys, tablefmt=tablefmt) @@ -288,9 +288,9 @@ def colorize(value, color): influential_removed = 0 entries = [] for package in head.values(): + influential = package.influential[package.name] base_package = base.get(package.name) if base_package is not None: - influential = package.influential[package.name] base_influential = base_package.influential.get(package.name, {}) for file, hash in influential.items(): base_hash = base_influential.get(file) @@ -306,6 +306,10 @@ def colorize(value, color): if file not in influential: influential_removed += 1 entries.append([package.name, colorize('REMOVED', 'YELLOW'), file]) + else: + influential_new += len(influential) + entries += [[package.name, colorize('NEW', 'YELLOW'), file] for file in influential.keys()] + status = f'Found {len(all_influential)} influential files, {influential_change} changed, ' status += f'{influential_new} added, {influential_removed} removed' print_result(entries, @@ -326,8 +330,6 @@ def colorize(value, color): num_packages += 1 head_conda = head_package.conda - base_package = base[name] - base_conda = base_package.conda status = 'OK' status_color = 'GREEN' @@ -337,8 +339,17 @@ def colorize(value, color): version_color = None build_color = None + base_package = base.get(name, None) + base_conda = base_package.conda if base_package else None + + # This is a new package + if base_package is None: + num_packages_changed += 1 + status = 'NEW' + status_color = 'CYAN' + # Hashes are different, something changed - if base_package.hash != head_package.hash: + elif base_package.hash != head_package.hash: num_packages_changed += 1 hash_color = 'YELLOW' @@ -383,21 +394,27 @@ def colorize(value, color): build_color = 'RED' # Something went wrong, make status and package red - if status not in ['OK', 'CHANGE']: + if status not in ['OK', 'CHANGE', 'NEW']: package_color = 'RED' status_color = 'RED' num_packages_failed += 1 if not brief or status != 'OK': - base_version = base_conda.version - if base_package.build_number is not None: - base_version += f' build {base_package.build_number}' + if base_package is None: + base_version = 'none' + base_hash = 'none' + else: + base_version = base_conda.version + if base_package.build_number is not None: + base_version += f' build {base_package.build_number}' + base_hash = base_package.hash + head_version = colorize(head_conda.version, version_color) if head_package.build_number is not None: head_version += colorize(f' build {head_package.build_number}', build_color) entries.append([colorize(name, package_color), colorize(status, status_color), - base_package.hash, + base_hash, colorize(head_package.hash, hash_color), base_version, head_version]) From fe447df34f165382388751c49aa7f9769245ee8d Mon Sep 17 00:00:00 2001 From: "Zachary M. Prince" Date: Mon, 6 Oct 2025 11:54:54 -0600 Subject: [PATCH 3/3] Adding tests for versioner verify #31668 --- scripts/tests/test_versioner.py | 256 +++++++++++++++++++++++++++++++- scripts/versioner.py | 6 +- 2 files changed, 257 insertions(+), 5 deletions(-) diff --git a/scripts/tests/test_versioner.py b/scripts/tests/test_versioner.py index 98f1e76a7275..31d3ab364986 100644 --- a/scripts/tests/test_versioner.py +++ b/scripts/tests/test_versioner.py @@ -14,11 +14,14 @@ import yaml import subprocess # for assertRaises import mooseutils -from mock import patch +from mock import patch, MagicMock +from io import StringIO +import tempfile +import shutil MOOSE_DIR = mooseutils.git_root_dir() sys.path.insert(0, os.path.join(MOOSE_DIR, 'scripts')) -from versioner import Versioner +from versioner import Versioner, Package, CondaPackage, MOOSE_DIR with open(os.path.join(MOOSE_DIR, 'scripts', 'tests', 'versioner_hashes.yaml'), 'r') as stream: OLD_HASHES = yaml.safe_load(stream) @@ -150,5 +153,254 @@ def testMatchDate(self): self.assertEqual(Versioner.match_date('xxx2025.04.04xxx'), (2025, 4, 4)) self.assertEqual(Versioner.match_date('20.01.01'), None) +class TestVerify(unittest.TestCase): + def setUp(self): + # Mock CLI args sent to verify_recipes + self.args = MagicMock(verify="qwertyuiop", brief=False) + # Remove color from tables for easy comparison + self._colorText_patch = patch("versioner.colorText", side_effect=lambda s, c: s) + self._colorText_mock = self._colorText_patch.start() + # Don't actually tabulate, we'll just get the args used + self._tabulate_patch = patch("tabulate.tabulate") + self.tabulate_mock = self._tabulate_patch.start() + # Mock stdout so we can more easily get print statements + self._stdout_patcher = patch("sys.stdout", new=StringIO()) + self.stdout_mock: StringIO = self._stdout_patcher.start() + # Temporary directory + self._temp_dir = tempfile.mkdtemp() + + def tearDown(self): + self._colorText_patch.stop() + self._tabulate_patch.stop() + self._stdout_patcher.stop() + shutil.rmtree(self._temp_dir, ignore_errors=True) + + class MockGetPackages: + def __init__(self, head: list[Package], base: list[Package]): + self.head = {p.name: p for p in head} + self.base = {p.name: p for p in base} + + def get_packages(self, ref: str) -> dict[str, Package]: + return self.head if ref == "HEAD" else self.base + + @staticmethod + def buildMockPackage(**kwargs) -> Package: + name = kwargs.get("name", "mock") + influential = kwargs.get("influential", {name: {}}) + conda = CondaPackage( + name=name, + version=kwargs.get("version", "1993.11.03"), + build_number=kwargs.get("build_number", None), + build_string=str(kwargs.get("build_number", "")) or None, + install=kwargs.get("version", "1993.11.03"), + conda_dir=None, + meta={}, + ) + return Package( + name=name, + config=kwargs.get("config", {}), + version=kwargs.get("version", "1993.11.03"), + build_number=kwargs.get("build_number", None), + full_version=kwargs.get("full_version", "1993.11.03"), + hash=kwargs.get("hash", "asdfghjkl"), + all_influential=kwargs.get("all_influential", influential[name]), + influential=kwargs.get("influential", influential), + dependencies=kwargs.get("dependencies", []), + templates=kwargs.get("templates", {}), + apptainer=kwargs.get("apptainer", None), + conda=conda, + is_app=kwargs.get("is_app", False), + ) + + @patch("versioner.Versioner.get_packages") + def testNoChanges(self, mock_get_packages): + head = [self.buildMockPackage()] + base = [self.buildMockPackage()] + mock_get_packages.side_effect = self.MockGetPackages(head, base).get_packages + + Versioner().verify_recipes(self.args) + output = self.stdout_mock.getvalue() + + # All table headers are there + self.assertIn("Versioner templates", output) + self.assertIn("Versioner influential files", output) + self.assertIn("Versioner versions", output) + + # Check summaries + self.assertIn("Found 0 templates, 0 failed", output) + self.assertIn("Found 0 influential files, 0 changed, 0 added, 0 removed", output) + + # Check version table + data = self.tabulate_mock.call_args_list[0][0][0] + headers = self.tabulate_mock.call_args_list[0][1]["headers"] + self.assertListEqual(data, [['mock', 'OK', 'asdfghjkl', 'asdfghjkl', '1993.11.03', '1993.11.03']]) + self.assertListEqual(headers, ['package', 'status', 'hash', 'to hash', 'version', 'to version']) + + # Check success message + self.assertIn("Verification succeeded.", output) + + @patch("versioner.Versioner.get_packages") + def testSuccessfulChange(self, mock_get_packages): + head = [self.buildMockPackage(version="2019.06.10", hash="qwertyuiop")] + base = [self.buildMockPackage()] + mock_get_packages.side_effect = self.MockGetPackages(head, base).get_packages + + Versioner().verify_recipes(self.args) + output = self.stdout_mock.getvalue() + + data = self.tabulate_mock.call_args_list[0][0][0] + headers = self.tabulate_mock.call_args_list[0][1]["headers"] + self.assertListEqual(data, [['mock', 'CHANGE', 'asdfghjkl', 'qwertyuiop', '1993.11.03', '2019.06.10']]) + self.assertListEqual(headers, ['package', 'status', 'hash', 'to hash', 'version', 'to version']) + + self.assertIn("Changes were found in 1 packages.", output) + self.assertIn("Verification succeeded.", output) + + @patch("versioner.Versioner.get_packages") + def testNewPackage(self, mock_get_packages): + head = [ + self.buildMockPackage(), + self.buildMockPackage(name="sparkly"), + ] + base = [self.buildMockPackage()] + mock_get_packages.side_effect = self.MockGetPackages(head, base).get_packages + + Versioner().verify_recipes(self.args) + output = self.stdout_mock.getvalue() + + data = self.tabulate_mock.call_args_list[0][0][0] + headers = self.tabulate_mock.call_args_list[0][1]["headers"] + self.assertListEqual(headers, ['package', 'status', 'hash', 'to hash', 'version', 'to version']) + self.assertListEqual( + data, + [ + ['mock', 'OK', 'asdfghjkl', 'asdfghjkl', '1993.11.03', '1993.11.03'], + ['sparkly', 'NEW', 'none', 'asdfghjkl', 'none', '1993.11.03'], + ] + ) + + self.assertIn("Changes were found in 1 packages.", output) + self.assertIn("Verification succeeded.", output) + + def testBadChanges(self): + + def basicCheck(head, base) -> list[list[str]]: + self.stdout_mock.truncate(0) + with patch("versioner.Versioner.get_packages") as mock_get_packages: + mock_get_packages.side_effect = self.MockGetPackages([head], [base]).get_packages + with self.assertRaises(SystemExit): + Versioner().verify_recipes(self.args) + output = self.stdout_mock.getvalue() + headers = self.tabulate_mock.call_args_list[0][1]["headers"] + self.assertListEqual(headers, ['package', 'status', 'hash', 'to hash', 'version', 'to version']) + self.assertIn("Changes were found in 1 packages.", output) + self.assertIn("Verification failed.", output) + return self.tabulate_mock.call_args_list[-1][0][0] + + # Hash changed but no version bump + data = basicCheck(self.buildMockPackage(hash="qwertyuiop"), self.buildMockPackage()) + self.assertListEqual(data, [['mock', 'NEED BUMP', 'asdfghjkl', 'qwertyuiop', '1993.11.03', '1993.11.03']]) + + # Date decrease + data = basicCheck(self.buildMockPackage(version="1991.05.21", hash="qwertyuiop"), self.buildMockPackage()) + self.assertListEqual(data, [['mock', 'DATE DECREASE', 'asdfghjkl', 'qwertyuiop', '1993.11.03', '1991.05.21']]) + + # Future date + data = basicCheck(self.buildMockPackage(version="3001.01.01", hash="qwertyuiop"), self.buildMockPackage()) + self.assertListEqual(data, [['mock', 'FUTURE DATE', 'asdfghjkl', 'qwertyuiop', '1993.11.03', '3001.01.01']]) + + # Build non-zero + data = basicCheck(self.buildMockPackage(version="2019.06.10", hash="qwertyuiop", build_number=1), self.buildMockPackage()) + self.assertListEqual(data, [['mock', 'BUILD NONZERO', 'asdfghjkl', 'qwertyuiop', '1993.11.03', '2019.06.10 build 1']]) + + @patch("versioner.Versioner.git_file") + @patch("versioner.Versioner.get_packages") + def testTemplate(self, mock_get_packages, mock_git_file): + # Don't search commit and just read the file + def mockGitFile(file, commit, *args, **kwargs): + with open(file) as fid: + content = fid.read() + return content + mock_git_file.side_effect = mockGitFile + + # File paths of template-file pair + file = os.path.join(self._temp_dir, "mock.yaml") + template_file = os.path.join(self._temp_dir, "mock.yaml.template") + rel_template_file = os.path.relpath(template_file, MOOSE_DIR) + templates = {rel_template_file: file} + + # Create packages with templates + head = [self.buildMockPackage(templates=templates)] + base = [self.buildMockPackage(templates=templates)] + mock_get_packages.side_effect = self.MockGetPackages(head, base).get_packages + + # Good template + with open(template_file, "w") as fid: + fid.write("__VERSIONER_MOCK_VERSION__") + with open(file, "w") as fid: + fid.write("1993.11.03") + Versioner().verify_recipes(self.args) + output = self.stdout_mock.getvalue() + headers = self.tabulate_mock.call_args_list[-3][1]["headers"] + data = self.tabulate_mock.call_args_list[-3][0][0] + self.assertListEqual(headers, ['package', 'status', 'file', 'from']) + self.assertListEqual(data, [['mock', 'OK', file, rel_template_file]]) + self.assertIn("Verification succeeded.", output) + + # Bad template + self.stdout_mock.truncate(0) + with open(file, "w") as fid: + fid.write("2019.06.10") + with self.assertRaises(SystemExit): + Versioner().verify_recipes(self.args) + output = self.stdout_mock.getvalue() + headers = self.tabulate_mock.call_args_list[-3][1]["headers"] + data = self.tabulate_mock.call_args_list[-3][0][0] + self.assertListEqual(headers, ['package', 'status', 'file', 'from']) + self.assertListEqual(data, [['mock', 'BEHIND', file, rel_template_file]]) + self.assertIn("Verification failed.", output) + + # Unused template variable + self.stdout_mock.truncate(0) + with open(template_file, "w") as fid: + fid.write("__VERSIONER_MOCK_FOOBAR__") + with self.assertRaises(SystemExit): + Versioner().verify_recipes(self.args) + output = self.stdout_mock.getvalue() + self.assertIn(f"Unused template variable __VERSIONER_MOCK_FOOBAR__ still exists in {rel_template_file}", output) + + + @patch("versioner.Versioner.get_packages") + def testInfluential(self, mock_get_packages): + head = [ + self.buildMockPackage(influential={"mock": { + "not_changed": "qwerty", + "changed": "qwerty", + "added": "qwerty", + }}), + self.buildMockPackage(name="sparkly", influential={"sparkly": { + "influence": "qwerty", + }}), + ] + base = [self.buildMockPackage(influential={"mock": { + "not_changed": "qwerty", + "changed": "asdfgh", + "removed": "qwerty", + }})] + mock_get_packages.side_effect = self.MockGetPackages(head, base).get_packages + + Versioner().verify_recipes(self.args) + headers = self.tabulate_mock.call_args_list[0][1]["headers"] + self.assertListEqual(headers, ['package', 'status', 'file']) + data = self.tabulate_mock.call_args_list[0][0][0] + self.assertIn(['mock', 'CHANGE', 'changed'], data) + self.assertIn(['mock', 'NEW', 'added'], data) + self.assertIn(['mock', 'REMOVED', 'removed'], data) + self.assertIn(['sparkly', 'NEW', 'influence'], data) + + output = self.stdout_mock.getvalue() + self.assertIn("Verification succeeded.", output) + + if __name__ == '__main__': unittest.main(verbosity=2, buffer=True) diff --git a/scripts/versioner.py b/scripts/versioner.py index 88379efd1e85..db1ea315b33d 100755 --- a/scripts/versioner.py +++ b/scripts/versioner.py @@ -269,7 +269,7 @@ def colorize(value, color): if not changes and brief: continue change_text = 'BEHIND' if changes else colorize('OK', 'GREEN') - entries.append((package.name, change_text, to_path, template_path)) + entries.append([package.name, change_text, to_path, template_path]) if changes: num_templates_failed += 1 entries[-1] = [colorize(v, 'RED') for v in entries[-1]] @@ -377,8 +377,8 @@ def colorize(value, color): version_color = 'RED' status = 'FUTURE DATE' # Version is bumped, but build isn't zero - elif head_package.build_number is not None \ - and head_package.build_number != 0: + if head_package.build_number is not None \ + and head_package.build_number != 0 and version_color != 'RED': build_color = 'RED' status = 'BUILD NONZERO' # Version is bumped correctly