Skip to content

Commit a1060bc

Browse files
committed
locker: improve nested marker propagation
Resolves: #3160
1 parent 946393c commit a1060bc

File tree

2 files changed

+86
-25
lines changed

2 files changed

+86
-25
lines changed

poetry/packages/locker.py

+27-25
Original file line numberDiff line numberDiff line change
@@ -215,10 +215,18 @@ def __get_locked_package(
215215

216216
for dependency in project_requires:
217217
dependency = deepcopy(dependency)
218-
if pinned_versions:
219-
locked_package = __get_locked_package(dependency)
220-
if locked_package:
221-
dependency.set_constraint(locked_package.to_dependency().constraint)
218+
locked_package = __get_locked_package(dependency)
219+
if locked_package:
220+
locked_dependency = locked_package.to_dependency()
221+
locked_dependency.marker = dependency.marker.intersect(
222+
locked_package.marker
223+
)
224+
225+
if not pinned_versions:
226+
locked_dependency.set_constraint(dependency.constraint)
227+
228+
dependency = locked_dependency
229+
222230
project_level_dependencies.add(dependency.name)
223231
dependencies.append(dependency)
224232

@@ -228,7 +236,9 @@ def __get_locked_package(
228236

229237
nested_dependencies = dict()
230238

231-
def __walk_level(__dependencies): # type: (List[Dependency]) -> None
239+
def __walk_level(
240+
__dependencies, __level
241+
): # type: (List[Dependency], int) -> None
232242
if not __dependencies:
233243
return
234244

@@ -239,18 +249,19 @@ def __walk_level(__dependencies): # type: (List[Dependency]) -> None
239249

240250
if __locked_package:
241251
for require in __locked_package.requires:
242-
if not require.marker.is_empty():
252+
if require.marker.is_empty():
253+
require.marker = requirement.marker
254+
else:
243255
require.marker = require.marker.intersect(
244256
requirement.marker
245257
)
246-
else:
247-
require.marker = requirement.marker
258+
248259
require.marker = require.marker.intersect(
249260
__locked_package.marker
250261
)
251262
_next_level.append(require)
252263

253-
if requirement.name in project_level_dependencies:
264+
if requirement.name in project_level_dependencies and __level == 0:
254265
# project level dependencies take precedence
255266
continue
256267

@@ -272,20 +283,11 @@ def __walk_level(__dependencies): # type: (List[Dependency]) -> None
272283
)
273284

274285
# dependencies use extra to indicate that it was activated via parent
275-
# package's extras
276-
marker = requirement.marker.without_extras()
277-
for project_requirement in project_requires:
278-
if (
279-
pkg.name == project_requirement.name
280-
and project_requirement.constraint.allows(pkg.version)
281-
):
282-
requirement.marker = marker.intersect(
283-
project_requirement.marker
284-
)
285-
break
286-
else:
287-
# this dependency was not from a project requirement
288-
requirement.marker = marker.intersect(pkg.marker)
286+
# package's extras, this is not required for nested exports as we assume
287+
# the resolver already selected this dependency
288+
requirement.marker = requirement.marker.without_extras().intersect(
289+
pkg.marker
290+
)
289291

290292
key = (requirement.name, requirement.pretty_constraint)
291293
if key not in nested_dependencies:
@@ -295,9 +297,9 @@ def __walk_level(__dependencies): # type: (List[Dependency]) -> None
295297
key
296298
].marker.intersect(requirement.marker)
297299

298-
return __walk_level(_next_level)
300+
return __walk_level(_next_level, __level + 1)
299301

300-
__walk_level(dependencies)
302+
__walk_level(dependencies, 0)
301303

302304
return sorted(
303305
itertools.chain(dependencies, nested_dependencies.values()),

tests/utils/test_exporter.py

+59
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,65 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers(
256256
assert expected == {}
257257

258258

259+
def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers_any(
260+
tmp_dir, poetry
261+
):
262+
poetry.locker.mock_lock_data(
263+
{
264+
"package": [
265+
{
266+
"name": "a",
267+
"version": "1.2.3",
268+
"category": "main",
269+
"optional": False,
270+
"python-versions": "*",
271+
},
272+
{
273+
"name": "b",
274+
"version": "4.5.6",
275+
"category": "dev",
276+
"optional": False,
277+
"python-versions": "*",
278+
"dependencies": {"a": ">=1.2.3"},
279+
},
280+
],
281+
"metadata": {
282+
"python-versions": "*",
283+
"content-hash": "123456789",
284+
"hashes": {"a": [], "b": []},
285+
},
286+
}
287+
)
288+
289+
poetry.package.requires = [
290+
Factory.create_dependency(
291+
name="a", constraint=dict(version="^1.2.3", python="<3.8")
292+
),
293+
]
294+
poetry.package.dev_requires = [
295+
Factory.create_dependency(
296+
name="b", constraint=dict(version="^4.5.6"), category="dev"
297+
),
298+
Factory.create_dependency(name="a", constraint=dict(version="^1.2.3")),
299+
]
300+
301+
exporter = Exporter(poetry)
302+
303+
exporter.export("requirements.txt", Path(tmp_dir), "requirements.txt", dev=True)
304+
305+
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
306+
content = f.read()
307+
308+
assert (
309+
content
310+
== """\
311+
a==1.2.3
312+
a==1.2.3; python_version < "3.8"
313+
b==4.5.6
314+
"""
315+
)
316+
317+
259318
def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes(
260319
tmp_dir, poetry
261320
):

0 commit comments

Comments
 (0)