Skip to content

hybrid_property support #98

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

Open
ckarnell opened this issue Aug 20, 2019 · 7 comments
Open

hybrid_property support #98

ckarnell opened this issue Aug 20, 2019 · 7 comments

Comments

@ckarnell
Copy link
Contributor

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.

@ckarnell
Copy link
Contributor Author

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_propertys 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.

@jace
Copy link

jace commented Jan 19, 2021

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]

@mrcljx
Copy link

mrcljx commented Jan 19, 2021

@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

@jace
Copy link

jace commented Jan 20, 2021

Thank you. This does indeed work. The .expression and .comparator decorators also work without issue.

@zzzeek
Copy link

zzzeek commented Feb 19, 2021

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.

@zzzeek
Copy link

zzzeek commented Feb 19, 2021

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

@zzzeek
Copy link

zzzeek commented Feb 19, 2021

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants