Skip to content

Commit 4f6c408

Browse files
committed
Enhance app functionality with dynamic stylesheet registration and improved error handling
1 parent b128ea6 commit 4f6c408

File tree

3 files changed

+127
-29
lines changed

3 files changed

+127
-29
lines changed

examples/hello_world_app.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# main.py
2+
from violetear import App
3+
from violetear.markup import Document, Element
4+
5+
app = App()
6+
7+
@app.route("/")
8+
def index():
9+
doc = Document(title="Hello Violetear")
10+
doc.body.extend(
11+
Element("h1", text="Hello World!"),
12+
Element("p", text="Served via Violetear App Engine."),
13+
)
14+
return doc
15+
16+
if __name__ == "__main__":
17+
app.run()

violetear/app.py

Lines changed: 75 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
import os
2-
from typing import Any, Callable, Dict, Optional, List
2+
import inspect
3+
from typing import Any, Callable, Dict, List, Union
4+
35

46
# --- Optional Server Dependencies ---
57
try:
6-
from fastapi import FastAPI, APIRouter, Request
7-
from fastapi.responses import HTMLResponse, Response
8+
from fastapi import FastAPI, APIRouter, Request, Response
9+
from fastapi.responses import HTMLResponse
810
from fastapi.staticfiles import StaticFiles
9-
11+
import uvicorn
1012
HAS_SERVER = True
1113
except ImportError:
1214
HAS_SERVER = False
13-
# Dummy classes for type hinting if dependencies are missing
14-
# FastAPI = object # type: ignore
15-
# APIRouter = object # type: ignore
16-
# Request = object # type: ignore
15+
# Dummy classes for type hinting
16+
FastAPI = object # type: ignore
17+
APIRouter = object # type: ignore
18+
Request = object # type: ignore
19+
Response = object # type: ignore
20+
21+
22+
from .stylesheet import StyleSheet
23+
from .markup import Document, StyleResource
1724

1825

1926
class App:
@@ -26,27 +33,78 @@ def __init__(self, title: str = "Violetear App"):
2633
if not HAS_SERVER:
2734
raise ImportError(
2835
"Violetear Server dependencies are missing. "
29-
"Please install them using `pip install violetear[server]`"
36+
"Please install them with: uv add --extra server 'fastapi[standard]'"
3037
)
3138

3239
self.title = title
3340
self.api = FastAPI(title=title)
34-
self._routes: List[Dict[str, Any]] = []
3541

36-
# We will add the Asset Registry here in Phase 2.2
37-
self.styles = {}
42+
# Registry of served styles to prevent duplicate route registration
43+
self.served_styles: Dict[str, StyleSheet] = {}
44+
45+
def add_style(self, path: str, sheet: StyleSheet):
46+
"""
47+
Registers a stylesheet to be served by the app at a specific path.
48+
49+
Overrides any previous stylesheet at that path.
50+
"""
51+
if path not in self.served_styles:
52+
# Register the route dynamically (just once)
53+
@self.api.get(path)
54+
def serve_css():
55+
# Render the full CSS content
56+
css_content = self.served_styles[path]
57+
return Response(content=css_content, media_type="text/css")
58+
59+
# Set the stylesheet, overrides if existing
60+
# This means we can change stylesheets dynamically
61+
self.served_styles[path] = sheet
62+
63+
def _register_document_styles(self, doc: Document):
64+
"""
65+
Scans a Document for external stylesheets defined in Python
66+
and registers their routes on the fly.
67+
"""
68+
for resource in doc.head.styles:
69+
if isinstance(resource, StyleResource):
70+
# If it has a sheet object AND a URL, it needs to be served
71+
if resource.sheet and resource.href and not resource.inline:
72+
self.add_style(resource.href, resource.sheet)
3873

