From 638ec10920268b0332c1bf1bb2bf1181979b3f9c Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 20 Feb 2021 12:19:30 -0500 Subject: [PATCH] document declarative base made non-dynamically officially document how to make declarative base non-dynamically such that mypy and similar tools can process it without plugins Change-Id: I884f9a7c06c4a8b8111948a2dd64e308e7dce4fc References: #4609 --- doc/build/orm/mapping_styles.rst | 24 ++++++++++++++++++++++++ lib/sqlalchemy/orm/__init__.py | 1 + lib/sqlalchemy/orm/decl_api.py | 14 +++++++++++++- test/orm/declarative/test_basic.py | 17 ++++++++++++++++- 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/doc/build/orm/mapping_styles.rst b/doc/build/orm/mapping_styles.rst index 3abcafed35..622159bd73 100644 --- a/doc/build/orm/mapping_styles.rst +++ b/doc/build/orm/mapping_styles.rst @@ -102,6 +102,30 @@ Documentation for Declarative mapping continues at :ref:`declarative_config_topl :ref:`declarative_config_toplevel` +.. _orm_explicit_declarative_base: + +Creating an Explicit Base Non-Dynamically (for use with mypy, similar) +---------------------------------------------------------------------- + +Tools like mypy are not necessarily compatible with the dynamically +generated ``Base`` delivered by SQLAlchemy functions like :func:`_orm.declarative_base`. +To build a declarative base in a non-dynamic fashion, the +:class:`_orm.DeclarativeMeta` class may be used directly as follows:: + + from sqlalchemy.orm import registry + from sqlalchemy.orm.decl_api import DeclarativeMeta + + mapper_registry = registry() + + class Base(metaclass=DeclarativeMeta): + __abstract__ = True + registry = mapper_registry + metadata = mapper_registry.metadata + +The above ``Base`` is equivalent to one created using the +:meth:`_orm.registry.generate_base` method and will be fully understood by +type analysis tools without the use of plugins. + .. _orm_declarative_decorator: diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 4793fc6386..ac06efba6b 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -22,6 +22,7 @@ from .attributes import QueryableAttribute # noqa from .context import QueryContext # noqa from .decl_api import as_declarative # noqa from .decl_api import declarative_base # noqa +from .decl_api import DeclarativeMeta # noqa from .decl_api import declared_attr # noqa from .decl_api import has_inherited_table # noqa from .decl_api import registry # noqa diff --git a/lib/sqlalchemy/orm/decl_api.py b/lib/sqlalchemy/orm/decl_api.py index 8afdb3a50b..1166d307e3 100644 --- a/lib/sqlalchemy/orm/decl_api.py +++ b/lib/sqlalchemy/orm/decl_api.py @@ -657,11 +657,23 @@ class registry(object): __tablename__ = "my_table" id = Column(Integer, primary_key=True) + The above dynamically generated class is equivalent to the + non-dynamic example below:: + + from sqlalchemy.orm import registry + from sqlalchemy.orm.decl_api import DeclarativeMeta + + mapper_registry = registry() + + class Base(metaclass=DeclarativeMeta): + __abstract__ = True + registry = mapper_registry + metadata = mapper_registry.metadata + The :meth:`_orm.registry.generate_base` method provides the implementation for the :func:`_orm.declarative_base` function, which creates the :class:`_orm.registry` and base class all at once. - See the section :ref:`orm_declarative_mapping` for background and examples. diff --git a/test/orm/declarative/test_basic.py b/test/orm/declarative/test_basic.py index c779d214c8..7fbd14b473 100644 --- a/test/orm/declarative/test_basic.py +++ b/test/orm/declarative/test_basic.py @@ -61,9 +61,20 @@ class DeclarativeTestBase( ): __dialect__ = "default" + base_style = "dynamic" + def setup_test(self): global Base - Base = declarative_base(testing.db) + + if self.base_style == "dynamic": + Base = declarative_base(testing.db) + elif self.base_style == "explicit": + mapper_registry = registry(_bind=testing.db) + + class Base(with_metaclass(DeclarativeMeta)): + __abstract__ = True + registry = mapper_registry + metadata = mapper_registry.metadata def teardown_test(self): close_all_sessions() @@ -71,6 +82,9 @@ class DeclarativeTestBase( Base.metadata.drop_all(testing.db) +@testing.combinations( + ("dynamic",), ("explicit",), argnames="base_style", id_="s" +) class DeclarativeTest(DeclarativeTestBase): def test_basic(self): class User(Base, fixtures.ComparableEntity): @@ -2266,6 +2280,7 @@ class DeclarativeTest(DeclarativeTestBase): eq_(UserType._set_random_keyword_used_here, True) +# TODO: this should be using @combinations def _produce_test(inline, stringbased): class ExplicitJoinTest(fixtures.MappedTest): @classmethod -- 2.47.2