diff --git a/src/layup/utilities/data_utilities_for_tests.py b/src/layup/utilities/data_utilities_for_tests.py index 7afa481c..e12ede27 100644 --- a/src/layup/utilities/data_utilities_for_tests.py +++ b/src/layup/utilities/data_utilities_for_tests.py @@ -44,3 +44,23 @@ def get_test_filepath(filename): # Returned path: `/tests/data/filename` return os.path.join(THIS_DIR, "tests/data", filename) + + +def layup_cli(*args): + """Build an argv that runs this environment's ``layup``, not ``PATH``'s. + + ``subprocess.run(["layup", ...])`` resolves the name against ``PATH``, so + the test runs whichever layup comes first on the machine instead of the one + being tested. On a machine with an older layup installed the test fails on + arguments the current code added, and -- worse -- when the older one happens + to accept them, it passes without testing anything (issue #500). + + Console scripts are installed next to the interpreter, so resolving from + ``sys.executable`` pins the call to this environment while still going + through the real entry point and its verb dispatch. + """ + import sys + from pathlib import Path + + exe = Path(sys.executable).parent / "layup" + return [str(exe) if exe.exists() else "layup", *args] diff --git a/src/layup_cmdline/main.py b/src/layup_cmdline/main.py index 6bbb262a..958f2a88 100644 --- a/src/layup_cmdline/main.py +++ b/src/layup_cmdline/main.py @@ -1,8 +1,6 @@ import argparse -import subprocess import sys -import shutil -import os +from importlib.metadata import distribution # # Generic verb dispatcher code @@ -10,14 +8,17 @@ def find_layup_verbs(): - """Find available layup commands in the system's PATH.""" - layup_verbs = [] - for directory in os.environ.get("PATH", "").split(os.pathsep): - if os.path.isdir(directory): - for item in os.listdir(directory): - if item.startswith("layup-") and os.access(os.path.join(directory, item), os.X_OK): - layup_verbs.append(item[len("layup-") :]) - return sorted(set(layup_verbs)) + """Return the verbs this installation provides, as a dict mapping verb name + to entry point. + + The names come from the installed package's own metadata, so they are the + verbs belonging to this layup, not whichever ones happen to be first on PATH. + """ + verbs = {} + for ep in distribution("layup").entry_points: + if ep.group == "console_scripts" and ep.name.startswith("layup-"): + verbs[ep.name[len("layup-") :]] = ep + return verbs def main(): @@ -58,7 +59,7 @@ def main(): action="store_true", ) - parser.add_argument("verb", nargs="?", choices=available_verbs, help="Verb to execute") + parser.add_argument("verb", nargs="?", choices=sorted(available_verbs), help="Verb to execute") parser.add_argument("args", nargs=argparse.REMAINDER, help="Arguments for the verb") args = parser.parse_args() @@ -75,21 +76,27 @@ def main(): parser.print_help() sys.exit(1) - # Construct the full command name utility = f"layup-{args.verb}" - - # Ensure the command is available - if not shutil.which(utility): + entry = available_verbs.get(args.verb) + if entry is None: print(f"Error: '{utility}' is not available.") sys.exit(1) - # Execute the command with the remaining arguments + # Run the verb in this process. Nothing is resolved by name, so the verb + # that runs is always the one belonging to this installation. + verb_main = entry.load() + argv = sys.argv + sys.argv = [utility, *args.args] try: - result = subprocess.run([utility] + args.args, check=True) - sys.exit(result.returncode) - except subprocess.CalledProcessError as e: - print(f"Error: Command '{utility}' failed with exit code {e.returncode}.") - sys.exit(e.returncode) + code = verb_main() + except SystemExit as exc: # the verbs exit on their own error paths + code = exc.code + finally: + sys.argv = argv + if code not in (0, None): + print(f"Error: Command '{utility}' failed with exit code {code}.") + sys.exit(code) + sys.exit(0) if __name__ == "__main__": diff --git a/tests/layup/test_comet.py b/tests/layup/test_comet.py index 24983ca7..e4855a0a 100644 --- a/tests/layup/test_comet.py +++ b/tests/layup/test_comet.py @@ -6,7 +6,7 @@ import numpy as np from numpy.testing import assert_allclose, assert_equal from layup.comet import _remove_spc, _assist_integrate, _direction_of_integration, _apply_comet, comet_cli -from layup.utilities.data_utilities_for_tests import get_test_filepath +from layup.utilities.data_utilities_for_tests import get_test_filepath, layup_cli from layup.utilities.file_io.CSVReader import CSVDataReader import pandas as pd import assist @@ -214,7 +214,7 @@ def test_comet_output(tmpdir): # The demo comet fixture is keyed by ObjID; comet's -pid now defaults to # provID (CLI-consistency), so pass -pid ObjID explicitly. result = subprocess.run( - ["layup", "comet", str(input_file), "-f", "--stem", str(temp_out_file), "-pid", "ObjID"] + layup_cli("comet", str(input_file), "-f", "--stem", str(temp_out_file), "-pid", "ObjID") ) assert result.returncode == 0 diff --git a/tests/layup/test_predict.py b/tests/layup/test_predict.py index 94309239..e7b4e9a1 100644 --- a/tests/layup/test_predict.py +++ b/tests/layup/test_predict.py @@ -15,7 +15,7 @@ layup_get_residual_vectors, layup_calculate_rates_and_geometry, ) -from layup.utilities.data_utilities_for_tests import get_test_filepath +from layup.utilities.data_utilities_for_tests import get_test_filepath, layup_cli from layup.utilities.file_io.CSVReader import CSVDataReader @@ -221,8 +221,7 @@ def test_predict_output(tmpdir): temp_out_file = f"test_output_{input_file.stem}" result = subprocess.run( - [ - "layup", + layup_cli( "predict", str(input_file), "-f", @@ -232,7 +231,7 @@ def test_predict_output(tmpdir): str(tmpdir), "-s", start, - ] + ) ) assert result.returncode == 0 @@ -292,8 +291,7 @@ def test_predict_output(tmpdir): # Testing the output of the sexagesimal conversion separately result = subprocess.run( - [ - "layup", + layup_cli( "predict", str(input_file), "-f", @@ -302,7 +300,7 @@ def test_predict_output(tmpdir): "-s", start, "-sg", - ] + ) ) assert result.returncode == 0 @@ -431,8 +429,7 @@ def test_get_onsky_data_output(tmpdir): temp_out_file = f"test_output_{input_file.stem}" result = subprocess.run( - [ - "layup", + layup_cli( "predict", str(input_file), "-f", @@ -443,7 +440,7 @@ def test_get_onsky_data_output(tmpdir): "-s", start, "-osd", - ] + ) ) assert result.returncode == 0 result_file = Path(f"{tmpdir}/{temp_out_file}.csv")