3974
def route(self, path: str, methods: List[str] = ["GET"]):
4075
"""
4176
Decorator to register a route.
42-
Supports standard SSR (returning Documents) out of the box.
4377
"""
44-
4578
def decorator(func: Callable):
46-
# We will implement the wrapper logic in Phase 2.2
47-
self.api.add_api_route(path, func, methods=methods)
48-
return func
79+
@self.api.api_route(path, methods=methods)
80+
async def wrapper(request: Request):
81+
# 1. Handle Request (POST/GET)
82+
if request.method == "POST":
83+
form_data = await request.form()
84+
# Simple check if function accepts arguments
85+
if inspect.signature(func).parameters:
86+
response = func(form_data)
87+
else:
88+
response = func()
89+
else:
90+
response = func()
91+
92+
# Await if async
93+
if inspect.isawaitable(response):
94+
response = await response
4995

96+
# 2. Handle Document Rendering
97+
if isinstance(response, Document):
98+
# JIT: Check if this doc uses any new stylesheets we need to serve
99+
self._register_document_styles(response)
100+
101+
# Render the HTML (which will contain <link href="..."> tags)
102+
return HTMLResponse(response.render())
103+
104+
# 3. Return raw response (JSON, Dict, etc.)
105+
return response
106+
107+
return wrapper
50108
return decorator
51109

52110
def mount_static(self, directory: str, path: str = "/static"):
@@ -56,6 +114,4 @@ def mount_static(self, directory: str, path: str = "/static"):
56114

57115
def run(self, host="0.0.0.0", port=8000, **kwargs):
58116
"""Helper to run via uvicorn programmatically."""
59-
import uvicorn
60-
61117
uvicorn.run(self.api, host=host, port=port, **kwargs)

violetear/markup.py

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import abc
4+
from dataclasses import dataclass
45
import io
56
from pathlib import Path
67
from typing import (
@@ -231,23 +232,39 @@ def root(self) -> Element:
231232
return self._parent.root()
232233

233234

235+
@dataclass
236+
class StyleResource:
237+
"""
238+
Represents a CSS resource attached to a document.
239+
"""
240+
sheet: StyleSheet | None = None
241+
href: str | None = None
242+
inline: bool = False
243+
244+
234245
class Document(Markup):
235246
def __init__(self, lang: str = "en", **head_kwargs) -> None:
236247
self.lang = lang
237248
self.head = Head(**head_kwargs)
238249
self.body = Body()
239-
self.styles = []
240250

241251
def style(
242-
self, sheet: StyleSheet, inline: bool = False, href: str = None
252+
self, sheet: StyleSheet | None = None, inline: bool = False, href: str | None = None
243253
) -> Document:
254+
if sheet is None and href is None:
255+
raise ValueError("Need either a sheet or an external href")
256+
244257
if not inline and href is None:
245-
raise ValueError("Need an href when inline is false")
258+
raise ValueError("Need an href when inline is False")
246259

247-
if inline:
248-
self.styles.append(sheet)
249-
else:
250-
self.head.styles.append(href)
260+
if inline and sheet is None:
261+
raise ValueError("Need a sheet when inline is True")
262+
263+
self.head.styles.append(StyleResource(
264+
sheet=sheet,
265+
href=href,
266+
inline=inline,
267+
))
251268

252269
return self
253270

@@ -263,7 +280,7 @@ class Head(Markup):
263280
def __init__(self, charset: str = "UTF-8", title: str = "") -> None:
264281
self.charset = charset
265282
self.title = title
266-
self.styles = []
283+
self.styles: list[StyleResource] = []
267284

268285
def _render(self, fp, indent: int):
269286
self._write_line(fp, "<head>", indent)
@@ -278,8 +295,16 @@ def _render(self, fp, indent: int):
278295
)
279296
self._write_line(fp, f"<title>{self.title}</title>", indent + 1)
280297

281-
for href in self.styles:
282-
self._write_line(fp, f'<link rel="stylesheet" href="{href}">', indent + 1)
298+
for resource in self.styles:
299+
if resource.inline and resource.sheet:
300+
# Render Inline
301+
self._write_line(fp, '<style>', indent + 1)
302+
resource.sheet.render(fp)
303+
self._write_line(fp, '</style>', indent + 1)
304+
305+
elif resource.href:
306+
# Render Link
307+
self._write_line(fp, f'<link rel="stylesheet" href="{resource.href}">', indent + 1)
283308

284309
self._write_line(fp, "</head>", indent)
285310

0 commit comments

Comments
 (0)