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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ usethis # usethis: Automate Python project setup and d
│ │ ├── errors # Error types for INI file operations.
│ │ └── io_ # INI file I/O manager.
│ ├── pyproject_toml # pyproject.toml file reading and writing.
│ │ ├── deps # Dependency extraction from pyproject.toml.
│ │ ├── errors # Error types for pyproject.toml operations.
│ │ ├── io_ # pyproject.toml file I/O manager.
│ │ ├── name # Project name and description extraction from pyproject.toml.
Expand Down
1 change: 1 addition & 0 deletions docs/module-tree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ usethis # usethis: Automate Python project setup and d
│ │ ├── errors # Error types for INI file operations.
│ │ └── io_ # INI file I/O manager.
│ ├── pyproject_toml # pyproject.toml file reading and writing.
│ │ ├── deps # Dependency extraction from pyproject.toml.
│ │ ├── errors # Error types for pyproject.toml operations.
│ │ ├── io_ # pyproject.toml file I/O manager.
│ │ ├── name # Project name and description extraction from pyproject.toml.
Expand Down
25 changes: 24 additions & 1 deletion src/usethis/_backend/uv/available.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
"""Check whether the uv CLI is available."""

from packaging.requirements import InvalidRequirement

from usethis._backend.uv.call import call_uv_subprocess
from usethis._backend.uv.errors import UVSubprocessFailedError
from usethis._file.pyproject_toml.deps import get_dep_groups, get_project_deps
from usethis._file.pyproject_toml.errors import PyprojectTOMLError
from usethis._types.deps import Dependency


def is_uv_available() -> bool:
"""Check if the `uv` command is available in the current environment."""
try:
call_uv_subprocess(["--version"], change_toml=False)
except UVSubprocessFailedError:
return False
return _is_uv_a_dep()

return True


def _is_uv_a_dep() -> bool:
"""Check if uv is declared as a project dependency or in a dependency group."""
uv_dep = Dependency(name="uv")

try:
project_deps = get_project_deps()
except (PyprojectTOMLError, InvalidRequirement):
project_deps = []

try:
dep_groups = get_dep_groups()
except (PyprojectTOMLError, InvalidRequirement):
dep_groups = {}

all_group_deps = [dep for group in dep_groups.values() for dep in group]
return any(dep.name == uv_dep.name for dep in project_deps + all_group_deps)
83 changes: 17 additions & 66 deletions src/usethis/_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@

from __future__ import annotations

from typing import Literal
from typing import TYPE_CHECKING, Literal

import pydantic
from packaging.requirements import Requirement
from pydantic import TypeAdapter
from typing_extensions import assert_never

from usethis._backend.dispatch import get_backend
Expand All @@ -19,11 +16,20 @@
from usethis._backend.uv.errors import UVDepGroupError
from usethis._config import usethis_config
from usethis._console import instruct_print, tick_print
from usethis._file.pyproject_toml.deps import (
get_dep_groups as _get_dep_groups,
)
from usethis._file.pyproject_toml.deps import (
get_project_deps as _get_project_deps,
)
from usethis._file.pyproject_toml.errors import PyprojectTOMLDepsError
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._types.backend import BackendEnum
from usethis._types.deps import Dependency
from usethis.errors import DepGroupError

if TYPE_CHECKING:
from usethis._types.deps import Dependency


def get_project_deps() -> list[Dependency]:
"""Get all project dependencies.
Expand All @@ -35,71 +41,16 @@ def get_project_deps() -> list[Dependency]:
of the `pyproject.toml` file.
"""
try:
pyproject = PyprojectTOMLManager().get()
except FileNotFoundError:
return []

try:
project_section = pyproject["project"]
except KeyError:
return []

if not isinstance(project_section, dict):
return []

try:
dep_section = project_section["dependencies"]
except KeyError:
return []

try:
req_strs = TypeAdapter(list[str]).validate_python(dep_section)
except pydantic.ValidationError as err:
msg = (
"Failed to parse the 'project.dependencies' section in 'pyproject.toml':\n"
f"{err}\n\n"
"Please check the section and try again."
)
raise UVDepGroupError(msg) from None

