Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for multiple-constraint direct origin dependencies with same version #5715

Merged
merged 4 commits into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions src/poetry/installation/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from poetry.repositories import Pool
from poetry.repositories import Repository
from poetry.repositories.installed_repository import InstalledRepository
from poetry.repositories.lockfile_repository import LockfileRepository
from poetry.utils.extras import get_extra_package_names
from poetry.utils.helpers import canonicalize_name
from poetry.utils.helpers import pluralize
Expand Down Expand Up @@ -107,9 +108,7 @@ def run(self) -> int:
self._write_lock = False
self._execute_operations = False

local_repo = Repository()

return self._do_install(local_repo)
return self._do_install()

def dry_run(self, dry_run: bool = True) -> Installer:
self._dry_run = dry_run
Expand Down Expand Up @@ -204,14 +203,14 @@ def _do_refresh(self) -> int:
):
ops = solver.solve(use_latest=[]).calculate_operations()

local_repo = Repository()
self._populate_local_repo(local_repo, ops)
lockfile_repo = LockfileRepository()
self._populate_lockfile_repo(lockfile_repo, ops)

self._write_lock_file(local_repo, force=True)
self._write_lock_file(lockfile_repo, force=True)

return 0

def _do_install(self, local_repo: Repository) -> int:
def _do_install(self) -> int:
from poetry.puzzle.solver import Solver

locked_repository = Repository()
Expand Down Expand Up @@ -266,10 +265,11 @@ def _do_install(self, local_repo: Repository) -> int:
# currently installed
ops = self._get_operations_from_lock(locked_repository)

self._populate_local_repo(local_repo, ops)
lockfile_repo = LockfileRepository()
self._populate_lockfile_repo(lockfile_repo, ops)

if self._update:
self._write_lock_file(local_repo)
self._write_lock_file(lockfile_repo)

if self._lock:
# If we are only in lock mode, no need to go any further
Expand All @@ -292,8 +292,8 @@ def _do_install(self, local_repo: Repository) -> int:
# Making a new repo containing the packages
# newly resolved and the ones from the current lock file
repo = Repository()
for package in local_repo.packages + locked_repository.packages:
if not repo.has_package(package):
for package in lockfile_repo.packages + locked_repository.packages:
if not package.is_direct_origin() and not repo.has_package(package):
repo.add_package(package)

pool.add_repository(repo)
Expand All @@ -318,7 +318,7 @@ def _do_install(self, local_repo: Repository) -> int:

transaction = Transaction(
locked_repository.packages,
[(package, 0) for package in local_repo.packages],
[(package, 0) for package in lockfile_repo.packages],
installed_packages=self._installed_repository.packages,
root_package=root,
)
Expand All @@ -332,12 +332,12 @@ def _do_install(self, local_repo: Repository) -> int:
# We need to filter operations so that packages
# not compatible with the current system,
# or optional and not requested, are dropped
self._filter_operations(ops, local_repo)
self._filter_operations(ops, lockfile_repo)

# Execute operations
return self._execute(ops)

def _write_lock_file(self, repo: Repository, force: bool = False) -> None:
def _write_lock_file(self, repo: LockfileRepository, force: bool = False) -> None:
if self._write_lock and (force or self._update):
updated_lock = self._locker.set_lock_data(self._package, repo.packages)

Expand Down Expand Up @@ -460,8 +460,8 @@ def _execute_uninstall(self, operation: Uninstall) -> None:

self._installer.remove(operation.package)

