55
66import dataclasses
77import enum
8+ import logging
89import os
910import sys
1011from importlib .util import find_spec
12+ from typing import Any
13+
14+ LOG = logging .getLogger (__name__ )
1115
1216
1317class EntrypointBuildMode (enum .Enum ):
@@ -24,6 +28,17 @@ class EntrypointBuildMode(enum.Enum):
2428 BUILD_HOOK = "build-hook"
2529
2630
31+ class BuildBackend (enum .Enum ):
32+ """
33+ The build backend integration to use. Currently, we support setuptools and hatchling. If set to ``auto``, there
34+ is an algorithm to detect the build backend automatically from the config.
35+ """
36+
37+ AUTO = "auto"
38+ SETUPTOOLS = "setuptools"
39+ HATCHLING = "hatchling"
40+
41+
2742@dataclasses .dataclass
2843class PluxConfiguration :
2944 """
@@ -47,13 +62,17 @@ class PluxConfiguration:
4762 entrypoint_static_file : str = "plux.ini"
4863 """The name of the entrypoint ini file if entrypoint_build_mode is set to MANUAL."""
4964
65+ build_backend : BuildBackend = BuildBackend .AUTO
66+ """The build backend to use. If set to ``auto``, the build backend will be detected automatically from the config."""
67+
5068 def merge (
5169 self ,
5270 path : str = None ,
5371 exclude : list [str ] = None ,
5472 include : list [str ] = None ,
5573 entrypoint_build_mode : EntrypointBuildMode = None ,
5674 entrypoint_static_file : str = None ,
75+ build_backend : BuildBackend = None ,
5776 ) -> "PluxConfiguration" :
5877 """
5978 Merges or overwrites the given values into the current configuration and returns a new configuration object.
@@ -69,6 +88,7 @@ def merge(
6988 entrypoint_static_file = entrypoint_static_file
7089 if entrypoint_static_file is not None
7190 else self .entrypoint_static_file ,
91+ build_backend = build_backend if build_backend is not None else self .build_backend ,
7292 )
7393
7494
@@ -81,8 +101,7 @@ def read_plux_config_from_workdir(workdir: str = None) -> PluxConfiguration:
81101 :return: A plux configuration object
82102 """
83103 try :
84- pyproject_file = os .path .join (workdir or os .getcwd (), "pyproject.toml" )
85- return parse_pyproject_toml (pyproject_file )
104+ return parse_pyproject_toml (workdir or os .getcwd ())
86105 except FileNotFoundError :
87106 return PluxConfiguration ()
88107
@@ -96,18 +115,7 @@ def parse_pyproject_toml(path: str | os.PathLike[str]) -> PluxConfiguration:
96115 :return: A plux configuration object containing the parsed values.
97116 :raises FileNotFoundError: If the file does not exist.
98117 """
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 )
118+ pyproject_config = load_pyproject_toml (path )
111119
112120 # find the [tool.plux] section
113121 tool_table = pyproject_config .get ("tool" , {})
@@ -127,4 +135,102 @@ def parse_pyproject_toml(path: str | os.PathLike[str]) -> PluxConfiguration:
127135 # will raise a ValueError exception if the mode is invalid
128136 kwargs ["entrypoint_build_mode" ] = EntrypointBuildMode (mode )
129137
138+ # parse build_backend
139+ if build_backend := kwargs .get ("build_backend" ):
140+ # will raise a ValueError exception if the build backend is invalid
141+ kwargs ["build_backend" ] = BuildBackend (build_backend )
142+
130143 return PluxConfiguration (** kwargs )
144+
145+
146+ def determine_build_backend_from_pyproject_config (pyproject_config : dict [str , Any ]) -> BuildBackend | None :
147+ """
148+ Determine the build backend to use based on the pyproject.toml configuration.
149+ """
150+ build_backend = pyproject_config .get ("build-system" , {}).get ("build-backend" , "" )
151+ if build_backend .startswith ("setuptools." ):
152+ return BuildBackend .SETUPTOOLS
153+ if build_backend .startswith ("hatchling." ):
154+ return BuildBackend .HATCHLING
155+ else :
156+ return None
157+
158+
159+ def load_pyproject_toml (pyproject_file_or_workdir : str | os .PathLike [str ] = None ) -> dict [str , Any ]:
160+ """
161+ Loads a pyproject.toml file from the given path or the current working directory. Uses tomli or tomllib to parse.
162+
163+ :param pyproject_file_or_workdir: Path to the pyproject.toml file or the directory containing it. Defaults to the current working directory.
164+ :return: The parsed pyproject.toml file as a dictionary.
165+ """
166+ if pyproject_file_or_workdir is None :
167+ pyproject_file_or_workdir = os .getcwd ()
168+ if os .path .isfile (pyproject_file_or_workdir ):
169+ pyproject_file = pyproject_file_or_workdir
170+ else :
171+ pyproject_file = os .path .join (pyproject_file_or_workdir , "pyproject.toml" )
172+
173+ if find_spec ("tomllib" ):
174+ from tomllib import load as load_toml
175+ elif find_spec ("tomli" ):
176+ from tomli import load as load_toml
177+ else :
178+ raise ImportError ("Could not find a TOML parser. Please install either tomllib or tomli." )
179+
180+ # read the file
181+ if not os .path .exists (pyproject_file ):
182+ raise FileNotFoundError (f"No .toml file found at { pyproject_file } " )
183+ with open (pyproject_file , "rb" ) as file :
184+ pyproject_config = load_toml (file )
185+
186+ return pyproject_config
187+
188+
189+ def determine_build_backend_from_config (workdir : str ) -> BuildBackend :
190+ """
191+ Algorithm to determine the build backend to use based on the given workdir. First, it checks the pyproject.toml to
192+ see whether there's a [tool.plux] build_backend =... is configured directly. If not found, it checks the
193+ ``build-backend`` attribute in the pyproject.toml. Then, as a fallback, it tries to import both setuptools and
194+ hatchling, and uses the first one that works
195+ """
196+ # parse config to get build backend
197+ plux_config = read_plux_config_from_workdir (workdir )
198+
199+ if plux_config .build_backend != BuildBackend .AUTO :
200+ # first, check if the user configured one
201+ return plux_config .build_backend
202+
203+ # otherwise, try to determine it from the build-backend attribute in the pyproject.toml
204+ try :
205+ backend = determine_build_backend_from_pyproject_config (load_pyproject_toml (workdir ))
206+ if backend is not None :
207+ return backend
208+ except FileNotFoundError :
209+ pass
210+
211+ # if that also fails, just try to import both build backends and return the first one that works
212+ try :
213+ import setuptools # noqa
214+
215+ try :
216+ # Try import here again to log proper warning if both are present in the environment
217+ import hatchling
218+
219+ LOG .warning (
220+ "Both setuptools and hatchling build backends available. Please manually choose a build-backend in the plux config. Defaulting to setuptools."
221+ )
222+ except ImportError :
223+ pass
224+
225+ return BuildBackend .SETUPTOOLS
226+ except ImportError :
227+ pass
228+
229+ try :
230+ import hatchling # noqa
231+
232+ return BuildBackend .HATCHLING
233+ except ImportError :
234+ pass
235+
236+ raise ValueError ("No supported build backend found. Plux needs either setuptools or hatchling to work." )
0 commit comments