Skip to content

Commit ac6fdfc

Browse files
authored
refactor: lazy load and use entry points (#1)
Signed-off-by: Henry Schreiner <[email protected]>
1 parent f3d777d commit ac6fdfc

File tree

9 files changed

+71
-56
lines changed

9 files changed

+71
-56
lines changed

pyproject.toml

+7
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ Changelog = "https://github.com/scikit-build/scikit-build-cli/releases"
6161
[project.scripts]
6262
skbuild = "scikit_build_cli.__main__:run_cli"
6363

64+
[project.entry-points."skbuild.commands"]
65+
build = "scikit_build_cli.commands.build:build"
66+
configure = "scikit_build_cli.commands.configure:configure"
67+
dynamic-metadata = "scikit_build_cli.commands.dynamic_metadata:dynamic_metadata"
68+
metadata = "scikit_build_cli.commands.metadata:metadata"
69+
install = "scikit_build_cli.commands.install:install"
70+
6471
[tool.hatch]
6572
version.source = "vcs"
6673
build.hooks.vcs.version-file = "src/scikit_build_cli/_version.py"

src/scikit_build_cli/commands/__init__.py

Whitespace-only changes.

src/scikit_build_cli/build.py renamed to src/scikit_build_cli/commands/build.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
import click
66

7-
from .main import skbuild
8-
from .utils import _build_dir
7+
from ..utils import _build_dir
98

109
if TYPE_CHECKING:
1110
from pathlib import Path
@@ -17,7 +16,7 @@ def __dir__() -> list[str]:
1716
return __all__
1817

1918

20-
@skbuild.command()
19+
@click.command()
2120
@_build_dir
2221
@click.pass_context
2322
def build(ctx: click.Context, build_dir: Path) -> None: # noqa: ARG001

src/scikit_build_cli/configure.py renamed to src/scikit_build_cli/commands/configure.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
import click
66

7-
from .main import skbuild
8-
from .utils import _build_dir
7+
from ..utils import _build_dir
98

109
if TYPE_CHECKING:
1110
from pathlib import Path
@@ -17,7 +16,7 @@ def __dir__() -> list[str]:
1716
return __all__
1817

1918

20-
@skbuild.command()
19+
@click.command()
2120
@_build_dir
2221
@click.pass_context
2322
def configure(ctx: click.Context, build_dir: Path) -> None: # noqa: ARG001

src/scikit_build_cli/dynamic_metadata.py renamed to src/scikit_build_cli/commands/dynamic_metadata.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22

33
import click
44

5-
from .main import skbuild
6-
75
__all__: list[str] = ["dynamic_metadata"]
86

97

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

1311

14-
@skbuild.command()
12+
@click.command()
1513
@click.pass_context
1614
def dynamic_metadata(ctx: click.Context) -> None: # noqa: ARG001
1715
"""

src/scikit_build_cli/install.py renamed to src/scikit_build_cli/commands/install.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
import click
66

7-
from .main import skbuild
8-
from .utils import _build_dir
7+
from ..utils import _build_dir
98

109
if TYPE_CHECKING:
1110
from pathlib import Path
@@ -17,7 +16,7 @@ def __dir__() -> list[str]:
1716
return __all__
1817

1918

20-
@skbuild.command()
19+
@click.command()
2120
@_build_dir
2221
@click.pass_context
2322
def install(ctx: click.Context, build_dir: Path) -> None: # noqa: ARG001

src/scikit_build_cli/metadata.py renamed to src/scikit_build_cli/commands/metadata.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22

33
import click
44

5-
from .main import skbuild
6-
75
__all__: list[str] = ["metadata"]
86

97

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

1311

14-
@skbuild.command()
12+
@click.command()
1513
@click.pass_context
1614
def metadata(ctx: click.Context) -> None: # noqa: ARG001
1715
"""

src/scikit_build_cli/main.py

+44-18
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from __future__ import annotations
22

33
import pathlib
4-
import sys
4+
from collections.abc import MutableMapping, Sequence
5+
from importlib.metadata import EntryPoint
56

67
import click
78

@@ -15,7 +16,47 @@ def __dir__() -> list[str]:
1516
return __all__
1617

1718

18-
@click.group("skbuild")
19+
class LazyGroup(click.Group):
20+
"""
21+
Lazy loader for click commands. Based on Click's documentation, but uses
22+
EntryPoints.
23+
"""
24+
25+
def __init__(
26+
self,
27+
name: str | None = None,
28+
commands: MutableMapping[str, click.Command]
29+
| Sequence[click.Command]
30+
| None = None,
31+
*,
32+
lazy_subcommands: Sequence[EntryPoint] = (),
33+
**kwargs: object,
34+
):
35+
super().__init__(name, commands, **kwargs)
36+
self.lazy_subcommands = {v.name: v for v in lazy_subcommands}
37+
38+
def list_commands(self, ctx: click.Context) -> list[str]:
39+
return sorted([*super().list_commands(ctx), *self.lazy_subcommands])
40+
41+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
42+
if cmd_name in self.lazy_subcommands:
43+
return self._lazy_load(cmd_name)
44+
return super().get_command(ctx, cmd_name)
45+
46+
def _lazy_load(self, cmd_name: str) -> click.Command:
47+
ep = self.lazy_subcommands[cmd_name]
48+
cmd_object = ep.load()
49+
if not isinstance(cmd_object, click.Command):
50+
msg = f"Lazy loading of {ep} failed by returning a non-command object"
51+
raise ValueError(msg)
52+
return cmd_object
53+
54+
55+
# Add all plugin commands.
56+
CMDS = list(metadata.entry_points(group="skbuild.commands"))
57+
58+
59+
@click.group("skbuild", cls=LazyGroup, lazy_subcommands=CMDS)
1960
@click.version_option(__version__)
2061
@click.option(
2162
"--root",
@@ -27,26 +68,11 @@ def __dir__() -> list[str]:
2768
writable=True,
2869
path_type=pathlib.Path,
2970
),
30-
help="Path to the python project's root",
71+
help="Path to the Python project's root",
3172
)
3273
@click.pass_context
3374
def skbuild(ctx: click.Context, root: pathlib.Path) -> None: # noqa: ARG001
3475
"""
3576
scikit-build Main CLI interface
3677
"""
3778
# TODO: Add specific implementations
38-
39-
40-
# Add all plugin commands. Native subcommands are loaded in the package's __init__
41-
for ep in metadata.entry_points(group="skbuild.commands"):
42-
try:
43-
# Entry point can either point to a whole module or the decorated command
44-
if not ep.attr:
45-
# If it's a module, just load the module. It should have the necessary `skbuild.command` interface
46-
ep.load()
47-
else:
48-
# Otherwise assume it is a decorated command that needs to be loaded manually
49-
skbuild.add_command(ep.load())
50-
except Exception as err:
51-
# TODO: the print should go through the click logging interface
52-
print(f"Could not load cli plugin: {ep}\n{err}", file=sys.stderr) # noqa: T201

