From b827c2a9b64f3c26dfd983043ac4d774e7c0bf1f Mon Sep 17 00:00:00 2001
From: Petter Strandmark <petter.strandmark@gmail.com>
Date: Mon, 29 Jun 2020 09:04:53 +0200
Subject: [PATCH 1/2] installed repository: handle executable .pth files

Resolves: #2597
---
 poetry/repositories/installed_repository.py   | 52 ++++++++++++++-----
 .../METADATA                                  | 22 ++++++++
 .../site-packages/editable-with-import.pth    |  1 +
 .../repositories/test_installed_repository.py | 23 +++++---
 4 files changed, 79 insertions(+), 19 deletions(-)
 create mode 100644 tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-with-import-2.3.4.dist-info/METADATA
 create mode 100644 tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-with-import.pth

diff --git a/poetry/repositories/installed_repository.py b/poetry/repositories/installed_repository.py
index d25034e1602..27feacffad7 100644
--- a/poetry/repositories/installed_repository.py
+++ b/poetry/repositories/installed_repository.py
@@ -1,3 +1,5 @@
+from typing import Set
+
 from poetry.core.packages import Package
 from poetry.utils._compat import Path
 from poetry.utils._compat import metadata
@@ -10,6 +12,37 @@
 
 
 class InstalledRepository(Repository):
