Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ Mypy plugin and stubs for SQLAlchemy
[![Build Status](https://travis-ci.org/dropbox/sqlalchemy-stubs.svg?branch=master)](https://travis-ci.org/dropbox/sqlalchemy-stubs)
[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)

This package contains [type stubs](https://www.python.org/dev/peps/pep-0561/) and soon a
mypy plugin to provide more precise static types
and type inference for [SQLAlchemy framework](http://docs.sqlalchemy.org/en/latest/).
SQLAlchemy uses some Python "magic" that
makes having precise types for some code patterns problematic. This is why we need to
accompany the stubs with mypy plugins. The final goal is to be able to get precise types
for most common patterns. A simple example:

This package contains [type stubs](https://www.python.org/dev/peps/pep-0561/) and a
[mypy plugin](https://mypy.readthedocs.io/en/latest/extending_mypy.html#extending-mypy-using-plugins)
to provide more precise static types and type inference for
[SQLAlchemy framework](http://docs.sqlalchemy.org/en/latest/). SQLAlchemy uses some
Python "magic" that makes having precise types for some code patterns problematic.
This is why we need to accompany the stubs with mypy plugins. The final goal is to
be able to get precise types for most common patterns. Currently, basic operations
with models are supported. A simple example:
```python
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
Expand All @@ -25,11 +25,33 @@ class User(Base):
id = Column(Integer, primary_key=True)
name = Column(String)

user: User
user = User(id=42, name=42) # Error: Incompatible type for "name" of "User"
# (got "int", expected "Optional[str]"
user.id # Inferred type is "int"
User.name # Inferred type is "Column[str]"
User.name # Inferred type is "Column[Optional[str]]"
```

Some auto-generated attributes are added to models. Simple relationships
are supported but require models to be imported:
```python
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from models.address import Address

...

class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
address = relationship('Address') # OK, mypy understands string references.
```

The next step is to support precise types for table definitions (e.g.
inferring `Column[Optional[str]]` for `users.c.name`, currently it is just
`Column[Any]`), and precise types for results of queries made using `query()`
and `select()`.

## Installation

To install the latest version of the package:
Expand All @@ -45,6 +67,12 @@ stable version as:
pip install -U sqlalchemy-stubs
```

*Important*: you need to enable the plugin in your mypy config file:
```
[mypy]
plugins = sqlmypy
```

## Development Setup

First, clone the repo and cd into it, like in _Installation_, then:
Expand Down
9 changes: 5 additions & 4 deletions sqlmypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ def add_model_init_hook(ctx: ClassDefContext) -> None:
if '__init__' in ctx.cls.info.names:
# Don't override existing definition.
return
typ = AnyType(TypeOfAny.special_form)
var = Var('kwargs', typ)
kw_arg = Argument(variable=var, type_annotation=typ, initializer=None, kind=ARG_STAR2)
any = AnyType(TypeOfAny.special_form)
var = Var('kwargs', any)
kw_arg = Argument(variable=var, type_annotation=any, initializer=None, kind=ARG_STAR2)
add_method(ctx, '__init__', [kw_arg], NoneTyp())
ctx.cls.info.metadata.setdefault('sqlalchemy', {})['generated_init'] = True

Expand Down Expand Up @@ -300,7 +300,8 @@ class User(Base):
new_arg = fill_typevars_with_any(sym.node)
else:
ctx.api.fail('Cannot find model "{}"'.format(name), ctx.context)
ctx.api.note('Only imported models can be found; use "if TYPE_CHECKING: ..." to avoid import cycles', ctx.context)
ctx.api.note('Only imported models can be found; use "if TYPE_CHECKING: ..." to avoid import cycles',
ctx.context)
new_arg = AnyType(TypeOfAny.from_error)
else:
if isinstance(arg_type, CallableType) and arg_type.is_type_obj():
Expand Down