From: Mike Bayer Date: Sat, 10 Sep 2011 20:54:23 +0000 (-0400) Subject: - New event hook, MapperEvents.after_configured(). X-Git-Tag: rel_0_7_3~53 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=bb62d80217e584acf3f899804bb66b19f71205e2;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - New event hook, MapperEvents.after_configured(). Called after a configure() step has completed and mappers were in fact affected. Theoretically this event is called once per application, unless new mappings are constructed after existing ones have been used already. - New declarative features: - __declare_last__() method, establishes an event listener for the class method that will be called when mappers are completed with the final "configure" step. - __abstract__ flag. The class will not be mapped at all when this flag is present on the class. - New helper classes ConcreteBase, AbstractConcreteBase. Allow concrete mappings using declarative which automatically set up the "polymorphic_union" when the "configure" mapper step is invoked. - The mapper itself has semi-private methods that allow the "with_polymorphic" selectable to be assigned to the mapper after it has already been configured. [ticket:2239] --- diff --git a/CHANGES b/CHANGES index 72476994b7..a98f3d0c46 100644 --- a/CHANGES +++ b/CHANGES @@ -49,6 +49,29 @@ CHANGES ArgumentError, rather than UnmappedClassError. [ticket:2196] + - New event hook, MapperEvents.after_configured(). + Called after a configure() step has completed and + mappers were in fact affected. Theoretically this + event is called once per application, unless new mappings + are constructed after existing ones have been used + already. + + - New declarative features: + - __declare_last__() method, establishes an event + listener for the class method that will be called + when mappers are completed with the final "configure" + step. + - __abstract__ flag. The class will not be mapped + at all when this flag is present on the class. + - New helper classes ConcreteBase, AbstractConcreteBase. + Allow concrete mappings using declarative which automatically + set up the "polymorphic_union" when the "configure" + mapper step is invoked. + - The mapper itself has semi-private methods that allow + the "with_polymorphic" selectable to be assigned + to the mapper after it has already been configured. + [ticket:2239] + -sql - Behavioral improvement: empty conjunctions such as and_() and or_() will be diff --git a/doc/build/orm/extensions/declarative.rst b/doc/build/orm/extensions/declarative.rst index 9e9694df2f..aaa0261b6d 100644 --- a/doc/build/orm/extensions/declarative.rst +++ b/doc/build/orm/extensions/declarative.rst @@ -21,3 +21,8 @@ API Reference .. autofunction:: comparable_using .. autofunction:: instrument_declarative + +.. autoclass:: AbstractConcreteBase + +.. autoclass:: ConcreteBase + diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index 3b260a7976..200b0d6c6e 100755 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -459,10 +459,52 @@ before the class is built:: __table__ = managers __mapper_args__ = {'polymorphic_identity':'manager', 'concrete':True} -There is a recipe which allows the above pattern to be executed -using the declarative form, via a special base class that defers -the creation of the mapper. That recipe is available at -`DeclarativeAbstractConcreteBase `_ +Using the Concrete Helpers +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +New helper classes released in 0.7.3 provides a simpler pattern for concrete inheritance. +With these objects, the ``__declare_last__`` helper is used to configure the "polymorphic" +loader for the mapper after all subclasses have been declared. + +A basic abstract example of the :class:`.AbstractConcreteBase` class:: + + from sqlalchemy.ext.declarative import AbstractConcreteBase + + class Employee(AbstractConcreteBase, Base): + pass + +To have a concrete ``employee`` table, use :class:`.ConcreteBase` instead:: + + from sqlalchemy.ext.declarative import ConcreteBase + + class Employee(ConcreteBase, Base): + __tablename__ = 'employee' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + __mapper_args__ = { + 'polymorphic_identity':'employee', + 'concrete':True} + + +Either ``Employee`` base can be used in the normal fashion:: + + class Manager(Employee): + __tablename__ = 'manager' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(40)) + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + class Engineer(Employee): + __tablename__ = 'engineer' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + engineer_info = Column(String(40)) + __mapper_args__ = {'polymorphic_identity':'engineer', + 'concrete':True} + .. _declarative_mixins: @@ -825,6 +867,44 @@ it as part of ``__table_args__``:: __tablename__ = 'atable' c = Column(Integer,primary_key=True) +Special Directives +================== + +``__declare_last__()`` +~~~~~~~~~~~~~~~~~~~~~~ + +The ``__declare_last__()`` hook, introduced in 0.7.3, allows definition of +a class level function that is automatically called by the :meth:`.MapperEvents.after_configured` +event, which occurs after mappings are assumed to be completed and the 'configure' step +has finished:: + + class MyClass(Base): + @classmethod + def __declare_last__(cls): + "" + # do something with mappings + + +``__abstract__`` +~~~~~~~~~~~~~~~~~~~ + +``__abstract__`` is introduced in 0.7.3 and causes declarative to skip the production +of a table or mapper for the class entirely. A class can be added within a hierarchy +in the same way as mixin (see :ref:`declarative_mixins`), allowing subclasses to extend +just from the special class:: + + class SomeAbstractBase(Base): + __abstract__ = True + + def some_helpful_method(self): + "" + + @declared_attr + def __mapper_args__(cls): + return {"helpful mapper arguments":True} + + class MyMappedClass(SomeAbstractBase): + "" Class Constructor ================= @@ -862,6 +942,8 @@ from sqlalchemy.orm.properties import RelationshipProperty, ColumnProperty, Comp from sqlalchemy.orm.util import _is_mapped_class from sqlalchemy import util, exc from sqlalchemy.sql import util as sql_util, expression +from sqlalchemy import event +from sqlalchemy.orm.util import polymorphic_union, _mapper_or_none __all__ = 'declarative_base', 'synonym_for', \ @@ -906,6 +988,18 @@ def _as_declarative(cls, classname, dict_): declarative_props = (declared_attr, util.classproperty) for base in cls.__mro__: + _is_declarative_inherits = hasattr(base, '_decl_class_registry') + + if '__declare_last__' in base.__dict__: + @event.listens_for(mapper, "after_configured") + def go(): + cls.__declare_last__() + if '__abstract__' in base.__dict__: + if (base is cls or + (base in cls.__bases__ and not _is_declarative_inherits) + ): + return + class_mapped = _is_mapped_class(base) if class_mapped: parent_columns = base.__table__.c.keys() @@ -1156,8 +1250,8 @@ class DeclarativeMeta(type): def __init__(cls, classname, bases, dict_): if '_decl_class_registry' in cls.__dict__: return type.__init__(cls, classname, bases, dict_) - - _as_declarative(cls, classname, cls.__dict__) + else: + _as_declarative(cls, classname, cls.__dict__) return type.__init__(cls, classname, bases, dict_) def __setattr__(cls, key, value): @@ -1454,3 +1548,108 @@ def _undefer_column_name(key, column): column.key = key if column.name is None: column.name = key + +class ConcreteBase(object): + """A helper class for 'concrete' declarative mappings. + + :class:`.ConcreteBase` will use the :func:`.polymorphic_union` + function automatically, against all tables mapped as a subclass + to this class. The function is called via the + ``__declare_last__()`` function, which is essentially + a hook for the :func:`.MapperEvents.after_configured` event. + + :class:`.ConcreteBase` produces a mapped + table for the class itself. Compare to :class:`.AbstractConcreteBase`, + which does not. + + Example:: + + from sqlalchemy.ext.declarative import ConcreteBase + + class Employee(ConcreteBase, Base): + __tablename__ = 'employee' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + __mapper_args__ = { + 'polymorphic_identity':'employee', + 'concrete':True} + + class Manager(Employee): + __tablename__ = 'manager' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(40)) + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + """ + + @classmethod + def _create_polymorphic_union(cls, mappers): + return polymorphic_union(dict( + (mapper.polymorphic_identity, mapper.local_table) + for mapper in mappers + ), 'type', 'pjoin') + + @classmethod + def __declare_last__(cls): + m = cls.__mapper__ + if m.with_polymorphic: + return + mappers = [ sm for sm in [ + _mapper_or_none(klass) + for klass in cls.__subclasses__() + ] if sm is not None] + [m] + pjoin = cls._create_polymorphic_union(mappers) + m._set_with_polymorphic(("*",pjoin)) + m._set_polymorphic_on(pjoin.c.type) + +class AbstractConcreteBase(ConcreteBase): + """A helper class for 'concrete' declarative mappings. + + :class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union` + function automatically, against all tables mapped as a subclass + to this class. The function is called via the + ``__declare_last__()`` function, which is essentially + a hook for the :func:`.MapperEvents.after_configured` event. + + :class:`.AbstractConcreteBase` does not produce a mapped + table for the class itself. Compare to :class:`.ConcreteBase`, + which does. + + Example:: + + from sqlalchemy.ext.declarative import ConcreteBase + + class Employee(AbstractConcreteBase, Base): + pass + + class Manager(Employee): + __tablename__ = 'manager' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(40)) + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + """ + + __abstract__ = True + + @classmethod + def __declare_last__(cls): + if hasattr(cls, '__mapper__'): + return + table = cls._create_polymorphic_union( + m for m in [ + _mapper_or_none(klass) + for klass in cls.__subclasses__() + ] if m is not None + ) + cls.__mapper__ = m = mapper(cls, table, polymorphic_on=table.c.type) + for scls in cls.__subclasses__(): + sm = _mapper_or_none(scls) + if sm.concrete and cls in scls.__bases__: + sm._set_concrete_base(m) diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index 714cc92452..d551ecf7a9 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -404,6 +404,21 @@ class MapperEvents(event.Events): """ # TODO: need coverage for this event + def after_configured(self): + """Called after a series of mappers have been configured. + + This corresponds to the :func:`.orm.configure_mappers` call, which + note is usually called automatically as mappings are first + used. + + Theoretically this event is called once per + application, but is actually called any time new mappers + have been affected by a :func:`.orm.configure_mappers` call. If new mappings + are constructed after existing ones have already been used, + this event can be called again. + + """ + def translate_row(self, mapper, context, row): """Perform pre-processing on the given result row and return a new row instance. diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 5a2ca6ed45..b1a6b1a33f 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -151,17 +151,7 @@ class Mapper(object): self.allow_partial_pks = allow_partial_pks - if with_polymorphic == '*': - self.with_polymorphic = ('*', None) - elif isinstance(with_polymorphic, (tuple, list)): - if isinstance(with_polymorphic[0], (basestring, tuple, list)): - self.with_polymorphic = with_polymorphic - else: - self.with_polymorphic = (with_polymorphic, None) - elif with_polymorphic is not None: - raise sa_exc.ArgumentError("Invalid setting for with_polymorphic") - else: - self.with_polymorphic = None + self._set_with_polymorphic(with_polymorphic) if isinstance(self.local_table, expression._SelectBase): raise sa_exc.InvalidRequestError( @@ -225,10 +215,10 @@ class Mapper(object): local_table = None """The :class:`.Selectable` which this :class:`.Mapper` manages. - + Typically is an instance of :class:`.Table` or :class:`.Alias`. May also be ``None``. - + The "local" table is the selectable that the :class:`.Mapper` is directly responsible for managing from an attribute access and flush perspective. For @@ -239,15 +229,15 @@ class Mapper(object): single-table inheriting mapper, local_table will be ``None``. See also :attr:`~.Mapper.mapped_table`. - + """ mapped_table = None """The :class:`.Selectable` to which this :class:`.Mapper` is mapped. - + Typically an instance of :class:`.Table`, :class:`.Join`, or - :class:`.Alias`. - + :class:`.Alias`. + The "mapped" table is the selectable that the mapper selects from during queries. For non-inheriting mappers, the mapped table is the same as the "local" table. @@ -255,99 +245,99 @@ class Mapper(object): full :class:`.Join` representing full rows for this particular subclass. For single-table inheritance mappers, mapped_table references the base table. - + See also :attr:`~.Mapper.local_table`. - + """ inherits = None """References the :class:`.Mapper` which this :class:`.Mapper` inherits from, if any. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ configured = None """Represent ``True`` if this :class:`.Mapper` has been configured. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + See also :func:`.configure_mappers`. - + """ concrete = None """Represent ``True`` if this :class:`.Mapper` is a concrete inheritance mapper. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ tables = None """An iterable containing the collection of :class:`.Table` objects which this :class:`.Mapper` is aware of. - + If the mapper is mapped to a :class:`.Join`, or an :class:`.Alias` representing a :class:`.Select`, the individual :class:`.Table` objects that comprise the full construct will be represented here. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ primary_key = None """An iterable containing the collection of :class:`.Column` objects which comprise the 'primary key' of the mapped table, from the perspective of this :class:`.Mapper`. - + This list is against the selectable in :attr:`~.Mapper.mapped_table`. In the case of inheriting mappers, some columns may be managed by a superclass mapper. For example, in the case of a :class:`.Join`, the primary key is determined by all of the primary key columns across all tables referenced by the :class:`.Join`. - + The list is also not necessarily the same as the primary key column collection associated with the underlying tables; the :class:`.Mapper` features a ``primary_key`` argument that can override what the :class:`.Mapper` considers as primary key columns. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ class_ = None """The Python class which this :class:`.Mapper` maps. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ class_manager = None """The :class:`.ClassManager` which maintains event listeners and class-bound descriptors for this :class:`.Mapper`. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. """ single = None """Represent ``True`` if this :class:`.Mapper` is a single table - inheritance mapper. - + inheritance mapper. + :attr:`~.Mapper.local_table` will be ``None`` if this flag is set. - - This is a *read only* attribute determined during mapper construction. + + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ non_primary = None @@ -355,35 +345,35 @@ class Mapper(object): mapper, e.g. a mapper that is used only to selet rows but not for persistence management. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ polymorphic_on = None """The :class:`.Column` specified as the ``polymorphic_on`` column for this :class:`.Mapper`, within an inheritance scenario. - + This attribute may also be of other types besides :class:`.Column` in a future SQLAlchemy release. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ polymorphic_map = None """A mapping of "polymorphic identity" identifiers mapped to :class:`.Mapper` instances, within an inheritance scenario. - + The identifiers can be of any type which is comparable to the type of column represented by :attr:`~.Mapper.polymorphic_on`. - + An inheritance chain of mappers will all reference the same polymorphic map object. The object is used to correlate incoming result rows to target mappers. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. """ @@ -391,32 +381,32 @@ class Mapper(object): polymorphic_identity = None """Represent an identifier which is matched against the :attr:`~.Mapper.polymorphic_on` column during result row loading. - + Used only with inheritance, this object can be of any type which is comparable to the type of column represented by :attr:`~.Mapper.polymorphic_on`. - - This is a *read only* attribute determined during mapper construction. + + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ base_mapper = None """The base-most :class:`.Mapper` in an inheritance chain. - + In a non-inheriting scenario, this attribute will always be this :class:`.Mapper`. In an inheritance scenario, it references the :class:`.Mapper` which is parent to all other :class:`.Mapper` objects in the inheritance chain. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ columns = None """A collection of :class:`.Column` or other scalar expression objects maintained by this :class:`.Mapper`. - + The collection behaves the same as that of the ``c`` attribute on any :class:`.Table` object, except that only those columns included in this mapping are present, and are keyed based on the attribute name @@ -424,9 +414,9 @@ class Mapper(object): :class:`.Column` itself. Additionally, scalar expressions mapped by :func:`.column_property` are also present here. - This is a *read only* attribute determined during mapper construction. + This is a *read only* attribute determined during mapper construction. Behavior is undefined if directly modified. - + """ validators = None @@ -530,16 +520,6 @@ class Mapper(object): if self.polymorphic_identity is not None: self.polymorphic_map[self.polymorphic_identity] = self - if self.polymorphic_on is None: - for mapper in self.iterate_to_root(): - # try to set up polymorphic on using - # correesponding_column(); else leave - # as None - if mapper.polymorphic_on is not None: - self.polymorphic_on = \ - self.mapped_table.corresponding_column( - mapper.polymorphic_on) - break else: self._all_tables = set() self.base_mapper = self @@ -553,6 +533,59 @@ class Mapper(object): "Mapper '%s' does not have a mapped_table specified." % self) + def _set_with_polymorphic(self, with_polymorphic): + if with_polymorphic == '*': + self.with_polymorphic = ('*', None) + elif isinstance(with_polymorphic, (tuple, list)): + if isinstance(with_polymorphic[0], (basestring, tuple, list)): + self.with_polymorphic = with_polymorphic + else: + self.with_polymorphic = (with_polymorphic, None) + elif with_polymorphic is not None: + raise sa_exc.ArgumentError("Invalid setting for with_polymorphic") + else: + self.with_polymorphic = None + + if isinstance(self.local_table, expression._SelectBase): + raise sa_exc.InvalidRequestError( + "When mapping against a select() construct, map against " + "an alias() of the construct instead." + "This because several databases don't allow a " + "SELECT from a subquery that does not have an alias." + ) + + if self.with_polymorphic and \ + isinstance(self.with_polymorphic[1], + expression._SelectBase): + self.with_polymorphic = (self.with_polymorphic[0], + self.with_polymorphic[1].alias()) + if self.configured: + self._expire_memoizations() + + def _set_concrete_base(self, mapper): + """Set the given :class:`.Mapper` as the 'inherits' for this :class:`.Mapper`, + assuming this :class:`.Mapper` is concrete and does not already have + an inherits.""" + + assert self.concrete + assert not self.inherits + assert isinstance(mapper, Mapper) + self.inherits = mapper + self.inherits.polymorphic_map.update(self.polymorphic_map) + self.polymorphic_map = self.inherits.polymorphic_map + for mapper in self.iterate_to_root(): + if mapper.polymorphic_on is not None: + mapper._requires_row_aliasing = True + self.batch = self.inherits.batch + self.base_mapper = self.inherits.base_mapper + self.inherits._inheriting_mappers.add(self) + self.passive_updates = self.inherits.passive_updates + self._all_tables = self.inherits._all_tables + + def _set_polymorphic_on(self, polymorphic_on): + self.polymorphic_on = polymorphic_on + self._configure_polymorphic_setter(True) + def _configure_legacy_instrument_class(self): if self.inherits: @@ -818,7 +851,7 @@ class Mapper(object): init=False, setparent=True) - def _configure_polymorphic_setter(self): + def _configure_polymorphic_setter(self, init=False): """Configure an attribute on the mapper representing the 'polymorphic_on' column, if applicable, and not already generated by _configure_properties (which is typical). @@ -834,6 +867,17 @@ class Mapper(object): # but we need it for the base mapper setter = False + if self.polymorphic_on is None: + for mapper in self.iterate_to_root(): + # try to set up polymorphic on using + # correesponding_column(); else leave + # as None + if mapper.polymorphic_on is not None: + self.polymorphic_on = \ + self.mapped_table.corresponding_column( + mapper.polymorphic_on) + break + if self.polymorphic_on is not None: setter = True @@ -861,7 +905,7 @@ class Mapper(object): self._configure_property( col.key, properties.ColumnProperty(col, _instrument=instrument), - init=False, setparent=True) + init=init, setparent=True) polymorphic_key = col.key else: polymorphic_key = self._columntoproperty[self.polymorphic_on].key @@ -2714,6 +2758,7 @@ def configure_mappers(): if not _new_mappers: return + _call_configured = None _COMPILE_MUTEX.acquire() try: global _already_compiling @@ -2744,6 +2789,7 @@ def configure_mappers(): mapper._post_configure_properties() mapper._expire_memoizations() mapper.dispatch.mapper_configured(mapper, mapper.class_) + _call_configured = mapper except: exc = sys.exc_info()[1] if not hasattr(exc, '_configure_failed'): @@ -2755,7 +2801,8 @@ def configure_mappers(): _already_compiling = False finally: _COMPILE_MUTEX.release() - + if _call_configured is not None: + _call_configured.dispatch.after_configured() def reconstructor(fn): """Decorate a method as the 'reconstructor' hook. diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 3dc1f86763..d778bef3d7 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -601,6 +601,9 @@ def has_identity(object): return state.has_identity def _is_mapped_class(cls): + """Return True if the given object is a mapped class, + :class:`.Mapper`, or :class:`.AliasedClass`.""" + if isinstance(cls, (AliasedClass, mapperlib.Mapper)): return True if isinstance(cls, expression.ClauseElement): @@ -610,6 +613,16 @@ def _is_mapped_class(cls): return manager and _INSTRUMENTOR in manager.info return False +def _mapper_or_none(cls): + """Return the :class:`.Mapper` for the given class or None if the + class is not mapped.""" + + manager = attributes.manager_of_class(cls) + if manager is not None and _INSTRUMENTOR in manager.info: + return manager.info[_INSTRUMENTOR] + else: + return None + def instance_str(instance): """Return a string describing an instance.""" diff --git a/test/ext/test_declarative.py b/test/ext/test_declarative.py index 22733223de..e338ef0396 100644 --- a/test/ext/test_declarative.py +++ b/test/ext/test_declarative.py @@ -14,7 +14,7 @@ from sqlalchemy.orm import relationship, create_session, class_mapper, \ Session from test.lib.testing import eq_ from sqlalchemy.util import classproperty -from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.ext.declarative import declared_attr, AbstractConcreteBase, ConcreteBase from test.lib import fixtures class DeclarativeTestBase(fixtures.TestBase, testing.AssertsExecutionResults): @@ -2014,7 +2014,30 @@ class DeclarativeInheritanceTest(DeclarativeTestBase): assert A.__mapper__.inherits is a_1.__mapper__ - def test_concrete(self): +from test.orm.test_events import _RemoveListeners +class ConcreteInhTest(_RemoveListeners, DeclarativeTestBase): + def _roundtrip(self, Employee, Manager, Engineer, polymorphic=True): + Base.metadata.create_all() + sess = create_session() + e1 = Engineer(name='dilbert', primary_language='java') + e2 = Engineer(name='wally', primary_language='c++') + m1 = Manager(name='dogbert', golf_swing='fore!') + e3 = Engineer(name='vlad', primary_language='cobol') + sess.add_all([e1, e2, m1, e3]) + sess.flush() + sess.expunge_all() + if polymorphic: + eq_(sess.query(Employee).order_by(Employee.name).all(), + [Engineer(name='dilbert'), Manager(name='dogbert'), + Engineer(name='vlad'), Engineer(name='wally')]) + else: + eq_(sess.query(Engineer).order_by(Engineer.name).all(), + [Engineer(name='dilbert'), Engineer(name='vlad'), + Engineer(name='wally')]) + eq_(sess.query(Manager).all(), [Manager(name='dogbert')]) + + + def test_explicit(self): engineers = Table('engineers', Base.metadata, Column('id', Integer, primary_key=True, test_needs_autoincrement=True), Column('name' @@ -2027,47 +2050,35 @@ class DeclarativeInheritanceTest(DeclarativeTestBase): punion = polymorphic_union({'engineer': engineers, 'manager' : managers}, 'type', 'punion') - class Person(Base, fixtures.ComparableEntity): + class Employee(Base, fixtures.ComparableEntity): __table__ = punion __mapper_args__ = {'polymorphic_on': punion.c.type} - class Engineer(Person): + class Engineer(Employee): __table__ = engineers __mapper_args__ = {'polymorphic_identity': 'engineer', 'concrete': True} - class Manager(Person): + class Manager(Employee): __table__ = managers __mapper_args__ = {'polymorphic_identity': 'manager', 'concrete': True} - - Base.metadata.create_all() - sess = create_session() - e1 = Engineer(name='dilbert', primary_language='java') - e2 = Engineer(name='wally', primary_language='c++') - m1 = Manager(name='dogbert', golf_swing='fore!') - e3 = Engineer(name='vlad', primary_language='cobol') - sess.add_all([e1, e2, m1, e3]) - sess.flush() - sess.expunge_all() - eq_(sess.query(Person).order_by(Person.name).all(), - [Engineer(name='dilbert'), Manager(name='dogbert'), - Engineer(name='vlad'), Engineer(name='wally')]) + self._roundtrip(Employee, Manager, Engineer) def test_concrete_inline_non_polymorphic(self): """test the example from the declarative docs.""" - class Person(Base, fixtures.ComparableEntity): + class Employee(Base, fixtures.ComparableEntity): __tablename__ = 'people' id = Column(Integer, primary_key=True, test_needs_autoincrement=True) name = Column(String(50)) - class Engineer(Person): + class Engineer(Employee): __tablename__ = 'engineers' __mapper_args__ = {'concrete': True} @@ -2076,7 +2087,7 @@ class DeclarativeInheritanceTest(DeclarativeTestBase): primary_language = Column(String(50)) name = Column(String(50)) - class Manager(Person): + class Manager(Employee): __tablename__ = 'manager' __mapper_args__ = {'concrete': True} @@ -2084,20 +2095,57 @@ class DeclarativeInheritanceTest(DeclarativeTestBase): test_needs_autoincrement=True) golf_swing = Column(String(50)) name = Column(String(50)) + self._roundtrip(Employee, Manager, Engineer, polymorphic=False) + + def test_abstract_concrete_extension(self): + class Employee(AbstractConcreteBase, Base, fixtures.ComparableEntity): + pass + + class Manager(Employee): + __tablename__ = 'manager' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + golf_swing = Column(String(40)) + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + class Engineer(Employee): + __tablename__ = 'engineer' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + primary_language = Column(String(40)) + __mapper_args__ = {'polymorphic_identity':'engineer', + 'concrete':True} + + self._roundtrip(Employee, Manager, Engineer) + + def test_concrete_extension(self): + class Employee(ConcreteBase, Base, fixtures.ComparableEntity): + __tablename__ = 'employee' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + __mapper_args__ = { + 'polymorphic_identity':'employee', + 'concrete':True} + class Manager(Employee): + __tablename__ = 'manager' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + golf_swing = Column(String(40)) + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + class Engineer(Employee): + __tablename__ = 'engineer' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + primary_language = Column(String(40)) + __mapper_args__ = {'polymorphic_identity':'engineer', + 'concrete':True} + self._roundtrip(Employee, Manager, Engineer) - Base.metadata.create_all() - sess = create_session() - e1 = Engineer(name='dilbert', primary_language='java') - e2 = Engineer(name='wally', primary_language='c++') - m1 = Manager(name='dogbert', golf_swing='fore!') - e3 = Engineer(name='vlad', primary_language='cobol') - sess.add_all([e1, e2, m1, e3]) - sess.flush() - sess.expunge_all() - eq_(sess.query(Engineer).order_by(Engineer.name).all(), - [Engineer(name='dilbert'), Engineer(name='vlad'), - Engineer(name='wally')]) - eq_(sess.query(Manager).all(), [Manager(name='dogbert')]) def _produce_test(inline, stringbased):