From 41761ffb9819109c944b654ff17b8adda41b758d Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Sun, 29 Jun 2025 18:56:09 +1200 Subject: [PATCH 01/55] First draft of adding --backend flag to the usethis tool interface --- pyproject.toml | 13 +- src/usethis/_backend.py | 16 ++ src/usethis/_config.py | 37 ++-- src/usethis/_config_file.py | 2 +- src/usethis/_core/author.py | 2 +- src/usethis/_core/ci.py | 2 +- src/usethis/_core/docstyle.py | 2 +- src/usethis/_core/show.py | 2 +- src/usethis/_core/status.py | 4 +- src/usethis/_core/tool.py | 4 +- .../{_integrations/uv/deps.py => _deps.py} | 169 +++++++++--------- .../backend}/__init__.py | 0 .../{ => backend}/uv/__init__.py | 0 .../_integrations/{ => backend}/uv/call.py | 23 ++- src/usethis/_integrations/backend/uv/deps.py | 50 ++++++ .../_integrations/{ => backend}/uv/errors.py | 4 +- .../_integrations/{ => backend}/uv/init.py | 4 +- .../{ => backend}/uv/link_mode.py | 2 +- .../_integrations/{ => backend}/uv/python.py | 4 +- .../_integrations/{ => backend}/uv/toml.py | 0 .../_integrations/{ => backend}/uv/used.py | 0 .../_integrations/ci/bitbucket/steps.py | 2 +- src/usethis/_integrations/pre_commit/core.py | 6 +- src/usethis/_interface/docstyle.py | 2 +- src/usethis/_interface/init.py | 8 +- src/usethis/_interface/readme.py | 2 +- src/usethis/_interface/status.py | 2 +- src/usethis/_interface/tool.py | 88 +++++++-- src/usethis/_options.py | 4 + src/usethis/_tool/base.py | 12 +- src/usethis/_tool/config.py | 2 +- src/usethis/_tool/impl/codespell.py | 4 +- src/usethis/_tool/impl/coverage_py.py | 6 +- src/usethis/_tool/impl/deptry.py | 4 +- src/usethis/_tool/impl/import_linter.py | 4 +- src/usethis/_tool/impl/pre_commit.py | 6 +- src/usethis/_tool/impl/pyproject_fmt.py | 4 +- src/usethis/_tool/impl/pyproject_toml.py | 2 +- src/usethis/_tool/impl/pytest.py | 8 +- src/usethis/_tool/impl/requirements_txt.py | 4 +- src/usethis/_tool/impl/ruff.py | 4 +- src/usethis/_types/__init__.py | 0 src/usethis/_types/backend.py | 13 ++ src/usethis/{_core/enums => _types}/ci.py | 0 src/usethis/_types/deps.py | 13 ++ .../{_core/enums => _types}/docstyle.py | 0 src/usethis/{_core/enums => _types}/status.py | 0 src/usethis/errors.py | 11 ++ tests/conftest.py | 2 +- tests/test_install.py | 2 +- tests/usethis/_core/test_core_tool.py | 19 +- tests/usethis/_core/test_docstyle.py | 4 +- tests/usethis/_core/test_rule.py | 3 +- tests/usethis/_core/test_status.py | 2 +- .../pre_commit/test_pre_commit_core.py | 3 +- .../sonarqube/test_sonarqube_config.py | 4 +- tests/usethis/_integrations/uv/test_call.py | 10 +- tests/usethis/_integrations/uv/test_deps.py | 23 ++- tests/usethis/_integrations/uv/test_init.py | 13 +- .../_integrations/uv/test_link_mode.py | 4 +- tests/usethis/_integrations/uv/test_python.py | 10 +- tests/usethis/_integrations/uv/test_used.py | 2 +- tests/usethis/_interface/test_docstyle.py | 2 +- tests/usethis/_interface/test_format_.py | 3 +- tests/usethis/_interface/test_lint.py | 3 +- tests/usethis/_interface/test_spellcheck.py | 3 +- tests/usethis/_interface/test_test.py | 3 +- tests/usethis/_interface/test_tool.py | 2 +- tests/usethis/_tool/test_base.py | 3 +- tests/usethis/test_config.py | 9 +- 70 files changed, 434 insertions(+), 246 deletions(-) create mode 100644 src/usethis/_backend.py rename src/usethis/{_integrations/uv/deps.py => _deps.py} (64%) rename src/usethis/{_core/enums => _integrations/backend}/__init__.py (100%) rename src/usethis/_integrations/{ => backend}/uv/__init__.py (100%) rename src/usethis/_integrations/{ => backend}/uv/call.py (70%) create mode 100644 src/usethis/_integrations/backend/uv/deps.py rename src/usethis/_integrations/{ => backend}/uv/errors.py (85%) rename src/usethis/_integrations/{ => backend}/uv/init.py (93%) rename src/usethis/_integrations/{ => backend}/uv/link_mode.py (94%) rename src/usethis/_integrations/{ => backend}/uv/python.py (90%) rename src/usethis/_integrations/{ => backend}/uv/toml.py (100%) rename src/usethis/_integrations/{ => backend}/uv/used.py (100%) create mode 100644 src/usethis/_types/__init__.py create mode 100644 src/usethis/_types/backend.py rename src/usethis/{_core/enums => _types}/ci.py (100%) create mode 100644 src/usethis/_types/deps.py rename src/usethis/{_core/enums => _types}/docstyle.py (100%) rename src/usethis/{_core/enums => _types}/status.py (100%) diff --git a/pyproject.toml b/pyproject.toml index bd643186..0f6aad5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -179,16 +179,22 @@ name = "usethis" type = "layers" layers = [ "_test | __main__", + # UI "_app", "_interface", "_options", + # Tool implementations "_toolset", "_core", "_tool", - "_config_file", + # Specific config file and backend implementations + "_deps", + "_config_file | _backend", "_integrations", "_io | _pipeweld | _subprocess | _console", - "_config | errors", + # Global state and constants + "_config", + "_types | errors", ] containers = [ "usethis" ] exhaustive = true @@ -211,7 +217,6 @@ layers = [ # docstyle uses (Ruff) tool, badge uses readme "badge | docstyle | list | rule", "author | browse | ci | readme | show | status | tool", - "enums", ] containers = [ "usethis._core" ] exhaustive = true @@ -246,7 +251,7 @@ name = "usethis._integrations" type = "layers" layers = [ "ci | pre_commit", - "uv | pytest | pydantic | sonarqube", + "backend | pytest | pydantic | sonarqube", "project | python", "file", ] diff --git a/src/usethis/_backend.py b/src/usethis/_backend.py new file mode 100644 index 00000000..c9966d08 --- /dev/null +++ b/src/usethis/_backend.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from typing import Literal + +from usethis._config import usethis_config +from usethis._integrations.backend.uv.used import is_uv_used +from usethis._types.backend import BackendEnum + + +def get_backend() -> Literal[BackendEnum.uv, BackendEnum.none]: + if usethis_config.backend is not BackendEnum.auto: + return usethis_config.backend + + if is_uv_used(): + return BackendEnum.uv + return BackendEnum.none diff --git a/src/usethis/_config.py b/src/usethis/_config.py index 450fb918..95e682b2 100644 --- a/src/usethis/_config.py +++ b/src/usethis/_config.py @@ -5,14 +5,18 @@ from pathlib import Path from typing import TYPE_CHECKING +from usethis._types.backend import BackendEnum + if TYPE_CHECKING: from collections.abc import Generator + HOW_DEFAULT = False REMOVE_DEFAULT = False FROZEN_DEFAULT = False OFFLINE_DEFAULT = False QUIET_DEFAULT = False +BACKEND_DEFAULT = "auto" @dataclass @@ -24,7 +28,8 @@ class UsethisConfig: quiet: Suppress all output, regardless of any other options. frozen: Do not install dependencies, nor update lockfiles. alert_only: Suppress all output except for warnings and errors. - disable_uv_subprocess: Raise an error if a uv subprocess invocation is tried. + backend: The package manager backend to use. Attempted subprocesses to other + backends will raise an error. disable_pre_commit: Disable pre-commit integrations. Assume that pre-commit is never used (unless explicitly requested via a function whose purpose is to modify pre-commit configuration). @@ -33,11 +38,11 @@ class UsethisConfig: working directory dynamically determined at runtime. """ - offline: bool = False - quiet: bool = False - frozen: bool = False + offline: bool = OFFLINE_DEFAULT + quiet: bool = QUIET_DEFAULT + frozen: bool = FROZEN_DEFAULT alert_only: bool = False - disable_uv_subprocess: bool = False + backend: BackendEnum = BackendEnum(BACKEND_DEFAULT) # noqa: RUF009 disable_pre_commit: bool = False subprocess_verbose: bool = False project_dir: Path | None = None @@ -50,7 +55,7 @@ def set( # noqa: PLR0913 quiet: bool | None = None, frozen: bool | None = None, alert_only: bool | None = None, - disable_uv_subprocess: bool | None = None, + backend: BackendEnum | None = None, disable_pre_commit: bool | None = None, subprocess_verbose: bool | None = None, project_dir: Path | str | None = None, @@ -60,7 +65,7 @@ def set( # noqa: PLR0913 old_quiet = self.quiet old_frozen = self.frozen old_alert_only = self.alert_only - old_disable_uv_subprocess = self.disable_uv_subprocess + old_backend = self.backend old_disable_pre_commit = self.disable_pre_commit old_subprocess_verbose = self.subprocess_verbose old_project_dir = self.project_dir @@ -73,8 +78,8 @@ def set( # noqa: PLR0913 frozen = old_frozen if alert_only is None: alert_only = self.alert_only - if disable_uv_subprocess is None: - disable_uv_subprocess = old_disable_uv_subprocess + if backend is None: + backend = self.backend if disable_pre_commit is None: disable_pre_commit = old_disable_pre_commit if subprocess_verbose is None: @@ -86,7 +91,7 @@ def set( # noqa: PLR0913 self.quiet = quiet self.frozen = frozen self.alert_only = alert_only - self.disable_uv_subprocess = disable_uv_subprocess + self.backend = backend self.disable_pre_commit = disable_pre_commit self.subprocess_verbose = subprocess_verbose if isinstance(project_dir, str): @@ -97,7 +102,7 @@ def set( # noqa: PLR0913 self.quiet = old_quiet self.frozen = old_frozen self.alert_only = old_alert_only - self.disable_uv_subprocess = old_disable_uv_subprocess + self.backend = old_backend self.disable_pre_commit = old_disable_pre_commit self.subprocess_verbose = old_subprocess_verbose self.project_dir = old_project_dir @@ -109,12 +114,4 @@ def cpd(self) -> Path: return self.project_dir -usethis_config = UsethisConfig( - offline=OFFLINE_DEFAULT, - quiet=QUIET_DEFAULT, - frozen=FROZEN_DEFAULT, - alert_only=False, - disable_uv_subprocess=False, - disable_pre_commit=False, - subprocess_verbose=False, -) +usethis_config = UsethisConfig() diff --git a/src/usethis/_config_file.py b/src/usethis/_config_file.py index fb739b86..292347cd 100644 --- a/src/usethis/_config_file.py +++ b/src/usethis/_config_file.py @@ -4,11 +4,11 @@ from pathlib import Path from typing import TYPE_CHECKING +from usethis._integrations.backend.uv.toml import UVTOMLManager from usethis._integrations.file.ini.io_ import INIFileManager from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._integrations.file.setup_cfg.io_ import SetupCFGManager from usethis._integrations.file.toml.io_ import TOMLFileManager -from usethis._integrations.uv.toml import UVTOMLManager if TYPE_CHECKING: from collections.abc import Iterator diff --git a/src/usethis/_core/author.py b/src/usethis/_core/author.py index f0076982..5ee5aa8f 100644 --- a/src/usethis/_core/author.py +++ b/src/usethis/_core/author.py @@ -1,5 +1,5 @@ +from usethis._integrations.backend.uv.init import ensure_pyproject_toml from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.uv.init import ensure_pyproject_toml def add_author( diff --git a/src/usethis/_core/ci.py b/src/usethis/_core/ci.py index fe5b64dc..cc5cac10 100644 --- a/src/usethis/_core/ci.py +++ b/src/usethis/_core/ci.py @@ -1,11 +1,11 @@ from __future__ import annotations from usethis._console import box_print, info_print +from usethis._integrations.backend.uv.init import ensure_pyproject_toml from usethis._integrations.ci.bitbucket.config import ( add_bitbucket_pipeline_config, remove_bitbucket_pipeline_config, ) -from usethis._integrations.uv.init import ensure_pyproject_toml from usethis._tool.impl.codespell import CodespellTool from usethis._tool.impl.deptry import DeptryTool from usethis._tool.impl.pre_commit import PreCommitTool diff --git a/src/usethis/_core/docstyle.py b/src/usethis/_core/docstyle.py index 7a738b33..34cabdcb 100644 --- a/src/usethis/_core/docstyle.py +++ b/src/usethis/_core/docstyle.py @@ -6,7 +6,7 @@ from usethis._tool.impl.ruff import RuffTool if TYPE_CHECKING: - from usethis._core.enums.docstyle import DocStyleEnum + from usethis._types.docstyle import DocStyleEnum def use_docstyle(style: DocStyleEnum) -> None: diff --git a/src/usethis/_core/show.py b/src/usethis/_core/show.py index 690fde11..54888a32 100644 --- a/src/usethis/_core/show.py +++ b/src/usethis/_core/show.py @@ -2,9 +2,9 @@ from usethis._config import usethis_config from usethis._console import plain_print +from usethis._integrations.backend.uv.init import ensure_pyproject_toml from usethis._integrations.project.name import get_project_name from usethis._integrations.sonarqube.config import get_sonar_project_properties -from usethis._integrations.uv.init import ensure_pyproject_toml def show_name() -> None: diff --git a/src/usethis/_core/status.py b/src/usethis/_core/status.py index 054f35b4..9299d82e 100644 --- a/src/usethis/_core/status.py +++ b/src/usethis/_core/status.py @@ -1,9 +1,9 @@ from __future__ import annotations from usethis._console import tick_print -from usethis._core.enums.status import DevelopmentStatusEnum +from usethis._integrations.backend.uv.init import ensure_pyproject_toml from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.uv.init import ensure_pyproject_toml +from usethis._types.status import DevelopmentStatusEnum def use_development_status( diff --git a/src/usethis/_core/tool.py b/src/usethis/_core/tool.py index 237e156f..1db323c0 100644 --- a/src/usethis/_core/tool.py +++ b/src/usethis/_core/tool.py @@ -8,6 +8,8 @@ from usethis._config import usethis_config from usethis._console import box_print, tick_print +from usethis._integrations.backend.uv.call import call_uv_subprocess +from usethis._integrations.backend.uv.init import ensure_pyproject_toml from usethis._integrations.ci.bitbucket.used import is_bitbucket_used from usethis._integrations.file.pyproject_toml.valid import ensure_pyproject_validity from usethis._integrations.pre_commit.core import ( @@ -21,8 +23,6 @@ get_hook_ids, ) from usethis._integrations.pytest.core import add_pytest_dir, remove_pytest_dir -from usethis._integrations.uv.call import call_uv_subprocess -from usethis._integrations.uv.init import ensure_pyproject_toml from usethis._tool.all_ import ALL_TOOLS from usethis._tool.impl.codespell import CodespellTool from usethis._tool.impl.coverage_py import CoveragePyTool diff --git a/src/usethis/_integrations/uv/deps.py b/src/usethis/_deps.py similarity index 64% rename from src/usethis/_integrations/uv/deps.py rename to src/usethis/_deps.py index 9816c2f4..34ef85f4 100644 --- a/src/usethis/_integrations/uv/deps.py +++ b/src/usethis/_deps.py @@ -2,26 +2,21 @@ import pydantic from packaging.requirements import Requirement -from pydantic import BaseModel, TypeAdapter +from pydantic import TypeAdapter +from typing_extensions import assert_never +from usethis._backend import get_backend from usethis._config import usethis_config from usethis._console import box_print, tick_print +from usethis._integrations.backend.uv.call import add_default_groups_via_uv +from usethis._integrations.backend.uv.deps import ( + add_dep_to_group_via_uv, + get_default_groups_via_uv, +) from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.uv.call import call_uv_subprocess -from usethis._integrations.uv.errors import UVDepGroupError, UVSubprocessFailedError -from usethis._integrations.uv.toml import UVTOMLManager - - -class Dependency(BaseModel): - name: str - extras: frozenset[str] = frozenset() - - def __str__(self) -> str: - extras = sorted(self.extras or set()) - return self.name + "".join(f"[{extra}]" for extra in extras) - - def __hash__(self) -> int: - return hash((self.__class__.__name__, self.name, self.extras)) +from usethis._types.backend import BackendEnum +from usethis._types.deps import Dependency +from usethis.errors import DepGroupError def get_dep_groups() -> dict[str, list[Dependency]]: @@ -33,8 +28,8 @@ def get_dep_groups() -> dict[str, list[Dependency]]: try: dep_groups_section = pyproject["dependency-groups"] except KeyError: - # In the past might have been in [tool.uv.dev-dependencies] section but this - # will be deprecated. + # In the past might have been in [tool.uv.dev-dependencies] section when using + # uv but this will be deprecated, so we don't support it in usethis. return {} try: @@ -47,7 +42,7 @@ def get_dep_groups() -> dict[str, list[Dependency]]: f"{err}\n\n" "Please check the section and try again." ) - raise UVDepGroupError(msg) from None + raise DepGroupError(msg) from None reqs_by_group = { group: [Requirement(req_str) for req_str in req_strs] for group, req_strs in req_strs_by_group.items() @@ -94,26 +89,25 @@ def register_default_group(group: str) -> None: def add_default_groups(groups: list[str]) -> None: - if UVTOMLManager().path.exists(): - UVTOMLManager().extend_list(keys=["default-groups"], values=groups) + backend = get_backend() + if backend is BackendEnum.uv: + add_default_groups_via_uv(groups) + elif backend is BackendEnum.none: + # This is not really a meaningful concept without a package manager + pass else: - PyprojectTOMLManager().extend_list( - keys=["tool", "uv", "default-groups"], values=groups - ) + assert_never(backend) def get_default_groups() -> list[str]: - try: - if UVTOMLManager().path.exists(): - default_groups = UVTOMLManager()[["default-groups"]] - else: - default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] - if not isinstance(default_groups, list): - default_groups = [] - except KeyError: - default_groups = [] - - return default_groups + backend = get_backend() + if backend is BackendEnum.uv: + return get_default_groups_via_uv() + elif backend is BackendEnum.none: + # This is not really a meaningful concept without a package manager + return [] + else: + assert_never(backend) def ensure_dev_group_is_defined() -> None: @@ -121,41 +115,6 @@ def ensure_dev_group_is_defined() -> None: PyprojectTOMLManager().extend_list(keys=["dependency-groups", "dev"], values=[]) -def add_deps_to_group(deps: list[Dependency], group: str) -> None: - """Add a package as a non-build dependency using PEP 735 dependency groups.""" - existing_group = get_deps_from_group(group) - - to_add_deps = [ - dep for dep in deps if not is_dep_satisfied_in(dep, in_=existing_group) - ] - - if not to_add_deps: - return - - deps_str = ", ".join([f"'{dep}'" for dep in to_add_deps]) - ies = "y" if len(to_add_deps) == 1 else "ies" - tick_print( - f"Adding dependenc{ies} {deps_str} to the '{group}' group in 'pyproject.toml'." - ) - - if usethis_config.frozen: - box_print(f"Install the dependenc{ies} {deps_str}.") - - for dep in to_add_deps: - try: - call_uv_subprocess( - ["add", "--group", group, str(dep)], - change_toml=True, - ) - except UVSubprocessFailedError as err: - msg = f"Failed to add '{dep}' to the '{group}' dependency group:\n{err}" - msg += (usethis_config.cpd() / "pyproject.toml").read_text() - raise UVDepGroupError(msg) from None - - # Register the group - don't do this before adding the deps in case that step fails - register_default_group(group) - - def is_dep_satisfied_in(dep: Dependency, *, in_: list[Dependency]) -> bool: return any(_is_dep_satisfied_by(dep, by=by) for by in in_) @@ -176,21 +135,69 @@ def remove_deps_from_group(deps: list[Dependency], group: str) -> None: deps_str = ", ".join([f"'{dep}'" for dep in _deps]) ies = "y" if len(_deps) == 1 else "ies" - tick_print( - f"Removing dependenc{ies} {deps_str} from the '{group}' group in 'pyproject.toml'." - ) + backend = get_backend() - for dep in _deps: - try: - call_uv_subprocess(["remove", "--group", group, str(dep)], change_toml=True) - except UVSubprocessFailedError as err: - msg = ( - f"Failed to remove '{dep}' from the '{group}' dependency group:\n{err}" - ) - raise UVDepGroupError(msg) from None + if backend is BackendEnum.uv: + tick_print( + f"Removing dependenc{ies} {deps_str} from the '{group}' group in 'pyproject.toml'." + ) + for dep in _deps: + add_dep_to_group_via_uv(dep, group) + elif backend is BackendEnum.none: + box_print(f"Remove the {group} dependenc{ies} {deps_str}.") + else: + assert_never(backend) def is_dep_in_any_group(dep: Dependency) -> bool: return is_dep_satisfied_in( dep, in_=[dep for group in get_dep_groups().values() for dep in group] ) + + +def add_deps_to_group(deps: list[Dependency], group: str) -> None: + """Add a package as a non-build dependency using PEP 735 dependency groups.""" + existing_group = get_deps_from_group(group) + + to_add_deps = [ + dep for dep in deps if not is_dep_satisfied_in(dep, in_=existing_group) + ] + + if not to_add_deps: + return + + backend = get_backend() + + # Message regarding declaration of the dependencies + deps_str = ", ".join([f"'{dep}'" for dep in to_add_deps]) + ies = "y" if len(to_add_deps) == 1 else "ies" + if backend is BackendEnum.uv: + tick_print( + f"Adding dependenc{ies} {deps_str} to the '{group}' group in 'pyproject.toml'." + ) + elif backend is BackendEnum.none: + box_print(f"Install the {group} dependenc{ies} {deps_str}.") + else: + assert_never(backend) + + # Installation of the dependencies, and declaration if the package manager supports + # a combined workflow. + if usethis_config.frozen: + box_print(f"Install the dependenc{ies} {deps_str}.") + for dep in to_add_deps: + if backend is BackendEnum.uv: + add_dep_to_group_via_uv(dep, group) + elif backend is BackendEnum.none: + # We've already used a combined message + pass + else: + assert_never(backend) + + # Register the group - don't do this before adding the deps in case that step fails + if backend is BackendEnum.uv: + register_default_group(group) + elif backend is BackendEnum.none: + # This is not really a meaningful concept without a package manager + pass + else: + assert_never(backend) diff --git a/src/usethis/_core/enums/__init__.py b/src/usethis/_integrations/backend/__init__.py similarity index 100% rename from src/usethis/_core/enums/__init__.py rename to src/usethis/_integrations/backend/__init__.py diff --git a/src/usethis/_integrations/uv/__init__.py b/src/usethis/_integrations/backend/uv/__init__.py similarity index 100% rename from src/usethis/_integrations/uv/__init__.py rename to src/usethis/_integrations/backend/uv/__init__.py diff --git a/src/usethis/_integrations/uv/call.py b/src/usethis/_integrations/backend/uv/call.py similarity index 70% rename from src/usethis/_integrations/uv/call.py rename to src/usethis/_integrations/backend/uv/call.py index e0582ebb..e40100f1 100644 --- a/src/usethis/_integrations/uv/call.py +++ b/src/usethis/_integrations/backend/uv/call.py @@ -1,13 +1,16 @@ from __future__ import annotations from usethis._config import usethis_config +from usethis._integrations.backend.uv.errors import UVSubprocessFailedError +from usethis._integrations.backend.uv.link_mode import ensure_symlink_mode +from usethis._integrations.backend.uv.toml import UVTOMLManager from usethis._integrations.file.pyproject_toml.io_ import ( PyprojectTOMLManager, ) from usethis._integrations.file.pyproject_toml.valid import ensure_pyproject_validity -from usethis._integrations.uv.errors import UVSubprocessFailedError -from usethis._integrations.uv.link_mode import ensure_symlink_mode from usethis._subprocess import SubprocessFailedError, call_subprocess +from usethis._types.backend import BackendEnum +from usethis.errors import ForbiddenBackendError def call_uv_subprocess(args: list[str], change_toml: bool) -> str: @@ -19,9 +22,9 @@ def call_uv_subprocess(args: list[str], change_toml: bool) -> str: Raises: UVSubprocessFailedError: If the subprocess fails. """ - if usethis_config.disable_uv_subprocess: - msg = "The `disable_uv_subprocess` option is set." - raise UVSubprocessFailedError(msg) + if usethis_config.backend not in {BackendEnum.uv, BackendEnum.auto}: + msg = f"The '{usethis_config.backend.value}' backend is enabled, but a uv subprocess was invoked." + raise ForbiddenBackendError(msg) is_pyproject_toml = (usethis_config.cpd() / "pyproject.toml").exists() @@ -77,3 +80,13 @@ def call_uv_subprocess(args: list[str], change_toml: bool) -> str: PyprojectTOMLManager().read_file() return output + + +def add_default_groups_via_uv(groups: list[str]) -> None: + """Add default groups using the uv command-line tool.""" + if UVTOMLManager().path.exists(): + UVTOMLManager().extend_list(keys=["default-groups"], values=groups) + else: + PyprojectTOMLManager().extend_list( + keys=["tool", "uv", "default-groups"], values=groups + ) diff --git a/src/usethis/_integrations/backend/uv/deps.py b/src/usethis/_integrations/backend/uv/deps.py new file mode 100644 index 00000000..8dd926a3 --- /dev/null +++ b/src/usethis/_integrations/backend/uv/deps.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from usethis._config import usethis_config +from usethis._integrations.backend.uv.call import call_uv_subprocess +from usethis._integrations.backend.uv.errors import ( + UVDepGroupError, + UVSubprocessFailedError, +) +from usethis._integrations.backend.uv.toml import UVTOMLManager +from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager + +if TYPE_CHECKING: + from usethis._types.deps import Dependency + + +def add_dep_to_group_via_uv(dep: Dependency, group: str): + try: + call_uv_subprocess( + ["add", "--group", group, str(dep)], + change_toml=True, + ) + except UVSubprocessFailedError as err: + msg = f"Failed to add '{dep}' to the '{group}' dependency group:\n{err}" + msg += (usethis_config.cpd() / "pyproject.toml").read_text() + raise UVDepGroupError(msg) from None + + +def remove_dep_from_group_via_uv(dep: Dependency, group: str): + try: + call_uv_subprocess(["remove", "--group", group, str(dep)], change_toml=True) + except UVSubprocessFailedError as err: + msg = f"Failed to remove '{dep}' from the '{group}' dependency group:\n{err}" + raise UVDepGroupError(msg) from None + + +def get_default_groups_via_uv() -> list[str]: + """Get the default dependency groups from the uv configuration.""" + try: + if UVTOMLManager().path.exists(): + default_groups = UVTOMLManager()[["default-groups"]] + else: + default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] + if not isinstance(default_groups, list): + default_groups = [] + except KeyError: + default_groups = [] + + return default_groups diff --git a/src/usethis/_integrations/uv/errors.py b/src/usethis/_integrations/backend/uv/errors.py similarity index 85% rename from src/usethis/_integrations/uv/errors.py rename to src/usethis/_integrations/backend/uv/errors.py index 9fa711a2..da700e78 100644 --- a/src/usethis/_integrations/uv/errors.py +++ b/src/usethis/_integrations/backend/uv/errors.py @@ -1,13 +1,13 @@ from __future__ import annotations -from usethis.errors import UsethisError +from usethis.errors import DepGroupError, UsethisError class UVError(UsethisError): """Base class for exceptions relating to uv.""" -class UVDepGroupError(UVError): +class UVDepGroupError(DepGroupError): """Raised when adding or removing a dependency from a group fails.""" diff --git a/src/usethis/_integrations/uv/init.py b/src/usethis/_integrations/backend/uv/init.py similarity index 93% rename from src/usethis/_integrations/uv/init.py rename to src/usethis/_integrations/backend/uv/init.py index c195b05b..c537d9e7 100644 --- a/src/usethis/_integrations/uv/init.py +++ b/src/usethis/_integrations/backend/uv/init.py @@ -2,10 +2,10 @@ from usethis._config import usethis_config from usethis._console import tick_print +from usethis._integrations.backend.uv import call +from usethis._integrations.backend.uv.errors import UVInitError, UVSubprocessFailedError from usethis._integrations.file.pyproject_toml.errors import PyprojectTOMLInitError from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.uv import call -from usethis._integrations.uv.errors import UVInitError, UVSubprocessFailedError def opinionated_uv_init() -> None: diff --git a/src/usethis/_integrations/uv/link_mode.py b/src/usethis/_integrations/backend/uv/link_mode.py similarity index 94% rename from src/usethis/_integrations/uv/link_mode.py rename to src/usethis/_integrations/backend/uv/link_mode.py index 37e619a7..53d03364 100644 --- a/src/usethis/_integrations/uv/link_mode.py +++ b/src/usethis/_integrations/backend/uv/link_mode.py @@ -1,10 +1,10 @@ import contextlib +from usethis._integrations.backend.uv.toml import UVTOMLManager from usethis._integrations.file.pyproject_toml.errors import ( PyprojectTOMLValueAlreadySetError, ) from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.uv.toml import UVTOMLManager def ensure_symlink_mode() -> None: diff --git a/src/usethis/_integrations/uv/python.py b/src/usethis/_integrations/backend/uv/python.py similarity index 90% rename from src/usethis/_integrations/uv/python.py rename to src/usethis/_integrations/backend/uv/python.py index fdd5315e..e50f94de 100644 --- a/src/usethis/_integrations/uv/python.py +++ b/src/usethis/_integrations/backend/uv/python.py @@ -2,6 +2,8 @@ import re +from usethis._integrations.backend.uv.call import call_uv_subprocess +from usethis._integrations.backend.uv.errors import UVUnparsedPythonVersionError from usethis._integrations.file.pyproject_toml.requires_python import ( MissingRequiresPythonError, get_requires_python, @@ -10,8 +12,6 @@ extract_major_version, get_python_version, ) -from usethis._integrations.uv.call import call_uv_subprocess -from usethis._integrations.uv.errors import UVUnparsedPythonVersionError def get_available_python_versions() -> set[str]: diff --git a/src/usethis/_integrations/uv/toml.py b/src/usethis/_integrations/backend/uv/toml.py similarity index 100% rename from src/usethis/_integrations/uv/toml.py rename to src/usethis/_integrations/backend/uv/toml.py diff --git a/src/usethis/_integrations/uv/used.py b/src/usethis/_integrations/backend/uv/used.py similarity index 100% rename from src/usethis/_integrations/uv/used.py rename to src/usethis/_integrations/backend/uv/used.py diff --git a/src/usethis/_integrations/ci/bitbucket/steps.py b/src/usethis/_integrations/ci/bitbucket/steps.py index a49ea410..ef91bb4a 100644 --- a/src/usethis/_integrations/ci/bitbucket/steps.py +++ b/src/usethis/_integrations/ci/bitbucket/steps.py @@ -10,6 +10,7 @@ import usethis._pipeweld.func from usethis._config import usethis_config from usethis._console import box_print, tick_print +from usethis._integrations.backend.uv.python import get_supported_major_python_versions from usethis._integrations.ci.bitbucket.anchor import ScriptItemAnchor from usethis._integrations.ci.bitbucket.cache import _add_caches_via_doc, remove_cache from usethis._integrations.ci.bitbucket.dump import bitbucket_fancy_dump @@ -35,7 +36,6 @@ ) from usethis._integrations.ci.bitbucket.schema_utils import step1tostep from usethis._integrations.file.yaml.update import update_ruamel_yaml_map -from usethis._integrations.uv.python import get_supported_major_python_versions if TYPE_CHECKING: from ruamel.yaml.anchor import Anchor diff --git a/src/usethis/_integrations/pre_commit/core.py b/src/usethis/_integrations/pre_commit/core.py index e4a78750..43a34324 100644 --- a/src/usethis/_integrations/pre_commit/core.py +++ b/src/usethis/_integrations/pre_commit/core.py @@ -2,10 +2,10 @@ from usethis._config import usethis_config from usethis._console import box_print, info_print, tick_print +from usethis._integrations.backend.uv.call import call_uv_subprocess +from usethis._integrations.backend.uv.errors import UVSubprocessFailedError +from usethis._integrations.backend.uv.used import is_uv_used from usethis._integrations.pre_commit.errors import PreCommitInstallationError -from usethis._integrations.uv.call import call_uv_subprocess -from usethis._integrations.uv.errors import UVSubprocessFailedError -from usethis._integrations.uv.used import is_uv_used def remove_pre_commit_config() -> None: diff --git a/src/usethis/_interface/docstyle.py b/src/usethis/_interface/docstyle.py index 739fdb6a..383265b6 100644 --- a/src/usethis/_interface/docstyle.py +++ b/src/usethis/_interface/docstyle.py @@ -2,8 +2,8 @@ import typer -from usethis._core.enums.docstyle import DocStyleEnum from usethis._options import quiet_opt +from usethis._types.docstyle import DocStyleEnum def docstyle( diff --git a/src/usethis/_interface/init.py b/src/usethis/_interface/init.py index 44d38814..4fdea8a6 100644 --- a/src/usethis/_interface/init.py +++ b/src/usethis/_interface/init.py @@ -2,10 +2,10 @@ import typer -from usethis._core.enums.ci import CIServiceEnum -from usethis._core.enums.docstyle import DocStyleEnum -from usethis._core.enums.status import DevelopmentStatusEnum from usethis._options import frozen_opt, offline_opt, quiet_opt +from usethis._types.ci import CIServiceEnum +from usethis._types.docstyle import DocStyleEnum +from usethis._types.status import DevelopmentStatusEnum def init( # noqa: PLR0913, PLR0915 @@ -64,7 +64,7 @@ def init( # noqa: PLR0913, PLR0915 from usethis._core.readme import add_readme from usethis._core.status import use_development_status from usethis._core.tool import use_pre_commit - from usethis._integrations.uv.init import opinionated_uv_init + from usethis._integrations.backend.uv.init import opinionated_uv_init from usethis._toolset.format_ import use_formatters from usethis._toolset.lint import use_linters from usethis._toolset.spellcheck import use_spellcheckers diff --git a/src/usethis/_interface/readme.py b/src/usethis/_interface/readme.py index 22d2c59b..dcf7a0f2 100644 --- a/src/usethis/_interface/readme.py +++ b/src/usethis/_interface/readme.py @@ -20,7 +20,7 @@ def readme( get_uv_badge, ) from usethis._core.readme import add_readme - from usethis._integrations.uv.used import is_uv_used + from usethis._integrations.backend.uv.used import is_uv_used from usethis._tool.impl.pre_commit import PreCommitTool from usethis._tool.impl.ruff import RuffTool from usethis.errors import UsethisError diff --git a/src/usethis/_interface/status.py b/src/usethis/_interface/status.py index 99a530d6..0ca002a0 100644 --- a/src/usethis/_interface/status.py +++ b/src/usethis/_interface/status.py @@ -2,8 +2,8 @@ import typer -from usethis._core.enums.status import DevelopmentStatusEnum from usethis._options import quiet_opt +from usethis._types.status import DevelopmentStatusEnum def status( diff --git a/src/usethis/_interface/tool.py b/src/usethis/_interface/tool.py index 6f5030dc..b999cf23 100644 --- a/src/usethis/_interface/tool.py +++ b/src/usethis/_interface/tool.py @@ -4,13 +4,23 @@ import typer -from usethis._options import frozen_opt, how_opt, offline_opt, quiet_opt, remove_opt +from usethis._options import ( + backend_opt, + frozen_opt, + how_opt, + offline_opt, + quiet_opt, + remove_opt, +) +from usethis._types.backend import BackendEnum if TYPE_CHECKING: from usethis._core.tool import UseToolFunc app = typer.Typer(help="Add and configure individual tools.", add_completion=False) +# ruff: noqa: PLR0913 since there are many options for these commands. + @app.command( name="codespell", @@ -23,15 +33,18 @@ def codespell( offline: bool = offline_opt, quiet: bool = quiet_opt, frozen: bool = frozen_opt, + backend: BackendEnum = backend_opt, ) -> None: from usethis._config import usethis_config from usethis._config_file import files_manager - from usethis._core.tool import ( - use_codespell, - ) + from usethis._core.tool import use_codespell + + assert isinstance(backend, BackendEnum) with ( - usethis_config.set(offline=offline, quiet=quiet, frozen=frozen), + usethis_config.set( + offline=offline, quiet=quiet, frozen=frozen, backend=backend + ), files_manager(), ): _run_tool(use_codespell, remove=remove, how=how) @@ -55,13 +68,18 @@ def coverage_py( offline: bool = offline_opt, quiet: bool = quiet_opt, frozen: bool = frozen_opt, + backend: BackendEnum = backend_opt, ) -> None: from usethis._config import usethis_config from usethis._config_file import files_manager from usethis._core.tool import use_coverage_py + assert isinstance(backend, BackendEnum) + with ( - usethis_config.set(offline=offline, quiet=quiet, frozen=frozen), + usethis_config.set( + offline=offline, quiet=quiet, frozen=frozen, backend=backend + ), files_manager(), ): _run_tool(use_coverage_py, remove=remove, how=how) @@ -78,13 +96,18 @@ def deptry( offline: bool = offline_opt, quiet: bool = quiet_opt, frozen: bool = frozen_opt, + backend: BackendEnum = backend_opt, ) -> None: from usethis._config import usethis_config from usethis._config_file import files_manager from usethis._core.tool import use_deptry + assert isinstance(backend, BackendEnum) + with ( - usethis_config.set(offline=offline, quiet=quiet, frozen=frozen), + usethis_config.set( + offline=offline, quiet=quiet, frozen=frozen, backend=backend + ), files_manager(), ): _run_tool(use_deptry, remove=remove, how=how) @@ -101,13 +124,18 @@ def import_linter( offline: bool = offline_opt, quiet: bool = quiet_opt, frozen: bool = frozen_opt, + backend: BackendEnum = backend_opt, ) -> None: from usethis._config import usethis_config from usethis._config_file import files_manager from usethis._core.tool import use_import_linter + assert isinstance(backend, BackendEnum) + with ( - usethis_config.set(offline=offline, quiet=quiet, frozen=frozen), + usethis_config.set( + offline=offline, quiet=quiet, frozen=frozen, backend=backend + ), files_manager(), ): _run_tool(use_import_linter, remove=remove, how=how) @@ -124,13 +152,18 @@ def pre_commit( offline: bool = offline_opt, quiet: bool = quiet_opt, frozen: bool = frozen_opt, + backend: BackendEnum = backend_opt, ) -> None: from usethis._config import usethis_config from usethis._config_file import files_manager from usethis._core.tool import use_pre_commit + assert isinstance(backend, BackendEnum) + with ( - usethis_config.set(offline=offline, quiet=quiet, frozen=frozen), + usethis_config.set( + offline=offline, quiet=quiet, frozen=frozen, backend=backend + ), files_manager(), ): _run_tool(use_pre_commit, remove=remove, how=how) @@ -147,13 +180,18 @@ def pyproject_fmt( offline: bool = offline_opt, quiet: bool = quiet_opt, frozen: bool = frozen_opt, + backend: BackendEnum = backend_opt, ) -> None: from usethis._config import usethis_config from usethis._config_file import files_manager from usethis._core.tool import use_pyproject_fmt + assert isinstance(backend, BackendEnum) + with ( - usethis_config.set(offline=offline, quiet=quiet, frozen=frozen), + usethis_config.set( + offline=offline, quiet=quiet, frozen=frozen, backend=backend + ), files_manager(), ): _run_tool(use_pyproject_fmt, remove=remove, how=how) @@ -170,13 +208,18 @@ def pyproject_toml( offline: bool = offline_opt, quiet: bool = quiet_opt, frozen: bool = frozen_opt, + backend: BackendEnum = backend_opt, ) -> None: from usethis._config import usethis_config from usethis._config_file import files_manager from usethis._core.tool import use_pyproject_toml + assert isinstance(backend, BackendEnum) + with ( - usethis_config.set(offline=offline, quiet=quiet, frozen=frozen), + usethis_config.set( + offline=offline, quiet=quiet, frozen=frozen, backend=backend + ), files_manager(), ): _run_tool(use_pyproject_toml, remove=remove, how=how) @@ -191,13 +234,18 @@ def pytest( offline: bool = offline_opt, quiet: bool = quiet_opt, frozen: bool = frozen_opt, + backend: BackendEnum = backend_opt, ) -> None: from usethis._config import usethis_config from usethis._config_file import files_manager from usethis._core.tool import use_pytest + assert isinstance(backend, BackendEnum) + with ( - usethis_config.set(offline=offline, quiet=quiet, frozen=frozen), + usethis_config.set( + offline=offline, quiet=quiet, frozen=frozen, backend=backend + ), files_manager(), ): _run_tool(use_pytest, remove=remove, how=how) @@ -214,13 +262,18 @@ def requirements_txt( offline: bool = offline_opt, quiet: bool = quiet_opt, frozen: bool = frozen_opt, + backend: BackendEnum = backend_opt, ) -> None: from usethis._config import usethis_config from usethis._config_file import files_manager from usethis._core.tool import use_requirements_txt + assert isinstance(backend, BackendEnum) + with ( - usethis_config.set(offline=offline, quiet=quiet, frozen=frozen), + usethis_config.set( + offline=offline, quiet=quiet, frozen=frozen, backend=backend + ), files_manager(), ): _run_tool(use_requirements_txt, remove=remove, how=how) @@ -231,12 +284,13 @@ def requirements_txt( help="Use Ruff: an extremely fast Python linter and code formatter.", rich_help_panel="Code Quality Tools", ) -def ruff( # noqa: PLR0913 +def ruff( remove: bool = remove_opt, how: bool = how_opt, offline: bool = offline_opt, quiet: bool = quiet_opt, frozen: bool = frozen_opt, + backend: BackendEnum = backend_opt, linter: bool = typer.Option( True, "--linter/--no-linter", @@ -252,8 +306,12 @@ def ruff( # noqa: PLR0913 from usethis._config_file import files_manager from usethis._core.tool import use_ruff + assert isinstance(backend, BackendEnum) + with ( - usethis_config.set(offline=offline, quiet=quiet, frozen=frozen), + usethis_config.set( + offline=offline, quiet=quiet, frozen=frozen, backend=backend + ), files_manager(), ): _run_tool(use_ruff, remove=remove, how=how, linter=linter, formatter=formatter) diff --git a/src/usethis/_options.py b/src/usethis/_options.py index 2630c13c..c5e1dd9c 100644 --- a/src/usethis/_options.py +++ b/src/usethis/_options.py @@ -1,6 +1,7 @@ import typer from usethis._config import ( + BACKEND_DEFAULT, FROZEN_DEFAULT, HOW_DEFAULT, OFFLINE_DEFAULT, @@ -23,3 +24,6 @@ "--frozen", help="Do not install dependencies, nor update lockfiles.", ) +backend_opt = typer.Option( + BACKEND_DEFAULT, "--backend", help="Package manager backend to use." +) diff --git a/src/usethis/_tool/base.py b/src/usethis/_tool/base.py index fd215400..4f15579a 100644 --- a/src/usethis/_tool/base.py +++ b/src/usethis/_tool/base.py @@ -7,6 +7,7 @@ from usethis._config import usethis_config from usethis._console import tick_print, warn_print +from usethis._deps import add_deps_to_group, is_dep_in_any_group, remove_deps_from_group from usethis._integrations.ci.bitbucket.steps import ( add_bitbucket_step_in_default, bitbucket_steps_are_equivalent, @@ -21,11 +22,6 @@ hook_ids_are_equivalent, remove_hook, ) -from usethis._integrations.uv.deps import ( - add_deps_to_group, - is_dep_in_any_group, - remove_deps_from_group, -) from usethis._tool.config import ConfigSpec, NoConfigValue from usethis._tool.pre_commit import PreCommitConfig from usethis._tool.rule import RuleConfig @@ -34,11 +30,11 @@ if TYPE_CHECKING: from pathlib import Path - from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep - from usethis._integrations.pre_commit.schema import LocalRepo, UriRepo - from usethis._integrations.uv.deps import ( + from usethis._integrations.backend.uv.deps import ( Dependency, ) + from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep + from usethis._integrations.pre_commit.schema import LocalRepo, UriRepo from usethis._io import KeyValueFileManager from usethis._tool.config import ResolutionT from usethis._tool.rule import Rule diff --git a/src/usethis/_tool/config.py b/src/usethis/_tool/config.py index d92eb017..7b55a462 100644 --- a/src/usethis/_tool/config.py +++ b/src/usethis/_tool/config.py @@ -7,8 +7,8 @@ from pydantic import BaseModel, InstanceOf from usethis._config import usethis_config +from usethis._integrations.backend.uv.init import ensure_pyproject_toml from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.uv.init import ensure_pyproject_toml from usethis._io import Key, KeyValueFileManager if TYPE_CHECKING: diff --git a/src/usethis/_tool/impl/codespell.py b/src/usethis/_tool/impl/codespell.py index 1ced5ae9..dccb62c0 100644 --- a/src/usethis/_tool/impl/codespell.py +++ b/src/usethis/_tool/impl/codespell.py @@ -6,6 +6,7 @@ from usethis._config_file import CodespellRCManager from usethis._console import box_print +from usethis._integrations.backend.uv.used import is_uv_used from usethis._integrations.ci.bitbucket.anchor import ( ScriptItemAnchor as BitbucketScriptItemAnchor, ) @@ -14,11 +15,10 @@ from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._integrations.file.setup_cfg.io_ import SetupCFGManager from usethis._integrations.pre_commit.schema import HookDefinition, UriRepo -from usethis._integrations.uv.deps import Dependency -from usethis._integrations.uv.used import is_uv_used from usethis._tool.base import Tool from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec from usethis._tool.pre_commit import PreCommitConfig +from usethis._types.deps import Dependency class CodespellTool(Tool): diff --git a/src/usethis/_tool/impl/coverage_py.py b/src/usethis/_tool/impl/coverage_py.py index 9aeea2e1..24554264 100644 --- a/src/usethis/_tool/impl/coverage_py.py +++ b/src/usethis/_tool/impl/coverage_py.py @@ -7,15 +7,13 @@ ToxINIManager, ) from usethis._console import box_print +from usethis._integrations.backend.uv.used import is_uv_used from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._integrations.file.setup_cfg.io_ import SetupCFGManager from usethis._integrations.project.layout import get_source_dir_str -from usethis._integrations.uv.deps import ( - Dependency, -) -from usethis._integrations.uv.used import is_uv_used from usethis._tool.base import Tool from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec +from usethis._types.deps import Dependency class CoveragePyTool(Tool): diff --git a/src/usethis/_tool/impl/deptry.py b/src/usethis/_tool/impl/deptry.py index cad76d22..2e81dcb7 100644 --- a/src/usethis/_tool/impl/deptry.py +++ b/src/usethis/_tool/impl/deptry.py @@ -6,6 +6,7 @@ from typing_extensions import assert_never from usethis._console import box_print, info_print, tick_print +from usethis._integrations.backend.uv.used import is_uv_used from usethis._integrations.ci.bitbucket.anchor import ( ScriptItemAnchor as BitbucketScriptItemAnchor, ) @@ -14,8 +15,6 @@ from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._integrations.pre_commit.schema import HookDefinition, Language, LocalRepo from usethis._integrations.project.layout import get_source_dir_str -from usethis._integrations.uv.deps import Dependency -from usethis._integrations.uv.used import is_uv_used from usethis._tool.base import Tool from usethis._tool.config import ( ConfigEntry, @@ -24,6 +23,7 @@ ensure_file_manager_exists, ) from usethis._tool.pre_commit import PreCommitConfig +from usethis._types.deps import Dependency if TYPE_CHECKING: from usethis._io import KeyValueFileManager diff --git a/src/usethis/_tool/impl/import_linter.py b/src/usethis/_tool/impl/import_linter.py index 226fcee8..4bb435ad 100644 --- a/src/usethis/_tool/impl/import_linter.py +++ b/src/usethis/_tool/impl/import_linter.py @@ -10,6 +10,7 @@ from usethis._config import usethis_config from usethis._config_file import DotImportLinterManager from usethis._console import box_print, info_print, warn_print +from usethis._integrations.backend.uv.used import is_uv_used from usethis._integrations.ci.bitbucket.anchor import ( ScriptItemAnchor as BitbucketScriptItemAnchor, ) @@ -26,13 +27,12 @@ ) from usethis._integrations.project.name import get_project_name from usethis._integrations.project.packages import get_importable_packages -from usethis._integrations.uv.deps import Dependency -from usethis._integrations.uv.used import is_uv_used from usethis._tool.base import Tool from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec, NoConfigValue from usethis._tool.impl.ruff import RuffTool from usethis._tool.pre_commit import PreCommitConfig from usethis._tool.rule import RuleConfig +from usethis._types.deps import Dependency if TYPE_CHECKING: from usethis._io import KeyValueFileManager diff --git a/src/usethis/_tool/impl/pre_commit.py b/src/usethis/_tool/impl/pre_commit.py index 7622d8f7..c9e71d41 100644 --- a/src/usethis/_tool/impl/pre_commit.py +++ b/src/usethis/_tool/impl/pre_commit.py @@ -4,16 +4,14 @@ from usethis._config import usethis_config from usethis._console import box_print +from usethis._integrations.backend.uv.used import is_uv_used from usethis._integrations.ci.bitbucket.anchor import ( ScriptItemAnchor as BitbucketScriptItemAnchor, ) from usethis._integrations.ci.bitbucket.schema import Script as BitbucketScript from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep -from usethis._integrations.uv.deps import ( - Dependency, -) -from usethis._integrations.uv.used import is_uv_used from usethis._tool.base import Tool +from usethis._types.deps import Dependency class PreCommitTool(Tool): diff --git a/src/usethis/_tool/impl/pyproject_fmt.py b/src/usethis/_tool/impl/pyproject_fmt.py index 705adc06..edf88180 100644 --- a/src/usethis/_tool/impl/pyproject_fmt.py +++ b/src/usethis/_tool/impl/pyproject_fmt.py @@ -5,6 +5,7 @@ from typing_extensions import assert_never from usethis._console import box_print +from usethis._integrations.backend.uv.used import is_uv_used from usethis._integrations.ci.bitbucket.anchor import ( ScriptItemAnchor as BitbucketScriptItemAnchor, ) @@ -12,11 +13,10 @@ from usethis._integrations.ci.bitbucket.schema import Step as BitbucketStep from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._integrations.pre_commit.schema import HookDefinition, UriRepo -from usethis._integrations.uv.deps import Dependency -from usethis._integrations.uv.used import is_uv_used from usethis._tool.base import Tool from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec from usethis._tool.pre_commit import PreCommitConfig +from usethis._types.deps import Dependency class PyprojectFmtTool(Tool): diff --git a/src/usethis/_tool/impl/pyproject_toml.py b/src/usethis/_tool/impl/pyproject_toml.py index 93b0f7e2..a9772fb3 100644 --- a/src/usethis/_tool/impl/pyproject_toml.py +++ b/src/usethis/_tool/impl/pyproject_toml.py @@ -17,7 +17,7 @@ from usethis._tool.impl.ruff import RuffTool if TYPE_CHECKING: - from usethis._integrations.uv.deps import ( + from usethis._integrations.backend.uv.deps import ( Dependency, ) diff --git a/src/usethis/_tool/impl/pytest.py b/src/usethis/_tool/impl/pytest.py index 288098bd..12ea1903 100644 --- a/src/usethis/_tool/impl/pytest.py +++ b/src/usethis/_tool/impl/pytest.py @@ -13,6 +13,8 @@ ToxINIManager, ) from usethis._console import box_print +from usethis._integrations.backend.uv.python import get_supported_major_python_versions +from usethis._integrations.backend.uv.used import is_uv_used from usethis._integrations.ci.bitbucket.anchor import ( ScriptItemAnchor as BitbucketScriptItemAnchor, ) @@ -25,14 +27,10 @@ from usethis._integrations.file.setup_cfg.io_ import SetupCFGManager from usethis._integrations.project.build import has_pyproject_toml_declared_build_system from usethis._integrations.project.layout import get_source_dir_str -from usethis._integrations.uv.deps import ( - Dependency, -) -from usethis._integrations.uv.python import get_supported_major_python_versions -from usethis._integrations.uv.used import is_uv_used from usethis._tool.base import Tool from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec from usethis._tool.rule import RuleConfig +from usethis._types.deps import Dependency if TYPE_CHECKING: from usethis._io import KeyValueFileManager diff --git a/src/usethis/_tool/impl/requirements_txt.py b/src/usethis/_tool/impl/requirements_txt.py index 556d0d19..5f1fa0d0 100644 --- a/src/usethis/_tool/impl/requirements_txt.py +++ b/src/usethis/_tool/impl/requirements_txt.py @@ -6,13 +6,13 @@ from typing_extensions import assert_never from usethis._console import box_print +from usethis._integrations.backend.uv.used import is_uv_used from usethis._integrations.pre_commit.schema import HookDefinition, Language, LocalRepo -from usethis._integrations.uv.used import is_uv_used from usethis._tool.base import Tool from usethis._tool.pre_commit import PreCommitConfig if TYPE_CHECKING: - from usethis._integrations.uv.deps import ( + from usethis._integrations.backend.uv.deps import ( Dependency, ) diff --git a/src/usethis/_tool/impl/ruff.py b/src/usethis/_tool/impl/ruff.py index f4d6efd0..cfa1fe57 100644 --- a/src/usethis/_tool/impl/ruff.py +++ b/src/usethis/_tool/impl/ruff.py @@ -7,6 +7,7 @@ from usethis._config_file import DotRuffTOMLManager, RuffTOMLManager from usethis._console import box_print, tick_print +from usethis._integrations.backend.uv.used import is_uv_used from usethis._integrations.ci.bitbucket.anchor import ( ScriptItemAnchor as BitbucketScriptItemAnchor, ) @@ -20,8 +21,6 @@ Language, LocalRepo, ) -from usethis._integrations.uv.deps import Dependency -from usethis._integrations.uv.used import is_uv_used from usethis._tool.base import Tool from usethis._tool.config import ( ConfigEntry, @@ -30,6 +29,7 @@ ensure_file_manager_exists, ) from usethis._tool.pre_commit import PreCommitConfig, PreCommitRepoConfig +from usethis._types.deps import Dependency if TYPE_CHECKING: from usethis._integrations.ci.bitbucket.schema import Pipe as BitbucketPipe diff --git a/src/usethis/_types/__init__.py b/src/usethis/_types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/usethis/_types/backend.py b/src/usethis/_types/backend.py new file mode 100644 index 00000000..889d365c --- /dev/null +++ b/src/usethis/_types/backend.py @@ -0,0 +1,13 @@ +from enum import Enum + + +class BackendEnum(Enum): + """Enumeration of available backends for usethis. + + "auto" doesn't represent an actual backend, but rather a mode where usethis + automatically selects the backend based on the current environment or configuration. + """ + + auto = "auto" + uv = "uv" + none = "none" diff --git a/src/usethis/_core/enums/ci.py b/src/usethis/_types/ci.py similarity index 100% rename from src/usethis/_core/enums/ci.py rename to src/usethis/_types/ci.py diff --git a/src/usethis/_types/deps.py b/src/usethis/_types/deps.py new file mode 100644 index 00000000..da990482 --- /dev/null +++ b/src/usethis/_types/deps.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel + + +class Dependency(BaseModel): + name: str + extras: frozenset[str] = frozenset() + + def __str__(self) -> str: + extras = sorted(self.extras or set()) + return self.name + "".join(f"[{extra}]" for extra in extras) + + def __hash__(self) -> int: + return hash((self.__class__.__name__, self.name, self.extras)) diff --git a/src/usethis/_core/enums/docstyle.py b/src/usethis/_types/docstyle.py similarity index 100% rename from src/usethis/_core/enums/docstyle.py rename to src/usethis/_types/docstyle.py diff --git a/src/usethis/_core/enums/status.py b/src/usethis/_types/status.py similarity index 100% rename from src/usethis/_core/enums/status.py rename to src/usethis/_types/status.py diff --git a/src/usethis/errors.py b/src/usethis/errors.py index dc091e12..50804c1d 100644 --- a/src/usethis/errors.py +++ b/src/usethis/errors.py @@ -31,3 +31,14 @@ def name(self) -> str: NotImplementedError if not overridden. """ raise NotImplementedError + + +class ForbiddenBackendError(UsethisError): + """Raised when an unexpected attempt to use a particular backend. + + For example, when the uv backend is not enabled but a uv subprocess is invoked. + """ + + +class DepGroupError(UsethisError): + """Raised when adding or removing a dependency from a group fails.""" diff --git a/tests/conftest.py b/tests/conftest.py index 43759a98..113d22fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import pytest from usethis._config import usethis_config -from usethis._integrations.uv.call import call_subprocess, call_uv_subprocess +from usethis._integrations.backend.uv.call import call_subprocess, call_uv_subprocess from usethis._test import change_cwd, is_offline diff --git a/tests/test_install.py b/tests/test_install.py index d494d0d1..37fb92ae 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -6,8 +6,8 @@ import pytest from usethis._config import usethis_config +from usethis._integrations.backend.uv.call import call_uv_subprocess from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.uv.call import call_uv_subprocess from usethis._subprocess import call_subprocess from usethis._test import change_cwd diff --git a/tests/usethis/_core/test_core_tool.py b/tests/usethis/_core/test_core_tool.py index 071cc244..b320b0fe 100644 --- a/tests/usethis/_core/test_core_tool.py +++ b/tests/usethis/_core/test_core_tool.py @@ -22,24 +22,17 @@ use_ruff, use_tool, ) +from usethis._deps import add_deps_to_group, get_deps_from_group, is_dep_satisfied_in +from usethis._integrations.backend.uv.call import call_uv_subprocess +from usethis._integrations.backend.uv.link_mode import ensure_symlink_mode +from usethis._integrations.backend.uv.toml import UVTOMLManager from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.pre_commit.hooks import ( - _HOOK_ORDER, - get_hook_ids, -) +from usethis._integrations.pre_commit.hooks import _HOOK_ORDER, get_hook_ids from usethis._integrations.python.version import get_python_version -from usethis._integrations.uv.call import call_uv_subprocess -from usethis._integrations.uv.deps import ( - Dependency, - add_deps_to_group, - get_deps_from_group, - is_dep_satisfied_in, -) -from usethis._integrations.uv.link_mode import ensure_symlink_mode -from usethis._integrations.uv.toml import UVTOMLManager from usethis._test import change_cwd from usethis._tool.all_ import ALL_TOOLS from usethis._tool.impl.ruff import RuffTool +from usethis._types.deps import Dependency class TestAllHooksList: diff --git a/tests/usethis/_core/test_docstyle.py b/tests/usethis/_core/test_docstyle.py index 4a22bda9..eabbb080 100644 --- a/tests/usethis/_core/test_docstyle.py +++ b/tests/usethis/_core/test_docstyle.py @@ -4,10 +4,10 @@ from usethis._config_file import files_manager from usethis._core.docstyle import use_docstyle -from usethis._core.enums.docstyle import DocStyleEnum from usethis._core.tool import use_ruff -from usethis._integrations.uv.init import ensure_pyproject_toml +from usethis._integrations.backend.uv.init import ensure_pyproject_toml from usethis._test import change_cwd +from usethis._types.docstyle import DocStyleEnum class TestUseDocstyle: diff --git a/tests/usethis/_core/test_rule.py b/tests/usethis/_core/test_rule.py index 1b26ea8b..4fbe30db 100644 --- a/tests/usethis/_core/test_rule.py +++ b/tests/usethis/_core/test_rule.py @@ -9,10 +9,11 @@ select_rules, unignore_rules, ) -from usethis._integrations.uv.deps import Dependency, get_deps_from_group +from usethis._deps import get_deps_from_group from usethis._test import change_cwd from usethis._tool.impl.deptry import DeptryTool from usethis._tool.impl.ruff import RuffTool +from usethis._types.deps import Dependency class TestSelectRules: diff --git a/tests/usethis/_core/test_status.py b/tests/usethis/_core/test_status.py index 4062adb6..76adb9fa 100644 --- a/tests/usethis/_core/test_status.py +++ b/tests/usethis/_core/test_status.py @@ -2,10 +2,10 @@ import pytest -from usethis._core.enums.status import DevelopmentStatusEnum from usethis._core.status import use_development_status from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._test import change_cwd +from usethis._types.status import DevelopmentStatusEnum class TestUseDevelopmentStatus: diff --git a/tests/usethis/_integrations/pre_commit/test_pre_commit_core.py b/tests/usethis/_integrations/pre_commit/test_pre_commit_core.py index 112c06da..a437f55b 100644 --- a/tests/usethis/_integrations/pre_commit/test_pre_commit_core.py +++ b/tests/usethis/_integrations/pre_commit/test_pre_commit_core.py @@ -2,6 +2,7 @@ import pytest +from usethis._deps import add_deps_to_group from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._integrations.pre_commit.core import ( install_pre_commit_hooks, @@ -10,8 +11,8 @@ ) from usethis._integrations.pre_commit.errors import PreCommitInstallationError from usethis._integrations.pre_commit.hooks import add_placeholder_hook -from usethis._integrations.uv.deps import Dependency, add_deps_to_group from usethis._test import change_cwd +from usethis._types.deps import Dependency class TestRemovePreCommitConfig: diff --git a/tests/usethis/_integrations/sonarqube/test_sonarqube_config.py b/tests/usethis/_integrations/sonarqube/test_sonarqube_config.py index 1027e31a..0cfe21ae 100644 --- a/tests/usethis/_integrations/sonarqube/test_sonarqube_config.py +++ b/tests/usethis/_integrations/sonarqube/test_sonarqube_config.py @@ -2,6 +2,8 @@ import pytest +from usethis._integrations.backend.uv.init import ensure_pyproject_toml +from usethis._integrations.backend.uv.python import python_pin from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._integrations.python.version import get_python_version from usethis._integrations.sonarqube.config import ( @@ -13,8 +15,6 @@ InvalidSonarQubeProjectKeyError, MissingProjectKeyError, ) -from usethis._integrations.uv.init import ensure_pyproject_toml -from usethis._integrations.uv.python import python_pin from usethis._test import change_cwd diff --git a/tests/usethis/_integrations/uv/test_call.py b/tests/usethis/_integrations/uv/test_call.py index 6de6093d..95a28579 100644 --- a/tests/usethis/_integrations/uv/test_call.py +++ b/tests/usethis/_integrations/uv/test_call.py @@ -2,11 +2,11 @@ import pytest -import usethis._integrations.uv.call +import usethis._integrations.backend.uv.call from usethis._config import usethis_config +from usethis._integrations.backend.uv.call import call_uv_subprocess +from usethis._integrations.backend.uv.errors import UVSubprocessFailedError from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.uv.call import call_uv_subprocess -from usethis._integrations.uv.errors import UVSubprocessFailedError from usethis._test import change_cwd @@ -35,7 +35,9 @@ def mock_call_subprocess(args: list[str], *, cwd: Path | None = None) -> str: return " ".join(args) monkeypatch.setattr( - usethis._integrations.uv.call, "call_subprocess", mock_call_subprocess + usethis._integrations.backend.uv.call, + "call_subprocess", + mock_call_subprocess, ) with usethis_config.set(frozen=True, offline=False): diff --git a/tests/usethis/_integrations/uv/test_deps.py b/tests/usethis/_integrations/uv/test_deps.py index bb2220f2..62a8f3f1 100644 --- a/tests/usethis/_integrations/uv/test_deps.py +++ b/tests/usethis/_integrations/uv/test_deps.py @@ -4,13 +4,11 @@ import usethis import usethis._integrations -import usethis._integrations.uv -import usethis._integrations.uv.deps +import usethis._integrations.backend.uv +import usethis._integrations.backend.uv.deps from usethis._config import usethis_config from usethis._config_file import files_manager -from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.uv.deps import ( - Dependency, +from usethis._deps import ( add_default_groups, add_deps_to_group, get_default_groups, @@ -21,9 +19,14 @@ register_default_group, remove_deps_from_group, ) -from usethis._integrations.uv.errors import UVDepGroupError, UVSubprocessFailedError -from usethis._integrations.uv.toml import UVTOMLManager +from usethis._integrations.backend.uv.errors import ( + UVDepGroupError, + UVSubprocessFailedError, +) +from usethis._integrations.backend.uv.toml import UVTOMLManager +from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._test import change_cwd +from usethis._types.deps import Dependency class TestGetDepGroups: @@ -305,7 +308,9 @@ def mock_call_uv_subprocess(*_, **__): raise UVSubprocessFailedError monkeypatch.setattr( - usethis._integrations.uv.deps, "call_uv_subprocess", mock_call_uv_subprocess + usethis._integrations.backend.uv.deps, + "call_uv_subprocess", + mock_call_uv_subprocess, ) # Act, Assert @@ -458,7 +463,7 @@ def mock_call_uv_subprocess(*_, **__): raise UVSubprocessFailedError monkeypatch.setattr( - usethis._integrations.uv.deps, + usethis._integrations.backend.uv.deps, "call_uv_subprocess", mock_call_uv_subprocess, ) diff --git a/tests/usethis/_integrations/uv/test_init.py b/tests/usethis/_integrations/uv/test_init.py index 7928937f..43e97b6b 100644 --- a/tests/usethis/_integrations/uv/test_init.py +++ b/tests/usethis/_integrations/uv/test_init.py @@ -3,11 +3,14 @@ import pytest -import usethis._integrations.uv.call +import usethis._integrations.backend.uv.call +from usethis._integrations.backend.uv.errors import UVInitError, UVSubprocessFailedError +from usethis._integrations.backend.uv.init import ( + ensure_pyproject_toml, + opinionated_uv_init, +) from usethis._integrations.file.pyproject_toml.errors import PyprojectTOMLInitError from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.uv.errors import UVInitError, UVSubprocessFailedError -from usethis._integrations.uv.init import ensure_pyproject_toml, opinionated_uv_init from usethis._test import change_cwd @@ -34,7 +37,7 @@ def mock_call_uv_subprocess(*_: Any, **__: Any) -> None: raise UVSubprocessFailedError monkeypatch.setattr( - usethis._integrations.uv.call, + usethis._integrations.backend.uv.call, "call_uv_subprocess", mock_call_uv_subprocess, ) @@ -146,7 +149,7 @@ def mock_call_uv_subprocess(*_: Any, **__: Any) -> None: raise UVSubprocessFailedError monkeypatch.setattr( - usethis._integrations.uv.call, + usethis._integrations.backend.uv.call, "call_uv_subprocess", mock_call_uv_subprocess, ) diff --git a/tests/usethis/_integrations/uv/test_link_mode.py b/tests/usethis/_integrations/uv/test_link_mode.py index 67043d83..83a8d7fa 100644 --- a/tests/usethis/_integrations/uv/test_link_mode.py +++ b/tests/usethis/_integrations/uv/test_link_mode.py @@ -1,8 +1,8 @@ from pathlib import Path +from usethis._integrations.backend.uv.link_mode import ensure_symlink_mode +from usethis._integrations.backend.uv.toml import UVTOMLManager from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.uv.link_mode import ensure_symlink_mode -from usethis._integrations.uv.toml import UVTOMLManager from usethis._test import change_cwd diff --git a/tests/usethis/_integrations/uv/test_python.py b/tests/usethis/_integrations/uv/test_python.py index 4a90e497..013e4727 100644 --- a/tests/usethis/_integrations/uv/test_python.py +++ b/tests/usethis/_integrations/uv/test_python.py @@ -2,17 +2,17 @@ import pytest +from usethis._integrations.backend.uv.python import ( + _parse_python_version_from_uv_output, + get_available_python_versions, + get_supported_major_python_versions, +) from usethis._integrations.file.pyproject_toml.errors import PyprojectTOMLNotFoundError from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._integrations.python.version import ( extract_major_version, get_python_version, ) -from usethis._integrations.uv.python import ( - _parse_python_version_from_uv_output, - get_available_python_versions, - get_supported_major_python_versions, -) from usethis._test import change_cwd diff --git a/tests/usethis/_integrations/uv/test_used.py b/tests/usethis/_integrations/uv/test_used.py index bb7eddeb..c88d460e 100644 --- a/tests/usethis/_integrations/uv/test_used.py +++ b/tests/usethis/_integrations/uv/test_used.py @@ -1,7 +1,7 @@ from pathlib import Path from usethis._config_file import files_manager -from usethis._integrations.uv.used import is_uv_used +from usethis._integrations.backend.uv.used import is_uv_used from usethis._test import change_cwd diff --git a/tests/usethis/_interface/test_docstyle.py b/tests/usethis/_interface/test_docstyle.py index 6fec0ac9..dc7f76a3 100644 --- a/tests/usethis/_interface/test_docstyle.py +++ b/tests/usethis/_interface/test_docstyle.py @@ -3,9 +3,9 @@ from typer.testing import CliRunner from usethis._app import app -from usethis._core.enums.docstyle import DocStyleEnum from usethis._interface.docstyle import docstyle from usethis._test import change_cwd +from usethis._types.docstyle import DocStyleEnum class TestDocstyle: diff --git a/tests/usethis/_interface/test_format_.py b/tests/usethis/_interface/test_format_.py index 014a34ea..86386940 100644 --- a/tests/usethis/_interface/test_format_.py +++ b/tests/usethis/_interface/test_format_.py @@ -4,8 +4,9 @@ from usethis._app import app from usethis._config_file import files_manager -from usethis._integrations.uv.deps import Dependency, get_deps_from_group +from usethis._deps import get_deps_from_group from usethis._test import change_cwd +from usethis._types.deps import Dependency class TestFormat: diff --git a/tests/usethis/_interface/test_lint.py b/tests/usethis/_interface/test_lint.py index 443c5914..0007d239 100644 --- a/tests/usethis/_interface/test_lint.py +++ b/tests/usethis/_interface/test_lint.py @@ -4,8 +4,9 @@ from usethis._app import app from usethis._config_file import files_manager -from usethis._integrations.uv.deps import Dependency, get_deps_from_group +from usethis._deps import get_deps_from_group from usethis._test import change_cwd +from usethis._types.deps import Dependency class TestLint: diff --git a/tests/usethis/_interface/test_spellcheck.py b/tests/usethis/_interface/test_spellcheck.py index a17a4566..bbae9c25 100644 --- a/tests/usethis/_interface/test_spellcheck.py +++ b/tests/usethis/_interface/test_spellcheck.py @@ -4,8 +4,9 @@ from usethis._app import app from usethis._config_file import files_manager -from usethis._integrations.uv.deps import Dependency, get_deps_from_group +from usethis._deps import get_deps_from_group from usethis._test import change_cwd +from usethis._types.deps import Dependency class TestSpellcheck: diff --git a/tests/usethis/_interface/test_test.py b/tests/usethis/_interface/test_test.py index 450b7d4f..9edd3c5a 100644 --- a/tests/usethis/_interface/test_test.py +++ b/tests/usethis/_interface/test_test.py @@ -4,8 +4,9 @@ from usethis._app import app from usethis._config_file import files_manager -from usethis._integrations.uv.deps import Dependency, get_deps_from_group +from usethis._deps import get_deps_from_group from usethis._test import change_cwd +from usethis._types.deps import Dependency class TestSpellcheck: diff --git a/tests/usethis/_interface/test_tool.py b/tests/usethis/_interface/test_tool.py index 9e22fcce..09229134 100644 --- a/tests/usethis/_interface/test_tool.py +++ b/tests/usethis/_interface/test_tool.py @@ -5,8 +5,8 @@ from usethis._config import usethis_config from usethis._config_file import files_manager +from usethis._integrations.backend.uv.call import call_uv_subprocess from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._integrations.uv.call import call_uv_subprocess from usethis._interface.tool import ALL_TOOL_COMMANDS, app from usethis._subprocess import SubprocessFailedError, call_subprocess from usethis._test import change_cwd diff --git a/tests/usethis/_tool/test_base.py b/tests/usethis/_tool/test_base.py index 8a7f9a45..09343283 100644 --- a/tests/usethis/_tool/test_base.py +++ b/tests/usethis/_tool/test_base.py @@ -5,17 +5,18 @@ from usethis._config_file import files_manager from usethis._console import box_print +from usethis._deps import add_deps_to_group from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._integrations.file.setup_cfg.io_ import SetupCFGManager from usethis._integrations.pre_commit.hooks import _PLACEHOLDER_ID, get_hook_ids from usethis._integrations.pre_commit.schema import HookDefinition, UriRepo -from usethis._integrations.uv.deps import Dependency, add_deps_to_group from usethis._io import KeyValueFileManager from usethis._test import change_cwd from usethis._tool.base import Tool from usethis._tool.config import ConfigEntry, ConfigItem, ConfigSpec from usethis._tool.pre_commit import PreCommitConfig, PreCommitRepoConfig from usethis._tool.rule import RuleConfig +from usethis._types.deps import Dependency class DefaultTool(Tool): diff --git a/tests/usethis/test_config.py b/tests/usethis/test_config.py index 78c6c6de..6b73db97 100644 --- a/tests/usethis/test_config.py +++ b/tests/usethis/test_config.py @@ -3,9 +3,10 @@ import pytest from usethis._config import UsethisConfig, usethis_config -from usethis._integrations.uv.call import call_uv_subprocess -from usethis._integrations.uv.errors import UVSubprocessFailedError +from usethis._integrations.backend.uv.call import call_uv_subprocess +from usethis._integrations.backend.uv.errors import UVSubprocessFailedError from usethis._test import change_cwd +from usethis._types.backend import BackendEnum class TestUsethisConfig: @@ -40,7 +41,7 @@ def test_raises_error_when_disabled(self): # Act & Assert with ( change_cwd(Path.cwd()), - usethis_config.set(disable_uv_subprocess=True), + usethis_config.set(backend=BackendEnum.none), pytest.raises(UVSubprocessFailedError), ): call_uv_subprocess(["python", "list"], change_toml=False) @@ -49,7 +50,7 @@ def test_does_not_raise_error_when_enabled(self): # Act & Assert with ( change_cwd(Path.cwd()), - usethis_config.set(disable_uv_subprocess=False), + usethis_config.set(backend=BackendEnum.uv), ): output = call_uv_subprocess(["python", "list"], change_toml=False) From d11eb0c3a6096f8751c8320eb6c8efd5f652148e Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Mon, 30 Jun 2025 17:17:19 +1200 Subject: [PATCH 02/55] Tweak backend selection logic to check whether uv is available. Fix add_* function when needing to remove Tweak test config --- src/usethis/_backend.py | 4 +++- src/usethis/_deps.py | 3 ++- src/usethis/_integrations/backend/uv/available.py | 11 +++++++++++ src/usethis/_integrations/backend/uv/init.py | 6 +++--- .../ci/bitbucket/test_bitbucket_schema.py | 4 ++-- tests/usethis/_integrations/uv/test_deps.py | 3 ++- tests/usethis/test_config.py | 4 ++-- 7 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 src/usethis/_integrations/backend/uv/available.py diff --git a/src/usethis/_backend.py b/src/usethis/_backend.py index c9966d08..e38e91d1 100644 --- a/src/usethis/_backend.py +++ b/src/usethis/_backend.py @@ -3,6 +3,7 @@ from typing import Literal from usethis._config import usethis_config +from usethis._integrations.backend.uv.available import is_uv_available from usethis._integrations.backend.uv.used import is_uv_used from usethis._types.backend import BackendEnum @@ -11,6 +12,7 @@ def get_backend() -> Literal[BackendEnum.uv, BackendEnum.none]: if usethis_config.backend is not BackendEnum.auto: return usethis_config.backend - if is_uv_used(): + if is_uv_used() or is_uv_available(): return BackendEnum.uv + return BackendEnum.none diff --git a/src/usethis/_deps.py b/src/usethis/_deps.py index 34ef85f4..d3b555fe 100644 --- a/src/usethis/_deps.py +++ b/src/usethis/_deps.py @@ -12,6 +12,7 @@ from usethis._integrations.backend.uv.deps import ( add_dep_to_group_via_uv, get_default_groups_via_uv, + remove_dep_from_group_via_uv, ) from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._types.backend import BackendEnum @@ -142,7 +143,7 @@ def remove_deps_from_group(deps: list[Dependency], group: str) -> None: f"Removing dependenc{ies} {deps_str} from the '{group}' group in 'pyproject.toml'." ) for dep in _deps: - add_dep_to_group_via_uv(dep, group) + remove_dep_from_group_via_uv(dep, group) elif backend is BackendEnum.none: box_print(f"Remove the {group} dependenc{ies} {deps_str}.") else: diff --git a/src/usethis/_integrations/backend/uv/available.py b/src/usethis/_integrations/backend/uv/available.py new file mode 100644 index 00000000..559ea41a --- /dev/null +++ b/src/usethis/_integrations/backend/uv/available.py @@ -0,0 +1,11 @@ +from usethis._integrations.backend.uv.call import call_uv_subprocess +from usethis._integrations.backend.uv.errors import UVSubprocessFailedError + + +def is_uv_available() -> bool: + """Check if the `uv` command is available in the current environment.""" + try: + call_uv_subprocess(["--version"], change_toml=True) + return True + except UVSubprocessFailedError: + return False diff --git a/src/usethis/_integrations/backend/uv/init.py b/src/usethis/_integrations/backend/uv/init.py index c537d9e7..bcdcc118 100644 --- a/src/usethis/_integrations/backend/uv/init.py +++ b/src/usethis/_integrations/backend/uv/init.py @@ -2,7 +2,7 @@ from usethis._config import usethis_config from usethis._console import tick_print -from usethis._integrations.backend.uv import call +from usethis._integrations.backend.uv.call import call_uv_subprocess from usethis._integrations.backend.uv.errors import UVInitError, UVSubprocessFailedError from usethis._integrations.file.pyproject_toml.errors import PyprojectTOMLInitError from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager @@ -18,7 +18,7 @@ def opinionated_uv_init() -> None: tick_print("Writing 'pyproject.toml' and initializing project.") try: - call.call_uv_subprocess( + call_uv_subprocess( ["init", "--lib", usethis_config.cpd().as_posix()], change_toml=True, ) @@ -36,7 +36,7 @@ def ensure_pyproject_toml(*, author: bool = True) -> None: try: author_from = "auto" if author else "none" - call.call_uv_subprocess( + call_uv_subprocess( [ "init", "--bare", diff --git a/tests/usethis/_integrations/ci/bitbucket/test_bitbucket_schema.py b/tests/usethis/_integrations/ci/bitbucket/test_bitbucket_schema.py index c6dde9fb..abd4eda9 100644 --- a/tests/usethis/_integrations/ci/bitbucket/test_bitbucket_schema.py +++ b/tests/usethis/_integrations/ci/bitbucket/test_bitbucket_schema.py @@ -38,6 +38,6 @@ def test_matches_schema_store(self): # TIP: go into debug mode to copy-and-paste into updated schema.json assert local_schema_json == online_schema_json - def test_target_python_version(self): + def test_target_python_version(self, usethis_dev_dir: Path): # If this test fails, we should bump the version in the command in schema.py - assert Path(".python-version").read_text().startswith("3.10") + assert (usethis_dev_dir / ".python-version").read_text().startswith("3.10") diff --git a/tests/usethis/_integrations/uv/test_deps.py b/tests/usethis/_integrations/uv/test_deps.py index 62a8f3f1..68e3a1eb 100644 --- a/tests/usethis/_integrations/uv/test_deps.py +++ b/tests/usethis/_integrations/uv/test_deps.py @@ -27,6 +27,7 @@ from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._test import change_cwd from usethis._types.deps import Dependency +from usethis.errors import DepGroupError class TestGetDepGroups: @@ -116,7 +117,7 @@ def test_invalid_dtype(self, tmp_path: Path): with ( change_cwd(tmp_path), PyprojectTOMLManager(), - pytest.raises(UVDepGroupError), + pytest.raises(DepGroupError), ): get_dep_groups() diff --git a/tests/usethis/test_config.py b/tests/usethis/test_config.py index 6b73db97..5df320a1 100644 --- a/tests/usethis/test_config.py +++ b/tests/usethis/test_config.py @@ -4,9 +4,9 @@ from usethis._config import UsethisConfig, usethis_config from usethis._integrations.backend.uv.call import call_uv_subprocess -from usethis._integrations.backend.uv.errors import UVSubprocessFailedError from usethis._test import change_cwd from usethis._types.backend import BackendEnum +from usethis.errors import ForbiddenBackendError class TestUsethisConfig: @@ -42,7 +42,7 @@ def test_raises_error_when_disabled(self): with ( change_cwd(Path.cwd()), usethis_config.set(backend=BackendEnum.none), - pytest.raises(UVSubprocessFailedError), + pytest.raises(ForbiddenBackendError), ): call_uv_subprocess(["python", "list"], change_toml=False) From 75f78a1bcd7c74e807c4c9ad75ec06a93695ab78 Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Mon, 30 Jun 2025 17:38:22 +1200 Subject: [PATCH 03/55] Move tests to reflect new package structure --- tests/usethis/_integrations/{ => backend}/uv/test_call.py | 0 tests/usethis/_integrations/{ => backend}/uv/test_deps.py | 0 tests/usethis/_integrations/{ => backend}/uv/test_init.py | 0 tests/usethis/_integrations/{ => backend}/uv/test_link_mode.py | 0 tests/usethis/_integrations/{ => backend}/uv/test_python.py | 0 tests/usethis/_integrations/{ => backend}/uv/test_used.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename tests/usethis/_integrations/{ => backend}/uv/test_call.py (100%) rename tests/usethis/_integrations/{ => backend}/uv/test_deps.py (100%) rename tests/usethis/_integrations/{ => backend}/uv/test_init.py (100%) rename tests/usethis/_integrations/{ => backend}/uv/test_link_mode.py (100%) rename tests/usethis/_integrations/{ => backend}/uv/test_python.py (100%) rename tests/usethis/_integrations/{ => backend}/uv/test_used.py (100%) diff --git a/tests/usethis/_integrations/uv/test_call.py b/tests/usethis/_integrations/backend/uv/test_call.py similarity index 100% rename from tests/usethis/_integrations/uv/test_call.py rename to tests/usethis/_integrations/backend/uv/test_call.py diff --git a/tests/usethis/_integrations/uv/test_deps.py b/tests/usethis/_integrations/backend/uv/test_deps.py similarity index 100% rename from tests/usethis/_integrations/uv/test_deps.py rename to tests/usethis/_integrations/backend/uv/test_deps.py diff --git a/tests/usethis/_integrations/uv/test_init.py b/tests/usethis/_integrations/backend/uv/test_init.py similarity index 100% rename from tests/usethis/_integrations/uv/test_init.py rename to tests/usethis/_integrations/backend/uv/test_init.py diff --git a/tests/usethis/_integrations/uv/test_link_mode.py b/tests/usethis/_integrations/backend/uv/test_link_mode.py similarity index 100% rename from tests/usethis/_integrations/uv/test_link_mode.py rename to tests/usethis/_integrations/backend/uv/test_link_mode.py diff --git a/tests/usethis/_integrations/uv/test_python.py b/tests/usethis/_integrations/backend/uv/test_python.py similarity index 100% rename from tests/usethis/_integrations/uv/test_python.py rename to tests/usethis/_integrations/backend/uv/test_python.py diff --git a/tests/usethis/_integrations/uv/test_used.py b/tests/usethis/_integrations/backend/uv/test_used.py similarity index 100% rename from tests/usethis/_integrations/uv/test_used.py rename to tests/usethis/_integrations/backend/uv/test_used.py From 80ceaf22b9ef9b585419cfc2e756a3366b6f8d29 Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Mon, 30 Jun 2025 17:40:15 +1200 Subject: [PATCH 04/55] Remove duplicated `Test` in `TestOpinionatedUVInit` name --- tests/usethis/_integrations/backend/uv/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/usethis/_integrations/backend/uv/test_init.py b/tests/usethis/_integrations/backend/uv/test_init.py index 43e97b6b..d7a7f133 100644 --- a/tests/usethis/_integrations/backend/uv/test_init.py +++ b/tests/usethis/_integrations/backend/uv/test_init.py @@ -14,7 +14,7 @@ from usethis._test import change_cwd -class TestTestOpinionatedUVInit: +class TestOpinionatedUVInit: def test_empty_dir(self, tmp_path: Path): # Act with change_cwd(tmp_path): From 3f853e5624d0bb09572d165fe0f29d571b4b9b9a Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Mon, 30 Jun 2025 17:44:54 +1200 Subject: [PATCH 05/55] Revert to importing module to pass mocked tests --- src/usethis/_integrations/backend/uv/init.py | 8 +++++--- tests/usethis/_integrations/backend/uv/test_init.py | 6 +----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/usethis/_integrations/backend/uv/init.py b/src/usethis/_integrations/backend/uv/init.py index bcdcc118..470c073c 100644 --- a/src/usethis/_integrations/backend/uv/init.py +++ b/src/usethis/_integrations/backend/uv/init.py @@ -2,7 +2,9 @@ from usethis._config import usethis_config from usethis._console import tick_print -from usethis._integrations.backend.uv.call import call_uv_subprocess +from usethis._integrations.backend.uv import ( # Use this style to allow test mocking + call, +) from usethis._integrations.backend.uv.errors import UVInitError, UVSubprocessFailedError from usethis._integrations.file.pyproject_toml.errors import PyprojectTOMLInitError from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager @@ -18,7 +20,7 @@ def opinionated_uv_init() -> None: tick_print("Writing 'pyproject.toml' and initializing project.") try: - call_uv_subprocess( + call.call_uv_subprocess( ["init", "--lib", usethis_config.cpd().as_posix()], change_toml=True, ) @@ -36,7 +38,7 @@ def ensure_pyproject_toml(*, author: bool = True) -> None: try: author_from = "auto" if author else "none" - call_uv_subprocess( + call.call_uv_subprocess( [ "init", "--bare", diff --git a/tests/usethis/_integrations/backend/uv/test_init.py b/tests/usethis/_integrations/backend/uv/test_init.py index d7a7f133..e6310a6f 100644 --- a/tests/usethis/_integrations/backend/uv/test_init.py +++ b/tests/usethis/_integrations/backend/uv/test_init.py @@ -43,11 +43,7 @@ def mock_call_uv_subprocess(*_: Any, **__: Any) -> None: ) # Act - with ( - change_cwd(tmp_path), - PyprojectTOMLManager(), - pytest.raises(UVInitError), - ): + with change_cwd(tmp_path), PyprojectTOMLManager(), pytest.raises(UVInitError): opinionated_uv_init() From b6a10adbcd46d59acf7daea78f7d04d803e004fe Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Mon, 30 Jun 2025 18:29:51 +1200 Subject: [PATCH 06/55] Add tests for `is_uv_available` and for `get_backend` --- .../_integrations/backend/uv/available.py | 5 +- .../backend/uv/test_available.py | 29 ++++++++ tests/usethis/test_backend.py | 67 +++++++++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 tests/usethis/_integrations/backend/uv/test_available.py create mode 100644 tests/usethis/test_backend.py diff --git a/src/usethis/_integrations/backend/uv/available.py b/src/usethis/_integrations/backend/uv/available.py index 559ea41a..b537df13 100644 --- a/src/usethis/_integrations/backend/uv/available.py +++ b/src/usethis/_integrations/backend/uv/available.py @@ -5,7 +5,8 @@ def is_uv_available() -> bool: """Check if the `uv` command is available in the current environment.""" try: - call_uv_subprocess(["--version"], change_toml=True) - return True + call_uv_subprocess(["--version"], change_toml=False) except UVSubprocessFailedError: return False + + return True diff --git a/tests/usethis/_integrations/backend/uv/test_available.py b/tests/usethis/_integrations/backend/uv/test_available.py new file mode 100644 index 00000000..781db3be --- /dev/null +++ b/tests/usethis/_integrations/backend/uv/test_available.py @@ -0,0 +1,29 @@ +import pytest + +import usethis._integrations.backend.uv.available +from usethis._integrations.backend.uv.available import is_uv_available +from usethis._integrations.backend.uv.errors import UVSubprocessFailedError + + +class TestIsUVAvailable: + def test_available_when_running_test_suite(self): + # Having uv is a pre-requisite for running the test suite + assert is_uv_available() + + def test_mock_not_available(self, monkeypatch: pytest.MonkeyPatch): + # Arrange + + def mock_call_uv_subprocess(*_, **__): + raise UVSubprocessFailedError + + monkeypatch.setattr( + usethis._integrations.backend.uv.available, + "call_uv_subprocess", + mock_call_uv_subprocess, + ) + + # Act + result = is_uv_available() + + # Assert + assert not result, "Expected uv to be unavailable, but it was available." diff --git a/tests/usethis/test_backend.py b/tests/usethis/test_backend.py new file mode 100644 index 00000000..aba22fec --- /dev/null +++ b/tests/usethis/test_backend.py @@ -0,0 +1,67 @@ +from pathlib import Path + +import pytest + +import usethis._integrations.backend.uv.available +from usethis._backend import get_backend +from usethis._config import usethis_config +from usethis._config_file import files_manager +from usethis._integrations.backend.uv.errors import UVSubprocessFailedError +from usethis._test import change_cwd +from usethis._types.backend import BackendEnum + + +class TestGetBackend: + def test_non_auto_remains(self): + for backend in BackendEnum: + if backend is not BackendEnum.auto: + with usethis_config.set(backend=backend): + assert get_backend() == backend + + def test_uv_used(self, tmp_path: Path): + # Arrange + (tmp_path / "uv.lock").touch() + + # Act + with change_cwd(tmp_path), usethis_config.set(backend=BackendEnum.auto): + result = get_backend() + + # Assert + assert result == BackendEnum.uv + + def test_uv_not_used_but_available(self, tmp_path: Path): + # Act + with ( + change_cwd(tmp_path), + usethis_config.set(backend=BackendEnum.auto), + files_manager(), + ): + result = get_backend() + + # Assert + assert result == BackendEnum.uv + + def test_uv_not_used_and_not_available( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + # Arrange + + def mock_call_uv_subprocess(*_, **__): + raise UVSubprocessFailedError + + monkeypatch.setattr( + usethis._integrations.backend.uv.available, + "call_uv_subprocess", + mock_call_uv_subprocess, + ) + + # Act + with ( + change_cwd(tmp_path), + usethis_config.set(backend=BackendEnum.auto), + files_manager(), + ): + result = get_backend() + + # Assert + assert result == BackendEnum.none From 01a6d155633b0b313a4001f8591845f23046298b Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Mon, 30 Jun 2025 18:43:35 +1200 Subject: [PATCH 07/55] Move tests module to reflect package restructure --- .../_integrations/backend/uv/test_deps.py | 746 ------------------ tests/usethis/test_deps.py | 740 +++++++++++++++++ 2 files changed, 740 insertions(+), 746 deletions(-) create mode 100644 tests/usethis/test_deps.py diff --git a/tests/usethis/_integrations/backend/uv/test_deps.py b/tests/usethis/_integrations/backend/uv/test_deps.py index 68e3a1eb..e69de29b 100644 --- a/tests/usethis/_integrations/backend/uv/test_deps.py +++ b/tests/usethis/_integrations/backend/uv/test_deps.py @@ -1,746 +0,0 @@ -from pathlib import Path - -import pytest - -import usethis -import usethis._integrations -import usethis._integrations.backend.uv -import usethis._integrations.backend.uv.deps -from usethis._config import usethis_config -from usethis._config_file import files_manager -from usethis._deps import ( - add_default_groups, - add_deps_to_group, - get_default_groups, - get_dep_groups, - get_deps_from_group, - is_dep_in_any_group, - is_dep_satisfied_in, - register_default_group, - remove_deps_from_group, -) -from usethis._integrations.backend.uv.errors import ( - UVDepGroupError, - UVSubprocessFailedError, -) -from usethis._integrations.backend.uv.toml import UVTOMLManager -from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager -from usethis._test import change_cwd -from usethis._types.deps import Dependency -from usethis.errors import DepGroupError - - -class TestGetDepGroups: - def test_no_dev_section(self, tmp_path: Path): - (tmp_path / "pyproject.toml").touch() - - with change_cwd(tmp_path), PyprojectTOMLManager(): - assert get_dep_groups() == {} - - def test_empty_section(self, tmp_path: Path): - (tmp_path / "pyproject.toml").write_text("""\ -[dependency-groups] -""") - - with change_cwd(tmp_path), PyprojectTOMLManager(): - assert get_dep_groups() == {} - - def test_empty_group(self, tmp_path: Path): - (tmp_path / "pyproject.toml").write_text("""\ -[dependency-groups] -test=[] -""") - - with change_cwd(tmp_path), PyprojectTOMLManager(): - assert get_dep_groups() == {"test": []} - - def test_single_dev_dep(self, tmp_path: Path): - (tmp_path / "pyproject.toml").write_text("""\ -[dependency-groups] -test=['pytest'] -""") - - with change_cwd(tmp_path), PyprojectTOMLManager(): - assert get_dep_groups() == {"test": [Dependency(name="pytest")]} - - def test_multiple_dev_deps(self, tmp_path: Path): - (tmp_path / "pyproject.toml").write_text("""\ -[dependency-groups] -qa=["flake8", "black", "isort"] -""") - - with change_cwd(tmp_path), PyprojectTOMLManager(): - assert get_dep_groups() == { - "qa": [ - Dependency(name="flake8"), - Dependency(name="black"), - Dependency(name="isort"), - ] - } - - def test_multiple_groups(self, tmp_path: Path): - (tmp_path / "pyproject.toml").write_text( - """\ -[dependency-groups] -qa=["flake8", "black", "isort"] -test=['pytest'] -""" - ) - - with change_cwd(tmp_path), PyprojectTOMLManager(): - assert get_dep_groups() == { - "qa": [ - Dependency(name="flake8"), - Dependency(name="black"), - Dependency(name="isort"), - ], - "test": [ - Dependency(name="pytest"), - ], - } - - def test_no_pyproject(self, tmp_path: Path): - # Act - with change_cwd(tmp_path), PyprojectTOMLManager(): - result = get_dep_groups() - - # Assert - assert result == {} - - def test_invalid_dtype(self, tmp_path: Path): - # Arrange - (tmp_path / "pyproject.toml").write_text("""\ -[dependency-groups] -test="not a list" -""") - # Act, Assert - with ( - change_cwd(tmp_path), - PyprojectTOMLManager(), - pytest.raises(DepGroupError), - ): - get_dep_groups() - - -class TestAddDepsToGroup: - @pytest.mark.usefixtures("_vary_network_conn") - def test_pyproject_changed(self, uv_init_dir: Path): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Act - add_deps_to_group([Dependency(name="pytest")], "test") - - # Assert - assert is_dep_satisfied_in( - Dependency(name="pytest"), in_=get_deps_from_group("test") - ) - - @pytest.mark.usefixtures("_vary_network_conn") - def test_single_dep(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Act - add_deps_to_group([Dependency(name="pytest")], "test") - - # Assert - assert get_deps_from_group("test") == [Dependency(name="pytest")] - out, err = capfd.readouterr() - assert not err - assert ( - out - == "✔ Adding dependency 'pytest' to the 'test' group in 'pyproject.toml'.\n" - "☐ Install the dependency 'pytest'.\n" - ) - - @pytest.mark.usefixtures("_vary_network_conn") - def test_multiple_deps(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Act - add_deps_to_group( - [Dependency(name="flake8"), Dependency(name="black")], "qa" - ) - - # Assert - assert set(get_deps_from_group("qa")) == { - Dependency(name="flake8"), - Dependency(name="black"), - } - out, err = capfd.readouterr() - assert not err - assert ( - out - == "✔ Adding dependencies 'flake8', 'black' to the 'qa' group in 'pyproject.toml'.\n" - "☐ Install the dependencies 'flake8', 'black'.\n" - ) - - @pytest.mark.usefixtures("_vary_network_conn") - def test_multi_but_one_already_exists( - self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str] - ): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Arrange - with usethis_config.set(quiet=True): - add_deps_to_group([Dependency(name="pytest")], "test") - - # Act - add_deps_to_group( - [Dependency(name="pytest"), Dependency(name="black")], "test" - ) - - # Assert - assert set(get_deps_from_group("test")) == { - Dependency(name="pytest"), - Dependency(name="black"), - } - out, err = capfd.readouterr() - assert not err - assert ( - out - == "✔ Adding dependency 'black' to the 'test' group in 'pyproject.toml'.\n" - "☐ Install the dependency 'black'.\n" - ) - - @pytest.mark.usefixtures("_vary_network_conn") - def test_extras(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Act - add_deps_to_group( - [Dependency(name="pytest", extras=frozenset({"extra"}))], "test" - ) - - # Assert - assert is_dep_satisfied_in( - Dependency(name="pytest", extras=frozenset({"extra"})), - in_=get_deps_from_group("test"), - ) - content = (uv_init_dir / "pyproject.toml").read_text() - assert "pytest[extra]" in content - out, err = capfd.readouterr() - assert not err - assert ( - out - == "✔ Adding dependency 'pytest' to the 'test' group in 'pyproject.toml'.\n" - "☐ Install the dependency 'pytest'.\n" - ) - - @pytest.mark.usefixtures("_vary_network_conn") - def test_empty_deps(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Act - add_deps_to_group([], "test") - - # Assert - assert not get_deps_from_group("test") - out, err = capfd.readouterr() - assert not err - assert not out - - @pytest.mark.usefixtures("_vary_network_conn") - def test_extra_when_nonextra_already_present(self, uv_init_dir: Path): - # https://github.com/usethis-python/usethis-python/issues/227 - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Arrange - add_deps_to_group([Dependency(name="coverage")], "test") - - # Act - add_deps_to_group( - [Dependency(name="coverage", extras=frozenset({"toml"}))], "test" - ) - - # Assert - content = (uv_init_dir / "pyproject.toml").read_text() - assert "coverage[toml]" in content - - @pytest.mark.usefixtures("_vary_network_conn") - def test_extras_combining_together(self, uv_init_dir: Path): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Arrange - add_deps_to_group( - [Dependency(name="coverage", extras=frozenset({"toml"}))], "test" - ) - - # Act - add_deps_to_group( - [Dependency(name="coverage", extras=frozenset({"extra"}))], "test" - ) - - # Assert - content = (uv_init_dir / "pyproject.toml").read_text() - assert "coverage[extra,toml]" in content - - @pytest.mark.usefixtures("_vary_network_conn") - def test_combine_extras_alphabetical(self, uv_init_dir: Path): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Arrange - add_deps_to_group( - [Dependency(name="coverage", extras=frozenset({"extra"}))], "test" - ) - - # Act - add_deps_to_group( - [Dependency(name="coverage", extras=frozenset({"toml"}))], "test" - ) - - # Assert - content = (uv_init_dir / "pyproject.toml").read_text() - assert "coverage[extra,toml]" in content - - @pytest.mark.usefixtures("_vary_network_conn") - def test_registers_default_group(self, uv_init_dir: Path): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Act - add_deps_to_group([Dependency(name="pytest")], "test") - - # Assert - default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] - assert "test" in default_groups - - @pytest.mark.usefixtures("_vary_network_conn") - def test_dev_group_not_registered(self, uv_init_dir: Path): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Act - add_deps_to_group([Dependency(name="black")], "dev") - - # Assert - assert ["tool", "uv", "default-groups"] not in PyprojectTOMLManager() - - def test_uv_subprocess_error( - self, uv_init_dir: Path, monkeypatch: pytest.MonkeyPatch - ): - def mock_call_uv_subprocess(*_, **__): - raise UVSubprocessFailedError - - monkeypatch.setattr( - usethis._integrations.backend.uv.deps, - "call_uv_subprocess", - mock_call_uv_subprocess, - ) - - # Act, Assert - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - with pytest.raises( - UVDepGroupError, - match="Failed to add 'pytest' to the 'test' dependency group", - ): - add_deps_to_group([Dependency(name="pytest")], "test") - - # Assert contd - # We want to check that registration hasn't taken place - default_groups = get_default_groups() - assert "test" not in default_groups - - -class TestRemoveDepsFromGroup: - @pytest.mark.usefixtures("_vary_network_conn") - def test_pyproject_changed(self, uv_init_dir: Path): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Arrange - add_deps_to_group([Dependency(name="pytest")], "test") - - # Act - remove_deps_from_group([Dependency(name="pytest")], "test") - - # Assert - assert "pytest" not in get_deps_from_group("test") - - @pytest.mark.usefixtures("_vary_network_conn") - def test_single_dep(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Arrange - with usethis_config.set(quiet=True): - add_deps_to_group([Dependency(name="pytest")], "test") - - # Act - remove_deps_from_group([Dependency(name="pytest")], "test") - - # Assert - assert not get_deps_from_group("test") - out, err = capfd.readouterr() - assert not err - assert ( - out - == "✔ Removing dependency 'pytest' from the 'test' group in 'pyproject.toml'.\n" - ) - - @pytest.mark.usefixtures("_vary_network_conn") - def test_multiple_deps(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Arrange - with usethis_config.set(quiet=True): - add_deps_to_group( - [Dependency(name="flake8"), Dependency(name="black")], "qa" - ) - - # Act - remove_deps_from_group( - [Dependency(name="flake8"), Dependency(name="black")], "qa" - ) - - # Assert - assert not get_deps_from_group("qa") - out, err = capfd.readouterr() - assert not err - assert ( - out - == "✔ Removing dependencies 'flake8', 'black' from the 'qa' group in \n'pyproject.toml'.\n" - ) - - @pytest.mark.usefixtures("_vary_network_conn") - def test_multi_but_only_not_exists( - self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str] - ): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Arrange - with usethis_config.set(quiet=True): - add_deps_to_group([Dependency(name="pytest")], "test") - - # Act - remove_deps_from_group( - [Dependency(name="pytest"), Dependency(name="black")], "test" - ) - - # Assert - assert not get_deps_from_group("test") - out, err = capfd.readouterr() - assert not err - assert ( - out - == "✔ Removing dependency 'pytest' from the 'test' group in 'pyproject.toml'.\n" - ) - - @pytest.mark.usefixtures("_vary_network_conn") - def test_extras(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Arrange - with usethis_config.set(quiet=True): - add_deps_to_group( - [Dependency(name="pytest", extras=frozenset({"extra"}))], "test" - ) - - # Act - remove_deps_from_group( - [Dependency(name="pytest", extras=frozenset({"extra"}))], "test" - ) - - # Assert - assert not get_deps_from_group("test") - out, err = capfd.readouterr() - assert not err - assert ( - out - == "✔ Removing dependency 'pytest' from the 'test' group in 'pyproject.toml'.\n" - ) - - @pytest.mark.usefixtures("_vary_network_conn") - def test_group_not_in_dependency_groups( - self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str] - ): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - # Arrange - with usethis_config.set(quiet=True): - add_deps_to_group([Dependency(name="pytest")], "test") - - # Remove the group from dependency-groups but keep it in default-groups - del PyprojectTOMLManager()[["dependency-groups", "test"]] - - # Act - remove_deps_from_group([Dependency(name="pytest")], "test") - - # Assert - assert not get_deps_from_group("test") - out, err = capfd.readouterr() - assert not err - assert not out - - def test_uv_subprocess_error( - self, uv_init_dir: Path, monkeypatch: pytest.MonkeyPatch - ): - with ( - change_cwd(uv_init_dir), - PyprojectTOMLManager(), - ): - # Arrange - add_deps_to_group([Dependency(name="pytest")], "test") - - def mock_call_uv_subprocess(*_, **__): - raise UVSubprocessFailedError - - monkeypatch.setattr( - usethis._integrations.backend.uv.deps, - "call_uv_subprocess", - mock_call_uv_subprocess, - ) - - # Act - with pytest.raises( - UVDepGroupError, - match="Failed to remove 'pytest' from the 'test' dependency group", - ): - remove_deps_from_group([Dependency(name="pytest")], "test") - - -class TestIsDepInAnyGroup: - def test_no_group(self, uv_init_dir: Path): - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - assert not is_dep_in_any_group(Dependency(name="pytest")) - - @pytest.mark.usefixtures("_vary_network_conn") - def test_in_group(self, uv_init_dir: Path): - # Arrange - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - add_deps_to_group([Dependency(name="pytest")], "test") - - # Act - result = is_dep_in_any_group(Dependency(name="pytest")) - - # Assert - assert result - - @pytest.mark.usefixtures("_vary_network_conn") - def test_not_in_group(self, uv_init_dir: Path): - # Arrange - with change_cwd(uv_init_dir), PyprojectTOMLManager(): - add_deps_to_group([Dependency(name="pytest")], "test") - - # Act - result = is_dep_in_any_group(Dependency(name="black")) - - # Assert - assert not result - - -class TestIsDepSatisfiedIn: - def test_empty(self): - # Arrange - dep = Dependency(name="pytest") - in_ = [] - - # Act - result = is_dep_satisfied_in(dep, in_=in_) - - # Assert - assert not result - - def test_same(self): - # Arrange - dep = Dependency(name="pytest") - in_ = [Dependency(name="pytest")] - - # Act - result = is_dep_satisfied_in(dep, in_=in_) - - # Assert - assert result - - def test_same_name_superset_extra(self): - # Arrange - dep = Dependency(name="pytest", extras=frozenset({"extra"})) - in_ = [Dependency(name="pytest")] - - # Act - result = is_dep_satisfied_in(dep, in_=in_) - - # Assert - assert not result - - def test_same_name_subset_extra(self): - # Arrange - dep = Dependency(name="pytest") - in_ = [Dependency(name="pytest", extras=frozenset({"extra"}))] - - # Act - result = is_dep_satisfied_in(dep, in_=in_) - - # Assert - assert result - - def test_multiple(self): - # Arrange - dep = Dependency(name="pytest") - in_ = [Dependency(name="flake8"), Dependency(name="pytest")] - - # Act - result = is_dep_satisfied_in(dep, in_=in_) - - # Assert - assert result - - -class TestRegisterDefaultGroup: - def test_section_not_exists_adds_dev(self, tmp_path: Path): - # Arrange - (tmp_path / "pyproject.toml").write_text("") - - with change_cwd(tmp_path), PyprojectTOMLManager(): - # Act - register_default_group("test") - - # Assert - default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] - assert set(default_groups) == {"test", "dev"} - - def test_empty_section_adds_dev(self, tmp_path: Path): - # Arrange - (tmp_path / "pyproject.toml").write_text("""\ -[tool.uv] -""") - - with change_cwd(tmp_path), PyprojectTOMLManager(): - # Act - register_default_group("test") - - # Assert - default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] - assert set(default_groups) == {"test", "dev"} - - def test_empty_default_groups_adds_dev(self, tmp_path: Path): - # Arrange - (tmp_path / "pyproject.toml").write_text("""\ -[tool.uv] -default-groups = [] -""") - - with change_cwd(tmp_path), PyprojectTOMLManager(): - # Act - register_default_group("test") - - # Assert - default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] - assert set(default_groups) == {"test", "dev"} - - def test_existing_section_no_dev_added_if_no_other_groups(self, tmp_path: Path): - # Arrange - (tmp_path / "pyproject.toml").write_text("""\ -[tool.uv] -default-groups = ["test"] -""") - - with change_cwd(tmp_path), PyprojectTOMLManager(): - # Act - register_default_group("test") - - # Assert - default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] - assert set(default_groups) == {"test"} - - def test_existing_section_no_dev_added_if_dev_exists(self, tmp_path: Path): - # Arrange - (tmp_path / "pyproject.toml").write_text("""\ -[tool.uv] -default-groups = ["test", "dev"] -""") - - with change_cwd(tmp_path), PyprojectTOMLManager(): - # Act - register_default_group("docs") - - # Assert - default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] - assert set(default_groups) == {"test", "dev", "docs"} - - def test_existing_section_adds_dev_with_new_group(self, tmp_path: Path): - # Arrange - (tmp_path / "pyproject.toml").write_text("""\ -[tool.uv] -default-groups = ["test"] -""") - - with change_cwd(tmp_path), PyprojectTOMLManager(): - # Act - register_default_group("docs") - - # Assert - default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] - assert set(default_groups) == {"test", "docs", "dev"} - - def test_dev_not_added_if_missing(self, tmp_path: Path): - # Arrange - (tmp_path / "pyproject.toml").write_text("""\ -[tool.uv] -default-groups = ["test"] -""") - - with change_cwd(tmp_path), PyprojectTOMLManager(): - # Act - register_default_group("test") - - # Assert - default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] - assert set(default_groups) == {"test"} - - -class TestAddDefaultGroups: - def test_uv_toml(self, tmp_path: Path): - # Arrange - (tmp_path / "uv.toml").touch() - - # Act - with change_cwd(tmp_path), files_manager(): - add_default_groups(["test"]) - - # Assert - with change_cwd(tmp_path), UVTOMLManager(): - assert ( - (tmp_path / "uv.toml").read_text() - == """\ -default-groups = ["test"] -""" - ) - - -class TestGetDefaultGroups: - def test_empty_pyproject_toml(self, tmp_path: Path): - # Arrange - (tmp_path / "pyproject.toml").touch() - - with change_cwd(tmp_path), PyprojectTOMLManager(): - # Act - result = get_default_groups() - - # Assert - assert result == [] - - def test_invalid_default_groups(self, tmp_path: Path): - # Arrange - (tmp_path / "pyproject.toml").write_text("""\ -[tool.uv] -default-groups = "not a list" -""") - - with change_cwd(tmp_path), PyprojectTOMLManager(): - # Act - result = get_default_groups() - - # Assert - assert result == [] - - def test_uv_toml(self, tmp_path: Path): - # Arrange - (tmp_path / "uv.toml").write_text("""\ -default-groups = ["test"] -""") - # Even if the pyproject.toml disagrees! - (tmp_path / "pyproject.toml").write_text("""\ -[tool.uv] -default-groups = ["doc"] -""") - - with change_cwd(tmp_path), files_manager(): - # Act - result = get_default_groups() - - # Assert - assert result == ["test"] - - def test_uv_toml_empty(self, tmp_path: Path): - # Arrange - (tmp_path / "uv.toml").touch() - (tmp_path / "pyproject.toml").write_text("""\ -[tool.uv] -default-groups = ["doc"] -""") - - with change_cwd(tmp_path), files_manager(): - # Act - result = get_default_groups() - - # Assert - assert result == [] diff --git a/tests/usethis/test_deps.py b/tests/usethis/test_deps.py new file mode 100644 index 00000000..35ac405f --- /dev/null +++ b/tests/usethis/test_deps.py @@ -0,0 +1,740 @@ +from pathlib import Path + +import pytest + +import usethis._integrations.backend.uv.deps +from usethis._config import usethis_config +from usethis._config_file import files_manager +from usethis._deps import ( + add_default_groups, + add_deps_to_group, + get_default_groups, + get_dep_groups, + get_deps_from_group, + is_dep_in_any_group, + is_dep_satisfied_in, + register_default_group, + remove_deps_from_group, +) +from usethis._integrations.backend.uv.errors import UVSubprocessFailedError +from usethis._integrations.backend.uv.toml import UVTOMLManager +from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager +from usethis._test import change_cwd +from usethis._types.deps import Dependency +from usethis.errors import DepGroupError + + +class TestGetDepGroups: + def test_no_dev_section(self, tmp_path: Path): + (tmp_path / "pyproject.toml").touch() + + with change_cwd(tmp_path), PyprojectTOMLManager(): + assert get_dep_groups() == {} + + def test_empty_section(self, tmp_path: Path): + (tmp_path / "pyproject.toml").write_text("""\ +[dependency-groups] +""") + + with change_cwd(tmp_path), PyprojectTOMLManager(): + assert get_dep_groups() == {} + + def test_empty_group(self, tmp_path: Path): + (tmp_path / "pyproject.toml").write_text("""\ +[dependency-groups] +test=[] +""") + + with change_cwd(tmp_path), PyprojectTOMLManager(): + assert get_dep_groups() == {"test": []} + + def test_single_dev_dep(self, tmp_path: Path): + (tmp_path / "pyproject.toml").write_text("""\ +[dependency-groups] +test=['pytest'] +""") + + with change_cwd(tmp_path), PyprojectTOMLManager(): + assert get_dep_groups() == {"test": [Dependency(name="pytest")]} + + def test_multiple_dev_deps(self, tmp_path: Path): + (tmp_path / "pyproject.toml").write_text("""\ +[dependency-groups] +qa=["flake8", "black", "isort"] +""") + + with change_cwd(tmp_path), PyprojectTOMLManager(): + assert get_dep_groups() == { + "qa": [ + Dependency(name="flake8"), + Dependency(name="black"), + Dependency(name="isort"), + ] + } + + def test_multiple_groups(self, tmp_path: Path): + (tmp_path / "pyproject.toml").write_text( + """\ +[dependency-groups] +qa=["flake8", "black", "isort"] +test=['pytest'] +""" + ) + + with change_cwd(tmp_path), PyprojectTOMLManager(): + assert get_dep_groups() == { + "qa": [ + Dependency(name="flake8"), + Dependency(name="black"), + Dependency(name="isort"), + ], + "test": [ + Dependency(name="pytest"), + ], + } + + def test_no_pyproject(self, tmp_path: Path): + # Act + with change_cwd(tmp_path), PyprojectTOMLManager(): + result = get_dep_groups() + + # Assert + assert result == {} + + def test_invalid_dtype(self, tmp_path: Path): + # Arrange + (tmp_path / "pyproject.toml").write_text("""\ +[dependency-groups] +test="not a list" +""") + # Act, Assert + with ( + change_cwd(tmp_path), + PyprojectTOMLManager(), + pytest.raises(DepGroupError), + ): + get_dep_groups() + + +class TestAddDepsToGroup: + @pytest.mark.usefixtures("_vary_network_conn") + def test_pyproject_changed(self, uv_init_dir: Path): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Act + add_deps_to_group([Dependency(name="pytest")], "test") + + # Assert + assert is_dep_satisfied_in( + Dependency(name="pytest"), in_=get_deps_from_group("test") + ) + + @pytest.mark.usefixtures("_vary_network_conn") + def test_single_dep(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Act + add_deps_to_group([Dependency(name="pytest")], "test") + + # Assert + assert get_deps_from_group("test") == [Dependency(name="pytest")] + out, err = capfd.readouterr() + assert not err + assert ( + out + == "✔ Adding dependency 'pytest' to the 'test' group in 'pyproject.toml'.\n" + "☐ Install the dependency 'pytest'.\n" + ) + + @pytest.mark.usefixtures("_vary_network_conn") + def test_multiple_deps(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Act + add_deps_to_group( + [Dependency(name="flake8"), Dependency(name="black")], "qa" + ) + + # Assert + assert set(get_deps_from_group("qa")) == { + Dependency(name="flake8"), + Dependency(name="black"), + } + out, err = capfd.readouterr() + assert not err + assert ( + out + == "✔ Adding dependencies 'flake8', 'black' to the 'qa' group in 'pyproject.toml'.\n" + "☐ Install the dependencies 'flake8', 'black'.\n" + ) + + @pytest.mark.usefixtures("_vary_network_conn") + def test_multi_but_one_already_exists( + self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str] + ): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Arrange + with usethis_config.set(quiet=True): + add_deps_to_group([Dependency(name="pytest")], "test") + + # Act + add_deps_to_group( + [Dependency(name="pytest"), Dependency(name="black")], "test" + ) + + # Assert + assert set(get_deps_from_group("test")) == { + Dependency(name="pytest"), + Dependency(name="black"), + } + out, err = capfd.readouterr() + assert not err + assert ( + out + == "✔ Adding dependency 'black' to the 'test' group in 'pyproject.toml'.\n" + "☐ Install the dependency 'black'.\n" + ) + + @pytest.mark.usefixtures("_vary_network_conn") + def test_extras(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Act + add_deps_to_group( + [Dependency(name="pytest", extras=frozenset({"extra"}))], "test" + ) + + # Assert + assert is_dep_satisfied_in( + Dependency(name="pytest", extras=frozenset({"extra"})), + in_=get_deps_from_group("test"), + ) + content = (uv_init_dir / "pyproject.toml").read_text() + assert "pytest[extra]" in content + out, err = capfd.readouterr() + assert not err + assert ( + out + == "✔ Adding dependency 'pytest' to the 'test' group in 'pyproject.toml'.\n" + "☐ Install the dependency 'pytest'.\n" + ) + + @pytest.mark.usefixtures("_vary_network_conn") + def test_empty_deps(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Act + add_deps_to_group([], "test") + + # Assert + assert not get_deps_from_group("test") + out, err = capfd.readouterr() + assert not err + assert not out + + @pytest.mark.usefixtures("_vary_network_conn") + def test_extra_when_nonextra_already_present(self, uv_init_dir: Path): + # https://github.com/usethis-python/usethis-python/issues/227 + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Arrange + add_deps_to_group([Dependency(name="coverage")], "test") + + # Act + add_deps_to_group( + [Dependency(name="coverage", extras=frozenset({"toml"}))], "test" + ) + + # Assert + content = (uv_init_dir / "pyproject.toml").read_text() + assert "coverage[toml]" in content + + @pytest.mark.usefixtures("_vary_network_conn") + def test_extras_combining_together(self, uv_init_dir: Path): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Arrange + add_deps_to_group( + [Dependency(name="coverage", extras=frozenset({"toml"}))], "test" + ) + + # Act + add_deps_to_group( + [Dependency(name="coverage", extras=frozenset({"extra"}))], "test" + ) + + # Assert + content = (uv_init_dir / "pyproject.toml").read_text() + assert "coverage[extra,toml]" in content + + @pytest.mark.usefixtures("_vary_network_conn") + def test_combine_extras_alphabetical(self, uv_init_dir: Path): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Arrange + add_deps_to_group( + [Dependency(name="coverage", extras=frozenset({"extra"}))], "test" + ) + + # Act + add_deps_to_group( + [Dependency(name="coverage", extras=frozenset({"toml"}))], "test" + ) + + # Assert + content = (uv_init_dir / "pyproject.toml").read_text() + assert "coverage[extra,toml]" in content + + @pytest.mark.usefixtures("_vary_network_conn") + def test_registers_default_group(self, uv_init_dir: Path): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Act + add_deps_to_group([Dependency(name="pytest")], "test") + + # Assert + default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] + assert "test" in default_groups + + @pytest.mark.usefixtures("_vary_network_conn") + def test_dev_group_not_registered(self, uv_init_dir: Path): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Act + add_deps_to_group([Dependency(name="black")], "dev") + + # Assert + assert ["tool", "uv", "default-groups"] not in PyprojectTOMLManager() + + def test_uv_subprocess_error( + self, uv_init_dir: Path, monkeypatch: pytest.MonkeyPatch + ): + def mock_call_uv_subprocess(*_, **__): + raise UVSubprocessFailedError + + monkeypatch.setattr( + usethis._integrations.backend.uv.deps, + "call_uv_subprocess", + mock_call_uv_subprocess, + ) + + # Act, Assert + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + with pytest.raises( + DepGroupError, + match="Failed to add 'pytest' to the 'test' dependency group", + ): + add_deps_to_group([Dependency(name="pytest")], "test") + + # Assert contd + # We want to check that registration hasn't taken place + default_groups = get_default_groups() + assert "test" not in default_groups + + +class TestRemoveDepsFromGroup: + @pytest.mark.usefixtures("_vary_network_conn") + def test_pyproject_changed(self, uv_init_dir: Path): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Arrange + add_deps_to_group([Dependency(name="pytest")], "test") + + # Act + remove_deps_from_group([Dependency(name="pytest")], "test") + + # Assert + assert "pytest" not in get_deps_from_group("test") + + @pytest.mark.usefixtures("_vary_network_conn") + def test_single_dep(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Arrange + with usethis_config.set(quiet=True): + add_deps_to_group([Dependency(name="pytest")], "test") + + # Act + remove_deps_from_group([Dependency(name="pytest")], "test") + + # Assert + assert not get_deps_from_group("test") + out, err = capfd.readouterr() + assert not err + assert ( + out + == "✔ Removing dependency 'pytest' from the 'test' group in 'pyproject.toml'.\n" + ) + + @pytest.mark.usefixtures("_vary_network_conn") + def test_multiple_deps(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Arrange + with usethis_config.set(quiet=True): + add_deps_to_group( + [Dependency(name="flake8"), Dependency(name="black")], "qa" + ) + + # Act + remove_deps_from_group( + [Dependency(name="flake8"), Dependency(name="black")], "qa" + ) + + # Assert + assert not get_deps_from_group("qa") + out, err = capfd.readouterr() + assert not err + assert ( + out + == "✔ Removing dependencies 'flake8', 'black' from the 'qa' group in \n'pyproject.toml'.\n" + ) + + @pytest.mark.usefixtures("_vary_network_conn") + def test_multi_but_only_not_exists( + self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str] + ): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Arrange + with usethis_config.set(quiet=True): + add_deps_to_group([Dependency(name="pytest")], "test") + + # Act + remove_deps_from_group( + [Dependency(name="pytest"), Dependency(name="black")], "test" + ) + + # Assert + assert not get_deps_from_group("test") + out, err = capfd.readouterr() + assert not err + assert ( + out + == "✔ Removing dependency 'pytest' from the 'test' group in 'pyproject.toml'.\n" + ) + + @pytest.mark.usefixtures("_vary_network_conn") + def test_extras(self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str]): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Arrange + with usethis_config.set(quiet=True): + add_deps_to_group( + [Dependency(name="pytest", extras=frozenset({"extra"}))], "test" + ) + + # Act + remove_deps_from_group( + [Dependency(name="pytest", extras=frozenset({"extra"}))], "test" + ) + + # Assert + assert not get_deps_from_group("test") + out, err = capfd.readouterr() + assert not err + assert ( + out + == "✔ Removing dependency 'pytest' from the 'test' group in 'pyproject.toml'.\n" + ) + + @pytest.mark.usefixtures("_vary_network_conn") + def test_group_not_in_dependency_groups( + self, uv_init_dir: Path, capfd: pytest.CaptureFixture[str] + ): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + # Arrange + with usethis_config.set(quiet=True): + add_deps_to_group([Dependency(name="pytest")], "test") + + # Remove the group from dependency-groups but keep it in default-groups + del PyprojectTOMLManager()[["dependency-groups", "test"]] + + # Act + remove_deps_from_group([Dependency(name="pytest")], "test") + + # Assert + assert not get_deps_from_group("test") + out, err = capfd.readouterr() + assert not err + assert not out + + def test_uv_subprocess_error( + self, uv_init_dir: Path, monkeypatch: pytest.MonkeyPatch + ): + with ( + change_cwd(uv_init_dir), + PyprojectTOMLManager(), + ): + # Arrange + add_deps_to_group([Dependency(name="pytest")], "test") + + def mock_call_uv_subprocess(*_, **__): + raise UVSubprocessFailedError + + monkeypatch.setattr( + usethis._integrations.backend.uv.deps, + "call_uv_subprocess", + mock_call_uv_subprocess, + ) + + # Act + with pytest.raises( + DepGroupError, + match="Failed to remove 'pytest' from the 'test' dependency group", + ): + remove_deps_from_group([Dependency(name="pytest")], "test") + + +class TestIsDepInAnyGroup: + def test_no_group(self, uv_init_dir: Path): + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + assert not is_dep_in_any_group(Dependency(name="pytest")) + + @pytest.mark.usefixtures("_vary_network_conn") + def test_in_group(self, uv_init_dir: Path): + # Arrange + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + add_deps_to_group([Dependency(name="pytest")], "test") + + # Act + result = is_dep_in_any_group(Dependency(name="pytest")) + + # Assert + assert result + + @pytest.mark.usefixtures("_vary_network_conn") + def test_not_in_group(self, uv_init_dir: Path): + # Arrange + with change_cwd(uv_init_dir), PyprojectTOMLManager(): + add_deps_to_group([Dependency(name="pytest")], "test") + + # Act + result = is_dep_in_any_group(Dependency(name="black")) + + # Assert + assert not result + + +class TestIsDepSatisfiedIn: + def test_empty(self): + # Arrange + dep = Dependency(name="pytest") + in_ = [] + + # Act + result = is_dep_satisfied_in(dep, in_=in_) + + # Assert + assert not result + + def test_same(self): + # Arrange + dep = Dependency(name="pytest") + in_ = [Dependency(name="pytest")] + + # Act + result = is_dep_satisfied_in(dep, in_=in_) + + # Assert + assert result + + def test_same_name_superset_extra(self): + # Arrange + dep = Dependency(name="pytest", extras=frozenset({"extra"})) + in_ = [Dependency(name="pytest")] + + # Act + result = is_dep_satisfied_in(dep, in_=in_) + + # Assert + assert not result + + def test_same_name_subset_extra(self): + # Arrange + dep = Dependency(name="pytest") + in_ = [Dependency(name="pytest", extras=frozenset({"extra"}))] + + # Act + result = is_dep_satisfied_in(dep, in_=in_) + + # Assert + assert result + + def test_multiple(self): + # Arrange + dep = Dependency(name="pytest") + in_ = [Dependency(name="flake8"), Dependency(name="pytest")] + + # Act + result = is_dep_satisfied_in(dep, in_=in_) + + # Assert + assert result + + +class TestRegisterDefaultGroup: + def test_section_not_exists_adds_dev(self, tmp_path: Path): + # Arrange + (tmp_path / "pyproject.toml").write_text("") + + with change_cwd(tmp_path), PyprojectTOMLManager(): + # Act + register_default_group("test") + + # Assert + default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] + assert set(default_groups) == {"test", "dev"} + + def test_empty_section_adds_dev(self, tmp_path: Path): + # Arrange + (tmp_path / "pyproject.toml").write_text("""\ +[tool.uv] +""") + + with change_cwd(tmp_path), PyprojectTOMLManager(): + # Act + register_default_group("test") + + # Assert + default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] + assert set(default_groups) == {"test", "dev"} + + def test_empty_default_groups_adds_dev(self, tmp_path: Path): + # Arrange + (tmp_path / "pyproject.toml").write_text("""\ +[tool.uv] +default-groups = [] +""") + + with change_cwd(tmp_path), PyprojectTOMLManager(): + # Act + register_default_group("test") + + # Assert + default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] + assert set(default_groups) == {"test", "dev"} + + def test_existing_section_no_dev_added_if_no_other_groups(self, tmp_path: Path): + # Arrange + (tmp_path / "pyproject.toml").write_text("""\ +[tool.uv] +default-groups = ["test"] +""") + + with change_cwd(tmp_path), PyprojectTOMLManager(): + # Act + register_default_group("test") + + # Assert + default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] + assert set(default_groups) == {"test"} + + def test_existing_section_no_dev_added_if_dev_exists(self, tmp_path: Path): + # Arrange + (tmp_path / "pyproject.toml").write_text("""\ +[tool.uv] +default-groups = ["test", "dev"] +""") + + with change_cwd(tmp_path), PyprojectTOMLManager(): + # Act + register_default_group("docs") + + # Assert + default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] + assert set(default_groups) == {"test", "dev", "docs"} + + def test_existing_section_adds_dev_with_new_group(self, tmp_path: Path): + # Arrange + (tmp_path / "pyproject.toml").write_text("""\ +[tool.uv] +default-groups = ["test"] +""") + + with change_cwd(tmp_path), PyprojectTOMLManager(): + # Act + register_default_group("docs") + + # Assert + default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] + assert set(default_groups) == {"test", "docs", "dev"} + + def test_dev_not_added_if_missing(self, tmp_path: Path): + # Arrange + (tmp_path / "pyproject.toml").write_text("""\ +[tool.uv] +default-groups = ["test"] +""") + + with change_cwd(tmp_path), PyprojectTOMLManager(): + # Act + register_default_group("test") + + # Assert + default_groups = PyprojectTOMLManager()[["tool", "uv", "default-groups"]] + assert set(default_groups) == {"test"} + + +class TestAddDefaultGroups: + def test_uv_toml(self, tmp_path: Path): + # Arrange + (tmp_path / "uv.toml").touch() + + # Act + with change_cwd(tmp_path), files_manager(): + add_default_groups(["test"]) + + # Assert + with change_cwd(tmp_path), UVTOMLManager(): + assert ( + (tmp_path / "uv.toml").read_text() + == """\ +default-groups = ["test"] +""" + ) + + +class TestGetDefaultGroups: + def test_empty_pyproject_toml(self, tmp_path: Path): + # Arrange + (tmp_path / "pyproject.toml").touch() + + with change_cwd(tmp_path), PyprojectTOMLManager(): + # Act + result = get_default_groups() + + # Assert + assert result == [] + + def test_invalid_default_groups(self, tmp_path: Path): + # Arrange + (tmp_path / "pyproject.toml").write_text("""\ +[tool.uv] +default-groups = "not a list" +""") + + with change_cwd(tmp_path), PyprojectTOMLManager(): + # Act + result = get_default_groups() + + # Assert + assert result == [] + + def test_uv_toml(self, tmp_path: Path): + # Arrange + (tmp_path / "uv.toml").write_text("""\ +default-groups = ["test"] +""") + # Even if the pyproject.toml disagrees! + (tmp_path / "pyproject.toml").write_text("""\ +[tool.uv] +default-groups = ["doc"] +""") + + with change_cwd(tmp_path), files_manager(): + # Act + result = get_default_groups() + + # Assert + assert result == ["test"] + + def test_uv_toml_empty(self, tmp_path: Path): + # Arrange + (tmp_path / "uv.toml").touch() + (tmp_path / "pyproject.toml").write_text("""\ +[tool.uv] +default-groups = ["doc"] +""") + + with change_cwd(tmp_path), files_manager(): + # Act + result = get_default_groups() + + # Assert + assert result == [] From 151d45a5fcb3c5968e93d10c178022a521a935dd Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Mon, 30 Jun 2025 18:48:04 +1200 Subject: [PATCH 08/55] Test `add_default_groups` for the `backend=BackendEnum.none` case --- tests/usethis/test_deps.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/usethis/test_deps.py b/tests/usethis/test_deps.py index 35ac405f..a2c1c6b8 100644 --- a/tests/usethis/test_deps.py +++ b/tests/usethis/test_deps.py @@ -20,6 +20,7 @@ from usethis._integrations.backend.uv.toml import UVTOMLManager from usethis._integrations.file.pyproject_toml.io_ import PyprojectTOMLManager from usethis._test import change_cwd +from usethis._types.backend import BackendEnum from usethis._types.deps import Dependency from usethis.errors import DepGroupError @@ -679,6 +680,22 @@ def test_uv_toml(self, tmp_path: Path): """ ) + def test_none_backend(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]): + # N.B. should have no effect - default groups are not really defined if we + # use the 'none' backend. + with usethis_config.set(backend=BackendEnum.none): + # Act + with change_cwd(tmp_path), files_manager(): + add_default_groups(["test"]) + + # Assert + assert not (tmp_path / "uv.toml").exists() + assert not (tmp_path / "pyproject.toml").exists() + + out, err = capfd.readouterr() + assert not err + assert not out + class TestGetDefaultGroups: def test_empty_pyproject_toml(self, tmp_path: Path): From 7efdca7e3bed8448b5b213a31cf3ea4bcd57668d Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Sat, 12 Jul 2025 15:35:36 +1200 Subject: [PATCH 09/55] Remove empty file --- tests/usethis/_integrations/backend/uv/test_deps.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/usethis/_integrations/backend/uv/test_deps.py diff --git a/tests/usethis/_integrations/backend/uv/test_deps.py b/tests/usethis/_integrations/backend/uv/test_deps.py deleted file mode 100644 index e69de29b..00000000 From 80751311792c2ce9d5fdffc465cd3d8fd94a9f5c Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Sat, 12 Jul 2025 15:49:36 +1200 Subject: [PATCH 10/55] Add some tests to the deps module --- src/usethis/_deps.py | 2 +- tests/usethis/test_deps.py | 55 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/usethis/_deps.py b/src/usethis/_deps.py index d3b555fe..8d675e8e 100644 --- a/src/usethis/_deps.py +++ b/src/usethis/_deps.py @@ -177,7 +177,7 @@ def add_deps_to_group(deps: list[Dependency], group: str) -> None: f"Adding dependenc{ies} {deps_str} to the '{group}' group in 'pyproject.toml'." ) elif backend is BackendEnum.none: - box_print(f"Install the {group} dependenc{ies} {deps_str}.") + box_print(f"Add the {group} dependenc{ies} {deps_str}.") else: assert_never(backend) diff --git a/tests/usethis/test_deps.py b/tests/usethis/test_deps.py index a2c1c6b8..0c351dbd 100644 --- a/tests/usethis/test_deps.py +++ b/tests/usethis/test_deps.py @@ -322,6 +322,26 @@ def mock_call_uv_subprocess(*_, **__): default_groups = get_default_groups() assert "test" not in default_groups + def test_none_backend(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]): + # Arrange + (tmp_path / "pyproject.toml").write_text("""\ +[dependency-groups] +test = [] +""") + + # Act + with ( + usethis_config.set(backend=BackendEnum.none), + change_cwd(tmp_path), + PyprojectTOMLManager(), + ): + add_deps_to_group([Dependency(name="pytest")], "test") + + # Assert + out, err = capfd.readouterr() + assert not err + assert out == "☐ Add the test dependency 'pytest'.\n" + class TestRemoveDepsFromGroup: @pytest.mark.usefixtures("_vary_network_conn") @@ -471,6 +491,26 @@ def mock_call_uv_subprocess(*_, **__): ): remove_deps_from_group([Dependency(name="pytest")], "test") + def test_none_backend(self, tmp_path: Path, capfd: pytest.CaptureFixture[str]): + # Arrange + (tmp_path / "pyproject.toml").write_text("""\ +[dependency-groups] +test = ["pytest"] +""") + + # Act + with ( + usethis_config.set(backend=BackendEnum.none), + change_cwd(tmp_path), + PyprojectTOMLManager(), + ): + remove_deps_from_group([Dependency(name="pytest")], "test") + + # Assert + out, err = capfd.readouterr() + assert not err + assert out == "☐ Remove the test dependency 'pytest'.\n" + class TestIsDepInAnyGroup: def test_no_group(self, uv_init_dir: Path): @@ -755,3 +795,18 @@ def test_uv_toml_empty(self, tmp_path: Path): # Assert assert result == [] + + def test_none_backend(self, tmp_path: Path): + # Arrange + (tmp_path / "pyproject.toml").write_text("""\ +[tool.uv] +default-groups = ["test"] +""") + + with usethis_config.set(backend=BackendEnum.none): + # Act + with change_cwd(tmp_path), PyprojectTOMLManager(): + result = get_default_groups() + + # Assert + assert result == [] From aa25d602ced4b43411412b66e3dd0246db603c37 Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Sat, 12 Jul 2025 16:09:43 +1200 Subject: [PATCH 11/55] Document `ForbiddenBackendError` in docstring to `call_uv_subprocess` --- src/usethis/_integrations/backend/uv/call.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/usethis/_integrations/backend/uv/call.py b/src/usethis/_integrations/backend/uv/call.py index e40100f1..4c3c7b30 100644 --- a/src/usethis/_integrations/backend/uv/call.py +++ b/src/usethis/_integrations/backend/uv/call.py @@ -21,6 +21,7 @@ def call_uv_subprocess(args: list[str], change_toml: bool) -> str: Raises: UVSubprocessFailedError: If the subprocess fails. + ForbiddenBackendError: If the current backend is not uv (or auto). """ if usethis_config.backend not in {BackendEnum.uv, BackendEnum.auto}: msg = f"The '{usethis_config.backend.value}' backend is enabled, but a uv subprocess was invoked." From 013bcee0b69733a49ec7e451761f5f1c2e96b9d0 Mon Sep 17 00:00:00 2001 From: Nathan McDougall Date: Sat, 12 Jul 2025 16:33:55 +1200 Subject: [PATCH 12/55] Add `--backend` option to all interfaces where it makes sense --- README.md | 62 +++++++++++++++++++++++++--- src/usethis/_interface/author.py | 6 ++- src/usethis/_interface/ci.py | 11 ++++- src/usethis/_interface/docstyle.py | 7 +++- src/usethis/_interface/format_.py | 19 +++++++-- src/usethis/_interface/init.py | 12 +++++- src/usethis/_interface/lint.py | 19 +++++++-- src/usethis/_interface/readme.py | 8 +++- src/usethis/_interface/rule.py | 13 ++++-- src/usethis/_interface/spellcheck.py | 19 +++++++-- src/usethis/_interface/status.py | 5 ++- src/usethis/_interface/test.py | 15 ++++++- 12 files changed, 167 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index d19a0ed9..1cbe1e22 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,6 @@ usethis gives detailed messages about what it is doing (and what you need to do Inspired by an [**R** package of the same name](https://usethis.r-lib.org/index.html), this package brings a similar experience to the Python ecosystem as a CLI tool. -> [!TIP] -> usethis is great for fresh projects using [uv](https://docs.astral.sh/uv), but also supports updating existing projects. However, this should be considered experimental. If you encounter problems or have feedback, please [open an issue](https://github.com/usethis-python/usethis-python/issues/new?template=idea.md). - ## Highlights - 🧰 First-class support for state-of-the-practice tooling: uv, Ruff, pytest, pre-commit, and many more. @@ -202,6 +199,11 @@ Supported options: - `--offline` to disable network access and rely on caches - `--quiet` to suppress output - `--frozen` to leave the virtual environment and lockfile unchanged (i.e. do not install dependencies, nor update lockfiles) +- `--backend` to specify a package manager backend to use. The default is to auto-detect. +Possible values: + - `auto` to auto-detect the backend (default) + - `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. ### `usethis format` @@ -220,6 +222,11 @@ Supported options: - `--offline` to disable network access and rely on caches - `--frozen` to leave the virtual environment and lockfile unchanged - `--quiet` to suppress output +- `--backend` to specify a package manager backend to use. The default is to auto-detect. +Possible values: + - `auto` to auto-detect the backend (default) + - `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. See [`usethis tool`](#usethis-tool) for more information. @@ -240,6 +247,11 @@ Supported options: - `--offline` to disable network access and rely on caches - `--frozen` to leave the virtual environment and lockfile unchanged - `--quiet` to suppress output +- `--backend` to specify a package manager backend to use. The default is to auto-detect. +Possible values: + - `auto` to auto-detect the backend (default) + - `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. See [`usethis tool`](#usethis-tool) for more information. @@ -260,6 +272,11 @@ Supported options: - `--offline` to disable network access and rely on caches - `--frozen` to leave the virtual environment and lockfile unchanged - `--quiet` to suppress output +- `--backend` to specify a package manager backend to use. The default is to auto-detect. +Possible values: + - `auto` to auto-detect the backend (default) + - `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. See [`usethis tool`](#usethis-tool) for more information. @@ -280,6 +297,11 @@ Supported options: - `--offline` to disable network access and rely on caches - `--frozen` to leave the virtual environment and lockfile unchanged - `--quiet` to suppress output +- `--backend` to specify a package manager backend to use. The default is to auto-detect. +Possible values: + - `auto` to auto-detect the backend (default) + - `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. See [`usethis tool`](#usethis-tool) for more information. @@ -321,6 +343,11 @@ Supported options: - `--offline` to disable network access and rely on caches - `--frozen` to leave the virtual environment and lockfile unchanged - `--quiet` to suppress output +- `--backend` to specify a package manager backend to use. The default is to auto-detect. +Possible values: + - `auto` to auto-detect the backend (default) + - `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. For `usethis tool ruff`, in addition to the above options, you can also specify: @@ -340,6 +367,11 @@ Supported options: - `--remove` to remove the CI configuration instead of adding it - `--offline` to disable network access and rely on caches - `--quiet` to suppress output +- `--backend` to specify a package manager backend to use. The default is to auto-detect. +Possible values: + - `auto` to auto-detect the backend (default) + - `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. ### `usethis badge` @@ -366,8 +398,13 @@ Add a README.md file to the project. Supported options: -- `--quiet` to suppress output - `--badges` to also add badges to the README.md file +- `--quiet` to suppress output +- `--backend` to specify a package manager backend to use. The default is to auto-detect. +Possible values: + - `auto` to auto-detect the backend (default) + - `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. ### `usethis author` @@ -382,6 +419,11 @@ Other supported options: - `--email` to set the author email address - `--overwrite` to overwrite all existing author information - `--quiet` to suppress output +- `--backend` to specify a package manager backend to use. The default is to auto-detect. +Possible values: + - `auto` to auto-detect the backend (default) + - `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. ### `usethis docstyle