feat: folder scan — depth-limited natural-sorted image listing

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 16:24:26 +02:00
parent d589a59fd1
commit ba8de1253e
2 changed files with 69 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
"""Pure folder-scan layer for Folder Image Loader. Stdlib only — no torch."""
import os
import re
from pathlib import Path
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"}
def natural_key(s):
return [int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", s)]
def list_images(folder, depth=0):
root = Path(folder)
if not root.is_dir():
raise NotADirectoryError(f"Not a folder: {folder}")
root_depth = len(root.parts)
results = []
for dirpath, dirnames, filenames in os.walk(root):
cur = Path(dirpath)
rel_depth = len(cur.parts) - root_depth
if depth >= 0 and rel_depth >= depth:
dirnames[:] = [] # don't descend past `depth`
if depth >= 0 and rel_depth > depth:
continue
for name in filenames:
if Path(name).suffix.lower() in IMAGE_EXTS:
results.append(str(cur / name))
results.sort(key=lambda p: natural_key(os.path.relpath(p, root)))
return results
+39
View File
@@ -0,0 +1,39 @@
# tests/test_scan.py
from gates import scan
def _touch(p, data=b"x"):
p.parent.mkdir(parents=True, exist_ok=True)
p.write_bytes(data)
def test_natural_sort_orders_numerically():
items = ["img10.png", "img2.png", "img1.png"]
assert sorted(items, key=scan.natural_key) == ["img1.png", "img2.png", "img10.png"]
def test_list_images_top_level_only_default(tmp_path):
_touch(tmp_path / "a.png"); _touch(tmp_path / "b.jpg"); _touch(tmp_path / "note.txt")
_touch(tmp_path / "sub" / "c.png")
got = [p.split("/")[-1] for p in scan.list_images(str(tmp_path))]
assert got == ["a.png", "b.jpg"] # depth 0: no sub/, no .txt
def test_list_images_depth_one(tmp_path):
_touch(tmp_path / "a.png")
_touch(tmp_path / "sub" / "c.png")
_touch(tmp_path / "sub" / "deep" / "d.png")
got = [p.split("/")[-1] for p in scan.list_images(str(tmp_path), depth=1)]
assert got == ["a.png", "c.png"] # depth 1: include sub/, not sub/deep/
def test_list_images_unlimited_depth(tmp_path):
_touch(tmp_path / "a.png"); _touch(tmp_path / "sub" / "deep" / "d.png")
got = scan.list_images(str(tmp_path), depth=-1)
assert len(got) == 2
def test_list_images_natural_sort_by_relpath(tmp_path):
for n in ["img1.png", "img2.png", "img10.png"]:
_touch(tmp_path / n)
got = [p.split("/")[-1] for p in scan.list_images(str(tmp_path))]
assert got == ["img1.png", "img2.png", "img10.png"]
def test_list_images_bad_path_raises(tmp_path):
import pytest
with pytest.raises(NotADirectoryError):
scan.list_images(str(tmp_path / "nope"))