diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 15b3a8fc..82de1a95 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -191,6 +191,7 @@ jobs: strategy: matrix: pyver: + - 3.13t - 3.13 - 3.12 - 3.11 @@ -240,7 +241,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 @@ -257,7 +258,10 @@ jobs: - name: Install dependencies uses: py-actions/py-dependency-install@v4 with: - path: requirements/test.txt + path: >- + requirements/test${{ + matrix.pyver == '3.13t' && '-freethreading' || '' + }}.txt - name: Determine pre-compiled compatible wheel env: # NOTE: When `pip` is forced to colorize output piped into `jq`, @@ -371,7 +375,8 @@ jobs: pytest, OS-${{ runner.os }}, VM-${{ matrix.os }}, - Py-${{ steps.python-install.outputs.python-version }} + Py-${{ steps.python-install.outputs.python-version }}${{ + matrix.pyver == '3.13t' && 't' || '' }} fail_ci_if_error: true benchmark: @@ -417,7 +422,7 @@ jobs: - name: Install dependencies uses: py-actions/py-dependency-install@v4 with: - path: requirements/test.txt + path: requirements/codspeed.txt - name: Determine pre-compiled compatible wheel env: # NOTE: When `pip` is forced to colorize output piped into `jq`, 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/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index b9339498..95c9c563 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -1,5 +1,6 @@ Bluesky Bugfixes +CPython Changelog Codecov Cython diff --git a/packaging/pep517_backend/_backend.py b/packaging/pep517_backend/_backend.py index 9829c043..d5e4f7cd 100644 --- a/packaging/pep517_backend/_backend.py +++ b/packaging/pep517_backend/_backend.py @@ -4,6 +4,7 @@ from __future__ import annotations import os +import sysconfig import typing as t from contextlib import contextmanager, nullcontext, suppress from functools import partial @@ -371,10 +372,12 @@ def get_requires_for_build_wheel( stacklevel=999, ) - c_ext_build_deps = [] if is_pure_python_build else [ - 'Cython ~= 3.0.0; python_version >= "3.12"', - 'Cython; python_version < "3.12"', - ] + if is_pure_python_build: + c_ext_build_deps = [] + elif sysconfig.get_config_var('Py_GIL_DISABLED'): + c_ext_build_deps = ['Cython ~= 3.1.0a1'] + else: + c_ext_build_deps = ['Cython ~= 3.0.12'] return _setuptools_get_requires_for_build_wheel( config_settings=config_settings, diff --git a/pyproject.toml b/pyproject.toml index 81900af5..5a4d0b50 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,12 @@ pure-python = "false" [tool.cibuildwheel.windows] before-test = [] # Windows cmd has different syntax and pip chooses wheels + +# TODO: Remove this when there's a Cython 3.1 final release +# Remove PIP_CONSTRAINT from the environment +[[tool.cibuildwheel.overrides]] +select = "cp313t-*" + +test-requires = "-r requirements/test-freethreading.txt" +inherit.environment = "append" +environment = {PIP_CONSTRAINT = "requirements/cython-freethreading.txt"} diff --git a/requirements/codspeed.txt b/requirements/codspeed.txt new file mode 100644 index 00000000..25cc1722 --- /dev/null +++ b/requirements/codspeed.txt @@ -0,0 +1,2 @@ +-r test.txt +pytest-codspeed==3.2.0 diff --git a/requirements/cython-freethreading.txt b/requirements/cython-freethreading.txt new file mode 100644 index 00000000..b71204d4 --- /dev/null +++ b/requirements/cython-freethreading.txt @@ -0,0 +1 @@ +cython==3.1.0a1 diff --git a/requirements/dev.txt b/requirements/dev.txt index 2a4069d3..f1d0b300 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,2 +1,2 @@ --r test.txt +-r codspeed.txt -r towncrier.txt diff --git a/requirements/test-freethreading.txt b/requirements/test-freethreading.txt new file mode 100644 index 00000000..5919a29f --- /dev/null +++ b/requirements/test-freethreading.txt @@ -0,0 +1,2 @@ +-r cython-freethreading.txt +-r test-pure.txt diff --git a/requirements/test-pure.txt b/requirements/test-pure.txt new file mode 100644 index 00000000..1b7ace2c --- /dev/null +++ b/requirements/test-pure.txt @@ -0,0 +1,8 @@ +covdefaults +hypothesis>=6.0 +idna==3.10 +multidict==6.1.0 +propcache==0.3.0 +pytest==8.3.5 +pytest-cov>=2.3.1 +pytest-xdist diff --git a/requirements/test.txt b/requirements/test.txt index 608feac5..48887360 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,10 +1,2 @@ -r cython.txt -covdefaults -hypothesis>=6.0 -idna==3.10 -multidict==6.1.0 -propcache==0.3.0 -pytest==8.3.5 -pytest-cov>=2.3.1 -pytest-xdist -pytest_codspeed==3.2.0 +-r test-pure.txt diff --git a/tests/test_quoting_benchmarks.py b/tests/test_quoting_benchmarks.py index fb71b5a5..5e66123a 100644 --- a/tests/test_quoting_benchmarks.py +++ b/tests/test_quoting_benchmarks.py @@ -1,6 +1,11 @@ """codspeed benchmark for yarl._quoting module.""" -from pytest_codspeed import BenchmarkFixture +import pytest + +try: + from pytest_codspeed import BenchmarkFixture +except ImportError: # pragma: no branch # only hit in cibuildwheel + pytestmark = pytest.mark.skip("pytest-codspeed needs to be installed") from yarl._quoting import _Quoter, _Unquoter @@ -15,70 +20,70 @@ LONG_QUERY_WITH_PCT = LONG_QUERY + "&d=%25%2F%3F%3A%40%26%3B%3D%2B" -def test_quote_query_string(benchmark: BenchmarkFixture) -> None: +def test_quote_query_string(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): QUERY_QUOTER("a=1&b=2&c=3&d=4&e=5&f=6&g=7&h=8&i=9&j=0") -def test_quoter_ascii(benchmark: BenchmarkFixture) -> None: +def test_quoter_ascii(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): QUOTER_SLASH_SAFE("/path/to") -def test_quote_long_path(benchmark: BenchmarkFixture) -> None: +def test_quote_long_path(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): PATH_QUOTER(LONG_PATH) -def test_quoter_pct(benchmark: BenchmarkFixture) -> None: +def test_quoter_pct(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): QUOTER("abc%0a") -def test_long_query(benchmark: BenchmarkFixture) -> None: +def test_long_query(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): QUERY_QUOTER(LONG_QUERY) -def test_long_query_with_pct(benchmark: BenchmarkFixture) -> None: +def test_long_query_with_pct(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): QUERY_QUOTER(LONG_QUERY_WITH_PCT) -def test_quoter_quote_utf8(benchmark: BenchmarkFixture) -> None: +def test_quoter_quote_utf8(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): PATH_QUOTER("/шлях/файл") -def test_unquoter_short(benchmark: BenchmarkFixture) -> None: +def test_unquoter_short(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): UNQUOTER("/path/to") -def test_unquoter_long_ascii(benchmark: BenchmarkFixture) -> None: +def test_unquoter_long_ascii(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): UNQUOTER(LONG_QUERY) -def test_unquoter_long_pct(benchmark: BenchmarkFixture) -> None: +def test_unquoter_long_pct(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): diff --git a/tests/test_url_benchmarks.py b/tests/test_url_benchmarks.py index c8ae1055..3847dc42 100644 --- a/tests/test_url_benchmarks.py +++ b/tests/test_url_benchmarks.py @@ -1,6 +1,11 @@ """codspeed benchmarks for yarl.URL.""" -from pytest_codspeed import BenchmarkFixture +import pytest + +try: + from pytest_codspeed import BenchmarkFixture +except ImportError: # pragma: no branch # only hit in cibuildwheel + pytestmark = pytest.mark.skip("pytest-codspeed needs to be installed") from yarl import URL @@ -29,49 +34,59 @@ class _SubClassedStr(str): """A subclass of str that does nothing.""" -def test_url_build_with_host_and_port(benchmark: BenchmarkFixture) -> None: +def test_url_build_with_host_and_port( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(host="www.domain.tld", path="/req", port=1234) -def test_url_build_with_simple_query(benchmark: BenchmarkFixture) -> None: +def test_url_build_with_simple_query( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(host="www.domain.tld", query=SIMPLE_QUERY) -def test_url_build_no_netloc(benchmark: BenchmarkFixture) -> None: +def test_url_build_no_netloc(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(path="/req/req/req") -def test_url_build_no_netloc_relative(benchmark: BenchmarkFixture) -> None: +def test_url_build_no_netloc_relative( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(path="req/req/req") -def test_url_build_encoded_with_host_and_port(benchmark: BenchmarkFixture) -> None: +def test_url_build_encoded_with_host_and_port( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(host="www.domain.tld", path="/req", port=1234, encoded=True) -def test_url_build_with_host(benchmark: BenchmarkFixture) -> None: +def test_url_build_with_host(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(host="domain") -def test_url_build_access_username_password(benchmark: BenchmarkFixture) -> None: +def test_url_build_access_username_password( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): @@ -80,7 +95,9 @@ def _run() -> None: url.raw_password -def test_url_build_access_raw_host(benchmark: BenchmarkFixture) -> None: +def test_url_build_access_raw_host( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): @@ -88,7 +105,9 @@ def _run() -> None: url.raw_host -def test_url_build_access_fragment(benchmark: BenchmarkFixture) -> None: +def test_url_build_access_fragment( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): @@ -96,7 +115,9 @@ def _run() -> None: url.fragment -def test_url_build_access_raw_path(benchmark: BenchmarkFixture) -> None: +def test_url_build_access_raw_path( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): @@ -104,77 +125,97 @@ def _run() -> None: url.raw_path -def test_url_build_with_different_hosts(benchmark: BenchmarkFixture) -> None: +def test_url_build_with_different_hosts( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for host in MANY_HOSTS: URL.build(host=host) -def test_url_build_with_host_path_and_port(benchmark: BenchmarkFixture) -> None: +def test_url_build_with_host_path_and_port( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL.build(host="www.domain.tld", port=1234) -def test_url_make_no_netloc(benchmark: BenchmarkFixture) -> None: +def test_url_make_no_netloc(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): URL("/req/req/req") -def test_url_make_no_netloc_relative(benchmark: BenchmarkFixture) -> None: +def test_url_make_no_netloc_relative( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL("req/req/req") -def test_url_make_with_host_path_and_port(benchmark: BenchmarkFixture) -> None: +def test_url_make_with_host_path_and_port( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://www.domain.tld:1234/req") -def test_url_make_encoded_with_host_path_and_port(benchmark: BenchmarkFixture) -> None: +def test_url_make_encoded_with_host_path_and_port( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://www.domain.tld:1234/req", encoded=True) -def test_url_make_with_host_and_path(benchmark: BenchmarkFixture) -> None: +def test_url_make_with_host_and_path( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://www.domain.tld") -def test_url_make_with_many_hosts(benchmark: BenchmarkFixture) -> None: +def test_url_make_with_many_hosts( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for url in MANY_URLS: URL(url) -def test_url_make_with_many_ipv4_hosts(benchmark: BenchmarkFixture) -> None: +def test_url_make_with_many_ipv4_hosts( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for url in MANY_IPV4_URLS: URL(url) -def test_url_make_with_many_ipv6_hosts(benchmark: BenchmarkFixture) -> None: +def test_url_make_with_many_ipv6_hosts( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for url in MANY_IPV6_URLS: URL(url) -def test_url_make_access_raw_host(benchmark: BenchmarkFixture) -> None: +def test_url_make_access_raw_host( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): @@ -182,7 +223,7 @@ def _run() -> None: url.raw_host -def test_raw_host_empty_cache(benchmark: BenchmarkFixture) -> None: +def test_raw_host_empty_cache(benchmark: "BenchmarkFixture") -> None: url = URL("http://www.domain.tld") @benchmark @@ -192,7 +233,9 @@ def _run() -> None: url.raw_host -def test_url_make_access_fragment(benchmark: BenchmarkFixture) -> None: +def test_url_make_access_fragment( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): @@ -200,7 +243,9 @@ def _run() -> None: url.fragment -def test_url_make_access_raw_path(benchmark: BenchmarkFixture) -> None: +def test_url_make_access_raw_path( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): @@ -208,7 +253,9 @@ def _run() -> None: url.raw_path -def test_url_make_access_username_password(benchmark: BenchmarkFixture) -> None: +def test_url_make_access_username_password( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): @@ -217,49 +264,59 @@ def _run() -> None: url.raw_password -def test_url_make_empty_username(benchmark: BenchmarkFixture) -> None: +def test_url_make_empty_username(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://:password@www.domain.tld") -def test_url_make_empty_password(benchmark: BenchmarkFixture) -> None: +def test_url_make_empty_password(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://user:@www.domain.tld") -def test_url_make_with_ipv4_address_path_and_port(benchmark: BenchmarkFixture) -> None: +def test_url_make_with_ipv4_address_path_and_port( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://127.0.0.1:1234/req") -def test_url_make_with_ipv4_address_and_path(benchmark: BenchmarkFixture) -> None: +def test_url_make_with_ipv4_address_and_path( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://127.0.0.1/req") -def test_url_make_with_ipv6_address_path_and_port(benchmark: BenchmarkFixture) -> None: +def test_url_make_with_ipv6_address_path_and_port( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://[::1]:1234/req") -def test_url_make_with_ipv6_address_and_path(benchmark: BenchmarkFixture) -> None: +def test_url_make_with_ipv6_address_and_path( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): URL("http://[::1]/req") -def test_extend_query_subclassed_str(benchmark: BenchmarkFixture) -> None: +def test_extend_query_subclassed_str( + benchmark: "BenchmarkFixture", +) -> None: """Test extending a query with a subclassed str.""" subclassed_query = {str(i): _SubClassedStr(i) for i in range(10)} @@ -269,84 +326,94 @@ def _run() -> None: BASE_URL.with_query(subclassed_query) -def test_with_query_mapping(benchmark: BenchmarkFixture) -> None: +def test_with_query_mapping(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.with_query(SIMPLE_QUERY) -def test_with_query_mapping_int_values(benchmark: BenchmarkFixture) -> None: +def test_with_query_mapping_int_values( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.with_query(SIMPLE_INT_QUERY) -def test_with_query_sequence_mapping(benchmark: BenchmarkFixture) -> None: +def test_with_query_sequence_mapping( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.with_query(QUERY_SEQ) -def test_with_query_empty(benchmark: BenchmarkFixture) -> None: +def test_with_query_empty(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.with_query({}) -def test_with_query_none(benchmark: BenchmarkFixture) -> None: +def test_with_query_none(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.with_query(None) -def test_update_query_mapping(benchmark: BenchmarkFixture) -> None: +def test_update_query_mapping(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.update_query(SIMPLE_QUERY) -def test_update_query_mapping_with_existing_query(benchmark: BenchmarkFixture) -> None: +def test_update_query_mapping_with_existing_query( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(25): QUERY_URL.update_query(SIMPLE_QUERY) -def test_update_query_sequence_mapping(benchmark: BenchmarkFixture) -> None: +def test_update_query_sequence_mapping( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.update_query(QUERY_SEQ) -def test_update_query_empty(benchmark: BenchmarkFixture) -> None: +def test_update_query_empty(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.update_query({}) -def test_update_query_none(benchmark: BenchmarkFixture) -> None: +def test_update_query_none(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.update_query(None) -def test_update_query_string(benchmark: BenchmarkFixture) -> None: +def test_update_query_string(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(25): BASE_URL.update_query(QUERY_STRING) -def test_url_extend_query_simple_query_dict(benchmark: BenchmarkFixture) -> None: +def test_url_extend_query_simple_query_dict( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(25): @@ -354,7 +421,7 @@ def _run() -> None: def test_url_extend_query_existing_query_simple_query_dict( - benchmark: BenchmarkFixture, + benchmark: "BenchmarkFixture", ) -> None: @benchmark def _run() -> None: @@ -362,91 +429,95 @@ def _run() -> None: QUERY_URL.extend_query(SIMPLE_QUERY) -def test_url_extend_query_existing_query_string(benchmark: BenchmarkFixture) -> None: +def test_url_extend_query_existing_query_string( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(25): QUERY_URL.extend_query(QUERY_STRING) -def test_url_to_string(benchmark: BenchmarkFixture) -> None: +def test_url_to_string(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): str(BASE_URL) -def test_url_with_path_to_string(benchmark: BenchmarkFixture) -> None: +def test_url_with_path_to_string(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): str(URL_WITH_PATH) -def test_url_with_query_to_string(benchmark: BenchmarkFixture) -> None: +def test_url_with_query_to_string( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): str(QUERY_URL) -def test_url_with_fragment(benchmark: BenchmarkFixture) -> None: +def test_url_with_fragment(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_fragment("fragment") -def test_url_with_user(benchmark: BenchmarkFixture) -> None: +def test_url_with_user(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_user("user") -def test_url_with_password(benchmark: BenchmarkFixture) -> None: +def test_url_with_password(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_password("password") -def test_url_with_host(benchmark: BenchmarkFixture) -> None: +def test_url_with_host(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_host("www.domain.tld") -def test_url_with_port(benchmark: BenchmarkFixture) -> None: +def test_url_with_port(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_port(1234) -def test_url_with_scheme(benchmark: BenchmarkFixture) -> None: +def test_url_with_scheme(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_scheme("https") -def test_url_with_name(benchmark: BenchmarkFixture) -> None: +def test_url_with_name(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_name("other.tld") -def test_url_with_path(benchmark: BenchmarkFixture) -> None: +def test_url_with_path(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.with_path("/req") -def test_url_origin(benchmark: BenchmarkFixture) -> None: +def test_url_origin(benchmark: "BenchmarkFixture") -> None: urls = [URL(BASE_URL_STR) for _ in range(100)] @benchmark @@ -455,7 +526,9 @@ def _run() -> None: url.origin() -def test_url_origin_with_user_pass(benchmark: BenchmarkFixture) -> None: +def test_url_origin_with_user_pass( + benchmark: "BenchmarkFixture", +) -> None: urls = [URL(URL_WITH_USER_PASS_STR) for _ in range(100)] @benchmark @@ -464,7 +537,7 @@ def _run() -> None: url.origin() -def test_url_with_path_origin(benchmark: BenchmarkFixture) -> None: +def test_url_with_path_origin(benchmark: "BenchmarkFixture") -> None: urls = [URL(URL_WITH_PATH_STR) for _ in range(100)] @benchmark @@ -473,14 +546,14 @@ def _run() -> None: url.origin() -def test_url_with_path_relative(benchmark: BenchmarkFixture) -> None: +def test_url_with_path_relative(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): URL_WITH_PATH.relative() -def test_url_with_path_parent(benchmark: BenchmarkFixture) -> None: +def test_url_with_path_parent(benchmark: "BenchmarkFixture") -> None: cache = URL_WITH_PATH._cache @benchmark @@ -490,21 +563,23 @@ def _run() -> None: URL_WITH_PATH.parent -def test_url_join(benchmark: BenchmarkFixture) -> None: +def test_url_join(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.join(REL_URL) -def test_url_joinpath_encoded(benchmark: BenchmarkFixture) -> None: +def test_url_joinpath_encoded(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.joinpath("req", encoded=True) -def test_url_joinpath_encoded_long(benchmark: BenchmarkFixture) -> None: +def test_url_joinpath_encoded_long( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): @@ -513,21 +588,23 @@ def _run() -> None: ) -def test_url_joinpath(benchmark: BenchmarkFixture) -> None: +def test_url_joinpath(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL.joinpath("req", encoded=False) -def test_url_joinpath_with_truediv(benchmark: BenchmarkFixture) -> None: +def test_url_joinpath_with_truediv( + benchmark: "BenchmarkFixture", +) -> None: @benchmark def _run() -> None: for _ in range(100): BASE_URL / "req/req/req" -def test_url_equality(benchmark: BenchmarkFixture) -> None: +def test_url_equality(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): @@ -536,7 +613,7 @@ def _run() -> None: URL_WITH_PATH == URL_WITH_PATH -def test_url_hash(benchmark: BenchmarkFixture) -> None: +def test_url_hash(benchmark: "BenchmarkFixture") -> None: cache = BASE_URL._cache @benchmark @@ -546,7 +623,7 @@ def _run() -> None: hash(BASE_URL) -def test_is_default_port(benchmark: BenchmarkFixture) -> None: +def test_is_default_port(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): @@ -554,7 +631,7 @@ def _run() -> None: URL_WITH_NOT_DEFAULT_PORT.is_default_port() -def test_human_repr(benchmark: BenchmarkFixture) -> None: +def test_human_repr(benchmark: "BenchmarkFixture") -> None: @benchmark def _run() -> None: for _ in range(100): @@ -566,7 +643,7 @@ def _run() -> None: REL_URL.human_repr() -def test_query_string(benchmark: BenchmarkFixture) -> None: +def test_query_string(benchmark: "BenchmarkFixture") -> None: urls = [URL(QUERY_URL_STR) for _ in range(100)] @benchmark @@ -575,7 +652,7 @@ def _run() -> None: url.query_string -def test_empty_query_string(benchmark: BenchmarkFixture) -> None: +def test_empty_query_string(benchmark: "BenchmarkFixture") -> None: urls = [URL(BASE_URL_STR) for _ in range(100)] @benchmark @@ -584,7 +661,9 @@ def _run() -> None: url.query_string -def test_empty_query_string_uncached(benchmark: BenchmarkFixture) -> None: +def test_empty_query_string_uncached( + benchmark: "BenchmarkFixture", +) -> None: urls = [URL(BASE_URL_STR) for _ in range(100)] @benchmark @@ -593,7 +672,7 @@ def _run() -> None: URL.query_string.wrapped(url) -def test_query(benchmark: BenchmarkFixture) -> None: +def test_query(benchmark: "BenchmarkFixture") -> None: urls = [URL(QUERY_URL_STR) for _ in range(100)] @benchmark @@ -602,7 +681,7 @@ def _run() -> None: url.query -def test_empty_query(benchmark: BenchmarkFixture) -> None: +def test_empty_query(benchmark: "BenchmarkFixture") -> None: urls = [URL(BASE_URL_STR) for _ in range(100)] @benchmark @@ -611,7 +690,9 @@ def _run() -> None: url.query -def test_url_host_port_subcomponent(benchmark: BenchmarkFixture) -> None: +def test_url_host_port_subcomponent( + benchmark: "BenchmarkFixture", +) -> None: cache_non_default = URL_WITH_NOT_DEFAULT_PORT._cache cache = BASE_URL._cache @@ -624,7 +705,7 @@ def _run() -> None: BASE_URL.host_port_subcomponent -def test_empty_path(benchmark: BenchmarkFixture) -> None: +def test_empty_path(benchmark: "BenchmarkFixture") -> None: """Test accessing empty path.""" @benchmark @@ -633,7 +714,7 @@ def _run() -> None: BASE_URL.path -def test_empty_path_uncached(benchmark: BenchmarkFixture) -> None: +def test_empty_path_uncached(benchmark: "BenchmarkFixture") -> None: """Test accessing empty path without cache.""" @benchmark @@ -642,7 +723,7 @@ def _run() -> None: URL.path.wrapped(BASE_URL) -def test_empty_path_safe(benchmark: BenchmarkFixture) -> None: +def test_empty_path_safe(benchmark: "BenchmarkFixture") -> None: """Test accessing empty path safe.""" @benchmark @@ -651,7 +732,9 @@ def _run() -> None: BASE_URL.path_safe -def test_empty_path_safe_uncached(benchmark: BenchmarkFixture) -> None: +def test_empty_path_safe_uncached( + benchmark: "BenchmarkFixture", +) -> None: """Test accessing empty path safe without cache.""" @benchmark @@ -660,7 +743,7 @@ def _run() -> None: URL.path_safe.wrapped(BASE_URL) -def test_path_safe(benchmark: BenchmarkFixture) -> None: +def test_path_safe(benchmark: "BenchmarkFixture") -> None: """Test accessing path safe.""" @benchmark @@ -669,7 +752,7 @@ def _run() -> None: URL_WITH_PATH.path_safe -def test_path_safe_uncached(benchmark: BenchmarkFixture) -> None: +def test_path_safe_uncached(benchmark: "BenchmarkFixture") -> None: """Test accessing path safe without cache.""" @benchmark @@ -678,7 +761,7 @@ def _run() -> None: URL.path_safe.wrapped(URL_WITH_PATH) -def test_empty_raw_path_qs(benchmark: BenchmarkFixture) -> None: +def test_empty_raw_path_qs(benchmark: "BenchmarkFixture") -> None: """Test accessing empty raw path with query.""" @benchmark @@ -687,7 +770,9 @@ def _run() -> None: BASE_URL.raw_path_qs -def test_empty_raw_path_qs_uncached(benchmark: BenchmarkFixture) -> None: +def test_empty_raw_path_qs_uncached( + benchmark: "BenchmarkFixture", +) -> None: """Test accessing empty raw path with query without cache.""" @benchmark @@ -696,7 +781,7 @@ def _run() -> None: URL.raw_path_qs.wrapped(BASE_URL) -def test_raw_path_qs(benchmark: BenchmarkFixture) -> None: +def test_raw_path_qs(benchmark: "BenchmarkFixture") -> None: """Test accessing raw path qs without query.""" @benchmark @@ -705,7 +790,7 @@ def _run() -> None: URL_WITH_PATH.raw_path_qs -def test_raw_path_qs_uncached(benchmark: BenchmarkFixture) -> None: +def test_raw_path_qs_uncached(benchmark: "BenchmarkFixture") -> None: """Test accessing raw path qs without query and without cache.""" @benchmark @@ -714,7 +799,7 @@ def _run() -> None: URL.raw_path_qs.wrapped(URL_WITH_PATH) -def test_raw_path_qs_with_query(benchmark: BenchmarkFixture) -> None: +def test_raw_path_qs_with_query(benchmark: "BenchmarkFixture") -> None: """Test accessing raw path qs with query.""" @benchmark @@ -723,7 +808,9 @@ def _run() -> None: IPV6_QUERY_URL.raw_path_qs -def test_raw_path_qs_with_query_uncached(benchmark: BenchmarkFixture) -> None: +def test_raw_path_qs_with_query_uncached( + benchmark: "BenchmarkFixture", +) -> None: """Test accessing raw path qs with query and without cache.""" @benchmark diff --git a/yarl/_quoting_c.pyx b/yarl/_quoting_c.pyx index 067ba96e..5b35fff8 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 @@ -13,6 +13,7 @@ from cpython.unicode cimport ( from libc.stdint cimport uint8_t, uint64_t from libc.string cimport memcpy, memset +import sysconfig from string import ascii_letters, digits @@ -26,6 +27,7 @@ cdef str QS = '+&=;' DEF BUF_SIZE = 8 * 1024 # 8KiB cdef char BUFFER[BUF_SIZE] +cdef bint IS_GIL_DISABLED = sysconfig.get_config_var("Py_GIL_DISABLED") cdef inline Py_UCS4 _to_hex(uint8_t v) noexcept: if v < 10: @@ -49,14 +51,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,7 +93,15 @@ cdef struct Writer: cdef inline void _init_writer(Writer* writer): - writer.buf = &BUFFER[0] + cdef char *buf + if IS_GIL_DISABLED: + buf = PyMem_Malloc(BUF_SIZE) + if buf == NULL: + PyErr_NoMemory() + return + writer.buf = buf + else: + writer.buf = &BUFFER[0] writer.size = BUF_SIZE writer.pos = 0 writer.changed = 0 @@ -255,6 +265,7 @@ cdef class _Quoter: Writer *writer ): cdef Py_UCS4 ch + cdef long chl cdef int changed cdef Py_ssize_t idx = 0 @@ -262,11 +273,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 +354,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 +365,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