Skip to content

Commit 29c62b3

Browse files
Merge branch 'master' into for-loop-len-cache
2 parents 1fa74d3 + dcaca0e commit 29c62b3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+2052
-544
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ Mypy can be integrated into popular IDEs:
142142
- Emacs: using [Flycheck](https://github.com/flycheck/)
143143
- Sublime Text: [SublimeLinter-contrib-mypy](https://github.com/fredcallaway/SublimeLinter-contrib-mypy)
144144
- PyCharm: [mypy plugin](https://github.com/dropbox/mypy-PyCharm-plugin)
145+
- IDLE: [idlemypyextension](https://github.com/CoolCat467/idlemypyextension)
145146
- pre-commit: use [pre-commit mirrors-mypy](https://github.com/pre-commit/mirrors-mypy), although
146147
note by default this will limit mypy's ability to analyse your third party dependencies.
147148

docs/requirements-docs.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
-r ../mypy-requirements.txt
12
sphinx>=8.1.0
23
furo>=2022.3.4
34
myst-parser>=4.0.0

docs/source/command_line.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,9 +728,22 @@ of the above sections.
728728
if text != b'other bytes': # Error: non-overlapping equality check!
729729
...
730730
731-
assert text is not None # OK, check against None is allowed as a special case.
731+
assert text is not None # OK, check against None is allowed
732732
733733
734+
.. option:: --strict-equality-for-none
735+
736+
This flag extends :option:`--strict-equality <mypy --strict-equality>` for checks
737+
against ``None``:
738+
739+
.. code-block:: python
740+
741+
text: str
742+
assert text is not None # Error: non-overlapping identity check!
743+
744+
Note that :option:`--strict-equality-for-none <mypy --strict-equality-for-none>`
745+
only works in combination with :option:`--strict-equality <mypy --strict-equality>`.
746+
734747
.. option:: --strict-bytes
735748

736749
By default, mypy treats ``bytearray`` and ``memoryview`` as subtypes of ``bytes`` which

docs/source/config_file.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,7 +834,15 @@ section of the command line docs.
834834
:default: False
835835

836836
Prohibit equality checks, identity checks, and container checks between
837-
non-overlapping types.
837+
non-overlapping types (except ``None``).
838+
839+
.. confval:: strict_equality_for_none
840+
841+
:type: boolean
842+
:default: False
843+
844+
Include ``None`` in strict equality checks (requires :confval:`strict_equality`
845+
to be activated).
838846

839847
.. confval:: strict_bytes
840848

docs/source/error_code_list2.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,29 @@ literal:
145145
def is_magic(x: bytes) -> bool:
146146
return x == b'magic' # OK
147147
148+
:option:`--strict-equality <mypy --strict-equality>` does not include comparisons with
149+
``None``:
150+
151+
.. code-block:: python
152+
153+
# mypy: strict-equality
154+
155+
def is_none(x: str) -> bool:
156+
return x is None # OK
157+
158+
If you want such checks, you must also activate
159+
:option:`--strict-equality-for-none <mypy --strict-equality-for-none>` (we might merge
160+
these two options later).
161+
162+
.. code-block:: python
163+
164+
# mypy: strict-equality strict-equality-for-none
165+
166+
def is_none(x: str) -> bool:
167+
# Error: Non-overlapping identity check
168+
# (left operand type: "str", right operand type: "None")
169+
return x is None
170+
148171
.. _code-no-untyped-call:
149172

150173
Check that no untyped functions are called [no-untyped-call]

mypy/cache.py

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@
33
from collections.abc import Sequence
44
from typing import TYPE_CHECKING, Final
55

6+
from mypy_extensions import u8
7+
68
try:
79
from native_internal import (
810
Buffer as Buffer,
911
read_bool as read_bool,
1012
read_float as read_float,
1113
read_int as read_int,
1214
read_str as read_str,
15+
read_tag as read_tag,
1316
write_bool as write_bool,
1417
write_float as write_float,
1518
write_int as write_int,
1619
write_str as write_str,
20+
write_tag as write_tag,
1721
)
1822
except ImportError:
1923
# TODO: temporary, remove this after we publish mypy-native on PyPI.
@@ -32,6 +36,12 @@ def read_int(data: Buffer) -> int:
3236
def write_int(data: Buffer, value: int) -> None:
3337
raise NotImplementedError
3438

39+
def read_tag(data: Buffer) -> u8:
40+
raise NotImplementedError
41+
42+
def write_tag(data: Buffer, value: u8) -> None:
43+
raise NotImplementedError
44+
3545
def read_str(data: Buffer) -> str:
3646
raise NotImplementedError
3747

@@ -51,45 +61,48 @@ def write_float(data: Buffer, value: float) -> None:
5161
raise NotImplementedError
5262

5363

54-
LITERAL_INT: Final = 1
55-
LITERAL_STR: Final = 2
56-
LITERAL_BOOL: Final = 3
57-
LITERAL_FLOAT: Final = 4
58-
LITERAL_COMPLEX: Final = 5
59-
LITERAL_NONE: Final = 6
64+
# Always use this type alias to refer to type tags.
65+
Tag = u8
66+
67+
LITERAL_INT: Final[Tag] = 1
68+
LITERAL_STR: Final[Tag] = 2
69+
LITERAL_BOOL: Final[Tag] = 3
70+
LITERAL_FLOAT: Final[Tag] = 4
71+
LITERAL_COMPLEX: Final[Tag] = 5
72+
LITERAL_NONE: Final[Tag] = 6
6073

6174

62-
def read_literal(data: Buffer, marker: int) -> int | str | bool | float:
63-
if marker == LITERAL_INT:
75+
def read_literal(data: Buffer, tag: Tag) -> int | str | bool | float:
76+
if tag == LITERAL_INT:
6477
return read_int(data)
65-
elif marker == LITERAL_STR:
78+
elif tag == LITERAL_STR:
6679
return read_str(data)
67-
elif marker == LITERAL_BOOL:
80+
elif tag == LITERAL_BOOL:
6881
return read_bool(data)
69-
elif marker == LITERAL_FLOAT:
82+
elif tag == LITERAL_FLOAT:
7083
return read_float(data)
71-
assert False, f"Unknown literal marker {marker}"
84+
assert False, f"Unknown literal tag {tag}"
7285

7386

7487
def write_literal(data: Buffer, value: int | str | bool | float | complex | None) -> None:
7588
if isinstance(value, bool):
76-
write_int(data, LITERAL_BOOL)
89+
write_tag(data, LITERAL_BOOL)
7790
write_bool(data, value)
7891
elif isinstance(value, int):
79-
write_int(data, LITERAL_INT)
92+
write_tag(data, LITERAL_INT)
8093
write_int(data, value)
8194
elif isinstance(value, str):
82-
write_int(data, LITERAL_STR)
95+
write_tag(data, LITERAL_STR)
8396
write_str(data, value)
8497
elif isinstance(value, float):
85-
write_int(data, LITERAL_FLOAT)
98+
write_tag(data, LITERAL_FLOAT)
8699
write_float(data, value)
87100
elif isinstance(value, complex):
88-
write_int(data, LITERAL_COMPLEX)
101+
write_tag(data, LITERAL_COMPLEX)
89102
write_float(data, value.real)
90103
write_float(data, value.imag)
91104
else:
92-
write_int(data, LITERAL_NONE)
105+
write_tag(data, LITERAL_NONE)
93106

94107

95108
def read_int_opt(data: Buffer) -> int | None:

mypy/checker.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5552,10 +5552,10 @@ def visit_continue_stmt(self, s: ContinueStmt) -> None:
55525552
return
55535553

55545554
def visit_match_stmt(self, s: MatchStmt) -> None:
5555-
named_subject = self._make_named_statement_for_match(s)
55565555
# In sync with similar actions elsewhere, narrow the target if
55575556
# we are matching an AssignmentExpr
55585557
unwrapped_subject = collapse_walrus(s.subject)
5558+
named_subject = self._make_named_statement_for_match(s, unwrapped_subject)
55595559
with self.binder.frame_context(can_skip=False, fall_through=0):
55605560
subject_type = get_proper_type(self.expr_checker.accept(s.subject))
55615561

@@ -5646,9 +5646,8 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
56465646
with self.binder.frame_context(can_skip=False, fall_through=2):
56475647
pass
56485648

5649-
def _make_named_statement_for_match(self, s: MatchStmt) -> Expression:
5649+
def _make_named_statement_for_match(self, s: MatchStmt, subject: Expression) -> Expression:
56505650
"""Construct a fake NameExpr for inference if a match clause is complex."""
5651-
subject = s.subject
56525651
if self.binder.can_put_directly(subject):
56535652
# Already named - we should infer type of it as given
56545653
return subject

mypy/checker_shared.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ def checking_await_set(self) -> Iterator[None]:
272272
def get_precise_awaitable_type(self, typ: Type, local_errors: ErrorWatcher) -> Type | None:
273273
raise NotImplementedError
274274

275+
@abstractmethod
276+
def add_any_attribute_to_type(self, typ: Type, name: str) -> Type:
277+
raise NotImplementedError
278+
275279
@abstractmethod
276280
def is_defined_in_stub(self, typ: Instance, /) -> bool:
277281
raise NotImplementedError

mypy/checkexpr.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3721,7 +3721,7 @@ def visit_comparison_expr(self, e: ComparisonExpr) -> Type:
37213721
elif operator == "is" or operator == "is not":
37223722
right_type = self.accept(right) # validate the right operand
37233723
sub_result = self.bool_type()
3724-
if self.dangerous_comparison(left_type, right_type):
3724+
if self.dangerous_comparison(left_type, right_type, identity_check=True):
37253725
# Show the most specific literal types possible
37263726
left_type = try_getting_literal(left_type)
37273727
right_type = try_getting_literal(right_type)
@@ -3763,6 +3763,7 @@ def dangerous_comparison(
37633763
original_container: Type | None = None,
37643764
seen_types: set[tuple[Type, Type]] | None = None,
37653765
prefer_literal: bool = True,
3766+
identity_check: bool = False,
37663767
) -> bool:
37673768
"""Check for dangerous non-overlapping comparisons like 42 == 'no'.
37683769
@@ -3790,10 +3791,12 @@ def dangerous_comparison(
37903791

37913792
left, right = get_proper_types((left, right))
37923793

3793-
# We suppress the error if there is a custom __eq__() method on either
3794-
# side. User defined (or even standard library) classes can define this
3794+
# We suppress the error for equality and container checks if there is a custom __eq__()
3795+
# method on either side. User defined (or even standard library) classes can define this
37953796
# to return True for comparisons between non-overlapping types.
3796-
if custom_special_method(left, "__eq__") or custom_special_method(right, "__eq__"):
3797+
if (
3798+
custom_special_method(left, "__eq__") or custom_special_method(right, "__eq__")
3799+
) and not identity_check:
37973800
return False
37983801

37993802
if prefer_literal:
@@ -3817,7 +3820,10 @@ def dangerous_comparison(
38173820
#
38183821
# TODO: find a way of disabling the check only for types resulted from the expansion.
38193822
return False
3820-
if isinstance(left, NoneType) or isinstance(right, NoneType):
3823+
if self.chk.options.strict_equality_for_none:
3824+
if isinstance(left, NoneType) and isinstance(right, NoneType):
3825+
return False
3826+
elif isinstance(left, NoneType) or isinstance(right, NoneType):
38213827
return False
38223828
if isinstance(left, UnionType) and isinstance(right, UnionType):
38233829
left = remove_optional(left)
@@ -6004,6 +6010,10 @@ def analyze_cond_branch(
60046010
def _combined_context(self, ty: Type | None) -> Type | None:
60056011
ctx_items = []
60066012
if ty is not None:
6013+
if has_any_type(ty):
6014+
# HACK: Any should be contagious, `dict[str, Any] or <x>` should still
6015+
# infer Any in x.
6016+
return ty
60076017
ctx_items.append(ty)
60086018
if self.type_context and self.type_context[-1] is not None:
60096019
ctx_items.append(self.type_context[-1])

mypy/checkpattern.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -671,12 +671,15 @@ def visit_class_pattern(self, o: ClassPattern) -> PatternType:
671671
has_local_errors = local_errors.has_new_errors()
672672
if has_local_errors or key_type is None:
673673
key_type = AnyType(TypeOfAny.from_error)
674-
self.msg.fail(
675-
message_registry.CLASS_PATTERN_UNKNOWN_KEYWORD.format(
676-
typ.str_with_options(self.options), keyword
677-
),
678-
pattern,
679-
)
674+
if not (type_info and type_info.fullname == "builtins.object"):
675+
self.msg.fail(
676+
message_registry.CLASS_PATTERN_UNKNOWN_KEYWORD.format(
677+
typ.str_with_options(self.options), keyword
678+
),
679+
pattern,
680+
)
681+
elif keyword is not None:
682+
new_type = self.chk.add_any_attribute_to_type(new_type, keyword)
680683

681684
inner_type, inner_rest_type, inner_captures = self.accept(pattern, key_type)
682685
if is_uninhabited(inner_type):

0 commit comments

Comments
 (0)