From: Mike Bayer Date: Fri, 7 Oct 2022 18:03:16 +0000 (-0400) Subject: rename MappedCollection and related X-Git-Tag: rel_2_0_0b1~8^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4ef7bcf580844a431c5354896e954ca4ce1042ce;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git rename MappedCollection and related For consistency with the prominent ORM concept :class:`_orm.Mapped`, the names of the dictionary-oriented collections, :func:`_orm.attribute_mapped_collection`, :func:`_orm.column_mapped_collection`, and :class:`_orm.MappedCollection`, are changed to :func:`_orm.attribute_keyed_dict`, :func:`_orm.column_keyed_dict` and :class:`_orm.KeyFuncDict`, using the phrase "dict" to minimize any confusion against the term "mapped". The old names will remain indefinitely with no schedule for removal. Docs here are also updated for typing as we can type these collections as ``Mapped[dict[str, cls]]``, don't need KeyFuncDict / MappedCollection for these Fixes: #8608 Change-Id: Ib5cf63e0aef1c389e023a75e454bb21f9d779b54 --- diff --git a/doc/build/changelog/unreleased_20/8608.rst b/doc/build/changelog/unreleased_20/8608.rst new file mode 100644 index 0000000000..548e8d6644 --- /dev/null +++ b/doc/build/changelog/unreleased_20/8608.rst @@ -0,0 +1,12 @@ +.. change:: + :tags: orm, change + :tickets: 8608 + + For consistency with the prominent ORM concept :class:`_orm.Mapped`, the + names of the dictionary-oriented collections, + :func:`_orm.attribute_mapped_collection`, + :func:`_orm.column_mapped_collection`, and :class:`_orm.MappedCollection`, + are changed to :func:`_orm.attribute_keyed_dict`, + :func:`_orm.column_keyed_dict` and :class:`_orm.KeyFuncDict`, using the + phrase "dict" to minimize any confusion against the term "mapped". The old + names will remain indefinitely with no schedule for removal. diff --git a/doc/build/orm/collection_api.rst b/doc/build/orm/collection_api.rst index 8f830f4274..aba0ca104c 100644 --- a/doc/build/orm/collection_api.rst +++ b/doc/build/orm/collection_api.rst @@ -15,7 +15,6 @@ This section presents additional information about collection configuration and techniques. -.. currentmodule:: sqlalchemy.orm.collections .. _custom_collections: @@ -144,25 +143,25 @@ Dictionary Collections A little extra detail is needed when using a dictionary as a collection. This because objects are always loaded from the database as lists, and a key-generation strategy must be available to populate the dictionary correctly. The -:func:`.attribute_mapped_collection` function is by far the most common way +:func:`.attribute_keyed_dict` function is by far the most common way to achieve a simple dictionary collection. It produces a dictionary class that will apply a particular attribute of the mapped class as a key. Below we map an ``Item`` class containing a dictionary of ``Note`` items keyed to the ``Note.keyword`` attribute. -When using :func:`.attribute_mapped_collection`, the :class:`_orm.Mapped` -annotation may be typed using the :class:`_orm.MappedCollection` -type, however the :paramref:`_orm.relationship.collection_class` parameter -is required in this case so that the :func:`.attribute_mapped_collection` +When using :func:`.attribute_keyed_dict`, the :class:`_orm.Mapped` +annotation may be typed using the :class:`_orm.KeyFuncDict` +or just plain ``dict`` as illustrated in the following example. However, +the :paramref:`_orm.relationship.collection_class` parameter +is required in this case so that the :func:`.attribute_keyed_dict` may be appropriately parametrized:: from typing import Optional from sqlalchemy import ForeignKey - from sqlalchemy.orm import attribute_mapped_collection + from sqlalchemy.orm import attribute_keyed_dict from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship - from sqlalchemy.orm import MappedCollection class Base(DeclarativeBase): @@ -174,8 +173,8 @@ may be appropriately parametrized:: id: Mapped[int] = mapped_column(primary_key=True) - notes: Mapped[MappedCollection[str, "Note"]] = relationship( - collection_class=attribute_mapped_collection("keyword"), + notes: Mapped[dict[str, "Note"]] = relationship( + collection_class=attribute_keyed_dict("keyword"), cascade="all, delete-orphan", ) @@ -199,7 +198,7 @@ may be appropriately parametrized:: >>> item.notes.items() {'a': <__main__.Note object at 0x2eaaf0>} -:func:`.attribute_mapped_collection` will ensure that +:func:`.attribute_keyed_dict` will ensure that the ``.keyword`` attribute of each ``Note`` complies with the key in the dictionary. Such as, when assigning to ``Item.notes``, the dictionary key we supply must match that of the actual ``Note`` object:: @@ -210,7 +209,7 @@ key we supply must match that of the actual ``Note`` object:: "b": Note("b", "btext"), } -The attribute which :func:`.attribute_mapped_collection` uses as a key +The attribute which :func:`.attribute_keyed_dict` uses as a key does not need to be mapped at all! Using a regular Python ``@property`` allows virtually any detail or combination of details about the object to be used as the key, as below when we establish it as a tuple of ``Note.keyword`` and the first ten letters @@ -221,8 +220,8 @@ of the ``Note.text`` field:: id: Mapped[int] = mapped_column(primary_key=True) - notes: Mapped[MappedCollection[str, "Note"]] = relationship( - collection_class=attribute_mapped_collection("note_key"), + notes: Mapped[dict[str, "Note"]] = relationship( + collection_class=attribute_keyed_dict("note_key"), back_populates="item", cascade="all, delete-orphan", ) @@ -257,11 +256,11 @@ is added to the ``Item.notes`` dictionary and the key is generated for us automa >>> item.notes {('a', 'atext'): <__main__.Note object at 0x2eaaf0>} -Other built-in dictionary types include :func:`.column_mapped_collection`, -which is almost like :func:`.attribute_mapped_collection` except given the :class:`_schema.Column` +Other built-in dictionary types include :func:`.column_keyed_dict`, +which is almost like :func:`.attribute_keyed_dict` except given the :class:`_schema.Column` object directly:: - from sqlalchemy.orm import column_mapped_collection + from sqlalchemy.orm import column_keyed_dict class Item(Base): @@ -269,13 +268,13 @@ object directly:: id: Mapped[int] = mapped_column(primary_key=True) - notes: Mapped[MappedCollection[str, "Note"]] = relationship( - collection_class=column_mapped_collection(Note.__table__.c.keyword), + notes: Mapped[dict[str, "Note"]] = relationship( + collection_class=column_keyed_dict(Note.__table__.c.keyword), cascade="all, delete-orphan", ) as well as :func:`.mapped_collection` which is passed any callable function. -Note that it's usually easier to use :func:`.attribute_mapped_collection` along +Note that it's usually easier to use :func:`.attribute_keyed_dict` along with a ``@property`` as mentioned earlier:: from sqlalchemy.orm import mapped_collection @@ -286,7 +285,7 @@ with a ``@property`` as mentioned earlier:: id: Mapped[int] = mapped_column(primary_key=True) - notes: Mapped[MappedCollection[str, "Note"]] = relationship( + notes: Mapped[dict[str, "Note"]] = relationship( collection_class=mapped_collection(lambda note: note.text[0:10]), cascade="all, delete-orphan", ) @@ -300,7 +299,7 @@ for examples. Dealing with Key Mutations and back-populating for Dictionary collections ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -When using :func:`.attribute_mapped_collection`, the "key" for the dictionary +When using :func:`.attribute_keyed_dict`, the "key" for the dictionary is taken from an attribute on the target object. **Changes to this key are not tracked**. This means that the key must be assigned towards when it is first used, and if the key changes, the collection will not be mutated. @@ -312,8 +311,8 @@ to populate an attribute mapped collection. Given the following:: id: Mapped[int] = mapped_column(primary_key=True) - bs: Mapped[MappedCollection[str, "B"]] = relationship( - collection_class=attribute_mapped_collection("data"), + bs: Mapped[dict[str, "B"]] = relationship( + collection_class=attribute_keyed_dict("data"), back_populates="a", ) @@ -376,12 +375,6 @@ collection as well:: obj.a.bs[value] = obj obj.a.bs.pop(previous) -.. autofunction:: attribute_mapped_collection - -.. autofunction:: column_mapped_collection - -.. autofunction:: mapped_collection - .. _orm_custom_collection: Custom Collection Implementations @@ -547,44 +540,41 @@ interface marked for SQLAlchemy's use. Append and remove methods will be called with a mapped entity as the single argument, and iterator methods are called with no arguments and must return an iterator. -.. autoclass:: collection - :members: - .. _dictionary_collections: Custom Dictionary-Based Collections ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :class:`.MappedCollection` class can be used as +The :class:`.KeyFuncDict` class can be used as a base class for your custom types or as a mix-in to quickly add ``dict`` collection support to other classes. It uses a keying function to delegate to ``__setitem__`` and ``__delitem__``: .. sourcecode:: python+sql - from sqlalchemy.orm.collections import MappedCollection + from sqlalchemy.orm.collections import KeyFuncDict - class MyNodeMap(MappedCollection): + class MyNodeMap(KeyFuncDict): """Holds 'Node' objects, keyed by the 'name' attribute.""" def __init__(self, *args, **kw): super().__init__(keyfunc=lambda node: node.name) dict.__init__(self, *args, **kw) -When subclassing :class:`.MappedCollection`, user-defined versions +When subclassing :class:`.KeyFuncDict`, user-defined versions of ``__setitem__()`` or ``__delitem__()`` should be decorated with :meth:`.collection.internally_instrumented`, **if** they call down -to those same methods on :class:`.MappedCollection`. This because the methods -on :class:`.MappedCollection` are already instrumented - calling them +to those same methods on :class:`.KeyFuncDict`. This because the methods +on :class:`.KeyFuncDict` are already instrumented - calling them from within an already instrumented call can cause events to be fired off repeatedly, or inappropriately, leading to internal state corruption in rare cases:: - from sqlalchemy.orm.collections import MappedCollection, collection + from sqlalchemy.orm.collections import KeyFuncDict, collection - class MyMappedCollection(MappedCollection): + class MyKeyFuncDict(KeyFuncDict): """Use @internally_instrumented when your methods call down to already-instrumented methods. @@ -593,12 +583,12 @@ rare cases:: @collection.internally_instrumented def __setitem__(self, key, value, _sa_initiator=None): # do something with key, value - super(MyMappedCollection, self).__setitem__(key, value, _sa_initiator) + super(MyKeyFuncDict, self).__setitem__(key, value, _sa_initiator) @collection.internally_instrumented def __delitem__(self, key, _sa_initiator=None): # do something with key - super(MyMappedCollection, self).__delitem__(key, _sa_initiator) + super(MyKeyFuncDict, self).__delitem__(key, _sa_initiator) The ORM understands the ``dict`` interface just like lists and sets, and will automatically instrument all "dict-like" methods if you choose to subclass @@ -607,8 +597,6 @@ must decorate appender and remover methods, however- there are no compatible methods in the basic dictionary interface for SQLAlchemy to use by default. Iteration will go through ``values()`` unless otherwise decorated. -.. autoclass:: sqlalchemy.orm.MappedCollection - :members: Instrumentation and Custom Types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -634,13 +622,39 @@ to restrict the decorations to just your usage in relationships. For example: The ORM uses this approach for built-ins, quietly substituting a trivial subclass when a ``list``, ``set`` or ``dict`` is used directly. +Collection API +----------------------------- + +.. currentmodule:: sqlalchemy.orm + +.. autofunction:: attribute_keyed_dict + +.. autofunction:: column_keyed_dict + +.. autofunction:: keyfunc_mapping + +.. autodata:: attribute_mapped_collection + +.. autodata:: column_mapped_collection + +.. autodata:: mapped_collection + +.. autoclass:: sqlalchemy.orm.KeyFuncDict + :members: + +.. autodata:: sqlalchemy.orm.MappedCollection + + Collection Internals --------------------- +----------------------------- -Various internal methods. +.. currentmodule:: sqlalchemy.orm.collections .. autofunction:: bulk_replace +.. autoclass:: collection + :members: + .. autodata:: collection_adapter .. autoclass:: CollectionAdapter diff --git a/doc/build/orm/extensions/associationproxy.rst b/doc/build/orm/extensions/associationproxy.rst index 184074e9e6..de85bea643 100644 --- a/doc/build/orm/extensions/associationproxy.rst +++ b/doc/build/orm/extensions/associationproxy.rst @@ -281,7 +281,7 @@ Proxying to Dictionary Based Collections ---------------------------------------- The association proxy can proxy to dictionary based collections as well. SQLAlchemy -mappings usually use the :func:`.attribute_mapped_collection` collection type to +mappings usually use the :func:`.attribute_keyed_dict` collection type to create dictionary collections, as well as the extended techniques described in :ref:`dictionary_collections`. @@ -301,7 +301,7 @@ when new elements are added to the dictionary:: from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm import DeclarativeBase, relationship - from sqlalchemy.orm.collections import attribute_mapped_collection + from sqlalchemy.orm.collections import attribute_keyed_dict class Base(DeclarativeBase): @@ -318,7 +318,7 @@ when new elements are added to the dictionary:: user_keyword_associations = relationship( "UserKeywordAssociation", back_populates="user", - collection_class=attribute_mapped_collection("special_key"), + collection_class=attribute_keyed_dict("special_key"), cascade="all, delete-orphan", ) # proxy to 'user_keyword_associations', instantiating @@ -386,7 +386,7 @@ present on ``UserKeywordAssociation``:: from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm import DeclarativeBase, relationship - from sqlalchemy.orm.collections import attribute_mapped_collection + from sqlalchemy.orm.collections import attribute_keyed_dict class Base(DeclarativeBase): @@ -401,7 +401,7 @@ present on ``UserKeywordAssociation``:: user_keyword_associations = relationship( "UserKeywordAssociation", back_populates="user", - collection_class=attribute_mapped_collection("special_key"), + collection_class=attribute_keyed_dict("special_key"), cascade="all, delete-orphan", ) # the same 'user_keyword_associations'->'keyword' proxy as in @@ -496,7 +496,7 @@ to a related object, as in the example mapping below:: from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.orm import DeclarativeBase, relationship - from sqlalchemy.orm.collections import attribute_mapped_collection + from sqlalchemy.orm.collections import attribute_keyed_dict class Base(DeclarativeBase): diff --git a/examples/adjacency_list/adjacency_list.py b/examples/adjacency_list/adjacency_list.py index fee0f413f6..38503f9f33 100644 --- a/examples/adjacency_list/adjacency_list.py +++ b/examples/adjacency_list/adjacency_list.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import backref from sqlalchemy.orm import joinedload from sqlalchemy.orm import relationship from sqlalchemy.orm import Session -from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.orm.collections import attribute_keyed_dict Base = declarative_base() @@ -30,7 +30,7 @@ class TreeNode(Base): backref=backref("parent", remote_side=id), # children will be represented as a dictionary # on the "name" attribute. - collection_class=attribute_mapped_collection("name"), + collection_class=attribute_keyed_dict("name"), ) def __init__(self, name, parent=None): diff --git a/examples/association/dict_of_sets_with_default.py b/examples/association/dict_of_sets_with_default.py index 14045b7f56..96e30c1e28 100644 --- a/examples/association/dict_of_sets_with_default.py +++ b/examples/association/dict_of_sets_with_default.py @@ -23,7 +23,7 @@ from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from sqlalchemy.orm import Session -from sqlalchemy.orm.collections import MappedCollection +from sqlalchemy.orm.collections import KeyFuncDict class Base: @@ -33,7 +33,7 @@ class Base: Base = declarative_base(cls=Base) -class GenDefaultCollection(MappedCollection): +class GenDefaultCollection(KeyFuncDict): def __missing__(self, key): self[key] = b = B(key) return b diff --git a/examples/versioned_rows/versioned_map.py b/examples/versioned_rows/versioned_map.py index c2fa6c2a91..fd457946f8 100644 --- a/examples/versioned_rows/versioned_map.py +++ b/examples/versioned_rows/versioned_map.py @@ -43,7 +43,7 @@ from sqlalchemy.orm import relationship from sqlalchemy.orm import Session from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import validates -from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.orm.collections import attribute_keyed_dict @event.listens_for(Session, "before_flush") @@ -83,7 +83,7 @@ class ConfigData(Base): elements = relationship( "ConfigValueAssociation", - collection_class=attribute_mapped_collection("name"), + collection_class=attribute_keyed_dict("name"), backref=backref("config_data"), lazy="subquery", ) diff --git a/examples/vertical/dictlike-polymorphic.py b/examples/vertical/dictlike-polymorphic.py index 95b582a761..0343d53e12 100644 --- a/examples/vertical/dictlike-polymorphic.py +++ b/examples/vertical/dictlike-polymorphic.py @@ -132,7 +132,7 @@ if __name__ == "__main__": create_engine, ) from sqlalchemy.orm import relationship, Session - from sqlalchemy.orm.collections import attribute_mapped_collection + from sqlalchemy.orm.collections import attribute_keyed_dict from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.associationproxy import association_proxy @@ -162,7 +162,7 @@ if __name__ == "__main__": name = Column(Unicode(100)) facts = relationship( - "AnimalFact", collection_class=attribute_mapped_collection("key") + "AnimalFact", collection_class=attribute_keyed_dict("key") ) _proxied = association_proxy( diff --git a/examples/vertical/dictlike.py b/examples/vertical/dictlike.py index b74b317762..d0a952d7c7 100644 --- a/examples/vertical/dictlike.py +++ b/examples/vertical/dictlike.py @@ -71,7 +71,7 @@ if __name__ == "__main__": create_engine, ) from sqlalchemy.orm import relationship, Session - from sqlalchemy.orm.collections import attribute_mapped_collection + from sqlalchemy.orm.collections import attribute_keyed_dict from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.associationproxy import association_proxy @@ -95,7 +95,7 @@ if __name__ == "__main__": name = Column(Unicode(100)) facts = relationship( - "AnimalFact", collection_class=attribute_mapped_collection("key") + "AnimalFact", collection_class=attribute_keyed_dict("key") ) _proxied = association_proxy( diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 8523e520b9..c6b61f3b47 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -93,12 +93,16 @@ from .interfaces import RelationshipDirection as RelationshipDirection from .interfaces import UserDefinedOption as UserDefinedOption from .loading import merge_frozen_result as merge_frozen_result from .loading import merge_result as merge_result +from .mapped_collection import attribute_keyed_dict as attribute_keyed_dict from .mapped_collection import ( attribute_mapped_collection as attribute_mapped_collection, ) +from .mapped_collection import column_keyed_dict as column_keyed_dict from .mapped_collection import ( column_mapped_collection as column_mapped_collection, ) +from .mapped_collection import keyfunc_mapping as keyfunc_mapping +from .mapped_collection import KeyFuncDict as KeyFuncDict from .mapped_collection import mapped_collection as mapped_collection from .mapped_collection import MappedCollection as MappedCollection from .mapper import configure_mappers as configure_mappers diff --git a/lib/sqlalchemy/orm/collections.py b/lib/sqlalchemy/orm/collections.py index 5dbd2dc305..e3051e268f 100644 --- a/lib/sqlalchemy/orm/collections.py +++ b/lib/sqlalchemy/orm/collections.py @@ -134,19 +134,23 @@ from ..util.typing import Protocol if typing.TYPE_CHECKING: from .attributes import AttributeEventToken from .attributes import CollectionAttributeImpl - from .mapped_collection import attribute_mapped_collection - from .mapped_collection import column_mapped_collection - from .mapped_collection import mapped_collection - from .mapped_collection import MappedCollection # noqa: F401 + from .mapped_collection import attribute_keyed_dict + from .mapped_collection import column_keyed_dict + from .mapped_collection import keyfunc_mapping + from .mapped_collection import KeyFuncDict # noqa: F401 from .state import InstanceState __all__ = [ "collection", "collection_adapter", - "mapped_collection", - "column_mapped_collection", - "attribute_mapped_collection", + "keyfunc_mapping", + "column_keyed_dict", + "attribute_keyed_dict", + "column_keyed_dict", + "attribute_keyed_dict", + "MappedCollection", + "KeyFuncDict", ] __instrumentation_mutex = threading.Lock() @@ -1550,8 +1554,15 @@ __interfaces: util.immutabledict[ def __go(lcls): - global mapped_collection, column_mapped_collection - global attribute_mapped_collection, MappedCollection + global keyfunc_mapping, mapped_collection + global column_keyed_dict, column_mapped_collection + global MappedCollection, KeyFuncDict + global attribute_keyed_dict, attribute_mapped_collection + + from .mapped_collection import keyfunc_mapping + from .mapped_collection import column_keyed_dict + from .mapped_collection import attribute_keyed_dict + from .mapped_collection import KeyFuncDict from .mapped_collection import mapped_collection from .mapped_collection import column_mapped_collection @@ -1565,7 +1576,7 @@ def __go(lcls): # see [ticket:2406]. _instrument_class(InstrumentedList) _instrument_class(InstrumentedSet) - _instrument_class(MappedCollection) + _instrument_class(KeyFuncDict) __go(locals()) diff --git a/lib/sqlalchemy/orm/mapped_collection.py b/lib/sqlalchemy/orm/mapped_collection.py index 1f95d9d77a..1aa864f7e5 100644 --- a/lib/sqlalchemy/orm/mapped_collection.py +++ b/lib/sqlalchemy/orm/mapped_collection.py @@ -105,12 +105,15 @@ class _SerializableColumnGetterV2(_PlainColumnGetter): return cols -def column_mapped_collection( +def column_keyed_dict( mapping_spec, *, ignore_unpopulated_attribute: bool = False ): """A dictionary-based collection type with column-based keying. - Returns a :class:`.MappedCollection` factory which will produce new + .. versionchanged:: 2.0 Renamed :data:`.column_mapped_collection` to + :class:`.column_keyed_dict`. + + Returns a :class:`.KeyFuncDict` factory which will produce new dictionary keys based on the value of a particular :class:`.Column`-mapped attribute on ORM mapped instances to be added to the dictionary. @@ -137,7 +140,7 @@ def column_mapped_collection( .. versionadded:: 2.0 an error is raised by default if the attribute being used for the dictionary key is determined that it was never populated with any value. The - :paramref:`.column_mapped_collection.ignore_unpopulated_attribute` + :paramref:`.column_keyed_dict.ignore_unpopulated_attribute` parameter may be set which will instead indicate that this condition should be ignored, and the append operation silently skipped. This is in contrast to the behavior of the 1.x series which would @@ -170,12 +173,15 @@ class _AttrGetter: return _AttrGetter, (self.attr_name,) -def attribute_mapped_collection( +def attribute_keyed_dict( attr_name: str, *, ignore_unpopulated_attribute: bool = False -) -> Type["MappedCollection"]: +) -> Type["KeyFuncDict"]: """A dictionary-based collection type with attribute-based keying. - Returns a :class:`.MappedCollection` factory which will produce new + .. versionchanged:: 2.0 Renamed :data:`.attribute_mapped_collection` to + :func:`.attribute_keyed_dict`. + + Returns a :class:`.KeyFuncDict` factory which will produce new dictionary keys based on the value of a particular named attribute on ORM mapped instances to be added to the dictionary. @@ -200,7 +206,7 @@ def attribute_mapped_collection( .. versionadded:: 2.0 an error is raised by default if the attribute being used for the dictionary key is determined that it was never populated with any value. The - :paramref:`.attribute_mapped_collection.ignore_unpopulated_attribute` + :paramref:`.attribute_keyed_dict.ignore_unpopulated_attribute` parameter may be set which will instead indicate that this condition should be ignored, and the append operation silently skipped. This is in contrast to the behavior of the 1.x series which would @@ -216,14 +222,17 @@ def attribute_mapped_collection( ) -def mapped_collection( +def keyfunc_mapping( keyfunc: Callable[[Any], _KT], *, ignore_unpopulated_attribute: bool = False, -) -> Type["MappedCollection[_KT, Any]"]: +) -> Type["KeyFuncDict[_KT, Any]"]: """A dictionary-based collection type with arbitrary keying. - Returns a :class:`.MappedCollection` factory with a keying function + .. versionchanged:: 2.0 Renamed :data:`.mapped_collection` to + :func:`.keyfunc_mapping`. + + Returns a :class:`.KeyFuncDict` factory with a keying function generated from keyfunc, a callable that takes an entity and returns a key value. @@ -262,21 +271,24 @@ def mapped_collection( ) -class MappedCollection(Dict[_KT, _VT]): +class KeyFuncDict(Dict[_KT, _VT]): """Base for ORM mapped dictionary classes. Extends the ``dict`` type with additional methods needed by SQLAlchemy ORM - collection classes. Use of :class:`_orm.MappedCollection` is most directly - by using the :func:`.attribute_mapped_collection` or - :func:`.column_mapped_collection` class factories. - :class:`_orm.MappedCollection` may also serve as the base for user-defined + collection classes. Use of :class:`_orm.KeyFuncDict` is most directly + by using the :func:`.attribute_keyed_dict` or + :func:`.column_keyed_dict` class factories. + :class:`_orm.KeyFuncDict` may also serve as the base for user-defined custom dictionary classes. + .. versionchanged:: 2.0 Renamed :class:`.MappedCollection` to + :class:`.KeyFuncDict`. + .. seealso:: - :func:`_orm.attribute_mapped_collection` + :func:`_orm.attribute_keyed_dict` - :func:`_orm.column_mapped_collection` + :func:`_orm.column_keyed_dict` :ref:`orm_dictionary_collection` @@ -304,12 +316,12 @@ class MappedCollection(Dict[_KT, _VT]): @classmethod def _unreduce(cls, keyfunc, values): - mp = MappedCollection(keyfunc) + mp = KeyFuncDict(keyfunc) mp.update(values) return mp def __reduce__(self): - return (MappedCollection._unreduce, (self.keyfunc, dict(self))) + return (KeyFuncDict._unreduce, (self.keyfunc, dict(self))) def _raise_for_unpopulated(self, value, initiator): mapper = base.instance_state(value).mapper @@ -322,7 +334,7 @@ class MappedCollection(Dict[_KT, _VT]): raise sa_exc.InvalidRequestError( f"In event triggered from population of attribute {relationship} " "(likely from a backref), " - f"can't populate value in MappedCollection; " + f"can't populate value in KeyFuncDict; " "dictionary key " f"derived from {base.instance_str(value)} is not " f"populated. Ensure appropriate state is set up on " @@ -365,7 +377,7 @@ class MappedCollection(Dict[_KT, _VT]): if self[key] != value: raise sa_exc.InvalidRequestError( "Can not remove '%s': collection holds '%s' for key '%s'. " - "Possible cause: is the MappedCollection key function " + "Possible cause: is the KeyFuncDict key function " "based on mutable properties or properties that only obtain " "values after flush?" % (value, self[key], key) ) @@ -373,7 +385,7 @@ class MappedCollection(Dict[_KT, _VT]): def _mapped_collection_cls(keyfunc, ignore_unpopulated_attribute): - class _MKeyfuncMapped(MappedCollection): + class _MKeyfuncMapped(KeyFuncDict): def __init__(self): super().__init__( keyfunc, @@ -381,3 +393,36 @@ def _mapped_collection_cls(keyfunc, ignore_unpopulated_attribute): ) return _MKeyfuncMapped + + +MappedCollection = KeyFuncDict +"""A synonym for :class:`.KeyFuncDict`. + +.. versionchanged:: 2.0 Renamed :class:`.MappedCollection` to + :class:`.KeyFuncDict`. + +""" + +mapped_collection = keyfunc_mapping +"""A synonym for :func:`_orm.keyfunc_mapping`. + +.. versionchanged:: 2.0 Renamed :data:`.mapped_collection` to + :func:`_orm.keyfunc_mapping` + +""" + +attribute_mapped_collection = attribute_keyed_dict +"""A synonym for :func:`_orm.attribute_keyed_dict`. + +.. versionchanged:: 2.0 Renamed :data:`.attribute_mapped_collection` to + :func:`_orm.attribute_keyed_dict` + +""" + +column_mapped_collection = column_keyed_dict +"""A synonym for :func:`_orm.column_keyed_dict. + +.. versionchanged:: 2.0 Renamed :func:`.column_mapped_collection` to + :func:`_orm.column_keyed_dict` + +""" diff --git a/test/ext/mypy/plain_files/keyfunc_dict.py b/test/ext/mypy/plain_files/keyfunc_dict.py new file mode 100644 index 0000000000..0e18697bdb --- /dev/null +++ b/test/ext/mypy/plain_files/keyfunc_dict.py @@ -0,0 +1,45 @@ +import typing +from typing import Optional + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import attribute_keyed_dict +from sqlalchemy.orm import DeclarativeBase +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import relationship + + +class Base(DeclarativeBase): + pass + + +class Item(Base): + __tablename__ = "item" + + id: Mapped[int] = mapped_column(primary_key=True) + + notes: Mapped[dict[str, "Note"]] = relationship( + collection_class=attribute_keyed_dict("keyword"), + cascade="all, delete-orphan", + ) + + +class Note(Base): + __tablename__ = "note" + + id: Mapped[int] = mapped_column(primary_key=True) + item_id: Mapped[int] = mapped_column(ForeignKey("item.id")) + keyword: Mapped[str] + text: Mapped[Optional[str]] + + def __init__(self, keyword: str, text: str): + self.keyword = keyword + self.text = text + + +item = Item() +item.notes["a"] = Note("a", "atext") + +if typing.TYPE_CHECKING: + # EXPECTED_TYPE: dict_items[str, Note] + reveal_type(item.notes.items()) diff --git a/test/ext/mypy/plain_files/trad_relationship_uselist.py b/test/ext/mypy/plain_files/trad_relationship_uselist.py index 4d17dab78d..8d7d7e71a2 100644 --- a/test/ext/mypy/plain_files/trad_relationship_uselist.py +++ b/test/ext/mypy/plain_files/trad_relationship_uselist.py @@ -16,7 +16,7 @@ from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship -from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.orm.collections import attribute_keyed_dict class Base(DeclarativeBase): @@ -80,11 +80,11 @@ class Address(Base): user_style_nine = relationship(User, uselist=True) user_style_ten = relationship( - User, collection_class=attribute_mapped_collection("name") + User, collection_class=attribute_keyed_dict("name") ) user_style_ten_typed: Mapped[Dict[str, User]] = relationship( - User, collection_class=attribute_mapped_collection("name") + User, collection_class=attribute_keyed_dict("name") ) # pylance rejects this however. cannot get both to work at the same diff --git a/test/ext/test_associationproxy.py b/test/ext/test_associationproxy.py index 67b0c93e0f..ffaae7db37 100644 --- a/test/ext/test_associationproxy.py +++ b/test/ext/test_associationproxy.py @@ -27,7 +27,7 @@ from sqlalchemy.orm import declared_attr from sqlalchemy.orm import mapper from sqlalchemy.orm import relationship from sqlalchemy.orm import Session -from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.orm.collections import attribute_keyed_dict from sqlalchemy.orm.collections import collection from sqlalchemy.testing import assert_raises from sqlalchemy.testing import assert_raises_message @@ -195,7 +195,7 @@ class AutoFlushTest(fixtures.MappedTest): collection[obj.name] = obj self._test_premature_flush( - collections.attribute_mapped_collection("name"), set_, is_dict=True + collections.attribute_keyed_dict("name"), set_, is_dict=True ) @@ -1465,7 +1465,7 @@ class ReconstitutionTest(fixtures.MappedTest): properties=dict( children=relationship( KVChild, - collection_class=collections.mapped_collection( + collection_class=collections.keyfunc_mapping( PickleKeyFunc("name") ), ) @@ -2356,7 +2356,7 @@ class DictOfTupleUpdateTest(fixtures.MappedTest): a, properties={ "orig": relationship( - B, collection_class=attribute_mapped_collection("key") + B, collection_class=attribute_keyed_dict("key") ) }, ) diff --git a/test/orm/declarative/test_tm_future_annotations.py b/test/orm/declarative/test_tm_future_annotations.py index a63378c26d..b1e80f5d93 100644 --- a/test/orm/declarative/test_tm_future_annotations.py +++ b/test/orm/declarative/test_tm_future_annotations.py @@ -12,12 +12,12 @@ from sqlalchemy import ForeignKey from sqlalchemy import Integer from sqlalchemy import Numeric from sqlalchemy import Table -from sqlalchemy.orm import attribute_mapped_collection +from sqlalchemy.orm import attribute_keyed_dict from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import DynamicMapped +from sqlalchemy.orm import KeyFuncDict from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column -from sqlalchemy.orm import MappedCollection from sqlalchemy.orm import relationship from sqlalchemy.orm import WriteOnlyMapped from sqlalchemy.testing import expect_raises_message @@ -113,7 +113,7 @@ class MappedColumnTest(_MappedColumnTest): is_true(optional_col.nullable) -class MappedOneArg(MappedCollection[str, _R]): +class MappedOneArg(KeyFuncDict[str, _R]): pass @@ -210,8 +210,8 @@ class RelationshipLHSTest(_RelationshipLHSTest): id: Mapped[int] = mapped_column(primary_key=True) data: Mapped[str] = mapped_column() - bs: Mapped[MappedCollection[str, B]] = relationship( # noqa: F821 - collection_class=attribute_mapped_collection("name") + bs: Mapped[KeyFuncDict[str, B]] = relationship( # noqa: F821 + collection_class=attribute_keyed_dict("name") ) class B(decl_base): @@ -231,10 +231,8 @@ class RelationshipLHSTest(_RelationshipLHSTest): id: Mapped[int] = mapped_column(primary_key=True) data: Mapped[str] = mapped_column() - bs: Mapped[ - MappedCollection[str, "B"] - ] = relationship( # noqa: F821 - collection_class=attribute_mapped_collection("name") + bs: Mapped[KeyFuncDict[str, "B"]] = relationship( # noqa: F821 + collection_class=attribute_keyed_dict("name") ) class B(decl_base): @@ -246,7 +244,7 @@ class RelationshipLHSTest(_RelationshipLHSTest): self._assert_dict(A, B) def test_collection_cls_not_locatable(self, decl_base): - class MyCollection(MappedCollection): + class MyCollection(KeyFuncDict): pass with expect_raises_message( @@ -261,7 +259,7 @@ class RelationshipLHSTest(_RelationshipLHSTest): data: Mapped[str] = mapped_column() bs: Mapped[MyCollection["B"]] = relationship( # noqa: F821 - collection_class=attribute_mapped_collection("name") + collection_class=attribute_keyed_dict("name") ) def test_collection_cls_one_arg(self, decl_base): @@ -272,7 +270,7 @@ class RelationshipLHSTest(_RelationshipLHSTest): data: Mapped[str] = mapped_column() bs: Mapped[MappedOneArg["B"]] = relationship( # noqa: F821 - collection_class=attribute_mapped_collection("name") + collection_class=attribute_keyed_dict("name") ) class B(decl_base): diff --git a/test/orm/declarative/test_typed_mapping.py b/test/orm/declarative/test_typed_mapping.py index 5c5b481db1..7ef35a8504 100644 --- a/test/orm/declarative/test_typed_mapping.py +++ b/test/orm/declarative/test_typed_mapping.py @@ -42,8 +42,8 @@ from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship from sqlalchemy.orm import undefer from sqlalchemy.orm import WriteOnlyMapped -from sqlalchemy.orm.collections import attribute_mapped_collection -from sqlalchemy.orm.collections import MappedCollection +from sqlalchemy.orm.collections import attribute_keyed_dict +from sqlalchemy.orm.collections import KeyFuncDict from sqlalchemy.schema import CreateTable from sqlalchemy.testing import eq_ from sqlalchemy.testing import expect_raises @@ -1412,10 +1412,8 @@ class RelationshipLHSTest(fixtures.TestBase, testing.AssertsCompiledSQL): id: Mapped[int] = mapped_column(primary_key=True) data: Mapped[str] = mapped_column() - bs: Mapped[ - MappedCollection[str, "B"] # noqa: F821 - ] = relationship( - collection_class=attribute_mapped_collection("name") + bs: Mapped[KeyFuncDict[str, "B"]] = relationship( # noqa: F821 + collection_class=attribute_keyed_dict("name") ) class B(decl_base): diff --git a/test/orm/test_attributes.py b/test/orm/test_attributes.py index 53b306f5b1..f0a91cf392 100644 --- a/test/orm/test_attributes.py +++ b/test/orm/test_attributes.py @@ -9,7 +9,7 @@ from sqlalchemy.orm import attributes from sqlalchemy.orm import exc as orm_exc from sqlalchemy.orm import instrumentation from sqlalchemy.orm import NO_KEY -from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.orm.collections import attribute_keyed_dict from sqlalchemy.orm.collections import collection from sqlalchemy.orm.state import InstanceState from sqlalchemy.testing import assert_raises @@ -2584,7 +2584,7 @@ class HistoryTest(fixtures.TestBase): "someattr", uselist=True, useobject=True, - typecallable=attribute_mapped_collection("name"), + typecallable=attribute_keyed_dict("name"), ) hi = Bar(name="hi") there = Bar(name="there") @@ -3209,7 +3209,7 @@ class CollectionKeyTest(fixtures.ORMTest): "someattr", uselist=True, useobject=True, - typecallable=attribute_mapped_collection("name"), + typecallable=attribute_keyed_dict("name"), ) _register_attribute( Bar, diff --git a/test/orm/test_cascade.py b/test/orm/test_cascade.py index 5a171e3722..8baa52f19f 100644 --- a/test/orm/test_cascade.py +++ b/test/orm/test_cascade.py @@ -20,7 +20,7 @@ from sqlalchemy.orm import Session from sqlalchemy.orm import util as orm_util from sqlalchemy.orm import with_parent from sqlalchemy.orm.attributes import instance_state -from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.orm.collections import attribute_keyed_dict from sqlalchemy.orm.decl_api import declarative_base from sqlalchemy.testing import assert_raises from sqlalchemy.testing import assert_raises_message @@ -4462,10 +4462,10 @@ class CollectionCascadesNoBackrefTest(fixtures.TestBase): @testing.combinations( (set, "add"), (list, "append"), - (attribute_mapped_collection("key"), "__setitem__"), - (attribute_mapped_collection("key"), "setdefault"), - (attribute_mapped_collection("key"), "update_dict"), - (attribute_mapped_collection("key"), "update_kw"), + (attribute_keyed_dict("key"), "__setitem__"), + (attribute_keyed_dict("key"), "setdefault"), + (attribute_keyed_dict("key"), "update_dict"), + (attribute_keyed_dict("key"), "update_kw"), argnames="collection_class,methname", ) def test_cascades_on_collection( diff --git a/test/orm/test_collection.py b/test/orm/test_collection.py index 34b21921e4..1c8bee00f4 100644 --- a/test/orm/test_collection.py +++ b/test/orm/test_collection.py @@ -89,7 +89,7 @@ class Canary: class OrderedDictFixture: @testing.fixture def ordered_dict_mro(self): - return type("ordered", (collections.MappedCollection,), {}) + return type("ordered", (collections.KeyFuncDict,), {}) class CollectionsTest(OrderedDictFixture, fixtures.ORMTest): @@ -1403,7 +1403,7 @@ class CollectionsTest(OrderedDictFixture, fixtures.ORMTest): self.assert_(getattr(MyDict, "_sa_instrumented") == id(MyDict)) def test_dict_subclass2(self): - class MyEasyDict(collections.MappedCollection): + class MyEasyDict(collections.KeyFuncDict): def __init__(self): super(MyEasyDict, self).__init__(lambda e: e.a) @@ -1418,7 +1418,7 @@ class CollectionsTest(OrderedDictFixture, fixtures.ORMTest): def test_dict_subclass3(self, ordered_dict_mro): class MyOrdered(ordered_dict_mro): def __init__(self): - collections.MappedCollection.__init__(self, lambda e: e.a) + collections.KeyFuncDict.__init__(self, lambda e: e.a) util.OrderedDict.__init__(self) self._test_adapter( @@ -1988,15 +1988,15 @@ class DictHelpersTest(OrderedDictFixture, fixtures.MappedTest): ) def test_mapped_collection(self): - collection_class = collections.mapped_collection(lambda c: c.a) + collection_class = collections.keyfunc_mapping(lambda c: c.a) self._test_scalar_mapped(collection_class) def test_mapped_collection2(self): - collection_class = collections.mapped_collection(lambda c: (c.a, c.b)) + collection_class = collections.keyfunc_mapping(lambda c: (c.a, c.b)) self._test_composite_mapped(collection_class) def test_attr_mapped_collection(self): - collection_class = collections.attribute_mapped_collection("a") + collection_class = collections.attribute_keyed_dict("a") self._test_scalar_mapped(collection_class) def test_declarative_column_mapped(self): @@ -2015,7 +2015,7 @@ class DictHelpersTest(OrderedDictFixture, fixtures.MappedTest): ((Foo.id, Foo.bar_id), Foo(id=3, bar_id=12), (3, 12)), ): eq_( - collections.column_mapped_collection(spec)().keyfunc(obj), + collections.column_keyed_dict(spec)().keyfunc(obj), expected, ) @@ -2024,27 +2024,27 @@ class DictHelpersTest(OrderedDictFixture, fixtures.MappedTest): sa_exc.ArgumentError, "Column expression expected " "for argument 'mapping_spec'; got 'a'.", - collections.column_mapped_collection, + collections.column_keyed_dict, "a", ) assert_raises_message( sa_exc.ArgumentError, "Column expression expected " "for argument 'mapping_spec'; got .*TextClause.", - collections.column_mapped_collection, + collections.column_keyed_dict, text("a"), ) def test_column_mapped_collection(self): children = self.tables.children - collection_class = collections.column_mapped_collection(children.c.a) + collection_class = collections.column_keyed_dict(children.c.a) self._test_scalar_mapped(collection_class) def test_column_mapped_collection2(self): children = self.tables.children - collection_class = collections.column_mapped_collection( + collection_class = collections.column_keyed_dict( (children.c.a, children.c.b) ) self._test_composite_mapped(collection_class) @@ -2052,7 +2052,7 @@ class DictHelpersTest(OrderedDictFixture, fixtures.MappedTest): def test_mixin(self, ordered_dict_mro): class Ordered(ordered_dict_mro): def __init__(self): - collections.MappedCollection.__init__(self, lambda v: v.a) + collections.KeyFuncDict.__init__(self, lambda v: v.a) util.OrderedDict.__init__(self) collection_class = Ordered @@ -2061,7 +2061,7 @@ class DictHelpersTest(OrderedDictFixture, fixtures.MappedTest): def test_mixin2(self, ordered_dict_mro): class Ordered2(ordered_dict_mro): def __init__(self, keyfunc): - collections.MappedCollection.__init__(self, keyfunc) + collections.KeyFuncDict.__init__(self, keyfunc) util.OrderedDict.__init__(self) def collection_class(): @@ -2135,7 +2135,7 @@ class ColumnMappedWSerialize(fixtures.MappedTest): from sqlalchemy.testing.util import picklers for spec, obj, expected in specs: - coll = collections.column_mapped_collection(spec)() + coll = collections.column_keyed_dict(spec)() eq_(coll.keyfunc(obj), expected) # ensure we do the right thing with __reduce__ for loads, dumps in picklers(): @@ -2294,7 +2294,7 @@ class CustomCollectionsTest(fixtures.MappedTest): properties={ "bars": relationship( Bar, - collection_class=collections.column_mapped_collection( + collection_class=collections.column_keyed_dict( someothertable.c.data ), ) @@ -2641,11 +2641,11 @@ class UnpopulatedAttrTest(fixtures.TestBase): data = Column(String) a_id = Column(ForeignKey("a.id")) - if collection_fn is collections.attribute_mapped_collection: + if collection_fn is collections.attribute_keyed_dict: cc = collection_fn( "data", ignore_unpopulated_attribute=ignore_unpopulated ) - elif collection_fn is collections.column_mapped_collection: + elif collection_fn is collections.column_keyed_dict: cc = collection_fn( B.data, ignore_unpopulated_attribute=ignore_unpopulated ) @@ -2665,8 +2665,8 @@ class UnpopulatedAttrTest(fixtures.TestBase): return A, B @testing.combinations( - collections.attribute_mapped_collection, - collections.column_mapped_collection, + collections.attribute_keyed_dict, + collections.column_keyed_dict, argnames="collection_fn", ) @testing.combinations(True, False, argnames="ignore_unpopulated") @@ -2689,8 +2689,8 @@ class UnpopulatedAttrTest(fixtures.TestBase): a1.bs["bar"] = B(a=a1) @testing.combinations( - collections.attribute_mapped_collection, - collections.column_mapped_collection, + collections.attribute_keyed_dict, + collections.column_keyed_dict, argnames="collection_fn", ) @testing.combinations(True, False, argnames="ignore_unpopulated") diff --git a/test/orm/test_deprecations.py b/test/orm/test_deprecations.py index 71c03aee78..c816896cf3 100644 --- a/test/orm/test_deprecations.py +++ b/test/orm/test_deprecations.py @@ -873,7 +873,7 @@ class InstrumentationTest(fixtures.ORMTest): "AttributeEvents" ): - class MyDict(collections.MappedCollection): + class MyDict(collections.KeyFuncDict): def __init__(self): super(MyDict, self).__init__(lambda value: "k%d" % value) diff --git a/test/orm/test_merge.py b/test/orm/test_merge.py index a83ca41947..ef0db6d055 100644 --- a/test/orm/test_merge.py +++ b/test/orm/test_merge.py @@ -20,7 +20,7 @@ from sqlalchemy.orm import relationship from sqlalchemy.orm import selectinload from sqlalchemy.orm import Session from sqlalchemy.orm import synonym -from sqlalchemy.orm.collections import attribute_mapped_collection +from sqlalchemy.orm.collections import attribute_keyed_dict from sqlalchemy.orm.interfaces import MapperOption from sqlalchemy.testing import assert_raises_message from sqlalchemy.testing import eq_ @@ -600,9 +600,7 @@ class MergeTest(_fixtures.FixtureTest): "addresses": relationship( self.mapper_registry.map_imperatively(Address, addresses), backref="user", - collection_class=attribute_mapped_collection( - "email_address" - ), + collection_class=attribute_keyed_dict("email_address"), ) }, ) @@ -1870,7 +1868,7 @@ class DeferredMergeTest(fixtures.MappedTest): class Book(cls.Basic): pass - def test_deferred_column_mapping(self): + def test_deferred_column_keyed_dict(self): # defer 'excerpt' at mapping level instead of query level Book, book = self.classes.Book, self.tables.book self.mapper_registry.map_imperatively( diff --git a/test/orm/test_pickled.py b/test/orm/test_pickled.py index a0706a1337..710c98ee6d 100644 --- a/test/orm/test_pickled.py +++ b/test/orm/test_pickled.py @@ -17,8 +17,8 @@ from sqlalchemy.orm import state as sa_state from sqlalchemy.orm import subqueryload from sqlalchemy.orm import with_loader_criteria from sqlalchemy.orm import with_polymorphic -from sqlalchemy.orm.collections import attribute_mapped_collection -from sqlalchemy.orm.collections import column_mapped_collection +from sqlalchemy.orm.collections import attribute_keyed_dict +from sqlalchemy.orm.collections import column_keyed_dict from sqlalchemy.testing import assert_raises_message from sqlalchemy.testing import eq_ from sqlalchemy.testing import fixtures @@ -580,9 +580,7 @@ class PickleTest(fixtures.MappedTest): properties={ "addresses": relationship( Address, - collection_class=attribute_mapped_collection( - "email_address" - ), + collection_class=attribute_keyed_dict("email_address"), ) }, ) @@ -603,7 +601,7 @@ class PickleTest(fixtures.MappedTest): properties={ "addresses": relationship( Address, - collection_class=column_mapped_collection( + collection_class=column_keyed_dict( addresses.c.email_address ), ) @@ -629,7 +627,7 @@ class PickleTest(fixtures.MappedTest): properties={ "addresses": relationship( Address, - collection_class=column_mapped_collection( + collection_class=column_keyed_dict( [addresses.c.id, addresses.c.email_address] ), ) diff --git a/test/orm/test_validators.py b/test/orm/test_validators.py index 6b0fee49db..adfb6cb74d 100644 --- a/test/orm/test_validators.py +++ b/test/orm/test_validators.py @@ -225,7 +225,7 @@ class ValidatorTest(_fixtures.FixtureTest): properties={ "addresses": relationship( Address, - collection_class=collections.attribute_mapped_collection( + collection_class=collections.attribute_keyed_dict( "email_address" ), )