diff --git a/src/poetry/puzzle/provider.py b/src/poetry/puzzle/provider.py index d2f5f12c130..804716f170d 100644 --- a/src/poetry/puzzle/provider.py +++ b/src/poetry/puzzle/provider.py @@ -35,6 +35,7 @@ from poetry.packages import DependencyPackage from poetry.packages.package_collection import PackageCollection from poetry.puzzle.exceptions import OverrideNeeded +from poetry.repositories.exceptions import PackageNotFound from poetry.utils.helpers import download_file from poetry.vcs.git import Git @@ -46,10 +47,12 @@ from poetry.core.packages.dependency import Dependency from poetry.core.packages.package import Package + from poetry.core.packages.specification import PackageSpecification from poetry.core.semver.version_constraint import VersionConstraint from poetry.core.version.markers import BaseMarker from poetry.repositories import Pool + from poetry.repositories import Repository from poetry.utils.env import Env @@ -124,6 +127,7 @@ def __init__( pool: Pool, io: Any, env: Env | None = None, + installed: Repository | None = None, ) -> None: self._package = package self._pool = pool @@ -136,6 +140,7 @@ def __init__( self._deferred_cache: dict[Dependency, Package] = {} self._load_deferred = True self._source_root: Path | None = None + self._installed = installed @property def pool(self) -> Pool: @@ -185,6 +190,36 @@ def validate_package_for_dependency( f" package's name: {package.name}" ) + def search_for_installed_packages( + self, + specification: PackageSpecification, + ) -> list[Package]: + """ + Search for installed packages, when available, that provides the given + specification. + + This is useful when dealing with packages that are under development, not + published on package sources and/or only available via system installations. + """ + if not self._installed: + return [] + + logger.debug( + "Falling back to installed packages to discover metadata for %s", + specification.complete_name, + ) + packages = [ + package + for package in self._installed.packages + if package.provides(specification) + ] + logger.debug( + "Found %d compatible packages for %s", + len(packages), + specification.complete_name, + ) + return packages + def search_for( self, dependency: ( @@ -227,6 +262,9 @@ def search_for( reverse=True, ) + if not packages: + packages = self.search_for_installed_packages(dependency) + return PackageCollection(dependency, packages) def search_for_vcs(self, dependency: VCSDependency) -> list[Package]: @@ -478,15 +516,25 @@ def complete_package(self, package: DependencyPackage) -> DependencyPackage: "url", "git", }: - package = DependencyPackage( - package.dependency, - self._pool.package( - package.name, - package.version.text, - extras=list(package.dependency.extras), - repository=package.dependency.source_name, - ), - ) + try: + package = DependencyPackage( + package.dependency, + self._pool.package( + package.name, + package.version.text, + extras=list(package.dependency.extras), + repository=package.dependency.source_name, + ), + ) + except PackageNotFound as e: + for pkg in self.search_for_installed_packages(package.dependency): + package = DependencyPackage( + package.dependency, + pkg, + ) + break + else: + raise e from e requires = package.requires else: requires = package.requires diff --git a/src/poetry/puzzle/solver.py b/src/poetry/puzzle/solver.py index b0d2515a7b6..359bdc94db2 100644 --- a/src/poetry/puzzle/solver.py +++ b/src/poetry/puzzle/solver.py @@ -58,7 +58,9 @@ def __init__( self._io = io if provider is None: - provider = Provider(self._package, self._pool, self._io) + provider = Provider( + self._package, self._pool, self._io, installed=installed + ) self._provider = provider self._overrides: list[dict[DependencyPackage, dict[str, Dependency]]] = [] diff --git a/tests/puzzle/test_solver.py b/tests/puzzle/test_solver.py index b9c3a0bff02..b61f5c36764 100644 --- a/tests/puzzle/test_solver.py +++ b/tests/puzzle/test_solver.py @@ -87,7 +87,12 @@ def solver( io: NullIO, ) -> Solver: return Solver( - package, pool, installed, locked, io, provider=Provider(package, pool, io) + package, + pool, + installed, + locked, + io, + provider=Provider(package, pool, io, installed=installed), ) @@ -174,6 +179,36 @@ def test_install_non_existing_package_fail( solver.solve() +def test_install_unpublished_package_does_not_fail( + installed: InstalledRepository, + solver: Solver, + repo: Repository, + package: ProjectPackage, +): + package.add_dependency(Factory.create_dependency("B", "1")) + + package_a = get_package("A", "1.0") + package_b = get_package("B", "1") + package_b.add_dependency(Factory.create_dependency("A", "1.0")) + + repo.add_package(package_a) + installed.add_package(package_b) + + transaction = solver.solve() + + check_solver_result( + transaction, + [ + {"job": "install", "package": package_a}, + { + "job": "install", + "package": package_b, + "skipped": True, # already installed + }, + ], + ) + + def test_solver_with_deps(solver: Solver, repo: Repository, package: ProjectPackage): package.add_dependency(Factory.create_dependency("A", "*"))