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/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 ----- 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/tests/unit/decode_test.py b/tests/unit/decode_test.py index cbf370f..9311e14 100644 --- a/tests/unit/decode_test.py +++ b/tests/unit/decode_test.py @@ -653,7 +653,15 @@ 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 diff --git a/tests/unit/example_test.py b/tests/unit/example_test.py index 38ab51c..9fa1d66 100644 --- a/tests/unit/example_test.py +++ b/tests/unit/example_test.py @@ -296,7 +296,16 @@ 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 +343,15 @@ 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), ), 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 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