Skip to content

Make classmethod's first argument be Type[...] #5646

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

Merged
merged 4 commits into from
Sep 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 0 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,8 +892,6 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str])
self.fail(msg, defn)
if note:
self.note(note, defn)
if defn.is_class and isinstance(arg_type, CallableType):
arg_type.is_classmethod_class = True
elif isinstance(arg_type, TypeVarType):
# Refuse covariant parameter type variables
# TODO: check recursively for inner type variables
Expand Down
15 changes: 8 additions & 7 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,16 +602,16 @@ def check_call(self, callee: Type, args: List[Expression],
return callee.ret_type, callee

if (callee.is_type_obj() and callee.type_object().is_abstract
# Exceptions for Type[...] and classmethod first argument
and not callee.from_type_type and not callee.is_classmethod_class
# Exception for Type[...]
and not callee.from_type_type
and not callee.type_object().fallback_to_any):
type = callee.type_object()
self.msg.cannot_instantiate_abstract_class(
callee.type_object().name(), type.abstract_attributes,
context)
elif (callee.is_type_obj() and callee.type_object().is_protocol
# Exceptions for Type[...] and classmethod first argument
and not callee.from_type_type and not callee.is_classmethod_class):
# Exception for Type[...]
and not callee.from_type_type):
self.chk.fail('Cannot instantiate protocol class "{}"'
.format(callee.type_object().name()), context)

Expand Down Expand Up @@ -737,6 +737,9 @@ def analyze_type_type_callee(self, item: Type, context: Context) -> Type:
for c in callee.items()])
if callee:
return callee
# We support Type of namedtuples but not of tuples in general
if isinstance(item, TupleType) and item.fallback.type.fullname() != 'builtins.tuple':
return self.analyze_type_type_callee(item.fallback, context)

self.msg.unsupported_type_type(item, context)
return AnyType(TypeOfAny.from_error)
Expand Down Expand Up @@ -1133,9 +1136,7 @@ def check_arg(self, caller_type: Type, original_caller_type: Type,
caller_type.is_type_obj() and
(caller_type.type_object().is_abstract or caller_type.type_object().is_protocol) and
isinstance(callee_type.item, Instance) and
(callee_type.item.type.is_abstract or callee_type.item.type.is_protocol) and
# ...except for classmethod first argument
not caller_type.is_classmethod_class):
(callee_type.item.type.is_abstract or callee_type.item.type.is_protocol)):
self.msg.concrete_only_call(callee_type, context)
elif not is_subtype(caller_type, callee_type):
if self.chk.should_suppress_optional_error([caller_type, callee_type]):
Expand Down
4 changes: 2 additions & 2 deletions mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from mypy.tvar_scope import TypeVarScope
from mypy.types import (
Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType,
AnyType, TypeList, UnboundType, TypeOfAny
AnyType, TypeList, UnboundType, TypeOfAny, TypeType,
)
from mypy.messages import MessageBuilder
from mypy.options import Options
Expand Down Expand Up @@ -93,7 +93,7 @@ def anal_type(self, t: Type, *,
raise NotImplementedError

@abstractmethod
def class_type(self, info: TypeInfo) -> Type:
def class_type(self, self_type: Type) -> Type:
raise NotImplementedError

@abstractmethod
Expand Down
17 changes: 0 additions & 17 deletions mypy/plugins/attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,23 +495,6 @@ def _add_init(ctx: 'mypy.plugin.ClassDefContext', attributes: List[Attribute],
[attribute.argument(ctx) for attribute in attributes if attribute.init],
NoneTyp()
)
for stmt in ctx.cls.defs.body:
# The type of classmethods will be wrong because it's based on the parent's __init__.
# Set it correctly.
if isinstance(stmt, Decorator) and stmt.func.is_class:
func_type = stmt.func.type
if isinstance(func_type, CallableType):
func_type.arg_types[0] = ctx.api.class_type(ctx.cls.info)
if isinstance(stmt, OverloadedFuncDef) and stmt.is_class:
func_type = stmt.type
if isinstance(func_type, Overloaded):
class_type = ctx.api.class_type(ctx.cls.info)
for item in func_type.items():
item.arg_types[0] = class_type
if stmt.impl is not None:
assert isinstance(stmt.impl, Decorator)
if isinstance(stmt.impl.func.type, CallableType):
stmt.impl.func.type.arg_types[0] = class_type


class MethodAdder:
Expand Down
17 changes: 0 additions & 17 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,23 +92,6 @@ def transform(self) -> None:
args=[attr.to_argument(info) for attr in attributes if attr.is_in_init],
return_type=NoneTyp(),
)
for stmt in self._ctx.cls.defs.body:
# Fix up the types of classmethods since, by default,
# they will be based on the parent class' init.
if isinstance(stmt, Decorator) and stmt.func.is_class:
func_type = stmt.func.type
if isinstance(func_type, CallableType):
func_type.arg_types[0] = self._ctx.api.class_type(self._ctx.cls.info)
if isinstance(stmt, OverloadedFuncDef) and stmt.is_class:
func_type = stmt.type
if isinstance(func_type, Overloaded):
class_type = ctx.api.class_type(ctx.cls.info)
for item in func_type.items():
item.arg_types[0] = class_type
if stmt.impl is not None:
assert isinstance(stmt.impl, Decorator)
if isinstance(stmt.impl.func.type, CallableType):
stmt.impl.func.type.arg_types[0] = class_type

