Skip to content

Commit 6d215cb

Browse files
authored
feat: support vendoring with command (#32)
Signed-off-by: Henry Schreiner <[email protected]>
1 parent 5c53d7d commit 6d215cb

File tree

6 files changed

+191
-1
lines changed

6 files changed

+191
-1
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ repos:
5656
additional_dependencies:
5757
- pytest
5858
- scikit-build-core
59+
- importlib-resources
5960

6061
- repo: https://github.com/codespell-project/codespell
6162
rev: "v2.3.0"

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,31 @@
1515

1616
<!-- SPHINX-START -->
1717

18+
## Vendoring
19+
20+
You can vendor FindCython and/or UseCython into your package, as well. This
21+
avoids requiring a dependency at build time and protects you against changes in
22+
this package, at the expense of requiring manual re-vendoring to get bugfixes
23+
and/or improvements. This mechanism is also ideal if you want to support direct
24+
builds, outside of scikit-build-core.
25+
26+
You should make a CMake helper directory, such as `cmake`. Add this to your
27+
`CMakeLists.txt` like this:
28+
29+
```cmake
30+
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
31+
```
32+
33+
Then, you can vendor our files into that folder:
34+
35+
```bash
36+
pipx run cython-cmake vendor cmake
37+
```
38+
39+
If you want to just vendor one of the two files, use `--member FindCython` or
40+
`--member UseCython`. You can rerun this command to revendor. The directory must
41+
already exist.
42+
1843
<!-- prettier-ignore-start -->
1944
[actions-badge]: https://github.com/scikit-build/cython-cmake/workflows/CI/badge.svg
2045
[actions-link]: https://github.com/scikit-build/cython-cmake/actions

pyproject.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,16 @@ classifiers = [
3030
"Typing :: Typed",
3131
]
3232
dynamic = ["version"]
33-
dependencies = []
33+
dependencies = [
34+
"importlib_resources; python_version<'3.9'"
35+
]
3436

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

40+
[project.scripts]
41+
cython-cmake = "cython_cmake.__main__:main"
42+
3843
[project.optional-dependencies]
3944
test = [
4045
"pytest >=6",
@@ -152,7 +157,9 @@ similarities.ignore-imports = "yes"
152157
messages_control.disable = [
153158
"design",
154159
"fixme",
160+
"invalid-name",
155161
"line-too-long",
162+
"missing-class-docstring",
156163
"missing-module-docstring",
157164
"wrong-import-position",
158165
]

src/cython_cmake/__main__.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import enum
5+
import functools
6+
import operator
7+
from collections.abc import Sequence
8+
from pathlib import Path
9+
from typing import Any
10+
11+
from ._version import version as __version__
12+
from .vendor import Members, vendorize
13+
14+
__all__ = ["main"]
15+
16+
17+
def __dir__() -> list[str]:
18+
return __all__
19+
20+
21+
class FlagAction(argparse.Action):
22+
def __init__(self, *args: Any, **kwargs: Any) -> None:
23+
enum_type = kwargs.pop("type", None)
24+
if enum_type is None:
25+
msg = "enum type is required"
26+
raise ValueError(msg)
27+
if not issubclass(enum_type, enum.Flag):
28+
msg = "type must be an Flag when using FlagAction"
29+
raise TypeError(msg)
30+
31+
kwargs.setdefault("choices", tuple(e.name for e in enum_type))
32+
33+
super().__init__(*args, **kwargs)
34+
35+
self._enum = enum_type
36+
37+
def __call__(
38+
self,
39+
parser: argparse.ArgumentParser, # noqa: ARG002
40+
namespace: argparse.Namespace,
41+
values: str | Sequence[Any] | None,
42+
option_string: str | None = None, # noqa: ARG002
43+
) -> None:
44+
if not isinstance(values, list):
45+
values = [values]
46+
flags = functools.reduce(operator.or_, (self._enum[e] for e in values))
47+
setattr(namespace, self.dest, flags)
48+
49+
50+
def main() -> None:
51+
"""
52+
Entry point.
53+
"""
54+
parser = argparse.ArgumentParser(
55+
prog="cython_cmake", description="CMake Cython module helper"
56+
)
57+
parser.add_argument(
58+
"--version", action="version", version=f"%(prog)s {__version__}"
59+
)
60+
subparser = parser.add_subparsers(required=True)
61+
vendor_parser = subparser.add_parser("vendor", help="Vendor CMake helpers")
62+
vendor_parser.add_argument(
63+
"target", type=Path, help="Directory to vendor the CMake helpers"
64+
)
65+
vendor_parser.add_argument(
66+
"--members",
67+
type=Members,
68+
nargs="*",
69+
action=FlagAction,
70+
default=functools.reduce(operator.or_, list(Members)),
71+
help="Members to vendor, defaults to all",
72+
)
73+
args = parser.parse_args()
74+
vendorize(args.target, args.members)
75+
76+
77+
if __name__ == "__main__":
78+
main()

src/cython_cmake/vendor.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from __future__ import annotations
2+
3+
import enum
4+
import sys
5+
from pathlib import Path
6+
7+
if sys.version_info < (3, 9):
8+
from importlib_resources import files
9+
else:
10+
from importlib.resources import files
11+
12+
__all__ = ["vendorize", "Members"]
13+
14+
15+
def __dir__() -> list[str]:
16+
return __all__
17+
18+
19+
class Members(enum.Flag):
20+
FindCython = enum.auto()
21+
UseCython = enum.auto()
22+
23+
24+
def vendorize(
25+
target: Path, members: Members = Members.FindCython | Members.UseCython
26+
) -> None:
27+
"""
28+
Vendorize files into a directory. Directory must exist.
29+
"""
30+
if not target.is_dir():
31+
msg = f"Target directory {target} does not exist"
32+
raise AssertionError(msg)
33+
34+
cmake_dir = files("cython_cmake") / "cmake"
35+
if Members.FindCython in members:
36+
find = cmake_dir / "FindCython.cmake"
37+
find_target = target / "FindCython.cmake"
38+
find_target.write_text(find.read_text(encoding="utf-8"), encoding="utf-8")
39+
40+
if Members.UseCython in members:
41+
use = cmake_dir / "UseCython.cmake"
42+
use_target = target / "UseCython.cmake"
43+
use_target.write_text(use.read_text(encoding="utf-8"), encoding="utf-8")

tests/test_vendorize.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
from pathlib import Path
5+
6+
import pytest
7+
8+
from cython_cmake.__main__ import main
9+
10+
DIR = Path(__file__).parent.resolve()
11+
FIND_CYTHON = DIR.parent.joinpath("src/cython_cmake/cmake/FindCython.cmake").read_text()
12+
USE_CYTHON = DIR.parent.joinpath("src/cython_cmake/cmake/UseCython.cmake").read_text()
13+
14+
15+
def test_copy_files(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
16+
path = tmp_path / "copy_all"
17+
path.mkdir()
18+
19+
monkeypatch.setattr(sys, "argv", [sys.executable, "vendor", str(path)])
20+
main()
21+
22+
assert path.joinpath("FindCython.cmake").read_text() == FIND_CYTHON
23+
assert path.joinpath("UseCython.cmake").read_text() == USE_CYTHON
24+
25+
26+
def test_copy_only_find(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
27+
path = tmp_path / "copy_find"
28+
path.mkdir()
29+
30+
monkeypatch.setattr(
31+
sys, "argv", [sys.executable, "vendor", str(path), "--member", "FindCython"]
32+
)
33+
main()
34+
35+
assert path.joinpath("FindCython.cmake").read_text() == FIND_CYTHON
36+
assert not path.joinpath("UseCython.cmake").exists()

0 commit comments

Comments
 (0)