Skip to content
Open
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
9 changes: 9 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ This also works for secret settings, like credentials:
export POETRY_HTTP_BASIC_MY_REPOSITORY_PASSWORD=secret
```

### Add caller data to the User-Agent

Set `POETRY_USER_AGENT_USER_DATA` to append caller-provided context to the
User-Agent on requests made by Poetry, including package downloads and uploads:

```bash
export POETRY_USER_AGENT_USER_DATA=build/42
```

## Configuration sources

When a setting is set in multiple places, Poetry applies the following precedence
Expand Down
6 changes: 2 additions & 4 deletions src/poetry/publishing/uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@
from packaging.metadata import parse_email
from poetry.core.constraints.version import Version
from poetry.core.masonry.utils.helpers import distribution_name
from requests_toolbelt import user_agent
from requests_toolbelt.multipart import MultipartEncoder
from requests_toolbelt.multipart import MultipartEncoderMonitor

from poetry.__version__ import __version__
from poetry.publishing.hash_manager import HashManager
from poetry.utils.constants import REQUESTS_TIMEOUT
from poetry.utils.patterns import wheel_file_re
from poetry.utils.user_agent import get_user_agent


if TYPE_CHECKING:
Expand All @@ -48,8 +47,7 @@ def __init__(self, poetry: Poetry, io: IO, dist_dir: Path | None = None) -> None

@property
def user_agent(self) -> str:
agent: str = user_agent("poetry", __version__)
return agent
return get_user_agent()

@property
def default_dist_dir(self) -> Path:
Expand Down
5 changes: 2 additions & 3 deletions src/poetry/utils/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@

from cachecontrol import CacheControlAdapter
from cachecontrol.caches import SeparateBodyFileCache
from requests_toolbelt import user_agent

from poetry.__version__ import __version__
from poetry.config.config import Config
from poetry.console.exceptions import ConsoleMessage
from poetry.console.exceptions import PoetryRuntimeError
Expand All @@ -30,6 +28,7 @@
from poetry.utils.constants import STATUS_FORCELIST
from poetry.utils.password_manager import HTTPAuthCredential
from poetry.utils.password_manager import PasswordManager
from poetry.utils.user_agent import get_user_agent


if TYPE_CHECKING:
Expand Down Expand Up @@ -133,7 +132,7 @@ def __init__(
self._get_repository_config_for_url
)
self._pool_size = pool_size
self._user_agent = user_agent("poetry", __version__)
self._user_agent = get_user_agent()

def create_session(self) -> requests.Session:
session = requests.Session()
Expand Down
17 changes: 17 additions & 0 deletions src/poetry/utils/user_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from __future__ import annotations

import os

from requests_toolbelt import user_agent as requests_user_agent

from poetry.__version__ import __version__


def get_user_agent() -> str:
"""Build Poetry's User-Agent, including optional caller context."""
extras: list[tuple[str, str]] = []
user_data = os.environ.get("POETRY_USER_AGENT_USER_DATA")
if user_data is not None:
extras.append(("user_data", user_data))
Comment on lines +12 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): requests_toolbelt.user_agent expects extras to contain strings such as "user_data/build/42", but this code appends a tuple. When POETRY_USER_AGENT_USER_DATA is set, User-Agent construction raises TypeError instead of returning a header, breaking repository requests and uploads.

Triggers: When POETRY_USER_AGENT_USER_DATA is set.

Suggested fix: Append f"user_data/{user_data}" as a string rather than the tuple ("user_data", user_data).

Suggested change
extras: list[tuple[str, str]] = []
user_data = os.environ.get("POETRY_USER_AGENT_USER_DATA")
if user_data is not None:
extras.append(("user_data", user_data))
extras: list[str] = []
user_data = os.environ.get("POETRY_USER_AGENT_USER_DATA")
if user_data is not None:
extras.append(f"user_data/{user_data}")


return str(requests_user_agent("poetry", __version__, extras=extras))
9 changes: 9 additions & 0 deletions tests/publishing/test_uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import responses

from pytest import MonkeyPatch
from pytest_mock import MockerFixture

from poetry.poetry import Poetry
Expand All @@ -34,6 +35,14 @@ def uploader(poetry: Poetry) -> Uploader:
return Uploader(poetry, NullIO())


def test_user_agent_includes_user_data(
uploader: Uploader, monkeypatch: MonkeyPatch
) -> None:
monkeypatch.setenv("POETRY_USER_AGENT_USER_DATA", "build/42")

assert "user_data/build/42" in uploader.user_agent


@pytest.mark.parametrize(
("files", "expected_files", "expected_version"),
[
Expand Down
8 changes: 8 additions & 0 deletions tests/utils/test_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ def test_authenticator_ignores_failing_keyring(
assert spy_get_credential.call_count == spy_get_password.call_count == 0


def test_authenticator_user_agent_includes_user_data(monkeypatch: MonkeyPatch) -> None:
monkeypatch.setenv("POETRY_USER_AGENT_USER_DATA", "build/42")

authenticator = Authenticator(disable_cache=True)

assert "user_data/build/42" in authenticator.create_session().headers["User-Agent"]


def test_authenticator_uses_password_only_credentials(
mock_config: Config, mock_remote: None, http: responses.RequestsMock
) -> None:
Expand Down