Skip to content

Commit

Permalink
Update mypy pin and fix related issues (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
ilevkivskyi authored Nov 22, 2019
1 parent 6d07636 commit ea93332
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 25 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
mkdir tmpd
cp -r sqlalchemy-stubs tmpd/sqlalchemy
cd tmpd
python3 -m mypy --new-semantic-analyzer -p sqlalchemy
python3 -m mypy -p sqlalchemy
- name: "run flake8 on stubs"
python: 3.7
script: |
Expand Down
2 changes: 1 addition & 1 deletion external/mypy
Submodule mypy updated 446 files
9 changes: 8 additions & 1 deletion sqlalchemy-stubs/engine/result.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import sys

from typing import Any, List, Mapping, Optional, Iterator, Union, AbstractSet, Tuple
from ..sql.schema import Column

if sys.version_info >= (3, 0):
_RowItems = AbstractSet[Tuple[str, Any]]
else:
_RowItems = List[Tuple[str, Any]]

def rowproxy_reconstructor(cls, state): ...

class BaseRowProxy(Mapping[str, Any]):
Expand All @@ -22,7 +29,7 @@ class RowProxy(BaseRowProxy):
def __eq__(self, other): ...
def __ne__(self, other): ...
def has_key(self, key): ...
def items(self) -> AbstractSet[Tuple[str, Any]]: ...
def items(self) -> _RowItems: ...
def keys(self): ...
def iterkeys(self): ...
def itervalues(self): ...
Expand Down
62 changes: 43 additions & 19 deletions sqlmypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@
from mypy.plugins.common import add_method
from mypy.nodes import (
NameExpr, Expression, StrExpr, TypeInfo, ClassDef, Block, SymbolTable, SymbolTableNode, GDEF,
Argument, Var, ARG_STAR2, MDEF, TupleExpr, RefExpr
Argument, Var, ARG_STAR2, MDEF, TupleExpr, RefExpr, FuncBase, SymbolNode
)
from mypy.types import (
UnionType, NoneTyp, Instance, Type, AnyType, TypeOfAny, UninhabitedType, CallableType
)
from mypy.typevars import fill_typevars_with_any

from typing import Optional, Callable, Dict, List, TypeVar
try:
from mypy.types import get_proper_type
except ImportError:
get_proper_type = lambda x: x

from typing import Optional, Callable, Dict, List, TypeVar, Union

MYPY = False # we should support Python 3.5.1 and cases where typing_extensions is not available.
if MYPY:
Expand All @@ -29,6 +34,24 @@
RELATIONSHIP_NAME = 'sqlalchemy.orm.relationships.RelationshipProperty' # type: Final


# See https://github.com/python/mypy/issues/6617 for plugin API updates.

def fullname(x: Union[FuncBase, SymbolNode]) -> str:
"""Compatibility helper for mypy 0.750 vs older."""
fn = x.fullname
if callable(fn):
return fn()
return fn


def shortname(x: Union[FuncBase, SymbolNode]) -> str:
"""Compatibility helper for mypy 0.750 vs older."""
fn = x.name
if callable(fn):
return fn()
return fn


def is_declarative(info: TypeInfo) -> bool:
"""Check if this is a subclass of a declarative base."""
if info.mro:
Expand Down Expand Up @@ -92,7 +115,7 @@ def add_var_to_class(name: str, typ: Type, info: TypeInfo) -> None:
"""
var = Var(name)
var.info = info
var._fullname = info.fullname() + '.' + name
var._fullname = fullname(info) + '.' + name
var.type = typ
info.names[name] = SymbolTableNode(MDEF, var)

Expand Down Expand Up @@ -200,7 +223,7 @@ def model_hook(ctx: FunctionContext) -> Type:
Note: this is still not perfect, since the context for inference of
argument types is 'Any'.
"""
assert isinstance(ctx.default_return_type, Instance)
assert isinstance(ctx.default_return_type, Instance) # type: ignore[misc]
model = ctx.default_return_type.type
metadata = model.metadata.get('sqlalchemy')
if not metadata or not metadata.get('generated_init'):
Expand All @@ -211,11 +234,12 @@ def model_hook(ctx: FunctionContext) -> Type:
expected_types = {} # type: Dict[str, Type]
for cls in model.mro[::-1]:
for name, sym in cls.names.items():
if isinstance(sym.node, Var) and isinstance(sym.node.type, Instance):
tp = sym.node.type
if tp.type.fullname() in (COLUMN_NAME, RELATIONSHIP_NAME):
assert len(tp.args) == 1
expected_types[name] = tp.args[0]
if isinstance(sym.node, Var):
tp = get_proper_type(sym.node.type)
if isinstance(tp, Instance):
if fullname(tp.type) in (COLUMN_NAME, RELATIONSHIP_NAME):
assert len(tp.args) == 1
expected_types[name] = tp.args[0]

assert len(ctx.arg_names) == 1 # only **kwargs in generated __init__
assert len(ctx.arg_types) == 1
Expand All @@ -226,14 +250,14 @@ def model_hook(ctx: FunctionContext) -> Type:
continue
if actual_name not in expected_types:
ctx.api.fail('Unexpected column "{}" for model "{}"'.format(actual_name,
model.name()),
shortname(model)),
ctx.context)
continue
# Using private API to simplify life.
ctx.api.check_subtype(actual_type, expected_types[actual_name], # type: ignore
ctx.context,
'Incompatible type for "{}" of "{}"'.format(actual_name,
model.name()),
shortname(model)),
'got', 'expected')
return ctx.default_return_type

