Skip to content

Commit 0b4ccae

Browse files
authored
consolidate literal bool argument error messages (#14693)
Follow up on some of the recurring feedback from #14580 and #14657. There are many error messages similar to `X must be True or False.` in MyPy. This commit updates them all to: - remove the dangling period for consistency with other error messages - clarify that we need a `True` or `False` literal - use the `literal-required` error code for consistency with other literal errors This should have no impact outside of error message formatting.
1 parent 563e29d commit 0b4ccae

9 files changed

+84
-40
lines changed

mypy/plugins/attrs.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing_extensions import Final, Literal
77

88
import mypy.plugin # To avoid circular imports.
9+
from mypy.errorcodes import LITERAL_REQ
910
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
1011
from mypy.nodes import (
1112
ARG_NAMED,
@@ -246,7 +247,11 @@ def _get_decorator_optional_bool_argument(
246247
return False
247248
if attr_value.fullname == "builtins.None":
248249
return None
249-
ctx.api.fail(f'"{name}" argument must be True or False.', ctx.reason)
250+
ctx.api.fail(
251+
f'"{name}" argument must be a True, False, or None literal',
252+
ctx.reason,
253+
code=LITERAL_REQ,
254+
)
250255
return default
251256
return default
252257
else:

mypy/plugins/common.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
Var,
2121
)
2222
from mypy.plugin import CheckerPluginInterface, ClassDefContext, SemanticAnalyzerPluginInterface
23-
from mypy.semanal_shared import ALLOW_INCOMPATIBLE_OVERRIDE, set_callable_name
23+
from mypy.semanal_shared import (
24+
ALLOW_INCOMPATIBLE_OVERRIDE,
25+
require_bool_literal_argument,
26+
set_callable_name,
27+
)
2428
from mypy.typeops import ( # noqa: F401 # Part of public API
2529
try_getting_str_literals as try_getting_str_literals,
2630
)
@@ -54,11 +58,7 @@ def _get_bool_argument(ctx: ClassDefContext, expr: CallExpr, name: str, default:
5458
"""
5559
attr_value = _get_argument(expr, name)
5660
if attr_value:
57-
ret = ctx.api.parse_bool(attr_value)
58-
if ret is None:
59-
ctx.api.fail(f'"{name}" argument must be True or False.', expr)
60-
return default
61-
return ret
61+
return require_bool_literal_argument(ctx.api, attr_value, name, default)
6262
return default
6363

6464

mypy/plugins/dataclasses.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
add_method_to_class,
4242
deserialize_and_fixup_type,
4343
)
44-
from mypy.semanal_shared import find_dataclass_transform_spec
44+
from mypy.semanal_shared import find_dataclass_transform_spec, require_bool_literal_argument
4545
from mypy.server.trigger import make_wildcard_trigger
4646
from mypy.state import state
4747
from mypy.typeops import map_type_from_supertype
@@ -678,11 +678,7 @@ def _get_bool_arg(self, name: str, default: bool) -> bool:
678678
# class's keyword arguments (ie `class Subclass(Parent, kwarg1=..., kwarg2=...)`)
679679
expression = self._cls.keywords.get(name)
680680
if expression is not None:
681-
value = self._api.parse_bool(self._cls.keywords[name])
682-
if value is not None:
683-
return value
684-
else:
685-
self._api.fail(f'"{name}" argument must be True or False', expression)
681+
return require_bool_literal_argument(self._api, expression, name, default)
686682
return default
687683

688684

mypy/semanal.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@
216216
calculate_tuple_fallback,
217217
find_dataclass_transform_spec,
218218
has_placeholder,
219+
require_bool_literal_argument,
219220
set_callable_name as set_callable_name,
220221
)
221222
from mypy.semanal_typeddict import TypedDictAnalyzer
@@ -6473,15 +6474,19 @@ def parse_dataclass_transform_spec(self, call: CallExpr) -> DataclassTransformSp
64736474
typing.dataclass_transform."""
64746475
parameters = DataclassTransformSpec()
64756476
for name, value in zip(call.arg_names, call.args):
6477+
# Skip any positional args. Note that any such args are invalid, but we can rely on
6478+
# typeshed to enforce this and don't need an additional error here.
6479+
if name is None:
6480+
continue
6481+
64766482
# field_specifiers is currently the only non-boolean argument; check for it first so
64776483
# so the rest of the block can fail through to handling booleans
64786484
if name == "field_specifiers":
64796485
self.fail('"field_specifiers" support is currently unimplemented', call)
64806486
continue
64816487

6482-
boolean = self.parse_bool(value)
6488+
boolean = require_bool_literal_argument(self, value, name)
64836489
if boolean is None:
6484-
self.fail(f'"{name}" argument must be a True or False literal', call)
64856490
continue
64866491

64876492
if name == "eq_default":

mypy/semanal_shared.py

+42-3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
from __future__ import annotations
44

55
from abc import abstractmethod
6-
from typing import Callable
7-
from typing_extensions import Final, Protocol
6+
from typing import Callable, overload
7+
from typing_extensions import Final, Literal, Protocol
88

99
from mypy_extensions import trait
1010

1111
from mypy import join
12-
from mypy.errorcodes import ErrorCode
12+
from mypy.errorcodes import LITERAL_REQ, ErrorCode
1313
from mypy.nodes import (
1414
CallExpr,
1515
ClassDef,
@@ -26,6 +26,7 @@
2626
SymbolTableNode,
2727
TypeInfo,
2828
)
29+
from mypy.plugin import SemanticAnalyzerPluginInterface
2930
from mypy.tvar_scope import TypeVarLikeScope
3031
from mypy.type_visitor import ANY_STRATEGY, BoolTypeQuery
3132
from mypy.types import (
@@ -420,3 +421,41 @@ def find_dataclass_transform_spec(node: Node | None) -> DataclassTransformSpec |
420421
return metaclass_type.type.dataclass_transform_spec
421422

422423
return None
424+
425+
426+
# Never returns `None` if a default is given
427+
@overload
428+
def require_bool_literal_argument(
429+
api: SemanticAnalyzerInterface | SemanticAnalyzerPluginInterface,
430+
expression: Expression,
431+
name: str,
432+
default: Literal[True] | Literal[False],
433+
) -> bool:
434+
...
435+
436+
437+
@overload
438+
def require_bool_literal_argument(
439+
api: SemanticAnalyzerInterface | SemanticAnalyzerPluginInterface,
440+
expression: Expression,
441+
name: str,
442+
default: None = None,
443+
) -> bool | None:
444+
...
445+
446+
447+
def require_bool_literal_argument(
448+
api: SemanticAnalyzerInterface | SemanticAnalyzerPluginInterface,
449+
expression: Expression,
450+
name: str,
451+
default: bool | None = None,
452+
) -> bool | None:
453+
"""Attempt to interpret an expression as a boolean literal, and fail analysis if we can't."""
454+
value = api.parse_bool(expression)
455+
if value is None:
456+
api.fail(
457+
f'"{name}" argument must be a True or False literal', expression, code=LITERAL_REQ
458+
)
459+
return default
460+
461+
return value

mypy/semanal_typeddict.py

+8-9
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@
3131
TypeInfo,
3232
)
3333
from mypy.options import Options
34-
from mypy.semanal_shared import SemanticAnalyzerInterface, has_placeholder
34+
from mypy.semanal_shared import (
35+
SemanticAnalyzerInterface,
36+
has_placeholder,
37+
require_bool_literal_argument,
38+
)
3539
from mypy.typeanal import check_for_explicit_any, has_any_from_unimported_type
3640
from mypy.types import (
3741
TPDICT_NAMES,
@@ -320,10 +324,7 @@ def analyze_typeddict_classdef_fields(
320324
self.fail("Right hand side values are not supported in TypedDict", stmt)
321325
total: bool | None = True
322326
if "total" in defn.keywords:
323-
total = self.api.parse_bool(defn.keywords["total"])
324-
if total is None:
325-
self.fail('Value of "total" must be True or False', defn)
326-
total = True
327+
total = require_bool_literal_argument(self.api, defn.keywords["total"], "total", True)
327328
required_keys = {
328329
field
329330
for (field, t) in zip(fields, types)
@@ -436,11 +437,9 @@ def parse_typeddict_args(
436437
)
437438
total: bool | None = True
438439
if len(args) == 3:
439-
total = self.api.parse_bool(call.args[2])
440+
total = require_bool_literal_argument(self.api, call.args[2], "total")
440441
if total is None:
441-
return self.fail_typeddict_arg(
442-
'TypedDict() "total" argument must be True or False', call
443-
)
442+
return "", [], [], True, [], False
444443
dictexpr = args[1]
445444
tvar_defs = self.api.get_and_bind_all_tvars([t for k, t in dictexpr.items])
446445
res = self.parse_typeddict_fields_with_types(dictexpr.items, call)

test-data/unit/check-attr.test

+3-3
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@ class D:
151151
[case testAttrsNotBooleans]
152152
import attr
153153
x = True
154-
@attr.s(cmp=x) # E: "cmp" argument must be True or False.
154+
@attr.s(cmp=x) # E: "cmp" argument must be a True, False, or None literal
155155
class A:
156-
a = attr.ib(init=x) # E: "init" argument must be True or False.
156+
a = attr.ib(init=x) # E: "init" argument must be a True or False literal
157157
[builtins fixtures/bool.pyi]
158158

159159
[case testAttrsInitFalse]
@@ -1866,4 +1866,4 @@ reveal_type(D) # N: Revealed type is "def (a: builtins.int, b: builtins.str) ->
18661866
D(1, "").a = 2 # E: Cannot assign to final attribute "a"
18671867
D(1, "").b = "2" # E: Cannot assign to final attribute "b"
18681868

1869-
[builtins fixtures/property.pyi]
1869+
[builtins fixtures/property.pyi]

test-data/unit/check-dataclass-transform.test

+4-4
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,12 @@ class BaseClass:
8383
class Metaclass(type): ...
8484

8585
BOOL_CONSTANT = True
86-
@my_dataclass(eq=BOOL_CONSTANT) # E: "eq" argument must be True or False.
86+
@my_dataclass(eq=BOOL_CONSTANT) # E: "eq" argument must be a True or False literal
8787
class A: ...
88-
@my_dataclass(order=not False) # E: "order" argument must be True or False.
88+
@my_dataclass(order=not False) # E: "order" argument must be a True or False literal
8989
class B: ...
90-
class C(BaseClass, eq=BOOL_CONSTANT): ... # E: "eq" argument must be True or False
91-
class D(metaclass=Metaclass, order=not False): ... # E: "order" argument must be True or False
90+
class C(BaseClass, eq=BOOL_CONSTANT): ... # E: "eq" argument must be a True or False literal
91+
class D(metaclass=Metaclass, order=not False): ... # E: "order" argument must be a True or False literal
9292

9393
[typing fixtures/typing-full.pyi]
9494
[builtins fixtures/dataclasses.pyi]

test-data/unit/check-typeddict.test

+6-6
Original file line numberDiff line numberDiff line change
@@ -1084,8 +1084,8 @@ reveal_type(d) \
10841084

10851085
[case testTypedDictWithInvalidTotalArgument]
10861086
from mypy_extensions import TypedDict
1087-
A = TypedDict('A', {'x': int}, total=0) # E: TypedDict() "total" argument must be True or False
1088-
B = TypedDict('B', {'x': int}, total=bool) # E: TypedDict() "total" argument must be True or False
1087+
A = TypedDict('A', {'x': int}, total=0) # E: "total" argument must be a True or False literal
1088+
B = TypedDict('B', {'x': int}, total=bool) # E: "total" argument must be a True or False literal
10891089
C = TypedDict('C', {'x': int}, x=False) # E: Unexpected keyword argument "x" for "TypedDict"
10901090
D = TypedDict('D', {'x': int}, False) # E: Unexpected arguments to TypedDict()
10911091
[builtins fixtures/dict.pyi]
@@ -1179,12 +1179,12 @@ reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {'x'?: builtins.in
11791179

11801180
[case testTypedDictClassWithInvalidTotalArgument]
11811181
from mypy_extensions import TypedDict
1182-
class D(TypedDict, total=1): # E: Value of "total" must be True or False
1182+
class D(TypedDict, total=1): # E: "total" argument must be a True or False literal
11831183
x: int
1184-
class E(TypedDict, total=bool): # E: Value of "total" must be True or False
1184+
class E(TypedDict, total=bool): # E: "total" argument must be a True or False literal
11851185
x: int
1186-
class F(TypedDict, total=xyz): # E: Value of "total" must be True or False \
1187-
# E: Name "xyz" is not defined
1186+
class F(TypedDict, total=xyz): # E: Name "xyz" is not defined \
1187+
# E: "total" argument must be a True or False literal
11881188
x: int
11891189
[builtins fixtures/dict.pyi]
11901190

0 commit comments

Comments
 (0)