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

Use namespaces for function type variables #17311

Merged
merged 9 commits into from
Jun 5, 2024
4 changes: 3 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2171,7 +2171,9 @@ def bind_and_map_method(
def get_op_other_domain(self, tp: FunctionLike) -> Type | None:
if isinstance(tp, CallableType):
if tp.arg_kinds and tp.arg_kinds[0] == ARG_POS:
return tp.arg_types[0]
# For generic methods, domain comparison is tricky, as a first
# approximation erase all remaining type variables to bounds.
return erase_typevars(tp.arg_types[0], {v.id for v in tp.variables})
return None
elif isinstance(tp, Overloaded):
raw_items = [self.get_op_other_domain(it) for it in tp.items]
Expand Down
13 changes: 7 additions & 6 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
TypedDictType,
TypeOfAny,
TypeType,
TypeVarId,
TypeVarLikeType,
TypeVarTupleType,
TypeVarType,
Expand Down Expand Up @@ -4916,7 +4917,7 @@ def check_lst_expr(self, e: ListExpr | SetExpr | TupleExpr, fullname: str, tag:
tv = TypeVarType(
"T",
"T",
id=-1,
id=TypeVarId(-1, namespace="<lst>"),
values=[],
upper_bound=self.object_type(),
default=AnyType(TypeOfAny.from_omitted_generics),
Expand Down Expand Up @@ -5147,15 +5148,15 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
kt = TypeVarType(
"KT",
"KT",
id=-1,
id=TypeVarId(-1, namespace="<dict>"),
values=[],
upper_bound=self.object_type(),
default=AnyType(TypeOfAny.from_omitted_generics),
)
vt = TypeVarType(
"VT",
"VT",
id=-2,
id=TypeVarId(-2, namespace="<dict>"),
values=[],
upper_bound=self.object_type(),
default=AnyType(TypeOfAny.from_omitted_generics),
Expand Down Expand Up @@ -5547,7 +5548,7 @@ def check_generator_or_comprehension(
tv = TypeVarType(
"T",
"T",
id=-1,
id=TypeVarId(-1, namespace="<genexp>"),
values=[],
upper_bound=self.object_type(),
default=AnyType(TypeOfAny.from_omitted_generics),
Expand All @@ -5574,15 +5575,15 @@ def visit_dictionary_comprehension(self, e: DictionaryComprehension) -> Type:
ktdef = TypeVarType(
"KT",
"KT",
id=-1,
id=TypeVarId(-1, namespace="<dict>"),
values=[],
upper_bound=self.object_type(),
default=AnyType(TypeOfAny.from_omitted_generics),
)
vtdef = TypeVarType(
"VT",
"VT",
id=-2,
id=TypeVarId(-2, namespace="<dict>"),
values=[],
upper_bound=self.object_type(),
default=AnyType(TypeOfAny.from_omitted_generics),
Expand Down
17 changes: 9 additions & 8 deletions mypy/plugins/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
Type,
TypeOfAny,
TypeType,
TypeVarId,
TypeVarType,
UninhabitedType,
UnionType,
Expand Down Expand Up @@ -807,25 +808,25 @@ def _add_order(ctx: mypy.plugin.ClassDefContext, adder: MethodAdder) -> None:
# AT = TypeVar('AT')
# def __lt__(self: AT, other: AT) -> bool
# This way comparisons with subclasses will work correctly.
fullname = f"{ctx.cls.info.fullname}.{SELF_TVAR_NAME}"
tvd = TypeVarType(
SELF_TVAR_NAME,
ctx.cls.info.fullname + "." + SELF_TVAR_NAME,
id=-1,
fullname,
# Namespace is patched per-method below.
id=TypeVarId(-1, namespace=""),
values=[],
upper_bound=object_type,
default=AnyType(TypeOfAny.from_omitted_generics),
)
self_tvar_expr = TypeVarExpr(
SELF_TVAR_NAME,
ctx.cls.info.fullname + "." + SELF_TVAR_NAME,
[],
object_type,
AnyType(TypeOfAny.from_omitted_generics),
SELF_TVAR_NAME, fullname, [], object_type, AnyType(TypeOfAny.from_omitted_generics)
)
ctx.cls.info.names[SELF_TVAR_NAME] = SymbolTableNode(MDEF, self_tvar_expr)

args = [Argument(Var("other", tvd), tvd, None, ARG_POS)]
for method in ["__lt__", "__le__", "__gt__", "__ge__"]:
namespace = f"{ctx.cls.info.fullname}.{method}"
tvd = tvd.copy_modified(id=TypeVarId(tvd.id.raw_id, namespace=namespace))
args = [Argument(Var("other", tvd), tvd, None, ARG_POS)]
adder.add_method(method, args, bool_type, self_type=tvd, tvd=tvd)


Expand Down
5 changes: 3 additions & 2 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
TupleType,
Type,
TypeOfAny,
TypeVarId,
TypeVarType,
UninhabitedType,
UnionType,
Expand Down Expand Up @@ -314,8 +315,8 @@ def transform(self) -> bool:
obj_type = self._api.named_type("builtins.object")
order_tvar_def = TypeVarType(
SELF_TVAR_NAME,
info.fullname + "." + SELF_TVAR_NAME,
id=-1,
f"{info.fullname}.{SELF_TVAR_NAME}",
id=TypeVarId(-1, namespace=f"{info.fullname}.{method_name}"),
values=[],
upper_bound=obj_type,
default=AnyType(TypeOfAny.from_omitted_generics),
Expand Down
15 changes: 9 additions & 6 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@
TypedDictType,
TypeOfAny,
TypeType,
TypeVarId,
TypeVarLikeType,
TypeVarTupleType,
TypeVarType,
Expand Down Expand Up @@ -894,15 +895,17 @@ def analyze_func_def(self, defn: FuncDef) -> None:
self.prepare_method_signature(defn, self.type, has_self_type)

# Analyze function signature
with self.tvar_scope_frame(self.tvar_scope.method_frame()):
with self.tvar_scope_frame(self.tvar_scope.method_frame(defn.fullname)):
if defn.type:
self.check_classvar_in_signature(defn.type)
assert isinstance(defn.type, CallableType)
# Signature must be analyzed in the surrounding scope so that
# class-level imported names and type variables are in scope.
analyzer = self.type_analyzer()
tag = self.track_incomplete_refs()
result = analyzer.visit_callable_type(defn.type, nested=False)
result = analyzer.visit_callable_type(
defn.type, nested=False, namespace=defn.fullname
)
# Don't store not ready types (including placeholders).
if self.found_incomplete_ref(tag) or has_placeholder(result):
self.defer(defn)
Expand Down Expand Up @@ -1114,7 +1117,7 @@ def update_function_type_variables(self, fun_type: CallableType, defn: FuncItem)
if defn is generic. Return True, if the signature contains typing.Self
type, or False otherwise.
"""
with self.tvar_scope_frame(self.tvar_scope.method_frame()):
with self.tvar_scope_frame(self.tvar_scope.method_frame(defn.fullname)):
a = self.type_analyzer()
fun_type.variables, has_self_type = a.bind_function_type_variables(fun_type, defn)
if has_self_type and self.type is not None:
Expand Down Expand Up @@ -1152,7 +1155,7 @@ def setup_self_type(self) -> None:
info.self_type = TypeVarType(
"Self",
f"{info.fullname}.Self",
id=0,
id=TypeVarId(0), # 0 is a special value for self-types.
values=[],
upper_bound=fill_typevars(info),
default=AnyType(TypeOfAny.from_omitted_generics),
Expand Down Expand Up @@ -1441,15 +1444,15 @@ def add_function_to_symbol_table(self, func: FuncDef | OverloadedFuncDef) -> Non
self.add_symbol(func.name, func, func)

def analyze_arg_initializers(self, defn: FuncItem) -> None:
with self.tvar_scope_frame(self.tvar_scope.method_frame()):
with self.tvar_scope_frame(self.tvar_scope.method_frame(defn.fullname)):
# Analyze default arguments
for arg in defn.arguments:
if arg.initializer:
arg.initializer.accept(self)

def analyze_function_body(self, defn: FuncItem) -> None:
is_method = self.is_class_scope()
with self.tvar_scope_frame(self.tvar_scope.method_frame()):
with self.tvar_scope_frame(self.tvar_scope.method_frame(defn.fullname)):
# Bind the type variables again to visit the body.
if defn.type:
a = self.type_analyzer()
Expand Down
31 changes: 19 additions & 12 deletions mypy/semanal_namedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
Type,
TypeOfAny,
TypeType,
TypeVarId,
TypeVarLikeType,
TypeVarType,
UnboundType,
Expand Down Expand Up @@ -569,40 +570,46 @@ def add_field(
add_field(Var("__match_args__", match_args_type), is_initialized_in_class=True)

assert info.tuple_type is not None # Set by update_tuple_type() above.
tvd = TypeVarType(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the name "tvd" supposed to mean?

Glad to see it gone, the only thing I can even come up with is "type var default" but that doesn't seem like the best name for something that's only ever self type even in non-abbreviated form.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"tvd" stays for TypeVarDef which is not a thing since around couple years ago.

shared_self_type = TypeVarType(
name=SELF_TVAR_NAME,
fullname=info.fullname + "." + SELF_TVAR_NAME,
fullname=f"{info.fullname}.{SELF_TVAR_NAME}",
# Namespace is patched per-method below.
id=self.api.tvar_scope.new_unique_func_id(),
values=[],
upper_bound=info.tuple_type,
default=AnyType(TypeOfAny.from_omitted_generics),
)
selftype = tvd

def add_method(
funcname: str,
ret: Type,
ret: Type | None, # None means use (patched) self-type
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize we can't pass self_type anymore because the ids are now per method. Would it make sense to create a no-op class for this

class SelfTypeMarker:
    pass

so that the type here would be

ret: Type | SelfTypeMarker

?

Given the comment here I don't think it makes that much difference to understandability of the implementation, but lifting the semantics to the type level probably makes callsites easier to read and will make things like signature help in the IDE more descriptive.

args: list[Argument],
is_classmethod: bool = False,
is_new: bool = False,
) -> None:
fullname = f"{info.fullname}.{funcname}"
self_type = shared_self_type.copy_modified(
id=TypeVarId(shared_self_type.id.raw_id, namespace=fullname)
)
if ret is None:
ret = self_type
if is_classmethod or is_new:
first = [Argument(Var("_cls"), TypeType.make_normalized(selftype), None, ARG_POS)]
first = [Argument(Var("_cls"), TypeType.make_normalized(self_type), None, ARG_POS)]
else:
first = [Argument(Var("_self"), selftype, None, ARG_POS)]
first = [Argument(Var("_self"), self_type, None, ARG_POS)]
args = first + args

types = [arg.type_annotation for arg in args]
items = [arg.variable.name for arg in args]
arg_kinds = [arg.kind for arg in args]
assert None not in types
signature = CallableType(cast(List[Type], types), arg_kinds, items, ret, function_type)
signature.variables = [tvd]
signature.variables = [self_type]
func = FuncDef(funcname, args, Block([]))
func.info = info
func.is_class = is_classmethod
func.type = set_callable_name(signature, func)
func._fullname = info.fullname + "." + funcname
func._fullname = fullname
func.line = line
if is_classmethod:
v = Var(funcname, func.type)
Expand All @@ -620,13 +627,13 @@ def add_method(

add_method(
"_replace",
ret=selftype,
ret=None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(all the calls in this block are examples of where I think the code is less readable now, and making a dummy SelfTypeMarker class would help)

args=[Argument(var, var.type, EllipsisExpr(), ARG_NAMED_OPT) for var in vars],
)
if self.options.python_version >= (3, 13):
add_method(
"__replace__",
ret=selftype,
ret=None,
args=[Argument(var, var.type, EllipsisExpr(), ARG_NAMED_OPT) for var in vars],
)

Expand All @@ -635,11 +642,11 @@ def make_init_arg(var: Var) -> Argument:
kind = ARG_POS if default is None else ARG_OPT
return Argument(var, var.type, default, kind)

add_method("__new__", ret=selftype, args=[make_init_arg(var) for var in vars], is_new=True)
add_method("__new__", ret=None, args=[make_init_arg(var) for var in vars], is_new=True)
add_method("_asdict", args=[], ret=ordereddictype)
add_method(
"_make",
ret=selftype,
ret=None,
is_classmethod=True,
args=[Argument(Var("iterable", iterable_type), iterable_type, None, ARG_POS)],
)
Expand Down
4 changes: 2 additions & 2 deletions mypy/semanal_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ def __call__(self, fully_qualified_name: str, args: list[Type] | None = None) ->
def paramspec_args(
name: str,
fullname: str,
id: TypeVarId | int,
id: TypeVarId,
*,
named_type_func: _NamedTypeCallback,
line: int = -1,
Expand All @@ -337,7 +337,7 @@ def paramspec_args(
def paramspec_kwargs(
name: str,
fullname: str,
id: TypeVarId | int,
id: TypeVarId,
*,
named_type_func: _NamedTypeCallback,
line: int = -1,
Expand Down
14 changes: 9 additions & 5 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1792,7 +1792,9 @@ def are_args_compatible(
# If both arguments are required allow_partial_overlap has no effect.
allow_partial_overlap = False

def is_different(left_item: object | None, right_item: object | None) -> bool:
def is_different(
left_item: object | None, right_item: object | None, allow_overlap: bool
) -> bool:
"""Checks if the left and right items are different.

If the right item is unspecified (e.g. if the right callable doesn't care
Expand All @@ -1802,19 +1804,21 @@ def is_different(left_item: object | None, right_item: object | None) -> bool:
if the left callable also doesn't care."""
if right_item is None:
return False
if allow_partial_overlap and left_item is None:
if allow_overlap and left_item is None:
return False
return left_item != right_item

# If right has a specific name it wants this argument to be, left must
# have the same.
if is_different(left.name, right.name):
if is_different(left.name, right.name, allow_partial_overlap):
# But pay attention to whether we're ignoring positional arg names
if not ignore_pos_arg_names or right.pos is None:
return False

# If right is at a specific position, left must have the same:
if is_different(left.pos, right.pos) and not allow_imprecise_kinds:
# If right is at a specific position, left must have the same.
# TODO: partial overlap logic is flawed for positions.
# We disable it to avoid false positives at a cost of few false negatives.
if is_different(left.pos, right.pos, allow_overlap=False) and not allow_imprecise_kinds:
return False

# If right's argument is optional, left's must also be
Expand Down
Loading
Loading