diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 5e943f7a..4f611343 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -38,7 +38,7 @@ env: PY_COLORS: 1 # Recognized by the `py` package, dependency of `pytest` PYTHONIOENCODING: utf-8 PYTHONUTF8: 1 - PYTHON_LATEST: 3.12 + PYTHON_LATEST: 3.13 jobs: @@ -184,6 +184,7 @@ jobs: strategy: matrix: pyver: + - 3.13t - 3.13 - 3.12 - 3.11 @@ -231,7 +232,7 @@ jobs: - name: Setup Python ${{ matrix.pyver }} id: python-install - uses: actions/setup-python@v5 + uses: quansight-labs/setup-python@v5 with: python-version: ${{ matrix.pyver }} allow-prereleases: true @@ -241,6 +242,14 @@ jobs: uses: py-actions/py-dependency-install@v4 with: path: requirements/test.txt + - name: Install Cython nightly on free-threading + if: matrix.pyver == '3.13t' + run: | + python -m pip uninstall -y cython + python -m pip install cython \ + --pre \ + --extra-index-url \ + https://pypi.anaconda.org/scientific-python-nightly-wheels/simple - name: Determine pre-compiled compatible wheel env: # NOTE: When `pip` is forced to colorize output piped into `jq`, diff --git a/.github/workflows/reusable-linters.yml b/.github/workflows/reusable-linters.yml index 68b06d49..e127ea23 100644 --- a/.github/workflows/reusable-linters.yml +++ b/.github/workflows/reusable-linters.yml @@ -21,7 +21,7 @@ env: PY_COLORS: 1 # Recognized by the `py` package, dependency of `pytest` PYTHONIOENCODING: utf-8 PYTHONUTF8: 1 - PYTHON_LATEST: 3.12 + PYTHON_LATEST: 3.13 jobs: diff --git a/CHANGES/1456.feature.rst b/CHANGES/1456.feature.rst new file mode 100644 index 00000000..c5da0e04 --- /dev/null +++ b/CHANGES/1456.feature.rst @@ -0,0 +1 @@ +Implemented support for the free-threaded build of CPython 3.13 -- by :user:`lysnikolaou`. diff --git a/CHANGES/1456.packaging.rst b/CHANGES/1456.packaging.rst new file mode 100644 index 00000000..911a78b2 --- /dev/null +++ b/CHANGES/1456.packaging.rst @@ -0,0 +1 @@ +Started building wheels for the free-threaded build of CPython 3.13 -- by :user:`lysnikolaou`. diff --git a/MANIFEST.in b/MANIFEST.in index 904cf688..6bb7eb82 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -12,6 +12,7 @@ graft docs graft CHANGES graft requirements graft tests +graft scripts global-exclude *.pyc global-exclude *.cache exclude yarl/*.c diff --git a/pyproject.toml b/pyproject.toml index 81900af5..097ffda6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,6 +64,7 @@ linetrace = "True" # Implies `profile=True` [tool.cibuildwheel] build-frontend = "build" +enable = ["cpython-freethreading"] before-test = [ # NOTE: Attempt to have pip pre-compile PyYAML wheel with our build # NOTE: constraints unset. The hope is that pip will cache that wheel @@ -91,3 +92,8 @@ pure-python = "false" [tool.cibuildwheel.windows] before-test = [] # Windows cmd has different syntax and pip chooses wheels + +[[tool.cibuildwheel.overrides]] +select = "cp313t-*" +build-frontend = "build; args: --no-isolation" +before-build = "bash {package}/scripts/cibw_before_build.sh" diff --git a/scripts/cibw_before_build.sh b/scripts/cibw_before_build.sh new file mode 100644 index 00000000..718d89d7 --- /dev/null +++ b/scripts/cibw_before_build.sh @@ -0,0 +1,7 @@ +# TODO: Delete when there's a PyPI Cython release (3.1.0) that supports free-threaded Python 3.13. +FREE_THREADED_BUILD="$(python -c"import sysconfig; print(bool(sysconfig.get_config_var('Py_GIL_DISABLED')))")" +if [[ $FREE_THREADED_BUILD == "True" ]]; then + python -m pip install -U pip + python -m pip install -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple cython + python -m pip install setuptools expandvars +fi diff --git a/yarl/_quoting_c.pyx b/yarl/_quoting_c.pyx index 067ba96e..6fed3926 100644 --- a/yarl/_quoting_c.pyx +++ b/yarl/_quoting_c.pyx @@ -1,4 +1,4 @@ -# cython: language_level=3 +# cython: language_level=3, freethreading_compatible=True from cpython.exc cimport PyErr_NoMemory from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc @@ -25,7 +25,6 @@ cdef str ALLOWED = UNRESERVED + SUB_DELIMS_WITHOUT_QS cdef str QS = '+&=;' DEF BUF_SIZE = 8 * 1024 # 8KiB -cdef char BUFFER[BUF_SIZE] cdef inline Py_UCS4 _to_hex(uint8_t v) noexcept: if v < 10: @@ -49,14 +48,14 @@ cdef inline int _is_lower_hex(Py_UCS4 v) noexcept: return 'a' <= v <= 'f' -cdef inline Py_UCS4 _restore_ch(Py_UCS4 d1, Py_UCS4 d2): +cdef inline long _restore_ch(Py_UCS4 d1, Py_UCS4 d2): cdef int digit1 = _from_hex(d1) if digit1 < 0: - return -1 + return -1 cdef int digit2 = _from_hex(d2) if digit2 < 0: - return -1 - return (digit1 << 4 | digit2) + return -1 + return digit1 << 4 | digit2 cdef uint8_t ALLOWED_TABLE[16] @@ -91,15 +90,18 @@ cdef struct Writer: cdef inline void _init_writer(Writer* writer): - writer.buf = &BUFFER[0] + cdef char *buf = PyMem_Malloc(BUF_SIZE) + if buf == NULL: + PyErr_NoMemory() + return + writer.buf = buf writer.size = BUF_SIZE writer.pos = 0 writer.changed = 0 cdef inline void _release_writer(Writer* writer): - if writer.buf != BUFFER: - PyMem_Free(writer.buf) + PyMem_Free(writer.buf) cdef inline int _write_char(Writer* writer, Py_UCS4 ch, bint changed): @@ -109,17 +111,10 @@ cdef inline int _write_char(Writer* writer, Py_UCS4 ch, bint changed): if writer.pos == writer.size: # reallocate size = writer.size + BUF_SIZE - if writer.buf == BUFFER: - buf = PyMem_Malloc(size) - if buf == NULL: - PyErr_NoMemory() - return -1 - memcpy(buf, writer.buf, writer.size) - else: - buf = PyMem_Realloc(writer.buf, size) - if buf == NULL: - PyErr_NoMemory() - return -1 + buf = PyMem_Realloc(writer.buf, size) + if buf == NULL: + PyErr_NoMemory() + return -1 writer.buf = buf writer.size = size writer.buf[writer.pos] = ch @@ -255,6 +250,7 @@ cdef class _Quoter: Writer *writer ): cdef Py_UCS4 ch + cdef long chl cdef int changed cdef Py_ssize_t idx = 0 @@ -262,11 +258,12 @@ cdef class _Quoter: ch = PyUnicode_READ(kind, data, idx) idx += 1 if ch == '%' and self._requote and idx <= length - 2: - ch = _restore_ch( + chl = _restore_ch( PyUnicode_READ(kind, data, idx), PyUnicode_READ(kind, data, idx + 1) ) - if ch != -1: + if chl != -1: + ch = chl idx += 2 if ch < 128: if bit_at(self._protected_table, ch): @@ -342,6 +339,7 @@ cdef class _Unquoter: cdef Py_ssize_t consumed cdef str unquoted cdef Py_UCS4 ch = 0 + cdef long chl = 0 cdef Py_ssize_t idx = 0 cdef Py_ssize_t start_pct cdef int kind = PyUnicode_KIND(val) @@ -352,11 +350,12 @@ cdef class _Unquoter: idx += 1 if ch == '%' and idx <= length - 2: changed = 1 - ch = _restore_ch( + chl = _restore_ch( PyUnicode_READ(kind, data, idx), PyUnicode_READ(kind, data, idx + 1) ) - if ch != -1: + if chl != -1: + ch = chl idx += 2 assert buflen < 4 buffer[buflen] = ch