From 6bc1b8369522123b7386dd1262fcbbfb474a633e Mon Sep 17 00:00:00 2001 From: Ido Shraga Date: Fri, 10 Sep 2021 16:28:06 +0300 Subject: [PATCH 1/3] add option to filter by free regex and option to search by whole word --- mongoengine/fields.py | 11 ++++++++-- mongoengine/queryset/transform.py | 4 ++++ tests/queryset/test_queryset.py | 36 ++++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 25bf37ef..fd8236a6 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -157,10 +157,17 @@ class StringField(BaseField): regex = r"%s$" elif op == "exact": regex = r"^%s$" + elif op == "has_word": + regex = r"\b%s\b" + elif op == "regex": + regex = value # escape unsafe characters which could lead to a re.error - value = re.escape(value) - value = re.compile(regex % value, flags) + if op == 'regex': + value = re.compile(regex, flags) + else: + value = re.escape(value) + value = re.compile(regex % value, flags) return super().prepare_query_value(op, value) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 4c3d051b..375cceb4 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -51,6 +51,10 @@ STRING_OPERATORS = ( "iendswith", "exact", "iexact", + "regex", + "iregex", + "has_word", + "ihas_word", ) CUSTOM_OPERATORS = ("match",) MATCH_OPERATORS = ( diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index 1a15a114..cde7dc10 100644 --- a/tests/queryset/test_queryset.py +++ b/tests/queryset/test_queryset.py @@ -1257,6 +1257,35 @@ class TestQueryset(unittest.TestCase): obj = self.Person.objects(name__iexact="gUIDO VAN rOSSU").first() assert obj is None + + # Test has_word + obj = self.Person.objects(name__has_word="Guido").first() + assert obj == person + obj = self.Person.objects(name__has_word="rossum").first() + assert obj is None + obj = self.Person.objects(name__has_word="Rossu").first() + assert obj is None + + # Test ihas_word + obj = self.Person.objects(name__ihas_word="rOSSUM").first() + assert obj == person + obj = self.Person.objects(name__ihas_word="rOSSU").first() + assert obj is None + + # Test regex + obj = self.Person.objects(name__regex="^[Guido].*[Rossum]$").first() + assert obj == person + obj = self.Person.objects(name__regex="^[guido].*[rossum]$").first() + assert obj is None + obj = self.Person.objects(name__regex="^[uido].*[Rossum]$").first() + assert obj is None + + # Test iregex + obj = self.Person.objects(name__iregex="^[guido].*[rossum]$").first() + assert obj == person + obj = self.Person.objects(name__iregex="^[Uido].*[Rossum]$").first() + assert obj is None + # Test unsafe expressions person = self.Person(name="Guido van Rossum [.'Geek']") person.save() @@ -1341,7 +1370,12 @@ class TestQueryset(unittest.TestCase): person.save() people = self.Person.objects - people = people.filter(name__startswith="Gui").filter(name__not__endswith="tum") + people = people.filter(name__startswith="Gui")\ + .filter(name__not__endswith="tum")\ + .filter(name__icontains="VAN")\ + .filter(name__regex="^Guido")\ + .filter(name__has_word="Guido")\ + .filter(name__has_word="van") assert people.count() == 1 def assertSequence(self, qs, expected): From 3d6b6505928e87fd351facdba2622235e1093a95 Mon Sep 17 00:00:00 2001 From: Ido Shraga Date: Sun, 12 Sep 2021 18:21:01 +0300 Subject: [PATCH 2/3] add doc change has_word ==> wholeword --- docs/guide/querying.rst | 4 ++++ mongoengine/fields.py | 11 ++--------- mongoengine/queryset/transform.py | 4 ++-- tests/queryset/test_queryset.py | 18 +++++++++--------- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index e9e1de24..1cf5ec3c 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -86,6 +86,10 @@ expressions: * ``istartswith`` -- string field starts with value (case insensitive) * ``endswith`` -- string field ends with value * ``iendswith`` -- string field ends with value (case insensitive) +* ``wholeword`` -- string field contains whole word +* ``iwholeword`` -- string field contains whole word (case insensitive) +* ``regex`` -- string field match by regex +* ``iregex`` -- string field match by regex (case insensitive) * ``match`` -- performs an $elemMatch so you can match an entire document within an array diff --git a/mongoengine/fields.py b/mongoengine/fields.py index fd8236a6..11c32f84 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -157,7 +157,7 @@ class StringField(BaseField): regex = r"%s$" elif op == "exact": regex = r"^%s$" - elif op == "has_word": + elif op == "wholeword": regex = r"\b%s\b" elif op == "regex": regex = value @@ -1094,14 +1094,7 @@ class DictField(ComplexBaseField): def prepare_query_value(self, op, value): match_operators = [ - "contains", - "icontains", - "startswith", - "istartswith", - "endswith", - "iendswith", - "exact", - "iexact", + *STRING_OPERATORS ] if op in match_operators and isinstance(value, str): diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 375cceb4..cea2ef4c 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -53,8 +53,8 @@ STRING_OPERATORS = ( "iexact", "regex", "iregex", - "has_word", - "ihas_word", + "wholeword", + "iwholeword", ) CUSTOM_OPERATORS = ("match",) MATCH_OPERATORS = ( diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index cde7dc10..859c4cb5 100644 --- a/tests/queryset/test_queryset.py +++ b/tests/queryset/test_queryset.py @@ -1258,18 +1258,18 @@ class TestQueryset(unittest.TestCase): assert obj is None - # Test has_word - obj = self.Person.objects(name__has_word="Guido").first() + # Test wholeword + obj = self.Person.objects(name__wholeword="Guido").first() assert obj == person - obj = self.Person.objects(name__has_word="rossum").first() + obj = self.Person.objects(name__wholeword="rossum").first() assert obj is None - obj = self.Person.objects(name__has_word="Rossu").first() + obj = self.Person.objects(name__wholeword="Rossu").first() assert obj is None - # Test ihas_word - obj = self.Person.objects(name__ihas_word="rOSSUM").first() + # Test iwholeword + obj = self.Person.objects(name__iwholeword="rOSSUM").first() assert obj == person - obj = self.Person.objects(name__ihas_word="rOSSU").first() + obj = self.Person.objects(name__iwholeword="rOSSU").first() assert obj is None # Test regex @@ -1374,8 +1374,8 @@ class TestQueryset(unittest.TestCase): .filter(name__not__endswith="tum")\ .filter(name__icontains="VAN")\ .filter(name__regex="^Guido")\ - .filter(name__has_word="Guido")\ - .filter(name__has_word="van") + .filter(name__wholeword="Guido")\ + .filter(name__wholeword="van") assert people.count() == 1 def assertSequence(self, qs, expected): From 7a0a58c163449f658389b512824ff68d647ffb6c Mon Sep 17 00:00:00 2001 From: Ido Shraga Date: Fri, 1 Oct 2021 07:48:01 +0300 Subject: [PATCH 3/3] black reformat --- mongoengine/fields.py | 6 ++---- tests/queryset/test_queryset.py | 27 +++++++++++++-------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 11c32f84..b9c49553 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -163,7 +163,7 @@ class StringField(BaseField): regex = value # escape unsafe characters which could lead to a re.error - if op == 'regex': + if op == "regex": value = re.compile(regex, flags) else: value = re.escape(value) @@ -1093,9 +1093,7 @@ class DictField(ComplexBaseField): return DictField(db_field=member_name) def prepare_query_value(self, op, value): - match_operators = [ - *STRING_OPERATORS - ] + match_operators = [*STRING_OPERATORS] if op in match_operators and isinstance(value, str): return StringField().prepare_query_value(op, value) diff --git a/tests/queryset/test_queryset.py b/tests/queryset/test_queryset.py index 859c4cb5..9a2f2862 100644 --- a/tests/queryset/test_queryset.py +++ b/tests/queryset/test_queryset.py @@ -872,17 +872,15 @@ class TestQueryset(unittest.TestCase): self.Person.objects.create(name="Foo", age=11) bob = self.Person.objects.as_pymongo().first() - assert 'age' in bob - assert bob['age'] == 11 + assert "age" in bob + assert bob["age"] == 11 - self.Person.objects(name="Foo").update( - rename__age='person_age' - ) + self.Person.objects(name="Foo").update(rename__age="person_age") bob = self.Person.objects.as_pymongo().first() - assert 'age' not in bob - assert 'person_age' in bob - assert bob['person_age'] == 11 + assert "age" not in bob + assert "person_age" in bob + assert bob["person_age"] == 11 def test_save_and_only_on_fields_with_default(self): class Embed(EmbeddedDocument): @@ -1257,7 +1255,6 @@ class TestQueryset(unittest.TestCase): obj = self.Person.objects(name__iexact="gUIDO VAN rOSSU").first() assert obj is None - # Test wholeword obj = self.Person.objects(name__wholeword="Guido").first() assert obj == person @@ -1370,12 +1367,14 @@ class TestQueryset(unittest.TestCase): person.save() people = self.Person.objects - people = people.filter(name__startswith="Gui")\ - .filter(name__not__endswith="tum")\ - .filter(name__icontains="VAN")\ - .filter(name__regex="^Guido")\ - .filter(name__wholeword="Guido")\ + people = ( + people.filter(name__startswith="Gui") + .filter(name__not__endswith="tum") + .filter(name__icontains="VAN") + .filter(name__regex="^Guido") + .filter(name__wholeword="Guido") .filter(name__wholeword="van") + ) assert people.count() == 1 def assertSequence(self, qs, expected):