Skip to content

Commit eb7fbd5

Browse files
committed
Include animation support by default, add installer tests
- Move manim, Pillow, imageio-ffmpeg to core dependencies (no [animations] extra) - Update README, CHANGELOG, AGENTS.md to reflect bundled animations - Add 13 new tests for Marp CLI installer: - Download URL construction and accessibility - Platform detection and mapping - Cached binary detection and permissions - Binary execution verification (--version, --help) - Cache directory structure
1 parent 5409837 commit eb7fbd5

5 files changed

Lines changed: 165 additions & 22 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ cdl-slides compile slides.md --format pdf
9999

100100
## NOTES
101101

102-
- Animations require `pip install cdl-slides[animations]`
102+
- Animation support included by default with `pip install cdl-slides`
103103
- Marp CLI auto-downloads on first use
104104
- GIFs cached by content hash in output directory (e.g., `animations/` alongside slides)
105105
- Graceful degradation: missing manim shows warning-box, not crash

CHANGELOG.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2929

3030
### Features
3131

32-
- Zero-config installation: `pip install cdl-slides`
33-
- Optional animation support: `pip install cdl-slides[animations]`
32+
- Zero-config installation: `pip install cdl-slides` includes everything (Manim, FFmpeg, fonts)
3433
- Bundled fonts: Avenir LT Std, Fira Code, Noto Sans SC
3534
- Bundled ffmpeg via `imageio-ffmpeg` — no system installation required
3635
- GIF caching by content hash for fast re-compilation
37-
- Graceful degradation: missing manim shows warning-box instead of crash
36+
- Graceful degradation: animation errors show warning-box instead of crash
3837

3938
### CLI Commands
4039

