Skip to content

Commit 871d204

Browse files
committed
Improve error messages
2 parents aafc409 + 8c8aa10 commit 871d204

40 files changed

+874
-130
lines changed

CHANGELOG.md

Lines changed: 120 additions & 46 deletions
Large diffs are not rendered by default.

docs/requirements-docs.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
sphinx>=4.2.0,<5.0.0
1+
sphinx>=5.1.0
22
furo>=2022.3.4

docs/source/class_basics.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ effect at runtime:
263263
Abstract base classes and multiple inheritance
264264
**********************************************
265265

266-
Mypy supports Python :doc:`abstract base classes <library/abc>` (ABCs). Abstract classes
266+
Mypy supports Python :doc:`abstract base classes <python:library/abc>` (ABCs). Abstract classes
267267
have at least one abstract method or property that must be implemented
268268
by any *concrete* (non-abstract) subclass. You can define abstract base
269269
classes using the :py:class:`abc.ABCMeta` metaclass and the :py:func:`@abc.abstractmethod <abc.abstractmethod>`
@@ -371,8 +371,7 @@ property or an instance variable.
371371
Slots
372372
*****
373373

374-
When a class has explicitly defined
375-
`__slots__ <https://docs.python.org/3/reference/datamodel.html#slots>`_,
374+
When a class has explicitly defined :std:term:`__slots__`,
376375
mypy will check that all attributes assigned to are members of ``__slots__``:
377376

378377
.. code-block:: python

docs/source/config_file.rst

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,8 @@ section of the command line docs.
238238
Crafting a single regular expression that excludes multiple files while remaining
239239
human-readable can be a challenge. The above example demonstrates one approach.
240240
``(?x)`` enables the ``VERBOSE`` flag for the subsequent regular expression, which
241-
`ignores most whitespace and supports comments`__. The above is equivalent to:
242-
``(^one\.py$|two\.pyi$|^three\.)``.
243-
244-
.. __: https://docs.python.org/3/library/re.html#re.X
241+
:py:data:`ignores most whitespace and supports comments <re.VERBOSE>`.
242+
The above is equivalent to: ``(^one\.py$|two\.pyi$|^three\.)``.
245243

246244
For more details, see :option:`--exclude <mypy --exclude>`.
247245

docs/source/error_code_list2.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -524,8 +524,7 @@ that only existed during type-checking.
524524
In runtime it fails with expected ``NameError``,
525525
which can cause real problem in production, hidden from mypy.
526526

527-
But, in Python3.11 ``reveal_type``
528-
`was added to typing.py <https://docs.python.org/3/library/typing.html#typing.reveal_type>`_.
527+
But, in Python3.11 :py:func:`typing.reveal_type` was added.
529528
``typing_extensions`` ported this helper to all supported Python versions.
530529

531530
Now users can actually import ``reveal_type`` to make the runtime code safe.

docs/source/getting_started.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,7 @@ Mypy can also understand how to work with types from libraries that you use.
256256

257257
For instance, mypy comes out of the box with an intimate knowledge of the
258258
Python standard library. For example, here is a function which uses the
259-
``Path`` object from the
260-
`pathlib standard library module <https://docs.python.org/3/library/pathlib.html>`_:
259+
``Path`` object from the :doc:`pathlib standard library module <python:library/pathlib>`:
261260

262261
.. code-block:: python
263262

docs/source/html_builder.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
from sphinx.addnodes import document
1010
from sphinx.application import Sphinx
1111
from sphinx.builders.html import StandaloneHTMLBuilder
12+
from sphinx.environment import BuildEnvironment
1213

1314

1415
class MypyHTMLBuilder(StandaloneHTMLBuilder):
15-
def __init__(self, app: Sphinx) -> None:
16-
super().__init__(app)
16+
def __init__(self, app: Sphinx, env: BuildEnvironment) -> None:
17+
super().__init__(app, env)
1718
self._ref_to_doc = {}
1819

1920
def write_doc(self, docname: str, doctree: document) -> None:

docs/source/more_types.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -829,7 +829,7 @@ Typing async/await
829829

830830
Mypy lets you type coroutines that use the ``async/await`` syntax.
831831
For more information regarding coroutines, see :pep:`492` and the
832-
`asyncio documentation <https://docs.python.org/3/library/asyncio.html>`_.
832+
`asyncio documentation <python:library/asyncio>`_.
833833

834834
Functions defined using ``async def`` are typed similar to normal functions.
835835
The return type annotation should be the same as the type of the value you

