Skip to content

Commit 6c76c80

Browse files
authored
Drop Python 3.9 support (#342)
Coming from #331 (comment) Airflow dropped support: - apache/airflow#56734 - apache/airflow#57662
1 parent 4d21700 commit 6c76c80

File tree

8 files changed

+55
-699
lines changed

8 files changed

+55
-699
lines changed

.github/workflows/ci.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ jobs:
2323
fail-fast: true
2424
matrix:
2525
include:
26-
- os: ubuntu-latest
27-
python-version: "3.9"
2826
- os: ubuntu-latest
2927
python-version: "3.10"
3028
- os: ubuntu-latest

cadwyn/_utils.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,11 @@
1818
from asyncio import iscoroutinefunction # noqa: F401
1919

2020

21-
if sys.version_info >= (3, 10):
22-
UnionType = type(int | str) | type(Union[int, str])
23-
DATACLASS_SLOTS: dict[str, Any] = {"slots": True}
24-
ZIP_STRICT_TRUE: dict[str, Any] = {"strict": True}
25-
ZIP_STRICT_FALSE: dict[str, Any] = {"strict": False}
26-
DATACLASS_KW_ONLY: dict[str, Any] = {"kw_only": True}
27-
else:
28-
UnionType = type(Union[int, str])
29-
DATACLASS_SLOTS: dict[str, Any] = {}
30-
DATACLASS_KW_ONLY: dict[str, Any] = {}
31-
ZIP_STRICT_TRUE: dict[str, Any] = {}
32-
ZIP_STRICT_FALSE: dict[str, Any] = {}
21+
UnionType = type(int | str) | type(Union[int, str])
22+
DATACLASS_SLOTS: dict[str, Any] = {"slots": True}
23+
ZIP_STRICT_TRUE: dict[str, Any] = {"strict": True}
24+
ZIP_STRICT_FALSE: dict[str, Any] = {"strict": False}
25+
DATACLASS_KW_ONLY: dict[str, Any] = {"kw_only": True}
3326

3427

3528
def get_name_of_function_wrapped_in_pydantic_validator(func: Any) -> str:

cadwyn/schema_generation.py

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import ast
21
import copy
32
import dataclasses
43
import functools
54
import inspect
65
import sys
7-
import textwrap
86
import types
97
import typing
108
from collections.abc import Callable, Sequence
@@ -297,21 +295,7 @@ def _rebuild_annotated(name: str):
297295
for name, value in model.__annotations__.items()
298296
}
299297

