From: Simon McVittie <smcv@collabora.com>
Date: Wed, 18 May 2022 18:17:01 +0100
Subject: Handle SDL 2.23+ new versioning scheme (#230)

* test: Add more realistic tests of SDL_VERSIONNUM

The minor version overflows into the thousands digit in 2.23.0 and up.
Because the major version is fixed at 2 until SDL 3 (which will be a
separate library, like the difference between SDL 1.2 and 2), the
numbers from here can still be used as a sort order, but are now a bit
more confusing than they used to be.

Signed-off-by: Simon McVittie <smcv@collabora.com>

* test: Avoid assertions that will break in upcoming versions

Future SDL 2 versions are going to make more use of the minor version,
so SDL 2.0.22, SDL_image 2.0.5, SDL_mixer 2.0.4 and SDL_ttf 2.0.18 will
be followed by 2.24.0, 2.6.0, 2.6.0 and 2.20.0 respectively.

This means assertions like version.patch >= 5 will no longer be true, so
compare the entire version number as a tuple instead.

Resolves: https://github.com/py-sdl/py-sdl2/issues/228
Signed-off-by: Simon McVittie <smcv@collabora.com>

* dll: Represent version number as a tuple, similar to sys.version_info

This avoids artificially limiting the range of possible version numbers
that will sort correctly.

For developer convenience, leave dll.version and dll.DLL.version
working in terms of small integers, because comparisons with those are
less verbose.

Signed-off-by: Simon McVittie <smcv@collabora.com>
Origin: backport, 0.9.12, commit:2986e60ff33df202c0b6ce6d2f1ae03a52d0098e
---
 sdl2/dll.py                | 76 ++++++++++++++++++++++++++++------------------
 sdl2/test/sdlimage_test.py |  5 ++-
 sdl2/test/sdlmixer_test.py |  5 ++-
 sdl2/test/sdlttf_test.py   |  5 ++-
 sdl2/test/version_test.py  | 33 ++++++++++++++++----
 5 files changed, 85 insertions(+), 39 deletions(-)

diff --git a/sdl2/dll.py b/sdl2/dll.py
index f735dc1..02fa3fe 100644
--- a/sdl2/dll.py
+++ b/sdl2/dll.py
@@ -53,25 +53,29 @@ class SDL_version(Structure):
         ("patch", c_uint8),
     ]
 
-def _version_str_to_int(s):
-    # Convert an SDL version string to an integer (e.g. "2.0.18" to 2018)
-    v = [int(n) for n in s.split('.')]
-    if v[1] > 0:
+def _version_tuple_to_int(v):
+    # Convert a tuple to an integer (e.g. (2,0,18) to 2018, (2,24,1) to 2241)
+    if v[1] > 99:
+        # Cap the minor version at 99 to avoid ambiguity: in practice
+        # the version number is unlikely to reach 2.99.z.
+        return v[0] * 1000 + 999
+    elif v[1] > 0:
         # For SDL2 >= 2.23.0 (new version scheme): 2.23.0 -> 2230
-        return v[0] * 1000 + v[1] * 10 + v[2]
+        # Note that this is not the same encoding as SDL_VERSIONNUM.
+        # Cap the micro version at 9 to avoid ambiguity: we're unlikely
+        # to need to distinguish between 2.y.9 and 2.y.10, since those
+        # would be patch/bugfix releases from the same branch anyway.
+        return v[0] * 1000 + v[1] * 10 + min(v[2], 9)
     else:
         # For SDL2 <= 2.0.22 (old version scheme): 2.0.22 -> 2022
+        # This is the same encoding as SDL_VERSIONNUM.
         return v[0] * 1000 + v[1] * 100 + v[2]
 
-def _version_int_to_str(i):
-    v = str(i)
-    if int(v[1]) > 0:
-        # For SDL2 >= 2.23.0 (new version scheme): 2230 -> 2.23.0
-        v = [v[0], str(int(v[1:3])), v[3]]
-    else:
-        # For SDL2 <= 2.0.22 (old version scheme): 2022 -> 2.0.22
-        v = [v[0], v[1], str(int(v[2:4]))]
-    return ".".join(v)
+def _version_tuple_to_str(v):
+    return ".".join(map(str, v))
+
+def _version_str_to_tuple(s):
+    return tuple(map(int, s.split('.')))
 
 def _so_version_num(libname):
     """Extracts the version number from an .so filename as a list of ints."""
