From 8f53ecd277fca551bb1756ae6a8d4b291eea0890 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Sun, 18 Oct 2020 00:14:48 +0200 Subject: [PATCH] locker: handle cyclic dependencies during walk Resolves: #3213 --- poetry/packages/locker.py | 52 ++++++++++++++++----------------- tests/utils/test_exporter.py | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 27 deletions(-) diff --git a/poetry/packages/locker.py b/poetry/packages/locker.py index 9dd75e66519..de4ad4cdb45 100644 --- a/poetry/packages/locker.py +++ b/poetry/packages/locker.py @@ -217,45 +217,43 @@ def __walk_dependency_level( next_level_dependencies = [] for requirement in dependencies: + key = (requirement.name, requirement.pretty_constraint) locked_package = cls.__get_locked_package(requirement, packages_by_name) if locked_package: - for require in locked_package.requires: - if require.marker.is_empty(): - require.marker = requirement.marker - else: - require.marker = require.marker.intersect(requirement.marker) + # create dependency from locked package to retain dependency metadata + # if this is not done, we can end-up with incorrect nested dependencies + marker = requirement.marker + requirement = locked_package.to_dependency() + requirement.marker = requirement.marker.intersect(marker) + + key = (requirement.name, requirement.pretty_constraint) + + if pinned_versions: + requirement.set_constraint( + locked_package.to_dependency().constraint + ) + + if key not in nested_dependencies: + for require in locked_package.requires: + if require.marker.is_empty(): + require.marker = requirement.marker + else: + require.marker = require.marker.intersect( + requirement.marker + ) - require.marker = require.marker.intersect(locked_package.marker) - next_level_dependencies.append(require) + require.marker = require.marker.intersect(locked_package.marker) + next_level_dependencies.append(require) if requirement.name in project_level_dependencies and level == 0: # project level dependencies take precedence continue - 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 - marker = requirement.marker - requirement = locked_package.to_dependency() - requirement.marker = requirement.marker.intersect(marker) - else: + if not locked_package: # we make a copy to avoid any side-effects requirement = deepcopy(requirement) - if pinned_versions: - requirement.set_constraint( - cls.__get_locked_package(requirement, packages_by_name) - .to_dependency() - .constraint - ) - - # dependencies use extra to indicate that it was activated via parent - # package's extras, this is not required for nested exports as we assume - # the resolver already selected this dependency - requirement.marker = requirement.marker.without_extras() - - key = (requirement.name, requirement.pretty_constraint) if key not in nested_dependencies: nested_dependencies[key] = requirement else: diff --git a/tests/utils/test_exporter.py b/tests/utils/test_exporter.py index d810bb8b08b..3074f5459a9 100644 --- a/tests/utils/test_exporter.py +++ b/tests/utils/test_exporter.py @@ -696,6 +696,62 @@ def test_exporter_can_export_requirements_txt_with_nested_packages(tmp_dir, poet assert expected == content +def test_exporter_can_export_requirements_txt_with_nested_packages_cyclic( + tmp_dir, poetry +): + poetry.locker.mock_lock_data( + { + "package": [ + { + "name": "foo", + "version": "1.2.3", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": {"bar": {"version": "4.5.6"}}, + }, + { + "name": "bar", + "version": "4.5.6", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": {"baz": {"version": "7.8.9"}}, + }, + { + "name": "baz", + "version": "7.8.9", + "category": "main", + "optional": False, + "python-versions": "*", + "dependencies": {"foo": {"version": "1.2.3"}}, + }, + ], + "metadata": { + "python-versions": "*", + "content-hash": "123456789", + "hashes": {"foo": [], "bar": [], "baz": []}, + }, + } + ) + set_package_requires(poetry, skip={"bar", "baz"}) + + 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 = """\ +bar==4.5.6 +baz==7.8.9 +foo==1.2.3 +""" + + assert expected == content + + def test_exporter_can_export_requirements_txt_with_git_packages_and_markers( tmp_dir, poetry ):