Skip to content

Commit 6622b36

Browse files
authored
Add python 3.9 support (#254)
1 parent 6b35875 commit 6622b36

30 files changed

+795
-507
lines changed

.github/workflows/ci.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ jobs:
2222
fail-fast: true
2323
matrix:
2424
include:
25+
- os: ubuntu-latest
26+
python-version: "3.9"
2527
- os: ubuntu-latest
2628
python-version: "3.10"
2729
- os: ubuntu-latest
@@ -82,7 +84,7 @@ jobs:
8284
path: .
8385
- uses: actions/setup-python@v5
8486
with:
85-
python-version: "3.10"
87+
python-version: "3.12"
8688
- run: |
8789
pip install uv
8890
uv pip install --system tox tox-uv
@@ -99,7 +101,7 @@ jobs:
99101
- uses: actions/checkout@v4
100102
- uses: actions/setup-python@v5
101103
with:
102-
python-version: "3.10"
104+
python-version: "3.12"
103105
- run: |
104106
python -m pip install uv
105107
uv pip install --system pre-commit pre-commit-uv
@@ -113,7 +115,7 @@ jobs:
113115
with:
114116
# When this version is updated,
115117
# update the pyright `base_python` version in `tox.ini`, too.
116-
python-version: "3.10"
118+
python-version: "3.12"
117119
- run: tox run -e docs
118120
- name: Validate links
119121
uses: umbrelladocs/action-linkspector@v1
@@ -130,5 +132,5 @@ jobs:
130132
with:
131133
# When this version is updated,
132134
# update the pyright `base_python` version in `tox.ini`, too.
133-
python-version: "3.10"
135+
python-version: "3.12"
134136
- run: tox run -e pyright

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ Please follow [the Keep a Changelog standard](https://keepachangelog.com/en/1.0.
55

66
## [Unreleased]
77

8+
## [5.1.0]
9+
10+
### Added
11+
12+
* Support for python 3.9
13+
814
## [5.0.0]
915

1016
### Added

cadwyn/_asts.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
import inspect
33
from collections.abc import Callable
44
from enum import Enum, auto
5-
from types import GenericAlias, LambdaType, NoneType
5+
from types import GenericAlias, LambdaType
66
from typing import ( # noqa: UP035
77
Any,
88
List,
9+
Union,
910
cast,
1011
get_args,
1112
get_origin,
@@ -17,6 +18,7 @@
1718
from cadwyn.exceptions import InvalidGenerationInstructionError
1819

1920
_LambdaFunctionName = (lambda: None).__name__ # pragma: no branch
21+
NoneType = type(None)
2022

2123

2224
# A parent type of typing._GenericAlias
@@ -25,17 +27,18 @@
2527
# type(list[int]) and type(List[int]) are different which is why we have to do this.
2628
# Please note that this problem is much wider than just lists which is why we use typing._BaseGenericAlias
2729
# instead of typing._GenericAlias.
28-
GenericAliasUnion = GenericAlias | _BaseGenericAlias
30+
GenericAliasUnion = Union[GenericAlias, _BaseGenericAlias]
31+
GenericAliasUnionArgs = get_args(GenericAliasUnion)
2932

3033

3134
def get_fancy_repr(value: Any) -> Any:
3235
if isinstance(value, annotated_types.GroupedMetadata) and hasattr(type(value), "__dataclass_fields__"):
3336
return transform_grouped_metadata(value)
34-
if isinstance(value, list | tuple | set | frozenset):
37+
if isinstance(value, (list, tuple, set, frozenset)):
3538
return transform_collection(value)
3639
if isinstance(value, dict):
3740
return transform_dict(value)
38-
if isinstance(value, GenericAliasUnion):
41+
if isinstance(value, GenericAliasUnionArgs):
3942
return transform_generic_alias(value)
4043
if value is None or value is NoneType:
4144
return transform_none(value)
@@ -46,7 +49,7 @@ def get_fancy_repr(value: Any) -> Any:
4649
if isinstance(value, auto): # pragma: no cover # it works but we no longer use auto
4750
return transform_auto(value)
4851
if isinstance(value, UnionType):
49-
return transform_union(value)
52+
return transform_union(value) # pragma: no cover
5053
if isinstance(value, LambdaType) and _LambdaFunctionName == value.__name__:
5154
return transform_lambda(value)
5255
if inspect.isfunction(value):
@@ -72,7 +75,7 @@ def transform_grouped_metadata(value: "annotated_types.GroupedMetadata"):
7275
)
7376

7477

75-
def transform_collection(value: list | tuple | set | frozenset) -> Any:
78+
def transform_collection(value: Union[list, tuple, set, frozenset]) -> Any:
7679
return PlainRepr(value.__class__(map(get_fancy_repr, value)))
7780

7881

@@ -102,7 +105,7 @@ def transform_auto(_: auto) -> Any: # pragma: no cover # it works but we no lon
102105
return PlainRepr("auto()")
103106

104107

105-
def transform_union(value: UnionType) -> Any:
108+
def transform_union(value: UnionType) -> Any: # pragma: no cover
106109
return "typing.Union[" + (", ".join(get_fancy_repr(a) for a in get_args(value))) + "]"
107110

108111

cadwyn/_render.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import inspect
33
import textwrap
44
from enum import Enum
5-
from typing import TYPE_CHECKING
5+
from typing import TYPE_CHECKING, Union
66

77
import typer
88
from issubclass import issubclass as lenient_issubclass
@@ -30,7 +30,7 @@ def render_module_by_path(module_path: str, app_path: str, version: str):
3030
attributes_to_alter = [
3131
name
3232
for name, value in module.__dict__.items()
33-
if lenient_issubclass(value, Enum | BaseModel) and value.__module__ == module.__name__
33+
if lenient_issubclass(value, (Enum, BaseModel)) and value.__module__ == module.__name__
3434
]
3535

3636
try:
@@ -53,12 +53,12 @@ def render_module_by_path(module_path: str, app_path: str, version: str):
5353

5454
def render_model_by_path(model_path: str, app_path: str, version: str) -> str:
5555
# cadwyn render model schemas:MySchema --app=run:app --version=2000-01-01
56-
model: type[BaseModel | Enum] = import_attribute_from_string(model_path)
56+
model: type[Union[BaseModel, Enum]] = import_attribute_from_string(model_path)
5757
app: Cadwyn = import_attribute_from_string(app_path)
5858
return render_model(model, app.versions, version)
5959

6060

61-
def render_model(model: type[BaseModel | Enum], versions: VersionBundle, version: str) -> str:
61+
def render_model(model: type[Union[BaseModel, Enum]], versions: VersionBundle, version: str) -> str:
6262
try:
6363
original_cls_node = ast.parse(textwrap.dedent(inspect.getsource(model))).body[0]
6464
except (OSError, SyntaxError, ValueError): # pragma: no cover
@@ -71,7 +71,7 @@ def render_model(model: type[BaseModel | Enum], versions: VersionBundle, version
7171

7272

7373
def _render_model_from_ast(
74-
model_ast: ast.ClassDef, model: type[BaseModel | Enum], versions: VersionBundle, version: str
74+
model_ast: ast.ClassDef, model: type[Union[BaseModel, Enum]], versions: VersionBundle, version: str
7575
):
7676
versioned_models = generate_versioned_models(versions)
7777
generator = versioned_models[version]
@@ -97,7 +97,7 @@ def _render_enum_model(wrapper: _EnumWrapper, original_cls_node: ast.ClassDef):
9797
]
9898

9999
old_body = [
100-
n for n in original_cls_node.body if not isinstance(n, ast.AnnAssign | ast.Assign | ast.Pass | ast.Constant)
100+
n for n in original_cls_node.body if not isinstance(n, (ast.AnnAssign, ast.Assign, ast.Pass, ast.Constant))
101101
]
102102
docstring = pop_docstring_from_cls_body(old_body)
103103

@@ -130,7 +130,7 @@ def _render_pydantic_model(wrapper: _PydanticModelWrapper, original_cls_node: as
130130
n
131131
for n in original_cls_node.body
132132
if not (
133-
isinstance(n, ast.AnnAssign | ast.Assign | ast.Pass | ast.Constant)
133+
isinstance(n, (ast.AnnAssign, ast.Assign, ast.Pass, ast.Constant))
134134
or (isinstance(n, ast.FunctionDef) and n.name in wrapper.validators)
135135
)
136136
]

cadwyn/_utils.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,40 @@
1+
import sys
12
from collections.abc import Callable
23
from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union
34

45
from pydantic._internal._decorators import unwrap_wrapped_function
56

67
Sentinel: Any = object()
7-
UnionType = type(int | str) | type(Union[int, str])
8+
89
_T = TypeVar("_T", bound=Callable)
910

1011

1112
_P_T = TypeVar("_P_T")
1213
_P_R = TypeVar("_P_R")
1314

1415

16+
if sys.version_info >= (3, 10):
17+
UnionType = type(int | str) | type(Union[int, str])
18+
DATACLASS_SLOTS: dict[str, Any] = {"slots": True}
19+
ZIP_STRICT_TRUE: dict[str, Any] = {"strict": True}
20+
ZIP_STRICT_FALSE: dict[str, Any] = {"strict": False}
21+
DATACLASS_KW_ONLY: dict[str, Any] = {"kw_only": True}
22+
else:
23+
UnionType = type(Union[int, str])
24+
DATACLASS_SLOTS: dict[str, Any] = {}
25+
DATACLASS_KW_ONLY: dict[str, Any] = {}
26+
ZIP_STRICT_TRUE: dict[str, Any] = {}
27+
ZIP_STRICT_FALSE: dict[str, Any] = {}
28+
29+
30+
def get_name_of_function_wrapped_in_pydantic_validator(func: Any) -> str:
31+
if hasattr(func, "wrapped"):
32+
return get_name_of_function_wrapped_in_pydantic_validator(func.wrapped)
33+
if hasattr(func, "__func__"):
34+
return get_name_of_function_wrapped_in_pydantic_validator(func.__func__)
35+
return func.__name__
36+
37+
1538
class classproperty(Generic[_P_T, _P_R]): # noqa: N801
1639
def __init__(self, func: Callable[[_P_T], _P_R]) -> None:
1740
super().__init__()
@@ -49,7 +72,7 @@ def fully_unwrap_decorator(func: Callable, is_pydantic_v1_style_validator: Any):
4972

5073
else:
5174

52-
def lenient_issubclass(cls: type, other: T | tuple[T, ...]) -> bool:
75+
def lenient_issubclass(cls: type, other: Union[T, tuple[T, ...]]) -> bool:
5376
try:
5477
return issubclass(cls, other)
5578
except TypeError: # pragma: no cover

cadwyn/applications.py

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from datetime import date
55
from logging import getLogger
66
from pathlib import Path
7-
from typing import TYPE_CHECKING, Annotated, Any, cast
7+
from typing import TYPE_CHECKING, Annotated, Any, Union, cast
88

99
import fastapi
1010
from fastapi import APIRouter, FastAPI, HTTPException, routing
@@ -26,7 +26,7 @@
2626
from starlette.types import Lifespan
2727
from typing_extensions import Self, assert_never, deprecated
2828

29-
from cadwyn._utils import same_definition_as_in
29+
from cadwyn._utils import DATACLASS_SLOTS, same_definition_as_in
3030
from cadwyn.changelogs import CadwynChangelogResource, _generate_changelog
3131
from cadwyn.exceptions import CadwynStructureError
3232
from cadwyn.middleware import (
@@ -48,7 +48,7 @@
4848
logger = getLogger(__name__)
4949

5050

51-
@dataclasses.dataclass(slots=True)
51+
@dataclasses.dataclass(**DATACLASS_SLOTS)
5252
class FakeDependencyOverridesProvider:
5353
dependency_overrides: dict[Callable[..., Any], Callable[..., Any]]
5454

@@ -61,7 +61,7 @@ def __init__(
6161
*,
6262
versions: VersionBundle,
6363
api_version_header_name: Annotated[
64-
str | None,
64+
Union[str, None],
6565
deprecated(
6666
"api_version_header_name is deprecated and will be removed in the future. "
6767
"Use api_version_parameter_name instead."
@@ -70,49 +70,51 @@ def __init__(
7070
api_version_location: APIVersionLocation = "custom_header",
7171
api_version_format: APIVersionFormat = "date",
7272
api_version_parameter_name: str = "x-api-version",
73-
api_version_default_value: str | None | Callable[[Request], Awaitable[str]] = None,
73+
api_version_default_value: Union[str, None, Callable[[Request], Awaitable[str]]] = None,
7474
versioning_middleware_class: type[VersionPickingMiddleware] = VersionPickingMiddleware,
75-
changelog_url: str | None = "/changelog",
75+
changelog_url: Union[str, None] = "/changelog",
7676
include_changelog_url_in_schema: bool = True,
7777
debug: bool = False,
7878
title: str = "FastAPI",
79-
summary: str | None = None,
79+
summary: Union[str, None] = None,
8080
description: str = "",
8181
version: str = "0.1.0",
82-
openapi_url: str | None = "/openapi.json",
83-
openapi_tags: list[dict[str, Any]] | None = None,
84-
servers: list[dict[str, str | Any]] | None = None,
85-
dependencies: Sequence[Depends] | None = None,
82+
openapi_url: Union[str, None] = "/openapi.json",
83+
openapi_tags: Union[list[dict[str, Any]], None] = None,
84+
servers: Union[list[dict[str, Union[str, Any]]], None] = None,
85+
dependencies: Union[Sequence[Depends], None] = None,
8686
default_response_class: type[Response] = JSONResponse,
8787
redirect_slashes: bool = True,
88-
routes: list[BaseRoute] | None = None,
89-
docs_url: str | None = "/docs",
90-
redoc_url: str | None = "/redoc",
91-
swagger_ui_oauth2_redirect_url: str | None = "/docs/oauth2-redirect",
92-
swagger_ui_init_oauth: dict[str, Any] | None = None,
93-
middleware: Sequence[Middleware] | None = None,
88+
routes: Union[list[BaseRoute], None] = None,
89+
docs_url: Union[str, None] = "/docs",
90+
redoc_url: Union[str, None] = "/redoc",
91+
swagger_ui_oauth2_redirect_url: Union[str, None] = "/docs/oauth2-redirect",
92+
swagger_ui_init_oauth: Union[dict[str, Any], None] = None,
93+
middleware: Union[Sequence[Middleware], None] = None,
9494
exception_handlers: (
95-
dict[
96-
int | type[Exception],
97-
Callable[[Request, Any], Coroutine[Any, Any, Response]],
95+
Union[
96+
dict[
97+
Union[int, type[Exception]],
98+
Callable[[Request, Any], Coroutine[Any, Any, Response]],
99+
],
100+
None,
98101
]
99-
| None
100102
) = None,
101-
on_startup: Sequence[Callable[[], Any]] | None = None,
102-
on_shutdown: Sequence[Callable[[], Any]] | None = None,
103-
lifespan: Lifespan[Self] | None = None,
104-
terms_of_service: str | None = None,
105-
contact: dict[str, str | Any] | None = None,
106-
license_info: dict[str, str | Any] | None = None,
103+
on_startup: Union[Sequence[Callable[[], Any]], None] = None,
104+
on_shutdown: Union[Sequence[Callable[[], Any]], None] = None,
105+
lifespan: Union[Lifespan[Self], None] = None,
106+
terms_of_service: Union[str, None] = None,
107+
contact: Union[dict[str, Union[str, Any]], None] = None,
108+
license_info: Union[dict[str, Union[str, Any]], None] = None,
107109
openapi_prefix: str = "",
108110
root_path: str = "",
109111
root_path_in_servers: bool = True,
110-
responses: dict[int | str, dict[str, Any]] | None = None,
111-
callbacks: list[BaseRoute] | None = None,
112-
webhooks: APIRouter | None = None,
113-
deprecated: bool | None = None,
112+
responses: Union[dict[Union[int, str], dict[str, Any]], None] = None,
113+
callbacks: Union[list[BaseRoute], None] = None,
114+
webhooks: Union[APIRouter, None] = None,
115+
deprecated: Union[bool, None] = None,
114116
include_in_schema: bool = True,
115-
swagger_ui_parameters: dict[str, Any] | None = None,
117+
swagger_ui_parameters: Union[dict[str, Any], None] = None,
116118
generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( # noqa: B008
117119
generate_unique_id
118120
),

0 commit comments

Comments
 (0)