diff options
Diffstat (limited to 'lib/sqlalchemy/orm/decl_api.py')
-rw-r--r-- | lib/sqlalchemy/orm/decl_api.py | 160 |
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 |