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

build: configure type checkers to run on min supported version #2474

Merged
merged 9 commits into from
Oct 21, 2023
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ jobs:

- uses: actions/setup-python@v4
with:
python-version: "3.12"
python-version: "3.8"
allow-prereleases: true

- uses: pdm-project/setup-pdm@v3
name: Set up PDM
with:
python-version: "3.12"
python-version: "3.8"
allow-python-prereleases: false
cache: true
cache-dependency-path: |
Expand All @@ -63,13 +63,13 @@ jobs:

- uses: actions/setup-python@v4
with:
python-version: "3.12"
python-version: "3.8"
allow-prereleases: true

- uses: pdm-project/setup-pdm@v3
name: Set up PDM
with:
python-version: "3.12"
python-version: "3.8"
allow-python-prereleases: false
cache: true
cache-dependency-path: |
Expand Down
11 changes: 10 additions & 1 deletion CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ Contribution guide
Setting up the environment
--------------------------

.. tip:: Choosing your Python version.

We strongly encourage setting up your environment to use the lowest version of Python that is supported by
Litestar. This will ensure that the changes you made are backward compatible and will not fail in CI. This will also
ensure that when you run type checkers locally, you will get the same results as CI. Doing this will save you time!

The lowest currently supported version is Python 3.8. You can use `pyenv <https://github.com/pyenv/pyenv>`_ to manage
multiple Python versions on your system.

.. tip:: We maintain a Makefile with several commands to help with common tasks.
You can run ``make help`` to see a list of available commands.

Expand Down Expand Up @@ -102,7 +111,7 @@ enforce type safety. You can run them with:
- ``make typecheck`` to run both
- ``make lint`` to run pre-commit hooks and type checkers.

Our type checkers are run on Python 3.12 in CI, so you should make sure to run them on the same version locally as well.
Our type checkers are run on Python 3.8 in CI, so you should make sure to run them on the same version locally as well.

Project documentation
---------------------
Expand Down
2 changes: 1 addition & 1 deletion litestar/types/builtin_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
NoneType: type[None] = type(None)

try:
from types import UnionType # pyright: ignore
from types import UnionType # type: ignore[attr-defined]
except ImportError:
UnionType: TypeAlias = Union # type: ignore[no-redef]

Expand Down
4 changes: 2 additions & 2 deletions litestar/utils/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
D = TypeVar("D")

try:
async_next = anext # pyright: ignore
async_next = anext # type: ignore[name-defined]
except NameError: # pragma: no cover

