Spec.satisfies should be commutative when strict=False (#35598)

The call:
```
x.satisfies(y[, strict=False])
```
is commutative, and tests non-empty intersection, whereas:
```
x.satsifies(y, strict=True)
```
is not commutative, and tests set-inclusion.

There are 2 fast paths. When strict=False both self and other need to 
be concrete, when strict=True we can optimize when other is concrete.
This commit is contained in:
Harmen Stoppels 2023-02-21 14:30:47 +01:00 committed by GitHub
parent 33bf1fd033
commit bc24a8f290
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 36 additions and 10 deletions

View File

@ -3451,25 +3451,38 @@ def _autospec(self, spec_like):
return spec_like
return Spec(spec_like)
def satisfies(self, other, deps=True, strict=False, strict_deps=False):
def satisfies(self, other, deps=True, strict=False):
"""Determine if this spec satisfies all constraints of another.
There are two senses for satisfies:
There are two senses for satisfies, depending on the ``strict``
argument.
* `loose` (default): the absence of a constraint in self
implies that it *could* be satisfied by other, so we only
check that there are no conflicts with other for
constraints that this spec actually has.
* ``strict=False``: the left-hand side and right-hand side have
non-empty intersection. For example ``zlib`` satisfies
``zlib@1.2.3`` and ``zlib@1.2.3`` satisfies ``zlib``. In this
sense satisfies is a commutative operation: ``x.satisfies(y)``
if and only if ``y.satisfies(x)``.
* `strict`: strict means that we *must* meet all the
constraints specified on other.
* ``strict=True``: the left-hand side is a subset of the right-hand
side. For example ``zlib@1.2.3`` satisfies ``zlib``, but ``zlib``
does not satisfy ``zlib@1.2.3``. In this sense satisfies is not
commutative: the left-hand side should be at least as constrained
as the right-hand side.
"""
other = self._autospec(other)
# The only way to satisfy a concrete spec is to match its hash exactly.
# Optimizations for right-hand side concrete:
# 1. For subset (strict=True) tests this means the left-hand side must
# be the same singleton with identical hash. Notice that package hashes
# can be different for otherwise indistinguishable concrete Spec objects.
# 2. For non-empty intersection (strict=False) we only have a fast path
# when the left-hand side is also concrete.
if other.concrete:
return self.concrete and self.dag_hash() == other.dag_hash()
if strict:
return self.concrete and self.dag_hash() == other.dag_hash()
elif self.concrete:
return self.dag_hash() == other.dag_hash()
# If the names are different, we need to consider virtuals
if self.name != other.name and self.name and other.name:

View File

@ -1293,3 +1293,16 @@ def test_package_hash_affects_dunder_and_dag_hash(mock_packages, default_mock_co
assert hash(a1) != hash(a2)
assert a1.dag_hash() != a2.dag_hash()
assert a1.process_hash() != a2.process_hash()
def test_satisfies_is_commutative_with_concrete_specs(mock_packages, default_mock_concretization):
a1 = default_mock_concretization("a@1.0")
a2 = Spec("a@1.0")
# strict=False means non-empty intersection, which is commutative.
assert a1.satisfies(a2)
assert a2.satisfies(a1)
# strict=True means set inclusion, which is not commutative.
assert a1.satisfies(a2, strict=True)
assert not a2.satisfies(a1, strict=True)