+    @classmethod
+    def get_package_paths(cls, sitedir, name):  # type: (Path, str) -> Set[Path]
+        """
+        Process a .pth file within the site-packages directory, and return any valid
+        paths. We skip executable .pth files as there is no reliable means to do this
+        without side-effects to current run-time. Mo check is made that the item refers
+        to a directory rather than a file, however, in order to maintain backwards
+        compatibility, we allow non-existing paths to be discovered. The latter
+        behaviour is different to how Python's site-specific hook configuration works.
+
+        Reference: https://docs.python.org/3.8/library/site.html
+
+        :param sitedir: The site-packages directory to search for .pth file.
+        :param name: The name of the package to search .pth file for.
+        :return: A `Set` of valid `Path` objects.
+        """
+        paths = set()
+
+        pth_file = sitedir.joinpath("{}.pth".format(name))
+        if pth_file.exists():
+            with pth_file.open() as f:
+                for line in f:
+                    line = line.strip()
+                    if line and not line.startswith(("#", "import ", "import\t")):
+                        path = Path(line)
+                        if not path.is_absolute():
+                            path = sitedir.joinpath(path)
+                        paths.add(path)
+
+        return paths
+
     @classmethod
     def load(cls, env):  # type: (Env) -> InstalledRepository
         """
@@ -49,19 +82,14 @@ def load(cls, env):  # type: (Env) -> InstalledRepository
                     is_standard_package = False
 
                 if is_standard_package:
-                    if (
-                        path.name.endswith(".dist-info")
-                        and env.site_packages.joinpath(
-                            "{}.pth".format(package.pretty_name)
-                        ).exists()
-                    ):
-                        with env.site_packages.joinpath(
-                            "{}.pth".format(package.pretty_name)
-                        ).open() as f:
-                            directory = Path(f.readline().strip())
+                    if path.name.endswith(".dist-info"):
+                        paths = cls.get_package_paths(
+                            sitedir=env.site_packages, name=package.pretty_name
+                        )
+                        if paths:
+                            # TODO: handle multiple source directories?
                             package.source_type = "directory"
-                            package.source_url = directory.as_posix()
-
+                            package.source_url = paths.pop().as_posix()
                     continue
 
                 src_path = env.path / "src"
diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-with-import-2.3.4.dist-info/METADATA b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-with-import-2.3.4.dist-info/METADATA
new file mode 100644
index 00000000000..7a9324ec11c
--- /dev/null
+++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-with-import-2.3.4.dist-info/METADATA
@@ -0,0 +1,22 @@
+Metadata-Version: 2.1
+Name: editable-with-import
+Version: 2.3.4
+Summary: Editable description.
+License: MIT
+Keywords: cli,commands
+Author: Foo Bar
+Author-email: foo@bar.com
+Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Description-Content-Type: text/x-rst
+
+Editable
+####
diff --git a/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-with-import.pth b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-with-import.pth
new file mode 100644
index 00000000000..21b405d8c2d
--- /dev/null
+++ b/tests/repositories/fixtures/installed/lib/python3.7/site-packages/editable-with-import.pth
@@ -0,0 +1 @@
+import os
diff --git a/tests/repositories/test_installed_repository.py b/tests/repositories/test_installed_repository.py
index 243c25d11ee..14ae6d01ba8 100644
--- a/tests/repositories/test_installed_repository.py
+++ b/tests/repositories/test_installed_repository.py
@@ -18,6 +18,7 @@
     ),
     metadata.PathDistribution(VENDOR_DIR / "attrs-19.3.0.dist-info"),
     metadata.PathDistribution(SITE_PACKAGES / "editable-2.3.4.dist-info"),
+    metadata.PathDistribution(SITE_PACKAGES / "editable-with-import-2.3.4.dist-info"),
 ]
 
 
@@ -46,7 +47,7 @@ def test_load(mocker):
     mocker.patch("poetry.repositories.installed_repository._VENDORS", str(VENDOR_DIR))
     repository = InstalledRepository.load(MockEnv(path=ENV_DIR))
 
-    assert len(repository.packages) == 4
+    assert len(repository.packages) == 5
 
     cleo = repository.packages[0]
     assert cleo.name == "cleo"
@@ -56,11 +57,11 @@ def test_load(mocker):
         == "Cleo allows you to create beautiful and testable command-line interfaces."
     )
 
-    foo = repository.packages[2]
+    foo = repository.packages[3]
     assert foo.name == "foo"
     assert foo.version.text == "0.1.0"
 
-    pendulum = repository.packages[3]
+    pendulum = repository.packages[4]
     assert pendulum.name == "pendulum"
     assert pendulum.version.text == "2.0.5"
     assert pendulum.description == "Python datetimes made easy"
@@ -71,8 +72,16 @@ def test_load(mocker):
     for pkg in repository.packages:
         assert pkg.name != "attrs"
 
+    # test editable package with text .pth file
     editable = repository.packages[1]
-    assert "editable" == editable.name
-    assert "2.3.4" == editable.version.text
-    assert "directory" == editable.source_type
-    assert "/path/to/editable" == editable.source_url
+    assert editable.name == "editable"
+    assert editable.version.text == "2.3.4"
+    assert editable.source_type == "directory"
+    assert editable.source_url == Path("/path/to/editable").as_posix()
+
+    # test editable package with executable .pth file
+    editable = repository.packages[2]
+    assert editable.name == "editable-with-import"
+    assert editable.version.text == "2.3.4"
+    assert editable.source_type == ""
+    assert editable.source_url == ""

From 02d6476f1a63169ce2bb90169b02da238ddfae33 Mon Sep 17 00:00:00 2001
From: Arun Babu Neelicattu <arun.neelicattu@gmail.com>
Date: Tue, 30 Jun 2020 23:50:15 +0200
Subject: [PATCH 2/2] installed repository: refactor test cases

---
 .../repositories/test_installed_repository.py | 38 ++++++++++++++++---
 1 file changed, 33 insertions(+), 5 deletions(-)

diff --git a/tests/repositories/test_installed_repository.py b/tests/repositories/test_installed_repository.py
index 14ae6d01ba8..20443882b84 100644
--- a/tests/repositories/test_installed_repository.py
+++ b/tests/repositories/test_installed_repository.py
@@ -1,8 +1,12 @@
+import pytest
+
 from poetry.repositories.installed_repository import InstalledRepository
+from poetry.utils._compat import PY36
 from poetry.utils._compat import Path
 from poetry.utils._compat import metadata
 from poetry.utils._compat import zipp
 from poetry.utils.env import MockEnv as BaseMockEnv
+from pytest_mock.plugin import MockFixture
 
 
 FIXTURES_DIR = Path(__file__).parent / "fixtures"
@@ -28,7 +32,13 @@ def site_packages(self):  # type: () -> Path
         return SITE_PACKAGES
 
 
-def test_load(mocker):
+@pytest.fixture
+def env():  # type: () -> MockEnv
+    return MockEnv(path=ENV_DIR)
+
+
+@pytest.fixture
+def repository(mocker, env):  # type: (MockFixture, MockEnv) -> InstalledRepository
     mocker.patch(
         "poetry.utils._compat.metadata.Distribution.discover",
         return_value=INSTALLED_RESULTS,
@@ -45,10 +55,19 @@ def test_load(mocker):
         ],
     )
     mocker.patch("poetry.repositories.installed_repository._VENDORS", str(VENDOR_DIR))
-    repository = InstalledRepository.load(MockEnv(path=ENV_DIR))
+    return InstalledRepository.load(env)
+
 
+def test_load_successful(repository):
     assert len(repository.packages) == 5
 
+
+def test_load_ensure_isolation(repository):
+    for pkg in repository.packages:
+        assert pkg.name != "attrs"
+
+
+def test_load_standard_package(repository):
     cleo = repository.packages[0]
     assert cleo.name == "cleo"
     assert cleo.version.text == "0.7.6"
@@ -61,6 +80,8 @@ def test_load(mocker):
     assert foo.name == "foo"
     assert foo.version.text == "0.1.0"
 
+
+def test_load_git_package(repository):
     pendulum = repository.packages[4]
     assert pendulum.name == "pendulum"
     assert pendulum.version.text == "2.0.5"
@@ -69,16 +90,23 @@ def test_load(mocker):
     assert pendulum.source_url == "https://github.com/sdispater/pendulum.git"
     assert pendulum.source_reference == "bb058f6b78b2d28ef5d9a5e759cfa179a1a713d6"
 
-    for pkg in repository.packages:
-        assert pkg.name != "attrs"
 
+@pytest.mark.skipif(
+    not PY36, reason="pathlib.resolve() does not support strict argument"
+)
+def test_load_editable_package(repository):
     # test editable package with text .pth file
     editable = repository.packages[1]
     assert editable.name == "editable"
     assert editable.version.text == "2.3.4"
     assert editable.source_type == "directory"
-    assert editable.source_url == Path("/path/to/editable").as_posix()
+    assert (
+        editable.source_url
+        == Path("/path/to/editable").resolve(strict=False).as_posix()
+    )
+
 
+def test_load_editable_with_import_package(repository):
     # test editable package with executable .pth file
     editable = repository.packages[2]
     assert editable.name == "editable-with-import"