exc.py 7.5 KB

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