Expand Down Expand Up @@ -277,7 +301,7 @@ def column_hook(ctx: FunctionContext) -> Type:
TODO: check the type of 'default'.
"""
assert isinstance(ctx.default_return_type, Instance)
assert isinstance(ctx.default_return_type, Instance) # type: ignore[misc]

nullable_arg = get_argument_by_name(ctx, 'nullable')
primary_arg = get_argument_by_name(ctx, 'primary_key')
Expand Down Expand Up @@ -309,13 +333,13 @@ def grouping_hook(ctx: FunctionContext) -> Type:
Grouping(Column(String), nullable=False) -> Grouping[str]
Grouping(Column(String)) -> Grouping[Optional[str]]
"""
assert isinstance(ctx.default_return_type, Instance)
assert isinstance(ctx.default_return_type, Instance) # type: ignore[misc]

element_arg_type = get_argtype_by_name(ctx, 'element')
element_arg_type = get_proper_type(get_argtype_by_name(ctx, 'element'))

if element_arg_type is not None and isinstance(element_arg_type, Instance):
if element_arg_type.type.has_base(CLAUSE_ELEMENT_NAME) and not \
element_arg_type.type.has_base(COLUMN_ELEMENT_NAME):
if (element_arg_type.type.has_base(CLAUSE_ELEMENT_NAME) and not
element_arg_type.type.has_base(COLUMN_ELEMENT_NAME)):
return ctx.default_return_type.copy_modified(args=[NoneTyp()])
return ctx.default_return_type

Expand All @@ -339,12 +363,12 @@ class User(Base):
This also tries to infer the type argument for 'RelationshipProperty'
using the 'uselist' flag.
"""
assert isinstance(ctx.default_return_type, Instance)
assert isinstance(ctx.default_return_type, Instance) # type: ignore[misc]
original_type_arg = ctx.default_return_type.args[0]
has_annotation = not isinstance(original_type_arg, UninhabitedType)
has_annotation = not isinstance(get_proper_type(original_type_arg), UninhabitedType)

arg = get_argument_by_name(ctx, 'argument')
arg_type = get_argtype_by_name(ctx, 'argument')
arg_type = get_proper_type(get_argtype_by_name(ctx, 'argument'))

uselist_arg = get_argument_by_name(ctx, 'uselist')

Expand Down
25 changes: 23 additions & 2 deletions test/test-data/sqlalchemy-basics.test
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ from sqlalchemy import Column, Integer, String
from sqlalchemy.sql.type_api import TypeEngine

from typing import TypeVar, Optional
T = TypeVar('T', bound=int)
T = TypeVar('T', bound=Optional[int])

def func(tp: TypeEngine[Optional[T]]) -> T: ...
def func(tp: TypeEngine[T]) -> T: ...
reveal_type(func(Integer())) # N: Revealed type is 'builtins.int*'
func(String()) # E: Value of type variable "T" of "func" cannot be "str"
[out]
Expand Down Expand Up @@ -143,3 +143,24 @@ class Other(Base):
other: Other
session.query(User).filter(User.other == other)
[out]

[case testColumnFieldsDeclaredAliases]
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String

Base = declarative_base()

CI = Column[int]
CS = Column[str]

class User(Base):
id: CI
name: CS

user: User
reveal_type(user.id) # N: Revealed type is 'builtins.int*'
reveal_type(User.name) # N: Revealed type is 'sqlalchemy.sql.schema.Column[builtins.str*]'
User(id=1)
User(id='no') # E: Incompatible type for "id" of "User" (got "str", expected "int")
User(undefined=0) # E: Unexpected column "undefined" for model "User"
[out]
4 changes: 3 additions & 1 deletion test/test-data/sqlalchemy-sql-schema.test
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ user_preference = Table('user_preference', metadata,
[out]

[case testColumnWithForeignKeyDeclarative]
from typing import Optional

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, ForeignKey
Base = declarative_base()

class Mytable(Base):
__tablename__ = 'mytable'
objid = Column(ForeignKey('othertable.objid'), index=True)
objid: Column[Optional[int]] = Column(ForeignKey('othertable.objid'), index=True)
[out]

[case testTableWithIndexes]
Expand Down
1 change: 1 addition & 0 deletions test/testsql.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
mypy_cmdline = [
'--show-traceback',
'--no-silence-site-packages',
'--no-error-summary',
'--config-file={}/sqlalchemy.ini'.format(inipath),
]
py2 = testcase.name.lower().endswith('python2')
Expand Down

0 comments on commit ea93332

Please sign in to comment.