Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
72db384
Initial plan
Copilot Mar 27, 2026
cfcdb9a
Add poetry backend infrastructure and update all backend dispatch points
Copilot Mar 27, 2026
61b2211
Add poetry backend tests and update dispatch test
Copilot Mar 27, 2026
03ff9ad
Fix lint errors, type errors, and sync module tree docs
Copilot Mar 27, 2026
330a98c
Address code review: fix poetry init cwd and author param handling
Copilot Mar 27, 2026
e6e5ec6
Add poetry as test dependency and achieve 100% test coverage on poetr…
Copilot Mar 27, 2026
577c86c
Refactor: add BackendSubprocessFailedError and call_backend_subproces…
Copilot Mar 27, 2026
db2ae05
Merge branch 'main' into copilot/add-support-for-poetry-backend
nathanjmcdougall Mar 27, 2026
1682f3e
Merge branch 'main' into copilot/add-support-for-poetry-backend
nathanjmcdougall Mar 27, 2026
30ed70b
Fix static check failures: PLR0912 in use_requirements_txt, ty type e…
Copilot Mar 27, 2026
b209c45
Merge branch 'main' into copilot/add-support-for-poetry-backend
nathanjmcdougall Mar 28, 2026
4a17032
Fix basedpyright error: handle BackendEnum.poetry in requirements_txt…
Copilot Mar 28, 2026
33a50b8
Add poetry backend test coverage for all uncovered branches
Copilot Mar 29, 2026
8e32839
Address PR review: shared _prepare_pyproject_write, remove backend pa…
Copilot Mar 29, 2026
0382de0
Fix ruff lint errors: move imports to top level, fix SIM117 and EM101…
Copilot Mar 29, 2026
abc83d0
Merge branch 'main' into copilot/add-support-for-poetry-backend
nathanjmcdougall Mar 29, 2026
9e46c39
Fix static checks: add docstrings to poetry deps, fix import-linter v…
Copilot Mar 29, 2026
3824ea0
Merge branch 'main' into copilot/add-support-for-poetry-backend
nathanjmcdougall Mar 30, 2026
c94a28a
Address review: move --no-interaction, update docs, restructure tests…
Copilot Mar 30, 2026
a19e0af
Merge branch 'main' into copilot/add-support-for-poetry-backend
nathanjmcdougall Mar 30, 2026
9a8bb50
Fix static checks: unused imports, doc sync, basedpyright warnings
Copilot Mar 30, 2026
515278f
Merge branch 'main' into copilot/add-support-for-poetry-backend
nathanjmcdougall Apr 1, 2026
1596a69
Merge branch 'main' into copilot/add-support-for-poetry-backend
nathanjmcdougall Apr 1, 2026
7ada779
Run ruff format
nathanjmcdougall Apr 1, 2026
3d30db2
Merge branch 'main' into copilot/add-support-for-poetry-backend
nathanjmcdougall Apr 1, 2026
b915d51
Fix import ordering in test_init.py
Copilot Apr 1, 2026
95ce142
Fix ruff formatting in test_init.py
Copilot Apr 1, 2026
95247f5
Support Poetry's custom dependency specification format
Copilot Apr 1, 2026
5e4286d
docs: mention poetry.toml and [tool.poetry] as detection methods in b…
Copilot Apr 1, 2026
a271da7
Merge branch 'main' into copilot/add-support-for-poetry-backend
nathanjmcdougall Apr 1, 2026
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
22 changes: 19 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ usethis # usethis: Automatically manage Python tooling
├── _backend # Backend dispatch and tool-specific backend implementations.
│ ├── dispatch # Backend selection and dispatch logic.
│ ├── poetry # Poetry backend implementation.
│ │ └── detect # Detection of Poetry usage in a project.
│ │ ├── available # Check whether the Poetry CLI is available.
│ │ ├── call # Subprocess wrappers for invoking Poetry commands.
│ │ ├── deps # Dependency group operations via the Poetry backend.
│ │ ├── detect # Detection of Poetry usage in a project.
│ │ ├── errors # Error types for the Poetry backend.
│ │ └── init # Project initialization via Poetry.
│ └── uv # uv backend implementation.
│ ├── available # Check whether the uv CLI is available.
│ ├── call # Subprocess wrappers for invoking uv commands.
Expand Down Expand Up @@ -67,7 +72,8 @@ usethis # usethis: Automatically manage Python tooling
│ │ ├── project # Access the [project] section of pyproject.toml.
│ │ ├── remove # Removal of the pyproject.toml file.
│ │ ├── requires_python # Python version requirement queries from pyproject.toml.
│ │ └── valid # Validation and repair of pyproject.toml structure.
│ │ ├── valid # Validation and repair of pyproject.toml structure.
│ │ └── write # Preparation helpers for writing pyproject.toml via subprocesses.
│ ├── setup_cfg # setup.cfg file reading and writing.
│ │ ├── errors # Error types for setup.cfg operations.
│ │ └── io_ # setup.cfg file I/O manager.
Expand Down Expand Up @@ -210,7 +216,14 @@ ALWAYS check whether an existing function already covers your use case before im
<!-- sync:docs/functions.txt -->

