Repository: python-poetry/poetry

Issue:
poetry export adds extra url multiple times
<!--
  Hi there! Thank you for discovering and submitting an issue.

  Before you submit this; let's make sure of a few things.
  Please make sure the following boxes are ticked if they are correct.
  If not, please try and fulfill these first.
-->

<!-- Checked checkbox should look like this: [x] -->
- [x] I am on the [latest](https://github.com/sdispater/poetry/releases/latest) Poetry version.
- [x] I have searched the [issues](https://github.com/sdispater/poetry/issues) of this repo and believe that this is not a duplicate.
- [x] If an exception occurs when executing a command, I executed it again in debug mode (`-vvv` option).

<!--
  Once those are done, if you're able to fill in the following list with your information,
  it'd be very helpful to whoever handles the issue.
-->

- **OS version and name**: macOS Mojave 10.14.6
- **Poetry version**: 1.0.0b1
- **Link of a [Gist](https://gist.github.com/) with the contents of your pyproject.toml file**: I can't give this out as it's not open source, but I have a source in `[[tool.poetry.source]]` which is an internal pypi repo.

## Issue
If I run `poetry export -f requirements.txt` the result include the same `--extra-index-url` entry with the same URL at the top of the output once for each one of the packages in my poetry.lock. I suspect this is because poetry.lock (rightly) includes the same `[package.source]` entry for each dependency as they were downloaded from the source in `pyproject.toml` `[[tool.poetry.source]]`.

An interesting extra bit of information is that if I add `default = true` to the `[[tool.poetry.source]]` I only get one `--index-url` entry.

I'm also seeing some odd inconsistencies with the poetry.lock. When I deleted it and recreated it with the source set to default I got a poetry.lock with no (or at least fewer) sources. Running poetry update seemed to put them back. I'm now not able to reproduce this issue, though, so I'm not sure what exactly caused it.


Discussion:

Resolving patch:
diff --git a/poetry/utils/exporter.py b/poetry/utils/exporter.py
--- a/poetry/utils/exporter.py
+++ b/poetry/utils/exporter.py
@@ -54,7 +54,7 @@ def _export_requirements_txt(
         extras=None,
         with_credentials=False,
     ):  # type: (Path, Union[IO, str], bool, bool, bool) -> None
-        indexes = []
+        indexes = set()
         content = ""
         packages = self._poetry.locker.locked_repository(dev).packages
 
@@ -108,7 +108,7 @@ def _export_requirements_txt(
                 package.source_type not in {"git", "directory", "file", "url"}
                 and package.source_url
             ):
-                indexes.append(package.source_url)
+                indexes.add(package.source_url)
 
             if package.files and with_hashes:
                 hashes = []
@@ -134,10 +134,9 @@ def _export_requirements_txt(
             content += line
 
         if indexes:
-            # If we have extra indexes, we add them to the begin
-            # of the output
+            # If we have extra indexes, we add them to the beginning of the output
             indexes_header = ""
-            for index in indexes:
+            for index in sorted(indexes):
                 repository = [
                     r
                     for r in self._poetry.pool.repositories


Test patch:
diff --git a/tests/utils/test_exporter.py b/tests/utils/test_exporter.py
--- a/tests/utils/test_exporter.py
+++ b/tests/utils/test_exporter.py
@@ -707,6 +707,86 @@ def test_exporter_exports_requirements_txt_with_legacy_packages(tmp_dir, poetry)
     assert expected == content
 
 
+def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_sources(
+    tmp_dir, poetry
+):
+    poetry.pool.add_repository(
+        LegacyRepository(
+            "custom",
+            "https://example.com/simple",
+            auth=Auth("https://example.com/simple", "foo", "bar"),
+        )
+    )
+    poetry.pool.add_repository(LegacyRepository("custom", "https://foobaz.com/simple",))
+    poetry.locker.mock_lock_data(
+        {
+            "package": [
+                {
+                    "name": "foo",
+                    "version": "1.2.3",
+                    "category": "main",
+                    "optional": False,
+                    "python-versions": "*",
+                    "source": {
+                        "type": "legacy",
+                        "url": "https://example.com/simple",
+                        "reference": "",
+                    },
+                },
+                {
+                    "name": "bar",
+                    "version": "4.5.6",
+                    "category": "dev",
+                    "optional": False,
+                    "python-versions": "*",
+                    "source": {
+                        "type": "legacy",
+                        "url": "https://example.com/simple",
+                        "reference": "",
+                    },
+                },
+                {
+                    "name": "baz",
+                    "version": "7.8.9",
+                    "category": "dev",
+                    "optional": False,
+                    "python-versions": "*",
+                    "source": {
+                        "type": "legacy",
+                        "url": "https://foobaz.com/simple",
+                        "reference": "",
+                    },
+                },
+            ],
+            "metadata": {
+                "python-versions": "*",
+                "content-hash": "123456789",
+                "hashes": {"foo": ["12345"], "bar": ["67890"], "baz": ["24680"]},
+            },
+        }
+    )
+    exporter = Exporter(poetry)
+
+    exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True)
+
+    with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
+        content = f.read()
+
+    expected = """\
+--extra-index-url https://example.com/simple
+--extra-index-url https://foobaz.com/simple
+
+bar==4.5.6 \\
+    --hash=sha256:67890
+baz==7.8.9 \\
+    --hash=sha256:24680
+foo==1.2.3 \\
+    --hash=sha256:12345
+"""
+
+    assert expected == content
+
+
 def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials(
     tmp_dir, poetry, config
 ):