misc/gen_blog_post_html.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
"""Converter from CHANGELOG.md (Markdown) to HTML suitable for a mypy blog post.
2+
3+
How to use:
4+
5+
1. Write release notes in CHANGELOG.md.
6+
2. Make sure the heading for the next release is of form `## Mypy X.Y`.
7+
2. Run `misc/gen_blog_post_html.py X.Y > target.html`.
8+
4. Manually inspect and tweak the result.
9+
10+
Notes:
11+
12+
* There are some fragile assumptions. Double check the output.
13+
"""
14+
15+
import argparse
16+
import html
17+
import os
18+
import re
19+
import sys
20+
21+
22+
def format_lists(h: str) -> str:
23+
a = h.splitlines()
24+
r = []
25+
i = 0
26+
bullets = ("- ", "* ", " * ")
27+
while i < len(a):
28+
if a[i].startswith(bullets):
29+
r.append("<p><ul>")
30+
while i < len(a) and a[i].startswith(bullets):
31+
r.append("<li>%s" % a[i][2:].lstrip())
32+
i += 1
33+
r.append("</ul>")
34+
else:
35+
r.append(a[i])
36+
i += 1
37+
return "\n".join(r)
38+
39+
40+
def format_code(h: str) -> str:
41+
a = h.splitlines()
42+
r = []
43+
i = 0
44+
while i < len(a):
45+
if a[i].startswith(" ") or a[i].startswith("```"):
46+
indent = a[i].startswith(" ")
47+
if not indent:
48+
i += 1
49+
r.append("<pre>")
50+
while i < len(a) and (
51+
(indent and a[i].startswith(" ")) or (not indent and not a[i].startswith("```"))
52+
):
53+
# Undo &gt; and &lt;
54+
line = a[i].replace("&gt;", ">").replace("&lt;", "<")
55+
if not indent:
56+
line = " " + line
57+
r.append(html.escape(line))
58+
i += 1
59+
r.append("</pre>")
60+
if not indent and a[i].startswith("```"):
61+
i += 1
62+
else:
63+
r.append(a[i])
64+
i += 1
65+
return "\n".join(r)
66+
67+
68+
def convert(src: str) -> str:
69+
h = src
70+
71+
# Replace < and >.
72+
h = re.sub(r"<", "&lt;", h)
73+
h = re.sub(r">", "&gt;", h)
74+
75+
# Title
76+
h = re.sub(r"^## (Mypy [0-9.]+)", r"<h1>\1 Released</h1>", h, flags=re.MULTILINE)
77+
78+
# Subheadings
79+
h = re.sub(r"\n#### ([A-Z`].*)\n", r"\n<h2>\1</h2>\n", h)
80+
81+
# Sub-subheadings
82+
h = re.sub(r"\n\*\*([A-Z_`].*)\*\*\n", r"\n<h3>\1</h3>\n", h)
83+
h = re.sub(r"\n`\*\*([A-Z_`].*)\*\*\n", r"\n<h3>`\1</h3>\n", h)
84+
85+
# Translate `**`
86+
h = re.sub(r"`\*\*`", "<tt>**</tt>", h)
87+
88+
# Paragraphs
89+
h = re.sub(r"\n([A-Z])", r"\n<p>\1", h)
90+
91+
# Bullet lists
92+
h = format_lists(h)
93+
94+
# Code blocks
95+
h = format_code(h)
96+
97+
# Code fragments
98+
h = re.sub(r"`([^`]+)`", r"<tt>\1</tt>", h)
99+
100+
# Remove **** noise
101+
h = re.sub(r"\*\*\*\*", "", h)
102+
103+
# Bold text
104+
h = re.sub(r"\*\*([A-Za-z].*?)\*\*", r" <b>\1</b>", h)
105+
106+
# Emphasized text
107+
h = re.sub(r" \*([A-Za-z].*?)\*", r" <i>\1</i>", h)
108+
109+
# Remove redundant PR links to avoid double links (they will be generated below)
110+
h = re.sub(r"\[(#[0-9]+)\]\(https://github.com/python/mypy/pull/[0-9]+/?\)", r"\1", h)
111+
112+
# Issue and PR links
113+
h = re.sub(r"\((#[0-9]+)\) +\(([^)]+)\)", r"(\2, \1)", h)
114+
h = re.sub(
115+
r"fixes #([0-9]+)",
116+
r'fixes issue <a href="https://github.com/python/mypy/issues/\1">\1</a>',
117+
h,
118+
)
119+
h = re.sub(r"#([0-9]+)", r'PR <a href="https://github.com/python/mypy/pull/\1">\1</a>', h)
120+
h = re.sub(r"\) \(PR", ", PR", h)
121+
122+
# Markdown links
123+
h = re.sub(r"\[([^]]*)\]\(([^)]*)\)", r'<a href="\2">\1</a>', h)
124+
125+
# Add random links in case they are missing
126+
h = re.sub(
127+
r"contributors to typeshed:",
128+
'contributors to <a href="https://github.com/python/typeshed">typeshed</a>:',
129+
h,
130+
)
131+
132+
# Add missing top-level HTML tags
133+
h = '<html>\n<meta charset="utf-8" />\n<body>\n' + h + "</body>\n</html>"
134+
135+
return h
136+
137+
138+
def extract_version(src: str, version: str) -> str:
139+
a = src.splitlines()
140+
i = 0
141+
heading = f"## Mypy {version}"
142+
while i < len(a):
143+
if a[i].strip() == heading:
144+
break
145+
i += 1
146+
else:
147+
raise RuntimeError(f"Can't find heading {heading!r}")
148+
j = i + 1
149+
while not a[j].startswith("## "):
150+
j += 1
151+
return "\n".join(a[i:j])
152+
153+
154+
def main() -> None:
155+
parser = argparse.ArgumentParser(
156+
description="Generate HTML release blog post based on CHANGELOG.md and write to stdout."
157+
)
158+
parser.add_argument("version", help="mypy version, in form X.Y or X.Y.Z")
159+
args = parser.parse_args()
160+
version: str = args.version
161+
if not re.match(r"[0-9]+(\.[0-9]+)+$", version):
162+
sys.exit(f"error: Version must be of form X.Y or X.Y.Z, not {version!r}")
163+
changelog_path = os.path.join(os.path.dirname(__file__), os.path.pardir, "CHANGELOG.md")
164+
src = open(changelog_path).read()
165+
src = extract_version(src, version)
166+
dst = convert(src)
167+
sys.stdout.write(dst)
168+
169+
170+
if __name__ == "__main__":
171+
main()

