Pyrefly incorrectly rejects Protocol with descriptor attributes
- Dominant language
- Rust
- Stars
- 7k
- Forks
- 516
- PR merge metrics
- No merged PRs in 30d
Description
## Description
Pyrefly rejects classes with descriptor attributes when used with Protocol bounds, even when the descriptor types match exactly. This affects common patterns like SQLAlchemy ORM models where columns are defined using `Mapped[T]` descriptors.
## Minimal Reproducible Example
```python
from typing import TypeVar, Protocol, Generic, overload
T = TypeVar('T')
class Mapped(Generic[T]):
@overload
def __get__(self, obj: None, objtype: type) -> 'Mapped[T]': ...
@overload
def __get__(self, obj: object, objtype: type) -> T: ...
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, f'_{self._name}')
def __set__(self, obj, value: T) -> None:
setattr(obj, f'_{self._name}', value)
def __set_name__(self, owner, name):
self._name = name
class HasID(Protocol):
@property
def id(self) -> int: ...
def __init__(self, id: int) -> None: ...
TModel = TypeVar('TModel', bound=HasID)
class User:
id: Mapped[int] = Mapped[int]()
def __init__(self, id: int):
self.id = id
def get_by_id(cls: type[TModel], id_value: int) -> TModel:
return cls(id_value)
user = get_by_id(User, 42)
print(user.id)
```
[Sandbox Link](https://pyrefly.org/sandbox/?project=N4IgZglgNgpgziAXKOBDAdgEwEYHsAeAdAA4CeS4ATrgLYAEALqcROgOZ0Q3G6UN0AVZjABqqSgBo6ABWoNcAY1xQpAcRjoYlCAqm4Ablqi5UmADroLFgXQC8g4WMoAKAOQDXASiuX0CqKhwcHQAsqjExDCYzuqa2goA2gIAup6IFnSZdAACBkYm5uhZdJgwYHQA%2BhVsMAxVznAwUGB62ABWiHQAcriarW1MkZ2DMJ50ALQAfHSuYRFRScmunYSrGVm5hpTGpuuZpeVVNXUVDU0tdLjtnVdtMAoM-SPDwmNTgitrRVkHldW19UazX6T2Eth6mjSe2KnHKt04wQhMHS3xhMMotQArpQikCwNDihiGNiisdUAwGC5blIwK4KsA8YQKuhUDQYABfLzQ6G-KqNE5nYGXdpSfSoKCY5GCN7TJEotGZfnkynOal0Wn0xnM1kc1yi8WS7youg8sp-fnatmA856ADucSkLLZUONxS1TpgdjoHp8Fn8gWCAAlAgBJAAizlkuHkSigLuK2WI1EifFIpvKEGieJlnHQDE%2BhG5xt5FVYEAFeKkmc6rAYObldFWhd81hCuFKUC9QkiTjcAjbHb1dDwmKwtmDcHDRp8-qCdAAqo1KPKstXQuFIpgErXkl65pvt3nks4jcV038yxWbZxMDW8-GFYzM17M76sGbjhVsKRS9F-HAXkiJIByaZIq0wCoxQlKVaxzft2yaFdMiJEk6H-ZxM0gg1RjfTEly9T9v1-ZxFy0KQABYACYjSTWtnDwrRCEzTwQAkEBMQYaA4BIchEBAABiBdOKgctSHVUcHggXo4B8X4wF4GhyWZTEaGwLRnHwO86wmaY4EpJC6BQnF1TMEAuhUtTlzoYB8HZUyLFYkAyAxMAoFIQh5BoKAKEE6RSBcty6DQLA8HwNDekgNhsXJKT0GbQSAGUYE9AALCliAAgB6TLnLKNzCF4NhMo0TLMEUOBMqUdBIuizjeky9VeDoVAxWgVBsFgcLqogKLKBi3pLmIOr0G4iwyAYFLenGLY4Fir1TIAZkIABGSj7PQEB2TY1BJMMAAxaAYAoYKcAIHjNqAA)
## Expected Behavior
A class with `id: Mapped[int]` should be assignable to a Protocol that also defines `id: Mapped[int]`, since the types match structurally at the class level.
Mypy accepts this pattern without issues.
## Actual Behavior
Pyrefly produces the error:
```
ERROR sandbox.py:45:17-27: `User` is not assignable to upper bound `HasID` of type variable `TModel` [[bad-specialization](https://pyrefly.org/en/docs/error-kinds/#bad-specialization)]
```
## Additional Context
This pattern is commonly used with SQLAlchemy 2.0+ where ORM models use `Mapped[T]` descriptors:
```python
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase
class HasID(Protocol):
id: Mapped[int]
class User(DeclarativeBase):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
```
Contributor guide
Assessment
This issue has not been assessed yet.