Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,17 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-13, macos-latest]
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
exclude:
# Python 3.8 and 3.9 are not available on macOS 14
# Python 3.9 is not available on macOS 14
- os: macos-13
python-version: '3.10'
- os: macos-13
python-version: '3.11'
- os: macos-13
python-version: '3.12'
- os: macos-latest
python-version: '3.8'
python-version: '3.13'
- os: macos-latest
python-version: '3.9'

Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v5.0.0
hooks:
- id: no-commit-to-branch
- id: check-yaml
Expand Down
2 changes: 1 addition & 1 deletion demo/sse.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import asyncio
from collections.abc import AsyncIterable
from itertools import chain
from typing import AsyncIterable

from fastapi import APIRouter
from fastui import FastUI
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ line-length = 120
extend-select = ["Q", "RUF100", "UP", "I"]
flake8-quotes = {inline-quotes = "single", multiline-quotes = "double"}
format.quote-style="single"
target-version = "py38"
target-version = "py39"

[tool.pyright]
include = ["src/python-fastui/fastui"]
Expand Down
2 changes: 1 addition & 1 deletion src/python-fastui/fastui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class FastUI(pydantic.RootModel):
The root component of a FastUI application.
"""

root: _t.List[AnyComponent]
root: list[AnyComponent]

@pydantic.field_validator('root', mode='before')
def coerce_to_list(cls, v):
Expand Down
17 changes: 9 additions & 8 deletions src/python-fastui/fastui/auth/github.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, AsyncIterator, Dict, List, Tuple, Union, cast
from typing import TYPE_CHECKING, Union, cast
from urllib.parse import urlencode

from pydantic import BaseModel, SecretStr, TypeAdapter, field_validator
Expand All @@ -25,10 +26,10 @@ class GitHubExchangeError:
class GitHubExchange:
access_token: str
token_type: str
scope: List[str]
scope: list[str]

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


Expand Down Expand Up @@ -61,7 +62,7 @@ class GitHubEmail(BaseModel):
visibility: Union[str, None]


github_emails_ta = TypeAdapter(List[GitHubEmail])
github_emails_ta = TypeAdapter(list[GitHubEmail])


class GitHubAuthProvider:
Expand All @@ -76,7 +77,7 @@ def __init__(
github_client_secret: SecretStr,
*,
redirect_uri: Union[str, None] = None,
scopes: Union[List[str], None] = None,
scopes: Union[list[str], None] = None,
state_provider: Union['StateProvider', bool] = True,
exchange_cache_age: Union[timedelta, None] = timedelta(seconds=30),
):
Expand Down Expand Up @@ -202,7 +203,7 @@ async def get_github_user(self, exchange: GitHubExchange) -> GithubUser:
user_response.raise_for_status()
return GithubUser.model_validate_json(user_response.content)

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

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

class ExchangeCache:
def __init__(self):
self._data: Dict[str, Tuple[datetime, GitHubExchange]] = {}
self._data: dict[str, tuple[datetime, GitHubExchange]] = {}

def get(self, key: str, max_age: timedelta) -> Union[GitHubExchange, None]:
self._purge(max_age)
Expand Down
10 changes: 5 additions & 5 deletions src/python-fastui/fastui/auth/shared.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, List, Tuple, Union
from typing import TYPE_CHECKING, Union

from .. import AnyComponent, FastUI, events
from .. import components as c
Expand All @@ -17,7 +17,7 @@ class AuthException(ABC, Exception):
"""

@abstractmethod
def response_data(self) -> Tuple[int, str]:
def response_data(self) -> tuple[int, str]:
raise NotImplementedError


Expand All @@ -26,7 +26,7 @@ def __init__(self, message: str, *, code: str):
super().__init__(message)
self.code = code

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


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

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


Expand Down
6 changes: 3 additions & 3 deletions src/python-fastui/fastui/class_name.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# could be renamed to something general if there's more to add
from typing import Dict, List, Literal, Union
from typing import Annotated, Literal, Union

from pydantic import Field
from typing_extensions import Annotated, TypeAliasType
from typing_extensions import TypeAliasType

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

NamedStyle = TypeAliasType('NamedStyle', Union[Literal['primary', 'secondary', 'warning'], None])
Expand Down
24 changes: 12 additions & 12 deletions src/python-fastui/fastui/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class PageTitle(BaseModel, extra='forbid'):
class Div(BaseModel, defer_build=True, extra='forbid'):
"""A generic container component."""

components: '_t.List[AnyComponent]'
components: 'list[AnyComponent]'
"""List of components to render inside the div."""

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

components: '_t.List[AnyComponent]'
components: 'list[AnyComponent]'
"""List of components to render on the page."""

class_name: _class_name.ClassNameField = None
Expand Down Expand Up @@ -245,7 +245,7 @@ class Button(BaseModel, extra='forbid'):
class Link(BaseModel, defer_build=True, extra='forbid'):
"""Link component."""

components: '_t.List[AnyComponent]'
components: 'list[AnyComponent]'
"""List of components to render attached to the link."""

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

links: _t.List[Link]
links: list[Link]
"""List of links to render."""

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

start_links: _t.List[Link] = []
start_links: list[Link] = []
"""List of links to render at the start of the navbar."""

end_links: _t.List[Link] = []
end_links: list[Link] = []
"""List of links to render at the end of the navbar."""

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

