Skip to content
Open

fixs #153

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
7 changes: 5 additions & 2 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.9'
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -23,9 +23,12 @@ jobs:
- name: Run test
run: |
PYTHONPATH=src uv run pytest .
- name: Run lint
run: |
uv run ruff check .
- name: Build and publish
run: |
git fetch --unshallow --tags
uv version $(git describe --tags --abbrev=0)
uv build
uv publish --username __token__ --password ${{ secrets.PYPI_TOKEN }}
uv publish --username __token__ --password ${{ secrets.PYPI_TOKEN }}
11 changes: 9 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,24 @@ on:
jobs:
run-tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.9'
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install uv
uv sync
- name: Run lint
run: |
uv run ruff check .
- name: Run test
run: |
PYTHONPATH=src uv run pytest .
PYTHONPATH=src uv run pytest .
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,7 @@ Pipfile
*.html

node_modules/
.DS_Store
.DS_Store

.opencode/
AGENTS.md
16 changes: 16 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,20 @@ ignore = [
"D102",
"EM101",
"PLR0913",
"SIM117",
"D100",
"D103",
"D104",
"D105",
"D107",
"PGH004",
"INP001",
"ANN204",
"G004",
"B008",
"EM102",
"E501",
]

[tool.ruff.lint.per-file-ignores]
"tests/**/test_*.py" = ["S101", "ANN201"]
39 changes: 22 additions & 17 deletions src/uiwiz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,6 @@ def __init__(
self.add_middleware(AsgiRequestMiddleware)
self.add_middleware(GZipMiddleware)
self.add_middleware(AsgiTtlMiddleware, cache_age=cache_age)
# self.add_middleware(StripHiddenFormFieldMiddleware)
self.extensions: dict[str, Path] = {}
self.app_paths: dict[str, Path] = {}

self.exception_handler(RequestValidationError)(self.handle_validation_error)

Expand All @@ -91,7 +88,7 @@ def get_extension(extension: str, filename: str) -> Response:
if resource_key not in resources:
return Response(status_code=404)

with open(resources[resource_key], encoding="utf-8") as f:
with resources[resource_key].open(encoding="utf-8") as f:
content = f.read()

content_type, _ = guess_type(resource_key)
Expand All @@ -104,26 +101,36 @@ def add_static_files(self, url_path: str, local_directory: str | Path) -> None:
def page(
self,
path: str,
*args, # noqa: ANN002
title: str | None = None,
favicon: str | None = None,
**kwargs, # noqa: ANN003
) -> PageRouter:
return PageRouter(page_definition_class=self.page_definition_class).page(
path,
*args,
title=title,
favicon=favicon,
router=self.router,
**kwargs,
)

def ui(self, path: str, *args, include_js: bool = True, include_css: bool = True, **kwargs) -> PageRouter:
def ui(self, path: str, *, include_js: bool = True, include_css: bool = True, **kwargs: dict) -> PageRouter:
return PageRouter().ui(path=path, include_js=include_js, include_css=include_css, router=self.router, **kwargs)

async def handle_validation_error(self, request: Request, exc: RequestValidationError) -> Response:
fields_with_errors = [item.get("loc")[1] for item in exc.errors()]
ok_fields = [item for item in exc.body if item not in fields_with_errors]
async def handle_validation_error(self, _: Request, exc: RequestValidationError) -> Response:
error_details = exc.errors()
fields_with_errors: list[str] = []
for item in error_details:
loc = item.get("loc") or ()
if len(loc) > 1:
field = str(loc[1])
elif len(loc) == 1:
field = str(loc[0])
else:
field = "body"
fields_with_errors.append(field)

body = exc.body if isinstance(exc.body, dict) else {}
ok_fields = [item for item in body if item not in fields_with_errors]

Frame.get_stack().del_stack()
Frame.get_stack()
Expand All @@ -133,11 +140,7 @@ async def handle_validation_error(self, request: Request, exc: RequestValidation
toast.attributes["hx-swap-oob"] = "afterbegin"
toast.attributes["hx-toast-data"] = json.dumps(
jsonable_encoder(
{
"detail": exc.errors(),
"fieldErrors": fields_with_errors,
"fieldOk": ok_fields,
},
{"detail": error_details, "fieldErrors": fields_with_errors, "fieldOk": ok_fields},
),
)
html = Html("").classes("alert alert-error relative")
Expand All @@ -146,8 +149,10 @@ async def handle_validation_error(self, request: Request, exc: RequestValidation
html.attributes["hx-toast-delete-button"] = lambda: btn.id
with html:
with Col(gap="").classes("relative"):
for item in exc.errors():
Element(content=f"{item.get('loc')[1]}: {item.get('msg')}")
for item in error_details:
loc = item.get("loc") or ()
loc_text = str(loc[1]) if len(loc) > 1 else str(loc[0]) if len(loc) == 1 else "body"
Element(content=f"{loc_text}: {item.get('msg')}")
if not self.auto_close_toast_error:
btn = Button("✕").classes("btn btn-sm btn-circle btn-ghost absolute right-2 top-2")

Expand Down
13 changes: 8 additions & 5 deletions src/uiwiz/docs/layout.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from __future__ import annotations

from pathlib import Path
from typing import Callable
from typing import TYPE_CHECKING

from typing_extensions import override

from uiwiz import PageDefinition, ui
from uiwiz.svg.svg_handler import get_svg

if TYPE_CHECKING:
from collections.abc import Callable

parent = Path(__file__).parent
pages = []

Expand All @@ -27,7 +30,7 @@ def __init__(self, path: str, title: str, file: Path | str | Callable[[], str] |
if path not in [page.path for page in pages]:
pages.append(self)

async def render(self):
async def render(self) -> None:
"""Render the page content."""
with ui.container(padding="p-4"):
if callable(self.content):
Expand All @@ -41,7 +44,7 @@ async def render(self):

class Layout(PageDefinition):
def __init__(self) -> None:
"""Layout
"""Layout.

A layout element that provides a consistent structure for the application.
"""
Expand All @@ -67,13 +70,13 @@ def content(self, _: ui.element) -> ui.element | None:
return content

@override
def footer(self, _: ui.element):
def footer(self, _: ui.element) -> None:
with ui.footer().classes("footer mx-auto footer-center p-4 text-base-content"):
with ui.element("div").classes("flex flex-col items-center justify-center"):
ui.label("Made with ❤️ by Uiwiz").classes("text-sm")
ui.link("GitHub", "https://github.com/declow/uiwizard")

def nav(self, drawer):
def nav(self, drawer: ui.drawer) -> None:
with ui.element().classes(
"sticky top-0 flex h-16 justify-center bg-opacity-90 backdrop-blur transition-shadow duration-100 [transform:translate3d(0,0,0)] shadow-sm z-40",
):
Expand Down
18 changes: 9 additions & 9 deletions src/uiwiz/docs/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

import uvicorn
from docs.layout import Layout, Page, pages
from docs.page_docs import docs_router
from fastapi import Request
from fastapi.responses import HTMLResponse

from docs.layout import Layout, Page, pages
from docs.page_docs import docs_router
from uiwiz import PageDefinition, PageRouter, UiwizApp, ui
from uiwiz.frame import Frame

Expand All @@ -14,9 +15,8 @@


@asynccontextmanager
async def lifespan(app: UiwizApp):
async def lifespan(app: UiwizApp) -> AsyncGenerator[None, None]:
"""Lifespan event handler for the application."""

for page in pages:
"""
Register each page with the application.
Expand All @@ -34,24 +34,24 @@ async def lifespan(app: UiwizApp):
app: UiwizApp = UiwizApp(lifespan=lifespan, page_definition_class=Layout)


async def render_md(request: Request):
page = page_dict.get(request.url.path, None)
async def render_md(request: Request) -> None:
page = page_dict.get(request.url.path)
if page:
await page.render()
else:
with ui.container(padding="p-4"):
ui.markdown("Page not found.")


async def not_found():
async def not_found() -> None:
with ui.container(padding="p-4"):
ui.markdown("Page not found. Please check the URL or return to the home page.")
for page in pages:
ui.link(page.title, page.path)


@app.exception_handler(404)
async def not_found_exception_handler(request: Request, exc: Exception):
async def not_found_exception_handler(request: Request, _: Exception) -> HTMLResponse:
await app.page_definition_class().render(not_found, request, title="Not Found")
return HTMLResponse(
content=Frame.get_stack().render(),
Expand All @@ -61,4 +61,4 @@ async def not_found_exception_handler(request: Request, exc: Exception):


if __name__ == "__main__":
uvicorn.run("docs.main:app", host="0.0.0.0", port=8080, reload=True)
uvicorn.run("docs.main:app", host="0.0.0.0", port=8080, reload=True) # noqa: S104
12 changes: 7 additions & 5 deletions src/uiwiz/docs/page_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,32 @@

from docs.layout import Layout, Page
from docs.pages.docs.elements import create_docs_element, create_elements
from fastapi import Request

from uiwiz import PageRouter, ui

parent = Path(__file__).parent


class LayoutDocs(Layout):
def after_render(self, request):
self.drawer.always_open(True)
def after_render(self, request: Request) -> None: # noqa: ARG002
self.drawer.always_open(value=True)


docs_router = PageRouter(prefix="/reference")


@docs_router.page("/element/{name}", title="Elements")
async def docs_element(name: str):
async def docs_element(name: str) -> None:
create_docs_element(getattr(ui, name), docs_router)


@docs_router.page("/elements", title="Elements")
async def docs_elements():
async def docs_elements() -> None:
create_elements(docs_router)


async def reference():
async def reference() -> None:
with ui.container(padding="p-4"):
ui.markdown("This is the reference page. It will contain links to all the elements and their documentation.")
for value in dir(ui):
Expand Down
Loading
Loading