|
1 | 1 | # The following comment should be removed at some point in the future.
|
2 | 2 | # mypy: strict-optional=False
|
3 | 3 |
|
| 4 | +import functools |
4 | 5 | import logging
|
5 | 6 | import os
|
6 | 7 | import shutil
|
|
16 | 17 | from pip._vendor.packaging.utils import canonicalize_name
|
17 | 18 | from pip._vendor.packaging.version import Version
|
18 | 19 | from pip._vendor.packaging.version import parse as parse_version
|
19 |
| -from pip._vendor.pep517.wrappers import HookMissing, Pep517HookCaller |
| 20 | +from pip._vendor.pep517.wrappers import Pep517HookCaller |
20 | 21 | from pip._vendor.pkg_resources import Distribution
|
21 | 22 |
|
22 | 23 | from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
|
|
53 | 54 | redact_auth_from_url,
|
54 | 55 | )
|
55 | 56 | from pip._internal.utils.packaging import get_metadata
|
| 57 | +from pip._internal.utils.subprocess import runner_with_spinner_message |
56 | 58 | from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
|
57 | 59 | from pip._internal.utils.virtualenv import running_under_virtualenv
|
58 | 60 | from pip._internal.vcs import vcs
|
@@ -196,11 +198,6 @@ def __init__(
|
196 | 198 | # but after loading this flag should be treated as read only.
|
197 | 199 | self.use_pep517 = use_pep517
|
198 | 200 |
|
199 |
| - # supports_pyproject_editable will be set to True or False when we try |
200 |
| - # to prepare editable metadata or build an editable wheel. None means |
201 |
| - # "we don't know yet". |
202 |
| - self.supports_pyproject_editable: Optional[bool] = None |
203 |
| - |
204 | 201 | # This requirement needs more preparation before it can be built
|
205 | 202 | self.needs_more_preparation = False
|
206 | 203 |
|
@@ -247,6 +244,18 @@ def name(self) -> Optional[str]:
|
247 | 244 | return None
|
248 | 245 | return pkg_resources.safe_name(self.req.name)
|
249 | 246 |
|
| 247 | + @functools.lru_cache() # use cached_property in python 3.8+ |
| 248 | + def supports_pyproject_editable(self) -> bool: |
| 249 | + if not self.use_pep517: |
| 250 | + return False |
| 251 | + assert self.pep517_backend |
| 252 | + with self.build_env: |
| 253 | + runner = runner_with_spinner_message( |
| 254 | + "Checking if build backend supports build_editable" |
| 255 | + ) |
| 256 | + with self.pep517_backend.subprocess_runner(runner): |
| 257 | + return "build_editable" in self.pep517_backend._supported_features() |
| 258 | + |
250 | 259 | @property
|
251 | 260 | def specifier(self) -> SpecifierSet:
|
252 | 261 | return self.req.specifier
|
@@ -503,93 +512,59 @@ def load_pyproject_toml(self) -> None:
|
503 | 512 | backend_path=backend_path,
|
504 | 513 | )
|
505 | 514 |
|
506 |
| - def _generate_editable_metadata(self) -> str: |
507 |
| - """Invokes metadata generator functions, with the required arguments.""" |
508 |
| - if self.use_pep517: |
509 |
| - assert self.pep517_backend is not None |
510 |
| - try: |
511 |
| - metadata_directory = generate_editable_metadata( |
512 |
| - build_env=self.build_env, |
513 |
| - backend=self.pep517_backend, |
514 |
| - ) |
515 |
| - except HookMissing as e: |
516 |
| - self.supports_pyproject_editable = False |
517 |
| - if not os.path.exists(self.setup_py_path) and not os.path.exists( |
518 |
| - self.setup_cfg_path |
519 |
| - ): |
520 |
| - raise InstallationError( |
521 |
| - f"Project {self} has a 'pyproject.toml' and its build " |
522 |
| - f"backend is missing the {e} hook. Since it does not " |
523 |
| - f"have a 'setup.py' nor a 'setup.cfg', " |
524 |
| - f"it cannot be installed in editable mode. " |
525 |
| - f"Consider using a build backend that supports PEP 660." |
526 |
| - ) |
527 |
| - # At this point we have determined that the build_editable hook |
528 |
| - # is missing, and there is a setup.py or setup.cfg |
529 |
| - # so we fallback to the legacy metadata generation |
530 |
| - logger.info( |
531 |
| - "Build backend does not support editables, " |
532 |
| - "falling back to setup.py egg_info." |
533 |
| - ) |
534 |
| - else: |
535 |
| - self.supports_pyproject_editable = True |
536 |
| - return metadata_directory |
537 |
| - elif not os.path.exists(self.setup_py_path) and not os.path.exists( |
538 |
| - self.setup_cfg_path |
539 |
| - ): |
540 |
| - raise InstallationError( |
541 |
| - f"File 'setup.py' or 'setup.cfg' not found " |
542 |
| - f"for legacy project {self}. " |
543 |
| - f"It cannot be installed in editable mode." |
544 |
| - ) |
545 |
| - |
546 |
| - return generate_metadata_legacy( |
547 |
| - build_env=self.build_env, |
548 |
| - setup_py_path=self.setup_py_path, |
549 |
| - source_dir=self.unpacked_source_directory, |
550 |
| - isolated=self.isolated, |
551 |
| - details=self.name or f"from {self.link}", |
552 |
| - ) |
| 515 | + def isolated_editable_sanity_check(self) -> None: |
| 516 | + """Check that an editable requirement if valid for use with PEP 517/518. |
553 | 517 |
|
554 |
| - def _generate_metadata(self) -> str: |
555 |
| - """Invokes metadata generator functions, with the required arguments.""" |
556 |
| - if self.use_pep517: |
557 |
| - assert self.pep517_backend is not None |
558 |
| - try: |
559 |
| - return generate_metadata( |
560 |
| - build_env=self.build_env, |
561 |
| - backend=self.pep517_backend, |
562 |
| - ) |
563 |
| - except HookMissing as e: |
564 |
| - raise InstallationError( |
565 |
| - f"Project {self} has a pyproject.toml but its build " |
566 |
| - f"backend is missing the required {e} hook." |
567 |
| - ) |
568 |
| - elif not os.path.exists(self.setup_py_path): |
| 518 | + This verifies that an editable that has a pyproject.toml either supports PEP 660 |
| 519 | + or as a setup.py or a setup.cfg |
| 520 | + """ |
| 521 | + if ( |
| 522 | + self.editable |
| 523 | + and self.use_pep517 |
| 524 | + and not self.supports_pyproject_editable() |
| 525 | + and not os.path.isfile(self.setup_py_path) |
| 526 | + and not os.path.isfile(self.setup_cfg_path) |
| 527 | + ): |
569 | 528 | raise InstallationError(
|
570 |
| - f"File 'setup.py' not found for legacy project {self}." |
| 529 | + f"Project {self} has a 'pyproject.toml' and its build " |
| 530 | + f"backend is missing the 'build_editable' hook. Since it does not " |
| 531 | + f"have a 'setup.py' nor a 'setup.cfg', " |
| 532 | + f"it cannot be installed in editable mode. " |
| 533 | + f"Consider using a build backend that supports PEP 660." |
571 | 534 | )
|
572 | 535 |
|
573 |
| - return generate_metadata_legacy( |
574 |
| - build_env=self.build_env, |
575 |
| - setup_py_path=self.setup_py_path, |
576 |
| - source_dir=self.unpacked_source_directory, |
577 |
| - isolated=self.isolated, |
578 |
| - details=self.name or f"from {self.link}", |
579 |
| - ) |
580 |
| - |
581 | 536 | def prepare_metadata(self) -> None:
|
582 | 537 | """Ensure that project metadata is available.
|
583 | 538 |
|
584 |
| - Under PEP 517, call the backend hook to prepare the metadata. |
| 539 | + Under PEP 517 and PEP 660, call the backend hook to prepare the metadata. |
585 | 540 | Under legacy processing, call setup.py egg-info.
|
586 | 541 | """
|
587 | 542 | assert self.source_dir
|
588 | 543 |
|
589 |
| - if self.editable and self.permit_editable_wheels: |
590 |
| - self.metadata_directory = self._generate_editable_metadata() |
| 544 | + if self.use_pep517: |
| 545 | + assert self.pep517_backend is not None |
| 546 | + if ( |
| 547 | + self.editable |
| 548 | + and self.permit_editable_wheels |
| 549 | + and self.supports_pyproject_editable() |
| 550 | + ): |
| 551 | + self.metadata_directory = generate_editable_metadata( |
| 552 | + build_env=self.build_env, |
| 553 | + backend=self.pep517_backend, |
| 554 | + ) |
| 555 | + else: |
| 556 | + self.metadata_directory = generate_metadata( |
| 557 | + build_env=self.build_env, |
| 558 | + backend=self.pep517_backend, |
| 559 | + ) |
591 | 560 | else:
|
592 |
| - self.metadata_directory = self._generate_metadata() |
| 561 | + self.metadata_directory = generate_metadata_legacy( |
| 562 | + build_env=self.build_env, |
| 563 | + setup_py_path=self.setup_py_path, |
| 564 | + source_dir=self.unpacked_source_directory, |
| 565 | + isolated=self.isolated, |
| 566 | + details=self.name or f"from {self.link}", |
| 567 | + ) |
593 | 568 |
|
594 | 569 | # Act on the newly generated metadata, based on the name and version.
|
595 | 570 | if not self.name:
|
|
0 commit comments