Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drop Python 3.8 support #756

Merged
merged 1 commit into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading