sqlalchemy-stubs
sqlalchemy-stubs copied to clipboard
hybrid_property support
Hi, I saw the thread a this topic here, but can't haven't seen any TODOs in this repo for it, so I'll go ahead and make one.
We need support to infer types for hybrid_property
similar to property
, returning different types based on whether it's being accessed on a class or an instance, and support for the @<property>.expression
decorator.
If anyone's reading this and is curious if there's a quick way to get an easy (but janky) fix here, you can do something like this to basically sub in mypy's type checking for property
for your hybrid_property
s by using a third variable called typed_hybrid_property
who's type changes depending on whether it's runtime.
if TYPE_CHECKING:
# Use this to make hybrid_property's have the same typing as a normal property until stubs are improved.
typed_hybrid_property = property
else:
from sqlalchemy.ext.hybrid import hybrid_property as typed_hybrid_property
This also allows mypy to type expression functions, which is nice.
I tried keeping the name and mypy doesn't recognise it as an alias for property:
if TYPE_CHECKING:
hybrid_property = property # type: ignore[misc,assignment]
Maybe it's because of the errors raised:
error: Cannot assign to a type [misc]
error: Incompatible types in assignment (expression has type "Type[property]", variable has type "Type[hybrid_property]") [assignment]
@jace Make sure that you're not re-assigning hybrid_property
(which is what the error message points out).
The following should work:
if TYPE_CHECKING:
hybrid_property = property
else:
from sqlalchemy.ext.hybrid import hybrid_property
# hybrid_property will be Type[hybrid_property] during type checking
# but refer to sqlalchemy.ext.hybrid.hybrid_property at runtime
The following won't:
from sqlalchemy.ext.hybrid import hybrid_property
if TYPE_CHECKING:
hybrid_property = property # reassignment -> conflict
Thank you. This does indeed work. The .expression
and .comparator
decorators also work without issue.
im playing with descriptors today and shouldn't this be the general approach for a descriptor?
from typing import Any, Union, overload
class Descriptor:
def some_method(self) -> Any:
pass
@overload
def __get__(self, instance: None, other: Any) -> "Descriptor": ...
@overload
def __get__(self, instance: object, other: Any) -> "int": ...
def __get__(self, instance: object, other: Any) -> "Any":
if instance is None:
return self
else:
return 5
class Foo:
value = Descriptor()
# class level access, you get "Descriptor"
# (for hybrid this would be ColumnElement)
Foo.value.some_method()
f1 = Foo()
# instance level, you get a value type
val : int = f1.value
that is, the hybrid_property separation of "expression" and "instance" is made apparent by the type of "instance" passed to the descriptor protocol.
can someone comment on this approach? considering this is what I would try to adapt to hybrid properties, which are just descriptors with pluggable class/instance level functions.
Here's POC 1 for this approach with hybrids:
from typing import Any
from typing import Callable
from typing import Generic
from typing import Optional
from typing import overload
from typing import Type
from typing import TypeVar
from typing import Union
from sqlalchemy import column
from sqlalchemy import Integer
from sqlalchemy.sql import ColumnElement
_T = TypeVar("_T")
class hybrid_property(Generic[_T]):
def __init__(
self,
fget: Callable[[Any], _T],
expr: Callable[[Any], ColumnElement[_T]],
):
self.fget = fget
self.expr = expr
@overload
def __get__(
self, instance: None, owner: Optional[Type[Any]]
) -> "ColumnElement[_T]":
...
@overload
def __get__(self, instance: object, owner: Optional[Type[Any]]) -> _T:
...
def __get__(
self, instance: Union[object, None], owner: Optional[Type[Any]] = None
) -> Any:
if instance is None:
return self.expr(owner)
else:
return self.fget(instance)
def expression(
self, expr: "Callable[[Any], ColumnElement[_T]]"
) -> "hybrid_property[_T]":
return hybrid_property(self.fget, expr)
class MyClass:
def my_thing_inst(self) -> int:
return 5
def my_thing_expr(cls) -> "ColumnElement[int]":
return column("five", Integer)
my_thing = hybrid_property(my_thing_inst, my_thing_expr)
mc = MyClass()
int_value: int = mc.my_thing
expr: ColumnElement[int] = MyClass.my_thing
Here we go, this is just about the whole thing, how about this
from typing import Any
from typing import Callable
from typing import Generic
from typing import Optional
from typing import overload
from typing import Type
from typing import TypeVar
from typing import Union
from sqlalchemy import column
from sqlalchemy import Integer
from sqlalchemy.sql import ColumnElement
_T = TypeVar("_T")
class hybrid_property(Generic[_T]):
def __init__(
self,
fget: Callable[[Any], Union[_T, ColumnElement[_T]]],
expr: Optional[Callable[[Any], ColumnElement[_T]]] = None,
):
self.fget = fget
if expr is None:
self.expr = fget
else:
self.expr = expr
@overload
def __get__(
self, instance: None, owner: Optional[Type[Any]]
) -> "ColumnElement[_T]":
...
@overload
def __get__(self, instance: object, owner: Optional[Type[Any]]) -> _T:
...
def __get__(
self, instance: Union[object, None], owner: Optional[Type[Any]] = None
) -> Any:
if instance is None:
return self.expr(owner)
else:
return self.fget(instance)
def expression(
self, expr: "Callable[[Any], ColumnElement[_T]]"
) -> "hybrid_property[_T]":
return hybrid_property(self.fget, expr)
class MyClass:
# seems like "use the name twice" pattern isn't accepted by
# mypy, so use two separate names?
@hybrid_property
def _my_thing_inst(self) -> int:
return 5
@_my_thing_inst.expression
def my_thing(cls) -> "ColumnElement[int]":
return column("five", Integer)
mc = MyClass()
int_value: int = mc.my_thing
expr: ColumnElement[int] = MyClass.my_thing