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.


Comments:

Patch:
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 90ac8ed6e1..4c36a521dc 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -54,6 +54,7 @@ Changelog
  * Maintenance: Upgrade Willow to v1.6.2 to support MIME type data without reliance on `imghdr` (Jake Howard)
  * Maintenance: Replace `imghdr` with Willow's built-in MIME type detection (Jake Howard)
  * Maintenance: Migrate all other `data-tippy` HTML attribute usage to the Stimulus data-*-value attributes for w-tooltip & w-dropdown (Subhajit Ghosh, LB (Ben) Johnston)
+ * Maintenance: Replace `@total_ordering` usage with comparison functions implementation (Virag Jain)
 
 
 5.1.2 (xx.xx.20xx) - IN DEVELOPMENT
diff --git a/docs/releases/5.2.md b/docs/releases/5.2.md
index aa30a690fa..f411817951 100644
--- a/docs/releases/5.2.md
+++ b/docs/releases/5.2.md
@@ -73,6 +73,7 @@ depth: 1
  * Upgrade Willow to v1.6.2 to support MIME type data without reliance on `imghdr` (Jake Howard)
  * Replace `imghdr` with Willow's built-in MIME type detection (Jake Howard)
  * Migrate all other `data-tippy` HTML attribute usage to the Stimulus data-*-value attributes for w-tooltip & w-dropdown (Subhajit Ghosh, LB (Ben) Johnston)
+ * Replace `@total_ordering` usage with comparison functions implementation (Virag Jain)
 
 
 ## Upgrade considerations - changes affecting all projects
diff --git a/wagtail/admin/search.py b/wagtail/admin/search.py
index 1c666549c2..6f5c338175 100644
--- 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 import hooks
 from wagtail.admin.forms.search import SearchForm
 
 
-@total_ordering
 class SearchArea(metaclass=MediaDefiningClass):
     template = "wagtailadmin/shared/search_area.html"
 
@@ -31,9 +28,28 @@ class SearchArea(metaclass=MediaDefiningClass):
             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
index 5f904f1926..7cc361dbbe 100644
--- 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 django.utils.html import format_html
 from wagtail import hooks
 
 
-@total_ordering
 class Button:
     show = True
 
@@ -42,6 +39,21 @@ class Button:
             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
index 2f5594ec69..c6e23092e3 100644
--- a/wagtail/admin/tests/test_admin_search.py
+++ b/wagtail/admin/tests/test_admin_search.py
@@ -3,10 +3,11 @@ Tests for the search box in the admin side menu, and the custom search hooks.
 """
 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 @@ class TestSearchAreaNoPagePermissions(BaseSearchAreaTestCase):
 
         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
index 04b8a39733..820b34816a 100644
--- 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 @@ class TestPageHeaderButtonsHooks(TestButtonsHooks):
         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)