From bc17b3e7c08df8847523a8482df502c9049c9dfe Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 9 Nov 2025 08:16:07 +0000 Subject: [PATCH 1/6] :bug: fix size calculation in test_does_not_error_when_parsing_a_very_long_list for compatibility with PyPy --- tests/unit/decode_test.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/unit/decode_test.py b/tests/unit/decode_test.py index cbf370f..adf5c3f 100644 --- a/tests/unit/decode_test.py +++ b/tests/unit/decode_test.py @@ -653,7 +653,14 @@ def test_continues_parsing_when_no_parent_is_found( def test_does_not_error_when_parsing_a_very_long_list(self) -> None: buf: str = "a[]=a" - while getsizeof(buf) < 128 * 1024: + def _approx_size(value: str) -> int: + try: + return getsizeof(value) + except TypeError: + # PyPy does not implement getsizeof; fall back to len, which matches byte size for ASCII payloads. + return len(value) + + while _approx_size(buf) < 128 * 1024: buf += "&" buf += buf From 2d7ca15722df63d6a2360b4738bda895adac41ab Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 9 Nov 2025 08:16:15 +0000 Subject: [PATCH 2/6] :bug: fix datetime serialization in EncodeOptions to handle timezone-aware dates correctly --- tests/unit/example_test.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/unit/example_test.py b/tests/unit/example_test.py index 38ab51c..cdc26b8 100644 --- a/tests/unit/example_test.py +++ b/tests/unit/example_test.py @@ -296,7 +296,18 @@ def custom_decoder(value: t.Any, charset: t.Optional[qs_codec.Charset]): else datetime.datetime.utcfromtimestamp(7) ) }, - qs_codec.EncodeOptions(encode=False, serialize_date=lambda date: str(int(date.timestamp()))), + qs_codec.EncodeOptions( + encode=False, + serialize_date=lambda date: str( + int( + ( + date + if date.tzinfo is not None + else date.replace(tzinfo=datetime.timezone.utc) + ).timestamp() + ) + ), + ), ) == "a=7" ) @@ -334,7 +345,17 @@ def custom_decoder(value: t.Any, charset: t.Optional[qs_codec.Charset]): encode=False, filter=lambda prefix, value: { "b": None, - "e[f]": int(value.timestamp()) if isinstance(value, datetime.datetime) else value, + "e[f]": ( + int( + ( + value + if value.tzinfo is not None + else value.replace(tzinfo=datetime.timezone.utc) + ).timestamp() + ) + if isinstance(value, datetime.datetime) + else value + ), "e[g][0]": value * 2 if isinstance(value, int) else value, }.get(prefix, value), ), From c889b151591ca3801a2d312613479d0c01cacf95 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 9 Nov 2025 08:16:20 +0000 Subject: [PATCH 3/6] :bug: ensure garbage collection is triggered after deleting weak references in tests --- tests/unit/weakref_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/weakref_test.py b/tests/unit/weakref_test.py index ca8fa12..08e6f0e 100644 --- a/tests/unit/weakref_test.py +++ b/tests/unit/weakref_test.py @@ -18,6 +18,7 @@ def test_weak_key_dict_with_dict_keys(self) -> None: assert d.get(foo) == 123 assert d.get(foo_copy) == 123 del foo + gc.collect() assert len(d) == 0 assert d.get(foo_copy) is None @@ -31,6 +32,7 @@ def test_weak_key_dict_with_nested_dict_keys(self) -> None: assert d.get(foo) == 123 assert d.get(foo_copy) == 123 del foo + gc.collect() assert len(d) == 0 assert d.get(foo_copy) is None From 88b82b5b115380216732396d9850a0eca085eae9 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 9 Nov 2025 08:16:56 +0000 Subject: [PATCH 4/6] :art: add PyPy support in tox and update classifiers in pyproject.toml --- .github/workflows/test.yml | 10 +++++++++- pyproject.toml | 3 +++ tox.ini | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e9ee96b..8fddd75 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,6 +56,14 @@ jobs: py: "3.13" - toxenv: "python3.14" py: "3.14" + - toxenv: "pypy3.8" + py: "pypy-3.8" + - toxenv: "pypy3.9" + py: "pypy-3.9" + - toxenv: "pypy3.10" + py: "pypy-3.10" + - toxenv: "pypy3.11" + py: "pypy-3.11" steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.py }} @@ -128,4 +136,4 @@ jobs: else echo "The outputs are different." exit 1 - fi \ No newline at end of file + fi diff --git a/pyproject.toml b/pyproject.toml index 3aeb085..8946c23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,10 @@ classifiers = [ "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries", + "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Text Processing :: General", "Topic :: Utilities", diff --git a/tox.ini b/tox.ini index d16ab94..15fae24 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,10 @@ envlist = python3.12, python3.13, python3.14, + pypy3.8, + pypy3.9, + pypy3.10, + pypy3.11, black, flake8, linters, @@ -22,6 +26,10 @@ python = 3.12: python3.12 3.13: python3.13 3.14: python3.14 + pypy-3.8: pypy3.8 + pypy-3.9: pypy3.9 + pypy-3.10: pypy3.10 + pypy-3.11: pypy3.11 [testenv] deps = @@ -66,6 +74,18 @@ deps = commands = pylint --rcfile=tox.ini src/qs_codec +[testenv:pypy3.8] +basepython = pypy3.8 + +[testenv:pypy3.9] +basepython = pypy3.9 + +[testenv:pypy3.10] +basepython = pypy3.10 + +[testenv:pypy3.11] +basepython = pypy3.11 + [testenv:bandit] basepython = python3 skip_install = true From 05768562f1ac86ae863ecaf0f883568c74b90b01 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 9 Nov 2025 08:17:02 +0000 Subject: [PATCH 5/6] :art: add compatibility section to documentation for CPython and PyPy versions --- README.rst | 6 ++++++ docs/index.rst | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/README.rst b/README.rst index fb0b2ee..e787b39 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,12 @@ Highlights - Safety limits: configurable nesting depth, parameter limit, and list index limit; optional strict-depth errors; duplicate-key strategies (combine/first/last). - Extras: numeric entity decoding (e.g. ``☺`` → ☺), alternate delimiters/regex, and query-prefix helpers. +Compatibility +------------- + +- CPython 3.8–3.14 (default tox envs). +- PyPy 3.8–3.11 (run ``tox -e pypy3.8`` through ``tox -e pypy3.11`` locally; CI mirrors this matrix). + Usage ----- diff --git a/docs/index.rst b/docs/index.rst index 5927a6a..ac7b5d9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,6 +32,12 @@ Highlights - Safety limits: configurable nesting depth, parameter limit, and list index limit; optional strict-depth errors; duplicate-key strategies (combine/first/last). - Extras: numeric entity decoding (e.g. ``☺`` → ☺), alternate delimiters/regex, and query-prefix helpers. +Compatibility +------------- + +- CPython 3.8–3.14 (default tox envs). +- PyPy 3.8–3.11 (run ``tox -e pypy3.8`` through ``tox -e pypy3.11`` locally; CI mirrors this matrix). + Usage ----- From 44572d89a1940621cdad7079b798f1895a2d136d Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 9 Nov 2025 08:20:57 +0000 Subject: [PATCH 6/6] :bug: fix datetime handling in tests for timezone-aware dates --- tests/unit/decode_test.py | 1 + tests/unit/example_test.py | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/unit/decode_test.py b/tests/unit/decode_test.py index adf5c3f..9311e14 100644 --- a/tests/unit/decode_test.py +++ b/tests/unit/decode_test.py @@ -653,6 +653,7 @@ def test_continues_parsing_when_no_parent_is_found( def test_does_not_error_when_parsing_a_very_long_list(self) -> None: buf: str = "a[]=a" + def _approx_size(value: str) -> int: try: return getsizeof(value) diff --git a/tests/unit/example_test.py b/tests/unit/example_test.py index cdc26b8..9fa1d66 100644 --- a/tests/unit/example_test.py +++ b/tests/unit/example_test.py @@ -301,9 +301,7 @@ def custom_decoder(value: t.Any, charset: t.Optional[qs_codec.Charset]): serialize_date=lambda date: str( int( ( - date - if date.tzinfo is not None - else date.replace(tzinfo=datetime.timezone.utc) + date if date.tzinfo is not None else date.replace(tzinfo=datetime.timezone.utc) ).timestamp() ) ), @@ -348,9 +346,7 @@ def custom_decoder(value: t.Any, charset: t.Optional[qs_codec.Charset]): "e[f]": ( int( ( - value - if value.tzinfo is not None - else value.replace(tzinfo=datetime.timezone.utc) + value if value.tzinfo is not None else value.replace(tzinfo=datetime.timezone.utc) ).timestamp() ) if isinstance(value, datetime.datetime)