Skip to content
Open
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
37 changes: 37 additions & 0 deletions packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26059,6 +26059,32 @@ export function createTypeEvaluator(
);
}

// The "bytes" entry of "typePromotions" (see below) groups together the
// "bytes-like" types that support cross-type content comparisons via
// "==" and "!=", even though they're otherwise unrelated (non-overlapping)
// types. Unlike the numeric promotions ("int" -> "float" -> "complex"),
// where two disjoint literal subtypes (e.g. "Literal[1]" and "Literal[2]")
// must still be treated as non-comparable, every pairing within this
// group is mutually comparable, so it's safe to treat fullName membership
// in this specific set as sufficient without consulting literal values.
const bytesLikeTypeNames = new Set(['builtins.bytes', ...(typePromotions.get('builtins.bytes') ?? [])]);

function isBytesLikeType(type: ClassType) {
return type.shared.mro.some((mroClass) => isClass(mroClass) && bytesLikeTypeNames.has(mroClass.shared.fullName));
}

// Determines whether the two types are both members of the "bytes-like"
// group of types ("bytes", "bytearray" and "memoryview"). Pyright models
// this relationship as a "type promotion" (used elsewhere for
// assignability), but that grouping is also useful for determining
// whether two otherwise-disjoint built-in types can be compared with
// "==" or "!=", since these types share compatible "__eq__" semantics
// regardless of whether the "disableBytesTypePromotions" setting allows
// the assignment.
function isTypePromotionRelated(leftType: ClassType, rightType: ClassType) {
return isBytesLikeType(leftType) && isBytesLikeType(rightType);
}

// Determines whether the two types are potentially comparable -- i.e.
// their types overlap in such a way that it makes sense for them to
// be compared with an == or != operator. The functional also supports
Expand Down Expand Up @@ -26167,6 +26193,17 @@ export function createTypeEvaluator(
return boolVal === (intVal === 1);
}

// Types like "bytes", "bytearray" and "memoryview" are otherwise
// disjoint but are still comparable with "==" and "!=" because
// their "__eq__" methods support cross-type content comparisons.
// Pyright models this relationship as a "type promotion" (used
// elsewhere for assignability), so reuse that same list here
// regardless of the "disableBytesTypePromotions" setting, since
// comparability isn't affected by that assignability toggle.
if (isTypePromotionRelated(leftType, rightType)) {
return true;
}

return false;
}
}
Expand Down
36 changes: 36 additions & 0 deletions packages/pyright-internal/src/tests/samples/comparison1.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,39 @@ def func4(val: str | None):
# This should generate an error because there is no overlap in types.
if val == 42:
...


def func5(data1: bytearray, data2: bytes):
# "bytearray" and "bytes" are disjoint types, but they should still be
# considered comparable because their "__eq__" methods support
# cross-type content comparisons.
if data1 == data2:
...

if data1 == b"\x00\x00\x00\x00":
...


def func6(x: int, y: str):
# This should generate an error because there is no overlap in types.
if x == y:
...


def func7(x: list[int], y: dict[str, int]):
# This should generate an error because there is no overlap in types.
if x == y:
...


def func8(a: memoryview, b: bytes, c: bytearray):
# "memoryview", "bytes" and "bytearray" are all mutually comparable
# for the same reason as func5, above.
if a == b:
...

if a == c:
...

if b == c:
...
2 changes: 1 addition & 1 deletion packages/pyright-internal/src/tests/typeEvaluator6.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ test('Comparison1', () => {

configOptions.diagnosticRuleSet.reportUnnecessaryComparison = 'error';
const analysisResults2 = TestUtils.typeAnalyzeSampleFiles(['comparison1.py'], configOptions);
TestUtils.validateResults(analysisResults2, 7);
TestUtils.validateResults(analysisResults2, 9);
});

test('Comparison2', () => {
Expand Down