@@ -3833,6 +3833,359 @@ def update_bounded_value(value: HasBoundedValue) -> None:
38333833 value.bounded_value = " bad" # error: [invalid-assignment]
38343834```
38353835
3836+ ### Specialized protocol setter value types
3837+
3838+ ` P[int] ` does not depend on the setter's unused ` U ` . A property setter accepting ` P[int] ` therefore
3839+ satisfies the same writable member, including for strict subtyping:
3840+
3841+ ``` toml
3842+ [environment ]
3843+ python-version = " 3.12"
3844+ ```
3845+
3846+ ``` py
3847+ from typing import Protocol
3848+ from ty_extensions import static_assert
3849+ from ty_extensions._internal import is_subtype_of
3850+
3851+ class P[T](Protocol):
3852+ def method (self ) -> T: ...
3853+
3854+ class Descriptor :
3855+ def __init__ (self , getter : object ) -> None : ...
3856+ def __get__ (self , instance : object , owner : type | None = None ) -> object :
3857+ return object ()
3858+
3859+ def __set__[U](self , instance: object , value: P[int ]) -> None : ...
3860+
3861+ class HasValue (Protocol ):
3862+ @Descriptor
3863+ def value (self ) -> object : ...
3864+
3865+ class Implementation :
3866+ @ property
3867+ def value (self ) -> object :
3868+ return object ()
3869+
3870+ @value.setter
3871+ def value (self , value : P[int ]) -> None : ...
3872+
3873+ static_assert(is_subtype_of(Implementation, HasValue))
3874+ ```
3875+
3876+ A setter accepting only ` P[Literal[1]] ` is too narrow:
3877+
3878+ ``` py
3879+ from typing import Literal
3880+
3881+ class Narrowed :
3882+ @ property
3883+ def value (self ) -> object :
3884+ return object ()
3885+
3886+ @value.setter
3887+ def value (self , value : P[Literal[1 ]]) -> None : ...
3888+
3889+ x: HasValue = Narrowed() # error: [invalid-assignment]
3890+ ```
3891+
3892+ ### Local protocol setter types
3893+
3894+ A setter's unused type parameter does not affect the accepted value type, even when that type is a
3895+ local protocol. A property setter accepting the same protocol satisfies the writable member:
3896+
3897+ ``` toml
3898+ [environment ]
3899+ python-version = " 3.12"
3900+ ```
3901+
3902+ ``` py
3903+ from typing import Protocol
3904+ from ty_extensions import static_assert
3905+ from ty_extensions._internal import is_subtype_of
3906+
3907+ def _ () -> None :
3908+ class Local (Protocol ):
3909+ member: int
3910+
3911+ class Descriptor :
3912+ def __init__ (self , getter : object ) -> None : ...
3913+ def __get__ (self , instance : object , owner : type | None = None ) -> int :
3914+ return 1
3915+
3916+ def __set__[U](self , instance: object , value: Local) -> None : ...
3917+
3918+ class HasValue (Protocol ):
3919+ @Descriptor
3920+ def value (self ) -> int : ...
3921+
3922+ class Implementation :
3923+ @ property
3924+ def value (self ) -> int :
3925+ return 1
3926+
3927+ @value.setter
3928+ def value (self , value : Local) -> None : ...
3929+
3930+ static_assert(is_subtype_of(Implementation, HasValue))
3931+ ```
3932+
3933+ ### Recursive protocol setter value types
3934+
3935+ ` P[int] ` does not depend on the setter's ` U ` , so a matching property setter satisfies the writable
3936+ member even though ` P ` 's method returns another instance of the protocol:
3937+
3938+ ``` toml
3939+ [environment ]
3940+ python-version = " 3.12"
3941+ ```
3942+
3943+ ``` py
3944+ from __future__ import annotations
3945+
3946+ from typing import Protocol
3947+ from ty_extensions import static_assert
3948+ from ty_extensions._internal import is_subtype_of
3949+
3950+ class P[T](Protocol):
3951+ def method (self ) -> P[int ]: ...
3952+
3953+ class Descriptor :
3954+ def __init__ (self , getter : object ) -> None : ...
3955+ def __get__ (self , instance : object , owner : type | None = None ) -> object :
3956+ return object ()
3957+
3958+ def __set__[U](self , instance: object , value: P[int ]) -> None : ...
3959+
3960+ class HasValue (Protocol ):
3961+ @Descriptor
3962+ def value (self ) -> object : ...
3963+
3964+ class Implementation :
3965+ @ property
3966+ def value (self ) -> object :
3967+ return object ()
3968+
3969+ @value.setter
3970+ def value (self , value : P[int ]) -> None : ...
3971+
3972+ static_assert(is_subtype_of(Implementation, HasValue))
3973+ ```
3974+
3975+ ### Setter value types with permuted protocol arguments
3976+
3977+ Recursion through different specializations remains independent of the setter's type parameter.
3978+ ` Swapped[int, str] ` alternates with ` Swapped[str, int] ` , and a setter accepting that type satisfies
3979+ the writable member:
3980+
3981+ ``` toml
3982+ [environment ]
3983+ python-version = " 3.12"
3984+ ```
3985+
3986+ ``` py
3987+ from __future__ import annotations
3988+
3989+ from typing import Protocol
3990+ from ty_extensions import static_assert
3991+ from ty_extensions._internal import is_subtype_of
3992+
3993+ class Swapped[A, B](Protocol):
3994+ def value (self ) -> A: ...
3995+ def next (self ) -> Swapped[B, A]: ...
3996+
3997+ class Descriptor :
3998+ def __init__ (self , getter : object ) -> None : ...
3999+ def __get__ (self , instance : object , owner : type | None = None ) -> object :
4000+ return object ()
4001+
4002+ def __set__[U](self , instance: object , value: Swapped[int , str ]) -> None : ...
4003+
4004+ class HasValue (Protocol ):
4005+ @Descriptor
4006+ def value (self ) -> object : ...
4007+
4008+ class Implementation :
4009+ @ property
4010+ def value (self ) -> object :
4011+ return object ()
4012+
4013+ @value.setter
4014+ def value (self , value : Swapped[int , str ]) -> None : ...
4015+
4016+ static_assert(is_subtype_of(Implementation, HasValue))
4017+ ```
4018+
4019+ The second argument is used after following the recursive reference. The setter's ` U: int ` bound
4020+ therefore restricts that argument:
4021+
4022+ ``` py
4023+ class GenericDescriptor :
4024+ def __init__ (self , getter : object ) -> None : ...
4025+ def __get__ (self , instance : object , owner : type | None = None ) -> object :
4026+ return object ()
4027+
4028+ def __set__[U: int ](self , instance: object , value: Swapped[str , U]) -> None : ...
4029+
4030+ class HasGenericValue (Protocol ):
4031+ @GenericDescriptor
4032+ def value (self ) -> object : ...
4033+
4034+ def update (value : HasGenericValue, accepted : Swapped[str , int ], rejected : Swapped[str , str ]) -> None :
4035+ value.value = accepted # no diagnostic
4036+ value.value = rejected # error: [invalid-assignment]
4037+ ```
4038+
4039+ ### Setter value types with nested recursive arguments
4040+
4041+ Each ` next ` member nests another ` list ` in the protocol's type argument. This recursion does not
4042+ depend on ` __set__ ` 's type parameter, so assignments are checked against ` Recursive[int] ` :
4043+
4044+ ``` toml
4045+ [environment ]
4046+ python-version = " 3.12"
4047+ ```
4048+
4049+ ``` py
4050+ from __future__ import annotations
4051+
4052+ from typing import Protocol
4053+ from ty_extensions import static_assert
4054+ from ty_extensions._internal import is_subtype_of
4055+
4056+ class Recursive[T](Protocol):
4057+ next : Recursive[list[T]]
4058+
4059+ class Descriptor :
4060+ def __init__ (self , getter : object ) -> None : ...
4061+ def __get__ (self , instance : object , owner : type | None = None ) -> int :
4062+ return 1
4063+
4064+ def __set__[U](self , instance: object , value: Recursive[int ]) -> None : ...
4065+
4066+ class HasValue (Protocol ):
4067+ @Descriptor
4068+ def value (self ) -> int : ...
4069+
4070+ def _ (x : HasValue, value : Recursive[int ]) -> None :
4071+ x.value = value # no diagnostic
4072+ x.value = 1 # error: [invalid-assignment]
4073+ ```
4074+
4075+ The same property setter satisfies the protocol, while an ` int ` attribute cannot accept
4076+ ` Recursive[int] ` values:
4077+
4078+ ``` py
4079+ class Implementation :
4080+ @ property
4081+ def value (self ) -> int :
4082+ return 1
4083+
4084+ @value.setter
4085+ def value (self , value : Recursive[int ]) -> None : ...
4086+
4087+ static_assert(is_subtype_of(Implementation, HasValue))
4088+
4089+ class IntAttribute :
4090+ value: int
4091+
4092+ x: HasValue = IntAttribute() # error: [invalid-assignment]
4093+ ```
4094+
4095+ ### Recursive ` TypedDict ` setter value types
4096+
4097+ A recursive ` TypedDict ` can also be independent of the setter's type parameter. Assignments are
4098+ checked against ` Payload[int] ` :
4099+
4100+ ``` toml
4101+ [environment ]
4102+ python-version = " 3.12"
4103+ ```
4104+
4105+ ``` py
4106+ from __future__ import annotations
4107+
4108+ from typing import Protocol, TypedDict
4109+
4110+ class Payload[T](TypedDict):
4111+ child: Payload[list[T]]
4112+
4113+ class PayloadDescriptor :
4114+ def __init__ (self , getter : object ) -> None : ...
4115+ def __get__ (self , instance : object , owner : type | None = None ) -> int :
4116+ return 1
4117+
4118+ def __set__[U](self , instance: object , value: Payload[int ]) -> None : ...
4119+
4120+ class HasPayload (Protocol ):
4121+ @PayloadDescriptor
4122+ def value (self ) -> int : ...
4123+
4124+ def _ (x : HasPayload, value : Payload[int ]) -> None :
4125+ x.value = value # no diagnostic
4126+ x.value = 1 # error: [invalid-assignment]
4127+ ```
4128+
4129+ ### Recursive alias setter value types
4130+
4131+ A recursive alias independent of the setter's type parameter behaves the same way:
4132+
4133+ ``` toml
4134+ [environment ]
4135+ python-version = " 3.12"
4136+ ```
4137+
4138+ ``` py
4139+ from typing import Protocol
4140+
4141+ type RecursiveAlias[T] = list[RecursiveAlias[list[T]]]
4142+
4143+ class AliasDescriptor :
4144+ def __init__ (self , getter : object ) -> None : ...
4145+ def __get__ (self , instance : object , owner : type | None = None ) -> int :
4146+ return 1
4147+
4148+ def __set__[U](self , instance: object , value: RecursiveAlias[int ]) -> None : ...
4149+
4150+ class HasAlias (Protocol ):
4151+ @AliasDescriptor
4152+ def value (self ) -> int : ...
4153+
4154+ def _ (x : HasAlias, value : RecursiveAlias[int ]) -> None :
4155+ x.value = value # no diagnostic
4156+ x.value = 1 # error: [invalid-assignment]
4157+ ```
4158+
4159+ ### Recursive aliases in generic setters
4160+
4161+ A generic setter still rejects values incompatible with the outer type of a recursive alias.
4162+ ` Recursive[T] ` always denotes a ` list ` , so an ` int ` assignment is invalid:
4163+
4164+ ``` toml
4165+ [environment ]
4166+ python-version = " 3.12"
4167+ ```
4168+
4169+ ``` py
4170+ from typing import Protocol
4171+
4172+ type Recursive[T] = list[Recursive[list[T]]]
4173+
4174+ class Descriptor :
4175+ def __init__ (self , getter : object ) -> None : ...
4176+ def __get__ (self , instance : object , owner : type | None = None ) -> int :
4177+ return 1
4178+
4179+ def __set__[T](self , instance: object , value: Recursive[T]) -> None : ...
4180+
4181+ class HasValue (Protocol ):
4182+ @Descriptor
4183+ def value (self ) -> int : ...
4184+
4185+ def _ (x : HasValue) -> None :
4186+ x.value = 1 # error: [invalid-assignment]
4187+ ```
4188+
38364189### Type variables from the surrounding function
38374190
38384191A type variable supplied by the surrounding function is still the descriptor's value type. Assigning
@@ -3910,6 +4263,37 @@ def update_aliased_receiver(value: HasAliasedReceiver) -> None:
39104263 value.value = " bad" # error: [invalid-assignment]
39114264```
39124265
4266+ ### Unused alias arguments do not constrain setters
4267+
4268+ ` Ignored[T] ` always denotes ` int ` , so the setter accepts ` int ` independently of its type parameter
4269+ and rejects ` str ` .
4270+
4271+ ``` toml
4272+ [environment ]
4273+ python-version = " 3.12"
4274+ ```
4275+
4276+ ``` py
4277+ from typing import Protocol
4278+
4279+ type Ignored[T] = int
4280+
4281+ class Descriptor :
4282+ def __init__ (self , getter : object ) -> None : ...
4283+ def __get__ (self , instance : object , owner : type | None = None ) -> int :
4284+ return 1
4285+
4286+ def __set__[T](self , instance: object , value: Ignored[T]) -> None : ...
4287+
4288+ class HasValue (Protocol ):
4289+ @Descriptor
4290+ def value (self ) -> int : ...
4291+
4292+ def _ (x : HasValue) -> None :
4293+ x.value = 1 # no diagnostic
4294+ x.value = " bad" # error: [invalid-assignment]
4295+ ```
4296+
39134297### Constrained generic setters
39144298
39154299A setter with a constrained type variable must choose one constraint for each call. It accepts ` int `
0 commit comments