Compare commits

...

66 Commits

Author SHA1 Message Date
2169938df7 chore(deps): bump version from 5.1.10 to 5.2.0 2024-12-24 11:12:02 +01:00
a207248deb chore(deps): update lock 2024-12-24 11:10:58 +01:00
1c859991b8 chore(deps): pre-commit autoupdate (#504)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.3 → v0.8.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.3...v0.8.4)
- [github.com/pre-commit/mirrors-mypy: v1.13.0 → v1.14.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.13.0...v1.14.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-24 11:10:04 +01:00
a24915665d chore(deps): pre-commit autoupdate (#501)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.8.2 → v0.8.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.8.2...v0.8.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-18 10:44:08 +01:00
5cce117050 chore(deps): bump astral-sh/setup-uv from 3 to 4 (#492)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 3 to 4.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v3...v4)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-12-11 16:10:01 +01:00
bd76fbdfd9 chore(deps): pre-commit autoupdate (#486)
* chore(deps): pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.7.1 → v0.8.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.1...v0.8.2)

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-11 15:55:29 +01:00
5b026919ea chore(deps): bump ManimGL to 1.7.1 (#499)
* chore(deps): bump ManimGL to 1.7.1

Bump ManimGL's minimal version, so relax constraints on other deps and remove compatibility issues with Manim

* fix(docs): correct PR number

* fix(lib): update ManimGL's init

See https://github.com/3b1b/manim/issues/2261

* fix(lib): force float

* chore(tests): correctly ignore warning

* fix(tests)

* fix(tests): add skips

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(tests)

* oops

* fix on 3.12

* fix(lib): correctly patch ManimGL

* fix(deps): pyrr issue

* fix: version

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-11 15:54:59 +01:00
05c1a16ca3 feat(cli): smarter info window hiding logic (#482)
* Adds features from #327

This changes the ``--hide-info-window-`` command to accept three options: auto, always, and never. Auto will hide the info window if there is only one screen. This also fixes the --full-screen option by moving the info window to monitor 1 and the presentation to monitor 0 by default.

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Let Qt decide the screens

* Fix full screen bug

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(cli): improve logic

* fix

* Revert fixes and clean code

* Update changelog

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-12-11 09:16:50 +01:00
3bd8c386b1 chore(deps): bump codecov/codecov-action from 4 to 5 (#491)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-12-06 18:05:18 +01:00
628c8da832 fix(convert): blank web page when converting multiple slides into HTML (#497)
* fix(convert): Blank web page when converting multiple slides into HTML

* chore(docs): add changelog

* Fix typo
2024-12-04 12:25:34 +01:00
3b62e6b788 chore(deps): bump version from 5.1.9 to 5.1.10 2024-12-03 12:57:36 +01:00
ff9aac49d7 chore(deps): pin av<14 to avoid breaking changes (#495)
* chore(deps): Pin `av<14` to avoid breaking changes

Closes #494

* chore(docs): syntax
2024-12-03 12:56:16 +01:00
16d9d32d33 fix(ci): actually run tests on multiple Python versions (#493) 2024-12-03 12:17:32 +01:00
988011ff7d feat(cli): allow multiple reverses (#488)
* Permit multiple reverses

* chore(docs): add changelog entry

---------

Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-11-12 12:09:35 +01:00
3dbe12b480 fix(ci): typo 2024-11-06 10:25:18 +01:00
6ba657c0d5 fix(ci): feature_request.yml 2024-11-06 10:24:50 +01:00
6c8ab61f9d chore(deps): pre-commit autoupdate (#480)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0)
- [github.com/astral-sh/ruff-pre-commit: v0.6.8 → v0.7.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.8...v0.7.1)
- [github.com/pre-commit/mirrors-mypy: v1.11.2 → v1.13.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.2...v1.13.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-11-01 10:01:53 +01:00
91e6e139e3 fix(docs): some typos (#484) 2024-11-01 10:01:28 +01:00
d8acbae165 feat(convert): allow fully offline HTML presentation (#440)
* feat(cli): allow offline HTML presentations

* feat(convert): allow fully offline HTML presentation

TODO: check if this is really the case, especially for nested dependencies?

Closes #438

* fix(cli): typo

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(tests): add tests

* fix(cli): use full path

* fix(tests): typo

* chore(ci): avoid specific kernel name

* fix ?

* chore(lib): simplify logic

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-01 10:00:48 +01:00
75af26e601 chore(docker): drop legacy syntax for env variables 2024-10-15 17:58:20 +02:00
a8903b809d chore(deps): bump version from 5.1.8 to 5.1.9 2024-10-15 17:24:48 +02:00
d813aaf313 chore(docs): add changelog entry 2024-10-15 17:24:37 +02:00
d5679924b9 fix(ci): docker image (#481)
* fix(ci): docker image

* fix(ci): test build docker

* fix(ci): typo :-(

* fix(ci): don't use `--system`
2024-10-15 17:22:03 +02:00
fb562d88ac chore(deps): bump version from 5.1.7 to 5.1.8 2024-10-02 19:56:23 +02:00
8a1fb4c259 chore(deps): pre-commit autoupdate (#479)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.7 → v0.6.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.7...v0.6.8)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-30 23:03:39 +02:00
9db2358793 chore(deps): bump astral-sh/setup-uv from 2 to 3 (#477)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 2 to 3.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](https://github.com/astral-sh/setup-uv/compare/v2...v3)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 10:08:09 +02:00
de91ac7b7c chore(ci): from Rye to uv (#476)
* chore(ci): from Rye to uv

uv is just better for what I need to do, and Rye will eventually be replaced by uv anyway

* chore(ci): add tests' extra

* chore(ci): oops

* fix(ci): some tests

* chore(ci): remove -W error

Because it breaks on the CI, but not locally..
2024-09-24 17:15:18 +02:00
6ad89ecdf6 chore(docs): document new potential fix for PPTX (#475)
* chore(docs): document new potential fix for PPTX

Ref: https://github.com/jeertmans/manim-slides/issues/392#issuecomment-2368198106.

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-24 16:14:34 +02:00
c98501b71c chore(deps): pre-commit autoupdate (#474)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.4 → v0.6.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.4...v0.6.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-24 15:22:32 +02:00
2d7d097276 fix(ci): wrong link 2024-09-23 10:22:24 +02:00
eeac42c638 chore(deps): pre-commit autoupdate (#471)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.3 → v0.6.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.3...v0.6.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-09-15 11:41:11 +02:00
ae42403962 chore(cli): switch option order 2024-09-11 15:57:21 +02:00
1539b2f7e4 feat(cli/convert): add HTML zip output (#472)
Closes #437
2024-09-11 15:30:38 +02:00
a39a9c2bb1 feat(lib): use and add more config option (#452)
* feat(lib): use and add more config option

Closes #441

* feat(lib): add support for more options and test them

* chore(docs): add attributes to documentation

* chore(docs): sort members by member type

* chore(docs): small improvements

* chore(docs): cleanup
2024-09-05 14:08:10 +02:00
87867762ea chore(deps): pre-commit autoupdate (#468)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.2 → v0.6.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.2...v0.6.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-02 22:39:57 +02:00
2f4fe9bd06 fix(lib): deprecation warnings (#467)
* fix(lib): deprecation warnings

* fix(tests): collect tests in the correct order

* fix(tests): ignore pydub warning

* fix(tests): correctly ignore warnings

* fix(ci): do we need faulthandler?
2024-08-27 14:57:23 +02:00
924d8210d9 fix(cli): remove echo 2024-08-27 13:50:44 +02:00
59236b84db chore(deps): pre-commit autoupdate (#466)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.6.1 → v0.6.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.1...v0.6.2)
- [github.com/pre-commit/mirrors-mypy: v1.11.1 → v1.11.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.1...v1.11.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-08-27 12:55:59 +02:00
c047da67b1 feat(cli): manim-slides checkhealth (#458)
* feat(cli): `manim-slides checkhealth`

Closes #457

* chore(fmt): auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(tests): implement some basic tests

* chore(docs): document changes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-27 12:23:24 +02:00
5b6f5eb1e4 fix(present): black video flashes and other bugs linked to Qt (#465)
* Relies on Pixel Format instead of Size

* chore(lib): remove deprecated warning

* chore(deps): update lockfiles

* chore(lib): cleanup code

* chore(ci): run fmt

* chore: update changelog

* chore: typo

* chore(docs): remove ref. to black screen

* fix(deps): issue on Windows

---------

Co-authored-by: PeculiarProgrammer <179261820+PeculiarProgrammer@users.noreply.github.com>
2024-08-26 17:29:39 +02:00
3b23c7739d chore(fmt): auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2024-08-23 21:17:15 +00:00
517fbc6240 Fix for #293 and similar errors
This commit fixes the screen going black after the video ends in versions of PySide6 v6.5.3 and newer. This allows for other errors (like #315) that require a newer version of PySide6 to also be fixed.
2024-08-23 17:05:27 -04:00
c3e1aa0276 chore(deps): pre-commit autoupdate (#463)
* chore(deps): pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.6 → v0.6.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.6...v0.6.1)

* fix(ci): ignore F403 and F405 in notebooks

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Jérome Eertmans <jeertmans@icloud.com>
2024-08-20 11:31:13 +02:00
3e7174c331 chore(deps): pre-commit autoupdate (#459)
updates:
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.13.0 → v2.14.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.13.0...v2.14.0)
- [github.com/astral-sh/ruff-pre-commit: v0.4.10 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.10...v0.5.6)
- [github.com/pre-commit/mirrors-mypy: v1.10.0 → v1.11.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.0...v1.11.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-09 13:20:42 +02:00
6572dc3e21 chore(lib): enhance error message for --folder (#462)
Closes #461.
2024-08-07 12:05:53 +02:00
f707b6cd0c fix(docs): broken link 2024-07-23 14:38:54 +02:00
98f6dbd864 chore(docs): add citing 2024-07-23 14:35:23 +02:00
8c0dd6a605 chore(ci): update pre-commit ci config 2024-07-19 15:54:30 +02:00
ba3756b314 fix(ci): typo 2024-07-19 15:47:45 +02:00
1413a5eb9e fix(ci): move placeholder to correct input 2024-07-19 15:46:57 +02:00
467f744450 fix(ci): label name 2024-07-19 15:43:50 +02:00
4fe347e66a fix(ci): removing template and changing input types 2024-07-19 15:42:35 +02:00
9b62b62386 chore(ci): rework issue templates (#456)
* chore(ci): rework issue templates

Closes #406

* chore(ci): small fixes

* up

* add changelog entry
2024-07-19 15:32:26 +02:00
e2d8c5667f fix(lib): Manim fixes, bump to >= 0.18, and tests (#447)
* fix(lib): Manim fixes, bump to >= 0.18, and tests

* chore(ci): tests and happy mypy

* chore(deps): fix override

* fix(tests): correct skipping

* fix(ci): coverage

* fix(docs): dead links

* fix(tests): deps fixes

* fix(deps): add missing override

* fix(tests): correctly ignore

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(tests): no filterwarning

* chore(tests): add a check to see if we can install package

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* chore(ci): typo

* fix(ci): typo

* oops

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix(ci): double quote instead of single

* chore(ci): add OSes requirements

* chore(tests): removed `full-gl` extra

* chore(ci): automatically cancel jobs

* fix(docs): typo

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-01 18:19:24 +02:00
e80d1d08eb fix(ci): add missing 'v' in tag 2024-06-27 09:57:17 +02:00
5924c329a8 [pre-commit.ci] pre-commit autoupdate (#450)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.8 → v0.4.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.8...v0.4.10)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-26 10:57:46 +02:00
20f91a4590 chore(deps): bump docker/build-push-action from 5 to 6 (#451)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-18 08:13:40 +02:00
964de66563 chore(deps): pin rtoml==0.9.0 on Windows (#432)
* chore(deps): pin `rtoml==0.9.0` on Windows

See #398 and https://github.com/samuelcolvin/rtoml/issues/74.

* chore(ci): trying to fix on Python 3.12
2024-06-12 10:32:50 +02:00
89377e3a55 [pre-commit.ci] pre-commit autoupdate (#444)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.4 → v0.4.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.4...v0.4.8)
- [github.com/codespell-project/codespell: v2.2.6 → v2.3.0](https://github.com/codespell-project/codespell/compare/v2.2.6...v2.3.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-12 10:32:23 +02:00
0aa624c8ad chore(lib): Remove old validation check preventing loop=True with auto_next=True (#445)
* remove old validation check

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update CHANGELOG.md

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-02 13:47:49 +02:00
9460f6b135 fix(convert): Whitespace issue in default RevealJS template (#442)
* Fix whitespace issue in RevealJS template

* Edit changelog
2024-05-27 20:49:14 +02:00
c0a240d758 fix(ci): update rye url
See https://github.com/astral-sh/rye/issues/1111
2024-05-24 21:45:45 +02:00
b08073983b fix(lib): prevent filename collision (#429)
* fix(lib): prevent filename collision

Apparently, ManimCE can produce two different animations with the same name (i.e., the same hash). As documented, ManimGL would any produce files with the same name so this fix was needed.

Closes #428

* chore(lib): update comment

chore(lib): update comment

* chore(tests): add test

* chore(tests): remove redundant underscore

* chore(docs): add changelog entry
2024-05-18 10:17:25 +02:00
993acf0e3f [pre-commit.ci] pre-commit autoupdate (#427)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.2 → v0.4.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.2...v0.4.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-18 10:09:41 +02:00
962ff2bffe chore(deps): bump version from 5.1.6 to 5.1.7 2024-05-03 13:48:01 +02:00
9f2e4757ec chore(ci): fancy README and better bump config (#425)
* chore(ci): fancy README and better bump config

* fix(ci): CITATION.cff

* chore(docs): add changelog entry
2024-05-03 13:47:44 +02:00
52 changed files with 5687 additions and 1434 deletions

View File

@ -1,16 +0,0 @@
[bumpversion]
current_version = 5.1.6
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?
serialize =
{major}.{minor}.{patch}-rc{release}
{major}.{minor}.{patch}
commit = True
message = chore(version): bump {current_version} to {new_version}
[bumpversion:file:manim_slides/__version__.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"
[bumpversion:file:CITATION.cff]
search = version: v{current_version}
replace = version: v{new_version}

View File

@ -1,40 +1,177 @@
name: Bug
description: Report an issue to help improve the project.
labels: bug
title: '[BUG] <description>'
title: '[BUG] <short-description-here>'
labels: [bug]
body:
- type: markdown
id: preamble
attributes:
value: |
**Thank you for reporting a problem about Manim Slides!**
If you know how to solve your problem, feel free to submit a PR too!
> [!WARNING]
> Before reporting your bug, please make sure to:
>
> 1. create and activate virtual environment (venv);
> 2. install `manim-slides` and the necessary dependencies;
> 3. and reduce your Python to a minimal working example (MWE).
>
> You can skip the last step if your issue occurs during installation.
- type: checkboxes
id: terms
attributes:
label: Terms
description: 'By submitting this issue, I have:'
options:
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Abug+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
required: true
- label: Checked the [frequently asked questions]](https://manim-slides.eertmans.be/latest/faq.html);
required: true
- label: Read the [installation instructions](https://manim-slides.eertmans.be/latest/installation.html);
required: true
- label: Created a virtual environment in which I can reproduce my bug;
- type: textarea
id: description
attributes:
label: Description
description: A brief description of the question or issue, also include what you tried and what didn't work
label: Describe the issue
description: A description of the issue, also include what you tried and what didn't work.
validations:
required: true
- type: textarea
id: version
- type: input
id: command
attributes:
label: Version
description: Which version of Manim Slides are you using? You can use `manim-slides --version` to get that information.
label: Command
description: |
Enter the command that failed.
This will be automatically formatted into code, so no need for backticks.
placeholder: manim-slides render mwe.py MWE
validations:
required: true
- type: dropdown
id: issue-type
attributes:
label: Issue Type
description: >
Please select the option in the drop-down.
options:
- Installation issue
- Visual bug when presenting (`manim-slides present`)
- Bug when presenting with HTML/PowerPoint/... format (`manim-slides convert`)
- Other
validations:
required: true
- type: input
id: py-version
attributes:
label: Python version
description: |
Please copy and paste the output of `python --version`.
Make sure to activate your virtual environment first (if any).
placeholder: Python 3.11.8
validations:
required: true
- type: textarea
id: venv
attributes:
label: Python environment
description: |
Please copy and paste the output of `manim-slides checkhealth`.
Make sure to activate your virtual environment first (if any).
This will be automatically formatted into code, so no need for backticks.
If Manim Slides installation failed, enter 'N/A' instead.
render: shell
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
description: What is your platform. Linux, macOS, or Windows?
label: What is your platform?
multiple: true
options:
- Linux
- macOS
- Windows
- Other (please precise below)
validations:
required: true
- type: input
id: platform-other
attributes:
label: Other platform
description: Please answer if you have replied *Other* above.
validations:
required: false
- type: textarea
id: code
attributes:
label: Manim Slides Python code
description: |
Please copy and paste a minimal working example (MWE) of your Python code that can reproduce your bug.
This will be automatically formatted into code, so no need for backticks.
placeholder: |
from manim import *
from manim_slides.slide import Slide
class MWE(Slide):
def construct(self):
circle = Circle(radius=2, color=RED)
dot = Dot()
self.play(GrowFromCenter(circle))
self.next_slide(loop=True)
self.play(MoveAlongPath(dot, circle), run_time=0.5)
self.next_slide()
self.play(dot.animate.move_to(ORIGIN))
render: python
validations:
required: false
- type: textarea
id: logs
attributes:
label: Relevant log output
description: |
Please copy and paste any relevant log output.
This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots if applicable
description: Please add screenshots if applicable.
validations:
required: false
- type: textarea
id: extrainfo
id: extra-info
attributes:
label: Additional information
description: Is there anything else we should know about this bug?
validations:
required: false
- type: textarea
id: suggested-fix
attributes:
label: Recommended fix or suggestions
description: A clear and concise description of how you want to update it.
validations:
required: false

View File

@ -1,57 +1,61 @@
name: Documentation
description: Ask / Report an issue related to the documentation.
title: 'DOC: <description>'
labels: [bug, docs]
title: '[DOC] <short-description-here>'
labels: [documentation]
body:
- type: markdown
id: preamble
attributes:
value: >
**Thank you for wanting to report a problem with manim-slides docs!**
value: |
**Thank you for reporting a problem about Manim Slides' documentation!**
If you know how to solve your problem, feel free to submit a PR too!
If the problem seems straightforward, feel free to submit a PR instead!
Verify first that your issue is not already reported on GitHub [Issues].
[Issues]:
https://github.com/jeertmans/manim-slides/issues
- type: checkboxes
id: terms
attributes:
label: Terms
description: 'By submitting this issue, I have:'
options:
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Adocumentation+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
required: true
- type: textarea
id: description
attributes:
label: Describe the Issue
label: Describe the issue
description: A clear and concise description of the issue you encountered.
validations:
required: true
- type: input
- type: textarea
id: pages
attributes:
label: Affected Page
description: Add a link to page with the problem.
label: Affected page(s)
description: Link to page(s) with the problem.
placeholder: |
+ https://manim-slides.eertmans.be/latest/installation.html
+ https://manim-slides.eertmans.be/latest/features_table.html
validations:
required: true
- type: dropdown
id: issue-type
attributes:
label: Issue Type
label: Issue type
description: >
Please select the option in the drop-down.
<details>
<summary>
<em>Issue?</em>
</summary>
</details>
options:
- Documentation Enhancement
- Documentation Report
- Typo, spelling mistake, broken link, etc.
- Something is missing
- Documentation enhancement
- Other
validations:
required: true
- type: textarea
id: suggested-fix
attributes:
label: Recommended fix or suggestions
description: A clear and concise description of how you want to update it.

View File

@ -1,8 +1,26 @@
name: Feature Request
name: Feature request
description: Have a new idea/feature? Please suggest!
labels: enhancement
title: '[FEATURE] <description>'
title: '[FEATURE] <short-description-here>'
labels: [enhancement]
body:
- type: markdown
id: preamble
attributes:
value: |
**Thank you for suggesting a new feature!**
If you know how to implement it, feel free to submit a PR too!
- type: checkboxes
id: terms
attributes:
label: Terms
description: 'By submitting this issue, I have:'
options:
- label: Checked the [existing issues](https://github.com/jeertmans/manim-slides/issues?q=is%3Aissue+label%3Aenhancement+) and [discussions](https://github.com/jeertmans/manim-slides/discussions) to see if my issue had not already been reported;
required: true
- type: textarea
id: description
attributes:
@ -10,6 +28,7 @@ body:
description: A brief description of the enhancement you propose, also include what you tried and what worked.
validations:
required: true
- type: textarea
id: screenshots
attributes:
@ -17,6 +36,7 @@ body:
description: Please add screenshots if applicable
validations:
required: false
- type: textarea
id: extrainfo
attributes:

View File

@ -1,14 +0,0 @@
name: Question/Help/Support
description: Ask us about Manim Slides
title: 'Support: Ask us anything'
labels: [help, question]
body:
- type: textarea
attributes:
label: "Please explain the issue you're experiencing (with as much detail as possible):"
description: >
Please make sure to leave a reference to the document/code you're
referring to.
validations:
required: true

View File

@ -10,11 +10,13 @@
<!-- Describe all the proposed changes in your PR -->
## Check List (Check all the applicable boxes)
## Check List
- [ ] I understand that my contributions needs to pass the checks.
- [ ] If I created new functions / methods, I documented them and add type hints.
- [ ] If I modified already existing code, I updated the documentation accordingly.
Check all the applicable boxes:
- [ ] I understand that my contributions needs to pass the checks;
- [ ] If I created new functions / methods, I documented them and add type hints;
- [ ] If I modified already existing code, I updated the documentation accordingly;
- [ ] The title of my pull request is a short description of the requested changes.
## Screenshots

View File

@ -18,18 +18,13 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Rye
env:
RYE_INSTALL_OPTION: --yes
run: |
curl -sSf https://rye-up.com/get | bash
echo "$HOME/.rye/shims" >> $GITHUB_PATH
- name: Configure Rye
run: rye config --set-bool behavior.use-uv=true
- name: Setup uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
- name: Build package
run: rye build
run: uv build
- name: Publish to PyPI
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
@ -41,7 +36,6 @@ jobs:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
steps:
- name: Checkout
uses: actions/checkout@v4
@ -68,11 +62,11 @@ jobs:
print(f"tag_name={ref_tag}", file=f)
- name: Build and push
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
platforms: linux/arm64,linux/amd64
file: docker/Dockerfile
push: true
push: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }}
tags: |
ghcr.io/jeertmans/manim-slides:latest
ghcr.io/jeertmans/manim-slides:${{ steps.create_release.outputs.tag_name }}

View File

@ -7,7 +7,52 @@ on:
name: Tests
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
pip-install:
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
pyversion: ['3.9', '3.10', '3.11', '3.12']
extras: [pyside6-full, manimgl]
exclude:
- pyversion: '3.12'
extras: manimgl
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.pyversion }}
cache: pip
- name: Install manim dependencies on MacOS
if: matrix.os == 'macos-latest'
run: brew install ffmpeg py3cairo pango pkg-config scipy
- name: Install manim dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
nohup Xvfb $DISPLAY &
- name: Install Windows dependencies
if: matrix.os == 'windows-latest'
run: choco install ffmpeg
- name: Install package
shell: bash
env:
extras: ${{ matrix.extras }}
run: pip install ".[$extras]"
pytest:
strategy:
fail-fast: false
@ -18,47 +63,26 @@ jobs:
env:
QT_QPA_PLATFORM: offscreen
MANIM_SLIDES_VERBOSITY: error
PYTHONFAULTHANDLER: 1
DISPLAY: :99
GITHUB_WORKFLOWS: 1
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Rye
if: matrix.os != 'windows-latest'
env:
RYE_TOOLCHAIN_VERSION: ${{ matrix.pyversion}}
RYE_INSTALL_OPTION: --yes
run: |
curl -sSf https://rye-up.com/get | bash
echo "$HOME/.rye/shims" >> $GITHUB_PATH
# Stolen from https://github.com/bluss/pyproject-local-kernel/blob/2b641290694adc998fb6bceea58d3737523a68b7/.github/workflows/ci.yaml
- name: Install Rye (Windows)
if: matrix.os == 'windows-latest'
shell: bash
run: |
C:/msys64/usr/bin/wget.exe -q 'https://github.com/astral-sh/rye/releases/latest/download/rye-x86_64-windows.exe' -O rye-x86_64-windows.exe
./rye-x86_64-windows.exe self install --toolchain-version ${{ matrix.pyversion }} --modify-path -y
echo "$HOME\\.rye\\shims" >> $GITHUB_PATH
- name: Configure Rye
shell: bash
run: |
rye config --set-bool behavior.use-uv=true
rye pin ${{ matrix.pyversion }}
- name: Setup uv
uses: astral-sh/setup-uv@v4
with:
enable-cache: true
- name: Install manim dependencies on MacOS
if: matrix.os == 'macos-latest'
run: brew install ffmpeg py3cairo
run: brew install ffmpeg py3cairo pango pkg-config scipy
- name: Install manim dependencies on Ubuntu
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev
sudo apt-get install xvfb
sudo apt-get install build-essential python3-dev libcairo2-dev libpango1.0-dev ffmpeg freeglut3-dev xvfb
nohup Xvfb $DISPLAY &
- name: Install Windows dependencies
@ -69,22 +93,11 @@ jobs:
if: matrix.os == 'windows-latest'
uses: ssciwr/setup-mesa-dist-win@v2
- name: Install Manim Slides
shell: bash
run: rye sync
- name: Run pytest
shell: bash
if: matrix.os != 'ubuntu-latest' || matrix.pyversion != '3.11'
run: rye run pytest
- name: Run pytest and coverage
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
run: rye run pytest --cov-report xml --cov=manim_slides tests/
run: uv run --python ${{ matrix.pyversion }} --frozen --extra tests pytest
- name: Upload to codecov.io
if: matrix.os == 'ubuntu-latest' && matrix.pyversion == '3.11'
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:

2
.gitignore vendored
View File

@ -4,7 +4,6 @@ __pycache__/
/build
/dist
*.egg-info/
.pdm-python
# Manim files
images/
@ -45,6 +44,7 @@ paper/paper.pdf
paper/media/
# Others
.coverage*
coverage.xml
rendering_times.csv

View File

@ -1,36 +1,38 @@
ci:
autofix_commit_msg: |
chore(fmt): auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
autoupdate_commit_msg: 'chore(deps): pre-commit autoupdate'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: check-yaml
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.13.0
rev: v2.14.0
hooks:
- id: pretty-format-yaml
args: [--autofix]
- id: pretty-format-toml
exclude: poetry.lock
args: [--autofix, --trailing-commas]
- repo: https://github.com/keewis/blackdoc
rev: v0.3.9
hooks:
- id: blackdoc
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.2
rev: v0.8.4
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.10.0
rev: v1.14.0
hooks:
- id: mypy
additional_dependencies: [types-requests, types-setuptools]
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
rev: v2.3.0
hooks:
- id: codespell
additional_dependencies:

View File

@ -6,9 +6,6 @@ build:
apt_packages:
- libpango1.0-dev
- ffmpeg
jobs:
post_install:
- ipython kernel install --name "manim-slides" --user
sphinx:
builder: html
configuration: docs/source/conf.py
@ -19,5 +16,3 @@ python:
path: .
extra_requirements:
- docs
- magic
- sphinx-directive

View File

@ -8,7 +8,133 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
<!-- start changelog -->
(unreleased)=
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.1.6...HEAD)
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v5.2.0...HEAD)
(v5.2.0)=
## [v5.2.0](https://github.com/jeertmans/manim-slides/compare/v5.1.10...v5.2.0)
(v5.2.0-changed)=
### Changed
- The info window is now only shown in presentations when there
are multiple monitors. However, the `--show-info-window` option
was added to `manim-slides present` to force the info window.
When there are multiple monitors, the info window will no longer
be on the same monitor as the main window, unless overridden.
[@PeculiarProgrammer](https://github.com/PeculiarProgrammer)
[#482](https://github.com/jeertmans/manim-slides/pull/482)
(v5.2.0-chore)=
### Chore
- Bump ManimGL to `>=1.7.1`, to remove conflicting dependencies
with Manim's.
[#499](https://github.com/jeertmans/manim-slides/pull/499)
(v5.1.10)=
## [v5.1.10](https://github.com/jeertmans/manim-slides/compare/v5.1.9...v5.1.10)
(v5.1.10-added)=
### Added
- Added `--offline` option to `manim-slides convert` for offline
HTML presentations.
[#440](https://github.com/jeertmans/manim-slides/pull/440)
(v5.1.10-changed)=
### Changed
- Allow multiple slide reverses by going backward [@PeculiarProgrammer](https://github.com/PeculiarProgrammer).
[#488](https://github.com/jeertmans/manim-slides/pull/488)
(v5.1.10-fixed)=
### Fixed
- Fixed PyAV issue by pinning its version to `<14`.
A future release will contain a fix that supports both `av>=14`
and `av<14`, as their syntax differ, but the former doesn't
provide binary wheels for Python 3.9.
[#494](https://github.com/jeertmans/manim-slides/pull/494)
- Fixed blank web page when converting multiple slides into HTML.
[#497](https://github.com/jeertmans/manim-slides/pull/497)
(v5.1.9)=
## [v5.1.9](https://github.com/jeertmans/manim-slides/compare/v5.1.8...v5.1.9)
(v5.1.9-fixed)=
## Chore
- Fixed failing docker builds.
[#481](https://github.com/jeertmans/manim-slides/pull/481)
(v5.1.8)=
## [v5.1.8](https://github.com/jeertmans/manim-slides/compare/v5.1.7...v5.1.8)
(v5.1.8-added)=
### Added
- Added `manim-slides checkhealth` command to easily obtain important information
for debug purposes.
[#458](https://github.com/jeertmans/manim-slides/pull/458)
- Added support for `disable_caching` and `flush_cache` options from Manim, and
also the possibility to configure them through class options.
[#452](https://github.com/jeertmans/manim-slides/pull/452)
- Added `--to=zip` convert format to generate an archive with HTML output
and asset files.
[#470](https://github.com/jeertmans/manim-slides/pull/470)
(v5.1.8-chore)=
### Chore
- Pin `rtoml==0.9.0` on Windows platforms,
see [#398](https://github.com/jeertmans/manim-slides/pull/398),
until
[samuelcolvin/rtoml#74](https://github.com/samuelcolvin/rtoml/issues/74)
is solved.
[#432](https://github.com/jeertmans/manim-slides/pull/432)
- Removed an old validation check that prevented setting `loop=True` with
`auto_next=True` on `next_slide()`
[#445](https://github.com/jeertmans/manim-slides/pull/445)
- Improved (and fixed) tests for Manim(GL), bumped minimal ManimCE version,
improved coverage, and override dependency conflicts.
[#447](https://github.com/jeertmans/manim-slides/pull/447)
- Improved issue templates.
[#456](https://github.com/jeertmans/manim-slides/pull/456)
- Enhanced the error message when the slides folder does not exist.
[#462](https://github.com/jeertmans/manim-slides/pull/462)
- Fixed deprecation warnings.
[#467](https://github.com/jeertmans/manim-slides/pull/467)
- Documented potential fix for PPTX issue.
[#475](https://github.com/jeertmans/manim-slides/pull/475)
- Changed project manager from Rye to uv.
[#476](https://github.com/jeertmans/manim-slides/pull/476)
(v5.1.8-fixed)=
### Fixed
- Fix combining assets from multiple scenes to avoid filename collision.
[#429](https://github.com/jeertmans/manim-slides/pull/429)
- Fixed whitespace issue in default RevealJS template.
[#442](https://github.com/jeertmans/manim-slides/pull/442)
- Fixed black screen issue on recent Qt versions and device loss detected,
thanks to [@PeculiarProgrammer](https://github.com/PeculiarProgrammer)!
[#465](https://github.com/jeertmans/manim-slides/pull/465)
(v5.1.8-removed)=
### Removed
- Removed `full-gl` extra, because it does not make sense to ship both
`manimgl` and `manim` together.
[#447](https://github.com/jeertmans/manim-slides/pull/447)
(v5.1.7)=
## [v5.1.7](https://github.com/jeertmans/manim-slides/compare/v5.1.6...v5.1.7)
(v5.1.7-chore)=
### Chore
- Improved the CI for bumping the version and README rendering on PyPI.
[#425](https://github.com/jeertmans/manim-slides/pull/425)
(v5.1.6)=
## [v5.1.6](https://github.com/jeertmans/manim-slides/compare/v5.1.5...v5.1.6)

View File

@ -26,7 +26,7 @@ keywords:
- PowerPoint
- Python
license: MIT
version: v5.1.6
version: v5.2.0
preferred-citation:
publisher:
name: The Open Journal

View File

@ -4,6 +4,8 @@
<img alt="Manim Slides Logo" src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png">
</picture>
<!-- start pypi -->
[![Latest Release][pypi-version-badge]][pypi-version-url]
[![Python version][pypi-python-version-badge]][pypi-version-url]
[![PyPI - Downloads][pypi-download-badge]][pypi-version-url]
@ -29,6 +31,7 @@ Manim Slides will *automatically* detect the one you are using!
- [Usage](#usage)
- [Comparison with Similar Tools](#comparison-with-similar-tools)
- [F.A.Q](https://eertmans.be/manim-slides/latest/faq.html)
- [Citing](#citing)
- [Contributing](#contributing)
* [Reporting an Issue](#reporting-an-issue)
* [Seeking for Help](#seeking-for-help)
@ -149,6 +152,24 @@ Below is a comparison of the most used ones with Manim Slides:
| Web Browser presentations | Yes | No | Yes | No |
| Offline presentations | Yes, with Qt | Yes, with OpenCV | No | No
## Citing
If you use this project, please cite it using the following reference:
```bibtex
@article{Jerome_Eertmans_Manim_Slides_A_2023,
title = {{Manim Slides: A Python package for presenting Manim content anywhere}},
author = {{Jérome Eertmans}},
year = 2023,
month = aug,
journal = {Journal of Open Source Education},
volume = 6,
doi = {10.21105/jose.00206}
}
```
or by linking this GitHub repository at the end of the presentation.
## Contributing
Contributions are more than welcome! Please read through

View File

@ -1,9 +1,8 @@
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/68bd79093e1ebc1ed9f8051942ffe6e72a9e66a7/docker/Dockerfile
# Mostly a copy from https://github.com/ManimCommunity/manim/blob/v0.18.1/docker/Dockerfile
FROM python:3.11-slim
RUN apt-get update -qq \
&& apt-get install --no-install-recommends -y \
ffmpeg \
build-essential \
gcc \
cmake \
@ -24,21 +23,23 @@ RUN wget -O /tmp/install-tl-unx.tar.gz http://mirror.ctan.org/systems/texlive/tl
tar -xzf /tmp/install-tl-unx.tar.gz -C /tmp/install-tl --strip-components=1 && \
/tmp/install-tl/install-tl --profile=/tmp/texlive-profile.txt \
&& tlmgr install \
amsmath babel-english cbfonts-fd cm-super ctex doublestroke dvisvgm everysel \
amsmath babel-english cbfonts-fd cm-super count1to ctex doublestroke dvisvgm everysel \
fontspec frcursive fundus-calligra gnu-freefont jknapltx latex-bin \
mathastext microtype ms physics preview ragged2e relsize rsfs \
mathastext microtype multitoc physics prelim2e preview ragged2e relsize rsfs \
setspace standalone tipa wasy wasysym xcolor xetex xkeyval
# clone and build manim-slides
COPY . /opt/manim-slides
WORKDIR /opt/manim-slides
RUN pip install --no-cache manim[jupyterlab] .[sphinx-directive]
ENV UV_PYTHON=/usr/local/bin/python
RUN pip install --no-cache-dir uv
RUN uv pip install --no-cache-dir manim[jupyterlab] .[sphinx-directive]
ARG NB_USER=manimslidesuser
ARG NB_UID=1000
ENV USER ${NB_USER}
ENV NB_UID ${NB_UID}
ENV HOME /manim-slides
ENV USER=${NB_USER}
ENV NB_UID=${NB_UID}
ENV HOME=/manim-slides
RUN adduser --disabled-password \
--gecos "Default user" \

View File

@ -32,7 +32,7 @@
data-background-video="{{ file }}"
{% if loop.index == 1 and outer_loop.index == 1 -%}
data-background-video-muted
{%- endif -%}
{%- endif %}
{% if slide_config.loop -%}
data-background-video-loop
{%- endif -%}

View File

@ -24,25 +24,30 @@ the repository, and clone it locally.
As for every Python project, using virtual environment is recommended to avoid
conflicts between modules.
For this project, we use [Rye](https://rye-up.com/) to easily manage project
For this project, we use [uv](https://github.com/astral-sh/uv) to easily manage project
and development dependencies. If not already, please install this tool.
## Installing Python modules
With Rye, installation becomes straightforward:
With uv, installation becomes straightforward:
```bash
rye sync --all-features
uv sync --all-extras
```
:::{note}
You still need the same dependencies as to install Manim and ManimGL,
so please check their respective installation guides.
:::
## Running commands
Because modules are installed in a new Python environment,
you cannot use them directly in the shell.
Instead, you either need to prepend `rye run` to any command, e.g.:
Instead, you either need to prepend `uv run` to any command, e.g.:
```bash
rye run manim-slides wizard
uv run manim-slides wizard
```
## Testing your code
@ -51,7 +56,7 @@ Most of the tests are done with GitHub actions, thus not on your computer.
The only command you should run locally is:
```bash
rye run pre-commit run --all-files
uv run pre-commit run --all-files
```
This runs a few linter and formatter to make sure the code quality and style stay
@ -61,7 +66,7 @@ If a warning or an error is displayed, please fix it before going to next step.
For testing your code, simply run:
```bash
rye run pytest
uv run pytest
```
## Building the documentation
@ -73,7 +78,7 @@ To generate the documentation, run the following:
```bash
cd docs
rye run make html
uv run make html
```
Then, the output index file is located at `docs/build/html/index.html` and

View File

@ -29,13 +29,8 @@ ManimGL support is only guaranteed to work
on a very minimal set of versions, because it differs quite a lot from ManimCE,
and its development is not very active.
The typical issues are that (1) ManimGL needs an outdated NumPy version
and (2) ManimGL **should not** be installed from the GitHub repository,
at least not from the `main` branch, but from a version released to PyPI.
To solve the NumPy issue, you can safely downgrade NumPy to a version supported
by ManimGL,
while ignoring the possible *conflicting dependencies* messages from `pip` (or else).
The typical issue is that ManimGL `<1.7.1` needs an outdated NumPy version, but
can be resolved by manually downgrading NumPy, or upgrading ManimGL (**recommended**).
### Presenting
@ -54,9 +49,12 @@ with ManimCE or ManimGL.
### Slides go black when video finishes
This is an issue with Qt,
which cannot be solve on all platforms and Python versions,
which cannot be solved on all platforms and Python versions,
see [#293](https://github.com/jeertmans/manim-slides/issues/293).
Recent version of Manim Slides, i.e., `manim-slides>5.1.7`, come
with a fix that should work fine.
### How to increase quality on Windows
On Windows platform, one may encounter a lower image resolution than expected.
@ -120,3 +118,7 @@ This issue is (probably) caused by PowerPoint never freeing
memory, causing memory allocation errors, and can be partially
solved by reducing the video quality or the number of slides,
see [#392](https://github.com/jeertmans/manim-slides/issues/392).
Another solution, suggested by [@Azercoco](https://github.com/Azercoco) in
[#392 (comment)](https://github.com/jeertmans/manim-slides/issues/392#issuecomment-2368198106),
is to disable hardware/GPU acceleration.

View File

@ -23,10 +23,10 @@ using Manim Slides presentations.
Daniel publishes his presentations on *Cosmology, String Theory and related*
topics on his
[personal website](https://panopepino.github.io/web_page/main_page/slides.html).
[personal website](https://panopepino.github.io/Web_Page/main_page/slides.html). https://panopepino.github.io/Web_Page/main_page/slides.html
For example, below are the slides of a seminar he gave titled
[Our Universe on a (Dark) Bubble](https://panopepino.github.io/web_page/main_page/presentations/2023_11_long/LS.html).
[Our Universe on a (Dark) Bubble](https://panopepino.github.io/Web_Page/main_page/presentations/2023_11_long/LS.html).
<div style="position:relative;padding-bottom:56.25%;">
<iframe
@ -37,7 +37,7 @@ For example, below are the slides of a seminar he gave titled
height="100%"
allowfullscreen
allow="autoplay"
src="https://panopepino.github.io/web_page/main_page/presentations/2023_11_long/LS.html">
src="https://panopepino.github.io/Web_Page/main_page/presentations/2023_11_long/LS.html">
</iframe>
</div>

View File

@ -29,19 +29,6 @@ please refer to their specific installation guidelines:
- [Manim](https://docs.manim.community/en/stable/installation.html)
- [ManimGL](https://3b1b.github.io/manim/getting_started/installation.html)
:::{warning}
If you install Manim from its git repository, as suggested by ManimGL,
make sure to first check out a supported version (e.g., `git checkout tags/v1.6.1`
for ManimGL), otherwise it might install an unsupported version of Manim!
See [#314](https://github.com/jeertmans/manim-slides/issues/314).
Also, note that ManimGL uses outdated dependencies, and may
not work out-of-the-box. One example is NumPy: ManimGL
does not specify any restriction on this package, but
only `numpy<1.25` will work, see
[#2053](https://github.com/3b1b/manim/issues/2053).
:::
<!-- end deps -->
## Pip Install
@ -108,17 +95,12 @@ using optional dependencies:
- `full`, to include `magic`, `manim`, and
`sphinx-directive` extras (see below);
- `full-gl`, to include `magic`, `manimgl`, and
`sphinx-directive` extras (see below);
- `magic`, to include a Jupyter magic to render
animations inside notebooks. This automatically installs `manim`,
and does not work with ManimGL;
- `manim` and `manimgl`, for installing the corresponding
dependencies;
- `pyqt6` to include PyQt6 Qt bindings. Those bindings are available
on most platforms and Python version, but produce a weird black
screen between slide with `manim-slides present`,
see [#QTBUG-118501](https://bugreports.qt.io/browse/QTBUG-118501);
- `pyqt6` to include PyQt6 Qt bindings;
- `pyqt6-full` to include `full` and `pyqt6`;
- `pyside6` to include PySide6 Qt bindings. Those bindings are available
on most platforms and Python version, except on Python 3.12[^2];
@ -142,12 +124,12 @@ Manim Slides is distributed under Nixpkgs >=24.05.
If you are using Nix or NixOS, you can find Manim Slides under:
- `nixpkgs.manim-slides`, which is meant to be a stand alone application and
includes pyqt6 (see above);
includes PyQt6 (see above);
- `nixpkgs.python3Packages.manim-slides`, which is meant to be used as a
module (for notebook magics), and includes IPython but not does not include
any Qt bindings.
module (for notebook magics), and includes IPython but does not include
any Qt binding.
You can try out the Manim Slides package with
You can try out the Manim Slides package with:
```sh
nix-shell -p manim ffmpeg manim-slides
@ -165,7 +147,7 @@ nix-shell -p manim ffmpeg "python3.withPackages(ps: with ps; [ manim-slides, ...
or bundle this into [your Nix environment](https://wiki.nixos.org/wiki/Python).
:::{note}
Nix current does not support `manimgl`.
Nix does not currently support `manimgl`.
:::
## When you need a Qt backend

View File

@ -78,9 +78,9 @@
],
"metadata": {
"kernelspec": {
"display_name": "manim-slides",
"display_name": ".venv",
"language": "python",
"name": "manim-slides"
"name": "python3"
},
"language_info": {
"codemirror_mode": {
@ -92,7 +92,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
"version": "3.11.8"
}
},
"nbformat": 4,

View File

@ -166,7 +166,7 @@ Pages. Please refer to the template page for usage instructions.
### With PowerPoint (*EXPERIMENTAL*)
A recent conversion feature is to the PowerPoint format, thanks to the
A convenient conversion feature is to the PowerPoint format, thanks to the
`python-pptx` package. Even though it is fully working,
it is still considered in an *EXPERIMENTAL* status because we do not
exactly know what versions of PowerPoint (or LibreOffice Impress) are supported.

View File

@ -5,6 +5,7 @@ import requests
from click_default_group import DefaultGroup
from .__version__ import __version__
from .checkhealth import checkhealth
from .convert import convert
from .logger import logger
from .present import list_scenes, present
@ -63,6 +64,7 @@ def cli(notify_outdated_version: bool) -> None:
cli.add_command(convert)
cli.add_command(checkhealth)
cli.add_command(init)
cli.add_command(list_scenes)
cli.add_command(present)

View File

@ -1 +1 @@
__version__ = "5.1.6"
__version__ = "5.2.0"

View File

@ -0,0 +1,37 @@
import sys
import click
from .__version__ import __version__
@click.command()
def checkhealth() -> None:
"""Check Manim Slides' installation."""
click.echo(f"Manim Slides version: {__version__}")
click.echo(f"Python executable: {sys.executable}")
click.echo("Manim bindings:")
try:
from manim import __version__ as manimce_version
click.echo(f"\tmanim (version: {manimce_version})")
except ImportError:
click.secho("\tmanim not found", bold=True)
try:
from manimlib import __version__ as manimlib_version
click.echo(f"\tmanimgl (version: {manimlib_version})")
except ImportError:
click.secho("\tmanimgl not found", bold=True)
try:
from qtpy import API, QT_VERSION
click.echo(f"Qt API: {API} (version: {QT_VERSION})")
except ImportError:
click.secho(
"No Qt API found, some Manim Slides commands will not be available",
bold=True,
)

View File

@ -70,11 +70,22 @@ def verbosity_option(function: F) -> F:
def folder_path_option(function: F) -> F:
"""Wrap a function to add folder path option."""
def callback(ctx: Context, param: Parameter, value: Path) -> Path:
if not value.exists():
raise click.UsageError(
f"Invalid value for '--folder': Directory '{value}' does not exist. "
"Did you render the animations first?",
ctx=ctx,
)
return value
wrapper: Wrapper = click.option(
"--folder",
metavar="DIRECTORY",
default=FOLDER_PATH,
type=click.Path(exists=True, file_okay=False, path_type=Path),
type=click.Path(file_okay=False, path_type=Path),
callback=callback,
help="Set slides folder.",
show_default=True,
)

View File

@ -13,6 +13,8 @@ from pydantic import (
FilePath,
PositiveInt,
PrivateAttr,
conset,
field_serializer,
field_validator,
model_validator,
)
@ -47,20 +49,13 @@ def key_id(name: str) -> PositiveInt:
class Key(BaseModel): # type: ignore[misc]
"""Represents a list of key codes, with optionally a name."""
ids: list[PositiveInt] = Field(unique=True)
ids: conset(PositiveInt, min_length=1) # type: ignore[valid-type]
name: Optional[str] = None
__signal: Signal = PrivateAttr(default_factory=Signal)
@field_validator("ids")
@classmethod
def ids_is_non_empty_set(cls, ids: set[Any]) -> set[Any]:
if len(ids) <= 0:
raise ValueError("Key's ids must be a non-empty set")
return ids
def set_ids(self, *ids: int) -> None:
self.ids = list(set(ids))
self.ids = set(ids)
def match(self, key_id: int) -> bool:
m = key_id in self.ids
@ -77,6 +72,10 @@ class Key(BaseModel): # type: ignore[misc]
def connect(self, function: Receiver) -> None:
self.__signal.connect(function)
@field_serializer("ids")
def serialize_dt(self, ids: set[int]) -> list[int]:
return list(self.ids)
class Keys(BaseModel): # type: ignore[misc]
QUIT: Key = Field(default_factory=lambda: Key(ids=[key_id("Q")], name="QUIT"))
@ -180,7 +179,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
fun_kwargs = {
key: value
for key, value in kwargs.items()
if key not in cls.__fields__
if key not in cls.model_fields
}
fun_kwargs[arg_name] = cls(**kwargs)
return fun(*args, **fun_kwargs)
@ -194,7 +193,7 @@ class BaseSlideConfig(BaseModel): # type: ignore
default=field_info.default,
annotation=field_info.annotation,
)
for field_name, field_info in cls.__fields__.items()
for field_name, field_info in cls.model_fields.items()
]
sig = sig.replace(parameters=parameters)
@ -231,7 +230,7 @@ class PreSlideConfig(BaseSlideConfig):
return cls(
start_animation=start_animation,
end_animation=end_animation,
**base_slide_config.dict(),
**base_slide_config.model_dump(),
)
@field_validator("start_animation", "end_animation")
@ -262,21 +261,6 @@ class PreSlideConfig(BaseSlideConfig):
return pre_slide_config
@model_validator(mode="after")
@classmethod
def loop_and_auto_next_disallowed(
cls, pre_slide_config: "PreSlideConfig"
) -> "PreSlideConfig":
if pre_slide_config.loop and pre_slide_config.auto_next:
raise ValueError(
"You cannot have both `loop=True` and `auto_next=True`, "
"because a looping slide has no ending. "
"This may be supported in the future if "
"https://github.com/jeertmans/manim-slides/pull/299 gets merged."
)
return pre_slide_config
@property
def slides_slice(self) -> slice:
return slice(self.start_animation, self.end_animation)
@ -292,7 +276,7 @@ class SlideConfig(BaseSlideConfig):
def from_pre_slide_config_and_files(
cls, pre_slide_config: PreSlideConfig, file: Path, rev_file: Path
) -> "SlideConfig":
return cls(file=file, rev_file=rev_file, **pre_slide_config.dict())
return cls(file=file, rev_file=rev_file, **pre_slide_config.model_dump())
class PresentationConfig(BaseModel): # type: ignore[misc]
@ -324,23 +308,22 @@ class PresentationConfig(BaseModel): # type: ignore[misc]
f.write(self.model_dump_json(indent=2))
def copy_to(
self, folder: Path, use_cached: bool = True, include_reversed: bool = True
) -> "PresentationConfig":
self,
folder: Path,
use_cached: bool = True,
include_reversed: bool = True,
prefix: str = "",
) -> None:
"""Copy the files to a given directory."""
for slide_config in self.slides:
file = slide_config.file
rev_file = slide_config.rev_file
dest = folder / file.name
rev_dest = folder / rev_file.name
slide_config.file = dest
slide_config.rev_file = rev_dest
dest = folder / f"{prefix}{file.name}"
rev_dest = folder / f"{prefix}{rev_file.name}"
if not use_cached or not dest.exists():
shutil.copy(file, dest)
if include_reversed and (not use_cached or not rev_dest.exists()):
shutil.copy(rev_file, rev_dest)
return self

View File

@ -1,6 +1,7 @@
import mimetypes
import os
import platform
import shutil
import subprocess
import tempfile
import webbrowser
@ -14,6 +15,8 @@ from typing import Any, Callable, Optional, Union
import av
import click
import pptx
import requests
from bs4 import BeautifulSoup
from click import Context, Parameter
from jinja2 import Template
from lxml import etree
@ -115,9 +118,9 @@ class Converter(BaseModel): # type: ignore
"""
return ""
def open(self, file: Path) -> Any:
def open(self, file: Path) -> None:
"""Open a file, generated with converter, using appropriate application."""
raise NotImplementedError
open_with_default(file)
@classmethod
def from_string(cls, s: str) -> type["Converter"]:
@ -126,6 +129,7 @@ class Converter(BaseModel): # type: ignore
"html": RevealJS,
"pdf": PDF,
"pptx": PowerPoint,
"zip": HtmlZip,
}[s]
@ -285,8 +289,11 @@ class RevealTheme(str, StrEnum):
class RevealJS(Converter):
# Export option: use data-uri
# Export option:
data_uri: bool = False
offline: bool = Field(
False, description="Download remote assets for offline presentation."
)
# Presentation size options from RevealJS
width: Union[Str, int] = Str("100%")
height: Union[Str, int] = Str("100%")
@ -380,40 +387,60 @@ class RevealJS(Converter):
return resources.files(templates).joinpath("revealjs.html").read_text()
def open(self, file: Path) -> bool:
return webbrowser.open(file.absolute().as_uri())
def open(self, file: Path) -> None:
webbrowser.open(file.absolute().as_uri())
def convert_to(self, dest: Path) -> None:
def convert_to(self, dest: Path) -> None: # noqa: C901
"""
Convert this configuration into a RevealJS HTML presentation, saved to
DEST.
"""
if self.data_uri:
assets_dir = Path("") # Actually we won't care.
else:
dirname = dest.parent
basename = dest.stem
ext = dest.suffix
dirname = dest.parent
basename = dest.stem
ext = dest.suffix
assets_dir = Path(
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
)
full_assets_dir = dirname / assets_dir
assets_dir = Path(
self.assets_dir.format(dirname=dirname, basename=basename, ext=ext)
)
full_assets_dir = dirname / assets_dir
if not self.data_uri or self.offline:
logger.debug(f"Assets will be saved to: {full_assets_dir}")
full_assets_dir.mkdir(parents=True, exist_ok=True)
for presentation_config in self.presentation_configs:
presentation_config.copy_to(full_assets_dir, include_reversed=False)
if not self.data_uri:
num_presentation_configs = len(self.presentation_configs)
if num_presentation_configs > 1:
# Prevent possible name collision, see:
# https://github.com/jeertmans/manim-slides/issues/428
# With ManimCE, this can happen when caching is disabled as filenames are
# 'uncached_000x.mp4'
# With ManimGL, this can easily occur since filenames are just basic integers...
num_digits = len(str(num_presentation_configs - 1))
def prefix(i: int) -> str:
return f"s{i:0{num_digits}d}_"
else:
def prefix(i: int) -> str:
return ""
for i, presentation_config in enumerate(self.presentation_configs):
presentation_config.copy_to(
full_assets_dir, include_reversed=False, prefix=prefix(i)
)
dest.parent.mkdir(parents=True, exist_ok=True)
with open(dest, "w") as f:
revealjs_template = Template(self.load_template())
options = self.dict()
options["assets_dir"] = assets_dir
options = self.model_dump()
if assets_dir is not None:
options["assets_dir"] = assets_dir
has_notes = any(
slide_config.notes != ""
@ -426,12 +453,49 @@ class RevealJS(Converter):
get_duration_ms=get_duration_ms,
has_notes=has_notes,
env=os.environ,
prefix=prefix if not self.data_uri else None,
**options,
)
if self.offline:
soup = BeautifulSoup(content, "html.parser")
session = requests.Session()
for tag, inner in [("link", "href"), ("script", "src")]:
for item in soup.find_all(tag):
if item.has_attr(inner) and (link := item[inner]).startswith(
"http"
):
asset_name = link.rsplit("/", 1)[1]
asset = session.get(link)
with open(full_assets_dir / asset_name, "wb") as asset_file:
asset_file.write(asset.content)
item[inner] = str(assets_dir / asset_name)
content = str(soup)
f.write(content)
class HtmlZip(RevealJS):
def open(self, file: Path) -> None:
super(RevealJS, self).open(file) # Override opening with web browser
def convert_to(self, dest: Path) -> None:
"""
Convert this configuration into a zipped RevealJS HTML presentation, saved to
DEST.
"""
with tempfile.TemporaryDirectory() as directory_name:
directory = Path(directory_name)
html_file = directory / dest.with_suffix(".html").name
super().convert_to(html_file)
shutil.make_archive(str(dest.with_suffix("")), "zip", directory_name)
class FrameIndex(str, Enum):
first = "first"
last = "last"
@ -442,9 +506,6 @@ class PDF(Converter):
resolution: PositiveFloat = 100.0
model_config = ConfigDict(use_enum_values=True, extra="forbid")
def open(self, file: Path) -> None:
return open_with_default(file)
def convert_to(self, dest: Path) -> None:
"""Convert this configuration into a PDF presentation, saved to DEST."""
images = []
@ -479,9 +540,6 @@ class PowerPoint(Converter):
poster_frame_image: Optional[FilePath] = None
model_config = ConfigDict(use_enum_values=True, extra="forbid")
def open(self, file: Path) -> None:
return open_with_default(file)
def convert_to(self, dest: Path) -> None:
"""Convert this configuration into a PowerPoint presentation, saved to DEST."""
prs = pptx.Presentation()
@ -556,7 +614,7 @@ class PowerPoint(Converter):
def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wrap a function to add a `--show-config` option."""
"""Wrap a function to add a '--show-config' option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
@ -587,7 +645,7 @@ def show_config_options(function: Callable[..., Any]) -> Callable[..., Any]:
def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
"""Wrap a function to add a `--show-template` option."""
"""Wrap a function to add a '--show-template' option."""
def callback(ctx: Context, param: Parameter, value: bool) -> None:
if not value or ctx.resilient_parsing:
@ -620,7 +678,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
@click.argument("dest", type=click.Path(dir_okay=False, path_type=Path))
@click.option(
"--to",
type=click.Choice(["auto", "html", "pdf", "pptx"], case_sensitive=False),
type=click.Choice(["auto", "html", "pdf", "pptx", "zip"], case_sensitive=False),
metavar="FORMAT",
default="auto",
show_default=True,
@ -632,7 +690,6 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
is_flag=True,
help="Open the newly created file using the appropriate application.",
)
@click.option("-f", "--force", is_flag=True, help="Overwrite any existing file.")
@click.option(
"-c",
"--config",
@ -640,7 +697,7 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
multiple=True,
callback=validate_config_option,
help="Configuration options passed to the converter. "
"E.g., pass ``-cslide_number=true`` to display slide numbers.",
"E.g., pass '-cslide_number=true' to display slide numbers.",
)
@click.option(
"--use-template",
@ -648,7 +705,13 @@ def show_template_option(function: Callable[..., Any]) -> Callable[..., Any]:
metavar="FILE",
type=click.Path(exists=True, dir_okay=False, path_type=Path),
help="Use the template given by FILE instead of default one. "
"To echo the default template, use ``--show-template``.",
"To echo the default template, use '--show-template'.",
)
@click.option(
"--offline",
is_flag=True,
help="Download any remote content and store it in the assets folder. "
"The is a convenient alias to '-coffline=true'.",
)
@show_template_option
@show_config_options
@ -659,9 +722,9 @@ def convert(
dest: Path,
to: str,
open_result: bool,
force: bool,
config_options: dict[str, str],
template: Optional[Path],
offline: bool,
) -> None:
"""Convert SCENE(s) into a given format and writes the result in DEST."""
presentation_configs = get_scenes_presentation_config(scenes, folder)
@ -672,13 +735,20 @@ def convert(
try:
cls = Converter.from_string(fmt)
except KeyError:
logger.warn(
logger.warning(
f"Could not guess conversion format from {dest!s}, defaulting to HTML."
)
cls = RevealJS
else:
cls = Converter.from_string(to)
if (
offline
and issubclass(cls, (RevealJS, HtmlZip))
and "offline" not in config_options
):
config_options["offline"] = "true"
converter = cls(
presentation_configs=presentation_configs,
template=template,

View File

@ -283,8 +283,7 @@ class ManimSlidesDirective(Directive):
# Rendering is skipped if the tag skip-manim is present,
# or if we are making the pot-files
should_skip = (
"skip-manim-slides"
in self.state.document.settings.env.app.builder.tags.tags
self.state.document.settings.env.app.builder.tags.has("skip-manim-slides")
or self.state.document.settings.env.app.builder.name == "gettext"
or "SKIP_MANIM_SLIDES" in os.environ
)

View File

@ -228,7 +228,7 @@ class ManimSlidesMagic(Magics): # type: ignore
# TODO: FIXME
# Seems like files are blocked so date-uri is the only working option...
if kwargs.get("data_uri", "false").lower().strip() == "false":
logger.warn(
logger.warning(
"data_uri option is currently automatically enabled, "
"because using local video files does not seem to work properly."
)

View File

@ -1,7 +1,7 @@
import signal
import sys
from pathlib import Path
from typing import Optional
from typing import Literal, Optional
import click
from click import Context, Parameter
@ -11,23 +11,6 @@ from ..commons import config_path_option, folder_path_option, verbosity_option
from ..config import Config, PresentationConfig
from ..logger import logger
PREFERRED_QT_VERSIONS = ("6.5.1", "6.5.2")
def warn_if_non_desirable_pyside6_version() -> None:
from qtpy import API, QT_VERSION
if sys.version_info < (3, 12) and (
API != "pyside6" or QT_VERSION not in PREFERRED_QT_VERSIONS
):
logger.warn(
f"You are using {API = }, {QT_VERSION = }, "
"but we recommend installing 'PySide6==6.5.2', mainly to avoid "
"flashing screens between slides, "
"see issue https://github.com/jeertmans/manim-slides/issues/293. "
"You can do so with `pip install 'manim-slides[pyside6]'`."
)
@click.command()
@folder_path_option
@ -50,7 +33,7 @@ def _list_scenes(folder: Path) -> list[str]:
except (
Exception
) as e: # Could not parse this file as a proper presentation config
logger.warn(
logger.warning(
f"Something went wrong with parsing presentation config `{filepath}`: {e}"
)
@ -239,8 +222,14 @@ def start_at_callback(
)
@click.option(
"--hide-info-window",
is_flag=True,
help="Hide info window.",
flag_value="always",
help="Hide info window. By default, hide the info window if there is only one screen.",
)
@click.option(
"--show-info-window",
"hide_info_window",
flag_value="never",
help="Force to show info window.",
)
@click.option(
"--info-window-screen",
@ -248,11 +237,13 @@ def start_at_callback(
metavar="NUMBER",
type=int,
default=None,
help="Put info window on the given screen (a.k.a. display).",
help="Put info window on the given screen (a.k.a. display). "
"If there is more than one screen, it will by default put the info window "
"on a different screen than the main player.",
)
@click.help_option("-h", "--help")
@verbosity_option
def present(
def present( # noqa: C901
scenes: list[str],
config_path: Path,
folder: Path,
@ -268,7 +259,7 @@ def present(
screen_number: Optional[int],
playback_rate: float,
next_terminates_loop: bool,
hide_info_window: bool,
hide_info_window: Optional[Literal["always", "never"]],
info_window_screen_number: Optional[int],
) -> None:
"""
@ -302,8 +293,6 @@ def present(
if start_at[1]:
start_at_slide_number = start_at[1]
warn_if_non_desirable_pyside6_version()
from qtpy.QtCore import Qt
from qtpy.QtGui import QScreen
@ -313,22 +302,36 @@ def present(
app = qapp()
app.setApplicationName("Manim Slides")
screens = app.screens()
def get_screen(number: int) -> Optional[QScreen]:
try:
return app.screens()[number]
return screens[number]
except IndexError:
logger.error(
f"Invalid screen number {number}, "
f"allowed values are from 0 to {len(app.screens())-1} (incl.)"
f"allowed values are from 0 to {len(screens)-1} (incl.)"
)
return None
should_hide_info_window = False
if hide_info_window is None:
should_hide_info_window = len(screens) == 1
elif hide_info_window == "always":
should_hide_info_window = True
if should_hide_info_window and info_window_screen_number is not None:
logger.warning(
f"Ignoring `--info-window-screen` because `--hide-info-window` is set to `{hide_info_window}`."
)
if screen_number is not None:
screen = get_screen(screen_number)
else:
screen = None
if info_window_screen_number is not None:
if info_window_screen_number is not None and not should_hide_info_window:
info_window_screen = get_screen(info_window_screen_number)
else:
info_window_screen = None
@ -352,11 +355,11 @@ def present(
screen=screen,
playback_rate=playback_rate,
next_terminates_loop=next_terminates_loop,
hide_info_window=hide_info_window,
hide_info_window=should_hide_info_window,
info_window_screen=info_window_screen,
)
player.show()
player.show(screens)
signal.signal(signal.SIGINT, signal.SIG_DFL)
sys.exit(app.exec())

View File

@ -4,7 +4,7 @@ from typing import Optional
from qtpy.QtCore import Qt, QTimer, QUrl, Signal, Slot
from qtpy.QtGui import QCloseEvent, QIcon, QKeyEvent, QScreen
from qtpy.QtMultimedia import QAudioOutput, QMediaPlayer
from qtpy.QtMultimedia import QAudioOutput, QMediaPlayer, QVideoFrame
from qtpy.QtMultimediaWidgets import QVideoWidget
from qtpy.QtWidgets import (
QHBoxLayout,
@ -28,7 +28,6 @@ class Info(QWidget): # type: ignore[misc]
def __init__(
self,
*,
full_screen: bool,
aspect_ratio_mode: Qt.AspectRatioMode,
screen: Optional[QScreen],
) -> None:
@ -38,9 +37,6 @@ class Info(QWidget): # type: ignore[misc]
self.setScreen(screen)
self.move(screen.geometry().topLeft())
if full_screen:
self.setWindowState(Qt.WindowFullScreen)
layout = QHBoxLayout()
# Current slide view
@ -226,6 +222,8 @@ class Player(QMainWindow): # type: ignore[misc]
self.icon = QIcon(":/icon.png")
self.setWindowIcon(self.icon)
self.frame = QVideoFrame()
self.audio_output = QAudioOutput()
self.video_widget = QVideoWidget()
self.video_sink = self.video_widget.videoSink()
@ -241,15 +239,12 @@ class Player(QMainWindow): # type: ignore[misc]
self.slide_changed.connect(self.slide_changed_callback)
self.info = Info(
full_screen=full_screen,
aspect_ratio_mode=aspect_ratio_mode,
screen=info_window_screen,
)
self.info.close_event.connect(self.closeEvent)
self.info.key_press_event.connect(self.keyPressEvent)
self.video_sink.videoFrameChanged.connect(
lambda frame: self.info.video_sink.setVideoFrame(frame)
)
self.video_sink.videoFrameChanged.connect(self.frame_changed)
self.hide_info_window = hide_info_window
# Connecting key callbacks
@ -319,7 +314,7 @@ class Player(QMainWindow): # type: ignore[misc]
elif -self.presentations_count <= index < 0:
self.__current_presentation_index = index + self.presentations_count
else:
logger.warn(f"Could not set presentation index to {index}.")
logger.warning(f"Could not set presentation index to {index}.")
return
self.presentation_changed.emit()
@ -343,7 +338,7 @@ class Player(QMainWindow): # type: ignore[misc]
elif -self.current_slides_count <= index < 0:
self.__current_slide_index = index + self.current_slides_count
else:
logger.warn(f"Could not set slide index to {index}.")
logger.warning(f"Could not set slide index to {index}.")
return
self.slide_changed.emit()
@ -484,11 +479,28 @@ class Player(QMainWindow): # type: ignore[misc]
self.info.next_media_player.setSource(url)
self.info.next_media_player.play()
def show(self) -> None:
def show(self, screens: list[QScreen]) -> None:
"""Screens is necessary to prevent the info window from being shown on the same screen as the main window (especially in full screen mode)."""
super().show()
if not self.hide_info_window:
self.info.show()
if len(screens) > 1 and self.isFullScreen():
self.ensure_different_screens(screens)
if self.isFullScreen():
self.info.showFullScreen()
else:
self.info.show()
if (
len(screens) > 1 and self.info.screen() == self.screen()
): # It is better when Qt assigns the location, but if it fails to, this is a fallback
self.ensure_different_screens(screens)
def ensure_different_screens(self, screens: list[QScreen]) -> None:
target_screen = screens[1] if self.screen() == screens[0] else screens[0]
self.info.setScreen(target_screen)
self.info.move(target_screen.geometry().topLeft())
@Slot()
def close(self) -> None:
@ -515,6 +527,9 @@ class Player(QMainWindow): # type: ignore[misc]
@Slot()
def reverse(self) -> None:
if self.playing_reversed_slide and self.current_slide_index >= 1:
self.current_slide_index -= 1
self.load_reversed_slide()
self.preview_next_slide()
@ -535,8 +550,10 @@ class Player(QMainWindow): # type: ignore[misc]
def full_screen(self) -> None:
if self.windowState() == Qt.WindowFullScreen:
self.setWindowState(Qt.WindowNoState)
self.info.setWindowState(Qt.WindowNoState)
else:
self.setWindowState(Qt.WindowFullScreen)
self.info.setWindowState(Qt.WindowFullScreen)
@Slot()
def hide_mouse(self) -> None:
@ -545,6 +562,34 @@ class Player(QMainWindow): # type: ignore[misc]
else:
self.setCursor(Qt.BlankCursor)
def frame_changed(self, frame: QVideoFrame) -> None:
"""
Slot to handle possibly invalid frames.
This slot cannot be decorated with ``@Slot`` as
the video sinks are handled in different threads.
As of Qt>=6.5.3, the last frame of every video is "flushed",
resulting in a short black screen between each slide.
To avoid this issue, we check every frame, and avoid playing
invalid ones.
References
----------
1. https://github.com/jeertmans/manim-slides/issues/293
2. https://github.com/jeertmans/manim-slides/pull/464
:param frame: The most recent frame.
"""
if frame.isValid():
self.frame = frame
else:
self.video_sink.setVideoFrame(self.frame) # Reuse previous frame
self.info.video_sink.setVideoFrame(self.frame)
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
self.close()

View File

@ -1,7 +1,7 @@
__all__ = [
"API_NAME",
"MANIM",
"MANIMGL",
"API_NAME",
"Slide",
"ThreeDSlide",
]

View File

@ -3,6 +3,7 @@ from __future__ import annotations
__all__ = ["BaseSlide"]
import platform
import shutil
from abc import abstractmethod
from collections.abc import MutableMapping, Sequence, ValuesView
from pathlib import Path
@ -32,6 +33,10 @@ LEFT: np.ndarray = np.array([-1.0, 0.0, 0.0])
class BaseSlide:
disable_caching: bool = False
flush_cache: bool = False
skip_reversing: bool = False
def __init__(
self, *args: Any, output_folder: Path = FOLDER_PATH, **kwargs: Any
) -> None:
@ -170,11 +175,23 @@ class BaseSlide:
animations. You must still call :code:`self.add` or
play some animation that introduces each Mobject for
it to appear. The same applies when removing objects.
.. seealso::
:attr:`canvas` for usage examples.
"""
self._canvas.update(objects)
def remove_from_canvas(self, *names: str) -> None:
"""Remove objects from the canvas."""
"""
Remove objects from the canvas.
:param names: The names of objects to remove.
.. seealso::
:attr:`canvas` for usage examples.
"""
for name in names:
self._canvas.pop(name)
@ -186,8 +203,12 @@ class BaseSlide:
@property
def mobjects_without_canvas(self) -> Sequence[Mobject]:
"""
Return the list of objects contained in the scene, minus those present in
Return the list of Mobjects contained in the scene, minus those present in
the canvas.
.. seealso::
:attr:`canvas` for usage examples.
"""
return [
mobject
@ -275,7 +296,7 @@ class BaseSlide:
next slide is played. By default, this is the right arrow key.
:param args:
Positional arguments to be passed to
Positional arguments passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
or ignored if `manimlib` API is used.
:param loop:
@ -284,27 +305,38 @@ class BaseSlide:
If set, next slide will play immediately play the next slide
upon terminating.
Note that this is only supported by ``manim-slides present``
and ``manim-slides convert --to=html``.
.. warning::
Only supported by ``manim-slides present``
and ``manim-slides convert --to=html``.
:param playback_rate:
Playback rate at which the video is played.
Note that this is only supported by ``manim-slides present``.
.. warning::
Only supported by ``manim-slides present``.
:param reversed_playback_rate:
Playback rate at which the reversed video is played.
Note that this is only supported by ``manim-slides present``.
.. warning::
Only supported by ``manim-slides present``.
:param notes:
Presenter notes, in Markdown format.
Note that PowerPoint does not support Markdown.
.. note::
PowerPoint does not support Markdown formatting,
so the text will be displayed as is.
Note that this is only supported by ``manim-slides present``
and ``manim-slides convert --to=html/pptx``.
.. warning::
Only supported by ``manim-slides present``,
``manim-slides convert --to=html`` and
``manim-slides convert --to=pptx``.
:param dedent_notes:
If set, apply :func:`textwrap.dedent` to notes.
:param kwargs:
Keyword arguments to be passed to
Keyword arguments passed to
:meth:`Scene.next_section<manim.scene.scene.Scene.next_section>`,
or ignored if `manimlib` API is used.
@ -445,11 +477,17 @@ class BaseSlide:
)
)
def _save_slides(self, use_cache: bool = True) -> None:
def _save_slides(
self,
use_cache: bool = True,
flush_cache: bool = False,
skip_reversing: bool = False,
) -> None:
"""
Save slides, optionally using cached files.
Note that cached files only work with Manim.
.. warning:
Caching files only work with Manim.
"""
self._add_last_slide()
@ -458,6 +496,9 @@ class BaseSlide:
scene_name = str(self)
scene_files_folder = files_folder / scene_name
if flush_cache and scene_files_folder.exists():
shutil.rmtree(scene_files_folder)
scene_files_folder.mkdir(parents=True, exist_ok=True)
files: list[Path] = self._partial_movie_files
@ -492,7 +533,10 @@ class BaseSlide:
# We only reverse video if it was not present
if not use_cache or not rev_file.exists():
reverse_video_file(dst_file, rev_file)
if skip_reversing:
rev_file = dst_file
else:
reverse_video_file(dst_file, rev_file)
slides.append(
SlideConfig.from_pre_slide_config_and_files(

View File

@ -2,6 +2,8 @@ from pathlib import Path
from typing import Any, Optional
from manim import Scene, ThreeDScene, config
from manim.renderer.opengl_renderer import OpenGLRenderer
from manim.utils.color import rgba_to_color
from ..config import BaseSlideConfig
from .base import BaseSlide
@ -11,27 +13,58 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
"""
Inherits from :class:`Scene<manim.scene.scene.Scene>` and provide necessary tools
for slides rendering.
:param args: Positional arguments passed to scene object.
:param output_folder: Where the slide animation files should be written.
:param kwargs: Keyword arguments passed to scene object.
:cvar bool disable_caching: :data:`False`: Whether to disable the use of
cached animation files.
:cvar bool flush_cache: :data:`False`: Whether to flush the cache.
Unlike with Manim, flushing is performed before rendering.
:cvar bool skip_reversing: :data:`False`: Whether to generate reversed animations.
If set to :data:`False`, and no cached reversed animation
exists (or caching is disabled) for a given slide,
then the reversed animation will be simply the same
as the original one, i.e., ``rev_file = file``,
for the current slide config.
"""
@property
def _frame_shape(self) -> tuple[float, float]:
if isinstance(self.renderer, OpenGLRenderer):
return self.renderer.camera.frame_shape # type: ignore
else:
return (
self.renderer.camera.frame_height,
self.renderer.camera.frame_width,
)
@property
def _frame_height(self) -> float:
return config["frame_height"] # type: ignore
return self._frame_shape[0]
@property
def _frame_width(self) -> float:
return config["frame_width"] # type: ignore
return self._frame_shape[1]
@property
def _background_color(self) -> str:
color = self.camera.background_color
if hex_color := getattr(color, "hex", None):
return hex_color # type: ignore
else: # manim>=0.18, see https://github.com/ManimCommunity/manim/pull/3020
return color.to_hex() # type: ignore
if isinstance(self.renderer, OpenGLRenderer):
return rgba_to_color(self.renderer.background_color).to_hex() # type: ignore
else:
return self.renderer.camera.background_color.to_hex() # type: ignore
@property
def _resolution(self) -> tuple[int, int]:
return config["pixel_width"], config["pixel_height"]
if isinstance(self.renderer, OpenGLRenderer):
return self.renderer.get_pixel_shape() # type: ignore
else:
return (
self.renderer.camera.pixel_width,
self.renderer.camera.pixel_height,
)
@property
def _partial_movie_files(self) -> list[Path]:
@ -85,16 +118,29 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
)
def render(self, *args: Any, **kwargs: Any) -> None:
"""MANIM render."""
"""MANIM renderer."""
# We need to disable the caching limit since we rely on intermediate files
max_files_cached = config["max_files_cached"]
config["max_files_cached"] = float("inf")
flush_manim_cache = config["flush_cache"]
if flush_manim_cache:
# We need to postpone flushing *after* we saved slides
config["flush_cache"] = False
super().render(*args, **kwargs)
config["max_files_cached"] = max_files_cached
self._save_slides()
self._save_slides(
use_cache=not (config["disable_caching"] or self.disable_caching),
flush_cache=(config["flush_cache"] or self.flush_cache),
skip_reversing=self.skip_reversing,
)
if flush_manim_cache:
self.renderer.file_writer.flush_cache_directory()
class ThreeDSlide(Slide, ThreeDScene): # type: ignore[misc]

View File

@ -10,29 +10,39 @@ from .base import BaseSlide
class Slide(BaseSlide, Scene): # type: ignore[misc]
def __init__(self, *args: Any, **kwargs: Any) -> None:
kwargs.setdefault("file_writer_config", {}).update(
skip_animations=True,
break_into_partial_movies=True,
write_to_movie=True,
)
# See: https://github.com/3b1b/manim/issues/2261
if kwargs["file_writer_config"].setdefault("output_directory", ".") == "":
kwargs["file_writer_config"]["output_directory"] = "."
kwargs["preview"] = False # Avoid opening a preview window
super().__init__(*args, **kwargs)
@property
def _frame_height(self) -> float:
return self.camera.frame.get_height() # type: ignore
return float(self.camera.get_frame_height())
@property
def _frame_width(self) -> float:
return self.camera.frame.get_width() # type: ignore
return float(self.camera.get_frame_width())
@property
def _background_color(self) -> str:
return self.camera_config["background_color"].hex # type: ignore
rgba = self.camera.background_rgba
r = int(255 * rgba[0])
g = int(255 * rgba[1])
b = int(255 * rgba[2])
if rgba[3] == 1.0:
return f"#{r:02x}{g:02x}{b:02x}"
a = int(255 * rgba[3])
return f"#{r:02x}{g:02x}{b:02x}{a:02x}"
@property
def _resolution(self) -> tuple[int, int]:
return self.camera_config["pixel_width"], self.camera_config["pixel_height"]
return self.camera.get_pixel_width(), self.camera.get_pixel_height()
@property
def _partial_movie_files(self) -> list[Path]:
@ -62,7 +72,11 @@ class Slide(BaseSlide, Scene): # type: ignore[misc]
def run(self, *args: Any, **kwargs: Any) -> None:
"""MANIMGL renderer."""
super().run(*args, **kwargs)
self._save_slides(use_cache=False)
self._save_slides(
use_cache=False,
flush_cache=self.flush_cache,
skip_reversing=self.skip_reversing,
)
class ThreeDSlide(Slide):

View File

@ -19,31 +19,32 @@
<body>
<div class="reveal">
<div class="slides">
{%- for presentation_config in presentation_configs -%}
{% for presentation_config in presentation_configs -%}
{% set outer_loop = loop %}
{%- for slide_config in presentation_config.slides -%}
{%- if data_uri -%}
{% set file = file_to_data_uri(slide_config.file) %}
{%- else -%}
{% set file = assets_dir / slide_config.file.name %}
{% set file = assets_dir / (prefix(outer_loop.index0) + slide_config.file.name) %}
{%- endif -%}
<section
data-background-size={{ background_size }}
data-background-color="{{ presentation_config.background_color }}"
data-background-video="{{ file }}"
{% if loop.index == 1 and outer_loop.index == 1 -%}
data-background-video-muted
{%- endif -%}
{% if slide_config.loop -%}
data-background-video-loop
{%- endif -%}
{% if slide_config.auto_next -%}
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
{%- endif -%}>
{% if slide_config.notes != "" -%}
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
{%- endif %}
</section>
<section
data-background-size={{ background_size }}
data-background-color="{{ presentation_config.background_color }}"
data-background-video="{{ file }}"
{% if loop.index == 1 and outer_loop.index == 1 -%}
data-background-video-muted
{%- endif -%}
{% if slide_config.loop -%}
data-background-video-loop
{%- endif -%}
{% if slide_config.auto_next -%}
data-autoslide="{{ get_duration_ms(slide_config.file) }}"
{%- endif %}
>
{%- if slide_config.notes != "" -%}
<aside class="notes" data-markdown>{{ slide_config.notes }}</aside>
{%- endif %}
</section>
{%- endfor -%}
{%- endfor -%}
</div>

View File

@ -19,7 +19,7 @@ def concatenate_video_files(files: list[Path], dest: Path) -> None:
if len(container.streams.video) > 0:
yield file
else:
logger.warn(
logger.warning(
f"Skipping video file {file} because it does "
"not contain any video stream. "
"This is probably caused by Manim, see: "

View File

@ -63,7 +63,7 @@ class Wizard(QWidget): # type: ignore
self.layout = QGridLayout()
for i, (key, value) in enumerate(self.config.keys.dict().items()):
for i, (key, value) in enumerate(self.config.keys.model_dump().items()):
# Create label for key name information
label = QLabel()
key_info = value["name"] or key
@ -97,7 +97,7 @@ class Wizard(QWidget): # type: ignore
def save_config(self) -> None:
try:
Config.model_validate(self.config.dict())
Config.model_validate(self.config.model_dump())
except ValueError:
msg = QMessageBox()
msg.setIcon(QMessageBox.Critical)

View File

@ -1,6 +1,6 @@
[build-system]
build-backend = "hatchling.build"
requires = ["hatchling"]
requires = ["hatchling", "hatch-fancy-pypi-readme"]
[project]
authors = [{name = "Jérome Eertmans", email = "jeertmans@icloud.com"}]
@ -17,7 +17,8 @@ classifiers = [
"Topic :: Scientific/Engineering",
]
dependencies = [
"av>=9.0.0",
"av>=9.0.0,<14",
"beautifulsoup4>=4.12.3",
"click>=8.1.3",
"click-default-group>=1.2.2",
"jinja2>=3.1.2",
@ -30,20 +31,19 @@ dependencies = [
"qtpy>=2.4.1",
"requests>=2.28.1",
"rich>=13.3.2",
"rtoml>=0.9.0",
"rtoml>=0.11.0",
"tqdm>=4.64.1",
]
description = "Tool for live presentations using manim"
dynamic = ["version"]
dynamic = ["readme", "version"]
keywords = ["manim", "slides", "plugin", "manimgl"]
license = {text = "MIT"}
license = "MIT"
name = "manim-slides"
readme = "README.md"
requires-python = ">=3.9"
[project.optional-dependencies]
docs = [
"manim-slides[magic,sphinx-directive]",
"manim-slides[magic,manim,pyqt6,sphinx-directive]",
"furo>=2023.5.20",
"ipykernel>=6.25.1",
"myst-parser>=2.0.0",
@ -57,17 +57,22 @@ docs = [
full = [
"manim-slides[magic,manim,sphinx-directive]",
]
full-gl = [
"manim-slides[magic,manimgl,sphinx-directive]",
]
magic = ["manim-slides[manim]", "ipython>=8.12.2"]
manim = ["manim>=0.17.3"]
manimgl = ["manimgl>=1.6.1;python_version<'3.12'"]
pyqt6 = ["pyqt6>=6.6.1"]
manim = ["manim>=0.18.0"]
manimgl = ["manimgl>=1.7.1", "pyrr"]
pyqt6 = ["pyqt6>=6.7.0"]
pyqt6-full = ["manim-slides[full,pyqt6]"]
pyside6 = ["pyside6>=6.5.1,<6.5.3;python_version<'3.12'"]
pyside6 = ["pyside6>=6.6.1"]
pyside6-full = ["manim-slides[full,pyside6]"]
sphinx-directive = ["docutils>=0.20.1", "manim-slides[manim]"]
tests = [
"manim-slides[full,manimgl,pyqt6,pyside6,sphinx-directive]",
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
"pytest-missing-modules>=0.1.0",
"pytest-qt>=4.2.0",
]
[project.scripts]
manim-slides = "manim_slides.__main__:cli"
@ -79,11 +84,61 @@ Founding = "https://github.com/sponsors/jeertmans"
Homepage = "https://github.com/jeertmans/manim-slides"
Repository = "https://github.com/jeertmans/manim-slides"
[tool.bumpversion]
allow_dirty = false
commit = true
commit_args = ""
current_version = "5.2.0"
ignore_missing_version = false
message = "chore(deps): bump version from {current_version} to {new_version}"
parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-rc(?P<release>\d+))?'
regex = false
replace = "{new_version}"
search = "{current_version}"
serialize = ["{major}.{minor}.{patch}-rc{release}", "{major}.{minor}.{patch}"]
sign_tags = false
tag = false
tag_message = "Bump version: {current_version} → {new_version}"
tag_name = "v{new_version}"
[[tool.bumpversion.files]]
filename = "manim_slides/__version__.py"
replace = '__version__ = "{new_version}"'
search = '__version__ = "{current_version}"'
[[tool.bumpversion.files]]
filename = "CITATION.cff"
replace = "version: v{new_version}"
search = "version: v{current_version}"
[[tool.bumpversion.files]]
filename = "CHANGELOG.md"
replace = "v{new_version}"
search = "Unreleased"
[[tool.bumpversion.files]]
filename = "CHANGELOG.md"
replace = "v{new_version}"
search = "unreleased"
[[tool.bumpversion.files]]
filename = "CHANGELOG.md"
replace = "v{current_version}...v{new_version}"
search = "v{current_version}...HEAD"
[[tool.bumpversion.files]]
filename = "CHANGELOG.md"
replace = '''<!-- start changelog -->
(unreleased)=
## [Unreleased](https://github.com/jeertmans/manim-slides/compare/v{new_version}...HEAD)'''
search = "<!-- start changelog -->"
[tool.codespell]
builtin = "clear,rare,informal,usage,names,en-GB_to_en-US"
check-hidden = true
ignore-words-list = "master"
skip = "requirements.lock,requirements-dev.lock"
skip = "uv.lock"
[tool.coverage.report]
exclude_lines = [
@ -94,6 +149,25 @@ exclude_lines = [
]
precision = 2
[tool.hatch.metadata.hooks.fancy-pypi-readme]
content-type = "text/markdown"
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
text = """<p align="center">
<a href="https://www.github.com/jeertmans/manin-slides">
<img src="https://raw.githubusercontent.com/jeertmans/manim-slides/main/static/logo.png"/>
</a>
</p>
"""
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
path = "README.md"
start-after = "<!-- start pypi -->"
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
pattern = '> \[!([A-Z]+)\]'
replacement = '> **\1:**'
[tool.hatch.version]
path = "manim_slides/__version__.py"
@ -104,12 +178,20 @@ python_version = "3.9"
strict = true
[tool.pytest.ini_options]
addopts = [
"--cov-report=xml",
"--cov=manim_slides",
]
env = [
"QT_API=pyside6",
"QT_QPA_PLATFORM=offscreen",
]
filterwarnings = [
"error",
"ignore::DeprecationWarning",
'''ignore:'audioop' is deprecated:DeprecationWarning''',
'ignore:pkg_resources is deprecated as an API:DeprecationWarning',
'ignore::DeprecationWarning:pkg_resources.*:',
'ignore:invalid escape sequence.*:DeprecationWarning',
'ignore:invalid escape sequence.*:SyntaxWarning',
]
[tool.ruff]
@ -136,15 +218,13 @@ extend-ignore = [
extend-select = ["B", "C90", "D", "I", "N", "RUF", "UP", "T"]
isort = {known-first-party = ["manim_slides", "tests"]}
[tool.rye]
[tool.ruff.lint.per-file-ignores]
"docs/source/reference/magic_example.ipynb" = ["F403", "F405"]
"tests/test_slide.py" = ["N801"]
[tool.uv]
dev-dependencies = [
"bump2version>=1.0.1",
"manim-slides[manim,manimgl,pyqt6]",
"bump-my-version>=0.20.3",
"pre-commit>=3.5.0",
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-env>=0.8.2",
"pytest-qt>=4.2.0",
"pytest-xdist>=3.3.1",
"setuptools>=73.0.1",
]
managed = true

View File

@ -1,419 +0,0 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: true
# with-sources: false
-e file:.
alabaster==0.7.16
# via sphinx
annotated-types==0.6.0
# via pydantic
asttokens==2.4.1
# via stack-data
attrs==23.2.0
# via jsonschema
# via referencing
av==12.0.0
# via manim-slides
babel==2.14.0
# via sphinx
beautifulsoup4==4.12.3
# via furo
# via nbconvert
bleach==6.1.0
# via nbconvert
bump2version==1.0.1
certifi==2024.2.2
# via requests
cfgv==3.4.0
# via pre-commit
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via click-default-group
# via cloup
# via manim
# via manim-slides
# via sphinx-click
click-default-group==1.2.4
# via manim
# via manim-slides
cloup==0.13.1
# via manim
colour==0.1.5
# via manim
# via manimgl
comm==0.2.2
# via ipykernel
contourpy==1.2.1
# via matplotlib
coverage==7.4.4
# via pytest-cov
cycler==0.12.1
# via matplotlib
debugpy==1.8.1
# via ipykernel
decorator==5.1.1
# via ipython
# via manim
defusedxml==0.7.1
# via nbconvert
distlib==0.3.8
# via virtualenv
docutils==0.20.1
# via manim-slides
# via myst-parser
# via nbsphinx
# via sphinx
# via sphinx-click
execnet==2.1.1
# via pytest-xdist
executing==2.0.1
# via stack-data
fastjsonschema==2.19.1
# via nbformat
filelock==3.13.4
# via virtualenv
fonttools==4.51.0
# via matplotlib
furo==2024.1.29
# via manim-slides
glcontext==2.5.0
# via moderngl
identify==2.5.35
# via pre-commit
idna==3.7
# via requests
imagesize==1.4.1
# via sphinx
iniconfig==2.0.0
# via pytest
ipykernel==6.29.4
# via manim-slides
ipython==8.18.1
# via ipykernel
# via manim-slides
# via manimgl
isosurfaces==0.1.0
# via manim
# via manimgl
jedi==0.19.1
# via ipython
jinja2==3.1.3
# via manim-slides
# via myst-parser
# via nbconvert
# via nbsphinx
# via sphinx
jsonschema==4.21.1
# via nbformat
jsonschema-specifications==2023.12.1
# via jsonschema
jupyter-client==8.6.1
# via ipykernel
# via nbclient
jupyter-core==5.7.2
# via ipykernel
# via jupyter-client
# via nbclient
# via nbconvert
# via nbformat
jupyterlab-pygments==0.3.0
# via nbconvert
kiwisolver==1.4.5
# via matplotlib
lxml==5.2.1
# via manim-slides
# via python-pptx
manim==0.17.3
# via manim-slides
manimgl==1.6.1
# via manim-slides
manimpango==0.4.4
# via manim
# via manimgl
mapbox-earcut==1.0.1
# via manim
# via manimgl
markdown-it-py==3.0.0
# via mdit-py-plugins
# via myst-parser
# via rich
markupsafe==2.1.5
# via jinja2
# via nbconvert
matplotlib==3.8.4
# via manimgl
matplotlib-inline==0.1.7
# via ipykernel
# via ipython
mdit-py-plugins==0.4.0
# via myst-parser
mdurl==0.1.2
# via markdown-it-py
mistune==3.0.2
# via nbconvert
moderngl==5.10.0
# via manim
# via manimgl
# via moderngl-window
moderngl-window==2.4.4
# via manim
# via manimgl
mpmath==1.3.0
# via sympy
multipledispatch==1.0.0
# via pyrr
myst-parser==2.0.0
# via manim-slides
nbclient==0.10.0
# via nbconvert
nbconvert==7.16.3
# via nbsphinx
nbformat==5.10.4
# via nbclient
# via nbconvert
# via nbsphinx
nbsphinx==0.9.3
# via manim-slides
nest-asyncio==1.6.0
# via ipykernel
networkx==2.8.8
# via manim
nodeenv==1.8.0
# via pre-commit
numpy==1.26.4
# via contourpy
# via isosurfaces
# via manim
# via manim-slides
# via manimgl
# via mapbox-earcut
# via matplotlib
# via moderngl-window
# via pyrr
# via scipy
packaging==24.0
# via ipykernel
# via matplotlib
# via nbconvert
# via pytest
# via qtpy
# via sphinx
pandoc==2.3
# via manim-slides
pandocfilters==1.5.1
# via nbconvert
parso==0.8.4
# via jedi
pexpect==4.9.0
# via ipython
pillow==9.5.0
# via manim
# via manim-slides
# via manimgl
# via matplotlib
# via moderngl-window
# via python-pptx
platformdirs==4.2.0
# via jupyter-core
# via virtualenv
pluggy==1.4.0
# via pytest
# via pytest-qt
plumbum==1.8.2
# via pandoc
ply==3.11
# via pandoc
pre-commit==3.7.0
prompt-toolkit==3.0.43
# via ipython
psutil==5.9.8
# via ipykernel
ptyprocess==0.7.0
# via pexpect
pure-eval==0.2.2
# via stack-data
pycairo==1.26.0
# via manim
pydantic==2.7.0
# via manim-slides
# via pydantic-extra-types
pydantic-core==2.18.1
# via pydantic
pydantic-extra-types==2.6.0
# via manim-slides
pydub==0.25.1
# via manim
# via manimgl
pyglet==2.0.15
# via moderngl-window
pygments==2.17.2
# via furo
# via ipython
# via manim
# via manimgl
# via nbconvert
# via rich
# via sphinx
pyopengl==3.1.7
# via manimgl
pyparsing==3.1.2
# via matplotlib
pyqt6==6.6.1
# via manim-slides
pyqt6-qt6==6.6.3
# via pyqt6
pyqt6-sip==13.6.0
# via pyqt6
pyrr==0.10.3
# via moderngl-window
pyside6==6.5.2
# via manim-slides
pyside6-addons==6.5.2
# via pyside6
pyside6-essentials==6.5.2
# via pyside6
# via pyside6-addons
pytest==8.1.1
# via pytest-cov
# via pytest-env
# via pytest-qt
# via pytest-xdist
pytest-cov==5.0.0
pytest-env==1.1.3
pytest-qt==4.4.0
pytest-xdist==3.5.0
python-dateutil==2.9.0.post0
# via jupyter-client
# via matplotlib
python-pptx==0.6.23
# via manim-slides
pyyaml==6.0.1
# via manimgl
# via myst-parser
# via pre-commit
pyzmq==26.0.0
# via ipykernel
# via jupyter-client
qtpy==2.4.1
# via manim-slides
referencing==0.34.0
# via jsonschema
# via jsonschema-specifications
requests==2.31.0
# via manim
# via manim-slides
# via sphinx
rich==13.7.1
# via manim
# via manim-slides
# via manimgl
rpds-py==0.18.0
# via jsonschema
# via referencing
rtoml==0.10.0
# via manim-slides
scipy==1.13.0
# via manim
# via manimgl
screeninfo==0.8.1
# via manim
# via manimgl
setuptools==69.5.1
# via nodeenv
shiboken6==6.5.2
# via pyside6
# via pyside6-addons
# via pyside6-essentials
six==1.16.0
# via asttokens
# via bleach
# via python-dateutil
skia-pathops==0.7.4
# via manim
# via manimgl
snowballstemmer==2.2.0
# via sphinx
soupsieve==2.5
# via beautifulsoup4
sphinx==7.3.6
# via furo
# via manim-slides
# via myst-parser
# via nbsphinx
# via sphinx-basic-ng
# via sphinx-click
# via sphinx-copybutton
# via sphinxext-opengraph
sphinx-basic-ng==1.0.0b2
# via furo
sphinx-click==5.1.0
# via manim-slides
sphinx-copybutton==0.5.2
# via manim-slides
sphinxcontrib-applehelp==1.0.8
# via sphinx
sphinxcontrib-devhelp==1.0.6
# via sphinx
sphinxcontrib-htmlhelp==2.0.5
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-qthelp==1.0.7
# via sphinx
sphinxcontrib-serializinghtml==1.1.10
# via sphinx
sphinxext-opengraph==0.9.1
# via manim-slides
srt==3.5.3
# via manim
stack-data==0.6.3
# via ipython
svgelements==1.9.6
# via manim
# via manimgl
sympy==1.12
# via manimgl
tinycss2==1.2.1
# via nbconvert
tornado==6.4
# via ipykernel
# via jupyter-client
tqdm==4.66.2
# via manim
# via manim-slides
# via manimgl
traitlets==5.14.2
# via comm
# via ipykernel
# via ipython
# via jupyter-client
# via jupyter-core
# via matplotlib-inline
# via nbclient
# via nbconvert
# via nbformat
# via nbsphinx
typing-extensions==4.11.0
# via pydantic
# via pydantic-core
urllib3==2.2.1
# via requests
validators==0.28.0
# via manimgl
virtualenv==20.25.3
# via pre-commit
watchdog==2.3.1
# via manim
wcwidth==0.2.13
# via prompt-toolkit
webencodings==0.5.1
# via bleach
# via tinycss2
xlsxwriter==3.2.0
# via python-pptx

View File

@ -1,382 +0,0 @@
# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: false
# features: []
# all-features: true
# with-sources: false
-e file:.
alabaster==0.7.16
# via sphinx
annotated-types==0.6.0
# via pydantic
asttokens==2.4.1
# via stack-data
attrs==23.2.0
# via jsonschema
# via referencing
av==12.0.0
# via manim-slides
babel==2.14.0
# via sphinx
beautifulsoup4==4.12.3
# via furo
# via nbconvert
bleach==6.1.0
# via nbconvert
certifi==2024.2.2
# via requests
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via click-default-group
# via cloup
# via manim
# via manim-slides
# via sphinx-click
click-default-group==1.2.4
# via manim
# via manim-slides
cloup==0.13.1
# via manim
colour==0.1.5
# via manim
# via manimgl
comm==0.2.2
# via ipykernel
contourpy==1.2.1
# via matplotlib
cycler==0.12.1
# via matplotlib
debugpy==1.8.1
# via ipykernel
decorator==5.1.1
# via ipython
# via manim
defusedxml==0.7.1
# via nbconvert
docutils==0.20.1
# via manim-slides
# via myst-parser
# via nbsphinx
# via sphinx
# via sphinx-click
executing==2.0.1
# via stack-data
fastjsonschema==2.19.1
# via nbformat
fonttools==4.51.0
# via matplotlib
furo==2024.1.29
# via manim-slides
glcontext==2.5.0
# via moderngl
idna==3.7
# via requests
imagesize==1.4.1
# via sphinx
ipykernel==6.29.4
# via manim-slides
ipython==8.18.1
# via ipykernel
# via manim-slides
# via manimgl
isosurfaces==0.1.0
# via manim
# via manimgl
jedi==0.19.1
# via ipython
jinja2==3.1.3
# via manim-slides
# via myst-parser
# via nbconvert
# via nbsphinx
# via sphinx
jsonschema==4.21.1
# via nbformat
jsonschema-specifications==2023.12.1
# via jsonschema
jupyter-client==8.6.1
# via ipykernel
# via nbclient
jupyter-core==5.7.2
# via ipykernel
# via jupyter-client
# via nbclient
# via nbconvert
# via nbformat
jupyterlab-pygments==0.3.0
# via nbconvert
kiwisolver==1.4.5
# via matplotlib
lxml==5.2.1
# via manim-slides
# via python-pptx
manim==0.17.3
# via manim-slides
manimgl==1.6.1
# via manim-slides
manimpango==0.4.4
# via manim
# via manimgl
mapbox-earcut==1.0.1
# via manim
# via manimgl
markdown-it-py==3.0.0
# via mdit-py-plugins
# via myst-parser
# via rich
markupsafe==2.1.5
# via jinja2
# via nbconvert
matplotlib==3.8.4
# via manimgl
matplotlib-inline==0.1.7
# via ipykernel
# via ipython
mdit-py-plugins==0.4.0
# via myst-parser
mdurl==0.1.2
# via markdown-it-py
mistune==3.0.2
# via nbconvert
moderngl==5.10.0
# via manim
# via manimgl
# via moderngl-window
moderngl-window==2.4.4
# via manim
# via manimgl
mpmath==1.3.0
# via sympy
multipledispatch==1.0.0
# via pyrr
myst-parser==2.0.0
# via manim-slides
nbclient==0.10.0
# via nbconvert
nbconvert==7.16.3
# via nbsphinx
nbformat==5.10.4
# via nbclient
# via nbconvert
# via nbsphinx
nbsphinx==0.9.3
# via manim-slides
nest-asyncio==1.6.0
# via ipykernel
networkx==2.8.8
# via manim
numpy==1.26.4
# via contourpy
# via isosurfaces
# via manim
# via manim-slides
# via manimgl
# via mapbox-earcut
# via matplotlib
# via moderngl-window
# via pyrr
# via scipy
packaging==24.0
# via ipykernel
# via matplotlib
# via nbconvert
# via qtpy
# via sphinx
pandoc==2.3
# via manim-slides
pandocfilters==1.5.1
# via nbconvert
parso==0.8.4
# via jedi
pexpect==4.9.0
# via ipython
pillow==9.5.0
# via manim
# via manim-slides
# via manimgl
# via matplotlib
# via moderngl-window
# via python-pptx
platformdirs==4.2.0
# via jupyter-core
plumbum==1.8.2
# via pandoc
ply==3.11
# via pandoc
prompt-toolkit==3.0.43
# via ipython
psutil==5.9.8
# via ipykernel
ptyprocess==0.7.0
# via pexpect
pure-eval==0.2.2
# via stack-data
pycairo==1.26.0
# via manim
pydantic==2.7.0
# via manim-slides
# via pydantic-extra-types
pydantic-core==2.18.1
# via pydantic
pydantic-extra-types==2.6.0
# via manim-slides
pydub==0.25.1
# via manim
# via manimgl
pyglet==2.0.15
# via moderngl-window
pygments==2.17.2
# via furo
# via ipython
# via manim
# via manimgl
# via nbconvert
# via rich
# via sphinx
pyopengl==3.1.7
# via manimgl
pyparsing==3.1.2
# via matplotlib
pyqt6==6.6.1
# via manim-slides
pyqt6-qt6==6.6.3
# via pyqt6
pyqt6-sip==13.6.0
# via pyqt6
pyrr==0.10.3
# via moderngl-window
pyside6==6.5.2
# via manim-slides
pyside6-addons==6.5.2
# via pyside6
pyside6-essentials==6.5.2
# via pyside6
# via pyside6-addons
python-dateutil==2.9.0.post0
# via jupyter-client
# via matplotlib
python-pptx==0.6.23
# via manim-slides
pyyaml==6.0.1
# via manimgl
# via myst-parser
pyzmq==26.0.0
# via ipykernel
# via jupyter-client
qtpy==2.4.1
# via manim-slides
referencing==0.34.0
# via jsonschema
# via jsonschema-specifications
requests==2.31.0
# via manim
# via manim-slides
# via sphinx
rich==13.7.1
# via manim
# via manim-slides
# via manimgl
rpds-py==0.18.0
# via jsonschema
# via referencing
rtoml==0.10.0
# via manim-slides
scipy==1.13.0
# via manim
# via manimgl
screeninfo==0.8.1
# via manim
# via manimgl
shiboken6==6.5.2
# via pyside6
# via pyside6-addons
# via pyside6-essentials
six==1.16.0
# via asttokens
# via bleach
# via python-dateutil
skia-pathops==0.7.4
# via manim
# via manimgl
snowballstemmer==2.2.0
# via sphinx
soupsieve==2.5
# via beautifulsoup4
sphinx==7.3.6
# via furo
# via manim-slides
# via myst-parser
# via nbsphinx
# via sphinx-basic-ng
# via sphinx-click
# via sphinx-copybutton
# via sphinxext-opengraph
sphinx-basic-ng==1.0.0b2
# via furo
sphinx-click==5.1.0
# via manim-slides
sphinx-copybutton==0.5.2
# via manim-slides
sphinxcontrib-applehelp==1.0.8
# via sphinx
sphinxcontrib-devhelp==1.0.6
# via sphinx
sphinxcontrib-htmlhelp==2.0.5
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-qthelp==1.0.7
# via sphinx
sphinxcontrib-serializinghtml==1.1.10
# via sphinx
sphinxext-opengraph==0.9.1
# via manim-slides
srt==3.5.3
# via manim
stack-data==0.6.3
# via ipython
svgelements==1.9.6
# via manim
# via manimgl
sympy==1.12
# via manimgl
tinycss2==1.2.1
# via nbconvert
tornado==6.4
# via ipykernel
# via jupyter-client
tqdm==4.66.2
# via manim
# via manim-slides
# via manimgl
traitlets==5.14.2
# via comm
# via ipykernel
# via ipython
# via jupyter-client
# via jupyter-core
# via matplotlib-inline
# via nbclient
# via nbconvert
# via nbformat
# via nbsphinx
typing-extensions==4.11.0
# via pydantic
# via pydantic-core
urllib3==2.2.1
# via requests
validators==0.28.0
# via manimgl
watchdog==2.3.1
# via manim
wcwidth==0.2.13
# via prompt-toolkit
webencodings==0.5.1
# via bleach
# via tinycss2
xlsxwriter==3.2.0
# via python-pptx

View File

@ -1,5 +1,5 @@
#! /bin/bash
rye run manim-slides render -t -qk -s --format png --resolution 64,64 static/logo.py ManimSlidesFavicon && mv media/images/logo/*.png static/favicon.png
uv run manim-slides render -t -qk -s --format png --resolution 64,64 static/logo.py ManimSlidesFavicon && mv media/images/logo/*.png static/favicon.png
ln -f -r -s static/favicon.png docs/source/_static/favicon.png

View File

@ -1,21 +1,21 @@
#! /bin/bash
MANIM_SLIDES_THEME=light rye run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo.png
MANIM_SLIDES_THEME=light uv run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo.png
ln -f -r -s static/logo.png docs/source/_static/logo.png
MANIM_SLIDES_THEME=dark_docs rye run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_dark_docs.png
MANIM_SLIDES_THEME=dark_docs uv run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_dark_docs.png
ln -f -r -s static/logo_dark_docs.png docs/source/_static/logo_dark_docs.png
MANIM_SLIDES_THEME=dark_github rye run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_dark_github.png
MANIM_SLIDES_THEME=dark_github uv run manim-slides render -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_dark_github.png
ln -f -r -s static/logo_dark_github.png docs/source/_static/logo_dark_github.png
MANIM_SLIDES_THEME=light rye run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_light_transparent.png
MANIM_SLIDES_THEME=light uv run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_light_transparent.png
ln -f -r -s static/logo_light_transparent.png docs/source/_static/logo_light_transparent.png
MANIM_SLIDES_THEME=dark_docs rye run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_dark_transparent.png
MANIM_SLIDES_THEME=dark_docs uv run manim-slides render -t -qk -s --format png --resolution 2560,1280 static/logo.py ManimSlidesLogo && mv media/images/logo/*.png static/logo_dark_transparent.png
ln -f -r -s static/logo_dark_transparent.png docs/source/_static/logo_dark_transparent.png

View File

@ -71,8 +71,20 @@ def paths() -> Generator[list[Path], None, None]:
yield [random_path() for _ in range(20)]
@pytest.fixture(scope="session")
@pytest.fixture
def presentation_config(
slides_folder: Path,
) -> Generator[PresentationConfig, None, None]:
yield PresentationConfig.from_file(slides_folder / "BasicSlide.json")
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
"""Make sure missing modules run at the very end."""
def uses_missing_modules_fixtures(item: pytest.Item) -> int:
if "missing_modules" in getattr(item, "fixturenames", []):
return 1
return 0
items.sort(key=uses_missing_modules_fixtures)

View File

@ -38,3 +38,7 @@ class BasicSlide(Slide):
self.next_slide()
self.zoom(other_text, [])
class BasicSlideSkipReversing(BasicSlide):
skip_reversing = True

73
tests/test_checkhealth.py Normal file
View File

@ -0,0 +1,73 @@
import importlib.util
import sys
from itertools import chain, combinations
import pytest
from click.testing import CliRunner
from pytest_missing_modules.plugin import MissingModulesContextGenerator
from manim_slides.__version__ import __version__
from manim_slides.checkhealth import checkhealth
MANIM_NOT_INSTALLED = importlib.util.find_spec("manim") is None
MANIMGL_NOT_INSTALLED = importlib.util.find_spec("manimlib") is None
PYQT6_NOT_INSTALLED = importlib.util.find_spec("PyQt6") is None
PYSIDE6_NOT_INSTALLED = importlib.util.find_spec("PySide6") is None
@pytest.mark.filterwarnings("ignore:Selected binding 'pyqt6' could not be found")
@pytest.mark.parametrize(
"names",
list(
chain.from_iterable(
combinations(("manim", "manimlib", "PyQt6", "PySide6"), r=r)
for r in range(0, 5)
)
),
)
def test_checkhealth(
names: tuple[str, ...], missing_modules: MissingModulesContextGenerator
) -> None:
runner = CliRunner()
manim_missing = "manim" in names or MANIM_NOT_INSTALLED
manimlib_missing = "manimlib" in names or MANIMGL_NOT_INSTALLED
pyqt6_missing = "PyQt6" in names or PYQT6_NOT_INSTALLED
pyside6_missing = "PySide6" in names or PYSIDE6_NOT_INSTALLED
if "qtpy" in sys.modules:
del sys.modules["qtpy"] # Avoid using cached module
with missing_modules(*names):
if (
not manimlib_missing
and not MANIMGL_NOT_INSTALLED
and sys.version_info < (3, 10)
):
pytest.skip("See https://github.com/3b1b/manim/issues/2263")
result = runner.invoke(
checkhealth,
env={"QT_API": "pyqt6", "FORCE_QT_API": "1"},
)
assert result.exit_code == 0
assert f"Manim Slides version: {__version__}" in result.output
assert sys.executable in result.output
if manim_missing:
assert "manim not found" in result.output
else:
assert "manim (version:" in result.output
if manimlib_missing:
assert "manimgl not found" in result.output
else:
assert "manimgl (version:" in result.output
if pyqt6_missing and pyside6_missing:
assert "No Qt API found" in result.output
elif pyqt6_missing:
assert "Qt API: pyside6 (version:" in result.output
else:
assert "Qt API: pyqt6 (version:" in result.output

View File

@ -1,3 +1,4 @@
import shutil
from enum import EnumMeta
from pathlib import Path
@ -17,6 +18,7 @@ from manim_slides.convert import (
ControlsLayout,
Converter,
Display,
HtmlZip,
JsBool,
JsFalse,
JsNull,
@ -138,7 +140,8 @@ def test_unquoted_enum(enum_type: EnumMeta) -> None:
class TestConverter:
@pytest.mark.parametrize(
("name", "converter"), [("html", RevealJS), ("pdf", PDF), ("pptx", PowerPoint)]
("name", "converter"),
[("html", RevealJS), ("pdf", PDF), ("pptx", PowerPoint), ("zip", HtmlZip)],
)
def test_from_string(self, name: str, converter: type) -> None:
assert Converter.from_string(name) == converter
@ -150,9 +153,69 @@ class TestConverter:
RevealJS(presentation_configs=[presentation_config]).convert_to(out_file)
assert out_file.exists()
assert Path(tmp_path / "slides_assets").is_dir()
file_contents = Path(out_file).read_text()
file_contents = out_file.read_text()
assert "manim" in file_contents.casefold()
def test_revealjs_offline_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:
out_file = tmp_path / "slides.html"
RevealJS(presentation_configs=[presentation_config], offline="true").convert_to(
out_file
)
assert out_file.exists()
assets_dir = Path(tmp_path / "slides_assets")
assert assets_dir.is_dir()
for file in [
"black.min.css",
"reveal.min.css",
"reveal.min.js",
"zenburn.min.css",
]:
assert (assets_dir / file).exists()
def test_htmlzip_converter(
self, tmp_path: Path, presentation_config: PresentationConfig
) -> None:
archive = tmp_path / "got.zip"
expected = tmp_path / "expected.html"
got = tmp_path / "got.html"
HtmlZip(presentation_configs=[presentation_config]).convert_to(archive)
RevealJS(presentation_configs=[presentation_config]).convert_to(expected)
shutil.unpack_archive(str(archive), extract_dir=tmp_path)
assert archive.exists()
assert got.exists()
assert expected.exists()
assert got.read_text() == expected.read_text().replace(
"expected_assets", "got_assets"
)
@pytest.mark.parametrize("num_presentation_configs", (1, 2))
def test_revealjs_multiple_scenes_converter(
self,
tmp_path: Path,
presentation_config: PresentationConfig,
num_presentation_configs: int,
) -> None:
out_file = tmp_path / "slides.html"
RevealJS(
presentation_configs=[
presentation_config for _ in range(num_presentation_configs)
]
).convert_to(out_file)
assert out_file.exists()
assets_dir = Path(tmp_path / "slides_assets")
assert assets_dir.is_dir()
got = sum(1 for _ in assets_dir.iterdir())
expected = num_presentation_configs * len(presentation_config.slides)
assert got == expected
@pytest.mark.parametrize("frame_index", ("first", "last"))
def test_pdf_converter(
self, frame_index: str, tmp_path: Path, presentation_config: PresentationConfig

View File

@ -6,6 +6,11 @@ import pytest
import manim_slides.slide as slide
skip_if_py39 = pytest.mark.skipif(
sys.version_info < (3, 10),
reason="See https://github.com/3b1b/manim/issues/2263",
)
def assert_import(
*,
@ -20,6 +25,7 @@ def assert_import(
assert slide.MANIMGL == manimgl
@skip_if_py39
def test_force_api() -> None:
pytest.importorskip("manimlib")
import manim # noqa: F401
@ -53,6 +59,7 @@ def test_invalid_api() -> None:
del os.environ[slide.MANIM_API]
@skip_if_py39
@pytest.mark.filterwarnings("ignore:assert_import")
def test_manim_and_manimgl_imported() -> None:
pytest.importorskip("manimlib")
@ -79,6 +86,7 @@ def test_manim_imported() -> None:
)
@skip_if_py39
def test_manimgl_imported() -> None:
pytest.importorskip("manimlib")
import manimlib # noqa: F401

View File

@ -2,13 +2,11 @@ import random
import shutil
import sys
from pathlib import Path
from typing import Any, Union
import manim
import numpy as np
import pytest
from click.testing import CliRunner
from manim import (
BLACK,
BLUE,
DOWN,
LEFT,
@ -21,13 +19,39 @@ from manim import (
GrowFromCenter,
Text,
)
from packaging import version
from pydantic import ValidationError
from manim.renderer.opengl_renderer import OpenGLRenderer
from manim_slides.config import PresentationConfig
from manim_slides.defaults import FOLDER_PATH
from manim_slides.render import render
from manim_slides.slide.manim import Slide
from manim_slides.slide.manim import Slide as CESlide
if sys.version_info < (3, 10):
class _GLSlide:
def construct(self) -> None:
pass
def render(self) -> None:
pass
GLSlide = pytest.param(
_GLSlide,
marks=pytest.mark.skip(reason="See https://github.com/3b1b/manim/issues/2263"),
)
else:
from manim_slides.slide.manimlib import Slide as GLSlide
_GLSlide = GLSlide
class CEGLSlide(CESlide):
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, renderer=OpenGLRenderer(), **kwargs)
SlideType = Union[type[CESlide], type[_GLSlide], type[CEGLSlide]]
Slide = Union[CESlide, _GLSlide, CEGLSlide]
@pytest.mark.parametrize(
@ -37,9 +61,8 @@ from manim_slides.slide.manim import Slide
pytest.param(
"--GL",
marks=pytest.mark.skipif(
version.parse(np.__version__) >= version.parse("1.25")
or sys.version_info >= (3, 12),
reason="ManimGL requires numpy<1.25, which is outdated and Python < 3.12",
sys.version_info < (3, 10),
reason="See https://github.com/3b1b/manim/issues/2263.",
),
),
],
@ -83,272 +106,417 @@ def test_render_basic_slide(
assert local_presentation_config.resolution == presentation_config.resolution
def assert_constructs(cls: type) -> type:
class Wrapper:
@classmethod
def test_construct(_) -> None: # noqa: N804
cls().construct()
def test_clear_cache(
slides_file: Path,
) -> None:
runner = CliRunner()
return Wrapper
with runner.isolated_filesystem() as tmp_dir:
local_media_folder = (
Path(tmp_dir)
/ "media"
/ "videos"
/ slides_file.stem
/ "480p15"
/ "partial_movie_files"
/ "BasicSlide"
)
local_slides_folder = Path(tmp_dir) / "slides"
assert not local_media_folder.exists()
assert not local_slides_folder.exists()
results = runner.invoke(render, [str(slides_file), "BasicSlide", "-ql"])
assert results.exit_code == 0, results
assert local_media_folder.is_dir() and list(local_media_folder.iterdir())
assert local_slides_folder.exists()
results = runner.invoke(
render, [str(slides_file), "BasicSlide", "-ql", "--flush_cache"]
)
assert results.exit_code == 0, results
assert local_media_folder.is_dir() and not list(local_media_folder.iterdir())
assert local_slides_folder.exists()
results = runner.invoke(
render, [str(slides_file), "BasicSlide", "-ql", "--disable_caching"]
)
assert results.exit_code == 0, results
assert local_media_folder.is_dir() and list(local_media_folder.iterdir())
assert local_slides_folder.exists()
results = runner.invoke(
render,
[
str(slides_file),
"BasicSlide",
"-ql",
"--disable_caching",
"--flush_cache",
],
)
assert results.exit_code == 0, results
assert local_media_folder.is_dir() and not list(local_media_folder.iterdir())
assert local_slides_folder.exists()
def assert_renders(cls: type) -> type:
class Wrapper:
@classmethod
def test_render(_) -> None: # noqa: N804
cls().render()
@pytest.mark.parametrize(
"renderer",
[
"--CE",
pytest.param(
"--GL",
marks=pytest.mark.skipif(
sys.version_info < (3, 10),
reason="See https://github.com/3b1b/manim/issues/2263.",
),
),
],
)
@pytest.mark.parametrize(
("klass", "skip_reversing"),
[("BasicSlide", False), ("BasicSlideSkipReversing", True)],
)
def test_skip_reversing(
renderer: str,
slides_file: Path,
manimgl_config: Path,
klass: str,
skip_reversing: bool,
) -> None:
runner = CliRunner()
return Wrapper
with runner.isolated_filesystem() as tmp_dir:
shutil.copy(manimgl_config, tmp_dir)
results = runner.invoke(render, [renderer, str(slides_file), klass, "-ql"])
assert results.exit_code == 0, results
local_slides_folder = (Path(tmp_dir) / "slides").resolve(strict=True)
local_config_file = (local_slides_folder / f"{klass}.json").resolve(strict=True)
local_presentation_config = PresentationConfig.from_file(local_config_file)
for slide in local_presentation_config.slides:
if skip_reversing:
assert slide.file == slide.rev_file
else:
assert slide.file != slide.rev_file
def init_slide(cls: SlideType) -> Slide:
if issubclass(cls, CESlide):
return cls()
elif issubclass(cls, GLSlide):
from manimlib.config import get_configuration, parse_cli
from manimlib.extract_scene import get_scene_config
args = parse_cli()
config = get_configuration(args)
scene_config = get_scene_config(config)
return cls(**scene_config)
raise ValueError(f"Unsupported class {cls}")
parametrize_base_cls = pytest.mark.parametrize(
"base_cls", (CESlide, GLSlide, CEGLSlide), ids=("CE", "GL", "CE(GL)")
)
def assert_constructs(cls: SlideType) -> None:
init_slide(cls).construct()
def assert_renders(cls: SlideType) -> None:
init_slide(cls).render()
class TestSlide:
@assert_constructs
class TestDefaultProperties(Slide):
def construct(self) -> None:
assert self._output_folder == FOLDER_PATH
assert len(self._slides) == 0
assert self._current_slide == 1
assert self._start_animation == 0
assert len(self._canvas) == 0
assert self._wait_time_between_slides == 0.0
def test_default_properties(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
assert self._output_folder == FOLDER_PATH
assert len(self._slides) == 0
assert self._current_slide == 1
assert self._start_animation == 0
assert len(self._canvas) == 0
assert self._wait_time_between_slides == 0.0
@pytest.mark.skipif(
version.parse(manim.__version__) < version.parse("0.18"),
reason="Manim change how color are represented in 0.18",
)
@assert_constructs
class TestBackgroundColor(Slide):
def construct(self) -> None:
assert self._background_color == BLACK.to_hex() # DEFAULT
self.camera.background_color = BLUE
assert self._background_color == BLUE.to_hex()
@parametrize_base_cls
def test_frame_height(self, base_cls: SlideType) -> None:
@assert_constructs
class _(base_cls): # type: ignore
def construct(self) -> None:
assert self._frame_height > 0 and isinstance(self._frame_height, float)
@assert_renders
class TestMultipleAnimationsInLastSlide(Slide):
"""Check against solution for issue #161."""
@parametrize_base_cls
def test_frame_width(self, base_cls: SlideType) -> None:
@assert_constructs
class _(base_cls): # type: ignore
def construct(self) -> None:
assert self._frame_width > 0 and isinstance(self._frame_width, float)
def construct(self) -> None:
circle = Circle(color=BLUE)
dot = Dot()
@parametrize_base_cls
def test_resolution(self, base_cls: SlideType) -> None:
@assert_constructs
class _(base_cls): # type: ignore
def construct(self) -> None:
pw, ph = self._resolution
assert isinstance(pw, int) and pw > 0
assert isinstance(ph, int) and ph > 0
self.play(GrowFromCenter(circle))
self.play(FadeIn(dot))
self.next_slide()
@parametrize_base_cls
def test_backround_color(self, base_cls: SlideType) -> None:
@assert_constructs
class _(base_cls): # type: ignore
def construct(self) -> None:
assert self._background_color in ["#000000", "#000"] # DEFAULT
self.play(dot.animate.move_to(RIGHT))
self.play(dot.animate.move_to(UP))
self.play(dot.animate.move_to(LEFT))
self.play(dot.animate.move_to(DOWN))
def test_multiple_animations_in_last_slide(self) -> None:
@assert_renders
class _(CESlide):
"""Check against solution for issue #161."""
@assert_renders
class TestFileTooLong(Slide):
"""Check against solution for issue #123."""
def construct(self) -> None:
circle = Circle(color=BLUE)
dot = Dot()
def construct(self) -> None:
circle = Circle(radius=3, color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle), run_time=0.1)
for _ in range(30):
direction = (random.random() - 0.5) * LEFT + (
random.random() - 0.5
) * UP
self.play(dot.animate.move_to(direction), run_time=0.1)
self.play(dot.animate.move_to(ORIGIN), run_time=0.1)
@assert_constructs
class TestLoop(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
assert not self._base_slide_config.loop
self.next_slide(loop=True)
self.play(text.animate.scale(2))
assert self._base_slide_config.loop
self.next_slide(loop=False)
assert not self._base_slide_config.loop
@assert_constructs
class TestAutoNext(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
assert not self._base_slide_config.auto_next
self.next_slide(auto_next=True)
self.play(text.animate.scale(2))
assert self._base_slide_config.auto_next
self.next_slide(auto_next=False)
assert not self._base_slide_config.auto_next
@assert_constructs
class TestLoopAndAutoNextFails(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
self.next_slide(loop=True, auto_next=True)
self.play(text.animate.scale(2))
with pytest.raises(ValidationError):
self.play(GrowFromCenter(circle))
self.play(FadeIn(dot))
self.next_slide()
@assert_constructs
class TestPlaybackRate(Slide):
def construct(self) -> None:
text = Text("Some text")
self.play(dot.animate.move_to(RIGHT))
self.play(dot.animate.move_to(UP))
self.play(dot.animate.move_to(LEFT))
self.play(dot.animate.move_to(DOWN))
self.add(text)
def test_file_too_long(self) -> None:
@assert_renders
class _(CESlide):
"""Check against solution for issue #123."""
assert self._base_slide_config.playback_rate == 1.0
def construct(self) -> None:
circle = Circle(radius=3, color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle), run_time=0.1)
self.next_slide(playback_rate=2.0)
self.play(text.animate.scale(2))
for _ in range(30):
direction = (random.random() - 0.5) * LEFT + (
random.random() - 0.5
) * UP
self.play(dot.animate.move_to(direction), run_time=0.1)
self.play(dot.animate.move_to(ORIGIN), run_time=0.1)
assert self._base_slide_config.playback_rate == 2.0
def test_loop(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
@assert_constructs
class TestReversedPlaybackRate(Slide):
def construct(self) -> None:
text = Text("Some text")
self.add(text)
self.add(text)
assert not self._base_slide_config.loop
assert self._base_slide_config.reversed_playback_rate == 1.0
self.next_slide(loop=True)
self.play(text.animate.scale(2))
self.next_slide(reversed_playback_rate=2.0)
self.play(text.animate.scale(2))
assert self._base_slide_config.loop
assert self._base_slide_config.reversed_playback_rate == 2.0
self.next_slide(loop=False)
@assert_constructs
class TestNotes(Slide):
def construct(self) -> None:
text = Text("Some text")
assert not self._base_slide_config.loop
self.add(text)
def test_auto_next(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
assert self._base_slide_config.notes == ""
self.add(text)
self.next_slide(notes="test")
self.play(text.animate.scale(2))
assert not self._base_slide_config.auto_next
assert self._base_slide_config.notes == "test"
self.next_slide(auto_next=True)
self.play(text.animate.scale(2))
@assert_constructs
class TestWipe(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
assert self._base_slide_config.auto_next
self.add(text)
self.next_slide(auto_next=False)
assert text in self.mobjects
assert bye not in self.mobjects
assert not self._base_slide_config.auto_next
self.wipe([text], [bye])
def test_loop_and_auto_next_succeeds(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
assert text not in self.mobjects
assert bye in self.mobjects
self.add(text)
@assert_constructs
class TestZoom(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.next_slide(loop=True, auto_next=True)
self.play(text.animate.scale(2))
self.add(text)
self.next_slide()
assert text in self.mobjects
assert bye not in self.mobjects
def test_playback_rate(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
self.zoom([text], [bye])
self.add(text)
assert text not in self.mobjects
assert bye in self.mobjects
assert self._base_slide_config.playback_rate == 1.0
@assert_constructs
class TestPlay(Slide):
def construct(self) -> None:
assert self._current_animation == 0
circle = Circle(color=BLUE)
dot = Dot()
self.next_slide(playback_rate=2.0)
self.play(text.animate.scale(2))
self.play(GrowFromCenter(circle))
assert self._current_animation == 1
self.play(FadeIn(dot))
assert self._current_animation == 2
assert self._base_slide_config.playback_rate == 2.0
@assert_constructs
class TestWaitTimeBetweenSlides(Slide):
def construct(self) -> None:
self._wait_time_between_slides = 1.0
assert self._current_animation == 0
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert self._current_animation == 1
self.next_slide()
assert self._current_animation == 2 # self.wait = +1
def test_reversed_playback_rate(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
@assert_constructs
class TestNextSlide(Slide):
def construct(self) -> None:
assert self._current_slide == 1
self.next_slide()
assert self._current_slide == 1
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
self.next_slide()
assert self._current_slide == 2
self.next_slide()
assert self._current_slide == 2
self.add(text)
@assert_constructs
class TestCanvas(Slide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
assert self._base_slide_config.reversed_playback_rate == 1.0
assert len(self.canvas) == 0
self.next_slide(reversed_playback_rate=2.0)
self.play(text.animate.scale(2))
self.add(text)
assert self._base_slide_config.reversed_playback_rate == 2.0
assert len(self.canvas) == 0
def test_notes(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
self.add_to_canvas(text=text)
self.add(text)
assert len(self.canvas) == 1
assert self._base_slide_config.notes == ""
self.add(bye)
self.next_slide(notes="test")
self.play(text.animate.scale(2))
assert len(self.canvas) == 1
assert self._base_slide_config.notes == "test"
assert text not in self.mobjects_without_canvas
assert bye in self.mobjects_without_canvas
def test_wipe(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
self.remove(text)
self.add(text)
assert len(self.canvas) == 1
assert text in self.mobjects
assert bye not in self.mobjects
self.add_to_canvas(bye=bye)
self.wipe([text], [bye])
assert len(self.canvas) == 2
assert text not in self.mobjects
assert bye in self.mobjects
self.remove_from_canvas("text", "bye")
def test_zoom(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
assert len(self.canvas) == 0
self.add(text)
with pytest.raises(KeyError):
self.remove_from_canvas("text")
assert text in self.mobjects
assert bye not in self.mobjects
self.zoom([text], [bye])
assert text not in self.mobjects
assert bye in self.mobjects
def test_animation_count(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
assert self._current_animation == 0
circle = Circle(color=BLUE)
dot = Dot()
self.play(GrowFromCenter(circle))
assert self._current_animation == 1
self.play(FadeIn(dot))
assert self._current_animation == 2
def test_wait_time_between_slides(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
self._wait_time_between_slides = 1.0
assert self._current_animation == 0
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
assert self._current_animation == 1
self.next_slide()
assert self._current_animation == 2 # self.wait = +1
def test_next_slide(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
assert self._current_slide == 1
self.next_slide()
assert self._current_slide == 1
circle = Circle(color=BLUE)
self.play(GrowFromCenter(circle))
self.next_slide()
assert self._current_slide == 2
self.next_slide()
assert self._current_slide == 2
def test_canvas(self) -> None:
@assert_constructs
class _(CESlide):
def construct(self) -> None:
text = Text("Some text")
bye = Text("Bye")
assert len(self.canvas) == 0
self.add(text)
assert len(self.canvas) == 0
self.add_to_canvas(text=text)
assert len(self.canvas) == 1
self.add(bye)
assert len(self.canvas) == 1
assert text not in self.mobjects_without_canvas
assert bye in self.mobjects_without_canvas
self.remove(text)
assert len(self.canvas) == 1
self.add_to_canvas(bye=bye)
assert len(self.canvas) == 2
self.remove_from_canvas("text", "bye")
assert len(self.canvas) == 0
with pytest.raises(KeyError):
self.remove_from_canvas("text")

View File

@ -69,7 +69,7 @@ class TestWizard:
monkeypatch.setattr(QMessageBox, "exec", exec_patched)
for i, (key, _) in enumerate(wizard.config.keys.dict().items()):
for i, (key, _) in enumerate(wizard.config.keys.model_dump().items()):
open_dialog(i, getattr(wizard.config.keys, key))
wizard.button_box.accepted.emit()
@ -89,7 +89,7 @@ def test_init() -> None:
assert results.exit_code == 0
assert CONFIG_PATH.exists()
assert Config().dict() == Config.from_file(CONFIG_PATH).dict()
assert Config().model_dump() == Config.from_file(CONFIG_PATH).model_dump()
def test_init_custom_path() -> None:
@ -106,7 +106,7 @@ def test_init_custom_path() -> None:
assert results.exit_code == 0
assert not CONFIG_PATH.exists()
assert custom_path.exists()
assert Config().dict() == Config.from_file(custom_path).dict()
assert Config().model_dump() == Config.from_file(custom_path).model_dump()
def test_init_path_exists() -> None:
@ -120,7 +120,7 @@ def test_init_path_exists() -> None:
assert results.exit_code == 0
assert CONFIG_PATH.exists()
assert Config().dict() == Config.from_file(CONFIG_PATH).dict()
assert Config().model_dump() == Config.from_file(CONFIG_PATH).model_dump()
results = runner.invoke(init, input="o")
@ -156,7 +156,7 @@ def test_wizard(monkeypatch: MonkeyPatch) -> None:
assert results.exit_code == 0
assert CONFIG_PATH.exists()
assert Config().dict() == Config.from_file(CONFIG_PATH).dict()
assert Config().model_dump() == Config.from_file(CONFIG_PATH).model_dump()
def test_wizard_closed_without_saving(monkeypatch: MonkeyPatch) -> None:

4117
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff