diff --git a/poetry/packages/locker.py b/poetry/packages/locker.py index 2a82fef816a..1e428f84d4e 100644 --- a/poetry/packages/locker.py +++ b/poetry/packages/locker.py @@ -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) @@ -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(), ) diff --git a/tests/utils/test_exporter.py b/tests/utils/test_exporter.py index a75fb3da502..15d8dcc3ccb 100644 --- a/tests/utils/test_exporter.py +++ b/tests/utils/test_exporter.py @@ -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 @@ -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 ):