Skip to content

Commit 7aed696

Browse files
authored
Stubtest: check _value_ for ellipsis-valued stub enum members (#19760)
Currently stubtest allows unsound definitions: ```python # a.pyi from enum import Enum class E(Enum): _value_: str FOO = ... ``` ```python # a.py from enum import Enum class E(Enum): FOO = 0 ``` This PR teaches `stubtest` that `_value_` attribute ([spec](https://typing.python.org/en/latest/spec/enums.html#member-values)) should be used as a fallback in such case.
1 parent 3618369 commit 7aed696

File tree

2 files changed

+39
-3
lines changed

2 files changed

+39
-3
lines changed

mypy/stubtest.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,7 @@ def verify_var(
12741274
yield Error(object_path, "is read-only at runtime but not in the stub", stub, runtime)
12751275

12761276
runtime_type = get_mypy_type_of_runtime_value(runtime, type_context=stub.type)
1277+
note = ""
12771278
if (
12781279
runtime_type is not None
12791280
and stub.type is not None
@@ -1286,17 +1287,28 @@ def verify_var(
12861287
runtime_type = get_mypy_type_of_runtime_value(runtime.value)
12871288
if runtime_type is not None and is_subtype_helper(runtime_type, stub.type):
12881289
should_error = False
1289-
# We always allow setting the stub value to ...
1290+
# We always allow setting the stub value to Ellipsis (...), but use
1291+
# _value_ type as a fallback if given. If a member is ... and _value_
1292+
# type is given, all runtime types should be assignable to _value_.
12901293
proper_type = mypy.types.get_proper_type(stub.type)
12911294
if (
12921295
isinstance(proper_type, mypy.types.Instance)
12931296
and proper_type.type.fullname in mypy.types.ELLIPSIS_TYPE_NAMES
12941297
):
1295-
should_error = False
1298+
value_t = stub.info.get("_value_")
1299+
if value_t is None or value_t.type is None or runtime_type is None:
1300+
should_error = False
1301+
elif is_subtype_helper(runtime_type, value_t.type):
1302+
should_error = False
1303+
else:
1304+
note = " (incompatible '_value_')"
12961305

12971306
if should_error:
12981307
yield Error(
1299-
object_path, f"variable differs from runtime type {runtime_type}", stub, runtime
1308+
object_path,
1309+
f"variable differs from runtime type {runtime_type}{note}",
1310+
stub,
1311+
runtime,
13001312
)
13011313

13021314

mypy/test/teststubtest.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,6 +1483,30 @@ class HasEmptySlots:
14831483
""",
14841484
error=None,
14851485
)
1486+
yield Case(
1487+
stub="""
1488+
class HasCompatibleValue(enum.Enum):
1489+
_value_: str
1490+
FOO = ...
1491+
""",
1492+
runtime="""
1493+
class HasCompatibleValue(enum.Enum):
1494+
FOO = "foo"
1495+
""",
1496+
error=None,
1497+
)
1498+
yield Case(
1499+
stub="""
1500+
class HasIncompatibleValue(enum.Enum):
1501+
_value_: int
1502+
FOO = ...
1503+
""",
1504+
runtime="""
1505+
class HasIncompatibleValue(enum.Enum):
1506+
FOO = "foo"
1507+
""",
1508+
error="HasIncompatibleValue.FOO",
1509+
)
14861510

14871511
@collect_cases
14881512
def test_decorator(self) -> Iterator[Case]:

0 commit comments

Comments
 (0)