Repository: readthedocs/readthedocs.org

Issue:
Addons: refactor flyout sorting
`sort_versions_python_packaging` and `sort_versions_custom_pattern` functions share most of their code. The only part that differs is what's inside try/catch. Here is the code involved:

https://github.com/readthedocs/readthedocs.org/blob/f0c74a644cc1e18c04ae11a31c9fbef01b8ca073/readthedocs/projects/version_handling.py#L226-L272

Reference https://github.com/readthedocs/readthedocs.org/pull/11069#discussion_r1520128348


Discussion:
I jumped into this issue and I'm still not sure what's the best way to refactor this code. Basically, the only part we need to change is the `try/except` clause. All the rest is identical.

On that `try/except` clause the different parts are:
- the exception that needs to be handled
- the second argument required to generate the tuple

Would it be enough to isolate all this code in a function that receives `exception=` and `parse_version=`? 🤔 

Resolving patch:
diff --git a/readthedocs/projects/version_handling.py b/readthedocs/projects/version_handling.py
--- a/readthedocs/projects/version_handling.py
+++ b/readthedocs/projects/version_handling.py
@@ -169,23 +169,27 @@ def determine_stable_version(version_list):
     return None
 
 
-def sort_versions_python_packaging(version_list, latest_stable_at_beginning):
+def sort_versions_generic(
+    version_list,
+    exception,
+    parse_version,
+    latest_stable_at_beginning,
+    raw_pattern=None,
+):
     """
-    Sort Read the Docs versions list using ``packaging`` algorithm.
+    Sort Read the Docs versions based on ``parse_version`` function.
 
-    All the invalid version (raise ``InvalidVersion``) are added at the end
-    sorted alphabetically.
+    ``parse_version`` function is called with ``slug`` and ``raw_pattern`` as arguments to decide how to sort them.
 
-    https://pypi.org/project/packaging/
-    https://packaging.python.org/en/latest/specifications/version-specifiers/
+    All versions that raise ``exception`` are added at the end sorted alphabetically.
     """
+
     alphabetically_sorted_version_list = sorted(
         version_list,
         key=operator.attrgetter("slug"),
     )
 
     initial_versions = []
-
     valid_versions = []
     invalid_versions = []
     for i, version in enumerate(alphabetically_sorted_version_list):
@@ -196,8 +200,16 @@ def sort_versions_python_packaging(version_list, latest_stable_at_beginning):
                 continue
 
         try:
-            valid_versions.append((version, Version(version.slug)))
-        except InvalidVersion:
+            valid_versions.append(
+                (
+                    version,
+                    parse_version(
+                        slug=version.slug,
+                        raw_pattern=raw_pattern,
+                    ),
+                )
+            )
+        except exception:
             # When the version is invalid, we put it at the end while keeping
             # the alphabetically sorting between the invalid ones.
             invalid_versions.append((version, None))
@@ -207,19 +219,28 @@ def sort_versions_python_packaging(version_list, latest_stable_at_beginning):
         + sorted(valid_versions, key=operator.itemgetter(1), reverse=True)
         + invalid_versions
     )
-
     return [item[0] for item in all_versions if item[0] is not None]
 
 
-def sort_versions_calver(version_list, latest_stable_at_beginning):
+def sort_versions_python_packaging(version_list, latest_stable_at_beginning):
     """
-    Sort Read the Docs versions using CalVer pattern: ``YYYY.0M.0M``.
+    Sort Read the Docs versions list using ``packaging`` algorithm.
 
-    All the invalid version are added at the end sorted alphabetically.
+    All the invalid version (raise ``InvalidVersion``) are added at the end
+    sorted alphabetically.
+
+    https://pypi.org/project/packaging/
+    https://packaging.python.org/en/latest/specifications/version-specifiers/
     """
-    raw_pattern = "YYYY.0M.0D"
-    return sort_versions_custom_pattern(
-        version_list, raw_pattern, latest_stable_at_beginning
+
+    def parse_version(*args, slug=None, **kwargs):
+        return Version(slug)
+
+    return sort_versions_generic(
+        version_list,
+        InvalidVersion,
+        parse_version,
+        latest_stable_at_beginning,
     )
 
 
@@ -233,40 +254,28 @@ def sort_versions_custom_pattern(version_list, raw_pattern, latest_stable_at_beg
     It uses ``Bumpver`` behinds the scenes for the parsing and sorting.
     https://github.com/mbarkhau/bumpver
     """
-    alphabetically_sorted_version_list = sorted(
+
+    def parse_version(*args, slug=None, raw_pattern=None, **kwargs):
+        return parse_version_info(slug, raw_pattern=raw_pattern)
+
+    return sort_versions_generic(
         version_list,
-        key=operator.attrgetter("slug"),
+        PatternError,
+        parse_version,
+        latest_stable_at_beginning,
+        raw_pattern,
     )
 
-    initial_versions = []
-    valid_versions = []
-    invalid_versions = []
-    for i, version in enumerate(alphabetically_sorted_version_list):
-        if latest_stable_at_beginning:
-            if version.slug in (STABLE, LATEST):
-                # It relies on the version list sorted alphabetically first ("l" comes first than "s")
-                initial_versions.append((version, version.slug))
-                continue
 
-        try:
-            valid_versions.append(
-                (
-                    version,
-                    parse_version_info(
-                        version.slug,
-                        raw_pattern=raw_pattern,
-                    ),
-                )
-            )
-        except PatternError:
-            # When the version is invalid, we put it at the end while keeping
-            # the alphabetically sorting between the invalid ones.
-            invalid_versions.append((version, None))
+def sort_versions_calver(version_list, latest_stable_at_beginning):
+    """
+    Sort Read the Docs versions using CalVer pattern: ``YYYY.0M.0M``.
 
-    all_versions = (
-        initial_versions
-        + sorted(valid_versions, key=operator.itemgetter(1), reverse=True)
-        + invalid_versions
+    All the invalid version are added at the end sorted alphabetically.
+    """
+    raw_pattern = "YYYY.0M.0D"
+    return sort_versions_custom_pattern(
+        version_list,
+        raw_pattern,
+        latest_stable_at_beginning,
     )
-
-    return [item[0] for item in all_versions if item[0] is not None]


Test patch: