Skip to content

Commit 4c165b0

Browse files
committed
create more explicit build backend selection
1 parent dedb84e commit 4c165b0

2 files changed

Lines changed: 113 additions & 41 deletions

File tree

plux/build/config.py

Lines changed: 107 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import os
99
import sys
1010
from importlib.util import find_spec
11+
from typing import Any
1112

1213

1314
class EntrypointBuildMode(enum.Enum):
@@ -24,6 +25,17 @@ class EntrypointBuildMode(enum.Enum):
2425
BUILD_HOOK = "build-hook"
2526

2627

28+
class BuildBackend(enum.Enum):
29+
"""
30+
The build backend integration to use. Currently, we support setuptools and hatchling. If set to ``auto``, there
31+
is an algorithm to detect the build backend automatically from the config.
32+
"""
33+
34+
AUTO = "auto"
35+
SETUPTOOLS = "setuptools"
36+
HATCHLING = "hatchling"
37+
38+
2739
@dataclasses.dataclass
2840
class PluxConfiguration:
2941
"""
@@ -47,13 +59,17 @@ class PluxConfiguration:
4759
entrypoint_static_file: str = "plux.ini"
4860
"""The name of the entrypoint ini file if entrypoint_build_mode is set to MANUAL."""
4961

62+
bild_backend: BuildBackend = BuildBackend.AUTO
63+
"""The build backend to use. If set to ``auto``, the build backend will be detected automatically from the config."""
64+
5065
def merge(
5166
self,
5267
path: str = None,
5368
exclude: list[str] = None,
5469
include: list[str] = None,
5570
entrypoint_build_mode: EntrypointBuildMode = None,
5671
entrypoint_static_file: str = None,
72+
bild_backend: BuildBackend = None,
5773
) -> "PluxConfiguration":
5874
"""
5975
Merges or overwrites the given values into the current configuration and returns a new configuration object.
@@ -69,6 +85,7 @@ def merge(
6985
entrypoint_static_file=entrypoint_static_file
7086
if entrypoint_static_file is not None
7187
else self.entrypoint_static_file,
88+
bild_backend=bild_backend if bild_backend is not None else self.bild_backend,
7289
)
7390

7491

@@ -81,8 +98,7 @@ def read_plux_config_from_workdir(workdir: str = None) -> PluxConfiguration:
8198
:return: A plux configuration object
8299
"""
83100
try:
84-
pyproject_file = os.path.join(workdir or os.getcwd(), "pyproject.toml")
85-
return parse_pyproject_toml(pyproject_file)
101+
return parse_pyproject_toml(workdir or os.getcwd())
86102
except FileNotFoundError:
87103
return PluxConfiguration()
88104

@@ -96,18 +112,7 @@ def parse_pyproject_toml(path: str | os.PathLike[str]) -> PluxConfiguration:
96112
:return: A plux configuration object containing the parsed values.
97113
:raises FileNotFoundError: If the file does not exist.
98114
"""
99-
if find_spec("tomllib"):
100-
from tomllib import load as load_toml
101-
elif find_spec("tomli"):
102-
from tomli import load as load_toml
103-
else:
104-
raise ImportError("Could not find a TOML parser. Please install either tomllib or tomli.")
105-
106-
# read the file
107-
if not os.path.exists(path):
108-
raise FileNotFoundError(f"No pyproject.toml found at {path}")
109-
with open(path, "rb") as file:
110-
pyproject_config = load_toml(file)
115+
pyproject_config = load_pyproject_toml(path)
111116

112117
# find the [tool.plux] section
113118
tool_table = pyproject_config.get("tool", {})
@@ -127,4 +132,92 @@ def parse_pyproject_toml(path: str | os.PathLike[str]) -> PluxConfiguration:
127132
# will raise a ValueError exception if the mode is invalid
128133
kwargs["entrypoint_build_mode"] = EntrypointBuildMode(mode)
129134

