Repository: wagtail/wagtail

Issue:
Replace `total_ordering` usage with comparison functions implementation
### Is your proposal related to a problem?

We have two instances of `total_ordering` usage within the codebase:

https://github.com/wagtail/wagtail/blob/cd5200c8e1ac0d7299fd9c398b2b994606b3c7d2/wagtail/admin/search.py#L12-L13

https://github.com/wagtail/wagtail/blob/cd5200c8e1ac0d7299fd9c398b2b994606b3c7d2/wagtail/admin/widgets/button.py#L11-L12

Even though it's convenient, `total_ordering` is known to be slow. According to [Python's docs](https://docs.python.org/3/library/functools.html#functools.total_ordering):

> **Note**
> While this decorator makes it easy to create well behaved totally ordered types, it does come at the cost of slower execution and more complex stack traces for the derived comparison methods. If performance benchmarking indicates this is a bottleneck for a given application, implementing all six rich comparison methods instead is likely to provide an easy speed boost.

Django recently removed their usage of `total_ordering` in https://github.com/django/django/pull/16958/commits/ee36e101e8f8c0acde4bb148b738ab7034e902a0 (probably not all usages, I haven't checked).

### Describe the solution you'd like

<!--
  Provide a clear and concise description of what you want to happen.
-->

Replace `total_ordering` with implementations of `__eq__()`, `__ne__()`, `__lt__()`, `__le__()`, `__gt__()`, and `__ge__()`.

### Describe alternatives you've considered

<!--
  Let us know about other solutions you've tried or researched.
-->

Keep using `total_ordering`

### Additional context

I found this while fixing an incorrect import of `total_ordering` in #10525.



Discussion:
I am pretty sure I can contribute to solving this issue
Hi @laymonage, I tried implementing the functions and have linked a PR. Please let me know if anything's wrong, Thanks in advance :)
Thanks @WhatIsThisVJ! I see you've made the change on your own fork of Wagtail, but it looks like you haven't actually opened a PR yet to get it merged into Wagtail itself - you'll need to do that from https://github.com/wagtail/wagtail/pulls.

Resolving patch:
diff --git a/wagtail/admin/search.py b/wagtail/admin/search.py
--- a/wagtail/admin/search.py
+++ b/wagtail/admin/search.py
@@ -1,5 +1,3 @@
-from functools import total_ordering
-
 from django.forms import Media, MediaDefiningClass
 from django.forms.utils import flatatt
 from django.template.loader import render_to_string
@@ -11,7 +9,6 @@
 from wagtail.admin.forms.search import SearchForm
 
 
-@total_ordering
 class SearchArea(metaclass=MediaDefiningClass):
     template = "wagtailadmin/shared/search_area.html"
 
@@ -31,9 +28,28 @@ def __init__(
             self.attr_string = ""
 
     def __lt__(self, other):
+        if not isinstance(other, SearchArea):
+            return NotImplemented
         return (self.order, self.label) < (other.order, other.label)
 
+    def __le__(self, other):
+        if not isinstance(other, SearchArea):
+            return NotImplemented
+        return (self.order, self.label) <= (other.order, other.label)
+
+    def __gt__(self, other):
+        if not isinstance(other, SearchArea):
+            return NotImplemented
+        return (self.order, self.label) > (other.order, other.label)
+
+    def __ge__(self, other):
+        if not isinstance(other, SearchArea):
+            return NotImplemented
+        return (self.order, self.label) >= (other.order, other.label)
+
     def __eq__(self, other):
+        if not isinstance(other, SearchArea):
+            return NotImplemented
         return (self.order, self.label) == (other.order, other.label)
 
     def is_shown(self, request):
diff --git a/wagtail/admin/widgets/button.py b/wagtail/admin/widgets/button.py
--- a/wagtail/admin/widgets/button.py
+++ b/wagtail/admin/widgets/button.py
@@ -1,5 +1,3 @@
-from functools import total_ordering
-
 from django.forms.utils import flatatt
 from django.template.loader import render_to_string
 from django.utils.functional import cached_property
@@ -8,7 +6,6 @@
 from wagtail import hooks
 
 
-@total_ordering
 class Button:
     show = True
 
@@ -42,6 +39,21 @@ def __lt__(self, other):
             return NotImplemented
         return (self.priority, self.label) < (other.priority, other.label)
 
+    def __le__(self, other):
+        if not isinstance(other, Button):
+            return NotImplemented
+        return (self.priority, self.label) <= (other.priority, other.label)
+
+    def __gt__(self, other):
+        if not isinstance(other, Button):
+            return NotImplemented
+        return (self.priority, self.label) > (other.priority, other.label)
+
+    def __ge__(self, other):
+        if not isinstance(other, Button):
+            return NotImplemented
+        return (self.priority, self.label) >= (other.priority, other.label)
+
     def __eq__(self, other):
         if not isinstance(other, Button):
             return NotImplemented


Test patch:
diff --git a/wagtail/admin/tests/test_admin_search.py b/wagtail/admin/tests/test_admin_search.py
--- a/wagtail/admin/tests/test_admin_search.py
+++ b/wagtail/admin/tests/test_admin_search.py
@@ -3,10 +3,11 @@
 """
 from django.contrib.auth.models import Permission
 from django.template import Context, Template
-from django.test import RequestFactory, TestCase
+from django.test import RequestFactory, SimpleTestCase, TestCase
 from django.urls import reverse
 
 from wagtail.admin.auth import user_has_any_page_permission
+from wagtail.admin.search import SearchArea
 from wagtail.test.utils import WagtailTestUtils
 
 
@@ -107,3 +108,91 @@ def test_search_other(self):
 
         self.assertNotIn("Pages", rendered)
         self.assertIn("My Search", rendered)
+
+
+class SearchAreaComparisonTestCase(SimpleTestCase):
+    """Tests the comparison functions."""
+
+    def setUp(self):
+        self.search_area1 = SearchArea("Label 1", "/url1", order=100)
+        self.search_area2 = SearchArea("Label 2", "/url2", order=200)
+        self.search_area3 = SearchArea("Label 1", "/url3", order=300)
+        self.search_area4 = SearchArea("Label 1", "/url1", order=100)
+
+    def test_eq(self):
+        # Same label and order, should be equal
+        self.assertTrue(self.search_area1 == self.search_area4)
+
+        # Different order, should not be equal
+        self.assertFalse(self.search_area1 == self.search_area2)
+
+        # Not a SearchArea, should not be equal
+        self.assertFalse(self.search_area1 == "Something")
+
+    def test_lt(self):
+        # Less order, should be True
+        self.assertTrue(self.search_area1 < self.search_area2)
+
+        # Same label, but less order, should be True
+        self.assertTrue(self.search_area1 < self.search_area3)
+
+        # Greater order, should be False
+        self.assertFalse(self.search_area2 < self.search_area1)
+
+        # Not a SearchArea, should raise TypeError
+        with self.assertRaises(TypeError):
+            self.search_area1 < "Something"
+
+    def test_le(self):
+        # Less order, should be True
+        self.assertTrue(self.search_area1 <= self.search_area2)
+
+        # Same label, but less order, should be True
+        self.assertTrue(self.search_area1 <= self.search_area3)
+
+        # Same object, should be True
+        self.assertTrue(self.search_area1 <= self.search_area1)
+
+        # Same label and order, should be True
+        self.assertTrue(self.search_area1 <= self.search_area4)
+
+        # Greater order, should be False
+        self.assertFalse(self.search_area2 <= self.search_area1)
+
+        # Not a SearchArea, should raise TypeError
+        with self.assertRaises(TypeError):
+            self.search_area1 <= "Something"
+
+    def test_gt(self):
+        # Greater order, should be True
+        self.assertTrue(self.search_area2 > self.search_area1)
+
+        # Same label, but greater order, should be True
+        self.assertTrue(self.search_area3 > self.search_area1)
+
+        # Less order, should be False
+        self.assertFalse(self.search_area1 > self.search_area2)
+
+        # Not a SearchArea, should raise TypeError
+        with self.assertRaises(TypeError):
+            self.search_area1 > "Something"
+
+    def test_ge(self):
+        # Greater order, should be True
+        self.assertTrue(self.search_area2 >= self.search_area1)
+
+        # Same label, but greater order, should be True
+        self.assertTrue(self.search_area3 >= self.search_area1)
+
+        # Same object, should be True
+        self.assertTrue(self.search_area1 >= self.search_area1)
+
+        # Same label and order, should be True
+        self.assertTrue(self.search_area1 >= self.search_area4)
+
+        # Less order, should be False
+        self.assertFalse(self.search_area1 >= self.search_area2)
+
+        # Not a SearchArea, should raise TypeError
+        with self.assertRaises(TypeError):
+            self.search_area1 >= "Something"
diff --git a/wagtail/admin/tests/test_buttons_hooks.py b/wagtail/admin/tests/test_buttons_hooks.py
--- a/wagtail/admin/tests/test_buttons_hooks.py
+++ b/wagtail/admin/tests/test_buttons_hooks.py
@@ -1,10 +1,11 @@
-from django.test import TestCase
+from django.test import SimpleTestCase, TestCase
 from django.urls import reverse
 from django.utils.http import urlencode
 
 from wagtail import hooks
 from wagtail.admin import widgets as wagtailadmin_widgets
 from wagtail.admin.wagtail_hooks import page_header_buttons, page_listing_more_buttons
+from wagtail.admin.widgets.button import Button
 from wagtail.models import Page
 from wagtail.test.testapp.models import SimplePage
 from wagtail.test.utils import WagtailTestUtils
@@ -293,3 +294,98 @@ def test_delete_button_with_invalid_next_url(self):
         unpublish_button = next(buttons)
         full_url = unpublish_base_url + "?" + urlencode({"next": next_url})
         self.assertEqual(unpublish_button.url, full_url)
+
+
+class ButtonComparisonTestCase(SimpleTestCase):
+    """Tests the comparison functions."""
+
+    def setUp(self):
+        self.button1 = Button(
+            "Label 1", "/url1", classes={"class1", "class2"}, priority=100
+        )
+        self.button2 = Button(
+            "Label 2", "/url2", classes={"class2", "class3"}, priority=200
+        )
+        self.button3 = Button(
+            "Label 1", "/url3", classes={"class1", "class2"}, priority=300
+        )
+        self.button4 = Button(
+            "Label 1", "/url1", classes={"class1", "class2"}, priority=100
+        )
+
+    def test_eq(self):
+        # Same properties, should be equal
+        self.assertTrue(self.button1 == self.button4)
+
+        # Different priority, should not be equal
+        self.assertFalse(self.button1 == self.button2)
+
+        # Different URL, should not be equal
+        self.assertFalse(self.button1 == self.button3)
+
+        # Not a Button, should not be equal
+        self.assertFalse(self.button1 == "Something")
+
+    def test_lt(self):
+        # Less priority, should be True
+        self.assertTrue(self.button1 < self.button2)
+
+        # Same label, but less priority, should be True
+        self.assertTrue(self.button1 < self.button3)
+
+        # Greater priority, should be False
+        self.assertFalse(self.button2 < self.button1)
+
+        # Not a Button, should raise TypeError
+        with self.assertRaises(TypeError):
+            self.button1 < "Something"
+
+    def test_le(self):
+        # Less priority, should be True
+        self.assertTrue(self.button1 <= self.button2)
+
+        # Same label, but less priority, should be True
+        self.assertTrue(self.button1 <= self.button3)
+
+        # Same object, should be True
+        self.assertTrue(self.button1 <= self.button1)
+
+        # Same label and priority, should be True
+        self.assertTrue(self.button1 <= self.button4)
+
+        # Greater priority, should be False
+        self.assertFalse(self.button2 <= self.button1)
+
+        # Not a Button, should raise TypeError
+        with self.assertRaises(TypeError):
+            self.button1 <= "Something"
+
+    def test_gt(self):
+        # Greater priority, should be True
+        self.assertTrue(self.button2 > self.button1)
+
+        # Same label, but greater priority, should be True
+        self.assertTrue(self.button3 > self.button1)
+
+        # Less priority, should be False
+        self.assertFalse(self.button1 > self.button2)
+
+        # Not a Button, should raise TypeError
+        with self.assertRaises(TypeError):
+            self.button1 > "Something"
+
+    def test_ge(self):
+        # Greater priority, should be True
+        self.assertTrue(self.button2 >= self.button1)
+
+        # Same label, but greater priority, should be True
+        self.assertTrue(self.button3 >= self.button1)
+
+        # Same object, should be True
+        self.assertTrue(self.button1 >= self.button1)
+
+        # Same label and priority, should be True
+        self.assertTrue(self.button1 >= self.button4)
+
+        # Less priority, should be False
+        self.assertFalse(self.button1 >= self.button2)