From 133218a398b102c654e64c2f5d73507dca06924d Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Mon, 23 May 2022 12:25:14 +0200 Subject: [PATCH 1/4] package spec: add is_direct_origin helper method --- src/poetry/core/packages/package.py | 2 +- src/poetry/core/packages/specification.py | 8 ++++++++ tests/packages/test_specification.py | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/poetry/core/packages/package.py b/src/poetry/core/packages/package.py index d96981fb2..3a6f0ffd7 100644 --- a/src/poetry/core/packages/package.py +++ b/src/poetry/core/packages/package.py @@ -511,7 +511,7 @@ def to_dependency(self) -> Dependency: if not self.python_constraint.is_any(): dep.python_versions = self.python_versions - if self._source_type not in ["directory", "file", "url", "git"]: + if not self.is_direct_origin(): return dep return dep.with_constraint(self._version) diff --git a/src/poetry/core/packages/specification.py b/src/poetry/core/packages/specification.py index 86c9a5be4..bced7cf57 100644 --- a/src/poetry/core/packages/specification.py +++ b/src/poetry/core/packages/specification.py @@ -71,6 +71,14 @@ def source_subdirectory(self) -> str | None: def features(self) -> frozenset[str]: return self._features + def is_direct_origin(self) -> bool: + return self._source_type in [ + "directory", + "file", + "url", + "git", + ] + def is_same_package_as(self, other: PackageSpecification) -> bool: if other.complete_name != self.complete_name: return False diff --git a/tests/packages/test_specification.py b/tests/packages/test_specification.py index d7f5df861..bdc82e51b 100644 --- a/tests/packages/test_specification.py +++ b/tests/packages/test_specification.py @@ -21,3 +21,18 @@ def test_is_same_package_source_type( expected: bool, ) -> None: assert spec1.is_same_package_as(spec2) == expected + + +@pytest.mark.parametrize( + ("source_type", "result"), + [ + ("directory", True), + ("file", True), + ("url", True), + ("git", True), + ("legacy", False), + (None, False), + ], +) +def test_is_direct_origin(source_type: str | None, result: bool) -> None: + assert PackageSpecification("package", source_type).is_direct_origin() == result From 54d944096db0c850493509368d7db547ecf8a2be Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Mon, 23 May 2022 13:44:12 +0200 Subject: [PATCH 2/4] package spec: add provides helper --- src/poetry/core/packages/specification.py | 11 ++++++ tests/packages/test_specification.py | 41 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/poetry/core/packages/specification.py b/src/poetry/core/packages/specification.py index bced7cf57..ee9ecedf9 100644 --- a/src/poetry/core/packages/specification.py +++ b/src/poetry/core/packages/specification.py @@ -79,6 +79,17 @@ def is_direct_origin(self) -> bool: "git", ] + def provides(self, other: PackageSpecification) -> bool: + """ + Helper method to determine if this package provides the given specification. + + This determination is made to be true, if the names are the same and this + package provides all features required by the other specification. + + Source type checks are explicitly ignored here as this is not of interest. + """ + return self.name == other.name and self.features.issuperset(other.features) + def is_same_package_as(self, other: PackageSpecification) -> bool: if other.complete_name != self.complete_name: return False diff --git a/tests/packages/test_specification.py b/tests/packages/test_specification.py index bdc82e51b..21d5c165e 100644 --- a/tests/packages/test_specification.py +++ b/tests/packages/test_specification.py @@ -36,3 +36,44 @@ def test_is_same_package_source_type( ) def test_is_direct_origin(source_type: str | None, result: bool) -> None: assert PackageSpecification("package", source_type).is_direct_origin() == result + + +@pytest.mark.parametrize( + "spec1, spec2, expected", + [ + (PackageSpecification("a"), PackageSpecification("a"), True), + (PackageSpecification("a"), PackageSpecification("b"), False), + (PackageSpecification("a", features=["x"]), PackageSpecification("a"), True), + ( + PackageSpecification("a", features=["x"]), + PackageSpecification("a", features=["x"]), + True, + ), + ( + PackageSpecification("a", features=["x"]), + PackageSpecification("b", features=["x"]), + False, + ), + ( + PackageSpecification("a", features=["x"]), + PackageSpecification("a", features=["y"]), + False, + ), + ( + PackageSpecification("a", features=["x"]), + PackageSpecification("a", features=["x", "y"]), + False, + ), + ( + PackageSpecification("a", features=["x", "y"]), + PackageSpecification("a", features=["x"]), + True, + ), + ], +) +def test_specification_provides( + spec1: PackageSpecification, + spec2: PackageSpecification, + expected: bool, +) -> None: + assert spec1.provides(spec2) == expected From f0bc6143730c8a38f7a55cbff4cf8bc354000cc5 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Mon, 23 May 2022 13:45:35 +0200 Subject: [PATCH 3/4] package spec: split out source comparison --- src/poetry/core/packages/specification.py | 85 ++++++++++++----------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/src/poetry/core/packages/specification.py b/src/poetry/core/packages/specification.py index ee9ecedf9..f34a8773e 100644 --- a/src/poetry/core/packages/specification.py +++ b/src/poetry/core/packages/specification.py @@ -90,62 +90,67 @@ def provides(self, other: PackageSpecification) -> bool: """ return self.name == other.name and self.features.issuperset(other.features) - def is_same_package_as(self, other: PackageSpecification) -> bool: - if other.complete_name != self.complete_name: + def is_same_source_as(self, other: PackageSpecification) -> bool: + if self._source_type != other.source_type: return False - if self._source_type != other.source_type: + if not self._source_type: + # both packages are of source type None + # no need to check further + return True + + if ( + self._source_url or other.source_url + ) and self._source_url != other.source_url: return False - if self._source_type: + if ( + self._source_subdirectory or other.source_subdirectory + ) and self._source_subdirectory != other.source_subdirectory: + return False - if ( - self._source_url or other.source_url - ) and self._source_url != other.source_url: + # We check the resolved reference first: + # if they match we assume equality regardless + # of their source reference. + # This is important when comparing a resolved branch VCS + # dependency to a direct commit reference VCS dependency + if ( + self._source_resolved_reference + and other.source_resolved_reference + and self._source_resolved_reference == other.source_resolved_reference + ): + return True + + if self._source_reference or other.source_reference: + # special handling for packages with references + if not self._source_reference or not other.source_reference: + # case: one reference is defined and is non-empty, but other is not return False - if ( - self._source_subdirectory or other.source_subdirectory - ) and self._source_subdirectory != other.source_subdirectory: + if not ( + self._source_reference == other.source_reference + or self._source_reference.startswith(other.source_reference) + or other.source_reference.startswith(self._source_reference) + ): + # case: both references defined, but one is not equal to or a short + # representation of the other return False - # We check the resolved reference first: - # if they match we assume equality regardless - # of their source reference. - # This is important when comparing a resolved branch VCS - # dependency to a direct commit reference VCS dependency if ( self._source_resolved_reference and other.source_resolved_reference - and self._source_resolved_reference == other.source_resolved_reference + and self._source_resolved_reference != other.source_resolved_reference ): - return True - - if self._source_reference or other.source_reference: - # special handling for packages with references - if not self._source_reference or not other.source_reference: - # case: one reference is defined and is non-empty, but other is not - return False - - if not ( - self._source_reference == other.source_reference - or self._source_reference.startswith(other.source_reference) - or other.source_reference.startswith(self._source_reference) - ): - # case: both references defined, but one is not equal to or a short - # representation of the other - return False - - if ( - self._source_resolved_reference - and other.source_resolved_reference - and self._source_resolved_reference - != other.source_resolved_reference - ): - return False + return False return True + def is_same_package_as(self, other: PackageSpecification) -> bool: + if other.complete_name != self.complete_name: + return False + + return self.is_same_source_as(other) + def __hash__(self) -> int: if not self._source_type: return hash(self._name) From 9fb371a33dba05435a298d3b6910589d094cf334 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Mon, 23 May 2022 13:46:04 +0200 Subject: [PATCH 4/4] package: add satisfies helper method --- src/poetry/core/packages/package.py | 17 +++++++++++++++ tests/packages/test_package.py | 32 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/poetry/core/packages/package.py b/src/poetry/core/packages/package.py index 3a6f0ffd7..2d6ea5df3 100644 --- a/src/poetry/core/packages/package.py +++ b/src/poetry/core/packages/package.py @@ -536,6 +536,23 @@ def with_features(self: T, features: Iterable[str]) -> T: def without_features(self: T) -> T: return self.with_features([]) + def satisfies( + self, dependency: Dependency, ignore_source_type: bool = False + ) -> bool: + """ + Helper method to check if this package satisfies a given dependency. + + This is determined by assessing if this instance provides the package and + features specified by the given dependency. Further, version and source + types are checked. + """ + if not self.provides(dependency) or not dependency.constraint.allows( + self.version + ): + return False + + return ignore_source_type or self.is_same_source_as(dependency) + def clone(self: T) -> T: clone = self.__class__(self.pretty_name, self.version) clone.__dict__ = copy.deepcopy(self.__dict__) diff --git a/tests/packages/test_package.py b/tests/packages/test_package.py index 79ea24989..d12fc25e4 100644 --- a/tests/packages/test_package.py +++ b/tests/packages/test_package.py @@ -8,6 +8,7 @@ import pytest from poetry.core.factory import Factory +from poetry.core.packages.dependency import Dependency from poetry.core.packages.dependency_group import DependencyGroup from poetry.core.packages.directory_dependency import DirectoryDependency from poetry.core.packages.file_dependency import FileDependency @@ -470,3 +471,34 @@ def test_set_readme_property() -> None: assert package.readmes == (Path("README.md"),) with pytest.deprecated_call(): assert package.readme == Path("README.md") + + +@pytest.mark.parametrize( + ("package", "dependency", "ignore_source_type", "result"), + [ + (Package("foo", "0.1.0"), Dependency("foo", ">=0.1.0"), False, True), + (Package("foo", "0.1.0"), Dependency("foo", "<0.1.0"), False, False), + ( + Package("foo", "0.1.0"), + Dependency("foo", ">=0.1.0", source_type="git"), + False, + False, + ), + ( + Package("foo", "0.1.0"), + Dependency("foo", ">=0.1.0", source_type="git"), + True, + True, + ), + ( + Package("foo", "0.1.0"), + Dependency("foo", "<0.1.0", source_type="git"), + True, + False, + ), + ], +) +def test_package_satisfies( + package: Package, dependency: Dependency, ignore_source_type: bool, result: bool +) -> None: + assert package.satisfies(dependency, ignore_source_type) == result