README.md

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,19 @@ Compile Markdown files into beautiful CDL-themed [Marp](https://marp.app/) prese
6363
pip install cdl-slides
6464
```
6565

66-
That's it. Marp CLI is automatically downloaded on first compile if not already installed.
66+
That's it! This installs everything you need:
67+
- **Marp CLI** is automatically downloaded on first compile if not already installed
68+
- **Manim** for animated equations and visualizations
69+
- **FFmpeg** bundled via `imageio-ffmpeg` — no system installation required
6770

68-
For **manim animations** (optional), install with the animations extra:
69-
70-
```bash
71-
pip install cdl-slides[animations]
72-
```
73-
74-
This installs manim and bundled ffmpeg. Note: manim requires system libraries (pango, cairo) on Linux — see [manim installation docs](https://docs.manim.community/en/stable/installation.html) if you encounter issues.
71+
**Note:** On Linux, manim requires system libraries (pango, cairo) — see [manim installation docs](https://docs.manim.community/en/stable/installation.html) if you encounter issues.
7572

7673
Or install from source:
7774

7875
```bash
7976
git clone https://github.com/ContextLab/cdl-slides.git
8077
cd cdl-slides
81-
pip install -e . # Basic install
82-
pip install -e ".[animations]" # With animation support
78+
pip install -e .
8379
```
8480

8581
### Marp CLI resolution
@@ -309,8 +305,6 @@ Use the ```` ```flow ```` syntax for simple pipeline diagrams:
309305

310306
Embed animated math visualizations using the **Animate DSL** — a simple, declarative syntax that compiles to [Manim Community](https://www.manim.community/). Animations are rendered to transparent GIFs and embedded in slides.
311307

312-
**Requires:** `pip install cdl-slides[animations]`
313-
314308
FFmpeg is bundled automatically via `imageio-ffmpeg` — no system ffmpeg installation required.
315309

316310
**Usage:**
@@ -406,7 +400,7 @@ plot "np.sin(x)" on ax color=blue as wave
406400

407401
**Available colors:** blue, red, green, yellow, orange, white, black
408402

409-
**Requires:** `pip install cdl-slides[animations]`
403+
Animation support is included by default with `pip install cdl-slides`.
410404

411405
### Scale Directives
412406

pyproject.toml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,16 @@ classifiers = [
3030
dependencies = [
3131
"click>=8.0",
3232
"pygments>=2.15",
33+
"manim>=0.18.0",
34+
"Pillow>=9.0",
35+
"imageio-ffmpeg>=0.5.1",
3336
]
3437

3538
[project.optional-dependencies]
3639
dev = [
3740
"pytest>=7.0",
3841
"ruff>=0.4",
3942
]
40-
animations = [
41-
"manim>=0.18.0",
42-
"Pillow>=9.0",
43-
"imageio-ffmpeg>=0.5.1",
44-
]
4543

4644
[project.urls]
4745
Homepage = "https://www.context-lab.com"

tests/test_marp_cli.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
"""Tests for cdl_slides.marp_cli."""
22

33
import platform
4+
import stat
5+
import subprocess
6+
import urllib.request
7+
from urllib.request import urlopen
48

59
import click.testing
610

711
from cdl_slides.cli import main
812
from cdl_slides.marp_cli import (
13+
_PLATFORM_MAP,
14+
_RELEASE_BASE,
915
MARP_CLI_VERSION,
1016
_get_cache_dir,
17+
_get_cached_marp_path,
1118
_get_platform_key,
1219
get_marp_version_info,
1320
resolve_marp_cli,
@@ -148,3 +155,148 @@ def test_setup_command_idempotent_on_repeated_calls(self):
148155
# Both calls should succeed (exit code 0 or 1)
149156
assert result1.exit_code in (0, 1)
150157
assert result2.exit_code in (0, 1)
158+
159+
160+
class TestInstallerDownloadUrl:
161+
"""Test the download URL construction for Marp CLI installer."""
162+
163+
def test_release_base_url_format(self):
164+
"""Verify the release base URL is correctly formatted."""
165+
assert _RELEASE_BASE.startswith("https://github.com/marp-team/marp-cli/releases/download/")
166+
assert MARP_CLI_VERSION in _RELEASE_BASE
167+
168+
def test_download_url_is_accessible(self):
169+
"""Verify the download URL for current platform is accessible (HEAD request)."""
170+
platform_key = _get_platform_key()
171+
assert platform_key is not None, "Platform should be supported"
172+
173+
os_name, archive_ext = _PLATFORM_MAP[platform_key]
174+
filename = f"marp-cli-{MARP_CLI_VERSION}-{os_name}.{archive_ext}"
175+
url = f"{_RELEASE_BASE}/{filename}"
176+
177+
# Use HEAD request to verify URL exists without downloading
178+
179+
req = urllib.request.Request(url, method="HEAD")
180+
try:
181+
response = urlopen(req, timeout=30)
182+
assert response.status == 200, f"Expected 200, got {response.status}"
183+
except Exception as e:
184+
# If HEAD fails, try GET with small range
185+
req = urllib.request.Request(url)
186+
req.add_header("Range", "bytes=0-0")
187+
response = urlopen(req, timeout=30)
188+
assert response.status in (200, 206), f"URL not accessible: {e}"
189+
190+
191+
class TestInstallerPlatformMapping:
192+
"""Test platform detection and mapping for the installer."""
193+
194+
def test_all_platform_keys_have_valid_structure(self):
195+
"""Verify all platform map entries have correct structure."""
196+
for key, value in _PLATFORM_MAP.items():
197+
assert len(key) == 2, f"Key {key} should be (system, machine) tuple"
198+
assert len(value) == 2, f"Value {value} should be (os_name, archive_ext) tuple"
199+
os_name, archive_ext = value
200+
assert os_name in ("mac", "linux", "win"), f"Unknown OS: {os_name}"
201+
assert archive_ext in ("tar.gz", "zip"), f"Unknown archive: {archive_ext}"
202+
203+
def test_current_platform_is_supported(self):
204+
"""Verify the current platform is in the supported list."""
205+
key = _get_platform_key()
206+
assert key is not None, "Current platform should be supported"
207+
assert key in _PLATFORM_MAP or key[0] in [k[0] for k in _PLATFORM_MAP]
208+
209+
def test_windows_uses_zip(self):
210+
"""Verify Windows platforms use zip archives."""
211+
for key, value in _PLATFORM_MAP.items():
212+
if key[0] == "Windows":
213+
assert value[1] == "zip", "Windows should use zip archives"
214+
215+
def test_unix_uses_tar_gz(self):
216+
"""Verify Unix platforms use tar.gz archives."""
217+
for key, value in _PLATFORM_MAP.items():
218+
if key[0] in ("Darwin", "Linux"):
219+
assert value[1] == "tar.gz", "Unix should use tar.gz archives"
220+
221+
222+
class TestInstallerCachedBinary:
223+
"""Test the cached binary detection and execution."""
224+
225+
def test_cached_path_in_version_directory(self):
226+
"""Verify cached binary path includes version directory."""
227+
cached = _get_cached_marp_path()
228+
if cached is not None:
229+
assert MARP_CLI_VERSION in str(cached), "Cached path should include version"
230+
231+
def test_cached_binary_is_executable(self):
232+
"""Verify the cached binary has executable permissions."""
233+
cached = _get_cached_marp_path()
234+
if cached is not None:
235+
assert cached.exists(), "Cached binary should exist"
236+
mode = cached.stat().st_mode
237+
assert mode & stat.S_IXUSR, "Binary should be user-executable"
238+
239+
240+
class TestInstallerBinaryExecution:
241+
"""Test that the resolved Marp CLI binary actually works."""
242+
243+
def test_resolved_binary_executes_version_command(self):
244+
"""Verify the resolved binary can execute --version."""
245+
result = resolve_marp_cli()
246+
assert result is not None, "Should resolve to something"
247+
248+
if isinstance(result, str):
249+
# Direct binary path
250+
proc = subprocess.run([result, "--version"], capture_output=True, text=True, timeout=30)
251+
else:
252+
# npx command list
253+
proc = subprocess.run(result + ["--version"], capture_output=True, text=True, timeout=60)
254+
255+
assert proc.returncode == 0, f"--version failed: {proc.stderr}"
256+
assert "marp" in proc.stdout.lower() or len(proc.stdout) > 0, "Should output version info"
257+
258+
def test_resolved_binary_help_command(self):
259+
"""Verify the resolved binary can execute --help."""
260+
result = resolve_marp_cli()
261+
assert result is not None
262+
263+
if isinstance(result, str):
264+
proc = subprocess.run([result, "--help"], capture_output=True, text=True, timeout=30)
265+
else:
266+
proc = subprocess.run(result + ["--help"], capture_output=True, text=True, timeout=60)
267+
268+
assert proc.returncode == 0, f"--help failed: {proc.stderr}"
269+
# Help output should mention common options
270+
output_lower = proc.stdout.lower()
271+
assert "html" in output_lower or "pdf" in output_lower or "output" in output_lower
272+
273+
274+
class TestInstallerCacheDirectory:
275+
"""Test cache directory creation and structure."""
276+
277+
def test_cache_dir_is_created(self):
278+
"""Verify cache directory is created if it doesn't exist."""
279+
cache_dir = _get_cache_dir()
280+
assert cache_dir.exists(), "Cache directory should be created"
281+
assert cache_dir.is_dir(), "Cache directory should be a directory"
282+
283+
def test_cache_dir_is_writable(self):
284+
"""Verify cache directory is writable."""
285+
cache_dir = _get_cache_dir()
286+
test_file = cache_dir / ".write_test"
287+
try:
288+
test_file.write_text("test")
289+
assert test_file.exists()
290+
finally:
291+
if test_file.exists():
292+
test_file.unlink()
293+
294+
def test_cache_dir_contains_version_subdir_after_resolve(self):
295+
"""Verify version subdirectory exists after resolving."""
296+
resolve_marp_cli() # Ensure binary is downloaded/cached
297+
cache_dir = _get_cache_dir()
298+
version_dir = cache_dir / MARP_CLI_VERSION
299+
# Only check if we're using cached binary (not system marp or npx)
300+
info = get_marp_version_info()
301+
if info["source"] == "cached":
302+
assert version_dir.exists(), "Version directory should exist for cached binary"

0 commit comments

Comments
 (0)