From: Mike Bayer Date: Tue, 3 Apr 2012 22:53:39 +0000 (-0400) Subject: begin implementing inspection system for #2208 X-Git-Tag: rel_0_8_0b1~474^2~2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=f1bdd4e4bbf8366ff7177ebc3ee6647f32fd414f;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git begin implementing inspection system for #2208 --- diff --git a/lib/sqlalchemy/__init__.py b/lib/sqlalchemy/__init__.py index 4e00437ea9..ee7bf4c6f9 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 71d97e65f8..b2a5a02eff 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 0000000000..9ce52beab5 --- /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 af9ef7841a..1012af67a9 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 e96b7549a9..4fa8dfe24f 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 4803ecdc3d..156686bc73 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 0c5f203a72..8d334ce17e 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: diff --git a/test/base/test_inspect.py b/test/base/test_inspect.py new file mode 100644 index 0000000000..b95b7d8c57 --- /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 f385a0fa23..e59849d98f 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 5def54e3a4..7431a3a836 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__ = () @@ -48,6 +49,48 @@ class FixtureTest(fixtures.MappedTest): class CompositePk(Base): 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, diff --git a/test/orm/test_inspect.py b/test/orm/test_inspect.py new file mode 100644 index 0000000000..3c824d8497 --- /dev/null +++ b/test/orm/test_inspect.py @@ -0,0 +1,194 @@ +"""test the inspection registry system.""" + +from test.lib.testing import eq_, assert_raises +from sqlalchemy import exc, util +from sqlalchemy import inspect +from test.orm import _fixtures +from sqlalchemy.orm import class_mapper, synonym +from sqlalchemy.orm.attributes import instance_state + +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_synonyms(self): + User = self.classes.User + syn = inspect(User).synonyms + + # TODO: some of the synonym debacle in 0.7 + # has led User.name_syn.property to be the + # ColumnProperty. not sure if we want that + # implicit jump in there though, perhaps get Query/etc. to + # call upon "effective_property" or something like that + + eq_(inspect(User).synonyms, { + "name_syn":class_mapper(User).get_property("name_syn") + }) + + # TODO: test all these accessors... + +""" +# column collection +>>> b.columns +[, ] + +# its a ColumnCollection +>>> b.columns.id + + +# i.e. from mapper +>>> b.primary_key +(, ) + +# i.e. from mapper +>>> b.local_table + + +# ColumnProperty +>>> b.attr.id.columns +[] + +# but perhaps we use a collection with some helpers +>>> b.attr.id.columns.first + + +# and a mapper? its None since this is a column +>>> b.attr.id.mapper +None + +# attr is basically the _props +>>> b.attr.keys() +['id', 'name', 'name_syn', 'addresses'] + +# b itself is likely just the mapper +>>> b + + +# get only column attributes +>>> b.column_attrs +[, ] + +# its a namespace +>>> b.column_attrs.id + + +# get only synonyms +>>> b.synonyms +[] + +# get only relationships +>>> b.relationships +[] + +# its a namespace +>>> b.relationships.addresses + + +# point inspect() at a class level attribute, +# basically returns ".property" +>>> b = inspect(User.addresses) +>>> b + + +# mapper +>>> b.mapper +
+ +# None columns collection, just like columnprop has empty mapper +>>> b.columns +None + +# the parent +>>> b.parent + + +# __clause_element__() +>>> b.expression +User.id==Address.user_id + +>>> inspect(User.id).expression + + + +# inspect works on instances ! +>>> u1 = User(id=3, name='x') +>>> b = inspect(u1) + +# what's b here ? probably InstanceState +>>> b + + +>>> b.attr.keys() +['id', 'name', 'name_syn', 'addresses'] + +# this is class level stuff - should this require b.mapper.columns ? +>>> b.columns +[, ] + +# does this return '3'? or an object? +>>> b.attr.id + + +# or does this ? +>>> b.attr.id.value +3 + +>>> b.attr.id.history + + +>>> 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] + +# the mapper level key +>>> b.identity_key +(User, [3]) + +>>> b.persistent +True + +>>> b.transient +False + +>>> b.deleted +False + +>>> b.detached +False + +>>> b.session + + +# the object. this navigates obj() +# of course, would be nice if it was b.obj... +>>> b.object_ + + +""" diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index 1c5f29b716..bec813f39d 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 bcc9768165..bd9f10d217 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