Skip to content

Commit 6eaef34

Browse files
committed
pye
1 parent 37c0eeb commit 6eaef34

3 files changed

Lines changed: 182 additions & 37 deletions

File tree

examples/pye/server.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
logger = logging.getLogger("pye")
2626

2727
APP_TTL = 10 * 60 # 10 minutes
28+
AUTO_INDEX = False # Set to False to disable directory index listing
2829

2930

3031
def get_app_mtime(p: Path) -> float:
@@ -242,6 +243,27 @@ def _unload_expired_apps(self):
242243
self.router_logger.info(f"TTL expired for htag app '{name}', unloading...")
243244
info.app = None
244245

246+
async def _serve_htag_app(self, name: str, info: AppInfo, scope: Scope, receive: Receive, send: Send, new_path: str) -> bool:
247+
"""Loads and serves an htag application."""
248+
info.last_accessed = time.time()
249+
mtime = get_app_mtime(info.file)
250+
251+
if info.app is None or (scope["type"] == "http" and mtime > info.mtime):
252+
action = "Reloading" if info.app else "Loading"
253+
self.router_logger.info(f"{action} htag app: {name}")
254+
app_class = load_htag_app(info.mod_path)
255+
if app_class:
256+
info.app, info.mtime = WebApp(app_class).app, mtime
257+
else:
258+
return False
259+
260+
if info.app:
261+
scope["path"] = new_path
262+
scope["root_path"] = scope.get("root_path", "") + "/" + name
263+
await info.app(scope, receive, send)
264+
return True
265+
return False
266+
245267
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
246268
if scope["type"] not in ["http", "websocket"]: return
247269
if scope["type"] == "http": self._unload_expired_apps()
@@ -255,20 +277,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
255277
for name in sorted(self.apps.keys(), key=len, reverse=True):
256278
if rel_path == name or rel_path.startswith(name + "/"):
257279
info = self.apps[name]
258-
info.last_accessed = time.time()
259-
mtime = get_app_mtime(info.file)
260-
261-
if info.app is None or (scope["type"] == "http" and mtime > info.mtime):
262-
action = "Reloading" if info.app else "Loading"
263-
self.router_logger.info(f"{action} htag app: {name}")
264-
app_class = load_htag_app(info.mod_path)
265-
if app_class:
266-
info.app, info.mtime = WebApp(app_class).app, mtime
267-
268-
if info.app:
269-
scope["path"] = path[len("/" + name):] or "/"
270-
scope["root_path"] = scope.get("root_path", "") + "/" + name
271-
await info.app(scope, receive, send)
280+
new_path = path[len("/" + name):] or "/"
281+
if await self._serve_htag_app(name, info, scope, receive, send, new_path):
272282
return
273283

274284
# 2. Directory or Index
@@ -284,8 +294,24 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
284294
if rel_path and not path.endswith("/"):
285295
await HTMLResponse("", status_code=301, headers={"Location": path + "/"})(scope, receive, send)
286296
else:
287-
self.router_logger.info(f"Serving directory index: {path}")
288-
await HTMLResponse(IndexGenerator.generate(self.apps_dir, self.apps, rel_path))(scope, receive, send)
297+
if AUTO_INDEX:
298+
self.router_logger.info(f"Serving directory index: {path}")
299+
await HTMLResponse(IndexGenerator.generate(self.apps_dir, self.apps, rel_path))(scope, receive, send)
300+
else:
301+
index_name = f"{rel_path}/index" if rel_path else "index"
302+
if index_name in self.apps:
303+
if await self._serve_htag_app(index_name, self.apps[index_name], scope, receive, send, "/"):
304+
return
305+
306+
index_file = full_path / "index.py"
307+
if index_file.is_file():
308+
self.router_logger.info(f"Running default index script: {index_file}")
309+
response = await ScriptRunner.run(index_file, scope)
310+
await response(scope, receive, send)
311+
return
312+
313+
self.router_logger.warning(f"No entrypoint found and AUTO_INDEX is False: {path}")
314+
await HTMLResponse("No default entrypoint", status_code=400)(scope, receive, send)
289315
return
290316

291317
# 3. Static or Script
@@ -317,5 +343,6 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
317343

318344
if __name__ == "__main__":
319345
import uvicorn
346+
AUTO_INDEX = True # Enable indexing for demo
320347
uvicorn.run(app, host="0.0.0.0", port=8000)
321348

examples/pye/test_server.py

Lines changed: 139 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,21 @@ def client(temp_apps_dir):
6666
return TestClient(app)
6767

6868
def test_root_index(client):
69-
response = client.get("/")
70-
assert response.status_code == 200
71-
assert "text/html" in response.headers["content-type"]
72-
assert "Root Index" in response.text
73-
# Check that our test files are listed
74-
assert "test.txt" in response.text
75-
assert "myscript.py" in response.text
76-
assert "testapp" in response.text
77-
assert "myfolder/" in response.text
69+
import server
70+
original_val = server.AUTO_INDEX
71+
try:
72+
server.AUTO_INDEX = True
73+
response = client.get("/")
74+
assert response.status_code == 200
75+
assert "text/html" in response.headers["content-type"]
76+
assert "Root Index" in response.text
77+
# Check that our test files are listed
78+
assert "test.txt" in response.text
79+
assert "myscript.py" in response.text
80+
assert "testapp" in response.text
81+
assert "myfolder/" in response.text
82+
finally:
83+
server.AUTO_INDEX = original_val
7884

7985
def test_static_file(client, temp_apps_dir):
8086
response = client.get("/test.txt")
@@ -197,18 +203,130 @@ def test_script_timeout(client, temp_apps_dir):
197203
assert "done" in response.text
198204

