Skip to content

Commit 9461797

Browse files
committed
locker: propagate cumulative markers to nested deps
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
1 parent 2ca46d0 commit 9461797

File tree

2 files changed

+106
-9
lines changed

2 files changed

+106
-9
lines changed

poetry/packages/locker.py

+39-9
Original file line numberDiff line numberDiff line change
@@ -226,19 +226,40 @@ def __get_locked_package(
226226
# return only with project level dependencies
227227
return dependencies
228228

229-
nested_dependencies = list()
229+
nested_dependencies = dict()
230+
231+
def __walk_level(__dependencies): # type: (List[Dependency]) -> None
232+
if not __dependencies:
233+
return
234+
235+
_next_level = []
236+
237+
for requirement in __dependencies:
238+
__locked_package = __get_locked_package(requirement)
239+
240+
if __locked_package:
241+
for require in __locked_package.requires:
242+
if not require.marker.is_empty():
243+
require.marker = require.marker.intersect(
244+
requirement.marker
245+
)
246+
else:
247+
require.marker = requirement.marker
248+
require.marker = require.marker.intersect(
249+
__locked_package.marker
250+
)
251+
_next_level.append(require)
230252

231-
for pkg in packages: # type: Package
232-
for requirement in pkg.requires: # type: Dependency
233253
if requirement.name in project_level_dependencies:
234254
# project level dependencies take precedence
235255
continue
236256

237-
locked_package = __get_locked_package(requirement)
238-
if locked_package:
257+
if __locked_package:
239258
# create dependency from locked package to retain dependency metadata
240259
# if this is not done, we can end-up with incorrect nested dependencies
241-
requirement = locked_package.to_dependency()
260+
marker = requirement.marker
261+
requirement = __locked_package.to_dependency()
262+
requirement.marker = requirement.marker.intersect(marker)
242263
else:
243264
# we make a copy to avoid any side-effects
244265
requirement = deepcopy(requirement)
@@ -266,11 +287,20 @@ def __get_locked_package(
266287
# this dependency was not from a project requirement
267288
requirement.marker = marker.intersect(pkg.marker)
268289

269-
if requirement not in nested_dependencies:
270-
nested_dependencies.append(requirement)
290+
key = (requirement.name, requirement.pretty_constraint)
291+
if key not in nested_dependencies:
292+
nested_dependencies[key] = requirement
293+
else:
294+
nested_dependencies[key].marker = nested_dependencies[
295+
key
296+
].marker.intersect(requirement.marker)
297+
298+
return __walk_level(_next_level)
299+
300+
__walk_level(dependencies)
271301

272302
return sorted(
273-
itertools.chain(dependencies, nested_dependencies),
303+
itertools.chain(dependencies, nested_dependencies.values()),
274304
key=lambda x: x.name.lower(),
275305
)
276306

tests/utils/test_exporter.py

+67
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,73 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_markers
175175
assert expected == content
176176

177177

178+
def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers(
179+
tmp_dir, poetry
180+
):
181+
poetry.locker.mock_lock_data(
182+
{
183+
"package": [
184+
{
185+
"name": "a",
186+
"version": "1.2.3",
187+
"category": "main",
188+
"optional": False,
189+
"python-versions": "*",
190+
"marker": "python_version < '3.7'",
191+
"dependencies": {"b": ">=0.0.0", "c": ">=0.0.0"},
192+
},
193+
{
194+
"name": "b",
195+
"version": "4.5.6",
196+
"category": "main",
197+
"optional": False,
198+
"python-versions": "*",
199+
"marker": "platform_system == 'Windows'",
200+
"dependencies": {"d": ">=0.0.0"},
201+
},
202+
{
203+
"name": "c",
204+
"version": "7.8.9",
205+
"category": "main",
206+
"optional": False,
207+
"python-versions": "*",
208+
"marker": "sys_platform == 'win32'",
209+
"dependencies": {"d": ">=0.0.0"},
210+
},
211+
{
212+
"name": "d",
213+
"version": "0.0.1",
214+
"category": "main",
215+
"optional": False,
216+
"python-versions": "*",
217+
},
218+
],
219+
"metadata": {
220+
"python-versions": "*",
221+
"content-hash": "123456789",
222+
"hashes": {"a": [], "b": [], "c": [], "d": []},
223+
},
224+
}
225+
)
226+
set_package_requires(poetry, skip={"b", "c", "d"})
227+
228+
exporter = Exporter(poetry)
229+
230+
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt")
231+
232+
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
233+
content = f.read()
234+
235+
expected = """\
236+
a==1.2.3; python_version < "3.7"
237+
b==4.5.6; platform_system == "Windows" and python_version < "3.7"
238+
c==7.8.9; sys_platform == "win32" and python_version < "3.7"
239+
d==0.0.1; python_version < "3.7" and platform_system == "Windows" and sys_platform == "win32"
240+
"""
241+
242+
assert expected == content
243+
244+
178245
def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes(
179246
tmp_dir, poetry
180247
):

0 commit comments

Comments
 (0)