Skip to content

Commit f7567fd

Browse files
Merge branch 'master' into constant-fold-checkers-for-c-type
2 parents 44e9353 + 374fefb commit f7567fd

34 files changed

+630
-159
lines changed

docs/source/common_issues.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ This example demonstrates both safe and unsafe overrides:
731731
732732
class NarrowerReturn(A):
733733
# A more specific return type is fine
734-
def test(self, t: Sequence[int]) -> List[str]: # OK
734+
def test(self, t: Sequence[int]) -> list[str]: # OK
735735
...
736736
737737
class GeneralizedReturn(A):
@@ -746,7 +746,7 @@ not necessary:
746746
.. code-block:: python
747747
748748
class NarrowerArgument(A):
749-
def test(self, t: List[int]) -> Sequence[str]: # type: ignore[override]
749+
def test(self, t: list[int]) -> Sequence[str]: # type: ignore[override]
750750
...
751751
752752
.. _unreachable:

docs/source/error_code_list2.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,3 +676,26 @@ Example:
676676
print("red")
677677
case _:
678678
print("other")
679+
680+
.. _code-untyped-decorator:
681+
682+
Error if an untyped decorator makes a typed function effectively untyped [untyped-decorator]
683+
--------------------------------------------------------------------------------------------
684+
685+
If enabled with :option:`--disallow-untyped-decorators <mypy --disallow-untyped-decorators>`
686+
mypy generates an error if a typed function is wrapped by an untyped decorator
687+
(as this would effectively remove the benefits of typing the function).
688+
689+
Example:
690+
691+
.. code-block:: python
692+
693+
def printing_decorator(func):
694+
def wrapper(*args, **kwds):
695+
print("Calling", func)
696+
return func(*args, **kwds)
697+
return wrapper
698+
# A decorated function.
699+
@printing_decorator # E: Untyped decorator makes function "add_forty_two" untyped [untyped-decorator]
700+
def add_forty_two(value: int) -> int:
701+
return value + 42

docs/source/stubtest.rst

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,75 @@ to mypy build errors". In this case, you will need to mitigate those errors
9999
before stubtest will run. Despite potential overlap in errors here, stubtest is
100100
not intended as a substitute for running mypy directly.
101101

102+
Allowlist
103+
*********
104+
102105
If you wish to ignore some of stubtest's complaints, stubtest supports a
103-
pretty handy allowlist system.
106+
pretty handy :option:`--allowlist` system.
107+
108+
Let's say that you have this python module called ``ex``:
109+
110+
.. code-block:: python
111+
112+
try:
113+
import optional_expensive_dep
114+
except ImportError:
115+
optional_expensive_dep = None
116+
117+
first = 1
118+
if optional_expensive_dep:
119+
second = 2
120+
121+
Let's say that you can't install ``optional_expensive_dep`` in CI for some reason,
122+
but you still want to include ``second: int`` in the stub file:
123+
124+
.. code-block:: python
125+
126+
first: int
127+
second: int
128+
129+
In this case stubtest will correctly complain:
130+
131+
.. code-block:: shell
132+
133+
error: ex.second is not present at runtime
134+
Stub: in file /.../ex.pyi:2
135+
builtins.int
136+
Runtime:
137+
MISSING
138+
139+
Found 1 error (checked 1 module)
140+
141+
To fix this, you can add an ``allowlist`` entry:
142+
143+
.. code-block:: ini
144+
145+
# Allowlist entries in `allowlist.txt` file:
146+
147+
# Does not exist if `optional_expensive_dep` is not installed:
148+
ex.second
149+
150+
And now when running stubtest with ``--allowlist=allowlist.txt``,
151+
no errors will be generated anymore.
152+
153+
Allowlists also support regular expressions,
154+
which can be useful to ignore many similar errors at once.
155+
They can also be useful for suppressing stubtest errors that occur sometimes,
156+
but not on every CI run. For example, if some CI workers have
157+
``optional_expensive_dep`` installed, stubtest might complain with this message
158+
on those workers if you had the ``ex.second`` allowlist entry:
159+
160+
.. code-block:: ini
161+
162+
note: unused allowlist entry ex.second
163+
Found 1 error (checked 1 module)
164+
165+
Changing ``ex.second`` to be ``(ex\.second)?`` will make this error optional,
166+
meaning that stubtest will pass whether or not a CI runner
167+
has``optional_expensive_dep`` installed.
168+
169+
CLI
170+
***
104171

105172
The rest of this section documents the command line interface of stubtest.
106173

@@ -119,15 +186,15 @@ The rest of this section documents the command line interface of stubtest.
119186
.. option:: --allowlist FILE
120187

121188
Use file as an allowlist. Can be passed multiple times to combine multiple
122-
allowlists. Allowlists can be created with --generate-allowlist. Allowlists
123-
support regular expressions.
189+
allowlists. Allowlists can be created with :option:`--generate-allowlist`.
190+
Allowlists support regular expressions.
124191

