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

Add support for Self type #14041

Merged
merged 26 commits into from
Nov 15, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b205241
Start working on Self type
ilevkivskyi Nov 7, 2022
5e5bd6f
Add support for special forms; some fixes
ilevkivskyi Nov 7, 2022
7f91f3f
More fixes; more tests
ilevkivskyi Nov 7, 2022
80a11f6
Improve a test
ilevkivskyi Nov 8, 2022
2eb0db1
More tests few more fixes
ilevkivskyi Nov 8, 2022
535d936
Add incremental tests
ilevkivskyi Nov 8, 2022
ca7c7e8
Add docs
ilevkivskyi Nov 8, 2022
2ee66ec
Minor cleanup
ilevkivskyi Nov 8, 2022
ccb74a7
Fix self-compilation
ilevkivskyi Nov 8, 2022
504fe2c
Best effort support for unusual locations for self
ilevkivskyi Nov 8, 2022
1a99961
Some cleanups
ilevkivskyi Nov 9, 2022
ce8d345
Enable ClassVar (to some safe extent)
ilevkivskyi Nov 9, 2022
324eff2
Allow redundant Self by default; add error code
ilevkivskyi Nov 9, 2022
d96cfdc
Prohibit Self with arguments
ilevkivskyi Nov 9, 2022
0b953cf
Address CR; minor cleanups
ilevkivskyi Nov 10, 2022
5829804
Prohibit unclear cases; some more tests
ilevkivskyi Nov 10, 2022
3ec47b9
Make ClassVar in generics better
ilevkivskyi Nov 10, 2022
24dd649
More cleanup
ilevkivskyi Nov 10, 2022
ac6234d
Fix TypeVar id clash
ilevkivskyi Nov 11, 2022
61c0589
Final tweaks + couple tests
ilevkivskyi Nov 12, 2022
cbd97b1
Fix another bug from mypy_primer
ilevkivskyi Nov 12, 2022
362d84a
Fix upper bound for Self
ilevkivskyi Nov 12, 2022
a5740eb
More CR (docstring)
ilevkivskyi Nov 12, 2022
6694f3b
Fix Self import; fix method bodies; simplify id handling
ilevkivskyi Nov 12, 2022
3f86bf2
Allow using plain class in final classes
ilevkivskyi Nov 12, 2022
16e017d
Merge branch 'master' into self-type
ilevkivskyi Nov 14, 2022
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
50 changes: 40 additions & 10 deletions docs/source/generics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -264,15 +264,8 @@ Generic methods and generic self
You can also define generic methods — just use a type variable in the
method signature that is different from class type variables. In particular,
``self`` may also be generic, allowing a method to return the most precise
type known at the point of access.

.. note::

This feature is experimental. Checking code with type annotations for self
arguments is still not fully implemented. Mypy may disallow valid code or
allow unsafe code.
ilevkivskyi marked this conversation as resolved.
Show resolved Hide resolved

In this way, for example, you can typecheck chaining of setter methods:
type known at the point of access. In this way, for example, you can typecheck
chaining of setter methods:

.. code-block:: python

Expand Down Expand Up @@ -335,6 +328,43 @@ possibly by making use of the ``Any`` type.

For some advanced uses of self-types see :ref:`additional examples <advanced_self>`.

Automatic self types using typing.Self
**************************************

The patterns described above are quite common, so there is a syntactic sugar
for them introduced in :pep:`673`. Instead of defining a type variable and
using an explicit ``self`` annotation, you can import a magic type ``typing.Self``
that is automatically transformed into a type variable with an upper bound of
current class, and you don't need an annotation for ``self`` (or ``cls`` for
class methods). The above example can thus be rewritten as:

.. code-block:: python

from typing import Self

class Friend:
other: Self | None = None

@classmethod
def make_pair(cls) -> tuple[Self, Self]:
a, b = cls(), cls()
a.other = b
b.other = a
return a, b

class SuperFriend(Friend):
pass

a, b = SuperFriend.make_pair()

This is more compact than using explicit type variables, plus additionally
you can use ``Self`` in attribute annotations, not just in methods.

.. note::

This feature is available on Python 3.11 or newer. On older Python versions
you can import backported ``Self`` from latest ``typing_extensions``.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd suggest replacing 'latest' with 'recent enough' since otherwise it go stale quickly.

Copy link
Member

Choose a reason for hiding this comment

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

