Skip to content

Commit

Permalink
Updated comparison semantics - passing all tests
Browse files Browse the repository at this point in the history
  • Loading branch information
daveraja committed May 6, 2024
1 parent fd30945 commit 2f0dc21
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 15 deletions.
24 changes: 13 additions & 11 deletions clorm/orm/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,14 @@ def hashable(self):
def is_leaf(self):
return not hasattr(self, "_predicate_class")

# --------------------------------------------------------------------------
# If the leaf of the path is a Predicate class then return it else None
# --------------------------------------------------------------------------
@property
def complex(self):
fld = self._parent._get_field()
return None if fld is None else fld.complex

# --------------------------------------------------------------------------
# attrgetter
# --------------------------------------------------------------------------
Expand Down Expand Up @@ -2453,10 +2461,7 @@ def get_field_definition(defn: Any, module: str = "") -> BaseField:
def _create_complex_term(defn: Any, default_value: Any = MISSING, module: str = "") -> BaseField:
# NOTE: relies on a dict preserving insertion order - this is true from Python 3.7+. Python
# 3.7 is already end-of-life so there is no longer a reason to use OrderedDict.
#proto = {f"arg{idx+1}": get_field_definition(dn) for idx, dn in enumerate(defn)}
proto: Dict[str, Any] = collections.OrderedDict(
[(f"arg{i+1}", get_field_definition(d, module)) for i, d in enumerate(defn)]
)
proto = {f"arg{idx+1}": get_field_definition(dn) for idx, dn in enumerate(defn)}
class_name = (
f'ClormAnonTuple({",".join(f"{arg[0]}={repr(arg[1])}" for arg in proto.items())})'
)
Expand Down Expand Up @@ -3027,9 +3032,8 @@ def _make_predicatedefn(

reserved = set(["meta", "raw", "clone", "sign", "Field"])

# Generate the fields - NOTE: this relies on dct being an OrderedDict()
# which is true from Python 3.5+ (see PEP520
# https://www.python.org/dev/peps/pep-0520/)
# Generate the fields - NOTE: this relies on dict being an ordered which is true from
# Python 3.5+ (see PEP520 https://www.python.org/dev/peps/pep-0520/)

# Predicates (or complexterms) that are defined within the current Predicate context may be
# necessary to help resolve the postponed annotations.
Expand Down Expand Up @@ -3527,10 +3531,8 @@ def simple_predicate(
"""
subclass_name = name if name else "AnonSimplePredicate"

# Use an OrderedDict to ensure the correct order of the field arguments
proto: Dict[str, Any] = collections.OrderedDict(
[("arg{}".format(i + 1), RawField()) for i in range(0, arity)]
)
# Note: dict is preserves the ordering
proto: Dict[str, Any] = {f"arg{i+1}": RawField() for i in range(0, arity)}
proto["Meta"] = type(
"Meta", (object,), {"name": predicate_name, "is_tuple": False, "_anon": True}
)
Expand Down
25 changes: 23 additions & 2 deletions clorm/orm/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,27 @@ def membership_op_keyable(sc, indexes):
# is not exposed outside clorm.
# ------------------------------------------------------------------------------

# Helper function to try to convert Python tuples into a matching clorm anon tuple. With this
# we can pass a Python tuple to a query and not have to overload the Predicate comparison
# operator and hash function to match the tuple.
def _try_to_convert_tuple_argument_to_clorm_tuple(op, args):
def _try_to_convert(tup, possiblepath):
if not isinstance(possiblepath, PredicatePath):
return tup
complex = possiblepath.meta.complex
if complex is None or not complex.meta.is_tuple:
return tup
return complex(*tup)

if op not in {operator.eq, operator.ne, operator.lt, operator.le, operator.gt, operator.ge}:
return args
if len(args) != 2:
return args
if isinstance(args[0], tuple):
return (_try_to_convert(args[0], args[1]), args[1])
if isinstance(args[1], tuple):
return (args[0], _try_to_convert(args[1], args[0]))
return args

class StandardComparator(Comparator):
class Preference(enum.IntEnum):
Expand Down Expand Up @@ -625,7 +646,7 @@ def __init__(self, operator, args):
).format(operator)
)
self._operator = operator
self._args = tuple(args)
self._args = tuple(_try_to_convert_tuple_argument_to_clorm_tuple(operator, args))
self._hashableargs = tuple(
[hashable_path(a) if isinstance(a, PredicatePath) else a for a in self._args]
)
Expand Down Expand Up @@ -789,7 +810,7 @@ def swap(self):
self._operator
)
)
return StandardComparator(spec.swapop, reversed(self._args))
return StandardComparator(spec.swapop, tuple(reversed(self._args)))

def keyable(self, indexes):
spec = StandardComparator.operators[self._operator]
Expand Down
28 changes: 28 additions & 0 deletions tests/test_orm_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,34 @@ class T(Predicate):
self.assertTrue(p1.tuple_ != tuple1)
self.assertFalse(tuple(p1.tuple_) != tuple1)


# --------------------------------------------------------------------------
# Testing comparison between tuple fields where the corresponding python tuples
# can contain incomparible objects.
# --------------------------------------------------------------------------
def test_predicate_with_incomparable_python_tuples(self):
class P(Predicate):
tuple_ = field((SimpleField, SimpleField))

class Q(Predicate):
tuple_ = field((IntegerField, IntegerField))

ptuple = P.tuple_.meta.complex
qtuple = Q.tuple_.meta.complex

self.assertTrue(ptuple(1, 2) == qtuple(1, 2))
self.assertTrue(ptuple("a", "b") != qtuple(1, 2))

# NOTE: the following throws a TypeError when comparing two Python tuples but works
# with clorm tuples.
# error: self.assertTrue(("a", "b") > (1,2))
self.assertTrue(ptuple("a", "b") > qtuple(1, 2))
self.assertTrue(ptuple("a", "b") >= qtuple(1, 2))
self.assertFalse(ptuple("a", "b") < qtuple(1, 2))
self.assertFalse(ptuple("a", "b") <= qtuple(1, 2))



# --------------------------------------------------------------------------
# Test predicates with default fields
# --------------------------------------------------------------------------
Expand Down
98 changes: 96 additions & 2 deletions tests/test_orm_factbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
ConstantField,
FactBase,
IntegerField,
SimpleField,
Predicate,
StringField,
alias,
Expand All @@ -27,6 +28,7 @@
func,
hashable_path,
in_,
notin_,
path,
ph1_,
ph2_,
Expand All @@ -47,6 +49,7 @@
"FactBaseTestCase",
"QueryAPI1TestCase",
"QueryAPI2TestCase",
"QueryWithTupleTestCase",
"SelectJoinTestCase",
"MembershipQueriesTestCase",
"FactBasePicklingTestCase",
Expand Down Expand Up @@ -1284,8 +1287,7 @@ class Fact(Predicate):
self.assertEqual(list(s1.get(20)), [f2, f1])
self.assertEqual(list(s2.get(CT(20, "b"))), [f2])

# NOTE: Important test as it requires tuple complex terms to have the
# same hash as the corresponding python tuple.
# NOTE: This requires Python tuple to be converted to a clorm tuple.
self.assertEqual(list(s3.get((1, 2))), [f2])
self.assertEqual(list(s4.get((2, 1))), [f2])

Expand Down Expand Up @@ -1697,6 +1699,98 @@ def test_api_bad_join_statement(self):
check_errmsg("A query over multiple predicates is incomplete", ctx)


# ------------------------------------------------------------------------------------------
# Test some special cases involving passing a Python tuple instead of using the clorm tuple.
# ------------------------------------------------------------------------------------------

class QueryWithTupleTestCase(unittest.TestCase):
def setUp(self):
class F(Predicate):
atuple = field((IntegerField, StringField))
other = StringField

class G(Predicate):
atuple = field((SimpleField, StringField))
other = StringField

self.F = F
self.G = G

self.factbase = FactBase(
[
F((1, "a"), "i"), F((2, "b"), "j"), F((3, "a"), "k"),
G(("a", "a"), "x"), G((2, "b"), "y"), G(("e", "e"), "z")
]
)


# --------------------------------------------------------------------------
# Some tuple predicate instance comparisons
# --------------------------------------------------------------------------
def test_basic_clorm_tuple_in_comparison(self):
G = self.G
fb = self.factbase
cltuple = G.atuple.meta.complex
expected = [G((2, "b"), "y"), G(("a", "a"), "x"), ]

# NOTE: the version with python tuples fails to find the matching tuples because we can
# no longer directly compare python tuples with clorm tuples.

pytuples = {("a", "a"), (2, "b")}
q1 = fb.query(G).where(in_(G.atuple, pytuples)).ordered()
self.assertEqual(list(q1.all()), [])
# this fails: self.assertEqual(list(q1.all()), expected)

cltuples = {cltuple("a", "a"), cltuple(2, "b")}
q2 = fb.query(G).where(in_(G.atuple, cltuples)).ordered()
self.assertEqual(list(q2.all()), expected)


# --------------------------------------------------------------------------
# Complex query where the join is on a tuple object
# --------------------------------------------------------------------------
def test_api_join_on_clorm_tuple(self):
F = self.F
G = self.G
fb = self.factbase

# Select everything with an equality join
q = fb.query(G, F).join(F.atuple == G.atuple).where(F.other == "j")
expected = [(G((2, "b"), "y"), F((2, "b"), "j"))]
self.assertEqual(list(q.all()), expected)


# --------------------------------------------------------------------------
# Complex query with a where containing a tuple
# --------------------------------------------------------------------------
def test_api_where_with_python_tuple(self):
F = self.F
fb = self.factbase

# The Python tuple passed in the where clause
q = fb.query(F).where(F.atuple == (2, "b"))
expected = [F((2, "b"), "j")]
self.assertEqual(list(q.all()), expected)

# The Python tuple passed in the bind
q = fb.query(F).where(F.atuple == ph1_).bind((2, "b"))
expected = [F((2, "b"), "j")]
self.assertEqual(list(q.all()), expected)

# --------------------------------------------------------------------------
# Complex query sorting on Clorm tuple where the corresponding Python tuple
# is incomparable.
# --------------------------------------------------------------------------
def test_api_sorting_with_incomparable_elements(self):
G = self.G
fb = self.factbase

q = fb.query(G).order_by(G.atuple)
expected = [
G((2, "b"), "y"), G(("a", "a"), "x"), G(("e", "e"), "z")
]
self.assertEqual(list(q.all()), expected)

# ------------------------------------------------------------------------------
# Tests for additional V2 select join statements
# ------------------------------------------------------------------------------
Expand Down

0 comments on commit 2f0dc21

Please sign in to comment.