125192
The presence of an entry in the allowlist means stubtest will not generate
126193
any errors for the corresponding definition.
127194

128195
.. option:: --generate-allowlist
129196

130-
Print an allowlist (to stdout) to be used with --allowlist
197+
Print an allowlist (to stdout) to be used with :option:`--allowlist`.
131198

132199
When introducing stubtest to an existing project, this is an easy way to
133200
silence all existing errors.
@@ -141,17 +208,17 @@ The rest of this section documents the command line interface of stubtest.
141208

142209
Note if an allowlist entry is a regex that matches the empty string,
143210
stubtest will never consider it unused. For example, to get
144-
`--ignore-unused-allowlist` behaviour for a single allowlist entry like
211+
``--ignore-unused-allowlist`` behaviour for a single allowlist entry like
145212
``foo.bar`` you could add an allowlist entry ``(foo\.bar)?``.
146213
This can be useful when an error only occurs on a specific platform.
147214

148215
.. option:: --mypy-config-file FILE
149216

150-
Use specified mypy config file to determine mypy plugins and mypy path
217+
Use specified mypy config *file* to determine mypy plugins and mypy path
151218

152219
.. option:: --custom-typeshed-dir DIR
153220

154-
Use the custom typeshed in DIR
221+
Use the custom typeshed in *DIR*
155222

156223
.. option:: --check-typeshed
157224

mypy/build.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ def __init__(self, manager: BuildManager, graph: Graph) -> None:
143143
self.errors: list[str] = [] # Filled in by build if desired
144144

145145