@@ -199,11 +203,11 @@ class DLL(object):
         self._libname = libinfo
         self._version = None
         minversions = {
-            "SDL2": 2005,
-            "SDL2_mixer": 2001,
-            "SDL2_ttf": 2014,
-            "SDL2_image": 2001,
-            "SDL2_gfx": 1003
+            "SDL2": (2, 0, 5),
+            "SDL2_mixer": (2, 0, 1),
+            "SDL2_ttf": (2, 0, 14),
+            "SDL2_image": (2, 0, 1),
+            "SDL2_gfx": (1, 0, 3)
         }
         foundlibs = _findlib(libnames, path)
         dllmsg = "PYSDL2_DLL_PATH: %s" % (os.getenv("PYSDL2_DLL_PATH") or "unset")
@@ -214,10 +218,10 @@ class DLL(object):
             try:
                 self._dll = CDLL(libfile)
                 self._libfile = libfile
-                self._version = self._get_version(libinfo, self._dll)
-                if self._version < minversions[libinfo]:
-                    versionstr = _version_int_to_str(self._version)
-                    minimumstr = _version_int_to_str(minversions[libinfo])
+                self._version_tuple = self._get_version_tuple(libinfo, self._dll)
+                if self._version_tuple < minversions[libinfo]:
+                    versionstr = _version_tuple_to_str(self._version_tuple)
+                    minimumstr = _version_tuple_to_str(minversions[libinfo])
                     err = "{0} (v{1}) is too old to be used by py-sdl2"
                     err += " (minimum v{0})".format(minimumstr)
                     raise RuntimeError(err.format(libfile, versionstr))
@@ -253,10 +257,10 @@ class DLL(object):
                 function was added, in the format '2.x.x'.
         """
         func = getattr(self._dll, funcname, None)
-        min_version = _version_str_to_int(added) if added else None
+        min_version = _version_str_to_tuple(added) if added else None
         if not func:
-            versionstr = _version_int_to_str(self._version)
-            if min_version and min_version > self._version:
+            versionstr = _version_tuple_to_str(self._version_tuple)
+            if min_version and min_version > self._version_tuple:
                 e = "'{0}' requires {1} <= {2}, but the loaded version is {3}."
                 errmsg = e.format(funcname, self._libname, added, versionstr)
                 return _unavailable(errmsg)
@@ -268,7 +272,7 @@ class DLL(object):
         func.restype = returns
         return func
 
-    def _get_version(self, libname, dll):
+    def _get_version_tuple(self, libname, dll):
         """Gets the version of the linked SDL library"""
         if libname == "SDL2":
             dll.SDL_GetVersion.argtypes = [POINTER(SDL_version)]
@@ -286,20 +290,31 @@ class DLL(object):
             func.argtypes = None
             func.restype = POINTER(SDL_version)
             v = func().contents
-        return v.major * 1000 + v.minor * 100 + v.patch
+
+        return (v.major, v.minor, v.patch)
 
     @property
     def libfile(self):
         """str: The filename of the loaded library."""
         return self._libfile
 
+    @property
+    def version_tuple(self):
+        """tuple: The version of the loaded library in the form of a
+        tuple of integers (e.g. (2, 24, 1) for SDL 2.24.1).
+        """
+        return self._version_tuple
+
     @property
     def version(self):
         """int: The version of the loaded library in the form of a 4-digit
         integer (e.g. '2008' for SDL 2.0.8, '2231' for SDL 2.23.1).
-        """
-        return self._version
 
+        Note that this is not the same version encoding as SDL_VERSIONNUM,
+        and the exact encoding used is not guaranteed.
+        In new code, use version_tuple instead.
+        """
+        return _version_tuple_to_int(self._version_tuple)
 
 
 # Once the DLL class is defined, try loading the main SDL2 library
@@ -315,3 +330,4 @@ def get_dll_file():
 
 _bind = dll.bind_function
 version = dll.version
+version_tuple = dll.version_tuple
diff --git a/sdl2/test/sdlimage_test.py b/sdl2/test/sdlimage_test.py
index 03f29c9..3e87667 100644
--- a/sdl2/test/sdlimage_test.py
+++ b/sdl2/test/sdlimage_test.py
@@ -53,7 +53,10 @@ def test_IMG_Linked_Version():
     assert isinstance(v.contents, version.SDL_version)
     assert v.contents.major == 2
     assert v.contents.minor >= 0
-    assert v.contents.patch >= 1
+    assert v.contents.patch >= 0
+    t = (v.contents.major, v.contents.minor, v.contents.patch)
+    assert t >= (2, 0, 1)
+    assert t == sdlimage.dll.version_tuple
 
 def test_IMG_Init():
     SDL_Init(0)
diff --git a/sdl2/test/sdlmixer_test.py b/sdl2/test/sdlmixer_test.py
index 48424f8..fefee85 100644
--- a/sdl2/test/sdlmixer_test.py
+++ b/sdl2/test/sdlmixer_test.py
@@ -21,8 +21,11 @@ def test_Mix_Linked_Version():
     assert v.contents.major == 2
     assert v.contents.minor >= 0
     assert v.contents.patch >= 0
