88import os
99import sys
1010from importlib .util import find_spec
11+ from typing import Any
1112
1213
1314class 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
2840class 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." )
0 commit comments