From b827c2a9b64f3c26dfd983043ac4d774e7c0bf1f Mon Sep 17 00:00:00 2001 From: Petter Strandmark Date: Mon, 29 Jun 2020 09:04:53 +0200 Subject: [PATCH] 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 == ""