From 38dbde9d2ae7a9df86b7aa8a7696aa9489bcc96b Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:21:24 +0100 Subject: [PATCH 1/2] Add support for Python 3.14, update dependencies --- package-lock.json | 8 ++--- src/python-fastui/fastui/components/forms.py | 6 +++- src/python-fastui/fastui/components/tables.py | 7 +++- src/python-fastui/pyproject.toml | 1 + src/python-fastui/requirements/pyproject.txt | 22 ++++++------ src/python-fastui/requirements/test.txt | 35 ++++++++++--------- src/python-fastui/tests/test_auth_github.py | 21 ++++++----- src/python-fastui/tests/test_dev.py | 5 +-- src/python-fastui/tests/test_json_schema.py | 6 ++-- 9 files changed, 63 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6264d682..acbdc06b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6810,7 +6810,7 @@ }, "src/npm-fastui": { "name": "@pydantic/fastui", - "version": "0.0.24", + "version": "0.0.25", "license": "MIT", "dependencies": { "@microsoft/fetch-event-source": "^2.0.1", @@ -6827,7 +6827,7 @@ }, "src/npm-fastui-bootstrap": { "name": "@pydantic/fastui-bootstrap", - "version": "0.0.24", + "version": "0.0.25", "license": "MIT", "dependencies": { "bootstrap": "^5.3.2", @@ -6837,12 +6837,12 @@ "sass": "^1.69.5" }, "peerDependencies": { - "@pydantic/fastui": "0.0.24" + "@pydantic/fastui": "0.0.25" } }, "src/npm-fastui-prebuilt": { "name": "@pydantic/fastui-prebuilt", - "version": "0.0.24", + "version": "0.0.25", "license": "MIT", "devDependencies": { "@vitejs/plugin-react-swc": "^3.3.2", diff --git a/src/python-fastui/fastui/components/forms.py b/src/python-fastui/fastui/components/forms.py index e12b345e..f749bd48 100644 --- a/src/python-fastui/fastui/components/forms.py +++ b/src/python-fastui/fastui/components/forms.py @@ -212,11 +212,15 @@ class Form(BaseForm, defer_build=True): FormFieldsModel = _t.TypeVar('FormFieldsModel', bound=pydantic.BaseModel) +# In Python 3.14, when evaluating the annotation of field `model`, `type` would refer +# to the assigned value to the field `type` (of value `'ModelForm'`): +type_ = type + class ModelForm(BaseForm, defer_build=True): """Form component generated from a Pydantic model.""" - model: 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' diff --git a/src/python-fastui/fastui/components/tables.py b/src/python-fastui/fastui/components/tables.py index 0e09bf6e..b55c054d 100644 --- a/src/python-fastui/fastui/components/tables.py +++ b/src/python-fastui/fastui/components/tables.py @@ -12,6 +12,11 @@ # TODO allow dataclasses and typed dicts here too +# In Python 3.14, when evaluating the annotation of field `data_model`, `type` would refer +# to the assigned value to the field `type` (of value `'Table'`): +type_ = type + + class Table(BaseModel, extra='forbid'): """Table component.""" @@ -21,7 +26,7 @@ class Table(BaseModel, extra='forbid'): 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[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 diff --git a/src/python-fastui/pyproject.toml b/src/python-fastui/pyproject.toml index e11e3b5d..7d4470e7 100644 --- a/src/python-fastui/pyproject.toml +++ b/src/python-fastui/pyproject.toml @@ -22,6 +22,7 @@ classifiers = [ 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', + 'Programming Language :: Python :: 3.14', "Intended Audience :: Developers", "Intended Audience :: Information Technology", "Framework :: Pydantic :: 2", diff --git a/src/python-fastui/requirements/pyproject.txt b/src/python-fastui/requirements/pyproject.txt index 3c38c8cb..fc5fe385 100644 --- a/src/python-fastui/requirements/pyproject.txt +++ b/src/python-fastui/requirements/pyproject.txt @@ -1,40 +1,42 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --constraint=src/python-fastui/requirements/lint.txt --extra=fastapi --output-file=src/python-fastui/requirements/pyproject.txt --strip-extras src/python-fastui/pyproject.toml # +annotated-doc==0.0.3 + # via fastapi annotated-types==0.6.0 # via pydantic -anyio==4.2.0 +anyio==4.11.0 # via starlette dnspython==2.5.0 # via email-validator email-validator==2.1.0.post1 # via pydantic -fastapi==0.109.2 +fastapi==0.120.1 # via fastui (src/python-fastui/pyproject.toml) -idna==3.6 +idna==3.11 # via # anyio # email-validator -pydantic==2.11.7 +pydantic==2.12.3 # via # fastapi # fastui (src/python-fastui/pyproject.toml) -pydantic-core==2.33.2 +pydantic-core==2.41.4 # via pydantic python-multipart==0.0.7 # via fastui (src/python-fastui/pyproject.toml) -sniffio==1.3.0 +sniffio==1.3.1 # via anyio -starlette==0.36.3 +starlette==0.48.0 # via fastapi -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via # fastapi # pydantic # pydantic-core # typing-inspection -typing-inspection==0.4.1 +typing-inspection==0.4.2 # via pydantic diff --git a/src/python-fastui/requirements/test.txt b/src/python-fastui/requirements/test.txt index 2d8e26df..23ec25d5 100644 --- a/src/python-fastui/requirements/test.txt +++ b/src/python-fastui/requirements/test.txt @@ -1,10 +1,10 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --constraint=src/python-fastui/requirements/lint.txt --constraint=src/python-fastui/requirements/pyproject.txt --output-file=src/python-fastui/requirements/test.txt --strip-extras src/python-fastui/requirements/test.in # -anyio==4.2.0 +anyio==4.11.0 # via # -c src/python-fastui/requirements/pyproject.txt # httpx @@ -18,44 +18,45 @@ dirty-equals==0.7.1.post0 # via -r src/python-fastui/requirements/test.in h11==0.14.0 # via httpcore -httpcore==1.0.2 +httpcore==1.0.8 # via httpx -httpx==0.26.0 +httpx==0.28.1 # via -r src/python-fastui/requirements/test.in -idna==3.6 +idna==3.11 # via # -c src/python-fastui/requirements/pyproject.txt # anyio # httpx -iniconfig==2.0.0 +iniconfig==2.3.0 # via pytest markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -packaging==23.2 +packaging==25.0 # via pytest -pluggy==1.4.0 +pluggy==1.6.0 # via pytest -pygments==2.17.2 - # via rich +pygments==2.19.2 + # via + # pytest + # rich pyjwt==2.8.0 # via -r src/python-fastui/requirements/test.in -pytest==7.4.4 +pytest==8.4.2 # via # -r src/python-fastui/requirements/test.in # pytest-asyncio # pytest-pretty -pytest-asyncio==0.23.4 +pytest-asyncio==1.2.0 # via -r src/python-fastui/requirements/test.in -pytest-pretty==1.2.0 +pytest-pretty==1.3.0 # via -r src/python-fastui/requirements/test.in -pytz==2024.1 +pytz==2025.2 # via dirty-equals -rich==13.7.0 +rich==14.2.0 # via pytest-pretty -sniffio==1.3.0 +sniffio==1.3.1 # via # -c src/python-fastui/requirements/pyproject.txt # anyio - # httpx diff --git a/src/python-fastui/tests/test_auth_github.py b/src/python-fastui/tests/test_auth_github.py index d1234e6c..c0b226a7 100644 --- a/src/python-fastui/tests/test_auth_github.py +++ b/src/python-fastui/tests/test_auth_github.py @@ -1,11 +1,11 @@ from datetime import datetime from typing import Optional -import httpx import pytest from fastapi import FastAPI from fastui.auth import AuthError, GitHubAuthProvider, GitHubEmail from fastui.auth.github import EXCHANGE_CACHE +from httpx import ASGITransport, AsyncClient from pydantic import SecretStr @@ -69,12 +69,13 @@ async def user_emails(): @pytest.fixture async def httpx_client(fake_github_app: FastAPI): - async with httpx.AsyncClient(app=fake_github_app) as client: + transport = ASGITransport(app=fake_github_app) + async with AsyncClient(transport=transport, base_url='http://test') as client: yield client @pytest.fixture -async def github_auth_provider(httpx_client: httpx.AsyncClient): +async def github_auth_provider(httpx_client: AsyncClient): return GitHubAuthProvider( httpx_client=httpx_client, github_client_id='1234', @@ -90,7 +91,7 @@ async def test_get_auth_url(github_auth_provider: GitHubAuthProvider): assert url == 'https://github.com/login/oauth/authorize?client_id=1234' -async def test_get_auth_url_scopes(httpx_client: httpx.AsyncClient): +async def test_get_auth_url_scopes(httpx_client: AsyncClient): provider = GitHubAuthProvider( httpx_client=httpx_client, github_client_id='1234', @@ -131,7 +132,7 @@ async def test_exchange_bad_unexpected(github_auth_provider: GitHubAuthProvider) @pytest.fixture -async def github_auth_provider_state(fake_github_app: FastAPI, httpx_client: httpx.AsyncClient): +async def github_auth_provider_state(fake_github_app: FastAPI, httpx_client: AsyncClient): return GitHubAuthProvider( httpx_client=httpx_client, github_client_id='1234', @@ -182,7 +183,7 @@ async def test_exchange_ok_repeat(github_auth_provider: GitHubAuthProvider, gith async def test_exchange_ok_repeat_cached( - fake_github_app: FastAPI, httpx_client: httpx.AsyncClient, github_requests: list[str] + fake_github_app: FastAPI, httpx_client: AsyncClient, github_requests: list[str] ): github_auth_provider = GitHubAuthProvider( httpx_client=httpx_client, @@ -202,7 +203,7 @@ async def test_exchange_ok_repeat_cached( assert github_requests == ['/login/oauth/access_token code=good', '/login/oauth/access_token code=good_user'] -async def test_exchange_cached_purge(fake_github_app: FastAPI, httpx_client: httpx.AsyncClient): +async def test_exchange_cached_purge(fake_github_app: FastAPI, httpx_client: AsyncClient): github_auth_provider = GitHubAuthProvider( httpx_client=httpx_client, github_client_id='1234', @@ -221,9 +222,7 @@ async def test_exchange_cached_purge(fake_github_app: FastAPI, httpx_client: htt assert len(EXCHANGE_CACHE) == 1 -async def test_exchange_redirect_url( - fake_github_app: FastAPI, httpx_client: httpx.AsyncClient, github_requests: list[str] -): +async def test_exchange_redirect_url(fake_github_app: FastAPI, httpx_client: AsyncClient, github_requests: list[str]): github_auth_provider = GitHubAuthProvider( httpx_client=httpx_client, github_client_id='1234', @@ -265,4 +264,4 @@ async def test_get_github_user_emails(github_auth_provider: GitHubAuthProvider, async def test_create(): async with GitHubAuthProvider.create('foo', SecretStr('bar')) as provider: - assert isinstance(provider._httpx_client, httpx.AsyncClient) + assert isinstance(provider._httpx_client, AsyncClient) diff --git a/src/python-fastui/tests/test_dev.py b/src/python-fastui/tests/test_dev.py index a533c433..a1b607f3 100644 --- a/src/python-fastui/tests/test_dev.py +++ b/src/python-fastui/tests/test_dev.py @@ -1,7 +1,7 @@ from unittest.mock import patch from fastui.dev import dev_fastapi_app -from httpx import AsyncClient +from httpx import ASGITransport, AsyncClient def mock_signal(_sig, on_signal): @@ -12,7 +12,8 @@ async def test_dev_connect(): with patch('fastui.dev.signal.signal', new=mock_signal): app = dev_fastapi_app() async with app.router.lifespan_context(app): - async with AsyncClient(app=app, base_url='http://test') as client: + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url='http://test') as client: r = await client.get('/api/__dev__/reload') assert r.status_code == 200 assert r.headers['content-type'] == 'text/plain; charset=utf-8' diff --git a/src/python-fastui/tests/test_json_schema.py b/src/python-fastui/tests/test_json_schema.py index c6ba4b45..e11965d3 100644 --- a/src/python-fastui/tests/test_json_schema.py +++ b/src/python-fastui/tests/test_json_schema.py @@ -2,7 +2,7 @@ from fastapi import FastAPI from fastui import FastUI, components from fastui.generate_typescript import generate_json_schema -from httpx import AsyncClient +from httpx import ASGITransport, AsyncClient async def test_json_schema(): @@ -23,7 +23,9 @@ async def test_openapi(): def test_endpoint(): return [components.Text(text='hello')] - async with AsyncClient(app=app, base_url='http://test') as client: + transport = ASGITransport(app=app) + + async with AsyncClient(transport=transport, base_url='http://test') as client: r = await client.get('/openapi.json') assert r.status_code == 200 assert r.headers['content-type'] == 'application/json' From 18aea65a41da2f51798c680d6516ded4e00fef76 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Tue, 28 Oct 2025 09:39:46 +0100 Subject: [PATCH 2/2] Drop 3.9 --- .github/workflows/ci.yml | 13 +- pyproject.toml | 2 +- src/python-fastui/fastui/__init__.py | 6 +- src/python-fastui/fastui/auth/github.py | 44 ++-- src/python-fastui/fastui/auth/shared.py | 4 +- src/python-fastui/fastui/class_name.py | 6 +- .../fastui/components/__init__.py | 200 +++++++++--------- .../fastui/components/display.py | 10 +- src/python-fastui/fastui/components/forms.py | 67 +++--- src/python-fastui/fastui/components/tables.py | 6 +- src/python-fastui/fastui/dev.py | 2 +- src/python-fastui/fastui/events.py | 24 +-- src/python-fastui/fastui/forms.py | 12 +- src/python-fastui/fastui/json_schema.py | 8 +- src/python-fastui/pyproject.toml | 3 +- src/python-fastui/tests/test_auth_github.py | 3 +- src/python-fastui/tests/test_forms.py | 6 +- 17 files changed, 202 insertions(+), 214 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95a785cc..9587ba5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,20 +67,17 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-13, macos-latest] - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + os: [ubuntu-latest, macos-15-intel, macos-latest] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] exclude: - # Python 3.9 is not available on macOS 14 - - os: macos-13 + - os: macos-15-intel python-version: '3.10' - - os: macos-13 + - os: macos-15-intel python-version: '3.11' - - os: macos-13 + - os: macos-15-intel python-version: '3.12' - os: macos-latest python-version: '3.13' - - os: macos-latest - python-version: '3.9' runs-on: ${{ matrix.os }} diff --git a/pyproject.toml b/pyproject.toml index 99dd84a6..a52fe7c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = "py39" +target-version = "py310" [tool.pyright] include = ["src/python-fastui/fastui"] diff --git a/src/python-fastui/fastui/__init__.py b/src/python-fastui/fastui/__init__.py index 292e0f54..e6ca812c 100644 --- a/src/python-fastui/fastui/__init__.py +++ b/src/python-fastui/fastui/__init__.py @@ -30,9 +30,9 @@ def coerce_to_list(cls, v): def prebuilt_html( *, title: str = '', - api_root_url: _t.Union[str, None] = None, - api_path_mode: _t.Union[_t.Literal['append', 'query'], None] = None, - api_path_strip: _t.Union[str, None] = None, + api_root_url: str | None = None, + api_path_mode: _t.Literal['append', 'query'] | None = None, + api_path_strip: str | None = None, ) -> str: """ Returns a simple HTML page which includes the FastUI react frontend, loaded from https://www.jsdelivr.com/. diff --git a/src/python-fastui/fastui/auth/github.py b/src/python-fastui/fastui/auth/github.py index 030c3961..2211879e 100644 --- a/src/python-fastui/fastui/auth/github.py +++ b/src/python-fastui/fastui/auth/github.py @@ -2,7 +2,7 @@ from contextlib import asynccontextmanager from dataclasses import dataclass from datetime import datetime, timedelta, timezone -from typing import TYPE_CHECKING, Union, cast +from typing import TYPE_CHECKING, cast from urllib.parse import urlencode from pydantic import BaseModel, SecretStr, TypeAdapter, field_validator @@ -19,7 +19,7 @@ @dataclass class GitHubExchangeError: error: str - error_description: Union[str, None] = None + error_description: str | None = None @dataclass @@ -33,13 +33,13 @@ def check_scope(cls, v: str) -> list[str]: return [s for s in v.split(',') if s] -github_exchange_type = TypeAdapter(Union[GitHubExchange, GitHubExchangeError]) +github_exchange_type = TypeAdapter(GitHubExchange | GitHubExchangeError) class GithubUser(BaseModel): login: str - name: Union[str, None] - email: Union[str, None] + name: str | None + email: str | None avatar_url: str created_at: datetime updated_at: datetime @@ -47,19 +47,19 @@ class GithubUser(BaseModel): public_gists: int followers: int following: int - company: Union[str, None] - blog: Union[str, None] - location: Union[str, None] - hireable: Union[bool, None] - bio: Union[str, None] - twitter_username: Union[str, None] = None + company: str | None + blog: str | None + location: str | None + hireable: bool | None + bio: str | None + twitter_username: str | None = None class GitHubEmail(BaseModel): email: str primary: bool verified: bool - visibility: Union[str, None] + visibility: str | None github_emails_ta = TypeAdapter(list[GitHubEmail]) @@ -76,10 +76,10 @@ def __init__( github_client_id: str, github_client_secret: SecretStr, *, - redirect_uri: Union[str, None] = None, - scopes: Union[list[str], None] = None, - state_provider: Union['StateProvider', bool] = True, - exchange_cache_age: Union[timedelta, None] = timedelta(seconds=30), + redirect_uri: str | None = None, + scopes: list[str] | None = None, + state_provider: 'StateProvider | bool' = True, + exchange_cache_age: timedelta | None = timedelta(seconds=30), ): """ Arguments: @@ -114,9 +114,9 @@ async def create( client_id: str, client_secret: SecretStr, *, - redirect_uri: Union[str, None] = None, - state_provider: Union['StateProvider', bool] = True, - exchange_cache_age: Union[timedelta, None] = timedelta(seconds=10), + redirect_uri: str | None = None, + state_provider: 'StateProvider | bool' = True, + exchange_cache_age: timedelta | None = timedelta(seconds=10), ) -> AsyncIterator['GitHubAuthProvider']: """ Async context manager to create a GitHubAuth instance with a new `httpx.AsyncClient`. @@ -146,7 +146,7 @@ async def authorization_url(self) -> str: params['state'] = await self._state_provider.new_state() return f'https://github.com/login/oauth/authorize?{urlencode(params)}' - async def exchange_code(self, code: str, state: Union[str, None] = None) -> GitHubExchange: + async def exchange_code(self, code: str, state: str | None = None) -> GitHubExchange: """ Exchange a code for an access token. @@ -164,7 +164,7 @@ async def exchange_code(self, code: str, state: Union[str, None] = None) -> GitH else: return await self._exchange_code(code, state) - async def _exchange_code(self, code: str, state: Union[str, None] = None) -> GitHubExchange: + async def _exchange_code(self, code: str, state: str | None = None) -> GitHubExchange: if self._state_provider: if state is None: raise AuthError('Missing GitHub auth state', code='missing_state') @@ -224,7 +224,7 @@ class ExchangeCache: def __init__(self): self._data: dict[str, tuple[datetime, GitHubExchange]] = {} - def get(self, key: str, max_age: timedelta) -> Union[GitHubExchange, None]: + def get(self, key: str, max_age: timedelta) -> GitHubExchange | None: self._purge(max_age) if v := self._data.get(key): return v[1] diff --git a/src/python-fastui/fastui/auth/shared.py b/src/python-fastui/fastui/auth/shared.py index 3caf740a..33142fec 100644 --- a/src/python-fastui/fastui/auth/shared.py +++ b/src/python-fastui/fastui/auth/shared.py @@ -1,6 +1,6 @@ import json from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING from .. import AnyComponent, FastUI, events from .. import components as c @@ -36,7 +36,7 @@ class AuthRedirect(AuthException): FastUI components to redirect the user to a new page. """ - def __init__(self, path: str, message: Union[str, None] = None): + def __init__(self, path: str, message: str | None = None): super().__init__(f'Auth redirect to `{path}`' + (f': {message}' if message else '')) self.path = path self.message = message diff --git a/src/python-fastui/fastui/class_name.py b/src/python-fastui/fastui/class_name.py index 6c886a7b..ccacd31d 100644 --- a/src/python-fastui/fastui/class_name.py +++ b/src/python-fastui/fastui/class_name.py @@ -1,11 +1,11 @@ # could be renamed to something general if there's more to add -from typing import Annotated, Literal, Union +from typing import Annotated, Literal from pydantic import Field from typing_extensions import TypeAliasType -ClassName = TypeAliasType('ClassName', Union[str, list['ClassName'], dict[str, Union[bool, None]], None]) +ClassName = TypeAliasType('ClassName', str | list['ClassName'] | dict[str, bool | None] | None) ClassNameField = Annotated[ClassName, Field(serialization_alias='className')] -NamedStyle = TypeAliasType('NamedStyle', Union[Literal['primary', 'secondary', 'warning'], None]) +NamedStyle = TypeAliasType('NamedStyle', Literal['primary', 'secondary', 'warning'] | None) NamedStyleField = Annotated[NamedStyle, Field(serialization_alias='namedStyle')] diff --git a/src/python-fastui/fastui/components/__init__.py b/src/python-fastui/fastui/components/__init__.py index 39037b97..52cec32e 100644 --- a/src/python-fastui/fastui/components/__init__.py +++ b/src/python-fastui/fastui/components/__init__.py @@ -139,7 +139,7 @@ class Heading(BaseModel, extra='forbid'): level: _t.Literal[1, 2, 3, 4, 5, 6] = 1 """The level of the heading. 1 is the largest, 6 is the smallest.""" - html_id: _t.Union[str, None] = None + html_id: str | None = None """Optional HTML ID to apply to the heading's HTML component.""" class_name: _class_name.ClassNameField = None @@ -159,7 +159,7 @@ def __get_pydantic_json_schema__( return json_schema -CodeStyle = _te.Annotated[_t.Union[str, None], _p.Field(serialization_alias='codeStyle')] +CodeStyle = _te.Annotated[str | None, _p.Field(serialization_alias='codeStyle')] """ Code style to apply to a `Code` component. @@ -194,7 +194,7 @@ class Code(BaseModel, extra='forbid'): text: str """The code to render.""" - language: _t.Union[str, None] = None + language: str | None = None """Optional language of the code. If None, no syntax highlighting is applied.""" code_style: CodeStyle = None @@ -226,10 +226,10 @@ class Button(BaseModel, extra='forbid'): text: str """The text to display on the button.""" - on_click: _t.Union[events.AnyEvent, None] = None + on_click: events.AnyEvent | None = None """Optional event to trigger when the button is clicked.""" - html_type: _t.Union[_t.Literal['button', 'reset', 'submit'], None] = None + html_type: _t.Literal['button', 'reset', 'submit'] | None = None """Optional HTML type of the button. If None, defaults to 'button'.""" named_style: _class_name.NamedStyleField = None @@ -248,16 +248,16 @@ class Link(BaseModel, defer_build=True, extra='forbid'): components: 'list[AnyComponent]' """List of components to render attached to the link.""" - on_click: _t.Union[events.AnyEvent, None] = None + on_click: events.AnyEvent | None = None """Optional event to trigger when the link is clicked.""" - mode: _t.Union[_t.Literal['navbar', 'footer', 'tabs', 'vertical', 'pagination'], None] = None + mode: _t.Literal['navbar', 'footer', 'tabs', 'vertical', 'pagination'] | None = None """Optional mode of the link.""" - active: _t.Union[str, bool, None] = None + active: str | bool | None = None """Optional active state of the link.""" - locked: _t.Union[bool, None] = None + locked: bool | None = None """Optional locked state of the link.""" class_name: _class_name.ClassNameField = None @@ -273,7 +273,7 @@ class LinkList(BaseModel, extra='forbid'): links: list[Link] """List of links to render.""" - mode: _t.Union[_t.Literal['tabs', 'vertical', 'pagination'], None] = None + mode: _t.Literal['tabs', 'vertical', 'pagination'] | None = None """Optional mode of the link list.""" class_name: _class_name.ClassNameField = None @@ -286,10 +286,10 @@ class LinkList(BaseModel, extra='forbid'): class Navbar(BaseModel, extra='forbid'): """Navbar component used for moving between pages.""" - title: _t.Union[str, None] = None + title: str | None = None """Optional title to display in the navbar.""" - title_event: _t.Union[events.AnyEvent, None] = None + title_event: events.AnyEvent | None = None """Optional event to trigger when the title is clicked. Often used to navigate to the home page.""" start_links: list[Link] = [] @@ -321,7 +321,7 @@ class Footer(BaseModel, extra='forbid'): links: list[Link] """List of links to render in the footer.""" - extra_text: _t.Union[str, None] = None + extra_text: str | None = None """Optional extra text to display in the footer.""" class_name: _class_name.ClassNameField = None @@ -340,13 +340,13 @@ class Modal(BaseModel, defer_build=True, extra='forbid'): body: 'list[AnyComponent]' """List of components to render in the modal body.""" - footer: '_t.Union[list[AnyComponent], None]' = None + footer: 'list[AnyComponent] | None' = None """Optional list of components to render in the modal footer.""" - open_trigger: _t.Union[events.PageEvent, None] = None + open_trigger: events.PageEvent | None = None """Optional event to trigger when the modal is opened.""" - open_context: _t.Union[events.ContextType, None] = None + open_context: events.ContextType | None = None """Optional context to pass to the open trigger event.""" class_name: _class_name.ClassNameField = None @@ -362,19 +362,19 @@ class ServerLoad(BaseModel, defer_build=True, extra='forbid'): path: str """The URL to load the component from.""" - load_trigger: _t.Union[events.PageEvent, None] = None + load_trigger: events.PageEvent | None = None """Optional event to trigger when the component is loaded.""" - components: '_t.Union[list[AnyComponent], None]' = None + components: '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 + sse: bool | None = None """Optional flag to enable server-sent events (SSE) for the server load.""" - sse_retry: _t.Union[int, None] = None + sse_retry: int | None = None """Optional time in milliseconds to retry the SSE connection.""" - method: _t.Union[_t.Literal['GET', 'POST', 'PATCH', 'PUT', 'DELETE'], None] = None + method: _t.Literal['GET', 'POST', 'PATCH', 'PUT', 'DELETE'] | None = None """Optional HTTP method to use when loading the component.""" type: _t.Literal['ServerLoad'] = 'ServerLoad' @@ -387,36 +387,33 @@ class Image(BaseModel, extra='forbid'): src: str """The URL of the image to display.""" - alt: _t.Union[str, None] = None + alt: str | None = None """Optional alt text for the image.""" - width: _t.Union[str, int, None] = None + width: str | int | None = None """Optional width used to display the image.""" - height: _t.Union[str, int, None] = None + height: str | int | None = None """Optional height used to display the image.""" - referrer_policy: _t.Union[ - _t.Literal[ - 'no-referrer', - 'no-referrer-when-downgrade', - 'origin', - 'origin-when-cross-origin', - 'same-origin', - 'strict-origin', - 'strict-origin-when-cross-origin', - 'unsafe-url', - ], - None, - ] = None + referrer_policy: _t.Literal[ + 'no-referrer', + 'no-referrer-when-downgrade', + 'origin', + 'origin-when-cross-origin', + 'same-origin', + 'strict-origin', + 'strict-origin-when-cross-origin', + 'unsafe-url', + ] | None = None """Optional referrer policy for the image. Specifies what information to send when fetching the image. For more info, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy.""" - loading: _t.Union[_t.Literal['eager', 'lazy'], None] = None + loading: _t.Literal['eager', 'lazy'] | None = None """Optional loading strategy for the image.""" - on_click: _t.Union[events.AnyEvent, None] = None + on_click: events.AnyEvent | None = None """Optional event to trigger when the image is clicked.""" class_name: _class_name.ClassNameField = None @@ -432,22 +429,22 @@ class Iframe(BaseModel, extra='forbid'): src: _p.HttpUrl """The URL of the content to display.""" - title: _t.Union[str, None] = None + title: str | None = None """Optional title for the iframe.""" - width: _t.Union[str, int, None] = None + width: str | int | None = None """Optional width used to display the iframe.""" - height: _t.Union[str, int, None] = None + height: str | int | None = None """Optional height used to display the iframe.""" class_name: _class_name.ClassNameField = None """Optional class name to apply to the iframe's HTML component.""" - srcdoc: _t.Union[str, None] = None + srcdoc: str | None = None """Optional HTML content to display in the iframe.""" - sandbox: _t.Union[str, None] = None + sandbox: str | None = None """Optional sandbox policy for the iframe. Specifies restrictions on the HTML content in the iframe.""" type: _t.Literal['Iframe'] = 'Iframe' @@ -460,25 +457,25 @@ class Video(BaseModel, extra='forbid'): sources: list[_p.AnyUrl] """List of URLs to the video sources.""" - autoplay: _t.Union[bool, None] = None + autoplay: bool | None = None """Optional flag to enable autoplay for the video.""" - controls: _t.Union[bool, None] = None + controls: bool | None = None """Optional flag to enable controls (pause, play, etc) for the video.""" - loop: _t.Union[bool, None] = None + loop: bool | None = None """Optional flag to enable looping for the video.""" - muted: _t.Union[bool, None] = None + muted: bool | None = None """Optional flag to mute the video.""" - poster: _t.Union[_p.AnyUrl, None] = None + poster: _p.AnyUrl | None = None """Optional URL to an image to display as the video poster (what is shown when the video is loading or until the user plays it).""" - width: _t.Union[str, int, None] = None + width: str | int | None = None """Optional width used to display the video.""" - height: _t.Union[str, int, None] = None + height: str | int | None = None """Optional height used to display the video.""" class_name: _class_name.ClassNameField = None @@ -494,7 +491,7 @@ class FireEvent(BaseModel, extra='forbid'): event: events.AnyEvent """The event to fire.""" - message: _t.Union[str, None] = None + message: str | None = None """Optional message to display when the event is fired. Defaults to a blank message.""" type: _t.Literal['FireEvent'] = 'FireEvent' @@ -510,7 +507,7 @@ class Error(BaseModel, extra='forbid'): description: str """The description of the error.""" - status_code: _t.Union[int, None] = None + status_code: int | None = None """Optional status code of the error.""" class_name: _class_name.ClassNameField = None @@ -533,7 +530,7 @@ def __get_pydantic_json_schema__( class Spinner(BaseModel, extra='forbid'): """Spinner component that displays a loading spinner.""" - text: _t.Union[str, None] = None + text: str | None = None """Optional text to display with the spinner.""" class_name: _class_name.ClassNameField = None @@ -553,26 +550,23 @@ class Toast(BaseModel, defer_build=True, extra='forbid'): """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. - position: _t.Union[ - _t.Literal[ - 'top-start', - 'top-center', - 'top-end', - 'middle-start', - 'middle-center', - 'middle-end', - 'bottom-start', - 'bottom-center', - 'bottom-end', - ], - None, - ] = None + position: _t.Literal[ + 'top-start', + 'top-center', + 'top-end', + 'middle-start', + 'middle-center', + 'middle-end', + 'bottom-start', + 'bottom-center', + 'bottom-end', + ] | None = None """Optional position of the toast.""" - open_trigger: _t.Union[events.PageEvent, None] = None + open_trigger: events.PageEvent | None = None """Optional event to trigger when the toast is opened.""" - open_context: _t.Union[events.ContextType, None] = None + open_context: events.ContextType | None = None """Optional context to pass to the open trigger event.""" class_name: _class_name.ClassNameField = None @@ -591,7 +585,7 @@ class Custom(BaseModel, extra='forbid'): sub_type: str """The sub-type of the custom component.""" - library: _t.Union[str, None] = None + library: str | None = None """Optional library to use for the custom component.""" class_name: _class_name.ClassNameField = None @@ -602,39 +596,37 @@ class Custom(BaseModel, extra='forbid'): AnyComponent = _te.Annotated[ - _t.Union[ - Text, - Paragraph, - PageTitle, - Div, - Page, - Heading, - Markdown, - Code, - Json, - Button, - Link, - LinkList, - Navbar, - Footer, - Modal, - ServerLoad, - Image, - Iframe, - Video, - FireEvent, - Error, - Spinner, - Custom, - Table, - Pagination, - Display, - Details, - Form, - FormField, - ModelForm, - Toast, - ], + Text + | Paragraph + | PageTitle + | Div + | Page + | Heading + | Markdown + | Code + | Json + | Button + | Link + | LinkList + | Navbar + | Footer + | Modal + | ServerLoad + | Image + | Iframe + | Video + | FireEvent + | Error + | Spinner + | Custom + | Table + | Pagination + | Display + | Details + | Form + | FormField + | ModelForm + | Toast, _p.Field(discriminator='type'), ] """Union of all components. diff --git a/src/python-fastui/fastui/components/display.py b/src/python-fastui/fastui/components/display.py index 6b20120a..3151403e 100644 --- a/src/python-fastui/fastui/components/display.py +++ b/src/python-fastui/fastui/components/display.py @@ -33,13 +33,13 @@ class DisplayMode(str, enum.Enum): class DisplayBase(BaseModel, ABC): """Base class for display components.""" - mode: _t.Union[DisplayMode, None] = None + mode: DisplayMode | None = None """Display mode for the value.""" - title: _t.Union[str, None] = None + title: str | None = None """Title to display for the value.""" - on_click: _t.Union[events.AnyEvent, None] = None + on_click: events.AnyEvent | None = None """Event to trigger when the value is clicked.""" @@ -49,7 +49,7 @@ class DisplayLookup(DisplayBase, extra='forbid'): field: str """Field to display.""" - table_width_percent: _t.Union[_te.Annotated[int, _at.Interval(ge=0, le=100)], None] = None + table_width_percent: _te.Annotated[int, _at.Interval(ge=0, le=100)] | None = None """Percentage width - 0 to 100, specific to tables.""" @@ -69,7 +69,7 @@ class Details(BaseModel, extra='forbid'): data: pydantic.SerializeAsAny[_types.DataModel] """Data model to display.""" - fields: _t.Union[list[_t.Union[DisplayLookup, Display]], None] = None + fields: list[DisplayLookup | Display] | None = None """Fields to display.""" class_name: _class_name.ClassNameField = None diff --git a/src/python-fastui/fastui/components/forms.py b/src/python-fastui/fastui/components/forms.py index f749bd48..18926d6b 100644 --- a/src/python-fastui/fastui/components/forms.py +++ b/src/python-fastui/fastui/components/forms.py @@ -21,22 +21,22 @@ class BaseFormField(BaseModel, ABC, defer_build=True): name: str """Name of the field.""" - title: _t.Union[list[str], str] + title: list[str] | str """Title of the field to display. Can be a list of strings for multi-line titles.""" required: bool = False """Whether the field is required. Defaults to False.""" - error: _t.Union[str, None] = None + error: str | None = None """Error message to display if the field is invalid.""" locked: bool = False """Whether the field is locked. Defaults to False.""" - description: _t.Union[str, None] = None + description: str | None = None """Description of the field.""" - display_mode: _t.Union[_t.Literal['default', 'inline'], None] = None + display_mode: _t.Literal['default', 'inline'] | None = None """Display mode for the field.""" class_name: _class_name.ClassNameField = None @@ -49,13 +49,13 @@ class FormFieldInput(BaseFormField): html_type: InputHtmlType = 'text' """HTML input type for the field.""" - initial: _t.Union[str, float, None] = None + initial: str | float | None = None """Initial value for the field.""" - placeholder: _t.Union[str, None] = None + placeholder: str | None = None """Placeholder text for the field.""" - autocomplete: _t.Union[str, None] = None + autocomplete: str | None = None """Autocomplete value for the field.""" type: _t.Literal['FormFieldInput'] = 'FormFieldInput' @@ -65,19 +65,19 @@ class FormFieldInput(BaseFormField): class FormFieldTextarea(BaseFormField): """Form field for text area input.""" - rows: _t.Union[int, None] = None + rows: int | None = None """Number of rows for the text area.""" - cols: _t.Union[int, None] = None + cols: int | None = None """Number of columns for the text area.""" - initial: _t.Union[str, None] = None + initial: str | None = None """Initial value for the text area.""" - placeholder: _t.Union[str, None] = None + placeholder: str | None = None """Placeholder text for the text area.""" - autocomplete: _t.Union[str, None] = None + autocomplete: str | None = None """Autocomplete value for the text area.""" type: _t.Literal['FormFieldTextarea'] = 'FormFieldTextarea' @@ -87,7 +87,7 @@ class FormFieldTextarea(BaseFormField): class FormFieldBoolean(BaseFormField): """Form field for boolean input.""" - initial: _t.Union[bool, None] = None + initial: bool | None = None """Initial value for the field.""" mode: _t.Literal['checkbox', 'switch'] = 'checkbox' @@ -100,10 +100,10 @@ class FormFieldBoolean(BaseFormField): class FormFieldFile(BaseFormField): """Form field for file input.""" - multiple: _t.Union[bool, None] = None + multiple: bool | None = None """Whether multiple files can be selected.""" - accept: _t.Union[str, None] = None + accept: str | None = None """Accepted file types.""" type: _t.Literal['FormFieldFile'] = 'FormFieldFile' @@ -116,19 +116,19 @@ class FormFieldSelect(BaseFormField): options: forms.SelectOptions """Options for the select field.""" - multiple: _t.Union[bool, None] = None + multiple: bool | None = None """Whether multiple options can be selected.""" - initial: _t.Union[list[str], str, None] = None + initial: list[str] | str | None = None """Initial value for the field.""" - vanilla: _t.Union[bool, None] = None + vanilla: bool | None = None """Whether to use a vanilla (plain) select element.""" - placeholder: _t.Union[str, None] = None + placeholder: str | None = None """Placeholder text for the field.""" - autocomplete: _t.Union[str, None] = None + autocomplete: str | None = None """Autocomplete value for the field.""" type: _t.Literal['FormFieldSelect'] = 'FormFieldSelect' @@ -141,25 +141,26 @@ class FormFieldSelectSearch(BaseFormField): search_url: str """URL to search for options.""" - multiple: _t.Union[bool, None] = None + multiple: bool | None = None """Whether multiple options can be selected.""" - initial: _t.Union[forms.SelectOption, None] = None + initial: forms.SelectOption | None = None """Initial value for the field.""" - debounce: _t.Union[int, None] = None + debounce: int | None = None """Time in milliseconds to debounce requests by. Defaults to 300ms.""" - placeholder: _t.Union[str, None] = None + placeholder: str | None = None """Placeholder text for the field.""" type: _t.Literal['FormFieldSelectSearch'] = 'FormFieldSelectSearch' """The type of the component. Always 'FormFieldSelectSearch'.""" -FormField = _t.Union[ - FormFieldInput, FormFieldTextarea, FormFieldBoolean, FormFieldFile, FormFieldSelect, FormFieldSelectSearch -] +FormField = ( + FormFieldInput | FormFieldTextarea | FormFieldBoolean | FormFieldFile | FormFieldSelect | FormFieldSelectSearch +) + """Union of all form field types.""" @@ -169,25 +170,25 @@ class BaseForm(BaseModel, ABC, defer_build=True, extra='forbid'): submit_url: str """URL to submit the form data to.""" - initial: _t.Union[dict[str, _types.JsonData], None] = None + initial: dict[str, _types.JsonData] | None = None """Initial values for the form fields, mapping field names to values.""" method: _t.Literal['POST', 'GOTO', 'GET'] = 'POST' """HTTP method to use for the form submission.""" - display_mode: _t.Union[_t.Literal['default', 'page', 'inline'], None] = None + display_mode: _t.Literal['default', 'page', 'inline'] | None = None """Display mode for the form.""" - submit_on_change: _t.Union[bool, None] = None + submit_on_change: bool | None = None """Whether to submit the form on change.""" - submit_trigger: _t.Union[events.PageEvent, None] = None + submit_trigger: events.PageEvent | None = None """Event to trigger form submission.""" - loading: '_t.Union[list[AnyComponent], None]' = None + loading: 'list[AnyComponent] | None' = None """Components to display while the form is submitting.""" - footer: '_t.Union[list[AnyComponent], None]' = None + footer: 'list[AnyComponent] | None' = None """Components to display in the form footer.""" class_name: _class_name.ClassNameField = None diff --git a/src/python-fastui/fastui/components/tables.py b/src/python-fastui/fastui/components/tables.py index b55c054d..7c982333 100644 --- a/src/python-fastui/fastui/components/tables.py +++ b/src/python-fastui/fastui/components/tables.py @@ -23,13 +23,13 @@ class Table(BaseModel, extra='forbid'): data: _t.Sequence[pydantic.SerializeAsAny[_types.DataModel]] """Sequence of data models to display in the table.""" - columns: _t.Union[list[display.DisplayLookup], None] = None + columns: 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[type_[pydantic.BaseModel], None] = pydantic.Field(default=None, exclude=True) + data_model: 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 + no_data_message: str | None = None """Message to display when there is no data.""" class_name: _class_name.ClassNameField = None diff --git a/src/python-fastui/fastui/dev.py b/src/python-fastui/fastui/dev.py index 148efe8d..8a358f46 100644 --- a/src/python-fastui/fastui/dev.py +++ b/src/python-fastui/fastui/dev.py @@ -21,7 +21,7 @@ def dev_fastapi_app(reload_path: str = '/api/__dev__/reload', **fastapi_kwargs) class DevReload: - def __init__(self, default_lifespan: _t.Union[types.Lifespan[FastAPI], None]): + def __init__(self, default_lifespan: types.Lifespan[FastAPI] | None): self.default_lifespan = default_lifespan self.stop = asyncio.Event() diff --git a/src/python-fastui/fastui/events.py b/src/python-fastui/fastui/events.py index 699ed5a3..8d7c08f8 100644 --- a/src/python-fastui/fastui/events.py +++ b/src/python-fastui/fastui/events.py @@ -1,27 +1,27 @@ -from typing import Annotated, Literal, Union +from typing import Annotated, Literal from pydantic import Field from typing_extensions import TypeAliasType from .base import BaseModel -ContextType = TypeAliasType('ContextType', dict[str, Union[str, int]]) +ContextType = TypeAliasType('ContextType', dict[str, str | int]) class PageEvent(BaseModel, defer_build=True): name: str - push_path: Union[str, None] = None - context: Union[ContextType, None] = None - clear: Union[bool, None] = None - next_event: 'Union[AnyEvent, None]' = None + push_path: str | None = None + context: ContextType | None = None + clear: bool | None = None + next_event: 'AnyEvent | None' = None type: Literal['page'] = 'page' class GoToEvent(BaseModel): # can be a path or a full URL - url: Union[str, None] = None - query: Union[dict[str, Union[str, float, None]], None] = None - target: Union[Literal['_blank'], None] = None + url: str | None = None + query: dict[str, str | float | None] | None = None + target: Literal['_blank'] | None = None type: Literal['go-to'] = 'go-to' @@ -31,11 +31,11 @@ class BackEvent(BaseModel): class AuthEvent(BaseModel): # False means clear the token and thereby logout the user - token: Union[str, Literal[False]] - url: Union[str, None] = None + token: str | Literal[False] + url: str | None = None type: Literal['auth'] = 'auth' -AnyEvent = Annotated[Union[PageEvent, GoToEvent, BackEvent, AuthEvent], Field(discriminator='type')] +AnyEvent = Annotated[PageEvent | GoToEvent | BackEvent | AuthEvent, Field(discriminator='type')] PageEvent.model_rebuild(_types_namespace={'AnyEvent': AnyEvent}) diff --git a/src/python-fastui/fastui/forms.py b/src/python-fastui/fastui/forms.py index 1e064715..28f9d22b 100644 --- a/src/python-fastui/fastui/forms.py +++ b/src/python-fastui/fastui/forms.py @@ -52,7 +52,7 @@ async def run_fastui_form(request: fastapi.Request): class FormFile: __slots__ = 'accept', 'max_size' - def __init__(self, accept: _t.Union[str, None] = None, max_size: _t.Union[int, None] = None): + def __init__(self, accept: str | None = None, max_size: int | None = None): self.accept = accept self.max_size = max_size @@ -118,7 +118,7 @@ def _validate_file(self, file: ds.UploadFile) -> None: ) def __get_pydantic_core_schema__(self, source_type: type[_t.Any], *_args) -> core_schema.CoreSchema: - if _t.get_origin(source_type) == list: + if _t.get_origin(source_type) is list: args = _t.get_args(source_type) if len(args) == 1 and issubclass(args[0], ds.UploadFile): return core_schema.no_info_plain_validator_function(self.validate_multiple) @@ -146,7 +146,7 @@ def __repr__(self): _mime_types = MimeTypes() -def get_content_type(file: ds.UploadFile) -> _t.Union[str, None]: +def get_content_type(file: ds.UploadFile) -> str | None: if file.content_type: return file.content_type elif file.filename: @@ -163,7 +163,7 @@ class SelectGroup(_te.TypedDict): options: list[SelectOption] -SelectOptions = _te.TypeAliasType('SelectOptions', _t.Union[list[SelectOption], list[SelectGroup]]) +SelectOptions = _te.TypeAliasType('SelectOptions', list[SelectOption] | list[SelectGroup]) class SelectSearchResponse(pydantic.BaseModel): @@ -186,7 +186,7 @@ def unflatten(form_data: ds.FormData) -> NestedDict: if values == ['']: continue - d: dict[_t.Union[str, int], _t.Any] = result_dict + d: dict[str | int, _t.Any] = result_dict *path, last_key = name_to_loc(key) for part in path: @@ -229,5 +229,5 @@ def name_to_loc(name: str) -> 'json_schema.SchemeLocation': # Use uppercase for consistency with pydantic.Field, which is also a function -def Textarea(rows: _t.Union[int, None] = None, cols: _t.Union[int, None] = None) -> _t.Any: # N802 +def Textarea(rows: int | None = None, cols: int | None = None) -> _t.Any: # N802 return pydantic.Field(json_schema_extra={'format': 'textarea', 'rows': rows, 'cols': cols}) diff --git a/src/python-fastui/fastui/json_schema.py b/src/python-fastui/fastui/json_schema.py index c03e69c5..f3b8cee4 100644 --- a/src/python-fastui/fastui/json_schema.py +++ b/src/python-fastui/fastui/json_schema.py @@ -178,7 +178,7 @@ def json_schema_field_to_field( schema: JsonSchemaField, loc: SchemeLocation, title: list[str], - description: _t.Union[str, None], + description: str | None, required: bool, ) -> FormField: name = loc_to_name(loc) @@ -214,7 +214,7 @@ def json_schema_array_to_fields( schema: JsonSchemaArray, loc: SchemeLocation, title: list[str], - description: _t.Union[str, None], + description: str | None, required: bool, defs: JsonSchemaDefs, ) -> _t.Iterable[FormField]: @@ -249,10 +249,10 @@ def special_string_field( schema: JsonSchemaConcrete, name: str, title: list[str], - description: _t.Union[str, None], + description: str | None, required: bool, multiple: bool, -) -> _t.Union[FormField, None]: +) -> FormField | None: if schema['type'] == 'string': if schema.get('format') == 'binary': return FormFieldFile( diff --git a/src/python-fastui/pyproject.toml b/src/python-fastui/pyproject.toml index 7d4470e7..1ee79f8a 100644 --- a/src/python-fastui/pyproject.toml +++ b/src/python-fastui/pyproject.toml @@ -17,7 +17,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', @@ -28,7 +27,7 @@ classifiers = [ "Framework :: Pydantic :: 2", "Framework :: FastAPI", ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = ["pydantic[email]>=2.8.0"] dynamic = ["version"] diff --git a/src/python-fastui/tests/test_auth_github.py b/src/python-fastui/tests/test_auth_github.py index c0b226a7..82ff8be0 100644 --- a/src/python-fastui/tests/test_auth_github.py +++ b/src/python-fastui/tests/test_auth_github.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import Optional import pytest from fastapi import FastAPI @@ -19,7 +18,7 @@ def fake_github_app(github_requests: list[str]) -> FastAPI: app = FastAPI() @app.post('/login/oauth/access_token') - async def access_token(code: str, client_id: str, client_secret: str, redirect_uri: Optional[str] = None): + async def access_token(code: str, client_id: str, client_secret: str, redirect_uri: str | None = None): r = f'/login/oauth/access_token code={code}' if redirect_uri: r += f' redirect_uri={redirect_uri}' diff --git a/src/python-fastui/tests/test_forms.py b/src/python-fastui/tests/test_forms.py index b51efc86..fdfcf12c 100644 --- a/src/python-fastui/tests/test_forms.py +++ b/src/python-fastui/tests/test_forms.py @@ -1,7 +1,7 @@ import enum from contextlib import asynccontextmanager from io import BytesIO -from typing import Annotated, Union +from typing import Annotated import pytest from fastapi import HTTPException @@ -21,7 +21,7 @@ class FakeRequest: TODO replace this with httpx or similar maybe, perhaps this is sufficient """ - def __init__(self, form_data_list: list[tuple[str, Union[str, UploadFile]]]): + def __init__(self, form_data_list: list[tuple[str, str | UploadFile]]): self._form_data = FormData(form_data_list) @asynccontextmanager @@ -441,7 +441,7 @@ class VarTuple(BaseModel): def test_tuple_optional(): class TupleOptional(BaseModel): - foo: tuple[str, Union[str, None]] + foo: tuple[str, str | None] m = components.ModelForm(model=TupleOptional, submit_url='/foo/') with pytest.raises(NotImplementedError, match='Tuples with optional fields are not yet supported'):