Skip to content

Commit

Permalink
Drop Python 3.8 support
Browse files Browse the repository at this point in the history
  • Loading branch information
jcrist committed Oct 20, 2024
1 parent 74ebfe0 commit bbd6bbd
Show file tree
Hide file tree
Showing 13 changed files with 47 additions and 215 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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/[email protected]
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
3 changes: 0 additions & 3 deletions msgspec/_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 3 additions & 14 deletions msgspec/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -99,6 +99,6 @@
else ""
),
long_description_content_type="text/markdown",
python_requires=">=3.8",
python_requires=">=3.9",
zip_safe=False,
)
15 changes: 0 additions & 15 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
48 changes: 16 additions & 32 deletions tests/test_common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import abc
import base64
import collections
import datetime
Expand All @@ -15,6 +14,7 @@
from dataclasses import dataclass, field, make_dataclass
from datetime import timedelta
from typing import (
Annotated,
ClassVar,
Deque,
Dict,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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"}
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -3743,23 +3729,23 @@ 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

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

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)
]
Expand Down Expand Up @@ -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",
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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"
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
34 changes: 3 additions & 31 deletions tests/test_constraints.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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))]
Expand Down
Loading

0 comments on commit bbd6bbd

Please sign in to comment.