Skip to content

Commit 9e4fb44

Browse files
committed
provider: allow fallback to installed packages
1 parent 2d8ea84 commit 9e4fb44

File tree

3 files changed

+100
-11
lines changed

3 files changed

+100
-11
lines changed

src/poetry/puzzle/provider.py

+61-9
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from poetry.packages import DependencyPackage
3636
from poetry.packages.package_collection import PackageCollection
3737
from poetry.puzzle.exceptions import OverrideNeeded
38+
from poetry.repositories.exceptions import PackageNotFound
3839
from poetry.utils.helpers import download_file
3940
from poetry.vcs.git import Git
4041

@@ -46,10 +47,12 @@
4647

4748
from poetry.core.packages.dependency import Dependency
4849
from poetry.core.packages.package import Package
50+
from poetry.core.packages.specification import PackageSpecification
4951
from poetry.core.semver.version_constraint import VersionConstraint
5052
from poetry.core.version.markers import BaseMarker
5153

5254
from poetry.repositories import Pool
55+
from poetry.repositories import Repository
5356
from poetry.utils.env import Env
5457

5558

@@ -124,6 +127,7 @@ def __init__(
124127
pool: Pool,
125128
io: Any,
126129
env: Env | None = None,
130+
installed: Repository | None = None,
127131
) -> None:
128132
self._package = package
129133
self._pool = pool
@@ -136,6 +140,7 @@ def __init__(
136140
self._deferred_cache: dict[Dependency, Package] = {}
137141
self._load_deferred = True
138142
self._source_root: Path | None = None
143+
self._installed = installed
139144

140145
@property
141146
def pool(self) -> Pool:
@@ -185,6 +190,36 @@ def validate_package_for_dependency(
185190
f" package's name: {package.name}"
186191
)
187192

193+
def search_for_installed_packages(
194+
self,
195+
specification: PackageSpecification,
196+
) -> list[Package]:
197+
"""
198+
Search for installed packages, when available, that provides the given
199+
specification.
200+
201+
This is useful when dealing with packages that are under development, not
202+
published on package sources and/or only available via system installations.
203+
"""
204+
if not self._installed:
205+
return []
206+
207+
logger.debug(
208+
"Falling back to installed packages to discover metadata for <c2>%s</>",
209+
specification.complete_name,
210+
)
211+
packages = [
212+
package
213+
for package in self._installed.packages
214+
if package.provides(specification)
215+
]
216+
logger.debug(
217+
"Found <c2>%d</> compatible packages for <c2>%s</>",
218+
len(packages),
219+
specification.complete_name,
220+
)
221+
return packages
222+
188223
def search_for(
189224
self,
190225
dependency: (
@@ -227,6 +262,9 @@ def search_for(
227262
reverse=True,
228263
)
229264

265+
if not packages:
266+
packages = self.search_for_installed_packages(dependency)
267+
230268
return PackageCollection(dependency, packages)
231269

232270
def search_for_vcs(self, dependency: VCSDependency) -> list[Package]:
@@ -478,15 +516,29 @@ def complete_package(self, package: DependencyPackage) -> DependencyPackage:
478516
"url",
479517
"git",
480518
}:
481-
package = DependencyPackage(
482-
package.dependency,
483-
self._pool.package(
484-
package.name,
485-
package.version.text,
486-
extras=list(package.dependency.extras),
487-
repository=package.dependency.source_name,
488-
),
489-
)
519+
try:
520+
package = DependencyPackage(
521+
package.dependency,
522+
self._pool.package(
523+
package.name,
524+
package.version.text,
525+
extras=list(package.dependency.extras),
526+
repository=package.dependency.source_name,
527+
),
528+
)
529+
except PackageNotFound as e:
530+
try:
531+
package = next(
532+
DependencyPackage(
533+
package.dependency,
534+
pkg,
535+
)
536+
for pkg in self.search_for_installed_packages(
537+
package.dependency
538+
)
539+
)
540+
except StopIteration:
541+
raise e from e
490542
requires = package.requires
491543
else:
492544
requires = package.requires

src/poetry/puzzle/solver.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ def __init__(
5858
self._io = io
5959

6060
if provider is None:
61-
provider = Provider(self._package, self._pool, self._io)
61+
provider = Provider(
62+
self._package, self._pool, self._io, installed=installed
63+
)
6264

6365
self._provider = provider
6466
self._overrides: list[dict[DependencyPackage, dict[str, Dependency]]] = []

tests/puzzle/test_solver.py

+36-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,12 @@ def solver(
8787
io: NullIO,
8888
) -> Solver:
8989
return Solver(
90-
package, pool, installed, locked, io, provider=Provider(package, pool, io)
90+
package,
91+
pool,
92+
installed,
93+
locked,
94+
io,
95+
provider=Provider(package, pool, io, installed=installed),
9196
)
9297

9398

@@ -174,6 +179,36 @@ def test_install_non_existing_package_fail(
174179
solver.solve()
175180

176181

182+
def test_install_unpublished_package_does_not_fail(
183+
installed: InstalledRepository,
184+
solver: Solver,
185+
repo: Repository,
186+
package: ProjectPackage,
187+
):
188+
package.add_dependency(Factory.create_dependency("B", "1"))
189+
190+
package_a = get_package("A", "1.0")
191+
package_b = get_package("B", "1")
192+
package_b.add_dependency(Factory.create_dependency("A", "1.0"))
193+
194+
repo.add_package(package_a)
195+
installed.add_package(package_b)
196+
197+
transaction = solver.solve()
198+
199+
check_solver_result(
200+
transaction,
201+
[
202+
{"job": "install", "package": package_a},
203+
{
204+
"job": "install",
205+
"package": package_b,
206+
"skipped": True, # already installed
207+
},
208+
],
209+
)
210+
211+
177212
def test_solver_with_deps(solver: Solver, repo: Repository, package: ProjectPackage):
178213
package.add_dependency(Factory.create_dependency("A", "*"))
179214

0 commit comments

Comments
 (0)