summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm/decl_api.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/orm/decl_api.py')
-rw-r--r--lib/sqlalchemy/orm/decl_api.py160
1 files changed, 137 insertions, 23 deletions
diff --git a/lib/sqlalchemy/orm/decl_api.py b/lib/sqlalchemy/orm/decl_api.py
index 59fabb9b6..5ac9966dd 100644
--- a/lib/sqlalchemy/orm/decl_api.py
+++ b/lib/sqlalchemy/orm/decl_api.py
@@ -11,7 +11,9 @@ import typing
from typing import Any
from typing import Callable
from typing import ClassVar
+from typing import Mapping
from typing import Optional
+from typing import Type
from typing import TypeVar
from typing import Union
import weakref
@@ -31,7 +33,7 @@ from .decl_base import _declarative_constructor
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 .descriptor_props import Synonym as _orm_synonym
from .mapper import Mapper
from .. import exc
from .. import inspection
@@ -39,14 +41,18 @@ from .. import util
from ..sql.elements import SQLCoreOperations
from ..sql.schema import MetaData
from ..sql.selectable import FromClause
+from ..sql.type_api import TypeEngine
from ..util import hybridmethod
from ..util import hybridproperty
+from ..util import typing as compat_typing
if typing.TYPE_CHECKING:
from .state import InstanceState # noqa
_T = TypeVar("_T", bound=Any)
+_TypeAnnotationMapType = Mapping[Type, Union[Type[TypeEngine], TypeEngine]]
+
def has_inherited_table(cls):
"""Given a class, return True if any of the classes it inherits from has a
@@ -67,8 +73,22 @@ def has_inherited_table(cls):
return False
+class _DynamicAttributesType(type):
+ def __setattr__(cls, key, value):
+ if "__mapper__" in cls.__dict__:
+ _add_attribute(cls, key, value)
+ else:
+ type.__setattr__(cls, key, value)
+
+ def __delattr__(cls, key):
+ if "__mapper__" in cls.__dict__:
+ _del_attribute(cls, key)
+ else:
+ type.__delattr__(cls, key)
+
+
class DeclarativeAttributeIntercept(
- type, inspection.Inspectable["Mapper[Any]"]
+ _DynamicAttributesType, inspection.Inspectable["Mapper[Any]"]
):
"""Metaclass that may be used in conjunction with the
:class:`_orm.DeclarativeBase` class to support addition of class
@@ -76,15 +96,16 @@ class DeclarativeAttributeIntercept(
"""
- def __setattr__(cls, key, value):
- _add_attribute(cls, key, value)
-
- def __delattr__(cls, key):
- _del_attribute(cls, key)
+class DeclarativeMeta(
+ _DynamicAttributesType, inspection.Inspectable["Mapper[Any]"]
+):
+ metadata: MetaData
+ registry: "RegistryType"
-class DeclarativeMeta(type, inspection.Inspectable["Mapper[Any]"]):
- def __init__(cls, classname, bases, dict_, **kw):
+ def __init__(
+ cls, classname: Any, bases: Any, dict_: Any, **kw: Any
+ ) -> None:
# early-consume registry from the initial declarative base,
# assign privately to not conflict with subclass attributes named
# "registry"
@@ -103,12 +124,6 @@ class DeclarativeMeta(type, inspection.Inspectable["Mapper[Any]"]):
_as_declarative(reg, cls, dict_)
type.__init__(cls, classname, bases, dict_)
- def __setattr__(cls, key, value):
- _add_attribute(cls, key, value)
-
- def __delattr__(cls, key):
- _del_attribute(cls, key)
-
def synonym_for(name, map_column=False):
"""Decorator that produces an :func:`_orm.synonym`
@@ -250,6 +265,9 @@ class declared_attr(interfaces._MappedAttribute[_T]):
self._cascading = cascading
self.__doc__ = fn.__doc__
+ def _collect_return_annotation(self) -> Optional[Type[Any]]:
+ return util.get_annotations(self.fget).get("return")
+
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.
@@ -409,6 +427,11 @@ def _setup_declarative_base(cls):
else:
metadata = None
+ if "type_annotation_map" in cls.__dict__:
+ type_annotation_map = cls.__dict__["type_annotation_map"]
+ else:
+ type_annotation_map = None
+
reg = cls.__dict__.get("registry", None)
if reg is not None:
if not isinstance(reg, registry):
@@ -416,8 +439,18 @@ def _setup_declarative_base(cls):
"Declarative base class has a 'registry' attribute that is "
"not an instance of sqlalchemy.orm.registry()"
)
+ elif type_annotation_map is not None:
+ raise exc.InvalidRequestError(
+ "Declarative base class has both a 'registry' attribute and a "
+ "type_annotation_map entry. Per-base type_annotation_maps "
+ "are not supported. Please apply the type_annotation_map "
+ "to this registry directly."
+ )
+
else:
- reg = registry(metadata=metadata)
+ reg = registry(
+ metadata=metadata, type_annotation_map=type_annotation_map
+ )
cls.registry = reg
cls._sa_registry = reg
@@ -476,6 +509,44 @@ class DeclarativeBase(
mappings. The superclass makes use of the ``__init_subclass__()``
method to set up new classes and metaclasses aren't used.
+ When first used, the :class:`_orm.DeclarativeBase` class instantiates a new
+ :class:`_orm.registry` to be used with the base, assuming one was not
+ provided explicitly. The :class:`_orm.DeclarativeBase` class supports
+ class-level attributes which act as parameters for the construction of this
+ registry; such as to indicate a specific :class:`_schema.MetaData`
+ collection as well as a specific value for
+ :paramref:`_orm.registry.type_annotation_map`::
+
+ from typing import Annotation
+
+ from sqlalchemy import BigInteger
+ from sqlalchemy import MetaData
+ from sqlalchemy import String
+ from sqlalchemy.orm import DeclarativeBase
+
+ bigint = Annotation(int, "bigint")
+ my_metadata = MetaData()
+
+ class Base(DeclarativeBase):
+ metadata = my_metadata
+ type_annotation_map = {
+ str: String().with_variant(String(255), "mysql", "mariadb"),
+ bigint: BigInteger()
+ }
+
+ Class-level attributes which may be specified include:
+
+ :param metadata: optional :class:`_schema.MetaData` collection.
+ If a :class:`_orm.registry` is constructed automatically, this
+ :class:`_schema.MetaData` collection will be used to construct it.
+ Otherwise, the local :class:`_schema.MetaData` collection will supercede
+ that used by an existing :class:`_orm.registry` passed using the
+ :paramref:`_orm.DeclarativeBase.registry` parameter.
+ :param type_annotation_map: optional type annotation map that will be
+ passed to the :class:`_orm.registry` as
+ :paramref:`_orm.registry.type_annotation_map`.
+ :param registry: supply a pre-existing :class:`_orm.registry` directly.
+
.. versionadded:: 2.0
"""
@@ -516,12 +587,13 @@ def add_mapped_attribute(target, key, attr):
def declarative_base(
- metadata=None,
+ metadata: Optional[MetaData] = None,
mapper=None,
cls=object,
name="Base",
- constructor=_declarative_constructor,
- class_registry=None,
+ class_registry: Optional[clsregistry._ClsRegistryType] = None,
+ type_annotation_map: Optional[_TypeAnnotationMapType] = None,
+ constructor: Callable[..., None] = _declarative_constructor,
metaclass=DeclarativeMeta,
) -> Any:
r"""Construct a base class for declarative class definitions.
@@ -593,6 +665,14 @@ def declarative_base(
to share the same registry of class names for simplified
inter-base relationships.
+ :param type_annotation_map: optional dictionary of Python types to
+ SQLAlchemy :class:`_types.TypeEngine` classes or instances. This
+ is used exclusively by the :class:`_orm.MappedColumn` construct
+ to produce column types based on annotations within the
+ :class:`_orm.Mapped` type.
+
+ .. versionadded:: 2.0
+
:param metaclass:
Defaults to :class:`.DeclarativeMeta`. A metaclass or __metaclass__
compatible callable to use as the meta type of the generated
@@ -608,6 +688,7 @@ def declarative_base(
metadata=metadata,
class_registry=class_registry,
constructor=constructor,
+ type_annotation_map=type_annotation_map,
).generate_base(
mapper=mapper,
cls=cls,
@@ -651,9 +732,10 @@ class registry:
def __init__(
self,
- metadata=None,
- class_registry=None,
- constructor=_declarative_constructor,
+ metadata: Optional[MetaData] = None,
+ class_registry: Optional[clsregistry._ClsRegistryType] = None,
+ type_annotation_map: Optional[_TypeAnnotationMapType] = None,
+ constructor: Callable[..., None] = _declarative_constructor,
):
r"""Construct a new :class:`_orm.registry`
@@ -679,6 +761,14 @@ class registry:
to share the same registry of class names for simplified
inter-base relationships.
+ :param type_annotation_map: optional dictionary of Python types to
+ SQLAlchemy :class:`_types.TypeEngine` classes or instances. This
+ is used exclusively by the :class:`_orm.MappedColumn` construct
+ to produce column types based on annotations within the
+ :class:`_orm.Mapped` type.
+
+ .. versionadded:: 2.0
+
"""
lcl_metadata = metadata or MetaData()
@@ -690,7 +780,9 @@ class registry:
self._non_primary_mappers = weakref.WeakKeyDictionary()
self.metadata = lcl_metadata
self.constructor = constructor
-
+ self.type_annotation_map = {}
+ if type_annotation_map is not None:
+ self.update_type_annotation_map(type_annotation_map)
self._dependents = set()
self._dependencies = set()
@@ -699,6 +791,25 @@ class registry:
with mapperlib._CONFIGURE_MUTEX:
mapperlib._mapper_registries[self] = True
+ def update_type_annotation_map(
+ self,
+ type_annotation_map: Mapping[
+ Type, Union[Type[TypeEngine], TypeEngine]
+ ],
+ ) -> None:
+ """update the :paramref:`_orm.registry.type_annotation_map` with new
+ values."""
+
+ self.type_annotation_map.update(
+ {
+ sub_type: sqltype
+ for typ, sqltype in type_annotation_map.items()
+ for sub_type in compat_typing.expand_unions(
+ typ, include_union=True, discard_none=True
+ )
+ }
+ )
+
@property
def mappers(self):
"""read only collection of all :class:`_orm.Mapper` objects."""
@@ -1131,6 +1242,9 @@ class registry:
return _mapper(self, class_, local_table, kw)
+RegistryType = registry
+
+
def as_declarative(**kw):
"""
Class decorator which will adapt a given class into a