diff --git a/src/poetry_plugin_export/exporter.py b/src/poetry_plugin_export/exporter.py index e0ffc84..a287fc0 100644 --- a/src/poetry_plugin_export/exporter.py +++ b/src/poetry_plugin_export/exporter.py @@ -143,7 +143,7 @@ def _export_generic_txt( and not is_direct_local_reference and package.source_url ): - indexes.add(package.source_url) + indexes.add(package.source_url.rstrip("/")) if package.files and self._with_hashes: hashes = [] @@ -171,22 +171,16 @@ def _export_generic_txt( if indexes and self._with_urls: # If we have extra indexes, we add them to the beginning of the output indexes_header = "" - for index in sorted(indexes): - repositories = [ - r - for r in self._poetry.pool.all_repositories - if isinstance(r, HTTPRepository) and r.url == index.rstrip("/") - ] - if not repositories: - continue - repository = repositories[0] - if repository is self._poetry.pool.repositories[0]: - url = ( - repository.authenticated_url - if self._with_credentials - else repository.url - ) - indexes_header += f"--index-url {url}\n" + has_pypi_repository = any( + r.name.lower() == "pypi" for r in self._poetry.pool.all_repositories + ) + # Iterate over repositories so that we get the repository with the highest + # priority first so that --index-url comes before --extra-index-url + for repository in self._poetry.pool.all_repositories: + if ( + not isinstance(repository, HTTPRepository) + or repository.url not in indexes + ): continue url = ( @@ -197,7 +191,13 @@ def _export_generic_txt( parsed_url = urllib.parse.urlsplit(url) if parsed_url.scheme == "http": indexes_header += f"--trusted-host {parsed_url.netloc}\n" - indexes_header += f"--extra-index-url {url}\n" + if ( + not has_pypi_repository + and repository is self._poetry.pool.repositories[0] + ): + indexes_header += f"--index-url {url}\n" + else: + indexes_header += f"--extra-index-url {url}\n" content = indexes_header + "\n" + content diff --git a/tests/test_exporter.py b/tests/test_exporter.py index 9eb8a07..9df8dfb 100644 --- a/tests/test_exporter.py +++ b/tests/test_exporter.py @@ -1878,8 +1878,8 @@ def test_exporter_exports_requirements_txt_with_two_primary_sources( content = f.read() expected = f"""\ ---extra-index-url https://foo:bar@a.example.com/simple --index-url https://baz:qux@b.example.com/simple +--extra-index-url https://foo:bar@a.example.com/simple bar==4.5.6 ; {MARKER_PY} \\ --hash=sha256:67890 @@ -2795,3 +2795,122 @@ def test_exporter_not_confused_by_extras_in_sub_dependencies( typer[all]==0.9.0 ; python_version >= "3.11" and python_version < "4.0" """ assert io.fetch_output() == expected + + +@pytest.mark.parametrize( + ("priorities", "expected"), + [ + ([("custom-a", Priority.PRIMARY), ("custom-b", Priority.PRIMARY)], ("a", "b")), + ([("custom-b", Priority.PRIMARY), ("custom-a", Priority.PRIMARY)], ("b", "a")), + ( + [("custom-b", Priority.SUPPLEMENTAL), ("custom-a", Priority.PRIMARY)], + ("a", "b"), + ), + ([("custom-b", Priority.EXPLICIT), ("custom-a", Priority.PRIMARY)], ("a", "b")), + ( + [ + ("PyPI", Priority.PRIMARY), + ("custom-a", Priority.PRIMARY), + ("custom-b", Priority.PRIMARY), + ], + ("", "a", "b"), + ), + ( + [ + ("PyPI", Priority.EXPLICIT), + ("custom-a", Priority.PRIMARY), + ("custom-b", Priority.PRIMARY), + ], + ("", "a", "b"), + ), + ( + [ + ("custom-a", Priority.PRIMARY), + ("custom-b", Priority.PRIMARY), + ("PyPI", Priority.SUPPLEMENTAL), + ], + ("", "a", "b"), + ), + ], +) +def test_exporter_index_urls( + tmp_path: Path, + poetry: Poetry, + priorities: list[tuple[str, Priority]], + expected: tuple[str, ...], +) -> None: + pypi = poetry.pool.repository("PyPI") + poetry.pool.remove_repository("PyPI") + for name, prio in priorities: + if name.lower() == "pypi": + repo = pypi + else: + repo = LegacyRepository(name, f"https://{name[-1]}.example.com/simple") + poetry.pool.add_repository(repo, priority=prio) + + poetry.locker.mock_lock_data( # type: ignore[attr-defined] + { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "optional": False, + "python-versions": "*", + "source": { + "type": "legacy", + "url": "https://a.example.com/simple", + "reference": "", + }, + }, + { + "name": "bar", + "version": "4.5.6", + "optional": False, + "python-versions": "*", + "source": { + "type": "legacy", + "url": "https://b.example.com/simple", + "reference": "", + }, + }, + ], + "metadata": { + "python-versions": "*", + "content-hash": "123456789", + "files": { + "foo": [{"name": "foo.whl", "hash": "12345"}], + "bar": [{"name": "bar.whl", "hash": "67890"}], + }, + }, + } + ) + set_package_requires(poetry, dev={"bar"}) + + exporter = Exporter(poetry, NullIO()) + exporter.only_groups([MAIN_GROUP, "dev"]) + exporter.export("requirements.txt", tmp_path, "requirements.txt") + + with (tmp_path / "requirements.txt").open(encoding="utf-8") as f: + content = f.read() + + expected_urls = [ + f"--extra-index-url https://{name[-1]}.example.com/simple" + for name in expected[1:] + ] + if expected[0]: + expected_urls = [ + f"--index-url https://{expected[0]}.example.com/simple", + *expected_urls, + ] + url_string = "\n".join(expected_urls) + + expected_content = f"""\ +{url_string} + +bar==4.5.6 ; {MARKER_PY} \\ + --hash=sha256:67890 +foo==1.2.3 ; {MARKER_PY} \\ + --hash=sha256:12345 +""" + + assert content == expected_content