Skip to content

Commit c588f6d

Browse files
committed
site: use importlib instead of adhoc file searches
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.
1 parent 00309be commit c588f6d

File tree

6 files changed

+181
-122
lines changed

6 files changed

+181
-122
lines changed

Diff for: poetry/installation/executor.py

+8-22
Original file line numberDiff line numberDiff line change
@@ -714,19 +714,6 @@ def _download_archive(self, operation: Union[Install, Update], link: Link) -> Pa
714714
def _should_write_operation(self, operation: Operation) -> bool:
715715
return not operation.skipped or self._dry_run or self._verbose
716716

717-
@staticmethod
718-
def _package_dist_info_path(package: "Package") -> Path:
719-
from poetry.core.masonry.utils.helpers import escape_name
720-
from poetry.core.masonry.utils.helpers import escape_version
721-
722-
return Path(
723-
f"{escape_name(package.pretty_name)}-{escape_version(package.version.text)}.dist-info"
724-
)
725-
726-
@classmethod
727-
def _direct_url_json_path(cls, package: "Package") -> Path:
728-
return cls._package_dist_info_path(package) / "direct_url.json"
729-
730717
def _save_url_reference(self, operation: "OperationTypes") -> None:
731718
"""
732719
Create and store a PEP-610 `direct_url.json` file, if needed.
@@ -742,11 +729,12 @@ def _save_url_reference(self, operation: "OperationTypes") -> None:
742729
# distribution.
743730
# That's not what we want so we remove the direct_url.json file,
744731
# if it exists.
745-
for direct_url in self._env.site_packages.find(
746-
self._direct_url_json_path(package), True
732+
for (
733+
direct_url_json
734+
) in self._env.site_packages.find_distribution_direct_url_json_files(
735+
distribution_name=package.name, writable_only=True
747736
):
748-
direct_url.unlink()
749-
737+
direct_url_json.unlink(missing_ok=True)
750738
return
751739

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

763751
if url_reference:
764-
for path in self._env.site_packages.find(
765-
self._package_dist_info_path(package), writable_only=True
752+
for dist in self._env.site_packages.distributions(
753+
name=package.name, writable_only=True
766754
):
767-
self._env.site_packages.write_text(
768-
path / "direct_url.json",
755+
dist._path.joinpath("direct_url.json").write_text(
769756
json.dumps(url_reference),
770757
encoding="utf-8",
771758
)
772-
break
773759

774760
def _create_git_url_reference(
775761
self, package: "Package"

Diff for: poetry/installation/pip_installer.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ def remove(self, package: "Package") -> None:
117117
raise
118118

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

123125
# If we have a VCS package, remove its source directory

Diff for: poetry/masonry/builders/editable.py

+20-14
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ def build(self) -> None:
6161

6262
self._run_build_script(self._package.build_script)
6363

64+
for removed in self._env.site_packages.remove_distribution_files(
65+
distribution_name=self._package.name
66+
):
67+
self._debug(
68+
" - Removed <c2>{}</c2> directory from <b>{}</b>".format(
69+
removed.name, removed.parent
70+
)
71+
)
72+
6473
added_files = []
6574
added_files += self._add_pth()
6675
added_files += self._add_scripts()
@@ -115,6 +124,16 @@ def _add_pth(self) -> List[Path]:
115124
content += decode(path + os.linesep)
116125

117126
pth_file = Path(self._module.name).with_suffix(".pth")
127+
128+
# remove any pre-existing pth files for this package
129+
for file in self._env.site_packages.find(path=pth_file, writable_only=True):
130+
self._debug(
131+
" - Removing existing <c2>{}</c2> from <b>{}</b> for {}".format(
132+
file.name, file.parent, self._poetry.file.parent
133+
)
134+
)
135+
file.unlink(missing_ok=True)
136+
118137
try:
119138
pth_file = self._env.site_packages.write_text(
120139
pth_file, content, encoding="utf-8"
@@ -199,20 +218,7 @@ def _add_dist_info(self, added_files: List[Path]) -> None:
199218
added_files = added_files[:]
200219

201220
builder = WheelBuilder(self._poetry)
202-
203-
dist_info_path = Path(builder.dist_info)
204-
for dist_info in self._env.site_packages.find(
205-
dist_info_path, writable_only=True
206-
):
207-
if dist_info.exists():
208-
self._debug(
209-
" - Removing existing <c2>{}</c2> directory from <b>{}</b>".format(
210-
dist_info.name, dist_info.parent
211-
)
212-
)
213-
shutil.rmtree(str(dist_info))
214-
215-
dist_info = self._env.site_packages.mkdir(dist_info_path)
221+
dist_info = self._env.site_packages.mkdir(Path(builder.dist_info))
216222

217223
self._debug(
218224
" - Adding the <c2>{}</c2> directory to <b>{}</b>".format(

Diff for: poetry/repositories/installed_repository.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from poetry.core.packages.utils.utils import url_to_path
1212
from poetry.core.utils.helpers import canonicalize_name
1313
from poetry.core.utils.helpers import module_name
14-
from poetry.utils._compat import metadata
1514
from poetry.utils.env import Env
1615

1716
from .repository import Repository
@@ -20,7 +19,9 @@
2019
_VENDORS = Path(__file__).parent.parent.joinpath("_vendor")
2120

2221
if TYPE_CHECKING:
23-
from importlib.metadata import Distribution
22+
from poetry.utils._compat import metadata
23+
24+
Distribution = metadata.Distribution
2425

2526

2627
try:

Diff for: poetry/utils/env.py

+106-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import base64
22
import hashlib
3+
import itertools
34
import json
45
import os
56
import platform
@@ -17,6 +18,7 @@
1718
from typing import Any
1819
from typing import ContextManager
1920
from typing import Dict
21+
from typing import Iterable
2022
from typing import Iterator
2123
from typing import List
2224
from typing import Optional
@@ -43,6 +45,7 @@
4345
from poetry.utils._compat import decode
4446
from poetry.utils._compat import encode
4547
from poetry.utils._compat import list_to_shell_command
48+
from poetry.utils._compat import metadata
4649
from poetry.utils.helpers import is_dir_writable
4750
from poetry.utils.helpers import paths_csv
4851
from poetry.utils.helpers import temporary_directory
@@ -166,7 +169,12 @@ def __init__(
166169

167170
self._fallbacks = fallbacks or []
168171
self._skip_write_checks = skip_write_checks
169-
self._candidates = list({self._purelib, self._platlib}) + self._fallbacks
172+
173+
self._candidates: List[Path] = []
174+
for path in itertools.chain([self._purelib, self._platlib], self._fallbacks):
175+
if path not in self._candidates:
176+
self._candidates.append(path)
177+
170178
self._writable_candidates = None if not skip_write_checks else self._candidates
171179

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

199207
return self._writable_candidates
200208

201-
def make_candidates(self, path: Path, writable_only: bool = False) -> List[Path]:
209+
def make_candidates(
210+
self, path: Path, writable_only: bool = False, strict: bool = False
211+
) -> List[Path]:
202212
candidates = self._candidates if not writable_only else self.writable_candidates
203213
if path.is_absolute():
204214
for candidate in candidates:
@@ -214,7 +224,91 @@ def make_candidates(self, path: Path, writable_only: bool = False) -> List[Path]
214224
)
215225
)
216226

217-
return [candidate / path for candidate in candidates if candidate]
227+
results = [candidate / path for candidate in candidates if candidate]
228+
229+
if not results and strict:
230+
raise RuntimeError(
231+
'Unable to find a suitable destination for "{}" in {}'.format(
232+
str(path), paths_csv(self._candidates)
233+
)
234+
)
235+
236+
return results
237+
238+
def distributions(
239+
self, name: Optional[str] = None, writable_only: bool = False
240+
) -> Iterable[metadata.PathDistribution]:
241+
path = list(
242+
map(
243+
str, self._candidates if not writable_only else self.writable_candidates
244+
)
245+
)
246+
for distribution in metadata.PathDistribution.discover(
247+
name=name, path=path
248+
): # type: metadata.PathDistribution
249+
yield distribution
250+
251+
def find_distribution(
252+
self, name: str, writable_only: bool = False
253+
) -> Optional[metadata.PathDistribution]:
254+
for distribution in self.distributions(name=name, writable_only=writable_only):
255+
return distribution
256+
else:
257+
return None
258+
259+
def find_distribution_files_with_suffix(
260+
self, distribution_name: str, suffix: str, writable_only: bool = False
261+
) -> Iterable[Path]:
262+
for distribution in self.distributions(
263+
name=distribution_name, writable_only=writable_only
264+
):
265+
for file in distribution.files:
266+
if file.name.endswith(suffix):
267+
yield Path(distribution.locate_file(file))
268+
269+
def find_distribution_files_with_name(
270+
self, distribution_name: str, name: str, writable_only: bool = False
271+
) -> Iterable[Path]:
272+
for distribution in self.distributions(
273+
name=distribution_name, writable_only=writable_only
274+
):
275+
for file in distribution.files:
276+
if file.name == name:
277+
yield Path(distribution.locate_file(file))
278+
279+
def find_distribution_nspkg_pth_files(
280+
self, distribution_name: str, writable_only: bool = False
281+
) -> Iterable[Path]:
282+
return self.find_distribution_files_with_suffix(
283+
distribution_name=distribution_name,
284+
suffix="-nspkg.pth",
285+
writable_only=writable_only,
286+
)
287+
288+
def find_distribution_direct_url_json_files(
289+
self, distribution_name: str, writable_only: bool = False
290+
) -> Iterable[Path]:
291+
return self.find_distribution_files_with_name(
292+
distribution_name=distribution_name,
293+
name="direct_url.json",
294+
writable_only=writable_only,
295+
)
296+
297+
def remove_distribution_files(self, distribution_name: str) -> List[Path]:
298+
paths = []
299+
300+
for distribution in self.distributions(
301+
name=distribution_name, writable_only=True
302+
):
303+
for file in distribution.files:
304+
Path(distribution.locate_file(file)).unlink(missing_ok=True)
305+
306+
if distribution._path.exists():
307+
shutil.rmtree(str(distribution._path))
308+
309+
paths.append(distribution._path)
310+
311+
return paths
218312

219313
def _path_method_wrapper(
220314
self,
@@ -228,14 +322,9 @@ def _path_method_wrapper(
228322
if isinstance(path, str):
229323
path = Path(path)
230324

231-
candidates = self.make_candidates(path, writable_only=writable_only)
232-
233-
if not candidates:
234-
raise RuntimeError(
235-
'Unable to find a suitable destination for "{}" in {}'.format(
236-
str(path), paths_csv(self._candidates)
237-
)
238-
)
325+
candidates = self.make_candidates(
326+
path, writable_only=writable_only, strict=True
327+
)
239328

240329
results = []
241330

@@ -244,8 +333,7 @@ def _path_method_wrapper(
244333
result = candidate, getattr(candidate, method)(*args, **kwargs)
245334
if return_first:
246335
return result
247-
else:
248-
results.append(result)
336+
results.append(result)
249337
except OSError:
250338
# TODO: Replace with PermissionError
251339
pass
@@ -267,7 +355,11 @@ def exists(self, path: Union[str, Path]) -> bool:
267355
for value in self._path_method_wrapper(path, "exists", return_first=False)
268356
)
269357

270-
def find(self, path: Union[str, Path], writable_only: bool = False) -> List[Path]:
358+
def find(
359+
self,
360+
path: Union[str, Path],
361+
writable_only: bool = False,
362+
) -> List[Path]:
271363
return [
272364
value[0]
273365
for value in self._path_method_wrapper(

0 commit comments

Comments
 (0)