summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/orm')
-rw-r--r--lib/sqlalchemy/orm/__init__.py6
-rw-r--r--lib/sqlalchemy/orm/_orm_constructors.py242
-rw-r--r--lib/sqlalchemy/orm/attributes.py103
-rw-r--r--lib/sqlalchemy/orm/base.py91
-rw-r--r--lib/sqlalchemy/orm/decl_api.py184
-rw-r--r--lib/sqlalchemy/orm/descriptor_props.py7
-rw-r--r--lib/sqlalchemy/orm/interfaces.py49
-rw-r--r--lib/sqlalchemy/orm/properties.py9
-rw-r--r--lib/sqlalchemy/orm/relationships.py13
9 files changed, 566 insertions, 138 deletions
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py
index 17167a7de..55f2f3100 100644
--- a/lib/sqlalchemy/orm/__init__.py
+++ b/lib/sqlalchemy/orm/__init__.py
@@ -25,18 +25,22 @@ from ._orm_constructors import contains_alias as contains_alias
from ._orm_constructors import create_session as create_session
from ._orm_constructors import deferred as deferred
from ._orm_constructors import dynamic_loader as dynamic_loader
+from ._orm_constructors import mapped_column as mapped_column
from ._orm_constructors import query_expression as query_expression
from ._orm_constructors import relationship as relationship
from ._orm_constructors import synonym as synonym
from ._orm_constructors import with_loader_criteria as with_loader_criteria
from .attributes import AttributeEvent as AttributeEvent
from .attributes import InstrumentedAttribute as InstrumentedAttribute
-from .attributes import Mapped as Mapped
from .attributes import QueryableAttribute as QueryableAttribute
+from .base import Mapped as Mapped
from .context import QueryContext as QueryContext
+from .decl_api import add_mapped_attribute as add_mapped_attribute
from .decl_api import as_declarative as as_declarative
from .decl_api import declarative_base as declarative_base
from .decl_api import declarative_mixin as declarative_mixin
+from .decl_api import DeclarativeBase as DeclarativeBase
+from .decl_api import DeclarativeBaseNoMeta as DeclarativeBaseNoMeta
from .decl_api import DeclarativeMeta as DeclarativeMeta
from .decl_api import declared_attr as declared_attr
from .decl_api import has_inherited_table as has_inherited_table
diff --git a/lib/sqlalchemy/orm/_orm_constructors.py b/lib/sqlalchemy/orm/_orm_constructors.py
index be0d23d00..80607670e 100644
--- a/lib/sqlalchemy/orm/_orm_constructors.py
+++ b/lib/sqlalchemy/orm/_orm_constructors.py
@@ -6,11 +6,16 @@
# the MIT License: https://www.opensource.org/licenses/mit-license.php
import typing
+from typing import Any
from typing import Callable
+from typing import Collection
+from typing import Optional
+from typing import overload
from typing import Type
from typing import Union
from . import mapper as mapperlib
+from .base import Mapped
from .descriptor_props import CompositeProperty
from .descriptor_props import SynonymProperty
from .properties import ColumnProperty
@@ -21,6 +26,11 @@ from .util import LoaderCriteriaOption
from .. import sql
from .. import util
from ..exc import InvalidRequestError
+from ..sql.schema import Column
+from ..sql.schema import SchemaEventTarget
+from ..sql.type_api import TypeEngine
+from ..util.typing import Literal
+
_RC = typing.TypeVar("_RC")
_T = typing.TypeVar("_T")
@@ -41,9 +51,138 @@ def contains_alias(alias) -> "AliasOption":
return AliasOption(alias)
+@overload
+def mapped_column(
+ *args: SchemaEventTarget,
+ nullable: bool = ...,
+ primary_key: bool = ...,
+ **kw: Any,
+) -> "Mapped":
+ ...
+
+
+@overload
+def mapped_column(
+ __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"],
+ *args: SchemaEventTarget,
+ nullable: Union[Literal[None], Literal[True]] = ...,
+ primary_key: Union[Literal[None], Literal[False]] = ...,
+ **kw: Any,
+) -> "Mapped[Optional[_T]]":
+ ...
+
+
+@overload
+def mapped_column(
+ __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"],
+ *args: SchemaEventTarget,
+ nullable: Union[Literal[None], Literal[True]] = ...,
+ primary_key: Union[Literal[None], Literal[False]] = ...,
+ **kw: Any,
+) -> "Mapped[Optional[_T]]":
+ ...
+
+
+@overload
+def mapped_column(
+ __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"],
+ *args: SchemaEventTarget,
+ nullable: Union[Literal[None], Literal[False]] = ...,
+ primary_key: Literal[True] = True,
+ **kw: Any,
+) -> "Mapped[_T]":
+ ...
+
+
+@overload
+def mapped_column(
+ __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"],
+ *args: SchemaEventTarget,
+ nullable: Literal[False] = ...,
+ primary_key: bool = ...,
+ **kw: Any,
+) -> "Mapped[_T]":
+ ...
+
+
+@overload
+def mapped_column(
+ __name: str,
+ __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"],
+ *args: SchemaEventTarget,
+ nullable: Union[Literal[None], Literal[True]] = ...,
+ primary_key: Union[Literal[None], Literal[False]] = ...,
+ **kw: Any,
+) -> "Mapped[Optional[_T]]":
+ ...
+
+
+@overload
+def mapped_column(
+ __name: str,
+ __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"],
+ *args: SchemaEventTarget,
+ nullable: Union[Literal[None], Literal[True]] = ...,
+ primary_key: Union[Literal[None], Literal[False]] = ...,
+ **kw: Any,
+) -> "Mapped[Optional[_T]]":
+ ...
+
+
+@overload
+def mapped_column(
+ __name: str,
+ __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"],
+ *args: SchemaEventTarget,
+ nullable: Union[Literal[None], Literal[False]] = ...,
+ primary_key: Literal[True] = True,
+ **kw: Any,
+) -> "Mapped[_T]":
+ ...
+
+
+@overload
+def mapped_column(
+ __name: str,
+ __type: Union[Type["TypeEngine[_T]"], "TypeEngine[_T]"],
+ *args: SchemaEventTarget,
+ nullable: Literal[False] = ...,
+ primary_key: bool = ...,
+ **kw: Any,
+) -> "Mapped[_T]":
+ ...
+
+
+def mapped_column(*args, **kw) -> "Mapped":
+ """construct a new ORM-mapped :class:`_schema.Column` construct.
+
+ The :func:`_orm.mapped_column` function is shorthand for the construction
+ of a Core :class:`_schema.Column` object delivered within a
+ :func:`_orm.column_property` construct, which provides for consistent
+ typing information to be delivered to the class so that it works under
+ static type checkers such as mypy and delivers useful information in
+ IDE related type checkers such as pylance. The function can be used
+ in declarative mappings anywhere that :class:`_schema.Column` is normally
+ used::
+
+ from sqlalchemy.orm import mapped_column
+
+ class User(Base):
+ __tablename__ = 'user'
+
+ id = mapped_column(Integer)
+ name = mapped_column(String)
+
+
+ .. versionadded:: 2.0
+
+ """
+ return column_property(Column(*args, **kw))
+
+
def column_property(
column: sql.ColumnElement[_T], *additional_columns, **kwargs
-) -> "ColumnProperty[_T]":
+) -> "Mapped[_T]":
r"""Provide a column-level property for use with a mapping.
Column-based properties can normally be applied to the mapper's
@@ -130,7 +269,7 @@ def column_property(
return ColumnProperty(column, *additional_columns, **kwargs)
-def composite(class_: Type[_T], *attrs, **kwargs) -> "CompositeProperty[_T]":
+def composite(class_: Type[_T], *attrs, **kwargs) -> "Mapped[_T]":
r"""Return a composite column-based property for use with a Mapper.
See the mapping documentation section :ref:`mapper_composite` for a
@@ -359,13 +498,106 @@ def with_loader_criteria(
)
+@overload
+def relationship(
+ argument: Union[str, Type[_RC], Callable[[], Type[_RC]]],
+ secondary=None,
+ *,
+ uselist: Literal[True] = None,
+ primaryjoin=None,
+ secondaryjoin=None,
+ foreign_keys=None,
+ order_by=False,
+ backref=None,
+ back_populates=None,
+ overlaps=None,
+ post_update=False,
+ cascade=False,
+ viewonly=False,
+ lazy="select",
+ collection_class=None,
+ passive_deletes=RelationshipProperty._persistence_only["passive_deletes"],
+ passive_updates=RelationshipProperty._persistence_only["passive_updates"],
+ remote_side=None,
+ enable_typechecks=RelationshipProperty._persistence_only[
+ "enable_typechecks"
+ ],
+ join_depth=None,
+ comparator_factory=None,
+ single_parent=False,
+ innerjoin=False,
+ distinct_target_key=None,
+ doc=None,
+ active_history=RelationshipProperty._persistence_only["active_history"],
+ cascade_backrefs=RelationshipProperty._persistence_only[
+ "cascade_backrefs"
+ ],
+ load_on_pending=False,
+ bake_queries=True,
+ _local_remote_pairs=None,
+ query_class=None,
+ info=None,
+ omit_join=None,
+ sync_backref=None,
+ _legacy_inactive_history_style=False,
+) -> Mapped[Collection[_RC]]:
+ ...
+
+
+@overload
+def relationship(
+ argument: Union[str, Type[_RC], Callable[[], Type[_RC]]],
+ secondary=None,
+ *,
+ uselist: Optional[bool] = None,
+ primaryjoin=None,
+ secondaryjoin=None,
+ foreign_keys=None,
+ order_by=False,
+ backref=None,
+ back_populates=None,
+ overlaps=None,
+ post_update=False,
+ cascade=False,
+ viewonly=False,
+ lazy="select",
+ collection_class=None,
+ passive_deletes=RelationshipProperty._persistence_only["passive_deletes"],
+ passive_updates=RelationshipProperty._persistence_only["passive_updates"],
+ remote_side=None,
+ enable_typechecks=RelationshipProperty._persistence_only[
+ "enable_typechecks"
+ ],
+ join_depth=None,
+ comparator_factory=None,
+ single_parent=False,
+ innerjoin=False,
+ distinct_target_key=None,
+ doc=None,
+ active_history=RelationshipProperty._persistence_only["active_history"],
+ cascade_backrefs=RelationshipProperty._persistence_only[
+ "cascade_backrefs"
+ ],
+ load_on_pending=False,
+ bake_queries=True,
+ _local_remote_pairs=None,
+ query_class=None,
+ info=None,
+ omit_join=None,
+ sync_backref=None,
+ _legacy_inactive_history_style=False,
+) -> Mapped[_RC]:
+ ...
+
+
def relationship(
argument: Union[str, Type[_RC], Callable[[], Type[_RC]]],
secondary=None,
+ *,
primaryjoin=None,
secondaryjoin=None,
foreign_keys=None,
- uselist=None,
+ uselist: Optional[bool] = None,
order_by=False,
backref=None,
back_populates=None,
@@ -399,7 +631,7 @@ def relationship(
omit_join=None,
sync_backref=None,
_legacy_inactive_history_style=False,
-) -> RelationshipProperty[_RC]:
+) -> Mapped[_RC]:
"""Provide a relationship between two mapped classes.
This corresponds to a parent-child or associative table relationship.
@@ -1261,7 +1493,7 @@ def synonym(
comparator_factory=None,
doc=None,
info=None,
-) -> "SynonymProperty":
+) -> "Mapped":
"""Denote an attribute name as a synonym to a mapped property,
in that the attribute will mirror the value and expression behavior
of another attribute.
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py
index d24250ea0..5a605b7c6 100644
--- a/lib/sqlalchemy/orm/attributes.py
+++ b/lib/sqlalchemy/orm/attributes.py
@@ -13,10 +13,14 @@ defines a large part of the ORM's interactivity.
"""
-
+from collections import namedtuple
import operator
-from typing import Generic
+from typing import Any
+from typing import List
+from typing import NamedTuple
+from typing import Tuple
from typing import TypeVar
+from typing import Union
from . import collections
from . import exc as orm_exc
@@ -57,6 +61,8 @@ from ..sql import roles
from ..sql import traversals
from ..sql import visitors
+_T = TypeVar("_T")
+
class NoKey(str):
pass
@@ -67,9 +73,9 @@ NO_KEY = NoKey("no name")
@inspection._self_inspects
class QueryableAttribute(
- interfaces._MappedAttribute,
+ interfaces._MappedAttribute[_T],
interfaces.InspectionAttr,
- interfaces.PropComparator,
+ interfaces.PropComparator[_T],
traversals.HasCopyInternals,
roles.JoinTargetRole,
roles.OnClauseRole,
@@ -362,80 +368,7 @@ def _queryable_attribute_unreduce(key, mapped_class, parententity, entity):
return getattr(entity, key)
-_T = TypeVar("_T")
-_Generic_T = Generic[_T]
-
-
-class Mapped(QueryableAttribute, _Generic_T):
- """Represent an ORM mapped :term:`descriptor` attribute for typing purposes.
-
- This class represents the complete descriptor interface for any class
- attribute that will have been :term:`instrumented` by the ORM
- :class:`_orm.Mapper` class. When used with typing stubs, it is the final
- type that would be used by a type checker such as mypy to provide the full
- behavioral contract for the attribute.
-
- .. tip::
-
- The :class:`_orm.Mapped` class represents attributes that are handled
- directly by the :class:`_orm.Mapper` class. It does not include other
- Python descriptor classes that are provided as extensions, including
- :ref:`hybrids_toplevel` and the :ref:`associationproxy_toplevel`.
- While these systems still make use of ORM-specific superclasses
- and structures, they are not :term:`instrumented` by the
- :class:`_orm.Mapper` and instead provide their own functionality
- when they are accessed on a class.
-
- When using the :ref:`SQLAlchemy Mypy plugin <mypy_toplevel>`, the
- :class:`_orm.Mapped` construct is used in typing annotations to indicate to
- the plugin those attributes that are expected to be mapped; the plugin also
- applies :class:`_orm.Mapped` as an annotation automatically when it scans
- through declarative mappings in :ref:`orm_declarative_table` style. For
- more indirect mapping styles such as
- :ref:`imperative table <orm_imperative_table_configuration>` it is
- typically applied explicitly to class level attributes that expect
- to be mapped based on a given :class:`_schema.Table` configuration.
-
- :class:`_orm.Mapped` is defined in the
- `sqlalchemy2-stubs <https://pypi.org/project/sqlalchemy2-stubs>`_ project
- as a :pep:`484` generic class which may subscribe to any arbitrary Python
- type, which represents the Python type handled by the attribute::
-
- class MyMappedClass(Base):
- __table_ = Table(
- "some_table", Base.metadata,
- Column("id", Integer, primary_key=True),
- Column("data", String(50)),
- Column("created_at", DateTime)
- )
-
- id : Mapped[int]
- data: Mapped[str]
- created_at: Mapped[datetime]
-
- For complete background on how to use :class:`_orm.Mapped` with
- pep-484 tools like Mypy, see the link below for background on SQLAlchemy's
- Mypy plugin.
-
- .. versionadded:: 1.4
-
- .. seealso::
-
- :ref:`mypy_toplevel` - complete background on Mypy integration
-
- """
-
- def __get__(self, instance, owner):
- raise NotImplementedError()
-
- def __set__(self, instance, value):
- raise NotImplementedError()
-
- def __delete__(self, instance):
- raise NotImplementedError()
-
-
-class InstrumentedAttribute(Mapped):
+class InstrumentedAttribute(QueryableAttribute[_T]):
"""Class bound instrumented attribute which adds basic
:term:`descriptor` methods.
@@ -469,9 +402,7 @@ class InstrumentedAttribute(Mapped):
return self.impl.get(state, dict_)
-HasEntityNamespace = util.namedtuple(
- "HasEntityNamespace", ["entity_namespace"]
-)
+HasEntityNamespace = namedtuple("HasEntityNamespace", ["entity_namespace"])
HasEntityNamespace.is_mapper = HasEntityNamespace.is_aliased_class = False
@@ -1837,7 +1768,7 @@ _NO_HISTORY = util.symbol("NO_HISTORY")
_NO_STATE_SYMBOLS = frozenset([id(PASSIVE_NO_RESULT), id(NO_VALUE)])
-class History(util.namedtuple("History", ["added", "unchanged", "deleted"])):
+class History(NamedTuple):
"""A 3-tuple of added, unchanged and deleted values,
representing the changes which have occurred on an instrumented
attribute.
@@ -1862,11 +1793,13 @@ class History(util.namedtuple("History", ["added", "unchanged", "deleted"])):
"""
+ added: Union[Tuple[()], List[Any]]
+ unchanged: Union[Tuple[()], List[Any]]
+ deleted: Union[Tuple[()], List[Any]]
+
def __bool__(self):
return self != HISTORY_BLANK
- __nonzero__ = __bool__
-
def empty(self):
"""Return True if this :class:`.History` has no changes
and no existing, unchanged state.
@@ -2012,7 +1945,7 @@ class History(util.namedtuple("History", ["added", "unchanged", "deleted"])):
)
-HISTORY_BLANK = History(None, None, None)
+HISTORY_BLANK = History((), (), ())
def get_history(obj, key, passive=PASSIVE_OFF):
diff --git a/lib/sqlalchemy/orm/base.py b/lib/sqlalchemy/orm/base.py
index 93e2d609a..7ab4b7737 100644
--- a/lib/sqlalchemy/orm/base.py
+++ b/lib/sqlalchemy/orm/base.py
@@ -13,17 +13,25 @@ import operator
import typing
from typing import Any
from typing import Generic
+from typing import overload
from typing import TypeVar
+from typing import Union
from . import exc
from .. import exc as sa_exc
from .. import inspection
from .. import util
+from ..sql.elements import SQLCoreOperations
from ..util import typing as compat_typing
+from ..util.langhelpers import TypingOnly
+if typing.TYPE_CHECKING:
+ from .attributes import InstrumentedAttribute
+
_T = TypeVar("_T", bound=Any)
+
PASSIVE_NO_RESULT = util.symbol(
"PASSIVE_NO_RESULT",
"""Symbol returned by a loader callable or other attribute/history
@@ -579,7 +587,88 @@ class InspectionAttrInfo(InspectionAttr):
return {}
-class _MappedAttribute(Generic[_T]):
+class SQLORMOperations(SQLCoreOperations[_T], TypingOnly):
+ __slots__ = ()
+
+ if typing.TYPE_CHECKING:
+
+ def of_type(self, class_):
+ ...
+
+ def and_(self, *criteria):
+ ...
+
+ def any(self, criterion=None, **kwargs): # noqa A001
+ ...
+
+ def has(self, criterion=None, **kwargs):
+ ...
+
+
+class Mapped(Generic[_T], util.TypingOnly):
+ """Represent an ORM mapped attribute for typing purposes.
+
+ This class represents the complete descriptor interface for any class
+ attribute that will have been :term:`instrumented` by the ORM
+ :class:`_orm.Mapper` class. Provides appropriate information to type
+ checkers such as pylance and mypy so that ORM-mapped attributes
+ are correctly typed.
+
+ .. tip::
+
+ The :class:`_orm.Mapped` class represents attributes that are handled
+ directly by the :class:`_orm.Mapper` class. It does not include other
+ Python descriptor classes that are provided as extensions, including
+ :ref:`hybrids_toplevel` and the :ref:`associationproxy_toplevel`.
+ While these systems still make use of ORM-specific superclasses
+ and structures, they are not :term:`instrumented` by the
+ :class:`_orm.Mapper` and instead provide their own functionality
+ when they are accessed on a class.
+
+ .. versionadded:: 1.4
+
+
+ """
+
+ __slots__ = ()
+
+ if typing.TYPE_CHECKING:
+
+ @overload
+ def __get__(
+ self, instance: None, owner: Any
+ ) -> "InstrumentedAttribute[_T]":
+ ...
+
+ @overload
+ def __get__(self, instance: object, owner: Any) -> _T:
+ ...
+
+ def __get__(
+ self, instance: object, owner: Any
+ ) -> Union["InstrumentedAttribute[_T]", _T]:
+ ...
+
+ @classmethod
+ def _empty_constructor(cls, arg1: Any) -> "SQLORMOperations[_T]":
+ ...
+
+ @overload
+ def __set__(self, instance: Any, value: _T) -> None:
+ ...
+
+ @overload
+ def __set__(self, instance: Any, value: SQLCoreOperations) -> None:
+ ...
+
+ def __set__(self, instance, value):
+ ...
+
+ def __delete__(self, instance: Any):
+ ...
+
+
+class _MappedAttribute(Mapped[_T], TypingOnly):
"""Mixin for attributes which should be replaced by mapper-assigned
attributes.
diff --git a/lib/sqlalchemy/orm/decl_api.py b/lib/sqlalchemy/orm/decl_api.py
index 99b2e9b6f..00c5574fa 100644
--- a/lib/sqlalchemy/orm/decl_api.py
+++ b/lib/sqlalchemy/orm/decl_api.py
@@ -7,6 +7,13 @@
"""Public API functions and helpers for declarative."""
import itertools
import re
+import typing
+from typing import Any
+from typing import Callable
+from typing import ClassVar
+from typing import Optional
+from typing import TypeVar
+from typing import Union
import weakref
from . import attributes
@@ -15,7 +22,9 @@ from . import exc as orm_exc
from . import instrumentation
from . import interfaces
from . import mapperlib
+from .attributes import InstrumentedAttribute
from .base import _inspect_mapped_class
+from .base import Mapped
from .decl_base import _add_attribute
from .decl_base import _as_declarative
from .decl_base import _declarative_constructor
@@ -23,13 +32,18 @@ from .decl_base import _DeferredMapperConfig
from .decl_base import _del_attribute
from .decl_base import _mapper
from .descriptor_props import SynonymProperty as _orm_synonym
+from .mapper import Mapper
from .. import exc
from .. import inspection
from .. import util
+from ..sql.elements import SQLCoreOperations
from ..sql.schema import MetaData
+from ..sql.selectable import FromClause
from ..util import hybridmethod
from ..util import hybridproperty
+_T = TypeVar("_T", bound=Any)
+
def has_inherited_table(cls):
"""Given a class, return True if any of the classes it inherits from has a
@@ -50,11 +64,21 @@ def has_inherited_table(cls):
return False
-class DeclarativeMeta(type):
- # DeclarativeMeta could be replaced by __subclass_init__()
- # except for the class-level __setattr__() and __delattr__ hooks,
- # which are still very important.
+class DeclarativeAttributeIntercept(type):
+ """Metaclass that may be used in conjunction with the
+ :class:`_orm.DeclarativeBase` class to support addition of class
+ attributes dynamically.
+
+ """
+
+ def __setattr__(cls, key, value):
+ _add_attribute(cls, key, value)
+
+ def __delattr__(cls, key):
+ _del_attribute(cls, key)
+
+class DeclarativeMeta(type):
def __init__(cls, classname, bases, dict_, **kw):
# early-consume registry from the initial declarative base,
# assign privately to not conflict with subclass attributes named
@@ -121,7 +145,7 @@ def synonym_for(name, map_column=False):
return decorate
-class declared_attr(interfaces._MappedAttribute, property):
+class declared_attr(interfaces._MappedAttribute[_T]):
"""Mark a class-level method as representing the definition of
a mapped property or special declarative member name.
@@ -204,39 +228,52 @@ class declared_attr(interfaces._MappedAttribute, property):
""" # noqa E501
- def __init__(self, fget, cascading=False):
- super(declared_attr, self).__init__(fget)
- self.__doc__ = fget.__doc__
+ if typing.TYPE_CHECKING:
+
+ def __set__(self, instance, value):
+ ...
+
+ def __delete__(self, instance: Any):
+ ...
+
+ def __init__(
+ self,
+ fn: Callable[..., Union[Mapped[_T], SQLCoreOperations[_T]]],
+ cascading=False,
+ ):
+ self.fget = fn
self._cascading = cascading
+ self.__doc__ = fn.__doc__
- def __get__(desc, self, cls):
+ def __get__(self, instance, owner) -> InstrumentedAttribute[_T]:
# 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.
+ cls = owner
manager = attributes.manager_of_class(cls)
if manager is None:
- if not re.match(r"^__.+__$", desc.fget.__name__):
+ if not re.match(r"^__.+__$", self.fget.__name__):
# if there is no manager at all, then this class hasn't been
# run through declarative or mapper() at all, emit a warning.
util.warn(
"Unmanaged access of declarative attribute %s from "
- "non-mapped class %s" % (desc.fget.__name__, cls.__name__)
+ "non-mapped class %s" % (self.fget.__name__, cls.__name__)
)
- return desc.fget(cls)
+ return self.fget(cls)
elif manager.is_mapped:
# the class is mapped, which means we're outside of the declarative
# scan setup, just run the function.
- return desc.fget(cls)
+ return self.fget(cls)
# here, we are inside of the declarative scan. use the registry
# that is tracking the values of these attributes.
declarative_scan = manager.declarative_scan
reg = declarative_scan.declared_attr_reg
- if desc in reg:
- return reg[desc]
+ if self in reg:
+ return reg[self]
else:
- reg[desc] = obj = desc.fget(cls)
+ reg[self] = obj = self.fget(cls)
return obj
@hybridmethod
@@ -361,6 +398,115 @@ def declarative_mixin(cls):
return cls
+def _setup_declarative_base(cls):
+ if "metadata" in cls.__dict__:
+ metadata = cls.metadata
+ else:
+ metadata = None
+
+ reg = cls.__dict__.get("registry", None)
+ if reg is not None:
+ if not isinstance(reg, registry):
+ raise exc.InvalidRequestError(
+ "Declarative base class has a 'registry' attribute that is "
+ "not an instance of sqlalchemy.orm.registry()"
+ )
+ else:
+ reg = registry(metadata=metadata)
+ cls.registry = reg
+
+ cls._sa_registry = reg
+
+ if "metadata" not in cls.__dict__:
+ cls.metadata = cls.registry.metadata
+
+
+class DeclarativeBaseNoMeta:
+ """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["registry"]
+ _sa_registry: ClassVar["registry"]
+ metadata: ClassVar[MetaData]
+ __mapper__: ClassVar[Mapper]
+ __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 DeclarativeBase(metaclass=DeclarativeAttributeIntercept):
+ """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::
+
+
+ from sqlalchemy.orm import DeclarativeBase
+
+ class Base(DeclarativeBase):
+ pass
+
+
+ The above ``Base`` class is now usable as the base for new declarative
+ mappings. The superclass makes use of the ``__init_subclass__()``
+ method to set up new classes and metaclasses aren't used.
+
+ .. versionadded:: 2.0
+
+ """
+
+ registry: ClassVar["registry"]
+ _sa_registry: ClassVar["registry"]
+ metadata: ClassVar[MetaData]
+ __mapper__: ClassVar[Mapper]
+ __table__: Optional[FromClause]
+
+ if typing.TYPE_CHECKING:
+
+ def __init__(self, **kw: Any):
+ ...
+
+ def __init_subclass__(cls) -> None:
+ if DeclarativeBase in cls.__bases__:
+ _setup_declarative_base(cls)
+ else:
+ cls._sa_registry.map_declaratively(cls)
+
+
+def add_mapped_attribute(target, key, attr):
+ """Add a new mapped attribute to an ORM mapped class.
+
+ E.g.::
+
+ add_mapped_attribute(User, "addresses", relationship(Address))
+
+ This may be used for ORM mappings that aren't using a declarative
+ metaclass that intercepts attribute set operations.
+
+ .. versionadded:: 2.0
+
+
+ """
+ _add_attribute(target, key, attr)
+
+
def declarative_base(
metadata=None,
mapper=None,
@@ -369,7 +515,7 @@ def declarative_base(
constructor=_declarative_constructor,
class_registry=None,
metaclass=DeclarativeMeta,
-):
+) -> Any:
r"""Construct a base class for declarative class definitions.
The new base class will be given a metaclass that produces
@@ -1010,7 +1156,9 @@ def as_declarative(**kw):
).as_declarative_base(**kw)
-@inspection._inspects(DeclarativeMeta)
+@inspection._inspects(
+ DeclarativeMeta, DeclarativeBase, DeclarativeAttributeIntercept
+)
def _inspect_decl_meta(cls):
mp = _inspect_mapped_class(cls)
if mp is None:
diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py
index 80fce86d0..5e67b64cd 100644
--- a/lib/sqlalchemy/orm/descriptor_props.py
+++ b/lib/sqlalchemy/orm/descriptor_props.py
@@ -28,6 +28,7 @@ from ..sql import expression
from ..sql import operators
_T = TypeVar("_T", bound=Any)
+_PT = TypeVar("_PT", bound=Any)
class DescriptorProperty(MapperProperty[_T]):
@@ -362,7 +363,7 @@ class CompositeProperty(DescriptorProperty[_T]):
return proc
- class Comparator(PropComparator):
+ class Comparator(PropComparator[_PT]):
"""Produce boolean, comparison, and other operators for
:class:`.CompositeProperty` attributes.
@@ -448,7 +449,7 @@ class CompositeProperty(DescriptorProperty[_T]):
return str(self.parent.class_.__name__) + "." + self.key
-class ConcreteInheritedProperty(DescriptorProperty):
+class ConcreteInheritedProperty(DescriptorProperty[_T]):
"""A 'do nothing' :class:`.MapperProperty` that disables
an attribute on a concrete subclass that is only present
on the inherited mapper, not the concrete classes' mapper.
@@ -501,7 +502,7 @@ class ConcreteInheritedProperty(DescriptorProperty):
self.descriptor = NoninheritedConcreteProp()
-class SynonymProperty(DescriptorProperty):
+class SynonymProperty(DescriptorProperty[_T]):
def __init__(
self,
name,
diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py
index df265db57..08189a1b7 100644
--- a/lib/sqlalchemy/orm/interfaces.py
+++ b/lib/sqlalchemy/orm/interfaces.py
@@ -17,7 +17,9 @@ are exposed when inspecting mappings.
"""
import collections
+import typing
from typing import Any
+from typing import cast
from typing import TypeVar
from . import exc as orm_exc
@@ -32,6 +34,7 @@ from .base import MANYTOMANY
from .base import MANYTOONE
from .base import NOT_EXTENSION
from .base import ONETOMANY
+from .base import SQLORMOperations
from .. import inspect
from .. import inspection
from .. import util
@@ -307,8 +310,10 @@ class MapperProperty(
@inspection._self_inspects
-class PropComparator(operators.ColumnOperators):
- r"""Defines SQL operators for :class:`.MapperProperty` objects.
+class PropComparator(
+ SQLORMOperations[_T], operators.ColumnOperators[SQLORMOperations]
+):
+ r"""Defines SQL operations for ORM mapped attributes.
SQLAlchemy allows for operators to
be redefined at both the Core and ORM level. :class:`.PropComparator`
@@ -316,12 +321,6 @@ class PropComparator(operators.ColumnOperators):
including those of :class:`.ColumnProperty`,
:class:`.RelationshipProperty`, and :class:`.CompositeProperty`.
- .. note:: With the advent of Hybrid properties introduced in SQLAlchemy
- 0.7, as well as Core-level operator redefinition in
- SQLAlchemy 0.8, the use case for user-defined :class:`.PropComparator`
- instances is extremely rare. See :ref:`hybrids_toplevel` as well
- as :ref:`types_operators`.
-
User-defined subclasses of :class:`.PropComparator` may be created. The
built-in Python comparison and math operator methods, such as
:meth:`.operators.ColumnOperators.__eq__`,
@@ -463,18 +462,34 @@ class PropComparator(operators.ColumnOperators):
return self.property.info
@staticmethod
- def any_op(a, b, **kwargs):
+ def _any_op(a, b, **kwargs):
return a.any(b, **kwargs)
@staticmethod
- def has_op(a, b, **kwargs):
- return a.has(b, **kwargs)
+ def _has_op(left, other, **kwargs):
+ return left.has(other, **kwargs)
@staticmethod
- def of_type_op(a, class_):
+ def _of_type_op(a, class_):
return a.of_type(class_)
- def of_type(self, class_):
+ any_op = cast(operators.OperatorType, _any_op)
+ has_op = cast(operators.OperatorType, _has_op)
+ of_type_op = cast(operators.OperatorType, _of_type_op)
+
+ if typing.TYPE_CHECKING:
+
+ def operate(
+ self, op: operators.OperatorType, *other: Any, **kwargs: Any
+ ) -> "SQLORMOperations":
+ ...
+
+ def reverse_operate(
+ self, op: operators.OperatorType, other: Any, **kwargs: Any
+ ) -> "SQLORMOperations":
+ ...
+
+ def of_type(self, class_) -> "SQLORMOperations[_T]":
r"""Redefine this object in terms of a polymorphic subclass,
:func:`_orm.with_polymorphic` construct, or :func:`_orm.aliased`
construct.
@@ -500,7 +515,7 @@ class PropComparator(operators.ColumnOperators):
return self.operate(PropComparator.of_type_op, class_)
- def and_(self, *criteria):
+ def and_(self, *criteria) -> "SQLORMOperations[_T]":
"""Add additional criteria to the ON clause that's represented by this
relationship attribute.
@@ -528,7 +543,7 @@ class PropComparator(operators.ColumnOperators):
"""
return self.operate(operators.and_, *criteria)
- def any(self, criterion=None, **kwargs):
+ def any(self, criterion=None, **kwargs) -> "SQLORMOperations[_T]":
r"""Return true if this collection contains any member that meets the
given criterion.
@@ -546,7 +561,7 @@ class PropComparator(operators.ColumnOperators):
return self.operate(PropComparator.any_op, criterion, **kwargs)
- def has(self, criterion=None, **kwargs):
+ def has(self, criterion=None, **kwargs) -> "SQLORMOperations[_T]":
r"""Return true if this element references a member which meets the
given criterion.
@@ -565,7 +580,7 @@ class PropComparator(operators.ColumnOperators):
return self.operate(PropComparator.has_op, criterion, **kwargs)
-class StrategizedProperty(MapperProperty):
+class StrategizedProperty(MapperProperty[_T]):
"""A MapperProperty which uses selectable strategies to affect
loading behavior.
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index 8ee26315e..c4aac5a38 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -13,7 +13,6 @@ mapped attributes.
"""
from typing import Any
-from typing import Generic
from typing import TypeVar
from . import attributes
@@ -32,6 +31,7 @@ from ..sql import coercions
from ..sql import roles
_T = TypeVar("_T", bound=Any)
+_PT = TypeVar("_PT", bound=Any)
__all__ = [
"ColumnProperty",
@@ -43,7 +43,7 @@ __all__ = [
@log.class_logger
-class ColumnProperty(StrategizedProperty, Generic[_T]):
+class ColumnProperty(StrategizedProperty[_T]):
"""Describes an object attribute that corresponds to a table column.
Public constructor is the :func:`_orm.column_property` function.
@@ -90,6 +90,7 @@ class ColumnProperty(StrategizedProperty, Generic[_T]):
)
for c in columns
]
+ self.parent = self.key = None
self.group = kwargs.pop("group", None)
self.deferred = kwargs.pop("deferred", False)
self.raiseload = kwargs.pop("raiseload", False)
@@ -253,7 +254,7 @@ class ColumnProperty(StrategizedProperty, Generic[_T]):
dest_dict, [self.key], no_loader=True
)
- class Comparator(util.MemoizedSlots, PropComparator):
+ class Comparator(util.MemoizedSlots, PropComparator[_PT]):
"""Produce boolean, comparison, and other operators for
:class:`.ColumnProperty` attributes.
@@ -361,4 +362,6 @@ class ColumnProperty(StrategizedProperty, Generic[_T]):
return op(col._bind_param(op, other), col, **kwargs)
def __str__(self):
+ if not self.parent or not self.key:
+ return object.__repr__(self)
return str(self.parent.class_.__name__) + "." + self.key
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index 330d45430..c5ea07051 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -15,8 +15,8 @@ and `secondaryjoin` aspects of :func:`_orm.relationship`.
"""
import collections
import re
+from typing import Any
from typing import Callable
-from typing import Generic
from typing import Type
from typing import TypeVar
from typing import Union
@@ -53,7 +53,8 @@ from ..sql.util import join_condition
from ..sql.util import selectables_overlap
from ..sql.util import visit_binary_product
-_RC = TypeVar("_RC")
+_T = TypeVar("_T", bound=Any)
+_PT = TypeVar("_PT", bound=Any)
def remote(expr):
@@ -96,7 +97,7 @@ def foreign(expr):
@log.class_logger
-class RelationshipProperty(StrategizedProperty, Generic[_RC]):
+class RelationshipProperty(StrategizedProperty[_T]):
"""Describes an object property that holds a single item or list
of items that correspond to a related database table.
@@ -125,7 +126,7 @@ class RelationshipProperty(StrategizedProperty, Generic[_RC]):
def __init__(
self,
- argument: Union[str, Type[_RC], Callable[[], Type[_RC]]],
+ argument: Union[str, Type[_T], Callable[[], Type[_T]]],
secondary=None,
primaryjoin=None,
secondaryjoin=None,
@@ -285,7 +286,7 @@ class RelationshipProperty(StrategizedProperty, Generic[_RC]):
doc=self.doc,
)
- class Comparator(PropComparator):
+ class Comparator(PropComparator[_PT]):
"""Produce boolean, comparison, and other operators for
:class:`.RelationshipProperty` attributes.
@@ -861,6 +862,8 @@ class RelationshipProperty(StrategizedProperty, Generic[_RC]):
self.prop.parent._check_configure()
return self.prop
+ comparator: Comparator[_T]
+
def _with_parent(self, instance, alias_secondary=True, from_entity=None):
assert instance is not None
adapt_source = None