enums.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. """
  2. This file contains a variety of plugins for refining how mypy infers types of
  3. expressions involving Enums.
  4. Currently, this file focuses on providing better inference for expressions like
  5. 'SomeEnum.FOO.name' and 'SomeEnum.FOO.value'. Note that the type of both expressions
  6. will vary depending on exactly which instance of SomeEnum we're looking at.
  7. Note that this file does *not* contain all special-cased logic related to enums:
  8. we actually bake some of it directly in to the semantic analysis layer (see
  9. semanal_enum.py).
  10. """
  11. from __future__ import annotations
  12. from typing import Iterable, Sequence, TypeVar, cast
  13. from typing_extensions import Final
  14. import mypy.plugin # To avoid circular imports.
  15. from mypy.nodes import TypeInfo
  16. from mypy.semanal_enum import ENUM_BASES
  17. from mypy.subtypes import is_equivalent
  18. from mypy.typeops import fixup_partial_type, make_simplified_union
  19. from mypy.types import CallableType, Instance, LiteralType, ProperType, Type, get_proper_type
  20. ENUM_NAME_ACCESS: Final = {f"{prefix}.name" for prefix in ENUM_BASES} | {
  21. f"{prefix}._name_" for prefix in ENUM_BASES
  22. }
  23. ENUM_VALUE_ACCESS: Final = {f"{prefix}.value" for prefix in ENUM_BASES} | {
  24. f"{prefix}._value_" for prefix in ENUM_BASES
  25. }
  26. def enum_name_callback(ctx: mypy.plugin.AttributeContext) -> Type:
  27. """This plugin refines the 'name' attribute in enums to act as if
  28. they were declared to be final.
  29. For example, the expression 'MyEnum.FOO.name' normally is inferred
  30. to be of type 'str'.
  31. This plugin will instead make the inferred type be a 'str' where the
  32. last known value is 'Literal["FOO"]'. This means it would be legal to
  33. use 'MyEnum.FOO.name' in contexts that expect a Literal type, just like
  34. any other Final variable or attribute.
  35. This plugin assumes that the provided context is an attribute access
  36. matching one of the strings found in 'ENUM_NAME_ACCESS'.
  37. """
  38. enum_field_name = _extract_underlying_field_name(ctx.type)
  39. if enum_field_name is None:
  40. return ctx.default_attr_type
  41. else:
  42. str_type = ctx.api.named_generic_type("builtins.str", [])
  43. literal_type = LiteralType(enum_field_name, fallback=str_type)
  44. return str_type.copy_modified(last_known_value=literal_type)
  45. _T = TypeVar("_T")
  46. def _first(it: Iterable[_T]) -> _T | None:
  47. """Return the first value from any iterable.
  48. Returns ``None`` if the iterable is empty.
  49. """
  50. for val in it:
  51. return val
  52. return None
  53. def _infer_value_type_with_auto_fallback(
  54. ctx: mypy.plugin.AttributeContext, proper_type: ProperType | None
  55. ) -> Type | None:
  56. """Figure out the type of an enum value accounting for `auto()`.
  57. This method is a no-op for a `None` proper_type and also in the case where
  58. the type is not "enum.auto"
  59. """
  60. if proper_type is None:
  61. return None
  62. proper_type = get_proper_type(fixup_partial_type(proper_type))
  63. if not (isinstance(proper_type, Instance) and proper_type.type.fullname == "enum.auto"):
  64. return proper_type
  65. assert isinstance(ctx.type, Instance), "An incorrect ctx.type was passed."
  66. info = ctx.type.type
  67. # Find the first _generate_next_value_ on the mro. We need to know
  68. # if it is `Enum` because `Enum` types say that the return-value of
  69. # `_generate_next_value_` is `Any`. In reality the default `auto()`
  70. # returns an `int` (presumably the `Any` in typeshed is to make it
  71. # easier to subclass and change the returned type).
  72. type_with_gnv = _first(ti for ti in info.mro if ti.names.get("_generate_next_value_"))
  73. if type_with_gnv is None:
  74. return ctx.default_attr_type
  75. stnode = type_with_gnv.names["_generate_next_value_"]
  76. # This should be a `CallableType`
  77. node_type = get_proper_type(stnode.type)
  78. if isinstance(node_type, CallableType):
  79. if type_with_gnv.fullname == "enum.Enum":
  80. int_type = ctx.api.named_generic_type("builtins.int", [])
  81. return int_type
  82. return get_proper_type(node_type.ret_type)
  83. return ctx.default_attr_type
  84. def _implements_new(info: TypeInfo) -> bool:
  85. """Check whether __new__ comes from enum.Enum or was implemented in a
  86. subclass. In the latter case, we must infer Any as long as mypy can't infer
  87. the type of _value_ from assignments in __new__.
  88. """
  89. type_with_new = _first(
  90. ti
  91. for ti in info.mro
  92. if ti.names.get("__new__") and not ti.fullname.startswith("builtins.")
  93. )
  94. if type_with_new is None:
  95. return False
  96. return type_with_new.fullname not in ("enum.Enum", "enum.IntEnum", "enum.StrEnum")
  97. def enum_value_callback(ctx: mypy.plugin.AttributeContext) -> Type:
  98. """This plugin refines the 'value' attribute in enums to refer to
  99. the original underlying value. For example, suppose we have the
  100. following:
  101. class SomeEnum:
  102. FOO = A()
  103. BAR = B()
  104. By default, mypy will infer that 'SomeEnum.FOO.value' and
  105. 'SomeEnum.BAR.value' both are of type 'Any'. This plugin refines
  106. this inference so that mypy understands the expressions are
  107. actually of types 'A' and 'B' respectively. This better reflects
  108. the actual runtime behavior.
  109. This plugin works simply by looking up the original value assigned
  110. to the enum. For example, when this plugin sees 'SomeEnum.BAR.value',
  111. it will look up whatever type 'BAR' had in the SomeEnum TypeInfo and
  112. use that as the inferred type of the overall expression.
  113. This plugin assumes that the provided context is an attribute access
  114. matching one of the strings found in 'ENUM_VALUE_ACCESS'.
  115. """
  116. enum_field_name = _extract_underlying_field_name(ctx.type)
  117. if enum_field_name is None:
  118. # We do not know the enum field name (perhaps it was passed to a
  119. # function and we only know that it _is_ a member). All is not lost
  120. # however, if we can prove that the all of the enum members have the
  121. # same value-type, then it doesn't matter which member was passed in.
  122. # The value-type is still known.
  123. if isinstance(ctx.type, Instance):
  124. info = ctx.type.type
  125. # As long as mypy doesn't understand attribute creation in __new__,
  126. # there is no way to predict the value type if the enum class has a
  127. # custom implementation
  128. if _implements_new(info):
  129. return ctx.default_attr_type
  130. stnodes = (info.get(name) for name in info.names)
  131. # Enums _can_ have methods and instance attributes.
  132. # Omit methods and attributes created by assigning to self.*
  133. # for our value inference.
  134. node_types = (
  135. get_proper_type(n.type) if n else None
  136. for n in stnodes
  137. if n is None or not n.implicit
  138. )
  139. proper_types = list(
  140. _infer_value_type_with_auto_fallback(ctx, t)
  141. for t in node_types
  142. if t is None or not isinstance(t, CallableType)
  143. )
  144. underlying_type = _first(proper_types)
  145. if underlying_type is None:
  146. return ctx.default_attr_type
  147. # At first we try to predict future `value` type if all other items
  148. # have the same type. For example, `int`.
  149. # If this is the case, we simply return this type.
  150. # See https://github.com/python/mypy/pull/9443
  151. all_same_value_type = all(
  152. proper_type is not None and proper_type == underlying_type
  153. for proper_type in proper_types
  154. )
  155. if all_same_value_type:
  156. if underlying_type is not None:
  157. return underlying_type
  158. # But, after we started treating all `Enum` values as `Final`,
  159. # we start to infer types in
  160. # `item = 1` as `Literal[1]`, not just `int`.
  161. # So, for example types in this `Enum` will all be different:
  162. #
  163. # class Ordering(IntEnum):
  164. # one = 1
  165. # two = 2
  166. # three = 3
  167. #
  168. # We will infer three `Literal` types here.
  169. # They are not the same, but they are equivalent.
  170. # So, we unify them to make sure `.value` prediction still works.
  171. # Result will be `Literal[1] | Literal[2] | Literal[3]` for this case.
  172. all_equivalent_types = all(
  173. proper_type is not None and is_equivalent(proper_type, underlying_type)
  174. for proper_type in proper_types
  175. )
  176. if all_equivalent_types:
  177. return make_simplified_union(cast(Sequence[Type], proper_types))
  178. return ctx.default_attr_type
  179. assert isinstance(ctx.type, Instance)
  180. info = ctx.type.type
  181. # As long as mypy doesn't understand attribute creation in __new__,
  182. # there is no way to predict the value type if the enum class has a
  183. # custom implementation
  184. if _implements_new(info):
  185. return ctx.default_attr_type
  186. stnode = info.get(enum_field_name)
  187. if stnode is None:
  188. return ctx.default_attr_type
  189. underlying_type = _infer_value_type_with_auto_fallback(ctx, get_proper_type(stnode.type))
  190. if underlying_type is None:
  191. return ctx.default_attr_type
  192. return underlying_type
  193. def _extract_underlying_field_name(typ: Type) -> str | None:
  194. """If the given type corresponds to some Enum instance, returns the
  195. original name of that enum. For example, if we receive in the type
  196. corresponding to 'SomeEnum.FOO', we return the string "SomeEnum.Foo".
  197. This helper takes advantage of the fact that Enum instances are valid
  198. to use inside Literal[...] types. An expression like 'SomeEnum.FOO' is
  199. actually represented by an Instance type with a Literal enum fallback.
  200. We can examine this Literal fallback to retrieve the string.
  201. """
  202. typ = get_proper_type(typ)
  203. if not isinstance(typ, Instance):
  204. return None
  205. if not typ.type.is_enum:
  206. return None
  207. underlying_literal = typ.last_known_value
  208. if underlying_literal is None:
  209. return None
  210. # The checks above have verified this LiteralType is representing an enum value,
  211. # which means the 'value' field is guaranteed to be the name of the enum field
  212. # as a string.
  213. assert isinstance(underlying_literal.value, str)
  214. return underlying_literal.value