Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit58f7628

Browse files
authored
Issue warning for enum with no members in stub (#18068)
Follow up to#17207
1 parent3596793 commit58f7628

File tree

10 files changed

+118
-59
lines changed

10 files changed

+118
-59
lines changed

‎CHANGELOG.md‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,46 @@
22

33
##Next release
44

5+
###Change to enum membership semantics
6+
7+
As per the updated[typing specification for enums](https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members),
8+
enum members must be left unannotated.
9+
10+
```python
11+
classPet(Enum):
12+
CAT=1# Member attribute
13+
DOG=2# Member attribute
14+
WOLF:int=3# New error: Enum members must be left unannotated
15+
16+
species:str# Considered a non-member attribute
17+
```
18+
19+
In particular, the specification change can result in issues in type stubs (`.pyi` files), since
20+
historically it was common to leave the value absent:
21+
22+
```python
23+
# In a type stub (.pyi file)
24+
25+
classPet(Enum):
26+
# Change in semantics: previously considered members, now non-member attributes
27+
CAT:int
28+
DOG:int
29+
30+
# Mypy will now issue a warning if it detects this situation in type stubs:
31+
# > Detected enum "Pet" in a type stub with zero members.
32+
# > There is a chance this is due to a recent change in the semantics of enum membership.
33+
# > If so, use `member = value` to mark an enum member, instead of `member: type`
34+
35+
classPet(Enum):
36+
# As per the specification, you should now do one of the following:
37+
DOG=1# Member attribute with value 1 and known type
38+
WOLF= cast(int,...)# Member attribute with unknown value but known type
39+
LION=...# Member attribute with unknown value and unknown type
40+
```
41+
42+
Contributed by Terence Honles in PR[17207](https://github.com/python/mypy/pull/17207) and
43+
Shantanu Jain in PR[18068](https://github.com/python/mypy/pull/18068).
44+
545
##Mypy 1.13
646

747
We’ve just uploaded mypy 1.13 to the Python Package Index ([PyPI](https://pypi.org/project/mypy/)).

‎mypy/checker.py‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2588,20 +2588,30 @@ def check_typevar_defaults(self, tvars: Sequence[TypeVarLikeType]) -> None:
25882588

25892589
defcheck_enum(self,defn:ClassDef)->None:
25902590
assertdefn.info.is_enum
2591-
ifdefn.info.fullnamenotinENUM_BASES:
2592-
forsymindefn.info.names.values():
2593-
if (
2594-
isinstance(sym.node,Var)
2595-
andsym.node.has_explicit_value
2596-
andsym.node.name=="__members__"
2597-
):
2598-
# `__members__` will always be overwritten by `Enum` and is considered
2599-
# read-only so we disallow assigning a value to it
2600-
self.fail(message_registry.ENUM_MEMBERS_ATTR_WILL_BE_OVERRIDEN,sym.node)
2591+
ifdefn.info.fullnamenotinENUM_BASESand"__members__"indefn.info.names:
2592+
sym=defn.info.names["__members__"]
2593+
ifisinstance(sym.node,Var)andsym.node.has_explicit_value:
2594+
# `__members__` will always be overwritten by `Enum` and is considered
2595+
# read-only so we disallow assigning a value to it
2596+
self.fail(message_registry.ENUM_MEMBERS_ATTR_WILL_BE_OVERRIDEN,sym.node)
26012597
forbaseindefn.info.mro[1:-1]:# we don't need self and `object`
26022598
ifbase.is_enumandbase.fullnamenotinENUM_BASES:
26032599
self.check_final_enum(defn,base)
26042600

2601+
ifself.is_stubandself.tree.fullnamenotin {"enum","_typeshed"}:
2602+
ifnotdefn.info.enum_members:
2603+
self.fail(
2604+
f'Detected enum "{defn.info.fullname}" in a type stub with zero members. '
2605+
"There is a chance this is due to a recent change in the semantics of "
2606+
"enum membership. If so, use `member = value` to mark an enum member, "
2607+
"instead of `member: type`",
2608+
defn,
2609+
)
2610+
self.note(
2611+
"See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members",
2612+
defn,
2613+
)
2614+
26052615
self.check_enum_bases(defn)
26062616
self.check_enum_new(defn)
26072617

‎mypy/checkmember.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
ARG_POS,
1818
ARG_STAR,
1919
ARG_STAR2,
20+
EXCLUDED_ENUM_ATTRIBUTES,
2021
SYMBOL_FUNCBASE_TYPES,
2122
Context,
2223
Decorator,
@@ -48,7 +49,6 @@
4849
type_object_type_from_function,
4950
)
5051
frommypy.typesimport (
51-
ENUM_REMOVED_PROPS,
5252
AnyType,
5353
CallableType,
5454
DeletedType,
@@ -1173,7 +1173,7 @@ def analyze_enum_class_attribute_access(
11731173
itype:Instance,name:str,mx:MemberContext
11741174
)->Type|None:
11751175
# Skip these since Enum will remove it
1176-
ifnameinENUM_REMOVED_PROPS:
1176+
ifnameinEXCLUDED_ENUM_ATTRIBUTES:
11771177
returnreport_missing_attribute(mx.original_type,itype,name,mx)
11781178
# Dunders and private names are not Enum members
11791179
ifname.startswith("__")andname.replace("_","")!="":

‎mypy/nodes.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2902,6 +2902,10 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T:
29022902
}
29032903
)
29042904

2905+
# Attributes that can optionally be defined in the body of a subclass of
2906+
# enum.Enum but are removed from the class __dict__ by EnumMeta.
2907+
EXCLUDED_ENUM_ATTRIBUTES:Final=frozenset({"_ignore_","_order_","__order__"})
2908+
29052909

29062910
classTypeInfo(SymbolNode):
29072911
"""The type structure of a single class.
@@ -3229,6 +3233,19 @@ def protocol_members(self) -> list[str]:
32293233
members.add(name)
32303234
returnsorted(members)
32313235

3236+
@property
3237+
defenum_members(self)->list[str]:
3238+
return [
3239+
name
3240+
forname,syminself.names.items()
3241+
if (
3242+
isinstance(sym.node,Var)
3243+
andnamenotinEXCLUDED_ENUM_ATTRIBUTES
3244+
andnotname.startswith("__")
3245+
andsym.node.has_explicit_value
3246+
)
3247+
]
3248+
32323249
def__getitem__(self,name:str)->SymbolTableNode:
32333250
n=self.get(name)
32343251
ifn:

‎mypy/semanal_enum.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
frommypy.nodesimport (
1111
ARG_NAMED,
1212
ARG_POS,
13+
EXCLUDED_ENUM_ATTRIBUTES,
1314
MDEF,
1415
AssignmentStmt,
1516
CallExpr,
@@ -30,7 +31,7 @@
3031
)
3132
frommypy.optionsimportOptions
3233
frommypy.semanal_sharedimportSemanticAnalyzerInterface
33-
frommypy.typesimportENUM_REMOVED_PROPS,LiteralType,get_proper_type
34+
frommypy.typesimportLiteralType,get_proper_type
3435

3536
# Note: 'enum.EnumMeta' is deliberately excluded from this list. Classes that directly use
3637
# enum.EnumMeta do not necessarily automatically have the 'name' and 'value' attributes.
@@ -43,7 +44,7 @@
4344
"value",
4445
"_name_",
4546
"_value_",
46-
*ENUM_REMOVED_PROPS,
47+
*EXCLUDED_ENUM_ATTRIBUTES,
4748
# Also attributes from `object`:
4849
"__module__",
4950
"__annotations__",

‎mypy/test/teststubtest.py‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,9 +1267,9 @@ def test_enum(self) -> Iterator[Case]:
12671267
yieldCase(
12681268
stub="""
12691269
class X(enum.Enum):
1270-
a: int
1271-
b: str
1272-
c: str
1270+
a = ...
1271+
b = "asdf"
1272+
c = "oops"
12731273
""",
12741274
runtime="""
12751275
class X(enum.Enum):
@@ -1282,8 +1282,8 @@ class X(enum.Enum):
12821282
yieldCase(
12831283
stub="""
12841284
class Flags1(enum.Flag):
1285-
a: int
1286-
b: int
1285+
a = ...
1286+
b = 2
12871287
def foo(x: Flags1 = ...) -> None: ...
12881288
""",
12891289
runtime="""
@@ -1297,8 +1297,8 @@ def foo(x=Flags1.a|Flags1.b): pass
12971297
yieldCase(
12981298
stub="""
12991299
class Flags2(enum.Flag):
1300-
a: int
1301-
b: int
1300+
a = ...
1301+
b = 2
13021302
def bar(x: Flags2 | None = None) -> None: ...
13031303
""",
13041304
runtime="""
@@ -1312,8 +1312,8 @@ def bar(x=Flags2.a|Flags2.b): pass
13121312
yieldCase(
13131313
stub="""
13141314
class Flags3(enum.Flag):
1315-
a: int
1316-
b: int
1315+
a = ...
1316+
b = 2
13171317
def baz(x: Flags3 | None = ...) -> None: ...
13181318
""",
13191319
runtime="""
@@ -1346,8 +1346,8 @@ class WeirdEnum(enum.Enum):
13461346
yieldCase(
13471347
stub="""
13481348
class Flags4(enum.Flag):
1349-
a: int
1350-
b: int
1349+
a = 1
1350+
b = 2
13511351
def spam(x: Flags4 | None = None) -> None: ...
13521352
""",
13531353
runtime="""
@@ -1362,7 +1362,7 @@ def spam(x=Flags4(0)): pass
13621362
stub="""
13631363
from typing_extensions import Final, Literal
13641364
class BytesEnum(bytes, enum.Enum):
1365-
a: bytes
1365+
a = b'foo'
13661366
FOO: Literal[BytesEnum.a]
13671367
BAR: Final = BytesEnum.a
13681368
BAZ: BytesEnum
@@ -1897,7 +1897,7 @@ def test_good_literal(self) -> Iterator[Case]:
18971897
18981898
import enum
18991899
class Color(enum.Enum):
1900-
RED: int
1900+
RED = ...
19011901
19021902
NUM: Literal[1]
19031903
CHAR: Literal['a']

‎mypy/typeops.py‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -957,16 +957,20 @@ class Status(Enum):
957957
items= [
958958
try_expanding_sum_type_to_union(item,target_fullname)foritemintyp.relevant_items()
959959
]
960-
elifisinstance(typ,Instance)andtyp.type.fullname==target_fullname:
961-
iftyp.type.is_enum:
962-
items= [LiteralType(name,typ)fornameintyp.get_enum_values()]
963-
eliftyp.type.fullname=="builtins.bool":
960+
returnmake_simplified_union(items,contract_literals=False)
961+
962+
ifisinstance(typ,Instance)andtyp.type.fullname==target_fullname:
963+
iftyp.type.fullname=="builtins.bool":
964964
items= [LiteralType(True,typ),LiteralType(False,typ)]
965-
else:
966-
returntyp
965+
returnmake_simplified_union(items,contract_literals=False)
966+
967+
iftyp.type.is_enum:
968+
items= [LiteralType(name,typ)fornameintyp.type.enum_members]
969+
ifnotitems:
970+
returntyp
971+
returnmake_simplified_union(items,contract_literals=False)
967972

968-
# if the expanded union would be `Never` leave the type as is
969-
returntypifnotitemselsemake_simplified_union(items,contract_literals=False)
973+
returntyp
970974

971975

972976
deftry_contracting_literals_in_union(types:Sequence[Type])->list[ProperType]:
@@ -990,7 +994,7 @@ def try_contracting_literals_in_union(types: Sequence[Type]) -> list[ProperType]
990994
iffullnamenotinsum_types:
991995
sum_types[fullname]= (
992996
(
993-
set(typ.fallback.get_enum_values())
997+
set(typ.fallback.type.enum_members)
994998
iftyp.fallback.type.is_enum
995999
else {True,False}
9961000
),
@@ -1023,7 +1027,7 @@ def coerce_to_literal(typ: Type) -> Type:
10231027
iftyp.last_known_value:
10241028
returntyp.last_known_value
10251029
eliftyp.type.is_enum:
1026-
enum_values=typ.get_enum_values()
1030+
enum_values=typ.type.enum_members
10271031
iflen(enum_values)==1:
10281032
returnLiteralType(value=enum_values[0],fallback=typ)
10291033
returnoriginal_type

‎mypy/types.py‎

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,6 @@
150150

151151
OVERLOAD_NAMES:Final= ("typing.overload","typing_extensions.overload")
152152

153-
# Attributes that can optionally be defined in the body of a subclass of
154-
# enum.Enum but are removed from the class __dict__ by EnumMeta.
155-
ENUM_REMOVED_PROPS:Final= ("_ignore_","_order_","__order__")
156-
157153
NEVER_NAMES:Final= (
158154
"typing.NoReturn",
159155
"typing_extensions.NoReturn",
@@ -1559,23 +1555,10 @@ def is_singleton_type(self) -> bool:
15591555
# Also make this return True if the type corresponds to NotImplemented?
15601556
return (
15611557
self.type.is_enum
1562-
andlen(self.get_enum_values())==1
1558+
andlen(self.type.enum_members)==1
15631559
orself.type.fullnamein {"builtins.ellipsis","types.EllipsisType"}
15641560
)
15651561

1566-
defget_enum_values(self)->list[str]:
1567-
"""Return the list of values for an Enum."""
1568-
return [
1569-
name
1570-
forname,syminself.type.names.items()
1571-
if (
1572-
isinstance(sym.node,mypy.nodes.Var)
1573-
andnamenotinENUM_REMOVED_PROPS
1574-
andnotname.startswith("__")
1575-
andsym.node.has_explicit_value
1576-
)
1577-
]
1578-
15791562

15801563
classFunctionLike(ProperType):
15811564
"""Abstract base class for function types."""

‎mypyc/irbuild/classdef.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
fromtypingimportCallable,Final
88

99
frommypy.nodesimport (
10+
EXCLUDED_ENUM_ATTRIBUTES,
1011
TYPE_VAR_TUPLE_KIND,
1112
AssignmentStmt,
1213
CallExpr,
@@ -27,7 +28,7 @@
2728
TypeParam,
2829
is_class_var,
2930
)
30-
frommypy.typesimportENUM_REMOVED_PROPS,Instance,UnboundType,get_proper_type
31+
frommypy.typesimportInstance,UnboundType,get_proper_type
3132
frommypyc.commonimportPROPSET_PREFIX
3233
frommypyc.ir.class_irimportClassIR,NonExtClassInfo
3334
frommypyc.ir.func_irimportFuncDecl,FuncSignature
@@ -683,7 +684,7 @@ def add_non_ext_class_attr(
683684
cdef.info.bases
684685
andcdef.info.bases[0].type.fullname=="enum.Enum"
685686
# Skip these since Enum will remove it
686-
andlvalue.namenotinENUM_REMOVED_PROPS
687+
andlvalue.namenotinEXCLUDED_ENUM_ATTRIBUTES
687688
):
688689
# Enum values are always boxed, so use object_rprimitive.
689690
attr_to_cache.append((lvalue,object_rprimitive))

‎test-data/unit/check-enum.test‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,14 +1788,17 @@ import lib
17881788

17891789
[file lib.pyi]
17901790
from enum import Enum
1791-
class A(Enum):
1791+
class A(Enum): # E: Detected enum "lib.A" in a type stub with zero members. There is a chance this is due to a recent change in the semantics of enum membership. If so, use `member = value` to mark an enum member, instead of `member: type` \
1792+
# N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members
17921793
x: int
17931794
class B(A): # E: Cannot extend enum with existing members: "A"
17941795
x = 1 # E: Cannot override writable attribute "x" with a final one
17951796

17961797
class C(Enum):
17971798
x = 1
1798-
class D(C): # E: Cannot extend enum with existing members: "C"
1799+
class D(C): # E: Cannot extend enum with existing members: "C" \
1800+
# E: Detected enum "lib.D" in a type stub with zero members. There is a chance this is due to a recent change in the semantics of enum membership. If so, use `member = value` to mark an enum member, instead of `member: type` \
1801+
# N: See https://typing.readthedocs.io/en/latest/spec/enums.html#defining-members
17991802
x: int # E: Cannot assign to final name "x"
18001803
[builtins fixtures/bool.pyi]
18011804

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp