Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support name mangling #16715

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
778017e
suggest attributes of anchestors
tyralla Dec 29, 2023
b0df41b
add a note to `is_private`
tyralla Dec 29, 2023
9ae8dc8
handle enum.__member
tyralla Dec 29, 2023
e36fa0f
implement and apply `NameMangler`
tyralla Dec 29, 2023
1735d3e
fix `testSelfTypeReallyTrickyExample`
tyralla Dec 29, 2023
9c6ca04
fix stubtest `test_name_mangling`
tyralla Dec 29, 2023
d7e8cd5
add name mangling test cases
tyralla Dec 29, 2023
d4c7da0
use `/` instead of `__` to mark positional-only method parameters
tyralla Dec 29, 2023
907f7c5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 29, 2023
208f014
improve auto fix
tyralla Dec 30, 2023
99c64ee
first hacky step/suggestion for introducing a flag to avoid method pa…
tyralla Dec 30, 2023
a2fb094
mangle strings in __slots__
tyralla Jan 2, 2024
ff70bb6
different annotation handling when using `from __future__ import anno…
tyralla Jan 2, 2024
513fc97
improve visit_ClassDef
tyralla Jan 2, 2024
549a709
improve visit_FunctionDef
tyralla Jan 2, 2024
c3b8442
refactor visit_FunctionDef and visit_AsyncFunctionDef
tyralla Jan 3, 2024
69d88d6
different return type annotation handling when using `from __future__…
tyralla Jan 3, 2024
ce621db
fix
tyralla Jan 3, 2024
4d5c741
support decorators
tyralla Jan 4, 2024
adf3cc1
fix
tyralla Jan 4, 2024
86d0fe5
Add TypeVar tests
tyralla Jan 4, 2024
7286a59
for testing: do not mangle annotations when defined in stub files
tyralla Jan 4, 2024
01d5c5a
refactor
tyralla Jan 4, 2024
e37398b
remove method `is_private` by mangling "__mypy-replace" and "__mypy-p…
tyralla Jan 5, 2024
d4bd13a
fix
tyralla Jan 5, 2024
a9144f8
Merge branch 'master' into feature/name_mangling
tyralla Mar 28, 2024
4b17cfb
Merge branch 'master' into feature/name_mangling
hauntsaninja Nov 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 10 additions & 25 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1314,7 +1314,7 @@ def check_func_def(
if (
arg_type.variance == COVARIANT
and defn.name not in ("__init__", "__new__", "__post_init__")
and not is_private(defn.name) # private methods are not inherited
and "mypy-" not in defn.name # skip internally added methods
):
ctx: Context = arg_type
if ctx.line < 0:
Expand Down Expand Up @@ -1957,7 +1957,6 @@ def check_explicit_override_decorator(
and found_method_base_classes
and not defn.is_explicit_override
and defn.name not in ("__init__", "__new__")
and not is_private(defn.name)
):
self.msg.explicit_override_decorator_missing(
defn.name, found_method_base_classes[0].fullname, context or defn
Expand Down Expand Up @@ -2011,7 +2010,7 @@ def check_method_or_accessor_override_for_base(
base_attr = base.names.get(name)
if base_attr:
# First, check if we override a final (always an error, even with Any types).
if is_final_node(base_attr.node) and not is_private(name):
if is_final_node(base_attr.node):
self.msg.cant_override_final(name, base.name, defn)
# Second, final can't override anything writeable independently of types.
if defn.is_final:
Expand Down Expand Up @@ -2313,9 +2312,6 @@ def check_override(
if original.type_is is not None and override.type_is is None:
fail = True

if is_private(name):
fail = False

if fail:
emitted_msg = False

Expand Down Expand Up @@ -2617,25 +2613,26 @@ def check_enum(self, defn: ClassDef) -> None:

def check_final_enum(self, defn: ClassDef, base: TypeInfo) -> None:
for sym in base.names.values():
if self.is_final_enum_value(sym):
if self.is_final_enum_value(sym, base):
self.fail(f'Cannot extend enum with existing members: "{base.name}"', defn)
break

def is_final_enum_value(self, sym: SymbolTableNode) -> bool:
def is_final_enum_value(self, sym: SymbolTableNode, base: TypeInfo) -> bool:
if isinstance(sym.node, (FuncBase, Decorator)):
return False # A method is fine
if not isinstance(sym.node, Var):
return True # Can be a class or anything else

# Now, only `Var` is left, we need to check:
# 1. Private name like in `__prop = 1`
# 1. Mangled name like in `_class__prop = 1`
# 2. Dunder name like `__hash__ = some_hasher`
# 3. Sunder name like `_order_ = 'a, b, c'`
# 4. If it is a method / descriptor like in `method = classmethod(func)`
name = sym.node.name
if (
is_private(sym.node.name)
or is_dunder(sym.node.name)
or is_sunder(sym.node.name)
(name.startswith(f"_{base.name}__") and not name.endswith("__"))
or is_dunder(name)
or is_sunder(name)
# TODO: make sure that `x = @class/staticmethod(func)`
# and `x = property(prop)` both work correctly.
# Now they are incorrectly counted as enum members.
Expand Down Expand Up @@ -2750,8 +2747,6 @@ def check_multiple_inheritance(self, typ: TypeInfo) -> None:
# Normal checks for attribute compatibility should catch any problems elsewhere.
non_overridden_attrs = base.names.keys() - typ.names.keys()
for name in non_overridden_attrs:
if is_private(name):
continue
for base2 in mro[i + 1 :]:
# We only need to check compatibility of attributes from classes not
# in a subclass relationship. For subclasses, normal (single inheritance)
Expand Down Expand Up @@ -2859,7 +2854,7 @@ class C(B, A[int]): ... # this is unsafe because...
ok = True
# Final attributes can never be overridden, but can override
# non-final read-only attributes.
if is_final_node(second.node) and not is_private(name):
if is_final_node(second.node):
self.msg.cant_override_final(name, base2.name, ctx)
if is_final_node(first.node):
self.check_if_final_var_override_writable(name, second.node, ctx)
Expand Down Expand Up @@ -3329,9 +3324,6 @@ def check_compatibility_all_supers(
):
continue

if is_private(lvalue_node.name):
continue

base_type, base_node = self.lvalue_type_from_base(lvalue_node, base)
if isinstance(base_type, PartialType):
base_type = None
Expand Down Expand Up @@ -3501,8 +3493,6 @@ def check_compatibility_final_super(
"""
if not isinstance(base_node, (Var, FuncBase, Decorator)):
return True
if is_private(node.name):
return True
if base_node.is_final and (node.is_final or not isinstance(base_node, Var)):
# Give this error only for explicit override attempt with `Final`, or
# if we are overriding a final method with variable.
Expand Down Expand Up @@ -8654,11 +8644,6 @@ def is_overlapping_types_for_overload(left: Type, right: Type) -> bool:
)


def is_private(node_name: str) -> bool:
"""Check if node is private to class definition."""
return node_name.startswith("__") and not node_name.endswith("__")


def is_string_literal(typ: Type) -> bool:
strs = try_getting_str_literals_from_type(typ)
return strs is not None and len(strs) == 1
Expand Down
109 changes: 108 additions & 1 deletion mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import re
import sys
import warnings
from typing import Any, Callable, Final, List, Optional, Sequence, TypeVar, Union, cast
from typing import Any, Callable, Final, Iterable, List, Optional, Sequence, TypeVar, Union, cast
from typing_extensions import Literal, overload

from mypy import defaults, errorcodes as codes, message_registry
Expand Down Expand Up @@ -352,6 +352,105 @@ def is_no_type_check_decorator(expr: ast3.expr) -> bool:
return False


_T_FuncDef = TypeVar("_T_FuncDef", ast3.FunctionDef, ast3.AsyncFunctionDef)


class NameMangler(ast3.NodeTransformer):
"""Mangle (nearly) all private identifiers within a class body (including nested classes)."""

_classname_complete: str
_classname_trimmed: str
_mangle_annotations: bool
_unmangled_args: set[str]

_MANGLE_ARGS: bool = False # ToDo: remove it or make it an option?

def __init__(self, classname: str, mangle_annotations: bool) -> None:
self._classname_complete = classname
self._classname_trimmed = classname.lstrip("_")
self._mangle_annotations = mangle_annotations
self._unmangled_args = set()

def _mangle_name(self, name: str) -> str:
if name.startswith("__") and not name.endswith("__"):
return f"_{self._classname_trimmed}{name}"
return name

def _mangle_slots(self, node: ast3.ClassDef) -> None:
for assign in node.body:
if isinstance(assign, ast3.Assign):
for target in assign.targets:
if isinstance(target, ast3.Name) and (target.id == "__slots__"):
constants: Iterable[ast3.expr] = ()
if isinstance(values := assign.value, ast3.Constant):
constants = (values,)
elif isinstance(values, (ast3.Tuple, ast3.List)):
constants = values.elts
elif isinstance(values, ast3.Dict):
constants = (key for key in values.keys if key is not None)
for value in constants:
if isinstance(value, ast3.Constant) and isinstance(value.value, str):
value.value = self._mangle_name(value.value)

def visit_ClassDef(self, node: ast3.ClassDef) -> ast3.ClassDef:
if self._classname_complete == node.name:
for stmt in node.body:
self.visit(stmt)
self._mangle_slots(node)
else:
for dec in node.decorator_list:
self.visit(dec)
NameMangler(node.name, self._mangle_annotations).visit(node)
node.name = self._mangle_name(node.name)
return node

def _visit_funcdef(self, node: _T_FuncDef) -> _T_FuncDef:
node.name = self._mangle_name(node.name)
if not self._MANGLE_ARGS:
self = NameMangler(self._classname_complete, self._mangle_annotations)
self.visit(node.args)
for dec in node.decorator_list:
self.visit(dec)
if self._mangle_annotations and (node.returns is not None):
self.visit(node.returns)
for stmt in node.body:
self.visit(stmt)
return node

def visit_FunctionDef(self, node: ast3.FunctionDef) -> ast3.FunctionDef:
return self._visit_funcdef(node)

def visit_AsyncFunctionDef(self, node: ast3.AsyncFunctionDef) -> ast3.AsyncFunctionDef:
return self._visit_funcdef(node)

def visit_arg(self, node: ast3.arg) -> ast3.arg:
if self._MANGLE_ARGS:
node.arg = self._mangle_name(node.arg)
else:
self._unmangled_args.add(node.arg)
if self._mangle_annotations and (node.annotation is not None):
self.visit(node.annotation)
return node

def visit_AnnAssign(self, node: ast3.AnnAssign) -> ast3.AnnAssign:
self.visit(node.target)
if node.value is not None:
self.visit(node.value)
if self._mangle_annotations:
self.visit(node.annotation)
return node

def visit_Attribute(self, node: ast3.Attribute) -> ast3.Attribute:
node.attr = self._mangle_name(node.attr)
self.generic_visit(node)
return node

def visit_Name(self, node: Name) -> Name:
if self._MANGLE_ARGS or (node.id not in self._unmangled_args):
node.id = self._mangle_name(node.id)
return node


def find_disallowed_expression_in_annotation_scope(expr: ast3.expr | None) -> ast3.expr | None:
if expr is None:
return None
Expand Down Expand Up @@ -1147,6 +1246,14 @@ def visit_ClassDef(self, n: ast3.ClassDef) -> ClassDef:
if sys.version_info >= (3, 12) and n.type_params:
explicit_type_params = self.translate_type_params(n.type_params)

mangle_annotations = not self.is_stub and not any(
isinstance(i, ImportFrom)
and (i.id == "__future__")
and any(j[0] == "annotations" for j in i.names)
for i in self.imports
)
NameMangler(n.name, mangle_annotations).visit(n)

cdef = ClassDef(
n.name,
self.as_required_block(n.body),
Expand Down
4 changes: 3 additions & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,9 @@ def has_no_attr(
)
failed = True
else:
alternatives = set(original_type.type.names.keys())
alternatives: set[str] = set()
for type_ in original_type.type.mro:
alternatives.update(type_.names.keys())
if module_symbol_table is not None:
alternatives |= {
k for k, v in module_symbol_table.items() if v.module_public
Expand Down
12 changes: 8 additions & 4 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,20 +415,22 @@ def _add_internal_replace_method(self, attributes: list[DataclassAttribute]) ->
Stashes the signature of 'dataclasses.replace(...)' for this specific dataclass
to be used later whenever 'dataclasses.replace' is called for this dataclass.
"""
mangled_name = f"_{self._cls.name.lstrip('_')}{_INTERNAL_REPLACE_SYM_NAME}"
add_method_to_class(
self._api,
self._cls,
_INTERNAL_REPLACE_SYM_NAME,
mangled_name,
args=[attr.to_argument(self._cls.info, of="replace") for attr in attributes],
return_type=NoneType(),
is_staticmethod=True,
)

def _add_internal_post_init_method(self, attributes: list[DataclassAttribute]) -> None:
mangled_name = f"_{self._cls.name.lstrip('_')}{_INTERNAL_POST_INIT_SYM_NAME}"
add_method_to_class(
self._api,
self._cls,
_INTERNAL_POST_INIT_SYM_NAME,
mangled_name,
args=[
attr.to_argument(self._cls.info, of="__post_init__")
for attr in attributes
Expand Down Expand Up @@ -1020,7 +1022,8 @@ def _get_expanded_dataclasses_fields(
ctx, get_proper_type(typ.upper_bound), display_typ, parent_typ
)
elif isinstance(typ, Instance):
replace_sym = typ.type.get_method(_INTERNAL_REPLACE_SYM_NAME)
mangled_name = f"_{typ.type.name.lstrip('_')}{_INTERNAL_REPLACE_SYM_NAME}"
replace_sym = typ.type.get_method(mangled_name)
if replace_sym is None:
return None
replace_sig = replace_sym.type
Expand Down Expand Up @@ -1104,7 +1107,8 @@ def check_post_init(api: TypeChecker, defn: FuncItem, info: TypeInfo) -> None:
return
assert isinstance(defn.type, FunctionLike)

ideal_sig_method = info.get_method(_INTERNAL_POST_INIT_SYM_NAME)
mangled_name = f"_{info.name.lstrip('_')}{_INTERNAL_POST_INIT_SYM_NAME}"
ideal_sig_method = info.get_method(mangled_name)
assert ideal_sig_method is not None and ideal_sig_method.type is not None
ideal_sig = ideal_sig_method.type
assert isinstance(ideal_sig, ProperType) # we set it ourselves
Expand Down
8 changes: 4 additions & 4 deletions mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1811,7 +1811,7 @@ class X:
def __mangle_good(self, text): pass
def __mangle_bad(self, text): pass
""",
error="X.__mangle_bad",
error="X._X__mangle_bad",
)
yield Case(
stub="""
Expand All @@ -1828,7 +1828,7 @@ class __Mangled2:
def __mangle_good(self, text): pass
def __mangle_bad(self, text): pass
""",
error="Klass.__Mangled1.__Mangled2.__mangle_bad",
error="Klass._Klass__Mangled1._Mangled1__Mangled2._Mangled2__mangle_bad",
)
yield Case(
stub="""
Expand All @@ -1841,7 +1841,7 @@ class __Dunder__:
def __mangle_good(self, text): pass
def __mangle_bad(self, text): pass
""",
error="__Dunder__.__mangle_bad",
error="__Dunder__._Dunder____mangle_bad",
)
yield Case(
stub="""
Expand All @@ -1854,7 +1854,7 @@ class _Private:
def __mangle_good(self, text): pass
def __mangle_bad(self, text): pass
""",
error="_Private.__mangle_bad",
error="_Private._Private__mangle_bad",
)

@collect_cases
Expand Down
2 changes: 1 addition & 1 deletion mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1965,7 +1965,7 @@ def tuple_type(self, items: list[Type], line: int, column: int) -> TupleType:


class MsgCallback(Protocol):
def __call__(self, __msg: str, __ctx: Context, *, code: ErrorCode | None = None) -> None: ...
def __call__(self, msg: str, ctx: Context, /, *, code: ErrorCode | None = None) -> None: ...


def get_omitted_any(
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-async-await.test
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ async def f() -> None:
[builtins fixtures/async_await.pyi]
[typing fixtures/typing-async.pyi]
[out]
main:7: error: "Coroutine[Any, Any, AsyncGenerator[str, None]]" has no attribute "__aiter__" (not async iterable)
main:7: error: "Coroutine[Any, Any, AsyncGenerator[str, None]]" has no attribute "__aiter__"; maybe "__await__"? (not async iterable)
main:7: note: Maybe you forgot to use "await"?

[case testAsyncForErrorCanBeIgnored]
Expand Down
Loading
Loading