From d1ca3a8d462c3c8c32afc9bccbb9d566ff02796e Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 25 Sep 2017 20:00:20 -0400 Subject: [PATCH] Document and test __table_cls__ A use case has been identified for __table_cls__, which was added in 1.0 just for the purpose of test fixtures. Add this to public API and ensure the target use case (conditional table generation) stays supported. Change-Id: I87be5bcb72205cab89871fa586663bf147450995 Fixes: #4082 (cherry picked from commit 04bbad660bcbb7b920f3e75110a7b1187d9ddc38) --- doc/build/orm/extensions/declarative/api.rst | 53 ++++++++++++++++++-- test/ext/declarative/test_basic.py | 41 ++++++++++++++- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/doc/build/orm/extensions/declarative/api.rst b/doc/build/orm/extensions/declarative/api.rst index 5ef209b75f..d7625d4775 100644 --- a/doc/build/orm/extensions/declarative/api.rst +++ b/doc/build/orm/extensions/declarative/api.rst @@ -49,8 +49,6 @@ assumed to be completed and the 'configure' step has finished:: "" # do something with mappings -.. versionadded:: 0.7.3 - ``__declare_first__()`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -109,6 +107,55 @@ created perhaps within distinct databases:: DefaultBase.metadata.create_all(some_engine) OtherBase.metadata_create_all(some_other_engine) -.. versionadded:: 0.7.3 + +``__table_cls__`` +~~~~~~~~~~~~~~~~~ + +Allows the callable / class used to generate a :class:`.Table` to be customized. +This is a very open-ended hook that can allow special customizations +to a :class:`.Table` that one generates here:: + + class MyMixin(object): + @classmethod + def __table_cls__(cls, name, metadata, *arg, **kw): + return Table( + "my_" + name, + metadata, *arg, **kw + ) + +The above mixin would cause all :class:`.Table` objects generated to include +the prefix ``"my_"``, followed by the name normally specified using the +``__tablename__`` attribute. + +``__table_cls__`` also supports the case of returning ``None``, which +causes the class to be considered as single-table inheritance vs. its subclass. +This may be useful in some customization schemes to determine that single-table +inheritance should take place based on the arguments for the table itself, +such as, define as single-inheritance if there is no primary key present:: + + class AutoTable(object): + @declared_attr + def __tablename__(cls): + return cls.__name__ + + @classmethod + def __table_cls__(cls, *arg, **kw): + for obj in arg[1:]: + if (isinstance(obj, Column) and obj.primary_key) or \ + isinstance(obj, PrimaryKeyConstraint): + return Table(*arg, **kw) + + return None + + class Person(AutoTable, Base): + id = Column(Integer, primary_key=True) + + class Employee(Person): + employee_name = Column(String) + +The above ``Employee`` class would be mapped as single-table inheritance +against ``Person``; the ``employee_name`` column would be added as a member +of the ``Person`` table. +.. versionadded:: 1.0.0 diff --git a/test/ext/declarative/test_basic.py b/test/ext/declarative/test_basic.py index d91a88276a..f178006fe9 100644 --- a/test/ext/declarative/test_basic.py +++ b/test/ext/declarative/test_basic.py @@ -1,6 +1,6 @@ from sqlalchemy.testing import eq_, assert_raises, \ - assert_raises_message + assert_raises_message, is_ from sqlalchemy.ext import declarative as decl from sqlalchemy import exc import sqlalchemy as sa @@ -17,6 +17,7 @@ from sqlalchemy.testing import fixtures, mock from sqlalchemy.orm.events import MapperEvents from sqlalchemy.orm import mapper from sqlalchemy import event +from sqlalchemy import inspect Base = None @@ -1141,6 +1142,44 @@ class DeclarativeTest(DeclarativeTestBase): assert Bar.__table__.c.id.references(Foo2.__table__.c.id) assert Bar.__table__.kwargs['mysql_engine'] == 'InnoDB' + def test_table_cls_attribute(self): + class Foo(Base): + __tablename__ = "foo" + + @classmethod + def __table_cls__(cls, *arg, **kw): + name = arg[0] + return Table(name + 'bat', *arg[1:], **kw) + + id = Column(Integer, primary_key=True) + + eq_(Foo.__table__.name, "foobat") + + def test_table_cls_attribute_return_none(self): + from sqlalchemy.schema import Column, PrimaryKeyConstraint + + class AutoTable(object): + @declared_attr.cascading + def __tablename__(cls): + return cls.__name__ + + @classmethod + def __table_cls__(cls, *arg, **kw): + for obj in arg[1:]: + if (isinstance(obj, Column) and obj.primary_key) or \ + isinstance(obj, PrimaryKeyConstraint): + return Table(*arg, **kw) + + return None + + class Person(AutoTable, Base): + id = Column(Integer, primary_key=True) + + class Employee(Person): + employee_name = Column(String) + + is_(inspect(Employee).local_table, Person.__table__) + def test_expression(self): class User(Base, fixtures.ComparableEntity): -- 2.47.2