Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/cli/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ Supported options:
- `uv` to use the [uv](https://docs.astral.sh/uv) package manager
- `none` to not use a package manager backend and display messages for some operations.

- `--build-backend` to specify the build backend for the project. Defaults to `hatch`.

Possible values:
- `hatch` for [Hatchling](https://hatch.pypa.io/) (default)
- `uv` for [uv](https://docs.astral.sh/uv/concepts/build-backend/)

## `usethis arch`

Add recommended architecture analysis tools to the project (namely, [Import Linter](https://import-linter.readthedocs.io/en/stable/)), including:
Expand Down
6 changes: 3 additions & 3 deletions src/usethis/_backend/uv/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def opinionated_uv_init() -> None:
"init",
"--lib",
"--build-backend",
"hatch",
usethis_config.build_backend.value,
usethis_config.cpd().as_posix(),
],
change_toml=True,
Expand All @@ -43,8 +43,8 @@ def ensure_pyproject_toml_via_uv(*, author: bool = True) -> None:
"--bare",
"--vcs=none",
f"--author-from={author_from}",
"--build-backend", # https://github.com/usethis-python/usethis-python/issues/347
"hatch", # until https://github.com/astral-sh/uv/issues/3957
"--build-backend",
usethis_config.build_backend.value,
usethis_config.cpd().as_posix(),
],
change_toml=True,
Expand Down
9 changes: 2 additions & 7 deletions src/usethis/_backend/uv/version.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import json

from packaging.version import Version

from usethis._backend.uv.call import call_uv_subprocess
from usethis._backend.uv.errors import UVSubprocessFailedError
from usethis._fallback import FALLBACK_UV_VERSION
from usethis._fallback import FALLBACK_UV_VERSION, next_breaking_version


def get_uv_version() -> str:
Expand All @@ -26,7 +24,4 @@ def next_breaking_uv_version(version: str) -> str:
For versions with major >= 1, bumps the major version (e.g. 1.0.2 -> 2.0.0).
For versions with major == 0, bumps the minor version (e.g. 0.10.2 -> 0.11.0).
"""
v = Version(version)
if v.major >= 1:
return f"{v.major + 1}.0.0"
return f"0.{v.minor + 1}.0"
return next_breaking_version(version)
9 changes: 9 additions & 0 deletions src/usethis/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import TYPE_CHECKING, Literal

from usethis._types.backend import BackendEnum
from usethis._types.build_backend import BuildBackendEnum

if TYPE_CHECKING:
from collections.abc import Generator
Expand All @@ -17,6 +18,7 @@
OFFLINE_DEFAULT = False
QUIET_DEFAULT = False
BACKEND_DEFAULT = "auto"
BUILD_BACKEND_DEFAULT = "hatch"


@dataclass
Expand Down Expand Up @@ -47,6 +49,7 @@ class UsethisConfig:
instruct_only: bool = False
backend: BackendEnum = BackendEnum(BACKEND_DEFAULT) # noqa: RUF009
inferred_backend: Literal[BackendEnum.uv, BackendEnum.none] | None = None
build_backend: BuildBackendEnum = BuildBackendEnum(BUILD_BACKEND_DEFAULT) # noqa: RUF009
disable_pre_commit: bool = False
subprocess_verbose: bool = False
project_dir: Path | None = None
Expand All @@ -61,6 +64,7 @@ def set( # noqa: PLR0913, PLR0915
alert_only: bool | None = None,
instruct_only: bool | None = None,
backend: BackendEnum | None = None,
build_backend: BuildBackendEnum | None = None,
disable_pre_commit: bool | None = None,
subprocess_verbose: bool | None = None,
project_dir: Path | str | None = None,
Expand All @@ -73,6 +77,7 @@ def set( # noqa: PLR0913, PLR0915
old_instruct_only = self.instruct_only
old_backend = self.backend
old_inferred_backend = self.inferred_backend
old_build_backend = self.build_backend
old_disable_pre_commit = self.disable_pre_commit
old_subprocess_verbose = self.subprocess_verbose
old_project_dir = self.project_dir
Expand All @@ -89,6 +94,8 @@ def set( # noqa: PLR0913, PLR0915
instruct_only = self.instruct_only
if backend is None:
backend = self.backend
if build_backend is None:
build_backend = self.build_backend
if disable_pre_commit is None:
disable_pre_commit = old_disable_pre_commit
if subprocess_verbose is None:
Expand All @@ -104,6 +111,7 @@ def set( # noqa: PLR0913, PLR0915
self.backend = backend
if backend is not BackendEnum.auto:
self.inferred_backend = backend
self.build_backend = build_backend
self.disable_pre_commit = disable_pre_commit
self.subprocess_verbose = subprocess_verbose
if isinstance(project_dir, str):
Expand All @@ -117,6 +125,7 @@ def set( # noqa: PLR0913, PLR0915
self.instruct_only = old_instruct_only
self.backend = old_backend
self.inferred_backend = old_inferred_backend
self.build_backend = old_build_backend
self.disable_pre_commit = old_disable_pre_commit
self.subprocess_verbose = old_subprocess_verbose
self.project_dir = old_project_dir
Expand Down
15 changes: 15 additions & 0 deletions src/usethis/_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,24 @@
``tests/usethis/test_fallback.py``.
"""

from packaging.version import Version

FALLBACK_UV_VERSION = "0.10.12"
FALLBACK_HATCHLING_VERSION = "1.29.0"
FALLBACK_PRE_COMMIT_VERSION = "4.5.1"
FALLBACK_RUFF_VERSION = "v0.15.7"
FALLBACK_SYNC_WITH_UV_VERSION = "v0.5.0"
FALLBACK_PYPROJECT_FMT_VERSION = "v2.20.0"
FALLBACK_CODESPELL_VERSION = "v2.4.2"


def next_breaking_version(version: str) -> str:
"""Get the next breaking version for a version string, following semver.

For versions with major >= 1, bumps the major version (e.g. 1.0.2 -> 2.0.0).
For versions with major == 0, bumps the minor version (e.g. 0.10.2 -> 0.11.0).
"""
v = Version(version)
if v.major >= 1:
return f"{v.major + 1}.0.0"
return f"0.{v.minor + 1}.0"
30 changes: 27 additions & 3 deletions src/usethis/_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,30 @@
from usethis._config import usethis_config
from usethis._console import tick_print
from usethis._deps import get_project_deps
from usethis._fallback import (
FALLBACK_HATCHLING_VERSION,
FALLBACK_UV_VERSION,
next_breaking_version,
)
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._integrations.project.name import get_project_name
from usethis._types.backend import BackendEnum
from usethis._types.build_backend import BuildBackendEnum

_BUILD_SYSTEM_CONFIG: dict[BuildBackendEnum, tuple[list[str], str]] = {
BuildBackendEnum.hatch: (
[
f"hatchling>={FALLBACK_HATCHLING_VERSION},<{next_breaking_version(FALLBACK_HATCHLING_VERSION)}"
],
"hatchling.build",
),
BuildBackendEnum.uv: (
[
f"uv_build>={FALLBACK_UV_VERSION},<{next_breaking_version(FALLBACK_UV_VERSION)}"
],
"uv_build",
),
}


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

tick_print("Writing 'pyproject.toml'.")
backend = get_backend()
build_backend = usethis_config.build_backend
if backend is BackendEnum.uv:
ensure_pyproject_toml_via_uv(author=author)
elif backend is BackendEnum.none:
requires, build_backend_str = _BUILD_SYSTEM_CONFIG[build_backend]
requires_str = ", ".join(f'"{r}"' for r in requires)
(usethis_config.cpd() / "pyproject.toml").write_text(
f"""\
[project]
Expand All @@ -114,15 +138,15 @@ def ensure_pyproject_toml(*, author: bool = True) -> None:
dependencies = []

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
requires = [{requires_str}]
build-backend = "{build_backend_str}"
""",
encoding="utf-8",
)
else:
assert_never(backend)

if not (
if build_backend is BuildBackendEnum.hatch and not (
(usethis_config.cpd() / "src").exists()
and (usethis_config.cpd() / "src").is_dir()
):
Expand Down
12 changes: 12 additions & 0 deletions src/usethis/_types/build_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from enum import Enum


class BuildBackendEnum(Enum):
"""Enumeration of available build backends for project initialization.

These correspond to a subset of the build backends supported by
`uv init --build-backend`.
"""

hatch = "hatch"
uv = "uv"
5 changes: 5 additions & 0 deletions src/usethis/_ui/interface/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@

from usethis._config import usethis_config
from usethis._types.backend import BackendEnum
from usethis._types.build_backend import BuildBackendEnum
from usethis._types.ci import CIServiceEnum
from usethis._types.docstyle import DocStyleEnum
from usethis._types.status import DevelopmentStatusEnum
from usethis._ui.options import (
backend_opt,
frozen_opt,
init_arch_opt,
init_build_backend_opt,
init_ci_opt,
init_doc_opt,
init_docstyle_opt,
Expand Down Expand Up @@ -46,6 +48,7 @@ def init(
quiet: bool = quiet_opt,
frozen: bool = frozen_opt,
backend: BackendEnum = backend_opt,
build_backend: BuildBackendEnum = init_build_backend_opt,
path: str | None = init_path_arg,
) -> None:
"""Initialize a new project with recommended tooling."""
Expand All @@ -54,6 +57,7 @@ def init(
from usethis.errors import UsethisError

assert isinstance(backend, BackendEnum)
assert isinstance(build_backend, BuildBackendEnum)

if path is not None:
path_ = Path(path)
Expand All @@ -68,6 +72,7 @@ def init(
quiet=quiet,
frozen=frozen,
backend=backend,
build_backend=build_backend,
project_dir=path,
),
files_manager(),
Expand Down
6 changes: 6 additions & 0 deletions src/usethis/_ui/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from usethis._config import (
BACKEND_DEFAULT,
BUILD_BACKEND_DEFAULT,
FROZEN_DEFAULT,
HOW_DEFAULT,
OFFLINE_DEFAULT,
Expand Down Expand Up @@ -106,6 +107,11 @@
None,
help="The path to use for the project. Defaults to the current working directory.",
)
init_build_backend_opt = typer.Option(
BUILD_BACKEND_DEFAULT,
"--build-backend",
help="The build backend to use for the project.",
)

# readme command options
badges_opt = typer.Option(False, "--badges", help="Add relevant badges")
Expand Down
14 changes: 14 additions & 0 deletions tests/usethis/_backend/uv/test_init.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from pathlib import Path

from usethis._backend.uv.init import opinionated_uv_init
from usethis._config import usethis_config
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._test import change_cwd
from usethis._types.build_backend import BuildBackendEnum


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

# Assert
assert manager[["build-system", "build-backend"]] == "hatchling.build"

def test_build_backend_is_uv(self, tmp_path: Path):
with (
change_cwd(tmp_path),
PyprojectTOMLManager() as manager,
usethis_config.set(build_backend=BuildBackendEnum.uv),
):
# Act
opinionated_uv_init()

# Assert
assert manager[["build-system", "build-backend"]] == "uv_build"
23 changes: 23 additions & 0 deletions tests/usethis/_ui/interface/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,26 @@ def test_none_backend(self, tmp_path: Path):
"☐ Add the dev dependency 'ty'.\n"
"☐ Run 'ty check' to run the ty type checker.\n"
)

def test_build_backend_uv(self, tmp_path: Path):
# Act
runner = CliRunner()
with change_cwd(tmp_path):
result = runner.invoke_safe(app, ["init", "--build-backend", "uv"])

# Assert
assert result.exit_code == 0, result.output
assert (tmp_path / "pyproject.toml").exists()
content = (tmp_path / "pyproject.toml").read_text()
assert 'build-backend = "uv_build"' in content

def test_build_backend_default_is_hatch(self, tmp_path: Path):
# Act
runner = CliRunner()
with change_cwd(tmp_path):
result = runner.invoke_safe(app, ["init"])

# Assert
assert result.exit_code == 0, result.output
content = (tmp_path / "pyproject.toml").read_text()
assert 'build-backend = "hatchling.build"' in content
35 changes: 35 additions & 0 deletions tests/usethis/test_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
from usethis._config import usethis_config
from usethis._fallback import (
FALLBACK_CODESPELL_VERSION,
FALLBACK_HATCHLING_VERSION,
FALLBACK_PRE_COMMIT_VERSION,
FALLBACK_PYPROJECT_FMT_VERSION,
FALLBACK_RUFF_VERSION,
FALLBACK_SYNC_WITH_UV_VERSION,
FALLBACK_UV_VERSION,
next_breaking_version,
)
from usethis._integrations.ci.github.errors import GitHubTagError
from usethis._integrations.ci.github.tags import get_github_latest_tag
Expand Down Expand Up @@ -40,6 +42,22 @@ def test_latest_version(self):
raise err


class TestFallbackHatchlingVersion:
@pytest.mark.usefixtures("_vary_network_conn")
def test_latest_version(self):
if os.getenv("CI"):
pytest.skip("Avoid flaky pipelines by testing version bumps manually")

try:
assert (
get_github_latest_tag(owner="pypa", repo="hatch")
== f"hatchling-v{FALLBACK_HATCHLING_VERSION}"
)
except GitHubTagError as err:
_skip_on_github_error(err)
raise err


class TestPreCommitVersion:
@pytest.mark.usefixtures("_vary_network_conn")
def test_latest_version(self):
Expand Down Expand Up @@ -120,3 +138,20 @@ def test_latest_version(self):
except GitHubTagError as err:
_skip_on_github_error(err)
raise err


class TestNextBreakingVersion:
def test_pre_one_bumps_minor(self):
assert next_breaking_version("0.10.2") == "0.11.0"

def test_post_one_bumps_major(self):
assert next_breaking_version("1.0.2") == "2.0.0"

def test_pre_one_zero_minor(self):
assert next_breaking_version("0.0.5") == "0.1.0"

def test_exact_one(self):
assert next_breaking_version("1.0.0") == "2.0.0"

def test_high_major(self):
assert next_breaking_version("3.2.1") == "4.0.0"
Loading
Loading