join together various class-level and attribute-level functions into a unified interface

Issue #2208 resolved
Mike Bayer repo owner created an issue

proposed: browse() inspect(). Usage of this function will replace:

class_mapper()

object_mapper()

object_session()

attribute_history()

instance_state()

SomeClass.someattr.property

SomeClass.someattr.property.columns

mapper.iterate_properties

mapper.get_property()

others ?

Inspector.from_engine() ? (non orm ! what other non-orm things could we inspect ?)

Some ideas:

class User(Base):
    __tablename__ = 'user'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    name_syn = synonym(name)
    addresses = relationship(Address)

# universal entry point is inspect()
>>> b = inspect(User)

# column collection
>>> b.columns
[column>, <name column>](<id)

# its a ColumnCollection
>>> b.columns.id
<id column>

# i.e. from mapper
>>> b.primary_key
(<id column>, )

# i.e. from mapper
>>> b.local_table
<user table>

# ColumnProperty
>>> b.attr.id.columns
[column>](<id)

# but perhaps we use a collection with some helpers
>>> b.attr.id.columns.first
<id column>

# and a mapper?  its None since this is a column
>>> b.attr.id.mapper
None

# attr is basically the _props
>>> b.attr.keys()
['name', 'name_syn', 'addresses']('id',)

# b itself is likely just the mapper
>>> b
<User mapper>

# get only column attributes
>>> b.column_attrs
[prop>, <name prop>](<id)

# its a namespace
>>> b.column_attrs.id
<id prop>

# get only synonyms
>>> b.synonyms
[syn prop>](<name)

# get only relationships
>>> b.relationships
[prop>](<addresses)

# its a namespace
>>> b.relationships.addresses
<addresses prop>


# point inspect() at a class level attribute,
# basically returns ".property"
>>> b = inspect(User.addresses)
>>> b
<addresses prop>

# mapper
>>> b.mapper
<Address mapper>

# None columns collection, just like columnprop has empty mapper
>>> b.columns
None

# the parent
>>> b.parent
<User mapper>

# __clause_element__()
>>> b.expression
User.id==Address.user_id

>>> inspect(User.id).expression
<id column with ORM annotations>


# inspect works on instances !  
>>> u1 = User(id=3, name='x')
>>> b = inspect(u1)

# what's b here ?  probably InstanceState
>>> b
<InstanceState>

>>> b.attr.keys()
['name', 'name_syn', 'addresses']('id',)

# this is class level stuff - should this require b.mapper.columns ?
>>> b.columns
[column>, <name column>](<id)

# does this return '3'?  or an object?
>>> b.attr.id
<magic attribute inspect thing>

# or does this ?
>>> b.attr.id.value 
3

>>> b.attr.id.history
<history object>

>>> b.attr.id.history.unchanged
3

>>> b.attr.id.history.deleted
None

# lets assume the object is persistent
>>> s = Session()
>>> s.add(u1)
>>> s.commit()

# big one - the primary key identity !  always
# works in query.get()
>>> b.identity
[3](3)

# the mapper level key
>>> b.identity_key
(User, [3](3))

>>> b.persistent
True

>>> b.transient
False

>>> b.deleted
False

>>> b.detached
False

>>> b.session
<session>

# the object.  this navigates obj()
# of course, would be nice if it was b.obj...
>>> b.object_
<User instance u1>

# should we allow state manipulation?
>>> b.expire()
>>> b.detach()
>>> b.make_transient()

# or is that TMTOWTDI ?   otherwise this:
>>> b.session.expire(b.object_)
>>> b.session.detach(b.object_)
>>> make_transient(b.object_)

Comments (13)

  1. Mike Bayer reporter

    for inspect() to be in core, we'd use a subscription system like event:

    inspection.register_lookup(type, lambda obj: attributes.manager_of_class(obj).mapper)
    inspection.register_lookup(object, attributes.instance_state)
    inspection.register_lookup(Engine, Inspector.from_engine)
    
    def inspect(obj):
        # do a more efficient multimethod approach here
        for reg in registry:
            if reg.handles_object_type(obj):
                return reg.getter_function(obj)
        else:
            raise InvalidRequestError(...)
    
  2. Mike Bayer reporter

    let's add mapper.properties too, removing the old error message, as mapper.iterate_properties is incorrectly named for an attribute.

  3. Mike Bayer reporter

    another idea:

    MyClass.someattribute.info

    a namespace that unions all the 'info' attributes of the mapped columns.

  4. Mike Bayer reporter

    here's part of mapper

    diff -r d60bc21fc69f70c8e7b63b7ed88483bbbe42250a lib/sqlalchemy/orm/mapper.py
    --- a/lib/sqlalchemy/orm/mapper.py  Sun Feb 26 19:42:16 2012 -0500
    +++ b/lib/sqlalchemy/orm/mapper.py  Sun Feb 26 21:30:09 2012 -0500
    @@ -33,6 +33,7 @@
     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 @@
                             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):
    
  5. Mike Bayer reporter

    OK I've got as far we need to go with this all up in that branch, a patch is attached which illustrates it.

  6. Mike Bayer reporter

    we will also need inspect() to work for an AliasedClass instance, and it will need to return a new inspection object where "mapped_table" returns the alias() object. "local_table" and such shouldn't be present, it will have "mapper" that leads to the mapper object. Some kind of clue that it's "aliased" as well so that it can be distinguished from inspect() on a regular mapped class.

    also consider AliasedClass.table returning the alias, though this is controversial....

  7. Log in to comment