allow non-descriptor subtypes
Description
consider the following example, where we want to define a Protocol for any object that has system: str and a code: str attributes:
from typing import Protocol, runtime_checkable
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass
class Base(MappedAsDataclass, DeclarativeBase):
pass
@runtime_checkable
class ConceptProto(Protocol):
@property
def system(self) -> str: ...
@property
def code(self) -> str: ...
class Concept(Base):
__tablename__ = "concept"
system: Mapped[str]
code: Mapped[str]
def fun(c: ConceptProto):
print(c.system, c.code)
fun(Concept(system="SNOMED", code="123456789"))
this currently fails with basedpyright:
#########################/proto.py
#########################/proto.py:30:5 - error: Argument of type "Concept" cannot be assigned to parameter "c" of type "ConceptProto" in function "fun"
"Concept" is incompatible with protocol "ConceptProto"
"system" is an incompatible type
"Mapped[str]" is not assignable to "str"
"code" is an incompatible type
"Mapped[str]" is not assignable to "str" (reportArgumentType)
1 error, 0 warnings, 0 notes
it would be nice if sqlalchemy models could be used within that context, and manipulated by duck-typed libs without ever having to import sqlalchemy
you can see the initial discussion with the maintainer of sqlalchemy here: https://github.com/sqlalchemy/sqlalchemy/discussions/12889
i think the issue here is that a regular attribute should be considered a valid subtype of an attribute that uses descriptors as long as it's compatible with the descriptor type:
from typing import Protocol
class Foo[T]:
def __get__(self, instance: object, owner: type[object]) -> T: ...
def __set__(self, instance: object, value: T): ...
class Bar(Protocol):
a: int
class Baz:
a: Foo[int] = Foo()
baz = Baz()
baz.a = 2
foo: Bar = baz # error: "Foo[int]" is not assignable to "int"
would have to think about this some more but at first glance it seems safe to not report an error here
@DetachHead thanks for your always-amazing reactivity and detailed answer
Do you want me to open an issue upstream?
you can if you want. i have a feeling it will be rejected though
Just hit a similar problem.
from __future__ import annotations
from typing import Protocol
# PROTOCOL
class ServiceProto(Protocol):
@property
def value(self) -> int: ...
# DESCRIPTOR
class Descriptor:
def __get__(self, instance: ServiceImpl, owner: type[ServiceImpl]) -> int:
return 5
# IMPLEMENTATION
class ServiceImpl:
value = Descriptor()
class App[ServiceT: ServiceProto]:
def __init__(self, service: ServiceT) -> None:
self.service = service
def bootstrap() -> App[ServiceImpl]:
return App(ServiceImpl())
Both lines of the bootstrap function are flagged by basedpyright (Descriptor is not assignable to int). It seems like it may be the same issue, but maybe not. Let me know if I need to open a different issue.
For reference, mypy handles this somewhat inconsistently (as of 1.18): if the bootstrap function is in the same file no errors, but if in a different file, mypy fails too (https://github.com/python/mypy/issues/20317).