1# orm/exc.py
2# Copyright (C) 2005-2019 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: http://www.opensource.org/licenses/mit-license.php
7
8"""SQLAlchemy ORM exceptions."""
9from .. import exc as sa_exc
10from .. import util
11
12NO_STATE = (AttributeError, KeyError)
13"""Exception types that may be raised by instrumentation implementations."""
14
15
16class StaleDataError(sa_exc.SQLAlchemyError):
17    """An operation encountered database state that is unaccounted for.
18
19    Conditions which cause this to happen include:
20
21    * A flush may have attempted to update or delete rows
22      and an unexpected number of rows were matched during
23      the UPDATE or DELETE statement.   Note that when
24      version_id_col is used, rows in UPDATE or DELETE statements
25      are also matched against the current known version
26      identifier.
27
28    * A mapped object with version_id_col was refreshed,
29      and the version number coming back from the database does
30      not match that of the object itself.
31
32    * A object is detached from its parent object, however
33      the object was previously attached to a different parent
34      identity which was garbage collected, and a decision
35      cannot be made if the new parent was really the most
36      recent "parent".
37
38    """
39
40
41ConcurrentModificationError = StaleDataError
42
43
44class FlushError(sa_exc.SQLAlchemyError):
45    """A invalid condition was detected during flush()."""
46
47
48class UnmappedError(sa_exc.InvalidRequestError):
49    """Base for exceptions that involve expected mappings not present."""
50
51
52class ObjectDereferencedError(sa_exc.SQLAlchemyError):
53    """An operation cannot complete due to an object being garbage
54    collected.
55
56    """
57
58
59class DetachedInstanceError(sa_exc.SQLAlchemyError):
60    """An attempt to access unloaded attributes on a
61    mapped instance that is detached."""
62
63    code = "bhk3"
64
65
66class UnmappedInstanceError(UnmappedError):
67    """An mapping operation was requested for an unknown instance."""
68
69    @util.dependencies("sqlalchemy.orm.base")
70    def __init__(self, base, obj, msg=None):
71        if not msg:
72            try:
73                base.class_mapper(type(obj))
74                name = _safe_cls_name(type(obj))
75                msg = (
76                    "Class %r is mapped, but this instance lacks "
77                    "instrumentation.  This occurs when the instance "
78                    "is created before sqlalchemy.orm.mapper(%s) "
79                    "was called." % (name, name)
80                )
81            except UnmappedClassError:
82                msg = _default_unmapped(type(obj))
83                if isinstance(obj, type):
84                    msg += (
85                        "; was a class (%s) supplied where an instance was "
86                        "required?" % _safe_cls_name(obj)
87                    )
88        UnmappedError.__init__(self, msg)
89
90    def __reduce__(self):
91        return self.__class__, (None, self.args[0])
92
93
94class UnmappedClassError(UnmappedError):
95    """An mapping operation was requested for an unknown class."""
96
97    def __init__(self, cls, msg=None):
98        if not msg:
99            msg = _default_unmapped(cls)
100        UnmappedError.__init__(self, msg)
101
102    def __reduce__(self):
103        return self.__class__, (None, self.args[0])
104
105
106class ObjectDeletedError(sa_exc.InvalidRequestError):
107    """A refresh operation failed to retrieve the database
108    row corresponding to an object's known primary key identity.
109
110    A refresh operation proceeds when an expired attribute is
111    accessed on an object, or when :meth:`.Query.get` is
112    used to retrieve an object which is, upon retrieval, detected
113    as expired.   A SELECT is emitted for the target row
114    based on primary key; if no row is returned, this
115    exception is raised.
116
117    The true meaning of this exception is simply that
118    no row exists for the primary key identifier associated
119    with a persistent object.   The row may have been
120    deleted, or in some cases the primary key updated
121    to a new value, outside of the ORM's management of the target
122    object.
123
124    """
125
126    @util.dependencies("sqlalchemy.orm.base")
127    def __init__(self, base, state, msg=None):
128        if not msg:
129            msg = (
130                "Instance '%s' has been deleted, or its "
131                "row is otherwise not present." % base.state_str(state)
132            )
133
134        sa_exc.InvalidRequestError.__init__(self, msg)
135
136    def __reduce__(self):
137        return self.__class__, (None, self.args[0])
138
139
140class UnmappedColumnError(sa_exc.InvalidRequestError):
141    """Mapping operation was requested on an unknown column."""
142
143
144class NoResultFound(sa_exc.InvalidRequestError):
145    """A database result was required but none was found."""
146
147
148class MultipleResultsFound(sa_exc.InvalidRequestError):
149    """A single database result was required but more than one were found."""
150
151
152def _safe_cls_name(cls):
153    try:
154        cls_name = ".".join((cls.__module__, cls.__name__))
155    except AttributeError:
156        cls_name = getattr(cls, "__name__", None)
157        if cls_name is None:
158            cls_name = repr(cls)
159    return cls_name
160
161
162@util.dependencies("sqlalchemy.orm.base")
163def _default_unmapped(base, cls):
164    try:
165        mappers = base.manager_of_class(cls).mappers
166    except NO_STATE:
167        mappers = {}
168    except TypeError:
169        mappers = {}
170    name = _safe_cls_name(cls)
171
172    if not mappers:
173        return "Class '%s' is not mapped" % name
174