|
23 | 23 | from pip._vendor import pkg_resources
|
24 | 24 | from pip._vendor.distlib.scripts import ScriptMaker
|
25 | 25 | from pip._vendor.distlib.util import get_export_entry
|
| 26 | +from pip._vendor.packaging.utils import canonicalize_name |
26 | 27 | from pip._vendor.six import StringIO
|
27 | 28 |
|
28 | 29 | from pip._internal.exceptions import InstallationError
|
@@ -283,6 +284,98 @@ def make(self, specification, options=None):
|
283 | 284 | return super(PipScriptMaker, self).make(specification, options)
|
284 | 285 |
|
285 | 286 |
|
| 287 | +def _get_file_owners( |
| 288 | + lib_dir, # type: str |
| 289 | + top_level, # type: str |
| 290 | + ignore_name, # type: str |
| 291 | +): |
| 292 | + # type: (...) -> Dict[str, List[str]] |
| 293 | + """Return dict mapping distributions to files under a top level directory |
| 294 | +
|
| 295 | + Look through lib_dir for distributions that own files under the |
| 296 | + top_level directory specified. Skip distributions that match the |
| 297 | + ignore_name. Return a dict mapping filenames to the distributions |
| 298 | + that own them. |
| 299 | +
|
| 300 | + """ |
| 301 | + file_owners = {} # type: Dict[str, List[str]] |
| 302 | + existing_env = pkg_resources.Environment([lib_dir]) |
| 303 | + canonical_name = canonicalize_name(ignore_name) |
| 304 | + for existing_dist_name in existing_env: |
| 305 | + for d in existing_env[existing_dist_name]: |
| 306 | + if canonicalize_name(d.project_name) == canonical_name: |
| 307 | + continue |
| 308 | + |
| 309 | + existing_info_dir = os.path.join( |
| 310 | + lib_dir, |
| 311 | + '{}-{}.dist-info'.format(d.project_name, d.version), |
| 312 | + ) |
| 313 | + |
| 314 | + top_level_path = os.path.join(existing_info_dir, 'top_level.txt') |
| 315 | + if not os.path.exists(top_level_path): |
| 316 | + continue |
| 317 | + with open(top_level_path, 'r') as f: |
| 318 | + existing_top_level = f.read().strip() |
| 319 | + if existing_top_level != top_level: |
| 320 | + continue |
| 321 | + |
| 322 | + record_path = os.path.join(existing_info_dir, 'RECORD') |
| 323 | + if not os.path.exists(record_path): |
| 324 | + continue |
| 325 | + with open(record_path, **csv_io_kwargs('r')) as record_file: |
| 326 | + for row in csv.reader(record_file): |
| 327 | + file_owners.setdefault(row[0], []).append(str(d)) |
| 328 | + |
| 329 | + return file_owners |
| 330 | + |
| 331 | + |
| 332 | +def _report_file_owner_conflicts( |
| 333 | + lib_dir, # type: str |
| 334 | + name, # type: str |
| 335 | + source_dir, # type: str |
| 336 | + info_dir, # type: str |
| 337 | +): |
| 338 | + # type: (...) -> None |
| 339 | + """Report files owned by other distributions that are being overwritten. |
| 340 | +
|
| 341 | + Scan the lib_dir for distributions that own files under the same |
| 342 | + top level directory as the wheel being installed and report any |
| 343 | + files owned by those other distributions that are going to be |
| 344 | + overwritten. |
| 345 | +
|
| 346 | + """ |
| 347 | + installing_top_level_path = os.path.join( |
| 348 | + source_dir, info_dir, 'top_level.txt') |
| 349 | + if not os.path.exists(installing_top_level_path): |
| 350 | + # We cannot determine the top level directory, so there is no |
| 351 | + # point in continuing. |
| 352 | + return |
| 353 | + |
| 354 | + with open(installing_top_level_path, 'r') as fd: |
| 355 | + installing_top_level = fd.read().strip() |
| 356 | + files_from_other_owners = _get_file_owners( |
| 357 | + lib_dir, installing_top_level, name) |
| 358 | + if not files_from_other_owners: |
| 359 | + # Nothing else owns files under this top level directory, so |
| 360 | + # we don't need to scan the source. |
| 361 | + return |
| 362 | + |
| 363 | + for dir, subdirs, files in os.walk(source_dir): |
| 364 | + basedir = dir[len(source_dir):].lstrip(os.path.sep) |
| 365 | + for f in files: |
| 366 | + partial_src = os.path.join(basedir, f) |
| 367 | + if partial_src not in files_from_other_owners: |
| 368 | + # There are no other owners for this file. |
| 369 | + continue |
| 370 | + destfile = os.path.join(lib_dir, basedir, f) |
| 371 | + for owner in files_from_other_owners[partial_src]: |
| 372 | + warnings.warn( |
| 373 | + 'Overwriting or removing {} for {} ' |
| 374 | + 'which is also owned by {}'.format( |
| 375 | + destfile, name, owner), |
| 376 | + FutureWarning) |
| 377 | + |
| 378 | + |
286 | 379 | def install_unpacked_wheel(
|
287 | 380 | name, # type: str
|
288 | 381 | wheeldir, # type: str
|
@@ -415,6 +508,13 @@ def clobber(
|
415 | 508 | changed = fixer(destfile)
|
416 | 509 | record_installed(srcfile, destfile, changed)
|
417 | 510 |
|
| 511 | + # Look for packages containing files that are already using the |
| 512 | + # same toplevel directory as the wheel we are installing but that |
| 513 | + # have a different dist name. Do this before calling clobber(), so |
| 514 | + # that when the warning is eventually changed to a hard error no |
| 515 | + # partial installation occurs. |
| 516 | + _report_file_owner_conflicts(lib_dir, name, source, info_dir) |
| 517 | + |
418 | 518 | clobber(source, lib_dir, True)
|
419 | 519 |
|
420 | 520 | dest_info_dir = os.path.join(lib_dir, info_dir)
|
|
0 commit comments