Tribler/py-ipv8

Python 3.12 support

qstokkink opened this issue · 1 comments

IPv8 does not work out-of-the box with Python 3.12 yet.

I'll keep track of all known issues with Python 3.12 support in this issue. The list (so far) is as follows:

  • aiohttp fails to install using pip. Known workaround: pip install aiohttp==3.9.0b0
    click me for log output
    aiohttp/_websocket.c(1475): warning C4996: 'Py_OptimizeFlag': deprecated in 3.12
    aiohttp/_websocket.c(3042): error C2039: 'ob_digit': is not a member of '_longobject'
    C:\Python\Python312\include\cpython/longintrepr.h(87): note: see declaration of '_longobject'
    aiohttp/_websocket.c(3097): error C2039: 'ob_digit': is not a member of '_longobject'
    C:\Python\Python312\include\cpython/longintrepr.h(87): note: see declaration of '_longobject'
    aiohttp/_websocket.c(3238): error C2039: 'ob_digit': is not a member of '_longobject'
    C:\Python\Python312\include\cpython/longintrepr.h(87): note: see declaration of '_longobject'
    aiohttp/_websocket.c(3293): error C2039: 'ob_digit': is not a member of '_longobject'
    C:\Python\Python312\include\cpython/longintrepr.h(87): note: see declaration of '_longobject'
    aiohttp/_websocket.c(3744): error C2039: 'ob_digit': is not a member of '_longobject'
    C:\Python\Python312\include\cpython/longintrepr.h(87): note: see declaration of '_longobject'
    error: command 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.37.32822\bin\HostX86\x64\cl.exe' failed with exit code 2
  • object.__new__ can no longer be used, causing type_from_format() to crash. Known workaround: out = TypeVar.__new__(TypeVar, fmt)
    click me for log output
    File "C:\py-ipv8\ipv8\test\messaging\test_payload_dataclass.py", line 11, in
    varlenH = type_from_format('varlenH') # noqa: N816
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "C:\py-ipv8\ipv8\messaging\payload_dataclass.py", line 15, in type_from_format
    out = object.new(TypeVar)
    ^^^^^^^^^^^^^^^^^^^^^^^
    TypeError: object.new(typing.TypeVar) is not safe, use typing.TypeVar.new()
  • When importing apispec, distutils is not available: ModuleNotFoundError: No module named 'distutils'. Workaround described below.
  • TestBase._callTearDown hard locks on self._callAsync(self.tearDown). Known workaround: use self._asyncioTestContext.run(self.tearDown) instead. Edit: previously stated workaround was incorrect. The actual block occurred in web.TCPSite.stop during wait_for.

When the above is fixed, the unittests pass again.

It seems IPv8 itself also uses distutils.version.LooseVersion. Additionally, inside of apispec, I also spotted a use of LooseVersion. Therefore, the distutils removal is a bigger issue for IPv8 than just a one-line fix.

We can follow https://peps.python.org/pep-0632/ and switch to https://pypi.org/project/packaging/ However, for external downstream dependencies (that we obviously cannot edit directly) we should locally wrap the required distutils functionality and load the wrapper as the distutils module into sys.modules (temporarily, until our dependencies update).

A quick and dirty fix would be the following (CLICK ME!)
from __future__ import annotations

import sys

import packaging.version


class LooseVersion(packaging.version.Version):

    @property
    def version(self):
        return self.release

    @property
    def vstring(self):
        return str(self)

    def __lt__(self, other: str | "_BaseVersion") -> bool:
        if isinstance(other, str):
            return self < LooseVersion(other)
        return super().__lt__(other)

    def __le__(self, other: str | "_BaseVersion") -> bool:
        if isinstance(other, str):
            return self <= LooseVersion(other)
        return super().__le__(other)

    def __eq__(self, other: object) -> bool:
        if isinstance(other, str):
            return self == LooseVersion(other)
        return super().__eq__(other)

    def __ge__(self, other: str | "_BaseVersion") -> bool:
        if isinstance(other, str):
            return self >= LooseVersion(other)
        return super().__ge__(other)

    def __gt__(self, other: str | "_BaseVersion") -> bool:
        if isinstance(other, str):
            return self > LooseVersion(other)
        return super().__gt__(other)

    def __ne__(self, other: object) -> bool:
        if isinstance(other, str):
            return self != LooseVersion(other)
        return super().__ne__(other)


packaging.version.LooseVersion = LooseVersion
sys.modules["distutils.version"] = packaging.version