- `get_backend()` (`usethis._backend.dispatch`) — Get the current package manager backend.
- `call_backend_subprocess()` (`usethis._backend.dispatch`) — Dispatch a subprocess call to the appropriate backend.
- `is_poetry_available()` (`usethis._backend.poetry.available`) — Check if the `poetry` command is available in the current environment.
- `call_poetry_subprocess()` (`usethis._backend.poetry.call`) — Run a subprocess using the Poetry command-line tool.
- `add_dep_to_group_via_poetry()` (`usethis._backend.poetry.deps`) — Add a dependency to the named group using Poetry.
- `remove_dep_from_group_via_poetry()` (`usethis._backend.poetry.deps`) — Remove a dependency from the named group using Poetry.
- `is_poetry_used()` (`usethis._backend.poetry.detect`) — Check if Poetry is being used in the project.
- `ensure_pyproject_toml_via_poetry()` (`usethis._backend.poetry.init`) — Create a pyproject.toml file using `poetry init`.
- `opinionated_poetry_init()` (`usethis._backend.poetry.init`) — Subprocess `poetry init` with opinionated arguments.
- `is_uv_available()` (`usethis._backend.uv.available`) — Check if the `uv` command is available in the current environment.
- `call_uv_subprocess()` (`usethis._backend.uv.call`) — Run a subprocess using the uv command-line tool.
- `add_default_groups_via_uv()` (`usethis._backend.uv.call`) — Add default groups using the uv command-line tool.
Expand Down Expand Up @@ -282,7 +295,7 @@ ALWAYS check whether an existing function already covers your use case before im
- `use_ty()` (`usethis._core.tool`) — Add and configure the ty type checker tool.
- `use_tool()` (`usethis._core.tool`) — General dispatch function to add or remove a tool to/from the project.
- `get_project_deps()` (`usethis._deps`) — Get all project dependencies.
- `get_dep_groups()` (`usethis._deps`) — Get all dependency groups from the dependency-groups section of pyproject.toml.
- `get_dep_groups()` (`usethis._deps`) — Get all dependency groups from pyproject.toml.
- `get_deps_from_group()` (`usethis._deps`) — Get the list of dependencies in a named dependency group.
- `register_default_group()` (`usethis._deps`) — Register a group in the default-groups configuration if it's not already there.
- `add_default_groups()` (`usethis._deps`) — Register the given dependency groups as default groups in the package manager configuration.
Expand All @@ -300,13 +313,16 @@ ALWAYS check whether an existing function already covers your use case before im
- `print_keys()` (`usethis._file.print_`) — Convert a list of keys to a string.
- `get_project_deps()` (`usethis._file.pyproject_toml.deps`) — Get all project dependencies from [project.dependencies].
- `get_dep_groups()` (`usethis._file.pyproject_toml.deps`) — Get all dependency groups from [dependency-groups].
- `get_poetry_project_deps()` (`usethis._file.pyproject_toml.deps`) — Get project dependencies from [tool.poetry.dependencies].
- `get_poetry_dep_groups()` (`usethis._file.pyproject_toml.deps`) — Get dependency groups from [tool.poetry.group.*.dependencies].
- `get_name()` (`usethis._file.pyproject_toml.name`) — Get the project name from pyproject.toml.
- `get_description()` (`usethis._file.pyproject_toml.name`) — Get the project description from pyproject.toml.
- `get_project_dict()` (`usethis._file.pyproject_toml.project`) — Get the contents of the [project] section from pyproject.toml.
- `remove_pyproject_toml()` (`usethis._file.pyproject_toml.remove`) — Remove the pyproject.toml file from the project.
- `get_requires_python()` (`usethis._file.pyproject_toml.requires_python`) — Get the requires-python constraint from pyproject.toml.
- `get_required_minor_python_versions()` (`usethis._file.pyproject_toml.requires_python`) — Get Python minor versions that match the project's requires-python constraint.
- `ensure_pyproject_validity()` (`usethis._file.pyproject_toml.valid`) — Ensure pyproject.toml has a valid structure, adding missing required fields.
- `prepare_pyproject_write()` (`usethis._file.pyproject_toml.write`) — Prepare the pyproject.toml file for a subprocess that will modify it.
- `edit_yaml()` (`usethis._file.yaml.io_`) — A context manager to modify a YAML file in-place, with managed read and write.
- `read_yaml()` (`usethis._file.yaml.io_`) — A context manager to read a YAML file.
- `update_ruamel_yaml_map()` (`usethis._file.yaml.update`) — Update the values of a ruamel.yaml map in-place using a diff-like algorithm.
Expand Down
10 changes: 9 additions & 1 deletion docs/about/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ The [`uv`](https://docs.astral.sh/uv) backend is recommended for new projects. W
- Generate and update lockfiles.
- Give instructions using `uv run` to run tools like `ruff`, `pytest`, etc.

### `poetry`

The [`poetry`](https://python-poetry.org/) backend supports Poetry-managed projects. When the `poetry` backend is active, usethis will:

- Install and uninstall dependencies automatically using `poetry add` and `poetry remove`.
- Configure default dependency groups via `[tool.poetry.group.GROUPNAME]` with the `optional` flag.
- Give instructions using `poetry run` to run tools like `ruff`, `pytest`, etc.

### `none`

The `none` backend means no package manager is being used by usethis. When this backend is active, usethis will still configure tools and update configuration files, but it will not install or uninstall any dependencies for you. Instead, the console output will display instructions for you to follow up on manually.
Expand All @@ -20,7 +28,7 @@ The `none` backend means no package manager is being used by usethis. When this

By default, usethis auto-detects the appropriate backend using the following logic:

1. If [Poetry](https://python-poetry.org/) usage is detected, usethis warns that Poetry is not fully supported, and falls back to `none`.
1. If [Poetry](https://python-poetry.org/) usage is detected (via the presence of a `poetry.lock` file, a `poetry.toml` file, or a `[tool.poetry]` section in `pyproject.toml`), the `poetry` backend is selected.
2. If `uv` usage is detected (e.g. via the presence of a `uv.lock` file), the `uv` backend is selected.
3. If no `pyproject.toml` exists yet and `uv` is available on your system, the `uv` backend is selected.
4. Otherwise, the `none` backend is used.
Expand Down
12 changes: 11 additions & 1 deletion docs/functions.txt
Comment thread
nathanjmcdougall marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
- `get_backend()` (`usethis._backend.dispatch`) — Get the current package manager backend.
- `call_backend_subprocess()` (`usethis._backend.dispatch`) — Dispatch a subprocess call to the appropriate backend.
- `is_poetry_available()` (`usethis._backend.poetry.available`) — Check if the `poetry` command is available in the current environment.
- `call_poetry_subprocess()` (`usethis._backend.poetry.call`) — Run a subprocess using the Poetry command-line tool.
- `add_dep_to_group_via_poetry()` (`usethis._backend.poetry.deps`) — Add a dependency to the named group using Poetry.
- `remove_dep_from_group_via_poetry()` (`usethis._backend.poetry.deps`) — Remove a dependency from the named group using Poetry.
- `is_poetry_used()` (`usethis._backend.poetry.detect`) — Check if Poetry is being used in the project.
- `ensure_pyproject_toml_via_poetry()` (`usethis._backend.poetry.init`) — Create a pyproject.toml file using `poetry init`.
- `opinionated_poetry_init()` (`usethis._backend.poetry.init`) — Subprocess `poetry init` with opinionated arguments.
- `is_uv_available()` (`usethis._backend.uv.available`) — Check if the `uv` command is available in the current environment.
- `call_uv_subprocess()` (`usethis._backend.uv.call`) — Run a subprocess using the uv command-line tool.
- `add_default_groups_via_uv()` (`usethis._backend.uv.call`) — Add default groups using the uv command-line tool.
Expand Down Expand Up @@ -71,7 +78,7 @@
- `use_ty()` (`usethis._core.tool`) — Add and configure the ty type checker tool.
- `use_tool()` (`usethis._core.tool`) — General dispatch function to add or remove a tool to/from the project.
- `get_project_deps()` (`usethis._deps`) — Get all project dependencies.
- `get_dep_groups()` (`usethis._deps`) — Get all dependency groups from the dependency-groups section of pyproject.toml.
- `get_dep_groups()` (`usethis._deps`) — Get all dependency groups from pyproject.toml.
- `get_deps_from_group()` (`usethis._deps`) — Get the list of dependencies in a named dependency group.
- `register_default_group()` (`usethis._deps`) — Register a group in the default-groups configuration if it's not already there.
- `add_default_groups()` (`usethis._deps`) — Register the given dependency groups as default groups in the package manager configuration.
Expand All @@ -89,13 +96,16 @@
- `print_keys()` (`usethis._file.print_`) — Convert a list of keys to a string.
- `get_project_deps()` (`usethis._file.pyproject_toml.deps`) — Get all project dependencies from [project.dependencies].
- `get_dep_groups()` (`usethis._file.pyproject_toml.deps`) — Get all dependency groups from [dependency-groups].
- `get_poetry_project_deps()` (`usethis._file.pyproject_toml.deps`) — Get project dependencies from [tool.poetry.dependencies].
- `get_poetry_dep_groups()` (`usethis._file.pyproject_toml.deps`) — Get dependency groups from [tool.poetry.group.*.dependencies].
- `get_name()` (`usethis._file.pyproject_toml.name`) — Get the project name from pyproject.toml.
- `get_description()` (`usethis._file.pyproject_toml.name`) — Get the project description from pyproject.toml.
- `get_project_dict()` (`usethis._file.pyproject_toml.project`) — Get the contents of the [project] section from pyproject.toml.
- `remove_pyproject_toml()` (`usethis._file.pyproject_toml.remove`) — Remove the pyproject.toml file from the project.
- `get_requires_python()` (`usethis._file.pyproject_toml.requires_python`) — Get the requires-python constraint from pyproject.toml.
- `get_required_minor_python_versions()` (`usethis._file.pyproject_toml.requires_python`) — Get Python minor versions that match the project's requires-python constraint.
- `ensure_pyproject_validity()` (`usethis._file.pyproject_toml.valid`) — Ensure pyproject.toml has a valid structure, adding missing required fields.
- `prepare_pyproject_write()` (`usethis._file.pyproject_toml.write`) — Prepare the pyproject.toml file for a subprocess that will modify it.
- `edit_yaml()` (`usethis._file.yaml.io_`) — A context manager to modify a YAML file in-place, with managed read and write.
- `read_yaml()` (`usethis._file.yaml.io_`) — A context manager to read a YAML file.
- `update_ruamel_yaml_map()` (`usethis._file.yaml.update`) — Update the values of a ruamel.yaml map in-place using a diff-like algorithm.
Expand Down
10 changes: 8 additions & 2 deletions docs/module-tree.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ usethis # usethis: Automatically manage Python tooling
├── _backend # Backend dispatch and tool-specific backend implementations.
│ ├── dispatch # Backend selection and dispatch logic.
│ ├── poetry # Poetry backend implementation.
│ │ └── detect # Detection of Poetry usage in a project.
│ │ ├── available # Check whether the Poetry CLI is available.
│ │ ├── call # Subprocess wrappers for invoking Poetry commands.
│ │ ├── deps # Dependency group operations via the Poetry backend.
│ │ ├── detect # Detection of Poetry usage in a project.
│ │ ├── errors # Error types for the Poetry backend.
│ │ └── init # Project initialization via Poetry.
│ └── uv # uv backend implementation.
│ ├── available # Check whether the uv CLI is available.
│ ├── call # Subprocess wrappers for invoking uv commands.
Expand Down Expand Up @@ -56,7 +61,8 @@ usethis # usethis: Automatically manage Python tooling
│ │ ├── project # Access the [project] section of pyproject.toml.
│ │ ├── remove # Removal of the pyproject.toml file.
│ │ ├── requires_python # Python version requirement queries from pyproject.toml.
│ │ └── valid # Validation and repair of pyproject.toml structure.
│ │ ├── valid # Validation and repair of pyproject.toml structure.
│ │ └── write # Preparation helpers for writing pyproject.toml via subprocesses.
│ ├── setup_cfg # setup.cfg file reading and writing.
│ │ ├── errors # Error types for setup.cfg operations.
│ │ └── io_ # setup.cfg file I/O manager.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ test = [
"click>=8.1.8",
"coverage[toml]>=7.7.1",
"gitpython>=3.1.44",
"poetry>=2.3.2",
"pytest>=8.3.5",
"pytest-codspeed>=3.2.0",
"pytest-cov>=6.0.0",
Expand Down
35 changes: 29 additions & 6 deletions src/usethis/_backend/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@

from typing import Literal

from typing_extensions import assert_never

from usethis._backend.poetry.call import call_poetry_subprocess
from usethis._backend.poetry.detect import is_poetry_used
from usethis._backend.uv.available import is_uv_available
from usethis._backend.uv.call import call_uv_subprocess
from usethis._backend.uv.detect import is_uv_used
from usethis._config import usethis_config
from usethis._console import warn_print
from usethis._types.backend import BackendEnum


def get_backend() -> Literal[BackendEnum.uv, BackendEnum.none]:
def get_backend() -> Literal[BackendEnum.uv, BackendEnum.poetry, BackendEnum.none]:
"""Get the current package manager backend."""
# Effectively we cache the inference, storing it in usethis_config.
if usethis_config.inferred_backend is not None:
Expand All @@ -21,10 +24,7 @@ def get_backend() -> Literal[BackendEnum.uv, BackendEnum.none]:
if usethis_config.backend is not BackendEnum.auto:
usethis_config.inferred_backend = usethis_config.backend
elif is_poetry_used():
warn_print(
"This project is using Poetry, which is not fully supported by usethis."
)
usethis_config.inferred_backend = BackendEnum.none
usethis_config.inferred_backend = BackendEnum.poetry
elif is_uv_used():
usethis_config.inferred_backend = BackendEnum.uv
elif not (usethis_config.cpd() / "pyproject.toml").exists() and is_uv_available():
Expand All @@ -34,3 +34,26 @@ def get_backend() -> Literal[BackendEnum.uv, BackendEnum.none]:
usethis_config.inferred_backend = BackendEnum.none

return usethis_config.inferred_backend


def call_backend_subprocess(
args: list[str],
*,
change_toml: bool,
backend: Literal[BackendEnum.uv, BackendEnum.poetry, BackendEnum.none],
) -> str:
"""Dispatch a subprocess call to the appropriate backend.

Raises:
BackendSubprocessFailedError: If the subprocess fails (via the
backend-specific subclass).
"""
if backend is BackendEnum.uv:
return call_uv_subprocess(args, change_toml=change_toml)
elif backend is BackendEnum.poetry:
return call_poetry_subprocess(args, change_toml=change_toml)
elif backend is BackendEnum.none:
msg = "Cannot call a backend subprocess when no backend is active."
raise ValueError(msg)
else:
assert_never(backend)
14 changes: 14 additions & 0 deletions src/usethis/_backend/poetry/available.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Check whether the Poetry CLI is available."""

from usethis._backend.poetry.call import call_poetry_subprocess
from usethis._backend.poetry.errors import PoetrySubprocessFailedError


def is_poetry_available() -> bool:
"""Check if the `poetry` command is available in the current environment."""
try:
call_poetry_subprocess(["--version"], change_toml=False)
except PoetrySubprocessFailedError:
return False

return True
49 changes: 49 additions & 0 deletions src/usethis/_backend/poetry/call.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Subprocess wrappers for invoking Poetry commands."""

from __future__ import annotations

from usethis._backend.poetry.errors import PoetrySubprocessFailedError
from usethis._config import usethis_config
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
from usethis._file.pyproject_toml.write import prepare_pyproject_write
from usethis._subprocess import SubprocessFailedError, call_subprocess
from usethis._types.backend import BackendEnum
from usethis.errors import ForbiddenBackendError


def call_poetry_subprocess(args: list[str], *, change_toml: bool) -> str:
"""Run a subprocess using the Poetry command-line tool.

Returns:
str: The output of the subprocess.

Raises:
PoetrySubprocessFailedError: If the subprocess fails.
ForbiddenBackendError: If the current backend is not poetry (or auto).
"""
if usethis_config.backend not in {BackendEnum.poetry, BackendEnum.auto}:
msg = f"The '{usethis_config.backend.value}' backend is enabled, but a Poetry subprocess was invoked."
raise ForbiddenBackendError(msg)

if change_toml:
prepare_pyproject_write()

new_args = ["poetry", "--no-interaction", *args]

if usethis_config.subprocess_verbose:
new_args = [*new_args[:1], "-vvv", *new_args[1:]]
elif args[:1] != ["--version"]:
new_args = [*new_args[:1], "--quiet", *new_args[1:]]

try:
output = call_subprocess(new_args, cwd=usethis_config.cpd())
except SubprocessFailedError as err:
raise PoetrySubprocessFailedError(err) from None
except FileNotFoundError:
msg = "Poetry is not installed or not found on PATH."
raise PoetrySubprocessFailedError(msg) from None

if change_toml and PyprojectTOMLManager().is_locked():
PyprojectTOMLManager().read_file()

return output
Loading
Loading