diff options
author | Rodrigo Menezes <rodrigo.menezes@moat.com> | 2014-08-14 17:08:55 -0400 |
---|---|---|
committer | Rodrigo Menezes <rodrigo.menezes@moat.com> | 2014-08-14 17:08:55 -0400 |
commit | 8af9c7670e07037259dc89510559d34a4e7ccc6f (patch) | |
tree | 0c3a79ffbf82387177d1c236f4148731a0e06b4f | |
parent | 649f06759d933f4aacdfbb302e845e2bcb5e7641 (diff) | |
parent | e2d05259caf2c7c033a0a9376c0d3b7a1b040183 (diff) | |
download | sqlalchemy-8af9c7670e07037259dc89510559d34a4e7ccc6f.tar.gz |
Merge branch 'master' of https://github.com/rclmenezes/sqlalchemy
-rw-r--r-- | doc/build/changelog/changelog_10.rst | 35 | ||||
-rw-r--r-- | doc/build/orm/internals.rst | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/__init__.py | 5 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/base.py | 91 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/associationproxy.py | 6 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/hybrid.py | 12 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/base.py | 76 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/descriptor_props.py | 19 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/instrumentation.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/interfaces.py | 32 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 10 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/state.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/util.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/schema.py | 54 | ||||
-rw-r--r-- | lib/sqlalchemy/testing/plugin/plugin_base.py | 9 | ||||
-rw-r--r-- | lib/sqlalchemy/util/langhelpers.py | 4 | ||||
-rw-r--r-- | test/dialect/postgresql/test_reflection.py | 66 | ||||
-rw-r--r-- | test/ext/test_hybrid.py | 18 | ||||
-rw-r--r-- | test/orm/test_mapper.py | 4 | ||||
-rw-r--r-- | test/sql/test_metadata.py | 65 |
21 files changed, 415 insertions, 105 deletions
diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst index 1d501f85b..815de72c7 100644 --- a/doc/build/changelog/changelog_10.rst +++ b/doc/build/changelog/changelog_10.rst @@ -17,6 +17,33 @@ :version: 1.0.0 .. change:: + :tags: orm, feature + :tickets: 2963 + + The ``info`` parameter has been added to the constructor for + :class:`.SynonymProperty` and :class:`.ComparableProperty`. + + .. change:: + :tags: sql, feature + :tickets: 2963 + + The ``info`` parameter has been added as a constructor argument + to all schema constructs including :class:`.MetaData`, + :class:`.Index`, :class:`.ForeignKey`, :class:`.ForeignKeyConstraint`, + :class:`.UniqueConstraint`, :class:`.PrimaryKeyConstraint`, + :class:`.CheckConstraint`. + + .. change:: + :tags: orm, feature + :tickets: 2971 + + The :meth:`.InspectionAttr.info` collection is now moved down to + :class:`.InspectionAttr`, where in addition to being available + on all :class:`.MapperProperty` objects, it is also now available + on hybrid properties, association proxies, when accessed via + :attr:`.Mapper.all_orm_descriptors`. + + .. change:: :tags: sql, feature :tickets: 3027 :pullrequest: bitbucket:29 @@ -26,6 +53,14 @@ courtesy Malik Diarra. .. change:: + :tags: postgresql, feature + :pullreq: github:126 + + Added new method :meth:`.PGInspector.get_enums`, when using the + inspector for Postgresql will provide a list of ENUM types. + Pull request courtesy Ilya Pekelny. + + .. change:: :tags: mysql, bug The MySQL dialect will now disable :meth:`.ConnectionEvents.handle_error` diff --git a/doc/build/orm/internals.rst b/doc/build/orm/internals.rst index 857ea78d5..0283f6cac 100644 --- a/doc/build/orm/internals.rst +++ b/doc/build/orm/internals.rst @@ -27,7 +27,7 @@ sections, are listed here. :members: -.. autoclass:: sqlalchemy.orm.interfaces._InspectionAttr +.. autoclass:: sqlalchemy.orm.interfaces.InspectionAttr :members: diff --git a/lib/sqlalchemy/dialects/postgresql/__init__.py b/lib/sqlalchemy/dialects/postgresql/__init__.py index d755e6aa1..1cff8e3a0 100644 --- a/lib/sqlalchemy/dialects/postgresql/__init__.py +++ b/lib/sqlalchemy/dialects/postgresql/__init__.py @@ -13,7 +13,7 @@ from .base import \ INTEGER, BIGINT, SMALLINT, VARCHAR, CHAR, TEXT, NUMERIC, FLOAT, REAL, \ INET, CIDR, UUID, BIT, MACADDR, OID, DOUBLE_PRECISION, TIMESTAMP, TIME, \ DATE, BYTEA, BOOLEAN, INTERVAL, ARRAY, ENUM, dialect, array, Any, All, \ - TSVECTOR + TSVECTOR, DropEnumType from .constraints import ExcludeConstraint from .hstore import HSTORE, hstore from .json import JSON, JSONElement, JSONB @@ -26,5 +26,6 @@ __all__ = ( 'DOUBLE_PRECISION', 'TIMESTAMP', 'TIME', 'DATE', 'BYTEA', 'BOOLEAN', 'INTERVAL', 'ARRAY', 'ENUM', 'dialect', 'Any', 'All', 'array', 'HSTORE', 'hstore', 'INT4RANGE', 'INT8RANGE', 'NUMRANGE', 'DATERANGE', - 'TSRANGE', 'TSTZRANGE', 'json', 'JSON', 'JSONB', 'JSONElement' + 'TSRANGE', 'TSTZRANGE', 'json', 'JSON', 'JSONB', 'JSONElement', + 'DropEnumType' ) diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index b3506f5d2..19d2c7ca4 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -401,6 +401,23 @@ The value passed to the keyword argument will be simply passed through to the underlying CREATE INDEX command, so it *must* be a valid index type for your version of PostgreSQL. +Special Reflection Options +-------------------------- + +The :class:`.Inspector` used for the Postgresql backend is an instance +of :class:`.PGInspector`, which offers additional methods:: + + from sqlalchemy import create_engine, inspect + + engine = create_engine("postgresql+psycopg2://localhost/test") + insp = inspect(engine) # will be a PGInspector + + print(insp.get_enums()) + +.. autoclass:: PGInspector + :members: + + """ from collections import defaultdict import re @@ -1570,11 +1587,32 @@ class PGInspector(reflection.Inspector): reflection.Inspector.__init__(self, conn) def get_table_oid(self, table_name, schema=None): - """Return the oid from `table_name` and `schema`.""" + """Return the OID for the given table name.""" return self.dialect.get_table_oid(self.bind, table_name, schema, info_cache=self.info_cache) + def get_enums(self, schema=None): + """Return a list of ENUM objects. + + Each member is a dictionary containing these fields: + + * name - name of the enum + * schema - the schema name for the enum. + * visible - boolean, whether or not this enum is visible + in the default search path. + * labels - a list of string labels that apply to the enum. + + :param schema: schema name. If None, the default schema + (typically 'public') is used. May also be set to '*' to + indicate load enums for all schemas. + + .. versionadded:: 1.0.0 + + """ + schema = schema or self.default_schema_name + return self.dialect._load_enums(self.bind, schema) + class CreateEnumType(schema._CreateDropBase): __visit_name__ = "create_enum_type" @@ -2056,7 +2094,12 @@ class PGDialect(default.DefaultDialect): c = connection.execute(s, table_oid=table_oid) rows = c.fetchall() domains = self._load_domains(connection) - enums = self._load_enums(connection) + enums = dict( + ( + "%s.%s" % (rec['schema'], rec['name']) + if not rec['visible'] else rec['name'], rec) for rec in + self._load_enums(connection, schema='*') + ) # format columns columns = [] @@ -2130,10 +2173,9 @@ class PGDialect(default.DefaultDialect): elif attype in enums: enum = enums[attype] coltype = ENUM - if "." in attype: - kwargs['schema'], kwargs['name'] = attype.split('.') - else: - kwargs['name'] = attype + kwargs['name'] = enum['name'] + if not enum['visible']: + kwargs['schema'] = enum['schema'] args = tuple(enum['labels']) break elif attype in domains: @@ -2444,7 +2486,8 @@ class PGDialect(default.DefaultDialect): for name, uc in uniques.items() ] - def _load_enums(self, connection): + def _load_enums(self, connection, schema=None): + schema = schema or self.default_schema_name if not self.supports_native_enum: return {} @@ -2460,31 +2503,37 @@ class PGDialect(default.DefaultDialect): LEFT JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace LEFT JOIN pg_catalog.pg_enum e ON t.oid = e.enumtypid WHERE t.typtype = 'e' - ORDER BY "name", e.oid -- e.oid gives us label order """ + if schema != '*': + SQL_ENUMS += "AND n.nspname = :schema " + + # e.oid gives us label order within an enum + SQL_ENUMS += 'ORDER BY "schema", "name", e.oid' + s = sql.text(SQL_ENUMS, typemap={ 'attname': sqltypes.Unicode, 'label': sqltypes.Unicode}) + + if schema != '*': + s = s.bindparams(schema=schema) + c = connection.execute(s) - enums = {} + enums = [] + enum_by_name = {} for enum in c.fetchall(): - if enum['visible']: - # 'visible' just means whether or not the enum is in a - # schema that's on the search path -- or not overridden by - # a schema with higher precedence. If it's not visible, - # it will be prefixed with the schema-name when it's used. - name = enum['name'] - else: - name = "%s.%s" % (enum['schema'], enum['name']) - - if name in enums: - enums[name]['labels'].append(enum['label']) + key = (enum['schema'], enum['name']) + if key in enum_by_name: + enum_by_name[key]['labels'].append(enum['label']) else: - enums[name] = { + enum_by_name[key] = enum_rec = { + 'name': enum['name'], + 'schema': enum['schema'], + 'visible': enum['visible'], 'labels': [enum['label']], } + enums.append(enum_rec) return enums diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py index a987ab413..1aa68ac32 100644 --- a/lib/sqlalchemy/ext/associationproxy.py +++ b/lib/sqlalchemy/ext/associationproxy.py @@ -77,16 +77,16 @@ def association_proxy(target_collection, attr, **kw): ASSOCIATION_PROXY = util.symbol('ASSOCIATION_PROXY') -"""Symbol indicating an :class:`_InspectionAttr` that's +"""Symbol indicating an :class:`InspectionAttr` that's of type :class:`.AssociationProxy`. - Is assigned to the :attr:`._InspectionAttr.extension_type` + Is assigned to the :attr:`.InspectionAttr.extension_type` attibute. """ -class AssociationProxy(interfaces._InspectionAttr): +class AssociationProxy(interfaces.InspectionAttr): """A descriptor that presents a read/write view of an object attribute.""" is_attribute = False diff --git a/lib/sqlalchemy/ext/hybrid.py b/lib/sqlalchemy/ext/hybrid.py index 9f4e09e92..e2739d1de 100644 --- a/lib/sqlalchemy/ext/hybrid.py +++ b/lib/sqlalchemy/ext/hybrid.py @@ -634,10 +634,10 @@ from .. import util from ..orm import attributes, interfaces HYBRID_METHOD = util.symbol('HYBRID_METHOD') -"""Symbol indicating an :class:`_InspectionAttr` that's +"""Symbol indicating an :class:`InspectionAttr` that's of type :class:`.hybrid_method`. - Is assigned to the :attr:`._InspectionAttr.extension_type` + Is assigned to the :attr:`.InspectionAttr.extension_type` attibute. .. seealso:: @@ -647,10 +647,10 @@ HYBRID_METHOD = util.symbol('HYBRID_METHOD') """ HYBRID_PROPERTY = util.symbol('HYBRID_PROPERTY') -"""Symbol indicating an :class:`_InspectionAttr` that's +"""Symbol indicating an :class:`InspectionAttr` that's of type :class:`.hybrid_method`. - Is assigned to the :attr:`._InspectionAttr.extension_type` + Is assigned to the :attr:`.InspectionAttr.extension_type` attibute. .. seealso:: @@ -660,7 +660,7 @@ HYBRID_PROPERTY = util.symbol('HYBRID_PROPERTY') """ -class hybrid_method(interfaces._InspectionAttr): +class hybrid_method(interfaces.InspectionAttr): """A decorator which allows definition of a Python object method with both instance-level and class-level behavior. @@ -703,7 +703,7 @@ class hybrid_method(interfaces._InspectionAttr): return self -class hybrid_property(interfaces._InspectionAttr): +class hybrid_property(interfaces.InspectionAttr): """A decorator which allows definition of a Python descriptor with both instance-level and class-level behavior. diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 67e4dca9b..66197ba0e 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -30,7 +30,7 @@ from .base import state_str, instance_str @inspection._self_inspects class QueryableAttribute(interfaces._MappedAttribute, - interfaces._InspectionAttr, + interfaces.InspectionAttr, interfaces.PropComparator): """Base class for :term:`descriptor` objects that intercept attribute events on behalf of a :class:`.MapperProperty` diff --git a/lib/sqlalchemy/orm/base.py b/lib/sqlalchemy/orm/base.py index a85f59f37..3390ceec4 100644 --- a/lib/sqlalchemy/orm/base.py +++ b/lib/sqlalchemy/orm/base.py @@ -144,38 +144,42 @@ _INSTRUMENTOR = ('mapper', 'instrumentor') EXT_CONTINUE = util.symbol('EXT_CONTINUE') EXT_STOP = util.symbol('EXT_STOP') -ONETOMANY = util.symbol('ONETOMANY', - """Indicates the one-to-many direction for a :func:`.relationship`. +ONETOMANY = util.symbol( + 'ONETOMANY', + """Indicates the one-to-many direction for a :func:`.relationship`. -This symbol is typically used by the internals but may be exposed within -certain API features. + This symbol is typically used by the internals but may be exposed within + certain API features. -""") + """) -MANYTOONE = util.symbol('MANYTOONE', - """Indicates the many-to-one direction for a :func:`.relationship`. +MANYTOONE = util.symbol( + 'MANYTOONE', + """Indicates the many-to-one direction for a :func:`.relationship`. -This symbol is typically used by the internals but may be exposed within -certain API features. + This symbol is typically used by the internals but may be exposed within + certain API features. -""") + """) -MANYTOMANY = util.symbol('MANYTOMANY', - """Indicates the many-to-many direction for a :func:`.relationship`. +MANYTOMANY = util.symbol( + 'MANYTOMANY', + """Indicates the many-to-many direction for a :func:`.relationship`. -This symbol is typically used by the internals but may be exposed within -certain API features. + This symbol is typically used by the internals but may be exposed within + certain API features. -""") + """) -NOT_EXTENSION = util.symbol('NOT_EXTENSION', - """Symbol indicating an :class:`_InspectionAttr` that's - not part of sqlalchemy.ext. +NOT_EXTENSION = util.symbol( + 'NOT_EXTENSION', + """Symbol indicating an :class:`InspectionAttr` that's + not part of sqlalchemy.ext. - Is assigned to the :attr:`._InspectionAttr.extension_type` - attibute. + Is assigned to the :attr:`.InspectionAttr.extension_type` + attibute. -""") + """) _none_set = frozenset([None, NEVER_SET, PASSIVE_NO_RESULT]) @@ -419,7 +423,7 @@ def class_mapper(class_, configure=True): return mapper -class _InspectionAttr(object): +class InspectionAttr(object): """A base class applied to all ORM objects that can be returned by the :func:`.inspect` function. @@ -456,7 +460,7 @@ class _InspectionAttr(object): :class:`.QueryableAttribute` which handles attributes events on behalf of a :class:`.MapperProperty`. But can also be an extension type such as :class:`.AssociationProxy` or :class:`.hybrid_property`. - The :attr:`._InspectionAttr.extension_type` will refer to a constant + The :attr:`.InspectionAttr.extension_type` will refer to a constant identifying the specific subtype. .. seealso:: @@ -484,6 +488,32 @@ class _InspectionAttr(object): """ + @util.memoized_property + def info(self): + """Info dictionary associated with the object, allowing user-defined + data to be associated with this :class:`.InspectionAttr`. + + The dictionary is generated when first accessed. Alternatively, + it can be specified as a constructor argument to the + :func:`.column_property`, :func:`.relationship`, or :func:`.composite` + functions. + + .. versionadded:: 0.8 Added support for .info to all + :class:`.MapperProperty` subclasses. + + .. versionchanged:: 1.0.0 :attr:`.InspectionAttr.info` moved + from :class:`.MapperProperty` so that it can apply to a wider + variety of ORM and extension constructs. + + .. seealso:: + + :attr:`.QueryableAttribute.info` + + :attr:`.SchemaItem.info` + + """ + return {} + class _MappedAttribute(object): """Mixin for attributes which should be replaced by mapper-assigned diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index 5ed24b8c0..f0f9a6468 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -496,7 +496,7 @@ class SynonymProperty(DescriptorProperty): def __init__(self, name, map_column=None, descriptor=None, comparator_factory=None, - doc=None): + doc=None, info=None): """Denote an attribute name as a synonym to a mapped property, in that the attribute will mirror the value and expression behavior of another attribute. @@ -531,6 +531,11 @@ class SynonymProperty(DescriptorProperty): conjunction with the ``descriptor`` argument in order to link a user-defined descriptor as a "wrapper" for an existing column. + :param info: Optional data dictionary which will be populated into the + :attr:`.InspectionAttr.info` attribute of this object. + + .. versionadded:: 1.0.0 + :param comparator_factory: A subclass of :class:`.PropComparator` that will provide custom comparison behavior at the SQL expression level. @@ -556,6 +561,8 @@ class SynonymProperty(DescriptorProperty): self.descriptor = descriptor self.comparator_factory = comparator_factory self.doc = doc or (descriptor and descriptor.__doc__) or None + if info: + self.info = info util.set_creation_order(self) @@ -608,7 +615,8 @@ class SynonymProperty(DescriptorProperty): class ComparableProperty(DescriptorProperty): """Instruments a Python property for use in query expressions.""" - def __init__(self, comparator_factory, descriptor=None, doc=None): + def __init__( + self, comparator_factory, descriptor=None, doc=None, info=None): """Provides a method of applying a :class:`.PropComparator` to any Python descriptor attribute. @@ -670,10 +678,17 @@ class ComparableProperty(DescriptorProperty): The like-named descriptor will be automatically retrieved from the mapped class if left blank in a ``properties`` declaration. + :param info: Optional data dictionary which will be populated into the + :attr:`.InspectionAttr.info` attribute of this object. + + .. versionadded:: 1.0.0 + """ self.descriptor = descriptor self.comparator_factory = comparator_factory self.doc = doc or (descriptor and descriptor.__doc__) or None + if info: + self.info = info util.set_creation_order(self) def _comparator_factory(self, mapper): diff --git a/lib/sqlalchemy/orm/instrumentation.py b/lib/sqlalchemy/orm/instrumentation.py index f58b8807f..eb5b65baa 100644 --- a/lib/sqlalchemy/orm/instrumentation.py +++ b/lib/sqlalchemy/orm/instrumentation.py @@ -97,7 +97,7 @@ class ClassManager(dict): def _all_sqla_attributes(self, exclude=None): """return an iterator of all classbound attributes that are - implement :class:`._InspectionAttr`. + implement :class:`.InspectionAttr`. This includes :class:`.QueryableAttribute` as well as extension types such as :class:`.hybrid_property` and @@ -110,7 +110,7 @@ class ClassManager(dict): for key in set(supercls.__dict__).difference(exclude): exclude.add(key) val = supercls.__dict__[key] - if isinstance(val, interfaces._InspectionAttr): + if isinstance(val, interfaces.InspectionAttr): yield key, val def _attr_has_impl(self, key): diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 9bc1c3dd0..49ec99ce4 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -19,15 +19,15 @@ classes within should be considered mostly private. from __future__ import absolute_import -from .. import exc as sa_exc, util, inspect +from .. import util from ..sql import operators -from collections import deque from .base import (ONETOMANY, MANYTOONE, MANYTOMANY, EXT_CONTINUE, EXT_STOP, NOT_EXTENSION) -from .base import _InspectionAttr, _MappedAttribute -from .path_registry import PathRegistry +from .base import InspectionAttr, _MappedAttribute import collections +# imported later +MapperExtension = SessionExtension = AttributeExtension = None __all__ = ( 'AttributeExtension', @@ -47,7 +47,7 @@ __all__ = ( ) -class MapperProperty(_MappedAttribute, _InspectionAttr): +class MapperProperty(_MappedAttribute, InspectionAttr): """Manage the relationship of a ``Mapper`` to a single class attribute, as well as that attribute as it appears on individual instances of the class, including attribute instrumentation, @@ -109,28 +109,6 @@ class MapperProperty(_MappedAttribute, _InspectionAttr): def instrument_class(self, mapper): # pragma: no-coverage raise NotImplementedError() - @util.memoized_property - def info(self): - """Info dictionary associated with the object, allowing user-defined - data to be associated with this :class:`.MapperProperty`. - - The dictionary is generated when first accessed. Alternatively, - it can be specified as a constructor argument to the - :func:`.column_property`, :func:`.relationship`, or :func:`.composite` - functions. - - .. versionadded:: 0.8 Added support for .info to all - :class:`.MapperProperty` subclasses. - - .. seealso:: - - :attr:`.QueryableAttribute.info` - - :attr:`.SchemaItem.info` - - """ - return {} - _configure_started = False _configure_finished = False diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 7e5166393..06ec2bf14 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -26,7 +26,7 @@ from ..sql import expression, visitors, operators, util as sql_util from . import instrumentation, attributes, exc as orm_exc, loading from . import properties from . import util as orm_util -from .interfaces import MapperProperty, _InspectionAttr, _MappedAttribute +from .interfaces import MapperProperty, InspectionAttr, _MappedAttribute from .base import _class_to_mapper, _state_mapper, class_mapper, \ state_str, _INSTRUMENTOR @@ -52,7 +52,7 @@ _CONFIGURE_MUTEX = util.threading.RLock() @inspection._self_inspects @log.class_logger -class Mapper(_InspectionAttr): +class Mapper(InspectionAttr): """Define the correlation of class attributes to database table columns. @@ -1979,7 +1979,7 @@ class Mapper(_InspectionAttr): @util.memoized_property def all_orm_descriptors(self): - """A namespace of all :class:`._InspectionAttr` attributes associated + """A namespace of all :class:`.InspectionAttr` attributes associated with the mapped class. These attributes are in all cases Python :term:`descriptors` @@ -1988,13 +1988,13 @@ class Mapper(_InspectionAttr): This namespace includes attributes that are mapped to the class as well as attributes declared by extension modules. It includes any Python descriptor type that inherits from - :class:`._InspectionAttr`. This includes + :class:`.InspectionAttr`. This includes :class:`.QueryableAttribute`, as well as extension types such as :class:`.hybrid_property`, :class:`.hybrid_method` and :class:`.AssociationProxy`. To distinguish between mapped attributes and extension attributes, - the attribute :attr:`._InspectionAttr.extension_type` will refer + the attribute :attr:`.InspectionAttr.extension_type` will refer to a constant that distinguishes between different extension types. When dealing with a :class:`.QueryableAttribute`, the diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index a9024b468..fe8ccd222 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -21,7 +21,7 @@ from .base import PASSIVE_NO_RESULT, SQL_OK, NEVER_SET, ATTR_WAS_SET, \ from . import base -class InstanceState(interfaces._InspectionAttr): +class InstanceState(interfaces.InspectionAttr): """tracks state information at the instance level. The :class:`.InstanceState` is a key object used by the diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 215de5f4b..ea7bfc294 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -15,7 +15,7 @@ import re from .base import instance_str, state_str, state_class_str, attribute_str, \ state_attribute_str, object_mapper, object_state, _none_set from .base import class_mapper, _class_to_mapper -from .base import _InspectionAttr +from .base import InspectionAttr from .path_registry import PathRegistry all_cascades = frozenset(("delete", "delete-orphan", "all", "merge", @@ -412,7 +412,7 @@ class AliasedClass(object): id(self), self._aliased_insp._target.__name__) -class AliasedInsp(_InspectionAttr): +class AliasedInsp(InspectionAttr): """Provide an inspection interface for an :class:`.AliasedClass` object. diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py index 69b3af306..8099dca75 100644 --- a/lib/sqlalchemy/sql/schema.py +++ b/lib/sqlalchemy/sql/schema.py @@ -76,7 +76,7 @@ class SchemaItem(SchemaEventTarget, visitors.Visitable): return [] def __repr__(self): - return util.generic_repr(self) + return util.generic_repr(self, omit_kwarg=['info']) @property @util.deprecated('0.9', 'Use ``<obj>.name.quote``') @@ -1403,6 +1403,7 @@ class ForeignKey(DialectKWArgs, SchemaItem): def __init__(self, column, _constraint=None, use_alter=False, name=None, onupdate=None, ondelete=None, deferrable=None, initially=None, link_to_name=False, match=None, + info=None, **dialect_kw): """ Construct a column-level FOREIGN KEY. @@ -1453,6 +1454,11 @@ class ForeignKey(DialectKWArgs, SchemaItem): DDL for this constraint. Typical values include SIMPLE, PARTIAL and FULL. + :param info: Optional data dictionary which will be populated into the + :attr:`.SchemaItem.info` attribute of this object. + + .. versionadded:: 1.0.0 + :param \**dialect_kw: Additional keyword arguments are dialect specific, and passed in the form ``<dialectname>_<argname>``. The arguments are ultimately handled by a corresponding @@ -1499,6 +1505,8 @@ class ForeignKey(DialectKWArgs, SchemaItem): self.initially = initially self.link_to_name = link_to_name self.match = match + if info: + self.info = info self._unvalidated_dialect_kw = dialect_kw def __repr__(self): @@ -2223,7 +2231,7 @@ class Constraint(DialectKWArgs, SchemaItem): __visit_name__ = 'constraint' def __init__(self, name=None, deferrable=None, initially=None, - _create_rule=None, + _create_rule=None, info=None, **dialect_kw): """Create a SQL constraint. @@ -2238,6 +2246,11 @@ class Constraint(DialectKWArgs, SchemaItem): Optional string. If set, emit INITIALLY <value> when issuing DDL for this constraint. + :param info: Optional data dictionary which will be populated into the + :attr:`.SchemaItem.info` attribute of this object. + + .. versionadded:: 1.0.0 + :param _create_rule: a callable which is passed the DDLCompiler object during compilation. Returns True or False to signal inline generation of @@ -2265,6 +2278,8 @@ class Constraint(DialectKWArgs, SchemaItem): self.name = name self.deferrable = deferrable self.initially = initially + if info: + self.info = info self._create_rule = _create_rule util.set_creation_order(self) self._validate_dialect_kwargs(dialect_kw) @@ -2381,7 +2396,7 @@ class CheckConstraint(Constraint): """ def __init__(self, sqltext, name=None, deferrable=None, - initially=None, table=None, _create_rule=None, + initially=None, table=None, info=None, _create_rule=None, _autoattach=True): """Construct a CHECK constraint. @@ -2404,10 +2419,15 @@ class CheckConstraint(Constraint): Optional string. If set, emit INITIALLY <value> when issuing DDL for this constraint. + :param info: Optional data dictionary which will be populated into the + :attr:`.SchemaItem.info` attribute of this object. + + .. versionadded:: 1.0.0 + """ super(CheckConstraint, self).\ - __init__(name, deferrable, initially, _create_rule) + __init__(name, deferrable, initially, _create_rule, info=info) self.sqltext = _literal_as_text(sqltext) if table is not None: self._set_parent_with_dispatch(table) @@ -2463,7 +2483,7 @@ class ForeignKeyConstraint(Constraint): def __init__(self, columns, refcolumns, name=None, onupdate=None, ondelete=None, deferrable=None, initially=None, use_alter=False, link_to_name=False, match=None, - table=None, **dialect_kw): + table=None, info=None, **dialect_kw): """Construct a composite-capable FOREIGN KEY. :param columns: A sequence of local column names. The named columns @@ -2508,6 +2528,11 @@ class ForeignKeyConstraint(Constraint): DDL for this constraint. Typical values include SIMPLE, PARTIAL and FULL. + :param info: Optional data dictionary which will be populated into the + :attr:`.SchemaItem.info` attribute of this object. + + .. versionadded:: 1.0.0 + :param \**dialect_kw: Additional keyword arguments are dialect specific, and passed in the form ``<dialectname>_<argname>``. See the documentation regarding an individual dialect at @@ -2517,7 +2542,7 @@ class ForeignKeyConstraint(Constraint): """ super(ForeignKeyConstraint, self).\ - __init__(name, deferrable, initially, **dialect_kw) + __init__(name, deferrable, initially, info=info, **dialect_kw) self.onupdate = onupdate self.ondelete = ondelete @@ -2888,6 +2913,11 @@ class Index(DialectKWArgs, ColumnCollectionMixin, SchemaItem): the index. Works in the same manner as that of :paramref:`.Column.quote`. + :param info=None: Optional data dictionary which will be populated + into the :attr:`.SchemaItem.info` attribute of this object. + + .. versionadded:: 1.0.0 + :param \**kw: Additional keyword arguments not mentioned above are dialect specific, and passed in the form ``<dialectname>_<argname>``. See the documentation regarding an @@ -2910,6 +2940,8 @@ class Index(DialectKWArgs, ColumnCollectionMixin, SchemaItem): self.expressions = expressions self.name = quoted_name(name, kw.pop("quote", None)) self.unique = kw.pop('unique', False) + if 'info' in kw: + self.info = kw.pop('info') self._validate_dialect_kwargs(kw) # will call _set_parent() if table-bound column @@ -3020,7 +3052,8 @@ class MetaData(SchemaItem): def __init__(self, bind=None, reflect=False, schema=None, quote_schema=None, - naming_convention=DEFAULT_NAMING_CONVENTION + naming_convention=DEFAULT_NAMING_CONVENTION, + info=None ): """Create a new MetaData object. @@ -3046,6 +3079,11 @@ class MetaData(SchemaItem): :class:`.Sequence`, and other objects which make usage of the local ``schema`` name. + :param info: Optional data dictionary which will be populated into the + :attr:`.SchemaItem.info` attribute of this object. + + .. versionadded:: 1.0.0 + :param naming_convention: a dictionary referring to values which will establish default naming conventions for :class:`.Constraint` and :class:`.Index` objects, for those objects which are not given @@ -3117,6 +3155,8 @@ class MetaData(SchemaItem): self.tables = util.immutabledict() self.schema = quoted_name(schema, quote_schema) self.naming_convention = naming_convention + if info: + self.info = info self._schemas = set() self._sequences = {} self._fk_memos = collections.defaultdict(list) diff --git a/lib/sqlalchemy/testing/plugin/plugin_base.py b/lib/sqlalchemy/testing/plugin/plugin_base.py index 4c245e9e9..c02f0556b 100644 --- a/lib/sqlalchemy/testing/plugin/plugin_base.py +++ b/lib/sqlalchemy/testing/plugin/plugin_base.py @@ -315,6 +315,7 @@ def _setup_requirements(argument): @post def _prep_testing_database(options, file_config): from sqlalchemy.testing import config + from sqlalchemy.testing.exclusions import against from sqlalchemy import schema, inspect if options.dropfirst: @@ -358,6 +359,14 @@ def _prep_testing_database(options, file_config): schema="test_schema") )) + if against(cfg, "postgresql"): + from sqlalchemy.dialects import postgresql + for enum in inspector.get_enums("*"): + e.execute(postgresql.DropEnumType( + postgresql.ENUM( + name=enum['name'], + schema=enum['schema']))) + @post def _set_table_options(options, file_config): diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index 8d6fe5a28..828e8f1f3 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -435,7 +435,7 @@ def unbound_method_to_callable(func_or_cls): return func_or_cls -def generic_repr(obj, additional_kw=(), to_inspect=None): +def generic_repr(obj, additional_kw=(), to_inspect=None, omit_kwarg=()): """Produce a __repr__() based on direct association of the __init__() specification vs. same-named attributes present. @@ -484,6 +484,8 @@ def generic_repr(obj, additional_kw=(), to_inspect=None): output.extend([repr(val) for val in getattr(obj, vargs)]) for arg, defval in kw_args.items(): + if arg in omit_kwarg: + continue try: val = getattr(obj, arg, missing) if val is not missing and val != defval: diff --git a/test/dialect/postgresql/test_reflection.py b/test/dialect/postgresql/test_reflection.py index 313be0b37..678c35881 100644 --- a/test/dialect/postgresql/test_reflection.py +++ b/test/dialect/postgresql/test_reflection.py @@ -1,5 +1,6 @@ # coding: utf-8 +from sqlalchemy.engine import reflection from sqlalchemy.testing.assertions import eq_, assert_raises, \ AssertsExecutionResults from sqlalchemy.testing import fixtures @@ -722,6 +723,71 @@ class ReflectionTest(fixtures.TestBase): for fk in fks: eq_(fk, fk_ref[fk['name']]) + @testing.provide_metadata + def test_inspect_enums_schema(self): + conn = testing.db.connect() + enum_type = postgresql.ENUM( + 'sad', 'ok', 'happy', name='mood', + schema='test_schema', + metadata=self.metadata) + enum_type.create(conn) + inspector = reflection.Inspector.from_engine(conn.engine) + eq_( + inspector.get_enums('test_schema'), [{ + 'visible': False, + 'name': 'mood', + 'schema': 'test_schema', + 'labels': ['sad', 'ok', 'happy'] + }]) + + @testing.provide_metadata + def test_inspect_enums(self): + enum_type = postgresql.ENUM( + 'cat', 'dog', 'rat', name='pet', metadata=self.metadata) + enum_type.create(testing.db) + inspector = reflection.Inspector.from_engine(testing.db) + eq_(inspector.get_enums(), [ + { + 'visible': True, + 'labels': ['cat', 'dog', 'rat'], + 'name': 'pet', + 'schema': 'public' + }]) + + @testing.provide_metadata + def test_inspect_enums_star(self): + enum_type = postgresql.ENUM( + 'cat', 'dog', 'rat', name='pet', metadata=self.metadata) + schema_enum_type = postgresql.ENUM( + 'sad', 'ok', 'happy', name='mood', + schema='test_schema', + metadata=self.metadata) + enum_type.create(testing.db) + schema_enum_type.create(testing.db) + inspector = reflection.Inspector.from_engine(testing.db) + + eq_(inspector.get_enums(), [ + { + 'visible': True, + 'labels': ['cat', 'dog', 'rat'], + 'name': 'pet', + 'schema': 'public' + }]) + + eq_(inspector.get_enums('*'), [ + { + 'visible': True, + 'labels': ['cat', 'dog', 'rat'], + 'name': 'pet', + 'schema': 'public' + }, + { + 'visible': False, + 'name': 'mood', + 'schema': 'test_schema', + 'labels': ['sad', 'ok', 'happy'] + }]) + class CustomTypeReflectionTest(fixtures.TestBase): diff --git a/test/ext/test_hybrid.py b/test/ext/test_hybrid.py index e7f392a33..b895d2fb2 100644 --- a/test/ext/test_hybrid.py +++ b/test/ext/test_hybrid.py @@ -5,6 +5,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext import hybrid from sqlalchemy.testing import eq_, AssertsCompiledSQL, assert_raises_message from sqlalchemy.testing import fixtures +from sqlalchemy import inspect class PropertyComparatorTest(fixtures.TestBase, AssertsCompiledSQL): __dialect__ = 'default' @@ -140,6 +141,14 @@ class PropertyExpressionTest(fixtures.TestBase, AssertsCompiledSQL): return A, B + def test_info(self): + A = self._fixture() + inspect(A).all_orm_descriptors.value.info["some key"] = "some value" + eq_( + inspect(A).all_orm_descriptors.value.info, + {"some key": "some value"} + ) + def test_set_get(self): A = self._fixture() a1 = A(value=5) @@ -267,6 +276,15 @@ class MethodExpressionTest(fixtures.TestBase, AssertsCompiledSQL): "foo(a.value, :foo_1) + :foo_2" ) + def test_info(self): + A = self._fixture() + inspect(A).all_orm_descriptors.value.info["some key"] = "some value" + eq_( + inspect(A).all_orm_descriptors.value.info, + {"some key": "some value"} + ) + + def test_aliased_expression(self): A = self._fixture() self.assert_compile( diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index e33c93977..0a9cbfc71 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -414,7 +414,9 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL): for constructor, args in [ (column_property, (users.c.name,)), (relationship, (Address,)), - (composite, (MyComposite, 'id', 'name')) + (composite, (MyComposite, 'id', 'name')), + (synonym, 'foo'), + (comparable_property, 'foo') ]: obj = constructor(info={"x": "y"}, *args) eq_(obj.info, {"x": "y"}) diff --git a/test/sql/test_metadata.py b/test/sql/test_metadata.py index 3a252c646..ff2755ab1 100644 --- a/test/sql/test_metadata.py +++ b/test/sql/test_metadata.py @@ -956,6 +956,71 @@ class ToMetaDataTest(fixtures.TestBase, ComparesTables): 'mytable.myid = othertable.myid') +class InfoTest(fixtures.TestBase): + def test_metadata_info(self): + m1 = MetaData() + eq_(m1.info, {}) + + m1 = MetaData(info={"foo": "bar"}) + eq_(m1.info, {"foo": "bar"}) + + def test_foreignkey_constraint_info(self): + fkc = ForeignKeyConstraint(['a'], ['b'], name='bar') + eq_(fkc.info, {}) + + fkc = ForeignKeyConstraint( + ['a'], ['b'], name='bar', info={"foo": "bar"}) + eq_(fkc.info, {"foo": "bar"}) + + def test_foreignkey_info(self): + fkc = ForeignKey('a') + eq_(fkc.info, {}) + + fkc = ForeignKey('a', info={"foo": "bar"}) + eq_(fkc.info, {"foo": "bar"}) + + def test_primarykey_constraint_info(self): + pkc = PrimaryKeyConstraint('a', name='x') + eq_(pkc.info, {}) + + pkc = PrimaryKeyConstraint('a', name='x', info={'foo': 'bar'}) + eq_(pkc.info, {'foo': 'bar'}) + + def test_unique_constraint_info(self): + uc = UniqueConstraint('a', name='x') + eq_(uc.info, {}) + + uc = UniqueConstraint('a', name='x', info={'foo': 'bar'}) + eq_(uc.info, {'foo': 'bar'}) + + def test_check_constraint_info(self): + cc = CheckConstraint('foo=bar', name='x') + eq_(cc.info, {}) + + cc = CheckConstraint('foo=bar', name='x', info={'foo': 'bar'}) + eq_(cc.info, {'foo': 'bar'}) + + def test_index_info(self): + ix = Index('x', 'a') + eq_(ix.info, {}) + + ix = Index('x', 'a', info={'foo': 'bar'}) + eq_(ix.info, {'foo': 'bar'}) + + def test_column_info(self): + c = Column('x', Integer) + eq_(c.info, {}) + + c = Column('x', Integer, info={'foo': 'bar'}) + eq_(c.info, {'foo': 'bar'}) + + def test_table_info(self): + t = Table('x', MetaData()) + eq_(t.info, {}) + + t = Table('x', MetaData(), info={'foo': 'bar'}) + eq_(t.info, {'foo': 'bar'}) + class TableTest(fixtures.TestBase, AssertsCompiledSQL): @testing.skip_if('mssql', 'different col format') |