300-
if sys.version_info >= (3, 10):
301-
defined_fields = model.__annotations__
302-
else:
303-
# Before 3.9, pydantic fills model_fields with all fields -- even the ones that were inherited.
304-
# So we need to get the list of fields from the AST.
305-
try:
306-
defined_fields, _ = _get_field_and_validator_names_from_model(model)
307-
except OSError: # pragma: no cover
308-
defined_fields = model.model_fields
309-
annotations = {
310-
name: value
311-
for name, value in annotations.items()
312-
# We need to filter out fields that were inherited
313-
if name not in model.model_fields or name in defined_fields
314-
}
298+
defined_fields = model.__annotations__
315299
fields = {
316300
field_name: PydanticFieldWrapper(
317301
model.model_fields[field_name],
@@ -346,43 +330,6 @@ def _rebuild_annotated(name: str):
346330
)
347331

348332

349-
@cache
350-
def _get_field_and_validator_names_from_model(cls: type) -> tuple[set[_FieldName], set[str]]:
351-
fields = cls.model_fields
352-
source = inspect.getsource(cls)
353-
cls_ast = cast("ast.ClassDef", ast.parse(textwrap.dedent(source)).body[0])
354-
validator_names = (
355-
_get_validator_info_or_none(node)
356-
for node in cls_ast.body
357-
if isinstance(node, ast.FunctionDef) and node.decorator_list
358-
)
359-
validator_names = {name for name in validator_names if name is not None}
360-
361-
return (
362-
{
363-
node.target.id
364-
for node in cls_ast.body
365-
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.target.id in fields
366-
},
367-
validator_names,
368-
)
369-
370-
371-
def _get_validator_info_or_none(method: ast.FunctionDef) -> Union[str, None]:
372-
for decorator in method.decorator_list:
373-
# The cases we handle here:
374-
# * `Name(id="root_validator")`
375-
# * `Call(func=Name(id="validator"), args=[Constant(value="foo")])`
376-
# * `Attribute(value=Name(id="pydantic"), attr="root_validator")`
377-
# * `Call(func=Attribute(value=Name(id="pydantic"), attr="root_validator"), args=[])`
378-
379-
if (isinstance(decorator, ast.Call) and ast.unparse(decorator.func).endswith("validator")) or (
380-
isinstance(decorator, (ast.Name, ast.Attribute)) and ast.unparse(decorator).endswith("validator")
381-
):
382-
return method.name
383-
return None
384-
385-
386333
@final
387334
@dataclasses.dataclass(**DATACLASS_SLOTS)
388335
class _PydanticModelWrapper(Generic[_T_PYDANTIC_MODEL]):

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description = "Production-ready community-driven modern Stripe-like API versioni
55
authors = [{ name = "Stanislav Zmiev", email = "zmievsa@gmail.com" }]
66
license = "MIT"
77
readme = "README.md"
8-
requires-python = ">=3.9"
8+
requires-python = ">=3.10"
99
keywords = [
1010
"python",
1111
"api",
@@ -29,7 +29,6 @@ classifiers = [
2929
"Operating System :: OS Independent",
3030
"Programming Language :: Python",
3131
"Programming Language :: Python :: 3",
32-
"Programming Language :: Python :: 3.9",
3332
"Programming Language :: Python :: 3.10",
3433
"Programming Language :: Python :: 3.11",
3534
"Programming Language :: Python :: 3.12",

tests/test_render.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,8 @@ def test__render_model__with_weird_types():
2626
else:
2727
rend_ann = "Annotated"
2828

29-
if sys.version_info >= (3, 10):
30-
rend_len = "Len(min_length=0, max_length=None)"
31-
rend_interval = "Interval(gt=12, ge=None, lt=None, le=None)"
32-
else:
33-
rend_len = "Len()"
34-
rend_interval = "Interval(gt=12)"
29+
rend_len = "Len(min_length=0, max_length=None)"
30+
rend_interval = "Interval(gt=12, ge=None, lt=None, le=None)"
3531

3632
# TODO: As you see, we do not rename bases correctly in render. We gotta fix it some day...
3733
assert code(result) == code(

tests/test_router_generation_with_from_future_annotations.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
from __future__ import annotations
22

3-
import sys
43
from typing import Annotated
54

6-
import pytest
75
from fastapi import Depends, Request
86
from fastapi.testclient import TestClient
97
from pydantic import BaseModel, Field, WithJsonSchema
@@ -80,10 +78,6 @@ async def __call__(self, request: Request) -> None:
8078
pass
8179

8280

83-
@pytest.mark.skipif(
84-
sys.version_info < (3, 10),
85-
reason="FastAPI doesn't properly resolve Request forward refs in callable class dependencies on Python 3.9",
86-
)
8781
def test__router_generation__using_callable_class_dependency_with_forwardref():
8882
"""Test that callable class instances work as dependencies with future annotations.
8983

tox.ini

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ envlist =
44
# When updating Python versions, use search-and-replace
55
# against the entire list of of versions
66
# to ensure consistency throughout this file.
7-
py{3.13, 3.12, 3.11, 3.10, 3.9}
7+
py{3.13, 3.12, 3.11, 3.10}
88
coverage_report
99
docs
1010
pyright
@@ -18,7 +18,7 @@ extras =
1818
package = wheel
1919
wheel_build_env = build_wheel
2020
depends =
21-
py{3.13, 3.12, 3.11, 3.10, 3.9}: coverage_erase
21+
py{3.13, 3.12, 3.11, 3.10}: coverage_erase
2222
commands = coverage run -m pytest {posargs}
2323

2424

@@ -30,7 +30,7 @@ commands = coverage erase
3030
[testenv:coverage_report]
3131
skip_install = true
3232
depends =
33-
py{3.13, 3.12, 3.11, 3.10, 3.9}
33+
py{3.13, 3.12, 3.11, 3.10}
3434
commands_pre =
3535
# Ignore the exit code of `coverage combine`
3636
# (in case the reports are already combined).

0 commit comments

Comments
 (0)