Skip to content

Commit 1449366

Browse files
authored
Allow objects matching SupportsKeysAndGetItem to be unpacked (#14990)
Fixes #14986 This PR allows any object matching `_typeshed.SupportsKeysAndGetItem[str, Any]` to be unpacked with `**`.
1 parent 69c774e commit 1449366

29 files changed

+140
-39
lines changed

mypy/checkexpr.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -2126,7 +2126,9 @@ def check_argument_types(
21262126
if actual_kind == nodes.ARG_STAR2 and not self.is_valid_keyword_var_arg(
21272127
actual_type
21282128
):
2129-
is_mapping = is_subtype(actual_type, self.chk.named_type("typing.Mapping"))
2129+
is_mapping = is_subtype(
2130+
actual_type, self.chk.named_type("_typeshed.SupportsKeysAndGetItem")
2131+
)
21302132
self.msg.invalid_keyword_var_arg(actual_type, is_mapping, context)
21312133
expanded_actual = mapper.expand_actual_type(
21322134
actual_type, actual_kind, callee.arg_names[i], callee_arg_kind
@@ -4346,7 +4348,11 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
43464348
for arg in stargs:
43474349
if rv is None:
43484350
constructor = CallableType(
4349-
[self.chk.named_generic_type("typing.Mapping", [kt, vt])],
4351+
[
4352+
self.chk.named_generic_type(
4353+
"_typeshed.SupportsKeysAndGetItem", [kt, vt]
4354+
)
4355+
],
43504356
[nodes.ARG_POS],
43514357
[None],
43524358
self.chk.named_generic_type("builtins.dict", [kt, vt]),
@@ -4936,14 +4942,14 @@ def is_valid_keyword_var_arg(self, typ: Type) -> bool:
49364942
is_subtype(
49374943
typ,
49384944
self.chk.named_generic_type(
4939-
"typing.Mapping",
4945+
"_typeshed.SupportsKeysAndGetItem",
49404946
[self.named_type("builtins.str"), AnyType(TypeOfAny.special_form)],
49414947
),
49424948
)
49434949
or is_subtype(
49444950
typ,
49454951
self.chk.named_generic_type(
4946-
"typing.Mapping", [UninhabitedType(), UninhabitedType()]
4952+
"_typeshed.SupportsKeysAndGetItem", [UninhabitedType(), UninhabitedType()]
49474953
),
49484954
)
49494955
or isinstance(typ, ParamSpecType)

mypy/checkstrformat.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -844,10 +844,14 @@ def build_dict_type(self, expr: FormatStringExpr) -> Type:
844844
any_type = AnyType(TypeOfAny.special_form)
845845
if isinstance(expr, BytesExpr):
846846
bytes_type = self.chk.named_generic_type("builtins.bytes", [])
847-
return self.chk.named_generic_type("typing.Mapping", [bytes_type, any_type])
847+
return self.chk.named_generic_type(
848+
"_typeshed.SupportsKeysAndGetItem", [bytes_type, any_type]
849+
)
848850
elif isinstance(expr, StrExpr):
849851
str_type = self.chk.named_generic_type("builtins.str", [])
850-
return self.chk.named_generic_type("typing.Mapping", [str_type, any_type])
852+
return self.chk.named_generic_type(
853+
"_typeshed.SupportsKeysAndGetItem", [str_type, any_type]
854+
)
851855
else:
852856
assert False, "Unreachable"
853857

mypy/test/data.py

+6
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ def parse_test_case(case: DataDrivenTestCase) -> None:
105105
src_path = join(os.path.dirname(case.file), item.arg)
106106
with open(src_path, encoding="utf8") as f:
107107
files.append((join(base_path, "typing.pyi"), f.read()))
108+
elif item.id == "_typeshed":
109+
# Use an alternative stub file for the _typeshed module.
110+
assert item.arg is not None
111+
src_path = join(os.path.dirname(case.file), item.arg)
112+
with open(src_path, encoding="utf8") as f:
113+
files.append((join(base_path, "_typeshed.pyi"), f.read()))
108114
elif re.match(r"stale[0-9]*$", item.id):
109115
passnum = 1 if item.id == "stale" else int(item.id[len("stale") :])
110116
assert passnum > 0

mypy/test/testdeps.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
5050
type_state.add_all_protocol_deps(deps)
5151

5252
for source, targets in sorted(deps.items()):
53-
if source.startswith(("<enum", "<typing", "<mypy")):
53+
if source.startswith(("<enum", "<typing", "<mypy", "<_typeshed.")):
5454
# Remove noise.
5555
continue
5656
line = f"{source} -> {', '.join(sorted(targets))}"

mypyc/test-data/fixtures/ir.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# These builtins stubs are used implicitly in AST to IR generation
22
# test cases.
33

4+
import _typeshed
45
from typing import (
56
TypeVar, Generic, List, Iterator, Iterable, Dict, Optional, Tuple, Any, Set,
67
overload, Mapping, Union, Callable, Sequence, FrozenSet, Protocol

mypyc/test-data/fixtures/typing-full.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ class Sequence(Iterable[T_co], Container[T_co]):
125125
def __getitem__(self, n: Any) -> T_co: pass
126126

127127
class Mapping(Iterable[T], Generic[T, T_co], metaclass=ABCMeta):
128+
def keys(self) -> Iterable[T]: pass # Approximate return type
128129
def __getitem__(self, key: T) -> T_co: pass
129130
@overload
130131
def get(self, k: T) -> Optional[T_co]: pass

test-data/unit/check-expressions.test

+32-3
Original file line numberDiff line numberDiff line change
@@ -1786,13 +1786,42 @@ b = {'z': 26, *a} # E: invalid syntax
17861786

17871787
[case testDictWithStarStarExpr]
17881788

1789-
from typing import Dict
1789+
from typing import Dict, Iterable
1790+
1791+
class Thing:
1792+
def keys(self) -> Iterable[str]:
1793+
...
1794+
def __getitem__(self, key: str) -> int:
1795+
...
1796+
17901797
a = {'a': 1}
17911798
b = {'z': 26, **a}
17921799
c = {**b}
17931800
d = {**a, **b, 'c': 3}
1794-
e = {1: 'a', **a} # E: Argument 1 to "update" of "dict" has incompatible type "Dict[str, int]"; expected "Mapping[int, str]"
1795-
f = {**b} # type: Dict[int, int] # E: List item 0 has incompatible type "Dict[str, int]"; expected "Mapping[int, int]"
1801+
e = {1: 'a', **a} # E: Argument 1 to "update" of "dict" has incompatible type "Dict[str, int]"; expected "SupportsKeysAndGetItem[int, str]"
1802+
f = {**b} # type: Dict[int, int] # E: List item 0 has incompatible type "Dict[str, int]"; expected "SupportsKeysAndGetItem[int, int]"
1803+
g = {**Thing()}
1804+
h = {**a, **Thing()}
1805+
i = {**Thing()} # type: Dict[int, int] # E: List item 0 has incompatible type "Thing"; expected "SupportsKeysAndGetItem[int, int]" \
1806+
# N: Following member(s) of "Thing" have conflicts: \
1807+
# N: Expected: \
1808+
# N: def __getitem__(self, int, /) -> int \
1809+
# N: Got: \
1810+
# N: def __getitem__(self, str, /) -> int \
1811+
# N: Expected: \
1812+
# N: def keys(self) -> Iterable[int] \
1813+
# N: Got: \
1814+
# N: def keys(self) -> Iterable[str]
1815+
j = {1: 'a', **Thing()} # E: Argument 1 to "update" of "dict" has incompatible type "Thing"; expected "SupportsKeysAndGetItem[int, str]" \
1816+
# N: Following member(s) of "Thing" have conflicts: \
1817+
# N: Expected: \
1818+
# N: def __getitem__(self, int, /) -> str \
1819+
# N: Got: \
1820+
# N: def __getitem__(self, str, /) -> int \
1821+
# N: Expected: \
1822+
# N: def keys(self) -> Iterable[int] \
1823+
# N: Got: \
1824+
# N: def keys(self) -> Iterable[str]
17961825
[builtins fixtures/dict.pyi]
17971826
[typing fixtures/typing-medium.pyi]
17981827

test-data/unit/check-formatting.test

+19-4
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,29 @@ b'%(x)s' % {b'x': b'data'}
125125
[typing fixtures/typing-medium.pyi]
126126

127127
[case testStringInterpolationMappingDictTypes]
128-
from typing import Any, Dict
128+
from typing import Any, Dict, Iterable
129+
130+
class StringThing:
131+
def keys(self) -> Iterable[str]:
132+
...
133+
def __getitem__(self, __key: str) -> str:
134+
...
135+
136+
class BytesThing:
137+
def keys(self) -> Iterable[bytes]:
138+
...
139+
def __getitem__(self, __key: bytes) -> str:
140+
...
141+
129142
a = None # type: Any
130143
ds, do, di = None, None, None # type: Dict[str, int], Dict[object, int], Dict[int, int]
131-
'%(a)' % 1 # E: Format requires a mapping (expression has type "int", expected type for mapping is "Mapping[str, Any]")
144+
'%(a)' % 1 # E: Format requires a mapping (expression has type "int", expected type for mapping is "SupportsKeysAndGetItem[str, Any]")
132145
'%()d' % a
133146
'%()d' % ds
134-
'%()d' % do # E: Format requires a mapping (expression has type "Dict[object, int]", expected type for mapping is "Mapping[str, Any]")
135-
b'%()d' % ds # E: Format requires a mapping (expression has type "Dict[str, int]", expected type for mapping is "Mapping[bytes, Any]")
147+
'%()d' % do # E: Format requires a mapping (expression has type "Dict[object, int]", expected type for mapping is "SupportsKeysAndGetItem[str, Any]")
148+
b'%()d' % ds # E: Format requires a mapping (expression has type "Dict[str, int]", expected type for mapping is "SupportsKeysAndGetItem[bytes, Any]")
149+
'%()s' % StringThing()
150+
b'%()s' % BytesThing()
136151
[builtins fixtures/primitives.pyi]
137152

138153
[case testStringInterpolationMappingInvalidSpecifiers]

test-data/unit/check-generic-subtyping.test

+1
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,7 @@ main:13: note: Revealed type is "builtins.dict[builtins.int, builtins.str]"
990990
main:14: error: Keywords must be strings
991991
main:14: error: Argument 1 to "func_with_kwargs" has incompatible type "**X1[str, int]"; expected "int"
992992
[builtins fixtures/dict.pyi]
993+
[typing fixtures/typing-medium.pyi]
993994

994995
[case testSubtypingMappingUnpacking3]
995996
from typing import Generic, TypeVar, Mapping, Iterable

test-data/unit/check-incremental.test

+8-8
Original file line numberDiff line numberDiff line change
@@ -3699,8 +3699,8 @@ cache_fine_grained = False
36993699
[file mypy.ini.2]
37003700
\[mypy]
37013701
cache_fine_grained = True
3702-
[rechecked a, builtins, typing]
3703-
[stale a, builtins, typing]
3702+
[rechecked _typeshed, a, builtins, typing]
3703+
[stale _typeshed, a, builtins, typing]
37043704
[builtins fixtures/tuple.pyi]
37053705

37063706
[case testIncrementalPackageNameOverload]
@@ -3751,8 +3751,8 @@ Signature: 8a477f597d28d172789f06886806bc55
37513751
[file b.py.2]
37523752
# uh
37533753
-- Every file should get reloaded, since the cache was invalidated
3754-
[stale a, b, builtins, typing]
3755-
[rechecked a, b, builtins, typing]
3754+
[stale _typeshed, a, b, builtins, typing]
3755+
[rechecked _typeshed, a, b, builtins, typing]
37563756
[builtins fixtures/tuple.pyi]
37573757

37583758
[case testIncrementalBustedFineGrainedCache2]
@@ -3764,8 +3764,8 @@ import b
37643764
[file b.py.2]
37653765
# uh
37663766
-- Every file should get reloaded, since the settings changed
3767-
[stale a, b, builtins, typing]
3768-
[rechecked a, b, builtins, typing]
3767+
[stale _typeshed, a, b, builtins, typing]
3768+
[rechecked _typeshed, a, b, builtins, typing]
37693769
[builtins fixtures/tuple.pyi]
37703770

37713771
[case testIncrementalBustedFineGrainedCache3]
@@ -3780,8 +3780,8 @@ import b
37803780
[file b.py.2]
37813781
# uh
37823782
-- Every file should get reloaded, since the cache was invalidated
3783-
[stale a, b, builtins, typing]
3784-
[rechecked a, b, builtins, typing]
3783+
[stale _typeshed, a, b, builtins, typing]
3784+
[rechecked _typeshed, a, b, builtins, typing]
37853785
[builtins fixtures/tuple.pyi]
37863786

37873787
[case testIncrementalWorkingFineGrainedCache]

test-data/unit/check-inference.test

+3-1
Original file line numberDiff line numberDiff line change
@@ -1671,7 +1671,9 @@ a() # E: "Dict[str, int]" not callable
16711671

16721672
[case testInferDictInitializedToEmptyUsingUpdateError]
16731673
a = {} # E: Need type annotation for "a" (hint: "a: Dict[<type>, <type>] = ...")
1674-
a.update([1, 2]) # E: Argument 1 to "update" of "dict" has incompatible type "List[int]"; expected "Mapping[Any, Any]"
1674+
a.update([1, 2]) # E: Argument 1 to "update" of "dict" has incompatible type "List[int]"; expected "SupportsKeysAndGetItem[Any, Any]" \
1675+
# N: "list" is missing following "SupportsKeysAndGetItem" protocol member: \
1676+
# N: keys
16751677
a() # E: "Dict[Any, Any]" not callable
16761678
[builtins fixtures/dict.pyi]
16771679

test-data/unit/check-kwargs.test

+15-7
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ g(**{})
499499

500500
[case testKeywordUnpackWithDifferentTypes]
501501
# https://github.com/python/mypy/issues/11144
502-
from typing import Dict, Generic, TypeVar, Mapping
502+
from typing import Dict, Generic, TypeVar, Mapping, Iterable
503503

504504
T = TypeVar("T")
505505
T2 = TypeVar("T2")
@@ -516,21 +516,29 @@ class C(Generic[T, T2]):
516516
class D:
517517
...
518518

519+
class E:
520+
def keys(self) -> Iterable[str]:
521+
...
522+
def __getitem__(self, key: str) -> float:
523+
...
524+
519525
def foo(**i: float) -> float:
520526
...
521527

522528
a: A[str, str]
523529
b: B[str, str]
524530
c: C[str, float]
525531
d: D
526-
e = {"a": "b"}
532+
e: E
533+
f = {"a": "b"}
527534

528535
foo(k=1.5)
529536
foo(**a)
530537
foo(**b)
531538
foo(**c)
532539
foo(**d)
533540
foo(**e)
541+
foo(**f)
534542

535543
# Correct:
536544

@@ -544,9 +552,9 @@ foo(**good1)
544552
foo(**good2)
545553
foo(**good3)
546554
[out]
547-
main:29: error: Argument 1 to "foo" has incompatible type "**A[str, str]"; expected "float"
548-
main:30: error: Argument 1 to "foo" has incompatible type "**B[str, str]"; expected "float"
549-
main:31: error: Argument after ** must be a mapping, not "C[str, float]"
550-
main:32: error: Argument after ** must be a mapping, not "D"
551-
main:33: error: Argument 1 to "foo" has incompatible type "**Dict[str, str]"; expected "float"
555+
main:36: error: Argument 1 to "foo" has incompatible type "**A[str, str]"; expected "float"
556+
main:37: error: Argument 1 to "foo" has incompatible type "**B[str, str]"; expected "float"
557+
main:38: error: Argument after ** must be a mapping, not "C[str, float]"
558+
main:39: error: Argument after ** must be a mapping, not "D"
559+
main:41: error: Argument 1 to "foo" has incompatible type "**Dict[str, str]"; expected "float"
552560
[builtins fixtures/dict.pyi]

test-data/unit/fine-grained-dataclass-transform.test

+2-2
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ class A(Dataclass):
8686

8787
[out]
8888
main:7: error: Unexpected keyword argument "x" for "B"
89-
builtins.pyi:12: note: "B" defined here
89+
builtins.pyi:13: note: "B" defined here
9090
main:7: error: Unexpected keyword argument "y" for "B"
91-
builtins.pyi:12: note: "B" defined here
91+
builtins.pyi:13: note: "B" defined here
9292
==
9393

9494
[case frozenInheritanceViaDefault]

test-data/unit/fine-grained-modules.test

+5-3
Original file line numberDiff line numberDiff line change
@@ -1279,12 +1279,12 @@ a.py:2: error: Too many arguments for "foo"
12791279

12801280
[case testAddModuleAfterCache3-only_when_cache]
12811281
# cmd: mypy main a.py
1282-
# cmd2: mypy main a.py b.py c.py d.py e.py f.py g.py h.py
1283-
# cmd3: mypy main a.py b.py c.py d.py e.py f.py g.py h.py
1282+
# cmd2: mypy main a.py b.py c.py d.py e.py f.py g.py h.py i.py j.py
1283+
# cmd3: mypy main a.py b.py c.py d.py e.py f.py g.py h.py i.py j.py
12841284
# flags: --ignore-missing-imports --follow-imports=skip
12851285
import a
12861286
[file a.py]
1287-
import b, c, d, e, f, g, h
1287+
import b, c, d, e, f, g, h, i, j
12881288
b.foo(10)
12891289
[file b.py.2]
12901290
def foo() -> None: pass
@@ -1294,6 +1294,8 @@ def foo() -> None: pass
12941294
[file f.py.2]
12951295
[file g.py.2]
12961296
[file h.py.2]
1297+
[file i.py.2]
1298+
[file j.py.2]
12971299

12981300
-- No files should be stale or reprocessed in the first step since the large number
12991301
-- of missing files will force build to give up on cache loading.

test-data/unit/fine-grained.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -7546,7 +7546,7 @@ def d() -> Dict[int, int]: pass
75467546
[builtins fixtures/dict.pyi]
75477547
[out]
75487548
==
7549-
main:5: error: Argument 1 to "update" of "dict" has incompatible type "Dict[int, int]"; expected "Mapping[int, str]"
7549+
main:5: error: Argument 1 to "update" of "dict" has incompatible type "Dict[int, int]"; expected "SupportsKeysAndGetItem[int, str]"
75507550

75517551
[case testAwaitAndAsyncDef-only_when_nocache]
75527552
from a import g

test-data/unit/fixtures/args.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Builtins stub used to support *args, **kwargs.
22

3+
import _typeshed
34
from typing import TypeVar, Generic, Iterable, Sequence, Tuple, Dict, Any, overload, Mapping
45

56
Tco = TypeVar('Tco', covariant=True)

test-data/unit/fixtures/dataclasses.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import _typeshed
12
from typing import (
23
Generic, Iterator, Iterable, Mapping, Optional, Sequence, Tuple,
34
TypeVar, Union, overload,

test-data/unit/fixtures/dict.pyi

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Builtins stub used in dictionary-related test cases.
22

3+
from _typeshed import SupportsKeysAndGetItem
4+
import _typeshed
35
from typing import (
46
TypeVar, Generic, Iterable, Iterator, Mapping, Tuple, overload, Optional, Union, Sequence
57
)
@@ -25,7 +27,7 @@ class dict(Mapping[KT, VT]):
2527
def __setitem__(self, k: KT, v: VT) -> None: pass
2628
def __iter__(self) -> Iterator[KT]: pass
2729
def __contains__(self, item: object) -> int: pass
28-
def update(self, a: Mapping[KT, VT]) -> None: pass
30+
def update(self, a: SupportsKeysAndGetItem[KT, VT]) -> None: pass
2931
@overload
3032
def get(self, k: KT) -> Optional[VT]: pass
3133
@overload

test-data/unit/fixtures/paramspec.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# builtins stub for paramspec-related test cases
22

3+
import _typeshed
34
from typing import (
45
Sequence, Generic, TypeVar, Iterable, Iterator, Tuple, Mapping, Optional, Union, Type, overload,
56
Protocol

test-data/unit/fixtures/primitives.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# builtins stub with non-generic primitive types
2+
import _typeshed
23
from typing import Generic, TypeVar, Sequence, Iterator, Mapping, Iterable, Tuple, Union
34

45
T = TypeVar('T')

test-data/unit/fixtures/tuple.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Builtins stub used in tuple-related test cases.
22

3+
import _typeshed
34
from typing import Iterable, Iterator, TypeVar, Generic, Sequence, Optional, overload, Tuple, Type
45

56
T = TypeVar("T")

test-data/unit/fixtures/typing-async.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ class Sequence(Iterable[T_co], Container[T_co]):
108108
def __getitem__(self, n: Any) -> T_co: pass
109109

110110
class Mapping(Iterable[T], Generic[T, T_co], metaclass=ABCMeta):
111+
def keys(self) -> Iterable[T]: pass # Approximate return type
111112
def __getitem__(self, key: T) -> T_co: pass
112113
@overload
113114
def get(self, k: T) -> Optional[T_co]: pass

0 commit comments

Comments
 (0)