Skip to content

Commit 57799f1

Browse files
committed
Merge remote-tracking branch 'upstream/master' into bugfix/pythongh-18862-typevar-default-alias
2 parents a871b87 + 454989f commit 57799f1

39 files changed

+680
-62
lines changed

.pre-commit-config.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ repos:
1111
- id: black
1212
exclude: '^(test-data/)'
1313
- repo: https://github.com/astral-sh/ruff-pre-commit
14-
rev: v0.9.10
14+
rev: v0.11.4
1515
hooks:
1616
- id: ruff
1717
args: [--exit-non-zero-on-fix]
1818
- repo: https://github.com/python-jsonschema/check-jsonschema
19-
rev: 0.31.0
19+
rev: 0.32.1
2020
hooks:
2121
- id: check-github-workflows
2222
- id: check-github-actions
@@ -43,7 +43,7 @@ repos:
4343
# but the integration only works if shellcheck is installed
4444
- "github.com/wasilibs/go-shellcheck/cmd/[email protected]"
4545
- repo: https://github.com/woodruffw/zizmor-pre-commit
46-
rev: v1.0.1
46+
rev: v1.5.2
4747
hooks:
4848
- id: zizmor
4949
- repo: local

docs/source/command_line.rst

+13-2
Original file line numberDiff line numberDiff line change
@@ -749,8 +749,19 @@ of the above sections.
749749

750750
.. option:: --strict
751751

752-
This flag mode enables all optional error checking flags. You can see the
753-
list of flags enabled by strict mode in the full :option:`mypy --help` output.
752+
This flag mode enables a defined subset of optional error-checking flags.
753+
This subset primarily includes checks for inadvertent type unsoundness (i.e
754+
strict will catch type errors as long as intentional methods like type ignore
755+
or casting were not used.)
756+
757+
Note: the :option:`--warn-unreachable` flag
758+
is not automatically enabled by the strict flag.
759+
760+
The strict flag does not take precedence over other strict-related flags.
761+
Directly specifying a flag of alternate behavior will override the
762+
behavior of strict, regardless of the order in which they are passed.
763+
You can see the list of flags enabled by strict mode in the full
764+
:option:`mypy --help` output.
754765

755766
Note: the exact list of flags enabled by running :option:`--strict` may change
756767
over time.

docs/source/runtime_troubles.rst

+1-2
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ version of Python considers legal code. This section describes these scenarios
88
and explains how to get your code running again. Generally speaking, we have
99
three tools at our disposal:
1010

11-
* Use of ``from __future__ import annotations`` (:pep:`563`)
12-
(this behaviour may eventually be made the default in a future Python version)
1311
* Use of string literal types or type comments
1412
* Use of ``typing.TYPE_CHECKING``
13+
* Use of ``from __future__ import annotations`` (:pep:`563`)
1514

1615
We provide a description of these before moving onto discussion of specific
1716
problems you may encounter.

mypy/dmypy/client.py

+5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from mypy.dmypy_os import alive, kill
2121
from mypy.dmypy_util import DEFAULT_STATUS_FILE, receive, send
2222
from mypy.ipc import IPCClient, IPCException
23+
from mypy.main import RECURSION_LIMIT
2324
from mypy.util import check_python_version, get_terminal_width, should_force_color
2425
from mypy.version import __version__
2526

@@ -268,6 +269,10 @@ class BadStatus(Exception):
268269
def main(argv: list[str]) -> None:
269270
"""The code is top-down."""
270271
check_python_version("dmypy")
272+
273+
# set recursion limit consistent with mypy/main.py
274+
sys.setrecursionlimit(RECURSION_LIMIT)
275+
271276
args = parser.parse_args(argv)
272277
if not args.action:
273278
parser.print_usage()

