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

feat: add support for Python 3.13 #3850

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
997a19c
feat: 3.13 testing
cofin Nov 9, 2024
882cabb
feat: use alternate `msgspec` 3.13 package
cofin Nov 16, 2024
5da2293
fix: updated lock
cofin Nov 17, 2024
7654cd2
fix: updated packages
cofin Dec 15, 2024
f7a3f8b
chore(build): build postgres client
cofin Dec 15, 2024
bc23712
fix: deprecated warnings
cofin Dec 15, 2024
8d911ef
fix: deprecated tests
cofin Dec 15, 2024
f0fb146
feat: updated deps
cofin Dec 15, 2024
82e8166
fix: adjusted deps
cofin Dec 15, 2024
4aa8e03
fix: picologging for 3.13
cofin Dec 15, 2024
10df3f3
fix: patch picologging tests
cofin Dec 15, 2024
e2e0839
fix: more cleanup for picologging
cofin Dec 15, 2024
cda3f86
fix: linting
cofin Dec 15, 2024
bfc24ec
fix: test is async
cofin Dec 15, 2024
6f66d3e
feat: use import or skip
cofin Dec 15, 2024
eed94c7
chore: lint
cofin Dec 15, 2024
f3e0dbf
fix: updated metadata creation process to prevent race condition
cofin Dec 15, 2024
837f2ca
fix: scope issues with locals
cofin Dec 15, 2024
80f8e7d
Feat: picologging skip if missing
cofin Dec 15, 2024
343c29d
fix: additional test fixes
cofin Dec 15, 2024
15c7cb8
Merge branch 'main' into msgspec-13
cofin Dec 15, 2024
2e53fac
chore: fix lock file
cofin Dec 15, 2024
0d76eed
fix: add ignore
cofin Dec 15, 2024
9fd3141
fix: updated
cofin Dec 15, 2024
d83b5b9
fix: testing
cofin Dec 16, 2024
0c04845
fix: formatting
cofin Dec 16, 2024
490ce3f
fix: monkeypatch
cofin Dec 16, 2024
1fe9e55
fix: remove purge
cofin Dec 16, 2024
08d429d
fix: linting
cofin Dec 16, 2024
0e110c6
fix: maybe?
cofin Dec 16, 2024
7cacc10
feat: finaly maybe
cofin Dec 16, 2024
0b57b71
fix: flaky test tweak
cofin Dec 18, 2024
4f6902a
feat: set upper limit for picologging
cofin Dec 18, 2024
1824379
fix: revert string updates
cofin Dec 18, 2024
0d3e6f6
feat: bump deps
cofin Dec 18, 2024
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
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
strategy:
fail-fast: true
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
uses: ./.github/workflows/test.yml
with:
coverage: ${{ (matrix.python-version == '3.12' || matrix.python-version == '3.8') }}
Expand All @@ -123,6 +123,9 @@ jobs:
version: "0.5.4"
enable-cache: true

- name: Install Build Dependencies
run: sudo apt-get install build-essential libpq-dev python3-dev -y

- name: Install dependencies
run: |
uv sync
Expand Down Expand Up @@ -190,6 +193,9 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Install Build Dependencies
run: sudo apt-get install build-essential libpq-dev python3-dev -y

- name: Set up Python
uses: actions/setup-python@v5
with:
Expand Down Expand Up @@ -260,6 +266,9 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4

- name: Install Build Dependencies
run: sudo apt-get install build-essential libpq-dev python3-dev -y

- name: Set up Python
uses: actions/setup-python@v5
with:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ jobs:
with:
python-version: ${{ inputs.python-version }}

