Skip to content

Commit ca2f097

Browse files
Add --build-backend option to usethis init (#1469)
* Initial plan * feat: add --build-backend option to usethis init Add a new --build-backend CLI option that allows users to choose which build backend to use when initializing a project. Supported values match those of uv init --build-backend: hatch (default), uv, flit, pdm, setuptools, maturin, scikit, and poetry. The build backend is stored in usethis_config and used by both the uv backend (passed through to uv init --build-backend) and the none backend (generates appropriate [build-system] config in pyproject.toml). Closes #347 Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/3d692c70-3b16-43c5-bd5a-8fdc2463c5f9 * style: apply formatting fixes from static checks Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/3d692c70-3b16-43c5-bd5a-8fdc2463c5f9 * refactor: limit build backends to hatch and uv, add config dict comprehensiveness test Reduce BuildBackendEnum to only hatch and uv for simpler maintenance. Remove tests for flit/setuptools/poetry/etc backends. Add TestBuildSystemConfig::test_keys_match_enum to verify _BUILD_SYSTEM_CONFIG keys stay in sync with BuildBackendEnum members. Update docs/cli/reference.md to only list hatch and uv. Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/69842fd3-e51b-4d58-9bd1-fd5136923fd9 * refactor: use fallback versions and next_breaking_version for build system config - Add FALLBACK_HATCHLING_VERSION = "1.29.0" to _fallback.py - Extract next_breaking_version() generic helper into _fallback.py - Refactor next_breaking_uv_version() to delegate to next_breaking_version() - Update _BUILD_SYSTEM_CONFIG to use FALLBACK_UV_VERSION, FALLBACK_HATCHLING_VERSION and next_breaking_version() instead of hard-coded version bounds - Add TestFallbackHatchlingVersion and TestNextBreakingVersion tests Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/0ab118f5-5448-457b-a6aa-6740f6b46a5d --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> Co-authored-by: Nathan McDougall <nathan.j.mcdougall@gmail.com>
1 parent d69f331 commit ca2f097

13 files changed

Lines changed: 176 additions & 13 deletions

File tree

docs/cli/reference.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ Supported options:
5555
- `uv` to use the [uv](https://docs.astral.sh/uv) package manager
5656
- `none` to not use a package manager backend and display messages for some operations.
5757

58+
- `--build-backend` to specify the build backend for the project. Defaults to `hatch`.
59+
60+
Possible values:
61+
- `hatch` for [Hatchling](https://hatch.pypa.io/) (default)
62+
- `uv` for [uv](https://docs.astral.sh/uv/concepts/build-backend/)
63+
5864
## `usethis arch`
5965

6066
Add recommended architecture analysis tools to the project (namely, [Import Linter](https://import-linter.readthedocs.io/en/stable/)), including:

src/usethis/_backend/uv/init.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def opinionated_uv_init() -> None:
1919
"init",
2020
"--lib",
2121
"--build-backend",
22-
"hatch",
22+
usethis_config.build_backend.value,
2323
usethis_config.cpd().as_posix(),
2424
],
2525
change_toml=True,
@@ -43,8 +43,8 @@ def ensure_pyproject_toml_via_uv(*, author: bool = True) -> None:
4343
"--bare",
4444
"--vcs=none",
4545
f"--author-from={author_from}",
46-
"--build-backend", # https://github.com/usethis-python/usethis-python/issues/347
47-
"hatch", # until https://github.com/astral-sh/uv/issues/3957
46+
"--build-backend",
47+
usethis_config.build_backend.value,
4848
usethis_config.cpd().as_posix(),
4949
],
5050
change_toml=True,

src/usethis/_backend/uv/version.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import json
22

3-
from packaging.version import Version
4-
53
from usethis._backend.uv.call import call_uv_subprocess
64
from usethis._backend.uv.errors import UVSubprocessFailedError
7-
from usethis._fallback import FALLBACK_UV_VERSION
5+
from usethis._fallback import FALLBACK_UV_VERSION, next_breaking_version
86

97

108
def get_uv_version() -> str:
@@ -26,7 +24,4 @@ def next_breaking_uv_version(version: str) -> str:
2624
For versions with major >= 1, bumps the major version (e.g. 1.0.2 -> 2.0.0).
2725
For versions with major == 0, bumps the minor version (e.g. 0.10.2 -> 0.11.0).
2826
"""
29-
v = Version(version)
30-
if v.major >= 1:
31-
return f"{v.major + 1}.0.0"
32-
return f"0.{v.minor + 1}.0"
27+
return next_breaking_version(version)

src/usethis/_config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import TYPE_CHECKING, Literal
77

88
from usethis._types.backend import BackendEnum
9+
from usethis._types.build_backend import BuildBackendEnum
910

1011
if TYPE_CHECKING:
1112
from collections.abc import Generator
@@ -17,6 +18,7 @@
1718
OFFLINE_DEFAULT = False
1819
QUIET_DEFAULT = False
1920
BACKEND_DEFAULT = "auto"
21+
BUILD_BACKEND_DEFAULT = "hatch"
2022

2123

2224
@dataclass
@@ -47,6 +49,7 @@ class UsethisConfig:
4749
instruct_only: bool = False
4850
backend: BackendEnum = BackendEnum(BACKEND_DEFAULT) # noqa: RUF009
4951
inferred_backend: Literal[BackendEnum.uv, BackendEnum.none] | None = None
52+
build_backend: BuildBackendEnum = BuildBackendEnum(BUILD_BACKEND_DEFAULT) # noqa: RUF009
5053
disable_pre_commit: bool = False
5154
subprocess_verbose: bool = False
5255
project_dir: Path | None = None
@@ -61,6 +64,7 @@ def set( # noqa: PLR0913, PLR0915
6164
alert_only: bool | None = None,
6265
instruct_only: bool | None = None,
6366
backend: BackendEnum | None = None,
67+
build_backend: BuildBackendEnum | None = None,
6468
disable_pre_commit: bool | None = None,
6569
subprocess_verbose: bool | None = None,
6670
project_dir: Path | str | None = None,
@@ -73,6 +77,7 @@ def set( # noqa: PLR0913, PLR0915
7377
old_instruct_only = self.instruct_only
7478
old_backend = self.backend
7579
old_inferred_backend = self.inferred_backend
80+
old_build_backend = self.build_backend
7681
old_disable_pre_commit = self.disable_pre_commit
7782
old_subprocess_verbose = self.subprocess_verbose
7883
old_project_dir = self.project_dir
@@ -89,6 +94,8 @@ def set( # noqa: PLR0913, PLR0915
8994
instruct_only = self.instruct_only
9095
if backend is None:
9196
backend = self.backend
97+
if build_backend is None:
98+
build_backend = self.build_backend
9299
if disable_pre_commit is None:
93100
disable_pre_commit = old_disable_pre_commit
94101
if subprocess_verbose is None:
@@ -104,6 +111,7 @@ def set( # noqa: PLR0913, PLR0915
104111
self.backend = backend
105112
if backend is not BackendEnum.auto:
106113
self.inferred_backend = backend
114+
self.build_backend = build_backend
107115
self.disable_pre_commit = disable_pre_commit
108116
self.subprocess_verbose = subprocess_verbose
109117
if isinstance(project_dir, str):
@@ -117,6 +125,7 @@ def set( # noqa: PLR0913, PLR0915
117125
self.instruct_only = old_instruct_only
118126
self.backend = old_backend
119127
self.inferred_backend = old_inferred_backend
128+
self.build_backend = old_build_backend
120129
self.disable_pre_commit = old_disable_pre_commit
121130
self.subprocess_verbose = old_subprocess_verbose
122131
self.project_dir = old_project_dir

src/usethis/_fallback.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,24 @@
55
``tests/usethis/test_fallback.py``.
66
"""
77

8+
from packaging.version import Version
9+
810
FALLBACK_UV_VERSION = "0.10.12"
11+
FALLBACK_HATCHLING_VERSION = "1.29.0"
912
FALLBACK_PRE_COMMIT_VERSION = "4.5.1"
1013
FALLBACK_RUFF_VERSION = "v0.15.7"
1114
FALLBACK_SYNC_WITH_UV_VERSION = "v0.5.0"
1215
FALLBACK_PYPROJECT_FMT_VERSION = "v2.20.0"
1316
FALLBACK_CODESPELL_VERSION = "v2.4.2"
17+
18+
19+
def next_breaking_version(version: str) -> str:
20+
"""Get the next breaking version for a version string, following semver.
21+
22+
For versions with major >= 1, bumps the major version (e.g. 1.0.2 -> 2.0.0).
23+
For versions with major == 0, bumps the minor version (e.g. 0.10.2 -> 0.11.0).
24+
"""
25+
v = Version(version)
26+
if v.major >= 1:
27+
return f"{v.major + 1}.0.0"
28+
return f"0.{v.minor + 1}.0"

src/usethis/_init.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,30 @@
1010
from usethis._config import usethis_config
1111
from usethis._console import tick_print
1212
from usethis._deps import get_project_deps
13+
from usethis._fallback import (
14+
FALLBACK_HATCHLING_VERSION,
15+
FALLBACK_UV_VERSION,
16+
next_breaking_version,
17+
)
1318
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
1419
from usethis._integrations.project.name import get_project_name
1520
from usethis._types.backend import BackendEnum
21+
from usethis._types.build_backend import BuildBackendEnum
22+
23+
_BUILD_SYSTEM_CONFIG: dict[BuildBackendEnum, tuple[list[str], str]] = {
24+
BuildBackendEnum.hatch: (
25+
[
26+
f"hatchling>={FALLBACK_HATCHLING_VERSION},<{next_breaking_version(FALLBACK_HATCHLING_VERSION)}"
27+
],
28+
"hatchling.build",
29+
),
30+
BuildBackendEnum.uv: (
31+
[
32+
f"uv_build>={FALLBACK_UV_VERSION},<{next_breaking_version(FALLBACK_UV_VERSION)}"
33+
],
34+
"uv_build",
35+
),
36+
}
1637

1738

1839
def project_init():
@@ -103,9 +124,12 @@ def ensure_pyproject_toml(*, author: bool = True) -> None:
103124

104125
tick_print("Writing 'pyproject.toml'.")
105126
backend = get_backend()
127+
build_backend = usethis_config.build_backend
106128
if backend is BackendEnum.uv:
107129
ensure_pyproject_toml_via_uv(author=author)
108130
elif backend is BackendEnum.none:
131+
requires, build_backend_str = _BUILD_SYSTEM_CONFIG[build_backend]
132+
requires_str = ", ".join(f'"{r}"' for r in requires)
109133
(usethis_config.cpd() / "pyproject.toml").write_text(
110134
f"""\
111135
[project]
@@ -114,15 +138,15 @@ def ensure_pyproject_toml(*, author: bool = True) -> None:
114138
dependencies = []
115139
116140
[build-system]
117-
requires = ["hatchling"]
118-
build-backend = "hatchling.build"
141+
requires = [{requires_str}]
142+
build-backend = "{build_backend_str}"
119143
""",
120144
encoding="utf-8",
121145
)
122146
else:
123147
assert_never(backend)
124148

125-
if not (
149+
if build_backend is BuildBackendEnum.hatch and not (
126150
(usethis_config.cpd() / "src").exists()
127151
and (usethis_config.cpd() / "src").is_dir()
128152
):
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from enum import Enum
2+
3+
4+
class BuildBackendEnum(Enum):
5+
"""Enumeration of available build backends for project initialization.
6+
7+
These correspond to a subset of the build backends supported by
8+
`uv init --build-backend`.
9+
"""
10+
11+
hatch = "hatch"
12+
uv = "uv"

src/usethis/_ui/interface/init.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77

88
from usethis._config import usethis_config
99
from usethis._types.backend import BackendEnum
10+
from usethis._types.build_backend import BuildBackendEnum
1011
from usethis._types.ci import CIServiceEnum
1112
from usethis._types.docstyle import DocStyleEnum
1213
from usethis._types.status import DevelopmentStatusEnum
1314
from usethis._ui.options import (
1415
backend_opt,
1516
frozen_opt,
1617
init_arch_opt,
18+
init_build_backend_opt,
1719
init_ci_opt,
1820
init_doc_opt,
1921
init_docstyle_opt,
@@ -46,6 +48,7 @@ def init(
4648
quiet: bool = quiet_opt,
4749
frozen: bool = frozen_opt,
4850
backend: BackendEnum = backend_opt,
51+
build_backend: BuildBackendEnum = init_build_backend_opt,
4952
path: str | None = init_path_arg,
5053
) -> None:
5154
"""Initialize a new project with recommended tooling."""
@@ -54,6 +57,7 @@ def init(
5457
from usethis.errors import UsethisError
5558

5659
assert isinstance(backend, BackendEnum)
60+
assert isinstance(build_backend, BuildBackendEnum)
5761

5862
if path is not None:
5963
path_ = Path(path)
@@ -68,6 +72,7 @@ def init(
6872
quiet=quiet,
6973
frozen=frozen,
7074
backend=backend,
75+
build_backend=build_backend,
7176
project_dir=path,
7277
),
7378
files_manager(),

src/usethis/_ui/options.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from usethis._config import (
44
BACKEND_DEFAULT,
5+
BUILD_BACKEND_DEFAULT,
56
FROZEN_DEFAULT,
67
HOW_DEFAULT,
78
OFFLINE_DEFAULT,
@@ -96,6 +97,11 @@
9697
None,
9798
help="The path to use for the project. Defaults to the current working directory.",
9899
)
100+
init_build_backend_opt = typer.Option(
101+
BUILD_BACKEND_DEFAULT,
102+
"--build-backend",
103+
help="The build backend to use for the project.",
104+
)
99105

100106
# readme command options
101107
badges_opt = typer.Option(False, "--badges", help="Add relevant badges")

tests/usethis/_backend/uv/test_init.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from pathlib import Path
22

33
from usethis._backend.uv.init import opinionated_uv_init
4+
from usethis._config import usethis_config
45
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
56
from usethis._test import change_cwd
7+
from usethis._types.build_backend import BuildBackendEnum
68

79

810
class TestOpinionatedUVINit:
@@ -13,3 +15,15 @@ def test_build_backend_is_hatch(self, tmp_path: Path):
1315

1416
# Assert
1517
assert manager[["build-system", "build-backend"]] == "hatchling.build"
18+
19+
def test_build_backend_is_uv(self, tmp_path: Path):
20+
with (
21+
change_cwd(tmp_path),
22+
PyprojectTOMLManager() as manager,
23+
usethis_config.set(build_backend=BuildBackendEnum.uv),
24+
):
25+
# Act
26+
opinionated_uv_init()
27+
28+
# Assert
29+
assert manager[["build-system", "build-backend"]] == "uv_build"

0 commit comments

Comments
 (0)