mypy/dmypy_server.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,9 @@ def fine_grained_increment_follow_imports(
620620
t1 = time.time()
621621
manager.log(f"fine-grained increment: find_changed: {t1 - t0:.3f}s")
622622

623+
# Track all modules encountered so far. New entries for all dependencies
624+
# are added below by other module finding methods below. All dependencies
625+
# in graph but not in `seen` are considered deleted at the end of this method.
623626
seen = {source.module for source in sources}
624627

625628
# Find changed modules reachable from roots (or in roots) already in graph.
@@ -736,7 +739,9 @@ def find_reachable_changed_modules(
736739
Args:
737740
roots: modules where to start search from
738741
graph: module graph to use for the search
739-
seen: modules we've seen before that won't be visited (mutated here!!)
742+
seen: modules we've seen before that won't be visited (mutated here!!).
743+
Needed to accumulate all modules encountered during update and remove
744+
everything that no longer exists.
740745
changed_paths: which paths have changed (stop search here and return any found)
741746
742747
Return (encountered reachable changed modules,
@@ -756,7 +761,8 @@ def find_reachable_changed_modules(
756761
changed.append((nxt.module, nxt.path))
757762
elif nxt.module in graph:
758763
state = graph[nxt.module]
759-
for dep in state.dependencies:
764+
ancestors = state.ancestors or []
765+
for dep in state.dependencies + ancestors:
760766
if dep not in seen:
761767
seen.add(dep)
762768
worklist.append(BuildSource(graph[dep].path, graph[dep].id, followed=True))
@@ -775,7 +781,9 @@ def find_added_suppressed(
775781
"""Find suppressed modules that have been added (and not included in seen).
776782
777783
Args:
778-
seen: reachable modules we've seen before (mutated here!!)
784+
seen: reachable modules we've seen before (mutated here!!).
785+
Needed to accumulate all modules encountered during update and remove
786+
everything that no longer exists.
779787
780788
Return suppressed, added modules.
781789
"""

mypy/fastparse.py

+24
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,29 @@ def parse(
239239
strip_function_bodies=strip_function_bodies,
240240
path=fnam,
241241
).visit(ast)
242+
243+
except RecursionError as e:
244+
# For very complex expressions it is possible to hit recursion limit
245+
# before reaching a leaf node.
246+
# Should reject at top level instead at bottom, since bottom would already
247+
# be at the threshold of the recursion limit, and may fail again later.
248+
# E.G. x1+x2+x3+...+xn -> BinOp(left=BinOp(left=BinOp(left=...
249+
try:
250+
# But to prove that is the cause of this particular recursion error,
251+
# try to walk the tree using builtin visitor
252+
ast3.NodeVisitor().visit(ast)
253+
except RecursionError:
254+
errors.report(
255+
-1, -1, "Source expression too complex to parse", blocker=False, code=codes.MISC
256+
)
257+
258+
tree = MypyFile([], [], False, {})
259+
260+
else:
261+
# re-raise original recursion error if it *can* be unparsed,
262+
# maybe this is some other issue that shouldn't be silenced/misdirected
263+
raise e
264+
242265
except SyntaxError as e:
243266
message = e.msg
244267
if feature_version > sys.version_info.minor and message.startswith("invalid syntax"):
@@ -406,6 +429,7 @@ def visit(self, node: AST | None) -> Any:
406429
method = "visit_" + node.__class__.__name__
407430
visitor = getattr(self, method)
408431
self.visitor_cache[typeobj] = visitor
432+
409433
return visitor(node)
410434

411435
def set_line(self, node: N, n: AstNode) -> N:

mypy/main.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242

4343
orig_stat: Final = os.stat
4444
MEM_PROFILE: Final = False # If True, dump memory profile
45+
RECURSION_LIMIT: Final = 2**14
4546

4647

4748
def stat_proxy(path: str) -> os.stat_result:
@@ -76,7 +77,7 @@ def main(
7677
util.check_python_version("mypy")
7778
t0 = time.time()
7879
# To log stat() calls: os.stat = stat_proxy
79-
sys.setrecursionlimit(2**14)
80+
sys.setrecursionlimit(RECURSION_LIMIT)
8081
if args is None:
8182
args = sys.argv[1:]
8283

mypy/meet.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,12 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type:
143143
]
144144
)
145145
if is_enum_overlapping_union(declared, narrowed):
146-
return original_narrowed
146+
# Quick check before reaching `is_overlapping_types`. If it's enum/literal overlap,
147+
# avoid full expansion and make it faster.
148+
assert isinstance(narrowed, UnionType)
149+
return make_simplified_union(
150+
[narrow_declared_type(declared, x) for x in narrowed.relevant_items()]
151+
)
147152
elif not is_overlapping_types(declared, narrowed, prohibit_none_typevar_overlap=True):
148153
if state.strict_optional:
149154
return UninhabitedType()

mypy/messages.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2439,7 +2439,7 @@ def generate_incompatible_tuple_error(
24392439
error_cnt = 0
24402440
notes: list[str] = []
24412441
for i, (lhs_t, rhs_t) in enumerate(zip(lhs_types, rhs_types)):
2442-
if not is_subtype(lhs_t, rhs_t):
2442+
if not is_subtype(rhs_t, lhs_t):
24432443
if error_cnt < 3:
24442444
notes.append(
24452445
"Expression tuple item {} has type {}; {} expected; ".format(

mypy/plugins/dataclasses.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -359,12 +359,12 @@ def transform(self) -> bool:
359359

360360
if decorator_arguments["frozen"]:
361361
if any(not parent["frozen"] for parent in parent_decorator_arguments):
362-
self._api.fail("Cannot inherit frozen dataclass from a non-frozen one", info)
362+
self._api.fail("Frozen dataclass cannot inherit from a non-frozen dataclass", info)
363363
self._propertize_callables(attributes, settable=False)
364364
self._freeze(attributes)
365365
else:
366366
if any(parent["frozen"] for parent in parent_decorator_arguments):
367-
self._api.fail("Cannot inherit non-frozen dataclass from a frozen one", info)
367+
self._api.fail("Non-frozen dataclass cannot inherit from a frozen dataclass", info)
368368
self._propertize_callables(attributes)
369369

370370
if decorator_arguments["slots"]:
@@ -381,7 +381,9 @@ def transform(self) -> bool:
381381
):
382382
str_type = self._api.named_type("builtins.str")
383383
literals: list[Type] = [
384-
LiteralType(attr.name, str_type) for attr in attributes if attr.is_in_init
384+
LiteralType(attr.name, str_type)
385+
for attr in attributes
386+
if attr.is_in_init and not attr.kw_only
385387
]
386388
match_args_type = TupleType(literals, self._api.named_type("builtins.tuple"))
387389
add_attribute_to_class(self._api, self._cls, "__match_args__", match_args_type)

mypy/semanal.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5591,7 +5591,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None:
55915591
self.msg.unimported_type_becomes_any("Type alias target", res, s)
55925592
res = make_any_non_unimported(res)
55935593
eager = self.is_func_scope()
5594-
if isinstance(res, ProperType) and isinstance(res, Instance) and not res.args:
5594+
if isinstance(res, ProperType) and isinstance(res, Instance):
55955595
fix_instance(res, self.fail, self.note, disallow_any=False, options=self.options)
55965596
alias_node = TypeAlias(
55975597
res,

mypy/semanal_main.py

+1
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ def semantic_analyze_target(
389389
analyzer.global_decls = [set()]
390390
analyzer.nonlocal_decls = [set()]
391391
analyzer.globals = tree.names
392+
analyzer.imports = set()
392393
analyzer.progress = False
393394
with state.wrap_context(check_blockers=False):
394395
refresh_node = node

mypy/semanal_typeddict.py

+40-31
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
StrExpr,
3131
TempNode,
3232
TupleExpr,
33+
TypeAlias,
3334
TypedDictExpr,
3435
TypeInfo,
3536
)
@@ -50,6 +51,7 @@
5051
TypedDictType,
5152
TypeOfAny,
5253
TypeVarLikeType,
54+
get_proper_type,
5355
)
5456

5557
TPDICT_CLASS_ERROR: Final = (
@@ -137,23 +139,18 @@ def analyze_typeddict_classdef(self, defn: ClassDef) -> tuple[bool, TypeInfo | N
137139
typeddict_bases_set.add("TypedDict")
138140
else:
139141
self.fail('Duplicate base class "TypedDict"', defn)
140-
elif isinstance(expr, RefExpr) and self.is_typeddict(expr):
141-
assert expr.fullname
142-
if expr.fullname not in typeddict_bases_set:
143-
typeddict_bases_set.add(expr.fullname)
142+
elif (
143+
isinstance(expr, RefExpr)
144+
and self.is_typeddict(expr)
145+
or isinstance(expr, IndexExpr)
146+
and self.is_typeddict(expr.base)
147+
):
148+
info = self._parse_typeddict_base(expr, defn)
149+
if info.fullname not in typeddict_bases_set:
150+
typeddict_bases_set.add(info.fullname)
144151
typeddict_bases.append(expr)
145152
else:
146-
assert isinstance(expr.node, TypeInfo)
147-
self.fail(f'Duplicate base class "{expr.node.name}"', defn)
148-
elif isinstance(expr, IndexExpr) and self.is_typeddict(expr.base):
149-
assert isinstance(expr.base, RefExpr)
150-
assert expr.base.fullname
151-
if expr.base.fullname not in typeddict_bases_set:
152-
typeddict_bases_set.add(expr.base.fullname)
153-
typeddict_bases.append(expr)
154-
else:
155-
assert isinstance(expr.base.node, TypeInfo)
156-
self.fail(f'Duplicate base class "{expr.base.node.name}"', defn)
153+
self.fail(f'Duplicate base class "{info.name}"', defn)
157154
else:
158155
self.fail("All bases of a new TypedDict must be TypedDict types", defn)
159156

@@ -190,22 +187,13 @@ def add_keys_and_types_from_base(
190187
readonly_keys: set[str],
191188
ctx: Context,
192189
) -> None:
190+
info = self._parse_typeddict_base(base, ctx)
193191
base_args: list[Type] = []
194-
if isinstance(base, RefExpr):
195-
assert isinstance(base.node, TypeInfo)
196-
info = base.node
197-
elif isinstance(base, IndexExpr):
198-
assert isinstance(base.base, RefExpr)
199-
assert isinstance(base.base.node, TypeInfo)
200-
info = base.base.node
192+
if isinstance(base, IndexExpr):
201193
args = self.analyze_base_args(base, ctx)
202194
if args is None:
203195
return
204196
base_args = args
205-
else:
206-
assert isinstance(base, CallExpr)
207-
assert isinstance(base.analyzed, TypedDictExpr)
208-
info = base.analyzed.info
209197

210198
assert info.typeddict_type is not None
211199
base_typed_dict = info.typeddict_type
@@ -231,6 +219,26 @@ def add_keys_and_types_from_base(
231219
required_keys.update(base_typed_dict.required_keys)
232220
readonly_keys.update(base_typed_dict.readonly_keys)
233221

222+
def _parse_typeddict_base(self, base: Expression, ctx: Context) -> TypeInfo:
223+
if isinstance(base, RefExpr):
224+
if isinstance(base.node, TypeInfo):
225+
return base.node
226+
elif isinstance(base.node, TypeAlias):
227+
# Only old TypeAlias / plain assignment, PEP695 `type` stmt
228+
# cannot be used as a base class
229+
target = get_proper_type(base.node.target)
230+
assert isinstance(target, TypedDictType)
231+
return target.fallback.type
232+
else:
233+
assert False
234+
elif isinstance(base, IndexExpr):
235+
assert isinstance(base.base, RefExpr)
236+
return self._parse_typeddict_base(base.base, ctx)
237+
else:
238+
assert isinstance(base, CallExpr)
239+
assert isinstance(base.analyzed, TypedDictExpr)
240+
return base.analyzed.info
241+
234242
def analyze_base_args(self, base: IndexExpr, ctx: Context) -> list[Type] | None:
235243
"""Analyze arguments of base type expressions as types.
236244
@@ -527,7 +535,7 @@ def parse_typeddict_args(
527535
return "", [], [], True, [], False
528536
dictexpr = args[1]
529537
tvar_defs = self.api.get_and_bind_all_tvars([t for k, t in dictexpr.items])
530-
res = self.parse_typeddict_fields_with_types(dictexpr.items, call)
538+
res = self.parse_typeddict_fields_with_types(dictexpr.items)
531539
if res is None:
532540
# One of the types is not ready, defer.
533541
return None
@@ -536,7 +544,7 @@ def parse_typeddict_args(
536544
return args[0].value, items, types, total, tvar_defs, ok
537545

538546
def parse_typeddict_fields_with_types(
539-
self, dict_items: list[tuple[Expression | None, Expression]], context: Context
547+
self, dict_items: list[tuple[Expression | None, Expression]]
540548
) -> tuple[list[str], list[Type], bool] | None:
541549
"""Parse typed dict items passed as pairs (name expression, type expression).
542550
@@ -609,10 +617,11 @@ def build_typeddict_typeinfo(
609617
# Helpers
610618

611619
def is_typeddict(self, expr: Expression) -> bool:
612-
return (
613-
isinstance(expr, RefExpr)
614-
and isinstance(expr.node, TypeInfo)
620+
return isinstance(expr, RefExpr) and (
621+
isinstance(expr.node, TypeInfo)
615622
and expr.node.typeddict_type is not None
623+
or isinstance(expr.node, TypeAlias)
624+
and isinstance(get_proper_type(expr.node.target), TypedDictType)
616625
)
617626

618627
def fail(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None:

mypy/subtypes.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
UnionType,
6868
UnpackType,
6969
find_unpack_in_list,
70+
flatten_nested_unions,
7071
get_proper_type,
7172
is_named_instance,
7273
split_with_prefix_and_suffix,
@@ -327,7 +328,9 @@ def _is_subtype(
327328
and isinstance(left, Instance)
328329
and (left.type.is_enum or left.type.fullname == "builtins.bool")
329330
):
330-
right = UnionType(mypy.typeops.try_contracting_literals_in_union(right.items))
331+
right = UnionType(
332+
mypy.typeops.try_contracting_literals_in_union(flatten_nested_unions(right.items))
333+
)
331334
if proper_subtype:
332335
is_subtype_of_item = any(
333336
is_proper_subtype(orig_left, item, subtype_context=subtype_context)

0 commit comments

Comments
 (0)