diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/sqlalchemy/exc.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/automap.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/declarative/__init__.py | 53 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/declarative/api.py | 823 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/declarative/extensions.py | 455 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 10 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/clsregistry.py (renamed from lib/sqlalchemy/ext/declarative/clsregistry.py) | 182 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/decl_api.py | 753 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/decl_base.py (renamed from lib/sqlalchemy/ext/declarative/base.py) | 425 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/instrumentation.py | 128 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 107 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 31 | ||||
-rw-r--r-- | lib/sqlalchemy/testing/entities.py | 10 | ||||
-rw-r--r-- | lib/sqlalchemy/testing/fixtures.py | 13 | ||||
-rw-r--r-- | lib/sqlalchemy/util/__init__.py | 1 | ||||
-rw-r--r-- | lib/sqlalchemy/util/deprecations.py | 21 |
16 files changed, 1852 insertions, 1166 deletions
diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index a17bb5cec..1a38fc756 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -644,6 +644,10 @@ class RemovedIn20Warning(SADeprecationWarning): "Indicates the version that started raising this deprecation warning" +class MovedIn20Warning(RemovedIn20Warning): + """subtype of RemovedIn20Warning to indicate an API that moved only.""" + + class SAPendingDeprecationWarning(PendingDeprecationWarning): """A similar warning as :class:`_exc.SADeprecationWarning`, this warning is not used in modern versions of SQLAlchemy. diff --git a/lib/sqlalchemy/ext/automap.py b/lib/sqlalchemy/ext/automap.py index 4ae3a415e..2dc7d54de 100644 --- a/lib/sqlalchemy/ext/automap.py +++ b/lib/sqlalchemy/ext/automap.py @@ -531,12 +531,12 @@ we've declared are in an un-mapped state. """ # noqa from .declarative import declarative_base as _declarative_base -from .declarative.base import _DeferredMapperConfig from .. import util from ..orm import backref from ..orm import exc as orm_exc from ..orm import interfaces from ..orm import relationship +from ..orm.decl_base import _DeferredMapperConfig from ..orm.mapper import _CONFIGURE_MUTEX from ..schema import ForeignKeyConstraint from ..sql import and_ diff --git a/lib/sqlalchemy/ext/declarative/__init__.py b/lib/sqlalchemy/ext/declarative/__init__.py index 6dc4d23c8..8b38945b2 100644 --- a/lib/sqlalchemy/ext/declarative/__init__.py +++ b/lib/sqlalchemy/ext/declarative/__init__.py @@ -5,16 +5,49 @@ # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from .api import AbstractConcreteBase -from .api import as_declarative -from .api import ConcreteBase -from .api import declarative_base -from .api import DeclarativeMeta -from .api import declared_attr -from .api import DeferredReflection -from .api import has_inherited_table -from .api import instrument_declarative -from .api import synonym_for +from .extensions import AbstractConcreteBase +from .extensions import ConcreteBase +from .extensions import DeferredReflection +from .extensions import instrument_declarative +from ... import util +from ...orm.decl_api import as_declarative as _as_declarative +from ...orm.decl_api import declarative_base as _declarative_base +from ...orm.decl_api import DeclarativeMeta +from ...orm.decl_api import declared_attr +from ...orm.decl_api import has_inherited_table as _has_inherited_table +from ...orm.decl_api import synonym_for as _synonym_for + + +@util.moved_20( + "The ``declarative_base()`` function is now available as " + ":func:`sqlalchemy.orm.declarative_base`." +) +def declarative_base(*arg, **kw): + return _declarative_base(*arg, **kw) + + +@util.moved_20( + "The ``as_declarative()`` function is now available as " + ":func:`sqlalchemy.orm.as_declarative`" +) +def as_declarative(*arg, **kw): + return _as_declarative(*arg, **kw) + + +@util.moved_20( + "The ``has_inherited_table()`` function is now available as " + ":func:`sqlalchemy.orm.has_inherited_table`." +) +def has_inherited_table(*arg, **kw): + return _has_inherited_table(*arg, **kw) + + +@util.moved_20( + "The ``synonym_for()`` function is now available as " + ":func:`sqlalchemy.orm.synonym_for`" +) +def synonym_for(*arg, **kw): + return _synonym_for(*arg, **kw) __all__ = [ diff --git a/lib/sqlalchemy/ext/declarative/api.py b/lib/sqlalchemy/ext/declarative/api.py deleted file mode 100644 index 076fa1120..000000000 --- a/lib/sqlalchemy/ext/declarative/api.py +++ /dev/null @@ -1,823 +0,0 @@ -# ext/declarative/api.py -# Copyright (C) 2005-2020 the SQLAlchemy authors and contributors -# <see AUTHORS file> -# -# This module is part of SQLAlchemy and is released under -# the MIT License: http://www.opensource.org/licenses/mit-license.php -"""Public API functions and helpers for declarative.""" - - -import re -import weakref - -from .base import _add_attribute -from .base import _as_declarative -from .base import _declarative_constructor -from .base import _DeferredMapperConfig -from .base import _del_attribute -from .base import _get_immediate_cls_attr -from .clsregistry import _class_resolver -from ... import exc -from ... import inspection -from ... import util -from ...orm import attributes -from ...orm import exc as orm_exc -from ...orm import interfaces -from ...orm import relationships -from ...orm import synonym as _orm_synonym -from ...orm.base import _inspect_mapped_class -from ...orm.base import _mapper_or_none -from ...orm.util import polymorphic_union -from ...schema import MetaData -from ...schema import Table -from ...util import hybridmethod -from ...util import hybridproperty -from ...util import OrderedDict - - -def instrument_declarative(cls, registry, metadata): - """Given a class, configure the class declaratively, - using the given registry, which can be any dictionary, and - MetaData object. - - """ - if "_decl_class_registry" in cls.__dict__: - raise exc.InvalidRequestError( - "Class %r already has been " "instrumented declaratively" % cls - ) - cls._decl_class_registry = registry - cls.metadata = metadata - _as_declarative(cls, cls.__name__, cls.__dict__) - - -def has_inherited_table(cls): - """Given a class, return True if any of the classes it inherits from has a - mapped table, otherwise return False. - - This is used in declarative mixins to build attributes that behave - differently for the base class vs. a subclass in an inheritance - hierarchy. - - .. seealso:: - - :ref:`decl_mixin_inheritance` - - """ - for class_ in cls.__mro__[1:]: - if getattr(class_, "__table__", None) is not None: - return True - return False - - -class DeclarativeMeta(type): - def __init__(cls, classname, bases, dict_, **kw): - if "_decl_class_registry" not in cls.__dict__: - _as_declarative(cls, classname, 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` - attribute in conjunction - with a Python descriptor. - - The function being decorated is passed to :func:`_orm.synonym` as the - :paramref:`.orm.synonym.descriptor` parameter:: - - class MyClass(Base): - __tablename__ = 'my_table' - - id = Column(Integer, primary_key=True) - _job_status = Column("job_status", String(50)) - - @synonym_for("job_status") - @property - def job_status(self): - return "Status: %s" % self._job_status - - The :ref:`hybrid properties <mapper_hybrids>` feature of SQLAlchemy - is typically preferred instead of synonyms, which is a more legacy - feature. - - .. seealso:: - - :ref:`synonyms` - Overview of synonyms - - :func:`_orm.synonym` - the mapper-level function - - :ref:`mapper_hybrids` - The Hybrid Attribute extension provides an - updated approach to augmenting attribute behavior more flexibly than - can be achieved with synonyms. - - """ - - def decorate(fn): - return _orm_synonym(name, map_column=map_column, descriptor=fn) - - return decorate - - -class declared_attr(interfaces._MappedAttribute, property): - """Mark a class-level method as representing the definition of - a mapped property or special declarative member name. - - @declared_attr turns the attribute into a scalar-like - property that can be invoked from the uninstantiated class. - Declarative treats attributes specifically marked with - @declared_attr as returning a construct that is specific - to mapping or declarative table configuration. The name - of the attribute is that of what the non-dynamic version - of the attribute would be. - - @declared_attr is more often than not applicable to mixins, - to define relationships that are to be applied to different - implementors of the class:: - - class ProvidesUser(object): - "A mixin that adds a 'user' relationship to classes." - - @declared_attr - def user(self): - return relationship("User") - - It also can 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__} - - """ - - def __init__(self, fget, cascading=False): - super(declared_attr, self).__init__(fget) - self.__doc__ = fget.__doc__ - self._cascading = cascading - - def __get__(desc, self, cls): - reg = cls.__dict__.get("_sa_declared_attr_reg", None) - if reg is None: - if ( - not re.match(r"^__.+__$", desc.fget.__name__) - and attributes.manager_of_class(cls) is None - ): - util.warn( - "Unmanaged access of declarative attribute %s from " - "non-mapped class %s" % (desc.fget.__name__, cls.__name__) - ) - return desc.fget(cls) - elif desc in reg: - return reg[desc] - else: - reg[desc] = obj = desc.fget(cls) - return obj - - @hybridmethod - def _stateful(cls, **kw): - return _stateful_declared_attr(**kw) - - @hybridproperty - def cascading(cls): - """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(object): - @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 MySubClass(MyClass): - "" - # ... - - 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``. - - .. seealso:: - - :ref:`declarative_inheritance` - - :ref:`mixin_inheritance_columns` - - - """ - return cls._stateful(cascading=True) - - -class _stateful_declared_attr(declared_attr): - def __init__(self, **kw): - self.kw = kw - - def _stateful(self, **kw): - new_kw = self.kw.copy() - new_kw.update(kw) - return _stateful_declared_attr(**new_kw) - - def __call__(self, fn): - return declared_attr(fn, **self.kw) - - -def declarative_base( - bind=None, - metadata=None, - mapper=None, - cls=object, - name="Base", - constructor=_declarative_constructor, - class_registry=None, - metaclass=DeclarativeMeta, -): - r"""Construct a base class for declarative class definitions. - - The new base class will be given a metaclass that produces - appropriate :class:`~sqlalchemy.schema.Table` objects and makes - the appropriate :func:`~sqlalchemy.orm.mapper` calls based on the - information provided declaratively in the class and any subclasses - of the class. - - :param bind: An optional - :class:`~sqlalchemy.engine.Connectable`, will be assigned - the ``bind`` attribute on the :class:`~sqlalchemy.schema.MetaData` - instance. - - :param metadata: - An optional :class:`~sqlalchemy.schema.MetaData` instance. All - :class:`~sqlalchemy.schema.Table` objects implicitly declared by - subclasses of the base will share this MetaData. A MetaData instance - will be created if none is provided. The - :class:`~sqlalchemy.schema.MetaData` instance will be available via the - `metadata` attribute of the generated declarative base class. - - :param mapper: - An optional callable, defaults to :func:`~sqlalchemy.orm.mapper`. Will - be used to map subclasses to their Tables. - - :param cls: - Defaults to :class:`object`. A type to use as the base for the generated - declarative base class. May be a class or tuple of classes. - - :param name: - Defaults to ``Base``. The display name for the generated - class. Customizing this is not required, but can improve clarity in - tracebacks and debugging. - - :param constructor: - Defaults to - :func:`~sqlalchemy.ext.declarative.base._declarative_constructor`, an - __init__ implementation that assigns \**kwargs for declared - fields and relationships to an instance. If ``None`` is supplied, - no __init__ will be provided and construction will fall back to - cls.__init__ by way of the normal Python semantics. - - :param class_registry: optional dictionary that will serve as the - registry of class names-> mapped classes when string names - are used to identify classes inside of :func:`_orm.relationship` - and others. Allows two or more declarative base classes - to share the same registry of class names for simplified - inter-base relationships. - - :param metaclass: - Defaults to :class:`.DeclarativeMeta`. A metaclass or __metaclass__ - compatible callable to use as the meta type of the generated - declarative base class. - - .. versionchanged:: 1.1 if :paramref:`.declarative_base.cls` is a - single class (rather than a tuple), the constructed base class will - inherit its docstring. - - .. seealso:: - - :func:`.as_declarative` - - """ - lcl_metadata = metadata or MetaData() - if bind: - lcl_metadata.bind = bind - - if class_registry is None: - class_registry = weakref.WeakValueDictionary() - - bases = not isinstance(cls, tuple) and (cls,) or cls - class_dict = dict( - _decl_class_registry=class_registry, metadata=lcl_metadata - ) - - if isinstance(cls, type): - class_dict["__doc__"] = cls.__doc__ - - if constructor: - class_dict["__init__"] = constructor - if mapper: - class_dict["__mapper_cls__"] = mapper - - return metaclass(name, bases, class_dict) - - -def as_declarative(**kw): - """ - Class decorator for :func:`.declarative_base`. - - Provides a syntactical shortcut to the ``cls`` argument - sent to :func:`.declarative_base`, allowing the base class - to be converted in-place to a "declarative" base:: - - from sqlalchemy.ext.declarative import as_declarative - - @as_declarative() - class Base(object): - @declared_attr - def __tablename__(cls): - return cls.__name__.lower() - id = Column(Integer, primary_key=True) - - class MyMappedClass(Base): - # ... - - All keyword arguments passed to :func:`.as_declarative` are passed - along to :func:`.declarative_base`. - - .. seealso:: - - :func:`.declarative_base` - - """ - - def decorate(cls): - kw["cls"] = cls - kw["name"] = cls.__name__ - return declarative_base(**kw) - - return decorate - - -class ConcreteBase(object): - """A helper class for 'concrete' declarative mappings. - - :class:`.ConcreteBase` will use the :func:`.polymorphic_union` - function automatically, against all tables mapped as a subclass - to this class. The function is called via the - ``__declare_last__()`` function, which is essentially - a hook for the :meth:`.after_configured` event. - - :class:`.ConcreteBase` produces a mapped - table for the class itself. Compare to :class:`.AbstractConcreteBase`, - which does not. - - Example:: - - from sqlalchemy.ext.declarative import ConcreteBase - - class Employee(ConcreteBase, Base): - __tablename__ = 'employee' - employee_id = Column(Integer, primary_key=True) - name = Column(String(50)) - __mapper_args__ = { - 'polymorphic_identity':'employee', - 'concrete':True} - - class Manager(Employee): - __tablename__ = 'manager' - employee_id = Column(Integer, primary_key=True) - name = Column(String(50)) - manager_data = Column(String(40)) - __mapper_args__ = { - 'polymorphic_identity':'manager', - 'concrete':True} - - - The name of the discriminator column used by :func:`.polymorphic_union` - defaults to the name ``type``. To suit the use case of a mapping where an - actual column in a mapped table is already named ``type``, the - discriminator name can be configured by setting the - ``_concrete_discriminator_name`` attribute:: - - class Employee(ConcreteBase, Base): - _concrete_discriminator_name = '_concrete_discriminator' - - .. versionadded:: 1.3.19 Added the ``_concrete_discriminator_name`` - attribute to :class:`_declarative.ConcreteBase` so that the - virtual discriminator column name can be customized. - - .. seealso:: - - :class:`.AbstractConcreteBase` - - :ref:`concrete_inheritance` - - - """ - - @classmethod - def _create_polymorphic_union(cls, mappers, discriminator_name): - return polymorphic_union( - OrderedDict( - (mp.polymorphic_identity, mp.local_table) for mp in mappers - ), - discriminator_name, - "pjoin", - ) - - @classmethod - def __declare_first__(cls): - m = cls.__mapper__ - if m.with_polymorphic: - return - - discriminator_name = ( - _get_immediate_cls_attr(cls, "_concrete_discriminator_name") - or "type" - ) - - mappers = list(m.self_and_descendants) - pjoin = cls._create_polymorphic_union(mappers, discriminator_name) - m._set_with_polymorphic(("*", pjoin)) - m._set_polymorphic_on(pjoin.c[discriminator_name]) - - -class AbstractConcreteBase(ConcreteBase): - """A helper class for 'concrete' declarative mappings. - - :class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union` - function automatically, against all tables mapped as a subclass - to this class. The function is called via the - ``__declare_last__()`` function, which is essentially - a hook for the :meth:`.after_configured` event. - - :class:`.AbstractConcreteBase` does produce a mapped class - for the base class, however it is not persisted to any table; it - is instead mapped directly to the "polymorphic" selectable directly - and is only used for selecting. Compare to :class:`.ConcreteBase`, - which does create a persisted table for the base class. - - .. note:: - - The :class:`.AbstractConcreteBase` class does not intend to set up the - mapping for the base class until all the subclasses have been defined, - as it needs to create a mapping against a selectable that will include - all subclass tables. In order to achieve this, it waits for the - **mapper configuration event** to occur, at which point it scans - through all the configured subclasses and sets up a mapping that will - query against all subclasses at once. - - While this event is normally invoked automatically, in the case of - :class:`.AbstractConcreteBase`, it may be necessary to invoke it - explicitly after **all** subclass mappings are defined, if the first - operation is to be a query against this base class. To do so, invoke - :func:`.configure_mappers` once all the desired classes have been - configured:: - - from sqlalchemy.orm import configure_mappers - - configure_mappers() - - .. seealso:: - - :func:`_orm.configure_mappers` - - - Example:: - - from sqlalchemy.ext.declarative import AbstractConcreteBase - - class Employee(AbstractConcreteBase, Base): - pass - - class Manager(Employee): - __tablename__ = 'manager' - employee_id = Column(Integer, primary_key=True) - name = Column(String(50)) - manager_data = Column(String(40)) - - __mapper_args__ = { - 'polymorphic_identity':'manager', - 'concrete':True} - - configure_mappers() - - The abstract base class is handled by declarative in a special way; - at class configuration time, it behaves like a declarative mixin - or an ``__abstract__`` base class. Once classes are configured - and mappings are produced, it then gets mapped itself, but - after all of its descendants. This is a very unique system of mapping - not found in any other SQLAlchemy system. - - Using this approach, we can specify columns and properties - that will take place on mapped subclasses, in the way that - we normally do as in :ref:`declarative_mixins`:: - - class Company(Base): - __tablename__ = 'company' - id = Column(Integer, primary_key=True) - - class Employee(AbstractConcreteBase, Base): - employee_id = Column(Integer, primary_key=True) - - @declared_attr - def company_id(cls): - return Column(ForeignKey('company.id')) - - @declared_attr - def company(cls): - return relationship("Company") - - class Manager(Employee): - __tablename__ = 'manager' - - name = Column(String(50)) - manager_data = Column(String(40)) - - __mapper_args__ = { - 'polymorphic_identity':'manager', - 'concrete':True} - - configure_mappers() - - When we make use of our mappings however, both ``Manager`` and - ``Employee`` will have an independently usable ``.company`` attribute:: - - session.query(Employee).filter(Employee.company.has(id=5)) - - .. versionchanged:: 1.0.0 - The mechanics of :class:`.AbstractConcreteBase` - have been reworked to support relationships established directly - on the abstract base, without any special configurational steps. - - .. seealso:: - - :class:`.ConcreteBase` - - :ref:`concrete_inheritance` - - """ - - __no_table__ = True - - @classmethod - def __declare_first__(cls): - cls._sa_decl_prepare_nocascade() - - @classmethod - def _sa_decl_prepare_nocascade(cls): - if getattr(cls, "__mapper__", None): - return - - to_map = _DeferredMapperConfig.config_for_cls(cls) - - # can't rely on 'self_and_descendants' here - # since technically an immediate subclass - # might not be mapped, but a subclass - # may be. - mappers = [] - stack = list(cls.__subclasses__()) - while stack: - klass = stack.pop() - stack.extend(klass.__subclasses__()) - mn = _mapper_or_none(klass) - if mn is not None: - mappers.append(mn) - - discriminator_name = ( - _get_immediate_cls_attr(cls, "_concrete_discriminator_name") - or "type" - ) - pjoin = cls._create_polymorphic_union(mappers, discriminator_name) - - # For columns that were declared on the class, these - # are normally ignored with the "__no_table__" mapping, - # unless they have a different attribute key vs. col name - # and are in the properties argument. - # In that case, ensure we update the properties entry - # to the correct column from the pjoin target table. - declared_cols = set(to_map.declared_columns) - for k, v in list(to_map.properties.items()): - if v in declared_cols: - to_map.properties[k] = pjoin.c[v.key] - - to_map.local_table = pjoin - - m_args = to_map.mapper_args_fn or dict - - def mapper_args(): - args = m_args() - args["polymorphic_on"] = pjoin.c[discriminator_name] - return args - - to_map.mapper_args_fn = mapper_args - - m = to_map.map() - - for scls in cls.__subclasses__(): - sm = _mapper_or_none(scls) - if sm and sm.concrete and cls in scls.__bases__: - sm._set_concrete_base(m) - - @classmethod - def _sa_raise_deferred_config(cls): - raise orm_exc.UnmappedClassError( - cls, - msg="Class %s is a subclass of AbstractConcreteBase and " - "has a mapping pending until all subclasses are defined. " - "Call the sqlalchemy.orm.configure_mappers() function after " - "all subclasses have been defined to " - "complete the mapping of this class." - % orm_exc._safe_cls_name(cls), - ) - - -class DeferredReflection(object): - """A helper class for construction of mappings based on - a deferred reflection step. - - Normally, declarative can be used with reflection by - setting a :class:`_schema.Table` object using autoload=True - as the ``__table__`` attribute on a declarative class. - The caveat is that the :class:`_schema.Table` must be fully - reflected, or at the very least have a primary key column, - at the point at which a normal declarative mapping is - constructed, meaning the :class:`_engine.Engine` must be available - at class declaration time. - - The :class:`.DeferredReflection` mixin moves the construction - of mappers to be at a later point, after a specific - method is called which first reflects all :class:`_schema.Table` - objects created so far. Classes can define it as such:: - - from sqlalchemy.ext.declarative import declarative_base - from sqlalchemy.ext.declarative import DeferredReflection - Base = declarative_base() - - class MyClass(DeferredReflection, Base): - __tablename__ = 'mytable' - - Above, ``MyClass`` is not yet mapped. After a series of - classes have been defined in the above fashion, all tables - can be reflected and mappings created using - :meth:`.prepare`:: - - engine = create_engine("someengine://...") - DeferredReflection.prepare(engine) - - The :class:`.DeferredReflection` mixin can be applied to individual - classes, used as the base for the declarative base itself, - or used in a custom abstract class. Using an abstract base - allows that only a subset of classes to be prepared for a - particular prepare step, which is necessary for applications - that use more than one engine. For example, if an application - has two engines, you might use two bases, and prepare each - separately, e.g.:: - - class ReflectedOne(DeferredReflection, Base): - __abstract__ = True - - class ReflectedTwo(DeferredReflection, Base): - __abstract__ = True - - class MyClass(ReflectedOne): - __tablename__ = 'mytable' - - class MyOtherClass(ReflectedOne): - __tablename__ = 'myothertable' - - class YetAnotherClass(ReflectedTwo): - __tablename__ = 'yetanothertable' - - # ... etc. - - Above, the class hierarchies for ``ReflectedOne`` and - ``ReflectedTwo`` can be configured separately:: - - ReflectedOne.prepare(engine_one) - ReflectedTwo.prepare(engine_two) - - """ - - @classmethod - def prepare(cls, engine): - """Reflect all :class:`_schema.Table` objects for all current - :class:`.DeferredReflection` subclasses""" - - to_map = _DeferredMapperConfig.classes_for_base(cls) - for thingy in to_map: - cls._sa_decl_prepare(thingy.local_table, engine) - thingy.map() - mapper = thingy.cls.__mapper__ - metadata = mapper.class_.metadata - for rel in mapper._props.values(): - if ( - isinstance(rel, relationships.RelationshipProperty) - and rel.secondary is not None - ): - if isinstance(rel.secondary, Table): - cls._reflect_table(rel.secondary, engine) - elif isinstance(rel.secondary, _class_resolver): - rel.secondary._resolvers += ( - cls._sa_deferred_table_resolver(engine, metadata), - ) - - @classmethod - def _sa_deferred_table_resolver(cls, engine, metadata): - def _resolve(key): - t1 = Table(key, metadata) - cls._reflect_table(t1, engine) - return t1 - - return _resolve - - @classmethod - def _sa_decl_prepare(cls, local_table, engine): - # autoload Table, which is already - # present in the metadata. This - # will fill in db-loaded columns - # into the existing Table object. - if local_table is not None: - cls._reflect_table(local_table, engine) - - @classmethod - def _sa_raise_deferred_config(cls): - raise orm_exc.UnmappedClassError( - cls, - msg="Class %s is a subclass of DeferredReflection. " - "Mappings are not produced until the .prepare() " - "method is called on the class hierarchy." - % orm_exc._safe_cls_name(cls), - ) - - @classmethod - def _reflect_table(cls, table, engine): - Table( - table.name, - table.metadata, - extend_existing=True, - autoload_replace=False, - autoload=True, - autoload_with=engine, - schema=table.schema, - ) - - -@inspection._inspects(DeclarativeMeta) -def _inspect_decl_meta(cls): - mp = _inspect_mapped_class(cls) - if mp is None: - if _DeferredMapperConfig.has_cls(cls): - _DeferredMapperConfig.raise_unmapped_for_cls(cls) - raise orm_exc.UnmappedClassError( - cls, - msg="Class %s has a deferred mapping on it. It is not yet " - "usable as a mapped class." % orm_exc._safe_cls_name(cls), - ) - return mp diff --git a/lib/sqlalchemy/ext/declarative/extensions.py b/lib/sqlalchemy/ext/declarative/extensions.py new file mode 100644 index 000000000..0b9a6f7ed --- /dev/null +++ b/lib/sqlalchemy/ext/declarative/extensions.py @@ -0,0 +1,455 @@ +# ext/declarative/extensions.py +# Copyright (C) 2005-2020 the SQLAlchemy authors and contributors +# <see AUTHORS file> +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +"""Public API functions and helpers for declarative.""" + + +from ... import inspection +from ... import util +from ...orm import exc as orm_exc +from ...orm import registry +from ...orm import relationships +from ...orm.base import _mapper_or_none +from ...orm.clsregistry import _resolver +from ...orm.decl_base import _DeferredMapperConfig +from ...orm.decl_base import _get_immediate_cls_attr +from ...orm.util import polymorphic_union +from ...schema import Table +from ...util import OrderedDict + + +@util.deprecated( + "2.0", + "the instrument_declarative function is deprecated " + "and will be removed in SQLAlhcemy 2.0. Please use " + ":meth:`_orm.registry.map_declaratively", +) +def instrument_declarative(cls, cls_registry, metadata): + """Given a class, configure the class declaratively, + using the given registry, which can be any dictionary, and + MetaData object. + + """ + return registry( + metadata=metadata, class_registry=cls_registry + ).instrument_declarative(cls) + + +class ConcreteBase(object): + """A helper class for 'concrete' declarative mappings. + + :class:`.ConcreteBase` will use the :func:`.polymorphic_union` + function automatically, against all tables mapped as a subclass + to this class. The function is called via the + ``__declare_last__()`` function, which is essentially + a hook for the :meth:`.after_configured` event. + + :class:`.ConcreteBase` produces a mapped + table for the class itself. Compare to :class:`.AbstractConcreteBase`, + which does not. + + Example:: + + from sqlalchemy.ext.declarative import ConcreteBase + + class Employee(ConcreteBase, Base): + __tablename__ = 'employee' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + __mapper_args__ = { + 'polymorphic_identity':'employee', + 'concrete':True} + + class Manager(Employee): + __tablename__ = 'manager' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(40)) + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + + The name of the discriminator column used by :func:`.polymorphic_union` + defaults to the name ``type``. To suit the use case of a mapping where an + actual column in a mapped table is already named ``type``, the + discriminator name can be configured by setting the + ``_concrete_discriminator_name`` attribute:: + + class Employee(ConcreteBase, Base): + _concrete_discriminator_name = '_concrete_discriminator' + + .. versionadded:: 1.3.19 Added the ``_concrete_discriminator_name`` + attribute to :class:`_declarative.ConcreteBase` so that the + virtual discriminator column name can be customized. + + .. seealso:: + + :class:`.AbstractConcreteBase` + + :ref:`concrete_inheritance` + + + """ + + @classmethod + def _create_polymorphic_union(cls, mappers, discriminator_name): + return polymorphic_union( + OrderedDict( + (mp.polymorphic_identity, mp.local_table) for mp in mappers + ), + discriminator_name, + "pjoin", + ) + + @classmethod + def __declare_first__(cls): + m = cls.__mapper__ + if m.with_polymorphic: + return + + discriminator_name = ( + _get_immediate_cls_attr(cls, "_concrete_discriminator_name") + or "type" + ) + + mappers = list(m.self_and_descendants) + pjoin = cls._create_polymorphic_union(mappers, discriminator_name) + m._set_with_polymorphic(("*", pjoin)) + m._set_polymorphic_on(pjoin.c[discriminator_name]) + + +class AbstractConcreteBase(ConcreteBase): + """A helper class for 'concrete' declarative mappings. + + :class:`.AbstractConcreteBase` will use the :func:`.polymorphic_union` + function automatically, against all tables mapped as a subclass + to this class. The function is called via the + ``__declare_last__()`` function, which is essentially + a hook for the :meth:`.after_configured` event. + + :class:`.AbstractConcreteBase` does produce a mapped class + for the base class, however it is not persisted to any table; it + is instead mapped directly to the "polymorphic" selectable directly + and is only used for selecting. Compare to :class:`.ConcreteBase`, + which does create a persisted table for the base class. + + .. note:: + + The :class:`.AbstractConcreteBase` class does not intend to set up the + mapping for the base class until all the subclasses have been defined, + as it needs to create a mapping against a selectable that will include + all subclass tables. In order to achieve this, it waits for the + **mapper configuration event** to occur, at which point it scans + through all the configured subclasses and sets up a mapping that will + query against all subclasses at once. + + While this event is normally invoked automatically, in the case of + :class:`.AbstractConcreteBase`, it may be necessary to invoke it + explicitly after **all** subclass mappings are defined, if the first + operation is to be a query against this base class. To do so, invoke + :func:`.configure_mappers` once all the desired classes have been + configured:: + + from sqlalchemy.orm import configure_mappers + + configure_mappers() + + .. seealso:: + + :func:`_orm.configure_mappers` + + + Example:: + + from sqlalchemy.ext.declarative import AbstractConcreteBase + + class Employee(AbstractConcreteBase, Base): + pass + + class Manager(Employee): + __tablename__ = 'manager' + employee_id = Column(Integer, primary_key=True) + name = Column(String(50)) + manager_data = Column(String(40)) + + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + configure_mappers() + + The abstract base class is handled by declarative in a special way; + at class configuration time, it behaves like a declarative mixin + or an ``__abstract__`` base class. Once classes are configured + and mappings are produced, it then gets mapped itself, but + after all of its descendants. This is a very unique system of mapping + not found in any other SQLAlchemy system. + + Using this approach, we can specify columns and properties + that will take place on mapped subclasses, in the way that + we normally do as in :ref:`declarative_mixins`:: + + class Company(Base): + __tablename__ = 'company' + id = Column(Integer, primary_key=True) + + class Employee(AbstractConcreteBase, Base): + employee_id = Column(Integer, primary_key=True) + + @declared_attr + def company_id(cls): + return Column(ForeignKey('company.id')) + + @declared_attr + def company(cls): + return relationship("Company") + + class Manager(Employee): + __tablename__ = 'manager' + + name = Column(String(50)) + manager_data = Column(String(40)) + + __mapper_args__ = { + 'polymorphic_identity':'manager', + 'concrete':True} + + configure_mappers() + + When we make use of our mappings however, both ``Manager`` and + ``Employee`` will have an independently usable ``.company`` attribute:: + + session.query(Employee).filter(Employee.company.has(id=5)) + + .. versionchanged:: 1.0.0 - The mechanics of :class:`.AbstractConcreteBase` + have been reworked to support relationships established directly + on the abstract base, without any special configurational steps. + + .. seealso:: + + :class:`.ConcreteBase` + + :ref:`concrete_inheritance` + + """ + + __no_table__ = True + + @classmethod + def __declare_first__(cls): + cls._sa_decl_prepare_nocascade() + + @classmethod + def _sa_decl_prepare_nocascade(cls): + if getattr(cls, "__mapper__", None): + return + + to_map = _DeferredMapperConfig.config_for_cls(cls) + + # can't rely on 'self_and_descendants' here + # since technically an immediate subclass + # might not be mapped, but a subclass + # may be. + mappers = [] + stack = list(cls.__subclasses__()) + while stack: + klass = stack.pop() + stack.extend(klass.__subclasses__()) + mn = _mapper_or_none(klass) + if mn is not None: + mappers.append(mn) + + discriminator_name = ( + _get_immediate_cls_attr(cls, "_concrete_discriminator_name") + or "type" + ) + pjoin = cls._create_polymorphic_union(mappers, discriminator_name) + + # For columns that were declared on the class, these + # are normally ignored with the "__no_table__" mapping, + # unless they have a different attribute key vs. col name + # and are in the properties argument. + # In that case, ensure we update the properties entry + # to the correct column from the pjoin target table. + declared_cols = set(to_map.declared_columns) + for k, v in list(to_map.properties.items()): + if v in declared_cols: + to_map.properties[k] = pjoin.c[v.key] + + to_map.local_table = pjoin + + m_args = to_map.mapper_args_fn or dict + + def mapper_args(): + args = m_args() + args["polymorphic_on"] = pjoin.c[discriminator_name] + return args + + to_map.mapper_args_fn = mapper_args + + m = to_map.map() + + for scls in cls.__subclasses__(): + sm = _mapper_or_none(scls) + if sm and sm.concrete and cls in scls.__bases__: + sm._set_concrete_base(m) + + @classmethod + def _sa_raise_deferred_config(cls): + raise orm_exc.UnmappedClassError( + cls, + msg="Class %s is a subclass of AbstractConcreteBase and " + "has a mapping pending until all subclasses are defined. " + "Call the sqlalchemy.orm.configure_mappers() function after " + "all subclasses have been defined to " + "complete the mapping of this class." + % orm_exc._safe_cls_name(cls), + ) + + +class DeferredReflection(object): + """A helper class for construction of mappings based on + a deferred reflection step. + + Normally, declarative can be used with reflection by + setting a :class:`_schema.Table` object using autoload=True + as the ``__table__`` attribute on a declarative class. + The caveat is that the :class:`_schema.Table` must be fully + reflected, or at the very least have a primary key column, + at the point at which a normal declarative mapping is + constructed, meaning the :class:`_engine.Engine` must be available + at class declaration time. + + The :class:`.DeferredReflection` mixin moves the construction + of mappers to be at a later point, after a specific + method is called which first reflects all :class:`_schema.Table` + objects created so far. Classes can define it as such:: + + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy.ext.declarative import DeferredReflection + Base = declarative_base() + + class MyClass(DeferredReflection, Base): + __tablename__ = 'mytable' + + Above, ``MyClass`` is not yet mapped. After a series of + classes have been defined in the above fashion, all tables + can be reflected and mappings created using + :meth:`.prepare`:: + + engine = create_engine("someengine://...") + DeferredReflection.prepare(engine) + + The :class:`.DeferredReflection` mixin can be applied to individual + classes, used as the base for the declarative base itself, + or used in a custom abstract class. Using an abstract base + allows that only a subset of classes to be prepared for a + particular prepare step, which is necessary for applications + that use more than one engine. For example, if an application + has two engines, you might use two bases, and prepare each + separately, e.g.:: + + class ReflectedOne(DeferredReflection, Base): + __abstract__ = True + + class ReflectedTwo(DeferredReflection, Base): + __abstract__ = True + + class MyClass(ReflectedOne): + __tablename__ = 'mytable' + + class MyOtherClass(ReflectedOne): + __tablename__ = 'myothertable' + + class YetAnotherClass(ReflectedTwo): + __tablename__ = 'yetanothertable' + + # ... etc. + + Above, the class hierarchies for ``ReflectedOne`` and + ``ReflectedTwo`` can be configured separately:: + + ReflectedOne.prepare(engine_one) + ReflectedTwo.prepare(engine_two) + + """ + + @classmethod + def prepare(cls, engine): + """Reflect all :class:`_schema.Table` objects for all current + :class:`.DeferredReflection` subclasses""" + + to_map = _DeferredMapperConfig.classes_for_base(cls) + + with inspection.inspect(engine)._inspection_context() as insp: + for thingy in to_map: + cls._sa_decl_prepare(thingy.local_table, insp) + thingy.map() + mapper = thingy.cls.__mapper__ + metadata = mapper.class_.metadata + for rel in mapper._props.values(): + if ( + isinstance(rel, relationships.RelationshipProperty) + and rel.secondary is not None + ): + if isinstance(rel.secondary, Table): + cls._reflect_table(rel.secondary, insp) + elif isinstance(rel.secondary, str): + + _, resolve_arg = _resolver(rel.parent.class_, rel) + + rel.secondary = resolve_arg(rel.secondary) + rel.secondary._resolvers += ( + cls._sa_deferred_table_resolver( + insp, metadata + ), + ) + + # contoversy! do we resolve it here? or leave + # it deferred? I think doing it here is necessary + # so the connection does not leak. + rel.secondary = rel.secondary() + + @classmethod + def _sa_deferred_table_resolver(cls, inspector, metadata): + def _resolve(key): + t1 = Table(key, metadata) + cls._reflect_table(t1, inspector) + return t1 + + return _resolve + + @classmethod + def _sa_decl_prepare(cls, local_table, inspector): + # autoload Table, which is already + # present in the metadata. This + # will fill in db-loaded columns + # into the existing Table object. + if local_table is not None: + cls._reflect_table(local_table, inspector) + + @classmethod + def _sa_raise_deferred_config(cls): + raise orm_exc.UnmappedClassError( + cls, + msg="Class %s is a subclass of DeferredReflection. " + "Mappings are not produced until the .prepare() " + "method is called on the class hierarchy." + % orm_exc._safe_cls_name(cls), + ) + + @classmethod + def _reflect_table(cls, table, inspector): + Table( + table.name, + table.metadata, + extend_existing=True, + autoload_replace=False, + autoload=True, + autoload_with=inspector, + schema=table.schema, + ) diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 199ae11e5..13626fb21 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -16,11 +16,21 @@ documentation for an overview of how this module is used. from . import exc # noqa from . import mapper as mapperlib # noqa from . import strategy_options +from .decl_api import as_declarative # noqa +from .decl_api import declarative_base # noqa +from .decl_api import declared_attr # noqa +from .decl_api import has_inherited_table # noqa +from .decl_api import registry # noqa +from .decl_api import synonym_for # noqa from .descriptor_props import CompositeProperty # noqa from .descriptor_props import SynonymProperty # noqa from .interfaces import EXT_CONTINUE # noqa from .interfaces import EXT_SKIP # noqa from .interfaces import EXT_STOP # noqa +from .interfaces import MANYTOMANY # noqa +from .interfaces import MANYTOONE # noqa +from .interfaces import MapperProperty # noqa +from .interfaces import ONETOMANY # noqa from .interfaces import PropComparator # noqa from .mapper import _mapper_registry from .mapper import class_mapper # noqa diff --git a/lib/sqlalchemy/ext/declarative/clsregistry.py b/lib/sqlalchemy/orm/clsregistry.py index 51af6f1b4..07b8afbf9 100644 --- a/lib/sqlalchemy/ext/declarative/clsregistry.py +++ b/lib/sqlalchemy/orm/clsregistry.py @@ -12,16 +12,15 @@ This system allows specification of classes and expressions used in """ import weakref -from ... import exc -from ... import inspection -from ... import util -from ...orm import class_mapper -from ...orm import ColumnProperty -from ...orm import interfaces -from ...orm import RelationshipProperty -from ...orm import SynonymProperty -from ...schema import _get_table_key - +from . import attributes +from . import interfaces +from .descriptor_props import SynonymProperty +from .properties import ColumnProperty +from .util import class_mapper +from .. import exc +from .. import inspection +from .. import util +from ..sql.schema import _get_table_key # strong references to registries which we place in # the _decl_class_registry, which is usually weak referencing. @@ -30,25 +29,25 @@ from ...schema import _get_table_key _registries = set() -def add_class(classname, cls): +def add_class(classname, cls, decl_class_registry): """Add a class to the _decl_class_registry associated with the given declarative class. """ - if classname in cls._decl_class_registry: + if classname in decl_class_registry: # class already exists. - existing = cls._decl_class_registry[classname] + existing = decl_class_registry[classname] if not isinstance(existing, _MultipleClassMarker): - existing = cls._decl_class_registry[ - classname - ] = _MultipleClassMarker([cls, existing]) + existing = decl_class_registry[classname] = _MultipleClassMarker( + [cls, existing] + ) else: - cls._decl_class_registry[classname] = cls + decl_class_registry[classname] = cls try: - root_module = cls._decl_class_registry["_sa_module_registry"] + root_module = decl_class_registry["_sa_module_registry"] except KeyError: - cls._decl_class_registry[ + decl_class_registry[ "_sa_module_registry" ] = root_module = _ModuleMarker("_sa_module_registry", None) @@ -70,6 +69,55 @@ def add_class(classname, cls): module.add_class(classname, cls) +def remove_class(classname, cls, decl_class_registry): + if classname in decl_class_registry: + existing = decl_class_registry[classname] + if isinstance(existing, _MultipleClassMarker): + existing.remove_item(cls) + else: + del decl_class_registry[classname] + + try: + root_module = decl_class_registry["_sa_module_registry"] + except KeyError: + return + + tokens = cls.__module__.split(".") + + while tokens: + token = tokens.pop(0) + module = root_module.get_module(token) + for token in tokens: + module = module.get_module(token) + module.remove_class(classname, cls) + + +def _key_is_empty(key, decl_class_registry, test): + """test if a key is empty of a certain object. + + used for unit tests against the registry to see if garbage collection + is working. + + "test" is a callable that will be passed an object should return True + if the given object is the one we were looking for. + + We can't pass the actual object itself b.c. this is for testing garbage + collection; the caller will have to have removed references to the + object itself. + + """ + if key not in decl_class_registry: + return True + + thing = decl_class_registry[key] + if isinstance(thing, _MultipleClassMarker): + for sub_thing in thing.contents: + if test(sub_thing): + return False + else: + return not test(thing) + + class _MultipleClassMarker(object): """refers to multiple classes of the same name within _decl_class_registry. @@ -85,6 +133,9 @@ class _MultipleClassMarker(object): ) _registries.add(self) + def remove_item(self, cls): + self._remove_item(weakref.ref(cls)) + def __iter__(self): return (ref() for ref in self.contents) @@ -104,7 +155,7 @@ class _MultipleClassMarker(object): return cls def _remove_item(self, ref): - self.contents.remove(ref) + self.contents.discard(ref) if not self.contents: _registries.discard(self) if self.on_remove: @@ -182,6 +233,11 @@ class _ModuleMarker(object): [cls], on_remove=lambda: self._remove_item(name) ) + def remove_class(self, name, cls): + if name in self.contents: + existing = self.contents[name] + existing.remove_item(cls) + class _ModNS(object): __slots__ = ("__parent",) @@ -259,27 +315,35 @@ def _determine_container(key, value): class _class_resolver(object): + __slots__ = "cls", "prop", "arg", "fallback", "_dict", "_resolvers" + def __init__(self, cls, prop, fallback, arg): self.cls = cls self.prop = prop - self.arg = self._declarative_arg = arg + self.arg = arg self.fallback = fallback self._dict = util.PopulateDict(self._access_cls) self._resolvers = () def _access_cls(self, key): cls = self.cls - if key in cls._decl_class_registry: - return _determine_container(key, cls._decl_class_registry[key]) - elif key in cls.metadata.tables: - return cls.metadata.tables[key] - elif key in cls.metadata._schemas: + + manager = attributes.manager_of_class(cls) + decl_base = manager.registry + decl_class_registry = decl_base._class_registry + metadata = decl_base.metadata + + if key in decl_class_registry: + return _determine_container(key, decl_class_registry[key]) + elif key in metadata.tables: + return metadata.tables[key] + elif key in metadata._schemas: return _GetTable(key, cls.metadata) elif ( - "_sa_module_registry" in cls._decl_class_registry - and key in cls._decl_class_registry["_sa_module_registry"] + "_sa_module_registry" in decl_class_registry + and key in decl_class_registry["_sa_module_registry"] ): - registry = cls._decl_class_registry["_sa_module_registry"] + registry = decl_class_registry["_sa_module_registry"] return registry.resolve_attr(key) elif self._resolvers: for resolv in self._resolvers: @@ -333,57 +397,25 @@ class _class_resolver(object): self._raise_for_name(n.args[0], n) -def _resolver(cls, prop): - import sqlalchemy - from sqlalchemy.orm import foreign, remote +_fallback_dict = None - fallback = sqlalchemy.__dict__.copy() - fallback.update({"foreign": foreign, "remote": remote}) - def resolve_arg(arg): - return _class_resolver(cls, prop, fallback, arg) +def _resolver(cls, prop): - def resolve_name(arg): - return _class_resolver(cls, prop, fallback, arg)._resolve_name + global _fallback_dict - return resolve_name, resolve_arg + if _fallback_dict is None: + import sqlalchemy + from sqlalchemy.orm import foreign, remote + _fallback_dict = util.immutabledict(sqlalchemy.__dict__).union( + {"foreign": foreign, "remote": remote} + ) -def _deferred_relationship(cls, prop): + def resolve_arg(arg): + return _class_resolver(cls, prop, _fallback_dict, arg) - if isinstance(prop, RelationshipProperty): - resolve_name, resolve_arg = _resolver(cls, prop) + def resolve_name(arg): + return _class_resolver(cls, prop, _fallback_dict, arg)._resolve_name - for attr in ( - "order_by", - "primaryjoin", - "secondaryjoin", - "secondary", - "_user_defined_foreign_keys", - "remote_side", - ): - v = getattr(prop, attr) - if isinstance(v, util.string_types): - setattr(prop, attr, resolve_arg(v)) - - for attr in ("argument",): - v = getattr(prop, attr) - if isinstance(v, util.string_types): - setattr(prop, attr, resolve_name(v)) - - if prop.backref and isinstance(prop.backref, tuple): - key, kwargs = prop.backref - for attr in ( - "primaryjoin", - "secondaryjoin", - "secondary", - "foreign_keys", - "remote_side", - "order_by", - ): - if attr in kwargs and isinstance( - kwargs[attr], util.string_types - ): - kwargs[attr] = resolve_arg(kwargs[attr]) - - return prop + return resolve_name, resolve_arg diff --git a/lib/sqlalchemy/orm/decl_api.py b/lib/sqlalchemy/orm/decl_api.py new file mode 100644 index 000000000..1df916e03 --- /dev/null +++ b/lib/sqlalchemy/orm/decl_api.py @@ -0,0 +1,753 @@ +# ext/declarative/api.py +# Copyright (C) 2005-2020 the SQLAlchemy authors and contributors +# <see AUTHORS file> +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +"""Public API functions and helpers for declarative.""" +from __future__ import absolute_import + +import re +import weakref + +from . import attributes +from . import clsregistry +from . import exc as orm_exc +from . import interfaces +from .base import _inspect_mapped_class +from .decl_base import _add_attribute +from .decl_base import _as_declarative +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 .. import inspection +from .. import util +from ..sql.schema import MetaData +from ..util import hybridmethod +from ..util import hybridproperty + +if util.TYPE_CHECKING: + from .mapper import Mapper + + +def has_inherited_table(cls): + """Given a class, return True if any of the classes it inherits from has a + mapped table, otherwise return False. + + This is used in declarative mixins to build attributes that behave + differently for the base class vs. a subclass in an inheritance + hierarchy. + + .. seealso:: + + :ref:`decl_mixin_inheritance` + + """ + for class_ in cls.__mro__[1:]: + if getattr(class_, "__table__", None) is not None: + return True + return False + + +class DeclarativeMeta(type): + def __init__(cls, classname, bases, dict_, **kw): + if not cls.__dict__.get("__abstract__", False): + _as_declarative(cls.registry, cls, 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` + attribute in conjunction with a Python descriptor. + + The function being decorated is passed to :func:`_orm.synonym` as the + :paramref:`.orm.synonym.descriptor` parameter:: + + class MyClass(Base): + __tablename__ = 'my_table' + + id = Column(Integer, primary_key=True) + _job_status = Column("job_status", String(50)) + + @synonym_for("job_status") + @property + def job_status(self): + return "Status: %s" % self._job_status + + The :ref:`hybrid properties <mapper_hybrids>` feature of SQLAlchemy + is typically preferred instead of synonyms, which is a more legacy + feature. + + .. seealso:: + + :ref:`synonyms` - Overview of synonyms + + :func:`_orm.synonym` - the mapper-level function + + :ref:`mapper_hybrids` - The Hybrid Attribute extension provides an + updated approach to augmenting attribute behavior more flexibly than + can be achieved with synonyms. + + """ + + def decorate(fn): + return _orm_synonym(name, map_column=map_column, descriptor=fn) + + return decorate + + +class declared_attr(interfaces._MappedAttribute, property): + """Mark a class-level method as representing the definition of + a mapped property or special declarative member name. + + @declared_attr turns the attribute into a scalar-like + property that can be invoked from the uninstantiated class. + Declarative treats attributes specifically marked with + @declared_attr as returning a construct that is specific + to mapping or declarative table configuration. The name + of the attribute is that of what the non-dynamic version + of the attribute would be. + + @declared_attr is more often than not applicable to mixins, + to define relationships that are to be applied to different + implementors of the class:: + + class ProvidesUser(object): + "A mixin that adds a 'user' relationship to classes." + + @declared_attr + def user(self): + return relationship("User") + + It also can 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__} + + """ + + def __init__(self, fget, cascading=False): + super(declared_attr, self).__init__(fget) + self.__doc__ = fget.__doc__ + self._cascading = cascading + + def __get__(desc, self, cls): + # 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. + manager = attributes.manager_of_class(cls) + if manager is None: + if not re.match(r"^__.+__$", desc.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__) + ) + return desc.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) + + # 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] + else: + reg[desc] = obj = desc.fget(cls) + return obj + + @hybridmethod + def _stateful(cls, **kw): + return _stateful_declared_attr(**kw) + + @hybridproperty + def cascading(cls): + """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(object): + @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 MySubClass(MyClass): + "" + # ... + + 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``. + + .. seealso:: + + :ref:`declarative_inheritance` + + :ref:`mixin_inheritance_columns` + + + """ + return cls._stateful(cascading=True) + + +class _stateful_declared_attr(declared_attr): + def __init__(self, **kw): + self.kw = kw + + def _stateful(self, **kw): + new_kw = self.kw.copy() + new_kw.update(kw) + return _stateful_declared_attr(**new_kw) + + def __call__(self, fn): + return declared_attr(fn, **self.kw) + + +def declarative_base( + bind=None, + metadata=None, + mapper=None, + cls=object, + name="Base", + constructor=_declarative_constructor, + class_registry=None, + metaclass=DeclarativeMeta, +): + r"""Construct a base class for declarative class definitions. + + The new base class will be given a metaclass that produces + appropriate :class:`~sqlalchemy.schema.Table` objects and makes + the appropriate :func:`~sqlalchemy.orm.mapper` calls based on the + information provided declaratively in the class and any subclasses + of the class. + + The :func:`_orm.declarative_base` function is a shorthand version + of using the :meth:`_orm.registry.generate_base` + method. That is, the following:: + + from sqlalchemy.orm import declarative_base + + Base = declarative_base() + + Is equvialent to:: + + from sqlalchemy.orm import registry + + mapper_registry = registry() + Base = mapper_registry.generate_base() + + See the docstring for :class:`_orm.registry` + and :meth:`_orm.registry.generate_base` + for more details. + + .. versionchanged:: 1.4 The :func:`_orm.declarative_base` + function is now a specialization of the more generic + :class:`_orm.registry` class. The function also moves to the + ``sqlalchemy.orm`` package from the ``declarative.ext`` package. + + + :param bind: An optional + :class:`~sqlalchemy.engine.Connectable`, will be assigned + the ``bind`` attribute on the :class:`~sqlalchemy.schema.MetaData` + instance. + + .. deprecated:: 1.4 The "bind" argument to declarative_base is + deprecated and will be removed in SQLAlchemy 2.0. + + :param metadata: + An optional :class:`~sqlalchemy.schema.MetaData` instance. All + :class:`~sqlalchemy.schema.Table` objects implicitly declared by + subclasses of the base will share this MetaData. A MetaData instance + will be created if none is provided. The + :class:`~sqlalchemy.schema.MetaData` instance will be available via the + ``metadata`` attribute of the generated declarative base class. + + :param mapper: + An optional callable, defaults to :func:`~sqlalchemy.orm.mapper`. Will + be used to map subclasses to their Tables. + + :param cls: + Defaults to :class:`object`. A type to use as the base for the generated + declarative base class. May be a class or tuple of classes. + + :param name: + Defaults to ``Base``. The display name for the generated + class. Customizing this is not required, but can improve clarity in + tracebacks and debugging. + + :param constructor: + Specify the implementation for the ``__init__`` function on a mapped + class that has no ``__init__`` of its own. Defaults to an + implementation that assigns \**kwargs for declared + fields and relationships to an instance. If ``None`` is supplied, + no __init__ will be provided and construction will fall back to + cls.__init__ by way of the normal Python semantics. + + :param class_registry: optional dictionary that will serve as the + registry of class names-> mapped classes when string names + are used to identify classes inside of :func:`_orm.relationship` + and others. Allows two or more declarative base classes + to share the same registry of class names for simplified + inter-base relationships. + + :param metaclass: + Defaults to :class:`.DeclarativeMeta`. A metaclass or __metaclass__ + compatible callable to use as the meta type of the generated + declarative base class. + + .. seealso:: + + :class:`_orm.registry` + + """ + + return registry( + bind=bind, + metadata=metadata, + class_registry=class_registry, + constructor=constructor, + ).generate_base(mapper=mapper, cls=cls, name=name, metaclass=metaclass,) + + +class registry(object): + """Generalized registry for mapping classes. + + The :class:`_orm.registry` serves as the basis for maintaining a collection + of mappings, and provides configurational hooks used to map classes. + + The three general kinds of mappings supported are Declarative Base, + Declarative Decorator, and Imperative Mapping. All of these mapping + styles may be used interchangeably: + + * :meth:`_orm.registry.generate_base` returns a new declarative base + class, and is the underlying implementation of the + :func:`_orm.declarative_base` function. + + * :meth:`_orm.registry.mapped` provides a class decorator that will + apply declarative mapping to a class without the use of a declarative + base class. + + * :meth:`_orm.registry.map_imperatively` will produce a + :class:`_orm.Mapper` for a class without scanning the class for + declarative class attributes. This method suits the use case historically + provided by the + :func:`_orm.mapper` classical mapping function. + + .. versionadded:: 1.4 + + .. seealso:: + + :ref:`orm_mapping_classes_toplevel` - overview of class mapping + styles. + + """ + + def __init__( + self, + bind=None, + metadata=None, + class_registry=None, + constructor=_declarative_constructor, + ): + r"""Construct a new :class:`_orm.registry` + + :param metadata: + An optional :class:`_schema.MetaData` instance. All + :class:`_schema.Table` objects generated using declarative + table mapping will make use of this :class:`_schema.MetaData` + collection. If this argument is left at its default of ``None``, + a blank :class:`_schema.MetaData` collection is created. + + :param constructor: + Specify the implementation for the ``__init__`` function on a mapped + class that has no ``__init__`` of its own. Defaults to an + implementation that assigns \**kwargs for declared + fields and relationships to an instance. If ``None`` is supplied, + no __init__ will be provided and construction will fall back to + cls.__init__ by way of the normal Python semantics. + + :param class_registry: optional dictionary that will serve as the + registry of class names-> mapped classes when string names + are used to identify classes inside of :func:`_orm.relationship` + and others. Allows two or more declarative base classes + to share the same registry of class names for simplified + inter-base relationships. + + :param bind: An optional + :class:`~sqlalchemy.engine.Connectable`, will be assigned + the ``bind`` attribute on the :class:`~sqlalchemy.schema.MetaData` + instance. + + .. deprecated:: 1.4 The "bind" argument to registry is + deprecated and will be removed in SQLAlchemy 2.0. + + + """ + lcl_metadata = metadata or MetaData() + if bind: + lcl_metadata.bind = bind + + if class_registry is None: + class_registry = weakref.WeakValueDictionary() + + self._class_registry = class_registry + self.metadata = lcl_metadata + self.constructor = constructor + + def _dispose_declarative_artifacts(self, cls): + clsregistry.remove_class(cls.__name__, cls, self._class_registry) + + def generate_base( + self, mapper=None, cls=object, name="Base", metaclass=DeclarativeMeta, + ): + """Generate a declarative base class. + + Classes that inherit from the returned class object will be + automatically mapped using declarative mapping. + + E.g.:: + + from sqlalchemy.orm import registry + + mapper_registry = registry() + + Base = mapper_registry.generate_base() + + class MyClass(Base): + __tablename__ = "my_table" + id = Column(Integer, primary_key=True) + + 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. + + + See the section :ref:`orm_declarative_mapping` for background and + examples. + + :param mapper: + An optional callable, defaults to :func:`~sqlalchemy.orm.mapper`. + This function is used to generate new :class:`_orm.Mapper` objects. + + :param cls: + Defaults to :class:`object`. A type to use as the base for the + generated declarative base class. May be a class or tuple of classes. + + :param name: + Defaults to ``Base``. The display name for the generated + class. Customizing this is not required, but can improve clarity in + tracebacks and debugging. + + :param metaclass: + Defaults to :class:`.DeclarativeMeta`. A metaclass or __metaclass__ + compatible callable to use as the meta type of the generated + declarative base class. + + .. seealso:: + + :ref:`orm_declarative_mapping` + + :func:`_orm.declarative_base` + + """ + metadata = self.metadata + + bases = not isinstance(cls, tuple) and (cls,) or cls + + class_dict = dict(registry=self, metadata=metadata) + if isinstance(cls, type): + class_dict["__doc__"] = cls.__doc__ + + if self.constructor: + class_dict["__init__"] = self.constructor + + class_dict["__abstract__"] = True + if mapper: + class_dict["__mapper_cls__"] = mapper + + return metaclass(name, bases, class_dict) + + def mapped(self, cls): + """Class decorator that will apply the Declarative mapping process + to a given class. + + E.g.:: + + from sqlalchemy.orm import registry + + mapper_registry = registry() + + @mapper_registry.mapped + class Foo: + __tablename__ = 'some_table' + + id = Column(Integer, primary_key=True) + name = Column(String) + + See the section :ref:`orm_declarative_mapping` for complete + details and examples. + + :param cls: class to be mapped. + + :return: the class that was passed. + + .. seealso:: + + :ref:`orm_declarative_mapping` + + :meth:`_orm.registry.generate_base` - generates a base class + that will apply Declarative mapping to subclasses automatically + using a Python metaclass. + + """ + _as_declarative(self, cls, cls.__dict__) + return cls + + def as_declarative_base(self, **kw): + """ + Class decorator which will invoke + :meth:`_orm.registry.generate_base` + for a given base class. + + E.g.:: + + from sqlalchemy.orm import registry + + mapper_registry = registry() + + @mapper_registry.as_declarative_base() + class Base(object): + @declared_attr + def __tablename__(cls): + return cls.__name__.lower() + id = Column(Integer, primary_key=True) + + class MyMappedClass(Base): + # ... + + All keyword arguments passed to + :meth:`_orm.registry.as_declarative_base` are passed + along to :meth:`_orm.registry.generate_base`. + + """ + + def decorate(cls): + kw["cls"] = cls + kw["name"] = cls.__name__ + return self.generate_base(**kw) + + return decorate + + def map_declaratively(self, cls): + # type: (type) -> Mapper + """Map a class declaratively. + + In this form of mapping, the class is scanned for mapping information, + including for columns to be associaed with a table, and/or an + actual table object. + + Returns the :class:`_orm.Mapper` object. + + E.g.:: + + from sqlalchemy.orm import registry + + mapper_registry = registry() + + class Foo: + __tablename__ = 'some_table' + + id = Column(Integer, primary_key=True) + name = Column(String) + + mapper = mapper_registry.map_declaratively(Foo) + + This function is more conveniently invoked indirectly via either the + :meth:`_orm.registry.mapped` class decorator or by subclassing a + declarative metaclass generated from + :meth:`_orm.registry.generate_base`. + + See the section :ref:`orm_declarative_mapping` for complete + details and examples. + + :param cls: class to be mapped. + + :return: a :class:`_orm.Mapper` object. + + .. seealso:: + + :ref:`orm_declarative_mapping` + + :meth:`_orm.registry.mapped` - more common decorator interface + to this function. + + :meth:`_orm.registry.map_imperatively` + + """ + return _as_declarative(self, cls, cls.__dict__) + + def map_imperatively(self, class_, local_table=None, **kw): + r"""Map a class imperatively. + + In this form of mapping, the class is not scanned for any mapping + information. Instead, all mapping constructs are passed as + arguments. + + This method is intended to be fully equivalent to the classic + SQLAlchemy :func:`_orm.mapper` function, except that it's in terms of + a particular registry. + + E.g.:: + + from sqlalchemy.orm import registry + + mapper_registry = registry() + + my_table = Table( + "my_table", + mapper_registry.metadata, + Column('id', Integer, primary_key=True) + ) + + class MyClass: + pass + + mapper_registry.map_imperatively(MyClass, my_table) + + See the section :ref:`orm_imperative_mapping` for complete background + and usage examples. + + :param class\_: The class to be mapped. Corresponds to the + :paramref:`_orm.mapper.class_` parameter. + + :param local_table: the :class:`_schema.Table` or other + :class:`_sql.FromClause` object that is the subject of the mapping. + Corresponds to the + :paramref:`_orm.mapper.local_table` parameter. + + :param \**kw: all other keyword arguments are passed to the + :func:`_orm.mapper` function directly. + + .. seealso:: + + :ref:`orm_imperative_mapping` + + :ref:`orm_declarative_mapping` + + """ + return _mapper(self, class_, local_table, kw) + + +def as_declarative(**kw): + """ + Class decorator which will adapt a given class into a + :func:`_orm.declarative_base`. + + This function makes use of the :meth:`_orm.registry.as_declarative_base` + method, by first creating a :class:`_orm.registry` automatically + and then invoking the decorator. + + E.g.:: + + from sqlalchemy.orm import as_declarative + + @as_declarative() + class Base(object): + @declared_attr + def __tablename__(cls): + return cls.__name__.lower() + id = Column(Integer, primary_key=True) + + class MyMappedClass(Base): + # ... + + .. seealso:: + + :meth:`_orm.registry.as_declarative_base` + + """ + bind, metadata, class_registry = ( + kw.pop("bind", None), + kw.pop("metadata", None), + kw.pop("class_registry", None), + ) + + return registry( + bind=bind, metadata=metadata, class_registry=class_registry + ).as_declarative_base(**kw) + + +@inspection._inspects(DeclarativeMeta) +def _inspect_decl_meta(cls): + mp = _inspect_mapped_class(cls) + if mp is None: + if _DeferredMapperConfig.has_cls(cls): + _DeferredMapperConfig.raise_unmapped_for_cls(cls) + raise orm_exc.UnmappedClassError( + cls, + msg="Class %s has a deferred mapping on it. It is not yet " + "usable as a mapped class." % orm_exc._safe_cls_name(cls), + ) + return mp diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/orm/decl_base.py index 9b72fe8ab..b9c890429 100644 --- a/lib/sqlalchemy/ext/declarative/base.py +++ b/lib/sqlalchemy/orm/decl_base.py @@ -5,33 +5,32 @@ # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php """Internal implementation for declarative.""" +from __future__ import absolute_import import collections import weakref +from sqlalchemy.orm import attributes from sqlalchemy.orm import instrumentation from . import clsregistry -from ... import event -from ... import exc -from ... import util -from ...orm import class_mapper -from ...orm import exc as orm_exc -from ...orm import mapper -from ...orm import mapperlib -from ...orm import synonym -from ...orm.attributes import QueryableAttribute -from ...orm.base import _is_mapped_class -from ...orm.base import InspectionAttr -from ...orm.descriptor_props import CompositeProperty -from ...orm.interfaces import MapperProperty -from ...orm.properties import ColumnProperty -from ...schema import Column -from ...schema import Table -from ...sql import expression -from ...util import topological - - -declared_attr = declarative_props = None +from . import exc as orm_exc +from . import mapper as mapperlib +from .attributes import QueryableAttribute +from .base import _is_mapped_class +from .base import InspectionAttr +from .descriptor_props import CompositeProperty +from .descriptor_props import SynonymProperty +from .interfaces import MapperProperty +from .mapper import Mapper as mapper +from .properties import ColumnProperty +from .util import class_mapper +from .. import event +from .. import exc +from .. import util +from ..sql import expression +from ..sql.schema import Column +from ..sql.schema import Table +from ..util import topological def _declared_mapping_info(cls): @@ -49,7 +48,7 @@ def _resolve_for_abstract_or_classical(cls): if cls is object: return None - if _get_immediate_cls_attr(cls, "__abstract__", strict=True): + if cls.__dict__.get("__abstract__", False): for sup in cls.__bases__: sup = _resolve_for_abstract_or_classical(sup) if sup is not None: @@ -57,31 +56,12 @@ def _resolve_for_abstract_or_classical(cls): else: return None else: - classical = _dive_for_classically_mapped_class(cls) - if classical is not None: - return classical - else: - return cls - - -def _dive_for_classically_mapped_class(cls): - # support issue #4321 - - # if we are within a base hierarchy, don't - # search at all for classical mappings - if hasattr(cls, "_decl_class_registry"): - return None + clsmanager = _dive_for_cls_manager(cls) - manager = instrumentation.manager_of_class(cls) - if manager is not None: - return cls - else: - for sup in cls.__bases__: - mapper = _dive_for_classically_mapped_class(sup) - if mapper is not None: - return sup + if clsmanager: + return clsmanager.class_ else: - return None + return cls def _get_immediate_cls_attr(cls, attrname, strict=False): @@ -95,21 +75,24 @@ def _get_immediate_cls_attr(cls, attrname, strict=False): inherit from. """ + + # the rules are different for this name than others, + # make sure we've moved it out. transitional + assert attrname != "__abstract__" + if not issubclass(cls, object): return None - for base in cls.__mro__: - _is_declarative_inherits = hasattr(base, "_decl_class_registry") - _is_classicial_inherits = ( - not _is_declarative_inherits - and _dive_for_classically_mapped_class(base) is not None - ) + if attrname in cls.__dict__: + return getattr(cls, attrname) + + for base in cls.__mro__[1:]: + _is_classicial_inherits = _dive_for_cls_manager(base) if attrname in base.__dict__ and ( base is cls or ( (base in cls.__bases__ if strict else True) - and not _is_declarative_inherits and not _is_classicial_inherits ) ): @@ -118,22 +101,44 @@ def _get_immediate_cls_attr(cls, attrname, strict=False): return None -def _as_declarative(cls, classname, dict_): - global declared_attr, declarative_props - if declared_attr is None: - from .api import declared_attr +def _dive_for_cls_manager(cls): + # because the class manager registration is pluggable, + # we need to do the search for every class in the hierarchy, + # rather than just a simple "cls._sa_class_manager" - declarative_props = (declared_attr, util.classproperty) + # python 2 old style class + if not hasattr(cls, "__mro__"): + return None - if _get_immediate_cls_attr(cls, "__abstract__", strict=True): - return + for base in cls.__mro__: + manager = attributes.manager_of_class(base) + if manager: + return manager + return None - _MapperConfig.setup_mapping(cls, classname, dict_) +def _as_declarative(registry, cls, dict_): + + # declarative scans the class for attributes. no table or mapper + # args passed separately. + + return _MapperConfig.setup_mapping(registry, cls, dict_, None, {}) + + +def _mapper(registry, cls, table, mapper_kw): + _ImperativeMapperConfig(registry, cls, table, mapper_kw) + return cls.__mapper__ -def _check_declared_props_nocascade(obj, name, cls): - if isinstance(obj, declarative_props): +@util.preload_module("sqlalchemy.orm.decl_api") +def _is_declarative_props(obj): + declared_attr = util.preloaded.orm_decl_api.declared_attr + + return isinstance(obj, (declared_attr, util.classproperty)) + + +def _check_declared_props_nocascade(obj, name, cls): + if _is_declarative_props(obj): if getattr(obj, "_cascading", False): util.warn( "@declared_attr.cascading is not supported on the %s " @@ -146,8 +151,19 @@ def _check_declared_props_nocascade(obj, name, cls): class _MapperConfig(object): + __slots__ = ("cls", "classname", "properties", "declared_attr_reg") + @classmethod - def setup_mapping(cls, cls_, classname, dict_): + def setup_mapping(cls, registry, cls_, dict_, table, mapper_kw): + manager = attributes.manager_of_class(cls) + if manager and manager.class_ is cls_: + raise exc.InvalidRequestError( + "Class %r already has been " "instrumented declaratively" % cls + ) + + if cls_.__dict__.get("__abstract__", False): + return + defer_map = _get_immediate_cls_attr( cls_, "_sa_decl_prepare_nocascade", strict=True ) or hasattr(cls_, "_sa_decl_prepare") @@ -155,45 +171,142 @@ class _MapperConfig(object): if defer_map: cfg_cls = _DeferredMapperConfig else: - cfg_cls = _MapperConfig + cfg_cls = _ClassScanMapperConfig - cfg_cls(cls_, classname, dict_) - - def __init__(self, cls_, classname, dict_): + return cfg_cls(registry, cls_, dict_, table, mapper_kw) + def __init__(self, registry, cls_): self.cls = cls_ + self.classname = cls_.__name__ + self.properties = util.OrderedDict() + self.declared_attr_reg = {} + + instrumentation.register_class( + self.cls, + finalize=False, + registry=registry, + declarative_scan=self, + init_method=registry.constructor, + ) + + event.listen( + cls_, + "class_uninstrument", + registry._dispose_declarative_artifacts, + ) + + def set_cls_attribute(self, attrname, value): + + manager = instrumentation.manager_of_class(self.cls) + manager.install_member(attrname, value) + return value + + def _early_mapping(self, mapper_kw): + self.map(mapper_kw) + + +class _ImperativeMapperConfig(_MapperConfig): + __slots__ = ("dict_", "local_table", "inherits") + + def __init__( + self, registry, cls_, table, mapper_kw, + ): + super(_ImperativeMapperConfig, self).__init__(registry, cls_) + + self.dict_ = {} + self.local_table = self.set_cls_attribute("__table__", table) + + with mapperlib._CONFIGURE_MUTEX: + clsregistry.add_class( + self.classname, self.cls, registry._class_registry + ) + + self._setup_inheritance(mapper_kw) + + self._early_mapping(mapper_kw) + + def map(self, mapper_kw=util.EMPTY_DICT): + mapper_cls = mapper + + return self.set_cls_attribute( + "__mapper__", mapper_cls(self.cls, self.local_table, **mapper_kw), + ) + + def _setup_inheritance(self, mapper_kw): + cls = self.cls - # dict_ will be a dictproxy, which we can't write to, and we need to! - self.dict_ = dict(dict_) - self.classname = classname + inherits = mapper_kw.get("inherits", None) + + if inherits is None: + # since we search for classical mappings now, search for + # multiple mapped bases as well and raise an error. + inherits_search = [] + for c in cls.__bases__: + c = _resolve_for_abstract_or_classical(c) + if c is None: + continue + if _declared_mapping_info( + c + ) is not None and not _get_immediate_cls_attr( + c, "_sa_decl_prepare_nocascade", strict=True + ): + inherits_search.append(c) + + if inherits_search: + if len(inherits_search) > 1: + raise exc.InvalidRequestError( + "Class %s has multiple mapped bases: %r" + % (cls, inherits_search) + ) + inherits = inherits_search[0] + elif isinstance(inherits, mapper): + inherits = inherits.class_ + + self.inherits = inherits + + +class _ClassScanMapperConfig(_MapperConfig): + __slots__ = ( + "dict_", + "local_table", + "persist_selectable", + "declared_columns", + "column_copies", + "table_args", + "tablename", + "mapper_args", + "mapper_args_fn", + "inherits", + ) + + def __init__( + self, registry, cls_, dict_, table, mapper_kw, + ): + + super(_ClassScanMapperConfig, self).__init__(registry, cls_) + + self.dict_ = dict(dict_) if dict_ else {} self.persist_selectable = None - self.properties = util.OrderedDict() self.declared_columns = set() self.column_copies = {} self._setup_declared_events() - # temporary registry. While early 1.0 versions - # set up the ClassManager here, by API contract - # we can't do that until there's a mapper. - self.cls._sa_declared_attr_reg = {} - self._scan_attributes() with mapperlib._CONFIGURE_MUTEX: - clsregistry.add_class(self.classname, self.cls) + clsregistry.add_class( + self.classname, self.cls, registry._class_registry + ) self._extract_mappable_attributes() self._extract_declared_columns() - self._setup_table() + self._setup_table(table) - self._setup_inheritance() + self._setup_inheritance(mapper_kw) - self._early_mapping() - - def _early_mapping(self): - self.map() + self._early_mapping(mapper_kw) def _setup_declared_events(self): if _get_immediate_cls_attr(self.cls, "__declare_last__"): @@ -265,7 +378,7 @@ class _MapperConfig(object): if base is not cls: inherited_table_args = True elif class_mapped: - if isinstance(obj, declarative_props): + if _is_declarative_props(obj): util.warn( "Regular (i.e. not __special__) " "attribute '%s.%s' uses @declared_attr, " @@ -287,7 +400,7 @@ class _MapperConfig(object): "be declared as @declared_attr callables " "on declarative mixin classes." ) - elif isinstance(obj, declarative_props): + elif _is_declarative_props(obj): if obj._cascading: if name in dict_: # unfortunately, while we can use the user- @@ -395,8 +508,8 @@ class _MapperConfig(object): continue value = dict_[k] - if isinstance(value, declarative_props): - if isinstance(value, declared_attr) and value._cascading: + if _is_declarative_props(value): + if value._cascading: util.warn( "Use of @declared_attr.cascading only applies to " "Declarative 'mixin' and 'abstract' classes. " @@ -413,7 +526,7 @@ class _MapperConfig(object): ): # detect a QueryableAttribute that's already mapped being # assigned elsewhere in userland, turn into a synonym() - value = synonym(value.key) + value = SynonymProperty(value.key) setattr(cls, k, value) if ( @@ -446,8 +559,7 @@ class _MapperConfig(object): "for the MetaData instance when using a " "declarative base class." ) - prop = clsregistry._deferred_relationship(cls, value) - our_stuff[k] = prop + our_stuff[k] = value def _extract_declared_columns(self): our_stuff = self.properties @@ -488,24 +600,25 @@ class _MapperConfig(object): % (self.classname, name, (", ".join(sorted(keys)))) ) - def _setup_table(self): + def _setup_table(self, table=None): cls = self.cls tablename = self.tablename table_args = self.table_args dict_ = self.dict_ declared_columns = self.declared_columns + manager = attributes.manager_of_class(cls) + declared_columns = self.declared_columns = sorted( declared_columns, key=lambda c: c._creation_order ) - table = None - if hasattr(cls, "__table_cls__"): - table_cls = util.unbound_method_to_callable(cls.__table_cls__) - else: - table_cls = Table + if "__table__" not in dict_ and table is None: + if hasattr(cls, "__table_cls__"): + table_cls = util.unbound_method_to_callable(cls.__table_cls__) + else: + table_cls = Table - if "__table__" not in dict_: if tablename is not None: args, table_kw = (), {} @@ -522,14 +635,18 @@ class _MapperConfig(object): if autoload: table_kw["autoload"] = True - cls.__table__ = table = table_cls( - tablename, - cls.metadata, - *(tuple(declared_columns) + tuple(args)), - **table_kw + table = self.set_cls_attribute( + "__table__", + table_cls( + tablename, + manager.registry.metadata, + *(tuple(declared_columns) + tuple(args)), + **table_kw + ), ) else: - table = cls.__table__ + if table is None: + table = cls.__table__ if declared_columns: for c in declared_columns: if not table.c.contains_column(c): @@ -539,34 +656,40 @@ class _MapperConfig(object): ) self.local_table = table - def _setup_inheritance(self): + def _setup_inheritance(self, mapper_kw): table = self.local_table cls = self.cls table_args = self.table_args declared_columns = self.declared_columns - # since we search for classical mappings now, search for - # multiple mapped bases as well and raise an error. - inherits = [] - for c in cls.__bases__: - c = _resolve_for_abstract_or_classical(c) - if c is None: - continue - if _declared_mapping_info( - c - ) is not None and not _get_immediate_cls_attr( - c, "_sa_decl_prepare_nocascade", strict=True - ): - inherits.append(c) + inherits = mapper_kw.get("inherits", None) - if inherits: - if len(inherits) > 1: - raise exc.InvalidRequestError( - "Class %s has multiple mapped bases: %r" % (cls, inherits) - ) - self.inherits = inherits[0] - else: - self.inherits = None + if inherits is None: + # since we search for classical mappings now, search for + # multiple mapped bases as well and raise an error. + inherits_search = [] + for c in cls.__bases__: + c = _resolve_for_abstract_or_classical(c) + if c is None: + continue + if _declared_mapping_info( + c + ) is not None and not _get_immediate_cls_attr( + c, "_sa_decl_prepare_nocascade", strict=True + ): + inherits_search.append(c) + + if inherits_search: + if len(inherits_search) > 1: + raise exc.InvalidRequestError( + "Class %s has multiple mapped bases: %r" + % (cls, inherits_search) + ) + inherits = inherits_search[0] + elif isinstance(inherits, mapper): + inherits = inherits.class_ + + self.inherits = inherits if ( table is None @@ -614,13 +737,21 @@ class _MapperConfig(object): ): inherited_persist_selectable._refresh_for_new_column(c) - def _prepare_mapper_arguments(self): + def _prepare_mapper_arguments(self, mapper_kw): properties = self.properties + if self.mapper_args_fn: mapper_args = self.mapper_args_fn() else: mapper_args = {} + if mapper_kw: + mapper_args.update(mapper_kw) + + if "properties" in mapper_args: + properties = dict(properties) + properties.update(mapper_args["properties"]) + # make sure that column copies are used rather # than the original columns from any mixins for k in ("version_id_col", "polymorphic_on"): @@ -628,9 +759,16 @@ class _MapperConfig(object): v = mapper_args[k] mapper_args[k] = self.column_copies.get(v, v) - assert ( - "inherits" not in mapper_args - ), "Can't specify 'inherits' explicitly with declarative mappings" + if "inherits" in mapper_args: + inherits_arg = mapper_args["inherits"] + if isinstance(inherits_arg, mapper): + inherits_arg = inherits_arg.class_ + + if inherits_arg is not self.inherits: + raise exc.InvalidRequestError( + "mapper inherits argument given for non-inheriting " + "class %s" % (mapper_args["inherits"]) + ) if self.inherits: mapper_args["inherits"] = self.inherits @@ -674,8 +812,8 @@ class _MapperConfig(object): result_mapper_args["properties"] = properties self.mapper_args = result_mapper_args - def map(self): - self._prepare_mapper_arguments() + def map(self, mapper_kw=util.EMPTY_DICT): + self._prepare_mapper_arguments(mapper_kw) if hasattr(self.cls, "__mapper_cls__"): mapper_cls = util.unbound_method_to_callable( self.cls.__mapper_cls__ @@ -683,17 +821,16 @@ class _MapperConfig(object): else: mapper_cls = mapper - self.cls.__mapper__ = mp_ = mapper_cls( - self.cls, self.local_table, **self.mapper_args + return self.set_cls_attribute( + "__mapper__", + mapper_cls(self.cls, self.local_table, **self.mapper_args), ) - del self.cls._sa_declared_attr_reg - return mp_ -class _DeferredMapperConfig(_MapperConfig): +class _DeferredMapperConfig(_ClassScanMapperConfig): _configs = util.OrderedDict() - def _early_mapping(self): + def _early_mapping(self, mapper_kw): pass @property @@ -751,9 +888,9 @@ class _DeferredMapperConfig(_MapperConfig): ) return list(topological.sort(tuples, classes_for_base)) - def map(self): + def map(self, mapper_kw=util.EMPTY_DICT): self._configs.pop(self._cls, None) - return super(_DeferredMapperConfig, self).map() + return super(_DeferredMapperConfig, self).map(mapper_kw) def _add_attribute(cls, key, value): @@ -776,16 +913,12 @@ def _add_attribute(cls, key, value): cls.__table__.append_column(col) cls.__mapper__.add_property(key, value) elif isinstance(value, MapperProperty): - cls.__mapper__.add_property( - key, clsregistry._deferred_relationship(cls, value) - ) + cls.__mapper__.add_property(key, value) elif isinstance(value, QueryableAttribute) and value.key != key: # detect a QueryableAttribute that's already mapped being # assigned elsewhere in userland, turn into a synonym() - value = synonym(value.key) - cls.__mapper__.add_property( - key, clsregistry._deferred_relationship(cls, value) - ) + value = SynonymProperty(value.key) + cls.__mapper__.add_property(key, value) else: type.__setattr__(cls, key, value) cls.__mapper__._expire_memoizations() diff --git a/lib/sqlalchemy/orm/instrumentation.py b/lib/sqlalchemy/orm/instrumentation.py index f64744083..f390c49a7 100644 --- a/lib/sqlalchemy/orm/instrumentation.py +++ b/lib/sqlalchemy/orm/instrumentation.py @@ -39,6 +39,9 @@ from .. import util from ..util import HasMemoized +DEL_ATTR = util.symbol("DEL_ATTR") + + class ClassManager(HasMemoized, dict): """Tracks state information at the class level.""" @@ -50,9 +53,12 @@ class ClassManager(HasMemoized, dict): expired_attribute_loader = None "previously known as deferred_scalar_loader" - original_init = object.__init__ + init_method = None factory = None + mapper = None + declarative_scan = None + registry = None @property @util.deprecated( @@ -78,6 +84,7 @@ class ClassManager(HasMemoized, dict): self.new_init = None self.local_attrs = {} self.originals = {} + self._finalized = False self._bases = [ mgr @@ -93,14 +100,13 @@ class ClassManager(HasMemoized, dict): self.update(base_) self.dispatch._events._new_classmanager_instance(class_, self) - # events._InstanceEventsHold.populate(class_, self) for basecls in class_.__mro__: mgr = manager_of_class(basecls) if mgr is not None: self.dispatch._update(mgr.dispatch) + self.manage() - self._instrument_init() if "__del__" in class_.__dict__: util.warn( @@ -110,6 +116,52 @@ class ClassManager(HasMemoized, dict): "reference cycles. Please remove this method." % class_ ) + def _update_state( + self, + finalize=False, + mapper=None, + registry=None, + declarative_scan=None, + expired_attribute_loader=None, + init_method=None, + ): + + if mapper: + self.mapper = mapper + if registry: + self.registry = registry + if declarative_scan: + self.declarative_scan = declarative_scan + if expired_attribute_loader: + self.expired_attribute_loader = expired_attribute_loader + + if init_method: + assert not self._finalized, ( + "class is already instrumented, " + "init_method %s can't be applied" % init_method + ) + self.init_method = init_method + + if not self._finalized: + self.original_init = ( + self.init_method + if self.init_method is not None + and self.class_.__init__ is object.__init__ + else self.class_.__init__ + ) + + if finalize and not self._finalized: + self._finalize() + + def _finalize(self): + if self._finalized: + return + self._finalized = True + + self._instrument_init() + + _instrumentation_factory.dispatch.class_instrument(self.class_) + def __hash__(self): return id(self) @@ -210,26 +262,12 @@ class ClassManager(HasMemoized, dict): can post-configure the auto-generated ClassManager when needed. """ - manager = manager_of_class(cls) - if manager is None: - manager = _instrumentation_factory.create_manager_for_cls(cls) - return manager + return register_class(cls, finalize=False) def _instrument_init(self): - # TODO: self.class_.__init__ is often the already-instrumented - # __init__ from an instrumented superclass. We still need to make - # our own wrapper, but it would - # be nice to wrap the original __init__ and not our existing wrapper - # of such, since this adds method overhead. - self.original_init = self.class_.__init__ - self.new_init = _generate_init(self.class_, self) + self.new_init = _generate_init(self.class_, self, self.original_init) self.install_member("__init__", self.new_init) - def _uninstrument_init(self): - if self.new_init: - self.uninstall_member("__init__") - self.new_init = None - @util.memoized_property def _state_constructor(self): self.dispatch.first_init(self, self.class_) @@ -311,9 +349,10 @@ class ClassManager(HasMemoized, dict): def unregister(self): """remove all instrumentation established by this ClassManager.""" - self._uninstrument_init() + for key in list(self.originals): + self.uninstall_member(key) - self.mapper = self.dispatch = None + self.mapper = self.dispatch = self.new_init = None self.info.clear() for key in list(self): @@ -337,13 +376,15 @@ class ClassManager(HasMemoized, dict): "%r: requested attribute name conflicts with " "instrumentation attribute of the same name." % key ) - self.originals.setdefault(key, getattr(self.class_, key, None)) + self.originals.setdefault(key, self.class_.__dict__.get(key, DEL_ATTR)) setattr(self.class_, key, implementation) def uninstall_member(self, key): original = self.originals.pop(key, None) - if original is not None: + if original is not DEL_ATTR: setattr(self.class_, key, original) + else: + delattr(self.class_, key) def instrument_collection_class(self, key, collection_class): return collections.prepare_instrumentation(collection_class) @@ -484,7 +525,6 @@ class InstrumentationFactory(object): manager.factory = factory - self.dispatch.class_instrument(class_) return manager def _locate_extended_factory(self, class_): @@ -518,7 +558,15 @@ instance_dict = _default_dict_getter = base.instance_dict manager_of_class = _default_manager_getter = base.manager_of_class -def register_class(class_): +def register_class( + class_, + finalize=True, + mapper=None, + registry=None, + declarative_scan=None, + expired_attribute_loader=None, + init_method=None, +): """Register class instrumentation. Returns the existing or newly created class manager. @@ -528,6 +576,15 @@ def register_class(class_): manager = manager_of_class(class_) if manager is None: manager = _instrumentation_factory.create_manager_for_cls(class_) + manager._update_state( + mapper=mapper, + registry=registry, + declarative_scan=declarative_scan, + expired_attribute_loader=expired_attribute_loader, + init_method=init_method, + finalize=finalize, + ) + return manager @@ -550,14 +607,15 @@ def is_instrumented(instance, key): ) -def _generate_init(class_, class_manager): +def _generate_init(class_, class_manager, original_init): """Build an __init__ decorator that triggers ClassManager events.""" # TODO: we should use the ClassManager's notion of the # original '__init__' method, once ClassManager is fixed # to always reference that. - original__init__ = class_.__init__ - assert original__init__ + + if original_init is None: + original_init = class_.__init__ # Go through some effort here and don't change the user's __init__ # calling signature, including the unlikely case that it has @@ -570,23 +628,23 @@ def __init__(%(apply_pos)s): if new_state: return new_state._initialize_instance(%(apply_kw)s) else: - return original__init__(%(apply_kw)s) + return original_init(%(apply_kw)s) """ - func_vars = util.format_argspec_init(original__init__, grouped=False) + func_vars = util.format_argspec_init(original_init, grouped=False) func_text = func_body % func_vars if util.py2k: - func = getattr(original__init__, "im_func", original__init__) + func = getattr(original_init, "im_func", original_init) func_defaults = getattr(func, "func_defaults", None) else: - func_defaults = getattr(original__init__, "__defaults__", None) - func_kw_defaults = getattr(original__init__, "__kwdefaults__", None) + func_defaults = getattr(original_init, "__defaults__", None) + func_kw_defaults = getattr(original_init, "__kwdefaults__", None) env = locals().copy() exec(func_text, env) __init__ = env["__init__"] - __init__.__doc__ = original__init__.__doc__ - __init__._sa_original_init = original__init__ + __init__.__doc__ = original_init.__doc__ + __init__._sa_original_init = original_init if func_defaults: __init__.__defaults__ = func_defaults diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 755d4afc7..db2b94a4e 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -160,50 +160,23 @@ class Mapper( legacy_is_orphan=False, _compiled_cache_size=100, ): - r"""Return a new :class:`_orm.Mapper` object. - - This function is typically used behind the scenes - via the Declarative extension. When using Declarative, - many of the usual :func:`.mapper` arguments are handled - by the Declarative extension itself, including ``class_``, - ``local_table``, ``properties``, and ``inherits``. - Other options are passed to :func:`.mapper` using - the ``__mapper_args__`` class variable:: - - class MyClass(Base): - __tablename__ = 'my_table' - id = Column(Integer, primary_key=True) - type = Column(String(50)) - alt = Column("some_alt", Integer) - - __mapper_args__ = { - 'polymorphic_on' : type - } - - - Explicit use of :func:`.mapper` - is often referred to as *classical mapping*. The above - declarative example is equivalent in classical form to:: - - my_table = Table("my_table", metadata, - Column('id', Integer, primary_key=True), - Column('type', String(50)), - Column("some_alt", Integer) - ) - - class MyClass(object): - pass + r"""Direct consructor for a new :class:`_orm.Mapper` object. - mapper(MyClass, my_table, - polymorphic_on=my_table.c.type, - properties={ - 'alt':my_table.c.some_alt - }) + The :func:`_orm.mapper` function is normally invoked through the + use of the :class:`_orm.registry` object through either the + :ref:`Declarative <orm_declarative_mapping>` or + :ref:`Imperative <orm_imperative_mapping>` mapping styles. - .. seealso:: + .. versionchanged:: 1.4 The :func:`_orm.mapper` function should not + be called directly for classical mapping; for a classical mapping + configuration, use the :meth:`_orm.registry.map_imperatively` + method. The :func:`_orm.mapper` function may become private in a + future release. - :ref:`classical_mapping` - discussion of direct usage of - :func:`.mapper` + Parameters documented below may be passed to either the + :meth:`_orm.registry.map_imperatively` method, or may be passed in the + ``__mapper_args__`` declarative class attribute described at + :ref:`orm_declarative_mapper_options`. :param class\_: The class to be mapped. When using Declarative, this argument is automatically passed as the declared class @@ -342,12 +315,10 @@ class Mapper( mapping of the class to an alternate selectable, for loading only. - :paramref:`_orm.Mapper.non_primary` is not an often used option, but - is useful in some specific :func:`_orm.relationship` cases. - - .. seealso:: + .. seealso:: - :ref:`relationship_non_primary_mapper` + :ref:`relationship_aliased_class` - the new pattern that removes + the need for the :paramref:`_orm.Mapper.non_primary` flag. :param passive_deletes: Indicates DELETE behavior of foreign key columns when a joined-table inheritance entity is being deleted. @@ -1207,6 +1178,10 @@ class Mapper( """ + # we expect that declarative has applied the class manager + # already and set up a registry. if this is None, + # we will emit a deprecation warning below when we also see that + # it has no registry. manager = attributes.manager_of_class(self.class_) if self.non_primary: @@ -1226,9 +1201,6 @@ class Mapper( if manager.is_mapped: raise sa_exc.ArgumentError( "Class '%s' already has a primary mapper defined. " - "Use non_primary=True to " - "create a non primary Mapper. clear_mappers() will " - "remove *all* current mappers from all classes." % self.class_ ) # else: @@ -1238,19 +1210,36 @@ class Mapper( _mapper_registry[self] = True - # note: this *must be called before instrumentation.register_class* - # to maintain the documented behavior of instrument_class self.dispatch.instrument_class(self, self.class_) - if manager is None: - manager = instrumentation.register_class(self.class_) + # this invokes the class_instrument event and sets up + # the __init__ method. documented behavior is that this must + # occur after the instrument_class event above. + # yes two events with the same two words reversed and different APIs. + # :( + + manager = instrumentation.register_class( + self.class_, + mapper=self, + expired_attribute_loader=util.partial( + loading.load_scalar_attributes, self + ), + # finalize flag means instrument the __init__ method + # and call the class_instrument event + finalize=True, + ) + if not manager.registry: + util.warn_deprecated_20( + "Calling the mapper() function directly outside of a " + "declarative registry is deprecated." + " Please use the sqlalchemy.orm.registry.map_imperatively() " + "function for a classical mapping." + ) + from . import registry - self.class_manager = manager + manager.registry = registry() - manager.mapper = self - manager.expired_attribute_loader = util.partial( - loading.load_scalar_attributes, self - ) + self.class_manager = manager # The remaining members can be added by any mapper, # e_name None or not. @@ -2281,7 +2270,7 @@ class Mapper( @property def selectable(self): - """The :func:`_expression.select` construct this + """The :class:`_schema.FromClause` construct this :class:`_orm.Mapper` selects from by default. Normally, this is equivalent to :attr:`.persist_selectable`, unless diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index 794b9422c..1c95b6e06 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -2088,8 +2088,13 @@ class RelationshipProperty(StrategizedProperty): class or aliased class that is referred towards. """ + mapperlib = util.preloaded.orm_mapper - if callable(self.argument) and not isinstance( + + if isinstance(self.argument, util.string_types): + argument = self._clsregistry_resolve_name(self.argument)() + + elif callable(self.argument) and not isinstance( self.argument, (type, mapperlib.Mapper) ): argument = self.argument() @@ -2124,6 +2129,7 @@ class RelationshipProperty(StrategizedProperty): return self.entity.mapper def do_init(self): + self._check_conflicts() self._process_dependent_arguments() self._setup_join_conditions() @@ -2141,6 +2147,7 @@ class RelationshipProperty(StrategizedProperty): Callables are resolved, ORM annotations removed. """ + # accept callables for other attributes which may require # deferred initialization. This technique is used # by declarative "string configs" and some recipes. @@ -2153,7 +2160,12 @@ class RelationshipProperty(StrategizedProperty): "remote_side", ): attr_value = getattr(self, attr) - if callable(attr_value): + + if isinstance(attr_value, util.string_types): + setattr( + self, attr, self._clsregistry_resolve_arg(attr_value)() + ) + elif callable(attr_value): setattr(self, attr, attr_value()) # remove "annotations" which are present if mapped class @@ -2226,6 +2238,21 @@ class RelationshipProperty(StrategizedProperty): self._calculated_foreign_keys = jc.foreign_key_columns self.secondary_synchronize_pairs = jc.secondary_synchronize_pairs + @property + def _clsregistry_resolve_arg(self): + return self._clsregistry_resolvers[1] + + @property + def _clsregistry_resolve_name(self): + return self._clsregistry_resolvers[0] + + @util.memoized_property + @util.preload_module("sqlalchemy.orm.clsregistry") + def _clsregistry_resolvers(self): + _resolver = util.preloaded.orm_clsregistry._resolver + + return _resolver(self.parent.class_, self) + @util.preload_module("sqlalchemy.orm.mapper") def _check_conflicts(self): """Test that this relationship is legal, warn about diff --git a/lib/sqlalchemy/testing/entities.py b/lib/sqlalchemy/testing/entities.py index f5d207bc2..085c19196 100644 --- a/lib/sqlalchemy/testing/entities.py +++ b/lib/sqlalchemy/testing/entities.py @@ -39,10 +39,7 @@ class BasicEntity(object): _recursion_stack = set() -class ComparableEntity(BasicEntity): - def __hash__(self): - return hash(self.__class__) - +class ComparableMixin(object): def __ne__(self, other): return not self.__eq__(other) @@ -107,3 +104,8 @@ class ComparableEntity(BasicEntity): return True finally: _recursion_stack.remove(id(self)) + + +class ComparableEntity(ComparableMixin, BasicEntity): + def __hash__(self): + return hash(self.__class__) diff --git a/lib/sqlalchemy/testing/fixtures.py b/lib/sqlalchemy/testing/fixtures.py index 85d3374de..2d3b27917 100644 --- a/lib/sqlalchemy/testing/fixtures.py +++ b/lib/sqlalchemy/testing/fixtures.py @@ -15,11 +15,13 @@ from . import schema from .engines import drop_all_tables from .entities import BasicEntity from .entities import ComparableEntity +from .entities import ComparableMixin # noqa from .util import adict from .. import event from .. import util -from ..ext.declarative import declarative_base -from ..ext.declarative import DeclarativeMeta +from ..orm import declarative_base +from ..orm import registry +from ..orm.decl_api import DeclarativeMeta from ..schema import sort_tables_and_constraints @@ -383,10 +385,12 @@ class MappedTest(_ORMTest, TablesTest, assertions.AssertsExecutionResults): @classmethod def _setup_once_mappers(cls): if cls.run_setup_mappers == "once": + cls.mapper = cls._generate_mapper() cls._with_register_classes(cls.setup_mappers) def _setup_each_mappers(self): if self.run_setup_mappers == "each": + self.mapper = self._generate_mapper() self._with_register_classes(self.setup_mappers) def _setup_each_classes(self): @@ -394,6 +398,11 @@ class MappedTest(_ORMTest, TablesTest, assertions.AssertsExecutionResults): self._with_register_classes(self.setup_classes) @classmethod + def _generate_mapper(cls): + decl = registry() + return decl.map_imperatively + + @classmethod def _with_register_classes(cls, fn): """Run a setup method, framing the operation with a Base class that will catch new subclasses to be established within diff --git a/lib/sqlalchemy/util/__init__.py b/lib/sqlalchemy/util/__init__.py index 1d92084cc..5fdcdf654 100644 --- a/lib/sqlalchemy/util/__init__.py +++ b/lib/sqlalchemy/util/__init__.py @@ -101,6 +101,7 @@ from .deprecations import deprecated_20_cls # noqa from .deprecations import deprecated_cls # noqa from .deprecations import deprecated_params # noqa from .deprecations import inject_docstring_text # noqa +from .deprecations import moved_20 # noqa from .deprecations import SQLALCHEMY_WARN_20 # noqa from .deprecations import warn_deprecated # noqa from .deprecations import warn_deprecated_20 # noqa diff --git a/lib/sqlalchemy/util/deprecations.py b/lib/sqlalchemy/util/deprecations.py index 0a79344c5..eae4be768 100644 --- a/lib/sqlalchemy/util/deprecations.py +++ b/lib/sqlalchemy/util/deprecations.py @@ -27,10 +27,12 @@ if os.getenv("SQLALCHEMY_WARN_20", "false").lower() in ("true", "yes", "1"): def _warn_with_version(msg, version, type_, stacklevel): - if type_ is exc.RemovedIn20Warning and not SQLALCHEMY_WARN_20: + is_20 = issubclass(type_, exc.RemovedIn20Warning) + + if is_20 and not SQLALCHEMY_WARN_20: return - if type_ is exc.RemovedIn20Warning: + if is_20: msg += " (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)" warn = type_(msg) @@ -150,6 +152,12 @@ def deprecated( return decorate +def moved_20(message, **kw): + return deprecated( + "2.0", message=message, warning=exc.MovedIn20Warning, **kw + ) + + def deprecated_20(api_name, alternative=None, **kw): message = ( "The %s function/method is considered legacy as of the " @@ -325,19 +333,14 @@ def _decorate_with_warning( " (Background on SQLAlchemy 2.0 at: " ":ref:`migration_20_toplevel`)" ) - warning_only = ( - " (Background on SQLAlchemy 2.0 at: http://sqlalche.me/e/b8d9)" - ) else: - doc_only = warning_only = "" + doc_only = "" @decorator def warned(fn, *args, **kwargs): skip_warning = kwargs.pop("_sa_skip_warning", False) if not skip_warning: - _warn_with_version( - message + warning_only, version, wtype, stacklevel=3 - ) + _warn_with_version(message, version, wtype, stacklevel=3) return fn(*args, **kwargs) doc = func.__doc__ is not None and func.__doc__ or "" |