Mypy / Pep-484 Support for ORM Mappings¶
SQLAlchemy 2.0で導入された mapped_column()
構文ではなく、 Column
オブジェクトを直接参照するSQLAlchemy declarative マッピングを使用する場合の PEP 484 型付け注釈とMyPy_type checkingツールのサポート。
Deprecated since version 2.0:
SQLAlchemy Mypyプラグインは非推奨であり、早ければSQLAlchemy 2.1リリースで削除される予定です。ユーザーには、できるだけ早く移行するようお願いします。mypyプラグインもmypyバージョン1. 10.1までしか動作しません。バージョン1. 11.0以降は正しく動作しない可能性があります。
このプラグインは、mypyの継続的に変化するリリースにわたって維持することはできず、今後の安定性は保証できません。
Modern SQLAlchemyがs fully pep-484 compliant mapping syntaxes を提供するようになりました。移行の詳細については、リンクされたセクションを参照してください。
Installation¶
SQ LC alchemy 2.0のみ : スタブはインストールしないでください。SQLAlchemy-stubs_やsqlalchemy2-stubs_のようなパッケージは完全にアンインストールしてください。
Mypy_package自体は依存関係です。
Mypyはpipを使って”mypy”extrasフックを使ってインストールできます。:
pip install sqlalchemy[mypy]
プラグイン自体は Configuring mypy to use Plugins で説明されているように設定されています。モジュール名は sqlalchemy.ext.mypy.plugin
で、例えば setup.cfg
の中にあります:
[mypy]
plugins = sqlalchemy.ext.mypy.plugin
What the Plugin Does¶
Mypyプラグインの主な目的は、SQLAlchemy declarative mappings の静的な定義をインターセプトして変更し、 Mapper
オブジェクトによって instrumented された後の構造に一致させることです。これにより、クラス構造自体と、クラスを使用するコードの両方がMypyツールに対して意味を持つようになります。これは、宣言型マッピングが現在どのように機能しているかに基づいて、そうでなければそうではありません。このプラグインは、実行時にクラスを動的に変更する dataclasses のようなライブラリに必要な同様のプラグインとは異なりません。
これが起こる主な領域をカバーするために、 User
クラスを使用した以下のORMマッピングを考えてみましょう。:
from sqlalchemy import Column, Integer, String, select
from sqlalchemy.orm import declarative_base
# "Base" is a class that is created dynamically from the
# declarative_base() function
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
# "some_user" is an instance of the User class, which
# accepts "id" and "name" kwargs based on the mapping
some_user = User(id=5, name="user")
# it has an attribute called .name that's a string
print(f"Username: {some_user.name}")
# a select() construct makes use of SQL expressions derived from the
# User class itself
select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s"))
上記では、Mypyエクステンションが実行できるステップは次のとおりです。:
declarative_base()
によって生成されたBase
動的クラスを解釈し、そこから継承するクラスがマップされていることを認識できるようにします。また、 Declarative Mapping using a Decorator (no declarative base) で説明されているクラスデコレータアプローチにも対応できます。
宣言的な
インライン
スタイルで定義されたORMマップ属性の型推論。上の例では、User
クラスのid
属性とname
属性です。これには、User
のインスタンスがid
にint
を、name
にstr
を使用することも含まれます。また、User.id
とUser.name
のクラスレベル属性がアクセスされた場合、これらは前述のselect()
文のように、InstrumentedAttribute
属性記述子クラスから派生したSQL式の動作と互換性があります。
Mypyプラグインが上記のファイルを処理すると、Mypyツールに渡された静的クラス定義とPythonコードは次のようになります。:
from sqlalchemy import Column, Integer, String, select
from sqlalchemy.orm import Mapped
from sqlalchemy.orm.decl_api import DeclarativeMeta
class Base(metaclass=DeclarativeMeta):
__abstract__ = True
class User(Base):
__tablename__ = "user"
id: Mapped[Optional[int]] = Mapped._special_method(
Column(Integer, primary_key=True)
)
name: Mapped[Optional[str]] = Mapped._special_method(Column(String))
def __init__(self, id: Optional[int] = ..., name: Optional[str] = ...) -> None: ...
some_user = User(id=5, name="user")
print(f"Username: {some_user.name}")
select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s"))
上記で実行された主な手順は次のとおりです。:
Base
クラスは、動的なクラスではなく、明示的にDeclarativeMeta
クラスで定義されるようになりました。
id
属性とname
属性は、Mapped
クラスで定義されます。これは、クラスレベルとインスタンスレベルで異なる動作を示すPython記述子を表します。Mapped
クラスは、すべてのORMマップ属性に使用されるInstrumentedAttribute
クラスの基本クラスになりました。
Mapped
は、任意のPython型に対するジェネリッククラスとして定義されています。つまり、 Mapped
の特定の出現は、上記の Mapped[Optional[int]]
や Mapped[Optional[str]]
のように、特定のPython型に関連付けられます。
宣言的にマップされた属性の割り当ての右側は 削除 されます。これは、
Mapper
クラスが通常行う操作に似ています。つまり、これらの属性をInstrumentedAttribute
の特定のインスタンスに置き換えます。元の式は関数呼び出しに移動され、式の左側と競合することなく型チェックを行うことができます。Mypyの目的では、属性の動作を理解するためには、左側の型指定アノテーションで十分です。
正しいキーワードとデータ型を含む
User.__init__()
メソッドの型スタブが追加されました。
Usage¶
次のサブセクションでは、これまでpep-484への準拠が検討されてきた個々のユースケースについて説明します。
Introspection of Columns based on TypeEngine¶
明示的なデータ型を含むマップされた列がインライン属性としてマップされると、マップされた型が自動的に調べられます。:
class MyClass(Base):
# ...
id = Column(Integer, primary_key=True)
name = Column("employee_name", String(50), nullable=False)
other_name = Column(String(50))
上記では、究極のクラスレベルのデータ型である id
、 name
、 other_name
は、それぞれ Mapped[Optional[int]]
、 Mapped[Optional[str]]
、 Mapped[Optional[str]]
として紹介されています。これらの型はデフォルトでは 常に Optional
とみなされます。これは、プライマリキーやNULLを許容しない列であっても同様です。その理由は、データベースの列 id
と name
はNULLにできませんが、Pythonの属性 id
と name
は明示的なコンストラクタがなければ None
にできるからです。`
>>> m1 = MyClass()
>>> m1.id
None
上記の列の型は 明示的に 記述することができ、より明確な自己文書化と、どの型がオプションであるかを制御できるという2つの利点を提供します:
class MyClass(Base):
# ...
id: int = Column(Integer, primary_key=True)
name: str = Column("employee_name", String(50), nullable=False)
other_name: Optional[str] = Column(String(50))
Mypyプラグインは、上記の int
、 str
、 Optional[str]
を受け入れ、それらを囲む Mapped[]
型を含むように変換します。 Mapped[]
構文も明示的に使用できます。:
from sqlalchemy.orm import Mapped
class MyClass(Base):
# ...
id: Mapped[int] = Column(Integer, primary_key=True)
name: Mapped[str] = Column("employee_name", String(50), nullable=False)
other_name: Mapped[Optional[str]] = Column(String(50))
型が非オプションの場合、それは単に MyClass
のインスタンスからアクセスされる属性が非Noneであるとみなされることを意味します:
mc = MyClass(...)
# will pass mypy --strict
name: str = mc.name
オプション属性の場合、Mypyは型にNoneが含まれているか、そうでなければ Optional
でなければならないと考えています。:
mc = MyClass(...)
# will pass mypy --strict
other_name: Optional[str] = mc.name
マップされた属性が Optional
としてタイプされているかどうかに関わらず、 __init__()
メソッドの生成では、 すべてのキーワードがオプションであるとみなされます 。これもまた、SQLAlchemy ORMがコンストラクタを作成するときに実際に行うことと一致します。Pythonの dataclasses
のような検証システムの動作と混同しないでください。このシステムは、オプション属性と必須属性の観点からアノテーションと一致するコンストラクタを生成します。
Columns that Don’t have an Explicit Type¶
ForeignKey
修飾子を含む列は、SQLAlchemy宣言マッピングでデータ型を指定する必要はありません。このタイプの属性の場合、Mypyプラグインは明示的なタイプを送信する必要があることをユーザに通知します:
# .. other imports
from sqlalchemy.sql.schema import ForeignKey
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id = Column(ForeignKey("user.id"))
プラグインは次のようにメッセージを配信します。:
$ mypy test3.py --strict
test3.py:20: error: [SQLAlchemy Mypy plugin] Can't infer type from
ORM mapped expression assigned to attribute 'user_id'; please specify a
Python type or Mapped[<python type>] on the left hand side.
Found 1 error in 1 file (checked 1 source file)
解決するには、明示的な型アノテーションを``Address.user_id`` 列に適用します。:
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))
Mapping Columns with Imperative Table¶
imperative table style では、 Column
の定義は、マップされた属性自体とは別の Table
構成体の内部で与えられます。Mypyプラグインはこの Table
を考慮しませんが、代わりに、属性をマップされた属性として識別するために Mapped
クラスを**使用しなければならない**という完全な注釈で、属性を明示的に記述できることをサポートしています。:
class MyClass(Base):
__table__ = Table(
"mytable",
Base.metadata,
Column(Integer, primary_key=True),
Column("employee_name", String(50), nullable=False),
Column(String(50)),
)
id: Mapped[int]
name: Mapped[str]
other_name: Mapped[Optional[str]]
上記の Mapped
アノテーションはマップされた列とみなされ、デフォルトのコンストラクタに含まれます。また、クラスレベルとインスタンスレベルの両方で、 MyClass
の正しい型定義を提供します。
Mapping Relationships
関係のマッピング¶
このプラグインでは、型推論を使用して関係の型を検出するサポートに制限があります。型を検出できない場合は、有益なエラーメッセージが表示されます。また、すべての場合において、適切な型が明示的に提供されます。 Mapped
クラスを使用するか、インライン宣言で省略することもできます。また、プラグインは、関係がコレクションまたはスカラーを参照しているかどうかを判断する必要があります。そのためには、 relationship.uselist
および/または relationship.collection_class
パラメータの明示的な値に依存します。これらのパラメータが存在しない場合、および relationship()
のターゲット型がクラスではなく文字列または呼び出し可能な場合は、明示的な型が必要です:
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))
user = relationship(User)
上記のマッピングでは、次のエラーが発生します。:
test3.py:22: error: [SQLAlchemy Mypy plugin] Can't infer scalar or
collection for ORM mapped expression assigned to attribute 'user'
if both 'uselist' and 'collection_class' arguments are absent from the
relationship(); please specify a type annotation on the left hand side.
Found 1 error in 1 file (checked 1 source file)
このエラーは、 relationship(User, uselist=False)
を使用するか、型(この場合はスカラーの User
オブジェクト)を指定することで解決できます。:
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))
user: User = relationship(User)
コレクションに対しても同様のパターンが適用されます。ここで、 uselist=True
または relationship.collection_class
がない場合、 List
のようなコレクションアノテーションを使用することができます。また、pep-484でサポートされているように、アノテーションでクラスの文字列名を使用することも十分に適切です。クラスは、必要に応じて TYPE_CHECKING block にインポートされます。:
from typing import TYPE_CHECKING, List
from .mymodel import Base
if TYPE_CHECKING:
# if the target of the relationship is in another module
# that cannot normally be imported at runtime
from .myaddressmodel import Address
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
addresses: List["Address"] = relationship("Address")
列の場合と同じく、 Mapped
クラスを明示的に適用することもできます:
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
addresses: Mapped[List["Address"]] = relationship("Address", back_populates="user")
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))
user: Mapped[User] = relationship(User, back_populates="addresses")
Using @declared_attr and Declarative Mixins¶
declared_attr
クラスを使用すると、宣言的にマップされた属性をクラスレベルの関数で宣言することができ、特に declarative mixins を使用する場合に便利です。これらの関数では、関数の戻り値の型は、Mapped[]
構文を使用するか、関数によって返されるオブジェクトの正確な種類を示すことによって注釈を付ける必要があります。さらに、他の方法でマップされていない(つまり、 declarative_base()
クラスから拡張されていない、または registry.mapped()
などのメソッドでマップされていない)”ミックスイン”クラスは、 declarative_mixin()
デコレータで装飾する必要があります。デコレータは、特定のクラスが宣言的ミックスインとして機能しようとしていることをMypyプラグインにヒントを与えます:
from sqlalchemy.orm import declarative_mixin, declared_attr
@declarative_mixin
class HasUpdatedAt:
@declared_attr
def updated_at(cls) -> Column[DateTime]: # uses Column
return Column(DateTime)
@declarative_mixin
class HasCompany:
@declared_attr
def company_id(cls) -> Mapped[int]: # uses Mapped
return mapped_column(ForeignKey("company.id"))
@declared_attr
def company(cls) -> Mapped["Company"]:
return relationship("Company")
class Employee(HasUpdatedAt, HasCompany, Base):
__tablename__ = "employee"
id = Column(Integer, primary_key=True)
name = Column(String)
HasCompany.company
のようなメソッドの実際の戻り値の型と、アノテーションの内容が一致していないことに注意してください。Mypyプラグインは、この複雑さを避けるために、すべての @declared_attr
関数を単純なアノテーション付き属性に変換します。:
# what Mypy sees
class HasCompany:
company_id: Mapped[int]
company: Mapped["Company"]
Combining with Dataclasses or Other Type-Sensitive Attribute Systems¶
Applying ORM Mappings to an existing dataclass (legacy dataclass use) のPythonデータクラス統合の例には問題があります。Pythonデータクラスは、クラスを構築するために使用する明示的な型を必要とし、各代入文で与えられる値は重要です。つまり、次のようなクラスは、データクラスに受け入れられるためには、そのままの状態で記述する必要があります:
mapper_registry: registry = registry()
@mapper_registry.mapped
@dataclass
class User:
__table__ = Table(
"user",
mapper_registry.metadata,
Column("id", Integer, primary_key=True),
Column("name", String(50)),
Column("fullname", String(50)),
Column("nickname", String(12)),
)
id: int = field(init=False)
name: Optional[str] = None
fullname: Optional[str] = None
nickname: Optional[str] = None
addresses: List[Address] = field(default_factory=list)
__mapper_args__ = { # type: ignore
"properties": {"addresses": relationship("Address")}
}
上記のクラスは、実際にはMypyの型チェックに問題なく合格します。唯一欠けているのは、次のようなSQL式で使用される User
の属性の機能です。:
stmt = select(User.name).where(User.id.in_([1, 2, 3]))
これを回避するために、Mypyプラグインには追加の属性 _mypy_mapped_attrs
を指定できる機能が追加されています。これは、クラスレベルのオブジェクトまたはその文字列名を囲むリストです。この属性は、変数 TYPE_CHECKING
内で条件付きにすることができます。:
@mapper_registry.mapped
@dataclass
class User:
__table__ = Table(
"user",
mapper_registry.metadata,
Column("id", Integer, primary_key=True),
Column("name", String(50)),
Column("fullname", String(50)),
Column("nickname", String(12)),
)
id: int = field(init=False)
name: Optional[str] = None
fullname: Optional[str]
nickname: Optional[str]
addresses: List[Address] = field(default_factory=list)
if TYPE_CHECKING:
_mypy_mapped_attrs = [id, name, "fullname", "nickname", addresses]
__mapper_args__ = { # type: ignore
"properties": {"addresses": relationship("Address")}
}
With the above recipe, the attributes listed in _mypy_mapped_attrs
will be applied with the Mapped
typing information so that the User
class will behave as a SQLAlchemy mapped class when used in a class-bound context.
上記のレシピでは、 Mapped
型情報とともに、 _mypy_mapped_attrs
にリストされた属性が適用されます。これにより、クラスにバインドされたコンテキストで使用された場合、 User
クラスはSQLAlchemyマップクラスとして動作します。