diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9749718f7..a8ea444d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,9 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install package - run: pip install .[test,cov] cmake ninja rich + run: + pip install .[test,cov] cmake ninja rich hatch-fancy-pypi-readme + setuptools-scm - name: Test package run: pytest -ra --showlocals --cov=scikit_build_core @@ -91,9 +93,11 @@ jobs: with: name: ${{ runner.os }}-${{ matrix.python-version }} + # the min requirements are not compatible with the metadata plugin + # packages so we remove those first (they then won't be tested) - name: Min requirements run: | - pip uninstall -y cmake + pip uninstall -y cmake hatch-fancy-pypi-readme setuptools-scm pip install --constraint tests/constraints.txt .[test] - name: Setup CMake ${{ matrix.cmake-version }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5cf7ec453..3e9e88db0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,7 +63,12 @@ repos: rev: v1.1.1 hooks: - id: mypy - exclude: tests/packages/(simplest_c/src/simplest/__init__.py|.*/setup.py) + exclude: | + (?x)^( + tests/packages/simplest_c/src/simplest/__init__.py| + tests/packages/dynamic_metadata/src/dynamic/__init__.py| + tests/packages/.*/setup.py + ) files: ^(src|tests) args: [] additional_dependencies: @@ -71,13 +76,16 @@ repos: - cattrs - cmake - exceptiongroup - - importlib_metadata + - gitpython + - hatch-fancy-pypi-readme + - importlib-metadata - importlib_resources - ninja - packaging - pyproject_metadata - pytest - rich + - setuptools-scm - tomli - types-setuptools - typing_extensions >=4; python_version<'3.11' diff --git a/noxfile.py b/noxfile.py index 5aa931ded..8bf2bab38 100644 --- a/noxfile.py +++ b/noxfile.py @@ -27,7 +27,9 @@ def pylint(session: nox.Session) -> None: """ # This needs to be installed into the package environment, and is slower # than a pre-commit check - session.install("-e.[dev,test]", "pylint") + session.install( + "-e.[dev,test]", "pylint", "hatch-fancy-pypi-readme", "setuptools-scm" + ) session.run("pylint", "scikit_build_core", *session.posargs) @@ -37,7 +39,7 @@ def tests(session: nox.Session) -> None: Run the unit and regular tests. """ env = {"PIP_DISABLE_PIP_VERSION_CHECK": "1"} - extra = ["rich"] + extra = ["hatch-fancy-pypi-readme", "rich", "setuptools-scm"] # This will not work if system CMake is too old (<3.15) if shutil.which("cmake") is None and shutil.which("cmake3") is None: extra.append("cmake") diff --git a/pyproject.toml b/pyproject.toml index 18b89a455..a373a1357 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ rich = [ test = [ "build[virtualenv]", "cattrs >=22.2.0", + "gitpython", "importlib_metadata; python_version<'3.8'", "pathspec >=0.10.1", "pybind11", @@ -129,7 +130,7 @@ module = ["scikit_build_core.*"] disallow_untyped_defs = true [[tool.mypy.overrides]] -module = ["pathspec"] +module = ["pathspec", "setuptools_scm", "hatch_fancy_pypi_readme"] ignore_missing_imports = true @@ -197,6 +198,7 @@ extend-ignore = [ "PLR2004", "PLE1205", # Format check doesn't work with our custom logger "E501", # Line too long + "PT004", ] target-version = "py37" typing-modules = ["scikit_build_core._compat.typing"] diff --git a/src/scikit_build_core/build/sdist.py b/src/scikit_build_core/build/sdist.py index 67265465a..05c2a0510 100644 --- a/src/scikit_build_core/build/sdist.py +++ b/src/scikit_build_core/build/sdist.py @@ -8,9 +8,8 @@ import tarfile from pathlib import Path -from pyproject_metadata import StandardMetadata - from .._compat import tomllib +from ..settings.metadata import get_standard_metadata from ..settings.skbuild_read_settings import SettingsReader from ._file_processor import each_unignored_file from ._init import setup_logging @@ -69,7 +68,10 @@ def build_sdist( sdist_directory: str, config_settings: dict[str, list[str] | str] | None = None, ) -> str: - settings_reader = SettingsReader.from_file("pyproject.toml", config_settings) + with Path("pyproject.toml").open("rb") as f: + pyproject = tomllib.load(f) + + settings_reader = SettingsReader(pyproject, config_settings or {}) settings = settings_reader.settings setup_logging(settings.logging.level) @@ -77,15 +79,12 @@ def build_sdist( sdist_dir = Path(sdist_directory) - with Path("pyproject.toml").open("rb") as f: - pyproject = tomllib.load(f) - reproducible = settings.sdist.reproducible timestamp = get_reproducible_epoch() if reproducible else None + metadata = get_standard_metadata(pyproject, settings) # Using deepcopy here because of a bug in pyproject-metadata # https://github.com/FFY00/python-pyproject-metadata/pull/49 - metadata = StandardMetadata.from_pyproject(pyproject) pkg_info = bytes(copy.deepcopy(metadata).as_rfc822()) srcdirname = f"{metadata.name}-{metadata.version}" diff --git a/src/scikit_build_core/build/wheel.py b/src/scikit_build_core/build/wheel.py index c81ee1035..31619e3a9 100644 --- a/src/scikit_build_core/build/wheel.py +++ b/src/scikit_build_core/build/wheel.py @@ -9,7 +9,6 @@ from pathlib import Path from packaging.version import Version -from pyproject_metadata import StandardMetadata from .. import __version__ from .._compat import tomllib @@ -17,6 +16,7 @@ from ..builder.builder import Builder, archs_to_tags, get_archs from ..builder.wheel_tag import WheelTag from ..cmake import CMake, CMaker +from ..settings.metadata import get_standard_metadata from ..settings.skbuild_read_settings import SettingsReader from ._init import setup_logging from ._pathutil import packages_to_file_mapping @@ -63,16 +63,17 @@ def _build_wheel_impl( """ Build a wheel or just prepare metadata (if wheel dir is None). """ + pyproject_path = Path("pyproject.toml") + with pyproject_path.open("rb") as ft: + pyproject = tomllib.load(ft) - settings_reader = SettingsReader.from_file("pyproject.toml", config_settings) + settings_reader = SettingsReader(pyproject, config_settings or {}) settings = settings_reader.settings setup_logging(settings.logging.level) settings_reader.validate_may_exit() - with Path("pyproject.toml").open("rb") as ft: - pyproject = tomllib.load(ft) - metadata = StandardMetadata.from_pyproject(pyproject) + metadata = get_standard_metadata(pyproject, settings, config_settings) if metadata.version is None: msg = "project.version is not statically specified, must be present currently" diff --git a/src/scikit_build_core/metadata/__init__.py b/src/scikit_build_core/metadata/__init__.py new file mode 100644 index 000000000..bdec2fc8c --- /dev/null +++ b/src/scikit_build_core/metadata/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +__all__: list[str] = [] diff --git a/src/scikit_build_core/metadata/fancy_pypi_readme.py b/src/scikit_build_core/metadata/fancy_pypi_readme.py new file mode 100644 index 000000000..f67f67a61 --- /dev/null +++ b/src/scikit_build_core/metadata/fancy_pypi_readme.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import Any + +__all__ = ["dynamic_metadata"] + + +def __dir__() -> list[str]: + return __all__ + + +def dynamic_metadata( + pyproject_dict: dict[str, Any], + _config_settings: dict[str, list[str] | str] | None = None, +) -> dict[str, str | dict[str, str | None]]: + from hatch_fancy_pypi_readme._builder import build_text + from hatch_fancy_pypi_readme._config import load_and_validate_config + + config = load_and_validate_config( + pyproject_dict["tool"]["hatch"]["metadata"]["hooks"]["fancy-pypi-readme"] + ) + + return { + "readme": { + "content-type": config.content_type, + "text": build_text(config.fragments, config.substitutions), + } + } diff --git a/src/scikit_build_core/metadata/setuptools_scm.py b/src/scikit_build_core/metadata/setuptools_scm.py new file mode 100644 index 000000000..18ff7dabd --- /dev/null +++ b/src/scikit_build_core/metadata/setuptools_scm.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +__all__ = ["dynamic_metadata"] + + +def __dir__() -> list[str]: + return __all__ + + +def dynamic_metadata( + pyproject_dict: dict[str, object], # noqa: ARG001 + _config_settings: dict[str, list[str] | str] | None = None, +) -> dict[str, str | dict[str, str | None]]: + # this is a classic implementation, waiting for the release of + # vcs-versioning and an improved public interface + from setuptools_scm import Configuration, _get_version + + config = Configuration.from_file("pyproject.toml") + version: str = _get_version(config) + + return {"version": version} diff --git a/src/scikit_build_core/settings/metadata.py b/src/scikit_build_core/settings/metadata.py new file mode 100644 index 000000000..184da9288 --- /dev/null +++ b/src/scikit_build_core/settings/metadata.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import importlib +from typing import Any + +from pyproject_metadata import StandardMetadata + +from ..settings.skbuild_model import ScikitBuildSettings + +__all__ = ["get_standard_metadata"] + + +def __dir__() -> list[str]: + return __all__ + + +def _load( + mod_name: str, + pyproject_dict: dict[str, Any], + config_settings: dict[str, list[str] | str] | None = None, +) -> dict[str, Any]: + return importlib.import_module(mod_name).dynamic_metadata(pyproject_dict, config_settings) # type: ignore[no-any-return] + + +# If pyproject-metadata eventually supports updates, this can be simplified +def get_standard_metadata( + pyproject_dict: dict[str, Any], + settings: ScikitBuildSettings, + config_settings: dict[str, list[str] | str] | None = None, +) -> StandardMetadata: + # Handle any dynamic metadata + for field in settings.metadata: + if field not in pyproject_dict.get("project", {}).get("dynamic", []): + msg = f"{field} is not in project.dynamic" + raise KeyError(msg) + + plugins = set(settings.metadata.values()) + cached_plugins = { + key: _load(key, pyproject_dict, config_settings) for key in plugins + } + + for field, mod_name in settings.metadata.items(): + if field not in cached_plugins[mod_name]: + msg = f"{field} is not provided by plugin {mod_name}" + raise KeyError(msg) + + pyproject_dict["project"][field] = cached_plugins[mod_name][field] + pyproject_dict["project"]["dynamic"].remove(field) + + return StandardMetadata.from_pyproject(pyproject_dict) diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py index 34884b010..b52729068 100644 --- a/src/scikit_build_core/settings/skbuild_model.py +++ b/src/scikit_build_core/settings/skbuild_model.py @@ -110,6 +110,7 @@ class ScikitBuildSettings: sdist: SDistSettings = dataclasses.field(default_factory=SDistSettings) wheel: WheelSettings = dataclasses.field(default_factory=WheelSettings) backport: BackportSettings = dataclasses.field(default_factory=BackportSettings) + metadata: Dict[str, str] = dataclasses.field(default_factory=dict) #: Strictly check all config options. If False, warnings will be #: printed for unknown options. If True, an error will be raised. diff --git a/tests/constraints.txt b/tests/constraints.txt index a2e02f960..55a5283f0 100644 --- a/tests/constraints.txt +++ b/tests/constraints.txt @@ -3,6 +3,7 @@ cattrs==22.2.0 pytest==7.0.0 tomli==1.1.0 packaging==20.9 +importlib-metadata==4.13.0 importlib-resources==1.3.0 -pyproject-metadata==0.5.0 +pyproject-metadata==0.6.1 pathspec==0.10.1 diff --git a/tests/packages/dynamic_metadata/CMakeLists.txt b/tests/packages/dynamic_metadata/CMakeLists.txt new file mode 100644 index 000000000..d636f8911 --- /dev/null +++ b/tests/packages/dynamic_metadata/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.15...3.25) + +project( + ${SKBUILD_PROJECT_NAME} + LANGUAGES C + VERSION ${SKBUILD_PROJECT_VERSION}) + +find_package(Python COMPONENTS Interpreter Development.Module) +set(Python_SOABI ${SKBUILD_SOABI}) + +python_add_library(_module MODULE src/module.c WITH_SOABI) + +install(TARGETS _module DESTINATION ${SKBUILD_PROJECT_NAME}) diff --git a/tests/packages/dynamic_metadata/dual_project.toml b/tests/packages/dynamic_metadata/dual_project.toml new file mode 100644 index 000000000..f977a0af2 --- /dev/null +++ b/tests/packages/dynamic_metadata/dual_project.toml @@ -0,0 +1,11 @@ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[project] +name = "fancy" +dynamic = ["version", "license"] + +[tool.scikit-build.metadata] +version = "test_dual" +license = "test_dual" diff --git a/tests/packages/dynamic_metadata/faulty_dual_project.toml b/tests/packages/dynamic_metadata/faulty_dual_project.toml new file mode 100644 index 000000000..87797e393 --- /dev/null +++ b/tests/packages/dynamic_metadata/faulty_dual_project.toml @@ -0,0 +1,12 @@ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[project] +name = "fancy" +dynamic = ["version", "readme", "license"] + +[tool.scikit-build.metadata] +version = "test_dual" +license = "test_dual" +readme = "test_dual" diff --git a/tests/packages/dynamic_metadata/faulty_project.toml b/tests/packages/dynamic_metadata/faulty_project.toml new file mode 100644 index 000000000..dff81f045 --- /dev/null +++ b/tests/packages/dynamic_metadata/faulty_project.toml @@ -0,0 +1,10 @@ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[project] +name = "fancy" +version = "0.0.1" + +[tool.scikit-build.metadata] +readme = "scikit_build_core.metadata.fancy_pypi_readme" diff --git a/tests/packages/dynamic_metadata/plugin_project.toml b/tests/packages/dynamic_metadata/plugin_project.toml new file mode 100644 index 000000000..25039084d --- /dev/null +++ b/tests/packages/dynamic_metadata/plugin_project.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[project] +name = "fancy" +dynamic = ["readme", "version"] + +[tool.scikit-build.metadata] +version = "scikit_build_core.metadata.setuptools_scm" +readme = "scikit_build_core.metadata.fancy_pypi_readme" + +[tool.setuptools_scm] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/x-rst" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +text = "Fragment #1" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +text = "Fragment #2" diff --git a/tests/packages/dynamic_metadata/pyproject.toml b/tests/packages/dynamic_metadata/pyproject.toml new file mode 100644 index 000000000..4cf165400 --- /dev/null +++ b/tests/packages/dynamic_metadata/pyproject.toml @@ -0,0 +1,12 @@ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[project] +name = "dynamic" +dynamic = ["version", "readme", "license"] + +[tool.scikit-build.metadata] +version = "test_version" +readme = "test_readme" +license = "test_license" diff --git a/tests/packages/dynamic_metadata/src/dynamic/__init__.py b/tests/packages/dynamic_metadata/src/dynamic/__init__.py new file mode 100644 index 000000000..a43969896 --- /dev/null +++ b/tests/packages/dynamic_metadata/src/dynamic/__init__.py @@ -0,0 +1,3 @@ +from ._module import square + +__all__ = ["square"] diff --git a/tests/packages/dynamic_metadata/src/module.c b/tests/packages/dynamic_metadata/src/module.c new file mode 100644 index 000000000..b9a03a7cc --- /dev/null +++ b/tests/packages/dynamic_metadata/src/module.c @@ -0,0 +1,24 @@ +#define PY_SSIZE_T_CLEAN +#include + +float square(float x) { return x * x; } + +static PyObject *square_wrapper(PyObject *self, PyObject *args) { + float input, result; + if (!PyArg_ParseTuple(args, "f", &input)) { + return NULL; + } + result = square(input); + return PyFloat_FromDouble(result); +} + +static PyMethodDef pysimple_methods[] = { + {"square", square_wrapper, METH_VARARGS, "Square function"}, + {NULL, NULL, 0, NULL}}; + +static struct PyModuleDef pysimple_module = {PyModuleDef_HEAD_INIT, "pysimple", + NULL, -1, pysimple_methods}; + +PyMODINIT_FUNC PyInit__module(void) { + return PyModule_Create(&pysimple_module); +} diff --git a/tests/packages/dynamic_metadata/warn_project.toml b/tests/packages/dynamic_metadata/warn_project.toml new file mode 100644 index 000000000..4074cccc6 --- /dev/null +++ b/tests/packages/dynamic_metadata/warn_project.toml @@ -0,0 +1,11 @@ +[build-system] +requires = ["scikit-build-core"] +build-backend = "scikit_build_core.build" + +[project] +name = "fancy" +version = "0.0.1" +dynamic = ["readme"] + +[tool.scikit-build.metadata] +readme = "non_existent" diff --git a/tests/test_dynamic_metadata.py b/tests/test_dynamic_metadata.py new file mode 100644 index 000000000..dbe097794 --- /dev/null +++ b/tests/test_dynamic_metadata.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +import importlib +import shutil +import sys +import types +import zipfile +from pathlib import Path +from typing import Any + +import git +import pyproject_metadata +import pytest + +from scikit_build_core._compat import tomllib +from scikit_build_core.build import build_wheel +from scikit_build_core.settings.metadata import get_standard_metadata +from scikit_build_core.settings.skbuild_read_settings import SettingsReader + +DIR = Path(__file__).parent.resolve() +DYNAMIC = DIR / "packages/dynamic_metadata" + + +# these are mock plugins returning known results +# it turns out to be easier to create EntryPoint objects pointing to real +# functions than to mock them. +def ep_version( + _pyproject_dict: dict[str, Any], + _config_settings: dict[str, list[str] | str] | None = None, +) -> dict[str, str | dict[str, str | None]]: + return {"version": "0.0.2"} + + +def ep_readme( + _pyproject_dict: dict[str, Any], + _config_settings: dict[str, list[str] | str] | None = None, +) -> dict[str, str | dict[str, str | None]]: + return { + "readme": { + "content-type": "text/x-rst", + "text": "Some text", + } + } + + +def ep_license( + _pyproject_dict: dict[str, Any], + _config_settings: dict[str, list[str] | str] | None = None, +) -> dict[str, str | dict[str, str | None]]: + return {"license": {"text": "MIT License"}} + + +def ep_dual( + _pyproject_dict: dict[str, Any], + _config_settings: dict[str, list[str] | str] | None = None, +) -> dict[str, str | dict[str, str | None]]: + return { + "version": "0.3", + "license": {"text": "BSD License"}, + } + + +original_loader = importlib.import_module + + +def special_loader(name: str, *args: Any, **kwargs: Any) -> Any: + if name == "test_version": + test_version = types.ModuleType("test_version") + test_version.dynamic_metadata = ep_version # type: ignore[attr-defined] + return test_version + if name == "test_readme": + test_readme = types.ModuleType("test_readme") + test_readme.dynamic_metadata = ep_readme # type: ignore[attr-defined] + return test_readme + if name == "test_license": + test_license = types.ModuleType("test_license") + test_license.dynamic_metadata = ep_license # type: ignore[attr-defined] + return test_license + if name == "test_dual": + test_dual = types.ModuleType("test_dual") + test_dual.dynamic_metadata = ep_dual # type: ignore[attr-defined] + return test_dual + + return original_loader(name, *args, **kwargs) + + +@pytest.fixture() +def mock_entry_points(monkeypatch): + monkeypatch.setattr(importlib, "import_module", special_loader) + + +@pytest.mark.usefixtures("mock_entry_points") +def test_dynamic_metadata(monkeypatch): + monkeypatch.chdir(DYNAMIC) + + with Path("pyproject.toml").open("rb") as ft: + pyproject = tomllib.load(ft) + settings_reader = SettingsReader(pyproject, {}) + settings = settings_reader.settings + + settings_reader.validate_may_exit() + + metadata = get_standard_metadata(pyproject, settings) + + assert str(metadata.version) == "0.0.2" + assert metadata.license == pyproject_metadata.License("MIT License", None) + assert metadata.readme == pyproject_metadata.Readme("Some text", None, "text/x-rst") + + +def test_plugin_metadata(tmp_path, monkeypatch): + reason_msg = ( + "install hatch-fancy-pypi-readme and setuptools-scm to test the " + "dynamic metadata plugins" + ) + pytest.importorskip("hatch_fancy_pypi_readme", reason=reason_msg) + pytest.importorskip("setuptools_scm", reason=reason_msg) + build_dir = tmp_path / "build" + build_dir.mkdir() + + shutil.copy(DYNAMIC / "plugin_project.toml", build_dir / "pyproject.toml") + monkeypatch.chdir(build_dir) + + repo = git.repo.base.Repo.init(build_dir, initial_branch="main") + repo.config_writer().set_value("user", "name", "bot").release() + repo.config_writer().set_value("user", "email", "bot@scikit-build.net").release() + repo.index.add(["pyproject.toml"]) + repo.index.commit("first commit") + repo.create_tag("v0.1.0", message="initial commit") + + with Path("pyproject.toml").open("rb") as ft: + pyproject = tomllib.load(ft) + settings_reader = SettingsReader(pyproject, {}) + settings = settings_reader.settings + + settings_reader.validate_may_exit() + + metadata = get_standard_metadata(pyproject, settings) + + assert str(metadata.version) == "0.1.0" + assert metadata.readme == pyproject_metadata.Readme( + "Fragment #1Fragment #2", None, "text/x-rst" + ) + + +def test_faulty_metadata(monkeypatch): + monkeypatch.chdir(DYNAMIC) + + with Path("faulty_project.toml").open("rb") as ft: + pyproject = tomllib.load(ft) + settings_reader = SettingsReader(pyproject, {}) + settings = settings_reader.settings + + settings_reader.validate_may_exit() + + with pytest.raises(KeyError): + get_standard_metadata(pyproject, settings) + + +def test_warn_metadata(monkeypatch): + monkeypatch.chdir(DYNAMIC) + with Path("warn_project.toml").open("rb") as ft: + pyproject = tomllib.load(ft) + settings_reader = SettingsReader(pyproject, {}) + settings = settings_reader.settings + + settings_reader.validate_may_exit() + + with pytest.raises(ModuleNotFoundError): + get_standard_metadata(pyproject, settings) + + +@pytest.mark.usefixtures("mock_entry_points") +def test_dual_metadata(monkeypatch): + monkeypatch.chdir(DYNAMIC) + + with Path("dual_project.toml").open("rb") as ft: + pyproject = tomllib.load(ft) + settings_reader = SettingsReader(pyproject, {}) + settings = settings_reader.settings + + settings_reader.validate_may_exit() + + metadata = get_standard_metadata(pyproject, settings) + assert str(metadata.version) == "0.3" + assert metadata.license == pyproject_metadata.License("BSD License", None) + + with Path("faulty_dual_project.toml").open("rb") as ft: + pyproject = tomllib.load(ft) + settings_reader = SettingsReader(pyproject, {}) + settings = settings_reader.settings + + settings_reader.validate_may_exit() + + with pytest.raises(KeyError): + get_standard_metadata(pyproject, settings) + + +@pytest.mark.compile() +@pytest.mark.configure() +@pytest.mark.usefixtures("mock_entry_points") +def test_pep517_wheel(tmp_path, monkeypatch, virtualenv): + dist = tmp_path.resolve() / "dist" + monkeypatch.chdir(DYNAMIC) + if Path("dist").is_dir(): + shutil.rmtree("dist") + + out = build_wheel(str(dist)) + (wheel,) = dist.glob("dynamic-0.0.2-*.whl") + assert wheel == dist / out + + virtualenv.install(wheel) + virtualenv.install("importlib-metadata==4.13.0") + license = virtualenv.execute( + "from importlib_metadata import metadata; print(metadata('dynamic')['License'])" + ) + assert license == "MIT License" + + if sys.version_info >= (3, 8): + with wheel.open("rb") as f: + p = zipfile.Path(f) + file_names = {x.name for x in p.iterdir()} + dynamic_pkg = {x.name for x in p.joinpath("dynamic").iterdir()} + + filtered_pkg = {x for x in dynamic_pkg if not x.startswith("_module")} + + assert len(filtered_pkg) == len(dynamic_pkg) - 1 + assert {"dynamic-0.0.2.dist-info", "dynamic"} == file_names + assert { + "__init__.py", + } == filtered_pkg + + version = virtualenv.execute("from dynamic import square; print(square(2))") + assert version == "4.0" diff --git a/tests/test_setuptools_abi3.py b/tests/test_setuptools_abi3.py index e5d3958a6..e5d5a11f7 100644 --- a/tests/test_setuptools_abi3.py +++ b/tests/test_setuptools_abi3.py @@ -25,7 +25,12 @@ def test_abi3_wheel(tmp_path, monkeypatch, virtualenv): dist = tmp_path / "dist" dist.mkdir() - monkeypatch.chdir(ABI_PKG) + + # create a temporary copy of the package source so we don't contaminate the + # main source tree with build artefacts + src = tmp_path / "src" + shutil.copytree(ABI_PKG, src) + monkeypatch.chdir(src) if Path("dist").is_dir(): shutil.rmtree("dist") diff --git a/tests/test_setuptools_pep517.py b/tests/test_setuptools_pep517.py index 9f271fabe..b1126197c 100644 --- a/tests/test_setuptools_pep517.py +++ b/tests/test_setuptools_pep517.py @@ -31,7 +31,13 @@ def test_pep517_sdist(tmp_path, monkeypatch): dist = tmp_path / "dist" dist.mkdir() - monkeypatch.chdir(HELLO_PEP518) + + # create a temporary copy of the package source so we don't contaminate the + # main source tree with build artefacts + src = tmp_path / "src" + shutil.copytree(HELLO_PEP518, src) + monkeypatch.chdir(src) + if Path("dist").is_dir(): shutil.rmtree("dist") @@ -75,7 +81,13 @@ def test_pep517_sdist(tmp_path, monkeypatch): def test_pep517_wheel(tmp_path, monkeypatch, virtualenv): dist = tmp_path / "dist" dist.mkdir() - monkeypatch.chdir(HELLO_PEP518) + + # create a temporary copy of the package source so we don't contaminate the + # main source tree with build artefacts + src = tmp_path / "src" + shutil.copytree(HELLO_PEP518, src) + monkeypatch.chdir(src) + if Path("dist").is_dir(): shutil.rmtree("dist") out = build_wheel(str(dist)) diff --git a/tests/test_setuptools_pep518.py b/tests/test_setuptools_pep518.py index c6435b465..30740da94 100644 --- a/tests/test_setuptools_pep518.py +++ b/tests/test_setuptools_pep518.py @@ -18,10 +18,13 @@ @pytest.mark.skipif( sys.platform.startswith("cygwin"), reason="Cygwin fails here with ld errors" ) -def test_pep518_wheel(monkeypatch, isolated): - dist = HELLO_PEP518 / "dist" - shutil.rmtree(dist, ignore_errors=True) - monkeypatch.chdir(HELLO_PEP518) +def test_pep518_wheel(tmp_path, monkeypatch, isolated): + # create a temporary copy of the package source so we don't contaminate the + # main source tree with build artefacts + src = tmp_path / "src" + dist = src / "dist" + shutil.copytree(HELLO_PEP518, src) + monkeypatch.chdir(src) isolated.install("build[virtualenv]") isolated.module("build", "--wheel") (wheel,) = dist.iterdir() @@ -56,8 +59,12 @@ def test_pep518_wheel(monkeypatch, isolated): @pytest.mark.skipif( sys.platform.startswith("cygwin"), reason="Cygwin fails here with ld errors" ) -def test_pep518_pip(isolated): - isolated.install("-v", HELLO_PEP518) +def test_pep518_pip(isolated, tmp_path): + # create a temporary copy of the package source so we don't contaminate the + # main source tree with build artefacts + src = tmp_path / "src" + shutil.copytree(HELLO_PEP518, src) + isolated.install("-v", src) version = isolated.execute( "import cmake_example; print(cmake_example.__version__)",