# Add an eq method, but only if the class doesn't already have one.
if decorator_arguments['eq'] and info.get('__eq__') is None:
Expand Down
20 changes: 5 additions & 15 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
from mypy.types import (
FunctionLike, UnboundType, TypeVarDef, TupleType, UnionType, StarType, function_type,
CallableType, Overloaded, Instance, Type, AnyType,
TypeTranslator, TypeOfAny
TypeTranslator, TypeOfAny, TypeType,
)
from mypy.nodes import implicit_module_attrs
from mypy.typeanal import (
Expand Down Expand Up @@ -479,10 +479,9 @@ def prepare_method_signature(self, func: FuncDef, info: TypeInfo) -> None:
elif isinstance(functype, CallableType):
self_type = functype.arg_types[0]
if isinstance(self_type, AnyType):
leading_type = fill_typevars(info) # type: Type
if func.is_class or func.name() in ('__new__', '__init_subclass__'):
leading_type = self.class_type(info)
else:
leading_type = fill_typevars(info)
leading_type = self.class_type(leading_type)
func.type = replace_implicit_first_type(functype, leading_type)

def set_original_def(self, previous: Optional[Node], new: FuncDef) -> bool:
Expand Down Expand Up @@ -775,8 +774,6 @@ def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]:
# that were already set in build_namedtuple_typeinfo.
nt_names = named_tuple_info.names
named_tuple_info.names = SymbolTable()
# This is needed for the cls argument to classmethods to get bound correctly.
named_tuple_info.names['__new__'] = nt_names['__new__']

self.enter_class(named_tuple_info)

Expand Down Expand Up @@ -1343,15 +1340,8 @@ def object_type(self) -> Instance:
def str_type(self) -> Instance:
return self.named_type('__builtins__.str')

def class_type(self, info: TypeInfo) -> Type:
# Construct a function type whose fallback is cls.
from mypy import checkmember # To avoid import cycle.
leading_type = checkmember.type_object_type(info, self.builtin_type)
if isinstance(leading_type, Overloaded):
# Overloaded __init__ is too complex to handle. Plus it's stubs only.
return AnyType(TypeOfAny.special_form)
else:
return leading_type
def class_type(self, self_type: Type) -> Type:
return TypeType.make_normalized(self_type)