135+
# parse build_backend
136+
if build_backend := kwargs.get("build_backend"):
137+
# will raise a ValueError exception if the build backend is invalid
138+
kwargs["build_backend"] = BuildBackend(build_backend)
139+
130140
return PluxConfiguration(**kwargs)
141+
142+
143+
def determine_build_backend_from_pyproject_config(pyproject_config: dict[str, Any]) -> BuildBackend | None:
144+
"""
145+
Determine the build backend to use based on the pyproject.toml configuration.
146+
"""
147+
build_backend = pyproject_config.get("build-system", {}).get("build-backend", "")
148+
if build_backend.startswith("setuptools."):
149+
return BuildBackend.SETUPTOOLS
150+
if build_backend.startswith("hatchling."):
151+
return BuildBackend.HATCHLING
152+
else:
153+
return None
154+
155+
156+
def load_pyproject_toml(pyproject_file_or_workdir: str | os.PathLike[str] = None) -> dict[str, Any]:
157+
"""
158+
Loads a pyproject.toml file from the given path or the current working directory. Uses tomli or tomllib to parse.
159+
160+
:param pyproject_file_or_workdir: Path to the pyproject.toml file or the directory containing it. Defaults to the current working directory.
161+
:return: The parsed pyproject.toml file as a dictionary.
162+
"""
163+
if pyproject_file_or_workdir is None:
164+
pyproject_file_or_workdir = os.getcwd()
165+
if os.path.isfile(pyproject_file_or_workdir):
166+
pyproject_file = pyproject_file_or_workdir
167+
else:
168+
pyproject_file = os.path.join(pyproject_file_or_workdir, "pyproject.toml")
169+
170+
if find_spec("tomllib"):
171+
from tomllib import load as load_toml
172+
elif find_spec("tomli"):
173+
from tomli import load as load_toml
174+
else:
175+
raise ImportError("Could not find a TOML parser. Please install either tomllib or tomli.")
176+
177+
# read the file
178+
if not os.path.exists(pyproject_file):
179+
raise FileNotFoundError(f"No .toml file found at {pyproject_file}")
180+
with open(pyproject_file, "rb") as file:
181+
pyproject_config = load_toml(file)
182+
183+
return pyproject_config
184+
185+
186+
def determine_build_backend_from_config(workdir: str) -> BuildBackend:
187+
"""
188+
Algorithm to determine the build backend to use based on the given workdir. First, it checks the pyproject.toml to
189+
see whether there's a [tool.plux] build_backend =... is configured directly. If not found, it checks the
190+
``build-backend`` attribute in the pyproject.toml. Then, as a fallback, it tries to import both setuptools and
191+
hatchling, and uses the first one that works
192+
"""
193+
# parse config to get build backend
194+
plux_config = read_plux_config_from_workdir(workdir)
195+
196+
if plux_config.bild_backend != BuildBackend.AUTO:
197+
# first, check if the user configured one
198+
return plux_config.bild_backend
199+
200+
# otherwise, try to determine it from the build-backend attribute in the pyproject.toml
201+
try:
202+
backend = determine_build_backend_from_pyproject_config(load_pyproject_toml(workdir))
203+
if backend is not None:
204+
return backend
205+
except FileNotFoundError:
206+
pass
207+
208+
# if that also fails, just try to import both build backends and return the first one that works
209+
try:
210+
import setuptools # noqa
211+
212+
return BuildBackend.SETUPTOOLS
213+
except ImportError:
214+
pass
215+
216+
try:
217+
import hatchling # noqa
218+
219+
return BuildBackend.HATCHLING
220+
except ImportError:
221+
pass
222+
223+
raise ValueError("No supported build backend found. Plux needs either setuptools or hatchling to work.")

plux/cli/cli.py

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,41 +14,20 @@
1414
LOG = logging.getLogger(__name__)
1515

1616

17-
def _get_build_backend() -> str | None:
18-
"""
19-
Returns the name of the build backend to use. Currently, we only support setuptools and hatchling, and we prefer
20-
setuptools over hatchling if both are available.
21-
"""
22-
# TODO: should read this from the project configuration instead somehow.
23-
try:
24-
import setuptools # noqa
25-
26-
return "setuptools"
27-
except ImportError:
28-
pass
29-
30-
try:
31-
import hatchling # noqa
32-
33-
return "hatchling"
34-
except ImportError:
35-
pass
36-
37-
return None
38-
39-
4017
def _load_project(args: argparse.Namespace) -> Project:
41-
backend = _get_build_backend()
4218
workdir = args.workdir
4319

4420
if args.verbose:
45-
print(f"loading project config from {workdir}, determined build backend is: {backend}")
21+
print(f"loading project config from {workdir}")
22+
23+
# TODO: this is maybe a bit redundant since we parse the config once here, and then again when we create the Project
24+
backend = config.determine_build_backend_from_config(workdir)
4625

47-
if backend == "setuptools":
26+
if backend == config.BuildBackend.SETUPTOOLS:
4827
from plux.build.setuptools import SetuptoolsProject
4928

5029
return SetuptoolsProject(workdir)
51-
elif backend == "hatchling":
30+
elif backend == config.BuildBackend.HATCHLING:
5231
from plux.build.hatchling import HatchlingProject
5332

5433
return HatchlingProject(workdir)

0 commit comments

Comments
 (0)