entities.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. # testing/entities.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. # mypy: ignore-errors
  8. from __future__ import annotations
  9. import sqlalchemy as sa
  10. from .. import exc as sa_exc
  11. from ..orm.writeonly import WriteOnlyCollection
  12. _repr_stack = set()
  13. class BasicEntity:
  14. def __init__(self, **kw):
  15. for key, value in kw.items():
  16. setattr(self, key, value)
  17. def __repr__(self):
  18. if id(self) in _repr_stack:
  19. return object.__repr__(self)
  20. _repr_stack.add(id(self))
  21. try:
  22. return "%s(%s)" % (
  23. (self.__class__.__name__),
  24. ", ".join(
  25. [
  26. "%s=%r" % (key, getattr(self, key))
  27. for key in sorted(self.__dict__.keys())
  28. if not key.startswith("_")
  29. ]
  30. ),
  31. )
  32. finally:
  33. _repr_stack.remove(id(self))
  34. _recursion_stack = set()
  35. class ComparableMixin:
  36. def __ne__(self, other):
  37. return not self.__eq__(other)
  38. def __eq__(self, other):
  39. """'Deep, sparse compare.
  40. Deeply compare two entities, following the non-None attributes of the
  41. non-persisted object, if possible.
  42. """
  43. if other is self:
  44. return True
  45. elif not self.__class__ == other.__class__:
  46. return False
  47. if id(self) in _recursion_stack:
  48. return True
  49. _recursion_stack.add(id(self))
  50. try:
  51. # pick the entity that's not SA persisted as the source
  52. try:
  53. self_key = sa.orm.attributes.instance_state(self).key
  54. except sa.orm.exc.NO_STATE:
  55. self_key = None
  56. if other is None:
  57. a = self
  58. b = other
  59. elif self_key is not None:
  60. a = other
  61. b = self
  62. else:
  63. a = self
  64. b = other
  65. for attr in list(a.__dict__):
  66. if attr.startswith("_"):
  67. continue
  68. value = getattr(a, attr)
  69. if isinstance(value, WriteOnlyCollection):
  70. continue
  71. try:
  72. # handle lazy loader errors
  73. battr = getattr(b, attr)
  74. except (AttributeError, sa_exc.UnboundExecutionError):
  75. return False
  76. if hasattr(value, "__iter__") and not isinstance(value, str):
  77. if hasattr(value, "__getitem__") and not hasattr(
  78. value, "keys"
  79. ):
  80. if list(value) != list(battr):
  81. return False
  82. else:
  83. if set(value) != set(battr):
  84. return False
  85. else:
  86. if value is not None and value != battr:
  87. return False
  88. return True
  89. finally:
  90. _recursion_stack.remove(id(self))
  91. class ComparableEntity(ComparableMixin, BasicEntity):
  92. def __hash__(self):
  93. return hash(self.__class__)