From 135abfc8eb70d1f9bfcebac1edc5626c5c0c1f67 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 5 Jul 2024 15:31:17 +0300 Subject: [PATCH] Add `--strict-bool` flag to prohib treating `bool` as `int` --- docs/source/command_line.rst | 20 +++++++++++++++++++ mypy/main.py | 8 ++++++++ mypy/options.py | 5 +++++ mypy/subtypes.py | 7 +++++++ test-data/unit/check-flags.test | 34 +++++++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+) diff --git a/docs/source/command_line.rst b/docs/source/command_line.rst index 50a6ef65f4d08..6e2b9dd4396aa 100644 --- a/docs/source/command_line.rst +++ b/docs/source/command_line.rst @@ -642,6 +642,26 @@ of the above sections. assert text is not None # OK, check against None is allowed as a special case. +.. option:: --strict-bool + + By default ``bool`` values are treated as subtypes of ``int``, + just like in runtime: + + .. code-block:: python + + >>> bool.__mro__ + (, , ) + + While it will work in runtime, + some cases might require a little bit more strictness. + With this flag enabled, you will get the following error: + + .. code-block:: python + + def requires_int(arg: int) -> None: ... + + requires_int(5 > 0) # Error: Argument 1 has incompatible type "bool"; expected "int" + .. option:: --extra-checks This flag enables additional checks that are technically correct but may be diff --git a/mypy/main.py b/mypy/main.py index 05044335ecee0..fc42b95a8ebb2 100644 --- a/mypy/main.py +++ b/mypy/main.py @@ -843,6 +843,14 @@ def add_invertible_flag( group=strictness_group, ) + add_invertible_flag( + "--strict-bool", + default=False, + strict_flag=True, + help="Prohib to treat bool as int", + group=strictness_group, + ) + add_invertible_flag( "--extra-checks", default=False, diff --git a/mypy/options.py b/mypy/options.py index 5ef6bc2a35e71..ea725815930ab 100644 --- a/mypy/options.py +++ b/mypy/options.py @@ -50,6 +50,7 @@ class BuildType: "mypyc", "strict_concatenate", "strict_equality", + "strict_bool", "strict_optional", "warn_no_return", "warn_return_any", @@ -208,6 +209,10 @@ def __init__(self) -> None: # This makes 1 == '1', 1 in ['1'], and 1 is '1' errors. self.strict_equality = False + # Prohibit to treat `bool` as `int` in subtyping contexts. + # This makes `1 + True` an error. + self.strict_bool = False + # Deprecated, use extra_checks instead. self.strict_concatenate = False diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 649cbae4c8318..e332cff98a97b 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -512,6 +512,13 @@ def visit_instance(self, left: Instance) -> bool: if left.type.alt_promote and left.type.alt_promote.type is right.type: return True rname = right.type.fullname + if ( + self.options + and self.options.strict_bool + and left.type.fullname == "builtins.bool" + and rname == "builtins.int" + ): + return False # Always try a nominal check if possible, # there might be errors that a user wants to silence *once*. # NamedTuples are a special case, because `NamedTuple` is not listed diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 62711d5f0071a..abf5ee014c03c 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -2284,3 +2284,37 @@ class C(Generic[T]): ... A = Union[C, List] # OK [builtins fixtures/list.pyi] + +[case testStrictBool] +# flags: --strict-bool --show-error-codes +from typing import List, Union + +def a(x: int): ... +a(True) # E: Argument 1 to "a" has incompatible type "bool"; expected "int" [arg-type] +a(False) # E: Argument 1 to "a" has incompatible type "bool"; expected "int" [arg-type] + +bl: bool +a(bl) # E: Argument 1 to "a" has incompatible type "bool"; expected "int" [arg-type] + +def b() -> int: + return bl # E: Incompatible return value type (got "bool", expected "int") [return-value] + +c: List[int] = [ + True, # E: List item 0 has incompatible type "bool"; expected "int" [list-item] + False, # E: List item 1 has incompatible type "bool"; expected "int" [list-item] + bl, # E: List item 2 has incompatible type "bool"; expected "int" [list-item] +] + +# OK: +def d(x: Union[int, bool], y: bool): ... +d(1, True) +d(True, False) +d(bl, bl) +[builtins fixtures/list.pyi] + +[case testStrictBoolWithStrictFlag] +# flags: --strict + +def a(x: int) -> None: ... +b: bool +a(b) # E: Argument 1 to "a" has incompatible type "bool"; expected "int"