def _populate_local_repo(
self, local_repo: Repository, ops: Sequence[Operation]
def _populate_lockfile_repo(
self, repo: LockfileRepository, ops: Sequence[Operation]
) -> None:
for op in ops:
if isinstance(op, Uninstall):
Expand All @@ -471,8 +471,8 @@ def _populate_local_repo(
else:
package = op.package

if not local_repo.has_package(package):
local_repo.add_package(package)
if not repo.has_package(package):
repo.add_package(package)

def _get_operations_from_lock(
self, locked_repository: Repository
Expand Down
13 changes: 12 additions & 1 deletion src/poetry/packages/locker.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,18 @@ def _get_lock_data(self) -> TOMLDocument:
def _lock_packages(self, packages: list[Package]) -> list[dict[str, Any]]:
locked = []

for package in sorted(packages, key=lambda x: (x.name, x.version)):
for package in sorted(
packages,
key=lambda x: (
x.name,
x.version,
x.source_type or "",
x.source_url or "",
x.source_subdirectory or "",
x.source_reference or "",
x.source_resolved_reference or "",
),
):
spec = self._dump_package(package)

locked.append(spec)
Expand Down
40 changes: 16 additions & 24 deletions src/poetry/puzzle/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,39 +612,31 @@ def complete_package(self, package: DependencyPackage) -> DependencyPackage:
# An example of this is:
# - pypiwin32 (220); sys_platform == "win32" and python_version >= "3.6"
# - pypiwin32 (219); sys_platform == "win32" and python_version < "3.6"
#
# Additional care has to be taken to ensure that hidden constraints like that
# of source type, reference etc. are taking into consideration when duplicates
# are identified.
duplicates: dict[
tuple[str, str | None, str | None, str | None, str | None], list[Dependency]
] = {}
duplicates: dict[str, list[Dependency]] = defaultdict(list)
for dep in dependencies:
key = (
dep.complete_name,
dep.source_type,
dep.source_url,
dep.source_reference,
dep.source_subdirectory,
)
if key not in duplicates:
duplicates[key] = []
duplicates[key].append(dep)
duplicates[dep.complete_name].append(dep)

dependencies = []
for key, deps in duplicates.items():
for dep_name, deps in duplicates.items():
if len(deps) == 1:
dependencies.append(deps[0])
continue

extra_keys = ", ".join(k for k in key[1:] if k is not None)
dep_name = f"{key[0]} ({extra_keys})" if extra_keys else key[0]

self.debug(f"<debug>Duplicate dependencies for {dep_name}</debug>")

deps = self._merge_dependencies_by_marker(deps)
deps = self._merge_dependencies_by_constraint(deps)

non_direct_origin_deps: list[Dependency] = []
direct_origin_deps: list[Dependency] = []
for dep in deps:
if dep.is_direct_origin():
direct_origin_deps.append(dep)
else:
non_direct_origin_deps.append(dep)
deps = (
self._merge_dependencies_by_constraint(
self._merge_dependencies_by_marker(non_direct_origin_deps)
)
+ direct_origin_deps
)
if len(deps) == 1:
self.debug(f"<debug>Merging requirements for {deps[0]!s}</debug>")
dependencies.append(deps[0])
Expand Down
29 changes: 29 additions & 0 deletions src/poetry/repositories/lockfile_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from poetry.repositories import Repository


if TYPE_CHECKING:
from poetry.core.packages.package import Package


class LockfileRepository(Repository):
"""
Special repository that distinguishes packages not only by name and version,
but also by source type, url, etc.
"""

def has_package(self, package: Package) -> bool:
return any(p == package for p in self.packages)

def remove_package(self, package: Package) -> None:
index = None
for i, repo_package in enumerate(self.packages):
if repo_package == package:
index = i
break

if index is not None:
del self._packages[index]
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[[package]]
name = "demo"
version = "0.1.0"
description = ""
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"

[package.source]
type = "url"
url = "https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl"

[package.dependencies]
pendulum = ">=1.4.4"

[package.extras]
bar = ["tomlkit"]
foo = ["cleo"]

[[package]]
name = "demo"
version = "0.1.0"
description = ""
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"

[package.source]
type = "url"
url = "https://python-poetry.org/distributions/demo-0.1.0.tar.gz"

[package.dependencies]
pendulum = ">=1.4.4"

[package.extras]
bar = ["tomlkit"]
foo = ["cleo"]

[[package]]
name = "pendulum"
version = "1.4.4"
description = ""
category = "main"
optional = false
python-versions = "*"

[metadata]
python-versions = "*"
lock-version = "1.1"
content-hash = "123456789"

[metadata.files]
demo = []
pendulum = []
50 changes: 50 additions & 0 deletions tests/installation/test_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2231,6 +2231,56 @@ def test_run_installs_with_url_file(
assert installer.executor.installations_count == 2


@pytest.mark.parametrize("env_platform", ["linux", "win32"])
def test_run_installs_with_same_version_url_files(
pool: Pool,
locker: Locker,
installed: CustomInstalledRepository,
config: Config,
repo: Repository,
package: ProjectPackage,
env_platform: str,
) -> None:
urls = {
"linux": "https://python-poetry.org/distributions/demo-0.1.0.tar.gz",
"win32": (
"https://python-poetry.org/distributions/demo-0.1.0-py2.py3-none-any.whl"
),
}
for platform, url in urls.items():
package.add_dependency(
Factory.create_dependency(
"demo",
{"url": url, "markers": f"sys_platform == '{platform}'"},
)
)
repo.add_package(get_package("pendulum", "1.4.4"))

installer = Installer(
NullIO(),
MockEnv(platform=env_platform),
package,
locker,
pool,
config,
installed=installed,
executor=Executor(
MockEnv(platform=env_platform),
pool,
config,
NullIO(),
),
)
installer.use_executor(True)
installer.run()

expected = fixture("with-same-version-url-dependencies")
assert locker.written_data == expected
assert installer.executor.installations_count == 2
demo_package = next(p for p in installer.executor.installations if p.name == "demo")
assert demo_package.source_url == urls[env_platform]


def test_installer_uses_prereleases_if_they_are_compatible(
installer: Installer, locker: Locker, package: ProjectPackage, repo: Repository
):
Expand Down
46 changes: 45 additions & 1 deletion tests/packages/test_locker.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,26 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage):
source_reference="develop",
source_resolved_reference="123456",
)
packages = [package_a2, package_a, get_package("B", "1.2"), package_git]
package_url_linux = Package(
"url-package",
"1.0",
source_type="url",
source_url="https://example.org/url-package-1.0-cp39-manylinux_2_17_x86_64.whl",
)
package_url_win32 = Package(
"url-package",
"1.0",
source_type="url",
source_url="https://example.org/url-package-1.0-cp39-win_amd64.whl",
)
packages = [
package_a2,
package_a,
get_package("B", "1.2"),
package_git,
package_url_win32,
package_url_linux,
]

locker.set_lock_data(root, packages)

Expand Down Expand Up @@ -105,6 +124,30 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage):
reference = "develop"
resolved_reference = "123456"

[[package]]
name = "url-package"
version = "1.0"
description = ""
category = "main"
optional = false
python-versions = "*"

[package.source]
type = "url"
url = "https://example.org/url-package-1.0-cp39-manylinux_2_17_x86_64.whl"

[[package]]
name = "url-package"
version = "1.0"
description = ""
category = "main"
optional = false
python-versions = "*"

[package.source]
type = "url"
url = "https://example.org/url-package-1.0-cp39-win_amd64.whl"

[metadata]
lock-version = "1.1"
python-versions = "*"
Expand All @@ -118,6 +161,7 @@ def test_lock_file_data_is_ordered(locker: Locker, root: ProjectPackage):
]
B = []
git-package = []
url-package = []
"""

assert content == expected
Expand Down
Loading