From d76d1ce5004acf69c0c3dbceed908c3502b60034 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Mon, 7 Feb 2022 11:34:33 +0000 Subject: [PATCH] refactor poetry export --- src/poetry/packages/locker.py | 155 ++++----- src/poetry/utils/exporter.py | 32 +- tests/console/commands/test_export.py | 20 +- tests/utils/test_exporter.py | 456 +++++++++++++++++++++----- 4 files changed, 473 insertions(+), 190 deletions(-) diff --git a/src/poetry/packages/locker.py b/src/poetry/packages/locker.py index 4cf56b9ae93..44a88ca3a17 100644 --- a/src/poetry/packages/locker.py +++ b/src/poetry/packages/locker.py @@ -38,6 +38,7 @@ if TYPE_CHECKING: from tomlkit.toml_document import TOMLDocument + from poetry.core.version.markers import BaseMarker from poetry.repositories import Repository logger = logging.getLogger(__name__) @@ -204,69 +205,77 @@ def locked_repository(self, with_dev_reqs: bool = False) -> "Repository": @staticmethod def __get_locked_package( - _dependency: Dependency, packages_by_name: Dict[str, List[Package]] + dependency: Dependency, + packages_by_name: Dict[str, List[Package]], + decided: Optional[Dict[Package, Dependency]] = None, ) -> Optional[Package]: """ Internal helper to identify corresponding locked package using dependency version constraints. """ - for _package in packages_by_name.get(_dependency.name, []): - if _dependency.constraint.allows(_package.version): - return _package - return None + decided = decided or {} + + # Get the packages that are consistent with this dependency. + 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 - def __walk_dependency_level( + def __walk_dependencies( cls, dependencies: List[Dependency], - level: int, - pinned_versions: bool, packages_by_name: Dict[str, List[Package]], - project_level_dependencies: Set[str], - nested_dependencies: Dict[Tuple[str, str], Dependency], - ) -> Dict[Tuple[str, str], Dependency]: - if not dependencies: - return nested_dependencies - - 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: - # create dependency from locked package to retain dependency metadata - # if this is not done, we can end-up with incorrect nested dependencies - constraint = requirement.constraint - pretty_constraint = requirement.pretty_constraint - marker = requirement.marker - requirement = locked_package.to_dependency() - requirement.marker = requirement.marker.intersect(marker) + ) -> Dict[Package, Dependency]: + nested_dependencies: Dict[Package, Dependency] = {} - key = (requirement.name, pretty_constraint) - - if not pinned_versions: - requirement.set_constraint(constraint) + 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 require in locked_package.requires: - if require.marker.is_empty(): - require.marker = requirement.marker - else: - require.marker = require.marker.intersect(requirement.marker) + locked_package = cls.__get_locked_package( + requirement, packages_by_name, nested_dependencies + ) - require.marker = require.marker.intersect(locked_package.marker) + if not locked_package: + # Should normally be able to satisfy all requirements, but this case is + # permissible eg if we encounter a dev dependency when walking the + # non-dev dependencies. + continue - if key not in nested_dependencies: - next_level_dependencies.append(require) + # create dependency from locked package to retain dependency metadata + # if this is not done, we can end-up with incorrect nested dependencies + constraint = requirement.constraint + marker = requirement.marker + requirement = locked_package.to_dependency() + requirement.marker = requirement.marker.intersect(marker) - if requirement.name in project_level_dependencies and level == 0: - # project level dependencies take precedence - continue + requirement.set_constraint(constraint) - if not locked_package: - # we make a copy to avoid any side-effects - requirement = deepcopy(requirement) + for require in locked_package.requires: + require = deepcopy(require) + require.marker = require.marker.intersect(requirement.marker) + if not require.marker.is_empty(): + dependencies.append(require) + key = locked_package if key not in nested_dependencies: nested_dependencies[key] = requirement else: @@ -274,32 +283,26 @@ def __walk_dependency_level( requirement.marker ) - return cls.__walk_dependency_level( - 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, - ) + return nested_dependencies @classmethod def get_project_dependencies( cls, project_requires: List[Dependency], locked_packages: List[Package], - pinned_versions: bool = False, - with_nested: bool = False, - ) -> Iterable[Dependency]: + ) -> Iterable[Tuple[Package, Dependency]]: # group packages entries by name, this is required because requirement might use - # different constraints + # different constraints. packages_by_name = {} for pkg in locked_packages: if pkg.name not in packages_by_name: packages_by_name[pkg.name] = [] packages_by_name[pkg.name].append(pkg) - project_level_dependencies = set() + # Put higher versions first so that we prefer them. + for packages in packages_by_name.values(): + packages.sort(key=lambda package: package.version, reverse=True) + dependencies = [] for dependency in project_requires: @@ -311,38 +314,18 @@ def get_project_dependencies( locked_package.marker ) - if not pinned_versions: - locked_dependency.set_constraint(dependency.constraint) + locked_dependency.set_constraint(dependency.constraint) dependency = locked_dependency - project_level_dependencies.add(dependency.name) dependencies.append(dependency) - if not with_nested: - # return only with project level dependencies - return dependencies - - nested_dependencies = cls.__walk_dependency_level( + nested_dependencies = cls.__walk_dependencies( dependencies=dependencies, - level=0, - pinned_versions=pinned_versions, packages_by_name=packages_by_name, - project_level_dependencies=project_level_dependencies, - nested_dependencies={}, ) - # Merge same dependencies using marker union - 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()) + return nested_dependencies.items() def get_project_dependency_packages( self, @@ -382,16 +365,10 @@ def get_project_dependency_packages( selected.append(dependency) - for dependency in self.get_project_dependencies( + for package, dependency in self.get_project_dependencies( project_requires=selected, locked_packages=repository.packages, - with_nested=True, ): - try: - package = repository.find_packages(dependency=dependency)[0] - except IndexError: - continue - for extra in dependency.extras: package.requires_extras.append(extra) diff --git a/src/poetry/utils/exporter.py b/src/poetry/utils/exporter.py index 98070d19b23..c4be3cebb32 100644 --- a/src/poetry/utils/exporter.py +++ b/src/poetry/utils/exporter.py @@ -1,6 +1,6 @@ -import itertools import urllib.parse +from copy import deepcopy from typing import TYPE_CHECKING from typing import Optional from typing import Sequence @@ -70,21 +70,25 @@ def _export_requirements_txt( content = "" dependency_lines = set() - for package, groups in itertools.groupby( - self._poetry.locker.get_project_dependency_packages( - project_requires=self._poetry.package.all_requires, - dev=dev, - extras=extras, - ), - lambda dependency_package: dependency_package.package, + # Get project dependencies, and add the project-wide marker to them. + groups = ["dev"] if dev else [] + root_package = self._poetry.package.with_dependency_groups(groups) + project_requires = [] + for require in root_package.all_requires: + require = deepcopy(require) + require.marker = require.marker.intersect( + root_package.python_marker + ) + project_requires.append(require) + + for dependency_package in self._poetry.locker.get_project_dependency_packages( + project_requires=project_requires, + dev=dev, + extras=extras, ): line = "" - dependency_packages = list(groups) - dependency = dependency_packages[0].dependency - marker = dependency.marker - for dep_package in dependency_packages[1:]: - marker = marker.union(dep_package.dependency.marker) - dependency.marker = marker + dependency = dependency_package.dependency + package = dependency_package.package if package.develop: line += "-e " diff --git a/tests/console/commands/test_export.py b/tests/console/commands/test_export.py index 010dfd10617..1bd48397108 100644 --- a/tests/console/commands/test_export.py +++ b/tests/console/commands/test_export.py @@ -82,7 +82,9 @@ def _export_requirements(tester: "CommandTester", poetry: "Poetry") -> None: assert poetry.locker.lock.exists() 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 @@ -111,7 +113,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): tester.execute("--format requirements.txt") 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 @@ -121,7 +125,9 @@ def test_export_uses_requirements_txt_format_by_default( ): tester.execute() 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 @@ -129,8 +135,12 @@ def test_export_uses_requirements_txt_format_by_default( def test_export_includes_extras_by_flag(tester: "CommandTester", do_lock: None): tester.execute("--format requirements.txt --extras feature_bar") expected = """\ -bar==1.1.0 -foo==1.0.0 +bar==1.1.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 diff --git a/tests/utils/test_exporter.py b/tests/utils/test_exporter.py index 53dfd0a2282..fb9dff4d8ea 100644 --- a/tests/utils/test_exporter.py +++ b/tests/utils/test_exporter.py @@ -1,5 +1,4 @@ import sys -import textwrap from pathlib import Path from typing import TYPE_CHECKING @@ -129,8 +128,12 @@ def test_exporter_can_export_requirements_txt_with_standard_packages( content = f.read() expected = """\ -bar==4.5.6 -foo==1.2.3 +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" """ assert content == expected @@ -184,9 +187,15 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_markers content = f.read() expected = """\ -bar==4.5.6 -baz==7.8.9 ; sys_platform == "win32" -foo==1.2.3 ; python_version < "3.7" +bar==4.5.6 ;\ + python_version >= "2.7" and python_version < "2.8" or\ + 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 @@ -289,17 +298,29 @@ def test_exporter_can_export_requirements_txt_poetry(tmp_dir: str, poetry: "Poet # │ ├── cryptography >=2.0 # │ │ └── six >=1.4.1 # │ └── 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 = { - "poetry": Dependency.create_from_pep_508("poetry==1.1.4"), - "junit-xml": Dependency.create_from_pep_508("junit-xml==1.9"), - "keyring": Dependency.create_from_pep_508("keyring==21.8.0"), + "poetry": Dependency.create_from_pep_508( + f"poetry==1.1.4; {python27} or {python36}" + ), + "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==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==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"): @@ -373,11 +394,19 @@ def test_exporter_can_export_requirements_txt_pyinstaller( # ├── altgraph * dependencies into a single package. # ├── macholib >=1.8 -- only on Darwin # │ └── 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 = { - "pyinstaller": Dependency.create_from_pep_508("pyinstaller==4.0"), - "altgraph": Dependency.create_from_pep_508("altgraph==0.17"), + "pyinstaller": Dependency.create_from_pep_508( + 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==1.8 ; sys_platform == 'darwin'" + f"macholib==1.8 ; {python27} and {darwin} or {python36} and {darwin}" ), } @@ -446,17 +475,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: 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 = { - "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==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==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==0.0.1 ; platform_system == 'Windows' and python_version < '3.7' or" - " sys_platform == 'win32' and python_version < '3.7'" + f"d==0.0.1 ; {python27} and {windows} or {python36} and {windows} or" + f" {python27} and {win32} or {python36} and {win32}" ), } @@ -472,7 +505,25 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_markers( @pytest.mark.parametrize( ["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( tmp_dir: str, poetry: "Poetry", dev: bool, lines: List[str] @@ -565,9 +616,13 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes( content = f.read() 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 -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 """ @@ -614,8 +669,12 @@ def test_exporter_can_export_requirements_txt_with_standard_packages_and_hashes_ content = f.read() expected = """\ -bar==4.5.6 -foo==1.2.3 +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" """ assert content == expected @@ -659,7 +718,9 @@ def test_exporter_exports_requirements_txt_without_dev_packages_by_default( content = f.read() 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 """ @@ -704,9 +765,13 @@ def test_exporter_exports_requirements_txt_with_dev_packages_if_opted_in( content = f.read() 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 -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 """ @@ -751,7 +816,9 @@ def test_exporter_exports_requirements_txt_without_optional_packages( content = f.read() 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 """ @@ -761,10 +828,42 @@ def test_exporter_exports_requirements_txt_without_optional_packages( @pytest.mark.parametrize( ["extras", "lines"], [ - (None, ["foo==1.2.3"]), - (False, ["foo==1.2.3"]), - (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"]), + ( + None, + [ + '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( @@ -864,7 +963,9 @@ def test_exporter_can_export_requirements_txt_with_git_packages( content = f.read() 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 @@ -914,8 +1015,12 @@ def test_exporter_can_export_requirements_txt_with_nested_packages( content = f.read() expected = """\ -bar==4.5.6 -foo @ git+https://github.com/foo/foo.git@123456 +bar==4.5.6 ;\ + 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 @@ -969,9 +1074,15 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_cyclic( content = f.read() expected = """\ -bar==4.5.6 -baz==7.8.9 -foo==1.2.3 +bar==4.5.6 ;\ + python_version >= "2.7" and python_version < "2.8" or\ + 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 @@ -1041,26 +1152,21 @@ def test_exporter_can_export_requirements_txt_with_nested_packages_and_multiple_ with (Path(tmp_dir) / "requirements.txt").open(encoding="utf-8") as f: content = f.read() - expected = ( - # expectation for poetry-core <= 1.1.0a6 - textwrap.dedent( - """\ - bar==7.8.9 ; platform_system != "Windows" or platform_system == "Windows" - baz==10.11.13 ; platform_system == "Windows" - foo==1.2.3 - """ - ), - # expectation for poetry-core > 1.1.0a6 - textwrap.dedent( - """\ - bar==7.8.9 - baz==10.11.13 ; platform_system == "Windows" - foo==1.2.3 - """ - ), - ) + expected = """\ +bar==7.8.9 ;\ + 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" or\ + 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 in expected + assert content == expected def test_exporter_can_export_requirements_txt_with_git_packages_and_markers( @@ -1100,7 +1206,9 @@ def test_exporter_can_export_requirements_txt_with_git_packages_and_markers( content = f.read() 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 @@ -1142,7 +1250,9 @@ def test_exporter_can_export_requirements_txt_with_directory_packages( content = f.read() 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 @@ -1208,9 +1318,15 @@ def test_exporter_can_export_requirements_txt_with_nested_directory_packages( content = f.read() expected = f"""\ -bar @ {working_directory.as_uri()}/tests/fixtures/project_with_nested_local/bar -baz @ {working_directory.as_uri()}/tests/fixtures/project_with_nested_local -foo @ {working_directory.as_uri()}/tests/fixtures/sample_project +bar @ {working_directory.as_uri()}/tests/fixtures/project_with_nested_local/bar ;\ + python_version >= "2.7" and python_version < "2.8" or\ + 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 @@ -1253,8 +1369,9 @@ def test_exporter_can_export_requirements_txt_with_directory_packages_and_marker content = f.read() expected = f"""\ -foo @ {working_directory.as_uri()}/tests/fixtures/sample_project\ - ; python_version < "3.7" +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 < "3.7" """ assert content == expected @@ -1296,7 +1413,9 @@ def test_exporter_can_export_requirements_txt_with_file_packages( content = f.read() 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 @@ -1339,8 +1458,9 @@ def test_exporter_can_export_requirements_txt_with_file_packages_and_markers( content = f.read() expected = f"""\ -foo @ {working_directory.as_uri()}/tests/fixtures/distributions/demo-0.1.0.tar.gz\ - ; python_version < "3.7" +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 < "3.7" """ assert content == expected @@ -1397,9 +1517,13 @@ def test_exporter_exports_requirements_txt_with_legacy_packages( expected = """\ --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 -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 """ @@ -1457,9 +1581,13 @@ def test_exporter_exports_requirements_txt_with_url_false( content = f.read() 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 -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 """ @@ -1510,7 +1638,9 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_trusted_host( --trusted-host example.com --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 """ @@ -1520,8 +1650,26 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_trusted_host( @pytest.mark.parametrize( ["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( @@ -1654,11 +1802,17 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_duplicate_so --extra-index-url https://example.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 -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 -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 """ @@ -1725,9 +1879,13 @@ def test_exporter_exports_requirements_txt_with_legacy_packages_and_credentials( expected = """\ --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 -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 """ @@ -1770,8 +1928,142 @@ def test_exporter_exports_requirements_txt_to_standard_output( out, err = capsys.readouterr() expected = """\ -bar==4.5.6 -foo==1.2.3 +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" +""" + + assert out == expected + + +def test_exporter_doesnt_confuse_repeated_packages( + tmp_dir: str, poetry: "Poetry", capsys: "CaptureFixture" +): + # Testcase derived from . + 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": [], + }, + }, + } + ) + # set_package_requires(poetry) + 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_full_version >= "3.6.2" and python_full_version < "4.0.0"\ + and python_version >= "3.7" and python_version < "4.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_full_version < "3.0.0"\ + and python_version < "3.7" or\ + python_version >= "3.6" and python_version < "3.7" and python_full_version >= "3.5.0" +click==8.0.3 ;\ + python_version >= "3.7" and python_version < "4.0" or\ + python_full_version >= "3.6.2" and python_full_version < "4.0.0"\ + and python_version >= "3.7" and python_version < "4.0" """ assert out == expected