199205
def test_navigation_depth(client, temp_apps_dir):
200-
# Test deeper nesting and "Parent Directory" links
201-
sub = temp_apps_dir / "folder" / "sub"
202-
sub.mkdir(parents=True)
203-
(sub / "file.txt").write_text("deep")
206+
import server
207+
original_val = server.AUTO_INDEX
208+
try:
209+
server.AUTO_INDEX = True
210+
# Test deeper nesting and "Parent Directory" links
211+
sub = temp_apps_dir / "folder" / "sub"
212+
sub.mkdir(parents=True)
213+
(sub / "file.txt").write_text("deep")
214+
215+
# Check /folder/ index
216+
resp1 = client.get("/folder/")
217+
assert 'href="/folder/sub/"' in resp1.text
218+
assert 'href="/"' in resp1.text # Parent from /folder/ is /
219+
220+
# Check /folder/sub/ index
221+
resp2 = client.get("/folder/sub/")
222+
assert 'href="/folder/sub/file.txt"' in resp2.text
223+
assert 'href="/folder/"' in resp2.text # Parent from /folder/sub/ is /folder/
224+
finally:
225+
server.AUTO_INDEX = original_val
226+
227+
def test_auto_index_disabled(temp_apps_dir):
228+
import server
229+
# Backup original
230+
original_val = server.AUTO_INDEX
231+
try:
232+
server.AUTO_INDEX = False
233+
router = server.DynamicHtagApps(temp_apps_dir)
234+
from starlette.testclient import TestClient
235+
client = TestClient(router)
236+
237+
response = client.get("/")
238+
assert response.status_code == 400
239+
assert response.text == "No default entrypoint"
240+
finally:
241+
server.AUTO_INDEX = original_val
242+
243+
def test_default_index_app(client, temp_apps_dir):
244+
# Create an index htag app in a folder
245+
idx_dir = temp_apps_dir / "hasindex"
246+
idx_dir.mkdir()
247+
app_content = """from htag import Tag
248+
class App(Tag.App):
249+
def init(self):
250+
self += Tag.h1("I am the index app")
251+
"""
252+
(idx_dir / "index.py").write_text(app_content)
204253

205-
# Check /folder/ index
206-
resp1 = client.get("/folder/")
207-
assert 'href="/folder/sub/"' in resp1.text
208-
assert 'href="/"' in resp1.text # Parent from /folder/ is /
254+
import server
255+
original_val = server.AUTO_INDEX
256+
try:
257+
server.AUTO_INDEX = False # Must be False to trigger index check
258+
router = server.DynamicHtagApps(temp_apps_dir)
259+
from starlette.testclient import TestClient
260+
client = TestClient(router)
261+
262+
response = client.get("/hasindex/")
263+
assert response.status_code == 200
264+
assert "I am the index app" in response.text
265+
finally:
266+
server.AUTO_INDEX = original_val
267+
268+
def test_default_index_script(client, temp_apps_dir):
269+
# Create an index script in a folder
270+
idx_dir = temp_apps_dir / "hasidxscript"
271+
idx_dir.mkdir()
272+
(idx_dir / "index.py").write_text("print('I am the index script')")
209273

210-
# Check /folder/sub/ index
211-
resp2 = client.get("/folder/sub/")
212-
assert 'href="/folder/sub/file.txt"' in resp2.text
213-
assert 'href="/folder/"' in resp2.text # Parent from /folder/sub/ is /folder/
274+
import server
275+
original_val = server.AUTO_INDEX
276+
try:
277+
server.AUTO_INDEX = False # Must be False to trigger index check
278+
router = server.DynamicHtagApps(temp_apps_dir)
279+
from starlette.testclient import TestClient
280+
client = TestClient(router)
281+
282+
response = client.get("/hasidxscript/")
283+
assert response.status_code == 200
284+
assert "I am the index script" in response.text
285+
finally:
286+
server.AUTO_INDEX = original_val
287+
288+
def test_auto_index_enabled_ignores_index_file(client, temp_apps_dir):
289+
# If AUTO_INDEX is True, it should list the directory even if index.py exists
290+
idx_dir = temp_apps_dir / "index_enabled"
291+
idx_dir.mkdir()
292+
(idx_dir / "index.py").write_text("print('I should not run')")
293+
(idx_dir / "other.txt").write_text("other")
294+
295+
import server
296+
original_val = server.AUTO_INDEX
297+
try:
298+
server.AUTO_INDEX = True
299+
router = server.DynamicHtagApps(temp_apps_dir)
300+
from starlette.testclient import TestClient
301+
client = TestClient(router)
302+
303+
response = client.get("/index_enabled/")
304+
assert response.status_code == 200
305+
# Should be a listing
306+
assert "Index of index_enabled/" in response.text
307+
assert "index.py" in response.text
308+
assert "other.txt" in response.text
309+
assert "I should not run" not in response.text
310+
finally:
311+
server.AUTO_INDEX = original_val
312+
313+
def test_auto_index_disabled_in_subfolder(client, temp_apps_dir):
314+
# Verify 400 even in nested folders if AUTO_INDEX=False and no index
315+
sub = temp_apps_dir / "mysub" / "deep"
316+
sub.mkdir(parents=True)
317+
(sub / "hidden.txt").write_text("secret")
318+
319+
import server
320+
original_val = server.AUTO_INDEX
321+
try:
322+
server.AUTO_INDEX = False
323+
router = server.DynamicHtagApps(temp_apps_dir)
324+
from starlette.testclient import TestClient
325+
client = TestClient(router)
326+
327+
response = client.get("/mysub/deep/")
328+
assert response.status_code == 400
329+
assert response.text == "No default entrypoint"
330+
finally:
331+
server.AUTO_INDEX = original_val
214332

14.9 KB
Loading

0 commit comments

Comments
 (0)