Skip to content

Commit ec4409b

Browse files
committed
do not mangle TypedDict keywords
1 parent eb92319 commit ec4409b

File tree

4 files changed

+84
-2
lines changed

4 files changed

+84
-2
lines changed

mypy/fastparse.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,20 +359,24 @@ def find_disallowed_expression_in_annotation_scope(expr: ast3.expr | None) -> as
359359
class NameMangler(ast3.NodeTransformer):
360360
"""Mangle (nearly) all private identifiers within a class body (including nested classes)."""
361361

362+
mangled2unmangled: dict[str, str]
362363
_classname_complete: str
363364
_classname_trimmed: str
364365
_mangle_annotations: bool
365366
_unmangled_args: set[str]
366367

367368
def __init__(self, classname: str, mangle_annotations: bool) -> None:
369+
self.mangled2unmangled = {}
368370
self._classname_complete = classname
369371
self._classname_trimmed = classname.lstrip("_")
370372
self._mangle_annotations = mangle_annotations
371373
self._unmangled_args = set()
372374

373375
def _mangle_name(self, name: str) -> str:
374376
if name.startswith("__") and not name.endswith("__"):
375-
return f"_{self._classname_trimmed}{name}"
377+
mangled = f"_{self._classname_trimmed}{name}"
378+
self.mangled2unmangled[mangled] = name
379+
return mangled
376380
return name
377381

378382
def _mangle_slots(self, node: ast3.ClassDef) -> None:
@@ -1234,7 +1238,8 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef:
12341238
and any(j[0] == "annotations" for j in i.names)
12351239
for i in self.imports
12361240
)
1237-
NameMangler(n.name, mangle_annotations).visit(n)
1241+
mangler = NameMangler(n.name, mangle_annotations)
1242+
mangler.visit(n)
12381243

12391244
cdef = ClassDef(
12401245
n.name,
@@ -1244,6 +1249,7 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef:
12441249
metaclass=dict(keywords).get("metaclass"),
12451250
keywords=keywords,
12461251
type_args=explicit_type_params,
1252+
mangled2unmangled=mangler.mangled2unmangled,
12471253
)
12481254
cdef.decorators = self.translate_expr_list(n.decorator_list)
12491255
self.set_line(cdef, n)

mypy/nodes.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,7 @@ class ClassDef(Statement):
11361136
"has_incompatible_baseclass",
11371137
"docstring",
11381138
"removed_statements",
1139+
"mangled2unmangled",
11391140
)
11401141

11411142
__match_args__ = ("name", "defs")
@@ -1159,6 +1160,7 @@ class ClassDef(Statement):
11591160
has_incompatible_baseclass: bool
11601161
# Used by special forms like NamedTuple and TypedDict to store invalid statements
11611162
removed_statements: list[Statement]
1163+
mangled2unmangled: Final[dict[str, str]]
11621164

