diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-07-15 12:53:37 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2022-07-16 16:28:11 -0400 |
| commit | 9a37ebdf99d0e3fcca53ca8f00f101fe475a1b01 (patch) | |
| tree | 112107d938b959316253b3988132a2109947889f /lib/sqlalchemy | |
| parent | 6104c163eb58e35e46b0bb6a237e824ec1ee1d15 (diff) | |
| download | sqlalchemy-9a37ebdf99d0e3fcca53ca8f00f101fe475a1b01.tar.gz | |
update ORM declarative docs for new features
I screwed up a rebase or something so this was
temporarily in Ic51a12de3358f3a451bd7cf3542b375569499fc1
Change-Id: I847ee1336381221c0112b67854df022edf596b25
Diffstat (limited to 'lib/sqlalchemy')
| -rw-r--r-- | lib/sqlalchemy/ext/mutable.py | 80 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/_orm_constructors.py | 338 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/base.py | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/decl_api.py | 434 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/mapped_collection.py | 19 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/properties.py | 16 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/requirements.py | 6 |
9 files changed, 476 insertions, 453 deletions
diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py index 6c26c9266..1d6cf8a85 100644 --- a/lib/sqlalchemy/ext/mutable.py +++ b/lib/sqlalchemy/ext/mutable.py @@ -236,19 +236,19 @@ a geometric "point", and is introduced in :ref:`mapper_composite`. As is the case with :class:`.Mutable`, the user-defined composite class subclasses :class:`.MutableComposite` as a mixin, and detects and delivers change events to its parents via the :meth:`.MutableComposite.changed` method. -In the case of a composite class, the detection is usually via the usage of -Python descriptors (i.e. ``@property``), or alternatively via the special -Python method ``__setattr__()``. Below we expand upon the ``Point`` class -introduced in :ref:`mapper_composite` to subclass :class:`.MutableComposite` -and to also route attribute set events via ``__setattr__`` to the -:meth:`.MutableComposite.changed` method:: +In the case of a composite class, the detection is usually via the usage of the +special Python method ``__setattr__()``. In the example below, we expand upon the ``Point`` +class introduced in :ref:`mapper_composite` to include +:class:`.MutableComposite` in its bases and to route attribute set events via +``__setattr__`` to the :meth:`.MutableComposite.changed` method:: + import dataclasses from sqlalchemy.ext.mutable import MutableComposite + @dataclasses.dataclass class Point(MutableComposite): - def __init__(self, x, y): - self.x = x - self.y = y + x: int + y: int def __setattr__(self, key, value): "Intercept set events" @@ -259,16 +259,6 @@ and to also route attribute set events via ``__setattr__`` to the # alert all parents to the change self.changed() - def __composite_values__(self): - return self.x, self.y - - def __eq__(self, other): - return isinstance(other, Point) and \ - other.x == self.x and \ - other.y == self.y - - def __ne__(self, other): - return not self.__eq__(other) The :class:`.MutableComposite` class makes use of class mapping events to automatically establish listeners for any usage of :func:`_orm.composite` that @@ -276,38 +266,45 @@ specifies our ``Point`` type. Below, when ``Point`` is mapped to the ``Vertex`` class, listeners are established which will route change events from ``Point`` objects to each of the ``Vertex.start`` and ``Vertex.end`` attributes:: - from sqlalchemy.orm import composite, mapper - from sqlalchemy import Table, Column - - vertices = Table('vertices', metadata, - Column('id', Integer, primary_key=True), - Column('x1', Integer), - Column('y1', Integer), - Column('x2', Integer), - Column('y2', Integer), - ) + from sqlalchemy.orm import DeclarativeBase, Mapped + from sqlalchemy.orm import composite, mapped_column - class Vertex: + class Base(DeclarativeBase): pass - mapper(Vertex, vertices, properties={ - 'start': composite(Point, vertices.c.x1, vertices.c.y1), - 'end': composite(Point, vertices.c.x2, vertices.c.y2) - }) + + class Vertex(Base): + __tablename__ = "vertices" + + id: Mapped[int] = mapped_column(primary_key=True) + + start: Mapped[Point] = composite(mapped_column("x1"), mapped_column("y1")) + end: Mapped[Point] = composite(mapped_column("x2"), mapped_column("y2")) + + def __repr__(self): + return f"Vertex(start={self.start}, end={self.end})" Any in-place changes to the ``Vertex.start`` or ``Vertex.end`` members -will flag the attribute as "dirty" on the parent object:: +will flag the attribute as "dirty" on the parent object: - >>> from sqlalchemy.orm import Session +.. sourcecode:: python+sql - >>> sess = Session() + >>> from sqlalchemy.orm import Session + >>> sess = Session(engine) >>> v1 = Vertex(start=Point(3, 4), end=Point(12, 15)) >>> sess.add(v1) - >>> sess.commit() + {sql}>>> sess.flush() + BEGIN (implicit) + INSERT INTO vertices (x1, y1, x2, y2) VALUES (?, ?, ?, ?) + [...] (3, 4, 12, 15) - >>> v1.end.x = 8 + {stop}>>> v1.end.x = 8 >>> assert v1 in sess.dirty True + {sql}>>> sess.commit() + UPDATE vertices SET x2=? WHERE vertices.id = ? + [...] (8, 1) + COMMIT Coercing Mutable Composites --------------------------- @@ -319,6 +316,7 @@ Overriding the :meth:`.MutableBase.coerce` method is essentially equivalent to using a :func:`.validates` validation routine for all attributes which make use of the custom composite type:: + @dataclasses.dataclass class Point(MutableComposite): # other Point methods # ... @@ -341,6 +339,7 @@ to define a ``__getstate__`` that doesn't include the ``_parents`` dictionary. Below we define both a ``__getstate__`` and a ``__setstate__`` that package up the minimal form of our ``Point`` class:: + @dataclasses.dataclass class Point(MutableComposite): # ... @@ -354,7 +353,8 @@ As with :class:`.Mutable`, the :class:`.MutableComposite` augments the pickling process of the parent's object-relational state so that the :meth:`MutableBase._parents` collection is restored to all ``Point`` objects. -""" +""" # noqa: E501 + from collections import defaultdict import weakref diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 539cf2600..cda58d6a5 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -87,10 +87,14 @@ from .interfaces import PropComparator as PropComparator 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_mapped_collection -from .mapped_collection import column_mapped_collection -from .mapped_collection import mapped_collection -from .mapped_collection import MappedCollection +from .mapped_collection import ( + attribute_mapped_collection as attribute_mapped_collection, +) +from .mapped_collection import ( + column_mapped_collection as column_mapped_collection, +) +from .mapped_collection import mapped_collection as mapped_collection +from .mapped_collection import MappedCollection as MappedCollection from .mapper import configure_mappers as configure_mappers from .mapper import Mapper as Mapper from .mapper import reconstructor as reconstructor diff --git a/lib/sqlalchemy/orm/_orm_constructors.py b/lib/sqlalchemy/orm/_orm_constructors.py index ea95f1420..fe87d19dc 100644 --- a/lib/sqlalchemy/orm/_orm_constructors.py +++ b/lib/sqlalchemy/orm/_orm_constructors.py @@ -115,7 +115,9 @@ def mapped_column( Union[bool, Literal[SchemaConst.NULL_UNSPECIFIED]] ] = SchemaConst.NULL_UNSPECIFIED, primary_key: Optional[bool] = False, - deferred: bool = False, + deferred: Union[_NoArg, bool] = _NoArg.NO_ARG, + deferred_group: Optional[str] = None, + deferred_raiseload: bool = False, name: Optional[str] = None, type_: Optional[_TypeEngineArgument[Any]] = None, autoincrement: Union[bool, Literal["auto", "ignore_fk"]] = "auto", @@ -133,137 +135,37 @@ def mapped_column( comment: Optional[str] = None, **dialect_kwargs: Any, ) -> MappedColumn[Any]: - r"""construct a new ORM-mapped :class:`_schema.Column` construct. + r"""declare a new ORM-mapped :class:`_schema.Column` construct + for use within :ref:`Declarative Table <orm_declarative_table>` + configuration. The :func:`_orm.mapped_column` function provides an ORM-aware and Python-typing-compatible construct which is used with :ref:`declarative <orm_declarative_mapping>` mappings to indicate an attribute that's mapped to a Core :class:`_schema.Column` object. It provides the equivalent feature as mapping an attribute to a - :class:`_schema.Column` object directly when using declarative. + :class:`_schema.Column` object directly when using Declarative, + specifically when using :ref:`Declarative Table <orm_declarative_table>` + configuration. .. versionadded:: 2.0 :func:`_orm.mapped_column` is normally used with explicit typing along with - the :class:`_orm.Mapped` mapped attribute type, where it can derive the SQL - type and nullability for the column automatically, such as:: - - from typing import Optional - - from sqlalchemy.orm import Mapped - from sqlalchemy.orm import mapped_column - - class User(Base): - __tablename__ = 'user' - - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column() - options: Mapped[Optional[str]] = mapped_column() - - In the above example, the ``int`` and ``str`` types are inferred by the - Declarative mapping system to indicate use of the :class:`_types.Integer` - and :class:`_types.String` datatypes, and the presence of ``Optional`` or - not indicates whether or not each non-primary-key column is to be - ``nullable=True`` or ``nullable=False``. - - The above example, when interpreted within a Declarative class, will result - in a table named ``"user"`` which is equivalent to the following:: - - from sqlalchemy import Integer - from sqlalchemy import String - from sqlalchemy import Table - - Table( - 'user', - Base.metadata, - Column("id", Integer, primary_key=True), - Column("name", String, nullable=False), - Column("options", String, nullable=True), - ) - - The :func:`_orm.mapped_column` construct accepts the same arguments as - that of :class:`_schema.Column` directly, including optional "name" - and "type" fields, so the above mapping can be stated more explicitly - as:: + the :class:`_orm.Mapped` annotation type, where it can derive the SQL + type and nullability for the column based on what's present within the + :class:`_orm.Mapped` annotation. It also may be used without annotations + as a drop-in replacement for how :class:`_schema.Column` is used in + Declarative mappings in SQLAlchemy 1.x style. - from typing import Optional + For usage examples of :func:`_orm.mapped_column`, see the documentation + at :ref:`orm_declarative_table`. - from sqlalchemy import Integer - from sqlalchemy import String - from sqlalchemy.orm import Mapped - from sqlalchemy.orm import mapped_column - - class User(Base): - __tablename__ = 'user' + .. seealso:: - id: Mapped[int] = mapped_column("id", Integer, primary_key=True) - name: Mapped[str] = mapped_column("name", String, nullable=False) - options: Mapped[Optional[str]] = mapped_column( - "name", String, nullable=True - ) + :ref:`orm_declarative_table` - complete documentation - Arguments passed to :func:`_orm.mapped_column` always supersede those which - would be derived from the type annotation and/or attribute name. To state - the above mapping with more specific datatypes for ``id`` and ``options``, - and a different column name for ``name``, looks like:: - - from sqlalchemy import BigInteger - - class User(Base): - __tablename__ = 'user' - - id: Mapped[int] = mapped_column("id", BigInteger, primary_key=True) - name: Mapped[str] = mapped_column("user_name") - options: Mapped[Optional[str]] = mapped_column(String(50)) - - Where again, datatypes and nullable parameters that can be automatically - derived may be omitted. - - The datatypes passed to :class:`_orm.Mapped` are mapped to SQL - :class:`_types.TypeEngine` types with the following default mapping:: - - _type_map = { - int: Integer(), - float: Float(), - bool: Boolean(), - decimal.Decimal: Numeric(), - dt.date: Date(), - dt.datetime: DateTime(), - dt.time: Time(), - dt.timedelta: Interval(), - util.NoneType: NULLTYPE, - bytes: LargeBinary(), - str: String(), - } - - The above mapping may be expanded to include any combination of Python - datatypes to SQL types by using the - :paramref:`_orm.registry.type_annotation_map` parameter to - :class:`_orm.registry`, or as the attribute ``type_annotation_map`` upon - the :class:`_orm.DeclarativeBase` base class. - - Finally, :func:`_orm.mapped_column` is implicitly used by the Declarative - mapping system for any :class:`_orm.Mapped` annotation that has no - attribute value set up. This is much in the way that Python dataclasses - allow the ``field()`` construct to be optional, only needed when additional - parameters should be associated with the field. Using this functionality, - our original mapping can be stated even more succinctly as:: - - from typing import Optional - - from sqlalchemy.orm import Mapped - from sqlalchemy.orm import mapped_column - - class User(Base): - __tablename__ = 'user' - - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] - options: Mapped[Optional[str]] - - Above, the ``name`` and ``options`` columns will be evaluated as - ``Column("name", String, nullable=False)`` and - ``Column("options", String, nullable=True)``, respectively. + :ref:`whatsnew_20_orm_declarative_typing` - migration notes for + Declarative mappings using 1.x style mappings :param __name: String name to give to the :class:`_schema.Column`. This is an optional, positional only argument that if present must be the @@ -293,17 +195,54 @@ def mapped_column( ORM declarative process, and is not part of the :class:`_schema.Column` itself; instead, it indicates that this column should be "deferred" for loading as though mapped by :func:`_orm.deferred`. - :param default: This keyword argument, if present, is passed along to the - :class:`_schema.Column` constructor as the value of the - :paramref:`_schema.Column.default` parameter. However, as - :paramref:`_orm.mapped_column.default` is also consumed as a dataclasses - directive, the :paramref:`_orm.mapped_column.insert_default` parameter - should be used instead in a dataclasses context. + + .. seealso:: + + :ref:`deferred` + + :param deferred_group: Implies :paramref:`_orm.mapped_column.deferred` + to ``True``, and set the :paramref:`_orm.deferred.group` parameter. + :param deferred_raiseload: Implies :paramref:`_orm.mapped_column.deferred` + to ``True``, and set the :paramref:`_orm.deferred.raiseload` parameter. + + :param default: Passed directly to the + :paramref:`_schema.Column.default` parameter if the + :paramref:`_orm.mapped_column.insert_default` parameter is not present. + Additionally, when used with :ref:`orm_declarative_native_dataclasses`, + indicates a default Python value that should be applied to the keyword + constructor within the generated ``__init__()`` method. + + Note that in the case of dataclass generation when + :paramref:`_orm.mapped_column.insert_default` is not present, this means + the :paramref:`_orm.mapped_column.default` value is used in **two** + places, both the ``__init__()`` method as well as the + :paramref:`_schema.Column.default` parameter. While this behavior may + change in a future release, for the moment this tends to "work out"; a + default of ``None`` will mean that the :class:`_schema.Column` gets no + default generator, whereas a default that refers to a non-``None`` Python + or SQL expression value will be assigned up front on the object when + ``__init__()`` is called, which is the same value that the Core + :class:`_sql.Insert` construct would use in any case, leading to the same + end result. + :param insert_default: Passed directly to the :paramref:`_schema.Column.default` parameter; will supersede the value of :paramref:`_orm.mapped_column.default` when present, however :paramref:`_orm.mapped_column.default` will always apply to the constructor default for a dataclasses mapping. + + :param init: Specific to :ref:`orm_declarative_native_dataclasses`, + specifies if the mapped attribute should be part of the ``__init__()`` + method as generated by the dataclass process. + :param repr: Specific to :ref:`orm_declarative_native_dataclasses`, + specifies if the mapped attribute should be part of the ``__repr__()`` + method as generated by the dataclass process. + :param default_factory: Specific to + :ref:`orm_declarative_native_dataclasses`, + specifies a default-value generation function that will take place + as part of the ``__init__()`` + method as generated by the dataclass process. + :param \**kw: All remaining keyword argments are passed through to the constructor for the :class:`_schema.Column`. @@ -341,6 +280,8 @@ def mapped_column( comment=comment, system=system, deferred=deferred, + deferred_group=deferred_group, + deferred_raiseload=deferred_raiseload, **dialect_kwargs, ) @@ -569,6 +510,18 @@ def composite( :param info: Optional data dictionary which will be populated into the :attr:`.MapperProperty.info` attribute of this object. + :param init: Specific to :ref:`orm_declarative_native_dataclasses`, + specifies if the mapped attribute should be part of the ``__init__()`` + method as generated by the dataclass process. + :param repr: Specific to :ref:`orm_declarative_native_dataclasses`, + specifies if the mapped attribute should be part of the ``__repr__()`` + method as generated by the dataclass process. + :param default_factory: Specific to + :ref:`orm_declarative_native_dataclasses`, + specifies a default-value generation function that will take place + as part of the ``__init__()`` + method as generated by the dataclass process. + """ if __kw: raise _no_kw() @@ -825,87 +778,50 @@ def relationship( """Provide a relationship between two mapped classes. This corresponds to a parent-child or associative table relationship. - The constructed class is an instance of - :class:`.Relationship`. - - A typical :func:`_orm.relationship`, used in a classical mapping:: - - mapper(Parent, properties={ - 'children': relationship(Child) - }) - - Some arguments accepted by :func:`_orm.relationship` - optionally accept a - callable function, which when called produces the desired value. - The callable is invoked by the parent :class:`_orm.Mapper` at "mapper - initialization" time, which happens only when mappers are first used, - and is assumed to be after all mappings have been constructed. This - can be used to resolve order-of-declaration and other dependency - issues, such as if ``Child`` is declared below ``Parent`` in the same - file:: - - mapper(Parent, properties={ - "children":relationship(lambda: Child, - order_by=lambda: Child.id) - }) - - When using the :ref:`declarative_toplevel` extension, the Declarative - initializer allows string arguments to be passed to - :func:`_orm.relationship`. These string arguments are converted into - callables that evaluate the string as Python code, using the - Declarative class-registry as a namespace. This allows the lookup of - related classes to be automatic via their string name, and removes the - need for related classes to be imported into the local module space - before the dependent classes have been declared. It is still required - that the modules in which these related classes appear are imported - anywhere in the application at some point before the related mappings - are actually used, else a lookup error will be raised when the - :func:`_orm.relationship` - attempts to resolve the string reference to the - related class. An example of a string- resolved class is as - follows:: - - from sqlalchemy.ext.declarative import declarative_base - - Base = declarative_base() - - class Parent(Base): - __tablename__ = 'parent' - id = Column(Integer, primary_key=True) - children = relationship("Child", order_by="Child.id") + The constructed class is an instance of :class:`.Relationship`. .. seealso:: - :ref:`relationship_config_toplevel` - Full introductory and - reference documentation for :func:`_orm.relationship`. + :ref:`tutorial_orm_related_objects` - tutorial introduction + to :func:`_orm.relationship` in the :ref:`unified_tutorial` - :ref:`tutorial_orm_related_objects` - ORM tutorial introduction. + :ref:`relationship_config_toplevel` - narrative documentation :param argument: - A mapped class, or actual :class:`_orm.Mapper` instance, - representing - the target of the relationship. + This parameter refers to the class that is to be related. It + accepts several forms, including a direct reference to the target + class itself, the :class:`_orm.Mapper` instance for the target class, + a Python callable / lambda that will return a reference to the + class or :class:`_orm.Mapper` when called, and finally a string + name for the class, which will be resolved from the + :class:`_orm.registry` in use in order to locate the class, e.g.:: - :paramref:`_orm.relationship.argument` - may also be passed as a callable - function which is evaluated at mapper initialization time, and may - be passed as a string name when using Declarative. + class SomeClass(Base): + # ... - .. warning:: Prior to SQLAlchemy 1.3.16, this value is interpreted - using Python's ``eval()`` function. - **DO NOT PASS UNTRUSTED INPUT TO THIS STRING**. - See :ref:`declarative_relationship_eval` for details on - declarative evaluation of :func:`_orm.relationship` arguments. + related = relationship("RelatedClass") + + The :paramref:`_orm.relationship.argument` may also be omitted from the + :func:`_orm.relationship` construct entirely, and instead placed inside + a :class:`_orm.Mapped` annotation on the left side, which should + include a Python collection type if the relationship is expected + to be a collection, such as:: + + class SomeClass(Base): + # ... - .. versionchanged 1.3.16:: + related_items: Mapped[List["RelatedItem"]] = relationship() - The string evaluation of the main "argument" no longer accepts an - open ended Python expression, instead only accepting a string - class name or dotted package-qualified name. + Or for a many-to-one or one-to-one relationship:: + + class SomeClass(Base): + # ... + + related_item: Mapped["RelatedItem"] = relationship() .. seealso:: - :ref:`declarative_configuring_relationships` - further detail + :ref:`orm_declarative_properties` - further detail on relationship configuration when using Declarative. :param secondary: @@ -1530,13 +1446,17 @@ def relationship( A boolean that indicates if this property should be loaded as a list or a scalar. In most cases, this value is determined automatically by :func:`_orm.relationship` at mapper configuration - time, based on the type and direction + time. When using explicit :class:`_orm.Mapped` annotations, + :paramref:`_orm.relationship.uselist` may be derived from the + whether or not the annotation within :class:`_orm.Mapped` contains + a collection class. + Otherwise, :paramref:`_orm.relationship.uselist` may be derived from + the type and direction of the relationship - one to many forms a list, many to one forms a scalar, many to many is a list. If a scalar is desired where normally a list would be present, such as a bi-directional - one-to-one relationship, set :paramref:`_orm.relationship.uselist` - to - False. + one-to-one relationship, use an appropriate :class:`_orm.Mapped` + annotation or set :paramref:`_orm.relationship.uselist` to False. The :paramref:`_orm.relationship.uselist` flag is also available on an @@ -1552,8 +1472,8 @@ def relationship( .. seealso:: :ref:`relationships_one_to_one` - Introduction to the "one to - one" relationship pattern, which is typically when the - :paramref:`_orm.relationship.uselist` flag is needed. + one" relationship pattern, which is typically when an alternate + setting for :paramref:`_orm.relationship.uselist` is involved. :param viewonly=False: When set to ``True``, the relationship is used only for loading @@ -1622,6 +1542,19 @@ def relationship( .. versionadded:: 1.3 + :param init: Specific to :ref:`orm_declarative_native_dataclasses`, + specifies if the mapped attribute should be part of the ``__init__()`` + method as generated by the dataclass process. + :param repr: Specific to :ref:`orm_declarative_native_dataclasses`, + specifies if the mapped attribute should be part of the ``__repr__()`` + method as generated by the dataclass process. + :param default_factory: Specific to + :ref:`orm_declarative_native_dataclasses`, + specifies a default-value generation function that will take place + as part of the ``__init__()`` + method as generated by the dataclass process. + + """ return Relationship( @@ -1927,6 +1860,10 @@ def deferred( r"""Indicate a column-based mapped attribute that by default will not load unless accessed. + When using :func:`_orm.mapped_column`, the same functionality as + that of :func:`_orm.deferred` construct is provided by using the + :paramref:`_orm.mapped_column.deferred` parameter. + :param \*columns: columns to be mapped. This is typically a single :class:`_schema.Column` object, however a collection is supported in order @@ -1937,9 +1874,6 @@ def deferred( .. versionadded:: 1.4 - .. seealso:: - - :ref:`deferred_raiseload` Additional arguments are the same as that of :func:`_orm.column_property`. @@ -1947,6 +1881,8 @@ def deferred( :ref:`deferred` + :ref:`deferred_raiseload` + """ return ColumnProperty( column, diff --git a/lib/sqlalchemy/orm/base.py b/lib/sqlalchemy/orm/base.py index 63f873fd0..fa653a472 100644 --- a/lib/sqlalchemy/orm/base.py +++ b/lib/sqlalchemy/orm/base.py @@ -714,6 +714,18 @@ class Mapped(ORMDescriptor[_T], roles.TypedColumnsClauseRole[_T], TypingOnly): checkers such as pylance and mypy so that ORM-mapped attributes are correctly typed. + The most prominent use of :class:`_orm.Mapped` is in + the :ref:`Declarative Mapping <orm_explicit_declarative_base>` form + of :class:`_orm.Mapper` configuration, where used explicitly it drives + the configuration of ORM attributes such as :func:`_orm.mapped_class` + and :func:`_orm.relationship`. + + .. seealso:: + + :ref:`orm_explicit_declarative_base` + + :ref:`orm_declarative_table` + .. tip:: The :class:`_orm.Mapped` class represents attributes that are handled diff --git a/lib/sqlalchemy/orm/decl_api.py b/lib/sqlalchemy/orm/decl_api.py index 05d6dacfb..7249698c0 100644 --- a/lib/sqlalchemy/orm/decl_api.py +++ b/lib/sqlalchemy/orm/decl_api.py @@ -17,6 +17,7 @@ from typing import Callable from typing import ClassVar from typing import Dict from typing import FrozenSet +from typing import Generic from typing import Iterator from typing import Mapping from typing import Optional @@ -79,8 +80,11 @@ if TYPE_CHECKING: from .interfaces import MapperProperty from .state import InstanceState # noqa from ..sql._typing import _TypeEngineArgument + _T = TypeVar("_T", bound=Any) +_TT = TypeVar("_TT", bound=Any) + # it's not clear how to have Annotated, Union objects etc. as keys here # from a typing perspective so just leave it open ended for now _TypeAnnotationMapType = Mapping[Any, "_TypeEngineArgument[Any]"] @@ -228,100 +232,10 @@ def synonym_for( return decorate -class declared_attr(interfaces._MappedAttribute[_T]): - """Mark a class-level method as representing the definition of - a mapped property or special declarative member name. - - :class:`_orm.declared_attr` is typically applied as a decorator to a class - level method, turning the attribute into a scalar-like property that can be - invoked from the uninstantiated class. The Declarative mapping process - looks for these :class:`_orm.declared_attr` callables as it scans classes, - and assumes any attribute marked with :class:`_orm.declared_attr` will be a - callable that will produce an object specific to the Declarative mapping or - table configuration. - - :class:`_orm.declared_attr` is usually applicable to mixins, to define - relationships that are to be applied to different implementors of the - class. It is also used to define :class:`_schema.Column` objects that - include the :class:`_schema.ForeignKey` construct, as these cannot be - easily reused across different mappings. The example below illustrates - both:: - - class ProvidesUser: - "A mixin that adds a 'user' relationship to classes." - - @declared_attr - def user_id(self): - return Column(ForeignKey("user_account.id")) - - @declared_attr - def user(self): - return relationship("User") - - :class:`_orm.declared_attr` can also be applied to mapped classes, such as - to provide a "polymorphic" scheme for inheritance:: - - class Employee(Base): - id = Column(Integer, primary_key=True) - type = Column(String(50), nullable=False) - - @declared_attr - def __tablename__(cls): - return cls.__name__.lower() - - @declared_attr - def __mapper_args__(cls): - if cls.__name__ == 'Employee': - return { - "polymorphic_on":cls.type, - "polymorphic_identity":"Employee" - } - else: - return {"polymorphic_identity":cls.__name__} - - To use :class:`_orm.declared_attr` inside of a Python dataclass - as discussed at :ref:`orm_declarative_dataclasses_declarative_table`, - it may be placed directly inside the field metadata using a lambda:: - - @dataclass - class AddressMixin: - __sa_dataclass_metadata_key__ = "sa" - - user_id: int = field( - init=False, metadata={"sa": declared_attr(lambda: Column(ForeignKey("user.id")))} - ) - user: User = field( - init=False, metadata={"sa": declared_attr(lambda: relationship(User))} - ) - - :class:`_orm.declared_attr` also may be omitted from this form using a - lambda directly, as in:: - - user: User = field( - init=False, metadata={"sa": lambda: relationship(User)} - ) - - .. seealso:: - - :ref:`orm_mixins_toplevel` - illustrates how to use Declarative Mixins - which is the primary use case for :class:`_orm.declared_attr` - - :ref:`orm_declarative_dataclasses_mixin` - illustrates special forms - for use with Python dataclasses - - """ # noqa: E501 - - if typing.TYPE_CHECKING: - - def __set__(self, instance: Any, value: Any) -> None: - ... - - def __delete__(self, instance: Any) -> None: - ... - +class _declared_attr_common: def __init__( self, - fn: _DeclaredAttrDecorated[_T], + fn: Callable[..., Any], cascading: bool = False, ): # suppport @@ -341,21 +255,7 @@ class declared_attr(interfaces._MappedAttribute[_T]): def _collect_return_annotation(self) -> Optional[Type[Any]]: return util.get_annotations(self.fget).get("return") - # this is the Mapped[] API where at class descriptor get time we want - # the type checker to see InstrumentedAttribute[_T]. However the - # callable function prior to mapping in fact calls the given - # declarative function that does not return InstrumentedAttribute - @overload - def __get__(self, instance: None, owner: Any) -> InstrumentedAttribute[_T]: - ... - - @overload - def __get__(self, instance: object, owner: Any) -> _T: - ... - - def __get__( - self, instance: Optional[object], owner: Any - ) -> Union[InstrumentedAttribute[_T], _T]: + def __get__(self, instance: Optional[object], owner: Any) -> Any: # the declared_attr needs to make use of a cache that exists # for the span of the declarative scan_attributes() phase. # to achieve this we look at the class manager that's configured. @@ -394,70 +294,171 @@ class declared_attr(interfaces._MappedAttribute[_T]): reg[self] = obj = self.fget(cls) return obj # type: ignore - @hybridmethod - def _stateful(cls, **kw: Any) -> _stateful_declared_attr[_T]: - return _stateful_declared_attr(**kw) - @hybridproperty - def cascading(cls) -> _stateful_declared_attr[_T]: - """Mark a :class:`.declared_attr` as cascading. - - This is a special-use modifier which indicates that a column - or MapperProperty-based declared attribute should be configured - distinctly per mapped subclass, within a mapped-inheritance scenario. - - .. warning:: - - The :attr:`.declared_attr.cascading` modifier has several - limitations: - - * The flag **only** applies to the use of :class:`.declared_attr` - on declarative mixin classes and ``__abstract__`` classes; it - currently has no effect when used on a mapped class directly. - - * The flag **only** applies to normally-named attributes, e.g. - not any special underscore attributes such as ``__tablename__``. - On these attributes it has **no** effect. - - * The flag currently **does not allow further overrides** down - the class hierarchy; if a subclass tries to override the - attribute, a warning is emitted and the overridden attribute - is skipped. This is a limitation that it is hoped will be - resolved at some point. - - Below, both MyClass as well as MySubClass will have a distinct - ``id`` Column object established:: - - class HasIdMixin: - @declared_attr.cascading - def id(cls): - if has_inherited_table(cls): - return Column( - ForeignKey('myclass.id'), primary_key=True - ) - else: - return Column(Integer, primary_key=True) - - class MyClass(HasIdMixin, Base): - __tablename__ = 'myclass' - # ... +class _declared_directive(_declared_attr_common, Generic[_T]): + # see mapping_api.rst for docstring - class MySubClass(MyClass): - "" - # ... + if typing.TYPE_CHECKING: - The behavior of the above configuration is that ``MySubClass`` - will refer to both its own ``id`` column as well as that of - ``MyClass`` underneath the attribute named ``some_id``. + def __init__( + self, + fn: Callable[..., _T], + cascading: bool = False, + ): + ... - .. seealso:: + def __get__(self, instance: Optional[object], owner: Any) -> _T: + ... + + def __set__(self, instance: Any, value: Any) -> None: + ... - :ref:`declarative_inheritance` + def __delete__(self, instance: Any) -> None: + ... - :ref:`mixin_inheritance_columns` + def __call__(self, fn: Callable[..., _TT]) -> _declared_directive[_TT]: + # extensive fooling of mypy underway... + ... - """ +class declared_attr(interfaces._MappedAttribute[_T], _declared_attr_common): + """Mark a class-level method as representing the definition of + a mapped property or Declarative directive. + + :class:`_orm.declared_attr` is typically applied as a decorator to a class + level method, turning the attribute into a scalar-like property that can be + invoked from the uninstantiated class. The Declarative mapping process + looks for these :class:`_orm.declared_attr` callables as it scans classes, + and assumes any attribute marked with :class:`_orm.declared_attr` will be a + callable that will produce an object specific to the Declarative mapping or + table configuration. + + :class:`_orm.declared_attr` is usually applicable to + :ref:`mixins <orm_mixins_toplevel>`, to define relationships that are to be + applied to different implementors of the class. It may also be used to + define dynamically generated column expressions and other Declarative + attributes. + + Example:: + + class ProvidesUserMixin: + "A mixin that adds a 'user' relationship to classes." + + user_id: Mapped[int] = mapped_column(ForeignKey("user_table.id")) + + @declared_attr + def user(cls) -> Mapped["User"]: + return relationship("User") + + When used with Declarative directives such as ``__tablename__``, the + :meth:`_orm.declared_attr.directive` modifier may be used which indicates + to :pep:`484` typing tools that the given method is not dealing with + :class:`_orm.Mapped` attributes:: + + class CreateTableName: + @declared_attr.directive + def __tablename__(cls) -> str: + return cls.__name__.lower() + + :class:`_orm.declared_attr` can also be applied directly to mapped + classes, to allow for attributes that dynamically configure themselves + on subclasses when using mapped inheritance schemes. Below + illustrates :class:`_orm.declared_attr` to create a dynamic scheme + for generating the :paramref:`_orm.Mapper.polymorphic_identity` parameter + for subclasses:: + + class Employee(Base): + __tablename__ = 'employee' + + id: Mapped[int] = mapped_column(primary_key=True) + type: Mapped[str] = mapped_column(String(50)) + + @declared_attr.directive + def __mapper_args__(cls) -> dict[str, Any]: + if cls.__name__ == 'Employee': + return { + "polymorphic_on":cls.type, + "polymorphic_identity":"Employee" + } + else: + return {"polymorphic_identity":cls.__name__} + + class Engineer(Employee): + pass + + :class:`_orm.declared_attr` supports decorating functions that are + explicitly decorated with ``@classmethod``. This is never necessary from a + runtime perspective, however may be needed in order to support :pep:`484` + typing tools that don't otherwise recognize the decorated function as + having class-level behaviors for the ``cls`` parameter:: + + class SomethingMixin: + x: Mapped[int] + y: Mapped[int] + + @declared_attr + @classmethod + def x_plus_y(cls) -> Mapped[int]: + return column_property(cls.x + cls.y) + + .. versionadded:: 2.0 - :class:`_orm.declared_attr` can accommodate a + function decorated with ``@classmethod`` to help with :pep:`484` + integration where needed. + + + .. seealso:: + + :ref:`orm_mixins_toplevel` - Declarative Mixin documentation with + background on use patterns for :class:`_orm.declared_attr`. + + """ # noqa: E501 + + if typing.TYPE_CHECKING: + + def __init__( + self, + fn: _DeclaredAttrDecorated[_T], + cascading: bool = False, + ): + ... + + def __set__(self, instance: Any, value: Any) -> None: + ... + + def __delete__(self, instance: Any) -> None: + ... + + # this is the Mapped[] API where at class descriptor get time we want + # the type checker to see InstrumentedAttribute[_T]. However the + # callable function prior to mapping in fact calls the given + # declarative function that does not return InstrumentedAttribute + @overload + def __get__( + self, instance: None, owner: Any + ) -> InstrumentedAttribute[_T]: + ... + + @overload + def __get__(self, instance: object, owner: Any) -> _T: + ... + + def __get__( + self, instance: Optional[object], owner: Any + ) -> Union[InstrumentedAttribute[_T], _T]: + ... + + @hybridmethod + def _stateful(cls, **kw: Any) -> _stateful_declared_attr[_T]: + return _stateful_declared_attr(**kw) + + @hybridproperty + def directive(cls) -> _declared_directive[Any]: + # see mapping_api.rst for docstring + return _declared_directive # type: ignore + + @hybridproperty + def cascading(cls) -> _stateful_declared_attr[_T]: + # see mapping_api.rst for docstring return cls._stateful(cascading=True) @@ -557,45 +558,17 @@ def _setup_declarative_base(cls: Type[Any]) -> None: cls.metadata = cls.registry.metadata # type: ignore -class DeclarativeBaseNoMeta(inspection.Inspectable[Mapper[Any]]): - """Same as :class:`_orm.DeclarativeBase`, but does not use a metaclass - to intercept new attributes. - - The :class:`_orm.DeclarativeBaseNoMeta` base may be used when use of - custom metaclasses is desirable. - - .. versionadded:: 2.0 - - - """ - - registry: ClassVar[_RegistryType] - _sa_registry: ClassVar[_RegistryType] - metadata: ClassVar[MetaData] - __mapper__: ClassVar[Mapper[Any]] - __table__: Optional[FromClause] - - if typing.TYPE_CHECKING: - - def __init__(self, **kw: Any): - ... - - def __init_subclass__(cls) -> None: - if DeclarativeBaseNoMeta in cls.__bases__: - _setup_declarative_base(cls) - else: - cls._sa_registry.map_declaratively(cls) - - class MappedAsDataclass(metaclass=DCTransformDeclarative): """Mixin class to indicate when mapping this class, also convert it to be a dataclass. .. seealso:: - :meth:`_orm.registry.mapped_as_dataclass` + :ref:`orm_declarative_native_dataclasses` - complete background + on SQLAlchemy native dataclass mapping .. versionadded:: 2.0 + """ def __init_subclass__( @@ -636,7 +609,6 @@ class DeclarativeBase( ): """Base class used for declarative class definitions. - The :class:`_orm.DeclarativeBase` allows for the creation of new declarative bases in such a way that is compatible with type checkers:: @@ -689,7 +661,14 @@ class DeclarativeBase( :paramref:`_orm.registry.type_annotation_map`. :param registry: supply a pre-existing :class:`_orm.registry` directly. - .. versionadded:: 2.0 + .. versionadded:: 2.0 Added :class:`.DeclarativeBase`, so that declarative + base classes may be constructed in such a way that is also recognized + by :pep:`484` type checkers. As a result, :class:`.DeclarativeBase` + and other subclassing-oriented APIs should be seen as + superseding previous "class returned by a function" APIs, namely + :func:`_orm.declarative_base` and :meth:`_orm.registry.generate_base`, + where the base class returned cannot be recognized by type checkers + without using plugins. """ @@ -698,10 +677,11 @@ class DeclarativeBase( _sa_registry: ClassVar[_RegistryType] metadata: ClassVar[MetaData] + __name__: ClassVar[str] __mapper__: ClassVar[Mapper[Any]] __table__: ClassVar[Optional[FromClause]] - __tablename__: ClassVar[Optional[str]] + __tablename__: ClassVar[Any] def __init__(self, **kw: Any): ... @@ -713,6 +693,39 @@ class DeclarativeBase( _as_declarative(cls._sa_registry, cls, cls.__dict__) +class DeclarativeBaseNoMeta(inspection.Inspectable[Mapper[Any]]): + """Same as :class:`_orm.DeclarativeBase`, but does not use a metaclass + to intercept new attributes. + + The :class:`_orm.DeclarativeBaseNoMeta` base may be used when use of + custom metaclasses is desirable. + + .. versionadded:: 2.0 + + + """ + + if typing.TYPE_CHECKING: + registry: ClassVar[_RegistryType] + _sa_registry: ClassVar[_RegistryType] + metadata: ClassVar[MetaData] + + __name__: ClassVar[str] + __mapper__: ClassVar[Mapper[Any]] + __table__: ClassVar[Optional[FromClause]] + + __tablename__: ClassVar[Any] + + def __init__(self, **kw: Any): + ... + + def __init_subclass__(cls) -> None: + if DeclarativeBaseNoMeta in cls.__bases__: + _setup_declarative_base(cls) + else: + cls._sa_registry.map_declaratively(cls) + + def add_mapped_attribute( target: Type[_O], key: str, attr: MapperProperty[Any] ) -> None: @@ -751,6 +764,12 @@ def declarative_base( information provided declaratively in the class and any subclasses of the class. + .. versionchanged:: 2.0 Note that the :func:`_orm.declarative_base` + function is superseded by the new :class:`_orm.DeclarativeBase` class, + which generates a new "base" class using subclassing, rather than + return value of a function. This allows an approach that is compatible + with :pep:`484` typing tools. + The :func:`_orm.declarative_base` function is a shorthand version of using the :meth:`_orm.registry.generate_base` method. That is, the following:: @@ -818,8 +837,13 @@ def declarative_base( to produce column types based on annotations within the :class:`_orm.Mapped` type. + .. versionadded:: 2.0 + .. seealso:: + + :ref:`orm_declarative_mapped_column_type_map` + :param metaclass: Defaults to :class:`.DeclarativeMeta`. A metaclass or __metaclass__ compatible callable to use as the meta type of the generated @@ -928,6 +952,11 @@ class registry: .. versionadded:: 2.0 + .. seealso:: + + :ref:`orm_declarative_mapped_column_type_map` + + """ lcl_metadata = metadata or MetaData() @@ -1176,6 +1205,13 @@ class registry: __init__ = mapper_registry.constructor + .. versionchanged:: 2.0 Note that the + :meth:`_orm.registry.generate_base` method is superseded by the new + :class:`_orm.DeclarativeBase` class, which generates a new "base" + class using subclassing, rather than return value of a function. + This allows an approach that is compatible with :pep:`484` typing + tools. + The :meth:`_orm.registry.generate_base` method provides the implementation for the :func:`_orm.declarative_base` function, which creates the :class:`_orm.registry` and base class all at once. @@ -1282,7 +1318,9 @@ class registry: .. seealso:: - :meth:`_orm.registry.mapped` + :ref:`orm_declarative_native_dataclasses` - complete background + on SQLAlchemy native dataclass mapping + .. versionadded:: 2.0 diff --git a/lib/sqlalchemy/orm/mapped_collection.py b/lib/sqlalchemy/orm/mapped_collection.py index d1057ca5f..f34083c91 100644 --- a/lib/sqlalchemy/orm/mapped_collection.py +++ b/lib/sqlalchemy/orm/mapped_collection.py @@ -167,12 +167,21 @@ def mapped_collection( class MappedCollection(Dict[_KT, _VT]): - """A basic dictionary-based collection class. + """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 + custom dictionary classes. + + .. seealso:: + + :ref:`orm_dictionary_collection` + + :ref:`orm_custom_collection` - Extends dict with the minimal bag semantics that collection - classes require. ``set`` and ``remove`` are implemented in terms - of a keying function: any callable that takes an object and - returns an object for use as a dictionary key. """ diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 0241a3123..769b1b623 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -353,12 +353,16 @@ class Mapper( :param exclude_properties: A list or set of string column names to be excluded from mapping. - See :ref:`include_exclude_cols` for an example. + .. seealso:: + + :ref:`include_exclude_cols` :param include_properties: An inclusive list or set of string column names to map. - See :ref:`include_exclude_cols` for an example. + .. seealso:: + + :ref:`include_exclude_cols` :param inherits: A mapped class or the corresponding :class:`_orm.Mapper` @@ -542,8 +546,8 @@ class Mapper( ) __mapper_args__ = { - "polymorphic_on":employee_type, - "polymorphic_identity":"employee" + "polymorphic_on": "employee_type", + "polymorphic_identity": "employee" } When setting ``polymorphic_on`` to reference an diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index c5f50d7b4..051b6df8b 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -486,11 +486,16 @@ class MappedColumn( "foreign_keys", "_has_nullable", "deferred", + "deferred_group", + "deferred_raiseload", "_attribute_options", "_has_dataclass_arguments", ) deferred: bool + deferred_raiseload: bool + deferred_group: Optional[str] + column: Column[_T] foreign_keys: Optional[Set[ForeignKey]] _attribute_options: _AttributeOptions @@ -514,7 +519,14 @@ class MappedColumn( kw["default"] = kw.pop("insert_default", None) - self.deferred = kw.pop("deferred", False) + self.deferred_group = kw.pop("deferred_group", None) + self.deferred_raiseload = kw.pop("deferred_raiseload", None) + self.deferred = kw.pop("deferred", _NoArg.NO_ARG) + if self.deferred is _NoArg.NO_ARG: + self.deferred = bool( + self.deferred_group or self.deferred_raiseload + ) + self.column = cast("Column[_T]", Column(*arg, **kw)) self.foreign_keys = self.column.foreign_keys self._has_nullable = "nullable" in kw and kw.get("nullable") not in ( @@ -545,6 +557,8 @@ class MappedColumn( return ColumnProperty( self.column, deferred=True, + group=self.deferred_group, + raiseload=self.deferred_raiseload, attribute_options=self._attribute_options, ) else: diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py index c0a2bcf65..cb955ff3d 100644 --- a/lib/sqlalchemy/testing/requirements.py +++ b/lib/sqlalchemy/testing/requirements.py @@ -1395,6 +1395,12 @@ class SuiteRequirements(Requirements): ) @property + def python39(self): + return exclusions.only_if( + lambda: util.py39, "Python 3.9 or above required" + ) + + @property def cpython(self): return exclusions.only_if( lambda: util.cpython, "cPython interpreter needed" |