async def async_next(gen: AsyncGenerator[T, Any], default: D | EmptyType = Empty) -> T | D: # type: ignore[misc]
async def async_next(gen: AsyncGenerator[T, Any], default: D | EmptyType = Empty) -> T | D:
"""Backwards compatibility shim for Python<3.10."""
try:
return await gen.__anext__()
Expand Down
2 changes: 1 addition & 1 deletion pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ dependencies = [
"multidict>=6.0.2",
"polyfactory>=2.6.3",
"pyyaml",
"typing-extensions",
"typing-extensions>=4.6.0",
"click",
"rich>=13.0.0",
"rich-click",
Expand Down
8 changes: 4 additions & 4 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
if sys.version_info < (3, 9):

def randbytes(n: int) -> bytes:
return bytearray(RANDOM.getrandbits(8) for _ in range(n))
return bytes(bytearray(RANDOM.getrandbits(8) for _ in range(n)))

else:
randbytes = RANDOM.randbytes
Expand Down Expand Up @@ -56,8 +56,8 @@ def maybe_async_cm(obj: ContextManager[T] | AsyncContextManager[T]) -> AsyncCont
def get_exception_group() -> type[BaseException]:
"""Get the exception group class with version compatibility."""
try:
return ExceptionGroup
return cast("type[BaseException]", ExceptionGroup) # type:ignore[name-defined]
except NameError:
from exceptiongroup import ExceptionGroup as _ExceptionGroup # type: ignore[import-not-found]
from exceptiongroup import ExceptionGroup as _ExceptionGroup

return cast("type[BaseException]", _ExceptionGroup)
return _ExceptionGroup
4 changes: 3 additions & 1 deletion tests/unit/test_contrib/test_pydantic/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from typing import Any

from pydantic import VERSION, BaseModel, Field
Expand All @@ -13,7 +15,7 @@ class Model(BaseModel):

ModelDTO = PydanticDTO[Model]

@post(dto=ModelDTO)
@post(dto=ModelDTO, signature_types=[Model])
def handler(data: Model) -> Model:
return data

Expand Down
21 changes: 12 additions & 9 deletions tests/unit/test_kwargs/test_multipart_data.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# ruff: noqa: UP006, UP007
from __future__ import annotations

from collections import defaultdict
from dataclasses import dataclass
from os import path
Expand Down Expand Up @@ -52,7 +55,7 @@ async def form_handler(request: Request) -> Dict[str, Any]:
@post("/form")
async def form_multi_item_handler(request: Request) -> DefaultDict[str, list]:
data = await request.form()
output: defaultdict[str, list] = defaultdict(list)
output = defaultdict(list)
for key, value in data.multi_items():
for v in value:
if isinstance(v, UploadFile):
Expand Down Expand Up @@ -92,7 +95,7 @@ def test_request_body_multi_part(t_type: type) -> None:
test_path = "/test"
data = _model_dump(Form(name="Moishe Zuchmir", age=30, programmer=True, value="100"))

@post(path=test_path)
@post(path=test_path, signature_namespace={"t_type": t_type})
def test_method(data: Annotated[t_type, Body(media_type=RequestEncodingType.MULTI_PART)]) -> None: # type: ignore
assert data

Expand All @@ -107,7 +110,7 @@ class MultiPartFormWithMixedFields:
image: UploadFile
tags: List[int]

@post(path="/form")
@post(path="/form", signature_types=[MultiPartFormWithMixedFields])
async def test_method(data: MultiPartFormWithMixedFields = Body(media_type=RequestEncodingType.MULTI_PART)) -> None:
file_data = await data.image.read()
assert file_data == b"data"
Expand Down Expand Up @@ -385,7 +388,7 @@ def test_postman_multipart_form_data() -> None:


def test_image_upload() -> None:
@post("/")
@post("/", signature_types=[UploadFile])
async def hello_world(data: UploadFile = Body(media_type=RequestEncodingType.MULTI_PART)) -> None:
await data.read()

Expand All @@ -398,7 +401,7 @@ async def hello_world(data: UploadFile = Body(media_type=RequestEncodingType.MUL


def test_optional_formdata() -> None:
@post("/")
@post("/", signature_types=[UploadFile])
async def hello_world(data: Optional[UploadFile] = Body(media_type=RequestEncodingType.MULTI_PART)) -> None:
if data is not None:
await data.read()
Expand All @@ -410,7 +413,7 @@ async def hello_world(data: Optional[UploadFile] = Body(media_type=RequestEncodi

@pytest.mark.parametrize("limit", (1000, 100, 10))
def test_multipart_form_part_limit(limit: int) -> None:
@post("/")
@post("/", signature_types=[UploadFile])
async def hello_world(data: List[UploadFile] = Body(media_type=RequestEncodingType.MULTI_PART)) -> None:
assert len(data) == limit

Expand All @@ -429,7 +432,7 @@ def test_multipart_form_part_limit_body_param_precedence() -> None:
app_limit = 100
route_limit = 10

@post("/")
@post("/", signature_types=[UploadFile])
async def hello_world(
data: List[UploadFile] = Body(media_type=RequestEncodingType.MULTI_PART, multipart_form_part_limit=route_limit)
) -> None:
Expand All @@ -455,7 +458,7 @@ class ProductForm:


def test_multipart_handling_of_none_values() -> None:
@post("/")
@post("/", signature_types=[ProductForm])
def handler(
data: Annotated[ProductForm, Body(media_type=RequestEncodingType.MULTI_PART)],
) -> None:
Expand Down Expand Up @@ -513,7 +516,7 @@ class AddProductFormMsgspec(msgspec.Struct):
@pytest.mark.parametrize("form_object", [AddProductFormMsgspec, AddProductFormPydantic, AddProductFormAttrs])
@pytest.mark.parametrize("form_type", [RequestEncodingType.URL_ENCODED, RequestEncodingType.MULTI_PART])
def test_multipart_and_url_encoded_behave_the_same(form_object, form_type) -> None: # type: ignore[no-untyped-def]
@post(path="/form")
@post(path="/form", signature_namespace={"form_object": form_object, "form_type": form_type})
async def form_(request: Request, data: Annotated[form_object, Body(media_type=form_type)]) -> int:
assert isinstance(data.name, str)
return data.amount # type: ignore[no-any-return]
Expand Down
17 changes: 10 additions & 7 deletions tests/unit/test_openapi/test_responses.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# ruff: noqa: UP006
from __future__ import annotations

from dataclasses import dataclass
from http import HTTPStatus
from pathlib import Path
from types import ModuleType
from typing import Any, Callable, Dict, Type, TypedDict
from typing import Any, Callable, Dict, TypedDict
from unittest.mock import MagicMock

import pytest
Expand Down Expand Up @@ -43,12 +46,12 @@
from .utils import PetException


def get_registered_route_handler(handler: "HTTPRouteHandler | type[Controller]", name: str) -> HTTPRouteHandler:
def get_registered_route_handler(handler: HTTPRouteHandler | type[Controller], name: str) -> HTTPRouteHandler:
app = Litestar(route_handlers=[handler])
return app.asgi_router.route_handler_index[name] # type: ignore[return-value]


def test_create_responses(person_controller: Type[Controller], pet_controller: Type[Controller]) -> None:
def test_create_responses(person_controller: type[Controller], pet_controller: type[Controller]) -> None:
for route in Litestar(route_handlers=[person_controller]).routes:
assert isinstance(route, HTTPRoute)
for route_handler, _ in route.route_handler_map.values():
Expand Down Expand Up @@ -217,7 +220,7 @@ def handler() -> Response[PydanticPerson]:
return Response(content=PydanticPersonFactory.build())

handler = get_registered_route_handler(handler, "test")
schemas: Dict[str, Schema] = {}
schemas: dict[str, Schema] = {}
response = create_success_response(
handler, SchemaCreator(generate_examples=True, schemas=schemas, plugins=[PydanticSchemaPlugin()])
)
Expand Down Expand Up @@ -336,7 +339,7 @@ class UnknownError(TypedDict):
def handler() -> PydanticPerson:
return PydanticPersonFactory.build()

schemas: Dict[str, Schema] = {}
schemas: dict[str, Schema] = {}
responses = create_additional_responses(handler, SchemaCreator(schemas=schemas, plugins=[PydanticSchemaPlugin()]))

first_response = next(responses)
Expand Down Expand Up @@ -417,13 +420,13 @@ def test_create_response_for_response_subclass() -> None:
class CustomResponse(Response[T]):
pass

@get(path="/test", name="test")
@get(path="/test", name="test", signature_types=[CustomResponse])
def handler() -> CustomResponse[PydanticPerson]:
return CustomResponse(content=PydanticPersonFactory.build())

handler = get_registered_route_handler(handler, "test")

schemas: Dict[str, Schema] = {}
schemas: dict[str, Schema] = {}
response = create_success_response(
handler, SchemaCreator(generate_examples=True, schemas=schemas, plugins=[PydanticSchemaPlugin()])
)
Expand Down
6 changes: 4 additions & 2 deletions tests/unit/test_openapi/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from datetime import date, timedelta
from decimal import Decimal
from enum import Enum
Expand Down Expand Up @@ -29,7 +31,7 @@ class Gender(str, Enum):
ANY = "A"


constr_kws: "list[dict[str, Any]]" = [
constr_kws: list[dict[str, Any]] = [
{"pattern": "^[a-zA-Z]$"},
{"to_upper": True, "min_length": 1, "pattern": "^[a-zA-Z]$"},
{"to_lower": True, "min_length": 1, "pattern": "^[a-zA-Z]$"},
Expand All @@ -40,7 +42,7 @@ class Gender(str, Enum):
{"min_length": 10, "max_length": 100},
]

conlist_kws: "list[dict[str, Any]]" = [
conlist_kws: list[dict[str, Any]] = [
{"min_length": 1},
{"min_length": 1, "max_length": 10},
]
Expand Down
19 changes: 10 additions & 9 deletions tests/unit/test_utils/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@
from tests import PydanticPerson, PydanticPet

if version_info >= (3, 10):
from collections import deque
from collections import deque # noqa: F401

# Pyright will report an error for these types if you are running on python 3.8, we run on >= 3.9 in CI
# so we can safely ignore that error.
py_310_plus_annotation = [
(tuple[PydanticPerson, ...], True),
(list[PydanticPerson], True),
(deque[PydanticPerson], True),
(tuple[PydanticPet, ...], False),
(list[PydanticPet], False),
(deque[PydanticPet], False),
(eval(tp), exp)
for tp, exp in [
("tuple[PydanticPerson, ...]", True),
("list[PydanticPerson]", True),
("deque[PydanticPerson]", True),
("tuple[PydanticPet, ...]", False),
("list[PydanticPet]", False),
("deque[PydanticPet]", False),
]
]
else:
py_310_plus_annotation = []
Expand Down