diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-11-13 15:53:32 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-11-13 15:53:32 -0500 |
| commit | 6d630990dd35dad35609171eb003a0f1ef5b945d (patch) | |
| tree | 080b3b7a7b4e7a25ec1a66612eec22cbd6049cc7 | |
| parent | 8303afc9c002bade1dd2736a26f302475da9c398 (diff) | |
| parent | af4285e6adf2a052ce985f9d3d97cc89778fca96 (diff) | |
| download | sqlalchemy-6d630990dd35dad35609171eb003a0f1ef5b945d.tar.gz | |
merge tip
26 files changed, 364 insertions, 132 deletions
@@ -19,12 +19,33 @@ CHANGES is an outer join, thus allowing primary rows without a referenced child row to be correctly returned in results. [ticket:1954] + + - Fixed bug regarding "subqueryload" strategy whereby + strategy would fail if the entity was an aliased() + construct. [ticket:1964] + +- engine + - Implemented sequence check capability for the C + version of RowProxy, as well as 2.7 style + "collections.Sequence" registration for RowProxy. + [ticket:1871] - mysql - Fixed error handling for Jython + zxjdbc, such that has_table() property works again. Regression from 0.6.3 (we don't have a Jython buildbot, sorry) [ticket:1960] + +- sqlite + - The REFERENCES clause in a CREATE TABLE that includes + a remote schema to another table with the same schema + name now renders the remote name without + the schema clause, as required by SQLite. [ticket:1851] + + - On the same theme, the REFERENCES clause in a CREATE TABLE + that includes a remote schema to a *different* schema + than that of the parent table doesn't render at all, + as cross-schema references do not appear to be supported. 0.6.5 ===== diff --git a/README.unittests b/README.unittests index ca7699743..6b2320907 100644 --- a/README.unittests +++ b/README.unittests @@ -68,6 +68,16 @@ intersesting: $ nosetests -v +ALTERNATE TEST RUNNER +--------------------- + +The script "sqla_nose.py" is a front-end to Nose which manually associates +the SQLAlchemy testing plugin with Nose at runtime. This script can run the +tests without any reliance upon setuptools. In 0.7 we'll be removing the +Nose plugin from setup, so this will be the way going forward to run tests: + + $ python sqla_nose.py -v + RUNNING INDIVIDUAL TESTS ------------------------- Any directory of test modules can be run at once by specifying the directory diff --git a/doc/build/orm/mapper_config.rst b/doc/build/orm/mapper_config.rst index 15377436f..954d43646 100644 --- a/doc/build/orm/mapper_config.rst +++ b/doc/build/orm/mapper_config.rst @@ -570,6 +570,7 @@ passed in to a mapper as the table. .. sourcecode:: python+sql + from sqlalchemy.orm import mapper from sqlalchemy.sql import join class AddressUser(object): @@ -584,9 +585,29 @@ passed in to a mapper as the table. 'user_id': [users_table.c.user_id, addresses_table.c.user_id] }) -A second example: +Note that the list of columns is equivalent to the usage of :func:`.column_property` +with multiple columns:: -.. sourcecode:: python+sql + from sqlalchemy.orm import mapper, column_property + + mapper(AddressUser, j, properties={ + 'user_id': column_property(users_table.c.user_id, addresses_table.c.user_id) + }) + +The usage of :func:`.column_property` is required when using declarative to map +to multiple columns, since the declarative class parser won't recognize a plain +list of columns:: + + from sqlalchemy.ext.declarative import declarative_base + + Base = declarative_base() + + class AddressUser(Base): + __table__ = j + + user_id = column_property(users_table.c.user_id, addresses_table.c.user_id) + +A second example:: from sqlalchemy.sql import join @@ -611,6 +632,7 @@ mappers; these are aggregations of multiple columns into one mapper property, which instructs the mapper to keep both of those columns set at the same value. + Mapping a Class against Arbitrary Selects ------------------------------------------ diff --git a/lib/sqlalchemy/cextension/resultproxy.c b/lib/sqlalchemy/cextension/resultproxy.c index 7404b9ed2..73e127345 100644 --- a/lib/sqlalchemy/cextension/resultproxy.c +++ b/lib/sqlalchemy/cextension/resultproxy.c @@ -327,6 +327,12 @@ BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key) } static PyObject * +BaseRowProxy_getitem(PyObject *self, Py_ssize_t i) +{ + return BaseRowProxy_subscript((BaseRowProxy*)self, PyInt_FromSsize_t(i)); +} + +static PyObject * BaseRowProxy_getattro(BaseRowProxy *self, PyObject *name) { PyObject *tmp; @@ -506,7 +512,7 @@ static PySequenceMethods BaseRowProxy_as_sequence = { (lenfunc)BaseRowProxy_length, /* sq_length */ 0, /* sq_concat */ 0, /* sq_repeat */ - 0, /* sq_item */ + (ssizeargfunc)BaseRowProxy_getitem, /* sq_item */ 0, /* sq_slice */ 0, /* sq_ass_item */ 0, /* sq_ass_slice */ diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index b84b18e68..994904b6a 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -270,7 +270,21 @@ class SQLiteDDLCompiler(compiler.DDLCompiler): return super(SQLiteDDLCompiler, self).\ visit_primary_key_constraint(constraint) - + + def visit_foreign_key_constraint(self, constraint): + + local_table = constraint._elements.values()[0].parent.table + remote_table = list(constraint._elements.values())[0].column.table + + if local_table.schema != remote_table.schema: + return None + else: + return super(SQLiteDDLCompiler, self).visit_foreign_key_constraint(constraint) + + def define_constraint_remote_table(self, constraint, table, preparer): + """Format the remote table clause of a CREATE CONSTRAINT clause.""" + + return preparer.format_table(table, use_schema=False) def visit_create_index(self, create): index = create.element diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 9d80af7a7..326c33f2d 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -2142,6 +2142,14 @@ class RowProxy(BaseRowProxy): def itervalues(self): return iter(self) +try: + # Register RowProxy with Sequence, + # so sequence protocol is implemented + from collections import Sequence + Sequence.register(RowProxy) +except ImportError: + pass + class ResultMetaData(object): """Handle cursor.description, applying additional info from an execution diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index fcaabfddd..6872dd645 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -19,10 +19,7 @@ from sqlalchemy import util, event from sqlalchemy.orm import interfaces, collections, events import sqlalchemy.exceptions as sa_exc -# lazy imports -_entity_info = None -identity_equal = None -state = None +mapperutil = util.importlater("sqlalchemy.orm", "util") PASSIVE_NO_RESULT = util.symbol('PASSIVE_NO_RESULT') ATTR_WAS_SET = util.symbol('ATTR_WAS_SET') @@ -542,7 +539,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): compare_function=compare_function, **kwargs) if compare_function is None: - self.is_equal = identity_equal + self.is_equal = mapperutil.identity_equal def delete(self, state, dict_): old = self.get(state, dict_) diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 7fdf21c6c..75ba0b5c0 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -31,6 +31,8 @@ from sqlalchemy.orm.util import _INSTRUMENTOR, _class_to_mapper, \ _state_mapper, class_mapper, instance_str, state_str import sys +sessionlib = util.importlater("sqlalchemy.orm", "session") +properties = util.importlater("sqlalchemy.orm", "properties") __all__ = ( 'Mapper', @@ -54,13 +56,6 @@ NO_ATTRIBUTE = util.symbol('NO_ATTRIBUTE') # lock used to synchronize the "mapper compile" step _COMPILE_MUTEX = util.threading.RLock() -# initialize these lazily -ColumnProperty = None -RelationshipProperty = None -ConcreteInheritedProperty = None -_expire_state = None -_state_session = None - class Mapper(object): """Define the correlation of class attributes to database table columns. @@ -590,7 +585,7 @@ class Mapper(object): self._configure_property( col.key, - ColumnProperty(col, _instrument=instrument), + properties.ColumnProperty(col, _instrument=instrument), init=False, setparent=True) def _adapt_inherited_property(self, key, prop, init): @@ -599,7 +594,7 @@ class Mapper(object): elif key not in self._props: self._configure_property( key, - ConcreteInheritedProperty(), + properties.ConcreteInheritedProperty(), init=init, setparent=True) def _configure_property(self, key, prop, init=True, setparent=True): @@ -607,7 +602,7 @@ class Mapper(object): if not isinstance(prop, MapperProperty): # we were passed a Column or a list of Columns; - # generate a ColumnProperty + # generate a properties.ColumnProperty columns = util.to_list(prop) column = columns[0] if not expression.is_column(column): @@ -617,12 +612,12 @@ class Mapper(object): prop = self._props.get(key, None) - if isinstance(prop, ColumnProperty): + if isinstance(prop, properties.ColumnProperty): # TODO: the "property already exists" case is still not # well defined here. assuming single-column, etc. if prop.parent is not self: - # existing ColumnProperty from an inheriting mapper. + # existing properties.ColumnProperty from an inheriting mapper. # make a copy and append our column to it prop = prop.copy() else: @@ -637,9 +632,9 @@ class Mapper(object): # this hypothetically changes to # prop.columns.insert(0, column) when we do [ticket:1892] prop.columns.append(column) - self._log("appending to existing ColumnProperty %s" % (key)) + self._log("appending to existing properties.ColumnProperty %s" % (key)) - elif prop is None or isinstance(prop, ConcreteInheritedProperty): + elif prop is None or isinstance(prop, properties.ConcreteInheritedProperty): mapped_column = [] for c in columns: mc = self.mapped_table.corresponding_column(c) @@ -660,7 +655,7 @@ class Mapper(object): "force this column to be mapped as a read-only " "attribute." % (key, self, c)) mapped_column.append(mc) - prop = ColumnProperty(*mapped_column) + prop = properties.ColumnProperty(*mapped_column) else: raise sa_exc.ArgumentError( "WARNING: when configuring property '%s' on %s, " @@ -674,7 +669,7 @@ class Mapper(object): "columns get mapped." % (key, self, column.key, prop)) - if isinstance(prop, ColumnProperty): + if isinstance(prop, properties.ColumnProperty): col = self.mapped_table.corresponding_column(prop.columns[0]) # if the column is not present in the mapped table, @@ -713,7 +708,7 @@ class Mapper(object): col not in self._cols_by_table[col.table]: self._cols_by_table[col.table].add(col) - # if this ColumnProperty represents the "polymorphic + # if this properties.ColumnProperty represents the "polymorphic # discriminator" column, mark it. We'll need this when rendering # columns in SELECT statements. if not hasattr(prop, '_is_polymorphic_discriminator'): @@ -1887,7 +1882,7 @@ class Mapper(object): ) if readonly: - _expire_state(state, state.dict, readonly) + sessionlib._expire_state(state, state.dict, readonly) # if eager_defaults option is enabled, # refresh whatever has been expired. @@ -1925,7 +1920,7 @@ class Mapper(object): self._set_state_attr_by_column(state, dict_, c, params[c.key]) if postfetch_cols: - _expire_state(state, state.dict, + sessionlib._expire_state(state, state.dict, [self._columntoproperty[c].key for c in postfetch_cols] ) @@ -2430,7 +2425,7 @@ def _load_scalar_attributes(state, attribute_names): """initiate a column-based attribute refresh operation.""" mapper = _state_mapper(state) - session = _state_session(state) + session = sessionlib._state_session(state) if not session: raise orm_exc.DetachedInstanceError( "Instance %s is not bound to a Session; " diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 0cbbf630d..feee041ce 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -1483,6 +1483,3 @@ class RelationshipProperty(StrategizedProperty): PropertyLoader = RelationProperty = RelationshipProperty log.class_logger(RelationshipProperty) -mapper.ColumnProperty = ColumnProperty -mapper.RelationshipProperty = RelationshipProperty -mapper.ConcreteInheritedProperty = ConcreteInheritedProperty diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 710d3213a..c384cfc3d 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -1695,8 +1695,3 @@ def _state_session(state): pass return None -# Lazy initialization to avoid circular imports -unitofwork._state_session = _state_session -from sqlalchemy.orm import mapper -mapper._expire_state = _expire_state -mapper._state_session = _state_session diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 1de2dc3ee..17f165b64 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -741,14 +741,16 @@ class SubqueryLoader(AbstractRelationshipLoader): ("orig_query", SubqueryLoader), context.query) + subq_mapper = mapperutil._class_to_mapper(subq_path[0]) + # determine attributes of the leftmost mapper - if self.parent.isa(subq_path[0]) and self.key==subq_path[1]: + if self.parent.isa(subq_mapper) and self.key==subq_path[1]: leftmost_mapper, leftmost_prop = \ self.parent, self.parent_property else: leftmost_mapper, leftmost_prop = \ - subq_path[0], \ - subq_path[0].get_property(subq_path[1]) + subq_mapper, \ + subq_mapper.get_property(subq_path[1]) leftmost_cols, remote_cols = self._local_remote_columns(leftmost_prop) leftmost_attr = [ diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py index a9808e6ba..673591e8e 100644 --- a/lib/sqlalchemy/orm/unitofwork.py +++ b/lib/sqlalchemy/orm/unitofwork.py @@ -16,9 +16,7 @@ from sqlalchemy import util, topological from sqlalchemy.orm import attributes, interfaces from sqlalchemy.orm import util as mapperutil from sqlalchemy.orm.util import _state_mapper - -# Load lazily -_state_session = None +session = util.importlater("sqlalchemy.orm", "session") class UOWEventHandler(interfaces.AttributeExtension): """An event handler added to all relationship attributes which handles @@ -34,7 +32,7 @@ class UOWEventHandler(interfaces.AttributeExtension): # process "save_update" cascade rules for when # an instance is appended to the list of another instance - sess = _state_session(state) + sess = session._state_session(state) if sess: prop = _state_mapper(state).get_property(self.key) if prop.cascade.save_update and \ @@ -44,7 +42,7 @@ class UOWEventHandler(interfaces.AttributeExtension): return item def remove(self, state, item, initiator): - sess = _state_session(state) + sess = session._state_session(state) if sess: prop = _state_mapper(state).get_property(self.key) # expunge pending orphans @@ -59,7 +57,7 @@ class UOWEventHandler(interfaces.AttributeExtension): if oldvalue is newvalue: return newvalue - sess = _state_session(state) + sess = session._state_session(state) if sess: prop = _state_mapper(state).get_property(self.key) if newvalue is not None and \ diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index c2b79666c..9447eed30 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -13,7 +13,7 @@ from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE,\ AttributeExtension from sqlalchemy.orm import attributes, exc -mapperlib = None +mapperlib = util.importlater("sqlalchemy.orm", "mapperlib") all_cascades = frozenset(("delete", "delete-orphan", "all", "merge", "expunge", "save-update", "refresh-expire", @@ -460,10 +460,6 @@ def _entity_info(entity, compile=True): if isinstance(entity, AliasedClass): return entity._AliasedClass__mapper, entity._AliasedClass__alias, True - global mapperlib - if mapperlib is None: - from sqlalchemy.orm import mapperlib - if isinstance(entity, mapperlib.Mapper): mapper = entity @@ -558,24 +554,28 @@ def class_mapper(class_, compile=True): def _class_to_mapper(class_or_mapper, compile=True): if _is_aliased_class(class_or_mapper): return class_or_mapper._AliasedClass__mapper + elif isinstance(class_or_mapper, type): - return class_mapper(class_or_mapper, compile=compile) - elif hasattr(class_or_mapper, 'compile'): - if compile: - return class_or_mapper.compile() - else: - return class_or_mapper + try: + class_manager = attributes.manager_of_class(class_or_mapper) + mapper = class_manager.mapper + except exc.NO_STATE: + raise exc.UnmappedClassError(class_or_mapper) + elif isinstance(class_or_mapper, mapperlib.Mapper): + mapper = class_or_mapper else: raise exc.UnmappedClassError(class_or_mapper) + + if compile: + return mapper.compile() + else: + return mapper def has_identity(object): state = attributes.instance_state(object) return state.has_identity def _is_mapped_class(cls): - global mapperlib - if mapperlib is None: - from sqlalchemy.orm import mapperlib if isinstance(cls, (AliasedClass, mapperlib.Mapper)): return True if isinstance(cls, expression.ClauseElement): @@ -618,8 +618,3 @@ def identity_equal(a, b): return False return state_a.key == state_b.key - -# TODO: Avoid circular import. -attributes.identity_equal = identity_equal -attributes._is_aliased_class = _is_aliased_class -attributes._entity_info = _entity_info diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index 15b58e953..607b55d3f 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -33,7 +33,9 @@ from sqlalchemy import exc, util, dialects from sqlalchemy.sql import expression, visitors from sqlalchemy import event, events -URL = None +sqlutil = util.importlater("sqlalchemy.sql", "util") +url = util.importlater("sqlalchemy.engine", "url") + __all__ = ['SchemaItem', 'Table', 'Column', 'ForeignKey', 'Sequence', 'Index', 'ForeignKeyConstraint', 'PrimaryKeyConstraint', 'CheckConstraint', @@ -1957,11 +1959,7 @@ class MetaData(SchemaItem): def _bind_to(self, bind): """Bind this MetaData to an Engine, Connection, string or URL.""" - global URL - if URL is None: - from sqlalchemy.engine.url import URL - - if isinstance(bind, (basestring, URL)): + if isinstance(bind, (basestring, url.URL)): from sqlalchemy import create_engine self._bind = create_engine(bind) else: @@ -1985,8 +1983,7 @@ class MetaData(SchemaItem): """Returns a list of ``Table`` objects sorted in order of dependency. """ - from sqlalchemy.sql.util import sort_tables - return sort_tables(self.tables.itervalues()) + return sqlutil.sort_tables(self.tables.itervalues()) def reflect(self, bind=None, schema=None, views=False, only=None): """Load all available table definitions from the database. @@ -2156,11 +2153,7 @@ class ThreadLocalMetaData(MetaData): def _bind_to(self, bind): """Bind to a Connectable in the caller's thread.""" - global URL - if URL is None: - from sqlalchemy.engine.url import URL - - if isinstance(bind, (basestring, URL)): + if isinstance(bind, (basestring, url.URL)): try: self.context._engine = self.__engines[bind] except KeyError: diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index d3b8bf023..4b41c6ed3 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1306,7 +1306,7 @@ class DDLCompiler(engine.Compiled): text += "FOREIGN KEY(%s) REFERENCES %s (%s)" % ( ', '.join(preparer.quote(f.parent.name, f.parent.quote) for f in constraint._elements.values()), - preparer.format_table(remote_table), + self.define_constraint_remote_table(constraint, remote_table, preparer), ', '.join(preparer.quote(f.column.name, f.column.quote) for f in constraint._elements.values()) ) @@ -1314,6 +1314,11 @@ class DDLCompiler(engine.Compiled): text += self.define_constraint_deferrability(constraint) return text + def define_constraint_remote_table(self, constraint, table, preparer): + """Format the remote table clause of a CREATE CONSTRAINT clause.""" + + return preparer.format_table(table) + def visit_unique_constraint(self, constraint): text = "" if constraint.name is not None: diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 625893a68..c3dc339a5 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -29,13 +29,15 @@ to stay the same in future releases. import itertools, re from operator import attrgetter -from sqlalchemy import util, exc #, types as sqltypes +from sqlalchemy import util, exc from sqlalchemy.sql import operators from sqlalchemy.sql.visitors import Visitable, cloned_traverse import operator -functions, sql_util, sqltypes = None, None, None -DefaultDialect = None +functions = util.importlater("sqlalchemy.sql", "functions") +sqlutil = util.importlater("sqlalchemy.sql", "util") +sqltypes = util.importlater("sqlalchemy", "types") +default = util.importlater("sqlalchemy.engine", "default") __all__ = [ 'Alias', 'ClauseElement', 'ColumnCollection', 'ColumnElement', @@ -957,9 +959,6 @@ class _FunctionGenerator(object): o = self.opts.copy() o.update(kwargs) if len(self.__names) == 1: - global functions - if functions is None: - from sqlalchemy.sql import functions func = getattr(functions, self.__names[-1].lower(), None) if func is not None and \ isinstance(func, type) and \ @@ -1205,10 +1204,7 @@ class ClauseElement(Visitable): dictionary. """ - global sql_util - if sql_util is None: - from sqlalchemy.sql import util as sql_util - return sql_util.Annotated(self, values) + return sqlutil.Annotated(self, values) def _deannotate(self): """return a copy of this ClauseElement with an empty annotations @@ -1389,10 +1385,7 @@ class ClauseElement(Visitable): dialect = self.bind.dialect bind = self.bind else: - global DefaultDialect - if DefaultDialect is None: - from sqlalchemy.engine.default import DefaultDialect - dialect = DefaultDialect() + dialect = default.DefaultDialect() compiler = self._compiler(dialect, bind=bind, **kw) compiler.compile() return compiler @@ -2154,10 +2147,7 @@ class FromClause(Selectable): """ - global sql_util - if sql_util is None: - from sqlalchemy.sql import util as sql_util - return sql_util.ClauseAdapter(alias).traverse(self) + return sqlutil.ClauseAdapter(alias).traverse(self) def correspond_on_equivalents(self, column, equivalents): """Return corresponding_column for the given column, or if None @@ -3098,10 +3088,7 @@ class Join(FromClause): columns = [c for c in self.left.columns] + \ [c for c in self.right.columns] - global sql_util - if not sql_util: - from sqlalchemy.sql import util as sql_util - self._primary_key.extend(sql_util.reduce_columns( + self._primary_key.extend(sqlutil.reduce_columns( (c for c in columns if c.primary_key), self.onclause)) self._columns.update((col._label, col) for col in columns) self._foreign_keys.update(itertools.chain( @@ -3118,14 +3105,11 @@ class Join(FromClause): return self.left, self.right, self.onclause def _match_primaries(self, left, right): - global sql_util - if not sql_util: - from sqlalchemy.sql import util as sql_util if isinstance(left, Join): left_right = left.right else: left_right = None - return sql_util.join_condition(left, right, a_subset=left_right) + return sqlutil.join_condition(left, right, a_subset=left_right) def select(self, whereclause=None, fold_equivalents=False, **kwargs): """Create a :class:`Select` from this :class:`Join`. @@ -3145,11 +3129,8 @@ class Join(FromClause): underlying :func:`select()` function. """ - global sql_util - if not sql_util: - from sqlalchemy.sql import util as sql_util if fold_equivalents: - collist = sql_util.folded_equivalents(self) + collist = sqlutil.folded_equivalents(self) else: collist = [self.left, self.right] diff --git a/lib/sqlalchemy/test/requires.py b/lib/sqlalchemy/test/requires.py index 14c548f12..d29b7abc2 100644 --- a/lib/sqlalchemy/test/requires.py +++ b/lib/sqlalchemy/test/requires.py @@ -252,6 +252,12 @@ def sane_rowcount(fn): skip_if(lambda: not testing.db.dialect.supports_sane_rowcount) ) +def cextensions(fn): + return _chain_decorators_on( + fn, + skip_if(lambda: not _has_cextensions(), "C extensions not installed") + ) + def dbapi_lastrowid(fn): return _chain_decorators_on( fn, @@ -279,7 +285,23 @@ def python2(fn): "Python version 2.xx is required." ) ) + +def python26(fn): + return _chain_decorators_on( + fn, + skip_if( + lambda: sys.version_info < (2, 6), + "Python version 2.6 or greater is required" + ) + ) +def _has_cextensions(): + try: + from sqlalchemy import cresultproxy, cprocessors + return True + except ImportError: + return False + def _has_sqlite(): from sqlalchemy import create_engine try: diff --git a/lib/sqlalchemy/test/util.py b/lib/sqlalchemy/test/util.py index 98667d8c2..e5277f076 100644 --- a/lib/sqlalchemy/test/util.py +++ b/lib/sqlalchemy/test/util.py @@ -22,8 +22,6 @@ else: def lazy_gc(): pass - - def picklers(): picklers = set() # Py2K diff --git a/lib/sqlalchemy/types.py b/lib/sqlalchemy/types.py index ee1fdc67f..9f322d1eb 100644 --- a/lib/sqlalchemy/types.py +++ b/lib/sqlalchemy/types.py @@ -34,8 +34,8 @@ from sqlalchemy.sql.visitors import Visitable from sqlalchemy import util from sqlalchemy import processors import collections +default = util.importlater("sqlalchemy.engine", "default") -DefaultDialect = None NoneType = type(None) if util.jython: import array @@ -143,10 +143,7 @@ class AbstractType(Visitable): mod = ".".join(tokens) return getattr(__import__(mod).dialects, tokens[-1]).dialect() else: - global DefaultDialect - if DefaultDialect is None: - from sqlalchemy.engine.default import DefaultDialect - return DefaultDialect() + return default.DefaultDialect() def __str__(self): # Py3K diff --git a/lib/sqlalchemy/util.py b/lib/sqlalchemy/util.py index 7ac7660a5..c897d30f9 100644 --- a/lib/sqlalchemy/util.py +++ b/lib/sqlalchemy/util.py @@ -1563,7 +1563,51 @@ class group_expirable_memoized_property(object): self.attributes.append(fn.__name__) return memoized_property(fn) - +class importlater(object): + """Deferred import object. + + e.g.:: + + somesubmod = importlater("mypackage.somemodule", "somesubmod") + + is equivalent to:: + + from mypackage.somemodule import somesubmod + + except evaluted upon attribute access to "somesubmod". + + """ + def __init__(self, path, addtl=None): + self._il_path = path + self._il_addtl = addtl + + @memoized_property + def _il_module(self): + m = __import__(self._il_path) + for token in self._il_path.split(".")[1:]: + m = getattr(m, token) + if self._il_addtl: + try: + return getattr(m, self._il_addtl) + except AttributeError: + raise AttributeError( + "Module %s has no attribute '%s'" % + (self._il_path, self._il_addtl) + ) + else: + return m + + def __getattr__(self, key): + try: + attr = getattr(self._il_module, key) + except AttributeError: + raise AttributeError( + "Module %s has no attribute '%s'" % + (self._il_path, key) + ) + self.__dict__[key] = attr + return attr + class WeakIdentityMapping(weakref.WeakKeyDictionary): """A WeakKeyDictionary with an object identity index. diff --git a/sqla_nose.py b/sqla_nose.py index 6d76978a4..302fb5b08 100755 --- a/sqla_nose.py +++ b/sqla_nose.py @@ -2,9 +2,8 @@ """ nose runner script. -Only use this script if setuptools is not available, i.e. such as -on Python 3K. Otherwise consult README.unittests for the -recommended methods of running tests. +This script is a front-end to "nosetests" which doesn't +require that SQLA's testing plugin be installed via setuptools. """ import sys @@ -18,13 +17,5 @@ except ImportError: import nose - if __name__ == '__main__': - py3k = getattr(sys, 'py3kwarning', False) or sys.version_info >= (3, 0) - if py3k: - # this version breaks verbose output, - # but is the only API that nose3 currently supports - nose.main(plugins=[NoseSQLAlchemy()]) - else: - # this is the "correct" API - nose.main(addplugins=[NoseSQLAlchemy()]) + nose.main(addplugins=[NoseSQLAlchemy()]) diff --git a/test/aaa_profiling/test_compiler.py b/test/aaa_profiling/test_compiler.py index a7f64410b..bc589c0b2 100644 --- a/test/aaa_profiling/test_compiler.py +++ b/test/aaa_profiling/test_compiler.py @@ -5,6 +5,7 @@ from sqlalchemy.test import * class CompileTest(TestBase, AssertsExecutionResults): @classmethod def setup_class(cls): + global t1, t2, metadata metadata = MetaData() t1 = Table('t1', metadata, @@ -15,6 +16,10 @@ class CompileTest(TestBase, AssertsExecutionResults): Column('c1', Integer, primary_key=True), Column('c2', String(30))) + # do a "compile" ahead of time to load + # deferred imports + t1.insert().compile() + # go through all the TypeEngine # objects in use and pre-load their _type_affinity # entries. diff --git a/test/aaa_profiling/test_resultset.py b/test/aaa_profiling/test_resultset.py index d71b8dab8..bd9d3ae50 100644 --- a/test/aaa_profiling/test_resultset.py +++ b/test/aaa_profiling/test_resultset.py @@ -30,13 +30,13 @@ class ResultSetTest(TestBase, AssertsExecutionResults): metadata.drop_all() @profiling.function_call_count(14416, versions={'2.4': 13214, - '2.6+cextension': 409}) + '2.6+cextension': 409, '2.7+cextension':438}) def test_string(self): [tuple(row) for row in t.select().execute().fetchall()] # sqlite3 returns native unicode. so shouldn't be an increase here. @profiling.function_call_count(14396, versions={'2.4': 13214, - '2.6+cextension': 409}) + '2.6+cextension': 409, '2.7+cextension':409}) def test_unicode(self): [tuple(row) for row in t2.select().execute().fetchall()] diff --git a/test/dialect/test_sqlite.py b/test/dialect/test_sqlite.py index 0cdd3848e..19ec260d3 100644 --- a/test/dialect/test_sqlite.py +++ b/test/dialect/test_sqlite.py @@ -384,6 +384,57 @@ class SQLTest(TestBase, AssertsCompiledSQL): "SELECT CAST(STRFTIME('%s', t.col1) AS " "INTEGER) AS anon_1 FROM t" % subst) + def test_constraints_with_schemas(self): + metadata = MetaData() + t1 = Table('t1', metadata, + Column('id', Integer, primary_key=True), + schema='master') + t2 = Table('t2', metadata, + Column('id', Integer, primary_key=True), + Column('t1_id', Integer, ForeignKey('master.t1.id')), + schema='master' + ) + t3 = Table('t3', metadata, + Column('id', Integer, primary_key=True), + Column('t1_id', Integer, ForeignKey('master.t1.id')), + schema='alternate' + ) + t4 = Table('t4', metadata, + Column('id', Integer, primary_key=True), + Column('t1_id', Integer, ForeignKey('master.t1.id')), + ) + + # schema->schema, generate REFERENCES with no schema name + self.assert_compile( + schema.CreateTable(t2), + "CREATE TABLE master.t2 (" + "id INTEGER NOT NULL, " + "t1_id INTEGER, " + "PRIMARY KEY (id), " + "FOREIGN KEY(t1_id) REFERENCES t1 (id)" + ")" + ) + + # schema->different schema, don't generate REFERENCES + self.assert_compile( + schema.CreateTable(t3), + "CREATE TABLE alternate.t3 (" + "id INTEGER NOT NULL, " + "t1_id INTEGER, " + "PRIMARY KEY (id)" + ")" + ) + + # same for local schema + self.assert_compile( + schema.CreateTable(t4), + "CREATE TABLE t4 (" + "id INTEGER NOT NULL, " + "t1_id INTEGER, " + "PRIMARY KEY (id)" + ")" + ) + class InsertTest(TestBase, AssertsExecutionResults): diff --git a/test/engine/test_execute.py b/test/engine/test_execute.py index 5e6656431..9df23c92e 100644 --- a/test/engine/test_execute.py +++ b/test/engine/test_execute.py @@ -290,6 +290,38 @@ class ResultProxyTest(TestBase): finally: engine.dialect.execution_ctx_cls = execution_ctx_cls + @testing.requires.python26 + def test_rowproxy_is_sequence(self): + import collections + from sqlalchemy.engine import RowProxy + + row = RowProxy(object(), ['value'], [None], {'key' + : (None, 0), 0: (None, 0)}) + assert isinstance(row, collections.Sequence) + + @testing.requires.cextensions + def test_row_c_sequence_check(self): + import csv + import collections + from StringIO import StringIO + + metadata = MetaData() + metadata.bind = 'sqlite://' + users = Table('users', metadata, + Column('id', Integer, primary_key=True), + Column('name', String(40)), + ) + users.create() + + users.insert().execute(name='Test') + row = users.select().execute().fetchone() + + s = StringIO() + writer = csv.writer(s) + # csv performs PySequenceCheck call + writer.writerow(row) + assert s.getvalue().strip() == '1,Test' + class EngineEventsTest(TestBase): def _assert_stmts(self, expected, received): diff --git a/test/orm/test_subquery_relations.py b/test/orm/test_subquery_relations.py index 71f87a726..5eba68e13 100644 --- a/test/orm/test_subquery_relations.py +++ b/test/orm/test_subquery_relations.py @@ -4,7 +4,7 @@ from sqlalchemy.test.schema import Table, Column from sqlalchemy import Integer, String, ForeignKey, bindparam from sqlalchemy.orm import backref, subqueryload, subqueryload_all, \ mapper, relationship, clear_mappers, create_session, lazyload, \ - aliased, joinedload, deferred, undefer + aliased, joinedload, deferred, undefer, eagerload_all from sqlalchemy.test.testing import eq_, assert_raises, \ assert_raises_message from sqlalchemy.test.assertsql import CompiledSQL @@ -43,6 +43,59 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL): self.assert_sql_count(testing.db, go, 2) @testing.resolve_artifact_names + def test_from_aliased(self): + mapper(Dingaling, dingalings) + mapper(Address, addresses, properties={ + 'dingalings':relationship(Dingaling, order_by=Dingaling.id) + }) + mapper(User, users, properties={ + 'addresses':relationship( + Address, + order_by=Address.id) + }) + sess = create_session() + + u = aliased(User) + + q = sess.query(u).options(subqueryload(u.addresses)) + + def go(): + eq_( + [User(id=7, addresses=[ + Address(id=1, email_address='jack@bean.com')])], + q.filter(u.id==7).all() + ) + + self.assert_sql_count(testing.db, go, 2) + + def go(): + eq_( + self.static.user_address_result, + q.order_by(u.id).all() + ) + self.assert_sql_count(testing.db, go, 2) + + q = sess.query(u).\ + options(subqueryload_all(u.addresses, Address.dingalings)) + + def go(): + eq_( + [ + User(id=8, addresses=[ + Address(id=2, email_address='ed@wood.com', dingalings=[Dingaling()]), + Address(id=3, email_address='ed@bettyboop.com'), + Address(id=4, email_address='ed@lala.com'), + ]), + User(id=9, addresses=[ + Address(id=5, dingalings=[Dingaling()]) + ]), + ], + q.filter(u.id.in_([8, 9])).all() + ) + self.assert_sql_count(testing.db, go, 3) + + + @testing.resolve_artifact_names def test_from_get(self): mapper(User, users, properties={ 'addresses':relationship( |