def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) -> Instance:
sym = self.lookup_qualified(qualified_name, Context())
Expand Down
8 changes: 0 additions & 8 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,6 @@ def __init__(self,
column: int = -1,
is_ellipsis_args: bool = False,
implicit: bool = False,
is_classmethod_class: bool = False,
special_sig: Optional[str] = None,
from_type_type: bool = False,
bound_args: Sequence[Optional[Type]] = (),
Expand All @@ -730,7 +729,6 @@ def __init__(self,
self.variables = variables
self.is_ellipsis_args = is_ellipsis_args
self.implicit = implicit
self.is_classmethod_class = is_classmethod_class
self.special_sig = special_sig
self.from_type_type = from_type_type
if not bound_args:
Expand Down Expand Up @@ -780,7 +778,6 @@ def copy_modified(self,
is_ellipsis_args=(
is_ellipsis_args if is_ellipsis_args is not _dummy else self.is_ellipsis_args),
implicit=implicit if implicit is not _dummy else self.implicit,
is_classmethod_class=self.is_classmethod_class,
special_sig=special_sig if special_sig is not _dummy else self.special_sig,
from_type_type=from_type_type if from_type_type is not _dummy else self.from_type_type,
bound_args=bound_args if bound_args is not _dummy else self.bound_args,
Expand Down Expand Up @@ -816,9 +813,6 @@ def is_kw_arg(self) -> bool:
def is_type_obj(self) -> bool:
return self.fallback.type.is_metaclass()

def is_concrete_type_obj(self) -> bool:
return self.is_type_obj() and self.is_classmethod_class

def type_object(self) -> mypy.nodes.TypeInfo:
assert self.is_type_obj()
ret = self.ret_type
Expand Down Expand Up @@ -990,7 +984,6 @@ def serialize(self) -> JsonDict:
'variables': [v.serialize() for v in self.variables],
'is_ellipsis_args': self.is_ellipsis_args,
'implicit': self.implicit,
'is_classmethod_class': self.is_classmethod_class,
'bound_args': [(None if t is None else t.serialize())
for t in self.bound_args],
'def_extras': dict(self.def_extras),
Expand All @@ -1009,7 +1002,6 @@ def deserialize(cls, data: JsonDict) -> 'CallableType':
variables=[TypeVarDef.deserialize(v) for v in data['variables']],
is_ellipsis_args=data['is_ellipsis_args'],
implicit=data['implicit'],
is_classmethod_class=data['is_classmethod_class'],
bound_args=[(None if t is None else deserialize_type(t))
for t in data['bound_args']],
def_extras=data['def_extras']
Expand Down
17 changes: 15 additions & 2 deletions test-data/unit/check-attr.test
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,19 @@ A([1], '2') # E: Cannot infer type argument 1 of "A"

[builtins fixtures/list.pyi]

[case testAttrsGenericClassmethod]
from typing import TypeVar, Generic, Optional
import attr
T = TypeVar('T')
@attr.s(auto_attribs=True)
class A(Generic[T]):
x: Optional[T]
@classmethod
def clsmeth(cls) -> None:
reveal_type(cls) # E: Revealed type is 'Type[__main__.A[T`1]]'

[builtins fixtures/classmethod.pyi]

[case testAttrsForwardReference]
import attr
@attr.s(auto_attribs=True)
Expand Down Expand Up @@ -416,7 +429,7 @@ class A:
b: str = attr.ib()
@classmethod
def new(cls) -> A:
reveal_type(cls) # E: Revealed type is 'def (a: builtins.int, b: builtins.str) -> __main__.A'
reveal_type(cls) # E: Revealed type is 'Type[__main__.A]'
return cls(6, 'hello')
@classmethod
def bad(cls) -> A:
Expand Down Expand Up @@ -451,7 +464,7 @@ class A:

@classmethod
def foo(cls, x: Union[int, str]) -> Union[int, str]:
reveal_type(cls) # E: Revealed type is 'def (a: Any, b: Any =) -> __main__.A'
reveal_type(cls) # E: Revealed type is 'Type[__main__.A]'
reveal_type(cls.other()) # E: Revealed type is 'builtins.str'
return x

Expand Down
6 changes: 3 additions & 3 deletions test-data/unit/check-class-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ def f(a: Type[N]):
a()
[builtins fixtures/list.pyi]
[out]
main:8: error: Unsupported type Type["N"]
main:9: error: Too few arguments for "N"

[case testNewNamedTupleWithDefaults]
# flags: --fast-parser --python-version 3.6
Expand Down Expand Up @@ -587,7 +587,7 @@ class XMethBad(NamedTuple):
class MagicalFields(NamedTuple):
x: int
def __slots__(self) -> None: pass # E: Cannot overwrite NamedTuple attribute "__slots__"
def __new__(cls) -> None: pass # E: Name '__new__' already defined (possibly by an import)
def __new__(cls) -> None: pass # E: Cannot overwrite NamedTuple attribute "__new__"
def _source(self) -> int: pass # E: Cannot overwrite NamedTuple attribute "_source"
__annotations__ = {'x': float} # E: NamedTuple field name cannot start with an underscore: __annotations__ \
# E: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]" \
Expand Down Expand Up @@ -640,7 +640,7 @@ class HasClassMethod(NamedTuple):

@classmethod
def new(cls, f: str) -> 'HasClassMethod':
reveal_type(cls) # E: Revealed type is 'def (x: builtins.str) -> Tuple[builtins.str, fallback=__main__.HasClassMethod]'
reveal_type(cls) # E: Revealed type is 'Type[Tuple[builtins.str, fallback=__main__.HasClassMethod]]'
reveal_type(HasClassMethod) # E: Revealed type is 'def (x: builtins.str) -> Tuple[builtins.str, fallback=__main__.HasClassMethod]'
return cls(x=f)

Expand Down
59 changes: 58 additions & 1 deletion test-data/unit/check-classes.test
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,23 @@ class B(A):
def __new__(cls) -> int:
return 1

[case testOverride__new__AndCallObject]
from typing import TypeVar, Generic

class A:
def __new__(cls, x: int) -> 'A':
return object.__new__(cls)

T = TypeVar('T')
class B(Generic[T]):
def __new__(cls, foo: T) -> 'B[T]':
x = object.__new__(cls)
# object.__new__ doesn't have a great type :(
reveal_type(x) # E: Revealed type is 'Any'
return x

[builtins fixtures/__new__.pyi]

[case testInnerFunctionNotOverriding]
class A:
def f(self) -> int: pass
Expand Down Expand Up @@ -3001,7 +3018,7 @@ def f(a: Type[N]):
a()
[builtins fixtures/list.pyi]
[out]
main:3: error: Unsupported type Type["N"]
main:4: error: Too few arguments for "N"

[case testTypeUsingTypeCJoin]
from typing import Type
Expand Down Expand Up @@ -5118,6 +5135,46 @@ class C:
[builtins fixtures/property.pyi]
[out]

[case testClassMethodBeforeInit1]
class Foo:
@classmethod
def bar(cls) -> Foo:
return cls("bar")

def __init__(self, baz: str) -> None:
self.baz = baz
[builtins fixtures/classmethod.pyi]

[case testClassMethodBeforeInit2]
class Foo:
@classmethod
def bar(cls) -> Foo:
return cls(Bar())

def __init__(self, baz: 'Bar') -> None:
self.baz = baz

class Bar: pass
[builtins fixtures/classmethod.pyi]

[case testClassMethodBeforeInit3]
from typing import overload
class Foo:
@classmethod
@overload
def bar(cls, x: int) -> Foo: ...
@classmethod
@overload
def bar(cls, x: str) -> Foo: ...
@classmethod
def bar(cls, x: object) -> Foo:
return cls(x)

def __init__(self, baz: object) -> None:
self.baz = baz

[builtins fixtures/classmethod.pyi]

[case testNewAndInit1]
class A:
def __init__(self, x: int) -> None:
Expand Down
Loading