Skip to content

Commit d05a736

Browse files
authored
Make TypeEngine and RelationshipProperty covariant (#45)
1 parent 17e5cd5 commit d05a736

File tree

3 files changed

+24
-5
lines changed

3 files changed

+24
-5
lines changed

sqlalchemy-stubs/orm/relationships.pyi

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ def foreign(expr): ...
1212

1313

1414
_T = TypeVar('_T')
15+
_T_co = TypeVar('_T_co', covariant=True)
16+
1517

1618
# Note: typical use case is where argument is a string, so this will require
1719
# a plugin to infer '_T', otherwise a user will need to write an explicit annotation.
18-
class RelationshipProperty(StrategizedProperty, Generic[_T]):
20+
# It is not clear whether RelationshipProperty is covariant at this stage since
21+
# many types are still missing.
22+
class RelationshipProperty(StrategizedProperty, Generic[_T_co]):
1923
strategy_wildcard_key: str = ...
2024
uselist: Any = ...
2125
argument: Any = ...
@@ -93,9 +97,9 @@ class RelationshipProperty(StrategizedProperty, Generic[_T]):
9397
def do_init(self): ...
9498
# This doesn't exist at runtime, but is present here for better typing.
9599
@overload
96-
def __get__(self, instance: None, owner: Any) -> RelationshipProperty[_T]: ...
100+
def __get__(self, instance: None, owner: Any) -> RelationshipProperty[_T_co]: ...
97101
@overload
98-
def __get__(self, instance: object, owner: Any) -> _T: ...
102+
def __get__(self, instance: object, owner: Any) -> _T_co: ...
99103

100104
class JoinCondition(object):
101105
parent_selectable: Any = ...

sqlalchemy-stubs/sql/type_api.pyi

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ from . import operators
66
from .sqltypes import NullType, Integer, Boolean, String, MatchType, Indexable
77

88
_T = TypeVar('_T')
9+
_T_co = TypeVar('_T_co', covariant=True)
910

1011
# TODO: simplify import cycle with sqltypes
1112
BOOLEANTYPE: Boolean = ...
@@ -15,7 +16,9 @@ STRINGTYPE: String = ...
1516
MATCHTYPE: MatchType = ...
1617
INDEXABLE: Indexable = ...
1718

18-
class TypeEngine(Visitable, Generic[_T]):
19+
# It is not 100% clear that TypeEngine is covariant at this stage since
20+
# many types are still missing. Anyway, this looks plausible and practical.
21+
class TypeEngine(Visitable, Generic[_T_co]):
1922
class Comparator(operators.ColumnOperators):
2023
default_comparator: Any = ...
2124
expr: Any = ...
@@ -39,7 +42,7 @@ class TypeEngine(Visitable, Generic[_T]):
3942
def compare_values(self, x, y): ...
4043
def get_dbapi_type(self, dbapi): ...
4144
@property
42-
def python_type(self) -> Type[_T]: ...
45+
def python_type(self) -> Type[_T_co]: ...
4346
def with_variant(self, type_, dialect_name): ...
4447
def dialect_impl(self, dialect): ...
4548
def adapt(self, *args, **kw): ...

test/test-data/sqlalchemy-basics.test

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ reveal_type(user.id) # E: Revealed type is 'builtins.int*'
1616
reveal_type(User.name) # E: Revealed type is 'sqlalchemy.sql.schema.Column[builtins.str*]'
1717
[out]
1818

19+
[case testTypeEngineCovariance]
20+
from sqlalchemy import Column, Integer, String
21+
from sqlalchemy.sql.type_api import TypeEngine
22+
23+
from typing import TypeVar, Optional
24+
T = TypeVar('T', bound=int)
25+
26+
def func(tp: TypeEngine[Optional[T]]) -> T: ...
27+
reveal_type(func(Integer())) # E: Revealed type is 'builtins.int*'
28+
func(String()) # E: Value of type variable "T" of "func" cannot be "str"
29+
[out]
30+
1931
[case testColumnFieldsInferredInstance]
2032
from typing import Any
2133

0 commit comments

Comments
 (0)