Skip to content
Merged
5 changes: 2 additions & 3 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1164,11 +1164,10 @@ def check_func_def(
isinstance(defn, FuncDef)
and ref_type is not None
and i == 0
and not defn.is_static
and (not defn.is_static or defn.name == "__new__")
and typ.arg_kinds[0] not in [nodes.ARG_STAR, nodes.ARG_STAR2]
):
isclass = defn.is_class or defn.name in ("__new__", "__init_subclass__")
if isclass:
if defn.is_class or defn.name == "__new__":
ref_type = mypy.types.TypeType.make_normalized(ref_type)
erased = get_proper_type(erase_to_bound(arg_type))
if not is_subtype(ref_type, erased, ignore_type_params=True):
Expand Down
6 changes: 1 addition & 5 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,11 +319,7 @@ def analyze_instance_member_access(
mx.msg.cant_assign_to_method(mx.context)
signature = function_type(method, mx.named_type("builtins.function"))
signature = freshen_all_functions_type_vars(signature)
if name == "__new__" or method.is_static:
# __new__ is special and behaves like a static method -- don't strip
# the first argument.
pass
else:
if not method.is_static:
if name != "__call__":
# TODO: use proper treatment of special methods on unions instead
# of this hack here and below (i.e. mx.self_type).
Expand Down
2 changes: 1 addition & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ class FuncBase(Node):
"info",
"is_property",
"is_class", # Uses "@classmethod" (explicit or implicit)
"is_static", # Uses "@staticmethod"
"is_static", # Uses "@staticmethod" (explicit or implicit)
"is_final", # Uses "@final"
"is_explicit_override", # Uses "@override"
"_fullname",
Expand Down
8 changes: 5 additions & 3 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,9 +959,11 @@ def remove_unpack_kwargs(self, defn: FuncDef, typ: CallableType) -> CallableType

def prepare_method_signature(self, func: FuncDef, info: TypeInfo, has_self_type: bool) -> None:
"""Check basic signature validity and tweak annotation of self/cls argument."""
# Only non-static methods are special.
# Only non-static methods are special, as well as __new__.
functype = func.type
if not func.is_static:
if func.name == "__new__":
func.is_static = True
if not func.is_static or func.name == "__new__":
if func.name in ["__init_subclass__", "__class_getitem__"]:
func.is_class = True
if not func.arguments:
Expand Down Expand Up @@ -1383,7 +1385,7 @@ def analyze_function_body(self, defn: FuncItem) -> None:
# The first argument of a non-static, non-class method is like 'self'
# (though the name could be different), having the enclosing class's
# instance type.
if is_method and not defn.is_static and defn.arguments:
if is_method and (not defn.is_static or defn.name == "__new__") and defn.arguments:
if not defn.is_class:
defn.arguments[0].variable.is_self = True
else:
Expand Down
4 changes: 3 additions & 1 deletion mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,9 @@ def callable_type(
fdef: FuncItem, fallback: Instance, ret_type: Type | None = None
) -> CallableType:
# TODO: somewhat unfortunate duplication with prepare_method_signature in semanal
if fdef.info and not fdef.is_static and fdef.arg_names:
if fdef.name == "__new__":
Copy link
Copy Markdown
Collaborator

@hauntsaninja hauntsaninja Jul 4, 2023

Choose a reason for hiding this comment

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

Hmm, mutating fdef here seems a little suspicious to me. Why do we need this? (My weak mental model for when it's safe to mutate these kind of things is to try and keep it to semantic analysis, like you do in prepare method signature)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah, good catch, thanks! I'm actually not entirely sure why I added this -- perhaps this was before I made sure the semantic analyzer set the is_static property. At any rate, I've verified that is_static is set correctly for methods named __new__ by the time we get in typeops, meaning that it is not necessary to modify the attribute here. I've removed it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks! Github is having some problems, so your commit hasn't shown up. The workaround I found is to make an empty commit, push it, then reset to delete the commit, then force push

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I actually had some issues with my local git client suddenly deciding to use the wrong SSH key -- fixed that now, but I see that the commit still isn't visible. Will make an empty commit and push again, thanks for the tip!

fdef.is_static = True
if fdef.info and (not fdef.is_static or fdef.name == "__new__") and fdef.arg_names:
self_type: Type = fill_typevars(fdef.info)
if fdef.is_class or fdef.name == "__new__":
self_type = TypeType.make_normalized(self_type)
Expand Down
52 changes: 52 additions & 0 deletions test-data/unit/check-selftype.test
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,58 @@ class E:
def __init_subclass__(cls) -> None:
reveal_type(cls) # N: Revealed type is "Type[__main__.E]"

[case testSelfTypeNew_explicit]
from typing import TypeVar, Type

T = TypeVar('T', bound='A')
class A:
@staticmethod
def __new__(cls: Type[T]) -> T:
return cls()

@classmethod
def __init_subclass__(cls: Type[T]) -> None:
pass

class B:
@staticmethod
def __new__(cls: Type[T]) -> T: # E: The erased type of self "Type[__main__.A]" is not a supertype of its class "Type[__main__.B]"
return cls()

@classmethod
def __init_subclass__(cls: Type[T]) -> None: # E: The erased type of self "Type[__main__.A]" is not a supertype of its class "Type[__main__.B]"
pass

class C:
@staticmethod
def __new__(cls: Type[C]) -> C:
return cls()

@classmethod
def __init_subclass__(cls: Type[C]) -> None:
pass

class D:
@staticmethod
def __new__(cls: D) -> D: # E: The erased type of self "__main__.D" is not a supertype of its class "Type[__main__.D]"
return cls

@classmethod
def __init_subclass__(cls: D) -> None: # E: The erased type of self "__main__.D" is not a supertype of its class "Type[__main__.D]"
pass

class E:
@staticmethod
def __new__(cls) -> E:
reveal_type(cls) # N: Revealed type is "Type[__main__.E]"
return cls()

@classmethod
def __init_subclass__(cls) -> None:
reveal_type(cls) # N: Revealed type is "Type[__main__.E]"

[builtins fixtures/classmethod.pyi]

[case testSelfTypePropertyUnion]
from typing import Union
class A:
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/fixtures/__init_subclass__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ class bool: pass
class str: pass
class function: pass
class dict: pass
class classmethod: pass
1 change: 1 addition & 0 deletions test-data/unit/fixtures/__new__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ class bool: pass
class str: pass
class function: pass
class dict: pass
class staticmethod: pass
2 changes: 2 additions & 0 deletions test-data/unit/fixtures/dict.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ class bool(int): pass

class ellipsis:
__class__: object
class classmethod: pass
class staticmethod: pass
def isinstance(x: object, t: Union[type, Tuple[type, ...]]) -> bool: pass
class BaseException: pass

Expand Down
1 change: 1 addition & 0 deletions test-data/unit/fixtures/object_with_init_subclass.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class bytearray: pass
class tuple(Generic[T]): pass
class function: pass
class ellipsis: pass
class classmethod: pass

# copy-pasted from list.pyi
class list(Sequence[T]):
Expand Down
2 changes: 2 additions & 0 deletions test-data/unit/fixtures/primitives.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ class frozenset(Iterable[T]):
def __iter__(self) -> Iterator[T]: pass
class function: pass
class ellipsis: pass
class classmethod: pass
class staticmethod: pass

class range(Sequence[int]):
def __init__(self, __x: int, __y: int = ..., __z: int = ...) -> None: pass
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/fixtures/tuple.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class function:
__name__: str
class ellipsis: pass
class classmethod: pass
class staticmethod: pass

# We need int and slice for indexing tuples.
class int:
Expand Down