Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pep610: handle pure/plat lib differences cleanly #3891

Merged
merged 1 commit into from
Apr 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 25 additions & 20 deletions poetry/installation/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,16 +714,26 @@ def _download_archive(self, operation: Union[Install, Update], link: Link) -> Pa
def _should_write_operation(self, operation: Operation) -> bool:
return not operation.skipped or self._dry_run or self._verbose

@staticmethod
def _package_dist_info_path(package: "Package") -> Path:
from poetry.core.masonry.utils.helpers import escape_name
from poetry.core.masonry.utils.helpers import escape_version

return Path(
f"{escape_name(package.pretty_name)}-{escape_version(package.version.text)}.dist-info"
)

@classmethod
def _direct_url_json_path(cls, package: "Package") -> Path:
return cls._package_dist_info_path(package) / "direct_url.json"

def _save_url_reference(self, operation: "OperationTypes") -> None:
"""
Create and store a PEP-610 `direct_url.json` file, if needed.
"""
if operation.job_type not in {"install", "update"}:
return

from poetry.core.masonry.utils.helpers import escape_name
from poetry.core.masonry.utils.helpers import escape_version

package = operation.package

if not package.source_url:
Expand All @@ -732,14 +742,10 @@ def _save_url_reference(self, operation: "OperationTypes") -> None:
# distribution.
# That's not what we want so we remove the direct_url.json file,
# if it exists.
dist_info = self._env.site_packages.path.joinpath(
"{}-{}.dist-info".format(
escape_name(package.pretty_name),
escape_version(package.version.text),
)
)
if dist_info.exists() and dist_info.joinpath("direct_url.json").exists():
dist_info.joinpath("direct_url.json").unlink()
for direct_url in self._env.site_packages.find(
self._direct_url_json_path(package), True
):
direct_url.unlink()

return

Expand All @@ -755,16 +761,15 @@ def _save_url_reference(self, operation: "OperationTypes") -> None:
url_reference = self._create_file_url_reference(package)

if url_reference:
dist_info = self._env.site_packages.path.joinpath(
"{}-{}.dist-info".format(
escape_name(package.name), escape_version(package.version.text)
)
)

