exc.py 7.2 KB

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