Skip to content
Merged
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
2 changes: 1 addition & 1 deletion lib/examples/apollo/apollo.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# deps: palettes
# deps: palettes, pygraphics
# gallery: binaries
"""
apollo.py — Apollo Guidance Computer DSKY emulator.
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
--extra-index-url https://pypi.org/simple/

pydevices
pydevices-audioeffects
pydevices-audioif
pydevices-audioinstruments
pydevices-audioeffects
pydevices-desktop
pydevices-lvgl
pydevices-palettes
Expand Down
40 changes: 39 additions & 1 deletion scripts/gallery_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
from __future__ import annotations

import argparse
import ast
import json
from pathlib import Path
import re
Expand Down Expand Up @@ -104,6 +105,9 @@

HEADER_SCAN_LINES = 10
GALLERY_VALUES = frozenset({"featured", "skip", "binaries", "nochrome", "newwindow"})
INSTALLABLE_DEPS = frozenset(
{"palettes", "pygraphics", "pdwidgets", "audioinstruments", "audioeffects"}
)

LOCAL_IMPORT_RE = re.compile(
r"^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))",
Expand Down Expand Up @@ -429,6 +433,38 @@ def discover() -> list[Example]:
return found


def imported_top_level_modules(ex: Example) -> set[str]:
"""Return imports from every Python file shipped for an example."""
imported: set[str] = set()
for rel in ex.pyscript_files:
path = EXAMPLES_DIR / rel
tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"), filename=str(path))
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imported.update(alias.name.split(".", 1)[0] for alias in node.names)
elif isinstance(node, ast.ImportFrom) and node.module:
imported.add(node.module.split(".", 1)[0])
return imported


def validate_example_deps(examples: list[Example]) -> None:
"""Reject missing installable deps and warn about unused declarations."""
errors: list[str] = []
for ex in examples:
imported = imported_top_level_modules(ex)
missing = sorted((imported & INSTALLABLE_DEPS) - set(ex.deps))
if missing:
errors.append(f"{ex.source_rel}: missing # deps: {', '.join(missing)}")
unused = sorted(set(ex.deps) - imported)
if unused:
print(
f"warning: {ex.source_rel}: declared # deps not imported: {', '.join(unused)}",
file=sys.stderr,
)
if errors:
raise SystemExit("gallery dependency errors:\n " + "\n ".join(errors))


# Package deps that are also top-level PyDevices org repos get that repo's
# real ecosystem tier (see .site/pyscript/site-chrome.js ECOSYSTEM_DATA).
# appdev and multimer ship inside pydevices' own lib/, so they take its tier.
Expand Down Expand Up @@ -742,8 +778,10 @@ def main(argv: list[str] | None = None) -> int:
print(f"copied {n} gallery example file(s) to {args.copy_examples}")

# featured, then new-window, then A-Z
discovered = discover()
validate_example_deps(discovered)
examples = sorted(
discover(),
discovered,
key=lambda e: (0 if e.featured else 1 if e.new_window else 2, e.title.lower()),
)
stale: list[str] = []
Expand Down
6 changes: 6 additions & 0 deletions scripts/refresh-requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
PACKAGE_ORDER = (
"pydevices",
"pydevices-audioif",
# audioinstruments and audioeffects publish from PyDevices/audiocomponents
# and sit on audioif; drum_machine and piano need them installed. Added
# here so refresh-requirements stops trying to drop them (they reached
# requirements.txt by hand in 0880cc63 and the check has been red since).
"pydevices-audioinstruments",
"pydevices-audioeffects",
"pydevices-desktop",
"pydevices-lvgl",
"pydevices-palettes",
Expand Down
24 changes: 23 additions & 1 deletion tests/test_gallery_screenshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,33 @@


class TestGalleryScreenshots(unittest.TestCase):
def test_direct_stage_excludes_generated_gui_cache(self):
def test_direct_stage_excludes_generated_gui_cache_and_validates_dependencies(self):
utilities = gallery.tracked_utility_files()
self.assertIn("utils/tft_config.py", utilities)
self.assertFalse(any(path.startswith("utils/gui/") for path in utilities))

with tempfile.TemporaryDirectory() as tmp, mock.patch.object(
gallery, "EXAMPLES_DIR", Path(tmp)
):
package = Path(tmp) / "demo"
package.mkdir()
(package / "demo.py").write_text("import palettes\nfrom pygraphics import Area\n")
(package / "helper.py").write_text("from pdwidgets.button import Button\n")
example = gallery.Example("demo", "lib/examples/demo/demo.py", "manifest")
example.pyscript_files = ["demo/demo.py", "demo/helper.py"]
example.deps = ["palettes", "pygraphics", "pdwidgets", "audioif"]

with mock.patch("sys.stderr") as stderr:
gallery.validate_example_deps([example])
warning = "".join(call.args[0] for call in stderr.write.call_args_list)
self.assertIn("declared # deps not imported: audioif", warning)

example.deps.remove("pdwidgets")
with mock.patch("sys.stderr"), self.assertRaisesRegex(
SystemExit, r"missing # deps: pdwidgets"
):
gallery.validate_example_deps([example])

def test_card_uses_existing_thumbnail(self):
example = gallery.Example("demo", "demo.py", "module")
with tempfile.TemporaryDirectory() as tmp, mock.patch.object(
Expand Down
Loading