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
2 changes: 2 additions & 0 deletions docs/repositories.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,8 @@ typically different to the same one provided by the repository for the simple AP
in the example of [Test PyPI](https://test.pypi.org/), both the host (`test.pypi.org`) as
well as the path (`/legacy`) are different to its simple API (`https://test.pypi.org/simple`).

While the examples show URLs with a trailing slash (e.g., `https://test.pypi.org/legacy/`), Poetry automatically normalizes legacy repository URLs by adding a trailing slash if one is missing. This means both `https://test.pypi.org/legacy` and `https://test.pypi.org/legacy/` will work correctly when publishing.

{{% /note %}}

## Configuring Credentials
Expand Down
7 changes: 7 additions & 0 deletions src/poetry/publishing/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
logger = logging.getLogger(__name__)


def _normalize_legacy_repository_url(url: str) -> str:
if url.endswith("/legacy"):
url += "/"
return url


class Publisher:
"""
Registers and publishes packages to remote repositories.
Expand Down Expand Up @@ -52,6 +58,7 @@ def publish(
url = self._poetry.config.get(f"repositories.{repository_name}.url")
if url is None:
raise RuntimeError(f"Repository {repository_name} is not defined")
url = _normalize_legacy_repository_url(url)

if not (username and password):
# Check if we have a token first
Expand Down
43 changes: 43 additions & 0 deletions tests/publishing/test_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,46 @@ def test_publish_read_from_environment_variable(
("https://foo.bar",),
{"cert": True, "client_cert": None, "dry_run": False, "skip_existing": False},
]


@pytest.mark.parametrize(
("configured_url", "expected_url"),
[
# Missing trailing slash — should be added
("https://test.pypi.org/legacy", "https://test.pypi.org/legacy/"),
# Already has trailing slash — should not double up
("https://test.pypi.org/legacy/", "https://test.pypi.org/legacy/"),
# Non-legacy URL — should be unchanged
("https://test.pypi.org/simple", "https://test.pypi.org/simple"),
],
)
def test_publish_normalizes_legacy_repository_url(
fixture_dir: FixtureDirGetter,
mocker: MockerFixture,
config: Config,
configured_url: str,
expected_url: str,
) -> None:
uploader_auth = mocker.patch("poetry.publishing.uploader.Uploader.auth")
uploader_upload = mocker.patch("poetry.publishing.uploader.Uploader.upload")

poetry = Factory().create_poetry(fixture_dir("sample_project"))
poetry._config = config
poetry.config.merge(
{
"repositories": {"testpypi": {"url": configured_url}},
"http-basic": {"testpypi": {"username": "foo", "password": "bar"}},
}
)

publisher = Publisher(poetry, NullIO())
publisher.publish("testpypi", None, None)

uploader_auth.assert_called_once_with("foo", "bar")
uploader_upload.assert_called_once_with(
expected_url,
cert=True,
client_cert=None,
dry_run=False,
skip_existing=False,
)
Loading