summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2020-08-31 11:46:55 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2020-09-10 17:53:53 -0400
commit450f5c0d6519a439f4025c3892fe4cf3ee2d892c (patch)
tree1f3f2467306304a5e9ccb25f10bfdf9989327ae2 /lib
parent96bb6dc56d1da2b4fa30afd08ac4dfa665752913 (diff)
downloadsqlalchemy-450f5c0d6519a439f4025c3892fe4cf3ee2d892c.tar.gz
Build out new declarative systems; deprecate mapper()
The ORM Declarative system is now unified into the ORM itself, with new import spaces under ``sqlalchemy.orm`` and new kinds of mappings. Support for decorator-based mappings without using a base class, support for classical style-mapper() calls that have access to the declarative class registry for relationships, and full integration of Declarative with 3rd party class attribute systems like ``dataclasses`` and ``attrs`` is now supported. Fixes: #5508 Change-Id: I130b2b6edff6450bfe8a3e6baa099ff04b5471ff
Diffstat (limited to 'lib')
-rw-r--r--lib/sqlalchemy/exc.py4
-rw-r--r--lib/sqlalchemy/ext/automap.py2
-rw-r--r--lib/sqlalchemy/ext/declarative/__init__.py53
-rw-r--r--lib/sqlalchemy/ext/declarative/api.py823
-rw-r--r--lib/sqlalchemy/ext/declarative/extensions.py455
-rw-r--r--lib/sqlalchemy/orm/__init__.py10
-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.py753
-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.py128
-rw-r--r--lib/sqlalchemy/orm/mapper.py107
-rw-r--r--lib/sqlalchemy/orm/relationships.py31
-rw-r--r--lib/sqlalchemy/testing/entities.py10
-rw-r--r--lib/sqlalchemy/testing/fixtures.py13
-rw-r--r--lib/sqlalchemy/util/__init__.py1
-rw-r--r--lib/sqlalchemy/util/deprecations.py21
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 ""