fix `ProtocolInstanceType::is_equivalent_to` and update test assertions

This commit is contained in:
Alex Waygood 2025-04-26 15:17:43 +01:00
parent ca10890fa0
commit 8ab0bd7bd4
2 changed files with 116 additions and 132 deletions

View File

@ -230,7 +230,7 @@ And it is also an error to use `Protocol` in type expressions:
def f(
x: Protocol, # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions"
y: type[Protocol], # TODO: should emit `[invalid-type-form]` here too
) -> None:
):
reveal_type(x) # revealed: Unknown
# TODO: should be `type[Unknown]`
@ -266,9 +266,7 @@ class Bar(typing_extensions.Protocol):
static_assert(typing_extensions.is_protocol(Foo))
static_assert(typing_extensions.is_protocol(Bar))
# TODO: should pass
static_assert(is_equivalent_to(Foo, Bar)) # error: [static-assert-error]
static_assert(is_equivalent_to(Foo, Bar))
```
The same goes for `typing.runtime_checkable` and `typing_extensions.runtime_checkable`:
@ -284,9 +282,7 @@ class RuntimeCheckableBar(typing_extensions.Protocol):
static_assert(typing_extensions.is_protocol(RuntimeCheckableFoo))
static_assert(typing_extensions.is_protocol(RuntimeCheckableBar))
# TODO: should pass
static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar)) # error: [static-assert-error]
static_assert(is_equivalent_to(RuntimeCheckableFoo, RuntimeCheckableBar))
# These should not error because the protocols are decorated with `@runtime_checkable`
isinstance(object(), RuntimeCheckableFoo)
@ -488,21 +484,20 @@ class HasX(Protocol):
class Foo:
x: int
# TODO: these should pass
static_assert(is_subtype_of(Foo, HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(Foo, HasX)) # error: [static-assert-error]
static_assert(is_subtype_of(Foo, HasX))
static_assert(is_assignable_to(Foo, HasX))
class FooSub(Foo): ...
# TODO: these should pass
static_assert(is_subtype_of(FooSub, HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(FooSub, HasX)) # error: [static-assert-error]
static_assert(is_subtype_of(FooSub, HasX))
static_assert(is_assignable_to(FooSub, HasX))
class Bar:
x: str
static_assert(not is_subtype_of(Bar, HasX))
static_assert(not is_assignable_to(Bar, HasX))
# TODO: these should pass
static_assert(not is_subtype_of(Bar, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(Bar, HasX)) # error: [static-assert-error]
class Baz:
y: int
@ -524,14 +519,16 @@ class A:
def x(self) -> int:
return 42
static_assert(not is_subtype_of(A, HasX))
static_assert(not is_assignable_to(A, HasX))
# TODO: these should pass
static_assert(not is_subtype_of(A, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(A, HasX)) # error: [static-assert-error]
class B:
x: Final = 42
static_assert(not is_subtype_of(A, HasX))
static_assert(not is_assignable_to(A, HasX))
# TODO: these should pass
static_assert(not is_subtype_of(A, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(A, HasX)) # error: [static-assert-error]
class IntSub(int): ...
@ -541,8 +538,10 @@ class C:
# due to invariance, a type is only a subtype of `HasX`
# if its `x` attribute is of type *exactly* `int`:
# a subclass of `int` does not satisfy the interface
static_assert(not is_subtype_of(C, HasX))
static_assert(not is_assignable_to(C, HasX))
#
# TODO: these should pass
static_assert(not is_subtype_of(C, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(C, HasX)) # error: [static-assert-error]
```
All attributes on frozen dataclasses and namedtuples are immutable, so instances of these classes
@ -556,22 +555,23 @@ from typing import NamedTuple
class MutableDataclass:
x: int
# TODO: these should pass
static_assert(is_subtype_of(MutableDataclass, HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(MutableDataclass, HasX)) # error: [static-assert-error]
static_assert(is_subtype_of(MutableDataclass, HasX))
static_assert(is_assignable_to(MutableDataclass, HasX))
@dataclass(frozen=True)
class ImmutableDataclass:
x: int
static_assert(not is_subtype_of(ImmutableDataclass, HasX))
static_assert(not is_assignable_to(ImmutableDataclass, HasX))
# TODO: these should pass
static_assert(not is_subtype_of(ImmutableDataclass, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(ImmutableDataclass, HasX)) # error: [static-assert-error]
class NamedTupleWithX(NamedTuple):
x: int
static_assert(not is_subtype_of(NamedTupleWithX, HasX))
static_assert(not is_assignable_to(NamedTupleWithX, HasX))
# TODO: these should pass
static_assert(not is_subtype_of(NamedTupleWithX, HasX)) # error: [static-assert-error]
static_assert(not is_assignable_to(NamedTupleWithX, HasX)) # error: [static-assert-error]
```
However, a type with a read-write property `x` *does* satisfy the `HasX` protocol. The `HasX`
@ -590,9 +590,8 @@ class XProperty:
def x(self, x: int) -> None:
self._x = x**2
# TODO: these should pass
static_assert(is_subtype_of(XProperty, HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(XProperty, HasX)) # error: [static-assert-error]
static_assert(is_subtype_of(XProperty, HasX))
static_assert(is_assignable_to(XProperty, HasX))
```
Attribute members on protocol classes are allowed to have default values, just like instance
@ -717,9 +716,8 @@ from typing import Protocol
class UniversalSet(Protocol): ...
# TODO: these should pass
static_assert(is_assignable_to(object, UniversalSet)) # error: [static-assert-error]
static_assert(is_subtype_of(object, UniversalSet)) # error: [static-assert-error]
static_assert(is_assignable_to(object, UniversalSet))
static_assert(is_subtype_of(object, UniversalSet))
```
Which means that `UniversalSet` here is in fact an equivalent type to `object`:
@ -727,8 +725,7 @@ Which means that `UniversalSet` here is in fact an equivalent type to `object`:
```py
from knot_extensions import is_equivalent_to
# TODO: this should pass
static_assert(is_equivalent_to(UniversalSet, object)) # error: [static-assert-error]
static_assert(is_equivalent_to(UniversalSet, object))
```
`object` is a subtype of certain other protocols too. Since all fully static types (whether nominal
@ -739,17 +736,16 @@ means that these protocols are also equivalent to `UniversalSet` and `object`:
class SupportsStr(Protocol):
def __str__(self) -> str: ...
# TODO: these should pass
static_assert(is_equivalent_to(SupportsStr, UniversalSet)) # error: [static-assert-error]
static_assert(is_equivalent_to(SupportsStr, object)) # error: [static-assert-error]
static_assert(is_equivalent_to(SupportsStr, UniversalSet))
static_assert(is_equivalent_to(SupportsStr, object))
class SupportsClass(Protocol):
__class__: type
@property
def __class__(self) -> type: ...
# TODO: these should pass
static_assert(is_equivalent_to(SupportsClass, UniversalSet)) # error: [static-assert-error]
static_assert(is_equivalent_to(SupportsClass, SupportsStr)) # error: [static-assert-error]
static_assert(is_equivalent_to(SupportsClass, object)) # error: [static-assert-error]
static_assert(is_equivalent_to(SupportsClass, UniversalSet))
static_assert(is_equivalent_to(SupportsClass, SupportsStr))
static_assert(is_equivalent_to(SupportsClass, object))
```
If a protocol contains members that are not defined on `object`, then that protocol will (like all
@ -786,8 +782,7 @@ class HasX(Protocol):
class AlsoHasX(Protocol):
x: int
# TODO: this should pass
static_assert(is_equivalent_to(HasX, AlsoHasX)) # error: [static-assert-error]
static_assert(is_equivalent_to(HasX, AlsoHasX))
```
And unions containing equivalent protocols are recognised as equivalent, even when the order is not
@ -803,8 +798,7 @@ class AlsoHasY(Protocol):
class A: ...
class B: ...
# TODO: this should pass
static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A)) # error: [static-assert-error]
static_assert(is_equivalent_to(A | HasX | B | HasY, B | AlsoHasY | AlsoHasX | A))
```
## Intersections of protocols
@ -882,9 +876,9 @@ from knot_extensions import is_subtype_of, is_assignable_to, static_assert, Type
class HasX(Protocol):
x: int
# TODO: these should pass
# TODO: this should pass
static_assert(is_subtype_of(TypeOf[module], HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(TypeOf[module], HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(TypeOf[module], HasX))
class ExplicitProtocolSubtype(HasX, Protocol):
y: int
@ -896,9 +890,8 @@ class ImplicitProtocolSubtype(Protocol):
x: int
y: str
# TODO: these should pass
static_assert(is_subtype_of(ImplicitProtocolSubtype, HasX)) # error: [static-assert-error]
static_assert(is_assignable_to(ImplicitProtocolSubtype, HasX)) # error: [static-assert-error]
static_assert(is_subtype_of(ImplicitProtocolSubtype, HasX))
static_assert(is_assignable_to(ImplicitProtocolSubtype, HasX))
class Meta(type):
x: int
@ -933,23 +926,24 @@ def f(obj: ClassVarXProto):
class InstanceAttrX:
x: int
static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto))
static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto))
# TODO: these should pass
static_assert(not is_assignable_to(InstanceAttrX, ClassVarXProto)) # error: [static-assert-error]
static_assert(not is_subtype_of(InstanceAttrX, ClassVarXProto)) # error: [static-assert-error]
class PropertyX:
@property
def x(self) -> int:
return 42
static_assert(not is_assignable_to(PropertyX, ClassVarXProto))
static_assert(not is_subtype_of(PropertyX, ClassVarXProto))
# TODO: these should pass
static_assert(not is_assignable_to(PropertyX, ClassVarXProto)) # error: [static-assert-error]
static_assert(not is_subtype_of(PropertyX, ClassVarXProto)) # error: [static-assert-error]
class ClassVarX:
x: ClassVar[int] = 42
# TODO: these should pass
static_assert(is_assignable_to(ClassVarX, ClassVarXProto)) # error: [static-assert-error]
static_assert(is_subtype_of(ClassVarX, ClassVarXProto)) # error: [static-assert-error]
static_assert(is_assignable_to(ClassVarX, ClassVarXProto))
static_assert(is_subtype_of(ClassVarX, ClassVarXProto))
```
This is mentioned by the
@ -976,18 +970,16 @@ class HasXProperty(Protocol):
class XAttr:
x: int
# TODO: these should pass
static_assert(is_subtype_of(XAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(XAttr, HasXProperty))
static_assert(is_assignable_to(XAttr, HasXProperty))
class XReadProperty:
@property
def x(self) -> int:
return 42
# TODO: these should pass
static_assert(is_subtype_of(XReadProperty, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XReadProperty, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(XReadProperty, HasXProperty))
static_assert(is_assignable_to(XReadProperty, HasXProperty))
class XReadWriteProperty:
@property
@ -997,22 +989,20 @@ class XReadWriteProperty:
@x.setter
def x(self, val: int) -> None: ...
# TODO: these should pass
static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))
static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
class XClassVar:
x: ClassVar[int] = 42
static_assert(is_subtype_of(XClassVar, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XClassVar, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(XClassVar, HasXProperty))
static_assert(is_assignable_to(XClassVar, HasXProperty))
class XFinal:
x: Final = 42
# TODO: these should pass
static_assert(is_subtype_of(XFinal, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XFinal, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(XFinal, HasXProperty))
static_assert(is_assignable_to(XFinal, HasXProperty))
```
A read-only property on a protocol, unlike a mutable attribute, is covariant: `XSub` in the below
@ -1025,9 +1015,8 @@ class MyInt(int): ...
class XSub:
x: MyInt
# TODO: these should pass
static_assert(is_subtype_of(XSub, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XSub, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(XSub, HasXProperty))
static_assert(is_assignable_to(XSub, HasXProperty))
```
A read/write property on a protocol, where the getter returns the same type that the setter takes,
@ -1043,17 +1032,17 @@ class HasMutableXProperty(Protocol):
class XAttr:
x: int
# TODO: these should pass
static_assert(is_subtype_of(XAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(XAttr, HasXProperty))
static_assert(is_assignable_to(XAttr, HasXProperty))
class XReadProperty:
@property
def x(self) -> int:
return 42
static_assert(not is_subtype_of(XReadProperty, HasXProperty))
static_assert(not is_assignable_to(XReadProperty, HasXProperty))
# TODO: these should pass
static_assert(not is_subtype_of(XReadProperty, HasXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(XReadProperty, HasXProperty)) # error: [static-assert-error]
class XReadWriteProperty:
@property
@ -1063,15 +1052,15 @@ class XReadWriteProperty:
@x.setter
def x(self, val: int) -> None: ...
# TODO: these should pass
static_assert(is_subtype_of(XReadWriteProperty, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XReadWriteProperty, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(XReadWriteProperty, HasXProperty))
static_assert(is_assignable_to(XReadWriteProperty, HasXProperty))
class XSub:
x: MyInt
static_assert(not is_subtype_of(XSub, HasXProperty))
static_assert(not is_assignable_to(XSub, HasXProperty))
# TODO: should pass
static_assert(not is_subtype_of(XSub, HasXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(XSub, HasXProperty)) # error: [static-assert-error]
```
A protocol with a read/write property `x` is exactly equivalent to a protocol with a mutable
@ -1083,16 +1072,13 @@ from knot_extensions import is_equivalent_to
class HasMutableXAttr(Protocol):
x: int
# TODO: this should pass
static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty)) # error: [static-assert-error]
static_assert(is_equivalent_to(HasMutableXAttr, HasMutableXProperty))
# TODO: these should pass
static_assert(is_subtype_of(HasMutableXAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(HasMutableXAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(HasMutableXAttr, HasXProperty))
static_assert(is_assignable_to(HasMutableXAttr, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(HasMutableXProperty, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(HasMutableXProperty, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(HasMutableXProperty, HasXProperty))
static_assert(is_assignable_to(HasMutableXProperty, HasXProperty))
```
A read/write property on a protocol, where the setter accepts a subtype of the type returned by the
@ -1119,9 +1105,8 @@ class HasAsymmetricXProperty(Protocol):
class XAttr:
x: int
# TODO: these should pass
static_assert(is_subtype_of(XAttr, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XAttr, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(XAttr, HasAsymmetricXProperty))
static_assert(is_assignable_to(XAttr, HasAsymmetricXProperty))
```
The end conclusion of this is that the getter-returned type of a property is always covariant and
@ -1132,9 +1117,8 @@ regular mutable attribute, where the implied getter-returned and setter-accepted
class XAttrSub:
x: MyInt
# TODO: these should pass
static_assert(is_subtype_of(XAttrSub, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XAttrSub, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(XAttrSub, HasAsymmetricXProperty))
static_assert(is_assignable_to(XAttrSub, HasAsymmetricXProperty))
class MyIntSub(MyInt):
pass
@ -1142,8 +1126,9 @@ class MyIntSub(MyInt):
class XAttrSubSub:
x: MyIntSub
static_assert(not is_subtype_of(XAttrSubSub, HasAsymmetricXProperty))
static_assert(not is_assignable_to(XAttrSubSub, HasAsymmetricXProperty))
# TODO: should pass
static_assert(not is_subtype_of(XAttrSubSub, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(XAttrSubSub, HasAsymmetricXProperty)) # error: [static-assert-error]
```
An asymmetric property on a protocol can also be satisfied by an asymmetric property on a nominal
@ -1159,9 +1144,8 @@ class XAsymmetricProperty:
@x.setter
def x(self, x: int) -> None: ...
# TODO: these should pass
static_assert(is_subtype_of(XAsymmetricProperty, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XAsymmetricProperty, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(XAsymmetricProperty, HasAsymmetricXProperty))
static_assert(is_assignable_to(XAsymmetricProperty, HasAsymmetricXProperty))
```
A custom descriptor attribute on the nominal class will also suffice:
@ -1176,9 +1160,8 @@ class Descriptor:
class XCustomDescriptor:
x: Descriptor = Descriptor()
# TODO: these should pass
static_assert(is_subtype_of(XCustomDescriptor, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(XCustomDescriptor, HasAsymmetricXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(XCustomDescriptor, HasAsymmetricXProperty))
static_assert(is_assignable_to(XCustomDescriptor, HasAsymmetricXProperty))
```
Moreover, a read-only property on a protocol can be satisfied by a nominal class that defines a
@ -1191,19 +1174,20 @@ class HasGetAttr:
def __getattr__(self, attr: str) -> int:
return 42
# TODO: these should pass
static_assert(is_subtype_of(HasGetAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(HasGetAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(HasGetAttr, HasXProperty))
static_assert(is_assignable_to(HasGetAttr, HasXProperty))
static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr))
static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr))
# TODO: these should pass
static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) # error: [static-assert-error]
static_assert(not is_subtype_of(HasGetAttr, HasMutableXAttr)) # error: [static-assert-error]
class HasGetAttrWithUnsuitableReturn:
def __getattr__(self, attr: str) -> tuple[int, int]:
return (1, 2)
static_assert(not is_subtype_of(HasGetAttrWithUnsuitableReturn, HasXProperty))
static_assert(not is_assignable_to(HasGetAttrWithUnsuitableReturn, HasXProperty))
# TODO: these should pass
static_assert(not is_subtype_of(HasGetAttrWithUnsuitableReturn, HasXProperty)) # error: [static-assert-error]
static_assert(not is_assignable_to(HasGetAttrWithUnsuitableReturn, HasXProperty)) # error: [static-assert-error]
class HasGetAttrAndSetAttr:
def __getattr__(self, attr: str) -> MyInt:
@ -1211,9 +1195,10 @@ class HasGetAttrAndSetAttr:
def __setattr__(self, attr: str, value: int) -> None: ...
static_assert(is_subtype_of(HasGetAttrAndSetAttr, HasXProperty))
static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty))
# TODO: these should pass
static_assert(is_subtype_of(HasGetAttrAndSetAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(HasGetAttrAndSetAttr, HasXProperty)) # error: [static-assert-error]
static_assert(is_subtype_of(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error]
static_assert(is_assignable_to(HasGetAttrAndSetAttr, XAsymmetricProperty)) # error: [static-assert-error]
```
@ -1314,9 +1299,12 @@ class FalsyFooSubclass(FalsyFoo, Protocol):
y: str
def g(a: Truthy, b: FalsyFoo, c: FalsyFooSubclass):
reveal_type(bool(a)) # revealed: Literal[True]
reveal_type(bool(b)) # revealed: Literal[False]
reveal_type(bool(c)) # revealed: Literal[False]
# TODO should be `Literal[True]
reveal_type(bool(a)) # revealed: bool
# TODO should be `Literal[False]
reveal_type(bool(b)) # revealed: bool
# TODO should be `Literal[False]
reveal_type(bool(c)) # revealed: bool
```
It is not sufficient for a protocol to have a callable `__bool__` instance member that returns
@ -1363,12 +1351,12 @@ from knot_extensions import is_subtype_of, is_assignable_to
class NominalWithX:
x: int = 42
# TODO: these should pass
static_assert(is_assignable_to(NominalWithX, FullyStatic)) # error: [static-assert-error]
static_assert(is_assignable_to(NominalWithX, NotFullyStatic)) # error: [static-assert-error]
static_assert(is_subtype_of(NominalWithX, FullyStatic)) # error: [static-assert-error]
static_assert(is_assignable_to(NominalWithX, FullyStatic))
static_assert(is_assignable_to(NominalWithX, NotFullyStatic))
static_assert(is_subtype_of(NominalWithX, FullyStatic))
static_assert(not is_subtype_of(NominalWithX, NotFullyStatic))
# TODO: this should pass
static_assert(not is_subtype_of(NominalWithX, NotFullyStatic)) # error: [static-assert-error]
```
Empty protocols are fully static; this follows from the fact that an empty protocol is equivalent to

View File

@ -138,12 +138,8 @@ impl<'db> ProtocolInstanceType<'db> {
}
pub(super) fn normalized(self, db: &'db dyn Db) -> Type<'db> {
let members = self.protocol_members(db);
let object = KnownClass::Object.to_instance(db);
if members
.iter()
.all(|member| !object.member(db, member).symbol.is_unbound())
{
if object.satisfies_protocol(db, self) {
return object;
}
match self.0 {
@ -180,7 +176,7 @@ impl<'db> ProtocolInstanceType<'db> {
/// TODO: consider the types of the members as well as their existence
pub(super) fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.protocol_members(db).set_eq(other.protocol_members(db))
self.normalized(db) == other.normalized(db)
}
/// TODO: consider the types of the members as well as their existence