if dist_info.exists():
dist_info.joinpath("direct_url.json").write_text(
json.dumps(url_reference), encoding="utf-8"
for path in self._env.site_packages.find(
self._package_dist_info_path(package), writable_only=True
):
self._env.site_packages.write_text(
path / "direct_url.json",
json.dumps(url_reference),
encoding="utf-8",
)
break

def _create_git_url_reference(
self, package: "Package"
Expand Down
5 changes: 1 addition & 4 deletions poetry/installation/pip_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,7 @@ def remove(self, package: "Package") -> None:
raise

# This is a workaround for https://github.com/pypa/pip/issues/4176
nspkg_pth_file = self._env.site_packages.path / "{}-nspkg.pth".format(
package.name
)
if nspkg_pth_file.exists():
for nspkg_pth_file in self._env.site_packages.find(f"{package.name}-nspkg.pth"):
nspkg_pth_file.unlink()

# If we have a VCS package, remove its source directory
Expand Down
52 changes: 38 additions & 14 deletions poetry/utils/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,17 +152,34 @@ def _version_nodot(version):

class SitePackages:
def __init__(
self, path: Path, fallbacks: List[Path] = None, skip_write_checks: bool = False
self,
purelib: Path,
platlib: Optional[Path] = None,
fallbacks: List[Path] = None,
skip_write_checks: bool = False,
) -> None:
self._path = path
self._purelib = purelib
self._platlib = platlib or purelib

if platlib and platlib.resolve() == purelib.resolve():
self._platlib = purelib

self._fallbacks = fallbacks or []
self._skip_write_checks = skip_write_checks
self._candidates = [self._path] + self._fallbacks
self._candidates = list({self._purelib, self._platlib}) + self._fallbacks
self._writable_candidates = None if not skip_write_checks else self._candidates

@property
def path(self) -> Path:
return self._path
return self._purelib

@property
def purelib(self) -> Path:
return self._purelib

@property
def platlib(self) -> Path:
return self._platlib

@property
def candidates(self) -> List[Path]:
Expand Down Expand Up @@ -200,12 +217,16 @@ def make_candidates(self, path: Path, writable_only: bool = False) -> List[Path]
return [candidate / path for candidate in candidates if candidate]

def _path_method_wrapper(
self, path: Path, method: str, *args: Any, **kwargs: Any
self,
path: Union[str, Path],
method: str,
*args: Any,
return_first: bool = True,
writable_only: bool = False,
**kwargs: Any,
) -> Union[Tuple[Path, Any], List[Tuple[Path, Any]]]:

# TODO: Move to parameters after dropping Python 2.7
return_first = kwargs.pop("return_first", True)
writable_only = kwargs.pop("writable_only", False)
if isinstance(path, str):
path = Path(path)

candidates = self.make_candidates(path, writable_only=writable_only)

Expand Down Expand Up @@ -234,19 +255,19 @@ def _path_method_wrapper(

raise OSError("Unable to access any of {}".format(paths_csv(candidates)))

def write_text(self, path: Path, *args: Any, **kwargs: Any) -> Path:
def write_text(self, path: Union[str, Path], *args: Any, **kwargs: Any) -> Path:
return self._path_method_wrapper(path, "write_text", *args, **kwargs)[0]

def mkdir(self, path: Path, *args: Any, **kwargs: Any) -> Path:
def mkdir(self, path: Union[str, Path], *args: Any, **kwargs: Any) -> Path:
return self._path_method_wrapper(path, "mkdir", *args, **kwargs)[0]

def exists(self, path: Path) -> bool:
def exists(self, path: Union[str, Path]) -> bool:
return any(
value[-1]
for value in self._path_method_wrapper(path, "exists", return_first=False)
)

def find(self, path: Path, writable_only: bool = False) -> List[Path]:
def find(self, path: Union[str, Path], writable_only: bool = False) -> List[Path]:
return [
value[0]
for value in self._path_method_wrapper(
Expand Down Expand Up @@ -990,7 +1011,10 @@ def site_packages(self) -> SitePackages:
# we disable write checks if no user site exist
fallbacks = [self.usersite] if self.usersite else []
self._site_packages = SitePackages(
self.purelib, fallbacks, skip_write_checks=False if fallbacks else True
self.purelib,
self.platlib,
fallbacks,
skip_write_checks=False if fallbacks else True,
)
return self._site_packages

Expand Down
25 changes: 15 additions & 10 deletions tests/installation/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,9 +280,10 @@ def test_executor_should_write_pep610_url_references_for_files(
executor = Executor(tmp_venv, pool, config, io)
executor.execute([Install(package)])

dist_info = tmp_venv.site_packages.path.joinpath("demo-0.1.0.dist-info")
assert dist_info.exists()
dist_info = "demo-0.1.0.dist-info"
assert tmp_venv.site_packages.exists(dist_info)

dist_info = tmp_venv.site_packages.find(dist_info)[0]
direct_url_file = dist_info.joinpath("direct_url.json")

assert direct_url_file.exists()
Expand All @@ -303,9 +304,10 @@ def test_executor_should_write_pep610_url_references_for_directories(
executor = Executor(tmp_venv, pool, config, io)
executor.execute([Install(package)])

dist_info = tmp_venv.site_packages.path.joinpath("simple_project-1.2.3.dist-info")
assert dist_info.exists()
dist_info = "simple_project-1.2.3.dist-info"
assert tmp_venv.site_packages.exists(dist_info)

dist_info = tmp_venv.site_packages.find(dist_info)[0]
direct_url_file = dist_info.joinpath("direct_url.json")

assert direct_url_file.exists()
Expand All @@ -330,9 +332,10 @@ def test_executor_should_write_pep610_url_references_for_editable_directories(
executor = Executor(tmp_venv, pool, config, io)
executor.execute([Install(package)])

dist_info = tmp_venv.site_packages.path.joinpath("simple_project-1.2.3.dist-info")
assert dist_info.exists()
dist_info_dir = "simple_project-1.2.3.dist-info"
assert tmp_venv.site_packages.exists(dist_info_dir)

dist_info = tmp_venv.site_packages.find(dist_info_dir)[0]
direct_url_file = dist_info.joinpath("direct_url.json")

assert direct_url_file.exists()
Expand All @@ -355,9 +358,10 @@ def test_executor_should_write_pep610_url_references_for_urls(
executor = Executor(tmp_venv, pool, config, io)
executor.execute([Install(package)])

dist_info = tmp_venv.site_packages.path.joinpath("demo-0.1.0.dist-info")
assert dist_info.exists()
dist_info = "demo-0.1.0.dist-info"
assert tmp_venv.site_packages.exists(dist_info)

dist_info = tmp_venv.site_packages.find(dist_info)[0]
direct_url_file = dist_info.joinpath("direct_url.json")

assert direct_url_file.exists()
Expand Down Expand Up @@ -385,9 +389,10 @@ def test_executor_should_write_pep610_url_references_for_git(
executor = Executor(tmp_venv, pool, config, io)
executor.execute([Install(package)])

dist_info = tmp_venv.site_packages.path.joinpath("demo-0.1.2.dist-info")
assert dist_info.exists()
dist_info = "demo-0.1.2.dist-info"
assert tmp_venv.site_packages.exists(dist_info)

dist_info = tmp_venv.site_packages.find(dist_info)[0]
direct_url_file = dist_info.joinpath("direct_url.json")

assert direct_url_file.exists()
Expand Down
11 changes: 4 additions & 7 deletions tests/installation/test_pip_installer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
import shutil

from pathlib import Path
Expand Down Expand Up @@ -190,11 +191,6 @@ def test_uninstall_git_package_nspkg_pth_cleanup(mocker, tmp_venv, pool):
source_reference="master",
)

# we do this here because the virtual env might not be usable if failure case is triggered
pth_file_candidate = tmp_venv.site_packages.path / "{}-nspkg.pth".format(
package.name
)

# in order to reproduce the scenario where the git source is removed prior to proper
# clean up of nspkg.pth file, we need to make sure the fixture is copied and not
# symlinked into the git src directory
Expand All @@ -213,8 +209,9 @@ def copy_only(source, dest):
installer.install(package)
installer.remove(package)

assert not Path(pth_file_candidate).exists()
pth_file = f"{package.name}-nspkg.pth"
assert not tmp_venv.site_packages.exists(pth_file)

# any command in the virtual environment should trigger the error message
output = tmp_venv.run("python", "-m", "site")
assert "Error processing line 1 of {}".format(pth_file_candidate) not in output
assert not re.match(rf"Error processing line 1 of .*{pth_file}", output)
24 changes: 15 additions & 9 deletions tests/masonry/builders/test_editable_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,18 @@ def test_builder_installs_proper_files_for_standard_packages(simple_poetry, tmp_
builder.build()

assert tmp_venv._bin_dir.joinpath("foo").exists()
assert tmp_venv.site_packages.path.joinpath("simple_project.pth").exists()
pth_file = "simple_project.pth"
assert tmp_venv.site_packages.exists(pth_file)
assert (
simple_poetry.file.parent.resolve().as_posix()
== tmp_venv.site_packages.path.joinpath("simple_project.pth")
.read_text()
.strip(os.linesep)
== tmp_venv.site_packages.find(pth_file)[0].read_text().strip(os.linesep)
)

dist_info = tmp_venv.site_packages.path.joinpath("simple_project-1.2.3.dist-info")
assert dist_info.exists()
dist_info = "simple_project-1.2.3.dist-info"
assert tmp_venv.site_packages.exists(dist_info)

dist_info = tmp_venv.site_packages.find(dist_info)[0]

assert dist_info.joinpath("INSTALLER").exists()
assert dist_info.joinpath("METADATA").exists()
assert dist_info.joinpath("RECORD").exists()
Expand Down Expand Up @@ -134,7 +136,9 @@ def test_builder_installs_proper_files_for_standard_packages(simple_poetry, tmp_
assert metadata == dist_info.joinpath("METADATA").read_text(encoding="utf-8")

records = dist_info.joinpath("RECORD").read_text()
assert str(tmp_venv.site_packages.path.joinpath("simple_project.pth")) in records
pth_file = "simple_project.pth"
assert tmp_venv.site_packages.exists(pth_file)
assert str(tmp_venv.site_packages.find(pth_file)[0]) in records
assert str(tmp_venv._bin_dir.joinpath("foo")) in records
assert str(tmp_venv._bin_dir.joinpath("baz")) in records
assert str(dist_info.joinpath("METADATA")) in records
Expand Down Expand Up @@ -201,8 +205,10 @@ def test_builder_installs_proper_files_when_packages_configured(
builder = EditableBuilder(project_with_include, tmp_venv, NullIO())
builder.build()

pth_file = tmp_venv.site_packages.path.joinpath("with_include.pth")
assert pth_file.is_file()
pth_file = "with_include.pth"
assert tmp_venv.site_packages.exists(pth_file)

pth_file = tmp_venv.site_packages.find(pth_file)[0]

paths = set()
with pth_file.open() as f:
Expand Down