Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/poetry/core/packages/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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__)
Expand Down
104 changes: 64 additions & 40 deletions src/poetry/core/packages/specification.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,62 +71,86 @@ def source_subdirectory(self) -> str | None:
def features(self) -> frozenset[str]:
return self._features

def is_same_package_as(self, other: PackageSpecification) -> bool:
if other.complete_name != self.complete_name:
def is_direct_origin(self) -> bool:
return self._source_type in [
"directory",
"file",
"url",
"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_source_as(self, other: PackageSpecification) -> bool:
Comment thread
abn marked this conversation as resolved.
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)
Expand Down
32 changes: 32 additions & 0 deletions tests/packages/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
56 changes: 56 additions & 0 deletions tests/packages/test_specification.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,59 @@ 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


@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