src/scikit_build_cli/utils.py

+12-23
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import functools
43
import pathlib
54
from typing import TYPE_CHECKING
65

@@ -20,25 +19,15 @@ def __dir__() -> list[str]:
2019
return __all__
2120

2221

23-
def _build_dir(func: F) -> F:
24-
"""Add build_dir click option"""
25-
26-
@click.option(
27-
"--build-dir",
28-
"-B",
29-
type=click.Path(
30-
exists=True,
31-
file_okay=False,
32-
dir_okay=True,
33-
writable=True,
34-
path_type=pathlib.Path,
35-
),
36-
help="Path to cmake build directory",
37-
)
38-
@functools.wraps(func)
39-
# TODO: Fix mypy checks here.
40-
# See upstream approach: https://github.com/pallets/click/blob/main/src/click/decorators.py
41-
def wrapper(*args, **kwargs): # type: ignore[no-untyped-def]
42-
return func(*args, **kwargs)
43-
44-
return wrapper # type: ignore[return-value]
22+
_build_dir = click.option(
23+
"--build-dir",
24+
"-B",
25+
type=click.Path(
26+
exists=True,
27+
file_okay=False,
28+
dir_okay=True,
29+
writable=True,
30+
path_type=pathlib.Path,
31+
),
32+
help="Path to cmake build directory",
33+
)

0 commit comments

Comments
 (0)