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