Commit fb13b3a6 by David Hotham Committed by GitHub

locker: refactor dependency walk logic

Resolves: #5141
parent eb27f816
...@@ -32,6 +32,8 @@ from poetry.utils.extras import get_extra_package_names ...@@ -32,6 +32,8 @@ from poetry.utils.extras import get_extra_package_names
if TYPE_CHECKING: if TYPE_CHECKING:
from poetry.core.semver.version_constraint import VersionConstraint
from poetry.core.version.markers import BaseMarker
from tomlkit.items import InlineTable from tomlkit.items import InlineTable
from tomlkit.toml_document import TOMLDocument from tomlkit.toml_document import TOMLDocument
...@@ -203,69 +205,80 @@ class Locker: ...@@ -203,69 +205,80 @@ class Locker:
@staticmethod @staticmethod
def __get_locked_package( def __get_locked_package(
_dependency: Dependency, packages_by_name: dict[str, list[Package]] dependency: Dependency,
packages_by_name: dict[str, list[Package]],
decided: dict[Package, Dependency] | None = None,
) -> Package | None: ) -> Package | None:
""" """
Internal helper to identify corresponding locked package using dependency Internal helper to identify corresponding locked package using dependency
version constraints. version constraints.
""" """
for _package in packages_by_name.get(_dependency.name, []): decided = decided or {}
if _dependency.constraint.allows(_package.version):
return _package # Get the packages that are consistent with this dependency.
return None packages = [
package
for package in packages_by_name.get(dependency.name, [])
if package.python_constraint.allows_all(dependency.python_constraint)
and dependency.constraint.allows(package.version)
]
# If we've previously made a choice that is compatible with the current
# requirement, stick with it.
for package in packages:
old_decision = decided.get(package)
if (
old_decision is not None
and not old_decision.marker.intersect(dependency.marker).is_empty()
):
return package
return next(iter(packages), None)
@classmethod @classmethod
def __walk_dependency_level( def __walk_dependencies(
cls, cls,
dependencies: list[Dependency], dependencies: list[Dependency],
level: int,
pinned_versions: bool,
packages_by_name: dict[str, list[Package]], packages_by_name: dict[str, list[Package]],
project_level_dependencies: set[str], ) -> dict[Package, Dependency]:
nested_dependencies: dict[tuple[str, str], Dependency], nested_dependencies: dict[Package, Dependency] = {}
) -> dict[tuple[str, str], Dependency]:
if not dependencies:
return nested_dependencies
next_level_dependencies = [] visited: set[tuple[Dependency, BaseMarker]] = set()
while dependencies:
requirement = dependencies.pop(0)
if (requirement, requirement.marker) in visited:
continue
visited.add((requirement, requirement.marker))
for requirement in dependencies: locked_package = cls.__get_locked_package(
key = (requirement.name, requirement.pretty_constraint) requirement, packages_by_name, nested_dependencies
locked_package = cls.__get_locked_package(requirement, packages_by_name) )
if not locked_package:
raise RuntimeError(f"Dependency walk failed at {requirement}")
if locked_package:
# create dependency from locked package to retain dependency metadata # create dependency from locked package to retain dependency metadata
# if this is not done, we can end-up with incorrect nested dependencies # if this is not done, we can end-up with incorrect nested dependencies
constraint = requirement.constraint constraint = requirement.constraint
pretty_constraint = requirement.pretty_constraint
marker = requirement.marker marker = requirement.marker
extras = requirement.extras
requirement = locked_package.to_dependency() requirement = locked_package.to_dependency()
requirement.marker = requirement.marker.intersect(marker) requirement.marker = requirement.marker.intersect(marker)
key = (requirement.name, pretty_constraint)
if not pinned_versions:
requirement.set_constraint(constraint) requirement.set_constraint(constraint)
for require in locked_package.requires: for require in locked_package.requires:
if require.marker.is_empty(): if require.in_extras and extras.isdisjoint(require.in_extras):
require.marker = requirement.marker
else:
require.marker = require.marker.intersect(requirement.marker)
require.marker = require.marker.intersect(locked_package.marker)
if key not in nested_dependencies:
next_level_dependencies.append(require)
if requirement.name in project_level_dependencies and level == 0:
# project level dependencies take precedence
continue continue
if not locked_package: require = deepcopy(require)
# we make a copy to avoid any side-effects require.marker = require.marker.intersect(
requirement = deepcopy(requirement) requirement.marker.without_extras()
)
if not require.marker.is_empty():
dependencies.append(require)
key = locked_package
if key not in nested_dependencies: if key not in nested_dependencies:
nested_dependencies[key] = requirement nested_dependencies[key] = requirement
else: else:
...@@ -273,82 +286,49 @@ class Locker: ...@@ -273,82 +286,49 @@ class Locker:
requirement.marker requirement.marker
) )
return cls.__walk_dependency_level( return nested_dependencies
dependencies=next_level_dependencies,
level=level + 1,
pinned_versions=pinned_versions,
packages_by_name=packages_by_name,
project_level_dependencies=project_level_dependencies,
nested_dependencies=nested_dependencies,
)
@classmethod @classmethod
def get_project_dependencies( def get_project_dependencies(
cls, cls,
project_requires: list[Dependency], project_requires: list[Dependency],
locked_packages: list[Package], locked_packages: list[Package],
pinned_versions: bool = False, ) -> Iterable[tuple[Package, Dependency]]:
with_nested: bool = False,
) -> Iterable[Dependency]:
# group packages entries by name, this is required because requirement might use # group packages entries by name, this is required because requirement might use
# different constraints # different constraints.
packages_by_name: dict[str, list[Package]] = {} packages_by_name: dict[str, list[Package]] = {}
for pkg in locked_packages: for pkg in locked_packages:
if pkg.name not in packages_by_name: if pkg.name not in packages_by_name:
packages_by_name[pkg.name] = [] packages_by_name[pkg.name] = []
packages_by_name[pkg.name].append(pkg) packages_by_name[pkg.name].append(pkg)
project_level_dependencies = set() # Put higher versions first so that we prefer them.
dependencies = [] for packages in packages_by_name.values():
packages.sort(key=lambda package: package.version, reverse=True)
for dependency in project_requires:
dependency = deepcopy(dependency)
locked_package = cls.__get_locked_package(dependency, packages_by_name)
if locked_package:
locked_dependency = locked_package.to_dependency()
locked_dependency.marker = dependency.marker.intersect(
locked_package.marker
)
if not pinned_versions:
locked_dependency.set_constraint(dependency.constraint)
dependency = locked_dependency
project_level_dependencies.add(dependency.name) nested_dependencies = cls.__walk_dependencies(
dependencies.append(dependency) dependencies=project_requires,
if not with_nested:
# return only with project level dependencies
return dependencies
nested_dependencies = cls.__walk_dependency_level(
dependencies=dependencies,
level=0,
pinned_versions=pinned_versions,
packages_by_name=packages_by_name, packages_by_name=packages_by_name,
project_level_dependencies=project_level_dependencies,
nested_dependencies={},
) )
# Merge same dependencies using marker union return nested_dependencies.items()
for requirement in dependencies:
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.union(
requirement.marker
)
return sorted(nested_dependencies.values(), key=lambda x: x.name.lower())
def get_project_dependency_packages( def get_project_dependency_packages(
self, self,
project_requires: list[Dependency], project_requires: list[Dependency],
project_python_marker: VersionConstraint | None = None,
dev: bool = False, dev: bool = False,
extras: bool | Sequence[str] | None = None, extras: bool | Sequence[str] | None = None,
) -> Iterator[DependencyPackage]: ) -> Iterator[DependencyPackage]:
# Apply the project python marker to all requirements.
if project_python_marker is not None:
marked_requires: list[Dependency] = []
for require in project_requires:
require = deepcopy(require)
require.marker = require.marker.intersect(project_python_marker)
marked_requires.append(require)
project_requires = marked_requires
repository = self.locked_repository(with_dev_reqs=dev) repository = self.locked_repository(with_dev_reqs=dev)
# Build a set of all packages required by our selected extras # Build a set of all packages required by our selected extras
...@@ -379,16 +359,10 @@ class Locker: ...@@ -379,16 +359,10 @@ class Locker:
selected.append(dependency) selected.append(dependency)
for dependency in self.get_project_dependencies( for package, dependency in self.get_project_dependencies(
project_requires=selected, project_requires=selected,
locked_packages=repository.packages, locked_packages=repository.packages,
with_nested=True,
): ):
try:
package = repository.find_packages(dependency=dependency)[0]
except IndexError:
continue
for extra in dependency.extras: for extra in dependency.extras:
package.requires_extras.append(extra) package.requires_extras.append(extra)
......
from __future__ import annotations from __future__ import annotations
import itertools
import urllib.parse import urllib.parse
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
...@@ -70,21 +69,22 @@ class Exporter: ...@@ -70,21 +69,22 @@ class Exporter:
content = "" content = ""
dependency_lines = set() dependency_lines = set()
for package, groups in itertools.groupby( # Get project dependencies.
self._poetry.locker.get_project_dependency_packages( root_package = (
project_requires=self._poetry.package.all_requires, self._poetry.package.clone()
if dev
else self._poetry.package.with_dependency_groups(["default"], only=True)
)
for dependency_package in self._poetry.locker.get_project_dependency_packages(
project_requires=root_package.all_requires,
project_python_marker=root_package.python_marker,
dev=dev, dev=dev,
extras=extras, extras=extras,
),
lambda dependency_package: dependency_package.package,
): ):
line = "" line = ""
dependency_packages = list(groups) dependency = dependency_package.dependency
dependency = dependency_packages[0].dependency package = dependency_package.package
marker = dependency.marker
for dep_package in dependency_packages[1:]:
marker = marker.union(dep_package.dependency.marker)
dependency.marker = marker
if package.develop: if package.develop:
line += "-e " line += "-e "
......
...@@ -84,7 +84,9 @@ def _export_requirements(tester: CommandTester, poetry: Poetry) -> None: ...@@ -84,7 +84,9 @@ def _export_requirements(tester: CommandTester, poetry: Poetry) -> None:
assert poetry.locker.lock.exists() assert poetry.locker.lock.exists()
expected = """\ expected = """\
foo==1.0.0 foo==1.0.0 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.4" and python_version < "4.0"
""" """
assert content == expected assert content == expected
...@@ -113,7 +115,9 @@ def test_export_fails_on_invalid_format(tester: CommandTester, do_lock: None): ...@@ -113,7 +115,9 @@ def test_export_fails_on_invalid_format(tester: CommandTester, do_lock: None):
def test_export_prints_to_stdout_by_default(tester: CommandTester, do_lock: None): def test_export_prints_to_stdout_by_default(tester: CommandTester, do_lock: None):
tester.execute("--format requirements.txt") tester.execute("--format requirements.txt")
expected = """\ expected = """\
foo==1.0.0 foo==1.0.0 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.4" and python_version < "4.0"
""" """
assert tester.io.fetch_output() == expected assert tester.io.fetch_output() == expected
...@@ -123,7 +127,9 @@ def test_export_uses_requirements_txt_format_by_default( ...@@ -123,7 +127,9 @@ def test_export_uses_requirements_txt_format_by_default(
): ):
tester.execute() tester.execute()
expected = """\ expected = """\
foo==1.0.0 foo==1.0.0 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.4" and python_version < "4.0"
""" """
assert tester.io.fetch_output() == expected assert tester.io.fetch_output() == expected
...@@ -131,8 +137,12 @@ foo==1.0.0 ...@@ -131,8 +137,12 @@ foo==1.0.0
def test_export_includes_extras_by_flag(tester: CommandTester, do_lock: None): def test_export_includes_extras_by_flag(tester: CommandTester, do_lock: None):
tester.execute("--format requirements.txt --extras feature_bar") tester.execute("--format requirements.txt --extras feature_bar")
expected = """\ expected = """\
bar==1.1.0 bar==1.1.0 ;\
foo==1.0.0 python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.4" and python_version < "4.0"
foo==1.0.0 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.4" and python_version < "4.0"
""" """
assert tester.io.fetch_output() == expected assert tester.io.fetch_output() == expected
......
from __future__ import annotations from __future__ import annotations
import sys import sys
import textwrap
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
...@@ -126,8 +125,12 @@ def test_exporter_can_export_requirements_txt_with_standard_packages( ...@@ -126,8 +125,12 @@ def test_exporter_can_export_requirements_txt_with_standard_packages(
content = f.read() content = f.read()
expected = """\ expected = """\
bar==4.5.6 bar==4.5.6 ;\
foo==1.2.3 python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
""" """
assert content == expected assert content == expected
...@@ -181,9 +184,15 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_markers ...@@ -181,9 +184,15 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_markers
content = f.read() content = f.read()
expected = """\ expected = """\
bar==4.5.6 bar==4.5.6 ;\
baz==7.8.9 ; sys_platform == "win32" python_version >= "2.7" and python_version < "2.8" or\
foo==1.2.3 ; python_version < "3.7" python_version >= "3.6" and python_version < "4.0"
baz==7.8.9 ;\
python_version >= "2.7" and python_version < "2.8" and sys_platform == "win32" or\
python_version >= "3.6" and python_version < "4.0" and sys_platform == "win32"
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "3.7"
""" """
assert content == expected assert content == expected
...@@ -286,17 +295,29 @@ def test_exporter_can_export_requirements_txt_poetry(tmp_dir: str, poetry: Poetr ...@@ -286,17 +295,29 @@ def test_exporter_can_export_requirements_txt_poetry(tmp_dir: str, poetry: Poetr
# │ ├── cryptography >=2.0 # │ ├── cryptography >=2.0
# │ │ └── six >=1.4.1 # │ │ └── six >=1.4.1
# │ └── jeepney >=0.6 (circular dependency aborted here) # │ └── jeepney >=0.6 (circular dependency aborted here)
python27 = 'python_version >= "2.7" and python_version < "2.8"'
python36 = 'python_version >= "3.6" and python_version < "4.0"'
linux = 'sys_platform=="linux"'
expected = { expected = {
"poetry": Dependency.create_from_pep_508("poetry==1.1.4"), "poetry": Dependency.create_from_pep_508(
"junit-xml": Dependency.create_from_pep_508("junit-xml==1.9"), f"poetry==1.1.4; {python27} or {python36}"
"keyring": Dependency.create_from_pep_508("keyring==21.8.0"), ),
"junit-xml": Dependency.create_from_pep_508(
f"junit-xml==1.9 ; {python27} or {python36}"
),
"keyring": Dependency.create_from_pep_508(
f"keyring==21.8.0 ; {python27} or {python36}"
),
"secretstorage": Dependency.create_from_pep_508( "secretstorage": Dependency.create_from_pep_508(
"secretstorage==3.3.0 ; sys_platform=='linux'" f"secretstorage==3.3.0 ; {python27} and {linux} or {python36} and {linux}"
), ),
"cryptography": Dependency.create_from_pep_508( "cryptography": Dependency.create_from_pep_508(
"cryptography==3.2 ; sys_platform=='linux'" f"cryptography==3.2 ; {python27} and {linux} or {python36} and {linux}"
),
"six": Dependency.create_from_pep_508(
f"six==1.15.0 ; {python27} or {python36} or {python27} and {linux} or"
f" {python36} and {linux}"
), ),
"six": Dependency.create_from_pep_508("six==1.15.0"),
} }
for line in content.strip().split("\n"): for line in content.strip().split("\n"):
...@@ -368,11 +389,19 @@ def test_exporter_can_export_requirements_txt_pyinstaller(tmp_dir: str, poetry: ...@@ -368,11 +389,19 @@ def test_exporter_can_export_requirements_txt_pyinstaller(tmp_dir: str, poetry:
# ├── altgraph * dependencies into a single package. # ├── altgraph * dependencies into a single package.
# ├── macholib >=1.8 -- only on Darwin # ├── macholib >=1.8 -- only on Darwin
# │ └── altgraph >=0.15 # │ └── altgraph >=0.15
python27 = 'python_version >= "2.7" and python_version < "2.8"'
python36 = 'python_version >= "3.6" and python_version < "4.0"'
darwin = 'sys_platform=="darwin"'
expected = { expected = {
"pyinstaller": Dependency.create_from_pep_508("pyinstaller==4.0"), "pyinstaller": Dependency.create_from_pep_508(
"altgraph": Dependency.create_from_pep_508("altgraph==0.17"), f"pyinstaller==4.0 ; {python27} or {python36}"
),
"altgraph": Dependency.create_from_pep_508(
f"altgraph==0.17 ; {python27} or {python36} or {python27} and {darwin} or"
f" {python36} and {darwin}"
),
"macholib": Dependency.create_from_pep_508( "macholib": Dependency.create_from_pep_508(
"macholib==1.8 ; sys_platform == 'darwin'" f"macholib==1.8 ; {python27} and {darwin} or {python36} and {darwin}"
), ),
} }
...@@ -441,17 +470,21 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers( ...@@ -441,17 +470,21 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers(
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read() content = f.read()
python27 = 'python_version >= "2.7" and python_version < "2.8"'
python36 = 'python_version >= "3.6" and python_version < "3.7"'
windows = 'platform_system == "Windows"'
win32 = 'sys_platform == "win32"'
expected = { expected = {
"a": Dependency.create_from_pep_508("a==1.2.3 ; python_version < '3.7'"), "a": Dependency.create_from_pep_508(f"a==1.2.3 ; {python27} or {python36}"),
"b": Dependency.create_from_pep_508( "b": Dependency.create_from_pep_508(
"b==4.5.6 ; platform_system == 'Windows' and python_version < '3.7'" f"b==4.5.6 ; {python27} and {windows} or {python36} and {windows}"
), ),
"c": Dependency.create_from_pep_508( "c": Dependency.create_from_pep_508(
"c==7.8.9 ; sys_platform == 'win32' and python_version < '3.7'" f"c==7.8.9 ; {python27} and {win32} or {python36} and {win32}"
), ),
"d": Dependency.create_from_pep_508( "d": Dependency.create_from_pep_508(
"d==0.0.1 ; platform_system == 'Windows' and python_version < '3.7' or" f"d==0.0.1 ; {python27} and {windows} or {python36} and {windows} or"
" sys_platform == 'win32' and python_version < '3.7'" f" {python27} and {win32} or {python36} and {win32}"
), ),
} }
...@@ -467,7 +500,25 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers( ...@@ -467,7 +500,25 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers(
@pytest.mark.parametrize( @pytest.mark.parametrize(
["dev", "lines"], ["dev", "lines"],
[(False, ['a==1.2.3 ; python_version < "3.8"']), (True, ["a==1.2.3", "b==4.5.6"])], [
(
False,
[
'a==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "3.8"'
],
),
(
True,
[
'a==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "3.8" or python_version'
' >= "3.6" and python_version < "4.0"',
'b==4.5.6 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
],
),
],
) )
def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers_any( def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers_any(
tmp_dir: str, poetry: Poetry, dev: bool, lines: list[str] tmp_dir: str, poetry: Poetry, dev: bool, lines: list[str]
...@@ -560,9 +611,13 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes( ...@@ -560,9 +611,13 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes(
content = f.read() content = f.read()
expected = """\ expected = """\
bar==4.5.6 \\ bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890 --hash=sha256:67890
foo==1.2.3 \\ foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345 --hash=sha256:12345
""" """
...@@ -609,8 +664,12 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_ ...@@ -609,8 +664,12 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_
content = f.read() content = f.read()
expected = """\ expected = """\
bar==4.5.6 bar==4.5.6 ;\
foo==1.2.3 python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
""" """
assert content == expected assert content == expected
...@@ -654,7 +713,9 @@ def test_exporter_exports_requirements_txt_without_dev_packages_by_default( ...@@ -654,7 +713,9 @@ def test_exporter_exports_requirements_txt_without_dev_packages_by_default(
content = f.read() content = f.read()
expected = """\ expected = """\
foo==1.2.3 \\ foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345 --hash=sha256:12345
""" """
...@@ -699,9 +760,13 @@ def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in( ...@@ -699,9 +760,13 @@ def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in(
content = f.read() content = f.read()
expected = """\ expected = """\
bar==4.5.6 \\ bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890 --hash=sha256:67890
foo==1.2.3 \\ foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345 --hash=sha256:12345
""" """
...@@ -746,7 +811,9 @@ def test_exporter_exports_requirements_txt_without_optional_packages( ...@@ -746,7 +811,9 @@ def test_exporter_exports_requirements_txt_without_optional_packages(
content = f.read() content = f.read()
expected = """\ expected = """\
foo==1.2.3 \\ foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345 --hash=sha256:12345
""" """
...@@ -756,10 +823,42 @@ foo==1.2.3 \\ ...@@ -756,10 +823,42 @@ foo==1.2.3 \\
@pytest.mark.parametrize( @pytest.mark.parametrize(
["extras", "lines"], ["extras", "lines"],
[ [
(None, ["foo==1.2.3"]), (
(False, ["foo==1.2.3"]), None,
(True, ["bar==4.5.6", "foo==1.2.3", "spam==0.1.0"]), [
(["feature_bar"], ["bar==4.5.6", "foo==1.2.3", "spam==0.1.0"]), 'foo==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"'
],
),
(
False,
[
'foo==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"'
],
),
(
True,
[
'bar==4.5.6 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'foo==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'spam==0.1.0 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
],
),
(
["feature_bar"],
[
'bar==4.5.6 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'foo==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'spam==0.1.0 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
],
),
], ],
) )
def test_exporter_exports_requirements_txt_with_optional_packages( def test_exporter_exports_requirements_txt_with_optional_packages(
...@@ -859,7 +958,9 @@ def test_exporter_can_export_requirements_txt_with_git_packages( ...@@ -859,7 +958,9 @@ def test_exporter_can_export_requirements_txt_with_git_packages(
content = f.read() content = f.read()
expected = """\ expected = """\
foo @ git+https://github.com/foo/foo.git@123456 foo @ git+https://github.com/foo/foo.git@123456 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
""" """
assert content == expected assert content == expected
...@@ -909,8 +1010,12 @@ def test_exporter_can_export_requirements_txt_with_nested_packages( ...@@ -909,8 +1010,12 @@ def test_exporter_can_export_requirements_txt_with_nested_packages(
content = f.read() content = f.read()
expected = """\ expected = """\
bar==4.5.6 bar==4.5.6 ;\
foo @ git+https://github.com/foo/foo.git@123456 python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
foo @ git+https://github.com/foo/foo.git@123456 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
""" """
assert content == expected assert content == expected
...@@ -964,9 +1069,15 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_cyclic( ...@@ -964,9 +1069,15 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_cyclic(
content = f.read() content = f.read()
expected = """\ expected = """\
bar==4.5.6 bar==4.5.6 ;\
baz==7.8.9 python_version >= "2.7" and python_version < "2.8" or\
foo==1.2.3 python_version >= "3.6" and python_version < "4.0"
baz==7.8.9 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
""" """
assert content == expected assert content == expected
...@@ -1036,13 +1147,19 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_multiple_ ...@@ -1036,13 +1147,19 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_multiple_
with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f:
content = f.read() content = f.read()
expected = textwrap.dedent( expected = """\
"""\ bar==7.8.9 ;\
bar==7.8.9 python_version >= "2.7" and python_version < "2.8" and platform_system != "Windows" or\
baz==10.11.13 ; platform_system == "Windows" python_version >= "3.6" and python_version < "4.0" and platform_system != "Windows" or\
foo==1.2.3 python_version >= "2.7" and python_version < "2.8" and platform_system == "Windows" or\
""" python_version >= "3.6" and python_version < "4.0" and platform_system == "Windows"
) baz==10.11.13 ;\
python_version >= "2.7" and python_version < "2.8" and platform_system == "Windows" or\
python_version >= "3.6" and python_version < "4.0" and platform_system == "Windows"
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
"""
assert content == expected assert content == expected
...@@ -1084,7 +1201,9 @@ def test_exporter_can_export_requirements_txt_with_git_packages_and_markers( ...@@ -1084,7 +1201,9 @@ def test_exporter_can_export_requirements_txt_with_git_packages_and_markers(
content = f.read() content = f.read()
expected = """\ expected = """\
foo @ git+https://github.com/foo/foo.git@123456 ; python_version < "3.7" foo @ git+https://github.com/foo/foo.git@123456 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "3.7"
""" """
assert content == expected assert content == expected
...@@ -1126,7 +1245,9 @@ def test_exporter_can_export_requirements_txt_with_directory_packages( ...@@ -1126,7 +1245,9 @@ def test_exporter_can_export_requirements_txt_with_directory_packages(
content = f.read() content = f.read()
expected = f"""\ expected = f"""\
foo @ {working_directory.as_uri()}/tests/fixtures/sample_project foo @ {working_directory.as_uri()}/tests/fixtures/sample_project ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
""" """
assert content == expected assert content == expected
...@@ -1192,9 +1313,15 @@ def test_exporter_can_export_requirements_txt_with_nested_directory_packages( ...@@ -1192,9 +1313,15 @@ def test_exporter_can_export_requirements_txt_with_nested_directory_packages(
content = f.read() content = f.read()
expected = f"""\ expected = f"""\
bar @ {working_directory.as_uri()}/tests/fixtures/project_with_nested_local/bar bar @ {working_directory.as_uri()}/tests/fixtures/project_with_nested_local/bar ;\
baz @ {working_directory.as_uri()}/tests/fixtures/project_with_nested_local python_version >= "2.7" and python_version < "2.8" or\
foo @ {working_directory.as_uri()}/tests/fixtures/sample_project python_version >= "3.6" and python_version < "4.0"
baz @ {working_directory.as_uri()}/tests/fixtures/project_with_nested_local ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
foo @ {working_directory.as_uri()}/tests/fixtures/sample_project ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
""" """
assert content == expected assert content == expected
...@@ -1237,8 +1364,9 @@ def test_exporter_can_export_requirements_txt_with_directory_packages_and_marker ...@@ -1237,8 +1364,9 @@ def test_exporter_can_export_requirements_txt_with_directory_packages_and_marker
content = f.read() content = f.read()
expected = f"""\ expected = f"""\
foo @ {working_directory.as_uri()}/tests/fixtures/sample_project\ foo @ {working_directory.as_uri()}/tests/fixtures/sample_project ;\
; python_version < "3.7" python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "3.7"
""" """
assert content == expected assert content == expected
...@@ -1280,7 +1408,9 @@ def test_exporter_can_export_requirements_txt_with_file_packages( ...@@ -1280,7 +1408,9 @@ def test_exporter_can_export_requirements_txt_with_file_packages(
content = f.read() content = f.read()
expected = f"""\ expected = f"""\
foo @ {working_directory.as_uri()}/tests/fixtures/distributions/demo-0.1.0.tar.gz foo @ {working_directory.as_uri()}/tests/fixtures/distributions/demo-0.1.0.tar.gz ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
""" """
assert content == expected assert content == expected
...@@ -1323,8 +1453,9 @@ def test_exporter_can_export_requirements_txt_with_file_packages_and_markers( ...@@ -1323,8 +1453,9 @@ def test_exporter_can_export_requirements_txt_with_file_packages_and_markers(
content = f.read() content = f.read()
expected = f"""\ expected = f"""\
foo @ {working_directory.as_uri()}/tests/fixtures/distributions/demo-0.1.0.tar.gz\ foo @ {working_directory.as_uri()}/tests/fixtures/distributions/demo-0.1.0.tar.gz ;\
; python_version < "3.7" python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "3.7"
""" """
assert content == expected assert content == expected
...@@ -1381,9 +1512,13 @@ def test_exporter_exports_requirements_txt_with_legacy_packages( ...@@ -1381,9 +1512,13 @@ def test_exporter_exports_requirements_txt_with_legacy_packages(
expected = """\ expected = """\
--extra-index-url https://example.com/simple --extra-index-url https://example.com/simple
bar==4.5.6 \\ bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890 --hash=sha256:67890
foo==1.2.3 \\ foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345 --hash=sha256:12345
""" """
...@@ -1439,9 +1574,13 @@ def test_exporter_exports_requirements_txt_with_url_false(tmp_dir: str, poetry: ...@@ -1439,9 +1574,13 @@ def test_exporter_exports_requirements_txt_with_url_false(tmp_dir: str, poetry:
content = f.read() content = f.read()
expected = """\ expected = """\
bar==4.5.6 \\ bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890 --hash=sha256:67890
foo==1.2.3 \\ foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345 --hash=sha256:12345
""" """
...@@ -1492,7 +1631,9 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_trusted_host( ...@@ -1492,7 +1631,9 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_trusted_host(
--trusted-host example.com --trusted-host example.com
--extra-index-url http://example.com/simple --extra-index-url http://example.com/simple
bar==4.5.6 \\ bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890 --hash=sha256:67890
""" """
...@@ -1502,8 +1643,26 @@ bar==4.5.6 \\ ...@@ -1502,8 +1643,26 @@ bar==4.5.6 \\
@pytest.mark.parametrize( @pytest.mark.parametrize(
["dev", "expected"], ["dev", "expected"],
[ [
(True, ["bar==1.2.2", "baz==1.2.3", "foo==1.2.1"]), (
(False, ["bar==1.2.2", "foo==1.2.1"]), True,
[
'bar==1.2.2 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'baz==1.2.3 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'foo==1.2.1 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
],
),
(
False,
[
'bar==1.2.2 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
'foo==1.2.1 ; python_version >= "2.7" and python_version < "2.8" or'
' python_version >= "3.6" and python_version < "4.0"',
],
),
], ],
) )
def test_exporter_exports_requirements_txt_with_dev_extras( def test_exporter_exports_requirements_txt_with_dev_extras(
...@@ -1636,11 +1795,17 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_so ...@@ -1636,11 +1795,17 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_so
--extra-index-url https://example.com/simple --extra-index-url https://example.com/simple
--extra-index-url https://foobaz.com/simple --extra-index-url https://foobaz.com/simple
bar==4.5.6 \\ bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890 --hash=sha256:67890
baz==7.8.9 \\ baz==7.8.9 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:24680 --hash=sha256:24680
foo==1.2.3 \\ foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345 --hash=sha256:12345
""" """
...@@ -1707,9 +1872,13 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials( ...@@ -1707,9 +1872,13 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials(
expected = """\ expected = """\
--extra-index-url https://foo:bar@example.com/simple --extra-index-url https://foo:bar@example.com/simple
bar==4.5.6 \\ bar==4.5.6 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:67890 --hash=sha256:67890
foo==1.2.3 \\ foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0" \\
--hash=sha256:12345 --hash=sha256:12345
""" """
...@@ -1752,8 +1921,248 @@ def test_exporter_exports_requirements_txt_to_standard_output( ...@@ -1752,8 +1921,248 @@ def test_exporter_exports_requirements_txt_to_standard_output(
out, err = capsys.readouterr() out, err = capsys.readouterr()
expected = """\ expected = """\
bar==4.5.6 bar==4.5.6 ;\
foo==1.2.3 python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
foo==1.2.3 ;\
python_version >= "2.7" and python_version < "2.8" or\
python_version >= "3.6" and python_version < "4.0"
"""
assert out == expected
def test_exporter_doesnt_confuse_repeated_packages(
tmp_dir: str, poetry: Poetry, capsys: CaptureFixture
):
# Testcase derived from <https://github.com/python-poetry/poetry/issues/5141>.
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "celery",
"version": "5.1.2",
"category": "main",
"optional": False,
"python-versions": "<3.7",
"dependencies": {
"click": ">=7.0,<8.0",
"click-didyoumean": ">=0.0.3",
"click-plugins": ">=1.1.1",
},
},
{
"name": "celery",
"version": "5.2.3",
"category": "main",
"optional": False,
"python-versions": ">=3.7",
"dependencies": {
"click": ">=8.0.3,<9.0",
"click-didyoumean": ">=0.0.3",
"click-plugins": ">=1.1.1",
},
},
{
"name": "click",
"version": "7.1.2",
"category": "main",
"optional": False,
"python-versions": (
">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
),
},
{
"name": "click",
"version": "8.0.3",
"category": "main",
"optional": False,
"python-versions": ">=3.6",
"dependencies": {},
},
{
"name": "click-didyoumean",
"version": "0.0.3",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"click": "*"},
},
{
"name": "click-didyoumean",
"version": "0.3.0",
"category": "main",
"optional": False,
"python-versions": ">=3.6.2,<4.0.0",
"dependencies": {"click": ">=7"},
},
{
"name": "click-plugins",
"version": "1.1.1",
"category": "main",
"optional": False,
"python-versions": "*",
"dependencies": {"click": ">=4.0"},
},
],
"metadata": {
"lock-version": "1.1",
"python-versions": "^3.6",
"content-hash": (
"832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6"
),
"hashes": {
"celery": [],
"click-didyoumean": [],
"click-plugins": [],
"click": [],
},
},
}
)
root = poetry.package.with_dependency_groups([], only=True)
root.python_versions = "^3.6"
root.add_dependency(
Factory.create_dependency(
name="celery", constraint={"version": "5.1.2", "python": "<3.7"}
)
)
root.add_dependency(
Factory.create_dependency(
name="celery", constraint={"version": "5.2.3", "python": ">=3.7"}
)
)
poetry._package = root
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), sys.stdout)
out, err = capsys.readouterr()
expected = """\
celery==5.1.2 ; python_version >= "3.6" and python_version < "3.7"
celery==5.2.3 ; python_version >= "3.7" and python_version < "4.0"
click-didyoumean==0.0.3 ; python_version >= "3.6" and python_version < "3.7"
click-didyoumean==0.3.0 ; python_version >= "3.7" and python_full_version < "4.0.0"
click-plugins==1.1.1 ;\
python_version >= "3.6" and python_version < "3.7" or\
python_version >= "3.7" and python_version < "4.0"
click==7.1.2 ; python_version >= "3.6" and python_version < "3.7"
click==8.0.3 ;\
python_version >= "3.7" and python_version < "4.0" or\
python_version >= "3.7" and python_full_version < "4.0.0"
"""
assert out == expected
def test_exporter_handles_extras_next_to_non_extras(
tmp_dir: str, poetry: Poetry, capsys: CaptureFixture
):
# Testcase similar to the solver testcase added at #5305.
poetry.locker.mock_lock_data(
{
"package": [
{
"name": "localstack",
"python-versions": "*",
"version": "1.0.0",
"category": "main",
"optional": False,
"dependencies": {
"localstack-ext": [
{"version": ">=1.0.0"},
{
"version": ">=1.0.0",
"extras": ["bar"],
"markers": 'extra == "foo"',
},
]
},
"extras": {"foo": ["localstack-ext (>=1.0.0)"]},
},
{
"name": "localstack-ext",
"python-versions": "*",
"version": "1.0.0",
"category": "main",
"optional": False,
"dependencies": {
"something": "*",
"something-else": {
"version": ">=1.0.0",
"markers": 'extra == "bar"',
},
"another-thing": {
"version": ">=1.0.0",
"markers": 'extra == "baz"',
},
},
"extras": {
"bar": ["something-else (>=1.0.0)"],
"baz": ["another-thing (>=1.0.0)"],
},
},
{
"name": "something",
"python-versions": "*",
"version": "1.0.0",
"category": "main",
"optional": False,
"dependencies": {},
},
{
"name": "something-else",
"python-versions": "*",
"version": "1.0.0",
"category": "main",
"optional": False,
"dependencies": {},
},
{
"name": "another-thing",
"python-versions": "*",
"version": "1.0.0",
"category": "main",
"optional": False,
"dependencies": {},
},
],
"metadata": {
"lock-version": "1.1",
"python-versions": "^3.6",
"content-hash": (
"832b13a88e5020c27cbcd95faa577bf0dbf054a65c023b45dc9442b640d414e6"
),
"hashes": {
"localstack": [],
"localstack-ext": [],
"something": [],
"something-else": [],
"another-thing": [],
},
},
}
)
root = poetry.package.with_dependency_groups([], only=True)
root.python_versions = "^3.6"
root.add_dependency(
Factory.create_dependency(
name="localstack", constraint={"version": "^1.0.0", "extras": ["foo"]}
)
)
poetry._package = root
exporter = Exporter(poetry)
exporter.export("requirements.txt", Path(tmp_dir), sys.stdout)
out, err = capsys.readouterr()
expected = """\
localstack-ext==1.0.0 ; python_version >= "3.6" and python_version < "4.0"
localstack==1.0.0 ; python_version >= "3.6" and python_version < "4.0"
something-else==1.0.0 ; python_version >= "3.6" and python_version < "4.0"
something==1.0.0 ; python_version >= "3.6" and python_version < "4.0"
""" """
assert out == expected assert out == expected
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment