|
|
|
# testing/entities.py
|
|
|
|
# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
|
|
|
|
# <see AUTHORS file>
|
|
|
|
#
|
|
|
|
# This module is part of SQLAlchemy and is released under
|
|
|
|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
|
|
# mypy: ignore-errors
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
import sqlalchemy as sa
|
|
|
|
from .. import exc as sa_exc
|
|
|
|
from ..orm.writeonly import WriteOnlyCollection
|
|
|
|
|
|
|
|
_repr_stack = set()
|
|
|
|
|
|
|
|
|
|
|
|
class BasicEntity:
|
|
|
|
def __init__(self, **kw):
|
|
|
|
for key, value in kw.items():
|
|
|
|
setattr(self, key, value)
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
if id(self) in _repr_stack:
|
|
|
|
return object.__repr__(self)
|
|
|
|
_repr_stack.add(id(self))
|
|
|
|
try:
|
|
|
|
return "%s(%s)" % (
|
|
|
|
(self.__class__.__name__),
|
|
|
|
", ".join(
|
|
|
|
[
|
|
|
|
"%s=%r" % (key, getattr(self, key))
|
|
|
|
for key in sorted(self.__dict__.keys())
|
|
|
|
if not key.startswith("_")
|
|
|
|
]
|
|
|
|
),
|
|
|
|
)
|
|
|
|
finally:
|
|
|
|
_repr_stack.remove(id(self))
|
|
|
|
|
|
|
|
|
|
|
|
_recursion_stack = set()
|
|
|
|
|
|
|
|
|
|
|
|
class ComparableMixin:
|
|
|
|
def __ne__(self, other):
|
|
|
|
return not self.__eq__(other)
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
"""'Deep, sparse compare.
|
|
|
|
|
|
|
|
Deeply compare two entities, following the non-None attributes of the
|
|
|
|
non-persisted object, if possible.
|
|
|
|
|
|
|
|
"""
|
|
|
|
if other is self:
|
|
|
|
return True
|
|
|
|
elif not self.__class__ == other.__class__:
|
|
|
|
return False
|
|
|
|
|
|
|
|
if id(self) in _recursion_stack:
|
|
|
|
return True
|
|
|
|
_recursion_stack.add(id(self))
|
|
|
|
|
|
|
|
try:
|
|
|
|
# pick the entity that's not SA persisted as the source
|
|
|
|
try:
|
|
|
|
self_key = sa.orm.attributes.instance_state(self).key
|
|
|
|
except sa.orm.exc.NO_STATE:
|
|
|
|
self_key = None
|
|
|
|
|
|
|
|
if other is None:
|
|
|
|
a = self
|
|
|
|
b = other
|
|
|
|
elif self_key is not None:
|
|
|
|
a = other
|
|
|
|
b = self
|
|
|
|
else:
|
|
|
|
a = self
|
|
|
|
b = other
|
|
|
|
|
|
|
|
for attr in list(a.__dict__):
|
|
|
|
if attr.startswith("_"):
|
|
|
|
continue
|
|
|
|
|
|
|
|
value = getattr(a, attr)
|
|
|
|
|
|
|
|
if isinstance(value, WriteOnlyCollection):
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
# handle lazy loader errors
|
|
|
|
battr = getattr(b, attr)
|
|
|
|
except (AttributeError, sa_exc.UnboundExecutionError):
|
|
|
|
return False
|
|
|
|
|
|
|
|
if hasattr(value, "__iter__") and not isinstance(value, str):
|
|
|
|
if hasattr(value, "__getitem__") and not hasattr(
|
|
|
|
value, "keys"
|
|
|
|
):
|
|
|
|
if list(value) != list(battr):
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
if set(value) != set(battr):
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
if value is not None and value != battr:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
finally:
|
|
|
|
_recursion_stack.remove(id(self))
|
|
|
|
|
|
|
|
|
|
|
|
class ComparableEntity(ComparableMixin, BasicEntity):
|
|
|
|
def __hash__(self):
|
|
|
|
return hash(self.__class__)
|