- name: Install Build Dependencies
run: sudo apt-get install build-essential libpq-dev python3-dev -y

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
default_language_version:
python: "3"
python: "3.12"
repos:
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.6.0
Expand All @@ -24,7 +24,7 @@ repos:
- id: unasyncd
additional_dependencies: ["ruff"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.8.1"
rev: "v0.8.3"
hooks:
- id: ruff
args: ["--fix"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
from sqlalchemy.orm import Mapped, mapped_column, relationship

from litestar import Litestar, get
from litestar.contrib.sqlalchemy.base import UUIDAuditBase, UUIDBase
from litestar.contrib.sqlalchemy.plugins import AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyPlugin
from litestar.plugins.sqlalchemy import AsyncSessionConfig, SQLAlchemyAsyncConfig, SQLAlchemyPlugin, base


# The SQLAlchemy base includes a declarative model for you to use in your models.
# The `UUIDBase` class includes a `UUID` based primary key (`id`)
class Author(UUIDBase):
class Author(base.UUIDBase):
__tablename__ = "author"
name: Mapped[str]
dob: Mapped[date]
books: Mapped[List[Book]] = relationship(back_populates="author", lazy="selectin")
Expand All @@ -25,7 +25,8 @@ class Author(UUIDBase):
# The `UUIDAuditBase` class includes the same UUID` based primary key (`id`) and 2
# additional columns: `created_at` and `updated_at`. `created_at` is a timestamp of when the
# record created, and `updated_at` is the last time the record was modified.
class Book(UUIDAuditBase):
class Book(base.UUIDAuditBase):
__tablename__ = "book"
title: Mapped[str]
author_id: Mapped[UUID] = mapped_column(ForeignKey("author.id"))
author: Mapped[Author] = relationship(lazy="joined", innerjoin=True, viewonly=True)
Expand All @@ -37,7 +38,7 @@ class Book(UUIDAuditBase):
) # Create 'async_session' dependency.


async def on_startup() -> None:
async def on_startup(app: Litestar) -> None:
"""Adds some dummy data if no data is present."""
async with sqlalchemy_config.get_session() as session:
statement = select(func.count()).select_from(Author)
Expand Down
16 changes: 10 additions & 6 deletions litestar/contrib/sqlalchemy/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,20 @@ def __getattr__(attr_name: str) -> object:
value = globals()[attr_name] = locals()[attr_name] # pyright: ignore[reportUnknownVariableType]
return value # pyright: ignore[reportUnknownVariableType]
from advanced_alchemy.base import ( # pyright: ignore[reportMissingImports]
AuditColumns,
BigIntAuditBase,
BigIntBase,
BigIntPrimaryKey,
CommonTableAttributes,
ModelProtocol,
UUIDAuditBase,
UUIDBase,
UUIDPrimaryKey,
create_registry,
orm_registry,
)
from advanced_alchemy.mixins import ( # pyright: ignore[reportMissingImports]
AuditColumns,
BigIntPrimaryKey,
UUIDPrimaryKey,
)

warn_deprecation(
deprecated_name=f"litestar.contrib.sqlalchemy.base.{attr_name}",
Expand All @@ -78,15 +80,17 @@ def __getattr__(attr_name: str) -> object:
from advanced_alchemy.base import touch_updated_timestamp # type: ignore[no-redef,attr-defined]

from advanced_alchemy.base import ( # pyright: ignore[reportMissingImports]
AuditColumns,
BigIntAuditBase,
BigIntBase,
BigIntPrimaryKey,
CommonTableAttributes,
ModelProtocol,
UUIDAuditBase,
UUIDBase,
UUIDPrimaryKey,
create_registry,
orm_registry,
)
from advanced_alchemy.mixins import ( # pyright: ignore[reportMissingImports]
AuditColumns,
BigIntPrimaryKey,
UUIDPrimaryKey,
)
7 changes: 5 additions & 2 deletions litestar/logging/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,13 +292,16 @@ def configure(self) -> GetLogger:

if self.logging_module == "picologging":
try:
from picologging import config, getLogger
from picologging import ( # pyright: ignore[reportMissingImports,reportGeneralTypeIssues]
config, # pyright: ignore[reportMissingImports,reportGeneralTypeIssues]
getLogger, # pyright: ignore[reportMissingImports,reportGeneralTypeIssues]
)
except ImportError as e:
raise MissingDependencyException("picologging") from e

excluded_fields.add("incremental")
else:
from logging import config, getLogger # type: ignore[no-redef, assignment]
from logging import config, getLogger # type: ignore[no-redef,assignment,unused-ignore]

values = {
_field.name: getattr(self, _field.name)
Expand Down
12 changes: 6 additions & 6 deletions litestar/logging/picologging.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@


try:
import picologging # noqa: F401
import picologging # noqa: F401 # pyright: ignore[reportMissingImports]
except ImportError as e:
raise MissingDependencyException("picologging") from e

from picologging import StreamHandler
from picologging.handlers import QueueHandler, QueueListener
from picologging import StreamHandler # pyright: ignore[reportMissingImports]
from picologging.handlers import QueueHandler, QueueListener # pyright: ignore[reportMissingImports]


class QueueListenerHandler(QueueHandler):
class QueueListenerHandler(QueueHandler): # type: ignore[misc,unused-ignore]
"""Configure queue listener and handler to support non-blocking logging configuration."""

def __init__(self, handlers: list[Any] | None = None) -> None:
Expand All @@ -32,8 +32,8 @@ def __init__(self, handlers: list[Any] | None = None) -> None:
- Requires ``picologging`` to be installed.
"""
super().__init__(Queue(-1))
handlers = resolve_handlers(handlers) if handlers else [StreamHandler()]
self.listener = QueueListener(self.queue, *handlers)
handlers = resolve_handlers(handlers) if handlers else [StreamHandler()] # pyright: ignore[reportGeneralTypeIssues]
self.listener = QueueListener(self.queue, *handlers) # pyright: ignore[reportGeneralTypeIssues]
self.listener.start()

atexit.register(self.listener.stop)
57 changes: 36 additions & 21 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Software Development :: Libraries",
Expand All @@ -31,22 +32,25 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"anyio>=3",
"httpx>=0.22",
"exceptiongroup; python_version < \"3.11\"",
"importlib-metadata; python_version < \"3.10\"",
"importlib-resources>=5.12.0; python_version < \"3.9\"",
"msgspec>=0.18.2",
"multidict>=6.0.2",
"polyfactory>=2.6.3",
"pyyaml",
"typing-extensions",
"click",
"rich>=13.0.0",
"rich-click",
"multipart>=1.2.0",
# default litestar plugins
"litestar-htmx>=0.4.0"
"anyio>=3 ; python_version < \"3.9\"",
"anyio>=4 ; python_version >= \"3.9\"",
"httpx>=0.22",
"exceptiongroup; python_version < \"3.11\"",
"eval_type_backport; python_version <= \"3.9\"",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious: Why do we need this?

"importlib-metadata; python_version < \"3.10\"",
"importlib-resources>=5.12.0; python_version < \"3.9\"",
"litestar-msgspec; python_version > \"3.8\"",
"msgspec>=0.18.2,<0.19.0; python_version < \"3.9\"",
"multidict>=6.0.2",
"polyfactory>=2.6.3",
"pyyaml",
"typing-extensions",
"click",
"rich>=13.0.0",
"rich-click",
"multipart>=1.2.0",
# default litestar plugins
"litestar-htmx>=0.3.0"
]
description = "Litestar - A production-ready, highly performant, extensible ASGI API Framework"
keywords = ["api", "rest", "asgi", "litestar", "starlite"]
Expand Down Expand Up @@ -83,7 +87,8 @@ brotli = ["brotli"]
cli = ["jsbeautifier", "uvicorn[standard]", "uvloop>=0.18.0; sys_platform != 'win32'"]
cryptography = ["cryptography"]
full = [
"litestar[annotated-types,attrs,brotli,cli,cryptography,jinja,jwt,mako,minijinja,opentelemetry,piccolo,picologging,prometheus,pydantic,redis,sqlalchemy,standard,structlog,valkey]",
"litestar[annotated-types,attrs,brotli,cli,cryptography,jinja,jwt,mako,minijinja,opentelemetry,piccolo,picologging,prometheus,pydantic,redis,sqlalchemy,standard,structlog,valkey]; python_version < \"3.13\"",
"litestar[annotated-types,attrs,brotli,cli,cryptography,jinja,jwt,mako,minijinja,opentelemetry,piccolo,prometheus,pydantic,redis,sqlalchemy,standard,structlog,valkey]; python_version >= \"3.13\"",
]
jinja = ["jinja2>=3.1.2"]
jwt = [
Expand All @@ -94,9 +99,9 @@ mako = ["mako>=1.2.4"]
minijinja = ["minijinja>=1.0.0"]
opentelemetry = ["opentelemetry-instrumentation-asgi"]
piccolo = ["piccolo"]
picologging = ["picologging"]
picologging = ["picologging; python_version < \"3.13\""]
prometheus = ["prometheus-client"]
pydantic = ["pydantic", "email-validator", "pydantic-extra-types"]
pydantic = ["pydantic", "email-validator", "pydantic-extra-types<=2.9.0; python_version < \"3.9\"", "pydantic-extra-types>=2.9.0; python_version >= \"3.9\""]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an open build issue re: this.

redis = ["redis[hiredis]>=4.4.4"]
valkey = ["valkey[libvalkey]>=6.0.2"]
sqlalchemy = ["advanced-alchemy>=0.2.2"]
Expand Down Expand Up @@ -130,13 +135,14 @@ dev = [
"trio",
"aiosqlite",
"asyncpg>=0.29.0",
"psycopg[pool,binary]>=3.1.10,<3.2",
"psycopg[pool,binary]>=3.1.10,<3.2; python_version < \"3.13\"",
"psycopg[pool,c]; python_version >= \"3.13\"",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package needs to be built for 3.13

"psycopg2-binary",
"psutil>=5.9.8",
"hypercorn>=0.16.0",
"daphne>=4.0.0",
"opentelemetry-sdk",
"httpx-sse"
"httpx-sse",
]

docs = [
Expand Down Expand Up @@ -281,6 +287,8 @@ module = [
"pytimeparse.*",
"importlib_resources",
"exceptiongroup",
"picologging",
"picologging.*"
]

[[tool.mypy.overrides]]
Expand All @@ -305,6 +313,13 @@ module = [
]
disable_error_code = "arg-type"

[[tool.mypy.overrides]]
warn_unused_ignores = false
module = [
"tests.unit.test_logging.test_logging_config",
]
disable_error_code = "assignment"

[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
from pathlib import Path

import pytest
from pytest import MonkeyPatch
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.pool import NullPool

from litestar.plugins.sqlalchemy import AsyncSessionConfig, SQLAlchemyAsyncConfig
from litestar.testing import TestClient

pytestmark = pytest.mark.xdist_group("sqlalchemy_examples")


def test_sqlalchemy_declarative_models() -> None:
from docs.examples.contrib.sqlalchemy.sqlalchemy_declarative_models import app
async def test_sqlalchemy_declarative_models(tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this additional monkey-patching, there were intermittent failures on this job. This is the only way I could make the tests consistently pass on all python versions.

engine = create_async_engine("sqlite+aiosqlite:///test.sqlite", poolclass=NullPool)

session_config = AsyncSessionConfig(expire_on_commit=False)
sqlalchemy_config = SQLAlchemyAsyncConfig(
session_config=session_config,
create_all=True,
engine_instance=engine,
) # Create 'async_session' dependency.
from docs.examples.contrib.sqlalchemy import sqlalchemy_declarative_models

with TestClient(app) as client:
monkeypatch.setattr(sqlalchemy_declarative_models, "sqlalchemy_config", sqlalchemy_config)
async with engine.begin() as connection:
await connection.run_sync(sqlalchemy_declarative_models.Author.metadata.create_all)
await connection.commit()
with TestClient(sqlalchemy_declarative_models.app) as client:
response = client.get("/authors")
assert response.status_code == 200
assert len(response.json()) > 0
15 changes: 12 additions & 3 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from __future__ import annotations

import atexit
import importlib.util
import inspect
import logging
import random
import sys
from contextlib import AbstractContextManager, contextmanager
from pathlib import Path
from typing import Any, AsyncContextManager, Awaitable, ContextManager, Generator, TypeVar, cast, overload

import picologging
import pytest
from _pytest.logging import LogCaptureHandler, _LiveLoggingNullHandler

from litestar._openapi.schema_generation import SchemaCreator
Expand Down Expand Up @@ -90,9 +92,9 @@ def cleanup_logging_impl() -> Generator:
# Don't interfere with PyTest handler config
if not isinstance(std_handler, (_LiveLoggingNullHandler, LogCaptureHandler)):
std_root_logger.removeHandler(std_handler)

picologging = pytest.importorskip("picologging")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL about this. It will auto-skip if this import fails

# Reset root logger (`picologging` module)
pico_root_logger: picologging.Logger = picologging.getLogger()
pico_root_logger: picologging.Logger = picologging.getLogger() # type: ignore[name-defined,unused-ignore] # pyright: ignore[reportPrivateUsage,reportGeneralTypeIssues,reportAssignmentType,reportInvalidTypeForm]
for pico_handler in pico_root_logger.handlers:
pico_root_logger.removeHandler(pico_handler)

Expand All @@ -111,3 +113,10 @@ def cleanup_logging_impl() -> Generator:
def not_none(val: T | None) -> T:
assert val is not None
return val


def purge_module(module_names: list[str], path: str | Path) -> None:
for name in module_names:
if name in sys.modules:
del sys.modules[name]
Path(importlib.util.cache_from_source(path)).unlink(missing_ok=True) # type: ignore[arg-type]
Loading
Loading