|
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