diff --git a/CHANGES.md b/CHANGES.md index cefa88b..05e8503 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,9 @@ - Fix #46: Git tags in branch option are now correctly detected and handled during updates. Previously, updating from one tag to another failed because tags were incorrectly treated as branches. [jensens] +- Fix #22 and #25: Constraints file path in requirements-out is now correctly calculated as a relative path from the requirements file's directory. This allows requirements and constraints files to be in different directories. Previously, the path was written from the config file's perspective, causing pip to fail when looking for the constraints file. On Windows, paths are now normalized to use forward slashes for pip compatibility. + [jensens] + - Fix #53: Per-package target setting now correctly overrides default-target when constructing checkout paths. [jensens] diff --git a/src/mxdev/processing.py b/src/mxdev/processing.py index b11e0bb..1847e20 100644 --- a/src/mxdev/processing.py +++ b/src/mxdev/processing.py @@ -7,6 +7,7 @@ from urllib import request from urllib.error import URLError +import os import typing @@ -271,9 +272,26 @@ def write(state: State) -> None: logger.info(f"Write [r]: {cfg.out_requirements}") with open(cfg.out_requirements, "w") as fio: if constraints or cfg.overrides: + # Calculate relative path from requirements-out directory to constraints-out file + # This ensures pip can find the constraints file regardless of where requirements + # and constraints files are located + req_path = Path(cfg.out_requirements) + const_path = Path(cfg.out_constraints) + + # Calculate relative path from requirements directory to constraints file + try: + constraints_ref = os.path.relpath(const_path, req_path.parent) + # Convert backslashes to forward slashes for pip compatibility + # pip expects forward slashes even on Windows + constraints_ref = constraints_ref.replace("\\", "/") + except ValueError: + # On Windows, relpath can fail if paths are on different drives + # In that case, use absolute path with forward slashes + constraints_ref = str(const_path.absolute()).replace("\\", "/") + fio.write("#" * 79 + "\n") fio.write("# mxdev combined constraints\n") - fio.write(f"-c {cfg.out_constraints}\n\n") + fio.write(f"-c {constraints_ref}\n\n") write_dev_sources(fio, cfg.packages) fio.writelines(requirements) write_main_package(fio, cfg.settings) diff --git a/tests/test_processing.py b/tests/test_processing.py index ea3662f..8b15075 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -1,3 +1,4 @@ +import os import pathlib import pytest from io import StringIO @@ -407,3 +408,114 @@ def test_write_no_constraints(tmp_path): assert not const_file.exists() finally: os.chdir(old_cwd) + + +def test_relative_constraints_path_in_subdirectory(tmp_path): + """Test that constraints path in requirements-out is relative to requirements file location. + + This reproduces issue #22: when requirements-out and constraints-out are in subdirectories, + the constraints reference should be relative to the requirements file's directory. + """ + from mxdev.processing import read, write + from mxdev.state import State + from mxdev.config import Configuration + + old_cwd = os.getcwd() + try: + os.chdir(tmp_path) + + # Create subdirectory for output files + (tmp_path / "requirements").mkdir() + + # Create input constraints file + constraints_in = tmp_path / "constraints.txt" + constraints_in.write_text("requests==2.28.0\nurllib3==1.26.9\n") + + # Create input requirements file with a constraint reference + requirements_in = tmp_path / "requirements.txt" + requirements_in.write_text("-c constraints.txt\nrequests\n") + + # Create config with both output files in subdirectory + config_file = tmp_path / "mx.ini" + config_file.write_text( + """[settings] +requirements-in = requirements.txt +requirements-out = requirements/plone.txt +constraints-out = requirements/constraints.txt +""" + ) + + config = Configuration(str(config_file)) + state = State(configuration=config) + + # Read and write + read(state) + write(state) + + # Check requirements file contains relative path to constraints + req_file = tmp_path / "requirements" / "plone.txt" + assert req_file.exists() + req_content = req_file.read_text() + + # Bug: Currently writes "-c requirements/constraints.txt" + # Expected: Should write "-c constraints.txt" (relative to requirements file's directory) + assert "-c constraints.txt\n" in req_content, ( + f"Expected '-c constraints.txt' (relative path), " + f"but got:\n{req_content}" + ) + + # Should NOT contain the full path from config file's perspective + assert "-c requirements/constraints.txt" not in req_content + finally: + os.chdir(old_cwd) + + +def test_relative_constraints_path_different_directories(tmp_path): + """Test constraints path when requirements and constraints are in different directories.""" + from mxdev.processing import read, write + from mxdev.state import State + from mxdev.config import Configuration + + old_cwd = os.getcwd() + try: + os.chdir(tmp_path) + + # Create different subdirectories + (tmp_path / "reqs").mkdir() + (tmp_path / "constraints").mkdir() + + # Create input constraints file + constraints_in = tmp_path / "constraints.txt" + constraints_in.write_text("requests==2.28.0\nurllib3==1.26.9\n") + + # Create input requirements file with a constraint reference + requirements_in = tmp_path / "requirements.txt" + requirements_in.write_text("-c constraints.txt\nrequests\n") + + config_file = tmp_path / "mx.ini" + config_file.write_text( + """[settings] +requirements-in = requirements.txt +requirements-out = reqs/requirements.txt +constraints-out = constraints/constraints.txt +""" + ) + + config = Configuration(str(config_file)) + state = State(configuration=config) + + read(state) + write(state) + + req_file = tmp_path / "reqs" / "requirements.txt" + assert req_file.exists() + req_content = req_file.read_text() + + # Should write path relative to reqs/ directory + # From reqs/ to constraints/constraints.txt = ../constraints/constraints.txt + assert "-c ../constraints/constraints.txt\n" in req_content, ( + f"Expected '-c ../constraints/constraints.txt' (relative path), " + f"but got:\n{req_content}" + ) + finally: + os.chdir(old_cwd)