From bbd6bbd920ca7ecf37260baff254cf5e6270deb9 Mon Sep 17 00:00:00 2001 From: Jim Crist-Harif Date: Sun, 20 Oct 2024 15:07:10 -0500 Subject: [PATCH] Drop Python 3.8 support --- .github/workflows/ci.yml | 6 ++--- .github/workflows/docs.yml | 2 +- msgspec/_core.c | 3 --- msgspec/_utils.py | 17 +++----------- setup.py | 4 ++-- tests/conftest.py | 15 ------------ tests/test_common.py | 48 +++++++++++++------------------------- tests/test_constraints.py | 34 +++------------------------ tests/test_convert.py | 42 +++------------------------------ tests/test_inspect.py | 45 ++++++++--------------------------- tests/test_json.py | 15 +----------- tests/test_schema.py | 28 ++++------------------ tests/utils.py | 3 --- 13 files changed, 47 insertions(+), 215 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06fe5140..0074aa02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: env: CIBW_TEST_REQUIRES: "pytest msgpack pyyaml tomli tomli_w" CIBW_TEST_COMMAND: "pytest {project}/tests" - CIBW_BUILD: "cp38-* cp39-* cp310-* cp311-* cp312-* cp313-*" + CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*" CIBW_SKIP: "*-win32 *_i686 *_s390x *_ppc64le" CIBW_ARCHS_MACOS: "x86_64 arm64" CIBW_ARCHS_LINUX: "x86_64 aarch64" @@ -99,7 +99,7 @@ jobs: - name: Set up Environment if: github.event_name != 'release' run: | - echo "CIBW_SKIP=${CIBW_SKIP} *-musllinux_* cp38-*_aarch64 cp39-*_aarch64 cp311-*_aarch64 cp312-*_aarch64 cp313-*_aarch64" >> $GITHUB_ENV + echo "CIBW_SKIP=${CIBW_SKIP} *-musllinux_* cp39-*_aarch64 cp311-*_aarch64 cp312-*_aarch64 cp313-*_aarch64" >> $GITHUB_ENV - name: Build & Test Wheels uses: pypa/cibuildwheel@v2.21.3 @@ -122,7 +122,7 @@ jobs: - name: Install Python uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: "3.11" - name: Build source distribution run: python setup.py sdist diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 1331198a..687c8322 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,7 +14,7 @@ jobs: - name: Install Python uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: "3.11" - name: Install msgspec and dependencies run: | diff --git a/msgspec/_core.c b/msgspec/_core.c index 1b349c8b..c73362bb 100644 --- a/msgspec/_core.c +++ b/msgspec/_core.c @@ -5529,9 +5529,6 @@ structmeta_collect_base(StructMetaInfo *info, MsgspecState *mod, PyObject *base) if (((PyTypeObject *)base)->tp_dictoffset) { info->has_non_slots_bases = true; } - /* XXX: in Python 3.8 Generic defines __new__, but we can ignore it. - * This can be removed when Python 3.8 support is dropped */ - if (base == mod->typing_generic) return 0; static const char *attrs[] = {"__init__", "__new__"}; Py_ssize_t nattrs = 2; diff --git a/msgspec/_utils.py b/msgspec/_utils.py index 4c94152c..2a8a5715 100644 --- a/msgspec/_utils.py +++ b/msgspec/_utils.py @@ -3,13 +3,7 @@ import sys import typing -try: - from typing_extensions import _AnnotatedAlias -except Exception: - try: - from typing import _AnnotatedAlias - except Exception: - _AnnotatedAlias = None +from typing import _AnnotatedAlias # noqa: F401 try: from typing_extensions import get_type_hints as _get_type_hints @@ -25,13 +19,8 @@ Required = NotRequired = None -if Required is None and _AnnotatedAlias is None: - # No extras available, so no `include_extras` - get_type_hints = _get_type_hints -else: - - def get_type_hints(obj): - return _get_type_hints(obj, include_extras=True) +def get_type_hints(obj): + return _get_type_hints(obj, include_extras=True) # The `is_class` argument was new in 3.11, but was backported to 3.9 and 3.10. diff --git a/setup.py b/setup.py index 300a45bf..887191e1 100644 --- a/setup.py +++ b/setup.py @@ -82,11 +82,11 @@ classifiers=[ "License :: OSI Approved :: BSD License", "Development Status :: 4 - Beta", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], extras_require=extras_require, license="BSD", @@ -99,6 +99,6 @@ else "" ), long_description_content_type="text/markdown", - python_requires=">=3.8", + python_requires=">=3.9", zip_safe=False, ) diff --git a/tests/conftest.py b/tests/conftest.py index 608b8b45..88aa261c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,18 +46,3 @@ def shuffle(self, obj): @pytest.fixture def rand(): yield Rand() - - -@pytest.fixture -def Annotated(): - try: - from typing import Annotated - - return Annotated - except ImportError: - try: - from typing_extensions import Annotated - - return Annotated - except ImportError: - pytest.skip("Annotated types not available") diff --git a/tests/test_common.py b/tests/test_common.py index c5bd92fa..8debf902 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,6 +1,5 @@ from __future__ import annotations -import abc import base64 import collections import datetime @@ -15,6 +14,7 @@ from dataclasses import dataclass, field, make_dataclass from datetime import timedelta from typing import ( + Annotated, ClassVar, Deque, Dict, @@ -44,12 +44,10 @@ UTC = datetime.timezone.utc -PY39 = sys.version_info[:2] >= (3, 9) PY310 = sys.version_info[:2] >= (3, 10) PY311 = sys.version_info[:2] >= (3, 11) PY312 = sys.version_info[:2] >= (3, 12) -py39_plus = pytest.mark.skipif(not PY39, reason="3.9+ only") py310_plus = pytest.mark.skipif(not PY310, reason="3.10+ only") py311_plus = pytest.mark.skipif(not PY311, reason="3.11+ only") py312_plus = pytest.mark.skipif(not PY312, reason="3.12+ only") @@ -156,7 +154,6 @@ class subclass(cls): class TestDecoder: - @py39_plus def test_decoder_runtime_type_parameters(self, proto): dec = proto.Decoder[int](int) assert isinstance(dec, proto.Decoder) @@ -896,8 +893,6 @@ def test_multiple_literals(self): dec.decode(msgspec.msgpack.encode("carrot")) def test_nested_literals(self): - """Python 3.9+ automatically denest literals, can drop this test when - python 3.8 is dropped""" integers = Literal[-1, -2, -3] strings = Literal["apple", "banana"] both = Literal[integers, strings] @@ -2098,10 +2093,6 @@ class Base(cls): class Ex(Base, total=False): c: str - if not hasattr(Ex, "__required_keys__"): - # This should be Python 3.8, builtin typing only - pytest.skip("partially optional TypedDict not supported") - dec = proto.Decoder(Ex) x = {"a": 1, "b": "two", "c": "extra"} @@ -2126,10 +2117,6 @@ def test_required_and_notrequired(self, proto, use_typing_extensions): if not hasattr(ns, "Required"): pytest.skip(f"{module}.Required is not available") - if not hasattr(ns.TypedDict("C", {}), "__required_keys__"): - # This should be Python 3.8, builtin typing only - pytest.skip("partially optional TypedDict not supported") - source = f""" from __future__ import annotations from {module} import TypedDict, Required, NotRequired @@ -3273,7 +3260,6 @@ def test_encode_time_offset_rounds_to_nearest_minute(self, proto, offset, t_str) sol = proto.encode(t_str) assert res == sol - @py39_plus def test_encode_time_zoneinfo(self): import zoneinfo @@ -3743,7 +3729,7 @@ def test_decode_newtype(self, proto): with pytest.raises(ValidationError): proto.decode(proto.encode("bad"), type=UserId2) - def test_decode_annotated_newtype(self, proto, Annotated): + def test_decode_annotated_newtype(self, proto): UserId = NewType("UserId", int) dec = proto.Decoder(Annotated[UserId, msgspec.Meta(ge=0)]) assert dec.decode(proto.encode(1)) == 1 @@ -3751,7 +3737,7 @@ def test_decode_annotated_newtype(self, proto, Annotated): with pytest.raises(ValidationError): dec.decode(proto.encode(-1)) - def test_decode_newtype_annotated(self, proto, Annotated): + def test_decode_newtype_annotated(self, proto): UserId = NewType("UserId", Annotated[int, msgspec.Meta(ge=0)]) dec = proto.Decoder(UserId) assert dec.decode(proto.encode(1)) == 1 @@ -3759,7 +3745,7 @@ def test_decode_newtype_annotated(self, proto, Annotated): with pytest.raises(ValidationError): dec.decode(proto.encode(-1)) - def test_decode_annotated_newtype_annotated(self, proto, Annotated): + def test_decode_annotated_newtype_annotated(self, proto): UserId = Annotated[ NewType("UserId", Annotated[int, msgspec.Meta(ge=0)]), msgspec.Meta(le=10) ] @@ -3975,10 +3961,9 @@ def test_abstract_sequence(self, proto, typ): with pytest.raises(ValidationError, match="Expected `array`, got `str`"): proto.decode(proto.encode("a"), type=typ) - if PY39 or type(typ) is not abc.ABCMeta: - assert proto.decode(msg, type=typ[int]) == sol - with pytest.raises(ValidationError, match="Expected `int`, got `str`"): - proto.decode(proto.encode(["a"]), type=typ[int]) + assert proto.decode(msg, type=typ[int]) == sol + with pytest.raises(ValidationError, match="Expected `int`, got `str`"): + proto.decode(proto.encode(["a"]), type=typ[int]) @pytest.mark.parametrize( "typ", @@ -3996,10 +3981,9 @@ def test_abstract_mapping(self, proto, typ): with pytest.raises(ValidationError, match="Expected `object`, got `str`"): proto.decode(proto.encode("a"), type=typ) - if PY39 or type(typ) is not abc.ABCMeta: - assert proto.decode(msg, type=typ[str, int]) == sol - with pytest.raises(ValidationError, match="Expected `int`, got `str`"): - proto.decode(proto.encode({"a": "b"}), type=typ[str, int]) + assert proto.decode(msg, type=typ[str, int]) == sol + with pytest.raises(ValidationError, match="Expected `int`, got `str`"): + proto.decode(proto.encode({"a": "b"}), type=typ[str, int]) class TestUnset: @@ -4239,7 +4223,7 @@ def test_decode_final(self, proto): with pytest.raises(ValidationError): dec.decode(proto.encode("bad")) - def test_decode_final_annotated(self, proto, Annotated): + def test_decode_final_annotated(self, proto): dec = proto.Decoder(Final[Annotated[int, msgspec.Meta(ge=0)]]) assert dec.decode(proto.encode(1)) == 1 @@ -4323,7 +4307,7 @@ def test_lax_int_from_float(self, proto): with pytest.raises(ValidationError, match="Expected `int`, got `float`"): proto.decode(msg, type=int, strict=False) - def test_lax_int_constr(self, proto, Annotated): + def test_lax_int_constr(self, proto): typ = Annotated[int, Meta(ge=0)] msg = proto.encode("1") assert proto.decode(msg, type=typ, strict=False) == 1 @@ -4370,7 +4354,7 @@ def test_lax_float(self, proto): with pytest.raises(ValidationError, match="Expected `float`, got `str`"): proto.decode(msg, type=float, strict=False) - def test_lax_float_constr(self, proto, Annotated): + def test_lax_float_constr(self, proto): msg = proto.encode("1.5") assert proto.decode(msg, type=Annotated[float, Meta(ge=0)], strict=False) == 1.5 @@ -4383,7 +4367,7 @@ def test_lax_str(self, proto): msg = proto.encode(x) assert proto.decode(msg, type=str, strict=False) == x - def test_lax_str_constr(self, proto, Annotated): + def test_lax_str_constr(self, proto): typ = Annotated[str, Meta(max_length=10)] msg = proto.encode("xxx") assert proto.decode(msg, type=typ, strict=False) == "xxx" @@ -4438,7 +4422,7 @@ def test_lax_datetime_invalid_numeric_str(self, proto): proto.decode(msg, type=datetime.datetime, strict=False) @pytest.mark.parametrize("val", [123, -123, 123.456, "123.456"]) - def test_lax_datetime_naive_required(self, val, proto, Annotated): + def test_lax_datetime_naive_required(self, val, proto): msg = proto.encode(val) with pytest.raises(ValidationError, match="no timezone component"): proto.decode( @@ -4527,7 +4511,7 @@ def test_lax_union_invalid(self, x, proto): ("100.5", "`float` <= 100.0"), ], ) - def test_lax_union_invalid_constr(self, x, err, proto, Annotated): + def test_lax_union_invalid_constr(self, x, err, proto): """Ensure that values that parse properly but don't meet the specified constraints error with a specific constraint error""" msg = proto.encode(x) diff --git a/tests/test_constraints.py b/tests/test_constraints.py index 6dc86110..b85bc999 100644 --- a/tests/test_constraints.py +++ b/tests/test_constraints.py @@ -1,18 +1,10 @@ import datetime import math import re -from typing import Dict, List, Union +from typing import Dict, List, Union, Annotated import pytest -try: - from typing import Annotated -except ImportError: - try: - from typing_extensions import Annotated - except ImportError: - pytestmark = pytest.mark.skip("Annotated types not available") - import msgspec from msgspec import Meta @@ -25,26 +17,6 @@ def proto(request): return msgspec.msgpack -try: - nextafter = math.nextafter -except AttributeError: - - def nextafter(x, towards): - """This isn't a 100% accurate implementation, but is fine - for rough testing of Python 3.8""" - factor = float.fromhex("0x1.fffffffffffffp-1") - - def sign(x): - return -1 if x < 0 else 1 - - scale_up = sign(x) == sign(towards) - if scale_up: - out = (abs(x) / factor) * sign(x) - else: - out = (abs(x) * factor) * sign(x) - return out - - FIELDS = { "gt": 0, "ge": 0, @@ -398,9 +370,9 @@ def floorm1(x): if name.endswith("e"): good = bound - bad = nextafter(bound, -good_dir) + bad = math.nextafter(bound, -good_dir) else: - good = nextafter(bound, good_dir) + good = math.nextafter(bound, good_dir) bad = bound good_cases = [good, good_round(good), float(good_round(good))] bad_cases = [bad, bad_round(bad), float(bad_round(bad))] diff --git a/tests/test_convert.py b/tests/test_convert.py index ea697838..c7eb79af 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -9,6 +9,7 @@ from collections.abc import MutableMapping from dataclasses import dataclass, field from typing import ( + Annotated, Any, Dict, FrozenSet, @@ -19,6 +20,7 @@ Set, Tuple, TypeVar, + TypedDict, Union, ) @@ -28,22 +30,6 @@ import msgspec from msgspec import Meta, Struct, ValidationError, convert, to_builtins -try: - from typing import Annotated -except ImportError: - try: - from typing_extensions import Annotated - except ImportError: - Annotated = None - -try: - from typing import TypedDict -except ImportError: - try: - from typing_extensions import TypedDict - except ImportError: - TypedDict = None - try: import attrs except ImportError: @@ -53,8 +39,6 @@ PY311 = sys.version_info[:2] >= (3, 11) PY312 = sys.version_info[:2] >= (3, 12) -uses_annotated = pytest.mark.skipif(Annotated is None, reason="Annotated not available") - UTC = datetime.timezone.utc T = TypeVar("T") @@ -365,7 +349,6 @@ class myint(int): ("lt", -1, [-(2**64), -2], [-1, 2**63, 2**65]), ], ) - @uses_annotated def test_int_constr_bounds(self, name, bound, good, bad): class Ex(Struct): x: Annotated[int, Meta(**{name: bound})] @@ -380,7 +363,6 @@ class Ex(Struct): with pytest.raises(ValidationError, match=err_msg): convert({"x": x}, Ex) - @uses_annotated def test_int_constr_multiple_of(self): class Ex(Struct): x: Annotated[int, Meta(multiple_of=2)] @@ -402,7 +384,6 @@ class Ex(Struct): (Meta(ge=0, le=10), [0, 10], [-1, 11]), ], ) - @uses_annotated def test_int_constrs(self, meta, good, bad): class Ex(Struct): x: Annotated[int, meta] @@ -448,7 +429,6 @@ def test_float(self): (Meta(ge=0.0, le=10.0), [0.0, 2.0, 10.0], [-1.0, 11.5, 11]), ], ) - @uses_annotated def test_float_constrs(self, meta, good, bad): class Ex(Struct): x: Annotated[float, meta] @@ -465,7 +445,6 @@ def test_float_from_decimal(self): assert res == 1.5 assert type(res) is float - @uses_annotated def test_constr_float_from_decimal(self): typ = Annotated[float, Meta(ge=0)] res = convert(decimal.Decimal("1.5"), typ) @@ -496,7 +475,6 @@ def test_str(self): (Meta(max_length=3, pattern="x"), ["xy", "xyz"], ["y", "wxyz"]), ], ) - @uses_annotated def test_str_constrs(self, meta, good, bad): class Ex(Struct): x: Annotated[str, meta] @@ -535,7 +513,6 @@ def test_binary_base64_disabled(self, out_type): @pytest.mark.parametrize("in_type", [bytes, bytearray, memoryview, str]) @pytest.mark.parametrize("out_type", [bytes, bytearray, memoryview]) - @uses_annotated def test_binary_constraints(self, in_type, out_type): class Ex(Struct): x: Annotated[out_type, Meta(min_length=2, max_length=4)] @@ -600,7 +577,6 @@ def test_datetime_str_disabled(self): ) @pytest.mark.parametrize("as_str", [False, True]) - @uses_annotated def test_datetime_constrs(self, as_str): class Ex(Struct): x: Annotated[datetime.datetime, Meta(tz=True)] @@ -639,7 +615,6 @@ def test_time_str_disabled(self): convert("12:34:00Z", datetime.time, builtin_types=(datetime.time,)) @pytest.mark.parametrize("as_str", [False, True]) - @uses_annotated def test_time_constrs(self, as_str): class Ex(Struct): x: Annotated[datetime.time, Meta(tz=True)] @@ -1018,7 +993,6 @@ class Cache(Struct): convert(msg, Cache) @pytest.mark.parametrize("out_type", [list, tuple, set, frozenset]) - @uses_annotated def test_sequence_constrs(self, out_type): class Ex(Struct): x: Annotated[out_type, Meta(min_length=2, max_length=4)] @@ -1239,7 +1213,6 @@ class Cache(Struct): with max_call_depth(5): convert(msg, Cache) - @uses_annotated def test_dict_constrs(self, dictcls): class Ex(Struct): x: Annotated[dict, Meta(min_length=2, max_length=4)] @@ -1254,7 +1227,6 @@ class Ex(Struct): convert(dictcls({"x": x}), Ex) -@pytest.mark.skipif(TypedDict is None, reason="TypedDict not available") class TestTypedDict: def test_typeddict_total_true(self): class Ex(TypedDict): @@ -1304,10 +1276,6 @@ class Base(TypedDict): class Ex(Base, total=False): c: str - if not hasattr(Ex, "__required_keys__"): - # This should be Python 3.8, builtin typing only - pytest.skip("partially optional TypedDict not supported") - x = {"a": 1, "b": "two", "c": "extra"} assert convert(x, Ex) == x @@ -2349,7 +2317,6 @@ def test_lax_int_from_float(self): with pytest.raises(ValidationError, match="Expected `int`, got `float`"): convert(x, int, strict=False) - @uses_annotated def test_lax_int_constr(self): typ = Annotated[int, Meta(ge=0)] assert convert("1", typ, strict=False) == 1 @@ -2404,7 +2371,6 @@ def test_lax_float_nonfinite_invalid(self): ): convert(msg, float, strict=False) - @uses_annotated def test_lax_float_constr(self): assert convert("1.5", Annotated[float, Meta(ge=0)], strict=False) == 1.5 @@ -2415,7 +2381,6 @@ def test_lax_str(self): for x in ["1", "1.5", "false", "null"]: assert convert(x, str, strict=False) == x - @uses_annotated def test_lax_str_constr(self): typ = Annotated[str, Meta(max_length=10)] assert convert("xxx", typ, strict=False) == "xxx" @@ -2464,7 +2429,7 @@ def test_lax_datetime_invalid_numeric_str(self): convert(msg, type=datetime.datetime, strict=False) @pytest.mark.parametrize("msg", [123, -123, 123.456, "123.456"]) - def test_lax_datetime_naive_required(self, msg, Annotated): + def test_lax_datetime_naive_required(self, msg): with pytest.raises(ValidationError, match="no timezone component"): convert( msg, type=Annotated[datetime.datetime, Meta(tz=False)], strict=False @@ -2545,7 +2510,6 @@ def test_lax_union_invalid(self, msg): ("100.5", "`float` <= 100.0"), ], ) - @uses_annotated def test_lax_union_invalid_constr(self, msg, err): """Ensure that values that parse properly but don't meet the specified constraints error with a specific constraint error""" diff --git a/tests/test_inspect.py b/tests/test_inspect.py index 291fd99c..cf0d063c 100644 --- a/tests/test_inspect.py +++ b/tests/test_inspect.py @@ -1,4 +1,3 @@ -import abc import collections import datetime import decimal @@ -10,6 +9,7 @@ from copy import deepcopy from dataclasses import dataclass, field from typing import ( + Annotated, Any, Dict, Final, @@ -33,30 +33,13 @@ import msgspec.inspect as mi from msgspec import Meta -try: - from typing import Annotated -except ImportError: - try: - from typing_extensions import Annotated - except ImportError: - pytestmark = pytest.mark.skip("Annotated types not available") - -PY39 = sys.version_info[:2] >= (3, 9) PY312 = sys.version_info[:2] >= (3, 12) - py312_plus = pytest.mark.skipif(not PY312, reason="3.12+ only") T = TypeVar("T") -def type_index(typ, args): - try: - return typ[args] - except TypeError: - pytest.skip("Not supported in Python 3.8") - - @pytest.mark.parametrize( "a,b,sol", [ @@ -226,7 +209,7 @@ def test_typealias(src, typ): assert mi.type_info(mod.Ex) == mi.type_info(typ) -def test_final(Annotated): +def test_final(): cases = [ (int, mi.IntType()), (Annotated[int, Meta(ge=0)], mi.IntType(ge=0)), @@ -267,9 +250,9 @@ def test_sequence(kw, typ, info_type, has_item_type): if has_item_type: item_type = mi.IntType() if info_type is mi.VarTupleType: - typ = type_index(typ, (int, ...)) + typ = typ[int, ...] else: - typ = type_index(typ, int) + typ = typ[int] else: item_type = mi.AnyType() @@ -282,11 +265,9 @@ def test_sequence(kw, typ, info_type, has_item_type): @pytest.mark.parametrize("typ", [Tuple, tuple]) def test_tuple(typ): - assert mi.type_info(type_index(typ, ())) == mi.TupleType(()) - assert mi.type_info(type_index(typ, int)) == mi.TupleType((mi.IntType(),)) - assert mi.type_info(type_index(typ, (int, float))) == mi.TupleType( - (mi.IntType(), mi.FloatType()) - ) + assert mi.type_info(typ[()]) == mi.TupleType(()) + assert mi.type_info(typ[int]) == mi.TupleType((mi.IntType(),)) + assert mi.type_info(typ[int, float]) == mi.TupleType((mi.IntType(), mi.FloatType())) @pytest.mark.parametrize("typ", [Dict, dict]) @@ -294,7 +275,7 @@ def test_tuple(typ): @pytest.mark.parametrize("has_args", [False, True]) def test_dict(typ, kw, has_args): if has_args: - typ = type_index(typ, (int, float)) + typ = typ[int, float] key = mi.IntType() val = mi.FloatType() else: @@ -327,8 +308,7 @@ def test_abstract_sequence(typ): col_type = mi.ListType assert mi.type_info(typ) == col_type(mi.AnyType()) - if PY39 or type(typ) is not abc.ABCMeta: - assert mi.type_info(typ[int]) == col_type(mi.IntType()) + assert mi.type_info(typ[int]) == col_type(mi.IntType()) @pytest.mark.parametrize( @@ -342,8 +322,7 @@ def test_abstract_sequence(typ): ) def test_abstract_mapping(typ): assert mi.type_info(typ) == mi.DictType(mi.AnyType(), mi.AnyType()) - if PY39 or type(typ) is not abc.ABCMeta: - assert mi.type_info(typ[str, int]) == mi.DictType(mi.StrType(), mi.IntType()) + assert mi.type_info(typ[str, int]) == mi.DictType(mi.StrType(), mi.IntType()) @pytest.mark.parametrize("use_union_operator", [False, True]) @@ -587,10 +566,6 @@ class Base(cls): class Example(Base, total=False): c: int - if not hasattr(Example, "__required_keys__"): - # This should be Python 3.8, builtin typing only - pytest.skip("partially optional TypedDict not supported") - sol = mi.TypedDictType( Example, fields=( diff --git a/tests/test_json.py b/tests/test_json.py index 7c0af6fc..1d3776e5 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -14,6 +14,7 @@ from dataclasses import dataclass from decimal import Decimal from typing import ( + Annotated, Any, Dict, FrozenSet, @@ -33,9 +34,6 @@ UTC = datetime.timezone.utc -PY39 = sys.version_info[:2] >= (3, 9) -py39_plus = pytest.mark.skipif(not PY39, reason="3.9+ only") - class FruitInt(enum.IntEnum): APPLE = -1 @@ -842,7 +840,6 @@ def test_encode_datetime_offset_rounds_to_nearest_minute(self, offset, expected) s = msgspec.json.encode(x) assert s == expected - @py39_plus def test_encode_datetime_zoneinfo(self): import zoneinfo @@ -1958,11 +1955,6 @@ def test_decode_dict_enum_key(self): dec.decode(b'{"apple": 1, "carrot": 2}') def test_decode_dict_str_key_constraints(self): - try: - from typing import Annotated - except ImportError: - pytest.skip("Annotated types not available") - dec = msgspec.json.Decoder( Dict[Annotated[str, msgspec.Meta(min_length=3)], int] ) @@ -2058,11 +2050,6 @@ def test_decode_dict_big_int(self, x): assert type(list(res)[0]) is int def test_decode_dict_int_key_constraints(self): - try: - from typing import Annotated - except ImportError: - pytest.skip("Annotated types not available") - dec = msgspec.json.Decoder(Dict[Annotated[int, msgspec.Meta(ge=3)], int]) assert dec.decode(b'{"4": 1, "5": 2}') == {4: 1, 5: 2} diff --git a/tests/test_schema.py b/tests/test_schema.py index a4ffb9f4..e1b9df3c 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -7,6 +7,7 @@ from collections import namedtuple from dataclasses import dataclass from typing import ( + Annotated, Any, Dict, FrozenSet, @@ -28,25 +29,10 @@ import msgspec from msgspec import Meta -try: - from typing import Annotated -except ImportError: - try: - from typing_extensions import Annotated - except ImportError: - pytestmark = pytest.mark.skip("Annotated types not available") - T = TypeVar("T") -def type_index(typ, args): - try: - return typ[args] - except TypeError: - pytest.skip("Not supported in Python 3.8") - - def test_any(): assert msgspec.json.schema(Any) == {} @@ -199,13 +185,13 @@ def test_sequence_any(typ): ) def test_sequence_typed(cls): args = (int, ...) if cls in (tuple, Tuple) else int - typ = type_index(cls, args) + typ = cls[args] assert msgspec.json.schema(typ) == {"type": "array", "items": {"type": "integer"}} @pytest.mark.parametrize("cls", [tuple, Tuple]) def test_tuple(cls): - typ = type_index(cls, (int, float, str)) + typ = cls[int, float, str] assert msgspec.json.schema(typ) == { "type": "array", "minItems": 3, @@ -221,7 +207,7 @@ def test_tuple(cls): @pytest.mark.parametrize("cls", [tuple, Tuple]) def test_empty_tuple(cls): - typ = type_index(cls, ()) + typ = cls[()] assert msgspec.json.schema(typ) == { "type": "array", "minItems": 0, @@ -236,7 +222,7 @@ def test_dict_any(typ): @pytest.mark.parametrize("cls", [dict, Dict]) def test_dict_typed(cls): - typ = type_index(cls, (str, int)) + typ = cls[str, int] assert msgspec.json.schema(typ) == { "type": "object", "additionalProperties": {"type": "integer"}, @@ -640,10 +626,6 @@ class Example(Base, total=False): c: int - if not hasattr(Example, "__required_keys__"): - # This should be Python 3.8, builtin typing only - pytest.skip("partially optional TypedDict not supported") - assert msgspec.json.schema(Example) == { "$ref": "#/$defs/Example", "$defs": { diff --git a/tests/utils.py b/tests/utils.py index e95788ca..7f684eea 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -32,9 +32,6 @@ def max_call_depth(n): # set a recursionlimit < the current depth will raise a RecursionError. # We just try again with a slightly higher limit, bailing after an # unreasonable amount of adjustments. - # - # Note that python 3.8 also has a minimum recursion limit of 64, so - # there's some additional fiddliness there. for i in range(64): try: sys.setrecursionlimit(cur_depth + i + n)