11631165
def __init__(
11641166
self,
@@ -1169,6 +1171,7 @@ def __init__(
11691171
metaclass: Expression | None = None,
11701172
keywords: list[tuple[str, Expression]] | None = None,
11711173
type_args: list[TypeParam] | None = None,
1174+
mangled2unmangled: dict[str, str] | None = None,
11721175
) -> None:
11731176
super().__init__()
11741177
self.name = name
@@ -1186,6 +1189,7 @@ def __init__(
11861189
self.has_incompatible_baseclass = False
11871190
self.docstring: str | None = None
11881191
self.removed_statements = []
1192+
self.mangled2unmangled = mangled2unmangled or {}
11891193

11901194
@property
11911195
def fullname(self) -> str:

mypy/semanal_typeddict.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ def analyze_typeddict_classdef_fields(
325325
self.fail(TPDICT_CLASS_ERROR, stmt)
326326
else:
327327
name = stmt.lvalues[0].name
328+
name = defn.mangled2unmangled.get(name, name)
328329
if name in (oldfields or []):
329330
self.fail(f'Overwriting TypedDict field "{name}" while extending', stmt)
330331
if name in fields:

test-data/unit/check-mangling.test

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,74 @@ class G(A):
275275
G().__x = 1 # E: "G" has no attribute "__x"
276276
G()._G__x = 1
277277
[builtins fixtures/dict.pyi]
278+
279+
[case testNameManglingTypedDictBasics]
280+
from typing import TypedDict
281+
282+
class A(TypedDict):
283+
__a: int
284+
285+
a: A
286+
reveal_type(a) # N: Revealed type is "TypedDict('__main__.A', {'__a': builtins.int})"
287+
reveal_type(a["__a"]) # N: Revealed type is "builtins.int"
288+
a = {"__a": 1}
289+
a = {"_A__a": 1} # E: Missing key "__a" for TypedDict "A" \
290+
# E: Extra key "_A__a" for TypedDict "A"
291+
292+
class B(A):
293+
__b: int
294+
295+
b: B
296+
reveal_type(b) # N: Revealed type is "TypedDict('__main__.B', {'__a': builtins.int, '__b': builtins.int})"
297+
reveal_type(b["__a"] + b["__b"]) # N: Revealed type is "builtins.int"
298+
b = {"__a": 1, "__b": 2}
299+
b = {"_A__a": 1, "_B__b": 2} # E: Missing keys ("__a", "__b") for TypedDict "B" \
300+
# E: Extra keys ("_A__a", "_B__b") for TypedDict "B"
301+
302+
class C(TypedDict):
303+
__c: int
304+
305+
class D(B, C):
306+
pass
307+
308+
d: D
309+
reveal_type(d) # N: Revealed type is "TypedDict('__main__.D', {'__c': builtins.int, '__a': builtins.int, '__b': builtins.int})"
310+
reveal_type(d["__a"] + d["__b"] + d["__c"]) # N: Revealed type is "builtins.int"
311+
d = {"__a": 1, "__b": 2, "__c": 3}
312+
d = {"_A__a": 1, "_B__b": 2, "_C__c": 3} # E: Missing keys ("__c", "__a", "__b") for TypedDict "D" \
313+
# E: Extra keys ("_A__a", "_B__b", "_C__c") for TypedDict "D"
314+
315+
class E(D):
316+
__a: int # E: Overwriting TypedDict field "__a" while extending
317+
__b: int # E: Overwriting TypedDict field "__b" while extending
318+
__c: int # E: Overwriting TypedDict field "__c" while extending
319+
320+
[typing fixtures/typing-typeddict.pyi]
321+
322+
[case testNameManglingTypedDictNotTotal]
323+
from typing import TypedDict
324+
325+
class A(TypedDict, total=False):
326+
__a: int
327+
328+
a: A
329+
reveal_type(a) # N: Revealed type is "TypedDict('__main__.A', {'__a'?: builtins.int})"
330+
reveal_type(a["__a"]) # N: Revealed type is "builtins.int"
331+
a = {"__a": 1}
332+
a = {"_A__a": 1} # E: Extra key "_A__a" for TypedDict "A"
333+
334+
[typing fixtures/typing-typeddict.pyi]
335+
336+
[case testNameManglingTypedDictAlternativeSyntax]
337+
from typing import TypedDict
338+
339+
A = TypedDict("A", {"__a": int})
340+
341+
a: A
342+
reveal_type(a) # N: Revealed type is "TypedDict('__main__.A', {'__a': builtins.int})"
343+
reveal_type(a["__a"]) # N: Revealed type is "builtins.int"
344+
a = {"__a": 1}
345+
a = {"_A__a": 1} # E: Missing key "__a" for TypedDict "A" \
346+
# E: Extra key "_A__a" for TypedDict "A"
347+
348+
[typing fixtures/typing-typeddict.pyi]

0 commit comments

Comments
 (0)