From f1bdd4e4bbf8366ff7177ebc3ee6647f32fd414f Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 3 Apr 2012 18:53:39 -0400 Subject: begin implementing inspection system for #2208 --- lib/sqlalchemy/__init__.py | 8 ++++--- lib/sqlalchemy/engine/reflection.py | 7 +++++- lib/sqlalchemy/inspection.py | 44 +++++++++++++++++++++++++++++++++++ lib/sqlalchemy/orm/instrumentation.py | 2 +- lib/sqlalchemy/orm/mapper.py | 34 +++++++++++++++++++++++---- lib/sqlalchemy/orm/state.py | 4 ++++ lib/sqlalchemy/orm/util.py | 29 ++++++++++++++++++++--- 7 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 lib/sqlalchemy/inspection.py (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py index 4e00437ea..ee7bf4c6f 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.7.7' -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 +# +# 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/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 e96b7549a..4fa8dfe24 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', @@ -1392,12 +1393,35 @@ class Mapper(object): continue yield c - @property + @util.memoized_property def properties(self): - raise NotImplementedError( - "Public collection of MapperProperty objects is " - "provided by the get_property() and iterate_properties " - "accessors.") + 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 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/state.py b/lib/sqlalchemy/orm/state.py index 4803ecdc3..156686bc7 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -54,6 +54,10 @@ class InstanceState(object): def pending(self): return {} + @util.memoized_property + def mapper(self): + return self.manager.mapper + @property def has_identity(self): return bool(self.key) diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 0c5f203a7..8d334ce17 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 @@ -616,15 +616,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. @@ -633,6 +652,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: -- cgit v1.2.1 From f7bb3b17e6df09caa56c20c722364fc52edf7afc Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 4 Apr 2012 19:08:05 -0400 Subject: - the inspect interface is done, needs docs. - start dressing up InstanceState for it's coming out, start moving internal things to be underscored within the lib --- lib/sqlalchemy/orm/attributes.py | 41 +++++---- lib/sqlalchemy/orm/descriptor_props.py | 1 + lib/sqlalchemy/orm/dynamic.py | 2 +- lib/sqlalchemy/orm/mapper.py | 6 +- lib/sqlalchemy/orm/properties.py | 157 +++++++++++++++++++-------------- lib/sqlalchemy/orm/session.py | 8 +- lib/sqlalchemy/orm/state.py | 129 +++++++++++++++++++-------- 7 files changed, 218 insertions(+), 126 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 3b4f18b31..c351748df 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 @@ -77,7 +77,6 @@ PASSIVE_OFF = util.symbol('PASSIVE_OFF', """) - class QueryableAttribute(interfaces.PropComparator): """Base class for class-bound attributes. """ @@ -147,6 +146,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.""" @@ -184,11 +187,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 @@ -525,7 +530,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): @@ -545,7 +550,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): @@ -704,7 +709,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: @@ -716,7 +721,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: @@ -799,7 +804,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) @@ -807,7 +812,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: @@ -816,13 +821,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() @@ -849,7 +854,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) @@ -862,7 +867,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) @@ -918,7 +923,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') @@ -939,12 +944,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: @@ -1391,5 +1396,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 edf052870..91fbfcecd 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/mapper.py b/lib/sqlalchemy/orm/mapper.py index 4fa8dfe24..269242713 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1394,7 +1394,7 @@ class Mapper(object): yield c @util.memoized_property - def properties(self): + def attr(self): if _new_mappers: configure_mappers() return util.ImmutableProperties(self._props) @@ -1418,10 +1418,10 @@ class Mapper(object): def _filter_properties(self, type_): if _new_mappers: configure_mappers() - return dict( + 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 74ccf0157..bc9fa9eaf 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -33,9 +33,9 @@ from descriptor_props import CompositeProperty, SynonymProperty, \ class ColumnProperty(StrategizedProperty): """Describes an object attribute that corresponds to a table column. - + Public constructor is the :func:`.orm.column_property` function. - + """ def __init__(self, *columns, **kwargs): @@ -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 @@ -177,13 +184,13 @@ log.class_logger(ColumnProperty) class RelationshipProperty(StrategizedProperty): """Describes an object property that holds a single item or list of items that correspond to a related database table. - + Public constructor is the :func:`.orm.relationship` function. - + Of note here is the :class:`.RelationshipProperty.Comparator` class, which implements comparison operations for scalar- and collection-referencing mapped attributes. - + """ strategy_wildcard_key = 'relationship:*' @@ -276,7 +283,6 @@ class RelationshipProperty(StrategizedProperty): else: self.backref = backref - def instrument_class(self, mapper): attributes.register_descriptor( mapper.class_, @@ -293,7 +299,7 @@ class RelationshipProperty(StrategizedProperty): def __init__(self, prop, mapper, of_type=None, adapter=None): """Construction of :class:`.RelationshipProperty.Comparator` is internal to the ORM's attribute mechanics. - + """ self.prop = prop self.mapper = mapper @@ -332,10 +338,10 @@ class RelationshipProperty(StrategizedProperty): def of_type(self, cls): """Produce a construct that represents a particular 'subtype' of attribute for the parent class. - + Currently this is usable in conjunction with :meth:`.Query.join` and :meth:`.Query.outerjoin`. - + """ return RelationshipProperty.Comparator( self.property, @@ -345,7 +351,7 @@ class RelationshipProperty(StrategizedProperty): def in_(self, other): """Produce an IN clause - this is not implemented for :func:`~.orm.relationship`-based attributes at this time. - + """ raise NotImplementedError('in_() not yet supported for ' 'relationships. For a simple many-to-one, use ' @@ -362,15 +368,15 @@ class RelationshipProperty(StrategizedProperty): this will typically produce a clause such as:: - + mytable.related_id == - + Where ```` is the primary key of the given object. - + The ``==`` operator provides partial functionality for non- many-to-one comparisons: - + * Comparisons against collections are not supported. Use :meth:`~.RelationshipProperty.Comparator.contains`. * Compared to a scalar one-to-many, will produce a @@ -465,42 +471,42 @@ class RelationshipProperty(StrategizedProperty): def any(self, criterion=None, **kwargs): """Produce an expression that tests a collection against particular criterion, using EXISTS. - + An expression like:: - + session.query(MyClass).filter( MyClass.somereference.any(SomeRelated.x==2) ) - - + + Will produce a query like:: - + SELECT * FROM my_table WHERE EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id AND related.x=2) - + Because :meth:`~.RelationshipProperty.Comparator.any` uses a correlated subquery, its performance is not nearly as good when compared against large target tables as that of using a join. - + :meth:`~.RelationshipProperty.Comparator.any` is particularly useful for testing for empty collections:: - + session.query(MyClass).filter( ~MyClass.somereference.any() ) - + will produce:: - + SELECT * FROM my_table WHERE NOT EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id) - + :meth:`~.RelationshipProperty.Comparator.any` is only valid for collections, i.e. a :func:`.relationship` that has ``uselist=True``. For scalar references, use :meth:`~.RelationshipProperty.Comparator.has`. - + """ if not self.property.uselist: raise sa_exc.InvalidRequestError( @@ -515,14 +521,14 @@ class RelationshipProperty(StrategizedProperty): particular criterion, using EXISTS. An expression like:: - + session.query(MyClass).filter( MyClass.somereference.has(SomeRelated.x==2) ) - - + + Will produce a query like:: - + SELECT * FROM my_table WHERE EXISTS (SELECT 1 FROM related WHERE related.id==my_table.related_id AND related.x=2) @@ -531,12 +537,12 @@ class RelationshipProperty(StrategizedProperty): a correlated subquery, its performance is not nearly as good when compared against large target tables as that of using a join. - + :meth:`~.RelationshipProperty.Comparator.has` is only valid for scalar references, i.e. a :func:`.relationship` that has ``uselist=False``. For collection references, use :meth:`~.RelationshipProperty.Comparator.any`. - + """ if self.property.uselist: raise sa_exc.InvalidRequestError( @@ -547,44 +553,44 @@ class RelationshipProperty(StrategizedProperty): def contains(self, other, **kwargs): """Return a simple expression that tests a collection for containment of a particular item. - + :meth:`~.RelationshipProperty.Comparator.contains` is only valid for a collection, i.e. a :func:`~.orm.relationship` that implements one-to-many or many-to-many with ``uselist=True``. - + When used in a simple one-to-many context, an expression like:: - + MyClass.contains(other) - + Produces a clause like:: - + mytable.id == - + Where ```` is the value of the foreign key attribute on ``other`` which refers to the primary key of its parent object. From this it follows that :meth:`~.RelationshipProperty.Comparator.contains` is very useful when used with simple one-to-many operations. - + For many-to-many operations, the behavior of :meth:`~.RelationshipProperty.Comparator.contains` has more caveats. The association table will be rendered in the statement, producing an "implicit" join, that is, includes multiple tables in the FROM clause which are equated in the WHERE clause:: - + query(MyClass).filter(MyClass.contains(other)) - + Produces a query like:: - + SELECT * FROM my_table, my_association_table AS my_association_table_1 WHERE my_table.id = my_association_table_1.parent_id AND my_association_table_1.child_id = - + Where ```` would be the primary key of ``other``. From the above, it is clear that :meth:`~.RelationshipProperty.Comparator.contains` @@ -598,7 +604,7 @@ class RelationshipProperty(StrategizedProperty): a less-performant alternative using EXISTS, or refer to :meth:`.Query.outerjoin` as well as :ref:`ormtutorial_joins` for more details on constructing outer joins. - + """ if not self.property.uselist: raise sa_exc.InvalidRequestError( @@ -649,19 +655,19 @@ class RelationshipProperty(StrategizedProperty): """Implement the ``!=`` operator. In a many-to-one context, such as:: - + MyClass.some_prop != - + This will typically produce a clause such as:: - + mytable.related_id != - + Where ```` is the primary key of the given object. - + The ``!=`` operator provides partial functionality for non- many-to-one comparisons: - + * Comparisons against collections are not supported. Use :meth:`~.RelationshipProperty.Comparator.contains` @@ -682,7 +688,7 @@ class RelationshipProperty(StrategizedProperty): membership tests. * Comparisons against ``None`` given in a one-to-many or many-to-many context produce an EXISTS clause. - + """ if isinstance(other, (NoneType, expression._Null)): if self.property.direction == MANYTOONE: @@ -804,6 +810,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 @@ -818,7 +845,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' \ @@ -880,9 +907,9 @@ class RelationshipProperty(StrategizedProperty): def mapper(self): """Return the targeted :class:`.Mapper` for this :class:`.RelationshipProperty`. - + This is a lazy-initializing static attribute. - + """ if isinstance(self.argument, type): mapper_ = mapper.class_mapper(self.argument, @@ -952,9 +979,9 @@ class RelationshipProperty(StrategizedProperty): def _process_dependent_arguments(self): """Convert incoming configuration arguments to their proper form. - + Callables are resolved, ORM annotations removed. - + """ # accept callables for other attributes which may require # deferred initialization. This technique is used @@ -1011,10 +1038,10 @@ class RelationshipProperty(StrategizedProperty): def _determine_joins(self): """Determine the 'primaryjoin' and 'secondaryjoin' attributes, if not passed to the constructor already. - + This is based on analysis of the foreign key relationships between the parent and target mapped selectables. - + """ if self.secondaryjoin is not None and self.secondary is None: raise sa_exc.ArgumentError("Property '" + self.key @@ -1056,7 +1083,7 @@ class RelationshipProperty(StrategizedProperty): def _columns_are_mapped(self, *cols): """Return True if all columns in the given collection are mapped by the tables referenced by this :class:`.Relationship`. - + """ for c in cols: if self.secondary is not None \ @@ -1071,11 +1098,11 @@ class RelationshipProperty(StrategizedProperty): """Determine a list of "source"/"destination" column pairs based on the given join condition, as well as the foreign keys argument. - + "source" would be a column referenced by a foreign key, and "destination" would be the column who has a foreign key reference to "source". - + """ fks = self._user_defined_foreign_keys @@ -1186,7 +1213,7 @@ class RelationshipProperty(StrategizedProperty): def _determine_synchronize_pairs(self): """Resolve 'primary'/foreign' column pairs from the primaryjoin and secondaryjoin arguments. - + """ if self.local_remote_pairs: if not self._user_defined_foreign_keys: @@ -1221,10 +1248,10 @@ class RelationshipProperty(StrategizedProperty): def _determine_direction(self): """Determine if this relationship is one to many, many to one, many to many. - + This is derived from the primaryjoin, presence of "secondary", and in the case of self-referential the "remote side". - + """ if self.secondaryjoin is not None: self.direction = MANYTOMANY @@ -1304,7 +1331,7 @@ class RelationshipProperty(StrategizedProperty): """Determine pairs of columns representing "local" to "remote", where "local" columns are on the parent mapper, "remote" are on the target mapper. - + These pairs are used on the load side only to generate lazy loading clauses. 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 156686bc7..a8c9dea06 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -18,9 +18,11 @@ 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, \ - PASSIVE_NO_FETCH, NEVER_SET, ATTR_WAS_SET, NO_VALUE + PASSIVE_NO_FETCH, NEVER_SET, ATTR_WAS_SET, NO_VALUE,\ + PASSIVE_NO_INITIALIZE mapperlib = util.importlater("sqlalchemy.orm", "mapperlib") +sessionlib = util.importlater("sqlalchemy.orm", "session") import sys @@ -46,12 +48,67 @@ class InstanceState(object): self.callables = {} 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 @@ -62,11 +119,11 @@ class InstanceState(object): 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): @@ -110,35 +167,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__ @@ -173,7 +211,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) @@ -238,7 +276,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 @@ -256,7 +294,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: @@ -340,7 +378,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: @@ -419,7 +457,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): @@ -436,6 +474,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. @@ -528,7 +587,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.""" -- cgit v1.2.1