146+
def build_error(msg: str) -> NoReturn:
147+
raise CompileError([f"mypy: error: {msg}"])
148+
149+
146150
def build(
147151
sources: list[BuildSource],
148152
options: Options,
@@ -241,6 +245,9 @@ def _build(
241245
errors = Errors(options, read_source=lambda path: read_py_file(path, cached_read))
242246
plugin, snapshot = load_plugins(options, errors, stdout, extra_plugins)
243247

248+
# Validate error codes after plugins are loaded.
249+
options.process_error_codes(error_callback=build_error)
250+
244251
# Add catch-all .gitignore to cache dir if we created it
245252
cache_dir_existed = os.path.isdir(options.cache_dir)
246253

mypy/cache.py

Lines changed: 14 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,22 @@
11
from __future__ import annotations
22

33
from collections.abc import Sequence
4-
from typing import TYPE_CHECKING, Final
4+
from typing import Final
55

66
from mypy_extensions import u8
7-
8-
try:
9-
from native_internal import (
10-
Buffer as Buffer,
11-
read_bool as read_bool,
12-
read_float as read_float,
13-
read_int as read_int,
14-
read_str as read_str,
15-
read_tag as read_tag,
16-
write_bool as write_bool,
17-
write_float as write_float,
18-
write_int as write_int,
19-
write_str as write_str,
20-
write_tag as write_tag,
21-
)
22-
except ImportError:
23-
# TODO: temporary, remove this after we publish mypy-native on PyPI.
24-
if not TYPE_CHECKING:
25-
26-
class Buffer:
27-
def __init__(self, source: bytes = b"") -> None:
28-
raise NotImplementedError
29-
30-
def getvalue(self) -> bytes:
31-
raise NotImplementedError
32-
33-
def read_int(data: Buffer) -> int:
34-
raise NotImplementedError
35-
36-
def write_int(data: Buffer, value: int) -> None:
37-
raise NotImplementedError
38-
39-
def read_tag(data: Buffer) -> u8:
40-
raise NotImplementedError
41-
42-
def write_tag(data: Buffer, value: u8) -> None:
43-
raise NotImplementedError
44-
45-
def read_str(data: Buffer) -> str:
46-
raise NotImplementedError
47-
48-
def write_str(data: Buffer, value: str) -> None:
49-
raise NotImplementedError
50-
51-
def read_bool(data: Buffer) -> bool:
52-
raise NotImplementedError
53-
54-
def write_bool(data: Buffer, value: bool) -> None:
55-
raise NotImplementedError
56-
57-
def read_float(data: Buffer) -> float:
58-
raise NotImplementedError
59-
60-
def write_float(data: Buffer, value: float) -> None:
61-
raise NotImplementedError
62-
7+
from native_internal import (
8+
Buffer as Buffer,
9+
read_bool as read_bool,
10+
read_float as read_float,
11+
read_int as read_int,
12+
read_str as read_str,
13+
read_tag as read_tag,
14+
write_bool as write_bool,
15+
write_float as write_float,
16+
write_int as write_int,
17+
write_str as write_str,
18+
write_tag as write_tag,
19+
)
6320

6421
# Always use this type alias to refer to type tags.
6522
Tag = u8

mypy/checker.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1801,7 +1801,8 @@ def check___new___signature(self, fdef: FuncDef, typ: CallableType) -> None:
18011801
"but must return a subtype of",
18021802
)
18031803
elif not isinstance(
1804-
get_proper_type(bound_type.ret_type), (AnyType, Instance, TupleType, UninhabitedType)
1804+
get_proper_type(bound_type.ret_type),
1805+
(AnyType, Instance, TupleType, UninhabitedType, LiteralType),
18051806
):
18061807
self.fail(
18071808
message_registry.NON_INSTANCE_NEW_TYPE.format(

mypy/checkexpr.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from mypy.checker_shared import ExpressionCheckerSharedApi
1919
from mypy.checkmember import analyze_member_access, has_operator
2020
from mypy.checkstrformat import StringFormatterChecker
21+
from mypy.constant_fold import constant_fold_expr
2122
from mypy.erasetype import erase_type, remove_instance_last_known_values, replace_meta_vars
2223
from mypy.errors import ErrorInfo, ErrorWatcher, report_internal_error
2324
from mypy.expandtype import (
@@ -656,11 +657,12 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) ->
656657
return ret_type
657658

658659
def check_str_format_call(self, e: CallExpr) -> None:
659-
"""More precise type checking for str.format() calls on literals."""
660+
"""More precise type checking for str.format() calls on literals and folded constants."""
660661
assert isinstance(e.callee, MemberExpr)
661662
format_value = None
662-
if isinstance(e.callee.expr, StrExpr):
663-
format_value = e.callee.expr.value
663+
folded_callee_expr = constant_fold_expr(e.callee.expr, "<unused>")
664+
if isinstance(folded_callee_expr, str):
665+
format_value = folded_callee_expr
664666
elif self.chk.has_type(e.callee.expr):
665667
typ = get_proper_type(self.chk.lookup_type(e.callee.expr))
666668
if (

mypy/checkmember.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,8 @@ def analyze_type_callable_member_access(name: str, typ: FunctionLike, mx: Member
410410
ret_type = tuple_fallback(ret_type)
411411
if isinstance(ret_type, TypedDictType):
412412
ret_type = ret_type.fallback
413+
if isinstance(ret_type, LiteralType):
414+
ret_type = ret_type.fallback
413415
if isinstance(ret_type, Instance):
414416
if not mx.is_operator:
415417
# When Python sees an operator (eg `3 == 4`), it automatically translates that

mypy/config_parser.py

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
import sys
99
from io import StringIO
1010

11-
from mypy.errorcodes import error_codes
12-
1311
if sys.version_info >= (3, 11):
1412
import tomllib
1513
else:
@@ -87,15 +85,6 @@ def complain(x: object, additional_info: str = "") -> Never:
8785
complain(v)
8886

8987

90-
def validate_codes(codes: list[str]) -> list[str]:
91-
invalid_codes = set(codes) - set(error_codes.keys())
92-
if invalid_codes:
93-
raise argparse.ArgumentTypeError(
94-
f"Invalid error code(s): {', '.join(sorted(invalid_codes))}"
95-
)
96-
return codes
97-
98-
9988
def validate_package_allow_list(allow_list: list[str]) -> list[str]:
10089
for p in allow_list:
10190
msg = f"Invalid allow list entry: {p}"
@@ -209,8 +198,8 @@ def split_commas(value: str) -> list[str]:
209198
[p.strip() for p in split_commas(s)]
210199
),
211200
"enable_incomplete_feature": lambda s: [p.strip() for p in split_commas(s)],
212-
"disable_error_code": lambda s: validate_codes([p.strip() for p in split_commas(s)]),
213-
"enable_error_code": lambda s: validate_codes([p.strip() for p in split_commas(s)]),
201+
"disable_error_code": lambda s: [p.strip() for p in split_commas(s)],
202+
"enable_error_code": lambda s: [p.strip() for p in split_commas(s)],
214203
"package_root": lambda s: [p.strip() for p in split_commas(s)],
215204
"cache_dir": expand_path,
216205
"python_executable": expand_path,
@@ -234,8 +223,8 @@ def split_commas(value: str) -> list[str]:
234223
"always_false": try_split,
235224
"untyped_calls_exclude": lambda s: validate_package_allow_list(try_split(s)),
236225
"enable_incomplete_feature": try_split,
237-
"disable_error_code": lambda s: validate_codes(try_split(s)),
238-
"enable_error_code": lambda s: validate_codes(try_split(s)),
226+
"disable_error_code": lambda s: try_split(s),
227+
"enable_error_code": lambda s: try_split(s),
239228
"package_root": try_split,
240229
"exclude": str_or_array_as_list,
241230
"packages": try_split,

mypy/errorcodes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,10 @@ def __hash__(self) -> int:
302302
sub_code_of=MISC,
303303
)
304304

305+
UNTYPED_DECORATOR: Final = ErrorCode(
306+
"untyped-decorator", "Error if an untyped decorator makes a typed function untyped", "General"
307+
)
308+
305309
NARROWED_TYPE_NOT_SUBTYPE: Final = ErrorCode(
306310
"narrowed-type-not-subtype",
307311
"Warn if a TypeIs function's narrowed type is not a subtype of the original type",

0 commit comments

Comments
 (0)