diff --git a/pyproject.toml b/pyproject.toml index 62c17cbe..761f28a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -314,6 +314,7 @@ known-local-folder = ["pathutils"] "docs/conf.py" = ["TID251"] "docs/examples/**" = ["ANN"] "src/scikit_build_core/file_api/model/*.py" = ["N"] +"**/__main__.py" = ["T20"] [tool.check-sdist] diff --git a/src/scikit_build_core/_logging.py b/src/scikit_build_core/_logging.py index cfa6bf6f..ebe2a250 100644 --- a/src/scikit_build_core/_logging.py +++ b/src/scikit_build_core/_logging.py @@ -345,9 +345,16 @@ def rich_warning( color: Literal[ "", "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white" ] = "yellow", + file: object = None, **kwargs: object, ) -> None: - rich_print("{bold.yellow}WARNING:", *args, color=color, **kwargs) # type: ignore[arg-type] + rich_print( + "{bold.yellow}WARNING:", + *args, + color=color, + file=file or sys.stderr, + **kwargs, # type: ignore[arg-type] + ) def rich_error( @@ -355,7 +362,14 @@ def rich_error( color: Literal[ "", "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white" ] = "red", + file: object = None, **kwargs: object, ) -> NoReturn: - rich_print("{bold.red}ERROR:", *args, color=color, **kwargs) # type: ignore[arg-type] + rich_print( + "{bold.red}ERROR:", + *args, + color=color, + file=file or sys.stderr, + **kwargs, # type: ignore[arg-type] + ) raise SystemExit(7) diff --git a/src/scikit_build_core/build/__main__.py b/src/scikit_build_core/build/__main__.py new file mode 100644 index 00000000..0fa28fda --- /dev/null +++ b/src/scikit_build_core/build/__main__.py @@ -0,0 +1,81 @@ +import argparse +import json +from pathlib import Path +from typing import Literal + +from .._compat import tomllib +from .._logging import rich_warning +from ..builder._load_provider import process_dynamic_metadata +from . import ( + get_requires_for_build_editable, + get_requires_for_build_sdist, + get_requires_for_build_wheel, +) + + +def main_project_table(_args: argparse.Namespace, /) -> None: + """Get the full project table, including dynamic metadata.""" + with Path("pyproject.toml").open("rb") as f: + pyproject = tomllib.load(f) + + project = pyproject.get("project", {}) + metadata = pyproject.get("tool", {}).get("scikit-build", {}).get("metadata", {}) + new_project = process_dynamic_metadata(project, metadata) + print(json.dumps(new_project, indent=2)) + + +def main_requires(args: argparse.Namespace, /) -> None: + get_requires(args.mode) + + +def get_requires(mode: Literal["sdist", "wheel", "editable"]) -> None: + """Get the build requirements.""" + + with Path("pyproject.toml").open("rb") as f: + pyproject = tomllib.load(f) + + requires = pyproject.get("build-system", {}).get("requires", []) + backend = pyproject.get("build-system", {}).get("build-backend", "") + if backend != "scikit_build_core.build": + rich_warning("Might not be a scikit-build-core project.") + if mode == "sdist": + requires += get_requires_for_build_sdist({}) + elif mode == "wheel": + requires += get_requires_for_build_wheel({}) + elif mode == "editable": + requires += get_requires_for_build_editable({}) + print(json.dumps(sorted(set(requires)), indent=2)) + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Build backend utilities", + ) + + subparsers = parser.add_subparsers(help="Commands") + requires = subparsers.add_parser( + "requires", + help="Get the build requirements", + description="Includes the static build requirements, the dynamically generated ones, and dynamic-metadata ones.", + ) + requires.set_defaults(func=main_requires) + requires.add_argument( + "--mode", + choices=["sdist", "wheel", "editable"], + default="wheel", + help="The build mode to get the requirements for", + ) + + project_table = subparsers.add_parser( + "project-table", + help="Get the full project table, including dynamic metadata", + description="Processes static and dynamic metadata without triggering the backend, only handles scikit-build-core's dynamic metadata.", + ) + project_table.set_defaults(func=main_project_table) + + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/tests/test_broken_fallback.py b/tests/test_broken_fallback.py index 0c7b6d6b..3730037c 100644 --- a/tests/test_broken_fallback.py +++ b/tests/test_broken_fallback.py @@ -50,8 +50,8 @@ def test_fail_setting( build_wheel("dist") assert exc.value.code == 7 - out, _ = capsys.readouterr() - assert "fail setting was enabled" in out + _, err = capsys.readouterr() + assert "fail setting was enabled" in err @pytest.mark.usefixtures("broken_fallback") diff --git a/tests/test_build_cli.py b/tests/test_build_cli.py new file mode 100644 index 00000000..ad5ee6c0 --- /dev/null +++ b/tests/test_build_cli.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import json +import shutil +import sys +import sysconfig +from typing import TYPE_CHECKING + +import pytest + +from scikit_build_core._logging import rich_warning +from scikit_build_core.build.__main__ import main + +if TYPE_CHECKING: + from pathlib import Path + +PYPROJECT_1 = """ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" +[project] +name = "test" +dynamic = ["version"] + +[tool.scikit-build.metadata.version] +provider = "scikit_build_core.metadata.setuptools_scm" +""" + + +@pytest.mark.parametrize("mode", ["sdist", "wheel", "editable"]) +def test_requires_command( + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + mode: str, +) -> None: + monkeypatch.setattr( + sys, "argv", ["scikit_build_core.build", "requires", f"--mode={mode}"] + ) + monkeypatch.setattr(shutil, "which", lambda _: None) + (tmp_path / "pyproject.toml").write_text(PYPROJECT_1) + monkeypatch.chdir(tmp_path) + + main() + rich_warning.cache_clear() + out, err = capsys.readouterr() + assert "CMakeLists.txt not found" in err + jout = json.loads(out) + if mode == "sdist": + assert frozenset(jout) == {"scikit-build-core", "setuptools-scm"} + elif sysconfig.get_platform().startswith("win-"): + assert frozenset(jout) == { + "cmake>=3.15", + "scikit-build-core", + "setuptools-scm", + } + else: + assert frozenset(jout) == { + "cmake>=3.15", + "ninja>=1.5", + "scikit-build-core", + "setuptools-scm", + } + + +PYPROJECT_2 = """ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" +[project] +name = "test" +dynamic = ["version", "dependencies"] + +[tool.scikit-build.metadata.version] +provider = "scikit_build_core.metadata.regex" +input = "version.py" + +[tool.scikit-build.metadata.dependencies] +provider = "scikit_build_core.metadata.template" +result = ["self=={project[version]}"] +""" + + +def test_metadata_command( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setattr(sys, "argv", ["scikit_build_core.build", "project-table"]) + monkeypatch.setattr(shutil, "which", lambda _: None) + (tmp_path / "pyproject.toml").write_text(PYPROJECT_2) + (tmp_path / "version.py").write_text("version = '0.1.3'") + monkeypatch.chdir(tmp_path) + + main() + out, _ = capsys.readouterr() + jout = json.loads(out) + assert jout == { + "name": "test", + "version": "0.1.3", + "dynamic": [], + "dependencies": ["self==0.1.3"], + } diff --git a/tests/test_printouts.py b/tests/test_printouts.py index cac8e5c8..0e3b6dad 100644 --- a/tests/test_printouts.py +++ b/tests/test_printouts.py @@ -1,7 +1,14 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from scikit_build_core.builder.__main__ import main +if TYPE_CHECKING: + import pytest + -def test_builder_printout(capsys): +def test_builder_printout(capsys: pytest.CaptureFixture[str]) -> None: main() out, err = capsys.readouterr() assert "Detected Python Library" in out diff --git a/tests/test_skbuild_settings.py b/tests/test_skbuild_settings.py index 5da91990..2aff167a 100644 --- a/tests/test_skbuild_settings.py +++ b/tests/test_skbuild_settings.py @@ -746,7 +746,7 @@ def test_skbuild_settings_auto_cmake_warning( assert settings_reader.settings.cmake.version == SpecifierSet(">=3.15") - ex = capsys.readouterr().out + ex = capsys.readouterr().err ex = re.sub(r"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\))", "", ex) print(ex) assert ex.split() == [