diff --git a/README.md b/README.md index d8e17b6..af6e488 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: @@ -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: diff --git a/sqlmypy.py b/sqlmypy.py index 8dbae68..eeb4fa1 100644 --- a/sqlmypy.py +++ b/sqlmypy.py @@ -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 @@ -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():