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 tuple[object, ...] and dict[str, object] as upper bounds for ParamSpec.args and ParamSpec.kwargs #12668

Merged
merged 11 commits into from
Apr 29, 2022
2 changes: 1 addition & 1 deletion mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def _analyze_member_access(name: str,
return analyze_typeddict_access(name, typ, mx, override_info)
elif isinstance(typ, NoneType):
return analyze_none_member_access(name, typ, mx)
elif isinstance(typ, TypeVarType):
elif isinstance(typ, TypeVarLikeType):
return _analyze_member_access(name, typ.upper_bound, mx, override_info)
elif isinstance(typ, DeletedType):
mx.msg.deleted_as_rvalue(typ, mx.context)
Expand Down
2 changes: 1 addition & 1 deletion mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1369,7 +1369,7 @@ class Foo(Bar, Generic[T]): ...
del base_type_exprs[i]
tvar_defs: List[TypeVarLikeType] = []
for name, tvar_expr in declared_tvars:
tvar_def = self.tvar_scope.bind_new(name, tvar_expr)
tvar_def = self.tvar_scope.bind_new(name, tvar_expr, named_type_func=self.named_type)
tvar_defs.append(tvar_def)
return base_type_exprs, tvar_defs, is_protocol

Expand Down
9 changes: 6 additions & 3 deletions mypy/tvar_scope.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional, Dict, Union
from typing import Optional, Dict, Union, Callable
from mypy.types import (
TypeVarLikeType, TypeVarType, ParamSpecType, ParamSpecFlavor, TypeVarId, TypeVarTupleType,
Instance
)
from mypy.nodes import (
ParamSpecExpr, TypeVarExpr, TypeVarLikeExpr, SymbolTableNode, TypeVarTupleExpr,
Expand Down Expand Up @@ -61,7 +62,9 @@ def class_frame(self, namespace: str) -> 'TypeVarLikeScope':
"""A new scope frame for binding a class. Prohibits *this* class's tvars"""
return TypeVarLikeScope(self.get_function_scope(), True, self, namespace=namespace)

def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType:
def bind_new(
self, name: str, tvar_expr: TypeVarLikeExpr, *, named_type_func: Callable[..., Instance]
) -> TypeVarLikeType:
if self.is_class_scope:
self.class_id += 1
i = self.class_id
Expand All @@ -88,7 +91,7 @@ def bind_new(self, name: str, tvar_expr: TypeVarLikeExpr) -> TypeVarLikeType:
tvar_expr.fullname,
i,
flavor=ParamSpecFlavor.BARE,
upper_bound=tvar_expr.upper_bound,
named_type_func=named_type_func,
line=tvar_expr.line,
column=tvar_expr.column
)
Expand Down
20 changes: 8 additions & 12 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
# Change the line number
return ParamSpecType(
tvar_def.name, tvar_def.fullname, tvar_def.id, tvar_def.flavor,
tvar_def.upper_bound, line=t.line, column=t.column,
named_type_func=self.named_type, line=t.line, column=t.column,
)
if isinstance(sym.node, TypeVarExpr) and tvar_def is not None and self.defining_alias:
self.fail('Can\'t use bound type variable "{}"'
Expand Down Expand Up @@ -717,7 +717,7 @@ def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> Type:
else:
assert False, kind
return ParamSpecType(tvar_def.name, tvar_def.fullname, tvar_def.id, flavor,
upper_bound=self.named_type('builtins.object'),
named_type_func=self.named_type,
line=t.line, column=t.column)
return self.anal_type(t, nested=nested)

Expand Down Expand Up @@ -855,13 +855,11 @@ def analyze_callable_args_for_paramspec(
if not isinstance(tvar_def, ParamSpecType):
return None

# TODO: Use tuple[...] or Mapping[..] instead?
obj = self.named_type('builtins.object')
return CallableType(
[ParamSpecType(tvar_def.name, tvar_def.fullname, tvar_def.id, ParamSpecFlavor.ARGS,
upper_bound=obj),
named_type_func=self.named_type),
ParamSpecType(tvar_def.name, tvar_def.fullname, tvar_def.id, ParamSpecFlavor.KWARGS,
upper_bound=obj)],
named_type_func=self.named_type)],
[nodes.ARG_STAR, nodes.ARG_STAR2],
[None, None],
ret_type=ret_type,
Expand Down Expand Up @@ -891,18 +889,16 @@ def analyze_callable_args_for_concatenate(
if not isinstance(tvar_def, ParamSpecType):
return None

# TODO: Use tuple[...] or Mapping[..] instead?
obj = self.named_type('builtins.object')
# ick, CallableType should take ParamSpecType
prefix = tvar_def.prefix
# we don't set the prefix here as generic arguments will get updated at some point
# in the future. CallableType.param_spec() accounts for this.
return CallableType(
[*prefix.arg_types,
ParamSpecType(tvar_def.name, tvar_def.fullname, tvar_def.id, ParamSpecFlavor.ARGS,
upper_bound=obj),
named_type_func=self.named_type),
ParamSpecType(tvar_def.name, tvar_def.fullname, tvar_def.id, ParamSpecFlavor.KWARGS,
upper_bound=obj)],
named_type_func=self.named_type)],
[*prefix.arg_kinds, nodes.ARG_STAR, nodes.ARG_STAR2],
[*prefix.arg_names, None, None],
ret_type=ret_type,
Expand Down Expand Up @@ -1134,7 +1130,7 @@ def bind_function_type_variables(
assert var_node, "Binding for function type variable not found within function"
var_expr = var_node.node
assert isinstance(var_expr, TypeVarLikeExpr)
self.tvar_scope.bind_new(var.name, var_expr)
self.tvar_scope.bind_new(var.name, var_expr, named_type_func=self.named_type)
return fun_type.variables
typevars = self.infer_type_variables(fun_type)
# Do not define a new type variable if already defined in scope.
Expand All @@ -1144,7 +1140,7 @@ def bind_function_type_variables(
for name, tvar in typevars:
if not self.tvar_scope.allow_binding(tvar.fullname):
self.fail('Type variable "{}" is bound by an outer class'.format(name), defn)
self.tvar_scope.bind_new(name, tvar)
self.tvar_scope.bind_new(name, tvar, named_type_func=self.named_type)
binding = self.tvar_scope.get_binding(tvar.fullname)
assert binding is not None
defs.append(binding)
Expand Down
38 changes: 29 additions & 9 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from typing import (
Any, TypeVar, Dict, List, Tuple, cast, Set, Optional, Union, Iterable, NamedTuple,
Sequence
Sequence, Callable
)
from typing_extensions import ClassVar, Final, TYPE_CHECKING, overload, TypeAlias as _TypeAlias

Expand Down Expand Up @@ -583,19 +583,39 @@ class ParamSpecType(TypeVarLikeType):
prefix: 'Parameters'

def __init__(
self, name: str, fullname: str, id: Union[TypeVarId, int], flavor: int,
upper_bound: Type, *, line: int = -1, column: int = -1,
prefix: Optional['Parameters'] = None
self, name: str, fullname: str, id: Union[TypeVarId, int], flavor: int, *,
upper_bound: Optional[Type] = None,
named_type_func: Optional[Callable[..., 'Instance']] = None, line: int = -1,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Currently we at least mostly avoid calling back to the type checker in mypy.types or mypy.nodes (even through a callback), and I'd prefer to continue to maintain this principle, as a style issue. Also, it's better if constructors of essentially data objects such as types don't perform any non-trivial logic, such as calling get_fallback.

What about defining a helper function in mypy.semanal_shared that takes some of these arguments (named_type_func etc.) and calculates the fallback? get_fallback would be removed from here and would part of this helper function. You could name it calculate_param_spec_fallback, for example, similar to calculate_tuple_fallback that is already defined there.

Also, can you add argument types for the callback?

Copy link
Member Author

@AlexWaygood AlexWaygood Apr 28, 2022

Choose a reason for hiding this comment

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

I moved the code into semanal_shared -- although it could actually possibly be moved into typeanal, since it seems paramspec args and paramspec kwargs are only ever constructed in typeanal at the moment. (But perhaps that might change in the future -- what do you think?)

Moving the code into typeanal would mean that we wouldn't have to use a callback protocol at all, the functions could just call self.named_type() directly. But the functions might become more tightly coupled with the logic in typeanal.py.

column: int = -1, prefix: Optional['Parameters'] = None
) -> None:
if upper_bound is None:
assert named_type_func is not None, (
"Either 'upper_bound' or 'named_type_func' must be specified"
)
upper_bound = self.get_fallback(flavor, named_type_func)
super().__init__(name, fullname, id, upper_bound, line=line, column=column)
self.flavor = flavor
self.prefix = prefix or Parameters([], [], [])

@staticmethod
def get_fallback(flavor: int, named_type_func: Callable[..., 'Instance']) -> 'Instance':
builtins_object = named_type_func('builtins.object')
if flavor == ParamSpecFlavor.BARE:
return builtins_object
elif flavor == ParamSpecFlavor.ARGS:
return named_type_func('builtins.tuple', [builtins_object])
else:
return named_type_func(
'builtins.dict',
[named_type_func('builtins.str'), builtins_object]
)

@staticmethod
def new_unification_variable(old: 'ParamSpecType') -> 'ParamSpecType':
new_id = TypeVarId.new(meta_level=1)
return ParamSpecType(old.name, old.fullname, new_id, old.flavor, old.upper_bound,
line=old.line, column=old.column, prefix=old.prefix)
return ParamSpecType(old.name, old.fullname, new_id, old.flavor,
upper_bound=old.upper_bound, line=old.line, column=old.column,
prefix=old.prefix)

def with_flavor(self, flavor: int) -> 'ParamSpecType':
return ParamSpecType(self.name, self.fullname, self.id, flavor,
Expand All @@ -610,7 +630,7 @@ def copy_modified(self, *,
self.fullname,
id if id is not _dummy else self.id,
flavor if flavor is not _dummy else self.flavor,
self.upper_bound,
upper_bound=self.upper_bound,
line=self.line,
column=self.column,
prefix=prefix if prefix is not _dummy else self.prefix,
Expand Down Expand Up @@ -656,7 +676,7 @@ def deserialize(cls, data: JsonDict) -> 'ParamSpecType':
data['fullname'],
data['id'],
data['flavor'],
deserialize_type(data['upper_bound']),
upper_bound=deserialize_type(data['upper_bound']),
prefix=Parameters.deserialize(data['prefix'])
)

Expand Down Expand Up @@ -1739,7 +1759,7 @@ def param_spec(self) -> Optional[ParamSpecType]:
# TODO: confirm that all arg kinds are positional
prefix = Parameters(self.arg_types[:-2], self.arg_kinds[:-2], self.arg_names[:-2])
return ParamSpecType(arg_type.name, arg_type.fullname, arg_type.id, ParamSpecFlavor.BARE,
arg_type.upper_bound, prefix=prefix)
upper_bound=arg_type.upper_bound, prefix=prefix)

def expand_param_spec(self,
c: Union['CallableType', Parameters],
Expand Down
2 changes: 1 addition & 1 deletion mypy/typevars.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def fill_typevars(typ: TypeInfo) -> Union[Instance, TupleType]:
)
else:
assert isinstance(tv, ParamSpecType)
tv = ParamSpecType(tv.name, tv.fullname, tv.id, tv.flavor, tv.upper_bound,
tv = ParamSpecType(tv.name, tv.fullname, tv.id, tv.flavor, upper_bound=tv.upper_bound,
line=-1, column=-1)
tvs.append(tv)
inst = Instance(typ, tvs)
Expand Down
Loading