reqs = [Requirement(req_str) for req_str in req_strs]
deps = [Dependency(name=req.name, extras=frozenset(req.extras)) for req in reqs]
return deps
return _get_project_deps()
except PyprojectTOMLDepsError as err:
raise UVDepGroupError(str(err)) from None


def get_dep_groups() -> dict[str, list[Dependency]]:
try:
pyproject = PyprojectTOMLManager().get()
except FileNotFoundError:
return {}

try:
dep_groups_section = pyproject["dependency-groups"]
except KeyError:
# 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:
req_strs_by_group = TypeAdapter(dict[str, list[str]]).validate_python(
dep_groups_section
)
except pydantic.ValidationError as err:
msg = (
"Failed to parse the 'dependency-groups' section in 'pyproject.toml':\n"
f"{err}\n\n"
"Please check the section and try again."
)
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()
}
deps_by_group = {
group: [Dependency(name=req.name, extras=frozenset(req.extras)) for req in reqs]
for group, reqs in reqs_by_group.items()
}
return deps_by_group
return _get_dep_groups()
except PyprojectTOMLDepsError as err:
raise DepGroupError(str(err)) from None


def get_deps_from_group(group: str) -> list[Dependency]:
Expand Down
83 changes: 83 additions & 0 deletions src/usethis/_file/pyproject_toml/deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Dependency extraction from pyproject.toml."""

from __future__ import annotations

import pydantic
from packaging.requirements import Requirement
from pydantic import TypeAdapter

from usethis._file.pyproject_toml.errors import PyprojectTOMLDepsError
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._types.deps import Dependency


def get_project_deps() -> list[Dependency]:
"""Get all project dependencies from [project.dependencies].

This does not include development dependencies, e.g. not those in the
dependency-groups section, not extras/optional dependencies, not build dependencies.
"""
try:
pyproject = PyprojectTOMLManager().get()
except FileNotFoundError:
return []

try:
project_section = pyproject["project"]
except KeyError:
return []

if not isinstance(project_section, dict):
return []

try:
dep_section = project_section["dependencies"]
except KeyError:
return []

try:
req_strs = TypeAdapter(list[str]).validate_python(dep_section)
except pydantic.ValidationError as err:
msg = (
"Failed to parse the 'project.dependencies' section in 'pyproject.toml':\n"
f"{err}\n\n"
"Please check the section and try again."
)
raise PyprojectTOMLDepsError(msg) from None

reqs = [Requirement(req_str) for req_str in req_strs]
return [Dependency(name=req.name, extras=frozenset(req.extras)) for req in reqs]


def get_dep_groups() -> dict[str, list[Dependency]]:
"""Get all dependency groups from [dependency-groups]."""
try:
pyproject = PyprojectTOMLManager().get()
except FileNotFoundError:
return {}

try:
dep_groups_section = pyproject["dependency-groups"]
except KeyError:
return {}

try:
req_strs_by_group = TypeAdapter(dict[str, list[str]]).validate_python(
dep_groups_section
)
except pydantic.ValidationError as err:
msg = (
"Failed to parse the 'dependency-groups' section in 'pyproject.toml':\n"
f"{err}\n\n"
"Please check the section and try again."
)
raise PyprojectTOMLDepsError(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()
}
return {
group: [Dependency(name=req.name, extras=frozenset(req.extras)) for req in reqs]
for group, reqs in reqs_by_group.items()
}
4 changes: 4 additions & 0 deletions src/usethis/_file/pyproject_toml/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,7 @@ class PyprojectTOMLValueAlreadySetError(PyprojectTOMLError, TOMLValueAlreadySetE

class PyprojectTOMLValueMissingError(PyprojectTOMLError, TOMLValueMissingError):
"""Raised when a value is unexpectedly missing from the 'pyproject.toml' file."""


class PyprojectTOMLDepsError(PyprojectTOMLError):
"""Raised when dependency sections in 'pyproject.toml' cannot be parsed."""
Loading
Loading