diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-04-23 12:03:54 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2012-04-23 12:03:54 -0400 |
| commit | 54017d9de202ed67072a352ce2f6dbfd74bf48f3 (patch) | |
| tree | a632045bd8a5e7d5932d69d5f436bc27f1b9788d | |
| parent | 8f35f7a803c67f4ab0620686592a021a24e4b331 (diff) | |
| parent | f7bb3b17e6df09caa56c20c722364fc52edf7afc (diff) | |
| download | sqlalchemy-54017d9de202ed67072a352ce2f6dbfd74bf48f3.tar.gz | |
merge patch for [ticket:2208]. This still needs documentation.
| -rw-r--r-- | CHANGES | 15 | ||||
| -rw-r--r-- | lib/sqlalchemy/__init__.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/reflection.py | 7 | ||||
| -rw-r--r-- | lib/sqlalchemy/inspection.py | 44 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 41 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/descriptor_props.py | 1 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/dynamic.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/instrumentation.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 36 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/properties.py | 31 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/session.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/state.py | 137 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/util.py | 29 | ||||
| -rw-r--r-- | test/base/test_inspect.py | 62 | ||||
| -rw-r--r-- | test/engine/test_reflection.py | 29 | ||||
| -rw-r--r-- | test/orm/_fixtures.py | 45 | ||||
| -rw-r--r-- | test/orm/test_backref_mutations.py | 4 | ||||
| -rw-r--r-- | test/orm/test_inspect.py | 290 | ||||
| -rw-r--r-- | test/orm/test_mapper.py | 8 | ||||
| -rw-r--r-- | test/orm/test_query.py | 40 | ||||
| -rw-r--r-- | test/orm/test_session.py | 2 |
21 files changed, 697 insertions, 144 deletions
@@ -29,6 +29,17 @@ those which apply to an 0.7 release are noted. approach can be upgraded to this new approach. [ticket:1401] + - [feature] Added new core function "inspect()", + which serves as a generic gateway to + introspection into mappers, objects, + others. The Mapper and InstanceState + objects have been enhanced with a public + API that allows inspection of mapped + attributes, including filters for column-bound + or relationship-bound properties, inspection + of current object state, history of + attributes, etc. [ticket:2208] + - [feature] Query now "auto correlates" by default in the same way as select() does. Previously, a Query used as a subquery @@ -80,6 +91,10 @@ those which apply to an 0.7 release are noted. also in 0.7.7. - sql + - [feature] The Inspector object can now be + acquired using the new inspect() service, + part of [ticket:2208] + - [bug] Removed warning when Index is created with no columns; while this might not be what the user intended, it is a valid use case diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py index 962a1a761..422d9e43e 100644 --- a/lib/sqlalchemy/__init__.py +++ b/lib/sqlalchemy/__init__.py @@ -4,7 +4,7 @@ # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -import inspect +import inspect as _inspect import sys import sqlalchemy.exc as exceptions @@ -111,15 +111,17 @@ from sqlalchemy.schema import ( UniqueConstraint, ) +from sqlalchemy.inspection import inspect + from sqlalchemy.engine import create_engine, engine_from_config __all__ = sorted(name for name, obj in locals().items() - if not (name.startswith('_') or inspect.ismodule(obj))) + if not (name.startswith('_') or _inspect.ismodule(obj))) __version__ = '0.8.0b1' -del inspect, sys +del _inspect, sys from sqlalchemy import util as _sa_util _sa_util.importlater.resolve_all() diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index 71d97e65f..b2a5a02ef 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -30,7 +30,8 @@ from sqlalchemy import util from sqlalchemy.util import topological from sqlalchemy.types import TypeEngine from sqlalchemy import schema as sa_schema - +from sqlalchemy import inspection +from sqlalchemy.engine.base import Connectable @util.decorator def cache(fn, self, con, *args, **kw): @@ -118,6 +119,10 @@ class Inspector(object): return bind.dialect.inspector(bind) return Inspector(bind) + @inspection._inspects(Connectable) + def _insp(bind): + return Inspector.from_engine(bind) + @property def default_schema_name(self): """Return the default schema name presented by the dialect diff --git a/lib/sqlalchemy/inspection.py b/lib/sqlalchemy/inspection.py new file mode 100644 index 000000000..9ce52beab --- /dev/null +++ b/lib/sqlalchemy/inspection.py @@ -0,0 +1,44 @@ +# sqlalchemy/inspect.py +# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file> +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Base inspect API. + +:func:`.inspect` provides access to a contextual object +regarding a subject. + +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 sqlalchemy import util +_registrars = util.defaultdict(list) + +def inspect(subject): + type_ = type(subject) + for cls in type_.__mro__: + if cls in _registrars: + reg = _registrars[cls] + break + else: + raise exc.InvalidRequestError( + "No inspection system is " + "available for object of type %s" % + type_) + return reg(subject) + +def _inspects(*types): + def decorate(fn_or_cls): + for type_ in types: + if type_ in _registrars: + raise AssertionError( + "Type %s is already " + "registered" % type_) + _registrars[type_] = fn_or_cls + return fn_or_cls + return decorate diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index ec0b84a60..d75682443 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -16,7 +16,7 @@ defines a large part of the ORM's interactivity. import operator from operator import itemgetter -from sqlalchemy import util, event, exc as sa_exc +from sqlalchemy import util, event, exc as sa_exc, inspection from sqlalchemy.orm import interfaces, collections, events, exc as orm_exc @@ -94,7 +94,6 @@ PASSIVE_NO_FETCH_RELATED = PASSIVE_OFF ^ RELATED_OBJECT_OK PASSIVE_ONLY_PERSISTENT = PASSIVE_OFF ^ NON_PERSISTENT_OK - class QueryableAttribute(interfaces.PropComparator): """Base class for class-bound attributes. """ @@ -164,6 +163,10 @@ class QueryableAttribute(interfaces.PropComparator): return self.comparator.property +@inspection._inspects(QueryableAttribute) +def _get_prop(source): + return source.property + class InstrumentedAttribute(QueryableAttribute): """Class bound instrumented attribute which adds descriptor methods.""" @@ -201,11 +204,13 @@ def create_proxied_attribute(descriptor): """ - def __init__(self, class_, key, descriptor, comparator, + def __init__(self, class_, key, descriptor, property_, + comparator, adapter=None, doc=None): self.class_ = class_ self.key = key self.descriptor = descriptor + self.original_property = property_ self._comparator = comparator self.adapter = adapter self.__doc__ = doc @@ -542,7 +547,7 @@ class ScalarAttributeImpl(AttributeImpl): if self.dispatch.remove: self.fire_remove_event(state, dict_, old, None) - state.modified_event(dict_, self, old) + state._modified_event(dict_, self, old) del dict_[self.key] def get_history(self, state, dict_, passive=PASSIVE_OFF): @@ -562,7 +567,7 @@ class ScalarAttributeImpl(AttributeImpl): if self.dispatch.set: value = self.fire_replace_event(state, dict_, value, old, initiator) - state.modified_event(dict_, self, old) + state._modified_event(dict_, self, old) dict_[self.key] = value def fire_replace_event(self, state, dict_, value, previous, initiator): @@ -721,7 +726,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): for fn in self.dispatch.remove: fn(state, value, initiator or self) - state.modified_event(dict_, self, value) + state._modified_event(dict_, self, value) def fire_replace_event(self, state, dict_, value, previous, initiator): if self.trackparent: @@ -733,7 +738,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl): for fn in self.dispatch.set: value = fn(state, value, previous, initiator or self) - state.modified_event(dict_, self, previous) + state._modified_event(dict_, self, previous) if self.trackparent: if value is not None: @@ -816,7 +821,7 @@ class CollectionAttributeImpl(AttributeImpl): for fn in self.dispatch.append: value = fn(state, value, initiator or self) - state.modified_event(dict_, self, NEVER_SET, True) + state._modified_event(dict_, self, NEVER_SET, True) if self.trackparent and value is not None: self.sethasparent(instance_state(value), state, True) @@ -824,7 +829,7 @@ class CollectionAttributeImpl(AttributeImpl): return value def fire_pre_remove_event(self, state, dict_, initiator): - state.modified_event(dict_, self, NEVER_SET, True) + state._modified_event(dict_, self, NEVER_SET, True) def fire_remove_event(self, state, dict_, value, initiator): if self.trackparent and value is not None: @@ -833,13 +838,13 @@ class CollectionAttributeImpl(AttributeImpl): for fn in self.dispatch.remove: fn(state, value, initiator or self) - state.modified_event(dict_, self, NEVER_SET, True) + state._modified_event(dict_, self, NEVER_SET, True) def delete(self, state, dict_): if self.key not in dict_: return - state.modified_event(dict_, self, NEVER_SET, True) + state._modified_event(dict_, self, NEVER_SET, True) collection = self.get_collection(state, state.dict) collection.clear_with_event() @@ -866,7 +871,7 @@ class CollectionAttributeImpl(AttributeImpl): value = self.fire_append_event(state, dict_, value, initiator) assert self.key not in dict_, \ "Collection was loaded during event handling." - state.get_pending(self.key).append(value) + state._get_pending_mutation(self.key).append(value) else: collection.append_with_event(value, initiator) @@ -879,7 +884,7 @@ class CollectionAttributeImpl(AttributeImpl): self.fire_remove_event(state, dict_, value, initiator) assert self.key not in dict_, \ "Collection was loaded during event handling." - state.get_pending(self.key).remove(value) + state._get_pending_mutation(self.key).remove(value) else: collection.remove_with_event(value, initiator) @@ -935,7 +940,7 @@ class CollectionAttributeImpl(AttributeImpl): return # place a copy of "old" in state.committed_state - state.modified_event(dict_, self, old, True) + state._modified_event(dict_, self, old, True) old_collection = getattr(old, '_sa_adapter') @@ -956,12 +961,12 @@ class CollectionAttributeImpl(AttributeImpl): state.commit(dict_, [self.key]) - if self.key in state.pending: + if self.key in state._pending_mutations: # pending items exist. issue a modified event, # add/remove new items. - state.modified_event(dict_, self, user_data, True) + state._modified_event(dict_, self, user_data, True) - pending = state.pending.pop(self.key) + pending = state._pending_mutations.pop(self.key) added = pending.added_items removed = pending.deleted_items for item in added: @@ -1408,5 +1413,5 @@ def flag_modified(instance, key): """ state, dict_ = instance_state(instance), instance_dict(instance) impl = state.manager[key].impl - state.modified_event(dict_, impl, NO_VALUE) + state._modified_event(dict_, impl, NO_VALUE) diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index ed0d4924e..57c245028 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -64,6 +64,7 @@ class DescriptorProperty(MapperProperty): self.parent.class_, self.key, self.descriptor, + self, lambda: self._comparator_factory(mapper), doc=self.doc ) diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index 1a3e52a36..18fc76aa9 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -97,7 +97,7 @@ class DynamicAttributeImpl(attributes.AttributeImpl): if self.key not in state.committed_state: state.committed_state[self.key] = CollectionHistory(self, state) - state.modified_event(dict_, + state._modified_event(dict_, self, attributes.NEVER_SET) diff --git a/lib/sqlalchemy/orm/instrumentation.py b/lib/sqlalchemy/orm/instrumentation.py index af9ef7841..1012af67a 100644 --- a/lib/sqlalchemy/orm/instrumentation.py +++ b/lib/sqlalchemy/orm/instrumentation.py @@ -23,7 +23,7 @@ An example of full customization is in /examples/custom_attributes. from sqlalchemy.orm import exc, collections, events from operator import attrgetter, itemgetter -from sqlalchemy import event, util +from sqlalchemy import event, util, inspection import weakref from sqlalchemy.orm import state, attributes diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index afabac05a..0771bbf3d 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -33,6 +33,7 @@ from sqlalchemy.orm.util import _INSTRUMENTOR, _class_to_mapper, \ import sys sessionlib = util.importlater("sqlalchemy.orm", "session") properties = util.importlater("sqlalchemy.orm", "properties") +descriptor_props = util.importlater("sqlalchemy.orm", "descriptor_props") __all__ = ( 'Mapper', @@ -1393,12 +1394,35 @@ class Mapper(object): continue yield c - @property - def properties(self): - raise NotImplementedError( - "Public collection of MapperProperty objects is " - "provided by the get_property() and iterate_properties " - "accessors.") + @util.memoized_property + def attr(self): + if _new_mappers: + configure_mappers() + return util.ImmutableProperties(self._props) + + @_memoized_configured_property + def synonyms(self): + return self._filter_properties(descriptor_props.SynonymProperty) + + @_memoized_configured_property + def column_attrs(self): + return self._filter_properties(properties.ColumnProperty) + + @_memoized_configured_property + def relationships(self): + return self._filter_properties(properties.RelationshipProperty) + + @_memoized_configured_property + def composites(self): + return self._filter_properties(descriptor_props.CompositeProperty) + + def _filter_properties(self, type_): + if _new_mappers: + configure_mappers() + return util.ImmutableProperties(dict( + (k, v) for k, v in self._props.iteritems() + if isinstance(v, type_) + )) @_memoized_configured_property def _get_clause(self): diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 424795ee4..fd64e7b81 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -99,6 +99,13 @@ class ColumnProperty(StrategizedProperty): else: self.strategy_class = strategies.ColumnLoader + @property + def expression(self): + """Return the primary column or expression for this ColumnProperty. + + """ + return self.columns[0] + def instrument_class(self, mapper): if not self.instrument: return @@ -276,7 +283,6 @@ class RelationshipProperty(StrategizedProperty): else: self.backref = backref - def instrument_class(self, mapper): attributes.register_descriptor( mapper.class_, @@ -805,6 +811,27 @@ class RelationshipProperty(StrategizedProperty): dest_state.get_impl(self.key).set(dest_state, dest_dict, obj, None) + def _value_as_iterable(self, state, dict_, key, + passive=attributes.PASSIVE_OFF): + """Return a list of tuples (state, obj) for the given + key. + + returns an empty list if the value is None/empty/PASSIVE_NO_RESULT + """ + + impl = state.manager[key].impl + x = impl.get(state, dict_, passive=passive) + if x is attributes.PASSIVE_NO_RESULT or x is None: + return [] + elif hasattr(impl, 'get_collection'): + return [ + (attributes.instance_state(o), o) for o in + impl.get_collection(state, dict_, x, passive=passive) + ] + else: + return [(attributes.instance_state(x), x)] + + def cascade_iterator(self, type_, state, dict_, visited_states, halt_on=None): #assert type_ in self.cascade @@ -819,7 +846,7 @@ class RelationshipProperty(StrategizedProperty): get_all_pending(state, dict_) else: - tuples = state.value_as_iterable(dict_, self.key, + tuples = self._value_as_iterable(state, dict_, self.key, passive=passive) skip_pending = type_ == 'refresh-expire' and 'delete-orphan' \ diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 14778705d..87968da82 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -835,7 +835,7 @@ class Session(object): """ for state in self.identity_map.all_states() + list(self._new): - state.detach() + state._detach() self.identity_map = self._identity_cls() self._new = {} @@ -1135,7 +1135,7 @@ class Session(object): state.expire(state.dict, self.identity_map._modified) elif state in self._new: self._new.pop(state) - state.detach() + state._detach() @util.deprecated("0.7", "The non-weak-referencing identity map " "feature is no longer needed.") @@ -1177,11 +1177,11 @@ class Session(object): def _expunge_state(self, state): if state in self._new: self._new.pop(state) - state.detach() + state._detach() elif self.identity_map.contains_state(state): self.identity_map.discard(state) self._deleted.pop(state, None) - state.detach() + state._detach() elif self.transaction: self.transaction._deleted.pop(state, None) diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 30a08faba..64fca8715 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -17,12 +17,12 @@ from sqlalchemy import util from sqlalchemy.orm import exc as orm_exc, attributes, interfaces,\ util as orm_util -from sqlalchemy.orm.attributes import PASSIVE_OFF, PASSIVE_NO_RESULT, \ - SQL_OK, NEVER_SET, ATTR_WAS_SET, NO_VALUE +from sqlalchemy.orm.attributes import PASSIVE_NO_RESULT, \ + SQL_OK, NEVER_SET, ATTR_WAS_SET, NO_VALUE,\ + PASSIVE_NO_INITIALIZE mapperlib = util.importlater("sqlalchemy.orm", "mapperlib") - -import sys +sessionlib = util.importlater("sqlalchemy.orm", "session") class InstanceState(object): """tracks state information at the instance level.""" @@ -47,22 +47,81 @@ class InstanceState(object): self.committed_state = {} @util.memoized_property + def attr(self): + return util.ImmutableProperties( + dict( + (key, InspectAttr(self, key)) + for key in self.manager + ) + ) + + @property + def transient(self): + return self.key is None and \ + not self._attached + + @property + def pending(self): + return self.key is None and \ + self._attached + + @property + def persistent(self): + return self.key is not None and \ + self._attached + + @property + def detached(self): + return self.key is not None and \ + not self._attached + + @property + def _attached(self): + return self.session_id is not None and \ + self.session_id in sessionlib._sessions + + @property + def session(self): + return sessionlib._state_session(self) + + @property + def object(self): + return self.obj() + + @property + def identity(self): + if self.key is None: + return None + else: + return self.key[1] + + @property + def identity_key(self): + # TODO: just change .key to .identity_key across + # the board ? probably + return self.key + + @util.memoized_property def parents(self): return {} @util.memoized_property - def pending(self): + def _pending_mutations(self): return {} + @util.memoized_property + def mapper(self): + return self.manager.mapper + @property def has_identity(self): return bool(self.key) - def detach(self): + def _detach(self): self.session_id = None - def dispose(self): - self.detach() + def _dispose(self): + self._detach() del self.obj def _cleanup(self, ref): @@ -106,35 +165,16 @@ class InstanceState(object): def get_impl(self, key): return self.manager[key].impl - def get_pending(self, key): - if key not in self.pending: - self.pending[key] = PendingCollection() - return self.pending[key] - - def value_as_iterable(self, dict_, key, passive=PASSIVE_OFF): - """Return a list of tuples (state, obj) for the given - key. - - returns an empty list if the value is None/empty/PASSIVE_NO_RESULT - """ - - impl = self.manager[key].impl - x = impl.get(self, dict_, passive=passive) - if x is PASSIVE_NO_RESULT or x is None: - return [] - elif hasattr(impl, 'get_collection'): - return [ - (attributes.instance_state(o), o) for o in - impl.get_collection(self, dict_, x, passive=passive) - ] - else: - return [(attributes.instance_state(x), x)] + def _get_pending_mutation(self, key): + if key not in self._pending_mutations: + self._pending_mutations[key] = PendingCollection() + return self._pending_mutations[key] def __getstate__(self): d = {'instance':self.obj()} d.update( (k, self.__dict__[k]) for k in ( - 'committed_state', 'pending', 'modified', 'expired', + 'committed_state', '_pending_mutations', 'modified', 'expired', 'callables', 'key', 'parents', 'load_options', 'mutable_dict', 'class_', ) if k in self.__dict__ @@ -169,7 +209,7 @@ class InstanceState(object): mapperlib.configure_mappers() self.committed_state = state.get('committed_state', {}) - self.pending = state.get('pending', {}) + self._pending_mutations = state.get('_pending_mutations', {}) self.parents = state.get('parents', {}) self.modified = state.get('modified', False) self.expired = state.get('expired', False) @@ -234,7 +274,7 @@ class InstanceState(object): self.committed_state.clear() - self.__dict__.pop('pending', None) + self.__dict__.pop('_pending_mutations', None) self.__dict__.pop('mutable_dict', None) # clear out 'parents' collection. not @@ -252,7 +292,7 @@ class InstanceState(object): self.manager.dispatch.expire(self, None) def expire_attributes(self, dict_, attribute_names): - pending = self.__dict__.get('pending', None) + pending = self.__dict__.get('_pending_mutations', None) mutable_dict = self.mutable_dict for key in attribute_names: @@ -336,7 +376,7 @@ class InstanceState(object): def _is_really_none(self): return self.obj() - def modified_event(self, dict_, attr, previous, collection=False): + def _modified_event(self, dict_, attr, previous, collection=False): if attr.key not in self.committed_state: if collection: if previous is NEVER_SET: @@ -415,7 +455,7 @@ class InstanceState(object): """ self.committed_state.clear() - self.__dict__.pop('pending', None) + self.__dict__.pop('_pending_mutations', None) callables = self.callables for key in list(callables): @@ -432,6 +472,27 @@ class InstanceState(object): self.modified = self.expired = False self._strong_obj = None +class InspectAttr(object): + """Provide inspection interface to an object's state.""" + + def __init__(self, state, key): + self.state = state + self.key = key + + @property + def loaded_value(self): + return self.state.dict.get(self.key, NO_VALUE) + + @property + def value(self): + return self.state.manager[self.key].__get__( + self.state.obj(), self.state.class_) + + @property + def history(self): + return self.state.get_history(self.key, + PASSIVE_NO_INITIALIZE) + class MutableAttrInstanceState(InstanceState): """InstanceState implementation for objects that reference 'mutable' attributes. @@ -524,7 +585,7 @@ class MutableAttrInstanceState(InstanceState): instance_dict = self._instance_dict() if instance_dict: instance_dict.discard(self) - self.dispose() + self._dispose() def __resurrect(self): """A substitute for the obj() weakref function which resurrects.""" diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index c14c22bac..f4dfef3d5 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -5,7 +5,7 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy import sql, util, event, exc as sa_exc +from sqlalchemy import sql, util, event, exc as sa_exc, inspection from sqlalchemy.sql import expression, util as sql_util, operators from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE,\ PropComparator, MapperProperty @@ -642,15 +642,34 @@ def object_mapper(instance): Raises UnmappedInstanceError if no mapping is configured. + This function is available via the inspection system as:: + + inspect(instance).mapper + + """ + return object_state(instance).mapper + +@inspection._inspects(object) +def object_state(instance): + """Given an object, return the primary Mapper associated with the object + instance. + + Raises UnmappedInstanceError if no mapping is configured. + + This function is available via the inspection system as:: + + inspect(instance) + """ try: - state = attributes.instance_state(instance) - return state.manager.mapper + return attributes.instance_state(instance) except exc.UnmappedClassError: raise exc.UnmappedInstanceError(instance) except exc.NO_STATE: raise exc.UnmappedInstanceError(instance) + +@inspection._inspects(type) def class_mapper(class_, compile=True): """Given a class, return the primary :class:`.Mapper` associated with the key. @@ -659,6 +678,10 @@ def class_mapper(class_, compile=True): on the given class, or :class:`.ArgumentError` if a non-class object is passed. + This function is available via the inspection system as:: + + inspect(some_mapped_class) + """ try: diff --git a/test/base/test_inspect.py b/test/base/test_inspect.py new file mode 100644 index 000000000..b95b7d8c5 --- /dev/null +++ b/test/base/test_inspect.py @@ -0,0 +1,62 @@ +"""test the inspection registry system.""" + +from test.lib.testing import eq_, assert_raises +from sqlalchemy import exc, util +from sqlalchemy import inspection, inspect +from test.lib import fixtures + +class TestFixture(object): + pass + +class TestEvents(fixtures.TestBase): + """Test class- and instance-level event registration.""" + + def tearDown(self): + for type_ in list(inspection._registrars): + if issubclass(type_, TestFixture): + del inspection._registrars[type_] + + def test_def_insp(self): + class SomeFoo(TestFixture): + pass + + @inspection._inspects(SomeFoo) + def insp_somefoo(subject): + return {"insp":subject} + + somefoo = SomeFoo() + insp = inspect(somefoo) + assert insp["insp"] is somefoo + + def test_class_insp(self): + class SomeFoo(TestFixture): + pass + + @inspection._inspects(SomeFoo) + class SomeFooInspect(object): + def __init__(self, target): + self.target = target + + somefoo = SomeFoo() + insp = inspect(somefoo) + assert isinstance(insp, SomeFooInspect) + assert insp.target is somefoo + + def test_hierarchy_insp(self): + class SomeFoo(TestFixture): + pass + + class SomeSubFoo(SomeFoo): + pass + + @inspection._inspects(SomeFoo) + def insp_somefoo(subject): + return 1 + + @inspection._inspects(SomeSubFoo) + def insp_somesubfoo(subject): + return 2 + + somefoo = SomeFoo() + eq_(inspect(SomeFoo()), 1) + eq_(inspect(SomeSubFoo()), 2) diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py index f385a0fa2..e59849d98 100644 --- a/test/engine/test_reflection.py +++ b/test/engine/test_reflection.py @@ -1,8 +1,7 @@ from test.lib.testing import eq_, assert_raises, assert_raises_message import StringIO, unicodedata from sqlalchemy import types as sql_types -from sqlalchemy import schema, events, event -from sqlalchemy.engine.reflection import Inspector +from sqlalchemy import schema, events, event, inspect from sqlalchemy import MetaData, Integer, String from test.lib.schema import Table, Column import sqlalchemy as sa @@ -10,8 +9,6 @@ from test.lib import ComparesTables, \ testing, engines, AssertsCompiledSQL from test.lib import fixtures -create_inspector = Inspector.from_engine - metadata, users = None, None class ReflectionTest(fixtures.TestBase, ComparesTables): @@ -773,7 +770,7 @@ class ReflectionTest(fixtures.TestBase, ComparesTables): def test_inspector_conn_closing(self): m1 = MetaData() c = testing.db.connect() - i = Inspector.from_engine(testing.db) + i = inspect(testing.db) assert not c.closed @testing.provide_metadata @@ -1050,7 +1047,7 @@ class UnicodeReflectionTest(fixtures.TestBase): @testing.requires.unicode_connections def test_get_names(self): - inspector = Inspector.from_engine(self.bind) + inspector = inspect(self.bind) names = dict( (tname, (cname, ixname)) for tname, cname, ixname in self.names ) @@ -1362,18 +1359,18 @@ class ComponentReflectionTest(fixtures.TestBase): @testing.requires.schemas def test_get_schema_names(self): - insp = Inspector(testing.db) + insp = inspect(testing.db) self.assert_('test_schema' in insp.get_schema_names()) def test_dialect_initialize(self): engine = engines.testing_engine() assert not hasattr(engine.dialect, 'default_schema_name') - insp = Inspector(engine) + insp = inspect(engine) assert hasattr(engine.dialect, 'default_schema_name') def test_get_default_schema_name(self): - insp = Inspector(testing.db) + insp = inspect(testing.db) eq_(insp.default_schema_name, testing.db.dialect.default_schema_name) @testing.provide_metadata @@ -1384,7 +1381,7 @@ class ComponentReflectionTest(fixtures.TestBase): meta.create_all() _create_views(meta.bind, schema) try: - insp = Inspector(meta.bind) + insp = inspect(meta.bind) if table_type == 'view': table_names = insp.get_view_names(schema) table_names.sort() @@ -1428,7 +1425,7 @@ class ComponentReflectionTest(fixtures.TestBase): _create_views(meta.bind, schema) table_names = ['users_v', 'email_addresses_v'] try: - insp = Inspector(meta.bind) + insp = inspect(meta.bind) for table_name, table in zip(table_names, (users, addresses)): schema_name = schema @@ -1490,7 +1487,7 @@ class ComponentReflectionTest(fixtures.TestBase): meta = self.metadata users, addresses, dingalings = createTables(meta, schema) meta.create_all() - insp = Inspector(meta.bind) + insp = inspect(meta.bind) users_pkeys = insp.get_primary_keys(users.name, schema=schema) eq_(users_pkeys, ['user_id']) @@ -1517,7 +1514,7 @@ class ComponentReflectionTest(fixtures.TestBase): meta = self.metadata users, addresses, dingalings = createTables(meta, schema) meta.create_all() - insp = Inspector(meta.bind) + insp = inspect(meta.bind) expected_schema = schema # users users_fkeys = insp.get_foreign_keys(users.name, @@ -1561,7 +1558,7 @@ class ComponentReflectionTest(fixtures.TestBase): createIndexes(meta.bind, schema) # The database may decide to create indexes for foreign keys, etc. # so there may be more indexes than expected. - insp = Inspector(meta.bind) + insp = inspect(meta.bind) indexes = insp.get_indexes('users', schema=schema) expected_indexes = [ {'unique': False, @@ -1590,7 +1587,7 @@ class ComponentReflectionTest(fixtures.TestBase): view_name1 = 'users_v' view_name2 = 'email_addresses_v' try: - insp = Inspector(meta.bind) + insp = inspect(meta.bind) v1 = insp.get_view_definition(view_name1, schema=schema) self.assert_(v1) v2 = insp.get_view_definition(view_name2, schema=schema) @@ -1613,7 +1610,7 @@ class ComponentReflectionTest(fixtures.TestBase): meta = self.metadata users, addresses, dingalings = createTables(meta, schema) meta.create_all() - insp = create_inspector(meta.bind) + insp = inspect(meta.bind) oid = insp.get_table_oid(table_name, schema) self.assert_(isinstance(oid, (int, long))) diff --git a/test/orm/_fixtures.py b/test/orm/_fixtures.py index 5def54e3a..7431a3a83 100644 --- a/test/orm/_fixtures.py +++ b/test/orm/_fixtures.py @@ -2,7 +2,8 @@ from sqlalchemy import MetaData, Integer, String, ForeignKey from sqlalchemy import util from test.lib.schema import Table from test.lib.schema import Column -from sqlalchemy.orm import attributes +from sqlalchemy.orm import attributes, mapper, relationship, \ + backref, configure_mappers from test.lib import fixtures __all__ = () @@ -49,6 +50,48 @@ class FixtureTest(fixtures.MappedTest): pass @classmethod + def _setup_stock_mapping(cls): + Node, composite_pk_table, users, Keyword, items, Dingaling, \ + order_items, item_keywords, Item, User, dingalings, \ + Address, keywords, CompositePk, nodes, Order, orders, \ + addresses = cls.classes.Node, \ + cls.tables.composite_pk_table, cls.tables.users, \ + cls.classes.Keyword, cls.tables.items, \ + cls.classes.Dingaling, cls.tables.order_items, \ + cls.tables.item_keywords, cls.classes.Item, \ + cls.classes.User, cls.tables.dingalings, \ + cls.classes.Address, cls.tables.keywords, \ + cls.classes.CompositePk, cls.tables.nodes, \ + cls.classes.Order, cls.tables.orders, cls.tables.addresses + + mapper(User, users, properties={ + 'addresses':relationship(Address, backref='user', order_by=addresses.c.id), + 'orders':relationship(Order, backref='user', order_by=orders.c.id), # o2m, m2o + }) + mapper(Address, addresses, properties={ + 'dingaling':relationship(Dingaling, uselist=False, backref="address") #o2o + }) + mapper(Dingaling, dingalings) + mapper(Order, orders, properties={ + 'items':relationship(Item, secondary=order_items, order_by=items.c.id), #m2m + 'address':relationship(Address), # m2o + }) + mapper(Item, items, properties={ + 'keywords':relationship(Keyword, secondary=item_keywords) #m2m + }) + mapper(Keyword, keywords) + + mapper(Node, nodes, properties={ + 'children':relationship(Node, + backref=backref('parent', remote_side=[nodes.c.id]) + ) + }) + + mapper(CompositePk, composite_pk_table) + + configure_mappers() + + @classmethod def define_tables(cls, metadata): Table('users', metadata, Column('id', Integer, primary_key=True, test_needs_autoincrement=True), diff --git a/test/orm/test_backref_mutations.py b/test/orm/test_backref_mutations.py index c633cb8ee..b3214984f 100644 --- a/test/orm/test_backref_mutations.py +++ b/test/orm/test_backref_mutations.py @@ -551,7 +551,7 @@ class M2MCollectionMoveTest(_fixtures.FixtureTest): # list is still here. eq_( set(attributes.instance_state(i1). - pending['keywords'].added_items), + _pending_mutations['keywords'].added_items), set([k2]) ) # because autoflush is off, k2 is still @@ -564,7 +564,7 @@ class M2MCollectionMoveTest(_fixtures.FixtureTest): # the pending collection was removed assert 'keywords' not in attributes.\ instance_state(i1).\ - pending + _pending_mutations def test_duplicate_adds(self): Item, Keyword = (self.classes.Item, self.classes.Keyword) diff --git a/test/orm/test_inspect.py b/test/orm/test_inspect.py new file mode 100644 index 000000000..9973c31c2 --- /dev/null +++ b/test/orm/test_inspect.py @@ -0,0 +1,290 @@ +"""test the inspection registry system.""" + +from test.lib.testing import eq_, assert_raises, is_ +from sqlalchemy import exc, util +from sqlalchemy import inspect +from test.orm import _fixtures +from sqlalchemy.orm import class_mapper, synonym, Session +from sqlalchemy.orm.attributes import instance_state, NO_VALUE +from test.lib import testing + +class TestORMInspection(_fixtures.FixtureTest): + @classmethod + def setup_mappers(cls): + cls._setup_stock_mapping() + inspect(cls.classes.User).add_property( + "name_syn",synonym("name") + ) + + def test_class_mapper(self): + User = self.classes.User + + assert inspect(User) is class_mapper(User) + + def test_instance_state(self): + User = self.classes.User + u1 = User() + + assert inspect(u1) is instance_state(u1) + + def test_column_collection_iterate(self): + User = self.classes.User + user_table = self.tables.users + insp = inspect(User) + eq_( + list(insp.columns), + [user_table.c.id, user_table.c.name] + ) + is_( + insp.columns.id, user_table.c.id + ) + + def test_primary_key(self): + User = self.classes.User + user_table = self.tables.users + insp = inspect(User) + eq_(insp.primary_key, + (user_table.c.id,) + ) + + def test_local_table(self): + User = self.classes.User + user_table = self.tables.users + insp = inspect(User) + is_(insp.local_table, user_table) + + def test_property(self): + User = self.classes.User + user_table = self.tables.users + insp = inspect(User) + is_(insp.attr.id, class_mapper(User).get_property('id')) + + def test_col_property(self): + User = self.classes.User + user_table = self.tables.users + insp = inspect(User) + id_prop = insp.attr.id + + eq_(id_prop.columns, [user_table.c.id]) + is_(id_prop.expression, user_table.c.id) + + assert not hasattr(id_prop, 'mapper') + + def test_attr_keys(self): + User = self.classes.User + insp = inspect(User) + eq_( + set(insp.attr.keys()), + set(['addresses', 'orders', 'id', 'name', 'name_syn']) + ) + + def test_col_filter(self): + User = self.classes.User + insp = inspect(User) + eq_( + list(insp.column_attrs), + [insp.get_property('id'), insp.get_property('name')] + ) + eq_( + insp.column_attrs.keys(), + ['id', 'name'] + ) + is_( + insp.column_attrs.id, + User.id.property + ) + + def test_synonym_filter(self): + User = self.classes.User + syn = inspect(User).synonyms + + eq_( + list(syn.keys()), ['name_syn'] + ) + is_(syn.name_syn, User.name_syn.original_property) + eq_(dict(syn), { + "name_syn":User.name_syn.original_property + }) + + def test_relationship_filter(self): + User = self.classes.User + rel = inspect(User).relationships + + eq_( + rel.addresses, + User.addresses.property + ) + eq_( + set(rel.keys()), + set(['orders', 'addresses']) + ) + + def test_insp_prop(self): + User = self.classes.User + prop = inspect(User.addresses) + is_(prop, User.addresses.property) + + def test_rel_accessors(self): + User = self.classes.User + Address = self.classes.Address + prop = inspect(User.addresses) + is_(prop.parent, class_mapper(User)) + is_(prop.mapper, class_mapper(Address)) + + assert not hasattr(prop, 'columns') + assert not hasattr(prop, 'expression') + + def test_instance_state(self): + User = self.classes.User + u1 = User() + insp = inspect(u1) + is_(insp, instance_state(u1)) + + def test_instance_state_attr(self): + User = self.classes.User + u1 = User(name='ed') + insp = inspect(u1) + + eq_( + set(insp.attr.keys()), + set(['id', 'name', 'name_syn', 'addresses', 'orders']) + ) + eq_( + insp.attr.name.value, + 'ed' + ) + eq_( + insp.attr.name.loaded_value, + 'ed' + ) + + def test_instance_state_attr_passive_value_scalar(self): + User = self.classes.User + u1 = User(name='ed') + insp = inspect(u1) + # value was not set, NO_VALUE + eq_( + insp.attr.id.loaded_value, + NO_VALUE + ) + # regular accessor sets it + eq_( + insp.attr.id.value, + None + ) + # now the None is there + eq_( + insp.attr.id.loaded_value, + None + ) + + def test_instance_state_attr_passive_value_collection(self): + User = self.classes.User + u1 = User(name='ed') + insp = inspect(u1) + # value was not set, NO_VALUE + eq_( + insp.attr.addresses.loaded_value, + NO_VALUE + ) + # regular accessor sets it + eq_( + insp.attr.addresses.value, + [] + ) + # now the None is there + eq_( + insp.attr.addresses.loaded_value, + [] + ) + + def test_instance_state_attr_hist(self): + User = self.classes.User + u1 = User(name='ed') + insp = inspect(u1) + hist = insp.attr.addresses.history + eq_( + hist.unchanged, None + ) + u1.addresses + hist = insp.attr.addresses.history + eq_( + hist.unchanged, [] + ) + + def test_instance_state_ident_transient(self): + User = self.classes.User + u1 = User(name='ed') + insp = inspect(u1) + is_(insp.identity, None) + + def test_instance_state_ident_persistent(self): + User = self.classes.User + u1 = User(name='ed') + s = Session(testing.db) + s.add(u1) + s.flush() + insp = inspect(u1) + eq_(insp.identity, (u1.id,)) + is_(s.query(User).get(insp.identity), u1) + + def test_identity_key(self): + User = self.classes.User + u1 = User(name='ed') + s = Session(testing.db) + s.add(u1) + s.flush() + insp = inspect(u1) + eq_( + insp.identity_key, + (User, (11, )) + ) + + def test_persistence_states(self): + User = self.classes.User + u1 = User(name='ed') + insp = inspect(u1) + + eq_( + (insp.transient, insp.pending, + insp.persistent, insp.detached), + (True, False, False, False) + ) + s = Session(testing.db) + s.add(u1) + + eq_( + (insp.transient, insp.pending, + insp.persistent, insp.detached), + (False, True, False, False) + ) + + s.flush() + eq_( + (insp.transient, insp.pending, + insp.persistent, insp.detached), + (False, False, True, False) + ) + s.expunge(u1) + eq_( + (insp.transient, insp.pending, + insp.persistent, insp.detached), + (False, False, False, True) + ) + + def test_session_accessor(self): + User = self.classes.User + u1 = User(name='ed') + insp = inspect(u1) + + is_(insp.session, None) + s = Session() + s.add(u1) + is_(insp.session, s) + + def test_object_accessor(self): + User = self.classes.User + u1 = User(name='ed') + insp = inspect(u1) + is_(insp.object, u1) + diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index 4478e5d80..78a1c29a4 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -87,14 +87,6 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL): - def test_prop_accessor(self): - users, User = self.tables.users, self.classes.User - - mapper(User, users) - assert_raises(NotImplementedError, - getattr, sa.orm.class_mapper(User), 'properties') - - def test_friendly_attribute_str_on_uncompiled_boom(self): User, users = self.classes.User, self.tables.users diff --git a/test/orm/test_query.py b/test/orm/test_query.py index 1b57299f0..6e945aa72 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -29,45 +29,7 @@ class QueryTest(_fixtures.FixtureTest): @classmethod def setup_mappers(cls): - Node, composite_pk_table, users, Keyword, items, Dingaling, \ - order_items, item_keywords, Item, User, dingalings, \ - Address, keywords, CompositePk, nodes, Order, orders, \ - addresses = cls.classes.Node, \ - cls.tables.composite_pk_table, cls.tables.users, \ - cls.classes.Keyword, cls.tables.items, \ - cls.classes.Dingaling, cls.tables.order_items, \ - cls.tables.item_keywords, cls.classes.Item, \ - cls.classes.User, cls.tables.dingalings, \ - cls.classes.Address, cls.tables.keywords, \ - cls.classes.CompositePk, cls.tables.nodes, \ - cls.classes.Order, cls.tables.orders, cls.tables.addresses - - mapper(User, users, properties={ - 'addresses':relationship(Address, backref='user', order_by=addresses.c.id), - 'orders':relationship(Order, backref='user', order_by=orders.c.id), # o2m, m2o - }) - mapper(Address, addresses, properties={ - 'dingaling':relationship(Dingaling, uselist=False, backref="address") #o2o - }) - mapper(Dingaling, dingalings) - mapper(Order, orders, properties={ - 'items':relationship(Item, secondary=order_items, order_by=items.c.id), #m2m - 'address':relationship(Address), # m2o - }) - mapper(Item, items, properties={ - 'keywords':relationship(Keyword, secondary=item_keywords) #m2m - }) - mapper(Keyword, keywords) - - mapper(Node, nodes, properties={ - 'children':relationship(Node, - backref=backref('parent', remote_side=[nodes.c.id]) - ) - }) - - mapper(CompositePk, composite_pk_table) - - configure_mappers() + cls._setup_stock_mapping() class MiscTest(QueryTest): run_create_tables = None diff --git a/test/orm/test_session.py b/test/orm/test_session.py index 79852c7a4..a82606b2b 100644 --- a/test/orm/test_session.py +++ b/test/orm/test_session.py @@ -1158,7 +1158,7 @@ class DisposedStates(fixtures.MappedTest): for obj in objs: state = attributes.instance_state(obj) sess.identity_map.discard(state) - state.dispose() + state._dispose() def _test_session(self, **kwargs): global sess |
