diff --git a/docs/repositories.md b/docs/repositories.md index 07bf2ef14d1..9c3b5c40054 100644 --- a/docs/repositories.md +++ b/docs/repositories.md @@ -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 diff --git a/src/poetry/publishing/publisher.py b/src/poetry/publishing/publisher.py index f752a0d5ee4..a31464355dc 100644 --- a/src/poetry/publishing/publisher.py +++ b/src/poetry/publishing/publisher.py @@ -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. @@ -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 diff --git a/tests/publishing/test_publisher.py b/tests/publishing/test_publisher.py index 53f30e75f78..b40c9048d07 100644 --- a/tests/publishing/test_publisher.py +++ b/tests/publishing/test_publisher.py @@ -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, + )