Skip to content

Commit 6e22480

Browse files
authored
Update supported Python versions, add support for Pydantic 2.11 (#379)
1 parent 98b8a33 commit 6e22480

File tree

20 files changed

+132
-100
lines changed

20 files changed

+132
-100
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,17 @@ jobs:
6868
fail-fast: false
6969
matrix:
7070
os: [ubuntu-latest, macos-13, macos-latest]
71-
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
71+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
7272
exclude:
73-
# Python 3.8 and 3.9 are not available on macOS 14
73+
# Python 3.9 is not available on macOS 14
7474
- os: macos-13
7575
python-version: '3.10'
7676
- os: macos-13
7777
python-version: '3.11'
7878
- os: macos-13
7979
python-version: '3.12'
8080
- os: macos-latest
81-
python-version: '3.8'
81+
python-version: '3.13'
8282
- os: macos-latest
8383
python-version: '3.9'
8484

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v4.3.0
3+
rev: v5.0.0
44
hooks:
55
- id: no-commit-to-branch
66
- id: check-yaml

demo/sse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import asyncio
2+
from collections.abc import AsyncIterable
23
from itertools import chain
3-
from typing import AsyncIterable
44

55
from fastapi import APIRouter
66
from fastui import FastUI

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ line-length = 120
66
extend-select = ["Q", "RUF100", "UP", "I"]
77
flake8-quotes = {inline-quotes = "single", multiline-quotes = "double"}
88
format.quote-style="single"
9-
target-version = "py38"
9+
target-version = "py39"
1010

1111
[tool.pyright]
1212
include = ["src/python-fastui/fastui"]

src/python-fastui/fastui/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class FastUI(pydantic.RootModel):
1313
The root component of a FastUI application.
1414
"""
1515

16-
root: _t.List[AnyComponent]
16+
root: list[AnyComponent]
1717

1818
@pydantic.field_validator('root', mode='before')
1919
def coerce_to_list(cls, v):

src/python-fastui/fastui/auth/github.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
from collections.abc import AsyncIterator
12
from contextlib import asynccontextmanager
23
from dataclasses import dataclass
34
from datetime import datetime, timedelta, timezone
4-
from typing import TYPE_CHECKING, AsyncIterator, Dict, List, Tuple, Union, cast
5+
from typing import TYPE_CHECKING, Union, cast
56
from urllib.parse import urlencode
67

78
from pydantic import BaseModel, SecretStr, TypeAdapter, field_validator
@@ -25,10 +26,10 @@ class GitHubExchangeError:
2526
class GitHubExchange:
2627
access_token: str
2728
token_type: str
28-
scope: List[str]
29+
scope: list[str]
2930

3031
@field_validator('scope', mode='before')
31-
def check_scope(cls, v: str) -> List[str]:
32+
def check_scope(cls, v: str) -> list[str]:
3233
return [s for s in v.split(',') if s]
3334

3435

@@ -61,7 +62,7 @@ class GitHubEmail(BaseModel):
6162
visibility: Union[str, None]
6263

6364

64-
github_emails_ta = TypeAdapter(List[GitHubEmail])
65+
github_emails_ta = TypeAdapter(list[GitHubEmail])
6566

6667

6768
class GitHubAuthProvider:
@@ -76,7 +77,7 @@ def __init__(
7677
github_client_secret: SecretStr,
7778
*,
7879
redirect_uri: Union[str, None] = None,
79-
scopes: Union[List[str], None] = None,
80+
scopes: Union[list[str], None] = None,
8081
state_provider: Union['StateProvider', bool] = True,
8182
exchange_cache_age: Union[timedelta, None] = timedelta(seconds=30),
8283
):
@@ -202,7 +203,7 @@ async def get_github_user(self, exchange: GitHubExchange) -> GithubUser:
202203
user_response.raise_for_status()
203204
return GithubUser.model_validate_json(user_response.content)
204205

205-
async def get_github_user_emails(self, exchange: GitHubExchange) -> List[GitHubEmail]:
206+
async def get_github_user_emails(self, exchange: GitHubExchange) -> list[GitHubEmail]:
206207
"""
207208
See https://docs.github.com/en/rest/users/emails
208209
"""
@@ -212,7 +213,7 @@ async def get_github_user_emails(self, exchange: GitHubExchange) -> List[GitHubE
212213
return github_emails_ta.validate_json(emails_response.content)
213214

214215
@staticmethod
215-
def _auth_headers(exchange: GitHubExchange) -> Dict[str, str]:
216+
def _auth_headers(exchange: GitHubExchange) -> dict[str, str]:
216217
return {
217218
'Authorization': f'Bearer {exchange.access_token}',
218219
'Accept': 'application/vnd.github+json',
@@ -221,7 +222,7 @@ def _auth_headers(exchange: GitHubExchange) -> Dict[str, str]:
221222

222223
class ExchangeCache:
223224
def __init__(self):
224-
self._data: Dict[str, Tuple[datetime, GitHubExchange]] = {}
225+
self._data: dict[str, tuple[datetime, GitHubExchange]] = {}
225226

226227
def get(self, key: str, max_age: timedelta) -> Union[GitHubExchange, None]:
227228
self._purge(max_age)

src/python-fastui/fastui/auth/shared.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22
from abc import ABC, abstractmethod
3-
from typing import TYPE_CHECKING, List, Tuple, Union
3+
from typing import TYPE_CHECKING, Union
44

55
from .. import AnyComponent, FastUI, events
66
from .. import components as c
@@ -17,7 +17,7 @@ class AuthException(ABC, Exception):
1717
"""
1818

1919
@abstractmethod
20-
def response_data(self) -> Tuple[int, str]:
20+
def response_data(self) -> tuple[int, str]:
2121
raise NotImplementedError
2222

2323

@@ -26,7 +26,7 @@ def __init__(self, message: str, *, code: str):
2626
super().__init__(message)
2727
self.code = code
2828

29-
def response_data(self) -> Tuple[int, str]:
29+
def response_data(self) -> tuple[int, str]:
3030
return 401, json.dumps({'detail': str(self)})
3131

3232

@@ -41,8 +41,8 @@ def __init__(self, path: str, message: Union[str, None] = None):
4141
self.path = path
4242
self.message = message
4343

44-
def response_data(self) -> Tuple[int, str]:
45-
components: List[AnyComponent] = [c.FireEvent(event=events.GoToEvent(url=self.path), message=self.message)]
44+
def response_data(self) -> tuple[int, str]:
45+
components: list[AnyComponent] = [c.FireEvent(event=events.GoToEvent(url=self.path), message=self.message)]
4646
return 345, FastUI(root=components).model_dump_json(exclude_none=True)
4747

4848

src/python-fastui/fastui/class_name.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# could be renamed to something general if there's more to add
2-
from typing import Dict, List, Literal, Union
2+
from typing import Annotated, Literal, Union
33

44
from pydantic import Field
5-
from typing_extensions import Annotated, TypeAliasType
5+
from typing_extensions import TypeAliasType
66

7-
ClassName = TypeAliasType('ClassName', Union[str, List['ClassName'], Dict[str, Union[bool, None]], None])
7+
ClassName = TypeAliasType('ClassName', Union[str, list['ClassName'], dict[str, Union[bool, None]], None])
88
ClassNameField = Annotated[ClassName, Field(serialization_alias='className')]
99

1010
NamedStyle = TypeAliasType('NamedStyle', Union[Literal['primary', 'secondary', 'warning'], None])

src/python-fastui/fastui/components/__init__.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ class PageTitle(BaseModel, extra='forbid'):
107107
class Div(BaseModel, defer_build=True, extra='forbid'):
108108
"""A generic container component."""
109109

110-
components: '_t.List[AnyComponent]'
110+
components: 'list[AnyComponent]'
111111
"""List of components to render inside the div."""
112112

113113
class_name: _class_name.ClassNameField = None
@@ -120,7 +120,7 @@ class Div(BaseModel, defer_build=True, extra='forbid'):
120120
class Page(BaseModel, defer_build=True, extra='forbid'):
121121
"""Similar to `container` in many UI frameworks, this acts as a root component for most pages."""
122122

123-
components: '_t.List[AnyComponent]'
123+
components: 'list[AnyComponent]'
124124
"""List of components to render on the page."""
125125

126126
class_name: _class_name.ClassNameField = None
@@ -245,7 +245,7 @@ class Button(BaseModel, extra='forbid'):
245245
class Link(BaseModel, defer_build=True, extra='forbid'):
246246
"""Link component."""
247247

248-
components: '_t.List[AnyComponent]'
248+
components: 'list[AnyComponent]'
249249
"""List of components to render attached to the link."""
250250

251251
on_click: _t.Union[events.AnyEvent, None] = None
@@ -270,7 +270,7 @@ class Link(BaseModel, defer_build=True, extra='forbid'):
270270
class LinkList(BaseModel, extra='forbid'):
271271
"""List of Link components."""
272272

273-
links: _t.List[Link]
273+
links: list[Link]
274274
"""List of links to render."""
275275

276276
mode: _t.Union[_t.Literal['tabs', 'vertical', 'pagination'], None] = None
@@ -292,10 +292,10 @@ class Navbar(BaseModel, extra='forbid'):
292292
title_event: _t.Union[events.AnyEvent, None] = None
293293
"""Optional event to trigger when the title is clicked. Often used to navigate to the home page."""
294294

295-
start_links: _t.List[Link] = []
295+
start_links: list[Link] = []
296296
"""List of links to render at the start of the navbar."""
297297

298-
end_links: _t.List[Link] = []
298+
end_links: list[Link] = []
299299
"""List of links to render at the end of the navbar."""
300300

301301
class_name: _class_name.ClassNameField = None
@@ -318,7 +318,7 @@ def __get_pydantic_json_schema__(
318318
class Footer(BaseModel, extra='forbid'):
319319
"""Footer component."""
320320

321-
links: _t.List[Link]
321+
links: list[Link]
322322
"""List of links to render in the footer."""
323323

324324
extra_text: _t.Union[str, None] = None
@@ -337,10 +337,10 @@ class Modal(BaseModel, defer_build=True, extra='forbid'):
337337
title: str
338338
"""The text displayed on the modal trigger button."""
339339

340-
body: '_t.List[AnyComponent]'
340+
body: 'list[AnyComponent]'
341341
"""List of components to render in the modal body."""
342342

343-
footer: '_t.Union[_t.List[AnyComponent], None]' = None
343+
footer: '_t.Union[list[AnyComponent], None]' = None
344344
"""Optional list of components to render in the modal footer."""
345345

346346
open_trigger: _t.Union[events.PageEvent, None] = None
@@ -365,7 +365,7 @@ class ServerLoad(BaseModel, defer_build=True, extra='forbid'):
365365
load_trigger: _t.Union[events.PageEvent, None] = None
366366
"""Optional event to trigger when the component is loaded."""
367367

368-
components: '_t.Union[_t.List[AnyComponent], None]' = None
368+
components: '_t.Union[list[AnyComponent], None]' = None
369369
"""Optional list of components to render while the server is loading the new component(s)."""
370370

371371
sse: _t.Union[bool, None] = None
@@ -457,7 +457,7 @@ class Iframe(BaseModel, extra='forbid'):
457457
class Video(BaseModel, extra='forbid'):
458458
"""Video component that displays a video or multiple videos."""
459459

460-
sources: _t.List[_p.AnyUrl]
460+
sources: list[_p.AnyUrl]
461461
"""List of URLs to the video sources."""
462462

463463
autoplay: _t.Union[bool, None] = None
@@ -549,7 +549,7 @@ class Toast(BaseModel, defer_build=True, extra='forbid'):
549549
title: str
550550
"""The title of the toast."""
551551

552-
body: '_t.List[AnyComponent]'
552+
body: 'list[AnyComponent]'
553553
"""List of components to render in the toast body."""
554554

555555
# TODO: change these before the release (top left, center, end, etc). Can be done with the toast bug fix.

src/python-fastui/fastui/components/display.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class Details(BaseModel, extra='forbid'):
6969
data: pydantic.SerializeAsAny[_types.DataModel]
7070
"""Data model to display."""
7171

72-
fields: _t.Union[_t.List[_t.Union[DisplayLookup, Display]], None] = None
72+
fields: _t.Union[list[_t.Union[DisplayLookup, Display]], None] = None
7373
"""Fields to display."""
7474

7575
class_name: _class_name.ClassNameField = None
@@ -80,15 +80,15 @@ class Details(BaseModel, extra='forbid'):
8080

8181
@pydantic.model_validator(mode='after')
8282
def _fill_fields(self) -> _te.Self:
83-
fields = {**self.data.model_fields, **self.data.model_computed_fields}
83+
fields = {**type(self.data).model_fields, **type(self.data).model_computed_fields}
8484

8585
if self.fields is None:
8686
self.fields = [DisplayLookup(field=name, title=field.title) for name, field in fields.items()]
8787
else:
8888
# add pydantic titles to fields that don't have them
8989
for field in (c for c in self.fields if c.title is None):
9090
if isinstance(field, DisplayLookup):
91-
pydantic_field = self.data.model_fields.get(field.field)
91+
pydantic_field = type(self.data).model_fields.get(field.field)
9292
if pydantic_field and pydantic_field.title:
9393
field.title = pydantic_field.title
9494
elif isinstance(field, Display):

0 commit comments

Comments
 (0)