diff options
Diffstat (limited to 'lib/sqlalchemy/orm')
| -rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 6 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/_orm_constructors.py | 242 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 103 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/base.py | 91 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/decl_api.py | 184 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/descriptor_props.py | 7 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/interfaces.py | 49 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/properties.py | 9 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 13 |
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 |
