Skip to content

Commit

Permalink
site: use importlib instead of adhoc file searches
Browse files Browse the repository at this point in the history
This change replaces various cases in which installed distribution and
file look-ups used path searches with importlib.metadata backed
look-ups.

This fixes issues with `dist-info` lookup for PEP610 implementation as
well as `.pth` file look-up and `dist-info` removal.
  • Loading branch information
abn committed Apr 9, 2021
1 parent 00309be commit d4c9263
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 120 deletions.
30 changes: 8 additions & 22 deletions poetry/installation/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -714,19 +714,6 @@ 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.
Expand All @@ -742,11 +729,12 @@ 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.
for direct_url in self._env.site_packages.find(
self._direct_url_json_path(package), True
for (
direct_url_json
) in self._env.site_packages.find_distribution_direct_url_json_files(
distribution_name=package.name, writable_only=True
):
direct_url.unlink()

direct_url_json.unlink(missing_ok=True)
return

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

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

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

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

# If we have a VCS package, remove its source directory
Expand Down
34 changes: 20 additions & 14 deletions poetry/masonry/builders/editable.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ def build(self) -> None:

self._run_build_script(self._package.build_script)

for removed in self._env.site_packages.remove_distribution_files(
distribution_name=self._package.name
):
self._debug(
" - Removed <c2>{}</c2> directory from <b>{}</b>".format(
removed.name, removed.parent
)
)

added_files = []
added_files += self._add_pth()
added_files += self._add_scripts()
Expand Down Expand Up @@ -115,6 +124,16 @@ def _add_pth(self) -> List[Path]:
content += decode(path + os.linesep)

pth_file = Path(self._module.name).with_suffix(".pth")

# remove any pre-existing pth files for this package
for file in self._env.site_packages.find(path=pth_file, writable_only=True):
self._debug(
" - Removing existing <c2>{}</c2> from <b>{}</b> for {}".format(
file.name, file.parent, self._poetry.file.parent
)
)
file.unlink(missing_ok=True)

try:
pth_file = self._env.site_packages.write_text(
pth_file, content, encoding="utf-8"
Expand Down Expand Up @@ -199,20 +218,7 @@ def _add_dist_info(self, added_files: List[Path]) -> None:
added_files = added_files[:]

builder = WheelBuilder(self._poetry)

dist_info_path = Path(builder.dist_info)
for dist_info in self._env.site_packages.find(
dist_info_path, writable_only=True
):
if dist_info.exists():
self._debug(
" - Removing existing <c2>{}</c2> directory from <b>{}</b>".format(
dist_info.name, dist_info.parent
)
)
shutil.rmtree(str(dist_info))

dist_info = self._env.site_packages.mkdir(dist_info_path)
dist_info = self._env.site_packages.mkdir(Path(builder.dist_info))

self._debug(
" - Adding the <c2>{}</c2> directory to <b>{}</b>".format(
Expand Down
120 changes: 106 additions & 14 deletions poetry/utils/env.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import base64
import hashlib
import itertools
import json
import os
import platform
Expand All @@ -12,11 +13,13 @@

from contextlib import contextmanager
from copy import deepcopy
from importlib.metadata import PathDistribution
from pathlib import Path
from subprocess import CalledProcessError
from typing import Any
from typing import ContextManager
from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
Expand Down Expand Up @@ -166,7 +169,12 @@ def __init__(

self._fallbacks = fallbacks or []
self._skip_write_checks = skip_write_checks
self._candidates = list({self._purelib, self._platlib}) + self._fallbacks

self._candidates: List[Path] = []
for path in itertools.chain([self._purelib, self._platlib], self._fallbacks):
if path not in self._candidates:
self._candidates.append(path)

self._writable_candidates = None if not skip_write_checks else self._candidates

@property
Expand Down Expand Up @@ -198,7 +206,9 @@ def writable_candidates(self) -> List[Path]:

return self._writable_candidates

def make_candidates(self, path: Path, writable_only: bool = False) -> List[Path]:
def make_candidates(
self, path: Path, writable_only: bool = False, strict: bool = False
) -> List[Path]:
candidates = self._candidates if not writable_only else self.writable_candidates
if path.is_absolute():
for candidate in candidates:
Expand All @@ -214,7 +224,91 @@ def make_candidates(self, path: Path, writable_only: bool = False) -> List[Path]
)
)

return [candidate / path for candidate in candidates if candidate]
results = [candidate / path for candidate in candidates if candidate]

if not results and strict:
raise RuntimeError(
'Unable to find a suitable destination for "{}" in {}'.format(
str(path), paths_csv(self._candidates)
)
)

return results

def distributions(
self, name: Optional[str] = None, writable_only: bool = False
) -> Iterable[PathDistribution]:
path = list(
map(
str, self._candidates if not writable_only else self.writable_candidates
)
)
for distribution in PathDistribution.discover(
name=name, path=path
): # type: PathDistribution
yield distribution

def find_distribution(
self, name: str, writable_only: bool = False
) -> Optional[PathDistribution]:
for distribution in self.distributions(name=name, writable_only=writable_only):
return distribution
else:
return None

def find_distribution_files_with_suffix(
self, distribution_name: str, suffix: str, writable_only: bool = False
) -> Iterable[Path]:
for distribution in self.distributions(
name=distribution_name, writable_only=writable_only
):
for file in distribution.files:
if file.name.endswith(suffix):
yield Path(distribution.locate_file(file))

def find_distribution_files_with_name(
self, distribution_name: str, name: str, writable_only: bool = False
) -> Iterable[Path]:
for distribution in self.distributions(
name=distribution_name, writable_only=writable_only
):
for file in distribution.files:
if file.name == name:
yield Path(distribution.locate_file(file))

def find_distribution_nspkg_pth_files(
self, distribution_name: str, writable_only: bool = False
) -> Iterable[Path]:
return self.find_distribution_files_with_suffix(
distribution_name=distribution_name,
suffix="-nspkg.pth",
writable_only=writable_only,
)

def find_distribution_direct_url_json_files(
self, distribution_name: str, writable_only: bool = False
) -> Iterable[Path]:
return self.find_distribution_files_with_name(
distribution_name=distribution_name,
name="direct_url.json",
writable_only=writable_only,
)

def remove_distribution_files(self, distribution_name: str) -> List[Path]:
paths = []

for distribution in self.distributions(
name=distribution_name, writable_only=True
):
for file in distribution.files:
Path(distribution.locate_file(file)).unlink(missing_ok=True)

if distribution._path.exists():
shutil.rmtree(str(distribution._path))

paths.append(distribution._path)

return paths

def _path_method_wrapper(
self,
Expand All @@ -228,14 +322,9 @@ def _path_method_wrapper(
if isinstance(path, str):
path = Path(path)

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

if not candidates:
raise RuntimeError(
'Unable to find a suitable destination for "{}" in {}'.format(
str(path), paths_csv(self._candidates)
)
)
candidates = self.make_candidates(
path, writable_only=writable_only, strict=True
)

results = []

Expand All @@ -244,8 +333,7 @@ def _path_method_wrapper(
result = candidate, getattr(candidate, method)(*args, **kwargs)
if return_first:
return result
else:
results.append(result)
results.append(result)
except OSError:
# TODO: Replace with PermissionError
pass
Expand All @@ -267,7 +355,11 @@ def exists(self, path: Union[str, Path]) -> bool:
for value in self._path_method_wrapper(path, "exists", return_first=False)
)

def find(self, path: Union[str, 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
Loading

0 comments on commit d4c9263

Please sign in to comment.