From dff7c2ad2c913ed0ec5979ff9470dd5dd5813483 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 18 Jul 2012 14:13:18 -0400 Subject: - document the inspection system --- lib/sqlalchemy/engine/reflection.py | 39 ++++--- lib/sqlalchemy/exc.py | 2 +- lib/sqlalchemy/inspection.py | 48 +++++++-- lib/sqlalchemy/orm/attributes.py | 202 +++++++++++++++++++----------------- lib/sqlalchemy/orm/state.py | 70 ++++++++++++- lib/sqlalchemy/orm/util.py | 59 +++++++++-- lib/sqlalchemy/util/langhelpers.py | 29 +++--- 7 files changed, 307 insertions(+), 142 deletions(-) (limited to 'lib') diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index 3a12819f1..e1d702146 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -40,8 +40,8 @@ def cache(fn, self, con, *args, **kw): if info_cache is None: return fn(self, con, *args, **kw) key = ( - fn.__name__, - tuple(a for a in args if isinstance(a, basestring)), + fn.__name__, + tuple(a for a in args if isinstance(a, basestring)), tuple((k, v) for k, v in kw.iteritems() if isinstance(v, (basestring, int, float))) ) ret = info_cache.get(key) @@ -59,8 +59,15 @@ class Inspector(object): consistent interface as well as caching support for previously fetched metadata. - The preferred method to construct an :class:`.Inspector` is via the - :meth:`Inspector.from_engine` method. I.e.:: + A :class:`.Inspector` object is usually created via the + :func:`.inspect` function:: + + from sqlalchemy import inspect, create_engine + engine = create_engine('...') + insp = inspect(engine) + + The inspection method above is equivalent to using the + :meth:`.Inspector.from_engine` method, i.e.:: engine = create_engine('...') insp = Inspector.from_engine(engine) @@ -74,9 +81,9 @@ class Inspector(object): def __init__(self, bind): """Initialize a new :class:`.Inspector`. - :param bind: a :class:`~sqlalchemy.engine.base.Connectable`, - which is typically an instance of - :class:`~sqlalchemy.engine.base.Engine` or + :param bind: a :class:`~sqlalchemy.engine.base.Connectable`, + which is typically an instance of + :class:`~sqlalchemy.engine.base.Engine` or :class:`~sqlalchemy.engine.base.Connection`. For a dialect-specific instance of :class:`.Inspector`, see @@ -103,9 +110,9 @@ class Inspector(object): def from_engine(cls, bind): """Construct a new dialect-specific Inspector object from the given engine or connection. - :param bind: a :class:`~sqlalchemy.engine.base.Connectable`, - which is typically an instance of - :class:`~sqlalchemy.engine.base.Engine` or + :param bind: a :class:`~sqlalchemy.engine.base.Connectable`, + which is typically an instance of + :class:`~sqlalchemy.engine.base.Engine` or :class:`~sqlalchemy.engine.base.Connection`. This method differs from direct a direct constructor call of :class:`.Inspector` @@ -322,7 +329,7 @@ class Inspector(object): def reflecttable(self, table, include_columns, exclude_columns=()): """Given a Table object, load its internal constructs based on introspection. - This is the underlying method used by most dialects to produce + This is the underlying method used by most dialects to produce table reflection. Direct usage is like:: from sqlalchemy import create_engine, MetaData, Table @@ -416,11 +423,11 @@ class Inspector(object): # Primary keys pk_cons = self.get_pk_constraint(table_name, schema, **tblkw) if pk_cons: - pk_cols = [table.c[pk] - for pk in pk_cons['constrained_columns'] + pk_cols = [table.c[pk] + for pk in pk_cons['constrained_columns'] if pk in table.c and pk not in exclude_columns ] + [pk for pk in table.primary_key if pk.key in exclude_columns] - primary_key_constraint = sa_schema.PrimaryKeyConstraint(name=pk_cons.get('name'), + primary_key_constraint = sa_schema.PrimaryKeyConstraint(name=pk_cons.get('name'), *pk_cols ) @@ -457,7 +464,7 @@ class Inspector(object): table.append_constraint( sa_schema.ForeignKeyConstraint(constrained_columns, refspec, conname, link_to_name=True)) - # Indexes + # Indexes indexes = self.get_indexes(table_name, schema) for index_d in indexes: name = index_d['name'] @@ -470,5 +477,5 @@ class Inspector(object): "Omitting %s KEY for (%s), key covers omitted columns." % (flavor, ', '.join(columns))) continue - sa_schema.Index(name, *[table.columns[c] for c in columns], + sa_schema.Index(name, *[table.columns[c] for c in columns], **dict(unique=unique)) diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index 0d69795d1..f9f97718c 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -96,7 +96,7 @@ class InvalidRequestError(SQLAlchemyError): """ class NoInspectionAvailable(InvalidRequestError): - """A class to :func:`sqlalchemy.inspection.inspect` produced + """A subject passed to :func:`sqlalchemy.inspection.inspect` produced no context for inspection.""" class ResourceClosedError(InvalidRequestError): diff --git a/lib/sqlalchemy/inspection.py b/lib/sqlalchemy/inspection.py index 8b0a751ad..34a47217b 100644 --- a/lib/sqlalchemy/inspection.py +++ b/lib/sqlalchemy/inspection.py @@ -4,22 +4,54 @@ # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Base inspect API. +"""The inspection module provides the :func:`.inspect` function, +which delivers runtime information about a wide variety +of SQLAlchemy objects, both within the Core as well as the +ORM. -:func:`.inspect` provides access to a contextual object -regarding a subject. +The :func:`.inspect` function is the entry point to SQLAlchemy's +public API for viewing the configuration and construction +of in-memory objects. Depending on the type of object +passed to :func:`.inspect`, the return value will either be +a related object which provides a known interface, or in many +cases it will return the object itself. + +The rationale for :func:`.inspect` is twofold. One is that +it replaces the need to be aware of a large variety of "information +getting" functions in SQLAlchemy, such as :meth:`.Inspector.from_engine`, +:func:`.orm.attributes.instance_state`, :func:`.orm.class_mapper`, +and others. The other is that the return value of :func:`.inspect` +is guaranteed to obey a documented API, thus allowing third party +tools which build on top of SQLAlchemy configurations to be constructed +in a forwards-compatible way. + +.. versionadded:: 0.8 The :func:`.inspect` system is introduced + as of version 0.8. -Various subsections of SQLAlchemy, -such as the :class:`.Inspector`, :class:`.Mapper`, and -others register themselves with the "inspection registry" here -so that they may return a context object given a certain kind -of argument. """ from . import util, exc _registrars = util.defaultdict(list) def inspect(subject, raiseerr=True): + """Produce an inspection object for the given target. + + The returned value in some cases may be the + same object as the one given, such as if a + :class:`.orm.Mapper` object is passed. In other + cases, it will be an instance of the registered + inspection type for the given object, such as + if a :class:`.engine.Engine` is passed, an + :class:`.engine.Inspector` object is returned. + + :param subject: the subject to be inspected. + :param raiseerr: When ``True``, if the given subject + does not + correspond to a known SQLAlchemy inspected type, + :class:`sqlalchemy.exc.NoInspectionAvailable` + is raised. If ``False``, ``None`` is returned. + + """ type_ = type(subject) for cls in type_.__mro__: if cls in _registrars: diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 0bf9ea438..9a1c60aa7 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -52,7 +52,7 @@ indicating that the attribute had not been assigned to previously. """ ) -NO_CHANGE = util.symbol("NO_CHANGE", +NO_CHANGE = util.symbol("NO_CHANGE", """No callables or SQL should be emitted on attribute access and no state should change""", canonical=0 ) @@ -80,29 +80,42 @@ value can be obtained. ) NON_PERSISTENT_OK = util.symbol("NON_PERSISTENT_OK", -"""callables can be emitted if the parent is not persistent.""", +"""callables can be emitted if the parent is not persistent.""", canonical=16 ) - # pre-packaged sets of flags used as inputs -PASSIVE_OFF = RELATED_OBJECT_OK | \ - NON_PERSISTENT_OK | \ - INIT_OK | \ - CALLABLES_OK | \ - SQL_OK - -PASSIVE_RETURN_NEVER_SET = PASSIVE_OFF ^ INIT_OK -PASSIVE_NO_INITIALIZE = PASSIVE_RETURN_NEVER_SET ^ CALLABLES_OK -PASSIVE_NO_FETCH = PASSIVE_OFF ^ SQL_OK -PASSIVE_NO_FETCH_RELATED = PASSIVE_OFF ^ RELATED_OBJECT_OK -PASSIVE_ONLY_PERSISTENT = PASSIVE_OFF ^ NON_PERSISTENT_OK +PASSIVE_OFF = util.symbol("PASSIVE_OFF", + "Callables can be emitted in all cases.", + canonical=(RELATED_OBJECT_OK | NON_PERSISTENT_OK | + INIT_OK | CALLABLES_OK | SQL_OK) +) +PASSIVE_RETURN_NEVER_SET = util.symbol("PASSIVE_RETURN_NEVER_SET", + """PASSIVE_OFF ^ INIT_OK""", + canonical=PASSIVE_OFF ^ INIT_OK +) +PASSIVE_NO_INITIALIZE = util.symbol("PASSIVE_NO_INITIALIZE", + "PASSIVE_RETURN_NEVER_SET ^ CALLABLES_OK", + canonical=PASSIVE_RETURN_NEVER_SET ^ CALLABLES_OK +) +PASSIVE_NO_FETCH = util.symbol("PASSIVE_NO_FETCH", + "PASSIVE_OFF ^ SQL_OK", + canonical=PASSIVE_OFF ^ SQL_OK +) +PASSIVE_NO_FETCH_RELATED = util.symbol("PASSIVE_NO_FETCH_RELATED", + "PASSIVE_OFF ^ RELATED_OBJECT_OK", + canonical=PASSIVE_OFF ^ RELATED_OBJECT_OK +) +PASSIVE_ONLY_PERSISTENT = util.symbol("PASSIVE_ONLY_PERSISTENT", + "PASSIVE_OFF ^ NON_PERSISTENT_OK", + canonical=PASSIVE_OFF ^ NON_PERSISTENT_OK +) class QueryableAttribute(interfaces.PropComparator): """Base class for class-bound attributes. """ - def __init__(self, class_, key, impl=None, + def __init__(self, class_, key, impl=None, comparator=None, parententity=None, of_type=None): self.class_ = class_ @@ -115,7 +128,7 @@ class QueryableAttribute(interfaces.PropComparator): manager = manager_of_class(class_) # manager is None in the case of AliasedClass if manager: - # propagate existing event listeners from + # propagate existing event listeners from # immediate superclass for base in manager._bases: if key in base: @@ -166,8 +179,8 @@ class QueryableAttribute(interfaces.PropComparator): except AttributeError: raise AttributeError( 'Neither %r object nor %r object has an attribute %r' % ( - type(self).__name__, - type(self.comparator).__name__, + type(self).__name__, + type(self.comparator).__name__, key) ) @@ -187,7 +200,7 @@ class InstrumentedAttribute(QueryableAttribute): """Class bound instrumented attribute which adds descriptor methods.""" def __set__(self, instance, value): - self.impl.set(instance_state(instance), + self.impl.set(instance_state(instance), instance_dict(instance), value, None) def __delete__(self, instance): @@ -215,13 +228,13 @@ def create_proxied_attribute(descriptor): class Proxy(QueryableAttribute): """Presents the :class:`.QueryableAttribute` interface as a - proxy on top of a Python descriptor / :class:`.PropComparator` + proxy on top of a Python descriptor / :class:`.PropComparator` combination. """ - def __init__(self, class_, key, descriptor, - comparator, + def __init__(self, class_, key, descriptor, + comparator, adapter=None, doc=None, original_property=None): self.class_ = class_ @@ -272,8 +285,8 @@ def create_proxied_attribute(descriptor): except AttributeError: raise AttributeError( 'Neither %r object nor %r object has an attribute %r' % ( - type(descriptor).__name__, - type(self.comparator).__name__, + type(descriptor).__name__, + type(self.comparator).__name__, attribute) ) @@ -289,7 +302,7 @@ class AttributeImpl(object): def __init__(self, class_, key, callable_, dispatch, trackparent=False, extension=None, - compare_function=None, active_history=False, + compare_function=None, active_history=False, parent_token=None, expire_missing=True, **kwargs): """Construct an AttributeImpl. @@ -326,12 +339,12 @@ class AttributeImpl(object): parent_token Usually references the MapperProperty, used as a key for the hasparent() function to identify an "owning" attribute. - Allows multiple AttributeImpls to all match a single + Allows multiple AttributeImpls to all match a single owner attribute. expire_missing if False, don't add an "expiry" callable to this attribute - during state.expire_attributes(None), if no value is present + during state.expire_attributes(None), if no value is present for this key. """ @@ -370,7 +383,7 @@ class AttributeImpl(object): def hasparent(self, state, optimistic=False): - """Return the boolean value of a `hasparent` flag attached to + """Return the boolean value of a `hasparent` flag attached to the given state. The `optimistic` flag determines what the default return value @@ -414,8 +427,8 @@ class AttributeImpl(object): "state %s along attribute '%s', " "but the parent record " "has gone stale, can't be sure this " - "is the most recent parent." % - (orm_util.state_str(state), + "is the most recent parent." % + (orm_util.state_str(state), orm_util.state_str(parent_state), self.key)) @@ -445,8 +458,8 @@ class AttributeImpl(object): raise NotImplementedError() def get_all_pending(self, state, dict_): - """Return a list of tuples of (state, obj) - for all objects in this attribute's current state + """Return a list of tuples of (state, obj) + for all objects in this attribute's current state + history. Only applies to object-based attributes. @@ -455,8 +468,8 @@ class AttributeImpl(object): which roughly corresponds to: get_state_history( - state, - key, + state, + key, passive=PASSIVE_NO_INITIALIZE).sum() """ @@ -517,14 +530,14 @@ class AttributeImpl(object): self.set(state, dict_, value, initiator, passive=passive) def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - self.set(state, dict_, None, initiator, + self.set(state, dict_, None, initiator, passive=passive, check_old=value) def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF): - self.set(state, dict_, None, initiator, + self.set(state, dict_, None, initiator, passive=passive, check_old=value, pop=True) - def set(self, state, dict_, value, initiator, + def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF, check_old=None, pop=False): raise NotImplementedError() @@ -571,7 +584,7 @@ class ScalarAttributeImpl(AttributeImpl): return History.from_scalar_attribute( self, state, dict_.get(self.key, NO_VALUE)) - def set(self, state, dict_, value, initiator, + def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF, check_old=None, pop=False): if initiator and initiator.parent_token is self.parent_token: return @@ -582,7 +595,7 @@ class ScalarAttributeImpl(AttributeImpl): old = dict_.get(self.key, NO_VALUE) if self.dispatch.set: - value = self.fire_replace_event(state, dict_, + value = self.fire_replace_event(state, dict_, value, old, initiator) state._modified_event(dict_, self, old) dict_[self.key] = value @@ -604,7 +617,7 @@ class ScalarAttributeImpl(AttributeImpl): class ScalarObjectAttributeImpl(ScalarAttributeImpl): - """represents a scalar-holding InstrumentedAttribute, + """represents a scalar-holding InstrumentedAttribute, where the target object is also instrumented. Adds events to delete/set operations. @@ -650,7 +663,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): else: return [] - def set(self, state, dict_, value, initiator, + def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF, check_old=None, pop=False): """Set a value on the given InstanceState. @@ -729,12 +742,12 @@ class CollectionAttributeImpl(AttributeImpl): typecallable=None, trackparent=False, extension=None, copy_function=None, compare_function=None, **kwargs): super(CollectionAttributeImpl, self).__init__( - class_, - key, + class_, + key, callable_, dispatch, trackparent=trackparent, extension=extension, - compare_function=compare_function, + compare_function=compare_function, **kwargs) if copy_function is None: @@ -762,11 +775,11 @@ class CollectionAttributeImpl(AttributeImpl): if self.key in state.committed_state: original = state.committed_state[self.key] if original is not NO_VALUE: - current_states = [((c is not None) and - instance_state(c) or None, c) + current_states = [((c is not None) and + instance_state(c) or None, c) for c in current] - original_states = [((c is not None) and - instance_state(c) or None, c) + original_states = [((c is not None) and + instance_state(c) or None, c) for c in original] current_set = dict(current_states) @@ -853,13 +866,13 @@ class CollectionAttributeImpl(AttributeImpl): def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF): try: # TODO: better solution here would be to add - # a "popper" role to collections.py to complement + # a "popper" role to collections.py to complement # "remover". self.remove(state, dict_, value, initiator, passive=passive) except (ValueError, KeyError, IndexError): pass - def set(self, state, dict_, value, initiator, + def set(self, state, dict_, value, initiator, passive=PASSIVE_OFF, pop=False): """Set a value on the given object. @@ -938,7 +951,7 @@ class CollectionAttributeImpl(AttributeImpl): return user_data - def get_collection(self, state, dict_, + def get_collection(self, state, dict_, user_data=None, passive=PASSIVE_OFF): """Retrieve the CollectionAdapter associated with the given state. @@ -967,19 +980,19 @@ def backref_listeners(attribute, key, uselist): old_state, old_dict = instance_state(oldchild),\ instance_dict(oldchild) impl = old_state.manager[key].impl - impl.pop(old_state, - old_dict, - state.obj(), + impl.pop(old_state, + old_dict, + state.obj(), initiator, passive=PASSIVE_NO_FETCH) if child is not None: child_state, child_dict = instance_state(child),\ instance_dict(child) child_state.manager[key].impl.append( - child_state, - child_dict, - state.obj(), - initiator, + child_state, + child_dict, + state.obj(), + initiator, passive=PASSIVE_NO_FETCH) return child @@ -987,10 +1000,10 @@ def backref_listeners(attribute, key, uselist): child_state, child_dict = instance_state(child), \ instance_dict(child) child_state.manager[key].impl.append( - child_state, - child_dict, - state.obj(), - initiator, + child_state, + child_dict, + state.obj(), + initiator, passive=PASSIVE_NO_FETCH) return child @@ -999,29 +1012,29 @@ def backref_listeners(attribute, key, uselist): child_state, child_dict = instance_state(child),\ instance_dict(child) child_state.manager[key].impl.pop( - child_state, - child_dict, - state.obj(), + child_state, + child_dict, + state.obj(), initiator, passive=PASSIVE_NO_FETCH) if uselist: - event.listen(attribute, "append", - emit_backref_from_collection_append_event, + event.listen(attribute, "append", + emit_backref_from_collection_append_event, retval=True, raw=True) else: - event.listen(attribute, "set", - emit_backref_from_scalar_set_event, + event.listen(attribute, "set", + emit_backref_from_scalar_set_event, retval=True, raw=True) # TODO: need coverage in test/orm/ of remove event - event.listen(attribute, "remove", - emit_backref_from_collection_remove_event, + event.listen(attribute, "remove", + emit_backref_from_collection_remove_event, retval=True, raw=True) _NO_HISTORY = util.symbol('NO_HISTORY') _NO_STATE_SYMBOLS = frozenset([ - id(PASSIVE_NO_RESULT), - id(NO_VALUE), + id(PASSIVE_NO_RESULT), + id(NO_VALUE), id(NEVER_SET)]) class History(tuple): """A 3-tuple of added, unchanged and deleted values, @@ -1062,7 +1075,7 @@ class History(tuple): return not bool( (self.added or self.deleted) or self.unchanged and self.unchanged != [None] - ) + ) def sum(self): """Return a collection of added + unchanged + deleted.""" @@ -1114,7 +1127,7 @@ class History(tuple): elif attribute.is_equal(current, original) is True: return cls((), [current], ()) else: - # current convention on native scalars is to not + # current convention on native scalars is to not # include information # about missing previous value in "deleted", but # we do include None, which helps in some primary @@ -1140,11 +1153,11 @@ class History(tuple): elif current is original: return cls((), [current], ()) else: - # current convention on related objects is to not + # current convention on related objects is to not # include information # about missing previous value in "deleted", and # to also not include None - the dependency.py rules - # ignore the None in any case. + # ignore the None in any case. if id(original) in _NO_STATE_SYMBOLS or original is None: deleted = () else: @@ -1165,11 +1178,11 @@ class History(tuple): return cls((), list(current), ()) else: - current_states = [((c is not None) and instance_state(c) or None, c) - for c in current + current_states = [((c is not None) and instance_state(c) or None, c) + for c in current ] - original_states = [((c is not None) and instance_state(c) or None, c) - for c in original + original_states = [((c is not None) and instance_state(c) or None, c) + for c in original ] current_set = dict(current_states) @@ -1184,7 +1197,7 @@ class History(tuple): HISTORY_BLANK = History(None, None, None) def get_history(obj, key, passive=PASSIVE_OFF): - """Return a :class:`.History` record for the given object + """Return a :class:`.History` record for the given object and attribute key. :param obj: an object whose class is instrumented by the @@ -1192,10 +1205,11 @@ def get_history(obj, key, passive=PASSIVE_OFF): :param key: string attribute name. - :param passive: indicates if the attribute should be - loaded from the database if not already present (:attr:`.PASSIVE_NO_FETCH`), and - if the attribute should be not initialized to a blank value otherwise - (:attr:`.PASSIVE_NO_INITIALIZE`). Default is :attr:`PASSIVE_OFF`. + :param passive: indicates loading behavior for the attribute + if the value is not already present. This is a + bitflag attribute, which defaults to the symbol + :attr:`.PASSIVE_OFF` indicating all necessary SQL + should be emitted. """ if passive is True: @@ -1223,14 +1237,14 @@ def register_attribute(class_, key, **kw): comparator = kw.pop('comparator', None) parententity = kw.pop('parententity', None) doc = kw.pop('doc', None) - desc = register_descriptor(class_, key, + desc = register_descriptor(class_, key, comparator, parententity, doc=doc) register_attribute_impl(class_, key, **kw) return desc def register_attribute_impl(class_, key, - uselist=False, callable_=None, - useobject=False, + uselist=False, callable_=None, + useobject=False, impl_class=None, backref=None, **kw): manager = manager_of_class(class_) @@ -1262,7 +1276,7 @@ def register_attribute_impl(class_, key, manager.post_configure_attribute(key) return manager[key] -def register_descriptor(class_, key, comparator=None, +def register_descriptor(class_, key, comparator=None, parententity=None, doc=None): manager = manager_of_class(class_) @@ -1288,10 +1302,10 @@ def init_collection(obj, key): collection_adapter.append_without_event(elem) For an easier way to do the above, see - :func:`~sqlalchemy.orm.attributes.set_committed_value`. + :func:`~sqlalchemy.orm.attributes.set_committed_value`. obj is an instrumented object instance. An InstanceState - is accepted directly for backwards compatibility but + is accepted directly for backwards compatibility but this usage is deprecated. """ @@ -1309,7 +1323,7 @@ def init_state_collection(state, dict_, key): def set_committed_value(instance, key, value): """Set the value of an attribute with no history events. - Cancels any previous history present. The value should be + Cancels any previous history present. The value should be a scalar value for scalar-holding attributes, or an iterable for any collection-holding attribute. @@ -1366,7 +1380,7 @@ def del_attribute(instance, key): def flag_modified(instance, key): """Mark an attribute on an instance as 'modified'. - This sets the 'modified' flag on the instance and + This sets the 'modified' flag on the instance and establishes an unconditional change event for the given attribute. """ diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 2b846832e..17ffa9e7a 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -50,6 +50,13 @@ class InstanceState(interfaces._InspectionAttr): @util.memoized_property def attr(self): + """Return a namespace representing each attribute on + the mapped object, including its current value + and history. + + The returned object is an instance of :class:`.InspectAttr`. + + """ return util.ImmutableProperties( dict( (key, InspectAttr(self, key)) @@ -59,21 +66,25 @@ class InstanceState(interfaces._InspectionAttr): @property def transient(self): + """Return true if the object is transient.""" return self.key is None and \ not self._attached @property def pending(self): + """Return true if the object is pending.""" return self.key is None and \ self._attached @property def persistent(self): + """Return true if the object is persistent.""" return self.key is not None and \ self._attached @property def detached(self): + """Return true if the object is detached.""" return self.key is not None and \ not self._attached @@ -84,14 +95,32 @@ class InstanceState(interfaces._InspectionAttr): @property def session(self): + """Return the owning :class:`.Session` for this instance, + or ``None`` if none available.""" + return sessionlib._state_session(self) @property def object(self): + """Return the mapped object represented by this + :class:`.InstanceState`.""" return self.obj() @property def identity(self): + """Return the mapped identity of the mapped object. + This is the primary key identity as persisted by the ORM + which can always be passed directly to + :meth:`.Query.get`. + + Returns ``None`` if the object has no primary key identity. + + .. note:: + An object which is transient or pending + does **not** have a mapped identity until it is flushed, + even if its attributes include primary key values. + + """ if self.key is None: return None else: @@ -99,6 +128,14 @@ class InstanceState(interfaces._InspectionAttr): @property def identity_key(self): + """Return the identity key for the mapped object. + + This is the key used to locate the object within + the :attr:`.Session.identity_map` mapping. It contains + the identity as returned by :attr:`.identity` within it. + + + """ # TODO: just change .key to .identity_key across # the board ? probably return self.key @@ -113,10 +150,17 @@ class InstanceState(interfaces._InspectionAttr): @util.memoized_property def mapper(self): + """Return the :class:`.Mapper` used for this mapepd object.""" return self.manager.mapper @property def has_identity(self): + """Return ``True`` if this object has an identity key. + + This should always have the same value as the + expression ``state.persistent or state.detached``. + + """ return bool(self.key) def _detach(self): @@ -465,7 +509,14 @@ class InstanceState(interfaces._InspectionAttr): state._strong_obj = None class InspectAttr(object): - """Provide inspection interface to an object's state.""" + """Provide an inspection interface corresponding + to a particular attribute on a particular mapped object. + + The :class:`.InspectAttr` object is created by + accessing the :attr:`.InstanceState.attr` + collection. + + """ def __init__(self, state, key): self.state = state @@ -473,15 +524,32 @@ class InspectAttr(object): @property def loaded_value(self): + """The current value of this attribute as loaded from the database. + + If the value has not been loaded, or is otherwise not present + in the object's dictionary, returns NO_VALUE. + + """ return self.state.dict.get(self.key, NO_VALUE) @property def value(self): + """Return the value of this attribute. + + This operation is equivalent to accessing the object's + attribute directly or via ``getattr()``, and will fire + off any pending loader callables if needed. + + """ return self.state.manager[self.key].__get__( self.state.obj(), self.state.class_) @property def history(self): + """Return the current pre-flush change history for + this attribute, via the :class:`.History` interface. + + """ return self.state.get_history(self.key, PASSIVE_NO_INITIALIZE) diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 517f1acb4..97bf4e7a9 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -563,7 +563,40 @@ AliasedInsp = util.namedtuple("AliasedInsp", [ ]) class AliasedInsp(_InspectionAttr, AliasedInsp): + """Provide an inspection interface for an + :class:`.AliasedClass` object. + + The :class:`.AliasedInsp` object is returned + given an :class:`.AliasedClass` using the + :func:`.inspect` function:: + + from sqlalchemy import inspect + from sqlalchemy.orm import aliased + + my_alias = aliased(MyMappedClass) + insp = inspect(my_alias) + + Attributes on :class:`.AliasedInsp` + include: + + * ``entity`` - the :class:`.AliasedClass` represented. + * ``mapper`` - the :class:`.Mapper` mapping the underlying class. + * ``selectable`` - the :class:`.Alias` construct which ultimately + represents an aliased :class:`.Table` or :class:`.Select` + construct. + * ``name`` - the name of the alias. Also is used as the attribute + name when returned in a result tuple from :class:`.Query`. + * ``with_polymorphic_mappers`` - collection of :class:`.Mapper` objects + indicating all those mappers expressed in the select construct + for the :class:`.AliasedClass`. + * ``polymorphic_on`` - an alternate column or SQL expression which + will be used as the "discriminator" for a polymorphic load. + + """ + is_aliased_class = True + "always returns True" + inspection._inspects(AliasedClass)(lambda target: target._aliased_insp) @@ -864,28 +897,35 @@ def object_mapper(instance): """Given an object, return the primary Mapper associated with the object instance. - Raises UnmappedInstanceError if no mapping is configured. + Raises :class:`sqlalchemy.orm.exc.UnmappedInstanceError` + if no mapping is configured. This function is available via the inspection system as:: inspect(instance).mapper + Using the inspection system will raise plain + :class:`sqlalchemy.exc.NoInspectionAvailable` if the instance is + not part of a mapping. + """ return object_state(instance).mapper def object_state(instance): - """Given an object, return the primary Mapper associated with the object - instance. + """Given an object, return the :class:`.InstanceState` + associated with the object. - Raises UnmappedInstanceError if no mapping is configured. + Raises :class:`sqlalchemy.orm.exc.UnmappedInstanceError` + if no mapping is configured. - Equivalent functionality is available via the inspection system as:: + Equivalent functionality is available via the :func:`.inspect` + function as:: inspect(instance) - Using the inspection system will raise plain - :class:`.InvalidRequestError` if the instance is not part of - a mapping. + Using the inspection system will raise + :class:`sqlalchemy.exc.NoInspectionAvailable` if the instance is + not part of a mapping. """ state = _inspect_mapped_object(instance) @@ -902,7 +942,8 @@ def class_mapper(class_, configure=True): on the given class, or :class:`.ArgumentError` if a non-class object is passed. - Equivalent functionality is available via the inspection system as:: + Equivalent functionality is available via the :func:`.inspect` + function as:: inspect(some_mapped_class) diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 8d08da4d8..e9e8e35c2 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -15,7 +15,8 @@ import re import sys import types import warnings -from compat import update_wrapper, set_types, threading, callable, inspect_getfullargspec, py3k_warning +from compat import update_wrapper, set_types, threading, \ + callable, inspect_getfullargspec, py3k_warning from sqlalchemy import exc def _unique_symbols(used, *bases): @@ -80,7 +81,7 @@ class PluginLoader(object): from sqlalchemy import exc raise exc.ArgumentError( - "Can't load plugin: %s:%s" % + "Can't load plugin: %s:%s" % (self.group, name)) def register(self, name, modulepath, objname): @@ -202,7 +203,8 @@ def format_argspec_plus(fn, grouped=True): self_arg = None # Py3K - #apply_pos = inspect.formatargspec(spec[0], spec[1], spec[2], None, spec[4]) + #apply_pos = inspect.formatargspec(spec[0], spec[1], + # spec[2], None, spec[4]) #num_defaults = 0 #if spec[3]: # num_defaults += len(spec[3]) @@ -218,11 +220,12 @@ def format_argspec_plus(fn, grouped=True): # end Py2K if num_defaults: - defaulted_vals = name_args[0-num_defaults:] + defaulted_vals = name_args[0 - num_defaults:] else: defaulted_vals = () - apply_kw = inspect.formatargspec(name_args, spec[1], spec[2], defaulted_vals, + apply_kw = inspect.formatargspec(name_args, spec[1], spec[2], + defaulted_vals, formatvalue=lambda x: '=' + x) if grouped: return dict(args=args, self_arg=self_arg, @@ -281,7 +284,7 @@ def unbound_method_to_callable(func_or_cls): def generic_repr(obj): """Produce a __repr__() based on direct association of the __init__() specification vs. same-named attributes present. - + """ def genargs(): try: @@ -590,10 +593,10 @@ class importlater(object): from mypackage.somemodule import somesubmod except evaluted upon attribute access to "somesubmod". - + importlater() currently requires that resolve_all() be called, typically at the bottom of a package's __init__.py. - This is so that __import__ still called only at + This is so that __import__ still called only at module import time, and not potentially within a non-main thread later on. @@ -623,7 +626,7 @@ class importlater(object): if self in importlater._unresolved: raise ImportError( "importlater.resolve_all() hasn't " - "been called (this is %s %s)" + "been called (this is %s %s)" % (self._il_path, self._il_addtl)) m = self._initial_import @@ -638,14 +641,14 @@ class importlater(object): importlater._unresolved.discard(self) if self._il_addtl: self._initial_import = __import__( - self._il_path, globals(), locals(), + self._il_path, globals(), locals(), [self._il_addtl]) else: self._initial_import = __import__(self._il_path) def __getattr__(self, key): if key == 'module': - raise ImportError("Could not resolve module %s" + raise ImportError("Could not resolve module %s" % self._full_path) try: attr = getattr(self.module, key) @@ -918,8 +921,8 @@ def warn(msg, stacklevel=3): If msg is a string, :class:`.exc.SAWarning` is used as the category. - .. note:: - + .. note:: + This function is swapped out when the test suite runs, with a compatible version that uses warnings.warn_explicit, so that the warnings registry can -- cgit v1.2.1