|
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 self.pep517_backend._supports_build_editable() |
| 258 | + |
250 | 259 | @property |
251 | 260 | def specifier(self) -> SpecifierSet: |
252 | 261 | return self.req.specifier |
@@ -503,93 +512,58 @@ 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 |
| 515 | + def prepare_metadata(self) -> None: |
| 516 | + """Ensure that project metadata is available. |
| 517 | +
|
| 518 | + Under PEP 517 and PEP 660, call the backend hook to prepare the metadata. |
| 519 | + Under legacy processing, call setup.py egg-info. |
| 520 | + """ |
| 521 | + assert self.source_dir |
| 522 | + |
| 523 | + if ( |
| 524 | + self.editable |
| 525 | + and self.use_pep517 |
| 526 | + and not self.supports_pyproject_editable() |
| 527 | + and not os.path.isfile(self.setup_py_path) |
| 528 | + and not os.path.isfile(self.setup_cfg_path) |
539 | 529 | ): |
| 530 | + # Most other project configuration sanity checks are done in |
| 531 | + # load_pyproject_toml. This specific one cannot be done earlier because we |
| 532 | + # do a 'setup.py develop' fallback also for projects with pyproject.toml and |
| 533 | + # setup.cfg without setup.py, and to decide if this is valid we must have |
| 534 | + # determined that the build backend does not support PEP 660. |
540 | 535 | 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." |
| 536 | + f"Project {self} has a 'pyproject.toml' and its build " |
| 537 | + f"backend is missing the 'build_editable' hook. Since it does not " |
| 538 | + f"have a 'setup.py' nor a 'setup.cfg', " |
| 539 | + f"it cannot be installed in editable mode. " |
| 540 | + f"Consider using a build backend that supports PEP 660." |
544 | 541 | ) |
545 | 542 |
|
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 | | - ) |
553 | | - |
554 | | - def _generate_metadata(self) -> str: |
555 | | - """Invokes metadata generator functions, with the required arguments.""" |
556 | 543 | if self.use_pep517: |
557 | 544 | assert self.pep517_backend is not None |
558 | | - try: |
559 | | - return generate_metadata( |
| 545 | + if ( |
| 546 | + self.editable |
| 547 | + and self.permit_editable_wheels |
| 548 | + and self.supports_pyproject_editable() |
| 549 | + ): |
| 550 | + self.metadata_directory = generate_editable_metadata( |
560 | 551 | build_env=self.build_env, |
561 | 552 | backend=self.pep517_backend, |
562 | 553 | ) |
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." |
| 554 | + else: |
| 555 | + self.metadata_directory = generate_metadata( |
| 556 | + build_env=self.build_env, |
| 557 | + backend=self.pep517_backend, |
567 | 558 | ) |
568 | | - elif not os.path.exists(self.setup_py_path): |
569 | | - raise InstallationError( |
570 | | - f"File 'setup.py' not found for legacy project {self}." |
571 | | - ) |
572 | | - |
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 | | - def prepare_metadata(self) -> None: |
582 | | - """Ensure that project metadata is available. |
583 | | -
|
584 | | - Under PEP 517, call the backend hook to prepare the metadata. |
585 | | - Under legacy processing, call setup.py egg-info. |
586 | | - """ |
587 | | - assert self.source_dir |
588 | | - |
589 | | - if self.editable and self.permit_editable_wheels: |
590 | | - self.metadata_directory = self._generate_editable_metadata() |
591 | 559 | else: |
592 | | - self.metadata_directory = self._generate_metadata() |
| 560 | + self.metadata_directory = generate_metadata_legacy( |
| 561 | + build_env=self.build_env, |
| 562 | + setup_py_path=self.setup_py_path, |
| 563 | + source_dir=self.unpacked_source_directory, |
| 564 | + isolated=self.isolated, |
| 565 | + details=self.name or f"from {self.link}", |
| 566 | + ) |
593 | 567 |
|
594 | 568 | # Act on the newly generated metadata, based on the name and version. |
595 | 569 | if not self.name: |
|
0 commit comments