Also, it's misleading to start with "This feature is available on Python 3.11 or newer." Users will think they can't use Self before 3.11. I'd suggest saying:

Suggested change
you can import backported ``Self`` from latest ``typing_extensions``.
``Self`` can be imported from ``typing`` on Python 3.11 or higher, and also from
``typing_extensions`` on version 4.0.0 or higher.

(Ref: https://github.com/python/typing_extensions/blob/main/CHANGELOG.md#release-400-november-14-2021)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I will make it more clear (or maybe just commit what Jelle proposed)


.. _variance-of-generics:

Variance of generic types
Expand Down Expand Up @@ -548,7 +578,7 @@ Note that class decorators are handled differently than function decorators in
mypy: decorating a class does not erase its type, even if the decorator has
incomplete type annotations.

Suppose we have the following decorator, not type annotated yet,
Suppose we have the following decorator, not type annotated yet,
that preserves the original function's signature and merely prints the decorated function's name:

.. code-block:: python
Expand Down
8 changes: 7 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from mypy.erasetype import erase_type, erase_typevars, remove_instance_last_known_values
from mypy.errorcodes import TYPE_VAR, UNUSED_AWAITABLE, UNUSED_COROUTINE, ErrorCode
from mypy.errors import Errors, ErrorWatcher, report_internal_error
from mypy.expandtype import expand_type, expand_type_by_instance
from mypy.expandtype import expand_self_type, expand_type, expand_type_by_instance
from mypy.join import join_types
from mypy.literals import Key, literal, literal_hash
from mypy.maptype import map_instance_to_supertype
Expand Down Expand Up @@ -2468,6 +2468,10 @@ class C(B, A[int]): ... # this is unsafe because...
second_sig = self.bind_and_map_method(second, second_type, ctx, base2)
ok = is_subtype(first_sig, second_sig, ignore_pos_arg_names=True)
elif first_type and second_type:
if isinstance(first.node, Var):
first_type = expand_self_type(first.node, first_type, fill_typevars(base1))
if isinstance(second.node, Var):
second_type = expand_self_type(second.node, second_type, fill_typevars(base2))
ok = is_equivalent(first_type, second_type)
if not ok:
second_node = base2[name].node
Expand Down Expand Up @@ -3048,6 +3052,8 @@ def lvalue_type_from_base(
if base_var:
base_node = base_var.node
base_type = base_var.type
if isinstance(base_node, Var) and base_type is not None:
base_type = expand_self_type(base_node, base_type, fill_typevars(base))
if isinstance(base_node, Decorator):
base_node = base_node.func
base_type = base_node.type
Expand Down
5 changes: 5 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2666,6 +2666,10 @@ def analyze_ordinary_member_access(self, e: MemberExpr, is_lvalue: bool) -> Type

if isinstance(base, RefExpr) and isinstance(base.node, MypyFile):
module_symbol_table = base.node.names
if isinstance(base, RefExpr) and isinstance(base.node, Var):
is_self = base.node.is_self
else:
is_self = False

member_type = analyze_member_access(
e.name,
Expand All @@ -2679,6 +2683,7 @@ def analyze_ordinary_member_access(self, e: MemberExpr, is_lvalue: bool) -> Type
chk=self.chk,
in_literal_context=self.is_literal_context(),
module_symbol_table=module_symbol_table,
is_self=is_self,
)

return member_type
Expand Down
24 changes: 18 additions & 6 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from mypy import meet, message_registry, subtypes
from mypy.erasetype import erase_typevars
from mypy.expandtype import expand_type_by_instance, freshen_function_type_vars
from mypy.expandtype import expand_self_type, expand_type_by_instance, freshen_function_type_vars
from mypy.maptype import map_instance_to_supertype
from mypy.messages import MessageBuilder
from mypy.nodes import (
Expand Down Expand Up @@ -90,6 +90,7 @@ def __init__(
self_type: Type | None,
module_symbol_table: SymbolTable | None = None,
no_deferral: bool = False,
is_self: bool = False,
) -> None:
self.is_lvalue = is_lvalue
self.is_super = is_super
Expand All @@ -101,6 +102,7 @@ def __init__(
self.chk = chk
self.module_symbol_table = module_symbol_table
self.no_deferral = no_deferral
self.is_self = is_self

def named_type(self, name: str) -> Instance:
return self.chk.named_type(name)
Expand Down Expand Up @@ -152,6 +154,7 @@ def analyze_member_access(
self_type: Type | None = None,
module_symbol_table: SymbolTable | None = None,
no_deferral: bool = False,
is_self: bool = False,
) -> Type:
"""Return the type of attribute 'name' of 'typ'.

Expand Down Expand Up @@ -187,6 +190,7 @@ def analyze_member_access(
self_type=self_type,
module_symbol_table=module_symbol_table,
no_deferral=no_deferral,
is_self=is_self,
)
result = _analyze_member_access(name, typ, mx, override_info)
possible_literal = get_proper_type(result)
Expand Down Expand Up @@ -682,12 +686,12 @@ def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type:
return inferred_dunder_get_type.ret_type


def is_instance_var(var: Var, info: TypeInfo) -> bool:
def is_instance_var(var: Var) -> bool:
"""Return if var is an instance variable according to PEP 526."""
return (
# check the type_info node is the var (not a decorated function, etc.)
var.name in info.names
and info.names[var.name].node is var
var.name in var.info.names
and var.info.names[var.name].node is var
and not var.is_classvar
# variables without annotations are treated as classvar
and not var.is_inferred
Expand Down Expand Up @@ -723,11 +727,13 @@ def analyze_var(
if mx.is_lvalue and var.is_classvar:
mx.msg.cant_assign_to_classvar(name, mx.context)
t = get_proper_type(expand_type_by_instance(typ, itype))
if not (mx.is_self or mx.is_super):
t = get_proper_type(expand_self_type(var, t, mx.original_type))
result: Type = t
typ = get_proper_type(typ)
if (
var.is_initialized_in_class
and (not is_instance_var(var, info) or mx.is_operator)
and (not is_instance_var(var) or mx.is_operator)
and isinstance(typ, FunctionLike)
and not typ.is_type_obj()
):
Expand Down Expand Up @@ -945,7 +951,12 @@ def analyze_class_attribute_access(
# x: T
# C.x # Error, ambiguous access
# C[int].x # Also an error, since C[int] is same as C at runtime
if isinstance(t, TypeVarType) or has_type_vars(t):
# Exception is Self type wrapped in ClassVar, that is safe.
if node.node.info.self_type is not None and node.node.is_classvar:
exclude = {node.node.info.self_type.id}
else:
exclude = set()
ilevkivskyi marked this conversation as resolved.
Show resolved Hide resolved
if isinstance(t, TypeVarType) and t.id not in exclude or has_type_vars(t, exclude):
# Exception: access on Type[...], including first argument of class methods is OK.
if not isinstance(get_proper_type(mx.original_type), TypeType) or node.implicit:
if node.node.is_classvar:
Expand All @@ -958,6 +969,7 @@ def analyze_class_attribute_access(
# In the above example this means that we infer following types:
# C.x -> Any
# C[int].x -> int
t = get_proper_type(expand_self_type(node.node, t, itype))
t = erase_typevars(expand_type_by_instance(t, isuper))

is_classmethod = (is_decorated and cast(Decorator, node.node).func.is_class) or (
Expand Down
19 changes: 18 additions & 1 deletion mypy/expandtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Iterable, Mapping, Sequence, TypeVar, cast, overload

from mypy.nodes import ARG_STAR
from mypy.nodes import ARG_STAR, Var
from mypy.types import (
AnyType,
CallableType,
Expand Down Expand Up @@ -383,3 +383,20 @@ def expand_unpack_with_variables(
raise NotImplementedError(f"Invalid type replacement to expand: {repl}")
else:
raise NotImplementedError(f"Invalid type to expand: {t.type}")


@overload
def expand_self_type(var: Var, typ: ProperType, replacement: ProperType) -> ProperType:
...


@overload
def expand_self_type(var: Var, typ: Type, replacement: Type) -> Type:
...


def expand_self_type(var: Var, typ: Type, replacement: Type) -> Type:
"""Expand appearances of Self type in a variable type."""
if var.info.self_type is not None and not var.is_property:
return expand_type(typ, {var.info.self_type.id: replacement})
return typ
8 changes: 8 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2810,6 +2810,7 @@ class is generic then it will be a type constructor of higher kind.
"has_type_var_tuple_type",
"type_var_tuple_prefix",
"type_var_tuple_suffix",
"self_type",
)

_fullname: Bogus[str] # Fully qualified name
Expand Down Expand Up @@ -2950,6 +2951,9 @@ class is generic then it will be a type constructor of higher kind.
# in case we are doing multiple semantic analysis passes.
special_alias: TypeAlias | None

# Shared type variable for typing.Self in this class (if used, otherwise None).
self_type: mypy.types.TypeVarType | None

FLAGS: Final = [
"is_abstract",
"is_enum",
Expand Down Expand Up @@ -3002,6 +3006,7 @@ def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None
self.is_newtype = False
self.is_intersection = False
self.metadata = {}
self.self_type = None

def add_type_vars(self) -> None:
self.has_type_var_tuple_type = False
Expand Down Expand Up @@ -3219,6 +3224,7 @@ def serialize(self) -> JsonDict:
"metadata": self.metadata,
"slots": list(sorted(self.slots)) if self.slots is not None else None,
"deletable_attributes": self.deletable_attributes,
"self_type": self.self_type.serialize() if self.self_type is not None else None,
}
return data

Expand Down Expand Up @@ -3275,6 +3281,8 @@ def deserialize(cls, data: JsonDict) -> TypeInfo:
ti.slots = set(data["slots"]) if data["slots"] is not None else None
ti.deletable_attributes = data["deletable_attributes"]
set_flags(ti, data["flags"])
st = data["self_type"]
ti.self_type = mypy.types.TypeVarType.deserialize(st) if st is not None else None
return ti


Expand Down
38 changes: 27 additions & 11 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from __future__ import annotations

from typing import Optional
from typing_extensions import Final

from mypy.expandtype import expand_type
from mypy.nodes import (
ARG_NAMED,
ARG_NAMED_OPT,
Expand Down Expand Up @@ -50,6 +52,7 @@
TypeVarType,
get_proper_type,
)
from mypy.typevars import fill_typevars

# The set of decorators that generate dataclasses.
dataclass_makers: Final = {"dataclass", "dataclasses.dataclass"}
Expand Down Expand Up @@ -83,7 +86,7 @@ def __init__(
self.info = info
self.kw_only = kw_only

def to_argument(self) -> Argument:
def to_argument(self, current_info: TypeInfo) -> Argument:
arg_kind = ARG_POS
if self.kw_only and self.has_default:
arg_kind = ARG_NAMED_OPT
Expand All @@ -92,11 +95,23 @@ def to_argument(self) -> Argument:
elif not self.kw_only and self.has_default:
arg_kind = ARG_OPT
return Argument(
variable=self.to_var(), type_annotation=self.type, initializer=None, kind=arg_kind
variable=self.to_var(current_info),
type_annotation=self.expand_type(current_info),
initializer=None,
kind=arg_kind,
)

def to_var(self) -> Var:
return Var(self.name, self.type)
def expand_type(self, current_info: TypeInfo) -> Optional[Type]:
if self.type is not None and self.info.self_type is not None:
# In general, it is not safe to call `expand_type()` during semantic analyzis,
# however this plugin is called very late, so all types should be fully ready.
# Also, it is tricky to avoid eager expansion of Self types here (e.g. because
# we serialize attributes).
return expand_type(self.type, {self.info.self_type.id: fill_typevars(current_info)})
return self.type

def to_var(self, current_info: TypeInfo) -> Var:
return Var(self.name, self.expand_type(current_info))

def serialize(self) -> JsonDict:
assert self.type
Expand Down Expand Up @@ -175,11 +190,12 @@ def transform(self) -> bool:
and attributes
):

args = [
attr.to_argument()
for attr in attributes
if attr.is_in_init and not self._is_kw_only_type(attr.type)
]
with state.strict_optional_set(ctx.api.options.strict_optional):
args = [
attr.to_argument(info)
for attr in attributes
if attr.is_in_init and not self._is_kw_only_type(attr.type)
]

if info.fallback_to_any:
# Make positional args optional since we don't know their order.
Expand Down Expand Up @@ -548,7 +564,7 @@ def _freeze(self, attributes: list[DataclassAttribute]) -> None:
if isinstance(var, Var):
var.is_property = True
else:
var = attr.to_var()
var = attr.to_var(info)
var.info = info
var.is_property = True
var._fullname = info.fullname + "." + var.name
Expand All @@ -567,7 +583,7 @@ def _propertize_callables(
info = self._ctx.cls.info
for attr in attributes:
if isinstance(get_proper_type(attr.type), CallableType):
var = attr.to_var()
var = attr.to_var(info)
var.info = info
var.is_property = True
var.is_settable_property = settable
Expand Down
Loading