mypy/checker.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1879,6 +1879,7 @@ def check_explicit_override_decorator(
18791879
found_method_base_classes
18801880
and not defn.is_explicit_override
18811881
and defn.name not in ("__init__", "__new__")
1882+
and not is_private(defn.name)
18821883
):
18831884
self.msg.explicit_override_decorator_missing(
18841885
defn.name, found_method_base_classes[0].fullname, context or defn
@@ -1921,7 +1922,7 @@ def check_method_or_accessor_override_for_base(
19211922
base_attr = base.names.get(name)
19221923
if base_attr:
19231924
# First, check if we override a final (always an error, even with Any types).
1924-
if is_final_node(base_attr.node):
1925+
if is_final_node(base_attr.node) and not is_private(name):
19251926
self.msg.cant_override_final(name, base.name, defn)
19261927
# Second, final can't override anything writeable independently of types.
19271928
if defn.is_final:
@@ -2680,7 +2681,7 @@ class C(B, A[int]): ... # this is unsafe because...
26802681
ok = True
26812682
# Final attributes can never be overridden, but can override
26822683
# non-final read-only attributes.
2683-
if is_final_node(second.node):
2684+
if is_final_node(second.node) and not is_private(name):
26842685
self.msg.cant_override_final(name, base2.name, ctx)
26852686
if is_final_node(first.node):
26862687
self.check_if_final_var_override_writable(name, second.node, ctx)
@@ -3308,6 +3309,8 @@ def check_compatibility_final_super(
33083309
"""
33093310
if not isinstance(base_node, (Var, FuncBase, Decorator)):
33103311
return True
3312+
if is_private(node.name):
3313+
return True
33113314
if base_node.is_final and (node.is_final or not isinstance(base_node, Var)):
33123315
# Give this error only for explicit override attempt with `Final`, or
33133316
# if we are overriding a final method with variable.

mypy/checkexpr.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3617,8 +3617,9 @@ def dangerous_comparison(
36173617
self,
36183618
left: Type,
36193619
right: Type,
3620-
original_container: Type | None = None,
36213620
*,
3621+
original_container: Type | None = None,
3622+
seen_types: set[tuple[Type, Type]] | None = None,
36223623
prefer_literal: bool = True,
36233624
) -> bool:
36243625
"""Check for dangerous non-overlapping comparisons like 42 == 'no'.
@@ -3639,6 +3640,12 @@ def dangerous_comparison(
36393640
if not self.chk.options.strict_equality:
36403641
return False
36413642

3643+
if seen_types is None:
3644+
seen_types = set()
3645+
if (left, right) in seen_types:
3646+
return False
3647+
seen_types.add((left, right))
3648+
36423649
left, right = get_proper_types((left, right))
36433650

36443651
# We suppress the error if there is a custom __eq__() method on either
@@ -3694,17 +3701,21 @@ def dangerous_comparison(
36943701
abstract_set = self.chk.lookup_typeinfo("typing.AbstractSet")
36953702
left = map_instance_to_supertype(left, abstract_set)
36963703
right = map_instance_to_supertype(right, abstract_set)
3697-
return self.dangerous_comparison(left.args[0], right.args[0])
3704+
return self.dangerous_comparison(
3705+
left.args[0], right.args[0], seen_types=seen_types
3706+
)
36983707
elif left.type.has_base("typing.Mapping") and right.type.has_base("typing.Mapping"):
36993708
# Similar to above: Mapping ignores the classes, it just compares items.
37003709
abstract_map = self.chk.lookup_typeinfo("typing.Mapping")
37013710
left = map_instance_to_supertype(left, abstract_map)
37023711
right = map_instance_to_supertype(right, abstract_map)
37033712
return self.dangerous_comparison(
3704-
left.args[0], right.args[0]
3705-
) or self.dangerous_comparison(left.args[1], right.args[1])
3713+
left.args[0], right.args[0], seen_types=seen_types
3714+
) or self.dangerous_comparison(left.args[1], right.args[1], seen_types=seen_types)
37063715
elif left_name in ("builtins.list", "builtins.tuple") and right_name == left_name:
3707-
return self.dangerous_comparison(left.args[0], right.args[0])
3716+
return self.dangerous_comparison(
3717+
left.args[0], right.args[0], seen_types=seen_types
3718+
)
37083719
elif left_name in OVERLAPPING_BYTES_ALLOWLIST and right_name in (
37093720
OVERLAPPING_BYTES_ALLOWLIST
37103721
):
@@ -4902,7 +4913,7 @@ def tuple_context_matches(self, expr: TupleExpr, ctx: TupleType) -> bool:
49024913
return len([e for e in expr.items if not isinstance(e, StarExpr)]) <= len(ctx.items)
49034914
# For variadic context, the only easy case is when structure matches exactly.
49044915
# TODO: try using tuple type context in more cases.
4905-
if len([e for e in expr.items if not isinstance(e, StarExpr)]) != 1:
4916+
if len([e for e in expr.items if isinstance(e, StarExpr)]) != 1:
49064917
return False
49074918
expr_star_index = next(i for i, lv in enumerate(expr.items) if isinstance(lv, StarExpr))
49084919
return len(expr.items) == len(ctx.items) and ctx_unpack_index == expr_star_index
@@ -4941,6 +4952,9 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type:
49414952
if type_context_items is not None:
49424953
unpack_in_context = find_unpack_in_list(type_context_items) is not None
49434954
seen_unpack_in_items = False
4955+
allow_precise_tuples = (
4956+
unpack_in_context or PRECISE_TUPLE_TYPES in self.chk.options.enable_incomplete_feature
4957+
)
49444958

49454959
# Infer item types. Give up if there's a star expression
49464960
# that's not a Tuple.
@@ -4981,10 +4995,7 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type:
49814995
# result in an error later, just do something predictable here.
49824996
j += len(tt.items)
49834997
else:
4984-
if (
4985-
PRECISE_TUPLE_TYPES in self.chk.options.enable_incomplete_feature
4986-
and not seen_unpack_in_items
4987-
):
4998+
if allow_precise_tuples and not seen_unpack_in_items:
49884999
# Handle (x, *y, z), where y is e.g. tuple[Y, ...].
49895000
if isinstance(tt, Instance) and self.chk.type_is_iterable(tt):
49905001
item_type = self.chk.iterable_item_type(tt, e)

mypy/constraints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,7 @@ def visit_instance(self, template: Instance) -> list[Constraint]:
949949
for item in actual.items:
950950
if isinstance(item, UnpackType):
951951
unpacked = get_proper_type(item.type)
952-
if isinstance(unpacked, TypeVarType):
952+
if isinstance(unpacked, TypeVarTupleType):
953953
# Cannot infer anything for T from [T, ...] <: *Ts
954954
continue
955955
assert (

0 commit comments

Comments
 (0)