From: Mike Bayer Date: Mon, 31 Jan 2011 01:29:48 +0000 (-0500) Subject: - SchemaItem, SchemaType now descend from common type X-Git-Tag: rel_0_7b1~43 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=12073e281eebdece0fe4e24c6704d57eafdc9247;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - SchemaItem, SchemaType now descend from common type SchemaEventTarget, which supplies dispatch - the dispatch now provides before_parent_attach(), after_parent_attach(), events which generally bound the _set_parent() event. [ticket:2037] - the _on_table_attach mechanism now usually uses the event dispatch - fixed class-level event dispatch to propagate to all subclasses, not just immediate subclasses - fixed class-level event unpickling to handle more involved inheritance hierarchies, needed by the new schema event dispatch. - ForeignKeyConstraint doesn't re-call the column attach event on ForeignKey objects that are already associated with the correct Column - we still need that ImportError on mysqldb CLIENT FLAGS to support mock DBAPIs --- diff --git a/doc/build/core/events.rst b/doc/build/core/events.rst index 6c8b4d064a..ffa0fe6251 100644 --- a/doc/build/core/events.rst +++ b/doc/build/core/events.rst @@ -28,3 +28,6 @@ Schema Events .. autoclass:: sqlalchemy.events.DDLEvents :members: +.. autoclass:: sqlalchemy.events.SchemaEventTarget + :members: + diff --git a/doc/build/core/schema.rst b/doc/build/core/schema.rst index 639525581c..fdd086247c 100644 --- a/doc/build/core/schema.rst +++ b/doc/build/core/schema.rst @@ -376,6 +376,9 @@ Schema API Constructs :undoc-members: :show-inheritance: +.. autoclass:: SchemaItem + :show-inheritance: + .. autoclass:: Table :members: :undoc-members: @@ -386,6 +389,7 @@ Schema API Constructs :undoc-members: :show-inheritance: + .. _metadata_reflection: Reflecting Database Objects diff --git a/lib/sqlalchemy/connectors/mysqldb.py b/lib/sqlalchemy/connectors/mysqldb.py index 27a56b749b..189c412a00 100644 --- a/lib/sqlalchemy/connectors/mysqldb.py +++ b/lib/sqlalchemy/connectors/mysqldb.py @@ -87,10 +87,13 @@ class MySQLDBConnector(Connector): # supports_sane_rowcount. client_flag = opts.get('client_flag', 0) if self.dbapi is not None: - CLIENT_FLAGS = __import__( - self.dbapi.__name__ + '.constants.CLIENT' - ).constants.CLIENT - client_flag |= CLIENT_FLAGS.FOUND_ROWS + try: + CLIENT_FLAGS = __import__( + self.dbapi.__name__ + '.constants.CLIENT' + ).constants.CLIENT + client_flag |= CLIENT_FLAGS.FOUND_ROWS + except (AttributeError, ImportError): + pass opts['client_flag'] = client_flag return [[], opts] diff --git a/lib/sqlalchemy/event.py b/lib/sqlalchemy/event.py index 66b7e2d1b8..0fcc8ef499 100644 --- a/lib/sqlalchemy/event.py +++ b/lib/sqlalchemy/event.py @@ -48,7 +48,11 @@ class _UnpickleDispatch(object): """ def __call__(self, _parent_cls): - return _parent_cls.__dict__['dispatch'].dispatch_cls(_parent_cls) + for cls in _parent_cls.__mro__: + if 'dispatch' in cls.__dict__: + return cls.__dict__['dispatch'].dispatch_cls(_parent_cls) + else: + raise AttributeError("No class with a 'dispatch' member present.") class _Dispatch(object): """Mirror the event listening definitions of an Events class with @@ -167,11 +171,17 @@ class _DispatchDescriptor(object): assert isinstance(target, type), \ "Class-level Event targets must be classes." - for cls in [target] + target.__subclasses__(): + stack = [target] + while stack: + cls = stack.pop(0) + stack.extend(cls.__subclasses__()) self._clslevel[cls].append(obj) def remove(self, obj, target): - for cls in [target] + target.__subclasses__(): + stack = [target] + while stack: + cls = stack.pop(0) + stack.extend(cls.__subclasses__()) self._clslevel[cls].remove(obj) def clear(self): diff --git a/lib/sqlalchemy/events.py b/lib/sqlalchemy/events.py index c1f10977d8..8a2776898d 100644 --- a/lib/sqlalchemy/events.py +++ b/lib/sqlalchemy/events.py @@ -10,12 +10,22 @@ from sqlalchemy import event, exc class DDLEvents(event.Events): """ - Define create/drop event listers for schema objects. - - These events currently apply to :class:`.Table` - and :class:`.MetaData` objects as targets. - - e.g.:: + Define event listeners for schema objects, + that is, :class:`.SchemaItem` and :class:`.SchemaEvent` + subclasses, including :class:`.MetaData`, :class:`.Table`, + :class:`.Column`. + + :class:`.MetaData` and :class:`.Table` support events + specifically regarding when CREATE and DROP + DDL is emitted to the database. + + Attachment events are also provided to customize + behavior whenever a child schema element is associated + with a parent, such as, when a :class:`.Column` is associated + with its :class:`.Table`, when a :class:`.ForeignKeyConstraint` + is associated with a :class:`.Table`, etc. + + Example using the ``after_create`` event:: from sqlalchemy import event from sqlalchemy import Table, Column, Metadata, Integer @@ -117,6 +127,35 @@ class DDLEvents(event.Events): """ + def before_parent_attach(self, target, parent): + """Called before a :class:`.SchemaItem` is associated with + a parent :class:`.SchemaItem`. + + """ + + def after_parent_attach(self, target, parent): + """Called after a :class:`.SchemaItem` is associated with + a parent :class:`.SchemaItem`. + + """ + +class SchemaEventTarget(object): + """Base class for elements that are the targets of :class:`.DDLEvents` events. + + This includes :class:`.SchemaItem` as well as :class:`.SchemaType`. + + """ + dispatch = event.dispatcher(DDLEvents) + + def _set_parent(self, parent): + """Associate with this SchemaEvent's parent object.""" + + raise NotImplementedError() + + def _set_parent_with_dispatch(self, parent): + self.dispatch.before_parent_attach(self, parent) + self._set_parent(parent) + self.dispatch.after_parent_attach(self, parent) class PoolEvents(event.Events): """Available events for :class:`.Pool`. diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index cfed006103..9cf5c7f14e 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -48,7 +48,7 @@ __all__.sort() RETAIN_SCHEMA = util.symbol('retain_schema') -class SchemaItem(visitors.Visitable): +class SchemaItem(events.SchemaEventTarget, visitors.Visitable): """Base class for items that define a database schema.""" __visit_name__ = 'schema_item' @@ -59,12 +59,7 @@ class SchemaItem(visitors.Visitable): for item in args: if item is not None: - item._set_parent(self) - - def _set_parent(self, parent): - """Associate with this SchemaItem's parent object.""" - - raise NotImplementedError() + item._set_parent_with_dispatch(self) def get_children(self, **kwargs): """used to allow SchemaVisitor access""" @@ -177,8 +172,6 @@ class Table(SchemaItem, expression.TableClause): __visit_name__ = 'table' - dispatch = event.dispatcher(events.DDLEvents) - def __new__(cls, *args, **kw): if not args: # python3k pickle seems to call this @@ -207,9 +200,11 @@ class Table(SchemaItem, expression.TableClause): raise exc.InvalidRequestError( "Table '%s' not defined" % (key)) table = object.__new__(cls) + table.dispatch.before_parent_attach(table, metadata) metadata._add_table(name, schema, table) try: table._init(name, metadata, *args, **kw) + table.dispatch.after_parent_attach(table, metadata) return table except: metadata._remove_table(name, schema) @@ -365,12 +360,12 @@ class Table(SchemaItem, expression.TableClause): def append_column(self, column): """Append a ``Column`` to this ``Table``.""" - column._set_parent(self) + column._set_parent_with_dispatch(self) def append_constraint(self, constraint): """Append a ``Constraint`` to this ``Table``.""" - constraint._set_parent(self) + constraint._set_parent_with_dispatch(self) def append_ddl_listener(self, event_name, listener): """Append a DDL event listener to this ``Table``. @@ -708,14 +703,13 @@ class Column(SchemaItem, expression.ColumnClause): self.autoincrement = kwargs.pop('autoincrement', True) self.constraints = set() self.foreign_keys = set() - self._table_events = set() # check if this Column is proxying another column if '_proxies' in kwargs: self.proxies = kwargs.pop('_proxies') # otherwise, add DDL-related events elif isinstance(self.type, types.SchemaType): - self.type._set_parent(self) + self.type._set_parent_with_dispatch(self) if self.default is not None: if isinstance(self.default, (ColumnDefault, Sequence)): @@ -777,7 +771,7 @@ class Column(SchemaItem, expression.ColumnClause): return False def append_foreign_key(self, fk): - fk._set_parent(self) + fk._set_parent_with_dispatch(self) def __repr__(self): kwarg = [] @@ -851,15 +845,10 @@ class Column(SchemaItem, expression.ColumnClause): "Index object external to the Table.") table.append_constraint(UniqueConstraint(self.key)) - for fn in self._table_events: - fn(table, self) - del self._table_events - def _on_table_attach(self, fn): if self.table is not None: - fn(self.table, self) - else: - self._table_events.add(fn) + fn(self, self.table) + event.listen(self, 'after_parent_attach', fn) def copy(self, **kw): """Create a copy of this ``Column``, unitialized. @@ -873,7 +862,7 @@ class Column(SchemaItem, expression.ColumnClause): [c.copy(**kw) for c in self.constraints] + \ [c.copy(**kw) for c in self.foreign_keys if not c.constraint] - c = Column( + return Column( name=self.name, type_=self.type, key = self.key, @@ -891,9 +880,6 @@ class Column(SchemaItem, expression.ColumnClause): doc=self.doc, *args ) - if hasattr(self, '_table_events'): - c._table_events = list(self._table_events) - return c def _make_proxy(self, selectable, name=None): """Create a *proxy* for this column. @@ -920,9 +906,7 @@ class Column(SchemaItem, expression.ColumnClause): selectable._columns.add(c) if self.primary_key: selectable.primary_key.add(c) - for fn in c._table_events: - fn(selectable, c) - del c._table_events + c.dispatch.after_parent_attach(c, selectable) return c def get_children(self, schema_visitor=False, **kwargs): @@ -1210,7 +1194,7 @@ class ForeignKey(SchemaItem): self.parent.foreign_keys.add(self) self.parent._on_table_attach(self._set_table) - def _set_table(self, table, column): + def _set_table(self, column, table): # standalone ForeignKey - create ForeignKeyConstraint # on the hosting Table when attached to the Table. if self.constraint is None and isinstance(table, Table): @@ -1220,7 +1204,7 @@ class ForeignKey(SchemaItem): deferrable=self.deferrable, initially=self.initially, ) self.constraint._elements[self.parent] = self - self.constraint._set_parent(table) + self.constraint._set_parent_with_dispatch(table) table.foreign_keys.add(self) class DefaultGenerator(SchemaItem): @@ -1382,7 +1366,7 @@ class Sequence(DefaultGenerator): super(Sequence, self)._set_parent(column) column._on_table_attach(self._set_table) - def _set_table(self, table, column): + def _set_table(self, column, table): self.metadata = table.metadata @property @@ -1407,7 +1391,7 @@ class Sequence(DefaultGenerator): bind.drop(self, checkfirst=checkfirst) -class FetchedValue(object): +class FetchedValue(events.SchemaEventTarget): """A marker for a transparent database-side default. Use :class:`.FetchedValue` when the database is configured @@ -1556,7 +1540,7 @@ class ColumnCollectionMixin(object): if self._pending_colargs and \ isinstance(self._pending_colargs[0], Column) and \ self._pending_colargs[0].table is not None: - self._set_parent(self._pending_colargs[0].table) + self._set_parent_with_dispatch(self._pending_colargs[0].table) def _set_parent(self, table): for col in self._pending_colargs: @@ -1643,7 +1627,7 @@ class CheckConstraint(Constraint): __init__(name, deferrable, initially, _create_rule) self.sqltext = expression._literal_as_text(sqltext) if table is not None: - self._set_parent(table) + self._set_parent_with_dispatch(table) def __visit_name__(self): if isinstance(self.parent, Table): @@ -1744,7 +1728,7 @@ class ForeignKeyConstraint(Constraint): ) if table is not None: - self._set_parent(table) + self._set_parent_with_dispatch(table) @property def columns(self): @@ -1761,7 +1745,10 @@ class ForeignKeyConstraint(Constraint): # resolved to Column objects if isinstance(col, basestring): col = table.c[col] - fk._set_parent(col) + + if not hasattr(fk, 'parent') or \ + fk.parent is not col: + fk._set_parent_with_dispatch(col) if self.use_alter: def supports_alter(ddl, event, schema_item, bind, **kw): @@ -1924,8 +1911,6 @@ class MetaData(SchemaItem): __visit_name__ = 'metadata' - dispatch = event.dispatcher(events.DDLEvents) - def __init__(self, bind=None, reflect=False): """Create a new MetaData object. diff --git a/lib/sqlalchemy/types.py b/lib/sqlalchemy/types.py index f9320cf3eb..3a14157966 100644 --- a/lib/sqlalchemy/types.py +++ b/lib/sqlalchemy/types.py @@ -32,7 +32,7 @@ from sqlalchemy.util import pickle from sqlalchemy.util.compat import decimal from sqlalchemy.sql.visitors import Visitable from sqlalchemy import util -from sqlalchemy import processors +from sqlalchemy import processors, events import collections default = util.importlater("sqlalchemy.engine", "default") @@ -1391,12 +1391,17 @@ class Binary(LargeBinary): 'LargeBinary.') LargeBinary.__init__(self, *arg, **kw) -class SchemaType(object): +class SchemaType(events.SchemaEventTarget): """Mark a type as possibly requiring schema-level DDL for usage. Supports types that must be explicitly created/dropped (i.e. PG ENUM type) as well as types that are complimented by table or schema level constraints, triggers, and other rules. + + :class:`.SchemaType` classes can also be targets for the + :meth:`.DDLEvents.before_parent_attach` and :meth:`.DDLEvents.after_parent_attach` + events, where the events fire off surrounding the association of + the type object with a parent :class:`.Column`. """ @@ -1414,7 +1419,7 @@ class SchemaType(object): def _set_parent(self, column): column._on_table_attach(util.portable_instancemethod(self._set_table)) - def _set_table(self, table, column): + def _set_table(self, column, table): table.append_ddl_listener('before-create', util.portable_instancemethod( self._on_table_create)) @@ -1550,9 +1555,9 @@ class Enum(String, SchemaType): return not self.native_enum or \ not compiler.dialect.supports_native_enum - def _set_table(self, table, column): + def _set_table(self, column, table): if self.native_enum: - SchemaType._set_table(self, table, column) + SchemaType._set_table(self, column, table) e = schema.CheckConstraint( @@ -1713,7 +1718,7 @@ class Boolean(TypeEngine, SchemaType): def _should_create_constraint(self, compiler): return not compiler.dialect.supports_native_boolean - def _set_table(self, table, column): + def _set_table(self, column, table): if not self.create_constraint: return diff --git a/test/aaa_profiling/test_zoomark.py b/test/aaa_profiling/test_zoomark.py index d34ee04771..d798c48a6e 100644 --- a/test/aaa_profiling/test_zoomark.py +++ b/test/aaa_profiling/test_zoomark.py @@ -358,7 +358,7 @@ class ZooMarkTest(TestBase): metadata = MetaData(engine) engine.connect() - @profiling.function_call_count(3012, {'2.4': 1711}) + @profiling.function_call_count(3266, {'2.4': 1711}) def test_profile_1_create_tables(self): self.test_baseline_1_create_tables() diff --git a/test/aaa_profiling/test_zoomark_orm.py b/test/aaa_profiling/test_zoomark_orm.py index ce2cfdc8e3..ee0d59b2c9 100644 --- a/test/aaa_profiling/test_zoomark_orm.py +++ b/test/aaa_profiling/test_zoomark_orm.py @@ -330,7 +330,7 @@ class ZooMarkTest(TestBase): session = sessionmaker()() engine.connect() - @profiling.function_call_count(4592) + @profiling.function_call_count(4912) def test_profile_1_create_tables(self): self.test_baseline_1_create_tables() diff --git a/test/sql/test_metadata.py b/test/sql/test_metadata.py index db087331ae..38788330ab 100644 --- a/test/sql/test_metadata.py +++ b/test/sql/test_metadata.py @@ -5,7 +5,8 @@ from test.lib.testing import emits_warning import pickle from sqlalchemy import Integer, String, UniqueConstraint, \ CheckConstraint, ForeignKey, MetaData, Sequence, \ - ForeignKeyConstraint, ColumnDefault, Index + ForeignKeyConstraint, ColumnDefault, Index, event,\ + events from test.lib.schema import Table, Column from sqlalchemy import schema, exc import sqlalchemy as tsa @@ -68,13 +69,16 @@ class MetaDataTest(TestBase, ComparesTables): def test_uninitialized_column_copy_events(self): msgs = [] - def write(t, c): + def write(c, t): msgs.append("attach %s.%s" % (t.name, c.name)) c1 = Column('foo', String()) - c1._on_table_attach(write) m = MetaData() for i in xrange(3): cx = c1.copy() + # as of 0.7, these events no longer copy. its expected + # that listeners will be re-established from the + # natural construction of things. + cx._on_table_attach(write) t = Table('foo%d' % i, m, cx) eq_(msgs, ['attach foo0.foo', 'attach foo1.foo', 'attach foo2.foo']) @@ -585,3 +589,47 @@ class ColumnOptionsTest(TestBase): c.info['bar'] = 'zip' assert c.info['bar'] == 'zip' + +class CatchAllEventsTest(TestBase): + + def teardown(self): + events.SchemaEventTarget.dispatch._clear() + + def test_all_events(self): + canary = [] + def before_attach(obj, parent): + canary.append("%s->%s" % (obj.__class__.__name__, parent.__class__.__name__)) + + def after_attach(obj, parent): + canary.append("%s->%s" % (obj, parent)) + + event.listen(events.SchemaEventTarget, "before_parent_attach", before_attach) + event.listen(events.SchemaEventTarget, "after_parent_attach", after_attach) + + m = MetaData() + t1 = Table('t1', m, + Column('id', Integer, Sequence('foo_id'), primary_key=True), + Column('bar', String, ForeignKey('t2.id')) + ) + t2 = Table('t2', m, + Column('id', Integer, primary_key=True), + ) + + # TODO: test more conditions here, constraints, defaults, etc. + eq_( + canary, + [ + 'Sequence->Column', + "Sequence('foo_id', start=None, increment=None, optional=False)->id", + 'ForeignKey->Column', + "ForeignKey('t2.id')->bar", + 'Table->MetaData', + 'Column->Table', 't1.id->t1', + 'Column->Table', 't1.bar->t1', + 'ForeignKeyConstraint->Table', + 'ForeignKeyConstraint()->t1', + 't1->MetaData(None)', + 'Table->MetaData', 'Column->Table', + 't2.id->t2', 't2->MetaData(None)'] + ) +