links: _t.List[Link]
links: list[Link]
"""List of links to render in the footer."""

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

body: '_t.List[AnyComponent]'
body: 'list[AnyComponent]'
"""List of components to render in the modal body."""

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

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

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

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

sources: _t.List[_p.AnyUrl]
sources: list[_p.AnyUrl]
"""List of URLs to the video sources."""

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

body: '_t.List[AnyComponent]'
body: 'list[AnyComponent]'
"""List of components to render in the toast body."""

# TODO: change these before the release (top left, center, end, etc). Can be done with the toast bug fix.
Expand Down
6 changes: 3 additions & 3 deletions src/python-fastui/fastui/components/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class Details(BaseModel, extra='forbid'):
data: pydantic.SerializeAsAny[_types.DataModel]
"""Data model to display."""

fields: _t.Union[_t.List[_t.Union[DisplayLookup, Display]], None] = None
fields: _t.Union[list[_t.Union[DisplayLookup, Display]], None] = None
"""Fields to display."""

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

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

if self.fields is None:
self.fields = [DisplayLookup(field=name, title=field.title) for name, field in fields.items()]
else:
# add pydantic titles to fields that don't have them
for field in (c for c in self.fields if c.title is None):
if isinstance(field, DisplayLookup):
pydantic_field = self.data.model_fields.get(field.field)
pydantic_field = type(self.data).model_fields.get(field.field)
if pydantic_field and pydantic_field.title:
field.title = pydantic_field.title
elif isinstance(field, Display):
Expand Down
16 changes: 8 additions & 8 deletions src/python-fastui/fastui/components/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class BaseFormField(BaseModel, ABC, defer_build=True):
name: str
"""Name of the field."""

title: _t.Union[_t.List[str], str]
title: _t.Union[list[str], str]
"""Title of the field to display. Can be a list of strings for multi-line titles."""

required: bool = False
Expand Down Expand Up @@ -119,7 +119,7 @@ class FormFieldSelect(BaseFormField):
multiple: _t.Union[bool, None] = None
"""Whether multiple options can be selected."""

initial: _t.Union[_t.List[str], str, None] = None
initial: _t.Union[list[str], str, None] = None
"""Initial value for the field."""

vanilla: _t.Union[bool, None] = None
Expand Down Expand Up @@ -169,7 +169,7 @@ class BaseForm(BaseModel, ABC, defer_build=True, extra='forbid'):
submit_url: str
"""URL to submit the form data to."""

initial: _t.Union[_t.Dict[str, _types.JsonData], None] = None
initial: _t.Union[dict[str, _types.JsonData], None] = None
"""Initial values for the form fields, mapping field names to values."""

method: _t.Literal['POST', 'GOTO', 'GET'] = 'POST'
Expand All @@ -184,10 +184,10 @@ class BaseForm(BaseModel, ABC, defer_build=True, extra='forbid'):
submit_trigger: _t.Union[events.PageEvent, None] = None
"""Event to trigger form submission."""

loading: '_t.Union[_t.List[AnyComponent], None]' = None
loading: '_t.Union[list[AnyComponent], None]' = None
"""Components to display while the form is submitting."""

footer: '_t.Union[_t.List[AnyComponent], None]' = None
footer: '_t.Union[list[AnyComponent], None]' = None
"""Components to display in the form footer."""

class_name: _class_name.ClassNameField = None
Expand All @@ -203,7 +203,7 @@ def default_footer(self) -> _te.Self:
class Form(BaseForm, defer_build=True):
"""Form component."""

form_fields: _t.List[FormField]
form_fields: list[FormField]
"""List of form fields."""

type: _t.Literal['Form'] = 'Form'
Expand All @@ -216,14 +216,14 @@ class Form(BaseForm, defer_build=True):
class ModelForm(BaseForm, defer_build=True):
"""Form component generated from a Pydantic model."""

model: _t.Type[pydantic.BaseModel] = pydantic.Field(exclude=True)
model: type[pydantic.BaseModel] = pydantic.Field(exclude=True)
"""Pydantic model from which to generate the form."""

type: _t.Literal['ModelForm'] = 'ModelForm'
"""The type of the component. Always 'ModelForm'."""

@pydantic.computed_field(alias='formFields')
def form_fields(self) -> _t.List[FormField]:
def form_fields(self) -> list[FormField]:
from ..json_schema import model_json_schema_to_fields

return model_json_schema_to_fields(self.model)
4 changes: 2 additions & 2 deletions src/python-fastui/fastui/components/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ class Table(BaseModel, extra='forbid'):
data: _t.Sequence[pydantic.SerializeAsAny[_types.DataModel]]
"""Sequence of data models to display in the table."""

columns: _t.Union[_t.List[display.DisplayLookup], None] = None
columns: _t.Union[list[display.DisplayLookup], None] = None
"""List of columns to display in the table. If not provided, columns will be inferred from the data model."""

data_model: _t.Union[_t.Type[pydantic.BaseModel], None] = pydantic.Field(default=None, exclude=True)
data_model: _t.Union[type[pydantic.BaseModel], None] = pydantic.Field(default=None, exclude=True)
"""Data model to use for the table. If not provided, the model will be inferred from the first data item."""

no_data_message: _t.Union[str, None] = None
Expand Down
Loading
Loading