Skip to content

Commit

Permalink
feat: support vendoring with command (#32)
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Schreiner <[email protected]>
  • Loading branch information
henryiii authored Sep 6, 2024
1 parent 5c53d7d commit 6d215cb
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 1 deletion.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ repos:
additional_dependencies:
- pytest
- scikit-build-core
- importlib-resources

- repo: https://github.com/codespell-project/codespell
rev: "v2.3.0"
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,31 @@

<!-- SPHINX-START -->

## Vendoring

You can vendor FindCython and/or UseCython into your package, as well. This
avoids requiring a dependency at build time and protects you against changes in
this package, at the expense of requiring manual re-vendoring to get bugfixes
and/or improvements. This mechanism is also ideal if you want to support direct
builds, outside of scikit-build-core.

You should make a CMake helper directory, such as `cmake`. Add this to your
`CMakeLists.txt` like this:

```cmake
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
```

Then, you can vendor our files into that folder:

```bash
pipx run cython-cmake vendor cmake
```

If you want to just vendor one of the two files, use `--member FindCython` or
`--member UseCython`. You can rerun this command to revendor. The directory must
already exist.

<!-- prettier-ignore-start -->
[actions-badge]: https://github.com/scikit-build/cython-cmake/workflows/CI/badge.svg
[actions-link]: https://github.com/scikit-build/cython-cmake/actions
Expand Down
9 changes: 8 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,16 @@ classifiers = [
"Typing :: Typed",
]
dynamic = ["version"]
dependencies = []
dependencies = [
"importlib_resources; python_version<'3.9'"
]

[project.entry-points."cmake.module"]
any = "cython_cmake.cmake"

[project.scripts]
cython-cmake = "cython_cmake.__main__:main"

[project.optional-dependencies]
test = [
"pytest >=6",
Expand Down Expand Up @@ -152,7 +157,9 @@ similarities.ignore-imports = "yes"
messages_control.disable = [
"design",
"fixme",
"invalid-name",
"line-too-long",
"missing-class-docstring",
"missing-module-docstring",
"wrong-import-position",
]
78 changes: 78 additions & 0 deletions src/cython_cmake/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from __future__ import annotations

import argparse
import enum
import functools
import operator
from collections.abc import Sequence
from pathlib import Path
from typing import Any

from ._version import version as __version__
from .vendor import Members, vendorize

__all__ = ["main"]


def __dir__() -> list[str]:
return __all__


class FlagAction(argparse.Action):
def __init__(self, *args: Any, **kwargs: Any) -> None:
enum_type = kwargs.pop("type", None)
if enum_type is None:
msg = "enum type is required"
raise ValueError(msg)
if not issubclass(enum_type, enum.Flag):
msg = "type must be an Flag when using FlagAction"
raise TypeError(msg)

kwargs.setdefault("choices", tuple(e.name for e in enum_type))

super().__init__(*args, **kwargs)

self._enum = enum_type

def __call__(
self,
parser: argparse.ArgumentParser, # noqa: ARG002
namespace: argparse.Namespace,
values: str | Sequence[Any] | None,
option_string: str | None = None, # noqa: ARG002
) -> None:
if not isinstance(values, list):
values = [values]
flags = functools.reduce(operator.or_, (self._enum[e] for e in values))
setattr(namespace, self.dest, flags)


def main() -> None:
"""
Entry point.
"""
parser = argparse.ArgumentParser(
prog="cython_cmake", description="CMake Cython module helper"
)
parser.add_argument(
"--version", action="version", version=f"%(prog)s {__version__}"
)
subparser = parser.add_subparsers(required=True)
vendor_parser = subparser.add_parser("vendor", help="Vendor CMake helpers")
vendor_parser.add_argument(
"target", type=Path, help="Directory to vendor the CMake helpers"
)
vendor_parser.add_argument(
"--members",
type=Members,
nargs="*",
action=FlagAction,
default=functools.reduce(operator.or_, list(Members)),
help="Members to vendor, defaults to all",
)
args = parser.parse_args()
vendorize(args.target, args.members)


if __name__ == "__main__":
main()
43 changes: 43 additions & 0 deletions src/cython_cmake/vendor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

import enum
import sys
from pathlib import Path

if sys.version_info < (3, 9):
from importlib_resources import files
else:
from importlib.resources import files

__all__ = ["vendorize", "Members"]


def __dir__() -> list[str]:
return __all__


class Members(enum.Flag):
FindCython = enum.auto()
UseCython = enum.auto()


def vendorize(
target: Path, members: Members = Members.FindCython | Members.UseCython
) -> None:
"""
Vendorize files into a directory. Directory must exist.
"""
if not target.is_dir():
msg = f"Target directory {target} does not exist"
raise AssertionError(msg)

cmake_dir = files("cython_cmake") / "cmake"
if Members.FindCython in members:
find = cmake_dir / "FindCython.cmake"
find_target = target / "FindCython.cmake"
find_target.write_text(find.read_text(encoding="utf-8"), encoding="utf-8")

if Members.UseCython in members:
use = cmake_dir / "UseCython.cmake"
use_target = target / "UseCython.cmake"
use_target.write_text(use.read_text(encoding="utf-8"), encoding="utf-8")
36 changes: 36 additions & 0 deletions tests/test_vendorize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

import sys
from pathlib import Path

import pytest

from cython_cmake.__main__ import main

DIR = Path(__file__).parent.resolve()
FIND_CYTHON = DIR.parent.joinpath("src/cython_cmake/cmake/FindCython.cmake").read_text()
USE_CYTHON = DIR.parent.joinpath("src/cython_cmake/cmake/UseCython.cmake").read_text()


def test_copy_files(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
path = tmp_path / "copy_all"
path.mkdir()

monkeypatch.setattr(sys, "argv", [sys.executable, "vendor", str(path)])
main()

assert path.joinpath("FindCython.cmake").read_text() == FIND_CYTHON
assert path.joinpath("UseCython.cmake").read_text() == USE_CYTHON


def test_copy_only_find(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
path = tmp_path / "copy_find"
path.mkdir()

monkeypatch.setattr(
sys, "argv", [sys.executable, "vendor", str(path), "--member", "FindCython"]
)
main()

assert path.joinpath("FindCython.cmake").read_text() == FIND_CYTHON
assert not path.joinpath("UseCython.cmake").exists()

0 comments on commit 6d215cb

Please sign in to comment.