+    t = (v.contents.major, v.contents.minor, v.contents.patch)
+    assert t >= (2, 0, 0)
+    assert t == sdlmixer.dll.version_tuple
 
-@pytest.mark.skipif(sdlmixer.dll.version < 2004, reason="Broken in official binaries")
+@pytest.mark.skipif(sdlmixer.dll.version_tuple < (2, 0, 4), reason="Broken in official binaries")
 def test_Mix_Init():
     SDL_Init(sdl2.SDL_INIT_AUDIO)
     supported = []
diff --git a/sdl2/test/sdlttf_test.py b/sdl2/test/sdlttf_test.py
index 08eb45f..a58fb31 100644
--- a/sdl2/test/sdlttf_test.py
+++ b/sdl2/test/sdlttf_test.py
@@ -30,7 +30,10 @@ class TestSDLTTF(object):
         assert isinstance(v.contents, version.SDL_version)
         assert v.contents.major == 2
         assert v.contents.minor >= 0
-        assert v.contents.patch >= 12
+        assert v.contents.patch >= 0
+        t = (v.contents.major, v.contents.minor, v.contents.patch)
+        assert t >= (2, 0, 12)
+        assert t == sdlttf.dll.version_tuple
 
     def test_TTF_Font(self):
         font = sdlttf.TTF_Font()
diff --git a/sdl2/test/version_test.py b/sdl2/test/version_test.py
index 79caedc..8821581 100644
--- a/sdl2/test/version_test.py
+++ b/sdl2/test/version_test.py
@@ -7,6 +7,16 @@ from sdl2 import version, dll, __version__, version_info
 class TestSDLVersion(object):
     __tags__ = ["sdl"]
 
+    def test__version_tuple(self):
+        # Note that this is not public API.
+        assert dll._version_tuple_to_int((2, 0, 18)) == 2018
+        assert dll._version_tuple_to_int((2, 24, 1)) == 2241
+        # Micro version stops at 9 in this encoding
+        assert dll._version_tuple_to_int((2, 24, 15)) == 2249
+        assert dll._version_tuple_to_int((2, 99, 9)) == 2999
+        # Minor version stops at 99 in this encoding
+        assert dll._version_tuple_to_int((2, 103, 6)) == 2999
+
     def test_SDL_version(self):
         v = version.SDL_version(0, 0, 0)
         assert v.major == 0
@@ -19,7 +29,9 @@ class TestSDLVersion(object):
         assert type(v) == version.SDL_version
         assert v.major == 2
         assert v.minor >= 0
-        assert v.patch >= 5
+        assert v.patch >= 0
+        assert (v.major, v.minor, v.patch) >= (2, 0, 5)
+        assert (v.major, v.minor, v.patch) == dll.version_tuple
 
     def test_SDL_VERSIONNUM(self):
         assert version.SDL_VERSIONNUM(1, 2, 3) == 1203
@@ -27,6 +39,12 @@ class TestSDLVersion(object):
         assert version.SDL_VERSIONNUM(2, 0, 0) == 2000
         assert version.SDL_VERSIONNUM(17, 42, 3) == 21203
 
+        # This is a bit weird now that SDL uses the minor version more often,
+        # but does sort in the correct order against all versions of SDL 2.
+        assert version.SDL_VERSIONNUM(2, 23, 0) == 4300
+        # This is the highest possible SDL 2 version
+        assert version.SDL_VERSIONNUM(2, 255, 99) == 27599
+
     def test_SDL_VERSION_ATLEAST(self):
         assert version.SDL_VERSION_ATLEAST(1, 2, 3)
         assert version.SDL_VERSION_ATLEAST(2, 0, 0)
@@ -34,13 +52,16 @@ class TestSDLVersion(object):
         assert not version.SDL_VERSION_ATLEAST(2, 0, 100)
 
     def test_SDL_GetRevision(self):
-        if dll.version >= 2016:
-            assert version.SDL_GetRevision()[0:4] == b"http"
-        else:
-            assert version.SDL_GetRevision()[0:3] == b"hg-"
+        rev = version.SDL_GetRevision()
+        # If revision not empty string (e.g. Conda), test the prefix
+        if len(rev):
+            if dll.version_tuple >= (2, 0, 16):
+                assert rev[0:4] == b"http"
+            else:
+                assert version.SDL_GetRevision()[0:3] == b"hg-"
 
     def test_SDL_GetRevisionNumber(self):
-        if sys.platform in ("win32",) or dll.version >= 2016:
+        if sys.platform in ("win32",) or dll.version_tuple >= (2, 0, 16):
             # HG tip on Win32 does not set any revision number
             assert version.SDL_GetRevisionNumber() >= 0
         else:
