Skip to content

Commit

Permalink
locker: propagate cumulative markers to nested deps
Browse files Browse the repository at this point in the history
This change ensures that markers are propagated from top level
dependencies to the deepest level by walking top to bottom instead of
iterating over all available packages.

In addition, we also compress any dependencies with the same name and
constraint to provide a more concise representation.

Resolves: #3112
  • Loading branch information
abn committed Oct 9, 2020
1 parent 2ca46d0 commit 946393c
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 9 deletions.
48 changes: 39 additions & 9 deletions poetry/packages/locker.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,19 +226,40 @@ def __get_locked_package(
# return only with project level dependencies
return dependencies

nested_dependencies = list()
nested_dependencies = dict()

def __walk_level(__dependencies): # type: (List[Dependency]) -> None
if not __dependencies:
return

_next_level = []

for requirement in __dependencies:
__locked_package = __get_locked_package(requirement)

if __locked_package:
for require in __locked_package.requires:
if not require.marker.is_empty():
require.marker = require.marker.intersect(
requirement.marker
)
else:
require.marker = requirement.marker
require.marker = require.marker.intersect(
__locked_package.marker
)
_next_level.append(require)

for pkg in packages: # type: Package
for requirement in pkg.requires: # type: Dependency
if requirement.name in project_level_dependencies:
# project level dependencies take precedence
continue

locked_package = __get_locked_package(requirement)
if locked_package:
if __locked_package:
# create dependency from locked package to retain dependency metadata
# if this is not done, we can end-up with incorrect nested dependencies
requirement = locked_package.to_dependency()
marker = requirement.marker
requirement = __locked_package.to_dependency()
requirement.marker = requirement.marker.intersect(marker)
else:
# we make a copy to avoid any side-effects
requirement = deepcopy(requirement)
Expand Down Expand Up @@ -266,11 +287,20 @@ def __get_locked_package(
# this dependency was not from a project requirement
requirement.marker = marker.intersect(pkg.marker)

if requirement not in nested_dependencies:
nested_dependencies.append(requirement)
key = (requirement.name, requirement.pretty_constraint)
if key not in nested_dependencies:
nested_dependencies[key] = requirement
else:
nested_dependencies[key].marker = nested_dependencies[
key
].marker.intersect(requirement.marker)

return __walk_level(_next_level)

__walk_level(dependencies)

return sorted(
itertools.chain(dependencies, nested_dependencies),
itertools.chain(dependencies, nested_dependencies.values()),
key=lambda x: x.name.lower(),
)

Expand Down
81 changes: 81 additions & 0 deletions tests/utils/test_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest

from poetry.core.packages import dependency_from_pep_508
from poetry.core.toml.file import TOMLFile
from poetry.factory import Factory
from poetry.packages import Locker as BaseLocker
Expand Down Expand Up @@ -175,6 +176,86 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_markers
assert expected == content


def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers(
tmp_dir, poetry
):
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "a",
"version": "1.2.3",
"category": "main",
"optional": False,
"python-versions": "*",
"marker": "python_version < '3.7'",
"dependencies": {"b": ">=0.0.0", "c": ">=0.0.0"},
},
{
"name": "b",
"version": "4.5.6",
"category": "main",
"optional": False,
"python-versions": "*",
"marker": "platform_system == 'Windows'",
"dependencies": {"d": ">=0.0.0"},
},
{
"name": "c",
"version": "7.8.9",
"category": "main",
"optional": False,
"python-versions": "*",
"marker": "sys_platform == 'win32'",
"dependencies": {"d": ">=0.0.0"},
},
{
"name": "d",
"version": "0.0.1",
"category": "main",
"optional": False,
"python-versions": "*",
},
],
"metadata": {
"python-versions": "*",
"content-hash": "123456789",
"hashes": {"a": [], "b": [], "c": [], "d": []},
},
}
)
set_package_requires(poetry, skip={"b", "c", "d"})

exporter = Exporter(poetry)

exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")

with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read()

expected = {
"a": dependency_from_pep_508("a==1.2.3; python_version < '3.7'"),
"b": dependency_from_pep_508(
"b==4.5.6; platform_system == 'Windows' and python_version < '3.7'"
),
"c": dependency_from_pep_508(
"c==7.8.9; sys_platform == 'win32' and python_version < '3.7'"
),
"d": dependency_from_pep_508(
"d==0.0.1; python_version < '3.7' and platform_system == 'Windows' and sys_platform == 'win32'"
),
}

for line in content.strip().split("\n"):
dependency = dependency_from_pep_508(line)
assert dependency.name in expected
expected_dependency = expected.pop(dependency.name)
assert dependency == expected_dependency
assert dependency.marker == expected_dependency.marker

assert expected == {}


def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes(
tmp_dir, poetry
):
Expand Down

0 comments on commit 946393c

Please sign in to comment.